洛谷P6033 合并果子 加强版 \color{green}{\texttt{洛谷P6033 合并果子 加强版}} 洛谷P6033 合并果子 加强版
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
[Solution] \color{blue}{\texttt{[Solution]}} [Solution]
这题的弱化版相信大家一定都知道——就是洛谷的P1090。它是一道非常经典的贪心(或者说建哈弗曼树)的题目。
可是,这道题的数据范围非常的变态,以至于我们只能用 O ( n ) O(n) O(n) 的算法,连 O ( n × log n ) O(n \times \log n) O(n×logn) 的算法都不行。
一点一点来解决,先看看原题(弱化版)中有哪些地方是 O ( n × log n ) O(n \times \log n) O(n×logn) 的,再来想怎么样把它们变成 O ( n ) O(n) O(n)。
注意,这是一个非常有用的思考方式。先考虑一个比较简单易想的弱化版,再看弱化版中哪些地方耗费了大量的时间,最后优化这些地方,使得整个算法的时间复杂度可以让我们接受。
第一个地方就是排序。我们发现这道题有一个非常好的性质: 1 ≤ a i ≤ 1 × 1 0 5 ( 1 ≤ i ≤ n ) 1 \leq a_i \leq 1 \times 10^5(1 \leq i \leq n) 1≤ai≤1×105(1≤i≤n)。
这有什么用呢?我们可以用桶排来代替快排。所谓的桶排,就是开一个桶 cnt \texttt{cnt} cnt,其中 cnt i \texttt{cnt}_{i} cnti 表示数字 i i i 的出现次数。排序变得非常简单:
for(int i=1;i<=100000;i++)
for(int j=1;j<=cnt[i];j++)
b[++tot]=i;//b数组即排序后数组
第二个地方就是贪心的过程。我们需要用优先队列,而优先队列是 O ( n × log n ) O(n \times \log n) O(n×logn) 的。怎么办办呢?两个方法。
- 换一个数据结构。但这个对于本题来说有点难,至少笔者不会。
- 用其它数据结构代替它。
用什么数据结构来代替它呢?两个队列!
队列 Q 1 Q_1 Q1 初始化为排好序后的 b b b 数组, Q 2 Q_2 Q2 初始化为空。有什么用呢?
首先要取出最小的两个数。这个简单,每次取一个数只需要从 Q 1 Q_1 Q1 和 Q 2 Q_2 Q2 的对头(它们是这个队列中最小的元素),看看谁更小,谁小要谁。
然后是插入元素。我们把它直接放到 Q 2 Q_2 Q2 的尾部。
就这么简单。但是它为什么是对的呢? Q 1 Q_1 Q1 就说是我们人为的排好了序,是有序的, Q 2 Q_2 Q2 也一定是有序的吗?
答案是肯定的,大家可以想想为什么(也可以去搜索一下,真的很简单,这里就不证明了)。
于是我们可以在 O ( n ) O(n) O(n) 的时间复杂度内解决这道题了。
[code] \color{blue}{\texttt{[code]}} [code]
const int N=1e7+100;
const int M=1e5+100;
long long ans;int n;
int a[N],cnt[M];//桶
queue<long long> q1,q2;
inline long long get_first(){
if (q2.empty()||(!q1.empty()&&q1.front()<q2.front()))
{long long x=q1.front();q1.pop();return x;}
else{long long x=q2.front();q2.pop();return x;}
}
int main(){
memset(cnt,0,sizeof(cnt));
n=read();ans=0ll;//init
for(int i=1;i<=n;i++)
cnt[a[i]=read()]++;
for(int i=1;i<=100000;i++)
for(int j=1;j<=cnt[i];j++)
q1.push(i);//O(N)的桶排
for(register int i=1;i<n;i++){
long long x=get_first();
long long y=get_first();
ans+=x+y;q2.push(x+y);
}
printf("%lld",ans);
return 0;
}
read() 函数就是快读函数,这里不给出了。