DFS 树

DFS 树,顾名思义,是一张图按 DFS 序遍历后形成的树。主要有 4 种边:

  1. 树边:每次由一个已遍历的点到达未遍历的点,就形成了一条树边。
  2. 反祖边:也叫做回边,指向该节点的祖先。
  3. 横叉边:搜索时遇到一个已访问的节点,但这个节点并不是该节点的祖先。
  4. 前向边:访问时遇到子树中的节点时形成的。

其中,树边是 DFS 树的边。而另外三种边是原图的边在 DFS 树上保留下来形成的边。

例题一

CF1361E James and the Chase
给定一张强连通有向图,定义一个节点是好的当且仅当它到其它每一个点都有且仅有 \(1\) 条路径。若有 \(20\%\) 的点是好的就输所有好点,否则输出 \(-1\)
\(1\le n\le 10^5,0\le m\le 2\times 10^5\)

解法

先考虑如何判断一个点是否为好点。显然,我们可以以这个节点为根建出 DFS 树,然后这棵树上若没有横叉边和前向边,那么这个节点即为好点。
接下来考虑当知道一个好点时,如何找出所有好点。若当前正在判断节点为 \(u\),则 \(u\) 的子树内必定有且仅有一条反祖边,否则 \(u\) 到每个节点的路径就不一定唯一。若这条反祖边指向了 \(v\),那么显然 \(v\) 必须是好点才能使得 \(u\) 有可能是好点。那么综上所述,\(u\) 是好点当且仅当它的子树内只有一条反祖边指向一个好点。这个可以很方便的树上差分解决。
考虑如何找出第一个好点。每个点判断显然不行,因为这个显然只能暴力判断。所以发现这就是不太可做的样子,然后自然想到人类智慧解法。充分发扬人类智慧,直接随机 \(100\) 个点,没找到的话好点必定小于 \(20\%\),找到了就直接按上述方法解决即可。不难发现这样的错误概率趋近于 \(0\)。然后如果你错了就改个种子再交完事。
代码:

#include<bits/stdc++.h>
using namespace std;
mt19937 s(time(0));
const int MAXN=1e5+10;
int n,m,val[MAXN],back[MAXN],ans,dep[MAXN],now;
bool use[MAXN],vis[MAXN],r[MAXN];
vector<int> G[MAXN];
bool check(int x)
{
    vis[x]=true; use[x]=true;
    for(auto &i:G[x])
    {
        if(!vis[i]) {if(!check(i)) return false;}
        else if(!use[i]) return false;
    }
    use[x]=false;
    return true;
}
void dfs1(int x)
{
    use[x]=true;
    back[x]=x;
    for(auto &i:G[x])
    {
        if(!use[i])
        {
            dep[i]=dep[x]+1;
            dfs1(i);
            val[x]+=val[i];
            if(dep[back[i]]<dep[back[x]]) back[x]=back[i];
        }
        else
        {
            if(dep[i]<dep[back[x]]) back[x]=i;
            val[x]++;val[i]--;
        }
    }
    use[x]=false;
}
void dfs2(int x)
{
    use[x]=true;
    r[x]=(x==now||(val[x]==1&&r[back[x]]));
    ans+=r[x];
    for(auto &i:G[x]) if(!use[i]) dfs2(i);
}
int main(  )
{
    int T;
    cin>>T;
    while(T--)
    {
        scanf("%d%d",&n,&m);
        now=ans=0;
        for(int i=1;i<=n;i++)
        {
            use[i]=r[i]=vis[i]=back[i]=dep[i]=val[i]=0;
            G[i].clear();
        }
        for(int i=1;i<=m;i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            G[u].push_back(v);
        }
        for(int i=1;i<=100;i++)
        {
            int o=s()%n+1;
            for(int j=1;j<=n;j++) use[j]=vis[j]=false;
            if(check(o))
            {
                now=o;
                break;
            }
        }
        if(!now)
        {
            cout<<"-1\n";
            continue;
        }
        dfs1(now);dfs2(now);
        if(ans*5>=n)
        {
            for(int i=1;i<=n;i++) if(r[i]) cout<<i<<" ";
        }
        else cout<<-1;
        cout<<'\n';
    }
}

例题二

P11363 [NOIP2024] 树的遍历
给定一颗树,再新建一颗有 \(n-1\) 个节点的树,两个节点之间有边当且仅当原树中这两条边有公共节点。再给定 \(k\) 个新树上的关键点,求以每个关键点为起点能遍历出多少不同的 DFS 树。
\(2\le n\le 10^5,1\le k<n\)

解法

先考虑 \(k=1\) 的情况,显然这时只需以任意顺序走完一条链,然后访问子树,答案即为 \(\prod{(d_i-1)!}\)。其中,\(d_i\) 为节点 \(i\) 的度数。
再扩展到一般情况。考虑什么时候会遍历出相同的树。画几个图然后充分发扬人类注意力即可注意到,可能的遍历起始点一定是原图上一条叶子到叶子的链。
p.s.我想给你们画个图,然后就可以证明了,但是我画图真的是大粪呃啊。
而一颗新树恰好有这样一条链,所以答案为:\(\prod{(d_i-1)!}\times \prod{(d_v-1)^{-1}}\)。然后就可以写代码辣!

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10,mod=1e9+7;
int n,k,u[MAXN],v[MAXN],w[MAXN],d[MAXN],inv[MAXN],ans,sum[MAXN][2];
struct node
{
    int v,w;
    node(int v_=0,int w_=0):v(v_),w(w_){}
};
vector<node> G[MAXN];
void dfs(int x,int fa)
{
    sum[x][0]=sum[x][1]=0;
    int res=0;
    for(auto &i:G[x])
    {
        if(i.v==fa) continue;
        dfs(i.v,x);
        if(i.w) sum[i.v][1]+=sum[i.v][0],sum[i.v][0]=0;
        res=1ll*(res+1ll*sum[i.v][1]*((sum[x][0]+sum[x][1])%mod)%mod+1ll*sum[i.v][0]*sum[x][1]%mod)%mod;
        sum[x][0]=(sum[x][0]+sum[i.v][0])%mod;
        sum[x][1]=(sum[x][1]+sum[i.v][1])%mod;
    }
    
    ans=(ans+1ll*res*inv[d[x]]%mod)%mod;
    if(!d[x]) sum[x][0]++;
    sum[x][0]=1ll*sum[x][0]*inv[d[x]]%mod;
    sum[x][1]=1ll*sum[x][1]*inv[d[x]]%mod;
}
int main(  )
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    inv[0]=inv[1]=1;
    for(int i=2;i<MAXN;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
    int c,T;
    cin>>c>>T;
    while(T--)
    {
        cin>>n>>k;
        for(int i=1;i<=n;i++)
        {
            d[i]=-1;
            G[i].clear();
            w[i]=0;
            ans=0;
        }
        for(int i=1;i<n;i++)
        {
            cin>>u[i]>>v[i];
            d[u[i]]++;d[v[i]]++;
        }
        while(k--)
        {
            int x;
            cin>>x;
            w[x]=1;
        }
        if(n==2)
        {
            cout<<1<<'\n';
            continue;
        }
        for(int i=1;i<n;i++) G[u[i]].push_back(node(v[i],w[i])),G[v[i]].push_back(node(u[i],w[i]));
        int root=0;
        for(int i=1;i<=n;i++) if(d[i]) root=i;
        dfs(root,0);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=d[i];j++) ans=1ll*ans*j%mod;
        cout<<ans<<'\n';
    }
}

我一定会苦练画图的!

posted @ 2026-05-30 11:56  duchuyuanX  阅读(14)  评论(0)    收藏  举报