上次,我考过了GESP六级,而且是 98 98 98分,原先是 85.5 85.5 85.5,后来官方改成 98 98 98的。
也就是说选择题错了一道,判断、编程全对。
于是准备在 26.6 26.6 26.6拿下八级。
今日选题:[GESP202409 八级] 美丽路径。
考察知识点:树形DP、深搜DFS。
P11251 [GESP202409 八级] 美丽路径
题目背景
对应的选择、判断题:https://ti.luogu.com.cn/problemset/1164
题目描述
小杨有一棵包含 n n n 个节点的树,节点从 1 1 1 到 n n n 编号,并且每个节点要么是白色,要么是黑色。
对于树上的一条简单路径(不经过重复节点的路径),小杨认为它是美丽 的当且仅当路径上相邻节点的颜色均不相同。例如下图,其中节点 1 1 1 和节点 4 4 4 是黑色,其余节点是白色,路径 2 − 1 − 3 − 4 2-1-3-4 2−1−3−4 是美丽路径,而路径 2 − 1 − 3 − 5 2-1-3-5 2−1−3−5 不是美丽路径(相邻节点 3 3 3 和 5 5 5 颜色相同)。

对于树上的一条简单路径,小杨认为它的长度是路径包含节点的数量。小杨想知道最长的美丽路径的长度是多少。
输入格式
第一行包含一个正整数 n n n,代表节点数量。
第二行包含 n n n 个整数 c 1 , c 2 , ... , c n c_1,c_2,\dots,c_n c1,c2,...,cn,代表每个节点的颜色,如果 c i = 0 c_i=0 ci=0,代表节点 i i i 为白色,如果 c i = 1 c_i=1 ci=1,代表节点 i i i 为黑色。
之后 n − 1 n-1 n−1 行,每行包含两个正整数 u i , v i u_i,v_i ui,vi,代表存在一条连接节点 u i u_i ui 和节点 v i v_i vi 的边。
输出格式
输出一个整数,代表最长美丽路径的长度。
输入输出样例 #1
输入 #1
Sample
5
1 0 0 1 0
1 2
3 5
4 3
1 3
输出 #1
Sample
4
输入输出样例 #2
输入 #2
Sample
5
0 0 0 0 0
1 2
2 3
3 4
4 5
输出 #2
Sample
1
说明/提示
| 子任务编号 | 数据点占比 | n n n | 特殊条件 |
|---|---|---|---|
| 1 1 1 | 30 % 30\% 30% | ≤ 1000 \leq 1000 ≤1000 | 树的形态是一条链 |
| 2 2 2 | 30 % 30\% 30% | ≤ 1000 \leq 1000 ≤1000 | |
| 3 3 3 | 40 % 40\% 40% | ≤ 10 5 \leq 10^5 ≤105 |
对于全部数据,保证有 1 ≤ n ≤ 10 5 , 0 ≤ c i ≤ 1 1 \leq n \leq 10^5,0 \leq c_i \leq 1 1≤n≤105,0≤ci≤1,同时保证给出的数据构成一棵树。
大致题意
给你一棵有 n n n个节点的有根树,每个节点的颜色为 c i ∈ { 0 , 1 } c_i\in\{0,1\} ci∈{0,1},在树中找一条路径,使得这条路径上相邻两个节点的颜色不同,求这条路径的长度最大是多少?
审题提炼
根据上一段,我们已经知道:
- 已知:一棵有 n n n个节点的有根树,每个节点的颜色为 c i ∈ { 0 , 1 } c_i\in\{0,1\} ci∈{0,1}
- 做什么:在树中找一条路径
- 限制条件:这条路径上相邻两个节点的颜色不同
- 求什么:这条路径的最大长度
暴力解法(60pts)
很明显,对于 60 % 60\% 60%的数据, 1 ≤ n ≤ 1000 1\le n\le1000 1≤n≤1000,此时建议使用 O ( n 2 ) O(n^2) O(n2)的暴力算法,适合新手。
选一条路径,这条路径一定会有:
- 起点
- 终点
- 长度
其实可能还有更多,但这三个是在本题中最关键的。
想一想,如果知道起点,如何知道剩下的两个?
没错,就是通过DFS枚举终点,记录下中间的长度。
那么非常简单,思路就是这样。
- 输入数据
- 枚举起点
- DFS穷举路径,得到每个路径的终点和长度(如果遇到相邻节点相等就不能继续往下DFS了)
- 将合法路径长度打擂台取最大,最后输出结果
(只能说CSDN的Markdown有亿点拉,好好的 1 , 2 , 3 1,2,3 1,2,3换行时变成了 4 , 5 , 6 4,5,6 4,5,6,不过只需要把第一个改为 1 1 1后续就会自动排列,希望CSDN官方能修复一下)

什么?你连树都不知道,你对DFS一无所知?
那你还考个(第十六个)八级!
暴力代码-拆解
输入
cpp
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>c[i]; //读颜色
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y; //读边
g[x].push_back(y); //祖传规定 如果题目没说从x到y 就建双向边+DFS传两个参
g[y].push_back(x);
}
循环穷举起点
cpp
for(int i=1;i<=n;i++) //循环枚举
{
dfs(i,i,1); //以后会定义 初始长度为1
}
输出
cpp
cout<<ans<<endl;
return 0;
重头戏:DFS
注意有全局数组
cpp
vector<int>g[100009]; //也许能得60+
int c[100009],ans; //颜色&答案
然后就是
cpp
void dfs(int i,int fa,int len) //第三个参数表示长度
{
ans=max(ans,len); //打擂台取最大
for(int k:g[i]) //新版C++写法 也可以写for(int k=0;k<g[i].size();k++) 但是复杂而且循环里面也要改
{
if(k!=fa) //依旧判断爸爸的爸爸叫儿子
{
if(c[i]!=c[k])dfs(k,i,len+1); //这里长度要+1
//除非你们两个在一起一点都不合适 那就别往下走了
}
}
}
暴力代码(C++)
cpp
#include<bits/stdc++.h> //老演员了
using namespace std; //how old are you
vector<int>g[100009]; //也许能得60+
int c[100009],ans; //颜色&答案
void dfs(int i,int fa,int len) //第三个参数表示长度
{
ans=max(ans,len); //打擂台取最大
for(int k:g[i]) //新版C++写法 也可以写for(int k=0;k<g[i].size();k++) 但是复杂而且循环里面也要改
{
if(k!=fa) //依旧判断爸爸的爸爸叫儿子
{
if(c[i]!=c[k])dfs(k,i,len+1); //这里长度要+1
//除非你们两个在一起一点都不合适 那就别往下走了
}
}
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>c[i]; //读颜色
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y; //读边
g[x].push_back(y); //祖传规定 如果题目没说从x到y 就建双向边+DFS传两个参
g[y].push_back(x);
}
for(int i=1;i<=n;i++) //循环枚举
{
dfs(i,i,1); //以后会定义 初始长度为1
}
cout<<ans<<endl;
return 0;
}
样例过了


提交...
W T F?????????

好吧这数据真是拉爆了。
是的我又换头像了,这次是Phigros All Perfect的标志,从Phira-Render里面提取出来的。

正解(100pts)
我就以我喜欢的方式讲,尽管方法有很多种。
其实树上的一条简单路径不过就是一个倒V或斜线。
1
2
3
4
5
6
7
下面是绘图代码,真实语言是小写的mermaid。
MERMAID
flowchart TD
id1((1))
id2((2))
id1-->id2
id3((3))
id1-->id3
id4((4))
id2-->id4
id5((5))
id2-->id5
id6((6))
id3-->id6
id7((7))
id3-->id7
这里 1 − 7 1-7 1−7的路径 1 − 3 − 7 1-3-7 1−3−7(较长线用于强调路径):
1
/
3
/
/
/
7
1 − 4 1-4 1−4的路径 1 − 2 − 4 1-2-4 1−2−4(由于题目没说二叉树 所以树的结构可能有所打乱):
1
2
/
4
/
/
/
4 − 7 4-7 4−7的路径 4 − 2 − 1 − 3 − 7 4-2-1-3-7 4−2−1−3−7(为了强调顺序):
1
2
3
4
/
/
/
这mermaid也是真肺雾。
启动B计划。
不管是这三条简单路径中的哪一条,凡经过的节点当中,深度最小(处在位置最高)的点只有一个(否则最高那层的多个节点最近公共祖先一定比这些节点高,产生矛盾)。
那么,知道了唯一的点,接下来就是要在下面的多叉枝杆里面找出两叉(或一条链/不找),求合法路径的最大长度。
很明显,如果这个最高点确定下来,那么路径上它的子节点往下的路径上的节点一定形成一条链。
这一点大家可能不明白。
例如图上画的路径, 1 1 1的子节点里在路径上的有 2 , 3 2,3 2,3,而 2 2 2往下的在路径上的节点有 3 3 3, 2 , 3 2,3 2,3形成一条链; 3 3 3往下的在路径上的节点有 7 7 7, 3 , 7 3,7 3,7形成一条链,但 1 , 2 , 3 , 4 , 7 1,2,3,4,7 1,2,3,4,7实际上是树状结构。
明白了吗?
举一个错误的例子:我特别喜欢树状结构,我直接使用路径 5 − 2 − 4 − 2 − 1 − 3 − 6 − 3 − 7 5-2-4-2-1-3-6-3-7 5−2−4−2−1−3−6−3−7,确实遍历到了所有点,确实是路径,目前先假设颜色的事不用考虑,由于不是简单路径,所以根本不合法。
再举一个:这回我来到了一棵三叉树的顶端,根正好有 3 3 3个儿子,我能不能将根连带着三个儿子一起作为答案呢?不行。因为你无法一笔画出大写字母T,像TT(四个儿子),TTT(五个儿子)就更不行了,更何况还带上孙子等节点。
来来来CSDN你什么意思

刷新就好,但凡再打一个字HTML字数就会瞬间爆炸。
结论:
- 对于每个节点作为路径上最高点,它只能向子节点延申 0 ∼ 2 0\sim2 0∼2个叉。
- 凡在路径上是作为另外一个路径上节点的子节点的节点,其向下只能形成一条链。
- 满足上述条件的同时,还要做到相邻节点颜色不同。
- 求合法路径的最大长度
树形DP的引入
上述结论相当完整,但是实现的时候还是 O ( n 2 ) O(n^2) O(n2),根本无法做到真正一样上的 O ( 1 ) O(1) O(1)查询。
首先,对于每个最高节点,其答案一定是由次高节点推出来的。
但是最终求的是允许多叉答案,可子节点不允许多叉。
这怎么办?
你猜一猜!
这是以前GESP六级样题:亲朋数的代码。
cpp
#include<bits/stdc++.h>
using namespace std;
long long p[129],m[129];
int main()
{
int p;
string s;
cin>>p>>s;
long long ans=0;
for(int i=0;i<s.length();i++)
{
int si=s[i]-'0';
for(int j=0;j<p;j++)
{
::p[j]=m[j];
m[j]=0;
}
for(int j=0;j<p;j++)
{
m[(j*10+si)%p]+=::p[j];
}
m[si%p]++;
ans+=m[0];
}
cout<<ans;
}
你发现了什么?
没错,这里用了两个数组!
那这道题里也可以使用两个DP数组!
DP构思
搬运远古DP构思过程

以什么划分阶段?
太简单了,树形DP,几乎永远以节点划分阶段 。
但是注意要自底向上求解。
决策是什么?
因为是自底向上求解,所以是和子节点有关联。
那么很明显了,最多允许二叉,那就是选哪两个子节点作为多叉路径中的节点 。
但如果不允许二叉,就是选哪一个子节点作为链状路径中的节点 。
注:这里最大化选择节点数目就是因为结果要取最大。
状态如何描述?
允许二叉: f 1 ( i ) f1(i) f1(i)表示以 i i i作为最高节点的合法简单多叉路径中的最大长度 。
不允许二叉: f 2 ( i ) f2(i) f2(i)表示以 i i i作为最高节点的合法简单链状路径中的最大长度。
当前状态去掉最后一步的决策,能由哪些状态推来?
不允许二叉:为了最大化答案,我们对于 f 2 ( i ) f2(i) f2(i)会选择其子节点对应 f 2 f2 f2值最大的一个(前提是颜色不同),然后再执行 + 1 +1 +1的连接操作。
而允许二叉:为了最大化答案,我们对于 f 1 ( i ) f1(i) f1(i)会选择其子节点对应 f 1 f1 f1值最大的两个(前提是颜色不同)之和(当然如果只能选出一个就只选一个),然后再执行 + 1 +1 +1的连接操作。
因此------
允许二叉:对于 f 1 ( i ) f1(i) f1(i),设子节点序列为 S i 1 , S i 2 , ⋯ , S i cntSon i S_{i_1},S_{i_2},\cdots,S_{i_{\text{cntSon}i}} Si1,Si2,⋯,SicntSoni,则 f 1 ( i ) f1(i) f1(i)会由 f 2 ( S x ) f2(S_x) f2(Sx)和 f 2 ( S y ) f2(S_y) f2(Sy)推来,其中 1 ≤ x < y ≤ cntSon i , c S i x ≠ c i , c S i y ≠ c i 1\le x<y\le \text{cntSon}i,c{S{i_x}}\neq c_i,c_{S_{i_y}}\neq c_i 1≤x<y≤cntSoni,cSix=ci,cSiy=ci (如果无法选出合法的 ⟨ x , y ⟩ \langle x,y\rangle ⟨x,y⟩数对,则 f 1 ( i ) f1(i) f1(i)和 f 2 ( i ) f2(i) f2(i)等价,即 f 1 ( i ) = f 2 ( i ) f1(i)=f2(i) f1(i)=f2(i))
不允许二叉:对于 f 2 ( i ) f2(i) f2(i),设子节点序列为 S i 1 , S i 2 , ⋯ , S i cntSon i S_{i_1},S_{i_2},\cdots,S_{i_{\text{cntSon}_i}} Si1,Si2,⋯,SicntSoni,则 f 2 ( i ) f2(i) f2(i)会由 f 2 ( S x ) f2(S_x) f2(Sx)推来,其中 1 ≤ x ≤ cntSon i , c S x ≠ c i 1\le x\le \text{cntSon}i,c{S_x}\neq c_i 1≤x≤cntSoni,cSx=ci (如果无法选出合法的 x x x,则 f 2 ( i ) = 1 f2(i)=1 f2(i)=1,表示必须新开一条路径。)
方程?
坏了是方程我们没救了
这里要列出两个状态转移方程。
用代码实现比用数学语言描述要简单,像 cntSon \text{cntSon} cntSon那东西就是g[i].size()。
但是严格遵守

完了是多行 LaTeX \LaTeX LATEX我们死定了
f 1 ( i ) = max { f 2 ( x ) + f 2 ( y ) 1 ≤ x < y ≤ cntSon i , c S i x ≠ c i , c S i y ≠ c i } + 1 f 2 ( i ) = max { f 2 ( x ) 1 ≤ x ≤ cntSon i , c S i x ≠ c i } + 1 f1(i)=\max\begin{Bmatrix} f2(x)+f2(y)&1\le x<y\le\text{cntSon}i,c{S_{i_x}}\neq c_i,c_{S_{i_y}}\neq c_i \end{Bmatrix}+1\\ f2(i)=\max\begin{Bmatrix} f2(x)&1\le x\le\text{cntSon}i,c{S_{i_x}}\neq c_i \end{Bmatrix}+1 f1(i)=max{f2(x)+f2(y)1≤x<y≤cntSoni,cSix=ci,cSiy=ci}+1f2(i)=max{f2(x)1≤x≤cntSoni,cSix=ci}+1
但问题是,枚举 ⟨ l , r ⟩ \langle l,r\rangle ⟨l,r⟩的总时间复杂度约为 O ( n 2 ) O(n^2) O(n2),会超时。
不过,只要知道合法的子节点,就可以使用特殊方法求得 Top 2 \text{Top 2} Top 2进一步求出 f 1 ( i ) f1(i) f1(i)。
这里的特殊方法:
cpp
int fmx=0,smx=0; //first max/second max
for(int i:vec)
{
//假设i是新加入的数
if(i>fmx) //夯爆了
{
smx=fmx; //原来的smx下场 原来的fmx降级为smx i登上fmx宝座
fmx=i;
}
else if(i>smx) //人上人
{
smx=i;
}
//else 拉完了
}
//最后求得了最大和次大
写正式代码的时候参与比较的i改为f2[i]。
求解顺序:后根遍历递归求子节点,然后再求f1[i],f2[i],最后求的这两个值顺序不限。
正解代码拆分
输入完全一致。
全局变量&数组
在原基础上添加f1,f2。
cpp
vector<int>g[100009];
int c[100009],f1[100009],f2[100009]; //颜色&答案
核心DFS
cpp
void dfs(int i,int fa) //祖传DFS
{
int fmx=0,smx=0; //必须初始化成0
for(int k:g[i]) //依旧C++11写法
{
if(k!=fa) //依旧禁止反向认亲
{
dfs(k,i); //递归
if(c[i]!=c[k])
{
//调用Top2代码模板
if(f2[k]>fmx) //夯爆了
{
smx=fmx; //降级
fmx=f2[k]; //帝王座位
}
else if(f2[k]>smx) //人上人
{
smx=f2[k];
}
//else 拉完了
}
}
}
f1[i]=fmx+smx+1; //允许多叉就是好 最大次大都加上
f2[i]=fmx+1; //不允许多叉只能加最大
//二者都有1
}
输出*max_element(f1+1,f1+n+1)即可,表示f1中的最大值,需要使用algorithm头文件(除非你用了万能头)。
100pts完整代码
cpp
#include<bits/stdc++.h>
using namespace std;
vector<int>g[100009];
int c[100009],f1[100009],f2[100009]; //颜色&答案
void dfs(int i,int fa) //祖传DFS
{
int fmx=0,smx=0; //必须初始化成0
for(int k:g[i]) //依旧C++11写法
{
if(k!=fa) //依旧禁止反向认亲
{
dfs(k,i); //递归
if(c[i]!=c[k])
{
//调用Top2代码模板
if(f2[k]>fmx) //夯爆了
{
smx=fmx; //降级
fmx=f2[k]; //帝王座位
}
else if(f2[k]>smx) //人上人
{
smx=f2[k];
}
//else 拉完了
}
}
}
f1[i]=fmx+smx+1; //允许多叉就是好 最大次大都加上
f2[i]=fmx+1; //不允许多叉只能加最大
//二者都有1
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>c[i]; //读颜色
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y; //读边
g[x].push_back(y); //祖传规定 如果题目没说从x到y 就建双向边+DFS传两个参
g[y].push_back(x);
}
dfs(1,0); //就以1为根
cout<<*max_element(f1+1,f1+n+1)<<endl;
return 0;
}
样例测试过了


提交AC!

总结
这是一道难度较大的树形DP题目(因为分析难),不过逐步拆解并不难理解。
是挑战GESP八级路上的一个小怪,打败后前进的路就会开放。
