# 问题引入

如果你遇到了一类这样的问题:

nn 个物品,你必须在遵守一定限制的前提下从中选出 mm 个物品,最大 / 小化这 mm 个物品的总价值。

你可能会想到动态规划,但是这样动态规划时间复杂度是 O(nm)O(nm) 起步的。

但如果这个问题去掉 mm 个物品的要求,你可以直接用一维动态规划,或许会更容易解决;而且你发现,如果要求选 xx 个物品的情况下最大 / 小价值为 g(x)g(x),函数 g(x)g(x) 具有上凸 / 下凸的性质(实际上大多数这类问题都有这个性质),那么你可以用到一个利器来优化朴素的二维 dp\text {dp}wqs\text {wqs} 二分。

# 算法介绍

我们设 kk 表示要求选的物品个数,g(k)g(k) 表示选出 kk 个物品所能得到的最大 / 小价值,以 kk 为横轴,以 g(k)g(k) 为纵轴,可以画出这样的图:

wqs-1.jpg

凸包有什么性质?斜率单调递减!如果我们考虑用一条直线去切这个凸包:

wqs-2.jpg

我们总能切到一个最高点,设这个点是 (x,g(x))(x, g(x)),直线斜率为 cc,那么有如下等式成立:

g(x)=cx+bg(x) = cx + b

即:

b=g(x)cxb = g(x) - cx

那么 cx-cx 有什么实际含义吗?我们可以理解为 把每个物品的价值都减去一个 cc,则我们选出的 xx 个物品总价值就正好减去了 cxcx

如果我们一开始设定了这条直线的斜率为 cc,然后我们把每个物品的价值都减去 cc,进行一次不限定物品个数的一维 dp\text {dp},能找到一个最大值,这个最大值就对应着这条相切直线的截距,如果我们在 dp\text {dp} 的过程中还记录下我们选了多少个物品,就能很容易地反推出 g(x)g(x) 是多少!

于是我们的优化思路就出来了:我们二分这条直线的斜率,找到一个斜率使直线能和我们需要的 (k,g(k))(k, g(k)) 相切,我们就能反推出 g(x)g(x) 是多少!

关于斜率的上下界,我们找到 (0,g(0)),(1,g(1))(0, g(0)), (1, g(1)) 之间的斜率和 (n1,g(n1)),(n,g(n))(n - 1, g(n - 1)), (n, g(n)) 之间的斜率就可以确定下来。

大体思路就是这样,时间复杂度为 O(nlogp)O(n \log p),其中 nn 为物品个数(此处我们假定一维 dp\text {dp} 是线性可做的),pp 是价值的值域。但我们还有一个细节问题需要说明。如果我们的图长这样:

wqs-2.jpg

也就是说存在一条直线和图像的多个点相切的话,我们该如何判断当前斜率是大了还是小了?我们不妨把切到的这个 xx 永远设定为最小的那个 xx,我们在二分的过程中就能处理这个问题了。

由此可见,wqs\text {wqs} 二分使用的关键在于证明答案的凹凸性。一般的证明思路是 g(k)g(k1)>g(k+1)g(k)g(k) - g(k - 1) > g(k + 1) - g(k)g(k)g(k1)<g(k+1)g(k)g(k) - g(k - 1) < g(k + 1) - g(k),即后面越选物品能获得的价值越大 / 小。

废话不多说,上例题!

# 例题

# 例题 1. 朝日捡石头

C6 - 2024级程序设计基础第六次上机赛 J. 朝日捡石头

这是一道 wqs\text {wqs} 二分的模板题。