contributed by < chloe0919
>
測驗中的程式碼透過多項式乘法逐步拆解,例如:
也就是將 、、 相加後再加上兩倍的兩兩元素相乘
現在逐步將 展開 :
所以最後可以獲得以下式子,也就是 即為所求平方根
而且
因為目標是要求出 ,也就是從 累加到 ,所以要試出從 到 所有的 , 還是 , 只要計算出 即求得解,但因為運算成本過高,所以提供了一個想法是利用以下推導將此輪的差值 () 用上一輪的差值 () 和 表示
接下來將 拆解成 和 ,,
藉由位元運算可以從 , 推出下一輪的 ,
(假設 ,,)
綜合上述的推導後,演算法求 從 開始算到 ,所以需要算到 ,即為所求的
將程式碼改為用以上推導使用到的變數做表示,因為首項 所以先用 (31 - __builtin_clz(n)
算出 MSB,再來 & ~1UL
是為了要把將 MSB 扣回最接近的偶數 (),最後再向左移 個 bit 算出 ,而且每次迭代都會將 d >>= 2
對應到 的數學公式
y = c + d
對應到的是執行 後算出 ,而每次迭代也會執行 c >>= 1
將 除以 2
舉例
n | d | c | y | c(after shift) | n>=y | n | c(after add) |
---|---|---|---|---|---|---|---|
36 | 16 | 0 | 16 | 0 | True | 20 | 16 |
20 | 4 | 16 | 20 | 8 | True | 0 | 12 |
0 | 1 | 12 | 13 | 6 | False | - | - |
最後 return 6
根據除法原理:,其中
採用 bitwise operation 來實作除法,但是因為 10 無法直接用 2 的冪項來表示,而且在以下程式碼中因為 b[i]
和 a[i]
都是介於 0-9 的個位數,carry
則為 0 or 1,所以 tmp
的範圍介於 19
~0
因此為了得到精確的 ,要先找出除數 () 的範圍,假設需要計算的被除數 () 為 ( 為十位數 為個位數),且 ( 為十位數 為個位數) 為比 小的非負整數,我們可以得到此不等式:
因為 ,所以一定在精確度內
再來需要證明左邊的式子成立:
一樣先假設
因為需要證明的是 最大只有到 ,所以假設 代入上述式子中也會成立:
接下來分別去證明左右兩邊的假設皆成立:
最後可以證明出剛開始的假設是成立的,而且只需要針對 19
討論,代表 在此範圍間得到的 tmp / x
直到小數點後第一位都是精確的:
因為10 無法直接用 2 的冪項來表示,所以需要找到接近 的數字 來表示,此測驗中挑選 ,,計算出來 有落在上述範圍內
接下來透過 bitwise operation 進行計算,首先執行 tmp >> 3) + (tmp >> 1) + tmp
計算出 ,之後再 << 3
算出 ,因為直行右移的動作,所以需要將後面被移掉的補回去 (d0
、d1
、d2
),最後再往右 7 bit 算出 ,也就是 的結果
而餘數的計算方式為 ,也就是先 (q << 2) + q)
計算出 後再左移 1 bit 得到
一開始 (in | 1) - (in >> 2)
是再計算 ,而且如果 in
為偶數需要加一,但仍不清楚為何需要此操作,之後 q = (x >> 4) + x
是要計算 ,為了使算出來的 更接近 接下來連續做了 4 次逼近,最後得到一個非常接近 的值,再將 除以 8 後得到
此外使用 q & ~0x7
保留除了最後三個 bits 以外的位元,也就是相當於執行 *div << 3
,例如 ,,而 q & ~0x7
為 ,相當於 *div << 3
= 。(q & ~0x7) + (*div << 1)
就是在算出 *div * 10
,*mod
會等於 的結果
ilog2
i
每次迭代都會往右移 1 bit,直到找到最高位元的位置即結束,最後計算出來的 log
也就是 i
以 2 為底的對數
size_t ilog2
此操作是從 開始一直逼近到 ,假設有大於 的情況就進行累加後再右移
__builtin_clz(v)
可以計算出 v 的最高位左邊連續 0 的個數
參考 stevendd543
Exponentially Weighted Moving Average
是一種取平均的統計手法,採取 Exponential 主要是為了使越舊的歷史資料的權重更低,以下為計算公式:
用以下函式算出 n
是否為 2 的冪,實際操作是去看兩種情況皆滿足時則 return True :
n
不為 0n
和 n-1
做 and 運算時為 0其中,只有當 n
為 2 的冪時,n-1
才會是除了最高位為 0 其他皆 1 的 情況,所以 n & (n - 1)
就會是 0,例如 ,,n & (n - 1)
為 0
struct ewma
ewma_init
因為 weight
和 factor
都要是 2 的冪,所以要先用 is_power_of_2
判斷,接下來要將 weight
和 factor
都分別使用 ilog2
,也就是只儲存指數的部分
ewma_add
接下來 ewma_add
主要的操作是假設 ,並且利用 將公式改寫成 :
avg->internal
對應到的公式是 (還沒計算出 ,所以目前取得的是 ),avg->weight
對應到的是 的指數部分,這是為了進行 bitwise operation
操作對應如下,特別要注意的是 val
需要先進行定點數的轉換,也就是需要左移 avg->factor
(scaling factor) 個 bit:
(avg->internal << avg->weight) | avg->internal | val << avg->factor |
而最後將整個公式 >> avg->weight
,就能算出
此測驗和測驗三不同的是這邊已知輸入必為 uint32_t
,所以不需要和測驗三一樣採用 while 去做判斷,其中 r = (x > 0xFFFF) << 4
會先去計算出 x
大於 時需要移動的 bit 指數的值,若 x
大於 則 (x > 0xFFFF)
會為 1 並且將 1 往右移 4 bits,也就是乘上 ,最後將 x
往右 16() bits,其中 r |= shift
對應到的是 result += 8
的操作
最後 return 會判斷 x>1
是因為目標要計算的是 log 向上取整數的值,所以要避免忽略掉 x==0x3
和 x==0x2
的情況
而這邊會先將 x--
再去做判斷,是因為如果不加的話結果會多一
popcount 用來計算數值的二進位表示中位元是 1 的個數,假設 x = ,可以得知 popcount 的數學式為 :
假設每次都將 x 往右移 1 bit :
相減後,可以整理出以下數學式 :
又因為 為 1,所以可以用 去計算 popcount
popcount_branchless 原理 :
為了避免檢查 32 次,popcount_branchless
的實作會以 4 個位元為單位並去計算
,最後使用 v = (v + (v >> 4)) & 0x0F0F0F0F
將每 4 位元中的 set bit 的個數全部加到 byte 中
Hamming distance :
A ^ B
會計算出 A 和 B 二進位的每個位元的差,之後再以 __builtin_popcount(A ^ B)
就能計算出 A ^ B
總共的 set bit 個數
針對 Total Hamming Distance,要計算出 nums
中所有的 Hamming Distance,所以要以兩個迴圈去計算出整個陣列的 Hamming Distance,最後 return total >> 1
是因為多計算了一次
利用 mask
表示每次需要取出的 bit 的遮罩,並且在 for 迴圈中計算出 nums
中元素該 bit 所有為 1 的個數,並且 total+=(one*(numsSize-one))
是去計算為 1 和為 0 的個數相乘後的結果再累加
分析時間:
上述方法會使用到兩個 for 迴圈對 nums
中任兩個元素作計算,還有每次都要計算 nums[i] ^ nums[j]
也就是需要 的位元長度,因此時間複雜度超過
採用改進方法只需要時間複雜度
(其中 為 32 bits)
Remainder by Summing digits
當 n 以二進位進行表示時,可以寫成
並且可以利用以下推導將 n 改寫
改寫後的 n 為以下的表達式,也就是將 n 的偶數位元相加 - 奇數位元相加 ,對應程式碼也就是 n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
。
接下來為了將程式碼進一步簡化,用以下的定理進行推導(其中 代表 ,而 為 , 為 ) :
首先可以將表達式中的奇數位元相加改寫成以下,也就是先將 的奇數位元先視為全 1 後,再扣掉 實際奇數位元為 0 的總數(以 代表),最後再整理式子得到 :
可以發現其中 也就是 x 的偶數位元為 1 的總數 + x 奇數位元為 0 的總數,並且觀察 的真值表可以發現當 為 0 時 而 為 1 時
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
最後可以將程式碼改寫成 n = popcount(n ^ 0xAAAAAAAA) - 16
將上面推導出來的數學式運用後會發現 n = popcount(n ^ 0xAAAAAAAA) - 16
計算出來的結果範圍在 之間,在此範例中為了避免 算出來的結果為負數,所以我們要將它加上一個足夠大的 3 的倍數(例如 39),現在 的範圍就會落在 之間,再經過一個 後(這次只需要 6 bit,因為 55 只需要 6 bit 就能表示了), 的範圍為 ,最後 -3
是將範圍限定在 。
但因為 ,所以最後我們想將範圍限定在 0 到 2 之間,當 經過 (n >> 31) & 3
後會為 3, 則是會得到 0,所以最後就能達成
算出 k = popcount(n ^ 0xAAAAAAAA)
後會得到 的範圍 : ,也就是對應到 table 中的 33 個元素,例如 時
利用 move_masks
定義棋盤中九個位置各自的連線狀態,視每四個 bit 為一組,分別表達該位置在連線中的順序,共 8 組則表達 8 條連線。
其中,32 bit :
以 0x40040040
為例,0x40040040 = 0100 0000 0000 0100 0000 0000 0100 0000 表示和棋盤中第一個位置有關的三種連線方式,包括第一條、第四條、第七條,其中為 0100 是因為 a 位於個個連線中的順序一:
第一條 :
1 | 2 | 3 | |
---|---|---|---|
1 | a | b | c |
2 | d | e | f |
3 | g | h | i |
第四條 :
1 | 2 | 3 | |
---|---|---|---|
1 | a | b | c |
2 | d | e | f |
3 | g | h | i |
第七條 :
1 | 2 | 3 | |
---|---|---|---|
1 | a | b | c |
2 | d | e | f |
3 | g | h | i |
之後會以 board |= move_masks[move];
取得了選定走法 move 對應的連線狀態,然後更新玩家的棋盤狀態,由此可見當任四個 bit 出現 0111 時,代表任一條連線的三個位置都有棋子,也就是玩家勝利,所以利用 player_board + 0x11111111
將任四個 bit 中的 0111 轉成 1000 判斷是否玩家勝利