最近一直在学图论,然后吧,由于学的东西实在是太多太杂了,加上蒟蒻本蒻又经常颓,所以落了好多好多板子题的整理没写啊嘤嘤嘤,不过把这些东西学的差不多了,再一块写个整理,其实感觉还不错?????也算是很神奇吧,大概就是知识的积淀这一块有了一点用
好了,话不多说,我们来进入正题
P3378 【模板】堆
我们从板子题入手,慢慢的了解堆(其实是自己巩固而已QWQ)
这里我们学习的堆其实是二叉堆,算是比较狭义的一种定义吧,首先,
堆是一种特殊的二叉树,而且是完全二叉树
为什么我们接触到的比较早的数据结构是堆呢?
其实有这么几个原因:
1.因为堆是一颗完全二叉树,所以在父亲和儿子方面,就完全不需要结构体存树,而是严格按照完全二叉树的定义来就可以
一棵树上的一个节点i,他的儿子是i*2和i*2+1,他的父亲是i/2(此处为整除)
所以处理的时候就很方便啊。
2.c++自带的STL对于堆非常友好,基本上随便写几个STL就能构建一个堆,即使是手写,大约时间也就是十分钟左右,并不是太慢。
3.堆的应用不少,比如堆排序,以及了解一种数据结构,为将来学习可并堆,斐波那契堆打下坚实基础,而且能优化dijkstra(图论).
下面来讲一讲堆的一些基本操作步骤
插入(大雾
我们要对堆插入一个元素,但是并不是扔进去,让尾指针++就完事了,我们还得对堆的正确性进行维护。
来说一下思想,对于添加进来的元素,设其位置为now,那么他爹就是now/2(now>>1 位运算更快哦)
只要比较二者大小,如果新元素更小,那么就交换即可,否则意味着合法,我们直接退出循环就可以,循环的终止条件就是now!=1(因为当now==1时,它已经是堆首元素,没爹。。。。多苦的一孩子(大雾)
来看代码
inline void add(int x)
{
Heap[++cnt] = x;//这里小小的压了一下行
int now = cnt;
while (now!=1)
{
if (Heap[now] < Heap[now >> 1])
swap(Heap[now], Heap[now >> 1]),now>>=1;
else
break;
}
}
弹出
这个也要分为两种,一种是弹出堆首元素,另一种是弹出任意位置的元素(这种一般与寻找元素相结合,考察对DFS,BFS之类的搜索方法的能力)
先看弹出堆首元素,
想要弹出的话,我们就把最小值修改为INF(我比较喜欢1e9),然后和之前相反,向下比较直到找到合适为止为止。
具体讲一讲
因为小根堆的要求是所有根节点都得比他孩子小,所以我们定义首节点的位置为root=1,因为已经置成INF了,所以我们向下开始比较;
先让两个孩子比,最小的那个再和root比,如果比root小,那么就交换二者,否则符合条件就直接退出循环,这里的循环终止条件是 root << 1 <= cnt,也就是说root的儿子已经比当前的堆的长度大了,也就是不存在儿子了
但是这种方法其实不是很好啊,因为你排到最后,最底下就一大堆INF,难看的要死还占空间,倒不如直接交换首元素和尾元素,然后直接把尾指针减一就可以,这样的话最小值就被删除了,之后进行一下动态维护就可以。
来看代码
inline void pop() { Heap[1] = Heap[cnt--]; int root = 1; while (root << 1 <= cnt) { int son; if ((root << 1) + 1 > cnt || Heap[root << 1] < Heap[(root << 1) + 1]) { son = root << 1; } else son = (root << 1) + 1; if (Heap[son] > Heap[root]) break; swap(Heap[root], Heap[son]); root = son; } }
输出堆首元素
这东西其实没啥好讲的,因为堆首元素就肯定是Heap[1]嘛,知道就行,然后就可以输出了,因为不对堆中元素进行移动和修改,是不影响堆的合法性的。
这些都看的差不多了,就直接略微修改,板子题就切掉啦
#include <iostream> #include <queue> #include <algorithm> #include <cstdio> using namespace std; const int INF = 214748367; int n, a, b, Heap[1000001], cnt = 0; inline void add(int x) { Heap[++cnt] = x; int now = cnt; while (now!=1) { if (Heap[now] < Heap[now >> 1]) swap(Heap[now], Heap[now >> 1]),now>>=1; else break; } } inline void print() { printf("%d ", Heap[1]); } inline void pop() { Heap[1] = Heap[cnt--]; int root = 1; while (root << 1 <= cnt) { int son; if ((root << 1) + 1 > cnt || Heap[root << 1] < Heap[(root << 1) + 1]) { son = root << 1; } else son = (root << 1) + 1; if (Heap[son] > Heap[root]) break; swap(Heap[root], Heap[son]); root = son; } } int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &a); if (a == 1) { scanf("%d", &b); add(b); } if (a == 2) print(); if (a == 3) pop(); } return 0; }
还是肥肠感谢gh神仙的热心帮忙啊,,,,QWQ一个板子题交了三四遍才过,真的是太辣鸡了
看完板子题,我们来看看合并果子这个题
P1090 合并果子
这个题在学完堆之后就好做多了,不过蒟蒻我很早以前做的时候是用dp加快排做的,那叫一个惨不忍睹啊,,,,,,,,连个样例都没过,所以干脆也就没有提交记录了
但是我们要分析分析,为什么这种算法会TLE
思考一下,我们对于,每一堆果子的个数进行排序,这样的话,先合并前两个果子,看似没什么问题,但是当你合并完了之后,你又得干啥呢???
我当时就傻乎乎的又用sort,然后就连本地编译都会超时,我们分析一下时间复杂度,其实就是O(nlongn+(n-1)log(n-1)+(n-2)log(n-2)+.....+log 1)
这样的话就肥肠慢了啊,因为每一次快排都是遍历了所有的数,但是其实很大一部分都是有序的,所以快排也没什么用,反而浪费时间。
那么用堆解决就没有任何问题了
每次合并前两个小的集合,然后把合并完的集合加到ans中,再扔回堆进行维护,这道题就做完啦
上代码
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; int n,ans,Heap[10010],num,cnt=0,sum; inline void add(int x) { Heap[++cnt] = x; int now = cnt; while (now!=1) { if (Heap[now] < Heap[now >> 1]) swap(Heap[now], Heap[now >> 1]),now>>=1; else break; } } inline void pop() { Heap[1] = Heap[cnt--]; int root = 1; while (root << 1 <= cnt) { int son; if ((root << 1) + 1 > cnt || Heap[root << 1] < Heap[(root << 1) + 1]) { son = root << 1; } else son = (root << 1) + 1; if (Heap[son] > Heap[root]) break; swap(Heap[root], Heap[son]); root = son; } } int main() { scanf("%d",&n); for(int i=1;i<=n;++i) { scanf("%d",&num); add(num); } while(cnt>=2) { sum+=Heap[1]; pop(); sum+=Heap[1]; pop(); ans+=sum; add(sum); sum=0; } printf("%d",ans); return 0; }