状态压缩DP

概念

1. 什么是状态压缩

状态压缩顾名思义便是把一大堆状态压缩成二进制数,节省了空间

2. 如何状态压缩

\(int\)类型来存储二进制数,再用位运算进行计算

例题

1. 最短Hamilton路径

题意:求一笔画一张无权图的长度的最小值

思路:通过枚举从i经过所有点到j的情况长度,求出最小值

闫氏dp分析法:

微信图片_20220508142639.png

其中\(i\)是二进制的状态,\(j\)是目前到了的点

状态转移方程:
\(f[i][j] = min(f[i][j], f[i - (1 << j)][k] + g[k][j])\)

\(i - (1 << j)​\) 表示除去\(j​\)后的所有路径 —— 1 << j用二进制表示就是\(00\cdots010\underbrace{\cdots0}_{j个0}​\)
\(f[i - (1 << j)][k] + g[k][j]​\) 就是从i到k的最短长度加上k到j的路程

码来!

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << N; // state需要的空间+1

int f[M][N], g[N][N], n; // 邻接矩阵存储图

int main()
{
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        for (int j = 0; j < n; j ++ )
            scanf("%d", &g[i][j]);
    
    memset(f, 0x3f, sizeof f); // 普通的初始化
    f[1][0] = 0; // 对于起点路程是0
    
    for(int i = 0; i < 1 << n; i++) // 枚举所有状态
        for (int j = 0; j < n; j ++ ) // 枚举所有当前点
            if(i >> j & 1) // 判断j是否走过
                for(int k = 0; k < 0; k++) // 枚举所有要经过的点
                    if(i >> k & 1) // 判断k是否走过
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + g[k][j]); // 状态转移方程
    
    printf("%d", f[(1 << n) - 1][n - 1]);
    return 0;
}

2. 蒙德里安的梦想

题意:求用1*2的长方形去填一个棋盘的方案数

思路:一列一列枚举,看每一列中有多少个合法的方案

闫氏dp分析法:

微信图片_20220508223236.png

状态转移方程:\(f[i][j] += f[i-1][k]\)

即枚举上一列的状态(k)所有的可能情况
注意:需要判断情况的合法性,如图,当从第\(i-2\)列伸到第\(i-1\)列的方块是躺着的,从第\(i-1\)列伸到第\(i\)列的方块也是躺着的时,就会重叠,也就是\((j \& k) == 0\)时才是合法的

两数进行&运算时,只有每一位相同结果才是0
\(j | k\)表示\(i-1\)\(i\)的状态和\(i-2\)\(i-1\)的状态总共有多少个1,\(st[j | k]\)则用于判断i-1这一行是否合法
不合法方案.png

暴力代码:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N;

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

int main()
{
    while(cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0; // cnt统计连续0的个数,如果有奇数个0就是不合法情况
            st[i] = true; // st[]相当于记录每一列的cnt
            for (int j = 0; j < n; j ++ )
            {
                if(i >> j & 1) // 如果i的第j位是1,也就是第j行有伸出来的方格
                {
                    if(cnt % 2 == 1)
                    {
                        st[i] = false; // 当0的个数是奇数时就记录下来这种情况不合法,也可以写为cnt & 1
                        break;
                    }
                    cnt = 0; // 判断过一段了就可以从0开始重新统计
                }
                else cnt ++; // 因为i的第j位是0所以cnt++
            }
            if(cnt % 2 == 1) st[i] = false; // 在末尾还要进行一次判断,避免错过最后一段连续的0
        }
        memset(f, 0, sizeof f); // 将所有状态初始化为0
    
        f[0][0] = 1; // 回顾集合定义,由于第-1列不存在所以自然而然地只有一种方案 —— 全立着
    
        for(int i = 1; i <= m; i++) // 枚举列
        {
            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]) // 如果是合法的
                        f[i][j] += f[i-1][k]; // 状态转移
                }
            }
        }
    
        cout << f[m][0] << endl;
        // 回顾集合定义:
        // f[m][0]代表m-1已经确定,从m-1伸向m的状态是00000,也就是没有伸向m的方块,棋盘只有0~m-1列
    }
    
    
    return 0;
}

进行一个小优化:预处理每一次的合法状态,最后直接用就好了

码来!

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

using namespace std;

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

long long f[N][M];
vector<int> state[M]; // 这个数组预处理存储合法状态
bool st[M];
int n, m;

int main()
{
    while(cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            bool &isValid = st[i]; // 引用
            isValid = true;
            for (int j = 0; j < n; j ++ )
            {
                if(i >> j & 1)
                {
                    if(cnt % 2 == 1)
                    {
                        isValid = false;
                        break;
                    }
                    cnt = 0;
                }
                else cnt ++;
            }
            if(cnt % 2 == 1) isValid = false;
        }
        
        // ========这就是优化的东西=========
        for(int i = 0; i < 1 << n; i++) // 预处理合法状态
        {
            state[i].clear(); // 每组数据的state是独立,所以每次都要清除一遍
            for(int j = 0; j < 1 << n; j++)
            {
                if((i & j) == 0 && st[i | j]) // 此处只是把原来的j和k改成了此处的i和j
                    state[i].push_back(j); // 把合法的j推进去
            }
        }
        // ========这就是优化的东西=========
        
        
        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 (auto k : state[j]) // 此处遍历之前预处理的数组
                {
                    
                    f[i][j] += f[i-1][k];
                }
            }
        }
    
        cout << f[m][0] << endl;
    }
    
    
    return 0;
}

题图也是Hamilton,谐音梗((((别打我

posted @ 2022-07-23 00:32  MoyouSayuki  阅读(141)  评论(0编辑  收藏  举报
:name :name