最小生成树思想

我们的标题叫做思想,这也就意味着这些题目并不是单纯的要求最小生成树。
我们知道,求最小生成树的方法有几个来?primkruskal 两种对吧。
我们来回顾一下 primkruskal 两种算法的本质。
我们知道,两种算法都是贪心,但是对于 kruskal 而言,他的本质更趋于集合合并,所以针对这个点出题人经常喜欢考 dsu on tree 等与集合相关的题。
我们来看几个典例。

CF888G

作为场上最后一道题,他的通过数却比 CF888F 多,可见并不是所有的 G 都不可做。
我们先来想一想怎么做,看到 \(a_i\oplus a_j\) 一共就想这么几个常见的,第一线性基,第二 01 Trie,第三二进制拆分。
首先这里用线性基实在是不合理,因为他不是求最大的异或和,显然我们这道题二进制拆分也不是很好做(或许有二进制拆分后暴力建边的?不管了)。
所以我们想到建立字典树。
我们把所有的 \(a_i\) 装入一个字典树,然后我们考虑怎么求最小生成树。
我们还是朴素的回溯到最小生成树的思路上,即选取最小的边权,判断是否合法,然后加入。
那么在字典树上怎么找到最小的边权呢?
我们发现,每一次最小的边权就是能合并的两个数在字典树上 LCA 最大的。所以我们考虑遍历字典树,从下往上合并子树,显然合并之前子树之间不连通,所以也不需要并查集维护了。我们采用树上启发式合并。
至于找到两个子树之间的最小边权,我们遍历集合大小较小的权值,然后我们去 \(\mathcal O(\log V)\) 的求最小值即可,不会的看这里
总复杂度为 \(\mathcal O(n\log n\log V)\),可以通过本题。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5;
int T[N*20][2],cnt,ans,a[N*20],dep[N*20];
vector<int> G[N*20];
void ins(int x)
{
	int cur=0;
	for(int i=30;~i;i--)
	{
		int c=(x>>i)&1;
		if(!T[cur][c])
			T[cur][c]=++cnt,a[cnt]=c,dep[cnt]=i;
		cur=T[cur][c];
	}
	G[cur].push_back(x);
}
int DFS(int y,int v)
{
	int cur=y,res=1<<dep[y];
	for(int i=dep[y]-1;~i;i--)
	{
		int c=(v>>i)&1;
		if(T[cur][c]) cur=T[cur][c];
		else
		{
			res|=(1<<i);
			cur=T[cur][c^1];
		}
	} 
	return res;
}
void dfs(int x)
{
	bool flag=false;
	if(T[x][0]) dfs(T[x][0]),flag=true;
	if(T[x][1]) dfs(T[x][1]),flag=true;
	if(!flag) return;
	if(!T[x][0])
	{
		swap(G[x],G[T[x][1]]);
		return;
	}
	if(!T[x][1])
	{
		swap(G[x],G[T[x][0]]);
		return;
	}
	if(x==27)
	{
		new char;
	}
	int s1=T[x][0],s2=T[x][1];
	if(G[s1].size()>G[s2].size()) swap(s1,s2);
	int mn=1e9;
	for(int u:G[s1])
	{
		G[s2].push_back(u);
		mn=min(mn,DFS(s2,u));
	}
	swap(G[x],G[s2]);
	ans+=mn;
}
signed main()
{
	int n;cin>>n;
	for(int i=1,x;i<=n;i++)
		cin>>x,ins(x);
	dfs(0);
	cout<<ans;
	return 0;
}

P3639 [APIO2013] 道路费用

这道题大家只需看懂就行,别去写,小心写到吐/tuu。
首先想个暴力的做法,我们看到 \(k\) 这么小就可以直接状压了。
我们枚举这 \(k\) 条边的选或不选的情况,然后我们再去做最小生成树即可,复杂度为 \(\mathcal O(2^km\log m)\),铁定 TLE
怎么办呢?我们在枚举的时候,应该注意 \(0\)\(2^k-1\) 时,我们所选的其他的边的情况。我们容易发现,当我们全选 \(k\) 条边时,假设其他的边集为 \(R\),那么当我们的状态 \(S\in [0,2^k-1]\) 时,\(R\) 总是出现在最小生成树里。
证明感性理解就行了,不需要会证……
那么这样我们的复杂度就大大降低了,我们对于 \(R\) 进行缩点,那么最后就有 \(k+1\) 个连通块,那么我们这个时候再去 \(2^k\) 枚举时,我们的边数就变成了 \(2k\) 条边,所以复杂度即为 \(\mathcal O(2^kk\log k)\),通过本题。
听着简单,实际非常难写,首先你要跑生成树,然后你要缩点,然后还要跑状压,状压的时候想要计算权值还需要 LCA…………
代码千万行,安全第一行。不是大肝帝,不要写此题!

#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define f first
#define s second
using namespace std;
const int N=1e5+5,M=3e5+5,K=55; 
struct node
{
	int u,v,w;
	friend bool operator<(const node &x,const node &y)
	{
		return x.w<y.w;
	}
}E1[M],E2[M];
pii e[K];
int n,m,k,a[N],scc[N],cnt,val[K],rt,tot,vis[K<<1],fa[K],dep[K],V[K],mx[K],sum,ans,ne[K],to[K],h[K],num;
void add(int u,int v)
{
	ne[num]=h[u];
	to[num]=v;
	h[u]=num++;
}
struct DSU
{
    int f[N];
    int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
}D1,D2,D3;
void dfs(int x,int F)
{
	fa[x]=F,dep[x]=dep[F]+1,V[x]=val[x];
	for(int i=h[x];~i;i=ne[i])
	{
		int v=to[i];
		if(v==F) continue;
		dfs(v,x),V[x]+=V[v];
	}
}
void ST(int u,int v,int w)
{
    if(dep[u]<dep[v]) swap(u,v);
    while(dep[u]>dep[v])
    {
        mx[u]=min(mx[u],w);
        u=fa[u];
    }
    while(u!=v)
    {
        mx[u]=min(mx[u],w);
        mx[v]=min(mx[v],w);
        u=fa[u],v=fa[v];
    }
}
signed main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++)
		cin>>E1[i].u>>E1[i].v>>E1[i].w;
	for(int i=1;i<=k;i++)
		cin>>e[i].f>>e[i].s;
	sort(E1+1,E1+1+m);
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=1;i<=n;i++)
		D1.f[i]=D2.f[i]=i; 
	for(int i=1;i<=k;i++)
	{
		int u=e[i].f,v=e[i].s;
		int fu=D1.find(u),fv=D1.find(v);
		if(fu!=fv) D1.f[fu]=D1.f[fv];
	} 
	for(int i=1;i<=m;i++)
	{
		int u=E1[i].u,v=E1[i].v;
		int fu=D1.find(u),fv=D1.find(v);
		if(fu==fv) continue;
		D1.f[fu]=D1.f[fv];
		fu=D2.find(u),fv=D2.find(v);
		if(fu!=fv) D2.f[fu]=D2.f[fv];
	}
	for(int i=1;i<=n;i++)
		if(D2.find(i)==i)
		{
			scc[i]=++cnt;
			val[cnt]=a[i];
		}
	for(int i=1;i<=n;i++)
		if(D2.find(i)!=i)
		{
			scc[i]=scc[D2.find(i)];
			val[scc[i]]+=a[i];
		}
	rt=scc[1];
	for(int i=1;i<=m;i++)
	{
		int u=E1[i].u,v=E1[i].v;
		int fu=D2.find(u),fv=D2.find(v);
		if(fu==fv) continue;
		E2[++tot]={scc[u],scc[v],E1[i].w};
		D2.f[fu]=D2.f[fv];
	}
	for(int S=0;S<(1<<k);S++)
	{
		num=0;
		for(int i=1;i<=cnt;i++) D3.f[i]=i;
		memset(h,-1,sizeof(h));
		memset(vis,0,sizeof(vis));
		memset(mx,0x3f,sizeof(mx));
		memset(fa,0,sizeof(fa));
		memset(dep,0,sizeof(dep));
		memset(V,0,sizeof(V));
		for(int i=1;i<=k;i++)
		{
			if((S>>(i-1))&1^1) continue;
			int u=scc[e[i].f],v=scc[e[i].s];
			int fu=D3.find(u),fv=D3.find(v);
			if(fu==fv) continue;
			D3.f[fu]=D3.f[fv],add(u,v),add(v,u);
		}
		for(int i=1;i<=tot;i++)
		{
			int u=E2[i].u,v=E2[i].v;
			int fu=D3.find(u),fv=D3.find(v);
			if(fu==fv) continue;
			D3.f[fu]=D3.f[fv];
			vis[i]=1,add(u,v),add(v,u);
		}
		dfs(rt,0);
		for(int i=1;i<=tot;i++)
		{
			if(vis[i]) continue;
			ST(E2[i].u,E2[i].v,E2[i].w);
		}
		sum=0;
		for(int i=1;i<=k;i++)
		{
			if((S>>(i-1))&1^1) continue;
			int u=scc[e[i].f],v=scc[e[i].s];
			if(dep[u]<dep[v]) swap(u,v);
			sum+=mx[u]*V[u];
		}
		ans=max(ans,sum);
	}
	printf("%lld",ans);
	return 0;
}

P4784 [BalticOI 2016] 城市 (Day2)

这道题其实就是模板最小斯坦纳树。这道题还是很好写的,而且是双倍经验。
对于最小斯坦纳树来说,我们不跑 kruskal,而是进行 dp
我们设 \(f_{i,S}\) 表示枚举到第 \(i\) 个城市,然后我们所包含的关键城市的状态为 \(S\),所需要的最小总成本。
首先是初始化,这很简单。\(f_{a_i,2^{i-1}}=0\)
其次考虑转移。

  1. 如果是不同状态的转移,我们发现 \(f_{i,S}=\min_{T\in S}(f_{i,S},f_{i,T}+f_{i,S-T})\)。这个复杂度是 \(\mathcal O(n3^k)\)
  2. 如果是同一状态的转移,我们发现 \(f_{i,S}=\min_{(i,j)\in E}(f_{i,S},f_{j,S}+w)\),其中 \(w\)\((i,j)\) 的边权,对于这种情况我们可以用 DIJ 松弛一遍,复杂度为 \(\mathcal O(2^km\log m)\)
    综上,总时间复杂度为 \(\mathcal O(3^kn+2^km\log m)\),可以通过。这就是模板斯坦纳树。
#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
using ll=long long;
using pil=pair<int,ll>;
using pli=pair<ll,int>;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int N=2e5+5;
int n,m,k,vis[N];
ll f[N][40];
vector<pil> G[N];
priority_queue<pli> q;
void dij(int S)
{
	memset(vis,0,sizeof(vis));
	while(!q.empty())
	{
		pli tmp=q.top();q.pop();
		int x=tmp.se;
		if(vis[x]) continue;
		vis[x]=1;
		for(pil tmp:G[x])
		{
			int v=tmp.fi;ll w=tmp.se;
			if(f[v][S]>f[x][S]+w)
			{
				f[v][S]=f[x][S]+w;
				q.push({-f[v][S],v});
			}
		}
	}
}
int main()
{
	cin>>n>>k>>m;
	memset(f,0x3f,sizeof(f));
	for(int i=1,x;i<=k;i++)
		cin>>x,f[x][1<<(i-1)]=0;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;cin>>u>>v>>w;
		G[u].emplace_back(v,w);
		G[v].emplace_back(u,w);
	}
	for(int S=0;S<(1<<k);S++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int T=S&(S-1);T;T=S&(T-1))
				f[i][S]=min(f[i][S],f[i][T]+f[i][S^T]);
			if(f[i][S]<INF) q.push({-f[i][S],i});
		}
		dij(S);
	}
	ll mn=1e18;
	for(int i=1;i<=n;i++)
		mn=min(mn,f[i][(1<<k)-1]);
	cout<<mn;
	return 0;
}
posted @ 2025-04-05 18:09  I_AK_CTSC  阅读(23)  评论(0)    收藏  举报