Luogu 3642 [APIO 2016] 烟火表演
- 传送门
- 引例(上一道题)
- 凸函数
- 一开始的思路
- 正解
- 参考代码
- 总结
传送门
引例(上一道题)
凸函数
回忆我们上一道题是怎么做的。我们维护的东西的实质是一个(下)凸函数。由于我们的操作相当于是加上一个凸函数,而凸函数与凸函数的和仍然为凸函数。我们只保存斜率发生变化的点,利用题目给函数带来的特殊性质,只用这些点的横坐标就计算出了答案。
一开始的思路
不难发现,如果最后的时间过长,那么一定会浪费;如果最后时间过短,那么也会有不必要的缩短,感性地理解,好像二分答案可做。不过我们没有办法进行可行性检验。
正解
根据前面的思路,我们可以发现:若设 fi(x)fi(x)f_i(x) 表示以 iii 为根的子树需要 x" role="presentation" style="position: relative;">xxx 秒去引爆时的最小代价,那么 fi(x)fi(x)f_i(x) 是一个(下)凸函数。
我们考虑在子树 uuu 的根结点上加上它父亲连向它的一条边权为 w" role="presentation" style="position: relative;">www 的边对 fu(x)fu(x)f_u(x) 的影响。我们先设 [L,R][L,R][L, R] 表示 fi(x)fi(x)f_i(x) 斜率为 000 的那一段的左右端点。
f_u(x) = \begin{cases} f_{u'}(x) + w & x \le L & \text{让新的边权为 } 0 \\ f_{u'}(L) + (w - (x - L)) & L
其中 fu(x)fu(x)f_u(x) 表示新函数,fu′(x)fu′(x)f_{u'}(x) 表示原函数。显然,最后的答案为 f1(x1)f1(x1)f_1(x_1),其中 xixix_i 表示使得 fi(x)fi(x)f_i(x) 取得最小值的 xxx。
我们先来理解下为什么状态转移方程是这样的。首先需要注意的是,我们设的 x" role="presentation" style="position: relative;">xxx 与上一道题的 xxx 并不一样,所以转移的方法肯定也不一样。
可以知道,x=x′+w′(w′≥0)" role="presentation" style="position: relative;">x=x′+w′(w′≥0)x=x′+w′(w′≥0)x = x' + w' \pod{w' \ge 0},其中 x′x′x' 表示原来引爆的总时间,w′w′w' 表示最后决定把这条边的边权从 www 变成 w′" role="presentation" style="position: relative;">w′w′w'。当 x≥Lx≥Lx \ge L 时,我们要令 w′w′w' 越小越好,因为当 x′x′x' 越小(即 w′w′w' 越大)时,为了让原来引爆的总时间变成 x′x′x' 的代价就越高,且斜率 ≤−1≤−1\le -1,而修改 www 的单位代价仅为 1" role="presentation" style="position: relative;">111,所以这并不划算,因此我们令 w′=0w′=0w' = 0。对于第二部分,我们保证 x′=Lx′=Lx' = L 就好,这样我们可以少减少 www。由于最终 w′=x−L" role="presentation" style="position: relative;">w′=x−Lw′=x−Lw' = x - L(这样才有 x′=Lx′=Lx' = L),因此花费的代价是 w−(x−L)w−(x−L)w - (x - L)。对于第三部分,我们不用改变 www 都保证了 L≤x≤R" role="presentation" style="position: relative;">L≤x≤RL≤x≤RL \le x \le R,因此不需要操作。对于第四部分,由于越往后斜率越大,且至少为 111,而我们修改 w" role="presentation" style="position: relative;">www 的单位代价为 111,同时我们可以无限制地加长 w" role="presentation" style="position: relative;">www,因此我们选择让 x′=Rx′=Rx' = R,所以花费的代价就是 (x−R)−w(x−R)−w(x - R) - w。
我们来仔细分析一下这个过程究竟干了什么。首先很明显,对于 x≤Lx≤Lx \le L 的部分,我们把它向上平移了 www 个单位。对于第二部分,我们从第一部分的结尾开始画了一条斜率为 −1" role="presentation" style="position: relative;">−1−1-1 ,(横坐标)长度为 www 的直线,到第三个部分,我们接着画了一条斜率为 0" role="presentation" style="position: relative;">000,长度为 R−LR−LR - L 的直线,再到第四个部分,我们接着画了一条斜率为 111 的直线,一直延伸到正无穷……
有没有感觉跟上一道题的图形有点像?可以发现,各关键点间形成的直线的斜率是递增的。但是这个递增对我们来说暂时还没有什么用,毕竟到这里我都还完全做不来。我们应该仔细看看这个函数有没有其它性质,因为我们能够维护的只有拐点的横坐标,我们必须通过这些横坐标算出我们想要的信息(就像上一题一样,我们把答案拆开来算,因此就不用管最后的函数值了)。
我们发现:利用上面的函数进行转移时,函数的斜率一定会是 ⋯,−3,−2,−1,0,1" role="presentation" style="position: relative;">⋯,−3,−2,−1,0,1⋯,−3,−2,−1,0,1\cdots, -3, -2, -1, 0, 1,如果某个斜率不存在,那也一定在某个位置有两个重合的点,我们把它视作一个长度为 000 的区间,认为它仍然存在。
证明
当一开始这个函数为空时,进行转移相当于是叶结点接上了父结点,这时,我们得到了斜率为 −1" role="presentation" style="position: relative;">−1−1-1 和 111 的两条直线,我们视它中间存在一条长度为 0" role="presentation" style="position: relative;">000 且斜率为 000 的直线。
当我们对一个符合条件的函数进行转移时,我们相当于是在斜率为 −1" role="presentation" style="position: relative;">−1−1-1 和斜率为 000 的直线的拐点处增加了一条斜率为 −1" role="presentation" style="position: relative;">−1−1-1 的直线,这并不影响函数的这个性质。故这个性质成立。为什么要规定函数有这么一个性质(你有没有觉得我们的强行规定使得这个东西很没说服力?)?因为 f(0)f(0)f(0) 是相当好求的:就是所有导火索的原长度之和。如果我们知道了所有拐点的位置,我们只需要减去一定的值,就能算出 f(L)f(L)f(L) 了。
像上一道题一样,我们得到了只需要拐点坐标就求得所需函数值的方法,岂不美哉?
考虑程序实现。对于叶结点,我们只需要在 www 处插入两个点就好了,它左边代表一条斜率为 −1" role="presentation" style="position: relative;">−1−1-1 的直线,中间代表一条斜率为 000 的直线(长度为 0" role="presentation" style="position: relative;">000),右边代表一条斜率为 111 的直线。对于一棵树,我们将所有儿子对应子树的函数加起来就好了。不难发现,通过保存拐点,函数仍然满足前面我们规定的性质。那么,怎么维护函数取得最小值时的横坐标呢?
这需要结合我们的转移对函数的影响进行考虑。我们的转移只会增加一条斜率为 1" role="presentation" style="position: relative;">111 的直线,并且我们用最右边的拐点对它进行表示。当多个函数加起来时,有多少个函数,斜率的最大值就为多少。我们在维护时保留 RRR 端点,那么我们只需要在合并后删除坐标最大的 k" role="presentation" style="position: relative;">kkk 个拐点就好了,kkk 代表儿子个数。(但是为了方便,为了让叶结点和非叶结点进行统一,我们要保存 R" role="presentation" style="position: relative;">RRR,也就是说只删除 k−1k−1k - 1 个拐点就可以了)
那么怎么考虑非叶结点的转移呢?由于插入了一条斜率为 −1−1-1,长度为 www 的直线,因此斜率为 0" role="presentation" style="position: relative;">000 的那一段相当于是向右平移了 www 个单位。我们删除原来的拐点,重新插入在新的位置即可(因为 0" role="presentation" style="position: relative;">000 左边的直线斜率为 −1−1-1,所以不用再插入新的拐点,相当于是斜率为 −1−1-1 的直线变长了)。
最后,我们只保留 LLL 及其左边的拐点,然后依次减去它们的横坐标,正好就是我们要的函数值。
(灵魂之图)
参考代码
#include <cstdio> #include <cstdlib> #include <cmath> #include <cstring> #include <cassert> #include <cctype> #include <climits> #include <ctime> #include <iostream> #include <algorithm> #include <vector> #include <string> #include <stack> #include <queue> #include <deque> #include <map> #include <set> #include <bitset> #include <list> #include <functional> typedef long long LL; typedef unsigned long long ULL; using std::cin; using std::cout; using std::endl; typedef LL INT_PUT; INT_PUT readIn() {INT_PUT a = 0; bool positive = true;char ch = getchar();while (!(ch == '-' || std::isdigit(ch))) ch = getchar();if (ch == '-') { positive = false; ch = getchar(); }while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }return positive ? -a : a; } void printOut(INT_PUT x) {char buffer[20]; int length = 0;if (x < 0) putchar('-'); else x = -x;do buffer[length++] = -(x % 10) + '0'; while (x /= 10);do putchar(buffer[--length]); while (length);putchar('\n'); }template <typename T, typename C = std::less<T> > class priority_queue {struct Node{T v;int dis;Node* ch[2];Node() : v(), dis(-1), ch() {}Node(const T& v) : v(v), dis(0), ch() {}};Node* root;int s;private:void operator=(const priority_queue&) {} // 禁止拷贝public:priority_queue() : root(), s() {}~priority_queue() { clear(); }private:void clear(Node* &r){if (!r) return;clear(r->ch[0]);clear(r->ch[1]);delete r;r = NULL;} public:void clear() { clear(root); }public:int size() { return s; }bool empty() { return !s; }// significant below private:static Node* merge(Node* a, Node* b){if (!b) return a;if (!a) return b;if (C()(b->v, a->v)) std::swap(a, b);a->ch[1] = merge(a->ch[1], b);if (!a->ch[0] || (a->ch[1] && a->ch[1]->dis > a->ch[0]->dis)) // notestd::swap(a->ch[0], a->ch[1]);if (a->ch[1])a->dis = a->ch[1]->dis + 1;elsea->dis = 0;return a;} public:void merge(priority_queue& b){root = merge(root, b.root);s += b.s;b.root = NULL;b.s = NULL;}public:void push(const T& x){root = merge(root, new Node(x));s++;}const T& top() const{return root->v;}void pop(){Node* del = root;root = merge(root->ch[0], root->ch[1]);delete del;s--;} };// if (x <= L) f[x] += w; // 让新的边权为 0 // if (L < x && x <= L + w) f[x] = f[L] + (w - (x - L)); // 让新的边权为 x - L // if (L + w < x && x <= R + w) f[x] = f[L]; // 让新的边权为 w // if (x > R + w) f[x] = f[L] + ((x - R) - w); // 让新的边权为 x - Rconst int maxn = int(6e5) + 5; int n, m, E; int degree[maxn]; int parent[maxn]; int weight[maxn]; priority_queue<long long, std::greater<long long> > pq[maxn]; LL sum;void run() {n = readIn();m = readIn();for (int i = 2; i <= n + m; i++){degree[parent[i] = readIn()]++;sum += weight[i] = readIn();}for (int i = n + m; i > 1; i--){long long l = 0, r = 0;if (i <= n){for (int j = 1; j < degree[i]; j++)pq[i].pop(); // 最后留下了 L 和 Rr = pq[i].top();pq[i].pop();l = pq[i].top();pq[i].pop();}pq[i].push(l + weight[i]);pq[i].push(r + weight[i]);pq[parent[i]].merge(pq[i]); // 合并到父结点}for (int i = 1; i <= degree[1]; i++)pq[1].pop(); // 最后只剩下了 Lwhile (pq[1].size()){sum -= pq[1].top(); // 依次减去横坐标,正好就是函数值pq[1].pop();}printOut(sum); }int main() {run();return 0; }
总结
一道思维很深邃却又透露出套路的下凸函数的题目。这类题,首先你要发现下凸性质,其次你需要想清楚该怎么转移,更重要的,你需要想清楚在只保存拐点(关键点)的情况下如何计算出答案:是利用转移的性质边算边累计(如第一题)?还是利用函数性质最后来算(如这一题)?取决于你有没有观察出题目的性质了……一般来说,维护拐点通常都选择使用大根堆,把斜率大于 0" role="presentation" style="position: relative;">000 的点给删去了(至少我做的唯一两道题是这样),如果遇到树(比如这道题),可以用可并堆。两个(下)凸函数相加后还是凸函数,且可能还满足某些别的性质(比如这道题)。
Luogu 3642 [APIO 2016] 烟火表演相关推荐
- 【倍增】【线段树】雨林跳跃(luogu 7599[APIO 2021 T2])
正题 luogu 7599[APIO 2021 T2] 题目大意 给你一排树中每棵树的高度,每次跳跃可以跳到左/右边第一棵比该树高的树,问你从A-B中某棵树跳到C-D中的某棵树的最小步数(A⩽B< ...
- 【堆】【DP】Niyaz and Small Degrees(luogu 7600[APIO 2021 T3]/luogu-CF1119F)
正题 luogu 7600[APIO 2021 T3] luogu-CF1119F 题目大意 给你一棵树,给出每条边割掉的代价,问你对于0⩽k<n0\leqslant k<n0⩽k< ...
- [Luogu P3642] [BZOJ 4585] [APIO2016]烟火表演
洛谷传送门 BZOJ传送门 题目描述 烟花表演是最引人注目的节日活动之一.在表演中,所有的烟花必须同时爆炸.为了确保安全,烟花被安置在远离开关的位置上,通过一些导火索与开关相连.导火索的连接方式形成一 ...
- (APIO)烟火表演
- - 不要问我发生了什么- 要问就去这篇博客下面留言,拷问这个博主的良心 于是!我今天来做这道题了- (我也是够会作的-) 题目描述 众所周知,是最引人注目的节日活动之一.在表演中,所有的烟花必须同 ...
- luogu P3642 [APIO2016]烟火表演
https://www.luogu.com.cn/problem/P3642 好毒瘤啊!!! 首先按照套路 设f(x)表示以u为根的,距离为x的最小代价设f(x)表示以u为根的,距离为x的最小代价设f ...
- [APIO2016]烟火表演
链接:https://www.luogu.org/problemnew/show/P3642 跟上一道题类似但更难,首先也是观察出在某个节点代价是下凸的函数,并且得到转移方程: 1.x<=L f ...
- BZOJ4585: [Apio2016]烟火表演
Description 烟花表演是最引人注目的节日活动之一.在表演中,所有的烟花必须同时爆炸.为了确保安 全,烟花被安置在远离开关的位置上,通过一些导火索与开关相连.导火索的连接方式形成 一棵树,烟花 ...
- P3642 [APIO2016]烟火表演(左偏树、函数)
解析 感觉是左偏树的神题了. 首先有一个比较显然的结论,一个合法的方案中,两个叶子到它们 lca\text{lca}lca 的距离必须相等. 考虑设计 dp\text{dp}dp : fi,xf_{i ...
- 【APIO2016】烟火表演(可并堆)(折线DP)
传送门 题解: 设fi(x)f_i(x)fi(x)表示在iii的子树中,所有叶子到iii距离为xxx的时候,子树内部修改的最小代价. 显然是个分段一次函数,大力讨论记录下端点就行了. 注意到可能会出 ...
最新文章
- 河南省住建厅调研新郑智慧城市建设 市民享受服务便利
- 用javascript伪造太阳系模型系统
- linux下卸载 dev sd*下硬盘,Linux下硬盘操作解析
- Go 语言web 框架 Gin 练习6
- UE4异步编程专题 - 多线程
- Javascript学习笔记8——用JSON做原型
- 【CAM应用】谈CAM软件在实际生产中的应用举例
- 如何查看电脑上是否安装有IIS服务
- SQL AZURE数据导入导出,云计算体验之四
- 23亿美元大市场,NFV做好了准备吗?
- 海康威视工业相机SDK二次开发(VS+Opencv+QT+海康SDK+C++)(二)
- 强化学习从K-摇臂老虎机开始
- 计算某个日期到今天的天数
- Win64 驱动签名
- 基于Problem Solving with Algorithms and Data Structures using Python的学习记录(4)——Recursion
- 推荐一位从外包走进腾讯的朋友
- 计算机网络基础第5版教案,计算机网络基础 第5章教案
- 80老翁谈人生(173):老翁力挺转基因,问责“反转派”
- 零基础语法入门第十二/十三讲指示代词和不定代词以及形容词
- Linux内核怎么学?看这一份书单足够!
热门文章
- 常见文档注释工具简介
- ubuntu安装android应用程序,Anbox将使Ubuntu手机能运行Android应用程序
- Scrapy 2.6 Downloader Middleware 下载器中间件使用指南
- python 正则过滤四字节字符 表情字符
- 年终总结——过去已逝,未来可期不可欺
- 用python写情书_用Python给喜欢人的发一封邮件吧(群发)
- vmlinuz文件解压缩
- iOS 基于 AVFoundation 制作的用于剪辑视频项目
- MySQL数据库-表的插入详解
- 管理系统类毕设(二)---学生管理系统说明