加权有向图
之前学习的加权无向图中,边是没有方向的,并且同一条边会同时出现在该边的两个顶点的邻接表中,为了能够处 理含有方向性的图的问题,我们需要实现以下加权有向图。
加权有向图边的表示
类名 | DirectedEdge |
---|---|
构造方法 | DirectedEdge(int v,int w,double weight):通过顶点v和w,以及权重weight值构造一个边对象 |
成员方法 | 1.public double weight():获取边的权重值 2.public int from():获取有向边的起点 3.public int to():获取有向边的终点 |
成员变量 | 1.private final int v:起点 2.private final int w:终点 3.private final double weight:当前边的权重 |
代码:
/**
* 有向边
* @author wen.jie
* @date 2021/8/30 14:39
*/
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 this.weight;
}
//起点
public int from() {
return v;
}
//终点
public int to() {
return w;
}
@Override
public String toString() {
return "DirectedEdge{" +
"v=" + v +
", w=" + w +
", weight=" + weight +
'}';
}
}
加权有向图的实现
类名 | EdgeWeightedDigraph |
---|---|
构造方法 | EdgeWeightedDigraph(int V):创建一个含有V个顶点的空加权有向图 |
成员方法 | 1.public int V():获取图中顶点的数量 2.public int E():获取图中边的数量 3.public void addEdge(DirectedEdge e):向加权有向图中添加一条边e 4.public Queue adj(int v):获取由顶点v指出的所有的边 5.public Queue edges():获取加权有向图的所有边 |
成员变量 | 1.private final int V: 记录顶点数量 .private int E: 记录边数量 3.private Queue[] adj: 邻接表 |
代码:
/**
* 加权有向图
* @author wen.jie
* @date 2021/8/30 14:42
*/
public class EdgeWeightedDigraph{
private final int V;
private int E;
private Queue<DirectedEdge>[] adj;
public EdgeWeightedDigraph(int V) {
this.V = V;
this.E = E;
this.adj = new Queue[V];
for (int i = 0; i < adj.length; i++) {
adj[i] = new Queue<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
//向加权有向图中添加一条边
public void addEdge(DirectedEdge e) {
int v = e.from();
adj[v].enqueue(e);
E++;
}
//获取由顶点v指出的所有的边
public Queue<DirectedEdge> adj(int v) {
return adj[v];
}
//获取加权有向图中的所有边
public Queue<DirectedEdge> edges() {
Queue<DirectedEdge> allEdges = new Queue<>();
for (int v = 0; v < V; v++) {
for (DirectedEdge edge : adj[v]) {
allEdges.enqueue(edge);
}
}
return null;
}
}
最短路径
最短路径定义及性质
定义: 在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径。
性质:
1.路径具有方向性;
2.权重不一定等价于距离。权重可以是距离、时间、花费等内容,权重最小指的是成本最低
3.只考虑连通图。一副图中并不是所有的顶点都是可达的,如果s和t不可达,那么它们之间也就不存在最短路径,为了简化问题,这里只考虑连通图。
4.最短路径不一定是唯一的。从一个顶点到达另外一个顶点的权重最小的路径可能会有很多条,这里只需要找出一条即可。
最短路径树:给定一副加权有向图和一个顶点s,以s为起点的一棵最短路径树是图的一副子图,它包含顶点s以及从s可达的所有顶点。这棵有向树的根结点为s,树的每条路径都是有向图中的一条最短路径。
最短路径树API设计
计算最短路径树的经典算法是dijstra算法,为了实现它,先设计如下API:
类名 | DijkstraSP |
---|---|
构造方法 | public DijkstraSP(EdgeWeightedDigraph G, int s):根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象 |
成员方法 | 1.private void relax(EdgeWeightedDigraph G, int v):松弛图G中的顶点v 2.public double distTo(int v):获取从顶点s到顶点v的最短路径的总权重 3.public boolean hasPathTo(int v):判断从顶点s到顶点v是否可达 4.public Queue pathTo(int v):查询从起点s到顶点v的最短路径中所有的边 |
成员变量 | 1.private DirectedEdge[] edgeTo: 索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边 2.private double[] distTo: 索引代表顶点,值从顶点s到当前顶点的最短路径的总权重 3.private IndexMinPriorityQueue<Double> pq:存放树中顶点与非树中顶点之间的有效横切边 |
松弛技术
松弛这个词来源于生活:一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以放松了。
松弛这种简单的原理刚好可以用来计算最短路径树。
在我们的API中,需要用到两个成员变量edgeTo和distTo,分别存储边和权重。一开始给定一幅图G和顶点s,我们只知道图的边以及这些边的权重,其他的一无所知,此时初始化顶点s到顶点s的最短路径的总权重disto[s]=0;顶点s到其他顶点的总权重默认为无穷大,随着算法的执行,不断的使用松弛技术处理图的边和顶点,并按一定的条件更新edgeTo和distTo中的数据,最终就可以得到最短路径树。
边的松弛:
放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再从v到w?
如果是,则v-w这条边需要加入到最短路径树中,更新edgeTo和distTo中的内容:edgeTo[w]=表示v->w这条边的DirectedEdge对象,distTo[w]=distTo[v]+v->w这条边的权重;
如果不是,则忽略v->w这条边。
顶点的松弛:
顶点的松弛是基于边的松弛完成的,只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。
如果把起点设置为顶点0,那么找出起点0到顶点6的最短路径0->2->7>3->6的过程如下:
Dijstra算法实现
Disjstra算法的实现和Prim算法很类似,构造最短路径树的每一步都是向这棵树中添加一条新的边,而这条新的边是有效横切边pq队列中的权重最小的边。
代码:
/**
* Dijkstra算法
* @author wen.jie
* @date 2021/8/30 15:14
*/
public class DijkstraSP {
//索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边
private DirectedEdge[] edgeTo;
//索引代表顶点,值从顶点s到当前顶点的最短路径的总权重
private double[] distTo;
//存放树中顶点与非树中顶点之间的有效横切边
private IndexMinPriorityQueue<Double> pq;
public DijkstraSP(EdgeWeightedDigraph G, int s){
this.edgeTo = new DirectedEdge[G.V()];
this.distTo = new double[G.V()];
Arrays.fill(distTo, Double.POSITIVE_INFINITY);
pq = new IndexMinPriorityQueue<>(G.V());
distTo[s] = 0.0;
pq.insert(s, 0.0);
while (!pq.isEmpty()) {
relax(G, pq.delMin());
}
}
//松弛图G中的顶点v
private void relax(EdgeWeightedDigraph G, int v){
for (DirectedEdge edge : G.adj(v)) {
int w = edge.to();
//松弛技术
if (distTo(v) + edge.weight() < distTo(w)){
distTo[w] = distTo[v] + edge.weight();
edgeTo[w] = edge;
if (pq.contains(w)) {
pq.changeItem(w, distTo(w));
} else {
pq.insert(w, distTo(w));
}
}
}
}
//获取从顶点s到顶点v的最短路径的总权重
public double distTo(int v){
return distTo[v];
}
//判断从顶点s到顶点v是否可达
public boolean hasPathTo(int v){
return distTo[v] < Double.POSITIVE_INFINITY;
}
//查询从起点s到顶点v的最短路径中所有的边
public Queue<DirectedEdge> pathTo(int v){
if (!hasPathTo(v)){
return null;
}
Queue<DirectedEdge> allEdges = new Queue<>();
while (true) {
DirectedEdge e = edgeTo[v];
if (e == null)
break;
allEdges.enqueue(e);
v = e.from();
}
return allEdges;
}
}
测试:
String[] strs = new String[]{
"4 5 0.35",
"5 4 0.35",
"4 7 0.37",
"5 7 0.28",
"7 5 0.28",
"5 1 0.32",
"0 4 0.38",
"0 2 0.26",
"7 3 0.39",
"1 3 0.29",
"2 7 0.34",
"6 2 0.40",
"3 6 0.52",
"6 0 0.58",
"6 4 0.93"
};
@Test
public void test() {
EdgeWeightedDigraph G = new EdgeWeightedDigraph(8);
for (String str : strs) {
String[] split = str.split(" ");
DirectedEdge e = new DirectedEdge(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Double.parseDouble(split[2]));
G.addEdge(e);
}
DijkstraSP dijkstraSP = new DijkstraSP(G, 0);
Queue<DirectedEdge> edges = dijkstraSP.pathTo(6);
for (DirectedEdge edge : edges) {
System.out.println(edge);
}
}
本文所有代码均已上传:https://gitee.com/wj204811/algorithm