演算法學習寶典_3_字首和和差分
1、字首和
字首和是指某序列的前n項和,可以把它理解為數學上的數列的前n項和,而差分可以看成字首和的逆運算。合理的使用字首和與差分,可以將某些複雜的問題簡單化。
2、字首和演算法有什麼好處?
先來了解這樣一個問題:
輸入一個長度為n的整數序列。接下來再輸入m個詢問,每個詢問輸入一對l, r。對於每個詢問,輸出原序列中從第l個數到第r個數的和。
我們很容易想出暴力解法,遍歷區間求和。
程式碼如下:
int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); while(m--) {int l,r; int sum=0; scanf("%d%d",&l,&r); for(int i=l;i<=r;i++) { sum+=a[i]; } printf("%d\n",sum); }
這樣的時間複雜度為O(n*m)
,如果n
和m
的資料量稍微大一點就有可能超時,而我們如果使用字首和的方法來做的話就能夠將時間複雜度降到O(n+m)
,大大提高了運算效率。
具體做法:
首先做一個預處理,定義一個sum[]
陣列,sum[i]
代表a
陣列中前i
個數的和。
求字首和運算:
const int N=1e5+10; int sum[N],a[N]; //sum[i]=a[1]+a[2]+a[3].....a[i]; for(int i=1;i<=n;i++) { sum[i]=sum[i-1]+a[i]; }
然後查詢操作:
scanf("%d%d",&l,&r); printf("%d\n", sum[r]-sum[l-1]);
對於每次查詢,只需執行sum[r]-sum[l-1]
,時間複雜度為O(1)
原理
sum[r] =a[1]+a[2]+a[3]+a[l-1]+a[l]+a[l+1]......a[r]
;sum[l-1]=a[1]+a[2]+a[3]+a[l-1]
sum[r]-sum[l-1]=a[l]+a[l+1]+......+a[r]
;
圖解
這樣,對於每個詢問,只需要執行 sum[r]-sum[l-1]
。輸出原序列中從第l
個數到第r個數的和的時間複雜度變成了O(1)
。
我們把它叫做一維字首和。
總結:
練習題目:
560. 和為 K 的子陣列 ---》 字首和和UThash 配合
974. 和可被 K 整除的子陣列 ---》 字首和 和 UTahsh + 同餘定理
523. 連續的子陣列和 ---》 字首和 和 UTahsh + 同餘定理 + prefix
/// 此種解決方法會超出時間限制 int subarraySum1(int* nums, int numsSize, int k){ int *presum = (int *)malloc(sizeof(int) * (numsSize + 1)); memset(presum, 0, sizeof(int) * (numsSize + 1)); presum[0] = 0; int res = 0; for(int i = 1; i <= numsSize; i++) { presum[i] = nums[i - 1] + presum[i - 1]; } for(int i = 0; i < numsSize; i++) { for(int j = i + 1; j < numsSize + 1; j++) { if(presum[j] - presum[i] == k) { res++; } } } return res; } typedef struct{ int key; // key 為字首和 int cnt; // 記錄 key 有多少個 UT_hash_handle hh; }HashTable; //HashTable *users = NULL; 錯誤用法 HashTable *users; int subarraySum(int* nums, int numsSize, int k){ users = NULL; int res = 0; //字首和 int sum = 0; HashTable *tmp = (HashTable *)malloc(sizeof(HashTable)); // 加 字首和的第一個節點 tmp->key = sum; tmp->cnt = 1; HASH_ADD_INT(users, key, tmp); for(int i = 0; i < numsSize; i++){ sum+=nums[i]; HashTable *tmp = NULL; int value = sum - k; HASH_FIND_INT(users, &value, tmp); if(tmp != NULL) { res += tmp->cnt; } //增加新的節點 HASH_FIND_INT(users, &sum, tmp); if(tmp != NULL){ tmp->cnt++; } else { HashTable *tmp = (struct HashTable *)malloc(sizeof(HashTable)); tmp->key = sum; tmp->cnt = 1; HASH_ADD_INT(users, key, tmp); } } return res; }View Code
//974 typedef struct{ int key; int cnt; UT_hash_handle hh; }HashTable; // 同餘定理 //若兩個數a,b除以同一個數m得到的餘數相同,則a,b的差一定能被m整除 HashTable *users; int subarraysDivByK(int* nums, int numsSize, int k){ users = NULL; HashTable *tmp = (HashTable *)malloc(sizeof(HashTable)); tmp->key = 0; tmp->cnt = 1; HASH_ADD_INT(users, key, tmp); int value = 0; int presum = 0; int res = 0; for(int i = 0; i < numsSize; i++) { presum+=nums[i]; value = (presum % k + k) % k ; // 處理負數 ,同餘定理 HASH_FIND_INT(users, &value, tmp); if(tmp != NULL) { res += tmp->cnt; tmp->cnt++; } else { tmp = (HashTable *)malloc(sizeof(HashTable)); tmp->key = value; tmp->cnt = 1; HASH_ADD_INT(users, key, tmp); } } return res; }View Code
typedef struct{ long long key; long long preindex; UT_hash_handle hh; }HashTable; HashTable *g_users; bool checkSubarraySum(int* nums, int numsSize, int k){ g_users = NULL; HashTable *tmp = NULL; tmp = (HashTable *)malloc(sizeof(HashTable)); tmp->key = 0; tmp->preindex = -1; HASH_ADD_INT(g_users, key, tmp); long long presum = 0; long long remainder = 0; for(int i = 0; i < numsSize; i++) { presum += nums[i]; remainder = (presum %k + k)%k; HASH_FIND_INT(g_users, &remainder, tmp); if(tmp != NULL) { /// 不更新只判斷。。。。 if((i - (tmp->preindex)) >= 2) { return true; } } else { tmp = (HashTable *)malloc(sizeof(HashTable)); tmp->preindex = i; tmp->key = remainder; HASH_ADD_INT(g_users, key, tmp); } } return false; }View Code
3、二維字首和
如果陣列變成了二維陣列怎麼辦呢?
先給出問題:
輸入一個n行m列的整數矩陣,再輸入q個詢問,每個詢問包含四個整數x1, y1, x2, y2,表示一個子矩陣的左上角座標和右下角座標。對於每個詢問輸出子矩陣中所有數的和。
同一維字首和一樣,我們先來定義一個二維陣列s[][], s[i][j]表示二維陣列中,左上角(1,1)到右下角( i,j )所包圍的矩陣元素的和。接下來推導二維字首和的公式。
先看一張圖:
紫色面積是指(1,1)
左上角到(i,j-1)
右下角的矩形面積, 綠色面積是指(1,1)
左上角到(i-1, j )
右下角的矩形面積。每一個顏色的矩形面積都代表了它所包圍元素的和。
從圖中我們很容易看出,整個外圍藍色矩形面積s[i][j]
= 綠色面積s[i-1][j]
+ 紫色面積s[i][j-1]
- 重複加的紅色的面積s[i-1][j-1]
+小方塊的面積a[i][j]
;
因此得出二維字首和預處理公式
s[i] [j] = s[i-1][j] + s[i][j-1 ] + a[i] [j] - s[i-1][ j-1]
接下來回歸問題去求以(x1,y1)
為左上角和以(x2,y2)
為右下角的矩陣的元素的和。
如圖:
紫色面積是指 ( 1,1 )
左上角到(x1-1,y2)
右下角的矩形面積 ,黃色面積是指(1,1)
左上角到(x2,y1-1)
右下角的矩形面積;
不難推出:
綠色矩形的面積 = 整個外圍面積s[x2, y2]
- 黃色面積s[x2, y1 - 1]
- 紫色面積s[x1 - 1, y2]
+ 重複減去的紅色面積 s[x1 - 1, y1 - 1]
因此二維字首和的結論為:
以(x1, y1)
為左上角,(x2, y2)
為右下角的子矩陣的和為:s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]
總結:
練習一道完整題目: 輸入一個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> #include<cstdio> using namespace std; const int N=1010; int a[N][N],s[N][N]; int main() { int n,m,q; scanf("%d%d%d",&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++) s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1]; while(q--) { int x1,y1,x2,y2; scanf("%d%d%d%d",&x1,&y1,&x2,&y2); printf("%d\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]); } return 0; }View Code
4、差分
5、一維差分
類似於數學中的求導和積分,差分可以看成字首和的逆運算。
差分陣列:
首先給定一個原陣列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陣列中從頭開始的一段區間和。
考慮如何構造差分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陣列,通過字首和運算,就可以在O(n) 的時間內得到a陣列 。
知道了差分陣列有什麼用呢? 彆著急,慢慢往下看。
話說有這麼一個問題:
給定區間[l ,r ],讓我們把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陣列中的 b[l] + c ,通過字首和運算,a陣列變成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;
然後我們打個補丁,b[r+1] - c, 通過字首和運算,a陣列變成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;
為啥還要打個補丁?
我們畫個圖理解一下這個公式的由來:
b[l] + c,效果使得a陣列中 a[l]及以後的數都加上了c(紅色部分),但我們只要求l到r區間加上c, 因此還需要執行 b[r+1] - c,讓a陣列中a[r+1]及往後的區間再減去c(綠色部分),這樣對於a[r] 以後區間的數相當於沒有發生改變。
因此我們得出一維差分結論:給a陣列中的[ l, r]區間中的每一個數都加上c,只需對差分陣列b做 b[l] + = c, b[r+1] - = c。時間複雜度為O(1), 大大提高了效率。
總結:
題目一:
輸入一個長度為n的整數序列。 接下來輸入m個操作,每個操作包含三個整數l, r, c,表示將序列中[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 //差分 時間複雜度 o(m) #include<iostream> using namespace std; const int N=1e5+10; int a[N],b[N]; int main() { int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); b[i]=a[i]-a[i-1]; //構建差分陣列 } int l,r,c; while(m--) { scanf("%d%d%d",&l,&r,&c); b[l]+=c; //表示將序列中[l, r]之間的每個數加上c b[r+1]-=c; } for(int i=1;i<=n;i++) { b[i]+=b[i-1]; //求字首和運算 printf("%d ",b[i]); } return 0; }View Code
2.差分代表題目
1094. 拼車
1109. 航班預訂統計
1094
車上最初有 capacity 個空座位。車 只能 向一個方向行駛(也就是說,不允許掉頭或改變方向) 給定整數 capacity 和一個數組 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他們和放他們的位置分別是 fromi 和 toi 。這些位置是從汽車的初始位置向東的公里數。 當且僅當你可以在所有給定的行程中接送所有乘客時,返回 true,否則請返回 false。 示例 1: 輸入:trips = [[2,1,5],[3,3,7]], capacity = 4 輸出:false bool carPooling(int** trips, int tripsSize, int* tripsColSize, int capacity){ int people[MAX] = { 0 }; for(int i = 0; i < tripsSize; i++) { people[trips[i][1]] += trips[i][0]; people[trips[i][2]] -= trips[i][0]; } int num = 0; for(int i = 0; i < MAX; i++) { num+=people[i]; if(num > capacity) { return false; } } return true; }View Code
1109
這裡有 n 個航班,它們分別從 1 到 n 進行編號。 有一份航班預訂表 bookings ,表中第 i 條預訂記錄 bookings[i] = [firsti, lasti, seatsi] 意味著在從 firsti 到 lasti (包含 firsti 和 lasti )的 每個航班 上預訂了 seatsi 個座位。 請你返回一個長度為 n 的陣列 answer,裡面的元素是每個航班預定的座位總數。 示例 1: 輸入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 輸出:[10,55,45,25,25] 解釋: 航班編號 1 2 3 4 5 預訂記錄 1 : 10 10 預訂記錄 2 : 20 20 預訂記錄 3 : 25 25 25 25 總座位數: 10 55 45 25 25 因此,answer = [10,55,45,25,25] int* corpFlightBookings(int** bookings, int bookingsSize, int* bookingsColSize, int n, int* returnSize){ int *diff = malloc(sizeof(int) * n); memset(diff, 0, sizeof(int) * n); *returnSize = n; // 差分 for(int i = 0;i < bookingsSize; i++) { // 原始陣列[start ... end]全部增加add ==> 差分陣列diff[start]增加add,diff[end+1]減少add diff[bookings[i][0] - 1] += bookings[i][2]; if(bookings[i][1] < n) { diff[bookings[i][1]] -= bookings[i][2]; } } // 差分陣列的字首和陣列 == 原始陣列更新後的陣列 for(int i = 1; i < n ;i++) { diff[i] += diff[i - 1]; } return diff; }View Code
參考: 網站:https://blog.csdn.net/weixin_45629285/article/details/111146240