本文是刷leetcode第207題的感想,《演算法》4th english version這本書已經很久沒看了,嘗試自己寫出有向圖的leetcode題,心想以前在公司專案裡擼過DAG(有向無環圖),現在做題肯定不在話下,而且正好檢測一下學習成果,也能看看自己的理解是否深刻。
結果做倒是做出來了,就是時間慘不忍睹(250ms,僅超越約6%),很尷尬,再看了看書,發現以前對書上演算法的理解有問題,特此記錄。
我的做題思路
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//完成所有課程,說明沒有環(有環就互為前置了)
//需要都可達嗎? 不需要。。並沒有這個需求
Object[] adj = new Object[numCourses];
boolean[] onStack = new boolean[numCourses];
//構造出鄰接連結串列,這不重要
int i = prerequisites.length;
for(int j = 0;j<i;j++){
Integer left = prerequisites[j][1];
Integer right = prerequisites[j][0];
if(adj[right] == null){
Set<Integer> set = new HashSet<>();
set.add(left);
adj[right] = set;
}else{
Set<Integer> set = (Set<Integer>)adj[right];
set.add(left);
adj[right] = set;
}
}
//對每個節點進行一次dfs,如果發現一次迴圈之後有環就退出
for(int k = 0;k<numCourses;k++){
boolean b = dfs(k,adj,onStack);
if(b==false){return false;}
}
return true;
}
private boolean dfs(int i, Object[] adj, boolean[] onStack){
//是否在方法呼叫棧上,也就是,是否在來當前節點的路徑上
//這裡的棧,其實是方法呼叫棧的思想,思想是棧,但是為了能隨意獲取某個元素用了陣列
onStack[i] = true;
Set<Integer> set = (Set<Integer>)adj[i];
if(set == null){
onStack[i] = false;
return true;};
for(Integer integer: set){
if(onStack[integer]==true){
return false;}
boolean b = dfs(integer,adj,onStack);
if(!b){return b;}
}
onStack[i] = false;
return true;
}
}
複製程式碼
與書上演算法的區別
public class DirectedCycle {
private boolean[] marked; // marked[v] = has vertex v been marked?
private int[] edgeTo; // edgeTo[v] = previous vertex on path to v
private boolean[] onStack; // onStack[v] = is vertex on the stack?
private Stack<Integer> cycle; // directed cycle (or null if no such cycle)
public DirectedCycle(Digraph G) {
marked = new boolean[G.V()];
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
for (int v = 0; v < G.V(); v++)
if (!marked[v] && cycle == null) dfs(G, v);
}
// check that algorithm computes either the topological order or finds a directed cycle
private void dfs(Digraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (int w : G.adj(v)) {
// short circuit if directed cycle found
if (cycle != null) return;
// found new vertex, so recur
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}
// trace back directed cycle
else if (onStack[w]) {
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
}
}
onStack[v] = false;
}
}
複製程式碼
看了看書,發現書上的變數比我多兩個edgeTo、marked,edgeTo是因為需要記錄一下到底環由哪幾個節點構成,我們這裡只需要檢測是否有環,因此edgeTo不需要。
那麼再來看marked
- 在DirectedCycle方法中,就已經用到了marked,也就是,如果已經到過的節點,那麼就不做dfs了。
- 在dfs中
- 對於沒有到過的節點,做dfs
- 到過的節點&在方法呼叫棧上,就說明有環
- 到過的節點&不在方法呼叫棧上,說明在其他路徑上已經處理過該節點,什麼都不做
這裡有兩個問題:
- 在DirectedCycle方法中,對於到過的節點不做dfs
- 在dfs方法中,對於到過&不在方法呼叫棧上的節點,什麼都不做
也可以說是一個問題:
從其他節點出發到過的節點,或者對其他節點dfs已經處理過的節點,是否能確保是安全的。
這裡的安全包括兩點:
- 如果已經處理過的路徑有環,那麼正在處理的路徑也有環(這種情況實際不可能存在,因為如果已經處理過的路徑有環,那麼方法就會退出,也不會處理新的路徑了)
- 如果已經處理過的路徑無環,假如已經處理過的路徑的一部分是正在處理的路徑的一部分,那麼已經處理過的路徑所在的部分無環。(如果包含已處理過路徑的正在處理路徑有環,那麼被包含的部分應該也在環中,該命題成立,則需要被證明的逆否命題也成立)
因此可以得出已被處理過的節點是安全的結論,因此可以忽略marked==true的節點。
我在我的程式碼中加入了marked優化,直接讓執行時間從250ms降低到了6ms!!!
一個小重點
其實有一個重點結論:如果dfs到了存在於環中的某個節點,那麼一定會進入環!那麼他的逆否命題就是,如果dfs處理過的節點沒有進入環,那麼該節點一定不在環中
因為節點在環中,那麼節點的鄰接連結串列中一定存在一個同樣處在環中的節點。這麼顯而易見的特點,一定一定要利用起來。
誤區
之前是直接看的書,沒有刷題,有些東西沒有想透,比如這個marked,我認為書中的程式碼,是要實現的功能的最小可用版本,但是marked又看不懂,而for迴圈又是從0開始的,書上的各種圖都是0就是起始節點,因此我認為,只有在從起始結點開始dfs時,這個marked也有用。包括我在做專案時,我也定了一個起始節點。
但是在做題的時候發現,他並沒有一個起始節點,我對每一個節點做dfs毫無問題,加上marked之後也毫無問題,於是我發現了,並不需要從一個起始節點開始dfs, 即使一開始dfs的是比較靠後的節點,之後迴圈的是靠前的節點,那也是沒有問題的,而且對這種情況可以做優化。
我以為marked是實現某個功能的,沒想到marked是為了優化。但是其實我也能感覺到自己的思路有問題,假設0123是有關聯關係的,45是有關聯關係的,那難道就一定是4之後是5嗎?書中完全沒有這樣的前提條件,是我自己在瞎想罷了。
之所以沒想通,還是因為有向圖和無向圖的區別其實有點大,無向圖中的marked好理解,有向圖中的marked還是多了些需要注意的地方。
edgeTo
可以注意到,只有marked[w]為false的時候才新增edgeTo[w]=v,也就是說,對每個節點來說,只有第一次到達這個節點的時候才設定edgeTo,那麼這是為什麼呢?
會不會edgeTo設定錯了?也就是說edgeTo對某個在環中的點設定了不在環中的路徑?基於此假設,那麼
- 節點V應有某個不在環中的節點指向V
- 節點V在環中
那麼V應該是這樣的樣子:
因為V在環中,所以V除了不在環中的節點指向V,其他必然至少有兩條路徑,一條出,一條入,而這條出的路徑又在環中,那麼根據程式碼和畫圖來看
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
複製程式碼
雖然edgeTo[w]不等於V,但是程式碼裡對w和v進行了手動push,並不是利用edgtTo,因此沒有問題。
總結
今天的策略,即先自己寫再看書,今天看起來還是很成功的,不僅如此,以後刷題也要多看高效率的題解,多想著從各個角度解決問題。不過,第一遍目測還是做完拉倒吧哈哈哈哈。