目录
普通并查集
模板
poj1611
poj2524
poj2236
Kruskal算法。
poj1861
带权并查集:
poj1182
poj1703
poj2492
poj1988
hdu3047
hdu3038
poj2912
poj1733
poj1417/xujcoj1016
并查集,有n个元素的集合问题中,我们通常是在开始让每个元素构成一个单元素集合,然后按一定顺序将所属同一组的元素所在集合合并,中间还要查询元素在哪个集合,这时候我们用并查集来解决
并查集用一个元素来作为整个集合的代表,可以成为根或者祖先。
每次要初始化祖先为自己
有两个操作,一个是查找元素的祖先,另一个合并两个集合
具体代码
#include
const int maxN = 10000;
int pre[maxN];//pre[i]:i的祖先是pre[i]
//查找x的祖先
int find(const int &x) {
int r = x;
while (pre[r] != r)r = pre[r];//如果他的上一级不是自己则继续往上找
return r;
}
//合并x,y的集合
void unite(int x, int y) {
int a = find(x), b = find(y);//查找x,y的祖先
if (a == b)return;//如果相同就说明已经在一个集合了,不用合并了
pre[a] = b;//把x所在的集合合并到y所在的集合,也就是把x的祖先连到y的祖先
}
当然这样还是可以优化的,如果数据很极端的话,这个并查集就会退化成一个单链表,就失去了我们直接查找祖先的意义了
具体优化:
路径压缩,数据大的别用递归
#include
const int maxN = 10000;
int pre[maxN];
int find(const int &x) {
int j, k = x, r = x;
while (pre[r] != r)r = pre[r];//找祖先
//路径压缩
while (k != r) {
j = pre[k];//先保存一下上一级
pre[k] = r;//让上一级改成祖先
k = j;//继续改上一级
}
return r;
}
//递归版
int find(int x) {
if (pre[x] == x)return x;
int root = find(pre[x]);
return pre[x] = root;
}
按秩合并(初始化时要额外初始化r数组为一个数字,多少都行,一样就可以了,一般都是0或者1)
#include
const int maxN = 10000;
int pre[maxN], r[maxN];//r表示秩
int find(const int &x) {
int j, k = x, r = x;
while (pre[r] != r)r = pre[r];//找祖先
//路径压缩
while (k != r) {
j = pre[k];//先保存一下上一级
pre[k] = r;//让上一级改成祖先
k = j;//继续改上一级
}
return r;
}
int unite(int x, int y) {
x = find(x), y = find(y);
if (x == y)return;
if (r[x] < r[y])pre[x] = y;//把秩少的元素合并到秩多的
else {
pre[y] = x;
if (r[x] == r[y])++r[x];//如果相同则+1
}
}
这里要说个细节,很多代码不是写r而是rank,要注意c++自己也有一个rank,会产生冲突。
解决办法:1.去掉using namespace std; 2.换变量名
这两个优化一般就够了
-----------------------------------------------------------------
下面做几道题练习一下(最下面还有kruskal算法和带权并查集,和普通并查集不一样,别直接右上角了23333)
题目大意:有n个人和m个组,有一个人0号感染了sars,和感染了sars的或者是怀疑感染了sars的人同组则认为整组都被感染了,问怀疑几个人感染了sars
解法:多开一个数组用来存每个集合的人数,接受数据的时候将每组的第一个人作为根来代表整组并进行合并整组的其他元素,合并的时候把元素的个数也加过去,最后输出一下0所在的集合的人数就可以了
#include
const int maxN = 30000;
int pre[maxN], r[maxN], num[maxN];
int find(const int &x) {
int j, k = x, r = x;
while (pre[r] != r)r = pre[r];
//路径压缩
while (k != r) {
j = pre[k];
pre[k] = r;
k = j;
}
return r;
}
void unite(int x, int y) {
x = find(x), y = find(y);
if (x == y)return;
//秩合并
if (r[x]0) {
for (int i = 0; i
------------------------------------------------
题目大意:有n个人,都有信仰,接下来有m对整数x和y,表示x和y是同一个信仰,问一共有几种信仰
解法:先假设有ans个信仰,每发现相同信仰就合并两个人并--ans,最后输出ans就可以了
#include
const int maxN = 50001;
int pre[maxN], r[maxN], n, ans;
int find(const int &x) {
int j, k = x, r = x;
while (pre[r] != r)r = pre[r];
while (k != r) {
j = pre[k];
pre[k] = r;
k = j;
}
return r;
}
void unite(int x, int y) {
x = find(x), y = find(y);
if (x == y)return;
--ans;
if (r[x]0) {
++cnt;
for (int i = 1; i <= n; ++i) {//初始化
pre[i] = i;
r[i] = 1;
}
ans = n;
while (m--) {
scanf("%d%d", &a, &b);
unite(a, b);
}
printf("Case %d: %d\n", cnt, ans);
}
}
--------------------------------------------------------------
题目大意:有n个电脑,坏了,要试图修好并连接他们,连接两台电脑它们有两种方式,1.2台电脑电脑都修好了并且距离小于d 2.2台电脑都连着另一台电脑。输出n和d,接着输入n台电脑的左边,最后多组输入“O p"代表修理p,“S p q"代表问p和q能不能连接
解法:每修理一次就遍历所有的电脑,看看能不能连接,如果可以则合并到一个集合,输入S的时候只要看是不是一个集合的就可以了
#include
#include
const int maxN=1002;
int pre[maxN],r[maxN];
int find(const int &x){
int j,k=x,r=x;
while(pre[r]!=r)r=pre[r];
while(k!=r){
j=pre[r];
pre[k]=r;
k=j;
}
return r;
}
void unite(int x,int y){
x=find(x),y=find(y);
if(x==y)return;
if(r[x]
----------------------------------------------------------------
用来找到最小生成树。具体操作就是先去掉所有的边,留下点,将所有的边按价值从小到大排序,然后每次加边到图上去,要注意如果产生了回路则不能用这条边,当加到了点数-1的时候完成。
Kruskal算法出来的最小生成树不唯一
如何判断有没有回路靠的就是并查集,端点u,v如果属于同一个集合则必产生回路(证明略),所以每次加边要判断是不是同一个集合的,如果是则不能加
-------------------------------------------------------------------
题意:n个点和m条边,问价值最大的边,和最小生成树边的个数,最后输出所有的边,(注意样例是错的)
#include
#include
using namespace std;
const int maxN = 1001;
class Edge {
public:
int u, v, price;
Edge(int u, int v, int price) :u(u), v(v), price(price) {}
bool operator <(const Edge& t)const {
return price > t.price;
}
};
int pre[maxN], r[maxN], n, m, ans[maxN][2];
int find(const int& x) {
int p = x;
while (p != pre[p])p = pre[p];
int adjust = x;
while (adjust != pre[adjust]) {
int temp = pre[adjust];
pre[adjust] = p;
adjust = temp;
}
return p;
}
void unite(int x, int y) {
x = find(x), y = find(y);
if (x == y)return;
if (r[x] < r[y])pre[x] = y;
else {
pre[y] = x;
if (r[x] == r[y])++r[x];
}
}
priority_queue q;
void kruskal() {
int maxPrice = 0, cnt = 0, result = 0;
while (!q.empty()) {
int u, v, w;
u = q.top().u;
v = q.top().v;
w = q.top().price;
q.pop();
if (find(u) == find(v)) {
continue;
}
unite(u, v);
maxPrice = w > maxPrice ? w : maxPrice;
result += w;
ans[cnt][0] = u;
ans[cnt][1] = v;
++cnt;
}
if (cnt != n - 1)result = -1;
printf("%d\n%d\n", maxPrice, cnt);
for (int i = 0; i < cnt; ++i)printf("%d %d\n", ans[i][0], ans[i][1]);
}
int main() {
while (scanf("%d%d", &n, &m) != EOF) {
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
q.push(Edge(u, v, w));
//q.push(Edge(v, u, w));
}
for (int i = 1; i <= n; ++i) {
pre[i] = i;
r[i] = 1;
}
kruskal();
}
return 0;
}
-----------------------------------------------------------------
在原来的并查集基础上还要描述和根的关系,一开始看可能很难,但是做到后面发现其实就只有一个套路
带权并查集一般用0,1这两种或者0,1,2这三种来描述和根的关系,其实一般这三个就够了
在路径压缩的时候要利用a和b的关系,b和根的关系,推出a和根的关系,在合并集合的时候,要利用a和根的关系推出根和a的关系,再用题目给的a和b的关系以及b和根的关系,最终推出a的根和b的根的关系。只要做到这两个,带权并查集就很简单了
关系其实也很好推,一般就看看加起来对一个数字取余,或者相减之后取余,或者总共有几个关系减去自己之类的
--------------------------------------------------------------------
带权并查集入门经典:
中文题目
首先我们重新定义题目中的关系,A与B的关系是0,代表A与B同类,1代表A吃B,2代表B吃A,也就是题目中的d-1,这就是上面说的和根的关系0,1,2三种
接着要重写find函数
要写find函数就需要知道A与根的关系,我们可以利用传递性,A与B的关系和B与C的关系推出A与C的关系
A与B | B与C | A与C |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
0 | 2 | 2 |
1 | 0 | 1 |
1 | 1 | 2 |
1 | 2 | 0 |
2 | 0 | 2 |
2 | 1 | 3 |
2 | 2 | 1 |
立即推 A与C的关系=(A与B的关系+B与C的关系)%3
接着需要写一个check函数,用来检查他们说的话是不是真的
由题目可知x>N||y>N||d==2&&x==y为false,然后寻找出他们的根,如果不是同一个根说明还没有建立关系,就先认为是真的
如果已经建立关系,就需要判断A和B的关系,我们可以借助A与根的关系以及B与根推出A与B的关系
A与根 | B与根 | A与B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 2 |
0 | 2 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
1 | 2 | 2 |
2 | 0 | 2 |
2 | 1 | 1 |
2 | 2 | 0 |
立即推 A与B的关系=(A与根的关系-B与根的关系)%3
因为c++取余可以是负数(貌似有的题解就是这么错的却不知道),立即推 A与B的关系=(A与根的关系-B与根的关系+3)%3
再判断是否与给定的d-1相等,相等就是真的,不相等就是假的
如果为真我们需要合并两个集合,所以最后要重新写unite(union)函数,把x的根指向y的根,所以需要x的根和y的根的关系
由于A和A的根的关系已知,A和B的关系已知(题目给的)B和根的关系已知,如果知道A的根与A的关系就可以由A的根->A->B->B的根
A与A的根 | A的根与A |
---|---|
0 | 0 |
1 | 2 |
2 | 1 |
立即推 根与A的关系=(3-A与根的关系)%3
a的根->a->b->b的根
立即推 A的根与A的关系=3-A与A的根的关系
A与B的关系为d-1,
又因为 A与C的关系=(A与B的关系+B与C的关系)%3
立即推 A的根与B的关系=(A的根与A的关系+A与B的关系)%3
B与B的根的关系已知
立即推 A的根与B的根的关系=((A的根与A的关系+A与B的关系)%3+B与B的根的关系)%3
=(3-A与A的根+A与B的关系+B与B的根的关系)%3
这样这题就解决了
AC代码
#include
const int maxN=50001;
int parent[maxN],parentRelation[maxN],n;
//A与B的关系是0表示A与B同类,1表示A吃B,2表示B吃A
//题目中的1是同类,相当于这里的0,题目中的2是x吃y,相当于这里的1,立即推 这里的关系=题目中的关系-1
int find(int x){
if(parent[x]==x)return x;
/*
A与B B与C A与C
0 0 0
0 1 1
0 2 2
1 0 1
1 1 2
1 2 0
2 0 2
2 1 0
2 2 1
立即推 A与C的关系=(A与B的关系+B与C的关系)%3
*/
int root=find(parent[x]);//找到根
parentRelation[x]=(parentRelation[x]+parentRelation[parent[x]])%3;
return parent[x]=root;//压缩路径
}
bool check(int d,int x,int y){
if(x>n||y>n)return false;
if(d==2&&x==y)return false;
int a=find(x),b=find(y);
if(a!=b)return true;
/*
A与根 B与根 A与B
0 0 0
0 1 2
0 2 1
1 0 1
1 1 0
1 2 2
2 0 2
2 1 1
2 2 0
立即推 A与B的关系=(A与根的关系-B与根的关系)%3
因为c++取余可以是负数,立即推 A与B的关系=(A与根的关系-B与根的关系+3)%3
*/
return (parentRelation[x]-parentRelation[y]+3)%3==d-1;
}
void unite(int d,int x,int y){
//将x的根连接到y的根上
int a=find(x),b=find(y);
if(a==b)return;
/*
A与根 根与A
0 0
1 2
2 1
立即推 根与A的关系=(3-A与根的关系)%3
a的根->a->b->b的根
立即推 A的根与A的关系=3-A与A的根的关系
A与B的关系为d-1,
又因为 A与C的关系=(A与B的关系+B与C的关系)%3
立即推 A的根与B的关系=(A的根与A的关系+A与B的关系)%3
B与B的根的关系是parentRelation[B]
立即推 A的根与B的根的关系=((A的根与A的关系+A与B的关系)%3+B与B的根的关系)%3
=(3-A与A的根+A与B的关系+B与B的根的关系)%3
*/
parent[a]=b;
parentRelation[a]=(3-parentRelation[x]+(d-1)+parentRelation[y])%3;
}
int main(){
int k,d,a,b,ans;
scanf("%d%d",&n,&k);
ans=0;
for(int i=1;i<=n;++i){
parent[i]=i;
parentRelation[i]=0;
}
while(k--){
scanf("%d%d%d",&d,&a,&b);
if(check(d,a,b))unite(d,a,b);
else ++ans;
}
printf("%d\n",ans);
return 0;
}
不带注释
#include
const int maxN=50001;
int parent[maxN],parentRelation[maxN],n;
int find(int x){
if(parent[x]==x)return x;
int root=find(parent[x]);/
parentRelation[x]=(parentRelation[x]+parentRelation[parent[x]])%3;
return parent[x]=root;
}
bool check(int d,int x,int y){
if(x>n||y>n)return false;
if(d==2&&x==y)return false;
int a=find(x),b=find(y);
if(a!=b)return true;
return (parentRelation[x]-parentRelation[y]+3)%3==d-1;
}
void unite(int d,int x,int y){
int a=find(x),b=find(y);
if(a==b)return;
parent[a]=b;
parentRelation[a]=(3-parentRelation[x]+(d-1)+parentRelation[y])%3;
}
int main(){
int k,d,a,b,ans;
scanf("%d%d",&n,&k);
ans=0;
for(int i=1;i<=n;++i){
parent[i]=i;
parentRelation[i]=0;
}
while(k--){
scanf("%d%d%d",&d,&a,&b);
if(check(d,a,b))unite(d,a,b);
else ++ans;
}
printf("%d\n",ans);
return 0;
}
做完这题基本上带权并查集就会了
-------------------------------------------------------
题目大意:有两种罪犯,输入d表示两个人属于不同帮派,输入a则输出这两个是不是同一个帮派,或者不确定
解法:这题就是上一题的简化版只有两种关系了,0表示相同,1表示不同
步骤和上一题差不多
A和B | B和C | A和C |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
关系为(r[a]+r[b])%2或者r[a]^a[b](建议写异或的,要不然可能就跟其他人一样,怎么死的都不知道了)
接着是a和根以及根和a
A和根 | 根和A |
---|---|
0 | 0 |
1 | 1 |
关系为r[a],或者2-r[a],或者(-r[a]+2)%2
接着是a的根到b的根
A到A的根 | B到B的根 | A的根到B的根 |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
关系为(-r[a]+d+r[b])%2,或者(r[a]+d+r[b])%2或者r[a]^d^r[b](建议写这个)或者~(r[a]^r[b]),其中这题d为1,换到其他题目也是通用的
关系确定完就可以解完了
#include
const int maxN = 100001;
int parent[maxN], parentRelation[maxN], n, m;
int find(int x) {
if (parent[x] == x)return x;
int root = find(parent[x]);
parentRelation[x] = parentRelation[x] ^ parentRelation[parent[x]];
return parent[x] = root;
}
void unite(int x, int y) {
int a = find(x), b = find(y);
if (a == b)return;
parent[a] = b;
parentRelation[a] = parentRelation[x] ^ 1 ^ parentRelation[y];
}
int main() {
char c[3];
int t, a, b;
scanf("%d", &t);
while (t--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
parent[i] = i;
parentRelation[i] = 0;
}
while (m--) {
scanf("%s%d%d", c, &a, &b);
if (c[0] == 'A') {
if (find(a) == find(b)) {
if (parentRelation[a] == parentRelation[b])printf("In the same gang.\n");
else printf("In different gangs.\n");
}
else printf("Not sure yet.\n");
}
else unite(a, b);
}
}
return 0;
}
----------------------------------------------------
题目大意:有n个虫子,给出m对数字,对于任意的两个数字,表示这两个是相恋,问,有没有同性恋的
解法:当还没有确定关系的时候,默认异性恋,如果确定了,就可以判断同性恋还是异性恋了
#include
const int maxN = 2001;
int pre[maxN], r[maxN];
int find(int x) {
if (x == pre[x])return x;
int root = find(pre[x]);
r[x] = r[x] ^ r[pre[x]];
return pre[x] = root;
}
void unite(int x, int y) {
int a = find(x), b = find(y);
if (a == b)return;
pre[a] = b;
r[a] = r[x] ^ 1 ^ r[y];
}
int main() {
bool flag;
int t, n, m, a, b;
scanf("%d", &t);
for (int T = 1; T <= t; ++T) {
scanf("%d%d", &n, &m);
flag = false;
for (int i = 1; i <= n; ++i) {
pre[i] = i;
r[i] = 0;
}
while (m--) {
scanf("%d%d", &a, &b);
if (flag || find(a) == find(b) && r[a] == r[b])flag = true;
else unite(a, b);
}
printf("Scenario #%d:\n", T);
if (flag)printf("Suspicious bugs found!\n\n");
else printf("No suspicious bugs found!\n\n");
}
return 0;
}
---------------------------------------
题目大意:有n个栈,栈各有一个方块,有两种操作,一种是把一个栈的所有元素扔到另一个栈的上方,另一种是数某个方块下有几个方块
解法:多开一个数组num记录栈的元素个数,r数组为到根关系为到根有几个方块,数方块的时候栈的总元素-到根的元素个数-1
#include
const int maxN = 30001;
int pre[maxN], num[maxN], rank[maxN];
int find(int x) {
if (pre[x] == x)return x;
int root = find(pre[x]);
rank[x] += rank[pre[x]];
return pre[x] = root;
}
void unite(int x, int y) {
int a = find(x), b = find(y);
if (a == b)return;
rank[b] = num[a];
num[a] += num[b];
pre[b] = a;
}
int main() {
for (int i = 1; i
-----------------------------------
题目大意:有一个体育馆,有三百个位置,围成环,顺时针数,有n个人,m个语句,对于接下来的m条输入a b x,代表a+x=b,问有几个错误的。
解法:到根的关系为距离,推算一下距离就可以了
#include
const int maxN=50001;
int pre[maxN],rank[maxN],result;
int find(int x){
if(pre[x]==x)return x;
int root=find(pre[x]);
rank[x]+=rank[pre[x]];
return pre[x]=root;
}
void unite(int a,int b,int x){
int aa=find(a),bb=find(b);
if(aa==bb){
if(rank[a]+x!=rank[b])++result;
return;
}
pre[bb] = aa;
rank[bb] = -rank[b] + rank[a] + x;
}
int main(){
int n,m,a,b,x;
while(scanf("%d%d",&n,&m)!=EOF){
result=0;
for(int i=1;i
--------------------------
题目大意,给出m个区间的和,问你有几个是错误的
解法:和上一题差不多,不过这次合并集合的时候用(a-1,b]左闭右开区间,不用a和b
#include
const int maxN=200000;
int pre[maxN],rank[maxN],cnt;
int find(int x){
if(pre[x]==x)return x;
int root=find(pre[x]);
rank[x]+=rank[pre[x]];
return pre[x]=root;
}
void unite(int a,int b,int s){
int x=find(a),y=find(b);
if(x==y){
if(rank[b]-rank[a]!=s)++cnt;
return;
}
pre[y]=x;
rank[y]=-rank[b]+s+rank[a];
}
int main(){
int n,m,a,b,s;
while(scanf("%d%d",&n,&m)!=EOF){
for(int i=1;i<=n;++i){
pre[i]=i;
rank[i]=0;
}
cnt=0;
while(m--){
scanf("%d%d%d",&a,&b,&s);
unite(a-1,b,s);
}
printf("%d\n",cnt);
}
return 0;
}
-----------------------------------
题目大意:有n个人,分成3组+1个裁判,每个组任意一个人只能出石头或者剪刀或者布,但是裁判可以随便出,问能否找到唯一的裁判,如果能,输出最少的回合数,如果不能不可能,如果有多个解输出无法判断
解法:食物链的升级版,枚举所有人为裁判的情况,可能会产生矛盾,记录最后产生矛盾的地方id,这就是确定裁判的最少回合数,因为产生矛盾都是和裁判产生了矛盾,然后枚举,比如第一条,0能当裁判,1能当裁判,2能到裁判...第二条0能当裁判,1能当裁判,2不能当裁判.....第三句0能当裁判,1不能当裁判,2不能当裁判,类似这样,所以,是取矛盾产生的最后的位置,不是最开始。关系方面就和食物链一样
#include
const int maxN=500,maxM=2001;
int pre[maxN],rank[maxN];
int find(int x){
if(x==pre[x])return x;
int root=find(pre[x]);
rank[x]=(rank[x]+rank[pre[x]])%3;
return pre[x]=root;
}
bool unite(int x,int y,int d){
int a=find(x),b=find(y);
if(a==b){
if((rank[x]-rank[y]+3)%3!=d)return false;
return true;
}
pre[a]=b;
rank[a]=(3-rank[x]+d+rank[y])%3;
return true;
}
int main(){
bool flag;
char c;
int n,m,a[maxM][3],cnt,id,ans;
while(scanf("%d%d",&n,&m)!=EOF){
for(int i=1;i<=m;++i){
scanf("%d%c%d",&a[i][0],&c,&a[i][1]);
if(c=='<')a[i][2]=2;
else if(c=='>')a[i][2]=1;
else a[i][2]=0;
}
cnt=ans=id=0;
for(int i=0;ians?j:ans;
break;
}
}
if(!flag){
id=i;
++cnt;
}
}
if(!cnt)printf("Impossible\n");
else if(cnt==1)printf("Player %d can be determined to be the judge after %d lines\n",id,ans);
else printf("Can not determine\n");
}
return 0;
}
-------------------------------------------------
题目大意:有一个序列由01组成,长度为n,对于m个语句,输入a b s,s为英文的奇数或者偶数,表示a到b这个区间的1的奇偶性,问是否存在一个数x,使得前x句是对的,x+1句数错的,如果全对,则输出m
解法:n最大有10亿,数组绝对不能开着么大,所以我们要换个思路,因为m比较小,所以我们考虑只存,用到的下标,对于每个输入的下标从0开始编号,所以我们用一个map进行映射,每次下标假如map的时候要判断map里是否已经有了,如果有就不用管了,如果没有才开始重新编号,最后关系还是找(a-1,b]的1的奇偶性。
#include
#include
-------------------------------------
题目大意:有好人和坏人,好人总是不说谎,坏人总是说谎,每个人可以说另一个人是好人还是坏人,问好人分别是谁
解法:如果a说b是好人则a和b是同类的,a说b是坏人,a和b异类(简单枚举一下就知道了),合并完每个集合分成两类,一类是和根同类,一种是和根异类,这两类都有可能是好人,问题就转化为n个集合有两个小集合,从每个集合中挑选一个小集合,使得人数刚好为好人数,这个就是背包的升级版,最后判断一下有几种方案,如果只有一种,则输出所有的好人数,否则输出no
#include
#include
#define min(a,b) (a