并行课程 II【LC1494】
给你一个整数
n
表示某所大学里课程的数目,编号为1
到n
,数组relations
中,relations[i] = [xi, yi]
表示一个先修课的关系,也就是课程xi
必须在课程yi
之前上。同时你还有一个整数k
。在一个学期中,你 最多 可以同时上
k
门课,前提是这些课的先修课在之前的学期里已经上过了。请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
今天的也好难呀
-
思路:拓扑排序【错误】
第一眼觉得是用拓扑排序做,将入度为0的节点放在队列中,队列中的所有节点都可以在同一学期上,因此把队列中的课程全部上完需要的学期数 ⌈ s i z e / k ⌉ \lceil size/k \rceil ⌈size/k⌉。但是WA了,原因该做法不是最优的,因为队列中的元素的后置课程的前置课程还没有全部完成,那么其的前置课程可以延期至下一轮再上
-
思路:状态压缩dp
-
子问题:
- 全集为 U U U,我们需要解决的问题为上完所有的课程,最少需要多少个学期。
- 假设已经上了几门课程后,剩余的课程对应的集合为 A A A,那么问题转变为上完集合 A A A中所有的课程,最少需要多少个学期
- 这是一个和原问题相似的子问题,因此我们可以用递归/dp解决。
-
递归函数定义:定义 d f s ( i ) dfs(i) dfs(i)为上完集合 i i i所有的课程,最少需要多少个学期,此时补集 C u i C_ui Cui中的所有课程已经上完。 d f s ( U ) dfs(U) dfs(U)即为答案。
-
状态转移:
由于每个学期最多上 k k k门课,我们可以枚举 i i i的大小不超过 k k k的非空子集 j j j( 0 < ∣ j ∣ ≤ k 0<|j|\le k 0<∣j∣≤k)。
优化: j j j中的每门课程的先修课一定在补集 C u i C_ui Cui中,因此可以求出先修课在补集 C u i C_ui Cui中的课程集合,记为 i 1 i_1 i1。那么我们可以枚举 i 1 i_1 i1的补集 j j j,剩余课程为 i ∖ j i \setminus j i∖j,那么可以得到转态转移方程为
d f s ( i ) = d f s ( i ∖ j ) + 1 dfs(i)=dfs(i \setminus j) + 1 dfs(i)=dfs(i∖j)+1 -
递归边界
i = 0 i=0 i=0时,表示为空集,返回0
-
记忆化:递归过程有重复调用,因此可以记录状态,当一个状态不是第一次遇到时,直接返回保存的结果
-
状态压缩/位运算:由于n小于等于15,因此可以使用状态压缩mask表示该课程是否在当前选课表中,mask的第 i i i位表示当前选择第 i 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) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。