带权并查集
带权并查集
- 普通并查集可以高效地判断某个元素所属集合,以及判断两个元素是否在一个集合
- 带权并查集在普通并查集基础上额外维护了一个权值,这个权值可以表示同一集合内元素之间的某种关系,比如距离、差值等,通过这些关系可以在合并和查询中获得元素之间的某种关系
- 查询操作:在查找元素的根节点时,除了进行普通的路径压缩,还需要更新元素的权值。路径压缩的目的是将元素直接连接到根节点,以减少后续查找的时间复杂度。在更新权值时,需要根据当前元素到其父节点的权值以及其父节点到根节点的权值来计算当前元素到根节点的权值。
- 合并操作:在合并两个集合时,需要先找到两个元素的根节点,在查找过程中会调用find函数并自动修正dist中的值。然后将一个根节点的父节点设置为另一个根节点,并根据两个元素之间的关系更新权值。更新权值的具体方式取决于权值所表示的实际意义。
P8779 推导部分和
题目描述
对于一个长度为NNN的整数数列$A_{1}, A_{2}, \cdots A_{N} ,小蓝想知道下标,小蓝想知道下标,小蓝想知道下标l到到到r$的部分和 ∑i=lrAl \sum_{i=l}^{r}A_l i=l∑rAl是多少?
然而,小蓝并不知道数列中每个数的值是多少,他只知道它的MMM个部分和的值。其中第 iii 个部分和是下标lil_{i}li到rir_{i}ri的部分和 ∑j=liri=Ali+Ali+1+⋯+Ari \sum_{j=l_{i}}^{r_{i}}=A_{l_{i}}+A_{l_{i}+1}+\cdots+A_{r_{i}} j=li∑ri=Ali+Ali+1+⋯+Ari, 值是SiS_{i}Si。
思路
-dis[i]dis[i]dis[i]代表以iii为结尾的前缀和,那么给出的区间[l,r][l,r][l,r]代表了disr−disl−1dis_r-dis_{l-1}disr−disl−1,我们只需要将l−1l-1l−1向rrr连一条边权为sumsumsum的边即可
-mergemergemerge时若将xxx的根节点由fxfxfx变为fyfyfy,且dis(x,y)=wdis(x,y)=wdis(x,y)=w则fx到fy的距离可以看作 fx→x→y→fy fx \rightarrow x \rightarrow y \rightarrow fy fx→x→y→fy,即−disx+w+disy-dis_x+w+dis_y−disx+w+disy,其中disdisdis为节点到其集合根节点的距离
-queryqueryquery时,要求[l,r][l,r][l,r]的和,可看作l→fl→rl \rightarrow fl \rightarrow rl→fl→r,即 dis[l]−dis[r]d is[l]-dis[r]dis[l]−dis[r]
代码
const int N=1e5+10;int p[N];
ll dis[N];int find(int x)
{if(x!=p[x]){int fa=p[x];p[x]=find(p[x]);dis[x]+=dis[fa];}return p[x];
}void merge(int x,int y,ll w)
{int fx=find(x);int fy=find(y);if(fx!=fy){p[fx]=fy;dis[fx]=dis[y]-dis[x]+w;}return;
}ll query(int x,int y)
{int fx=find(x);int fy=find(y);if(fx!=fy) return INF;else return dis[x]-dis[y];//看作l->root->r
}void solve()
{int n,m,q;cin>>n>>m>>q;for(int i=0;i<=n;i++) p[i]=i;for(int i=1;i<=m;i++){ll l,r,w;cin>>l>>r>>w;merge(l-1,r,w);}while(q--){int l,r;cin>>l>>r;ll res=query(l-1,r);if(res==INF) cout<<"UNKNOWN"<<endl;else cout<<res<<endl;}}
P2294 [HNOI2005] 狡猾的商人
题目描述
刁姹接到一个任务,为税务部门调查一位商人的账本,看看账本是不是伪造的。账本上记录了nnn个月以来的收入情况,其中第iii个月的收入额为aia_iai,i=1,2,…,n−1,ni=1,2,\ldots,n-1,ni=1,2,…,n−1,n。当ai>0a_i>0ai>0时表示这个月盈利aia_iai元,当ai<0a_i<0ai<0时表示这个月亏损∣ai∣|a_i|∣ai∣元。所谓一段时间内的总收入,就是这段时间内每个月的收入额的总和。
刁姹的任务是秘密进行的,为了调查商人的账本,她只好跑到商人那里打工。她趁商人不在时去偷看账本,可是她无法将账本偷出来,每次偷看账本时她都只能看某段时间内账本上记录的收入情况,并且她只能记住这段时间内的总收入。
现在,姹总共偷看了mmm次账本,当然也就记住了mmm段时间内的总收入,你的任务是根据记住的这些信息来判断账本是不是假的。
思路
和上面的一样,记录区间前缀和信息,对于每个区间若之前已经处于同一集合中,那么我们可以queryqueryquery出其正确的区间和,和输入比较是否相等即可
代码
const int N=110;
int p[N];
ll dis[N];int find(int x)
{if(x!=p[x]){int fx=p[x];p[x]=find(p[x]);dis[x]+=dis[fx];}return p[x];
}void solve()
{int n,m;cin>>n>>m;for(int i=0;i<=n;i++) {p[i]=i;dis[i]=0;}for(int i=1;i<=m;i++){int a,b,c;cin>>a>>b>>c;a--;int fa=find(a);int fb=find(b);if(fa==fb){ll res=dis[a]-dis[b];if(res!=c) {cout<<"false"<<endl;return;}}else{p[fa]=fb;dis[fa]=dis[b]-dis[a]+c;}}cout<<"true"<<endl;
}
P1196 [NOI2002] 银河英雄传说
题目背景
题目描述
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成300003000030000列,每列依次编号为1,2,…,300001, 2,\ldots ,300001,2,…,30000。之后,他把自己的战舰也依次编号为1,2,…,300001, 2, \ldots , 300001,2,…,30000,让第iii号战舰处于第iii列,形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为 M i j
,含义为第iii号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第jjj号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。
然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j
。该指令意思是,询问电脑,杨威利的第iii号战舰与第jjj号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
思路
设置dis数组维护每个战舰距离其所在集合根节点有多远,即前面有多少个战舰,由于两个集合合并时会直接将一个战舰群放在另一个战舰群下面,我们还需要siz数组维护每个战舰群的大小,因此x和y所在集合合并时,dis[fx]=siz[fy]dis[fx]=siz[fy]dis[fx]=siz[fy],查询[l,r][l,r][l,r]之间战舰数量时,可看作l到根战舰的数量减去r到根战舰的数量的绝对值,由于根节点计算了两次,还需要-1,即abs(dis[l]−dis[r])−1abs(dis[l]-dis[r])-1abs(dis[l]−dis[r])−1
代码
const int N=3e4+10;int p[N];
int dis[N];
int siz[N];int find(int x)
{if(x!=p[x]){int fx=p[x];p[x]=find(p[x]);dis[x]+=dis[fx];}return p[x];
}void merge(int x,int y)
{int fx=find(x);int fy=find(y);if(fx!=fy){p[fx]=fy;dis[fx]=siz[fy];siz[fy]+=siz[fx];}return;
}ll query(int x,int y)
{int fx=find(x);int fy=find(y);if(fx!=fy) return INF;else return abs(dis[x]-dis[y])-1;
}void solve()
{int q;cin>>q;for(int i=1;i<=30000;i++) {p[i]=i;siz[i]=1;}while(q--){char op;int l,r;cin>>op>>l>>r;if(op=='M') merge(l,r);else{ll res=query(l,r);if(res==INF) cout<<-1<<endl;else cout<<res<<endl;}}
}
P2024 [NOI2001] 食物链
题目描述
动物王国中有三类动物A,B,CA,B,CA,B,C,这三类动物的食物链构成了有趣的环形。AAA吃BBB,$ B吃吃吃C,,, C吃吃吃A$。
现有NNN个动物,以1∼N1 \sim N1∼N编号。每个动物都是A,B,CA,B,CA,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这NNN个动物所构成的食物链关系进行描述:
- 第一种说法是
1 X Y
,表示XXX和YYY是同类。 - 第二种说法是
2 X Y
,表示XXX吃YYY。
此人对NNN个动物,用上述两种说法,一句接一句地说出KKK句话,这KKK句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中XXX或YYY比NNN大,就是假话;
- 当前的话表示XXX吃XXX,就是假话。
你的任务是根据给定的NNN和KKK句话,输出假话的总数。
思路
按深度分层后按照同余关系判断x和y的关系
- 同余为0:y为x的同类
- 同余为1:y为x的猎物
- 同余为2:y为x的天敌
代码
const int N=5e4+10;
int p[N];
int dis[N];int find(int x)
{if(x!=p[x]){int fx=p[x];p[x]=find(p[x]);dis[x]=(dis[x]+dis[fx])%3;}return p[x];
}void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=n;i++) p[i]=i;int ans=0;while(m--)//0:a=b 1:a>b 2:a<b{int a,b,c;cin>>c>>a>>b;int fa=find(a);int fb=find(b);if((a>n || b>n) || (c==2 && a==b)) {ans++;continue;}if(c==1){if(fa==fb && dis[a]!=dis[b]) ans++;else if(fa!=fb){p[fa]=fb;dis[fa]=(dis[b]-dis[a]+3)%3;}}if(c==2){if(fa==fb && (-dis[b]+dis[a]+3)%3!=1) ans++;else if(fa!=fb){p[fa]=fb;dis[fa]=(dis[b]-dis[a]+1+3)%3;}}}cout<<ans<<endl;}