1. 程式人生 > 其它 >演算法學習寶典_3_字首和和差分

演算法學習寶典_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),如果nm的資料量稍微大一點就有可能超時,而我們如果使用字首和的方法來做的話就能夠將時間複雜度降到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
預訂記錄 110  10
預訂記錄 220  20
預訂記錄 325  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