引入
本週的雙週賽最後一題:5435. 並行課程 II,遇到了一個坑,原本以爲是一道很簡單的題,但是加上了某個條件後,解法完全不一樣了。
題目是這樣的:
給你一個整數 n 表示某所大學裏課程的數目,編號爲 1 到 n ,數組 dependencies 中, dependencies[i] = [xi, yi] 表示一個先修課的關係,也就是課程 xi 必須在課程 yi 之前上。同時你還有一個整數 k 。
在一個學期中,你 最多 可以同時上 k 門課,前提是這些課的先修課在之前的學期裏已經上過了。
請你返回上完所有課最少需要多少個學期。題目保證一定存在一種上完所有課的方式。
示例 1:
輸入:n = 4, dependencies = [[2,1],[3,1],[1,4]], k = 2
輸出:3
解釋:上圖展示了題目輸入的圖。在第一個學期中,我們可以上課程 2 和課程 3 。然後第二個學期上課程 1 ,第三個學期上課程 4 。
這道題的坑點在於每個學期最多選k門課,我一開始使用的圖的入度和出度來解答問題:
public class Solution {
public int minNumberOfSemesters(int n, int[][] dependencies, int k) {
//記錄入度和出度的數組pre
int[] pre=new int[n];
//記錄圖
List<List<Integer>> list=new ArrayList<>();
for (int i=0;i<n;i++){
list.add(new LinkedList<>());
}
for (int i=0;i<dependencies.length;i++){
int a=dependencies[i][0]-1;
int b=dependencies[i][1]-1;
pre[b]++;//入度+1
list.get(a).add(b);
}
int countTerm=0;
Queue<Integer> queue=new LinkedList<>();
for (int i=0;i<n;i++){
if (pre[i]==0){
//入度爲0,本學期可以學習
queue.add(i);
}
}
while(!queue.isEmpty()){
int size=queue.size()<k?queue.size():k;
countTerm++;//學期+1
for (int i=0;i<size;i++){
int pos=queue.poll();
System.out.println(countTerm+" "+(pos+1));
List<Integer> curr=list.get(pos);
for (int j=0;j<curr.size();j++){
pre[curr.get(j)]--;
//入度爲0,表示可以選擇該門課了
if (pre[curr.get(j)]==0){
queue.add(curr.get(j));
}
}
}
}
return countTerm;
}
}
看起來還好,但是在跑下面的用例的時候,我發現了錯誤:
9
[[4,8],[3,6],[6,8],[7,6],[4,2],[4,1],[4,7],[3,7],[5,2],[5,9],[3,4],[6,9],[5,7]]
2
輸出:
6
實際答案5
我的代碼執行順序如下圖所示:(每隔顏色代表一個學期)
而實際上,最優解應該是這樣的:
也就是在第三學期選擇1、7或者2、7,提前解鎖了6,從而下一個學期能同時修1、6。
所以,當k小於目前隊列queue的長度的時候,選擇順序很重要。
如果要弄好選擇順序,一般就是用回溯的方式了,不過我們這裏既用了queue,再用一個回溯,感覺代碼比較複雜。
一般能用回溯的題都能用動態規劃來解決,那麼這道題如何用動態規劃呢?
如何建模?
遇到這道題,即使告訴你用動態規劃來做,你也會一瞬間愣住。圖的情況下,如何來做動態規劃呢?
看了看題解,太過複雜了,因爲需要狀態壓縮DP之類的,題解太少,不好理解。
這裏我先留個坑,等以後題解多了再做動態規劃的解法。
import java.util.*;
public class Solution {
public static void main(String[] args) {
int[] input = new int[]{};
int[] output = new int[]{2, 3};
System.out.println(new Solution());
}
public int get(int x, int i) {
return (x >> i) & 1;
}
public int minNumberOfSemesters(int n, int[][] dependencies, int k) {
int[] pre = new int[n];
int[] post = new int[n];
for (int[] e : dependencies) {
int a = e[0] - 1;
int b = e[1] - 1;
pre[b] |= 1 << a;
post[a] |= 1 << b;
}
int[] dp = new int[1 << n];
dp[0] = 0;
int inf = (int) 1e8;
SubsetGenerator sg = new SubsetGenerator();
for (int i = 1; i < 1 << n; i++) {
boolean valid = true;
int set = 0;
dp[i] = inf;
for (int j = 0; j < n; j++) {
if (get(i, j) == 0) {
continue;
}
if ((pre[j] & i) != pre[j]) {
valid = false;
}
if ((post[j] & i) == 0) {
set |= 1 << j;
}
}
if (!valid) {
continue;
}
sg.reset(set);
while (sg.hasNext()) {
int next = sg.next();
if (next != 0 && Integer.bitCount(next) <= k) {
dp[i] = Math.min(dp[i - next] + 1, dp[i]);
}
}
}
return dp[dp.length - 1];
}
}
class SubsetGenerator {
private int m;
private int x;
public void reset(int m) {
this.m = m;
this.x = m + 1;
}
public boolean hasNext() {
return x != 0;
}
public int next() {
return x = (x - 1) & m;
}
}
其他方式:貪心
貪心的方式題目可以AC,但是還有一些題目沒有包含用例是跑不通的。貪心的思想是:考慮優先去學最大出度的課程,最後才學最小出度的課程。
這種方式雖然不能說正確,但是比較好解決,只需要把代碼改成PriorityQueue,或者增加一個出度的數組,然後每次從Queue中拿出所有的比較,選出k個即可。