contributed by < ssheep773
>
1
目的: 計算 N
的整數平方根
方法一 :
通過逐步逼近來計算 N
的平方根。它首先找到 N
的最大位元位置 msb
,並將最大位元位置對應的數值儲存在 a
。然後逐步減小 a
的值,同時檢查 是否小於或等於 N
。如果是的話,就將 result
加上 a
,以逼近 N
的平方根。這樣的操作會持續進行,直到 a
的值為 0。
方法二 :
利用 Digit-by-digit calculation 提及常見的直式開方法 的變形實作開平方根。
若要求 的平方根,可以假設 ,那麼 即為我們要求的平方根數值,接著,將 拆成
經過整理
這裡假設 ,那麼 即為我們要求的 , 可在分解為
可以得到一般式
我們的目的是要試出所有 ,。從 一路試到 ,試驗 是否成立來求得
然而如果每次都試驗 的運算成本會太高,一種想法是透過上一輪 的差值推估 相關的證明如下:
因為前面 的假設 可以整理為
並可推展出一般式
經過移項
假設
以及假設 , 即為每次判斷 的條件,可以推廣 為前一次的差值
我們就可以得到,從上一輪的差值 減去 得到
將原本 的問題轉換為 即使用到上一輪的差值來計算。
進一步調整,將 拆成 和
以此推出下一輪
這裡說明初始與中止條件,因為是判斷 ,所以我們需要準備兩者的初始條件,在程式進行時,會由 往下試
因為 恆大於 所以終止條件可以設為 d != 0
,又 故求出 則等於求出 的平方根。
最後看到程式的部分
首先排除 x
為負數及 0 或 1 的情況
變數 m
對應到 ,其中因為 m = ,所以 m 不可能為奇數,因此要 & ~1UL
,同時再前一個步驟也先處理 x = 1
的情況
因為 所以每次迴圈 m
右移 2 。
x
對應到 呼應初始條件 ,變數 b
對應到 ,z
對應到 同時也說明
而計算 ,不管 是否成立 都需要除以二,這裡以 z >>= 1
實作
且若 則減去 得 。
前面提到我們將原本 的問題轉換為 同時也等價 ,若成立則
ffs
是找最低為 1 的位元,所以透過迴圈的方式由低往高位元找,到最高的有效位元的位置為止,並且還需要將其減一才會與 (31 - __builtin_clz(x)
相等,若沒有減一,雖對於結果沒有影響,但是會多花費一輪的運算成本。
2
針對正整數在相鄰敘述進行 mod 10
和 div 10
操作,減少運算成本
原始的方法:
可利用除法原理將 mod 10 和 div 10 合併。根據除法原理:
其中 被除數, 除數, 商, 餘數
將程式碼改寫為
這邊參考《Hacker's Delight》,採用 bitwise operation 來實作 10 的除法,但是因為 10 有 5 這個因數,無法完全用 2 的冪項來表示,因此結果會是不精確的。
我們的目標是,得到 tmp / 10 且直到小數點後第一位都是精確的。
為此,提出一個猜想,假設我目標的最大數是 ,而 是小於 的非負整數,則只要以 算出來的除數在 除以該除數後在精確度內,則 除與該除數仍然會在精確度內,證明如下:
假設 ( 是十位數 是個位數), ( 是十位數 是個位數)。
首先看到 ,因為我們是要確保小數點後第一位是精確的,所以 可以表示如下 :
透過上面的轉換,我們將問題轉換為確保 的範圍,來達到 在精度以內,下面是對 範圍的探討
的右不等式 ,因此一定在精確度內。
的左不等式 無法直接證明,透過代入 來證明,的不等式如下:
,透過將 代入可表示為
左不等式:
最後推得 左式顯然成立。
右不等式:
其中 ,因為 所以右不等式也成立
於是前述猜想也成立,接下來只需要針對 來做討論即可,而因為 int tmp = (b[i] - '0') + (a[i] - '0') + carry
所以 tmp
最大是 9+9+1=19, 也即為 19 ,下面針對 19
做討論即可
上述不等式可得知除數介於 至 之間皆可程式中達到相同效果,即除數直到小數點後第一位都是精確的,而我們的商會比實際的還要大有正偏差。
我們透過 bitwise operation 找到介於 至 之間的除數,除數的算式必定是 ,其中 是非負整數, 是正整數,而計算 的商可以寫成 。
因此只需要透過查看 在配對適合的 即可
這裡 即為可用的除數解
接著看到實際的 bitwise operation
q
是要求的商 ,在這裡是 ,我們首先要得到 13 ,透過 (tmp >> 3) + (tmp >> 1) + tmp) << 3
得到 再 << 3
得到 ,最後再除以 128 即得到我們要的商
(((q << 2) + q) << 1)
這部分是將 q * 10
透過 (q*4 + q) * 2
達到
而因為原有的 tmp
在右移後,會有部分位元被捨棄,因此要另行處理。
然而我覺得這步驟可以省略,因為後續的操作會在將 tmp >> 7
等於是補回來的位元馬上又被捨棄,而我思考若是在產生 13 而右移的位數大於 的 時,則需要處理,而目前分別是 3 跟 7 ,所以可以省略不處理。
在來看到包裝的函式
uint32_t x = (in | 1) - (in >> 2)
將
再透過 uint32_t q = (x >> 4) + x
等於是 ,而其中
後續再用 q = (q >> 8) + x
對 q
做逼近增加精度更靠近
*div = (q >> 3)
是計算商,而我們前面求的 q
是 ,所以需要再除以 8 也就是 q >> 3
。
*mod = in - ((q & ~0x7) + (*div << 1))
是計算餘數,其中的 q & ~0x7
等於是 *div << 3
,那麼就程式碼可以轉換成 *mod = in - (*(div << 3) + (*div << 1))
後半部的 *(div << 3) + (*div << 1)
等於是 ,根據餘數定理
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 的數學定義:
表示歷史資料加權降低的程度,介在 0 ~ 1 之間,越高的 會使歷史資料減少的越快
表示在時間 時的資料點
表示在時間 時計算出的 EWMA
首先看到 ewma 結構,internal
儲存我們的 EWMA 值也就是 ,值得注意的是它的型態是 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
在二進位時,不會有相同的位數會是錯開的,利用這個特點檢測
最後看到 ewma_add
,這裡可想而知會用到前面提到的 ,先說明 avg->internal
等於 0 的情況,將 avg->internal
設為輸入的 val
對應到 ,但別忘了 scaling 所以還要 val << avg->factor
在來看到大於 0 的情況,第一次看到 (((avg->internal << avg->weight) - avg->internal) +(val << avg->factor)) >> avg->weight
時感覺跟 公式有關,但是仔細看時發現正負號會有錯,這邊再看過 ShchangAnderson 同學的筆記後才了解,其實 ,所以程式是先將
將 乘進去原始公式,再簡單地移項方便對證程式碼,可以得到
其中先乘以 再乘 是為了提高精度,程式即是使用上述的公式做計算,並且在每次輸入 val
都會 scaling
5
這個程式碼實做一個函式,用來計算輸入的 32 位元無號整數 的 2 次方指數值向上進位的結果。也就是說,對於傳入的參數 ,回傳最小的整數 ,滿足 。
在函式的開始將 x 減 1,這樣可以確保當 x 是 2 的冪時的正確性,因為 ,當等於時不用進位。
接著看到
這裡其實跟測驗 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++
,因為在二補數系統中 ,經過移項
透過觀察 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
如何不使用任何除法就算出某數除以另一個數的餘數呢?若除數符合 ,則可運用以下手法來達成這個目的。
以除數為 3 為例,我們可以觀察到 且 。 將後者不斷的乘上前者可得
因此若 n 的二進位表示為 ,則 ,由以上的推論可得 將奇偶數分開
可以利用 population count 來計算位元和,因此可以將上式表示為 n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
,其中 & 0x55555555
是留下 n 的奇數位元, & 0xAAAAAAAA
是留下 n 的偶數位元,並利用以下定理可以進一步化簡。
於是可以寫為 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 中提到不使用指令的方法
這段程式碼主要是利用 簡化求
首先假設 n 前的 16 位元是 ,後 16 位元是 ,那麼我們就可以將 n 的值寫成 ,再透過 可以將 表示為
上述的數學式可轉換成 n = (n >> 16) + (n & 0xFFFF)
,以此類推重複 5 次將 n 縮小至最大 0x6
最後一行類似查表,將餘數值存在 0x0924
,透過位元操作取值,其中因為 3 的餘數是 {0,1,2} 可以用 2 個位元即可表示,所以 0x0924
是每隔 2 位元存一個餘數值,並透過 (n << 1)
與 & 3
搭配取得。
tictactoe.c
中使用到的 mod7 方法說明
上述提到利用 簡化求 ,同理利用 簡化求 ,這裡 (x >> 15)
使用 15 是因為 ,所以是每隔 3 位元有同餘的結果,而我們要將原 32 位元的數縮小,找 16 附近的 3 的倍數 15 。
在這個方法中不像 HackerDelight 提到的例子,繼續縮小輸入的值後查表求餘數,而是透過 求 7 的餘數,先將數字乘以 也就是 0x2492492
再右移 29 得到剩餘的三位數即為解。
不太明白為何
tictactoe.c
是一個井字遊戲,他的特別之處在於儲存棋子的方式,以往可能是用一個矩陣儲存井字中每個位置的棋子狀態,這個程式是用棋子可以連成的線來儲存,這個方法的優點在於可以更快速的判斷贏家,也可以更方便取得連線的資訊,
3