轮廓线DP
算法概述
轮廓线 DP,是属于状压 DP 的一个衍生算法。它相较于普通的状压在算法复杂度上会有所优化。
它较难的衍生是插头 DP,但本文不做展开。【因为作者不会】
当我们发现问题有如下特征是就大概率可以用轮廓线 DP。
- 可以抽象成网格问题;
- 每个“格点”会受周围“格点”的约束且范围一般很小;【比如相邻】
- 某一维的范围很小,状压可以接受。
- 是个 DP 可以求解的问题。【如计数,最值等】
算法内容
例1 Corn Fields G
题目简述:给出一个
12\times 12 的网格,每个格点根据输入得知是否可选。统计合法方案数,对于一个方案而言,不存在相邻的选择格点。
显然可以用朴素的状压
::::info[朴素状压代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=1e8;
const int N=1e5+10;
int n,m,dp[15][N];
bool vis[15][15];
bool check(int x,int k)
{
vector<int>a(1);
int ld=x;
while(x)
{
a.push_back(x%2);
x/=2;
}
for(int i=1;i<a.size();i++)
{
if(!vis[k][i] && a[i])return false;
if(i>1 && a[i-1]==a[i] && a[i])return false;
}
return true;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>vis[i][j];
}
}
for(int i=0;i<(1<<m);i++)
{
if(!check(i,1))continue;
// cout<<i<<"\n";
dp[1][i]=1;
}
for(int i=2;i<=n;i++)
{
for(int j=0;j<(1<<m);j++)
{
if(!check(j,i))continue;
for(int k=0;k<(1<<m);k++)
{
if(!check(k,i-1))continue;
int x=j,y=k;
bool flag=false;
while(x&&y)
{
if(x%2 && y%2)
{
flag=true;
break;
}
x/=2,y/=2;
}
if(flag)continue;
dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;
}
}
}
int ans=0;
for(int i=0;i<(1<<m);i++)ans=(ans+dp[n][i])%mod;
cout<<ans;
return 0;
}
::::
时间复杂度应该最坏是
最基础的状压是一层一层转移的,而我们可以一格一格的转移。对于一个点
如图,橙色是
我们只需要知道它蓝色那条轮廓线的状态就可以判断当前状态和之后状态的转移了。
我们定义。
我们认为
比如当前我们选择种草,你们对于我们下一个格子他的轮廓线就是
而这么判断是否可以种草。首先输入确定这里可以种草,其次这个格子的上方和左边不能种草。【不用关心右下,因为右下种草的时候会关心当前格子】对于这里的轮廓线,判断最高位和最低位是不是有为
如果不种草则不加一,当
最后初始化就将dp[1][1][0]=1即可。因为第 dp[1][1][0]=1。
::::success[轮廓线做法代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=100000000;
int n,m;
bool vis[15][15];
int dp[15][15][1<<12];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>vis[i][j];
}
}
dp[1][1][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=0;k<(1<<m);k++)
{
dp[i][j][k]%=mod;
if(vis[i][j] && (j==1 || !(k%2)) && !(k>>(m-1)))//(i,j)种草
{
int now=(k*2+1)%(1<<m);
if(j==m)dp[i+1][1][now]+=dp[i][j][k];
else dp[i][j+1][now]+=dp[i][j][k];
}
int nwe=(k<<1)%(1<<m);
if(j==m)dp[i+1][1][nwe]+=dp[i][j][k];
else dp[i][j+1][nwe]+=dp[i][j][k];
}
}
}
int ans=0;
for(int k=0;k<(1<<m);k++)
{
// cout<<k<<" "<<dp[n+1][1][k]<<"\n";
ans=(ans+dp[n+1][1][k])%mod;
}
cout<<ans;
return 0;
}
::::
这样的时间复杂度为
::::info[测评结果对比]
直接状压
轮廓线做法
::::
例2 Mondriaan's Dream
题目简述:
1\times 2 的骨牌填n\times m(n,m\le 12) 的网格,在填满的情况下问有多少种不同的填法。
非常经典的骨牌题目,首先明确每个格子有
【看看做法就行,本文重点不在这】
::::warning[三进制状压做法]
我们发现当前需要上面依赖的格子上面只能是依赖下面的,在每次转移一层前先预处理一下可以转移到这个状态的方案数hd()函数。
【最后跑了829ms】
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+10;
int n,m,dp[15][N];
//位置为0表示为横着的纸的一部分,为1表示和下一行的形成一部分,为2表示与上面的形成一部分
struct S{
int val[25]={0};
};
S work(int itm)
{
S res;
for(int i=1;i<=m;i++)
{
res.val[i]=itm%3;
itm/=3;
}
return res;
}
void init()
{
for(int i=0;i<pow(3,m);i++)
{
S now=work(i);
bool flag=false;
int l=-1;
for(int j=1;j<=m;j++)
{
if(now.val[j]==0)
{
if(l==-1)l=j;
}
else
{
if(l!=-1)
{
if((j-l)%2!=0)
{
flag=true;
break;
}
}
l=-1;
}
if(now.val[j]==2)
{
flag=true;
break;
}
}
if(flag)continue;
if(l!=-1 && (m-l+1)%2)continue;
dp[1][i]=1;
}
}
int f[3000];
int hd(S itm)//可以和itm匹配的方案数
{
int cnt=0;
for(int k=1;k<=m;k++)
{
cnt*=2;
if(itm.val[k]==2)
{
cnt+=1;
}
}
return f[cnt];
}
void pre(int i)
{
memset(f,0,sizeof f);
for(int j=0;j<pow(3,m);j++)
{
int cnt=0;
S now=work(j);
bool flag=false;
int l=-1;
for(int k=1;k<=m;k++)
{
if(now.val[k]==0)
{
if(l==-1)l=k;
}
else
{
if(l!=-1)
{
if((k-l)%2!=0)
{
flag=true;
break;
}
}
l=-1;
}
cnt*=2;
if(now.val[k]==1)
{
cnt+=1;
}
}
if(flag)continue;
if(l!=-1 && (m-l+1)%2)continue;
f[cnt]+=dp[i][j];
}
}
void solve()
{
if((n*m)%2!=0)
{
cout<<"0\n";
return ;
}
init();
int ans=0;
for(int i=2;i<=n;i++)
{
pre(i-1);
for(int j=0;j<pow(3,m);j++)
{
S now=work(j);
bool flag=false;
int l=-1;
for(int k=1;k<=m;k++)
{
if(now.val[k]==0)
{
if(l==-1)l=k;
}
else
{
if(l!=-1)
{
if((k-l)%2!=0)
{
flag=true;
break;
}
}
l=-1;
}
}
if(flag)continue;
if(l!=-1 && (m-l+1)%2)continue;
dp[i][j]=hd(now);
}
}
for(int i=0;i<pow(3,m);i++)
{
S now=work(i);
bool flag=false;
int l=-1;
for(int k=1;k<=m;k++)
{
if(now.val[k]==1)
{
flag=true;
break;
}
if(now.val[k]==0)
{
if(l==-1)l=k;
}
else
{
if(l!=-1)
{
if((k-l)%2!=0)
{
flag=true;
break;
}
}
l=-1;
}
}
if(flag)continue;
if(l!=-1 && (m-l+1)%2)continue;
ans+=dp[n][i];
}
cout<<ans<<"\n";
return ;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
while(1)
{
cin>>n>>m;
if(n==0 && m==0)return 0;
solve();
}
return 0;
}
::::
对每个点的状态进行再一步的减少,
回顾轮廓线的思路,这个位置的上方状态如果是
一个格子只考虑它和它右边的格子形成骨牌,将这个状态转移给它右边的右边格子即可。因为这样骨牌格子转移的时候就不会包含和右边人一起形成骨牌的方案数,自然就不会受影响。
所以,当格子上放为
int now=(k<<1)%(1<<m);
if(j==m)dp[i+1][1][now]+=dp[i][j][k];
else dp[i][j+1][now]+=dp[i][j][k];
如果上面是
int now=(k<<1)%(1<<m);
if(j!=m && !(now>>(m-1)))
{
if(j==m-1) dp[i+1][1][(now<<1)%(1<<m)]+=dp[i][j][k];//横着摆一个
else dp[i][j+2][(now<<1)%(1<<m)]+=dp[i][j][k];
}
if(j!=m)dp[i][j+1][now+1]+=dp[i][j][k];//这个位置和下面的配对
else dp[i+1][1][now+1]+=dp[i][j][k];
最后答案就就是是dp[n+1][1][0]。因为最后一行不可能在和下一行的形成骨牌了。
初始化也是同理,因为第 dp[1][1][0]=1。
最后在处理一下边界即可。
::::success[轮廓线做法代码]
#include<bits/stdc++.h>
using namespace std;
#define int long long
int dp[15][15][1<<13];
void solve(int n,int m)
{
if(n*m%2)
{
cout<<"0\n";
return ;
}
memset(dp,0,sizeof dp);
dp[1][1][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=0;k<(1<<m);k++)
{
int now=(k<<1)%(1<<m);
if(k>>(m-1))//头顶上的是1
{
//只能选0和上面配对
if(j==m)dp[i+1][1][now]+=dp[i][j][k];
else dp[i][j+1][now]+=dp[i][j][k];
}
else
{
if(j!=m && !(now>>(m-1)))
{
if(j==m-1) dp[i+1][1][(now<<1)%(1<<m)]+=dp[i][j][k];//横着摆一个
else dp[i][j+2][(now<<1)%(1<<m)]+=dp[i][j][k];
}
if(j!=m)dp[i][j+1][now+1]+=dp[i][j][k];//这个位置和下面的配对
else dp[i+1][1][now+1]+=dp[i][j][k];
}
}
}
}
cout<<dp[n+1][1][0]<<"\n";
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
while(1)
{
int x,y;
cin>>x>>y;
if(x*y)solve(max(x,y),min(x,y));
else break;
}
return 0;
}
:::: 而这道题目就可以体现出轮廓线时间上的优越性。 ::::info[测评结果对比]
普通做法
轮廓线做法
::::
拓展题目
互不侵犯
增加约束状态考虑。
炮兵阵地
增加轮廓线的长度,多层考虑。