状态压缩DP

状态压缩DP一般是基于二进制进行的,大家做这类题最好对此有所了解

状态压缩类动态规划(简称状压DP)也是一种很特殊的 DP 算法,其精髓就是将所有物品的状态(一般是选或不选,用01表示,当然也有特殊情况)压缩成一个整数,进行状态的转移并节约空间。

接下来我们来两道例题感受一下状态DP

eg:

 

分析题目:

 (注:图片来自Acwing)

 核心:我们的方案数==横着放的小方块数量

在这个题目里面我们要解决两个问题:(1)

  自己手画的将就看看 别嫌弃!!!

  假如i-2列和i-1列放的小方块和i-1和i列放的有重复的地方(如图中红绿相交的情况)就会产生冲突!

我们rp++肯定不会跟别人有冲突的说

如何解决呢?

我们可以用二进制来表示每一列的情况 如图中i-1列的情况就是1010100 我们用j来记录

所以状态表示f[i][j]:表示已经将前 i -1 列摆好,且从第i − 1 列,伸出到第 i 列的状态是 j 的所有方案。

(2)所有连续空着的的位置一定是偶数不能是奇数(如果是奇数这个位置竖着放不了)

 
状态转移:
我们可以来分析一下i-2和i-1与i-1和i列的状态转换关系;

我们用k来表示i-2伸出到i-1列的所有方案 例如(其他一个方案是01011)0代表着没有横着放方块;

加入j是i-1到i列的所有方案,那么k&j==0 意思就是不可以有相同的1不然就会出现冲突;

朴素的写法:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N;

int n, m;
long long f[N][M];
bool st[M];

int main()
{
    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 ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];

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



还有一种去掉无效状态 (更快)

#include <bits/stdc++.h>
using namespace std;


const int N = 12, M = 1<< N;  

long long f[N][M] ;//第一维是列数,第二维是所有状态

bool st[M]; //判断每种状态连续空着的位置是否是偶数个


vector<vector<int>> state(M);  //二维数组

int m, n;

int main() {

    while (cin >> n >> m, n || m) { 
  

        //对于每种状态,先预处理每列不能有奇数个连续的0

        for(int i = 0; i < (1 << n); i ++) {

            int cnt = 0 ;//记录连续的0的个数

            bool flag = true; // 某种状态没有奇数个连续的0则标记为true

            for(int j = 0; j < n; j ++) { //遍历这一列,从上到下

                 if ( (i >> j) & 1) {  
                     //i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位; 
                     
                    if (cnt & 1) { 
                    //如果是奇数(cnt &1为真)则该状态不合法
                        flag =false; break;
                    } 

                    cnt = 0; 
                 }
                 else cnt ++; //该位还是0,则统计连续0的计数器++。
            }
            if (cnt & 1)  flag = false; //最下面的那一段判断一下连续的0的个数

            st[i]  = flag; //状态i是否有奇数个连续的0的情况,输入到数组st中
        }

        
        //下面来看进一步的判断:看第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列伸出来的不冲突(不在同一行) 
               
               

                    state[j].push_back(k);  
                    
            }

        }

        //第三部分:dp

        memset(f, 0, sizeof f);  
        //全部初始化为0,因为是连续读入,这里是一个清空操作。
        //类似上面的state[j].clear()

        f[0][0] = 1 ;
        

        for (int i = 1; i <= m; i ++) { //遍历每一列:第i列合法范围是(0~m-1列)
            for (int j = 0; j < (1<<n); j ++) {  //遍历当前列(第i列)所有状态j
                for (auto k : state[j])    // 遍历第i-1列的状态k,如果“真正”可行,就转移
                    f[i][j] += f[i-1][k];    // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
            }
        }


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

    }
}   

eg:

#include<bits/stdc++.h>
using namespace std;
const int N=20,M=1<<N;
int w[N][N];
int f[M][N];
int n;
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
      for(int j=0;j<n;j++) cin>>w[i][j];
    memset(f,0x3f,sizeof f);
    f[1][0]=0;
     for (int i = 0; i < 1 << n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1)
                for (int k = 0; k < n; k ++ )
                    if (i >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1];

    return 0;
}

 这道题比上面那到题简单 就不写题解啦

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值