状压DP杂谈(+轮廓线,插头)
command_block · · 个人记录
状压DP思想 : 用集合作为状态,采用特殊的寻址方法。
约定
- P1879 [USACO06NOV]Corn Fields G
这是一道最最最模板的题目了。
题意 : 有一片
边界 :
答案 :
这样的复杂度是
事实上,剪枝剪得好复杂度是
容易发现可以滚动数组,下面的代码都采用了此优化。
这份代码跑 n=m=16 无障碍矩阵,耗时 1.7s。
#include<cstring>
#include<cstdio>
#define MaxS 4500
#define mod 100000000
using namespace std;
int n,m,lim,e[15],g[MaxS],f[MaxS],ans;
int main()
{
scanf("%d%d",&n,&m);
lim=(1<<m);
for (int i=1;i<=n;i++)
for (int j=1,c;j<=m;j++){
scanf("%d",&c);
e[i]=e[i]<<1|(!c);
}
for (int s=0;s<lim;s++)
if (!(s&e[1])&&!(s&(s<<1)))g[s]=1;
for (int i=2;i<=n;i++){
memset(f,0,sizeof(int)*(lim+5));
for (int u=0;u<lim;u++)if (g[u])
for (int v=0;v<lim;v++)
if (!(v&(u|(v<<1)|e[i])))
f[v]=(f[v]+g[u])%mod;
memcpy(g,f,sizeof(int)*(lim+5));
}for (int i=0;i<lim;i++)
ans=(ans+g[i])%mod;
printf("%d",ans);
return 0;
}
- 缩小状态集合
考虑到题目中要求不能在相邻的土地种菜,这是一个很强的约束。
只考虑对于单独一行的效果,我们发现,大多数的
可以枚举(搜索)得到合法的状态,总共仅约
为什么状态量这么少呢?
合法的
代换一下能得到这就是个斐波那契数列……
复杂度就是
跑 n=m=16 无障碍矩阵,耗时 115ms。
#include<cstring>
#include<cstdio>
#define MaxS 405
#define mod 100000000
using namespace std;
int n,m,tn,z[MaxS],e[15],g[MaxS],f[MaxS];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
for (int j=1,c;j<=m;j++){
scanf("%d",&c);
e[i]=e[i]<<1|(!c);
}
for (int s=0;s<(1<<m);s++)
if (!(s&(s<<1)))z[++tn]=s;
for (int i=1;i<=tn;i++)
if (!(z[i]&e[1]))g[i]=1;
for (int i=2;i<=n;i++){
memset(f,0,sizeof(f));
for (int u=1;u<=tn;u++)
for (int v=1;v<=tn;v++)
if (!(z[v]&z[u])&&!(z[v]&e[i]))
f[v]=(f[v]+g[u])%mod;
memcpy(g,f,sizeof(f));
}int ans=0;
for (int i=1;i<=tn;i++)
ans=(ans+g[i])%mod;
printf("%d",ans);
return 0;
}
- 枚举子集
我们发现,从列的角度看
考虑对于每个
枚举
居然从
也就是说,我们如果能精准地枚举每个合法
容易发现,
有下列代码,可以令 s2 遍历 s 的真子集 :
for(int s2=s;s2;s2=(s2-1)&s)
这是怎么工作的呢?考虑减法的退位:
s2是s的某个子集,-1之后,会把s2的lowbit清0,然后后面的都变为1。
然后对s取与,就把不合法的退位都消除了。效果就是在s的子集内做减法。
s : 10101101
s2 : 00101000
s2-1 : 00100111
(s2-1)&s : 00100101
事实上,如果善用剪枝,复杂度会优于
跑n=m=16无障碍矩阵,耗时170ms
#include<cstring>
#include<cstdio>
#define MaxS 4500
#define mod 100000000
using namespace std;
int n,m,lim,e[15],g[MaxS],f[MaxS],ans;
int main()
{
scanf("%d%d",&n,&m);
lim=(1<<m);
for (int i=1;i<=n;i++)
for (int j=1,c;j<=m;j++){
scanf("%d",&c);
e[i]=e[i]<<1|(!c);
}
for (int s=0;s<lim;s++)
if (!(s&e[1])&&!(s&(s<<1)))g[s]=1;
for (int i=2;i<=n;i++){
memset(f,0,sizeof(int)*(lim+5));
for (int u=0;u<lim;u++)if (g[u]){
int s=(lim-1)^u;
for (int v=s;v;v=(v-1)&s)
if (!(v&((v<<1)|e[i])))
f[v]=(f[v]+g[u])%mod;
f[0]=(f[0]+g[u])%mod;
}memcpy(g,f,sizeof(int)*(lim+5));
}for (int i=0;i<lim;i++)
ans=(ans+g[i])%mod;
printf("%d",ans);
return 0;
}
两个优化共用并不会带来很大的性能提升,读者可以自行尝试。
- 轮廓线优化
前面的想法是一行行转移,我们也可以尝试填表式转移。
*****
***##
###..
.....
.....
状态可以这样记录 :
暴露出来的部分即为图中#。
这样定义状态有什么好处呢?
前面我们是一次性转移一行,目标状态十分庞大,现在是一次填写一个,目标状态就很小了。
只有
-
填0 :
f[i][1][s0]+=f[i-1][m][s0]+f[i-1][m][s1] -
填1 :
f[i][1][s1]+=f[i-1][m][s0]
-
当
j>1 时:只有
f[i][j-1][s0/s1] 能够向f[i][j][s] 贡献。-
填0 :
f[i][j][s0]+=f[i][j-1][s0]+f[i][j-1][s1] -
填1 :
f[i][j-1][s1]+=f[i][j-1][s0] (要求s0 不含第j-1 个元素)
-
-
边界
-
f[0][m][0]=1
-
容易发现,对于一个状态,只有
跑 n=m=18 无障碍矩阵,耗时 220ms ; 跑 n=m=20 无障碍矩阵,耗时 1.1s。
#include<cstring>
#include<cstdio>
#define MaxS 4500
#define mod 100000000
using namespace std;
int n,m,lim,e[15],g[MaxS],f[MaxS],ans;
int main()
{
scanf("%d%d",&n,&m);
lim=(1<<m);
for (int i=1;i<=n;i++)
for (int j=1,c;j<=m;j++){
scanf("%d",&c);
e[i]=e[i]<<1|(!c);
}
g[0]=1;
for (int i=1;i<=n;i++){
memset(f,0,sizeof(int)*(lim+3));
for (int u=0;u<lim;u++)
if (!(u&1))
f[u]=(f[u]+g[u&(u^1)]+g[u|1])%mod;
else if (!(e[i]&1))
f[u]=(f[u]+g[u&(u^1)])%mod;
memcpy(g,f,sizeof(int)*(lim+3));
for (int j=1;j<m;j++){
memset(f,0,sizeof(int)*(lim+3));
for (int u=0;u<lim;u++)
if (!(u&(1<<j)))
f[u]=(f[u]+g[u&(u^(1<<j))]+g[u|(1<<j)])%mod;
else if (!(u&(1<<(j-1)))&&!(e[i]&(1<<j)))
f[u]=(f[u]+g[u&(u^(1<<j))])%mod;
memcpy(g,f,sizeof(int)*(lim+3));
}
}
for (int i=0;i<lim;i++)
ans=(ans+g[i])%mod;
printf("%d",ans);
return 0;
}
- 缩小状态集合 + 轮廓线优化
容易发现,这两个优化可以一并使用。
考虑
除了前文的
这样单个
怎么枚举呢?可以先不考虑断点处的
然后再枚举一次,如果满足断点相邻两个是
问题在于怎样寻址,也就是说如何找到
我们可以直接开一个
然后滚动一下,避免不必要的内存开销。这样理论复杂度是
实现的时候发现时间戳好难写……就写丑了一点,复杂度是
跑 n=m=20 无障碍矩阵,耗时 220ms ; 跑 n=m=22 无障碍矩阵,耗时 670ms。
#include<algorithm>
#include<cstring>
#include<cstdio>
#define MaxS 220000
#define mod 100000000
using namespace std;
int n,m,tn,e[24],z[MaxS],
v0[24][MaxS],v1[24][MaxS],
g[MaxS],f[MaxS];
int main()
{
scanf("%d%d",&n,&m);
for (int s=0;s<(1<<m);s++){
int u=s&(s<<1);u^=u&-u;
if (!u)z[++tn]=s;
}
for (int i=1;i<=tn;i++){
for (int j=0,u,tp;j<m;j++){
u=z[i]&(z[i]^(1<<j));
tp=lower_bound(z+1,z+tn+1,u)-z;
if (z[tp]==u)v0[j][i]=tp;
u=z[i]|(1<<j);
tp=lower_bound(z+tp,z+tn+1,u)-z;
if (z[tp]==u)v1[j][i]=tp;
}
}
for (int i=1;i<=n;i++)
for (int j=1,c;j<=m;j++){
scanf("%d",&c);
e[i]=e[i]<<1|(!c);
}
g[1]=1;
for (int i=1;i<=n;i++){
for (int k=1;k<=tn;k++){
int u=z[k];
if (!(u&1))
f[k]=(g[v0[0][k]]+g[v1[0][k]])%mod;
else if (!(e[i]&1))
f[k]=g[v0[0][k]];
else f[k]=0;
}memcpy(g,f,sizeof(int)*(tn+3));
for (int j=1;j<m;j++){
for (int k=1;k<=tn;k++){
int u=z[k];
if (!(u&(1<<j)))
f[k]=(g[v0[j][k]]+g[v1[j][k]])%mod;
else if (!(u&(1<<(j-1)))&&!(e[i]&(1<<j)))
f[k]=g[v0[j][k]];
else f[k]=0;
}memcpy(g,f,sizeof(int)*(tn+3));
}
}int ans=0;
for (int i=1;i<=tn;i++)
ans=(ans+g[i])%mod;
printf("%d\n",ans);
return 0;
}
- P2704 [NOI2001]炮兵阵地
和上一题很类似,只不过相邻距离增加为2,而且求的是最大值。
这时我们就要记录两层的状态。
设
同样有naive的枚举目标状态暴力,复杂度
考虑缩小状态集合,发现在
实质上是递推式
仍然暴力枚举目标状态,复杂度就是
#include<algorithm>
#include<cstring>
#include<cstdio>
#define MaxS 75
using namespace std;
int cnt(int s){
int ret=0;
while(s){ret++;s^=s&(-s);}
return ret;
}
int n,m,las[MaxS][MaxS],f[MaxS][MaxS],tn,z[MaxS];
void dfs(int l,int s,int sum)
{
if (l>m){z[++tn]=s;return ;}
dfs(l+1,s,sum);
if ((l>2&&s&(1<<l-3))||(l>1&&s&(1<<l-2)))return ;
dfs(l+1,s+(1<<l-1),sum+1);
}
char c[205];
int ms[205];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++){
scanf("%s",c);
for (int j=0;j<m;j++)
if (c[j]=='H')
ms[i]|=1<<j;
}dfs(1,0,0);
for (int i=1;i<=n;i++){
memset(f,0,sizeof(f));
for (int j=1;j<=tn;j++)
if (!(z[j]&ms[i])){
int c=cnt(z[j]);
for (int k=1;k<=tn;k++)
if (!(z[k]&z[j])){
for (int p=1;p<=tn;p++)
if (!((z[j]&z[p])||(z[p]&z[k])))
f[k][j]=max(f[k][j],las[p][k]);
f[k][j]+=c;
}
}
memcpy(las,f,sizeof(f));
}int ans=0;
for (int i=1;i<=tn;i++)
for (int j=1;j<=tn;j++)
ans=max(ans,f[i][j]);
printf("%d\n",ans);
return 0;
}
这里因为
注意到
同样可以轮廓线优化,但是优化效果并不显著(少了一个
- P2435 染色
轮廓线DP的专业题目。
题意 : 有一个
给定第一行与最后一行的染色,求总染色方案数。
这题毒瘤之处在于,有一个点
对于 xor,可以直接
否则就是正常状压DP。
设
这里
暴力枚举目标状态转移,复杂度是
考虑减少状态量,容易发现,只考虑行的约束时,
总复杂度变为
考虑轮廓线优化,设
-
j=1 -
当
j>1 时:f[i][j-1][s']\rightarrow f[i][j][s] -
边界
-
f[1][m][$初始$]=1
-
注意我们只会对单个位进行操作,所以复杂度可以不必额外乘
复杂度就是
- P3959 宝藏
认真的子集DP,请自行忽略可以被随机化随便水过的事实……
题意 : 给出一张带权无向图。
可以钦定一个点作为起点,然后生成一棵树,每条边的代价是到起点的点数
问代价最小的生成树。
每条边的花费和到起点的点距离有关,我们可以按照BFS的方式来DP.
设BFS中,已经扩展到的点为
每次转移的时候,向外扩展的边默认
然后考虑一次性扩展点集
可以暴力枚举边集计算,复杂度
方程 :
边界是
DP的部分是
考虑对预处理进行优化,同样可以借用最优子结构。
人话说就是目标集合新增了一个点,起始集合一起来干它。
这样的复杂度就是
另外赠送一个骗分方法。
随机生成一个点的排列,然后按顺序考虑,每次找到最优的边连上。这样的暴力跑一次是
这样子,总方案数是
每次得到正解的概率是
总的复杂度就是
对于一棵分叉很多的树,可以感知
如果正解是一条链,根在一端,这样
事实上,CCF根本没给这种数据,这个骗分随机AC了……
- P5074 Eat the Trees
插头DP入门题。
考虑每个位置能铺成怎样的状态,容易发现只有如下
━ ┃ ┛ ┓ ┏ ┗
直接枚举一行的状态显然不可承受,考虑填表式轮廓线DP.
注意到我们可以只在乎插头,我们用
端点处还有一个侧向插头,需要单独记录。