圖 Graph

  1. 鄰接矩陣
    一維數組存儲頂點信息(數據元素),二維數組存儲邊或弧的信息(數據元素之間的關係)java

    int[][] map = new int[n][n];// 頂點 0 ~ n-1, map[i][j] 表示頂點 i 到頂點 j 的邊或邊的權值
  2. 鄰接表
    稀疏圖或結點數超過必定數量(如100K)
    List<Integer>[] map = new List[n]; // 頂點 0 ~ n-1, map[i] 表示頂點 i 的全部鄰接點
  3. <E, V> 最短路徑 Dijkstra 算法
    邊集 E, 默認點集 V={0,1,...,n-1}node

    struct Edge {
      int i, j; // 表示頂點 i 到頂點 j 的邊
      int w;    // 邊的權值
      boolean visited; // 訪問標誌位
    };
  4. BFS
    廣度優先遍歷 (Breadth-First Search): 從圖中某個頂點 v 出發,在訪問該頂點後依次訪問 v 的各個未被訪問的鄰接點,而後分別從這些鄰接點出發依次訪問它們的鄰接點,並使「先被訪問的頂點的鄰接點」先於「後被訪問的頂點的鄰接點」被訪問,直至圖中全部已被訪問的頂點的鄰接點都被訪問到;若此時圖中還有頂點未被訪問到,則另選圖中一個未被訪問的頂點做爲起始點,重複上述過程,直至圖中全部頂點都被訪問到。
    類比:樹是無向無環圖,樹的層次遍歷是 DFS 的特例
    思想:從圖中某個頂點 v 出發,由近至遠依次訪問和 v 有路徑相同且路徑長度爲 1,2,... 的頂點,每一輪遍歷的頂點都與頂點 v 的距離相同。設 dk 爲第 k 個頂點與起始點 v 的距離,容易推導出:BFS 中依序遍歷的頂點 i和 j 有 di <= dj. 利用這個結論能夠求解最短路徑等「最優解」的問題:第一次遍歷到目標頂點所通過的路徑爲最短路徑
    適宜求解:尋找最優解,求解無權圖的最短路徑
    優勢:不需回溯。
    缺點:在樹的層次較深或頂點的鄰接點較多時,消耗內存十分嚴重。
    程序實現 BFS: 用隊列保存每一輪遍歷獲得的頂點;標記已遍歷過的頂點。
    走迷宮:最短路徑算法

    計算網格中從原點 (0,0) 到指定點 (tr,tc) 的最短路徑長度,1表示該位置能夠通過
      {{1,1,0,1},
       {1,0,1,0},
       {1,1,1,1},
       {1,0,1,1}};
      
      class Point {
        int x, y;
        public Point(int x, int y) {
          this.x = x;
          this.y = y;
        }
      }
      int minPathLength(int[][] grids, int tr, int tc) {
          if(grids==null || grids.length==0 || grids[0].length==0
                         || tr>=grids.length || tc>=grids[0].length) {
              return -1;
          }
          // 方向數組,表示 →東E ←西W ↓南S ↑北N 四個方向
          final int[][] directions = {{1,0}, {-1,0}, {0,1}, {0,-1}};
          final int rows = grids.length, cols = grids[0].length;
          int result = 0;
          Queue<Point> queue = new LinkedList<Point>();
          queue.add(new Point(0,0)); // 原點
          grids[0][0] = 0; // 進隊列時標記,設爲 0 表示已被訪問過
          while(!queue.isEmpty()) {
              result++;
              int size = queue.size();
              while(size-- > 0) {
                  Point cur = queue.poll();
                  for(int[] dir: directions) {
                      Point next = new Point(cur.x+dir[0], cur.y+dir[1]);
                      if(next.x<0 || next.x>=rows || next.y>=cols || next.y<0
                                  || grids[next.x][next.y]==0) {
                          continue;
                      }
                      if(next.x==tr && next.y==tc) { // 找到位置
                          return result;
                      }
                      queue.add(next);
                      grids[cur.x][cur.y]=0;// 進隊列時標記,設爲 0 表示已被訪問過
                  }
              }
          }
          return -1;
      }
    成語接龍的最短單詞路徑
      輸入: beginWord = "hit", endWord = "cog", dict = ["hot","dot","dog","lot","log","cog"]
      輸出: 5
      解釋: 最短的單詞路徑爲 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 故路徑長度爲 5.
      
      int ladderLength(String start, String end, Set<String> dict) {
          if(start==null || end==null || dict==null || dict.size()==0) {
            return 0;
          }
          if(start.equals(end)) return 1;
          // 構造詞典,包含 start 和 end.
          List<String> words = new ArrayList<String>(dict);
          int startIndx = findWord(words, start);
          int endIndx   = findWord(words, end);
          // 建立以鄰接表表示的圖
          List<Integer>[] graph = buildGraph(words);
          return calculateLength(graph, startIndx, endIndx);
      }
      // 計算最短單詞路徑
      int calculateLength(List<Integer>[] graph, int startIndx, int endIndx) {
        boolean[] visited = new boolean[graph.length];
        Queue<Integer> queue = new LinkedList<Integer>();
        queue.add(startIndx);
        visited[startIndx] = true;
        int result = 1;
        while(!queue.isEmpty()) {
            result++;
            int size = queue.size();
            while(size-- > 0) {
                int cur = queue.poll();
                for(int next: graph[cur]) {
                    if(next==endIndx) return result;
                    if(!visited[next]) {
                        queue.add(next);
                        visited[next]=true;
                    }
                }
            }
        }
        return result;
    }
    // 建立以鄰接表表示的圖
    List<Integer>[] buildGraph(List<String> words) {
        List<Integer>[] result = new List[words.size()];
        for(int i=0; i<words.size(); i++) {
            result[i] = new ArrayList<Integer>();
            for(int j=0; j<words.size(); j++) {
                if(isConnective(words.get(i), words.get(j))) {
                    result[i].add(j);
                }
            }
        }
        return result;
    }
    // 判斷單詞之間是否能經過改變一個字符而相互轉換
    boolean isConnective(String word1, String word2) {
        if(word1.length()!=word2.length()) return false;
        int diffCnt = 0;
        for(int i=0; i<word1.length(); i++) {
            if(word1.charAt(i)!=word2.charAt(i)) diffCnt++;
        }
        return diffCnt==1;
    }
    // 在單詞列表中查找單詞
    int findWord(List<String> words, String word) {
        if(words.contains(word)) {
            int i=0;
            for(; i<words.size(); i++) {
                if(word.equals(words.get(i))) break;
            }
            return i;
        } else {
            words.add(word);
            return words.size()-1;
        }
    }
  5. DFS
    深度優先遍歷 (Depth-First Search): 從圖中某個頂點 v 出發,訪問該頂點,而後依次從 v 的未被訪問的的鄰接點出發深度優先遍歷圖,直至圖中全部和 v 有路徑相通的頂點都被訪問到;若此時圖中還有頂點未被訪問到,則另選圖中一個未被訪問的頂點做爲起始點,重複以上過程,直至圖中全部頂點都被訪問到。
    類比:樹是無向無環圖,樹的先序優先遍歷是 DFS 的特例
    思想:從圖中某個頂點 v 出發,沿着一條通路走到底,若不能達到目標解(沒有未被訪問的頂點),回溯到上一個頂點,沿另外一條通路走到底。
    適宜求解 「可達性」 問題:給定初始狀態跟目標狀態,要求判斷從初始狀態到目標狀態是否有解。
    優勢:內存消耗小
    缺點:難以尋找最優解,僅僅只能尋找是否有解
    程序實現 DFS: 用棧保存當前頂點信息,當遍歷其鄰接點返回後可以繼續遍歷當前頂點(可以使用遞歸棧);標記已遍歷過的頂點。
    求 n 個物品的組合狀況:複雜度爲 2^n數組

    給定一個包含了一些 0 和 1的非空二維數組 grid, 一個島嶼是由四個方向 (水平或垂直) 的 1 (表明土地) 構成的組合,
     你能夠假設二維矩陣的四個邊緣都被水包圍着。找到給定的二維數組中最大的島嶼面積。
     輸入: [[0,0,1,0,0,0,0,1,0,0,0,0,0],
         [0,0,0,0,0,0,0,1,1,1,0,0,0],
         [0,1,1,0,1,0,0,0,0,0,0,0,0],
         [0,1,0,0,1,1,0,0,1,0,1,0,0],
         [0,1,0,0,1,1,0,0,1,1,1,0,0],
         [0,0,0,0,0,0,0,0,0,0,1,0,0],
         [0,0,0,0,0,0,0,1,1,1,0,0,0],
         [0,0,0,0,0,0,0,1,1,0,0,0,0]]
     輸出: 6
    
    int maxAreaOfIsland(int[][] grid) {
        if(grid==null || grid.length==0 || grid[0].length==0) {
            return 0;
        }
        int maxArea = 0;
        for(int i=0; i<grid.length; i++) {
            for(int j=0; j<grid[0].length; j++) {
                maxArea = Math.max(maxArea , dfs(grid, i, j));
            }
        }
    }
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 東南西北
    int dfs(int[][] grid, int row, int col) {
        if(row<0 || row>=grid.length || col<0 || col>=grid[0].length || grid[row][col]==0) {
            return 0;
        }
        grid[row][col]=0;
        int area = 1;
        for(int[] dir: dirs) {
            area += dfs(grid, row+dir[0], col+dir[1]);
        }
        return area;
    }
  6. 最小生成樹
    在n個城市之間創建通訊網絡, 連通這n個城市只需n-1條線路, 如何使總代價最小.
    最小生成樹: 連通網中權值最小的邊必定在最小生成樹中.網絡

    Prim算法: 連通網 N={V,{E}}. 初始化頂點集U={u0}, 最小生成樹中邊的集合TE={}; 重複如下步驟:
             在全部邊<u,v> ∈ E (u∈U, v∈V-U) 中找一條權值最小的邊 <u0,v0> 併入集合 TE, 
             同時將 v0 併入集合 U, 直到 U=V 爲止.
             最終 TE 中必有 n-1 條邊, 最小生成樹 MST={U,{TE}}.
    int prim(int[][] g, int n) { // 鄰接矩陣 g[n][n]
        int res = 0;
        int[] dis=new int[n]; // 頂點到最小生成樹的距離
        int[] par=new int[n]; // 頂點 i 的父節點 par[i]
        boolean[] vt=new boolean[n]; // 頂點訪問標誌
        vt[0]=true;
        for(int i=1; i<n; i++) {
            dis[i]=g[0][i];
        }
        for(int i=1; i<n; i++) {
            // 在未被訪問的頂點中找到與最小生成樹頂點集合中距離最短的頂點
            int k=-1, minDis=Integer.MAX_VALUE;
            for(int j=0; j<n; j++)
                if(!vt[j] && minDis>dis[j]){
                    minDis=dis[j]; k=j;
                }
            if(k==-1) return -1;
            // 加入到最小生成樹的頂點集合裏
            vt[k]=true; res+=minDis;
            // 加入頂點更新各個頂點到最小生成樹的距離
            for(int j=0; j<n; j++) 
                if(!vt[j] && dis[j]>g[k][j]) {
                    par[j]=k; dis[j]=g[k][j];
                }
        }
        return res;
    }
    Kruskal算法:連通網 N={V,{E}}.初始化最小生成樹爲只有 n 個頂點而無邊的非連通圖 T={V, {}}, 圖中
                每一個頂點自成一個連通份量; 在 E 中選擇權值最小的邊, 若該邊的兩個頂點落在 T 中不一樣的
                連通份量上, 則將此邊加入到 T 中, 不然去掉此邊而選擇權值次小的邊; 依次類推, 直至 T
                中全部頂點都在同一連通份量上.
    class Edge{
        int i, j, cost; // 邊的起點、終點、權值
    }
    public List<Edge> primMST(List<Edge> edges) {
        if(edges==null || edges.size()<=1) return edges;
        List<Edge> res = new ArrayList<Edge>();
        Collections.sort(edges, new Comparator<Edge>(){ // 將邊按權值的升序排列
            public int compareTo(Edge e1, Edge e2) {
                if(e1.cost!=e2.cost) return e1.cost-e2.cost;
                if(e1.i==e2.i) return e1.j-e2.j;
                return e1.i-e2.i;
            }
        });
        Set<Integer> nodes = new HashSet<Integer>();
        for(Edge e: Edges) {
            nodes.add(e.i); nodes.add(e.j);
        }
        int[] id = new int[nodes.size()+1];
        for(Edge e: Edges) {
            int idI = find(id, e.i);
            int idJ = find(id, e.j);
            if(idI!=idJ) {
                id[idI] = idJ; result.add(e);
            }
        }
        if(result.size()==nodes.size()-1) return result;
        return new ArrayList<Edge>();
    }
    public int find(int[] id, int i) {
        while(id[i]!=0) i = id[i];
        return i;
    }
  7. 拓撲排序
    AOV-網:頂點表示活動、弧表示活動間優先關係的有向無環圖;弧 <i, j> 表示從頂點 i 到 j 的有向路徑。
    算法:在AOV-網中, 1) 選一個沒有前驅(入度爲0)的頂點且輸出; 2) 在圖中刪除該頂點以及以它爲起點的弧.
    重複步驟1)、2) 直至全部頂點都輸出.ui

    List<Integer> topologicalSort(List<Integer>[] g) {
        // 計算每一個頂點的入度
        int[] in = new int[g.length];
        for(List<Integer> nds: g) 
            for(int nd: nds) in[nd]++;
        Queue<Integer> s = new LinkedList<Integer>();
        // 入度爲 0 的頂點入隊
        for(int i=0; i<in.length; i++) 
            if(in[i]==0) s.add(i);
        List<Integer> res = new ArrayList<Integer>();
        while(s.size()>0) {
            // 每次從隊列中彈出入度爲 0 的頂點
            int nd = s.remove(); res.add(nd);
            // 將該頂點的鄰接點的入度減 1, 若結果爲 0 則將該鄰接點入隊
            for(int i: g[nd])
                if(--in[i]==0) s.add(i);
        }
        if(res.size()==g.length) return res;
        // 拓撲排序輸出的頂點數少於頂點總數, 則說明 AOE-網中有環
        throw new RuntimeException("AOV-Net has rings");
     }
  8. 關鍵路徑
    AOE-網:頂點表示事件、弧表示活動、弧的權值表示活動持續時間的有向無環圖;只有一個起點(入度爲 0 的頂點)和一個終點(出度爲 0 的頂點);定義活動實施的前後順序,決定工程完成的最短期。this

    ai 是弧 <j,k> 上的活動:
     活動 ai 的持續時間:弧 <j,k> 的權值 dut<j,k>;
     事件最先發生時間:ve[j] = max{ve[i]+dut<i,j>}
     事件最遲發生時間:vl[i] = min{ve[j]-dut<i,j>}
     活動最先發生時間:從起點 v0 到頂點 vj 的最長路徑長度 e[j]=ve[j]
     活動最遲發生時間:不推遲工期的最晚開工時間 l[j]=vl[k]-dut<j,k>
     關鍵路徑:AOE-網中從起點到終點最長路徑的長度(邊的權值之和)
     關鍵活動:關鍵路徑上的活動

    算法:code

    初始化 AOE-網;
     1) 令起點處 ve[0]=0, 按拓撲順序計算每一個頂點的最先發生時間 ve[j]=max{ve[i]+dut<i,j>}
     2) 令終點處 vl[n-1]=ve[n-1], 按拓撲逆序計算每一個頂點的最遲發生時間 vl[i]=min{ve[j]-dut<i,j>}
     3) 計算每一個弧上活動的最先發生時間 e[i]=ve[i] 和最遲發生時間 l[i]=vl[j]-dut<i,j>,
        若 e[i]=l[i] 則將該活動加入到關鍵路徑中。
    class Edge{
          int end, cost;
      }
      List<int[]> criticalPath(List<Edge>[] g) {
          Stack<Integer> s = new Stack<Integer>();
          int[] ve = topologic(g, s);
          int[] vl = reverseTop(g, s, ve);
          List<int[]> res = new ArrayList<int[]>();
          for(int i=0; i<g.length; i++)
              for(Edge e: g[i])
                  if(ve[i]==vl[e.end]-e.cost)
                      res.add(new int[]{i, e.end, e.cost});
          return res;
       }
       int[] reverseTop(List<Edge>[] g, Stack<Integer> s, int[] ve) {
           int[] vl = new int[g.length];
           Arrays.fill(vl, Integer.MAX_VALUE);
           vl[g.length-1]=ve[g.length-1];
           while(s.size()>0) {
               int nd = s.pop();
               for(Edge e: g[nd])
                   vl[nd] = Math.min(vl[nd], vl[e.end]-e.cost);
           }
           return vl;
       }
       int[] topologic(List<Edge>[] g, Stack<Integer> revs) {
           int[] in=new int[g.length];
           for(List<Edge> es: g)
               for(Edge e: es) in[e.end]++;
           Stack<Integer> s = new Stack<Integer>();
           for(int i=0; i<in.length; i++)
               if(in[i]==0) s.push(i);
           int[] ve = new int[g.length];
           while(s.size()>0) {
               int nd = s.pop(); revs.add(nd);
               for(Edge e: g[nd]) {
                   if(--in[e.end]==0) s.push(e.end);
                   ve[e.end] = Math.max(ve[e.end], ve[nd]+e.cost);
               }
           }
           if(revs.size()<g.length()) throw new RuntimeException("AOE-Net has rings");
           return ve;
       }
  9. 最短路徑 Dijkstra 算法排序

    1) 鄰接表表示的有向圖中,集合 S 表示源點 v 可達的且找到最短路徑的頂點集合,初始化爲空,
       數組元素 dis[i] 表示當前從源點 v 到頂點 vi 的最短路徑長度,初始化爲 dis[i] = g[v][i].
    2) 選擇 vj 使得 dis[j] = min{dis[i]|v∈V-S}, 將頂點 vj 加入到集合 S 中。
    3) 修改從 v 出發到 V-S 中任一頂點 vk 可達的最短路徑長度: dis[k]=max{dis[k], dis[j]+g[j][k]}.
    4) 重複步驟 2) 和 3) n-1 次。
    Map<Integer, String> dijkstraShortestPath(int[][] g, int v) {
         int[] dis=new int[g.length];
         Map<Integer, String> idToPath = new HashMap<Integer, String>();
         for(int j=0; j<g[v].length; j++) {
             path.put(j, ""+j);
             if(v==j) dis[j]=0;
             else if(g[v][j]==-1) // j 不可達表示爲 g[v][j]=-1;
                 dis[j]=Integer.MAX_VALUE;
             else {dis[j]=g[v][j]; idToPath.put(j, v+"-->"+j);}
         }
         Queue<Integer> visited = new LinkedList<Integer>(); // v 可達且找到最短路徑的頂點集合
         visited.add(v);
         
         while(visited.size()<g.length) {
             int k = getClosestV(dis, visited);
             if(k==-1) break;
             visited.add(k);
             for(int j=0; j<g[k].length; j++) {
                 if(g[k][j]!=-1) {
                     if(dis[j]>dis[k]+g[k][j]) {
                         dis[j]=dis[k]+g[k][j];
                         idToPath.put(j, idToPath.get(k)+"-->"+j);
                     }
                 }
             }
         }
         for(int j=0; j<dis.length; j++)
             if(dis[j]==0 || dis[j]==Integer.MAX_VALUE) idToPath.remove(j);
             else idToPath.put(j, idToPath.get(j)+"("+dis[j]+")");
         return idToPath;
     }
     int getClosest(int[] dis, Queue<Integer> visited) {
         int min=Integer.MAX_VALUE, minIndex=-1;
         for(int i=0; i<dis.length; i++) {
             if(!visited.contains(i)) {
                 if(dis[i]<min) {
                     min=dis[i]; minIndex=-1;
                 }
             }
         }
         return minIndex;
     }
  10. 最短路徑 Floyd 算法遞歸

    1) 初始化最短路徑矩陣 p, 使得 p[i][j]=g[i][j];
    2) vi->vk 表示從 vi 到 vk 的中間頂點序號不大於 k-1 的最短路徑,
       vk->vj 表示從 vk 到 vj 的中間頂點序號不大於 k-1 的最短路徑,
       將已知的 vi->vj 即從 vi 到 vj 的中間頂點序號不大於 k-1 的最短路徑與 vi->vk和vk->vj 比較,
       長度較短者是從 vi 到 vj 的中間頂點序號不大於 k 的最短路徑
       p(k)[i][j] = min{p(k-1)[i][j], p(k-1)[i][k]+p(k-1)[k][j]}
    3) 通過 n 次比較後最終求得 vi->vj 的最短路徑
    4) 重複2) 3) 最終求得每對頂點之間的最短路徑
    class DisPath {
        int dis;
        String path;
        public DisPath(int dis, String path) {
            this.dis  = dis;
            this.path = path;
        }
    }
    void floyd(int[][] g) {
        DisPath[][] p = new DisPath[g.length][g[0].length];
        for(int k=0; k<g.length; k++)
            for(int i=0; i<g.length; i++)
                for(int j=0; j<g.length; j++) { // 從 i 通過 k 到 j 的一條路徑更短
                    int temp = (p[i][k].dis==Integer.MAX_VALUE || 
                                p[k][j].dis==Integer.MAX_VALUE) ? Integer.MAX_VALUE:
                                p[i][k].dis + p[k][j].dis;
                    if(temp<p[i][j].dis) {
                        p[i][j].dis = temp;
                        p[i][j].path = p[i][k].path + p[k][j].path.substring(1);
                    }
                }
    }
相關文章
相關標籤/搜索