红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【图论】最短路径

最短路径

Dijkstra

思路

基本步骤:

  • 选定一个点,满足:
    • 未被选过
    • 距离最短
  • 对这个点的所有邻接点尝试松弛(如下图,从a到c权值为5,而从a到b到c权值为3,我们用后者代替前者成为最短路,此之为成功的松弛)

从a开始,依次寻找下一个节点,使得从a到当前节点的路径最短,直到找到结尾

遍历时,我们依次寻找a到当前节点的最短路径以及最近节点

当我们得到了最短权值,我们可以反推回a从而获取最短路径

用矩阵表示如下

代码

邻接矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 2e5 + 10;
const int mod = 1e9 + 7;
vector<vector<int>> G;
vector<int> path;
vector<int> dijkstra(vector<vector<int>> &G, int s){
  int n = G.size();
  vector<int> dis(n,0x3f3f3f3f);
  vector<int> vis(n,0);
  dis[s] = 0;
  for (int i = 0;i<n-1;i++){
    int node = -1;
    for (int j = 0;j<n;j++){
      if (!vis[j]&&(node==-1||dis[j]<dis[node])){
        node = j;
      }
    }
    for (int j = 0;j<n;j++){
      if (dis[node]+G[node][j]<dis[j]){
        dis[j] = dis[node]+G[node][j];
        path[j] = node;
      }
    }
    vis[node] = true;
  }
  return dis;

}
// 路径获取
void get_path(int x, vector<int> &P){
  if (!x){
    P.push_back(0);
    return ;
  }
  // 若该节点前有节点,则先获取前面节点的内容
  if (path[x]!=-1)get_path(path[x], P);
  P.push_back(x);

}
void solve() {
  int n, m, s;
  cin >> n >> m >> s;
  path = vector<int>(n,-1);
  G = vector<vector<int>> (n,vector<int>(n, 0x3f3f3f3f));
  for (int i = 0;i<m;i++){
    int u,v,w;
    cin >> u >> v >> w;
    u--,v--;
    if (!G[u][v])G[u][v]=w;
    else G[u][v]=min(G[u][v],w);
  }
  auto dis = dijkstra(G, s-1);
  for (auto it: dis)cout << it <<' ';
  
}
int main()
{
  ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
  int tomorin=1;
  //cin >> tomorin;
  while (tomorin--) solve();
  return 0;
}

最小堆优化

上述朴素算法的问题:

  • 寻找最小点时,通过遍历n个点来获得最小点
  • 对该点邻接点进行松弛时,我们也是通过遍历去寻找邻接点

对于以上问题,我们可以选用小根堆与邻接表直接获取,可以实现代码的优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
vector<int> edge, weight, Next, head;
int n, m, s;
int idx=0;
vector<int> dijkstra(int s){
    vector<int> dis(n,0x3f3f3f3f);
    vector<int> vis(n,0);
    dis[s]=0;
    // 最小堆,first记录dis, second记录node
    priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> mh;
    mh.push({dis[s],0});
    while(!mh.empty()){
        int node = mh.top().second;
        mh.pop();
        if(vis[node]) continue;
        for(int i = head[node];~i;i=Next[i]){
            if (dis[edge[i]]>dis[node]+weight[i]){
                dis[edge[i]]=dis[node]+weight[i];
                mh.push({dis[edge[i]],edge[i]});
            }
        }
        vis[node]=true;
    }
    return dis;
}
void add_edge(int u, int v, int w){
    edge[idx] = v;
    weight[idx] = w;
    Next[idx] = head[u];
    head[u]=idx++;
}
int main(){
    cin >> n >> m >> s;
    head = vector<int> (n,-1);
    edge = weight = Next = vector<int> (m,0);
    while(m--){
        int u, v, w;
        cin >> u >> v >> w;
        u--,v--;
        add_edge(u,v,w);
    }
    auto dis = dijkstra(s-1);
    for(auto it:dis) cout << it <<' ';
    return 0;

}

Floyd

思路

Floyd与Dijkstra都是看节点间的值,但是前者看的是每两点间最短距离

Dis数组,记录两点距离:

这里矩阵中每个位置表示两点间的距离,而D上的下标表示从何处中转得到的距离(-1表示不进行中转)

如此时\(D^0\)为从\(v_0\)进行中转得到的两点间距离,且距离可以根据红色式子表示,即经过中转点后的距离与上一步已有距离的最小值

Path数组,记录经过点(最短路径)

Path数组记录了两点间经过的中转点是哪个点,若为直达则记录终点,如\(P^{-1}\)指的是没有中转,其中记录的都是终点,\(P^0\)指经过\(v_0\)中转,故\(v_1\to v_2\)对应的值为0,表示中转点为\(v_0\)

通过path数组推出最短路径:

如寻找\(v_1\)\(v_0\)的最短路径:找到第一行第零列的点对应的点,该点即为到达终点的前一个点;再找到该点对应的点,即为到达该点的前一个点,直到推到起点

代码

(邻接矩阵)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <bits/stdc++.h>
using namespace std;
const int N = 100;
const int M = 200;
const int MAX = 0x10000;
typedef char VertexType ;
typedef int EdgeType;

struct mat_grph{
  VertexType vertex[N];
  EdgeType arc[N][N];
  int vertex_num;
  int edge_num;
};

void createGraph(mat_grph* G){
  
  G->vertex_num = 9;
  G->edge_num = 15; 
  for (int i = 0; i<G->vertex_num;i++){
    G->vertex[i] = i;
  }
  for (int i = 0;i<G->vertex_num;i++){
    for (int j = 0;j<G->vertex_num;j++){
      if(i==j) G->arc[i][j] = 0;
      else G->arc[i][j]=MAX;
    }
  }
  G->arc[0][1] = 1;
  G->arc[0][2] = 5;
  G->arc[1][2] = 3;
  G->arc[1][3] = 7;
  G->arc[1][4] = 5;
  G->arc[2][4] = 1;
  G->arc[2][5] = 7;
  G->arc[3][4] = 2;
  G->arc[3][6] = 3;
  G->arc[4][5] = 3;
  G->arc[4][6] = 6;
  G->arc[4][7] = 9;
  G->arc[5][7] = 5;
  G->arc[6][7] = 2;
  G->arc[6][8] = 7;
  G->arc[7][8] = 4;
  for (int i = 0;i<G->vertex_num;i++){
    for (int j = 0; j<G->vertex_num;j++){
      G->arc[j][i] =  G->arc[i][j];
    }
  }
}
void floyd(mat_grph G){
  int path[N][N];
  int dis[N][N];
  for (int i = 0;i<G.vertex_num;i++){
    for (int j = 0;j<G.vertex_num;j++){
      dis[i][j] = G.arc[i][j]; // 最初的dis数组
      path[i][j] = j; // 最初的path数组
    }
  }
  for (int i = 0;i<G.vertex_num;i++){
    for (int j = 0;j<G.vertex_num;j++){
      for (int k = 0;k<G.vertex_num;k++){
        if(dis[j][k]>dis[j][i]+dis[i][k]){
          dis[j][k] = dis[j][i]+dis[i][k];
          path[j][k] = path[j][i];
        }
      }
    }
  }
  int k;
  for (int i = 0; i<G.vertex_num;i++){
    for (int j = i+1; j<G.vertex_num;j++){
      printf("v%d->v%d weight:%d ", i, j, dis[i][j]);
      k = path[i][j];
      printf("path:v%d", i);
      while(k!=j){
        printf("->v%d", k);
        k = path[k][j];
      }
      printf("->v%d\n", j);
    }
    printf("\n");
  }
}

int main(){
  mat_grph G;
  createGraph(&G);
  floyd(G);
  return 0;
}

A*

解决问题:给定原点与目标点,最少代价路径是什么

特点:与dijkstra不同,A*算法考虑从原点到当前点的距离+当前点到目标点距离的估计距离,最后排大小

如该方格,起点为G,终点为U

从G到U预估走4步,G到G走0步,故参与排序为0+4,放入小根堆,并将原点到当前点距离放入distance表中

G 4取出,G到L,N,H,F为1,放入distance,并分别算出到U的预估距离,相加放入小根堆

在取出堆顶并重复上述操作

dij可以看作A*每次预估距离都为0,是一个特例

预估函数

预估函数是一种吸引力,合适的预估函数(越接近真实距离)会大大提升速度

要求:预估距离小于等于到终点的真实距离

预估距离经常选择:曼哈顿距离、欧氏距离、对角线距离等

如:若允许走斜线,曼哈顿距离就不能保证正确性了,此时可以取行与行、列与列间差值最大值

板子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
vector<int> edge, weight, Next, head;
int n, m, s, t;
int idx=0;
vector<int> idis; // 根据需求填入计算合适理想距离
vector<int> astar(int s){
    vector<int> dis(n,0x3f3f3f3f);
    vector<int> vis(n,0);
    dis[s]=0;
    // 最小堆,first记录dis, second记录node
    priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> mh;
    mh.push({dis[s]+idis[s],s});
    while(!mh.empty()){
        int node = mh.top().second;
        mh.pop();
        if(vis[node]) continue; 
        if (s==t) return dis;
        for(int i = head[node];~i;i=Next[i]){
            if (dis[edge[i]]>dis[node]+weight[i]){
                dis[edge[i]]=dis[node]+weight[i];
                mh.push({dis[edge[i]]+idis[edge[i]],edge[i]});
            }
        }
        vis[node]=true;
    }
    return dis;
}
void add_edge(int u, int v, int w){
    edge[idx] = v;
    weight[idx] = w;
    Next[idx] = head[u];
    head[u]=idx++;
}
int main(){
    cin >> n >> m >> s >> t;
    head = vector<int> (n,-1);
    edge = weight = Next = vector<int> (m,0);
    idis = vector<int> (n,0);

    while(m--){
        int u, v, w;
        cin >> u >> v >> w;
        u--,v--;
        add_edge(u,v,w);
    }
    auto dis = astar(s-1);
    for(auto it:dis) cout << it <<' ';
    // if (it==0x3f3f3f3f) cout <<2147483647<<' ';
    // else cout << it <<' ';
    return 0;

}