给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 relations 中, relations[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。
在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。
k,n范围[1,15],releastions 范围[0,n*n/2],xi!=yi
这个问题是很久之前写过的,也是比较有意思的一类问题。现在的思路比之前要清晰。
一般来说,对于课程,常见的处理就是拓扑排序。而在这里,就不一定合适。因为有限制,一个学期semester中,最多可以同时上k门课,也就是说在一个学期中上课数量是1~k。
而且每个学期能选择的课程也是会受到上一个学期的已学课程限制。即必须在本学期之前完成该课程的所有前置课程,才可以学习这个课程。
一般看到这里,都会想到使用拓扑。但是正如上面说的,每个学期选课的限制,选择不同的课程会影响到最终的学期数。
因此简单的使用拓扑是不行的。
而最简单的思路就是暴力,一共最多有15个课程,而课程的前置课程数量可能是0~15。
可以通过位运算,用1表示已经完成的课程,那么,原始的课程编号1~ 15,在位运算中就对应0~14位,那么全部完成的状态就是 e n d s t a t e = ( 1 < < n ) − 1 endstate =(1<
为了方便处理,对前置课程进行预处理,用pre[i]表示课程i的前置课程集合。
如果想到达最终的状态endstate,那么就必须到达前一个状态prestate ,而这样一个最接近endstate的状态,就需要枚举了。
关键在于枚举的范围,首先prestate一定是endstate的子集,其次因为存在限制,要保证prestate中1的数量cnt1
另一个递归思路,角度稍有不同,是以dfs(i)表示 要完成未完成的课程集合i,需要花费的最少学期。也就是说 集合i 的补集 A = ∁ U i A= \complement_Ui A=∁Ui,A表示已经完成的课程集合。
对i的每一位进行检查,从而筛选出当前学期可以选择的课程。用candidate表示当前学期可以选择的课程,如果i的第j位为1,那么 p r e [ j ] ∣ A = = A pre[j]|A==A pre[j]∣A==A ,就说明课程j的前置课程已经都完成了,课程j可以加入candidate, c a n d i d a t e ∣ = ( 1 < < j ) candidate|=(1<
拿到本学期可以选择的课程集合candidate,接下来就是第二个限制k。
如果candidate的课程数量即1的数量<=k,就说明本学期可以把candidate的课程全部完成,那么下学期就剩余的课程为 ( i ⊕ c a n d i d a t e ) (i\oplus candidate) (i⊕candidate).
如果candidate的课程数量大于k,那么就必须进行枚举,即从candidate集合中枚举子集,子集的条件则是子集中必须有k个课程。当一个子集为sub时,那么说明当前学期可以完成sub的课程,下一学期开始就需要完成的课程为 ( i ⊕ c a n d i d a t e ) (i\oplus candidate) (i⊕candidate).
在枚举子集过程中,会有许多结果 d f s ( i ⊕ s u b ) dfs(i\oplus sub) dfs(i⊕sub),为了达到最小,故选择 r e s = m i n ( d f s ( i ⊕ s u b ) ) + 1 res= min(dfs(i\oplus sub))+1 res=min(dfs(i⊕sub))+1.
A = ∁ U i , / / A 是 i 的补集 D F S ( i ) = min D F S ( i ⊕ s u b ) + 1 , c n t O n e ( s u b ) ∈ [ 1 , k ] , A N D , s u b ⊂ c a n d i d a t e c a n d i d a t e 为当前可选课程的集合, c n t O n e 为计算集合中元素的数量 A= \complement_Ui ,//A 是i的补集 \\ DFS(i) = \min{DFS(i\oplus sub)}+1 , cntOne(sub)\in[1,k] ,AND, sub\subset candidate\\ candidate为当前可选课程的集合,cntOne为计算集合中元素的数量 A=∁Ui,//A是i的补集DFS(i)=minDFS(i⊕sub)+1,cntOne(sub)∈[1,k],AND,sub⊂candidatecandidate为当前可选课程的集合,cntOne为计算集合中元素的数量
在dfs的过程中,必然会发生重复的计算,所以加入memo进行记忆化搜索。
class Solution {
private int[] pre, memo;
private int _k, U,INF;
public int minNumberOfSemesters(int n, int[][] relations, int k) {
_k = k;
INF= Integer.MAX_VALUE>>1;
pre = new int[n];
for (int[] r : relations){
pre[r[1] - 1] |= 1 << (r[0] - 1);
}
U = (1 << n) - 1; // 全集
memo = new int[1 << n];
Arrays.fill(memo, INF); // INF 表示没有计算过
return dfs(U);
}
private int dfs(int i) {
if (i == 0) return 0; // 空集
if (memo[i] != INF) return memo[i]; // 之前算过了
int candidate = 0, A = U ^ i; //A 是 i 的补集
for (int j = 0; j < pre.length; j++){
if ((i >> j & 1) > 0 && (pre[j] | A) == A){
// pre[j]前置课程已经完成,j可以学
candidate |= 1 << j;
}
}
if (Integer.bitCount(candidate) <= _k){
// 如果小于 k,本学期可以完成
return memo[i] = dfs(i ^ candidate) + 1;
}
int res = INF;
// 枚举 candidate 的子集 sub
for (int sub = candidate; sub > 0; sub = (sub - 1) & candidate){
if (Integer.bitCount(sub) == _k){//在sub中挑选k个课程
res = Math.min(res, dfs(i ^ sub) + 1);
}
}
return memo[i] = res;
}
}
时间复杂度 O(指数级)
空间复杂度: O(指数级)
转换自底向上的递推。从1开始枚举直到U,f[i]表示完成状态i的课程最少需要的学期。
A = ∁ U i , / / A 是 i 的补集 F [ i ] = min F ( i ⊕ s u b ) + 1 , c n t O n e ( s u b ) ∈ [ 1 , k ] , A N D , s u b ⊂ c a n d i d a t e c a n d i d a t e 为当前可选课程的集合, c n t O n e 为计算集合中元素的数量 A= \complement_Ui ,//A 是i的补集 \\ F[i] = \min{F(i\oplus sub)}+1 , cntOne(sub)\in[1,k] ,AND, sub\subset candidate\\ candidate为当前可选课程的集合,cntOne为计算集合中元素的数量 A=∁Ui,//A是i的补集F[i]=minF(i⊕sub)+1,cntOne(sub)∈[1,k],AND,sub⊂candidatecandidate为当前可选课程的集合,cntOne为计算集合中元素的数量
class Solution {
public int minNumberOfSemesters(int n, int[][] relations, int k) {
int INF= Integer.MAX_VALUE>>1,U = (1 << n) - 1; // 全集
int[] pre = new int[n],f = new int[U+1];;
for (int[] r : relations){
pre[r[1] - 1] |= 1 << (r[0] - 1);
}
for(int i = 1;i<=U;i++){
int candidate = 0, A = U ^ i; //A 是 i 的补集
for (int j = 0; j < pre.length; j++){
if ((i >> j & 1) > 0 && (pre[j] | A) == A){
// pre[j]前置课程已经完成,j可以学
candidate |= 1 << j;
}
}
if (Integer.bitCount(candidate) <= k){
// 如果小于 k,本学期可以完成
f[i] = f[i ^ candidate] + 1;
continue;
}
f[i] = INF;
// 枚举 candidate 的子集 sub
for (int sub = candidate; sub > 0; sub = (sub - 1) & candidate){
if (Integer.bitCount(sub) == k){//在sub中挑选k个课程
f[i] = Math.min(f[i], f[i^sub] + 1);
}
}
}
return f[U];
}
}
代码的思路,来源于灵神大佬,值得学习参考。
预备知识需要知道如何枚举子集,以及位运算相关。
Tag
Graph
Bit Manipulation
Dynamic Programming
Bitmask