贪心算法
前言
贪心?本质上要先找到影响问题的根本因素,再安排最优化策略。
热身题
——找找本质
题意
你有\(n\)头奶牛,第\(i\)头奶牛的产奶量是\(c_i\)升。有\(m\)个小卖部,第\(i\)个小卖部需要牛奶\(q_i\)升,每升的采购价格是\(p_i\)元。有\(r\)个农场,每个农场至多购买1头奶牛,收购价为\(r_i\)元。
对于每一头奶牛,你有\(2\)种选择:1、卖给某个农场。2、把该头奶牛产的牛奶卖给小卖部;不一定要1次卖完。
请最大化总收益。\(1 \leq n,m,r \leq 10^5\)。
题解
收益只有\(2\)种:来自农场,来自小卖部。
若1头牛卖给农场,收益与该奶牛的产奶量无关,只与该农场的收购价有关。
若1头牛不卖给农场,收益与它卖出的奶量、小卖部的收购单价有关;更准确的说,不同奶牛的奶本质相同,可以将所有奶牛的奶集中起来再卖给特定的小卖部。
既然任一卖入农场的奶牛的收益只与农场收购价有关,那么我们应该按照收购价从大到小的顺序安排奶牛对应的农场;进一步地,卖入农场的总收益仅与卖入农场的奶牛数目相关。除去卖入农场的奶牛,我们可将剩余的奶牛的奶集中处理,按照采购单价从大到小的顺序卖给小卖部;这部分的收益仅与剩余奶牛的总奶量有关。
因此,不妨枚举卖入农场的奶牛数目\(x\),为了令剩余奶牛的总奶量最大化,这\(x\)头奶牛按照产奶量从小到大的顺序进行选择;剩余的奶集中起来,按照单价从大到小的顺序卖入对应的小卖部。
该过程通过排序、前缀和+二分实现,时间复杂度\(O(nlogn)\)。代码见此。
范围自小而大
——机不可失,时不再来?
例题1:POJ 3614
题意
给定\(n\)个区间\([l_i,r_i]\),\(m\)个元素\(x_i\)。
你可以将元素\(x_i\)和区间\([l_j,r_j]\)进行匹配,当且仅当\(x_i \in[l_j,r_j]\)。
求解最大匹配数。\(1 \leq n,m \leq 10^5\)。
题解
首先这是个点与区间匹配的问题,理区间选点和点选区间是等价的。
考虑1种区间选点的思路:从选择范围较小的区间开始选点。
原限制\(x_i \in[l_j,r_j]\)可以拆分成\(2\)个限制:\(x_i \geq l_j 且 x_i \leq r_j\)。
因此不妨将区间按照左端点大小进行排序,处理掉其中1个限制。
对于左端点不同的区间,我们从左端点更大的区间(即\(x_i \leq l_j\)的点数更少)的区间开始取点;对于左端点相同的区间,我们从右端点更小(即\(x_i \leq r_j\)的点数更少)的区间开始取点。每次取走选择范围内点权最大者。
正确性证明:按照左端点从大到小的顺序考虑区间,先考虑的区间取走的点与后考虑的区间有\(2\)种关系:在其中、不在其中。若不在其中正确性显然;若在其中,最坏的情况是后考虑的区间无法取点,那么交换1下,将该点给予后考虑的区间,结果也不会变优。证毕。
时间复杂度:\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/poj 3614 nlogn.cpp)。
归纳结论
某些问题中,从可选集合小的地方开始选取,并且对后续影响最小化、局部策略最优化,即可得到全局最优解。
进阶练习1:HDU 4864
题意
给定\(n\)个二元组\((x_i,y_i)\)和另外\(m\)个二元组\((a_i,b_i)\)。
你可将\((a_j,b_j)\)与\((x_i,y_i)\)匹配,需要满足\(a_j \leq x_i\)且\(b_j \leq y_i\)。每个\((a_i,b_i)\)和\((x_i,y_i)\)都至多可以进行1次匹配。
求解最大匹配数,以及最大匹配数下最大总价值。
总价值的计算公式为,对于被分配的\((a_i,b_i)\),对\(500a_i + 2b_i\)求和。
\(1 \leq n,m\leq10^5,0 < x_i,a_i < 1440 , 0 \leq y_i,b_i \leq100\)
题解
若仅考虑最大匹配数,则将\((a_j,b_j)\)以\(a_j\)为第一关键字,以\(b_j\)为第二关键字从大到小排序。随着\(a_j\)的减小,依次载入\(x_i \geq a_j\)的\((x_i,y_i)\)。每个\((a_j,b_j)\)选取可匹配的\((x_i,y_i)\)中\(y_i\)最小者。
现在还要考虑总价值。我们以\(a_i\)为第一关键字排序选取的策略可以保证\(500a_i\)的总和最大。现在考虑\(2b_i\)的存在是否需要我们改变选取策略。注意到\(a_i\)和\(b_i\)的范围和系数并不相同:\(a_i\)的改变至少使结果变化\(500\),而\(b_i\)的改变至多使结果变化\(100\)。由于\(a_i\)权重的压倒性优势,我们依旧应以\(a_i\)为第一关键字排序选取;只不过在\(a_i\)相同时优先选取\(b_i\)较大者,与上文策略再次吻合;求解最大匹配数时顺便计算即可。时间复杂度\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/hdu 4864.cpp)。
进阶练习2:UVa 1316
题意
给定\(n\)个物品,每个物品有截止时间\(t_i\)和价值\(p_i\)。第\(i\)个物品只能在\(t_i\)及以前卖出,每1天至多能卖出1件物品。
最大化卖出物品的价值总和。\(1 \leq n \leq 10^4 , 1 \leq t_i,p_i \leq 10^4\)。
题解
对于每个物品,\(t_i\)的大小决定了它可卖出的时间的集合,\(t_i\)越小可选集合越小。不妨按照\(t_i\)从小到大的顺序考虑各个物品,查询\(t \leq t_i\)中最早的空天并安排;但本题的收益与每1天卖出的物品价值有关(而不是简单的匹配数),因此之前已经安排的物品应在适当的时候被替换成价值更大的物品(“后悔”思想)。具体地,对于每个物品,如果存在空天则直接安排,若不能则求解已安排商品中价值最小者并尝试替换。时间复杂度\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/UVa 1316 1.cpp)。
另1个种思路是,对每一天进行考虑。对于第\(i\)天,只有\(t_j \geq i\)的物品可以被卖出;因此我们按天数从大到小(可选物品集合从小到大)的顺序进行考虑,每次选择集合中价值最大者。时间复杂度\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/UVa 1316 2.cpp)。
最大化利用
——用了不会差,不用好不了
例题1:POJ 3190
题意
给定\(n\)个区间\([l_i,r_i]\)。问最少使用多少个大区间,可以完全覆盖这\(n\)个区间。
覆盖的限制是,同1大区间中的任意两个区间不能有交集。\(1 \leq n \leq 2 \times 10^5\)。
题解
基于“最大化利用”的思想,考虑让每1个大区间被尽可能地充满。
具体地,先将各个小区间按照左端点从小到大排序;依次考虑各个小区间,若目前存在结束点小于\(l_i\)的大区间,则塞入其中任意1个;否则新开1个。
正确性证明:
(关于将小区间塞入已有大区间)考虑安排该小区间后的影响,它只会改变1个大区间的结束点,至多妨碍1个后续小区间的选择,到时再新开1个大区间,结果不会变差。
(关于将小区间塞入任意合法大区间都等价)由于我们按照左端点从小到大的顺序考虑各个小区间,当前小区间可选的大区间,后面的小区间一定也可选,因此不管塞入哪1个大区间,对后续的小区间的影响都是使得1个可选区间结束点变为\(r_i\),其余依旧可选。
本题而言,我们需要查询任意1个\(r_j <l_i\)的大区间。高效的做法是将已有区间按照\(r_j\)大小有序存储;最直观地,建1棵平衡树将已有区间存起来,每次查询是否存在\(r_j <l_i\)的区间;进一步简化,取出\(r_j\)最小的区间即可判断是否有合法解,使用小根堆存储即可。时间复杂度\(O(nlogn)\),[代码见此](https://github.com/littlewyy/OI/blob/master/poj 3190.cpp)。
进阶练习1:[penalty]
题意
给定1棵点数为\(n\)的树,询问删除恰好\(k\)条边后,最小化最大联通块大小。
\(1 \leq n \leq 10^5 , n > k\)
题解
二分答案\(mid\),求解每个联通块大小都不超过\(mid\)的前提下的最少删边数目。
递归求解。对于当前子树的根\(x\)和它的各个儿子\(y\),考虑\((x,y)\)是否删去。设\(y\)所在的联通块大小为\(Siz[y]\),对于保留的\((x,y)\),\(Siz[x] += Siz[y]\);对于删去的\((x,y)\),进行计数。
同样的删边数目,删除\(Siz\)更大的会更优;故将\(Siz\)从大到小排序,确定删边数目即可确定删边集合,也可以确定\(x\)所在的联通块大小。根据“最大化利用”的思想,考虑1种策略:仅在剩余联通块大小大于\(mid\) 时才进行删边操作。正确性证明:最坏情况,\(x\)上传后立即删去\((Fa[x],x)\),答案也不会比现在多删1边差。
时间复杂度:\(O(nlogn)\)。代码见此。
进阶练习2:luogu 5021
题意
给定1棵点数为\(n\),带边权的树。要求你选出\(m\)条边不相交的路径,最大化最短路径长度。
\(1 \leq n,m\leq 5\times 10^4\)。
题解
二分答案\(mid\),求解每条路径长度均大于等于\(mid\)的最大路径数目。
这里有1个容易陷入的误区:不是每条边都一定要用上!差边多了反而可能导致无法构成合法路径。
递归求解。对于当前子树的根\(x\)和它的各个儿子\(y\),设\(len[y]\)表示与\(y\)相连(包括\(w(x,y)\))的路径长度。考虑"最大化利用"的策略:在当前子树内产生尽可能多的合法路径;在当前子树内不能产生多1条合法路径的情况下,最大化上传的长度。正确性基于1段路径至多助推1条合法路径的组成。
具体操作中,首先将所有\(len[y]\)从小到大排序。求解当前子树内产生的最大合法路径数:不上传路径,从大到小扫描\(len[y]\),对合法的\(len[y]\)直接计数,对非法的\(len[y]\)匹配尽可能小的\(len[z]\)并计数,得到\(ans\)。再二分不上传的路径的\(id\),求解\(len[id]\)不能使用时的最大合法路径数,若等于\(ans\)则\(lo = id\)。
时间复杂度:\(O(nlog^2n)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/luogu 5021.cpp)。
微扰
——元素互相干扰难以排列?考虑微扰,定其他,变一两个,进而推广结论。
例题1:SMOJ 2985
题意
给定\(2\)行各\(n\)个物品,第\(1\)行的物品有权值\(p_i\)和\(d_i\),第2行的物品有权值\(w_i\)。给定另1个常数\(t\)。
你应将第1行的物品与第2行的物品一对一分配。
设第2行的物品\(i\)分配到第1行的物品\(j\),最大化\(\sum w_i \times p_j - w_i \times d_j \times t\)。
\(1 \leq n \leq 10^5 , 1\leq p_i,d_i,w_i \leq 10^6\)。
题解
不妨先对上述式子化简,将\(i\)与\(j\)拆分开来,最大化\(\sum w_i \times (p_j - d_j \times t)\)。方便起见,设\(val_j = p_j - d_j \times t\)。
任意的分配方案都等价于:不改变第1行物品的相对顺序,仅交换第2行的物品,\(\sum w_i \times val_i\)即为所求。
考虑任意2个第2行的物品是否要进行交换,它们的交换不影响其它物品的收益。
设\(a < b\),交换前的总收益为\(w_a \times val_a + w_b \times val_b\);交换后的总收益为\(w_b \times val_a + w_a \times val_b\)。
若不交换,则应满足$w_a \times val_a + w_b \times val_b >w_b \times val_a + w_a \times val_b $。
化简,可得\((w_a - w_b)(val_a - val_b) > 0\)。因此,只需将\(w_i\)、\(val_i\)分别从大到小排序,一对一地匹配。
时间复杂度\(O(nlogn)\),代码见此。
例题2:SMOJ 2992
题意
\(n\)个学生要进行投票,第\(i\)个人有\(p_i\)的概率投支持票,相应地有\(1 - p_i\)的概率投反对票。
现在要从中选择\(k\)个人,使得这\(k\)个人平票(即支持和反对的人数相同)的概率最大。
输出最大的平票概率。
对于\(70\)%的数据,\(1 \leq n \leq 100\),\(1 \leq k \leq n\)
对于\(100\)%的数据,\(1 \leq n \leq 2000 , 1 \leq k \leq n\)
题解
若把这\(k\)个人选了出来,平票的概率可以通过\(O(n^2)\)的\(dp\)计算。
问题的关键在于如何选择这\(k\)个人,使得\(f(k)(\frac {k}{2})\)最大。
设\(f(i)(j)\)为前\(i\)个人,有\(j\)个人投支持票的概率。考虑确定了\(i - 1\)个人,第\(i\)个人的人选\(x\)对\(f(i)(j)\)的影响。
对递推式提出变量与不变量,\(f(i)(j) = f(i - 1)(j) +(f(i - 1)(j - 1) - f(i - 1)(j)) \times p_x\)。
可以发现\(f(i)(j)\)是关于\(p_x\)的一次函数,当\(f(i - 1)(j - 1) - f(i - 1)(j) > 0\) 时,\(f(i)(j)\)随着\(p_x\)的增大而增大,反之\(f(i)(j)\)随着\(p_x\)的增大而减小。由此可知,最优解\(p_x\)在可选集合中要么选最小者,要么选最大者。
进而得到推论:将所有人按照\(p_i\)排序后,最优人选一定是一段前缀与一段后缀的组合。
考虑枚举前缀选择的人数\(i\),则后缀选择的人数为\(k - i\),每次重新进行\(dp\),复杂度\(O(n ^ 3)\)。
根据乘法原理,我们可以将问题拆分到前缀和后缀各自做,最后再相乘。复杂度\(O(n^2)\)。代码见此。
Huffman
例题1:SMOJ 1757
题意
给定\(n\)堆沙子,每一堆沙子有权值\(w_i\)。给定常数\(m\)。
你需要将这\(n\)堆沙子并成1堆,每次可以合并任意的\(k(2 \leq k \leq m)\)堆沙子,合并的代价为这\(k\)堆沙子的权值和。
最小化总代价。\(1 \leq n \leq 10^5,1 \leq m \leq 500\)。
题解
可用\(k\)叉树表示合并的过程,每个结点\(x\)代表1堆沙子,其深度\(dep[x]\)代表合并次数。最小化\(\sum val[x] \times dep[x]\)。
按照深度由小到大的顺序考虑。深度越小的层,点数越多越好,点权越大越好。因此考虑1种贪心:从第1层开始,除了底层不够点的情况,每1层的点数都为\(m\)。计算价值时自底而上选点,维护1个堆表示当前各点的权值。设\(num = (n - 1) mod (m - 1)\),若\(num > 0\)则一开始将权值最小的\(num + 1\)个点合并成1个点。后面的每1层都是选取当前点中权值最小的\(m\)个点进行合并,再将合并后的点丢入堆中。
时间复杂度\(O(nlogn)\)。代码见此。(远古代码,码风奇丑慎入)
进阶练习1:SMOJ 2976
题意
给定1棵点数为\(2n - 1\)的表达式树,它包括\(n\)个互不相同的变量和\(n - 1\)个运算符。
运算符只有加法和乘法两种,并给出进行1次加法运算的代价\(p\),进行1次乘法运算的代价\(q\)。
运算1个表达式的\(AoB\)的时间为\(max(time_A,time_B) + time_o\),其中\(A,B\)是表达式或变量,\(o\)是运算符。特殊得,单个变量所需时间为0。
现在只运用加法和乘法的交换律和结合律,你希望最小化计算1个表达式的时间。 \(1 \leq n \leq 5 \times 10^5\)。
样例:\(p = q = 1\)时,\((a + (b * (c +d))) + (e * f)\)在变换成\(((a + (e * f)) + (b * (c + d)))\)后最优,耗时为3。
题解
交换律和结合律的实质是,将优先级相同的同种符号取出,将它们连接的各个子问题以任意顺序合并。
假设我们已经递归处理出了子问题各自的最小代价,考虑将其全部合并的最小代价。假设共有\(x\)个子问题,则合并的次数必为\(x - 1\),每次合并的代价为两个子问题代价的最小值再加上该符号的价值。符号的价值是个定值可以不用管;只需考虑最小化 每次合并的两个子问题的代价最小值 的和。类似\(huffman\)树的构建方法,用1个堆维护所有子问题,每次取出子问题中代价最小的两个进行合并即可。
本题的另1个难点可能在于“优先级相同的同种符号”的理解与处理。实际上它等价于表达式树上同种符号的联通块。具体地,假设当前子树的根\(x\)收集到了若干子问题\(y\),若\(x\)与\(Fa[x]\)是同种符号,则将这些子问题直接上传;否则将它们按照上述方法合并后再上传。
对于各个子问题的存储与上传,最直接的做法是在每个节点维护1个小根堆,上传时将\(x\)的小根堆中的节点全部取出放入\(Fa[x]\)的小根堆中,这将会导致每个子问题被上传多次,考虑使用启发式合并,时间复杂度\(O(nlog^2n)\),理论上难以通过\(100\)%的数据。考虑减少不必要的维护和上传。我们仅在\(x != Fa[x]\)的地方需要用到子问题,因此考虑令每个子问题直接贡献到需要它的节点上。具体地,在\(Dfs\)时带参数\(rt\)表示根到\(x\)的路径中最后1个\(x != Fa[x]\)的位置,当\(x != rt\)时将子问题贡献到\(rt\)上即可。时间复杂度\(O(nlogn)\)。代码见此。
化归应用结论
——往往在极端情况下易得最优策略,将任意情况都转化成该极端情况进行求解
例题1:Luogu 3620
题意
给定\(n\)个数轴上的整点,两点间的距离为其坐标差值。
要求选定\(k\)对整点,最小化距离和;这些整点必须两两相异。
\(1 \leq n \leq 10^5 , 1 \leq k \leq \lfloor \frac{n}{2} \rfloor\)
题解
首先容易知道,在最优解中,选择的每对整点都是相邻的两点。
因此,不妨预处理出两两相邻点的间距,设为\(D[1 \rightarrow n - 1]\),问题转化为在\(n - 1\)个数中选择\(k\)个数,这\(k\)个数两两不相邻,最小化数值总和。
不妨从极端情况开始考虑。当\(k = 1\)时,我们一定选择最小值。当\(k = 2\)时,第一步选择最小值意味着下1步会选择除它左右两边的数的最小值;若同时选择在最小值左右的两个数,可能更优。简单来说,\(k = 2\)时仅存在2种策略,要么同时选择最小值左右两边的数,要么选择最小值和与它不相邻的数中的最小值。
考虑将类似的结论推广到\(k\)为任意整数的情况。
推论:在最优解中,要么选择了与最小值相邻的所有元素,要么选择了最小值。
证明:若最优解中不包括最小值及其相邻元素,那么将任意元素替换成最小值结果都不会变差。若最优解中不包含最小值的所有相邻元素,那么将其替换成最小值结果也不会变差。
我们考虑一种化归方法,使得原问题使用推论逐步解决。具体地,假设我们规定选择最小值\(val[i]\),那么其左右邻点都不可选;如果我们仅从序列中取走这个最小值,由于它对相邻元素的影响,你在后续过程中无法确定选择策略。考虑将该最小值及其左右点合并成1个点再放回原位置,即可消除本次选取的影响,使得问题性质不变,可以继续运用推论选点,直到选择完毕。
具体地,设当前最小值为\(val[i]\),每1步将\(val[i]\)计入答案,将其合成1个点权为\(val[i - 1] + val[i + 1] - val[i]\)的点放回原位置。这样处理可以与实际的选择完全等价,且符合推论:若日后选择该新点,则代表选择了\(val[i - 1] 、val[i + 1]\)这两个点,并且\(val[i - 2]、val[i + 2]\)不可被选。
等等,还要考虑1下边界问题。当选择了第\(1\)个点或是最后1个点时,与其相邻的点只有1个,若将这2个点压成新点处理,若选中该新点则等价于选中唯1的邻点,但总共使用了2次选点机会,答案会出错。考虑是否有将其压成新点以便于“反悔”的必要性:答案是没有,在最小值为边界点时,选择最小值一定优于选择邻点。但是在实际编码中,最方便的写法还是统一处理;为了避免“反悔”的执行,为\(val[0]\)和\(val[next + 1]\)设置权值\(inf\)即可。
删点和插点使用链表实现,查询最值使用堆实现。值得注意的是,此处涉及堆中元素的删除,STL无法处理;更为简洁的写法是延迟删除法,即不立即在堆中删除元素,仅用数组标记元素是否仍存在,在取出元素时再进行判断。时间复杂度\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/edit/master/luogu 3620 heap.cpp)。
进阶练习1:POJ 2054
题意
一颗树有\(n\)个节点,这些节点被标号为:\(1,2,3…n\),每个节点 \(i\) 都有一个权值\(a_i\)。
现在要把这棵树的节点全部染色,染色的规则是:
根节点\(r\)可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。
每次染色的代价为\(t*a_i\),其中\(t\)代表当前是第几次染色。
求把这棵树染色的最小总代价。\(1 \leq n \leq 1000 , 1 \leq a_i \leq 1000\)。
题解
本题棘手的地方在于,对点染色除了会累计答案,还有“解锁”效应,且各点的解锁效应与其权值大小没有必然联系。因此在每1步选择权值最大的节点进行染色的策略是错误的。你发现很难直接制定1个同时考虑点权大小和解锁效应的染色方案。
没有对全局的最优方案?考虑一种必定属于全局最优方案的局部方案,并将其不断应用于原问题上。往往在极值处会有这样的局部方案。具体地,我们不能确定染色的方案,是因为解锁的点是未知的。但是,当某次可选点中包含整棵树中点权最大的点时,一定立即选取该点。证明从略。因此,考虑从它开始,扩展出全局的最优选点策略。
具体地,设点权最大的点为\(x\),它被解锁意味着\(fa[x]\)已经被染色;并且一定是\(fa[x]\)被染色后\(x\)立即被染。\(fa[x]\)与\(x\)染色的相对顺序已经确定,故可以缩成1个新点放回树中,继续进行全局最优选点顺序的确定。关键在于为新点赋予合适的权值,使得它在任意时刻被染色,其优劣性都与原两点被染色相同。
考虑大胆猜想:新点的价值为 其包含的点的价值和 除以 其包含的点的数目。
具体证明:
考虑代表\(a\)个点的新点与代表\(b\)个点的新点进行合并时的并点方式。
设这\(a\)个点的价值为\(val[1\rightarrow a]\);这\(b\)个点的价值为\(val[a + 1 \rightarrow a+ b]\)。这里的价值已经按照操作顺序排列。
前者先,则代价总和为\(\sum_{i = 1}^{a}{i \times val[i]} + \sum_{i = a + 1}^{a + b}i\times val[i]\)。
后者先,则代价总和为\(\sum_{i = 1}^{a}{(b + i) \times val[i]} + \sum_{i = a + 1}^{a + b}(i -a)\times val[i]\)
作差可得\(diff = a \sum_{i = a + 1}^{a + b}val[i]-b \sum_{i = 1}^{a}val[i]\)。
将\(diff\)式转化为仅与这两点自身信息相关,设\(n_x,n_y\)分别表示这两点中点的个数,\(v_x,v_y\)分别表示这两点中点的价值和。则\(diff = n_xv_y - n_yv_x > 0\)等价于\(\frac{v_x}{n_x} < \frac{v_y}{n_y}\),因此我们可用点中 实际点的价值和 除以 实际点的数目 作为其等效权值,求解操作序列。
具体的实现中,我们用1个\(vector\)保存新点代表的操作序列。每次查询点权最大者\(p\),并将其代表的操作序列顺次加入\(fa[p]\)的\(vector\)后面,并相应地改变树的结构,将\(p\)的儿子的子节点的父亲节点指向\(fa[p]\),为\(fa[p]\)添加子节点。复杂度瓶颈在于操作序列的合并,\(O(n^2)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/poj 2054.cpp)。
后悔思想
——不一定最优?为后续策略的改变留1个后悔的机会。
例题1:POJ 2431
题意
给定坐标轴上的\(n\)个加油站,加油站在原点前方\(d_i\)的位置,油量上限\(lim_i\)。
你从原点出发,目的是前往与原点相距\(l\)的终点。
你一开始有\(p\)升油。路途中,每走1个单位的距离会失去1升油,当油量为0时无法前进。
为了顺利到达终点,你可以选择在某些加油站停靠加油,但加油量不能超过该加油站的油量上限。
你的油箱没有容量上限,请最小化到达终点的停靠加油次数。若无法到达终点,输出"-1"。
\(1 \leq n \leq 10^4\)。
题解
首先,若停留在某个加油站,一定会加\(lim_i\)的油。
关键问题在于选择哪些加油站停靠。我们当然希望停靠的加油站油箱上限尽量大,但前提是它足够必要且能够救场
我们可以将加油站的存在视作1次加油的机会。前行时先不加油,在油不够时再“反悔”,在之前没停靠的加油站中,从上限大的开始取。由此可以保证加油站必要而最优。
使用大根堆维护,时间复杂度\(O(nlogn)\)。[代码见此](https://github.com/littlewyy/OI/blob/master/poj 2431.cpp)。
进阶习题
最大流算法!通过建立反向边,使得一直有“反悔”,(即改变选择策略)的机会,不断增广直至无法增广。
结语
关于贪心算法的一些思想的总结就到这里。本人才疏学浅,希望通过这个汇总得到一些思维上的启发。
CSP-S 2019 RP ++。