题目来源
Problem - 4546 (hdu.edu.cn)
题目描述
最近,小明出了一些ACM编程题,决定在HDOJ举行一场公开赛。
假设题目的数量一共是n道,这些题目的难度被评级为一个不超过1000的非负整数,并且一场比赛至少需要一个题,而这场比赛的难度,就是所有题目的难度之和,同时,我们认为一场比赛与本场题目的顺序无关,而且题目也不会重复。
显而易见,很容易得到如下信息:
- 假设比赛只用1个题目,有n种方案;
- 假设比赛使用2个题目,有(n-1)*n/2种方案;
- 假设比赛使用3个题目,有(n-2)*(n-1)*n/6种方案;
- ............
- 假设比赛使用全部的n个题目,此时方案只有1种。
经过简单估算,小明发现总方案数几乎是一个天文数字!
为了简化问题,现在小明只想知道在所有的方案里面第m小的方案,它的比赛难度是多少呢?
输入描述
输入数据的第一行为一个整数T(1 <= T <= 20),表示有T组测试数据。
每组测试数据:
- 第一行为两个整数n, m(0 < n, m <= 10000),表示有n个题目,现在要求第m小方案的比赛难度。
- 第二行有n个数字,分别表示这n个题目的难度值。
输出描述
对于每组测试数据,输出一行"Case #c: ans"(不包含引号),ans 表示要求的第m小的比赛难度,输入数据保证存在第m小的方案,具体参见样例。
用例
输入 | 2 5 6 1 1 1 1 1 5 25 1 2 3 4 5 |
输出 | Case #1: 2 Case #2: 11 |
说明 | 无 |
题目解析
本题其实就是让我们求解: 全组合中"第m小"组合(按组合之和排大小)。
比如nums = [1,2,3],则全部组合如下:
- [1]
- [1,2]
- [1,3]
- [1,2,3]
- [2]
- [2,3]
- [3]
其中:
- 第1小的组合为[1]
- 第2小的组合为[2]
- 第3小的组合为[3]
- 第4小的组合为[1,2]
- 第5小的组合为[2,3]
- 第6小的组合为[1,2,3]
由于本题数量级较大,因此如果暴力地求出全组合,然后按组合之和排序,求出第m小的组合,肯定会超时。
如果我们将nums进行升序,比如nums = [1, 2, 3, 4],那么第1小的组合肯定是 [1]。
而第1小组合[1],可以看成是基于一个空组合[],加了一个nums最小元素1产生的。
第2小组合此时就有了两种来源:
- [1] 组合加入下一个最小元素2,形成[1,2]
- [] 组合加入下一个最小元素2(理论上,空组合下一个元素应该是1,但是由于空组合合入过1形成了[1]组合,因此下一步应该合入2),形成[2]
对比可得第2小组合是[2]。
此时,我们有了三个最小组合,分别是[],[1],[2],如果在为每个组合标出下一个合入元素的话,则可得信息如下:
- 当前组合 = [],将要被合入的元素 = 3
- 当前组合 = [1],将要被合入的元素 = 2
- 当前组合 = [2],将要被合入的元素 = 3
此时根据”当前组合“、”将被被合入的元素“,我们可以得出将要产生的"新组合":
- 当前组合 = [],将要被合入的元素 = 3,将要产生的新组合 = [3]
- 当前组合 = [1],将要被合入的元素 = 2,将要产生的新组合 = [1,2]
- 当前组合 = [2],将要被合入的元素 = 3,将要产生的新组合 = [2,3]
可以对比出,将要产生的新组合中[3]和[1,2]是本轮最小的,我们可以任选一个作为整体第3小。
假设我们选择[3]作为第3小组合,则组合信息变化如下:
- 当前组合 = [],将要被合入的元素 = 4
- 当前组合 = [1],将要被合入的元素 = 2
- 当前组合 = [2],将要被合入的元素 = 3
- 当前组合 = [3],将要被合入的元素 = 4
此时再算出新组合信息,如下:
- 当前组合 = [],将要被合入的元素 = 4,将要产生的新组合 = [4]
- 当前组合 = [1],将要被合入的元素 = 2,将要产生的新组合 = [1,2]
- 当前组合 = [2],将要被合入的元素 = 3,将要产生的新组合 = [2,3]
- 当前组合 = [3],将要被合入的元素 = 4,将要产生的新组合 = [3,4]
可以得出本轮最小,整体第4小组合是[1,2]。
此时组合信息变化如下:
- 当前组合 = [],将要被合入的元素 = 5
- 当前组合 = [1],将要被合入的元素 = 2
- 当前组合 = [2],将要被合入的元素 = 3
- 当前组合 = [3],将要被合入的元素 = 4
- 当前组合 = [1,2],将要被加入的元素 = 3
由于nums只有[1,2,3,4]元素,没有5,因此对于组合 [] 来说,已经没有新元素可以合入了,因此我们只需要处理剩下的组合即可
- 当前组合 = [],处理完毕
- 当前组合 = [1],将要被合入的元素 = 3,将要产生的新组合 = [1,3]
- 当前组合 = [2],将要被合入的元素 = 3,将要产生的新组合 = [2,3]
- 当前组合 = [3],将要被合入的元素 = 4,将要产生的新组合 = [3,4]
- 当前组合 = [1,2],将要被加入的元素 = 3,将要产生的新组合 = [1,2,3]
可以得出本轮最小,整体第5小组合是[1,3]。
此时组合信息变化如下:
- 当前组合 = [],处理完毕
- 当前组合 = [1],将要被合入的元素 = 4
- 当前组合 = [2],将要被合入的元素 = 3
- 当前组合 = [3],将要被合入的元素 = 4
- 当前组合 = [1,2],将要被加入的元素 = 3
- 当前组合 = [1,3],将要被加入的元素 = 4
按上面逻辑,第m轮得出的最小组合就是第m小的组合。
具体代码实现中,我们可以使用优先队列来找出每轮最小的:”将要产生的新组合“。
而加入优先队列的组合模型设计如下:
- 当前组合之和 curSum
- 将要被加入当前组合的新元素索引位置 nextIdx
根据这两个信息可以得出”将要产生的新组合“之和 = curSum + nums[nextIdx]
而对于一个组合模型,其”将要产生的新组合“之和越小,则优先级越高。
JS算法源码
关于JS中优先队列的实现可以参考:LeetCode - 1705 吃苹果的最大数目_leetcode - 1705 吃苹果的最 数 _伏城之外的博客-csdn博客_伏城之外的博客-CSDN博客
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;// 输入处理
void (async function () {const t = parseInt(await readline());const ans = [];for (let i = 1; i <= t; i++) {const [n, m] = (await readline()).split(" ").map(Number);const nums = (await readline()).split(" ").map(Number);ans.push(`Case #${i}: ${getResult(n, m, nums)}`);}for (let an of ans) console.log(an);
})();// 算法入口
function getResult(n, m, nums) {nums.sort((a, b) => a - b);// 对于一个组合模型,其”将要产生的新组合“之和越小,则优先级越高// curSum + nums[nextIdx] 为 ”将要产生的新组合“之和const pq = new PriorityQueue((a, b) => a.curSum + nums[a.nextIdx] - (b.curSum + nums[b.nextIdx]));// 空组合的和为0, 将要加入的新元素是nums[0], 即索引0的元素,其将要产生的新组合之和为 0 + nums[0]let c = new CombineModel(0, 0);for (let i = 1; i < m; i++) {// c是当前最小组合模型,最小的组合模型指的是将要产生的新组合之和在对应轮次中最小// 如果当前组合模型c还有可合入的下一个元素,即c.nextIdx + 1 < n, 则说明可以基于当前组合模型产生一个新组合if (c.nextIdx + 1 < n) {// 基于当前组合模型产生的新组合,也是本轮最小的组合,即第 i 小组合pq.offer(new CombineModel(c.curSum + nums[c.nextIdx], c.nextIdx + 1));// 当前组合需要更新nextIdx后,重新加入优先队列c.nextIdx += 1;pq.offer(c);}// 取出优先队列中最小组合(注意这里的最小,指的是基于当前组合,将要产生的新组合之和最小)c = pq.poll();}// 经过m-1轮后, 优先队列中存储的最最小组合模型 得出的 新组合是 第m小组合return c.curSum + nums[c.nextIdx];
}// 组合模型
class CombineModel {constructor(curSum, nextIdx) {this.curSum = curSum; // 当前组合之和this.nextIdx = nextIdx; // 将要被加入当前组合的新元素索引位置}
}// 基于堆实现优先队列
class PriorityQueue {constructor(cpr) {this.queue = [];this.size = 0;this.cpr = cpr;}swap(i, j) {let tmp = this.queue[i];this.queue[i] = this.queue[j];this.queue[j] = tmp;}// 上浮swim() {let ch = this.queue.length - 1;while (ch !== 0) {let fa = Math.floor((ch - 1) / 2);const ch_node = this.queue[ch];const fa_node = this.queue[fa];if (this.cpr(ch_node, fa_node) < 0) {this.swap(ch, fa);ch = fa;} else {break;}}}// 下沉sink() {let fa = 0;while (true) {let ch_left = 2 * fa + 1;let ch_right = 2 * fa + 2;let ch_max;let ch_max_node;const fa_node = this.queue[fa];const ch_left_node = this.queue[ch_left];const ch_right_node = this.queue[ch_right];if (ch_left_node && ch_right_node) {// 注意这里应该要>=0,因为左边优先级高if (this.cpr(ch_left_node, ch_right_node) <= 0) {ch_max = ch_left;ch_max_node = ch_left_node;} else {ch_max = ch_right;ch_max_node = ch_right_node;}} else if (ch_left_node && !ch_right_node) {ch_max = ch_left;ch_max_node = ch_left_node;} else if (!ch_left_node && ch_right_node) {ch_max = ch_right;ch_max_node = ch_right_node;} else {break;}// 注意这里应该要>0,因为父优先级高if (this.cpr(ch_max_node, fa_node) < 0) {this.swap(ch_max, fa);fa = ch_max;} else {break;}}}// 向优先队列中加入元素offer(ele) {this.queue.push(ele);this.size++;this.swim();}// 取出最高优先级元素poll() {this.swap(0, this.queue.length - 1);this.size--;const ans = this.queue.pop();this.sink();return ans;}
}
Java算法源码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc = new Scanner(System.in);ArrayList<String> ans = new ArrayList<>();int t = sc.nextInt();for (int i = 1; i <= t; i++) {int n = sc.nextInt();int m = sc.nextInt();int[] nums = new int[n];for (int j = 0; j < n; j++) {nums[j] = sc.nextInt();}ans.add("Case #" + i + ": " + getResult(n, m, nums));}for (String an : ans) {System.out.println(an);}}static class CombineModel {int curSum; // 当前组合之和int nextIdx; // 将要被加入当前组合的新元素索引位置public CombineModel(int curSum, int nextIdx) {this.curSum = curSum;this.nextIdx = nextIdx;}}public static int getResult(int n, int m, int[] nums) {Arrays.sort(nums);// 对于一个组合模型,其”将要产生的新组合“之和越小,则优先级越高// curSum + nums[nextIdx] 为 ”将要产生的新组合“之和PriorityQueue<CombineModel> pq =new PriorityQueue<>((a, b) -> a.curSum + nums[a.nextIdx] - (b.curSum + nums[b.nextIdx]));// 空组合的和为0, 将要加入的新元素是nums[0], 即索引0的元素,其将要产生的新组合之和为 0 + nums[0]CombineModel c = new CombineModel(0, 0);for (int i = 1; i < m; i++) {// c是当前最小组合模型,最小的组合模型指的是将要产生的新组合之和在对应轮次中最小// 如果当前组合模型c还有可合入的下一个元素,即c.nextIdx + 1 < n, 则说明可以基于当前组合模型产生一个新组合if (c.nextIdx + 1 < n) {// 基于当前组合模型产生的新组合,也是本轮最小的组合,即第 i 小组合pq.offer(new CombineModel(c.curSum + nums[c.nextIdx], c.nextIdx + 1));// 当前组合需要更新nextIdx后,重新加入优先队列c.nextIdx += 1;pq.offer(c);}// 取出优先队列中最小组合(注意这里的最小,指的是基于当前组合,将要产生的新组合之和最小)c = pq.poll();}// 经过m-1轮后, 优先队列中存储的最最小组合模型 得出的 新组合是 第m小组合return c.curSum + nums[c.nextIdx];}
}
Python算法源码
import queuedef getResult(n, m, nums):nums.sort()pq = queue.PriorityQueue()class CombineModel:def __init__(self, curSum, nextIdx):self.curSum = curSum # 当前组合之和self.nextIdx = nextIdx # 将要被加入当前组合的新元素索引位置def __lt__(self, other):# 对于一个组合模型,其”将要产生的新组合“之和越小,则优先级越高# curSum + nums[nextIdx] 为 ”将要产生的新组合“之和return self.curSum + nums[self.nextIdx] < (other.curSum + nums[other.nextIdx])# 空组合的和为0, 将要加入的新元素是nums[0], 即索引0的元素,其将要产生的新组合之和为 0 + nums[0]c = CombineModel(0, 0)for _ in range(1, m):# c是当前最小组合模型,最小的组合模型指的是将要产生的新组合之和在对应轮次中最小# 如果当前组合模型c还有可合入的下一个元素,即c.nextIdx + 1 < n, 则说明可以基于当前组合模型产生一个新组合if c.nextIdx + 1 < n:# 基于当前组合模型产生的新组合,也是本轮最小的组合,即第 i 小组合pq.put(CombineModel(c.curSum + nums[c.nextIdx], c.nextIdx + 1))# 当前组合需要更新nextIdx后,重新加入优先队列c.nextIdx += 1pq.put(c)# 取出优先队列中最小组合(注意这里的最小,指的是基于当前组合,将要产生的新组合之和最小)c = pq.get()# 经过m-1轮后, 优先队列中存储的最最小组合模型 得出的 新组合是 第m小组合return c.curSum + nums[c.nextIdx]t = int(input())ans = []for i in range(t):n, m = map(int, input().split())nums = list(map(int, input().split()))ans.append(f"Case #{i+1}: {getResult(n, m, nums)}")for an in ans:print(an)