[学习笔记] 圆方树

前言

( 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);
}
原文地址:https://www.cnblogs.com/C202044zxy/p/14608523.html