这次我们通过两道例题来总结一下优先队列的用法和实现:
目录:
【BHOJ 1512】女娲加农炮
核心:贪心 + 优先队列
URL:【BHOJ 1512】女娲加农炮
时间限制: 3000 ms 内存限制: 131072 kb
总通过人数: 96 总提交人数: 172
题目简述
已知有N种不同的原子核,第i种原子核的重量为a[i]。
将两种原子核聚合在一起,消耗的能量等于两种原子核的重量之和。在n-1次聚合之后,聚合完成。求出最小能量消耗值。
输入
多组数据输入,第一个数为原子核的种类N。(N <= 1e6)
接下来N个整数,代表N种原子核的重量。(在int范围内并用空格隔开)
输出
对于每组数据,输出一行,为聚合过程能量消耗的最小值。(保证结果在int范围内)
输入样例
4
1 2 3 4
输出样例
19
分析
这道题是典型的贪心 + 优先队列,主要的思路是每次都选当前待聚合原子核中最轻的两个来聚合,直到聚合得到一个唯一的原子核,期间累加能量消耗,最后输出即可。
还有一个要注意的小细节就是求和的时候要用 long long。
代码
首先是 STL 版:
#include <iostream>
#include <vector>
#include <queue>
#define PC putchar
#define LL long long
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
void PRT(const LL a){if(a>=10)PRT(a/10);putchar(a%10+48);}
std::priority_queue<LL, std::vector<LL>, std::greater<LL>> pq;
int main()
{
int n;
while (1)
{
se(n)
LL ans = 0, sum;
while (n--)
{
int tp;
sc(tp)
pq.push(tp);
}
while (pq.size() >= 2) // 至少还有两个,仍然需要继续聚合
{
sum = pq.top(); // 每次拿两个最小的出来
pq.pop();
sum += pq.top();
pq.pop();
ans += sum;
pq.push(sum);
}
PRT(ans), PC(10);
while (pq.size()) // 清空优先队列
pq.pop();
}
}
然后是通过最小堆实现的自定义优先队列:
为了提高效率:
- 建立了哨兵,节省了进行上浮操作时对下标进行判断的时间(push的时候,如果加入的元素比当前堆内所有有效元素都小,那么就会自动在哨兵处停下,不必担心不判断下标而陷入死循环)
- 使用数组一次性开辟足够的空间而不是多次动态申请空间
- 使用了宏函数而不是通用函数去定义下沉操作函数(消除频繁调用函数的开销)
- 当使用C++原生类型的时候可以不使用引用类型以加快速度(用 typedef会报错构造函数,只好用#define)
具体代码:
#include <iostream>
#define PC putchar
#define LL long long
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
void PRT(const LL a){if(a>=10)PRT(a/10);putchar(a%10+48);}
template <class Type, int MN> // 小顶堆
class KPQ
{
#define ChosenType Type // 用原生类型还是自定义类
#define CMP < // >就变成大顶堆
#define perc_down(root) int p = root; \
for (int c, E=cnt>>1; p<=E; p=c) \
{ \
c = p<<1; \
if (c!=cnt && heap[c+1] CMP heap[c]) \
c++; \
if (heap[c] CMP tp) \
heap[p] = heap[c]; \
else break; \
} \
heap[p] = tp
public:
Type heap[MN];
int cnt;
public:
KPQ(const ChosenType MIN_SENTRY) : cnt(0)
{
*heap = MIN_SENTRY;
}
void push(const ChosenType data)
{
register int i = ++cnt;
for (; data CMP heap[i>>1]; i>>=1)
heap[i] = heap[i>>1];
heap[i] = data;
}
void pop(void)
{
const ChosenType tp = heap[cnt--];
perc_down(1);
}
inline const ChosenType top(void)
{
return heap[1];
}
inline bool empty(void)
{
return !cnt;
}
inline int size(void)
{
return cnt;
}
inline void clear(void)
{
cnt = 0;
}
void build(int n)
{
cnt = n;
// for i:[1,n] scan
for (int i=1; i<=n; i++)
sc(heap[i])
for (int i=n>>1; i>0; i--)
{
Type tp = heap[i];
perc_down(i);
}
}
void erase(const ChosenType data)
{
for (int i=1; i<=cnt; i++)
{
if (heap[i] == data)
{
heap[i] = heap[cnt--];
const ChosenType tp = heap[cnt+1];
perc_down(i);
return;
}
}
}
};
KPQ<LL, 1000007> pq(-1);
int main()
{
int n;
while (1)
{
se(n)
LL ans = 0, sum;
pq.build(n);
while (pq.size() >= 2) // 至少还有两个,仍然需要继续聚合
{
sum = pq.top(); // 每次拿两个最小的出来
pq.pop();
sum += pq.top();
pq.pop();
ans += sum;
pq.push(sum);
}
PRT(ans), PC(10);
pq.clear(); // 清空优先队列
}
}
【BHOJ 1517】女娲加农炮II
核心:贪心 + 优先队列
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 70 总提交人数: 77
题目简述
已知有N种不同的原子核(这里请忽略自然界原子核的种类上限),第i种原子核的重量为a[i]。
将三种原子核聚合在一起,消耗的能量等于三种原子核的重量之和。在经过多次聚合之后,聚合完成。(如果原子核的数量小于3则不进行进一步的聚合)芸如想使整个聚合过程消耗的能量最小,请求出这个最小的能量值。
输入
多组数据输入,第一个数为原子核的种类N。(N <= 1e6)
接下来N个整数,代表N种原子核的重量。(在int范围内并用空格隔开)
输出
对于每组数据,输出一行,为聚合过程能量消耗的最小值。(保证结果在int范围内)
输入样例
4
1 2 3 4
输出样例
6
分析
这道题女娲加农炮 I 的思路和解题方法都是一样的,也是朴素的 贪心 + 优先队列,不过是每次合并三个原子核。
代码
#include <iostream>
#include <algorithm>
#define PC putchar
#define LL long long
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
void PRT(const LL a){if(a>=10)PRT(a/10);putchar(a%10+48);}
template <class Type, int MN> // 小顶堆
class KPQ
{
#define ChosenType Type
#define CMP < // >就变成大顶堆
#define perc_down(root) int p = root; \
for (int c, E=cnt>>1; p<=E; p=c) \
{ \
c = p<<1; \
if (c!=cnt && heap[c+1] CMP heap[c]) \
c++; \
if (heap[c] CMP tp) \
heap[p] = heap[c]; \
else break; \
} \
heap[p] = tp
public:
Type heap[MN];
int cnt;
public:
KPQ(const ChosenType MIN_SENTRY) : cnt(0)
{
*heap = MIN_SENTRY;
}
void push(const ChosenType data)
{
register int i = ++cnt;
for (; data CMP heap[i>>1]; i>>=1)
heap[i] = heap[i>>1];
heap[i] = data;
}
void pop(void)
{
const ChosenType tp = heap[cnt--];
perc_down(1);
}
inline const ChosenType top(void)
{
return heap[1];
}
inline bool empty(void)
{
return !cnt;
}
inline int size(void)
{
return cnt;
}
inline void clear(void)
{
cnt = 0;
}
void build(int n)
{
cnt = n;
// for i:[1,n] scan
for (int i=1; i<=n; i++)
sc(heap[i])
for (int i=n>>1; i>0; i--)
{
const Type tp = heap[i];
perc_down(i);
}
}
void erase(const ChosenType data)
{
for (int i=1; i<=cnt; i++)
{
if (heap[i] == data)
{
heap[i] = heap[cnt--];
const ChosenType tp = heap[cnt+1];
perc_down(i);
return;
}
}
}
};
KPQ<LL, 1000007> pq(-1);
int main()
{
int n;
while (1)
{
se(n)
LL ans = 0, sum;
pq.build(n);
while (pq.size() >= 3) // 至少还有三个,仍然需要继续聚合
{
sum = pq.top(); // 每次拿三个最小的出来
pq.pop();
sum += pq.top();
pq.pop();
sum += pq.top();
pq.pop();
ans += sum;
pq.push(sum);
}
PRT(ans), PC(10);
pq.clear(); // 清空优先队列
}
}
当然优先队列也可以通过平衡树实现,不过用堆实现的优先队列已经有比较好的性能了,从实际角度考虑还是选用堆。