题解: [GESP202409 八级] 美丽路径

上次,我考过了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)的暴力算法,适合新手。

选一条路径,这条路径一定会有:

  1. 起点
  2. 终点
  3. 长度

其实可能还有更多,但这三个是在本题中最关键的。

想一想,如果知道起点,如何知道剩下的两个?

没错,就是通过DFS枚举终点,记录下中间的长度。

那么非常简单,思路就是这样。

  1. 输入数据
  2. 枚举起点
  3. DFS穷举路径,得到每个路径的终点和长度(如果遇到相邻节点相等就不能继续往下DFS了)
  4. 将合法路径长度打擂台取最大,最后输出结果

(只能说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计划。

绘图工具:Canva可画

点此查看

不管是这三条简单路径中的哪一条,凡经过的节点当中,深度最小(处在位置最高)的点只有一个(否则最高那层的多个节点最近公共祖先一定比这些节点高,产生矛盾)。

那么,知道了唯一的点,接下来就是要在下面的多叉枝杆里面找出两叉(或一条链/不找),求合法路径的最大长度。

很明显,如果这个最高点确定下来,那么路径上它的子节点往下的路径上的节点一定形成一条链。

这一点大家可能不明白。

例如图上画的路径, 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字数就会瞬间爆炸。

结论:

  1. 对于每个节点作为路径上最高点,它只能向子节点延申 0 ∼ 2 0\sim2 0∼2个叉。
  2. 凡在路径上是作为另外一个路径上节点的子节点的节点,其向下只能形成一条链。
  3. 满足上述条件的同时,还要做到相邻节点颜色不同。
  4. 求合法路径的最大长度

树形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八级路上的一个小怪,打败后前进的路就会开放。

相关推荐
今儿敲了吗1 小时前
链表篇(五)——链表中间结点
数据结构·笔记·算法·链表
码农的神经元1 小时前
2026 年数维杯A 题:抱轨式磁浮列车的悬浮电磁铁故障检测问题
人工智能·算法·数学建模
YYYing.1 小时前
【C++项目之高并发内存池 (三)】万字解析CentralCache与PageCache的初步实现
c++·笔记·哈希算法·高并发·c/c++·内存池
gumichef1 小时前
栈和队列(1)
开发语言·数据结构
小新同学^O^1 小时前
算法学习 --> 快速输入和输出
java·学习·算法
脑子加油站1 小时前
K8S-Ingress资源对象
算法·贪心算法·k8s
Chase_______2 小时前
【算法】LeetCode 1052 & 3679:定长滑动窗口进阶——增益最大化与频率约束贪心
算法·leetcode
天若有情6732 小时前
从零搭建局域网手机遥控电脑网页项目,吃透工程化与架构设计思维
服务器·前端·数据库·算法·开源·node·工程化
凯瑟琳.奥古斯特2 小时前
力扣1367:二叉树中查找链表路径
数据结构·算法·leetcode·链表