题目链接:
http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3734
题目大意:
若干微博账户形成了一个转发树(即一个有根树)。
每个账户有自己的价值,每个账户也有自己的态度(赞或蜡烛)。
如果一个账户的态度是“赞”,它的价值就会被加到“赞”的一边,反之亦然。
Edward 可以从“赞”的一边拿出X 的价值去翻转一个账户,即把它的态度换到相反的一边。
但是Edward 发现,有的账户已经被别人翻转过了,对于这些账户,Edward 就要花费Y 的价值去翻转它们。
一旦一个账户被翻转了一次,它的所有子账户也会被翻转一次。
求“赞”的一边的价值总数与“蜡烛”一边的价值总数的最大差值。若最大差值为负数则输出“HAHAHAOMG”。
算法:
典型的树状DP。
但是如果直接自下向上更新的话,会发现父节点是否翻转的信息无法传递给子节点。
于是,除了记录当前节点是否直接被Edward 翻转以外,我们不妨再加一维状态,表示当前节点的父节点实际是否翻转(直接、间接都算在内)。
DP维护以下两个状态:
flag_pre 代表该节点的父节点实际是否被翻转(注意不是指父节点是否直接被Edward 翻转)
flag_cur 代表该节点是否直接被Edward 翻转(注意该节点直接被Edward 翻转不代表该节点最终一定被翻转了,因为有可能它之前被别人翻转过或者它的父节点被翻转了)
对于每个节点u,假设它的父节点是 p。
那么,它的 dp[u][0][x1]状态会被更新到dp[p][x2][0]状态上,它的 dp[u][1][x1]状态会被更新到dp[p][x2][1]状态上。
这样就保证了子节点和父节点间信息的一致性。
根据以上两个状态可以计算出以下两个状态
flag_nxt = flag_pre ^ flag_cur ^ s[i],代表该节点实际是否被翻转 。该节点的flag_nxt 对应的是它子节点的flag_pre。
flag_nxt ^ p[i],代表这个节点最终的态度(赞或者蜡烛)
其实只要维护这个点实际有没有翻转这一个状态就可以了,别的都是可以推出来的。
但是这样就要特判根节点,因为根节点不能翻转。我懒。。。
PS:
这道题在我写过的树状DP里算是代码非常之短的了。
50行您买不了吃亏~ 50行您买不了上当~ 耶!
代码如下:
#include <cstdio> #include <climits> #include <algorithm> #include <vector> const int maxn = 51000; std::vector <int> map[maxn]; int v[maxn], f[maxn], s[maxn], p[maxn]; int dp[maxn][2][2]; int n, x, y; int dfs(int u, int flag_pre, int flag_cur) { if (dp[u][flag_pre][flag_cur] != INT_MIN) { return dp[u][flag_pre][flag_cur]; } int flag_nxt = flag_pre ^ flag_cur ^ s[u]; int tmp = (flag_nxt ^ p[u]) ? -v[u] : v[u]; if (flag_cur) { tmp -= s[u] ? y : x; } for (int i = 0 ; i < map[u].size(); i ++) { int v = map[u][i]; tmp += std::max(dfs(v, flag_nxt, 0), dfs(v, flag_nxt, 1)); } return dp[u][flag_pre][flag_cur] = tmp; } int main() { while (scanf("%d %d %d", &n, &x, &y) == 3) { for (int i = 0; i <= n; i ++) { map[i].clear(); dp[i][0][0] = dp[i][0][1] = dp[i][1][0] = dp[i][1][1] = INT_MIN; } for (int i = 1; i <= n; i ++) { scanf("%d %d %d %d", &v[i], &f[i], &s[i], &p[i]); map[f[i]].push_back(i); } dfs(0, 0, 0) < 0 ? puts("HAHAHAOMG") : printf("%d\n", dp[0][0][0]); } return 0; }