注释:很不要脸的把大佬的博客复制下来了,因为有些地方需要加一点注释更好直观理解,强烈推荐此人博客:https://blog.csdn.net/u013480600/article/details/19569291
POJ2411 Mondriaan's Dream
题意:
给你n*m(1<=n,m<=11)的方格矩阵,要求用1*2的多米诺骨牌去填充,问有多少种填充方法。
本题可以通过轮廓线动态规划求解:
http://blog.csdn.net/u013480600/article/details/19499899
分析:
首先我们定义如下这种填充表示方式:如果一个骨牌是横着放的,那么它所在的两个方格都填充1.如果它是竖着放的,那么它所在的两个格子中,上面的那个填0,下面的这个填1.如下图所示:
由此可以得到断言:该矩阵的骨牌摆放方法和该矩阵的二进制表示法是一一对应的。
而且如果给定了前i行的二进制形式,那么前i-1行的骨牌摆放方式就可以100%确定了。
然后我们还可以论证出,对于任意连续的两行i-1和i行来说,第i-1行的二进制表示和i行的二进制表示有一个兼容的关系。
如上图例子,图中第1行为110011 通过分析我们知道它的下一行只能是001111或者111111或001100或111100(图中出现了001111和111111作为它的下一行)。为什么呢?
首先110011中的00表示这两个位置都是竖放的,则它的下一行必然是1.然后11这个位置(有两处,分别在头和尾)只可能是横放或者是该行之前的一行竖放留下来的尾巴,所以11这个位置对应的下一行只能是11或者00.
所以对于上图来说任意两个6位二进制数都有可能存在兼容关系(如110011兼容001111或者111111或001100或111100 ,即51兼容15或63或12或60)。
我们只需要求出所有对应的兼容关系,就可以由第一行的合法二进制形式出现的次数以及它的后续兼容二进制合法值,推出最终我们要求的整个矩阵的摆放方法数。
对于DP的状态设计,我们设d[i][num]=x表示第i行摆放骨牌的二进制形式对应的十进制值为num,且从第0行到第i行这之间的所有相邻行都兼容时,有多少种摆放方式。
比如d[3][1101]=3表示当前已经确定了前三行的合法摆放方式且第3行为1101时,有多少种摆放方法。
Y |
Y |
Y |
Y |
Y |
Y |
Y |
Y |
1 |
1 |
0 |
1 |
|
|
|
|
如下例所示:2*2的矩阵有多少种骨牌摆放方法呢?
1 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
只有以上两种。我们如何通过兼容关系推出这个结果呢?
首先第一行的合法二进制值是什么?肯定不是所有的二位二进制值都合法。我们假想在第一行上面还有一个第0行,而且它的二进制值是11(为什么这个假象合法?请思考,最后一行肯定都是1),所以能和11兼容的二进制值(我们指的是能做11下一行的二进制值)就是合法的第一行二进制值。且这个11出现的次数我们定为1次,然后由1次11产生后续所有的兼容值,最后我们要求的是第二行的二进制值为11出现的次数。(为2,由由于0行11兼容 1行的11和00,且出现1次,则1行的11和00各出现1次,由于1行的11和00分别兼容2行的11且2行为尾行只要算11出现的次数即可,所以2*2矩阵总方法数为2)
所以我们现在要求各种对应的兼容关系,并且设第0行的11…111出现1次,则可以计算出最后一行的11…111出现了多少次,该值即为所求矩阵摆放骨牌的方法数。
现在我们专注于这个问题:如何求相邻两行二进制值的对应关系?可以枚举i-1行的所有二进制值情况,然后判断这个值本身是否合法,如果合法(其实只要这行是中间行所有二进制值都合法的,因为首行我们虚构了第0行为全1序列,最后一行我们只需要全1序列对应的值),再通过它推断出和它兼容的第i行二进制值(和i-1行二进制值兼容的第i行二进制为:第i-1行为0的位,第i行应为1。第i-1行为1的位,第i行为0或1,且如果第i行为1那么表示第i行此时的对应位置是横着放的,应该有偶数个连续的1才合法,只需判断i-1行中为1的位在第i行中如果也为1必须偶数个这样的1相连)。
AC代码:422MS 本代码生成兼容模式用的是穷举法
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,w;
const int maxn =15;
long long d[maxn][1<<15];
int path[5000000][2];//path[10][0]表示第11种(从0计数)兼容方式的前一行二进制形式对应的的值
//path[10][1]表示第11种(从0计数)兼容方式的后一行二进制形式对应的的值
//一共可能有(1<<11)*(1<<11)种兼容方式void get(int m)//得到具有m列的矩阵的所有对应兼容方式存入path中
{for(int i=0;i<(1<<m); i++){for(int j=0;j<(1<<m); j++){bool ok =true;//表示i与j兼容for(int k=0;k<m; k++)if(ok){if( ( i&(1<<k) )==0)//i的二进制形式第k位为0{if( (j&(1<<k))==0 )//j的二进制形式第k位为0但是不为1{ok=false;break;}}else//i的二进制形式第k位为1{if((j&(1<<k))==0)//j的二进制形式第k位为0continue;++k;if( k>=m ||((i&(1<<k)) == 0))//i的二进制形式第k+1位为0或者i根本没有第k+1位了{ok = false;break;}else //此时i有第k+1位且为1,那么就看j的第k位和k+1位的组合是否是11了。{if( (j&(1<<(k-1))) && (j&(1<<(k)))==0 )//j的k和k+1位为:10{ok = false;break;}else if ( (j&(1<<(k-1)))==0 && (j&(1<<(k))))//j的k和k+1位为:01{k--;}}}}if(ok){path[w][0]=i;path[w++][1]=j;}}}
}int main()
{while(scanf("%d%d",&n,&m)==2&&n&&m){w=0;//兼容方式总数if(m>n)swap(n,m);//始终保持n为行,m为列数,且m较小get(m);//得到所有兼容方式memset(d,0,sizeof(d));d[0][(1<<m)-1]=1;//假想的第0行且二进制形式为全1时 出现1次for(int i=0;i<n; i++)//共n行for(int j=0;j<w; j++)//每行有w种对应的兼容方式{d[i+1][path[j][1]]+=d[i][path[j][0]];//path[j][0]与path[j][1]分别指兼容的两个二进制//d[i][x]表示第i行且该行的二进制表示为X时,共有多少种摆放骨牌的方式。}printf("%I64d\n",d[n][(1<<m)-1]);}return 0;
}
对于兼容模式的生成我们还有更快捷的方法吗?由以上分析可以知道我们知道前一行的二进制形式,无法知道后一行的确切二进制形式。(因为前一行可能对应多个兼容的后一行)。我们如果知道后一行的合法二进制形式也不可以推出前一行的二进制行(因为也是1对多的关系)。
我们现在来通过模拟后一行的操作进而推断出前一行的二进制形式,因为如果后一行的摆放方式定了,那么它前面的1到i-1行的二进制形式都定了(见开头红字结论)。
断言:只要我们对当前行执行的一组模拟操作 从而生成的当前行的二进制值合法(不会长度越界),那么必定存在唯一一组二进制值作为该行的前一行与该行兼容。我们可以通过深度优先搜索生成所有可行的模拟操作,且这种模拟可以生成所有的兼容对,因为不可能有兼容对不是通过实际操作而凭空得来的。
例:在6*6矩阵中,当后一行的二进制形式为000011时,与它兼容的前一行分别为111111和111100.后一行每位进行的操作只有3种,不放(其实是下放),右放(等同于前一位的左方),上放。所以当后一行执行的操作为:不放不放不放不放右放 时 它兼容的前一行为111111。模拟操作时,二进制表示法不变,该位不放时,则该位为0,因为该位会被下面的位用上放操作来填满。右放时,该位及其右边那位都为1(如果存在右边那位的话)。上放时,该位及其上面那位都为1.因为我们只关心当前行和它的前一行,所以我们不管当前模拟位的下一位是什么。
PS:直观解释一下:
0
1 |
1 1 |
1 11 |
前一行
1 1 |
0
1 |
后一行
不放 上放 右放
当后一行执行的操作为不放不放不放不放上放上放时,它兼容的前一行为111100 。 再无其他合法的与000011兼容的前一行了。
用pre表示前一行的二进制形式,now表示后一行的二进制形式,c表示当前操作的列号。c,pre,now初值都为0.由于是从左边位模拟到最右边的位,所以对应3种操作的结果为:
当前位不放,则前行的当前位必定为1才能兼容且后行为0:c=c+1,(pre<<1)|1,now<<1
当前位上方,则前行的当前位必定为0才能兼容且后行为1:c=c+1,(pre<<1),(now<<1)|1
当前位右方,则前行的当前2位必定为1才能兼容且后行当前2位为1:c=c+2,(pre<<1)|3,(now<<1)|3
且要注意到:如果执行以上操作而使得c==m,则表明生成了一个兼容对。如果c>m,则表明生成了一非法的长度越界兼容对,要抛弃。
AC代码; 时间250MS
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,w;
const int maxn =15;
long long d[maxn][1<<15];
int path[5000000][2];//path[10][0]表示第11种(从0计数)兼容方式的前一行二进制形式对应的的值
//path[10][1]表示第11种(从0计数)兼容方式的后一行二进制形式对应的的值
//一共可能有(1<<11)*(1<<11)种兼容方式void get(int c,int pre,int now)//得到具有m列的矩阵的所有对应兼容方式存入path中
{if(c>m)return ;else if(c==m){path[w][0]=pre;path[w++][1]=now;return;}get(c+1,(pre<<1)|1,now<<1);get(c+1,(pre<<1),(now<<1)|1);get(c+2,(pre<<2)|3,(now<<2)|3);
}int main()
{while(scanf("%d%d",&n,&m)==2&&n&&m){w=0;//兼容方式总数if(m>n)swap(n,m);//始终保持n为行,m为列数,且m较小get(0,0,0);//得到所有兼容方式memset(d,0,sizeof(d));d[0][(1<<m)-1]=1;//假想的第0行且二进制形式为全1时 出现1次for(int i=0; i<n; i++)//共n行for(int j=0; j<w; j++)//每行有w种对应的兼容方式{d[i+1][path[j][1]] +=d[i][path[j][0]];//path[j][0]与path[j][1]分别指兼容的两个二进制//d[i][x]表示第i行且该行的二进制表示为X时,共有多少种摆放骨牌的方式。}printf("%I64d\n",d[n][(1<<m)-1]);}return 0;
}