【状压dp】| AcWing 算法基础班试题总结

291. 蒙德里安的梦想

题目描述

求把N×M的棋盘分割成若干个1×2的的长方形,有多少种方案。

例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。

如下图所示:

输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数N和M。

当输入用例N=0,M=0时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
1≤N,M≤11

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205

分析

1. 怎么求?
方形可以横着摆,也可以竖着摆,把所有合理的横着的摆放方案数求出来,对应的竖着的方形直接对应放进去即可,即方案数不改变。
所以总方案数等价于求只摆放横着的方形的合理方案数

2. 怎么判断当前方案是否合法?
看当前的剩余位置能否填满竖着的方形。

可以按列来看,每一列横着摆放的方形之间连续的空着的行需要是偶数个,这样才可以插入x个完整的竖着的方形。因为一个竖着的方形占两行。

一个横着的方形占两列,我们可以假设方形都是从第i-1列伸出,到第i列的,占据两列。
然后观察每行摆放的横着的方形,假设有n行,那他就有2n种状态,从0000……0到1111……1,即从0到2n -1,共占n位,代表的含义是,为1则代表该行有一个方形,为0则代表该行没有方形。

dp[2][11001] 下标本身的含义就是如图所示:
第1列伸出到第2列的方形占据的行数是第0行、第1行、第4行。
二进制数11001 就是一种状态。

这样就可以让计算机判断当前方案是否合法了,

一、 先预处理好合理的状态进行标记,即每一列连续的空行是偶数个,状态共有0000……0到1111……1,共2n种。

二、 第i列的方形从第i-1列伸出,同时第i-1列既是起点,又是终点,是第i-2列伸出方形的终点。因此对于第i-1列方形的状态合理性,必须要考虑从第i-2列伸出的到第i-1列的方形的状态k从第i-1列伸出的到第i列的方形的状态j二者相或后的状态的合理性。即 j|k 要符合预处理的合理状态。

三、 状态k和j的方形在第i-1列不能产生冲突。即i-1列的第j行不能既作为起点又作为终点。所以 j&k==0

注意:状态j和k不需要合理,j|k合理就行。如果要求状态j和k合理反而错了。

我们要求的棋盘共有n行、m列,从下标0开始存储,即从第0行到第n-1行,从第0列到第m-1列。

状态表示:dp[i][j]的值 表示从第i-1列伸出的,到第i列的状态是j的方案数。

在求dp[i][j]时,说明以 前i - 2列 为起点伸出的方形的所有状态已经全部求好,所以前i-2列都是合理的;在求出dp[i][j]后,说明在以 前i-1列 为起点伸出的方形的状态是j 已经求好,在这种状态下前i-1列的摆放是合理的。

可以归结为:在第i列时考虑第i-1列的状态的合理性。

因此我们想求出n×m的棋盘摆放的方案数时,仅仅求到第m-1列是不够的,要多求1列,把第m列也求出来,这样才能确保0~m-1列的摆放是合理的。然后dp[m][0]就是我们要求的答案,意思是 前m-1列已经全部摆好,以m-1列为起点伸出的方形的所有状态都求出来了,但我们只需要从第m-1列到m列状态是0的方形摆放,意即从第m-1列到第m列没有伸出来方形的的所有方案,就是整个棋盘全部摆好方形的方案。

状态计算: 求每一个dp[i][j]的值,就是求从第i-1列伸出的,到第i列的状态是j的方形摆放的方案数,将其初始化为0,我们需要找出第i-2列中所有从第i-2列伸出的,到第i-1列状态时k的方形摆放,得到所有的f[i-1][k],然后加到f[i][j]上面: f [ i ] [ j ] + = f [ i − 1 ] [ k ] ; f[i][j]+=f[i-1][k]; f[i][j]+=f[i1][k];

状态j和k要满足上面提到的条件:j&k==0 && j|k是合理的

初始化情况:

第0列前面没有起点,即没有一列可以作为起点伸出方形到第0列,第0列不能作为终点,所以第0列只能作为起点。

我们假设有第-1列,那么第-1列也可以向第0列伸出2n种方形的状态,但是根据题意,只有000……00的方形的状态,即第-1列不向第0列伸出方形。
因此我们可以初始化dp[0][0]=1,其它j>0时的dp[0][j]都为0。

我们接下来从第1列开始递推,而不是从0列,求以第0列作为起点到第1列的伸出的方形的合理状态,但是递推第1列时也比较特殊,因为我们求的是第0列的合理性,而我们的递推公式仅限于该列既是起点又是终点的情况,假想一种状态j=10100,这是不合理的,即从第0列到第1列不能伸出这种状态的方形,但是当假想第-1列到第0列的状态k=01000时,j|k=11100,它就又是合理的了,这与事实不符,采取措施:

  1. 把第一列也预先初始化,我们注意到第0列到第1列伸出方形的状态j是不受k约束的实际上,因此只要j合理,就可以作为伸出的一种状态,也不用考虑冲突问题,可以取dp[1][j]=1
  2. 依然这么递推,不过只有k=0时的一次循环有意义,其它情况下,当k>0时,不管伸出方形的状态合理不合理,都是没有意义的,好在dp[0][k]=0,所以对dp[1][[j]的值没有影响,仅仅当k=0时,dp[0][0]会影响dp[1][j]的值。可能在后面找到的满足条件的状态k在第i-2列也没有伸出来对应的方形

总结,
第0列是固定的,dp[0][0]=1;
第1列的值只可能是0和1,在状态j合理的情况下,dp[1][j]=1。

后面的就可以按照常规情况考虑了,从1列开始,后面的每一列既可以作为起点,又可以作为终点,直到循环到第m列时,求出第m-1列伸到第m列方形的状态为0时的方案数时即可,这样0~m-1列就全部摆好了。

措施1就是开始就初始化两列,方法二就是只初始化一列,第1列在for循环时处理。
输出结果时,要多处理一列。假如只有一列,dp[0][j],那就要多处理一列,最后输出dp[1][0],所以当只有1列时也是要处理第1列的,所以两种措施是一样的,担心的是一列也不存在,即有0列的情况,那么措施1就不太好了,因为只要输出dp[0][0]的值就够了,但是题目规定列数一定是大于等于1的,所以二者措施都可。

算法实现

1<<n 左移n位,是n+1位,1个1,n个0,代表十进制数2n ,而我们需要的表示的n行的二进制状态是从000……0到111……1,工n位,从n个0到n个1,从0~2n -1。

措施1:

#include <iostream>
#include <cstring>

using namespace std;

const int N=12,M=1<<N; //M=2^12
long long dp[N][M];
bool st[M];

int main()
{
    
    
    int n,m; "n行m列,从0存储,第n-1行,第m-1列"
    while (cin>>n>>m,n||m) {
    
    
         memset(dp,0,sizeof dp);  "每次都要初始化"
        
     "预处理,去掉无效的状态,同时初始化第1列"
        for (int i=0;i<(1<<n);i++) {
    
    "0~2^n-1,共2^n种状态"
            int cnt=0;  st[i]=true;
            for (int j=0;j<n;j++) "0~2^n-1,n位二进制表示,下标0到n-1,进行移位"
                if (i>>j&1) {
    
    
                    if (cnt & 1) {
    
    st[i]=false; break;} "判断两个插入的行之间的空行是否是偶数"
                    cnt=0;
                } else cnt++;
            if (cnt & 1) st[i]=false;  "判断最后一次插入的行到最后一行之间的空行"
            else dp[1][i]=1; "如果状态i合理的话,用以初始化第1列"
        }
        
        "初始化第0列,假想一个第-1列向第0列伸出的状态,即什么也没有伸出。所以dp[0][0]=1,其它状态dp[0][j]=0;"
        dp[0][0]=1; "最好写上,预防m=0的情况,一列也不存在,需要输出dp[0][0]"
        
       "开始从第2列处理"
        for (int i=2;i<=m;i++) "多处理一列,第m列,第m-1列是数据输入的最后一列,求当从第m-1列伸出状态为0的方形时的方案数即可。"
            for (int j=0;j<(1<<n);j++) "枚举从第i-1列到第i列可以伸出的状态"
                for (int k=0;k<(1<<n);k++) "枚举从第i-2列到第i-1列伸出的状态"
                    if ((j&k)==0 && st[j|k] )  dp[i][j]+=dp[i-1][k]; "dp[i][j]初始化为0"
        
        printf("%lld\n",dp[m][0]);            
    }
    
    return 0;
}

措施2:

#include <iostream>
#include <cstring>

using namespace std;

const int N=12,M=1<<N; //M=2^12
long long dp[N][M];
bool st[M];

int main()
{
    
    
    int n,m; //n行m列,从0存储,第n-1行,第m-1列
    while (cin>>n>>m,n||m) {
    
    
        //预处理,去掉无效的状态
        for (int i=0;i<(1<<n);i++) {
    
    
            int cnt=0;  st[i]=true;
            for (int j=0;j<n;j++) //n位数,下标0到n-1,进行移位
                if (i>>j&1) {
    
    
                    if (cnt & 1) {
    
    st[i]=false; break;} //判断两个插入的行之间的空行是否是偶数
                    cnt=0;
                } else cnt++;
            if (cnt & 1) st[i]=false;  //判断最后一次插入的行到最后一行之间的空行    
        }
        
        //初始化第0列,假想一个第-1列向第0列伸出的状态,即什么也没有伸出。所以dp[0][0]=1,其它状态dp[0][j]=0;
        memset(dp,0,sizeof dp);
        dp[0][0]=1;
        
        //开始从第1列处理
        for (int i=1;i<=m;i++) //多处理一列,第m列,第m-1列是数据输入的最后一列,求当从第m-1列伸出状态为0的木块时的方案数即可。
            for (int j=0;j<(1<<n);j++) //枚举从第i-1列到第i列可以伸出的状态
                for (int k=0;k<(1<<n);k++) //枚举从第i-2列到第i-1列伸出的状态
                    if ((j&k)==0 && st[j|k] )  dp[i][j]+=dp[i-1][k]; //dp[i][j]为0
        
        printf("%lld\n",dp[m][0]);            
    }
    
    return 0;
}

时间复杂度
dp的时间复杂度 =状态表示× 状态转移

状态表示 f[i,j] 第一维i可取11,第二维j(二进制数)可取211 ,所以状态表示 11×211
状态转移 也是211
所以总的时间复杂度11×211×211=11×222=44×220≈4.4×107 可以过。

注意到思想和代码实现上的差距:

思想上, 对于第i-1列伸到第i列的方形的状态j,我们要考虑第i-2列伸出的方形的状态k,使其满足我们上面要求的两个条件,以此来保证第i-1列合理。
但是,实际上, 我们并没有从第i-2列伸出的方形的状态来考虑,而是枚举所有与状态j匹配的状态k,将其套在第i-2列上,这就可能存在一种情况,第i-2列并没有伸出这样的方形的状态k,因此实际的判断条件应该写成:
if (dp[i-1][k] && (j&k)==0 && st[j|k] ) dp[i][j]+=dp[i-1][k];
如果dp[i-1][k]为0,说明没有这样状态的方形从第i-2列伸到第i-1列,如果不为0,才要相加。

但是如果没有这样的状态,就算让它加上也没事,因为dp[i-1][k]==0,加上也没影响。

加一个判断条件dp[i-1][k]>0,执行时间上就加长了。

这也就解释了计算第1列时候,虽然不存在方形伸向第0列,但是我们初始化了dp[0][0]=1,说明假想了第-1列,它向第0列只伸出了状态为0的方形,其它dp[0][j]都为0,所以就算从第0列伸向第1列的状态j和假想的k>0时的状态k匹配上也没事,因为dp[0][k]的值为0,或者按照我们的思路,计算第1列时的dp[1][j]不受限于k,直接初始化即可。

优化处理

在处理时观察到,三层循环的第二层、第三层每次都枚举一样的,可以先预处理好每一种伸出方形的状态j能够匹配成功的伸出方形的状态k,这样就可以优化第三层循环了,只走一遍预处理即可。

#include <iostream>
#include <cstring>
#include <vector>

using namespace std;

const int N=12,M=1<<N;
long long dp[N][M]; "易错点一"
bool st[M];
vector<int> links[M];

int main()
{
    
    
    int n,m;
    while (cin>>n>>m,n||m) {
    
    
        "初始化第0列"
        memset(dp,0,sizeof dp);
        dp[0][0]=1;
        "预处理,去除无效状态,同时初始化第一列"
        for (int i=0;i<(1<<n);i++) {
    
       0~2^n-1,共2^n种状态
            int cnt=0; st[i]=true;
            for (int j=0;j<n;j++)  0~2^n-1,都是n位,下标从0到n-1,进行移位
                if (i>>j&1) {
    
    
                    if (cnt&1) {
    
    st[i]=false; break;}
                    cnt=0;
                } else cnt++;
            if (cnt&1) st[i]=false;
            else dp[1][i]=1;
        }
     "预处理,找到所有状态下的可以和j匹配的合法的k:j&k==0 && st[j|k]==true"
        for (int j=0;j<(1<<n);j++) {
    
    
            links[j].clear();   多组输入,需要先清空
            for (int k=0;k<(1<<n);k++)
                if ( (j&k)==0&&st[j|k] )  links[j].push_back(k);
        }       "易错点二:j&k==0"
        
     递推
        for (int i=2;i<=m;i++)
            for (int j=0;j<(1<<n);j++)
                for (auto k:links[j]) dp[i][j]+=dp[i-1][k];
                
        printf("%lld\n",dp[m][0]);        
    }
    return 0;
}

两个易错点:

  1. 虽然最多11行、11列,但是结果dp[m][0]的范围可能超过3e9,较大,建议开long long。
  2. 运算符优先级问题:j&k==0(j&k)==0不一样。

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114452891