一、差分介紹與代碼模板
如果我們需要維護的數據是“兩個相鄰數據之差”,這就是差分。令,則稱是的差分數組。也可以寫成。
差分的寫法與前綴和專欄的那篇介紹文章差不多,我們使用的數據數組和差分數組都是從1開始:
【差分模板】:
void getDiff(int a[], int n) { //a[0]=0, 使用[1,n]
for (int i = 1; i <= n; ++i)
d[i] = a[i] - a[i - 1]; //d[0]=0
}
二、從差分到原數組
其實從前綴和也可以很輕易的求出原數組,不過是求區間和的區間的左右兩端點相等罷了(即)。
同樣的思想,我們也可以拿着差分數組推出數組。而且。相當於是差分數組的前綴和 … 不說相當於,其實,數組就是數組的前綴和數組。
如果要求數組的區間和,怎麼做呢?(^_^)
所以,從我們約定的前綴和的寫法(方便遞推求前綴和數組)和求差分這兩種角度來看,就寫。
簡單的舉例:
a : 0 | 3 7 5 8 (a[0]默認爲0)
d : 0 | 3 4 -2 3
sum : 0 | 3 7 5 8
很容易想明白,現在我在0樓,上了3樓,又上了4層樓,再往下走了兩層樓,然後又上了三層樓,那麼我現在位於0+3+4-2+3=8
樓。
代碼如下:
void getA(int p[], int n) { //d[0]=0, 使用[1,n]
for (int i = 1; i <= n; ++i)
a[i] = a[i - 1] + d[i];
}
前綴和可以讓人以O(1)的時間求出一個區間的和。差分的用途也差不多,用來優化算法,甚至可以優化掉一個次方的複雜度。
下面具體介紹其應用。
三、差分的應用【區間加】
給定一個序列,其初值全爲0,有很多(m)次操作,允許離線,形如:,將中的每個值加上。爲方便,這裏的。最後輸出整個數組,複雜度要求。
舉例如下:
a : 0 | 0 0 0 0 0 0
A 1 2 3
-> a : 0 | 3 3 0 0 0 0
A 2 5 -2
-> a : 0 | 3 1 -2 -2 -2 0
A 5 6 3
-> a : 0 | 3 1 -2 -2 1 3
最簡單的想法,就是對原數組進行操作,每次對區間進行相應的操作,時間爲,m很大時甚至會達到平方的複雜度。下面的做法,可以真正做到。
不難發現,區間加k,實際上發生瞭如下的兩件事:
- 比前一個元素多了;
- 比前一個元素少了;(不用大了小了的說法,會引起誤解)
在差分數組的基礎上面,可以對應轉換爲兩個操作:
- ;
- ;
即,一次區間加,只修改這兩個元素,區間內部的數據之間的差值沒有變化,對應於差分數組相應區別不變。
最後利用上面的(求前綴和)方法,從數組求出數組,即爲答案。
例子:
a : 0 | 0 0 0 0 0 0
d : 0 | 0 0 0 0 0 0
A 1 2 3
-> a : 0 | 3 3 0 0 0 0
-> d : 0 | 3 0 -3 0 0 0
A 2 5 -2
-> a : 0 | 3 1 -2 -2 -2 0
-> d : 0 | 3 -2 -3 0 0 2
A 5 6 3
-> a : 0 | 3 1 -2 -2 1 3
-> d : 0 | 3 -2 -3 0 3 2 (r + 1 > n, 計不計算都行, p數組開大一些)
區間加:
void add(int l, int r, int k) {
d[l] += k;
d[r + 1] -= k;
}
void getA() {
for (int i = 1; i <= n; ++i) //預先指定a[0]=0
a[i] = a[i - 1] + d[i];
如果是單點加,用上面的add
完全沒問題:
本質和區間加一模一樣。
四、差分應用拓展【區間加等差+單點查詢】
給定一個序列,其初值全爲0,有很多(m)次操作,操作有兩種,強制在線,形如:
- 區間加等差數列:,將中的每個元素,加上首項爲、公差爲的等差數列對應的元素,即,,… 。
- 單點查詢: ,求出。
爲方便,這裏的。最後輸出整個數組。
目前,給加上首項爲、公差爲的等差數列,變爲。
原來的,修改後的數組:。顯然:區間加等差,相當於區間加常數、端點單點修改。
我們自然要把加上,加上;那麼右端點呢?要把剛纔的操作復原:。
因此,我們要解決的問題在於,維護數組,支持單點修改、區間加,可以使用線段樹加以解決。
五、總結
這裏介紹了簡單的差分思想,還有區間加二次函數,多次差分,樹上差分,有限微積分…