ACM 树上背包DP总结
题目一
CCF 201909-5 城市规划(http://118.190.20.162/view.page?gpid=T90)
题目大意
思路
表示在u这棵子树(含u)里选了i个点的最小代价。
考虑转移:
假设这棵子树选择了q个,那么这条边经过了(k-q)*q次。注意的是:背包的DP必须从大到小或者用辅助数组。
#include <bits/stdc++.h>
#define LL long long
const LL INF=0x3f3f3f3f3f3f3f3fll;
using namespace std;
struct node{
int to, w;
};
vector<node> v[50005];
int s[50005]={0};
LL num[50005]={0};
LL f[50005][105]={0}, t[105];
int n, m, k;
void dfs(int u, int fa){
f[u][0]=0;
if(s[u]==1){
f[u][1]=0;
num[u]=1;
}
for(int r=0; r<v[u].size(); r++){
int to=v[u][r].to, w=v[u][r].w;
if(to!=fa){
dfs(to, u);
int sum=min(1ll*k ,num[u]+num[to]), mn=min(1ll*k, num[u]);
for(int i=0; i<=sum; i++){
t[i]=f[u][i];
}
for(int p=0; p<=mn; p++){//其余子树i个
for(int q=0; p+q<=sum; q++){//当前子树j个
t[p+q]=min(t[p+q], f[u][p]+f[to][q]+(q)*(k-q)*1ll*w);
}
}
for(int i=0; i<=sum; i++){
f[u][i]=min(f[u][i], t[i]);
}
num[u]+=num[to];
}
}
}
int main()
{
int u, to, w;
scanf("%d%d%d", &n, &m, &k);
for(int i=1; i<=m; i++){
scanf("%d", &u);
s[u]=1;
}
memset(f, INF, sizeof(f));
for(int i=1; i<=n-1; i++){
scanf("%d%d%d", &u, &to, &w);
v[u].push_back(node{to, w});
v[to].push_back(node{u, w});
}
dfs(1, 0);
printf("%lld\n", f[1][k]);
return 0;
}
题目二
Wannafly挑战赛27 蓝魔法师(https://ac.nowcoder.com/acm/problem/20811)
题目描述
“你,你认错人了。我真的,真的不是食人魔。”--蓝魔法师
给出一棵树,求有多少种删边方案,使得删后的图每个连通块大小小于等于k,两种方案不同当且仅当存在一条边在一个方案中被删除,而在另一个方案中未被删除,答案对998244353取模
输入描述:
第一行两个整数n,k, 表示点数和限制
2 <= n <= 2000, 1 <= k <= 2000
接下来n-1行,每行包括两个整数u,v,表示u,v两点之间有一条无向边
保证初始图联通且合法
输出描述:
共一行,一个整数表示方案数对998244353取模的结果
示例1
输入
5 2
1 2
1 3
2 4
2 5
输出
7
思路
表示u的所有子树满足条件,并且u在的连通块的大小为i的方案树。
考虑转移:
u和to这棵子树不连通, 那么to子树的所有方案都满足条件。
如果u和to连通,考虑u这棵子树选择了p个,to选择了q个。
时间复杂度分析
每次合并两棵子树 u, v 时,需要花费 siz[u] * siz[v] 的代价,所以总复杂度是 。整体来看,这就是说每个点对只会在 LCA 处,对复杂度有 1 的贡献,所以复杂度是点对数,也就是 O(n^2)。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const LL mod=998244353;
LL f[2005][2005], num[2005];
LL T[2005], siz[2005];
LL k;
struct Edge{
int to, nxt;
}e[5005];
int cut=0, head[2005];
void addedge(int u, int v){
e[++cut]={v, head[u]};
head[u]=cut;
}
void DFS(int u, int fa) {
f[u][1]=siz[u]=1;
for(int i=head[u]; i; i=e[i].nxt) {
int x=e[i].to;
if(x==fa) continue;
DFS(x, u);
for(int i=1; i<=k; i++) {
T[i]=0;
}
int n=min(siz[u], k), m=min(k, siz[u]+siz[x]);
for (int p = 1; p <= n; p++) { // 枚举已经处理了的包含根节点的子树对新连通块的贡献;
for (int q = 0; q <= siz[x] && p + q <= m; q++) // 当前准备处理的子树对新连通块的贡献;
// 新联通块的大小为 p + q,贡献的方案数为 f[u][p] * f[to][q];
T[p + q] = (T[p + q] % mod + f[u][p] * f[x][q] % mod) % mod;
}
for(int i=1; i<=k; i++) {
f[u][i]=T[i];// 用T暂存再赋值回去;
}
siz[u]+=siz[x];// 更新已经处理了的子树的大小;
}
for(int i=1; i<=k; i++) {// 将以u为根的所有的可行方案数存在dp[u][0]里;
f[u][0]+=f[u][i];
f[u][0]%=mod;
}
}
int main() {
int n, x, y;
scanf("%d%lld", &n, &k);
for(int i=1; i<n; i++) {
scanf("%d%d", &x, &y);
addedge(x, y);
addedge(y, x);
}
DFS(1, 0);
printf("%lld\n", f[1][0]);
return 0;
}
题目三
P2014 [CTSC1997]选课 (https://www.luogu.com.cn/problem/P2014)
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 NN 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程a是课程b的先修课即只有学完了课程a才能学习课程b)。一个学生要从这些课程里选择M门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数 N , M 用空格隔开。( 1<=N<=300, 1≤M≤300 )
接下来的 N 行,第 I+1行包含两个整数 ki和si, ki表示第I门课的直接先修课,si表示第I门课的学分。若 ki=0表示没有直接先修课
(1≤ki≤N , 1≤si≤20)。
输出格式
只有一行,选 MM 门课程的最大得分。
输入
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出
13
思路
题目条件可以建立一颗树
表示在u的子树选择i个节点(必须和u连通)的最大得分。
考虑转移:f[u][p+q]=max(f[u][p+q], f[u][p]+f[to][q]);
因为必须连通,那么p>=1。
从大到小DP就可以了。
因为可能是一个森林,我们把所有的树的f数组DP出来。再进行分组背包就可以了。
表示在前i棵树选择j个节点的最大得分。
这里我写复杂了,应该用0节点连接的树,那么一次树形DP就可以了
#include<bits/stdc++.h>
#define LL long long
using namespace std;
struct Edge {
int to, nxt;
} e[5005];
int cut=0, head[2005];
void addedge(int u, int v) {
e[++cut]= {v, head[u]};
head[u]=cut;
}
int n, M;
int w[305], f[305][305], siz[305];
int dp[305][305];
void DFS(int u){
siz[u]++;
f[u][1]=w[u];
for(int i=head[u]; i; i=e[i].nxt){
int to=e[i].to;
DFS(to);
int np=min(M, siz[u]), nq=min(M, siz[to]);
for(int p=np; p>=1; p--){//u选择的个数,有先修的条件,至少选择一个才能选择子树
for(int q=min(M-p, nq); q>=0; q--){//v选择的个数
f[u][p+q]=max(f[u][p+q], f[u][p]+f[to][q]);
}
}
siz[u]+=siz[to];
}
// for(int i=1; i<=siz[u]; i++){
// printf("f[%d][%d]=%d\n", u, i, f[u][i]);
// }
}
int main() {
vector<int> a;
scanf("%d%d", &n, &M);
for(int i=1; i<=n; i++) {
int x; scanf("%d%d", &x, &w[i]);
if(x) {
addedge(x, i);
} else {
a.push_back(i);
}
}
//树形DP背包
for(auto x: a){
DFS(x);
}
//分组背包
for(int i=1; i<=a.size(); i++){
int x=a[i-1];
for(int s=0; s<=M; s++){
for(int k=1; k<=siz[x]; k++){
dp[i][s]=max(dp[i][s], dp[i-1][s]);
if(s>=k){
dp[i][s]=max(dp[i][s], dp[i-1][s-k]+f[x][k]);
}
}
}
}
printf("%d\n", dp[a.size()][M]);
return 0;
}
题目四
P1273 有线电视网 (https://www.luogu.com.cn/problem/P1273)
题目描述
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入格式
输入文件的第一行包含两个用空格隔开的整数N和M,其中2≤N≤3000,1≤M≤N-1,N为整个有线电视网的结点总数,M为用户终端的数量。
第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。
接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。
输出格式
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。
输入
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
输出
2
说明/提示
样例解释
如图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:
2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。
思路
和前面的思路一样:表示u节点在子树选择了j个用户的最小花费。
考虑转移
在叶子节点一定是用户:
答案就是f[1][i]>=0的最大的i。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
struct Edge {
int to, w, nxt;
} e[10000010];
int cut=0, head[3005], s[3005];
void addedge(int u, int v, int w) {
e[++cut]= {v, w, head[u]};
head[u]=cut;
}
int n, m;
int siz[3005], f[3005][3005];
void DFS(int u){
if(u>n-m){//一定是叶子节点
f[u][0]=0; f[u][1]=s[u]; siz[u]=1;
return ;
}
f[u][0]=0;//非叶子节点
for(int i=head[u]; i; i=e[i].nxt){
int to=e[i].to, w=e[i].w;
DFS(to);
for(int p=siz[u]; p>=0; p--){
for(int q=siz[to]; q>=0; q--){
f[u][p+q]=max(f[u][p+q], f[u][p]+f[to][q]-(q?w:0));
}
}
siz[u]+=siz[to];
}
}
int main() {
memset(f, ~0x3f, sizeof(f));
scanf("%d%d", &n, &m);
for(int i=1; i<=n-m; i++){
int k; scanf("%d", &k);
while(k--){
int x, y; scanf("%d%d", &x, &y);
addedge(i, x, y);
}
}
for(int i=n-m+1; i<=n; i++){
scanf("%d", &s[i]);
}
DFS(1);
for(int i=m; i>=0; i--){
if(f[1][i]>=0){
printf("%d\n", i);
break;
}
}
return 0;
}
题目五
P2015 二叉苹果树(https://www.luogu.com.cn/problem/P2015)
题目描述
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)
这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第1行2个数,N和Q(1<=Q<= N,1<N<=100)。
N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。
每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。
每根树枝上的苹果不超过30000个。
输出格式
一个数,最多能留住的苹果的数量。
输入
5 2
1 3 1
1 4 10
2 3 20
3 5 20
输出
21
思路
和题目三不一样的是这里是边权。
i的子树选择了i条边的最大权值,向to转移时,u-to这条边必须选。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
struct Edge {
int to, w, nxt;
} e[10010];
int cut=0, head[3005], s[3005];
void addedge(int u, int v, int w) {
e[++cut]= {v, w, head[u]};
head[u]=cut;
}
int n, m;
int siz[3005], f[3005][3005];
void DFS(int u, int fa){
for(int i=head[u]; i; i=e[i].nxt){
int to=e[i].to, w=e[i].w;
if(to!=fa){
DFS(to, u);
int np=min(siz[u], m);
for(int p=siz[u]; p>=0; p--){
for(int q=min(siz[to], m-p); q>=1; q--){
f[u][p+q]=max(f[u][p+q], f[u][p]+f[to][q-1]+w);//必须选择u-to
}
}
siz[u]+=siz[to];
}
}
siz[u]++;
}
int main() {
scanf("%d%d", &n, &m);
for(int i=1; i<n; i++){
int x, y, w; scanf("%d%d%d", &x, &y, &w);
addedge(x, y, w);
addedge(y, x, w);
}
DFS(1, 0);
printf("%d\n", f[1][m]);
return 0;
}
题目六
poj1947 Rebuilding Roads(http://poj.org/problem?id=1947)
题目描述
有一个N(1<=N<=150)节点的树,问你删除最少的边使得到一个大小为P(1<=P<= N)的子树连通块。
输入
-
Line 1: N 和 P
-
Lines 2..N: N-1 条边
输出
删除最少的边,得到一个大小为P的连通块。
Sample Input
11 6
1 2
1 3
1 4
1 5
2 6
2 7
2 8
4 9
4 10
4 11
Sample Output
2
Hint
[A subtree with nodes (1, 2, 3, 6, 7, 8) will become isolated if roads 1-4 and 1-5 are destroyed.]
思路
:以u为根,并且这个连通块的大小为i的最少删除的边数。初始化:f[u][1]=G[u].size()
转移时:
在u-同转移时,-2是因为在之前的状态f[u][i]是断开u-to这条边的,并且f[to][i]是to为根,也是断开u-to,现在u-to连接了。那么多删除了2次。
#include <bits/stdc++.h>
#define LL long long
using namespace std;
vector<int> G[155];
int n, m, ans=1<<30;
int siz[155], f[155][155];
void DFS(int u, int fa){
f[u][1]=G[u].size();
siz[u]=1;
for(int i=0; i<G[u].size(); i++){
//cout<<u<<"-"<<to<<endl;
int to=G[u][i];
if(to!=fa){
DFS(to, u);
int np=min(m, siz[u]);
for(int p=np; p>=1; p--){
for(int q=min(siz[to], m-p); q>=1; q--){
f[u][p+q]=min(f[u][p+q], f[u][p]+f[to][q]-2);
}
}
siz[u]+=siz[to];
}
}
//cout<<u<<" "<<m<<" "<<f[u][m]<<endl;
ans=min(ans, f[u][m]);
}
int main() {
memset(f, 0x3f, sizeof(f));
scanf("%d%d", &n, &m);
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);
}
DFS(1, 0);
printf("%d\n", ans);
return 0;
}
题目七
[HAOI2010]软件安装 (https://ac.nowcoder.com/acm/problem/19981)
题目描述
现在我们的手头有N个软件,对于一个软件i,它要占用Wi的磁盘空间,它的价值为Vi。我们希望从中选择一些软件安装到一台磁盘容量为M计算机上,使得这些软件的价值尽可能大(即Vi的和最大)。
但是现在有个问题:软件之间存在依赖关系,即软件i只有在安装了软件j(包括软件j的直接或间接依赖)的情况下才能正确工作(软件i依赖软件j)。幸运的是,一个软件最多依赖另外一个软件。如果一个软件不能正常工作,那么它能够发挥的作用为0。
我们现在知道了软件之间的依赖关系:软件i依赖软件Di。现在请你设计出一种方案,安装价值尽量大的软件。一个软件只能被安装一次,如果一个软件没有依赖则Di=0,这时只要这个软件安装了,它就能正常工作。
输入描述
第1行:N, M (0 ≤ N ≤ 100, 0 ≤ M ≤ 500)
第2行:W1, W2, ... Wi, ..., Wn (0 ≤ Wi ≤ M )
第3行:V1, V2, ..., Vi, ..., Vn (0 ≤ Vi ≤ 1000 )
第4行:D1, D2, ..., Di, ..., Dn (0 ≤ Di ≤ N, Di≠i )
输出描述
一个整数,代表最大价值。
输入
3 10
5 5 6
2 3 4
0 1 1
输出
5
思路
首先读题要仔细——安装和运行在这里是两个概念。安装相当于去选择一个软件集合,而在选择完所有的以后才能开始运行。我们可以选择建图,从d[i]向i连边。通过读题不难发现所有节点的入度都不超过1。因此对于每一个连通块,要么是一个环,要么是一个DAG。而对于一个环,要么全部安装并运行,要么全部不安装(不可能安装一部分的环)。因此自然而然的,我们可以把环看做一个点,因此首先进行缩点。
缩点完了之后,整张图就变成了一个多个连通块的DAG。由于不存在环,每个连通块有且仅有存在入度为0的点。从一个虚拟根节点像所有入度为0的点连边,就形成了一个联通的DAG—由于入度不超过1,可以将其看做一棵树。
于是问题转化为树上的01背包,前提是要选子树的话必须选择该子树的根节点。
考虑树形背包的做法(这一点非常关键)。一般我们都令dp[u][j]表示以u为根节点的子树中,总空间设定为j时的最大价值。然而这样的定义经常使人受到误导——这让人觉得很难转移。
实际上我们定义的原型应该是dp[u][i][j]表示以u为根节点的子树中,前i棵子树中,总空间设定为j时的最大价值。因此我们有状态转移方程
那为什么在普通的定义中没有ii呢?因为ii已经通过滚动数组优化了
这个方程的实质是爆扫——枚举当前子树分配到的空间,剩下的空间给之前的。但是这里有一个特殊的点,就是根节点必须选。这就意味着必须包含根节点,其实质也就是。这样每次转移的时候不至于让根节点消失了。
#include<bits/stdc++.h>
using namespace std;
struct egde {
int u, to, nxt;
} e[500005];
int head[500005], cut=0;
int Addedge(int x, int y) {
e[++cut]= {x, y, head[x]};
head[x]=cut;
}
int low[500005], dfn[500005], vis[500005], T=0, N=0;
int scc[500005];
stack<int> sk;
void getscc(int u) {
low[u]=dfn[u]=++T;
sk.push(u);
vis[u]=1;
for(int i=head[u]; i; i=e[i].nxt) {
int to=e[i].to;
if(!dfn[to]) {
getscc(to);
low[u]=min(low[u], low[to]);
} else if(vis[to]) {
low[u]=min(low[u], dfn[to]);
}
}
if(low[u]==dfn[u]) {
++N;
while(1) {
int now=sk.top();
sk.pop();
scc[now]=N;
vis[now]=0;
if(now==u) {
break;
}
}
}
}
int w[105], v[105], d[105];
int mp[105][105];
int xx[105], yy[105];
vector<int> G[105];
int W[105], V[105];
int f[105][1005];
int siz[105];
int n, m;
void DFS(int u){
for(int j=W[u]; j<=m; ++j){
f[u][j]=V[u];
}
for(auto x: G[u]){
DFS(x);
for(int j=m; j>=W[u]; --j){//j>=W[u]因为u节点选择
for(int k=0; k<=j-W[u]; ++k){
f[u][j]=max(f[u][j], f[u][j-k]+f[x][k]);
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++) {
scanf("%d", &w[i]);
}
for(int i=1; i<=n; i++) {
scanf("%d", &v[i]);
}
for(int i=1; i<=n; i++) {
int x;
scanf("%d", &x);
if(x!=0) {
Addedge(x, i);
xx[i]=x, yy[i]=i;
}
}
for(int i=1; i<=n; i++) {
if(!dfn[i]) {
getscc(i);
}
}
for(int i=1; i<=n; i++) {//遍历每条边
if(yy[i]!=0){
int x=scc[xx[i]], y=scc[yy[i]];
if(mp[x][y]==0&&x!=y){
mp[x][y]=1;
G[x].push_back(y);
d[y]++;//入度
}
}
}
for(int i=1; i<=n; i++){//每个连通块的W[i], V[i]
int x=scc[i];
W[x]+=w[i], V[x]+=v[i];
}
for(int i=1; i<=N; i++){//建虚根
if(d[i]==0){
G[0].push_back(i);
}
}
DFS(0);//DP
printf("%d\n", f[0][m]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了