题目:
给定一个
n
×
m
n \times m
n×m的网格状土地
a
a
a,其中
a
i
,
j
a_{i,j}
ai,j为1表示这块草地可以种草,为0表示不能种草,且相邻(有公共边)的土地不能同时种草,问有多少种合法的种草方案。
(
1
≤
n
,
m
≤
12
)
(1 \le n,m \le 12)
(1≤n,m≤12)
题解:
这是一道状压 d p dp dp的简单题,但可以用多种状压 d p dp dp的技巧去解这道题。
-
解法1
令 d p i , s dp_{i,s} dpi,s表示前 i i i行且第 i i i行的种草状态为 s s s的合法方案数,首先 s s s要满足 s ∣ a i = a i s|a_i = a_i s∣ai=ai(满足土地的限制), ( s > > 1 ) & s = 0 (s>>1)\&s=0 (s>>1)&s=0(满足相邻土地不同为1的限制),可以从 d p i − 1 , s ′ dp_{i-1,s'} dpi−1,s′转移过来,其中要满足 s ′ & s = 0 s' \& s=0 s′&s=0,边界为 d p 0 , 0 = 1 dp_{0,0}=1 dp0,0=1。
复杂度: O ( n 2 m F m ) < O ( n 3. 2 m ) O(n2^mF_m)<O(n3.2^m) O(n2mFm)<O(n3.2m),其中 F m F_m Fm为斐波那契数列的第 m m m项,看似为 O ( n 4 m ) O(n4^m) O(n4m)的复杂度,其实在枚举第二维状态时要满足土地限制,那么不会有连续两个1这种状态,我们令 f i , 0 / 1 f_{i,0/1} fi,0/1表示前 i i i位且第 i i i位为0/1的合法状态数,转移为 f i , 0 = f i − 1 , 0 + f i − 1 , 1 , f i , 1 = f i − 1 , 0 f_{i,0}=f_{i-1,0}+f_{i-1,1},f_{i,1}=f_{i-1,0} fi,0=fi−1,0+fi−1,1,fi,1=fi−1,0,边界为 f 1 , 0 = f 1 , 1 = 1 f_{1,0}=f_{1,1}=1 f1,0=f1,1=1,令 f 1 , 1 = F 1 , f 1 , 0 = F 2 f_{1,1}=F_1,f_{1,0}=F_2 f1,1=F1,f1,0=F2,则 f 2 , 1 = F 3 , f 2 , 0 = F 2 f_{2,1}=F_3,f_{2,0}=F_2 f2,1=F3,f2,0=F2,以此类推, f m , 0 = F m + 1 , f m , 1 = F m f_{m,0}=F_{m+1},f_{m,1}=F_{m} fm,0=Fm+1,fm,1=Fm,所以总的状态数为 f m , 0 + f m , 1 = F m + 2 f_{m,0}+f_{m,1}=F_{m+2} fm,0+fm,1=Fm+2。
代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<string>
#include<bitset>
#include<sstream>
#include<ctime>
//#include<chrono>
//#include<random>
//#include<unordered_map>
using namespace std;
#define ll long long
#define ls o<<1
#define rs o<<1|1
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define sz(x) (int)(x).size()
#define all(x) (x).begin(),(x).end()
const double pi=acos(-1.0);
const double eps=1e-6;
const int mod=1e9;
const int INF=0x3f3f3f3f;
const int maxn=13;
ll read(){
ll 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;
}
int n,m;
int dp[maxn][1<<maxn],a[maxn][maxn],s[maxn];
int main(void){
// freopen("in.txt","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
if(a[i][j])s[i]+=1<<(j-1);
}
}
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<m);j++){
if((j>>1)&j||(j|s[i])!=s[i])continue;
for(int k=0;k<(1<<m);k++){
if(!dp[i-1][k]||k&j)continue;
dp[i][j]+=dp[i-1][k];dp[i][j]%=mod;
}
}
}
int ans=0;
for(int i=0;i<(1<<m);i++){
ans+=dp[n][i];
ans%=mod;
}
printf("%d\n",ans);
return 0;
}
-
解法2
从上述复杂度的分析可以看出,如果我们事先预处理出满足土地限制的合法状态,那么状态转移就不是 O ( 2 m ) O(2^m) O(2m)了,而是 O ( F m ) O(F_m) O(Fm)了,所以我们可以预处理出所有合法状态以后再进行转移。
复杂度: O ( n ( F m ) 2 ) < O ( n 2. 6 m ) O(n(F_m)^2)<O(n2.6^m) O(n(Fm)2)<O(n2.6m)
代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<string>
#include<bitset>
#include<sstream>
#include<ctime>
//#include<chrono>
//#include<random>
//#include<unordered_map>
using namespace std;
#define ll long long
#define ls o<<1
#define rs o<<1|1
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define sz(x) (int)(x).size()
#define all(x) (x).begin(),(x).end()
const double pi=acos(-1.0);
const double eps=1e-6;
const int mod=1e9;
const int INF=0x3f3f3f3f;
const int maxn=13;
ll read(){
ll 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;
}
int n,m;
int dp[maxn][1<<maxn],g[1<<maxn],a[maxn][maxn],st[maxn];
int main(void){
// freopen("in.txt","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
if(a[i][j])st[i]+=1<<(j-1);
}
}
int num=0;
for(int i=0;i<(1<<m);i++){
if((i>>1)&i)continue;
g[++num]=i;
}
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=num;j++){
int s=g[j];
if((s|st[i])!=st[i])continue;
for(int k=1;k<=num;k++){
int ss=g[k];
if(!dp[i-1][ss]||ss&s)continue;
dp[i][s]+=dp[i-1][ss];dp[i][s]%=mod;
}
}
}
int ans=0;
for(int i=1;i<=num;i++){
ans+=dp[n][g[i]];ans%=mod;
}
printf("%d\n",ans);
return 0;
}
-
解法3
从相邻两行的关系入手,我们观察 d p i , s dp_{i,s} dpi,s可以从哪些状态 d p i − 1 , s ′ dp_{i-1,s'} dpi−1,s′转移过来,显然 s ′ s' s′必须是 s s s的补集的子集,那么我们转移的时候可以直接枚举 s s s的补集的子集。
复杂度: O ( n 3 m ) O(n3^m) O(n3m)。令 c n t ( s ) cnt(s) cnt(s)为 s s s中为1的位数,那么对于每个 s s s,有 2 m − c n t ( s ) 2^{m-cnt(s)} 2m−cnt(s)个子集,那么总的枚举 s s s加其子集的数量为 ∑ i = 0 m ( m i ) 2 m − i = 3 m \sum_{i=0}^m \tbinom{m}{i}2^{m-i}=3^m ∑i=0m(im)2m−i=3m,所以总复杂度为 O ( n 3 m ) O(n3^m) O(n3m)。
PS: 关于精确枚举子集的实现,用以下代码即可。
代码:for(int s=s0;s;s=(s-1)&s0)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<string>
#include<bitset>
#include<sstream>
#include<ctime>
//#include<chrono>
//#include<random>
//#include<unordered_map>
using namespace std;
#define ll long long
#define ls o<<1
#define rs o<<1|1
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define sz(x) (int)(x).size()
#define all(x) (x).begin(),(x).end()
const double pi=acos(-1.0);
const double eps=1e-6;
const int mod=1e9;
const int INF=0x3f3f3f3f;
const int maxn=13;
ll read(){
ll 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;
}
int n,m;
int dp[maxn][1<<maxn],a[maxn][maxn],s[maxn];
int main(void){
// freopen("in.txt","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
if(a[i][j])s[i]+=1<<(j-1);
}
}
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<m);j++){
if((j>>1)&j||(j|s[i])!=s[i])continue;
int t=((1<<m)-1)^j;
for(int k=t;k;k=(k-1)&t){
if(!dp[i-1][k])continue;
dp[i][j]+=dp[i-1][k];dp[i][j]%=mod;
}
dp[i][j]+=dp[i-1][0];dp[i][j]%=mod;
}
}
int ans=0;
for(int i=0;i<(1<<m);i++){
ans+=dp[n][i];
ans%=mod;
}
printf("%d\n",ans);
return 0;
}
-
解法4
轮廓线 d p dp dp,我们不再一行一行地转移,这样转移的复杂度较大,考虑一个一个转移。令 d p i , j , s dp_{i,j,s} dpi,j,s为处理到第 i i i行第 j j j列且暴露出来的状态为 s s s,下图中#集合的状态即为 s s s。
++++++
+++###
###
转移方程为
d p i , 1 , s = { d p i − 1 , m , s − { j } 1 ∈ s d p i − 1 , m , s + d p i − 1 , m , s + { j } 1 ∉ s d p i , j , s = { d p i , j − 1 , s − { j } j ∈ s & & j − 1 ∉ s d p i , j − 1 , s + d p i , j − 1 , s + { j } j ∉ s j ≠ 1 dp_{i,1,s}=\left\{\begin{aligned} dp_{i-1,m,s-\{j\}}\quad 1 \in s \\ dp_{i-1,m,s}+dp_{i-1,m,s+\{j\}}\quad 1 \notin s \end{aligned}\right. \\ dp_{i,j,s}=\left\{\begin{aligned} dp_{i,j-1,s-\{j\}}\quad j \in s \&\& j-1 \notin s \\ dp_{i,j-1,s}+dp_{i,j-1,s+\{j\}}\quad j \notin s \end{aligned}\right. \quad j \ne 1 dpi,1,s={dpi−1,m,s−{j}1∈sdpi−1,m,s+dpi−1,m,s+{j}1∈/sdpi,j,s={dpi,j−1,s−{j}j∈s&&j−1∈/sdpi,j−1,s+dpi,j−1,s+{j}j∈/sj=1
转移的复杂度为 O ( 1 ) O(1) O(1)。
复杂度: O ( n m 2 m ) O(nm2^m) O(nm2m)
代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<string>
#include<bitset>
#include<sstream>
#include<ctime>
//#include<chrono>
//#include<random>
//#include<unordered_map>
using namespace std;
#define ll long long
#define ls o<<1
#define rs o<<1|1
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define sz(x) (int)(x).size()
#define all(x) (x).begin(),(x).end()
const double pi=acos(-1.0);
const double eps=1e-6;
const int mod=1e9;
const int INF=0x3f3f3f3f;
const int maxn=13;
ll read(){
ll 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;
}
int n,m;
int dp[maxn][maxn][1<<maxn],a[maxn][maxn];
int main(void){
// freopen("in.txt","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
}
}
dp[0][m][0]=1;
for(int i=1;i<=n;i++){
for(int k=0;k<(1<<m);k++){
if(k&1){
if(a[i][1]==0)continue;
dp[i][1][k]=dp[i-1][m][k^1];
}
else{
dp[i][1][k]=(dp[i-1][m][k]+dp[i-1][m][k|1])%mod;
}
}
for(int j=2;j<=m;j++){
for(int k=0;k<(1<<m);k++){
if((k>>(j-1))&1){
if(a[i][j]==0||(k>>(j-2))&1)continue;
dp[i][j][k]=dp[i][j-1][k^(1<<(j-1))];
}
else{
dp[i][j][k]=(dp[i][j-1][k]+dp[i][j-1][k|(1<<(j-1))])%mod;
}
}
}
}
int ans=0;
for(int i=0;i<(1<<m);i++){
ans+=dp[n][m][i];
ans%=mod;
}
printf("%d\n",ans);
return 0;
}