并查集 提高篇
概述
主要记录各种并查集的例题
例1.格子游戏
题目链接
题目描述
Alice和Bob玩了一个古老的游戏:首先画一个 n×n 的点阵(下图 n=3 )。
接着,他们两个轮流在相邻的点之间画上红边和蓝边:
直到围成一个封闭的圈(面积不必为 1)为止,“封圈”的那个人就是赢家。因为棋盘实在是太大了,他们的游戏实在是太长了!
他们甚至在游戏中都不知道谁赢得了游戏。
于是请你写一个程序,帮助他们计算他们是否结束了游戏?
输入格式
输入数据第一行为两个整数 n 和 m。n表示点阵的大小,m 表示一共画了 m 条线。
以后 m 行,每行首先有两个数字 (x,y),代表了画线的起点坐标,接着用空格隔开一个字符,假如字符是 D,则是向下连一条边,如果是 R 就是向右连一条边。
输入数据不会有重复的边且保证正确。
输出格式
输出一行:在第几步的时候结束。
假如 m 步之后也没有结束,则输出一行“draw”。
数据范围
\(1≤n≤200,\)
\(1≤m≤24000\)
输入样例
3 5
1 1 D
1 1 R
1 2 D
2 1 R
2 2 D
输出样例
题解
将二维的点映射成一维,若两点连线,则有相同的根节点,每次判断即可
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 #include <iostream>
#include <cstring>
#include <algorithm>
using namespace std ;
const int N = 40005 ;
int n , m ;
int a , b ;
char op ;
int pre [ N ];
int find ( int x )
{
return pre [ x ] == x ? x : pre [ x ] = find ( pre [ x ]);
}
void init ( int n )
{
for ( int i = 1 ; i <= n ; i ++ ) pre [ i ] = i ;
}
int get ( int x , int y )
{
return ( x -1 ) * n + y ;
}
int main ()
{
cin >> n >> m ;
init ( n * n );
int cnt = 0 ;
bool flag = false ;
while ( m -- )
{
cnt ++ ;
cin >> a >> b >> op ;
int cur = get ( a , b );
int tmp ;
if ( op == 'D' ) tmp = get ( a + 1 , b );
else tmp = get ( a , b + 1 );
if ( find ( cur ) == find ( tmp ))
{
cout << cnt << endl ;
flag = true ;
break ;
}
else
{
int fx = find ( cur );
int fy = find ( tmp );
pre [ fx ] = fy ;
}
}
if ( ! flag ) cout << "draw" << endl ;
return 0 ;
}
例2.搭配购买
题目链接
题目描述
Joe觉得云朵很美,决定去山上的商店买一些云朵。
商店里有 n 朵云,云朵被编号为 1,2,…,n,并且每朵云都有一个价值。
但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买。
但是Joe的钱有限,所以他希望买的价值越多越好。
输入格式
第 1 行包含三个整数 n,m,w,表示有 n 朵云,m 个搭配,Joe有 w 的钱。
第 2∼n+1行,每行两个整数 ci,di 表示 i 朵云的价钱和价值。
第 n+2∼n+1+m 行,每行两个整数 ui,vi,表示买 ui 就必须买 vi,同理,如果买 vi 就必须买 ui。
输出格式
一行,表示可以获得的最大价值。
数据范围
\(1≤n≤10000,\)
\(0≤m≤5000,\)
\(1≤w≤10000,\)
\(1≤ci≤5000,\)
\(1≤di≤100,\)
\(1≤ui,vi≤n\)
输入样例
5 3 10
3 10
3 10
3 10
5 100
10 1
1 3
3 2
4 2
输出样例
题解
将必须买的物品用并查集维护,必须买的物品相当于一个联通块,对所有的联通块进行01背包即可
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 #include <iostream>
#include <cstring>
#include <algorithm>
using namespace std ;
const int N = 10005 ;
int dp [ N ];
int n , m , w ;
int pre [ N ], sumc [ N ], sumd [ N ];
int c [ N ], d [ N ];
int u , v ;
void init ( int n )
{
for ( int i = 1 ; i <= n ; i ++ )
{
pre [ i ] = i ;
sumc [ i ] = c [ i ];
sumd [ i ] = d [ i ];
}
}
int find ( int x )
{
return pre [ x ] == x ? x : pre [ x ] = find ( pre [ x ]);
}
int main ()
{
cin >> n >> m >> w ;
for ( int i = 1 ; i <= n ; i ++ ) cin >> c [ i ] >> d [ i ];
init ( n );
while ( m -- )
{
cin >> u >> v ;
if ( find ( u ) != find ( v ))
{
int fx = find ( u );
int fy = find ( v );
pre [ fx ] = fy ;
sumc [ fy ] += sumc [ fx ];
sumd [ fy ] += sumd [ fx ];
}
}
memset ( c , 0 , sizeof ( c ));
memset ( d , 0 , sizeof ( d ));
int cnt = 0 ;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( pre [ i ] == i )
{
c [ ++ cnt ] = sumc [ i ];
d [ cnt ] = sumd [ i ];
}
}
for ( int i = 1 ; i <= cnt ; i ++ )
{
for ( int j = w ; j >= c [ i ]; j -- )
{
dp [ j ] = max ( dp [ j ], dp [ j - c [ i ]] + d [ i ]);
}
}
cout << dp [ w ] << endl ;
return 0 ;
}
例3.银河英雄传说
题目链接
题目描述
有一个划分为 N 列的星际战场,各列依次编号为 1,2,…,N。
有 N 艘战舰,也依次编号为 1,2,…,N,其中第 i 号战舰处于第 i 列。
有 T 条指令,每条指令格式为以下两种之一:
M i j
,表示让第 i 号战舰所在列的全部战舰保持原有顺序,接在第 j 号战舰所在列的尾部。
C i j
,表示询问第 i 号战舰与第 j 号战舰当前是否处于同一列中,如果在同一列中,它们之间间隔了多少艘战舰。
现在需要你编写一个程序,处理一系列的指令。
输入格式
第一行包含整数 T,表示共有 T 条指令。
接下来 T 行,每行一个指令,指令有两种形式:M i j 或 C i j。
其中 M 和 C 为大写字母表示指令类型,i 和 j 为整数,表示指令涉及的战舰编号。
输出格式
你的程序应当依次对输入的每一条指令进行分析和处理:
如果是 M i j
形式,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息;
如果是 C i j
形式,你的程序要输出一行,仅包含一个整数,表示在同一列上,第 i 号战舰与第 j 号战舰之间布置的战舰数目,如果第 i 号战舰与第 j 号战舰当前不在同一列上,则输出 −1。
数据范围
\(N≤30000,T≤500000\)
输入样例
4
M 2 3
C 1 2
M 2 4
C 4 2
输出样例
题解
并查集可以用于维护具有传递性关系的作用,每个集合的大小,绑定到根结点上,每个点到根结点的距离,绑定到每个元素的结点上
可以在find的过程中更新当前点到根节点的距离(d[x])与集合大小(sz[x]),具体更新见代码
题目问的是两条船之间的距离,因此有两种情况
a==b
:0
a!=b
:abs(d[a] - d[b]) - 1;
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 #include <iostream>
using namespace std ;
const int N = 30005 ;
int pre [ N ];
int d [ N ], sz [ N ];
void init ( int n )
{
for ( int i = 1 ; i <= n ; i ++ )
{
pre [ i ] = i ;
sz [ i ] = 1 ;
}
}
int find ( int x )
{
if ( pre [ x ] == x ) return x ;
int root = find ( pre [ x ]);
d [ x ] += d [ pre [ x ]]; // d表示到根节点的距离,需要加上父节点到根节点的距离
return pre [ x ] = root ;
}
void solve ()
{
int t ;
char op ;
int x , y ;
cin >> t ;
init ( N -5 );
while ( t -- )
{
cin >> op >> x >> y ;
int fx = find ( x ), fy = find ( y );
if ( op == 'M' )
{
pre [ fx ] = fy ; // 更新根节点
d [ fx ] = sz [ fy ]; // 由于是接在末尾,x的根节点到y根节点的距离为原y集合的大小
sz [ fy ] += sz [ fx ]; // y集合大小增加x集合的大小
}
else
{
if ( fx != fy ) cout << "-1" << endl ;
else
{
cout << max ( 0 , abs ( d [ x ] - d [ y ]) -1 ) << endl ;
}
}
}
}
int main ()
{
solve ();
return 0 ;
}
例4.程序自动分析
题目链接
题目描述
在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
考虑一个约束满足问题的简化版本:假设 x1,x2,x3,… 代表程序中出现的变量,给定 n 个形如 xi=xj 或 xi≠xj 的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。
例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x1≠x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
现在给出一些约束满足问题,请分别对它们进行判定。
输入格式
输入文件的第 1 行包含 1 个正整数 t,表示需要判定的问题个数,注意这些问题之间是相互独立的。
对于每个问题,包含若干行:
第 1 行包含 1 个正整数 n,表示该问题中需要被满足的约束条件个数。
接下来 n 行,每行包括 3 个整数 i,j,e,描述 1 个相等/不等的约束条件,相邻整数之间用单个空格隔开。若 e=1,则该约束条件为 xi=xj;若 e=0,则该约束条件为 xi≠xj。
输出格式
输出文件包括 t 行。
输出文件的第 k 行输出一个字符串 YES 或者 NO,YES 表示输入中的第 k 个问题判定为可以被满足,NO 表示不可被满足。
数据范围
\(1≤n≤105\)
\(1≤i,j≤109\)
输入样例
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1
输出样例
题解
首先发现数据范围过大,离散化,合并操作在判断操作之前进行,离线操作后利用并查集维护和判断即可
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 #include <bits/stdc++.h>
#define int long long
using namespace std ;
const int N = 1e5 + 5 ;
int ini [ N ], inj [ N ];
int pre [ 2 * N ];
int e [ N ];
int a [ 2 * N + 5 ];
int n ;
void init ( int n )
{
for ( int i = 1 ; i <= n ; i ++ ) pre [ i ] = i ;
}
int find ( int x )
{
return pre [ x ] == x ? x : pre [ x ] = find ( pre [ x ]);
}
signed main ()
{
int t ;
cin >> t ;
while ( t -- )
{
cin >> n ;
int cnt = 0 ;
for ( int i = 1 ; i <= n ; i ++ )
{
cin >> ini [ i ] >> inj [ i ] >> e [ i ];
a [ ++ cnt ] = ini [ i ];
a [ ++ cnt ] = inj [ i ];
}
sort ( a + 1 , a + cnt + 1 );
int len = unique ( a + 1 , a + cnt + 1 ) - a -1 ;
init ( len );
//cout<<len<<endl;
bool flag = true ;
for ( int i = 1 ; i <= n ; i ++ )
{
ini [ i ] = lower_bound ( a + 1 , a + len + 1 , ini [ i ]) - a ;
inj [ i ] = lower_bound ( a + 1 , a + len + 1 , inj [ i ]) - a ;
int fx = find ( ini [ i ]), fy = find ( inj [ i ]);
if ( e [ i ])
{
if ( fx != fy )
{
pre [ fx ] = fy ;
}
}
}
//for(int i=1;i<=n;i++) cout<<ini[i]<<" "<<inj[i]<<endl;
for ( int i = 1 ; i <= n ; i ++ )
{
if ( ! e [ i ])
{
int fx = find ( ini [ i ]), fy = find ( inj [ i ]);
if ( fx == fy )
{
flag = false ;
break ;
}
}
}
if ( flag ) cout << "YES" << endl ;
else cout << "NO" << endl ;
}
return 0 ;
}
例5.Graph Destruction
题目链接
题解
题目大意:给出一个无向图,无向图有n个点,m条边,依次删除1-n节点,输出每次删除后有多少个连通分量。我们知道并查集能很容易合并两个集合,但对于分裂操作很难,因此我们可以倒着考虑,从编号最大的点开始依次加点合并,离线倒着处理所有的答案然后输出就行了,我们每次加点实际判断的是编号比自己大的点的联通情况,而题目保证了\(a_i \lt b_i\) ,因此建单向边即可。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 #include <bits/stdc++.h>
using namespace std ;
const int N = 2e5 + 5 ;
int e [ N ], ne [ N ], head [ N ], idx = 0 ;
void add ( int a , int b ) {
e [ idx ] = b ;
ne [ idx ] = head [ a ];
head [ a ] = idx ++ ;
}
int n , m ;
int res [ N ];
int pre [ N ];
void init ( int n ) {
for ( int i = 1 ; i <= n ; i ++ ) pre [ i ] = i ;
}
int find ( int x ) {
return x == pre [ x ] ? x : pre [ x ] = find ( pre [ x ]);
}
bool vis [ N ];
int main () {
cin . tie ( 0 );
cout . tie ( 0 );
ios :: sync_with_stdio ( 0 );
cin >> n >> m ;
init ( n );
memset ( head , -1 , sizeof ( head ));
int tmp = 0 ;
for ( int i = 1 ; i <= m ; i ++ ){
int a , b ;
cin >> a >> b ;
add ( a , b );
}
for ( int i = n ; i >= 1 ; i -- ) {
tmp ++ ;
for ( int j = head [ i ]; j != -1 ; j = ne [ j ]) {
int k = e [ j ];
int fx = find ( i );
int fy = find ( k );
if ( fx != fy )
pre [ fy ] = fx , tmp -- ;
}
res [ i - 1 ] = tmp ;
}
for ( int i = 1 ; i <= n ; i ++ ) cout << res [ i ] << endl ;
return 0 ;
}