把操作次数记成 c_u,颜色由 c_u 的奇偶决定、相邻两点满足 c_u <= c_{a_u}+[a_u=s];在 s 所在的功能图基环树上做树形 DP,环上用前缀 max 转移。
OJ: luogu
题目 ID: P9716
难度:省选/NOI-
标签:动态规划 DP基环树树形 DP功能图
创建: 2026-10-02 15:23
更新: 2026-10-03 12:34
形式化题目
给出
可以任意多次执行操作:选一个
最终得分是「颜色为
由于每个点恰好有一条出边,把
正解
思路
第一步:把「操作序列」翻译成每个点被改色的次数。
设
-
颜色由次数的奇偶性决定。 初始时只有
是 ,其余是 。点 每被改一次颜色就翻转一次,所以最终颜色为 于是点贡献的权值是 为 时的 ,否则为 。 -
相邻两次改色之间必须有颜色可复制,因此次数受父亲限制。 点
的第 次改色,是把此刻 的颜色复制过来,所以 不可能比 被改得更多次:要有第 次改色的机会,就得先有 的颜色变化。形式化地: 其中是因为根 一开始就持有颜色 ,相当于白送一次。 根
不受这条约束(没有「更上层」的点能限制它)。
第二步:为什么总操作次数就是
这是一个在基环树上做选择的 DP。注意
第三步:
此时从
设
转移时对每个儿子
对
答案是在根处取
第四步:
先找出
对环上每个点先把它环外的子树用上面的树形 DP 合并进
-
环长
: 环上只有 和 两个点。此时枚举 的三种可行组合即可,答案为 -
环长
: 环上相邻两个点的次数差至多为 :一旦 , 的第 次改色就取不到颜色(它的父亲还没改那么多次),所以非法;而沿着环走一圈最多让次数每次增加 。于是枚举 (注意把 自带的初始颜色也算进 ,所以 ),用 来转移。初始
( 是因为 的计数里额外含了初始的那一次颜色), 。对 : 最后对所有
取 的最大值。 这份
的枚举是完备的:沿环走一圈,次数最多比 少 (把 的初始颜色 1 沿着环推一圈再推回来的构造可以达到这个差值),再少就不合法了。
状态转移示例。 以样例一(
| 0 | |||
| 1 | |||
| 2 |
取
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:23
* update_at: 2026-10-03 12:30
*
* P9716 [EC Final 2022] Coloring
*
* 模型:把操作 c_i <- c_{a_i} 看成"把 i 的父亲 a_i 的颜色复制给 i"。
* 对每个点 u 记 c_u = 它被操作(改色)的次数,则:
* - u 最终颜色 = (c_u + [u == s]) % 2,因为初始只有 s 是 1;
* - 要第 k 次改色 u,必须先有颜色从 a_u 流过来,所以 c_u 与 c_{a_u} 满足
* c_u <= c_{a_u} + [a_u == s](u != s),根 s 不受这条约束;
* - 总代价 = sum c_u * p_u(同一颜色连续写两次无意义,所以总次数就是 c_u)。
* 于是题目变成:在基环树上按上面的约束选 c_u,最大化
* sum_{u: 最终颜色=1} w_u - sum c_u * p_u
*
* 做法:先找 s 所在的环。
* - s 不在环上:把 a_i -> i 看成以 s 为根的外向树,树形 DP
* f[u][i] = u 被改色 i 次时 u 子树的最大收益,转移用前缀 max 优化。
* 答案 = max(f[s][0], f[s][1])(改色超过 1 次不会更优)。
* - s 在环上(环长 k):环上点的子树各自树形 DP,环上再套一层 DP。
* k == 2 时只有三种合法组合,直接取 max。
* k > 2 时枚举 c_s = r,用 g[i][j] (j = 0/1/2) 表示走到环上第 i 个点时
* 它的改色次数是 r - j。相邻两点的次数最多相差 1(循环一次)。
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 5005;
const ll NEG = -(ll)4e18; // 负无穷,注意别让它参加加法后溢出
int n, s; // 点数,初始颜色为 1 的点(输入 1 下标,代码里转 0 下标)
ll w[MAXN], p[MAXN]; // 价值、单次改色代价
int a[MAXN]; // a[i] 指向的元素
vector<int> g[MAXN]; // 反图 a_i -> i,即以 i 为父亲的儿子集合
int on_cycle[MAXN]; // 是否属于 s 所在的环
vector<int> cyc; // s 所在的环(rot 成以 s 开头)
ll f[MAXN][MAXN]; // f[u][i]: u 被改色 i 次时,u 子树的最大收益
// 计算 u 子树(u 在环上或树上,环上结点的环上儿子被跳过)
void dfs(int u) {
// 基础值:u 最终颜色 = (i + [u == s]) % 2,产生价值 w[u];代价 i * p[u]
for (int i = 0; i <= n; i++) {
int color = (i + (u == s)) & 1;
f[u][i] = (color ? w[u] : 0) - (ll)i * p[u];
}
f[u][n + 1] = NEG;
for (int v : g[u]) {
if (on_cycle[v]) continue; // 环上的儿子由环 DP 负责
dfs(v);
ll best = NEG; // 前缀 max:max_{0 <= j <= i} f[v][j]
for (int i = 0; i <= n; i++) {
best = max(best, f[v][i]);
ll add = best;
// 父亲是 s 时,儿子可以多用一次:j <= i + 1
if (u == s) add = max(add, f[v][i + 1]);
f[u][i] += add;
}
}
}
// 找到 s 所在环上的所有点
void find_cycle() {
int x = s;
vector<char> vis(n, 0);
while (!vis[x]) { vis[x] = 1; x = a[x]; }
vector<char> used(n, 0);
int y = x;
while (!used[y]) { cyc.push_back(y); used[y] = 1; y = a[y]; }
reverse(cyc.begin(), cyc.end());
for (int u : cyc) on_cycle[u] = 1;
// 旋转成以 s 开头
int pos = 0;
for (int i = 0; i < (int)cyc.size(); i++) if (cyc[i] == s) pos = i;
rotate(cyc.begin(), cyc.begin() + pos, cyc.end());
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> s;
s--; // 转成 0 下标
for (int i = 0; i < n; i++) cin >> w[i];
for (int i = 0; i < n; i++) cin >> p[i];
for (int i = 0; i < n; i++) { cin >> a[i]; a[i]--; }
for (int i = 0; i < n; i++) g[a[i]].push_back(i);
find_cycle();
if (!on_cycle[s]) {
// s 不在环上:s 为根的树,直接树形 DP
dfs(s);
cout << max(f[s][0], f[s][1]) << "\n";
return 0;
}
// s 在环上:环上每个点跑一遍子树 DP
for (int u : cyc) dfs(u);
int k = (int)cyc.size();
if (k == 2) {
// 只有 s 和 t 两个点,枚举它们的改色次数组合
int t = cyc[1];
ll ans = max(f[s][0] + f[t][0], f[s][0] + f[t][1]);
ans = max(ans, f[s][1] + f[t][0]);
cout << ans << "\n";
return 0;
}
// k > 2:枚举 c_s = r(把 s 的初始颜色 1 也算进去,所以 r >= 1)
ll ans = NEG;
for (int r = 1; r <= n + 1; r++) {
// g[j]: 当前环上点改色次数为 r - j 时的最大收益
ll g[3] = {f[s][r - 1], NEG, NEG};
for (int i = 1; i < k; i++) {
ll ng[3] = {NEG, NEG, NEG};
// 本点次数 = r:前一个点次数只能也是 r
if (g[0] > NEG / 2) ng[0] = max(ng[0], g[0] + f[cyc[i]][r]);
// 本点次数 = r-1:前一个点是 r 或 r-1
{
ll pre = max(g[0], g[1]);
if (pre > NEG / 2) ng[1] = max(ng[1], pre + f[cyc[i]][r - 1]);
}
// 本点次数 = r-2:前一个点可以是 r / r-1 / r-2
if (r > 1) {
ll pre = max(max(g[0], g[1]), g[2]);
if (pre > NEG / 2) ng[2] = max(ng[2], pre + f[cyc[i]][r - 2]);
}
g[0] = ng[0]; g[1] = ng[1]; g[2] = ng[2];
}
ans = max(ans, max(max(g[0], g[1]), g[2]));
}
cout << ans << "\n";
return 0;
}代码里的实现要点:
on_cycle[u]标记所在环上的点; dfs(u)跳过环上的儿子,避免在环上递归。f[u][i]的下标开到 ,因为合并环上/树上儿子时需要访问 ,多开一格并置为 。 - 找环时从
出发沿 走,先用 vis找到第一个重复出现的点,再从它绕一圈,得到的就是所在环(对不连通的功能图也成立,与题面的基环树连通性无关)。
复杂度
- 树上部分:每个点对每个
做一次 的前缀 合并,共 。 - 环上部分:枚举
共 种,每种沿环走 步、每步 ,总计 。 - 总时间复杂度
,空间复杂度 ( 时约 MB,限制为 GB)。
总结
- 关键在于把「一串操作」重新解释成每个点的改色次数
:颜色只看 的奇偶,代价是 ,而操作可行性被压成了一条简单的局部约束 。 - 有了这个模型,问题就归约成基环树上的 DP:
不在环上时是普通树形 DP,用前缀 把合并降到 ; 在环上时先对每个环上点跑子树 DP,再在环上枚举 ,用「相邻次数差 」做长度为 的环上 DP。 - 边界情况主要有三类:环长为
需要单独枚举;结点权值可为负,所以「不选点」是合法选择( 的初值里颜色为 时贡献 );答案可能为负,不能用 初始化。