图类算法总结
Posted 爱吃牛肉的牛牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图类算法总结相关的知识,希望对你有一定的参考价值。
在开始各类图算法之前,先将图的结构进行分类。
图的表示,在实际实现过程中,有以下几种基本的方式可以来表示图。
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类型的顶点索引数组来标记顶点是否在树中),待选择横切边(使用优先队列来处理带选择的横切边,根据
以上是关于图类算法总结的主要内容,如果未能解决你的问题,请参考以下文章
KDD 2018 | 最佳论文:首个面向FacebookarXiv网络图类的对抗攻击研究