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

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

并行课程 II【LC1494】

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

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

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

今天的也好难呀

  • 思路:拓扑排序【错误】
    第一眼觉得是用拓扑排序做,将入度为0的节点放在队列中,队列中的所有节点都可以在同一学期上,因此把队列中的课程全部上完需要的学期数⌈ s i z e / k ⌉ 。但是WA了,原因该做法不是最优的,因为队列中的元素的后置课程的前置课程还没有全部完成,那么的前置课程可以延期至下一轮再上
  • 子问题
  • 全集为U,我们需要解决的问题为上完所有的课程,最少需要多少个学期。
  • 假设已经上了几门课程后,剩余的课程对应的集合为A,那么问题转变为上完集合A中所有的课程,最少需要多少个学期
  • 这是一个和原问题相似的子问题,因此我们可以用递归/dp解决。
  • 递归函数定义:定义dfs(i)为上完集合i所有的课程,最少需要多少个学期,此时补集Cui中的所有课程已经上完。dfs(U)即为答案。
  • 状态转移
    由于每个学期最多上k门课,我们可以枚举i的大小不超过k的非空子集j(0 < ∣ j ∣ ≤ k)

优化:j中的每门课程的先修课一定在补集Cui中,因此可以求出先修课在补集Cui中的课程集合,记为i1。那么我们可以枚举i1的补集j,剩余课程为ij,那么可以得到转态转移方程为

                                               dfs(i)=dfs(ij)+1

递归边界

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

  • 递归过程有重复调用,因此可以记录状态,当一个状态不是第一次遇到时,直接返回保存的结果
  • 状态压缩/位运算:由于n小于等于15,因此可以使用状态压缩mask表示该课程是否在当前选课表中,mask的第i位表示当前选择第i门课。
  • 位运算
  • 全集:当总共有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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
目录
相关文章
|
7月前
【每日一题Day297】LC1444切披萨的方案数 | 动态规划+二维前缀和
【每日一题Day297】LC1444切披萨的方案数 | 动态规划+二维前缀和
73 0
|
7月前
【每日一题Day312】LC2240买钢笔和铅笔的方案数 | 完全背包 数学
【每日一题Day312】LC2240买钢笔和铅笔的方案数 | 完全背包 数学
63 0
|
7月前
|
人工智能 BI
【每日一题Day267】LC834树中距离之和 | 换根dp
【每日一题Day267】LC834树中距离之和 | 换根dp
50 0
|
7月前
【每日一题Day149】LC2389和有限的最长子序列 | 贪心+前缀和+二分查找
【每日一题Day149】LC2389和有限的最长子序列 | 贪心+前缀和+二分查找
47 0
|
7月前
【每日一题Day163】LC2367算术三元组的数目 | 二分查找 哈希表
【每日一题Day163】LC2367算术三元组的数目 | 二分查找 哈希表
32 0
|
7月前
【每日一题Day179】LC1157子数组中占绝大多数的元素 | 线段树
【每日一题Day179】LC1157子数组中占绝大多数的元素 | 线段树
54 0
|
7月前
【错题集-编程题】删除相邻数字的最大分数(动态规划 - 线性 dp)
【错题集-编程题】删除相邻数字的最大分数(动态规划 - 线性 dp)
|
7月前
【每日一题Day257】LC2178拆分成最多数目的正偶数之和 | 贪心
【每日一题Day257】LC2178拆分成最多数目的正偶数之和 | 贪心
35 2
|
7月前
|
vr&ar
【每日一题Day166】LC1053交换一次的先前排列 | 贪心
【每日一题Day166】LC1053交换一次的先前排列 | 贪心
76 1
|
7月前
【每日一题Day160】LC1092最短公共超序列 | 动态规划
【每日一题Day160】LC1092最短公共超序列 | 动态规划
50 0
【每日一题Day160】LC1092最短公共超序列 | 动态规划