• JZOJ5883【NOIP2018模拟A组9.25】到不了——动态LCA裸题


    题目描述

    Description

    wy 和 wjk 是好朋友。
    今天他们在一起聊天,突然聊到了以前一起唱过的《到不了》。
    “说到到不了,我给你讲一个故事吧。”
    “嗯?”
    “从前,神和凡人相爱了,愤怒的神王把他们关进了一个迷宫里,迷宫是由许多棵有根树组 成的。神王每次把两个人扔进其中的某一棵有根树里面,两个相邻节点的距离为 1,两人的 每一步都只能从儿子走到父亲,不能从父亲走到儿子,他们约定,走到同一个节点相见,由 于在迷宫里面行走十分消耗体力,他们决定找出那个使得他们走的总路程最少的节点,他们 当然会算出那个节点了,可是神王有时候会把两棵有根树合并为一棵,这下就麻烦了。。。”
    “唔。。。”
    [已经了解树,森林的相关概念的同学请跳过下面一段]
    树:由 n 个点,n-1 条边组成的无向连通图。
    父亲/儿子:把树的边距离定义为 1,root 是树的根,对于一棵树里面相邻的两个点 u,v,到 root 的距离近的那个点是父亲,到 root 距离远的那个点是儿子
    森林:由若干棵树组成的图
    [简化版题目描述]
    维护一个森林,支持连边操作和查询两点 LCA 操作

    Input

    第一行一个整数 N,M,代表森林里的节点总数和有根树的数目。
    第二行 M 个整数,第 i 个整数 ri 代表第 i 棵有根树的根是编号为 ri 的节点
    接下来 N-M 行,每行两个整数 u,v 表示 u 和 v 相邻
    接下来一行一个整数 Q,表示 Q 个事件发生了
    接下来 Q 行,每行若干个整数,表示一个事件
    如果第一个数 op=1,接下来两个整数 u,v,代表神王把 u 号节点所在的树和 v 号节点所在的树 合并到一起(即 u 到 v 连了一条边),新的根为原来 u 号节点所在的树的根(如果 u,v 已经联通, 忽略这个事件)。
    如果第一个数 op=2,接下来两个整数 u,v,代表一次询问,当一个人在 u 号节点,一个人 在 v 号节点,询问他们找到的那个节点的编号

    Output

    对于每一个询问(op=2 的操作),输出一行一个整数,代表节点编号,如果 u,v 不联通,输 出 orzorz。

    Sample Input

    【样例 1】
    2 2
    1 2
    2
    1 1 2
    2 1 2
    【样例 2】
    2 2
    1 2
    2
    1 2 1
    2 1 2

    Sample Output

    【样例 1】
    1
    【样例 2】
    2

    Data Constraint

    对于 30%的数据 1 ≤ N ≤ 1000 1 ≤ Q ≤ 1000
    对于 100%的数据 1 ≤ N ≤ 100000 1 ≤ Q ≤ 100000

    题目大意

    给你一个森林,接下来有好多个操作,分别是连边和询问两点的LCA。
    其中,如果是将点uu和点vv连起来,是将uu的根节点作为连完边后的根。


    思考历程

    如果这题没有连边操作,那就十分happy了。
    想过是否可以用链剖,发现好像不能。
    但是,既然有边的修改,那么——
    当然是用LCT了!
    思考了半天,我想到一个结论:
    对于两个点uuvv,它们到rootroot的路径之交中,深度最大的,就是它们的LCALCA
    所以,我一开始的想法是这样的:

    access(u)
    将u到root的路径上的所有点打个标记
    access(v)
    找到v所在splay中带标记的最深的点即LCA
    

    打着打着,突然发现很麻烦。
    想一下,access其实就是一个从当前点的splay上沿着虚边往上跳,将虚边改为实边的过程。
    access(u),如果再access(v),就可以发现:
    过程中修改的最后一个虚边的父亲,必定就是它们的LCA。
    原因很显然,因为跳到LCA后,再跳就没了。
    我在打access时,总喜欢设一个返回值,这个返回值是最后修改的点(也就是最后修改虚边的父亲)。因为这个点同时也是在过程后,根所在的splay中的根节点。(如果不记录,翻转的时候还需要splay一遍,多浪费啊!)
    其实可以发现,就算是uuvv同在一条链上,结果也是正确的。
    所以,想要切掉这题,只是一个LCT模板……
    还有,对于连通性,直接并查集就好。
    时间O(QlgN)O(Qlg N)


    题解&正解

    题解的做法有两种:

    离线做法

    有一个神奇的性质:

    在一棵以任意点为根的树中,uuvv对于rootroot为根的LCA为LCA(u,v)LCA(u,v)LCA(u,root)LCA(u,root)LCA(v,root)LCA(v,root)中最深的那个。同时,这三个数值中至少有两个是重复的,那个不重复的(或者三个都一样时的随意一个)就是LCA,可以写作LCA(u,v)LCA(u,root)LCA(v,root)LCA(u,v) igoplus LCA(u,root) igoplus LCA(v,root)

    具体怎么证明,请自行分类讨论,很好证的。
    那么我们可以处理出森林最后的样子,随便以一个点为根。
    在处理操作的过程中,用并查集记录每个点当时所处的树中的根是谁。
    直接利用上面的性质就好,求LCA用倍增或链剖。
    时间O(QlgN)O(Qlg N)

    在线做法

    和上面的做法本质差不多,要用倍增的方法。
    在合并两棵树的时候,简单粗暴地将那棵较小的树安在那棵较大的树下面,暴力更新它的倍增数组。
    暴力重建的个数不超过NlgNN lg N个,所以时间是O(Qlg2N)O(Q lg^2 N)


    代码

    LCT做法

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define MAXN 100000
    int n,m;
    int root[MAXN+1];
    struct Node{//LCT的节点
    	Node *fa,*c[2];
    	bool res,is_root;
    	inline bool getson() {return fa->c[0]!=this;}
    	inline void reserve(){
    		swap(c[0],c[1]);
    		res^=1;
    	}
    	inline void pushdown(){
    		if (res){
    			c[0]->reserve();
    			c[1]->reserve();
    			res=0;
    		}		
    	}
    	inline void push(){
    		if (!is_root)
    			fa->push();
    		pushdown();
    	}
    	inline void rotate(){
    		Node *y=fa,*z=y->fa;
    		bool k=getson();
    		fa=z;
    		if (y->is_root){
    			y->is_root=0;
    			is_root=1;
    		}
    		else 
    			z->c[y->getson()]=this;
    		y->c[k]=c[!k];
    		c[!k]->fa=y;
    		c[!k]=y;
    		y->fa=this;
    	}
    	inline void splay(){
    		push();
    		while (!is_root){
    			if (!fa->is_root){
    				if (getson()!=fa->getson())
    					rotate();
    				else 
    					fa->rotate();
    			}
    			rotate();
    		}
    	}
    	inline Node *access();
    	inline Node *mroot();
    	inline void link(Node *v);
    } d[MAXN+1];
    Node *null=d;
    inline Node *Node::access(){
    	Node *x=this,*y=null;
    	for (;x!=null;y=x,x=x->fa){
    		x->splay();
    		x->c[1]->is_root=1;
    		x->c[1]=y;
    		y->is_root=0;
    	}
    	return y;//返回最后修改的点
    }
    inline Node *Node::mroot(){
    	Node *sr=access();
    	sr->reserve();
    	return sr;
    }
    inline void Node::link(Node *v){
    	v->mroot()->fa=this;
    }
    int bcj[MAXN+1];//并查集
    int gettop(int x){
    	if (bcj[x]==x)
    		return x;
    	return bcj[x]=gettop(bcj[x]);
    }
    int main(){
    	freopen("arrival.in","r",stdin);
    	freopen("arrival.out","w",stdout);
    	scanf("%d%d",&n,&m);
    	for (int i=1;i<=n;++i){
    		d[i].fa=d[i].c[0]=d[i].c[1]=null;
    		d[i].is_root=1;
    	}
    	for (int i=1;i<=n;++i)
    		bcj[i]=i;
    	for (int i=1;i<=m;++i)
    		scanf("%d",&root[i]);
    	for (int i=1;i<=n-m;++i){
    		int u,v;
    		scanf("%d%d",&u,&v);
    		d[u].link(&d[v]);
    		bcj[gettop(u)]=gettop(v);
    	}
    	for (int i=1;i<=m;++i)
    		d[root[i]].mroot();
    	int Q;
    	scanf("%d",&Q);
    	for (int i=1;i<=Q;++i){
    		int op,u,v;
    		scanf("%d%d%d",&op,&u,&v);
    		if (op==1){
    			if (gettop(u)!=gettop(v)){
    				bcj[gettop(u)]=gettop(v);
    				d[u].link(&d[v]);
    			}
    		}
    		else{
    			if (gettop(u)==gettop(v)){
    				d[u].access();
    				printf("%d
    ",d[v].access()-d);
    			}
    			else
    				printf("orzorz
    ");
    		}
    	}
    	return 0;
    }
    

    题解做法(离线做法)

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define MAXN 100000
    #define MAXQ 100000
    int n,m;
    int root[MAXN+1];
    struct EDGE{
    	int to;
    	EDGE *las;
    } e[MAXN*2+1];
    int ne;
    EDGE *last[MAXN+1];
    inline void link(int u,int v){
    	e[++ne]={v,last[u]};
    	last[u]=e+ne;
    }
    int bcj[MAXN+1],tmp[MAXN+1];
    int gettop(int x){
    	if (bcj[x]==x)
    		return x;
    	return bcj[x]=gettop(bcj[x]);
    }
    int fa[MAXN+1][17];//倍增数组
    int dep[MAXN+1];
    void dfs(int);
    inline int lca(int,int);
    struct Operation{
    	int ty;
    	int u,v;
    } o[MAXQ+1];
    int main(){
    	freopen("arrival.in","r",stdin);
    	freopen("arrival.out","w",stdout);
    	scanf("%d%d",&n,&m);
    	for (int i=1;i<=n;++i)
    		bcj[i]=i;
    	for (int i=1;i<=m;++i)
    		scanf("%d",&root[i]);
    	for (int i=1;i<=n-m;++i){
    		int u,v;
    		scanf("%d%d",&u,&v);
    		link(u,v),link(v,u);
    		bcj[gettop(u)]=gettop(v);
    	}
    	for (int i=1;i<=m;++i){
    		bcj[gettop(root[i])]=root[i];
    		bcj[root[i]]=root[i];
    	}
    	memcpy(tmp,bcj,sizeof bcj);//bcj要用来处理连边时判断是否出现环,所以暂存一下
    	int Q;
    	scanf("%d",&Q);
    	for (int i=1;i<=Q;++i){
    		scanf("%d%d%d",&o[i].ty,&o[i].u,&o[i].v);
    		if (o[i].ty==1){
    			if (gettop(o[i].u)!=gettop(o[i].v)){
    				bcj[gettop(o[i].v)]=o[i].v;
    				bcj[o[i].v]=gettop(o[i].u);
    				link(o[i].u,o[i].v),link(o[i].v,o[i].u);
    			}
    		}
    	}
    	for (int i=1;i<=m;++i)
    		if (gettop(root[i])==root[i])
    			dfs(root[i]);
    	swap(bcj,tmp);//这样交换只会交换指针,不会交换整个数组
    	for (int i=1;i<=Q;++i)
    		if (o[i].ty==1){
    			if (gettop(o[i].u)!=gettop(o[i].v)){
    				bcj[gettop(o[i].v)]=o[i].v;
    				bcj[o[i].v]=gettop(o[i].u);
    			}
    		}
    		else{
    			int rt=gettop(o[i].u);
    			if (rt!=gettop(o[i].v)){
    				printf("orzorz
    ");
    				continue;
    			}
    			printf("%d
    ",lca(o[i].u,o[i].v)^lca(o[i].u,rt)^lca(o[i].v,rt));
    		}
    	return 0;
    }
    void dfs(int x){
    	dep[x]=dep[fa[x][0]]+1;
    	for (int i=1;1<<i<dep[x];++i)
    		fa[x][i]=fa[fa[x][i-1]][i-1];
    	for (EDGE *ei=last[x];ei;ei=ei->las)
    		if (ei->to!=fa[x][0]){
    			fa[ei->to][0]=x;
    			dfs(ei->to);
    		}
    }
    inline int lca(int u,int v){
    	if (dep[u]<dep[v])
    		swap(u,v);
    	for (int k=dep[u]-dep[v],i=0;k;k>>=1,++i)
    		if (k&1)
    			u=fa[u][i];
    	if (u==v)
    		return u;
    	for (int i=16;i>=0;--i)
    		if (fa[u][i]!=fa[v][i])
    			u=fa[u][i],v=fa[v][i];
    	return fa[u][0];
    }
    

    总结

    LCT大法好,比题解的在线做法好多了。
    如果题目加上个删边操作……题解的两个方法都做不了,除非有什么变态的改进。
    其实LCT也挺好打的,很好手推。

  • 相关阅读:
    [LeetCode] 347. Top K Frequent Elements 前K个高频元素
    [CareerCup] 18.2 Shuffle Cards 洗牌
    [CareerCup] 18.1 Add Two Numbers 两数相加
    [LeetCode] 346. Moving Average from Data Stream 从数据流中移动平均值
    [LintCode] Decode Ways 解码方法
    [LintCode] Perfect Squares 完全平方数
    [LintCode] Paint Fence 粉刷篱笆
    [LintCode] Paint House 粉刷房子
    [LintCode] Paint House II 粉刷房子之二
    [LintCode] Ugly Number 丑陋数
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145271.html
Copyright © 2020-2023  润新知