poj2411

11 篇文章 0 订阅

题目有难度。大致题意为:用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;
}
		
		


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值