Leetcode5435. 並行課程 II

引入

本週的雙週賽最後一題: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個即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章