动态规划——状态压缩DP——蒙德里安的梦想

状态压缩DP是我现阶段学的比较难的DP问题,其主要思想就是利用一个数来表示一种状态(通常使用二进制来表示)。

我们先来看一下题目。

在这里插入图片描述

根据做DP题的步骤,先进行状态表示。这里非常巧妙,我们设立一个 f[N][M] 的数组,其中 M 为 2^n 次方。f[i][j] 表示 i-1列的方案数已经确定,从i-1列伸出,并且第i列的状态是j的所有方案数。举个简单的例子,比如我们有5行,则我们用二进制表示5行中的空格和填充部分(0表示空格,1表示填充),则有 00000 , 00001 , 00100 … 。至于为什么要用一列来表示,是因为我们通过横放来决定,并对每一列剩余空格进行判断和筛选,找出合法的二进制数。

当我们做好了状态表示,现在该求取递推式了。本题在找递推式之前,先弄好满足条件。这一题关键在于只要我们确立了横木块的放置方法,就相当于确立了放置方法,只要我们每一列都空余出偶数个连续空位,就能填满。

在这里插入图片描述

特别注意,我们放置的是横木块的第二块,如果我们第 i 列表示填充,则同行(假设第 j 行)的两个格子 f[i - 1][j] 和 f[i][j] 构成一块横木块。

在这里插入图片描述

那如何判断当前方案是否合法呢?

第 i 列方案只与第i - 1 列有关,i=2, j=11001 表示下列图的状态

在这里插入图片描述

但是i-1, i列已经固定,所以集合划分是依据i-2 列伸到 i-1 列的不同状态 k 来划分。i-2 列伸到 i-1 列的状态 k=00100

在这里插入图片描述

在这里插入图片描述

第 i-2 列伸到 i-1 列的状态为 k , 是否能成功转移到 第 i-1 列伸到 i 列的状态为 j 。说白了就是怎么在 i - 2 以及 i - 1 列放了横木块之后,再在 i - 1 列 和第 i 列放置横木块是合法?

(1)首先肯定是不同行,即一行中只能由 i-1 列和 i 列组成的横木块,或者 i - 2 列和 i - 1 列放置的横木块,若同行,则会共用第 i - 1 列的木块。我们可以用 j & k == 0 来表示。其含义就是两个二进制数进行与操作,若两者同一位数同时为1(同一行放置了两块横木板),即为不合法,否则当两二进制数与为0,表示不从在同位为1,即满足了条件。

(2)其次,每一列,所有连续着空着的小方格必须是偶数个。这里我们用 st[ i | j ] 来表示。其中波尔(bool) st[M] 数组存储的是M种情况下,满足情况的是那些二进制数。而至于为什么是 i | j , 首先j表示 i - 2 列合法伸出的二进制表示,当 i 和 j 进行或操作,就可以把 i - 1 列的二进数表示出来。

现在我们来看一下代码

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 12 , M = 1 << N;//这里使用位运算,表示2的N次方 
int n , m;

//f[i][j]表示 i-1列的方案数已经确定,从i-1列伸出,并且第i列的状态是j的所有方案数
LL f[N][M];

bool st[M];
vector<int> state[M];

int main()
{
    
    
    while(cin >> n >> m , n || m)
    {
    
    
        for(int i = 0 ; i < 1<<n ; i++)
        {
    
    
            int cnt = 0;//连续存放0的个数 
            bool value = true;
            for(int j = 0 ; j < n; j++)
            {
    
    
                if(i >> j & 1)//如果第j位填充了小方格 
                {
    
    
                    if( cnt & 1 )//如果cnt为奇数,则这一列不能放置竖的木块 
                    {
    
    
                        value = false;
                        break;
                    }
                    cnt = 0;
                }
                else cnt++; //继续为空,不放置小方格 
            }

            if( cnt & 1 )  value = false;
            st[i] = value;
        }

        for(int i = 0 ; i < 1<<n ; i++)
        {
    
    
            state[i].clear();//由于连续输入,因此要清除原来的数据 
            for(int j = 0 ; j < 1<<n ; j++)
            {
    
       //判断是否合法:两个木块是否在同一行,以及这两行中空格数是否为偶数个 
                if( (i & j) == 0 && st[i | j] ) state[i].push_back(j); 
                //st[i|j]是为了后面转移看i-1列能不能填竖着的
            }
        }

        //初始化f 数组 
        memset(f , 0 , sizeof f);

        f[0][0] = 1;//最开始第0列,只有全部都不伸这一种情况

        for(int i = 1; i <= m ;i++)
        {
    
    
            for(int j = 0 ; j < 1<<n; j++)
            {
    
    
                for( auto k : state[j] )
                {
    
    
                    f[i][j] += f[i - 1][k];
                }
            }
        }

        cout << f[m][0] << endl;
    }
}

作者:chenxuanqi6@163.com
链接:https://www.acwing.com/activity/content/code/content/541644/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/MrChen666/article/details/109280705