题解:洛谷 P6033 [NOIP 2004 提高组] 合并果子 加强版

洛谷 P6033 [NOIP 2004 提高组] 合并果子 加强版

题意

给定一个可重集 \(S\),每次可以从中选出两个数 \(x,y\),删除这两个数并且加入 \(x+y\),同时消耗 \(x+y\) 的代价。

\(|S|\le10^7\)\(\forall x\in S,x\le10^5\)

解法

下记 \(n=|S|\)

题目范围提示我们必须使用 \(O(n)\) 的做法。那么使用堆的原版做法是不能实现的了,同时需要快速读入。


由于集合中的值域并不大,所以可以使用计数排序先进行排序,依次从小到大放入队列 \(p\) 中。那么此时 \(p\) 中元素是从小到大递增的。

第一次,我们必然取出 \(p\) 中的前两个元素 \(i,j\)(也就是最小的两个元素),组成 \(i+j\)。但是 \(i+j\) 应该放在哪里呢?如果放回 \(p\) 里面且要求继续保持单调性的话,起码也需要在队列里面二分,这样子就和堆没有什么区别了。

考虑再开一个队列 \(q\),并且把 \(i+j\) 放入 \(q\) 中。同理接下来合并出来的东西也都放入 \(q\) 中。显然我们每次合并得到的数值是越来越大的,那么 \(q\)​ 的单调性也就得以保证。

下具体证明 \(q\) 的单调性。

假设在第 \(i\) 次选择了 \(a,b\),得到了 \(s=a+b\);在第 \(i+1\) 次选择了 \(c,d\),得到了 \(t=c+d\)

根据我们的贪心策略,我们每次选的都是可行中的最小的两个,所以 \(a\le c,d\)\(b\le c,d\),那么 \(s\le t\),故 \(q\) 单调性满足。

现在 \(p,q\) 都是单调的,也就是说小的数都在 \(p,q\) 的前部。那么我们两次都选出 \(p,q\) 队头的较小值,那么必然是最小的两个数。

数组大小

下面分析 \(p,q\) 应该开多长。

容易发现,每一次合并,\(n\) 都会减一,故合并次数为 \(n-1\)

那么 \(p,q\) 开到 \(10^7\) 就可以了。经计算,不会超空间范围。

时间复杂度

时间复杂度为 \(O(n+V)\),可以通过。

代码

需要注意 long long 有没有开全!

/**
 * Problem: P6033 [NOIP 2004 提高组] 合并果子 加强版
 * Author:  OIer_wst
 * Date:    2025-06-22
 */
#include <bits/stdc++.h>
using lint = long long;
const int N = 1e7 + 10, V = 1e5 + 10; // V 表示数字的值域

void read(int &x) { // 快速读入
  x = 0;
  int sgn = 1, ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -sgn;
  for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
  x *= sgn;
}

lint ans;
int n, cnt[N];

int hh1, tt1; lint p[N];
int hh2, tt2; lint q[N];

void init() {
  hh1 = hh2 = 0, tt1 = tt2 = -1;
  for (int i = 0; i < V; ++i)
    while (cnt[i]--) p[++tt1] = i;
}

int main() {
  read(n);
  for (int i = 0, a; i < n; ++i) read(a), ++cnt[a];
  init(); // 计数排序+初始化队列
  for (lint i = 1, x, y; i < n; ++i) { // 合并
    // 取最小数 x
    if (hh1 <= tt1 && (hh2 > tt2 || p[hh1] < q[hh2])) x = p[hh1++];
    else x = q[hh2++];
    // 取次小数 y
    if (hh1 <= tt1 && (hh2 > tt2 || p[hh1] < q[hh2])) y = p[hh1++];
    else y = q[hh2++];
    ans += x + y, q[++tt2] = x + y;
  }
  std::cout << ans << std::endl;
  return 0;
}

总结

这是一个经典的哈夫曼编码问题,而双队列法是针对此类问题在权值与个数有特殊限制时的线性时间优化。

  • 在一般的问题中,我们使用堆来维护最小值,时间复杂度 \(O(n\log n)\),与数字的值域大小无关。
  • \(n\) 比较大但是 \(V\) 比较小时,可以计数排序后,用两个队列模拟堆,时间复杂度 \(O(n+V)\),与数字的值域相关。
posted @ 2025-06-22 11:40  OIer_wst  阅读(18)  评论(0)    收藏  举报