HDU - 4546 比赛难度(Java & JS & Python)

题目来源

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 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 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 queue


def 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 += 1
            pq.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)

你可能感兴趣的:(算法与数据结构,算法,HDU-4546,Java,JavaScript,Python)