对于一个区间问题,即给定一定的范围(不一定是序列,也可以是树等),对于范围中的某一些连续区间进行操作时,我们可以将给定的范围分成k块,那么我们就可以将某一个区间[l , r]分为3段:左端点所在的一块,右端点所在的一块(无所谓是否完整)和中间的整块。对于左右端点所在的两块,我们暴力处理(一般来说是一个一个点处理),对于中间的整块,我们将一块一块处理,或直接多块一起处理。
那么,我们可以发现,最终的复杂度取决于我们究竟怎样对序列进行分块,若每一块分得很少,左右端点的暴力固然可以很轻松,但对于中间的块数就会变多;分得多,左右端点的暴力就会很复杂。所以,我们需要分析它的复杂度:假设序列长度为n,分为m块,对于暴力单点和一块的操作都是O(1),那么不难发现,整个的时间复杂度为O(n / m + m),由于均值不等式,m取√n时复杂度达到最低,总复杂度为O(q√n),q为操作次数。
你或许会问,如果操作不是O(1),还取√n吗?我只能说,√n或许不是最优,但一般来说满足大部分的题目要求。
(ps:1:数学大佬直接手推,请无视我说的话
2:说几个常用的,对于操作logn级的每一块大概:sqrt(n / logn))(用cmath的log2()函数就行了),对于莫队呢,每一块n2/3比较优秀。(不对的话别来找我))
你或许会问,我们为什么要用分块,直接用线段树维护不是更优秀吗?事实上,很多时候,题目需要维护的值不具有可加和性或者单调性(即不是和,不是最值之类的东西),这个时候,我们就需要用到分块,以达到直接维护序列中元素的信息。
理解了分块的思想,你就会发现,这其实就是个暴力,不过是个比较优秀的暴力罢了。说说它一般的实现,对于一些简单题目,直接无脑上分块,暴力就完事了,但对于有些题目,你还需要预处理一些信息(如某个元素在某块内出现次数、两个块之间的答案等)。那么我们就要注意了,一般来说,预处理的复杂度不会大于分块暴力的复杂度即n√n,所以在题目需要预处理的时候,一定要考虑清楚复杂度再写。比如我们看看这道题:
https://www.luogu.org/problemnew/show/P4135
这道题就需要预处理出现次数和答案,附代码:
#include
using namespace std;
const int maxn = 100010 , maxk = 1501;
inline char get_char()
{
static char buf[100000] , *p1 = buf , *p2 = buf;
if (p1 == p2)
{
p2 = (p1 = buf) + fread(buf , 1 , 100000 , stdin);
if (p1 == p2)
{
return EOF;
}
}
return *p1++;
}
inline int read()
{
int res;
char ch;
while (!isdigit(ch = get_char()));
res = ch - '0';
while (isdigit(ch = get_char()))
{
res = res * 10 + ch - '0';
}
return res;
}
int n , m , c , bc , top;
int id[maxn] , L[maxk] , R[maxk];
int ans[maxk][maxk] , cnt[maxk][maxn];
int mark[maxn] , num[maxn] , s[maxn];
void pre()
{
for (int i = 1; i <= id[n]; i++)
{
int tot = 0;
for (int j = L[i]; j <= n; j++)
{
cnt[i][num[j]]++;
if ((cnt[i][num[j]] & 1) && cnt[i][num[j]] > 1)
{
tot--;
}
else if (!(cnt[i][num[j]] & 1))
{
tot++;
}
if (id[j] != id[j + 1])
{
ans[i][id[j]] = tot;
}
}
}
}
int main()
{
//freopen("data.in" , "r" , stdin);
n = read() , c = read() , m = read();
int bc = sqrt(n / log2(n));
for (int i = 1; i <= n; i++)
{
id[i] = (i - 1) / bc + 1;
num[i] = read();
}
for (int i = 1; i <= id[n]; i++)
{
L[i] = (i - 1) * bc + 1 , R[i] = i * bc;
}
R[id[n]] = n;
pre();
int lastans = 0;
for (int i = 1; i <= m; i++)
{
int l = read() , r = read();
l = (l + lastans) % n + 1;
r = (r + lastans) % n + 1;
if (l > r)
{
swap(l , r);
}
//cout << l << " " << r << endl;
int res = ans[id[l] + 1][id[r] - 1];
top = 0;
if (id[l] == id[r])
{
for (int j = l; j <= r; j++)
{
mark[num[j]]++;
}
for (int j = l; j <= r; j++)
{
if (mark[num[j]])
{
if (!(mark[num[j]] & 1))
{
res++;
}
mark[num[j]] = 0;
}
}
}
else
{
for (int j = l; j <= R[id[l]]; j++)
{
mark[num[j]]++;
s[++top] = num[j];
}
for (int j = L[id[r]]; j <= r; j++)
{
mark[num[j]]++;
s[++top] = num[j];
}
for (int j = 1; j <= top; j++)
{
if (mark[s[j]])
{
int now = max(0 , cnt[id[l] + 1][s[j]] - cnt[id[r]][s[j]]);
mark[s[j]] += now;
if (mark[s[j]] & 1)
{
if (!(now & 1) && now > 0)
{
res--;
}
}
else
{
if ((now & 1) || now == 0)
{
res++;
}
}
mark[s[j]] = 0;
}
}
}
printf("%d\n" , lastans = res);
}
return 0;
}
emmm,如果觉得难以理解先别急。你可以先刷刷这几道题:
入门9题:http://hzwer.com/8053.html (没必要全刷)
觉得入门了就看看上面那道题吧。
进阶 : http://hzwer.com/category/algorithm/data-structure/basic-data-structure/piecemeal
顺便可以再bzoj上看看,题目不少。