contributed by < ssheep773
>
1
目的: 計算 N
的整數平方根
方法一 :
通過逐步逼近來計算 N
的平方根。它首先找到 N
的最大位元位置 msb
,並將最大位元位置對應的數值儲存在 a
\((a = 2^{msb})\) 。然後逐步減小 a
的值,同時檢查 \((result + a)^2\) 是否小於或等於 N
。如果是的話,就將 result
加上 a
,以逼近 N
的平方根。這樣的操作會持續進行,直到 a
的值為 0。
方法二 :
利用 Digit-by-digit calculation 提及常見的直式開方法 \((x + y)^2 = x^2 + 2xy + y^2\) 的變形實作開平方根。
若要求 \(x\) 的平方根,可以假設 \(x = N^2\) ,那麼 \(N\) 即為我們要求的平方根數值,接著,將 \(N^2\) 拆成
\[ N^2=(a_{n}+a_{n-1}+a_{n-2}+...+{a_0})^2 = [(a_{n}+a_{n-1}+...+{a_1})+({a_0})]^2 \]
經過整理
\[ N^2 = (\sum_{i=0}^na_i)^2 = (\sum_{i=1}^{n} a_i)^2 + 2a_0(\sum_{i=1}^{n}a_i) + a_0^2 \]
這裡假設 \(P_m = \sum_{i=m}^n a_i\) ,那麼 \(P_0 = a_{n}+a_{n-1}+a_{n-2}+...+{a_0}\) 即為我們要求的 \(N\), \(P_0\) 可在分解為 \(P_0 = (a_{n}+a_{n-1}+...+{a_1})+({a_0}) =P_1 + a_0\)
可以得到一般式
\[
P_m = P_{m+1} +a_m
\]
我們的目的是要試出所有 \(a_m\) ,\(a_m = 2^m or 0\)。從 \(m = n\) 一路試到 \(m = 0\) ,試驗 \(P_m^2 \le N^2\) 是否成立來求得 \(a_m\)
\[ \begin{cases} P_m = P_{m+1} +2^m \ \ ,if \ \ P_m^2 \le N^2\\ P_m = P_{m+1} \ \ ,if \ \ a_m=0\\ \end{cases} \]
然而如果每次都試驗 \(P_m^2 \le N^2\) 的運算成本會太高,一種想法是透過上一輪\(N^2 - P_{m+1}^2\) 的差值推估 \(N^2 - P_m\) 相關的證明如下:
因為前面 \(P_m\) 的假設 \(N\) 可以整理為
\[
N^2 = P_0^2 = a_0^2 + 2a_0P_1 + P_1^2
\]
並可推展出一般式
\[
P_m^2 = a_m^2 + 2a_mP_{m+1} + P_{m+1}^2
\]
經過移項
\[
P_m^2 -P_{m+1}^2 = 2a_mP_{m+1} + a_m^2
\]
假設 \(Y_m = P_m^2 -P_{m+1}^2 = 2a_mP_{m+1} + a_m^2\)
以及假設 \(X_m = N^2 -P_m^2\) ,\(X_m\geq0\) 即為每次判斷 \(a^m\) 的條件,可以推廣 \(X_{m+1} = N^2 -P_{m+1}^2\) 為前一次的差值
\[
Y_m = P_m^2 -P_{m+1}^2 = (N^2 -X_m) - (N^2 -X_{m+1}) = X_{m+1}-X_m
\]
我們就可以得到,從上一輪的差值 \(X_{m+1}\) 減去 \(Y_m\) 得到 \(X_m\)
\(X_m = N^2 -P_m^2 = X_{m+1} - Y_m\)
將原本 \(X_m \geq 0\) 的問題轉換為 \((X_{m+1} - Y_m) \geq 0\) 即使用到上一輪的差值來計算。
進一步調整,將 \(Y_m\) 拆成 \(c_m\) 和 \(d_m\)
以此推出下一輪
這裡說明初始與中止條件,因為是判斷 \((X_{m+1} - Y_m) \geq 0\) ,所以我們需要準備兩者的初始條件,在程式進行時,會由 \(a_n\) 往下試
因為 \(d_m\) 恆大於 \(0\) 所以終止條件可以設為 d != 0
,又 \(c_{-1} = P_02^0 = P_0 = a_n + a_{n-1} +...+ a_0\) 故求出 \(c_{-1}\) 則等於求出 \(N\) 的平方根。
最後看到程式的部分
首先排除 x
為負數及 0 或 1 的情況
變數 m
對應到 \(d_m\) ,其中因為 m = \(d_m = a_{m}^2 = (2^{m})^2\) ,所以 m 不可能為奇數,因此要 & ~1UL
,同時再前一個步驟也先處理 x = 1
的情況
因為 \(d_{m-1} = \dfrac{d_m}{4}\) 所以每次迴圈 m
右移 2 。
x
對應到 \(X_m\) 呼應初始條件 \(X_{n+1} = N\),變數 b
對應到 \(Y_m\) ,z
對應到 \(c_m\) 同時也說明 \(Y_m = c_m + d_m\)
而計算 \(c_{m-1} =
\begin{cases}
c_m/2 +d_m , if a_m = 2^m\\
c_m/2 , if a_m = 0 \\
\end{cases}\) ,不管 \(a_m = 2^m\) 是否成立 \(c_m\) 都需要除以二,這裡以 z >>= 1
實作
且若 則減去 \(Y_m\) 得 \(X_{m-1}\)。
前面提到我們將原本 \(X_m \geq 0\) 的問題轉換為 \((X_{m+1} - Y_m) \geq 0\) 同時也等價 \(X_{m+1} \geq Y_m\) ,若成立則 \(X_{m-1} = X_m - Y_m\)
ffs
是找最低為 1 的位元,所以透過迴圈的方式由低往高位元找,到最高的有效位元的位置為止,並且還需要將其減一才會與 (31 - __builtin_clz(x)
相等,若沒有減一,雖對於結果沒有影響,但是會多花費一輪的運算成本。
2
針對正整數在相鄰敘述進行 mod 10
和 div 10
操作,減少運算成本
原始的方法:
可利用除法原理將 mod 10 和 div 10 合併。根據除法原理:
\(f = g \times Q +r\)
其中 \(f:\) 被除數, \(g:\) 除數, \(Q:\) 商, \(r:\) 餘數
將程式碼改寫為
這邊參考《Hacker's Delight》,採用 bitwise operation 來實作 10 的除法,但是因為 10 有 5 這個因數,無法完全用 2 的冪項來表示,因此結果會是不精確的。
我們的目標是,得到 tmp / 10 且直到小數點後第一位都是精確的。
為此,提出一個猜想,假設我目標的最大數是 \(n\) ,而 \(l\) 是小於 \(n\) 的非負整數,則只要以 \(n\) 算出來的除數在 \(n\) 除以該除數後在精確度內,則 \(l\) 除與該除數仍然會在精確度內,證明如下:
假設 \(n=ab\) ( \(a\) 是十位數 \(b\) 是個位數),\(l=cd\) ( \(c\) 是十位數 \(d\) 是個位數)。
首先看到 \(\frac{n}{x}\),因為我們是要確保小數點後第一位是精確的,所以 \(\frac{n}{x}\) 可以表示如下 :
\(a.b\leq\frac{n}{x}\leq a.b9\\ \Rightarrow\frac{n}{a.b9}\leq x\leq\frac{n}{a.b}\)
透過上面的轉換,我們將問題轉換為確保 \(x\) 的範圍,來達到 \(\frac{n}{x}\) 在精度以內,下面是對 \(x\) 範圍的探討
\(x\) 的右不等式 \(x \leq \frac{n}{a.b} = \frac{ab}{a.b} = 10\) ,因此一定在精確度內。
\(x\) 的左不等式 \(\frac{n}{a.b9}\leq x\) 無法直接證明,透過代入 \(l\) 來證明,\(l\)的不等式如下:
\(c.d\leq\frac{l}{x}\leq c.d9\) ,透過將 \(\frac{n}{a.b9}\leq x\) 代入可表示為
\[ c.d\leq l \times \frac{a.b9}{n} \leq c.d9 \Rightarrow c.d\leq cd \times \frac{a.b9}{ab} \leq c.d9 \]
左不等式:
\[c.d\leq cd\times\frac{a.b9}{ab} = cd\times (\frac{a.b}{ab}+ \frac{0.09}{ab}) = c.d + \frac{0.09cd}{ab}
\]
最後推得 \(c.d\leq c.d + \frac{0.09cd}{ab}\) 左式顯然成立。
右不等式:
\[cd \times \frac{a.b9}{ab} \leq c.d9 \Rightarrow c.d + \frac{0.09cd}{ab}\leq c.d9 \Rightarrow c.d + \frac{0.09cd}{ab}\leq c.d + 0.09
\]
其中 \(\frac{0.09cd}{ab}\leq 0.09\) ,因為 \(ab > cd\) 所以右不等式也成立
於是前述猜想也成立,接下來只需要針對 \(n\) 來做討論即可,而因為 int tmp = (b[i] - '0') + (a[i] - '0') + carry
所以 tmp
最大是 9+9+1=19,\(n\) 也即為 19 ,下面針對 19
做討論即可
\[ 1.9\leq \frac{19}{x}\leq 1.99\\ \Rightarrow 9.55\leq x\leq10 \]
上述不等式可得知除數介於 \(9.55\) 至 \(10\) 之間皆可程式中達到相同效果,即除數直到小數點後第一位都是精確的,而我們的商會比實際的還要大有正偏差。
我們透過 bitwise operation 找到介於 \(9.55\) 至 \(10\) 之間的除數,除數的算式必定是 \(\frac{2^N}{a}\) ,其中 \(N\) 是非負整數, \(a\) 是正整數,而計算 \(n\) 的商可以寫成 \(\frac{an}{2^N}\)。
因此只需要透過查看 \(2^N\) 在配對適合的 \(a\) 即可
這裡 \(2^N = 128, a= 13 ,\frac{128}{13} \approx 9.84\) 即為可用的除數解
接著看到實際的 bitwise operation
q
是要求的商 \(\frac{an}{2^N}\) ,在這裡是 \(\frac{13tmp}{2^7}\),我們首先要得到 13 ,透過 (tmp >> 3) + (tmp >> 1) + tmp) << 3
得到 \(\frac{13tmp}{8} = \frac{tmp}{8}+\frac{4tmp}{8}+tmp\) 再 << 3
得到 \(13tmp\),最後再除以 128 即得到我們要的商 \(\frac{13tmp}{128}\)
(((q << 2) + q) << 1)
這部分是將 q * 10
透過 (q*4 + q) * 2
達到
而因為原有的 tmp
在右移後,會有部分位元被捨棄,因此要另行處理。
然而我覺得這步驟可以省略,因為後續的操作會在將 tmp >> 7
等於是補回來的位元馬上又被捨棄,而我思考若是在產生 13 而右移的位數大於 \(2^N\) 的 \(N\) 時,則需要處理,而目前分別是 3 跟 7 ,所以可以省略不處理。
在來看到包裝的函式
uint32_t x = (in | 1) - (in >> 2)
將 \(x =\frac{3}{4}in\)
再透過 uint32_t q = (x >> 4) + x
等於是 \(q = \frac{3}{4*2^4}in + \frac{3}{4}in = \frac{102}{128}in\) ,而其中 \(\frac{102}{128} \approx 0.797 \approx \frac{8}{10}\)
後續再用 q = (q >> 8) + x
對 q
做逼近增加精度更靠近 \(\frac{8}{10}\)
*div = (q >> 3)
是計算商,而我們前面求的 q
是 \(\frac{8}{10}in\) ,所以需要再除以 8 也就是 q >> 3
。
*mod = in - ((q & ~0x7) + (*div << 1))
是計算餘數,其中的 q & ~0x7
等於是 *div << 3
,那麼就程式碼可以轉換成 *mod = in - (*(div << 3) + (*div << 1))
後半部的 *(div << 3) + (*div << 1)
等於是 \(商\times8 + 商\times2 = 商 \times (8+2) = 商 \times10\) ,根據餘數定理 \(餘數 = 被除數(in) - 商(div) \times10 除數(10)\)
3
ilog2 可計算以 2 為底的對數,且其輸入和輸出皆為整數,這裡注意到因為輸出為整數,所以輸入的最高二進位有效位元位置即為結果。
方法一 :
由最低位元往高位元處找最高二進位有效位元位置
方法二 :
透過二元搜尋的方法尋找
方法三:
利用 GNU extension __builtin_clz
,找出最高有效位元前面 0 的數量,因此 31 - __builtin_clz(v | 1)
即為最高有效位元的位置,而因為 __builtin_clz
輸入若是 0 則無定義,所以使用 v | 1
確保輸入不為 0 。
在 linux/log2.h 中使用 fls
找出最高有效位元並且說明輸入 0 是未定義的。
4
Exponentially Weighted Moving Average (EWMA; 指數加權移動平均) 是種取平均的統計手法,並且使經過時間越久的歷史資料的權重也會越低,以下為 EWMA 的數學定義:
\[ S_t = \begin{cases} \ Y_0 \qquad\qquad\qquad\qquad ,t=0\\ \ \alpha Y_t +(1-\alpha) \cdot S_{t-1} \ \ ,t>0\\ \end{cases} \]
\(\alpha\) 表示歷史資料加權降低的程度,介在 0 ~ 1 之間,越高的 \(\alpha\) 會使歷史資料減少的越快
\(Y_t\) 表示在時間 \(t\) 時的資料點
\(S_t\) 表示在時間 \(t\) 時計算出的 EWMA
首先看到 ewma 結構,internal
儲存我們的 EWMA 值也就是 \(S_t\) ,值得注意的是它的型態是 unsigned long
是無號整數,所以我們可以知道這裡是使用定點數,並在 ewma_init()
註解處中有說明 factor
是 Scaling factor , 其中註解處有提到internal
可記錄最大值的計算公式為 ULONG_MAX / (factor * weight)
,其中 factor
是因為做 scaling 提高數值精度變相會犧牲可容納的對大值,而 weight
的部分不太理解,只能夠推測因為程式中有做 avg->internal << avg->weight
與 avg->internal >> avg->weight
與 factor
一樣。
並在 ewma_read
可以看到輸出時還原 scaling ,可見 avg->internal >> avg->factor
接著看到 ewma_init
函式,目的是初始化 ewma
,其中使用 is_power_of_2
來確保 weight
跟 factor
是 2 的冪,因為 internal
是定點數若是與 2 的冪做乘除法,可以用位元運算。
is_power_of_2
是運用當數值 n
是 2 的冪時,n
跟 n-1
在二進位時,不會有相同的位數會是錯開的,利用這個特點檢測
\[ S_t = \begin{cases} \ Y_0 \qquad\qquad\qquad\qquad ,t=0\\ \ \alpha Y_t +(1-\alpha) \cdot S_{t-1} \ \ ,t>0\\ \end{cases} \]
最後看到 ewma_add
,這裡可想而知會用到前面提到的 \(S_t\) ,先說明 avg->internal
等於 0 的情況,將 avg->internal
設為輸入的 val
對應到 \(Y_0\) ,但別忘了 scaling 所以還要 val << avg->factor
在來看到大於 0 的情況,第一次看到 (((avg->internal << avg->weight) - avg->internal) +(val << avg->factor)) >> avg->weight
時感覺跟 \(\alpha Y_t +(1-\alpha) \cdot S_{t-1}\) 公式有關,但是仔細看時發現正負號會有錯,這邊再看過 ShchangAnderson 同學的筆記後才了解,其實 \(\alpha = \frac{1}{2^{avg->weight}}\),所以程式是先將
\[
\alpha Y_t +(1-\alpha) \cdot S_{t-1}
= [ \alpha Y_t +(1-\alpha) \cdot S_{t-1} ] \times 2^{avg->weight} \times \frac{1}{2^{avg->weight}}
\]
將 \(2^{avg->weight}\) 乘進去原始公式,再簡單地移項方便對證程式碼,可以得到
\[
[ 2^{avg->weight}\cdot S_{t-1} - S_{t-1} + Y_t] \times \frac{1}{2^{avg->weight}}
\]
其中先乘以 \(2^{avg->weight}\) 再乘 \(\frac{1}{2^{avg->weight}}\) 是為了提高精度,程式即是使用上述的公式做計算,並且在每次輸入 val
都會 scaling
5
這個程式碼實做一個函式,用來計算輸入的 32 位元無號整數 \(x\) 的 2 次方指數值向上進位的結果。也就是說,對於傳入的參數 \(x\) ,回傳最小的整數 \(n\),滿足 \(x\leq2^n\) 。
在函式的開始將 x 減 1,這樣可以確保當 x 是 2 的冪時的正確性,因為 \(x\leq2^n\),當等於時不用進位。
接著看到
這裡其實跟測驗 3
是相同的概念,將測驗三的變數調整方便比較,其中 0xFFFF = 2^16
,並且將數字都以 2 的冪表示。
此方法與測驗三的差異在於紀錄 r
的方式是透過 bitwise 操作而非加法運算, r | shift
等效於 r + shift
。
最後 return (r | shift | x > 1) + 1
的部分我們分開看,其中 r | shift
是將前一部分的 r |= shift
合併進來, x > 1
是為了要處理前面 shift = (x > 0x3) << 1
會忽略 x= 0x2
的情況,最後再加上一作為取上界 (ceil) 。
當 x=0
時會因為減一變成 0xFFFFFFFF
,而這不是我們想要的結果。
我想到在 likely / unlikely
中有用到 !!(x)
將整數輸入結果控制在 0
跟 1
, 那麼就可以將 x--
變為 x = x - !!x
,當 x > 0
時減一,而當 x = 0
時則不變。
1
population count 簡稱 popcount 或叫 sideways sum,計算數值的二進位表示中 1 的個數。
v &= (v - 1)
: reset LSB 是將 最低有效位 (LSB) 設為 0 。這讓我想到這跟前面提到的 is_power_of_2
觀念類似,都是判斷 (n & (n - 1)) == 0
只是 is_power_of_2
只判斷一次,而 popcount_naive
是重複判斷來計算 1 的個數。
n = -(~n)
等同 n++
,因為在二補數系統中 \(-n = \sim n+1\) ,經過移項 \(-(\sim n) = n +1\)
透過觀察 Total Hamming Distance 的規則
可以得到一般式:
觀察每個 Number 的第 i 個 bit ,若有 cnt
個 1 ,其餘皆為 0 ,則 Hamming Distance : (cnt) * (numsize - cnt)
我們要走訪所有的 bit ,在每次走訪時計算第 i 個 bit 為 1 的 number 個數(cnt
),其中使用 (nums[j] >> i) & 1
得知目前的number 第 i 個 bit 是否為 1 ,最後透過公式 (cnt) * (numsize - cnt)
計算得到第 i 個 bit 的 Hamming Distance ,最後加總所有位元的 Hamming Distance 即為 nums 中所有整數的 Hamming Distance 。
2
Remainder by Summing digits
如何不使用任何除法就算出某數除以另一個數的餘數呢?若除數符合 \(2^k \pm 1\) ,則可運用以下手法來達成這個目的。
以除數為 3 為例,我們可以觀察到 \(1 \equiv 1 (mod \ \ 3)\) 且 \(2 \equiv -1 (mod \ \ 3)\)。 將後者不斷的乘上前者可得
\[2^k \equiv \begin{cases} 1 (mod \ \ 3), \ \ k \ even\\ -1 (mod \ \ 3), \ \ k \ odd \\ \end{cases}\]
因此若 n 的二進位表示為 \(b_{n-1}b_{n-2}b_{n-3}...b_1b_0\) ,則 \(n = \sum_{i=0}^{n-1} b_i \cdot 2^i\) ,由以上的推論可得 \(n \equiv \sum_{i=0}^{n-1} b_i \cdot (-1)^i \ \ (mod \ \ 3)\) 將奇偶數分開 \(n \equiv \sum_{i=even} b_i - \sum_{i=odd} b_i \ \ (mod \ \ 3)\)
可以利用 population count 來計算位元和,因此可以將上式表示為 n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
,其中 & 0x55555555
是留下 n 的奇數位元, & 0xAAAAAAAA
是留下 n 的偶數位元,並利用以下定理可以進一步化簡。
\[ popcount(x \& \overline{m}) - popcount(x \& m) = popcount(x \oplus m) - popcount(m) \]
於是可以寫為 n = popcount(n ^ 0xAAAAAAAA) - 16
,計算結果的範圍會落在 -16 至 16 之間 ,但為了避免 n 變成負數,我們要將它加上一個足夠大的 3 的倍數,文中的例子是加上 39 讓數值介於 23 到 55 ,我們再做一次與上面相同的操作,只是這次的數值較小,相差 32 所以跟 0x2A = 0b101010
做運算而 popcount(0x2A) = 3
所以是減三,最後區間會介於 -3 <= n <= 2
,還需要對負數加 3 ,所以最後 (n >> 31) & 3
判斷最高位元判斷正負來決定是否加 3 。
另一種變形是利用 lookup table ,其中 n 介於 0 到 32 透過查表直接得知。
然而在 HackerDelight 中提到不使用指令的方法
這段程式碼主要是利用 \(4^k \equiv 1 (mod \ \ 3)\) 簡化求 \(n \ (mod \ \ 3)\)
首先假設 n 前的 16 位元是 \(c\) ,後 16 位元是 \(d\) ,那麼我們就可以將 n 的值寫成 \(n=c\cdot2^{16}+d\) ,再透過 \(4^k \equiv 1 (mod \ \ 3)\) 可以將 \(n (mod \ \ 3)\)表示為
\[ n (mod \ \ 3) = c\cdot2^{16}+d(mod \ \ 3) = c+d(mod \ \ 3) \]
上述的數學式可轉換成 n = (n >> 16) + (n & 0xFFFF)
,以此類推重複 5 次將 n 縮小至最大 0x6
最後一行類似查表,將餘數值存在 0x0924
,透過位元操作取值,其中因為 3 的餘數是 {0,1,2} 可以用 2 個位元即可表示,所以 0x0924
是每隔 2 位元存一個餘數值,並透過 (n << 1)
與 & 3
搭配取得。
tictactoe.c
中使用到的 mod7 方法說明
上述提到利用 \(4^k \equiv 2^{2k}\equiv 1 (mod \ \ 3)\) 簡化求 \(n \ (mod \ \ 3)\) ,同理利用 \(8^k \equiv 2^{3k} \equiv 1 (mod \ \ 7)\) 簡化求 \(n \ (mod \ \ 7)\) ,這裡 (x >> 15)
使用 15 是因為 \(2^{3k}\) ,所以是每隔 3 位元有同餘的結果,而我們要將原 32 位元的數縮小,找 16 附近的 3 的倍數 15 。
在這個方法中不像 HackerDelight 提到的例子,繼續縮小輸入的值後查表求餘數,而是透過 \(n (mod \ 7) \equiv \lfloor\frac{8}{7}n\rfloor (mod \ 8)\) 求 7 的餘數,先將數字乘以 \(\lceil\frac{2^{32}}{7}\rceil\) 也就是 0x2492492
再右移 29 得到剩餘的三位數即為解。
不太明白為何 \(n (mod \ 7) \equiv \lfloor\frac{8}{7}n\rfloor (mod \ 8)\)
tictactoe.c
是一個井字遊戲,他的特別之處在於儲存棋子的方式,以往可能是用一個矩陣儲存井字中每個位置的棋子狀態,這個程式是用棋子可以連成的線來儲存,這個方法的優點在於可以更快速的判斷贏家,也可以更方便取得連線的資訊,
3