状态压缩DP超详解+题型解析

状态压缩DP

前言

众所周知动态规划主要解决多阶段决策最优解问题。我们通常分析“最优子结构”、“无后效性”、“重复子问题”

这三个要素去判断这个问题是否可以用动态规划问题去解决。而在解决动态规划问题时,又要从“状态、阶段、决策”这三要素进行考虑。

以著名的 0 , 1 0,1 0,1背包问题为例,我们将物品的种类作为阶段,物品只有放与不放两种状态,从前 i − 1 i-1 i1个物品变到第 i i i个物品的这个过程叫做转移同时也是一个决策的过程。这是线性dp。

想一想,如果将 0 , 1 0,1 0,1背包问题改为请问每一种放置物品的方式其价值是多少,请问该怎么算呢?

状态压缩到底压缩了什么?

如果要计算每一种放置物品的方式其价值,因为每一种物品只有放与不放两种状态,所以 n n n种物品就有 2 n 2^n 2n种状态,如果按照原来的定义方式,至少需要 n n n个维度才能将所有的状态表示出来。例如: f [ 1 ] [ 0 ] [ 0 ] f[1][0][0] f[1][0][0]可以表示放入 A A A物品不放置物品 B , C B,C B,C时的物品价值。但当物品数量多时,显然我们不可能设置这么多的维度去表示。而我们又想到每种物品只有两种状态,分别可以用 0 , 1 0,1 0,1表示,那么 A , B , C A,B,C A,B,C这三个物品是不是就可以用一个三位二进制数进行表示了。

例如: ( 111 ) 2 (111)_2 (111)2就可以表示 A , B , C A,B,C A,B,C都放入了背包,而二进制与十进制又可以相互转换,所以 f [ ( 111 ) 2 ] = f [ ( 7 ) 10 ] f[(111)_2]=f[(7)_{10}] f[(111)2]=f[(7)10],即可表示出 A , B , C A,B,C A,B,C都放入背包后的价值。而这个就叫做状态压缩,把原来的 n n n维借助二进制压缩成了 1 1 1维。

什么时候用状态压缩呢?
情况一:全排列类型

例题:P1879 [USACO06NOV]Corn Fields G

农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

分析:

观察数据范围,再看题目,每块格子只有种草与不种草两种状态,咦,那么每一行的格子不就构成了我们喜欢 0 , 1 0,1 0,1串?那么就可以把每一行作为阶段, f [ i ] [ j ] f[i][j] f[i][j]表示在第 i i i行种草方式为 j j j时的种植方案。并且我们设种草的状态为1,荒芜的状态为0。而每种种草方式就是n个0,1的全排列。所以下一步我们就要考虑什么方式是正确的。

一般状态压缩的难点有两个,一是如何进行合法的状态转移,二是位运算的运用。看到本题,一个合法的转移应该满足以下三个条件:

1.不存在荒地上种草的情况。

2.纵向没有相邻的种草格子。

3.横向没有相邻的种草格子。

为了满足条件1,我们可以将初始状态存储在数组 c [ m ] c[m] c[m]中。设当前第 i i i行的状态为 j j j,如果 c [ i ] & j = = j c[i]\&j==j c[i]&j==j则表示不存在荒地上种草的情况。因为荒地上不能种草,所以合法的方案应该是其子集。(可以自己动手试一试)

为了满足条件2,我们设上一行的状态为 j j j,这一行的状态为 k k k,那么合法的条件是 j & k = = 0 j\&k==0 j&k==0,这样才能保证纵向没有相邻的种草格子。则有转移方程 f [ i ] [ j ] + = f [ i − 1 ] [ k ]      i f ( j & k = = 0 ) f[i][j]+=f[i-1][k]\ \ \ \ if(j\&k==0) f[i][j]+=f[i1][k]    if(j&k==0)

为了满足条件3,我们可以进行预处理。遍历所有情况,将不符合横向没有相邻格子的情况剔除。时间复杂度为$ 2^n$

最后遍历所有状态将 f [ m ] [ j ] f[m][j] f[m][j]累加即可得出答案。

难点:

分析合法状态比较难我就不说了,但是请大家看代码的时候留意一下位运算的使用。

位运算的使用

1.如何将原状态存储在一维数组 c [ i ] c[i] c[i]

2.怎么横向快速判断是否有两个1相连

代码

#include <bits/stdc++.h>
using namespace std;
const int M = 1e9;
int m, n, f[13][4096], F[13], field[13][13];
// max state: (11111111111)2 = (4095)10
bool state[4096];
int main()
{
    cin >> m >> n;
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            cin >> field[i][j];
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            F[i] = (F[i] << 1) + field[i][j];//将原状态转为二进制数 
    // F[i]: state on line i
    int MAXSTATE = 1 << n;
    for (int i = 0; i < MAXSTATE; i++)
        state[i] = ((i&(i<<1))==0) && ((i&(i>>1))==0);//预处理横向有1相连的情况 
    f[0][0] = 1;
    for (int i = 1; i <= m; i++)
        for (int j = 0; j < MAXSTATE; j++)//遍历本行的情况 
            if (state[j] && ((j & F[i]) == j))//如果横向满足并且也没有在荒地上种草 
                for (int k = 0; k < MAXSTATE; k++)//遍历上一行的情况 
                    if ((k & j) == 0)//如果纵向不相邻 
                        f[i][j] = (f[i][j] + f[i-1][k]) % M;
    int ans = 0;
    for (int i = 0; i < MAXSTATE; i++) 
        ans += f[m][i], ans %= M;
    cout << ans << endl;
    return 0;
}

例题:最短Hamilton路径

这道题也是也是一道全排列问题,但是它放在了图上。

在这里插入图片描述

可以用二进制表示出哪些点已经被访问了,哪些点没有被访问,从而求出所有点都被访问后的最小值。

情况二:放置物品型

这类题目往往会以物品摆放的形式出现,比如说在一个 n × m n\times m n×m的矩阵中放置一个十字架,长方体等,问你总共有多少种方案。往往遇到这种物品摆放类的题目,学生没有头绪。其实物品摆放的方式是有限的,但每一个格子都要考虑到,这就很符合状压dp的特点,决策有限但要把每个状态都表示出来就需要很多维度。

处理这类题目我们一般利用分行dp的思想进行处理。

蒙德里安的梦想

在这里插入图片描述

分析(摘自算法竞赛进阶指南):

在这里插入图片描述

在这里插入图片描述

难点:

这道题比较关键的点是不容易想到将每个格子上长方形的状态分为竖着的一半和其他情况。同时摆放是否符合题目要求的条件也是需要格外注意的,只有符合条件的才能进行转移。

代码

#include<iostream>
#include<cstring>

using namespace std;

//数据范围1~11
const int N = 12;
//每一列的每一个空格有两种选择,放和不放,所以是2^n
const int M = 1 << N;
//方案数比较大,所以要使用long long 类型
//f[i][j]表示 i-1列的方案数已经确定,从i-1列伸出,并且第i列的状态是j的所有方案数
long long f[N][M];
//第 i-2 列伸到 i-1 列的状态为 k , 是否能成功转移到 第 i-1 列伸到 i 列的状态为 j
//st[j|k]=true 表示能成功转移
bool st[M];
//n行m列
int n, m;

int main() {
//    预处理st数组
    while (cin >> n >> m, n || m) {
        for (int i = 0; i < 1 << n; i++) {
//            第 i-2 列伸到 i-1 列的状态为 k , 
//            能成功转移到 
//            第 i-1 列伸到 i 列的状态为 j
            st[i] = true;
//            记录一列中0的个数
            int cnt = 0;
            for (int j = 0; j < n; j++) {
//                通过位操作,i状态下j行是否放置方格,
//                0就是不放, 1就是放
                if (i >> j & 1) {
//                    如果放置小方块使得连续的空白格子数成为奇数,
//                    这样的状态就是不行的,
                    if (cnt & 1) {
                        st[i] = false;
                        break;
                    }
                }else cnt++;
//                不放置小方格
            }

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

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

//        棋盘是从第0列开始,没有-1列,所以第0列第0行,不会有延伸出来的小方块
//        没有横着摆放的小方块,所有小方块都是竖着摆放的,这种状态记录为一种方案
        f[0][0] = 1;
//        遍历每一列
        for (int i = 1; i <= m; i++) {
//            枚举i列每一种状态
            for (int j = 0; j < 1 << n; j++) {
//                枚举i-1列每一种状态
                for (int k = 0; k < 1 << n; k++) {
//                    f[i-1][k] 成功转到 f[i][j]
                    if ((j & k) == 0 && st[j | k]) {
                        f[i][j] += f[i - 1][k]; //那么这种状态下它的方案数等于之前每种k状态数目的和
                    }
                }
            }
        }
//        棋盘一共有0~m-1列
//        f[i][j]表示 前i-1列的方案数已经确定,从i-1列伸出,并且第i列的状态是j的所有方案数
//        f[m][0]表示 前m-1列的方案数已经确定,从m-1列伸出,并且第m列的状态是0的所有方案数
//        也就是m列不放小方格,前m-1列已经完全摆放好并且不伸出来的状态
        cout << f[m][0] << endl;
    }
    return 0;
}

作者:松鼠爱葡萄
链接:https://www.acwing.com/solution/content/15616/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
情况三:树与图问题

这类问题往往将问题抽象成了树或者图,尤其是抽象成树的情况,需要一层一层的扩展,就体现出来分层dp的特点。这类题目一般需要提前预处理好每个点能向下扩展一层的点有哪些,上一层能通过扩展到下一层的集合。

例题:P3959 [NOIP2017 提高组] 宝藏

在这里插入图片描述

分析:

因为最后要求所有宝藏都联通并且代价最小,很显然这是抽象成了树形结构,我们可以遍历每个节点让其为根,然后每一次扩展一层,而在扩展时需要考虑从上一层的状态K是否可以通过增加道路变成这一层的状态J,这个就需要进行预处理。

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 14,M = 1 << 12,K = 1010;
const int INF = 0x3f3f3f3f;
int f[M][N];//f[i][j]表示在状态i、树(y总所讲的树)高度为j时的最小花费
int g[M];//g[i]表示状态i中所有的点及这些点所能一步达到的所有点,也用二进制表示。
int dist[N][N];//dist[i][j]表示i号点和j号点之间边的长度,若之间没有边,则长度定义为为INF(正无穷)
int n,m;
int main(){
    cin >> n >> m;
    memset(dist,0x3f,sizeof dist);//初始化dist数组为正无穷
    for(int i = 0;i < n;i ++) dist[i][i] = 0;
    //一个点到它自身的距离一定是0,这为g数组g[i]可以表示i状态本身含有的点作下铺垫

    for(int i = 0;i < m;i ++){
        int a,b,c;
        cin >> a >> b >> c;
        a --,b --;
        //因为我们要用状态压缩,故所有点从0开始比较好
        //若从1开始,第一个点就是2的1次方,即10,明显不好统计

        dist[a][b] = dist[b][a] = min(dist[a][b],c);//防止重边
    }

    //这里初始化g数组,方便状态转移计算
    for(int i = 0;i < 1 << n;i ++){//枚举每个状态,例:当i为1010时,表示此状态有第2、4个宝藏点
        for(int j = 0;j < n;j ++)//枚举状态i中的每一位
            if(i >> j & 1){//判定状态i中第j位是否为1,即判定状态i中是否存在j这个宝藏点
                //下面三行用来判断j这个点能否一步到达的所有宝藏点
                for(int k = 0;k < n;k ++)//枚举每一个宝藏点
                    if(dist[j][k] != INF)//判断宝藏点j能否一步到达宝藏点k
                        g[i] |= 1 << k;
                        /*
                        如果能到达,由于宝藏点j包含于状态i中,故状态i能一步到达宝藏点k,g[i]中即可包含k点
                        要注意的是,当j和k相等时,dist[j][k]也满足条件,故g[i]也包含状态i中的所有点
                        */
            }
    }

    memset(f,0x3f,sizeof f);//初始化f数组为正无穷,方便统计最小值

    //"赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道",下面这行表示这个意思
    for(int i = 0;i < n;i ++) f[1 << i][0] = 0;

    //状态转移
    for(int i = 0;i < 1 << n;i ++)//枚举每个状态

        /*
        这里y总代码搞错了,j应该初始化为(i - 1) & i
        若j初始化为(i - 1),当i为10时,j为1,不是i的子集,不符合条件
        */
        for(int j = (i - 1) & i;j;j = (j - 1) & i){//枚举i的每一个子集
            if((g[j] & i) == i){//g[j]表示j能一步到达的状态,若此状态包含状态i的所有点,则j能一步到达i
                int remain = i ^ j;
                //因为j是i的子集,remain表示i中j的补集,即状态j到达状态i过程中新增的宝藏点的状态

                //下面是为j -> i新增的宝藏点找到最小边的操作,同时统计最小花费
                int cost = 0;//cost表示在状态j到达状态i过程中用到的最小花费
                for(int k = 0;k < n;k ++){//枚举remain状态每一位
                    if(remain >> k & 1){//找出remain状态中的宝藏点
                        int t = INF;//t表示remain中每一位(即新增的宝藏点)到达j中某一点的最小花费
                        for(int u = 0;u < n;u ++)//枚举j状态的每一位
                            if(j >> u & 1)//找出j状态中的宝藏点
                                t = min(t,dist[k][u]);
                        cost += t;
                    }
                }
                for(int k = 1;k < n;k ++)//枚举树的高度,因为这种状态可能出现在任何一层
                    f[i][k] = min(f[i][k],f[j][k - 1] + cost * k);//状态转移
            }           
        }

    //下面就是枚举答案最小值了
    int ans = INF;

    //最小值可能出现在任意一层,比如这道题的样例,从1号点开始挖,途径1-2,1-4,4-3,最大层是2层
    for(int i = 0;i < n;i ++) ans = min(ans,f[(1 << n) - 1][i]);//(1 << n) - 1表示全选状态,即111111...
    cout << ans;
    return 0;
}

作者:正在加载中...
链接:https://www.acwing.com/solution/content/34620/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
题单

P3052 [USACO12MAR]Cows in a Skyscraper G(想想还有什么其他方法可以做)

P3694 邦邦的大合唱站队

P2157 [SDOI2009] 学校食堂

P1896 [SCOI2005] 互不侵犯

P3226 [HNOI2012]集合选数 构造+状压

P2150 [NOI2015] 寿司晚宴

P2566 [SCOI2009]围豆豆

P4363 [九省联考 2018] 一双木棋 chess

CF53E Dead Ends

P3622 [APIO2007] 动物园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值