# 2024q1 Homework4 (quiz3+4) ## [第三周](https://hackmd.io/@sysprog/linux2024-quiz3) ### 測驗一 #### 版本一 - 使用 log2 計算開平方 首先先使用 log2 取得最高位元,然後從最高位元開始檢查該位元是否有值,判斷依據使用 `if ((result + a) * (result + a) <= N)`,若小於輸入代表還沒超過 N 的值,所以要加一,從高位元做到低位元,慢慢收斂。 #### 版本二 - 不使用 log2 計算開平方 版本一和版本二概念可以說是一模一樣,差別是在最高位元的取得,版本一使用 log2 而版本二則使用 while loop 達成。 ```c while (n > 1) { n >>= 1; msb++; } ``` #### 版本三 - Digit-by-digit calculation $N$ 是我們想要求的平方根, $N^2=(a_n+a_{n-1}+a_{n-2}+...+a_0)^2, a_m=s^m \, or \, a_m=0$ $\left[ \begin{array}{ccccc} a_0a_0 & a_1a_0 & a_2a_0 & ... & a_na_0\\ a_0a_1 & a_1a_1 & a_2a_1 & ... & a_na_1\\ a_0a_2 & a_1a_2 & a_2a_2 & ... & a_na_2\\ ... & ... & ... & ... & ... \\ a_0a_n & a_1a_n & a_2a_n & ... & a_na_n\\ \end{array} \right]$ $\left[ \begin{array}{ccccc} a_0a_0 & a_1a_0 & a_2a_0 & ... & a_na_0\\ a_0a_1 & \\ a_0a_2 & \\ ... & \\ a_0a_n\\ \end{array} \right]$ $\left[ \begin{array}{ccccc} & \\ & a_1a_1 & a_2a_1 & ... & a_na_1\\ & a_1a_2 &\\ & ... & \\ & a_1a_n &\\ \end{array} \right]$ $\left[ \begin{array}{ccccc} & \\ & \\ & & a_2a_2 & ... & a_na_2\\ & & ... & \\ & & a_2a_n & \\ \end{array} \right]$ 透過觀察上面的矩陣,我們可以把 $N^2$ 的式子整理成: $N^2=a_n^2+[2a_n+a_{n-1}]a_{n-1}+...+[2(\sum_{i=1}^na_i)+a_0]a_0$ 可以歸納出 $P_m =a_n+a_{n-1}+a_{n-2}+...+a_{m+1}+a_m = P_{m+1}+a_M$ 現在我們得到一個關鍵的式子 $P_m = P_{m+1} + a_m$,這說明了我們可以透過迭帶的方式求出 $P_0$。方法是從高位到低位檢查是否 $P_m^2\leq N^2$,若成立則代表該位元可以被設為 $1$,若不成立則代表該位元為 $0$。 講解完理論後該如何實作,上面提到檢查是否 $P_m^2\leq N^2$,但算平方的成本太高,因此文章提出使用**上一輪的資訊**來判斷,我們定義一個新的參數 $X$,然後將 $X_m$ 表示為與上一輪有關,這時會有多一項參數 $Y_m$: $X_m=N^2-P_m^2=X_{m+1}-Y_m$ 將 $Y_m$ 定義成下面式子: $Y_m=P_m^2-P_{m+1}^2=(a_n+a_{n-1}+...+a_{m+1}+a_m)^2-(a_n+a_{n-1}+...+a_{m+1})^2=(2P_{m+1}+a_m)a_m$ 如果該位元為 $0$,則 $Y_m=0$ 如果該位元為 $1$,則 $Y_m=2P_{m+1} a_m+a_m^2=P_{m+1}a_{m+1}+a_m^2$ 令 $c_m = P_{m+1}a_{m+1}$ $d_m = a_m^2$ 可以推出下一位元 $c_{m-1} = P_m2^m=(P_{m+1}+a_m)2^m = P_{m+1}2^m+a_m2^m$ $d_{m-1} = \frac{d_m}{4}$ 如果該位元為 $0$,則 $c_{m-1}=c_m/2$ 如果該位元為 $1$,則 $c_{m-1}=c_m/2+d_m$ 所以說我們就可以透過 $c_{m-1}$,最後一個位元為 $n = 0$,因此 $c_{0-1}=P_0=a_n+a_{n-1}+...+a_0$ 即可算出所求。 對比程式碼 ```c int i_sqrt(int x) { if (x <= 1) /* Assume x is always positive */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= AAAA) { int b = z + m; z >>= BBBB; if (x >= b) x -= b, z += m; } return z; } ``` 與前兩個版本相同,都是從最高位開始計算,因此可以看到初始值使用 `__builtin_clz(x)` 來取得最高位,然後我們需要對 `x = 0` 的情況做處理。然後 `m` 即為上述數學推導中的 $d_m$,還記得不管該位元是否為 $0$,下一輪的 $d_m$ 也就是 $d_{m-1}=d_m/4$,無論如何都會除以 $4$,因此利用 `AAAA = 2`,進行迭代。接下來 `b` 為數學推導的 $Y_m$,所以 `z` 為 $c_m$。因為 若該位為 $1$,$c_{m-1}=c_m/2+d_m$ 若該位為 $0$,$c_{m-1}=c_m/2$ 所以我們將 $c_m/2$ 提出去,`z >>= 1`,所以 `BBBB = 1`。然後再進行比較 `if (x >= b)`,若成立的話就將 $d_m$ 也就是 `m` 加到 `z`,經過反覆迭代即可以求出 $P_0$ 所求。 ### 測驗二 為了可以不要使用 `/` `%` 來做除法,可以使用近似的方式求解,假設我們的除數為 $q$,那麼 $x / q$ 則可以表示成 $x * \frac{1}{q}$ 倒數形式,如果 $q$ 是 2 的冪,那麼除法實作則會相當簡單,但並不是所有的數字的倒數都可以使用 2 進位精準表示,如 $1 / 3$ 使用二進位會表示成 `0.010101010101010...` 的無理數,因此如何近似除數就成為這個測驗的主要技巧。 接下來我們來探討除數為 10 的情況,$1/10$ 使用二進位可以表示成 `0.000110001...`,表示的不精準,因此我們也需要透過近似方式求解,文章中除數使用 $\frac{128}{13} \approx 9.84$,這個除數是根據精確度而決定。 假設 `l` 是一個比 `n` 還小的非負整數,只要以 $n$ 算出來的除數在 $n$ 除以該除術後在精度內,則 $l$ 除以該除數能然會在精度內,近似的這個手法就是根據這一個猜想。因此我們可以根據最大情況 `19` 來考慮: $$ 1.9\leq\frac{19}{x}\leq1.99 \Rightarrow 9.55\leq x\leq10 $$ 我們就可以使用 $\frac{an}{2^N}$ 來配出一個可以用的數字 $\frac{128}{13} \approx 9.84$。 決定好除數為 $\frac{128}{13}$ 後,也就是$x * \frac{13}{128}$,我們就可以使用 shift 來拼湊出這一個數字,首先將 $\frac{13}{128} = \frac{13}{8} * \frac{1}{16}$,因此我們可以得到 $\frac{13}{8}*tmp = \frac{tmp}{8}+\frac{tmp}{2}+tmp$,如下。 ```c (tmp >> 3) + (tmp >> 1) + tmp ``` 但因為我們會先將 `tmp` 右移,造成最低 3 位會被捨棄,因此需要先將最低 3 位存起來,然後再合併起來。 ```c d0 = q & 0b1; d1 = q & 0b11; d2 = q & 0b111; ``` ```c ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) ``` 這一個步驟我想了很久,為甚麼不先將 `tmp * 13` 再除 8,如下。 ```c ((tmp << 3) + (tmp << 2) + tmp) >> 3 ``` 我認為是因為 overflow 的問題,理論上來說要先左移也可以,不過被保存的數字也需要做相應的調整,需要保存最高的 3 位。 最後考慮 128,即可以得到商數和餘數。 ```c q = ((((tmp >> 3) + (tmp >> 1) + tmp) << 3) + d0 + d1 + d2) >> 7; r = tmp - (((q << 2) + q) << 1); ``` #### 包裝函式 接下來看到包裝的函式,這個函式與我們所討論的看起來落差相當大。 ```c #include <stdint.h> void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod) { uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */ uint32_t q = (x >> 4) + x; x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; *div = (q >> CCCC); *mod = in - ((q & ~0x7) + (*div << DDDD)); } ``` `x = (in | 1) - (in >> 2);` 主要是將`x = 0.75 * in`。而我們可以透過下方的程式碼讓 `q` 趨近於 0.8,如此一來我們可以用到上面的概念,將 $x/10$ 表示成 $x * \frac{a}{8}$,其中 $a$ 趨近於 0.8。 下方程式碼則是在使 q 趨近於 0.8。首先經過 `q = (x >> 4) + x;` 得到 $q=in*\frac{51}{64}=0.796875*in$,然後會經過 4 次的 `q = (q >> 8) + x;`,得到 $q=in*(\frac{51}{64} + \frac{51}{64*256})=0.79998*in$,最後經過四次會讓 `q` 趨近於 `0.8 * in`。所以說要求商的話需要再除以 8,也就是左移 3 位,因此 `CCCC = 3`。 ```c uint32_t q = (x >> 4) + x; x = q; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; q = (q >> 8) + x; ``` 最後要算餘數,$mod = in - div * 10$,比較 `*mod = in - ((q & ~0x7) + (*div << DDDD));`其中 `((q & ~0x7))` 的意思為 $8div$,因此我們可以透過對 `div` 左移一位得到 $2div$,所以`DDDD = 1`。 比較這兩種方法,他們其實都是在處理除數的近似,只是數字不同罷了。 ### 測驗三 #### 版本一 看到第一種 ilog2 的實作,採用了非常簡單的概念,檢查 `i` 裡面有沒有值,如果有則 `log++` 沒有的話就 shift 一位,會一直做到最高位被移走。可以注意到 `log` 的初始值為 `-1`,這是因為需要考慮到 $2^0$ 的情況,也就是說不能把 $2^0$ 算進去。這個實作方法相當簡單,同時精度也不高,只能處理 log2 的整數部分。 ```c int ilog2(int i) { int log = -1; while (i) { i >>= 1; log++; } return log; } ``` #### 版本二 接下來看到版本二的實作,觀察下方程式碼我們可以發現,基本概念與版本一相同,不同的地方是將 $2^{16}$, $2^8$, $2^4$, $2^2$ 特別處理,為甚麼要這麼做呢,就是因為如此一來,可以直接檢查很多位,不用像版本一這樣需要把所有的位數都檢查一遍。因此根據 $2^{16}$,一次檢查 16 位,將 i shift 16 位,並將結果 +16,`AAAA = 65536`,依此類推,`BBBB = 256`,`CCCC = 16`。 這個版本可以有效加速運算,我們實際舉一個例子,假設 `ilog2(257)`,因此會在 `while (i >= 256)` 這個迴圈執行一次然後就結束,但版本一要執行 9 次,因為 257 的 binary 有 9 位。 ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= AAAA) { result += 16; i >>= 16; } while (i >= BBBB) { result += 8; i >>= 8; } while (i >= CCCC) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` #### 版本三 - 使用 GNU extension 版本三的程式相當簡潔,使用了 GNU extension,`__builtin_clz` 是一個內建函式,用來計算一個整數在二進制表示中,從最高位(最左邊)開始的零位的個數,前兩個版本都是從最低位開始算,而這個正好相反。 可以來推理 `DDDD` 的答案,值觀上來想,`v` 應該就是答案了,但是如果 `v` 是 0 呢? `__builtin_clz()` 若是參數為 0 則結果未定義,所以我們要讓 `v` 無論如何都會有 1 位,如此一來則不會有未定義的情況發生,因此 `v|1` 這個運算僅僅是在加強函式的強健性。 ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(DDDD)); } ``` ### 測驗四 這一測驗主要在計算指數加權移動平均 EMWA,使經過時間越久的歷史資料的權重也會越低,數學的定義為: $S_t=\begin{cases} Y_0 , \quad \quad \quad \quad \quad \quad \quad \quad t=0 \\ \alpha Y_t+(1-\alpha)\cdot S_{t-1} \quad t>0\\ \end{cases}$ 觀察這一個式子可以發現 $\alpha$ 就是所謂的權重,$\alpha$ 越高代表先前所影響越少,因為會被 $(1-\alpha)$ 的次方稀釋掉,我們可以透過調整這一個參數以控制以往資訊對 EMWA 的重要性,其中 $Y_0$ 為初始的資料,$Y_t$ 為當前時刻的資料,$S_t$ 為前一刻所計算出的平均數。 $S_t=\alpha Y_t+(1-\alpha)\cdot S_{t-1}$ $\quad =\alpha(Y_t+(1-\alpha)Y_{t-1}+(1-\alpha)^2Y_{t-2}+...+(1-\alpha)^kY_{t-k}+...+Y_0)$ 接下來看到程式碼的部分,首先定義一個 `struct ewma`,裡面有 `internal` 代表計算出的平均數,`factor` 為一個縮放因子,我們要將 value 放進這一個結構裡都要乘上 `factor`,而 `factor` 為 2 的冪。`weight` 為權重,一樣是 2 的冪。 ```c struct ewma { unsigned long internal; unsigned long factor; unsigned long weight; }; ``` 以下程式碼為初始化的部分,可以觀察到存進去 `weight` `factor` 都是以 `log2` 存進去,而 `interval` 一開始會被設為 0。 ```c void ewma_init(struct ewma *avg, unsigned long factor, unsigned long weight) { if (!is_power_of_2(weight) || !is_power_of_2(factor)) assert(0 && "weight and factor have to be a power of two!"); avg->weight = ilog2(weight); avg->factor = ilog2(factor); avg->internal = 0; } ``` 接下來這個程式為主要計算平均值的地方,首先我們要先判斷 `avg->internal` 有沒有東西,如果沒有的話我們直接把 `val` 加進去 `internal`,但要注意的是,要將 `val` 乘上 `factor`,對應到數學式的 $Y_0$,因為 `avg->factor` 已經取過對數,因此我們可以用左移來實現。 如果 `avg->interval` 已經有值的話,那我們需要使用 $S_t=\alpha Y_t+(1-\alpha)\cdot S_{t-1}$ 來做運算,以數學表示來說,$\alpha$ 是一個介於 0 到 1 的值,但我們的 struct 裡面定義的 `weight` 為 `unsigned long`,因此需要對數學式做一些操作。 $set\ \ w=\frac{1}{\alpha}$ $S_t=Y_t/w+(1-1/w)\cdot S_{t-1}$ $\ \ \ \ =(Y_t/w+(1-1/w)\cdot S_{t-1})\cdot w /w$ $\ \ \ \ =(Y_t+(w-1)\cdot S_{t-1})/w$ $\ \ \ \ =((S_{t-1}\cdot w - S_{t-1})+Y_t)/w$ 根據上式,可以對照程式碼,`EEEE = avg->weight`,`DDDD = avg->factor`。 ```c struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal ? (((avg->internal << EEEE) - avg->internal) + (val << FFFF)) >> avg->weight : (val << avg->factor); return avg; } ``` ### 測驗五 測驗五結合了 ceil 和 log2,可以看到以下程式碼與測驗三的版本二類似。差在測驗五會先 `x--`,進而造成判斷的數值在測驗三為`0d65536`,而測驗五為 `0d65536 - 0d1 = 0xFFFF`。 測驗三中的 `result += 16` 在測驗五中為 `r |= shift`,而在最後一行`return (r | shift | x > GGG) + 1;` 與測驗三不一樣的地方在於 `x > GGG`,這一部分是為了要無條件進位,`GGG = 1`,如此一來只要大於等於 2 的數字在第一個位元就一定是 1。 ```c int ceil_ilog2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x > GGG) + 1; } ``` 測驗三 - 版本二 ```c while (i >= 65536) { result += 16; i >>= 16; } while (i >= 256) { result += 8; i >>= 8; } ... ``` ## [第四周](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-2) ### 測驗一 popcount 是用來計算二進位表示中有多少位元是 1。透過 `v &= (v - 1)`,可以將最低位元的 1 清掉並記錄,舉例來說 `0b10100 - 0b1 = 0b10011`,會讓最低位元為 1 以下(含)的位元取反,然後再做且運算即可以達到效果。而 `n = -(~n)` 可以用二補數來解釋 $-n = ~n+1$ 為二補數的定義,因此這一段程式碼要表達的其實與 `n++` 相同。 ```c unsigned popcount_naive(unsigned v) { unsigned n = 0; while (v) v &= (v - 1), n = -(~n); return n; } ``` 然而因為上述算法的時間複雜度取決於 set bit 的個數,因此可以改寫為常數時間時間複雜度的實作。 以下程式碼的運算基於以下數學運算: $popcount(x) = x -\lfloor \frac{x}{2}\rfloor-\lfloor \frac{x}{4}\rfloor-...--\lfloor \frac{x}{2^{31}}\rfloor$ 假設 $x=b_{31}...b_3b_2b_1b_0$,$x[3:0]$ 這四個位元可以推導成: $popcount(x)=(2^3b_3+2^2b_2+2^1b_1+2^0b_0)-(2^2b_3+2^1b_2+2^0b_1)-(2^1b_3+2^0b_2)-2^0b_3$ $\quad \quad \quad \quad \quad \ \ = \\ (2^3-2^2-2^1-2^0)b_3+(2^2-2^1-2^0)b_2+(2^1-2^0)b1+2^0b_0$ 程式碼實作上以 4 bit 為單位,以上運算對應到 ```c n = (v >> 1) & 0x77777777; v -= n ``` 透過將 `v` 右移 1 位並減掉達成 $(2^3-2^2-2^1-2^0)b_3$,重複三次可以得到$(2^3b_3+2^2b_2+2^1b_1+2^0b_0)-(2^2b_3+2^1b_2+2^0b_1)-(2^1b_3+2^0b_2)$如此一來我們便可以得到 4 bit 的 popcount。 接下來這一步驟,目的是要獲取所有位元也就是 8 個 4 bit 的 popcount,首先 `(v + (v >> 4))` 透過位移得到前後兩組 4 bit 的總和,再透過 `0x0F0F0F0F` 當作遮罩,濾掉重複部分。 ``` B7 B6 B5 B4 B3 B2 B1 B0 // v 0 B7 B6 B5 B4 B3 B2 B1 // (v >> 4) 0 (B7+B6) 0 (B5+B4) 0 (B3+B2) 0 (B1+B0) // (v + (v >> 4)) & 0x0F0F0F0F ``` 接下來 `v *= 0x01010101` 利用乘法值式的特性,會再中間項出現所有 nibble 的累加,而這個位置剛好會在 $2^7$ 這個位置,因此最後將 `v` 右移 24 位。 ```c v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; ``` ```c unsigned popcount_branchless(unsigned v) { unsigned n; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; return v >> 24; } ``` 接下來針對 LeetCode 477 考慮 totalHammingDistance。我們可以用矩陣的觀點來看,total hamming distance 是倆倆比較的總和,假設有 A, B, C,三個元素相互比較可以建出下表,其中 `hd()` 為 `hammingDistance()`。 | | A | B | C | | --- |:--------:|:--------:| -------- | | A | 0 | hd(A, B) | hd(A, C) | | B | hd(A, B) | 0 | hd(B, C) | | C | hd(A, C) | hd(B, C) | 0 | 透過這個表格我們可以明白對稱性之影響,因此兩個 for loop 加總起來必須除以 2,`AAAA = 1`。 ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0;; for (int i = 0;i < numsSize;i++) for (int j = 0; j < numsSize;j++) total += __builtin_popcount(nums[i] ^ nums[j]); return total >> AAAA; } ``` ### 測驗二 這一個測驗的目標是不使用任何除法就計算出餘數,相比第三周的題目,需要先算出商數,再使用被除數減掉除數成以商數,這種方法可以直接計算出餘數。根據[Hacker's Delight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf) 提出的定理。 $If \ \ a \equiv b (mod\ \ m) \ \ and c \equiv d (mod\ \ m), then$ $$ a+c \equiv b+d (mod\ \ m) and $$ $$ ac \equiv bd (mod\ \ m) $$ 因此我們以 $mod\ \ 3$ 為例可以看到 $1 \equiv 1 (mod\ \ 3), \ \ 2 \equiv -1 (mod\ \ 3)$,我們可以透過對 $1$ 和 $-1$ 取餘數表示成一個二進位的式子。 $$ 2^k = 1(mod\ \ 3)\cdot (-1(mod\ \ 3))^k $$ 並套用 Hacker's Delight 提出的定理。 $$ 2^k=(-1)^k\cdot (mod\ \ 3) $$ $n$ 設為輸入,將 $n$ 表示為 $b_{n-1}b_{n-2}...b_1b_0$,則 $n=\sum_{i=0}^{n-1} b_i\cdot 2^i=n=\sum_{i=0}^{n-1} b_i\cdot (-1)^i(mod\ \ 3)$,如此一來便可以透過對奇數位和偶數位的位元總和計算出 $3$ 的模數。 看到以下程式碼 `popcount(n ^ 0xAAAAAAAA) + 23`,這一步是從另外一個定理推導出來的, $$ popcount(x\& \overline m)-popcount(x\&m) = popcount(x \oplus m)-popcount(m) $$ `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)`,透過這一個式子我們可以比較奇數位偶數位的模數,因為若是奇數位有值,他的模數會是 -1,偶數則是 1,透過相加即可以計算出所有的模數,所以我們要減掉奇數位的 `popcount`。 `popcount(n ^ 0xAAAAAAAA) - 16 + 39`,這邊為了不要讓算出來的數落在負數,所以我們可以加上一個 3 的倍數,在這個程式選擇了 39,理論上可以選擇任何 3 的倍數,但後續的運算也需要做相對調整,此時 $23\leq n \leq 23 +32=55$,所以我們要針對 n 在做一次運算,因為這時 `n` 的最大值為 55,也就是說不會超過 6 bit,所以使用 `n = popcount(n^ 0x2A) - 3` 來計算模數,這時 $-3\leq n \leq 2$。 `return n + ((n >> 31) & 3);` 這時 $-3\leq n \leq 2$,所以我們要針對負數做處理,方法是將 `n >> 31` 也就是判斷 sign bit,注意這邊是使用算數位移而非邏輯位移,所以若是負數的話會造成全 1,此時可以與 3 座且運算,也就是若小於 0 則加 3。 ```c int mod3(unsigned n) { n = popcount(n ^ 0xAAAAAAAA) + 23; n = popcount(n ^ 0x2A) - 3; return n + ((n >> 31) & 3); } ``` `n = popcount(n ^ 0xAAAAAAAA)`可以看成 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA) + 16`,可以看到因為我們加上了 16 所以會造成 table 有偏移的狀況,我們需要手動修正。假設輸入為 16,也就是 0b10000,經過運算得到的 n 為 17,就可以對第 17 個元素修正為 16(mod 3),推回來可以決定第 0 個元素是從 2 開始。 ```c int mod3(unsigned n) { static char table[33] = {2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1 }; n = popcount(n ^ 0xAAAAAAAA); return table[n]; } ``` Hacker's Delight 還提出了一種不用使用 popcount 的計算模數方法,其中關鍵在於,可以將 n 拆成兩部分,並將左邊的部分右移並與右邊相加不會對模數計算造成影響,所以要怎麼拆就是一大關鍵。取 7 的模數會有此關係式: $8^k \equiv 1(mod \ \ 7)$,$8^k = 2^{3k}$,所以可以看出只要是 shift 3 的倍數就不會影響到 mod 7 運算。值得注意的是 mod 不同的基數會有不同的限制。 ```c int remu7(unsigned n) { static char table[75] = {0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4,5,6, 0,1,2,3,4}; n = (n >> 15) + (n & 0x7FFF); // Max 0x27FFE. n = (n >> 9) + (n & 0x001FF); // Max 0x33D. n = (n >> 6) + (n & 0x0003F); // Max 0x4A. return table[n]; } ``` 這邊利用了 $n \ \ mod \ \ 7=\frac{8}{7}n(mod \ \ 8)$,`0x24924924` 為 $\lfloor\frac{2^{32}}{7}\rfloor$。最後一步則是將原本的 0~7 mapping 為 0~6。 ```c int remu7(unsigned n) { n = (0x24924924*n + (n >> 1) + (n >> 4)) >> 29; return n & ((int)(n - 7) >> 31); } ``` 透過結合上述兩種方法,可以判斷 `CCCC = 15`,因為 `0x7FFF`,將 `x` 拆成兩部分。 ```c static inline int mod7(uint32_t x) { x = (x >> CCCC) + (x & UINT32_C(0x7FFF)); /* Take reminder as (mod 8) by mul/shift. Since the multiplier * was calculated using ceil() instead of floor(), it skips the * value '7' properly. * M <- ceil(ldexp(8/7, 29)) */ return (int) ((x * UINT32_C(0x24924925)) >> 29); } ``` 觀察 `move_masks`,我們可以發現紀錄井字遊戲的位置時不單單只是記錄九宮格的位置,而是使用八條可能的路徑紀錄,三條橫線,三條直線,兩條斜線,也就是說一個位置的改變會改變到多條路徑。`move_masks` 以 4 bit 表示一條路徑,若為 `0b0100`,則代表該路徑上的第一個位置有值,`0b0010`代表該路徑上的第二個位置有值,以此類推。而從最高位到最低位依序代表的路徑為第一條橫線,第二條橫線,第三條橫線,第一條直線,第二條直線,第三條直線,第一條斜線,第二條斜線。所以我們以中間的位置為例,該點影響了第二條直線,第二條橫線和兩條斜線,而在三條路徑上剛好都是第二個位置有值,此點可表達為 `0x02002022`。 ```c static const uint32_t move_masks[9] = { 0x40040040, 0x20004000, 0x10000404, 0x04020000, 0x02002022, 0x01000200, 0x00410001, 0x00201000, 0x00100110, }; ``` 顯而易見的,勝利的條件即為任意一條路徑三個位置都為同一方,也就是 `0b0111`,因此只要有任意一條 `+0x1` 然後與 `0x8` 且運算有值, 即為獲勝,所以 `BBBB = 0x11111111`。 ```c /* Determine if the tic-tac-toe board is in a winning state. */ static inline uint32_t is_win(uint32_t player_board) { return (player_board + BBBB) & 0x88888888; } ``` ### 測驗三 X Tree 結合了 AVL Tree 和 紅黑樹的特性。 看到 `remove` 的函式,首先檢查愈刪除節點的右子樹,若存在那我們會尋找最小的節點,使用 `xt_first` 尋找,反之若要找最大局點,則使用 `xt_last` 。找到最小節點後,利用 `xt_replace_right` 將最小節點放到 `del` 指標,如此才會符合 小-中-大 的特性,所以 `AAAA = least`。因為改變了 `del` 右子樹的結構,所以我們要使用 `xt_update` 去檢查目前是否平衡,若不平衡就要進行 rotate。 同理,若存在左子樹,就要將左子樹的最大節點放到 `del` 的位置,因此 `CCCC = most`,而 `DDDD = xt_left(most)`。 函式的最後我們要檢查整棵樹是否達成平衡,因此 `EEEE = root` `FFFF = parent`,也就是檢查 `root` 是否達到平衡。 ```c static void __xt_remove(struct xt_node **root, struct xt_node *del) { if (xt_right(del)) { struct xt_node *least = xt_first(xt_right(del)); if (del == *root) *root = least; xt_replace_right(del, AAAA); xt_update(root, BBBB); return; } if (xt_left(del)) { struct xt_node *most = xt_last(xt_left(del)); if (del == *root) *root = most; xt_replace_left(del, CCCC); xt_update(root, DDDD); return; } if (del == *root) { *root = 0; return; } /* empty node */ struct xt_node *parent = xt_parent(del); if (xt_left(parent) == del) xt_left(parent) = 0; else xt_right(parent) = 0; xt_update(EEEE, FFFF); } ``` 來看看 `xt_update` ,勇於更新樹中節點的平衡狀態和 `hint` 值,首先透過 `xt_balance` 計算平衡因子,並獲取節點 `n` 的前一個 `hint` 和父節點 `p`,若平衡因子小於 -1 代表樹向右傾斜,要將樹進行右旋轉,若大於 1 代表向左傾斜,要將樹進行左旋轉,然後更新 `n` 的 `hint` 為右子樹中的最大值。最後如果節點的提示值為 0 或者 `hint` 發生了變化,則遞迴地使用 `xt_update`,更新父節點 p 的平衡狀態和 `hint`。 ```c static inline void xt_update(struct xt_node **root, struct xt_node *n) { if (!n) return; int b = xt_balance(n); int prev_hint = n->hint; struct xt_node *p = xt_parent(n); if (b < -1) { /* leaning to the right */ if (n == *root) *root = xt_right(n); xt_rotate_right(n); } else if (b > 1) { /* leaning to the left */ if (n == *root) *root = xt_left(n); xt_rotate_left(n); } n->hint = xt_max_hint(n); if (n->hint == 0 || n->hint != prev_hint) xt_update(root, p); } ```