下面我們介紹一種很酷的操作,叫做 lowbit
,它可以高效地計算 ,即我們要證明:
其中 是將 表示成二進制以後,從右向左數,遇到 則停止時,數出的 的個數。
通過 lowbit
高效計算
lowbit(i) = i & (-i)
理解這行僞代碼需要一些二進制和位運算的知識作爲鋪墊。首先,我們知道負數的二進制表示爲:相應正數的二進制表示的反碼 + 1
。
例 8
計算 的二進制表示。
分析: 的二進制表示爲 ,先表示成反碼,即“ 變 , 變 ”,得 ,再加 ,得 。
例 9
當
i = 6
時,計算 。
分析:
- 由例 7 及「與」運算的定義,把它們按照數位對齊上下寫好:
0000 0110
1111 1010
0000 0010
- 上下同時爲 才寫 ,否則寫 ,最後得到
0000 0010
,這個二進制數表示成十進制數就是 。建議大家多在稿紙上寫幾個具體的例子來計算 ,進而理解爲什麼 ; - 下面我給出一個我的直觀解釋:如果我們直接將一個整數「位取反」,再與原來的數做「與」運算,一定得到 。巧就巧在,負數的二進制表示上,除了要求對「按位取反」以外,還要「加」 ,在「加」 的過程中產生的進位數即是「將 表示成二進制以後,從右向左數,遇到 停止時數出 的個數」。
那麼我們知道了 以後,又有什麼用呢?由於位運算是十分高效的,它能幫助我們在樹狀數組中高效計算「從子結點到父結點」(即對應「單點更新」操作),高效計算「前綴和由預處理數組的那些元素表示」(即對應「前綴和查詢操作」)。
體會 lowbit
的作用
1、「單點更新」操作:從子結點到父結點
例 10
修改 , 分析對數組 產生的變化。
分析:
- 從圖中我們可以看出 的父結點以及祖先結點依次是 、、 ,所以修改了 以後 、、 的值也要修改;
- 先看 ,, 就是 的父親結點 的下標值;
- 再看 ,, 就是 的父親結點 的下標值;
- 從圖中,也可以驗證:紅色結點的下標值 + 右下角藍色圓形結點的值 = 紅色結點的雙親結點的下標值。
下面試圖解釋這個現象(個人理解):
- 即 ,從右向左,遇到 放過,遇到 爲止,給這個數位加 ,這個操作就相當於加上了一個 的二進制數,即一個 值,有意思的事情就發生在此時,馬上就發發生了進位,得到 ,即 的二進制表示;
- 接下來處理 ,從右向左,從右向左,遇到 放過,遇到 爲止,給這個數位加 ,同樣地,這個操作就相當於加上了一個 的二進制數,即一個 值,可以看到,馬上就發發生了進位,得到 ,即 的二進制表示;
- 從上面的敘述中,你可以發現,我們又在做「從右邊到左邊數,遇到 之前數出 的個數」這件事情了,
由此我們可以總結出規律:從已知子結點的索引 ,則結點 的父結點的索引 的計算公式爲:
還需要說明的是,這不是巧合和循環論證,這正是因爲對「從右邊到左邊數出 的個數,遇到 停止這件事情」的定義,使用 可以快速計算這件事成立,纔會有的。
分析到這裏「單點更新」的代碼就可以馬上寫出來了。
Java 代碼:
/**
* 單點更新
*
* @param i 原始數組索引 i
* @param delta 變化值 = 更新以後的值 - 原始值
*/
public void update(int i, int delta) {
// 從下到上更新,注意,預處理數組,比原始數組的 len 大 1,故 預處理索引的最大值爲 len
while (i <= len) {
tree[i] += delta;
i += lowbit(i);
}
}
public static int lowbit(int x) {
return x & (-x);
}
2、「前綴和查詢」操作:計算前綴和由預處理數組的那些元素表示
還是上面那張圖。
例 11
求出「前綴和(6)」。
- 由圖可以看出前綴和(6) = + ;
- 先看 ,, 正好是 的上一個非葉子結點 的下標值。這裏給出我的一個直觀解釋,如果下標表示高度,那麼上一個非葉子結點,其實就是從右邊向左邊畫一條水平線,遇到的牆的下標。只要這個值大於 ,都能正確求出來。
例 12
求出「前綴和(5)」。
- 再看 ,, 正好是 的上一個非葉子結點 的下標值,故「前綴和(5)」 = + 。
例 13
求出「前綴和(7)」。
- 再看 ,, 正好是 的上一個非葉子結點 的下標值,再由例 9 的分析,「前綴和(7)」 = + + 。
例 14
求出「前綴和(8)」。
- 再看 ,, , 表示沒有,從圖上也可以看出從右邊向左邊畫一條水平線,不會遇到的牆,故「前綴和(8)」 = 。
經過以上的分析,求前綴和的代碼也可以寫出來了。
Java 代碼:
/**
* 查詢前綴和
*
* @param i 前綴的最大索引,即查詢區間 [0, i] 的所有元素之和
*/
public int query(int i) {
// 從右到左查詢
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
可以看出「單點更新」和「前綴和查詢操作」的代碼量其實是很少的。
3、樹狀數組的初始化
- 這裏要說明的是,初始化前綴和數組應該交給調用者來決定;
- 下面是一種初始化的方式。樹狀數組的初始化可以通過「單點更新」來實現,因爲「最最開始」的時候,數組的每個元素的值都爲 ,每個都對應地加上原始數組的值,就完成了預處理數組 的創建;
- 這裏要特別注意,
update
操作的第 個索引值是一個變化值,而不是變化以後的值。因爲我們的操作是逐層上報,彙報變更值會讓我們的操作更加簡單,這一點請大家反覆體會。
Java 代碼:
public FenwickTree(int[] nums) {
this.len = nums.length + 1;
tree = new int[this.len + 1];
for (int i = 1; i <= len; i++) {
update(i, nums[i]);
}
}
基於以上所述,樹狀數組的完整代碼已經可以寫出來了。
Java 代碼:
public class FenwickTree {
/**
* 預處理數組
*/
private int[] tree;
private int len;
public FenwickTree(int n) {
this.len = n;
tree = new int[n + 1];
}
/**
* 單點更新
*
* @param i 原始數組索引 i
* @param delta 變化值 = 更新以後的值 - 原始值
*/
public void update(int i, int delta) {
// 從下到上更新,注意,預處理數組,比原始數組的 len 大 1,故 預處理索引的最大值爲 len
while (i <= len) {
tree[i] += delta;
i += lowbit(i);
}
}
/**
* 查詢前綴和
*
* @param i 前綴的最大索引,即查詢區間 [0, i] 的所有元素之和
*/
public int query(int i) {
// 從右到左查詢
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
public static int lowbit(int x) {
return x & (-x);
}
}
Python 代碼:
class FenwickTree:
def __init__(self, n):
self.size = n
self.tree = [0 for _ in range(n + 1)]
def __lowbit(self, index):
return index & (-index)
# 單點更新:從下到上,最多到 size,可以取等
def update(self, index, delta):
while index <= self.size:
self.tree[index] += delta
index += self.__lowbit(index)
# 區間查詢:從上到下,最少到 1,可以取等
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= self.__lowbit(index)
return res