【题解】Codeforces1209E2 Rotate Columns (hard version) 状压DP

不断补题才能不断进步。
参考官方题解xzyxzy?_?的题解
T(40)组数据,给定n(12)*m(2000)的数字矩阵(1e5),可以任意旋转每一列,然后从每行选择一个值,使得和最大。


做法是DP:

  1. 状态表示: d p [ i ] [ j ] , i 表 示 前 i 列 , j 表 示 每 行 是 否 已 经 被 确 定 值 的 m a s k , d p [ i ] [ j ] 表 示 当 前 最 大 值 。 dp[i][j],i表示前i列,j表示每行是否已经被确定值的mask,dp[i][j]表示当前最大值。 dp[i][j]iijmaskdp[i][j]
  2. 状态转移: d p [ i ] [ j ] = m a x { d p [ i − 1 ] [ k ] + c a l ( i , j − k ) } , 其 中 k 是 j 的 子 集 dp[i][j] = max\{dp[i-1][k] + cal(i,j-k)\},其中k是j的子集 dp[i][j]=max{dp[i1][k]+cal(i,jk)}kj,cal(i,x)表示选择第i列的一个旋转,填充集合x对应的最大值。
  3. 状态边界: d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0
  4. 复杂度:单组 O ( m ∗ 3 n ∗ n 2 ) O(m*3^n*n^2) O(m3nn2),m表示列数, 3 n 3^n 3n表示枚举集合以及子集, n 2 n^2 n2表示旋转列以及填充列的花费。

关于子集的子集个数之和为3^n的证明:
来自@Rhodoks
设i,j分别表示一个mask,j是i的子集当且仅当所有的n位都满足以下任一条件:

  1. 属于i,属于j
  2. 属于i,不属于j
  3. 不属于i,不属于j

对于长为n的集合,总有 3 n 3^n 3n个(i,j)对满足上述条件,即集合的子集的子集个数是 3 n 3^n 3n

此时有两个复杂度优化:

  1. 最多只会有n列的值产生效果,所以根本不需要把m列全部dp完成。可以按最大值对列排序,然后只选最大的n列进行dp。
    此时单组复杂度 O ( 3 n ∗ n 3 ) O(3^n*n^3) O(3nn3) 40 ∗ 3 12 ∗ 1 2 3 = 3.6 e 10 40*3^{12}*12^3=3.6e10 40312123=3.6e10
  2. 可以花费 O ( n 3 ∗ 2 n ) O(n^3*2^n) O(n32n)预处理出所有cal函数的值,三个n分别是:列数、枚举旋转、尝试填充。然后在转移过程中直接使用处理好的cal值,转移复杂度 O ( n ∗ 3 n ) O(n*3^n) O(n3n),单组总复杂度 O ( n ∗ 3 n + n 3 ∗ 2 n ) O(n*3^n+n^3*2^n) O(n3n+n32n) 40 ∗ ( 12 ∗ 3 12 + 1 2 3 ∗ 2 12 ) = 5 e 8 40*(12*3^{12}+12^3*2^{12})=5e8 40(12312+123212)=5e8


学到的:

  1. n个元素的集合,子集的子集个数之和是 3 n 3^n 3n
  2. 状压DP得到了练习。
/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();

int save[32][2048], n, m;
vector<int> get_cols() //获得候选列
{
	for(int j=1; j<=m; ++j)
	{
		save[0][j] = 0;
		for(int i=1; i<=n; ++i)
			save[0][j] = max(save[0][j], save[i][j]);
	}
	vector<int> cols;
	for(int j=1; j<=m; ++j)
		cols.push_back(j);
	sort(cols.begin(), cols.end(), [](int a,int b){
		return save[0][a]>save[0][b];
	});
	cols.resize(min(n,m));
	return cols;
}
int cal_table[16][4096]; //cal_table[i][j]表示第i个候选列填满j的最大值
int cal(int c, int x) //选择第c列的一个旋转,获得填满x的最大值
{
	int ans = 0;
	for(int i=1; i<=n; ++i) //枚举列起点
	{
		int tmp = 0;
		for(int j=0; j<n; ++j) //列偏移
			if(x>>j&1)
				tmp += save[i+j][c];
		ans = max(ans, tmp);
	}
	return ans;
}
int dp[16][4096];
vector<int> sub[4096];
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

	//预处理每个集合的子集
	for(int i=0; i<4096; ++i)
		for(int j=0; j<=i; ++j) if((i|j)==i)
			sub[i].push_back(j);

	int T = read();
	while(T--)
	{
		n = read(), m = read();
		for(int i=1; i<=n; ++i)
			for(int j=1; j<=m; ++j)
				save[i][j] = read();
		vector<int> cols = get_cols(); 
		m = min(n,m);
		for(int c:cols) //扩展长度
			for(int i=1; i<=n; ++i)
				save[i+n][c] = save[i][c];

		for(int i=1; i<=m; ++i) //预处理所有cal值
			for(int x=0; x<(1<<n); ++x)
				cal_table[i][x] = cal(cols[i-1], x);

		//dp
		for(int i=1; i<=m; ++i)
		for(int j=0; j<(1<<n); ++j)
		{
			dp[i][j] = 0;
			for(int k:sub[j])
				dp[i][j] = max(dp[i][j], dp[i-1][k]+cal_table[i][j-k]);
		}

		printf("%d\n",dp[m][(1<<n)-1] );
	}

    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值