给你一个整数 n
表示某所大学里课程的数目,编号为 1
到 n
,数组 relations
中, relations[i] = [xi, yi]
表示一个先修课的关系,也就是课程 xi
必须在课程 yi
之前上。同时你还有一个整数 k
。
在一个学期中,你 最多 可以同时上 k
门课,前提是这些课的先修课在之前的学期里已经上过了。
请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
示例 1:
![](https://assets.leetcode-cn.com/aliyun-lc- upload/uploads/2020/06/27/leetcode_parallel_courses_1.png)
**输入:** n = 4, relations = [[2,1],[3,1],[1,4]], k = 2
**输出:** 3
**解释:** 上图展示了题目输入的图。在第一个学期中,我们可以上课程 2 和课程 3 。然后第二个学期上课程 1 ,第三个学期上课程 4 。
示例 2:
![](https://assets.leetcode-cn.com/aliyun-lc- upload/uploads/2020/06/27/leetcode_parallel_courses_2.png)
**输入:** 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]
。
题目输入的图是个有向无环图。
前言 本题解涉及到「二进制」中的「子集枚举」,具体表示为对给定的一个整数 x,不重复地枚举 x 的「二进制」表示的非空子集 y,其中 y 满足 y \And x = y。以下为以 C++
实现枚举 x 的非空子集的代码,其正确性证明详细可以见 OI_WIKI-二进制集合操作-子集遍历部分 。
1 2 3 4 for (int sub = x; sub; sub = (sub - 1 ) & x) { }
对于一个有「二进制」表示中有 k 个 1 的正整数 x,其非空子集有 2^k - 1 个。所以对于 x 枚举子集的时间复杂度为 O(2^k)。
方法一:动态规划 + 状态压缩 思路与算法
题目给出 n 门课程数目,编号从 1 到 n,和数组 relations,其中 relations}[i] = [x_i, y_i] 表示课程 y_i 的先修课程为 x_i,即:在学习课程 y_i 之前课程 x_i 必须要完成。现在在一个学期中我们最多可以同时上 k 门课,前提是这些课的先修课程在之前的学期中已全部完成学习。现在我们需要返回上完全部 n 门课程最少需要多少个学期。
由于题目限定 n 最多 15,所以我们可以通过「二进制」和「状态压缩」来表示一个课程集合。我们用一个整数 S 来表示当前还需要学习的课程集合:从 S 「二进制」表示的低位到高位,第 i 位为 1 则表示课程 i 还需要进行学习,否则表示课程 i 已经完成学习。现在我们尝试用「状态压缩动态规划」来解决本题,设 dp}[i] 和 need}[i] 分别表示现在我们需要完成学习的课程集合为 i 所需要的最少学期数和完成学习课程集合为 i 时的先修课程集合,并初始化每一个状态 i 的 dp}[i] = \text{INF,need}[i] = 0,其中 INF 为我们人为设定的最大值表示该课程集合无法完成学习。
现在我们考虑如何实现状态转移。首先为了方便状态表示,我们重新对课程进行编号,从 0 开始编号,即原来编号为 x 的课程现在为 x - 1。然后从给定数组 relations 中每一个 relations}[i] = [x_i, y_i] 更新 need}[2^{y_i}] = 2^{x_i,注意现在的 x_i 和 y_i 为重新编号后的课程编号。特别地当课程集合为空时,有 need}[0] = 0 和 dp}[0] = 0。那么现在我们可以写出状态转移方程:
\begin{aligned} \textit{need}[i] &= \textit{need}[i \oplus \textit{sub}] | \textit{need}[\textit{sub}] \ \textit{dp}[i] &= \min_{\textit{sub} \text{ is valid} }{dp[i \oplus \textit{sub}]} + 1 \end{aligned}
这两个状态方程什么意思呢?
对于 need}[i] 我们找到状态 i 的一个任意子集 sub,课程集合 i 的先修课程为集合 sub 和集合 i 中去掉 sub 后所需的先修课程集合的并集。这里 \oplus 表示「异或」运算,i \oplus \textit{sub 表示将 sub 从 i 移除的操作。为了计算方便,我们可以令 sub 为 i 的「二进制」的最低位,可以用 i \And (-i) 来表示,i \oplus \textit{sub} = i \And (i - 1):
need}[i] = \textit{need}[i \And (i - 1)] | \textit{need}[i \And (-i)]
对于 dp}[i] 我们对 i 进行「子集枚举」。其中枚举的子集 sub 表示现在我们在当前课程集合 i 的情况下,在该学期尝试学习课程集合 sub。那么在所有满足条件的 sub 中选取剩下课程所需要学习学期最少的情况,就可以得到此时学完课程 i 所需要的最少学期数。当 sub 满足以下几个条件时,我们可以对 sub 进行学习:
sub 的大小不能超过每学期最多可以上的课程数 k,即 popcount}(\textit{sub}) \le k,其中 popcount}(x) 表示整数 x 「二进制」表示中 1 的个数。
剩下的课程集合 i \oplus \textit{sub 为可以在有限的学期内完成学习,即:dp}[i \oplus \textit{sub}] \neq \text{INF。
剩下的课程集合 i \oplus \textit{sub 中不存在 sub 的先修课程,即:need}[\textit{sub}] \And (i \oplus \textit{sub}) = \textit{need}[\textit{sub}]。
若不存在满足条件的 sub,则 dp}[i] = \text{INF 不变。
然后我们从小到大来计算每一个状态 i 的 need}[i] 和 dp}[i],最后返回 dp}[2^n-1] 即为学完全部课程所需要的最少学期数。
在实现的过程中,我们可以做以下的优化:
当 i \mid \textit{need}[i] \neq i 时,说明无论如何枚举子集 sub,始终有课程不能完成学习,此时 dp}[i] 仍为 INF 不变,直接跳过该状态。
对于枚举的子集 sub 为 i} \oplus \textit{need}[i] 的子集。因为如果要完成课程状态 i 的学习,need}[i] 一定不能在最后完成。
当 i} \oplus \textit{need}[i] 的课程集合大小小于等于 k 时,我们可以全部进行学习,不必再进行子集枚举。
代码
[sol1-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : int minNumberOfSemesters (int n, vector<vector<int >>& relations, int k) { vector<int > dp (1 << n, INT_MAX) ; vector<int > need (1 << n, 0 ) ; for (auto & edge : relations) { need[(1 << (edge[1 ] - 1 ))] |= 1 << (edge[0 ] - 1 ); } dp[0 ] = 0 ; for (int i = 1 ; i < (1 << n); ++i) { need[i] = need[i & (i - 1 )] | need[i & (-i)]; if ((need[i] | i) != i) { continue ; } int valid = i ^ need[i]; if (__builtin_popcount(valid) <= k) { dp[i] = min (dp[i], dp[i ^ valid] + 1 ); } else { for (int sub = valid; sub; sub = (sub - 1 ) & valid) { if (__builtin_popcount(sub) <= k) { dp[i] = min (dp[i], dp[i ^ sub] + 1 ); } } } } return dp[(1 << n) - 1 ]; } };
[sol1-C] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #define MIN(a, b) ((a) < (b) ? (a) : (b)) const int INF = 0x3f3f3f3f ;int minNumberOfSemesters (int n, int ** relations, int relationsSize, int * relationsColSize, int k) { int dp[1 << n]; int need[1 << n]; memset (dp, 0x3f , sizeof (dp)); memset (need, 0 , sizeof (need)); for (int i = 0 ; i < relationsSize; i++) { int x = relations[i][0 ], y = relations[i][1 ]; need[(1 << (y - 1 ))] |= 1 << (x - 1 ); } dp[0 ] = 0 ; for (int i = 1 ; i < (1 << n); ++i) { need[i] = need[i & (i - 1 )] | need[i & (-i)]; if ((need[i] | i) != i) { continue ; } int valid = i ^ need[i]; if (__builtin_popcount(valid) <= k) { dp[i] = MIN(dp[i], dp[i ^ valid] + 1 ); } else { for (int sub = valid; sub; sub = (sub - 1 ) & valid) { if (__builtin_popcount(sub) <= k) { dp[i] = MIN(dp[i], dp[i ^ sub] + 1 ); } } } } return dp[(1 << n) - 1 ]; }
[sol1-Python3] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def minNumberOfSemesters (self, n: int , relations: List [List [int ]], k: int ) -> int : dp = [inf] * (1 << n) need = [0 ] * (1 << n) for edge in relations: need[(1 << (edge[1 ] - 1 ))] |= 1 << (edge[0 ] - 1 ) dp[0 ] = 0 for i in range (1 , (1 << n)): need[i] = need[i & (i - 1 )] | need[i & (-i)] if (need[i] | i) != i: continue sub = valid = i ^ need[i] if sub.bit_count() <= k: dp[i] = min (dp[i], dp[i ^ sub] + 1 ) else : while sub: if sub.bit_count() <= k: dp[i] = min (dp[i], dp[i ^ sub] + 1 ) sub = (sub - 1 ) & valid return dp[-1 ]
[sol1-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { public int minNumberOfSemesters (int n, int [][] relations, int k) { int [] dp = new int [1 << n]; Arrays.fill(dp, Integer.MAX_VALUE); int [] need = new int [1 << n]; for (int [] edge : relations) { need[(1 << (edge[1 ] - 1 ))] |= 1 << (edge[0 ] - 1 ); } dp[0 ] = 0 ; for (int i = 1 ; i < (1 << n); ++i) { need[i] = need[i & (i - 1 )] | need[i & (-i)]; if ((need[i] | i) != i) { continue ; } int valid = i ^ need[i]; if (Integer.bitCount(valid) <= k) { dp[i] = Math.min(dp[i], dp[i ^ valid] + 1 ); } else { for (int sub = valid; sub > 0 ; sub = (sub - 1 ) & valid) { if (Integer.bitCount(sub) <= k) { dp[i] = Math.min(dp[i], dp[i ^ sub] + 1 ); } } } } return dp[(1 << n) - 1 ]; } }
[sol1-Go] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func minNumberOfSemesters (n int , relations [][]int , k int ) int { dp := make ([]int , 1 << n) for i := range dp { dp[i] = math.MaxInt32 } need := make ([]int , 1 <<n) for _, edge := range relations { need[1 << (edge[1 ] - 1 )] |= 1 << (edge[0 ] - 1 ) } dp[0 ] = 0 for i := 1 ; i < (1 << n); i++ { need[i] = need[i & (i - 1 )] | need[i & -i] if (need[i] | i) != i { continue ; } valid := i ^ need[i]; if bits.OnesCount(uint (valid)) <= k { dp[i] = min(dp[i], dp[i ^ valid] + 1 ) } else { for sub := valid; sub > 0 ; sub = (sub - 1 ) & valid { if bits.OnesCount(uint (sub)) <= k { dp[i] = min(dp[i], dp[i ^ sub] + 1 ) } } } } return dp[(1 << n) - 1 ] } func min (a, b int ) int { if a < b { return a } return b }
[sol1-JavaScript] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var minNumberOfSemesters = function (n, relations, k ) { const dp = new Array (1 << n).fill (Infinity ); const need = new Array (1 << n).fill (0 ); for (const edge of relations) { need[(1 << (edge[1 ] - 1 ))] |= 1 << (edge[0 ] - 1 ); } dp[0 ] = 0 ; for (let i = 1 ; i < (1 << n); ++i) { need[i] = need[i & (i - 1 )] | need[i & (-i)]; if ((need[i] | i) !== i) { continue ; } const valid = i ^ need[i]; if (bitCount (valid) <= k) { dp[i] = Math .min (dp[i], dp[i ^ valid] + 1 ); } else { for (let sub = valid; sub; sub = (sub - 1 ) & valid) { if (bitCount (sub) <= k) { dp[i] = Math .min (dp[i], dp[i ^ sub] + 1 ); } } } } return dp[(1 << n) - 1 ]; } function bitCount (n ) { let count = 0 ; while (n) { count++; n &= n - 1 ; } return count; }
复杂度分析
时间复杂度:O(3^n),其中 n 为课程的数量,动态规划中共有 O(2^n) 种状态,将每一个状态看作一个集合,大小为 k 的集合有 C_n^k 个,其中 C_n^k 表示从 n 个数中选取 k 个数的组合数,并且每一个转移的个数为 2^k,根据「二项式定理」可以得到
\sum_{k = 0}^n{C_n^k2^k} = \sum_{k = 0}^n{C_n^k2^k1^{n - k} } = (2 + 1) ^ n = 3^n
因此总的时间复杂度为 O(3^n)。
空间复杂度:O(2^n),其中 n 为课程的数量。我们需要保存动态规划中的每一个状态。