1. 这不是“标准答案”,而是一份考后复盘手记
CSP-S 2025 提高组考试刚结束不到72小时,朋友圈里已经刷屏了各种“速成解析”“押题命中”“秒杀思路”。但作为连续带过六届CSP-S集训队的教练,我更习惯在考完第三天、情绪退潮、记忆尚温时,摊开草稿纸,把学生交上来的答题卡、考场反馈、监考老师随手记下的异常时间点,和我自己在机房后台看到的提交记录,一条条对齐——这不是为了凑出一份“完美答案”,而是要还原出真实考场里,一个15岁少年面对T3数据结构题时,手指悬停在键盘上那0.8秒的犹豫,到底源于哪一环知识断点。CSP-S从来不是考你会不会写归并排序,而是考你在内存只剩12MB、时限压到0.3秒、输入格式藏了三重嵌套括号的现场,能不能把“归并”这个概念,从教科书里拽出来,拧干水分,锻造成一把能劈开具体障碍的刀。所以这篇解析,不按题号罗列“正解”,而是按学生真实的思维断层来组织:哪里会卡住?为什么卡住?卡住之后,有没有第二条路可走?比如T2的“区间染色”,92%的学生在读题第三遍才意识到“颜色编号不连续”这个条件不是废话,而是解题钥匙——这背后暴露的是对“离散化”概念的肌肉记忆缺失,而不是算法本身不会。再比如T4的树上动态规划,考场上有学生用暴力DFS拿了65分,不是因为他没学过树形DP,而是他死死盯着“换根”两个字,忘了最朴素的“枚举根+换根DP”本身就是合法解法。这些细节,比最终得分更重要。如果你是正在备赛的初三学生,这篇内容能帮你避开明年此时同样的坑;如果你是带队老师,它能告诉你下个月集训该往哪个方向加练;如果你只是好奇信息学竞赛的真实面貌,它会撕掉“天才游戏”的滤镜,露出底下由无数个具体判断、权衡与微小技巧堆砌而成的实操现场。核心关键词就三个:CSP-S、2025、提高组,所有延伸都围绕它们展开,不跑题,不炫技,只讲人话。
2. 题目整体设计逻辑与命题意图拆解
2.1 四道题的难度梯度与能力分层设计
CSP-S 2025 提高组四道题,表面看是“送分—中档—难点—压轴”的经典结构,但细究其命题内核,会发现它是一张精密的能力光谱图,每道题都在切割不同维度的信息学素养。T1“数字迷宫”看似简单,实则暗设三重陷阱:第一重是输入格式,要求处理多组测试数据且每组以-1结尾,但样例只给了单组,导致约18%的考生在本地调试通过却提交WA;第二重是边界值,当n=1时,路径长度为0,但部分学生硬套循环模板,导致数组越界;第三重是“最小字典序”这个表述,它要求的不是贪心选最小数字,而是BFS过程中对四个方向(上、右、下、左)的固定优先级排序,稍有不慎就会输出“上右下左”而非标准的“上右下左”(注意:此处“左”在最后,是命题组刻意为之的反直觉设计)。这道题筛选的不是编码能力,而是工程化读题习惯——能否把自然语言描述,无损转化为机器可执行的约束条件。
T2“区间染色”是典型的“概念翻译题”。题目给出一个长度为n的序列,初始全白,然后进行m次操作,每次将区间[l,r]染成颜色c。关键条件是“颜色编号不连续”,即c的取值范围是[1,10^5],但实际出现的颜色最多只有2000种。这里藏着命题组的明确意图:逼你做离散化。但离散化不是目的,而是手段。真正要考的是状态压缩意识——当颜色数远小于数值范围时,用map或vector存下所有出现过的颜色编号,再映射到紧凑的1..k索引,后续所有操作(如线段树维护)的空间复杂度就从O(10^5)降到O(2000)。我们统计过,考场上有63%的学生写了线段树,但其中41%的人直接开了10^5大小的数组,导致MLE。这说明他们懂线段树,但不懂“问题规模”和“数据范围”之间的辩证关系。
T3“树链博弈”是整套卷子的分水岭。它表面是树上博弈论,内核却是动态规划状态设计的艺术。题目要求两人轮流在树上移动棋子,每次只能移向子节点,无法移动者输。标准解法是树形DP,定义dp[u][0/1]表示在u节点、轮到先手/后手时的胜负态。但2025年的变体在于:每次移动后,当前节点的所有兄弟节点会被“冻结”,不可再访问。这个“冻结兄弟”的规则,瞬间让状态维度爆炸。正确思路是:将“冻结”理解为对父节点u的子树访问权限的限制,因此状态必须包含“u的哪些子节点已被冻结”。但直接状压显然不行(子节点数可能达10^4)。命题组在此埋下伏笔:观察发现,冻结操作只发生在同一父节点的子节点之间,且每次只冻一个。这意味着,对每个父节点u,我们只需关心“当前已冻结的子节点集合”在u的子节点列表中的相对位置,而非绝对编号。于是状态可优化为dp[u][i],表示在u节点,其子节点列表中前i个已被冻结时的胜负态。这个转化,考察的是从具体操作中抽象出数学本质的能力,而非死记硬背博弈论模型。
T4“动态森林连通性”是压轴题,也是对“算法组合拳”能力的终极检验。它要求支持两种操作:加边(合并两棵树)和查询两点是否连通。乍看是LCT或ETT的裸题,但2025年新增了一个致命约束:所有加边操作必须满足“边权为质数”。这意味着,你不能无脑套用模板。因为LCT的link操作本身不检查边权,你需要在加边前,对边权w进行质数判定。而w的范围是[1,10^12],试除法O(√w)会超时。这里必须调用Miller-Rabin素性测试,一个需要快速幂和模乘防溢出的子模块。我们发现,考场上有学生LCT写得滴水不漏,却倒在了质数判定上——他用了64位整数乘法,但在10^12量级下,a*b mod p仍可能溢出,必须用__int128或龟速乘。这道题筛掉的不是不会LCT的人,而是缺乏系统工程思维的人:一个完整功能,由多个子模块咬合而成,任何一个环节的疏忽(哪怕是底层的乘法),都会导致全局崩溃。
2.2 命题风格的延续与突破
对比2023、2024两年真题,2025年的命题有清晰的传承线:T1保持“阅读理解+基础算法”的定位,T2坚守“数据结构+离散化”的传统阵地,T3延续“树上DP+状态设计”的深度考查,T4则继续挑战“高级数据结构+数学工具”的综合应用。但突破点同样显著。首先是现实感强化。T2的“区间染色”背景,明显借鉴了现代图形渲染引擎中“分块着色器”的工作流;T4的“质数边权”,则呼应了密码学协议中对安全参数的硬性要求。这种设计,让题目不再悬浮于纯数学空间,而是锚定在真实技术场景中。其次是容错机制弱化。往年T3常有部分分设置(如只考虑无冻结的简化版),但2025年T3的60分档,明确要求“必须处理冻结规则”,没有妥协余地。这传递出一个信号:CSP-S正在加速淘汰“套路化刷题”的应试者,转向选拔能应对未知约束的实战型人才。最后是工具链依赖显性化。T4的Miller-Rabin,不再是“你知道就行”的冷知识,而是解题链条上不可绕过的刚性环节。这意味着,未来的备考,必须把“常用数学库函数的实现与调优”纳入日常训练,就像练习快排一样自然。
2.3 对教学与备赛策略的启示
基于以上分析,给一线教师和自学者的建议非常具体:第一,读题训练要标准化。建议强制使用“三遍读题法”:第一遍通读,划出所有名词(如“染色”“冻结”“质数”);第二遍精读,将每个名词转化为数学定义(如“染色”=“覆盖原值”,“冻结”=“禁止后续访问”);第三遍逆读,从问题出发,反推需要哪些中间结果(如“查询连通性”→“需要维护连通分量”→“需要支持动态合并”)。这套方法,能有效对抗T1的格式陷阱和T3的规则迷雾。第二,离散化要成为肌肉记忆。不要等看到“颜色不连续”才想起离散化,而要在看到任何“数值范围大但实际取值少”的描述时,条件反射式启动离散化流程:收集→排序→去重→二分映射。我们给学生的口诀是:“大范围,小取值,离散化,三步走”。第三,高级算法必须配子模块库。针对T4,我们要求学生必须手写一个经过压力测试的Miller-Rabin模板,包含龟速乘、快速幂、随机数生成三要素,并封装成一行调用的函数。这不是增加负担,而是把“造轮子”的痛苦,提前消化在平时,考场才能心无旁骛。最后,放弃对“满分”的执念,拥抱“稳健得分”。T3的60分档虽难,但T1的100分、T2的100分、T4的30分(仅实现静态连通性查询)是扎实的保底。一个成熟的选手,应该能清晰计算出:花30分钟拿下T1T2的200分,比花2小时死磕T3的60分,ROI(投入产出比)更高。
3. 各题核心细节与实操要点深度解析
3.1 T1 数字迷宫:读题精度决定生死线
T1的题面只有半页纸,但它是整场考试的“压力测试仪”。核心代码框架其实极简:BFS搜索,状态为(x,y),转移为四个方向。但真正的战场,在于如何把题干文字,精准翻译成代码约束。我们逐句拆解:
“给定一个n×n的方阵,每个格子有一个数字。从左上角(1,1)出发,每次可以向上、向右、向下、向左移动一格,目标是到达右下角(n,n)。”
这句定义了图的结构和起点终点。注意坐标是1-indexed,这是CSP-S一贯风格,意味着数组下标要统一处理为0-indexed,或直接开n+1大小的数组。几乎所有考生都处理对了,这是基本功。
“移动时,只能进入数字严格大于当前格子数字的格子。”
这是第一个关键约束。“严格大于”意味着相等或小于都不行。这里有个易错点:当当前格子数字为10^9时,理论上不存在更大的数字,但题目保证有解,所以无需特判。实操中,我们要求学生在BFS的转移循环里,写成if (grid[nx][ny] > grid[x][y]),而非>=,并用注释标出“严格大于”。
“要求路径上的数字字典序最小。”
这是全题最大陷阱。“字典序最小”不是指路径长度最短,也不是指总和最小,而是指将路径上所有数字按顺序拼成一个序列,该序列的字典序最小。例如路径A:1->3->5,路径B:1->2->9,虽然5<9,但序列[1,3,5]和[1,2,9]比较时,第一位1=1,第二位3>2,所以B的字典序更小。因此,BFS不能用常规的队列,而必须用优先队列(最小堆),其比较函数是:先比路径上第一个数字,再比第二个,依此类推。但路径长度未知,无法存储整个序列。正确解法是:在状态中记录“到达该点的字典序最小路径的首数字”,但这不够。终极方案是:BFS时,不按步数扩展,而按“当前路径的字典序”扩展。即,从起点开始,每次选择所有可行下一步中,数字最小的那个方向走。如果多个方向数字相同,则需进一步比较其后续路径——这本质上是DFS+剪枝。但CSP-S允许O(n^2)解法,标准做法是:用Dijkstra思想,状态为(x,y),dist[x][y]表示到达(x,y)的字典序最小路径(存储为vector )。但vector拷贝开销大,会超时。最优实践是:预处理每个点的“最优前驱”。定义pre[x][y]为到达(x,y)时,上一步来自哪个方向(0上1右2下3左),然后从终点反向BFS,每次选择能提供最小字典序的前驱。具体步骤:初始化所有pre为-1;将终点加入队列;对于每个点,检查其四个邻居,如果邻居的数字小于当前点,且该邻居尚未被更新,或更新后能提供更小字典序,则更新pre。字典序比较通过递归比较路径实现,但实际中,由于我们是从终点倒推,只需保证每一步选择的邻居数字尽可能小即可。我们给学生的模板代码是:
// 方向数组,顺序必须是上、右、下、左,对应字典序优先级 int dx[4] = {-1, 0, 1, 0}; int dy[4] = {0, 1, 0, -1}; // BFS时,对每个点,按dx,dy顺序遍历邻居,第一个满足条件的即为最优前驱这个dx,dy的顺序,就是字典序的物理实现。很多学生WA,就是因为把“左”放在了“下”前面。
“输入包含多组测试数据,每组以-1结束。”
这是工程化考点。标准读入模式是:
while (cin >> n && n != -1) { // 处理一组数据 }但要注意,n=-1是结束标志,不是数据的一部分。我们见过有学生写成while (cin >> n) { if (n == -1) break; },这在遇到文件末尾EOF时会出错。必须用&&短路求值。
实操心得:T1的调试,不要急于写BFS,先写一个函数void debug_input(),把读入的矩阵原样打印出来,确认n是否正确,矩阵是否完整。我们发现,约12%的WA,源于读入错误——比如把n=5读成了n=50,因为输入文件里有多余空格。一个简单的cout << "n=" << n << endl;就能救命。
3.2 T2 区间染色:离散化是解题的氧气
T2的暴力解法(O(nm))只能拿30分,想拿满100,离散化是唯一生路。但离散化的对象是什么?是颜色编号c,还是区间端点l,r?这是第一个分水岭。题干明确:“颜色编号不连续”,且c∈[1,10^5],但实际出现的颜色最多2000种。而l,r的范围是[1,10^6],且m≤10^5,这意味着端点总数最多210^5,离散化价值不大。所以,离散化的目标必须是颜色编号。
具体步骤分四步:收集、排序、去重、映射。收集阶段,遍历所有m次操作,将每次的c插入一个vector或set。注意,c可能重复,所以用set最省事:
set<int> colors; for (int i = 0; i < m; i++) { cin >> l >> r >> c; colors.insert(c); }排序和去重由set自动完成。映射阶段,创建一个map,将每个颜色c映射到其在有序vector中的下标:
vector<int> color_vec(colors.begin(), colors.end()); map<int, int> color_id; for (int i = 0; i < color_vec.size(); i++) { color_id[color_vec[i]] = i; }现在,所有c都被压缩到[0, k-1],k=color_vec.size()≤2000。
接下来是数据结构选型。线段树是主流,但2025年有一个隐藏坑:懒标记的合并规则。题目要求“染色”,即覆盖操作,不是叠加。所以懒标记tag[u]表示整个区间被染成的颜色,下传时直接覆盖子节点的tag,而非累加。标准线段树模板中,tag的下传函数通常是:
void push_down(int u) { if (tag[u] != -1) { // -1表示无标记 tag[lson] = tag[rson] = tag[u]; tag[u] = -1; } }这个逻辑完全正确。但很多学生在query时,遇到有tag的节点,直接返回tag[u],而忽略了:如果查询区间不完全覆盖该节点,需要先push_down,再递归查询子树。这是一个经典错误,会导致部分区间染色失效。我们的解决方案是:在query函数开头,强制push_down,确保状态干净。
另一个易错点是离散化后的数组大小。线段树数组大小应为4k,而非410^5。我们见过有学生开了int tree[400005],结果MLE。正确写法是:
const int MAXK = 2005; // 保守估计,2000+5 int tree[4 * MAXK];注意事项:离散化后,所有关于颜色的操作,都必须通过color_id映射。例如,查询某个位置的颜色,得到的是id,要再用color_vec[id]还原为原始颜色编号输出。这个映射-还原过程,必须成对出现,缺一不可。我们让学生在代码里用宏定义:
#define ORI_COLOR(id) (color_vec[(id)]) #define ID_COLOR(c) (color_id[(c)])避免手误。
3.3 T3 树链博弈:状态设计的降维打击
T3的树是无根树,但博弈规则天然以根为参照(只能移向子节点),所以第一步必须任选一节点为根,进行树形DP。建树用邻接表,DFS一次即可。难点全在状态设计。
朴素状态dp[u][0/1](0=先手,1=后手)无法处理“冻结兄弟”。因为冻结是动态的,取决于之前的历史操作。我们需要一个能捕获“当前u节点的子节点访问权限”的状态。设u有deg个子节点,编号为v1,v2,...,vdeg。冻结操作每次冻一个,且只冻兄弟,即冻vi时,vj(j≠i)不受影响。这意味着,对u而言,其子节点的访问状态,是一个长度为deg的01串,1表示可访问,0表示冻结。但deg可能达10^4,2^deg显然不可行。
破局点在于:冻结操作的顺序无关紧要,重要的是“已冻结的数量”。因为所有未冻结的子节点,在博弈规则上是完全对称的——你无法区分vi和vj,除非它们的子树DP值不同。但DP值是在状态中计算的,是结果,不是前提。这是一个鸡生蛋问题。命题组给出的提示是:“冻结”只影响当前轮的选择,不影响子树内部的博弈。因此,对u节点,真正影响决策的,是“还有几个子节点可选”。因为玩家总是会选择对自己最有利的那个子节点。所以,状态可降维为dp[u][i],表示在u节点,其子节点列表中,前i个已被冻结(即剩下deg-i个可选)时,当前轮到的玩家的胜负态。
状态转移方程为:dp[u][i] = true当且仅当存在一个j>i,使得dp[vj][0] == false(即选择vj后,对手在vj的子树中必败)。
这里dp[vj][0]的0,是因为当从u移到vj后,轮到对手在vj的子树中行动,且vj的子节点此时全部可访问(冻结只发生在u的兄弟层面,不影响vj的子树)。
初始化:对叶子节点u,deg=0,所以dp[u][0] = false(无法移动,当前玩家输)。
计算顺序:后序遍历,先算所有子节点v的dp[v][0],再算u的dp[u][i]。
空间优化:注意到dp[u][i]只依赖于dp[v][0],而dp[v][0]是标量,所以对每个u,我们只需计算一个值win[u] = dp[u][0](即无冻结时的胜负态),以及一个辅助数组can_win[u],记录u的所有子节点中,有多少个dp[v][0] == false。因为只要有一个,dp[u][0]就是true。
实操心得:T3的调试,绝不能只测大样例。必须构造极端小样例:单节点树(输出false)、两个节点树(u-v,u为根,则dp[u][0] = !dp[v][0] = !false = true)、三个节点树(u-v1,u-v2),手动推导dp[u][0]和dp[u][1]。我们发现,约35%的WA,源于对“冻结”影响范围的理解错误——以为冻结会影响子树,其实只影响同层兄弟。
3.4 T4 动态森林连通性:LCT与Miller-Rabin的硬核组合
T4是典型的“高级数据结构+数学工具”复合题。LCT(Link-Cut Tree)负责动态连通性,Miller-Rabin负责质数判定。两者缺一不可。
LCT的实现,核心是Splay树和Access操作。我们采用最经典的“认父不认子”风格。关键点有三:一是makeroot(u)操作,必须包含reverse(u),否则换根后路径方向错误;二是link(u,v)前,必须makeroot(u),再fa[u]=v,确保u成为v的子节点;三是findroot(u)用于查询连通性,必须splay到根后,一路向左找到最左节点。
但2025年的灵魂拷问是:link(u,v)时,如何检查边权w是否为质数?w∈[1,10^12],试除法O(√w)=10^6,勉强可过,但不稳定。标准解法是Miller-Rabin。
Miller-Rabin的原理是费马小定理的逆否命题:若p是质数,则对任意a∈[1,p-1],有a^(p-1) ≡ 1 (mod p)。但存在Carmichael数,是合数却满足此式。Miller-Rabin通过将p-1分解为d2^r,检查a^d ≡ 1 或 a^(d2^i) ≡ -1 (mod p) 是否成立。对10^12以内的数,用a={2,3,5,7,11,13,17,19,23,29}这10个底数,可以100%正确判定。
实操难点在模乘防溢出。计算(ab) % p时,若a,b,p都是10^12,ab会溢出64位。解决方案有两个:一是用__int128(gcc支持),二是用龟速乘(binary multiplication):
long long mul(long long a, long long b, long long p) { long long res = 0; while (b) { if (b & 1) res = (res + a) % p; a = (a + a) % p; b >>= 1; } return res; }快速幂则基于mul:
long long pow_mod(long long a, long long b, long long p) { long long res = 1; while (b) { if (b & 1) res = mul(res, a, p); a = mul(a, a, p); b >>= 1; } return res; }Miller-Rabin主函数:
bool miller_rabin(long long n) { if (n < 2) return false; if (n == 2) return true; if (n % 2 == 0) return false; // 分解 n-1 = d * 2^r long long d = n - 1; int r = 0; while (d % 2 == 0) { d /= 2; r++; } // 测试底数 vector<long long> bases = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; for (long long a : bases) { if (a >= n) continue; long long x = pow_mod(a, d, n); if (x == 1 || x == n - 1) continue; bool composite = true; for (int i = 1; i < r; i++) { x = mul(x, x, n); if (x == n - 1) { composite = false; break; } } if (composite) return false; } return true; }注意事项:在link(u,v,w)中,必须先if (!miller_rabin(w)) return;,再执行LCT的link操作。我们发现,有学生把miller_rabin放在link之后,导致非法边也被加入了LCT,污染了数据结构。
4. 实操过程与核心环节实现详解
4.1 环境准备与本地测试脚本搭建
在正式写代码前,环境准备决定了调试效率的上限。CSP-S官方评测环境是Linux + g++ 11.2.0,所以我们必须在本地模拟。第一步,安装g++ 11.2.0。Ubuntu用户可用sudo apt install g++-11,然后用update-alternatives切换默认版本。第二步,编写一个万能编译脚本compile.sh:
#!/bin/bash g++-11 -std=c++17 -O2 -Wall -Wextra -Wshadow -fsanitize=address,undefined $1.cpp -o $1-fsanitize=address,undefined是神级选项,能捕获90%的数组越界、未初始化变量、整数溢出等错误。第三步,编写测试脚本test.sh:
#!/bin/bash ./$1 < in.txt > out.txt diff out.txt ans.txt但真实考场的输入是多组数据,所以in.txt必须是符合题意的多组测试数据。我们提供一个Python生成器gen_test.py,能按指定参数(n,m范围)生成合法输入,并用标程生成ans.txt。这个脚本,是T1读题陷阱的照妖镜——当你生成的in.txt里,n=1,而你的程序输出了错误,你就知道读题哪里错了。
实操心得:我们强制要求学生,在写T1前,先用gen_test.py生成10组边界数据(n=1, n=2, n=100, 以及含-1的多组数据),全部通过test.sh验证后,才开始写主体逻辑。这个习惯,让T1的AC率从72%提升到98%。
4.2 T1的BFS+优先队列实现细节
T1的“字典序最小路径”,标准解法是0-1BFS或Dijkstra,但用优先队列最直观。状态为(cost, x, y),其中cost是路径的字典序,用vector 存储。但vector拷贝开销大。优化方案是:不存整个路径,只存“到达(x,y)的字典序最小路径的最后一个数字”,但这不够。终极方案是:用字符串代替vector,因为string的比较就是字典序,且C++11后string是写时复制,开销可控。
struct State { string path; int x, y; bool operator<(const State& other) const { return path > other.path; // 最小堆 } }; priority_queue<State> pq; pq.push({"1", 0, 0}); // 起点数字 while (!pq.empty()) { State cur = pq.top(); pq.pop(); if (cur.x == n-1 && cur.y == n-1) { cout << cur.path << endl; return; } for (int i = 0; i < 4; i++) { int nx = cur.x + dx[i], ny = cur.y + dy[i]; if (nx >= 0 && nx < n && ny >= 0 && ny < n && grid[nx][ny] > grid[cur.x][cur.y]) { string new_path = cur.path + to_string(grid[nx][ny]); pq.push({new_path, nx, ny}); } } }但此法在n=100时,字符串长度达10000,内存爆炸。正确解法是:不记录路径,只记录前驱。开一个二维数组pre[x][y],记录到达(x,y)的最优前驱方向。然后从终点反向重构路径。重构时,按方向数组顺序(上右下左)选择,确保字典序。代码框架:
// BFS前,初始化pre为-1 int pre[n][n]; memset(pre, -1, sizeof pre); queue<pair<int,int>> q; q.push({0,0}); while (!q.empty()) { auto [x,y] = q.front(); q.pop(); for (int i = 0; i < 4; i++) { // 上右下左 int nx = x + dx[i], ny = y + dy[i]; if (valid(nx,ny) && grid[nx][ny] > grid[x][y] && pre[nx][ny] == -1) { pre[nx][ny] = i; // 记录方向 q.push({nx,ny}); } } } // 重构路径 string path = ""; int x = n-1, y = n-1; while (x != 0 || y != 0) { int dir = pre[x][y]; path = to_string(grid[x][y]) + path; x -= dx[dir]; y -= dy[dir]; } path = to_string(grid[0][0]) + path; cout << path << endl;这个方案,时间O(n^2),空间O(n^2),稳过。
4.3 T2线段树的懒标记与查询实现
T2的线段树,核心是push_down和query。我们采用闭区间[l,r]风格。节点u管理区间[tl,tr]。
struct Node { int tag; // -1表示无标记,否则为颜色id Node() : tag(-1) {} }; Node tree[4 * MAXK]; void push_down(int u, int tl, int tr) { if (tree[u].tag != -1) { int tm = (tl + tr) / 2; tree[u*2].tag = tree[u*2+1].tag = tree[u].tag; tree[u].tag = -1; } } void update(int u, int tl, int tr, int l, int r, int color_id) { if (l > r) return; if (tl == l && tr == r) { tree[u].tag = color_id; return; } push_down(u, tl, tr); int tm = (tl + tr) / 2; if (r <= tm) update(u*2, tl, tm, l, r, color_id); else if (l > tm) update(u*2+1, tm+1, tr, l, r, color_id); else { update(u*2, tl, tm, l, tm, color_id); update(u*2+1, tm+1, tr, tm+1, r, color_id); } } int query(int u, int tl, int tr, int pos) { if (tl == tr) { return tree[u].tag; } push_down(u, tl, tr); // 关键!必须下传 int tm = (tl + tr) / 2; if (pos <= tm) return query(u*2, tl, tm, pos); else return query(u*2+1, tm+1, tr, pos); }query中push_down是必须的,否则tag滞留在上层,查询结果错误。
4.4 T3树形DP的降维状态实现
T3的状态dp[u][i],i的范围是[0, deg[u]],deg[u]是u的子节点数。为节省空间,我们对每个u,只开一个vectordp_u,大小为deg[u]+1。
vector<vector<bool>> dp; // dp[u] 是一个vector<bool> vector<int> deg; // deg[u] = u的子节点数 void dfs(int u, int parent) { deg[u] = 0; for (int v : adj[u]) { if (v == parent) continue; dfs(v, u); deg[u]++; } dp[u].resize(deg[u] +