diff --git a/TangDou/Topic/HuanGenDp/CF1324F.cpp b/TangDou/Topic/HuanGenDp/CF1324F.cpp index 064a650..6e9caba 100644 --- a/TangDou/Topic/HuanGenDp/CF1324F.cpp +++ b/TangDou/Topic/HuanGenDp/CF1324F.cpp @@ -1,70 +1,55 @@ #include using namespace std; -const int N = 2e5 + 10, M = N << 1; - -// 链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} - -int f1[N]; -int f2[N]; -int st[N]; -int c[N]; // 颜色 -int n; // 节点数量 - -// 以1号节点为根,跑一遍dfs,填充每个节点的cnt1-cnt2的最大值 -void dfs1(int u, int fa) { - f1[u] = c[u]; // 1:白色,-1黑色,正好与 cnt1-cnt2一致 - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - dfs1(v, u); - if (f1[v] > 0) { // 如果儿子能给点钱,那我就拿着 - st[v] = 1; // v这个儿子给了它爸爸u钱 - f1[u] += f1[v]; // 钱要累加到一起 - } +using VI = vector; +using VVI = vector; + +VI a; +VI dp; +VI ans; +VVI e; + +void dfs(int x, int fa = -1) { + dp[x] = a[x]; + for (int to : e[x]) { + if (to == fa) continue; + dfs(to, x); + dp[x] += max(0, dp[to]); } } -// 换根dp -void dfs2(int u, int fa) { - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - // 如果 st[v]=1,说明v的贡献包含在f1[u]里面 - int val = f2[u] - (st[v] ? f1[v] : 0); - f2[v] = f1[v] + max(val, 0); - dfs2(v, u); +void rdfs(int x, int fa = -1) { + ans[x] = dp[x]; + for (int to : e[x]) { + if (to == fa) continue; + dp[x] -= max(0, dp[to]); + dp[to] += max(0, dp[x]); + rdfs(to, x); + dp[to] -= max(0, dp[x]); + dp[x] += max(0, dp[to]); } } int main() { - // 初始化链式前向星 - memset(h, -1, sizeof h); + int n; cin >> n; + a = dp = ans = VI(n); + e = VVI(n); - for (int i = 1; i <= n; i++) { - int x; - cin >> x; - c[i] = (x ? x : -1); // 白色c[i]=1,黑色c[i]=-1 + for (int i = 0; i < n; ++i) { + cin >> a[i]; + if (a[i] == 0) a[i] = -1; } - - for (int i = 1; i < n; i++) { - int a, b; - cin >> a >> b; - add(a, b), add(b, a); + for (int i = 0; i < n - 1; ++i) { + int x, y; + cin >> x >> y; + --x, --y; + e[x].push_back(y); + e[y].push_back(x); } + dfs(0); + rdfs(0); - // 第一次dfs - dfs1(1, 0); - - // 它们两个是一个意思 - f2[1] = f1[1]; - // 换根dp - dfs2(1, 0); - // 输出答案 - for (int i = 1; i <= n; i++) printf("%d ", f2[i]); + for (int ret : ans) cout << ret << " "; + cout << endl; return 0; -} \ No newline at end of file +} diff --git a/TangDou/Topic/【换根】dfs专题.md b/TangDou/Topic/【换根】dfs专题.md index 06a1bdc..fce948a 100644 --- a/TangDou/Topic/【换根】dfs专题.md +++ b/TangDou/Topic/【换根】dfs专题.md @@ -570,105 +570,46 @@ signed main() { #### [$CF1324F$.$Maximum$ $White$ $Subtree$](https://codeforces.com/problemset/problem/1324/F) ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401100839763.png) +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401101429621.png) -我们在代码中将黑色记为$-1$,白色记为$1$ -> **注**:因为$cnt_1-cnt_2$是所求: -> - 如果是白色,$cnt_1=1,cnt_2=0$,$cnt_1-cnt_2=1-0=1$ -> - 如果是黑色,$cnt_1=0,cnt_2=1$,$cnt_1-cnt_2=0-1=-1$ -> 所以,可以理解为黑色记为$-1$,白色记为$1$。 -则有: -$$\large f_1[u]=c[u]+\sum_{fa[v] = u}max(f_1[v],0)$$ -> **解释** -> ① $u$有很多儿子,其中某一个是$v$,对于以$v$为根的子树而言,如果它里面的$cnt_1-cnt_2>0$,则对于$u$而言,儿子$v$可以贡献$+(cnt_1-cnt_2)$的价值。 -如果$cnt_1-cnt_2<=0$,对于$u$而言,不搭理它就完了。题目中取最大值,就是有一个选还是不选的抉择问题:对我有益我要,对我无益我弃!所有好儿子给的钱累加到一起,都是我的钱。 -> ② $c[u]$可以理解为老头子自己的积蓄,除了儿子们给的,还要加上自己的老本。 +**思路分析** +这题要求的是求出对任何一个节点$v$,求出包含这个节点的子树$cnt_1−cnt_2$的最大值。 -我们记$vis[u]$表示在计算节点$u$的父亲的$f_1$值时,节点$u$是否有被选,$1$表示被选,$0$表示未选。 +#### 暴力想法 +首先思考下暴力写法应该如何写。 +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401101430095.png) -定义$f_2[u]$表示在整棵树中选一张包含$u$的连通子图的所有方案中,$cnt_1-cnt_2$的最大值。 +对于所有可能的路径的贡献值的累加,且贡献值需大于等于$0$。 +>- 白的比黑的多,有分, 这时我们选上这棵子树 +>- 黑的比白的多,没分, 这时我们放弃这棵子树 -考虑$f_2[u]$的组成:一部分是以其为根的子树,即$f_1[u]$;另一部分即是在全局中将其子树挖掉后剩下的部分。这两部分互相独立,分别取最大值即可。 +不妨设$f[u]$代表$u$结点的最大值。故 -考虑第二部分的计算:记$v$的父节点为$u$: -- ① 若$st[v]=1$,则其包含在$f_1[u]$中,故这一部分即为$f_2[u]-f_1[v]$ -- ② 若$st[v]=0$,其不包含在$f_1[u]$中,则这一部分为$f_1[u]$。 -最后将这一部分的值与$0$取$max$即可。 +$$\large f[u]=c[u]+\sum_{v \in son_u}max(0,f[v])$$ -所以整个算法只需要两遍$dfs$,第一遍自底向上计算出$f_1$数组,第二遍以$f_2[1]$(强制$1$为根节点)自上向下计算出$f_2$数组,即为答案。 +假如用暴力写法,就是对于每个结点$u$,暴力搜索所有的相邻结点,利用$dfs$暴力搜索。也就是以每个结点为棵出发,枚举$n$次$dfs$,但是结点最大为$2∗10^5$ 这个暴力算法显然会超时,考虑如何优化。 -```cpp {.line-numbers} -#include -using namespace std; -const int N = 2e5 + 10, M = N << 1; +#### 算法优化 + 对于从下往上的贡献,可以利用从下往上的$dfs$树形$dp$进行获取,剩余的就是刨去以$v$为根的子树的贡献值比较难求。 -// 链式前向星 -int e[M], h[N], idx, w[M], ne[M]; -void add(int a, int b, int c = 0) { - e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; -} +在这里我们设$u$为结点$v$的父节点。$f[v]$代表从下往上以$v$为根的白点数减去黑点数的最大值,$dp[v]$代表最终的最大值。因此根据刨去以$v$为根的子树的贡献值这个思想,我们可以发现: -int f1[N]; -int f2[N]; -int st[N]; -int c[N]; // 颜色 -int n; // 节点数量 +$$\large add=dp[u]−max(0,f[v])$$ -// 以1号节点为根,跑一遍dfs,填充每个节点的cnt1-cnt2的最大值 -void dfs1(int u, int fa) { - f1[u] = c[u]; // 1:白色,-1黑色,正好与 cnt1-cnt2一致 - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - dfs1(v, u); - if (f1[v] > 0) { // 如果儿子能给点钱,那我就拿着 - st[v] = 1; // v这个儿子给了它爸爸u钱 - f1[u] += f1[v]; // 钱要累加到一起 - } - } -} +就是刨去以$v$为根的子树的贡献值。因此最终我们可以写出状态转移方程: +$$\large dp[v] = + \left\{\begin{matrix} +f[v] & if \ v = root \\ + f[v]+max(0,dp[fa]-max(0,f[v]))& if \ v \neq root +\end{matrix}\right. +$$ -// 换根dp -void dfs2(int u, int fa) { - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - // 如果 st[v]=1,说明v的贡献包含在f1[u]里面 - int val = f2[u] - (st[v] ? f1[v] : 0); - f2[v] = f1[v] + max(val, 0); - dfs2(v, u); - } -} - -int main() { - // 初始化链式前向星 - memset(h, -1, sizeof h); - cin >> n; - - for (int i = 1; i <= n; i++) { - int x; - cin >> x; - c[i] = (x ? x : -1); // 白色c[i]=1,黑色c[i]=-1 - } - - for (int i = 1; i < n; i++) { - int a, b; - cin >> a >> b; - add(a, b), add(b, a); - } +因此最后我们的思路为: - // 第一次dfs - dfs1(1, 0); +- 从下往上树形$dp$,计算$f[v]$ +- 从上往下换根$dp$,计算$dp[v]$ - // 它们两个是一个意思 - f2[1] = f1[1]; - // 换根dp - dfs2(1, 0); - // 输出答案 - for (int i = 1; i <= n; i++) printf("%d ", f2[i]); - return 0; -} -``` [USACO12FEB]Nearby Cows G [COCI2014-2015#1]Kamp @@ -679,6 +620,7 @@ POJ3585 Accumulation Degree CF708C Centroids +Eg3: AT Educational DP Contest V-Subtree #### [$AcWing$ $1073$. 树的中心](https://www.cnblogs.com/littlehb/p/15786805.html)