收录一些比较冷门的 DP 优化方法。
树上依赖性背包形如在树上选出若干个物品做背包问题,满足这些物品连通。由于 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\),需要用这个信息转移到它更后面的位置。
注意这里 \(\to\) 符号表示将箭头前的背包按点值合并到箭头指向的背包,复杂度是 \(\mathcal{O}(V)\) 而非 \(\mathcal{O}(V ^ 2)\)。
不难发现我们在 \(\mathcal{O}(nV)\) 的时间内解决了简化后的问题。对于原问题而言,注意到我们选择作为根的节点时必然被选择的,所以任何一个包含根节点的方案均在本次 DP 中被考虑到。根节点裂开后整棵树形成若干连通块,这让我们联想到点分治。因此,用点分治优化上述 DP,这使得我们不用以每个节点作为根 DP 整棵树。时间复杂度 \(\mathcal{O}(n\log n V)\)。
给出代码。
#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;
}
问题相当于选择从根到某个点的路径,免费选一个苹果,再做树上依赖性背包。这个点肯定是叶子,因为多选免费苹果一定更优。
设 \(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)\),代码。
设 \(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 数组的做法更简洁。代码 和题解略有不同。
动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、
写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c
我希望Ruby的解析器会进行这种微不足道的优化,但似乎并没有(谈到YARV实现,Ruby1.9.x、2.0.0):require'benchmark'deffib1a,b=0,1whileb由于这两种方法除了在第二种方法中使用预定义常量而不是常量表达式外是相同的,因此Ruby解释器似乎在每个循环中一次又一次地计算幂常数。是否有一些Material说明为什么Ruby根本不进行这种基本优化或只在某些特定情况下进行? 最佳答案 很抱歉给出了另一个答案,但我不想删除或编辑我之前的答案,因为它下面有有趣的讨论。正如JörgWMittag所说,
我正在尝试从数据库中读取大量单元格(超过100.000个)并将它们写入VPSUbuntu服务器上的csv文件。碰巧服务器没有足够的内存。我正在考虑一次读取5000行并将它们写入文件,然后再读取5000行,等等。我应该如何重构我当前的代码以使内存不会被完全消耗?这是我的代码:defwrite_rows(emails)File.open(file_path,"w+")do|f|f该函数由sidekiqworker调用:write_rows(user.emails)感谢您的帮助! 最佳答案 这里的问题是,当您调用emails.each时,
文章目录前言约束硬约束的轨迹优化Corridor-BasedTrajectoryOptimizationBezierCurveOptimizationOtherOptions软约束的轨迹优化Distance-BasedTrajectoryOptimization优化方法前言可以看看我的这几篇Blog1,Blog2,Blog3。上次基于MinimumSnap的轨迹生成,有许多优点,比如:轨迹让机器人可以在某个时间点抵达某个航点。任何一个时刻,都能数学上求出期望的机器人的位置、速度、加速度、导数。MinimumSnap可以把问题转换为凸优化问题。缺点:MnimumSnap可以控制轨迹一定经过中间的
我对为我的RubyonRails3.1.3应用优化我的Unicorn设置的方法很感兴趣。我目前正在高CPU超大实例上生成14个工作进程,因为我的应用程序在负载测试期间似乎受CPU限制。在模拟负载测试中,每秒大约20个请求重放请求,我的实例上的所有8个内核都达到峰值,盒子负载飙升至7-8个。每个unicorn实例使用大约56-60%的CPU。我很好奇可以通过哪些方式对其进行优化?我希望能够每秒将更多请求汇集到这种大小的实例上。内存和所有其他I/O一样完全正常。在我的测试过程中,CPU越来越低。 最佳答案 如果您受CPU限制,您希望使用
美团外卖搜索工程团队在Elasticsearch的优化实践中,基于Location-BasedService(LBS)业务场景对Elasticsearch的查询性能进行优化。该优化基于Run-LengthEncoding(RLE)设计了一款高效的倒排索引结构,使检索耗时(TP99)降低了84%。本文从问题分析、技术选型、优化方案等方面进行阐述,并给出最终灰度验证的结论。1.前言最近十年,Elasticsearch已经成为了最受欢迎的开源检索引擎,其作为离线数仓、近线检索、B端检索的经典基建,已沉淀了大量的实践案例及优化总结。然而在高并发、高可用、大数据量的C端场景,目前可参考的资料并不多。因此
RTS在阿里云视频直播的基础上进行底层技术优化,通过集成阿里云播放器SDK,支持在千万级并发场景下节点间毫秒级延时直播的能力,弥补了传统直播存在3~6秒延时的问题,确保了超低延时、低卡顿、秒开流畅的直播观看体验。本文介绍了基于RTS超低延迟直播优化强互动场景体验的最佳实践方案,并以阿里云播放器Aliplayer为例,详细介绍RTS超低延迟拉流接入、自动降级、排障信息获取等逻辑的实现,助力企业打造互动直播行业的产品竞争力。适用场景该方案适用于对超低延迟直播有诉求的客户,尤其是业务中存在强互动场景直播的场景。强互动场景直播主要是指对主播和观众存在互动,或观众存在更高实时性观看、画面互动需求的情况,
我目前正在研究Ruby2.1.1的改进,但遇到了一些奇怪的事情。我正在尝试改进String类并定义一个名为FOO的常量。沙箱.rbmoduleFoobarrefineStringdoFOO="BAR"deffoobar"foobar"endendendusingFoobarputs"".class::FOO#=>uninitializedconstantString::FOO(NameError)puts"".foobar#=>"foobar"这给了我未初始化的常量String::FOO(NameError)。但是我可以调用"".foobar这让我相信我在正确的范围内。奇怪的是,如果我
我正在使用Rails4.0.0和Ruby2.0.0。我的Post(如在博客文章中)模型与用户相关联,该用户具有用户的user_name、first_name、last_name的组合。我想迁移数据,以便通过外键(即用户ID)将帖子关联到用户。我在posts表中有大约1100万条记录。我在Linux服务器上使用rake任务运行以下代码来迁移数据。然而,我的任务一直被服务器“杀死”,大概是由于rake任务,特别是下面的代码,消耗了太多内存。我发现将batch_size降低到20并将sleep(10)增加到sleep(60)允许任务运行更长的时间,在不被杀死的情况下总共更新更多的记录,但需要