四边形不等式学习笔记

· · 算法·理论

定义

四边形不等式是针对一个二元函数 \operatorname{w}(x,y) 所定义,若有 \forall x_1\leq x_2\leq y_1\leq y_2,\operatorname{w}(x_1,y_1)+\operatorname{w}(x_2,y_2)\leq\operatorname{w}(x_1,y_2)+\operatorname{w}(x_2,y_1),则认为二元函数 \operatorname{w}(x,y) 满足四边形不等式。

同时有一个等价结论 \forall x\leq y,\operatorname{w}(x,y)+\operatorname{w}(x+1,y+1)\leq\operatorname{w}(x,y+1)+\operatorname{w}(x+1,y)

同时也可以注意到,如果 \operatorname{f}(x,y),\operatorname{g}(x,y) 均满足四边形不等式,则 \operatorname{h}(x,y)=\operatorname{f}(x,y)+\operatorname{g}(x,y) 同样满足四边形不等式。

同时定义决策单调性,\forall i<j,\operatorname{o}(i)\leq\operatorname{o}(j),其中 \operatorname{o}(i) 指位置 i 的最优决策点。

优化

假设已知 \operatorname{w}(x,y) 满足四边形不等式,现在要求出 \operatorname{f}(i)=\min\limits_{j<=i} \operatorname{w}(j,i)

现在证明由 \operatorname{w}(x,y) 推知决策单调性,设上述问题中最优决策点为 p_i,假设 \exist i<j,p_i>p_j,显然 p_j<p_i<i<j 那么有 \operatorname{w}(p_j,i)+\operatorname{w}(p_i,j)\leq\operatorname{w}(p_i,i)+\operatorname{w}(p_j,j)

又有 \operatorname{w}(p_i,i)\leq\operatorname{w}(p_j,i),\operatorname{w}(p_j,j)\leq\operatorname{w}(p_i,j) 故矛盾。

实现

考虑到不需要掌握所有有关的实现,这里只介绍最为实用的。

分治处理转移较为复杂的原因是 p_{mid} 的计算依赖于 [pl,mid) 的答案,放弃求出 p_{mid} 的正确性,只保证 [0,pl] 上的正确性,即不立即保证 p_{mid} 的全局最优性,而是通过递归逐步收敛。

记只考虑 [0,pl]x 的最优决策点为 p_{x,pl},注意到如果要继续递归,需要 p_{mid,pl},p_{x,mid},对于 p_{mid,pl} 可以递归后枚举 [p_{pl},p_{x,pl} 内的所有决策点,对于 p_{x,mid} 可以在 (pl,mid] 计算完之后暴力枚举这个区间算出,神秘地发现,这算出了正确的 f_ip_i

考虑分治处理假设当前处理的区间是 (pl,pr],而 [0,pl] 均已完成了正确的计算,而 pr 都已经算出了在 [0,pl] 中的疑似最优决策点,这个疑似最优决策点指的是仅考虑 [0,pl] 时的最优决策点。

有下述几个步骤

  1. 现在已知只考虑 [0,pl] 的时的 pr 的最优决策点,把 pl 的决策点到 pr疑似最优决策点之间的所有点转移到 mid,计算并更新 f_{mid}mid疑似最优决策点。 这里展开解释一下,显然满足了可以递归处理的前提,由于决策单调性,所以只考虑 f_{pl}f_{pr}疑似最优决策点是正确的。
  2. 递归处理 (pl,mid]
  3. 再将 [pl+1,mid] 的最优决策点转移到 pr,计算并更新 f_{pr}pr疑似最优决策点。
  4. 递归处理 (mid,pr]

该分治策略的正确性依赖于决策单调性所保证的决策区间包含关系,具体可以查看原文。

小结一下,在什么条件下使用四边形不等式优化。

何时使用四边形不等式优化?

  • DP 形如 f(i) = \min_{j < i} \{ f(j) + w(j+1, i) \}
  • 代价函数 w 满足四边形不等式。

前面介绍的部分主要是求最小值,求最大值也是同理,可以结合例题思考。

例题

「POI2011 R1」避雷针 Lightning Conductor

问题等价于求 \forall j,k_i\geq h_j-h_i+\sqrt{|i-j|}。 先拆绝对值,假设 j\leq i,可以写成以下形式 f_i=k_iw_{i,j}=h_j-h_i+\sqrt{i-j}

现在证明 $w_{i,j}$ 满足四边形不等式,设 $a\leq b\leq c\leq d$,$w_{a,c}+w_{b,d}-w_{a,d}-w_{b,c}=(\sqrt{c-a}+\sqrt{d-b})-(\sqrt{d-a}+\sqrt{c-b})$,学习过导数的同学都知道 $\operatorname{f}(x)=x^{\frac{1}{2}}$ 的导函数是 $\operatorname{f'}(x)=\frac{x^{-\frac{1}{2}}}{2}$,导函数单调递减,因此上述值始终不小于 $0$,可以判断出 $w_{x,y}$ 满足四边形不等式。 ::::info[参考代码] ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int, int> PII; const int N = 5e5 + 10; int n, p [ N ]; ld h [ N ], f [ N ], g [ N ]; double w ( int j, int i ) { if ( j == 0 ) return -1e18; return h [ j ] + sqrt ( i - j ) - h [ i ]; } void sol ( int pl, int pr ) { if ( pr == pl + 1 ) { if ( w ( pr, pr ) > f [ pr ] ) f [ pr ] = w ( pr, pr ), p [ pr ] = pr; return; } int mid = pl + pr >> 1; p [ mid ] = p [ pl ], f [ mid ] = w ( p [ mid ], mid ); for ( int i = p [ pl ] + 1; i <= p [ pr ]; ++i ) if ( w ( i, mid ) > f [ mid ] ) f [ mid ] = w ( i, mid ), p [ mid ] = i; sol ( pl, mid ); for ( int i = pl + 1; i <= mid; ++i ) if ( w ( i, pr ) > f [ pr ] ) f [ pr ] = w ( i, pr ), p [ pr ] = i; sol ( mid, pr ); } signed main ( ) { cin >> n; for ( int i = 1; i <= n; ++i ) cin >> h [ i ]; for ( int i = 1; i <= n; ++i ) p [ i ] = i; p [ 0 ] = 0; sol ( 0, n ); for ( int i = 1; i <= n; ++i ) g [ i ] = f [ i ]; for ( int i = 1; i <= n; ++i ) f [ i ] = 0, p [ i ] = i; p [ 0 ] = 0; reverse ( h + 1, h + 1 + n ); sol ( 0, n ); for ( int i = 1; i <= n; ++i ) g [ i ] = max ( g [ i ], f [ n - i + 1 ] ); for ( int i = 1; i <= n; ++i ) { ll ans = ( ll ) ceil ( g [ i ] - 1e-10 ); if ( ans < 0 ) ans = 0; cout << ans << '\n'; } return 0; } ``` :::: --- [CF321E Ciel and Gondolas](https://codeforces.com/contest/321/problem/E) 设 $\operatorname{w}(x,y)$ 为矩阵左上角为 $(x,x)$,右下角为 $(y,y)$ 的和。 因此对于 $a\leq b\leq c\leq d$,$\operatorname{w}(a,c)+\operatorname{w}(b,d)\leq\operatorname{w}(a,d)+\operatorname{w}(b,c)$。 画图更容易验证,因为后者比前者覆盖的面积更大,且前者是完全被后者所覆盖。 状态转移方程显然是 $f_{i,j}=f_{i-1,k-1}+\operatorname{w}(k,j)$。 ::::info[参考代码] ```cpp #include <bits/stdc++.h> using namespace std; namespace IO { const int BUF_SIZE = 1 << 20; char inbuf[BUF_SIZE], outbuf[BUF_SIZE]; char *p1 = inbuf, *p2 = inbuf; int outpos = 0; inline char getchar() { if (p1 == p2) { p1 = inbuf; p2 = inbuf + fread(inbuf, 1, BUF_SIZE, stdin); if (p1 == p2) return EOF; }return *p1++; } template <typename T> inline T read() { T x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch - '0'); ch = getchar(); } return x * f; } template <typename T> inline void write(T x, char en = '\n') { if (outpos >= BUF_SIZE - 32) { fwrite(outbuf, 1, outpos, stdout); outpos = 0;} if (x < 0) { outbuf[outpos++] = '-'; x = -x; } char temp[64]; int len = 0; do { temp[len++] = x % 10 + '0'; x /= 10; } while (x); while (len--) outbuf[outpos++] = temp[len]; outbuf[outpos++] = en; } inline void flush() {if (outpos) fwrite(outbuf, 1, outpos, stdout); outpos = 0;} }using namespace IO; const int N = 4010, K = 810; int n, k, x, sum [ N ][ N ]; int f [ K ][ N ], p [ K ][ N ]; int w ( int u, int v ) { return sum [ v ][ v ] - sum [ v ][ u - 1 ] - sum [ u - 1 ][ v ] + sum [ u - 1 ][ u - 1 ]; } void sol ( int s, int pl, int pr ) { if ( pr == pl + 1 ) { if ( w ( pl + 1, pr ) + f [ s - 1 ][ pl ] < f [ s ][ pr ] ) f [ s ][ pr ] = w ( pl + 1, pr ) + f [ s - 1 ][ pl ], p [ s ][ pr ] = pl + 1; return; } int mid = pl + pr >> 1; p [ s ][ mid ] = p [ s ][ pl ], f [ s ][ mid ] = f [ s - 1 ][ p [ s ][ mid ] - 1 ] + w ( p [ s ][ mid ], mid ); for ( int i = p [ s ][ pl ] + 1; i <= p [ s ][ pr ]; ++i ) if ( w ( i, mid ) + f [ s - 1 ][ i - 1 ] < f [ s ][ mid ] ) f [ s ][ mid ] = w ( i, mid ) + f [ s - 1 ][ i - 1 ], p [ s ][ mid ] = i; sol ( s, pl, mid ); for ( int i = pl + 1; i <= mid; ++i ) if ( w ( i, pr ) + f [ s - 1 ][ i - 1 ] < f [ s ][ pr ] ) f [ s ][ pr ] = w ( i, pr ) + f [ s - 1 ][ i - 1 ], p [ s ][ pr ] = i; sol ( s, mid, pr ); } signed main ( ) { n = read < int > ( ), k = read < int > ( ); for ( int i = 1; i <= n; ++i ) { for ( int j = 1; j <= n; ++j ) { sum [ i ][ j ] = sum [ i - 1 ][ j ] + sum [ i ][ j - 1 ] - sum [ i - 1 ][ j - 1 ] + read < int > ( ); } } for ( int i = 1; i <= n; ++i ) f [ 0 ][ i ] = 1e9; f [ 0 ][ 0 ] = 0; for ( int i = 1; i <= k; ++i ) { for ( int j = 1; j <= n; ++j ) f [ i ][ j ] = 1e9; p [ i ][ 1 ] = 1, p [ i ][ n ] = n; sol ( i, 0, n ); } write ( f [ k ][ n ] >> 1 ), flush ( ); return 0; } ``` :::: --- ::::info[参考资料] - [参考资料1](https://www.luogu.com.cn/article/vqf42hah) - [参考资料2](https://oi-wiki.org/dp/opt/quadrangle/) ::::