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 森林上的边在最后的答案中一定不会出现。
如果无解,当且仅当满足以下条件之一:
- 连通块个数 \(c>k\);
- 存在一个连通块与 \(s\) 没有连边;
- \(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;
}

浙公网安备 33010602011771号