动态规划——状态压缩dp

状态压缩DP是通过将状态表示为二进制数,并利用位运算来优化动态规划过程的一种技术。在动态规划问题中,状态通常用于描述问题的各种情况或组合,而状态压缩则是通过二进制数来表示这些状态,从而简化状态的表示和存储,提高算法效率。

题目:291. 蒙德里安的梦想 - AcWing题库

 我们在填充整个棋盘时,若没有规律的去填充,那么状态转移将难以描述。在这里我们先横向填充完整个棋盘,所剩下的空格竖着填充完即可,横向填充的所有合法方案即是总方案数。
如何判断方案是否合法?1、横向填充时每次填充的矩阵不能有重叠。2、所留下的列的长度应为偶数,只有为偶数时,矩阵才能竖着插入剩下的空格。

在每一次横向填充时,每一行的第i - 1列都有两种选择延申 or 不延申。那么N行是否延申 有2^N种选择。我们可以用二进制来表示,1表示延申,0表示不延申。如图

10100表示第1第3行延申,第2、4、5行不延申。

闫氏dp:

状态表示:f[ i ][ j ]。集合表示前i - 1列已经摆好,从第i - 1列向第i列延申的状态为j的所有方案,上图中i = 2, j = 10100。属性:所有方案。
状态计算:既然第i列已经固定,我们来看一下第i - 2列是如何延申到第i - 1列上去的。
假设此时状态为k,那么i - 2转移到i - 1的方案数即为:f[ i - 1 ][ k ]。
此时的k需要满足什么条件呢?首先k不能与j同行,即k & i == 0(没有一行是相同的,互相错开,比如k = 01000,j = 10100)。即:相邻状态的延申不能同行。(若是f[ i - 2 ][ k ]则合法)
此时k = 10000,而j = 10100,有重叠。那么k & j = 10000 = 16,不为0。所以不合法
 

 接着每一行延申出来的长方形之间所留的空隙都应为偶数,这样才能使长方形能竖着塞到剩余的空间里,那么上面两种状态都是不合法的。
合法如右图行与行之间不冲突(00100 & 10000 == 0)。f[ 1 ][ 4 ]与f[ 3 ][ 16 ]长方形所空隙为偶数。(00100,10000都为二进制下,转化到十进制分别为4,16)。红色与绿色不是由同一列延伸出来的,它俩之间的空隙没有参考价值

 代码:

//先放横着的,空余位置塞竖着的长方形。

//预处理出来可以有哪些状态更新到j
#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 12, M = 1 << N;

int n, m;
//N行,状态M种
LL f[N][M];
//所有能转移的合法状态,先预处理出来
vector<int> state[M];
//判断是否合法,即判断当前一列的空着的连续小方格块是否为偶数
bool st[M];

int main()
{

    while(cin >> n >> m, n || m)
    {
        //预处理出合法方案。即判断每种状态下的连续的0是否为偶数个
        for(int i = 0; i < 1 << n; i ++ )
        {
            //cnt表示连续的0的个数
            int cnt = 0;
            bool isvaild = true;
            //遍历每一行
            for(int j = 0; j < n; j ++ )//判断该列从第1行到第n行的0的个数
                if(i >> j & 1)//如果当前数位等于1的话,判断当前行之前0的个数是否为偶数,偶数合法、奇数不合法
                {
                    if(cnt % 2)//奇数不合法,直接break掉
                    {
                        isvaild = false;
                        break;
                    }
//如果是偶数的话,那么合法,继续判断下面的行。既然要去判断下面的行,那么cnt置于0,便于下次判断
                    cnt = 0;
                }
                else cnt ++ ;//当前数位是0,则计数器 ++ 。
                
            if(cnt % 2) isvaild = false;//如果最后连续的0为奇数,则不合法
            st[i] = isvaild;//存下该种状态是否合法
        }
        
        
        //枚举一下所有的合法状态
        //先枚举i - 1到i的所有合法状态
        for(int i = 0; i < 1 << n; i ++ )
        {
            //清空上一个合法状态的集合
            state[i].clear();
            //再枚举i - 2到i - 1的合法状态
            for(int j = 0; j < 1 << n; j ++ )
                if(!(i & j) && st[i | j] ) 
                    state[i].push_back(j);//合法的话,将j放入state[i]中
        /* 
        i|j的含义:二进制下
        当由同一列转化而来的两种状态,即i = 1001,j = 0110时,i & j == 0两种状态不同行,合法。
        且数位是0的个数为偶数。i | j =1111,所含0的个数奇数,不合法。所以总的来说不合法。
        (上图中的绿色与红色就是由不同的列转化而来的,二者之间的间隙没有参考性)
        */
        }
        
        //边读入便操作,故清空上一个动态规划的状态
        memset(f, 0, sizeof f);
        f[0][0] = 1;//f[0][0]表示前-1列已经摆好,且从-1列延伸到第0列的所有方案数。没有第-1列,故只能竖着放,方案数为1
        //枚举每一列
        //没有第-1列,所以不能从第0列开始枚举
        for(int i = 1; i <= m; i ++ )
        //i - 1到第i列的方案
            for(int j = 0; j < 1 << n; j ++ )
            //第i - 2到第i - 1的方案
                for(auto k : state[j])
                //现在的方案等于之前每种合法方案的总和
                    f[i][j] += f[i - 1][k];

        //f[m][0]表示前m - 1列已经摆好,且从m - 1列到第m列的状态为0的所有方案数的总和。由于状态为0等于不伸,
        //所以即为前m - 1列的所有的方案数,即整个棋盘方案数的总和          
        cout << f[m][0] << endl;
    }
    
    return 0;
}

题目:91. 最短Hamilton路径 - AcWing题库

思路:

题目的含义:起点为0,终点为n - 1。经过0 ~ n - 1中的所有点且不重不漏,使总路径的权值最小。
映射到实际问题上:一个旅人,要游玩a、b、c、d国,且起点只能为a国,终点只能能为d国(且只能乘坐飞机),问如何计划路线使总费用最少?
相关机票的权重:
                        a -> b,10 ;a -> c, 5 ;a -> d, 3;
                        b -> a,10 ;b -> c, 2 ;b -> d, 4;
                        c -> a, 5 ; c -> b, 2 ;c -> d, 1;
由于起点是a国 终点是d国 那么我们有的方案数为2种,分别为:
                        ①a -> b -> c -> d。总费用=10 + 2 + 1 = 13
                        ②a -> c -> b -> d。总费用=5 + 2 + 4   = 11
显而易见,旅人经过深思熟虑后 会选择第②种乘坐方式,使得总机票费用最少且起点为a国,终点为b国,不重不漏得经过了四个国家。

回到本题:起点为0,终点为n - 1。
我们以样例来进行模拟,那么会产生(5 - 2)! = 6种遍历情况:
        ①:0 -> 1 -> 2 -> 3 -> 4。权重为:2 + 6 + 8 + 5 = 23。
        ②:0 -> 2 -> 1 -> 3 -> 4。权重为:4 + 6 + 5 + 5 = 20。
        ③:0 -> 1 -> 3 -> 2 -> 4。权重为:2 + 5 + 8 + 3 = 18。
        ④:0 -> 3 -> 1 -> 2 -> 4。权重为:5 + 5 + 6 + 3 = 19。
        ⑤:0 -> 2 -> 3 -> 1 -> 4。权重为:4 + 8 + 5 + 3 = 20。
        ⑥:0 -> 3 -> 2 -> 1 -> 4。权重为:5 + 8 + 6 + 3 = 22。
那么以0为起点,终点为4且不重不漏的经过0~4的所有路径的权重如上。最小的权重为18,也就是选择③这条路径。
我们来观察以上路径来寻找状态是如何进行转移。在我们抵达终点4终点过程中,我们不需要考虑经过点的顺序是怎样,我们只需要考虑0~4中的点是否被不重不漏的被遍历过,然后取一个最小值即可;在我们抵达终点4之前的一个点可能为:1、2、3,假若我们选择3作为4之前的一个点,同样的,我们不需要考虑是如何从0抵达3的(不需要考虑顺序),我们只需要考虑0~3中的点是否被不重不漏的遍历过即可;同样的当我们选定抵达3之前的一个点为2时.............
题意里图中的点不会超过20个点,那么我们可以用二进制1/0来表示当前这个点是/否被遍历过。
若有路径:0 -> 2 -> 3,其1、4号点没有被遍历,则其二进制下的表示即为:01101
用state来表示会遍历的点集,j来表示所遍历的终点。

闫氏dp法:
        状态表示:f[ state ][ j ]。集合:从起点不重不漏的经过点集state,抵达终点 j 的总权值。属性:MIN
        状态转移:当前 j 状态的权值f[ state ][ j ] = 该终点前的一点k的权值 + w[k][ j ]。而k的权值 = 
f[ state ^ (1 << j)][ k ]。state^(1 << j)的含义为:去除终点 j。由于以点k为终点的点集不包含 j ,所以需要将之前点集state里的 j 去除,假设之前state为111111,终点 j 为6,想要去除点6将state变成011111,通过抑或运算state^(1 << j)即可。w[k][ j ]的含义为k指向j的权值。
        综上状态转移方程为:f[ state ][ k ] = f[ state ^ (1 << j) ][ k ] + w[ k ][ j ]
最后输出值的点集:遍历所有点,以n - 1为终点。即state = 111111...(n个1)=(1 << n) - 1。
所以答案为:f[ (1 << n) - 1 ][ n - 1 ]

时间复杂度:状态转移×状态计算 = (2 ^ n * n) * n = n ^ 2 * 2 ^ n。

代码:
 

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

using namespace std;

const int N = 20, M = 1 << N;//点,不同状态

int n;
int f[M][N];//状态
int w[N][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, 0x3f3f3f3f, sizeof f);//因为是取得最小值,那么初始化为正无穷
    f[1][0] = 0;//点集为1,0为终点,只有点集为0是成立0 -> 0所以权值为0
    
    for(int i = 0; i < 1 << n; i ++ )//枚举所有点集
        for(int j = 0; j < n; j ++ )//枚举终点为j得情况
            if(i >> j & 1)//既然是以j作为终点,那么终点应该为j,i >> j & 1 == 1表示终点为j,否则不为j(为空)
                for(int k = 0; k < n; k ++ )//枚举以k作为终点的情况
                    if(i >> k & 1)//k为终点时才有更新价值
                        f[i][j] = min(f[i][j],f[(1 << j) ^ i][k] + w[k][j]);//取一个最小值
                        
    //公式
    cout << f[(1 << n) - 1][n - 1] << endl;
    
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值