contributed by < wu81177
>
版本一當中,可以理解為使用了 binary search 來找平方根,程式先找到 msb
作為初始值,在 msb
和兩倍 msb
之間不斷二分來勘根,而因為整數型會截斷小數部分,因此 a
就會是小於等於 n
二的冪次最大值,之後若 result
加 a
後平方超過 n
就把 a
除以二查找,而由於 a
最小單位是 1 ,對於非平方數只能找到平方根的整數部分。
版本二和一的差別在於 msb
起始值的計算方式,版本一是接使用 math.h
的 log2()
,而版本二是將 n 值不斷除以二直到小於一 ( bitwise 的操作會使 n 最終變 0),過程中用 msb
記數,一樣也能算出 整數部分。
而版本三的部分,根據 Digit-by-digit calculation ,所求 可以寫成 ,其中 為二的 次冪或0,可整理成:
可以發現 肯定是所求,同時也存在某些 滿足 ,因為某些 是 0 ,令:
上一輪底數為 都是已知,同時將 拆成
就可以藉由位元運算來計算 ,迭代求出 即為所求 ,了解原理後,版本三 AAAA 應為 2 ,因為 , BBBB 應為 1 ,因為
為了使用 bitwise 取代 /
和 %
,由於 0.1 寫成二進位會是無窮小數,需要找到一個二進位值近似 0.1 ,目標是在 0~19 範圍內的 temp
乘上它到小數點後第一位以前都正確,而測驗說明中證明了只需要檢查範圍內 temp
最大值是否達標就好,因為近似值乘上 temp
誤差會隨著 temp
等比變化,若是最大值在誤差範圍內,絕對值較小值想必也會在範圍內
假設目標的最大數是 n
, l
則是比 n
還要小的非負整數。假設 ( 是十位數 b 是個位數),且 ( 是十位數, 是個位數),則只要以 算出來的除數在 除以該除數後在精確度內,則 除與該除數仍然會在精確度內,證明如下:
假設 是近似值,
總之在這個例子中只需要檢查 temp
為 19 的情況就好。
0.1 近似值必須滿足一些條件,以有理數 表示,其中 為二的冪,就能用 bitwise 來運算,運算方式如下:
將 轉為二進位 , 寫為 ,可以將 寫為以下通式:
分母 2 的指數就是要右移的位數,以 為例,, ,可以寫為, d 是為了保留右移被捨棄的位元 :
從上述通式能看出若要找 0.1 近似值, 取 四捨五入至整數位誤差最小,但為了滿足小數點後第一位以前正確, 應該取 無條件進位值,這樣誤差才會是正的,而 越大誤差也就越小,如圖,橫軸為 N 縱軸為 0.1近似值*19的誤差:
可以看到當 就能滿足要求,然而 a 實際上可為小數。
接下來看題目實作的函式:
uint32_t x = (in | 1) - (in >> 2)
是將 in
減去 1/4 存為 x
,但不懂為何要 in | 1
,而第二行 q 約為 等於 ,之後的5行讓 q 超級接近 0.8 ,這時我才發現它想讓近似值的分子為 0.8,我在前面的討論沒有意識到分子可為小數。
因為分子是 0.8 ,分母應該要為 8 ,所以依照上面的規則 CCCC 應為 3 。
q & ~0x7
的效果是將後面三位設成0,相當於 ,因此後面那項應該要是 加起來才會是 , DDDD 應為 1 ,我的疑惑是不知為何 (q & ~0x7) + (*div << 1)
兩項不統一為其中一種寫法就好,不知道這樣有什麼好處。
版本一的做法是逐位右移,相當於是一直除以二並且計數,直到等於0就能求出二為底的對數
版本二的作法也一樣,差別是對於很大的數檢查如果大於某數就一次右移很多位,這樣計算的次數比起版本一會少很多,所以 AAAA 應為 ,BBBB 應為 , CCCC 為
版本三使用了 __builtin_clz 來改寫 ,他是 GCC 中的 extention,功能是會計算括號內數左邊0的個數
根據 GCC 手冊
Built-in Function: int __builtin_clz (unsigned int x)
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
所以括號中應該要填 v
,但為了避免 v
為 0 時 __builtin_clz
沒定義的問題, DDDD 應該要填 v|1
這樣最小值就是 1 不是 0
Exponentially Weighted Moving Average EWMA 是一種 moving average ,越新的樣本權重越大,隨著時間增加權重等比下降,但定義上永遠不會變0,所以和 simple moving average 不同,EWMA 是一種 IIR filter,然而得到新的點不用經過太多計算,因為可以用遞迴關係表示:
其中 ,如同其他 ME ,它是個低通濾波器,可以觀察到若 值越大,過去的樣本影響越小,因此留下的高頻成分越多,若 為最大值 1 ,EWME 將不發揮作用
題目實作程式碼如下:
一個 ewma
結構代表一個 EWMA 濾波器, internal
表示要輸出的樣本值, factor
是一個放大因子用來放大輸入樣本降低誤差, weight
則和 有關。
函式部分有 ewma_init
,用來初始化一個 EWMA 結構,它先檢查 weight
和 factor
是不是二的冪,然後將它們取 ,方便後面用平移運算取代乘法,還有將 internal
設為 0 。
再來是 ewma_add
函式:
函式首先判斷 internal
是不是非 0,也就是在判斷是不是初始狀態,如果是就將 val
放大取 前的 factor
倍; 如果不是初始狀態則要進行前面提到的遞迴運算 ,(avg->internal << EEEE) - avg->internal
可以對應到 的部分,整段可以寫成(忽略 factor) ,因此 EEEE 應為 avg->weight ,FFFF 應為 avg->factor。
從這邊也就能看的出來 weight
和 應為倒數關係。
函式一開始的 x--
是為了破壞 x
剛好是二的冪的情況,函式中會達到計算 並且捨棄小數的效果,最後 return 的地方會再加上一達成向上取整的功能,如果沒有 x--
二的冪的輸出就會多一。
再來的原理和測驗版本二類似,只是寫法更緊湊,需要好好思考; 由於 x
是 32 bits 的無號整數型,因此可以不使用迴圈逐步將檢查範圍覆蓋到八個 byte 就好, 依序檢查 x 是否大於 , , ,如果是就除掉,用 or operator 將結果依序更新到 r
,最後在 return 之前已經檢查了 個 bits ,還剩兩個要檢查,於是我代入函式傳入時 x=1~4 推定 GGG 為 1 。
Popcount 用來計算二進位數值中有幾個 1 ,實作版本一如下:
v &= (v - 1)
的效果是將最低位的 1 變成 0 , n = -(~n)
的作用相當於 n++
,因為在二補數系統中負號是先取反再加一,所以 -(~n)
= ~(~n)+1
= n+1
, n
會在迴圈中計數直到 v
為 0 終止,但這樣程式不是常數時間,可能有資安疑慮,因此可以改進成版本二:
這個方式是將每 4 個 bits 平行計算, (v >> 1) & 0x77777777
作用是先除以二,且為了保持每 4bits 的獨立性,將每四位第一位改成 0 。
考慮到計算 n bits popcount 公式如下:
所以 4 bits 就是 ,第 4~9 行用遞迴的方式達成此式。
但這樣還不是最終結果,還需要把 v
每 4bits 分開再加起來,第 11 行會讓第奇數個 nibble 為原本的每兩個相加,第偶數個變 0 ,因為原本每個 nibble 最大是 4 所以不會溢位,然後再乘以 0x01010101 再右移 24 bits 寫成直式就能看出會是所有 nibble 相加的效果。
兩數的 Hamming distance 可以理解為將一個數變成另一個數需要翻轉的位元個數,實作上可以先做 bitwise XOR 再用 popcount 來數:
其中__builtin_popcount
是 GCC 內建函式。
考慮 LeetCode 477. Total Hamming Distance 的解法:
題目要求加總陣列每兩個數的 Hamming distance ,for loop 算的每一組會重複配對一次,所以結果要除以二, AAAA 為 2 。
若 , 則 , 。
若除數為 3 ,則套用上面第二個性質可以推導出
考慮正整數 以二進位表示 : , ,套用上面性質得到 ,求和的部分相當於奇數位和減偶數位和: popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
, 的部分則是重複操作前面直到 n = 0~2 ,類似的方法也可以推廣到任何除數為 的時候。
考慮 tictactoe.c
中 mod 7 的部分:
不同於之前的討論,這裡是用 Hacker's Delight 中 14 頁的方法,可以避免使用 popcount ,對於每個 的除數,都可以從某些地方拆分成前後兩項再加起來而不影響求模結果,在這題由於後面是 & 0x7FFF 是 15 個 1,可以推測 CCCC 是 15。
如果想知道 BBBB 的答案就要先知道如何描述棋盤,它使用一個 32 bits 無號整數來描述棋盤狀態,經過比對 move_masks
可以發現,每四個 bits 描述一種連線,總共有八條剛好 32 bits,第一個 bit 為 0,第 2~4 分別代表那條線上三個位置有沒有值,所以 BBBB 是 0x11111111,因為這樣只要存在一條連線那條線的四個 bit 就會是 0111 + 0001 然後進位,接著 & 1000 看有沒有進位就完成檢查。
AVL tree 為了極致的降低搜尋速度,限制任何左右子樹高度差不能超過一,因此常常需要透過旋轉 (LL, LR, RR, RL)來平衡子樹,過程會消耗許多資源,因此 XTree 被設計出來,它比較不會消耗資源去平衡,但樹的分布較不平衡,平衡能力和搜尋時間介於 red-black tree 和 AVL tree 之間。
接著來看移除節點的函式:
根據註解,如果要刪除的節點有右子節點,則待刪除的節點會先替換為右子樹中遇到的第一個節點,因此 AAAA 為 least
,它在前面被設為 xt_first
,接著會對新接上節點的右子節點做 xt_update
來更新 hint
,並且決定要不要平衡,因此 BBBB 是 xt_right(least)
,同理 CCCC 為 most
, DDDD 為 xt_left(most)
。
如果要刪除的節點是 leaf 的情況可以直接刪除,但就要檢查它的親代,確保平衡狀況,所以 EEEE 是 root
, FFFF 是 parent
。