忘情(wqs二分+斜率优化dp)

一、题目及简单分析

定义一段序列的价值是 \(\dfrac{((\sum\limits_{i=1}^n a_i \times \overline{a}))^2}{\overline{a}^2}\)。给定一段长为 \(n\) 的序列,将其恰好分成 \(m\) 段,使得每一段价值总和最小。求这个最小值。\(m,n\le 10^5\)

有一个 \(O(n^2)\) 的 dp,\(dp(i,k)\) 表示考虑前 \(i\) 个数,分了 \(k\) 段时的最小值,\(dp(i,k)=\min\limits_{1\le j<i}\{dp(j,k-1)+\sum\limits_{l=j+1}^i a_i\}\)。怎样优化它?

二、wqs二分

显然地,分的段数越多,答案就越小;不那么显然地,分的段数越多,减小的就越慢——在分四段的基础上再分一段,比在分三段的基础上再分一段,其最小值变化量更小。由此,如果设 \(f(x)\) 表示分了 \(x\) 段的最小值,并把 \((x,f(x))\) 作为一个点扔到平面上,就能勾勒出一个下凸包

实际上,我们不清楚这个上凸包的具体形状,或者说,那些点的纵坐标 \(f(x)\) 正是我们想求的。但是,我们可以用一些直线去“探测”这个凸包。过程如下:

  1. 二分直线的斜率
  2. 平移这条直线,使它与凸包相切;
  3. 某种方法(下文中说明)计算切线截距,并回代解出切点坐标;
  4. 根据切点横坐标判断分的段数是多了还是少了,调整斜率,继续二分。

最终,我们将得到切在 \((m,f(m))\) 的直线斜率,也就知道了 \(f(m)\)。如果第 3 条里的某种操作是 \(O(n)\) 的,那整个时间复杂度就只有 \(O(n\log n)\) 了。

以上是 wqs 二分的全过程。下附代码(一个平平无奇的二分):

while (l <= r) {
	mid = (l + r) >> 1;
	if (check(mid)) ans = mid, r = mid - 1;
	else l = mid + 1;
}

为什么这么一波操作就大大优化了时间复杂度?考虑这样一件事:当我们在 \(O(n^2)\) dp 的时候,我们力图将每个 \(f(i)\) 都求出来——这是一个递推关系,没有前面的就算不出后面的。在不少题目里,这是唯一的思路。

然而,本题却凭借一个特殊的性质,与凸包建立了联系,进而可以运用一个巧妙的数学思维解决——如上所述,\(\log n\) 次探测,就解决了问题。前面没算出来?无关紧要。

所谓特殊的性质,也就是应用 wqs 二分的条件:

  • 求“恰好……个”的最值问题;
  • dp 方程能够抽象为凸包。

三、斜率优化

这道题还没有做完,还记得上文中提到的“求截距的‘某种方法’”吗?对于此题的数据范围,我们必须 \(O(n)\) 完成这件事。

考虑截距 \(f'(x)\) 的实际意义:\(f'(x)=f(x)-kx\),所以每次转移时加上一个附加权值 \(-k\),就可以按照不考虑“恰好选……个”的 dp 方式求出 \(f'(x)\) 了。

这个 dp 运用斜率优化,最终达到 \(O(n\log n)\) 的复杂度。下面是斜率优化部分代码:

#define Y(x) (f[x] + s[x] * s[x] - 2 * s[x])
#define X(x) (s[x])

inline double K(int a, int b) {
	return (double)(Y(b) - Y(a)) / (X(b) - X(a));
}

inline bool check(LL val) { 
	reg int i, l, r;
	for (i = 1; i <= n; ++i)
		f[i] = INF;
	q[l = r = 1] = 0;
	for (i = 1; i <= n; ++i) {
		while (l < r && K(q[l], q[l + 1]) < 2 * s[i]) ++l;
		f[i] = f[q[l]] + (s[i] - s[q[l]] + 1) * (s[i] - s[q[l]] + 1) + val;
		g[i] = g[q[l]] + 1;
		while (l < r && K(q[r - 1], q[r]) > K(q[r - 1], i)) --r;
		q[++r] = i;
	}
	return g[n] <= m;
}

THE END

posted @ 2021-09-09 23:38  q0000000  阅读(69)  评论(0编辑  收藏  举报