1. 邻接矩阵
    一维数组存储顶点信息(数据元素),二维数组存储边或弧的信息(数据元素之间的关系)

    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}

    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) 直至所有顶点都输出.

    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 的顶点);定义活动实施的先后顺序,决定工程完成的最短时间。

    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-网中从起点到终点最长路径的长度(边的权值之和)
     关键活动:关键路径上的活动
    

    算法:

    初始化 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);
                    }
                }
    }

KitorinZero
7 声望1 粉丝

« 上一篇
数组排序