状态压缩DP(子集DP)经典题目

状态压缩DP(子集DP)

Leeetcode 1986. 完成任务的最少工作时间段

题意

链接:https://leetcode-cn.com/problems/minimum-number-of-work-sessions-to-finish-the-tasks

你被安排了 n 个任务。任务需要花费的时间用长度为 n 的整数数组 tasks 表示,第 i 个任务需要花费 tasks[i] 小时完成。一个 工作时间段 中,你可以 至多 连续工作 sessionTime 个小时,然后休息一会儿。

你需要按照如下条件完成给定任务:

  • 如果你在某一个时间段开始一个任务,你需要在 同一个 时间段完成它。
  • 完成一个任务后,你可以 立马 开始一个新的任务。
  • 你可以按 任意顺序 完成任务。

给你 tasks 和 sessionTime ,请你按照上述要求,返回完成所有任务所需要的 最少 数目的 工作时间段 。

测试数据保证 sessionTime 大于等于 tasks[i] 中的 最大值 。

示例 1:

输入:tasks = [1,2,3], sessionTime = 3
输出:2
解释:你可以在两个工作时间段内完成所有任务。

  • 第一个工作时间段:完成第一和第二个任务,花费 1 + 2 = 3 小时。
  • 第二个工作时间段:完成第三个任务,花费 3 小时。

示例 2:

输入:tasks = [3,1,3,1,1], sessionTime = 8
输出:2
解释:你可以在两个工作时间段内完成所有任务。

  • 第一个工作时间段:完成除了最后一个任务以外的所有任务,花费 3 + 1 + 3 + 1 = 8 小时。
  • 第二个工作时间段,完成最后一个任务,花费 1 小时。

示例 3:

输入:tasks = [1,2,3,4,5], sessionTime = 15
输出:1
解释:你可以在一个工作时间段以内完成所有任务。

提示:

  • n == tasks.length
  • 1 <= n <= 14
  • 1 <= tasks[i] <= 10
  • max(tasks[i]) <= sessionTime <= 15

题解

状态定义dp[state]: 达到状态i的最少工作时间段,其中state是一个长度为 n 的二进制表示,state从低到高的第i位为i表示第i个任务已经完成,0 表示第i个任务未完成。
状态转移:

\[dp[state] = min( dp[state], dp[state\ 异或 \ sub] + 1) \]

说明:

  • sub是state的子集

    这里「子集」的含义为:sub 是state 的一个子集,当且仅当 sub 中任意的 1在state 中的对应位置均为 1。

  • state ^ sub, 就是state 和子集sub之间差的那一部分,差集

代码1

#define INF 0x3f3f3f3f
class Solution {
public:
  int minSessions(vector<int>& tasks, int sessionTime) {
    int n = tasks.size();
    int N = 1 << n;
    vector<int> dp(N, INF);
    dp[0] = 0;
    vector<int> sum(N, 0);

    // 预处理状态sum[i]和dp
    for (int i = 0; i < N; ++i) {
      for (int k = 0; k < n; ++k) {
        if (i & (1 << k)) sum[i] += tasks[k];
      }
    }

    for (int i = 1; i < N; ++i) {
        for(int j = 0; j <= i; j++){
            // 集合i是否包含集合j
            if((i | j) == i){
                if(sum[j] <= sessionTime){
                    // 拆分子集进行状态转移,i ^ sub = 两者差集 i - sub
                dp[i] = min(dp[i], dp[i ^ j] + 1);
                }
            }
        }
    }
    return dp[N - 1];
  }
};

代码2

更快的枚举属于集合i的子集合sub

细节

枚举 mask 的子集有一个经典的小技巧,对应的伪代码如下:

subset = mask
while subset != 0 do
    // subset 是 mask 的一个子集,可以用其进行状态转移
    ...
    // 使用按位与运算在 O(1) 的时间快速得到下一个(即更小的)mask 的子集
    subset = (subset - 1) & mask
end while
#define INF 0x3f3f3f3f
class Solution {
public:
  int minSessions(vector<int>& tasks, int sessionTime) {
    int n = tasks.size();
    int N = 1 << n;
    vector<int> dp(N, INF);
    dp[0] = 0;
    vector<int> sum(N, 0);
    // 预处理状态sum[i]和dp
    for (int i = 0; i < N; ++i) {
      for (int k = 0; k < n; ++k) {
        if (i & (1 << k)) sum[i] += tasks[k];
      }
    }

    for (int i = 1; i < N; ++i) {
      for (int sub = i; sub; sub = (sub - 1) & i) {
        if (sum[sub] <= sessionTime) {
          // 拆分子集进行状态转移,i ^ sub = 两者差集 i - sub
          dp[i] = min(dp[i], dp[i ^ sub] + 1);
        }
      }
    }
    return dp[N - 1];
  }
};

Leeetcode 1494. 并行课程 II

题意

给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 dependencies 中, dependencies[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。

在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。

请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。

示例 1:

image-20210831110622570

输入:n = 4, dependencies = [[2,1],[3,1],[1,4]], k = 2
输出:3
解释:上图展示了题目输入的图。在第一个学期中,我们可以上课程 2 和课程 3 。然后第二个学期上课程 1 ,第三个学期上课程 4 。
示例 2:

image-20210831110633161

输入:n = 5, dependencies = [[2,1],[3,1],[4,1],[1,5]], k = 2
输出:4
解释:上图展示了题目输入的图。一个最优方案是:第一学期上课程 2 和 3,第二学期上课程 4 ,第三学期上课程 1 ,第四学期上课程 5 。
示例 3:

输入:n = 11, dependencies = [], k = 2
输出:6

提示:

1 <= n <= 15
1 <= k <= n
0 <= dependencies.length <= n * (n-1) / 2
dependencies[i].length == 2
1 <= xi, yi <= n
xi != yi
所有先修关系都是不同的,也就是说 dependencies[i] != dependencies[j] 。
题目输入的图是个有向无环图。

题解

比上题多了前导课程的概念,用pre[i]储存状态i需要的前置课程集合即可,思路基本差不多。

class Solution {
public:
    int count_one_bits(unsigned int value)
    {
        int count = 0;
        while(value){ 
            value=value&value - 1;
            count++;
        }
        return count;
    }

    int minNumberOfSemesters(int n, vector<vector<int>>& relations, int k) {
        int N = 1 << n;
        vector<int> pre_c(n, 0);
        vector<int> dp(N, INT_MAX / 2);
        for(int i = 0; i < relations.size(); i++){
            pre_c[relations[i][1] - 1] |= 1<<(relations[i][0] - 1);
        }
        dp[0] = 0;
        // 处理状态i的前导课程和初始化dp[i]
        vector<int> pre(N, 0);
        for(int i = 1; i < N; i++){
            int count = 0;
            for(int j = 0; j < n; j++){
                if(i >> j & 1){
                    pre[i] |= pre_c[j];
                    count ++;
                }
            }
        }
    
        for(int i = 1; i < N; i++){
            for(int sub = i; sub; sub = (sub - 1) & i){
                // sub 表示要选的课程集合,i ^ sub表示已经选好的课程
                if(count_one_bits(sub) > k) continue;
                // 判断前导课程是否全部修完
                if((pre[i] & (i ^ sub)) == pre[i]){
                    dp[i] = min(dp[i], dp[i ^ sub] + 1);
                }
                //cout<<bitset<4>(i)<<" "<<bitset<4>(sub)<<" "<<bitset<4>(pre[i])<<" "<<dp[i]<<endl;
            }
        }
        return dp[N - 1];
    }
};

Leetcode 1655. 分配重复整数

题意

题目描述
给你一个长度为n的整数数组nums,这个数组中至多有50个不同的值。同时你有 m个顾客的订单 quantity,其中,整数quantity[i]是第i位顾客订单的数目。请你判断是否能将 nums中的整数分配给这些顾客,且满足:

第i位顾客 恰好有quantity[i]个整数。
第i位顾客拿到的整数都是 相同的。
每位顾客都满足上述两个要求。
如果你可以分配 nums中的整数满足上面的要求,那么请返回true,否则返回 false。

样例
示例1
输入:nums = [1,2,3,4], quantity = [2]
输出:false
解释:第 0 位顾客没办法得到两个相同的整数。
示例2
输入:nums = [1,2,3,3], quantity = [2]
输出:true
解释:第 0 位顾客得到 [3,3] 。整数 [1,2] 都没有被使用。
示例3
输入:nums = [1,1,2,2], quantity = [2,2]
输出:true
解释:第 0 位顾客得到 [1,1] ,第 1 位顾客得到 [2,2] 。
示例4
输入:nums = [1,1,2,3], quantity = [2,2]
输出:false
解释:尽管第 0 位顾客可以得到 [1,1] ,第 1 位顾客没法得到 2 个一样的整数。
示例5
输入:nums = [1,1,1,1,1], quantity = [2,3]
输出:true
解释:第 0 位顾客得到 [1,1] ,第 1 位顾客得到 [1,1,1] 。
提示
n == nums.length
1 <= n <= 10^5
1 <= nums[i] <= 1000
m == quantity.length
1 <= m <= 10
1 <= quantity[i] <= 105
nums中至多有50个不同的数字。

题解

首先依旧用二进制表示满足的顾客状态。

f[i] [j] 表示前 i 个数字可以满足顾客状态j。

如果f[i] [j] == true && cost(k) <= w[i + 1],

\[f[i + 1] [j | k] = true \]

具体含义为f[i] [j] 为真且第 i + 1 个数满足集合k所需的条件,所以可以转移,到f[i + 1] [j | k],j | k表示集合j和集合k合集。

cost(k) 表示状态k需要花费的代价,w[i + 1] 表示第i + 1 个数的个数

为了加快速度,集合k的枚举要枚举剩余元素的子集

 for (int t = j ^ ((1 << m) - 1), k = t; k; k = (k - 1) & t){
     
}

时间复杂度

image-20210914154128279

i -> i + 1

class Solution {
public:
    bool canDistribute(vector<int>& nums, vector<int>& quantity) {
        unordered_map<int, int> hash;
        for (auto x: nums) hash[x] ++ ;
        vector<int> w(1);
        for (auto [x, y]: hash) w.push_back(y);
        int n = hash.size(), m = quantity.size();
        vector<int> s(1 << m);
        for (int i = 0; i < 1 << m; i ++ )
            for (int j = 0; j < m; j ++ )
                if (i >> j & 1)
                    s[i] += quantity[j];
        vector<vector<int>> f(n + 1, vector<int>(1 << m));
        f[0][0] = 1;
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < 1 << m; j ++ )
                if (f[i][j]) {
                    f[i + 1][j] = 1;
                    for (int t = j ^ ((1 << m) - 1), k = t; k; k = (k - 1) & t)
                        if (s[k] <= w[i + 1])
                            f[i + 1][j | k] = 1;
                }

        return f[n][(1 << m) - 1];
    }
};

i- 1 -> i

class Solution {
public:
    bool canDistribute(vector<int>& nums, vector<int>& quantity) {
        int m = quantity.size(), N = 1<<m;
        unordered_map<int, int>count;
        for(int x: nums){
            count[x] += 1;
        }
        vector<int> w;
        for(auto &x: count){
            w.push_back(x.second);
        }
        vector<int> cost(N, 0);
        for(int i = 1; i < N; i++){
            for(int j = 0; j < m; j++){
                if(i >> j & 1){
                    cost[i] += quantity[j];
                }
            }
        }
        int n = w.size();
        vector<vector<bool>>f(n + 1, vector<bool>(N, false));
        f[0][0] = true;
        for(int i = 1; i <= n; i++){// 从1开始枚举,方便获得f[i][j] = f[i - 1][j]
            //cout<<i<<endl;
            for(int j = 0; j < N; j++){
                f[i][j] = f[i - 1][j];
                //cout<<bitset<4>(j)<<"::"<<endl;
                // !f[i][j]  求出f[i][j]  为正确的即可
                for(int sub = j; sub && !f[i][j]; sub = (sub - 1) & j){
                    if(cost[sub] <= w[i - 1]){
                        f[i][j] = f[i - 1][j  ^ sub];
                    }
                    //cout<<bitset<4>(sub)<<" "<<w[i-1]<<" "<<f[i][j]<<endl;
                }
            
            }
        }
        return f[n][N - 1];
    }
};
posted @ 2021-09-14 16:10  pxlsdz  阅读(233)  评论(0编辑  收藏  举报