【每日一题Day239】LC1494并行课程 II | 状态压缩 dp 位运算 子集

并行课程 II【LC1494】

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

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

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

今天的也好难呀

  • 思路:拓扑排序【错误】

    第一眼觉得是用拓扑排序做,将入度为0的节点放在队列中,队列中的所有节点都可以在同一学期上,因此把队列中的课程全部上完需要的学期数⌈size/k⌉\lceil size/k \rceilsize/k。但是WA了,原因该做法不是最优的,因为队列中的元素的后置课程的前置课程还没有全部完成,那么的前置课程可以延期至下一轮再上

  • 思路:状态压缩dp

    • 子问题

      • 全集为UUU,我们需要解决的问题为上完所有的课程,最少需要多少个学期。
      • 假设已经上了几门课程后,剩余的课程对应的集合为AAA,那么问题转变为上完集合AAA中所有的课程,最少需要多少个学期
      • 这是一个和原问题相似的子问题,因此我们可以用递归/dp解决。
    • 递归函数定义:定义dfs(i)dfs(i)dfs(i)为上完集合iii所有的课程,最少需要多少个学期,此时补集CuiC_uiCui中的所有课程已经上完。dfs(U)dfs(U)dfs(U)即为答案。

    • 状态转移

      由于每个学期最多上kkk门课,我们可以枚举iii的大小不超过kkk的非空子集jjj0<∣j∣≤k0<|j|\le k0<jk)。

      优化:jjj中的每门课程的先修课一定在补集CuiC_uiCui中,因此可以求出先修课在补集CuiC_uiCui中的课程集合,记为i1i_1i1。那么我们可以枚举i1i_1i1的补集jjj,剩余课程为i∖ji \setminus jij,那么可以得到转态转移方程为
      dfs(i)=dfs(i∖j)+1 dfs(i)=dfs(i \setminus j) + 1 dfs(i)=dfs(ij)+1

    • 递归边界

      i=0i=0i=0时,表示为空集,返回0

    • 记忆化:递归过程有重复调用,因此可以记录状态,当一个状态不是第一次遇到时,直接返回保存的结果

    • 状态压缩/位运算:由于n小于等于15,因此可以使用状态压缩mask表示该课程是否在当前选课表中,mask的第iii位表示当前选择第iii门课。

    • 位运算

      • 全集:当总共有15个状态时的全集,即集合大小为15,每位均为1

        int size = 15;
        int ALL = (1 < < size) - 1;
        
      • 补集:ALL^A

        集合A的补集

      • 差集:B是A的子集,A^B即为集合A-集合B剩余的元素

      • 判断B是否是A的子集:(A & B) == B

        如果一个集合B和另一个集合A的交集为它自身,那么B是A的子集

      • 枚举A的子集

        for (int j = A; j > 0; j = (j - 1) & A){
            
        }
        
      • 计算状态A中1的个数

        int cnt = 0;
        for (int i = 0; i < size; i++){
        	if (A & (1 << i)) cnt++;
        }
        
  • 实现

    class Solution {
        public int minNumberOfSemesters(int n, int[][] relations, int k) {
            var pre1 = new int[n];
            for (var r : relations)
                pre1[r[1] - 1] |= 1 << (r[0] - 1); // r[1] 的先修课程集合,下标改从 0 开始
    
            int u = (1 << n) - 1; // 全集
            var f = new int[1 << n];
            f[0] = 0;
            for (int i = 1; i < 1 << n; i++) {
                int i1 = 0, ci = u ^ i; // i 的补集
                for (int j = 0; j < n; j++)
                    if ((i >> j & 1) > 0 && (pre1[j] | ci) == ci) // pre1[j] 在 i 的补集中,可以学(否则这学期一定不能学)
                        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];
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/parallel-courses-ii/solutions/2310878/zi-ji-zhuang-ya-dpcong-ji-yi-hua-sou-suo-oxwd/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值