集合选数最值一类问题
Posted tkandi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了集合选数最值一类问题相关的知识,希望对你有一定的参考价值。
一共有两种类型,我分别介绍。
类型一
先来看一道简单的题目:
给你(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讲课
以上是关于集合选数最值一类问题的主要内容,如果未能解决你的问题,请参考以下文章