树上背包
书上背包 -> 树型dp 和 背包 结合的问题,对于背包问题如果熟悉的话其实是很简单的一个算法。
树上背包模板
题目大意:
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有若干学分,分别记作 s1,s2,⋯,s__N,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?
题目保证课程安排无冲突。(即不会有 a 是 b 的先修课,b 也是 a 的先修课这类情况存在。)
1≤N≤300 , 1≤M≤300, 若 ki=0 表示没有直接先修课 (0≤ki≤N,1≤si≤20)。
解题:树上背包
背包的题一般就有很模式化的状态表示,都是对于一个物品的选与不选的问题;而对于树形dp,对于一个节点的信息需要到其子节点的相应信息。
在本题中对于父节点的选课情况,根据当前节点的选与不选来分情况,然后从其子节点中获取相应的信息,所以本题的解法是树上背包。
定义状态表示:dp[x][i][j]: 从x的前i个子树中选j门课程的最大学分。
初始化:本题中课程的选择依据拓扑序,因此如果想要获取子节点的信息,根节点必选,dp[x][][1] = a[x]
状态转移方程:从x的孩子节点中选,显然是一个完全背包的问题,而从孩子节点中选择的节点的个数就是常规背包中的背包容量,需要额外枚举一个维度k,用来表示从子结点中选择的课程数量。
因此对于一个孩子节点的转移:
- \(dp[x][i][j] = \max_{k=0}^{j-1}(dp[x][i - 1][j - k] + dp[y][edges[y].size()][k])\),由于根节点必选,所有k最多到j -1.
对于所有的孩子节点,把信息并入到x节点,仅需循环x的所有子节点一遍即可。
答案:dp[0][edgse[0].size()][m]
code:
#include <iostream>
#include <vector>
using namespace std;
const int N = 3e2 + 10;
// dp[i][j][k]: 表示从i的子树中选,[j][k]构成一个分组背包,表示从前j个孩子中选,一共选k个结点,整体表示贡献的最大值
// 注意i号结点必选
int a[N], dp[N][N][N];
int n, m;
vector<int> edges[N];
// 时间复杂度O(n^3)
void dfs(int x)
{
// 从x的子树中选, 由于x必选,所有从前i个子树中选1个的情况总是a[x]
for (int i = 0; i <= m; i++) dp[x][i][1] = a[x];
for (int i = 0; i < edges[x].size(); i++)
{
int y = edges[x][i];
dfs(y);
for (int j = 2; j <= m; j++)
{
for (int k = 0; k < j; k++)
{
// 这里要取max,因为dp[x][i + 1][j]这个状态会进来多次
// 然后完全背包的val就是子树的dp值,weight就是在当前子树中选择的结点个数
dp[x][i + 1][j] = max(dp[x][i][j], dp[x][i + 1][j]);
dp[x][i + 1][j] = max(dp[x][i + 1][j], dp[x][i][j - k] + dp[y][edges[y].size()][k]);
}
}
}
}
int main()
{
cin >> n >> m;
m++;
for (int i = 1; i <= n; i++)
{
int x; cin >> x;
edges[x].push_back(i);
cin >> a[i];
}
dfs(0);
cout << dp[0][edges[0].size()][m] << endl;
return 0;
}
时间复杂度:O(n^3).
到这里来用背包的角度总结一下:
对于x的所有的子节点y。
- 子节点y,每个节点对应一个组,整个x节点状态转移的过程是一个完全背包的过程。
- 考虑y中的单一节点,我们枚举的是从中选择几个节点k,k即是物品的体积,背包的容量就是选择课程的数量。
- 然后子节点我们考虑的仅仅是它选完自己所有的子节点之后的dp值,这个值就是背包中的价值。
- 至于子节点的第二维选择的过程我们是不关心的,而且后两位组成的是完全背包,所以可以优化掉这一维。
状态表示:dp[x][j]:从x的子树中选择j个课程的最大学分。
状态转移:$dp[x][j] = \max_{k=0}^{j - 1}(dp[x][j-k]+dp[y][k])
$
初始化:dp[x][1] = a[x]
答案:dp[0][m]
转移循序:由于优化掉了第二维,分组背包的转移逻辑逆序遍历。
code:
#include <iostream>
#include <vector>
using namespace std;
const int N = 310;
// dp[i][j]: 从i的子树中选,更新了i的子树的size次之后,选择j个的最大贡献
// 实际上i总数需要子树的更新size轮之后的值,因此空间优化是可行的
int dp[N][N], a[N];
int n, m;
vector<int> edges[N];
void dfs(int x)
{
// 从x的子树中选,x必选,选择一种的答案总是a[x]
dp[x][1] = a[x];
// 由于不关心孩子结点的编号,这里直接走正常dfs的逻辑即可
// 其实这里枚举的就是i
for(auto& y : edges[x])
{
dfs(y);
// 把子树y并入到x的dp值中
// 注意空间优化之后要改变遍历方向
for(int j = m; j >= 2; j--)
for(int k = 0; k < j; k++)
dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[y][k]);
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int k; cin >> k;
edges[k].push_back(i);
cin >> a[i];
}
m++;
dfs(0);
cout << dp[0][m] << endl;
return 0;
}
优化一:上下界优化
令当前遍历到的x节点的个数为sz[x], 子树y的个数为sz[y]。
- 对于 j 这一维度,上界不会超过sz[x] + sz[y]。
- 对于k 这一维度,上界为min(j - 1, sz[y]), 下界为 max(1, j - sz[x])。
优化后的时间复杂度近似O(n^2), 某些情况下会被卡大
code:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
vector<vector<int>> dp;
int n, m;
int a[N], sz[N];
vector<int> edges[N];
// 这个是上下界优化的版本,时间复杂度近似O(n^2),也会被卡大
void dfs(int x)
{
dp[x][1] = a[x];
sz[x] = 1;
for (auto &y : edges[x])
{
dfs(y);
for (int j = min(m, sz[x] + sz[y]); j >= 2; j--)
for (int k = max(1, j - sz[x]); k <= min(sz[y], j - 1); k++)
dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[y][k]);
sz[x] += sz[y];
}
}
int main()
{
cin >> n >> m;
m++;
dp.resize(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i++)
{
int k;
cin >> k;
edges[k].push_back(i);
cin >> a[i];
}
dfs(0);
cout << dp[0][m] << endl;
return 0;
}
优化二:dfn序
在逆序遍历dfn序时,会逐渐还原出整棵树。
状态表示:dp[i][j]: 表示dfn序为 [i, n] 的节点中选 j 个课程的最大学分。
状态转移:
令dfn序为i的节点为x
- 由于如果节点 x 不选的话,其子树都不能选,dp[i][j] = dp[i + sz[x]][j].
- 如果节点x选了,那么从其子树中选,dp[i][j] = dp[i + 1][j - 1] + a[x].
- \(dp[i][j] = max(dp[i + sz[x]][j], dp[i + 1][j - 1] + a[x])\).
code:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
vector<int> edges[N];
int a[N], n, m;
// dp[i][j]: 从dfn序[i, n] 中选则j个节点的最大价值
// 由于dfn序的特点,当遍历到i的时候后面的结点及其子树已经遍历完成了
vector<vector<int>> dp;
int sz[N], dfn[N], seg[N], idx;
// dfn序优化树上背包,时间复杂度O(nm)
void dfs(int x)
{
dfn[x] = ++idx;
seg[idx] = x;
sz[x] = 1;
for (auto &y : edges[x])
{
dfs(y);
sz[x] += sz[y];
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int k;
cin >> k;
edges[k].push_back(i);
cin >> a[i];
}
m++;
dfs(0);
dp.resize(n + 10, vector<int>(m + 10));
for (int i = n + 1; i >= 1; i--)
{
int x = seg[i];
for (int j = 1; j <= m; j++)
{
// i 不选 -> i 的子树都不能选,[i + sz[x] + 1, n]
// i 选 -> [i + 1, n] + a[x]
dp[i][j] = max(dp[i + sz[x]][j], dp[i + 1][j - 1] + a[x]);
}
}
cout << dp[1][m] << endl;
return 0;
}
练习题
二叉苹果树
#include <iostream>
#include <vector>
using namespace std;
typedef long long LL;
typedef pair<int, LL> PII;
const int N = 110, M = 3e4 + 10;
// dp[i][j]: 从dfn序[i, n] 中选j个点的最大价值
LL dp[N][M];
int n, m;
LL w[N];
vector<PII> edges[N];
int sz[N], idx, dfn[N], seg[N];
void dfs(int x, int fa)
{
dfn[x] = ++idx;
seg[idx] = x;
sz[x] = 1;
for(auto& [y, z] : edges[x])
{
if(y == fa) continue;
dfs(y, x);
sz[x] += sz[y];
w[y] = z;
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n - 1; i++)
{
int x, y; LL z;
cin >> x >> y >> z;
edges[x].push_back({y, z});
edges[y].push_back({x, z});
}
dfs(1, 0);
for(int i = n; i >= 2; i--)
{
int x = seg[i];
for(int j = 1; j <= m; j++)
{
dp[i][j] = dp[i + sz[x]][j];
if(j - 1 >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + w[x]);
}
}
LL ans = 0;
// for(auto& [y, z] : edges[1]) ans = max(ans, dp[dfn[y]][m]);
// cout << ans << endl;
cout << dp[2][m] << endl;
return 0;
}
树形背包
#include <iostream>
#include <vector>
using namespace std;
const int N = 5e4 + 10;
typedef long long LL;
vector<int> edges[N];
vector<vector<LL>> dp;
int n, m;
int sz[N], idx, dfn[N], seg[N];
LL w[N], v[N];
void dfs(int x)
{
dfn[x] = ++idx;
seg[idx] = x;
sz[x] = 1;
for(auto& y : edges[x])
{
dfs(y);
sz[x] += sz[y];
}
}
// 本题是树上01背包,采用的dfn序优化背包转移
// dp[i][j]: 从dfn序[i, n]中选j个物品的最大价值
// 转移的方式和选课几乎一摸一样
// 上一题是对于子树的分组背包,即可以从x的子树中选择1-sz个结点
// 本题其实也可以理解成分组背包,即从子树中选择不超过1-m的重量的最大价值
// 但本质都是01背包,都是单一结点的选于不选的问题,dfn序优化比价容易体现这个问题
int main()
{
cin.tie(0); cout.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int x; cin >> x;
edges[x].push_back(i);
}
for(int i = 1; i <= n; i++) cin >> w[i];
for(int i = 1; i <= n; i++) cin >> v[i];
dfs(0);
dp.resize(n + 10, vector<LL> (m + 10));
for(int i = n + 1; i >= 1; i--)
{
int x = seg[i];
for(int j = 0; j <= m; j++)
{
dp[i][j] = dp[i + sz[x]][j];
if(j - w[x] >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - w[x]] + v[x]);
}
}
cout << dp[1][m] << endl;
return 0;
}
重建道路
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 200;
const int INF = 0x3f3f3f3f;
int dp[N][N];
int n, p;
int d[N];
vector<int> edges[N];
void dfs(int x)
{
dp[x][1] = d[x];
for(auto& y : edges[x])
{
dfs(y);
for(int j = p; j >= 2; j--)
{
for(int k = 0; k < j; k++)
{
dp[x][j] = min(dp[x][j], dp[x][j - k] + dp[y][k] - 2);
}
}
}
}
int main()
{
cin >> n >> p;
for(int i = 1; i <= n - 1; i++)
{
int x, y; cin >> x >> y;
edges[x].push_back(y);
d[x]++; d[y]++;
}
int ret = INF;
memset(dp, 0x3f, sizeof dp);
dfs(1);
for(int i = 1; i <= n; i++) ret = min(ret, dp[i][p]);
cout << ret << endl;
return 0;
}
有线电视网
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 3010;
int n, m, w[N];
vector<pair<int, int>> edges[N];
int f[N][N], cnt[N];
void dfs(int x)
{
f[x][0] = 0;
if (x > n - m)
{
f[x][1] = w[x];
cnt[x] = 1;
return;
}
for (auto &t : edges[x])
{
int y = t.first, z = t.second;
dfs(y);
for (int j = cnt[x] + cnt[y]; j >= 1; j--)
for (int k = 1; k <= min(j, cnt[y]); k++)
f[x][j] = max(f[x][j], f[x][j - k] + f[y][k] - z);
cnt[x] += cnt[y];
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n - m; i++)
{
int k;
cin >> k;
while (k--)
{
int a, c;
cin >> a >> c;
edges[i].push_back({a, c});
}
}
for (int i = n - m + 1; i <= n; i++)
cin >> w[i];
memset(f, -0x3f, sizeof f);
dfs(1);
for (int i = m; i >= 0; i--)
{
if (f[1][i] >= 0)
{
cout << i;
break;
}
}
return 0;
}
浙公网安备 33010602011771号