# 2020q3 Homework2 (quiz2) contributed by < `fwfly` > --- ### 測驗 3 原函式 $$ n*\dfrac{2^N}{d}*\dfrac{1}{2^N} $$ ```cpp const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { // M*n / 2^N uint64_t quotient = ((__uint128_t) M * n) >> 64; return n - quotient * D; } ``` 從原函式知道 shift 64位元相當於除以 $2^N$ ``` ((__uint128_t) M * n) >> 64 ``` 所以 $$ M*n = n\dfrac{2^N}{d} $$ XXX 的部分就會是 2^N = 0xFFFFFFFFFFFFFFFF 但是這樣並不能解釋這句 :::info 其中一種策略是將除法改為乘法運算 ::: 原因是 `UINT64_C(XXX) / (D) + 1` 依然還是除法,既然是除法理論上應該不會變得比較快 :::danger 1. 避免武斷地說「理論上」,你的「理論」適用於現代電腦架構嗎? 2. 避免說「透過網路上的文章」,你應該明確指出是哪一篇文章和時空背景,再來討論。不要跟記者^TM^一般說話。 :notes: jserv ::: 依據 [Faster remainders when the divisor is a constant](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) 可以得到這句話 :::info Thus if 2N/d has been precomputed, you can compute the division n/d as a multiplication and a shift ::: 也就是說 quickmod 的 quick 是來自於 precomputed.也可以理解 compiler 已經把除法的部份做完了( 因為兩個都是常數 ) #### assembly code 透過組合語言輸出可以看到其實程式還是有做除法 `divq` ```asm $ gcc -S W2Q3.c $ cat W2Q3.s quickmod: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 ... ... movl $0, %edx divq %rdi ``` 但是如果做了最佳化,就可以看到整個函式是沒有`除法`的. ```asm $ gcc -S -O1 W2Q3.c $ cat W2Q3.s quickmod: .LFB23: .cfi_startproc movl %edi, %eax movabsq $6148914691236517206, %rdx mulq %rdx leal (%rdx,%rdx,2), %eax subl %eax, %edi movl %edi, %eax ret .cfi_endproc ``` 所以從這邊可以知道要達到 quickmode 的條件有兩個 1. 兩個數字都是常數 (const 跟 0xFFF...) 2. 最佳化需要打開 (-O1 option) --- ### 測驗 4 程式碼包含解答如下 ```c bool divisible(uint32_t n) { uint64_t nm = n * M; uint64_t YYY = M - 1; printf("n: %u\n",n); printf("nM: %lu\n",nm); return n * M <= YYY; } ``` 這裡主要是靠 overflow 去驗證是否能夠被整除 從簡單的 n*M 可以得到以下結果,明顯看得出來能被整除的會導致 overflow ``` n: 695 nM: 12297829382473034874 n: 555 nM: 370 ``` 但是 695 明明就比 555 大卻沒有產生 overflow ? #### 理解 n*M 原因可以這樣看會比較好理解. 在除法部分 2^N/3 可以看成把 2^N 分成 3 等分. 在乘法部分 3*M 可以理解成 : 把 ( 2^N/3 + 1 ) 加了 3 次 所以會變成 $$ (\dfrac{2^N}{3} + 1) + ( \dfrac{2^N}{3}+1) + (\dfrac{2^N}{3} + 1) = 2^N + 3 $$ 2^N = 0xFFF...FFFF = 64位元最大整數 所以再加 3 就會 overflow 那如果 n = 4 會怎麼樣 ? $$ (\dfrac{2^N}{3} + 1) + ( \dfrac{2^N}{3}+1) + (\dfrac{2^N}{3} + 1) + (\dfrac{2^N}{3} + 1) = 2^N + 3 + (\dfrac{2^N}{3} + 1) $$ 事實上也是 overflow , 但是因為多加了一個 M 就看起來會很像沒有 overflow. 有點像是時鐘跑了 12 之後回到 1 重新計算.所以如果實際執行就會產生以下結果 ``` n: 1 nM: 6148914691236517206 n: 2 nM: 12297829382473034412 n: 3 nM: 2 n: 4 nM: 6148914691236517208 n: 5 nM: 12297829382473034414 n: 6 nM: 4 ``` 可以看得出來隨著 n 的變化只是不斷的在 loop. #### YYY 是什麼? 基本上 YYY 可以等於 M ,但是在當 n = 1 的時候會出現錯誤. 因為當 YYY = M 且 n = 1 的時候會出現 ```C return M <= M # 回傳 1 而不是 0 , 因為 n = 1 ``` 因此在這樣的情況下為了產生小於的結果 M 需要做減 1 所以 YYY = M - 1 ### 測驗 6 ```c int singleNumber(int *nums, int numsSize) { int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= ~higher; higher ^= nums[i]; higher &= ~lower; } return lower; } ``` #### XOR 根據 [XOR swap algorithm](https://en.wikipedia.org/wiki/XOR_swap_algorithm) 知道, XOR 有一個特性是自己 XOR 自己可以抵消 $$ A \oplus A = 0. $$ 所以如果有兩個數字相同 XOR 自己是會消失的. 比方說 [3,3,2,1,1] $$ 3 \oplus 3 \oplus 2 \oplus 1 \oplus 1 $$ :::danger 用字要精準,什麼「消失」?又什麼「自己」?你可以嘗試用英語將上述表達翻譯一次,看會得到什麼。 :notes: jserv ::: 3 跟 1 會因為自己跟自己 XOR 而為 0, 而 0 跟任何一個數字 XOR 都是原本的數字,所以透過 XOR 可以知道 2 唯一只有存在一個的數字. 但是這樣子不足以解決當重複數字為 3 或奇數的時候 #### & [Bitwise operation AND ](https://en.wikipedia.org/wiki/Bitwise_operation#AND) reference: * https://stackoverflow.com/questions/21297067/single-number-ii-from-leetcode * [leetcode 之 Single Number II](https://blog.csdn.net/yutianzuijin/article/details/50597413) * [leetcode-137-Single Number II-第二种解法](https://cloud.tencent.com/developer/article/1131945) * [[LeetCode] 137. Single Number II 深入浅出算法讲解和代码示例](https://blog.csdn.net/karen0310/article/details/78226261)