MST 专题

MST 专题

Problem A. CF1305G Kuroni and Antihype

Description

有一个初始为空的集合 S。可以进行 n 次操作:

  • 加入一个不在 S 中的元素 Ai。此操作没有收益。
  • 在集合 S 中找到一个 Aj 使得 AiandAj=0,获得 Aj 的收益,再把 Ai 加入到 S 中。

你需要将 A1,A2,A3,...An 全部加入到集合 S 中,问最后的最大收益。

Solution

考虑图论建模。

建立一个超级源点 0,令 A0=0,让 S 初始只含有 0 这个元素。

那么我们只需要考虑第二种结构。可以发现,我们将元素加入集合的顺序形成了一棵以 0 为根的外向树。

但我们现在树上的边权与边的方向有关。

我们考虑这样建图:所有 1i,jn,ij(i,j) 之间连一条边权为 Ai+Aj 的无向边。

这样我们最终生成的树中每个点的权值都在父亲处多算了一次。于是答案就是图的最大生成树权值减去 Ai

接下来考虑如何快速地求最大生成树。

利用 kruskal 的思想,我们从大到小枚举边权。对于一个边权为 w 的边,两端的点权值一定在二进制下把 w 划分为了两部分,也就是 AuorAv=wAuandAv=0

我们先把所有点权相等的点缩为一个,记录每个权值出现的次数。

假设我们枚举到了 w,那么我们枚举 w 在二进制表示下的子集 u,并计算出另一端的 v

如果两者不在一个连通块内,那么我们合并两个点,对答案造成 (cntFind(u)+cntFind(v)1)w 的贡献。

我们还需要合并 Find(u)Find(v),并让新连通块的 “根” 的 cnt1

时间复杂度为 O(3Bα(n)),其中 B=18。需要卡常。

int n,m,a[N];
ll ans,cnt[N],fa[N],siz[N];

int Find(int x){
    if(fa[x]==x) return x;
    return fa[x]=Find(fa[x]);
}

void Merge(int u,int v,ll w){
    u=Find(u),v=Find(v);
    if(u==v) return;
    ans+=w*(cnt[u]+cnt[v]-1);
    if(siz[u]>siz[v]) swap(u,v);
    fa[u]=v; siz[v]+=siz[u]; cnt[v]=1;
}

signed main(){
    //效率!效率!
    read(n); cnt[0]=1;
    for(int i=1;i<=n;i++){
        read(a[i]);
        Ckmax(m,a[i]);
        ans-=a[i];
        cnt[a[i]]++;
    }
    int AS=1;
    while(AS<=m) AS<<=1;
    AS--;
    for(int i=0;i<=AS;i++) fa[i]=i,siz[i]=1;
    for(int s=AS;s>=0;s--){
        for(int t=s;;t=(t-1)&s){
            if(t<(s^t)) break;
            if(cnt[t]&&cnt[s^t])
                Merge(s^t,t,s);
            if(!t) break;
        }
    }
    printf("%lld\n",ans);
    return 0;
}

Problem B. [JSOI2008] 最小生成树计数

Description

现在给出了一个简单无向加权图。求出这个图中有多少个不同的最小生成树。(如果两棵最小生成树中至少有一条边不同,则这两个最小生成树就是不同的)。输出方案数对 31011 的模。

数据保证不会出现自回边和重边。注意:具有相同权值的边不会超过 10 条。

对于全部数据,1n1001m10001ci109

Solution

引理:在 kruskal 的过程中,对于权值相同的边,无论以何种顺序处理,最终形成的连通块都是一样的。

正确性显然。如果两种处理顺序导致形成的连通块不同,那么这意味着我们可以在这两种方式中再多连几条权值相同的边,最终答案会变的更优。

利用 kruskal 的思想,从小到大枚举边权。

根据上面的引理,我们每次把边权相同的边拿出来一块处理。

由于题目保证了具有相同权值的边不会超过 10 条,那么我们可以二进制枚举这些边,计算出这些边有多少种方案可以让加入 MST 的边最多。

利用乘法原理,我们把每一次的答案乘起来,就是最终的答案。

时间复杂度 O(2k×m×α(n)),其中 k10,可以通过。

int n,m;

struct Edge{
    int u,v,w;
    bool operator < (const Edge& tmp) const{
        return w<tmp.w;
    }
};
vector<Edge> edge;
vector<Edge> e[M];

int fa[N],tot,siz[N];
int res,cnt;
ll ans;

const ll mod=31011;

inline ll Mod(ll x){return (x>=mod)?(x-mod):(x);}

inline void Add(ll &x,ll y){x=Mod(x+y);}

int Find(int x){
    if(fa[x]==x) return x;
    return Find(fa[x]);
}

void dfs(int x,int c,vector<Edge> &s){
    if(x==(signed)s.size()){
        if(c>res) res=c,cnt=1;
        else if(c==res) cnt++;
        return;
    }
    int u=s[x].u,v=s[x].v;
    int fu=Find(u),fv=Find(v);
    if(fu!=fv){
        int w=0;
        if(siz[fu]>siz[fv]) swap(fu,fv);
        fa[fu]=fv,siz[fv]+=siz[fu],w=siz[fu];
        dfs(x+1,c+1,s);
        fa[fu]=fu,siz[fv]-=w;
    }
    dfs(x+1,c,s);
}

signed main(){
    //效率!效率!
    read(n),read(m);
    for(int i=1;i<=m;i++){
        int u,v,w;
        read(u),read(v),read(w);
        edge.push_back({u,v,w});
    }
    sort(edge.begin(),edge.end());
    e[0].push_back({0,0,0});
    for(Edge i:edge){
        if(i.w==e[tot].back().w) e[tot].push_back(i);
        else e[++tot].push_back(i);
    }
    for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;
    ans=1;
    for(int i=1;i<=tot;i++){
        res=0,cnt=0;
        dfs(0,0,e[i]);
        (ans*=cnt)%=mod;
        for(Edge j:e[i]){
            int u=j.u,v=j.v;
            u=Find(u),v=Find(v);
            if(u!=v){
                if(siz[u]>siz[v]) swap(u,v);
                fa[u]=v,siz[v]+=siz[u];
            }
        }
    }
    for(int i=2;i<=n;i++){
        if(Find(i)!=Find(1))
            return puts("0"),0;
    }
    printf("%lld\n",ans);
    return 0;
}

Problem C. P5633 最小度限制生成树 (值得思考)

Description

给你一个有 n 个节点,m 条边的带权无向图,你需要求得一个生成树,使边权总和最小,且满足编号为 s 的节点正好连了 k 条边。

1sn5×1041m5×1051k1000w3×104

Solution

首先我们把 s 点删去,剩下的图会形成若干连通块。

我们对这些连通块求出一个 MST 森林。不在 MST 森林上的边在最后的答案中一定不会出现。

如果无解,当且仅当满足以下条件之一:

  1. 连通块个数 c>k
  2. 存在一个连通块与 s 没有连边;
  3. s 的度数 <k

我们只保留森林上的边。对于每一个连通块,我们找出其与 s 相连的最小边,其在最后的答案中一定会出现,所以我们先连上它们。

如果 c=k,那么结束;否则我们还需要向 s 多连 kc 条边。

我们找出一个还没有向 s 连边的点 x,如果我们要连边 (x,s),那么需要找到 x 所在连通块中已经向 s 连了边的 y(它一定存在),将 (xy) 这条路径上最大的边断掉。原来的一个连通块分裂为两个,且每个连通块仍然有且仅有一个点向 s 连了边,且可以发现,此时一定有 w(x,s)w(y,s)

我们从大到小枚举要断掉的边,找出断掉这条边后分出的两个连通块 X,Y,分别找出 X,Y 中与 s 直接相连且边权最小的 p,q。若 w(s,p)>w(s,q),我们连接 (s,p),否则连接 (s,q),然后分裂连通块。

但分裂操作不好维护。我们把这个过程倒过来,发现和 kruskal 的过程非常相似。

我们在 kruskal 中对每个点维护 valx,表示若要连接 (s,x),那么能够断掉的最大边权是多少。

我们把 w(s,u) 最小的 u 作为并查集的根节点,设 disu=w(s,u)。每次合并两个根 u,v 时,我们把 dis 较大的合并到较小的根上,并给 dis 较大的那个根的 val 赋值为 w(u,v)

kruskal 算法结束后,我们找出 val 最小的 kc 个点,向 s 连边,答案加上 disxvalx

int n,m,k,s;

struct Edge{
	int u,v,w;
	bool operator<(const Edge& tmp)const{
		return w<tmp.w;
	}
};
vector<Edge> e;

int fa[N],dis[N],val[N],p[N];

int Find(int x){
	if(fa[x]==x) return x;
	return fa[x]=Find(fa[x]);
}

void Merge(int u,int v,int w){
	if(dis[u]<dis[v]) swap(u,v);
	fa[u]=v; val[u]=dis[u]-w;
}

bool Cmp1(int x,int y){
	return dis[x]<dis[y];
}

bool Cmp2(int x,int y){
	return val[x]<val[y];
}

signed main(){
	read(n),read(m),read(s),read(k);
	memset(dis,0x3f,sizeof(dis));
	memset(val,0x3f,sizeof(val));
	for(int i=1;i<=m;i++){
		int u,v,w;
		read(u),read(v),read(w);
		if(u==s) Ckmin(dis[v],w);
		else if(v==s) Ckmin(dis[u],w);
		else e.push_back({u,v,w});
	}
	sort(e.begin(),e.end());
	for(int i=1;i<=n;i++) fa[i]=i;
	ll ans=0;
	for(Edge i:e){
		int u=i.u,v=i.v,w=i.w;
		u=Find(u),v=Find(v);
		if(u==v) continue;
		Merge(u,v,w); ans+=w;
	}
	int cnt=0;
	for(int i=1;i<=n;i++) p[i]=i;
	sort(p+1,p+n+1,Cmp1);
	for(int i=1;i<=n;i++){
		int x=p[i];
		if(dis[x]>1e9) break;
		if(Find(x)!=x) continue;
		ans+=dis[x]; val[x]=IINF; cnt++;
	}
	if(cnt>k) return puts("Impossible"),0;
	sort(p+1,p+n+1,Cmp2);
	for(int i=1;i<=n;i++){
		if(cnt==k) break;
		int x=p[i];
		if(val[x]>1e9)
			return puts("Impossible"),0;
		ans+=val[x],cnt++;
	}
	printf("%lld\n",ans);
    return 0;
}

posted @ 2025-03-04 15:02  XP3301_Pipi  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
Title
点击右上角即可分享
微信分享提示