字首和與差分

Violet_fan發表於2024-11-17

第一講 字首和與差分

聽講人:24級演算法組小萌新

卑微講題人:22049212-梁軍斌(本人正在瘋狂整理入黨材料中)

我認為,每個人最重要的不是過去,而是現在;雖然揹負著昨日的重擔,但也有自己想要追尋的東西。
希望你從今以後無論去往何處都不要忘記,世界上有那麼多愛你的人。
你道你機關算盡,可你真算到了自己也要下場當棋子嗎?
坐看日月行,細數千帆過。
年年今日,燈明如晝。原火不滅,願人依舊。

先來看一道例題

字首和

題目描述

給定一個長度為 \(n\) 的整數序列。接下來輸入 \(m\) 個詢問,每個詢問由一對整數 \(l, r\) 組成。

對於每個詢問,需要計算並輸出原序列中從第 \(l\) 個數到第 \(r\) 個數的和。

輸入格式

  • 第一行包含兩個整數 \(n\)\(m\)
  • 第二行包含 \(n\) 個整數,表示整數數列。
  • 接下來 \(m\) 行,每行包含兩個整數 \(l\)\(r\),表示一個詢問的區間範圍。

輸出格式

  • \(m\) 行,每行輸出一個詢問的結果。

資料範圍

  • \(1 \leq l \leq r \leq n\)
  • \(1 \leq n, m \leq 100000\)
  • \(-1000 \leq\) 數列中元素的值 \(\leq 1000\)

示例

輸入樣例

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

輸出樣例

3
6
10

參考程式碼:

#include<bits/stdc++.h>
#define int long long
using namespace std;
using i64 = long long;
const int N = 3e5+10;
void solve()
{
    int n,m;
    cin>>n>>m;
    vector<int> a(n+1,0);
    for(int i=1;i<=n;i++) cin>>a[i];
    vector<int> s(n+1,0);
    for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<s[r]-s[l-1]<<endl;
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _=1;
    //cin>>_;
    while(_--)
    {
        solve();
    }
    
    
}

字首和

定義

字首和可以簡單理解為「數列的前 \(n\) 項的和」,是一種重要的預處理方式,能大大降低查詢的時間複雜度。

\(C++\) 標準庫中實現了字首和函式 std::partial_sum,定義於標頭檔案 <numeric> 中。

設數列的前\(x\)項的和為\(S_x\)

  • 已知一個長度為\(n\)的陣列\(a\):

  • \(S_1\) = \(a_1\)

  • \(S_2\) = \(a_1\) + \(a_2\)

  • \(S_3\) = \(a_1\) + \(a_2\) + \(a_3\)

  • \(...\)

  • \(S_{n-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\)

  • \(S_{n}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\) + \(a_{n}\)

知道字首和,便可以知道任意區間內所有數之和,即\(Sum_{l,r}\) = \(S_r\) - \(S_{l-1}\):

  • \(S_{l-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{l-1}\)
  • \(S_{r}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{r-1}\) + \(a_{r}\)
  • \(Sum_{l,r}\) = \(a_l\) + \(a_{l+1}\) + \(a_{l+2}\) + \(...\) + \(a_{r}\) = \(S_r\) - \(S_{l-1}\);

例:

  • \(Sum_{3,9}\) = \(S_9\) - \(S_2\)
  • \(Sum_{3,7}\) = \(S_7\) - \(S_2\)
  • \(Sum_{3,3}\) = \(S_3\) - \(S_2\)

如何求字首和?

首先初始化\(a[0]=0\),隨後遍歷一遍陣列\(a\),得到字首和陣列

a[0]=0;
for(int i = 1;i <= n;i ++)
{
    a[i]+=a[i-1];
    // s[i]=s[i-1]+a[i];
}

二維字首和

基於容斥原理

這種方法多用於二維字首和的情形。給定大小為 \(m\times n\) 的二維陣列 \(A\),要求出其字首和 \(S\)。那麼,\(S\) 同樣是大小為 \(m\times n\) 的二維陣列,且

\[S_{i,j} = \sum_{i'\le i}\sum_{j'\le j}A_{i',j'}. \]

類比一維的情形,\(S_{i,j}\) 應該可以基於 \(S_{i-1,j}\)\(S_{i,j-1}\) 計算,從而避免重複計算前面若干項的和。但是,如果直接將 \(S_{i-1,j}\)\(S_{i,j-1}\) 相加,再加上 \(A_{i,j}\),會導致重複計算 \(S_{i-1,j-1}\) 這一重疊部分的字首和,所以還需要再將這部分減掉。這就是 容斥原理。由此得到如下遞推關係:

\[S_{i,j} = A_{i,j} + S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1}. \]

實現時,直接遍歷 \((i,j)\) 求和即可。

考慮一個具體的例子。

二位字首和示例

這裡,\(S\) 是給定矩陣 \(A\) 的字首和。根據定義,\(S_{3,3}\) 是左圖中虛線方框中的子矩陣的和。這裡,\(S_{3,2}\) 是藍色子矩陣的和,\(S_{2,3}\) 是紅色子矩陣的和,它們重疊部分的和是 \(S_{2,2}\)。由此可見,如果直接相加 \(S_{3,2}\)\(S_{2,3}\),會重複計算 \(S_{2,2}\),所以應該有

\[S_{3,3} = A_{3,3} + S_{2,3} + S_{3,2} - S_{2,2} = 5 + 18 + 15 - 9 = 29. \]

同樣的道理,在已經預處理出二位字首和後,要查詢左上角為 \((i_1,j_1)\)、右下角為 \((i_2,j_2)\) 的子矩陣的和,可以計算

\[S_{i_2,j_2} - S_{i_1,j_2} - S_{i_2,j_1} + S_{i_1,j_1}. \]

這可以在 \(O(1)\) 時間內完成。

在二維的情形,以上演算法的時間複雜度可以簡單認為是 \(O(mn)\),即與給定陣列的大小成線性關係。但是,當維度 \(k\) 增大時,由於容斥原理涉及的項數以指數級的速度增長,時間複雜度會成為 \(O(2^kN)\),這裡 \(k\) 是陣列維度,而 \(N\) 是給定陣列大小。因此,該演算法不再適用。

逐維字首和

對於一般的情形,給定 \(k\) 維陣列 \(A\),大小為 \(N\),同樣要求得其字首和 \(S\)。這裡,

\[S_{i_1,\cdots,i_k} = \sum_{i'_1\le i_1}\cdots\sum_{i'_k\le i_k} A_{i'_1,\cdots,i'_k}. \]

從上式可以看出,\(k\) 維字首和就等於 \(k\) 次求和。所以,一個顯然的演算法是,每次只考慮一個維度,固定所有其它維度,然後求若干個一維字首和,這樣對所有 \(k\) 個維度分別求和之後,得到的就是 \(k\) 維字首和。

樹上字首和

\(\textit{sum}_i\) 表示結點 \(i\) 到根節點的權值總和。
然後:

  • 若是點權,\(x,y\) 路徑上的和為 \(\textit{sum}_x + \textit{sum}_y - \textit{sum}_\textit{lca} - \textit{sum}_{\textit{fa}_\textit{lca}}\)

  • 若是邊權,\(x,y\) 路徑上的和為 \(\textit{sum}_x + \textit{sum}_y - 2\cdot\textit{sum}_{lca}\)

    LCA 的求法參見 最近公共祖先

題目描述

子矩陣的和

給定一個 \(n\)\(m\) 列的整數矩陣,以及 \(q\) 個詢問。每個詢問包含四個整數 \(x_1, y_1, x_2, y_2\),表示一個子矩陣的左上角座標和右下角座標。

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

輸入格式

  • 第一行包含三個整數 \(n\), \(m\), \(q\)
  • 接下來的 \(n\) 行,每行包含 \(m\) 個整數,表示整數矩陣。
  • 接下來 \(q\) 行,每行包含四個整數 \(x_1, y_1, x_2, y_2\),表示一組詢問。

輸出格式

\(q\) 行,每行輸出一個詢問的結果。

資料範圍

  • \(1 \leq n, m \leq 1000\)
  • \(1 \leq q \leq 200000\)
  • \(1 \leq x_1 \leq x_2 \leq n\)
  • \(1 \leq y_1 \leq y_2 \leq m\)
  • \(-1000 \leq\) 矩陣內元素的值 \(\leq 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<bits/stdc++.h>
using namespace std;
const int N=1010;
typedef long long ll;
int a[N][N];
int s[N][N];
int n,m,q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
         cin>>a[i][j],s[i][j]=0;
    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;
        cin>>x1>>y1>>x2>>y2;
        cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;
    }
}

差分

解釋

差分是一種和字首和相對的策略,可以當做是求和的逆運算。

這種策略的定義是令 \(b_i=\begin{cases}a_i-a_{i-1}\,&i \in[2,n] \\ a_1\,&i=1\end{cases}\)

性質

  • \(a_i\) 的值是 \(b_i\) 的字首和,即 \(a_n=\sum\limits_{i=1}^nb_i\)
  • 計算 \(a_i\) 的字首和 \(sum=\sum\limits_{i=1}^na_i=\sum\limits_{i=1}^n\sum\limits_{j=1}^{i}b_j=\sum\limits_{i=1}^n(n-i+1)b_i\)

它可以維護多次對序列的一個區間加上一個數,並在最後詢問某一位的數或是多次詢問某一位的數。注意修改操作一定要在查詢操作之前。

譬如使 \([l,r]\) 中的每個數加上一個 \(k\),即

\[b_l \leftarrow b_l + k,b_{r + 1} \leftarrow b_{r + 1} - k \]

其中 \(b_l+k=a_l+k-a_{l-1}\)\(b_{r+1}-k=a_{r+1}-(a_r+k)\)

最後做一遍字首和就好了。

C++ 標準庫中實現了差分函式 std::adjacent_difference,定義於標頭檔案 <numeric> 中。

題目描述

差分

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

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

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

輸入格式

  • 第一行包含兩個整數 \(n\)\(m\)
  • 第二行包含 \(n\) 個整數,表示整數序列。
  • 接下來 \(m\) 行,每行包含三個整數 \(l, r, c\),表示一個操作。

輸出格式

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

資料範圍

  • \(1 \leq n, m \leq 100000\)
  • \(1 \leq l \leq r \leq n\)
  • \(-1000 \leq c \leq 1000\)
  • \(-1000 \leq\) 整數序列中元素的值 \(\leq 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<bits/stdc++.h>
using namespace std;
int main()
{
    int n,m;
    cin>>n>>m;
    vector<int> a(n+1,0);
    vector<int> b(n+1,0);
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];
    while(m--)
    {
        int l,r,c;
        cin>>l>>r>>c;
        b[r+1]-=c;
        b[l]+=c;
    }
    for(int i=1;i<=n;i++) b[i]+=b[i-1];
    for(int i=1;i<=n;i++) cout<<b[i]<<" \n"[i==n];
}

二維差分

差分矩陣

題目描述

輸入一個 \(n\)\(m\) 列的整數矩陣,再輸入 \(q\) 個操作,每個操作包含五個整數 \(x_1, y_1, x_2, y_2, c\),其中 \((x_1, y_1)\)\((x_2, y_2)\) 表示一個子矩陣的左上角座標和右下角座標。

每個操作都要將選中的子矩陣中的每個元素的值加上 \(c\)

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

輸入格式

  • 第一行包含整數 \(n, m, q\)
  • 接下來 \(n\) 行,每行包含 \(m\) 個整數,表示整數矩陣。
  • 接下來 \(q\) 行,每行包含 5 個整數 \(x_1, y_1, x_2, y_2, c\),表示一個操作。

輸出格式

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

資料範圍

  • \(1 \leq n, m \leq 1000\)
  • \(1 \leq q \leq 100000\)
  • \(1 \leq x_1 \leq x_2 \leq n\)
  • \(1 \leq y_1 \leq y_2 \leq m\)
  • \(-1000 \leq c \leq 1000\)
  • \(-1000 \leq\) 矩陣內元素的值 \(\leq 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<bits/stdc++.h>
using namespace std;
int a[1010][1010];
int b[1010][1010];
int n,m,q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++)
        cin>>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;
        cin>>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;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j];
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
          cout<<a[i][j]<<" \n"[j==m];
    }
    
}

習題

字首和:

  • 洛谷 B3612【深進 1. 例 1】求區間和
  • 洛谷 U69096 字首和的逆
  • AtCoder joi2007ho_a 最大の和
  • 「USACO16JAN」子共七 Subsequences Summing to Sevens
  • 「USACO05JAN」Moo Volume S

二維/多維字首和:

  • HDU 6514 Monitor
  • 洛谷 P1387 最大正方形
  • 「HNOI2003」鐳射炸彈
  • CF 165E Compatible Numbers
  • CF 383E Vowels
  • ARC 100C Or Plus Max

樹上字首和:

  • LOJ 10134.Dis
  • LOJ 2491. 求和

差分:

  • 樹狀陣列 3:區間修改,區間查詢
  • P3397 地毯
  • 「Poetize6」IncDec Sequence

樹上差分:

  • 洛谷 3128 最大流
  • JLOI2014 松鼠的新家
  • NOIP2015 運輸計劃
  • NOIP2016 天天愛跑步

相關文章