-
邻接矩阵
一维数组存储顶点信息(数据元素),二维数组存储边或弧的信息(数据元素之间的关系)int[][] map = new int[n][n];// 顶点 0 ~ n-1, map[i][j] 表示顶点 i 到顶点 j 的边或边的权值
- 邻接表
稀疏图或结点数超过一定数量(如100K)
List<Integer>[] map = new List[n]; // 顶点 0 ~ n-1, map[i] 表示顶点 i 的所有邻接点 -
<E, V> 最短路径 Dijkstra 算法
边集 E, 默认点集 V={0,1,...,n-1}struct Edge { int i, j; // 表示顶点 i 到顶点 j 的边 int w; // 边的权值 boolean visited; // 访问标志位 };
-
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; } }
-
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; }
-
最小生成树
在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; }
-
拓扑排序
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"); }
-
关键路径
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; }
-
最短路径 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; }
-
最短路径 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); } } }
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。