YbtOJ 「图论」 第1章 并查集
并查集
判断该节点的祖先的一种结构 在向上合并的时候使用路径压缩降低复杂度 注意需要初始化fa[i]=i
int fa[100001];//fa是标记每个元素的代表元素(祖先)
//并查集是森林,集合是树,合并操作是把两棵树并到一起
int find ( int x )//查找
{
if ( fa[x] == x ) return x;
return fa[x] = find ( fa[x] );//在路径上就把每个根节点接到了祖先节点上
//== fa[x] = find ( fa[x] ); return fa[x];
}
void merge ( int x , int y )//合并
{
int fx = find ( x ) , fy = find ( y );
fa[fx] = fy;
return;
}
A. 【例题1】【模板】并查集
[题目描述]
如题,现在有一个并查集,你需要完成合并和查询操作。
[输入格式]
第一行包含两个整数
接下来
当
当
Y
;否则输出 N
。
[输出格式]
对于每一个 Y
或者 N
。
[算法分析]
裸题
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e4 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int fa[N] , n , m;
int find ( int x )
{
if ( x == fa[x] ) return x;
else return fa[x] = find(fa[x]);
}
void merge ( int x , int y )
{
int fx = find(x) , fy = find(y);
if ( fx != fy ) fa[fx] = fy;
}
signed main ()
{
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) fa[i] = i;
for ( int i = 1 , op , u , v ; i <= m ; i ++ )
{
op = read() , u = read() , v = read();
if ( op == 1 ) merge ( u , v );
else printf ( "%c\n" , find(u) == find(v) ? 'Y' : 'N' );
}
return 0;
}
B. 【例题2】银河英雄传说
[题目背景]
公元
宇宙历
[题目描述]
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成 M i j
,含义为第
然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j
。该指令意思是,询问电脑,杨威利的第
作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
[输入格式]
第一行有一个整数
以下有
-
M i j
: 和 是两个整数( ),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第 号战舰与第 号战舰不在同一列。 -
C i j
: 和 是两个整数( ),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。
[输出格式]
依次对输入的每一条指令进行分析和处理:
- 如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息。
- 如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第
号战舰与第 号战舰之间布置的战舰数目。如果第 号战舰与第 号战舰当前不在同一列上,则输出 。
[算法分析]
开一个
再开一个
在每一次合并的时候 将第一列接到第二列尾部 所以要将第一列的队头
这样 在每一次寻找的时候 每一个节点到队头的距离就等于每一个节点到祖先的距离加上这个祖先到队头的距离
这个操作的正确性是通过路径压缩来保证的 即查询时每一个点向上跳到它的祖先节点最多只有两层(第一层是它原本集合的祖先节点 第二层是这个祖先合并到的集合的祖先节点)
注意 每一个节点到祖先的距离指的就是原来的
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 3e4 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int fa[N] , n , m , sz[N] , dis[N];//记录每一个祖先集合的大小 和这个节点离祖先的距离
char op;
int find ( int x )
{
if ( x == fa[x] ) return x;
else
{
int fx = find(fa[x]);//每一次将祖先找到
dis[x] += dis[fa[x]];//将这个节点到队头的距离赋值为上一个
return fa[x] = fx;
}
}
void merge ( int x , int y ) //x接到y后面
{
int fx = find(x) , fy = find(y);
fa[fx] = fy;
dis[fx] = sz[fy];//记录这个节点到队头的距离
sz[fy] += sz[fx];//以这个顶点为队头的队列中有多少个节点
}
signed main ()
{
m = read();
for ( int i = 1 ; i <= 30000 ; i ++ )
sz[i] = 1 , fa[i] = i;
for ( int i = 1 , u , v ; i <= m ; i ++ )
{
cin >> op >> u >> v;
if ( op == 'M' ) merge ( u , v );
else
{
int fu = find(u) , fv = find(v);
if ( fu == fv ) printf ( "%d\n" , abs ( dis[u] - dis[v] ) - 1 );
else printf ( "-1\n" );
}
}
return 0;
}
C. 【例题3】食物链
[题目描述]
动物王国中有三类动物
现有
有人用两种说法对这
- 第一种说法是
1 X Y
,表示 和 是同类。 - 第二种说法是
2 X Y
,表示 吃 。
此人对
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中
或 比 大,就是假话; - 当前的话表示
吃 ,就是假话。
你的任务是根据给定的
[输入格式]
第一行两个整数,
第二行开始每行一句话(按照题目要求,见样例)
[输出格式]
一行,一个整数,表示假话的总数。
[算法分析]
并查集还是维护同类的关系 即"A的天敌和B的猎物是否是同类"这样的关系 那么我们为每一个动物开三个点 分别表示这个动物本身,天敌和同类(一倍存本身 二倍存天敌 三倍存猎物)
- 查询同类: 如果第一个点是第二个点的天敌或者猎物 那么是假话 否则是真话 将这两个点的本身,天敌,猎物分别合并到同一个集合中
- 查询
吃 时 如果 是 的同类或者猎物 那么是假话 否则是真话 合并对应集合
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 5e4 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int fa[N*3] , n , k , cnt;
int find ( int x )
{
if ( fa[x] == x ) return x;
return fa[x] = find ( fa[x] );
}
void merge ( int x , int y )
{
int fx = find(x), fy = find(y);
fa[fy] = fx;
}
signed main ()
{
n = read() , k = read();
for ( int i = 1 ; i <= 3 * n ; i ++ ) fa[i] = i;
for ( int i = 1 , op , x , y ; i <= k ; i ++ )
{
op = read() , x = read() , y = read();
if ( x > n || y > n ) { cnt ++; continue; }
if ( op == 1 )
{
if ( find(x+n) == find(y) || find(x+n*2) == find(y) ) { cnt ++; continue; }
merge ( x , y ) , merge ( x + n , y + n ) , merge ( x + 2 * n , y + 2 * n );
}
else
{
if ( find(x) == find(y) || find(x+2*n) == find(y) ) { cnt ++; continue; }
merge ( x , y + 2 * n ) , merge ( x + n , y ) , merge ( x + 2 * n , y + n );
}
}
printf ( "%d" , cnt );
return 0;
}
D. 【例题4】超市购物
[题面翻译]
- 给定
件物品,第 件物品有如下信息:- 卖出去可以得到
的收益。 - 过期时间为
,过了过期时间就不能再卖出去。
- 卖出去可以得到
- 卖掉一件物品要用
的时间,求最大收益。 - 多组数据,每组数据一行,首先一个整数
然后 对数 ,以文件终止符结束。 , 。
[算法分析]
对于每一个点按照权值大小进行排序
初始对于每一个截止日期维护一个并查集 集合的根就表示从这个日期开始向前数(包括自己) 第一个空闲的日期 初始
我们的贪心策略是:每一件物品都尽量晚地卖出去
所以 每一次搜索这个集合中还有没有能安排给它的空闲时间了 如果有 那么这个集合的根节点的父亲改为这个空闲时间(也就是老的根节点)-1
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e4 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int fa[N] , n , ans , longest;
struct node
{
int p , d;
bool operator < ( const node &a ) const
{
return p > a.p;
}
}e[N];
int find ( int x )
{
if ( fa[x] == x ) return x;
return fa[x] = find ( fa[x] );
}
void init()
{
longest = 0;
ans = 0;
memset ( e , 0 , sizeof ( e ) );
}
int main()
{
while ( scanf ( "%d" , &n ) == 1 )
{
init();
for ( int i = 1 ; i <= n ; i ++ )
{
e[i].p = read() , e[i].d = read();
longest = max ( longest , e[i].d );//longest是最长的过期时间
}
sort ( e + 1 , e + n + 1 );//权值最大的放在前面
for ( int i = 1 ; i <= longest ; i ++ ) fa[i] = i;//每一个时间节点开一个并查集
//每一个日期的并查集的root就是从这个日期开始向前数(包括自己) 第一个空闲的日期
for ( int i = 1 ; i <= n ; i ++ )
{
int xx = find ( e[i].d );//能安排给他的完成时间
if ( xx != 0 )//如果前面还有空闲时间
{
ans += e[i].p;
fa[xx] = xx - 1;//贪心策略就是这个物品越晚卖掉越好
}
}
printf ( "%d\n" , ans );
}
return 0;
}
E. 【例题5】逐个击破
[题目背景]
三大战役的平津战场上,傅作义集团在以北平、天津为中心,东起唐山西至张家口的铁路线上摆起子一字长蛇阵,并企图在溃败时从海上南逃或向西逃窜。为了就地歼敌不让其逃走,指挥官制定了先切断敌人东西两头退路然后再逐个歼灭敌人的战略方针。秉承伟大军事家的战略思想,作为一个有智慧的军长你,遇到了一个类似的战场局面。
[题目描述]
现在有
[输入格式]
第一行包含两个正整数
第二行包含
接下来
[输出格式]
输出一行一个整数,表示最少花费的代价。
[算法分析]
将所有敌军军团称为特殊点 则本题需要求的就是使这些点不联通的最小删边方案
正难则反 我们要求最小删边方案 也就是求这些点不联通的最大加边方案 (答案减边权相当于原图中加上了这条边 因为我们要求删边最小值 所以要满足连边最大值)
即:先将所有节点按照代价降序排序 每一次看最大代价的边的两个端点是否都是特殊点
- 如果都是特殊点 加上这条边就连通了 显然不可以
- 如果两条边在同一集合中显然可以(因为保证了在一个集合中的所有点只有小于等于一个特殊点)
- 如果不是两个都是特殊点就连边 再将非特殊点集合并入特殊点集合中
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int sum , ans;
struct edge { int u , v , w; } e[N];
int fa[N] , n , k , m , in[N];
int find ( int x ) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
signed main ()
{
n = read() , m = read() , k = read();
for ( int i = 1 ; i <= n ; i ++ ) fa[i] = i;
for ( int i = 1 ; i <= k ; i ++ ) in[read()+1] = 1;
for ( int i = 1 ; i <= m ; i ++ ) e[i].u = read() + 1, e[i].v = read() + 1 , e[i].w = read() , ans += e[i].w;
sort ( e + 1 , e + m + 1 , [](const edge a , const edge b) { return a.w > b.w; } );
for ( int i = 1 ; i <= m ; i ++ )
{
int fu = find(e[i].u) , fv = find(e[i].v);
if ( fu == fv ) ans -= e[i].w;//如果在同一个集合中 进行连边
else if ( !in[fu] || !in[fv] )/
{
ans -= e[i].w;
if ( !in[fu] ) fa[fu] = fv;//并入特殊点的集合
else fa[fv] = fu;
}
//最终目的是让所有集合中有些只有一个敌方节点
}
printf ( "%lld" , ans );
return 0;
}
F. 1.躲避拥挤
观察到询问的最小边权如果按照从小到大排序的话 后面的询问答案一定是在前面的询问答案基础之上加上一些贡献得到的
所以考虑从小到大排序边权和询问 对于每一次询问 将所有符合条件的边全部加入 用并查集统计答案并维护图的连通性
需要注意:在加边的时候 如果祖先节点相同的话 说明这两个点在前面连边的时候已经计算过贡献了 直接跳过
#include <bits/stdc++.h>
using namespace std;
#define inl inline
const int N = 1e5 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , q , fa[N] , sz[N] , ans[N] , res;
struct node { int u , v , w; } a[N];
struct query { int id , val; } qq[N];
int find ( int x )
{
if ( x == fa[x] ) return x;
else return fa[x] = find(fa[x]);
}
void merge ( int x , int y )
{
int fx = find(x) , fy = find(y);
if ( fx == fy ) return;
fa[fx] = fy;
res += sz[fx] * sz[fy] * 2;
sz[fy] += sz[fx];
}
void init()
{
memset ( a , 0 , sizeof a );
memset ( ans , 0 , sizeof ans );
memset ( qq , 0 , sizeof qq );
res = 0;
}
signed main ()
{
int T = read();
while ( T -- )
{
init();
n = read() , m = read() , q = read();
for ( int i = 1 ; i <= n ; i ++ ) sz[i] = 1 , fa[i] = i;
for ( int i = 1 ; i <= m ; i ++ ) a[i].u = read() , a[i].v = read() , a[i].w = read();
for ( int i = 1 ; i <= q ; i ++ ) qq[i].id = i , qq[i].val = read();
sort ( a + 1 , a + m + 1 , [](const node a , const node b) { return a.w < b.w; } );
sort ( qq + 1 , qq + q + 1 , [](const query a , const query b) { return a.val < b.val; } );
int stp = 1;
for ( int i = 1 ; i <= q ; i ++ )
{
while ( stp <= m && a[stp].w <= qq[i].val )
{
merge ( a[stp].u , a[stp].v );
stp ++;
}
ans[qq[i].id] = res;
}
for ( int i = 1 ; i <= q ; i ++ ) cout << ans[i] << endl;
}
return 0;
}
G. 2.约束条件
因为值域很大 所以想到离散化 将所有相等的条件排在前面处理 如果后面的不等关系出现冲突则输出
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , fa[N] , b[N] , cnt ;
struct node { int e , x , y; } a[N];
int find ( int x )
{
if ( x == fa[x] ) return x;
else return fa[x] = find(fa[x]);
}
void init()
{
memset ( a , 0 , sizeof a );
cnt = 0;
}
signed main ()
{
int T = read();
while ( T -- )
{
init();
n = read();
for ( int i = 1 ; i <= 1e6 ; i ++ ) fa[i] = i;
for ( int i = 1 ; i <= n ; i ++ )
{
a[i].x = read() , a[i].y = read() , a[i].e = read();
b[++cnt] = a[i].x , b[++cnt] = a[i].y;
}
sort ( b + 1 , b + cnt + 1 );
int sz = unique ( b + 1 , b + cnt + 1 ) - b - 1;
for ( int i = 1 ; i <= n ; i ++ )
{
a[i].x = lower_bound ( b + 1 , b + sz + 1 , a[i].x ) - b;
a[i].y = lower_bound ( b + 1 , b + sz + 1 , a[i].y ) - b;
}
sort ( a + 1 , a + n + 1 , [](const node &a , const node &b) { return a.e > b.e; } );
int flag = 1;
for ( int i = 1 ; i <= n ; i ++ )
{
int fx = find(a[i].x) , fy = find(a[i].y);
if ( fx == fy && !a[i].e ) { cout << "NO" << endl; flag = 0; break; }
if ( fx != fy && a[i].e ) fa[fx] = fy;
}
if ( flag ) cout << "YES" << endl;
}
return 0;
}
H. 3.染色操作
一道神题 设置
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , fa[N] , ans;//fa[i]表示从i位置开始向右第一个黑点的位置(包括自己
struct node { int u , v , w; } a[N];
int find ( int x )
{
if ( x == fa[x] ) return x;
else return fa[x] = find(fa[x]);
}
signed main ()
{
n = read() , m = read();
ans = n;
for ( int i = 1 ; i <= n + 1 ; i ++ ) fa[i] = i;
for ( int i = 1 ; i <= m ; i ++ )
{
int l = read() , r = read();
while(1)
{
int fl = find(l);
if ( fl > r ) break;
else fa[fl] = fl + 1;
ans --;
}
cout << ans << endl;
}
return 0;
}
I. 4.数列询问
又一道神题...
如果发现一组询问
我们用并查集维护这种关系 并查集中每一条边维护该点和父亲的差
对于一组询问 如果
对于查询 如果
在加边的时候 我们
所以
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , m , p , fa[N] , sum[N];
int find ( int x )
{
if ( fa[x] == x ) return x;
int fx = find(fa[x]);
sum[x] = ( sum[fa[x]] + sum[x] ) % p;
return fa[x] = fx;
}
signed main ()
{
n = read() , m = read() , p = read();
for ( int i = 1 ; i <= n ; i ++ ) fa[i] = i;
for ( int i = 1 ; i <= m ; i ++ )
{
int l = read() , r = read() , k = read();
int fl = find ( l - 1 ) , fr = find(r);
if ( fl == fr )
{
if ( ( sum[r] + sum[l-1] + p ) % p != k ) { cout << i - 1 << endl; return 0; }
}
else
{
fa[fr] = fl;
sum[fr] = k - sum[r] - sum[l-1];
}
}
cout << m << endl;
return 0;
}
J. 5.小明反击
和构造完全图有异曲同工之妙
关于排序:如果不排序 会导致出错 因为可能在以前加边的时候在这两个点之间加入了其他小边 不能保证两个点之间的最小边是这个大边
#include <bits/stdc++.h>
using namespace std;
#define inl inline
#define int long long
const int N = 1e6 + 5;
inl int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar (); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar (); }
return x * f;
}
int n , fa[N] , b[N] , ans , sz[N];
struct node { int u , v , w; } a[N];
int find ( int x )
{
if ( x == fa[x] ) return x;
else return fa[x] = find(fa[x]);
}
void init()
{
memset ( a , 0 , sizeof a );
ans = 0;
}
signed main ()
{
int T = read();
while ( T -- )
{
init();
n = read();
for ( int i = 1 ; i <= n ; i ++ ) sz[i] = 1 , fa[i] = i;
for ( int i = 1 ; i < n ; i ++ ) a[i].u = read() , a[i].v = read() , a[i].w = read();
sort ( a + 1 , a + n , [](const node &a , const node &b) { return a.w < b.w; } );
for ( int i = 1 ; i < n ; i ++ )
{
int fu = find(a[i].u) , fv = find(a[i].v);
ans += sz[fu] * sz[fv] * ( a[i].w + 1 ) - 1;
fa[fu] = fv;
sz[fv] += sz[fu];
}
cout << ans << endl;
}
return 0;
}
`
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)