数据结构
对于点稠密的图,可以使用邻接矩阵表示g[n][n],其中n表示图中的节点数,g[i][j]表示是否存在从i到j的路径(无权图)或者从节点i到节点j的权重(有权图)。
对于边稠密的图,如果是无权图可以使用邻接表List<Integer>[] g
表示,其中g[i]表示与节点i邻接的点;如果是有权图,可以使用邻接表’List<Integer, Map<Integer, Integer>> g`或者
1 2 3 4 5
| class Edge { int to; int cost; } List<Integer, Edge> g;
|
两种表示图的数据结构方法的算法分析如下:
ADT |
空间复杂度 |
检查w是否与v邻接 |
遍历所有与v邻接的点 |
邻接矩阵 |
V * V |
1 |
V |
连接表 |
E + V |
degree(v) |
degree(v) |
搜索
深度优先搜索(DFS)
使用一个标记数组标记已被访问过的节点,每次递归标记当前节点并访问与当前节点邻接的节点。可使用并查集记录dfs过程中节点的前驱节点,需要求源点到某节点路径时,从并查集中恢复即可。
1 2 3 4 5 6 7
| void dfs(List<Integer>[] g, boolean[] marked, int cur) { marked[cur] = true; for(int i : g[cur]) { if(!marked[i]) dfs(g, marked, i); } }
|
广度优先搜索(BFS)
使用一个队列保存与当前节点距离为1的未访问节点,直到队列为空,BFS可用于无权图中需找最短路径。与DFS类似,BFS也可以通过维护一个并查集记录BFS搜索过程中某节点的前驱节点,需要查找路径时,从并查集中恢复即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void bfs(List<Integer>[] g, int s) { boolean[] marked = new boolean[g.size()]; Deque<Integer> que = new ArrayDeque<Integer>(); int[] edgeTo = new int[g.size()]; marked[s] = true; que.add(s); while(!que.empty()) { int cur = que.remove(); for(int i : g[i]) { if(!marked[i]) { marked[i] = true; edgeTo[i] = cur; que.add(i); } } } }
|
单源最短路径
单源最短路径是固定一个起点,求它到其它所有点的最短路问题。终点固定的问题叫做两点之间的最短路问题。两点之间的最短路问题复杂度和单源最短路径问题相同,通常当做单源最短路径来求解。
Bellman-Ford算法
维护一个距离数组,初始源点到源点的距离为0其它距离都是无穷大。然后使用递推关系式d[i] = min(d[j] + (j到i的距离))
不断松弛距离值。松弛V - 1次(V表示图中顶点个数)即可求出源点s到所有顶点的最短距离值。如果循环次数超过V - 1次后仍然有值更新,则说明图中存在负圈。Bellman-Ford算法时间复杂度O(VE)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Edge { int from, cost, to; } Edge edge[E]; int dis[V];
boolean shortest_path(int s) { for(int i : dis) i = Integer.MAX_VALUE; dis[s] = 0; for(int i = 0; i < V; ++i) { for(int j = 0; j < E; ++j) { Edge e = edge[j]; if(dis[e.from] != Integer.MAX_VALUE && dis[e.to] < dis[e.from] + e.cost) { dis[e.to] = dis[e.from] + e.cost; if(i == V - 1) return true; } } } }
|
Dijkstra算法
Bellman-Ford算法中,每次循环都考察每条边的松弛情况,Dijkstra算法对这种情况进行了改进:每次确定一个最短距离的点,从这些点出发更新相邻顶点的最短距离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int dis[V];; boolean marked[V]; Edge[V] g;
void dijkstra(int s) { for(int i : dis) i = Integer.MAX_VALUE; dis[s] = 0; while(true) { int v = -1; for(int i = 0; i < V; ++i) if(!marked[i] && (v == -1 || dis[v] > dis[i])) v = i; if(v == -1) break; marked[v] = true; for(Edge e : g[v]) dis[e.to] = Math.min(dis[e.to], dis[v] + e.cost); } }
|
以上算法时间复杂度为O(V * V + E),可以使用一个优先队列加速最小距离顶点的查找,使得时间复杂度降低到O(VlogV + E)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Node implements Comparable{ int v, dis; public Node(int v, int dis) { this.v = v; this.dis = dis; } public int compareTo(Node other) { reutrn this.dis - other.dis; } }
void dijkstra(int s) { for(int i : dis) i = Integer.MAX_VALUE; dis[s] = 0; Deque<Node> que = new PriorityQueue<Node>(); que.add(new Node(s, 0)); while(!que.empty()) { Node node = que.remove(); if(node.dis < dis[node.v]) continue; int v = node.v; for(Edge e : g[v]) que.add(new Node(e.to, e.cost)); } }
|
Floyd-Warshall算法
Floyd-Warshall算法可以用于求解任意两点间的最短路问题,时间复杂度为O(VVV)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int dis[V][V]; void warshall_floyd() { for(int i = 0; i < V; ++i) { for(int j = 0; j < V; ++j) { if(i == j) dis[i][j] = 0; else if(g[i][j] != Integer.MAX_VALUE) dis[i][j] = g[i][j]; else dis[i][j] = Integer.MAX_VALUE; } } for(int k = 0; k < V; ++k) { for(int i = 0; i < V; ++i) { for(int j = 0; j < V; ++j) { dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]); } } } }
|
最小生成树
给定一个无向图,如果它的某个子图中任意两个顶点都相互连通并且是一棵树,那么这棵树就叫做生成树(Spanning Tree)。如果边上有权值,那么使得边权和最小的生成树叫做最小生成树(MST,Minimum Spanning Tree)。
Prim算法
Prim算法和Dijkstra算法非常相似,都是从某个顶点出发,不断添加边的算法。初始时生成树是一个只有一个顶点的树,然后贪心地选取与T相连的最小权值的边,并把它加到树中。不断执行这个过程,直到所有顶点都被添加到树中即可。Prim算法与Dijkstra算法时间复杂度一样,为O(V * V + E),可以使用一个优先队列加速最小距离顶点的查找,使得时间复杂度降低到O(VlogV + E)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int dis[V]; boolean marked[V];
int prim() { dis[0] = 0; int result = 0; while(true) { int v = -1; for(int i = 0; i < V; ++i) { if(!marked[i] && (v == -1 || dis[v] < dis[i]) v = i; } if(v == -1) break; marked[v] = true; result += dis[v]; for(int i = 0; i < V; ++i) dis[i] = Math.min(dis[i], g[v][i]); } }
|
Kruskal算法
Kruskal算法按照边的权值从小到大的顺序排序,然后依次选择边,如果加入新边后生成树不形成环,则加入生成树。可以使用并查集高效判断加入某边后是否会形成环。对于边<u, v>
,如果加入边之前u,v不在同一个连通分量中,那么加入边后也不会产生环。反之,如果u和v在同一个连通分量中,那么一定会产生环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Edge edges[V];
int kruskal() { Arrays.sort(edges); init_union(V); int result = 0; for(int i = 0; i < E; ++i) { Edge e = edges[i]; if(!same(e.u, e.v)) { union(e.u, e.v); result += e.cost; } } return result; }
|