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


    题目

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


    比赛思路

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


    解法

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

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

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

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

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

    我们可以在求LCALCA的递归中,在从儿子回来的时候,对儿子的答案信息进行初始化:
    hson,00=hson,01=fu,0fson,1+fson,1hson,10=fu,1fson,2+fson,0hson,11=fu,1fson,2+fson,1h_{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}
    首先,由于相邻的两个不能都不选,所以设为无限大。
    其它的东西,都是它父亲的贡献,减掉儿子对父亲的贡献,再加上儿子自己的贡献。

    对于这个东西,我们可以在getfathergetfather的过程中顺便维护它们的值。
    维护它的值就是要将两条链(其中有一个交点)的答案合并起来。
    合并其实很简答,只需要枚举一下交点的状态,然后接起来并且减去重复的贡献就好了。
    newij=min(hup,i0fup,0+hdown,0j,hup,i1fup,1+hdown,1j)new_{ij}=minleft(h_{up,i0}-f_{up,0}+h_{down,0j},h_{up,i1}-f_{up,1}+h_{down,1j} ight)
    其中downdown表示下面的点,upup表示上面的点(其实也就是fadownfa_{down},同时也是两条链的交点)
    这个东西其实就是核心操作了。

    当你在uu求出(u,v)(u,v)LCALCA的时候,不要急着求答案,因为getfather(v)getfather(v)已经上去了,而getfather(u)getfather(u)还没有上去。所以,求出LCALCA之后,我们可以将询问再挂到LCALCA上,在LCALCA处计算答案。

    在计算的时候,自然是分成两种情况:
    当一个是另一个的祖先时,设aabb的祖先,那么ans=hb,xy+ga,xans=h_{b,xy}+g_{a,x}
    否则,枚举LCA的状态,ans=min(ha,0x+hb,0yflca,0+glca,0,ha,1x+hb,1yflca,1+glca,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
    ",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]);
    	}
    }
    

    总结

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

  • 相关阅读:
    EF 错误解决
    TortoiseHg 学习笔记 (转)
    Mysql 命令行 使用 (转)
    2017-9-3 时间字符串格式化(转)
    2017-8-25 c# 获取url参数的五种方法(转)
    alert 的使用方法
    表单关键字查询写法
    Mysql和Mysqli的区别
    php MySQL中 增、删、改、查的写法格式
    一维、二维数组 与 常用的返回数组 以及 fetch_all与fetch_row的区别
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145259.html
Copyright © 2020-2023  润新知