分塊——優雅的暴力

williamYcY發表於2024-06-04

下面介紹一種暴力,當然呢這種暴力比一般快很多。
先說一下這個暴力的思路。對於一個長度為\(n\)的陣列\(a\),可以把陣列\(a\)分成\(k\)塊,其中每一塊的長度為\(len\),當然最後一行除外因為\(n\)可能不是\(k\)的倍數,最後一塊的長度可以不是\(len\)
那麼就可以用這些塊來維護資料。
那麼對於一個區間\([l..r]\)可以分成兩種情況:
\(1\).區間\([l..r]\)在同一個塊裡也就是:
image
這種情況下可以暴力列舉\(l\)\(r\)即可。
\(2\).區間\([l..r]\)不在同一個塊裡也就是:、
image
這種情況下首先應該考慮中間整塊的部分(圖中\(3,4f\)),然後就是\(l\)\(r\)中的殘塊也就是\(2\)\(5\)中間的那一部分。

例題I 線段樹1

點選檢視題面 ## 題目描述

如題,已知一個數列,你需要進行下面兩種操作:

  1. 將某區間每一個數加上 \(k\)
  2. 求出某區間每一個數的和。

輸入格式

第一行包含兩個整數 \(n, m\),分別表示該數列數字的個數和操作的總個數。

第二行包含 \(n\) 個用空格分隔的整數,其中第 \(i\) 個數字表示數列第 \(i\) 項的初始值。

接下來 \(m\) 行每行包含 \(3\)\(4\) 個整數,表示一個操作,具體如下:

  1. 1 x y k:將區間 \([x, y]\) 內每個數加上 \(k\)
  2. 2 x y:輸出區間 \([x, y]\) 內每個數的和。

輸出格式

輸出包含若干行整數,即為所有操作 2 的結果。

樣例 #1

樣例輸入 #1

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

樣例輸出 #1

11
8
20

提示

對於 \(30\%\) 的資料:\(n \le 8\)\(m \le 10\)
對於 \(70\%\) 的資料:\(n \le {10}^3\)\(m \le {10}^4\)
對於 \(100\%\) 的資料:\(1 \le n, m \le {10}^5\)

保證任意時刻數列中所有元素的絕對值之和 \(\le {10}^{18}\)

【樣例解釋】

現在這一題是要支援區間求和,很顯然用分塊來維護區間和,那麼可以用一個陣列\(s\)表示當前塊的最大和。
對於每一次的詢問仍舊分為兩部分\(l\)\(r\)在同一塊裡和\(l\)\(r\)不在同一塊裡。
那麼程式碼就是:

int query(int l,int r){
    int sid=id[l],eid=id[r];//表示開始塊編號和結束塊編號
    if(sid==eid){//l,r在同一塊裡
        int sum=0;
        for(int i=l;i<=r;i++)sum+=a[i]+b[sid];//這裡的a是原陣列,b是每一塊的懶標記
        return sum;
    }
    int sum=0;
    for(int i=l;id[i]==sid;i++)sum+=a[i]+b[sid];//l所對應的殘塊
    for(int i=sid+1;i<eid;i++)sum+=s[i];//l,r中間的真快,s是每一塊的和
    for(int i=r;id[i]==eid;i--)sum+=a[i]+b[eid];//r所對應的殘塊
    return sum;
}

修改的方法和詢問是一樣的

void add(int l,int r,int x){
	int sid=id[l],eid=id[r];
	if(sid==eid){
		for(int i = l;i <= r;i ++ )s[sid] += x,a[i] += x;
		return;
	}
	for(int i = l;id[i] == id[l];i ++ ) s[id[l]] +=x,a[i] +=x;
	for(int i = sid + 1;i < eid;i ++ )s[i] += len * x,b[i] += x;
	for(int i = r;id[i] == id[r];i -- ) s[id[r]] +=x, a[i] += x;
}

完整程式碼:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
int a[N],b[N],s[N];
int id[N];
int n,m,len;
int query(int l,int r){
    int sid=id[l],eid=id[r];
    if(sid==eid){
        int sum=0;
        for(int i=l;i<=r;i++)sum+=a[i]+b[sid];
        return sum;
    }
    int sum=0;
    for(int i=l;id[i]==sid;i++)sum+=a[i]+b[sid];
    for(int i=sid+1;i<eid;i++)sum+=s[i];
    for(int i=r;id[i]==eid;i--)sum+=a[i]+b[eid];
    return sum;
}
void add(int l,int r,int x){
    int sid=id[l],eid=id[r];
    if(sid==eid){
        for(int i = l;i <= r;i ++ ){
            s[sid] += x;
            a[i] += x;
        }
        return;
    }
    for(int i = l;id[i] == id[l];i ++ ) s[id[l]] +=x,a[i] +=x;
    for(int i = r;id[i] == id[r];i -- ) s[id[r]] +=x, a[i] += x;
    for(int i = sid + 1;i < eid;i ++ ){
        s[i] += len * x;
        b[i] += x;
    }
}
signed main(){
    cin>>n>>m;
    len=sqrt(n);
    for(int i=1;i<=n;i++){
        cin>>a[i];
        int cnt=(i-1)/len+1;
        id[i]=cnt;
        s[cnt]+=a[i];
    }
    while(m--){
        int ops,l,r,c;cin>>ops>>l>>r;
        if(ops==1)cin>>c,add(l,r,c);
        else cout<<query(l,r)<<'\n';
    }
    return 0;
}

例題II

點選檢視題面 # Anton and Permutation

題面描述

有一個長度為 \(n\) 的排列,初始為 \(1,2,\dots,n\)

現在對其進行 \(k\) 次操作,每次操作都是交換序列中的某兩個數。對於每一個操作,回答當前序列中有多少個逆序對。

樣例 #1

樣例輸入 #1

5 4
4 5
2 4
2 5
2 2

樣例輸出 #1

1
4
3
3

樣例 #2

樣例輸入 #2

2 1
2 1

樣例輸出 #2

1

樣例 #3

樣例輸入 #3

6 7
1 4
3 5
2 3
3 3
3 6
2 1
5 1

樣例輸出 #3

5
6
7
7
10
11
8
首先可以先把分塊陣列逆序對的個數初始化為$0$ 每次交換a[l]與a[r],對序列的逆序對個數影響有多大?我們設[l+1,r-1]區間內比 a[l]小的元素有Sl個,比a[l]大的有Bl個,比a[r]小的元素有Sr個,比a[r]大的有Br個。 則逆序對的個數ans+=(Bl-Sl)+(Sr-Br)=(r-l-1-2*Sl)+(Sr-(r-l-1-Sr))=2*(Sr-Sl) 也就是說,每次給出詢問[l,r],我們需要求出[l+1,r-1]區間內的Sl和Sr 另外,還需要單獨判斷一下a[l]和a[r]的大小關係,若a[l]

相關文章