1. 程式人生 > 實用技巧 >Java07-7_Java流程控制----for迴圈語句

Java07-7_Java流程控制----for迴圈語句

目錄

最小生成樹

1. 演算法分析

mst性質

  1. 最小生成樹的題目都是無向圖;

  2. 連通圖必存在最小生成樹;

  3. 不連通圖沒有最小生成樹(用這個判斷是否有最小生成樹)

  4. 任意一顆最小生成樹一定可以包含無向圖中權值最小的邊

  5. kruskal列舉的邊是遞增的,集合的數目遞減,具有單調性,因此很多二分的題目利用這個性質就可以省去二分的步驟

  6. kruskal能夠處理最小生成森林的問題

  7. 一張圖有多少個mst

  8. 哪些邊存在於所有的mst

  9. 最大邊最小的mst是哪個

最大生成樹

  1. 將圖中所有邊的邊權變為相反數,再跑一遍最小生成樹演算法。相反數最小,原數就最大。
  2. 對於kruskal,將“從小到大排序”改為“從大到小排序”;
  3. 對於prim,將“每次選到所有藍點代價最小的白點”改為“每次選到所有藍點代價最大的點”。

最短路徑樹
    最短路徑樹就是以一個節點為根,然後根節點到其他所有點的距離最短,然後形成了一棵樹,把不必要的邊刪除,其實我們用dij的時候求一個點到其他點的距離的時候就已經會把根節點到其他所有點的最短距離求出來了,只是我們不確定是哪些邊構成的
    假設我們要求的是從1出發的最短路徑樹,那麼我們就先求出最短路並標記用到了的邊:

2. 板子

2.1 prime演算法

#include <bits/stdc++.h>

using namespace std;

int const N = 5e2 + 10, M = 1e5 + 10;
int g[N][N], dis[N], st[N];
int n, m;

// prime演算法
int prime()
{
    memset(dis, 0x3f, sizeof dis);  // dis初始化
    int res = 0;  // 記錄最小生成樹的距離和
    for (int i = 0; i < n; ++i)  // 外迴圈n次
    {
        int t = -1;  // 記錄到集合最小的點
        for (int j = 1; j <= n; ++j)
            if (!st[j] && (t == -1 || dis[j] < dis[t]))
                t = j;  // 找到t
        if (i && dis[t] == 0x3f3f3f3f) return dis[t];  // 如果集合內有點且t點到集合的距離為無窮(即表示t不在連通圖內), 不存在最小生成樹
        if (i) res += dis[t];  // 如果集合內有點,累加res
        for (int j = 1; j <= n; ++j)  // 更新所有與t連線的點
            dis[j] = min(dis[j], g[t][j]);
        st[t] = 1;  // 把t放入集合
    }
    return res;  // 返回res
}

int main()
{
    cin >> n >> m;  // 讀入頂點和邊數
    memset(g, 0x3f, sizeof g);  // 初始化鄰接矩陣
    for (int i = 0; i < m; ++i)  // 讀入邊資訊
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);  // 無向圖注意要賦值兩次
    }
    int t = prime();
    if (t == 0x3f3f3f3f) cout << "impossible\n";
    else cout << t << endl;
    return 0;
}

2.2 kruskal演算法

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int p[N];
struct Edge  // 定義邊的資料結構,且按權值從小到大排序
{
    int a, b, w;
    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edge[N * 2];
int n, m;

// 並查集查詢操作
int find(int x)
{
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

// kruskal演算法
int kruskal()
{
    sort(edge, edge + m);  // 排序,使得從最小權值的邊開始
    int res = 0, cnt = 0;  // res記錄最小生成樹的權值,cnt記錄當前最小生成樹內有幾條邊
    for (int i = 1; i <= n; ++i) p[i] = i;  // 並查集初始化
    for (int i = 0; i < m ; ++i)  // 列舉每一條邊
    {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;  // 邊a, b, 權值為w
        a = find(a), b = find(b);  // 得到a的父節點和b的父節點
        if (a != b)  // 判斷a和b是否在同一個集合內
        {
            res += w;  // 在的話放入集合內
            cnt++;
            p[a] = b;
        }
    }
    if (cnt < n -1) return 0x3f3f3f3f;  // 最小生成樹內邊數如果小於n-1,說明無法得到最小生成樹
    else return res;
}

int main()
{
    scanf("%d%d", &n, &m);  // 輸入頂點和邊數目
    for (int i = 0; i < m; ++i)  // 輸入邊資訊
    {
        int a, b ,w;
        scanf("%d %d %d", &a, &b, &w);
        edge[i] = {a, b, w};
    }  
    int t = kruskal();  
    if (t == 0x3f3f3f3f) cout << "impossible\n";
    else cout << t << endl;
    return 0;
}

3. 典型例題

3.1 同時有點權和邊權的最小生成樹

acwing1146新的開始時
題意: 發展採礦業當然首先得有礦井,小 F 花了上次探險獲得的千分之一的財富請人在島上挖了 n 口礦井,但他似乎忘記了考慮礦井供電問題。
為了保證電力的供應,小 F 想到了兩種辦法:

  1. 在礦井 i 上建立一個發電站,費用為 vi(發電站的輸出功率可以供給任意多個礦井)。
  2. 將這口礦井 i 與另外的已經有電力供應的礦井 j 之間建立電網,費用為 pi,j。
    1≤n≤300,0≤vi,pi,j≤105

題解: 本題有點權有邊權,考慮把點權轉化為邊權,只需要把虛擬出一個節點n+1,把每個點和虛擬節點連線一條邊,這條邊的權值為點的權值,然後跑最小生成樹即可

程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 3e2 + 10;
int n;
int g[N][N], st[N], dis[N];

int prime() {
    int res = 0;
    memset(st, 0, sizeof st);
    memset(dis, 0x3f, sizeof dis);

    for (int i = 0; i < n + 1; ++i) {
        int t = -1;
        for (int j = 1; j <= n + 1; ++j)
            if (!st[j] && (t == -1 || dis[t] > dis[j])) t = j;
        
        st[t] = 1;
        if (i) res += dis[t];
        for (int j = 1; j <= n + 1; ++j) dis[j] = min(dis[j], g[t][j]);
    }
    
    return res;
}

int main() {
    cin >> n;

    // 讀邊,建立與虛擬節點的邊
    for (int i = 1; i <= n; ++i) {
        int t;
        cin >> t;
        g[i][n + 1] = t;
        g[n + 1][i] = t;
    }
    for (int i = 1 ; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            cin >> g[i][j];
    
    cout << prime() << endl;
    return 0;
}

小 FF 希望你幫他想出一個保證所有礦井電力供應的最小花費方案。

3.2 選定邊集最小生成樹

acwing1143聯絡員
題意: n個點m條邊的無向圖,其中m條邊分為兩個集合,集合1必須出現在最小生成樹中,集合2中的邊不一定出現在最小生成樹中。求出一個最小生成樹。1≤n≤2000,1≤m≤10000
題解: 先把集合1放入最小生成樹,然後再判斷集合2的邊是否能夠放入即可
程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10;
struct Edge {
    int a, b, w;
    bool operator< (const Edge &W) {
        return w < W.w;
    }
}edge[N];
int fa[N];
int n, m;

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

int main() {
    cin >> n >> m;
    vector<Edge> must, no_must;
    for (int i = 0; i < m; ++i) {
        int p, a, b, w;
        scanf("%d%d%d%d", &p, &a, &b, &w);
        if (p == 1) must.push_back({a, b, w});
        else no_must.push_back({a, b, w});
    }

    for (int i = 1; i <= n; ++i) fa[i] = i;

    // 加入必須邊(同時直接達到縮點效果)
    int res = 0;
    for (auto m: must) {
        int a = m.a, b = m.b, w = m.w;
        int pa = get(a), pb = get(b);
        
        res += w;

        if (pa != pb) {
            fa[pa] = pb;
        }
    }

    // 加入非必選邊
    sort(no_must.begin(), no_must.end());
    for (auto nm: no_must) {
        int a = nm.a, b = nm.b, w = nm.w;
        int pa = get(a), pb = get(b);
        if (pa != pb) {
            fa[pa] = pb;
            res += w;
        }
    }

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

3.3 最大邊最小--生成樹/森林

acwing1142繁忙的都市
題意: 一張n個點m條邊的無向圖,找出一個最大邊權最小的最小生成樹。1≤n≤300,1≤m≤8000,1≤邊權c≤10000
題解: 本題要求最大邊值最小的生成樹。由於kruskal保證了在列舉邊的時候是按照邊權從小到大來列舉的,因此一旦產生最小生成樹時就保證了當前邊的是生成樹的最大邊,且這條邊在所有的情況下最小
程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 8e3 + 10;
int n, m;
struct Edge {
    int a, b, w;
    bool operator< (const Edge &W) {
        return w < W.w;
    }
}edge[N];
int fa[N];

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

void kruskal() {
    int res = 0, cnt = 0;
    sort(edge, edge + m);
    for (int i = 1; i <= n; ++i) fa[i] = i;

    for (int i = 0; i < m; ++i) {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        int pa = get(a), pb = get(b);
        if (pa != pb) {
            res = max(res, w);
            fa[pa] = pb;
            cnt ++;
        }
        if (cnt == n - 1) break;
    }
    cout << n - 1 << " " << res << endl;
}   

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int a, b, w;
        scanf("%d %d %d", &a, &b, &w);
        edge[i] = {a, b, w};
    }
    kruskal();
    return 0;
}

acwing1145北極通訊網路
題意: 北極的某區域共有 n 座村莊,每座村莊的座標用一對整數 (x,y) 表示。為了加強聯絡,決定在村莊之間建立通訊網路,使每兩座村莊之間都可以直接或間接通訊。通訊工具可以是無線電收發機,也可以是衛星裝置。無線電收發機有多種不同型號,不同型號的無線電收發機有一個不同的引數 d,兩座村莊之間的距離如果不超過 d,就可以用該型號的無線電收發機直接通訊,d 值越大的型號價格越貴。現在要先選擇某一種型號的無線電收發機,然後t統一給所有村莊配備,數量不限,但型號都是 相同的。配備衛星裝置的兩座村莊無論相距多遠都可以直接通訊,但衛星裝置是有限的,只能給一部分村莊配備。現在有 k 臺衛星裝置,請你編一個程式,計算出應該如何分配這 k 臺衛星裝置,才能使所配備的無線電收發機的 d 值最小。
題解: 把本題翻譯過來就是求給定一個d,刪去圖中權值大於d的所有邊,做最小生成樹,得到的連通塊數目要求小於等於k,求這麼個d
通常的思路是二分d,然後求連通塊個數,判斷連通塊個數和k的關係
但是kruskal具有單調性,列舉的邊從小到大,集合的數目從大到小,具有單調性,因此不需要二分,只需要每次列舉完判斷一下當前的集合個數是否等於k,如果等於k,那麼說明當前放入的這條邊的權值就是d。
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef pair<double, double> PDD;
unordered_map<int, PDD> grid;
int n, k, cnt, m;
int const N = 5e2 + 10, M = N * N;
int fa[N];
struct Edge {
    int a, b;
    double w;

    bool operator< (const Edge &W) {
        return w < W.w;
    }
}edge[M];

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

void kruskal() {
    for (int i = 1; i <= cnt; ++i) fa[i] = i;

    int cnt_con = cnt;  // 記錄連通塊個數
    if (cnt_con < k) {
        cout << 0.00 << endl;
        return;
    }

    // 從小到大列舉邊
    sort(edge, edge + m);
    for (int i = 0; i < m; ++i) {
        int a = edge[i].a, b = edge[i].b;
        double w = edge[i].w;

        int pa = get(a), pb = get(b);
        if (pa != pb) {
            fa[pa] = pb;
            cnt_con--;  // 每次加入邊後,如果兩個端點不在一個連通塊內,那麼合併後連通塊個數將會減一
        }

        if (cnt_con == k) { // 一旦減到k,說明當前的邊權值即為d
            printf("%.2lf", w);
            return;
        }
    }
}

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

    // 給每個節點標號
    for (int i = 0; i < n; ++i) {
        int x, y;
        cin >> x >> y;
        grid[++cnt] = {x, y};
    }

    // 計算每個節點間的距離
    for (int i = 1; i <= cnt; ++i)
        for (int j = 1; j <= cnt; ++j) {
            int dx = grid[i].first - grid[j].first, dy = grid[i].second - grid[j].second;
            edge[m++] = {i, j, sqrt(dx * dx + dy * dy)};
        }

    // 做最小生成樹
    kruskal();
    return 0;
}

3.4 最優比率生成樹

acwing348 沙漠之王
題意: 大衛希望渠道的總成本和總長度的比值能夠達到最小。他只希望建立必要的渠道,為所有的村莊提供水資源,這意味著每個村莊都有且僅有一條路徑連線至首都。他的工程師對所有村莊的地理位置和高度都做了調查,發現所有渠道必須直接在兩個村莊之間水平建造。由於任意兩個村莊的高度均不同,所以每個渠道都需要安裝一個垂直的升降機,從而使得水能夠上升或下降。建設渠道的成本只跟升降機的高度有關,換句話說只和渠道連線的兩個村莊的高度差有關。需注意,所有村莊(包括首都)的高度都不同,不同渠道之間不能共享升降機。
題解: 本題是最優比率生成樹
要求找出一棵(a1/b1) + (a2/b2) +... + (an/bn)之和最大生成樹
考察的是01分數規劃模型,即我們設(a1/b1) + (a2/b2) +... + (an/bn) = mid。那麼對應於每一條邊我們可以得到一條新邊ai-mid * bi,採用二分的方式列舉mid,如果得到的使用新邊ai-mid * bi建成的最小生成樹的權值之和為0,那麼這個mid就是我們的答案;否則,找其他的mid
程式碼:

#include <bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10;

int n;
double dis[N], d[N][N], h[N][N]; // dis記錄到最小生成樹的最小距離,d陣列記錄兩個點的最小距離,h陣列記錄兩個點的最小高度
double x[N], y[N], z[N]; // 記錄每個點輸入的位置
bool vis[N]; // 判斷每個點是否在最小生成樹內

// prime演算法查詢新邊建立的最小生成樹是否滿足條件
bool prime (double mid)
{
    memset(dis, 0x3f, sizeof dis);
    memset(vis, 0, sizeof vis);
    double sum = 0; // 最小生成樹的邊權值和
    vis[1] = 1;
    
    // 把1號點放入集合後,計算和1號點相鄰的所有點的距離
    for (int i = 2; i <= n; ++i)
        dis[i] = h[1][i] - mid * d[1][i];
    
    for (int i = 2; i <= n; ++i)
    {
        // 找出到最小生成樹距離最小的那個點
        double mini = 0x3f3f3f3f;
        int u = -1;;
        for (int j = 2; j <= n; ++j)
        {
            if (!vis[j] && dis[j] < mini)
            {
                mini = dis[j], u = j;
            }
        }
        
        // 放入最小生成樹內
        vis[u] = 1;
        sum += dis[u];
        
        // 更新所有與u點相鄰的點
        for (int j = 2; j <= n; ++j)
        {
            if (!vis[j] && dis[j] > h[u][j] - mid * d[u][j])
                dis[j] = h[u][j] - mid * d[u][j];  // 用廣義邊更新
        }
    }
    
    // 判斷是否滿足條件
    if (sum >= 0) return false;  // mid取太小
    else return true;
}

int main()
{
    while (scanf("%d", &n) != EOF && n)
    {
        memset(x, 0, sizeof x);
        memset(y, 0, sizeof y);
        memset(z, 0, sizeof z);
        memset(d, 0, sizeof d);
        memset(h, 0, sizeof h);
        for (int i = 1; i <= n; ++i)
        {
            cin >> x[i] >> y[i] >> z[i];
            // 建立一張完全圖
            for (int j = 1; j < i; ++j)
            {
                d[i][j] = d[j][i] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));  // 記錄i點和前j個點的距離
                h[i][j] = h[j][i] = fabs(z[i] - z[j]); // 記錄高度差
            }
        }
        
        // 01分數規劃,二分查詢答案
        double l = 0, r = 1000.0, mid;
        while (r - l > 1e-6)
        {
             mid = (l + r) / 2;
             if (prime(mid)) r = mid; // 完全圖採用prime演算法
             else l = mid;
        }
        printf("%.3f\n", mid);
    }
    return 0;
}

3.5 尋找存在於所有最小生成樹的邊

2014-2015 ACM-ICPC, Asia Tokyo Regional Contest F.There is No Alternative
題意: 找出存在於所有最小生成樹的邊,打印出這些邊的數目和權值和。3≤點數N≤500,N−1≤邊數M≤min(50000,N(N−1)/2), 1≤邊權Ci≤10000
題解: 可以先用kruskal求出該圖的最小生成樹,在求的過程中標記上存在於最小生成樹的邊。由於存在於所有最小生成樹的邊必然存在於某棵最小生成樹上,因此可以暴力列舉每條邊,然後刪除這條邊,判斷是否還能構成最小生成樹,或者構成的最小生成樹比最早得到的最小生成樹大
程式碼:

#include <bits/stdc++.h>

using namespace std;

int n, m;
int const N = 5e4 + 10;
struct E {
    int u, v, w;
    bool operator<(const E &ed) {
        return w < ed.w;
    }
}edge[N];
int p[N], st[N];
vector<int> used;

int find(int x) {
    if (x == p[x]) return x;
    return p[x] = find(p[x]);
}

// kruskal演算法
int kruskal(int cn)
{
    int res = 0, cnt = 0;  // res記錄最小生成樹的權值,cnt記錄當前最小生成樹內有幾條邊
    for (int i = 1; i <= n; ++i) p[i] = i;  // 並查集初始化
    for (int i = 1; i <= m ; ++i)  // 列舉每一條邊
    {
        if (st[i]) continue;
        int a = edge[i].u, b = edge[i].v, w = edge[i].w;  // 邊a, b, 權值為w
        a = find(a), b = find(b);  // 得到a的父節點和b的父節點
        if (a != b)  // 判斷a和b是否在同一個集合內
        {
            res += w;  // 在的話放入集合內
            if (cn == -1) used.push_back(i);
            cnt++;
            p[a] = b;
        }
    }
    if (cnt < n -1) return -1;
    else return res;
}

int main() {
    memset(st, 0, sizeof st);
    cin >> n >> m;
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }
    sort(edge + 1, edge + 1 + m);
    int mst = kruskal(-1);  // 最早的最小生成樹
    vector<int> res;
    int sum = 0;
    for (int i = 0; i < used.size(); ++i) {  // 暴力列舉刪除的邊
        memset(st, 0, sizeof st);
        int idx = used[i];  // 邊的下標
        st[idx] = 1; // 該邊不能使用
        int new_mst = kruskal(i);
        if (new_mst != mst || new_mst == -1) {  // 如果無法生成最小生成樹或者生成的最小生成樹大於原先的最小生成樹
            res.push_back(i);
            sum += edge[idx].w;
        }
    }
    cout << res.size() << " " << sum;
    return 0;
}

3.6 最小生成樹恢復成完全圖

acwign346走廊潑水節
題意: 給定一棵N個節點的樹,要求增加若干條邊,把這棵樹擴充為完全圖,並滿足圖的唯一最小生成樹仍然是這棵樹。求增加的邊的權值總和最小是多少。注意: 樹中的所有邊權均為整數,且新加的所有邊權也必須為整數。1≤N≤6000,1≤Z≤100
題解: 要把一個最小生成樹補成完全圖,同時保證完全圖的最小生成樹不變。考慮kruskal的過程,每次把不同集合的連邊s加入最小生成樹,那麼把這兩個集合的其他邊互聯,只要互聯的邊的權值大於s就能保證這邊邊不會進入最小生成樹,同時kruskal按照邊權從小到大列舉保證了這樣得到的完全圖的邊權和最小
演算法步驟:
1.從小到大列舉每條邊
2.一旦某條邊的兩點屬於不同的集合,設這條邊權值為w,那麼這條邊屬於最小生成樹,這兩個集合的其他連邊屬於外部的完全圖,
因此答案加上(size[a] * size[b] - 1) * (w + 1)
程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 6e3 + 10;
struct Edge {
    int  a, b, w;
    bool operator< (const Edge &W) {
        return w < W.w;
    }
}edge[N * N / 2];
int fa[N], sizes[N];
int n;

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

int main() {
    int t;
    cin >> t;

    while (t--) {
        // 讀邊
        cin >> n;
        for (int i = 0; i < n - 1; ++i) {
            int a, b, c;
            cin >> a >> b >> c;
            edge[i] = {a, b, c};
        }

        // 初始化
        for (int i = 1; i <= n; ++i) fa[i] = i, sizes[i] = 1;
        sort(edge, edge + n - 1);

        // 列舉每條邊
        int res = 0;
        for (int i = 0; i < n - 1; ++i) {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            int pa = get(a), pb = get(b);
            if (pa != pb) { // 一旦屬於不同的集合
                res += (sizes[pa] * sizes[pb] - 1) * (w + 1);  // 把集合中除了edge[i]的其他邊連起來
                sizes[pb] += sizes[pa];
                fa[pa] = pb;
            }
        }
        
        printf("%d\n", res);
    }
    return 0;
}

3.7 最小生成森林

acwing1141 區域網
題意: 一張n個點k條邊的無向圖,可能不是聯通的,記點i到點j的邊權為f(i, j)。讓你刪除掉一些邊,使得原來互相連通的點還是互相聯通。求出刪除的邊的權值最大為多少。1≤n≤1000≤k≤200,1≤f(i,j)≤1000
題解: kruskal在計算過程中生成的最小生成森林一直都是最優的,而prime演算法沒有這個特性。因此可以使用kruskal演算法來得到最小生成森林(換言之,就是把最小生成樹去掉幾條邊)
程式碼:

#include<bits/stdc++.h>

using namespace std;

int const N = 2e2 + 10;
int n, k;
struct Edge {
    int a, b, w;
    bool operator< (const Edge &W) {
        return w < W.w;
    }
}edge[N * 2];
int fa[N];

int get(int x) {
    if (x == fa[x])return x;
    return fa[x] = get(fa[x]);
}

int kruskal() {
    for (int i = 1; i <= n; ++i) fa[i] = i;

    int res = 0;
    for (int i = 0; i < k; ++i) {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        int pa = get(a), pb = get(b);
        if (pa != pb) {
            res += w;
            fa[pa] = pb;
        }
    }
    return res;
}

int main() {
    int sum = 0;
    cin >> n >> k;
    for (int i = 0; i < k; ++i) {
        int a, b, w;
        scanf("%d %d %d", &a, &b, &w);
        edge[i] = {a, b, w};
        sum += w;
    }
    sort(edge, edge + k);
    cout << sum - kruskal() << endl;
    return 0;
}

3.8 最短路徑樹

3.8.1 求最短路徑樹的數目

acwing349 黑暗城堡
題意: 計算一張n個點m條邊的無向圖,以1號點為頂點的最短路徑樹有多少個。2≤點數N≤1000,N−1≤邊數M≤N(N−1)/2,1≤邊權L≤100
題解: 先做dijkstra演算法,求出每個點到1號點的最短路徑。然後計數到達每個點的最短路徑條數,最後的答案就是到達每個點的最短路徑條數的累乘。
程式碼:

#include <bits/stdc++.h>

using namespace std;

int const N = 1e3 + 10;
int g[N][N], dis[N];
int n, m;
int cnt[N]; // 記錄到達每個點的最短路徑條數
bool vis[N];

// 計算每個點的最短路徑
void dijkstra() {
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    for (int i = 1; i <= n; ++i) {
        int t = -1;
        for (int j = 1; j <= n ; ++j) {
            if ( !vis[j] && (t == -1 || dis[j] < dis[t]))
                t = j;
        }
        
        // 更新
        vis[t] = 1;
        for (int j = 1; j <= n; ++j)
            dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
    return;
}

int main() {
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= m; ++i) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    // dijkstra得到最短路徑
    dijkstra();
    
    // 遍歷每個點,判斷它的延伸點是否在最短路徑中
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (g[i][j] != 0x3f3f3f3f && dis[j] == dis[i] + g[i][j])
                cnt[j]++; // 計數到達每個點的最短路徑條數
        }
    }
    
    // 統計答案,把每個點的次數相乘
    long long ans = 1;
    for (int i = 1; i <= n; ++i) if (cnt[i]) ans = ( ans * cnt[i] ) % (2147483647);
    cout << ans << endl;
    return 0;
}

3.8.2 最短路徑樹必經邊

題意: n個城市用m條雙向公路連線,使得任意兩個城市都能直接或間接地連通。其中城市編號為1..n,公路編號為1..m。任意個兩個城市間的貨物運輸會選擇最短路徑,把這n*(n-1)條最短路徑的和記為S。現在你來尋找關鍵公路r,公路r必須滿足:當r堵塞之後,S的值會變大(如果r堵塞後使得城市u和v不可達,則S為無窮大)。
題解: 說白了就是求有多少條邊是所有最短路的必經邊。對於一個點u,如果刪去的邊不在它到其它點的最短路上,那麼S是不會變的。考慮到兩點之間的最短路不止一條,我們可以給每個點先建出一棵最短路徑樹出來,然後列舉最短路徑樹上的每條邊並把它刪掉,再跑一遍最短路,看S是否變大即可。如果最短路用dijkstra+heap來做,這個過程的時間複雜度就是:O(N∗((N+M)log(N+M)+N∗(N+M)log(N+M)))
程式碼:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<queue>
#define maxn 101
#define maxm 3001
using namespace std;
 
vector<int> to[maxn],w[maxn],id[maxn];
int dis[maxn],treeid[maxn];
bool vis[maxn],lzs[maxm];
int n,m;
 
inline int read(){
    register int x(0),f(1); register char c(getchar());
    while(c<'0'||'9'<c){ if(c=='-') f=-1; c=getchar(); }
    while('0'<=c&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
    return x*f;
}
 
inline void dijkstra(const int &s,const int &del){
    priority_queue< pair<int,int>,vector< pair<int,int> >,greater< pair<int,int> > > q;
    memset(vis,false,sizeof vis),memset(dis,0x3f,sizeof dis);
    q.push(make_pair(0,s)),dis[s]=0;
    while(q.size()){
        int u=q.top().second; q.pop();
        if(vis[u]) continue; vis[u]=true;
        for(register int i=0;i<to[u].size();i++) if(id[u][i]!=del){
            int v=to[u][i];
            if(dis[v]>dis[u]+w[u][i]){
                dis[v]=dis[u]+w[u][i],q.push(make_pair(dis[v],v));
                if(!del) treeid[v]=id[u][i];
            }
        }
    }
}

inline void out(int a){
    if(a>=10)out(a/10);
    putchar(a%10+'0');
}
 
int main(){
    n=read(),m=read();
    memset(dis,0x3f,sizeof dis);
    for(register int i=1;i<=m;i++){
        int u=read(),v=read(),_w=read();
        to[u].push_back(v),w[u].push_back(_w),id[u].push_back(i);
        to[v].push_back(u),w[v].push_back(_w),id[v].push_back(i);
    }
 
    for(register int i=1;i<=n;i++){
        dijkstra(i,0);
        int sum=0; for(register int j=1;j<=n;j++) sum+=dis[j];
        for(register int j=1;j<=n;j++) if(treeid[j]&&!lzs[treeid[j]]){
            dijkstra(i,treeid[j]);
            int cnt=0; for(register int k=1;k<=n;k++) cnt+=dis[k];
            if(cnt>sum) lzs[treeid[j]]=true;
        }
    }
 
    bool flag=false;
    for(register int i=1;i<=m;i++) if(lzs[i]){
        out(i),putchar('\n');
    }
    return 0;
}

3.9 次小生成樹

acwing356 次小生成樹
題意: 給定一張 N 個點 M 條邊的無向圖,求無向圖的嚴格次小生成樹。設最小生成樹的邊權之和為sum,嚴格次小生成樹就是指邊權之和大於sum的生成樹中最小的一個。N≤10^5^,M≤3∗10^5^
題解: 本題的思路是在求出最小生成樹的基礎上,找出一條非樹邊a->b,然後再樹上找出a->b的最大值,刪除這個最大值,加上非樹邊。基於這個思路,目標就是要找出這個a->b在樹邊的最大值。找出a->b在樹邊的最大值,可以先在樹上進行預處理,\(fa[i][j]\)表示i向上走\(2^j\)步到達的點,\(d1[i][j]\)表示i向上走\(2^j\)步範圍內的最大值,\(d2[i][j]\)表示i向上走\(2^j\)步範圍內的次大值,然後每次在找lca時順便找出,x到lca的最大值和次大值,y到lca的最大值和次大值,比較即可
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 100010, M = 300010, INF = 0x3f3f3f3f;

int n, m;
struct Edge {
    int a, b, w;
    bool used;
    bool operator< (const Edge &t) const {
        return w < t.w;
    }
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];

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

int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 找最小生成樹
LL kruskal() {
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    sort(edge, edge + m);
    LL res = 0;
    for (int i = 0; i < m; i ++ ) {
        int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
        if (a != b) {
            p[a] = b;
            res += w;
            edge[i].used = true;
        }
    }

    return res;
}

// 建樹
void build() {
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ )
        if (edge[i].used) {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            add(a, b, w), add(b, a, w);
        }
}

// 預處理fa,d1,d2,depth
void bfs() {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    q[0] = 1;  // 把1當成根節點
    int hh = 0, tt = 0;
    while (hh <= tt) {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;  // 更新j的深度
                q[ ++ tt] = j;
                fa[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;  // 求出d1和d2
                for (int k = 1; k <= 16; k ++ ) {
                    int anc = fa[j][k - 1];
                    fa[j][k] = fa[anc][k - 1];
                    int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
                    d1[j][k] = d2[j][k] = -INF;
                    for (int u = 0; u < 4; u ++ ) {
                        int d = distance[u];
                        if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
                    }
                }
            }
        }
    }
}

// 找出a和b的lca,順便求出a到b之間的最大值和次大值
int lca(int a, int b, int w) {
    static int distance[N * 2];
    int cnt = 0;
    if (depth[a] < depth[b]) swap(a, b);

    // 把a和b拉到同一個深度
    for (int k = 16; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b]) {
            distance[cnt ++ ] = d1[a][k];
            distance[cnt ++ ] = d2[a][k];
            a = fa[a][k];
        }

    // 把a和b之間的d1和d2的所有備選項求出來
    if (a != b) {
        for (int k = 16; k >= 0; k -- )
            if (fa[a][k] != fa[b][k]) {
                distance[cnt ++ ] = d1[a][k];
                distance[cnt ++ ] = d2[a][k];
                distance[cnt ++ ] = d1[b][k];
                distance[cnt ++ ] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        distance[cnt ++ ] = d1[a][0];
        distance[cnt ++ ] = d1[b][0];
    }

    // 把a和b之間的d1和d2求出來
    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; i ++ ) {
        int d = distance[i];
        if (d > dist1) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2) dist2 = d;
    }

    if (w > dist1) return w - dist1;
    if (w > dist2) return w - dist2;
    return INF;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i ++ ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }

    LL sum = kruskal();  // 計算最小生成樹的值
    build();  // 把所有的樹邊建樹
    bfs();  // 預處理出d1,d2,fa,depth陣列

    LL res = 1e18;
    for (int i = 0; i < m; i ++ )
        if (!edge[i].used) { // 找出每條非樹邊,然後替換掉最大的那條樹邊
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            res = min(res, sum + lca(a, b, w));
        }
    printf("%lld\n", res);

    return 0;
}