首先感谢原作者http://blog.csdn.net/shiwei408/article/details/8821853,这篇blog写的确实挺不错的
切入正题:
这三道问题具有相似性,于是我从简单的开始说起,POJ 2411
考虑大小为n×m的矩形
定义状态dp[i][s]代表当前处于第i行且第i行状态为s时的方案总数,每一位上用0表示没有放置方块,1表示有。因此初始条件是dp[0][(1<<m)-1]=1,也就是说第0行已经放满了,从第1行开始放起,实际上也就是打消了在第1行竖着放的念头。
状态转移方程是dp[i][s]=∑(ss∈K(s))dp[i-1][ss],其中K(s)是一个以01串为定义域,以01串的集合为值域的函数。K(s)={ss|第i行的状态为s时,第i-1行可以为ss}
举个例子,考虑大小为4×4的矩形,现在考虑第2行,状态为1111时,依据状态转移方程,上一行的状态ss可以是1111(上一行全都是满的,意味着这一行都是横着放),0000( 上一行都是空的,意味着这一行全都是竖着放的),0011(这一行前面两个竖着放,后一个横着放)......
因此需要一个dfs的预处理,把所有从上一行可以到下一行的状态预处理出来,然后就直接走for循环了
dfs预处理的方法是考虑3个量,第一个量是s,第二个量是now,第三个量是pre,s当前考虑第s列,now表示当前行的状态,pre表示上一行的状态
假若是把一块骨牌横着放的话,now就变成了now<<2|3,同时要求上一行也是两个1,因此pre同时也变成了pre<<2|3,s变成了s+2
考虑把骨牌竖着放的话,now变成了额now<<1|1,同时要求上一行的这一块是不放骨牌的,因此pre变成了pre<<1,s变成了s+1
考虑不放骨牌的话,now就是now<<1,但是上一行的这一块必须要有骨牌,不然以后再也没有机会填满了,因此pre变成了pre<<1|1,s变成了s+1
这一题的代码:因为POJ不支持C++11的缘故,把自己写的代码等价替换掉了。之所以有一个交换n和m的步骤是为了加速
实际上最快的方法是打表。。。。
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int maxm=1<<11;
typedef long long ll;
typedef pair<int,int> pa;
//using ll=long long;
//using pa=pair<int,int>;
int n,m,t;
vector<pa> state;
ll dp[13][maxm+10];
void dfs(int s,int now,int pre);
int main(){
ios_base::sync_with_stdio(false);
while(cin>>n>>m&&n+m){
if((n&1)&&(m&1))
cout<<0<<endl;
else{
if(n<m)
swap(n,m);
state.clear();
dfs(0,0,0);
memset(dp,0,sizeof dp);
dp[0][(1<<m)-1]=1;
for(int i=0;i<n;++i)
for(int j=0;j<state.size();++j)
//for(const auto& x:state)
dp[i+1][state[j].second]+=dp[i][state[j].first];
cout<<dp[n][(1<<m)-1]<<endl;
}
}
return 0;
}
void dfs(int s, int now, int pre){
if(s<=m){
if(s==m)
state.push_back(make_pair(pre,now));
else{
dfs(s+2,now<<2|3,pre<<2|3);
dfs(s+1,now<<1|1,pre<<1);
dfs(s+1,now<<1,pre<<1|1);
}
}
}
实际上和刚刚这题几乎一样,把列锁定为3就好,先预处理出上一行到下一行的所有状态,然后和上面一样的run就好,只不过这个可以把dfs放在main的开头,因为列数是固定的,最快的办法也是打表。。。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
typedef pair<int,int> pa;
const int maxm=1<<30;
ll dp[35][(1<<4)+10];
int n;
vector<pa> state;
void dfs(int s,int now,int pre);
int main(){
ios_base::sync_with_stdio(false);
dfs(0,0,0);
while(cin>>n&&n!=-1){
if(n&1)
cout<<0<<endl;
else{
memset(dp,0,sizeof(dp[0])*(n+1));
dp[0][(1<<3)-1]=1;
for(int i=0;i<n;++i)
for(int j=0;j<state.size();++j)
dp[i+1][state[j].second]+=dp[i][state[j].first];
cout<<dp[n][(1<<3)-1]<<endl;
}
}
return 0;
}
void dfs(int s, int now, int pre){
if(s<=3){
if(s==3)
state.push_back(make_pair(pre,now));
else{
dfs(s+2,now<<2|3,pre<<2|3);
dfs(s+1,now<<1|1,pre<<1);
dfs(s+1,now<<1,pre<<1|1);
}
}
}
POJ 3420需要稍微用一点点小技巧,即矩阵快速幂
使用矩阵快速幂的理由是状态转移方程实际上可以写成矩阵的形式,我们刚刚的转移方程是dp[i][s]=∑(ss∈K(s))dp[i-1][ss]
我们的矩阵大小是(1<<m)×(1<<m),其中matrix[i][j]表示从状态i到状态j有多少种方式
求初始矩阵的话,在dfs的时候变成matrix[pre][now]++就好
然后对初始矩阵求个n次幂,当然,用快速幂做
最后的答案是matrix[(1<<m)-1][(1<<m)-1]
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
ll n,m;
struct Matrix{
ll a[16][16];
Matrix(bool identity=false){
memset(a,0,sizeof a);
if(identity)
for(int i=0;i<16;++i)
a[i][i]=1;
}
Matrix(const Matrix& m){
for(int i=0;i<16;++i)
for(int j=0;j<16;++j)
a[i][j]=m.a[i][j];
}
Matrix operator* (const Matrix& ma){
Matrix te;
for(int i=0;i<16;++i)
for(int j=0;j<16;++j)
if(a[i][j])
for(int k=0;k<16;++k)
te.a[i][k]+=a[i][j]*ma.a[j][k],te.a[i][k]%=m;
return te;
}
Matrix qpow(int n){
Matrix ans(true),te(*this);
while(n){
if(n&1)
ans=ans*te;
te=te*te;
n>>=1;
}
return ans;
}
}ma;
void dfs(int s=0,int now=0,int pre=0);
int main(){
ios_base::sync_with_stdio(false);
dfs();
while(cin>>n>>m&&n+m)
cout<<(ma.qpow(n)).a[(1<<4)-1][(1<<4)-1]<<endl;
return 0;
}
void dfs(int s, int now, int pre){
if(s<=4)
if(s==4)
ma.a[pre][now]++;
else{
dfs(s+2,now<<2|3,pre<<2|3);
dfs(s+1,now<<1,pre<<1|1);
dfs(s+1,now<<1|1,pre<<1);
}
}
感想:不知道看到这里的人有没有看我的100道动态规划——28,那道题是一个AC自动机上的动态规划,也一样运用到了矩阵快速幂,这里也是一个动态规划,也用到的矩阵快速幂,两者的共同之处就是状态转移方程都可以写成dp(state)=∑(ss∈K(state))dp(ss),其中K(state)是一个集合,K(state)={ss| p(state)},其中p()代表一个谓词函数。对于这样的是可以同矩阵快速幂的。。。等等,我似乎想到了以前一些同样用矩阵快速幂的题目。。。在这些题目的下面一定有共同之处。只是我现在还没办法用很形式化的语言把这个特点表示出来