diff --git a/TangDou/Topic/HuanGenDp/P3047.cpp b/TangDou/Topic/HuanGenDp/P3047.cpp index c5a1281..450112a 100644 --- a/TangDou/Topic/HuanGenDp/P3047.cpp +++ b/TangDou/Topic/HuanGenDp/P3047.cpp @@ -1,60 +1,77 @@ #include +#define endl '\n' +#define INF 0x3f3f3f3f using namespace std; -#define int long long -#define endl "\n" +typedef long long ll; +typedef pair pii; +const int N = 1e5 + 10; +vector edge[N]; +int f[N][25], g[N][25]; +int val[N]; +int n, k; -const int N = 200010, M = N << 1, K = 21; +void dp(int u, int father) { + for (int i = 0; i <= k; i++) + f[u][i] = val[u]; + for (int i = 0; i < edge[u].size(); i++) { + int son = edge[u][i]; + if (son == father) + continue; + dp(son, u); -// 链式前向星 -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 n, q; -int dp[N][K]; - -void dfs1(int u, int fa) { - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - dfs1(v, u); - for (int j = 1; j <= q; j++) dp[u][j] += dp[v][j - 1]; // 第一遍dp + for (int j = 1; j <= k; j++) { + f[u][j] += f[son][j - 1]; + } } + return; } -void dfs2(int u, int fa) { - for (int i = h[u]; ~i; i = ne[i]) { - int v = e[i]; - if (v == fa) continue; - // 在第一次遍历时 dp[1][2] 包括了 dp[2][1] 2的子树权值; - // 然鹅 ans在统计dp[2][3] 的时候也加上了 dp[2][1] 2的子树权值; - // 第二次遍历 dp[2][3] 又加上了 dp[2][1]; - // 所以需要简单容斥一下; - for (int j = q; j >= 2; j--) - dp[v][j] -= dp[v][j - 2]; // 简单容斥 - for (int j = 1; j <= q; j++) - dp[v][j] += dp[u][j - 1]; // 第二遍dp - dfs2(v, u); +void dp2(int u, int father) { + for (int i = 0; i < edge[u].size(); i++) { + int son = edge[u][i]; + if (son == father) + continue; + g[son][0] = val[son]; + g[son][1] = f[son][1] + val[u]; + for (int j = 2; j <= k; j++) { + g[son][j] = g[u][j - 1] + f[son][j] - f[son][j - 2]; + } + + dp2(son, u); } } -signed main() { - // 初始化链式前向星 - memset(h, -1, sizeof h); - cin >> n >> q; - for (int i = 1; i < n; i++) { +void solve() { + cin >> n >> k; + for (int i = 0; i < n - 1; i++) { int a, b; cin >> a >> b; - add(a, b), add(b, a); + edge[a].push_back(b); + edge[b].push_back(a); } - for (int i = 1; i <= n; i++) cin >> dp[i][0]; // 每个节点往外0距离,就是它本身的权值; - dfs1(1, 0); - dfs2(1, 0); + for (int i = 1; i <= n; i++) + cin >> val[i]; + + dp(1, 0); + for (int i = 0; i <= k; i++) { + g[1][i] = f[1][i]; + } + dp2(1, 0); + // for(int i = 1; i <= n; i ++ ) + // { + // cout << f[i][k] << endl; + // } + // cout << endl; for (int i = 1; i <= n; i++) { - int ans = 0; - for (int j = 0; j <= q; j++) ans += dp[i][j]; // ans统计答案 - cout << ans << endl; + cout << g[i][k] << endl; } -} \ No newline at end of file +} + +int main() { + ios::sync_with_stdio(0); + cin.tie(0); + cout.tie(0); + + solve(); +} diff --git a/TangDou/Topic/HuanGenDp/P3047.eddx b/TangDou/Topic/HuanGenDp/P3047.eddx new file mode 100644 index 0000000..325dced Binary files /dev/null and b/TangDou/Topic/HuanGenDp/P3047.eddx differ diff --git a/TangDou/Topic/【换根】dfs专题.md b/TangDou/Topic/【换根】dfs专题.md index 49adc01..92bd07d 100644 --- a/TangDou/Topic/【换根】dfs专题.md +++ b/TangDou/Topic/【换根】dfs专题.md @@ -689,24 +689,85 @@ int main() { 看数据范围就知道暴力肯定是会$TLE$飞的,所以我们要考虑如何$dp$(代码习惯写$dfs$) -仔细思考一下我们发现点$i$走$k$步能到达的点分为以下两种 +仔细思考一下我们发现点$i$走$k$步能到达的点分为以下两种: -- 在$i$的子树中(由$i$点往下) -- 经过$i$的父亲(由$i$点往上) +- ① 在$i$的子树中(由$i$点往下) +- ② 经过$i$的父亲(由$i$点往上) -这样的问题一般可以用两次$dfs$解决 +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401151124943.png) + +这样的问题一般可以用两次$dfs$解决。 定义状态: - $f[i][j]$表示$i$点往下$j$步范围内的点权之和 -- $g[i][j]$表示$i$点往上和往下走$j$步范围内点权之和 +- $g[i][j]$表示$i$点往上和往下走$j$步范围内点权之和:【答案在这里】 + +第一次$dfs$我们求出所有的$f[i][k]$,这个简单,对于节点$u$和其儿子$v$: -第一次$dfs$我们求出所有的$f[n][k]$,这个比较简单,对于节点$u$和其儿子$v$,$f[u][k] += f[v][j - 1]$就行了。(第一次$dfs$已知叶子节点推父亲节点) +**初始值** +$$f[i][0]=a[i]$$ +> **解释**:每个节点,向下走$0$步,也就是一步不走,那还是它自己的点权,也就是$f[i][0]=a[i]$ -第二次$dfs$我们通过已经求出的$f$数组推$g$数组,对于$u$和$u$的儿子$v$, +**递推式** +$$f[u][j] = \sum_{v \in son[u]}f[v][j - 1]$$ + +第二次$dfs$我们通过已经求出的$f$数组推$g$数组,对于$u$和$v$, $$g[v][k] += (g[u][k - 1] - f[v][k - 2])$$ -注意数组下表不要越界。$g[i][j]$的初始值应该赋为$f[i][j]$,因为根节点的$g[i][j]$就是$f[i][j]$。(第二次$dfs$已知父亲节点推儿子节点) +---- + +题目简单地来说就是: +给你一棵 $n$ 个点的树,点带权,对于每个节点求出距离它不超过 $k$ 的所有节点权值和 $m_i$。 + +对于树中的某个节点而言,距离它不超过$k$的节点主要来源于两方面: +- 一个是该节点的子节点中距离该节点不超过距离$k$的节点的权值和 +- 一个是该节点向上沿着父节点方向不超过距离$k$的点的权值和 + +对于子节点方向的节点的权值和,我们可以先通过普通的树形$DP$计算出来。 + +因此,我们先写一个$DP$计算出子树中距离该点不超过$k$的点的权值和。 + +**1、状态表示** +$f[u][k]$表示以$u$为根节点的树中,距离$u$不超过$k$的子节点的权值和。 + +**2、状态转移** +$$f[u][j]=val[u]+\sum_{v \in son[u]}f[v][j−1] \ j \in [1,k]$$ + +到节点$u$不超过距离$k$,即距离$son$不超过$k−1$,然后加在一起即可。同时$u$节点本身也是答案,因为$u$节点本身是不超过距离$0$的节点。 + +**3、换根$DP$** +这个题目本身是个无根树,如果我们认为规定编号为$1$的节点是根的话,那么对于祖宗节点$1$来说,$f[1][k]$就是距离$1$节点不超过距离$k$的节点的权值和。因为祖宗节点是没有父亲节点的,所以我们就不需要考虑沿着父节点方向的节点权值和。 + +令:$g[u][k]$表示所有到$u$节点的不超过距离$k$的节点的权值和。根据刚刚的分析:$g[1][k]=f[1][k]$ + +这个就是我们换根$DP$的 **初始化**。其实受这个的启发,我们完全可以去把每个点都当作根,然后暴力跑出答案,但是这个暴力做法的时间复杂度是$O(n^2)$的,会超时。 + +所以当我们将祖宗节点从节点$1$换为另一个节点的时候,我们只能通过数学上的关系来计算出$g$数组元素的值。这个也是换根$DP$的意义。 + +我们看下面的图: + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401151316634.png) + +红色框是非常好理解的,直接写成$f[u][k]$即可。上面的部分,我们可以写成$g[fa(u)][k-1]$。 +因为到$u$不超过$k$的距离,即距离它的父亲节点不超过$k−1$的距离。 + +但是这么写对吗? + +答案是不对的,$g[fa(u)][k-1]$和$f[u][k]$是有重复部分的。我们需要减去这段重复的部分,那么关键问题是重复部分如何表示? + +重复部分肯定是出现在了红色框中,红色框中到$fa(u)$不超过距离$k−1$,即距离$u$不超过$k-2$,同时重复部分又恰好是节点$u$的子节点,所以这部分可以表示为:$f[u][k-2]$。 + +所以最终的结果就是: + +$$\large g[u][k]=f[u][k]+g[fa(u)][k−1]−f[u][k−2]$$ + +但是上述方程成立的条件是$k\geq 2$的。 + +所以我们还得想一想$\leq 1$的时候。 + +如果$k=0$,$g[u][0]$其实就是$val[u]$,因为不超过距离$0$的点只有本身。 +如果$k=1$,那么$g[u][1]$其实就是$f[u][1]+val[fa(u)]$,因为沿着父节点方向距离$u$不超过$1$的点,只有父节点,而树中,父节点是唯一的。沿着子节点方向,其实就是$u$的各个子节点,而这些子节点可以统统用$f[u][1]f[u][1]$表示。 #### [$P6419$ $Kamp$](https://www.luogu.com.cn/problem/P6419) https://www.cnblogs.com/Troverld/p/14601347.html