不断补题才能不断进步。
参考官方题解,xzyxzy?_?的题解
T(40)组数据,给定n(12)*m(2000)的数字矩阵(1e5),可以任意旋转每一列,然后从每行选择一个值,使得和最大。
做法是DP:
- 状态表示: 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],i表示前i列,j表示每行是否已经被确定值的mask,dp[i][j]表示当前最大值。
- 状态转移: 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[i−1][k]+cal(i,j−k)},其中k是j的子集,cal(i,x)表示选择第i列的一个旋转,填充集合x对应的最大值。
- 状态边界: d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0
- 复杂度:单组 O ( m ∗ 3 n ∗ n 2 ) O(m*3^n*n^2) O(m∗3n∗n2),m表示列数, 3 n 3^n 3n表示枚举集合以及子集, n 2 n^2 n2表示旋转列以及填充列的花费。
关于子集的子集个数之和为3^n的证明:
来自@Rhodoks
设i,j分别表示一个mask,j是i的子集当且仅当所有的n位都满足以下任一条件:
- 属于i,属于j
- 属于i,不属于j
- 不属于i,不属于j
对于长为n的集合,总有 3 n 3^n 3n个(i,j)对满足上述条件,即集合的子集的子集个数是 3 n 3^n 3n
此时有两个复杂度优化:
- 最多只会有n列的值产生效果,所以根本不需要把m列全部dp完成。可以按最大值对列排序,然后只选最大的n列进行dp。
此时单组复杂度 O ( 3 n ∗ n 3 ) O(3^n*n^3) O(3n∗n3), 40 ∗ 3 12 ∗ 1 2 3 = 3.6 e 10 40*3^{12}*12^3=3.6e10 40∗312∗123=3.6e10 - 可以花费 O ( n 3 ∗ 2 n ) O(n^3*2^n) O(n3∗2n)预处理出所有cal函数的值,三个n分别是:列数、枚举旋转、尝试填充。然后在转移过程中直接使用处理好的cal值,转移复杂度 O ( n ∗ 3 n ) O(n*3^n) O(n∗3n),单组总复杂度 O ( n ∗ 3 n + n 3 ∗ 2 n ) O(n*3^n+n^3*2^n) O(n∗3n+n3∗2n), 40 ∗ ( 12 ∗ 3 12 + 1 2 3 ∗ 2 12 ) = 5 e 8 40*(12*3^{12}+12^3*2^{12})=5e8 40∗(12∗312+123∗212)=5e8
学到的:
- n个元素的集合,子集的子集个数之和是 3 n 3^n 3n。
- 状压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;
}
304

被折叠的 条评论
为什么被折叠?



