集合选数最值一类问题
一共有两种类型,我分别介绍。
类型一
先来看一道简单的题目:
给你$m$个序列,每个序列有$n$个非负整数,你现在要在每个序列选一个数,这一共有$n^m$种方案,一种方案的值定义为所选的数的和,要你输出值最小的$n$种方案的和。
数据范围: $0 \lt m \le 100$, $0 \lt n \le 2000$。
先考虑$m = 2$的情况,一共有$n ^ 2$种方案,设两个序列为$a, b$,假设我们已经把它们排好序了,即$a_i \le a_{i+1} \ (1 \le i \lt n)$, $b_i \le b_{i+1} \ (1 \le i \lt n)$。方案$f(i, j)$的值为$a_i + b_j$。$f$有以下性质:$f(i, j) \le f(i, k) \ (j \lt k)$,$f(i, j) \ (1 \lt j \le n)$如果是最后的答案,那么$f(i, j - 1)$肯定也是答案。我们只需要维护一个优先队列,初始时将$f(i, 1)$加入优先队列,从优先队列中第$k$次取出的$f(i, j)$即为第$k$小的方案,每次取出后,若$j \lt n$,把$f(i, j + 1)$加入优先队列。时间复杂度为$O(nlog_2{m})$
至此,我们有了$m = 2$的合并算法,当$m \gt 2$时,我们只需要将$m$个序列进行$m - 1$次合并。得到的即为答案。总的时间复杂度为$O(mnlog_{2}m)$,可以通过此题。
我们考虑与此题类似的一个问题,给定$n$个多重集合$c_i$,第$i$个集合的大小为$s_i$,要在每个集合中选一个数,一种方案的值定义为所选的数的和(或积),求第$k$大(或小)的方案的值。
对于此问题上面这个做法还不够优秀。
以下介绍第一种更为优秀的做法。
以求第$k$大为例。
先把集合内元素排序,再以集合的最大值与次大值的差作为关键字对集合进行排序,即对于集合$i$满足$s_{i, j} \le s_{i, j + 1}, \ s_{i, 1} - s_{i, 2} \le s_{i + 1, 1} - s_{i + 1, 2}$ (如果某个集合只有一个元素,那么这个集合只有一种选法,就可以不用考虑了)。
把上面$f$的维数扩展到$n$维,$f(p_1, p_2, \cdots , p_n)$代表一个方案,第$i$个集合选了第$p_i$大的数。每个集合默认选择最大的数,这种选择作为初始方案,再进行逐个集合更改所选的数。我们只需要记录当前待选择的集合的编号$i$,当前集合所选的数第$j$大的数,以及当前方案的值。之前的选择$p_1, p_2, \cdots p_{i - 1}$,完全可以丢弃,$p_{i + 1}, p_{i + 2}, \cdots p_n$默认都是选择最大的,也不用记录,所以$f$只有三维$(i, j, sum)$。
对$f(i, j, sum)$的后继进行定义:
若$j \lt c_i$,$f(i, j + 1, sum - s_{i, j} + s_{i, j + 1})$是它的后继;
若$i \lt n$,$f(i + 1, 2, sum - s_{i + 1, 1} + s_{i + 1, 2})$是它的后继;
若$j = 2 \ \&\& \ i \lt n$,$f(i + 1, 2, sum + s_{i, 1} - s_{i, 2} - s_{i + 1, 1} + s_{i + 1, 2})$是它的后继;
相应的,我们定义前驱。
我们把每种方案看成一个节点,它与它的后继之间的边为后继边,它与它的前驱之间的边为前驱边。
存在以下性质:
$f(i, j, sum)$不小于它的后继,不大于它的前驱。
因为前面我们对集合进行了排序,
满足$s_{i, j} \ge s_{i, j + 1}$,所以第一种和第二种后继(如果存在)存在这种性质。
满足$s_{i, 1} - s_{i, 2} \le s_{i + 1, 1} - s_{i + 1, 2}$,所以第三种后继(如果存在)存在这种性质。$f(i, j, sum)$可以有多个后继,但只有唯一的前驱。
因为我们是逐位更改的,当前集合和所选的数的不同,这两种方案通过后继边所能达到的方案不会有交。对于任意合法的方案最终一定能通过前驱边到达初始方案。
即从最初方案通过后继边可以到达任意合法方案。
这很显然。
那么这就是一棵树。如果一种方案成为答案,那么它的前驱肯定也是答案。那么我们可以维护一个优先队列,存当前可能成为答案的方案,第$k$个答案即为优先队列第$k$次的最大值,一种方案出队后把它的所有后继加入优先队列。
事实上,这种结构只要是一个$DAG$,就可以用优先队列维护了,只不过可能要去重。
这个算法的时间复杂度为$O(nlog_{2}n + \sum_i^n{c_ilog_{2}c_i}+ klog_{2}mk)$,其中$m$为每种方案的平均后继个数。由于$m$是常数,可以忽略,那么时间复杂度就是$O(nlog_{2}n + \sum_i^n{c_ilog_{2}c_i}+ klog_{2}k)$。这个算法是相当优秀的。
类型二
假设我们始终在一个集合内选数,即给你一个有$n$个元素的多重集合$s$,选$m$个不同的数定义为一种方案,求第$k$大(或小)的方案的值。
利用上面的算法的思想,$f(p_1, p_2, \cdots , p_m) \ (1 \le p_1 \lt p_2 \lt \cdots \lt p_m \le n)$为一种方案。
定义后继:如果$f(p_1, p_2, \cdots , p_i + 1, \cdots , p_m)$合法,那么它为$f(p_1, p_2, \cdots , p_m)$的后继。
类似的定义前驱。
但是这是一个DAG,如果这样直接用优先队列做,需要去重。
我们再次利用逐位更改的思想,多加一维$i$,表示$p_{i + 1}, \cdots , p_m$不会更改。
$f(i, p_1, p_2, \cdots , p_i, \cdots , p_m)$的后继有:
如果$f(i, p_1, p_2, \cdots , p_i + 1, \cdots , p_m)$合法,那么它为$f(i, p_1, p_2, \cdots , p_m)$的后继。
如果$f(j, p_1, p_2, \cdots , p_j + 1, \cdots , p_m) \ (1 \le j \lt i)$,那么它为$f(i, p_1, p_2, \cdots , p_m)$的后继。
显然它也有上面的三个性质。
那么这就是一颗树了,可以直接用优先队列做,不需要去重了。
给出$n$个整数$a_1, a_2, \cdots , a_n$,问从中选$m$个数乘积第$k$大是多少。
数据范围:$1 \le n,k \le 10000$, $1 \le m \le13$, $k \le C^n_m$, $-10^6 \le a_i \le10^6$。
假如求的是数和的第$k$大,或正整数的乘积第$k$大是多少,那么可以直接用上面的那个算法。
但是所选的负数的个数或影响乘积的正负,所以我们枚举$m$中选的负数的个数。如果负数个数为偶数,这类方案的乘积非负,选择求绝对值前$k$大的乘积。反之,这类方案的乘积非正,选择求绝对值前$k$小的乘积。
参考资料
POJ2442 Sequence
第十三届北航程序设计竞赛预赛
第十三届北航程序设计竞赛预赛题解
LibreOj #6254. 最优卡组
Sgu 421. k-th Product
bzoj 1425: SGU 421 k-th Product
Sengxian’s Blog
ZJOI2010讲课