zoukankan      html  css  js  c++  java
  • 分块——优化的暴力

    对于很多的题目,我们都可以找到n^2的暴力算法。

    但是,当n在10000到200000之间的时候,n^2基本稳稳卡掉。

    发现,这样的题目,经常还与区间有关系的时候,可以考虑分块做法。

     

    分块,顾名思义,就是把待处理的整个大区间分成若干块。

    口诀是:块外暴力,块内查表。

    那么这个块的大小应该怎么分呢??

    应该是sqrt(n)大小。

    证明:

    大概是:如果块大小为k,那么块内查表可能是n/k,然后块外暴力是k的,

    要平均一下,n/k=k,那么,k=sqrt(n)最优。

    这样,块内处理,和块连在一起处理,都是sqrt(n)的。

     

    自我感觉,分块很巧妙的把各种复杂度都向sqrt(N)靠近

    发现很多的题,都是时间复杂度n根号n,空间复杂度也是n根号n

    而且不管是什么操作,基本上都是根号n。既没有n^2,也不存在O(1)

    感觉,巧妙地把复杂度平均了一下。

     

    分块虽然是暴力,但是是一种非常有水平的暴力。

    关键是状态的设计,怎样达到块外暴力,块内查表。

    大概的感觉是,都要围绕块来进行设计。

    而且,通常要有两个以上函数状态有机配合。。。

     

    例题1:BZOJ 4241历史研究

    Description:

    一句话题意:求区间加权众数。

    Solution:

    f[i][j]表示从第i块开头到j的最大值 (查询的时候,块内查表)
    cnt[i][j]表示从第i块开始到序列末尾j出现了多少次(便于暴力中的后缀差分)
    边角余料处理一下就好啦~

    Code:

    一般情况下,对于块的左右端点有两种求法。

    1.lower_bound(blo+1,blo+n+1,blo[x])块的左端点。

    lower_bound(blo+1,blo+n+1,blo[x]+1)块的右端点+1

    2.处理块的时候,直接结构体记录块的左右端点。(推荐)

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=100050;
    const int B=333;
    ll f[B][N],cnt[B][N];
    int a[N],b[N],tot;
    int num[N],sta[N],top;
    int blo[N],BLO;
    int n,m;
    ll ans;
    int main()
    {
        scanf("%d%d",&n,&m);
        BLO=sqrt(n);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]),blo[i]=(i-1)/BLO+1,b[i]=a[i];
        sort(b+1,b+n+1);tot=unique(b+1,b+n+1)-b-1;
        for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+n+1,a[i])-b;
        for(int i=1;i<=blo[n];i++){
            ll now=0;
            for(int j=lower_bound(blo+1,blo+n+1,i)-blo;j<=n;j++){
                cnt[i][a[j]]++,now=max(now,(ll)cnt[i][a[j]]*b[a[j]]),f[i][j]=now;
            }
        }
        int xx,yy;
        while(m--){
            scanf("%d%d",&xx,&yy);ans=f[blo[xx]+1][yy];
            int tmp=lower_bound(blo+1,blo+n+1,blo[yy])-blo;top=0;
            for(int i=tmp;i<=yy;i++) num[a[i]]++,sta[++top]=a[i];
            tmp=lower_bound(blo+1,blo+n+1,blo[xx]+1)-blo;
            for(int i=xx;i<tmp;i++){
                num[a[i]]++;sta[++top]=a[i];
                ans=max(ans,(ll)((ll)num[a[i]]+cnt[blo[xx]+1][a[i]]-cnt[blo[yy]][a[i]])*b[a[i]]);
            }
            for(int i=1;i<=top;i++)num[sta[i]]=0;
            printf("%lld
    ",ans);
        }
        return 0;
    }

     

     

    例题2:[HNOI2010]弹飞绵羊

    Description:

    某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏。游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会往后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。

    Solution:

    (LCT裸题,我们不管这个,因为我不会)

    (upda2018.11.14:现在会了,LCT请见[学习笔记]动态树Link-Cut-Tree

    发现,直接枚举是n^2的。但是,许多的位置出发,可能会走到同一个位置,再跳出去。

    类似于记忆化的思想。

    假如设f[i]表示,从i开始,跳f[i]步出去。预处理倒序O(n)即可

    查询是O(1)的。

    但是一旦修改了,就会整个前面的f都会变。就凉凉了。。

    要是修改只改一部分就好了。

    即使查询不是O(1)也没关系啊。

     

    分块,就来把这两个复杂度平均一下,都变成sqrt(n)

    维护f[i],g[i]
    f[i] 表示跳几次可以跳出所在块
    g[i] 表示跳出所在块后到达的位置。
    在查询时,我们O(sqrt(n))的时间进行“整块”的模拟,可以得到结果。

    这样,我们以块为单位,出了块就交给别人去管就可以了。

    f,g的巧妙搭配,使得查询只需要模拟即可!!

    而且,修改时,只有块内收到了影响。暴力倒序修改即可。

     

    Code:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=200000+10;
    int n,m;
    int BLO,blo[N];
    int f[N],g[N];
    int a[N]; 
    int ans;
    struct node{
        int l,r;
    }kua[N];
    int main()
    {
        scanf("%d",&n);BLO=sqrt(n);
        for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]),blo[i]=(i-1)/BLO+1;
        if(!kua[blo[i]].l) kua[blo[i]].l=i;
        kua[blo[i]].r=i;
        }
        for(int i=1;i<=blo[n];i++){
            for(int j=kua[i].r;j>=kua[i].l;j--){
                int to=j;
                while(to<=kua[i].r){
                    if(!f[to]) to=to+a[j],f[j]++;
                    else f[j]+=f[to],to=g[to];
                }
                g[j]=to;
            }
        }
        scanf("%d",&m);
        int op,x,y;
        while(m--){
            scanf("%d%d",&op,&x);x++;
            if(op==1){
                ans=0;
                int to=x;
                while(to<=n){
                    ans+=f[to];
                    to=g[to];
                }
                printf("%d
    ",ans);
            }
            else{
                scanf("%d",&y);
                a[x]=y;
                for(int i=kua[blo[x]].r;i>=kua[blo[x]].l;i--){
                    f[i]=g[i]=0;
                    int to=i;
                    while(to<=kua[blo[x]].r){
                        if(!f[to]) to=to+a[i],f[i]++;
                        else f[i]+=f[to],to=g[to];
                    }
                    g[i]=to;
                }
            }
        }
        return 0;
    }

     

    upda:2018.9.22

    以上两个题都是关于序列分块。

    其实,我们可以分块的不止是序列这个东西。

    还可以是抽象的。

    比如,把询问的模数p分块。

    p<=sqrt(n),余数<=sqrt(n)

    p>sqrt(n),x=p*k+v的k也<=sqrt(n)

    也是一种平均一下复杂度,达到msqrt(n)的均衡。

    例题:9.22模拟赛 经典的区间mod p=v的个数查询

    静态无修O(nsqrt(n))区间逆序对

    n根号n解决在线无修区间逆序对问题

    逆序对两种求法:树状数组,归并排序

    后者如果在已经排好序的情况下,只要O(元素个数)即可完成逆序对统计~!

    然后对权值离散化,

    1.每个块开一个桶维护根号个权值

    2.以及每个位置的值在所在块的排名

    块内查询:预处理每个位置到块头的贡献,差分,再处理[u,l-1][l,r]的贡献(桶排+归并)

    整块之间:f[l,r]=f[l,r-1]+f[l+1,r]-f[l+1,r-1]+calc(l,r),calc(l,r)归并

    散块和整块:预处理每个位置对整块的影响(前面的,后面的),做一个后缀和/前缀和。然后枚举每一个散块,对所有整块影响用后缀和/前缀和差分即可

    散块和散块:归并

     

    带修O(nsqrt(n))区间第K大

    (大概值域不能太大,因为带修改就不能离散化了。。。)

    序列分块,值域也分块!

    每个块维护S[i],块内权值i*S~(i+1)*S-1的个数,以及S2[i],块内值为i的数的个数

    枚举权值在哪一块,确定之后再枚举权值是哪一个。

    跨越块,S开成前缀和的,零散部分暴力处理S和S2

    (可能也就带插入区间K小值配合块状链表用用了,别的时候树状数组套线段树就可以了)

  • 相关阅读:
    kfx格式的复活
    HTC(HTML Component)开发简介
    window.open window.showModelDialog 打开一个新窗口/子窗口中调用父窗口的方法
    用 Firebug 动态调试和优化应用程序
    HTC——浏览器上的舞者
    innerHTML、innerText和outerHTML、outerText的区别
    window.parent与window.opener的区别与使用
    window.parent与window.opener、window.showModalDialog的区别 opener和showModalDialog刷新父页面的方法
    HTML Component(HTC)
    Hibernate下数据批量处理解决方案
  • 原文地址:https://www.cnblogs.com/Miracevin/p/9403620.html
Copyright © 2011-2022 走看看