分块算法思想及应用

分块算法

​ 所谓分块算法,就是将一个完整的区间,分成几块不同的区间,然后对这些区间进行处理。
树状数组以及线段树往往用于处理区间相关问题,树状数组是基于二进制与倍增的思想进行的,线段树则是基于分治的思想,然而并不是所有的区间问题都可以使用线段树或者树状数组解决,只有当区间满足区间可加减性时才能使用。

​ 分块算法的思想在于适当的划分,预处理部分信息,它更接近于一种朴素的算法,效率往往低于线段树以及树状数组。

​ 分块的思路如下:将一个长度为 n n n的序列分为 T T T块,每一块的长度为 n / T n / T n/T

对于每一个 T T T,我们都称为一个整块。

假定要处理的区间为 [ l , r ] [l, r] [l,r]。存在以下几种情况

1、 [ l , r ] [l, r] [l,r]在某个整块内。

一般对于不足整块的区域可以朴素地进行处理,即循环[l,r]的范围进行处理。

2、 [ l , r ] [l, r] [l,r]范围超过一个整块。

假定 l l l处于 p p p段中, r r r处于 q q q段中,那么整个区间 [ l , r ] [l,r] [l,r],可以被划分为三个段处理:

  • l l l p p p段的右边界

  • p + 1 p+1 p+1段到 q − 1 q-1 q1

  • q q q段的左边界到 r r r

同理,对于黄色区域的段,我们可以朴素地处理,而对于绿色的区域则是整段进行维护(可以增加标记等,类似于lazy tag)。

思想

分块算法的思想在于:提前预处理好整块的信息,对于整块的进行标记维护,记录区间的信息,对于不足整块的局部信息则是进行朴素的更新。

来看一道例题:

例题

开关 https://www.luogu.com.cn/problem/P3870

题目描述

现有 n 盏灯排成一排,从左到右依次编号为:1,2,……,n。然后依次执行 m 项操作。

操作分为两种:

指定一个区间 [a,b],然后改变编号在这个区间内的灯的状态(把开着的灯关上,关着的灯打开);

指定一个区间 [a,b],要求你输出这个区间内有多少盏灯是打开的。

灯在初始时都是关着的。

输入格式

第一行有两个整数 n 和 m,分别表示灯的数目和操作的数目。

接下来有 m 行,每行有三个整数,依次为:c、a、b。其中 c 表示操作的种类。

当 c 的值为 0 时,表示是第一种操作。

当 c 的值为 1 时,表示是第二种操作。

a 和 b 则分别表示了操作区间的左右边界。

输出格式

每当遇到第二种操作时,输出一行,包含一个整数,表示此时在查询的区间中打开的灯的数目。

输入样例

4 5
0 1 2
0 2 4
1 2 3
0 2 4
1 1 4

输出样例

1
2

思路:此题的方法很多,在此只考虑分块的做法:1表示开灯,0表示关灯。使用异或操作模拟开关灯,对于每一整块维护一个cnt表示灯的数量,不足整块的朴素进行操作。

参考程序

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

template <class T> 
inline void read (T &x) {
  x = 0; int f = 1; char c = getchar ();
  while (!isdigit (c)) {if (c == '-') f = -1; c = getchar ();}
  while (isdigit (c)) {x = (x << 3) + (x << 1) + c - 48; c = getchar ();}
  x *= f;
}

const int N = 2e5 + 15;
int n;
int a[N];
int sum[N], pos[N], l[N], r[N], tag[N];

void init () {
  int t =  sqrt (n);
  for (int i = 1; i <= t; i ++) {
    l[i] = (i - 1) * t + 1;
    r[i] = i * t;
  }
  if (r[t] < n) {
    t ++;
    l[t] = r[t - 1] + 1;
    r[t] = n;
  }

  for (int i = 1; i <= t; i ++) {
    for (int j = l[i]; j <= r[i]; j ++) {
      pos[j] = i;
//      sum[i] += a[j];
    }
  }

} 

void modify (int x, int y) {
  int p = pos[x], q = pos[y];
  if (p == q) {
    for (int i = x; i <= y; i ++) {
      a[i] ^= 1;
      if (a[i]) sum[p] ++;
      else sum[p] --;
    }
  } else {
    for (int i = p + 1; i <= q - 1; i ++) tag[i] ^= 1;
    for (int i = x; i <= r[p]; i ++) {
      a[i] ^= 1;
      if (a[i]) sum[p] ++;
      else sum[p] --;
    }
    for (int i = l[q]; i <= y; i ++) {
      a[i] ^= 1;
      if (a[i]) sum[q] ++;
      else sum[q] --;
    }
  }
}

int query (int x, int y) {
  int p = pos[x], q = pos[y], ans = 0;
  if (p == q) {
    for (int i = x; i <= y; i ++) ans += a[i] ^ tag[p]; 
  } else {
    for (int i = p + 1; i <= q - 1; i ++) {
      if (!tag[i]) ans += sum[i];
      else ans += (r[i] - l[i] + 1 - sum[i]);
    }
    for (int i = x; i <= r[p]; i ++) ans += a[i] ^ tag[p];
    for (int i = l[q]; i <= y; i ++) ans += a[i] ^ tag[q];
  } 
  return ans;
}

int main () {
  int tt, c, x, y;
  read (n), read (tt);
  init ();
  while (tt --) {
    read (c), read (x), read (y);
    if (c == 0) {
      modify (x, y);
    } else {
      cout << query (x, y) << endl;
    }
  }
  return 0;
}

再来看一道已经用线段树解决的简单问题

A Simple Problem with Integers (http://poj.org/problem?id=3468)

Description

You have N integers, A1, A2, … , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.

Input

The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.

The second line contains N numbers, the initial values of A1, A2, … , AN. -1000000000 ≤ Ai ≤ 1000000000.

Each of the next Q lines represents an operation.

“C a b c” means adding c to each of Aa, Aa+1, … , Ab. -10000 ≤ c ≤ 10000.

“Q a b” means querying the sum of Aa, Aa+1, … , Ab.

Output

You need to answer all Q commands in order. One answer in a line.

Sample Input

10 5

1 2 3 4 5 6 7 8 9 10

Q 4 4

Q 1 10

Q 2 4

C 3 6 3

Q 2 4

Sample Output

4

55

9

15

Hint

The sums may exceed the range of 32-bit integers.

题目大意:区间加法,区间求和。直接分为T块,每块长度sqrt(n),对于不足整块的朴素的处理,涉及到整块的,进行标记(类似于线段树的lazy tag)。

参考程序:

#include <iostream> 
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
const int N = 1e5 + 15;

ll sum[N], add[N], a[N];
int l[N], r[N], pos[N]; // 记录每一段的左右端点,以及当前这个值在第几段中 
int n;

void init () { // 预处理
  int t = sqrt (n);
  for (int i = 1; i <= t; i ++) {
    l[i] = (i - 1) * t + 1;
    r[i] = i * t; 
  } 
  if (r[t] < n) { // 最后一段不足sqrt (n)的区域 
    t ++;
    l[t] = r[t - 1] + 1;
    r[t] = n;
  }

  for (int i = 1; i <= t; i ++) {
    for (int j = l[i]; j <= r[i]; j ++) {
      sum[i] += a[j];
      pos[j] = i;
    }
  }   
}

void modify (int x, int y, ll d) {
  int p = pos[x], q = pos[y];
  if (p == q) { // 在同一段内 
    for (int i = x; i <= y; i ++)
      a[i] += d;
    sum[p] += d * (y - x + 1);
  } else {
    for (int i = p + 1; i <= q - 1; i ++) add[i] += d;
    for (int i = x; i <= r[p]; i ++) a[i] += d;
    sum[p] += (r[p] - x + 1) * d;
    for (int i = l[q]; i <= y; i ++) a[i] += d;
    sum[q] += (y - l[q] + 1) * d;
  }
}

ll query (int x, int y) {
  int p = pos[x], q = pos[y];
  ll ans = 0;
  if (p == q) {
    for (int i = x; i <= y; i ++) ans += a[i];
    ans += add[p] * (y - x + 1);
  }else {
    for (int i = p + 1; i <= q - 1; i ++) ans += sum[i] + add[i] * (r[i] - l[i] + 1);
    for (int i = x; i <= r[p]; i ++) ans += a[i];
    ans += add[p] * (r[p] - x + 1);
    for (int i = l[q]; i <= y; i ++) ans += a[i];
    ans += add[q] * (y - l[q] + 1);
  }
  return ans ;
}

int main () {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int tt;
  cin >> n >> tt;
  for (int i = 1; i <= n; i ++) cin >> a[i];
  char op;
  int x, y;
  ll d;
  init ();
  while ( tt -- ) {
    cin >> op;
    if (op == 'C') {
      cin >> x >> y >> d;
      modify (x, y, d);
    } else {
      cin >> x >> y;
      cout << query (x, y) << endl; 
    }
  }
  return 0;
}

再来看一道较难的题目

[Violet]蒲公英 (https://www.luogu.com.cn/problem/P4168)

题目描述

在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关。

为了简化起见,我们把所有的蒲公英看成一个长度为 n 的序列 a1,a2…an,其中 ai 为一个正整数,表示第 i 棵蒲公英的种类编号。

而每次询问一个区间 [l, r],你需要回答区间里出现次数最多的是哪种蒲公英,如果有若干种蒲公英出现次数相同,则输出种类编号最小的那个。

注意,你的算法必须是在线的。

输入格式

第一行有两个整数,分别表示蒲公英的数量 n 和询问次数 m。

第二行有 n 个整数,第 i 个整数表示第 i 棵蒲公英的种类 ai 。

接下来 m 行,每行两个整数 l0, r0 ,表示一次询问。输入是加密的,解密方法如下:

令上次询问的结果为 x(如果这是第一次询问,则 x = 0),设 l=((l0+x-1) mod n) + 1,r=((r0+x-1) mod n) + 1。如果 l > r,则交换 l, r。

最终的询问区间为计算后的 [l, r]。

输出格式

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

输入样例

6 3 
1 2 3 2 1 2 
1 5 
3 6 
1 5

输出样例

1 
2 
1

思路:题目的大意是,求区间众数。由于区间众数不满足区间可加性(两个子区间合并之后父区间,其区间众数不能由两个子区间得出),因此无法使用线段树,只好分块进行。

查询 [l, r]区间的众数时。可以分为两种情况探讨

1、[l, r]被包含在某个块中,此时朴素地查询即可

2、[l, r] 跨越不同的块。对于整块的部分,需要提前进行预处理。

由于要求区间的众数,因此我们要记录下每一个数字出现的次数cnt.

另外假定sum[i] [j] 表示从头开始到第i块中,j出现的次数,如果要求第x块到第y块中j出现的次数可以用:sum[y] [j] - sum[x - 1] [j], 类似于前缀和的思想。

m[i] [j]则表示块i到块j中最小的区间众数。

有了上述的几个数组,对于第二种情况,假定l处于x块,r处于y块,答案只可能出现在[x+1, y-1]的区间众数,或者x/y块中的某个数字,只需要把两边不足整块的区域,每一个数字都枚举一下,比较是否能成为答案即可。

注意:题目中的数字可能很大,需要离散化

参考程序:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>

using namespace std;

const int N = 1e5, M = 400;

int n;

vector <int> alls;

int sum[M][N], m[M][M]; // sum[i][j]表示从头到第i块中,j出现的次数。m[i][j] 表示第i块到第j块的区间众数
int a[N], pos[N], cnt[N]; // pos[i]表示i在第几块中,cnt[i]表示i出现的次数

int find (int x) {
    int l = 0, r = alls.size() - 1;
    while (l < r) {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;
}

void init () {
    int t = sqrt (n);
    for (int i = 1; i <= n; i ++) 
        pos[i] = (i - 1) / t + 1;

    for (int i = 1; i <= pos[n]; i ++) { // 枚举每一块的情况
        for (int j = 1; j <= alls.size (); j ++) sum[i][j] = sum[i - 1][j];
        for (int j = (i - 1) * t + 1; j <= n and j <= i * t; j ++) sum[i][a[j]] ++;
    }

    for (int i = 1; i <= pos[n]; i ++) {
        int ans = 0, res = 0; // ans用于表示区间众数、res表示该众数出现的次数
        for (int j = 1 + (i - 1) * t; j <= n; j ++) {
            cnt[a[j]] ++; 
            if (cnt[a[j]] > res || cnt[a[j]] == res and  alls[a[j] - 1] < alls[ans - 1]) {
                res = cnt[a[j]];
                ans = a[j];
            }
            m[i][pos[j]] = ans; // i块到j块的区间众数
        }
        memset (cnt, 0, sizeof cnt);
    }
    
}

int query (int l, int r) {
    int t = sqrt (n);
    int x = pos[l], y = pos[r]; // 找到l,r属于哪一块
    int ans = m[x + 1][y - 1], res = max (0, sum[y - 1][ans] - sum[x][ans]); // ans表示块x+1到y-1的最小众数,res表示从x+1到y-1块中ans的出现次数
    int lim = min (x * t, r), f = 0; // lim表示l点所在块的右边界

    if (x != y) f = 1;
    for (int i = l; i <= lim; i ++) 
        cnt[a[i]] = max (0, sum[y - 1][a[i]] - sum[x][a[i]]);
    if (f) {
        for (int i = (y - 1) * t + 1; i <= r; i ++)     
            cnt[a[i]] = max (0, sum[y - 1][a[i]] - sum[x][a[i]]);
    }

    for (int i = l; i <= lim; i ++) { // 枚举l所在的块是否有可能成为答案
        cnt[a[i]] ++;
        if (cnt[a[i]] > res || cnt[a[i]] == res and alls[a[i] - 1] < alls[ans - 1]) {
            res = cnt[a[i]];
            ans = a[i];
        }
    }
    if (f) {
        for (int i = (y - 1) * t + 1; i <= r; i ++) { // 枚举r所在的块是否有可能成为答案
            cnt[a[i]] ++;
            if (cnt[a[i]] > res || cnt[a[i]] == res and alls[a[i] - 1] < alls[ans - 1]) {
                res = cnt[a[i]];
                ans = a[i];
            }
        }
    }
    return ans;
}

int main () {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int tt;
    cin >> n >> tt;
    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        alls.push_back (a[i]);
    } 

    sort (alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    for (int i = 1; i <= n; i++) 
        a[i] = find(a[i]);
    
    init ();
    int x = 0, l, r;
    while ( tt -- ) {
        cin >> l >> r;
        l = (l + x - 1) % n + 1, r = (r + x - 1) % n + 1;
        if (l > r) swap (l, r);
        x = alls[query (l, r) - 1];
        cout << x << endl;
    }
    return 0;
}

小结

分块的核心思想:整段维护,局部朴素。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值