【题解】 CF1420E Battle Lemmings dp+补集转化
Legend
Link \(\textrm{to Codeforces}\)。
现在有一个长度为 \(n\ (1 \le n \le 80)\) 的 \(\textrm{01}\) 序列 \(a_i\),我们定义序列的牢固值为:
满足 \(i<j,a_i=0,a_j=0,\exist k \in[i,j],a_k=1\) 的 \((i,j)\) 的二元组对数。
现在定义一次操作为:交换两个相邻的两个不同的数字。求出操作至多 \(k\ (0 \le k \le \frac{n(n-1)}{2})\) 次序列的最大牢固值。
Editorial
what makes contributions?
先把答案所求的 \((i,j)\) 无序对先看成 \((i,j)\) 有序对。
考虑一个问题,给定一个序列,如何计算牢固值?
直接求牢固值非常的不好做,于是考虑转化成求有多少对位置不能贡献牢固值,再拿 \(n\times n\) 减去。
那么有哪些情况不会贡献牢固值呢?
- 出现了 \(1\) 的位置;
- 两个 \(0\) 之间没有 \(1\)。
两类。
transform
为什么要把不能造成贡献的二元组如此分成两类,因为前面一类是定值,不管如何交换序列都不会变。
第二类的贡献不是定值,我们的目标很明确,就是最小化它。
也就是最小化 \(0\) 连续段长度平方和。
什么是 \(0\) 连续段呢?举一个例子:
100100010
这里有 \(4\) 个 \(0\) 连续段,长度分别为 \(0,2,3,1\)。
我们用 \(v_i\) 表示第 \(i\) 个 \(0\) 连续段的长度。
长度平方和也就是 \(\sum v_i^2\)。
现在问题成功转化为:
给定长度为 \(n\) 的序列 \(v_i\),每次你可以从某一个位置取出 \(1\),加到另一个相邻位置去,称此为一次操作。
要求任何时刻序列里的数字都是自然数。最小化至多 \(k\) 次操作后的 \(\sum v_i^2\)。
just dp
考虑 \(\rm{dp}\)。
一个非常暴力的做法就是设 \(dp_{i,j,k}\) 表示考虑了前 \(i\) 个数字,用了 \(j\) 次操作,当前位置要给后面传递 \(k\) 个 \(1\)(包括从前面传过来的),的最小 \(\sum_{s=1}^{i} v_s^2\)。
若 \(k < 0\) 则认为是它要从后面索要 \(-k\) 个 \(1\)。
\(i,j,k\) 分别是 \(O(n),O(n^2),O(n)\) 级别。就算我们常数再小,转移也必须要 \(O(n)\) 以内完成。
转移直接枚举当前位置最后变成了什么数字,算个贡献就行了。
复杂度 \(O(n^5)\),但是常数很小,可以通过。
Code
代码意外很短,我以为我会在下标问题上纠结很久,毕竟 \(\rm{c++}\) 不支持负数下标,要手动设置偏移量。
结果我最后因为统计的是有序对,忘记 \(\div 2\) 了,调试了很久。
#include <bits/stdc++.h>
using namespace std;
#define debug(...) fprintf(stderr ,__VA_ARGS__)
const int MX = 80 + 2;
int n ,a[MX] ,v[MX] ,m;
int dp[MX][MX * (MX) / 2][MX * 2];
void chkmin(int &a ,int b){a = min(a ,b);}
void chkmax(int &a ,int b){a = max(a ,b);}
int main(){
memset(dp, 0x3f ,sizeof dp);
cin >> n;
int cnt = 0;
for(int i = 1 ; i <= n ; ++i){
cin >> a[i];
if(a[i]){
v[++m] = cnt;
cnt = 0;
}
else ++cnt;
}
v[++m] = cnt;
for(int i = 1 ; i <= m ; ++i){
debug("%d%c" ,v[i] ," \n"[i == m]);
}
int mxop = n * (n - 1) / 2 ,sum = n - m + 1;
dp[0][0][MX] = 0;
for(int i = 1 ; i <= m ; ++i){
for(int k = 0 ; k <= mxop ; ++k){
for(int last = MX - sum ; last <= MX + sum ; ++last){
int avalast = last - MX;
// 为正数说明可以传递出去
// 为负数说明要传递过来
for(int val = 0 ; val <= sum ; ++val){
int avanow = v[i] - val;
// 为正数说明当前可以传递多少
// 为负数说明当前需要多少
int giveout = avalast + avanow;
if(giveout > n || giveout < -n) continue;
chkmin(dp[i][k + abs(avalast)][giveout + MX]
,dp[i - 1][k][last] + val * val);
}
}
}
}
int premax = 0;
--m;
int org = n * n - m * m - 2 * m * (n - m);
debug("ORG = %d\n" ,org);
++m;
for(int i = 0 ; i <= n * (n - 1) / 2 ; ++i){
int Ansnow = (org - dp[m][i][MX]) / 2;
chkmax(premax ,Ansnow);
cout << premax << " ";
}
cout << endl;
return 0;
}