[学习笔记] 圆方树
前言
\(\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\) 他会对环的根造成什么样的影响,再根据造成的影响去确定方点 \(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);
}