YbtOJ 「动态规划」 第4章 树形DP
树形dp
这类题一般在图的遍历过程中dp
void dfs ( int u , int fa )
{
//dp初始化
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == fa ) continue;
dfs ( v , u );
//合并的时候统计信息
}
}
A. 【例题1】树上求和
[题目描述]
某大学有
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
[输入格式]
输入的第一行是一个整数
第
第
[输出格式]
输出一行一个整数代表最大的快乐指数。
[算法分析]
考虑设
反之
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
int n , a[N] , f[N][2] , in[N] , rt;
void dfs ( int u , int ff )
{
f[u][1] = a[u] , f[u][0] = 0;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dfs ( v , u );
f[u][1] += f[v][0];
f[u][0] += max ( f[v][1] , f[v][0] );
}
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 , u , ff ; i < n ; i ++ ) u = read() , ff = read() , add ( ff , u ) , in[u] = 1;
for ( int i = 1 ; i <= n ; i ++ ) if ( !in[i] ) rt = i;
dfs ( rt , 0 );
cout << max ( f[rt][1] , f[rt][0] ) << endl;
return 0;
}
B. 【例题2】结点覆盖
[题目描述]
五一来临,某地下超市为了便于疏通和指挥密集的人员和车辆,以免造成超市内的混乱和拥挤,准备临时从外单位调用部分保安来维持交通秩序。
已知整个地下超市的所有通道呈一棵树的形状;某些通道之间可以互相望见。总经理要求所有通道的每个端点(树的顶点)都要有人全天候看守,在不同的通道端点安排保安所需的费用不同。
一个保安一旦站在某个通道的其中一个端点,那么他除了能看守住他所站的那个端点,也能看到这个通道的另一个端点,所以一个保安可能同时能看守住多个端点(树的结点),因此没有必要在每个通道的端点都安排保安。
编程任务:
请你帮助超市经理策划安排,在能看守全部通道端点的前提下,使得花费的经费最少。
[输入格式]
第1行
第2行至第n+1行,每行描述每个通道端点的信息,依次为:该结点标号
对于一个
[输出格式]
最少的经费。
如右图的输入数据示例
输出数据示例:
[代码实现]
同树形dp 讨论三种情况(如果不是第二种情况这个点就一定不选 这是最关键的地方)
- 如果这个点被父亲覆盖 那么子节点不可能被父亲覆盖 而是在其余两种情况当中选取
- 如果这个点被自己覆盖 那么子节点三种情况都行 ( 因为子节点肯定已经被覆盖了 那么选哪种情况都可以了)
- 如果这个点被儿子覆盖 那么子节点必须被自己覆盖 那么再分两种情况:
- 如果
那么皆大欢喜 强制让子节点选取自己 同时标记 - 如果反之 则每一个点累加
并处理子节点选择自身和选择儿子的差值的最小值 作为损失 - 如果所有节点都遍历过了还是没有找到
的情况( ) 那么 作为损失(此时的方案选取情况就是:强制让这一个 差值所对应的子节点 改变选择 让自己覆盖自己 其余的出点 还是由自己的儿子覆盖 这样不会造成其他节点被错误覆盖 因为你所有子节点的 都被处理过了)
- 如果
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , a[N] , f[N][3] , in[N] , rt;
int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void dfs ( int u , int ff )
{
int minn = inf , flag = 0;
f[u][1] = a[u];
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dfs ( v , u );
f[u][0] += min ( f[v][1] , f[v][2] );
f[u][1] += min ( f[v][0] , min ( f[v][1] , f[v][2] ) );
if ( f[v][1] <= f[v][2] ) flag = 1 , f[u][2] += f[v][1];
else f[u][2] += f[v][2] , minn = min ( minn , f[v][1] - f[v][2] );
}
if ( !flag ) f[u][2] += minn;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
// memset ( f , inf , sizeof f );
n = read();
for ( int i = 1 , u , num , v ; i <= n ; i ++ )
{
u = read() , a[u] = read() , num = read();
for ( int j = 1 ; j <= num ; j ++ ) v = read() , add ( u , v ) , in[v] = 1;
}
for ( int i = 1 ; i <= n ; i ++ ) if ( !in[i] ) rt = i;
dfs ( rt , 0 );
cout << min ( f[rt][1] , f[rt][2] ) << endl;
return 0;
}
C. 【例题3】最长距离
树的直径模板题 一个结论:在树上 到一个点距离最远的点一定是这棵树的直径的两个端点之一
所以我们先
之后再从这次搜出来的最远点(直径的另一个端点)搜索 更新
所以对于每一个点的最长路径就是
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , s , dis1[N] , dis2[N] , maxlen;
int head[N] , cnt;
struct node { int to , nxt , w; } e[N];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
void dfs1 ( int u , int ff )
{
if ( dis1[u] > maxlen ) s = u , maxlen = dis1[u];
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dis1[v] = dis1[u] + e[i].w;
dfs1 ( v , u );
}
}
void dfs2 ( int u , int ff )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dis2[v] = dis2[u] + e[i].w;
dfs2 ( v , u );
}
}
void init()
{
memset ( dis1 , 0 , sizeof dis1 );
memset ( dis2 , 0 , sizeof dis2 );
memset ( head , 0 , sizeof head );
maxlen = 0 , cnt = 0;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
while ( cin >> n )
{
init();
for ( int i = 2 , u , w ; i <= n ; i ++ ) u = read() , w = read() , add ( i , u , w ) , add ( u , i , w );
dfs1 ( 1 , 0 ) , memset ( dis1 , 0 , sizeof dis1 );
maxlen = 0 , dfs1 ( s , 0 );
maxlen = 0 , dfs2 ( s , 0 );
for ( int i = 1 ; i <= n ; i ++ ) cout << max ( dis1[i] , dis2[i] ) << endl;
}
return 0;
}
D. 【例题4】选课方案
[题目描述]
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有
[输入格式]
第一行有两个整数
接下来的
[输出格式]
只有一行,选
[算法分析]
树上背包经典例题
得到
状态转移:
这道题是多叉树 但是可以在dp到
注意对于
[代码实现]
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , f[N][N] , m;
int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void dfs ( int u )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
dfs(v);
for ( int j = m ; j ; j -- )
for ( int k = 1 ; k <= j ; k ++ )//不能枚举到k 因为我们状态定义的就是u这个根节点必须选 因为只有选了这个节点才能选子节点
f[u][j] = max ( f[u][j] , f[v][j-k] + f[u][k] );
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ )
{
int u = read();
f[i][1] = read();
add ( u , i );
}
m ++ , dfs(0);
cout << f[0][m] << endl;
return 0;
}
E. 1.路径求和
我们考虑计算每条边对答案的贡献 对于每一条边 它的计算次数等于它分开的两棵子树中 左叶数
也就是
所以贡献再乘上
注意:题目中读入顺序是先边权后节点
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , s , leaf[N] , sz[N] , out[N] , ans;
int head[N] , cnt;
struct node { int to , nxt , w; } e[N<<1];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
void dfs1 ( int u , int ff )
{
sz[u] = 1;
if ( out[u] == 1 ) leaf[u] = 1;//叶子节点
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dfs1 ( v , u );
sz[u] += sz[v];
leaf[u] += leaf[v];
}
}
void dfs2 ( int u , int ff )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dfs2 ( v , u );
ans += e[i].w * ( sz[v] * ( leaf[1] - leaf[v] ) + ( sz[1] - sz[v] ) * leaf[v] );
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 , u , v , w ; i <= m ; i ++ ) w = read() , u = read() , v = read() , add ( v , u , w ) , add ( u , v , w ) , out[u] ++ , out[v] ++;
dfs1 ( 1 , 0 );
dfs2 ( 1 , 0 );
cout << ans << endl;
return 0;
}
F. 2.树上移动
对于第一个问题 答案就是所有边权之和乘二减去过起点
第二问即为求树的直径长度 用所有边权乘二减去直径长度即可
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , s , sum , dis1[N] , dis2[N] , maxlen , maxx;
int head[N] , cnt;
struct node { int to , nxt , w; } e[N<<1];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
void dfs1 ( int u , int ff )
{
if ( dis1[u] > maxlen ) s = u , maxlen = dis1[u];
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dis1[v] = dis1[u] + e[i].w;
dfs1 ( v , u );
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , s = read();
for ( int i = 1 , u , v , w ; i < n ; i ++ )
u = read() , v = read() , w = read() , add ( v , u , w ) , add ( u , v , w ) , sum += 2 * w;
dfs1 ( s , 0 );
cout << sum - maxlen << endl;
memset ( dis1 , 0 , sizeof dis1 );
maxlen = 0 , dfs1 ( s , 0 );
cout << sum - maxlen << endl;
return 0;
}
G. 3.块的计数
连通块:无向图
强连通:有向图
我们考虑
我们利用补集转化思想
先考虑如何求
然后
注意:还有一种情况 如果
最后统计答案就是
需要注意求
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
#define int long long
const int N = 2e5 + 5;
const int mod = 998244353;
const int inf = 2e18;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , maxx = -inf , a[N] , totg , totf , f[N] , g[N];
//f[i]表示以节点u为根的联通块总个数,g[i]表示以节点u为根且不包含这个最大喜好值的联通块个数
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void dfs ( int u , int ff )
{
if ( a[u] != maxx ) g[u] = 1;
f[u] = 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dfs ( v , u );
f[u] = f[u] * ( f[v] + 1 ) % mod;//f[v]的方案数+不选的方案
g[u] = g[u] * ( g[v] + 1 ) % mod;//g[v]的方案数+不选的方案
}
totf = ( totf + f[u] ) % mod;
totg = ( totg + g[u] ) % mod;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , maxx = max ( maxx , a[i] );
for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
dfs ( 1 , 0 );
cout << ( totf + mod - totg ) % mod;
return 0;
}
H. 4.树的合并
新生成的树上的直径只可能在以下三种情况中选取:
- 第一棵树的直径
- 第二颗树的直径
- 新生成的最长链(连的两个点在两棵树中延伸的最长距离+1)
那么我们首先求出左右两棵树的直径的最大值(记为
那么我们对于
计入答案为
即为前面
注意
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
#define int long long
const int N = 2e5 + 5;
const int inf = 0x3f3f3f3f;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , s , dis1[N] , dis2[N] , f1[N] , f2[N] , maxlen , maxx , sum[N] , ans;
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void init()
{
memset ( dis1 , 0 , sizeof dis1 );
memset ( dis2 , 0 , sizeof dis2 );
maxlen = 0 , s = 0;
}
void dfs1 ( int u , int ff )
{
if ( dis1[u] > maxlen ) maxlen = dis1[u] , s = u;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dis1[v] = dis1[u] + 1;
dfs1 ( v , u );
}
}
void dfs2 ( int u , int ff )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == ff ) continue;
dis2[v] = dis2[u] + 1;
dfs2 ( v , u );
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
dfs1 ( 1 , 0 );
memset ( dis1 , 0 , sizeof dis1 );
maxlen = 0 , dfs1 ( s , 0 );
maxx = maxlen;//记录左面直径长度
maxlen = 0 , dfs2 ( s , 0 );
for ( int i = 1 ; i <= n ; i ++ ) f1[i] = max ( dis1[i] , dis2[i] );
init();
for ( int i = 1 , u , v ; i < m ; i ++ ) u = read() , v = read() , add ( u + n , v + n ) , add ( v + n , u + n );
dfs1 ( n + 1 , 0 );
memset ( dis1 , 0 , sizeof dis1 );
maxlen = 0 , dfs1 ( s , 0 );
maxx = max ( maxx , maxlen );//记录左面直径长度
maxlen = 0 , dfs2 ( s , 0 );
for ( int i = n + 1 ; i <= n + m ; i ++ ) f2[i] = max ( dis1[i] , dis2[i] );//注意m2中的所有处理都是从n+1开始的
sort ( f1 + 1 , f1 + n + 1 , [](const int a , const int b) { return a > b; } );
for ( int i = 1 ; i <= n ; i ++ ) sum[i] = sum[i-1] + f1[i];
for ( int i = n + 1 ; i <= n + m ; i ++ )
{
int pos = lower_bound ( f1 + 1 , f1 + n + 1 , maxx - f2[i] - 1 , greater<int>() ) - f1 - 1;//大于maxx-f2[i]的最后一个
ans += sum[pos] + ( f2[i] + 1 ) * pos + ( n - pos ) * maxx;
}
cout << ans << endl;
return 0;
}
I. 5.权值统计
一道神题 先放代码 留坑待填
我们用
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 10;
const int mod = 10086;
int s , n , tot , a[N] , ans , maxx;
int f[N];
//g[i]表示以节点u为根的联通块总个数,f[i]表示以节点u为根且不包含这个最大喜好值的联通块个数
int head[N] , cnt;
//f[u]表示u子树的答案
struct edge
{
int to , nxt;
}e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
void dfs ( int u , int fa )
{
int s1 = 0 , s2 = 0 , s3 = 0;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == fa ) continue;
dfs ( v , u );
s1 += f[v];//子树答案之和
s2 += f[v] * f[v];
}
f[u] = ( s1 + 1 ) * a[u] % mod;
s3 = ( ( s1 * s1 - s2 ) >> 1 ) % mod;//s3是子树答案和
ans = ( ans + f[u] + s3 * a[u] ) % mod;
}
signed main()
{
scanf ( "%lld" , &n );
for ( int i = 1 ; i <= n ; i ++ ) scanf ( "%lld" , &a[i] ) , maxx = max ( maxx , a[i] );
for ( int i = 1 , u , v ; i < n ; i ++ ) scanf ( "%lld%lld" , &u , &v ) , add ( u , v ) , add ( v , u );
dfs ( 1 , 0 );
printf ( "%lld" , ans );
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探