分塊 and 莫隊

zuoqingyuan111發表於2024-08-19

分塊

一種暴力的資料結構,十分樸素的做法。能夠處理許多問題。

基礎分塊

\(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;
}

莫隊總結

真的很好用,在靜態區間問題中可以發揮巨大的作用。

相關文章