瑞瑞的符文神树

【题目来源】

瑞学堂:瑞瑞的符文神树

【题目描述】

瑞瑞在古老的神树遗迹中发现了一棵由 \(n\) 个结点组成的神树,结点编号 \(1∼n\),有 \(n−1\) 条无向边连接它们。
每个结点上都刻着一个神秘的符文,符文用一个整数表示其颜色。

瑞瑞随身携带了一卷古老的符文卷轴,上面记录着长度为 \(K\) 的颜色序列 \(b_1,b_2,…,b_K\)。传说如果在树中存在一条简单路径,路径上的结点颜色能够按顺序包含卷轴上的全部颜色(即存在 \(K\) 个结点 \(v_1,v_2,…,v_K\),它们在路径上按顺序排列,且颜色依次与卷轴相同),那么神树就会赐予瑞瑞强大的力量。

更具体地说,瑞瑞想知道:在这棵树上,是否存在一条简单路径,满足路径上结点的颜色序列中,卷轴的颜色序列 \(b\) 作为子序列出现(不要求连续,但必须保持顺序)。如果存在这样的路径,瑞瑞希望找到其中结点数最多的那条路径的结点个数;如果不存在任何满足条件的路径,请输出 \(0\)

简单路径是指路径上的结点互不相同的路径,路径的结点数定义为路径上包含的结点总数。

【输入】

第一行包含两个整数 \(n,K\),分别表示树中结点数和卷轴的颜色序列长度。
第二行包含 \(n\) 个整数 \(c_1,c_2,…,c_n\),其中 \(c_i\) 表示结点 \(i\) 的符文颜色。
第三行包含 \(K\) 个整数 \(b_1,b_2,…,b_K\),表示卷轴上的颜色序列。
接下来 \(n−1\) 行,每行两个整数 \(u,v\),表示结点 \(u\) 与结点 \(v\) 之间有一条边。

【输出】

输出一行一个整数,表示满足条件的最长简单路径的结点数。如果不存在任何满足条件的路径,输出 \(0\)

【输入样例】

3 2
1 2 3
1 3
1 2
2 3

【输出样例】

3

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的树,每个节点有颜色 \(c_i\),以及长度为 \(K\) 的目标颜色序列 \(b[1..K]\)。求树上是否存在一条简单路径,使得路径上的颜色序列包含 \(b\) 作为子序列(不要求连续,保持顺序),并求满足条件的最长路径的节点数。这是一个树形DP + 区间DP问题,关键在于用三维状态 \(down[u][l][r]\)\(up[u][l][r]\) 分别记录从节点 \(u\) 向下/向上走、能匹配 \(b[l..r]\) 的最长链长度。

  2. 算法选择

    • 树形DP(后序遍历):以节点 \(1\) 为根,自底向上计算每个节点的 \(down\)\(up\) 状态
    • 区间DP状态设计\(down[u][l][r]\) 表示从 \(u\) 出发向下走,路径颜色序列能匹配 \(b[l..r]\) 的最长链长度;\(up[u][l][r]\) 表示向上走(向父节点方向)的对应值
    • 子树合并(最大/次大值):路径经过 \(u\) 连接两棵子树时,需找左右两部分的最大值,且不能来自同一子节点(保证简单路径),用最大/次大值技巧处理
  3. 关键步骤

    • 建树与DFS:读入树结构,以 \(1\) 为根建立有根树,记录父节点和子节点列表
    • 后序遍历DP(自底向上处理每个节点 \(u\)):
      • 初始化\(down\_empty[u] = up\_empty[u] = 1\)(只含 \(u\) 的空链);若 \(c[u] = b[l]\),则 \(down[u][l][l] = up[u][l][l] = 1\)
      • 子节点转移:遍历每个子节点 \(v\)
        • 空链延续:down_empty[u] = max(down_empty[u], down_empty[v] + 1)
        • 匹配链延续:down[u][l][r] = max(down[u][l][r], down[v][l][r] + 1)
        • \(u\) 匹配 \(b[l]\)\(v\) 匹配 \(b[l+1..r]\)down[u][l][r] = max(down[u][l][r], base + 1)\(base\)\(v\) 匹配剩余部分)
        • \(up\) 方向对称:\(u\) 匹配 \(b[r]\)\(v\) 匹配 \(b[l..r-1]\)
      • 更新端点答案ans = max(ans, down[u][1][K], up[u][1][K])
      • 跨子树路径\(u\) 有两个以上子节点):
        • 枚举分割点 \(i\):左子树匹配 \(b[1..i]\),右子树匹配 \(b[i+1..K]\),找不同子节点的最大/次大值拼接
        • 枚举 \(u\) 匹配 \(b[mid]\):左子树匹配 \(b[1..mid-1]\),右子树匹配 \(b[mid+1..K]\)
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot K^2 \cdot \deg)\),其中 \(\deg\) 为节点度数,子树合并时枚举分割点并遍历子节点
    • 空间复杂度:\(O(n \cdot K^2)\)\(down\)\(up\) 数组
  5. 树形区间DP的核心思想

    • 方向分离:将路径拆分为"向下"和"向上"两个方向分别DP,避免同时处理双向的复杂性
    • 区间子序列匹配\(down[u][l][r]\) 表示匹配目标序列的一个连续子区间,利用子序列的连续性将问题分解为"当前节点匹配某个位置 + 子树匹配剩余区间"
    • 最大次大值保证简单路径:路径经过 \(u\) 连接两棵子树时,左右两部分必须来自不同子节点,否则路径会重复经过 \(u\) 到该子节点的边,违反简单路径定义
    • 空链作为边界\(down\_empty\)\(up\_empty\) 处理"不匹配目标序列任何部分"的情况,作为区间长度为 \(0\) 时的基础状态
    • 适用于"树上子序列匹配 + 最长简单路径"问题,核心在于将路径方向分离、区间匹配状态化、子树合并用最大次大值去重

【算法标签】

树形DP

【代码详解】

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
const int MAXN = 80005, MAXK = 13, INF = -1e9;  // 最大节点数、目标序列最大长度、负无穷表示不可达
int n, K, c[MAXN], b[MAXK];                     // n:节点数, K:目标序列长度, c[i]:节点i的颜色, b[i]:目标序列第i个颜色
int down[MAXN][MAXK][MAXK], up[MAXN][MAXK][MAXK]; // down[u][l][r]:从u向下走,能匹配b[l..r]的最长链长度; up[u][l][r]:从u向上走,能匹配b[l..r]的最长链长度
int down_empty[MAXN], up_empty[MAXN], ans;        // down_empty[u]:从u向下不匹配b任何部分的最长链; up_empty[u]:从u向上的对应值; ans:最终答案
vector<int> adj[MAXN];                          // 邻接表存储无向树
vector<int> children[MAXN];                     // 有根树中u的子节点列表
int parent[MAXN];                               // 有根树中u的父节点

int main()
{
    ios::sync_with_stdio(false); cin.tie(0);    // 关闭同步流、解除cin与cout绑定,加速输入输出
    cin >> n >> K;                              // 读入节点数和目标序列长度
    for (int i = 1; i <= n; ++i) cin >> c[i];   // 读入每个节点的颜色
    for (int i = 1; i <= K; ++i) cin >> b[i];   // 读入目标颜色序列b[1..K]
    for (int i = 1, u, v; i < n; ++i)           // 读入n-1条无向边
    {
        cin >> u >> v;
        adj[u].push_back(v);                    // u与v之间添加无向边
        adj[v].push_back(u);
    }
    parent[1] = 0;                              // 以1号节点为根,根节点的父节点设为0
    stack<int> st; st.push(1);                  // 用栈模拟DFS,将根节点入栈
    vector<int> order;                          // order记录DFS遍历顺序(先序)
    while (!st.empty())                         // 非递归DFS建树
    {
        int u = st.top(); st.pop();             // 取出栈顶节点u
        order.push_back(u);                     // 将u加入遍历顺序
        for (int v : adj[u])                    // 遍历u的所有邻居
        {
            if (v != parent[u])                 // 避免回到父节点
            {
                parent[v] = u;                  // 设置v的父节点为u
                children[u].push_back(v);       // 将v加入u的子节点列表
                st.push(v);                     // v入栈继续DFS
            }
        }
    }
    reverse(order.begin(), order.end());          // 将先序遍历顺序反转,得到后序遍历顺序(自底向上处理)
    ans = 0;                                      // 初始化答案为0
    for (int u : order)                           // 按后序遍历顺序处理每个节点(先处理子节点再处理父节点)
    {
        down_empty[u] = up_empty[u] = 1;        // 初始化:只包含u自身的空匹配链长度为1
        for (int l = 1; l <= K; ++l)            // 初始化down和up数组为负无穷(不可达)
            for (int r = l; r <= K; ++r)
                down[u][l][r] = up[u][l][r] = INF;
        for (int l = 1; l <= K; ++l)            // 若u的颜色恰好等于b[l],则单个节点可匹配b[l..l]
            if (c[u] == b[l])
                down[u][l][l] = up[u][l][l] = 1;
        for (int v : children[u])               // 遍历u的每个子节点v,进行状态转移
        {
            if (down_empty[v] + 1 > down_empty[u]) down_empty[u] = down_empty[v] + 1; // 更新向下空匹配链:从v延续过来
            if (up_empty[v] + 1 > up_empty[u]) up_empty[u] = up_empty[v] + 1;         // 更新向上空匹配链:从v延续过来
            for (int l = 1; l <= K; ++l)        // 枚举区间左端点l
            {
                for (int r = l; r <= K; ++r)  // 枚举区间右端点r
                {
                    int best = INF;             // best记录从子节点v转移来的最优值
                    if (down[v][l][r] != INF) best = max(best, down[v][l][r] + 1); // 情况1:从v延续匹配b[l..r]
                    if (c[u] == b[l])           // 情况2:在u处匹配b[l],v负责匹配b[l+1..r]
                    {
                        int base = (l + 1 <= r) ? down[v][l + 1][r] : down_empty[v]; // base为v匹配剩余部分的长度
                        if (base != INF) best = max(best, base + 1);                   // u匹配b[l],总长度+1
                    }
                    if (best > down[u][l][r]) down[u][l][r] = best;                    // 更新down[u][l][r]
                    best = INF;                     // 重置best,计算up方向的转移
                    if (up[v][l][r] != INF) best = max(best, up[v][l][r] + 1);         // 情况1:从v延续匹配b[l..r](向上方向)
                    if (c[u] == b[r])               // 情况2:在u处匹配b[r],v负责匹配b[l..r-1]
                    {
                        int base = (l <= r - 1) ? up[v][l][r - 1] : up_empty[v];       // base为v匹配前面部分的长度
                        if (base != INF) best = max(best, base + 1);                     // u匹配b[r],总长度+1
                    }
                    if (best > up[u][l][r]) up[u][l][r] = best;                        // 更新up[u][l][r]
                }
            }
        }
        if (down[u][1][K] > ans) ans = down[u][1][K]; // 更新答案:以u为端点向下匹配完整序列b[1..K]
        if (up[u][1][K] > ans) ans = up[u][1][K];     // 更新答案:以u为端点向上匹配完整序列b[1..K]
        int numC = children[u].size();                // u的子节点数量
        if (numC >= 2)                                // 若u有两个以上子节点,考虑路径经过u连接两棵子树
        {
            for (int i = 0; i <= K; ++i)              // 枚举分割点i:左子树匹配b[1..i],右子树匹配b[i+1..K]
            {
                int maxL = INF, maxL_idx = -1, secL = INF;  // maxL:左子树最大值, maxL_idx:对应子节点编号, secL:次大值
                int maxR = INF, maxR_idx = -1, secR = INF;   // maxR:右子树最大值, maxR_idx:对应子节点编号, secR:次大值
                for (int idx = 0; idx < numC; ++idx)   // 遍历所有子节点,找左/右子树匹配的最大和次大值
                {
                    int v = children[u][idx];
                    int lval = (i == 0) ? up_empty[v] : up[v][1][i];   // 左子树匹配b[1..i]的长度(i=0时用空匹配)
                    if (lval > maxL) { secL = maxL; maxL = lval; maxL_idx = idx; }      // 更新左子树最大和次大
                    else if (lval > secL) secL = lval;
                    int rval = (i == K) ? down_empty[v] : down[v][i + 1][K]; // 右子树匹配b[i+1..K]的长度(i=K时用空匹配)
                    if (rval > maxR) { secR = maxR; maxR = rval; maxR_idx = idx; }      // 更新右子树最大和次大
                    else if (rval > secR) secR = rval;
                }
                if (maxL != INF && maxR != INF)       // 若左右均可达
                {
                    if (maxL_idx != maxR_idx) ans = max(ans, maxL + maxR + 1);          // 来自不同子树,直接拼接+u
                    else                                  // 来自同一子树,用次大值替换其中一个
                    {
                        if (secL != INF) ans = max(ans, secL + maxR + 1);               // 左子树用次大
                        if (secR != INF) ans = max(ans, maxL + secR + 1);               // 右子树用次大
                    }
                }
            }
            for (int mid = 1; mid <= K; ++mid)        // 枚举u匹配b[mid]的情况:左子树匹配b[1..mid-1],右子树匹配b[mid+1..K]
            {
                if (c[u] != b[mid]) continue;         // u的颜色必须等于b[mid]才能作为中间匹配点
                int maxL = INF, maxL_idx = -1, secL = INF;  // 同上,记录左子树最大/次大
                int maxR = INF, maxR_idx = -1, secR = INF;  // 同上,记录右子树最大/次大
                for (int idx = 0; idx < numC; ++idx)   // 遍历所有子节点
                {
                    int v = children[u][idx];
                    int lval = (mid == 1) ? up_empty[v] : up[v][1][mid - 1];   // 左子树匹配b[1..mid-1]
                    if (lval > maxL) { secL = maxL; maxL = lval; maxL_idx = idx; }
                    else if (lval > secL) secL = lval;
                    int rval = (mid == K) ? down_empty[v] : down[v][mid + 1][K]; // 右子树匹配b[mid+1..K]
                    if (rval > maxR) { secR = maxR; maxR = rval; maxR_idx = idx; }
                    else if (rval > secR) secR = rval;
                }
                if (maxL != INF && maxR != INF)       // 若左右均可达
                {
                    if (maxL_idx != maxR_idx) ans = max(ans, maxL + maxR + 1);          // 不同子树直接拼接+u
                    else
                    {
                        if (secL != INF) ans = max(ans, secL + maxR + 1);               // 用次大值确保简单路径
                        if (secR != INF) ans = max(ans, maxL + secR + 1);
                    }
                }
            }
        }
    }
    cout << ans << '\n';                           // 输出最长合法路径的节点数
    return 0;
}

【运行结果】

3 2
1 2 3
1 3
1 2
2 3
3
posted @ 2026-08-07 15:27  团爸讲算法  阅读(6)  评论(0)    收藏  举报