瑞瑞的符文神树
【题目来源】
瑞学堂:瑞瑞的符文神树
【题目描述】
瑞瑞在古老的神树遗迹中发现了一棵由 \(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
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的树,每个节点有颜色 \(c_i\),以及长度为 \(K\) 的目标颜色序列 \(b[1..K]\)。求树上是否存在一条简单路径,使得路径上的颜色序列包含 \(b\) 作为子序列(不要求连续,保持顺序),并求满足条件的最长路径的节点数。这是一个树形DP + 区间DP问题,关键在于用三维状态 \(down[u][l][r]\) 和 \(up[u][l][r]\) 分别记录从节点 \(u\) 向下/向上走、能匹配 \(b[l..r]\) 的最长链长度。
-
算法选择:
- 树形DP(后序遍历):以节点 \(1\) 为根,自底向上计算每个节点的 \(down\) 和 \(up\) 状态
- 区间DP状态设计:\(down[u][l][r]\) 表示从 \(u\) 出发向下走,路径颜色序列能匹配 \(b[l..r]\) 的最长链长度;\(up[u][l][r]\) 表示向上走(向父节点方向)的对应值
- 子树合并(最大/次大值):路径经过 \(u\) 连接两棵子树时,需找左右两部分的最大值,且不能来自同一子节点(保证简单路径),用最大/次大值技巧处理
-
关键步骤:
- 建树与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\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n \cdot K^2 \cdot \deg)\),其中 \(\deg\) 为节点度数,子树合并时枚举分割点并遍历子节点
- 空间复杂度:\(O(n \cdot K^2)\),\(down\) 和 \(up\) 数组
-
树形区间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
浙公网安备 33010602011771号