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)​ O(1) 的时间内对一类相同元素找到代表元以及两类不同元素的合并,并查集所花费的时间可以说是常数级别的。注意,使用并查集的前提是集合中的所有元素之间的联系都是双向的且具有传递性,如果仅仅只是单向的关系或者不具有传递性,那么不能使用并查集。

而最小生成树求的就是一张图中将所有点连通且总权值最大的树,它分为两种常用的算法:Kruskal 以及 Prim,前者在稀疏图(点多边少)中的表现更好,后者在稠密图(点少边多)种的表现更好。Kruskal 的时间复杂度为 O ( E l o g E ) O(ElogE) O(ElogE),而 Prim 的时间复杂度为 O ( N 2 ) O(N^2) O(N2),Prim 若使用堆优化则时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。( E E E 表示边数, N N N 表示点数)

Kruskal 的实现需要使用并查集,而 Prim 则不需要。

一、easy

1001:How Many Tables(并查集)

题意:摆桌子,只有相互认识的人才能坐在同一张桌子旁,认识具有传递性,如果 A ​ A​ A 认识 B ​ B​ B,而 B ​ B​ B 认识 C ​ C​ C,那么 A ​ A​ A 就认识 C ​ C​ C。现在有 N ​ N​ N 个人, M ​ M​ M 种关系,问至少需要多少张桌子?

范围: 1 ≤ N , M ≤ 1000 1 \le N,M \le 1000 1N,M1000

分析:典型的并查集背景,将有关系的人放在同一个集合中,最后遍历所有人记录不同集合的个数即可。

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:小希的迷宫(并查集)

题意:有 N ​ N​ N 个房间,若干条通道双向连接两个房间,问现在的布局是否满足任意两个房间都可以相通,并且路径是唯一的。

范围: 1 ≤ N ≤ 1 e 5 1 \le N \le 1e5 1N1e5

分析:首先,道路是无向边,房间的连通关系满足传递性。现在问任意两个房间是否都相通,则可以用并查集判断是否所有的房间都在同一个集合中。题目还要求路径是唯一的,就是说图中不能出现环,并查集也可以判断,当加入的边两个端点已经属于同一个集合的时候,那么就说明出现了环。

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. 只有一个入度为 0 0 0 的点
  2. 其余所有点的入度都为 1 1 1
  3. 所有点都连通

无向边,且连通关系满足传递性,可以使用并查集。本题跟 1002 1002 1002 类似,只需要在其基础上加入入度数组 i n E d g e inEdge inEdge 判断入度条件即可。详见代码。

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(并查集)

题意:一个房间中有很多个男孩,有 N ​ N​ N 种朋友关系,可以指定任意个男孩出去,要保证最后留在房间里面的男孩都是直接或间接的朋友,问最后留在房间里面的男孩数量最多可以是多少?

范围: 1 ≤ N ≤ 1 e 5 1 \le N \le 1e5 1N1e5,男孩的数量可能有 10000000 10000000 10000000

分析:这题是真的暴力啊,这种 1 e 7 ​ 1e7​ 1e7 的数据范围多组输入都可以这样暴力,大拇指好吧。

首先朋友关系是双向的,并且是传递的,可以使用并查集。这个问题的本质就是问这些男孩之中最大的相互认识的团体的大小,即集合的大小。因此可以使用并查集计算每个集合中的元素个数,那么答案就是所有集合中最大的个数。

Notice:直接用 c i n cin cin 会超时的!用 s c a n f scanf scanf 或者关闭流同步!而且注意 n = = 0 ​ n==0​ n==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)

题意:有 N N N 个村庄, M ​ M​ M 条已经修好的边,现在问还需要修哪些边,才能在最小的花费下实现连通所有的村庄。

范围: 1 ≤ N ≤ 100   ,   0 ≤ M ≤ N ∗ ( N − 1 ) 2 1 \le N \le 100~,~0\le M \le \frac{N*(N-1)}{2} 1N100  0M2N(N1)

分析:明显是要我们求最小生成树,但是考虑到已经有现成的边可以白嫖,所以在求的时候就可以把已经连通的村庄整体看成一个村庄,可以使用并查集让他们在同一个集合里面。既然使用并查集了,还需要求最小生成树,那就自然用 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:畅通工程(并查集)

题意:有 N ​ N​ N 个城市, M ​ M​ M 条道路已经铺好,现在问还需要多少条边才能让所有的城市相互连通。

范围: 1 ≤ N ≤ 1000 1 \le N \le 1000 1N1000 M M M 的范围未指出

分析:道路是双向的,连通关系具有传递性,可以使用并查集。首先已经连通的城市整体可以看成是一个大城市,如果这样处理完之后图中只剩下孤立的点,表示一个个大城市,现在要让所有城市连通,就是要让剩下的这些点连通。假设剩下 A A A 个大城市,那么我们只需要 A − 1 A-1 A1 条边就可以形成一颗树,实现连通。

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:还是畅通工程(最小生成树)

题意:有 N ​ N​ N 个城市,给出任意两个城市之间的距离,求让这些城市连通的最小边权总和。

范围: 1 ≤ N ≤ 100 1 \le N \le 100 1N100

分析:最小生成树板子题。

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:畅通工程(最小生成树)

题意:有 N ​ N​ N 个城市, M ​ M​ M 条双向边,求让这些城市连通的最小边权总和。

范围: 1 ≤ N ≤ 100 ​ 1 \le N \le 100​ 1N100

分析:还是最小生成树板子题,不存在 M S T MST MST 则输出 “?” 。

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:畅通工程再续(最小生成树)

题意:有 N ​ N​ N 个城市的二维坐标,可以在任意两个城市之间建设道路,求让这些城市连通的最小道路长度总和。

范围: 1 ≤ N ≤ 100 1 \le N \le 100 1N100

分析:最小生成树板子题,把边权的地方改成 double,双重循环遍历所有村庄计算欧氏距离进行加边,最后输出答案的时候 ∗ 100 *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)

题意:有 N ​ N​ N 个城市的,给出所有城市之间的道路代价以及道路是否修建状态,求让这些城市连通的最小代价。

范围: 1 ≤ N ≤ 100 1 \le N \le 100 1N100

分析:跟 1005 1005 1005 差不多,对于已经修建好的道路,把两个端点放到同一个集合中,再使用 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)

题意:有 N N N 个城市的, M M M 条可以修建的道路, K ​ K​ K 个已经连通的城市群,求让这些城市连通的最小代价。

范围: 3 ≤ N ≤ 500   ,   0 ≤ m ≤ 25000   ,   0 ≤ K ≤ 100 3 \le N \le 500~,~0 \le m \le 25000~,~0 \le K \le 100 3N500 , 0m25000 , 0K100

分析:还是跟 1005 ​ 1005​ 1005 差不多,对于这些城市群,把里面的所有城市放到同一个集合中,再进行 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 < 27 ​ 1 < N < 27​ 1<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(最小生成树)

题意:给 N N N 滴墨水的二维坐标,问将这些墨水连在一起的最短距离和。

范围: 0 < N ≤ 100 0 < N \le 100 0<N100

分析:跟 1009 1009 1009 类似,不多说了,看代码。

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(细节+并查集)

题意:有一块 N ∗ M ​ N*M​ NM 的田地,里面有各种管子,类型从 A ​ A​ A K ​ K​ K,通过管子连通的土地只需要一次浇灌,现在问灌溉整块田地需要多少次浇灌?

范围: 1 ≤ N , M ≤ 50 1 \le N,M \le 50 1N,M50

分析:问题并不难,就是要想怎么写比较好写,细节比较多。

我这里说说我的写法,管子的种类很多,但是管子的属性只包括:向上连通、向下连通、向左连通以及向右连通这四种。因此我们可以保存具有某种属性的管子有哪些!

① 若当前的管子具有向上的属性,且上面的管子具有向下的属性,则连通

② 若当前的管子具有向下的属性,且下面的管子具有向上的属性,则连通

③ 若当前的管子具有向左的属性,且左边的管子具有向右的属性,则连通

④ 若当前的管子具有向右的属性,且右边的管子具有向左的属性,则连通

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)

题意:一张图有 N N N 个点, M M M 条无向边,每条边都有固定的 s p e e d speed speed 限制,现在有 Q Q Q 个询问,每个询问给定 S S S T T T,问从 S S S T ​ T​ T 所有路径中最大速度与最小速度的差值至少是多少?

范围: 1 < N ≤ 200   ,   M ≤ 1000   ,   Q < 11   ,   s p e e d ≤ 1 e 6 1 < N \le 200~,~M \le 1000~,~Q < 11~,~speed \le 1e6 1<N200 , M1000 , Q<11 , speed1e6

分析:直接去寻找 S S S T T T 路径中最大速度和最低速度不好找,注意到数据范围不大,可以考虑减少变量。先对边进行排序,尝试枚举路径中最低速度的边,因为是无向边,可以利用 Kruskal 的做法加边形成集合,当 S S S T ​ T​ T 在同一个集合中的时候就退出加边,此时加的最后一条边就是最大速度边,这样就得到了最大速度与最小速度,更新答案,继续枚举最小速度边。

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(离线+并查集缩点+拓扑排序)

题意:有 N ​ N​ N 个人, M ​ M​ M 个先后顺序(包括等号),现在问是否能够通过这些关系得到唯一的排名(OK),是否得到不唯一排名(UNCERTAIN),以及是否出现关系冲突(CONFLICT)。

范围: 0 ≤ N ≤ 1 e 4   ,   0 ≤ M ≤ 2 e 4 0 \le N \le 1e4~,~0\le M \le 2e4 0N1e4 , 0M2e4

分析:这道题目如果没有等号关系的话,那么就是一道裸的拓扑排序。现在考虑有了等号怎么处理,等号代表等价关系,可以通过并查集把关系中的等号全部消除掉,有等号关系的点全部缩成一个点,那么此时图中就只剩下大于小于号了。而为了先处理所有的等号,就只能把输入离线下来,处理完等号再继续。此时如果出现了等价的点有不等价关系(<,>),则说明冲突;如果没有,那么问题就转化成了只有 <,> 的拓扑问题,当队列里面有多个点的时候,说明此时的优先级关系无法确定!队列里的点全部处理完之后,如果图中还有点的话,必定成环,看是否还有入度非 0 ​ 0​ 0 的点。详见代码。

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(带权并查集)

题意:有 N N N 个孩子在操场上手牵手,一只手只能抓住另一只手,也可以没有抓,现在有 M M M 个孩子抓手的关系,形成的图代表原来的图。现在有 N N N’ 个孩子复现原来的图,有 M ​ M​ M‘ 个抓手的关系,形成的图代表新图。现在问这两张图是否同构?

范围: 0 ≤ N , M ≤ 1 e 5 0 \le N,M \le 1e5 0N,M1e5

分析:首先我们需要知道两张图同构是什么意思,同构指的是两张图的形状一致,即对应的连通块的形状一致,而不要求每个点的位置都一致。通过题干我们可以知道一个人只有两只手,顶多有两个抓手的关系,即度数最大为 2 2 2,那么每个连通块要么形成,要么就是一条简单的

对于一条链,是不是同构只要判断链上的点个数,对于一个环,同构也只需要判断环上的点个数。

因此我们维护所有的连通块的点个数,以及连通块的形状(链或环),排序后一一比对这些连通块是否同构即可。详见代码。

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(暴力枚举+最小生成树)

题意:给一张有 N N N 个带权点的完全图,所有边权已知,现在要找到图中包含 M M M 个点的树,且满足 ∑ e d g e   w e i g h t ∑ n o d e   w e i g h t \frac{\sum edge~weight}{\sum node~weight} node weightedge weight 最小。

范围: 2 ≤ M ≤ N ≤ 15 2 \le M \le N \le 15 2MN15,其余数字都是整数

分析:数据范围很小,因此我们可以暴力枚举所有选择 M M M 个点的方案,得到 M M M 个点之后将这些点独立出来,在这些点之间跑最小生成树,这样就得到了这些点所形成树的最小边权和,而枚举的时候可以计算出选择点的权值和,更新答案,如果相同的话需要比较字典序, v e c t o r vector vector 直接使用运算符 < ​ <​ < 来进行判断即可。

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(种类并查集)

题意:假设只有不同性别的臭虫可以相互作用,现在有 N N N 个臭虫以及 M ​ M​ M 个作用关系,要求判断是否存在不满足假设的情况。

范围: 1 ≤ N ≤ 2000   ,   M ≤ 1 e 6 1 \le N \le 2000~,~M \le 1e6 1N2000 , M1e6

分析:这道题目是种类并查集,其实我也不是太会,这题记录集合中每个点到根节点距离的奇偶性,如果有关系的两个点在同一个集合中且奇偶性相同,那么出错。

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(并查集+线段相交)

题意:现在有 N N N 条命令,第一种是在二维平面上插入一条线段,第二种是询问当前某条线段与多少条线段相交。

范围: N ≤ 1000 N \le 1000 N1000

分析:因为本题只设计到插入线段与查询,不需要删除,所以使用并查集就很方便。并查集的部分就不用说了,只要维护一个 n u m num num 数组保存集合中的线段数量即可。问题就在判断线段相交的部分,因为 n n n 只有 1000 1000 1000,所以在添加新的边的时候可以循环遍历之前的所有线段判断是否相交,相交则并起来。判断线段相交就直接上板子了,具体的原理是啥我也不知道,毕竟我也不懂计算几何!

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(带权并查集)

题意:给一张 N N N 个点, M M M 条边的图,边权为 C i C_i Ci,求最大伪林,伪林定义为每个连通块允许有一个环的图,伪林的权值为所有边权之和。

范围: 0 < N ≤ 1 e 4   ,   0 ≤ M ≤ 1 e 5   ,   0 < C i ≤ 1 e 4 0 < N \le 1e4~,~0 \le M \le 1e5~,~0 < C_i \le 1e4 0<N1e4 , 0M1e5 , 0<Ci1e4

分析:首先明确不能用最大生成树然后再加边,这样只能允许整张图有一个环,而题目的要求更宽,在一张图中可以有多个连通块,每个连通块可以有一个环。这道题可以用并查集来做,首先把边按照边权从大到小进行排序,对每条边,如果两个端点在一个集合内,且没有环,那么就可以加入这条边形成环,如果不在一个集合内,只要其中一个没有环就可以进行合并,处理完所有边之后输出答案即可。

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(并查集删点)

题意:有 N ​ N​ N 个命令,第一种命令 M   X   Y ​ M~X~Y​ M X Y 表示标记 X ​ X​ X Y ​ Y​ Y 是同一类,第二种命令 S   X ​ S~X​ S X 表示清除 X ​ X​ X 与其所有同类之间的关系。所有命令处理完之后输出不同类的数量。

范围: 1 ≤ N ≤ 1 e 5   ,   1 ≤ M ≤ 1 e 6 1 \le N \le 1e5~,~1 \le M \le 1e6 1N1e5 , 1M1e6

分析:对于第一种命令,可以用并查集很容易地实现,但是第二种命令,删除对与并查集来说就很难实现了。但是我们并不需要真正从集合中删除掉某个点,可以给要删除的点创建一个新的点(重获新生),实现一个映射关系,代表该点的最新版本。最后记录每个点的最新版本所在集合有多少个就可以了。

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+离线)

题意:定义一条路径需要消耗的能量为最长边的长度,两点之间需要消耗的能量为两点之间所有路径消耗的能量中的最小值。有 N N N 个点, M M M 条无向边,以及 Q Q Q 个询问,询问现在有 L L L 的能量,可以走多少种道路。

范围: 1 < N ≤ 1 e 3   ,   0 < M ≤ 5 e 4   ,   0 < Q ≤ 1 e 4   ,   0 ≤ L ≤ 1 e 8 1 < N \le 1e3~, ~0 < M \le5e4~,~0 < Q \le 1e4~,~ 0 \le L \le 1e8 1<N1e3 , 0<M5e4 , 0<Q1e4 , 0L1e8

分析:两点之间的 T T T 的定义:两点之间有多条路径,路径上最长的边称为 t t t,那么 T = m i n ( t 1 , t 2 , . . . ) T = min(t1, t2, ...) T=min(t1,t2,...)。边都是无向边,只需要考虑权值,那么我们从小到大枚举边。如果两个端点不在同一个集合内,说明原先不可到达,那么现在有这条边,一个集合中的所有点可以通过这条边到达另一个集合中的所有点。所有加入这条边之后新增的可走路径为集合1的点数*集合2的点数。如果两个端点已经在同一个集合内,此时增加的边权不可能更新 T T T,所以无视掉,这样记录以每条边为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(次小生成树)

题意:有 N ​ N​ N 个城市,每个城市都有自己的坐标 X , Y ​ X,Y​ X,Y 以及人口 P ​ P​ P,秦始皇要求最小生成树( M S T ​ MST​ MST),现在可以使用魔法让任意一条道路免费,问 m i n ( A B ) ​ min(\frac{A}{B})​ min(BA) 是多少, A ​ A​ A 表达魔法道路连接两个城市的人口和, B ​ B​ B 表示当前 M S T ​ MST​ MST 总长度减去该道路的长度。

范围: 2 < N ≤ 1000   ,   0 ≤ X , Y ≤ 1000   ,   0 < P < 1 e 5 2 < N \le 1000~,~ 0 \le X,Y \le 1000~,~0 < P < 1e5 2<N1000 , 0X,Y1000 , 0<P<1e5

分析:数据范围允许我们使用 O ( n 2 ) O(n^2) O(n2) 的算法。因此我们可以双重循环枚举所有的边,假设该边的费用变成 0 0 0,那么要求的就是剩下图的最小生成树。当然不可以再针对每一种情况再重新跑 M S T MST MST,我们考虑加入一条免费的边对原图最小生成树的影响。在 M S T MST MST 的基础上加入新边的时候, N N N 个点 N N N 条边,必定成环,为了重新变成一颗树,我们需要在这个环上找到权值最大的边删除掉,那么此时就是加入新边后的 M S T MST MST 了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDDj7INK-1581306344932)(assets/1581235523998.png)]

那么现在问题就转换成了如果找到原 M S T MST MST 中所有点对 ( i , j ) (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(期望+最小生成树+并查集)

题意:给一张 N ​ N​ N 个点, M ​ M​ M 条边的无向图,现在有 Q ​ Q​ Q 种可能性,每种可能性给一条边赋予了新的权值 C i ​ C_i​ Ci(保证大于原值),但是实际只会有一个可能性发生,现在问任意可能发生后该图 M S T ​ MST​ MST 的期望是多少。

范围: 1 ≤ N ≤ 3000   ,   0 ≤ M ≤ N ∗ N   ,   1 ≤ Q ≤ 1 e 4   ,   C i ≤ 1 e 7 1 \le N \le 3000~,~0 \le M \le N*N~,~1 \le Q \le 1e4~,~C_i \le 1e7 1N3000  0MNN  1Q1e4  Ci1e7

分析:这道题目要在 Q Q Q 条路上选一条增加权值,要求的是随机选择的情况下 M S T MST MST 的期望值。首先我们知道在原图上有 M S T MST MST,如果增加权值的边不在 M S T MST MST 上,那么对答案没有影响;如果在 M S T MST MST 上,那么我们需要去找是否有其他的更优解。

寻找更优解这个步骤,看网上说可以用树形 d p dp dp 找最优替代边,但是还有并查集的做法,本题的树形 d p dp dp 看不太懂,还是用并查集好了。

首先我们需要把增加边权的边从 M S T MST MST 中删掉,那么我们就把 M S T MST MST 分割成两棵树,可以认为是两个集合,那么我们可以从小到大遍历所有边,尝试连上这条边,如果连完之后两个集合变成一个,那么这个就是最优解,否则的话我只能只能认栽选择新增边权的边了。

(说实话这样的写法感觉很暴力,但是确实是能过的…)

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(并查集+快速幂)

题意:给定字符串长度 N ​ N​ N ,只有小写字母,以及 M ​ M​ M 个可以操作的区间,对于可以操作的区间能够将里面的所有字母同时变成字母序后一个的字母,如 a − b   ,   z − a ​ a-b~,~z-a​ ab , za。如果一个字符串能够通过操作这些区间任意次之后变成另外一个字符串,那么就认为这两个字符串是等价的。现在问所有长度为 N ​ N​ N 的不等价的字符串一共有多少个?

范围: 1 ≤ N ≤ 1 e 7   ,   0 ≤ M ≤ 1000 1\le N \le 1e7~,~0 \le M \le 1000 1N1e7 , 0M1000

分析:数据范围特别大,我们考虑找规律。

假设字符串长度为 3 3 3,所有长度为 3 3 3 的字符串数量为 2 6 3 26^3 263,有一个操作区间为 [ 1 , 1 ] [1,1] [1,1],那么相当于第一个字母可以变成 a − z a-z az 中的任意字母。此时第一个字母不论是什么都只能看做是同一个字母,所以只能变化后面的字母才能产生不等价的字符串,那么不等价的字符串数量为 2 6 2 ​ 26^2​ 262

如果有一个操作区间为 [ 1 , 2 ] [1, 2] [1,2],此时前两位 a b ab ab b c bc bc c d cd cd z a za za 是等价的,这里共 26 26 26 种组合,且前两位每一种组合 a a aa aa a b ab ab z z zz zz 都有其他 25 25 25 个组合等价,因此只能保留一个。所以区间 [ 1 , 2 ] [1, 2] [1,2] 中可以操作的组合数为 2 6 2 2 6 1 = 2 6 1 \frac{26^2}{26^1} = 26^1 261262=261,而其他的区间可以任意选,总的不等价字符串数量为 2 6 1 ∗ 2 6 1 = 2 6 2 26^1*26^1 = 26^2 261261=262

有没有发现什么?继续看,如果有一个操作区间为 [ 1 , 3 ] [1, 3] [1,3],此时前三位的所有组合数为 2 6 3 26^3 263,但是每种组合都有其他 25 25 25 个组合等价,因此总数还是要除以 26 26 26,即 2 6 2 26^2 262,也就是长度为 3 3 3 的所有不等价字符串数量。

我们发现当只有一个区间的时候,不论区间大小如何,总数只会除以 26 26 26!那如果多个区间的时候呢?

如果多个区间是相互独立的话,那么上面的推理一样使用,设区间个数为 k ​ k​ k,那么长度为 n ​ n​ n 的不等价字符串总数为 2 6 n − k ​ 26^{n-k}​ 26nk

如果多个区间有交集的话,假设有 2 2 2 个区间相交,那么 1 1 1 个字符串经过变化可以变成 2 6 2 26^2 262 个字符串,即每种情况都有 2 6 2 26^2 262 个字符串等价,因此总数为 2 6 n − 2 26^{n-2} 26n2,推到一般情况就是 2 6 n − k 26^{n-k} 26nk,跟区间之间相互独立的结果是一致的。

那么现在我们需要知道的就是可以操作的区间个数,但是其中还存在无效的区间。比如 ① 区间重复的情况,出现了 [ 1 , 6 ] [1, 6] [1,6],又出现 [ 1 , 6 ] [1, 6] [1,6],后者就是无效的; ② 出现 [ a , b ] [a, b] [a,b] [ b + 1 , c ] [b+1, c] [b+1,c],又出现了 [ a , c ] [a, c] [a,c],那么此时后者是无效的;

怎么处理无效区间呢?可以使用并查集,对每个区间 [ a , b ] [a, b] [a,b] b b b a − 1 a-1 a1 放到一个集合中,为什么是 a − 1 a-1 a1 呢?因为这样可以把相邻的区间放入一个集合中,处理了无效情况①的同时也处理了②,每次合并的时候有效的可以操作区间个数增加,最后跑快速幂即可。

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】感谢观看!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值