1. 程式人生 > 實用技巧 >leetcode-課程表

leetcode-課程表

課程表I

你這個學期必須選修 numCourse 門課程,記為 0 到 numCourse-1 。

在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們:[0,1]

給定課程總量以及它們的先決條件,請你判斷是否可能完成所有課程的學習?

題解

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        //構建入度表
        int[] indegrees = new int[numCourses];
        
//構建鄰接表 List<List<Integer>> adjacency = new ArrayList<>(); for(int i=0; i<numCourses; i++){ adjacency.add(new ArrayList<>()); } //構建佇列 Queue<Integer> queue = new LinkedList<>(); //對每門課程 設定其對應的入度和鄰接表(存放後繼successor結點的集合)
//想要學習課程0,先要完成課程1,表示為[0,1] for(int[] course : prerequisites){ //因為course[0]之前先要學習course[1],所以course[0]入度++ indegrees[course[0]]++; //key:課的編號,value:依賴它的後續課程(陣列) adjacency.get(course[1]).add(course[0]); } //將所有入度為0的節點入隊 for(int i=0; i<numCourses; i++){
if(indegrees[i] == 0){ queue.add(i); } } //當queue非空,依次將隊首節點出隊,鄰接表中刪除此節點 while(!queue.isEmpty()){ int pre = queue.poll(); numCourses--; //學習了pre課程,cure為接下來鄰接的課程,即接下來學習的課程。 //遍歷每一個鄰接節點 for(int cur:adjacency.get(pre)){ //當入度-1,鄰接節點cur的入度=0,則說明cur所有前驅節點都被刪除,此時cur入隊 if(--indegrees[cur] == 0){ queue.add(cur); } } } //即拓撲排序出隊次數等於課程數 return numCourses==0; } }

思路

將題目簡化為“課程安排表是否為無環有向圖”(通過拓撲排序判斷),如果是DAG,那麼課程間規定了前置條件。

對DAG的頂點排序,使得對每一條有向邊(u,v),u比v更顯出現。如上圖,上課程2,必須完成課程1;上課程4,必須完成課程2和1

通過課程條件表輸入,可以得到課程安排圖的鄰接表。然後通過廣度優先遍歷解決。

1.首先統計課程條件表中每個節點的入度(即每門課程當前需要上幾門課,如上圖的課程1的入度為0,課程2的入度為1),生成入度表

2.藉助一個佇列用來儲存所有入度為0的節點

3.遍歷課程表中的每一個節點,構建入度表和鄰接表

4.當佇列非空時,依次將隊首節點出隊,然後在鄰接表中刪除該節點。表示學習該課程,遍歷接下來的課程,表示學習其他的課程,那麼如果接下來的課程入度為0(需要--課程入度,因為此時課程的前導課剛剛學完),就將該課程入隊,說明接下來就學習該課程。並且執行numCourses--,若整個課程表為有向無環圖,所有的節點都一定入隊和出隊,完成拓撲排序。

5.當遍歷完,numCourses不為0,即還有節點沒有入隊和出隊,那麼說明該課程不能成功安排。

課程表II

現在你總共有 n 門課需要選,記為 0 到 n-1。

在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]

給定課程總量以及它們的先決條件,返回你為了學完所有課程所安排的學習順序。

可能會有多個正確的順序,你只要返回一種就可以了。如果不可能完成所有課程,返回一個空陣列。

題解

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        //佇列儲存入度=0的節點
        Queue<Integer> queue = new LinkedList<>();
        //構建鄰接表
        List<List<Integer>> adjacent = new ArrayList<>();
        for(int i=0; i<numCourses; i++){
            adjacent.add(new ArrayList<>());
        }
        //構建入度表
        int[] indegrees = new int[numCourses];

        for(int[] courseCouples:prerequisites){
            indegrees[courseCouples[0]]++;
            adjacent.get(courseCouples[1]).add(courseCouples[0]);
        }
        //將入度=0的課程,入隊
        for(int i=0; i<numCourses; i++){
            if(indegrees[i]==0){
                queue.add(i);
            }
        }
        //構建返回的課程表
        int[] ret = new int[numCourses];
        int count=0;
        //遍歷
        while(!queue.isEmpty()){
            int pre = queue.poll();
            ret[count++]=pre;
            for(int cur:adjacent.get(pre)){
                if(--indegrees[cur]==0){
                    queue.add(cur);
                }
            }
        }
        return count==numCourses?ret:new int[0];
    }
}

這個與上一題的差別在於,這裡記錄課程的學習順序。

擴充套件

通過DFS(深度優先遍歷也可以判斷圖中是否有環)

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> adjacency = new ArrayList<>();
        for(int i = 0; i < numCourses; i++)
            adjacency.add(new ArrayList<>());
        //構建一個標誌列表,判斷每個節點的狀態。每個節點表示一門課程,預設為0,即未被DFS訪問
        int[] flags = new int[numCourses];
        for(int[] courses : prerequisites)
            adjacency.get(courses [1]).add(courses [0]);
        //對每個節點依次執行DFS,判斷每個節點起步DFS是否存在環
        for(int i = 0; i < numCourses; i++)
            if(!dfs(adjacency, flags, i)) return false;
        //若整個圖DFS結束,也沒有環
        return true;
    }
    private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
        //若flags[i]==1,則已被當前節點啟動的DFS訪問,即本輪DFS搜尋中,當前節點已經被第二次訪問,即有環
        if(flags[i] == 1) return false;
        //若flags[i]==-1,則已被其他節點啟動的DFS訪問,無需再重複搜尋
        if(flags[i] == -1) return true;
        //否則flag=0,說明該節點之前沒有被訪問過,那麼第一次訪問=1
        flags[i] = 1;
        //遍歷當前節點的鄰接節點,判斷是否有環
        for(Integer j : adjacency.get(i))
            if(!dfs(adjacency, flags, j)) return false;
        //當前節點所有鄰接節點已被遍歷,並沒有發現環,則將該節點置為-1
        flags[i] = -1;
        return true;
    }
}

課程表III

這裡有 n 門不同的線上課程,他們按從 1 到 n 編號。每一門課程有一定的持續上課時間(課程時間)t 以及關閉時間第 d 天。一門課要持續學習 t 天直到第 d 天時要完成,你將會從第 1 天開始。

給出 n 個線上課程用 (t, d) 對錶示。你的任務是找出最多可以修幾門課

題解

class Solution {
    public int scheduleCourse(int[][] courses) {
        //按課程結束時間升序
        Arrays.sort(courses, (a,b)->(a[1]-b[1]));
//課程用時的大根優先順序 Queue
<Integer> queue = new PriorityQueue<>( (a,b)->(b-a)); //總用時 int times=0; for(int i=0; i<courses.length; i++){
//如果該課程可以學習(學習時長合適),則學習。總時長增加,課程入堆
if(times+courses[i][0]<=courses[i][1]){ times+=courses[i][0]; queue.add(courses[i][0]); }
//如果該課程不能學習,因為此課程的結束時間比之前所有的都晚。
else{
//先把該課程加上 queue.add(courses[i][
0]); //放棄之前用時最長的課程,如果當前課程是最長用時,那麼放棄當前課程(此課程比之前課程用時多,不學該課程)
//放棄之前用時最長的課程(此課程比之前課程用時少,學該課程) times=times+courses[i][0]-queue.poll(); } } return queue.size(); } }

思路

貪心演算法

按照課程結束時間排序?因為課程持續時間並不能保證 當對課程遍歷時每個課程都是合法的。先學習結束時間最靠前的最好,理想的情況是:所有前一門課程的結束時間和下一門課程的開始時間都不相交(目前用時+課程時長<課程關閉時間),那麼就可以學習所有課程。

一旦發現安排了當前課程之後,其結束時間超過最晚結束時間,那麼就從已安排的課程中,取消掉當前最耗時的一門課程

舉例:

如[[4,6],[5,5],[2,6]]

按照課程結束時間排序,[[5,5],[4,6],[2,6]]

課程的起始時間為0,首先選擇課程1:[5,5],那麼當前已選課程為[[5,5]]

課程的起始時間為5,接著選擇課程2:[4,6],那麼如果選擇學習課程2,持續時間4+當前起始時間5=9>要求的結束時間6,因此不可取。

那麼對於這兩門課程,無論怎麼選擇,都只能選擇其中的一門課程。

那麼肯定是選擇這兩門課程耗時更短的課程,因為當前起始時間相同,如果該課程耗時更短,那麼該課程也會更早結束,那麼留給其他課程的時間會更寬裕。課程2[4,6]的耗時小於課程1[5,5],那麼就用課程2替換掉課程1。

先把該課程加上,已選課程為[[5,5],[4,6]]

當前起始時間就變為5+4-5(耗時更長的課程)=4,那麼當前已選課程為[[4,6]]

再考慮課程3[2,6],當前起始時間為4,那麼如果學習課程3,持續時間2+當前起始時間4=要求的結束時間6,那麼就可以學該課程,把課程加上,並且當前起始時間=6

所以queue中的課程為[[4,6],[2,6]],size=2