政府邀请了你在火车站开饭店,但不允许同时在两个相连接的火车站开。任意两个火车站有且只有一条路径,每个火车站最多有 50 个和它相连接的火车站。
告诉你每个火车站的利润,问你可以获得的最大利润为多少。
最佳投资方案是在1,2,5,6这4个火车站开饭店可以获得利润为 90
第一行输入整数 N(≤100000) ,表示有 N 个火车站,分别用 1,2,…,N 来编号。接下来 N 行,每行一个整数表示每个站点的利润,接下来 N−1 行描述火车站网络,每行两个整数,表示相连接的两个站点。
输出一个整数表示可以获得的最大利润。
6
10
20
25
40
30
30
4 5
1 3
3 4
2 3
6 4
90
这道题目是树型 DP 的典型题,也是入门难度的,比较容易想。
我们先来分析一下题目的条件。
首先,“任意两个火车站有且只有一条路径”,这意味着这道题目是在一个无环的连通图上进行的。再想想,既然无环,其实我们完全可以指定一个根结点,把图的形式稍微变一下。例如,以下表示方式与样例是等价的。(作图的时候出了点问题,权且看作是一棵以 5 为根的倒过来的树吧)
看, 这就成了一棵树!于是我们的任务变成了可以确定顺序进行的。
“但不允许同时在两个相连接的火车站开”,那么意味着某些结点是选,另一些不选的。很容易会联想到前面的刚学的状压 DP,但是我们想想看,结点的个数这么大,根本连状态都表示不了。用状压 DP 简直就是无稽之谈。
如果我们回忆一下之前“信号灯”之类的题目,在一维的情况下,某一些连续的不能取,我们的解决方案是加维记录当前取哪个。同样的道理,虽然是在树上做 DP,但其实也是选或不选的问题,那么我们不妨分别用 f[root][0] 和 f[root][1] 表示以 root 为根的子树中,选 root 和不选 root 所能获得的最大利润。
转移方程很好理解,如果选了根结点,那么它的所有直接子结点都不能选;而如果不选根结点,它的子结点既可以选也可以不选。于是就有
边界条件就是对于所有叶子结点,取就得到它本身的价值,不取就为 0。整个问题的解就是 max(f[R][0],f[R][1]) ,其中 R 是我们指定的整棵树的一个根结点。
那求解的顺序呢,岂不是很难确定?不,我们可以递归求解!反正每个子问题只求一遍,实际上时间复杂度只是 O(n) 的!
还有一个细节问题:读入建树的时候我们还不知道根结点,对于每一条边也就不确定谁是父亲、谁是儿子,因此要给两个点都加一条到另一个点的边。
但是,遍历的时候就可能访问回去父亲结点,要避免这个问题,可以在递归求解的时候加一个参数 pre ,表示当前结点的父亲结点;或者用一个 bool 数组标记结点是否被访问过。这样就可以完美解决本题啦!
参考代码:
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 1e5 + 100;
int n;
int v[maxn];
vector g[maxn]; //用 vector 代替数组模拟指针,很方便
int dp[maxn][5];
bool vis[maxn];
void dfs(int r) {
vis[r] = true;
dp[r][1] = v[r];
dp[r][0] = 0;
for (int i = 0; i < g[r].size(); i++)
if (!vis[g[r][i]]) {
dfs(g[r][i]);
dp[r][0] += max(dp[g[r][i]][0], dp[g[r][i]][1]);
dp[r][1] += dp[g[r][i]][0];
}
}
int main(void) {
freopen("1782.in", "r", stdin);
freopen("1782.out", "w", stdout);
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &v[i]);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
g[x].push_back(y); g[y].push_back(x);
}
memset(vis, false, sizeof vis);
dfs(1);
printf("%d\n", max(dp[1][0], dp[1][1]));
return 0;
}