第一講 字首和與差分
聽講人: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}\) 應該可以基於 \(S_{i-1,j}\) 或 \(S_{i,j-1}\) 計算,從而避免重複計算前面若干項的和。但是,如果直接將 \(S_{i-1,j}\) 和 \(S_{i,j-1}\) 相加,再加上 \(A_{i,j}\),會導致重複計算 \(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}\),所以應該有
同樣的道理,在已經預處理出二位字首和後,要查詢左上角為 \((i_1,j_1)\)、右下角為 \((i_2,j_2)\) 的子矩陣的和,可以計算
這可以在 \(O(1)\) 時間內完成。
在二維的情形,以上演算法的時間複雜度可以簡單認為是 \(O(mn)\),即與給定陣列的大小成線性關係。但是,當維度 \(k\) 增大時,由於容斥原理涉及的項數以指數級的速度增長,時間複雜度會成為 \(O(2^kN)\),這裡 \(k\) 是陣列維度,而 \(N\) 是給定陣列大小。因此,該演算法不再適用。
逐維字首和
對於一般的情形,給定 \(k\) 維陣列 \(A\),大小為 \(N\),同樣要求得其字首和 \(S\)。這裡,
從上式可以看出,\(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+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 天天愛跑步