跟着hzw学习数列分块

前言
早就看过黄学长这篇博客一直没有学习过,写牛客的第一场多校的时候有一道莫队的题,之后突然想起来这篇,所以学习了一下,不得不说这种分块儿思想,真是优美的暴力啊~下面我会上9道题(其实就是黄学长博客上面的题)来简述一下分块这种思想 。



给出一个长为 n n 的数列,以及 n n 个操作,操作涉及区间加法,单点查值。
思路:这道题可以用很多数据结构写,我们这里引入分块的思想,我们把这长度为n的序列分成m块,现有一个L,R区间进行加和操作,那么分为两种:对于块内,我们维护一个标记数组,记录的是他整个区间的加法操作,那么对于快外的两个区间的话,我们暴力的去修改,对于查询操作,就是当前节点的值加上他所在块儿的值。
那么讨论一下复杂度:
显然对于一次区间操作来说,最多每次操作会涉及 n+m n + m 个整块儿,以及最多, 2m 2 ∗ m 个分块操作,那么对于总的时间复杂度其实就是 O(n+m) O ( n + m ) ,根据均值不等式的计算我们可以知道 当 m=(n) m = ( n ) 的时候有最小值,所以我们分块的大小为 n n
那么对于总的时间复杂度就是 O(nn) O ( n ∗ n )
上代码把:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50000 + 10;
int a[maxn] , pos[maxn] , tar[maxn] , sz , n;
void update(int l,int r,int c)
{
    for(int i = l ; i <= min(pos[l]*sz ,r) ; i ++) a[i] += c; // 对于块外的元素(左区间)
    if(pos[l] != pos[r]) for(int i = (pos[r]-1)* sz +1; i <= r ; i++) a[i] += c; // 右区间
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++) tar[i] += c; // 对于块内元素直接加一个标记
}
int main()
{
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&a[i]);
        pos[i] = (i-1)/sz+1;
    }
    int op,l,r,c;
    for(int i = 0 ; i < n ; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0)
        {
            update(l,r,c);
        }
        if(op == 1) printf("%d\n",a[r] + tar[pos[r]]);

    }
}

2
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
根据前面的一道题我们已经大致可以了解出分块时我们要考虑的东西了,就是:
1.对于整块元素我们怎样办?
2.对于块外元素怎样办?
第二个问题一般都非常简单,暴力查小于x的值的元素个数,那么对于第一个问题,我们应该怎样做呢?
仔细想想我们块内的元素顺序其实已经没有那种重要了,我们只需要知道这些元素在一个块儿内就好了,所以我们可以….排序+二分。对吧,对于快内元素我们排序二分一下就好了,对于区间加操作我么还是快外暴力加,加完之后对于一个 n n 的块重新排序时间复杂度是 nlogn n ∗ log ⁡ n ,之后块内打标记,对于查询操作,快外暴力找,块内二分查,想一下时间复杂度,每次加法操作涉及区间O( n n ),对于查询,块外是 O(n) O ( n ) ,块内 nlogn n ∗ log ⁡ n ,总时间复杂度是
O(nlogn + n√nlog√n)
上代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50000 + 10;
int v[maxn] , pos[maxn] , tar[maxn] , sz , n;
vector<int>V[maxn];
void reset(int x)
{
    V[pos[x]].clear();
    for(int i = (pos[x]-1) * sz + 1 ; i <= min(pos[x]*sz,n) ; i++) V[pos[x]].push_back(v[i]); 
    sort(V[pos[x]].begin() , V[pos[x]].end());
}
void add(int l,int r,int add)
{
    for(int i = l ; i <= min(r,pos[l]*sz) ; i++) v[i] += add; reset(l);
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r] - 1) * sz + 1; i <= r ; i++) v[i] += add;
        reset(r);
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++) tar[i] += add;
}
int query(int l,int r,int target)
{
    int ans = 0 ;
    for(int i = l ; i <= min(r,pos[l]*sz) ; i++) if(v[i] + tar[pos[l]]< target) ans++;
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r] - 1) * sz + 1 ; i <= r ; i++) if(v[i] + tar[pos[r]] < target) ans ++;
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++)
    {
        int x = target - tar[i];
        ans += lower_bound(V[i].begin() , V[i].end() , x) - V[i].begin();
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&v[i]);
        pos[i] = (i-1)/sz+1;
        V[pos[i]].push_back(v[i]);
    }
    for(int i = 1 ; i <= pos[n] ; i++) sort(V[i].begin() , V[i].end());
    int op,l,r,c;
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0) add(l,r,c);
        if(op == 1) printf("%d\n",query(l,r,c*c));
    }
}

3.
给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
思路
和第二题有点像,改改就好了,有没有更简单的操作?
set。。每个块里维护一个set,之后在set里二分找
代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100000+10;
int v[maxn] , pos[maxn] , tar[maxn] , sz , n;
set<int>S[maxn];
void add(int l,int r,int add)
{
    for(int i = l ; i <= min(r,pos[l]*sz) ; i++)
    {
        S[pos[l]].erase(v[i]);
        v[i] += add; 
        S[pos[l]].insert(v[i]);
    }
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r] - 1) * sz + 1; i <= r ; i++)
        {
            S[pos[r]].erase(v[i]);
            v[i] += add;
            S[pos[r]].insert(v[i]);
        }
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++) tar[i] += add;
}
int query(int l,int r,int target)
{
    int ans = -1 ;
    for(int i = l ; i <= min(r,pos[l]*sz) ; i++) if(v[i] + tar[pos[l]] < target) ans = max(ans,v[i] + tar[pos[l]]); 
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r] - 1) * sz + 1 ; i <= r ; i++) if(v[i] + tar[pos[r]] < target) ans = max(ans,v[i] + tar[pos[r]]);
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++)
    {
        int x = target - tar[i];
        auto it = S[i].lower_bound(x);
        if(it == S[i].begin()) continue;
        --it;
        ans = max(ans,*it + tar[i]);
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&v[i]);
        pos[i] = (i-1)/sz+1;
        S[pos[i]].insert(v[i]);
    }
    int op,l,r,c;
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0) add(l,r,c);
        if(op == 1) printf("%d\n",query(l,r,c));
    }
}

4.给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。
思路
块外怎么办,块内怎么办?
对于修改来说都已经很简单了,那么对于查询我们如何快速的查询区间的和呢?
我们需要另外开一个sum数组来记录区间的和,对于块外暴力修改的结果我们直接加到相对应的块的中,对于整个块的操作来说我们还是和原来一样记录他的标记,之后对于区间的求和我们块外就可以他的值加上他所在块儿内的值,块内我们就可以直接sum加上我们维护的tar数组。感觉这里说的不是很清晰,看了一眼黄学长写的

“ 这题的询问变成了区间上的询问,不完整的块还是暴力;而要想快速统计完整块的答案,需要维护每个块的元素和,先要预处理一下。
考虑区间修改操作,不完整的块直接改,顺便更新块的元素和;完整的块类似之前标记的做法,直接根据块的元素和所加的值计算元素和的增量。”
精髓呀
代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50000 + 10;
long long v[maxn] ,  sum[maxn] , tar[maxn];
int n,sz,pos[maxn] ;
void update(int l,int r,int c)
{
    for(int i = l ; i <= min(pos[l] * sz , r) ; i++) v[i] += c,sum[pos[l]] += c;
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r] -1 ) * sz + 1 ; i <= r ; i++) v[i] += c,sum[pos[r]] += c;
    }
    for(int i = pos[l] + 1 ; i <= pos[r] -1 ; i++) tar[i] += c;
}
long long query(int l,int r)
{
    long long ans = 0;
    for(int i = l ; i <= min(pos[l] * sz , r) ; i++) ans += v[i] + tar[pos[l]];
    if(pos[l] != pos[r]) for(int i = (pos[r] -1)*sz + 1 ; i <= r; i++)
    {
        ans += v[i] + tar[pos[r]];
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++) ans += sum[i] + tar[i] * sz;
    return ans;
}
int main()
{
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%lld",&v[i]);
        pos[i] = (i-1)/sz+1;
        sum[pos[i]] += v[i];
    }
    int op,l,r,c;
    for(int i = 0 ; i < n ;i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0) update(l,r,c);
        if(op == 1) printf("%lld\n",query(l,r) % (c+1));        
    }
}

5
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
思路
这道题对应的应该是一道我写过的线段树的题,仔细想想一个int类型值每次向下取整最多4次就会变成0(???),那么我们暴力的去修改暴力的去求和就好了,之后在块儿上面打标记,倘若这个块儿里的值全为0了,那么我们就不去修改他
代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50000 + 10;
int pos[maxn] , v[maxn] , sz , sum[maxn] , tar[maxn] ,flag[maxn];
void slove_sqrt(int x)
{
    if(flag[x]) return ;
    flag[x] = 1 ;
    sum[x] = 0;
    for(int i = (x - 1) * sz + 1; i <= x*sz ;i ++)
    {
        v[i] = sqrt(v[i]);
        sum[x] += v[i];
        if(v[i] > 1) flag[x] = 0;
    }
}
void update(int l,int r,int c)
{
    for(int i = l ; i <= min(pos[l]*sz , r) ; i++)
    {
        sum[pos[l]] -= v[i];
        v[i] = sqrt(v[i]);
        sum[pos[l]] += v[i];
    }
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r]-1) * sz + 1 ; i <= r ; i++)
        {
            sum[pos[r]] -= v[i];
            v[i] = sqrt(v[i]);
            sum[pos[r]] += v[i];
        }
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++)
    {
        slove_sqrt(i);
    }
}
int query(int l,int r)
{
    int ans = 0 ;
    for(int i = l ; i <= min(r,pos[l] * sz) ; i++) ans += v[i];
    if(pos[l] != pos[r])
    {
        for(int i = (pos[r]-1) * sz + 1 ; i <= r ; i++) ans+=v[i];
    }
    for(int i = pos[l]+ 1 ; i <= pos[r] - 1 ; i ++) ans += sum[i];
    return ans;
}
int main()
{
    int n;
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&v[i]);
        pos[i] = (i-1)/sz+1;
        sum[pos[i]] += v[i];
    }
    int op,l,r,c;
    for(int i = 0 ; i < n ; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0) update(l,r,c);
        if(op == 1)printf("%d\n",query(l,r));
    }
}

6
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
思路
对于一个块儿他可以插入数据我们很显然要用vector,开一个二维的vector ,之后在找到相应位置的时候,插在这个块里,但是当某一个块儿中的数过多的时候我们的复杂度就会变得不稳定,于是就引入了重新分块的这一操作,简单来说就是将这些块重新分组
代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 50000 + 10;
int pos[maxn] , v[maxn] , sz , sum[maxn] ,flag[maxn] , m ,te[maxn];
vector<int>V[maxn];
pair<int ,int> query(int x)
{
    int P = 1;
    while(x > V[P].size())
    {
        x -= V[P].size();
        P++;
    }
    return make_pair(P,x-1);
}
void rebuild()
{
    int cnt = 1;
    for(int i = 1 ; i <= m ; i ++)
    {
        for(int j = 0 ; j < V[i].size() ; j ++)
        {
            te[cnt++] = V[i][j];
        }
        V[i].clear();
    }
    int pos2 = sqrt(cnt);
    for(int i = 1 ; i <= cnt ; i++)
    {
        V[(i-1)/pos2+1].push_back(te[i]);
    }
    m = (cnt-1)/sz+1;
}
void insert(int l,int r)
{
    pair<int ,int > T = query(l);
    V[T.first].insert(V[T.first].begin()+T.second,r);
    if(V[T.first].size() > 20 * sz) rebuild();
}
int main()
{
    int n;
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&v[i]);
        V[(i-1)/sz+1].push_back(v[i]);
    }
    m = (n-1)/sz+1;
    int op,l,r,c;
    for(int i = 0 ; i < n ; i++)
    {
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op == 0) insert(l,r);
        if(op == 1) {
            pair<int,int> T = query(r);
            cout<<V[T.first][T.second]<<endl;
        }
    }
}

7.给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。
思路
还是老套路,块外怎么办,块内怎么办?
由于涉及加和乘操作,所以我们可以想到维护两个数组一个加法数组一个乘法数组(显然??), (3+7)3==33+73 ( 3 + 7 ) ∗ 3 == 3 ∗ 3 + 7 ∗ 3 对吧,那么每次涉及到乘法操作的时候就是他的加法数组也要乘上这个数字,考虑一下如果一个值的所在块已经有标记了,之后我们暴力去修改他会不会出问题?当然会了,所以这里我们要有一个线段树的类似push_down操作,当我们暴力去修改块儿元素的时候要把标记下放到我们的原数组里,之后标记清空就好了。
代码

#include <bits/stdc++.h>
const int maxn = 100000 + 10;
const int mod = 10007;
using namespace std;
int v[maxn],pos[maxn],sum[maxn],mtar[maxn],atar[maxn];
int sz , n;
void reset(int x)
{
    for(int i=(x-1)*sz+1;i<=min(n,x*sz);i++)
        v[i]=(v[i]*mtar[x]+atar[x])%mod;
    atar[x]=0;mtar[x]=1;
}
void update(int l,int r,int c,int op)
{
    reset(pos[l]);
    for(int i = l ; i <= min(r,pos[l]*sz) ; i++)
    {
        if(op) v[i] *= c;
        else v[i] += c;;
        v[i]%=mod;
    }
    if(pos[l] != pos[r])
    {
        reset(pos[r]);
        for(int i = (pos[r] - 1 ) * sz + 1 ; i <= r ; i ++)
        {
            if(op) v[i] *= c;
            else v[i] += c;
            v[i] %= mod;
        }
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++)
    {
        if(op){
            atar[i] = (atar[i] * c) % mod;
            mtar[i] = (mtar[i] * c) % mod;
        }
        else atar[i] = (atar[i] + c) % mod;
    }
}
int main()
{
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ;i ++)
    {
        scanf("%d",&v[i]);
        pos[i] = (i-1)/sz+1;
        sum[pos[i]] += v[i];
        mtar[i] = 1;
    }
    int op,a,b,c;
    for(int i = 0 ; i < n ; i++)
    {
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op == 2) printf("%d\n",((v[b] * mtar[pos[b]] + atar[pos[b]]) ) % mod);
        else update(a,b,c,op);
    }
}

8
给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。
思路

黄学长的解析:区间修改没有什么难度,这题难在区间查询比较奇怪,因为权值种类比较多,似乎没有什么好的维护方法。

模拟一些数据可以发现,询问后一整段都会被修改,几次询问后数列可能只剩下几段不同的区间了。

我们思考这样一个暴力,还是分块,维护每个分块是否只有一种权值,区间操作的时候,对于同权值的一个块就O(1)统计答案,否则暴力统计答案,并修改标记,不完整的块也暴力。

这样看似最差情况每次都会耗费O(n)的时间,但其实可以这样分析:

假设初始序列都是同一个值,那么查询是O(√n),如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是O(√n)。

换句话说,要想让一个操作耗费O(n)的时间,要先花费√n个操作对数列进行修改。

初始序列不同值,经过类似分析后,就可以放心的暴力啦。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100000+10;
int sz,v[maxn] , pos[maxn] , n , l,r,c , tar[maxn];
void reset(int x)
{
    if(tar[x] == -1) return ;
    for(int i = (x-1)*sz+1 ; i <= min(n,x*sz) ; i++) v[i] = tar[x];
    tar[x] = -1;
}
int update(int l,int r,int c)
{
    int ans = 0 ;
    reset(pos[l]);
    for(int i = l ; i <= min(r,pos[l] * sz ) ; i++)
    {
        if(v[i] != c) v[i] = c;
        else ans++;
    }
    if(pos[l] != pos[r])
    {
        reset(pos[r]);
        for(int i = (pos[r] - 1) * sz + 1 ; i <= r; i ++)
        {
            if(v[i] != c) v[i] = c;
            else ans++;
        }
    }
    for(int i = pos[l] + 1 ; i <= pos[r] - 1 ; i++)
    {
        if(tar[i] != -1)
        {
            if(tar[i] == c) ans+=sz;
            else tar[i] = c;
        }
        else 
        {
            for(int j = (i-1) * sz + 1 ; j <= min(n,i*sz) ;  j++)
            {
                if(v[j] != c) v[j] =c;
                else ans++;
            }
            tar[i] = c;
        }
    }
    return ans;
}
int main()
{
    memset(tar,-1,sizeof(tar));
    scanf("%d",&n);
    sz = sqrt(n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%d",&v[i]);
        pos[i] = (i-1)/sz+1;
    }
    for(int i = 0 ; i < n ; i++)
    {
        scanf("%d%d%d",&l,&r,&c);
        printf("%d\n",update(l,r,c));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值