题目有难度。大致题意为:用1*2的小矩阵填充h*w的大矩阵,问有多少种不同的填充方案。典型的状态压缩dp题型。
最开始的思路没有想到状态压缩。而是认为普通dp,令dp[i][j]为用1*2小矩阵填充高为i,宽为j的矩阵的不同填充个数。首先填充一个小矩阵,有两种填充方案:
1)横向填充,即长度为2的边在横向。这样一个矩阵可以分解为成四部分,我们将小矩阵横向和纵向的一条线作为分解线。
那么可以将填充情况分为四部分:
11)没有穿越任何一条分界线的小矩形的填充个数
12)存在(必须)穿越第一条分界线而不穿越第二条分界线的小矩形的填充个数
13)存在(必须)穿越第二条分界线而不穿越第一条分界线的小矩形的填充个数
14)存在(必须)穿越第一条和第二条分界线的小矩形的填充个数
2)纵向填充情形和第1)种相似,这里不再介绍。但是由于第4)种情况过于特殊,实在找不到表示的方法,故最开始没有考虑第四种情况,结果WA。上述思路应该是正确的 ,只是我还没有找到表示第4)种情况的方法。故这里不再展开介绍。不过dp方程应该如下:
dp[i][j]=dp[i][j-2]*dp[i-1][2]+dp[i-1][j]*dp[1][j-2]-dp[i-1][2]*dp[i-1][j-2]*dp[1][j-2]+第四种情况表达式
+dp[i-2][j]*dp[2][j-1]+dp[i-2][1]*dp[i][j-1]-dp[i-2][1]*dp[i-2][j-1]*dp[2][j-1]+第四种情况表达式;
下面进入正题,状态压缩dp。初始接触状态压缩dp,要搞清楚。分析如下:
首先,依据题目特征,我们可以发现,大矩阵的每一行的状态只与前一行的状态有关,严格的讲是:只受前一行状态的影响。而且我们可以利用0、1表示每一行的状态。表示如下: 令目前所在行为i,所在列为j,那么如果在第i行第j列这个位置小矩形横向填充,则记第i行第j列为1,并同时记第j+1列为1,因为横向填充,则必定要相连。若纵向填充,则记第i行第j列为0,并且其后一行同一列(第j列)必须为1。这样以后,进一步可以分析如下: 设若此时在第i行第j列
1)若前一行,即i-1行,同一列(第j列)为0,则说明小矩形是纵向填充,故该列也应该为状态1
2)若前一行,即i-1行,同一列(第j列)为0,则有两种选择
第一: 可以纵向填充小矩形,这样状态为0,且没有任何限制
第二: 可以横向填充小矩形,这样状态为1,且要满足如下条件,紧接其后的(即第j+1列)必须也为1,理由很明显, 既然是横向,则长度为2,那么必须要同时为1,且此时前一行(即第i-1行)第j+1列的状态必须为1,若为0,则说明前一行为纵向填充,则与第i行第j+1列横向填充矛盾。
这样不断的递推,我们再来分析一下最后一行的小矩形状态。其实已经出来了。由于是最后一行了,所以不能纵向填充,只能横向填充,故只可能为横向填充的状态1,后者为前一行(倒数第二行)某个状态为0,(纵向填充)的补充,即上面所说的前一行为0,则后一行同一列必须为1。也即最后一行的状态必须为全1。
再来分析一下初始(第一行)的状态,第一行由于是初始行,故没有前面那么多限制状态的条件,状态比较随意。但是仍需满足如下条件:即
1)若第j列为1,则第j+1列必须也为1,理由同上述
2)若第j列为0,则第j+1列必须没有限制,可为1,也可为0
注意在上面分析的过程中,有些时候是向前进一列(例如为0),有些情况是向前进两列(例如为1),当进两列发现不够时,说明条件不满足,要舍弃。
分析到这里,好像还没有进入主题,那么dp转移方程呢?
现令dp[i][j]表示从第1行到第i行,且第i行的状态(二进制转化为十进制)为j时的不同填充个数。状态转移方程分析如下:
由于每一行都有全0到全1种不同状态(即十进制为0——2^w-1,w为列宽度),现假定第i-1行第x种状态按照上述的分析规则可以转换到第i行第j种状态,那么,很明显从第1行到第i-1行x状态有多少种不同填充个数,那么到达第i行j状态就有多少种填充个数。依次类推:则可以知道dp[i][j]即为所有第i-1行从全0到全1状态中只要能够转化为第i行j状态的dp之和。也即:dp[i][j]=∑dp[i-1][x](x可以转哈为j)。而第一行的可行状态上面已经分析,故由此写出dp代码就很容易了。
下面是一种实现代码: 996K+2110MS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Maxx 13 // h的最大值限制
#define Max 1<<Maxx // 2^w的最大值限制
__int64 dp[Maxx][Max]; // dp数组
int w,h;
bool firstline(int nStatus) //检查状态nStatus是否为第一行的合法状态
{
int i = 0;
while( i < w)
{
if(nStatus & (0x1 << i)) // 若第i+1位为1
{
if( i == w -1 || (nStatus & (0x1 << (i+1))) == 0) // 若此时已经到了w位,没有位继续扩展了,或有位继续扩展但是该位为0,则不满足横向填充条件,为不和法状态,舍弃。
{
return false;
}
i += 2; //否则跳过该位和其下一位,继续检查
}
else // 若第i+1位为0,则检查下一位
{
i++;
}
}
return true; // 否则为合法状态
}
bool match(int nStatusA, int nStatusB) // 检查前一行的nStatusA状态是否可以转化为该行的nStatusB状态
{
int i = 0;
while( i < w) //枚举位
{
if( (nStatusA & (0x1 << i)) == 0) //若前一行第i+1位为0
{
if((nStatusB & (0x1 << i)) == 0) //若同时该行第i+1位也为0,则违反条件,不能转化
{
return false;
}
i++; //否则继续检查下一位
}
else
{
if((nStatusB & (0x1 << i)) == 0 ) //若前一行第i+1为1,同时该行为0
{
i++; //则继续检查下一位
}
else if( (i == w - 1) || ! ( (nStatusA & (0x1 << (i+1))) && (nStatusB & (0x1 << (i + 1)))) )
{
return false; //若该行为1,则检查是否还可以继续扩展,若不能则说明不能转化,否则检查下一位是否也为1,若不是则说明并不能转化,最后检查前一行第i+1位是否也为1,若不能则说明不能转化
}
else // 若满足上面的所有条件,则跳过第i+1,i+2列,继续检查
{
i += 2;
}
}
}
return true; //可以转化
}
int main(){
int i,j,k;
while(scanf("%d%d",&h,&w),h){
if(w>h){ // 由分析可知,该算法的时间复杂度为O(h*w*(2^w)^2)),故要选择w较小的,减少时间复杂度,结果不影响(对称性)w*h=h*w
int temp=w;
w=h;
h=temp;
}
int range=(1<<w)-1; // 二进制w位全1转化为十进制
memset(dp,0,sizeof(dp));// 初始化全0
for(i=0;i<=range;i++) // 求解第一行初始状态的dp
if(firstline(i)) //若满足条件,则说明有一种填充情况,dp赋值为1
dp[1][i]=1;
for(i=2;i<=h;i++) // 求解dp,依次求解第i行、i+1行、i+2行、……、h行
for(j=0;j<=range;j++) // 此行j状态
for(k=0;k<=range;k++) // 上一行k状态
if(match(k,j)) // 若可以转化
dp[i][j]+=dp[i-1][k]; //则累加
printf("%I64d\n",dp[h][range]); // 最后一行合法状态为全1,输出不同填充个数,注意大数据,用64为输出
}
return 0;
}
上面的算法时间复杂度为O(h*2^w*2^w*w),故应该选择w和h较小的作为w,这样当h不是非常大而w又较小的还算行。从上面的算法中,我们可以发现,每次前一行k状态与该行j状态都要检查匹配,而且没一个i,都是同样的情况,即k与j该匹配的还是匹配,不能匹配的还是不能匹配。即j与k的匹配与i无关,所以为什么还要重复匹配,为什么不干脆在循环之前就将数子j与k的匹配情况求解出来,这样就可以每次直接循环这些匹配的数据了。复杂度大大降低。
下面是代码: 660K+16MS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 12
#define Maxx 1<<Max
struct Node{
int high,low;
}node[4*Maxx];
__int64 dp[Max][Maxx];
int w,h,num;
void dfs(int l,int now,int pre){ // 深搜模拟枚举,将所有可能的j与k的匹配情况记录在结构数组node中
if(l>w) // 若不够扩展,则不合法,直接返回
return ;
if(l==w){ // 若到达了最后位,则记录上一行状态pre,和该行状态now
node[num].low=pre;
node[num++].high=now;
return ;
}
dfs(l+2,(now<<2)|3,(pre<<2)|3); //第一种情况,横向填充,填充后为第l+2位
dfs(l+1,(now<<1)|1,pre<<1); //第二种情况,该行为1,前一行为0,纵向填充,填充后为第l+1位
dfs(l+1,now<<1,(pre<<1)|1); //第三种情况,该行为0,前一行为1,填充后为第l+1位
}
bool firstline(int j){ //检查第一行的状态j是否可行
int index=0;
while(index<w){
if((j&(1<<index))==0)
index++;
else{
if(index==w-1 || (j&(1<<(index+1)))==0)
return false;
index+=2;
}
}
return true;
}
int main(){
int i,j,range;
while(scanf("%d%d",&h,&w),h){
if(w>h){
int temp=h;
h=w;
w=temp;
}
num=0;
dfs(0,0,0);
memset(dp,0,sizeof(dp));
range=(1<<w)-1;
for(i=0;i<=range;i++)
if(firstline(i))
dp[1][i]=1;
for(i=2;i<=h;i++)
for(j=0;j<num;j++) //直接枚举表中匹配的j与k
dp[i][node[j].high]+=dp[i-1][node[j].low];
printf("%I64d\n",dp[h][range]);
}
return 0;
}
上面的算法针对第一种算法效率有所提高。主要用在h并非很大,w比较小的情况下。下面在给出一个类似的题目,使用该方法求解就可以达到瞬间AC的效果。
POJ2663
大致题意为:给定一个3*n的矩阵,现要求用1*2的小矩阵填充,问填充方式多少种?实际上就是本题的特殊情况。题目中限定n最大为30,那么就可以将源码稍微改动一下,即可AC。注意0的情况下输出为1,非常坑爹!!
下面是代码: 176K+0MS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 40
#define Maxx 1<<5
struct Node{
int high,low;
}node[4*Maxx];
__int64 dp[Max][Maxx];
int w,h,num;
void dfs(int l,int now,int pre){
if(l>w)
return ;
if(l==w){
node[num].low=pre;
node[num++].high=now;
return ;
}
dfs(l+2,(now<<2)|3,(pre<<2)|3);
dfs(l+1,(now<<1)|1,pre<<1);
dfs(l+1,now<<1,(pre<<1)|1);
}
bool firstline(int j){
int index=0;
while(index<w){
if((j&(1<<index))==0)
index++;
else{
if(index==w-1 || (j&(1<<(index+1)))==0)
return false;
index+=2;
}
}
return true;
}
int main(){
int i,j,range;
while(scanf("%d",&h),h!=-1){
if(h==0){
printf("1\n");
continue;
}
w=3;
if(w>h){
int temp=h;
h=w;
w=temp;
}
num=0;
dfs(0,0,0);
memset(dp,0,sizeof(dp));
range=(1<<w)-1;
for(i=0;i<=range;i++)
if(firstline(i))
dp[1][i]=1;
for(i=2;i<=h;i++)
for(j=0;j<num;j++)
dp[i][node[j].high]+=dp[i-1][node[j].low];
printf("%I64d\n",dp[h][range]);
}
return 0;
}
但是如果当h的值非常大时,例如接近10^9之类的大数据时,若仍采用上述方法就等着TLE吧!现在介绍一种该情况下采用的算法,矩阵快速幂+状态压缩dp。
即可以把从前一行pre到后一行now状态的转化情况存于邻接矩阵中,这样以后,只需要求解邻接矩阵的h次幂就可以了,可以采用矩阵快速幂算法,整个算法的时间复杂度大约为O(logh* 2^w)这样就可以顺利解决问题了。
以POJ3240为例:
大致题意为:用1*2的小矩阵填充4*n的矩阵,问有多少种填充方案?其中n最大可达10^9.
下面是代码:176K+0MS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 16
#define Maxx(a,b) (a)>(b)?(a):(b)
#define Min(a,b) (a)<(b)?(a):(b)
typedef struct Node{ // 邻接矩阵节点
__int64 at[Max][Max];
}node;
node dat;
int n,mod;
int min_r,max_r,range;
void dfs(int l,int now,int pre){ //dfs打表,将j与k的匹配情况存于邻接矩阵at中
if(l>min_r) return ;
if(l==min_r){
dat.at[pre][now]=1;
return ;
}
dfs(l+2,(now<<2)|3,(pre<<2)|3);
dfs(l+1,(now<<1)|1,pre<<1);
dfs(l+1,now<<1,(pre<<1)|1);
}
node epo(node a,node b){ // 矩阵a*b
node c;
memset(c.at,0,sizeof(c.at));
for(int i=0;i<range;i++)
for(int k=0;k<range;k++)
if(a.at[i][k]){ // 减少不需要的累加
for(int j=0;j<range;j++){
c.at[i][j]+=a.at[i][k]*b.at[k][j];
if(c.at[i][j]>mod) //若超过mod,则取模
c.at[i][j]%=mod;
}
}
return c;
}
node calmi(node a,int k){ //求解a矩阵的k次幂, 矩阵快速幂算法
if(k==1) //若k为1,则直接返回a矩阵
return a;
node re;
memset(re.at,0,sizeof(re.at));
for(int i=1;i<range;i++)
re.at[i][i]=1;
if(k==0) //若为0次幂,则返回单位矩阵
return re;
while(k>0){ //否则,矩阵快速幂算法,利用二进制特征加速求解,但本是O(n)的算法简化为0(logn)
if(k&1) re=epo(re,a);
a=epo(a,a);
k>>=1;
}
return re;
}
int main(){
while(~scanf("%d%d",&n,&mod),n){
if(mod==1){
printf("0\n");
continue;
}
min_r=Min(n,4); //取h与w中的小值
max_r=Maxx(n,4); // 取大值
range=1<<min_r; // 2^小值
memset(dat.at,0,sizeof(dat.at)); //初始化为0
dfs(0,0,0); // 打表
dat=calmi(dat,max_r); // dat的大值次幂
printf("%I64d\n",dat.at[range-1][range-1]); //输出结果即为at[2^小值次幂-1][2^小值次幂-1]
}
return 0;
}