WHUT第八週訓練整理

WHUT第八週訓練整理

寫在前面的話:我的能力也有限,錯誤是在所難免的!因此如發現錯誤還請指出一同學習!

索引

(難度由題目自身難度與本週做題情況進行分類,僅供新生參考!)

零、並查集與最小生成樹

一、easy:01、02、03、04、05、06、07、08、09、10、11、12、13

二、medium:14、15、16、17、19、22、23、25、26

三、hard:18、20、21、24

本題解報告大部分使用的是C++語言,在必要的地方使用C語言解釋。

零、並查集與最小生成樹

兩個知識點的學習相信大家已經自己在網上學習過了,這裏就大概提一下就可以了。

首先並查集是一個性能很強的工具,路徑壓縮過後幾乎可以在 O(1)O(1)​ 的時間內對一類相同元素找到代表元以及兩類不同元素的合併,並查集所花費的時間可以說是常數級別的。注意,使用並查集的前提是集合中的所有元素之間的聯繫都是雙向的且具有傳遞性,如果僅僅只是單向的關係或者不具有傳遞性,那麼不能使用並查集。

而最小生成樹求的就是一張圖中將所有點連通且總權值最大的樹,它分爲兩種常用的算法:Kruskal 以及 Prim,前者在稀疏圖(點多邊少)中的表現更好,後者在稠密圖(點少邊多)種的表現更好。Kruskal 的時間複雜度爲 O(ElogE)O(ElogE),而 Prim 的時間複雜度爲 O(N2)O(N^2),Prim 若使用堆優化則時間複雜度爲 O(NlogN)O(NlogN)。(EE 表示邊數,NN 表示點數)

Kruskal 的實現需要使用並查集,而 Prim 則不需要。

一、easy

1001:How Many Tables(並查集)

題意:擺桌子,只有相互認識的人才能坐在同一張桌子旁,認識具有傳遞性,如果 AA​ 認識 BB​,而 BB​ 認識 CC​,那麼 AA​ 就認識 CC​。現在有 NN​ 個人,MM​ 種關係,問至少需要多少張桌子?

範圍:1N,M10001 \le N,M \le 1000

分析:典型的並查集背景,將有關係的人放在同一個集合中,最後遍歷所有人記錄不同集合的個數即可。

Code

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

const int MAXN = 1000 + 10;

int n, m;

int fa[MAXN];  // 每個集合的代表元

// 查
int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

// 並
void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int vis[MAXN];  // 標記第i個集合有沒有出現過

signed main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(vis, 0, sizeof(vis));  // 清空數組
        cin >> n >> m;
        // 使用並查集之前必須初始化,每個元素的代表元一開始都是自己
        for (int i = 1; i <= n; i++)
            fa[i] = i;
        for (int i = 0; i < m; i++)
        {
            int u, v;
            cin >> u >> v;
            unin(u, v);  // 有關係的並起來
        }
        int ans = 0;
        for (int i = 1; i <= n; i++)
        {
            int fx = find(i);
            // 如果該集合第一次出現,則統計答案
            if (!vis[fx])
            {
                vis[fx] = 1;
                ans++;
            }
        }
        cout << ans << endl;
    }
    return 0;
}

1002:小希的迷宮(並查集)

題意:有 NN​ 個房間,若干條通道雙向連接兩個房間,問現在的佈局是否滿足任意兩個房間都可以相通,並且路徑是唯一的。

範圍:1N1e51 \le N \le 1e5

分析:首先,道路是無向邊,房間的連通關係滿足傳遞性。現在問任意兩個房間是否都相通,則可以用並查集判斷是否所有的房間都在同一個集合中。題目還要求路徑是唯一的,就是說圖中不能出現環,並查集也可以判斷,當加入的邊兩個端點已經屬於同一個集合的時候,那麼就說明出現了環。

Notice:注意細節,本題的輸入還是要仔細處理的,詳見代碼。

Code

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

const int MAXN = 1e5 + 10;

int fa[MAXN], vis[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int main()
{
    int u, v;
    int maxV = 0;  // 用來確定房間的數目
    int flag = 0;  // 標記是否破壞規則
    for (int i = 1; i < MAXN; i++)
        fa[i] = i;
    while (cin >> u >> v)
    {
        if (u == v && u == -1)
            break;
        // 出現兩個0,說明所有的關係已經給出,現在判斷結果
        if (u == v && u == 0)
        {
            int f = -1;  // 唯一的集合標號
            for (int i = 1; i <= maxV; i++)
            {
                if (!vis[i])  // 沒有出現的房間不用管
                    continue;
                int fx = find(i);
                if (f == -1)
                    f = fx;
                else if (f != fx)  // 出現不同集合
                    flag = 1;
            }
            if (flag)
            {
                cout << "No" << endl;
            }
            else
            {
                cout << "Yes" << endl;
            }
            // 初始化
            for (int i = 1; i <= maxV; i++)
            {
                fa[i] = i;
                vis[i] = 0;
            }
            flag = maxV = 0;
            continue;
        }
        vis[u] = vis[v] = 1;  // 這兩個房間出現過
        maxV = max(maxV, max(u, v));  // 最大的房間編號
        int fx = find(u), fy = find(v);
        if (fx == fy)
            flag = 1;
        unin(fx, fy);
    }
    return 0;
}

1003:Is It A Tree?(並查集)

題意:給出若干個關係,判斷這張圖是否爲一顆樹。

範圍:未明確指出。

分析:需要滿足三個條件:

  1. 只有一個入度爲 00 的點
  2. 其餘所有點的入度都爲 11
  3. 所有點都連通

無向邊,且連通關係滿足傳遞性,可以使用並查集。本題跟 10021002 類似,只需要在其基礎上加入入度數組 inEdgeinEdge 判斷入度條件即可。詳見代碼。

Code

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

const int MAXN = 1e6 + 10;

int fa[MAXN], vis[MAXN], inEdge[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int main()
{
    int u, v;
    int kase = 1;
    int maxV = 0;
    int flag = 0; 
    for (int i = 1; i < MAXN; i++)
        fa[i] = i;
    while (cin >> u >> v)
    {
        if (u == v && u == -1)
            break;
        if (u == v && u == 0)
        {
            int cnt = 0;  // 入度爲0的數量
            int f = -1;  // 判斷條件3
            for (int i = 1; i <= maxV; i++)
            {
                if (!vis[i])
                    continue;
                if (!inEdge[i])  
                    cnt++;
                if (cnt > 1 || inEdge[i] > 1)  // 條件12
                    flag = 1;
                int fx = find(i);
                if (f == -1)
                    f = fx;
                else if (f != fx)  // 條件3
                    flag = 1;
            }
            cout << "Case " << kase++ << " is ";
            if (flag)
            {
                cout << "not a tree." << endl;
            }
            else
            {
                cout << "a tree." << endl;
            }
            for (int i = 1; i <= maxV; i++)
            {
                fa[i] = i;
                vis[i] = 0;
                inEdge[i] = 0;
            }
            flag = maxV = 0;
            continue;
        }
        vis[u] = vis[v] = 1;
        maxV = max(maxV, max(u, v));
        inEdge[v]++;  // 入度增加
        int fx = find(u), fy = find(v);
        if (fx == fy)
            flag = 1;
        unin(fx, fy);
    }
    return 0;
}

1004:More is better(並查集)

題意:一個房間中有很多個男孩,有 NN​ 種朋友關係,可以指定任意個男孩出去,要保證最後留在房間裏面的男孩都是直接或間接的朋友,問最後留在房間裏面的男孩數量最多可以是多少?

範圍:1N1e51 \le N \le 1e5,男孩的數量可能有 1000000010000000

分析:這題是真的暴力啊,這種 1e71e7​ 的數據範圍多組輸入都可以這樣暴力,大拇指好吧。

首先朋友關係是雙向的,並且是傳遞的,可以使用並查集。這個問題的本質就是問這些男孩之中最大的相互認識的團體的大小,即集合的大小。因此可以使用並查集計算每個集合中的元素個數,那麼答案就是所有集合中最大的個數。

Notice:直接用 cincin 會超時的!用 scanfscanf 或者關閉流同步!而且注意 n==0n==0​ 的情況!

Code

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

const int MAXN = 1e7 + 10;

int n, ans;

int fa[MAXN], num[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
    num[fy] += num[fx];  // 此時fy是代表元,集合的大小增加了fy所在集合的大小
    ans = max(ans, num[fy]);  // 取最大值
}

int main()
{
    while (~scanf("%d", &n))
    {
        if (n == 0)
        {
            cout << 1 << endl;
            continue;
        }
        // 暴力1e7初始化!男孩的序號可能到1e7
        for (int i = 0; i < MAXN; i++)
        {
            fa[i] = i;
            num[i] = 1;  // 每個集合一開始只有一個元素
        }
        ans = 0;
        for (int i = 0; i < n; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            unin(u, v);
        }
        cout << ans << endl;
    }
    return 0;
}

1005:Constructing Roads(Kruskal)

題意:有 NN 個村莊,MM​ 條已經修好的邊,現在問還需要修哪些邊,才能在最小的花費下實現連通所有的村莊。

範圍:1N100  0MN(N1)21 \le N \le 100~,~0\le M \le \frac{N*(N-1)}{2}

分析:明顯是要我們求最小生成樹,但是考慮到已經有現成的邊可以白嫖,所以在求的時候就可以把已經連通的村莊整體看成一個村莊,可以使用並查集讓他們在同一個集合裏面。既然使用並查集了,還需要求最小生成樹,那就自然用 Kruskal 啦。

回顧 Kruskal,首先將所有的邊按照長度從小到大排序,然後每次選出最小的邊,看兩端點是否已經連通,連通則跳過,否則加上這條邊。而這道題目要求構造的最小生成樹有點不同,圖中已經有一些邊存在,所以我們可以先將這些邊的端點利用並查集連接,然後再貪心地選擇長度最短的邊加入答案即可。

Code

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

const int MAXN = 100 + 10;

int n, q;

// 結構體——邊,重載運算符 < 實現自定義排序規則,sort時按照長度排序
struct Edge
{
    int u, v, len;
    bool operator<(Edge other) const
    {
        return len < other.len;
    }
} edges[MAXN * MAXN];

int fa[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int main()
{
    while (cin >> n)
    {
        for (int i = 1; i <= n; i++)
            fa[i] = i;
        int index = 0;
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                int v;
                cin >> v;
                edges[index++] = {i, j, v};  // 這樣寫比較方便,可以參考
            }
        }
        sort(edges, edges + index);  // 按長度從小到大排序
        cin >> q;
        // 已經修好的路所連接的村莊放入同一個集合
        for (int i = 0; i < q; i++)
        {
            int u, v;
            cin >> u >> v;
            unin(u, v);
        }
        int ans = 0;
        // Kruskal
        for (int i = 0; i < index; i++)
        {
            int u = edges[i].u, v = edges[i].v, len = edges[i].len;
            int fx = find(u), fy = find(v);
            if (fx == fy)
                continue;
            ans += len;
            unin(fx, fy);
        }
        cout << ans << endl;
    }
    return 0;
}

1006:暢通工程(並查集)

題意:有 NN​ 個城市,MM​ 條道路已經鋪好,現在問還需要多少條邊才能讓所有的城市相互連通。

範圍:1N10001 \le N \le 1000MM 的範圍未指出

分析:道路是雙向的,連通關係具有傳遞性,可以使用並查集。首先已經連通的城市整體可以看成是一個大城市,如果這樣處理完之後圖中只剩下孤立的點,表示一個個大城市,現在要讓所有城市連通,就是要讓剩下的這些點連通。假設剩下 AA 個大城市,那麼我們只需要 A1A-1 條邊就可以形成一顆樹,實現連通。

Code

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

const int MAXN = 1000 + 10;

int n, m;

int fa[MAXN], vis[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int main()
{
    while (~scanf("%d", &n), n)
    {
        memset(vis, 0, sizeof(vis));
        for (int i = 1; i <= n; i++)
            fa[i] = i;
        scanf("%d", &m);
        for (int i = 0; i < m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            unin(u, v);  // 形成大城市
        }
        int cnt = 0;  // 大城市數量
        for (int i = 1; i <= n; i++)
        {
            int fx = find(i);
            if (vis[fx])
                continue;
            vis[fx] = 1;
            cnt++;
        }
        printf("%d\n", cnt - 1);  // 大城市之間形成樹
    }
    return 0;
}

1007:還是暢通工程(最小生成樹)

題意:有 NN​ 個城市,給出任意兩個城市之間的距離,求讓這些城市連通的最小邊權總和。

範圍:1N1001 \le N \le 100

分析:最小生成樹板子題。

Code

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

const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;

int n;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    int w;    //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, int w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

int Kruskal(int n) //傳入點數,返回最小生成樹的權值,如果不連通則返回-1
{
    memset(F, -1, sizeof(F));
    sort(edge, edge + tol, cmp);
    int cnt = 0; //計算加入的邊數
    int ans = 0;
    for (int i = 0; i < tol; i++)
    {
        int u = edge[i].u;
        int v = edge[i].v;
        int w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans += w;
            F[tOne] = tTwo;
            cnt++;
        }
        if (cnt == n - 1)
        {
            break;
        }
    }
    if (cnt < n - 1)
    {
        return -1; //不連通
    }
    else
    {
        return ans;
    }
}

int main()
{
    while (~scanf("%d", &n), n)
    {
        tol = 0;
        for (int i = 0; i < n * (n - 1) / 2; i++)
        {
            int u, v, len;
            scanf("%d%d%d", &u, &v, &len);
            addEdge(u, v, len);
        }
        cout << Kruskal(n) << endl;
    }
    return 0;
}

1008:暢通工程(最小生成樹)

題意:有 NN​ 個城市,MM​ 條雙向邊,求讓這些城市連通的最小邊權總和。

範圍:1N1001 \le N \le 100​

分析:還是最小生成樹板子題,不存在 MSTMST 則輸出 “?” 。

Code

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

const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;

int n, m;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    int w;    //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, int w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

int Kruskal(int n) //傳入點數,返回最小生成樹的權值,如果不連通則返回-1
{
    memset(F, -1, sizeof(F));
    sort(edge, edge + tol, cmp);
    int cnt = 0; //計算加入的邊數
    int ans = 0;
    for (int i = 0; i < tol; i++)
    {
        int u = edge[i].u;
        int v = edge[i].v;
        int w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans += w;
            F[tOne] = tTwo;
            cnt++;
        }
        if (cnt == n - 1)
        {
            break;
        }
    }
    if (cnt < n - 1)
    {
        return -1; //不連通
    }
    else
    {
        return ans;
    }
}

int main()
{
    while (~scanf("%d%d", &m, &n), m)
    {
        tol = 0;
        for (int i = 0; i < m; i++)
        {
            int u, v, len;
            scanf("%d%d%d", &u, &v, &len);
            addEdge(u, v, len);
        }
        int ans = Kruskal(n);
        if (ans == -1)
        {
            cout << "?" << endl;
        }
        else
        {
            cout << ans << endl;
        }
    }
    return 0;
}

1009:暢通工程再續(最小生成樹)

題意:有 NN​ 個城市的二維座標,可以在任意兩個城市之間建設道路,求讓這些城市連通的最小道路長度總和。

範圍:1N1001 \le N \le 100

分析:最小生成樹板子題,把邊權的地方改成 double,雙重循環遍歷所有村莊計算歐氏距離進行加邊,最後輸出答案的時候 100*100 再控制精度輸出即可。

Code

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

const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;

int n;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    double w; //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, double w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

double Kruskal(int n) //傳入點數,返回最小生成樹的權值,如果不連通則返回-1
{
    memset(F, -1, sizeof(F));
    sort(edge, edge + tol, cmp);
    int cnt = 0; //計算加入的邊數
    double ans = 0;
    for (int i = 0; i < tol; i++)
    {
        int u = edge[i].u;
        int v = edge[i].v;
        double w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans += w;
            F[tOne] = tTwo;
            cnt++;
        }
        if (cnt == n - 1)
        {
            break;
        }
    }
    if (cnt < n - 1)
    {
        return -1; //不連通
    }
    else
    {
        return ans;
    }
}

int pos[MAXN][2];

// 歐氏距離
double distance(int i, int j)
{
    double x1 = pos[i][0], y1 = pos[i][1], x2 = pos[j][0], y2 = pos[j][1];
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        tol = 0;
        cin >> n;
        for (int i = 0; i < n; i++)
        {
            cin >> pos[i][0] >> pos[i][1];
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                double dis = distance(i, j);
                // 不滿足條件則不能加
                if (dis < 10 || dis > 1000)
                    continue;
                addEdge(i, j, dis);
            }
        }
        double ans = Kruskal(n);
        if (ans < 0)
        {
            cout << "oh!" << endl;
        }
        else
        {
            // 保留一位小數
            cout << fixed << setprecision(1) << ans * 100 << endl;
        }
    }
    return 0;
}

1010:繼續暢通工程(Kruskal)

題意:有 NN​ 個城市的,給出所有城市之間的道路代價以及道路是否修建狀態,求讓這些城市連通的最小代價。

範圍:1N1001 \le N \le 100

分析:跟 10051005 差不多,對於已經修建好的道路,把兩個端點放到同一個集合中,再使用 Kruskal 從小到大加邊即可。

Code

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

const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;

int n;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    double w; //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, double w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    return F[x] == x ? x : F[x] = find(F[x]);
}

int main()
{
    while (~scanf("%d", &n), n)
    {
        for (int i = 1; i <= n; i++)
            F[i] = i;
        tol = 0;
        for (int i = 0; i < n * (n - 1) / 2; i++)
        {
            int u, v, len, state;
            scanf("%d%d%d%d", &u, &v, &len, &state);
            if (state)  // 已經存在,加入同一條邊
            {
                F[find(u)] = find(v);
            }
            else  // 可以修建
            {
                addEdge(u, v, len);
            }
        }
        // Kruskal
        sort(edge, edge + tol, cmp);
        int ans = 0;
        for (int i = 0; i < tol; i++)
        {
            int u = edge[i].u, v = edge[i].v, w = edge[i].w;
            int fx = find(u), fy = find(v);
            if (fx == fy)
                continue;
            ans += w;
            F[fx] = fy;
        }
        cout << ans << endl;
    }
    return 0;
}

1011:Connect the Cities(Kruskal)

題意:有 NN 個城市的,MM 條可以修建的道路,KK​ 個已經連通的城市羣,求讓這些城市連通的最小代價。

範圍:3N500 , 0m25000 , 0K1003 \le N \le 500~,~0 \le m \le 25000~,~0 \le K \le 100

分析:還是跟 10051005​ 差不多,對於這些城市羣,把裏面的所有城市放到同一個集合中,再進行 Kruskal 即可。

Code

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

const int MAXN = 500 + 10;
const int MAXM = 25000 + 10;

int n, m, k;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    double w; //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, double w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    return F[x] == x ? x : F[x] = find(F[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    F[fx] = fy;
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d%d%d", &n, &m, &k);
        for (int i = 1; i <= n; i++)
            F[i] = i;
        tol = 0;
        for (int i = 0; i < m; i++)
        {
            int u, v, len;
            scanf("%d%d%d", &u, &v, &len);
            addEdge(u, v, len);
        }
        // 將每個城市羣中的城市放入同一個集合
        for (int i = 0; i < k; i++)
        {
            int num;
            scanf("%d", &num);
            int pre = -1;
            for (int j = 0; j < num; j++)
            {
                int v;
                scanf("%d", &v);
                if (pre == -1)
                    pre = v;
                else
                    unin(pre, v);
                pre = v;
            }
        }
        // Kruskal
        sort(edge, edge + tol, cmp);
        int ans = 0;
        for (int i = 0; i < tol; i++)
        {
            int u = edge[i].u, v = edge[i].v, w = edge[i].w;
            int fx = find(u), fy = find(v);
            if (fx == fy)
                continue;
            ans += w;
            unin(fx, fy);
        }
        int cnt = 0;
        for (int i = 1; i <= n; i++)
        {
            if (F[i] == i)
                cnt++;
        }
        if (cnt == 1)
            cout << ans << endl;
        else
            cout << -1 << endl;
    }
    return 0;
}

1012:Jungle Roads(最小生成樹)

題意:題目說了一大堆,說白了就是求最小生成樹!

範圍:1<N<271 < N < 27​

分析:最小生成樹,沒有什麼坑,就是注意細節,詳見代碼。

Code

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

const int MAXN = 30 + 10;
const int MAXM = MAXN * MAXN;

int n;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    int w;    //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, int w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

int Kruskal(int n) //傳入點數,返回最小生成樹的權值,如果不連通則返回-1
{
    memset(F, -1, sizeof(F));
    sort(edge, edge + tol, cmp);
    int cnt = 0; //計算加入的邊數
    int ans = 0;
    for (int i = 0; i < tol; i++)
    {
        int u = edge[i].u;
        int v = edge[i].v;
        int w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans += w;
            F[tOne] = tTwo;
            cnt++;
        }
        if (cnt == n - 1)
        {
            break;
        }
    }
    if (cnt < n - 1)
    {
        return -1; //不連通
    }
    else
    {
        return ans;
    }
}

int main()
{
    while (cin >> n, n)
    {
        tol = 0;
        for (int i = 0; i < n - 1; i++)
        {
            string str;
            int len;
            cin >> str >> len;
            for (int j = 0; j < len; j++)
            {
                string name;
                int dis;
                cin >> name >> dis;
                // 加邊 A對應0 B對應1 以此類推
                addEdge(str[0] - 'A', name[0] - 'A', dis);
            }
        }
        cout << Kruskal(n) << endl;
    }
    return 0;
}

1013:Eddy’s picture(最小生成樹)

題意:給 NN 滴墨水的二維座標,問將這些墨水連在一起的最短距離和。

範圍:0<N1000 < N \le 100

分析:跟 10091009 類似,不多說了,看代碼。

Code

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

const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;

int n;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    double w; //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, double w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

double Kruskal(int n) //傳入點數,返回最小生成樹的權值,如果不連通則返回-1
{
    memset(F, -1, sizeof(F));
    sort(edge, edge + tol, cmp);
    int cnt = 0; //計算加入的邊數
    double ans = 0;
    for (int i = 0; i < tol; i++)
    {
        int u = edge[i].u;
        int v = edge[i].v;
        double w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans += w;
            F[tOne] = tTwo;
            cnt++;
        }
        if (cnt == n - 1)
        {
            break;
        }
    }
    if (cnt < n - 1)
    {
        return -1; //不連通
    }
    else
    {
        return ans;
    }
}

double pos[MAXN][2];

double distance(int i, int j)
{
    double x1 = pos[i][0], y1 = pos[i][1], x2 = pos[j][0], y2 = pos[j][1];
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

int main()
{
    while (cin >> n)
    {
        tol = 0;
        for (int i = 0; i < n; i++)
        {
            cin >> pos[i][0] >> pos[i][1];
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                double dis = distance(i, j);
                addEdge(i, j, dis);
            }
        }
        cout << fixed << setprecision(2) << Kruskal(n) << endl;
    }
    return 0;
}

二、medium

1014:Farm Irrigation(細節+並查集)

題意:有一塊 NMN*M​ 的田地,裏面有各種管子,類型從 AA​KK​,通過管子連通的土地只需要一次澆灌,現在問灌溉整塊田地需要多少次澆灌?

範圍:1N,M501 \le N,M \le 50

分析:問題並不難,就是要想怎麼寫比較好寫,細節比較多。

我這裏說說我的寫法,管子的種類很多,但是管子的屬性只包括:向上連通、向下連通、向左連通以及向右連通這四種。因此我們可以保存具有某種屬性的管子有哪些!

① 若當前的管子具有向上的屬性,且上面的管子具有向下的屬性,則連通

② 若當前的管子具有向下的屬性,且下面的管子具有向上的屬性,則連通

③ 若當前的管子具有向左的屬性,且左邊的管子具有向右的屬性,則連通

④ 若當前的管子具有向右的屬性,且右邊的管子具有向左的屬性,則連通

Notice:注意細節,數組大小要開對,詳見代碼。

Code

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

const int MAXN = 50 + 10;
const int MAXM = 2e4 + 10;

int n, m;

char g[MAXN][MAXN];
int F[MAXN * MAXN], vis[MAXN * MAXN];  // !!

int find(int x)
{
    return F[x] == x ? x : F[x] = find(F[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    F[fx] = fy;
}

int id(int x, int y)  // 點(x, y)的編號
{
    return x * m + y;
}

// 管子向上、向下、向左、向右的有哪些
set<char> s[4] = {
    {'A', 'B', 'E', 'G', 'H', 'J', 'K'},
    {'C', 'D', 'E', 'H', 'I', 'J', 'K'},
    {'A', 'C', 'F', 'G', 'H', 'I', 'K'},
    {'B', 'D', 'F', 'G', 'I', 'J', 'K'}};

int main()
{
    while (cin >> n >> m, n > 0, m > 0)
    {
        memset(vis, 0, sizeof(vis));
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                cin >> g[i][j];
                F[id(i, j)] = id(i, j);
            }
        }
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                // 分別對應上面寫的四種規則
                if (s[0].count(g[i][j]) && i - 1 >= 0 && s[1].count(g[i - 1][j]))
                {
                    unin(id(i, j), id(i - 1, j));
                }
                if (s[1].count(g[i][j]) && i + 1 < n && s[0].count(g[i + 1][j]))
                {
                    unin(id(i, j), id(i + 1, j));
                }
                if (s[2].count(g[i][j]) && j - 1 >= 0 && s[3].count(g[i][j - 1]))
                {
                    unin(id(i, j), id(i, j - 1));
                }
                if (s[3].count(g[i][j]) && j + 1 < m && s[2].count(g[i][j + 1]))
                {
                    unin(id(i, j), id(i, j + 1));
                }
            }
        }
        // 記錄不同集合的數量,即最少需要的澆灌次數
        int cnt = 0;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                int fx = find(id(i, j));
                if (vis[fx])
                    continue;
                vis[fx] = 1;
                cnt++;
            }
        }
        cout << cnt << endl;
    }
    return 0;
}

1015:find the most comfortable road(枚舉+Kruskal)

題意:一張圖有 NN 個點,MM 條無向邊,每條邊都有固定的 speedspeed 限制,現在有 QQ 個詢問,每個詢問給定 SSTT,問從 SSTT​ 所有路徑中最大速度與最小速度的差值至少是多少?

範圍:1<N200 , M1000 , Q<11 , speed1e61 < N \le 200~,~M \le 1000~,~Q < 11~,~speed \le 1e6

分析:直接去尋找 SSTT 路徑中最大速度和最低速度不好找,注意到數據範圍不大,可以考慮減少變量。先對邊進行排序,嘗試枚舉路徑中最低速度的邊,因爲是無向邊,可以利用 Kruskal 的做法加邊形成集合,當 SSTT​ 在同一個集合中的時候就退出加邊,此時加的最後一條邊就是最大速度邊,這樣就得到了最大速度與最小速度,更新答案,繼續枚舉最小速度邊。

Code

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

const int MAXN = 200 + 10;
const int MAXM = 1000 + 10;
const int INF = 0x3f3f3f3f;

int n, m, q;
int s, t;

int F[MAXN]; //並查集使用
struct Edge
{
    int u;    //起點
    int v;    //終點
    int w;    //權值
} edge[MAXM]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, int w)
{
    edge[tol].u = u;
    edge[tol].v = v;
    edge[tol++].w = w;
    return;
}

bool cmp(Edge a, Edge b)
{
    //排序函數,將邊按照權值從小到大排序
    return a.w < b.w;
}

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    else
    {
        return F[x] = find(F[x]);
    }
}

int Kruskal(int e)
{
    memset(F, -1, sizeof(F));  // 清空並查集
    int ans = 0;
    for (int i = e; i < tol; i++)
    {
        // Kruskal
        int u = edge[i].u;
        int v = edge[i].v;
        int w = edge[i].w;
        int tOne = find(u);
        int tTwo = find(v);
        if (tOne != tTwo)
        {
            ans = max(ans, w);
            F[tOne] = tTwo;
        }
        // 如果s和t在統一集合中時,說明可以可達
        if (find(s) == find(t) && find(s) != -1)
        {
            return ans;
        }
    }
    return -1;  // 不可達
}

void solve()
{
    int ans = INF;  // 因爲求最小值,故置爲最大值
    for (int i = 0; i < m; i++)  // 枚舉最小速度邊
    {
        int maxV = Kruskal(i);  // 得到最大速度邊
        if (maxV == -1)
            break;
        ans = min(ans, maxV - edge[i].w);  // 更新答案
    }
    if (ans >= INF)
        cout << -1 << endl;
    else
        cout << ans << endl;
}

int main()
{
    while (cin >> n >> m)
    {
        tol = 0;
        for (int i = 0; i < m; i++)
        {
            int u, v, speed;
            cin >> u >> v >> speed;
            addEdge(u, v, speed);
        }
        sort(edge, edge + tol, cmp);  // 只需要排序一次
        cin >> q;
        for (int i = 0; i < q; i++)
        {
            cin >> s >> t;
            solve();
        }
    }
    return 0;
}

1016:Rank of Tetris(離線+並查集縮點+拓撲排序)

題意:有 NN​ 個人,MM​ 個先後順序(包括等號),現在問是否能夠通過這些關係得到唯一的排名(OK),是否得到不唯一排名(UNCERTAIN),以及是否出現關係衝突(CONFLICT)。

範圍:0N1e4 , 0M2e40 \le N \le 1e4~,~0\le M \le 2e4

分析:這道題目如果沒有等號關係的話,那麼就是一道裸的拓撲排序。現在考慮有了等號怎麼處理,等號代表等價關係,可以通過並查集把關係中的等號全部消除掉,有等號關係的點全部縮成一個點,那麼此時圖中就只剩下大於小於號了。而爲了先處理所有的等號,就只能把輸入離線下來,處理完等號再繼續。此時如果出現了等價的點有不等價關係(<,>),則說明衝突;如果沒有,那麼問題就轉化成了只有 <,> 的拓撲問題,當隊列裏面有多個點的時候,說明此時的優先級關係無法確定!隊列裏的點全部處理完之後,如果圖中還有點的話,必定成環,看是否還有入度非 00​ 的點。詳見代碼。

Code

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

const int MAXN = 1e4 + 10;
const int MAXM = 2e4 + 10;

int n, m;

vector<int> E[MAXN];  // 保存每個點的鄰邊
queue<int> que;  // 拓撲排序用

// fa:並查集 inEdge:入度 vis:標記數組 in:離線輸入
int fa[MAXN], inEdge[MAXN], vis[MAXN], in[MAXN][2];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

// 加邊
void addEdge(int u, int v)
{
    E[u].push_back(v);
}

// 初始化
void init()
{
    while (!que.empty())
        que.pop();
    for (int i = 0; i < n; i++)
    {
        E[i].clear();
        fa[i] = i;
        inEdge[i] = vis[i] = 0;
    }
}

int main()
{
    while (cin >> n >> m)
    {
        init();
        int index = 0;
        // 把輸入離線下來,消除等號
        for (int i = 0; i < m; i++)
        {
            int u, v;
            char ch;
            cin >> u >> ch >> v;
            if (ch == '=')
            {
                unin(u, v);  // 縮點
            }
            else if (ch == '>')
            {
                in[index][0] = u, in[index++][1] = v;
            }
            else
            {
                in[index][0] = v, in[index++][1] = u;
            }
        }
        // 處理每個集合的入度
        int flag = 0;
        for (int i = 0; i < index; i++)
        {
            int u = find(in[i][0]), v = find(in[i][1]);
            // 等號關係中出現大於或者小於關係,則衝突
            if (u == v)
            {
                flag = 2;
                break;
            }
            addEdge(u, v);
            inEdge[v]++;
        }
        if (flag == 2)
        {
            cout << "CONFLICT" << endl;
            continue;
        }
        // 將所有入度爲0的集合加入隊列
        for (int i = 0; i < n; i++)
        {
            int fx = find(i);
            if (vis[fx])
                continue;
            vis[fx] = 1;
            if (!inEdge[fx])
                que.push(fx);
        }
        // 拓撲排序
        while (!que.empty())
        {
            // 如果隊列中有多個點,此時優先級關係無法確定
            if (que.size() > 1)
            {
                flag = 1;
            }
            int u = que.front();
            que.pop();
            for (auto v : E[u])
            {
                inEdge[v]--;
                if (inEdge[v] == 0)
                    que.push(v);
            }
        }
        // 如果排序完還存在環的話,說明出現衝突
        for (int i = 0; i < n; i++)
        {
            if (inEdge[find(i)])
            {
                flag = 2;
                break;
            }
        }
        if (flag == 1)
            cout << "UNCERTAIN" << endl;
        else if (flag == 2)
            cout << "CONFLICT" << endl;
        else
            cout << "OK" << endl;
    }
    return 0;
}

1017:Hand in Hand(帶權並查集)

題意:有 NN 個孩子在操場上手牽手,一隻手只能抓住另一隻手,也可以沒有抓,現在有 MM 個孩子抓手的關係,形成的圖代表原來的圖。現在有 NN’ 個孩子復現原來的圖,有 MM​‘ 個抓手的關係,形成的圖代表新圖。現在問這兩張圖是否同構?

範圍:0N,M1e50 \le N,M \le 1e5

分析:首先我們需要知道兩張圖同構是什麼意思,同構指的是兩張圖的形狀一致,即對應的連通塊的形狀一致,而不要求每個點的位置都一致。通過題幹我們可以知道一個人只有兩隻手,頂多有兩個抓手的關係,即度數最大爲 22,那麼每個連通塊要麼形成,要麼就是一條簡單的

對於一條鏈,是不是同構只要判斷鏈上的點個數,對於一個環,同構也只需要判斷環上的點個數。

因此我們維護所有的連通塊的點個數,以及連通塊的形狀(鏈或環),排序後一一比對這些連通塊是否同構即可。詳見代碼。

Code

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

const int MAXN = 1e4 + 10;

int n1, n2, m1, m2;

int fa[2][MAXN];  // 保存兩張圖的並查集

struct Node
{
	int num, type;
	bool operator<(Node other) const
	{
        // 先按照數量排序,再按照形狀排序
		if (num == other.num)
			return type < other.type;
		else
			return num < other.num;
	}
} nodes[2][MAXN];

vector<Node> vec[2];

int find(int id, int x)
{
	return fa[id][x] == x ? x : fa[id][x] = find(id, fa[id][x]);
}

void unin(int id, int x, int y)
{
	int fx = find(id, x), fy = find(id, y);
	if (fx == fy)
	{
		nodes[id][fx].type = 1;
		return;
	}
	fa[id][fx] = fy;
	nodes[id][fy].num += nodes[id][fx].num;
}

int main()
{
	int T;
	cin >> T;
	int kase = 1;
	while (T--)
	{
		vec[0].clear();
		vec[1].clear();
		cin >> n1 >> m1;
        // 初始化
		for (int i = 1; i <= n1; i++)
		{
			fa[0][i] = fa[1][i] = i;
			nodes[0][i].type = nodes[1][i].type = 0;
			nodes[0][i].num = nodes[1][i].num = 1;
		}
		for (int i = 0; i < m1; i++)
		{
			int u, v;
			cin >> u >> v;
			unin(0, u, v);
		}
		cin >> n2 >> m2;
		for (int i = 0; i < m2; i++)
		{
			int u, v;
			cin >> u >> v;
			unin(1, u, v);
		}
		cout << "Case #" << kase++ << ": ";
        // 如果人數或者抓手關係的數量都不對的話肯定不同構
		if (n1 != n2 || m1 != m2)
		{
			cout << "NO" << endl;
			continue;
		}
        // 分別提取出兩張圖的所有連通塊信息
		for (int i = 1; i <= n1; i++)
		{
			for (int j = 0; j < 2; j++)
			{
				int fx = find(j, i);
				if (fx == i)
					vec[j].push_back(nodes[j][i]);
			}
		}
        // 如果連通塊數量不一致,也表示不同構
		if (vec[0].size() != vec[1].size())
		{
			cout << "NO" << endl;
			continue;
		}
        // 對兩張圖中所有連通塊進行排序
		for (int i = 0; i < 2; i++)
		{
			sort(vec[i].begin(), vec[i].end());
		}
		int flag = 1;
		for (int i = 0; i < vec[0].size(); i++)
		{
			Node x = vec[0][i], y = vec[1][i];
            // 如果對應連通塊點的數量不一致則不同構
			if (x.num != y.num)
			{
				flag = 0;
				break;
			}
            // 就算點的數量一致但形狀不一致仍然不同構
			else if (x.type != y.type)
			{
				flag = 0;
				break;
			}
		}
		if (flag)
			cout << "YES" << endl;
		else
			cout << "NO" << endl;
	}
	return 0;
}

1019:Minimal Ratio Tree(暴力枚舉+最小生成樹)

題意:給一張有 NN 個帶權點的完全圖,所有邊權已知,現在要找到圖中包含 MM 個點的樹,且滿足 edge weightnode weight\frac{\sum edge~weight}{\sum node~weight} 最小。

範圍:2MN152 \le M \le N \le 15,其餘數字都是整數

分析:數據範圍很小,因此我們可以暴力枚舉所有選擇 MM 個點的方案,得到 MM 個點之後將這些點獨立出來,在這些點之間跑最小生成樹,這樣就得到了這些點所形成樹的最小邊權和,而枚舉的時候可以計算出選擇點的權值和,更新答案,如果相同的話需要比較字典序,vectorvector 直接使用運算符 <<​ 來進行判斷即可。

Code

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

const int MAXN = 15 + 10;
const int INF = 0x3f3f3f3f;
const int eps = 1e-6;

struct Edge
{
    int u, v, len;
    bool operator<(Edge other) const
    {
        return len < other.len;
    }
};

int n, m;
double ans;
int w[MAXN], g[MAXN][MAXN], fa[MAXN], vis[MAXN];
vector<int> vec, temp;  // 保存最優點集、臨時點集
vector<Edge> E;  // 保存每種m個點的方案的所有鄰邊

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int calc()
{
    // 初始化
    E.clear();
    for (int i = 1; i <= n; i++)
    {
        fa[i] = i;
    }
    // 將這m個點內部的邊保存下來
    for (auto u : temp)
    {
        for (int i = 1; i <= n; i++)
        {
            if (u == i || !vis[i])
                continue;
            E.push_back({u, i, g[u][i]});
        }
    }
    // 跑Kruskal
    sort(E.begin(), E.end());
    int ans = 0;
    for (auto e : E)
    {
        int fx = find(e.u), fy = find(e.v);
        int len = e.len;
        if (fx == fy)
            continue;
        unin(fx, fy);
        ans += len;
    }
    return ans;
}

// now:當前枚舉到的點 select:已經選的數量 sum:總點權
void dfs(int now, int select, int sum)
{
    // 遍歷完所有點
    if (now == n + 1)
    {
        // 正好選擇m個點
        if (select == m)
        {
            // 計算最小邊權和
            int minV = calc();
            // 更新答案
            if (minV * 1.0 / sum < ans)
            {
                ans = minV * 1.0 / sum;
                vec = temp;
            }
            // 因爲是浮點數,所以判斷相等不能直接用 ==
            else if (abs(minV * 1.0 / sum - ans) < eps)
            {
                if (vec < temp)
                {
                    vec = temp;
                }
            }
        }
        return;
    }
    // 選擇該點
    vis[now] = 1;
    temp.push_back(now);
    dfs(now + 1, select + 1, sum + w[now]);
    // 不選擇該點
    vis[now] = 0;
    temp.pop_back();
    dfs(now + 1, select, sum);
}

int main()
{
    while (cin >> n >> m, n + m)
    {
        memset(vis, 0, sizeof(vis));
        vec.clear();
        temp.clear();
        E.clear();
        for (int i = 1; i <= n; i++)
        {
            cin >> w[i];
        }
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                cin >> g[i][j];
            }
        }
        ans = INF;  // 置爲最大值
        dfs(1, 0, 0);
        for (int i = 0; i < vec.size(); i++)
        {
            if (i)
                cout << " ";
            cout << vec[i];
        }
        cout << endl;
    }
    return 0;
}

1022:A Bug’s Life(種類並查集)

題意:假設只有不同性別的臭蟲可以相互作用,現在有 NN 個臭蟲以及 MM​ 個作用關係,要求判斷是否存在不滿足假設的情況。

範圍:1N2000 , M1e61 \le N \le 2000~,~M \le 1e6

分析:這道題目是種類並查集,其實我也不是太會,這題記錄集合中每個點到根節點距離的奇偶性,如果有關係的兩個點在同一個集合中且奇偶性相同,那麼出錯。

Code

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

const int MAXN = 2000 + 10;

int n, m;
int flag;
int fa[MAXN], dis[MAXN];

int find(int x)
{
	if (x == fa[x])
	{
		return x;
	}
	else
	{
		int t = find(fa[x]);
		dis[x] = (dis[x] + dis[fa[x]]) % 2;  // 更新距離
		return fa[x] = t;
	}
}

void unin(int x, int y)
{
	int fx = find(x), fy = find(y);
	if (fx == fy)
	{
		if (dis[x] % 2 == dis[y] % 2)  // 出現衝突
			flag = 1;
	}
	else
	{
		fa[fx] = fy;
		dis[fx] = (dis[x] + dis[y] + 1) % 2;  // 更新距離
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	int kase = 1;
	while (T--)
	{
		flag = 0;
		cin >> n >> m;
		for (int i = 1; i <= n; i++)
		{
			dis[i] = 0;
			fa[i] = i;
		}
		for (int i = 0; i < m; i++)
		{
			int u, v;
			cin >> u >> v;
			unin(u, v);
		}
		cout << "Scenario #" << kase++ << ":" << endl;
		if (flag)
		{
			cout << "Suspicious bugs found!" << endl;
		}
		else
		{
			cout << "No suspicious bugs found!" << endl;
		}
		cout << endl;
	}
	return 0;
}

1023:Segment set(並查集+線段相交)

題意:現在有 NN 條命令,第一種是在二維平面上插入一條線段,第二種是詢問當前某條線段與多少條線段相交。

範圍:N1000N \le 1000

分析:因爲本題只設計到插入線段與查詢,不需要刪除,所以使用並查集就很方便。並查集的部分就不用說了,只要維護一個 numnum 數組保存集合中的線段數量即可。問題就在判斷線段相交的部分,因爲 nn 只有 10001000,所以在添加新的邊的時候可以循環遍歷之前的所有線段判斷是否相交,相交則並起來。判斷線段相交就直接上板子了,具體的原理是啥我也不知道,畢竟我也不懂計算幾何!

Code

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

const int MAXN = 1000 + 10;

int n;

int fa[MAXN], num[MAXN];

struct Point
{ //點
    double x, y;
    Point() {}
    Point(int a, int b)
    { //方便賦值
        x = a;
        y = b;
    }
    void input()
    { //定義輸入函數方便用的時候
        scanf("%lf%lf", &x, &y);
    }
};

struct Line
{ //線段
    Point a, b;
    Line() {}
    Line(Point x, Point y)
    {
        a = x;
        b = y;
    }
    void input()
    {
        a.input();
        b.input();
    }
} line[MAXN];

// 判斷線段相交
bool judge(Point &a, Point &b, Point &c, Point &d)
{
    if (!(min(a.x, b.x) <= max(c.x, d.x) && min(c.y, d.y) <= max(a.y, b.y) && min(c.x, d.x) <= max(a.x, b.x) && min(a.y, b.y) <= max(c.y, d.y))) //這裏的確如此,這一步是判定兩矩形是否相交
        return false;
    double u, v, w, z; //分別記錄兩個向量
    u = (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
    v = (d.x - a.x) * (b.y - a.y) - (b.x - a.x) * (d.y - a.y);
    w = (a.x - c.x) * (d.y - c.y) - (d.x - c.x) * (a.y - c.y);
    z = (b.x - c.x) * (d.y - c.y) - (d.x - c.x) * (b.y - c.y);
    return (u * v <= 0.00000001 && w * z <= 0.00000001);
}

int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
    num[fy] += num[fx];
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n;
        // 初始化
        for (int i = 1; i <= n; i++)
        {
            fa[i] = i;
            num[i] = 1;
        }
        int index = 1;
        for (int i = 0; i < n; i++)
        {
            char op;
            cin >> op;
            // 插入線段
            if (op == 'P')
            {
                line[index++].input();  // 輸入一條邊
                // 判斷是否與之間的線段相交
                for (int i = 1; i < index - 1; i++)
                {
                    if (judge(line[i].a, line[i].b, line[index - 1].a, line[index - 1].b))
                    {
                        unin(i, index - 1);
                    }
                }
            }
            // 查詢
            else
            {
                int x;
                cin >> x;
                int fx = find(x);
                cout << num[fx] << endl;
            }
        }
        if (T)
            cout << endl;
    }
    return 0;
}

1025:Pseudoforest(帶權並查集)

題意:給一張 NN 個點,MM 條邊的圖,邊權爲 CiC_i,求最大僞林,僞林定義爲每個連通塊允許有一個環的圖,僞林的權值爲所有邊權之和。

範圍:0<N1e4 , 0M1e5 , 0<Ci1e40 < N \le 1e4~,~0 \le M \le 1e5~,~0 < C_i \le 1e4

分析:首先明確不能用最大生成樹然後再加邊,這樣只能允許整張圖有一個環,而題目的要求更寬,在一張圖中可以有多個連通塊,每個連通塊可以有一個環。這道題可以用並查集來做,首先把邊按照邊權從大到小進行排序,對每條邊,如果兩個端點在一個集合內,且沒有環,那麼就可以加入這條邊形成環,如果不在一個集合內,只要其中一個沒有環就可以進行合併,處理完所有邊之後輸出答案即可。

Code

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

const int MAXN = 1e4 + 10;
const int MAXM = 1e5 + 10;

int n, m;

struct Edge
{
    int u, v, len;
    bool operator<(Edge other) const
    {
        return len > other.len;  // 邊權從大到小排序
    }
} edges[MAXM];

int fa[MAXN], loop[MAXN];  // 標記是否有環

int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

int main()
{
    while (cin >> n >> m, n + m)
    {
        for (int i = 0; i <= n; i++)
        {
            fa[i] = i;
            loop[i] = 0;
        }
        for (int i = 0; i < m; i++)
        {
            int u, v, c;
            cin >> u >> v >> c;
            edges[i] = {u, v, c};
        }
        sort(edges, edges + m);
        int sum = 0;
        for (int i = 0; i < m; i++)
        {
            int u = edges[i].u, v = edges[i].v, len = edges[i].len;
            int fx = find(u), fy = find(v);
            if (fx != fy)
            {
                // 不能兩個都有環
                if (!loop[fx] || !loop[fy])
                {
                    sum += len;
                    fa[fx] = fy;
                    if (loop[fx])
                    {
                        loop[fy] = 1;
                    }
                }
            }
            else
            {
                // 不能有環
                if (!loop[fx])
                {
                    sum += len;
                    loop[fx] = 1;
                }
            }
        }
        cout << sum << endl;
    }
    return 0;
}

1026:Junk-Mail Filter(並查集刪點)

題意:有 NN​ 個命令,第一種命令 M X YM~X~Y​ 表示標記 XX​YY​ 是同一類,第二種命令 S XS~X​ 表示清除 XX​ 與其所有同類之間的關係。所有命令處理完之後輸出不同類的數量。

範圍:1N1e5 , 1M1e61 \le N \le 1e5~,~1 \le M \le 1e6

分析:對於第一種命令,可以用並查集很容易地實現,但是第二種命令,刪除對與並查集來說就很難實現了。但是我們並不需要真正從集合中刪除掉某個點,可以給要刪除的點創建一個新的點(重獲新生),實現一個映射關係,代表該點的最新版本。最後記錄每個點的最新版本所在集合有多少個就可以了。

Code

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

const int MAXN = 1e5 + 10;

int n, m, idx;

// actual[i]表示點i的最新版本點的編號
int fa[MAXN], actual[MAXN], vis[MAXN];

int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    fa[fx] = fy;
}

// 刪點
void delet(int x)
{
    actual[x] = idx;  // 分配一個新的點
    fa[idx] = idx;  // 設置父親結點爲自己
    idx++;  // 編號增加
}

void init()
{
    memset(vis, 0, sizeof(vis));
    idx = n;
    for (int i = 0; i <= n; i++)
    {
        fa[i] = i;
        actual[i] = i;  // 一開始的最新版本就是自己
    }
}

int main()
{
    // 數據量看起來有點大,還是關閉流同步好了,也可以scanf
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int kase = 1;
    while (cin >> n >> m, n + m)
    {
        init();
        for (int i = 0; i < m; i++)
        {
            char op;
            cin >> op;
            if (op == 'M')
            {
                int u, v;
                cin >> u >> v;
                unin(actual[u], actual[v]);  // 將最新版本的點合併
            }
            else
            {
                int u;
                cin >> u;
                delet(u);  // 刪除點不能用最新版本
            }
        }
        // 計算最新版本點所在集合的個數
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            int fx = find(actual[i]);
            if (!vis[fx])
            {
                ans++;
                vis[fx] = 1;
            }
        }
        cout << "Case #" << kase++ << ": " << ans << endl;
    }
    return 0;
}

三、hard

1018:Portal(Kruskal+離線)

題意:定義一條路徑需要消耗的能量爲最長邊的長度,兩點之間需要消耗的能量爲兩點之間所有路徑消耗的能量中的最小值。有 NN 個點,MM 條無向邊,以及 QQ 個詢問,詢問現在有 LL 的能量,可以走多少種道路。

範圍:1<N1e3 , 0<M5e4 , 0<Q1e4 , 0L1e81 < N \le 1e3~, ~0 < M \le5e4~,~0 < Q \le 1e4~,~ 0 \le L \le 1e8

分析:兩點之間的 TT 的定義:兩點之間有多條路徑,路徑上最長的邊稱爲 tt,那麼 T=min(t1,t2,...)T = min(t1, t2, ...)。邊都是無向邊,只需要考慮權值,那麼我們從小到大枚舉邊。如果兩個端點不在同一個集合內,說明原先不可到達,那麼現在有這條邊,一個集合中的所有點可以通過這條邊到達另一個集合中的所有點。所有加入這條邊之後新增的可走路徑爲集合1的點數*集合2的點數。如果兩個端點已經在同一個集合內,此時增加的邊權不可能更新 TT,所以無視掉,這樣記錄以每條邊爲T的路徑數量。考慮到詢問的數量很多,所以將詢問離線下來,直接輸出答案即可

Code

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

const int MAXN = 1e5 + 10;
const int MAXM = 5e5 + 10;

int n, m, q, ans;

struct Edge
{
    int u, v, len;
    bool operator<(Edge other) const
    {
        return len < other.len;
    }
} edges[MAXM];

// num:集合內點數量 out:按輸入順序保存的答案
int fa[MAXN], num[MAXN], out[MAXN];
// res:從小到大L對應的答案 qry:離線的輸入詢問
pair<int, int> res[MAXN], qry[MAXN];

int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    ans += num[fx] * num[fy];
    fa[fx] = fy;
    num[fy] += num[fx];
}

int main()
{
    while (cin >> n >> m >> q)
    {
        // 初始化
        for (int i = 1; i <= n; i++)
        {
            fa[i] = i;
            num[i] = 1;
        }
        // 保存邊
        for (int i = 0; i < m; i++)
        {
            int u, v, len;
            cin >> u >> v >> len;
            edges[i] = {u, v, len};
        }
        // 排序
        sort(edges, edges + m);
        // 得到從小到大各個能量能夠走的方案數
        ans = 0;
        for (int i = 0; i < m; i++)
        {
            Edge e = edges[i];
            int fx = find(e.u), fy = find(e.v);
            int len = e.len;
            unin(fx, fy);
            res[i] = {len, ans};
        }
        // 離線輸入
        for (int i = 0; i < q; i++)
        {
            cin >> qry[i].first;
            qry[i].second = i;
        }
        sort(qry, qry + q);
        // 根據輸入順序調整輸出答案的順序
        int index = 0;  // index一直往右走,時間是單獨的O(n)
        for (int i = 0; i < q; i++)
        {
            int ask = qry[i].first;
            // 找到輸入的能量能夠走的最大方案數
            while (index + 1 < m && res[index + 1].first <= ask)
                index++;
            out[qry[i].second] = res[index].second;
        }
        // 輸出答案
        for (int i = 0; i < q; i++)
        {
            cout << out[i] << endl;
        }
    }
    return 0;
}

1020:Qin Shi Huang’s National Road System(次小生成樹)

題意:有 NN​ 個城市,每個城市都有自己的座標 X,YX,Y​ 以及人口 PP​,秦始皇要求最小生成樹(MSTMST​),現在可以使用魔法讓任意一條道路免費,問 min(AB)min(\frac{A}{B})​ 是多少,AA​ 表達魔法道路連接兩個城市的人口和,BB​ 表示當前 MSTMST​ 總長度減去該道路的長度。

範圍:2<N1000 , 0X,Y1000 , 0<P<1e52 < N \le 1000~,~ 0 \le X,Y \le 1000~,~0 < P < 1e5

分析:數據範圍允許我們使用 O(n2)O(n^2) 的算法。因此我們可以雙重循環枚舉所有的邊,假設該邊的費用變成 00,那麼要求的就是剩下圖的最小生成樹。當然不可以再針對每一種情況再重新跑 MSTMST,我們考慮加入一條免費的邊對原圖最小生成樹的影響。在 MSTMST 的基礎上加入新邊的時候,NN 個點 NN 條邊,必定成環,爲了重新變成一顆樹,我們需要在這個環上找到權值最大的邊刪除掉,那麼此時就是加入新邊後的 MSTMST 了。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wDDj7INK-1581306344932)(assets/1581235523998.png)]

那麼現在問題就轉換成了如果找到原 MSTMST 中所有點對 (i,j)(i, j) 路徑上最大的邊權,這就是個經典問題了,這也是次小生成樹解決的問題,因此直接上次小生成樹板子就可以了。詳見代碼。

Code

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

const int MAXN = 1000 + 10;
const int INF = 0x3f3f3f3f;

int arr[MAXN][3];  // 保存橫縱座標以及人口

int used[MAXN][MAXN];
double G[MAXN][MAXN], path[MAXN][MAXN];  // path[i][j]就是MST中i和j路徑上的最大邊權
int vis[MAXN], per[MAXN];
double dis[MAXN];
int n, m;

void init()
{
    memset(used, 0, sizeof(used));
    memset(path, 0, sizeof(path));
    memset(G, INF, sizeof(G));
}

// 次小生成樹 O(n^2)
double Prim(int s)
{
    memset(vis, 0, sizeof(vis));
    double sum = 0;
    for (int i = 1; i <= n; i++)
    {
        dis[i] = G[s][i];
        per[i] = s;
    }
    vis[s] = 1;
    for (int i = 1; i < n; i++)
    {
        double mint = INF;
        int u = s;
        for (int j = 1; j <= n; j++)
        {
            if (!vis[j] && dis[j] < mint)
            {
                mint = dis[j];
                u = j;
            }
        }
        vis[u] = 1;
        sum += mint;
        used[u][per[u]] = used[per[u]][u] = 1;
        for (int j = 1; j <= n; j++)
        {
            if (vis[j] && j != u) //j到u路徑上的最大值
                path[j][u] = path[u][j] = max(path[j][per[u]], dis[u]);
            if (!vis[j])
            {
                if (dis[j] > G[u][j])
                {
                    dis[j] = G[u][j];
                    per[j] = u;
                }
            }
        }
    }
    return sum;
}

// 計算歐氏距離
double distance(int i, int j)
{
    double x1 = arr[i][0], y1 = arr[i][1], x2 = arr[j][0], y2 = arr[j][1];
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        init();
        cin >> n;
        for (int i = 1; i <= n; i++)
        {
            cin >> arr[i][0] >> arr[i][1] >> arr[i][2];
        }
        for (int i = 1; i <= n; i++)
        {
            for (int j = i; j <= n; j++)
            {
                G[i][j] = G[j][i] = distance(i, j);
            }
        }
        double sum = Prim(1);  // sum爲原MST的值
        double ans = 0;
        for (int i = 1; i <= n; i++)
        {
            for (int j = i + 1; j <= n; j++)
            {
                // 枚舉邊(i,j),計算i和j路徑上的最大邊權
                double maxV = path[i][j];
                // 更新答案
                ans = max(ans, (arr[i][2] + arr[j][2]) * 1.0 / (sum - maxV));
            }
        }
        cout << fixed << setprecision(2) << ans << endl;
    }
    return 0;
}

1021:Genghis Khan the Conqueror(期望+最小生成樹+並查集)

題意:給一張 NN​ 個點,MM​ 條邊的無向圖,現在有 QQ​ 種可能性,每種可能性給一條邊賦予了新的權值 CiC_i​(保證大於原值),但是實際只會有一個可能性發生,現在問任意可能發生後該圖 MSTMST​ 的期望是多少。

範圍:1N3000  0MNN  1Q1e4  Ci1e71 \le N \le 3000~,~0 \le M \le N*N~,~1 \le Q \le 1e4~,~C_i \le 1e7

分析:這道題目要在 QQ 條路上選一條增加權值,要求的是隨機選擇的情況下 MSTMST 的期望值。首先我們知道在原圖上有 MSTMST,如果增加權值的邊不在 MSTMST 上,那麼對答案沒有影響;如果在 MSTMST 上,那麼我們需要去找是否有其他的更優解。

尋找更優解這個步驟,看網上說可以用樹形 dpdp 找最優替代邊,但是還有並查集的做法,本題的樹形 dpdp 看不太懂,還是用並查集好了。

首先我們需要把增加邊權的邊從 MSTMST 中刪掉,那麼我們就把 MSTMST 分割成兩棵樹,可以認爲是兩個集合,那麼我們可以從小到大遍歷所有邊,嘗試連上這條邊,如果連完之後兩個集合變成一個,那麼這個就是最優解,否則的話我只能只能認栽選擇新增邊權的邊了。

(說實話這樣的寫法感覺很暴力,但是確實是能過的…)

Notice:第一次遇見並查集爆棧…,在代碼頭加入手動加棧還是沒用,因此瞭解了一下非遞歸的並查集路徑壓縮。

Code

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 3000 + 10;
const int MAXM = MAXN * MAXN;

int n, m, q, k;

int vis[MAXN][MAXN];
int F[MAXN]; //並查集使用

struct Edge
{
	int u;					 //起點
	int v;					 //終點
	int w;					 //權值
} edge[MAXM], newEdge[MAXN]; //存儲邊的信息

int tol; //邊數,加邊前賦值爲0

void addEdge(int u, int v, int w)
{
	edge[tol].u = u;
	edge[tol].v = v;
	edge[tol++].w = w;
	return;
}

bool cmp(Edge a, Edge b)
{
	//排序函數,將邊按照權值從小到大排序
	return a.w < b.w;
}

int find(int x)
{
	int k, j, r;
	r = x;
	while (r != F[r])
		r = F[r];
	k = x;
	while (k != r)
	{
		j = F[k];
		F[k] = r;
		k = j;
	}
	return r;
}

int Kruskal(int n)
{
	for (int i = 0; i <= n; i++)
		F[i] = i;
	sort(edge, edge + tol, cmp);
	int ans = 0;
	k = 0;
	for (int i = 0; i < tol; i++)
	{
		int u = edge[i].u;
		int v = edge[i].v;
		int w = edge[i].w;
		int tOne = find(u);
		int tTwo = find(v);
		if (tOne != tTwo)
		{
			k++;
			vis[u][v] = vis[v][u] = k;
			newEdge[k] = {u, v, w};
			ans += w;
			F[tOne] = tTwo;
		}
	}
	return ans;
}

void init()
{
	tol = 0;
	memset(vis, 0, sizeof(vis));
}

int main()
{
	while (~scanf("%d%d", &n, &m), n + m)
	{
		// 初始化
		init();
		// 加邊
		for (int i = 0; i < m; i++)
		{
			int u, v, len;
			scanf("%d%d%d", &u, &v, &len);
			addEdge(u, v, len);
		}
		// 計算MST,並且把MST中的邊記錄下來
		int sum = Kruskal(n);
		scanf("%d", &q);
		// ans保存所有情況的MST總和
		double ans = 0;
		for (int i = 0; i < q; i++)
		{
			int u, v, c;
			scanf("%d%d%d", &u, &v, &c);
			// 如果這條邊在原MST中
			if (vis[u][v])
			{
				// 清空並查集
				for (int i = 0; i <= n; i++)
					F[i] = i;
				// oldc保存這兩點在原MST中的邊權
				int oldc = 0;
				// 將原MST分割成兩個集合,即去除了邊(u, v)
				for (int i = 1; i <= k; i++)
				{
					int x = newEdge[i].u, y = newEdge[i].v;
					int c = newEdge[i].w;
					if (vis[u][v] == i)
					{
						oldc = c;
						continue;
					}
					int fx = find(x), fy = find(y);
					F[fx] = fy;
				}
				// 記錄是否找到更優解
				int update = 0;
				// 使用並查集加邊,如果某次加完邊u和v所在的兩個集合合併了,那麼就找到更優解
				for (int i = 0; i < m && edge[i].w < c; i++)
				{
					int x = edge[i].u, y = edge[i].v;
					if (vis[x][y])
						continue;
					int fx = find(x), fy = find(y);
					F[fx] = fy;
					fx = find(u), fy = find(v);
					if (fx == fy)
					{
						ans += sum - oldc + edge[i].w;
						update = 1;
						break;
					}
				}
				// 沒有找到更優解,那麼就選擇原來的邊,直接加上c-old
				if (!update)
				{
					ans += sum - oldc + c;
				}
			}
			// 不在原MST中,跟答案無關,直接加上答案
			else
			{
				ans += sum;
			}
		}
		// 輸出平均值
		printf("%.4f\n", ans / q);
	}
	return 0;
}

1024:Code Lock(並查集+快速冪)

題意:給定字符串長度 NN​ ,只有小寫字母,以及 MM​ 個可以操作的區間,對於可以操作的區間能夠將裏面的所有字母同時變成字母序後一個的字母,如 ab , zaa-b~,~z-a​。如果一個字符串能夠通過操作這些區間任意次之後變成另外一個字符串,那麼就認爲這兩個字符串是等價的。現在問所有長度爲 NN​ 的不等價的字符串一共有多少個?

範圍:1N1e7 , 0M10001\le N \le 1e7~,~0 \le M \le 1000

分析:數據範圍特別大,我們考慮找規律。

假設字符串長度爲 33,所有長度爲 33 的字符串數量爲 26326^3,有一個操作區間爲 [1,1][1,1],那麼相當於第一個字母可以變成 aza-z 中的任意字母。此時第一個字母不論是什麼都只能看做是同一個字母,所以只能變化後面的字母才能產生不等價的字符串,那麼不等價的字符串數量爲 26226^2​

如果有一個操作區間爲 [1,2][1, 2],此時前兩位 ababbcbccdcdzaza 是等價的,這裏共 2626 種組合,且前兩位每一種組合 aaaaababzzzz 都有其他 2525 個組合等價,因此只能保留一個。所以區間 [1,2][1, 2] 中可以操作的組合數爲 262261=261\frac{26^2}{26^1} = 26^1,而其他的區間可以任意選,總的不等價字符串數量爲 261261=26226^1*26^1 = 26^2

有沒有發現什麼?繼續看,如果有一個操作區間爲 [1,3][1, 3],此時前三位的所有組合數爲 26326^3,但是每種組合都有其他 2525 個組合等價,因此總數還是要除以 2626,即 26226^2,也就是長度爲 33 的所有不等價字符串數量。

我們發現當只有一個區間的時候,不論區間大小如何,總數只會除以 2626!那如果多個區間的時候呢?

如果多個區間是相互獨立的話,那麼上面的推理一樣使用,設區間個數爲 kk​,那麼長度爲 nn​ 的不等價字符串總數爲 26nk26^{n-k}​

如果多個區間有交集的話,假設有 22 個區間相交,那麼 11 個字符串經過變化可以變成 26226^2 個字符串,即每種情況都有 26226^2 個字符串等價,因此總數爲 26n226^{n-2},推到一般情況就是 26nk26^{n-k},跟區間之間相互獨立的結果是一致的。

那麼現在我們需要知道的就是可以操作的區間個數,但是其中還存在無效的區間。比如 ① 區間重複的情況,出現了 [1,6][1, 6],又出現 [1,6][1, 6],後者就是無效的; ② 出現 [a,b][a, b][b+1,c][b+1, c],又出現了 [a,c][a, c],那麼此時後者是無效的;

怎麼處理無效區間呢?可以使用並查集,對每個區間 [a,b][a, b]bba1a-1 放到一個集合中,爲什麼是 a1a-1 呢?因爲這樣可以把相鄰的區間放入一個集合中,處理了無效情況①的同時也處理了②,每次合併的時候有效的可以操作區間個數增加,最後跑快速冪即可。

Code

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

const int MAXN = 1e7 + 10;
const int MOD = 1000000007;

int n, m, ans;

int fa[MAXN];

int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void unin(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx == fy)
        return;
    ans--;  // 也可以是操作區間個數++,區別不大
    fa[fx] = fy;
}

// 快速冪
long long pow(int a, int n)
{
    long long ans = 1;
    long long pingfang = a;
    while (n != 0)
    {
        if (n % 2 == 1)
        {
            (ans *= pingfang % MOD) %= MOD; // 遇到二進制中的1時 說明需要取
        }
        (pingfang *= pingfang % MOD) %= MOD; // pingfang不斷往上翻
        n /= 2;
    }
    return ans;
}

int main()
{
    while (cin >> n >> m)
    {
        for (int i = 0; i <= n; i++)
        {
            fa[i] = i;
        }
        ans = n;  // 也可以置爲0,表示有效區間的個數
        for (int i = 0; i < m; i++)
        {
            int a, b;
            cin >> a >> b;
            unin(a-1, b);  // 合併,更新答案
        }
        cout << pow(26, ans) % MOD << endl;
    }
    return 0;
}

【END】感謝觀看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章