[学习笔记] 圆方树

前言

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

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

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

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

构造

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

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

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

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

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

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

  • 方点不会和方点相连

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

例一

题目描述

来源:\(\tt bzoj\space 2125\) 最短路,点此看题

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

\(1\leq n,q\leq 10000\)

解法

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

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

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

时间复杂度 \(O(n\log 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\n",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\n",d+min(tmp,sum[x]-tmp)); 
		}
	}
}

例二

题目描述

题目来源:\(\tt BZOJ\space 4316\)\(C\) 的独立集。

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

\(n\leq 50000,m\leq60000\)

解法

还是先建出圆方树,设 \(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\n",max(dp[1][0],dp[1][1]));
}

例三

题目描述

点此看题

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

\(1\leq n\leq 50000,0\leq m\leq 10000\)

解法

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

那么怎么套到圆方树上面去呢?这道题的边权可以像例一那样定义,就可以直接像树那样转移了。主要考虑下方点怎么贡献答案的,其实就是在环上找两点 \(i<j\) 最大化 \(dp[i]+dp[j]+dis(i,j)\),可以倍长数组来破环成链,对于每个点 \(j\) 找到 \(i\in[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\n",ans);
}
posted @ 2021-04-01 22:21  C202044zxy  阅读(113)  评论(0编辑  收藏  举报