标签:复杂度 printf puts 删除 朱刘算法 lin 生成 == 左偏树
定义对于带权有向图\(G=(V,E)\)对于根\(root\)最小树形图为以\(root\)为根的外向树最小边权和
题目给定了\(root\)
核心:
推论1:对于有向图上的一个点,对于它的所有入边加减一个权值,答案的树形图形态不变
因为所有非根点必然有一条入边,所以可以对于每个点,取入边边权最小值减去,把减去的部分加入答案
经过这样的操作使得每个点都有一条为0的入边
推论2:对于有向图上边权为0的有向边生成的环,可以把环上的点收缩
因为无论最后得到的树形图如何连边,一定可以通过断掉环上的一条边来生成一个可行的树形图
算法流程
1.为每个点的入边更改边权
2.收缩0环
? 2.1 存在环 : 回到1
? 2.2 不存在环:结束算法
此时存在两种情况
1.图不连通,无解
2.图联通,每个点一定存在一条为0的入边,取出一个合法边集,
然后依次展开每个被收缩的0环,即可得到一个最小树形图方案
复杂度分析:
每次收缩环需要依次遍历,每次至少缩小一个点,因此复杂度上限为\(O(nm)\)
Tips:
1.注意不要更改根的入边
2.0边构成的的图不连通
朴素的实现:tarjan强连通缩点
判断无解可以在每次调整边权时看是否有非根节点不存在入边,此时无解
const int N=10010,INF=1e9;
int n,m,rt,ans;
int U[N],V[N],W[N];
vector <int> G[N],E[N];
int id[N],T[N];
void Reweight(){
rep(i,1,n) if(i!=rt && id[i]==i) {
int w=INF;
for(int j:E[i]) cmin(w,W[j]);
if(w==INF) puts("-1"),exit(0);
for(int j:E[i]) W[j]-=w;
ans+=w;
}
}
int t[N],low[N],ins[N],stk[N],top,dfn,flag;
void Union(int u){
low[u]=t[u]=++dfn;
ins[stk[++top]=u]=1;
for(int i:G[u]) if(W[i]==0) {
int v=V[i];
if(!t[v]) Union(v),cmin(low[u],low[v]);
else if(ins[v]) cmin(low[u],t[v]);
}
if(low[u]==t[u]) {
int v;
do {
ins[v=stk[top--]]=0;
id[v]=u;
if(u!=v) flag=1;
} while(u!=v);
}
}
void ReBuild(){
rep(i,1,n) G[i].clear(),E[i].clear();
rt=id[rt];
rep(i,1,m) {
U[i]=id[U[i]],V[i]=id[V[i]];
if(U[i]==V[i]) continue;
G[U[i]].pb(i),E[V[i]].pb(i);
}
}
int main(){
n=rd(),m=rd(),rt=rd();
rep(i,1,n) id[i]=i;
rep(i,1,m) {
U[i]=rd(),V[i]=rd(),W[i]=rd();
G[U[i]].pb(i),E[V[i]].pb(i);
}
while(1) {
Reweight();
rep(i,1,n) t[i]=0;
dfn=flag=0;
rep(i,1,n) if(!t[i] && id[i]==i) Union(i);
if(!flag) break;
ReBuild();
}
Reweight();
printf("%d\n",ans);
}
更好的实现:只记录一条0边,每次循环收缩环(居然细节挺多的。。)
const int N=10010,INF=1e9;
int n,m,rt,ans;
int U[N],V[N],W[N];
int id[N],inw[N],pre[N];
void Reweight(){
rep(i,1,n) inw[i]=INF,pre[i]=0;
rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) if(inw[V[i]]>W[i]) inw[V[i]]=W[i],pre[V[i]]=U[i];
rep(i,1,n) if(i!=rt && id[i]==i) {
if(inw[i]==INF) puts("-1"),exit(0);
ans+=inw[i];
}
rep(i,1,m) if(U[i]!=V[i] && V[i]!=rt) W[i]-=inw[V[i]];
}
int vis[N];
int Union(){
int fl=0;
rep(i,1,n) vis[i]=0;
rep(i,1,n) if(id[i]==i && !vis[i]) {
int u=i;
while(u && !vis[u]) vis[u]=i,u=pre[u];
if(vis[u]==i) {
int v=pre[u];
fl=1;
while(v!=u) id[v]=u,v=pre[v];
}
}
return fl;
}
int main(){
n=rd(),m=rd(),rt=rd();
rep(i,1,n) id[i]=i;
rep(i,1,m) U[i]=rd(),V[i]=rd(),W[i]=rd();
while(1) {
Reweight();
if(!Union()) break;
rep(i,1,m) U[i]=id[U[i]],V[i]=id[V[i]];
rt=id[rt];
}
printf("%d\n",ans);
}
笔者没有写过这个东西的代码
涉及到的操作:
1.用可并堆维护合并点集入边的最小权值,并且支持全局减操作,单点删除操作(删除块内边)
2.用并查集维护判断是否出现了环,由于不能直接合并,需要用按秩合并来实现
比较常见的实现是左偏树,因为便于全局修改的标记下传操作,代码也比较好写
用可并堆维护朱刘算法的操作,单次合并操作为\(O(\log m)\),因此复杂度为\(O(n(\log m+\log n)+n\log n)\)
建立超级原点\(S\)向\(V\)中的点连边权极大的边,以限制每次只选一条这样的边
单次得到答案后减去这个极大值即可,注意如果答案有两个这样的极大值,是无解的
标签:复杂度 printf puts 删除 朱刘算法 lin 生成 == 左偏树
原文地址:https://www.cnblogs.com/chasedeath/p/13429457.html