仙人掌&圆方树学习笔记
一、连通分量与 \(\texttt{tarjan}\) 算法
有向图的强连通分量
定义
强连通图:如果有向图中任意两点\(u,v\)互相可达,那么这张图被称为强连通图。
强连通分量:有向图的极大强连通子图被称为强连通分量。
将图中所有强连通分量缩成一个点,得到的图一定是有向无环图( \(\texttt{DAG}\) )。
代码实现
\(\texttt{Tarjan}\) 算法能在 \(\mathcal O(n+m)\) 的时间内求出所有强连通分量。
dfn[u]
表示节点 \(u\) 的时间戳, low[u]
表示从 \(u\) 仅经过返祖边(可以走多步)能回溯到的最小时间戳。
为防止走横叉边,还要额外记录 ins[u]
表示 \(u\) 是否在栈中。
如果 dfn[u]==low[u]
,这意味着从 \(u\) 出发整棵子树已经搜索完毕,并且 \(u\) 是所在强连通分量的编号最小的点。
///P3387
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u),ins[u]=true;
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(ins[v])
low[u]=min(low[u],df[v]);
}
if(dfn[u]==low[u])
{
sum++;
int v;
do v=st.top(),st.pop(),bel[v]=sum,ins[v]=false;
while(v!=u);
}
}
无向图的点双连通分量
定义
点双连通图:如果无向图中任意删去一个点,整张图仍然连通,那么这张图被称为点双连通图。
点双连通分量:无向图的极大点双连通子图被称为点双连通分量。
割点:对于\(u\)所在的连通分支,如果删掉\(u\)后不再连通,那么称\(u\)为割点。
人为规定孤立点不算割点。
割点会属于多个点双连通分量。
节目预告:
- 保留原图中的点作为圆点,对每个点双连通分量新建一个方点,圆点向所在方点连边,可以得到广义圆方树。
代码实现
\(\texttt{Tarjan}\) 算法可以在 \(\mathcal O(n+m)\) 的时间内求出所有割点和点双连通分量。
dfn[u]
表示节点 \(u\) 的时间戳, low[u]
表示从节点 \(u\) 仅经过返祖边(可以走多步)能回溯到的最小时间戳。
先考虑如何判割点。
对于根节点,只需判断它是否有至少两棵子树。
对于其它点 \(u\) ,如果存在一棵子树 \(v\) 满足 low[v]>=dfn[u]
,就意味着删除节点 \(u\) 后,这棵子树和外界不再连通,即 \(u\) 是割点。
找割点本身不需要用到栈,但求点双需要。
///P3388
int dfn[maxn],low[maxn];
bool cut[maxn];
void tarjan(int u)
{
dfn[u]=low[u]=++cnt;
int num=0;
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(u==rt) num++;
else cut[u]|=low[v]>=dfn[u];
}
else low[u]=min(low[u],dfn[v]);
}
if(u==rt&&num>=2) cut[u]=true;
}
再来考虑如何统计点双。
如果存在一棵子树 \(v\) 满足 dfn[v]>=low[u]
,那么 \(v\) 的整棵子树构成一个点双,出栈即可。
实现细节:
根据
low
的定义,指向父节点的边也是返祖边,因此遍历出边时不需要单独过滤掉这条边。由于无向图没有横叉边,所以不需要记录每个点是否在栈中,直接更新
low
即可(前向边一定不优,因此只有返祖边会产生贡献)。弹栈时弹到 \(v\) 结束,但 \(u\) 也属于这个点双,将 \(u\) 单独加入即可。
///P8435
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u);
if(g[u].empty()) return vec[++num].push_back(u);///特判孤立点
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
int p;
vec[++num].push_back(u);
do p=st.top(),st.pop(),vec[num].push_back(p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
无向图的边双连通分量
定义
边双连通图:如果无向图中任意删去一个点,整张图仍然连通,那么这张图被称为边双连通图。
边双连通分量:无向图的极大边双连通子图被称为边双连通分量。
桥:对于一条边所在的连通分支,如果将其删掉后不再连通,那么称这条边为桥。
边双缩点以后也会得到一棵树,不过没有专业名称。
代码实现
\(\texttt{Tarjan}\) 算法可以在 \(\mathcal O(n+m)\) 的时间内求出所有桥和边双连通分量。
dfn[u]
表示节点 \(u\) 的时间戳, low[u]
表示从 \(u\) 出发仅经过非 \(dfs\) 树边能到达的最小时间戳。
先考虑如何判桥。
如果 dfn[v]>low[u]
,说明点 \(v\) 仅经过非树边到不了 \(u\),即边 \((u,v)\) 为桥。
和判定割点类似,求桥边不需要用到栈,但求边双需要。
///没有已知来源,随手写一个放在这里
void tarjan(int u,int from)
{
dfn[u]=low[u]=++cnt;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(i==(from^1)) continue;
if(!dfn[v])
{
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) bri[i]=bri[i^1]=true;
}
else low[u]=min(low[u],dfn[v]);
}
}
求点双可以类比强连通分量。由于限制不能走回边,因此如果将搜索过程看成有向图,原本的边双连通分量就会变成强连通分量。
有重边的情况下只能用边的编号来判重,这意味着链式前向星存图更为方便。
但如果题目保证没有重边,用点判重是一个不错的选择。
///P8436
void tarjan(int u,int from)
{
dfn[u]=low[u]=++cnt,st.push(u);
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(i==(from^1)) continue;
if(!dfn[v])
{
tarjan(v,i);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
num++;
int v;
do v=st.top(),st.pop(),vec[num].push_back(v);
while(v!=u);
}
}
概念辨析:
- 每次并上一个有公共边的环,可以得到点双连通分量。
- 每次并上一个有公共点的环,可以得到边双连通分量。
二、广义圆方树
定义和性质
广义圆方树:对于一张无向图,将图中的点称为圆点,每个点双新建一个方点,并向点双中对应的圆点连边。
圆方树的性质:
- 圆方树的每条边连接一个圆点和一个方点。
- 圆方树每个圆点的度数为所属的点双个数(注意只有割点会属于多个点双),每个方点的度数为对应点双中的点数。
- 原图 \(u\to v\) 所有简单路径的交为圆方树 \(u\to v\) 路径上的所有圆点。
- 原图 \(u\to v\) 所有简单路径的并为圆方树 \(u\to v\) 路径上所有方点对应的圆点。
温馨提示:
- 无论是广义还是狭义,如果对原图做 \(\texttt{tarjan}\) 和对圆方树(森林)做 \(dfs\) 的根节点相同,可以仅保留单向边。
代码实现
在执行 \(\texttt{tarjan}\) 算法过程中连边即可。
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u);
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{///low[v]>=dfn[u]说明找到一个新的点双,弹栈过程中连边即可
///若后续对圆方树dfs的根节点与此相同,则addedge只需从前者连边向后者
tree::addedge(u,++num);
static int p=0;
do p=st.top(),st.pop(),tree::addedge(num,p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
///初始化num=n
///编号<=n的点为圆点,编号>n的点为方点
三、狭义圆方树
仙人掌
定义:任意一条边至多在一个简单环中出现的无向连通图,称为仙人掌。
一个点可以同时在多个环中出现。
先解决一个对拍中会遇到的问题:如何随机生成一个仙人掌?
其实很简单,给一棵树随机剖分,选若干点向链顶连边即可。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=2e5+5;
int m,n;
int fa[maxn],top[maxn],vis[maxn];
pii p[maxn];
vector<int> g[maxn];
mt19937 rnd(random_device{}());
void dfs(int u,int topf)
{
top[u]=topf;
shuffle(g[u].begin(),g[u].end(),rnd);
if(g[u].size()) dfs(g[u][0],topf);
for(int i=1;i<g[u].size();i++) dfs(g[u][i],g[u][i]);
}
int main()
{
freopen("data.in","w",stdout);
n=10;
for(int i=2;i<=n;i++)
{
int x=rnd()%(i-1)+1;
p[++m]=mp(x,i),fa[i]=x,g[x].push_back(i);
}
dfs(1,1);
for(int i=1;i<=5;i++)///5为环数上限,读者可自行修改
{
int x=rnd()%n+1,y=top[x];
if(x!=y&&fa[x]!=y&&!vis[y]) p[++m]=mp(x,y),vis[y]=true;
}
printf("%d %d\n",n,m);
for(int i=1;i<=m;i++) printf("%d %d\n",p[i].fi,p[i].se);
return 0;
}
定义和性质
定义:对于一棵仙人掌,对每个环新建一个方点,环上所有圆点向这个方点连边,不在环上的边保留,得到的树被称为狭义圆方树。
温馨提示:
- 狭义圆方树存在圆点和圆点之间的边,这也是和广义圆方树的最大区别。
代码实现
狭义圆方树有点双建图和边双建图两种写法,其中边双建图较为常见。
这里两种做法分别给出代码实现:
///点双建图,可以处理二元环
void tarjan(int u,int from)
{///为防止重边的影响,使用前向星存图并用边的编号去重
dfn[u]=low[u]=++cnt,st.push(u);
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(i==(from^1)) continue;
if(!dfn[v])
{
tarjan(v,i);
low[u]=min(low[u],l[v]);
/**low[v]只有三种可能情况:
low[v]=dfn[v],这说明u->v为树边,应当在圆方树上保留
low[v]=dfn[u],这说明u为环上最浅的点,v为第一次入环走到的位置
low[v]<dfn[u],这说明u->v是环边,但不是最浅边,什么都不用做**/
if(low[v]==dfn[v]) tree::g[u].push_back(v),st.pop();///弹栈前栈顶一定为v
else if(low[v]==dfn[u])
{
tree::g[u].push_back(++num);
static int p=0;
do p=st.top(),st.pop(),tree::g[num].push_back(p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
///边双建图,一般不可以处理二元环
void tarjan(int u,int f)
{
dfn[u]=low[u]=++cnt;
for(auto v:g[u])
{
if(v==f) continue;///如果fa[v]<=u也过滤掉,那么可以处理二元环
if(!dfn[v])
{
fa[v]=u,tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
if(low[v]>dfn[u]) tree::g[u].push_back(v);///保留圆点之间的边
}
for(auto v:g[u])
{
if(fa[v]==u||dfn[v]<=dfn[u]) continue;
///对于一个环,u的出边v有两个,从u出发和转一圈回到u
///我们保留的是后者,这样沿着fa回退就能找到环上的所有点
///不能处理二元环是因为此时两个出边相同,被过滤掉了
tree::g[u].push_back(++num);
for(int i=v;i!=u;i=fa[i]) tree::g[num].push_back(i);
}
}
温馨提示:
- 由于圆方树的点数上限为 \(2n\) ,所以数组和多测清空都需要开两倍。
那么边双写法有没有办法处理二元环呢?
答案是有的,将第 \(7\) 行改成 if(v==f||fa[v]==u) continue;
即可。
这样我们不会从一个点出发,连续两次访问同一个点,从而达到将二元环改成圆圆边的目的。
四、相关例题
例1、\(\texttt{P4630 [APIO2018] 铁人两项}\)
题目描述
给定一张 \(n\) 个点, \(m\) 条边的无向图,求有多少不同的三元组 \((s,c,f)\) ,满足 \(s,c,f\) 两两不同,且存在 \(s\to c\to f\) 的路径。
数据范围
- \(1\le n\le 10^5,1\le m\le 2\cdot 10^5\) 。
时间限制 \(\texttt{1s}\),空间限制 \(\texttt{256MB}\)。
分析
对于固定的 \((s,f)\) ,不同的 \(c\) 的数量为 \(s\to f\) 所有路径并的大小减 \(2\)。
对应到圆方树上, \(c\) 能在 \(s\to f\) 路径上所有方点对应圆点的并集中任取(\(s,f\)除外)。
考虑容斥,令方点权值为相邻圆点个数,圆点权值为 \(-1\) ,则 \(c\) 的个数刚好为 \(s\to f\) 路径上点权之和。
问题转化为,在圆方树上对所有圆点二元组 \((s,f)\) ,求 \(s\to f\) 的路径点权和,树形 \(\texttt{dp}\) 维护子树权值和即可。
注意路径无向并且需要对每个连通块分别计算。
时间复杂度 \(\mathcal O(n+m)\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int m,n,u,v,cnt,num;
int dfn[maxn],low[maxn];
stack<int> st;
vector<int> g[maxn];
namespace tree
{
long long res;
int sz[maxn];
vector<int> g[maxn];
void addedge(int u,int v)
{
g[u].push_back(v),g[v].push_back(u);
}
void dfs(int u,int fa)
{
int w=u<=n?-1:g[u].size();
sz[u]=u<=n;
for(auto v:g[u])
{
if(v==fa) continue;
dfs(v,u),res+=2ll*sz[u]*sz[v]*w,sz[u]+=sz[v];
}
res+=2ll*sz[u]*(cnt-sz[u])*w;
}
}
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u);
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
tree::addedge(u,++num);
int p;
do p=st.top(),st.pop(),tree::addedge(num,p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main()
{
scanf("%d%d",&n,&m),num=n;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
for(int i=1;i<=n;i++) if(!dfn[i]) cnt=0,tarjan(i),tree::dfs(i,0);
printf("%lld\n",tree::res);
return 0;
}
例2、\(\texttt{P4606 [SDOI2018]战略游戏}\)
题目描述
\(T\) 组数据,给定一张 \(n\) 个点, \(m\) 条边的无向连通图。
\(q\) 次询问,每个给出一个关键点集合 \(S\) ,询问有几个非关键点满足,删掉它后存在两个关键点不连通。
数据范围
- \(1\le T\le 10\) 。
- \(2\le n\le 10^5,n-1\le m\le 2\cdot 10^5,1\le q\le 10^5\) 。
- 对于每组数据, \(1\le\sum|S|\le 2\cdot 10^5\) 。
时间限制 \(\texttt{10s}\) ,空间限制 \(\texttt{512MB}\) 。
分析
先考虑怎样的非关键点符合要求。
根据圆方树的性质,它一定在圆方树两个关键点的简单路径上。
因此这样的点的个数为关键点构成的极大连通块中圆点个数减去 \(|S|\) 。
记圆点点权为 \(1\) ,方点点权为 \(0\) ,将点权挂到它连向父节点的边上。
将关键点按照 \(dfs\) 序排序,将相邻两点(包括首尾)之间的距离。
这样每条边的贡献恰好被计算两次,除以二后再单独统计一下根节点(第一个点和最后一个点的 \(\texttt{lca}\) )的贡献即可。
时间复杂度 \(\mathcal O\big(T(n\log n+m+|S|\log|S|)\big)\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int m,n,q,t,cnt,num;
int dfn[maxn],low[maxn];
stack<int> st;
vector<int> g[maxn];
inline int read()
{
int q=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) q=10*q+ch-'0',ch=getchar();
return q;
}
namespace tree
{
int cnt;
int d[maxn],fa[maxn][18];
int dfn[maxn],val[maxn];
int a[maxn];
vector<int> g[maxn];
void dfs(int u)
{
dfn[u]=++cnt;
for(auto v:g[u])
{
d[v]=d[u]+1,fa[v][0]=u,val[v]=val[u]+(v<=n);
for(int i=1;i<=17;i++) fa[v][i]=fa[fa[v][i-1]][i-1];
dfs(v);
}
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int i=17;i>=0;i--) if(d[fa[u][i]]>=d[v]) u=fa[u][i];
if(u==v) return u;
for(int i=17;i>=0;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
int getdis(int u,int v)
{
return val[u]+val[v]-2*val[lca(u,v)];
}
void work()
{
d[1]=1,cnt=0,dfs(1);
for(q=read();q--;)
{
int k=read(),res=0;
for(int i=1;i<=k;i++) scanf("%d",&a[i]);
sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
for(int i=1;i<=k;i++) res+=getdis(a[i],a[i%k+1]);
printf("%d\n",res/2+(lca(a[1],a[k])<=n)-k);
}
for(int i=1;i<=num;i++) g[i].clear();
}
}
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u);
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
tree::g[u].push_back(++num);
int p;
do p=st.top(),st.pop(),tree::g[num].push_back(p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main()
{
for(t=read();t--;)
{
n=read(),m=read(),num=n;
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
g[u].push_back(v),g[v].push_back(u);
}
tarjan(1),tree::work();
cnt=tree::cnt=0;
for(int i=1;i<=n;i++) g[i].clear(),dfn[i]=low[i]=0;
}
return 0;
}
例3、\(\texttt{CF487E Tourists}\)
题目描述
给定一张 \(n\) 个点, \(m\) 条边的无向连通图,点有点权 \(w_i\) 。
接下来 \(q\) 次操作:
C u v
:将 \(w_u\) 修改为 \(v\) 。A u v
:询问 \(u\to v\) 的所有路径并集中的最小点权。
数据范围
- \(1\le n,m,q\le 10^5\) 。
- \(1\le w_i\le 10^9\) 。
时间限制 \(\texttt{2s}\),空间限制 \(\texttt{256MB}\) 。
分析
建出圆方树,询问即为求 \(u\to v\) 路径上所有方点的周围圆点的最小权值。
一个很简单的想法是,令圆点权值为 \(w_i\) ,方点权值为点双中的最小权值,那么只需要树剖求链 \(\min\) 。
但这样修改时会波及到很多方点,导致复杂度退化。
考虑修改一下方点权值的定义:所有子节点(一定是圆点)中的点权最小值。
询问时对于大多数方点,由于父节点也在链中,所以不会漏掉信息;唯一的例外是当 lca(u,v)
为方点时,需要单独统计它的父节点的权值。
这样修改只需要更新圆点 \(u\) 及其父节点 \(fa_u\) 的信息,用 multiset
维护方点即可。
时间复杂度 \(\mathcal O(n+m+q\log^2n)\)。
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=2e5+5,inf=1e9;
int m,n,q,u,v,cnt,num;
int dfn[maxn],low[maxn];
char ch[2];
stack<int> st;
vector<int> g[maxn];
namespace tree
{
int cnt;
int d[maxn],fa[maxn],sz[maxn],son[maxn];
int w[maxn],dfn[maxn],pos[maxn],top[maxn];
vector<int> g[maxn];
multiset<int> s[maxn];
struct node
{
int l,r,mn;
}f[4*maxn];
void dfs1(int u)
{
sz[u]=1;
for(auto v:g[u])
{
d[v]=d[u]+1,fa[v]=u,dfs1(v),sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,pos[cnt]=u,top[u]=topf;
if(son[u]) dfs2(son[u],topf);
for(auto v:g[u]) if(v!=son[u]) dfs2(v,v);
}
void pushup(int p)
{
f[p].mn=min(f[ls].mn,f[rs].mn);
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r) return f[p].mn=w[pos[l]],void();
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos,int val)
{
if(f[p].l==f[p].r) return f[p].mn=val,void();
int mid=(f[p].l+f[p].r)>>1;
modify(pos<=mid?ls:rs,pos,val);
pushup(p);
}
int query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].mn;
if(l>f[p].r||r<f[p].l) return inf;
return min(query(ls,l,r),query(rs,l,r));
}
void solve()
{
d[1]=1,dfs1(1),dfs2(1,1);
for(int i=1;i<=n;i++) s[fa[i]].insert(w[i]);
for(int i=n+1;i<=num;i++) w[i]=*s[i].begin();
build(1,1,num);
while(q--)
{
scanf("%s%d%d",ch,&u,&v);
if(ch[0]=='C')
{
if(fa[u])
{
s[fa[u]].erase(s[fa[u]].find(w[u]));
s[fa[u]].insert(v);
modify(1,dfn[fa[u]],*s[fa[u]].begin());
}
modify(1,dfn[u],w[u]=v);
}
else
{
int res=inf;
while(top[u]!=top[v])
{
if(d[top[u]]<d[top[v]]) swap(u,v);
res=min(res,query(1,dfn[top[u]],dfn[u])),u=fa[top[u]];
}
if(dfn[u]>dfn[v]) swap(u,v);
res=min(res,query(1,dfn[u],dfn[v]));
if(u>n) res=min(res,w[fa[u]]);
printf("%d\n",res);
}
}
}
}
void tarjan(int u)
{
dfn[u]=low[u]=++cnt,st.push(u);
for(auto v:g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
tree::g[u].push_back(++num);
int p;
do p=st.top(),st.pop(),tree::g[num].push_back(p);
while(p!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main()
{
scanf("%d%d%d",&n,&m,&q),num=n;
for(int i=1;i<=n;i++) scanf("%d",&tree::w[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
tarjan(1),tree::solve();
return 0;
}
例4、\(\texttt{P5236 【模板】静态仙人掌}\)
题目描述
给定一张 \(n\) 个点, \(m\) 条边的仙人掌,边有边权。
\(q\) 次询问,每次给定阶段 \(u,v\),求两点之间的最短路。
数据范围
- \(1\le n,q\le 10^4,1\le m\le 2\cdot 10^4\) 。
- \(1\le w\le 10^5\) 。
时间限制 \(\texttt{300ms}\),空间限制 \(\texttt{125MB}\) 。
分析
显然要建圆方树,我们希望圆方树中的边权能代表原图中的最短路。
圆点和圆点之间的边可以保留原来的边权,关键在于圆点和方点之间的边权含义。
对于一个环,记方点的父节点为链头,令方点到链头的边权为零。
对于点双中的其余所有点,令圆方树上它到方点的距离为原图中它到链头的最短路,也就是两边环长的最小值。
这样做的好处是,从任意一个点走到链头,圆方树和原图最短路长度相等。
再来考虑如何处理询问,记 lca(u,v)=p
。
如果 \(p\) 为圆点,答案为圆方树上 \(u,v\) 两点距离。
如果 \(p\) 为方点,最浅的一个环并不一定经过链头,需要特殊处理。
具体的,记 \(x,y\) 分别为 \(p\) 在 \(u,v\) 方向的子节点,可以在倍增 lca
过程中顺便求出。
\(u\to x,y\to v\) 两段路径可以直接计入答案,对于节点 \(p\) 所代表的环,我们需要计算 \(x\to y\) 的最短距离。
记录单方向每个点到链头的距离和,以及每个环的总长度,我们钦定环的方向为 \(u\to v\to fa_v\to\cdots\to u\) 。
注意链头自身记录的值不为零,因为它属于另一个环。
时间复杂度 \(\mathcal O((n+q)\log n+m)\) 。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=2e4+5;
int m,n,q,u,v,w,x,y,cnt,num;
int dfn[maxn],low[maxn];
pii fa[maxn];
vector<pii> g[maxn];
namespace tree
{
int dep[maxn],dis[maxn],fa[maxn][15];
int s[maxn];///对于圆点u,s[u]表示u到链头的距离前缀和;对于方点u,s[u]表示总环长
vector<pii> g[maxn];
void addedge(int u,int v,int w)
{
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
void dfs(int u,int f)
{
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==f) continue;
dep[v]=dep[u]+1,dis[v]=dis[u]+w,fa[v][0]=u;
for(int i=1;i<=14;i++) fa[v][i]=fa[fa[v][i-1]][i-1];
dfs(v,u);
}
}
int lca(int u,int v)
{
if(dep[u]<dep[v]) swap(u,v);///我们只关心x,y是什么,不关心谁对应谁
for(int i=14;i>=0;i--) if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
if(u==v) return u;
for(int i=14;i>=0;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
x=u,y=v;
return fa[u][0];
}
void solve()
{
dep[1]=1,dfs(1,0);
while(q--)
{
scanf("%d%d",&u,&v);
int p=lca(u,v);
if(p<=n) printf("%d\n",dis[u]+dis[v]-2*dis[p]);
else
{
int dl=dis[u]-dis[x],dr=dis[v]-dis[y];
int cur=abs(s[x]-s[y]),dm=min(cur,s[p]-cur);
printf("%d\n",dl+dm+dr);
}
}
}
}
using tree::s;
void tarjan(int u,int f)
{
dfn[u]=low[u]=++cnt;
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(v==f) continue;
if(!dfn[v])
{
fa[v]=mp(u,w),tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
if(low[v]>dfn[u]) tree::addedge(u,v,w);
}
for(auto p:g[u])
{
int v=p.fi,w=p.se;
if(fa[v].fi==u||dfn[v]<=dfn[u]) continue;
///钦定环的方向为u->v->fa[v]->...->u
///tarjan算法会由深到浅构建每个环,后面s[u]会被覆盖
for(int i=v,cur=w;i!=fa[u].fi;i=fa[i].fi) s[i]=cur,cur+=fa[i].se;
s[++num]=s[u];
for(int i=v,cur=w;i!=fa[u].fi;i=fa[i].fi) tree::addedge(num,i,min(s[i],s[num]-s[i]));
}
}
int main()
{
scanf("%d%d%d",&n,&m,&q),num=n;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
}
tarjan(1,0),tree::solve();
return 0;
}
例5、\(\texttt{P4244 [SHOI2008]仙人掌图 II}\)
题目描述
给定一张 \(n\) 个点,\(m\) 条边的仙人掌,求仙人掌的直径。
直径定义为 \(\max\limits_{1\le u\lt v\le n}dis(u,v)\) ,其中 \(dis(u,v)\) 表示 \(u,v\) 间的最短距离。
数据范围
- \(1\le n\le 5\cdot 10^4,1\le m\le 10^5\)。
时间限制 \(\texttt{1s}\),空间限制 \(\texttt{125MB}\) 。
分析
在圆方树上树形 \(\texttt{dp}\) ,令 \(f_u\) 表示 \(u\) 子树内的点到 \(u\) 的最大距离。
连接圆点和圆点之间的边可以直接转移, \(ans\gets f_u+f_v+w,f_u\gets f_v\) 。
方点的 \(\texttt{dp}\) 值无实际意义,对于一个环上的点 \(i,j\) (不妨 \(i\) 在环上的编号小于 \(j\)),令 \(s_i\) 为 \(i\) 到链头的距离。
转移方程 \(ans\gets f_i+f_j+\min\big(s_j-s_i,len-(s_j-s_i)\big)\) ,最后拿所有子节点的 \(\texttt{dp}\) 值更新链头即可。
考虑如何实现上述转移,先破环为链,将环上的点复制一份加在末尾。在环上距离 \(\le\frac{len}2\) 时且扫描到 \(j\) 时统计点对 \((i,j)\) 的贡献,此时 \(ans\gets (f_i-s_i)+(f_j+s_j)\) 。
满足条件的 \(i\) 是一段后缀,单调队列维护 \(f_i-s_i\) 的最小值即可。
时间复杂度 \(\mathcal O(n+m)\) ,此做法同样适用于有边权的情况。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int k,m,n,cnt,num,res;
int a[maxn],fa[maxn];
int dfn[maxn],low[maxn];
vector<int> g[maxn];
namespace tree
{
int f[maxn];
vector<int> g[maxn];
void addedge(int u,int v)
{
g[u].push_back(v),g[v].push_back(u);
}
inline void chmax(int &x,int y)
{
if(x<=y) x=y;
}
void dfs(int u,int fa)
{
for(auto v:g[u])
{
if(v==fa) continue;
if(v<=n)
{
dfs(v,u);
chmax(res,f[u]+f[v]+1),chmax(f[u],f[v]+1);
}
else
{
int l=g[v].size();
for(int i=1;i<l;i++) dfs(g[v][i],v);
vector<int> q(2*l),vec(2*l);
for(int i=0;i<l;i++) vec[i]=vec[l+i]=f[g[v][i]];
int h=0,t=-1;
for(int i=0;i<2*l;i++)
{
while(h<=t&&q[h]<i-l/2) h++;
if(h<=t) chmax(res,vec[q[h]]-q[h]+vec[i]+i);
while(h<=t&&vec[q[t]]-q[t]<=vec[i]-i) t--;
q[++t]=i;
}
for(int i=1;i<l;i++) chmax(f[u],f[g[v][i]]+min(i,l-i));
}
}
}
}
void addedge(int u,int v)
{
g[u].push_back(v),g[v].push_back(u);
}
void tarjan(int u,int f)
{
dfn[u]=low[u]=++cnt;
for(auto v:g[u])
{
if(v==f) continue;
if(!dfn[v])
{
fa[v]=u,tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
if(low[v]>dfn[u]) tree::addedge(u,v);
}
for(auto v:g[u])
{
if(fa[v]==u||dfn[v]<=dfn[u]) continue;
tree::addedge(u,++num);
for(int i=v;i!=u;i=fa[i]) tree::addedge(num,i);
}
}
int main()
{
scanf("%d%d",&n,&m),num=n;
while(m--)
{
scanf("%d",&k);
for(int i=1;i<=k;i++) scanf("%d",&a[i]);
for(int i=1;i<k;i++) addedge(a[i],a[i+1]);
}
tarjan(1,0),tree::dfs(1,0);
printf("%d\n",res);
return 0;
}
例6、\(\texttt{P3180 [HAOI2016] 地图}\)
题目描述
给定一棵 \(n\) 个点, \(m\) 条边的仙人掌,点权 \(w_i\) 。
\(q\) 次询问,求在封死 \(1\to x\) 的所有简单路径的前提下,所有 \(x\) 能走到的点中,\([1,y]\) 中作为点权出现次数奇偶性为 \(z\) 的数有多少个。
数据范围
- \(1\le n,q\le 10^5,1\le m\le 1.5\cdot 10^5\) 。
- \(1\le w_i\le 10^6\) 。
- \(1\le x\le n,0\le y\le 10^6,0\le z\le 1\) 。
时间限制 \(\texttt{1s}\),空间限制 \(\texttt{125MB}\)。
分析
以 \(1\) 号点为根, \(x\) 能走到的点就是圆方树上 \(x\) 的整棵子树。
用 \(dfs\) 序将子树拍成区间,将子树限制转化为区间限制。
看到出现次数就很难 \(\texttt{polylog}\) 。莫队求出对每组询问、每个点权的出现次数,数据结构分别维护出现次数为奇数和偶数的数,查询就是求前缀和。
有 \(\mathcal O(n\sqrt q)\) 次单点修改的操作和 \(\mathcal O(q)\) 次查询前缀和操作,用 \(\mathcal O(1)-\mathcal O(\sqrt v)\) 的分块来平衡。
时间复杂度 \(\mathcal O(n\sqrt q+m+q\sqrt v)\) 。
人为规定方点权值为零,注意权值为零的点不能加入贡献。
记得特判 \(y=0\) 的询问,此时答案为零。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5,maxv=1e6+5,B=900;
int m,n,q,cnt,num;
int w[maxn],fa[maxn],bel[maxv];
int dfn[maxn],low[maxn];
vector<int> g[maxn];
struct quer
{
int l,r,y,z,id;
}f[maxn];
bool cmp(quer a,quer b)
{
if(bel[a.l]!=bel[b.l]) return bel[a.l]<bel[b.l];
return bel[a.l]&1?a.l<b.l:a.r>b.r;
}
struct block
{
int a[maxv],sum[maxv];
inline void add(int x,int v)
{
a[x]+=v,sum[bel[x]]+=v;
}
inline int query(int x)
{
if(!x) return 0;
int res=0;
for(int i=1;i<bel[x];i++) res+=sum[i];
for(int i=B*(bel[x]-1)+1;i<=x;i++) res+=a[i];
return res;
}
}t[2];
namespace tree
{
int cnt;
int sz[maxn],dfn[maxn];
int nw[maxn],num[maxv],res[maxn];
vector<int> g[maxn];
void dfs(int u)
{
dfn[u]=++cnt,nw[cnt]=w[u],sz[u]=1;
for(auto v:g[u]) dfs(v),sz[u]+=sz[v];
}
inline void add(int x,int v)
{
if(num[x]) t[num[x]&1].add(x,-1);
if(num[x]+=v) t[num[x]&1].add(x,1);
}
void solve()
{
dfs(1),scanf("%d",&q);
for(int i=1,x=0,y=0,z=0;i<=q;i++)
{
scanf("%d%d%d",&z,&x,&y);
f[i]={dfn[x],dfn[x]+sz[x]-1,y,z,i};
}
for(int i=1;i<=maxv-5;i++) bel[i]=(i-1)/B+1;
sort(f+1,f+q+1,cmp);
for(int i=1,l=1,r=0;i<=q;i++)
{
while(l>f[i].l) add(nw[--l],1);
while(r<f[i].r) add(nw[++r],1);
while(l<f[i].l) add(nw[l++],-1);
while(r>f[i].r) add(nw[r--],-1);
res[f[i].id]=t[f[i].z].query(f[i].y);
}
for(int i=1;i<=q;i++) printf("%d\n",res[i]);
}
}
void tarjan(int u,int f)
{
dfn[u]=low[u]=++cnt;
for(auto v:g[u])
{
if(v==f||fa[v]==u) continue;
if(!dfn[v])
{
fa[v]=u,tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else low[u]=min(low[u],dfn[v]);
if(low[v]>dfn[u]) tree::g[u].push_back(v);
}
for(auto v:g[u])
{
if(fa[v]==u||dfn[v]<=dfn[u]) continue;
tree::g[u].push_back(++num);
for(int i=v;i!=u;i=fa[i]) tree::g[num].push_back(i);
}
}
int main()
{
scanf("%d%d",&n,&m),num=n;
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1,u=0,v=0;i<=m;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
tarjan(1,0),tree::solve();
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17486591.html