Loading

分 块 套 娃

众所周知, 如果我们用正常的分块做单点加区间求和, 时间复杂度高达 \(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;
}

理论上来说再分一层的时间会更少, 但是写起来太麻烦了(

posted @ 2022-08-13 20:23  pjykk  阅读(36)  评论(0编辑  收藏  举报