【CF891C】Envy-最小生成树+虚树+并查集

测试地址:Envy
题目大意: 给定一个带权无向连通图,每次询问给出 k i k_i ki条图中的边,问这些边能不能同时处在这个图的一棵最小生成树中。
做法: 本题需要用到最小生成树+虚树+并查集。
居然没看题解想出来了这道神题,非常开心。
首先,判断 k k k条边是不是能同时在最小生成树中,实际上只要求出包含这些边的边权和最小的生成树的权值,再和最小生成树比对即可。
怎么求呢?首先把 k k k条边先连上,形成一些连通块,注意到一个性质:要连通两个连通块,使用最小生成树上的边一定是最优的。证明可以用反证法,如果用的不是最小生成树上的边而且更优,那么这个“最小生成树”就不是最小的。这时我们想到先用并查集维护连通性,然后再暴力做一次最小生成树。这样算出的结果肯定是对的,但时间复杂度爆炸,因此我们需要优化。
注意到已经和其他点连通的点至多有 2 k 2k 2k个,我们想到使用虚树。这时我们发现,虚树外的那些最小生成树的边是一定要连的,不然就不连通,因此我们只需考虑虚树内的连通性。虚树上一条边就代表原图中的一条路径,且这些路径上的点都没有和其它点连通。我们尝试把这条路径缩成一条边。首先,路径内的点要连通,那么肯定是从路径的两端连树边,直到中间还有一条边没连上为止。这时,如果我们要求路径的两端不连通,那么我们就贪心地选择最大的边割掉即可,这条最大边显然可以通过倍增找到。于是要求路径两端连通就相当于在不连通的基础上,连通了那条最大的边,因此我们发现只有这条最大的边和这条路径的最优解有关,这样我们就把一条路径缩成了一条边。于是虚树中就只有 O ( k ) O(k) O(k)条边了,这时候再做最小生成树,时间复杂度就是 O ( k log ⁡ k ) O(k\log k) O(klogk),因此总的复杂度为 O ( ∑ k log ⁡ ∑ k ) O(\sum k\log \sum k) O(klogk),可以接受。
以下是本人代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,q,first[500010]={0},tot=0,totke,pos[500010],tim=0;
int ma[500010],mb[500010],pre[500010][21]={0},dep[500010]={0};
int p[2000010],totp,pe[500010],tote,fa[500010];
int st[500010],top;
ll mw[500010],mx[500010][21]={0},dis[500010]={0};
struct kruskaledge
{
	int a,b;
	ll w;
}me[500010];
struct treeedge
{
	int v,next;
	ll w;
}e[1000010];

bool cmp(kruskaledge a,kruskaledge b)
{
	return a.w<b.w;
}

void insert(int a,int b,ll w)
{
	e[++tot].v=b;
	e[tot].next=first[a];
	e[tot].w=w;
	first[a]=tot;
}

int find(int x)
{
	int r=x,i=x,j;
	while(r!=fa[r]) r=fa[r];
	while(i!=r) j=fa[i],fa[i]=r,i=j;
	return r;
}

void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	fa[fx]=fy;
}

ll kruskal(ll init,bool type)
{
	ll tot=init;
	sort(me+1,me+totke+1,cmp);
	for(int i=1;i<=totke;i++)
		if (find(me[i].a)!=find(me[i].b))
		{
			tot+=me[i].w;
			merge(me[i].a,me[i].b);
			if (type)
			{
				insert(me[i].a,me[i].b,me[i].w);
				insert(me[i].b,me[i].a,me[i].w);
			}
		}
	return tot;
}

void dfs(int v)
{
	pos[v]=++tim;
	for(int i=first[v];i;i=e[i].next)
		if (e[i].v!=pre[v][0])
		{
			pre[e[i].v][0]=v;
			dep[e[i].v]=dep[v]+1;
			mx[e[i].v][0]=e[i].w;
			dis[e[i].v]=dis[v]+e[i].w;
			dfs(e[i].v);
		}
}

bool cmpp(int a,int b)
{
	return pos[a]<pos[b];
}

int lca(int a,int b)
{
	if (dep[a]<dep[b]) swap(a,b);
	for(int i=20;i>=0;i--)
		if (dep[pre[a][i]]>=dep[b])
			a=pre[a][i];
	if (a==b) return a;
	for(int i=20;i>=0;i--)
		if (pre[a][i]!=pre[b][i])
			a=pre[a][i],b=pre[b][i];
	return pre[a][0];
}

ll findmx(int a,int b)
{
	ll ans=0;
	for(int i=20;i>=0;i--)
		if (dep[pre[a][i]]>=dep[b])
		{
			ans=max(ans,mx[a][i]);
			a=pre[a][i];
		}
	return ans;
}

void solve()
{
	int newp=totp;
	sort(p+1,p+totp+1,cmpp);
	for(int i=1;i<totp;i++)
		p[++newp]=lca(p[i],p[i+1]);
	totp=newp;
	sort(p+1,p+totp+1,cmpp);
	newp=0;
	for(int i=1;i<=totp;i++)
		if (i==1||p[i]!=p[newp])
			p[++newp]=p[i];
	totp=newp;
	
	ll basis=0,bestval=0;
	for(int i=1;i<=totp;i++)
		fa[p[i]]=p[i];
	for(int i=1;i<=tote;i++)
	{
		if (find(ma[pe[i]])==find(mb[pe[i]]))
		{
			printf("NO\n");
			return;
		}
		merge(ma[pe[i]],mb[pe[i]]);
		basis+=mw[pe[i]];
	}
	
	top=0;
	totke=0;
	for(int i=1;i<=totp;i++)
	{
		while(top&&lca(st[top],p[i])!=st[top]) top--;
		st[++top]=p[i];
		if (top>1)
		{
			ll sum,mx;
			sum=dis[st[top]]-dis[st[top-1]];
			mx=findmx(st[top],st[top-1]);
			bestval+=sum;
			basis+=sum-mx;
			if (find(st[top])!=find(st[top-1]))
			{
				me[++totke].a=st[top];
				me[totke].b=st[top-1];
				me[totke].w=mx;
			}
		}
	}
	
	if (kruskal(basis,0)==bestval) printf("YES\n");
	else printf("NO\n");
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%lld",&ma[i],&mb[i],&mw[i]);
		me[i].a=ma[i],me[i].b=mb[i],me[i].w=mw[i];
	}
	
	totp=n;
	totke=m;
	for(int i=1;i<=totp;i++)
		fa[i]=i;
	kruskal(0,1);
	
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++)
		for(int j=1;j<=n;j++)
		{
			pre[j][i]=pre[pre[j][i-1]][i-1];
			mx[j][i]=max(mx[j][i-1],mx[pre[j][i-1]][i-1]);
		}
	
	scanf("%d",&q);
	for(int i=1;i<=q;i++)
	{
		scanf("%d",&tote);
		totp=0;
		for(int i=1;i<=tote;i++)
		{
			scanf("%d",&pe[i]);
			p[++totp]=ma[pe[i]],p[++totp]=mb[pe[i]];
		}
		solve();
	}
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值