图
无向图
图是由— 组顶点和能够将两个顶点相连的边组成的。无向图的边 (edge)仅仅是两个顶点(vertex)之间的连接。
我们用v-w的记法来表示连接V和W的边,W-V是这条边的另一种表示方法。
在图中,路径是由边顺序连接的一系列顶点。简单路径是一条没有重复顶点的路径。环是一条至少含有一条边且起点和终点相同的路径。 简单环是一条(除了起点和终点必须相同之外 )不含有重复顶点和边的环。路径或者环的长度为其中所包含的边数。
当两个顶点之间存在一条连接双方的路径时,我们称一个顶点和另一个顶点是连通的。
树是一幅无环连通图。互不相连的树组成的集合称为森林。连通图的生成树是它的一幅子图,它含有图中的所有顶点且是一棵树。圈的生成树森林是它的所有连通子图的生成树的集合。
当且仅当一幅含有F 个结点的图G 满足下列5 个条件之一时,它就是一棵树:
- G 有F - 1条边且不含有环;
- G有条边且是连通的;
- G 是连通的,但删除任意一条边都会使它不再连通;
- G 是无环图,但添加任意一条边都会产生一条环;
- G 中的任意一对顶点之间仅存在一条简单路径。
我们要面对的下一个图处理问题就是用哪种方式(数据结构)来表示图并实现这份API,这包含以下两个要求:
- 它必须为可能在应用中碰到的各种类型的图预留出足够的空间;
- Graph的实例方法的实现一定要快—— 它们是开发处理图的各种用例的基础
图的几种表示方法
邻接矩阵:我们可以使用一个v乘 v 的布尔矩阵。当顶点v 和顶点w之间有相连接的边 时,定义V 行 W列的元素值为true,否则为false。这种表示方法不符合第一个条件—— 含有上百万个顶点的图是很常见的,V^2^个布尔值所需的空间是不能满足的。
边的数组:我们可以使用一个Edge类,它含有两个i n t 实例变量。这种表示方法很简洁但不满足第二个条件—— 要实现adj ( ) 需要检查图中的所有边。
邻接表数组:我们可以使用一个以顶点为索引 的列表数组,其中的每个元素都是和该顶点相邻的顶点列表,参见4.1.9。这种数据结构能够同时满足典型应用所需的以上两个条件。
邻接表的数据结构
非稠密图的标准表示称为邻接表的数据结构,它将每个顶点的所有相邻顶点都保存在该顶点对应的元素所指向的一张链表中。
这种 Graph的实现的性能有如下特点:
- 使用的空间和成正比 ;
- 添加一条边所需的时间为常数
- 遍历顶点V 的所有相邻顶点所需的时间和V的度数成正比(处理每个相邻顶点所需的时间为常数)。
注意,边的插入顺序决定了 Graph的邻接表中顶点的出现顺序,多个不同的邻接表可能表示着同一幅图。当使用构造函数从标准输入中读入一幅图时, 这就意味着输入的格式和边的顺序决定了 G「aph的邻接表数组中顶点的出现顺序。因为算法在使用adj( ) 来处理所有相邻的顶点时不会考虑它们在邻接表中的出现顺序,这种差异不会影响算法的正确 性,但在调试或是跟踪邻接表的轨迹时我们还是需要注意这一点。
public class Graph {
private final int V;
private int E;
private Bag <Integer> [] adj;
public Graph(int V) {
this.V = V;
this.E = 0;
adj = (Bag < Integer> []) new Bag[V]; // 创建邻接表
for ( int v = 0; v < V; v++) //将所有链表初始化为空
adj[v] = new Bag < Integer> ();
}
public Craph(In in) {
this(in.readlntO); //读取V并将图初始化
int E = in.readInt(); //读取E
for (int i = 0; i < E; i++) {
int v = in.readInt(); //读取一个顶点
int w = in .readInt(); //读取另一个顶点
addEdge(v, w ) ; //添加一条连接它们的边
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(int v, int w) {
adj[v].add(w); //将w添加到v的链表中
adj[w].add(v); //将v添加到W 的链表中
E++;
}
public Iterable <Integer > adj(int v) {
return adj[v];
}
}
深度优先搜索
public class DepthFirstSearch {
private boolean[] marked;
private int count;
public DepthFirstSearch(Graph G,int s) {
marked = new boolean[G.V()];
dfs(G,s);
}
private void dfs(Graph G,int v) {
marked[v] = true;
count++;
for (int w : G.adj(v))
if (!marked[w])
dfs(G,w);
}
public boolean marked(int w) {
return marked[w];
}
public int count() {
return count;
}
}
首先要注意的是,算法遍历边和访问顶点的顺序与图的表示是有关的,而不只是与图的结构或是算法有关。因为深度优先搜索只会访问和起点连通的顶点。要注意的第二点是,如前文所述,深度优先搜索中每条边都会被访问两次,且在第二次时总会发现这个顶点已经被标记过。这意味着 深度优先搜索的轨迹可能会比你想象的长一倍!
寻找路径
public class DepthFirstPaths {
private boolean[] marked;
private int[] edgeTo;
private final int s;
public DepthFirstPaths(Graph G,int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
dfs(G,s);
}
private void dfs(Graph G,int v) {
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w]) {
edgeTo[w] = v;
dfs(G,w);
}
}
public boolean hasPathTo(int v) {
return marked[v];
}
public Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v))
return null;
Stack<Integer> path = new Stack<Integer>();
for (int x = v; x != s; x = edgeTo[x])
path.push(x);
path.push(s);
return path;
}
}
edgeTo[w]=v表示v-w是第一次访问w时经过的边。edgeTo[]数组是一棵用父链接表示的以s 为根且含有所有与s 连通的顶点的树。
广度优先搜索
要找到从s 到 v 的最短路径,从 s 开始,在所有由一条边就可以到达的顶点中寻找V,如果找不到我们就继续在与s 距离两条边的所有顶点中查找V ,如此一直进行。深度优先搜索就好像是一个人在走迷宫,广度优先搜索则好像是一组人在一起朝各个方向走这座迷宫,每个人都有自己的绳子。当出现新的叉路时,可以假设一个探索者可以分裂为更多的人来搜索它们,当两个探索者相遇时,会合二为一(并继续使用先到达者的绳子)
在深度优先搜索中,我们用了一个可以下压的栈(这是由系统管理的,以支持递归搜索方法)。使用LIFO ( 后进先出)的规则来描述压栈和走迷宫时先探索相邻的通道类似。从有待搜索的通道中选择最晚遇到过的那条。在广度优先搜索中,我们希望按 照与起点的距离的顺序来遍历所有顶点,看起来这种顺序很容易实现:使用(FIFO ,先进先出)队列来代替栈(LIFO,后进先出)即可。
public class BreadthFirstPaths {
private boolean[] marked;
private int[] edgeTo;
private final int s;
public BreadthFirstPaths(Graph G,int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
bfs(G,s);
}
private void bfs(Graph G,int s) {
Queue<Integer> queue = new Queue<Integer>();
marked[s] = true;
queue.enqueue(s);
while (!queue.isEmpty()) {
int v = queue.dequeue();
for (int w : G.adj(v)) {
if (!marked[w]) {
edgeTo[w] = v;
marked[w] = true;
queue.enqueue(w);
}
}
}
}
public boolean hasPathTo(int v) {
return marked[v];
}
public Iterable<Integer> pathTo(int v) {
}
}
这段G r a p h的用例使用了广度优先搜索,以找出图中从构造函数得到的起点s 到与其他所有顶点的 最短路径。b f s ( )方法会标记所有与s 连通的顶点,因此用例可以调用h a s P a t h T o O来判定一个顶点与 s 是否连通并使用p a t h T o O 得到一条从s 到 v 的路径,确保没有其他从s 到v 的路径所含的边比这条路径更少。
这两个算法的不同之处仅在于从数据结构中获取下一个顶点的规则(对于广度优先搜索来说是最早加入的顶点,对于深度优先搜索来说是最晚加入的顶 点 )。这种差异得到了处理图的两种完全不同的视角,尽管无论使用哪种规则,所有与起点连通 的顶点和边都会被检查到。
连通分量
使用深度优先搜索找出图中的所有连通分量
public class CC {
private boolean[] marked;
private int[] id;
private int count;
public CC(Graph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
for (int s = 0; s < G.V(); s++) {
if (!marked[s]) {
dfs(G,s);
count++;
}
}
}
private void dfs(Graph G,int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v))
if (!marked[w])
dfs(G,w);
}
public boolean connected(int v,int w) {
return id[v] == id[w];
}
public int id(int v) {
return id[v];
}
public int count() {
return count;
}
}
判断G是否为无环图(假设不存在自环或平行边)
public class Cycle {
private boolean[] marked;
private boolean hasCycle;
public Cycle(Graph G) {
marked = new boolean[G.V()];
for (int s = 0; s < G.V();s++)
if (!marked[s])
dfs(G,s,s);
}
private void dfs(Graph G,int v,int u) {
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w])
dfs(G,w,v);
else if (w != u) //排除刚访问过的父节点
hasCycle = true;
}
public boolean hasCycle() {
return hasCycle;
}
}
判断G是否为二分图(双色问题)
双色问题。能够用两种颜色将图的所有顶点着色,使得任意一条边的两个端点的颜色都不相同吗?这个问题也等价于:这是一幅二分图吗?
public class TwoColor {
private boolean[] marked;
private boolean[] color;
private boolean isTwoColorable = true;
public TwoColor(Graph G) {
marked = new boolean[G.V()];
color = new boolean[G.V()];
for (int s = 0; s < G.V(); s++) {
if (!marked[s])
dfs(G,s);
}
}
private void dfs(Graph G,int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
color[w] = !color[v];
dfs(G,w);
} else if (color[w] == color[v])
isTwoColorable = false;
}
}
public boolean isBipartite() {
return isTwoColorable;
}
}
符号图
在典型应用中,图都是通过文件或者网页定义的,使用的是字符串而非整数来表示和指代顶点。为了适应这样的应用,我们定义了拥有以下性质的输人格式:
- 顶点名为字符串
- 用指定的分隔符来隔开顶点名(允许顶点名中含有空格);
- 每一行都表示一组边的集合,每一条边都连 接着这一行的第一个名称表示的顶点和其他名称所表示的顶点;
- 顶点总数K和边的总数E都是隐式定义的。
Symbol Graph的完整实现用到了以下3 种数据结构:
- 一个符号表st ,键的类型为String ( 顶点名),值的类型为int ( 索引)
- 一个数组keys[],用作反向索引,保存每个顶点索引所对应的顶点名
- 一个Graph对象G,它使用索引来引用图中顶点
符号图的数据类型
public class Symbol Graph {
private ST<String, Integer〉 st;
private String[] keys;
private Graph G;
public SymbolGraph(String stream, String sp) {
st = new ST<String, Integer>();
In in = new In(stream);
while (in.hasNextLine()) {
String[] a = in.readLine().split(sp);
for (int i = 0 ; i < a.length; i++)
if (!st.contains(a[i]))
st.put(a[i],st.size());
}
keys = new String [st.size()];
for (String name : st.keys())
keys[st.get(name)] = name;
G = new Gr aph(st.sizeO);
in = new In(stream);
while (in.hasNextLine()) {
String[] a = in.readLine().split(sp);
int v = st.get(a[0] ) ;
for (int i = 1 ; i < a.length; i++)
G.addEdge(v, st.get(a[i]));
}
}
public boolean contains(String s) {
return st.contains(s);
}
public int index(String s) {
return st.get(s);
}
public String name(int v) {
return keys[v];
}
public Graph G() {
return G;
}
这个Graph实现允许用例用字符串代替数字索引来表示图中的顶点。它维护了实例变量st (符号表用来映射顶点名和索引) 、 keys ( 数组用来映射索引和顶点名)和 G ( 使用索引表示顶点的图)。为了构造这些数据结构,代码会将图的定义处理两遍(定义的每一行都包含一个顶点及它的相邻顶点列表, 用分隔符sp隔开)。
有向图
在有向图中,边是单向的:每条边所连接的两个顶点都是一个有序对,它们的邻接性是单向的。
一幅有方向性的图(或有向图) 是由一组顶点和一组有方向的边组成的,每条有方向的边都连接着有序的一对顶点。
我们称一条有向边由第一个顶点指出并指向第二个顶点。在一幅有向图中,一个顶点的出度为由该顶点指出的边的总数;一个顶点的入度为指向该顶点的边的总数。一条有向边的第一个顶点称为它的头, 第二个顶点则被称为它的尾。将有向边画为由头指向尾的一个箭头。用v — w 来表示有向图中一条由v 指向w 的边。
在一幅有向图中, 有向路径由l 系列顶点组成, 对于其中的每个顶点都存在一条有向边从它指向序列中的下一个顶点。有向环为一条至少含有一条边且起点和终点栢同的有向路径。简单有向环是一条(除了起点和终点必须相同之外) 不含有重复的顶点和边的环。路径或者环的长度即为其中所包含的边数。
Digraph数据类型
public (lass Digraph {
private final int V;
private int E ;
private Bag <Integer> [] adj;
public Digraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<Integer>[]) new Bag[V];
for (int v = 0; v < V; \/++)
adj[v] = new Bag <Integer>();
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(int v, int w) {
adj[v].add(w);
E++;
}
public Iterab1e<Integer> adj(int v) {
return adj[v];
}
public Digraph reverse() {
Digraph R = new Digraph(V);
for (int v = 0; v < V; v++)
for (int w : adj(v))
R.addEdge(w, v );
return R;
}
}
深度优先搜素
有向图的可达性
在有向图中,深度优先搜索标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的出度之和成正比。
public class DirectedDFS {
private boolean[] marked;
public DirectedDFS(Digraph G,int s) {
marked = new boolean[G.V()];
dfs(G,s);
}
public DirectedDFS(Digraph G,Iterable<Integer> sources) {
marked = new boolean[G.V()];
for (int s : sources)
if (!marked[s])
dfs(G,s);
}
private void dfs(Diagraph G,int v) {
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w])
dfs(G,w);
}
public boolean marked(int v) {
return marked[v];
}
}
这份深度优先搜索的实现使得用例能够判断从给定的一个或者一组顶点能到达哪些其他顶点。
标记- 清除的垃圾收集
多点可达性的一个重要的实际应用是在典型的内存管理系统中,包括许多Java的实现。在一幅有向图中,一个顶点表示一个对象,一条边则表示一个对象对另一个对象的引用。这个模型很好地表现了运行中的Java程序的内存使用状况。在程序执行的任何时候都有某些对象是可以被直接访问的,而不能通过这些对象访问到的所有对象都应该被冋收以便释放内存(请见图4.2.4) 。 标记 - 清除的垃圾回收策略会为每个对象保留一个位做垃圾收集之用。它会周期性地运行一个类似于 DirectedDFS的有向图可达性算法来标记所有可以被访问到的对象,然后清理所有对象,回收没有被标记的对象,以腾出内存供新的对象使用。
image-20210623111406921
有向图的寻路
环和有向无环图 调度问题
一种应用广泛的模型是给定一组任务并安排它们的执行顺序,限制条件是这些任务的执行方法和起始时间。限制条件还可能包括任务的时耗以及消耗的其他资源。最重要的一种限制条件叫做优先级限制,它指明了哪些任务必须在哪些任务之前完成。不同类型的限制条件会产生不同类型不同难度的调度问题。
拓扑排序
给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素(或者说明无法做到这一点)。
有向图中的环
一般来说,如果一个有优先级限制的问题中存在有向环,那么这个问题肯定是无解的。要检查这种错误,需要解决下面这个问题。
有向环检测
给定的有向图中包含有向环吗?如果有,按照路径的方向从某个顶点并返回自己来找到环上的所有顶点。
一幅有向图中含有的环的数量可能是图的大小的指数级别,因此我们只需要找出一个环即可,而不是所有环。在任务调度和其他许多实际问题中不允许出现有向环,因此不含有环的有向图就变得很特殊。
寻找有向环
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle;
private boolean[] onStack;
public Directed[] onStack;
public DirectedCycle(Digraph G) {
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++)
if (!marked[v])
dfs(G,v);
}
private void dfs(Digraph G,int v) {
onStack[v] = true;
marked[v] = true;
for (int w : G.adj(v))
if (this.hasCycle())
return;
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G,w);
} 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;
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
}
该类为标准的递归d f s ( )方法添加了一个布尔类型的数组onStackC ]来保存递归调用期间桟上的所有顶点。当它找到一条边v — w且 w在栈中时,它就找到了一个有向环。环上的所有顶点可以通过edgeTo[ ]中的链接得到。
在执行dfs(G ,v )时,查找的是 一条由 起点到v 的有向路径。要保存这条路径,DirectedCycle维护了一个由顶点索引的数组onStack(),以标记递归调用的栈上的所有顶点(在调用dfs(G,v) 时将onStack[v]设为 true ,在调用结束时将其设为false) 。DirectedCycle同时也使用了一个edgeTo[] 数组,在找到有向环时返回环中的所有顶点。
顶点的深度优先次序与拓扑排序
优先级限制下的调度问题等价于计算有向无环图中的所有顶点的拓扑排序。当且仅当一幅有向图是无环图时它才能进行拓扑排序。
有向图中基于深度优先搜索的顶点排序
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre;
private Queue<Integer> post;
private Stack<Integer> reversePost;
public DepthFirstOrder(Digraph G) {
pre = new Queue<Integer>();
post = new Queue<Integer>();
reversePost = new Stack<Integer>();
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
if (!marked[v])
dfs(G,v);
}
}
private void dfs(Digraph G,int v) {
pre.enqueue(v);
marked[v] = true;
for (int w : G.adj(v))
if (!marked[w])
dfs(G,w);
post.enqueue(v);
reversePost.push(v);
}
public Iterable<Integer> pre() {
return pre;
}
public Iterable<Integer> post() {
return post;
}
public Iterable<Integer> reversePost() {
return reversePost;
}
}
该类允许用例用各种顺序遍历深度优先搜索经过的所有顶点。这在高级的有向图处理算法中非常有用。
拓扑排序
public class Topological {
private Iterable<Integer> order; // 顶点的拓扑顺序
public Topological(Digraph G) {
DirectedCycle cyclefinder = new DirectedCycle(G);
if (!cyclefinder.hasCycle()) {
DepthFi rstOrder dfs = new DepthFi rstOrder(G);
order = dfs.reversePost();
}
}
public Iterable<Integer> order() {
return order;
}
public boolean isDAG() {
return order != null;
}
}
一幅有向无环图的拓扑排序即为所有顶点的逆后序排列。
使用深度优先搜索对有向无环图进 行拓扑排序所需的时间和V+E成正比。
解决任务调度类应用通常需要以下3 步:
- 指明任务和优先级条件;
- 不断检测并去除有向图中的所有环,以确保存在可行方案的;
- 使用拓扑排序解决调度问题;
有向图中的强连通性
在一幅有向图中,如果从顶点V 有一条有向路径到达W ,则顶点W 是从顶点V 可达的,但从W 到达V 的路径可能存在也可能不存在。
如果两个顶点V 和 W 是互相可达的,则称它们为强连通的。也就是说,既存在一条从V 到 W 的有向路径,也存在一条从W 到 V 的有向路径。如果一幅有向图中的任意两个顶点都是强连通的,则称这幅有向图也是强连通的。
强连通分量
和无向图中的连通性一样,有向图中的强连通性也是一种顶点之间平等关系,因为它有着以下性质。
- 自反性:任意顶点V和自己都是强连通的
- 对称性:如果V 和 W 是强连通的,那么W 和 V 也是强连通的。
- 传递性:如果V 和 W 是强连通的且W 和 X 也是强连通的,那 么 V 和 X 也是强连通的。
作为一种平等关系,强连通性将所有顶点分为了一些平等的部分,每个部分都是由相互均为强连通的顶点的最大子集组成的。我们将这些子集称为强连通分量。一个含有F 个顶点的有向图含有1 〜F 个强连通分量---------个强连通图只含有一个强连通分量,而一个有向无环图中则含有V个强连通分量。需要注意的是强连通分量的定义是基于顶点的,而非边。有些边连接的两个顶点都在同一个强连通分量中,在不同的强连通分量中。后者不会出现在任何有向环之中。与识别连通分量在无向图中的重要性一样,在有向图的处理中识别强连通分量也是非常重要的。
计算强连通分量的Kosaraju算法
public class kosarajuSCC {
private boolean[] marked;
private int[] id;
private int count;
public KosarajuSCC(Digraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
DepthFirstOrder order = new DepthFirstOrder(G.reverse());
for (int s : order.reversePost())
if (!marked[s]) {
dfs(G,s);
count++;
}
}
private void dfs(Digraph G,int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v))
if (!marked[s])
dfs(G,w);
}
public boolean stronglyConnected(int v,int w) {
return id[v] = id[w];
}
public int id(int v) {
return id[v];
}
public int count() {
return count;
}
}
Kosaraju算法的预处理所需的时间和空间与 V+E 成正比且支持常数时间的有向图强连通性的查询。
顶点对的可达性
public class TransitiveClosure {
private DirectedDFS[] all;
TransitiveClosure(Digraph G) {
all = new DirectedDFS[G.V()];
for (int v = 0; v < G.V(); v++)
all[v] = DirectedDFS(G,v);
}
boolean reachable(int v,int w) {
return all[v].marked(w);
}
}
无论对于稀疏还是稠密的图,它都是理想解决方案,但它不适用于在实际应用中可能遇到的大型有向图,因为构造函数所需的空间和r 2成正比 , 所需的时间和V(V+E)成正比 :共有 F 个 DirectedDFS对象 ,每个所需的空间都与 F 成 正比 (它们都含有大小为V的 marked[]数组并会检查E条边来计算标记)。
最小生成树
图的生成树是它的一裸含有其所有顶点的无环连通子图。一幅加权无向图的最小生成树(M S T)是它的一棵权值(树中所有边的权值之和)最小的生成树
约定:
- **只考虑连通图。**如果一幅图是非连通的,我们只能使用这个算法来计算它的所有连通分量的最小生成树,合并在一起称其为最小生成森林。
- 边的权重不一定表示距离。权重也可能表示时间、费用或是其他完全不同的变量,而且也完全不一定会和距离成正比。
- 边的权重可能是0 或者负数。
- 所有边的权重都各不相同。如果不同边的权重可以相同,最小生成树就不一定唯一了。
树的两个最重要的性质:
- 用一条边连接树中的任意两个顶点都会产生一个新的环。
- 从树中删去一条边将会得到两棵独立的树。
带权重的边的数据类型
public class Edge implements Comparable<Edge> {
private final int v; //顶点之一
private final int w; //另一个顶点
private final double weight; //边的权重
private Edge(int v,int w,double weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public double weight() {
return weight;
}
public int either() {
return v;
}
public int other(int vertex) {
if (vertex == v)
return w;
else if (vertex == w)
return v;
else
throw new RuntimeException("Inconsistent edge");
}
public int compareTo(Edge that) {
if (this.weightQ < that.weightO)
return -1 ;
else if (this.weight() > that.weight())
reutrn 1;
else
return 0;
}
public Stri ng toString() {
return String.format(“%d-%d %.2f”,v, w, weight);
}
}
加权无向图的数据类型
public class EdgeWeightedGraph {
private final int V; // 顶点总数
private int E; // 边的总数
private Bag<Edge>[] adj ; // 邻接表
public EdgeWeightedGraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<Edge>[]) new Bag[v];
for (int v = 0; v < V; v++)
adj[v] = new Bag<Edge>();
}
public EdgeWeightedGraph (In in)
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(Edge e) {
int v = e.either(), w = e.other(v);
adj[v].add(e) ;
adj[w].add(e) ;
public Iterable<Edge> adj(int v) {
return adj[v] ;
}
public Iterable<Edge> edges()
}
Prim 算法
第一种计算最小生成树的方法叫做Prim算法,它的每一步都会为一棵生长中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边(黑色表示)加入树中(即由树中的顶点所定义的切分中的一条横切边)。
Prim算法能够得到任意加权无向图的最小生成树。
实现Prim算法需要用到一些简单常见的数据结构:
- 顶点 。使用一个由顶点索引的布尔数组marked[]] ,如果顶点v 在树中,那么marked[v]的值为true 。
- 边。选择以下两种数据结构之一:一条队列mst来保存最小生成树中的边,或者一个由顶点索引的Edge对象的数组edgeTo[],其 中 edgeTo[v]为 将 v 连接到树中的Edge对象。
- 横切边:使用一条优先队列MinPQ来根据权重比较所有边。
Prim算法的延时实现计算一幅含有FV个顶点和E 条边的连通加权无向图的最小生成树所需的空间与E成正比,所需的肘间与 ElogE 成 正 比 (最坏倩况)。
最小生成树的Prim算法的延时实现
public class LazyPrimMST {
private boolean[] marked; // 最小生成树的顶点
private Queue<Edge> mst; // 最小生成树的边
private MinPQ<Edge> pq; // 横切边 (包括失效的边)
public LazyPrimMST(EdgeWeightedGraph G) {
pq = new MinPQ<Edge>();
marked = new boolean[G.V()];
mst = new Queue<Edge>();
visit(G,0);
while (!pq.isEmpty()) {
Edge e = pq.delMin(); //从pq中得到权值最小的编
int v = e.either(),w = e.other(v); //跳过失效的边
if (marked[v] && marked[w])
continue;
mst.enqueue(e); //将边添加到树中
if (!marked[v]) //将顶点(v或w)添加到树中
visit(G,v);
if (!marked[w])
visit(G,w);
}
}
private void visit(EdgeWeightedGraph G,int v) {
marked[v] = true;
for (Edge e : G.adj(v))
if (!marked[e.other(v)])
pq.insert(e);
}
public Iterable<Edge> edges() {
return mst;
}
public double weight()
}
Prim算法的这种实现使用了一条优先队列来保存所有的横切边、一个由顶点索引的队列来标记树的顶点以及一条队列来保存最小生成树的边。这种延时实现会在优先队列中保留失效的边。
Prim算法的即时实现
要改进LazyPrimMST,可以尝试从优先队列中删除失效的边,这样优先队列就只含有树顶点和非树顶点之间的横切边,但其实还可以删除更多的边。关键在于,我们感兴趣的只是连接树顶点和非树顶点中权重最小的边。当我们将顶点v 添加到 树中时,对于每个非树顶点w产生的变化只可能使得w到最小生成树的距离更近了。我们只会在优先队列中保存每个非树顶点w 的一条边;将它与树中的顶点连接起来的权重最小的那条边。将 w和树的顶点连接起来的其他权重较大的边迟早都会失效,所以没必要在优先队列中保存它们。
PrimMST类使用了索引优先队列实现的Prim算法。它将LazyPrimMST中的marked[]和 mst[] 替换为两个顶点索引的数组edgeTo[]和 distTo[ ] ,它们具有如下性质:
- 如果顶点v不在树中但至少含有一'条边和树相连,那么edgeTo[v]是将v 和树连接的最短边,distTo [v]为这条边的权重。
- 所有这类顶点V 都保存在一条索引优先队列中,索引V 关联的值是edgeTo[V]的边的权重。
PrimMST会从优先队列中取出一条边v 并检查它的邻接链表中的每条边v-w。如果 w 已经被标记过,那么这条边就已经失效了;如 果 w不在优先队列中或 者 v -w 的权重小于目前已知的最小值 edgeTo[w],代码会更新数组,将v -w 作为将v 和树连接的最佳选择。
最小生成树的Prim算 法 (即时版本)
public class PrimMST {
private Edge[] edgeTo;
private double[] distTo;
private boolean[] marked;
private IndexMinPQ<Double> pq;
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean [G.V()];
for (int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ <Double>(G.V());
distTo[0] = 0.0;
pq.insert(0, 0.0); // 用顶点0和权重0初始化pq
while (!pq.isEmpty())
visit(G,pq.delMin()); //将最近的顶点添加到树中
}
private void visit(EdgeWeightedGraph G, int v) { //将顶点v添加到树中,更新数据
marked[v] = true;
for (Edge e : G.adj(v)) {
int w = e.other(v) ;
if (marked[w])
continue; // v-w失效
if (e.weight() < distTo[w]) { //连接w和树的最佳边Edge变为e
edgeTo[w] = e;
distTo[w] = e.weight();
if (pq.contains(w))
pq.change(w,distTo[w]);
else
pq.insert(w, distTo[w]);
}
}
}
public Iterable<Edge> edges() {
Bag<Edge> mst = new Bag<Edge>();
for (int v = 1; v < edgeTo.length; v++)
mst.add(edgeTo[v]);
return mst;
}
public double weight()
}
Prim算法的即时实现计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的空间和V成正比,所需的时间和ElogE 成正比(最坏情况)。
Kruskal 算法
第二种最小生成树算法的主要思想是按照边的权重顺序(从小到大)处理它们, 将边加人最小生成树中(图中的黑色边),加入的边不会与已经加入的边构成环,直到树中含有K-1条边为止。这些黑色的边逐渐由一片森林合并为一棵树,也就是图的最小生成树。这种计算方法被称为Kruskal 算法。
Kruskal算法构造最小生成树的时候也是一条边一条边地构造,但不同的是它寻找的边会连接一片森林中的两棵树。我们从一片由V棵单顶点的树构成的森林开始并不断将两棵树合并 (用可以找到的最短边)直到只剩下一棵树,它就是最小生成树。
public class KruskalMST {
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G) {
mst = new Queue<Edge>();
MinPQ<Edge> pq = new MinPQ<Edge> (G.edges());
UF uf = new UF(G.V());
while (!pq.isEmpty() && mst.size() < G.V() - 1) {
Edge e = pq.delMin(); //从pq得到权重最小的边和它的顶点
int v = e.either(),w = e.other(v);
if (uf.connected(v,w)) //忽略失效的边
continue;
uf.union(v,w); //合并分量
mst.enqueue(e); //将边添加到最小生成树中
}
}
public Iterable<Edge> edges() {
return mst;
}
public double weight()
}
Kruskal算法的计算一幅含有F 个顶点和五条边的连通加权无向图的最小生成树所需的空间和£ 成正比,所需的时间和£log_E成正比(最坏情况)。
Kruskal算法一般还是比Prim算法要慢,因为在处理每条边时除了两种算法都要完成的优先队列操作之外,它还需要进行一次connect() 操作。
最短路径
找到从一个顶点到达另一个顶点的成本最小的路径。
在一幅加权有向图中,从顶点S到顶点t的最短路径是所有从S到 t 的路径中的权重最小者。
单点最短路径
给定一幅加权有向图和一个起点s, 答 “ 从 S到给定的目的顶点V是否存在一条有向路径?如果有,找出最短(总权重最小)的那条路径。”
给定一幅加权有向图和一个顶点S ,以S为起点的一裸最短路径树是图的一幅子图,它包含S和从S 可达的所有顶点。这棵有向树的根结点为S, 树的每条路径都是有向图中的一条最短路径。
加权有向边的数据类型
public class DirectedEdge {
private final int v; // 边的起点
private final int w; // 边的终点
private final double weight; // 边的权重
public DirectedEdge(int v, int w, double weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public double weight() {
return weight;
}
public int from() {
return v;
}
public int to() {
return w;
}
public String toString() {
return String.format("%d->%d %.2f" ,v, w, weight);
}
因为边的两个端点是有区别的。用例可以使用惯用代码 int v = e .to(), w = e.from (); 来访问DirectedEdge的两个端点。
加权有向图的数据类型
public class EdgeWeightedDigraph {
private final int V;
private int E;
private Bag<DirectedEdge>[] adj;
public EdgeWeightedDigraph(int V) {
this.V = V;
this.E = 0;
adj = (Bag<DirectedEdge>[]) new Bag[V];
for (int v = 0; v < V; v++)
adj[v] = new Bag<DirectedEdge>();
}
public EdgeWeightedDigraph(In in)
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(DirectedEdge e) {
adj[e.from()].add(e);
E++;
}
public Iterable<Edge> adj(int v) {
return adj[v];
}
public Iterable<DirectedEdge> edges() {
Bag<DirectedEdge> bag = new Bag<DirectedEdge>();
for (int v = 0; v < V; v++)
for (DirectedEdge e : adj[v])
bag.add(e);
return bag;
}
}
EdgeWei ghted D i graph 类的实现混合了 EdgeWei ghtedG raph 类和 D igraph 类。它维护了一个由顶点索引的Bag对象的数组,Bag对象的内容为DirectedEdge对象。与 Digraph类一样,每条边在邻接表中只会出现一次:如果一条边从v 指向w,那么它只会出现在v 的邻接链表中。这个类可以处理自环和平行边。
最短路径的数据结构
- 最短路径树中的边。和深度优先搜索、广度优先搜索和Prim算法一样,使用一 个由顶点索引的DirectedEdge对象的 父链接数组edgeTo [ ] ,其中edgeTo [v] 的值为树中连接v 和它的父结点的边(也是从 s 到 v 的最短路径上的最后一条边)。
- 到达起点的距离。我们需要一个由顶点索引的数组distTo[],其中distTo [v ]为 从 s 到 v 的已知最短路径的长度。
edgeTo[s]的值为null,distTo [s ]的值为0。同时还约定,从起点到不可达的顶点的距离均Double.POSITIVEJNFINITY。
边的松弛
private void relax(DirectedEdge e) {
int v = e.from(),w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
一开始我们只知道图的边和它们的权重,distTo [ ] 中只有起点所对应的元素的值为0,其余元素的值均被初始化为Double POSITIV E _ INFINITY。 随着算法的执行,它将起点到其他顶点的最短路径信息存入了 edgeTo [ ] 和 distTo [ ]。数组中。在遇到新的边时,通过更新这些信息就可以得到新的最短路径。特别是,我们在其中会用到边的松弛技术,定义如下:放松边V—W意味着检查从S到 W的最短路径是否是先从S到 V,然后再由V 到 W 。如果是,则根据这个情况更新数据结构的内容。
下图显示的是边的放松操作之后可能出现的两种情况。一种情况是边失效(左边的例子),不更新任何数据;另一种情况是v —w就是到达w的最短路径(右边的例子),这将会更新edgeTo[w]和distTo[w ]( 这可能会使另一些边失效,但也可能产生一些新的有效边)。松弛这个术语来自于用一根橡皮筋沿着连接两个顶点的路径紧紧展开的比喻.•放松一条边就类似于将橡皮筋转移到一条更短的路径上,从而缓解了橡皮筋的压力。如 果re la xO 改变了和边e 相关的顶点的distTo[e.to〇 ]和edgeTo[e.to()]的值,就称e 的放松是成功的。
顶点的松弛
实际上,实现会放松从一个给定顶点指出的所有边,如下页框注中(被重载的)relax( ) 的实现所示。注意,从任意distTo [v]为有限值的顶点v 指向任意d is tT [ ] 为无穷的顶点的边都是有效的。如果v 被放松,那么这些有效边都会被添加到edgeTo() 中。某条从起点指出的边将会是第一条被加人edgeT0[ ] 中的边。算法会谨慎选择顶点,使得每次顶点松弛操作都能得出到达某个顶点的更短的路径,最后逐渐找出到达每个顶点的最短路径。
private void relax(EdgeWeightedDigraph G,int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
最短路径API中的查洵方法
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Iterable<DirecedEdge> pathTo(int v) {
if (!hasPathTo(v))
return null;
Stack<DirectedEdge> path = new Stack<DirectedEdge>();
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()])
path.push(e);
return path;
}
最短路径的最优性条件:令 G 为一幅加权有向图 ,顶点 s 是 G 中的起点,distTo[]是一个由顶点索引的数组,保存的是G 中路径的长度。对于从s 可达的所有顶点V ,distTo [v]的值是从s 到 v 的某条路径的长度,对于从s不可达的所有顶点V ,该值为无穷大。当且仅当对于从v 到 w 的任意一条边e,这些值都满足distTo[w]<=distTo[v]+e.weight()时 (换句话说,不存在有效边时),它们是最短路径的长度。 通用最短路径算法:将 distTo[s] 初始化为0,其他distTo [] 元素初始化为无穷大,继续如下操作:放松G 中的任意边,直到不存在有效边为止。对于任意从S 可达的顶点W,在进行这些操作之后,distTo [w ]的值即为从s 到 w 的最短路径的长度(且 edgeTo[w]的值即为该路径上的最后一条边)。
Dijkstra 算法
Dijkstra算法能够解决边权重非负的加权有向图的单起点最短路径问题。
我们只需对Dijkstra算法的实现稍作适当的 修改就能够解决这个问题的其他版本,例如,加权无向图中的单点最短路径。对于给定的加权无向图,创建一幅由相同顶点构成的加权有向图,且对于无向图中的每条边,相应地创建两条(方向不同) 有向边。有向图中的路径和无向图中的路径存在着一一对应的关系,路径的权重也是相同的—— 最短路径的问题是等价的。
public class DijkstraSP {
private DirectedEdge[] edgeTo;
private double[] distTo;
private IndexMinPQ<Double> pq;
public DijkstraSP(EdgeWeightedDigraph G,int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new doble[G.V()];
pq = new IndexMinPQ<Double>(G.V());
for (int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
distTO[s] = 0.0;
pq.insert(s,0.0);
while (!pq.isEmpty())
relax(G,pq.delMin())
}
private void relax(EdgeWeightedDigraph G,int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
if (pq.contains(w))
pq.change(w,distTo[w]);
else
pq.insert(w,distTo[w]);
}
}
}
public double distTo(int v)
public boolean hasPathTo(int v)
public Iterable<Edge> pathTo(int v)
}
在一幅含有V个顶点和五条边的加权有向图中,使用Dijkstra算法计算根结点为给定起点的最短路径树所需的空间与V成正比,时间与ElogV 成正比(最坏情况下)。
Dijkstra算法的实现每次都会为最短路径树添加一条边,该边由一个树中的顶点指向一个非树顶点w且它是到s最近的顶点。
给定两点的最短路径。给定一幅加权有向图以及一个起点s 和~个终点t ,找到从S到 t 的最短路径。要解决这个问题,你可以使用Dijkstm算法并在从优先队列中取到t 之后终止搜索。
任意顶点对之间的最短路径
public class DijktraAllPairsSP {
private DijkstraSP[] all;
DijkstraAllPairsSP(EdgeWeightedDigraph G) {
all = new DijkstraSP[G.V()];
for (int v = 0; v < G.V(); v++)
all[v] = new DijkstraSP(G,v);
}
Iterable<Edge> path(int s,int t) {
return all[s].pathTo(t);
}
double dist(int s,int t) {
return all[s].distTo(t);
}
}
无环加权有向图中的最短路径算法
特点:
- 能够在线性时间内解决单点最短路径问题;
- 能够处理负权重的边;
- 能够解决相关的问题,例如找出最长的路径。
特别的是,只要将顶点的放松和拓扑排序结合起来,马上就能够得到一种解决无环加权有向图中的最短路径问题的算法。首先,将 distTo[s ] 初始化为 0,其他 distTo[] 元素初始化为无穷大,然后一个一个地按照拓扑顺序放松所有顶点。
按照拓扑顺序放松顶点,就能在和E+V成正比的时间内解决无环加权有向图的单点最短路径问题。
无环加权有向图的最短路径算法
该实现中不需要布尔数组marked[] :因为是按照拓扑顺序处理无环有向图中的顶点,所以不可能再次遇到已经被放松过的顶点。
public class AcyclicSP {
private DirectedEdge[] edgeTo;
private double[] distTo;
public AcyclicSP(EdgeWeightedDigraph G,int s) {
edgeTo = new DirectedEdge[G.V()];
distTo = new double[G.V()];
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
distTo[s] = 0.0;
Topological top = new Topological(G);
for (int v : top.order())
relax(G,v);
private void relax(EdgeWeightedDigraph G,int v)
public double distTo(int v)
public Iterable<DirectedEdge> pathTo(int v)
}
}
对于最短路径问题,基于拓扑排序的方法比Dijkstra算法快的倍数与Dijkstra算法中所有优先队列操作的总成本成正比。
最长路径
考虑在无环加权有向图中寻找最长路径的问题,边的权重可正可负。
无环加权有向图中的单点最长路径。给定一幅无环加权有向图(边的权重可能为负)和一个起点 S ,回答“ 是否存在一从S 到给定的顶点V 的路径?如果有,找出最长( 总权重最大)的那条路径。 ”
解决无环加权有向图中的最长路径问题所需的时间与E+V成正比。
根据这种转换实现Acycl i cLP类来寻找一幅无环加权有向图中的最长路径就十分简单了。实现该类的一个更简单的方法是修改AcyclicSP ,将 d istT o[ ] 的初始值变为 Double.NEGATIVE_INFINITY 并改变 relax( ) 方法中的不等式的方向。
Bellman-Ford算法
在任意含有r 个顶点的加权有向图中给定起点s,从 s 无法到达任何负权重环,以下算法能够解决其中的单点最短路径问题:将 distTo[s]初始化为0,其他distTo[] 元素初始化为无穷大。以任意顺序放松有向图的所有边,重复V轮。
Bellman-Ford算法所需的时间和EV成正比 ,空间和V成正比。
基于队列的Bellman-Ford算法
基于以下两种其他的数据结构:
- 一条用来保存即将被放松的顶点的队列q;
- 一个由顶点索引的boolean数组onQ[],用来指示顶点是否已经存在于队列中,以防止将顶点重复插入队列
基于队列的Bellman-Ford算法
public class BellmanFordSP {
private double[] distTo; //从起点到某个顶点的路径长度
private DirectedEdge[] edgeTo; //从起点到某个顶点的最后一条边
private boolean[] onQ; //该顶点是否存在于队列中
private Queue<Integer> queue;//正在被放松的顶点
private int cost;//relax()调用次数
private Iterable<DirectedEdge> cycle;//edgeTo[] 中的是否有负权重环
public BellmanFordSP(EdgeWeightedDigraph G,int s) {
distTo = new double[G.V()];
edgeTo = new DirectedEdge[G.V()];
onQ = new boolean[G.V()];
queue = new Queue<Integer>();
for (int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
distTo[s] = 0.0;
queue.enqueue(s);
onQ[s] = true;
while (!queue.isEmpty() && !this.hasNegativeCycle()) {
int v = queue.dequeue();
onQ[v] = false;
relax(G,v);
}
}
private void relax(EdgeWeightedDigraph G,v)
public double distTo(int v)
public boolean hasPathTo(int v)
public Iterable<Edge> pathTo(int v)
public void findNegativeCycle()
public boolean hasNegativeCycle()
public Iterable<Edge> negativeCycle()
}
Bellman-Ford算法的实现修改了 relax() 方法,将被成功放松的边指向的所有顶点加入到一条FIFO队列中(以避免出现重复顶点)并周期性地检查edgeTo[] 表示的子图中是否存在负权重环。
Bellman-Ford算法的负权重环检测方法
private void findNegativeCycle() {
int V = edgeTo.length;
EdgeWeightedDigraph spt;
spt = new EdgeWeightedDigraph(V);
for ( int v = 0; v < V; v++)
if (edgeTo[v] != null)
spt.addEdge(edgeTo[v]) ;
EdgeWeightedCycleFinder cf;
cf = new EdgeWeightedCycleFinder(spt)
cycle = cf.cycle();
}
public boolean hasNegati veCycle() {
return cycle != null;
}
public Iterable<Edge > negativeCycle() {
return cycle;
}