分块 and 莫队

分块

一种暴力的数据结构,十分朴素的做法。能够处理许多问题。

基础分块

\(1\)P3372 【模板】线段树 1

经典老题,这次使用分块做法。

我们将整个序列分为若干大小为 \(T\) 的块,记录其块的和和懒标记,对 \([l,r]\) 进行操作时,设左边界 \(l\) 位与块 \(q\),左边界 \(r\) 位与块 \(p\)。我们对块 \(p,q\) 应当修改的部分进行朴素修改操作,直接修改区间和,对于 \([p+1,q-1]\) 之间的块,我们对其打上标记,再修改区间和。

同理,对于 \([l,r]\) 的区间和操作,我们还是对边缘朴素统计,对中间部分直接累加区间和。

分析这样做的时间复杂度,对于段边界朴素修改时间复杂度为 \(O(T)\),对中间部分简略修改为 \(O(\dfrac{n}{T})\)。总共进行 \(n\) 次操作,时间复杂度为 \(O(nT+\dfrac{n^2}{T})\)。其中,块的大小有一个取值技巧,另加号两边量级相等,也就是

\[nT=\dfrac{n^2}{T},T=\sqrt{n} \]

这也是为什么 \(T\) 的值经常取 \(\sqrt{n}\) 的原因。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const int N=1e5+10;
typedef long long ll;
inline void read(ll &a){
    ll 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<<3)+(x<<1)+ch-'0',ch=getchar();
    a=x*f;return;
}
ll a[N],n,m,t,lef[N],righ[N],pos[N],sum[N],add[N],op,x,y,k;
void change(ll l,ll r,ll k){
    ll p=pos[l],q=pos[r];
    if(p==q){
        for(int i=l;i<=r;i++)a[i]+=k;
        sum[p]+=(r-l+1)*k;
    }else{
        for(int i=p+1;i<q;i++)sum[i]+=(righ[i]-lef[i]+1)*k,add[i]+=k;
        for(int i=l;i<=righ[p];i++)a[i]+=k;
        sum[p]+=(righ[p]-l+1)*k;
        for(int i=lef[q];i<=r;i++)a[i]+=k;
        sum[q]+=(r-lef[q]+1)*k;
    }
    return;
}
ll ask(ll l,ll r){
    ll p=pos[l],q=pos[r],ans=0;
    if(p==q){
        for(int i=l;i<=r;i++)ans+=a[i]+add[p];
    }else{
        for(int i=p+1;i<q;i++)ans+=sum[i];
        for(int i=l;i<=righ[p];i++)ans+=a[i]+add[p];
        for(int i=lef[q];i<=r;i++)ans+=a[i]+add[q];
    }
    return ans;
}
int main(){
    read(n),read(m);
    t=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]);
    for(int i=1;i<=t;i++){
        lef[i]=(i-1)*t+1;
        righ[i]=i*t;
    }
    if(righ[t]<n)t++,lef[t]=righ[t-1]+1,righ[t]=n;
    for(int i=1;i<=t;i++){
        for(int j=lef[i];j<=righ[i];j++){
            pos[j]=i;
            sum[i]+=a[j];
        }
    }
    while(m--){
        read(op),read(x),read(y);
        if(op==1){
            read(k);
            change(x,y,k);
        }else printf("%lld\n",ask(x,y));
    }
    return 0;
}

\(2\)P3373 【模板】线段树 2

对于区间乘,区间加,区间求和的题目,还是换汤不换药,分别对乘和加操作建立懒标记的数组。在过程中取模即可。

在对边缘进行朴素操作时,应为有乘有加。所以会有懒标记下放的操作,但在询问求和时。则尽量不会下放懒标。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int N=1e5+10,mod=571373;
typedef long long ll;
inline void read(ll &a){
    ll 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<<3)+(x<<1)+ch-'0',ch=getchar();
    a=x*f;return;
}
ll a[N],n,m,t,L[N],R[N],pos[N],sum[N],add1[N],add2[N],op,x,y,k,Mod;
void cheng(ll l,ll r,ll k){
    ll p=pos[l],q=pos[r];
    if(p==q){
        sum[p]=0;
        for(int i=L[p];i<=R[p];i++){
            a[i]=(a[i]*add2[p]+add1[p])%mod;
            if(i>=l&&i<=r)a[i]=(a[i]*k)%mod;
            sum[p]=(sum[p]+a[i])%mod;
        }
        add1[p]=0,add2[p]=1;
    }else{
        for(int i=p+1;i<=q-1;i++){
            sum[i]=(sum[i]*k)%mod;
            add2[i]=(add2[i]*k)%mod,add1[i]=(add1[i]*k)%mod;
        }
        sum[p]=0;
        for(int i=L[p];i<=R[p];i++){
            a[i]=(a[i]*add2[p]+add1[p])%mod;
            if(i>=l)a[i]=(a[i]*k)%mod;
            sum[p]=(sum[p]+a[i])%mod;
        }
        add1[p]=0,add2[p]=1;
        sum[q]=0;
        for(int i=L[q];i<=R[q];i++){
            a[i]=(a[i]*add2[q]+add1[q])%mod;
            if(i<=r)a[i]=(a[i]*k)%mod;
            sum[q]=(sum[q]+a[i])%mod;
        }
        add1[q]=0,add2[q]=1;
    }
    return;
}
void jia(ll l,ll r,ll k){
    ll p=pos[l],q=pos[r];
    if(p==q){
        sum[p]=0;
        for(int i=L[p];i<=R[p];i++){
            a[i]=(a[i]*add2[p]+add1[p])%mod;
            if(i>=l&&i<=r)a[i]=(a[i]+k)%mod;
            sum[p]=(sum[p]+a[i])%mod;
        }
        add1[p]=0,add2[p]=1;
    }else{
        for(int i=p+1;i<=q-1;i++){
            sum[i]=(sum[i]+(R[i]-L[i]+1)*k)%mod;
            add1[i]=(add1[i]+k)%mod;
        }
        sum[p]=0;
        for(int i=L[p];i<=R[p];i++){
            a[i]=(a[i]*add2[p]+add1[p])%mod;
            if(i>=l)a[i]=(a[i]+k)%mod;
            sum[p]=(sum[p]+a[i])%mod;
        }
        add1[p]=0,add2[p]=1;
        sum[q]=0;
        for(int i=L[q];i<=R[q];i++){
            a[i]=(a[i]*add2[q]+add1[q])%mod;
            if(i<=r)a[i]=(a[i]+k)%mod;
            sum[q]=(sum[q]+a[i])%mod;
        }
        add1[q]=0,add2[q]=1;
    }
    return;
}
ll ask(ll l,ll r){
    ll p=pos[l],q=pos[r],ans=0;
    if(p==q){
        for(int i=l;i<=r;i++)ans=(ans+a[i]*add2[i]+add1[i])%mod;
    }else{
        for(int i=p+1;i<=q-1;i++)ans=(ans+sum[i])%mod;
        for(int i=l;i<=R[p];i++)ans=(ans+a[i]*add2[p]+add1[p])%mod;
        for(int i=L[q];i<=r;i++)ans=(ans+a[i]*add2[q]+add1[q])%mod;
    }
    return ans;
}
int main(){
    read(n),read(m),read(Mod);
    t=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]);
    for(int i=1;i<=t;i++){
        L[i]=(i-1)*t+1;
        R[i]=i*t;
    }
    if(R[t]<n)t++,L[t]=R[t-1]+1,R[t]=n;
    for(int i=1;i<=t;i++)add2[i]=1;
    for(int i=1;i<=t;i++){
        for(int j=L[i];j<=R[i];j++){
            pos[j]=i;
            sum[i]=(sum[i]+a[j])%mod;
        }
    }
    while(m--){
        read(op),read(x),read(y);
        if(op==1||op==2){
            read(k);
            if(op==2)jia(x,y,k);
            if(op==1)cheng(x,y,k);
        }else printf("%lld\n",ask(x,y));
    }
    return 0;
}

\(3\)P2801 教主的魔法

我们首先定义两个数组 \(a,b\),先同时储存英雄的身高,并按照编号分块。然后 \(a\) 数组不变,在按每个块的范围将 \(b\) 数组排序。同时记录每个块增加操作的懒标记 \(lazy_i\)

为何要对 \(b\) 数组进行排序?我们在查询操作时,对一整个块查询有对少人身高不低于 \(C\)。其实就是查询 \(a\) 数组中有多少元素大于等于 \(C-lazy_i\)。因为 \(b\) 数组经过排序,单调不减。所以可以二分查找 \(b\) 数组中第一个大于等于 \(C-lazy_i\) 的元素位置,记为 \(t\)。该块的右边界为 \(R_i\),左边界为 \(L_i\)。那么 \(b\) 中,\([t,R_i]\) 里的任何一个元素都大于 \(C-lazy_i\),这也是因为 \(b\) 数组单调不减的缘故。

那为何还要保留 \(a\) 数组?我们再想一想,在区间操作时。如果我们对一整个块中每个元素都增加 \(k\),显然对 \(b\) 数组中 \([L_i,R_i]\) 还是单调不减,因为每个元素都增加了,然而对边缘部分进行朴素操作时,块内的元素有的修改了,有的没有修改,单调性会受到影响。就需要再此排序,\(a\) 数组的作用就是朴素修改后将 \([L_i,R_i]\) 的部分 copy 到 \(b\) 数组继续排序。

记块的大小为 \(T\),排序速度显然 \(O(n\log n)\)。则时间复杂度为

\[O(n\times (T\log T+\dfrac{n}{T}))=O(nT\log T+\dfrac{n^2}{T}) \]

解得块的大小 \(T= \sqrt{\frac{n}{\log n}}\)。因为图方便,所以直接取了 \(\sqrt{n}\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath> 
#include <algorithm>
using namespace std;
const int N=1e6+10,M=1e3+10;
int n,q,a[N],t,pos[N],sum[M],add[M],b[N],x,y,c,L[M],R[M];
char str[3];
inline void read(int &a){
	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<<3)+(x<<1)+ch-'0',ch=getchar();
	a=x*f;return;
}
int lower(int l,int r,int k){
	int mid;
	//cout<<k<<endl;
	if(l==7&&r==9&&k==495)return 10;
	while(l<r){
		mid=(l+r)/2;
		if(b[mid]>=k)r=mid;
		else l=mid+1;
	}
	return l;
}
inline void jia(int l,int r,int k){
	int p=pos[l],q=pos[r];
	if(p==q){
		for(int i=L[p];i<=R[p];i++){
			if(i>=l&&i<=r)a[i]+=k;
			b[i]=a[i];
		}		
		sort(b+L[p],b+1+R[p]);
	}else{
		for(int i=p+1;i<=q-1;i++){
			add[i]+=k;
		}
		for(int i=L[p];i<=R[p];i++){
			if(i>=l)a[i]+=k;
			b[i]=a[i];
		}
		sort(b+L[p],b+1+R[p]);
		for(int i=L[q];i<=R[q];i++){
			if(i<=r)a[i]+=k;
			b[i]=a[i];
		}
		sort(b+L[q],b+1+R[q]);
	}
	return;
}
int ask(int l,int r,int k){
	int p=pos[l],q=pos[r],ans=0,kkk; 
	if(p==q){
		kkk=k-add[p];
		for(int i=l;i<=r;i++){
			if(a[i]>=kkk)ans++;
		}
	}else{
		kkk=k-add[p];
		for(int i=l;i<=R[p];i++)if(a[i]>=kkk)ans++;
		kkk=k-add[q];
		for(int i=L[q];i<=r;i++)if(a[i]>=kkk)ans++;
		for(int i=p+1;i<=q-1;i++){
			kkk=k-add[i];
			ans+=R[i]-lower(L[i],R[i],kkk)+1;
		}
	}
	return ans;
}
int main(){
	read(n),read(q);
	for(int i=1;i<=n;i++)read(a[i]),b[i]=a[i];
	t=sqrt(n);
	for(int i=1;i<=t;i++){
		L[i]=(i-1)*t+1;
		R[i]=i*t;
	}
	if(R[t]<n)t++,L[t]=R[t-1]+1,R[t]=n;
	for(int i=1;i<=t;i++){
		for(int j=L[i];j<=R[i];j++){
			pos[j]=i;
		}
		sort(b+L[i],b+1+R[i]);
	}
	while(q--){
		scanf("%s",str+1);
		read(x),read(y),read(c);
		if(str[1]=='M')jia(x,y,c);
		else printf("%d\n",ask(x,y,c));
	}
	return 0;
} 

有难度的分块

不会,还没写。

莫队

普通莫队

莫队算法,也可以说是一种数据结构,是一种能够有效解决静态区间问题的离线算法(数据结构)。如果一道题中,区间 \([l,r]\) 的答案可以转移到 \([l-1,r],[l,r-1],[l+1,r],[l,r+1]\) 的答案。那么就可以利用莫队解决,接下来通过几道具体莫队例题来说明这种算法。

\(1\)P3901 数列找不同

莫队模板题,虽然有其他做法。

如果 \([l,r]\) 中的各个数互不相同,可以将问题转化成 \([l,r]\) 内数的种数是否为 \(r-l+1\)。如果相等,那么区间中的所有数一定互不相同。关键如何求出序列中数的种数。

我们可以先将询问按照其右端点升序排序,然后依次处理 \(1,2\dots m\) 个询问,假设我们当前处理了区间为 \([l,r]\) 的询问,答案为 \(ans\),下一个询问是关于 \([ql,qr]\) 的。我们可以在 \(ans\) 的基础上得到下一个询问的答案。

如果 \(l<ql\),就另 \(l\) 不断增加,\(l\) 每次增加,都会有数从 \([l,r]\) 区间中删除。我们另 \(cnt_i\) 表示 \(i\) 在区间内出现的次数。如果要删除 \(a_l\)\(cnt_{a_l}-1=0\),这说明如果删去 \(a_l\) 或让区间内数的种数减一,我们就可以令 \(ans-1\)。同理,如果 \(l+1,r-1,r+1\)\(ans\) 的影响也可以分析出。

遗憾的是,如果特殊构造数据,仅仅依靠上述的算法,无法通过 \(10^5\) 的数据。虽然 \(r\) 的变化单调不减,但是 \(l\) 的变化则是无序的,最差情况下的复杂度可以达到 \(O(n^2)\)。而莫队算法则很好的解决了这个问题,他会首先对询问分块按照左端点分块,分为 \(\sqrt n\) 块,然后在每个点内部按照右端点排序,然后再处理询问。

这样做看似时间复杂度玄学,但其实大有考究。如果从第 \(i\) 的块的最后一个询问走到第 \(i+1\) 个块的第一个询问。那么单次转移最差 \(O(n)\),一共 \(\sqrt n\) 个块,时间复杂度 \(O(n\sqrt n)\)。在块内,每个询问的右端点单调不降,一个块内右端点的转移最差总共 \(O(n)\),一共 \(\sqrt n\) 个块,时间复杂度 \(O(n\sqrt n)\),左端点同一块内变化不超过 \(\sqrt n\),一共 \(n\) 个元素,总时间复杂度 \(O(n\sqrt n)\)。我们可以知道其时间复杂度最差在 \(O(n\sqrt n)\)。在运用中可能更快。

在代码中,有两个值得注意的点。

  1. 在转移时,我们习惯于用自加,自减运算,如果是新增加一个数,一般是运用 --l,++r,而在删除时,普遍是运用 l++,r--

  2. 在排序时,如果两个询问划分在同一个块中,如果是奇数块,右端点按照升序排序,如果是偶数编号块,则右端点按照降序排序。这是一个小小的常数优化,简单讲就是右端点在基数块中的最后一个询问走到了比较靠后的元素,而下一个偶数块的第一个询问的右端点靠前。这样就能有效转移右端点转移的常数。而块内右端点仍然具有单调性,所以对时间复杂度不影响。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=1e5+10;
struct question{
    int l,r,idx,ans=0;
}que[N];
int n,q,a[N],t,cnt[N],it1=0,it2=0,ans=0;
inline void read(int &a){
    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<<3)+(x<<1)+ch-'0',ch=getchar();
    a=x*f;return;
}
bool cmp1(question a,question b){return a.l/t==b.l/t?a.r<b.r:a.l<b.l;}
bool cmp2(question a,question b){return a.idx<b.idx;}
inline void del(int x){cnt[a[x]]--;if(cnt[a[x]]==0)ans--;}
inline void add(int x){cnt[a[x]]++;if(cnt[a[x]]==1)ans++;}
int main(){
    read(n),read(q);
    t=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]);
    for(int i=1;i<=q;i++)read(que[i].l),read(que[i].r),que[i].idx=i;
    sort(que+1,que+1+q,cmp1);
    it2=it1=1;cnt[a[1]]++,ans=1;
    for(int i=1;i<=q;i++){
        while(it1<que[i].l)del(it1++);
        while(it1>que[i].l)add(--it1);
        while(it2<que[i].r)add(++it2);
        while(it2>que[i].r)del(it2--);
        if(ans==it2-it1+1)que[i].ans=1;
    }
    sort(que+1,que+1+q,cmp2);
    for(int i=1;i<=q;i++){
        if(que[i].ans==1)printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

\(2\)P1494 [国家集训队] 小 Z 的袜子

我们维护一个桶 \(cnt\)\(cnt_i\) 表示颜色 \(i\) 的袜子在当前区间出现的次数,同时维护 \(ans\) 表示区间内抽取一双袜子的方案数。显然,如果当前区间内多增加了一只颜色为 \(k\) 的袜子,那么他和这个区间内的 \(cnt_k\) 只袜子都可以构成一双袜子。反之,颜色相同的袜子对数会减少 \(cnt_k-1\) 对。在 \(n\) 只袜子中随机选取两只袜子的方案为 \(\dfrac{(n-2)(n-1)}{2}\)。直接用合法方案数除以总方案数。

至于约分:\(\dfrac{a}{b}=\dfrac{a\div\gcd(a,b)}{b\div\gcd(a,b)},\gcd(a,b)=\begin{cases} a& b=0\\ \ \gcd(b,a\bmod b) & \text{其他情况} \end{cases}\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll n,m,a[N],t,l=1,r=0,ql,qr,cnt[N],ans,res[N][2];
struct node{
	ll l,r;
	int idx;
}b[N];
inline void read(ll &a){
	ll x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	a=f*x;return;
}
inline ll gcd(ll a,ll b){
	return b==0?a:gcd(b,a%b);
}
bool cmp(node a,node b){
	return (a.l/t)^(b.l/t)?a.l<b.l:((a.l/t)&1?(a.r<b.r):(a.r>b.r));
}
inline void del(ll x){
	cnt[a[x]]--;
	ans-=cnt[a[x]];
}
inline void add(ll x){
	ans+=cnt[a[x]];
	cnt[a[x]]++;
}
int main(){
	read(n),read(m);t=sqrt(n);
	for(int i=1;i<=n;i++)read(a[i]);
	for(int i=1;i<=m;i++)read(b[i].l),read(b[i].r),b[i].idx=i;
	sort(b+1,b+1+m,cmp);
	for(int i=1;i<=m;i++){
		ql=b[i].l,qr=b[i].r;
		while(l<ql)del(l++);
		while(l>ql)add(--l);
		while(r>qr)del(r--);
		while(r<qr)add(++r);
		res[b[i].idx][0]=ans;
		res[b[i].idx][1]=(qr-ql+1)*(qr-ql)/2;
	}
	for(int i=1;i<=m;i++){
		ll a=res[i][0],b=res[i][1];
		if(a==0){
			printf("0/1\n");
			continue;
		}
		ll t=gcd(a,b);a/=t,b/=t;
		printf("%lld/%lld\n",a,b);
	}
	return 0;
} 

\(3\)P2709 小B的询问

\(cnt_i\)表示当前区间 \(i\) 的个数,\(ans\) 表示 \(\sum\limits_{j=1}^k c_j^2\),若增加了一个 \(i\),对 \(c_i\) 的贡献就是完全平方公式(下面)

\[(a+1)^2=a^2+2a+1,(a-1)^2=a^2-2a+1 \]

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=5e4+10;
typedef long long ll;
struct node{
    ll l,r,idx;
}b[N];
ll n,m,k,t,a[N],l=1,r,ql,qr,cnt[N],ans,answer[N];
inline void read(ll &a){
    ll 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<<3)+(x<<1)+ch-'0',ch=getchar();
    a=x*f;return;
}
inline void write(ll x){
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
bool cmp(node a,node b){
    return (a.l/t)^(b.l/t)?a.l<b.l:((a.l/t)&1?a.r<b.r:a.r>b.r);
}
inline void add(int x){
    ans+=cnt[a[x]]*2+1;
    cnt[a[x]]++;
}
inline void del(int x){
    ans-=cnt[a[x]]*2-1;
    cnt[a[x]]--;
}
int main(){
    read(n),read(m),read(k);
    t=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]);
    for(int i=1;i<=m;i++)read(b[i].l),read(b[i].r),b[i].idx=i;
    sort(b+1,b+1+m,cmp);
    for(int i=1;i<=m;i++){
        ql=b[i].l,qr=b[i].r;
        while(l<ql)del(l++);
        while(l>ql)add(--l);
        while(r<qr)add(++r);
        while(r>qr)del(r--);
        answer[b[i].idx]=ans;
    }
    for(int i=1;i<=m;i++){
        write(answer[i]);putchar('\n');
    }
    return 0;
}

\(4\)P5268 [SNOI2017] 一个简单的询问

一个询问涉及了序列上的 \(4\) 个点,莫队显然无法解决。但根据前缀和的思想,我们可以把一个询问拆成 \(4\) 个涉及 \(2\) 个点的询问,再用莫队求解。拆分方法如下:

\(\text{S}(i,x)=\text{get}(1,i,x)\),则

\(\sum\limits_{x=0}^\infty \text{get}(l_1,r_1,x)\times \text{get}(l_2,r_2,x)=\sum\limits_{x=0}^\infty(\text{S}(r_1,x)-\text{S}(l_1-1,x))\times(\text{S}(r_2,x)-\text{S}(l_2-1,x))\)

\(\text{F}(i,j)=\sum\limits_{x=0}^\infty \text{S}(i,x)\times \text{S}(j,x)\)。则

\(=\sum\limits_{x=0}^\infty(\text{S}(r_1,x)-\text{S}(l_1-1,x))\times(\text{S}(r_2,x)-\text{S}(l_2-1,x))\)

\(=\sum\limits_{x=0}^\infty(\text{S}(r_1,x)\times\text{S}(r_2,x)+\text{S}(l_1-1,x)\times\text{S}(l_2-1,x)-\text{S}(r_1,x)\times\text{S}(l_2-1,x)-\text{S}(l_1-1,x)\times\text{S}(r_2,x))\)

\(=\text{F}(r_1,r_2)+\text{F}(l_1-1,l_2-1)-\text{F}(l_1-1,r_2)+\text{F}(l_2-1,r_2)\)

对于每个 \(\text{F}(i,j)\) 分别求解即可。我们可以维护两个 \(cnti,cntj\) 数组。如果 \(i\) 扩展时加入了 \(k\),那么所作贡献就是 \((\text{S}(i,k)+1)\times\text{S}(j,k)-\text{S}(i,k)\times\text{S}(j,k)=\text{S}(j,k)\)。加上另一个 \(cntj_k\) 就可以。同理,如果 \(j\) 扩展时加入了 \(k\),则加上 \(cnti_k\)。如果删去也如此

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=5e4+10;
typedef long long ll;
struct node{
	ll l,r,idx,f;
}b[4*N];
ll n,m,a[N],l,r,ql,qr,t,cntl[N],cntr[N],res[N],ans;
inline void read(ll &a){
    ll 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<<3)+(x<<1)+ch-'0',ch=getchar();
    a=x*f;return;
}
inline void write(ll x){
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
bool cmp(node a,node b){
	return (a.l/t)^(b.l/t)?a.l<b.l:((a.l/t)&1?a.r<b.r:a.r>b.r);
}
inline void del_l(int x){
    cntl[a[x]]--;
    ans-=cntr[a[x]];
}
inline void add_l(int x){
    cntl[a[x]]++;
    ans+=cntr[a[x]];
}
inline void del_r(int x){
    cntr[a[x]]--;
    ans-=cntl[a[x]];
}
inline void add_r(int x){
    cntr[a[x]]++;
    ans+=cntl[a[x]];
}
int main(){
	read(n);t=sqrt(n);
	for(int i=1;i<=n;i++)read(a[i]);
    read(m);
	for(int i=1;i<=m;i++){
		read(ql),read(qr),read(l),read(r);
        b[i*4-3]=node{qr,r,i,1};
        b[i*4-2]=node{ql-1,l-1,i,1};
        b[i*4-1]=node{ql-1,r,i,-1};
        b[i*4]=node{l-1,qr,i,-1};
	}
    for(int i=1;i<=4*m;i++)if(b[i].l>b[i].r)swap(b[i].l,b[i].r);
	sort(b+1,b+1+4*m,cmp);
    l=r=ans=0;
	for(int i=1;i<=4*m;i++){
		ql=b[i].l,qr=b[i].r;
        while(l<ql)add_l(++l);
        while(l>ql)del_l(l--);
        while(r<qr)add_r(++r);
        while(r>qr)del_r(r--);
        res[b[i].idx]+=b[i].f*ans;
	}
    for(int i=1;i<=m;i++){
        write(res[i]),putchar('\n');
    }
    return 0;
}

\(5\)P4462 [CQOI2018] 异或序列

题解

\(6\)P4396 [AHOI2013] 作业

一个比较笨的方法是,维护一个 \(cnt\) 数组,\(O(1)\)修改,每次的转移完成后,暴力求解 \(\sum\limits_{i=a}^b cnt_i\)\(\sum\limits_{i=a}^b [cnt_i\ne 0]\)。总算法复杂度 \(O(n^2\sqrt n)\)。稳稳超时。

考虑优化,在修改过程中,会对 \(cnt\) 进行单点修改,在查询时区间求和。显然可以使用树状数组维护值域。总时间复杂度下降至 \(O(n\sqrt n\log n)\)。可以勉强通过 \(10^5\)

这题还有一个时间复杂度更优的做法,如果我们利用分块来维护 \(cnt\)。单点修改 \(O(1)\),区间查询 \(O(\sqrt n)\)。总时间复杂度 \(O((n+m)\sqrt n)\)。和树状数组相比。其优势在于修改是 \(O(1)\) 的,树状数组是 \(O(\log n)\)。而修改的次数是 \(n\sqrt n\),查询次数 \(m\)。这就加大了分块的优势。几乎能够做到十分稳定的通过 \(10^5\) 的数据。

此外,双倍经验。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=1e5+10;
struct node{
    int l,r,a,b,idx;
}b[N];
int n,m,a[N],t,l,r,ql,qr,x,y,cnt[N],c[2][N],res[N][2];
inline void jia(int f,int x,int t){while(x<=n){c[f][x]+=t;x+=(x&-x);}}
inline int ask(int f,int x){int cnt=0;while(x>0){cnt+=c[f][x];x-=(x&-x);}return cnt;}
bool cmp(node a,node b){
    return (a.l/t)^(b.l/t)?a.l<b.l:((b.l/t)&1?a.r<b.r:a.r>b.r);
}
inline void add(int pos){
    jia(0,a[pos],1);
    if(!cnt[a[pos]])jia(1,a[pos],1);
    cnt[a[pos]]++;
}
inline void del(int pos){
    jia(0,a[pos],-1);
    cnt[a[pos]]--;
    if(!cnt[a[pos]])jia(1,a[pos],-1);
}
int main(){
    scanf("%d %d",&n,&m);t=sqrt(n);
    for(int i=1;i<=n;i++)scanf("%d",a+i);
    for(int i=1;i<=m;i++)scanf("%d %d %d %d",&b[i].l,&b[i].r,&b[i].a,&b[i].b),b[i].idx=i;
    sort(b+1,b+1+m,cmp);
    l=1;
    for(int i=1;i<=m;i++){
        ql=b[i].l,qr=b[i].r,x=b[i].a,y=b[i].b;
        while(l<ql)del(l++);
        while(l>ql)add(--l);
        while(r<qr)add(++r);
        while(r>qr)del(r--);
        res[b[i].idx][0]=ask(0,y)-ask(0,x-1);
        res[b[i].idx][1]=ask(1,y)-ask(1,x-1);
    }
    for(int i=1;i<=m;i++){
        printf("%d %d\n",res[i][0],res[i][1]);
    }
    return 0;
}

\(7\)大爷的字符串

大爷太强力了!(罗太音)

题面是很恶心的,梳理 rp 减一的条件。

  1. \(S\) 为空
  2. \(S\) 中有比当前的数大于等于现在的数

思考如何取使得 rp 最大。显然,无论如何都不会有两个数在同一个上升序列中,但是所有数都必须取完。也就是说,区间中那个数出现的个数最多,其数量决定了上升序列的数量。换成人话,就是求区间众数的出现个数。

因为操作设计单点加减,区间 \(\max\)。考虑使用线段树维护值域。但可以看到出题人丧心病狂。写值域线段树和暴力都只有 \(40\text{pts}\)所以我翻题解找到了时间复杂度最快的方法

维护两个数列 \(t,cnt\)\(cnt_x\) 表示当前区间 \(x\) 出现的次数。\(t_x\) 表示当前区间出现次数为 \(x\) 的数的个数。如果我们要增加一个数进入区间,那么可能会更新答案,取 \(\max\) 即可。如果要删去一个数,且其会影响答案。当且仅当删去的数 \(x\) 满足 \(cnt_x=ans,t_{cnt_x}=1\)。意思为当前区间中 \(x\) 是唯一的众数。随后将 \(ans=ans-1\)。因为当前区间中至少有一个出现次数为 \(cnt_x-1\)。因为 \(x\) 的出现次数减一后他的出现次数 \(cnt_x'=cnt_x-1\)\(cnt\) 表示修改前,\(cnt'\) 表示修改后。

听说这题好像有值域分块做法。

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const int N=2e5+10;
struct node{
    int l,r,idx;
}b[N];
int n,m,a[N],book[N],t1,l=1,r,ql,qr,res[N],tot,cnt[N],t[N],ans;
inline void add(int x){
    t[cnt[a[x]]]--;
    t[++cnt[a[x]]]++;
    ans=max(ans,cnt[a[x]]);
}
inline void del(int x){
    if(t[cnt[a[x]]]==1&&ans==cnt[a[x]])ans--;
    t[cnt[a[x]]]--;
    t[--cnt[a[x]]]++;
}
inline void read(int &a){
    char ch=getchar();
    while(ch<'0'||ch>'9')ch=getchar();
    while(ch>='0'&&ch<='9')a=(a<<3)+(a<<1)+ch-'0',ch=getchar();
    return;
}
inline void write(int a){
    if(a>9)write(a/10);
    putchar(a%10+'0');
}
bool cmp(node a,node b){
    return (a.l/t1)^(b.l/t1)?a.l<b.l:((a.l/t1)&1?a.r<b.r:a.r>b.r);
}
int main(){
    read(n),read(m);t1=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]),book[i]=a[i];
    for(int i=1;i<=m;i++){
        read(b[i].l),read(b[i].r),b[i].idx=i;
    }
    sort(book+1,book+1+n);
    tot=unique(book+1,book+1+n)-(book+1);
    for(int i=1;i<=n;i++)a[i]=lower_bound(book+1,book+1+tot,a[i])-book;
    sort(b+1,b+1+m,cmp);
    for(int i=1;i<=m;i++){
        ql=b[i].l,qr=b[i].r;
        while(l<ql)del(l++);
        while(l>ql)add(--l);
        while(r<qr)add(++r);
        while(r>qr)del(r--);
        res[b[i].idx]=ans;
    }
    for(int i=1;i<=m;i++){
        putchar('-');write(res[i]);putchar('\n');
    }
    return 0;
}

\(8\)Rmq Problem / mex

进行值域分块。维护 \(cnt_i,Cnt_i\) 分别表示当前区间中数字 \(i\) 出现的次数和第 \(i\) 块内出现的不同种类数的个数(值域块的上界是整个序列最大数)。显然每次边界扩张都可以做到 \(O(1)\) 转移。

在查询时,我们先查询答案在哪个块。具体来说,如果 \(Cnt_i\) 和第 \(i\) 块的长度不匹配。就说明该块内有一个自然数没有在当前区间出现,然后在这个块中枚举。如果所有块中出现的次数都满了,说明最小自然数就是序列最大数加一。枚举答案在那个块时间复杂度 \(O(\sqrt n)\)。朴素查找 \(O(\sqrt n)\)

修改次数 \(n\sqrt n\),单次修改时间复杂度 \(O(1)\)。询问次数 \(m\),单次查询时间复杂度 \(O(\sqrt n)\)。总时间复杂度 \(O((n+m)\sqrt n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N=2e5+10;
inline void read(int &x){
    char ch=getchar();
    while(ch<'0'||ch>'9')ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return;
}
inline void write(int x){
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
int n,m,a[N],l=1,r,ql,qr,res[N],maxn=-1,cnt[N],sum[500],L[500],R[500],t,pos[N],block;
struct node{
    int l,r,idx;
}b[N];
bool cmp(node a,node b){
    return (a.l/block)^(b.l/block)?a.l<b.l:((a.l/block)&1?a.r<b.r:a.r>b.r);
}
inline void add(int x){
    if(!cnt[a[x]])sum[pos[a[x]]]+=1;
    cnt[a[x]]++;
}
inline void del(int x){
    cnt[a[x]]--;
    if(!cnt[a[x]])sum[pos[a[x]]]-=1;
}
inline int ask(){
    for(int i=1;i<=t;i++){
        if(sum[i]!=(R[i]-L[i]+1)){
            for(int j=L[i];j<=R[i];j++){
                if(!cnt[j])return j;
            }
        }
    }
    return maxn+1;
}
int main(){
    read(n),read(m);block=sqrt(n);
    for(int i=1;i<=n;i++)read(a[i]),a[i]++,maxn=max(maxn,a[i]);
    for(int i=1;i<=m;i++)read(b[i].l),read(b[i].r),b[i].idx=i;
    sort(b+1,b+1+m,cmp);
    t=sqrt(maxn);
    for(int i=1;i<=t;i++)L[i]=(i-1)*t+1,R[i]=i*t;
    if(R[t]<=n)t++,L[t]=R[t-1]+1,R[t]=n;
    for(int i=1;i<=t;i++)for(int j=L[i];j<=R[i];j++)pos[j]=i;
    for(int i=1;i<=m;i++){
        ql=b[i].l,qr=b[i].r;
        while(l>ql)add(--l);
        while(l<ql)del(l++);
        while(r>qr)del(r--);
        while(r<qr)add(++r);
        res[b[i].idx]=ask()-1;
    }
    for(int i=1;i<=m;i++){
        write(res[i]);putchar('\n');
    }
    return 0;
}

莫队总结

真的很好用,在静态区间问题中可以发挥巨大的作用。

posted @ 2024-08-19 12:17  zuoqingyuan111  阅读(3)  评论(0编辑  收藏  举报