#P2671. 第3题-有根树
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 106
            Accepted: 10
            Difficulty: 10
            
          
          
          
                       所属公司 : 
                              美团
                                
            
                        
              时间 :2025年3月8日-开发岗
                              
                      
          
 
- 
                        算法标签>树          
 
第3题-有根树
解题思路
给定一棵以 1 为根的树,每个点携带一个大写字母。对每次询问 (u,v),取树上从 u 到 v 的简单路径,依次读出字母,判断这个序列是否包含子序列 "BUG";若不包含则输出 YES,否则输出 NO。
核心建模:4 态自动机 + 函数复合
判断序列是否包含 "BUG" 可用一个 4 态自动机线性扫描完成:
- 状态 0:尚未读到 
B - 状态 1:已读到 
B,未读到后续U - 状态 2:已读到 
BU,未读到后续G - 状态 3:已读到 
BUG(吸收态) 
对单字符 ch 的转移:
- 在 0 且 
ch=='B'→ 1 - 在 1 且 
ch=='U'→ 2 - 在 2 且 
ch=='G'→ 3 - 其余转移不变
 
把“一段序列”的效果抽象为一个长度为 4 的函数数组 f[0..3]:f[s] 表示从状态 s 出发,扫描该段后落到的终止状态。两段序列拼接的总体效果就是函数复合:若先扫段 A 再扫段 B,则整体函数 F = F_B ∘ F_A。便于实现,我们定义拼接运算:
op_concat(A, B) = B ∘ A    # 先 A 再 B
树上路径方向与数据结构
路径 u→v 可以拆为两部分:
- u→LCA(u,v) 的自底向上部分(不含 LCA)
 - LCA(u,v)→v 的自顶向下部分(含 LCA)
 
用重链剖分(HLD)+ 线段树加速链内区间查询。对每条重链按“链顶→链底”的顺序线性化;在线段树每个结点上同时维护两种方向的区间函数:
F_lr:区间按自顶向下扫描(left→right)得到的函数;F_rl:区间按自底向上扫描(right→left)得到的函数。
单点(叶子)两者相同,都是该字母的“单字符函数”。 区间合并(设左儿子 A 覆盖 [L,mid],右儿子 B 覆盖 [mid+1,R]):
F_lr([L,R]) = F_lr(B) ∘ F_lr(A)(先扫 A 再扫 B)F_rl([L,R]) = F_rl(A) ∘ F_rl(B)(反向等价于先扫右儿子 B(靠下)再扫左儿子 A(靠上),实现时写成left ∘ right)
关键细节(本题易错点,两处修正)
- 
反向区间查询的合并方向 做线段树查询
F_rl时,命中左端点的段应拼到右侧,命中右端点的段应拼到左侧,保证整体是“自底向上”的真实顺序。 - 
下半段(LCA→v)链段的拼接顺序 重链剖分从
v不断跳到上方时,遇到的链段顺序与真实路径顺序相反。因此在累积downF时必须把新得到的链段函数“前置”拼接: 把 
downF = op_concat(downF, seg)    # 错:把 seg 放在后面,顺序被反转
改为
downF = op_concat(seg, downF)    # 对:把 seg 放在前面,顺序正确
最终总函数 F_total = F_down ∘ F_up(先上半段,再下半段)。把初始状态 0 代入,若得到 3 表示出现 "BUG",输出 NO,否则 YES。
复杂度分析
- 预处理(两次 DFS + 建线段树):O(n)
 - 单次询问:重链剖分分解为 O(logn) 个链段,每段区间查询与 4 态函数复合均为 O(1),故 O(logn)
 - 总计:O((n+q)logn) 时间,O(n) 空间
 
代码实现
Python
# -*- coding: utf-8 -*-
# 解法:HLD + 线段树,维护区间正向/反向的4态自动机函数。
import sys
sys.setrecursionlimit(1 << 25)
# ---- 自动机:单字符函数 / 复合 / 恒等 ----
def letter_func(ch):
    f = [0, 1, 2, 3]
    if ch == 'B':
        f[0] = 1
    elif ch == 'U':
        f[1] = 2
    elif ch == 'G':
        f[2] = 3
    return f
def compose(g, f):
    # g ∘ f:先 f 后 g
    return [g[f[i]] for i in range(4)]
IDF = [0, 1, 2, 3]
def op_concat(a, b):
    # 先 a 再 b
    return compose(b, a)
# ---- 读入 ----
data = sys.stdin.read().strip().split()
it = iter(data)
n = int(next(it)); q = int(next(it))
father = [0]*(n+1)
for i in range(1, n+1):
    father[i] = int(next(it))
# 字母可能是一整串,也可能被拆成多个 token,这里拼到 n 个即可
letters = ['']
s = next(it)
while len(s) < n:
    s += next(it)
letters.append(s)  # 索引 1 的位置放整串
# 方便按节点 i 取字母:letters_str[i]
letters_str = ' ' + s
queries = []
for _ in range(q):
    u = int(next(it)); v = int(next(it))
    queries.append((u, v))
# ---- 建树(无向边)----
g = [[] for _ in range(n+1)]
for i in range(2, n+1):
    p = father[i]
    if p != 0:
        g[p].append(i)
        g[i].append(p)
# ---- HLD 预处理 ----
size = [0]*(n+1)
dep  = [0]*(n+1)
par  = [0]*(n+1)
heavy= [-1]*(n+1)
def dfs1(u, p):
    par[u] = p
    dep[u] = dep[p] + 1 if p != 0 else 0
    size[u] = 1
    mx = 0
    for v in g[u]:
        if v == p: continue
        dfs1(v, u)
        size[u] += size[v]
        if size[v] > mx:
            mx = size[v]
            heavy[u] = v
dfs1(1, 0)
head = [0]*(n+1)
pos  = [0]*(n+1)
rpos = [0]*(n+1)
cur = 0
def dfs2(u, h):
    global cur
    head[u] = h
    cur += 1
    pos[u] = cur
    rpos[cur] = u
    if heavy[u] != -1:
        dfs2(heavy[u], h)
        for v in g[u]:
            if v != par[u] and v != heavy[u]:
                dfs2(v, v)
dfs2(1, 1)
# ---- 线段树:存正向 FLR 与反向 FRL ----
size_pow2 = 1
while size_pow2 < n: size_pow2 <<= 1
FLR = [[0,1,2,3] for _ in range(2*size_pow2)]  # top->bottom
FRL = [[0,1,2,3] for _ in range(2*size_pow2)]  # bottom->top
# 叶子
for i in range(1, n+1):
    node = rpos[i]
    f = letter_func(letters_str[node])
    FLR[size_pow2 + i - 1] = f[:]
    FRL[size_pow2 + i - 1] = f[:]
# 空位置恒等
for i in range(size_pow2 + n, 2*size_pow2):
    FLR[i] = IDF[:]; FRL[i] = IDF[:]
# 向上建树
for i in range(size_pow2 - 1, 0, -1):
    # 正向:right ∘ left(先左后右)
    FLR[i] = op_concat(FLR[2*i], FLR[2*i+1])
    # 反向:left ∘ right(等价先扫靠下的右,再靠上的左)
    FRL[i] = op_concat(FRL[2*i+1], FRL[2*i])
def query_lr(l, r):
    """查询 [l,r] 的自顶向下函数"""
    if l > r: return IDF[:]
    l += size_pow2 - 1; r += size_pow2 - 1
    resL = IDF[:]; resR = IDF[:]
    while l <= r:
        if (l & 1) == 1:
            resL = op_concat(resL, FLR[l]); l += 1
        if (r & 1) == 0:
            resR = op_concat(FLR[r], resR); r -= 1
        l >>= 1; r >>= 1
    return op_concat(resL, resR)
def query_rl(l, r):
    """查询 [l,r] 的自底向上函数(合并方向需对调)"""
    if l > r: return IDF[:]
    l += size_pow2 - 1; r += size_pow2 - 1
    resL = IDF[:]; resR = IDF[:]
    while l <= r:
        if (l & 1) == 1:
            # 左端命中段放到右侧
            resR = op_concat(FRL[l], resR); l += 1
        if (r & 1) == 0:
            # 右端命中段放到左侧
            resL = op_concat(resL, FRL[r]); r -= 1
        l >>= 1; r >>= 1
    return op_concat(resL, resR)
def lca(u, v):
    while head[u] != head[v]:
        if dep[head[u]] > dep[head[v]]:
            u = par[head[u]]
        else:
            v = par[head[v]]
    return u if dep[u] < dep[v] else v
def path_good(u, v):
    w = lca(u, v)
    # 上半段:u -> w(不含 w),反向查询,顺序“追加在后”
    upF = IDF[:]
    x = u
    while head[x] != head[w]:
        upF = op_concat(upF, query_rl(pos[head[x]], pos[x]))
        x = par[head[x]]
    if pos[w] + 1 <= pos[x]:
        upF = op_concat(upF, query_rl(pos[w] + 1, pos[x]))
    # 下半段:w -> v(含 w),正向查询,注意“前置拼接”(关键修正)
    downF = IDF[:]
    y = v
    while head[y] != head[w]:
        downF = op_concat(query_lr(pos[head[y]], pos[y]), downF)  # 前置
        y = par[head[y]]
    downF = op_concat(query_lr(pos[w], pos[y]), downF)            # 前置
    totalF = compose(downF, upF)  # 先上段再下段
    return totalF[0] != 3
# ---- 回答询问 ----
out = []
for u, v in queries:
    out.append("YES" if path_good(u, v) else "NO")
print("\n".join(out))
Java
// 解法:HLD + 线段树维护区间的正/反两种扫描函数(4态自动机)。
// 修正点:1) 反向区间查询 queryRL 的合并方向;2) 下半段(LCA->v)的链段“前置拼接”。
import java.io.*;
import java.util.*;
public class Main {
    static class FastScanner {
        private final InputStream in;
        private final byte[] buf = new byte[1 << 16];
        private int p = 0, n = 0;
        FastScanner(InputStream is){ in = is; }
        private int read() throws IOException {
            if (p >= n) { n = in.read(buf); p = 0; if (n <= 0) return -1; }
            return buf[p++];
        }
        int nextInt() throws IOException {
            int c, s = 1, x = 0;
            do { c = read(); } while (c <= ' ' && c != -1);
            if (c == '-') { s = -1; c = read(); }
            while (c > ' ') { x = x*10 + (c-'0'); c = read(); }
            return x * s;
        }
        String next() throws IOException {
            int c; StringBuilder sb = new StringBuilder();
            do { c = read(); } while (c <= ' ' && c != -1);
            while (c > ' ') { sb.append((char)c); c = read(); }
            return sb.toString();
        }
    }
    static int n, q;
    static ArrayList<Integer>[] g;
    static int[] size, dep, par, heavy, head, pos, rpos;
    static int curPos;
    static int sizePow2;
    static int[][] FLR, FRL;
    static final int[] IDF = new int[]{0,1,2,3};
    static int[] letterFunc(char ch){
        int[] f = new int[]{0,1,2,3};
        if (ch=='B') f[0] = 1;
        else if (ch=='U') f[1] = 2;
        else if (ch=='G') f[2] = 3;
        return f;
    }
    static int[] compose(int[] g, int[] f) {
        int[] r = new int[4];
        for (int i=0;i<4;i++) r[i] = g[f[i]];
        return r;
    }
    static int[] opConcat(int[] a, int[] b){
        // 先 a 再 b
        return compose(b, a);
    }
    static void dfs1(int u, int p){
        par[u]=p;
        dep[u]=(p==0?0:dep[p]+1);
        size[u]=1;
        int mx=0; heavy[u]=-1;
        for (int v: g[u]) if (v!=p){
            dfs1(v,u);
            size[u]+=size[v];
            if (size[v] > mx){ mx=size[v]; heavy[u]=v; }
        }
    }
    static void dfs2(int u, int h){
        head[u]=h; pos[u]=++curPos; rpos[curPos]=u;
        if (heavy[u]!=-1){
            dfs2(heavy[u], h);
            for (int v: g[u]){
                if (v!=par[u] && v!=heavy[u]) dfs2(v, v);
            }
        }
    }
    static int[] queryLR(int l, int r){
        if (l>r) return IDF.clone();
        int L = l + sizePow2 - 1, R = r + sizePow2 - 1;
        int[] resL = IDF.clone(), resR = IDF.clone();
        while (L<=R){
            if ((L & 1)==1){ resL = opConcat(resL, FLR[L]); L++; }
            if ((R & 1)==0){ resR = opConcat(FLR[R], resR); R--; }
            L >>= 1; R >>= 1;
        }
        return opConcat(resL, resR);
    }
    static int[] queryRL(int l, int r){
        if (l>r) return IDF.clone();
        int L = l + sizePow2 - 1, R = r + sizePow2 - 1;
        int[] resL = IDF.clone(), resR = IDF.clone();
        while (L<=R){
            if ((L & 1)==1){ // 左端命中段放右侧(关键修正)
                resR = opConcat(FRL[L], resR); L++;
            }
            if ((R & 1)==0){ // 右端命中段放左侧
                resL = opConcat(resL, FRL[R]); R--;
            }
            L >>= 1; R >>= 1;
        }
        return opConcat(resL, resR);
    }
    static int lca(int u, int v){
        while (head[u]!=head[v]){
            if (dep[head[u]]>dep[head[v]]) u = par[head[u]];
            else v = par[head[v]];
        }
        return dep[u]<dep[v]?u:v;
    }
    static boolean pathGood(int u, int v){
        int w = lca(u, v);
        // 上半段:u -> w(不含 w),反向,顺序“追加在后”
        int[] upF = IDF.clone();
        int x = u;
        while (head[x]!=head[w]){
            upF = opConcat(upF, queryRL(pos[head[x]], pos[x]));
            x = par[head[x]];
        }
        if (pos[w]+1 <= pos[x]){
            upF = opConcat(upF, queryRL(pos[w]+1, pos[x]));
        }
        // 下半段:w -> v(含 w),正向,注意“前置拼接”(关键修正)
        int[] downF = IDF.clone();
        int y = v;
        while (head[y]!=head[w]){
            downF = opConcat(queryLR(pos[head[y]], pos[y]), downF); // 前置
            y = par[head[y]];
        }
        downF = opConcat(queryLR(pos[w], pos[y]), downF);           // 前置
        int[] totalF = compose(downF, upF); // 先上段再下段
        return totalF[0] != 3;
    }
    public static void main(String[] args) throws Exception {
        FastScanner fs = new FastScanner(System.in);
        n = fs.nextInt(); q = fs.nextInt();
        int[] father = new int[n+1];
        for (int i=1;i<=n;i++) father[i] = fs.nextInt();
        String s = fs.next();
        while (s.length() < n) s += fs.next();
        char[] letters = new char[n+1];
        for (int i=1;i<=n;i++) letters[i] = s.charAt(i-1);
        g = new ArrayList[n+1];
        for (int i=0;i<=n;i++) g[i] = new ArrayList<>();
        for (int i=2;i<=n;i++){
            int p = father[i];
            if (p!=0){ g[p].add(i); g[i].add(p); }
        }
        size = new int[n+1];
        dep  = new int[n+1];
        par  = new int[n+1];
        heavy= new int[n+1];
        head = new int[n+1];
        pos  = new int[n+1];
        rpos = new int[n+1];
        dfs1(1,0);
        curPos = 0;
        dfs2(1,1);
        sizePow2 = 1; while (sizePow2 < n) sizePow2 <<= 1;
        FLR = new int[2*sizePow2][4];
        FRL = new int[2*sizePow2][4];
        for (int i=0;i<2*sizePow2;i++){
            for (int j=0;j<4;j++){ FLR[i][j]=IDF[j]; FRL[i][j]=IDF[j]; }
        }
        // 叶子
        for (int i=1;i<=n;i++){
            int node = rpos[i];
            int[] f = letterFunc(letters[node]);
            FLR[sizePow2 + i - 1] = f.clone();
            FRL[sizePow2 + i - 1] = f.clone();
        }
        // 向上建树
        for (int i=sizePow2-1;i>=1;i--){
            // 正向:right ∘ left
            FLR[i] = opConcat(FLR[2*i], FLR[2*i+1]);
            // 反向:left ∘ right
            FRL[i] = opConcat(FRL[2*i+1], FRL[2*i]);
        }
        StringBuilder out = new StringBuilder();
        for (int i=0;i<q;i++){
            int u = fs.nextInt(), v = fs.nextInt();
            out.append(pathGood(u, v) ? "YES" : "NO").append('\n');
        }
        System.out.print(out.toString());
    }
}
C++
// 解法:重链剖分 + 线段树,区间维护正/反两种方向的4态自动机函数。
// 修正点:1) 反向 query_rl 合并方向;2) 下半段 (LCA->v) “前置拼接”。
#include <bits/stdc++.h>
using namespace std;
struct Func {
    int f[4];
    Func(){ f[0]=0; f[1]=1; f[2]=2; f[3]=3; } // 恒等
    static Func letter(char ch){
        Func r;
        if (ch=='B') r.f[0]=1;
        else if (ch=='U') r.f[1]=2;
        else if (ch=='G') r.f[2]=3;
        return r;
    }
};
inline Func compose(const Func& g, const Func& f){
    Func r;
    for (int i=0;i<4;i++) r.f[i] = g.f[f.f[i]];
    return r;
}
inline Func op_concat(const Func& a, const Func& b){
    // 先 a 再 b
    return compose(b, a);
}
int n, q;
vector<vector<int>> g;
vector<int> sz, dep, par, heavy, head_, pos, rpos;
int curPos;
int SZ;
vector<Func> FLR, FRL;
void dfs1(int u, int p){
    par[u]=p;
    dep[u]=(p==0?0:dep[p]+1);
    sz[u]=1;
    int mx=0; heavy[u]=-1;
    for (int v: g[u]) if (v!=p){
        dfs1(v,u);
        sz[u]+=sz[v];
        if (sz[v]>mx){ mx=sz[v]; heavy[u]=v; }
    }
}
void dfs2(int u, int h){
    head_[u]=h;
    pos[u]=++curPos;
    rpos[curPos]=u;
    if (heavy[u]!=-1){
        dfs2(heavy[u], h);
        for (int v: g[u]){
            if (v!=par[u] && v!=heavy[u]) dfs2(v, v);
        }
    }
}
Func query_lr(int l, int r){
    if (l>r) return Func();
    int L = l + SZ - 1, R = r + SZ - 1;
    Func resL, resR;
    while (L<=R){
        if (L & 1){ resL = op_concat(resL, FLR[L]); L++; }
        if (!(R & 1)){ resR = op_concat(FLR[R], resR); R--; }
        L >>= 1; R >>= 1;
    }
    return op_concat(resL, resR);
}
Func query_rl(int l, int r){
    if (l>r) return Func();
    int L = l + SZ - 1, R = r + SZ - 1;
    Func resL, resR;
    while (L<=R){
        if (L & 1){                 // 左端命中段放右侧(关键修正)
            resR = op_concat(FRL[L], resR); L++;
        }
        if (!(R & 1)){              // 右端命中段放左侧
            resL = op_concat(resL, FRL[R]); R--;
        }
        L >>= 1; R >>= 1;
    }
    return op_concat(resL, resR);
}
int lca(int u, int v){
    while (head_[u]!=head_[v]){
        if (dep[head_[u]]>dep[head_[v]]) u = par[head_[u]];
        else v = par[head_[v]];
    }
    return dep[u]<dep[v]?u:v;
}
bool path_good(int u, int v){
    int w = lca(u, v);
    // 上半段:u -> w(不含 w),反向,顺序“追加在后”
    Func upF;
    int x=u;
    while (head_[x]!=head_[w]){
        upF = op_concat(upF, query_rl(pos[head_[x]], pos[x]));
        x = par[head_[x]];
    }
    if (pos[w]+1 <= pos[x]){
        upF = op_concat(upF, query_rl(pos[w]+1, pos[x]));
    }
    // 下半段:w -> v(含 w),正向,注意“前置拼接”(关键修正)
    Func downF;
    int y=v;
    while (head_[y]!=head_[w]){
        downF = op_concat(query_lr(pos[head_[y]], pos[y]), downF); // 前置
        y = par[head_[y]];
    }
    downF = op_concat(query_lr(pos[w], pos[y]), downF);            // 前置
    Func totalF = compose(downF, upF); // 先上段再下段
    return totalF.f[0] != 3;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> q;
    vector<int> father(n+1);
    for (int i=1;i<=n;i++) cin >> father[i];
    string s, t; 
    cin >> s; 
    while ((int)s.size() < n){ cin >> t; s += t; }
    vector<char> letter(n+1);
    for (int i=1;i<=n;i++) letter[i] = s[i-1];
    g.assign(n+1, {});
    for (int i=2;i<=n;i++){
        int p = father[i];
        if (p!=0){ g[p].push_back(i); g[i].push_back(p); }
    }
    sz.assign(n+1,0);
    dep.assign(n+1,0);
    par.assign(n+1,0);
    heavy.assign(n+1,-1);
    head_.assign(n+1,0);
    pos.assign(n+1,0);
    rpos.assign(n+1,0);
    dfs1(1,0);
    curPos = 0;
    dfs2(1,1);
    SZ = 1; while (SZ < n) SZ <<= 1;
    FLR.assign(2*SZ, Func());
    FRL.assign(2*SZ, Func());
    for (int i=1;i<=n;i++){
        int node = rpos[i];
        Func f = Func::letter(letter[node]);
        FLR[SZ + i - 1] = f;
        FRL[SZ + i - 1] = f;
    }
    for (int i=SZ-1;i>=1;i--){
        // 正向:right ∘ left
        FLR[i] = op_concat(FLR[2*i], FLR[2*i+1]);
        // 反向:left ∘ right
        FRL[i] = op_concat(FRL[2*i+1], FRL[2*i]);
    }
    while (q--){
        int u, v; cin >> u >> v;
        cout << (path_good(u, v) ? "YES" : "NO") << '\n';
    }
    return 0;
}
        题目内容
小美有一个结点总数为 n 且根节点编号为 1 的有根树。
第 i 个结点的编号为 i ,所携带的字母为 ai。
小美是一名程序员,她认为一对 (u,v) 是好对,当且仅当从节点 u 到节点 v 的简单路径上依次拼接得到字符串 S 的子序列中不存在"BUG"。
小美会提出 q 次询问,你需要帮助她回答(u,v)是不是好对。
子序列:子序列是原始序列的一个子集,其元素顺序保持不变,但可以选择性地删除一些元素。换句话说,子序列是从原始序列中挑选出来的元素集合,这些元素在原始序列中的相对顺序保持一致。
例如:S=BAUCDEG,"BUG"为字符串 S 的一个子序列,但"BGA"不是 S 的子序列。
输入描述
第一行两个整数 n,q(3≤n,q≤105),分别表示节点个数和询问次数。
第二行 n 个整数,第 i 个为 fatheri(1≤fatheri≤i−1),表示第 i 个节点的父节点,特别的 father1=0 。
第三行 n 个字母,第 i 个为 ai(′A′≤ai<′Z′),表示第 i 个节点所携带的字母。
接下来 q 行,每行两个墪数 u,v(1≤u=≤n),表示询问的起点和终点。
输出描述
共 q 行,每行一个字符串,若 (u,v) 是好对输出"YES",否则输出"NO"。
样例1
输入
5 3
0 1 1 2 2
AUGBC
4 3
4 5
4 1
输出
NO
YES
YES