收录一些比较冷门的 DP 优化方法。

1. 树上依赖性背包

树上依赖性背包形如在树上选出若干个物品做背包问题,满足这些物品连通。由于 01 背包,多重背包和完全背包均可以在 \(\mathcal{O}(V)\) 的时间内加入一个物品,\(\mathcal{O}(V ^ 2)\) 的时间内合并两个背包,所以不妨设背包类型为多重背包。

先考虑一个弱化版问题。给定一棵有根树,若一个节点被选,则它的父亲必须被选。

显然存在一个 \(\mathcal{O}(nV ^ 2)\) 的树形 DP 做法,它能求出以每个节点为根时其子树的答案。

接下来引出科技:树上依赖性背包。我们发现对每个节点都求答案似乎有些累赘,因为我们只关心以 \(1\) 为根时的答案。对做法的形象描述为:让背包从根节点的地方出发,对于每个节点 \(i\),如果不选,那么跳过 \(i\) 的整棵子树,否则强制选该节点上的物品至少一件,并将这个背包带到子树里逛一圈(因为父亲节点选了)。注意到两种选择实际上是并列的,所以合并背包是合并它们的 点值,即对应位置取 \(\max\)。

让我们用更严谨的语言描述上述过程。不妨设节点已经按照它们的 dfs 序排好序了,节点 \(i\) 的子树大小为 \(sz\)。

设 \(f_i\) 表示前 \(i - 1\) 个节点在限制下的答案(是一个背包),对于当前节点 \(i\) 而言,我们已知 \(f_i\),需要用这个信息转移到它更后面的位置。

  • 如果节点 \(i\) 被选择,那么只需它的儿子子树满足限制。换言之,选择节点 \(i\) 之后,它们的儿子可以选择选或者不选,这个选择的自由留给子节点决策,所以只有节点 \(i\) 是否被选择的信息固定了下来的。因此 \(f_i + K_i \to f_{i + 1}\)。这里 \(+K_i\) 表示将物品 \(K_i\) 加入背包 \(f_i\)。
  • 如果节点 \(i\) 不被选择,那么它的整棵子树也不能选。所以它的整棵子树的状态就确定了下来:均不选。因此 \(f_i \to f_{i + sz_i}\)。

注意这里 \(\to\) 符号表示将箭头前的背包按点值合并到箭头指向的背包,复杂度是 \(\mathcal{O}(V)\) 而非 \(\mathcal{O}(V ^ 2)\)。

不难发现我们在 \(\mathcal{O}(nV)\) 的时间内解决了简化后的问题。对于原问题而言,注意到我们选择作为根的节点时必然被选择的,所以任何一个包含根节点的方案均在本次 DP 中被考虑到。根节点裂开后整棵树形成若干连通块,这让我们联想到点分治。因此,用点分治优化上述 DP,这使得我们不用以每个节点作为根 DP 整棵树。时间复杂度 \(\mathcal{O}(n\log n V)\)。

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] 苹果树

问题相当于选择从根到某个点的路径,免费选一个苹果,再做树上依赖性背包。这个点肯定是叶子,因为多选免费苹果一定更优。

设 \(f\) 表示当前可以继续往下延伸免费苹果的背包数组,\(g\) 表示不可以再向下延伸免费苹果的背包数组,则对于 \(u\) 及其子节点 \(v\),\(f_u \otimes f_v\to g_u\),\(f_u\otimes g_v\to g_u\),\(g_u\otimes g_v \to g_u\)。很遗憾,如果用树上依赖性背包,我们会发现上面三种转移无法合并,必须向下递归三个子问题。也就是说,每层将凭空多出来一个背包数组。这个方法行不通。

换种角度,想象一棵树,每个儿子按访问顺序从左到右排列,则从根到叶子的路径将整棵树劈成两半,左边和右边时间戳分别连续。对于中间有特殊部分的问题,套路地维护前后缀再合并。又因为树上依赖背包可以算出每个时间戳前缀的答案,所以可行。

因此,设 \(f_i\) 表示考虑到时间戳前缀 \(i\) 的答案,满足时间戳为 \(i\) 的节点 \(rev_i\) 到根的路径上所有节点还没有被加入背包。\(g_i\) 同理表示后缀。求出 \(f, g\) 后枚举每个节点 \(i\),则相当于合并 \(f_{dfn_i}\),\(g_{dfn_i}\) 和 \(i\) 到根上所有节点 \(j\) 在 \(a_j\) 减掉 \(1\) 之后的背包 \(h_i\),得到一个大背包 \(K\),则 \(K_k\) 加上 \(i\) 到根上所有节点的 \(v\) 之和的最大值即为答案。

这样还是不太行,因为 \(K_k\) 需要 \(k ^ 2\) 的时间。考虑将 \(h\) 巧妙地融合到 \(f\) 或 \(g\) 当中,发现设 \(f_i\) 满足 \(rev_i\) 到根的路径上所有节点 \(j\) 暂时只考虑了 \(a_j - 1\) 个苹果,且这 \(a_j - 1\) 个苹果不强制至少选一个,即可满足条件。也就是说,进入 \(j\) 时只不强制必须选地加入 \(a_j - 1\) 个苹果,回溯时再强制加入最后一个苹果。

单调队列优化多重背包,时间复杂度 \(\mathcal{O}(nk)\),代码

2. 值域定义域互换

*I. AT4927 [AGC033D] Complexity

设 \(f_{i, j, k, l}\) 表示以 \((i, j)\) 为左上角,\((k, l)\) 为右下角的矩形的混乱度,直接做时空复杂度至少 \(n ^ 4\),无法接受。

因为每次在矩形中间切一刀使得矩形大小减半,混乱度加 \(1\),所以答案为 \(\log\) 级别。进一步地,固定左边界 \(j\),上边界 \(i\) 和下边界 \(k\),当 \(l\) 向右移动时,混乱度不降。显然,若矩形 \(A\) 包含矩形 \(B\),则 \(A\) 的混乱度不小于 \(B\) 的混乱度。根据这个单调性,设 \(f_{i, j, k, a}\) 表示使得混乱度不大于 \(a\) 的最大的 \(l\)。\(a\) 这一维只有 \(\log\),且可以滚动数组优化掉。

初始化 \(f_{i, j, k} = l\) 当且仅当对应矩形字符全部相等,且 \(l + 1\) 对应矩形字符不全相等。枚举 \(i, j\),随着 \(k\) 递增 \(l\) 不降,可以 \(n ^ 3\) 预处理。

考虑横着切。枚举左边界 \(j\),上边界 \(i\),下边界 \(k\)。若再枚举切割位置 \(p\),则复杂度 \(n ^ 4\)。但我们注意到转移形如 \(f_{i, j, k} = \max\limits_{p = i} ^ {k - 1} \min(f_{i, j, p}, f_{p + 1, j, k})\),因为 \(f_{i, j, p}\) 在固定 \(i, j\) 时关于 \(p\) 单调,\(f_{p + 1, j, k}\) 在固定 \(j, k\) 时关于 \(p\) 单调,在固定 \(p, j\) 时关于 \(k\) 单调,所以当 \(k\) 递增时,决策点 \(p\) 单调不降。反证法结合单调性容易证明。因此不需要二分决策点,用指针维护即可。

竖着切就太简单了,枚举 \(i, j, k\),则 \(f_{i, f_{i, j, k} + 1, k}\) 贡献到新的 \(f_{i, j, k}\)。

时间复杂度 \(\mathcal{O}(n ^ 3\log n)\),比题解区 \(n ^ 3\log ^ 2 n\) 的做法时间复杂度更优,\(n ^ 3\log n\) 但需要两个 DP 数组的做法更简洁。代码 和题解略有不同。

DP 优化小技巧的更多相关文章

  1. 嵌入式C语言优化小技巧

    嵌入式C语言优化小技巧 1 概述 嵌入式系统是指完成一种或几种特定功能的计算机系统,具有自动化程度高,响应速度快等优点,目前已广泛应用于消费电子,工业控制等领域.嵌入式系统受其使用的硬件以及运行环境的 ...

  2. .NET性能优化小技巧

    .NET 性能优化小技巧 Intro 之前做了短信发送速度的提升,在大师的指导下,发送短信的速度有了极大的提升,学到了一些提升 .NET 性能的一些小技巧 HttpClient 优化 关于使用 Htt ...

  3. IT咨询顾问:一次吐血的项目救火 java或判断优化小技巧 asp.net core Session的测试使用心得 【.NET架构】BIM软件架构02:Web管控平台后台架构 NetCore入门篇:(十一)NetCore项目读取配置文件appsettings.json 使用LINQ生成Where的SQL语句 js_jquery_创建cookie有效期问题_时区问题

    IT咨询顾问:一次吐血的项目救火   年后的一个合作公司上线了一个子业务系统,对接公司内部的单点系统.我收到该公司的技术咨询:项目启动后没有规律的突然无法登录了,重新启动后,登录一断时间后又无法重新登 ...

  4. OI常用的常数优化小技巧

    注意:本文所介绍的优化并不是算法上的优化,那个就非常复杂了,不同题目有不同的优化.笔者要说的只是一些实用的常数优化小技巧,很简单,虽然效果可能不那么明显,但在对时间复杂度要求十分苛刻的时候,这些小的优 ...

  5. D3D9 优化小技巧

    此篇文章主要讲一些小技巧,针对前面转载的D3D9 GPU Hacks,我们可以做的一些优化. 在做延迟渲染或者其它需要深度的地方使用INTZ格式的纹理,这样可以直接对纹理进行操作,节省了显存和带宽,这 ...

  6. mysql优化小技巧

    对mysql优化时一个综合性的技术,主要包括 a: 表的设计合理化(符合3NF) b: 添加适当索引(index) [四种: 普通索引.主键索引.唯一索引unique.全文索引] c: 分表技术(水平 ...

  7. How Javascript works (Javascript工作原理) (十一) 渲染引擎及性能优化小技巧

    个人总结:读完这篇文章需要20分钟,这篇文章主要讲解了浏览器中引擎的渲染机制. DOMtree       ----|   |---->  RenderTree CSSOMtree  ----| ...

  8. JavaScript 工作原理之十一-渲染引擎及性能优化小技巧

    原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland. 本系列持续更新中,Github 地址请查阅这里. 这是 JavaScript 工作原理的第十一章. 迄 ...

  9. Spring Cloud OpenFeign 的 5 个优化小技巧!

    OpenFeign 是 Spring 官方推出的一种声明式服务调用和负载均衡组件.它的出现就是为了替代已经进入停更维护状态的 Feign(Netflix Feign),同时它也是 Spring 官方的 ...

随机推荐

  1. kube-scheduler的调度上下文

    前一章节了解到了kube-scheduler中的概念,该章节则对调度上下文的源码进行分析 Scheduler Scheduler 是整个 kube-scheduler 的一个 structure,提供 ...

  2. 20220723-Mac上使用IntelliJ IDEA

    目录 IDEA快捷键 IDEA模板 常用模板快捷键 个人随笔 软件:IntelliJ IDEA 电脑:Mac IDEA快捷键 打开/关闭 项目视图 快捷键:⌘ + 1 运行项目 快捷键:⌃ + ⇧ + ...

  3. Linux学习系列--用户(组)新增、查看和删除

    在实际的工作中,在接触Linux的用户组管理的时候,一般来说都是在系统开建设的时候设置好,root权限由特定的负责人保管用户密码,避免误操作带来不必要的麻烦. 在具体使用的时候,会利用相关的命令设置一 ...

  4. 丽泽普及2022交流赛day19 半社论

    目录 No Problem Str Not TSP 题面 题解 代码 Game 题面 题解 代码 No Problem 暴力 Str 存在循环节,大力找出来即可,长度显然不超过 \(10^3\) . ...

  5. 2500-使用MyBatis操作MySQL进行批量更新的注意事项

    原则上一条SQL只更新一条数据库操作,但有时需要批量操作数据,特别是一些DML语句,在操作数据库时,数据库会报出异常,不允许混合语句,此时需要额外配置进行兼容. 例如: Caused by: com. ...

  6. devops-1:代码仓库git的使用

    devops-gitlab 介绍 gitlab同github.gitee和bitbucket功能一致,都是提供一个存储代码的服务,这里就以gitlab为例,学习一下如何结合git工具使用. 核心组件: ...

  7. 封装Fraction-分数类(C++)

    Fraction 分数类 默认假分数,可自行修改 由于concept的原因 template <typename T> concept is_float_v = std::is_float ...

  8. Ansible yaml 剧本(傻瓜式)

    优化ansible安装MySQL: Ansible部署MySQL编译安装 - xiao智 - 博客园 (cnblogs.com) Ansible yaml 剧本(傻瓜式): --- - hosts: ...

  9. MQ系列3:RocketMQ 架构分析

    MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 1 背景 我们前面两篇对主流消息队列的基本构成和技术选型做了详细的分析.从本篇开始,我们会专注当下主流MQ之一的RocketMQ. 从 ...

  10. shellcode 注入执行技术学习

    shellcode 注入执行技术学习 注入执行方式 CreateThread CreateRemoteThread QueueUserAPC CreateThread是一种用于执行Shellcode的 ...