JZOJ5966【NOIP2018提高组D2T3】保卫王国(并查集)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/A1847225889/article/details/84309834

题目

还是懒得把题目放上来了。
大意:给你一棵带点权的树,你要花费一些代价选择一些点使得相邻的两个点至少有一个被选。
然后有很多个询问,每个询问强制两个点的状态,问强制了这两个点的状态后的方案。


比赛思路

没时间了,没时间了……
匆匆打个44分的暴力就好了。
结果混淆了概念,打出来的DP是求一个点自己或周围至少有一个选的方案,和题目就不是一个样子。
比赛结束了,我还没有调处来,然后就爆0了。


解法

先说说暴力。
这是一个非常典型的问题,设 f i , 0 / 1 f_{i,0/1} 表示以 i i 为根的子树中,不选或选 i i 的最优解。
(比赛时设的根本就不是同一道题)
这个DP的方程应该没有人不懂的吧:
f i , 0 = f s o n , 1 f i , 1 = min ( f s o n , 0 , f s o n , 1 ) f_{i,0}=\sum f_{son,1} \\ f_{i,1}=\sum \min\left(f_{son,0},f_{son,1}\right)
所以,暴力做法就是,将某一个值赋值为无限大,然后暴力地重新DP(当然,其实只需要更新它自己到根的这条路径就好了)。

考虑正解。
首先说一下,我的解法是在JZOJ、洛谷排名第一的并查集解法,时间复杂度几乎是线性的。倍增解法或许和并查集解法有很大相似之处,而对于那些打树链剖分的动态DP的方法,个人认为我的方法和他们的方法存在着太大的差别。

首先我们可以抽象地思考一下:
对于这个询问,其实就是将询问的两个点提起来,答案为限制了它们之后,链上的子树的贡献。
首先我们处理另一个DP,设 g i , 0 / 1 g_{i,0/1} 表示除以 i i 为根的子树外,不选或选 i i 的最优解。(将它提起来之后,之前的父亲也可以看成一个儿子。)
这个方程也是挺好想的(为了方便表达,设 f i , 2 = min ( f i , 0 , f i , 1 ) f_{i,2}=\min\left(f_{i,0},f_{i,1}\right) ):
g s o n , 0 = g i , 1 + ( f i , 1 f s o n , 2 ) g s o n , 1 = min ( g i , 0 + ( f i , 0 f s o n , 1 ) , g i , 1 + ( f i , 1 f s o n , 2 ) ) g_{son,0}=g_{i,1}+(f_{i,1}-f_{son,2}) \\ g_{son,1}=\min\left(g_{i,0}+(f_{i,0}-f_{son,1}),g_{i,1}+(f_{i,1}-f_{son,2})\right)
这个方程是从上往下转移的。
其中 f i , 1 f s o n , 2 f_{i,1}-f_{son,2} 中,由 f f 的转移方程得 f i , 1 f s o n , 2 = ( f s o n , 2 ) f s o n , 2 = s o n s o n f s o n , 2 f_{i,1}-f_{son,2}=\left(\sum f_{son',2}\right)-f_{son,2}=\sum_{son'\neq son} f_{son',2}
下面的那个类似。

这样子DP部分就搞完了,剩下的东西就是维护。
我们可以参考一下Tarjan求LCA的过程(这个名字有毒,和强联通分量的那个完全不是一个东西,不要被名字震撼到),其实也就是用并查集求LCA的过程。
简要地说一下过程:
对于一个节点 u u ,首先 f a u = u fa_u=u ,然后dfs它的儿子。当从它的儿子那里回溯上来的时候, f a s o n = u fa_{son}=u ,然后枚举和 u u 有关联的询问,设另一个点为 v v ,如果 f a v 0 fa_v \neq 0 ,则它还未被访问过,先不理它;否则, L C A LCA 就是 g e t f a t h e r ( v ) getfather(v)
至于这个算法是为什么,其实随便想一想就可以了。在dfs的时候,先到 L C A LCA ,再到 v v ,回溯上去,在 L C A LCA 处转弯,再走到 u u 。由此可见,自 v v u u ,深度最小的地方就是它们的 L C A LCA (莫名其妙地想起了ST表求LCA),深度最小的地方也就是 v v 在并查集上的最远祖先。

对于每个点,我们不只是记录一下它在并查集上的父亲,还要记录一下它和他父亲之间的答案。
每个节点的答案记录 4 4 条信息,表示父亲选或不选和它选或不选。
这个答案表示,在这个状态下,这条链上挂着的子树的最小答案。

我们可以在求 L C A LCA 的递归中,在从儿子回来的时候,对儿子的答案信息进行初始化:
h s o n , 00 = h s o n , 01 = f u , 0 f s o n , 1 + f s o n , 1 h s o n , 10 = f u , 1 f s o n , 2 + f s o n , 0 h s o n , 11 = f u , 1 f s o n , 2 + f s o n , 1 h_{son,00}=\infty \\ h_{son,01}=f_{u,0}-f_{son,1}+f_{son,1} \\ h_{son,10}=f_{u,1}-f_{son,2}+f_{son,0}\\ h_{son,11}=f_{u,1}-f_{son,2}+f_{son,1}
首先,由于相邻的两个不能都不选,所以设为无限大。
其它的东西,都是它父亲的贡献,减掉儿子对父亲的贡献,再加上儿子自己的贡献。

对于这个东西,我们可以在 g e t f a t h e r getfather 的过程中顺便维护它们的值。
维护它的值就是要将两条链(其中有一个交点)的答案合并起来。
合并其实很简答,只需要枚举一下交点的状态,然后接起来并且减去重复的贡献就好了。
n e w i j = min ( h u p , i 0 f u p , 0 + h d o w n , 0 j , h u p , i 1 f u p , 1 + h d o w n , 1 j ) new_{ij}=\min\left(h_{up,i0}-f_{up,0}+h_{down,0j},h_{up,i1}-f_{up,1}+h_{down,1j}\right)
其中 d o w n down 表示下面的点, u p up 表示上面的点(其实也就是 f a d o w n fa_{down} ,同时也是两条链的交点)
这个东西其实就是核心操作了。

当你在 u u 求出 ( u , v ) (u,v) L C A LCA 的时候,不要急着求答案,因为 g e t f a t h e r ( v ) getfather(v) 已经上去了,而 g e t f a t h e r ( u ) getfather(u) 还没有上去。所以,求出 L C A LCA 之后,我们可以将询问再挂到 L C A LCA 上,在 L C A LCA 处计算答案。

在计算的时候,自然是分成两种情况:
当一个是另一个的祖先时,设 a a b b 的祖先,那么 a n s = h b , x y + g a , x ans=h_{b,xy}+g_{a,x}
否则,枚举LCA的状态, a n s = min ( h a , 0 x + h b , 0 y f l c a , 0 + g l c a , 0 , h a , 1 x + h b , 1 y f l c a , 1 + g l c a , 1 ) ans=\min(h_{a,0x}+h_{b,0y}-f_{lca,0}+g_{lca,0},h_{a,1x}+h_{b,1y}-f_{lca,1}+g_{lca,1})

时间复杂度 O ( n α ( n ) ) O(n \alpha (n))


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100000
#define M 100000
#define INF 1000000000000ll
int n,m;
int a[N+1];
struct EDGE{
	int to;
	EDGE *las;
} e[N*2+1];
int ne;
EDGE *last[N+1];
long long f[N+1][3],g[N+1][2];
void init1(int x,int fa){
	f[x][0]=0,f[x][1]=a[x];
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=fa){
			init1(ei->to,x);
			f[x][0]+=f[ei->to][1];
			f[x][1]+=f[ei->to][2];
		}
	f[x][2]=min(f[x][0],f[x][1]);
}
void init2(int x,int fa){
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=fa){
			g[ei->to][0]=g[x][1]+(f[x][1]-f[ei->to][2]);
			g[ei->to][1]=min(g[x][0]+(f[x][0]-f[ei->to][1]),g[ei->to][0]);
			init2(ei->to,x);
		}
}
struct Query{
	int a,x,b,y;
	int lca;
} q[M+1];
struct list{
	int v;
	list *lst;
} d[M*3+1];//这是链表开的内存池
int cnt;
list *qv[N+1],*ql[N+1];//qv[u]表示与u有关的询问 ql[u]表示lca为u的询问
inline void insert(list * &end,int v){
	++cnt;
	d[cnt].v=v,d[cnt].lst=end;
	end=d+cnt;
}
int top[N+1];//表示并查集上的父亲(我才不喜欢打fa)
long long h[N+1][4];//表示的时候直接压位了……自认为好打一些
inline void merge(int down,int up){//合并操作,将h[down]变为h[down]+h[up]
	static long long res[4];
	res[0]=min(h[up][0]-f[up][0]+h[down][0],h[up][1]-f[up][1]+h[down][2]);
	res[1]=min(h[up][0]-f[up][0]+h[down][1],h[up][1]-f[up][1]+h[down][3]);
	res[2]=min(h[up][2]-f[up][0]+h[down][0],h[up][3]-f[up][1]+h[down][2]);
	res[3]=min(h[up][2]-f[up][0]+h[down][1],h[up][3]-f[up][1]+h[down][3]);
	memcpy(&h[down],res,sizeof res);
}
void gettop(int x){
	if (top[top[x]]==top[x])
		return;
	gettop(top[x]);
	merge(x,top[x]);//在gettop过程中,合并两个答案信息
	top[x]=top[top[x]];
}
void dfs(int,int);
long long ans[N+1];
int main(){
	freopen("defense.in","r",stdin);
	freopen("defense.out","w",stdout);
	scanf("%d%d%*s",&n,&m);
	for (int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	for (int i=1;i<n;++i){
		int u,v;
		scanf("%d%d",&u,&v);
		++ne;
		e[ne].to=v,e[ne].las=last[u],last[u]=e+ne;
		++ne;
		e[ne].to=u,e[ne].las=last[v],last[v]=e+ne;
	}
	init1(1,0);
	init2(1,0);
	for (int i=1;i<=m;++i){
		scanf("%d%d%d%d",&q[i].a,&q[i].x,&q[i].b,&q[i].y);
		insert(qv[q[i].a],i);
		insert(qv[q[i].b],i);
	}
	dfs(1,0);
	for (int i=1;i<=m;++i)
		printf("%lld\n",ans[i]>=INF?-1:ans[i]);
	return 0;
}
void dfs(int x,int fa){
	top[x]=x;
	for (list *i=qv[x];i;i=i->lst){
		int u=q[i->v].a^q[i->v].b^x;//表示a,b中除了x以外的另一个数
		if (!top[u])
			continue;
		gettop(u);
		q[i->v].lca=top[u];//求出LCA
		insert(ql[q[i->v].lca],i->v);//将询问挂在LCA上
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=fa){
			dfs(ei->to,x);
			//一坨初始化,具体解释见上
			h[ei->to][0]=INF;
			h[ei->to][1]=f[x][0]-f[ei->to][1]+f[ei->to][1];
			h[ei->to][2]=f[x][1]-f[ei->to][2]+f[ei->to][0];
			h[ei->to][3]=f[x][1]-f[ei->to][2]+f[ei->to][1];
			top[ei->to]=x;
		}
	for (list *i=ql[x];i;i=i->lst){
		int a=q[i->v].a,b=q[i->v].b;
		gettop(a),gettop(b);
		//具体解释见上
		if (x==a)
			ans[i->v]=h[b][q[i->v].x<<1|q[i->v].y]+g[a][q[i->v].x];
		else if (x==b)
			ans[i->v]=h[a][q[i->v].y<<1|q[i->v].x]+g[b][q[i->v].y];
		else
			ans[i->v]=min(h[a][0<<1|q[i->v].x]+h[b][0<<1|q[i->v].y]-f[x][0]+g[x][0],h[a][1<<1|q[i->v].x]+h[b][1<<1|q[i->v].y]-f[x][1]+g[x][1]);
	}
}

总结

只要不修改,离线之后,并查集方法有时可以代替倍增。
举个例子,询问树上两点之间的最大值……
用并查集维护它到 f a fa 这条链上的信息,然后在 L C A LCA 处计算就好了。

猜你喜欢

转载自blog.csdn.net/A1847225889/article/details/84309834