# 练习 ## 经典题目 ### CF 1800C2 贪心,对于每个整数 $0$,选取之前还未被删除的最大正整数并删除。 ### CF 1579D 本题的“配对模型”是一个经典的模型,需要牢记原理。 贪心策略为每次选择两个最大的正整数并减去 $1$。 贪心证明参考[CF 1579D 证明(官方和 chatgpt 概括证明)](CF%201579D%20证明(官方和%20chatgpt%20概括证明).md)(反证法)和[CF 1579D 证明(我的证明)](CF%201579D%20证明(我的证明).md)(数学归纳法)。 也可以一大一小配对。 ### CSES 1164 贪心+小根堆。 - 维护一个小根堆 `pq`,元素(状态)是 `(该房间最后一位顾客的退房日, 房间号)`。 - 将顾客按到达日 $a$ 升序排序(到达日相同的顾客顺序随便)。按顺序处理当前顾客 $(a,b)$: - 若堆中退房日最早的房间的退房日 $\lt a$,说明存在房间可复用(至少堆顶这个可复用),弹出堆顶并把该房间分给当前顾客,再把 `(b, 房间号)` 压回堆; - 否则开新房间,房间数 `rooms++`,分配给当前顾客,并压入 `(b, rooms)`。 实际上将本贪心策略改为选取任意一个退房日 $\lt a$ 的房间都是可以,具体证明请参考 [区间分组](区间分组.md)。 ### 洛谷 P1631 序列合并 堆优化。经典模型。 如果暴力记录所有和,再加上排序,时间复杂度高达 $O(n^2 \log n)$。 我们可以先对 $n^2$ 个和进行分组: * 第一组:$a_1 + b_1, a_1 + b_2, a_1 + b_3, \dots, a_1 + b_n$ * 第二组:$a_2 + b_1, a_2 + b_2, a_2 + b_3, \dots, a_2 + b_n$ * $\dots$ * 第 $n$ 组:$a_n + b_1, a_n + b_2, a_n + b_3, \dots, a_n + b_n$ 由于题目给出的序列 $a$ 和 $b$ 都是单调不减的,我们发现每组内的 $n$ 个和也都是单调不减的。 思想:可以对每一组维护最小值,然后找到 $n$ 个组的最小值,找到后,将该组的最小值更新。 暴力用 $n$ 个指针维护每一组的最小值,每次取一个最小值 $O(n)$,总时间 $O(n^2)$。 设计状态【和,和所属的组,和在所属组中是第几个】。一开始先将每组的最小和的状态存储在堆中。每次取出堆中的最小和后,将该组的后一个和压入到堆中。执行 $n$ 次这样的操作,最小的 $n$ 个和就得到了。总时间 $O(n \log n)$。 为了方便实现,使用结构体表示状态,使用 priority_queue 代替堆。 扩展:$n \times n$ 的数字矩阵中,每行取一个数字得到数字和,求前 $n$ 小数字和。 ### 洛谷 P4053 建筑抢修 反悔贪心。贪心题目基本上是通过大量刷题攒经验和玄学构造来解决的,少数同学能用天赋加速大量刷题的过程。贪心的证明亦如此。 这个题状态有两个属性【修理时长,截止时刻】,可以先对截止时刻排序(相当于是给所有建筑定义了一个优先级,截止时间越早的肯定越要先完成)。 按排序后的顺序模拟修理建筑,并记录修理总时长。反例如下: ``` 3 100 100 2 101 3 102 按此贪心只能修第 1 个建筑,实际能修第 2、3 个 建筑 ``` 如果总时长不超过当前建筑的截止时刻,那就把当前建筑加入到修理计划当中; 否则,说明必然有一个建筑是不能修理的(如何理解?),为了让后面的建筑修理有更多的时间,我们应该不去修理前面所有建筑中时长最长的那一个(如何理解?)。 可以用堆来存储所有修理建筑的时长,加入修理计划——堆中插入元素,移除时长最长的建筑——删除堆中最大值。 ## 前 K 大问题 ### abc234_d 我们可以使用优先队列动态地维护一个序列的前 $K$ 大元素。 假设 $A_1, A_2, \dots, A_i$ 中的前 $K$ 大元素用一个容器存储了下来。当加入 $A_{i + 1}$ 时,有两种情况: - 如果 $A_{i + 1}$ 比容器中最小的元素还要小,则 $A_1, \dots, A_{i + 1}$ 的前 $K$ 大就是该容器存储的元素。 - 否则,删掉容器中的最小元素,并往容器中加入 $A_{i + 1}$,得到 $A_1, \dots, A_{i + 1}$ 中的前 $K$ 大。 实际上,操作可以简化为,把 $A_{i + 1}$ 扔进容器,然后删除容器中的最小元素。使用优先队列可以在 $O(\log K)$ 的时间内维护。 (需要示意图) ### 对顶堆(双堆维护分界线) 对顶堆。用两个相反的堆来对所有元素按值域进行划分。 我们可以设置两个堆,一个大根堆存储较小部分的所有元素,一个小根堆存储较大部分的所有元素。例如当前已有一个排序好的序列 $[A_1, A_2, A_3, A_4, A_5]$,那大根堆中存储 $[A_1, A_2, A_3]$,小根堆中存储 $[A_4, A_5]$。 我们发现: * 堆的大小就是分界线的位置 * 堆顶元素就是分界线两端的元素 * 将一个堆顶元素移动到另一个堆中,就是调整分界线 (需要示意图) ### 洛谷 P1168 动态中位数 核心技术点: 1. 使用两个相反的堆对元素按大小进行分类 2. 将中位数记录在较小元素的大根堆堆顶中 3. 在插入元素后,及时调整分界线;每插入一次,分界线最多移动一次 那么我们在插入元素时,可以根据该元素与两个堆顶的大小来决定他所应该插入的堆(仔细思考插入过程)。在查询中位数时,通过移动堆顶元素(如何移动?)调整分界线的位置,使得大根堆存储的元素数量为 $\left \lceil \dfrac{N}{2} \right \rceil$($N$ 为当前已插入的元素数量),此时大根堆的堆顶恰好就是中位数。 对于此题,可以每次插入元素时,就调整分界线的位置。可以证明每次分界线最多移动一个单位。 插入元素总时间 $O(n \log n)$,移动分界线总时间 $O(n \log n)$(为什么最多移动 $n$ 次?需要示意图) 由于只需要存储值,使用 priority_queue 来实现。 ## 哈夫曼树 [哈夫曼树](哈夫曼树.md) ## 其他习题 ### 简单习题 [CSP-J2020 直播获奖](https://www.luogu.com.cn/problem/P7072) 对顶堆 [P1801 黑匣子](https://www.luogu.com.cn/problem/P1801) 对顶堆 [P2949 Work Scheduling](https://www.luogu.com.cn/problem/P2949) 反悔贪心 [P7913 廊桥分配](https://www.luogu.com.cn/problem/P7913) ### [洛谷 P2278 操作系统](https://www.luogu.com.cn/problem/P2278) 注意:当一个进程 $p$ 还在运行(未结束时),如果来了一个优先级更高的进程 $q$,保留 $p$ 的进度,然后进行 $q$。 枚举每个到来的进程 $i$: - 先执行完等待队列中不被 $i$ 干扰的进程。 - 如果还剩进程(会被干扰的),令接下来要执行的是进程 $j$,比较 $j$ 和 $i$ 的优先级 $p_j, p_i$ - $p_j \ge p_i$,执行 $j$,进程 $i$ 在等待队列中 - $p_j \lt p_i$,执行 $j$ 的一部分直到 $i$ 到来,然后执行 $i$。$i$ 后续是否有优先级更大的进程未知。 将所有进程按照以上步骤加入等待队列后,如果队列中还剩进程,模拟执行。用优先队列维护等待队列,记录每条进程的编号、到达时间、优先级、**剩余**执行时间。 ### [最小函数值](https://www.luogu.com.cn/problem/P2085) $\mathbb N*$ 是正整数集。根据一些初中代数知识可知,$F_1(x) \sim F_n(x)$ 在 $x \in [1, \infty)$ 上单调递增。类似于最大和,我们将 $F_1(1) \sim F_n(1)$ 存入优先队列中,每次弹出最小值 $F_t(x)$ 后将该函数的下一个值 $F_t(x + 1)$ 压入优先队列,操作 $m$ 次即可。时间复杂度 $O(m \log n)$。 ### [洛谷 P2827 蚯蚓](https://www.luogu.com.cn/problem/P2827) 本题先考虑暴力模拟,维护一个蚯蚓列表,每次在列表中查找蚯蚓长度最大值,切半,然后给其他所有蚯蚓长度 $+q$。时间复杂度 $O(m(n+m))$。 所使用到的查找、切半操作可以使用手写堆或优先队列维护,其他所有蚯蚓长度 $+q$,从相对的视角来看是给分出两个蚯蚓的长度 $-q$。这样单次操作能以 $O(\log (n+m))$ 的时间维护。注意切半时需要先还原为原长度。 对于满分做法,有一个结论是先切半分出的蚯蚓长度 $a, b$ 一定比后切半分出的蚯蚓的长度 $c, d$ 长,即 $a \gt c, b \gt d$,因此可以用三个队列分别存储原来的 $n$ 只蚯蚓(从大到小排好)、切半 $\lfloor px \rfloor$ 的蚯蚓、切半 $x - \lfloor px \rfloor$ 的蚯蚓,每次在三个队头寻找最大值,切半后的蚯蚓放在相应队尾即可。 ### [洛谷 P3045 Cow Coupons](https://www.luogu.com.cn/problem/P3045) 贪心策略为选择 $C_i$ 小的奶牛用完优惠券(花钱尽可能少),然后剩下奶牛直接购买。这个贪心显然是错的。 假设存在一个直接购买的奶牛 $j$,买完 $j$ 之后的价格 $x \le M$。如果用优惠券买 $j$,意味着用优惠券购买的奶牛集合中有一头要直接购买,令其为 $i$,此时花费 $x - P_j + C_j - C_i + P_i = x + (P_i - C_i) - (P_j - C_j)$。 令 $D_i = P_i - C_i$ 表示使用优惠券可以优惠的钱数,如果 $D_i \lt D_j$,$x + (P_i - C_i) - (P_j - C_j) = x + D_i - D_j \lt x$,所有后面的奶牛中有可能存在用优惠券买的。 贪心调整为,先选择 $C_i$ 小的 $K$ 个奶牛用优惠券,对于剩余的奶牛 $K + 1 \sim N$ 找直接购买花费最少的奶牛 $x$($P_x$ 最小的)和用优惠券购买花费最少的奶牛 $y$($C_y$ 最小的),然后选择其中花费最少的情况处理。注意延迟删除的问题。