贪心

反悔贪心

可以看成对于一个错误贪心的修正,使得可以带着反悔/撤回选项。

一般是由两个部分构成,直接选择 \(+\) 反悔。需要对于两个策略都进行设计。

P3620 [APIO/CTSC2007] 数据备份

我们可以发现对于某一位置 \(i\) 来说,如果 \(d_i<\{d_{i-1},d_{i+1}\}\),那么如果要选就是 \(d_i\) 或者 \(\{d_{i-1},d_{i+1}\}\) 打包选。于是如果选中某一位置,直接再插入一个 \(d_{i-1}+d_{i+1}-d_i\) 的选项即可。

CF865D Buy Low Sell High

这其实有点两两匹配的感觉,若干对二元组 \((i,k)\) 表示在第 \(i\) 天买入,第 \(k\) 天卖出。可是我们可能在 \(i<j<k\) 中的 \(j\) 点就匹配到了 \((i,j)\) 怎么办呢?我们可以加入一个反悔操作,\((i,k)=(i,j)+(j,k)\)

对于每个 \(i\) 我们先看看以堆中最小代价购买,能否通过 \(p_i\) 获利,如果可以就拿出去匹配一下,并加入撤回选项 \(-p_i\),最后要插入一个购买选择选项 \(-p_i\)

可能会有疑惑,在插入撤销选项了之后为什么还要加入购买选项呢?因为在 \(k\) 处撤销 \((j,i)\) 表示我们匹配了 \((j,k)\),此时 \(i\) 又空闲了,我们还需要一个购买选项。

P3545 [POI2012] HUR-Warehouse Store

能选就选,不能选就找找前面有没有选过 \(b_j>b_i\) 的,替换成 \(b_i\) 显然更优。

P2107 小Z的AK计划

和上一题同理维护即可,注意本题还有路程代价,计算路程代价的时候如果当前时间不够需要一直去除前面的选择直到够用。

P11268 【MX-S5-T2】买东西题

按照 \(a,w\) 降序扫描然后用优惠劵去匹配物品是一个很难搞的方法,我们应该以物品为主体去匹配优惠券。

对于 \(a,w\) 升序排序,用一个队列保存可使用优惠券和可替换优惠券(某个物品改为优惠价然后把券让给后面的物品)可优惠的价格反悔贪心即可。

CF436E Cardboard Box

本题直接选择的策略是花费 \(a_i\) 购买一颗星,和花 \(b_i-a_i\) 再购买第二颗星。

反悔策略一定要考虑全,反悔的策略是把一个一星不选,选另一个两星和把一个两星降为一星,选择另一个两星。由于一星已经放在直接选择的选项里了,所以反悔不需要考虑选择一星。

动态维护选 \(i\) 颗星的最小代价,维护若干选法的最小值,每次从堆中选择最小代价扩展一个即可。

P9600 [IOI 2023] 封锁时刻

由于 \(nk\) 是很大的,所以不是树上背包,是贪心之类的算法。

对于 \(X,Y\) 选择点不重合的时候,就是按照 \(dis\) 从小到大分配即可。

首先,\(X,Y\) 取到点可能会有交。于是设 \(c_{i,1}=\min(dis(X,i),dis(Y,i))\)\(c_{i,1}=\max(dis(X,i),dis(Y,i))\),分别表示 \(i\) 贡献 \(1/2\) 的代价。

这个形式就是 CF436E,但是会有若干约束。

对于 \(A\) 部分的 \(X\) 侧,如果对于某个 \(u\) 点其选择了某个状态 \(c_{u,i}\),则 \((u,X)\) 路径上每个点 \(v\) 必然选择状态 \(c_{v,\ge i}\)。对于 \(C\) 部分,约束的是 \(u\)\(X-Y\) 中点的路径。对于 \(B\) 部分约束其最上面那个在路径上的点。

可以发现对于 \(A\) 部分,\(X\to u\) 路径上的 \(c\) 是单调递增的,所以我们贪心的时候必然先选择了前面的点,因此这个约束必然满足,不需要额外限制。

对于 \(C\) 部分,可以发现链上 \(c_{i,2}\) 呈单谷,所以必然也是先选择了中间点,也是等价于不需要额外限制。对于 \(B\) 部分同理。

注意上面只说了 \(c_{i,2}\) 单谷,\(c_{i,1}\) 则是单峰的并不满足这个性质。所以我们先需要特判一下 \(X,Y\) 选择点不重合的情况。然后此时就必然重合了,我们就提前把 \((X,Y)\) 路径上的所有点的 \(c_{i,1}\) 给选上,然后再运行 CF436E 的贪心就行了。

Johnson 法则

对于邻项交换法中排序不满足不可比传递性,也就是可以通俗理解为排序函数不满足 \(A=B,B=C\),那么 \(A=C\) 的情况。

很经典的案例就是按照 \(\min(a_i,b_j)<\min(a_j,b_i)\) 排序。需要我们分类讨论之后拆开来排序。

例题:P1248 加工生产调度P2123 皇后游戏

杂题

P9755 [CSP-S 2023] 种树

显然是二分答案天数转化为判定。显然每个点每天高度单调递增,于是长到第 \(r\) 天显然是最优的。于是只需要求出最晚下种天数 \(t_i\) 是否满足 \(h(t_i,r)\ge a_i\)。这个过程同样为二分,\(c_i\) 可能小于 \(0\) 有点难搞,所以这个过程一定要小心讨论,不要乱讨论注意讨论的普适性。

  • $ c_i\ge 0 $ 则有 \(\sum\limits_{i=l}^r b_x+ic_x=(r-l+1)b_x+ \frac{(r-l+1)*(l+r)}{2} c_x\)
  • \(c_i<0\) ,可以求出 \(i_{max}\) 讨论 \(i_{max}\)\([t_i,r]\) 的关系分段计算即可,在 \([l,i_{max}] 和 [i_{max},r]\) 用不同策略。

然后就是一个贪心判定的过程。第一反应肯定是期限最短的树先种,但是貌似不对,因为这样可能会导致一个期限更长但是链也更长的一个位置无法种到。不过我们不能轻易否决任何一个做法,其实稍加证明这个做法是对的,如果先短后长会超时,我们交换一下二者顺序,发现先长后短也会超时。所以这种做法是正确的。

考场失误:没开 __int128,
if(val1-val2>=a[i]) T=calc(i,dat); 此处二分上界应该是 \(dat\) 而不是 1e9。如果是后者的话,会二分到负的地方去。
T=dat-i+1+a[i]+val2-val1; 一定要想清楚,这里还有 \(-i+1\)

P8272 [USACO22OPEN] Apple Catching G

列出奶牛可以接到苹果的条件,假如一头 \((t,x)\) 的奶牛可以接到 \((t',x')\) 掉落的苹果必须满足 $ \lvert x-x' \rvert \le t'-t$。得到 $ t+x \le t'+x' $ 且 \(t-x \le t'-x'\)。注意有这两个式子后必然满足 \(t \le t'\)

然后似乎不太好去分配奶牛的去接什么苹果。此时我们不妨大胆地贪心匹配。将约束条件画在二维平面中,发现每个奶牛能接到的范围就是一个倾斜的正方形。斜着不太好看,我们可以先旋转 \(45\) 度,发现越往左下角的正方形包括的区域越大肯定是更优的,于是我们可以贪心将右上角的不优的矩形去先匹配靠右上角的苹果,也就是横纵坐标之和更大的苹果。我们再旋转回来,对应的也就是纵坐标更大的苹果应该被靠上的矩形先匹配到。于是用一个 set 维护即可。

UVA1205 Color a Tree

贪心题。可以先考虑一个错误的贪心,就是最先染色当前可选点中权值最大的点,这显然不对。但是启发了我们,如果当前可选点就是所以未选点中最大的一个,那么我们应该对它进行染色。但是我们无法保证在当前局面下就存在这么一个点,所以我们应该提前操作或者倒序操作,先对权值最大的点进行处理,这个点应该与它的父亲节点操作连续,所以我们可以对他们俩合并,合并后权值如何计算呢,
不妨考虑三个点 \(x,y,z\)\(x\)\(y\) 操作连续。

\(x+2y+3z\) vs \(z+2x+3y\)

作差可得 \(2z-x-y\) , 就是比较 \(z\) 和 $ \frac {x+y}{2} $ 的大小。

于是我们可以给出等效权值的计算方法 \(val'= \frac{x+y}{2}\)

如果是多个节点的等效权值呢,不要想当然认为是 类似 $ \frac{ \frac{x+y}{2} +z} {2}$ 的形式,可以自己手动模拟一下其实是 $ \frac{ \sum c_i}{num}$。 如何理解呢,等效权值没有计算意义,只是为了比大小用的,所以在列式子的时候还是要拆开为每一个单点,不可用等效权值来列上面的不等式!

AGC018C Coins

三元选择显然是不太方便,我们可以先假设全选金币,然后再从金币中选一部分改为银币,还有一部分改为铜币,这是一个二元选择的问题。

我们可以用交换法来确定选择策略,设 \(s_i=b_i-a_i\)\(t_i=c_i-a_i\)。对于 \((s_i,t_i)\)\((s_j,t_j)\),目前 \(i\)\(s\)\(j\)\(t\),考虑何时交换更优。

满足 \(t_i-s_i+s_j-t_j \ge 0\),即 \(s_j-t_j \ge s_i-t_i\) 的时候 \(j\)\(s\)\(i\)\(t\) 更优。于是我们按照 \(s_i-t_i\) 升序排序。所有选 \(s\) 的都在选 \(t\) 的左边。但是注意这里不可选择前 \(y\) 个用 \(s\),后 \(z\) 个用 \(t\),而是应该找到一个分界点,满足在分界点之前中分配 \(s\),分界点之后分配 \(t\)。于是预处理一下前缀前 \(y\)\(s\) 和后缀前 \(z\)\(t\),然后枚举分界点即可。

ZROI2834.念念不忘

全局贪心

全局整体考虑,我们可以转化一下思路,不一定是从子节点往上移动。可以变成从无到有的放置棋子,只需要满足子树总和小于子树大小即可。
我们对于每一个点,将从目前状态放置一枚棋子的代价放入堆中,每次选取代价最小的点放置。同时给上方的链上所有点允许放置的个数集体减 \(1\)
查群该点如果还能放的话,就算一下下次添加的代价,放入堆中。可以用树链剖分维护,时间复杂度 \(O(n\log^2n)\)

子树贪心

赛时想到了将目前代价最大的节点往上放最近的可以放的,树剖维护。感觉复杂度要爆,而且难实现,正解很接近,思路有点像。我们不要零散往上放,而是从子树合并上来的时候一起决策放。
如果一个节点放了多个点,为了方便写,可以拆开成多个大代价的点插入好写点。按照上面所说的增减代价放。
复杂度看似极限情况下不对,但实际上是对的。比如我们决策 \(u\),肯定是将 \(u\) 子节点的中权值最大的一些替换为 \(u\) 中最小的一些。将 \(u\) 升序排列,\(v\) 降序排列。假设替换了 \(p\) 次,那么根据等差数列公差 \(u_{p+1}>2u_{p/2}\)\(v_{1,2,..p}<v_{p+1}\)\(u_{p+1}>v_{p+1}\),可得 \(v_{1,2..p}\) 都变成了小于自己一半的数。那么一个数最多也就会变化 \(\log\) 次,故复杂度正确。

ZROI3071.宇宙

定义一点的权值 \(w_u=a_u+dep_u\)

首先交换法可证我们必然是选择权值最大的点来放,当然我们必须先把它整个子树放完。于是我们可以用 solve(u) 表示放 \(u\) 子树内的所有点。用线段树维护 dfs 序,然后不断找到当前子树内的最大点 \(v\),执行 solve(v),然后将 \(v\) 的权值修改为 \(-\infty\),直到 \(u\) 子树内的所有点被放完。时间复杂度 \(O(n\log n)\)

P2512 [HAOI2008] 糖果传递

先考虑一条线的情况,我们设 \(ave\) 表示平均个数,\(pre\) 代表前缀和数组。
最后答案就是 \(\sum \lvert pre_i-ave\rvert\)

\(s_i=pre_i-ave\),考虑环形是改变了什么其实如果我们在环的一点断开,那么其实就是改变了 \(s\) 数组。假如断开了 \(k-k+1\) 这个位置,那么对于 \(i\in [k+1,n],s_i\to s_i-s_k\),对于 \(i\in[1,k],s_i\to s_i+s_n-s_k\)。又因为 \(s_n=0\),所以对于每个点的变化就是减去了 \(s_k\),那么现在就是最小化 \(\sum\lvert s_i-s_k \rvert\),选取中位数点即可。

道路(road)

\(n\) 个点,每个点有权值 \(a_i,b_i\)。从 \(i\)\(j\) 的代价是 \(\max(a_i^2-b_j^2,0)\),求从 \(1\) 出发,经过每个点各一次最后回到 \(1\) 的最小代价。

二分答案加判定是不太好做的。考虑贪心对于 \(a,b\) 排序,大的 \(a\) 匹配大的 \(b\),以此类推。这样子连出来是若干个环,我们要把它合并成一个大环。

P11983 [JOIST 2025] 展览会 3 / Exhibition 3

这题还是太牛了。可惜我一开始方向就错了。

题目需要重新排列 \(A\),我想的是贪心一下之后一些数的位置有一些取值范围,然后是一个数和位置的匹配形式。这个很难做。

事实上,考虑如下贪心。我们从大往小枚举每一个数字 \(i\),然后从前往后枚举 \(b\) 序列每一个位置 \(j\),根据贪心我们希望尽可能在这些位置填入当前数字。考虑动态维护对于 \(i\),已经确定填 \(i\) 的位置集合。尝试加入一条新线段 \([l_j,r_j]\),对于集合内部的位置所对应的线段还有当前线段,我们跑一遍最少点覆盖所有区间的贪心算法,如果需要点的个数 \(\le cnt_i\),那么代表当前线段可以加入。暴力做这个过程是 \(O(n^3)\) 的。

这是一个最基本的思路,而我之所以一开始就错了,是因为我认为需要看看 \(A\) 序列中哪个位置被占了不能填数字了,实际上这个过程是不需要的。假设我们在 \(j\) 的时候为了满足某一条线段要求而填入的位置和 \(i>j\)\(i\) 填入位置有冲突的话,那 \(i\) 早就可以提前满足过这个线段的要求了。

考虑优化,一开始 \(cnt_i\) 个点必然是用不满的,所以每次跑一遍区间选点太浪费了,可以直接二分出一个前缀满足可以恰好用掉 \(cnt_i\) 个的最长前缀。直接二分的复杂度是 \(O(n\log n)\),对于每个 \(i\) 都做的话,复杂度显然是错的了。我们希望复杂度和这次被选中区间的个数相关,这样子就可以满足复杂度均摊正确了。这里可以使用倍增,我们依次枚举长度为 \(2^0,2^1...2^k\) 的前缀,找到一个最大 \(k\) 满足 \([1,2^k]\) 前缀是合法的,这样子对于 \(i\) 的倍增求解的复杂度是 \(O(ans_i\log^2 n)\),均摊正确。为了确定前缀,我们还应该在 \([2^k,2^{k+1})\) 之中找到一个比 \(2^k\) 更远的位置,这个时候可以直接二分了,因为保证了长度是 \(O(ans_i)\) 级别的。

找到最长前缀之后,用满了 \(cnt_i\) 个点。剩下还会选一些零散线段,我们需要对于这个过程快速 check。

首先,思考如何判定能否加进去一个线段。我们跑正反两遍点覆盖区间,可以确定是每个点放置范围 \([nl_i,nr_i]\),只要当前线段 \([l_j,r_j]\) 和其中某个区间 \([nl_i,nr_i]\) 有交就可以加入线段 \(j\)

问题来到了如何在加单点之后快速更改为新的 \([nl_i',nr_i']\) 信息。通过加入的新线段,我们可以通过二分查找迅速定位到影响是的哪个点范围的左端点,哪个点的右端点。

假如说我们要新加入一个右端点的限制 \(R\),可以通过二分定位到需要修改某个点 \(i\) 的右端点。考虑一个从左往右的贪心过程,显然 \(i-1\) 以及之前的位置是不会发现改变的,根据贪心过程对于当前 \(i\)\(nr_i\) 需要覆盖到 \(\min\limits_{l_j>nr_{i-1}}r_j\),直接赋值即可。同时,由于 \(i\)\(nr\) 信息变动了,可能会影响其右边若干点的 \(nr\),所以我们需要不断找到受到影响的 \(nr\) 进行修改。还是同理寻找 \(\min\limits_{l_j>nr_i} r_j\) 来更新 \(nr_{i+1}\),直到无法更新就退出循环。

看起来一个一个修改是很暴力的东西,其实复杂度是正确的。令 \(S\) 集合代表当前选择线段的集合,还是以右端点为例,当前所有点取值范围的右端点构成的集合 \(T\),其实是 \(\subset \{r_i~|~i\in S \}\)。上述暴力更新的过程,其实就是点进入集合的过程,同时每个点最多进入集合一次,离开集合一次(否则就还能找到更长前缀,与当前是最长前缀矛盾)。所以暴力更新的总次数是 \(O(|S|)\) 的。

还剩最后一个问题,我们会了快速判定一条线段能否加入,以及快速更改加入线段之后的新信息。那么如何找到这么一条肯定能加入的线段呢,如果直接枚举,会枚举到一些不能立刻加入的线段,导致复杂度均摊不正确了。我们可以发现所有点的取值范围都是两两不交的,所以预先处理掉哪些满足完全包含住某个点取值范围的线段之后,剩下的线段都最多与两个点的取值范围有交。直接通过枚举点来找到这些线段就行了。以上操作都可以用线段树维护若干信息来完成。

时间复杂度 \(O(n\log^2 n)\)

LOJ4888. 「ROI 2025 Day1」树上的青蛙

首先有一个很显然的贪心,我们直接维护两个奇偶深度的 std::set存着子树内待匹配点的深度。启发式合并,然后在 \(u\) 进行合并,暴力遍历其中一个 set,在另一个 set 中进行匹配,尽可能匹配卡住上界 \(D\) 的点对。需要注意的是不能直接找到就默认匹配成功,如果找不到恰好等于 \(D-1\)\(D\) 的点,就不匹配,延迟到祖先再匹配,因为在不影响可能点对的情况下可以多保留一些决策。同时删除距离当前点 \(\ge D\) 的点。时间复杂度 \(O(nD\log n)\)。之所以是 \(D-1\) 是因为两个点每次上升深度变化总和为 \(2\),不过注意到两个点的深度奇偶性不同,所以 \(D-1\)\(D\) 只有一个有用,如果 \(D\) 为偶数,我们直接 \(D\gets D-1\) 即可,这样只需要判定 \(D\) 了。

注意到时间复杂度的瓶颈在于我们每次需要遍历整个集合,因为我们不知道哪些有用。如果我们只遍历到那些有用的,就可以把复杂度降下来,这个过程需要精细实现。将原先的 set 存所有点,改为对于每个深度单独维护,我们用两个代表不同奇偶的 std::map,其中 key 值为那些存在的深度取值,value 存的是一个 std::vector 里面是保存的点的编号(用来构造方案)。

对于新加入的一种 key 值(深度),我们在当前 map 内找到最能契合它的另一个深度,找到这个深度之后不能立即匹配,我们应该比如 \(d_1+d_2-2\times dep_u < D\) 的时候,可以发现我们应该在恰好 "=" \(D\) 的祖先去匹配,也就是说在深度为 \(\dfrac{D-d_1-d_2}{2}\) 的点来匹配这两种深度。用一个优先队列来维护这个东西,按照处理点的深度从大到小处理,每次在对应深度弹出可以匹配的点对,注意需要判断一下这一深度的点有没有被别的匹配用完。匹配完之后,需要重构两个集合中 \(\le d_1\)\(\le d_2\) 的匹配。

最后在根节点还剩若干未匹配点,我们用两个 std::set 直接贪心匹配即可。时间复杂度 \(O(n\log^2 n)\)

P8179 「EZEC-11」Tyres

如果没有初始的代价 \(t\),那么对于每种轮胎其代价关于使用圈数是单调的。直接做一个归并排序,取前 \(m\) 大之和就行了。

可以有了 \(t\) 之后,相当于对于第一次的代价进行了 \(+t\),这样子本来单调递增的一系列点中的第一个点发生了抬升,出现了一个悬浮点,也就不满足单调性了。我们就无法执行上述的归并排序的贪心了。

但是由于有 \(x^2\) 这一项这个点列的增长速度是很快的,经过 \(O(\sqrt t)\) 次增长之后,后续的点就都是在第一个点之上了。因此考虑对于 \(\sqrt t\) 之前的点特殊考虑,对于后续的点进行贪心。

具体来说,我们对于前 \(\sqrt t\) 个点跑一次 dp 的背包。对于后面的点跑一次贪心。分别求出两种的代价,合并一下即可。

时间复杂度 \(O(n^2t+m\log n)\)

posted @ 2024-09-28 10:06  Mirasycle  阅读(41)  评论(0)    收藏  举报