一些复杂最小生成树的例题

1 . CF609E Minimum spanning tree for each edge

luogu传送门

CodeForces传送门

前置芝士:\(Kruskal\) 算法求最小生成树,\(ST\) 表倍增。

题意

给你 \(n\) 个点,\(m\) 条边,对于编号为 \(i\) 的边求出必须包含 \(i\) 的最小生成树权值和。

很好理解,不做赘述。

题解

首先,我们不考虑每条边的限制,先将整张图的最小生成树求出,连边建树并求权值和。

现在加入限制。

对于第 \(i\) 条边,如果 \(i\) 在最小生成树上,直接输出权值和即可。

不在的情况值得讨论。

假设我们直接在树上加入了第 \(i\) 条边,设 \(i\) 连接 \(u\)\(v\),那么我们在加入的同时需要断掉原最小生成树上 \(u\)\(v\) 路径上的一条边,否则就不是一棵树了。

显而易见,设最小生成树权值和为 \(all\)\(i\) 的边权为 \(w\) ,短边边权为 \(w'\)

则有 \(ans=all+w-w'\)

所以显然需要断掉 \(u\)\(v\) 路径上边权最大的边。

求出这条边显然可以使用倍增。

所以整体思路就很清楚了,先建出最小生成树,再考虑每一条边,若在树上直接输出权值和,否则求个倍增最大值。结束了。

code

//writer:Oier_szc

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m;
struct node
{
	int u,v,w,id;
	bool operator<(const node &W) const
	{
		return w<W.w;
	}
}e[N];
int fa[N];
int find(int u)
{
	if(fa[u]==u) return fa[u];
	else return fa[u]=find(fa[u]);
}
void merge(int a,int b)
{
	fa[a]=b;
}
int head[N],ne[N<<1],to[N<<1],w[N<<1],tot=0;
void add(int u,int v,int W)
{
	to[++tot]=v;
	w[tot]=W;
	ne[tot]=head[u];
	head[u]=tot;
}
bool in[N];
int ANS=0,p[20][N],deep[N],st[20][N];
void dfs(int u,int fa)
{
	p[0][u]=fa;
	deep[u]=deep[fa]+1;
	for(int i=head[u];i;i=ne[i])
	{
		if(to[i]==fa) continue;
		st[0][to[i]]=w[i];
		dfs(to[i],u);
	}
}
void init()
{
	for(int i=1;i<20;++i)
	{
		for(int j=1;j<=n;++j)
		{
			if(p[i-1][j]!=-1) 
			{
				p[i][j]=p[i-1][p[i-1][j]];
				st[i][j]=max(st[i-1][j],st[i-1][p[i-1][j]]);
			}
		}
	}
}
int LCA(int a,int b)
{
	if(deep[b]>deep[a]) swap(a,b);
	int maxn=0;
	for(int i=19;i>=0;--i)
	{
		if(p[i][a]!=-1&&deep[p[i][a]]>=deep[b])
		{
			maxn=max(maxn,st[i][a]);
			a=p[i][a];
		}
	}
	if(a==b) return maxn;
	for(int i=19;i>=0;--i)
	{
		if(p[i][a]!=-1&&p[i][a]!=p[i][b])
		{
			maxn=max(maxn,max(st[i][a],st[i][b]));
			a=p[i][a];
			b=p[i][b];
		}
	}
	return max(maxn,max(st[0][a],st[0][b]));
}
int ans[N];
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;++i)
	{
		scanf("%lld%lld%lld",&e[i].u,&e[i].v,&e[i].w);
		e[i].id=i;
	}
	sort(e+1,e+1+m);
	int fu,fv;
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1;i<=m;++i)
	{
		fu=find(e[i].u);
		fv=find(e[i].v);
		if(fu==fv) continue;
		merge(fu,fv);
		add(e[i].u,e[i].v,e[i].w);
		add(e[i].v,e[i].u,e[i].w);
		in[e[i].id]=true;
		ANS+=e[i].w;
	}
	dfs(1,-1);
	init();
	for(int i=1;i<=m;++i)
	{
		if(in[e[i].id]) ans[e[i].id]=ANS;
		else
		{
			ans[e[i].id]=ANS+e[i].w-LCA(e[i].u,e[i].v);
		}
	}
	for(int i=1;i<=m;++i)
	{
		printf("%lld\n",ans[i]);
	}
	return 0;
}

2 . CF888G Xor-MST

luogu传送门

CodeForces传送门

前置芝士:Boruvka算法,01Trie

题意

给你一张最多有 \(2 \times 10^5\) 个点的完全图,两两之间边权为两点点权的异或,让你求最小生成树。

题解

又神又难写的题。

对于完全图的最小生成树,显然 \(Prim\)\(Kruskal\) 都难以胜任,这道题涉及到了一个全新的算法叫 \(Boruvka\) 算法。

\(Boruvka\) 算法流程

其实和 \(Kruskal\) 很类似。首先还是并查集。

先将所有单个点的祖先调整成自己。然后开始合并连通块。

对于每一次合并,我们枚举点,找到每一个连通块连到其它连通块的最小的一条边,然后将两个连通块合并。

当然,这里要注意是,可能会出现重边的情况。例如连通块A连出去到了连通块B,但连通块B连出去的最小边正好是集合A,不判重会算两次,会出问题。

对于复杂度,首先看每一次合并,显然是\(O(E)\);然后看合并次数,可以发现每次合并连通块数量至少减半,所以最多合并\(O(log\ N)\)次,总复杂度自然是\(O(E\ log\ N)\)

如果想深入学习该算法可以右转oi-wiki

回到题目x1

直接套这个算法怎么行,边都到 \(10\)\(10\) 次方级别了。

考虑优化找到最小边的过程。

对于一个点,如果想找到其它点与其异或起来的最小值,有一个很常用的套路。那就是 \(01Trie\)

如何找呢?再看一道模板题。

最长异或路径

会的跳过捏。

这里只是最小值变成了最大值。

不妨将每一个数变成二进制串,从高往低位插入字典树。

考虑访问。根据贪心的思想,对于一个数 \(a\) 的二进制表示,显然在访问的过程中要尽可能使得较高位尽可能与 \(a\) 的同位不同,因为较高位比下面几位都大,显然这样才能将异或结果最大化。

所以问题的解显而易见,在访问时从最高位开始尽可能的与 \(a\) 不同,最后返回值即可。

回到题目x2

同理,我们就可以通过 \(01Trie\) 求出每个点连到其它连通块的最小值。只要将访问时尽可能相反变成尽可能相同,即可将一次合并复杂度变为 \(O(N\ log\ V)\)

但是具体如何套进题目还有一堆细节。

显然,\(01Trie\) 上访问时不能访问到当前连通块的元素。为解决,不妨将 \(Trie\) 想成一个普通的树,我们设 \(l[i]\)\(r[i]\) ,表示 \(i\) 号点子树内所有元素所在连通块编号的最小值和最大值,那么当 \(l[to]=r[to]=id\)\(id\) 表示现在查询的元素所在的连通块)时,就不能进入子树 \(to\) 中访问。

接着很重要的一点是 \(a[i]\) 一定要去重,不然建 \(01Trie\) 时会出大问题,会 \(TLE\)

还有一点就是每一轮 \(Boruvka\) 中的合并后要将整个 \(Trie\) \(dfs\) 一遍,更新 \(01Trie\) 内所有点的 \(l[i]\)\(r[i]\),这相对好办。

最后,由于CF的毒瘤数据,全局 \(long long\)\(MLE\) ,只要将 \(ans\) 定义成 \(long long\) 即可。

将所有细节整合起来,耐心码一下代码,你就能A一道上位紫了!

code

//writer:Oier_szc

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n;
long long ans=0;
int a[N];
int tr[N*32][2],ID[N*32],idx=0;
int l[N*32],r[N*32];
int fa[N];
inline int find(int u)
{
	if(fa[u]==u) return fa[u];
	else return fa[u]=find(fa[u]);
}
inline void insert(int x,int id)
{
	int u=0;
	for(register int i=29;i>=0;--i)
	{
		int to=(x>>i)&1;
		if(!tr[u][to]) tr[u][to]=++idx;
		u=tr[u][to];
	}
	ID[u]=id;
}
inline void dfs(int u)
{
	if(ID[u])
	{
		l[u]=r[u]=find(ID[u]);
		return;
	}
	l[u]=0x3f3f3f3f;
	r[u]=0;
	if(tr[u][0])
	{
		dfs(tr[u][0]);
		l[u]=min(l[u],l[tr[u][0]]);
		r[u]=max(r[u],r[tr[u][0]]);
	}
	if(tr[u][1])
	{
		dfs(tr[u][1]);
		l[u]=min(l[u],l[tr[u][1]]);
		r[u]=max(r[u],r[tr[u][1]]);
	}
}
inline pair<int,int> query(int x,int id)
{
	int u=0,res=0;
	for(register int i=29;i>=0;--i)
	{
		int to=(x>>i)&1;
		if(tr[u][to]&&!(l[tr[u][to]]==r[tr[u][to]]&&l[tr[u][to]]==id))
		{
			res=(res<<1)+to;
			u=tr[u][to];
		}
		else if(tr[u][!to]&&!(l[tr[u][!to]]==r[tr[u][!to]]&&l[tr[u][!to]]==id))
		{
			res=(res<<1)+!to;
			u=tr[u][!to];
		}
		else break;
	}
	return make_pair(res,ID[u]);
}
int best[N],bestid[N];
signed main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;++i)
	{
		scanf("%d",&a[i]);
	} 
	sort(a+1,a+1+n);
	n=unique(a+1,a+1+n)-a-1;
	for(int i=1;i<=n;++i) 
	{
		fa[i]=i;
		insert(a[i],i);
	}
	int cnt=0; 
	while(cnt<n-1)
	{
		for(register int i=1;i<=n;++i)
		{
			best[i]=0x3f3f3f3f;
		}
		dfs(0);
		pair<int,int> qwq; 
		int fu,fv;
		for(register int i=1;i<=n;++i)
		{
			fu=find(i);
			qwq=query(a[i],fu);
			fv=find(qwq.second);
			if((a[i]^qwq.first)<best[fu])
			{
				best[fu]=(a[i]^qwq.first);
				bestid[fu]=fv;
			}
			if((a[i]^qwq.first)<best[fv])
			{
				best[fv]=(a[i]^qwq.first);
				bestid[fv]=fu;
			}
		}
		for(register int i=1;i<=n;++i)
		{
			if(fa[i]!=i) continue;
			if(best[i]<0x3f3f3f3f&&bestid[i]!=0)
			{
				if(bestid[bestid[i]]==i) bestid[bestid[i]]=0;//判重边
				fa[i]=bestid[i];
				ans+=best[i];
				++cnt;
			}
		}
	}
	printf("%lld",ans);
	return 0;
}

3 . [NOI2018] 归程

luogu传送门

前置芝士:最短路,\(kruskal\) 重构树,倍增

题意

给你一张最多 \(2 \times 10^5\) 个点和 \(4 \times 10^5\) 条边的图,每条边有两个属性:长度和海拔。对于水位线 \(p\) ,称所有海拔不超过 \(p\) 的边为有积水的边。定义一条由某点到 \(1\) 路径的步行长度为:将路径分成从出发点到某点的不能含积水边的一段和从那个点到 \(1\) 的可以含积水边的一段,"步行长度" 即为可以含积水边的一段的长度。给出 \(q\) 次询问,每次给出出发点 \(v\) 和水位线 \(p\) ,求最短步行长度。

笔者语文不好,只能概括成这样了。原题其实讲得很清楚,可以去那边看。

题解

将路径拆成两部分:开车和走路。

发现每次询问走路的部分都是某点到 \(1\) 的最短路,先从 \(1\) 开始跑个 \(Dijkstra\) 不用多说。

然后考虑开车的部分。

假设在点 \(v\) 下车,和上面求出的最短路结合,则有:

1:\(u\)\(v\) 路径的所有边海拔大于 \(s\)

2:\(v\) 是每一条路径上从 \(u\) 到第一条海拔小于 \(s\) 的边经过的所有点中,\(dis\) 最小的。

简化到这里,考虑一个叫 \(kruskal\) 重构树的高科技。

\(kruskal\) 重构树

最小或最大生成树的另一种表示。

还是按边权排序然后加边。但是,我们每加入一个边,假设连接了 \(a\)\(b\) ,那么新建一点 \(c\) 连接两点,其点权即为加入边的边权。说白了就是将边用点表示。

这样建出的树会有几个很有用的性质。

1:树的大小为 \(2 \times n-1\)

2:树是一个大根堆或小根堆。即所有节点往上跳的点权单调。

3:两个点之间原图上所有路径中的最大边权的最小值/最小边权的最大值为 \(kruskal\) 重构树上两点 \(LCA\) 的权值。事实上 \(3\)\(2\) 推导来。

回到原题

考虑根据海拔建重构树。

然后可以顺便将重构树中每个节点的子树内所有叶子节点(对应原图节点)的 \(dis\) 最大值记录下来。为方便说明用 \(dis2\) 来表示。

根据上面说的性质3,所以若 \(v\) 可以到达 \(v'\) ,则重构树上 \(LCA(v,v')\) 的点权大于 \(s\)

所以对于每一个询问,考虑从 \(v\) 开始往上跳,每跳到一个点将答案与 \(dis2[now]\) 取个 \(max\) (因为 \(now\) 子树内所有点的海拔一定不大于 \(now\) 的海拔,全部用 \(dis2\) 括起来考虑即可。\(now\) 为当前跳到的节点。)。直到跳到了一个点权小于等于 \(s\) 的重构树节点为止。

但是这样还是不够快,这时候可以考虑使用倍增。总时间复杂度 \(O(T \times n\ log\ n)\)

code

//writer:Oier_szc

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e5+5,M=8e5+5;
int t,n,m;
struct node
{
	int u,v,w;
	bool operator<(const node &W) const
	{
		return w>W.w;
	}
}e[M];
int head[N],ne[M<<1],to[M<<1],w[M<<1],tot=0;
void add(int u,int v,int W)
{
	to[++tot]=v;
	w[tot]=W;
	ne[tot]=head[u];
	head[u]=tot;
}
struct Node
{
	int id,quan;
	bool operator<(const Node &W) const
	{
		return quan>W.quan;
	};
};
priority_queue<Node> q;
int dis[N<<1],vis[N];
void dij()
{
	q.push(Node{1,0});
	memset(dis,0x3f,sizeof(dis));
	memset(vis,false,sizeof(vis));
	dis[1]=0;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		if(vis[now.id]) continue;
		vis[now.id]=true;
		for(int i=head[now.id];i;i=ne[i])
		{
			if(dis[now.id]+w[i]<dis[to[i]])
			{
				dis[to[i]]=dis[now.id]+w[i];
				q.push(Node{to[i],dis[to[i]]});
			}
		}
	}
}
int fa[N<<1];
int find(int u)
{
	if(fa[u]==u) return fa[u];
	else return fa[u]=find(fa[u]);
}
int p[N<<1][21],H[N<<1],idx;
void exkru()
{
	for(int i=1;i<=n*2;++i) fa[i]=i;
	sort(e+1,e+1+m);
	int fu,fv;
	idx=n;
	for(int i=1;i<=m;++i)
	{
		fu=find(e[i].u);
		fv=find(e[i].v);
		if(fu==fv) continue;
		fa[fu]=fa[fv]=++idx;
		H[idx]=e[i].w;
		p[fu][0]=p[fv][0]=idx;
		dis[idx]=min(dis[fu],dis[fv]);
	}
	for(int i=1;i<=20;++i)
	{
		for(int j=1;j<=idx;++j)
		{
			p[j][i]=p[p[j][i-1]][i-1];
		}
	}
}
int find(int u,int a)
{
	for(int i=20;i>=0;--i)
	{
		if(p[u][i]&&H[p[u][i]]>a)
		{
			u=p[u][i];
		}
	}
	return dis[u];
}
signed main()
{
	scanf("%lld",&t);
	while(t--)
	{
		memset(head,0,sizeof(head));
		memset(p,0,sizeof(p));
		tot=0;
		scanf("%lld%lld",&n,&m);
		int u,V,l,a;
		for(int i=1;i<=m;++i)
		{
			scanf("%lld%lld%lld%lld",&u,&V,&l,&a);
			add(u,V,l);
			add(V,u,l);
			e[i]=node{u,V,a};
		}
		dij();
		exkru();
		int q,k,s,Ans=0,v0,p0,v,p;
		scanf("%lld%lld%lld",&q,&k,&s);
		while(q--)
		{
			scanf("%lld%lld",&v0,&p0);
			v=(v0+k*Ans-1)%n+1;
			p=(p0+k*Ans)%(s+1);
			Ans=find(v,p);
			printf("%lld\n",Ans);
		}
	}
	return 0;
}

4 . CF1120D Power Tree

luogu传送门

CodeForces传送门

前置芝士:\(kruskal\) 最小生成树

题意

给定一个 \(n\) 个节点的树,每个节点有一个花费 \(w_i\) ,对于 \(i\) 号点,你可以花费 \(w_i\) 的代价,对点 \(i\) 进行操作,将 \(i\) 子树内所有叶子节点的点权加一个任意数。请找到一个最小的花费方案,使得无论叶子节点的权值是什么,都可以通过操作相同的点使得叶子节点点权全部归零。

再重申一遍作者语文满分120 100都够呛。

题解

又是一神题。要将问题灵活转换。

乍一看是看不出这是最小生成树,慢慢来。

看到是将子树内全部叶子节点加一个数,考虑将问题变成区间修改的问题。首先要将叶子节点设连续的新编号。这样的话对一个点进行操作等价于在一个叶子节点的序列中区间修改。

为实现编号连续,显然可以用 \(dfs\) 序实现。根据 \(dfs\) 的顺序给叶子设编号,并求出操作每个点所能操作的叶子节点区间。

继续看题。注意到将叶子节点点权全部归零。

不妨对叶子节点的序列中使用差分。设叶子节点有 \(n\) 个,则将 \(1\)\(n\) 全部清零等价于将 \(c_0,c_1...c_n\) 全部集中到 \(c_{n+1}\)

与上面的区间修改联系起来,设某一个点可以修改叶子序列中的 \(l\)\(r\),那么不妨将一次修改想成从 \(l\)\(r+1\) 连边,边权为 \(w_i\) ,代表 \(c_l\) 珂以通过一次操作使得 \(c_l=0\)\(c_r\) 加上 \(c_l\) 。我们要将所有点连向 \(c_{n+1}\)

这时候就很明显是最小生成树了,跑一遍 \(Kruskal\) 即可。

注意:题目要求的不是一种最优方案,而是所有包含在某个最优方案内的操作点。所以要魔改一下 \(kruskal\) ,记录一下答案。

code

你一定会问代码怎么和dalao liangbowen 这么像 . . .

//writer:Oier_szc

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,ans1=0;
int yes[N],ans2=0;
int w[N];
int head[N],ne[N<<1],to[N<<1],tot=0;
void add(int u,int v)
{
	to[++tot]=v;
	ne[tot]=head[u];
	head[u]=tot;
}
int dfnl[N],dfnr[N],timec=0;
void dfs(int u,int fa)
{
	bool is_leaf=true;
	dfnl[u]=0x3f3f3f3f;
	dfnr[u]=0;
	for(int i=head[u];i;i=ne[i])
	{
		if(to[i]==fa) continue;
		is_leaf=false;
		dfs(to[i],u);
		dfnl[u]=min(dfnl[u],dfnl[to[i]]);
		dfnr[u]=max(dfnr[u],dfnr[to[i]]);
	}
	if(is_leaf) dfnl[u]=dfnr[u]=++timec;
}
struct Edge2
{
	int u,v,w,id;
	bool operator<(const Edge2 &W) const
	{
		return w<W.w;
	}
}E[N];
int fa[N];
int find(int u)
{
	if(fa[u]==u) return fa[u];
	else return fa[u]=find(fa[u]);
}
void kruskal()
{
	for(int i=1;i<=n;++i)
	{
		E[i]=Edge2{dfnl[i],dfnr[i]+1,w[i],i};
	}
	sort(E+1,E+1+n);
	for(int i=1;i<=timec+1;++i) fa[i]=i;
	int fu,fv;
	for(int i=1,j=1;i<=n;)
	{
		while(j+1<=n&&E[i].w==E[j+1].w) ++j;
		for(int k=i;k<=j;++k)
		{
			fu=find(E[k].u);
			fv=find(E[k].v);
			if(fu!=fv) yes[E[k].id]=true,++ans2;
		}
		for(int k=i;k<=j;++k)
		{
			fu=find(E[k].u);
			fv=find(E[k].v);
			if(fu==fv) continue;
			fa[fu]=fv;
			ans1+=E[k].w;
		}
		i=++j;
	}
}
signed main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%lld",&w[i]);
	}
	int u,v;
	for(int i=1;i<=n-1;++i)
	{
		scanf("%lld%lld",&u,&v);
		add(u,v);
		add(v,u);
	}
	dfs(1,-1);
	kruskal();
	printf("%lld %lld\n",ans1,ans2);
	for(int i=1;i<=n;++i)
	{
		if(yes[i]) printf("%lld ",i);
	}
	return 0;
}
posted @ 2023-07-18 13:31  Oier_szc  阅读(538)  评论(0)    收藏  举报