【AcWing】蓝桥杯集训每日一题Day23|树形DP|字典序最小3465.病毒溯源(C++)

3465.病毒溯源
3465. 病毒溯源 - AcWing题库
难度:中等
时/空限制:0.4s / 64MB
总通过数:2108
总尝试数:4379
来源: CCCC天梯赛L2-038
算法标签 树形DP求方案

题目内容

病毒容易发生变异。

某种病毒可以通过突变产生若干变异的毒株,而这些变异的病毒又可能被诱发突变产生第二代变异,如此继续不断变化。

现给定一些病毒之间的变异关系,要求你找出其中最长的一条变异链。

在此假设给出的变异都是由突变引起的,不考虑复杂的基因重组变异问题 ------ 即每一种病毒都是由唯一的一种病毒突变而来,并且不存在循环变异的情况。

输入格式

输入在第一行中给出一个正整数 N,即病毒种类的总数。于是我们将所有病毒从 0 到 N−1 进行编号。

随后 N 行,每行按以下格式描述一种病毒的变异情况:
k 变异株1 ...... 变异株k

其中 k 是该病毒产生的变异毒株的种类数,后面跟着每种变异株的编号。第 i 行对应编号为 i 的病毒(0≤i<N)。题目保证病毒源头有且仅有一个。

输出格式

首先输出从源头开始最长变异链的长度。

在第二行中输出从源头开始最长的一条变异链,编号间以 1 个空格分隔,行首尾不得有多余空格。如果最长链不唯一,则输出最小序列。

注:我们称序列 {a1,...,an} 比序列 {b1,...,bn} "小",如果存在 1≤k≤n 满足 ai=bi 对所有 i<k 成立,且 ak<bk。

数据范围

1≤N≤10^4

输入样例:
复制代码
10
3 6 4 8
0
0
0
2 5 9
0
1 7
1 2
0
2 3 1
输出样例:
复制代码
4
0 4 9 1
题目解析

一开始是一个病毒,有可能会变异成多个不同的病毒,变异的病毒还可能变异成新的病毒

保证了变异的方式是一颗树,不会发生交叉变异和循环变异,一定是从父节点向子节点变异

在整个树中从根节点开始的最长的一条路径,并且把路径输出

如果有多个方案的话,输出字典序最小的方案

数据范围是10000

时间复杂度只需要在 O ( n log ⁡ n ) O(n\log n) O(nlogn)

要分成两步来看
怎么去求最大值

可以看成递归问题或简单的树形DP问题

考虑某一个局部,从当前节点开始,如果往下搜的话,最长的链有多长

比如当前节点有三个儿子的话,从当前节点往下走有三条路径,应该选哪条路径走呢

因为这个题要求最长的路径,要从三个儿子中选择一条能往下走的最长的路径

如果当前节点是u,三个儿子是abc的话

f[u]来表示从u往下走的最长路径,f[a],f[b],f[c]分别表示从abc往下走的最长路径
f [ u ] = m a x ( f [ a ] , f [ b ] , f [ c ] ) + 1 f[u] = max(f[a], f[b], f[c])+1 f[u]=max(f[a],f[b],f[c])+1

所以这个题就是非常简单的树形DP

只要按照这个递推方程,递归地求一遍就可以了,遍历一下整棵子树就可以了

如何去求方案,以及如何求字典序最小的方案

根节点是root

每次要知道,根节点要取最长路径的话,应该是往哪个儿子走

  1. 当然可以后面动态计算
    最后求方案的时候可以再递归一遍
    递归的时候,判断一下f[root]是从哪个儿子转移过来的,判断一下哪个儿子取了最大值就可以了
  2. 记忆化存储
    转移的时候,对u来说,记录一下u是从哪个儿子转移过来的,如果最大值是f[a]的话,就把son[u]记成a
    存下来之后,第一个点是root,第二个点就是son[root],接下来的点就是每次套一个son就可以了
    只要在转移的时候求一下son数组,就可以把整个路径找出来了
如何找字典序最小的方案

在求f[u]的时候,这三个链当中,只有唯一的一个最长

因为要保证最长,所以方案是唯一的

如果有两个儿子都可以取到最长

复制代码
f[a] = f[b] > f[c]

这时如何选a和b

由于要字典序最小,就看一下a和b哪个编号更小,取编号更小的那一个就可以了

输出方案可以用DP

递归求完每个状态之后,再从根节点再递归一遍就可以了

或者动态判断一下

没有告诉根节点,需要判断一下根节点是谁,看一下哪个点没有父节点

用bool数组标记

代码
复制代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10010;

int n;
int h[N], e[N], ne[N], idx;
int son[N];
bool st[N]; //标记有没有父节点

//加边函数模板
void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int dfs(int u)
{
	int res = 0;
	//当前点如果没有儿子的话,标记成-1
	son[u] = -1;

	//枚举一下当前点的所有子节点
	//~i等价于i != -1
	for (int i = h[u]; ~i; i = ne[i])
	{
		//用j表示当前子节点的编号
		int j = e[i];
		//递归求一下从j往后走的最大长度
		int d = dfs(j);
		//如果距离更长 或者 距离相等而且编号更小的话
		if (d > res || d == res && j < son[u])
		{
			res = d;
			son[u] = j;
		}
	}

	//最后返回最长路径+自己
	return res + 1;
}

int main()
{
	scanf("%d", &n);
	//先把路径表表头清空
	memset(h, -1, sizeof h);

	//读入每个点的所有儿子
	for (int i = 0; i < n; i ++)
	{
		//先读入每一个点儿子的数量
		int cnt;
		scanf("%d", &cnt);
		//依次读入所有的儿子
		while (cnt --)
		{
			int x;
			scanf("%d", &x);
			//添加一条边
			add(i, x);
			//i的儿子是x,所以x有父节点
			st[x] = true;
		}
	}

	//编号从0开始
	int root = 0;
	//找根节点
	while (st[root]) root ++;

	//递归求一下,从根节点开始的最大的路径长度
	int d = dfs(root);

	printf("%d\n", d);

	//行内没有空格
	printf("%d", root);

	for (int cur = son[root]; cur != -1; cur = son[cur])
		printf(" %d", cur);

	return 0;
}
相关推荐
tan180°3 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
彭祥.4 小时前
Jetson边缘计算主板:Ubuntu 环境配置 CUDA 与 cudNN 推理环境 + OpenCV 与 C++ 进行目标分类
c++·opencv·分类
lzb_kkk4 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
YuTaoShao4 小时前
【LeetCode 热题 100】48. 旋转图像——转置+水平翻转
java·算法·leetcode·职场和发展
胖大和尚6 小时前
clang 编译器怎么查看在编译过程中做了哪些优化
c++·clang
钱彬 (Qian Bin)7 小时前
一文掌握Qt Quick数字图像处理项目开发(基于Qt 6.9 C++和QML,代码开源)
c++·开源·qml·qt quick·qt6.9·数字图像处理项目·美观界面
双叶8368 小时前
(C++)学生管理系统(正式版)(map数组的应用)(string应用)(引用)(文件储存的应用)(C++教学)(C++项目)
c语言·开发语言·数据结构·c++
源代码•宸8 小时前
C++高频知识点(二)
开发语言·c++·经验分享
天真小巫8 小时前
2025.7.6总结
职场和发展
jyan_敬言9 小时前
【C++】string类(二)相关接口介绍及其使用
android·开发语言·c++·青少年编程·visual studio