DP 优化小技巧
收录一些比较冷门的 DP 优化方法。
1. 树上依赖性背包
树上依赖性背包形如在树上选出若干个物品做背包问题,满足这些物品连通。由于 01 背包,多重背包和完全背包均可以在
先考虑一个弱化版问题。给定一棵有根树,若一个节点被选,则它的父亲必须被选。
显然存在一个
接下来引出科技:树上依赖性背包。我们发现对每个节点都求答案似乎有些累赘,因为我们只关心以
让我们用更严谨的语言描述上述过程。不妨设节点已经按照它们的 dfs 序排好序了,节点
设
- 如果节点
被选择,那么只需它的儿子子树满足限制。换言之,选择节点 之后,它们的儿子可以选择选或者不选,这个选择的自由留给子节点决策,所以只有节点 是否被选择的信息固定了下来的。因此 。这里 表示将物品 加入背包 。 - 如果节点
不被选择,那么它的整棵子树也不能选。所以它的整棵子树的状态就确定了下来:均不选。因此 。
注意这里
不难发现我们在
I. P6326 Shopping
给出代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 500 + 5;
const int M = 4e3 + 5;
int n, m, ans;
int w[N], c[N], d[N];
vector <int> e[N];
struct Knapsack {
int a[M];
void clear() {memset(a, 0, M << 2);}
void merge(Knapsack rhs) {for(int i = 0; i <= m; i++) a[i] = max(a[i], rhs.a[i]);}
void insert(int c, int w, int v) {
static int d[M], f[M], hd, tl;
memset(f, 0xcf, M << 2);
for(int i = 0; i < w; i++) {
d[hd = tl = 1] = i;
for(int j = i + w; j <= m; j += w) {
while(hd <= tl && d[hd] + c * w < j) hd++;
f[j] = a[d[hd]] + (j - d[hd]) / w * v;
while(hd <= tl && a[j] - j / w * v >= a[d[tl]] - d[tl] / w * v) tl--;
d[++tl] = j; // ADD THIS LINE
}
}
memcpy(a, f, sizeof(a));
}
} f[N];
int vis[N], mx[N], sz[N], R;
void findroot(int id, int fa, int tot) {
sz[id] = 1, mx[id] = 0;
for(int it : e[id])
if(!vis[it] && it != fa) {
findroot(it, id, tot);
sz[id] += sz[it], mx[id] = max(mx[id], sz[it]);
}
mx[id] = max(mx[id], tot - sz[id]);
if(mx[id] < mx[R]) R = id;
}
int dn, dfn[N], rev[N];
void dfs(int id, int fa) {
rev[dfn[id] = ++dn] = id, sz[id] = 1;
for(int it : e[id]) if(!vis[it] && it != fa) dfs(it, id), sz[id] += sz[it]; // ADD sz[id] += sz[it]
}
void divide(int id) {
vis[id] = 1, dn = 0, dfs(id, 0);
f[dn + 1].clear(); // e -> f
for(int i = dn; i; i--) {
int id = rev[i];
f[i] = f[i + sz[id]];
Knapsack tmp = f[i + 1];
tmp.insert(d[id], c[id], w[id]); // i -> id
f[i].merge(tmp);
}
for(int i = 0; i <= m; i++) ans = max(ans, f[1].a[i]);
for(int it : e[id]) if(!vis[it]) R = 0, findroot(it, id, sz[it]), divide(R);
}
void solve() {
cin >> n >> m;
memset(vis, 0, sizeof(vis)), ans = 0; // ADD THIS LINE!!!!!
for(int i = 1; i <= n; i++) e[i].clear();
for(int i = 1; i <= n; i++) cin >> w[i];
for(int i = 1; i <= n; i++) cin >> c[i];
for(int i = 1; i <= n; i++) cin >> d[i];
for(int i = 1, u, v; i < n; i++) cin >> u >> v, e[u].push_back(v), e[v].push_back(u);
R = 0, findroot(1, 0, n), divide(R);
cout << ans << endl;
}
int main() {
mx[0] = N;
int T;
cin >> T;
while(T--) solve();
return 0;
}
*II. P3780 [SDOI2017] 苹果树
问题相当于选择从根到某个点的路径,免费选一个苹果,再做树上依赖性背包。这个点肯定是叶子,因为多选免费苹果一定更优。
设
换种角度,想象一棵树,每个儿子按访问顺序从左到右排列,则从根到叶子的路径将整棵树劈成两半,左边和右边时间戳分别连续。对于中间有特殊部分的问题,套路地维护前后缀再合并。又因为树上依赖背包可以算出每个时间戳前缀的答案,所以可行。
因此,设
这样还是不太行,因为
单调队列优化多重背包,时间复杂度
2. 值域定义域互换
*I. AT4927 [AGC033D] Complexity
设
因为每次在矩形中间切一刀使得矩形大小减半,混乱度加
初始化
考虑横着切。枚举左边界
竖着切就太简单了,枚举
时间复杂度
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!