ZR最小生成树练习题
最小生成树练习题
CF 891C
题意
给定无向图,和K个询问,每个询问给定一个边集,求这个边集里的边是否是同一棵最小生成树里的边
思路
- 首先,我们应该明确。
- 如果一条边权为 \(w\) 的边,在边权小于 \(w\) 的边都尝试加入,原图已连通时,则这条边一定不会出现在 \(MST\) 中,否则则可能出现在 \(MST\) 中
- 且对于边权相同的一些边我们
- 那么,我们可以考虑如果只有一个询问,我们把询问中的边从小到大排序并去重,对于当前的一条边权为 \(w\) 的边,我们用原图中边权小于 \(w\) 的边建一个森林或树。将这条边加入后如果出现环,那这条边一定不在最小生成树中,这个询问也就是 \(False\)
- 对于多组询问,每次询问都要保证是原图的联通性,那就要求并查集要有撤销操作。
- 我们可以按边权排序,对于存在这种边的询问依次判断。每次询问加完边后都要撤销
- 所以,并查集要支持撤销,可以按秩合并
小结
- 并查集按秩合并和撤销操作
代码
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
#include <utility>
#define M 500005
#define N 100005
using namespace std;
int n,m,k,q,top;
int fa[M],stk[M],s[M],ans[M];
struct node{
int id,from,to,val;
}e[M];
vector<node> a[M];
bool cmp(node a,node b){
return a.val <b.val ;
}
int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0',ch=getchar();}
return s*w;
}
int find(int x){
while(x!=fa[x]) x=fa[x];
return x;
}
bool unin(int x,int y){
int fx=find(x),fy=find(y);
if(fx==fy) return 0;
if(s[fx]>s[fy]) swap(fx,fy);
fa[fx]=fy;
s[fy]+=s[fx];
stk[++top]=fx;
return true;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
e[i].from =a,e[i].to =b,e[i].val =c;
}
scanf("%d",&q);
for(int i=1;i<=q;i++){
scanf("%d",&k);
for(int j=1;j<=k;j++){
int x;
scanf("%d",&x);
a[e[x].val].push_back({i,e[x].from,e[x].to,e[x].val});
}
}
sort(e+1,e+m+1,cmp);
for(int i=1;i<=n;i++) fa[i]=i,s[i]=1;
for(int i=1;i<=m;i++){
int val=e[i].val ;
top=0;
for(int j=0;j<a[val].size();j++){
if(ans[a[val][j].id]) continue;
if(j>0&&a[val][j].id!=a[val][j-1].id){
while(top){
int x=stk[top--];
s[fa[x]]-=s[x];
fa[x]=x;
}
}
if(!unin(a[val][j].from,a[val][j].to)) ans[a[val][j].id]=1;
}
while(e[i].val ==val) unin(e[i].from ,e[i].to),i++;
if(e[i].val!=val) i--;
}
for(int i=1;i<=q;i++)
if(ans[i]) printf("NO\n");
else printf("YES\n");
return 0;
}
CF125E
题意
- 给定 \(N\)个点 \(M\) 条边的无向图,求出 \(1\) 号点出度为 \(k\) 的最小生成树
- \(N<=5000,M<=10^5\)
思路
- 我们考虑先跑一遍最小生成树,如果1号点的度数是k,那么直接得到答案
- 如果度数<k,我们可以把1号点连的边边权统一减掉\(x\)
- 反之,如果度数\(>k\) ,我们则加上 \(x\)
- 那么,这个x取多少是合适的呢
- 由于\(x\)的取值具有单调性
- 我们可以考虑二分 \(x\)
- 但是有一个问题,最小生成树的形态不一,也就是说可能有与一号点连的边权值相同的边。我们应选与一号点连\(k\) 条边的那一条,但这不好操作。我们多二分几次,调整x使得生成的最小生成树恰好满足选与1号点相连的边。
小结
- 求最小度限制生成树,二分增量
Bug
- 写二分的时候注意 \(l,r\) 的取值
代码
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define M 100005
using namespace std;
int n,m,k,res,jus;
int fa[5005],ton[5005];
struct node{
int from,to,id;
int val;
bool tag;
}e[M],oge[M];
int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0',ch=getchar();}
return s*w;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
bool cmp(node a,node b){
if(a.val==b.val) return a.from <b.from ;
return a.val <b.val ;
}
int SPFA(){
int res=0;ton[0]=0;
for(int i=1;i<=n;i++) fa[i]=i;
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++){
int x=find(e[i].from) ,y=find(e[i].to) ;
if((x==y)||(e[i].from ==1&&res==k)) continue;
fa[x]=y;
if(e[i].from ==1) res++;
ton[++ton[0]]=e[i].id ;
}
return res;
}
bool check(int x){
for(int i=1;i<=m;i++) {
e[i]=oge[i];
if(oge[i].from ==1) e[i].val +=x;
}
int now=SPFA();
if(now==k) return 1;
return 0;
}
int main()
{
n=read();m=read();k=read();
for(int i=1;i<=m;i++){
oge[i].id=i,oge[i].from =read(),oge[i].to =read(),oge[i].val=read();
if(oge[i].from> oge[i].to){
int tep=oge[i].from ;
oge[i].from=oge[i].to ;
oge[i].to =tep;
}
}
sort(oge+1,oge+m+1,cmp);
int l=-100000,r=100000,ans;
while(l<=r){
int mid=(l+r)/2;
if(check(mid)) l=mid+1,ans=mid;
else r=mid-1;
// if(check(mid)==k) {ans=mid;break;}
// else if(check(mid)<k) r=mid-1;
// else if(check(mid)>k) l=mid+1,ans=mid;
// printf("%d %d %d %d\n",l,mid,r,check(mid));
// cout<<ans<<" "<<check(ans)<<endl;
}
if(!check(ans)||(ton[0]!=n-1)){printf("-1\n");return 0;}
printf("%d\n",n-1);
for(int i=1;i<=ton[0];i++) printf("%d ",ton[i]);
// for(int i=1;i<=m;i++)
// if(e[i].tag){printf("%d ",e[i].id);
// printf("%d %d %d\n",e[i].id,e[i].from ,e[i].to);
// }
return 0;
}
BZOJ 3732(Kruskal重构树
题意
给出一张 \(N\) 个点 \(M\) 条边的无向图。
有 \(Q\) 组询问,每组询问两点 \((s,t)\) 间,所有路径当中最长边的最小值是多少。
思路
- 必然是走在最小瓶颈生成树上的路径。
- 那么可以求出最小生成树后支持查找树上路径最大值。
- 传统做法树剖、倍增均可。
- 若使用 \(Kruskal\) 重构树,两点间路径最大值即为重构树上的两点 \(LCA\)。
Kruskal重构树
- 在 \(Kruskal\) 实现过程中, 在使用并查集合并两个集合(两棵树)的根时,我们新建一个点权为连接这两个集合的边权大小的节点作为树根。
- 性质:
- 最小生成树上两点间边权最大值为重构树上的LCA的点权
- 重构树是二叉树,点数是 \(2n-1\) ,边数是 \(4n-2\) ,深度是 \(n\)
- 整棵树是一个大根堆
Bug
- 重构树的边数应该是点数的4倍,边数的2倍
- 我又开小了一半
小结
- \(kruskal\) 重构树的空间一定要开够
代码
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define N 30005
#define M 30005
using namespace std;
int n,m,k,res,tot;
int head[N],fa[N],f[N][21],a[N],dep[N];
struct node{
int from,to,net,val;
}e[M<<1],mp[M];
void add(int a,int b){
e[++tot].to=b,e[tot].net =head[a],head[a]=tot;
e[++tot].to=a,e[tot].net =head[b],head[b]=tot;
}
bool cmp(node a,node b){
return a.val <b.val ;
}
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void work(){
for(int i=1;i<2*n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int u=mp[i].from ,v=mp[i].to ,w=mp[i].val ;
int fu=find(u),fv=find(v);
if(fu==fv) continue;
// cout<<fu<<" "<<fv<<" ";
++res;
fa[fu]=res;
fa[fv]=res;
// cout<<fa[fu]<<" "<<fa[fv]<<endl;
a[res]=w;
add(fu,res);
add(fv,res);
f[fu][0]=res;
f[fv][0]=res;
// cout<<"D"<<endl;
if(res==2*n-1) break;
}
}
void dfs(int x,int last){
for(int i=head[x];i;i=e[i].net){
int to=e[i].to ;
if(to==last) continue;
dep[to]=dep[x]+1;
dfs(to,x);
}
}
void pre(){
for(int j=1;j<=19;j++)
for(int i=1;i<=res;i++) f[i][j]=f[f[i][j-1]][j-1];
}
int LCA(int x,int y){
if(dep[x]<dep[y]) swap(x,y);
for(int i=19;i>=0;i--)
if(dep[f[x][i]]>=dep[y]) x=f[x][i];
if(x==y) return x;
for(int i=19;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
mp[i].from=a,mp[i].to=b,mp[i].val =c;
}
sort(mp+1,mp+m+1,cmp);
res=n;
work();
dep[res]=1;
dfs(res,0);
// f[res][0]=res;//<dep[i]<<" "<<f[i][0]<<" "
// for(int i=1;i<=res;i++) cout<<a[i]<<endl;
// cout<<endl;
// cout<<"De"<<endl;
pre();
while(k--){
int u,v;
scanf("%d%d",&u,&v);
// cout<<u<<" "<<v<<" "<<LCA(u,v)<<endl;
printf("%d\n",a[LCA(u,v)]);
}
return 0;
}
归程-pre
题意
给定 \(N\) 个点 \(M\) 条边的无向图
有 \(k\) 组询问。每次问从一个点 \(s_i\)出发,只允许经过边权\(<=w_i\)的边,能够到达的点权第\(k_i\)小的点。
思路
- 考虑暴力从每个起点出发bfs,能够走到的边就是一个连通块。答案是这个连通块中点权第 \(k_i\) 小的点。
- 题目条件等价于联通块里的最大边权小于等于 \(w_i\) ,对于最大边权小于等于 \(w_i\) ,完全等价于最小瓶颈生成树上路径边权最大值的限制
- //贪心的考虑走最小瓶颈生成树上的边这样可以使边权尽量小
- 考虑\(kruskal\)重构树,一个节点能到达的边权小于等于 \(w_i\) 的点在重构树上是一个子树。子树的根的点权 \(<=w_i\) 。
- 问题就转化成求子树内点权第 \(k_i\) 小的点
- 这个问题可以用主席树轻松解决,但是我不会
小结
- 记住可以用kruskal重构树做。暴力求子树点权第 \(k\) 小
归程
题意
给出一张N个点M条边的图。每条边有两个值:海拔和距离。
接下来Q组询问,每次给出水位线和起点。求
从起点出发到1号节点的最小步行距离。 从起点出发时可以驾车,驾车不算做步行距离。但车不能经过海拔低于水位线的边,在那之前需要弃车。
简单来说,就是对于起点,找到边权最小值大于等于 \(w\) 的边集中到1号点的最短路
思路
- 找从一个点到可以到达的点集,并边权满足不等式。
- 这问题在上题已经解决了
- 我们以海拔建最大瓶颈生成树。
- 以海拔建重构树,以每个点到1号点的最短路为重构树叶子节点上的点权
- 用倍增求出子树根,在这个子树中求点权最小的点。
- 在子树中求最小的点权可以dfs一下
Bug
- 跑最短路的图是原图,不能在重构树上跑,所以要建两次图。
- 建最大瓶颈生成树的点要2倍,边是点的4倍
- 多测,要清零,尤其是存边的变量tot和tit。
- 倍增往上跳的时候找到大于水位线的最高祖先
代码
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<vector>
#include<cstring>
#define M 400005
#define N 200005
#define ll long long
using namespace std;
typedef pair<int,int> pll;
int T,n,m,Q,K,S,tot,res,tit;
int head[M],tou[M],a[M],fa[M],dep[M],f[M][20],dis[M];
struct node{
int from,to,high,net,len;
}mp[M],e[M*4],tu[M<<1];
void add(int x,int y){
e[++tot].to=y,e[tot].net =head[x],head[x]=tot;
e[++tot].to=x,e[tot].net =head[y],head[y]=tot;
}
void add2(int x,int y,int z){
tu[++tit].to=x,tu[tit].len =z,tu[tit].net =tou[y],tou[y]=tit;
tu[++tit].to=y,tu[tit].len =z,tu[tit].net =tou[x],tou[x]=tit;
}
bool cmp(node a,node b){
return a.high >b.high ;
}
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void kruskal(){
res=n;
for(int i=1;i<2*n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int x=mp[i].from,y=mp[i].to ,z=mp[i].high ;
int fx=find(x),fy=find(y);
if(fx==fy) continue;
++res;
add(fx,res);
add(fy,res);
fa[fx]=res;
fa[fy]=res;
a[res]=z;
f[fx][0]=res;
f[fy][0]=res;
if(res==2*n-1) break;
}
}
void spfa(){
priority_queue<pll,vector<pll>,greater<pll> > q;
for(int i=1;i<=n;i++) dis[i]=1e9+7;
q.push(make_pair(0,1));
dis[1]=0;
while(!q.empty()){
int u=q.top().second;
if(dis[u]!=q.top().first){
q.pop() ;
continue;
}
q.pop() ;
for(int i=tou[u];i;i=tu[i].net){
int to=tu[i].to ;
if(dis[to]>dis[u]+tu[i].len){
dis[to]=dis[u]+tu[i].len ;
q.push(make_pair(dis[to],to));
}
}
}
}
void dfs(int x,int last){
for(int i=head[x];i;i=e[i].net){
int to=e[i].to ;
if(to==last) continue;
dep[to]=dep[x]+1;
f[to][0]=x;
dfs(to,x);
dis[x]=min(dis[x],dis[to]);
}
}
void cler(){
tot=0;tit=0;//bug
memset(tou,0,sizeof(tou));
memset(head,0,sizeof(head));
memset(mp,0,sizeof(mp));
memset(f,0,sizeof(f));
memset(dep,0,sizeof(dep));
}
inline int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return s*w;
}
int main()
{
T=read();
while(T--){
cler();
n=read();m=read();
for(int i=1;i<=m;++i){
int u,v,l,a;
u=read();v=read();l=read();a=read();
add2(u,v,l);
mp[i].from =u,mp[i].to =v,mp[i].high =a,mp[i].len =l;
}
sort(mp+1,mp+m+1,cmp);
kruskal();
spfa();
for(int i=n+1;i<=res;i++) dis[i]=1e9+7;
dep[res]=1;
dfs(res,0);
for(int j=1;j<=19;j++)
for(int i=1;i<=res;i++)f[i][j]=f[f[i][j-1]][j-1];
Q=read();K=read();S=read();
int lastans=0;
while(Q--){
int v,p;
v=read();p=read();
v=(v+K*lastans-1)%n+1;
p=(p+K*lastans)%(S+1);
for(int i=19;i>=0;i--)
if(dep[v]-(1<<i)>0&&a[f[v][i]]>p) v=f[v][i];
lastans=dis[v];
printf("%d\n",lastans);
}
}
return 0;
}
HNOI2006旅行
题目传送
思路
- 求路径上的最大边比最小边最小。
- 枚举最小边,将大于它的边一条一条的往里加,直到其联通,当然边从小到大排序。联通时的边一定是以当前边为最小边的答案。
- 一个疑问是这样做的前提是可以重复走边
小结
- 枚举最小边,比它大的往里加
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 505
using namespace std;
int n,up,down,m,s,t;
int fa[N];
bool first;
struct node{
int from,to,val;
}e[5005];
inline int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
bool cmp(node a ,node b){
return a.val <b.val;
}
int find(int x){
if(x==fa[x]) return fa[x];
return fa[x]=find(fa[x]);
}
int gcd(int x,int y){
if(y==0) return x;
return gcd(y,x%y);
}
void work(){
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++){
// int K=0;
for(int j=1;j<=n;j++) fa[j]=j;
for(int k=i;k<=m;k++){
int u=e[k].from ,v=e[k].to ;
int fu=find(u),fv=find(v);
if(fu==fv) continue;
fa[fu]=fv;
if(find(s)==find(t)){
if(first==0) first=1,up=e[k].val,down=e[i].val;
if(up*e[i].val>down*e[k].val){
up=e[k].val ;
down=e[i].val;
}
break;
}
}
// cout<<find(s)<<" "<<find(t)<<endl;
// if(find(s)!=find(t)) {
// printf("IMPOSSIBLE\n");
// continue;
// }
// cout<<e[i].val<<" "<<e[K].val<<endl;
}
}
int main(){
n=read();m=read();
for(int i=1;i<=m;i++) e[i].from =read(),e[i].to=read(),e[i].val =read();
s=read();t=read();
work();
// cout<<up<<" "<<down<<endl;
if(first==0) {
printf("IMPOSSIBLE\n");
return 0;
}
int gd=gcd(up,down);
up/=gd;
down/=gd;
if(down==1) printf("%d",up);
else printf("%d/%d",up,down);
return 0;
}