前言
当输入的个数比较少,而需要考虑其各种组合的状态时,可以使用状态压缩DP,用某一位的0或1来表示某一个元素的状态,用一个int值来表示整体的状态,对这个int值进行迭代。
题目
并行课程II
给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 dependencies 中, dependencies[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。
在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。
请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
分析
我们用一个int值state来表示当前已修课程的状态,对于state的第i位,如果为1表示第i门课程已修,为0表示第i门课程未修。dp[state]表示达到状态state所需的最少学期数。
我们从0开始迭代state。为什么可以从0开始迭代呢?这是因为我们注意到随着state的值增大,state中包含1的位逐渐向左拓展,因此对于一个state来说,其前置的状态为1的位,该state的对应位置也一定为1,换句话说,state的前置状态在int值上一定是小于state的,因此state可以从小到大迭代。
在state状态下,我们找到所有当前能修的课程,如何判断某门课程是否能修呢?我们使用一个掩码来表示某门课程的依赖课程,依赖的课程位置为1,这样我们当前的state按位或某门课程的掩码,如果state不变,则表明这门课程的依赖课程都满足了,我们维护一个当前可修课程的int值,将该门课程的对应位置1。
找到了我们在当前学习可以修的所有课程后,我们选择不多于k门课程来学习。我们该如何选择课程呢?假设有m门课程我们可以选择,我们是否需要枚举从m门中选择不多于k门课的所有可能呢?
答案是不需要。我们考虑这样的情况,假设在状态state下可以选择m门课,但我们最多选择k门课,如果我们只从m门课中序号较高的那k门课中选择,我们是否会漏掉某些状态呢?不会的,记我们选择了序号较高的k门课后,状态变为了state’,没有选到的课程在state’状态下依然是可选的,这样虽然我们之前没有选,但之后还是考虑到了这种情况,实际上整个解集是完备的。因此不会漏掉可能性。
代码
class Solution {
public int minNumberOfSemesters(int n, int[][] dependencies, int k) {
int N = 1<<n;
int[] mask = new int[n];
int[] dp = new int[N];
int[] cnt = new int[N];
//init
Arrays.fill(dp, n+1);
dp[0] = 0;
for(int[] i : dependencies) {
mask[i[1]-1] |= 1<<i[0]-1;
}
cnt[0] = 0;
for(int i = 1; i < N; i++) {
cnt[i] = cnt[i>>1]+(i&1);
}
//dp
for(int i = 0; i < N; i++) if(dp[i] < n) {
int cur = 0;
for(int j = 0; j < n; j++) if((i>>j&1)==0 && (mask[j]|i)==i){
cur |= 1<<j;
}
for(int j = cur; j != 0; j = j-1&cur) if(cnt[j] <= k) {
dp[i|j] = Math.min(dp[i|j], dp[i]+1);
}
}
return dp[N-1];
}
}