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);
    }
    
  • 相关阅读:
    MKMapVIew学习系列2 在地图上绘制出你运行的轨迹
    WPF SDK研究 Intro(6) WordGame1
    WPF SDK研究 Intro(3) QuickStart3
    WPF SDK研究 Layout(1) Grid
    WPF SDK研究 目录 前言
    WPF SDK研究 Intro(7) WordGame2
    WPF SDK研究 Layout(2) GridComplex
    对vs2005创建的WPF模板分析
    WPF SDK研究 Intro(4) QuickStart4
    《Programming WPF》翻译 第6章 资源
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/14608523.html
Copyright © 2011-2022 走看看