蒙德里安的梦想( 动态规划 + 状态压缩dp )

求把 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

这个解析写的好清楚啊!!!%%%%%orz 

Acwing291. 蒙德里安的梦想:状态压缩dp_阿正的梦工坊的博客-CSDN博客

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N;
LL f[N][M]; //第一维表示列,第二维表示所有可能的状态
bool st[M];//存储每种状态是否有奇数个连续的0,如果是为无效状态,为偶数个0位true
int m, n;
vector<vector<int>> state(M);//二维数组记录合法状态
//等价于vector<int> statep[M];
int main(){
    while(cin >> n >> m, n || m){//判断n,m均不为0则为合法输入,继续读入
        //预处理1
        //对于每一种状态,先预处理每列不能有奇数个连续的0
        for(int i = 0; i < (1 << n); i ++ ){
            int cnt = 0;//记录连续的0的个数
            bool isvalid = true;//某种状态没有奇数个连续的0则标记为true
            for(int j = 0; j < n; j ++ ){//从上到下对列进行遍历
                if((i >> j) & 1){
                    //表示i(i此时表示一种状态)的二进制数的第j位
                    //判断该位是否为1,是则进入if
                    if(cnt & 1){
                        //奇数(cnt &1为真)则该状态不合法
                        isvalid = false;
                        break;
                    }
                    cnt =0;//既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。
                    //不清没影响
                }
                else cnt ++ ;
            }
            if(cnt & 1){//对最下面那一段判断一下连续的0的个数
                isvalid  = false;
            }
            st[i] = isvalid;//状态i是否有奇数个连续的0的情况,输入到st数组中
        }
        //预处理2
        //在预处理1筛选一些状态后进行下一步判断
        //判断第i - 2列伸出来的部分与i - 1列伸出去的是否有冲突
        for(int j = 0; j < (1 << n); j ++ ){//对于第i列的所有状态
            state[j].clear();//清空上次操作遗留的状态,防止影响本次状态
            for(int k = 0; k < (1 << n); k ++ ){//对于第i- 1列所有状态
                if((j & k) == 0 && st[j | k]){
                    //第i- 2列伸出来的和i- 1列伸出来的不冲突(不在同一行
                    //st[]数组表示这一列没有连续个奇数0的情况
                    //考虑到第i- 1列(第i- 1列是主体)中是从第i- 2列横插过来的
                    //而第i- 1列有会横插到第i列
                    //j | k 表示的就是当前第i- 1列有几个1,即哪几行是有格子的
                    state[j].push_back(k);
                    //二维数组state[j]表示第j行
                    //j表示第i列“真正”可行的状态
                    //如果第i- 1列的状态k和j不冲突就压入state数组中的第j行
                    //“真正”指的是既没有前后两列伸进伸出的冲突,有没有连续奇数个0
                }
            }
        }
        //处理3:dp
        memset(f, 0, sizeof f);
        //因为是一个连续的读入,此处是类似于上面的state[j].clesr()的清空操作
        f[0][0] = 1;//回忆状态表示定义
        //前第i- 1列都摆好,且从-1列到第0列伸出来的状态为0的方案数
        //因为最少也是0列,所以不存在-1列
        //没有伸出来也没有横着摆的:这里第0列只有竖着摆着一种状态
        for(int i = 1; i <= m; i ++ ){//遍历每一列,第i列合法的范围为0~m-1列
            for(int j = 0; j < (1 << n); j ++ ){//遍历当前列的所有状态j
                for(auto k : state[j]){//遍历第i- 1列的状态k,如果真正可行就转移
                    f[i][j] += f[i - 1][k];//当前列的方案数就等于之前的第i- 1列所有状态k的累加
                }
            }
        }
        cout << f[m][0] << endl;//表示前m-1列都处理完,且第m-1列没有伸出来的方案数
    }
    return 0;
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值