状态压缩DP详细讲解

前言

在讲状压dp之前,我们应该清楚dp是解决多阶段决策最优化问题的一种思想方法,即利用各个阶段之间的关系,逐个求解,最终求得全局最优解。

我们通常需要确认原问题与子问题、动态规划状态、边界状态、状态转移方程。

动态规划多阶段一个重要的特性就是无后效性,即“未来与过去无关”。无后效性就是对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的发展。换句话说,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。

在这里插入图片描述

对于动态规划,如何定义状态是至关重要的,因为状态决定了阶段的划分,阶段的划分保证了无后效性。

在这里插入图片描述

状态压缩DP介绍

状态压缩DP其实是一种暴力的算法,因为它需要遍历每个状态,而每个状态是多个事件的集合,也就是以集合为状态,一个集合就是一个状态。集合问题一般是指数复杂度的NP问题,所以状态压缩DP的复杂度仍然是指数的,只能用于小规模问题的求解。

为了方便地同时表示一个状态的多个事件,状态一般用二进制数来表示。一个数就能表示一个状态,通常一个状态数据就是一个一串0和1组成的二进制数,每一位二进制数只有两种状态,比如说硬币的正反两面,10枚硬币的结果就可以用10位二进制数完全表示出来,每一个10位二进制数就表示了其中一种结果。

在这里插入图片描述

使用二进制数表示状态不仅缩小了数据存储空间,还能利用二进制数的位运算很方便地进行状态转移

下面列举了一些常见的二进制位的变换操作。

技巧示例代码实现
去掉最后一位(101101->10110)x >> 1
在最后加一个0(101101->1011010)x << 1
在最后加一个1(101101->1011011)x << 1 + 1
把最后一位变成1(101100->101101)x | 1
把最后一位变成0(101101->101100)x | 1 - 1
最后一位取反(101101->101100)x ^ 1
把右数第k位变成1(101001->101101,k=3)x | (1 << (k - 1))
把右数第k位变成0(101101->101001,k=3)x & ~(1 << (k - 1))
右数第k位取反(101001->101101,k=3)x ^ (1 << (k - 1))
取末k位(1101101->1101,k=5)x & (1 << k - 1)
取右数第k位(1101101->1,k=4)x >> (k - 1) & 1
把末k位变成1(101001->101111,k=4)x | (1 << k - 1)
末k位取反(101001->100110,k=4)x ^ (1 << k - 1)
把右起第一个0变成1(100101111->100111111)x | (x + 1)
把右起第一个1变成0(11011000->11010000)x & (x − 1)
把右边连续的0变成1(11011000->11011111)x | (x - 1)
把右边连续的1变成0(100101111->100100000)x & (x + 1)
取右边连续的1(100101111->1111)(x ^ (x + 1)) >> 1

例题讲解

2305. 公平分发饼干

给你一个整数数组 cookies ,其中 cookies[i] 表示在第 i 个零食包中的饼干数量。另给你一个整数 k 表示等待分发零食包的孩子数量,所有 零食包都需要分发。在同一个零食包中的所有饼干都必须分发给同一个孩子,不能分开。

分发的 不公平程度 定义为单个孩子在分发过程中能够获得饼干的最大总数。

返回所有分发的最小不公平程度。

提示:

  • 2 <= cookies.length <= 8
  • 1 <= cookies[i] <= 105
  • 2 <= k <= cookies.length

题意理解

n包具有一定饼干数量的零食分给k位小朋友,为了让能拿到最多饼干的小朋友拿到尽可能少的饼干,可理解为缩小贫富差距,求所有可行的零食分发方案中最多饼干那位小朋友最少的一种,为多少。

算法分析

如果你想先想出一套可行的零食分发算法再按部就班计算出答案,比如说使用贪心算法等,可能一辈子都解不出这道题来,因为这是一个NP类问题,即是一个可以在多项式时间内验证解的问题而目前无法在多项式时间内求出解的问题

既然我们无法给出快速求取精确解的算法,但是可以穷举所有可行解,根据题目需要选取最优解。

由于问题规模较小,我们使用穷举法枚举每一种可能结果。

对于每一种可能结果,n 包零食的分发状态需要明确,这里使用n位二进制数j来表示,共有(1 << n)种可能。

对于已经分好零食的当前 k 位小朋友,设此时 n 包零食状态为j,比方说第 k 位小朋友拿到了其中的 2 包零食,设零食状态为c,那么对于当前 k 位朋友分好零食得到的结果,等价于,已经将前 k 位小朋友分好零食,再将那 2 包零食分给第 k 位小朋友后得到的结果。也就是说,分好 k 位朋友可由分好前 k 位朋友经过决策转移而来。

对于最优的决策,我们需要比较所有可能的决策来确定,设第 k 位朋友得到的零食状态c,这里使用技巧for(int c=j;c;c=(c-1)&j)枚举所有可能决策。

对于分好前 k 位朋友的零食状态,我们可以使用位运算轻松表示为j ^ c

算法实现

class Solution {
public:
    int distributeCookies(vector<int>& cookies, int k) {
        int n=cookies.size();
        vector<int>s(1<<n); // 表示所有的零食包分发状态的不公平程度
        for(int i=0;i<n;i++){ 
            for(int j=0,c=(1<<i);j<c;j++){ // 将第i个零食包分发到所有子集
                s[j|c]=s[j]+cookies[i];
            }
        }
        vector<vector<int>>f(k,vector<int>(1<<n,INT_MAX)); // f[i][j]表示以零食包分发状态j时分给i个小朋友的最优解
        for(int i=0;i<(1<<n);i++){ // 分给一个小朋友时就是零食包分发状态的不公平程度
            f[0][i]=s[i];
        }
        for(int i=1;i<k;i++){
            for(int j=1;j<(1<<n);j++){
                for(int c=j;c;c=(c-1)&j){ // 将零食包分发状态j分给第i位小朋友
                    f[i][j]=min(f[i][j],max(f[i-1][j^c],s[c]));
                }
            }
        }
        return f[k-1][(1<<n)-1];
    }
};

容易注意到f[i][j]只涉及到f[i - 1][j]s[c]

s[c]可以通过预处理得到,

通过倒序枚举状态jf的第一个维度可以省略。

class Solution {
public:
    int distributeCookies(vector<int>& cookies, int k) {
        int n=cookies.size();
        vector<int>s(1<<n);
        for(int i=0;i<n;i++){
            for(int j=0,c=(1<<i);j<c;j++){
                s[j|c]=s[j]+cookies[i];
            }
        }
        vector<int>f(s);
        for(int i=1;i<k;i++){
            for(int j=(1<<n)-1;j;j--){
                for(int c=j;c;c=(c-1)&j){
                    f[j]=min(f[j],max(f[j^c],s[c]));
                }
            }
        }
        return f.back();
    }
};

1947. 最大兼容性评分和

有一份由 n 个问题组成的调查问卷,每个问题的答案要么是 0(no,否),要么是 1(yes,是)。

这份调查问卷被分发给 m 名学生和 m 名导师,学生和导师的编号都是从 0m - 1 。学生的答案用一个二维整数数组 students 表示,其中 students[i] 是一个整数数组,包含第 i 名学生对调查问卷给出的答案(下标从 0 开始)。导师的答案用一个二维整数数组 mentors 表示,其中 mentors[j] 是一个整数数组,包含第 j 名导师对调查问卷给出的答案(下标从 0 开始)。

每个学生都会被分配给 一名 导师,而每位导师也会分配到 一名 学生。配对的学生与导师之间的兼容性评分等于学生和导师答案相同的次数。

  • 例如,学生答案为[1, 0, 1] 而导师答案为 [0, 0, 1] ,那么他们的兼容性评分为 2 ,因为只有第二个和第三个答案相同。

请你找出最优的学生与导师的配对方案,以 最大程度上 提高 兼容性评分和

给你 studentsmentors ,返回可以得到的 最大兼容性评分和

提示:

  • m == students.length == mentors.length
  • n == students[i].length == mentors[j].length
  • 1 <= m, n <= 8
  • students[i][k]01
  • mentors[j][k]01

题意理解

  1. 求出使学生和导师答案相同的总次数最多的一种最优配对方案,然后返回总次数。
  2. m 名学生和 m 名导师一一配对,求所有配对方案中,学生和导师答案相同的最多总次数。

算法分析

由于数据量少,我们采用理解2的思路,进行优雅的暴力,枚举所有的配对方案,取最优答案。

首先预处理出每一名学生和每一名导师配对得到的兼容性评分和。

共有m对师生,为了方便处理,我们顺序地分配每一名学生,使用m位二进制数字i表示导师的分配状态,设当前分配了c位学生得到了最大兼容性评分和,可以认为是第c位学生选择了其中一位导师,剩余师生进行最优配对的结果。

对于第c位学生选择了哪一位导师,需要枚举所有可能配对进行最优决策。

剩余师生进行最优配对的结果即为当前结果的最优子结构,由之前的状态已经推出。

算法实现

class Solution {
public:
    int maxCompatibilitySum(vector<vector<int>>& students, vector<vector<int>>& mentors) {
        int m=students.size(),n=students[0].size();
        vector<vector<int>>g(m,vector<int>(m)); // g[i][j]表示学生i和导师j的兼容性评分
        // 预处理g[i][j]
        for(int i=0;i<m;i++)
            for(int j=0;j<m;j++)
                for(int k=0;k<n;k++)
                    g[i][j]+=(students[i][k]==mentors[j][k]);
        vector<int>f((1<<m)); // f[i]表示导师分配状态为i时,得到的最大兼容性评分和
        for(int i=1;i<(1<<m);i++){
            int c=__builtin_popcount(i); // c表示数字i中1的数目,即当前状态已经分配的导师数目
            for(int j=0;j<m;j++){
                if(i&(1<<j)){ // 枚举,将当前状态i的一名导师j分配给第c位学生,取兼容性评分和最大的结果 
                    f[i]=max(f[i],f[i^(1<<j)]+g[c-1][j]);
                }
            }
        }
        return f[(1<<m)-1];
    }
};

参考资料

刷题手册

数据结构与算法

有用的话点个赞吧~关注@曾续缘,持续获取更多资料。

  • 51
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我们来看一个具体的例子。假设有一个长度为 n 的数组 A,其中每个元素都是 0 或 1,现在需要求出所有长度为 k 的子串中,元素为 1 的个数的最小值。 传统的动态规划方法需要使用二维数组来记录状态,时间复杂度为 O(nk),空间复杂度为 O(nk)。而使用状态压缩dp,我们可以将状态压缩为一个长度为 n 的二进制数 i,其中第 j 位为 1 表示 A[j] 在当前子串中出现了一次或多次,为 0 则表示没有出现。因此,我们只需要使用一个一维数组 f 来记录当前状态的最小值即可。 具体实现如下: ```python def min_ones_in_k_substrings(A, k): n = len(A) f = [float('inf')] * (1 << n) f[0] = 0 for i in range(n): for j in range(1 << i): if bin(j).count('1') == k: ones = bin(j & ((1 << i) - 1)).count('1') + A[i] f[j] = min(f[j], f[j & ~(1 << i)] + ones) return f[(1 << n) - 1] ``` 其中,f[i] 表示状态为 i 时的最小值,初始化为正无穷。在状态转移时,我们枚举当前状态的所有子集 j,如果 j 中的元素个数等于 k,则计算 j 中包含的所有元素为 1 的个数 ones,然后更新 f[j] 的值为 f[j] 和 f[j - {i}] + ones 中的较小值。其中,j - {i} 表示将 j 中的第 i 位(即 A[i] 对应的位置)置为 0。 最终,我们返回状态为全集时的最小值 f[(1 << n) - 1] 即可。由于状态总数为 2^n,因此时间复杂度为 O(n^22^n),空间复杂度为 O(2^n)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值