zoukankan      html  css  js  c++  java
  • [学习笔记] 圆方树

    前言

    ( t NOI) 模拟考到了这东西,因为不会所以就来填坑啦。

    好神奇的东西!我好喜欢!这位巨佬讲的很清楚哦。

    圆方树的功能就是把我们不清楚的仙人掌问题转化成熟悉的树问题。

    这篇文章是写给我自己看的,所以可能和给出的材料有很大的重合(我就是复读了一遍额)

    构造

    仙人掌的定义:每条边最多属于一个环的无向连通图。

    构造原仙人掌的 (G) 的圆方树 (T_G),定义原仙人掌在圆方树上的点为圆点

    考虑对原仙人掌搞一遍 ( t Tarjan),因为每个环都是一个点双联通分量,考虑点 (i) 连出去的一个点 (j),如果 (dfn[i]=low[j]) 就说明 (i) 点是环的根,那么我们新建一个方点处理这个环。我们从栈中取出这个环的所有点,然后把方点连向所有环上所有点,注意我们让圆点 (i) 作为这个局部子树的根,那么就能构建出圆方树啦。

    圆方树有以下优美的性质:

    • 无论如何换根,圆方树形态不变(因为连成的是菊花)

    • 圆方树的子树 (=) 原仙人掌的子仙人掌

    • 方点不会和方点相连

    补充一点我的理解:圆方树其实是方便了我们单独考虑每个环内部的最优解(因为仙人掌的特殊性),再通过树的结构合并这些最优解,更多的还是要看例题啊。

    例一

    题目描述

    来源:( t bzojspace 2125) 最短路,点此看题

    给一个 (n) 个点 (m) 条边的仙人掌,(q) 次询问两个点之间的最短路径。

    (1leq n,qleq 10000)

    解法

    首先建出圆方树,但是圆方树上的距离不一定就等于仙人掌上最短路长度。

    现在要确定圆方树上的边是什么样子的,如果是圆圆边(连接了两个圆点)那么直接就是原仙人掌上的边权即可,如果是方圆边那么边权设置为环的根到这个圆点的最短距离,如果是圆方边的话边权设置为 (0)

    最短距离就考虑圆方树上的 ( t lca),如果 ( t lca) 是圆点,那么圆方树上路径长度就是最短路长度,因为路过的每个环我们都取的是最短路。如果 ( t lca) 是方点,可以通过倍增找到 ((x,y)) 子树的方向设为 ((p_x,p_y)),那么 (x ightarrow p_x,y ightarrow p_y) 都是最短路径,所以只需要找到环上点 ((p_x,p_y)) 的最短路径就可以了,这个可以通过前缀和和环上总的边长算出来。

    时间复杂度 (O(nlog n)),开打!如果没写过可以康康我怎么建圆方树的。

    #include <cstdio>
    #include <vector>
    #include <iostream>
    using namespace std;
    const int M = 100005;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,q,cnt,tot,f[M],dfn[M],low[M],sum[M];
    int Ind,fa[M],res[M],p[M][20],dis[M],dep[M];
    struct edge
    {
    	int v,c,next;
    }e[2*M];vector<edge> g[M];
    void work(int u,int v,int c)
    {
    	cnt++;
    	int tmp=c,i=v;
    	while(i!=fa[u])//一直跳父亲 
    	{
    		sum[i]=tmp;
    		tmp+=res[i];
    		i=fa[i];
    	}
    	sum[cnt]=sum[u];
    	i=v;sum[u]=0;
    	while(i!=fa[u])
    	{
    		//环上的最短路 
    		int d=min(sum[i],sum[cnt]-sum[i]);
    		g[i].push_back(edge{cnt,d,0});
    		g[cnt].push_back(edge{i,d,0});
    		i=fa[i];
    	}
    }
    void tarjan(int u)
    {
    	dfn[u]=low[u]=++Ind;
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v,c=e[i].c;
    		if(!dfn[v])
    		{
    			fa[v]=u;//存父亲 
    			res[v]=c;//存父边权值 
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}
    		else if(v!=fa[u])
    			low[u]=min(low[u],dfn[v]);
    		if(low[v]>dfn[u])//如果圆圆点建新边
    		{
    			g[u].push_back(edge{v,c,0});
    			g[v].push_back(edge{u,c,0});
    		}
    	}
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v;
    		if(fa[v]==u || dfn[v]<=dfn[u]) continue;
    		//相当于是找环最底下的那点
    		work(u,v,e[i].c); 
    	}
    }
    void dfs(int u,int fa)
    {
    	p[u][0]=fa;
    	dep[u]=dep[fa]+1;
    	for(int i=1;i<20;i++)
    		p[u][i]=p[p[u][i-1]][i-1];
    	for(int i=0;i<g[u].size();i++)
    	{
    		int v=g[u][i].v,c=g[u][i].c;
    		if(v==fa) continue;
    		dis[v]=dis[u]+c;
    		dfs(v,u);
    	}
    }
    int lca(int u,int v)
    {
    	if(dep[u]<dep[v]) swap(u,v);
    	for(int i=15;i>=0;i--)
    		if(dep[p[u][i]]>=dep[v])
    			u=p[u][i];
    	if(u==v) return u;
    	for(int i=15;i>=0;i--)
    		if(p[u][i]!=p[v][i])
    			u=p[u][i],v=p[v][i];
    	return p[u][0];
    }
    int jump(int x,int y)//找y在x方向的儿子 
    {
    	for(int i=15;i>=0;i--)
    		if(dep[p[x][i]]>dep[y])
    			x=p[x][i];
    	return x; 
    }
    int Abs(int x)
    {
    	return x>0?x:-x;
    }
    signed main()
    {
    	n=cnt=read();m=read();q=read();
    	for(int i=1;i<=m;i++)
    	{
    		int u=read(),v=read(),c=read();
    		e[++tot]=edge{v,c,f[u]},f[u]=tot;
    		e[++tot]=edge{u,c,f[v]},f[v]=tot;
    	}
    	tarjan(1);
    	dfs(1,0);
    	for(int i=1;i<=q;i++)
    	{
    		int u=read(),v=read(),x=lca(u,v);
    		int d=dis[u]+dis[v]-2*dis[x];
    		if(x<=n)//lca是圆点
    			printf("%d
    ",d);
    		else//lca是方点 
    		{
    			int pu=jump(u,x),pv=jump(v,x);
    			d=dis[u]-dis[pu]+dis[v]-dis[pv];
    			int tmp=Abs(sum[pu]-sum[pv]);
    			printf("%d
    ",d+min(tmp,sum[x]-tmp)); 
    		}
    	}
    }
    

    例二

    题目描述

    题目来源:( t BZOJspace 4316)(C) 的独立集。

    给定一个 (n) 个点 (m) 条边的仙人掌,问它的最大独立集。

    (nleq 50000,mleq60000)

    解法

    还是先建出圆方树,设 (dp[u][0/1]) 表示点 (u) 不选(/)选的最大独立集个数,圆点就正常树形 (dp) 即可:

    [dp[u][0]=sum max(dp[v][0],dp[v][1]) ]

    [dp[u][1]=sum dp[v][0]+1 ]

    考虑方点应该怎样转移?就考虑按照正常的树形 (dp) 他会对环的根造成什么样的影响,再根据造成的影响去确定方点 (dp) 状态代表的意义,关键还是设置方点的意义。

    考虑方点 (x) 和环的根 (u)(dp[x][0]) 显然会贡献到 (dp[u][1]) 去,而 (dp[u][1]) 代表的是选取 (u) 的状态,那么如果选了 (u) 这个环那么和 (u) 相邻的点都不能选,所以定义 (dp[x][0]) 为不选该环的两个端点并且不存在环上相邻的点的最大独立集。

    (dp[x][1]) 会贡献到 (dp[u][0]) 去,那么环上的点可以任意选,所以定义 (dp[x][1]) 为不存在环上相邻的点的最大独立集,注意 (dp[x][0/1]) 都不需要考虑环的根。

    不难发现 (dp[x][0/1]) 都可以一次线性 (dp) 求出,时间复杂度 (O(n))

    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <iostream>
    using namespace std;
    const int M = 100005;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,tot,Ind,cnt,f[M],low[M],dfn[M],fa[M];
    vector<int> g[M];int dp[M][2],tmp[M][2];
    struct edge
    {
    	int v,next;
    }e[2*M];
    void work(int u,int v)
    {
    	int i=v;++cnt;
    	while(i!=u)
    	{
    		g[cnt].push_back(i);
    		i=fa[i];
    	}
    	g[u].push_back(cnt);
    }
    void tarjan(int u)
    {
    	dfn[u]=low[u]=++Ind;
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v;
    		if(!dfn[v])
    		{
    			fa[v]=u;
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}
    		else if(v!=fa[u])
    			low[u]=min(low[u],dfn[v]);
    		if(low[v]>dfn[u])
    			g[u].push_back(v);
    	}
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v;
    		if(fa[v]==u || dfn[v]<=dfn[u]) continue;
    		work(u,v); 
    	}
    }
    void fuck(int u)//方点转移
    {
    	//处理dp[u][0],表示不能选和根直接相连的点
    	int len=g[u].size(),v1=g[u][0];
    	tmp[0][0]=dp[v1][0];tmp[0][1]=0;
    	for(int i=1;i<len;i++)
    	{
    		int v=g[u][i];
    		tmp[i][0]=max(tmp[i-1][0],tmp[i-1][1])+dp[v][0];
    		tmp[i][1]=dp[v][1]+tmp[i-1][0];
    	}
    	dp[u][0]=tmp[len-1][0];
    	//处理dp[u][1]表示不包括根的环最多独立点数
    	tmp[0][0]=dp[v1][0];tmp[0][1]=dp[v1][1];
    	for(int i=1;i<len;i++)
    	{
    		int v=g[u][i];
    		tmp[i][0]=max(tmp[i-1][0],tmp[i-1][1])+dp[v][0];
    		tmp[i][1]=dp[v][1]+tmp[i-1][0];
    	}
    	dp[u][1]=max(tmp[len-1][0],tmp[len-1][1]);
    }
    void dfs(int u)
    {
    	if(u<=n) dp[u][1]=1; 
    	for(int i=0;i<g[u].size();i++)
    	{
    		int v=g[u][i];
    		dfs(v);
    		if(u<=n)//圆点转移 
    		{
    			dp[u][1]+=dp[v][0];
    			dp[u][0]+=max(dp[v][0],dp[v][1]);
    		}
    	}
    	if(u>n) fuck(u);
    }
    signed main()
    {
    	n=cnt=read();m=read();
    	for(int i=1;i<=m;i++)
    	{
    		int u=read(),v=read();
    		e[++tot]=edge{v,f[u]},f[u]=tot;
    		e[++tot]=edge{u,f[v]},f[v]=tot;
    	}
    	tarjan(1);
    	dfs(1);
    	printf("%d
    ",max(dp[1][0],dp[1][1]));
    }
    

    例三

    题目描述

    点此看题

    求一个仙人掌的直径(最远两点的距离)

    (1leq nleq 50000,0leq mleq 10000)

    解法

    先建出圆方树,考虑树上的 (dp) 是定义 (dp[u])(u) 往下的最长链然后乱转移下就行了。

    那么怎么套到圆方树上面去呢?这道题的边权可以像例一那样定义,就可以直接像树那样转移了。主要考虑下方点怎么贡献答案的,其实就是在环上找两点 (i<j) 最大化 (dp[i]+dp[j]+dis(i,j)),可以倍长数组来破环成链,对于每个点 (j) 找到 (iin[j-n/2,j)) 使得 (dp[i]-i) 最大即可,这就是一个单调队列问题,总时间复杂度 (O(n))

    这里使用了简便写法,直接把 (dp) 放在建圆方树的时候做了。

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int M = 100005;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,tot,Ind,ans,a[2*M],q[2*M];
    int f[M],dfn[M],low[M],fa[M],dp[M];
    struct edge
    {
    	int v,next;
    }e[2*M];
    void work(int u,int v)
    {
    	int k=0;a[++k]=u;
    	for(int i=v;i!=u;i=fa[i])
    		a[++k]=i;
    	for(int i=1;i<=k;i++)
    		a[k+i]=a[i];
    	int l=1,r=0,len=k*2;
    	for(int i=1;i<=len;i++)
    	{
    		while(l<=r && q[l]<i-k/2) l++;
    		if(l<=r) ans=max(ans,dp[a[i]]+i+dp[a[q[l]]]-q[l]);
    		while(l<=r && dp[a[q[r]]]-q[r]<dp[a[i]]-i) r--;
    		q[++r]=i;
    	}
    	for(int i=2;i<=k;i++)
    		dp[u]=max(dp[u],dp[a[i]]+min(i-1,k-i+1));
    }
    void tarjan(int u)
    {
    	dfn[u]=low[u]=++Ind;
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v;
    		if(!dfn[v])
    		{
    			fa[v]=u;
    			tarjan(v);
    			low[u]=min(low[u],low[v]);
    		}
    		else if(v!=fa[u])
    			low[u]=min(low[u],dfn[v]);
    		if(dfn[u]<low[v])
    		{
    			ans=max(ans,dp[u]+dp[v]+1);
    			dp[u]=max(dp[u],dp[v]+1);
    		}
    	}
    	for(int i=f[u];i;i=e[i].next)
    	{
    		int v=e[i].v;
    		if(u==fa[v] || dfn[v]<=dfn[u]) continue;
    		work(u,v);
    	}
    }
    signed main()
    {
    	n=read();m=read();
    	for(int i=1;i<=m;i++)
    	{
    		int k=read(),u=read();
    		for(int j=2;j<=k;j++)
    		{
    			int v=read();
    			e[++tot]=edge{u,f[v]},f[v]=tot;
    			e[++tot]=edge{v,f[u]},f[u]=tot;
    			u=v;
    		}
    	}
    	tarjan(1);
    	printf("%d
    ",ans);
    }
    
  • 相关阅读:
    Using Apache Maven
    Getting Started(Google Cloud Storage Client Library)
    【iOS】PLA 3.3.12
    【Android】System.exit(0) 退出程序
    【iOS】Ineligible Devices || “无法下载应用程序”
    【iOS】No suitable application records found
    【Android】Failed to convert @drawable/picture into a drawable
    【SVN】eclipse 安装 SVN 插件
    【SVN】SVN Working copy is too old
    【Android】Android sdk content loader 0%
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/14608523.html
Copyright © 2011-2022 走看看