最短路

一、单源最短路径

1.朴素Dijkstra算法

#include <bits/stdc++.h>

using namespace std;
const int N = 1000;

//数组g为邻接矩阵用于存放权值, 数组dis[i]表示起点到节点i的距离, n代表点的个数, m代表边的个数
int g[N][N], dis[N], n, m;                              
bool v[N];                                              //数组v用于标记起点到当前节点的最短路径是否已经确定

void dijkstra() {
    memset(dis, 0x3f, sizeof dis);                      //初始化起点到各个点的距离为 +∞
    memset(v, 0, sizeof v);                             //初始化起点到各个点的最短距离为未确定状态
    dis[1] = 0;                                         //起点到起点的距离设为0

    for (int i = 1; i < n; i++) {
        int x = 0;                                      //x表示起点到当前节点为最短距离的那个节点的下标
        //找到未标记节点中dis最小的
        for (int j = 1; j <= n; j++)
            if (!v[j] && (x == 0 || dis[j] < dis[x]))   //节点j未标记并且起点到节点j的距离比到节点x的距离短
                x = j;                                  //更新一下代表最短路径的节点下标
        v[x] = 1;                                       //起点到当前的节点x的距离必定最短了, 所以可以标记节点x了
        for (int y = 1; y <= n; y++)                    //这次是遍历节点x的所有出边即x-->y 
            dis[y] = min(dis[y], dis[x] + g[x][y]);     //由于起点到节点x的最短距离已经确定, 那么起点到节点y的距离根据左式更新一下就ok
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= n; i++) g[i][i] = 0;           //去除自环
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z);                      //构建邻接矩阵
    }
    dijkstra();
    //这部分内容根据题意自己写

    return 0;
}

2.堆优化Dijkstra算法

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
typedef vector<ii> vii;
const int N = 10000;

int to[N], v[N], ne[N], h[N];
int dis[N];
int n, m, idx;
bool vv[N];                                     //上面有个v了又懒得写vis所以这里用vv代替(懒癌晚期)

//链式前向星加边
void add(int x, int y, int z) {
    to[++idx] = y, v[idx] = z;
    ne[idx] = h[x], h[x] = idx;
}

void dijkstra() {
    memset(dis, 0x3f, sizeof dis);              //初始化起点到各个点的距离为 +∞
    memset(vv, 0, sizeof vv);                   //初始化起点到各个点的最短距离为未确定状态
    dis[1] = 0;                                 //起点到起点的距离设为0

    //初始化小根堆, 第一个元素为距离, 第二个元素为节点编号,队内升序排列确保队头元素最小
    priority_queue<ii, vii, greater<ii>> q;     
    q.push({0, 1});                             //起点入队

    while (q.size()) {
        int x = q.top().second; q.pop();        //取出当前队头的下标, 记得还要弹出队头
        if(vv[x]) continue;                     //对于已经确定的最短路径则跳过
        vv[x] = 1;                              //将节点x标记一下

        //链式前向星遍历x的出边, 即x-->y
        for(int i = h[x]; ~i; i = ne[i]){
            int y = to[i], z = v[i];
            if(dis[y] > dis[x] + z){
                dis[y] = dis[x] + z;
                q.push({dis[y],y});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    idx = 0;
    memset(h, -1, sizeof h);  //初始化,h为-1则代表指向空

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    dijkstra();
    //这个部分根据题意自己写
    return 0;
}

3.Bellman-Ford算法

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
const int N = 1000, M = 1000;
int n, m, idx;
int dis[N];

//直接用结构体存图
struct edge {
    int x, y, z;
} e[M];

void Bellman_Ford() {
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            int x = e[j].x, y = e[j].y, z = e[j].z;
            if (dis[y] > dis[x] + z)  //利用三角不等式进行松弛操作
                dis[y] = dis[x] + z;
        }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    idx = 0;
    for (int i = 0; i < m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        //双向连边
        e[++idx].x = x, e[idx].y = y, e[idx].z = z;
        e[++idx].x = y, e[idx].y = x, e[idx].z = z;
    }
    Bellman_Ford();

    //一般这一块写判负环操作, 存在负环则输出-1, 否则输出起点到节点n的最短路径
    if (dis[n] > INF / 2) cout << -1 << endl;
    else cout << dis[n] << endl;

    return 0;
}

4.SPFA算法

4.1 常规SPFA

#include <bits/stdc++.h>

using namespace std;
const int N = 1000, M = 10000;

int to[N], v[N], ne[N], h[N];
int dis[N];
int n, m, idx;
bool vv[N];                                             //数组vv用于标识当前节点是否在队列中, 避免重复处理
queue<int> q;

//链式前向星加边
void add(int x, int y, int z) {
    to[++idx] = y, v[idx] = z;
    ne[idx] = h[x], h[x] = idx;
}

void SPFA() {
    memset(dis, 0x3f, sizeof dis);
    memset(vv, 0, sizeof vv);
    dis[1] = 0; 
    
    q.push(1); vv[1] = 1;
    while (q.size()) {
        int x = q.front(); q.pop();
        vv[x] = 0;                                      //出队后对应的节点标记为不在队列中
        //链式前向星遍历x的出边, 即x-->y
        for (int i = h[x]; ~i; i = ne[i]) {
            int y = to[i], z = v[i];
            if (dis[y] > dis[x] + z) {
                dis[y] = dis[x] + z;
                if (!vv[y]) q.push(y), vv[y] = 1;       //如果y不在队列中则加入
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    idx = 0;
    memset(h, -1, sizeof h);                            //使用链式前向星一定要记得初始化

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    SPFA();
    //这个部分根据题目自己写

    return 0;
}

4.2 SPFA判负环

#include <bits/stdc++.h>

using namespace std;
const int N = 1000, M = 10000;

int to[N], v[N], ne[N], h[N];
int dis[N], cnt[N];                                 //数组cnt用于存储起点到当前节点一共有多少条边
int n, m, idx;
bool vv[N];                                         //数组vv用于标识当前节点是否在队列中, 避免重复处理
queue<int> q;

//链式前向星加边
void add(int x, int y, int z) {
    to[++idx] = y, v[idx] = z;
    ne[idx] = h[x], h[x] = idx;
}

bool SPFA() {
    //这里不能只把起点加入队列了,有可能从起点到不了存在负环的地方, 所以要把所有的点都加入队列
    for(int i = 1;i <= n; i++){
        q.push(i);
        vv[i] = 1;
    }
    
    while (q.size()) {
        int x = q.front(); q.pop();
        vv[x] = 0;                                  //出队后对应的节点标记为不在队列中

        //链式前向星遍历x的出边, 即x-->y
        for (int i = h[x]; ~i; i = ne[i]) {
            int y = to[i], z = v[i];
            if (dis[y] > dis[x] + z) {
                dis[y] = dis[x] + z;
                cnt[y] = cnt[x] + 1;                //更新一下边数
                //根据抽屉原理, 如果起点到节点y一共经过了n条边则路径上必然有n+1个点, 说明存在负环
                if(cnt[y] >= n) return true;        
                if (!vv[y]) q.push(y), vv[y] = 1;   //如果y不在队列中则加入
            }
        }
    }
    return false;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    idx = 0;
    memset(h, -1, sizeof h);                        //使用链式前向星一定要记得初始化

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }

    //判负环
    if(SPFA()) cout<<"YES"<<endl;
    else cout<<"NO"<<endl;

    return 0;
}

二、All-Pairs最短路径问题

Floyd-Warshall算法

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f

using namespace std;
const int N = 1000;

int n, m;                                                   //n个点, m条边
int g[N][N];                                                //数组g为邻接矩阵用于存储边权

void floyd() {
    for (int k = 1; k <= n; k++)                            //k是决策阶段, 所以一定要写在最外层, i和j为附加状态写在内层
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);  
//上式本质上是动态规划, 原本式子为g[k][i][j] = min(g[k-1][i][j], g[k-1][i][k] + g[k-1][k][j]);
//但是k-1这个决策阶段对k是无影响的, 所以忽略决策阶段降为2维, 一个是从i直接到j, 另一个是从i到k再到j, 取两者最小值即得状态方程
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= n; i++) g[i][i] = 0;               //去除自环
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        g[x][y] = min(g[x][y], z);                          //构建邻接矩阵
    }
    floyd();
    //这个部分根据题意自己填

    return 0;
}
posted @ 2021-02-28 14:59  Xiezeju  阅读(62)  评论(0)    收藏  举报