一、题目
1、原题链接
1079. 叶子的颜色
2、题目描述
给一棵有 m 个节点的无根树,你可以选择一个度数大于 1 的节点作为根,然后给一些节点(根、内部节点、叶子均可)着以黑色或白色。
你的着色方案应保证根节点到各叶子节点的简单路径上都至少包含一个有色节点,哪怕是这个叶子本身。
对于每个叶子节点 u,定义 cu 为从根节点到 u 的简单路径上最后一个有色节点的颜色。
给出每个 cu 的值,设计着色方案使得着色节点的个数尽量少。
输入格式
第一行包括两个数 m,n,依次表示节点总数和叶子个数,节点编号依次为 1 至 m,其中编号 1 至 n 是叶子。
接下来 n 行每行一个 0 或 1 的数,其中 0 表示黑色,1 表示白色,依次为 c1,c2,…,cn 的值。
接下来 m−1 行每行两个整数 a,b,表示节点 a 与 b 有边相连。
输出格式
输出仅一个数,表示着色节点数的最小值。
数据范围
输入样例:
5 3
0
1
0
1 4
2 5
4 5
3 5
输出样例:
2
二、解题报告
1、思路分析
思路来源:y总讲解视频
y总yyds
(1)题目要求:染色方法满足每个叶子节点到根节点的路径上至少存在一个结点的颜色为c[i]。
(2)首先给出两个结论(详细证明见y总讲解视频):
无论以哪一个度大于1的结点作为根节点,最优解不变。
每个节点染色比不染色时的解更优。
(3)dp[i][无色]、dp[i][白色]、 dp[i][黑色]分别表示以i为根节点并且i为无色、白色、黑色时着色节点的最小数量。s[j]为i的每一棵子树的根节点。
dp[i][无色]+=min(dp[s[j]][白色],dp[s[j]][黑色]):由于i没有染色,所以要满足条件时,其子树的根节点需要染色,染成黑色或白色,取最其中最小值。
dp[i][白色]+=min(dp[s[j]][白色]-1,dp[s[j]][黑色])+1:由于i已经染成了白色,所以要满足条件时,其子树根节点如果要染成白色的话,可以省去该次操作,因为该路径上已经有一个白色的节点了,如果要染成黑色的话则需要染该子树的根节点,因为该路径上没有黑色的节点,两者取最小值,最后再加一次操作(i这个根节点染色需要一次操作)。
dp[i][黑色]+=min(dp[s[j]][白色],dp[s[j]][黑色]-1)+1:同理上面一种情况。
观察转移方程,后两种的染色次数的最小值小于等于第一种情况,所以可以只从后两种情况中进行转移求最优解即可。
2、时间复杂度
时间复杂度为O(n+m)
3、代码详解
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=10010,M=2*N,INF=1e8;
int m,n;
int h[N],e[M],ne[M],idx; //邻接表存储树
int dp[N][2];
int c[N];
//邻接表加边
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//dfs递归处理每个子树
void dfs(int u,int fa){
if(u<=n) return ;
dp[u][0]=dp[u][1]=1; //染当前点需要一次操作
//遍历u的每条边
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(j==fa) continue; //如果是j是根节点continue
dfs(j,u); //递归处理子树
dp[u][0]+=min(dp[j][0]-1,dp[j][1]); //染成白色的转移方程
dp[u][1]+=min(dp[j][1]-1,dp[j][0]); //染成黑色的转移方程
}
}
int main(){
cin>>m>>n;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<=n;i++){
dp[i][c[i]]=1; //当前叶节点为要求的颜色,最小染色次数为1
dp[i][!c[i]]=INF; //否则无解,设置为正无穷
}
for(int i=0;i<m-1;i++){
int a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
dfs(n+1,-1); //任意选一个根进行dfs
cout<<min(dp[n+1][0],dp[n+1][1]);
return 0;
}
三、知识风暴
树形DP