【洛谷4775】[NOI2018] 情报中心(线段树合并+虚树二合一)

点此看题面

  • 给定一棵(n)个点的树和(m)条树上路径,每条边有一个边权,每条路径有一个代价。
  • 要求选出两条相交的路径,求出路径并集的边权和减去两路径代价和的最大值。
  • (nle5 imes10^4,mle10^5)

这题真的恶心,一共写了5.30KB。。。(大概是今年第一次写这么长的代码)

因为线段树合并后忘写(PushUp)+虚树的根节点漏清了一个信息直接调试到原地升天。

路径的两类讨论

对于一条路径((x,y)),我们找出(z=LCA(x,y))称作这条路径的顶点,(x,y)则是这条路径的两个端点。

那么任意两条路径之间的关系都可以根据顶点是否相同分成两类,这是我们做这道题的基础。

而分成两类也就意味着码量倍增。。。

顶点不同:线段树合并

对于顶点不同的两条路径,它们的并集只可能是一条直链。

这也就启示我们,此时可以把一条路径拆成顶点到两个端点的两条直链。

这样一来我们可以去枚举直链的相交处(x),假设这两条直链的端点分别为(x_1,x_2),顶点分别为(z_1,z_2)

(d_x)(x)的深度,(D_x)(x)到根的边权和。

如果(d_{z_1}<d_{z_2}),那么路径并集的边权和就应该是(D_{x_1}+D_{x_2}-D_x-D_{z_1});如果(d_{z_1}>d_{z_2})反之;如果(D_{z_1}=D_{z_2})则顶点相等无法计算贡献。

(d_{z_1}<d_{z_2})为例,结合上另一端点(y_1,y_2)和路径的原代价(v_1,v_2),合起来总的计算式应该是:

[(D_{x_1}+D_{x_2}-D_x-D_{z_1})+(D_{y_1}-D_{z_1})+(D_{y_2}-D_{z_2})-v_1-v_2 ]

合并同一路径的项得到:

[(D_{x_1}+D_{y_1}-2 imes D_{z_1}-v_1)+(D_{x_2}+D_{y_2}-D_{z_2}-v_2)-D_x ]

因此,一条路径的价值分为两种,取决于其顶点和另一条路径顶点的深度关系。

所以我们以顶点深度为下标建一棵线段树,每个节点上维护两种价值各自的最大值。

这里把(D_{x}+D_{y}-D_{z}-v)称作(V1),把(D_{x}+D_{y}-2 imes dis_{z}-v)称作(V2)

由于(x)是路径相交处,所以必须保证产生贡献的两条路径来自不同子树或就是以当前(x)为端点的。那么就可以用线段树合并来计算答案,只要在每次拆区间的时候用一棵线段树左儿子的(V2)与另一棵线段树右儿子的(V1)更新答案即可。

这样统计出的是以(x)为相交处的答案,最后还要减去(D_x)再更新总答案。

顶点相同:虚树+直径合并

我们可以直接枚举顶点(z),将以(z)为顶点的所有路径的端点视作关键点建出一棵虚树。

接着枚举路径中某一直链的相交处(x)(x)子树内的两端点记作(x_1,x_2),另外的两端点自然就是(y_1,y_2)了。

这里我们考虑路径并集边权和的另一种计算方式,将路径边权和简单相加相当于将交集边权和计算了两次,而剩余部分可以看作是(dis(x_1,x_2))(dis(y_1,y_2)),于是发现并集的边权和就等于:

[frac12(dis(x_1,y_1)+dis(x_2,y_2)+dis(x_1,x_2)+dis(y_1,y_2)) ]

结合上路径的原代价(v_1,v_2),所以说一条路径总的计算式就应该是:

[frac12(dis(x_1,y_1)+dis(x_2,y_2)+dis(x_1,x_2)+dis(y_1,y_2)-2 imes v_1-2 imes v_2) ]

利用当前枚举的相交点(x)拆开(dis(x_1,x_2)=D_{x_1}+D_{x_2}-2 imes D_x),然后合并同一路径的项得到:

[frac12((dis(x_1,y_1)+D_{x_1}-2 imes v_1)+(dis(x_2,y_2)+D_{x_2}-2 imes v_2)+dis(y_1,y_2)-2 imes D_x) ]

发现这个(dis(y_1,y_2))同时关联到两条路径就显得非常棘手。

因此我们做一个转化,为(x_1,x_2)分别新建一个辅助点(p_1,p_2):把(p_1)接在(y_1)下面,边权为(dis(x_1,y_1)+D_{x_1}-2 imes v_1);同理把(p_2)接在(y_2)下面。

这样一来,式子就被写成了:

[frac12(dis(p_1,p_2)-2 imes D_x) ]

由于(x_1,x_2)必须来自(x)的不同子树或就是等于(x),我们分别维护出每个子树内所有(x_i)对应的​(p_i)集合,那么一次合并就是要求两个集合间的直径。

一个众所周知的事实,两个集合间的直径的两个端点必然分别取自两个集合各自直径的端点,所以维护直径合并就变得非常容易了。

具体实现详见代码。

代码:(O(mlogn))

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 50000
#define M 100000
#define LN 16
#define LL long long
#define INF (LL)2e18
#define pb emplace_back
#define Gmax(x,y) (x<(y)&&(x=(y)))
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].v=z)
using namespace std;
int n,m,ee,lnk[N+5];LL ans;struct edge {int to,nxt,v;}e[2*N+5];struct Q {int x,y;LL v;}q[M+5];
namespace FastIO
{
	#define FS 100000
	#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
	char oc,FI[FS],*FA=FI,*FB=FI;
	Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
	Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
}using namespace FastIO;
namespace Tree//树信息的初始化
{
	int f[N+5][LN+1],d[N+5];LL D[N+5];I void Init(CI x=1)//dfs求出节点深度和到根权值和
	{
		RI i;for(i=1;i<=LN;++i) f[x][i]=f[f[x][i-1]][i-1];
		for(i=lnk[x];i;i=e[i].nxt) e[i].to^f[x][0]&&(d[e[i].to]=d[f[e[i].to][0]=x]+1,D[e[i].to]=D[x]+e[i].v,Init(e[i].to),0);
	}
	I int LCA(RI x,RI y)//倍增LCA
	{
		RI i;for(d[x]<d[y]&&(swap(x,y),0),i=0;d[x]^d[y];++i) (d[x]^d[y])>>i&1&&(x=f[x][i]);if(x==y) return x;
		for(i=LN;~i;--i) f[x][i]^f[y][i]&&(x=f[x][i],y=f[y][i]);return f[x][0];
	}
}using namespace Tree;
namespace T//线段树合并
{
	int Rt[N+5];class SegmentTree
	{
		private:
			#define PT CI l=0,CI r=n
			#define New() (Et?Ep[Et--]:++Nt)
			#define PU(x) (O[x].V1=max(O[O[x].S[0]].V1,O[O[x].S[1]].V1),O[x].V2=max(O[O[x].S[0]].V2,O[O[x].S[1]].V2))//维护两种值各自的最大值
			int Nt,Et,Ep[N*30];struct node {LL V1,V2;int S[2];I node() {V1=V2=-INF,S[0]=S[1]=0;}}O[N*30];
		public:
			I void Cl() {Nt=Et=0;}//清空
			I void A(int& rt,CI x,Con LL& v1,Con LL& v2,PT)//插入元素
			{
				if(!rt&&(O[rt=New()]=node(),0),l==r) return (void)(Gmax(O[rt].V1,v1),Gmax(O[rt].V2,v2));
				RI mid=l+r>>1;x<=mid?A(O[rt].S[0],x,v1,v2,l,mid):A(O[rt].S[1],x,v1,v2,mid+1,r),PU(rt);
			}
			I void E(int& rt,CI x,PT)//删除元素
			{
				if(!rt) return;if(l==r) return (void)(Ep[++Et]=rt,rt=0);RI mid=l+r>>1;
				x<=mid?E(O[rt].S[0],x,l,mid):E(O[rt].S[1],x,mid+1,r),!O[rt].S[0]&&!O[rt].S[1]?(Ep[++Et]=rt,rt=0):PU(rt);
			}
			I void Merge(int& x,CI y,LL& t,PT)//合并时更新答案
			{
				if(!x||!y) return (void)(x|=y);Ep[++Et]=y;
				if(l==r) return (void)(Gmax(O[x].V1,O[y].V1),Gmax(O[x].V2,O[y].V2));//叶节点没有贡献,仅更新值
				Gmax(t,O[O[x].S[0]].V2+O[O[y].S[1]].V1),Gmax(t,O[O[x].S[1]].V1+O[O[y].S[0]].V2);//左儿子V2+右儿子V1
				RI mid=l+r>>1;Merge(O[x].S[0],O[y].S[0],t,l,mid),Merge(O[x].S[1],O[y].S[1],t,mid+1,r),PU(x);//递归合并
			}
	}S;
	struct OP {int d;LL v1,v2;I OP(CI x=0,Con LL& y=0,Con LL& z=0):d(x),v1(y),v2(z){};};vector<OP> p[N+5];
	vector<OP>::iterator it;I void Solve(CI x=1)
	{
		Rt[x]=0;LL t=-INF;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x][0]&&(Solve(e[i].to),S.Merge(Rt[x],Rt[e[i].to],t),0);//先做子节点然后将线段树合并
		RI rt;for(it=p[x].begin();it!=p[x].end();++it) rt=0,S.A(rt,it->d,it->v1,it->v2),S.Merge(Rt[x],rt,t);//以当前点为端点的新链,建出后合并
		Gmax(ans,t-D[x]),d[x]&&(S.E(Rt[x],d[x]-1),0),p[x].clear();//减去D[x]后更新答案,删去以父节点为顶点的链信息
	}
}
namespace V//虚树+直径合并
{
	int d,dI[N+5],dO[N+5],o[2*M+5],nf[2*M+5];LL nD[2*M+5];vector<int> p[N+5],w[N+5];vector<int>::iterator it;
	I void Init(CI x=1) {dI[x]=++d;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x][0]&&(Init(e[i].to),0);dO[x]=d;}//预处理dfs序
	I bool cmp(CI x,CI y) {return dI[x]<dI[y];}//根据dfs序排序
	I LL Dis(CI x,CI y) {return x&&y?nD[x]+nD[y]-2*D[LCA(nf[x],nf[y])]:-INF;}//求出两辅助点间距离
	I void Merge(int& x,int& y,CI a,CI b,LL& t)//合并树的直径
	{
		LL d1=Dis(x,a),d2=Dis(x,b),d3=Dis(y,a),d4=Dis(y,b);Gmax(t,d1),Gmax(t,d2),Gmax(t,d3),Gmax(t,d4);//用两集合间的直径更新答案
		LL p1=Dis(x,y),p2=Dis(a,b);if(p2>=p1&&p2>=d1&&p2>=d2&&p2>=d3&&p2>=d4) return (void)(x=a,y=b);//完全使用新集合直径
		if(p1>=d1&&p1>=d2&&p1>=d3&&p1>=d4) return;d1>=d2&&d1>=d3&&d1>=d4?y=a:(d2>=d3&&d2>=d4?y=b:(d3>=d4?x=a:x=b));//完全保留原有直径;保留最长的两集合间直径
	}
	int vis[N+5],anc[N+5],S[N+5],px[N+5],py[N+5];LL t[N+5];I void VTree(int *o,RI c)//建虚树求解
	{
		RI i,k,nc;for(sort(o+1,o+c+1,cmp),nc=c=unique(o+1,o+c+1)-o-1,i=1;i<=c;++i) vis[o[i]]=1;//关键点排序去重
		for(i=1;i^c;++i) !vis[k=LCA(o[i],o[i+1])]&&(vis[o[++nc]=k]=1);sort(o+1,o+nc+1,cmp),c=unique(o+1,o+nc+1)-o-1;//加入相邻关键点间LCA
		RI T;for(S[T=1]=1,i=2;i<=c;anc[i]=S[T],S[++T]=i++) W(dO[o[S[T]]]<dI[o[i]]) --T;//求出每个点虚树上的的父节点
		for(i=1;i<=c;++i) t[i]=-INF;//初始赋值为-INF
		for(i=c;i^1;Merge(px[anc[i]],py[anc[i]],px[i],py[i],t[anc[i]]),--i)//把当前直径合并给父节点
			{for(it=w[o[i]].begin();it!=w[o[i]].end();++it) Merge(px[i],py[i],*it,0,t[i]);Gmax(ans,(t[i]-2*D[o[i]])/2);}//加入当前点对应的辅助点,然后更新答案
		for(i=1;i<=c;++i) vis[o[i]]=anc[i]=px[i]=py[i]=0,w[o[i]].clear();//清空
	}
	I void Solve(CI x=1)
	{
		for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^f[x][0]&&(Solve(e[i].to),0);if(p[x].empty()) return;//如果没有以当前点为顶点的链直接退出
		RI c=0,qx,qy;for(it=p[x].begin();it!=p[x].end();++it) qx=q[*it].x,qy=q[*it].y,
			++c,w[o[c]=qx].pb(c),nD[c]=D[nf[c]=qy]+(D[qx]+D[qy]-2*D[LCA(qx,qy)]+D[qx]-2*q[*it].v),//为x在y下新建辅助点
			++c,w[o[c]=qy].pb(c),nD[c]=D[nf[c]=qx]+(D[qx]+D[qy]-2*D[LCA(qx,qy)]+D[qy]-2*q[*it].v);//为y在x下新建辅助点
		VTree(o,c),p[x].clear();
	}
}
int main()
{
	RI Tt,i,x,y,z;read(Tt);W(Tt--)
	{
		for(read(n),T::S.Cl(),V::d=ee=0,i=1;i<=n;++i) lnk[i]=0;//多组数据的清空
		for(i=1;i^n;++i) read(x,y,z),add(x,y,z),add(y,x,z);Init(),V::Init();
		for(read(m),i=1;i<=m;++i) read(q[i].x,q[i].y,q[i].v),V::p[z=LCA(q[i].x,q[i].y)].pb(i),
			q[i].x^z&&(T::p[q[i].x].pb(d[z],D[q[i].x]+D[q[i].y]-D[z]-q[i].v,D[q[i].x]+D[q[i].y]-2*D[z]-q[i].v),0),//拆成z到x的直链
			q[i].y^z&&(T::p[q[i].y].pb(d[z],D[q[i].x]+D[q[i].y]-D[z]-q[i].v,D[q[i].x]+D[q[i].y]-2*D[z]-q[i].v),0);//拆成z到y的直链
		ans=-INF/2,T::Solve(),V::Solve(),ans==-INF/2?puts("F"):printf("%lld
",ans);
	}return 0;
}
败得义无反顾,弱得一无是处
原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu4775.html