1. 程式人生 > 實用技巧 >Ekt 相關知識點

Ekt 相關知識點

目錄

最短路

1. 演算法分析

1.1 圖論最短/長路模型

  1. 單源最短/長路問題

    • dijkstra樸素版本求最短路 (O(n^2^)):適合稠密圖,用鄰接矩陣儲存,不能處理有負權邊情況
    • dijkstra堆優化版本求最短路 (O(mlogn)):與邊數有關,適合稀疏圖,使用鄰接表儲存,不能處理有負權邊情況
    • dijkstra雙端佇列優化版本求最短路 O(m+n):邊權為01特殊情況
    • dijkstra有條件約束求最短路 (O(mlogn)):補充一個數組作為約束條件
    • bellman_ford求最短路 (O(nm)):可以處理最多經過k條邊的題目,可以處理負權邊,但因為時間複雜度較高,一般被spfa替代
    • spfa求最短路 O(km),最壞複雜度可以到O(nm),可被網格圖卡,可以處理負權邊
    • spfa求最長路:最短路變化版本
    • bfs最短路模型求最短路 O(m + n): 只能處理邊權為1的情況
    • 拓撲排序模型求最短路 O(m+n):必須為DAG
  2. 多源最短路問題

    • floyd最短路問題 O(n^3)
    • floyd傳遞閉包問題 O(n^3)
    • floyd找有向/無向最小環 O(n^3)
    • floyd找恰好經過k條的最短距離
  3. 最短路計數模型

  4. 次短路模型與k短路模型

1.2 圖論建模技巧

  1. 把多源轉化為單源: 建立一個虛擬源點,該虛擬源點向每個點建立一條權值為0的邊
  2. dp轉化最短路求解:如果使用dp求解存在環,那麼必須轉化為最短路問題求解
  3. 最短路轉換為dp求解:如果能夠得到一張圖的拓撲序,沿著拓撲序dp更新即可得到最短路/最長路
  4. 分層建圖
  5. 二分圖建圖優化建圖
        對於一個二分圖,左邊的每個點都需要向右邊每個點連一條邊的建圖模型來說,可以設定一個虛擬節點,然後使得左邊每個點連向虛擬節點,虛擬節點再向右邊每個點連邊。這樣就把O(n ^ 2)優化到O(n)。

特點備註

  1. 樸素版dijkstra和雙端佇列bfs、堆優化版dijkstra需要st陣列來標記,這個陣列標記的是某個點是否使用過;spfa也需要st陣列來標記,不過標記的是這個點是否在佇列裡面;
  2. dijkstra、bfs不能處理負權邊。spfa、bellman-ford、floyd可以
  3. bfs求最短路、dijkstra求最短路對於每個點來說,具有拓撲序;spfa求最短路對於點來說,不具有拓撲序。(spfa每個點出入佇列多次,而dijkstra、bfs只出入隊一次)

2. 板子

2.1 dijkstra樸素版本求最短路 (O(n2)):適合稠密圖,用鄰接矩陣儲存,不能處理有負權邊情況

#include <bits/stdc++.h>

using namespace std;

int const N = 510;
int mp[N][N], n, m, dis[N], st[N];  // mp維護兩個點之間的距離,dis維護每個點到源點的距離,st[i]表示i點是否在S集合內(i點是否更新過),如果更新過那麼st[i]=1

// dijkstra求最短路
int dijkstra() {
    memset(dis, 0x3f, sizeof dis);  // 初始每個點到源點距離都是無窮遠
    dis[1] = 0;  // 把源點加入S集合
    for (int i = 1; i < n; ++i) {  // 需要進行n-1個回合,每個回合選出一個到源點最近的點,然後更新其他點到源點的距離
        int t = -1;  // 到源點最短距離的點為t
        for (int j = 1; j <= n; ++j) // 遍歷n個點
            if (!st[j] && (t == -1 || dis[j] < dis[t])) t = j;  // 第一次找到的就是源點
        st[t] = 1;  // 放入S集合
        for (int j = 1; j <= n; ++j) dis[j] = min(dis[j], dis[t] + mp[t][j]);  // 更新其他點到源點的距離,dijkstra保證了每次放入S集合的點是所有點中到源點距離最近的點
    }
    return dis[n] != 0x3f3f3f3f? dis[n]: -1;  // 需要特判無解的情況
}

int main() {
    cin >> n >> m;
    memset(mp, 0x3f, sizeof mp);  // 初始更新每個點間距離為無窮遠
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d%d%d", &a, &b, &c);
        mp[a][b] = min(mp[a][b], c);  // 防止重邊,取最小
    }
    cout << dijkstra();
    return 0;
}

2.2 dijkstra堆優化版本求最短路 (O(mlogn)):與邊數有關,適合稀疏圖,使用鄰接表儲存,不能處理有負權邊情況

#include<bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;
int const N = 2e5 + 10;
int e[N], ne[N],w[N], h[N], idx, n, m, dis[N], st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 堆優化版dijksyta
int dijkstra() {
    memset(dis, 0x3f, sizeof dis);  // 初始化距離為無窮
    priority_queue<PII, vector<PII>, greater<PII> > q;  // 定義一個按照距離從小到大排序的優先佇列,第一維:距離,第二維:點
    dis[1] = 0;  // 一開始源點距離為0
    q.push({0, 1});  // 把源點資訊放入佇列
    while (q.size()) {  // 每個點只出入佇列一次
        auto t = q.top();
        q.pop();
        
        int distance = t.first, ver = t.second;  // 最小距離和相對應的點
        if (st[ver]) continue;  // 這個操作保證每個點只出入隊一次,因為佇列裡面可能會出現{dis1[3], 3}, {dis2[3], 3}的情況,這樣保證dis1[3]<dis2[3]時,3號點只進出入隊一次
        st[ver] = 1;  // 標記,因為dijkstra的貪心策略保證每個點只需要進出隊一次
        
        for (int i = h[ver]; ~i; i = ne[i]) {  // 遍歷ver的鄰接點
            int j = e[i];
            if (dis[j] > distance + w[i]) {
                dis[j] = distance + w[i];
                q.push({dis[j], j});  // 這裡不需要判斷st,因為一旦更新發現更小必須放入佇列
            }
        }
    }
    return dis[n] != 0x3f3f3f3f? dis[n]: -1;
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);  
    for (int i = 1, a, b, c; i <= m; ++i) {  // 讀入m條邊
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    cout << dijkstra();
    return 0;
}

2.3 dijkstra雙端佇列優化版本求最短路 O(m+n):邊權為01特殊情況

#include <bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;
int const N = 110;
int e[N * N], ne[N * N], h[N], w[N * N], idx, n, S, E, dis[N], st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 雙端佇列優化dijkstra
int dijkstra() {
    memset(dis, 0x3f, sizeof dis);  // 初始化距離為無窮
    deque<PII> q;  // 定義一個雙端佇列
    dis[S] = 0;  // 一開始源點距離為0
    q.push_back({0, S});  // 把源點資訊放入佇列
    while (q.size()) {  // 每個點只出入佇列一次
        auto t = q.front();
        q.pop_front();
        
        int distance = t.first, ver = t.second;  // 最小距離和相對應的點
        if (st[ver]) continue;  // 這個操作保證每個點只出入隊一次,因為佇列裡面可能會出現{dis1[3], 3}, {dis2[3], 3}的情況,這樣保證dis1[3]<dis2[3]時,3號點只進出入隊一次
        st[ver] = 1;  // 標記,因為dijkstra的貪心策略保證每個點只需要進出隊一次
        
        for (int i = h[ver]; ~i; i = ne[i]) {  // 遍歷ver的鄰接點
            int j = e[i];
            if (dis[j] > distance + w[i]) {  // 如果能夠更新
                dis[j] = distance + w[i];
                if (w[i] == 1) q.push_back({dis[j], j});  // 如果邊權為1,那麼插入隊尾
                else q.push_front({dis[j], j});  // 如果邊權為0,那麼插入隊頭
            }
        }
    }
    return dis[E] != 0x3f3f3f3f? dis[E]: -1;
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> S >> E;
    
    // 建圖
    for (int i = 1, k; i <= n; ++i) {  
        scanf("%d", &k);
        for (int j = 1, t; j <= k; ++j) {  // 讀入相鄰的點
            scanf("%d", &t);
            add(i, t, (j == 1? 0: 1));  // 如果是第1個點,那麼邊權為0,其他店點的邊權為1
        }
    }
    cout << dijkstra();
    return 0;
}

2.4 dijkstra有條件約束求最短路 (O(mlogn)):補充一個數組作為約束條件

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
int const N = 1100, M = 110;
LL const INF = 0x3f3f3f3f3f3f3f3f;
// dis[i][j]:走到i點限制條件為j時的最小距離,st[i][j]:在i點且限制條件為j是否走到過, w1:邊權,w2:限制條件
LL C[M], e[N * N], ne[N * N], w1[N * N], w2[N * N], idx, h[N], dis[N][110], st[N][110];  
struct NODE {
    LL point, distance, strict;  // point:點,distance:最小距離,strict:限制條件
    bool operator<(const NODE &w) const {  // 按照距離的排序,小的排在前面
        return distance > w.distance;
    }
}node[N];  // 放在優先佇列內的資料結構
struct POINT {  
    int id, x, y;
}point[N];  // 0~n-1為中間站點,n為起點,n+1為終點
struct PATH {
    int id1, id2, c;
};  // 記錄id1->id2的邊權需要乘上c
int sx, sy, ex, ey, B, m, n;  // (sx, sy)為起點, (ex, ey)為終點,B為限制條件,m為邊型別數目,n為站點數

void add(int a, int b, LL c1, LL c2) {  // c1為邊權,c2為限制條件
    e[idx] = b, w1[idx] = c1, w2[idx] = c2, ne[idx] = h[a], h[a] = idx++;
}

// 計算id1和id2之間的距離
LL getdis(int id1, int id2) {
    int x1 = point[id1].x, x2 = point[id2].x, y1 = point[id1].y, y2 = point[id2].y;
    return (LL)ceil(sqrt((x2 - x1) * 1ll * (x2 - x1) + (y2 - y1) * 1ll * (y2 - y1)));
}

// 有條件限制的dijkstra演算法
LL dijkstra() {
    priority_queue<NODE> q;  // 定義一個按照距離從小到大排序的優先佇列
    memset(dis, 0x3f, sizeof dis);  // 距離初始化
    q.push({n, 0, 0});  // 把起點放入佇列
    dis[n][0] = 0;  // 記錄源點的距離

    while (q.size()) {
        auto t = q.top();
        q.pop();
        
        LL ver = t.point, distance = t.distance, strict = t.strict;  // 點、距離、限制條件
        if (st[ver][strict]) continue;  // 如果走過
        st[ver][strict] = 1;
        
        for (int i = h[ver]; ~i; i = ne[i]) {   // 遍歷ver的所有出邊
            int j = e[i];
            if (strict + w2[i] > B) continue;  // 如果超過限制條件,跳過
            if (dis[j][strict + w2[i]] > distance + w1[i]) {  // 如果能夠更新
                dis[j][strict + w2[i]] = distance + w1[i];  // 更新
                q.push({j, dis[j][strict + w2[i]], strict + w2[i]});  // 放入優先佇列
            }
        }
    }

    LL res = INF;  // 記錄到終點點的最小距離
    for (int i = 0; i <= B; ++i) res = min(res, dis[n + 1][i]);  // 遍歷每一個限制條件
    return res == INF? -1: res;
}

int main() {
    memset(h, -1, sizeof h);
    cin >> sx >> sy >> ex >> ey >> B >> C[0] >> m;  // 讀入起點、終點、限制條件、初始邊型別引數、邊型別數目
    for (int i = 1; i <= m; ++i) scanf("%lld", &C[i]);  // 讀入不同型別的邊引數
    cin >> n;
    vector<PATH> path;  // 記錄所有的連邊
    for (int i = 0, t; i < n; ++i) {
        scanf("%d%d", &point[i].x, &point[i].y);
        point[i].id = i;
        cin >> t;
        for (int j = 1, obj, c; j <= t; ++j) {
            scanf("%d%d", &obj, &c);
            path.push_back({i, obj, c});
        }
    }
    
    // 記錄起點和終點
    point[n].x = sx, point[n].y = sy, point[n + 1].x = ex, point[n + 1].y = ey;
    point[n].id = n, point[n + 1].id = n + 1;
    
    // 起點和終點能夠連到所有的站點
    for (auto p: path) {
        add(p.id1, p.id2, C[p.c] * (LL)getdis(p.id1, p.id2), (LL)getdis(p.id1, p.id2));
        add(p.id2, p.id1, C[p.c] * (LL)getdis(p.id1, p.id2), (LL)getdis(p.id1, p.id2));
    }
    for (int i = 0; i < n; ++i) {
        add(n, i, C[0] * (LL)getdis(n, i), (LL)getdis(n, i));
        add(i, n, C[0] * (LL)getdis(n, i), (LL)getdis(n, i));
        add(n + 1, i, C[0] * (LL)getdis(n + 1, i), (LL)getdis(n + 1, i));
        add(i, n + 1, C[0] * (LL)getdis(n + 1, i), (LL)getdis(n + 1, i));
    }
    add(n, n + 1, C[0] * (LL)getdis(n, n + 1), (LL)getdis(n, n + 1));
    add(n + 1, n, C[0] * (LL)getdis(n, n + 1), (LL)getdis(n, n + 1));
        
    // 計算有條件的dijkstra演算法
    cout << dijkstra();
    return 0;
}

2.5 bellman_ford求最短路 (O(nm)):可以處理最多經過k條邊的題目,可以處理負權邊,但因為時間複雜度較高,一般被spfa替代

#include <bits/stdc++.h>

using namespace std;

int const N = 510, M = 10010, INF = 0x3f3f3f3f;
struct EDGE {
    int a, b, c;
}edge[M];
int n, m, k, dis[N], backup[N];

// bellman-ford求有邊數限制的最短路
void bellman_ford() {
    memset(dis, 0x3f, sizeof dis);  // 初始化為無窮
    dis[1] = 0;  // 源點初始化
    for (int i = 1; i <= k; ++i) {  // 每次迴圈起碼得到一個正確的dis
        memcpy(backup, dis, sizeof dis);  // 備份,防止拿本回合更新過的更新其他邊
        for (int j = 1; j <= m; ++j) {  // 列舉所有的邊
            int a = edge[j].a, b = edge[j].b, c = edge[j].c;  
            dis[b] = min(backup[a] + c, dis[b]);  // 更新
        }
    }
    if (dis[n] > INF / 2) cout << "impossible";  // 防止出現負權邊情況
    else cout << dis[n];
}

int main() {
    cin >> n >> m >> k;
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d %d %d", &a, &b, &c); 
        edge[i] = {a, b, c};  // 讀入單向邊
    }
    bellman_ford();  
    return 0;
}

2.6 spfa求最短路: O(km),最壞複雜度可以到O(nm),可被網格圖卡,可以處理負權邊

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10, INF = 0x3f3f3f3f;
int e[N], ne[N], w[N], h[N], idx, m, n, dis[N], st[N];  // dis[i]標識i點到源點的最短距離,st[i]=1表示i點在佇列裡

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// spfa求最短路
void spfa() {
    memset(dis, 0x3f, sizeof dis);  // 初始化最短路
    queue<int> q;  // 建立新佇列
    dis[1] = 0;  // 源點距離為0
    st[1] = 1;  // 標識源點在佇列裡了
    q.push(1);  // 源點入隊
    
    while (q.size()) {
        auto t = q.front();  
        q.pop();  // 隊首元素出隊
        st[t] = 0;  // 記錄出隊
        
        for (int i = h[t]; ~i; i = ne[i]) {  // 遍歷所有鄰接點
            int j = e[i];
            if (dis[j] > dis[t] + w[i]) {  // 如果距離能夠更新
                dis[j] = dis[t] + w[i];  // 更新距離
                if (!st[j]) {  // 如果j點不在佇列裡
                    q.push(j);  // 入隊
                    st[j] = 1;  // 記錄
                }
            }
        }
    }
    
    if (dis[n] == INF) cout << "impossible";
    else cout << dis[n];
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);  // 讀入單向邊
    }
    spfa();
    return 0;
}

2.7 spfa求最長路

// 與求最短路不同的是:
// 1. 初始化:memset(dis, 0xc0, sizeof dis);
// 2. 更新條件變成dis[j] < dis[t] + w[i]
#include <bits/stdc++.h>

using namespace std;

int const N = 1510, M = 5e4 + 10, INF = 0xc0c0c0c0;
int e[M], ne[M], w[M], h[N], idx, m, n, dis[N], st[N];  // dis[i]標識i點到源點的最短距離,st[i]=1表示i點在佇列裡

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// spfa求最長路
void spfa() {
    memset(dis, 0xc0, sizeof dis);  // 初始化最長路
    queue<int> q;  // 建立新佇列
    dis[1] = 0;  // 源點距離為0
    st[1] = 1;  // 標識源點在佇列裡了
    q.push(1);  // 源點入隊
    
    while (q.size()) {
        auto t = q.front();  
        q.pop();  // 隊首元素出隊
        st[t] = 0;  // 記錄出隊
        
        for (int i = h[t]; ~i; i = ne[i]) {  // 遍歷所有鄰接點
            int j = e[i];
            if (dis[j] < dis[t] + w[i]) {  // 如果距離能夠更新
                dis[j] = dis[t] + w[i];  // 更新距離
                if (!st[j]) {  // 如果j點不在佇列裡
                    q.push(j);  // 入隊
                    st[j] = 1;  // 記錄
                }
            }
        }
    }
    
    cout << (dis[n] == INF? -1: dis[n]);
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);  // 讀入單向邊
    }
    spfa();
    return 0;
}

2.8 bfs最短路模型求最短路: O(m + n): 只能處理邊權為1的情況

#include <bits/stdc++.h>

using namespace std;

int const N = 1e3 + 10;
int a[N][N], st[N][N], n;
int pre[N * N];
queue<pair<int, int> >q;
vector<pair<int, int> > path;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

void bfs(pair<int, int> start) {
    q.push(start);
    st[start.first][start.second] = 1;
    
    while (q.size()) {
        auto t = q.front();
        q.pop();
        
        for (int i = 0; i < 4; ++i) {
            int x = t.first + dx[i], y = t.second + dy[i];
            if (x < 0 || x > n - 1 || y < 0 || y > n - 1) continue;
            if (st[x][y] || a[x][y] == 1) continue;
            
            pre[x * n + y] = t.first * n + t.second;
            if (x == n - 1 && y == n - 1) return;
            
            st[x][y] = 1;
            q.push({x, y});
        }
    }
}

pair<int, int> get_pos(int x) {
    pair<int, int> res;
    res.first = x / n;
    res.second = x % n;
    return res;
}

int main() {
    cin >> n;
    for (int i = 0; i < n; ++i) 
        for (int j = 0; j < n; ++j)
            cin >> a[i][j];
    
    bfs({0, 0});
    int cur = (n - 1) * n + n - 1;
    while (1) {
        path.push_back(get_pos(cur));
        cur = pre[cur];
        if (cur == 0) break;
    }
    path.push_back({0, 0});
    reverse(path.begin(), path.end());
    for (auto p: path) 
        cout << p.first << " " << p.second << endl;
    return 0;
}

2.9 拓撲排序模型求最短路: O(m+n):必須為DAG

#include<bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10, M = 2e6 + 10;
int n, m;
int e[M], ne[M], h[N], w[M], idx;
int din[N], st[N], dis[N];
vector<int> ans;

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void topsort() {
    queue<int> q;
    for (int i = 1; i <= n + m; ++i) if (!din[i]) q.push(i);

    while (q.size()) {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            din[j]--;
            if (!din[j]) q.push(j);
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        // 建圖(加入虛節點,平方->線性)
        memset(st, 0, sizeof st);
        int cnt, start = n, end = 1, ver = n + i;  // start記錄最小的點,end記錄最大的點,ver記錄虛擬節點
        cin >> cnt;
        for (int i = 0; i < cnt; ++i) {
            int t;
            cin >> t;
            st[t] = 1;
            start = min(start, t);
            end = max(end, t);
        }

        // 把a->b的邊拆成a->ver和ver->b
        for (int j = start; j <= end; ++j) {
            if (st[j]) add(ver, j, 1), din[j]++;
            else add(j, ver, 0), din[ver]++;
        }
    }

    // 拓撲排序得到更新的順序
    topsort();

    // dp求最大值
    for (int i = 1; i <= n; ++i) dis[i] = 1;
    for (int i = 0; i < ans.size(); ++i) {
        int k = ans[i];
        for (int j = h[k]; ~j; j = ne[j]) {
            int t = e[j];
            dis[t] = max(dis[t], dis[k] + w[j]);
        }
    }

    int res = 0;
    for (int i = 1; i <= n; ++i) res = max(res, dis[i]);
    cout << res << endl;
    return 0;
}

2.10 floyd最短路問題 O(n^3)

#include <bits/stdc++.h>

using namespace std;

int const N = 210, INF = 0x3f3f3f3f;
int mp[N][N], n, m, k;

// floyd求最短路
void floyd() {
    for (int k = 1; k <= n; ++k)  // 列舉中間點
        for (int i = 1; i <= n; ++i)  // 列舉起點
            for (int j = 1; j <= n; ++j)  // 列舉終點
                mp[i][j] = min(mp[i][j], mp[i][k] + mp[k][j]);  // 更新i到j的距離
}

int main() {
    cin >> n >> m >> k;  // 點、邊、詢問次數
    
    // 初始化距離:初始化為正無窮
    for (int i = 1; i <= n; ++i) 
        for (int j = 1; j <= n; ++j) 
            mp[i][j] = (i == j? 0: INF);  
            
    // 讀入邊
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d%d%d", &a, &b, &c);
        mp[a][b] = min(mp[a][b], c);
    }
    
    // 求最短路
    floyd();
    
    // 詢問
    for (int i = 1, a, b; i <= k; ++i) {
        scanf("%d%d", &a, &b);
        if (mp[a][b] > INF / 2) cout << "impossible\n";  // 防止負權邊,所有用INF/2
        else cout << mp[a][b] << endl;  // 列印
    }
    
    return 0;
}

3. 典型例題

3.1 有等級差值的限制的最短路

acwing903. 昂貴的聘禮
題意:
年輕的探險家來到了一個印第安部落裡。在那裡他和酋長的女兒相愛了,於是便向酋長去求親。酋長要他用10000個金幣作為聘禮才答應把女兒嫁給他。探險家拿不出這麼多金幣,便請求酋長降低要求。酋長說:”嗯,如果你能夠替我弄到大祭司的皮襖,我可以只要8000金幣。如果你能夠弄來他的水晶球,那麼只要5000金幣就行了。”探險家就跑到大祭司那裡,向他要求皮襖或水晶球,大祭司要他用金幣來換,或者替他弄來其他的東西,他可以降低價格。探險家於是又跑到其他地方,其他人也提出了類似的要求,或者直接用金幣換,或者找到其他東西就可以降低價格。不過探險家沒必要用多樣東西去換一樣東西,因為不會得到更低的價格。探險家現在很需要你的幫忙,讓他用最少的金幣娶到自己的心上人。另外他要告訴你的是,在這個部落裡,等級觀念十分森嚴。地位差距超過一定限制的兩個人之間不會進行任何形式的直接接觸,包括交易。他是一個外來人,所以可以不受這些限制。
但是如果他和某個地位較低的人進行了交易,地位較高的的人不會再和他交易,他們認為這樣等於是間接接觸,反過來也一樣。因此你需要在考慮所有的情況以後給他提供一個最好的方案。為了方便起見,我們把所有的物品從1開始進行編號,酋長的允諾也看作一個物品,並且編號總是1。每個物品都有對應的價格P,主人的地位等級L,以及一系列的替代品Ti和該替代品所對應的”優惠”Vi。如果兩人地位等級差距超過了M,就不能”間接交易”。你必須根據這些資料來計算出探險家最少需要多少金幣才能娶到酋長的女兒。
1≤N≤100,1≤P≤10000,1≤L,M≤N,0≤X<N
題解: 本題要求1~n的路徑上有等級差值的限制的最短路。 建立虛擬源點,然後做dijkstra。為了解決等級限制,就暴力去列舉能夠使用的等級區間,dijkstra要滿足等級區間限制才能進行轉移。
程式碼:

#include <bits/stdc++.h>

using namespace std;

const int N = 110, INF = 0x3f3f3f3f;

int n, m;
int w[N][N], level[N];
int dist[N];
bool st[N];

// dijkstra時要有等級的限制
int dijkstra(int down, int up) {
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);

    dist[0] = 0;
    for (int i = 1; i <= n + 1; i ++ ) {
        int t = -1;
        for (int j = 0; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                 t = j;

        st[t] = true;
        for (int j = 1; j <= n; j ++ )
            if (level[j] >= down && level[j] <= up)  // 滿足上下界才能轉移
                dist[j] = min(dist[j], dist[t] + w[t][j]);
    }

    return dist[1];
}

int main() {
    cin >> m >> n;

    memset(w, 0x3f, sizeof w);
    for (int i = 1; i <= n; i ++ ) w[i][i] = 0;

    for (int i = 1; i <= n; i ++ ) {
        int price, cnt;
        cin >> price >> level[i] >> cnt;
        w[0][i] = min(price, w[0][i]);
        while (cnt -- ) {
            int id, cost;
            cin >> id >> cost;
            w[id][i] = min(w[id][i], cost);
        }
    }

    int res = INF;
    // 列舉和1號點相關的等級區間
    for (int i = level[1] - m; i <= level[1]; i ++ ) res = min(res, dijkstra(i, i + m));  

    cout << res << endl;

    return 0;
}

3.2 有列舉順序的最短路

acwing1135.新年好
題意: 重慶城裡有 n 個車站,m 條 雙向 公路連線其中的某些車站。每兩個車站最多用一條公路連線,從任何一個車站出發都可以經過一條或者多條公路到達其他車站,但不同的路徑需要花費的時間可能不同。在一條路徑上花費的時間等於路徑上所有公路需要的時間之和。佳佳的家在車站 1,他有五個親戚,分別住在車站 a,b,c,d,e。過年了,他需要從自己的家出發,拜訪每個親戚(順序任意),給他們送去節日的祝福。怎樣走,才需要最少的時間?
\(1≤n≤50000,1≤m≤10^5,1<a,b,c,d,e≤n,1≤x,y≤n,1≤t≤100\)
題解: 本題要求找到一條路徑,該路徑能夠穿過給定的有限個點,找到這樣一條最短路。 分別以5個點為起點,各做dijkstra,這樣就能夠得到這5個點到其他店的最短路。然後dfs列舉拜訪的順序即可
程式碼:

#include <bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;
int const N = 2e5 + 10;
int e[N], ne[N], w[N], h[N], idx;
int n, m;
int dis[6][N];
int st[6];
int source[6];
int q[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dijkstra(int s, int dist[]) {
    memset(dist, 0x3f, N * 4);
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, s});
    dist[s] = 1;
    while (heap.size()) {
        auto t = heap.top();
        heap.pop();
        int distance = t.first, ver = t.second;
        for (int i = h[ver]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > distance + w[i]) {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }
}

int dfs(int u, int start, int distance) {
    if (u > 5) return distance;
    int res = 0x3f3f3f3f;
    for (int i = 1; i <= 5; ++i) {
        if (!st[i]) {
            st[i] = 1 ;
            res = min(res , dfs(u + 1, i, distance + dis[start][source[i]]));
            st[i] = 0;
        }
    }
    return res;
}

int main() {
    cin >> n >> m;
    source[0] = 1;
    for (int i = 1; i <= 5; ++i) cin >> source[i];
    memset(h, -1, sizeof h);
    for (int i = 1; i <= m; ++i) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    for (int i = 0; i <= 5; ++i)
        dijkstra(source[i], dis[i]);

    printf("%d\n", dfs(1, 0, 0));
    return 0;
}

3.3 找出第k+1大的邊權值

acwing340. 通訊線路
題意: 在郊區有 N 座通訊基站,P 條 雙向 電纜,第 i 條電纜連線基站Ai和Bi。特別地,1 號基站是通訊公司的總站,N 號基站位於一座農場中。現在,農場主希望對通訊線路進行升級,其中升級第 i 條電纜需要花費Li。電話公司正在舉行優惠活動。農產主可以指定一條從 1 號基站到 N 號基站的路徑,並指定路徑上不超過 K 條電纜,由電話公司免費提供升級服務。農場主只需要支付在該路徑上剩餘的電纜中,升級價格最貴的那條電纜的花費即可。求至少用多少錢可以完成升級。
題解: 找1~n上第k+1大的路徑。 二分檢查答案,然後每次把比答案大的邊當成1,比答案小的邊當成0,做dijkstra,check的條件就是dis[n]和k的關係。
程式碼:

#include<bits/stdc++.h>
 
using namespace std;

int const N = 2e4 + 10;
int e[N], ne[N], w[N], idx, h[N];
int k, n, p;
int dis[N];
int st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool check(int bound) {
    memset(dis, 0x3f, sizeof dis);
    memset(st, 0, sizeof st);
    dis[1] = 0;
    deque<int> q;
    q.push_front(1);
    while (q.size()) {
        auto t = q.front();
        q.pop_front();
        if (st[t]) continue;
        st[t] = 1;
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            int len = w[i] > bound;
            if (dis[j] > dis[t] + len) {
                dis[j] = dis[t] + len;
                if (len) q.push_back(j);
                else q.push_front(j);
            }
        }
    }
    return dis[n] <= k;
}
 
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> p >> k;
    for (int i = 1; i <= p; ++i) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    int l = 0, r = 1e6 + 1;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    if (l == 1e6 + 1) cout << -1 << endl;
    else cout << l << endl;
    return 0;
}

3.4 存在有向邊和無向邊的圖找最短路

acwing342. 道路與航線
題意: 農夫約翰正在一個新的銷售區域對他的牛奶銷售方案進行調查。他想把牛奶送到T個城鎮,編號為1~T。這些城鎮之間通過R條道路 (編號為1到R) 和P條航線 (編號為1到P) 連線。每條道路 i 或者航線 i 連線城鎮Ai到Bi,花費為Ci。對於道路,0≤Ci≤10,000;然而航線的花費很神奇,花費Ci可能是負數(−10,000≤Ci≤10,000)。道路是雙向的,可以從Ai到Bi,也可以從Bi到Ai,花費都是Ci。然而航線與之不同,只可以從Ai到Bi。事實上,由於最近恐怖主義太囂張,為了社會和諧,出臺了一些政策:保證如果有一條航線可以從Ai到Bi,那麼保證不可能通過一些道路和航線從Bi回到Ai。由於約翰的奶牛世界公認十分給力,他需要運送奶牛到每一個城鎮。他想找到從傳送中心城鎮S把奶牛送到每個城鎮的最便宜的方案。\(1≤T≤25000,1≤R,P≤50000,1≤Ai,Bi,S≤T\)
題解: 本題要在一張既有有向邊又有無向邊的圖上求出每個點到源點的最短距離,保證有向邊不會形成有向環。 先把無向邊建的圖分成很多連通塊,縮成超級點。再加上有向邊後,整張圖就變成一張dag。然後拓撲排序求最短路,當處理超級點時,用堆優化版dijkstra處理,dijkstra需要新增上一些特判的邏輯,具體見程式碼。
程式碼:

#include<bits/stdc++.h>
 
using namespace std;

typedef pair<int, int> PII;
int const N = 3e4 + 10, M = 2e5 + 10;
int e[M], ne[M], w[M], idx, h[M];
int t, r, p, source;
int id[N];
vector<int> block[N];
int bcnt;
int st[N];
int dis[N];
queue<int> q;
int din[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 按照連通塊縮點
void dfs(int u, int cnt) {
    id[u] = cnt;
    block[cnt].push_back(u);
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!id[j]) dfs(j, cnt);
    }
}

// 堆優化版dijkstra演算法求連通塊內的最短路
void dijkstra(int bid) { 
    priority_queue<PII, vector<PII>, greater<PII> >heap;
    for (auto b: block[bid]) heap.push({dis[b], b});
    while (heap.size()) {
        auto t = heap.top();
        heap.pop();
        int distance = t.first, ver = t.second;
        if (st[ver]) continue;
        st[ver] = 1;
        for (int i = h[ver]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dis[j] > distance + w[i])
            {
                dis[j] = distance + w[i];
                if (id[ver] == id[j]) heap.push({dis[j], j});  // 如果是一個超級點內才能更新
            }
            if (id[ver] != id[j])  // 不在一個超級點內,那麼需要按照拓撲排序邏輯更新
            {
                din[id[j]]--;
                if (!din[id[j]]) q.push(id[j]);
            }
        }
    }
}

// 拓撲排序求最短路
void top_sort() {
    for (int i = 1; i <= bcnt; ++i)
        if (!din[i]) q.push(i);
    memset(dis, 0x3f, sizeof dis);
    memset(st, 0, sizeof st);
    dis[source] = 0;
    while (q.size()) {
        auto t = q.front();
        q.pop();
        dijkstra(t);
    }
}
 
int main() {
    memset(h, -1, sizeof h);
    cin >> t >> r >> p >> source;
    for (int i = 1; i <= r; ++i) {  // 讀入無向邊
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }
    for (int i = 1; i <= t; ++i) {  // 把所有點按照連通塊劃分為超級點
        if (!id[i]) dfs(i, ++bcnt);
    }
    for (int i = 1; i <= p; ++i) {  // 讀入有向邊
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
        din[id[b]]++;
    }
    top_sort();  // 拓撲排序求最短路
    for (int i = 1; i <= t; ++i) {  // 判斷是否有最短路
        if (dis[i] > 0x3f3f3f3f / 2) cout << "NO PATH\n";
        else cout << dis[i] << endl;
    }
    return 0;
}

acwing341. 最優貿易
題意: C國有 n 個大城市和 m 條道路,每條道路連線這 n 個城市中的某兩個城市。任意兩個城市之間最多隻有一條道路直接相連。這 m 條道路中有一部分為單向通行的道路,一部分為雙向通行的道路,雙向通行的道路在統計條數時也計為1條。C國幅員遼闊,各地的資源分佈情況各不相同,這就導致了同一種商品在不同城市的價格不一定相同。但是,同一種商品在同一個城市的買入價和賣出價始終是相同的。商人阿龍來到C國旅遊。當他得知“同一種商品在不同城市的價格可能會不同”這一資訊之後,便決定在旅遊的同時,利用商品在不同城市中的差價賺一點旅費。設C國 n 個城市的標號從 1~n,阿龍決定從1號城市出發,並最終在 n 號城市結束自己的旅行。在旅遊的過程中,任何城市可以被重複經過多次,但不要求經過所有 n 個城市。阿龍通過這樣的貿易方式賺取旅費:他會選擇一個經過的城市買入他最喜歡的商品——水晶球,並在之後經過的另一個城市賣出這個水晶球,用賺取的差價當做旅費。因為阿龍主要是來C國旅遊,他決定這個貿易只進行最多一次,當然,在賺不到差價的情況下他就無需進行貿易。現在給出 n 個城市的水晶球價格,m 條道路的資訊(每條道路所連線的兩個城市的編號以及該條道路的通行情況)。請你告訴阿龍,他最多能賺取多少旅費。\(1≤n≤100000,1≤m≤500000,1≤各城市水晶球價格≤100\)
題解: 本題要在一張既有有向邊又有無向邊的圖上,找出兩個點i和j(i和j均在1~n的路徑上),使得\(w[j]-w[i]\)最大,其中i在的前面出現,有向邊可能形成有向環。 本題要找1 ~ n上最大的點和最小的點。考慮dp求解,維護陣列f1[i]表示i點開始到n點的最大價值,然後去列舉i為買的點,答案即為max{val[i]-f1[i]}。但是本題可能存在環,因此考慮tarjan演算法縮點變成dag(有向邊的環縮點,無向邊的聯通塊也會縮點)。本題的難點在於要求當前的點i必須是從1開始,到n結束。從1開始好處理,tarjan演算法的時候只做1為起點的tarjan,這樣求出來的都是從1開始的。而到n結束就必須維護一個數組f2[i]表示i是否能夠到n點,f2[i]為1表示i能夠到n點,為0表示不能到n點,每次都必須更新f2。維護陣列f1[i]表示i點開始到n點的最大價值,當且僅當f2[i]=1才能轉移,f1[i]=max[max{f1[u]}, val[i]]
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;

int const N = 2e5 + 10, M = 2e6 + 10;
// dfn記錄每個點的時間戳,low記錄每個點的回溯值,scc[i]=x表示i在標號為x的強連通分量裡,stk維護一個棧,sccnum記錄強連通分量的個數
int dfn[N], low[N], scc[N], stk[N], sccnum, top, timestamp;  
int h1[N], e[M], ne[M], idx, h2[N], w[N];
int n, m;
int f1[N], f2[N]; //f1[i]表示i點開始到n點的最大價值, f2[i]為1表示i能夠到n點,為0表示不能到n點
PII scc_count[N];  // first為max,second為min

// a->b有一條邊
void add(int a, int b, int h[])
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}   

// tarjan演算法求強連通分量
void tarjan(int root, int h[])
{
    if (dfn[root]) return;  // 時間戳為0,返回
    dfn[root] = low[root] = ++timestamp;  // 記錄當前點的時間戳和回溯值,初始化二者相同,而後dfn[root]>=low[root]
    stk[++top] = root;  // 把根放入棧內
    for (int i = h[root]; i != -1; i = ne[i])  // 遍歷每一個與根節點相鄰的點
    {
        int j = e[i];  // 與i相鄰的點為j
        if (!dfn[j])  // j點沒有訪問過
        {
            tarjan(j, h);  // 繼續dfs,得到所有以j點為根的子樹內所有的low和dfn
            low[root] = min(low[root], low[j]);  // 根的low是其子樹中low最小的那個
        }
        else if (!scc[j])  // 如果j這個點還在棧內(在棧內的話不屬於任何一個scc),同時一個棧內的點在一個scc內
        {
            low[root] = min(low[root], dfn[j]);  // low代表所能到達的最小的時間戳
        }
    }
    
    // 如果root的後代不能找到更淺的節點(更小的時間戳)
    if (low[root] == dfn[root])  // 只有某個強連通分量的根節點的low和dfn才會相同
    {
        sccnum++;
        scc_count[sccnum].first = -1;  // 計算最大值和最小值
        scc_count[sccnum].second = 1e9;
        while (1)  // 出棧直到等於root
        {
            int x = stk[top--];
            if (x == n) f2[sccnum] = 1;
            scc[x] = sccnum;
            scc_count[sccnum].first = max(scc_count[sccnum].first, w[x]);
            scc_count[sccnum].second = min(scc_count[sccnum].second, w[x]);
            if (x == root) break;
        }
    }
}

int main()
{
    cin >> n >> m;
    memset(h1, -1, sizeof h1);
    memset(h2, -1, sizeof h2);
    memset(f1, -1, sizeof f1);
    
    for (int i = 1; i <= n; ++i) scanf("%d", &w[i]);
    for (int i = 1, a, b, t; i <= m; ++i)
    {
        scanf("%d %d %d", &a, &b, &t);
        add(a, b, h1);
        if (t == 2) add(b, a, h1);
    }

    // tarjan求scc
    tarjan(1, h1);  // 這樣保證後面縮點的所有點都是從1開始
    
    // 縮點
    for (int i = 1; i <= n; ++i)
        for (int j = h1[i]; j != -1; j = ne[j])
        {
            int k = e[j];
            if (scc[i] != scc[k] && scc[i] && scc[k]) add(scc[i], scc[k], h2);
        }
    
    // 反拓撲序做dp
    int ans = 0;
    for (int i = 1; i <= sccnum; ++i) {  
        int maxv = -1;
        for (int j = h2[i]; ~j; j = ne[j]) {
            int k = e[j];
            f2[i] |= f2[k];  // 更新i是否能夠到達終點的情況
            if (f2[k]) maxv = max(maxv, f1[k]);  // 更新能夠到達終點的最大值
        }
        if (f2[i]) {  // 只要f2為1才更新
            f1[i] = max(scc_count[i].first, maxv);
            ans = max(ans, f1[i] - scc_count[i].second);
        }
    }

    cout << ans;

    return 0;
}

3.5 有條件限制的最短路

題意: 瑞恩被關在N*M的迷宮裡。南北或東西方向相鄰的 2 個單元之間可能互通,也可能有一扇鎖著的門,或者是一堵不可逾越的牆。注意: 門可以從兩個方向穿過,即可以看成一條無向邊。迷宮中有一些單元存放著鑰匙,同一個單元可能存放 多把鑰匙,並且所有的門被分成 P 類,開啟同一類的門的鑰匙相同,不同類門的鑰匙不同。瑞恩被關押在 (N,M) 單元裡。從(1, 1)進入迷宮,從一個單元移動到另一個相鄰單元的時間為 1,拿取所在單元的鑰匙的時間以及用鑰匙開門的時間可忽略不計。問最少多少時間可以把瑞恩拯救出來。
\(|Xi1−Xi2|+|Yi1−Yi2|=1,0≤Gi≤P,1≤Qi≤P,1≤N,M,P≤10,1≤k≤150\)
題解: 本題是有條件限制的最短路,限制的條件為鑰匙。因此可以專門拿出一維來做限制條件,然後做最短路時要滿足限制條件才能轉移。
因此本題分以下4個步驟:
1.讀邊:如果是門的話記錄下
2.建圖:
3.讀鑰匙
4.做雙端bfs
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;
int const N = 4e2 + 10;
int e[N], ne[N], idx, w[N], h[N];
int cnt;
int n, m, p, k, s;
int dis[110][1 << 10], st[110][1 << 10];
int g[15][15], key[110];
set<PII> exist;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 建圖:找四周可以走的點
void build()
{
    int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
        {
            for (int u = 0; u < 4; ++u)
            {
                int x = i + dx[u], y = j + dy[u];
                if (x <= 0 || x > n || y <= 0 || y > m) continue;
                if (exist.count({g[i][j], g[x][y]})) continue;
                add(g[i][j], g[x][y], 0);
            }
        }
}

int bfs()
{
    memset(dis, 0x3f, sizeof dis);
    dis[1][0] = 0;

    deque<PII> q;
    q.push_back({1, 0});  // 第一維為點,第二維維擁有的鑰匙

    while (q.size())
    {
        PII t = q.front();
        q.pop_front();

        if (st[t.first][t.second]) continue;
        st[t.first][t.second] = true;

        if (t.first == n * m) return dis[t.first][t.second];

        // 在本地不動,更新鑰匙情況
        if (key[t.first])
        {
            int state = t.second | key[t.first];  // 更新手中的鑰匙狀態
            if (dis[t.first][state] > dis[t.first][t.second])
            {
                dis[t.first][state] = dis[t.first][t.second];
                q.push_front({t.first, state});
            }
        }

        // 向四周走
        for (int i = h[t.first]; ~i; i = ne[i])
        {
            int j = e[i];
            if (w[i] && !(t.second >> w[i] - 1 & 1)) continue;   // 有門並且沒有鑰匙
            if (dis[j][t.second] > dis[t.first][t.second] + 1)
            {
                dis[j][t.second] = dis[t.first][t.second] + 1;
                q.push_back({j, t.second});
            }
        }
    }

    return -1;
}

int main()
{
    memset(h, -1, sizeof h);

    // 給每個座標標號
    cin >> n >> m >> p;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            g[i][j] = ++cnt;
    
    // 讀邊
    cin >> k;
    for (int i = 0; i < k; ++i)
    {
        int x1, y1, x2, y2, type;
        scanf("%d %d %d %d %d", &x1, &y1, &x2, &y2, &type);
        int a = g[x1][y1], b = g[x2][y2];
        exist.insert({a, b}), exist.insert({b, a});
        if (type)
        {
            add(a, b, type);  // 邊的權值為鑰匙的種類
            add(b, a, type);
        }
    }

    // 建圖
    build();

    // 讀鑰匙
    cin >> s;
    for (int i = 0; i < s; ++i)
    {
        int x, y, type;
        scanf("%d %d %d", &x, &y, &type);
        key[g[x][y]] |= 1 << type - 1;  // 記錄每個點的鑰匙
    }

    // 做雙端bfs
    cout << bfs() << endl;

    return 0;
}

3.6 最短路計數模型

AcWing 1134. 最短路計數
題意: 給出一個 N 個頂點 M 條邊的無向無權圖,頂點編號為 1 到 N。問從頂點 1 開始,到其他每個點的最短路有幾條。
題解: 對於點j,其字首點有u1,u2,u3,即方案數cnt[j],那麼:
if (dis[j] > dis[u] + 1) {
dis[j] = dis[u] + 1;
cnt[j] = cnt[ver];
}
else if (dis[j] == distance + 1) cnt[j] += cnt[u];
初始時方案數為1
同時,bfs和dijkstra演算法求出的點的順序是拓撲序,可以直接處理這類問題
而spfa演算法出點的順序不一定是拓撲序,不能直接處理這個問題,但是可以間接處理這個問題;
比如存在負權邊情況下,可以先使用spfa求出拓撲關係(即求出所有dis[j]=dis[u]+g[u][j])那麼可以知道
u->j就是一個拓撲序,一旦知道了拓撲序,就可以bfs求出方案數了
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;

int n, m;
int const N = 4e5 + 10, MOD = 1e5 + 3, INF = 0x3f3f3f3f;
int e[N], ne[N], idx, h[N];
int dis[N], cnt[N], st[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 跑最短路
void dijkstra() {
    memset(dis, 0x3f, sizeof dis);
    memset(st, 0, sizeof st);

    priority_queue<PII, vector<PII>, greater<PII> > q;
    q.push({0, 1});
    dis[1] = 0;
    cnt[1] = 1;

    while (q.size()) {
        auto t = q.top();
        q.pop();
        int ver = t.second, distance = t.first;
        if (st[ver]) continue;
        st[ver] = 1;
        
        for (int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];

            // 更新最短路和方案數
            if (dis[j] > distance + 1) {
                dis[j] = distance + 1;
                cnt[j] = cnt[ver];
                q.push({dis[j], j});
            }
            else if (dis[j] == distance + 1) cnt[j] = (cnt[j] + cnt[ver]) % MOD;
        }
    }
}

int main() {
    memset(h, -1, sizeof h);

    // 建圖
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b), add(b, a);
    }

    // 跑最短路
    dijkstra();

    // 輸出
    for (int i = 1; i <= n; ++i) printf("%d\n", dis[i] == INF? 0: cnt[i]);

    return 0;
}

3.7 次短路模型

acwing383. 觀光
題意: 給定一張n個點m條的有向圖,求出有向圖中最短路的數目+次短路的數目。(本題要求次短路長度為最短路長度+1)
題解:
本題在求最短路方案的基礎上要求次短路
求解次短路可以維護一個dis[j][1]表示到j點的次短路的距離,cnt[j][1]表示到j點次短路的方案
然後求的過程中把次短路當成一個新的節點求即可
次短路的會出現4個情況
if (dist[j][0] > distance + w[i]):能夠更新最短路(那麼自然次短路也會被更新)
else if (dist[j][0] == distance + w[i]) (不能夠更新最短路,那麼次短路不會被更新)
else if (dist[j][1] > distance + w[i])(不能更新最短路,但是能夠更新次短路)
else if (dist[j][1] == distance + w[i]) (不能更新最短路,也不能更新次短路)
程式碼:

#include<bits/stdc++.h>

using namespace std;

const int N = 1010, M = 20010;

struct Ver {
    int id, type, dist;
    bool operator> (const Ver &W) const {
        return dist > W.dist;
    }
};

int n, m, S, T;
int h[N], e[M], w[M], ne[M], idx;
int dist[N][2], cnt[N][2];
bool st[N][2];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int dijkstra()
{
    memset(st, 0, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);

    dist[S][0] = 0, cnt[S][0] = 1;  // 第二維為1表示次短路,為0表示最短路
    priority_queue<Ver, vector<Ver>, greater<Ver>> heap;
    heap.push({S, 0, 0});

    while (heap.size()) {
        Ver t = heap.top();
        heap.pop();

        int ver = t.id, type = t.type, distance = t.dist, count = cnt[ver][type];
        if (st[ver][type]) continue;
        st[ver][type] = true;

        for (int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];

            // 4種情況討論
            if (dist[j][0] > distance + w[i]) {
                dist[j][1] = dist[j][0], cnt[j][1] = cnt[j][0];
                heap.push({j, 1, dist[j][1]});
                dist[j][0] = distance + w[i], cnt[j][0] = count;
                heap.push({j, 0, dist[j][0]});
            }
            else if (dist[j][0] == distance + w[i]) cnt[j][0] += count;
            else if (dist[j][1] > distance + w[i]) {
                dist[j][1] = distance + w[i], cnt[j][1] = count;
                heap.push({j, 1, dist[j][1]});
            }
            else if (dist[j][1] == distance + w[i]) cnt[j][1] += count;
        }
    }

    int res = cnt[T][0];
    if (dist[T][0] + 1 == dist[T][1]) res += cnt[T][1];  // 如果次短路和最短路僅差1

    return res;
}

int main() {
    int cases;
    scanf("%d", &cases);
    while (cases -- ) {
        scanf("%d%d", &n, &m);
        memset(h, -1, sizeof h);
        idx = 0;

        // 建圖
        while (m -- ) {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c);
        }
        scanf("%d%d", &S, &T);
        // 計數最短路+次短路數目
        printf("%d\n", dijkstra());
    }

    return 0;
}

3.8 圖論建圖技巧

3.8.1 虛擬源點

acwing1137. 選擇最佳線路
題意: 給定一張n個點m條邊的有向圖,每條邊邊權為t,有w個源點,一個匯點s,求出一條最短路能夠從某個源點到達匯點。
\(1≤s≤n,0<w<n,0<t≤1000\)
題解: 多源問題新增一個虛擬節點作為源點,虛擬源點到其他源點的長度都為0,那麼問題就變成單源匯問題。
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;

int const N = 1e3 + 10, M = 2e4 + 13, INF = 0x3f3f3f3f;
int e[M], ne[M], w[M], idx, h[M];
int n, m, t;
int dis[N], st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int dijkstra()
{
    memset(dis, 0x3f, sizeof dis);
    memset(st, 0, sizeof st);
    priority_queue<PII, vector<PII>, greater<PII> > q;
    dis[n + 1] = 0;
    q.push({0, n + 1});
    while (q.size())
    {
        auto t = q.top();
        q.pop();
        int distance = t.first, ver = t.second;
        if (st[ver]) continue;
        st[ver] = 1;
        for (int i = h[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dis[j] > distance + w[i])
            {
                dis[j] = distance + w[i];
                q.push({dis[j], j});
            }
        }
    }
    return dis[t];
}

int main()
{
    while (scanf("%d %d %d", &n, &m, &t) != EOF)
    {
        memset(h, -1, sizeof h);
        idx = 0;

        for (int i = 0; i < m; ++i)
        {
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, c);
        }

        int s_num;
        scanf("%d", &s_num);
        for (int i = 0; i < s_num; ++i)  // 每個源點和虛擬源點連一條權值為0的邊
        {
            int tmp;
            scanf("%d", &tmp);
            add(n + 1, tmp, 0);
        }

        int t = dijkstra();
        if (t == INF) printf("-1\n");
        else printf("%d\n", t);
    }
    return 0;
}

3.8.2 二分圖建圖優化建圖

acwing456車站分級
題意: 一條單向的鐵路線上,依次有編號為1, 2, …, n 的n個火車站。每個火車站都有一個級別,最低為1級。現有若干趟車次在這條線路上行駛,每一趟都滿足如下要求:如果這趟車次停靠了火車站x,則始發站、終點站之間所有級別大於等於火車站x的都必須停靠。(注意:起始站和終點站自然也算作事先已知需要停靠的站點)
例如,下表是5趟車次的執行情況。
其中,前4趟車次均滿足要求,而第5趟車次由於停靠了3號火車站(2級)卻未停靠途經的6號火車站(亦為2級)而不滿足要求。

現有m趟車次的執行情況(全部滿足要求),試推算這n個火車站至少分為幾個不同的級別。1≤n,m≤1000
題解: 很明顯,不停靠的站點的優先順序一定比停靠的站點的優先順序要小,因此不停靠的站點的優先順序最小為1,且停靠的站點的優先順序>=不停靠的站點的優先順序+1,則本題可以轉換為一個差分約束問題,且邊權大於等於0。(這裡不需要tarjan判斷是否有正環,因為明確了有解,不可能出現正環,所以直接拓撲排序求拓撲序(tarjan的目的也是縮點完求拓撲序))。本題的另一個難點在於建圖,如果直接把不停靠的站點向停靠的站點連一條邊,那麼建圖的複雜度為O(mn^2^)。對於一個二分圖,左邊的每個點都需要向右邊每個點連一條邊的建圖模型來說,可以設定一個虛擬節點,然後使得左邊每個點連向虛擬節點,虛擬節點再向右邊每個點連邊。這樣就把O(n ^ 2)優化到O(n)。

程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10, M = 2e6 + 10;
int n, m;
int e[M], ne[M], h[N], w[M], idx;
int din[N], st[N], dis[N];
vector<int> ans;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void topsort()
{
    queue<int> q;
    for (int i = 1; i <= n + m; ++i) if (!din[i]) q.push(i);

    while (q.size())
    {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            din[j]--;
            if (!din[j]) q.push(j);
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        // 建圖(加入虛節點,平方->線性)
        memset(st, 0, sizeof st);
        int cnt, start = n, end = 1, ver = n + i;  // start記錄最小的點,end記錄最大的點,ver記錄虛擬節點
        cin >> cnt;
        for (int i = 0; i < cnt; ++i)
        {
            int t;
            cin >> t;
            st[t] = 1;
            start = min(start, t);
            end = max(end, t);
        }

        // 把a->b的邊拆成a->ver和ver->b
        for (int j = start; j <= end; ++j)
        {
            if (st[j]) add(ver, j, 1), din[j]++;
            else add(j, ver, 0), din[ver]++;
        }
    }

    // 拓撲排序得到更新的順序
    topsort();

    // dp求最大值
    for (int i = 1; i <= n; ++i) dis[i] = 1;
    for (int i = 0; i < ans.size(); ++i)
    {
        int k = ans[i];
        for (int j = h[k]; ~j; j = ne[j])
        {
            int t = e[j];
            dis[t] = max(dis[t], dis[k] + w[j]);
        }
    }

    int res = 0;
    for (int i = 1; i <= n; ++i) res = max(res, dis[i]);
    cout << res << endl;
    return 0;
}

3.9 多源匯最短路

3.9.1 一般多源匯問題

acwing1125牛的旅行
題意: 農民John的農場裡有很多牧區,有的路徑連線一些特定的牧區。一片所有連通的牧區稱為一個牧場。John將會在兩個牧場中各選一個牧區,然後用一條路徑連起來,使得連通後這個新的更大的牧場有最小的直徑。現在請你程式設計找出一條連線兩個不同牧場的路徑,使得連上這條路徑後,這個更大的新牧場有最小的直徑。
1≤N≤150,0≤X,Y≤10^5^
題解:
本題要找最遠的路徑,可以這麼考慮:最遠的路徑要不然是原來連通塊內部最遠路徑,要不然是連線兩個連通塊的長度加上到兩個端點最遠的距離
因此只需要求出每個連通塊內部最遠距離,和連線兩個連通塊的距離+到兩個端點的最大距離,二者做個max即可
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<double, double> PDD;
const double INF = 1e30;
int const N = 155;

PDD grid[N];
double dis[N][N], maxd[N];
char g[N][N];
int n;

double get_dis(PDD x, PDD y)
{
    double dx = x.first - y.first, dy = x.second - y.second;
    return sqrt(dx * dx + dy * dy);
}

int main()
{
    // 讀入座標、座標網
    cin >> n;
    for (int i = 0; i < n; ++i) cin >> grid[i].first >> grid[i].second;
    for (int i = 0; i < n; ++i) cin >> g[i];

    // 初始化每個點的距離
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
        {
            if (i == j) dis[i][j] = 0;
            else if (g[i][j] == '1') dis[i][j] = get_dis(grid[i], grid[j]);
            else dis[i][j] = INF;
        }
    
    // 計算每個點對間的距離
    for (int k = 0; k < n; ++k)
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < n; ++j)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
    
    // 計算每個連通塊內部的最大距離
    double res1 = -1;
    for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < n; ++j)
        {
            if (dis[i][j] < INF / 2) maxd[i] = max(maxd[i], dis[i][j]);
        }
        res1 = max(res1, maxd[i]);
    }

    // 計算連線不同連通塊的情況
    double res2 = INF;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
        {
            if (dis[i][j] > INF/ 2) res2 = min(res2, maxd[i] + maxd[j] + get_dis(grid[i], grid[j]));
        }
    
    printf("%.6lf\n", max(res1, res2));
    return 0;
}

3.9.2 傳遞閉包問題

acwing343. 排序
題意: 給定 n 個變數和 m 個不等式。其中 n 小於等於26,變數分別用前 n 的大寫英文字母表示。不等式之間具有傳遞性,即若 A>B 且 B>C ,則 A>C。
請從前往後遍歷每對關係,每次遍歷時判斷:

  • 如果能夠確定全部關係且無矛盾,則結束迴圈,輸出確定的次序;
  • 如果發生矛盾,則結束迴圈,輸出有矛盾;
  • 如果迴圈結束時沒有發生上述兩種情況,則輸出無定解。

題解: 傳遞閉包問題。 本題也可以不使用floyd,每次新增一條邊的時候直接進行更新相關的點即可。這樣可以把mn^3^優化到 mn^2^
程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 27;
int n, m;
int dis[N][N], st[N];

// 檢查是否有解
int check()
{
    // 矛盾
    for (int i = 0; i < n; ++i)
        if (dis[i][i]) return 1;

    // 不確定
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < i ; ++j)
            if (!dis[i][j] && !dis[j][i]) return 0;
    
    // 無解
    return 2;
}

// 列印當前最小的那個
void get_min()
{
    for (int i = 0; i < n; ++i)
    {
        bool flg = true;  // 初始認為i是最小的
        if (st[i]) continue;  // i必須沒用過
        for (int j = 0; j < n; ++j)  // 看其他點是否能夠走到i,要找到一個走不到i點的
        {
            if (!st[j] && dis[j][i])  // 如果j點能夠走到i點,說明i點不是最小的
            {
                flg = false;
                break;
            } 
        }  // 如果是最小的
        if (flg)
        {
            printf("%c", i + 'A'); // 列印
            st[i] = 1;  // 記錄
            break;
        }
    }
}

int main()
{
    while (cin >> n >> m && n && m)
    {
        memset(dis, 0, sizeof dis);
        int type = 0, t = 0;  // type=0:未確定,type=1:矛盾, type=2:有解,t記錄是在哪個式子就判斷出矛盾或者有解
        for (int i = 1; i <= m; ++i)
        {
            char str[5];
            cin >> str;
            int a = str[0] - 'A', b = str[2] - 'A';
            if (!type)  // 未確定才要需要繼續
            {
                dis[a][b] = 1;  // 給a->b連邊
                for (int x = 0; x < n; ++x)  
                {
                    if (dis[x][a]) dis[x][b] = 1;  // 如果x和a有邊,那麼x和b有邊
                    for (int y = 0; y < n; ++y)  // 列舉y
                    {
                        if (dis[b][y]) dis[a][y] = 1;  // 如果b和y有邊,那麼a和y有邊
                        if (dis[x][a] && dis[b][y]) dis[x][y] = 1;  // 如果x->a和b->y有邊,那麼x->y有邊
                    }
                }
                type = check();  // 判斷解的情況
                if (type) t = i;  // 記錄是在哪個式子處判斷出矛盾或者有解
            }

        }

        // 輸出情況
        if (!type) printf("Sorted sequence cannot be determined.\n");
        else if (type == 1) printf("Inconsistency found after %d relations.\n", t);
        else 
        {
            printf("Sorted sequence determined after %d relations: ", t);
            memset(st, 0, sizeof st);
            for (int i = 0; i < n; ++i) get_min();  // 每次打印出最小的那個
            printf(".\n");
        }
    }
    return 0;
}

3.9.3 floyd找有向/無向最小環 O(n^3)

acwing344. 觀光之旅
題意: 給定一張無向圖,求圖中一個至少包含3個點的環,環上的節點不重複,並且環上的邊的長度之和最小。該問題稱為無向圖的最小環問題。你需要輸出最小環的方案,若最小環不唯一,輸出任意一個均可。
程式碼:

#include <bits/stdc++.h>

using namespace std;

int const N = 110;
int a[N][N], d[N][N], pos[N][N];
int n, m;
int ans;
vector<int> path;

// 得到x->y這段之間的路徑,不包括x和y這兩個端點
void get_path(int x, int y) {
    if (pos[x][y] == 0) return ; // x與y之間沒有點
    get_path(x, pos[x][y]);
    path.push_back(pos[x][y]);
    get_path(pos[x][y], y);
}

int main() {
    scanf("%d %d", &n, &m);
    
    // 距離初始化
    memset(a, 0x3f, sizeof a);
    for (int i = 1; i <= n; ++i) a[i][i] = 0;
    
    // 讀入m條邊
    for (int i = 1; i <= m; ++i) {
        int t1, t2, t3;
        scanf("%d %d %d", &t1, &t2, &t3);
        a[t1][t2] = a[t2][t1] = min(a[t1][t2], t3);
    }

    ans = 0x3f3f3f3f;
    memcpy(d, a, sizeof a);
    
    // 假設剛剛開始更新k,則k-1及其以下的情況全部更新完畢
    for (int k = 1; k <= n; ++k)  // k為最小環內編號最大的點
    {
        for (int i = 1; i < k; ++i) // i為最小環內第三大編號的點
            for (int j = i + 1; j < k; ++j)  // j為最小環內第二大編號的點
            {
                // 在k-1的情況下找到最小環
                if ((long long)d[i][j] + a[j][k] + a[k][i] < ans)  {
                    ans = d[i][j] + a[j][k] + a[k][i]; // 更新最小環的所有邊權值和
                    
                    // 遞迴得到路徑
                    path.clear();
                    path.push_back(i);
                    get_path(i, j);
                    path.push_back(j);
                    path.push_back(k);
                }
            }
            
        // 在k的情況下進行鬆弛操作
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) {
                if (d[i][j] > d[i][k] + d[k][j]) {
                    d[i][j] = d[i][k] + d[k][j];
                    pos[i][j] = k;  // 記錄i和j之間經過k點
                }
            }
    }
    
    // 列印答案
    if (ans != 0x3f3f3f3f) {
        for (int i = 0; i < path.size(); ++i)
            cout << path[i] << " ";
    }
    else cout << "No solution.\n";
    return 0;
}

3.9.4 floyd找恰好經過N條的最短距離

acwing345. 牛站
題意: 給定一張由T條邊構成的無向圖,點的編號為1~1000之間的整數。求從起點S到終點E恰好經過N條邊(可以重複經過)的最短路。
程式碼:

#include <bits/stdc++.h>

using namespace std;

int const N = 201;
int n, t, s, e;
int cnt;  // 記錄所有的點的數目
int num[N]; // 記錄每一個點

// 定義一個矩陣
struct mat {
    int m[N][N];
}unit;

// 定義矩陣乘法
mat operator * (mat a, mat b) {
    mat res;
    memset(res.m, 0x3f, sizeof(res.m));
    for (int k = 1; k <= cnt; ++k)
        for (int i = 1; i <= cnt; ++i)
            for (int j = 1; j <= cnt; ++j)
                res.m[i][j] = min(res.m[i][j], a.m[i][k] + b.m[k][j]);
    return res;
}

// 矩陣快速冪
mat pow_mat(mat a, int n) {
    mat res = a;  // 這裡不能使用快速冪,因為這裡的矩陣乘法裡面是加法,需要的是前一個的狀態,而a^0這個狀態無法退出a^1
    n--;
    while (n) {
        if (n & 1) res = res * a;
        a = a * a;
        n >>= 1;
    }
    return res;
}

int main() {
    cin >> n >> t >> s >> e;
    mat tmp;
    memset(tmp.m, 0x3f, sizeof(tmp.m));
    for (int i = 1; i <= t; ++i) {
        int a, b, c;
        scanf("%d %d %d", &c, &a, &b);
        if (!num[a]) num[a] = ++cnt;  // 標記點a出現過,同時記錄所有出現的點的數目cnt
        if (!num[b]) num[b] = ++cnt;
        tmp.m[num[a]][num[b]] = tmp.m[num[b]][num[a]] = min(tmp.m[num[b]][num[a]], c);  // 記錄a點和b點距離為c
    }
    mat ans = pow_mat(tmp, n); // 矩陣快速冪
    cout << ans.m[num[s]][num[e]] << endl;
    return 0;
}