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
每次要知道,根节点要取最长路径的话,应该是往哪个儿子走
- 当然可以后面动态计算
最后求方案的时候可以再递归一遍
递归的时候,判断一下f[root]
是从哪个儿子转移过来的,判断一下哪个儿子取了最大值就可以了 - 记忆化存储
转移的时候,对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;
}