转载自: http://blog.youkuaiyun.com/sinat_29278271/article/details/47291659
题目的大意是维护一个能随时返回中位数的栈,这个问题其实可以直接简化为维护一个能返回中位数同时支持插入和删除的数据结构。
因为题目中说明每个数据都在[1,100000]之间,所以很朴素的一种解法就是设立一个Count[100005]。插入n的
时候Count[n]++,删除的时候Count[n]--,查找的时候遍历数组,寻找前缀和为(size+1)/2的下标。但是10^5
本身是个比较大的数字,在多次查找之后果断超时了。
1.树状数组+二分查找
树状数组(Binary Indexed Tree(BIT))是一种能高效查找前缀和的数据结构。使用树状数组是为了能进行二分查找,原先遍历Count数组,最多的时候能遍历10^5次,运用二分查找可以将查找次数优化为lg(10^5)/lg(2) < 15
- # include <cstdio>
- # include <stack>
- using namespace std;
- class BIT
- {
- private:
- int *Elem;
- int Size;
- int lowbit(int n)
- {
- return n&(-n);
- }
- public:
- BIT(int size):Size(size+1)
- {
- Elem = new int[Size];
- for (int i=0;i<Size;i++)
- Elem[i] = 0;
- }
- int GetSum(int right)
- {
- int sum = 0;
- while (right)
- {
- sum += Elem[right];
- right -= lowbit(right);
- }
- return sum;
- }
- int GetSum(int left,int right)
- {
- return GetSum(left-1) - GetSum(right);
- }
- void Add(int value,int index)
- {
- while (index < Size)
- {
- Elem[index] += value;
- index += lowbit(index);
- }
- }
- ~BIT()
- {
- delete[] Elem;
- }
- };
- BIT bit(100000);
- int getmid(int size)
- {
- int index = (size+1)/2;
- int left = 1,right = 100000,mid;
- while(left<right)
- {
- mid = (left+right)/2;
- if(bit.GetSum(mid)<index)
- left = mid+1;
- else
- right = mid;
- }
- return left;
- }
- int main()
- {
- int n,tmp;
- scanf("%d",&n);
- stack<int> s;
- char str[10];
- while (n--)
- {
- scanf("%s",str);
- switch(str[1])
- {
- case 'e':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- printf("%d\n",getmid(s.size()));
- break;
- }
- case 'o':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- {
- tmp = s.top();s.pop();
- printf("%d\n",tmp);
- bit.Add(-1,tmp);
- }
- break;
- }
- case 'u':
- {
- scanf("%d",&tmp);s.push(tmp);
- bit.Add(1,tmp);
- }
- break;
- }
- }
- return 0;
- }
2.分桶法(分治,分层HASH,平方分割)
分桶法的基本思路是分治,在一开始的暴力解法中,我们可以认为Count数组是一个大的桶,这个大的桶里有5*10^5个小桶,每个小桶能装一个数,在分桶法中,我们建立多个大桶,每个桶中又有小桶,比如,我们建立多个500个大桶,每个桶的容量是100,同时记录每个大桶中存放的数据的个数,在查找的时候我们可以通过每个大桶中元素的快速定位到放置中位数的那个小桶。当然你可以认为这是一种HASH,hash(key) = key/10。设每个大桶中含有k个小桶,共有m个大桶,m*k = n为定值。则一开始我们需要遍历大小为m的大桶数组,后来要遍历大小为k的单个大桶,时间复杂度为O(max(k,m))在n*k为定值的情况下,易知m = k = (m*k)^(1/2)的时候效率最高为n^(1/2)。
本题中为了方便,采用分层hash的策略,将值为key的元素放入bucke[k/100][k%100]中。
- # include <cstdio>
- # include <stack>
- using namespace std;
-
- const int _size = 100000;
- const int capi = 500;
- int bucket[_size/capi][capi];
- int count[_size/capi];
- int getmid(int size)
- {
- int ind = (size+1)/2,cnt=0,i,j;
- for (i=0;i<_size/capi;i++)
- {
- if (cnt + count[i]>=ind)
- break;
- cnt += count[i];
- }
- for (j=0;j<capi;j++)
- {
- cnt += bucket[i][j];
- if (cnt>=ind)
- return j+i*capi;
- }
-
- }
- char str[10];
- int main()
- {
- int n,tmp;
- scanf("%d",&n);
- stack<int> s;
- while (n--)
- {
- scanf("%s",str);
- switch(str[1])
- {
- case 'e':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- printf("%d\n",getmid(s.size())+1);
- break;
- }
- case 'o':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- {
- tmp = s.top();s.pop();
- printf("%d\n",tmp);
- tmp--;
- bucket[tmp/capi][tmp%capi]--;
- count[tmp/capi]--;
- }
- break;
- }
- case 'u':
- {
- scanf("%d",&tmp);s.push(tmp);
- tmp--;
- bucket[tmp/capi][tmp%capi]++;
- count[tmp/capi]++;
- }
- break;
- }
- }
- return 0;
- }
-
1.这个方法和树状数组+二分的方法并无矛盾,你同样可以用树状数组优化大桶元素的前缀和。
2.还有就是如果你乐意你完全可以多分几个层玩,比如key放在bucket[...][...][...]分层分多了以后,你会发现这个桶变成了一棵树,如果你分层的依据是二分法你出了一棵线段树。
3.如果数据范围增大,你可以修改hash使其映射到更小的空间,同时将每个大桶改为vector<int>数组,查询是对每个vector<int>中的元素排序,个人感觉不会很慢
3.线段树(分治)有种杀鸡用牛刀的感觉
线段树是个霸气的数据结构,基本上包含了分桶法和树状数组的全部功能。线段树的基础思想是分治,但是时间
复杂度上比分桶法更加高效,能将时间优化到O(lgn)然而在PAT的小数据之下,普通的线段树因为常数上的差距花
费的时间更长。具体的树的创建我就不说了,这里采用一点zkw线段树的思想,直接找到树的叶子自底向上走到树根,每个节点维护一个Cnt记录经过这里的路径的个数,查找中位数的时候根据每个节点的Cnt进入合适的子树进行查找。
- # include <cstdio>
- # include <stack>
- using namespace std;
-
- typedef int Node;
- class zkw_segtree
- {
- private:
- Node *T;
- int size;
- public:
- zkw_segtree(int range)
- {
- for (size = 1;size < range+2;size<<=1);
- T = new Node[2*size];
- for (int i=1;i<size+size;i++)
- T[i] = 0;
- }
- void Add(int value,int i)
- {
- for (i+=size;i;i>>=1)
- T[i] += value;
- }
- int Query(int s,int t)
- {
- int ret = 0;
- for (s+=size-1,t+=size+1;s^t^1;s>>=1,t>>=1)
- {
- if (~s^1) ret += T[s^1];
- if (t^1) ret += T[t^1];
- }
- return ret;
- }
- int Find_Kth(int k,int root = 1)
- {
- while (root<<1 < size<<1)
- {
- if (T[root<<1]>=k) root = root<<1;
- else
- {
- k -= T[root<<1];
- root = (root<<1) + 1;
- }
- }
- return root - size;
- }
- ~zkw_segtree()
- {
- delete[] T;
- }
- };
- zkw_segtree segtree(100000);
- int main()
- {
- int n,tmp;
- scanf("%d",&n);
- stack<int> s;
- char str[10];
- while (n--)
- {
- scanf("%s",str);
- switch(str[1])
- {
- case 'e':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- printf("%d\n",segtree.Find_Kth((s.size()+1)/2));
- break;
- }
- case 'o':
- {
- if (s.empty())
- printf("Invalid\n");
- else
- {
- tmp = s.top();s.pop();
- printf("%d\n",tmp);
- segtree.Add(-1,tmp);
- }
- break;
- }
- case 'u':
- {
- scanf("%d",&tmp);s.push(tmp);
- segtree.Add(1,tmp);
- }
- break;
- }
- }
- return 0;
- }
4.Prioriry Queue On Multiset(红黑树是支持插入与删除的堆)真正的牛刀
讲了那么多终于讲到最终的解法了,前三种方法归根结底是利用了输入数据范围有限这一条件,要想完美解决这一问题,我们不得不借助插入查找删除效率都为O(lgn)的高级搜索树。有一种利用堆查找第K个元素的算法,维持一个大顶堆,一个小顶堆,将K个元素压入小顶堆,其余压入大顶堆,随后如果大顶堆的堆顶元素大于小顶堆的堆顶元素,就将将两个堆得元素弹出并压入另一个堆中,直到大顶堆的堆顶元素小于小顶堆的堆顶元素。这样小顶堆的堆顶元素就是第K个元素。
然而对于这道题,这种算法并没有什么用处,这道题要求随时删除某一个元素,然而堆并不具备这种功能(废话,能随意删除某个元素还叫堆吗)。。
犹记得当初学数据结构的时候,是先教的高级搜索树,后教的优先队列。老师在教优先队列的时候,为了引出堆,说,其实想要实现优先队列,用高级搜索树是完全可以的,但高级搜索树实在是太强大了,实现也比较复杂,我们需要一种更简单高效的数据结构去实现优先队列。
于是传说中的堆诞生了,因为堆在结构上的特点,我们也无法做到随意删除其中的某个元素。
然而此时我们需要一个能删除任意元素的优先队列,OK,是时候启动最终形态了,优先级队列超进化,红黑优先级队列。以上文字过于中二,请谨慎阅读。
从时间复杂度上分析,插入查找和删除都是O(lgn)这里的n是指总元素的个数。
然后,估计大家都知道要怎么做了,下面是代码,为了简单起见,并没有扩展出寻找第K个数的功能,而是直接寻找中位数。
以下是代码
5. 平衡搜索树——Treap(我觉得从前维护两个set求中间值是牛刀,那时我还小)
Treap就是其中一个东西,不过主标题是平衡搜索树,就是说这道题可以使用平衡搜索树来实现,而treap只是其中一种方法。
我们先考虑不是平衡搜索树,而是一棵普通搜索树的情况,对于每棵树我们记录这个树中总共有几个节点。
现在记lsize是左子树的规模,rsize是右子树的规模。
当我们需要查找第K小的数时
如果K<=lsize,说明我们要到左子树中去查找第K大的数。
如果K>lsize+1,说明我们要到右子树中查找第第K-(lsize+1)的数
如果前两项不符合,说明当前节点就是我们需要寻找的第K小值,直接返回结果
然后解释为什么要使用平衡树,不用平衡树查找时间会很长,虽然我没有试过会不会超时。
最后安利一下Treap这个数据结构,毫无疑问是最好写的平衡二叉树,没有之一。我至今不会写红黑书,AVL树写了两次,差点把我写哭出来。但是Treap能在需要的时候随手敲出一个来。
以下是代码,虽然这段代码看起来很长,但是前头那个名片可以删除,头文件可以删除一部分,正文部分比较长是因为我出于效率考虑在单个节点中添加cnt记录重复节点的个数,这让代码变得很长,不这样做应该也是可以过的。
PS:习惯使用cin,cout的同学请小心,这道题目如果不取消cin,cout和标准输入输出流的同步,输入输出起码能用掉80ms,不要问我是怎么知道的。
-
-
-
-
-
-
- # include <cstdio>
- # include <cstring>
- # include <cmath>
- # include <cstdlib>
- # include <climits>
- # include <iostream>
- # include <iomanip>
- # include <set>
- # include <map>
- # include <vector>
- # include <stack>
- # include <queue>
- # include <algorithm>
- using namespace std;
-
- const int debug = 1;
- const int size = 5000 + 10;
- typedef long long ll;
-
- struct Treap_Node{
- int value;
- int fix,cnt,size;
- Treap_Node *left,*right;
- Treap_Node():cnt(0),size(0),left(NULL),right(NULL){}
- Treap_Node(int _value):value(_value),cnt(0),size(0),left(NULL),right(NULL){}
- }*root = NULL;
- inline void Treap_SetSize(Treap_Node *&P){
- if (P){
- P->size = P->cnt;
- if (P->left) P->size += P->left->size;
- if (P->right) P->size += P->right->size;
- }
- }
- inline int lsize(Treap_Node *&P){
- return P->left?P->left->size:0;
- }
- inline int rsize(Treap_Node *&P){
- return P->right?P->right->size:0;
- }
- void Treap_Left_Rotate(Treap_Node *&a){
- Treap_Node *b = a->right;
- a->right = b->left;
- b->left = a;
- a = b;
- Treap_SetSize(a->left);
- Treap_SetSize(a->right);
- Treap_SetSize(a);
- }
- void Treap_Right_Rotate(Treap_Node *&a){
- Treap_Node *b = a->left;
- a->left = b->right;
- b->right = a;
- a = b;
- Treap_SetSize(a->left);
- Treap_SetSize(a->right);
- Treap_SetSize(a);
- }
- void Treap_Insert(Treap_Node *&P,int value){
- if (!P){
- P = new Treap_Node;
- P->value = value;
- P->fix = rand();
- }
- if (value < P->value){
- Treap_Insert(P->left,value);
- if (P->left->fix < P->fix)
- Treap_Right_Rotate(P);
- }
- else if (P->value < value){
- Treap_Insert(P->right,value);
- if (P->right->fix < P->fix)
- Treap_Left_Rotate(P);
- }
- else {
- P->cnt++;
- }
- Treap_SetSize(P);
- }
- bool Treap_Delete(Treap_Node *&P,int value){
- bool ret = false;
- if (!P) {
- ret = false;
- }
- else {
- if (value < P->value)
- Treap_Delete(P->left,value);
- else if (P->value < value)
- Treap_Delete(P->right,value);
- else {
- if (P->cnt==0||(--P->cnt)==0){
- if (!P->left||!P->right){
- Treap_Node *t = P;
- if (!P->right)
- P = P->left;
- else
- P = P->right;
- delete t;
- ret = true;
- }
- else if (P->left->fix < P->right->fix){
- Treap_Right_Rotate(P);
- ret = Treap_Delete(P->right,value);
- }
- else {
- Treap_Left_Rotate(P);
- ret = Treap_Delete(P->left,value);
- }
- }
- }
- Treap_SetSize(P);
- }
- return ret;
- }
- Treap_Node* Treap_Findkth(Treap_Node *&P,int k){
- if (k <= lsize(P))
- return Treap_Findkth(P->left,k);
- else if (k > lsize(P)+P->cnt)
- return Treap_Findkth(P->right,k-(lsize(P)+P->cnt));
- else
- return P;
- }
- void Treap_Clear(Treap_Node *&root){
- if (root->left)
- Treap_Clear(root->left);
- if (root->right)
- Treap_Clear(root->right);
- delete root;
- root = NULL;
- }
-
- stack<int> stk;
- void push(){
- int tmp;
- cin >> tmp;
- stk.push(tmp);
- Treap_Insert(root,tmp);
- }
- void pop(){
- if (stk.empty()){
- cout << "Invalid\n";
- }else{
- cout << stk.top() << '\n';
- Treap_Delete(root,stk.top());
- stk.pop();
- }
- }
- void peekmedian(){
- if (stk.empty())
- cout << "Invalid\n";
- else {
- Treap_Node *T = Treap_Findkth(root,(stk.size()+1)/2);
- cout << T->value << '\n';
- }
- }
-
- void InOrderTravel(Treap_Node* root){
- if (root->left) InOrderTravel(root->left);
- cout << root->value << ' ' << root->cnt << '\n';
- if (root->right) InOrderTravel(root->right);
- }
- int main()
- {
- std::ios::sync_with_stdio(false);cin.tie(0);
- int i,j,k;
- int n;
- cin >> n;
- char cmd[100];
- for (i=1;i<=n;i++){
- cin >> cmd;
- switch (cmd[1]){
- case 'u':push();break;
- case 'o':pop();break;
- case 'e':peekmedian();break;
- }
- }
- return 0;
- }