CSP2019 Day2T2 划分
很显然有一个暴力 \(dp\),令 \(dp_{i, j}\) 表示最后一次划分在 \(i\) 上次划分在 \(j\) 的最小花费,令 \(S_i = \sum\limits_{j = 1} ^ i a_j\)。那么有转移:
可以发现 \(dp_{j, k}\) 的值是和 \(i\) 无关的,只是 \(k\) 的取值范围和 \(i\) 有关,那么我们只需要知道 \(k\) 的取值范围再取这个范围内的最小值即可。将后面那个条件移项可得 \(S_k \ge 2 \times S_j - S_i\),因此 \(k\) 的取值范围应该在 \(2 \times S_j - S_i \sim j - 1\),而当 \(j\) 固定时,因为 \(S_i\) 随着 \(i\) 的增大单调递增,所以 \(k\) 能取到的下界不断减小,于是我们可以考虑在 \(i\) 的同时维护每个 \(j\) 的下界以及当前能取到的最小 \(dp_{j, k}\) 这样均摊复杂度就是 \(O(n ^ 2)\) 的了。
rep(i, 1, n) dp[i][0] = sum[i] * sum[i], p[i] = i - 1, mx[i] = inf;
rep(i, 2, n){
rep(j, 1, i - 1){
while(p[j] >= 0 && sum[p[j]] >= 2 * sum[j] - sum[i]) mx[j] = min(mx[j], dp[j][p[j]]), --p[j];
if(p[j] < j - 1) ++p[j];
if(mx[j] != inf) dp[i][j] = mx[j] + (sum[i] - sum[j]) * (sum[i] - sum[j]);
}
}
rep(i, 0, n - 1) ans = min(ans, dp[n][i]);
printf("%lld", ans);
return 0;
如果这个 \(dp\) 不能压缩状态是显然无法优化的,但为了保证题目中的条件,我们根本无法压缩状态,下面一个常见的手段就是可以打个决策转移的表。可以发现我们每次转移 \(j \rightarrow i\) 都是选择了一个最大的能满足条件的 \(k\) 从 \(dp_{j, k} \rightarrow dp_{i, j}\),并且可以发现当 \(j\) 越大时 \(dp_{i, j}\) 越大。这样换句话来说就是每次我们让最后一段尽可能选择小的是更优的,实际上因为我们有 \((a + b) ^ 2 > a ^ 2 + b ^ 2\) 因此我们要尽可能多分段,如果我们每次选择最小的一段,不仅能够分更多的段,并且还能让后面尽可能分的段更多,并且能让当前的贡献更小,所以这样做一定是更优的。针对这个贪心,我们可以令 \(dp_i\) 表示当前划分到以 \(i\) 结尾的段的最小花费,令 \(p_i\) 表示 \(i\) 是由哪里转移来的,那么每次转移就要找到一个最大 \(j\) 满足 \(S_i - S_j \ge S_j - S_{p_j}\) 移项可得 \(S_i \ge 2 \times S_j - S_{p_j}\),我们大可以使用线段树二分出左边第一个比某个数大的位置,但实际上这里还有个 \(O(n)\) 做法。可以发现因为 \(S_i\) 是单调递增的,因此我们之前能够选择的 \(j\) 随着 \(i\) 的增大一定是还能继续选的。因此我们可以维护一个类似单调队列的东西,在队列中 \(2 \times S_j - S_{p_j}\) 单调递增,每次我新加入一个元素,从队尾一直开始弹出知道这个 \(2 \times S_{q_t} - S_{p_{q_t}} < 2 \times S_j - S_{p_j}\) 为止,那么这样我们获得的每个值的位置都将是当前最靠右的点,并且由于 \(S_i\) 在不断单调递增,我们只需要每次不断移动队尾找到队列中最后一个 \(\le S_i\) 的元素即可。
dp[1] = a[1] * a[1], h = 1, t = 2, q[1] = 0, q[2] = 1;
rep(i, 2, n){
while(h <= t && 2 * sum[q[h]] - sum[p[q[h]]] <= sum[i]) ++h;
--h, p[i] = q[h];
dp[i] = dp[p[i]] + (sum[i] - sum[p[i]]) * (sum[i] - sum[p[i]]);
while(h <= t && 2 * sum[i] - sum[p[i]] <= 2 * sum[q[t]] - sum[p[q[t]]]) --t;
q[++t] = i;
}
printf("%lld", dp[n]);
最后那 \(12\) 分需要使用高精度,根据我们的流程可以看出实际上我们不需要记录 \(dp\) 数组,只需要记录之前的 \(p\) 即可,最后我们只需要不断往下迭代加上每一段的值即可,我的高精太丑了(其实是写的 \(\_\_int128\))就不贴了。