contributed by < sammer1107
>
進階電腦系統理論與實作
10000000
作 AND。對應到 16 進位的 80
(d) 0x8080808080808080
'0'
, '1'
, '2'
, …, '9'
對應到 0x30
, 0x31
, 0x32
, … 0x39
'a'
, 'b'
, 'c'
, …, 'f'
(小寫) 對應到 0x61
, 0x62
, 0x63
, …, 0x66
'A'
, 'B'
, 'C'
, …, 'F'
對應到 0x41
, 0x42
, 0x43
, …, 0x46
首先我根據名字 letter
推測,第 3 行應該是要判斷輸入是否屬於字母 a~f。我注意到數字的16進位表示法中開頭都為 3 ,而小寫字母則是 6 ,大寫為 4。
將 3、4、6 的二進位寫出來,如下:
數字 | 二進位表示 |
---|---|
3 | 0011 |
4 | 0100 |
6 | 0110 |
可以看出來第三個 bit 可用來區分是否為字母,故 MASK = 0b01000000 = 0x40 |
由於不清楚第 4 行的作用,我先看第五行。
& 0xf
是只取最小的四個 bit 的意思,作用是確保回傳的值為 0~15。'1' = 0x31
, 0x31 & f = 0x01 = 1
回到第 4 行,我現在的任務就是當 letter
不為 0,我要用他湊出能把字母轉成數字的方法。我繼續觀察目前有的資訊:
letter
為 0b01000000
字母 | lower 4 bit (Decimal) |
對應數值 |
---|---|---|
a | 1 | 10 |
b | 2 | 11 |
c | 3 | 12 |
d | 4 | 13 |
e | 5 | 14 |
f | 6 | 15 |
a 對應到前四個 bit 為 1,b 對應到 2,後面的規律一樣。所以要讓字母對應到他們的數值,其實就只要 +9 就好了,所以 shift 應該要是 9。 |
00001001
,故可以用 letter 向右位移 3 和位移 6 來合成,所以選擇 AAA=3, BBB=6由於我根本看不懂題目說明,所以決定先追查到 jemalloc 的原始碼,看看有何線索。以下為 jemalloc/src/div.c
的註解所說(數學這裡用英文比較順):
Suppose for some integer , we have
where for some integer and . Because , if we select such that , we'll have and
這告訴我們只要今天 n 是被 d 整除且我們選擇的 k 夠大,我們就可以用 當作 magic number 。要做除法時就做 即可,對應到 c 語言中,即是乘法與位移兩個動作。
但要注意的是,他假設 是被 整除。但我們的情況不同,因為我們要算餘數, 當然不一定會被整除,因此我們需要修改一下他的推導。
According to division rule, we can say that for some and . Then, we have
The first and second equality comes from the same reasoning in the jemalloc case. The third equality comes from the fact that . The final equality stands because is an integer.
The final line equals to only if .
我們可以從 shift 64 的動作看出來 ,再經過計算我們得到 和 。 而 應該要等於 ,等式成立因為 。因此答案選擇 0xFFFFFFFFFFFFFFFF
。
帶進上面的推導,我們的方法要成立必須要滿足 。由於 在這裡最大是 2,而 受型別的限制所以 ,我們可以看出來 ,所以演算法成立
我們上面討論了演算法為何成立,那有沒有不成立的情況呢?
我們令 一樣是 ,則若我們選擇 ,且這個 對應到 ,則
演算法就會崩潰。
為了驗證我的理論,我修改了題目程式:
quickmod
使用的 type 為 uint64_t
,來因應我要測試的 (在 上下)main
,我從 開始測試到 ans
為對應初始值的正解,可以算出之後的正解。程式輸出
n - quotient * D
原本的 再多減了 變成 ,所以在無號數才會變成很大的數字。從這裡我們看到,隨著 以及 的範圍不同,選擇適當的 是很重要的,否則演算法不一定會成立。
這題我嘗試沿用上面的推導但不得其門而入,所以我重新觀察解答為何成立:
什麼樣的整數乘以 會小於 呢? 答案是不存在這樣的整數,要回答這題,必須把 overflow 考慮進來。由於 M
的 type 為 uint64_t
,根據 Integer Promotion 的規則,所有的運算與比較會在 uint64_t
底下完成。這對應到數學中的 的操作。以下分成兩個情況討論:
我好像找不到方法直接推出解答,但根據上面證明 (c) M - 1
的確是正確答案。
這裡我也用了小測試程式測試我的推導,但沒有測試所有數字:
PACKED_BYTE
的作用。可以看到 b 先被 &0xff
,也就是取最低的一個 byte。再來 * 0x0101010101010101u
的作用相當於是將此 byte 複製 8 次。0x80
來做 mask 。mask
的作用應該是在字元是 'A'-'Z'
的情況下,對應的 byte 的第6個 bit 應該要為 1,其他皆為0,如此來做到轉小寫。(A ^ Z) & PACKED_BYTE(VV4)
的作用是在屬於 'A'-'Z'
的 byte 的第 8 個位元產生一個 1 。合理判斷 VV4 為 0x80
,前提是只要當字母屬於 'A'-'Z'
的情況下, A
與 Z
的第 8 個 bit 不同即可。原始區間 | 經 +128-'A' 轉換後 |
經 +128-'Z'-1 轉換後 |
---|---|---|
<'A' |
<128 |
<128 |
>'Z' |
>128 |
>128 |
'A'-'Z' |
>128 |
<128 |
只有 'A'-'Z' 區間的字元會在 bit 8 不一樣。如此就可以達成區間的判別了。 |
受到
RinHizakura
的啟發,以下是我嘗試自己整理一次邏輯。
0 => 00
,1 => 01
,2 => 10
3個狀態來實作 算術。左邊叫 higher,右邊叫他 lower。根據題目,最後每個位置的狀態要嘛是 0=>00
,要嘛是 1=>01
。而取所有的 lower 反應出來的就是我們要的 了。0 | 0 | 1 | 0 | 1 |
0 | 1 | 1 | 1 | 0 |
1 | 0 | 1 | 0 | 0 |
1 | 1 | 1 | X | X |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 1 | 0 | X | X |
&= ~higher
可以獲得我們要的結果。0 | 0 | 1 | 1 | 1 |
0 | 1 | 1 | 1 | 0 |
1 | 0 | 1 | 0 | 0 |
1 | 1 | 1 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 1 | 0 | 1 | 0 |
我使用更直覺的加法器設計,每個位置一樣使用兩個位 (lower, higher) 代表狀態:
這樣就完成 Mod 3 計數器了。