【题解】 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;
}
posted @ 2020-09-28 21:38  Imakf  阅读(161)  评论(0编辑  收藏  举报