在開始各类图算法之前,先将图的结构进行分类。
图的表示,在实际实现过程中。有下面几种主要的方式能够来表示图。
1) 邻接矩阵:对于较小或者中等规模的图的构造较为适用。因为须要V*V大小的空间。
2) 边的数组:使用一个简单的自己定义edge类,还有两个变量,分别代表边的两个端点编号,实现简单,可是在求每一个点的邻接点的时候实现较为困难。
3) 邻接表数组:较为经常使用,使用一个以顶点为索引的数组。数组每一个元素都是和该顶点相邻的顶点列表。这样的数组占空间相对于邻接矩阵少了非常多。而且能非常好的找到某个给定点的全部邻接点。
依照图中边的方向将图分成有向图和无向图:
1)无向图:图中的边没有方向。
2)有向图:图中的边有方向。
对于有向图和无向图的详细实现表示能够使用前面介绍的三种方法。两种图在表示的时候大部分的实现代码都是一致的。
普通无向图的邻接数组表示方法的详细实现代码:
public class Graph {
private int V; //图中的顶点数目
private int E; //图中的边数目
private List<Integer>[] adj; //邻接数组
private int[][] a; //邻接矩阵
public CreatGraph(int V) {
this.E = 0;
this.V = V;
adj = new ArrayList[V];
a=new int[V][V];
for (int i = 0; i < V; i++)
adj[i] = new ArrayList<>();
}
//因为无向图中的边是没有方向的,所以加入边的时候须要在边的两个顶点相应的邻接列表中都加入顶点信息。
public void addEdge(int v1, int v2) {
a[v1][v2]=1;
a[v2][v1]=1;
adj[v1].add(v2);
adj[v2].add(v1);
E++;
}
public int V() {
return V;
}
public int E() {
return E;
}
//邻接数组返回给定点的全部邻接点
public List<Integer> adj(int i) {
return adj[i];
}
//邻接矩阵返回给定点的全部邻接点
public List<Integer> adj1(int i){
List<Integer> list=new ArrayList<>();
int[] adj1=new int[V];
adj1=a[i];
for(int v:adg1)
if(v!+0)list.add(v);
return list;
}
}
无权有向图的详细实现代码:
public class DirectedGraph {
private int V; //图中的顶点数目
private int E; //图中的边数目
private List<Integer>[] adj; //邻接数组
private int[][] a; //邻接矩阵
public DirectedGraph(int V) {
this.E = 0;
this.V = V;
adj = new ArrayList[V];
a=new int[V][V];
for (int i = 0; i < V; i++)
adj[i] = new ArrayList<>();
}
//因为无向图中的边是有方向的,所以加入边的时候须要仅仅须要在起始点的邻接列表中加入顶点信息。
public void addEdge(int v1, int v2) {
a[v1][v2]=1;
adj[v1].add(v2);
E++;
}
public int V() {
return V;
}
public int E() {
return E;
}
//邻接数组返回给定点的全部邻接点
public List<Integer> adj(int i) {
return adj[i];
}
//邻接矩阵返回给定点的全部邻接点
public List<Integer> adj1(int i){
List<Integer> list=new ArrayList<>();
int[] adj1=new int[V];
adj1=a[i];
for(int v:adg1)
if(v!+0)list.add(v);
return list;
}
}
一 图的遍历算法:
介绍两种比較基础的图遍历算法,广度优先搜索和深度优先搜索。
1)深度优先搜索:这是一种典型的递归算法用来搜索图(遍历全部的顶点);
思想:从图的某个顶点i開始,将顶点i标记为已訪问顶点,并将訪问顶点i的邻接列表中没有被标记的顶点j,将顶点j标记为已訪问,并在訪问顶点j的邻接列表中未被标记的顶点k依次深度遍历下去,直到某个点的全部邻接列表中的点都被标记为已訪问后。返回上层。反复以上过程直到图中的全部顶点都被标记为已訪问。
深度优先遍历和树的先序訪问非常相似,尽可能深的去訪问节点。深度优先遍历的大致过程(递归版本号):
a)在訪问一个节点的时候,将其设置为已訪问。
b)递归的訪问被标记顶点的邻接列表中没有被标记的全部顶点
(非递归版本号):
图的非递归遍历我们借助栈来实现。
a)假设栈为空,则退出程序,否则,訪问栈顶节点,但不弹出栈点节点。
b)假设栈顶节点的全部直接邻接点都已訪问过,则弹出栈顶节点。否则,将该栈顶节点的未訪问的当中一个邻接点压入栈。同一时候,标记该邻接点为已訪问,继续步骤a。
该算法訪问顶点的顺序是和图的表示有关的,而不仅仅是和图的结构或者是算法有关。
深度优先探索是个简单的递归算法(当然借助栈也能够实现非递归的版本号),可是却能有效的处理非常多和图有关的任务,比方:
a) 连通性:ex:给定的两个顶点是否联通 or 这个图有几个联通子图。
b) 单点路径:给定一幅图和一个固定的起点,寻找从s到达给定点的路径是否存在,若存在。找出这条路径。
寻找路径:
为了实现这个功能,须要在上面实现的深度优先搜索中中添加实例变量edgeTo[],它相当于绳索的功能。这个数组能够找到从每一个与起始点联通的顶点回到起始点的路径(详细实现的思路非常巧妙: 从边v-w第一次訪问w的时候,将edgeTo[w]的值跟新为v来记住这条道路,换句话说,v-w是从s到w的路径上最后一条已知的边,这样搜索结果就是一条以起始点为根结点的树,也就是edgeTo[]是个有父链接表示的树。
)
深度优先搜索的递归实现版本号和非递归版本号(递归是接住了递归中的隐藏栈来实现的。非递归,借助栈实现)
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DepthFirstSearch {
//用来记录顶点的标记状态 true表示为已訪问。false表示为未被訪问。
private boolean[] marked;
private int count;
//用来记录顶点索引所相应的父结点。假设遍历是从s到达的t那么edgeTo[s所对的所用]=t;
private int[] edgeTo;
//起始点
private int s;
private Deque<Integer> dq=new Deque<>();
public DepthFirstSearch(Graph G, int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
dq.push(s);
dfs(G, s);
}
//递归形式实现
public void dfs(Graph G, int s) {
marked[s] = true;
count++;
for (int temp : G.adj(s))
if (!marked[temp]) {
edgeTo[temp] = s;
dfs(G, temp);
}
}
//非递归形式实现
private void dfs(Graph G){
while(!dp.isEmpty()){
s=dp.peek();
needPop=true;
marked[s] = true;
for (int temp : G.adj(s))
if (!marked[temp]) {
dp.push(temp);
edgeTo[temp] = s;
needPop=false;
break;
}
}
if(needPop)
dp.pop();
}
public boolean hasPathTo(int v) {
return marked[v];
}
public List<Integer> pathTo(int v) {
if (hasPathTo(v))
return null;
List<Integer> list = new ArrayList<>();
v = edgeTo[v];
while (v != s) {
list.add(v);
v = edgeTo[v];
}
list.add(s);
Collections.reverse(list);
return list;
}
public int count() {
return count;
}
public static void main(String[] args){
int V = 0,E = 0;
Graph G=new Graph(V,E);
int s=0;
DepthFirstSearch dfs=new DepthFirstSearch(G,s);
for(int v=0;v<G.V();v++){
if(dfs.hasPathTo(v))
for(int x:dfs.pathTo(v))
if(x==s)
System.out.print(x);
else
System.out.print("-"+x);
System.out.println();
}
}
}
已经使用DFS攻克了一些问题,DFS事实上还能够解决非常多在无向图中的基础性问题。譬如:
1)计算图中的连通分支的数量;
public class ConnectComponent {
private boolean[] marked;
private int[] id; //标记结点所在的连通分支编号
private int count; //计算连通分支的个数
public ConnectComponent(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++;
}
}
}
public void dfs(Graph G, int s) {
marked[s] = true;
id[s] = count;
for (int temp : G.adj(s))
if (!marked[temp]) {
dfs(G, temp);
}
}
//推断点v和w是否在一个连通分支中
public boolean connected(int v, int w) {
if (id[v] == id[w])
return true;
else
return false;
}
public int id(int v) {
return id[v];
}
public int count() {
return count;
}
}
2)环检測:检測图中是否有环。
public class CycleDetect {
private boolean[] marked;
private boolean flag;
public CycleDetect(Graph G) {
marked = new boolean[G.V()];
for (int s = 0; s < G.V(); s++) {
if(!marked[s])
dfs(G, s, s);
}
}
public void dfs(Graph G, int s, int initial) {
marked[s] = true;
for (int temp : G.adj(s))
if (!marked[temp]) {
dfs(G, temp, initial);
} else {
if (temp == initial) {
flag = true;
return;
}
}
}
public boolean hasCycle(){
return flag;
}
}
3)二分图推断(双色问题):是否能用两种颜色给这个二分图进行着色,也就是说这个图是不是二分图。
public class IsBiagraph {
private boolean[] marked;
private boolean[] color;
private boolean flag=true;
public IsBiagraph(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);
}
}
public void dfs(Graph G, int s) {
marked[s] = true;
for (int temp : G.adj(s))
if (!marked[temp]) {
color[temp]=!color[s];
dfs(G, temp);
} else{
if(color[temp]==color[s])
flag=false;
}
}
public boolean isBiagraph (){
return flag;
}
}
2)广度优先搜索:
前面说过。深度优先搜索得到的路径不仅取决于图的结构,还取决于图的表示以及递归调用的性质,可是假设要求最短的路径(给定图G和起始点s寻找给定点v和s间是否存在路径,假设存在。找出最短的路径)。那么使用前面的DFS算法并不能解决该问题,所以出现了广度优先搜索BFS来实现这个目的,广度优先搜索也是其它算法的基础。
在程序中,搜索一幅图的时候会遇到有非常多条边都须要被遍历的情况,我们会选择当中一条并将其它边留到以后再继续搜索。在DFS中使用栈结构,使用LIFO的规则来描写叙述。从有待搜索的通道中选取最晚遇到的那个通道,然而在BFS算法中。我们希望依照与起点的距离来遍历全部的顶点,使用FIFO(队列)来进行搜索,也就是搜索最先遇到的那个通道。
BFS:使用一个队列来保存全部已经被标记过的可是其邻接点还未被检查过的顶点。现将顶点加入队列中,然后反复下面的操作。直至队列为空:
1)取队列中的下一个顶点v并标记它
2)将与v相邻的全部的未被标记的顶点加入队列中。
广度优先搜索相似于树的按层遍历
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
public class BreadFirstSearch {
private boolean[] marked;
private int[] edgeTo;
private int s;
public BreadFirstSearch(Graph G, int s) {
marked = new boolean[G.V()];
edgeTo = new int[G.V()];
this.s = s;
bfs(G, s);
}
public void bfs(Graph G, int s) {
Deque<Integer> deque = new ArrayDeque<>();
marked[s] = true;
deque.addFirst(s);
while (!deque.isEmpty()) {
s = deque.removeLast();
for (int temp : G.adj(s))
if (!marked[temp]) {
deque.push(temp);
marked[temp] = true;
edgeTo[temp] = s;
}
}
}
public boolean hasPathTo(int v) {
return marked[v];
}
public List<Integer> pathTo(int v) {
if (hasPathTo(v))
return null;
List<Integer> list = new ArrayList<>();
v = edgeTo[v];
while (v != s) {
list.add(v);
v = edgeTo[v];
}
list.add(s);
Collections.reverse(list);
return list;
}
}
DFS和BFS是两种基础的通用的图搜索算法。在搜索中我们都运用下面方法:
将起始点加入入某个数据结构中,然后反复下面步骤直至数据结构中的全部数据都被清空。
1) 取数据结构的下个数据v而且标记它。
2) 将v全部的相邻的未被标记的顶点加入到数据结构中。
这两种算法每次都扩展一个节点的全部子节点。而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索扩展的则是本次扩展的节点的兄弟节点
使用范围:DFS能够迅速的找到一个解,然后利用这个解进行剪枝,而BFS可找到最优解。
将上述的图像数据类型改成有向图就能够实现有向图中的遍历问题。
在有向图中单点的联通问题(即给定的两点是否联通)变成了可达问题(即对于给定的两个是否存在一条有向路)在有向图中,使用全然同样的代码。在就能够在有向图中就能够解决可达问题。
public class DirecedtDFS {
public boolean[] marked;
public DirecedtDFS(DiGraph G, int s) {
marked = new boolean[G.V()];
dfs(G, s);
}
public DirecedtDFS(DiGraph G, Set<Integer> set) {
marked = new boolean[G.V()];
for (int s : set)
if (!marked[s])
dfs(G, s);
}
private void dfs(DiGraph G, int s) {
marked[s] = true;
for (int temp : G.adj[s])
if (!marked[temp])
dfs(G, temp);
}
public boolean marked(int i) {
return marked[i];
}
}
然而在有向图中的环检測就和无向图中不大一样,须要保存某条道路上的全部信息。来推断是否形成有向环。
在运行dfs(G,s)的时候查找的总是一条由起点到达s的有向路径。要保存这条路径,程序实现的时候维护了一个由顶点索引的boolean类型数组onStack用来标记递归调用栈上的全部顶点(在dfs调用的时候,将onStack[s]设置成true,结束的时候(就是这条有向路结束了)再将其设置为false)当它在找到一条边v→w时候,w已经在栈中(这里的是表示这个点已经在这条路上出现过了,也就是在onStack中已经标记过了)它就找到可一个有向环,环上的顶点和无向图一样能够通过edgeTo数组获得。这里在检測遇到的点w是否在栈中,不像无向图那样检查是否在marked中标记过,是因为,在有向图中的环必须是一条首尾结点一样的有向路。环上的全部有向边的方向必须一致。
public class DirectedCycle {
private boolean marked[];
private int edgeTo[];
private boolean onStack[];
List<Integer> list;
private boolean flag;
public DirectedCycle(DiGraph G) {
//标记整个图上的遍历过的结点
marked = new boolean[G.V()];
//标记某条有向路上遇到的全部节点
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
for (int v = 0; v < G.V(); v++) {
dfs(G, v);
}
}
private void dfs(DiGraph G, int s) {
onStack[s] = true;
marked[s] = true;
for (int w : G.adj[s])
if (hasCycle())
return;
else if (!marked[w]) {
dfs(G, w);
edgeTo[w] = s;
} else if (onStack[w]) {
list = new ArrayList<>();
for (int v = s; v != w; v = edgeTo[v])
list.add(v);
list.add(w);
list.add(s);
}
onStack[s]=false;
}
public boolean hasCycle() {
return list.isEmpty();
}
public List<Integer> cycle() {
return list;
}
}
3)拓扑排序:
拓扑排序:给定一幅有向图,给全部的结点排序,排序后使得有向边均从排在前面的结点元素指向排在后面的结点元素(或者说明这个有向图不能进行拓扑排序)。
在对一个有向图进行拓扑排序的时候,必须保证它是无环的有向图,因为有环的图不能做到拓扑有序。
事实上对于标准的深度优先搜索算法加入一行代码就能实现这个问题。在使用深度优先搜索的时候,正好仅仅会訪问每一个结点一次,假设将dfs()訪问的结点存储在一个数据结构中,然后遍历这个结构就能够訪问图中全部结点。遍历的顺序取决于这个数据结构的性质以及是在递归前还是递归后保存
1) 前序:在递归前将顶点放入队列中
2) 后序:在递归调用之后将顶点放入队列中
3) 逆后序:在递归调用之后将顶点压入栈中。
一幅有序无环图的拓扑排序就是全部顶点的逆后序排列。
public class DFSOrder {
private boolean[] marked;
private List<Integer> pre; //前序
private List<Integer> post; //后序
private Deque<Integer> reseverpost; //逆后序
public DFSOrder(DiGraph G){
marked=new boolean[G.V()];
pre=new ArrayList<>(G.V());
post=new ArrayList<>(G.V());
reseverpost=new ArrayDeque<>(G.V());
for(int v=0;v<G.V();v++)
dfs(G,v);
}
public DFSOrder(EdgeWeightDigraph G){
marked=new boolean[G.V()];
pre=new ArrayList<>(G.V());
post=new ArrayList<>(G.V());
reseverpost=new ArrayDeque<>(G.V());
for(int v=0;v<G.V();v++)
dfs(G,v);
}
private void dfs(DiGraph G, int v) {
pre.add(v);
marked[v]=true;
for(int w:G.adj[v])
if(!marked[w])
dfs(G,w);
post.add(v);
reseverpost.addLast(v);
}
public List<Integer> pre(){
return pre;
}
public List<Integer> post(){
return post;
}
public Deque<Integer> reseverpost(){
return reseverpost;
}
}
拓扑排序实现代码:
public class Topological {
private Deque<Integer> oder=new ArrayDeque<>();
public Topological(DiGraph G){
DirectedCycle cycle=new DirectedCycle(G);
if(!cycle.hasCycle())
{
DFSOrder dfsorde=new DFSOrder(G);
oder=dfsorde.reseverpost();
}
}
public boolean isDAG(){
return oder!=null;
}
public Deque<Integer> order(){
return oder;
}
}
使用深度优先搜索进行拓扑排序,事实上是使用了两遍深度优先搜索,一遍是查看有向图中是否存在有向环第二遍就是产生顶点的逆后序。因为两次都訪问了全部的顶点和边,所以这个算法须要的时间是和V+E成正比的。
深度优先搜索须要先预处理,而它使用的时间和空间与V+E成正比。且在常数时间内处理关于图的连通性。Union-find也能够进行图搜索,可是实际上。union-find事实上更加快。因为不须要构造并表示一幅图,而且union-find算法是一种动态算法,可是深度优先算法须要对图进行预处理。
我们在完毕仅仅须要推断连通性或是须要完毕大量的连通性查询和插入操作混合等相似的任务时。更加倾向于使用union-find算法,而深度优先算法则跟适合实现图的抽象数据类型。
补充说明:Union-find算法(解决动态连通性问题):
问题描写叙述:给出一列整数对。每一个整数对(p,q)能够被觉得是相连的,我们假定相连是一种等价关系(这样的相连的属性满足自反性,对称性以及传递性,从图的角度来看,这个整数对能够看作无向边的两个端点)。这样的等价关系将输入的数据对划分为多个等价类(从图的角度来看,是将他们划分为多个连通分量)我们须要设计一种数据结构。来保存已经输入的数据对象(即已知的数据对)的全部信息,并用这些信息来推断,新的数据对是否是相连的(也就是新加入进的边的两个端点是否在同一个连通分支内),将这个问题通俗的称为动态连通性问题。这和我们前面所讲的使用DFS来推断图的连通性能够达到一样的目的,可是这个是在动态生成的过程中推断图的连通性。而DFS须要预处理整个图,不能在动态的过程中来推断。
union-find 算法中API:
1)void union(int v,int w) 在p和q之间加入一条连接
2)int find(int v) p所在的分量的标识
3)boolean connected(int v,int w)假设p和q在同一个分量中。那么返回true
4)int count() 返回连通分量的数目;
在详细实现过程中,选择一个以顶点为索引的数组,来记录相应顶点所在的联通分支的标识(将使用连通分量中某个顶点作为该分支的标识),在union中,假设p和q不在一个连通分量中,那么须要更具不同的算法改变其id数组中的值,假设在同一分量中,忽略不计就可以。
public class UF {
private int[] id;
private int count;
public UF(int N) {
id = new int[N];
count = N;
for (int i = 0; i < N; i++)
id[i] = i;
}
public int count() {
return count;
}
public boolean connect(int v, int w) {
return quick_find(w) == quick_find(v);
}
}
这里将介绍三种find-union的实现方法(每种方法仅仅是find和union实现不太同样),可是他们都是依据以顶点为索引的id数组来确定在不在同一连通分量中:
1)quick-find方法:
在这样的方法中,同一个连通分量中的id值都是同样的。
在find实现的时候,直接返回id中的值就可以。
在union实现的时候,先推断给定的数据对是否在同一个连通分量中。假设是就直接忽略。否则,合并p和q所在的连通分量(就是将p所在的连通分量的标识id全部替换成q所在的连通分量标识id(反之亦可)),为此,我们须要遍历整个数组;
public int quick_find(int w) {
return id[w];
}
public void union(int v,int w) {
//假设点v,w在同一个连通分量中,那么不须要採取不论什么措施
if(connect(v,w)) return;
//否则将v所在的连通分量的标记号全部改为w所在的连通分量的标记号码
int label_v=id[v];
int label_w=id[w];
for(int i=0;i<id.length;i++){
if(id[i]==label_v)
count--;
}
}
2)quick-union方法:
由上述可知。我们对于每对输入都须要遍历整个数组。因此quick-find无法处理大型数据,假设使用quick-find方法而且最后仅仅得到一个连通分支,那么至少须要调用N-1次union方法,每次union方法都须要至少N+3次操作,那么整个算法的性能就是平法级别的。而quick-union方法提高了union的效率。同样也是基于同样的数据结构-由顶点索引的id数组,可是该方法中的id数组的意义有所不同,这里的id[]中的元素都是同一个分量中其它顶点的编号。也有可能是自己,我们将这样的联系称为链接。在find方法中。我们从给定的顶点開始。由它的链接得到另外一个顶点,再由这个顶点的链接得到新的顶点,如此继续尾随顶点的连接到达新的顶点,直到链接指向自己(这样的链接所相应的顶点被称为跟结点),和quick-find不同,quick-union仅仅有两个顶点開始这一过程而且到达同样的跟结点,才干说它们是同一个分支中的。所以在详细的union实现中。我们须要依照上述过程找寻给定的两个顶点的跟结点。假设跟结点同样(说明这两个顶点在同一个分量中)。否则,将当中一个根结点中的链接连接到另外一个跟结点上(也就是为当中一个跟结点的链接又一次定义为另外一个跟结点)这样的实现find-union方式被称为quick-union方法。
public int find(int v){
while(v!=id[v]) v=id[v];
return v;
}
public void quick_union(int v,int w){
int lable_v=find(v);
int lable_w=find(w);
if(lable_v==lable_w) return;
else
id[lable_v]=lable_w;
count--;
}
3)加权的quick-union:
可是在前面讲的quick-union方法中,可能会出现依据不同的输入可能出现最坏的情况。就是树偏向一边,也就是实现仍然须要平方级别的的消耗,在上述实现思想中,再做个轻微的改进就能极大的提高算法的效率,就是每次合并两个分量的时候,不是随意的合并。而是将较小的分量的跟结点的链接改为较大跟结点。这样就能够在以某种程度上达到平衡性,这里须要维护一个size数组。使得跟结点相应索引的元素的值为这个分量的大小(即分量中元素的个数)每次在进行合并须要改变id值得时候。比較两个分量的跟结点的所在的分量的大小,总是将小的分量的链接改为大的分量的跟结点,而且跟新新的合成分量的跟结点的大小。
public void quick_weight_union(int v, int w) {
int lable_v = find(v);
int lable_w = find(w);
if (lable_v == lable_w)
return;
if (size[v] > size[w])
id[lable_w] = lable_v;
else
id[lable_v] = lable_w;
count--;
}
4)使用路径压缩的加权quick-union方法:
在这样的方法中,使得每一个节点都直接连接在其跟结点上,可是我们又不想像quick-find那样遍历整个数组,因此,在检查顶点的同一时候将他们直接连接在跟结点上。要想实现路径压缩,仅仅须要为find方法加上一个循环,将在路上遇到的全部节点都连接到跟结点上。
二.最小生成树:
在下面的讨论中做出例如以下约定:
1) 仅仅考虑连通图;
2) 边的权重能够是不论什么数;
3) 全部边的权重都各不同样(假设存在权值同样的边。最小生成树不唯一);
切分定理(解决最小生成树的全部算法的基础):
将加权图中的全部顶点分成两个集合(两个非空且不重合的集合)。检查横跨两个集合的全部边(这样的边被称为横切边:一条连接两个不属于同一集合顶点的边),并识别那条边是否应该属于图的最小生成树。
切分定理:在一幅加权图中,给定随意的划分,它的横切边中权值最小者必定属于最小生成树。(证明:使用反证法,假设e为权值最小的横切边,T为图的最小生成树。假设T中不包括e,那么必定包括一条横切边f,将e边加入最小生成树中。形成了一个环,包括e, f边,那么将f从环中删去。生成一个新的生成树T’显然。新的生成树比原来的最小生成树更小,这与已知矛盾)
从切分定理可知。(在假设前提下,全部边的权值都不同,那么最小生成树是唯一的)对于每一种切分,权值最小的横切边必定属于最小生成树。
可是,权值最小的横切边并非全部横切边中唯一属于图的最小生成树的边,实际中。一次切分产生的横切边可能有多条属于最小生成树。
全部求解最小生成树的算法都是使用的贪心策略(依据切分定理,每次选择一种划分。使得全部的横切边都没有被标记。那么选择权值最小的横切边,直至选择了V-1条边为止。仅仅只是对于不同的算法所使用的切分方法和推断权值最小的横切边的方式有所不同。)
1)Prim算法:
思想:最開始树中仅仅有一个顶点,每次为生长中的树加入一个边,直至加入了V-1条边为止,每次加入的边都是树中的顶点和非树中的顶点所划分的两个集合的横切边中最小的边。
在详细实现中使用一些简单的数据结构来实现树中点的标记(使用boolean类型的顶点索引数组来标记顶点是否在树中),待选择横切边(使用优先队列来处理带选择的横切边,依据横切边的权重),生成树中的边(顶点索引的Edge对象数组来保存最小生成树中的边)。
我们使用一个方法来为树加入顶点,将这个顶点标记为已訪问,而且将与它关联的全部未失效的(连接新加入的节点和其它已经在树中的顶点的全部边失效)边加入优先队列中。然后取出优先队列中权值最小的边。而且检查这个边是否有效。假设有效,将这个边的不在树中的点标记为已訪问,并将这个边加入最小生成树的边集合中,然后从优先队列中删除这个边。调用新的顶点来更新横切边集合。
prim算法的延时实现代码:
class Edge {
int v;
int w;
double weight;
public int other(int v) {
if (v == this.v)
return w;
else
return v;
}
public int either() {
return v;
}
}
class EdgeWeightGraph {
int V;
public List<Edge>[] adj;
public EdgeWeightGraph(int v) {
for (int i = 0; i < v; i++) {
adj[i] = new ArrayList<>();
}
}
public int V() {
return V;
}
}
public class LazyPrimMST {
private boolean[] marked; //标记最小生成树的顶点
private List<Edge> mst; //标记最小生成树的边
private MinPQ<Edge> pq; //横切边(包括了失效的边)
public LazyPrimMST(EdgeWeightGraph G) {
marked = new boolean[G.V()];
mst = new ArrayList<>(G.V());
pq = new MinPQ<Edge>();
visit(G, 0);
while (。pq.isEmpty()) {
Edge temp = pq.delMin();
int v = temp.either(), w = temp.other(v);
if (marked[v] && marked[w])
continue;
mst.add(temp);
if (!marked[v])
visit(G, v);
else if (!marked[w])
visit(G, w);
}
}
}
private void visit(EdgeWeightGraph G, int i) {
marked[i] = true;
for (Edge temp : G.adj[i])
if (!marked[temp.other(i)]) {
pq.insert(temp);
}
}
}
优化思想:改进Prim的延时实现,能够尝试在优先队列中删除失效的边。这样优先队列就能够仅仅包括真正的横切边。关键的优化思想在于:须要在意的仅仅是连接树顶点和非树顶点中权重最小的边。也就是说,当我们将V加入到生成树顶点集合中去后,对于每一个非树顶点w不用保存w到树的每条边,仅仅须要保存权重最小的那个边。也就是说,在优先队列中,仅仅须要保存每一个非树顶点的一条边:将它和树中顶点连接起来权重最小的那个边。其它权重较大的边迟早会失效。
即使实现的Prim算法:使用两个顶点索引数组edgeTo[] 和distTo[]来替换延时实现中的marked和mst数据结构。
EdgeTo假设顶点v不在树中。可是至少含有一条边和树相连,那么edgeTo[v] 就是将v和树连接的最小权重的边。而这个distTo则是这条边的权重。
将这类顶点v都保存在一条索引优先的队列中,索引v关联的值是edgeTo的边的权值。
2)Kruskal算法:
思想:依照边的权重顺序来处理它们,依次将当前权值最小的边加入生成树的边中(加入的边不能和已经加入的边构成环)直至加入了V-1条边。在整个过程中,加入的边组成的森林随着新加入的边渐渐合成一棵树。
使用一个优先队列来将全部的边(也就是为全部边依照权值排序),然后再使用union-find数据结构识别新加入的边是否会和已有的树中的边形成环(因为这是在动态处理中识别环是否存在,这里使用union-find(因为深度优先算法须要预处理整个图,在这里不是非常适用)。最后用一个队列或者别的数据结构来保存最小生成树的全部边。
三.最短路径算法:
在详细实现中使用到的类型结构;
1) 带有权重的有向边:
public class DirectedEdge {
private int v;
private int w;
private double weight;
public DirectedEdge(int v, int w, double weight) {
this.v = v;
this.w = w;
this.weight = weight;
}
public int form() {
return v;
}
public int to() {
return w;
}
double weight() {
return weight;
}
}
2) 加权有向图:
public class EdgeWeightDigraph {
private int V;
private int E;
public List<DirectedEdge>[] adj;
public EdgeWeightDigraph(int V) {
this.V = V;
this.E = 0;
adj = new ArrayList[V];
for (int v = 0; v < V; v++)
adj[v] = new ArrayList<>();
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(DirectedEdge edge) {
adj[edge.to()].add(edge);
E++;
}
public List<DirectedEdge> adj(int v) {
return adj[v];
}
public List<DirectedEdge> edges(){
List<DirectedEdge> list=new ArrayList<>();
for(int v=0;v<V;v++)
for(DirectedEdge temp:adj[v])
list.add(temp);
return list;
}
}
在详细实践中用到的数据结构:
1) 最短路径树的边:edgeTo[v]:由顶点索引的DirectedEdge的数组,当中edgeTo[v]的值是树中连接v和它的父节点的边
2) 到达起点的距离:distTo[]数组当中distTo[v]代表从点v到达起点的已知的最短路径。
边的松弛:
最短路径的API都基于一个松弛操作,放松边v→w意味着检查从s(起点)到w的最短轮径是否先到达v。再从v→w,假设是,则依据这个情况来跟新数据结构的内容。
也就是,那么distTo[v]与边v→w边的权重的和就可能成为新的s到达w的最短路径。而且跟新distTo[v],也就是说distTo[v]+e(vw).weight < distTo[w],否则就说这条边失效了,忽略它。
顶点的松弛:
将一个顶点所连接的全部边进行松弛操作(这里的边松弛和上述过程一样)。
1)Dijkstra算法:
该算法採用了的思想和在求最小生成树时候使用的prim算法相似的思想。首先将distTo[s]设置为0。distTo的其它元素设置为正无穷大,然后将distTo中最小的非树顶点放松并加入到树中,如此这般,直至全部顶点都在树中。或者全部非树顶点的distTo都无穷大。
Dijkstra能解决边权值非负的加权的有向图的最短路径问题:
public class Dijkstra {
private DirectedEdge[] edgeTo;
public double distTo[];
private IndexMinPQ<Double> pq;
private boolean flag = false;
public Dijkstra(EdgeWeightDigraph 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;
pq = new IndexMinPQ<>();
pq.insert(s, 0.0);
while (!pq.isEmpty()) {
int v = pq.delMin();
relax(G, v);
/*
* 求解给定两点的最短路径
* if (v == t) {
flag = true;
break;
}*/
}
}
private void relax(EdgeWeightDigraph G, int v) {
for (DirectedEdge edge : G.adj[v]) {
int w = edge.to();
if (distTo[w] > distTo[v] + edge.weight()) {
distTo[w] = distTo[v] + edge.weight();
edgeTo[w]=edge;
if (pq.contains(w))
pq.change(w, distTo[w]);
else
pq.insert(w, distTo[w]);
}
}
}
public Deque<DirectedEdge> pathTo(int t) {
if (!hasPath(t))
return null;
Deque<DirectedEdge> list=new ArrayDeque<>();
for(DirectedEdge edge=edgeTo[t];edge!=null;edge=edgeTo[edge.from()])
list.push(edge);
return list;
}
private boolean hasPath(int t) {
return distTo[t]<Double.POSITIVE_INFINITY;
}
}
2)无环加权有向图中的最短路径算法:
对照Dijstra算法来说。对于无环加权有向图的最短路径有个好的改进算法,该算法:
a) 能够在线性时间内解决单点最短路径;
b) 能够处理权值为负的边
c) 能够解决相关问题(譬如,最长路径求解)
这些都是在无环有向图的拓扑排序算法的简单扩展。特别的是,将顶点放松和拓扑排序结合起来就能得到这样的解决无环加权有向图的最短路径的简单算法。
首先将distTo[s]初始化为0。其它distTo数组元素初始化正无穷大,然后依照拓扑排序来一个个放松顶点。(这样的方法能在于V+E成正比的时间内解决无环加权有向图的最短路径问题)
对于无环问题来说,这样的拓扑排序和放松相结合的算法,大大简化了问题的推断,而且这样的算法和边的权值的正负无关。可是仅仅能适用于无环结构(有环结构不能进行拓扑排序)。
import java.util.ArrayDeque;
import java.util.Deque;
public class Dijkstra {
private DirectedEdge[] edgeTo;
private double distTo[];
public Dijkstra(EdgeWeightDigraph 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[0] = 0;
Topological top = new Topological(G);
Deque<Integer> dp = top.order();
for (int v = 0; v < G.V(); v++) {
relax(G, dp.poll());
}
}
private void relax(EdgeWeightDigraph G, Integer w) {
for (DirectedEdge edge : G.adj[w])
if (distTo[edge.to()] > distTo[w] + edge.weight()) {
distTo[edge.to()] = distTo[w] + edge.weight();
edgeTo[edge.to()] = edge;
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPath(int v) {
return distTo[v] < Double.POSITIVE_INFINITY;
}
public Deque<DirectedEdge> pathTo(int v) {
if (!hasPath(v))
return null;
else {
Deque<DirectedEdge> list = new ArrayDeque<>();
for (DirectedEdge s = edgeTo[v]; s != null; s = edgeTo[s.form()])
list.push(s);
return list;
}
}
}
使用上面的算法还能高速的解决无环加权有向图的单点最长路径问题。解决无环加权有向图的最长路径问题须要的时间和E+V成正比。(证明,复制原来的无环加权有向图的一个副本,并把副本中的全部边的权值取相反数,这样副本中的最短路径就是原图中的最长路径。事实上在、实现的一个更简单的方法就是。将distTo的初始值变为负无穷,然后改变松弛的条件方向)
对于这个算法还能够运用在优先级限制下的并行任务调度(这就是个无环加权有向图):
问题描写叙述:给定一组须要完毕的任务及其须要完毕的时间。一级乙组关于任务完毕的优先级限制顺序。在满足优先级限制的条件下,应该怎样在若干个处理器上安排任务并在最短的时间内完毕全部的任务。
存在一个线性时间内的算法“关键路径的方法被证明和无环加权有向图的最长路径问题是等价的。
解决并行任务调度的关键路径方法的过程例如以下:创建一副无环加权的有向图,当中包括一个起点s和一个终点t且每一个任务都相应着两个顶点(一个起始顶点,一个任务结束顶点,两个顶点间边的长度为任务完毕所须要的时间)对于每条优先级限制v→w加入一条从v到w的权重为0的边,还须要为每一个任务加入一个从起始点s指向该任务的起始点的权重为0的边,以及一条从该任务的结束点到达终点t的权重为0的边。这样。每一个任务的估计開始时间就是从起始点s到达该任务起始点的最长距离。
3)一般加权有向图中的最短路径算法(BellmanFord算法):
思想:在随意含有V个顶点的加权有向图中给定起始点s,从s无法到达不论什么负权重环,下面算法就能解决当中的单点最短路径问题,将distTo[s]初始化为0,其它元素的distTo初始化为正无穷大。然后以随意顺序放松有向图中的全部边,反复V轮。
可是依据经验,我们会非常easy的得出,随意一轮中很多边的放松都不会成功。仅仅有上一轮中distTo值发生变化的顶点连接的边才干够改变其它distTo中的元素值,我们能够使用FIFO队列来纪录发生变化的顶点。
实现的时候使用下面两种数据结构:
1)一条用来保存即将被放松的顶点的队列;
2)一个由顶点索引的boolean数组。用来指示顶点是否在队列中,防止反复的往队列中加入顶点。
为了完整的实现V轮候算法能够终止。实现这个目的一种方法就是显示的纪录轮数。
负权重值的检測