四边形不等式学习笔记
1.定义
1.1 四边形不等式
四边形不等式指的是二元函数 \(w(l,r)\) 对于 \(l_1 \le l_2 \le r_1 \le r_2\) 满足:
也就是交叉优于包含。
四边形不等式的等价形式是:
常见的满足四边形不等式的函数包括几类:
1.1.1. 线性类
\(w(l,r) = f(l) - g(r)\)。
显然不等号会取等。所以其满足四边形恒等式。
1.1.2. 凸函数
\(w(l,r) = f(a_r - a_l)\),其中 \(a_1 < a_2 < \dots < a_n\) 且 \(f\) 是凸函数(也就是二阶差分非负)。
常见的比如 \(f(x) = x^p(p > 1)\) 和 \(f(x) = -x^p(0 < p < 1)\)。
证明不难,我们其实要证明的就是对于任意非负整数 \(a,b,c\) 有 \(f(b+c) + f(a + b) \le f(b) + f(a+b+c)\)。
移项得到 \(f(a + b) - f(b) \le f(a+b+c) - f(b+c)\)。而由于相当于比较在不同的点经过 \(a\) 的变化。
考虑到二阶差分非负,所以前者一定小于等于后者,从而得证。
1.1.3 函数相加
\(w_1, w_2\) 满足四边形不等式,则对于任意非负 \(c_1,c_2\),\(c_1w_1 + c_2w_2\) 也满足四边形不等式。
证明显然。
1.1.4 二元函数
\(w(l,r) = (a_r - a_l)(b_r - b_l)\) 且 \(a,b\) 均递增,则 \(w\) 满足四边形不等式。
证明也不难,只考虑所有 \(l,r\) 乘起来的项即可。
1.1.5 二次前缀和
\(w(l,r) = \sum_{i=l}^r\sum_{j=l}^rf(i,j)\),其中 \(f\) 只与 \(i,j\) 有关。
\(w(l_1,r_2) + w(l_2,r_1)\) 相当于是整个正方形加上中间的小正方形,\(w(l_1, r_1) + w(l_2,r_2)\) 相当于是整个正方形减去一部分加上小正方形,显然优于包含的情况,所以有决策单调性。
1.1.6 斜率优化类
\(w(l,r) = -a_lb_r\) 且 \(a,b\) 递增时,也满足四边形不等式。
我们相当于要证明 \(a_{l1}b_{r1} + a_{l_2}b_{r_2} \ge a_{l1}b_{r2} + a_{l_1}b_{r_2}\),这就是很经典的大的乘大的,小的乘小的,显然成立。
所以斜率优化的题很多也可以用四边形不等式。
1.2 单调性
如果函数 \(w(l,r)\) 满足:
则其满足区间包含单调性。
对于一个动态规划来说,我们记录其最优决策点集合中最小的元素为 \(opt\),下面记为最优决策点(注意一定要取最小的或者最大的)。
我们假设下面的函数计算都是 \(O(1)\) 的,且都满足四边形不等式。
2. 离线版本
2.1 内容
考虑转移方程:
直接暴力是 \(O(n^2)\),我们考虑如何在 \(O(n \log n)\) 内计算。
我们定义 \(opt(i)\) 是 \(f_i\) 的最优决策点。我们可以证明若 \(i \le i'\),则 \(opt(i) \le opt(i')\),也就是其具有决策单调性。
证明不难,oi-wiki 上有。
所以我们现在考虑分治计算:取中点 \(m\),我们先算出 \(f(m),opt(m)\),然后递归计算两边的值。考虑到两边的 \(opt\) 集合被一分为二,所以每一层的总计算量都是 \(O(n)\) 的,总共 \(\log n\) 层,时间复杂度 \(O(n \log n)\)。
模板题 P3515 [POI2011] Lightning Conductor。
这道题转化一下就是 \(w(l,r) = a_r- a_l - \sqrt{|r - l|}\)。
这个函数的前半部分属于四边形不等式的函数类的第一种,后半部分属于第二种,所以这个函数满足四边形不等式。
然后我们就可以用分治求解了:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5e5 + 5;
int n, h[N] = {0};
double w(int j, int i) {
return h[j] - h[i] + sqrt(i - j);
}
void slv(int *f, int l, int r, int pl, int pr) {
if (l > r)
return;
int mid = (l + r) / 2, p = pl;
for (int i = pl + 1; i <= min(mid - 1, pr); i++)
if (w(i, mid) > w(p, mid))
p = i;
f[mid] = max(f[mid], (int)ceil(w(p, mid)));
slv(f, l, mid - 1, pl, p);
slv(f, mid + 1, r, p, pr);
}
int f[N] = {0};
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &h[i]);
slv(f, 1, n, 1, n);
reverse(h + 1, h + n + 1);
reverse(f + 1, f + n + 1);
slv(f, 1, n, 1, n);
for (int i = 1; i <= n; i++)
printf("%d\n", f[n - i + 1]);
return 0;
}
2.2 应用
P1912 [NOI2009] 诗人小G
\(w(l,r) = |s_r - s_l - 1 - L|^P\) 属于四边形不等式函数类的第二种。
6932 [ICPC2017 WF] Money for Nothing
我们显然就是要求 \(\max_{i,j}w(i,j)\),其中 \(w(i,j) = (q_i - p_j)(e_i - d_j)\)。为了用四边形不等式,我们取个反变成最小值。并且要求 \(e_i \ge d_j\)。
我们可以对应到一个二维平面上,选两个点组成的长方形面积最大。
这个问题有一个观察:有一些点肯定不优。我们删去被完爆的点后发现 \(m\) 个点的 \(d\) 单调递增,\(p\) 单调递减。\(n\) 个点的 \(e\) 单调递增,\(p\) 单调递减。
现在我们就可以证明其满足决策单调性了,所以我们用分治求解即可。
3. 1D-1D 在线版本
求:
考虑到 \(f(j) + w(j,i)\) 显然也是满足四边形不等式的,所以决策单调性依然存在,但是由于我们依赖前面的结果,所以我们不能用分治来求解了。
这里我们用的方法是二分队列。
我们考虑决策点的值。
刚开始所有都是 0,然后我们加入 1 后一段后缀会变成 1,加入 2 后一段比之前后缀会变成 2,以此类推。
我们用队列存储 \((l,r,p)\) 表示当前 \([l,r]\) 的最优决策点都是 \(p\),一开始只有 \((1,n,0)\),我们假设现在计算 \(i\)。
首先,我们计算 \(f(i)\),显然其最优决策点就是现在的队首。
计算完之后,我们先判断队首是否为空,如果为空就弹出。
然后我们开始和队尾比较,每次看 \(i\) 是否优于这一段的最优决策点,如果是,则弹出。最后我们有三种情况:
-
队列为空,则加入 \((i+1,n,i)\) 即可。
-
最后一段的 \(r\) 的最优决策点是 \(i\),这说明这一段的前半部分是 \(p\),后半部分是 \(i\),二分即可。
-
否则将剩下的后缀加入队列,最优决策点是 \(i\)。
时间复杂度 \(O(n \log n)\)。
\(w(l,r) = (s_r - s_l + r - l - 1 - L)^2\),显然满足四边形不等式。
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int N = 5e4 + 5;
struct Node {
int l, r, p;
Node (int _l = 0, int _r = 0, int _p = 0) :
l(_l), r(_r), p(_p) {}
} q[N];
int fr, ba;
int n, L;
long long s[N] = {0};
long long f[N] = {0};
long long F(int j, int i) {
return f[j] + (i - j + s[i] - s[j] - L - 1) * (i - j + s[i] - s[j] - L - 1);
}
int main() {
cin >> n >> L;
for (int i = 1, x; i <= n; i++) {
cin >> x;
s[i] = s[i - 1] + x;
}
fr = 1, ba = 0;
q[++ba] = Node(1, n, 0);
for (int i = 1; i <= n; i++) {
f[i] = F(q[fr].p, i);
if (++q[fr].l > q[fr].r)
fr++;
while (fr <= ba && F(i, q[ba].l) <= F(q[ba].p, q[ba].l))
ba--;
if (fr > ba)
q[++ba] = Node(i + 1, n, i);
else if (F(i, q[ba].r) <= F(q[ba].p, q[ba].r)) {
int l = q[ba].l - 1, r = q[ba].r;
while (l + 1 < r) {
int mid = (l + r) / 2;
if (F(i, mid) <= F(q[ba].p, mid))
r = mid;
else
l = mid;
}
q[ba].r = r - 1;
q[++ba] = Node(r, n, i);
}
else if (q[ba].r != n)
q[ba + 1] = Node(q[ba].r + 1, n, i), ba++;
}
cout << f[n] << endl;
return 0;
}
4. 1D-2D 分组问题
4.1 内容
求:
也就是在上个问题的情况下,多了要求分多少组的限制。这里要求 \(w\) 同时满足区间包含单调性。
第一种方法,我们按照 \(j\) 从小到大处理,每层都是一个 1D-1D 问题,时间复杂度 \(O(mn \log n)\)
还有第二种方法。
不妨设 \(opt(i,j)\) 是最优决策点,则我们可以证明:
所以我们考虑正序枚举 \(i\),倒序枚举 \(j\),由上面的式子得出 \(opt(i - 1, j) \le opt(i,j) \le opt(i, j + 1)\),从而实现 \(O(n^2)\) 的计算。
第二种方法:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int N = 3005;
const int M = 305;
int n, m;
int a[N] = {0};
long long w[N][N] = {{0}};
long long f[N][M] = {{0}};
int p[N][M] = {{0}};
long long F(int i, int k, int j) {
return f[k][j - 1] + w[k + 1][i];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++) {
int l = i, r = i, op = 0;
long long val = 0ll;
while (1 <= l && r <= n) {
w[l][r] = val;
if (op)
l--, val += a[i] - a[l];
else
r++, val += a[r] - a[i];
op = 1 - op;
}
}
memset(f, 0x3f, sizeof f);
f[0][0] = 0ll;
for (int i = 1; i <= n; i++) {
p[i][m + 1] = i - 1;
for (int j = m; j >= 1; j--) {
p[i][j] = i - 1;
for (int k = p[i - 1][j]; k <= p[i][j + 1]; k++) {
if (f[i][j] > F(i, k, j))
f[i][j] = F(i, k, j), p[i][j] = k;
}
}
}
cout << f[n][m] << endl;
return 0;
}
4.2 配合指针移动
在一些问题中,\(w(l,r)\) 难以计算,这时候我们可以在分治的时候利用莫队的技巧同时计算 \(w\),可以证明其时间复杂度是 \(O(n \log n)\) 的。
具体证明不妨考虑 \(f(n)\) 表示时间复杂度,则我们首先会将 \(l\) 从 \(1 \to n\) 遍历,然后进入左边,不妨设左边长度为 \(m\),则我们的 \(r\) 需要移动 \(n-m\) 的距离,然后处理完左边再回来就是再移动 \(n-m\),然后处理左边。
所以时间复杂度就是 \(f(n) = O(n) + f(n-m) + f(m) = O(n \log n)\) 的。
4.3 应用
GYM103536A Guards
典型的分组问题,\(w(l,r) = (r-l)(s_r - s_l)\),满足四边形不等式。
由于第二种方法不能滚动数组,所以我们只能用第一种方法分治计算,时间复杂度 \(O(mn \log n)\)。
CF321E Ciel and Gondolas
\(w(l,r) = \sum_{i=l}^r\sum_{j=i+1}^ru_{i,j}\),我们将其视作一个二维前缀和。
这就属于二次前缀和的情况,显然满足四边形不等式。
所以我们可以用四边形不等式求解,两种方法都可以,但是这题卡常,要快读。
CF643C Levels and Regions
首先有引理:如果一件事有 \(p\) 的概率成功,则我们期望做 \(\frac{1}{p}\) 次成功。所以我们可以预处理 \(s_i = \sum_{j=1}^i t_j\),\(p_i = \sum_{j=1}^i \frac{1}{t_j}\) 和 \(h_i = \sum_{j=1}^i\frac{s_j}{t_j}\)。
\(w(l,r)\) 的有效部分是 \(-s_l \times p_r\),由于两者都有单调性,属于斜率优化类,可以用四边形不等式。
CF838B The Bakery
首先将贡献取反。
我们将四边形不等式对应到两个差值的比较,\(w(l_1,r_2) - w(l_1,r_1)\) 和 \(w(l_2, r_2) - w(l_2, r_1)\),显然前者不会多于后者,因为前者新增的后者一定新增,前者没新增的后者有可能新增,所以其满足四边形不等式。
还有一种方法,既然四边形不等式可以相加,对于单个元素的贡献显然满足四边形不等式,所以加起来也满足。
这就是 \(w(l,r)\) 难以计算的问题,我们利用指针配合分治可以将其做到 \(O(kn\log n)\),非常优秀。
CF868F Yet Another Minimization Problem
和上道题类似。证明可以考虑单个元素的情况,考虑到 \(\binom{n}{2}\) 是凸函数,相当于若干个凸函数的和,显然是满足四边形不等式的。
CF1527E Partition Game
考虑单个元素的贡献不难发现属于线性类的,所以其满足四边形不等式。
有两种方法,一种可以用循环链表维护每个元素的所有出现位置,移动的时候插入或删除即可,也可以直接指针暴力跳,均摊也是相同时间复杂度。
5. 2D-2D 区间合并
求:
要求 \(w\) 还需满足区间包含单调性。
我们同样可以证明:
于是我们按照长度从小到大推,总时间复杂度 \(O(n^2)\)。
模板题石子合并的一排的版本,显然其满足四边形不等式和区间包含单调性,所以:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int n, a[N] = {0};
int opt[N][N] = {{0}};
int dp[N][N] = {{0}};
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i], a[i] += a[i - 1];
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++)
dp[i][i] = 0;
for (int i = 1; i <= n; i++)
opt[i][i + 1] = i, dp[i][i + 1] = a[i + 1] - a[i - 1];
for (int len = 3; len <= n; len++) {
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
for (int k = opt[i][j - 1]; k <= opt[i + 1][j]; k++)
if (dp[i][k] + dp[k + 1][j] + (a[j] - a[i - 1]) < dp[i][j])
dp[i][j] = dp[i][k] + dp[k + 1][j] + (a[j] - a[i - 1]), opt[i][j] = k;
}
}
cout << dp[1][n] << endl;
return 0;
}