分块的应用

3 篇文章 0 订阅
3 篇文章 0 订阅
本文详细介绍了分块思想在数据结构与算法中的多种应用,包括区间修改、单点查询、区间求和、小于特定值元素计数等操作。通过分块,可以将操作的时间复杂度降低到平方根级别,有效提高了算法效率。文章提供了多个实例,如区间加法、乘法、开方、查询等,展示了如何设计和实现分块算法来解决实际问题。
摘要由CSDN通过智能技术生成

分块

分块是指把序列分成若干个块,使得对序列的各种操作的时间复杂度达到均衡,都不至于太高。一般情况下,都是把序列分成 n \sqrt{n} n 块,每块的大小大致为 n \sqrt{n} n 。n表示序列中元素的个数。
分块是一种思想,理解起来很简单,用代码实现也不难,在很多数据结构的题目中都能用上。

例1:

给一个长度为 n n n的数组,现在有两种操作:
操作1:区间修改 ( 0 , l , r , c ) (0,l,r,c) (0,l,r,c) 表示将区间 [ l , r ] [l,r] [l,r]整体加上一个数值 c c c
操作2:单点查询$(1,l,r,c) 表 示 查 询 元 素 表示查询元素 r 的 值 。 此 时 忽 略 的值。此时忽略 l 和 和 c$。
数据规模: n ≤ 500000. n \leq 500000. n500000.

分析:

区间修改和单点查询。用树状数组和线段树肯定可以做。
现在我们考虑用分块来做。
我们首先将数组分成若干块,每块的长度为 n \sqrt{n} n ,最后的一块可能小一点,这没有关系。总的块数大概是根号级别的。
对于区间修改,比如 [ l , r , c ] [l,r,c] [l,r,c],我们可以计算区间 [ l , r ] [l,r] [l,r]包含了哪些块。一般情况下, [ l , r ] [l,r] [l,r]这个区间,可能在左右两端包含了不完整的块,中间包含了若干个整块。对于部分块,我们可以暴力地去修改,即每个元素实时地去修改;对于整体块,我们只需要记录一个增量,表示这个块整体增加的数值。

对于单点查询,直接查询好了。注意要加上该区间的增量。

时间复杂度分析

那这个分块算法的时间复杂度为多少呢?
先看修改操作:对于每次修改操作,部分块最多两个,块长最多为 n \sqrt{n} n ,这是暴力修改的,需要的时间复杂度为 n \sqrt{n} n , 对于中间的整体块,我们只需要记录增量,每个整体块花费的时间为 O ( 1 ) O(1) O(1),最多有 n \sqrt{n} n 个整体块。
所以,一次修改的时间复杂度为 O ( n ) O(\sqrt{n}) O(n ).
再看查询操作:对于每次查询操作,部分块最多两个,块长最多为sqrt(n) ;中间的整体块,每个整体块只需要花费 O ( 1 ) O(1) O(1)的时间,最多有 n \sqrt{n} n 个整体块。所以,一次查询的时间复杂度为 O ( n ) O(\sqrt{n}) O(n )
这样,不管是修改还是查询,其时间复杂度均为 O ( n ) O(\sqrt{n}) O(n ).
所以,总的时间复杂度为 O ( n ) O(\sqrt{n}) O(n ).

在实现过程中,对于每一次操作,我们可以大体将操作区间分成三个部分,左端的不完整块,中间若干个完整块,右端的不完整块。但有时,这三个部分也不一定都存在。编程时稍加注意就可以了。对于部分块,我们使用暴力;中间的完整块,进行整体操作;当到了结尾的部分块,又转为暴力操作。
一个块的起点和终点,是可以通过下标计算出来的。
假设数组下标从 1 1 1开始,块的编号从 0 0 0开始计算。

块大小为 b l o c k = c e i l ( s q r t ( n ) ) block=ceil(sqrt(n)) block=ceil(sqrt(n)).
则元素i所在的块的起点为: ( i − 1 ) / b l o c k ∗ b l o c k + 1 (i-1)/block*block+1 (i1)/blockblock+1
元素i所在的块的终点为: ( i − 1 ) / b l o c k ∗ b l o c k + b l o c k (i-1)/block*block+block (i1)/blockblock+block
元素i所在的块的编号为:$(i-1)/block $
判断元素i是否为一个块的起点: i f ( i % b l o c k = = 1 ) if(i\%block==1) if(i%block==1)
也可以在将每个元素所属的块保存在数组中,以及每个块的起点和终点都提前保存在数组中。
具体实现时灵活处理即可。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 50005
int arr[MAXN], delta[MAXN];
int n, op, l, r, c, ans;
int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
  int block = ceil(sqrt(n));
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &op, &l, &r, &c);
    if (op == 0) {
      if (l % block != 1)  //左端有一个部分块,暴力加
      {
        int rt = (l - 1) / block * block + block;  //部分块的右端点
        while (l <= r && l <= rt) {
          arr[l] += c;
          l++;
        }
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        delta[(l - 1) / block] += c;  //整体块加上c
        l = l + block;
      }
      while (l <= r)  //右边的部分块,暴力加
      {
        arr[l] += c;
        l++;
      }
    } else {
      printf("%d\n", arr[r] + delta[(r - 1) / block]);
    }
  }
  return 0;
}
例2.

给一个长度为 n n n的数组,有 n n n个操作,操作分为两种,一种是区间修改,即将一个区间 [ l , r ] [l,r] [l,r]整体加上一个整数 c c c;一种是区间求和,即查询区间 [ l , r ] [l,r] [l,r]的和。
$n \leq 50000 $

分析:

区间修改和上道题是一样的。
区间查询也很简单,分块以后,对两端的非完整块,暴力求和,中间的完整块依次统计。时间复杂度为 O ( N N ) O(N\sqrt{N}) O(NN ).
查询时,只要不要漏掉了区间的增量。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN];
LL sum[MAXN], delta[MAXN];
int n, block, blockcnt, blockid;
int opt, l, r, c;
LL ans;
int main() {
  scanf("%d", &n);
  block = ceil(sqrt(n));
  for (int i = 1; i <= n; i++) {
    scanf("%d", &arr[i]);
    sum[(i - 1) / block] += arr[i];
  }
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &opt, &l, &r, &c);
    if (opt == 0) {
      if (l % block != 1) {
        blockid = (l - 1) / block;
        int tmpr = min(r, blockid * block + block);
        while (l <= tmpr) {
          sum[blockid] += c;
          arr[l] += c;
          l++;
        }
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        blockid = (l - 1) / block;
        delta[blockid] += c;
        l += block;
      }
      blockid = (l - 1) / block;
      while (l <= r) {
        sum[blockid] += c;
        arr[l] += c;
        l++;
      }
    } else {
      ans = 0;
      if (l % block != 1) {
        blockid = (l - 1) / block;
        int tmpr = min(r, blockid * block + block);
        while (l <= tmpr) {
          ans += delta[blockid];
          ans = ans % (c + 1);
          ans += arr[l];

          l++;
        }
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        blockid = (l - 1) / block;
        ans += block * delta[blockid];
        ans = ans % (c + 1);
        ans += sum[blockid];
        ans = ans % (c + 1);
        l = l + block;
      }
      blockid = (l - 1) / block;
      while (l <= r) {
        ans += arr[l];
        ans += delta[blockid];
        ans = ans % (c + 1);
        l++;
      }
      printf("%lld\n", ans % (c + 1));
    }
  }
  return 0;
}
例3:

给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间加法,询问区间内小于某个值 x x x的元素个数。

输入格式

第一行输入一个数字 n n n
第二行输入 n n n个数字,第i个数字为 a i a_i ai,以空格隔开。
接下来输入 n n n行询问,每行输入四个数字 o p , l , r , c op,l,r,c op,l,r,c,以空格隔开。
o p = = 0 op==0 op==0,表示将位于 [ l , r ] [l,r] [l,r]的之间的数字都加上 c c c
o p = = 1 op==1 op==1,表示询问 [ l , r ] [l,r] [l,r]中,小于 c ∗ c c*c cc的数字的个数。

输出格式

对于每次询问,输出一行一个数字表示答案。

分析

仍然考虑用分块来做。
现在的查询要复杂一些了,查询的是区间中小于x的元素个数。很容易想到,可以将块中元素排序。
这样整体块中可以用二分查找。
那修改操作呢?如果整体块整体增加一个值,这个不影响排序,只需要记录增量即可。而如果是部分块,则暴力修改,并重新排序即可。
这样一个整体块的查询操作为 O ( l o g ( b l o c k ) ) O(log(block)) O(log(block)),修改操作为 O ( 1 ) O(1) O(1).部分块的查询操作为 O ( b l o c k ) O(block) O(block),修改操作为 O ( b l o c k + l o g ( b l o c k ) ) O(block+log(block)) O(block+log(block)).

参考代码:

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN], delta[MAXN];
int sortarr[MAXN];
int n, op, l, r, c, block, blockcnt, ans;
void partmodify(int l, int r) {
  int blockst = (l - 1) / block * block + 1,
      blocked = min(n, (blockst + block - 1)), blockid = (l - 1) / block;
  for (int i = blockst; i <= blocked; i++) {
    arr[i] += delta[blockid];
    if (i >= l && i <= r) arr[i] += c;
    sortarr[i] = arr[i];
  }
  delta[blockid] = 0;
  sort(sortarr + blockst, sortarr + blocked + 1);
}
int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d", &arr[i]);
    sortarr[i] = arr[i];
  }
  block = ceil(0.5 * sqrt(n));
  blockcnt = ceil(n / block);
  for (int i = 0; i < blockcnt - 1; i++)
    sort(sortarr + i * block + 1, sortarr + (i + 1) * block + 1);
  sort(sortarr + (blockcnt - 1) * block + 1, sortarr + n + 1);
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &op, &l, &r, &c);
    if (op == 0) {
      if (l % block != 1) {
        int tmpr = min(r, (l - 1) / block * block + block);
        partmodify(l, tmpr);
        l = tmpr + 1;
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        delta[(l - 1) / block] += c;
        l = l + block;
      }
      if (l <= r) {
        partmodify(l, r);
      }
    } else {
      ans = 0;
      int blockid = 0;
      if (l % block != 1) {
        int tmpr = min(r, (l - 1) / block * block + block);
        blockid = (l - 1) / block;
        while (l <= tmpr) {
          if (arr[l] + delta[blockid] < 1ll * c * c) ans++;
          l++;
        }
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        blockid = (l - 1) / block;
        ans += lower_bound(sortarr + l, sortarr + l + block,
                           1ll * c * c - delta[blockid]) -
               (sortarr + l);
        l = l + block;
      }
      blockid = (l - 1) / block;
      while (l <= r) {
        if (arr[l] + delta[blockid] < 1ll * c * c) ans++;
        l++;
      }
      printf("%d\n", ans);
    }
  }
  return 0;
}
例4:
题目描述

给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间加法,询问区间内小于某个值 x x x 的前驱(比其小的最大元素)。

输入格式

第一行输入一个数字n 。

第二行输入 n n n个数字,第 i i i个数字为 a i a_i ai,以空格隔开。

接下来输入 n n n行询问,每行输入四个数字 o p t 、 l 、 r 、 c opt、l、r、c optlrc,以空格隔开。

o p t = = 0 opt==0 opt==0,表示将位于 [ l , r ] [l,r] [l,r]的之间的数字都加 c c c

o p t = = 1 opt==1 opt==1,表示询问 [ l , r ] [l,r] [l,r]中, c c c的前驱。

输出格式

对于每次询问,输出一行一个数字表示答案。

分析

我们将原数组复制一份,然后进行分块。原数组和新数组采用同样的分块方式。新数组中需要对每个块中的元素进行排序。
原数组主要处理区间修改操作,新数组主要处理查询操作。
具体来讲,区间修改时,在原数组中,对于不完整的块(最多两个),对其中每个元素进行实时修改,并将这两个块同步到新数组中——块复制并重新排序;对整体块,直接记录整理增量即可。排序没有变化,所以不需要更新到新数组中。
一次修改的时间复杂度为 O ( n + n ∗ l o g n ) O(\sqrt{n}+\sqrt{n}*log\sqrt{n}) O(n +n logn )
区间查询时,对非完整块,逐个元素比较;在整体块中,采用二分查找。一次查询时间复杂度为 O ( n + n ∗ l o g n ) O(\sqrt{n}+\sqrt{n}*log\sqrt{n}) O(n +n logn )

#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN],delta[MAXN];
int sortarr[MAXN];
int n,op,l,r,c,block,blockcnt,ans;
void partmodify(int l,int r)
{
    int blockst=(l-1)/block*block+1,blocked=min(n,(blockst+block-1)),blockid=(l-1)/block;
    for(int i=blockst;i<=blocked;i++)
    {
        arr[i]+=delta[blockid];
        if(i>=l&&i<=r)arr[i]+=c;
        sortarr[i]=arr[i];
    }
    delta[blockid]=0;
    sort(sortarr+blockst,sortarr+blocked+1);
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&arr[i]);  
        sortarr[i]=arr[i]; 
    }
    block=ceil(sqrt(n));
    blockcnt=ceil(n/block);
    for(int i=0;i<blockcnt-1;i++)
    sort(sortarr+i*block+1,sortarr+(i+1)*block+1);
    sort(sortarr+(blockcnt-1)*block+1,sortarr+n+1);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op==0)
        {
            if(l%block!=1)
            {
                int tmpr=min(r,(l-1)/block*block+block);
                partmodify(l,tmpr);
                l=tmpr+1;
            }
            while(l/block<=r/block && r-l+1>=block)
            {
                delta[(l-1)/block]+=c;
                l=l+block;
            }
            if(l<=r)
            {
                partmodify(l,r);
            }
        }
        else
        {
            ans=1<<31;
            int blockid=0;
            if(l%block!=1)
            {
                int tmpr=min(r,(l-1)/block*block+block);
                blockid=(l-1)/block;
                while(l<=tmpr)
                {
                    if(arr[l]+delta[blockid]<c)
                    {
                        if(arr[l]+delta[blockid]>ans)
                        ans=arr[l]+delta[blockid];
                    }
                    l++;
                }
            }
            while(l/block<=r/block&&r-l+1>=block)
            {
                blockid=(l-1)/block;
                int pos=lower_bound(sortarr+l,sortarr+l+block,c-delta[blockid])-(sortarr+l);
                if(pos!=0)
                {
                    if(sortarr[l+pos-1]+delta[blockid]>ans)
                    ans=sortarr[l+pos-1]+delta[blockid];
                }
                l=l+block;
            }
            blockid=(l-1)/block;
            while(l<=r)
            {
                if(arr[l]+delta[blockid]<c)
                {
                    ans=max(ans,arr[l]+delta[blockid]);
                }
                l++;
            }
            if(ans==(1<<31))
            printf("-1\n");
            else printf("%d\n",ans);
        }
    }
    return 0;
}
例5:

给出一个长为n的数列 ,以及n个操作,操作涉及区间开方,区间求和。

输入格式

第一行输入一个数字n。

第二行输入n个数字,第i个数字为ai,以空格隔开。

接下来输入n行询问,每行输入四个数字 op,l,r,c,以空格隔开。

若 op==0,表示将位于[l,r]之间的数字都开方。对于区间中每个数ai,ai变成sqrt(ai)

若 op==1,表示询问位于[l,r]的所有数字的和。

输出格式

对于每次询问,输出一行一个数字表示答案。

分析:

一个正整数 n n n,经过若干次开方操作,就会变成1.这个过程是很快的。 1 0 8 10^8 108经过 5 5 5次开方操作就可以变成1.
我们对数组进行分块,对每个块记录它是否全为1.如果全为1,则开方操作可以省略了。否则,对块中每个元素进行开方。
看似非常暴力,但其实非常优秀,我们分析一下修改的时间复杂度。
每次修改最多包含两个不完整区间,假设每次都是不全为1,需要全部修改,花费 n \sqrt{n} n 次开方;对于完整区间,每个区间最多经过 5 5 5次左右开方就变成全 1 1 1了,累计下来最多是 5 ∗ n 5*n 5n次开方操作。所以,修改的总时间复杂度为 O ( n ∗ n + 5 ∗ n ) = O ( n ∗ n ) O(n*\sqrt{n}+5*n)=O(n*\sqrt{n}) O(nn +5n)=O(nn )
查询操作:遇到全1的,直接计算;遇到不全为1的,逐个元素累加。那如果连续很多次查询操作,可能会超时。怎么办?预处理处每个块的和,在修改时更新每个分块的和。这样,查询时遇到完整块,就可以对完整块整体操作了。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define LL long long int
int arr[MAXN], block, blockid, n, opt, l, r, c;
LL ans, sum[400];
int cntone[400], bsz[400];

int main() {
  scanf("%d", &n);
  assert(n > 0);
  block = ceil(sqrt(n));
  for (int i = 1; i <= n; i++) {
    blockid = (i - 1) / block;
    scanf("%d", &arr[i]);
    cntone[blockid] += (arr[i] == 1);
  }
  for (int i = 0; i < (n - 1) / block; i++) {
    bsz[i] = block;
  }
  bsz[(n - 1) / block] = (n - 1) % block + 1;
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &opt, &l, &r, &c);
    if (opt == 0)
      while (l <= r) {
        blockid = (l - 1) / block;
        if (cntone[blockid] == bsz[blockid]) {
          l = (blockid + 1) * block + 1;
        } else {
          if (arr[l] > 1) {
            arr[l] = int(sqrt(arr[l]));
            if (arr[l] == 1) cntone[(l - 1) / block]++;
          }
          l++;
        }
      }
    else {
      ans = 0;
      while (l <= r) {
        blockid = (l - 1) / block;
        if (cntone[blockid] == bsz[blockid]) {
          int tmpr = min(r, blockid * block + block);
          ans += (tmpr - l + 1);
          l = tmpr + 1;
        } else {
          ans += arr[l];
          l++;
        }
      }
      printf("%lld\n", ans);
    }
  }
  return 0;
}
例6.
题目描述

给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。

输入格式

第一行输入一个数字n。

第二行输入 n 个数字,第i个数字为ai,以空格隔开。

接下来输入n行询问,每行输入四个数字 opt、l、r、c,以空格隔开。

若opt==0,表示在第l个数字前插入数字 r( c忽略)。

若opt==1 ,表示询问ar的值(l和c忽略)。

分析:

分块以后,每个块用一个 v e c t o r vector vector保存,插入时直接在 v e c t o r vector vector中操作,同时修改块大小。
每次定位时,对块的大小求前缀和,先定位到块,然后在块中继续定位即可。
本题数据是随机生成的,插入的元素会平均分布在每个块中,这样做没有问题。

如果数据不是随机生成的,这样做就可能超时。只要构造这样的数据,使得插入都发生在一个块中,时间复杂度就变成了 O ( n 2 ) O(n^2) O(n2)了。
那如何解决呢?

我们可以设一个阈值,当块大小变为标准块的两倍时,就把这个块裂开,变成两个块。
vector不好用了,我们可以使用链表操作。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 210005
struct node {
  int val;
  node *next;
} arr[MAXN];
struct dnode {
  int cnt;
  node *head;
  dnode *nxtlist;
} lst[600];
int block, ans, n, opt, l, r, c;
int main() {
  int a;
  scanf("%d", &n);
  block = ceil(sqrt(2 * n));
  int lstid = 0, nodeid = 0;
  node *last = NULL;
  for (int i = 1, j = 1; i <= n; i++, j++) {
    scanf("%d", &a);
    arr[++nodeid].val = a;
    if (last == NULL) {
      lst[++lstid].head = &arr[nodeid];
      last = &arr[nodeid];
    } else {
      last->next = (&arr[nodeid]);
      last = &arr[nodeid];
    }
    if (j == block) {
      lst[lstid].nxtlist = &lst[lstid + 1];
      lst[lstid].cnt = block;
      last = NULL;
      j = 0;
    }
  }
  if (n % block != 0) lst[lstid].nxtlist = 0, lst[lstid].cnt = (n % block);
  int tot = n;
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &opt, &l, &r, &c);
    if (opt == 0) {
      dnode *p = &lst[1];
      int sum = 0;
      while (p->nxtlist != NULL && sum + (p->cnt) < l) {
        sum += p->cnt;
        p = p->nxtlist;
      }
      node *q = p->head;
      sum++;
      if (sum == l) {
        arr[++nodeid].val = r;
        arr[nodeid].next = q;
        p->head = &arr[nodeid];
        p->cnt++;
      } else {
        node *last = q;
        while (q && sum < l) {
          last = q;
          q = q->next;
          sum++;
        }
        arr[++nodeid].val = r;
        arr[nodeid].next = q;
        last->next = &arr[nodeid];
        p->cnt++;
      }
      if (p->cnt > 2 * block) {
        q = p->head;
        int pos = 0;
        while (pos < block) {
          pos++;
          q = q->next;
        }
        lst[++lstid].head = q->next;
        lst[lstid].cnt = p->cnt - pos - 1;
        q->next = 0;
        lst[lstid].nxtlist = p->nxtlist;
        p->nxtlist = &lst[lstid];
        p->cnt = pos + 1;
      }
    } 
      else {
      int sum = 0;
      dnode *p = &lst[1];
      while (p->nxtlist && sum + p->cnt < r) {
        sum = sum + p->cnt;
        p = p->nxtlist;
      }
      node *q = p->head;
      sum++;
      while (q->next && sum < r) {
        q = q->next;
        sum++;
      }
      printf("%d\n", q->val);
    }
  }
  return 0;
}
例7.
题目描述

给出一个长为 n n n的数列,以及 n n n个操作,操作涉及区间乘法,区间加法,单点询问。

输入格式

第一行输入一个数字n。

第二行输入n个数字,第 i 个数字为ai,以空格隔开。

接下来输入n行询问,每行输入四个数字 o p t 、 l 、 r 、 c opt、l、r、c optlrc,以空格隔开。

o p t = = 0 opt==0 opt==0,表示将位于 [ l , r ] [l,r] [l,r]的之间的数字都加 c c c

若$opt==1 , 表 示 将 位 于 ,表示将位于 [l,r] 的 之 间 的 数 字 都 乘 的之间的数字都乘 c$。

若$opt==2 , 表 示 询 问 ,表示询问 a_r 的 值 模 10007 ( 的值模10007( 10007l 和 和 c$忽略)

输出格式

对于每次询问,输出一行一个数字表示答案。

分析:

对数组进行分开,对每个块,用delta记录它整体加上的值,用multi记录它整体乘上的值。当delta和multi都有值时,表示先乘multi后再加上delta。
遇到完整块,
对于加法操作,则 d e l t a = d e l t a + c delta=delta+c delta=delta+c
对于乘法操作,则$multi=multic, delta=deltac $
遇到不完整块:
对于加法操作:先将 m u l t i multi multi乘到块中每个元素上,并把 m u l t i multi multi置为 1 1 1,然后再对操作区间内的数进行加 c c c的操作;
对于乘法操作:可以先将 m u l t i multi multi d e l t a delta delta更新到每个元素上,再将区间中的元素乘 c c c

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MOD 10007
int arr[MAXN], add[MAXN], mul[MAXN];
int n, op, l, r, c, block;
void modify(int be, int ed, bool flg)  // flg==0 add, flg==1 mul
{
  int blockst = (be - 1) / block * block + 1,
      blocked = min(n, (be - 1) / block * block + block),
      blockid = (be - 1) / block;
  for (int i = blockst; i <= blocked; i++) {
    arr[i] = (arr[i] * mul[blockid] + add[blockid]) % MOD;
    if (i >= be && i <= ed) {
      if (flg == 0)
        arr[i] = (arr[i] + c) % MOD;
      else
        arr[i] = arr[i] * c % MOD;
    }
  }
  mul[blockid] = 1, add[blockid] = 0;
}

int main() {
  scanf("%d", &n);
  block = ceil(sqrt(n));
  for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
  int blockid = 0;
  for (int i = 0; i <= 500; i++) mul[i] = 1, add[i] = 0;
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &op, &l, &r, &c);
    c = c % MOD;
    if (op <= 1)  // add c or multi c;
    {
      if (l % block != 1) {
        int tmpr = min(r, (l - 1) / block * block + block);
        modify(l, tmpr, op);
        l = tmpr + 1;
      }
      while (l / block <= r / block && r - l + 1 >= block) {
        blockid = (l - 1) / block;
        if (op == 0)
          add[blockid] = (add[blockid] + c) % MOD;
        else {
          mul[blockid] = mul[blockid] * c % MOD;
          add[blockid] = add[blockid] * c % MOD;
        }
        l = l + block;
      }
      if (l <= r) {
        modify(l, r, op);
      }
    } else {
      blockid = (r - 1) / block;
      printf("%d\n", (arr[r] * mul[blockid] + add[blockid]) % MOD);
    }
  }
  return 0;
}
例8.

给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数x的元素个数,并将这个区间的所有元素改为x。

输入格式

第一行输入一个数字n。

第二行输入n个数字,第 i 个数字为ai,以空格隔开。

接下来输入n行询问,每行输入三个数字$ l、r、c$,以空格隔开。

表示先查询位于[l,r]的数字有多少个是c,再把位于[l,r]的数字都改为c。

输出格式

对于每次询问,输出一行一个数字表示答案。

分析:

对数组分块,然后对每个分块,记录其内部是否全部相同,如果全部相同,记录该块的值 b l o c k v a l blockval blockval
每次操作 [ l , r , c ] [l,r,c] [l,r,c],查找它包含的所有块。
对于非完整块,暴力操作;
对于完整块,如果块中全同,则比较 b l o c k v a l blockval blockval是否等于 c c c,可以快速统计和修改;如果块中不全同,则暴力操作。
看似暴力操作比较多,但实际上复杂度并不高。
可以这样分析:每次操作时最多有两个非完整区间,这两个区间需要暴力;其他的完整区间如果是块中不全同才需要暴力,然后会变成全同,这个完整块的暴力操作的费用可以提前计算,即在一个完整块由全同变为不全同时,我们将它后面变为全同的费用提前支付。于是每次操作,最多两个区间需要支付暴力的费用,每个区间支付最多两次暴力费用(由全同变为不全同时才支付两次)。一次暴力的费用是 O ( n ) O(\sqrt{n}) O(n ).
所以时间复杂度为 O ( n n ) O(n\sqrt{n}) O(nn )

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
int arr[MAXN], val[400];
int n, l, r, c, block, ans, blockid;
bool same[400];
int modify(int be, int ed) {
  int res = 0;
  blockid = (be - 1) / block;
  int blockst = blockid * block + 1, blocked = min(n, blockid * block + block);
  if (same[blockid] == 1) {
    if (val[blockid] == c)
      res = (ed - be) + 1;
    else {
      res = 0;
      for (int i = blockst; i <= blocked; i++) {
        arr[i] = val[blockid];
        if (i >= be && i <= ed) arr[i] = c;
      }
      same[blockid] = 0;
    }
  } else {
    for (int i = be; i <= ed; i++) {
      if (arr[i] == c) res++;
      arr[i] = c;
    }
  }
  return res;
}
int main() {
  scanf("%d", &n);
  block = ceil(sqrt(n));
  for (int i = 1; i <= n; i++) scanf("%d", &arr[i]);
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d", &l, &r, &c);
    ans = 0;
    if (l % block != 1) {
      int tmpr = min(r, (l - 1) / block * block + block);
      ans += modify(l, tmpr);
      l = tmpr + 1;
    }
    while (l / block <= r / block && r - l + 1 >= block) {
      blockid = (l - 1) / block;
      int tmpr = min(r, blockid * block + block);
      if (same[blockid] == 1) {
        if (val[blockid] == c) ans += block;
      } else {
        for (int j = l; j <= tmpr; j++)
          if (arr[j] == c) ans++;
      }
      same[blockid] = 1;
      val[blockid] = c;
      l += block;
    }
    if (l <= r) {
      ans += modify(l, r);
    }
    printf("%d\n", ans);
  }
  return 0;
}
例9.
题目描述

给出一个长为 n n n的数列,以及 n n n个操作,操作涉及询问区间的最小众数。所谓众数,是指数量最多的数。

输入格式

第一行输入一个数字n。
第二行输入 n n n个数字,第 i i i个数字为 a i a_i ai,以空格隔开。
接下来输入 n n n行询问,每行输入两个数字 l 、 r l、r lr,以空格隔开。
表示查询位于 [ l , r ] [l,r] [l,r]的数字的众数。

输出格式

对于每次询问,输出一行一个数字表示答案。

分析:

这是一个经典难题,来自2013年的国家集训队互测试题。
考虑用分块来做。首先预处理出 c o m m o n n u m [ i ] [ j ] commonnum[i][j] commonnum[i][j],表示分块i到分块j这个区间内的众数。
这样,一次查询[l,r],众数要么是非完整区间内的数,要么完整区间中的 c o m m o n n u m commonnum commonnum中的。非完整区间的数可以遍历,最多 n \sqrt{n} n ;完整块中的数可以O(1)得到,然后对这些可能的众数进行检测,以确定真正的众数。
如果求 c o m m o n n u m [ i ] [ j ] commonnum[i][j] commonnum[i][j]呢?如何检测可能的众数是否是真正的众数呢?这都需要先求出每个数在前缀块中出现的次数—— p r e c n t [ v a l ] [ i ] precnt[val][i] precnt[val][i],它表示 v a l val val在前面的 i i i个块中出现的次数。有了它,一切就好办了。

#include <bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MAXC 355
int ans, anscnt;
int sz, n, m, l, r, arr[MAXN], num[MAXN], tmpcnt[MAXN], blockcnt[MAXN][MAXC];
int commonnum[MAXC][MAXC], commoncnt[MAXC][MAXC], precnt[MAXN][MAXC];
void getint(int &t){
    t = 0;
    char c;
    int flg = 1;
    while((c = getchar()) > '9' || c < '0')if(c == '-')flg = -flg;
    while(c >= '0' && c <= '9')t = t * 10 + c - '0', c = getchar();
    t *= flg; 
}
   
int main(){
    getint(n);
    for(int i = 1; i <= n; i++){
        getint(arr[i]);
        num[i] = arr[i];
    }
    sort(num + 1, num + n + 1);
    int cnt = unique(num + 1, num + n + 1) - num - 1;
    for(int i = 1; i <= n; i++){
        arr[i] = lower_bound(num + 1, num + cnt + 1, arr[i]) - num;
    }
       
    sz = floor(sqrt(n));
    m = ceil(1.0 * n / sz);
    int id;
    for(int i = 1; i <= n; i++){
        id = (i - 1) / sz + 1;
        blockcnt[arr[i]][id]++;
        if(commoncnt[id][id] < blockcnt[arr[i]][id] || (commoncnt[id][id] == blockcnt[arr[i]][id] && arr[i] < commonnum[id][id])){
            commoncnt[id][id] = blockcnt[arr[i]][id];
            commonnum[id][id] = arr[i];
        }
    }
      
    for(int i = 1; i <= cnt; i++){
        for(int j = 1; j <= m; j++)
        precnt[i][j] = precnt[i][j - 1] + blockcnt[i][j];
    }
    for(int i = 1; i <= m; i++){
        for(int j = i + 1; j <= m; j++){
            int l1 = j * sz -sz + 1, r1 = min(n, j * sz);
            commoncnt[i][j] = commoncnt[i][j - 1], commonnum[i][j] = commonnum[i][j - 1];
            for(int k = l1; k <= r1; k++){
                if(precnt[arr[k]][j] - precnt[arr[k]][i - 1] > commoncnt[i][j] || (precnt[arr[k]][j] - precnt[arr[k]][i - 1] == commoncnt[i][j] && arr[k] < commonnum[i][j])){
                    commoncnt[i][j] = precnt[arr[k]][j] - precnt[arr[k]][i - 1];
                    commonnum[i][j] = arr[k];
                }
            }
        }
    }
    for(int i = 1; i <= n; i++){
        getint(l), getint(r);
        int ll = min(r, (l - 1) / sz * sz + sz );
        for(int k = l; k <= ll; k++) tmpcnt[arr[k]]++;
        if((r - 1) / sz != (l - 1) / sz){
            for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
                tmpcnt[arr[k]]++;
            }
        }
        int block_start = (l - 1) / sz + 2, block_end = (r - 1) / sz;
        ans = 0, anscnt = 0;
        if(block_start <= block_end){
        ans = commonnum[block_start][block_end], anscnt = commoncnt[block_start][block_end];
        for(int k = l; k <=ll; k++){
            int tmp = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1];
            if(tmp > anscnt || (tmp == anscnt && arr[k] < ans)){
                anscnt = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1], ans = arr[k];
            }
            tmpcnt[arr[k]] = 0;
            }
            if((r - 1) / sz != (l - 1) / sz){
            for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
                int tmp = tmpcnt[arr[k]] + precnt[arr[k]][block_end] - precnt[arr[k]][block_start - 1];
                if(tmp > anscnt || (tmp == anscnt && arr[k] < ans)){
                anscnt = tmp, ans = arr[k];
            }
            tmpcnt[arr[k]] = 0;
            }
        }
        }
        else{
             for(int k = l; k <=ll; k++){
            if(tmpcnt[arr[k]] > anscnt ||(tmpcnt[arr[k]] == anscnt && arr[k] < ans)){
                anscnt = tmpcnt[arr[k]] , ans = arr[k];
            }
            tmpcnt[arr[k]] = 0;
            }
            if((r - 1) / sz != (l - 1) / sz){
            for(int k = (r - 1) / sz * sz + 1; k <= r; k++){
                if(tmpcnt[arr[k]]  > anscnt ||(tmpcnt[arr[k]] == anscnt && arr[k] < ans)){
                anscnt = tmpcnt[arr[k]], ans = arr[k];
            }
            tmpcnt[arr[k]] = 0;
            }
        }
        }
        printf("%lld\n", num[ans]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值