2024暑期集训补题(DFS + AC自动机)

杭电多校6 1008

在这里插入图片描述
题意:
给定两棵树,问树 A A A中存在多少个与树 B B B结构相同的子图

题解:
对于二叉树结构匹配这个问题,我们可以联想到用 01 01 01字符串来简化,题意保证树 B B B的叶子节点不超过 20 20 20,那么意味着我们可以将树 B B B分解为不超过 20 20 20 01 01 01字符串,然后拿这 20 20 20 01 01 01字符串在树 A A A上进行匹配,是一种接近于多模式串匹配的问题,多模式串匹配显然可以考虑到 A C A M ACAM ACAM A C A M ACAM ACAM是用来处理多模式串匹配问题的。
对于树 B B B,我们考虑分离出所有 01 01 01字符串,然后对其构建 A C A M ACAM ACAM,下图为树 B B B的一种结构例子,其中 3 , 7 , 8 , 5 , 9 , 10 3,7,8,5,9,10 3,7,8,5,9,10均为叶子节点,即存在失配情况,因此将其指向失配指针,相当于为其再次构建两个儿子,这两个儿子正是失配指针指向的节点的两个儿子,如果存在一个叶子节点,其失配指针也指向一个叶子节点,那么我们就继续失配,直到找到一个节点存在两个儿子为止。那么构建出来的 A C A M ACAM ACAM即下图所示,下划线 + 数字即表示失配再构建的儿子,这样的树形结构就可以保证无论如何向左或向右移动,必然存在一个节点与之对应。
现在考虑在树 A A A上进行 D F S DFS DFS,并保证树 B B B A C A M ACAM ACAM与之移动同步,那么每个树 A A A的节点必然对应一个树 B B B的(扩展)节点。然后我们再考虑如何计算答案,对于 D F S DFS DFS过程中,树 A A A的每一个节点都有一个扩展节点与之对应,那么如果在树 A A A中,树 B B B同步走到叶子节点,那么必然可以保证该叶子节点及以上的结构都存在,所以我们只需要当 D F S DFS DFS时,判断树 A A A当前在树 B B B中对应的节点是否为叶子节点即可,同时,我们维护 D F S DFS DFS过程中两树的深度,对于树 A A A的某一个节点 u u u,若其对应树 B B B的叶子节点 v v v,那么树 A A A中当前节点 u u u对应树 B B B的根节点深度为 d e p u − d e p v dep_u - dep_v depudepv,因为是 D F S DFS DFS,所以树 A A A中当前到树 A A A根节点的路径必然是一条链,所以用一个栈来维护这条链即可,所以树 A A A中对应树 B B B的每一个根节点计数加一,如果某一个点 u u u的计数数量等于树 B B B的叶子节点数量,那么保证该点从树 B B B的根节点同步走,必然都能走到树 B B B所有叶子节点,因此计入答案。
还有一个点需要注意,对于下图中 0 − 2 − 5 和 1 − 4 − 7 0-2-5和1-4-7 025147这两个相同的结构, 7 7 7同样也可以作为 5 5 5这个叶子节点让 1 1 1作为根节点计数 + 1 +1 +1,所以,对于失配指针指向叶子节点,我们在 D F S DFS DFS过程中,同样要走完这个失配指针,直到指向节点不再是叶子节点。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define debug(p) for (auto i : p)cerr << i << " "; cerr << endl;
#define debugs(p) for (auto i : p)cerr << i.first << " " << i.second << endl;
typedef pair<int, int> pll;
string yes = "YES";
string no = "NO";
constexpr int N = 3e5 * 20 + 7;
int n, m;
int leaf = 0;
int cnt[N];
vector<int>stk, ans;
struct ACAM{
    int tr[N][2], cnt[N], ne[N];//trie树存储,模式串叶子结点计数, 失配指针
    int idx = 0;
    
    int next[N], tag[N];//指向叶子节点的失配指针,叶子结点的标记深度
    int ls[N], rs[N];//存二叉树结构
    int n;//二叉树结构节点个数

    string s;
    void ins() //给定多个匹配串构建trie
    {
        int p = 0;
        for (int i = 0; i < s.size(); i++) {
            int u = s[i] - '0';
            if (!tr[p][u])
                tr[p][u] = ++idx;
            p = tr[p][u];
        }
        cnt[p]++;
    }
    void ins2()//给定二叉树构建trie
    {
        for (int i = 0; i <= n; i++)tag[i] = -1;
        queue<int>q;
        q.push(0);
        while(q.size())
        {
            int p = q.front();
            q.pop();
            if(ls[p])
            {
                tr[p][0] = ls[p];
                q.push(ls[p]);
            }
            if(rs[p])
            {
                tr[p][1] = rs[p];
                q.push(rs[p]);
            }
        }
    }
    void init()
    {
        for (int i = 0; i <= n; i++)
        {
            tr[i][0] = tr[i][1] = 0;
            tag[i] = -1;
            ls[i] = rs[i] = 0;
            ne[i] = next[i] = 0;
        }
    }
    void build() {
        queue<int>q;
        for (int i = 0; i < 2; i++) {
            if (tr[0][i])
                q.push(tr[0][i]);
        }
        while (q.size()) {
            int p = q.front();
            q.pop();
            //枚举每个可能存在的子节点,如果存在就添加虚边并加入队列,不存在就维护节点
            if(tag[ne[p]] >= 0)next[p] = ne[p];//存下失配指针指向叶子节点的失配方向
            else next[p] = next[ne[p]];
            for (int i = 0; i < 2; i++) {
                int s = tr[p][i];
                if (tr[p][i]) {
                    q.push(s);
                    ne[s] = tr[ne[p]][i];//子节点的虚边连向父节点虚边指向的另一个父节点的子节点
                } else
                    tr[p][i] = tr[ne[p]][i];
            }
        }
    }
}s, t;
void dfs1(int u, int depth)
{
    if(!t.tr[u][0] && !t.tr[u][1])
    {
        t.tag[u] = depth;
        leaf++;
        return;
    }
    if(t.tr[u][0])dfs1(t.tr[u][0], depth + 1);
    if(t.tr[u][1])dfs1(t.tr[u][1], depth + 1);
}
void dfs2(int p1, int p2)
{
    stk.push_back(p1);
    if(t.tag[p2] >= 0) cnt[stk[(int)stk.size() - t.tag[p2] - 1]]++;
    for (int i = t.next[p2]; i > 0; i = t.next[i]) cnt[stk[(int)stk.size() - t.tag[i] - 1]]++;
    if(s.tr[p1][0])dfs2(s.tr[p1][0], t.tr[p2][0]);
    if(s.tr[p1][1])dfs2(s.tr[p1][1], t.tr[p2][1]);
    if(cnt[p1] == leaf)ans.push_back(p1);
    stk.pop_back();
}
void init()
{
    leaf = 0;
    stk.clear();
    ans.clear();
    s.init();
    t.init();
    for (int i = 0; i <= n; i++) cnt[i] = 0;
}
void solve()
{
    cin >> n;
    s.n = n;
    s.init();
    for (int i = 0; i < n; i++) cin >> s.ls[i] >> s.rs[i];
    cin >> m;
    t.n = m;
    t.init();
    for (int i = 0; i < m; i++) cin >> t.ls[i] >> t.rs[i];
    s.ins2();
    t.ins2();
    dfs1(0, 0);
    t.build();
    dfs2(0, 0);
    cout << ans.size() << endl;
    if(ans.empty())cout << endl;
    else
    {
        sort(ans.begin(), ans.end());
        for (int i = 0; i < ans.size(); i++) cout << ans[i] << " \n"[i == ans.size() - 1];
    }
    init();
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int T = 1;
    cin >> T;
    while(T--)
    {
        solve();
    }
}
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值