Loading

分块小记

普通分块

分块思想是什么?(引自 OI-wiki

分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
当然,分块的缺点是渐进意义的复杂度,相较于线段树和树状数组不够好。

例题

1.守墓人

题目大意

原题见 Luogu P2357 守墓人
有一个长为 \(n\) 的序列,一共有 \(m\) 此操作,
每次操作会输入 \(opt\),对应操作如下:
1.\(opt = 1\),将 \([l,r]\) 这个区间所有数的值增加 \(k\)
2.\(opt = 2\), 将 \([1,1]\) 值增加 \(k\)
3.\(opt = 3\),将 \([1,1]\) 值减少 \(k\)
4.\(opt = 4\),统计 \([l,r]\) 的区间和
5.\(opt = 5\),求 \([1,1]\) 的区间和

代码(思路见注释)

点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define DB double
#define ST string

using namespace std;

const int N = 200010;
int n, m;
int idx[N], klen;
// 所在位置公式(下表为 i):(i - 1) / len + 1(len 为块长) 
int opt, l, r;
LL a[N], tag[N], sum[N], x; // 记得开 long long 

// 计算一个块的左端点 
int L(int x)
{ return klen * (x - 1) + 1; }

// 计算一个块的右端点 
int R(int x) 
{ 
	return min(n, x * klen); 
	// 记得取 min,因为最后一个块不一定是整个 
}

void add(int l, int r, int x)
{
	// 如果左右段点均在同一个块中,就暴力处理 
	if(idx[l] == idx[r])
	{
		for(int i = l; i <= r; i ++ )
			a[i] += x, sum[idx[i]] += x;
	}
	else
	{
		// 将左边的散块暴力处理 
		for(int i = l; i <= R(idx[l]); i ++ )
			a[i] += x, sum[idx[i]] += x;
		// 统一处理中间的整块 
		for(int i = idx[l] + 1; i <= idx[r] - 1; i ++ )
		{
			tag[i] += x; // 懒惰标记,统计散块和的时候加上
			sum[i] += (R(i) - L(i) + 1) * x;
		}
		// 将右边的散块暴力处理
		for(int i = L(idx[r]); i <= r; i ++ )
			a[i] += x, sum[idx[i]] += x;
	}
}

LL count(int l, int r)
{
	LL res = 0;
	
	if(idx[l] == idx[r])
	{
		for(int i = l; i <= r; i ++ )
			res += a[i] + tag[idx[i]]; // 记得加上懒惰标记中的值 
	}
	else
	{
		for(int i = l; i <= R(idx[l]); i ++ )
			res += a[i] + tag[idx[i]];
		for(int i = idx[l] + 1; i <= idx[r] - 1; i ++ )
			res += sum[i];
		for(int i = L(idx[r]); i <= r; i ++ )
			res += a[i] + tag[idx[i]];
	}
	
	return res;
}

signed main()
{
	scanf("%d%d", &n, &m);
	// 预处理出块长 
	klen = sqrt(n);
	for(int i = 1; i <= n; i ++ )
	{
		scanf("%lld", &a[i]);
		idx[i] = (i - 1) / klen + 1;
		sum[idx[i]] += a[i];
	}
	
	for(int i = 1; i <= m; i ++ )
	{
		scanf("%d", &opt);
		if(opt == 1)
		{
			scanf("%d%d%d", &l, &r, &x);
			add(l, r, x);
		}
		if(opt == 2)
			scanf("%d", &x), add(1, 1, x);
		if(opt == 3)
			scanf("%d", &x), add(1, 1, -x);
		if(opt == 4)
		{
			scanf("%d%d", &l, &r);
			printf("%lld\n", count(l, r));
		}
		if(opt == 5)
			printf("%lld\n", count(1, 1));
	}
	
	return 0;
}

2.蒲公英

题目大意

AcWing 249. 蒲公英

做题思路

\(s[p][x]\) 记录第 \(1~p\) 块中数 \(x\) 出现的次数
(tip:因为 \(x\) 比较大,所以要将 \(x\) 离散化)。
所以数 \(x\)\(p~q\) 块中出现次数即为 \(s[p][x]-s[q-1][x]\)
再用 \(f[p][q]\) 维护在 \(p~q\) 块中的众数。

对于一段(\(l~r\)),若 \(l\)\(r\) 在同一块,就暴力记录数出现的次数。
否则枚举左边散数和右边散数,如果这一个数是第一次出现,
那就把它在整块中出现的次数加上去(即 \(s[id[r]-1][x]-s[id[l]][x]\)),
最后把它与整快中的众数(即 \(f[id[l]+1][id[r]-1]\))作比较即可。

最后记得输出离散前的答案。

复杂度:\(O(N\sqrt{N})\)

代码

点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define DB double
#define ST string
#define endl '\n'

using namespace std;

const int N = 40010, M = 310;
int n, m, len, tot, a[N];
int id[N], s[M][N], f[M][M];
vector <int> mp1;
int l, r, lst; 
int num, cnt, numx, cntx, c[N];
unordered_map <int, int> mp2;

int L(int x)
{ return (x - 1) * len + 1; }

int R(int x)
{ return min(n, x * len); }

int query(int l, int r)
{
    int num, cnt = 0;

    if(id[l] == id[r])
    {
        for(int i = l; i <= r; i ++ )
        {
            c[a[i]] ++ ;
            if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
                cnt = c[a[i]], num = a[i];
        }
        for(int i = l; i <= r; i ++ )
            c[a[i]] = 0;
    }
    else
    {
        num = f[id[l] + 1][id[r] - 1];
        cnt = s[id[r] - 1][num] - s[id[l]][num];
        for(int i = l; i <= R(id[l]); i ++ )
        {
            if(c[a[i]] == 0)
                c[a[i]] += s[id[r] - 1][a[i]] - s[id[l]][a[i]];
            c[a[i]] ++ ;
            if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
                cnt = c[a[i]], num = a[i];
        }
        for(int i = L(id[r]); i <= r; i ++ )
        {
            if(c[a[i]] == 0)
                c[a[i]] += s[id[r] - 1][a[i]] - s[id[l]][a[i]];
            c[a[i]] ++ ;
            if(c[a[i]] > cnt || (c[a[i]] == cnt && a[i] < num))
                cnt = c[a[i]], num = a[i];
        }
        for(int i = l; i <= R(id[l]); i ++ )
            c[a[i]] = 0;
        for(int i = L(id[r]); i <= r; i ++ )
            c[a[i]] = 0;
    }

    return mp1[num];
}

signed main()
{
    scanf("%d%d", &n, &m);
    len = sqrt(n);
    for(int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
        mp1.push_back(a[i]);
        id[i] = (i - 1) / len + 1;
    }

    sort(mp1.begin(), mp1.end());
    tot = unique(mp1.begin(), mp1.end()) - mp1.begin();

    for(int i = 0; i < tot; i ++ )
        mp2[mp1[i]] = i;

    for(int i = 1; i <= n; i ++ )
        a[i] = mp2[a[i]];

    for(int i = 1; i <= id[n]; i ++ )
        for(int j = L(i); j <= R(i); j ++ )
            for(int k = i; k <= id[n]; k ++ )
                s[k][a[j]] ++ ;

    for(int i = 1; i <= id[n]; i ++ )
    {
        for(int j = i; j <= id[n]; j ++ )
        {
            num = f[i][j - 1];
            cnt = s[j][num] - s[i - 1][num];
            for(int k = L(j); k <= R(j); k ++ )
            {
                numx = a[k];
                cntx = s[j][numx] - s[i - 1][numx];
                if(cntx > cnt || (cntx == cnt && numx < num))
                    num = numx, cnt = cntx;
            }
            f[i][j] = num;
        }
    }

    while(m -- )
    {
        scanf("%d%d", &l, &r);
        l = ((l + lst - 1) % n) + 1;
        r = ((r + lst - 1) % n) + 1;
        if(l > r) swap(l, r);
        lst = query(l, r);
        printf("%d\n", lst);
    }

    return 0;
}

3.磁力块

题目大意

AcWing 250. 磁力块

做题思路

发现:可以将 \(x\)\(y\) 转化为与 \(x0\)\(y0\) 的距离,
但是为了避免精度问题,可以不对距离取平方根,而将所有的磁吸半径取平方(!!!)。

所以可以先将每一个磁力块按它与原点(即小取酒的位置)的距离先按从低到高排序,
对于每一块(块长根号N)再按它内部的磁块重量从小到大排序,并记录这个段中的最重磁块重量。

处理:
可以使用 BFS,在队列中存储未被使用的磁力块,每一次取出队头的磁力块属性。

接着从前到后枚举所有的块,若这个块的最大质量已经超过了当前磁力块的磁性,
那么就退出循环,进行散块操作
(即从前到后遍历整个块中的磁力块,若可以取,则标记为已取并加入磁力块队列),
否则就从上一次去到的磁力块的后面(记录在 \(lst[i]\) 中)的块遍历,
若该块的质量大于当前磁力块的磁力,那么退出循环(记得边做边更改 \(lst[i]\))。

tip:判断 这个块的最大质量已经超过了当前磁力块的磁性 需要放在操作之前。

复杂度:\(O(N\sqrt{N})\)

代码

点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define int long long
#define DB double
#define y0 zzxdalao

using namespace std;

const int N = 250010, M = 510;
int n, t, k, tot;
int x0, y0;
int mx[N], lst[N];
bool used[N];

struct Node
{
	int w, m, p, r;
}a[N];

int L(int x)
{ return (x - 1) * t + 1; }

int R(int x)
{ return min(x * t, n); }

signed main()
{
	scanf("%lld%lld", &x0, &y0);
	scanf("%lld%lld%lld", &a[0].p, &a[0].r, &n);
	// 记得平方
	a[0].r = a[0].r * a[0].r;
	
	t = sqrt(n), k = (n - 1) / t + 1;
	for(int i = 1, x, y; i <= n; i ++ )
	{
		scanf("%lld%lld", &x, &y);
		// 将 x 和 y 转化为与小取酒的距离
		a[i].w = pow(x - x0, 2) + pow(y - y0, 2);
		scanf("%lld%lld%lld", &a[i].m, &a[i].p, &a[i].r);
		// 记得平方
		a[i].r = a[i].r * a[i].r; 
	}
	
	// 按距离排序
	sort(a + 1, a + 1 + n, [&](Node x, Node y){ return x.w < y.w; });
	
	// 初始化
	for(int i = 1; i <= k; i ++ )
	{
	    lst[i] = L(i) - 1;
	    mx[i] = a[R(i)].w;
		sort(a + L(i), a + 1 + R(i), [&](Node x, Node y){ return x.m < y.m; });
	}
	
	queue <Node> q;
	q.push(a[0]);
	while(q.size())
	{
		Node frn = q.front(); q.pop();
		int i;
		// 整块处理
		for(i = 1; i <= k; i ++ )
		{
		    // 记得将这句话放在 for 循环之前
			if(mx[i] > frn.r) break;
			for(int j = lst[i] + 1; j <= R(i); j ++ )
			{
				if(a[j].m > frn.p) break;
				lst[i] = j;
				if(used[j]) continue;
				used[j] = 1;
				q.push(a[j]);
				tot ++ ;
			}
		}
		// 散块处理
		for(int j = lst[i] + 1; j <= R(i); j ++ )
		{
		    if(a[j].m <= frn.p && !used[j] && a[j].w <= frn.r)
		        used[j] = 1, tot ++ , q.push(a[j]);
		}
	}
	
	printf("%lld\n", tot);
	
	return 0;
}

莫队

莫队是什么?

我们令 \(f(l, r)\) 表示题目中给出的通过区间 \([l, r]\) 中算出的值(满足 \(f(l, r)\) 可以由 \(f(l, r - 1)\) \(f(l + 1, r)\) \(f(l - 1, r)\) \(f(l, r +1)\) 计算出来)
但是一定要记录非常多的值才能计算出 \(f(l, r)\),此时若分别记录在每一个块中,整块处理会十分的麻烦(甚至可能会退化)。

所以就可以将双指针与分块结合起来,即沿用上一次处理出的结果进行处理得出这一次处理的结果。
我们先用 \(l\)\(r\) 记录上一次计算出的区间,对于这一次处理区间的左端点和右端点进行移动,移动结束后记录答案。

为了防止退化,我们应该将询问区间进行双关键字排序,即:
若两区间左端点处于同一块中,按右端点升序排序;反之按左端点(其实就是左端点所处的块)排序。

例题

1.小Z的袜子

题目思路

Luogu P1494 [国家集训队] 小 Z 的袜子

做题思路

先将所有的询问按左端点排序,接着将左端点在同一个块中的询问按右端点排序。

\(l\)\(r\) 记录当前覆盖区间的左端点和右端点,再用 \(cur\) 记录现在的分子(分母可以直接计算)
接着一个一个块处理,对于每一个块中的询问的左端点和右端点进行对 \(l\)\(r\) 的移动,
边移动 \(l\)\(r\),边计算 \(cur\) 的值(见前置),移动完成后赋值分子的值
(此处需要特判 \(cur\) 是否等于 0,若等于 0,则要讲分母改为 1)。

最终按提前记录的 \(id\) 升序排序,输出即可。

tip:
1.\(\frac{x(x + 1)}{2} - \frac{x(x - 1)}{2} = \frac{x(x + 1 - x + 1)}{2} = \frac{2x}{2} = x\)
2.记录 \(l\)\(r\) 的时候需要用 long long 记录,因为计算 \(\frac{len \times (len - 1)}{2}\) 的时候,\(len \times (len - 1)\) 最大为 25 亿,会爆 int。

复杂度:\(O(N\sqrt{N})\)

代码

点击查看代码
#include <bits/stdc++.h>
#define PII pair <int, int>
#define LL long long
#define x first
#define y second

using namespace std;

const int N = 100010;
int n, m, a[N];
int L[N], R[N];
LL c[N];

struct Node
{
	LL l, r, p, q;
	int id;
}q[N];

LL gcd(LL x, LL y)
{ return y ? gcd(y, x % y) : x; }

signed main()
{
    scanf("%d%d", &n, &m);
    int len = sqrt(n);
    int k = (n - 1) / len + 1;
    for(int i = 1; i <= n; i ++ )
        scanf("%d", &a[i]);
    for(int i = 1; i <= m; i ++ )
    {
        q[i].id = i;
        scanf("%lld%lld", &q[i].l, &q[i].r);
        q[i].q = (q[i].r - q[i].l + 1ll) * (q[i].r - q[i].l) / 2ll;
    }
    
    sort(q + 1, q + 1 + m, [&](Node x, Node y){ return x.l < y.l; });
    for(int i = 1; i <= k; i ++ )
    {
        L[i] = R[i - 1] + 1;
        R[i] = R[i - 1];
        while(R[i] < m && q[R[i] + 1].l <= i * len) R[i] ++ ;
        sort(q + L[i], q + 1 + R[i], [&](Node x, Node y){ return x.r < y.r; });
    }
    
    c[0] ++ ;
    for(int i = 1, l = 0, r = 0, cur = 0; i <= k; i ++ )
    {
        for(int j = L[i]; j <= R[i]; j ++ )
        {
            while(r < q[j].r) cur += c[a[ ++ r]], c[a[r]] ++ ;
            while(l > q[j].l) cur += c[a[ -- l]], c[a[l]] ++ ;
            while(r > q[j].r) c[a[r]] -- , cur -= c[a[r -- ]];
            while(l < q[j].l) c[a[l]] -- , cur -= c[a[l ++ ]];
            
            if(!cur) q[j].q = 1;
            else
            { int d = gcd(cur, q[j].q); q[j].p = cur / d, q[j].q /= d; }
            
        }
    }
    
    sort(q + 1, q + 1 + m, [&](Node x, Node y){ return x.id < y.id; });
    for(int i = 1; i <= m; i ++ )
        printf("%lld/%lld\n", q[i].p, q[i].q);
    
	return 0;
}
posted @ 2025-03-06 18:21  Hyvial  阅读(30)  评论(0)    收藏  举报