「树形DP」叶子的染色

本题为3月15日23上半学期集训每日一题中B题的题解

题面

题目描述

给一棵有m个节点的无根树,你可以选择一个度数大于1的节点作为根,然后给一些节点(根、内部节点、叶子均可)着以黑色或白色。你的着色方案应保证根节点到各叶子节点的简单路径上都包含一个有色节点,哪怕是叶子本身。

对于每个叶子节点u,定义 \(c_u\) 为从根节点到u的简单路径上最后一个有色节点的颜色。给出每个 \(c_u\) 的值,设计着色方案使得着色节点的个数尽量少。

输入

第一行包括两个数m,n,依次表示节点总数和叶子个数,节点编号依次为1至m。

接下来n行每行一个0或1的数,其中0表示黑色,1表示白色,依次为 \(c_1,c_2,\cdots ,c_n\) 的值。

接下来m-1行每行两个整数a,b,表示节点a与b有边相连。

输出

输出仅一个数,表示着色节点数的最小值。

样例输入

5 3
0
1
0
1 4
2 5
4 5
3 5

样例输出

2

提示

数据范围与提示

数据 1 2 3 4 5 6 7 8 9 10
m 10 50 100 200 400 1000 4000 8000 10000 10000
n 5 23 50 98 197 498 2044 4004 5021 4996

思路分析

本题需要用到树形DP的思想,如果你还不熟悉动态规划,建议暂时放弃这道题,先去练习简单的动态规划问题.如果你已经对动态规划有所了解但是不熟悉树形DP,请自行搜索学习,如OI Wiki上关于树形DP专题页.

本题最大的难点在于不知道根,但是关于树的很多操作都必须从根开始.所以我们可以先尝试分别假设除了叶子节点(其度是1,不满足题目中对根节点的要求)以外的每一个点为根节点,计算出它的最小着色数,最后再取这些数的最小值.虽然这种方法不一定可行,但是我们可以尝试从这种方法入手,看看后续如何优化.

显然,当根确定之后,这就是一个比较裸的树形DP问题了.

由于叶子结点代表一条路径的终点,所以初始时每一条路径都需要染一次颜色.如果不进行任何操作,当多条路径相交之后,这条由多个路径合并出来的新"路径"的染色次数就是各个路径染色次数的和.

但其实在多条路径相交之后,由于对交叉点及以上的部分(也就是合并出来的新"路径")进行染色时,可以起到同时对多条路径染色的作用,所以当路径交叉时,如果我们将合并出来的新"路径"染成白色,那么其所有子路径都可以少染一次白色,对黑色当然也是同理的.所以在计算新"路径"的染色次数的求和过程中,与新"路径"染相同颜色的路径的染色次数减一.不过这里要注意,我们让其子路径少染一次颜色的前提是当前"路径"要染一种颜色,所以在求完和后还要再加1(另一种角度的理解是,我们上述这种染色方法的实质是只让其中一条路径染这种颜色,其他不染,但是前面计算的过程中,这条要染的也被减掉了,所以要加回来).

由此,我们用 \(dp[i][j]\) 表示第i个节点染第j种颜色时所需要染色的次数可以得到如下状态转移方程:

\(dp[i][j] = \begin{cases} 1, i为叶子节点,j为i所在路径需要染的颜色\\ \infty, i为叶子节点,j不为i所在路径需要染的颜色\\ 1 + \sum_{k}min(dp[k][j] - 1, dp[k][!j]), i不为叶子结点,k为i的各个子节点编号\\ \end{cases}\)

仔细观察上面状态转移的式子,我们可以发现,每一个节点的 \(dp[i][j]\) 的值,其都是从叶子节点转移过来的,而转移时利用的是路径的相交.所以我们只要保证叶子节点还在这条路径上,并且保证路径具有相同的相交情况,那么无论如何选择根节点都是可以的.显然,无论将哪个非叶子节点作为根节点,它们之间都是满足上面这两点的.所以我们可以任意选取一个点作为根节点,在我的代码中,我选择了编号为 \(n + 1\) (下标为n)的点.

当然,此题还可以用换根法来枚举每个节点作为根的情况,也可以用贪心的方法,这里就不做说明了,感兴趣可以去洛谷上看看这题的一些题解.

参考代码

时间复杂度: \(O(M)\)

空间复杂度: \(O(N + M)\)

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

vector<vector<int>> g; // 邻接表
int *c; // 每个叶子节点所在路径要求的颜色
bool *visited; // 当前节点是否被搜索过

int n; // 叶子节点总数

// 树形DP(借助DFS进行),此处dp的元素默认值为1
void dfs(int i, vector<vector<int>> &dp) {
    visited[i] = true;
    if (i < n) { // 当前节点为叶子节点,将它不需要染的颜色的方案数设为无穷大,这样不会影响后续的状态转移
        dp[i][!c[i]] = 0x3f3f3f3f;
    }

    for (auto j : g[i]) { // 遍历当前节点的每一个子节点
        if (!visited[j]) {
            dfs(j, dp); // 对其子节点启动一次dp

            // 状态转移,同色减一,异色不变
            dp[i][0] += min(dp[j][0] - 1, dp[j][1]);
            dp[i][1] += min(dp[j][1] - 1, dp[j][0]);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int m;
    cin >> m >> n;

    // 输入每个叶子节点所在路径要求的颜色
    c = new int[n];
    for (int i = 0; i < n; i++) {
        cin >> c[i];
    }

    // 输入树,构建邻接表
    g.resize(m);
    for (int i = 0; i < m - 1; i++) {
        int a, b;
        cin >> a >> b;
        a--; // 出于习惯,我将编号改为从0开始的下标
        b--;
        g[a].push_back(b);
        g[b].push_back(a);
    }

    // 以n为根节点启动树形DP
    vector<vector<int>> dp(m, vector<int>(2, 1));
    visited = new bool[m];
    memset(visited, false, sizeof(bool) * m);
    dfs(n, dp);

    // 最终结果就是把根节点染成0和1中,需要染色数最小的那种情况
    cout << min(dp[n][0], dp[n][1]) << "\n";

    delete[] c;
    delete[] visited;
    return 0;
}

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德

posted @ 2023-03-15 18:50  星双子  阅读(32)  评论(0编辑  收藏  举报