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