在线查询(分块思想)——附带示例以及完整代码

1 问题描述

给出一个非负整数序列A,元素的个数为N( A ≤ 1 0 5 , N ≤ 1 0 5 A\leq10^{5},N\leq10^5 A105,N105),在有可能随时添加或删除元素的情况下,实时查询序列元素第K大的数(即把序列从小到大排序后,从左到右第K个数)。

在线查询:在查询的过程中,元素可能发生改变(如插入、删除、修改)

离线查询:在查询的过程中,元素不会发生变化。

如果在线查询时,直接使用暴力做法,在删除或添加元素的时候,就要O(n)的时间复杂度来移动序列元素。

例如:对于序列{2,7,4,1,6}来说,此时序列第3大为5;在插入元素4后,这样序列第三大就是4;删除元素1后,序列第一大元素就是2。

2 解决方法

分块:就是把元素划分为若干块。
对于一个有N个元素的序列来说,出最后一块外,其余每一块的元素都应当为 ⌊ N ⌋ \left \lfloor \sqrt{N}\right \rfloor N (此处向下取整,方便程序实现),于是块数为 ⌈ N ⌉ \left \lceil \sqrt{N}\right\rceil N (此处为向上取整)。
这样就把有序序列划分为 ⌈ N ⌉ \left \lceil \sqrt{N}\right\rceil N 块,每块元素个数不超过 ⌊ N ⌋ \left \lfloor \sqrt{N}\right \rfloor N .

例如,对9个元素的序列来说,应该分为 ⌈ 9 ⌉ \lceil \sqrt{9} \rceil 9 = 3块,每块元素为分别为3,3,3;对11个元素的序列来说,应该分为 ⌈ 11 ⌉ \lceil \sqrt{11} \rceil 11 = 4块,每块元素为分别为3,3,3,2。

暴力做法,在删除或添加元素的时要O(n)的时间复杂度来移动序列元素。解决方法,考虑序列元素都是不超过105的非负数,可以设置一个hash数组table[100001],其中table[x]表示整数x的当前存在个数。借助分块思想,逻辑上将0~105分为 ⌈ 1 0 5 + 1 ⌉ = 317 \lceil \sqrt{10^{5}+1} \rceil=317 105+1 =317块,每块的元素个数为 ⌊ 1 0 5 + 1 ⌋ = 316 \left \lfloor \sqrt{10^{5}+1}\right \rfloor=316 105+1 =316

定一个统计数组block[317],block表示第i块元素存在的元素个数。
于是若要新增一个元素x,可以现计算除x所在的块号x/316,然后让block[x/316]加1,表示该块多增加了1;同时令table[x]加1,表示整数x的当前存在个数多了1。
同理,若要删除一个元素,只要让block[x/316]和table[x]都减1即可。

显然删除和增加元素,时间复杂度都是O(1)
那么该如何查询序列中第K大的元素呢?

  • 首先,从小到大枚举块号,利用block数组累加得到前i-1块中存在的元素总个数,然后判断加入i号的元素个数后元素总数能否达到K,
  • 如果能,则说明第K大的数就在当前枚举的这个块中,此时只需要从小到大遍历该块中的每个元素(其中i号快的第一个元素是i*316),利用table数组就绪累加元素的存在个数,直到累计总数达到K,则说明找到了序列第K大的数。
  • 显然思想就是先用 O ( N ) O(\sqrt{N}) O(N )的时间复杂度找到第K大的元素在哪一块,然后再利用 O ( N ) O(\sqrt{N}) O(N )的时间复杂度在块内找到这个元素,因此单词查询的总时间复杂度为 O ( N ) O(\sqrt{N}) O(N )

例如查询{1,1,4,4,5,8}第5大的数,即K= 5。
block[0] = 2,block[1] = 4,block[2] = 1,
table[0] = table[1]= table[3] =table[5] = table[8] = 1,
table[4] = 2;
令sum表示当前已经累计计算存在的数的个数,初始为0,

  • 1 遍历到第0号块时,sum+block[0] = 0+2 < 5,因此第K大的数不在0号块内,令sum为2;
  • 2 遍历到第1号块时,sum+block[0] = 2+4 = 6> 5,因此第K大的数在1号块内;
  • 3 遍历到元素3时,集散sum = sum+table[3] = 3 <4 ,因此3不是第K大的数;
  • 4 遍历到元素4时,计算sum= sum+table[4] = 5 > 4,因此4是第K大的数。

3 示例

3.1 题目

1057 Stack (30分)
Stack is one of the most fundamental data structures, which is based on the principle of Last In First Out (LIFO). The basic operations include Push (inserting an element onto the top position) and Pop (deleting the top element). Now you are supposed to implement a stack with an extra operation: PeekMedian – return the median value of all the elements in the stack. With N elements, the median value is defined to be the (N/2)-th smallest element if N is even, or ((N+1)/2)-th if N is odd.

Input Specification:
Each input file contains one test case. For each case, the first line contains a positive integer N (≤10​5​​ ). Then N lines follow, each contains a command in one of the following 3 formats:

Push key
Pop
PeekMedian

where key is a positive integer no more than 105.

Output Specification:
For each Push command, insert key into the stack and output nothing. For each Pop or PeekMedian command, print in a line the corresponding returned value. If the command is invalid, print Invalid instead.

Sample Input:
17
Pop
PeekMedian
Push 3
PeekMedian
Push 2
PeekMedian
Push 1
PeekMedian
Pop
Pop
Push 5
Push 4
PeekMedian
Pop
Pop
Pop
Pop

      
    
Sample Output:
Invalid
Invalid
3
2
2
1
2
4
4
5
3
Invalid

3.2 解析

3.2.1 题意

模拟栈的入栈、出栈,遇到PeekMedian命令时,输出中位数,当栈中没有元素时,输出Invalid.

3.2.2 思路

在模拟栈的操作同时,还需要支持在线查询,如果暴力求解肯定会超时(数据为105),因此使用分块来求解。
对于N次查询,总的时间复杂度为O(N N \sqrt{N} N ),对于N=105时,总复杂度才107.5,这是在理想情况最坏复杂度。

3 参考代码

#include <cstdio>
#include <cmath>
#include <stack>
#include <cstring>

using std::stack;

const int MAXN = 100010;
const int sqrtN = 317;//有多少块元素
const int blockNum = 316;//每块有多个元素
int table[MAXN];//每个元素有多个
int block[sqrtN];//每块有多少个元素
stack<int> S;


void Pop(){
    int x = S.top();
    S.pop();
    block[x / blockNum]--;
    table[x]--;
    printf("%d\n", x);
}


void Push(int x){
    S.push(x);
    block[x / blockNum]++;
    table[x]++;
}

void PeekMedian(int mid){
    int sum = 0;//目前计算时的实际个数
    int index = 0;
    while(sum + block[index] < mid){
        sum += block[index++];
    }
    int num = index * blockNum;
    while(sum + table[num] < mid){
        sum += table[num++];
    }
    printf("%d\n", num);
    
    //用for循环计算
    // for (index = 0; index < sqrtN; ++index)
    // {
    //     if(sum + block[index] < mid){
    //         sum += block[index];
    //     }else{
    //         break;
    //     }
    // }

    // for (int j = index * blockNum; j < (index + 1) * blockNum; ++j)
    // {
    //     sum += table[j];
    //     if(sum  >= mid){
    //         printf("%d\n", j);
    //         break;
    //     }
    // }
}


int main(int argc, char const *argv[])
{
    memset(table, 0, sizeof(table));
    memset(block, 0, sizeof(block));

    int n, x;
    char cmd[20];
    scanf("%d", &n);
    while(n--){
        scanf("%s", cmd);
        if(strcmp(cmd, "Pop") == 0){
            if(S.empty()== true){
                printf("Invalid\n");
            }else{
                Pop();
            }
        }else if(strcmp(cmd, "Push") == 0){
            scanf("%d", &x);
            Push(x);
        }else{
            if(S.empty() == true){
                printf("Invalid\n");
            }else{
                int mid = S.size();
                if(mid % 2 == 0){
                    mid /= 2;
                }else{
                    mid = (mid + 1) / 2;
                }
                PeekMedian(mid);
            }
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁星蓝雨

如果觉得文章不错可以请喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值