dijkstra算法
算法描述
Dijkstra算法是解决单源最短路径问题的贪心算法,它先求出长度最短的一条路径,再参照该最短路径求出长度次短的一条路径,直到求出从源点到其他各个顶点的最短路径。
题目引入
给定一个景点地图,景点编号[0,1…5],各个景点之间的路况以及路的长度如图所示,给定一个起始点0,求出该点到其他所有点的最短路径。
解题思路
确定数据结构
:通常情况下,大部分图都可以使用矩阵来存储,可根据需求选择对应的数据结构,这里通过简单的矩阵(二维数组[][])存储图。初始化
:在算法中,我们指定某一节点u为源点,对于源点到其他节点的距离,我们需要更新到最短路径数组dist[]中,dist[i]=G[u][i]。找最小
:根据贪心策略,找未遍历节点中,距离已遍历节点的最小距离的节点,访问该节点并标记该节点的状态为已遍历。松弛操作
:对于已遍历的节点,检查是否可以通过新遍历到的节点得到更短的"源点-旧节点"的距离。重复执行3、4
:当所有的节点都已被访问到(状态都标记为已遍历),算法结束。
符号含义
- G[][]:图的邻接矩阵表示
- dist[]:起始点到所有点的距离(最终迭代为最短距离)
- flag[]:boolean类型,记录点的访问情况,已访问为true,未访问未false
图解过程
确定数据结构
- 用矩阵(二维数组)
G [ ] = [ 0 2 5 ∞ ∞ ∞ 0 2 6 ∞ ∞ ∞ 0 7 1 ∞ ∞ 2 0 4 ∞ ∞ ∞ ∞ 0 ] G[] = \left[ \begin{matrix} 0 & 2 & 5 & \infty & \infty \\ \infty & 0 & 2 & 6 & \infty \\ \infty & \infty &0 & 7 & 1 \\ \infty & \infty & 2 & 0 & 4 \\ \infty & \infty & \infty & \infty & 0 \\ \end{matrix} \right] G[]=⎣⎢⎢⎢⎢⎡0∞∞∞∞20∞∞∞5202∞∞670∞∞∞140⎦⎥⎥⎥⎥⎤
初始化
我们约定,假设存在两个集合,已访问节点放在集合S中,未访问节点放在U中。对最短距离数组dist[]进行初始化,dist[i] = G[0][i]
- S={0}
- U={1,2,3,4}
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
dist[] | 0 | 2 | 5 | ∞ | ∞ |
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
flag[] | true | false | false | false | false |
找最小1
根据贪心策略,查找distp[]中最小的距离,dist[1]= 2 ,将点1添加进S集合
- S={0,1}
- U={2,3,4}
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
flag[] | true | true | false | false | false |
松弛操作1
对于U集合的所有点,检查是否可以借助刚添加进来的1号点,更新起始点到U中其他点的距离
- 1号点可以更新0-2的距离:dist[2] = dist[1] + G[1][2] = 2+2
- 1号点可以更新0-3的距离:dist[3] = dist[1] + G[1][3] = 2+6
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
dist[] | 0 | 2 | 4 | 8 | ∞ |
找最小2
根据贪心策略,查找distp[]中最小的距离,dist[2] = 4 ,将点2添加进S集合
- S={0,1,2}
- U={3,4}
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
flag[] | true | true | true | false | false |
松弛操作2
对于U集合的所有点,检查是否可以借助刚添加进来的2号点,更新起始点到U中其他点的距离
- 2号点可以更新0-4的距离:dist[4] = dist[2] + G[2][4] = 4+1
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
dist[] | 0 | 2 | 4 | 8 | 5 |
找最小3
根据贪心策略,查找distp[]中最小的距离,dist[2] = 4 ,将点2添加进S集合
- S={0,1,2,4}
- U={3}
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
flag[] | true | true | true | false | true |
松弛操作3
对于U集合的所有点,检查是否可以借助刚添加进来的4号点,更新起始点到U中其他点的距离
- 4号节点没有邻接节点,没得更新
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
dist[] | 0 | 2 | 4 | 8 | 5 |
找最小4
根据贪心策略,查找distp[]中最小的距离,此时只剩下3号点未被访问 ,将点3添加进S集合
- S={0,1,2,4,3}
- U={}
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
flag[] | true | true | true | true | true |
算法结束
当所有的点均被访问,算法结束。得到最终的最短距离数组dist[]
- 起始点 0 - 0 的距离:0
- 起始点 0 - 1 的距离:2
- 起始点 0 - 2 的距离:4
- 起始点 0 - 3 的距离:8
- 起始点 0 - 4 的距离:5
点 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
dist[] | 0 | 2 | 4 | 8 | 5 |
代码实现
//代码算出了起始点start到所有点的最短距离,若想返回start到所有点的最短距离,返回值改成dist即可
public class Dijkstra {
public static void main(String[] args) {
int max = Integer.MAX_VALUE/2;
int[][] G = {{0,2,5,max,max},{max,0,2,6,max},{max,max,0,7,1},{max,max,2,0,4},{max,max,max,max,0}};
System.out.println(dijkstra(G,0,4));
}
public static int dijkstra(int[][] G,int start,int end){
//初始化
int[] dist = new int[G.length];
boolean[] flag = new boolean[G.length];
for (int i = 0; i < G.length; i++) {
dist[i]=G[start][i];
}
//初始的集合S中只有起始点
flag[start]=true;
//外层for循环:重复找最小、松弛操作
for (int i = 0; i < G.length; i++) {
int temp = Integer.MAX_VALUE/2;
int small = start;
//找最小
for (int j = 0; j < G.length; j++) {
if(!flag[j] && dist[j]<temp){
small=j;
temp = dist[j];
}
}
flag[small]=true; //标记该点为已访问
//松弛操作
for (int j = 0; j < G.length; j++) {
if(!flag[j] && dist[j]>dist[small]+G[small][j]){
dist[j]=dist[small]+G[small][j];
}
}
}
return dist[end];
}
}
扩展题
有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = { start(i), end(i), price(i) } ,表示该航班都从城市 start(i) 开始,以价格 price(i) 抵达 end(i)。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,求一条中转的路线,使得从 src 到 dst 的 总价格最便宜 ,并返回该价格。
思路:
- 这样的题仍然是可以用图去解决的,但是要进行映射操作,即{“广州=>0”},{“深圳”=>1},将城市名称映射成数字,就可以像第一个例子那样去解了。
代码:
- 航班类
/**
* 航班类
*/
public class Flight {
private String start, end;
private int price;
Flight(String s, String e, int p) {
start = s;
end = e;
price = p;
}
// 获取起点名称
public String getStart() {
return start;
}
// 获取终点名称
public String getEnd() {
return end;
}
// 获取从起点到终点的机票价格
public int getPrice() {
return price;
}
}
- 方法类
public class TestDemo {
public static void main(String[] args) {
Flight[] flight = new Flight[8];
Flight flight1 = new Flight("广州","深圳",2);
Flight flight2 = new Flight("广州","上海",5);
Flight flight3 = new Flight("深圳","上海",2);
Flight flight4 = new Flight("深圳","北京",6);
Flight flight5 = new Flight("上海","北京",7);
Flight flight6 = new Flight("上海","重庆",1);
Flight flight7 = new Flight("北京","上海",2);
Flight flight8 = new Flight("北京","重庆",4);
flight[0]=flight1;
flight[1]=flight2;
flight[2]=flight3;
flight[3]=flight4;
flight[4]=flight5;
flight[5]=flight6;
flight[6]=flight7;
flight[7]=flight8;
TestDemo ts = new TestDemo();
String src = "广州";
String dst = "北京";
int minPrice = ts.findCheapestPrice(5,flight,src,dst);
System.out.println(src+"到"+dst+"的最小价格为:"+minPrice);
}
/**
* 计算最便宜的机票总价钱
* @param n 表示最多总共有n个城市
* @param flights 所有航班信息,以对象数组给出,每个Flight对象表示一个航班数据(起点-终点-价格)
* @param src 起点的名称
* @param dst 终点的名称
* @return 搜索一条路径可以从起点转乘一直飞到终点,并且机票总价格最小,输出最小机票价格
*/
public int findCheapestPrice(int n, Flight[] flights, String src, String dst) {
//1.准备图,做【节点-数字索引】的映射
int[][] G = new int[n][n]; //图
int[] dist = new int[n]; //源点到各点的最短路径数组
boolean flag[] = new boolean[n]; //节点对应的访问状态数组
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if(i==j){
G[i][j]=0;
}else{
G[i][j]=100000;
}
}
}
//2.映射
Set<String> pointSet = new HashSet<>(); //节点集合
Map<String,Integer> pointMap = new HashMap<>(); //节点-数字索引的映射
int m = 0;
for (Flight flight : flights) {
pointSet.add(flight.getStart());
pointSet.add(flight.getEnd());
}
for (String s : pointSet) {
pointMap.put(s,m++);
}
//航班数据映射填充到图的矩阵中
for (Flight flight : flights) {
String start = flight.getStart();
String end = flight.getEnd();
int price = flight.getPrice();
G[pointMap.get(start)][pointMap.get(end)]=price;
}
//3.初始化源点到所有节点的最短距离数组dist[]
for(int i=0;i<dist.length;i++){
dist[i]=G[pointMap.get(src)][i];
}
flag[pointMap.get(src)]=true;
dist[pointMap.get(src)]=0;
//重复【找最小-松弛操作】,直到所有节点都被访问
for(int i=0;i<n;i++){
int temp = 100000;
int small = pointMap.get(src);
//4.找最小,当节点未被访问,且小于定义的无穷标记10000
for (int j = 0; j < n; j++) {
if(!flag[j]&&dist[j]<temp){
//small和temp一值在更新,保证找到最小的
small=j;
temp=dist[j];
}
}
//找到了,这个点对应的状态数组中标记为true,代表已访问
flag[small]=true;
//5.松弛操作,借助上面找到的点进行距离更新
for (int j = 0; j < n; j++) {
//如果原先的距离:[源点->该节点] > [源点->新节点]+[新节点->该节点],更新最短距离
if(!flag[j] && dist[j]>dist[small]+G[small][j]){
dist[j]=dist[small]+G[small][j];
}
}
}
return dist[pointMap.get(dst)];
}