圆方树 学习笔记

前言

  • \(\mathcal{zjh}\) 好强拜谢 \(\mathcal{zjh}\)

圆方树

  • 前置芝士:塔尖

引入

  • 圆方树,顾名思义是圆方的树
  • 那我们为啥用圆方树
  • 因为对于一张图,我们除了最短路和流(msjing没学过流)很难统计图上信息,但是我们发现同样属于图的树就没有这方面问题,树上可以做 \(DP\),可以做重剖,可以差分可以求 \(LCA\),但是图不行
  • 所以我们考虑将图变成树
  • 对于一个点双联通分量,我们可以通过缩点操作将这个点双变成一个点,但是这样会有个弊端,就是原来的点双的割点会消失,举个例子

graph(5)graph(6)

  • 前面的是原图,后面的是缩点,你会发现原来的割点消失了
  • 所以我们要保留这个割点,还要使图变成树,圆方树就诞生了

构建

  • 在联通图里考虑,不连通会构建出森林
  • 圆方树的构建基于 \(tarjan\) 求割点,对于一个点双,我们新建一个节点,称为方点,原图中的点称为圆点,将方点与这个点双所有点连边,会构成一棵树,对于每个点双都这么做,就构建出了圆方树
  • 画张图

2026-09-05 11-22-02屏幕截图

  • 黑色是原图,蓝色是方点,紫色是圆方树的边
  • 这样圆方树就构建好了
点击查看代码
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
constexpr int maxn=1e5+10;
int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || ch>'9')
    {
        if (ch == '-') f=-1;
        ch=getchar();
    }
    while (ch>='0' && ch<='9')
    {
        x=(x<<1)+(x<<3)+ch-'0';
        ch=getchar();
    }
    return x*f;
}
int n,m;
int h[maxn],to[maxn],nxt[maxn],tot;
void addedge(int x,int y)
{
    tot++;
    to[tot]=y;
    nxt[tot]=h[x];
    h[x]=tot;
}
int dfn[maxn],low[maxn],num;
int root;
int cut[maxn];
int stk[maxn],top,cnt;
vector<int> dcc[maxn];
vector<int> e[maxn];
void tarjan(int x)
{
    dfn[x]=low[x]=++num;
    stk[++top]=x;
    int f=0;
    for (int i=h[x];i;i=nxt[i])
    {
        int y=to[i];
        if (!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if (dfn[x]<=low[y])
            {
                f++;
                if (x!=root || f>1) cut[x]=1;
                cnt++;
                int z=0;
                do
                {
                    z=stk[top--];
                    dcc[cnt].pb(z);
                }while (z!=y);
                dcc[cnt].pb(x);
            }
        }
        else
            low[x]=min(low[x],dfn[y]);
    }
}
void chk(int x,int fa)
{
    for (int y:e[x])
    {
        if (y == fa) continue;
        cout << x << "->" << y << endl;
        chk(y,x);
    }
}
int main()
{
    n=read(),m=read();
    for (int i=1;i<=m;i++)
    {
        int x=read(),y=read();
        addedge(x,y);
        addedge(y,x);
    }
    for (int i=1;i<=n;i++)
        if (!dfn[i])
            root=i,tarjan(i);
    int ot=n;
    for (int i=1;i<=cnt;i++)
    {
        ot++;
        for (int j:dcc[i])
        {
            e[ot].pb(j);
            e[j].pb(ot);
        }
    }
    chk(1,0);
    return 0;
}
  • 接下来我们结合例题讲解应用

例题

P7353 [2020-2021 集训队作业] Tom & Jerry

OIP-C

\(\mathcal{zjh}\):该降紫的

  • upd on 9.7 21:00:上面的是在 9.5 写的,断断续续写了三天,用时 \(2h45min\) 完成
  • 有两个做法,msjing觉得思路差不多
  • 先讲一个思路好像不好写的
  • 前置芝士:换根 \(DP\)
  • \(\mathcal{solution\>by\>zjh}\)
  • 没写代码所以纯口胡
  • 首先我们考虑如何汤姆才能抓到杰瑞
  • 汤姆和杰瑞都绝顶聪明,都会以最有决策移动
  • 我们发现杰瑞先动,可以向全图跑,所以汤姆想抓到杰瑞需要尽可能缩小杰瑞的移动范围,我们发现当且仅当汤姆站在割点上时,杰瑞无法到另一边

2026-09-07 21-17-12屏幕截图

  • 那这样汤姆每次一步步缩小杰瑞的移动范围,直到抓到

  • 由于图上不好做答案统计,所以在圆方树上做

  • 我们发现每次缩小范围都是将问题减小,在圆方树上就是不断向下找子树,这显然能做 \(DP\)

  • 所以我们将 \(DP\) 值只赋 \(0/1\),表示在这棵子树内能否抓到,不断向上转移,查询换根,进行超级大分讨(下面说)

  • 没写代码

  • 然后是一个听起来比较好写但是并非的

  • 前置芝士:滚木

  • \(\mathcal{solution\>by\>lzk}\)

  • 我们这次考虑杰瑞如何不让汤姆抓到

  • 当一个 \(dcc\) 中有一个点没有和这个 \(dcc\) 的所有点连边时,杰瑞就可以在这里等汤姆,当汤姆离开这个 \(dcc\) 的割点时,杰瑞直接窜出去,汤姆就得受着

graph

  • 比如这张图就是
  • 但是汤姆可以重新踩回这个 \(dcc\) 的割点,这样杰瑞就回不来了,所以这样的 \(dcc\) 至少有两个,才能保证杰瑞可以反复横跳,我们称为好的 \(dcc\)
  • 还有一种 \(dcc\),其中任意一个点都没有与这个 \(dcc\) 的所有点连边,还是上面这张图,就是这种 \(dcc\),在这种 \(dcc\) 里,杰瑞可以把汤姆溜自闭,因为汤姆进到这个 \(dcc\) 后,在任意一个点都不能抓到杰瑞,我们称为极好的 \(dcc\)
  • 杰瑞不会被抓到当且仅当图中存在两个好的 \(dcc\) 或存在一个极好的 \(dcc\)
  • 所以我们对于每个 \(dcc\),判是否为好的,如果好的个数大于等于 \(2\) 个,将其在圆方树对应的方节点设为 \(1\),否则设为 \(0\)
  • 当有且只有 \(1\)好的时,判断其是否为极好的,是同上将其对应的方节点设为 \(1\),否则设为 \(0\)
  • 接下来统计子树和,方便查询答案,子树和对于上面所说的赋的值进行加和,\(siz_i\) 表示 \(i\) 的子树大小,钦定 \(1\) 为根
  • 将汤姆初始节点设为 \(T\),杰瑞设为 \(J\),统计答案分三种情况:
  1. \(T\)\(J\) 不在一棵子树内,如下

2026-09-08 09-47-06屏幕截图

  • 杰瑞活动区间为整棵树,但是无法到达 \(T\) 及其子树
  • 此时答案为 \(siz_1 - siz_T\),当答案为 \(0\) 则可以抓到,反之不可以
  • 是否在一个子树内可以用 \(LCA\) 快速求解
  1. \(T\)\(J\) 在一棵子树内且 \(dep_T < dep_J\),如下

2026-09-08 09-50-21屏幕截图

  • 杰瑞活动区间为 \(T\) 的子树
  • 这时答案为 \(siz_{son_T}\),当答案为 \(0\) 则可以抓到,反之不可以,其中 \(son_T\) 表示 \(J\) 属于的 \(T\) 的子树节点编号

2026-09-08 09-54-06屏幕截图

  • \(son_T\) 可以倍增找,但是msjing的写的重剖 \(LCA\) 而且是暴力找的,随便来张菊花图就卡成 \(nm \log n\) 但是给放过去了。。。
  • 不改倍增了,就这样吧
  1. \(T\)\(J\) 在一棵子树内且 \(dep_T > dep_J\),如下

2026-09-08 09-55-10屏幕截图

  • 杰瑞活动区间为整棵树,但是无法到达 \(T\) 及其子树
  • 此时答案为 \(siz_1 - siz_T\),当答案为 \(0\) 则可以抓到,反之不可以
  • 特判纯环的情况,因为msjing的代码处理纯环会因为没有割点而导致后面全木大但是又给放过去了。。。
  • 代码不好调,写了 \(205\)
  • 由于是暴力找子树,所以复杂度是假的,正确复杂度请换倍增
点击查看代码
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
constexpr int maxn=4e5+10;
int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || ch>'9')
    {
        if (ch == '-') f=-1;
        ch=getchar();
    }
    while (ch>='0' && ch<='9')
    {
        x=(x<<1)+(x<<3)+ch-'0';
        ch=getchar();
    }
    return x*f;
}
int n,m,k;
int h[maxn],to[maxn],nxt[maxn],tot;
void addedge(int x,int y)
{
    tot++;
    to[tot]=y;
    nxt[tot]=h[x];
    h[x]=tot;
}
int Dfn[maxn],low[maxn],num;
int cut[maxn];
int stk[maxn],top,cnt;
vector<int> dcc[maxn];
vector<int> e[maxn];
int w[maxn];
void tarjan(int x)
{
    Dfn[x]=low[x]=++num;
    stk[++top]=x;
    int f=0;
    for (int i=h[x];i;i=nxt[i])
    {
        int y=to[i];
        if (!Dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if (Dfn[x]<=low[y])
            {
                f++;
                if (x!=1 || f>1) cut[x]=1;
                cnt++;
                int z=0;
                do
                {
                    z=stk[top--];
                    dcc[cnt].pb(z);
                }while (z!=y);
                dcc[cnt].pb(x);
            }
        }
        else
            low[x]=min(low[x],Dfn[y]);
    }
}
bitset<maxn> s,gd;
vector<int> good;
void getgood(int x)
{
    s.reset();
    for (int i=h[x];i;i=nxt[i])
        s.set(to[i]);
    for (int y:e[x])
    {
        int ct=0;
        for (int z:e[y])
            if (s[z])
                ct++;
        if (ct!=e[y].size()-1)
            gd.set(y);
    }
}
namespace TLPD
{
    int siz[maxn];
    int Siz[maxn],son[maxn],fa[maxn],dep[maxn];
    int dfn[maxn],rnk[maxn],top[maxn],nt;
    void dfs1(int x)
    {
        Siz[x]=1,son[x]=-1;
        if (w[x]) siz[x]++;
        for (int y:e[x])
        {
            if (dep[y]) continue;
            dep[y]=dep[x]+1;
            fa[y]=x;
            dfs1(y);
            Siz[x]+=Siz[y];
            siz[x]+=siz[y];
            if (son[x] == -1 || Siz[y]>Siz[son[x]])
                son[x]=y;
        }
    }
    void dfs2(int x,int t)
    {
        top[x]=t;
        dfn[x]=++nt,rnk[nt]=x;
        if (son[x] == -1) return;
        dfs2(son[x],t);
        for (int y:e[x])
        {
            if (y == fa[x] || y == son[x]) continue;
            dfs2(y,y);
        }
    }
    int lca(int x,int y)
	{
		while (top[x]!=top[y])
		{
			if (dep[top[x]]<dep[top[y]])
                swap(x,y);
			x=fa[top[x]];
		}
		return dep[x]<dep[y] ? x : y;
	}
}using namespace TLPD;
int flag=-1;
int main()
{
    n=read(),m=read(),k=read();
    for (int i=1;i<=m;i++)
    {
        int x=read(),y=read();
        addedge(x,y);
        addedge(y,x);
    }
    tarjan(1);
    int ot=n;
    for (int i=1;i<=cnt;i++)
    {
        ot++;
        for (int j:dcc[i])
            e[ot].pb(j),e[j].pb(ot);
    }
    for (int i=1;i<=n;i++)
        if (cut[i])
            getgood(i);
    for (int i=1;i<=ot;i++)
        if (gd[i])
            good.pb(i);
    int numofcut=0;
    for (int i=1;i<=n;i++)
        if (cut[i])
            numofcut++;
    if (!numofcut)
        n<=3 ? flag=0 : flag=1;
    if (good.size() == 1)
    {
        int x=good[0];
        int f=1;
        bitset<maxn> dc;
        for (int y:e[x])
            dc.set(y);
        for (int y:e[x])
        {
            int ct=0;
            for (int i=h[y];i;i=nxt[i])
                if (dc[to[i]])
                    ct++;
            if (ct == e[x].size()-1)
            {f=0;break;}
        }
        if (f) w[x]=1;
    }
    else if (good.size()>1)
        for (int i:good)
            w[i]=1;
    dep[1]=1;
    dfs1(1),dfs2(1,1);
    while (k--)
    {
        int T=read(),J=read();
        if (flag!=-1)
        {
            !flag ? puts("Yes") : puts("No");
            continue;
        }
        int Lca=lca(T,J);
        if (Lca!=T && Lca!=J)
            !(siz[1]-siz[T]) ? puts("Yes") : puts("No");
        else
        {
            if (dep[T]<dep[J])
            {
                int x=0;
                for (int y:e[T])
                    if (y == lca(y,J)) {x=y;break;}
                !siz[x] ? puts("Yes") : puts("No");
            }
            else
                !(siz[1]-siz[T]) ? puts("Yes") : puts("No");
        }
    }
    return 0;
}
posted @ 2026-09-08 10:00  msjing  阅读(13)  评论(0)    收藏  举报