状态压缩dp例题+分析

牵扯到选不选,而且选择达到的目标给的范围很小的时候,多半可以压缩状态。

AcWing 1243:

https://www.acwing.com/problem/content/description/1245/
在这里插入图片描述

  • 为啥用dp:最优子结构:所有口味都品尝到(1 1 1 1 1)的最小包数必定由某个状态(0 1 0 1 1) + 某一个可以把剩下的0填满的包转移过来。
  • 为什么要状态压缩:1.每个口味就两种状态 选到和没选到 -> 二进制 2.数据范围很小(口味一共20个,220)3.口味的组合可能很多,错综复杂。
  • 这里不需要关心选取的顺序问题,每个最优的合法状态肯定是由一包包糖果凑出来的,枚举的时候每个状态肯定都是最优的。
  • 为什么不需要将第几包也计入状态: 1.当前这包和前面的那些包并没有直接转移关系。(像后面学生座位的那道题就有关系)2. 并且如果第i包从第 i − 1 i-1 i1包的状态转移的话,还要先将第 i − 1 i-1 i1包已经优化的状态全部复制过去。(因为 d p ( i , s t ) dp(i,st) dp(i,st)全是初始值,而很多 s t st st已经优化过了)
  • 1.状态定义 d p ( s t ) dp(st) dp(st)为(口味)状态为 s t st st(二进制)的集合,属性是最小包数。
  • 2.状态转移 d p ( n s t ) = m i n ( d p ( s t ) + 1 , d p ( n s t ) ) ( n s t = s t ∣ t ) dp(nst)=min(dp(st)+1, dp(nst))(nst=st|t) dp(nst)=min(dp(st)+1,dp(nst))(nst=stt) 枚举每一包糖,然后枚举所有状态,用合法的状态(选择前面几包的时候能够获得的状态)与上当前这包所能填补的空位去获得新的状态,然后更新最小包数。对于集合的划分,就是到第有没有用当前这一包。其实这里不用的话,也没有去更新。用了的话,就去取最小值。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<string>
#include<map>
#include<set>
#include<unordered_map>
#include<unordered_set>
int n, m, k;
int can[100][20], dp[1 << 20];

int main(void){
    cin >> n >> m >> k;
    memset(can, 0, sizeof(can));
    memset(dp, -1, sizeof(dp));
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < k; ++j){
            cin >> can[i][j];
            can[i][j]--;
        }

    int lim = 1 << m;
    dp[0] = 0;
    for (int i = 0; i < n; ++i){
        int t = 0;
        for (int j = 0; j < k; ++j) t |= (1 << can[i][j]);
        for (int st = 0; st < lim; ++st){
            if (dp[st] == -1) continue;
            int nst = st | t;
            if (dp[nst] == -1 || dp[nst] > dp[st] + 1) dp[nst] = dp[st] + 1;
        }
    }

    cout << dp[lim - 1];
    return 0;
}

LeetCode 1125:

题目链接: https://leetcode-cn.com/problems/smallest-sufficient-team/
在这里插入图片描述
在这里插入图片描述

  • 这题和上体基本没啥区别,除了输出的时候要求选的是哪些人。
  • 那么只需要用key为状态,val为返回数组的字典即可。每次状态转移的时候,去更新当前nst映射的那个最佳人员(数组)即可。
class Solution {
private:
    unordered_map<string, int> rec;
    int dp[1 << 16 + 5];
    unordered_map<int, vector<int>> ans;
public:
    vector<int> smallestSufficientTeam(vector<string>& rS, vector<vector<string>>& peo) {
        int n = rS.size();
        for (int i = 0; i < n; ++i) rec[rS[i]] = i;
        memset(dp, -1, sizeof (dp));
        dp[0] = 0;
        int lim = 1 << n;
        
        for (int i = 0, len = peo.size(); i < len; ++i){
            const auto & arr = peo[i];
            int t = 0;
            for (const auto & skill: arr) t |= (1 << rec[skill]);
            for (int st = 0; st < lim; ++st){
                if (dp[st] == -1) continue;
                int nst = st | t;
                if (dp[nst] == -1 || dp[nst] > dp[st] + 1){
                    dp[nst] = dp[st] + 1;
                    ans[nst] = ans[st];
                    ans[nst].push_back(i);
                }
            }
        }

        return ans[lim - 1];
    }
};

LeetCode 1349

题目链接:https://leetcode-cn.com/problems/maximum-students-taking-exam/submissions/
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 为什么使用动态规划:该问题有最优子结构、子问题 ,前i行必定由前i-1行最多的 + 第i行的座位数(只不过第i行要和第i-1行不冲突)转移过来。
  • 为什么压状dp:首先因为第i行必定和第i-1行有关,行数必定在状态里,那么第i-1行的座位分布必定也需要在状态里,不然无法转移。因为这里座位数量很少,我们可以选择压缩成二进制。 不然的话,我们需要一个数组去判断每个位置是否有人。
  • 1.状态定义: d p ( i , s t ) dp(i, st) dp(i,st) 第i行,状态为st的时候,前i行的座位集合,属性为最多座位数。
  • 2.状态转移: d p ( i , s t ) = d p ( i − 1 , l s t ) + b i t s c o u n t ( s t ) dp(i,st)=dp(i-1,lst)+bitscount(st) dp(i,st)=dp(i1,lst)+bitscount(st) 当前这个状态,必定由上一行(与当前行符合条件的)状态 + 现在这行的座位数 转移过来。所以,我们首先枚举每一行,再枚举这行的每个状态,去掉不合法状态(坏掉位置坐人、相邻坐人),再去枚举上一行的状态,略去不合法状态(上一行本来就不合法、上一行和这一行冲突),然后转移。
  • 3.答案统计:由最后一行的各个状态中,取最大值。
class Solution {
private:
    static const int N = 10;
    int dp[N][1 << N];
    int bitsCount(int t){
        int cnt = 0;
        while (t){
            if (t & 1) cnt++;
            t >>= 1;
        }
        return cnt;
    }

public:
    int maxStudents(vector<vector<char>>& seats) {
        memset(dp, 0, sizeof(dp));
        int n = seats.size(), m = seats[0].size();

        for (int i = 1; i <= n; ++i){
            for (int j = 0; j < (1 << m); ++j){ // 枚举当前这一行的状态
                //当前状态 左右不能同时坐人 且要坐在合法位置上
                bool flag = true;
                for (int k = 0; k < m; ++k){ // 枚举座位,坏掉的座位不能坐人
                    if (seats[i - 1][k] == '#' && (1 & (j >> k)) ) {flag = false; break;}
                }
                
                if (!flag || (j & (j << 1)) || (j & (j >> 1))){
                    dp[i][j] = -1; continue;
                }
                
                for (int lst = 0; lst < (1 << m); ++lst){
                    if (dp[i - 1][lst] == -1) continue;
                    if ((lst >> 1) & j || (lst << 1) & j) continue;
                    dp[i][j] = max(dp[i][j], dp[i - 1][lst] + bitsCount(j));
                }

            }
        }

        int ans = 0;
        for (int st = 0; st < (1 << m); ++st) ans = max(ans, dp[n][st]);
        return ans;
    }
};

LeetCode 1434

题目链接:https://leetcode-cn.com/problems/number-of-ways-to-wear-different-hats-to-each-other/
在这里插入图片描述
在这里插入图片描述

  • 为什么用动态规划:最优子结构、重叠子问题:所有人都戴上帽子的方案数可以由(第i个人没戴帽子,其余人戴了帽子 )的方案数 + (第j个人没带,其余人戴了)的方案数(第i个人和第j个人都喜欢第h顶帽子)
  • 为什么状态压缩:这题和前面很相似:选不选,并且都选择达到的目标数据很小(每个人都戴帽子的状态),所以压缩状态。这题帽子也要记录在状态里(但是可以用滚动数组压成一维):按照之前分析的 (所有人都戴上帽子的方案数可以由(第i个人没戴帽子,其余人戴了帽子 )的方案数 + (第j个人没带,其余人戴了)的方案数(第i个人和第j个人都喜欢第h顶帽子)),第h顶的某个状态要由前h-1顶的那些状态转移过来。
  • 预处理:因为对于每顶帽子来说,我需要看它能够分给哪些人。所以新建立一个字典:key为帽子,val为可以分给的人数组。
  • 状态定义: d p ( i , s t ) dp(i,st) dp(i,st)前i顶帽子,状态为st的方案集合,属性为数量。
  • 状态转移: d p ( i , s t ) = d p ( i − 1 , s t ) + d p ( i − 1 , l s t ) ( l s t = s t ( 1 < < p ) ) dp(i,st)=dp(i-1,st)+dp(i-1,lst)(lst=st^(1<<p)) dp(i,st)=dp(i1,st)+dp(i1,lst)(lst=st(1<<p)) 枚举每顶帽子,枚举每个状态。集合的划分就由帽子每个人、给了哪些人来划分。 对于当前帽子,可以选择不给任何一个人,直接加上 d p ( i − 1 , s t ) dp(i-1,st) dp(i1,st);也可以选择给一个帽子可以发的人(当前状态那个人是需要帽子的),然后当前st^(1<<p)获得那个人还没戴帽子的状态,然后转移过来。
class Solution {
private:
    static const int MOD = 1e9 + 7;
    unordered_map<int, vector<int>> rec;
public:
    int numberWays(vector<vector<int>>& hats) {
        // 10个人
        // dp(i, j) 前i顶帽子 状态为j 的方案数
        int n = hats.size();
        int dp[41][1 << 10];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        // 第i顶帽子可以送给哪些人
        for (int i = 0; i < n; ++i){
            for (int j = 0, lim = hats[i].size(); j < lim; ++j){
                int h = hats[i][j];
                rec[h].push_back(i);
            }
        }

        for (int h = 1; h <= 40; ++h){
            for (int s = 0; s < (1 << n); ++s){
                dp[h][s] = (dp[h - 1][s] + dp[h][s]) % MOD;
                for (const int & p: rec[h]){
                    // 当前需要帽子
                    if (s & (1 << p)){
                    // 找到之前没戴帽子的状态
                        int ns = s ^ (1 << p);
                        dp[h][s] = (dp[h][s] + dp[h - 1][ns]) % MOD;
                    }
                }
            }
        }
        return dp[40][(1 << n) - 1];
   }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值