生成树

Nemlit

2018-08-30 21:40:22

Solution

管理员备注:本文中第二份代码在 hack 数据中会 WA,请读者注意 [原文地址](https://www.cnblogs.com/bcoier/p/10293072.html) ### 生成树的概念: 在一个无向图中,设顶点数为$n$,取其中$n-1$条边并使所有点相连,所得到的一棵树即为生成树。 ### 最小生成树: 如果还没有接触过生成树的同学,欢迎戳->[最小生成树详解](https://tbr-blog.blog.luogu.org/solution-p3366) # 次小生成树: 次小生成树顾名思义,就是边权之和次小的一棵生成树。有严格次小生成树与非严格次小生成树之分。 ~~尽管这个算法的名字叫做次小生成树,不过其实可以理解成一道数据结构~~ 次小生成树可以和次短路径一起理解,有兴趣的同学可以做做模板题 非严格次小生成树的边权之和不一定要比最小生成树的小,即 $Σw$次≥$Σw$最 严格次小生成树的边权之和一定要比最小生成树的小,即$Σw$次>$Σw$最 想要求出次小生成树? 显然我们可以dfs求出所有的生成树,在排序选出严格次小的就可以了。 但是上述方法显然不能快速求出次小生成树 为了快速的求出来次小生成树,我们需要知道一个结论:次小生成树和最小生成树之间只有一条边的差异。 这一条结论为什么是正确的呢? 我们先来看看$Kruskal$是怎么求最小生成树的 $Kruskal$是按照边权排序,按照贪心的思路一条一条的加进树边,所以如果要求出次小生成树,那么我们可以放弃一条小的边不选,加入另一条边使图联通。 如果我们"放弃"了两条边,那么只"放弃"一条边的生成树的边权和显然小于"放弃"两条边的生成树的边权和。 所以求出(非)严格次小生成树的朴素算法为:先建立一棵最小生成树。再枚举删去最小生成树上的每一条路径,再在新构成的生成树中选出一棵边权之和最小生成树的即可。 时间复杂度为$O(nmlogm)$。当然这种算法还不够优秀,我们可以继续优化 非严格次小生成树我们可以这样优化: 枚举边的时候,枚举没有被并入到最小生成树的边(我们将把这条边加入到生成树中,显然现在的图形不再是一棵树,所以我们要原图中删去一条最大边,使新图仍然联通) 加入边权值为$W1$。查询树上每一条的路径,在路径选取一个边权最大值$W2$。则当前枚举的答案为$W−W2+W1$(W为最小生成树的边权之和) 枚举所有的边之后,取最小值即可。 我们可以用倍增/树剖/LCT来实现查询树上最大值的操作,故复杂度为:$O(mlogn)$ 再来看看严格次小生成树 不难发现:求非严格最小生成树时,枚举一条边$W1$,之后再寻找一条生成树上的最大边$W2$ 显然$W1≥W2$,因此可能由此得到的次小生成树并非严格。所以我们可以在每次查询时,需要找到严格次小的$W1$,即$W1>W2$,这样我们就可以得到严格次小生成树了。 维护仍可以用倍增或者树剖思想。 这里介绍树剖的思路: 先用线段树维护区间最小值与严格次小值,如果是叶子节点最大值就设为他本身,次大值设为0 合并的时候把两个区间的这四个值(两个最大值与两个次大值)排序,再寻找最大值与严格次小值即可。 ### [严格次小生成树模板题](https://www.luogu.org/problemnew/show/P4180) ### 代码如下: ``` #include<bits/stdc++.h> using namespace std; #define re register #define il inline #define int long long //把所有int转成longlong #define debug printf("Now is line %d\n",__LINE__); il int read() { re int x=0,f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();} while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*f; }//快读 #define maxm 300005 #define inf 12345678900000000 #define maxn 100005 struct Edge{int u,v,w,next;}e[maxm<<1]; struct qj{int ma,ma2;}q[maxn<<2]; struct Edge1 { int u,v,w; bool operator <(const Edge1 &x) const{return w<x.w;}//按照边权排序 }edge[maxm]; int n,m,vis[maxm],ans=inf,head[maxn],cnt,fa[maxn],mtree; il void add(int u,int v,int w) { e[++cnt].v=v; e[cnt].w=w; e[cnt].next=head[u]; head[u]=cnt; }//前向星加边 namespace smallesttree { il int find(int x) { while(x!=fa[x]) x=fa[x]=fa[fa[x]]; return x; }//并查集找祖先 il void init() { for(re int i=1;i<=n;i++) fa[i]=i; //预处理并查集 for(re int i=0;i<m;i++) edge[i].u=read(),edge[i].v=read(),edge[i].w=read(); } il void kruskal() { init(); sort(edge,edge+m); re int T=0; for(re int i=0;i<m;++i) { re int eu=find(edge[i].u),ev=find(edge[i].v);//寻找祖先 if(eu!=ev) { add(edge[i].u,edge[i].v,edge[i].w),add(edge[i].v,edge[i].u,edge[i].w); mtree+=edge[i].w;//记录子树大小 fa[ev]=eu;//合并 vis[i]=1;//标记该边为树边 if(++T==n-1) break;//边数等于节点数+1即为一颗树 } } } } //求出最小生成树 namespace treecut { int dep[maxn],father[maxn],top[maxn],W[maxn],a[maxn],size[maxn],son[maxn],seg[maxn],col; //dep:深度 father:父亲节点 top:重链的顶端 W:到根节点的距离 a:点的权值 size:子树大小 son:重儿子 seg:在线段树中的序号(dfs序) il void dfs1(int u,int fr) { dep[u]=dep[fr]+1; size[u]=1; father[u]=fr; for(re int i=head[u];i;i=e[i].next) { re int v=e[i].v; if(v!=fr) { W[v]=W[u]+e[i].w;//W为每一个点到根节点的距离 dfs1(v,u); size[u]+=size[v]; if(size[v]>size[son[u]]) son[u]=v; } } }//预处理出dep、size、father以及son il void dfs2(int now,int fi) { top[now]=fi; seg[now]=++col; a[col]=W[now]-W[father[now]];//a为点的权值(它与之父亲节点边的权值)(相当于前缀和) if(!son[now]) return; dfs2(son[now],fi); for(re int i=head[now];i;i=e[i].next) { re int v=e[i].v; if(v!=son[now]&&v!=father[now]) dfs2(v,v); } }//预处理出每个节点的top、seg以及权值 //树剖模板就不解释了 #define ls k<<1 #define rs k<<1|1 il bool CMP(int a,int b){return a>b;} il int getse(int x,int g,int z,int c) { re int a[5]={x,g,z,c}; sort(a,a+4,CMP); for(re int i=1;i<3;++i) { if(a[i]!=a[0]) return a[i]; } }//找到两个区间的最大值和严格次大值(四个数)的最大值与严格次大值 // 就是合并两个区间的最大值和严格次大值 il void build(int k,int l,int r) { if(l==r) { q[k].ma=a[l]; return; } re int mid=(l+r)>>1; build(ls,l,mid),build(rs,mid+1,r); q[k].ma=max(q[ls].ma,q[rs].ma); q[k].ma2=getse(q[ls].ma,q[rs].ma,q[ls].ma2,q[rs].ma2); }//预处理出区间最大值与次大值 il qj query(int k,int l,int r,int ll,int rr) { if(ll>r||rr<l) return (qj){-inf,-inf}; if(ll<=l&&rr>=r) return (qj){q[k].ma,q[k].ma2}; re int mid=(l+r)>>1; re qj t1=query(ls,l,mid,ll,rr),t2=query(rs,mid+1,r,ll,rr); return (qj){max(t1.ma,t2.ma),getse(t1.ma,t2.ma,t1.ma2,t2.ma2)}; }//查询区间的区间的最大值与次小值 il int LCA(int u,int v,int d) { re int need=-inf; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); qj temp=query(1,1,n,seg[top[u]],seg[u]); u=father[top[u]]; need=max(need,(temp.ma==d)?temp.ma2:temp.ma);//严格次小边(如果temp.ma==k就是非严格次小) } if(dep[u]<dep[v]) swap(u,v);//找到LCA qj temp=query(1,1,n,seg[v]+1,seg[u]); return max(need,(temp.ma==d)?temp.ma2:temp.ma);//同上 } il void init() { dfs1(1,0),dfs2(1,1),build(1,1,n); } } //树链剖分 signed main() { n=read(),m=read(); smallesttree::kruskal();//求出最小生成树 treecut::init();//预处理 for(re int i=0;i<m;++i) { if(vis[i]) continue;//枚举所有非树边(没有在最小生成树的边) re int temp=mtree/*最小生成树边权和*/+edge[i].w/*本来的树边的边权*/-treecut::LCA(edge[i].u,edge[i].v,edge[i].w)/*找到严格次小边的边权*/; if(ans>temp&&temp!=mtree+e[i].w/*其实就是严格此小边不为0(没有找到严格次小边)*/&&temp>mtree) ans=temp; } printf("%lld",ans); return 0; } ``` ## $updata$ $in$ $2019$-$04$-$02$ 学习了LCT后,发现这道题貌似可以LCT做,而且代码短了很多? 先跑一边$Kruskal$,并在LCT上跑出最小生成树的样子 然后对于每一条非树边,加入后会产生一个环,我们删去环内最大值(若最大值和改变相同则插入次大值)即可 ``` #include<bits/stdc++.h> using namespace std; #define il inline #define re register #define debug printf("Now is Line : %d\n",__LINE__) #define file(a) freopen(#a".in","r",stdin);//freopen(#a".out","w",stdout) #define inf 1234567890000000000 #define ll long long il int read() { re int x = 0, f = 1; re char c = getchar(); while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar();} while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar(); return x * f; } #define rep(i, s, t) for(re int i = s; i <= t; ++ i) #define get_fa(x) ch[1][fa[x]] == x #define isroot(x) ch[1][fa[x]] == x || ch[0][fa[x]] == x #define updown(x) tag[x] ^= 1, swap(ch[0][x], ch[1][x]) #define maxn 600005 struct edge { int u, v, w; }e[maxn]; int n, m, is[maxn],ch[2][maxn], fa[maxn], tag[maxn], st[maxn], Fa[maxn]; int mx1[maxn], mx2[maxn], val[maxn]; ll ans, Ans = inf; il int find(int x) { while(Fa[x] != x) x = Fa[x] = Fa[Fa[x]]; return x; } il bool cmp(edge a, edge b) {return a.w < b.w;} il void pushdown(int x) { if(!tag[x]) return; if(ch[0][x]) updown(ch[0][x]); if(ch[1][x]) updown(ch[1][x]); tag[x] = 0; } il void pushup(int x) { mx1[x] = val[x]; if(mx1[x] < mx1[ch[0][x]]) mx2[x] = mx1[x], mx1[x] = mx1[ch[0][x]]; else if(mx1[x] > mx1[ch[0][x]]) mx2[x] = max(mx2[x], mx1[ch[0][x]]); if(mx1[x] < mx1[ch[1][x]]) mx2[x] = mx1[x], mx1[x] = mx1[ch[1][x]]; else if(mx1[x] > mx1[ch[1][x]]) mx2[x] = max(mx2[x], mx1[ch[1][x]]); mx2[x] = max(max(mx2[x], mx2[ch[1][x]]), mx2[ch[0][x]]); } il void rotate(int x) { int y = fa[x], z = fa[y], w = get_fa(x), k = get_fa(y); if(isroot(y)) ch[k][z] = x; fa[x] = z; ch[w][y] = ch[w ^ 1][x], fa[ch[w ^ 1][x]] = y; ch[w ^ 1][x] = y, fa[y] = x; pushup(y), pushup(x); } il void Splay(int x) { int top = 0, y = x; st[++ top] = y; while(isroot(y)) st[++ top] = y = fa[y]; while(top) pushdown(st[top --]); while(isroot(x)) { if(isroot(fa[x])) rotate(get_fa(x) == get_fa(fa[x]) ? fa[x] : x); rotate(x); } } il void access(int x) {for(re int y = 0; x; x = fa[y = x]) Splay(x), ch[1][x] = y, pushup(x);} il void makeroot(int x) {access(x), Splay(x), updown(x);} il int findroot(int x) { access(x), Splay(x); while(ch[0][x]) x = ch[0][x]; return Splay(x), x; } il void spilt(int x, int y) {makeroot(x), access(y), Splay(y);} il void link(int x, int y) { makeroot(x); if(findroot(y) != x) fa[x] = y; } int main() { n = read(), m = read(); rep(i, 1, m) e[i].u = read(), e[i].v = read(), e[i].w = read(); rep(i, 1, n) Fa[i] = i; sort(e + 1, e + 1 + m, cmp); rep(i, 1, m) { val[i + n] = e[i].w; int u = e[i].u, v = e[i].v, a = find(u), b = find(v); if(a != b) ans += e[i].w, link(u, i + n), link(i + n, v), is[i] = 1, Fa[a] = b; } rep(i, 1, m) { if(is[i]) continue; int u = e[i].u, v = e[i].v; spilt(u, v); if(e[i].w > mx1[v]) Ans = min(Ans, (ll)e[i].w - mx1[v]); else Ans = min(Ans, (ll)e[i].w - mx2[v]); } printf("%lld", Ans + ans); return 0; } ```