分 块 套 娃
众所周知, 如果我们用正常的分块做单点加区间求和, 时间复杂度高达 \(O(1)-O(n^{1/2})\) (修改-询问).
然后你发现这太慢了. 于是你考虑改一下分块的大小.
我们更改块的大小为 \(n^{2/3}\), 整块的复杂度成功变成了 \(O(n^{1/3})\)!
但是散块的复杂度变成了 \(O(n^{2/3})\), 成功劣化.
于是你考虑套娃, 对每个块再套用正常的分块, 块的大小是 \(n^{1/3}\).
于是时间复杂度就神奇地变成了 \(O(1)-O(n^{1/3})\)!
继续套娃.
分块大小改为 \(O(n^{3/4})\), 这样整块的复杂度就是 \(O(n^{1/4})\), 然后考虑使用刚刚得出的算法, 散块也是 \(O(n^{1/4})\)!
以此类推, 只要我们分 \(k\) 层, 我们就得到了 \(O(k)-O(n^{1/k})\) 的算法, 我们取一个 \(k\) 使得 \(k=n^{1/k}\), 容易发现 \(k\) 小于 \(\log n\), 我们取得了比线段树还优秀的时间复杂度!
这样分析太大意了. 实际上, 随着层数的增长, 查询的散块数量会指数级增长, 然后这个算法就寄掉了.
但是 \(O(1)-O(n^{1/3})\) 算法, 甚至 \(O(1)-O(n^{1/4})\) 都是可以试试的 (别问, 问就 ggb 画的
另外这种奇怪的思路可以被应用到二维分块中 (不会qaq), 我第一次看到是在 这里.
upd: \(O(n^{1/3})\) 神奇分块碾压 \(O(n^{1/2})\) 分块!!!!1
提交记录:
正常分块, 共耗时2.14s
神奇分块, 仅耗时1.11s
下面给出实现代码. 公平起见, 两份代码风格基本完全相同, 并且我没有对这两份代码进行任何的卡常.
注: 两份代码提交时间不同, 神奇分块是在半夜交的. 但是我随后也交了一份正常分块, 结果耗时反而变长了(
正常分块实现:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int B=720;//这个块长我简单地调过
int n,m,a[500010],sum[500010],bel[500010];
void add(int pos,int k)
{
a[pos]+=k;
sum[bel[pos]]+=k;
}
int query(int l,int r)
{
int lpos=bel[l],rpos=bel[r],ans=0;
if(lpos==rpos)for(int i=l;i<=r;i++)ans+=a[i];
else
{
for(int i=lpos+1;i<=rpos-1;i++)ans+=sum[i];
for(int i=l;bel[i]==lpos;i++)ans+=a[i];
for(int i=r;bel[i]==rpos;i--)ans+=a[i];
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=n;i++)bel[i]=(i-1)/B+1;
for(int i=1;i<=n;i++)sum[bel[i]]+=a[i];//实践证明这里一起算和分开算时间几乎一样
for(int i=1;i<=m;i++)
{
int opt,x,y;scanf("%d%d%d",&opt,&x,&y);
if(opt==1)add(x,y);
else printf("%d\n",query(x,y));
}
return 0;
}
神奇分块:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn=500010,B1=6400,B2=80;
int n,m,a[maxn],bel1[maxn],bel2[maxn],sum1[B2],sum2[B1];//这里的数组大小要小心计算
void add(int pos,int k)
{
a[pos]+=k;
sum1[bel1[pos]]+=k;
sum2[bel2[pos]]+=k;
}
int query2(int l,int r)
{
int lpos=bel2[l],rpos=bel2[r],ans=0;
if(lpos==rpos)for(int i=l;i<=r;i++)ans+=a[i];
else
{
for(int i=lpos+1;i<=rpos-1;i++)ans+=sum2[i];
for(int i=l;bel2[i]==lpos;i++)ans+=a[i];
for(int i=r;bel2[i]==rpos;i--)ans+=a[i];
}
return ans;
}
int query1(int l,int r)
{
int lpos=bel1[l],rpos=bel1[r],ans=0;
if(lpos==rpos)ans=query2(l,r);
else
{
for(int i=lpos+1;i<=rpos-1;i++)ans+=sum1[i];
ans+=query2(l,lpos*B1);
ans+=query2((rpos-1)*B1+1,r);
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
bel1[i]=(i-1)/B1+1;
sum1[bel1[i]]+=a[i];
bel2[i]=(i-1)%B1/B2+B2*(bel1[i]-1)+1;//神奇地计算出bel2, 这样算需要保证 B1=B2*B2
sum2[bel2[i]]+=a[i];
}
for(int i=1;i<=m;i++)
{
int opt,x,y;scanf("%d%d%d",&opt,&x,&y);
if(opt==1)add(x,y);
else printf("%d\n",query1(x,y));
}
return 0;
}
理论上来说再分一层的时间会更少, 但是写起来太麻烦了(