声明:
本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了按照《算法竞赛进阶指南》的目录顺序学习,包含书中的少部分重要知识点、例题解题报告及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可全都是我自己敲哒)部分内容由我个人编写而成,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》——作者李煜东,强烈安利,好书不火系列,谢谢配合。
下方链接为学习笔记目录链接(中转站)
学习笔记目录链接
ACM-ICPC在线模板
当我们在寻找祖先时,一旦元素多且来,并查集就会退化成单次 O ( n ) O(n) O(n)的算法,为了解决这一问题我们可以在寻找祖先的过程中直接将子节点连在祖先上,这样可以大大降低复杂度,均摊复杂度是 O ( l o g ( n ) ) O(log(n)) O(log(n))的
按秩合并也是常见的优化方法,“秩”的定义很广泛,举个例子,在不路径压缩的情况下,常见的情况是把子树的深度定义为秩
无论如何定义通常情况是把“秩”储存在根节点,合并的过程中把秩小的根节点插到根大的根节点上,这样可以减少操作的次数
特别的,如果把秩定义为集合的大小,那么采用了按秩合并的并查集又称“启发式并查集”
按秩合并的均摊复杂度是 O ( l o g ( n ) ) O(log(n)) O(log(n))的,如果同时采用按秩合并和路径压缩均摊复杂度是 O ( α ( n ) ) , α ( n ) O(α(n)),α(n) O(α(n)),α(n)是反阿克曼函数
∀ n ≤ 2 1 0 19729 , α ( n ) ≤ 5 ∀ n ≤2^{10^{19729}},α(n)≤5 ∀n≤21019729,α(n)≤5可以视为均摊复杂度为 O ( 1 ) O(1) O(1)
不过通常情况下我们只需要采用路径压缩就够了
int fa[N];
void init(){
for(int i = 1;i <= n;++i)
fa[i] = i;
memset(Rank,0, sizeof Rank);
}
int getfa(int x){
//查询
if(fa[x] == x) return x;
return fa[x] = getfa(fa[x]);
}
inline void union( int x, int y){
//合并
int fx = getfa(x) , fy = get(y);
fa[fx] = fy;
return ;
}
inline bool same(int x , int y){
//判读是否在同一结合
return getfa(x) == getfa(y) ;
}
//把深度当作秩的 按秩合并
inline void rank_union( int x, int y)
{
fx = getfa(x) , fy = getfa(y);
if(Rank[fx] < Rank[fy]) fa[fx] = fy;
else {
fa[fy] = fx;
if(Rank[fx] == Rank[fy]) Rank[fx]++;
}
return ;
}
并查集能在一张无向图中维护结点之间的连通性,实际上,并查集擅长动态维护许多具有传递性的关系。
并查集的模板题,注意该题的数据达到1e9,我们可以离散化。
注意多组数据并查集中的fa数组也要清空!
#include
#include
#include
#include
using namespace std;
const int N = 1000007;
struct node{
int x, y, z;
bool operator<(const node &t)const {
return z > t.z;
}
}a[N];
int fa[N];
int n, m;
int b[N << 2];
int find(int x){
if(x == fa[x])return x;
return fa[x] = find(fa[x]);
}
void merge(int x, int y){
x = find(x), y = find(y);
fa[x] = y;
}
int main(){
int t;
scanf("%d", &t);
while(t -- ){
scanf("%d", &n);
memset(b, 0, sizeof b);
memset(a, 0, sizeof a);
memset(fa, 0, sizeof fa);
bool flag = 0;
int cnt = 0;
for(int i = 1; i <= n; ++ i){
scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].z);
b[cnt ++ ] = a[i].x;
b[cnt ++ ] = a[i].y;
}
sort(b, b + cnt);
int res = unique(b, b + cnt) - b;
for(int i = 1;i <= n; ++ i){
a[i].x = lower_bound(b, b + res, a[i].x) - b;
a[i].y = lower_bound(b, b + res, a[i].y) - b;
}
for(int i = 1; i <= res; ++ i)
fa[i] = i;
sort(a + 1, a + 1 + n);
for(int i = 1; i <= n; ++ i){
int x = a[i].x, y = a[i].y;
if(a[i].z){
merge(x, y);
}
else {
if(find(x) == find(y)){
puts("NO");
flag = 1;
break;
}
}
}
if(!flag)puts("YES");
}
return 0;
}
并查集实际上是由若干棵树构成的森林,我们可以在树中的每一条边上记录权值,即维护一个数组 d d d,用 d [ x ] d[x] d[x]保存x到父结点 f [ x ] f[x] f[x]的边权,在每次路径压缩后,每个访问过的结点都会直接指向树根,我们可以在路径压缩的同时统计每个结点到树根之间的路径上的一些信息,这就是“边带权”并查集
原题也太长了吧,神TM讲故事的吧
边带权并查集其实写起来非常简单,就是多统计两个数组而已。
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
//#pragma GCC optimize (2)
//#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 1e5+7;
int n,m;
int a[N];
int T;
int f[N];
int Size[N],d[N];
void init(){
for(int i = 1;i<=N;++i)
f[i] = i,Size[i] = 1;
}
int Get(int x){
if(x == f[x])return x;
int root = Get(f[x]);//往上走一层
d[x] += d[f[x]];//因为是按原顺序,所以我要加上我前面的长度,这才是真正的距离
return f[x] = root;
}
void Merge(int x,int y){
int xx = Get(x),yy = Get(y);
f[xx] = yy;
d[xx] = Size[yy];//这里的xx是x的根所以肯定是等于
Size[yy] += Size[xx];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>T;
init();
over(i,1,T){
int x,y;
char ch;
cin>>ch>>x>>y;
if(ch == 'M'){
Merge(x,y);
}
else {
if(Get(x) == Get(y)){
printf("%d\n",abs(d[y] - d[x])-1);//去掉自己
}
else puts("-1");
}
}
return 0;
}
用scanf在AcWing上AC,但是在洛谷上全部RE,细思极恐
我们可以用sum数组表示序列S的前缀和,那么会得到以下性质.
s [ l ~ r ] s[l~r] s[l~r]有偶数个1,等价于 s u m [ l − 1 ] 与 s u m [ r ] sum[l-1]与sum[r] sum[l−1]与sum[r]奇偶性相同 ( 1 + 0 = 1 0 + 0 = 0 (1+0=1\ 0+0=0 (1+0=1 0+0=0,1是奇数,0是偶数)
s [ l ~ r ] s[l~r] s[l~r]有奇数个1,等价于 s u m [ l − 1 ] 与 s u m [ r ] sum[l-1]与sum[r] sum[l−1]与sum[r]奇偶性不同 ( 1 + 1 = 0 0 + 1 = 0 (1+1=0\ 0+1=0 (1+1=0 0+1=0,1是奇数,0是偶数)
因为奇加奇等于偶,偶加奇等于奇,就是异或里的1 ^ 1 = 0,1 ^ 0 = 1,所以用前缀异或和来处理 。
其中前缀异或和数组 d[N],d[i]表示i到根节点的路径上的异或和(0为偶1为奇)
三种情况:
如果说x1和x2奇偶性质相同,x2与x3奇偶性质相同,那么x1和x3也相同
如果说x1和x2奇偶性质相同,x2与x3奇偶性质不同,那么x1和x3也不同
如果说x1和x2奇偶性质不同,x2与x3奇偶性质不同,那么x1和x3就相同
再有就是本题的数据特别大,n为1e9,数组不可能存得下,所以需要离散化,这里n特别大,但是m却很小,所以我们把l - 1和r缩小到1~2M,(因为这里的区间实际上区间里面的所有的数都没有用到,区间大小很大和为1没有区别,本题只是要求的各各区间的关系,相当于m个点而已,所以可以离散化)
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
//#pragma GCC optimize (2)
//#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 2e4+7;
int tot;
struct node{
int l,r,ans;}query[N];
int n,m;
int a[N];
int f[N];
int d[N];//d[i]表示i到根节点的路径上的异或和(0为偶1为奇)
void read_discrete(){
//离散化
cin>>n>>m;
over(i,1,m){
char str[5];
scanf("%d%d%s",&query[i].l,&query[i].r,str);
query[i].ans = (str[0] == 'o'?1:0);
a[++tot] = query[i].l-1;
a[++tot] = query[i].r;
}
sort(a+1,a+1+tot);
n = unique(a+1,a+1+tot) -a-1;
}
void init(){
for(int i = 1;i <= n;++i)
f[i] = i;
}
int Get(int x){
if(x == f[x])return x;
int root = Get(f[x]);
d[x]^=d[f[x]];
return f[x] = root;
}
int main()
{
read_discrete();
init();
for(int i = 1;i <= m;++i){
int x = lower_bound(a+1,a+1+n,(query[i].l-1)) - a ;
int y = lower_bound(a+1,a+1+n,(query[i].r)) - a;
int p = Get(x);
int q = Get(y);
if(p == q){
//如果在同一个集合
if((d[x]^d[y]) != query[i].ans){
printf("%d\n",i - 1);
return 0;
}
}
else {
f[p] = q;
d[p] = d[x]^d[y]^query[i].ans;
}
}
printf("%d\n",m);
return 0;
}
并查集擅长的是动态维护图中具有传递性的关系。有的时候,我们需要传递的关系比较单一,但有的时候,传递的关系会比较复杂。这时候就需要用到并查集的扩展域。扩展域并查集可以维护多组关系,其主要思想是将一个点拆分成好几个点来维护多组关系。
对,还是我QWQ
洛谷题目链接
4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884
输出
3512
有意思的一道并查集的题,需要一些思维。
用并查集来维护,当a和b并到一起的时候说明他们两个在同一个监狱之中。
本题要求最大的仇恨值最小,所以用结构体存数据,先排序,仇恨值最大的排在前面,遍历这个结构体数组,遵循把敌人的敌人和我放在一个监狱的原则来add即可。其中要注意如果可以都不在一个监狱不发生冲突就输出0,所以循环要从1到m+1,
这样到m+1
的时候,数据:0,0,0,
直接会check
输出 0
#include
using namespace std;
typedef long long ll;
const ll N=1e5+7;
const ll mod=2147483647;
ll n,m,w,b,c;
ll f[N],vis[N];
struct node
{
ll x,y,z;
bool operator<(const node &s)const
{
return z>s.z;
}
}a[N];
inline void init()//并查集必须要初始化!
{
for(int i=1;i<=n;++i)
f[i]=i;
}
inline ll finds(ll x)
{
return f[x]==x?x:f[x]=finds(f[x]);
}
inline void add(ll x,ll y)
{
x=finds(x);
y=finds(y);
f[x]=y;
}
inline bool check(ll x,ll y)
{
x=finds(x);
y=finds(y);
return x==y;
}
int main()
{
scanf("%lld %lld",&n,&m);
init();
for(int i=1;i<=m;++i)
{
scanf("%lld %lld %lld",&a[i].x,&a[i].y,&a[i].z);
}
sort(a+1,a+1+m);
for(int i=1;i<=m+1;++i)
{
if(check(a[i].x,a[i].y))
{
printf("%lld\n",a[i].z);
break;
}
if(!vis[a[i].x])vis[a[i].x]=a[i].y;
else add(vis[a[i].x],a[i].y);
if(!vis[a[i].y])vis[a[i].y]=a[i].x;
else add(vis[a[i].y],a[i].x);
}
return 0;
}
这道题总共有两个监狱,把一群人分到两个监狱里很明显就是一个二分图
那么就可以直接二分答案并用二分图判断即可
别人的代码和详解:
二分答案+二分图判断
有任何疑问欢迎评论哦虽然我真的很菜
#include
int fa[300005];
int n,k,ans;
/*inline int read()
{
int sum=0;
char ch=getchar();
while(ch>'9'||ch<'0')ch=getchar();
while(ch<='9'&&ch>='0')
sum=sum*10+ch-48,ch=getchar();
return sum;
}*/
int find(int x)
{
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
int join(int x,int y)
{
int r1=find(fa[x]),r2=find(fa[y]);
fa[r1]=r2;
}
int main()
{
int a,b,c;
scanf("%d%d",&n,&k);
for(int i=1;i<=3*n;++i)fa[i]=i;
for(int i=1;i<=k;++i)
{
scanf("%d%d%d",&a,&b,&c);
if(c>n||b>n){
ans++;continue;}
if(a==1)
{
if(find(b+n)==find(c)||find(b+2*n)==find(c)){
ans++;continue;}
join(b,c);join(b+n,c+n);join(b+2*n,c+2*n);
}
else if(a==2)
{
if(b==c){
ans++;continue;}
if(find(b)==find(c)||find(b+2*n)==find(c)){
ans++;continue;}
join(b,c+2*n);join(b+n,c);join(b+2*n,c+n);
}
}
printf("%d\n",ans);
return 0;
}