四边形不等式学习笔记

1.定义

1.1 四边形不等式

四边形不等式指的是二元函数 \(w(l,r)\) 对于 \(l_1 \le l_2 \le r_1 \le r_2\) 满足:

\[w(l_1, r_1) + w(l_2, r_2) \le w(l_2, r_1) + w(l_1, r_1) \]

也就是交叉优于包含。

四边形不等式的等价形式是:

\[w(l, r - 1) + w(l + 1, r) \le w(l, r) + w(l + 1, r - 1) \]

常见的满足四边形不等式的函数包括几类:

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)\) 满足:

\[\forall l' \le l \le r \le r', w(l,r) \le w(l', r') \]

则其满足区间包含单调性。

对于一个动态规划来说,我们记录其最优决策点集合中最小的元素为 \(opt\),下面记为最优决策点(注意一定要取最小的或者最大的)。

我们假设下面的函数计算都是 \(O(1)\) 的,且都满足四边形不等式。

2. 离线版本

2.1 内容

考虑转移方程:

\[f(i) = \min_{1 \le j < i}\{w(j,i)\} \]

直接暴力是 \(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(i) = \min_{1 \le j < i}\{f(j) + w(j,i)\} \]

考虑到 \(f(j) + w(j,i)\) 显然也是满足四边形不等式的,所以决策单调性依然存在,但是由于我们依赖前面的结果,所以我们不能用分治来求解了。

这里我们用的方法是二分队列。

我们考虑决策点的值。

刚开始所有都是 0,然后我们加入 1 后一段后缀会变成 1,加入 2 后一段比之前后缀会变成 2,以此类推。

我们用队列存储 \((l,r,p)\) 表示当前 \([l,r]\) 的最优决策点都是 \(p\),一开始只有 \((1,n,0)\),我们假设现在计算 \(i\)

首先,我们计算 \(f(i)\),显然其最优决策点就是现在的队首。

计算完之后,我们先判断队首是否为空,如果为空就弹出。

然后我们开始和队尾比较,每次看 \(i\) 是否优于这一段的最优决策点,如果是,则弹出。最后我们有三种情况:

  1. 队列为空,则加入 \((i+1,n,i)\) 即可。

  2. 最后一段的 \(r\) 的最优决策点是 \(i\),这说明这一段的前半部分是 \(p\),后半部分是 \(i\),二分即可。

  3. 否则将剩下的后缀加入队列,最优决策点是 \(i\)

时间复杂度 \(O(n \log n)\)

模板题 P3195 [HNOI2008] 玩具装箱

\(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 内容

求:

\[f(i,j) = \min_{1 \le k \le i}\{f(k,j-1) + w(k,i)\} \]

也就是在上个问题的情况下,多了要求分多少组的限制。这里要求 \(w\) 同时满足区间包含单调性。

第一种方法,我们按照 \(j\) 从小到大处理,每层都是一个 1D-1D 问题,时间复杂度 \(O(mn \log n)\)

还有第二种方法。

不妨设 \(opt(i,j)\) 是最优决策点,则我们可以证明:

\[opt(i,j-1) \le opt(i,j) \le opt(i+1,j) \]

所以我们考虑正序枚举 \(i\),倒序枚举 \(j\),由上面的式子得出 \(opt(i - 1, j) \le opt(i,j) \le opt(i, j + 1)\),从而实现 \(O(n^2)\) 的计算。

模板题 P4767 [IOI2000] 邮局 加强版

第二种方法:

#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 区间合并

求:

\[f(l,r) = \min_{l \le k < r}\{f(l,k) + f(k + 1, r)\} + w(l,r) \]

要求 \(w\) 还需满足区间包含单调性。

我们同样可以证明:

\[opt(i,j-1) \le opt(i,j) \le opt(i+1,j) \]

于是我们按照长度从小到大推,总时间复杂度 \(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;
} 
posted @ 2024-08-28 17:44  rlc202204  阅读(10)  评论(0编辑  收藏  举报