加權有向圖和最短路徑
加權有向圖
之前學習的加權無向圖中,邊是沒有方向的,並且同一條邊會同時出現在該邊的兩個頂點的鄰接表中,為了能夠處 理含有方向性的圖的問題,我們需要實現以下加權有向圖。
加權有向圖邊的表示
類名 | 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