分组背包+树上DP
题目链接:https://www.luogu.com.cn/problem/P1273
题意:
转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
找出一个方案使得有限电视网在不亏本的情况下使得观看转播的用户人数尽可能的多。
解答:
分组背包+树上DP。f[u][i] 表示 以 u 为根节点,选择i名用户所得到的价值。
每个子节点的选择需要依赖父节点,由父节点 dfs 遍历到子节点,然后回溯将 f[u][i] 由子节点更新到父节点。
最后从 m 到 0 遍历,价值大于 0 的 f[u][i] 即为最大的用户人数。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 3e3+5;
int cnt = 0, n, m, K, a, c;
int val[N], head[N];
int f[N][N];
struct Edge{
int to, w, next;
}edge[N];
void add_edge(int u, int v, int w) { // 链式前向星 建树
edge[cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].w = w;
head[u] = cnt++;
}
// 若要选择v号节点的物品,必须先选择v的父亲节点上的物品
int dfs(int u) {
if (u >= (n-m+1)) { // 判断是否为 用户
f[u][1] = val[u]; // 表示 以 u 为根节点 选择1个用户所得到的价值
return 1; // 返回用户个数 1
}
int sum = 0, now = 0;
for (int i=head[u]; i!=-1; i=edge[i].next) { // 遍历每个节点的子节点
int v = edge[i].to;
now = dfs(v); // 继续往下遍历 并得到当前的(以v为根节点的子树)用户人数
// 以下为遍历到底后 回溯进行的操作 逐步从子节点更新到父节点
sum += now; // sum 为 父节点 和 子节点总共可选的用户数 (回溯时不断累加)
// 分组背包
for (int j=sum; j>=0; j--) { // 倒着遍历 防止重复选择 (组内平行)
for (int k=1; k<=now; k++) { // 对子节点可选的用户数 遍历 得到 局部最优解的一种情况
if (j>=k) f[u][j] = max(f[u][j], f[u][j-k]+f[v][k]-edge[i].w);
}
}
}
return sum; // 回溯后 用户数会增加
}
int main() {
memset(f, ~0x7f, sizeof(f)); // 由于其中可能出现负数 故将 f 数组元素的值设为一个极小值
memset(head, -1, sizeof(head));
cin >> n >> m;
for (int i=1; i<=n; i++) f[i][0] = 0; // 选择0个用户没有价值
for (int i=1; i<=(n-m); i++) { // 建树
cin >> K;
while (K--) {
cin >> a >> c;
add_edge(i, a, c);
}
}
for (int i=n-m+1; i<=n; i++) cin >> val[i]; // 用户价值
dfs(1);
for (int i=m; i>=0; i--) { // 从最大用户数 往下遍历
if (f[1][i] >=0) { // 价值大于等于0 说明未亏本所能选择的最大用户数
cout << i << endl;
break;
}
}
return 0;
}