首先先使用 log2 取得最高位元,然後從最高位元開始檢查該位元是否有值,判斷依據使用 if ((result + a) * (result + a) <= N)
,若小於輸入代表還沒超過 N 的值,所以要加一,從高位元做到低位元,慢慢收斂。
版本一和版本二概念可以說是一模一樣,差別是在最高位元的取得,版本一使用 log2 而版本二則使用 while loop 達成。
是我們想要求的平方根,
透過觀察上面的矩陣,我們可以把 的式子整理成:
可以歸納出
現在我們得到一個關鍵的式子 ,這說明了我們可以透過迭帶的方式求出 。方法是從高位到低位檢查是否 ,若成立則代表該位元可以被設為 ,若不成立則代表該位元為 。
講解完理論後該如何實作,上面提到檢查是否 ,但算平方的成本太高,因此文章提出使用上一輪的資訊來判斷,我們定義一個新的參數 ,然後將 表示為與上一輪有關,這時會有多一項參數 :
將 定義成下面式子:
如果該位元為 ,則
如果該位元為 ,則
令
可以推出下一位元
如果該位元為 ,則
如果該位元為 ,則
所以說我們就可以透過 ,最後一個位元為 ,因此 即可算出所求。
對比程式碼
與前兩個版本相同,都是從最高位開始計算,因此可以看到初始值使用 __builtin_clz(x)
來取得最高位,然後我們需要對 x = 0
的情況做處理。然後 m
即為上述數學推導中的 ,還記得不管該位元是否為 ,下一輪的 也就是 ,無論如何都會除以 ,因此利用 AAAA = 2
,進行迭代。接下來 b
為數學推導的 ,所以 z
為 。因為
若該位為 ,
若該位為 ,
所以我們將 提出去,z >>= 1
,所以 BBBB = 1
。然後再進行比較 if (x >= b)
,若成立的話就將 也就是 m
加到 z
,經過反覆迭代即可以求出 所求。
為了可以不要使用 /
%
來做除法,可以使用近似的方式求解,假設我們的除數為 ,那麼 則可以表示成 倒數形式,如果 是 2 的冪,那麼除法實作則會相當簡單,但並不是所有的數字的倒數都可以使用 2 進位精準表示,如 使用二進位會表示成 0.010101010101010...
的無理數,因此如何近似除數就成為這個測驗的主要技巧。
接下來我們來探討除數為 10 的情況, 使用二進位可以表示成 0.000110001...
,表示的不精準,因此我們也需要透過近似方式求解,文章中除數使用 ,這個除數是根據精確度而決定。
假設 l
是一個比 n
還小的非負整數,只要以 算出來的除數在 除以該除術後在精度內,則 除以該除數能然會在精度內,近似的這個手法就是根據這一個猜想。因此我們可以根據最大情況 19
來考慮:
我們就可以使用 來配出一個可以用的數字 。
決定好除數為 後,也就是,我們就可以使用 shift 來拼湊出這一個數字,首先將 ,因此我們可以得到 ,如下。
但因為我們會先將 tmp
右移,造成最低 3 位會被捨棄,因此需要先將最低 3 位存起來,然後再合併起來。
這一個步驟我想了很久,為甚麼不先將 tmp * 13
再除 8,如下。
我認為是因為 overflow 的問題,理論上來說要先左移也可以,不過被保存的數字也需要做相應的調整,需要保存最高的 3 位。
最後考慮 128,即可以得到商數和餘數。
接下來看到包裝的函式,這個函式與我們所討論的看起來落差相當大。
x = (in | 1) - (in >> 2);
主要是將x = 0.75 * in
。而我們可以透過下方的程式碼讓 q
趨近於 0.8,如此一來我們可以用到上面的概念,將 表示成 ,其中 趨近於 0.8。
下方程式碼則是在使 q 趨近於 0.8。首先經過 q = (x >> 4) + x;
得到 ,然後會經過 4 次的 q = (q >> 8) + x;
,得到 ,最後經過四次會讓 q
趨近於 0.8 * in
。所以說要求商的話需要再除以 8,也就是左移 3 位,因此 CCCC = 3
。
最後要算餘數,,比較 *mod = in - ((q & ~0x7) + (*div << DDDD));
其中 ((q & ~0x7))
的意思為 ,因此我們可以透過對 div
左移一位得到 ,所以DDDD = 1
。
比較這兩種方法,他們其實都是在處理除數的近似,只是數字不同罷了。
看到第一種 ilog2 的實作,採用了非常簡單的概念,檢查 i
裡面有沒有值,如果有則 log++
沒有的話就 shift 一位,會一直做到最高位被移走。可以注意到 log
的初始值為 -1
,這是因為需要考慮到 的情況,也就是說不能把 算進去。這個實作方法相當簡單,同時精度也不高,只能處理 log2 的整數部分。
接下來看到版本二的實作,觀察下方程式碼我們可以發現,基本概念與版本一相同,不同的地方是將 , , , 特別處理,為甚麼要這麼做呢,就是因為如此一來,可以直接檢查很多位,不用像版本一這樣需要把所有的位數都檢查一遍。因此根據 ,一次檢查 16 位,將 i shift 16 位,並將結果 +16,AAAA = 65536
,依此類推,BBBB = 256
,CCCC = 16
。
這個版本可以有效加速運算,我們實際舉一個例子,假設 ilog2(257)
,因此會在 while (i >= 256)
這個迴圈執行一次然後就結束,但版本一要執行 9 次,因為 257 的 binary 有 9 位。
版本三的程式相當簡潔,使用了 GNU extension,__builtin_clz
是一個內建函式,用來計算一個整數在二進制表示中,從最高位(最左邊)開始的零位的個數,前兩個版本都是從最低位開始算,而這個正好相反。
可以來推理 DDDD
的答案,值觀上來想,v
應該就是答案了,但是如果 v
是 0 呢? __builtin_clz()
若是參數為 0 則結果未定義,所以我們要讓 v
無論如何都會有 1 位,如此一來則不會有未定義的情況發生,因此 v|1
這個運算僅僅是在加強函式的強健性。
這一測驗主要在計算指數加權移動平均 EMWA,使經過時間越久的歷史資料的權重也會越低,數學的定義為:
觀察這一個式子可以發現 就是所謂的權重, 越高代表先前所影響越少,因為會被 的次方稀釋掉,我們可以透過調整這一個參數以控制以往資訊對 EMWA 的重要性,其中 為初始的資料, 為當前時刻的資料, 為前一刻所計算出的平均數。
接下來看到程式碼的部分,首先定義一個 struct ewma
,裡面有 internal
代表計算出的平均數,factor
為一個縮放因子,我們要將 value 放進這一個結構裡都要乘上 factor
,而 factor
為 2 的冪。weight
為權重,一樣是 2 的冪。
以下程式碼為初始化的部分,可以觀察到存進去 weight
factor
都是以 log2
存進去,而 interval
一開始會被設為 0。
接下來這個程式為主要計算平均值的地方,首先我們要先判斷 avg->internal
有沒有東西,如果沒有的話我們直接把 val
加進去 internal
,但要注意的是,要將 val
乘上 factor
,對應到數學式的 ,因為 avg->factor
已經取過對數,因此我們可以用左移來實現。
如果 avg->interval
已經有值的話,那我們需要使用 來做運算,以數學表示來說, 是一個介於 0 到 1 的值,但我們的 struct 裡面定義的 weight
為 unsigned long
,因此需要對數學式做一些操作。
根據上式,可以對照程式碼,EEEE = avg->weight
,DDDD = avg->factor
。
測驗五結合了 ceil 和 log2,可以看到以下程式碼與測驗三的版本二類似。差在測驗五會先 x--
,進而造成判斷的數值在測驗三為0d65536
,而測驗五為 0d65536 - 0d1 = 0xFFFF
。
測驗三中的 result += 16
在測驗五中為 r |= shift
,而在最後一行return (r | shift | x > GGG) + 1;
與測驗三不一樣的地方在於 x > GGG
,這一部分是為了要無條件進位,GGG = 1
,如此一來只要大於等於 2 的數字在第一個位元就一定是 1。
測驗三 - 版本二
popcount 是用來計算二進位表示中有多少位元是 1。透過 v &= (v - 1)
,可以將最低位元的 1 清掉並記錄,舉例來說 0b10100 - 0b1 = 0b10011
,會讓最低位元為 1 以下(含)的位元取反,然後再做且運算即可以達到效果。而 n = -(~n)
可以用二補數來解釋 為二補數的定義,因此這一段程式碼要表達的其實與 n++
相同。
然而因為上述算法的時間複雜度取決於 set bit 的個數,因此可以改寫為常數時間時間複雜度的實作。
以下程式碼的運算基於以下數學運算:
假設 , 這四個位元可以推導成:
程式碼實作上以 4 bit 為單位,以上運算對應到
透過將 v
右移 1 位並減掉達成 ,重複三次可以得到如此一來我們便可以得到 4 bit 的 popcount。
接下來這一步驟,目的是要獲取所有位元也就是 8 個 4 bit 的 popcount,首先 (v + (v >> 4))
透過位移得到前後兩組 4 bit 的總和,再透過 0x0F0F0F0F
當作遮罩,濾掉重複部分。
接下來 v *= 0x01010101
利用乘法值式的特性,會再中間項出現所有 nibble 的累加,而這個位置剛好會在 這個位置,因此最後將 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
。
這一個測驗的目標是不使用任何除法就計算出餘數,相比第三周的題目,需要先算出商數,再使用被除數減掉除數成以商數,這種方法可以直接計算出餘數。根據Hacker's Delight 提出的定理。
因此我們以 為例可以看到 ,我們可以透過對 和 取餘數表示成一個二進位的式子。
並套用 Hacker's Delight 提出的定理。
設為輸入,將 表示為 ,則 ,如此一來便可以透過對奇數位和偶數位的位元總和計算出 的模數。
看到以下程式碼 popcount(n ^ 0xAAAAAAAA) + 23
,這一步是從另外一個定理推導出來的,
n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
,透過這一個式子我們可以比較奇數位偶數位的模數,因為若是奇數位有值,他的模數會是 -1,偶數則是 1,透過相加即可以計算出所有的模數,所以我們要減掉奇數位的 popcount
。
popcount(n ^ 0xAAAAAAAA) - 16 + 39
,這邊為了不要讓算出來的數落在負數,所以我們可以加上一個 3 的倍數,在這個程式選擇了 39,理論上可以選擇任何 3 的倍數,但後續的運算也需要做相對調整,此時 ,所以我們要針對 n 在做一次運算,因為這時 n
的最大值為 55,也就是說不會超過 6 bit,所以使用 n = popcount(n^ 0x2A) - 3
來計算模數,這時 。
return n + ((n >> 31) & 3);
這時 ,所以我們要針對負數做處理,方法是將 n >> 31
也就是判斷 sign bit,注意這邊是使用算數位移而非邏輯位移,所以若是負數的話會造成全 1,此時可以與 3 座且運算,也就是若小於 0 則加 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 開始。
Hacker's Delight 還提出了一種不用使用 popcount 的計算模數方法,其中關鍵在於,可以將 n 拆成兩部分,並將左邊的部分右移並與右邊相加不會對模數計算造成影響,所以要怎麼拆就是一大關鍵。取 7 的模數會有此關係式: ,,所以可以看出只要是 shift 3 的倍數就不會影響到 mod 7 運算。值得注意的是 mod 不同的基數會有不同的限制。
這邊利用了 ,0x24924924
為 。最後一步則是將原本的 0~7 mapping 為 0~6。
透過結合上述兩種方法,可以判斷 CCCC = 15
,因為 0x7FFF
,將 x
拆成兩部分。
觀察 move_masks
,我們可以發現紀錄井字遊戲的位置時不單單只是記錄九宮格的位置,而是使用八條可能的路徑紀錄,三條橫線,三條直線,兩條斜線,也就是說一個位置的改變會改變到多條路徑。move_masks
以 4 bit 表示一條路徑,若為 0b0100
,則代表該路徑上的第一個位置有值,0b0010
代表該路徑上的第二個位置有值,以此類推。而從最高位到最低位依序代表的路徑為第一條橫線,第二條橫線,第三條橫線,第一條直線,第二條直線,第三條直線,第一條斜線,第二條斜線。所以我們以中間的位置為例,該點影響了第二條直線,第二條橫線和兩條斜線,而在三條路徑上剛好都是第二個位置有值,此點可表達為 0x02002022
。
顯而易見的,勝利的條件即為任意一條路徑三個位置都為同一方,也就是 0b0111
,因此只要有任意一條 +0x1
然後與 0x8
且運算有值,
即為獲勝,所以 BBBB = 0x11111111
。
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
是否達到平衡。
來看看 xt_update
,勇於更新樹中節點的平衡狀態和 hint
值,首先透過 xt_balance
計算平衡因子,並獲取節點 n
的前一個 hint
和父節點 p
,若平衡因子小於 -1 代表樹向右傾斜,要將樹進行右旋轉,若大於 1 代表向左傾斜,要將樹進行左旋轉,然後更新 n
的 hint
為右子樹中的最大值。最後如果節點的提示值為 0 或者 hint
發生了變化,則遞迴地使用 xt_update
,更新父節點 p 的平衡狀態和 hint
。