猫史档案馆


【OI学习基础】C++和C的数列分块问题

用户:爵士OIer爵士OIer查看:0 回复:3 评论:0 创建时间:2020-06-13T12:37:11


 

 

 

 

 

 

序列上的分块问题

 

 

 

 

回顾&引入

 

 

 

 

分块算法是一种很常见的根号算法,一般它的时间复杂度会带根号。 
分块和线段树的区别在于,分块算法可以维护一些线段树维护不了的东西,例如单调队列等,线段树能维护的东西必须能够进行信息合并,而分块则不需要。不过,它们也有共同点,分块和线段树一样,分块需要支持类似标记合并的东西。 
简单来说,分块算法就是优化过后的喵。

 

 

 

 

模板代码

 

 

 

1.区间加法,单点查值

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,单点查值。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都加 c

若 opt=1,表示询问 ar 的值( l 和 c 忽略)。

对于每次询问,输出一行一个数字表示答案。

 

样例输入:

4
1 2 2 3
0 1 3 1
1 0 1 0
0 1 2 2
1 0 2 0

 

样例输出:

2
5

 

 

 

 

 

首先是add操作,将[l,r]之间的元素都加c,对于整块的内容来说,直接维护一个lazy标记即可,lazy标记就记录这个整块中被add了多少(注意不是记录加了多少次,是记录该整块中所有加的值的总和);而对于非整块来说,因为块内的元素并不是太多,喵也是可以。接着就是quary操作,这里比较简单的就是,它是个单点查询,故查询函数的返回值就是a[r]+(a[r]所在块中的lazy值)。

预处理和维护内容:先要把n给分成各个小块,经由其他大佬们的分析,分成每个块中最多放sqrt(n)个元素的时间复杂是最小的,在这里我们就不必再多考虑了直接拿来用就是。用blong[i]来记录原数组第i个元素在第几个块中;L[x]记录第x个块的左边界,R[x]记录第x个块的右边界;当然也少不了去维护lazy数组,lazy[x]记录的就是第x块被add了多少的值。

 

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50010;
int n,m,block,L[N],R[N],blong[N],num;
ll a[N],lazy[N];
/*
a[i]原数列元素
block块的大小
num块的个数 
blong[i]表示属于哪一块
L[i]表示第i块的左边界
R[i]表示第i块的右边界 
lazy[i]对第i块的懒惰标记 
*/
void build()
{
	block=sqrt(n);//每个块的大小,sqrt(n)时复杂度最低 
	if(n%block==0) num = n/block;
	else num = n/block+1;  //最后一个快可能不够block个 
	for(int i=1;i<=num;i++)
	{
		L[i]=(i-1)*block+1,R[i]=i*block;//每个块的左右边界	
	} 
	R[num] = n; //最后一块特殊处理
	for(int i=1;i<=n;i++)
	{
		blong[i]=(i-1)/block + 1;//块的个数从1开始,So不应写成i/block 
	}
	 
}
void add(int l,int r,ll c )
{
	if(blong[l]==blong[r])//在同一块中,直接喵处理 
	{
		for(int i=l;i<=r;i++)
		{
			a[i]+=c;
		}
		return; 
	}
	for(int i=l;i<=R[blong[l]];i++)//左侧不完整快 
	{
		a[i]+=c;
	}
	for(int i=blong[l]+1;i<=blong[r]-1;i++)//处理中间的完整快 
	{
		lazy[i] += c; 
	} 
	for(int i=L[blong[r]];i<=r;i++)//右侧不完整块 
	{
		a[i]+=c;
	} 
} 
ll quary(int x)
{
	return a[x]+lazy[blong[x]];
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	build();
	m=n;
	while(m--)
	{
		int op,l,r;
		ll c;
		scanf("%d%d%d%lld",&op,&l,&r,&c);
		if(op==0)
		{
			add(l,r,c);
		}
		else{
			printf("%lld\n",quary(r));
		}
	}
	return 0;
}

 

 

 

 

 

模板二

 

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的元素个数。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都加 c

若 opt=1,表示询问 [l,r] 中,小于 c2 的数字的个数。

对于每次询问,输出一行一个数字表示答案。

 

样例输入:

4
1 2 2 3
0 1 3 1
1 1 3 2
1 1 4 1
1 2 3 2

 

样例输出:

3
0
2

 

 

center_image

center_image

center_image

 

 

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50010;
int n,m,L[N],R[N],block,num,blong[N];
ll a[N],lazy[N];
vector<ll> v[N];
void build()
{
    block=sqrt(n);//每个块的大小,sqrt(n)时复杂度最低 
    if(n%block==0) num = n/block;
    else num = n/block + 1;  //最后一个块可能不够block个 
    for(int i=1;i<=n;i++)
    {
        blong[i]=(i-1)/block + 1;//块的个数从1开始,So不应写成i/block 
        v[blong[i]].push_back(a[i]);
    }
    for(int i=1;i<=num;i++)
    {
        L[i]=(i-1)*block+1,R[i]=i*block;//每个块的左右边界
    } 
    R[num] = n; //最后一块特殊处理  
    for(int i=1;i<=num;i++)
    {
        sort(v[i].begin(),v[i].end());
    }
}
void resort(int x)//对x块进行排序
{
    v[x].clear();
    for(int i=L[x];i<=R[x];i++)
    {
        v[x].push_back(a[i]);   
    } 
    sort(v[x].begin(),v[x].end());
} 
void update(int l,int r,ll c)
{
    if(blong[l]==blong[r])
    {
        for(int i=l;i<=r;i++)
        {
            a[i]+=c;
        }
        resort(blong[l]);
        return;
    }
    for(int i=l;i<=R[blong[l]];i++)
    {
        a[i]+=c;
    }
    for(int i=blong[l]+1;i<=blong[r]-1;i++)
    {
        lazy[i]+=c;
    }
    for(int i=L[blong[r]];i<=r;i++)
    {
        a[i]+=c;
    }
    resort(blong[l]);
    resort(blong[r]);
}
int quary(int l,int r,ll c)
{
    int ans=0;
    if(blong[l]==blong[r])
    {
        for(int i=l;i<=r;i++)
        {
            if(a[i]+lazy[blong[i]]<c)
            ans++;
        }
        return ans;
    }
    for(int i=l;i<=R[blong[l]];i++)
    {
        if(a[i]+lazy[blong[i]]<c)
        ans++;
    }
    for(int i=blong[l]+1;i<=blong[r]-1;i++)
    {
        //我么要查询的是a[i]+lazy[blong[i]]>=c的第一个位置(vector下标是从零开始的)
        //移项后也就是a[i]>=c-lazy[blong[i]]啦,注意此a[i](指vector容器中的元素)非彼a[i](指原数组)
        ans += lower_bound(v[i].begin(),v[i].end(),c-lazy[i])-v[i].begin();
    }
    for(int i=L[blong[r]];i<=r;i++)
    {
        if(a[i]+lazy[blong[i]]<c)
        ans++;
    }
    return ans;
}
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
inline ll Read(){
    ll  x=0;
    bool f=0;
    char ch=getchar();
    while (ch<'0'||'9'<ch)    f|=ch=='-', ch=getchar();
    while ('0'<=ch && ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return f?-x:x;
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++) a[i]=Read();
    build();
    m=n;
    while(m--)
    {
        int op,l,r;
        ll c;
        op=read(),l=read(),r=read();c=Read();
        if(op==0) update(l,r,c);
        else printf("%d\n",quary(l,r,c*c));
    }
    return 0;
}

 

 

 

 

 

模板三

 

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,区间求和。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都加 c

若 opt=1,表示询问 [l,r] 的所有数字的和 mod(c+1) 。

对于每次询问,输出一行一个数字表示答案。

 

样例输入:

4
1 2 2 3
0 1 3 1
1 1 4 4
0 1 2 2
1 1 2 4

 

样例输出:

1
4

 

center_image

 

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 50010;
ll a[N],n;
//b[i]=a[i]-a[i-1] 
ll tr1[N],tr2[N];//sum1用来维护 b[i]的前缀和,sum2用来维护i*b[i]的前缀和 
ll lowbit(ll x)
{
    return x & -x;
}
void add(ll tr[],ll x,ll c)
{
    for(int i=x;i<=n;i+=lowbit(i))
    {
        tr[i]+=c;
    }
}
ll getsum(ll tr[],ll x)
{
    ll res=0;
    for(int i=x;i;i-=lowbit(i))
    {
        res+=tr[i];
    } 
    return res; 
}
ll getans(ll x)
{
    return (x+1)*getsum(tr1,x)-getsum(tr2,x);
}
int main()
{
    scanf("%lld",&n);
    ll m=n;
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        ll c=a[i]-a[i-1];
        add(tr1,i,c);
        add(tr2,i,i*c);
    }
    while(m--)
    {
        ll op,l,r,c;
        scanf("%lld%lld%lld%lld",&op,&l,&r,&c);
        if(op==1)
        {
            printf("%lld\n",(getans(r)-getans(l-1))%(c+1));
        }
        else{
            add(tr1,l,c),add(tr2,l,l*c);
            add(tr1,r+1,-c),add(tr2,r+1,-c*(r+1));
        }
    }
    return 0;
}

 

 

 

 

 

模板四

 

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,询问区间内小于某个值 x 的前驱(比其小的最大元素)。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都加 c

若 opt=1,表示询问 [l,r] 中 c 的前驱的值(不存在则输出 1 )。

对于每次询问,输出一行一个数字表示答案。

 

center_image

 

 

 

 

 

模板五

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间开方,区间求和。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都开方。对于区间中每个 ai(lir, aiai−−√

若 opt=1,表示询问 [l,r] 的所有数字的和 。

对于每次询问,输出一行一个数字表示答案。

 

center_image

 

 

 

 

模板六

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及单点插入,单点询问,数据随机生成。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示在第 l 个数字前插入数字 r ( c 忽略)。

若 opt=1,表示询问 ar 的值 ( l 和 c 忽略)。

对于每次询问,输出一行一个数字表示答案。

 

center_image

center_image

 

 

 

 

模板七

 

 

给出一个长为 n 的数列,以及 n 个操作,操作涉及区间乘法,区间加法,单点询问。

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入四个数字 optlrc,以空格隔开。

若 opt=0,表示将位于 [l,r] 的之间的数字都加 c

若 opt=1,表示将位于 [l,r] 的之间的数字都乘 c

若 opt=2,表示询问 ar 的值 mod 10007 ( l 和 c 忽略)

对于每次询问,输出一行一个数字表示答案。

 

center_image

 

 

 

 

 

模板八

 

 

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

第一行输入一个数字 n

第二行输入 n 个数字,第 i 个数字为 ai,以空格隔开。

接下来输入 n 行询问,每行输入三个数字 lrc,以空格隔开。

表示先查询位于 [l,r] 的数字有多少个是 c,再把位于 [l,r] 的数字都改为 c

对于每次询问,输出一行一个数字表示答案。

 

center_imagecenter_image

 

 

 

 

习题:作诗

 

 

原题链接:https://www.luogu.com.cn/problem/P4135

 

 

center_image

 

 

分块思想定了就好办了。

注意这题无良卡时间和空间(虽然很大程度和bzoj老爷机有关)

我们还是预处理两个数组:

1.sum[i][j]:i元素在前j块出现的次数。

2.ans[i][j]:i~j块的正偶数个数的个数。

显然预处理之后对于询问我们就有了如下算法:

1.跨度<=2个块长度:直接喵。

2.跨度>2个块长度:显然区间一定跨过了至少一些/个连续的块,这些连续的块的正偶数个数的个数,先更新到cur(即最终答案中),然后枚举非整块区间内的数i,统计i在非整块区间内的个数t,如果:

1.连续的块内没有i:那么我们判断t的奇偶即可,如果是偶数,cur++。

2.连续的块内有i:

  设连续的块内i的个数为c。

  1.c偶数,t奇数:cur--;

  2.c奇数,t奇数:cur++;

返回cur即可。

Q1:ans数组怎么处理?

A1:我们可以很轻松处理sum数组,然后用和上面的方法一样的思想求解ans即可。

大致如下:

1.ans[i][j]=a[i][j-1];

(1.1:清空数组,注意只清当前块的数,不然TLE没话说)

2.统计j块元素的出现个数;

3.如同上面的方法判断即可。

 

center_imagecenter_imagecenter_imagecenter_image


回复

上一页1 页 / 共 1下一页
爵士OIer爵士OIer

dd

点赞1


评论


AlcalaAlcala

dd

点赞0


评论


L54321L54321

emotion_编程猫_点赞

点赞0


评论