WQS二分(Alien Trick)

定义

如果有一个 d p dp dp问题,有一个维度是选择的个数,并且限制个数恰等于 m m m,由于这个维度的加入,暴力 d p dp dp的复杂度难以接受,即使转移是 O ( 1 ) O(1) O(1)的,往往状态数 O ( n m ) O(nm) O(nm)就已经爆了。

如果我们不考虑 m m m这个维度,剩下的往往就是一个线性 d p dp dp,可以在 O ( n ) O(n) O(n)或 O ( n log ⁡ n ) O(n\log n) O(nlogn)复杂度内解决。那么核心问题就是,怎么加速 m m m这个维度的处理?

如果 d p dp dp结果,也就是最小总代价,关于选择的个数 x x x的函数 g ( x ) g(x) g(x),是个凸函数,那么我们可以规定,每一次选择有个额外代价Δ,然后二分,在最优情况下,使得选择个数不小于 m m m的最大Δ, c h e c k check check里不用考虑恰好选 m m m个的约束了,直接线性 d p dp dp计算选任意个的最小代价,以及取到最小代价时,选了几个。

这个二分Δ的过程就是 W Q S WQS WQS(王钦石)二分(国外也叫Alien Trick,因为最早在一道叫Alien的题目被引入),最后我们找到了最大Δ,在这个Δ下进行 c h e c k check check里的,不考虑个数约束的 d p dp dp,计算出的就是恰好选 m m m个的最小代价。这样我们就在只增加了一个二分Δ的 log ⁡ \log log复杂度的条件下,解决了恰好选 m m m个这个约束。(实际上算出来的是考虑Δ的,我们选了 m m m个,减掉 m ∗ Δ m*Δ m∗Δ才是最终答案)

原理

为什么是对的?有两种解释

形象的解释

比较形象的解释是,选 m m m个东西看成消费者在市场上买东西,使得买的东西总价格最小,这是一个优化问题,可以 d p dp dp。然后政府给这个商品加税,每件商品固定加税Δ。消费者的目标仍然是使得总价格最小,那么考虑到税Δ,显然会影响到消费者买的东西个数,Δ越大,买的东西越少。

如果政府想限制消费者恰好只买 m m m个东西,可以通过加税来控制消费者的行为,或者说,让消费者的最优决策点移动(在 g ( x ) g(x) g(x)这个函数上移动, x x x是购买物品个数, g ( x ) g(x) g(x)是最小代价,这个函数上是不同Δ下的最优决策点)。

政府也就是 w q s wqs wqs二分,只用考虑如何让消费者恰好买 m m m个东西,消费者,也就是 c h e c k check check里的 d p dp dp,只用在给定Δ下,不限制购买格式,考虑如何最小化代价。最后通过政府,消费者两级优化,能得到恰好买 m m m个东西的最小代价。

严谨的分析

规定每个东西有额外代价Δ,等价于把优化目标改成最小化 g ( x ) + x ∗ Δ g(x)+x*Δ g(x)+x∗Δ,当Δ,变化,显然最小值点 x x x也会变化,求导可得最小值点就是 g ′ ( x ) = − Δ g^{'}(x)=-Δ g′(x)=−Δ的点。(这里也可以看成一个直线 y = − Δ ∗ x y=-Δ*x y=−Δ∗x,求和 g ( x ) g(x) g(x)的切点)

那么如果 g ( x ) g(x) g(x)是凸函数,随着Δ单调变化,最小值点也是单调变化的。所以我们可以通过二分Δ,来使得最小值点,来确定使得最小值点,也就是选择个数,恰为 m m m的Δ。

接下来对于这个Δ,跑 d p dp dp,最优解必然是恰好选择 m m m个的,这就满足了我们的要求。

这东西应该属于凸优化里的拉格朗日法,只是被引入到算法竞赛里了。

例题

太干燥了,看个具体的例子

P6246 [IOI 2000] 邮局 加强版 加强版

这是我在四边形不等式优化dp这篇文章里引用过的例题,当时利用决策单调性,可以实现最优解为 O ( n m ) O(nm) O(nm),或者说 O ( P V ) O(PV) O(PV)。这个复杂度对于加强后的本题 n , m = 5 e 5 n,m=5e5 n,m=5e5是过不去的。

对于 n n n个房子这个维度,可以用决策单调性优化到 O ( n log ⁡ n ) O(n\log n) O(nlogn)甚至 O ( n ) O(n) O(n),问题在于还有恰好建 m m m个邮局这个维度。

这个约束看起来就像是 W Q S WQS WQS二分擅长的,现在需要证明 g ( x ) g(x) g(x)是凸函数,就可以套 W Q S WQS WQS了。凸性先考虑定性证明,随着邮局个数增加,显然总的距离和是减小的,并且随着邮局个数越来越多,每新建一个邮局,距离和的减小量会越来越小,也就是边际效益递减,这样画出函数图像类似于一个开口向上的二次函数的左半边,单减,但是减小的1越来越慢。显然是凸函数。

那么我们可以外层套 W Q S WQS WQS二分,解决恰好 m m m个邮局这个约束,复杂度 O ( log ⁡ ∑ x i ) O(\log \sum x_i) O(log∑xi),这复杂度是因为二分的值Δ可以看成,去切 g ( x ) g(x) g(x)的切线斜率,所以Δ值域应该就是 g ( x ) g(x) g(x)斜率的值域,最小 0 0 0,由于 g ( x ) g(x) g(x)实际上是多个散点组成的,斜率等价于差分,最大斜率就是最大的差分 g ( 1 ) − g ( 0 ) g(1)-g(0) g(1)−g(0),从选 0 0 0个到选 1 1 1个,这不会超过把唯一的邮局建在 0 0 0的代价( ∑ x i \sum x_i ∑xi),故 ∑ x i \sum x_i ∑xi是个很宽的上界。

内层 c h e c k check check里利用决策单调性进行 d p dp dp,求最小代价。内层就是一个只有一层的线性 d p dp dp,无法分治或者记录决策点,只能二分队列,复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、

总复杂度 O ( n log ⁡ n log ⁡ ∑ x i ) O(n\log n\log \sum x_i) O(nlognlog∑xi),可以通过 n , m = 5 e 5 n,m=5e5 n,m=5e5的数据

c 复制代码
int a[N], s[N], n, m;
int f[N], g[N];
inline ll w(int i, int j) {
	if (i > j) return 0;
	int mid = (i + j) >> 1;
	return (a[mid] * (mid - i) - (s[mid - 1] - s[i - 1])) +
	       ((s[j] - s[mid]) - a[mid] * (j - mid));
}

inline ll calc(int delta, int k, int j) {
	return f[k] + w(k + 1, j) + delta;
}

int find_pos(int delta, int x, int y, int l, int r) {
	while (l <= r) {
		int mid = (l + r) / 2;
		if (calc(delta, x, mid) <= calc(delta, y, mid)) {
			r = mid - 1;
		} else {
			l = mid + 1;
		}
	}
	return l;
}

struct node {
	int k, l, r;
} q[N];


bool check(int delta) {
	int head = 1, tail = 1;
	q[1] = {0, 1, n};
	f[0] = 0;
	g[0] = 0;

	rep(j, 1, n) {
		while (head < tail && q[head].r < j) {
			head++;
		}
		f[j] = calc(delta, q[head].k, j);
		g[j] = g[q[head].k] + 1;

		int pos = -1;
		while (head <= tail) {
			if (calc(delta, j, q[tail].l) <= calc(delta, q[tail].k, q[tail].l)) {
				pos = q[tail].l;
				tail--;
			} else {
				if (calc(delta, j, q[tail].r) <= calc(delta, q[tail].k, q[tail].r)) {
					pos = find_pos(delta, j, q[tail].k, q[tail].l, q[tail].r);
					q[tail].r = pos - 1;
				}
				break;
			}
		}

		if (pos != -1) {
			q[++tail] = {j, pos, n};
		}
	}
	return g[n] >= m;
}
void solve() {
	cin >> n >> m;

	rep(i, 1, n) {
		cin >> a[i];
		s[i] = s[i - 1] + a[i];
	}

	int l = 0, r = s[n], ans = 0;
	while (l <= r) {
		int mid = (l + r) / 2;
		if (check(mid)) {
			l = mid + 1;
			ans = mid;
		} else {
			r = mid - 1;
		}
	}

	check(ans);
	cout << f[n] - ans*m;
}
相关推荐
xiaoye-duck1 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--6.计算布尔二叉树的值,7.求根节点到叶节点数字之和
c++·算法·深度优先·递归
greatofdream1 小时前
VIP和普通用户排队
算法
abant22 小时前
leetcode 84 单调栈
算法·leetcode·职场和发展
liuyao_xianhui2 小时前
递归_反转链表_C++
java·开发语言·数据结构·c++·算法·链表·动态规划
CoderCodingNo2 小时前
【GESP】C++七级考试大纲知识点梳理 (3) 图论基础与遍历算法
c++·算法·图论
深蓝轨迹2 小时前
LeetCode105. 从前序与中序遍历序列构造二叉树
数据结构·算法
TracyCoder1232 小时前
LeetCode Hot100(63/100)——31. 下一个排列
数据结构·算法·leetcode
智者知已应修善业2 小时前
【不用第三变量交换2个数】2024-10-18
c语言·数据结构·c++·经验分享·笔记·算法
会编程的土豆2 小时前
c语言时间戳从入门到精通
linux·c语言·算法