MST 专题

MST 专题

Problem A. CF1305G Kuroni and Antihype

Description

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

  • 加入一个不在 \(S\) 中的元素 \(A_i\)。此操作没有收益。
  • 在集合 \(S\) 中找到一个 \(A_j\) 使得 \(A_i \operatorname{and} A_j=0\),获得 \(A_j\) 的收益,再把 \(A_i\) 加入到 \(S\) 中。

你需要将 \(A_1,A_2,A_3,...A_n\) 全部加入到集合 \(S\) 中,问最后的最大收益。

Solution

考虑图论建模。

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

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

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

我们考虑这样建图:所有 \(1\leq i,j\leq n,i\ne j\)\((i,j)\) 之间连一条边权为 \(A_i+A_j\) 的无向边。

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

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

利用 kruskal 的思想,我们从大到小枚举边权。对于一个边权为 \(w\) 的边,两端的点权值一定在二进制下把 \(w\) 划分为了两部分,也就是 \(A_u \operatorname{or} A_v=w\)\(A_u \operatorname{and} A_v=0\)

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

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

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

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

时间复杂度为 \(O(3^B \alpha(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\) 条。

对于全部数据,\(1 \le n \le 100\)\(1 \le m \le 1000\)\(1\leq c_i\leq 10^9\)

Solution

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

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

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

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

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

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

时间复杂度 \(O(2^k\times m \times \alpha(n))\),其中 \(k\leq 10\),可以通过。

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\) 条边。

\(1\leq s \leq n \le 5\times 10^4\),$1\leq m \le 5\times 10^5 $,\(1\leq k \le 100\)\(0\leq w\leq 3\times 10^4\)

Solution

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

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

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

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

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

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

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

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

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

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

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

kruskal 算法结束后,我们找出 \(val\) 最小的 \(k-c\) 个点,向 \(s\) 连边,答案加上 \(dis_x-val_x\)

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  阅读(26)  评论(0)    收藏  举报
Title