仙人掌&圆方树学习笔记

一、连通分量与 \(\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;
}
posted @ 2023-06-16 21:51  peiwenjun  阅读(0)  评论(0编辑  收藏  举报