「樹狀數組」第 3 節:理解 lowbit 操作

下面我們介紹一種很酷的操作,叫做 lowbit ,它可以高效地計算 2k2^k,即我們要證明:

lowbit(i)=2k {\rm lowbit}(i) = 2^k

其中 kk 是將 ii 表示成二進制以後,從右向左數,遇到 11 則停止時,數出的 00 的個數。

通過 lowbit 高效計算 2k2^k

lowbit(i) = i & (-i)

理解這行僞代碼需要一些二進制和位運算的知識作爲鋪墊。首先,我們知道負數的二進制表示爲:相應正數的二進制表示的反碼 + 1

例 8

計算 6-6 的二進制表示。

分析:66 的二進制表示爲 0000  01100000\;0110,先表示成反碼,即“ 00111100”,得 1111  10011111\;1001,再加 11,得 1111  10101111\;1010

例 9

i = 6 時,計算 lowbit(i){\rm lowbit}(i)

分析:

  • 由例 7 及「與」運算的定義,把它們按照數位對齊上下寫好:
0000 0110
1111 1010
0000 0010
  • 上下同時爲 11 才寫 11,否則寫 00,最後得到 0000 0010,這個二進制數表示成十進制數就是 22。建議大家多在稿紙上寫幾個具體的例子來計算 lowbit{\rm lowbit},進而理解爲什麼 lowbit(i)=2k{\rm lowbit}(i)=2^k
  • 下面我給出一個我的直觀解釋:如果我們直接將一個整數「位取反」,再與原來的數做「與」運算,一定得到 00。巧就巧在,負數的二進制表示上,除了要求對「按位取反」以外,還要「加」 11,在「加」 11 的過程中產生的進位數即是「將 ii 表示成二進制以後,從右向左數,遇到 11 停止時數出 00 的個數」。

那麼我們知道了 lowbit{\rm lowbit} 以後,又有什麼用呢?由於位運算是十分高效的,它能幫助我們在樹狀數組中高效計算「從子結點到父結點」(即對應「單點更新」操作),高效計算「前綴和由預處理數組的那些元素表示」(即對應「前綴和查詢操作」)。

體會 lowbit 的作用

1、「單點更新」操作:從子結點到父結點

在這裏插入圖片描述

例 10

修改 A[3]A[3], 分析對數組 CC 產生的變化。

分析:

  • 從圖中我們可以看出 A[3]A[3] 的父結點以及祖先結點依次是 C[3]C[3]C[4]C[4]C[8]C[8] ,所以修改了 A[3]A[3] 以後 C[3]C[3]C[4]C[4]C[8]C[8] 的值也要修改;
  • 先看 C[3]C[3]lowbit(3)=1{\rm lowbit}(3) = 13+lowbit(3)=43 + {\rm lowbit}(3) = 4 就是 C[3]C[3] 的父親結點 C[4]C[4] 的下標值;
  • 再看 C[4]C[4]lowbit(4)=4{\rm lowbit}(4) = 44+lowbit(4)=84 + {\rm lowbit}(4) = 8 就是 C[4]C[4] 的父親結點 C[8]C[8] 的下標值;
  • 從圖中,也可以驗證:紅色結點的下標值 + 右下角藍色圓形結點的值 = 紅色結點的雙親結點的下標值。

下面試圖解釋這個現象(個人理解):

  • 3300110011,從右向左,遇到 00 放過,遇到 11 爲止,給這個數位加 11,這個操作就相當於加上了一個 2k2^k 的二進制數,即一個 lowbit{\rm lowbit} 值,有意思的事情就發生在此時,馬上就發發生了進位,得到 01000100,即 44 的二進制表示;
  • 接下來處理 01000100,從右向左,從右向左,遇到 00 放過,遇到 11 爲止,給這個數位加 11,同樣地,這個操作就相當於加上了一個 2k2^k 的二進制數,即一個 lowbit{\rm lowbit} 值,可以看到,馬上就發發生了進位,得到 10001000,即 88 的二進制表示;
  • 從上面的敘述中,你可以發現,我們又在做「從右邊到左邊數,遇到 11 之前數出 00 的個數」這件事情了,
    由此我們可以總結出規律:從已知子結點的索引 ii ,則結點 ii 的父結點的索引 parent{\rm parent} 的計算公式爲:

parent(i)=i+lowbit(i) {\rm parent}(i) = i + {\rm lowbit}(i)

還需要說明的是,這不是巧合和循環論證,這正是因爲對「從右邊到左邊數出 00 的個數,遇到 11 停止這件事情」的定義,使用 lowbit{\rm lowbit} 可以快速計算這件事成立,纔會有的。

分析到這裏「單點更新」的代碼就可以馬上寫出來了。

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) = C[6]C[6] + C[4]C[4]
  • 先看 C[6]C[6]lowbit(6)=2{\rm lowbit}(6) = 26lowbit(6)=46 - {\rm lowbit}(6) = 4 正好是 C[6]C[6] 的上一個非葉子結點 C[4]C[4] 的下標值。這裏給出我的一個直觀解釋,如果下標表示高度,那麼上一個非葉子結點,其實就是從右邊向左邊畫一條水平線,遇到的牆的下標。只要這個值大於 00,都能正確求出來。

例 12

求出「前綴和(5)」。

  • 再看 C[5]C[5]lowbit(5)=1{\rm lowbit}(5) = 15lowbit(6)=45 - {\rm lowbit}(6) = 4 正好是 C[5]C[5] 的上一個非葉子結點 C[4]C[4] 的下標值,故「前綴和(5)」 = C[5]C[5] + C[4]C[4]

例 13

求出「前綴和(7)」。

  • 再看 C[7]C[7]lowbit(7)=1{\rm lowbit}(7) = 17lowbit(7)=67 -{\rm lowbit}(7) = 6 正好是 C[7]C[7] 的上一個非葉子結點 C[6]C[6] 的下標值,再由例 9 的分析,「前綴和(7)」 =C[7]C[7] + C[6]C[6] + C[4]C[4]

例 14

求出「前綴和(8)」。

  • 再看 C[8]C[8]lowbit(8)=8{\rm lowbit}(8) = 88lowbit(8)=08 - {\rm lowbit}(8) = 000 表示沒有,從圖上也可以看出從右邊向左邊畫一條水平線,不會遇到的牆,故「前綴和(8)」 = C[8]C[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、樹狀數組的初始化

  • 這裏要說明的是,初始化前綴和數組應該交給調用者來決定;
  • 下面是一種初始化的方式。樹狀數組的初始化可以通過「單點更新」來實現,因爲「最最開始」的時候,數組的每個元素的值都爲 00,每個都對應地加上原始數組的值,就完成了預處理數組 CC 的創建;
  • 這裏要特別注意,update 操作的第 22 個索引值是一個變化值,而不是變化以後的值。因爲我們的操作是逐層上報,彙報變更值會讓我們的操作更加簡單,這一點請大家反覆體會。

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章