浅谈分块中的一些技巧
浅谈分块中的一些技巧
撰写人:high_sky
前言
我爱分块,更爱 lxl 的毒瘤分块题目。
分块核心和原理
- 将一些东西捆在一起处理。
- 数量不宜过多。
- 类似于一个块一个块地跳。
基础回顾
这么单纯可爱朴素的分块是一种十分牛逼(haowan)的数据结构,其代码简洁、可读性强受到了大众的喜爱。
分块
我们来回顾,分块为什么叫分块(不是我在说些什么啊?)。
因此,顾名思义,分块就是将一个东西分成很多块一样(这让我想到了分蛋糕时的乐趣)。
这样,我们假设平均分成了 \(B\) 块。
于是呢?通过下述的操作方法得到当 \(B=\sqrt n\) 的时候,时间复杂度最小。
修改&查询
经过上述,显然地,我们可以用一个变量来存储这个块内必要的东西。
修改可以两边暴力修改,中间整块修改。
例题
P4119 [Ynoi2018] 未来日记
题目大意
要求维护一个数据结构使能完成一下操作:
- 把 \([l,r]\) 中所有 \(x\) 替换成 \(y.\)
- 查询 \([l,r]\) 中的第 \(k\) 小值。
数据范围:\(1\leq n,m,a_i\leq 10^5.\)
解题思路
观察题目,可以看出这里的所有操作都是基于序列当中的数,并且跟其他的东西无关。
经过观察:\(a_i\leq 10^5.\)
不难想到可以用值域分块配合序列分块使用。
那么我们就设 \(cnt1_{i,j}\) 表示序列的前 \(i\) 块中值域的第 \(j\) 块有多少个,\(cnt2_{i,j}\) 表示序列的前 \(i\) 块中数字为 \(j\) 的有多少(这个是根据题目所需设置的)。
继续根据大步小步走的思想:先用大块的走到可能得到的范围,再小块小块地走得到准确答案。
因此我们也是很显然地得出了求第 \(k\) 小的代码:
int sum1[M],sum2[N];
inline int kth_query(int l,int r,int k) {
int sum = 0;
if (bl[l] == bl[r]) {
sequp(bl[l]);
for (int i = l;i <= r;i ++) sum2[i] = a[i];
nth_element(sum2 + l,sum2 + l + k - 1,sum2 + r + 1);
int ans = sum2[l + k - 1];
for (int i = l;i <= r;i ++) sum2[i] = 0;
return ans;
}
sequp(bl[l]),sequp(bl[r]);
for (int i = l;i <= R[bl[l]];i ++) sum1[bl[a[i]]] ++,sum2[a[i]] ++;
for (int i = L[bl[r]];i <= r;i ++) sum1[bl[a[i]]] ++,sum2[a[i]] ++;
for (int i = 1;i <= bl[N - 1];i ++)//
if (sum + sum1[i] + cnt1[bl[r] - 1][i] - cnt1[bl[l]][i] >= k) {//算贡献(块),此过程类似大步小步走(但算法本质不同,思想是一样的)
for (int j = L[i];j <= R[i];j ++)
if (sum + sum2[j] + cnt2[bl[r] - 1][j] - cnt2[bl[l]][j] >= k) {
for (int k = l;k <= R[bl[l]];k ++) sum1[bl[a[k]]] --,sum2[a[k]] --;
for (int k = L[bl[r]];k <= r;k ++) sum1[bl[a[k]]] --,sum2[a[k]] --;
return j;
}
else sum += sum2[j] + cnt2[bl[r] - 1][j] - cnt2[bl[l]][j];
}
else sum += sum1[i] + cnt1[bl[r] - 1][i] - cnt1[bl[l]][i];
}
然后我们就发现了一个问题:这修改怎么这么烦人啊!
但是我们发现:\(x,y\) 是一一对应的,并且我们发现这具有传递性:它一直变大,并且不会分裂。
那么我们就想一个值的对应关系。
于是就有:
- \(id_{i,x}\) 表示序列第 \(i\) 块中 \(x\) 的对应量。
- \(rid_{i,x}\) 表示序列第 \(i\) 块中对应量为 \(x\) 的原量。
为了方便暴力时的修改,就必须也要设:
- \(pos_i\) 表示当前序列的第 \(i\) 个位置的对应量。
也许此时,我们需要分类讨论以简化问题。
第一种:无 \(x\) 或者 \(x=y\)
显然不会做任何事情。
第二种:有 \(x\) 没有 \(y\)
这一种情况使块内的数的种类不变。
那么我们直接块更新就可以了。
第三种:有 \(x\) 有 \(y\)
这样会使得数的种类变少 \(1\)。
那么,我们先看看能不能暴力更新,对于一个块内,更新时间复杂度为 \(\mathcal{O}(\sqrt n).\)
注意到:这里的数的种类 \(\leq n+m.\)
感性理解一下:这里的数字种类不断减小,暴力的次数也因此不断减小,因此这一个的暴力复杂度看似是 \(\mathcal{O}(n)\) 的,其实达不到这种情况,均摊下来为 \(\mathcal O(\sqrt n).\)
对于这部分代码如下:
inline void sequp(int x) {
for (int i = L[x];i <= R[x];i ++) a[i] = rid[x][pos[i]];//暴力下传修改
}
inline void seqdown(int l,int r,int x,int y) {//暴力
for (int i = l;i <= r;i ++)
if (a[i] == x) {
cnt2[bl[l]][x] --,cnt2[bl[l]][y] ++;
cnt1[bl[l]][bl[x]] --,cnt1[bl[l]][bl[y]] ++;
a[i] = y;
}
}
inline void change(int i,int x,int y) {// 更新对应量
id[i][y] = id[i][x];
rid[i][id[i][x]] = y;
id[i][x] = 0;
}
inline void update(int l,int r,int x,int y) {
if (x == y || cnt2[bl[r]][x] - cnt2[bl[l] - 1][x] == 0) return;// 无意义,或者没有x
for (int i = bl[n];i >= bl[l];i --)
cnt2[i][x] -= cnt2[i - 1][x],cnt2[i][y] -= cnt2[i - 1][y],
cnt1[i][bl[x]] -= cnt1[i - 1][bl[x]],cnt1[i][bl[y]] -= cnt1[i - 1][bl[y]];
if (bl[l] == bl[r]) { //在同一个块内
sequp(bl[l]);
seqdown(l,r,x,y);
build(bl[l]);
for (int i = bl[l];i <= bl[n];i ++)
cnt2[i][x] += cnt2[i - 1][x],cnt2[i][y] += cnt2[i - 1][y],
cnt1[i][bl[x]] += cnt1[i - 1][bl[x]],cnt1[i][bl[y]] += cnt1[i - 1][bl[y]];
return;
}
sequp(bl[l]),sequp(bl[r]);//两边
seqdown(l,R[bl[l]],x,y),seqdown(L[bl[r]],r,x,y);
build(bl[l]),build(bl[r]);
//中间
for (int i = bl[l] + 1;i < bl[r];i ++) {
if (!cnt2[i][x]) continue;
if (cnt2[i][y]) {//数字种类少 1 ,暴力修改均摊时间复杂度为根号
sequp(i),seqdown(L[i],R[i],x,y);
build(i);
}
else {//数字种类不变 ,没有 y
cnt1[i][bl[y]] += cnt2[i][x],cnt1[i][bl[x]] -= cnt2[i][x];
cnt2[i][y] = cnt2[i][x],cnt2[i][x] = 0;
change(i,x,y);
}
}
for (int i = bl[l];i <= bl[n];i ++)
cnt2[i][x] += cnt2[i - 1][x],cnt2[i][y] += cnt2[i - 1][y],
cnt1[i][bl[x]] += cnt1[i - 1][bl[x]],cnt1[i][bl[y]] += cnt1[i - 1][bl[y]];
}
整体而来时间复杂度为 \(\mathcal{O}((n+m)\sqrt n).\)
代码如下:
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#define N 100005
#define M 320
using namespace std;
template<typename T>
inline void read(T &x) {
x = 0;
int f = 1;
char ch = getchar();
while(ch < '0' || ch > '9') f = (ch == '-' ? -1 : f),ch = getchar();
while('0' <= ch && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48),ch = getchar();
x *= f;
}
template<typename T>
inline void write(T x) {
if (x < 0) putchar('-'),x = -x;
if (x <= 9) putchar(x + '0');
else write(x / 10),putchar(x % 10 + '0');
}
int cnt1[M][M],cnt2[M][N],n,m,len,bl[N],a[N],L[N],R[N];
int id[M][N],rid[M][N],pos[N];
inline void build(int x) {
int tot = 0;
for (int i = 1;i <= len;i ++) id[x][rid[x][i]] = 0;
for (int i = L[x];i <= R[x];i ++)
if (!id[x][a[i]])
id[x][a[i]] = ++tot,rid[x][tot] = a[i];
for (int i = L[x];i <= R[x];i ++) pos[i] = id[x][a[i]];
}
inline void sequp(int x) {
for (int i = L[x];i <= R[x];i ++) a[i] = rid[x][pos[i]];//暴力下传修改
}
inline void seqdown(int l,int r,int x,int y) {//暴力
for (int i = l;i <= r;i ++)
if (a[i] == x) {
cnt2[bl[l]][x] --,cnt2[bl[l]][y] ++;
cnt1[bl[l]][bl[x]] --,cnt1[bl[l]][bl[y]] ++;
a[i] = y;
}
}
inline void change(int i,int x,int y) {// 更新对应量
id[i][y] = id[i][x];
rid[i][id[i][x]] = y;
id[i][x] = 0;
}
inline void update(int l,int r,int x,int y) {
if (x == y || cnt2[bl[r]][x] - cnt2[bl[l] - 1][x] == 0) return;// 无意义,或者没有x
for (int i = bl[n];i >= bl[l];i --)
cnt2[i][x] -= cnt2[i - 1][x],cnt2[i][y] -= cnt2[i - 1][y],
cnt1[i][bl[x]] -= cnt1[i - 1][bl[x]],cnt1[i][bl[y]] -= cnt1[i - 1][bl[y]];
if (bl[l] == bl[r]) { //在同一个块内
sequp(bl[l]);
seqdown(l,r,x,y);
build(bl[l]);
for (int i = bl[l];i <= bl[n];i ++)
cnt2[i][x] += cnt2[i - 1][x],cnt2[i][y] += cnt2[i - 1][y],
cnt1[i][bl[x]] += cnt1[i - 1][bl[x]],cnt1[i][bl[y]] += cnt1[i - 1][bl[y]];
return;
}
sequp(bl[l]),sequp(bl[r]);//两边
seqdown(l,R[bl[l]],x,y),seqdown(L[bl[r]],r,x,y);
build(bl[l]),build(bl[r]);
//中间
for (int i = bl[l] + 1;i < bl[r];i ++) {
if (!cnt2[i][x]) continue;
if (cnt2[i][y]) {//数字种类少 1 ,暴力修改均摊时间复杂度为根号
sequp(i),seqdown(L[i],R[i],x,y);
build(i);
}
else {//数字种类不变 ,没有 y
cnt1[i][bl[y]] += cnt2[i][x],cnt1[i][bl[x]] -= cnt2[i][x];
cnt2[i][y] = cnt2[i][x],cnt2[i][x] = 0;
change(i,x,y);
}
}
for (int i = bl[l];i <= bl[n];i ++)
cnt2[i][x] += cnt2[i - 1][x],cnt2[i][y] += cnt2[i - 1][y],
cnt1[i][bl[x]] += cnt1[i - 1][bl[x]],cnt1[i][bl[y]] += cnt1[i - 1][bl[y]];
}
int sum1[M],sum2[N];
inline int kth_query(int l,int r,int k) {
int sum = 0;
if (bl[l] == bl[r]) {
sequp(bl[l]);
for (int i = l;i <= r;i ++) sum2[i] = a[i];
nth_element(sum2 + l,sum2 + l + k - 1,sum2 + r + 1);
int ans = sum2[l + k - 1];
for (int i = l;i <= r;i ++) sum2[i] = 0;
return ans;
}
sequp(bl[l]),sequp(bl[r]);
for (int i = l;i <= R[bl[l]];i ++) sum1[bl[a[i]]] ++,sum2[a[i]] ++;
for (int i = L[bl[r]];i <= r;i ++) sum1[bl[a[i]]] ++,sum2[a[i]] ++;
for (int i = 1;i <= bl[N - 1];i ++)//
if (sum + sum1[i] + cnt1[bl[r] - 1][i] - cnt1[bl[l]][i] >= k) {//算贡献(块),此过程类似大步小步走(但算法本质不同,思想是一样的)
for (int j = L[i];j <= R[i];j ++)
if (sum + sum2[j] + cnt2[bl[r] - 1][j] - cnt2[bl[l]][j] >= k) {
for (int k = l;k <= R[bl[l]];k ++) sum1[bl[a[k]]] --,sum2[a[k]] --;
for (int k = L[bl[r]];k <= r;k ++) sum1[bl[a[k]]] --,sum2[a[k]] --;
return j;
}
else sum += sum2[j] + cnt2[bl[r] - 1][j] - cnt2[bl[l]][j];
}
else sum += sum1[i] + cnt1[bl[r] - 1][i] - cnt1[bl[l]][i];
}
signed main(){
read(n),read(m);
len = sqrt(n);
for (int i = 1;i < N;i ++) bl[i] = (i - 1) / len + 1;
for (int i = 1;i <= n;i ++) read(a[i]);
for (int i = 1;i <= bl[N - 1];i ++) L[i] = (i - 1) * len + 1,R[i] = i * len;
R[bl[n]] = n;
for (int i = 1;i <= bl[n];i ++) build(i);
for (int x = 1;x <= bl[n];x ++) {
for (int i = 1;i <= bl[N - 1];i ++) cnt1[x][i] = cnt1[x - 1][i];
for (int i = 1;i < N;i ++) cnt2[x][i] = cnt2[x - 1][i];
for (int i = L[x];i <= R[x];i ++) cnt1[x][bl[a[i]]]++,cnt2[x][a[i]]++;
}
for (int i = 1,t,l,r,x,y;i <= m;i ++) {
read(t),read(l),read(r);
if (t == 1) {
read(x),read(y);
update(l,r,x,y);
}
else {
read(x);
write(kth_query(l,r,x)),putchar('\n');
}
}
return 0;
}
这里卡常有点厉害,给出一些卡常小技巧:
- 把运算当作变量存储
- 顺序扫描可以带来更多的机遇去
break。 - 减少循环次数(利用已知条件)。
题目小结
- 碰到带有跟值修改有关的,一般跟值域分块有关。
- 大步小步走的思想在分块中十分适用。
- 根号算法其实跟 \(\log\) 算法不会很搭,这样会无形中多出一个 \(\log\),因此解决这种常数的办法就是运用同阶的暴力方法进行配合处理。
- 观察题目性质,得出对应的关系,这很重要
- 分类讨论有时并不会增加题目的复杂性,而是打开一个新世界,简化问题,思考变得更为通畅。
- 其实感性理解,也会成为你成功路上的加速器。
- 我们常常先从静态算法再到动态算法。
P4117 [Ynoi2018] 五彩斑斓的世界
一些信息
一个比较好用且大佬多的OJ:UOJ。
其中出题人的题解在这。
这道题最初是 lxl 出给 CF 的,在 CF 里的编号是 CF896E,与那个臭名昭著的 ODT 的板子题属于同一场比赛。
在赛后,lxl 加强了这题的数据并卡了卡常,然后放进了洛谷题库。
算是大分块中的入门题,个人认为是该系列中最简单的题。
虽然说出题人给此题的评分低于上一题(\(8.5/11\),此题 \(6/11\)),但还是有些东西需要思考。
题目概述
有两个操作:
- 将 \([l,r]\) 中大于 \(x\) 的数减去 \(x.\)
- 回答 \([l,r]\) 中 \(x\) 的个数。
时间限制:\(1000\text{ ms}\),空间限制:\(64\text{ MB}.\)
思路
首先看到这道题需要一些联想,就是这个第一个操作其实和上一题是类似的。
我们假设 \(name_x\) 表示 \(x\) 在当前块的第几个位置,\(pre_i\) 表示当前位置上的数 \(a_i\) 合并到了哪一个位置(并查集),以及 \(val_i\) 只表示根位置上的数值(指并查集),还有 \(sz_i\) 表示 \(i\) 的个数。
因此我们就可以很简单地初始化这一类东西:
inline void build(int x) {
int bl = (x - 1) * block + 1,br = x * block;
if (x == len) br = n;
for (int i = bl;i <= br;i ++) {
mx = max(mx,a[i]);
if (!name[a[i]]) {
pre[i] = i;
name[a[i]] = i;
val[i] = a[i];
}
else pre[i] = name[a[i]];
sz[a[i]] ++;
}
}
我们再考虑一些其他的事情。
第一个操作:将大于 \(x\) 的减去 \(x.\)
我们可以想到一种方式,使其的总复杂度为 \(\mathcal{O}(n)\)。
那么最普遍的技巧(trick)就是使两端的数越来越小,总复杂度就是 \(\mathcal{O}(n)\) 的。
假设最大值为 \(mx-lz\),要减去的数为 \(x\),那么具体地:
- 若 \(mx-lz\leq 2\times x\),则暴力将 \((x,mx]\) 的数全部修改,并更新 \(mx.\)
- 否则,暴力将 \([1,x]\) 的数加上 \(x\),然后让整个块整体减去 \(x.\)(这个可以用 \(lz\) 去标记)
这样就使得 \(mx\) 和最小值越来越小。
那么如何更新 \(mx\) 呢?
因为 \(mx-lz<2\times x\),因此有:\(mx-lz-x<x.\)
注意到:这里的 \(mx\) 表示的是没有减去 \(lz\)。
那么是赋值成 \(x + lz\) 吗?
也不对,因为:要取 \(\min\) 值。
有一种可能的情况:\(mx-lz<x.\)
这个是容易错的地方。
因此得到代码:
inline void update1(int x) {//全改情况
if (2 * x > (mx - lz)) {//改大于x的
for (int i = x + 1 + lz;i <= mx;i ++)//这里枚举的当前序列里面的没有加上lz的
if (name[i]) change(i,i - x);
mx = min(x + lz,mx);
}
else {
for (int i = x + lz;i >= lz;i --)
if (name[i]) change(i,i + x);
lz += x;//将要减的
}
}
inline void update2(int id,int l,int r,int x) {//暴力修改 (这个相当于原本在线的边边部分)
int bl = (id - 1) * block + 1,br = id * block;
if (id == n) br = n;
l = max(l,bl),r = min(r,br);
for (int i = bl;i <= br;i ++) {//暴力重构
int w = val[find(i)];
a[i] = w - lz;
name[w] = sz[w] = 0;
}
for (int i = bl;i <= br;i ++) val[i] = 0;
for (int i = l;i <= r;i ++)
if (a[i] > x) a[i] -= x;
build(id);
}
现在看查询。
如果是整个块的话,就是 \(sz_x\),不是就暴力找。
代码如下:
inline int query(int id,int l,int r,int x) {//边边部分
int res = 0;
int bl = (id - 1) * block + 1,br = id * block;
if (id == len) br = n;
l = max(l,bl),r = min(r,br);
for (int i = l;i <= r;i ++)
if (val[find(i)] - lz == x) res ++;
return res;
}
所有代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stdlib.h>
#include <cstring>
#include <vector>
#include <cmath>
#define N 1000006
#define M 500005
#define MA 100005
#define SN 1005
#define SM 710
#define isdigit(x) ('0' <= x && x <= '9')
using namespace std;
template<typename T>
void read(T &x) {
x = 0;
int f = 1;
char ch = getchar();
for (;!isdigit(ch);ch = getchar()) f = (ch == '-' ? -1 : f);
for (;isdigit(ch);ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
x *= f;
}
template<typename T>
void write(T x,string end = "") {
if (x < 0) x = -x,putchar('-');
if (x <= 9) putchar(x + '0');
else write(x / 10),putchar(x % 10 + '0');
for (auto i : end) putchar(i);
}
struct node{
int op,l,r,x;
}q[N];
int n,m,len,block,a[N],name[N],val[N],pre[N],sz[N],lz,mx = -2e9,ans[M];
inline void build(int x) {
mx = -2e9;
lz = 0;
int bl = (x - 1) * block + 1,br = x * block;
if (x == len) br = n;
for (int i = bl;i <= br;i ++) {
mx = max(mx,a[i]);
if (!name[a[i]]) {
pre[i] = i;
name[a[i]] = i;
val[i] = a[i];
}
else pre[i] = name[a[i]];
sz[a[i]] ++;
}
}
inline int find(int x) {
return (x == pre[x] ? x : pre[x] = find(pre[x]));
}
inline void change(int x,int y) {
if (name[y]) {//如果y存在这个序列里面
//由于 x -> y.
pre[name[x]] = name[y];
sz[y] += sz[x];
}
else {//不存在,pre直接顶替即可相当于作name[y]=name[x]的操作.
val[name[y] = name[x]] = y;
sz[y] += sz[x];
}
name[x] = sz[x] = 0;
}
inline void update1(int x) {//全改情况
if (2 * x > (mx - lz)) {//改大于x的
for (int i = x + 1 + lz;i <= mx;i ++)//这里枚举的当前序列里面的没有加上lz的
if (name[i]) change(i,i - x);
mx = min(x + lz,mx);
}
else {
for (int i = x + lz;i >= lz;i --)
if (name[i]) change(i,i + x);
lz += x;//将要减的
}
}
inline void update2(int id,int l,int r,int x) {//暴力修改 (这个相当于原本在线的边边部分)
int bl = (id - 1) * block + 1,br = id * block;
if (id == n) br = n;
l = max(l,bl),r = min(r,br);
for (int i = bl;i <= br;i ++) {//暴力重构
int w = val[find(i)];
a[i] = w - lz;
name[w] = sz[w] = 0;
}
for (int i = bl;i <= br;i ++) val[i] = 0;
for (int i = l;i <= r;i ++)
if (a[i] > x) a[i] -= x;
build(id);
}
inline int query(int id,int l,int r,int x) {//边边部分
int res = 0;
int bl = (id - 1) * block + 1,br = id * block;
if (id == len) br = n;
l = max(l,bl),r = min(r,br);
for (int i = l;i <= r;i ++)
if (val[find(i)] - lz == x) res ++;
return res;
}
signed main(){
read(n),read(m);
block = sqrt(n);
len = ceil(n * 1.0 / block);
for (int i = 1;i <= n;i ++) read(a[i]);
for (int i = 1;i <= m;i ++) read(q[i].op),read(q[i].l),read(q[i].r),read(q[i].x);
for (int i = 1;i <= len;i ++) {//枚举第几个块
if (i != 1) {
memset(name,0,sizeof name);
memset(sz,0,sizeof sz);
}
lz = 0;
mx = -2e9;
int bl = (i - 1) * block + 1;
int br = i * block;
if (i == len) br = n;
build(i);
for (int j = 1;j <= m;j ++) {
int op = q[j].op,l = q[j].l,r = q[j].r,x = q[j].x;
if (bl > r || br < l) continue;
if (op == 1) {
if (l <= bl && br <= r) update1(x);
else update2(i,l,r,x);
}
else {
if (x + lz > 1e5 + 1) continue;
if (l <= bl && br <= r) ans[j] += sz[x + lz];
else ans[j] += query(i,l,r,x);
}
}
}
for (int i = 1;i <= m;i ++)
if (q[i].op == 2) write(ans[i],"\n");
return 0;
}
浅析时间复杂度
看似是每一个块遍历了一次 \(m\),然后又暴力,时间复杂度 \(\mathcal{O}(nm)\) 的,但其实并不是。
这个其实和在线没有什么区别,修改、查询亦是如此。
总时间复杂度为 \(\mathcal{O}((n+m)\sqrt n).\)
题目小结
- 对于这种省空间的trick需要其满足以下的条件:
- 答案的可加性以及答案的可取最值性。
- 块与块之间相互独立,互不影响。
- 可以离线,每个块跑一遍。
- 序列两端的值不断靠近以减少总体复杂度的trick。
- 并查集常数很小用于维护改变数值的题目。
SP20644 ZQUERY - Zero Query
前言
感谢 Ta 给予我的意志,请允许我用一篇题解纪念 Ta,我是不会忘记你的。
思路
主要讲一讲赛时的思路。
先打了一个暴力:
for (int i = 1;i <= m;i ++) {
int l,r;
scanf("%lld%lld",&l,&r);
int ans = 0;
for (int j = l;j <= r;j ++) {
int p = j - 1,sum = 0;
for (int k = j;k <= r;k ++) {
sum += a[k];
if (sum == 0) p = k;
}
ans = max(ans,p - j + 1);
}
printf("%lld\n",ans);
}
然后受到天天爱跑步的思想:“数”形结合,我们不难用一个折线图来表现这道题所给出的 \(\{a\}\)。即用 \(x\) 轴代表第几个(下标),用 \(y\) 轴代表前缀和。
发现:当我们确定左端点 \(l\) 的时候,那么求的就是在 \([l+1,r]\) 内的折线最远与 \(y=sum_l\) 相交的那个点的下标(即 \(x\) 坐标)就是目前来说的最有情况。
拿一个 vector 容器作为一个桶,在进行二分即可,时间复杂度 \(\mathcal{O}(mn\log n).\)
注意到这道题目只用线段树、树状数组、或者是平衡树都不是很好做,考虑序列分块。
分块?嗯……是个好东西(回想到了我做毒瘤 noip 出的大分块时的题目)。
由于我们已经用桶存下了值域,所以就不再考虑再套一个值域分块了。
然后呢?怎么设?存什么?这是一个好问题。
注意到我们希望 \(\mathcal{O}(1)\) 的时间求出中间部分(整块整块的那部分)的答案,不妨设 \(cnt_{i,j}\) 表示第 \(i\) 个块到第 \(j\) 个块中的答案。
这个 \(cnt_{i,j}\) 是好求的,直接枚举每个块的左端点开始,一直扫到最后(也就是 \(n\)),就可以得到了。
具体的代码如下:
for (int i = 1;i <= bl[n];i ++) {
memset(p,0xff,sizeof p);
int Max = 0;
for (int j = L[i];j <= n;j ++) {
if (p[a[j]] == -1) p[a[j]] = j;
Max = max(Max,j - p[a[j]]);
if (bl[j] != bl[j + 1]) cnt[i][bl[j]] = Max;
}
}
考虑怎么得到答案。
中间的就不用算了,两边部分的也是挺好搞的。按照惯例直接枚举即可。
但,这你不炸了吗?其实没有,还有桶没有用上呢!直接求个 upper_bound 就可以得到答案啦~
整体时间复杂度 \(\mathcal{O}(n+n\sqrt n+m\sqrt n\log n)\),也是直接过了。
于是,一份分块在线代码诞生啦:
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <stdlib.h>
#include <cmath>
#include <vector>
#define int long long
#define N 50005
#define M 505
using namespace std;
#define isdigit(ch) ('0' <= ch && ch <= '9')
template<typename T>
void read(T &x) {
x = 0;
int f = 1;
char ch = getchar();
for (;!isdigit(ch);ch = getchar()) f = (ch == '-' ? -1 : f);
for (;isdigit(ch);ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
x *= f;
}
template<typename T>
void write(T x) {
if (x < 0) x = -x,putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
int n,m,a[N],bl[N],L[M],R[M],len,cnt[M][M],p[N << 1];//cnt_{i,j}->第i个到第j个的最大的
vector<int> v[N << 1];//折线的桶
void init() {
int sum = 0;
for (int i = 1;i <= n;i ++) {
sum += a[i];//处理折线
a[i] = sum + n;//前缀和
v[a[i]].push_back(i);
}
}
signed main(){//考虑数列分块,用 O(nsqrtn) 解决
read(n),read(m);
n ++;//要算 0
for (int i = 2;i <= n;i ++) read(a[i]);
len = sqrt(n);
for (int i = 1;i <= n;i ++) bl[i] = (i - 1) / len + 1;
for (int i = 1;i <= bl[n];i ++) L[i] = (i - 1) * len + 1,R[i] = i * len;
R[bl[n]] = n;
init();
for (int i = 1;i <= bl[n];i ++) {
memset(p,0xff,sizeof p);
int Max = 0;
for (int j = L[i];j <= n;j ++) {
if (p[a[j]] == -1) p[a[j]] = j;
Max = max(Max,j - p[a[j]]);
if (bl[j] != bl[j + 1]) cnt[i][bl[j]] = Max;
}
}
for (int i = 1;i <= m;i ++) {
int l,r;
read(l),read(r);
r ++;
int ans = cnt[bl[l] + 1][bl[r] - 1];
for (int i = l;i <= min(r,R[bl[l]]);i ++) {
auto x = upper_bound(v[a[i]].begin(),v[a[i]].end(),r);
--x;
ans = max(ans,(*x) - i);
}
if (bl[l] != bl[r]) {
for (int i = L[bl[r]];i <= r;i ++) {
auto x = upper_bound(v[a[i]].begin(),v[a[i]].end(),l - 1);
ans = max(ans,i - (*x));
}
}
printf("%lld\n",ans);
}
return 0;
}
//谁说我不爱数据结构,我超爱的~
//毒瘤的 lxl(noip)让我更加爱它
码量也是挺小的(至少比回滚少),但是打快读快写占了一些。
卡常优化
其实对于整体时间复杂度也有质的变化,考虑设块长为 \(B\),那么上述的时间复杂度就变成了 \(\mathcal{O}(n+n\times \frac{n}{B}+mB\log n)\)
考虑到最后的 \(mB\log n\) 比较大而 \(\frac{n^2}B\) 又比较小,为了使总体时间复杂度变小,我们可以使这两个值比较靠近一些。
而一开始设的 \(B=\sqrt n\) 会使其变大,可以尝试取一些 \(B=\sqrt[p]{n},p>2\),这样会使 \(mB\log n\) 小一些,而 \(n\times \frac{n}{B}\) 又不会太大,这样就可以使总体时间复杂度变小。
取 \(B=\sqrt[3]{n}\) 也是一个不错的选择,但考虑到没有这个函数,可以取 \(B=\sqrt{\frac{n}{3}}\) 也行,快了不少。
题目小结
- 不能用比较基础的数据结构做的话,可以考虑分块。
- 取块长也是个技术活,要综合时间复杂度做出抉择
- 要从实际的求解方向出发那进行状态设置(很重要)。
P4145 上帝造题的七分钟 2 / 花神游历各国
思路
似乎挺有趣的,感觉自己不会。而且模拟赛打了这个带平方的:http://dyoi.net:2024/d/test2024/p/210?tid=688ab07061a9cb18baab4734。
分块让人快乐——块乐。
注意到:\(10^{12}\) 最多开 \(6\) 次根。
所以说我们打包一下就可以了,标记这一整个区间有没有开完根之后值不变,考虑用分块写就有了下面的代码:
代码
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cmath>
#define int long long
#define N 100005
#define M 320
using namespace std;
int n,a[N],cnt[M],bl[N],L[M],R[M];
bool bj[M];
signed main(){
cin >> n;
for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
for (int i = 1;i < M;i ++) L[i] = 1e9;
int len = ceil(sqrt(n));
for (int i = 1;i <= n;i ++) {
bl[i] = i / len + 1;
L[bl[i]] = min(L[bl[i]],i),R[bl[i]] = i;
}
for (int i = 1;i <= n;i ++) cnt[bl[i]] += a[i];
int m;
cin >> m;
for (int k,l,r;m --;) {
scanf("%lld%lld%lld",&k,&l,&r);
if (l > r) swap(l,r);
if (k == 0) {
if (bl[l] == bl[r]) {
if (bj[bl[l]]) continue;
for (int i = l;i <= r;i ++)
cnt[bl[l]] -= a[i],a[i] = sqrt(a[i]),cnt[bl[l]] += a[i];
bool flag = 1;
for (int i = L[bl[l]];i <= R[bl[r]];i ++) flag &= (a[i] <= 1);
if (flag) bj[bl[l]] = 1;
}
else {
if (!bj[bl[l]]) {
for (int i = l;i <= R[bl[l]];i ++)
cnt[bl[l]] -= a[i],a[i] = sqrt(a[i]),cnt[bl[l]] += a[i];
bool flag = 1;
for (int i = L[bl[l]];i <= R[bl[l]];i ++) flag &= (a[i] <= 1);
bj[bl[l]] = flag;
}
for (int i = bl[l] + 1;i < bl[r];i ++) {
if (bj[i]) continue;
for (int j = L[i];j <= R[i];j ++)
cnt[i] -= a[j],a[j] = sqrt(a[j]),cnt[i] += a[j];
bool fl = 1;
for (int j = L[i];j <= R[i];j ++) fl &= (a[j] <= 1);
bj[i] = fl;
}
if (!bj[bl[r]]) {
for (int i = L[bl[r]];i <= r;i ++)
cnt[bl[r]] -= a[i],a[i] = sqrt(a[i]),cnt[bl[r]] += a[i];
bool flag = 0;
for (int i = L[bl[r]];i <= r;i ++) flag &= (a[i] <= 1);
bj[bl[r]] = flag;
}
}
}
else {
int ans = 0;
if (bl[l] == bl[r])
for (int i = l;i <= r;i ++) ans += a[i];
else {
for (int i = l;i <= R[bl[l]];i ++) ans += a[i];
for (int i = bl[l] + 1;i < bl[r];i ++) ans += cnt[i];
for (int i = L[bl[r]];i <= r;i ++) ans += a[i];
}
printf("%lld\n",ans);
}
}
return 0;
}
题目小结
- 要分析好题目性质。
- 写完暴力后想一想通过性质优化算法。
- 这样就有可能直接拿下这道题。
P3203 [HNOI2010] 弹飞绵羊
思路
挺典的一道LCT模版题。
我们考虑怎么用分块做。
注意到分块可以将一些一起的运算捆在一起算,也就是说我们可以注意到让跳这个行为放在一个块跳到另外一个块上面,这样可以降低时间复杂度,故设:\(f_i\) 表示从 \(i\) 跳出这个块所需要的步数,\(to_i\) 表示跳到哪了。
我们可以 \(\mathcal{O}(n)\) 预处理得出,当然我是直接 \(\mathcal{O}(n\sqrt n)\) 得出的。
考虑查询,每次最多只用跳 \(\sqrt n\) 个块,因此时间复杂度为 \(\mathcal{O}(\sqrt n)\) 的。
我们注意到如果修改 \(x\) 这个点,由于 \(x\) 所在块前面的点跳到这里后都是归这个块管,所以说,我们只需要暴力地去维护当前块内的 \(f_i\) 和 \(to_i\) 即可。
这个算法十分精妙。
代码
时间复杂度 \(\mathcal{O}((n+m)\sqrt n)\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <vector>
#include <cmath>
#define int long long
#define N 200005
#define M 450
using namespace std;
int n,a[N],f[N],to[N],bl[N],L[M],R[M];
signed main(){
cin >> n;
for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]);
int len = ceil(sqrt(n));
for (int i = 1;i < M;i ++) L[i] = 1e18;
for (int i = 1;i <= n;i ++) {
bl[i] = i / len + 1;
L[bl[i]] = min(L[bl[i]],i);
R[bl[i]] = i;
}
for (int i = 1;i <= n;i ++) {
int now = i;
for(;bl[now] == bl[i];f[i] ++,now = now + a[now]);
to[i] = min(now,n + 1);
}
int m;
cin >> m;
for (int op,id,k;m --;) {
scanf("%lld%lld",&op,&id);
id ++;
if (op == 1) {
int now = id,times = 0;
for (;now < n + 1;times += f[now],now = to[now]);
printf("%lld\n",times);
}
else {
scanf("%lld",&k);
a[id] = k;
for (int i = R[bl[id]];i >= L[bl[id]];i --) {
if (i + a[i] > R[bl[id]]) {
f[i] = 1ll;
to[i] = min(i + a[i],n + 1);
}
else {
f[i] = f[i + a[i]] + 1;
to[i] = to[i + a[i]];
}
}
}
}
return 0;
}
题目小结
- 分块擅长处理这种可以捆绑一起运算的题目。
- 合在一起计算和分开计算本质一样可以考虑一下分块。
P5046 [Ynoi2019 模拟赛] Yuno loves sqrt technology I
吐槽
卡常卡了差不多 \(3,4\) 页。
思路
考虑求答案时是几个连续块,不难设 \(ans_{i,j}\) 表示第 \(i\) 到 \(j\) 个块的逆序对个数。为了求逆序对可以考虑用树状数组或设 \(cnt_{i,j}\) 表示前 \(i\) 个块权值 \(\leq j\) 的个数。
我们发现预处理这些其实可以做到 \(\mathcal{O}(n\sqrt n)\),具体而言就是先搞出 \(cnt\) 以及 \(ans\)。
我们发现有 \(ans_{i,j}=ans_{i,j-1}+ans_{i+1,j}-ans_{i+1,j-1}-\text{calc}(i,j,L_i,R_i,L_j,R_j)\)。
其中 \(\text{calc}(x,y,l_1,r_1,l_2,r_2)\) 表示的是第 \(i\) 个块的 \([l_1,r_1]\) 跟第 \(j\) 个块的 \([l_2,r_2]\) 拼在一起的逆序对个数。
考虑怎么搞这个 \(\text{calc}\),不妨枚举整个块,但只算再这个区间范围内的答案,算答案可以考虑尺取法先对 \(a\) 排序然后尺取即可。
具体而言:
inline ll calc(int x,int y,int l1,int r1,int l2,int r2) {
ll res = 0;
int j = L[y] - 1,tot = 0;
for (int i = L[x];i <= R[x];i ++) {
if (b[i].id < l1 || b[i].id > r1) continue;
while(j < R[y] && b[j + 1].val < b[i].val) {
tot += (l2 <= b[j + 1].id && b[j + 1].id <= r2);
j ++;
}
res += tot;
}
return res;
}
然后考虑如何求答案。
对于在同一个块内的我们显然不能直接树状数组整,可以考虑搞个 \(pre_i,nxt_i\) 表示 \(i\) 到这个块(首/尾)组成的序列逆序对是多少,这个是可以 \(\mathcal{O}(n\log n)\) 求出来的。
那么我们计算答案的时候无非就是把每个段的求出来(\(3\) 个),然后在前左边散块有多少个新增的,右边散块有多少个新增的即可,这个可以用 \(cnt\) 和 \(\text{calc}\) 求出。
代码
总时间复杂度是 \(\mathcal{O}((n+m)\sqrt n)\) 的,代码如下:
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <cstring>
#include <algorithm>
#include <vector>
#include <cmath>
//#define int long long
#define ll long long
#define N 100005
#define M 700
using namespace std;
//char buf[1<<20],*p1=buf,*p2=buf,obuf[1<<20],*o=obuf;
////#define g() getchar()
//#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
//
//#define isdigit(ch) ('0' <= ch && ch <= '9')
template<typename T>
inline void read(T &x) {
x = 0;
int f = 1;
char ch = getchar();
for (;!isdigit(ch);ch = getchar());
for (;isdigit(ch);ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
x *= f;
}
template<typename T>
inline void write(T x) {
if (x < 0) putchar('-'),x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
int tr[N];
inline void update(int x,int val,int n) {
for (;x <= n;x += x & -x) tr[x] += val;
}
inline int query(int x) {
int res = 0;
for (;x;x -= x & -x) res += tr[x];
return res;
}
struct identity{
int id,val;
}b[N];
int n,m,a[N],bl[N],L[M],R[M];
int pre[N],nxt[N],cnt[M][N];
ll ans[M][M];
inline ll calc(int x,int y,int l1,int r1,int l2,int r2) {
ll res = 0;
int j = L[y] - 1,tot = 0;
for (int i = L[x];i <= R[x];i ++) {
if (b[i].id < l1 || b[i].id > r1) continue;
while(j < R[y] && b[j + 1].val < b[i].val) {
tot += (l2 <= b[j + 1].id && b[j + 1].id <= r2);
j ++;
}
res += tot;
}
return res;
}
//cnt[M][M],cnt2[M][M],sum[M][M],sum2[M][M];
int i,j;
signed main(){
read(n),read(m);
for (i = 1;i+3 <= n;i += 4){
read(a[i]),b[i] = {i,a[i]};
read(a[i+1]),b[i+1] = {i+1,a[i+1]};
read(a[i+2]),b[i+2] = {i+2,a[i+2]};
read(a[i+3]),b[i+3] = {i+3,a[i+3]};
}
for (;i <= n;i++) read(a[i]),b[i] = {i,a[i]};
int len = ceil(sqrt(n)) / 2;
int block = n / len;
for (i = 1;i <= block;i ++) {
L[i] = R[i - 1] + 1,R[i] = i * len;
}
R[block] = n;
// for (register int i = 1;i < M;i ++) L[i] = 1e9;
// for (register int i = 1;i <= n;i ++) {
// bl[i] = i / len + 1;
// L[bl[i]] = min(L[bl[i]],i);
// R[bl[i]] = i;
// }
for (register int i = 1;i <= block;i ++) {
// memset(tr,0,sizeof tr);
for (register int j = L[i];j <= R[i];j ++) {
bl[j] = i;
cnt[i][a[j]] ++;
if (j != L[i])
pre[j] = pre[j - 1] + query(n) - query(a[j]);
update(a[j],1,n);
}
for (register int j = L[i];j <= R[i];j ++) update(a[j],-1,n);
for (register int j = R[i];j >= L[i];j --) {
if (j != R[i])
nxt[j] = nxt[j + 1] + query(a[j]);
update(a[j],1,n);
}
for (register int j = L[i];j <= R[i];j ++) update(a[j],-1,n);
ans[i][i] = pre[R[i]];
ll res = 0;
// for (int j = 1;j <= n;j ++) cnt[i][j] = cnt[i - 1][j];
// for (int j = L[i];j <= R[i];j ++) cnt[i][a[j]] ++;
// for (int j = 1;j <= n;j ++) cnt[i][j] += cnt[i][j - 1];
for (register int j = 1;j <= n;j ++) res += cnt[i][j],cnt[i][j] = res + cnt[i - 1][j];
stable_sort(b + L[i],b + R[i] + 1,[](identity x,identity y) {
return x.val < y.val;
});
}
for (register int k = 2;k <= bl[n];k ++)
for (register int i = 1;i + k - 1 <= bl[n];i ++) {
int j = i + k - 1;
ans[i][j] = ans[i][j - 1] + ans[i + 1][j] - ans[i + 1][j - 1] + calc(i,j,L[i],R[i],L[j],R[j]);
}
// for (int i = 1;i <= bl[n];i ++) {
// for (int j = L[i];j <= R[i];j ++) cnt[i][bl[a[j]]] ++;
// for (int j = 1;j <= bl[n];j ++) sum[i][j] = sum[i][j - 1] + cnt[i][j];
// for (int j = L[i];j <= R[i];j ++);
// }
ll lstans = 0;
for (int l,r;m --;) {
read(l),read(r);
l ^= lstans,r ^= lstans;
// cout << l << ' ' << r << '\n';
// r = min(r,n),l = max(1ll,l);
// if (l > r || r > n || l < 1) {
// lstans = 0;
// puts("0");
// continue;
// }
int pp = bl[l],qq = bl[r];
if (pp == qq) {
if (l == L[pp]) {
write(lstans = pre[r]);
putchar('\n');
continue;
}
write(lstans = pre[r] - pre[l - 1] - calc(pp,pp,pp,l - 1,l,r));
putchar('\n');
continue;
}
ll res = nxt[l] + ans[pp + 1][qq - 1] + pre[r] + calc(pp,qq,l,R[pp],L[qq],r);
// cout << res << '\n';
for (register int i = l;i <= R[pp];i ++) res += cnt[qq - 1][a[i]] - cnt[pp][a[i]];
for (register int i = L[qq];i <= r;i ++)
res += R[qq - 1] - L[pp + 1] + 1 - (cnt[qq - 1][a[i]] - cnt[pp][a[i]]);
write(lstans = res);
putchar('\n');
}
return 0;
}
/*
10 5
3 6 10 7 5 2 8 1 9 4
6 8
1 8
2 7
9 9
4 6
2
18
0
0
3
*/
题目小结
- 要卡常
long long改int很重要。 - 从要求求的答案考虑整个算法很重要。
P4168 [Violet] 蒲公英
题目概述
给出序列,询问 \(l,r\),求 \([l,r]\) 之间的最小区间众数。强制在线。
分析
离散化一下。
我们来回顾一下区间众数可以怎么求:树套树、回滚莫队、分块、摩尔投票算法(这个只能求绝对众数)。
然后发现强制在线基本上要么是树套树,要么就是分块。
树套树的方法很简单我就不过多阐述,因此我们讲一讲怎么分块做。
同样套路地先考虑散块与整块的关系,我们所希望的是散块暴力算,整块直接算。
主要的,我们需要统计数的个数,想要直接得到整块的答案不妨设 \(f_{i,j}\) 表示从整块 \(i\) 到整块 \(j\) 的最小众数。
但是万一加上散块的不是他了怎么办呢?也很简单,来个整块的桶的前缀和就行,即设 \(cnt_{i,j}\) 表示前 \(i\) 个整块 \(j\) 的个数是多少。
然后散块暴力加,判断能不能更新即可。
代码
时间复杂度 \(\mathcal{O}(m\sqrt n)\)。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stdlib.h>
#include <cstring>
#include <vector>
#include <cmath>
#define getf(x) (box[x] + cnt[bl[l] + 1][x] - cnt[bl[r]][x])
#define int long long
#define N 40005
#define M 350
using namespace std;
int n,m,a[N],bl[N],L[M],R[M],cnt[M][N],box[N],f[M][M];
vector<int> ls;
signed main(){
cin >> n >> m;
for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]),ls.push_back(a[i]);
stable_sort(ls.begin(),ls.end());
ls.erase(unique(ls.begin(),ls.end()),ls.end());
for (int i = 1;i <= n;i ++) a[i] = lower_bound(ls.begin(),ls.end(),a[i]) - ls.begin() + 1;
int len = ceil(sqrt(n));
for (int i = 1;i <= n;i ++) {
bl[i] = i / len + 1;
if (!L[bl[i]]) L[bl[i]] = i;
R[bl[i]] = i;
}
for (int i = 1;i <= bl[n];i ++)
for (int j = L[i];j <= n;j ++) cnt[i][a[j]] ++;
for (int i = 1;i <= bl[n];i ++) {
for (int j = 0;j <= ls.size();j ++) box[j] = 0;
int j = i;
int res = 0;
for (;j <= bl[n];j ++) {
for (int k = L[j];k <= R[j];k ++) {
box[a[k]] ++;
if (box[a[k]] > box[res]) res = a[k];
else if (box[a[k]] == box[res]) res = min(res,a[k]);
}
f[i][j] = res;
}
}
memset(box,0,sizeof box);
for (int l,r,lstans = 0;m --;) {
scanf("%lld%lld",&l,&r);
l = (l + lstans - 1) % n + 1,r = (r + lstans - 1) % n + 1;
if (l > r) swap(l,r);
if (bl[l] == bl[r]) {
int res = 0;
for (int i = l;i <= r;i ++) {
box[a[i]] ++;
if (box[a[i]] > box[res]) res = a[i];
else if (box[a[i]] == box[res]) res = min(res,a[i]);
}
for (int i = l;i <= r;i ++) box[a[i]] = 0;
res = ls[res - 1];
printf("%lld\n",lstans = res);
continue;
}
int res = 0;
if (bl[l] + 1 <= bl[r] - 1) res = f[bl[l] + 1][bl[r] - 1];
for (int i = l;i <= R[bl[l]];i ++) {
box[a[i]] ++;
int t1 = getf(a[i]),t2 = getf(res);
if (t1 > t2) res = a[i];
else if (t1 == t2) res = min(res,a[i]);
}
for (int i = L[bl[r]];i <= r;i ++) {
box[a[i]] ++;
int t1 = getf(a[i]),t2 = getf(res);
if (t1 > t2) res = a[i];
else if (t1 == t2) res = min(res,a[i]);
}
for (int i = l;i <= R[bl[l]];i ++) box[a[i]] = 0;
for (int i = L[bl[r]];i <= r;i ++) box[a[i]] = 0;
res = ls[res - 1];
printf("%lld\n",lstans = res);
}
return 0;
}
题目小结
- 捆绑算。
- 找到整块很散块的关系。
- 前缀和也许能够解决整块的部分。

浙公网安备 33010602011771号