Codeforces1420E. Battle Lemmings 题解 动态规划
题目链接:https://codeforces.com/contest/1420/problem/E
题目大意:
给你一个长度为 \(n\) 的 \(01\)序列,每一次操作你可以交换相邻的两个元素。
定义序列的 保护值( protection )为“序列中一对数值为 \(0\) 的数,且这对数之间夹着至少一个 \(1\)”的对数。
请计算当最多执行 \(i\) 次交换操作(\(i \in [0, \frac{n(n-1)}{2}]\))的情况下序列保护值的最大值各是多少。
问题分析
对于给定的一个 \(01\) 序列 \(A\),我们定义另一种状态 \(f(A)\) 为 \(A\) 的另外一种表述方式。
\(f(A)\) 仍然是一个集合,但是它表示的含义如下:
- 其第一个元素表示 \(A\) 中第一个 \(1\) 前面的 \(0\) 的数量;
- 其第二个元素表示 \(A\) 中介于第一个 \(1\) 和第二个 \(1\) 之间的 \(0\) 的数量;
- ……
- 其最后一个元素表示在最后一个 \(1\) 后面的 \(0\) 的数量。
举个例子:\(f(011000101100) = \{1, 0, 3, 1, 0, 2\}\),在这个例子中:
- 在第一个 \(1\) 前面有 \(1\) 个 \(0\);
- 在第一个 \(1\) 和第二个 \(1\) 之间有 \(0\) 个 \(0\);
- 在第二个 \(1\) 和第三个 \(1\) 之间有 \(3\) 个 \(0\);
- ……
- 在最后一个 \(1\) 后面有 \(2\) 个 \(0\)。
可以发现,将原始的 \(A\) 转换为 \(f(A)\) 并不困难。
现在,如果我们交换序列 \(A\) 中两个相邻的不同的数,\(f(A)\) 也会发生相应的变化。也就是说,一旦交换两个相邻的数,\(f(A)\) 中的一对相邻的数就会发生变化(一个加一,一个减一)。
让我们定义如下一个任务,定义两个序列 \(A = \{ a_1, a_2, \cdots, a_k \}\) 以及 \(B = \{b_1, b_2, \cdots, b_k\}\)。我们需要计算将序列 \(A\) 变成序列 \(B\) 的最少交换次数(明显地,\(A\)中所有元素之和等于\(B\)中所有元素之和)。
为了解决这个问题,我们假设在第\(i\)个元素后面设置一个“屏障”。于是序列\(A\)变成了两部分:左边部分(第\(1\)到\(i\)个元素)和右边部分(第\(i+1\)到\(n\)个元素)。为了让序列\(A\)和\(B\)的左右部分相同,你需要在第\(i\)和\(i+1\)个元素之间进行 \(g_{A, B}(i) = \left|\sum_{j=1}^{i} a_i - \sum_{j=1}^i b_i\right|\) 次交换操作。而总的交换次数为
对于一个序列 \(A\) 来说,假设已知它的转换序列 \(f(A) = \{f_1, f_2, \cdots, f_k\}\), 则我们可以通过如下公式求得序列的 保护值( protection )\(p(A)\):
因为 \(\sum_{i=1}^k f_i\) 已知,所以我们的目的是最小化 \(\sum_{i=1}^k f_i^2\) 。
我们定义 一轮交换 的含义是:第 \(i\) 轮我们交换了 \(f_i\) 和 \(f_{i+1}\) 若干次,然后最终的 \(f_i\) 确定下来了 —— 这样算一轮。(也就是第 \(i\) 轮交换,我们需要将 \(f(A)\) 中的第 \(i\) 个元素和第 \(i+1\) 个元素交换若干轮,交换结束时,第 \(i\) 个元素就确定了,这一轮交换才算完成)
接下来我们尝试找到序列 \(f(A) = \{f_1, f_2, \cdots, f_k\}\) 在不超过 \(k\) 轮交换下的最佳序列。
我们需要定义一个状态 \(dp_{i,\ s,\ k}\) ,它表示的含义是:
我们已经确定了 \(f(A)\) 的前 \(i\) 个元素,并且进行了 \(k\) 轮 交换的情况下,并且 \(\sum_{i=1}^k f_i\) 等于 \(s\) 的情况下,对应的 \(\sum_{i=1}^k f_i^2\) 的最小值。
为了得到答案我们需要先计算得到 \(dp_{i,\ c_0,\ k}\),其中 \(c_0\) 表示原始序列中 \(0\) 的数量。
并且为了计算 \(dp_{i,\ s,\ k}\) 我们需要去遍历最终 \(f_{i+1}\) 可能的情况 —— 这里假设它会变为 \(h\)。
于是,可以得到状态转移方程如下:
这里, \(z_i\) 表示初始序列 \(f(A)\) 中 \(\sum_{j=1}^i f_i\) 的值。
总体时间复杂度为 \(O(n^5)\) ,但是代码实现中常数比较小,所以最终的时间复杂度能达到 \(O(\frac{n^5}{27})\)。
示例代码:
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 105;
int n, a[maxn], b[maxn], k, p[maxn], dp[maxn][maxn][maxn*(maxn-1)/2];
void chk_min(int &x, int y) {
x = min(x, y);
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++) cin >> a[i];
a[n] = 1;
for (int i = 0; i <= n; i ++) {
if (a[i] == 0) b[k] ++;
else k ++;
}
partial_sum(b, b+k, p);
memset(dp, 0x3f, sizeof(dp));
dp[0][0][0] = 0;
int l = p[k-1];
for (int i = 0; i < k; i ++) {
for (int j = 0; j <= l; j ++) {
for (int s = 0; s <= n*(n-1)/2; s ++) {
if (dp[i][j][s] == INF) continue;
for (int q = j; q <= l; q ++) {
chk_min(dp[i+1][q][s + abs(q - p[i])], dp[i][j][s] + (q-j)*(q-j));
}
}
}
}
int mn = INF;
for (int s = 0; s <= n*(n-1)/2; s ++) {
chk_min(mn, dp[k][l][s]);
int val = l * l - mn;
assert(val % 2 == 0);
cout << val / 2 << " ";
}
return 0;
}