分块练习整理

定义

分块,无非就是将长为 n n n 的序列分为长度为 n \sqrt{n} n 的若干小块,以每次处理或查询时提高效率。说白了就是优雅的暴力。

块内排序

一、Problem G: [loj6278]数列分块入门 2

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

思路

考虑到查询,就要使块内有序,即维护块的单调性。

预处理:逐一对块内排序,复杂度 O(nlogn) \text{O(nlogn)} O(nlogn)

所以修改:

  • 散块:暴力,逐一加上,然后重新排序以维护单调性;

  • 整块:修改 lazytag \text{lazytag} lazytag,不用再次排序。

最后,查询:

  • 散块:对每个元素逐一判断、统计即可。

  • 整块:二分思想,因为有序所以直接 lowerbound \text{lowerbound} lowerbound 即可。

这样总复杂度就是 O ( n l o g n + n n l o g n ) O(nlogn +n \sqrt{n} log \sqrt{n}) O(nlogn+nn logn )

代码

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define maxn 50005
#define maxm 750
#define rep(i, a, b) for(int i = a; i <= b; ++i)

int n, val[maxn];
int lz[maxm], blk[maxn], len;
vector <int> v[maxm];
int opt, a, b, c;

inline int gb(int x)
{
	int res = x / len;
	if(x % len) res++;
	return res;
}

inline void reset(int x)
{
	v[x].clear();
	rep(i, (x - 1) * len + 1, x * len)	
		v[x].push_back(val[i]);
	sort(v[x].begin(), v[x].end());
}

inline void updt(int l, int r, int k)
{
	rep(i, l, min(r, gb(l) * len))
		val[i] += k;
	reset(gb(l));
	if(gb(l) != gb(r))
	{
		rep(i, (gb(r) - 1) * len + 1, r)
			val[i] += k;
		reset(gb(r));
	}
	rep(i, gb(l) + 1, gb(r) - 1)
		lz[i] += k;
}

inline void qry(int l, int r, int k)
{
	int ans = 0;
	rep(i, l, min(r, gb(l) * len))
		if(val[i] + lz[gb(i)] < k) ans += 1;
	if(gb(l) != gb(r))
		rep(i, (gb(r) - 1) * len + 1, r)
			if(val[i] + lz[gb(i)] < k) ans += 1;
	rep(i, gb(l) + 1, gb(r) - 1)
		ans += lower_bound(v[i].begin(), v[i].end(), k - lz[i]) - v[i].begin();
	printf("%lld\n", ans);
}

signed main()
{
	scanf("%lld", &n);
	len = sqrt(n);
	rep(i, 1, n) scanf("%lld", &val[i]);
	rep(i, 1, n)
		v[gb(i)].push_back(val[i]);
	rep(i, 1, (n - 1) / len + 1)
		sort(v[i].begin(), v[i].end());
	rep(i, 1, n)
	{
		scanf("%lld %lld %lld %lld", &opt, &a, &b, &c);
		if(opt) qry(a, b, c * c);
		else updt(a, b, c);
	}
	return 0;
}

二、P5356 [Ynoi2017] 由乃打扑克

一道搞了我一上午加一中午的题。

给定长为 n n n 的序列,操作包含区间加和求区间第 k k k 小的数。

思路

和上一题一样,要维护块内的单调性。

所以对于预处理和区间加的操作一样。

查询:

二分,因为这题值域较小,所以 r \text{r} r 取区间 [ l , r ] [l,r] [l,r] 的最大者, l l l 取区间 [ l , r ] [l,r] [l,r] 最小值即可。每次使用 check \text{check} check 函数检查当前 mid \text{mid} mid 值在区间 [ l , r ] [l,r] [l,r] 中的排名,直到找到答案。

check \text{check} check 函数:

每次给定区间范围 [ l , r ] [l,r] [l,r] 和当前 mid \text{mid} mid 值。

  • 散块:逐一遍历,统计 ≤ mid \leq \text{mid} mid 的值;

  • 整块:因为块内有序,所以使用二分找到符合条件的数的数量即可。

  • 注意:对于上述整块的遍历,要添加一些特判以减少时间;不要为了省码量用 upperbound \text{upperbound} upperbound,会 TLE \text{TLE} TLE

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;

#define int long long
#define maxn 1000010
#define maxm 500010
#define rep(i, a, b) for(int i = a; i <= b; ++i)

int n, m, a[maxn];
int opt, l, r, k;
int len = 150, tot;
vector <int> v[maxm];
int lz[maxm];

inline int get(int x)
{
	int res = x / len;
	if(x % len != 0) res++;
	return res;
}

inline void reset(int x)
{
	v[x].clear();
	rep(i, (x - 1) * len + 1, min(n, x * len))
		v[x].push_back(a[i]);
	sort(v[x].begin(), v[x].end());
}

inline void updt(int l, int r, int k)
{
	int posl = get(l), posr = get(r);
	if(posl == posr)
	{
		rep(i, l, r) a[i] += k;
		reset(posl);
		return;
	}
	rep(i, l, posl * len)
		a[i] += k;
	reset(posl);
	rep(i, (posr - 1) * len + 1, r)
		a[i] += k;
	reset(posr);
	rep(i, posl + 1, posr - 1)
		lz[i] += k;
}

inline int check(int l, int r, int mid)
{
	int cnt = 0, posl = get(l), posr = get(r);
	if(posl == posr)
	{
		rep(i, l, r) 
			if(a[i] + lz[posl] <= mid) cnt += 1;
		return cnt;	
	}
	rep(i, l, posl * len)
		if(a[i] + lz[posl] <= mid) cnt++;
	rep(i, (posr - 1) * len + 1, r)
		if(a[i] + lz[posr] <= mid) cnt++;
	rep(i, posl + 1, posr - 1)
	{
		int L = 0, R = len - 1;
		if (v[i][0] + lz[i] > mid) continue;
		if(v[i][len - 1] + lz[i] <= mid)
		{
			cnt += len;
			continue;
		}
		while(L < R)
		{
			int Mid = (L + R) / 2 + 1;
			if(v[i][Mid] + lz[i] <= mid) L = Mid;
			else R = Mid - 1;
		}
		if(v[i][L] + lz[i] <= mid) cnt += L - 0 + 1;
	}
	return cnt;
}

inline int min_(int x,int y)
{
	int posl = get(x), posr = get(y);
	int ans = 2147483647;
	if(posl == posr)
	{
		rep(i, x, y) ans = min(ans, a[i] + lz[get(i)]);
		return ans;
	}
	rep(i, x, posl * len) ans = min(ans,a[i] + lz[get(i)]);
	rep(i, posl + 1, posr - 1) ans = min(ans, v[i][0] + lz[i]);
	rep(i, (posr - 1) * len + 1, y) ans = min(ans, a[i] + lz[get(i)]);
	return ans;
}

inline int max_(int x,int y)
{
	int posl = get(x), posr = get(y);
	int ans = -2147483647;
	if(posl == posr)
	{
		rep(i, x, y) ans = max(ans, a[i] + lz[get(i)]);
		return ans;
	}
	rep(i, x, posl * len) ans = max(ans,a[i] + lz[get(i)]);
	rep(i, posl + 1, posr - 1) ans = max(ans, v[i][len - 1] + lz[i]);
	rep(i, (posr - 1) * len + 1, y) ans = max(ans, a[i] + lz[get(i)]);
	return ans;
}

inline int qry(int l, int r, int k)
{
	if(k > r - l + 1 or k < 1) return -1;
	int posl = get(l), posr = get(r);
	int ans = -1, L = min_(l, r), R = max_(l, r);
	while(L <= R)
	{
		int mid = (L + R) >> 1;
		if(check(l, r, mid) < k) L = mid + 1;
		else ans = mid, R = mid - 1;
	}
	return ans;
}

signed main()
{
	scanf("%lld %lld", &n, &m);
	tot = ceil(n * 1.0 / len);
	rep(i, 1, n) 
	{
		scanf("%lld", &a[i]);	
		v[get(i)].push_back(a[i]);
	}
	rep(i, 1, tot) sort(v[i].begin(), v[i].end());
	rep(i, 1, m)
	{
		scanf("%lld %lld %lld %lld", &opt, &l, &r, &k);
		if(opt == 1)
			printf("%lld\n", qry(l, r, k));
		else updt(l, r, k);
	}
	return 0;
}

预处理和考虑影响

一、P4168 [Violet]蒲公英

给定长为 n n n 的序列和 m m m 次询问,每次询问区间 [ l , r ] [l,r] [l,r] 中最小的众数。

思路

众数:出现次数最多的数。

  1. 预处理

    为了方便后面的计算,这里要处理两个数组:

    1. 定义 P i , j P_{i,j} Pi,j 表示第 i \text{i} i j \text{j} j 个块中最小的众数;

    2. 定义 S i , j S_{i,j} Si,j 表示前 i \text{i} i (含第 i \text{i} i)个块中 j \text{j} j 这个数的出现次数(类似前缀和)。

  2. 查询

    分两种情况讨论。

    1. l \text{l} l r \text{r} r 分别所在的块相邻或相同:直接暴力遍历、统计即可。

    2. l \text{l} l r \text{r} r 分别所在的块不相邻:

      • 整块:

        对于整块中的数的答案就是 P p o s l + 1 , p o s r − 1 P_{posl+1,posr-1} Pposl+1,posr1 + 在散块中的出现次数( posl \text{posl} posl表示 l \text{l} l 所在的块, posr \text{posr} posr 同理)。

      • 散块:

        设一散块中的数为 $\text{x}$。那么它出现的次数为:在区间 $[l,posl* len]$ 中出现的次数 + 在整块 $[posl + 1,posr-1]$ 中出现的次数 + 在区间 $[(posr-1)* len + 1,r]$ 中出现的次数。
        
        也就是:在区间 $[l,posl* len]$ 中出现的次数 + $S_{posr-1,x}-S_{posl,x}$ + 在区间 $[(posr-1)* len + 1,r]$ 中出现的次数。
        
        第一项和第三项可以暴力统计,这样就可以得到答案了,再比较取最大值。
        

注意要先离散化数列中的值。

代码

#include<bits/stdc++.h>
using namespace std;

#define maxn 220
#define maxm 40040
#define rep(i, a, b) for(int i = a; i <= b; ++i)

int n, m, lst;
int s[maxn][maxm];
int pre[maxm], tmp[maxm];
struct node{
	int org, al, id;
}a[maxm];
int len, tot, vis[maxm];
struct node2{
	int num, tim;
}mxx, p[maxn][maxn], ans;
int l, r, posl, posr;

inline bool cmp1(node x, node y)
{
	return x.org < y.org;
}

inline bool cmp2(node x, node y)
{
	return x.id < y.id;
}

inline int getpos(int x)
{
	int res = x / len;
	if(x % len) return res + 1;
	return res;
}

int main()
{
	scanf("%d %d", &n, &m);
	len = sqrt(n), tot = (n - 1) / len + 1;
	rep(i, 1, n) 
		scanf("%d", &a[i].org), a[i].id = i;
	sort(a + 1, a + n + 1, cmp1);
	a[0].org = -1;
	rep(i, 1, n)
	{
		a[i].al = a[i - 1].al;
		if(a[i].org != a[i - 1].org) a[i].al += 1;
		pre[a[i].al] = a[i].org;
	}
	sort(a + 1, a + n + 1, cmp2);
   //输入+离散化
	rep(i, 1, tot)
	{
		mxx.tim = 0, mxx.num = 1e9 +5;
		memset(tmp, 0, sizeof tmp);
		rep(j, i, tot)
		{
			rep(k, (j - 1) * len + 1, min(n, j * len))
			{
				tmp[a[k].al] += 1;
				if(tmp[a[k].al] > mxx.tim)
					mxx.tim = tmp[a[k].al], mxx.num = a[k].al;
				else if(tmp[a[k].al] == mxx.tim)
					mxx.num = min(mxx.num, a[k].al);
			}
			p[i][j] = mxx;
		}
	}
	memset(tmp, 0, sizeof tmp);
	rep(i, 1, tot)
	{
		rep(j, 1, n) s[i][a[j].al] = s[i - 1][a[j].al];
		rep(j, (i - 1) * len + 1, min(n, i * len))
			s[i][a[j].al] += 1;
	}
   //预处理两个数组
	rep(i, 1, m)
	{
		scanf("%d %d", &l, &r);
		l = ((l + lst - 1) % n) + 1, r = ((r + lst - 1) % n) + 1;
		if(l > r) swap(l, r);
		posl = getpos(l), posr = getpos(r);
      //分情况讨论:
		if(posr - posl <= 2)
		{
			mxx.num = 1e9 + 5, mxx.tim = 0;
			memset(tmp, 0, sizeof tmp);
			rep(j, l, r)
			{
				tmp[a[j].al] += 1;
				if(tmp[a[j].al] > mxx.tim)
					mxx.tim = tmp[a[j].al], mxx.num = a[j].al;
				else if(tmp[a[j].al] == mxx.tim)
					mxx.num = min(mxx.num, a[j].al);
			}
			printf("%d\n", lst = pre[mxx.num]);
		}
		else
		{
			mxx.num = 1e9 + 5, mxx.tim = 0;
			memset(tmp, 0, sizeof tmp);
			memset(vis, 0, sizeof vis);
			ans = p[posl + 1][posr - 1];
			rep(j, l, min(n, posl * len)) 	
				tmp[a[j].al] += 1;
			rep(j, (posr - 1) * len + 1, r)
				tmp[a[j].al] += 1;
			rep(j, l, min(n, posl * len))
			{
				if(!vis[a[j].al])
				{
					vis[a[j].al] = 1;
					int tme = tmp[a[j].al] + s[posr - 1][a[j].al] - s[posl][a[j].al];
					if(tme > mxx.tim)
						mxx.tim = tme, mxx.num = a[j].al;
					else if(tme == mxx.tim)
						mxx.num = min(mxx.num, a[j].al);
				}
			}
			rep(j, (posr - 1) * len + 1, r)
			{
				if(!vis[a[j].al])
				{
					vis[a[j].al] = 1;
					int tme = tmp[a[j].al] + s[posr - 1][a[j].al] - s[posl][a[j].al];
					if(tme > mxx.tim)
						mxx.tim = tme, mxx.num = a[j].al;
					else if(tme == mxx.tim)
						mxx.num = min(mxx.num, a[j].al);
				}
			}
			if(mxx.tim > ans.tim + tmp[ans.num]) ans.num = mxx.num;
			else if(mxx.tim == ans.tim + tmp[ans.num]) ans.num = min(ans.num, mxx.num);
			printf("%d\n", lst = pre[ans.num]);
		}
	}
	return 0;
}

二、P4135 作诗

给定长为 n n n 的数列,每次给定区间范围 [ l , r ] [l,r] [l,r],求其中出现了正偶数次的数的个数。

强制算法在线。

思路

和蒲公英很像,和后者相比这题很良心了。

预处理:类似,不同之处在于要在 O(n sprtn) \text{O(n sprtn)} O(n sprtn) 内处理完。因此做出一些更改:

  • 定义 P i , j P_{i,j} Pi,j 表示第 i i i 到第 j j j 个块中出现正偶数次的数的数量;

  • 定义 S i , j S_{i,j} Si,j 表示从第 i i i 个块的块首到 n n n 中出现了正偶数次的数的数量。

查询也比较简单:

  • 区间范围小:暴力。

  • 区间范围大:统计散块中的数的出现次数,再和整块中其出现次数比较奇偶、统计即可。

代码

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

#define maxn 100005
#define maxm 505
#define rep(i, a, b) for(int i = a; i <= b; ++i)

int n, c, m, l, r, lst;
int len, tot;
int a[maxn], p[maxm][maxm], s[maxm][maxn];
int b[maxn], vis[maxn];
int st[maxn], top;

inline int get(int x)
{
	int res = x / len;
	if(x % len) res += 1;
	return res;
}

inline void pre()
{
	rep(i, 1, tot)
	{
		int tmp = 0;
		rep(j, (i - 1) * len + 1, n)
		{
			if(s[i][a[j]] & 1) tmp += 1;
			else if(s[i][a[j]] and !(s[i][a[j]] & 1)) tmp -= 1;
			s[i][a[j]] += 1; 
			if(!(j % len) or j == n) p[i][get(j)] = tmp;
		}
	}
}

int main()
{
	scanf("%d %d %d", &n, &c, &m);
	len = sqrt(n), tot = (n - 1) / len + 1;
	rep(i, 1, n) scanf("%d", &a[i]);
	pre();
	rep(i, 1, m)
	{
		scanf("%d %d", &l, &r);
		l = (l + lst) % n + 1, r = (r + lst) % n + 1;
		if(l > r) swap(l, r);
		int posl = get(l), posr = get(r);
		if(posr - posl <= 1)
		{
			int tmp = 0;
			rep(j, l, r)
			{
				if(b[a[j]] & 1) tmp += 1;
				else if(b[a[j]] and !(b[a[j]] & 1)) tmp -= 1;
				b[a[j]] += 1;
			}
			memset(b, 0, sizeof b);
			printf("%d\n", lst = tmp);
		}
		else
		{
			int ans;
			if(posl + 1 <= posr - 1) ans = p[posl + 1][posr - 1];
			rep(j, l, posl * len)
			{
				b[a[j]] += 1;
				st[++top] = a[j];
			}
			rep(j, (posr - 1) * len + 1, r)
			{
				b[a[j]] += 1;
				st[++top] = a[j];
			}
			while(top) 
			{
				int t = st[top];
				if(b[t] != 0) 
				{
					int tim = s[posl + 1][t] - s[posr][t];
					if((tim > 0) and (tim & 1) == 0 and (b[t] & 1)) ans--;
					else if(((tim > 0) and (tim & 1)) and (b[t] & 1)) ans++;
					else if((tim == 0) and (b[t] & 1) == 0) ans++;
					b[t] = 0;
				}
				top--;
			}
			printf("%d\n", lst = ans);
		}
	}
	return 0;
}
posted @ 2022-03-25 07:26  pldzy  阅读(54)  评论(0)    收藏  举报