do_while_true

一言(ヒトコト)

一类堆贪心前 k 优方案题

给雷暴磕头了/kt 给雷暴磕头了/kt 给雷暴磕头了/kt

求组合前 k 大用堆贪心,需要构造转移使得状态之间成为一个外向树,并且转移不重不漏,而且一个状态的后继很少。这样用堆贪心贪出来即可。

P1

给定非负整数构成的多重集,求前 \(k\) 小子集和。

从小往大排序,想用“先移动最右边的到指定位置,再移动最左边的到指定位置......”这个策略去描述状态的转移。首先选把选了前 \(i\) 个的状态 push 到堆里面。发现描述转移的时候需要记录的信息有:前缀 \([1,x]\) 还没有移动,当前正在移动的物品位于 \(y\),上个物品停在了 \(z\).转移就是 \(y\) 继续往后一格但是不能越过 \(z\),或者 \(y\) 彻底停住再去移动 \(x\)

P2

\(n\) 个正整数数组,一个方案的代价是每个数组里面恰好选一个数再求和,求前 \(k\) 小的方案的代价。P2541

设计策略是挨个数组确定选第几个。也就是 \((pos,x)\) 表示选到了第 \(pos\) 个数组,当前这个数组选的是第 \(x\) 个人.但是发现计重了,\((2,1,1)\)\((2,1)\)\((3,1)\) 两个都算到了。原因在于 \(x\) 可以为 \(1\),但是后缀还没决策的数组选的全是 \(1\),于是对于一个状态来说无法唯一确定它的 \(pos\)

于是强制让 \(x\geq 2\),那么转移就分三种:

  • 当前数组从第 \(x\) 个换成第 \(x+1\) 个;
  • 换到下一个数组,从第 \(2\) 个开始选;
  • 如果当前 \(x=2\),说明可以撤销回 \(x=1\),然后从下一个数组的第 \(2\) 个开始选。

为了让第三个转移非负,按照 \(a_2-a_1\) 将所有 \(a\) 排序即可。

除了全选第一个的最优解以外,其余的均能转移到并且仅被转移一次(按照这个策略去尝试把一个状态凑出来,只有一种方案)。

P3

P2 改成每个数组要选数量在 \([L_i,R_i]\) 中的若干个数。P6646

对于单个数组,用 P1 中的方法已经可以求出前 \(k\) 小的方案,看成一个黑箱,结合 P2 做法即可。

P4

给定非负整数 \(a\),对于排列 \(p\) 代价为 \(\sum a_ip_i\),求前 \(k\) 小的代价和。

最优就是 \(a\) 降序排序之后 \(p_i=i\),然后考虑一个排列从后往前插排去得到。这个时候状态记录 \((p,x)\) 为现在从后往前插排在将 \(p\) 往后移动移到了第 \(x\) 个位置。同 P2 中的问题一样,当 \(p=x\) 的时候会出现问题,但是套用 P2 中的做法,先强制 \(x>p\),然后再考虑撤销往前也不行,因为这里并不像 P2 中一样可以交换数组之间顺序使得转移非负。

这里转移非负的问题就在于,为了剪枝掉后继个数,当前的 \(p\) 已经插排完毕,选择下一个进行插排的 \(p\) 的时候想直接通过对 \(p=x\) 的状态进行撤销再选择另一个 \(p\)(形象一点的描述就是,运用了儿子兄弟表示法)。思路往回推,记录当前已经插排完毕的是哪个 \(p\),选择下一个 \(p\) 的时候从最优开始,最优完了再找次优,次优完了再往后找...

于是记录 \(pre\) 表示已经考虑完了 \(>pre\) 的所有位置,\([1,pre]\) 还没有尝试插排。现在有 \(pre=p-1\).如果继续移动 \(p\) 那么直接正常做;如果当前的 \(p\) 移动完了,去找下一个 \(p\),对 \([1,pre]\) 里面每个 \(i\) 记录 \(f_i\) 表示下一个交换 \(i\) 多出来的代价是多少,那么找 \(f\) 最小的那个位置当作下一个 \(p\),此时让 pre 不变

那么当 \(pre\geq p\) 的时候就意味着当前状态的 \(p\) 是尝试新开一个 \(p\) 得到的状态,那么它的转移就多了一个撤销当前的 \(p\),尝试比它次优的那个 \(p\),于是需要找到按 \(f\) 排序之后它的后继。只有 \(f_{pre}\) 不是 \(a_{pre+1}-a_{pre}\),其余的 \(f_i\) 均为 \(a_{i+1}-a_i\),所以尝试新的 \(p\) 的时候单独把 \(pre\) 作为一个转移拎出来,现在 \(f_i=a_{i+1}-a_i\),所以直接在主席树上二分即可找到后继。

整理一下现在的转移:

\(pre=p-1\) 时:

  1. 继续移动 \(p\),此时 \(x\gets x+1\)\(pre\) 被强制更新为 \(p-1\)
  2. 选择下一个 \(p'\),并且选择的就是 \(p-1\),此时 \(pre\) 被强制更新为 \(p-2\);
  3. 选择下一个 \(p'\),选择的是 \([1,pre)\) 中的,此时 \(p'\) 应选择 \(f\) 最小的,\(pre\) 不变。

\(pre\geq p\) 时:

  1. 同上;
  2. 同上;
  3. 类似,选择下一个 \(p'\),选择的是 \([1,p-1)\) 中的,此时 \(p'\) 应选择 \(f\) 最小的,然后 \(pre\) 被更新为 \(p-1\)
  4. 撤销当前选的 \(p\)\(p'\) 选为 \([1,pre]\) 里面选当前的 \(f\) 的后继,\(pre\) 不变。

还需要维护各个转移需要的代价,由于只有交换操作,所以用个可持久化线段树就行了。

posted @ 2023-08-20 21:11  do_while_true  阅读(90)  评论(0编辑  收藏  举报