《算法笔记》编程笔记——第十三章 专题扩展

  • 分块思想

    • 问题描述:给出一个非负整数序列A,元素个数为N(N<=10^5, A[i]<=10^5),在有可能随时添加或者删除元素的情况下,实时查询元素第K大,即把序列元素从小到大排序后从左到右的第K个元素。【在线查询,当暴力法排序做时容易超时】

    • 思想:将有序元素划分为若干块。对一个有N个元素的有序数列来说,除最后一块,其余每块中元素的个数都应当为int(sqrt(n))【即根号n往下取整个】,总共的块数应该为根号n往上取整。

      • 查找思路:先用O(sqrt(n))的时间复杂度找到第K大的元素在哪一块,然后再用O(sqrt(n))的时间复杂度在块内找到这个元素,因此单次查询的总时间复杂度是O(sqrt(n))。
    • 【题目描述PAT A 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 (≤105). 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.

      【题目分析】

      ①在线查询问题,暴力排序将超时;采用分块查询的方法。当采用分块查询时,时间复杂度为O(Nsqrt(N)),对于N=105来说,总复杂度大约为10(7.5),是可以接受的。

      【代码如下】

      #include<bits/stdc++.h>
      using namespace std;
      const int maxn = 100010;
      const int sqrN = 316; //表示块内元素个数,int(sqrt(10^5+1) )=316
      stack<int> st;
      int block[sqrN+1];//记录每一块中存在的元素个数
      int table[maxn];//hash数组,记录元素当前存在个数
      
      void peekMedian(int k){
      	int sum = 0; //sum存放当前累计存在的数的个数
      	int idx = 0; //块号
      	while(sum + block[idx] < k){ //找到第k大的数所在的块号 
      		sum += block[idx++]; //未达到k,则累加上当前块的元素个数 
      	}
      	int num = idx * sqrN; //idx号块的第一个数
      	while(sum + table[num] < k){
      		sum += table[num++]; //累加块内元素个数,直到sum达到k 
      	}
      	printf("%d\n", num); //sum达到k,找到了第k大的数为num 
      		
      } 
      void Push(int x){
      	st.push(x);
      	block[x / sqrN]++;//x所在块的元素个数加1
      	table[x]++;//x的存在个数加1 
      }
      void Pop(){
      	int x = st.top();
      	st.pop();
      	block[x / sqrN]--;
      	table[x]--;
      	printf("%d\n", x);
      } 
      
      int main(){
      	int x, query;
      	memset(block, 0, sizeof(block));
      	memset(table, 0, sizeof(table));
      	char cmd[20]; //命令
      	scanf("%d", &query);
      	for(int i = 0; i < query; i++){
      		scanf("%s", cmd);
      		if(strcmp(cmd, "Push") == 0){
      			scanf("%d", &x);
      			Push(x);
      		}
      		else if(strcmp(cmd, "Pop") == 0){
      			if(st.empty()){
      				printf("Invalid\n");
      			} else Pop();
      		}else{
      			if(st.empty()){
      				printf("Invalid\n");
      			}else{
      				int k = st.size();
      				if( k % 2 == 1) k  = (k + 1) / 2; //k为中间位置 
      				else k = k / 2;
      				peekMedian(k);//输出中位数 
      			}
      		}
      	} 
      	return 0;
      }
      
  • 树状数组(求前n个整数和)

    • 问题描述:给出一个整数序列A,元素的个数为N(N<= 10^5),接下来查询K次(K <= 10^5),每次查询将给出一个正整数x(x <= K),假设在查询过程中可能随时给第x个整数加上一个整数v,求前x个整数之和。

      • 解释分析:
    • 如何知道A[1]+……+A[x]对应树状数组中的哪些项呢?

      • 记SUM(1, x) = A[1] + …… +A[x],由于C[x]的覆盖长度是lowbit(x), 因此,有关系式——C[x] = A[x-lowbit(x) + 1] + …… + A[x]【比如C[9],那么从9这个位置开始往前推算,减去lowbit的覆盖长度然后再+1就是C[9]的开始位置了。】
      • 于是,SUM(1, x) = A[1] +……+ A[x] = A[1] + ……+A[x-lowbit(x) ] + A[x - lowbit(x) + 1] + …… +A[x] = SUM(1, x-lowbit(x)) + C[x]。就将问题缩小化了。
  • 问题分析:

    • ①设计函数getSum(x),返回前x个数之和A[1]+ ……+A[x]。

      • ②设计函数update(x, v),实现将第x个数加上一个数v的功能,即A[x] += v;

        • 如果让A[x]+v,那么需要寻找树状数组C中能覆盖A[x]的元素,让他们都加上v。
      • 代码如下:

        //树状数组c[i]的覆盖长度是lowbit(i),此外,该数组下标从1开始。
        //getSum函数
        int getSum(int x){
            int sum = 0;
            for(int i = x; i > 0; i -= lowbit[i]){ //因为数组下标从1开始,所以注意i不是减到0
                sum += c[i];        //累计c[i],然后将问题缩小到sum(1, i - lowbit(i));
            }
            return sum;
        }
        //如果要求数组下标在区间[x, y]之内的数的和,即A[x] + A[x + 1]+……+A[y],可以转换成getSum(y) - getSum(x-1)来解决。
        //update函数,将第x个整数加上v。时间复杂度为O(logn)
          void update(int x, int v){
            for(int i = x; i <= n; i += lowbit(i)){//注意i必须可以取到n
                  c[i] += v; //让c[i]加上v,然后让c[i+lowbit(i)]加上v
              }
        }
        
      • 树状数组的应用:给定一个有N个正整数的序列A(N <= 10^5, A[i] <= 10 ^5),对序列中的每个数,求出序列中它左边比它小的数的个数。

        #include<bits/stdc++.h>
        using namespace std;
        #define lowbit(i) ((i) & (-i)) //lowbit写成宏定义的形式,注意括号
        int c[maxn]; //树状数组
        //update函数
        void update(int x, int v){
            for(int i = x; i <= n; i += lowbit(i)){//注意i必须可以取到n
                c[i] += v; //让c[i]加上v,然后让c[i+lowbit(i)]加上v
            }
        }
        //getSum函数
        int getSum(int x){
            int sum = 0;
            for(int i = x; i > 0; i -= lowbit[i]){ //因为数组下标从1开始,所以注意i不是减到0
                sum += c[i];        //累计c[i],然后将问题缩小到sum(1, i - lowbit(i));
            }
            return sum;
        }
        int main(){
            int n, x;
            cin >> n;
            memset(c, 0, sizeof(c));
            for(int i = 0; i < n; i++){
              cin >> x;
                update(x, 1);//表示的意思是将x的出现次数加1
              printf("%d\n", getSum(x-1));//查询当前小于x的数的个数
            }
          return 0;
        

      }

      
      - 如果问题统计的是序列中在元素左边比元素大的元素的个数,就等价于计算getSum(n) - getSum(A[i]);  而如果要统计序列中在元素右边比该元素小的元素个数,只需要把原始数组从右往左遍历即可。
      
      - 离散化:如果一个序列A中元素为{520, 99999999999, 18, 666, 88888},那么它等价于{2, 5, 1, 3, 4},即序列的值可以等价于它在序列中的排名。 
      
      -  一般来说,只有当知道所有出现的元素之后,才能方便进行离散化。也就是离散化只适用于离散查询。
      -  离散化可以把任何不在合适区间的整数或者非整数都转换为不超过元素个数大下的整数。
      
      - 代码如下:
      
      ```c++
      #include<bits/stdc++.h>
      using namespace std;
      const int maxn = 100010;
      #define lowbit(i) ((i) & (-i))  //注意最后没有分号!!!
      struct node{
          int val;
          int pos; //原始序号
      }temp[maxn];
      int A[maxn];  //离散化后的原始数组
      int c[maxn];   //树状数组
      //update函数
      void update(int x, int v){
          for(int i = x; i <= n; i += lowbit(i)){//注意i必须可以取到n
              c[i] += v; //让c[i]加上v,然后让c[i+lowbit(i)]加上v
          }
      }
      //getSum函数
      int getSum(int x){
          int sum = 0;
          for(int i = x; i > 0; i -= lowbit[i]){ //因为数组下标从1开始,所以注意i不是减到0
              sum += c[i];        //累计c[i],然后将问题缩小到sum(1, i - lowbit(i));
          }
          return sum;
      }
      //按val从小到大排序
      bool cmp(node a, node b){
          return a.val < b.val;
      }
      int main(){
          int n;
          cin >> n;
          memset(c, 0, sizeof(c));
          for(int i = 0; i < n; i++){
              cin >> temp[i].val; //输入原始序列值
              temp[i].pos = i; //原始序列
          }
          //离散化
          sort(temp, temp + n, cmp);
          //“排名”模块
          for(int i = 0; i < n; i++){
              //与上一个元素值不同时,赋值为元素个数
              if(i == 0 || temp[i].val != temp[i-1].val){
                  A[temp[i].pos] = i + 1;//注意,这里需要从1开始
              }else{
                  A[temp[i].pos] = A[temp[i-1].pos];//如果与上一个元素相同,直接继承
            }
          }
        //正式进入更新和求和操作
          for(int i = 0; i < n; i++){
              update(A[i], 1); //A[i]出现的次数加1
            cout << getSum(A[i] - 1); << endl; //查询当前小于A[i]的数的个数
          }
        return 0;
      }
      
      
      • 树状数组求出序列第k大的问题解答,其实就等价于寻找第一个满足条件“getSum(i) >= K”的i。【以下都是对树状数组进行的单点更新、区间查询

        //查找问题可以综合二分法
        int findKthElement(int K){
            int l = 1, r = MAXN, mid;
            while(l < r){
                mid = (l + r) / 2;
                if(getSum(mid) >= K)r = mid;
                else l = mid + 1;
            }
            return l;
        }
        
        //扩展:如果要求A[a][b]--A[x][y]这个子矩阵的元素之和,只需要计算getSum(x, y) - getSum(x-a, y) - getSum(x, y - a) + getSum(x - a, y - a)即可。
        //二维树状数组代码如下:
        int c[maxn][maxn];
        //二维update函数位置为(x, y)的整数加上v
        void update(int x, int y, int v){
            for(int i = x; i < maxn; i += lowbit(i)){
                for(int j = y; j < maxn; j += lowbit(j)){
                    c[i][j] += v;
                }
            }
        }
        //二维getSum函数返回(1, 1)到(x, y)的子矩阵中元素之和
        int getSum(int x, int y){
          int sum = 0;
            for(int i = x; i > 0; i -= lowbit(i)){
              for(int j = y; j > 0; j -= lowbit(j)){
                  sum += c[i][j];
              }
          }
          return sum;
        }
        
      • 如果要对树状数组进行区间更新、单点查询,那么就需要解决的问题是:

        • ①设计getSum(x)函数,返回A[x]。

        • ②设计函数update(x, v),将A[1] - A[x]的每个数都加上一个数v。

        • 代码如下:

          //getSum函数返回第x个整数的值
          //思想就是将原先的update函数作为这里的getSum函数
          int getSum(int x){
              int sum = 0;
              for(int i = x; i < maxn; i += lowbit(i)){ //这时原先update函数的递增方式
                  sum += c[i];
              }
            return sum;
          }
          //update函数,将前x个整数都加上v
          //思想就是将原先的getSum函数作为这里的update函数
          void update(int x, int v){
            for(int i = x; i > 0; i -= lowbit(i)){
                  c[i] += v;//让c[i]加上v
              }
          }
          //扩展:如果要让A[x]-A[y]的每个数都加上v,只要先让A[1]-A[y]的每个数加上v,然后让A[1]-A[x-1]的每个数加上-v。即先执行update(y, v),后执行update(x-1, -v)。
          
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

梦想总比行动多

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值