1. 程式人生 > 其它 >徹底弄清楚字首和與差分!

徹底弄清楚字首和與差分!

字首和

一維字首和

普通求和

通常我們對一維陣列求和採用的是從頭到尾遍歷的方式,時間複雜度是O(n),但當計算很龐大的資料量時就很可能會超時!

int sum = 0;
for(int i = 0; i < nums.size(); i++)
	sum += nums[i]

一維字首求和

  1. 初始化字首和陣列(定義一個s[i]陣列,用來記錄(代表)前i項資料的和):s[i] = s[i - 1] + a[i]

    注:i是從1開始的,這樣就不用考慮邊界問題了。如:s[1] = s[0] + a[1],s[0] = 0

  2. 查詢操作:計算[l ~ r]的和:s[r] - s[l - 1]。時間複雜度是O(1)

【acwing.795字首和】

輸入一個長度為 n 的整數序列。

接下來再輸入 m 個詢問,每個詢問輸入一對 l,r。

對於每個詢問,輸出原序列中從第 ll 個數到第 rr 個數的和。

輸入格式

第一行包含兩個整數 n 和 m。

第二行包含 n 個整數,表示整數數列。

接下來 m 行,每行包含兩個整數 ll 和 rr,表示一個詢問的區間範圍。

輸出格式

共 m 行,每行輸出一個詢問的結果。

資料範圍

1≤l≤r≤n,
1≤n,m≤100000,
−1000≤數列中元素的值≤1000

輸入樣例:

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

輸出樣例:

3
6
10

【參考程式碼】

#include<iostream>

using namespace std;

int n,m;
const int N = 100000+10;
int a[N],s[N]; // 初始化陣列了 a[0]、s[0]=0

int main()
{
    
    cin>>n>>m;
    for(int i = 1; i <= n; i++) cin>>a[i];
    
    //初始化字首和陣列s[i]
    for(int i = 1; i <= n; i++) s[i] = s[i -1] + a[i];
    
    int l, r;
    while(m--)
    {
        cin>>l>>r;
        //求[l~r]的和
        cout<<s[r] - s[l - 1]<<endl;
    }
    return 0;
}

二維字首和(子矩陣的和)

  1. 初始化字首和陣列(定義一個二維s[i][j]陣列,用來記錄(代表)前(i,j)到原點所有數的和項資料的和

    (1)S[i,j]是什麼:S[i,j]即為圖中綠色部分所有數的的和為:

    (2)S[i,j]怎麼計算:S[i,j]=S[i,j−1]+S[i−1,j]−S[i−1,j−1]+a[i,j]

  1. (x1,y1),(x2,y2)這一子矩陣中所有數的和怎麼計算?res = S[x2,y2]−S[x1−1,y2]−S[x2,y1−1]+S[x1−1,y1−1]

【acwing 796. 子矩陣的和】

輸入一個 n 行 m 列的整數矩陣,再輸入 q 個詢問,每個詢問包含四個整數 x1,y1,x2,y2表示一個子矩陣的左上角座標和右下角座標。

對於每個詢問輸出子矩陣中所有數的和。

輸入格式

第一行包含三個整數n,m,q。

接下來 n 行,每行包含 m 個整數,表示整數矩陣。

接下來 q 行,每行包含四個整數 x1,y1,x2,y2,表示一組詢問。

輸出格式

共 q 行,每行輸出一個詢問的結果。

資料範圍

1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩陣內元素的值≤1000

輸入樣例:

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4

輸出樣例:

17
27
21

【參考程式碼】

#include<iostream>

using namespace std;
const int N = 1000+10;
int a[N][N],s[N][N];

int main()
{
    int n,m,q;
    cin>>n>>m>>q;
    
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            scanf("%d",&a[i][j]);
            //1.初始化字首和陣列
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]; 
        }
    }
    
    while(q--)
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1 , &y1, &x2, &y2);
        //2.計算(x1,y1)(x2,y2)子矩陣的和
        int res = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
        printf("%d\n",res);
    }
    
    return 0;
}

注:

同一維字首和為了不額外考慮邊界問題我們從1開始對字首和陣列進行初始化。

當是大量資料的輸入輸出時scanf和printf執行的是比cin和cout要快的!(所用時間更少)

差分

類似於數學中的求導和積分,差分可以看成字首和的逆運算。

一維差分

差分陣列:

首先給定一個原陣列a:a[1], a[2], a[3],,,,a[n];

然後我們要構造一個數組b : b[1] ,b[2] , b[3],,,, b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]

a陣列b陣列字首和陣列,反過來我們把b陣列叫做a陣列的差分陣列。即,每一個a[i]都是b陣列中從頭開始到i的的一段區間和。

(1)如何構造差分b陣列?

最為直接的方法

如下:

a[0 ]= 0;

b[1] = a[1] - a[0];

b[2] = a[2] - a[1];

b[3] =a [3] - a[2];

........

b[n] = a[n] - a[n-1];

兩邊各自相加得:b[1] + b[2] + ,,, + b[n] = a[n]

我們只要有b陣列,最後通過字首和運算,就可以在O(n) 的時間內得到a陣列

那我們構造出來的差分陣列b到底有什麼用?,看下面的解釋你就瞭解啦!

(2)現在我們要給a陣列在給定區間[l,r]上的每一個數加上c,即a[l] + c , a[l+1] + c , a[l+2] + c ,,,, a[r] + c。暴力的做法是for列舉l~r,時間複雜度是O(n),如果我們需要對原陣列執行m次這樣的操作,時間複雜度就會變成O(n*m)。當資料量很龐大時很可能就會超時,那有沒有更高效的方法呢?可以考慮一下差分操作!

始終要記得,a陣列是b陣列的字首和陣列對b陣列的b[i]的修改,就一定會影響到a陣列中從a[i]及往後的每一個數(圖示理解這句話!)

通過上面的解釋,那我們如何如和用差分陣列b實現給a陣列在[l,r]上+c呢?

  • 首先讓差分b陣列中的 b[l] + c ,a陣列變成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;(通過上面的例子我們知道對b[l]進行了修改(+c),那麼從l及後面所有的數都受到了影響(+c)),但我們想要的是給a陣列在定區間[l,r]上+c,即多了從r+1開始到後面所有數,這一段資料)。

  • 為此,我們打個補丁,b[r+1] - c, a陣列變成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;(即b[r+1] - c完成了減去上述從r+1開始都後面多出來的資料)

(3)如何求經過操作後的原陣列a呢?

我們只要有了b陣列,並對它進行b[l] += c,b[r + 1] -= c操作(使得a陣列在[l,r]區間上每一個數+C),最後通過對b陣列進行字首和運算,就可以在O(n) 的時間內得到a陣列

【acwing 797 差分】

輸入一個長度為 n 的整數序列。

接下來輸入 m個操作,每個操作包含三個整數l,r,c,表示將序列中 [l,r][l,r] 之間的每個數加上 c。

請你輸出進行完所有操作後的序列。

輸入格式

第一行包含兩個整數 n 和 m。

第二行包含 n 個整數,表示整數序列。

接下來 m 行,每行包含三個整數l,r,c,表示一個操作。

輸出格式

共一行,包含 n 個整數,表示最終序列。

資料範圍

1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整數序列中元素的值≤1000

輸入樣例:

6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

輸出樣例:

3 4 5 3 4 2

【參考程式碼】

#include<iostream>

using namespace std;
const int N = 100000+10;
int a[N],b[N];
int main()
{
	int n,m;
	cin>>n>>m;
	
	for(int i = 1; i <= n; i++)
	{
		scanf("%d",&a[i]);
		//構造差分陣列
		b[i] = a[i] - a[i - 1]; 
	}
	
	//給a陣列[l,r]上每一個數加上c  ===> b[l] += c, b[r+1] -= c
	while(m--)
	{
		int l,r,c;
		cin>>l>>r>>c;
		b[l] +=c;
		b[r + 1] -= c;	
	} 
	
	//求操作後的陣列a———> 對b陣列進行字首和操作即可
	for(int i = 1; i <= n; i++)
	{
		 b[i] += b[i - 1];
		 printf("%d ",b[i]); 
	}
	
	return 0;
}

【一維差分總結】

給區間[l,r]上的每一個數加上c:b[l] += c,b[r + 1] -= c

二維差分

如果擴充套件到二維,我們需要讓二維陣列被選中的子矩陣中的每個元素的值加上c,是否也可以達到O(1)的時間複雜度。答案是可以的,考慮二維差分。

a[][]陣列是b[][]陣列的字首和陣列,那麼b[][]是a[][]的差分陣列

原陣列: a[i][j]

我們去構造差分陣列: b[i][j]

使得a陣列中a[i][j]b陣列左上角(1,1)右下角(i,j)所包圍矩形元素的和。

(1)如何構造b陣列呢?

我們去逆向思考。

同一維差分,我們構造二維差分陣列目的是為了 讓原二維陣列a中所選中子矩陣中的每一個元素加上c的操作,可以由O(n*n)的時間複雜度優化成O(1)

在對子矩陣每一個數 + C操作之前,我們先要構造好差分陣列b[i][j]

程式碼如下:

// 構造差分陣列
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
            

(2)那我們如何給子矩陣(x1,y1),(x2,y2)中的每一個數都加上c呢?

已知原陣列a中被選中的子矩陣為 以(x1,y1)為左上角,以(x2,y2)為右上角所圍成的矩形區域;

始終要記得,a陣列是b陣列的字首和陣列,比如對b陣列的b[i][j]的修改,會影響到a陣列中從a[i][j]及往後的每一個數。

假定我們已經構造好了b陣列,類比一維差分,我們執行以下操作
來使被選中的子矩陣中的每個元素的值加上c

b[x1][y1] + = c;

b[x1,][y2+1] - = c;

b[x2+1][y1] - = c;

b[x2+1][y2+1] + = c;

每次對b陣列執行以上操作,等價於:

for(int i=x1;i<=x2;i++)
for(int j=y1;j<=y2;j++)
a[i][j]+=c;

我們畫個圖去理解一下這個過程:

b[x1][ y1 ] +=c ; 對應圖1 ,讓整個a陣列中藍色矩形面積的元素都加上了c
b[x1,][y2+1]-=c ; 對應圖2 ,讓整個a陣列中綠色矩形面積的元素再減去c,使其內元素不發生改變。
b[x2+1][y1]- =c ; 對應圖3 ,讓整個a陣列中紫色矩形面積的元素再減去c,使其內元素不發生改變。
b[x2+1][y2+1]+=c; 對應圖4,,讓整個a陣列中紅色矩形面積的元素再加上c,紅色內的相當於被減了兩次,再加上一次c,才能使其恢復。

我們將上述操作(子矩陣每一個數 + C)程式碼如下。時間複雜度O(1):

{   
	//對a陣列中的(x1,y1)到(x2,y2)之間的元素都加上了c
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

(3)如何求經過操作後的原陣列a呢?

當我們構造了二維差分陣列b[i][j],並對b[i][j]進行了 b[x1][y1]+=c; b[x2+1][y1]-=c; b[x1][y2+1]-=c; b[x2+1][y2+1]+=c;4個操作(使得a陣列的子矩陣每一個數都加上了C),最後通過對b陣列進行字首和運算(二維字首和運算),就可以在短時間時間內得到a陣列。

【acwing 798. 差分矩陣】

輸入一個 n 行 m 列的整數矩陣,再輸入 q 個操作,每個操作包含五個整數 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一個子矩陣的左上角座標和右下角座標。

每個操作都要將選中的子矩陣中的每個元素的值加上 cc。

請你將進行完所有操作後的矩陣輸出。

輸入格式

第一行包含整數 n,m,q。

接下來 n 行,每行包含 m 個整數,表示整數矩陣。

接下來 q 行,每行包含 5 個整數 x1,y1,x2,y2,c,表示一個操作。

** 輸出格式**

共 n 行,每行 m 個整數,表示所有操作進行完畢後的最終矩陣。

資料範圍

1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩陣內元素的值≤1000

輸入樣例:

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

輸出樣例:

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

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

int main()
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &a[i][j]);
// 構造差分陣列
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];

    while (q -- )
    {
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        b[x1][y1] += c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y1] -= c;
        b[x2 + 1][y2 + 1] += c;
    }
//  通過求差分陣列b的字首和來得到原陣列a
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] = b[i][j] + b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]; // 字首和公式

    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
        cout << endl;
    }

    return 0;
}

總結:

1.先構造好一維或者二維差分陣列

2.差分的作用:是在O(1)時間內給某一段區間[l,r]或者子矩陣(x1,y1),(x2,y2)中的每一個數都加上C

注:如果文章有任何錯誤或不足,請各位大佬盡情指出,評論留言留下您寶貴的建議!如果這篇文章對你有些許幫助,希望可愛親切的您點個贊推薦一手,非常感謝啦