题解:SP5150 JMFILTER - Junk-Mail Filter(并查集学习笔记))
SP5150 题解
题面
前置知识
- 并查集。
- 拥有 SPOJ 账号。
思路
首先这题很容易想到是并查集。(没学过的看后记。)
对于一个点 i,我们可以直接设 pai 为 i 的父节点。(跟的父节点设为自己。)
然后就是查询一个元素属于哪个集合的操作。(这很基础。)
long long find(long long x){
if(pa[x]==x) return x;
return pa[x]=find(pa[x]);//路径压缩,pa直接赋值为代表元素。
}
接着,对于合并的操作,也是基础的,没有改动。
只要让 x 的树根作为 y 的树根的子节点。
void merge(long long x, long long y) {
pa[find(x)]=find(y);
}
接下来就是初始化操作了,我们发现有了删除操作后,就需要“假点”这个东西,就是 pa0,1,2,⋯,n−1 为真点,pan,n+1,n+2⋯,2×n−1 为假点,pa2×n,2×n+1,⋯,2×n+m 是为了统计删除而存在的点。
void init(long long n, long long m) {
cnt=2*n;
ans=0;
for(int i=1; i<=n; i++) pa[i]=n+i;
for(int i=n+1; i<=cnt+m+1; i++) pa[i]=i;
memset(b,0,sizeof(b));
}
删除就很简单了,直接将要删除的节点指向一个新的节点即可,它就可以被分离。
void dele(int x){
pa[x]=cnt++;
}
cnt 为当前已用的点的数量。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
const int MN=10000005;
long long pa[MN],vis[MN],cnt,ans,n,m,x,y;
//pa[i]为自己的父亲节点。
char ch;
void init(long long n, long long m) {
cnt=2*n;
ans=0;
for(int i=0; i<n; i++) pa[i]=n+i;
for(int i=n; i<=cnt+m; i++) pa[i]=i;
memset(vis,0,sizeof(vis));
}
long long find(long long x){
if(pa[x]==x) return x;
return pa[x]=find(pa[x]);//路径压缩,pa直接赋值为代表元素。
}
void merge(long long x, long long y) {
pa[find(x)]=find(y);//让 x 的树根作为 y 的树根的子节点。
}
void dele(int x){
pa[x]=cnt++;
}
int main(){
long long i=0;
while(1){
scanf("%lld %lld",&n,&m);
if(!n&&!m) break;//要是两个都是0,结束程序。
init(n,m);//预处理
i++;
while(m--){
cin>>ch;
if(ch=='M'){
scanf("%lld %lld",&x,&y);
merge(x,y);//合并
}
else{
scanf("%lld",&x);
dele(x);//删除这个点。
}
}
for(int k=0; k<n; k++){
int tmp=find(k);
if(!vis[tmp]) ans++,vis[tmp]=1;
}
printf("Case #%lld: %lld\n",i,ans);
}
return 0;
}
帮你AC彻底
如果你和曾经的我和我的好同学一样一直 UKE,但是又注册不了 SPOJ 的账号,那就请点这里按教程注册。
总结
- 编号从 0 开始到 n−1,别错了。(曾经我可爱的同学就错了。)
- 要有 SPOJ 的账号。(来自 50 次 UKE 的倔强,和那之后的绝望。)
- 并查集的基础算法及其单点删除。
后记(并查集学习笔记)
定义(功能)
并查集是一种可以动态维护若干个不重叠的集合,并支持合并于查询的结构。
思维实现
可以想到,使用一颗树形结构来存储每个集合树根是集合的代表元素。
于是我们可以用一个 pa 数组来记录这个森林,fax 为 x 的父节点,查询是只要递归求即可,两个集合合并时将 paroot1=root2。
然而,上述做法时间复杂度堪忧,于是就需要路径压缩优化或者按秩合并。(这种这里不多讲,基本优化路径压缩就够了。)
其实很简单,只要在查询的时候八方问过的每个节点都直接指向树根即可,因为我们只关心每个集合对应的树形结构的根节点是什么,而不是它的形态。
时间复杂度 O(logn)
代码实现
- 初始化:
每个元素都独立形成一个集合,可以看成有若干个节点数为 1 的树,于是就将每个节点的父节点设为自己。
for(int i=1; i<=n; i++) pa[i]=i;
- 查询操作
若 x 是树根,则 x 就是集合代表,否则递归访问 pax 直至根节点。
long long find(long long x){
if(pa[x]==x) return x;
return pa[x]=find(pa[x]);//路径压缩,pa直接赋值为代表元素。
}
- 合并操作
让 x 的树根作为 y 的树根的子节点。
void merge(long long x, long long y) {
pa[find(x)]=find(y);//让 x 的树根作为 y 的树根的子节点。
}
好拉,你应该已经学会了吧,那就往上看做这道题吧。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现