Coursera - Algorithm (Princeton) - 课程笔记 - Week 8

Week 8
最小生成树 Minimum Spanning Tree
给定前提:一张连通无向图​,其边都具有正值权重

生成树:​的生成树是该图的一个子集,其既是一个树(连通且无环),同时还是一个生成(包含所有的顶点)

最小生成树:拥有最小权重的生成树

贪心算法 Greedy Algoithm
问题简化假设:

边权重都是不同的

图是连通

假设结果:MST存在且独一无二

如果权重存在相等的,MST仍然存在(但可能有多个解)

如果图不是连通的,这个方法仍然能找的各个通量的MST(一个通量一个连通图)

剪切性:

剪切定义:图上的剪切是将其顶点划分为两个非空集合

交边定义:交边分别连接着两个集合的顶点

剪切性质:给定任何剪切,其最小权重的交边在其MST上

贪心MST算法:

从标灰的所有边开始(续上一个点,分割的两个集合一个标灰,一个标白)

寻找没有标黑(纳入MST的边会标黑)交边的剪切,将其最小权重边标黑

重复上述过程,直到有​标黑边

性质:贪心算法可以计算MST,但是不实际

贪心算法的有效性:试图使用局部最小求得全局最小

带权边图API Edge-Weighted Graph API
带权边的API如下:

1
public class Edge implements Comparable
2
{
3
Edge(int v, int w, double weight); // create a weighted edge v-w
4
int either(); // either endpoint
5
int other(int v); // the endpoint that’s not v
6
int compareTo(Edge that); // compare this edge to that edge
7
double weight(); // the weight
8
String toString(); // string representation
9
}
使用“得到一个在查询另一个”的风格解决函数命名问题和功能歧义

Java代码实现如下:

1
public class Edge implements Comparable
2
{
3
private final int v, w;
4
private final double weight;
5

6
public Edge(int v, int w, double weight)
7
{
8
this.v = v;
9
this.w = w;
10
this.weight = weight;
11
}
12

13
public int either()
14
{ return v; }
15

16
public int other(int vertex)
17
{
18
if (vertex == v) return w;
19
else return v;
20
}
21

22
public int compareTo(Edge that)
23
{
24
if (this.weight < that.weight) return -1;
25
else if (this.weight > that.weight) return +1;
26
else return 0;
27
}
28
}
带权边图的API(在图中进一步的详细实现)

1
public class EdgeWeightedGraph
2
{
3
EdgeWeightedGraph(int V); // create an empty graph with V vertices
4
EdgeWeightedGraph(In in); // create a graph from input stream
5
void addEdge(Edge e); // add weighted edge e to this graph
6
Iterable adj(int v); // edges incident to v
7
Iterable edges(); // all edges in this graph
8
int V(); // number of vertices
9
int E(); // number of edges
10
String toString(); // string representation
11
}
规定:允许自循环和平行边(因为带了权重所以没有太大影响了)

图表示:邻接列表

部分区别于传统图的方法的代码实现:

1
public EdgeWeightedGraph(int V)
2
{
3
this.V = V;
4
adj = (Bag[]) new Bag[V];
5
for (int v = 0; v < V; v++)
6
adj[v] = new Bag();
7
}
8

9
public void addEdge(Edge e)
10
{
11
int v = e.either(), w = e.other(v);
12
adj[v].add(e);
13
adj[w].add(e);
14
}
15

16
public Iterable adj(int v)
17
{ return adj[v]; }
MST API如下:

1
public class MST
2
{
3
MST(EdgeWeightedGraph G); // constructor
4
Iterable edges(); // edges in MST
5
double weight(); // weight of MST
6
}
本课程的图处理算法都具有相同的风格:构造器完成所有的计算,所有其他的方法都是值的查询

克鲁斯卡尔算法 Kruskal‘s Algorithm
对边的权重进行排序:考虑边以权重升序排序

按照这个顺序不断地将边加入MST,除非会产生一个环(不加入之并跳过)

实际上Kruskal算法是上述贪心算法实现的一种特例

如何判断加入边是否形成环

DFS,可行,时间复杂度​

并查集,更好,时间复杂度​

对每一个连通分量​,维护一个集合

如果考察的边​,如果在同一集合,那么其会产生一个环

加入新边,则将包含v和w的集合做合并操作

代码实现:

1
public class KruskalMST
2
{
3
private Queue mst = new Queue();
4

5
public KruskalMST(EdgeWeightedGraph G)
6
{
7
MinPQ pq = new MinPQ();
8

9
for (Edge e : G.edges())
10
pq.insert(e);
11

12
UF uf = new UF(G.V());
13

14
while (!pq.isEmpty() && mst.size() < G.V()-1)
15
{
16
Edge e = pq.delMin();
17
int v = e.either(), w = e.other(v);
18
if (!uf.connected(v, w))
19
{
20
uf.union(v, w);
21
mst.enqueue(e);
22
}
23
}
24
}
25

26
public Iterable edges()
27
{ return mst; }
28
}
性质:时间复杂度​。如果所有的边都排好序了,那么复杂度为​(几乎是线性的)

实际应用中其实并不需要排序,只要设法让MST中存在​条边即可(小于传统排序方法消耗)

普利姆算法 Prim’s Algorithm
算法过程

从0顶点开始,贪心地增长树​

将当前其中一个端点在​中的最小权重边加入到树中

重复上述过程直到​条边

同样地,Prim算法也是贪心算法构建MST的一个特例

如何寻找有且只有一个端点在树上的最小权重边

检查所有的边,时间复杂度为​

优先队列,时间复杂度​

懒(Lazy)解决方案:维护一个优先队列,其中为一个端点在树上的边

键:边

优先级:边的权重

使用Delete-min决定下一条加入到​中的树

忽略两个端点都在​中的情况(即过去的情况没有清除)

否则,将与另一端点相连(另一个点不在​中)的边加入到队列中,并将该点加入到树

1
public class LazyPrimMST
2
{
3
private boolean[] marked; // MST vertices
4
private Queue mst; // MST edges
5
private MinPQ pq; // PQ of edges
6

7
public LazyPrimMST(WeightedGraph G)
8
{
9
pq = new MinPQ();
10
mst = new Queue();
11
marked = new boolean[G.V()];
12
visit(G, 0);
13

14
while (!pq.isEmpty() && mst.size() < G.V() - 1)
15
{
16
Edge e = pq.delMin();
17
int v = e.either(), w = e.other(v);
18
if (marked[v] && marked[w]) continue; // disregard
19
mst.enqueue(e); //enqueue
20
if (!marked[v]) visit(G, v); // check new point
21
if (!marked[w]) visit(G, w);
22
}
23
}
24

25
private void visit(WeightedGraph G, int v)
26
{
27
marked[v] = true;
28
for (Edge e : G.adj(v))
29
if (!marked[e.other(v)])
30
pq.insert(e);
31
}
32

33
public Iterable mst()
34
{ return mst; }
35
}
懒实现的算法的时间复杂度为​,空间复杂度为​(因为懒更新所以空间占用大)

勤(Eager)实现:维护一个优先队列,其中为与树​有边相连的顶点

其优先级为这个顶点连接到树的最小权重边

删除一个顶点,并将其对应边加入到树中

考虑每一个和删除的顶点v相连的边​

如果x已经在树中,忽略之

否则将其加入优先队列(注意最小权重)

如果​是x到树的最小权重边,减小其优先级(即更新最新的最小权重)

更加有效,因为一个点只能有一个项在队列中而不是边

需要的数据结构:索引的优先队列

对优先队列里的每一个键都有一个索引

可以按照指定的索引修改值

1
public class IndexMinPQ<Key extends Comparable>
2
{
3
IndexMinPQ(int N); //create indexed priority queue with indices 0, 1, …, N-1
4
void insert(int i, Key key); // associate key with index i
5
void decreaseKey(int i, Key key); // decrease the key associated with index i
6
boolean contains(int i); // is i an index on the priority queue?
7
int delMin(); // remove a minimal key and return its associated index
8
boolean isEmpty(); // is the priority queue empty?
9
int size(); // number of entries in the priority queue
10
}
实现

拥有和普通有限队列PQ相同的代码

维护三个平行的数组keys[],pq[],和qp[]

keys[i]表示i的优先级

pq[i]表示在堆位置i上的索引

qp[i]表示索引i对应的堆位置

使用swi(qp[k])实现decreaseKey(k, key)

这样我们就能在对数时间复杂度内实现对优先级的更新

运行时间:取决于PQ的实现,总共包含​次插入,​次取最小值,以及最高​次降低权重

对二元堆实现的PQ,时间复杂度为​

对稠密图,使用数组实现(​)

对稀疏图,使用二元堆实现更快

4路堆在一些性能关键的情况下更值得考虑(​)

斐波那契堆理论上最快(​)

最小生成树的应用 MST Context
目前未能得知是否存在一个线性时间构造MST的方法

欧几里得MST:给定平面上的N个点,寻找MST,权重即点之间的欧几里得距离(稠密图,任两点间都有距离,因此都有边)

使用Delaunay triangulation简化表示,实现​的算法

聚类:将点集分割为k个群,分割依据为“距离”,目标是各聚类键的点都距离很远

单链聚类:给定整数K,找到一个K聚类使最近的两个聚类的距离(两个聚类最近的点)最大

初始定义V个聚类

寻找分别位于两个聚类的最近点对,将之所在的聚类合并

重复上述过程直到K个聚类

这个就是kruskal算法,只是在K个通量是停止

或者使用prim算法,从PQ去除k-1条最大边

最短路 Shortest Paths
最短路:给定一个带权边的有向图,找出从s到t的最短(权重和最小)路径

去什么最短路?

源-汇型(Source-Sink):从一个顶点到另一个顶点

单源型:从一个顶点到所有其他顶点

所有配对型:所有点对

边权重的限制:

非负权重

任意权重

欧几里得权重(即欧几里得距离为权重)

对于环路的限制:

无有向环

无负环

本课程的简化假设:从s到每一个顶点v的最短路存在

最短路API Shortest Paths API
有向边的API如下:

1
public class DirectedEdge
2
{
3
DirectedEdge(int v, int w, double weight); // weighted edge v→w
4
int from(); // vertex v
5
int to(); // vertex w
6
double weight(); // weight of this edge
7
String toString(); // string representation
8
}
有向边的代码实现如下,无向带权边类似,但是方法指明了起始端点和终止端点:

1
public class DirectedEdge
2
{
3
private final int v, w;
4
private final double weight;
5

6
public DirectedEdge(int v, int w, double weight)
7
{
8
this.v = v;
9
this.w = w;
10
this.weight = weight;
11
}
12

13
public int from()
14
{ return v; }
15

16
public int to()
17
{ return w; }
18

19
public int weight()
20
{ return weight; }
21
}
带权有向图的API如下:

1
public class EdgeWeightedDigraph
2
{
3
EdgeWeightedDigraph(int V); // edge-weighted digraph with V vertices
4
EdgeWeightedDigraph(In in); // edge-weighted digraph from input stream
5
void addEdge(DirectedEdge e); // add weighted directed edge e
6
Iterable adj(int v); // edges pointing from v
7
int V(); // number of vertices
8
int E(); // number of edges
9
Iterable edges(); // all edges
10
String toString(); // string representation
11
}
允许自循环和平行边

图实现:邻接列表表示

特殊改进的操作实现:

1
public EdgeWeightedDigraph(int V)
2
{
3
this.V = V;
4
adj = (Bag[]) new Bag[V];
5
for (int v = 0; v < V; v++)
6
adj[v] = new Bag();
7
}
8

9
public void addEdge(DirectedEdge e)
10
{
11
int v = e.from();
12
adj[v].add(e);
13
}
14

15
public Iterable adj(int v)
16
{ return adj[v]; }
单源最短路API如下(找到距离s到每一个顶点的最短路):

1
public class SP
2
{
3
SP(EdgeWeightedDigraph G, int s); // shortest paths from s in graph G
4
double distTo(int v); // length of shortest path from s to v
5
Iterable pathTo(int v); // shortest path from s to v
6
boolean hasPathTo(int v); // is there a path from s to v?
7
}
最短路性质 Shortest Paths Properties
一个最短路径树的解是存在的,使用两个数组维护一个SPT

distTo[v]是从s到v的最短路长度

edgeTo[v]是从s到v的最短路的最后一条边

代码实现:

1
public double distTo(int v)
2
{ return distTo[v]; }
3

4
public Iterable pathTo(int v)
5
{
6
Stack path = new Stack();
7
for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()])
8
path.push(e);
9
return path;
10
}
边的松弛(Edge Relaxation):在已知ditsTo[v]和distTo[w]以及edgeTo[w]之后,我们会将v -> w(如果存在)考虑进去以进一步更新数据结构,如果获得更新,那么直接将这条边更新w的最短路相关信息即可

1
private void relax(DirectedEdge e)
2
{
3
int v = e.from(), w = e.to();
4
if (distTo[w] > distTo[v] + e.weight()) // 我们找到了到w的一条新最短路
5
{
6
distTo[w] = distTo[v] + e.weight();
7
edgeTo[w] = e;
8
}
9
}
性质(最优情况):令G为带权有向图,那么ditsTo[]是从s到每个顶点的最短路距离,当且仅当:

distTo[s]为0

对任意顶点v,distTo[v]是从s到v的某一路径的长度

对每一条边e = v -> w,都有distTo[w] <= distTo[v] + e.weight()

通用算法:

初始化distTop[s]=0且distTo[v] = ∞

重复以下过程直到满足最优情况

松弛任一边

此处尚未表明如何决定哪条边需要松弛以及如何判断达到最优情况

如果存在SPT,这一过程可以计算得出之

迪杰斯特拉算法 Djikstra’s Algorithm
算法设计

按照到从s到各顶点的距离升序考察每一个顶点(然后考察具有最小distTo[]的非树中节点)

将这个顶点加入到树中并松弛每一条从该点指出的边

重复上述过程直到全部顶点加入树中

代码实现:

1
public class DijkstraSP
2
{
3
private DirectedEdge[] edgeTo;
4
private double[] distTo;
5
private IndexMinPQ pq;
6

7
public DijkstraSP(EdgeWeightedDigraph G, int s)
8
{
9
edgeTo = new DirectedEdge[G.V()];
10
distTo = new double[G.V()];
11
pq = new IndexMinPQ(G.V());
12

13
for (int v = 0; v < G.V(); v++)
14
distTo[v] = Double.POSITIVE_INFINITY;
15

16
distTo[s] = 0.0;
17
pq.insert(s, 0.0);
18
while (!pq.isEmpty())
19
{
20
int v = pq.delMin();
21
for (DirectedEdge e : G.adj(v))
22
relax(e);
23
}
24
}
25

26
private void relax(DirectedEdge e)
27
{
28
int v = e.from(), w = e.to();
29
if (distTo[w] > distTo[v] + e.weight())
30
{
31
distTo[w] = distTo[v] + e.weight();
32
edgeTo[w] = e;
33
if (pq.contains(w)) pq.decreaseKey(w, distTo[w]);
34
else pq.insert (w, distTo[w]);
35
}
36
}
37
}
与Prim算法的比较

二者基本相同,都是在计算一个图的生成树

但是二者在选择树的下一个顶点的策略上不同

Prim算法选择到这个树的最近顶点(边权重最小,针对无向图)

Dijkstra算法选择到源点的最近顶点(路径权重最小,针对有向图)

同样,算法性能也取决于PQ的实现

带权边有向无环图 Edge-Weighted DAG
对于一个带权DAG,其寻找一条最短路要比普通有向图简单(因为不必考虑有向环路情况)

算法设计(拓扑序最短路算法):

按照图的(由于是DAG,必有拓扑序)拓扑序考虑每一个节点

松弛送这个节点发出的所有边

性质:拓扑序最短路算法在任意DAG(甚至是负权重边)的时间正比于​

代码实现:

1
public class AcyclicSP
2
{
3
private DirectedEdge[] edgeTo;
4
private double[] distTo;
5

6
public AcyclicSP(EdgeWeightedDigraph G, int s)
7
{
8
edgeTo = new DirectedEdge[G.V()];
9
distTo = new double[G.V()];
10

11
for (int v = 0; v < G.V(); v++)
12
distTo[v] = Double.POSITIVE_INFINITY;
13
distTo[s] = 0.0;
14

15
Topological topological = new Topological(G); // 产生拓扑序
16
for (int v : topological.order())
17
for (DirectedEdge e : G.adj(v))
18
relax(e);
19
}
20
}
寻找带权最短路中的最长路径:将其形式化为最短路问题

将所有权重值变号(拓扑序最短路算法可以工作于负权重上)

寻找最短路

再将结果符号变回来

相当于将松弛函数中的不等式方向反过来

平行工作调度:给定一组具有时长和流程限制的工作,调度之(为每一个工作找到一个开始时间),以实现最小完成时间

解决:关键路径算法(Critical Path method),创建一个带权DAG

源点(到任何其他顶点)和汇点(任何其他顶点到此)

每一个工作具有两个顶点(始和终)

每一个工作三条边

始到终(按时长赋权)

从源到开始(0权重)

从终到汇(0权重)

每一个流程限制有一条边(0权重)

使用从源开始的最长路径规划每一个工作

负权重 Negative Weights
Dijkstra算法不能用于带有负权重的有向图:因为可能存在已经松弛了所有边的顶点因为负权重的存在被其他顶点松弛时更新了最短路,这时该点的后续节点将得不到更新而无法得到真正的最短路(因为在算法原始假设中,每条边只会松弛一次,松弛完就不再入队了)

尝试解决方案:将所有权重平移一个正向值,以将所有权重变成正值:显然不行

负环:权重总和为负数的环

一个SPT存在,当且仅当图中没有负环

Bellman-Ford算法(不存在负环)

初始化distTo[s] = 0以及distTo[v] = ∞

以下步骤重复V次

松弛每一条边

1
for (int i = 0; i < G.V(); i++) // V次
2
for (int v = 0; v < G.V(); v++) // 对所有点遍历一次,松弛所有边
3
for (DirectedEdge e : G.adj(v))
4
relax(e);
时间复杂度正比于​

优化策略

如果在第i次遍历,distTo[v]没有更新,那么在第i+1次遍历,也不必考察从v出发的边(维护一个保存了改变了距离值的点的队列)

尽管最坏情况下时间仍正比于​,但是实际应用中更快(​)

寻找一个负环

两个API方法

1
boolean hasNegativeCycle(); // is there a negative cycle?
2
Iterable negativeCycle(); // negative cycle reachable from s
一个性质:对于BF算法,如果在第V次遍历中(最后一次遍历),顶点v仍然在更新,那么一定存在一个负环,并且可以通过edge[v]的追踪找到这个环

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值