题目大意
给出一个 n n n个点 m m m条边的无向图,每条边有边权,共 Q Q Q次询问,每次给出 k i k_i ki条边,问这些边能否同时在一棵最小生成树上。
Solution
首先明确两件事:
- 连通图中所有最小生成树中,所有权值的边的数量都是相等的。
- 各权值的边之间是无影响的。
分别说明:对于第一点,显然,由于用 K r u s k a l Kruskal Kruskal求出来的结果一定是最优的,如果某一权值的边少了,势必会多出一条权值更大的边,如果多了,那么势必会导致后面的边会无法加入,最终导致答案更劣。
对于第二点,我们考虑每一条边是否能加入生成树中,当权值小于当前边的边都处理过后,整张图会变成一个森林,并且各个连通块之间是没有权值小于当前边的(这不是显然吗),所以,当处理到当前的边时,如果当前边不能加入,是不可能通过改变之前的连接方式,使得方案仍然合法并且当前边能够加入生成树。换言之,之前所有权值的边的连接方式和当前边能否加入生成树是没有关系的,所以各权值的边之间是无影响的。
明确上述两点之后,就可以思考这题。首先我们一定可以将边按权值进行分类,每次都处理同一权值的边。根据上述的性质,如果能知道每次询问的边的权值时,并查集的状态,就可以方便地判断这条边是否能在生成树中。但是现在棘手的问题就是如何维护这个做到一半的并查集。
其实很简单,可以先预处理出连接完小于某个权值的边时,这个权值的边所连接的连通块的编号,根据性质二,这个编号是唯一的(除非是并查集合并时产生的差异,但是所指的连通块时同一个)。然后拿一个崭新的并查集去维护每个权值,在处理完一个权值后就还原改变过的并查集就可以了。
两个注意点:
由于要连接小于某一权值的边,所以在
K
r
u
s
k
a
l
Kruskal
Kruskal的时候必须要先存下同一权值的边,然后在预处理完所需要的东西之后(至于什么东西,懂得都懂),才能将边加入生成树。
同时注意,为什么不能用朴素的并查集,但是这样精准的复原就可以。因为题目中给出询问个数是 5 × 1 0 5 5\times10^5 5×105,显然,如果每次都是 O ( n ) O(n) O(n)的话,想不 T T T都难。但是有这么一句话
It is guaranteed that the sum of k i k_{i} ki for 1 < = i < = q 1<=i<=q 1<=i<=q does not exceed 5 ⋅ 1 0 5 5·10^{5} 5⋅105.
这就显然,只要每次是 O ( k ) O(k) O(k),总的复杂度是不会超出 O ( n ) O(n) O(n)的,可以通过。
综上,我们总结出一个可行的方案:
首先用
K
r
u
s
k
a
l
Kruskal
Kruskal来预处理出,小于当前边权的所有边都处理后,当前边所连接的连通块编号。
然后处理询问,对于每个询问,都先对边排序,然后对于同一边权的边,判断是否可以加入生成树中,利用预处理出的编号即可。
如果有边无法加入,即形成了环(用并查集判断即可),那么就是不合法的。
最后复原并查集即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define inf 1ll<<60
using namespace std;
const int MAXN=5e5+10;
struct node{
int u,v,w,id;
void input(){
scanf("%d%d%d",&u,&v,&w);
}
bool friend operator<(node a,node b){
return a.w<b.w;
}
}e[MAXN];
bool cmp(node x,node y){return x.id<y.id;}
int f[MAXN],conx[MAXN],cony[MAXN];
vector<node> cur;
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
int main()
{
int n,m,q;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
e[i].input();
e[i].id=i;
}
for(int i=1;i<=n;i++) f[i]=i;
sort(e+1,e+1+m);
int pre=e[1].w,lst=1;
for(int i=1;i<=m;i++){
if(e[i].w==pre) continue;//如果和上一次的权值相等,那么先存着
else{
for(int j=lst;j<i;j++){
int fu=find(e[j].u),fv=find(e[j].v);
conx[e[j].id]=fu;cony[e[j].id]=fv;//否则先处理连接的连通块
}
for(int j=lst;j<i;j++){
int fu=find(e[j].u),fv=find(e[j].v);
if(fu!=fv) f[fu]=fv;//然后加入生成树
}
pre=e[i].w;lst=i;
}
}
for(int j=lst;j<=m;j++){
int fu=find(e[j].u),fv=find(e[j].v);
conx[e[j].id]=fu;cony[e[j].id]=fv;
}
for(int j=lst;j<=m;j++){
int fu=find(e[j].u),fv=find(e[j].v);
if(fu!=fv) f[fu]=fv;
}
sort(e+1,e+1+m,cmp);
scanf("%d",&q);
for(int i=1;i<=n;i++) f[i]=i;
while(q--){
int k,id;
scanf("%d",&k);
cur.clear();
for(int i=1;i<=k;i++){
scanf("%d",&id);
cur.push_back(e[id]);
}
sort(cur.begin(),cur.end());
bool fl=1;
for(int i=0;i<cur.size()&&fl;){
int u=conx[cur[i].id],v=cony[cur[i].id],j=i+1;
if(u==v){fl=0;break;}
f[u]=v;
for(j=i+1;j<cur.size()&&cur[j].w==cur[i].w;j++){//把同权值的边进行处理
u=find(conx[cur[j].id]),v=find(cony[cur[j].id]);
if(u==v){fl=0;break;}//成环就退出
f[u]=v;
}
while(i<j)
f[conx[cur[i].id]]=conx[cur[i].id],
f[cony[cur[i].id]]=cony[cur[i].id],i++;//复原并查集
}
if(fl) puts("YES");
else puts("NO");
}
}