凸优化算法学习笔记:决策单调性与 wqs二分

前言

学习了凸性相关的算法。
有:决策单调性,wqs二分, 闵可夫斯基和, slope trick。
一般是用来优化dp。
文章写的太长了因此分成两篇发。

决策单调性

单调矩阵,完全单调矩阵,蒙日阵

  • 单调矩阵:每行最小值/最大值的位置单调的矩阵。
  • 完全单调矩阵:若一个矩阵 \(A\) 满足其所有子矩阵均为单调矩阵,那么称其为完全单调矩阵。

常见的决策单调性问题其实就是在单调矩阵上求一行最小值的问题。一般用 四边形不等式 判定是否具有决策单调性。

  • 蒙日阵:满足 \(\forall 1 \leq i_1 < i_2 \leq n, 1\leq j_1 < j_2 \leq m\)\(A_{i_1, j_1} + A_{i_2, j_2} \leq A_{i_1, j_2} + A_{i_2, j_1}\) 的矩阵 \(A\) 被称为 蒙日阵。也称 \(A\) 满足四边形不等式。其中 \(A_{i_1, j_1} + A_{i_2, j_2} \leq A_{i_1, j_2} + A_{i_2, j_1}\) 中的 \(\leq\) 理解为 优于

有重要结论:蒙日阵是完全单调矩阵

证明:

  1. 判断一个矩阵是否为完全单调矩阵只需要看任意的 \(2 \times 2\) 子矩阵是否为单调矩阵。可以画图理解。
  2. \(1\) 得:若一个矩阵 \(A\) 是完全单调矩阵,需满足 \(\forall 1 \leq i_1 < i_2 \leq n, 1 \leq j_1 < j_2 \leq m\)\(A_{i_1, j_1} \leq A_{i_1, j_2}\)\(A_{i_1, j_1} > A_{i_2, j_2}且 A_{i_2, j_1} > A_{i_2, j_2}\)。反过来讲,若 \(\exists 1 \leq i_1 < i_2 \leq n, 1 \leq j_1 < j_2 \leq m\),满足 \(A_{i_1, j_1} > A_{i_1, j_2} 且 A_{i_2, j_1} \leq A_{i_2, j_2}\),那么 \(A\) 就不是完全单调矩阵。发现此时 \(A_{i_1, j_1} + A_{i_2, j_2} > A_{i_1, j_2} + A_{i_2, j_1}\),与蒙日阵定义矛盾。证必。

这说明了,如果一个矩阵满足四边形不等式,那么它具有决策单调性。

判定一个矩阵为蒙日阵:

方法一:根据定义,证明 \(\forall 1 \leq i_1 < i_2 \leq n, 1\leq j_1 < j_2 \leq m\)\(A_{i_1, j_1} + A_{i_2, j_2} \leq A_{i_1, j_2} + A_{i_2, j_1}\)
方法二:证明 \(\forall 1 \leq i < n, 1 \leq j < m\)\(A_{i, j} + A_{i + 1, j + 1} \leq A_{i, j + 1} + A_{i + 1, j}\)。相当于仅考虑任意 \(2 \times 2\) 的子矩形。

对方法二正确性的说明:将定义式变形: \(A_{i_2, j_2} - A_{i_1, j_2} - A_{i_2, j_1} + A_{i_1, j_1} \leq 0\),发现这是二维差分的形式,相当于要求 \(A\) 的二维差分矩阵的任意一个矩形和小于等于 \(0\)。这等价于差分后每一个位置上的数都小于等于 \(0\)

特别说明:如果一个矩阵的左下角或右上角没有意义,可以通过给这些位置赋值为 \(k_i \times \infty\) 的方式让跨过斜对角线的 \(2 \times 2\) 子矩形满足四边形不等式。因此可以只关注另一部分是否满足四边形不等式。

蒙日阵的性质:

\(A, B\) 为蒙日阵。

  • \(A^{T}\) 为蒙日阵。
  • \(A + B\) 为蒙日阵。
  • \(\lambda A(\lambda \geq 0)\) 为蒙日阵。
  • \(A\) 的某一行或某一列加上任意常数 \(c\) 仍为蒙日阵。

一些蒙日阵的例子:

  • \(A_{x, y} = min(x, y)\)
  • \(A_{x, y} = max(x, y)\)
  • \(A_{x, y} = \sum_{i = 1}^{x}\sum_{j = 1}^{y}d_{i, j}\),其中 \(d\) 为非负矩阵。
  • \(A_{x, y} = f(y - x)\),其中 \(f(x)\) 为上凸函数。

决策单调性优化 \(dp\)

线性 \(dp\)

一般是划分段的 \(dp\)。通常会有代价函数 \(w_{l, r}\) 表示将 \([l, r]\) 划分成一段的代价。
\(w_{l, r}\) 看做矩阵第 \(l\) 行第 \(r\) 列的数,那么 \(w\) 可看做一个左下角无意义的矩阵。其满足四边形不等式的意义就是区间代价满足 交叉优于包含

分治(离线)

考虑转移式形如 \(f_{i, j} = \min\limits_{k < j}f_{i - 1, k} + w_{k, j}\)\(dp\)

通常第一维为层数或段数,称这样每一层仅有上一层转移得到的 \(dp\)分层转移。这样的 \(dp\) 并不限制转移顺序,可认为每一层内的转移是离线问题。

设第二维长度为 \(m\)\(f_{i - 1}\)\(f_{i}\) 的转移可以用一个 \(m \times m\) 的矩阵 \(A\) 表示:

\[A_{x, y} = \left\{\begin{matrix} f_{i - 1, y} +w_{y, x} & x \geq y\\ \infty & x < y \end{matrix}\right. \]

发现转移矩阵 \(A\) 就是 \(w\) 转置后每一列 \(j\) 加上常数 \(f_{i - 1, j}\)
由蒙日矩阵的性质:若 \(w\) 满足四边形不等式,那么 \(A\) 一定是蒙日阵,满足决策单调性。

如何求解:
定义函数 \(solve(l, r, ql, qr)\) 表示决策范围是 \([l, r]\),要给 \([ql, qr]\) 范围的状态进行转移。
那么每次找到中间状态 \(mid = \frac{ql + qr}{2}\),扫描 \([l, r]\) 找到最优决策点 \(p\)\(mid\) 进行转移。然后接着递归 \(solve(l, p, ql, mid - 1)\)\(solve(p, r, mid + 1, qr)\)

复杂度分析:假设每次求一个决策点的转移值复杂度 \(O(1)\),那么一共会递归 \(O(\log n)\) 层,每层复杂度 \(O(n)\),总复杂度 \(O(n \log n)\)
如果第一维的大小为 \(k\),那么总复杂度为 \(O(nk \log n)\)

局限性:只能离线,如果使用 \(cdq\) 会多一个 \(log\)
优点:好写,只要求是 单调矩阵\(A_{i, j}\) 不好计算时可以通过跳转指针增量求得

二分队列(在线)

考虑转移式形如 \(f_{i} = \min\limits_{j < i} f_j + w_{j, i}\)\(dp\):

发现转移顺序只能从小到大,视为在线问题。

与上面类似,转移同样可以写成一个 \(m \times m\) 的矩阵:

\[A_{x, y} = \left\{\begin{matrix} f_{y} +w_{y, x} & x \geq y\\ \infty & x < y \end{matrix}\right. \]

不同点在于这个是在线的每次将一列加一个常数。
但是并不影响蒙日阵的性质,因此如果 \(w\) 满足四边形不等式那么 \(A\) 同样具有决策单调性。

如何求解:
对每个点维护当前它的最有决策点。具体的说:开一个队列存储三元组 \((l, r, x)\),表示当前 \([l, r]\) 状态的最优决策点为 \(x\)。那么每次拿出队头的 \(i\) 并用决策点更新它,考虑 \(l\) 会更新那些状态的决策点:不难发现由于决策单调性因此影响的是一段后缀。因此从后往前合并决策点被更新成 \(i\) 的连续段,如果更新后缀的端点在一段的中间,就二分找到这个端点然后将这一段分裂。

复杂度分析:不难发现每次最多新开一段,并且一段只会被合并一次,因此合并连续段的复杂度均摊 \(O(n)\)。但是每次在段内二分,因此总复杂度 \(O(n \log n)\)

局限性:需要矩阵满足 完全单调
优点:离线和在线通用。

SMAWK

不会,先鸽了。

区间 \(dp\)

考虑对于形如 \(dp_{l, r} = \min\limits_{l \leq k \leq r}dp_{l, k} + dp_{k + 1, r} +w_{l, r}\) 的区间 \(dp\)

结论:
\(w\)满足四边形不等式 且满足 \(\forall l \leq l' \leq r' \leq r,w_{l',r'} \leq w_{l,r}\)(区间单调性),那么转移具有决策单调性。
\(p_{i, j}\) 表示 \(dp_{i, j}\) 的最优决策点,那么有 \(p_{i - 1, j} \leq p_{i, j} \leq p_{i, j + 1}\)

可以通过记录 \(p\) 然后缩小枚举范围的方式将复杂度优化到 \(O(n^2)\)

证明:不会

练习题

LOJ6039

\(n\) 个物品,每个物品体积为 \(w_i\),价值为 \(v_i\)
求当背包体积为 \(1 \sim k\) 时的最大价值。
\(n \leq 10^6,w_i \leq300, k \leq 5 \times 10^4\)

分析:
直接做 \(01\)背包复杂度为 \(O(nk)\),显然过不了。
注意到物品体积值域较小,启示我们把体积相同的物品一起考虑。
\(dp_{c, v}\) 表示考虑了体积为 \(1 \sim c\) 的物品,当前背包体积为 \(v\) 的最大价值。
转移显然把 \(v\%c\) 后分组:
\(dp_{c, c \times i + r} = \max\limits_{j \leq i}dp_{c - 1, c \times j + r} + f(c, i - j)\)
其中 \(f(c, x)\) 表示体积为 \(c\) 的物品价值前 \(x\) 大的物品的价值之和。
发现这与 \(dp_{k, i} = \max\limits_{j \leq i}dp_{k - 1, j} + w_{j, i}\) 的形式非常像,只是 \(w_{j, i} = f(c, i - j)\)
注意到 \(f(c, x)\) 是上凸的,因此 \(w\) 满足四边形不等式,每一组的转移满足决策单调性。
那么将每一组跑一遍决策单调性优化 \(dp\) 即可。
复杂度分析:对于一个 \(c\),一共有 \(c\) 组,每一组有 \(\frac{k}{c}\) 个状态,因此复杂度为 \(O(c \times \frac{k}{c} \times \log\frac{k}{c}) = O(k \times \log \frac{k}{c})\)。总复杂度为 \(O(\sum\limits_{c \leq max_w}k \times \log \frac{k}{c})\),卡常可过。

\(wqs\) 二分(蒙日阵最短路)

算法

考虑转移式形如 \(f_{i, j} = \min\limits_{k < j}f_{i - 1, k} + w_{k, j}\)\(dp\)
刚才我们说,这是一类离线问题,如果 \(w\) 是蒙日阵,假设第一维大小为 \(k\),第二维大小为 \(n\),那么可以再 \(O(nk\log n)\) 的复杂度求解。

但是如果第一维很大,复杂度就会爆炸。
但是如果最后只需要回答 \(f_{k, n}\),其中 \(k\) 是固定的,那么可以通过 \(wqs\) 二分优化复杂度。
通常这类问题的标志是: 恰好分成 \(k\)

做法
一. 转化成恰好走 \(k\) 条边的最短路问题

观察上述转移式,发现可以把 \(f_{i, j}\) 的含义理解成 从起点出发,经过了恰好 \(i\) 条边,到达了 \(j\) 号点 的最短路。
其中 \(i \to j\) 的边权为 \(w_{i, j}\),当 \(i > j\) 时, \(w_{i, j} = \infty\)
实际上这张图的邻接矩阵就是 \(w\)它是一个蒙日阵
那么答案就是从起点出发,恰好走 \(k\) 条边到达 \(n\) 的最短路。

二. 证明凸性

\(g(k) = f_{k, n}\)。那么有结论:当 \(w\) 为蒙日阵时,\(g(k)\) 是关于 \(k\) 的下凸函数。(如果取 \(max\) 就是上凸函数)

证明:
\(g(k)\) 为下凸函数等价于 \(\forall k \in[2, n - 1]\)\(g(k) - g(k - 1) \leq g(k + 1) - g(k)\)

假设能找到两条长度为 \(k\) 的路径 \(g'_1(k)\)\(g_2'(k)\) 满足 \(g_1'(k) + g_2'(k) \leq g(k - 1) + g(k + 1)\)
那么有 \(2g(k) \leq g'_1(k) + g'_2(k) \leq g(k - 1) + g(k + 1)\)。就可以证明原命题了。

假设 \(g(k - 1)\)\(g(k + 1)\) 的路径分别为 \(A,B\),用 \(a, b\) 表示两条路径上的点。
那么 \(A,B\) 是有序的,将两条路径的点按照编号从小到大归并到一起,形成的字符串如下:
\(aabbab...aab\)。其中 \(a\) 的数量比 \(b\) 多两个。
考虑找到一条分界线,满足分界线左边 \(a\) 恰好比 \(b\) 多一个,且分界线左右相邻的都是 \(a\)
那么如果将分别线左边的 \(a\) 和分界线右边的 \(b\) 拼接成一条路径,剩下的点拼成一条路径,不难发现两条路径的长度都为 \(k\)

7

并且两条路径的边权和与原来相比唯一改变的就是跨过分界线的两条边:b..aa..b 原来是 bb + aa 现在是 ba + ab。这个由 四边形不等式 可以得到现在两条路径的边权和更小。

这样就相当于找到了两条 \(g_1(k)\)\(g_2(k)\)

那么一定能找到这样的分界线吗?
不妨将 \(b\) 看做 \(-1\)\(a\) 看做 \(1\)。然后可以画出一条从 \((0, 0)\) 跑到 \((2k, 2)\) 的折线。那么这样的分界线的意义就是折线越过 \(y = 1\) 的横坐标。容易得到这样分界线一定存在。

三. \(wqs\) 二分

证明了 \(g(k)\) 是下凸函数,如何求解某个点值 \((x, g(x))\) 呢?

考虑二分一个斜率 \(k\),求斜率为 \(k\) 的直线与凸包的切点。如果这个切点横坐标大于 \(x\),说明二分的斜率偏大,否则斜率偏小。不断缩小 \(k\) 的范围直到直线与凸包相切在 \(x\) 就成功了。如果凸包和直线相切的是一段,也就是有多个切点,要规定此时选择最大或最小的那个。

怎么求一条斜率为 \(k\) 的直线和凸包的切点呢?
设直线为 \(y = kx + b\) 时与凸包相交,那么当直线与凸包相切时 \(b\) 最小。
\(f(x) = kx + b \Rightarrow b = f(x) - kx\)
也就是说,如果让每走一步需要减去一个额外代价 \(k\),此时不考虑步数,求出走到 \(n\) 时花费的最小代价就是最小的 \(b\)。此时走的步数 \(p\) 就是切点横坐标。

不难发现每走一步减去一个额外代价 \(k\) 相当于让蒙日阵整体减去一个常数,那么还是蒙日阵。问题就变成了不考虑步数的在线问题。仍然可以用 二分队列 优化转移。

复杂度 \(O(n \log n \log V)\)

进一步拓展
实际上 \(wqs\) 二分的应用并不局限于可以转化为蒙日阵最短路的问题。
只要看到题目上要求 某些变量的大小恰好为 \(k\),并且能够证明或者感受 答案关于这个变量是凸函数,那么就可以用 \(wqs\) 二分解决。

因此 \(wqs\) 二分与决策单调性的关系可以简单用一句话概括:
具有决策单调性一定可以用 \(wqs\) 二分,但是能用 \(wqs\) 二分不一定有决策单调性

需要注意:
\(wqs\) 二分只能求解对于 某个 给定的 \(k\)。对一个特定的 \(k\) 求答案的复杂度时 \(O(n \log n \log V)\) 的,当需要对所有 \(k\) 都求答案时, \(wqs\) 二分就无法解决。

\(wqs\) 二分构造方案
咕咕咕。

练习题

P2619 [国家集训队] Tree I

给你一张 \(V\) 个点, \(E\) 条边的无向连通图,每条边是黑色或白色。求一棵最小权的恰好有 \(k\) 条白色边的生成树。

\(1 \leq V \leq 5 \times 10^4\)\(1 \leq k \leq E \leq 10^5\)

分析:
\(f(k)\) 表示恰好有 \(k\) 条白色边的最小生成树。感性猜测 \(f(k)\) 关于 \(k\) 下凸,因此直接 \(wqs\) 二分。每次二分一个斜率 \(t\),相当于所有白色边的边权减去 \(t\),然后跑一边 \(kruskal\) 算法。需要注意要保证多个切点时切在最左边,因此每次排序如果一条白边和黑边边权相同那么把黑边排在前面。

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

数轴上有 \(n\) 个点,第 \(i\) 个点的位置为 \(a_i\)。你需要建立 \(m\) 个邮局,每个点的代价为它距离最近的邮局的距离。求所有点的最小代价和。

\(1 \leq m \leq n \leq 5 \times 10^5\)\(0 \leq a_i \leq 2 \times 10^6\)

分析:
由于要求 \(min\)\(min\),因此可以通过钦定每个点去哪个邮局 来计算代价,最后一定能转移出最优的。
对于一个邮局而言,显然前往它的点是一段区间。因此有一个暴力的 \(dp\)
\(dp_{i, j}\) 表示前 \(i\) 个点划分成 \(j\) 段,每一段放置一个邮局表示这一段点前往的邮局的最小花费。那么不难发现邮局的位置应该是这一段点的中位数,一段的花费可以 \(O(1)\) 计算。直接 \(dp\) 的复杂度为 \(O(n^2m)\)

然后是发现了这是 离线的最优段划分 问题。猜测满足四边形不等式,可以用决策单调性优化。
那么复杂度可以优化到 \(O(nm \log n)\)
注意到是 恰好 \(m\),并且满足蒙日阵,等价于蒙日阵最短路,答案具有凸性,可以 \(wqs\) 二分。
二分斜率 \(k\),那么每开一段需要减 \(k\),记录最优方案下开的段数以及花费即可。每次 \(dp\) 可以用 二分队列 优化。
复杂度 \(O(n \log n \log V)\)
CODE:

// wqs 二分 + 在线决策单调性 
#include<bits/stdc++.h>
#define MP make_pair
using namespace std;
typedef long long LL;
typedef pair< LL, int > PII;
const int N = 5e5 + 10;
int n, m;
LL a[N], s[N];
PII f[N];
struct seg {
	int L, R, x;
};
int l, r;
seg Stk[N];
LL w(int l, int r) {
	int mid = (l + r >> 1);
	return s[r] - s[mid] - (r - mid) * a[mid] + a[mid] * (mid - l + 1) - (s[mid] - s[l - 1]);
}
PII g(int x, int i, int c) {
	return MP(f[x].first + w(x + 1, i) - c, f[x].second + 1);	
}
void check(int c) {
	l = r = 1;
	Stk[l] = (seg) {1, n, 0};
	for(int i = 1; i <= n; i ++ ) {
		int x = Stk[l].x;
		f[i] = g(x, i, c);
		Stk[l].L ++; if(Stk[l].L > Stk[l].R) l ++;
		int ll = n + 1, rr = n;
		while(r >= l && g(i, Stk[r].L, c) < g(Stk[r].x, Stk[r].L, c)) {
			ll = Stk[r].L;
			r --;
		}
		if(r >= l) {
			int lt = Stk[r].L, rt = Stk[r].R, mid, res = -1;
			while(lt <= rt) {
				mid = (lt + rt >> 1);
				if(g(i, mid, c) < g(Stk[r].x, mid, c)) res = mid, rt = mid - 1;
				else lt = mid + 1;
 			}
 			if(res != -1) {
 				ll = res;
 				Stk[r].R = res - 1;
			}
		}
		if(ll <= rr) Stk[++ r] = (seg) {ll, rr, i};
	}
}
int main() {
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i ++ ) scanf("%lld", &a[i]);
	sort(a + 1, a + n + 1);
	for(int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
	int l = -1e9, r = 1e9, mid, res;
	check(-4);
	
	while(l <= r) {
		mid = (l + r >> 1);
		check(mid);
		if(f[n].second > m) r = mid - 1;
		else if(f[n].second == m) {printf("%lld\n", f[n].first + 1LL * m * mid); return 0;}
		else res = mid, l = mid + 1;
	}
	check(res);
	printf("%lld\n", f[n].first + 1LL * m * res);
	return 0;
}

P4383 [八省联考 2018] 林克卡特树

给你一棵 \(n\) 个点的树,每条边有边权 \(v_i\)。若 \(v_i < 0\),表示经过这条边需要花费 \(v_i\) 的代价。若 \(v_i > 0\),表示经过这条边能获得 \(v_i\) 的收益。你可以任意选择 \(K\) 条边并将它们删掉,然后加入 \(K\)\(v_i = 0\) 的边得到一棵新树。接着你可以选择两个点 \(p, q\) 并从 \(p\) 走到 \(q\)。求出可能的 收益 - 花费 最大是多少。

\(1 \leq n \leq 3 \times 10^5\)\(0 \leq K \leq 3\times 10^5\)\(K < n\)\(|v_i| \leq 10^6\)

分析:
假设我们已经确定了删除的 \(K\) 条边,考虑这时最大的 收益 - 花费 是多少。
发现会形成 \(K + 1\) 个连通块,那么每个连通块的最长简单路径之和就是答案。
可以证明答案不会超过这个值,也可以构造加的边取到这个值。

发现这 \(K + 1\) 条简单路径总是不交的。那么反过来想,任意取 \(K + 1\) 条两两不交的简单路径,一定存在一种删边方式把它们划分到不同的连通块里。

这样能说明原问题的答案就是 在树上选出 \(K + 1\) 条不交路径,路径之和的最大值
可以树背包:
\(f_{i, j}\) 表示 以 \(i\) 为根的子树里选了 \(j\) 条不交路径,没有一条 半路径 延伸上来的最大值。
\(g_{i, j}\) 则表示有一条半路径延伸上来。
那么转移时考虑将两半拼起来,并树背包转移即可。复杂度 \(O(n^2)\)

注意到要求 恰好选出 \(K + 1\) 条路径,考虑答案是否关于 \(K\) 存在凸性。
不难发现 \(K\) 越大答案越大,因为可以一条路径可以只有一个点。当 \(K\) 变大时答案的增量越来越小,因为肯定是先选出价值高的路径。
所以感性证明了答案关于 \(K\) 上凸。
那么直接 \(wqs\) 二分即可,每次新选中一条路径要减去斜率。
时间复杂度 \(O(n \log V)\)

CODE:

// wqs 二分 + 树形 dp
// 相当于求划分若干条不交链的最大权值和 
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
typedef long long LL;
int n, K;
LL sum[N]; // 到根的路径和 
struct edge {
	int v, last;
	LL w;
}E[N * 2];
struct G {
	LL v; int c;
	friend bool operator < (G x, G y) {
		return ((x.v < y.v) || (x.v == y.v && x.c > y.c));
	}
	friend G operator + (G x, G y) {
		G z = (G) {x.v + y.v, x.c + y.c};
		return z;
	}
};
int head[N], tot;
void add(int u, int v, LL w) {
	E[++ tot] = (edge) {v, head[u], w};
	head[u] = tot;
}
void dfs0(int x, int fa) {
	for(int i = head[x]; i; i = E[i].last) {
		int v = E[i].v; LL w = E[i].w;
		if(v == fa) continue;
		sum[v] = sum[x] + w;
		dfs0(v, x);
	}
}
G f[N], g[N], h[N]; // f[x] 没有半链延伸, g[x] 已经拼接过了, h[x] 有一条半链延伸上来了 
void dfs1(int x, int fa, LL cost) {
    f[x] = (G) {0, 0}; 
	g[x] = (G) {-cost, 1};
    h[x] = (G) {sum[x], 0};
	for(int i = head[x]; i; i = E[i].last) {
		int v = E[i].v;
		if(v == fa) continue;
		dfs1(v, x, cost);
		g[x] = g[x] + max(f[v], g[v]);
		g[x] = max(g[x], (G) {h[x].v + h[v].v - 2LL * sum[x] - cost, h[x].c + h[v].c + 1});
		h[x] = max(h[x] + max(g[v], f[v]), h[v] + f[x]);
		f[x] = f[x] + max(f[v], g[v]);
	}
}
void check(LL x) { // 新开一条路径的花费为 x 
	dfs1(1, 0, x);
}
int main() {
	scanf("%d%d", &n, &K); K ++;
	for(int i = 1; i < n; i ++ ) {
		int u, v; LL w;
		scanf("%d%d%lld", &u, &v, &w);
		add(u, v, w); add(v, u, w);
	}
	dfs0(1, 0);
	LL l = -3e11, r = 3e11, mid, res = 0;
	while(l <= r) {
		mid = (l + r >> 1);
		check(mid);
        G ans = max(f[1], g[1]);
		if(ans.c == K) {printf("%lld\n", ans.v + mid * K); return 0;}
		else if(ans.c > K) l = mid + 1LL;
		else res = mid, r = mid - 1LL;
 	}
 	check(res);
 	G ans = max(f[1], g[1]);
 	printf("%lld\n", ans.v + 1LL * K * res);
	return 0;
}

[ARC168E] Subsegments with Large Sums

给定长度为 \(n\) 的数列 \(\{a_i\}\) 和两个参数 \(k, s\),将 \(\{a_i\}\) 划分成 \(k\) 段,最大化和 \(\geq s\) 的段数。

\(1 \leq k \leq n \leq 250000\)\(1 \leq A_i \leq 10^9\)\(1 \leq s \leq 10^{15}\)

分析:
要恰好分成 \(k\) 段,考虑 \(wqs\) 二分。设 \(f(k)\) 表示恰好分成 \(k\) 段的答案,猜 \(f(k)\) 关于 \(k\) 上凸。
但是仔细想就会发现问题,因为可以把和 \(< s\) 的段分成两段,增加了段数但是答案不变,因此 \(f(k)\) 关于 \(k\) 并不是凸的。

枚举答案 \(t\),然后 \(check\) 能否有 \(\geq t\) 段的和 \(\geq s\)。不难发现这是满足单调性的,因为可以二分答案。
那么问题变成了如何 \(check\) 是否存在一种划分,满足划分的 \(k\) 段中至少有 \(t\) 段的和 \(\geq s\)
假设已经确定了这 \(t\) 段,那么只需要这 \(t\) 段外的位置数 \(\geq k - t\) 就合法,如果不满足就一定不合法。因为每段最少需要有一个元素。
假设此时这 \(t\) 段的长度之和为 \(g(t)\),只需要 \(n - g(t) \geq k - t\) 即可说明 \(t\) 合法。那么只需要我们求出最小的 \(g(t)\) 即可。

怎么求 \(g(t)\) 呢?
考虑一个暴力 \(dp\)\(dp_{i, j}\) 表示考虑前 \(j\) 个位置,当前选出了 \(i\) 个 和\(\geq s\) 的段,这些段长度之和的最小值。
那么转移有:
\(dp_{i, j} \gets dp_{i, j - 1}\)
\(dp_{i, j} \gets dp_{i - 1, lst_j - 1}\)
其中 \(lst_j\) 表示最大的 \(x\) 满足 \([x, j]\) 的和 \(\geq s\)
那么 \(g(t) = dp_{t, n}\)

这样复杂度\(O(n^2)\)。但是考虑要求的是 恰好 \(t\),考虑 \(wqs\) 二分。
不难发现此时 \(g(t)\) 关于 \(t\) 是上凸的,感性理解就是肯定先选长度较小的合法段,再选长度较大的段。
因此 \(wqs\) 二分就是对的。那么外层二分答案,内层 \(wqs\) 二分,每次 \(dp\) 复杂度 \(O(n)\),总复杂度就是 \(O(n \log n \log V)\)
CODE:

// f(x) 表示分成 x 段的最大合法段数, f(x) 不是凸的
// f(x) 表示分出 x 段合法段的最小代价,f(x) 是凸的。 合法的 x 就是一段前缀
// 二分,然后wqs二分求出点值check 
#include<bits/stdc++.h>
#define MP make_pair
using namespace std;
const int N = 250010;
typedef long long LL;
typedef pair< LL, int > PII;
int n, pre[N], K;
LL a[N], s, sum[N];
PII f[N];
void calc(int k) {
	f[0] = MP(0LL, 0);
	for(int i = 1; i <= n; i ++ ) {
		f[i] = f[i - 1];
		if(pre[i]) f[i] = min(f[i], MP(f[pre[i] - 1].first + i - pre[i] - k, f[pre[i] - 1].second + 1));
	}
}
LL F(int x) { // 求 x 的点值 
	int l = 1, r = n, mid, res;
	while(l <= r) {
		mid = (l + r >> 1);
		calc(mid);
		if(f[n].second == x) {return f[n].first + 1LL * x * mid;}
		else if(f[n].second < x) res = mid, l = mid + 1;
		else r = mid - 1;
	}
	calc(res);
	return f[n].first + 1LL * res * x;
}
int main() {
	scanf("%d%d%lld", &n, &K, &s);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%lld", &a[i]);
		sum[i] = sum[i - 1] + a[i];
	}
	int p = 1;
	for(int i = 1; i <= n; i ++ ) {
		while(p <= i && sum[i] - sum[p - 1] >= s) {
			p ++;
		}
		pre[i] = p - 1;
	}
	int l = 0, r = K, mid, res;
	while(l <= r) {
		mid = (l + r >> 1);
		if(F(mid) <= n - K) res = mid, l = mid + 1;
		else r = mid - 1;
	}
	cout << res << endl;
	return 0;
}

P5896 [IOI 2016] aliens

给你一个大小为 \(m \times m\) 的正方形,上面有 \(n\) 个点,你需要选出最多 \(k\) 个正方形使得这 \(k\) 个正方形的对角线与主对角线重合,并且这 \(k\) 个正方形能够覆盖所有的点,同时要求正方形的 面积并 最小。

\(1 \leq k \leq n \leq 10^5\)\(1 \leq m \leq 10^6\)

分析:
由于选出正方形的对角线与主对角线重合,因此我们直接在主对角线上每次选出一段作为某个正方形的对角线。
有两个问题:

  1. 覆盖所有点的限制怎么刻画
  2. 怎么求 面积并

对于第一个问题,发现一个点 \((x, y)\) 能被覆盖到的条件是对角线上选出一段 \([l, r]\) 包含 \([min(x, y), max(x, y)]\)
那么我们可以把点的限制转化为线段覆盖的问题,形如 \([l_i, r_i]\) 至少被某条线段覆盖。
然后是发现如果有限制相包含的情况可以删掉较短的那个,因为满足了较长的限制一定满足较短的。这样所有限制线段都是不交的,可以看作 \(l,r\) 分别单增。
把所有限制按照 \(l\)\(r\) 从小到大排序。那么每个选中的线段一定覆盖了一个区间的限制线段,假设覆盖了 \([L, R]\) 的限制,显然这一段取 \([l_L, r_R]\) 是最优的。

对于第二个问题,由于最优方案下我们选中的矩形不可能有包含情况,因此每个选中矩形的面积减去上一个它和上一个相邻的矩形的面积交就是它对答案的贡献。证明可以画图理解。

那么就可以 \(dp\)
\(dp_{i, j}\) 表示考虑了前 \(j\) 个限制,并且选中了 \(i\) 段使这 \(j\) 个限制至少被一条线段包含的最小代价。
怎么转移呢?

\(dp_{i, j} \gets dp_{i - 1, k} + w_{k + 1, j}\)

\(w_{i, j} = (r_{j} - l_{i} + 1)^2 - (r_{i -1} - l_{i} + 1)^2\)

但是这样转移当没有去掉矩形包含的情况。
其实不用考虑,因为如果存在矩形包含按照这种方式转移只会变得更劣,更不可能成为答案了。

接着发现这是一个典型的 离线最优段划分问题\(w\) 满足四边形不等式,因此转移具有决策单调性。
\(wqs\) 二分后用 二分队列 优化 \(dp\),复杂度 \(O(n \log n \log V)\)
好像 \(wqs\) 二分后可以用斜率优化去掉 \(\log n\),但是我没写。

CODE:

// wqs 二分之后决策单调性 
#include<bits/stdc++.h>
#define MP make_pair
using namespace std;
typedef long long LL;
typedef pair< LL, int > PII;
const int N = 1e5 + 10;
int n, m, K, tot;
int R[N], C[N];
PII dp[N];
struct seg {
	int l, r;
}s[N], h[N];
bool cmp(seg x, seg y) {
	return ((x.l < y.l) || (x.l == y.l && x.r > y.r));
}
inline LL calc(int x, int y) {
	x ++;
	LL S = 1LL * (h[y].r - h[x].l + 1) * (h[y].r - h[x].l + 1);
	LL ts = (h[x].l > h[x - 1].r ? 0 : 1LL * (h[x - 1].r - h[x].l + 1) * (h[x - 1].r - h[x].l + 1));
	return S - ts;
}
struct range {
	int l, r, x;
}stk[N];
int l, r;
inline PII get(int x, int i, LL c) {
	return MP(dp[x].first + calc(x, i) - c, dp[x].second + 1);
}
void solve(LL c) { // 每开一段 -c
    l = r = 1; 
    dp[0] = MP(0, 0);
	stk[l] = (range) {1, tot, 0};
	for(int i = 1; i <= tot; i ++ ) {
		int x = stk[l].x;
		dp[i] = get(x, i, c);
		stk[l].l ++; if(stk[l].l > stk[l].r) l ++;
		int lp = n + 1, rp = 0;
		while(r >= l && get(i, stk[r].l, c) < get(stk[r].x, stk[r].l, c)) {
			lp = min(lp, stk[r].l); rp = max(rp, stk[r].r);
			r --;
		}
		if(r >= l) { // 二分 
			int lt = stk[r].l, rt = stk[r].r, mid, res = -1;
			while(lt <= rt) {
				mid = (lt + rt >> 1);
				if(get(i, mid, c) < get(stk[r].x, mid, c)) res = mid, rt = mid - 1;
				else lt = mid + 1;
			}
			if(res != -1) {lp = res; rp = max(rp, stk[r].r); stk[r].r = res - 1;}
 		}
 		if(rp >= lp) stk[++ r] = (range) {lp, rp, i};
	}
}
int main() {
	scanf("%d%d%d", &n, &m, &K);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d%d", &R[i], &C[i]);
		R[i] ++; C[i] ++; 
		s[i] = (seg) {min(R[i], C[i]), max(R[i], C[i])};
	}
	sort(s + 1, s + n + 1, cmp);
	int mxr = 0;
	for(int i = 1; i <= n; i ++ ) {
		if(s[i].r <= mxr) continue;
		h[++ tot] = s[i];
		mxr = s[i].r;
	}
	K = min(K, tot);
    LL l = -1e12, r = 0, mid, res;
    while(l <= r) {
    	mid = (l + r >> 1);
    	solve(mid);
    	if(dp[tot].second == K) {printf("%lld\n", dp[tot].first + 1LL * K * mid); return 0;}
    	else if(dp[tot].second < K) res = mid, l = mid + 1;
    	else r = mid - 1;
	}
	solve(res);
	printf("%lld\n", dp[tot].first + 1LL * K * res);
	return 0; 
}
posted @ 2026-09-10 20:14  Czl2007  阅读(12)  评论(0)    收藏  举报