SZTU2024暑期集训并查集题解

A题:【模板】并查集 洛谷 - P3367

题目大意

不知道题目大意的这辈子有了(

AC代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
int f[N];
int find(int x)
{
    if (f[x] == x)
        return x;
    return f[x] = find(f[x]);
}

void merge(int x, int y) // 将x和y所在集合进行合并
{
    int fx = find(x), fy = find(y); // 查询根节点
    if (fx != fy)                   // 如果根节点不同
        f[fy] = fx;                 // 将fy合并到fx上
}

void solve()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 1; i <= m; i++)
    {
        int x, y, z;
        cin >> z >> x >> y;
        if (z == 1)
            merge(x, y);
        else if (z == 2)
        {
            int fx = find(x), fy = find(y);//找到x和y的祖先
            if (fx == fy)
                cout << 'Y' << endl;
            else
                cout << 'N' << endl;
        }
    }
    return;
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);//关闭同步流,可以略微加速
    solve();
    return 0;
}

B题:亲戚 洛谷 - P1551

题目大意

有 n 个人,给出 m 对亲戚关系,亲戚的亲戚还是亲戚,然后进行 p 次询问,判断某二人是否是亲戚

模版题。

AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 7;
int f[N];
int find(int x)
{
	if (f[x] == x)
		return x;
	return f[x] = find(f[x]);
}
void merge(int x, int y)
{
	int fx = find(x), fy = find(y);
	if (fx != fy)
		f[fx] = fy;
}
int main()
{
	int n, m, p;
	cin >> n >> m >> p;
	for (int i = 1; i <= n; i++)
		f[i] = i;
	for (int i = 1; i <= m; i++)
	{
		int a, b;
		cin >> a >> b;
		merge(a, b);
	}
	for (int i = 1; i <= p; i++)
	{
		int a, b;
		cin >> a >> b;
		if (find(a) == find(b))
			cout << "Yes" << endl;
		else
			cout << "No" << endl;
	}
	return 0;
}

C题:P3420 [POI2005] SKA-Piggy Banks

题目大意

有 n 个存钱罐,第 i 个存钱罐里有第 x 个存钱罐的钥匙,可以打碎存钱罐或者通过打碎存钱罐获得的钥匙打开其它存钱罐,问最少打碎几个存钱罐即可打开所有存钱罐

解题思路

这题依然是并查集,第 i 个存钱罐可以打开第 x 个存钱罐,说明需要把 i 和 x 合并

AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 7;
int f[N], g[N], h[N];
#define endl '\n'
int find(int x)
{
    if (f[x] == x)
        return x;
    return f[x] = find(f[x]);
}
void merge(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx != fy)
        f[fy] = fx;
}
void solve()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        f[i] = i; // 初始化
    for (int i = 1; i <= n; i++)
    {
        cin >> g[i];
        merge(i, g[i]); // 第i个存钱罐能打开第g[i]个,所以合并二者
    }
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        if (h[find(i)] == 0)
        {
            ans++;
            h[find(i)] = 1; // 枚举有几个集合
        }
    }
    cout << ans << endl;
    return;
}
signed main()
{
    solve();
    return 0;
}

D题:朋友 洛谷 - P2078

题目大意

一家公司全是男的,另一家全是女的,男的和男的交朋友,女的和女的交朋友,最后小明又和小美交朋友,通过他俩其他人可以谈恋爱(小美和小明也要谈恋爱),问小明和小美可以帮多少男女牵上手

……公司?监狱!!
题目把男女分开来,我们的合并查询操作当然也可以分男女处理。这里就男女分别用两个函数实现合并和查询,这个思路简单点

AC代码

#include <iostream>
#include <string>
#include <map>
using namespace std;
const int N = 2e4 + 7;
int f[N], g[N];
int find1(int x)
{
	if (f[x] == x)
		return x;
	return f[x] = find1(f[x]);
} // 找男朋友
int find2(int x)
{
	if (g[x] == x)
		return x;
	return g[x] = find2(g[x]);
} // 找女朋友
void merge1(int x, int y)
{
	int fx = find1(x), fy = find1(y);
	if (fx < fy)
		f[fy] = fx;
	else if (fy < fx)
		f[fx] = fy;
} // 男合并
void merge2(int x, int y)
{
	int fx = find2(x), fy = find2(y);
	if (fx < fy)
		g[fy] = fx;
	else if (fy < fx)
		g[fx] = fy;
} // 女合并
void solve()
{
	int n, m, p, q;
	cin >> n >> m >> p >> q;
	for (int i = 1; i <= max(m, n); i++)
		f[i] = i, g[i] = i; // 初始化
	int x, y;
	for (int i = 1; i <= p; i++)
	{
		cin >> x >> y;
		merge1(x, y);
	}
	for (int i = 1; i <= q; i++)
	{
		cin >> x >> y;
		merge2(abs(x), abs(y));
	}
	int cnt1 = 0, cnt2 = 0; // 表示小明和小美的小团体各有多少人
	for (int i = 1; i <= n; i++)
	{
		if (find1(i) == 1)
			cnt1++; // 祖先是1就cnt1++,女生同理
	}
	for (int i = 1; i <= m; i++)
		if (find2(i) == 1)
			cnt2++;
	cout << min(cnt1, cnt2) << endl;
	return;
}
signed main()
{
	solve();
	return 0;
}

E题:一中校运会之百米跑

题目大意

和模版题唯一的区别就是节点变成了字符串,实现字符串的并查集操作。这里使用STL容器中的map来实现

AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 7;
map<string, string> f; // 用字符串实现并查集
string find(string x)
{
	if (f[x] == x)
		return x;
	return f[x] = find(f[x]);
}
void merge(string x, string y)
{
	string fx = find(x), fy = find(y);
	if (fx != fy)
		f[fx] = fy;
}
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		string s;
		cin >> s;
		f[s] = s;
	}
	for (int i = 1; i <= m; i++)
	{
		string a, b;
		cin >> a >> b;
		merge(a, b);
	}
	int p;
	cin >> p;
	for (int i = 1; i <= p; i++)
	{
		string a, b;
		cin >> a >> b;
		if (find(a) == find(b))
			cout << "Yes." << endl;
		else
			cout << "No." << endl;
	}
	return 0;
}

F题:家谱 洛谷 - P2814

题目大意

给定数个字符串,根据第一个字符的不同实现合并和查询操作,和A题差不多,难点在于实现字符串的并查集,这里使用STL中的map结构实现

AC代码

#include <iostream>
#include <string>
#include <map>
using namespace std;
map<string, string> p; // 使用map,避免使用数字使问题复杂化
                       // p[s1]=s2表示s1的父亲是s2
string find(string x)
{
    if (p[x] == x)
        return x;
    return p[x] = find(p[x]);
}

void solve()
{
    string s;
    string father; // 表示上一个父亲节点
    while (cin >> s && s != "$")
    {
        string s1 = s.substr(1); // 截取下标从1到末尾的子串,获得人名
        if (p[s1] == "")         // 首次出现的名字,祖先是它自己
            p[s1] = s1;
        if (s[0] == '#')
        {
            father = s1;
        }
        else if (s[0] == '+')
        {
            p[s1] = find(father); // 合并
        }
        else if (s[0] == '?')
        {
            string s2 = find(s1); // 搜索祖先
            cout << s1 << ' ' << s2 << endl;
        }
    }
    return;
}
signed main()
{
    solve();
    return 0;
}

G题:修复公路 洛谷 - P1111

题目大意

给定村庄数和道路数,道路给定修复完成的时间,问最快什么时候所有村庄连接完成

考虑到合并与时间有关,使用结构体按时间排序,具体实现方法如下

AC代码

#include <iostream>
#include <string>
#include <map>
#include <algorithm>
using namespace std;
const int N = 1e3 + 7;
int f[N];
struct road
{
    int u, v, t; // 分别代表连接的两个村庄和修复完成的时间
};
int find(int x)
{
    if (f[x] == x)
        return x;
    return f[x] = find(f[x]);
}
void merge(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx < fy)
        f[fy] = fx;
    if (fy < fx)
        f[fx] = fy; // 让小的数字当祖先
}
bool cmp(road a, road b) // 用来给结构体排序
{
    return a.t < b.t;
}
void solve()
{
    int n, m;
    cin >> n >> m;
    road p[m + 1];
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 1; i <= m; i++)  cin >> p[i].u >> p[i].v >> p[i].t;
    sort(p + 1, p + m + 1, cmp); // 结构体按时间排序
    
    bool flag = false;           // 判断是否全部合并完成
    for (int i = 1; i <= m; i++)
    {
        merge(p[i].u, p[i].v);
        for (int j = 2; j <= n; j++)
        {
            if (find(j) != 1)//因为小的数字当祖先,所以只要有祖先不是1就说明没连完
            {
                flag = false;
                break;
            }
            flag = true;
        }
        if (flag)
        {
            cout << p[i].t;
            break;
        }
    }
    
    if (!flag)
        cout << -1;
    return;
}
signed main()
{
    solve();
    return 0;
}

H题:New Friends ABC350D

题目大意

给定一张 n 个节点和 m 条边的无向图。

若图中存在三元组 (a,b,c) 满足 a 和 b 有直接连边且 b 与 c 有直接连边且 a 和 c 没有直接连边,就在 a 与 c 之间连一条边。

求上述操作的最大执行次数。

解题思路

先补充图论的概念:
完全图
也称简单完全图。假设一个图有n个顶点,那么如果任意两个顶点之间都有边的话,该图就称为完全图。易知完全图总共有n*(n-1)/2条边
连通图
图中任意两点相互可达的图
补图在这里插入图片描述

根据题意,当所有边连接完成后,最终会出现若干个连通图,求将各个连通图补成数个完全图所需的边数。由以上概念可知所求边数就是各个完全图的总边数减去已知边数 m ,也就是求各个补图的边数

AC代码

#include <iostream>
#include <string>
#include <map>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 2e5 + 7;
int f[N];
ll p[N]; // p[i]表示以i为根节点的树的节点数,结果偏大,记得long long 
int find(int x)
{
    if (f[x] == x)
        return x;
    return f[x] = find(f[x]);
}
void merge(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx != fy)
        f[fx] = fy;
}
void solve()
{
    int n, m;
    cin >> n >> m;
    int x, y;
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 1; i <= m; i++)
    {
        cin >> x >> y;
        merge(x, y);
    }
    ll res = 0;//res为完全图的边数
    for (int i = 1; i <= n; i++)
    {
        p[find(i)]++;//根结点++
    }
    for (int i = 1; i <= n; i++)
    {
        if (p[i])//这个if其实可有可无,可以自己推一下
            res += p[i] * (p[i] - 1) / 2;
    }
    cout << res - m;
    return;
}
signed main()
{
    solve();
    return 0;
}

I题:集合

题目大意

给定区间 [a,b] ,给定下限值 p ,一开始每个整数都属于各自的集合。每次你需要选择两个属于不同集合的整数,如果这两个整数拥有大于等于 p 的公共质因数,那么把它们所在的集合合并。重复如上操作,直到没有可以合并的集合为止。

解题思路

首先要筛素数,然后把素数在 [a,b] 中的整数倍合并即可

AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 7;
int f[N], g[N], h[N];
#define endl '\n'
int find(int x)
{
    if (f[x] == x)
        return x;
    return f[x] = find(f[x]);
}
void merge(int x, int y)
{
    int fx = find(x), fy = find(y);
    if (fx != fy)
        f[fy] = fx;
}
void solve()
{
    int a, b, p;
    cin >> a >> b >> p;
    for (int i = a; i <= b; i++)
        f[i] = i; // 初始化
    for (int i = 2; i <= b; i++)
    {
        if (g[i] == 0)
        {
            for (int j = 2 * i; j <= b; j += i)
            {
                g[j] = 1; // 筛素数,如果g[j]=1表示j不是素数
            }
        }
    }
    for (int i = p; i <= b; i++)
    {
        if (!g[i]) // 如果i是素数的话
        {
            for (int j = i; j + i <= b; j += i)
            {
                if (j >= a)
                    merge(j, j + i); // 将素数的整数倍全部merge
            }
        }
    }
    int ans = 0;
    for (int i = a; i <= b; i++)
    {
        if (h[find(i)] == 0)
        {
            ans++; // 每有一个集合,ans++
            h[find(i)] = 1;
        }
    }
    cout << ans << endl;
    return;
}
signed main()
{
    solve();
    return 0;
}

J题:P1197 [JSOI2008] 星球大战

题目大意

一个星系有 n 个星球,星球之间有 m 条通道,邪恶の势力要摧毁其中 k 个星球。
题目会告诉你摧毁星球的顺序,需要你得出每次摧毁星球前后,这个星系总共有几个集合

解题思路

这题可以直接做,但是会发现每次摧毁完一个星球之后都要一个个枚举到底有几个点是连通的,这样做会导致时间复杂度很高,TLE是必然的(反正我没试过)

考虑正向做题难度大,不如思考逆向做题

假设现在星系已经被摧毁完了,再把时间往回拨,重新连接最后一个星球,倒数第二个星球,……,会发现实际上就是在进行“加点”操作。

所以思路是,先把要被摧毁的点放一边,把不会被摧毁的点先合并好,然后再倒序把被摧毁的星球与之前已有的点进行合并。每次合并成功就代表集合数-1,所以只需要在merge函数中添加res–就好了。

由于题目是要正序输出,所以用一个ans数组储存res的值,来实现正序输出

AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 7;                   // 题目没给k范围,2e5+7会爆空间,所以开1e6
int f[N], g[N], h[N], p[N], ans[N], res; // res是集合数
vector<int> q[N];                        // 储存被攻击的星球原本连接的其它星球
set<int> s;                              // 储存将要被攻击的星球,set自带二分搜索
#define endl '\n'
int findd(int x) // find函数名与set容器自带的find冲突,改名为findd
{
    if (f[x] == x)
        return x;
    return f[x] = findd(f[x]);
}
void merge(int x, int y)
{
    int fx = findd(x), fy = findd(y);
    if (fx != fy)
    {
        f[fy] = fx, res--; // 每次合并成功代表减少了一个集合
    }
}
void solve()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) // 初始化
        f[i] = i;
    // 初始所有星球孤立
    for (int i = 1; i <= m; i++)
    {
        cin >> g[i] >> h[i]; // g[i]和h[i]表示二者是要连接的星球
        g[i]++, h[i]++;      // 恢复到习惯上的1~n,不使用0~n-1
    }
    int k;
    cin >> k;
    for (int i = 1; i <= k; i++)
    {
        cin >> p[i];
        p[i]++;         // 同理
        s.insert(p[i]); // 在s中插入p[i]
    }
    res = n - k; // 这里res表示的是所有攻击完成之后的星球个体数,而非集合总数

    // 下面开始合并s以外的星球
    for (int i = 1; i <= m; i++)
    {
        auto x = s.find(g[i]), y = s.find(h[i]); // 使用set自带的二分搜索
        if (x != s.end())         // 如果找到了,即如果g[i]是要被攻击的星球
        {
            q[g[i]].push_back(h[i]); // 就在g[i]的数组里加入h[i],表示后面g[i]要连接h[i]
        }
        if (y != s.end())
        {
            q[h[i]].push_back(g[i]);
        }
        if (x == s.end() && y == s.end()) // 如果g[i]和h[i]都不是要被攻击的星球
        {
            merge(g[i], h[i]); // 就合并二者
        }
    }
    // 这个时候res就表示被完全攻击之后的集合数了
    // 下面开始合并s中的星球
    for (int i = k; i >= 1; i--)
    {
        ans[i] = res; // 储存每个星球恢复之后的集合数

        for (auto x : q[p[i]]) // 遍历,x是p[i]被攻击前原本连接的星球
        {
            auto y = s.find(x);
            if (y == s.end() && findd(x) != findd(p[i]))
            // 如果y不是之后要攻击的目标,且p[i]和x还未合并就合并二者
            {
                merge(p[i], x);
            }
        }
        s.erase(p[i]); // p[i]已经完成所有连接,从s中移除出去

        res++; // 每完成一次大循环,就新加入一个点,也就是增加了一个集合,所以res++
    }

    cout << res << endl; // 最后一个res没有被储存
    for (int i = 1; i <= k; i++)
        cout << ans[i] << endl;
    return;
}
signed main()
{
    solve();
    return 0;
}
  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值