LC-1494. 并行课程 II(状压DP)

1494. 并行课程 II

难度困难116

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

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

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

示例 1:

img

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

示例 2:

img

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

示例 3:

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

提示:

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

状态压缩DP

https://leetcode.cn/problems/parallel-courses-ii/solution/yu-niang-niang-1494-bing-xing-ke-cheng-i-duny/

为什么这题不能用拓扑排序?

这个图,实际最佳出队方式是:[7,8],[1,2],[3,4][5,6],[9,10]

只需要5次,因此拓扑不能用

方法:状压DP

  1. 因为点编号是1到n,所以对应于0到n-1的数组,映射位置应为 i-1

  2. 使用状压代表课程先修

  3. 枚举学习课程的已经学习情况

  4. 枚举当前学习情况,后续可学习的可能情况

  5. 枚举当前1的位置的子集(包含自身),然后用 当前课程= (当前课程-1)&所有可学课程 ,这种方式,循环找到所有课程枚举子集

细节

  1. 枚举可学课程需要排除已学课程

  2. 枚举选择的本轮课程需要排除掉一轮学习可能超过最大目标课程的情况

class Solution {
    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        int[] pre = new int[n];
        for(int[] re : relations){
            //1. 因为点编号是1到n,所以对应于0到n-1的数组,映射位置应为 i-1
            //2. 使用状压代表课程先修
            pre[re[1] - 1] |= 1 << (re[0] - 1);
        }
        int max = 1 << n;
        int[] dp = new int[max];
        Arrays.fill(dp, n);
        dp[0] = 0;
        //3. 枚举学习课程的已经学习情况
        for(int learned = 0; learned < max; learned++){
            //4. 枚举当前学习情况,后续可学习的可能情况
            int waitStudy = 0;
            for(int i = 0; i < n; i++){
                if((pre[i] & learned) == pre[i]){
                    waitStudy |= 1 << i;
                }
            }
            //细节1. 枚举可学课程需要排除已学课程
            waitStudy = waitStudy & (~learned);
            //5. 枚举当前1的位置的子集(包含自身),然后用 当前课程= (当前课程-1)&所有可学课程,
            // 这种方式,循环找到所有课程枚举子集
            for(int learnTerm = waitStudy; learnTerm > 0; learnTerm = (learnTerm-1)& waitStudy){
                //细节2. 枚举选择的本轮课程需要排除掉一轮学习可能超过最大目标课程的情况
                if(Integer.bitCount(learnTerm) > k)
                    continue;
                dp[learned|learnTerm] = Math.min(dp[learned|learnTerm],dp[learned]+1);
            }
        }
        return dp[max-1];
    }
}

记忆化搜索 ⇒ 动态规划

https://leetcode.cn/problems/parallel-courses-ii/solution/zi-ji-zhuang-ya-dpcong-ji-yi-hua-sou-suo-oxwd/

从 i1 出发,求为 1 的位的子集,开始这些为 1 的位全是 1,之后每次减去 1,再与 i1做与运算,得到的 1 所在的位仍然在最初的 i1 所在的位的集合中,只是某些位变为了 0,由于每次只减去 1,所以肯定可以遍历 11111…111 ~ 00000…000 的所有状态,即得到为 1 的位的子集。

class Solution {
    // 定义dfs(i)表示上完集合 i 中的课程,最少需要多少个学期
    // 考虑枚举 i 的大小不超过 k 的非空子集 j,作为一个学期内需要学完的课程
    //          这里 j 中所有元素的先修课必须在 i 的补集中
    // 用一个学期上完 j 中的课程,则剩余课程为i/j ,继续递归计算 dfs(i/j),所有情况取最小值
    // 递归边界 dfs(空集) = 0
    // 递归入口 dfs(全集)
    int[] pre1, memo;
    int k, u;
    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        this.k = k;
        pre1 = new int[n];
        for(int[] r : relations){
            // r[1] 的先修课程集合,下标改从 0 开始
            pre1[r[1] - 1] |= 1 << (r[0] - 1);
        }
        u = (1 << n) - 1; // 全集
        memo = new int[1 << n];
        Arrays.fill(memo, -1); // -1表示还没计算过
        return dfs(u);
    }

    public int dfs(int i){
        if(i == 0) return 0; // 空集
        if(memo[i] != -1) return memo[i]; // 之前计算过了
        int i1 = 0, ci = u ^ i; // i1 是当前可以学习的课程集合,ci 是 i 的补集(已经学过的课程)
        for(int j = 0; j < pre1.length; j++){
            // pre1[j] 在 i 的补集中,可以学(否则这学期一定不能学)
            if(((i >> j) & 1) == 1 && (pre1[j] | ci) == ci)
                i1 |= 1 << j;
        }
        if(Integer.bitCount(i1) <= k){ // 如果个数小于k,则可以全部学习,不用再枚举子集
            return memo[i] = dfs(i ^ i1) + 1; // dfs(i) = dfs(i \ i1) + 1
        }
        // 可以学的课程超过k个,需要枚举大小为 k 的子集
        int res = Integer.MAX_VALUE;
    
        for(int j = i1; j > 0; j = (j-1) & i1){ // 枚举 i1 的子集 j
            if(Integer.bitCount(j) == k)
                res = Math.min(res, dfs(i ^ j) + 1);
        }
        return memo[i] = res;
    }
}

记忆化搜索转递推

class Solution {
    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        int[] pre1 = new int[n];
        for(int[] r : relations)
            // r[1] 的先修课程集合,下标改从 0 开始
            pre1[r[1] - 1] |= 1 << (r[0] - 1);
        int u = (1 << n) - 1; // 全集
        // 定义f(i)表示上完集合 i 中的课程,最少需要多少个学期
        int[] f = new int[1 << n];
        f[0] = 0;
        for(int i = 1; i < (1 << n); i++){
            int i1 = 0, ci = u ^ i; // i1 是当前可以学习的课程集合,ci 是 i 的补集(已经学过的课程)
            for(int j = 0; j < n; j++)
                // pre1[j] 在 i 的补集中,可以学(否则这学期一定不能学)
                if(((i >> j) & 1) == 1 && (pre1[j] | ci) == ci)
                    i1 |= 1 << j;
            if(Integer.bitCount(i1) <= k){ // 如果个数小于k,则可以全部学习,不用再枚举子集
                f[i] = f[i ^ i1] + 1;
                continue;
            }
            f[i] = Integer.MAX_VALUE;
            for(int j = i1; j > 0; j = (j-1) & i1){ // 枚举 i1 的子集 j
                if(Integer.bitCount(j) == k)
                    f[i] = Math.min(f[i], f[i ^ j] + 1);
            }
        }
        return f[u];
    }
}

相似题目(状压 DP)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值