(二叉)堆是以数组储存的完全二叉树,是一种实现优先队列的数据结构。
堆的定义:n个元素要成为堆,当且仅当它的元素k1,k2,……,kn满足关系如下:
ki<=k2*i ki<=k2*i+1 1<=i<=n / 2(整除)
或: ki>=k2*i ki>=k2*i+1 1<=i<=n / 2(整除)
因为堆是以数组储存的完全二叉树,所以它具有完全二叉树的性质。最有用处的就是它是二叉树,查找的时候把时间复杂度降低至O(logk)k为树的深度。我觉得堆最大的用处就在于这里。
堆学习小结:
易混易错:
①上移和下移时优先级别的判断。
solution:规定好i为子节点,i/2为父节点,这样上移和下移的判断条件都是一样的。
②上移时漏写"i /= 2"或者写成"i *= 2“。
③上移时,"i /= 2"写在后面;下移时,"i *= 2写在前。
④注意i的范围,上移时,i>1恒成立;下移时,i * 2 < n恒成立(具体看2016NOIP堆复习中代码写法)
然后一共做了四道题目:黑匣子,最小函数,操作系统和序列合并。
最小函数:每一个函数的当前最小值算出来,并放入堆中。取堆顶,(删除堆顶)然后计算这个函数的下一个函数值。
注意:对于每一个f()函数来说,f(i) < f(j) (i < j),不懂的看题目。
//洛谷,P1085最小函数值
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string.h>
using namespace std;
struct node{
int id,v,num; //num为中点坐标
};
const int MAX_N = 10000 + 4;
node h[MAX_N];
int l[MAX_N],r[MAX_N],a[MAX_N],b[MAX_N],c[MAX_N];
int n;
char ch;
int getnum(){
ch = getchar();
for (; ch < '0' || ch >'9'; ch = getchar());
int ret = 0;
for (; ch >= '0' && ch <= '9'; ch = getchar()) ret = ret * 10 + ch -48;
return ret;
}
void sift_up(int i){
bool done = false;
while (i > 1 && !done){
if (h[i / 2].v > h[i].v) swap(h[i],h[i/2]);
else done = true;
i /= 2;
}
}
void sift_down(int i){
bool done = false;
while (i * 2 <= n && !done){
i *= 2;
if (i + 1 <= n && h[i].v > h[i + 1].v) i ++;
if (h[i / 2].v > h[i].v) swap(h[i],h[i/2]);
}
}
void Delete_top(){
h[1]=h[n]; n--; sift_down(1);
}
void Insert(int id, int v,int num){
n ++; h[n].id = id; h[n].v = v; h[n].num = num;
sift_up(n);
}
int main(){
freopen("lg2085.in","r",stdin);
freopen("lg2085.out","w",stdout);
double w1,w2;
int m,i,x,x1,x2,temp,temp1;
cin >> n >> m;
for (i = 1; i <= n; i ++){
a[i] = getnum(); b[i] = getnum(); c[i] = getnum();
w1=a[i]; w2=b[i];
h[i].num = round(- (w2 / (2 * w1))); if (h[i].num <= 0) h[i].num = 1; h[i].id = i;
x = h[i].num; h[i].v = a[i] * x * x + b[i] * x + c[i];
}
memset(l,0,sizeof(l)); memset(r,0,sizeof(r));
for (i = n/2; i >= 1; i--) sift_down(i);
for (i = 1; i <= m; i ++){
cout <<h[1].v << ' ';
x = h[1].num - l[h[1].id] - 1; if (x <= 0) x1 = 999999999; else x1 = a[h[1].id] * x * x + b[h[1].id] * x + c[h[1].id];
x = h[1].num + r[h[1].id] + 1; if (x <= 0) x2 = 999999999; else x2 = a[h[1].id] * x * x + b[h[1].id] * x + c[h[1].id];
if (x1 < x2){
temp = h[1].id; temp1 = h[1].num; Delete_top();
Insert(temp, x1, temp1); l[temp]++;
}
else{
temp = h[1].id; temp1 = h[1].num; Delete_top();
Insert(temp, x2, temp1); r[temp]++;
}
}
fclose(stdin); fclose(stdout);
return 0;
}
序列合并:类似于最小函数,以第一列数做基准,第二列数从小到大排好(好吧,题目给出的数列是单调不减的)。然后第一列的每一个数加上第二列的第一个数后把这个值加入堆顶。(建最小堆)建立好堆之后,每一次操作取堆顶,即最小值,同时把这个值取出来(删除堆顶),做相应更新——即与第二列的对于当前加的数下一个数相加。
注意:没什么好注意的。。。。不过这一题我用了STL来做,发现程序却是简洁了不少!其他的都是手打堆。。。。
//洛谷 P1631序列合并
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
const int MAX_N = 100000 + 24;
int a[MAX_N],b[MAX_N],n;
char ch;
class node{
public:
int ida, idb, v;
node(int a, int b, int c): ida(a), idb(b), v(c){
}
};
bool operator < (const node &t1, const node &t2){
return t1.v > t2.v;
}
int getnum(){
ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar());
int ret = 0;
for (; ch >= '0' && ch <= '9'; ch = getchar()) ret = ret * 10 + ch - 48;
return ret;
}
int main(){
freopen("lg1631.in","r",stdin); freopen("lg1631.out","w",stdout);
int i;
n = getnum();
for (i = 1; i <= n; i++) a[i] = getnum();
for (i = 1; i <= n; i++) b[i] = getnum();
priority_queue<node> q; //node temp;
for (i = 1; i <= n; i++)
q.push(node(i,1,a[i]+b[1]));
for (i = 1; i <= n; i++){
node temp = q.top();
cout << temp.v << ' ';
temp.idb = temp.idb + 1;
temp.v = a[temp.ida] + b[temp.idb];
q.pop(); q.push(temp);
}
fclose(stdin); fclose(stdout);
return 0;
}
操作系统:按时间线进行模拟每一秒当前哪个程序可运行。实现可以用优先队列(堆),因为程序具有明显的优先级别区分。
注意:按时间线来执行,申清楚题目后你就懂我为什么这么提示你了
//洛谷 P2278操作系统
#include<iostream>
#include<cstdio>
using namespace std;
char ch;
const int MAX_N = 1500000 + 4;
int b[MAX_N],c[MAX_N],d[MAX_N],h[MAX_N],n; //b是到达时间,c是执行时间,d是优先级
int getnum(){
ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) if (ch == -1) return 0;
int ret = 0;
for (; ch >= '0' && ch <= '9'; ch = getchar()) ret = ret * 10 + ch -48;
return ret;
}
void sift_up(int i){
bool done = false; int fa,son;
while (i > 1 && ! done){
fa = h[i/2]; son = h[i];
if (d[fa] < d[son] || d[fa] == d[son] && b[fa] > b[son]) swap(h[i], h[i/2]);
i /= 2;
}
}
void sift_down(int i){
bool done = false; int fa, son;
while (i*2 <= n && ! done){
i *= 2;
if (i + 1 <= n && (d[h[i]] < d[h[i+1]] || d[h[i]] == d[h[i+1]] && b[h[i]] > b[h[i+1]])) i++;
fa = h[i/2]; son = h[i];
if (d[fa] < d[son] || d[fa] == d[son] && b[fa] > b[son]) swap(h[i], h[i/2]);
}
}
void Insert(int num){
n ++; h[n] = num;
sift_up(n);
}
void delete_top(){
h[1] = h[n]; h[n] = 0; n --; sift_down(1);
}
int main(){
freopen("lg2278.in","r",stdin);
freopen("lg2278.out","w",stdout);
int num,i,time,a,j;
a = getnum(); num = 0;
while (a != 0){
b[a] = getnum(); c[a] = getnum(); d[a] = getnum();
if (a > num) num = a;
a = getnum();
}
time = 1; i = 1; j = 1;
while (j <= num){
while (c[h[1]] != 0 || h[1] == 0) {
while (b[i] <= time && b[i] != 0){
Insert(i); i++;
}
time ++; if (h[1] != 0) c[h[1]] --;
}
cout << h[1] << ' ' << time <<endl;
delete_top(); j++;
}
cout << endl;
fclose(stdin); fclose(stdout);
return 0;
}
黑匣子:(主角总是最后登场的)这四道题目里面我认为就是黑匣子666666,让我记忆犹新。有插入操作,询问操作,明显就是优先队列啊!可是,最坑的就是询问的不是最值,而是第k小的值。不过还好,其中还是有一些巧妙的东西的。
整理题目意思,发现主要操作是针对于一个队列,插入一个元素,或者询问第k小的元素。而且询问还暗藏玄机,①给出信息是当插入到第几个数时,当前队列的第k小②第一次询问问第1小,第二次是问第2小……第n次是问第n小。
由于是问第k小,这个就很让人抓狂。
如果每次都是问最小的,又会插入数,那么很明显,可以用最小堆来干这活儿。因为每次插入的时间复杂度为O(logn)。
而现实是残酷的,要查找第k小。一开始我想要不要保持整个队列,询问第k小的时候再一次次删除堆顶(k-1次),最后堆顶的就是第k小。不过这样询问的k变大了后会明显增加时间复杂度。不过也比其他的要好了。而后来,脑海里蹦出来一个想法,可不可以建立两个堆,一个最大堆,一个最小堆。虽然只是胡思乱想出来的,没有什么实际根据,但似乎有点可行。
add:①当最大堆的元素数目小于k时,直接加入最大堆。
②当最大堆的元素数目大于等于k时,判断加入的数是否小于最大堆堆顶元素,是就替换它,把替换出来的元素放到最小堆里;反之则加入到最小堆。
perfect!
//洛谷 P1801黑匣子
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXNUM = 200100;
int h1[MAXNUM],h2[MAXNUM],n,m,n1,n2,a[MAXNUM],b[MAXNUM],now,j;
void sift_up_1(int i){ //1是最大堆,2是最小堆
bool done=false;
while (i>1 && !done){
if (h1[i/2]<h1[i]) swap(h1[i/2],h1[i]);
else done =true;
i/=2;
}
}
void sift_up_2(int i){
bool done=false;
while (i>1 && !done){
if (h2[i/2]>h2[i]) swap(h2[i/2],h2[i]);
else done =true;
i/=2;
}
}
void sift_down_1(int i){
bool done=false;
while (i*2<=n1 && !done){
i*=2;
if (i+1 <= n1 && h1[i]<h1[i+1]) i++;
if (h1[i/2]<h1[i]) swap(h1[i/2],h1[i]);
else done = true;
}
}
void sift_down_2(int i){
bool done=false;
while (i*2<=n2 && !done){
i*=2;
if (i+1 <= n2 && h2[i]>h2[i+1]) i++;
if (h2[i/2]>h2[i]) swap(h2[i/2],h2[i]);
else done = true;
}
}
void Insert_1(int num){
n1++; h1[n1]=num;
sift_up_1(n1);
}
void Insert_2(int num){
n2++; h2[n2]=num;
sift_up_2(n2);
}
void Delete_top_1(){
h1[1]=h1[n1]; n1--;
sift_down_1(1);
}
void Delete_top_2(){
h2[1]=h2[n2]; n2--;
sift_down_2(1);
}
int main(){
freopen("lg1081.in","r",stdin);
freopen("lg1081.out","w",stdout);
int i,temp;
cin >>m >>n;
for (i=1; i<=m; i++) cin >>a[i];
for (i=1; i<=n; i++) cin >>b[i];
n1=n2=0; j=1;
for (i=1; i<=m; i++){
if (n1<j) Insert_1(a[i]);
else
if (h1[1]>a[i]){
temp=h1[1];
h1[1]=a[i]; sift_down_1(1);
Insert_2(temp);
}
else Insert_2(a[i]);
while (b[j]==i) {
cout <<h1[1] <<endl; j++;
if (n2==0) break;
Insert_1(h2[1]);
Delete_top_2();
}
}
fclose(stdin); fclose(stdout);
return 0;
}
就是这样,堆得学习还算可以吧。