忘情(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)\) 正是我们想求的。但是,我们可以用一些直线去“探测”这个凸包。过程如下:
- 二分直线的斜率;
- 平移这条直线,使它与凸包相切;
- 用某种方法(下文中说明)计算切线截距,并回代解出切点坐标;
- 根据切点横坐标判断分的段数是多了还是少了,调整斜率,继续二分。
最终,我们将得到切在 \((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;
}