# 2020q3 Homework4 (quiz4) contributed by < `OscarShiang` > ## 作業要求 - [x] 重新回答第 4 週測驗題 ## 測驗 `1` 本題的用意在於實作出可以計算兩個給定數字之間的 Hamming distance 根據 [Hamming distance - Wikipedia](https://en.wikipedia.org/wiki/Hamming_distance) 中的定義 > The Hamming distance between two equal-length strings of symbols is the number of positions at which the corresponding symbols are different. 假設給定的兩個字串 `001` 與 `010` ,因為兩者有在對應位置上有兩處不同,因此 Hamming distance 是 2。 而看到本題實作的部分 ```cpp int hammingDistance(int x, int y) { return __builtin_popcount(x OP y); } ``` 因為使用 `__builtin_popcount` 來計算 Hamming distance ,所以我們只需要將相異的兩個位元設定為 `1` 即可,所以 `OP` 的答案就會是 > OP = `^` ## 測驗 `2` 本題嘗試實作一個樹狀結構以及相關的操作,包括 - `treeAncestorCreate` - `treeAncestorGetKthAncestor` - `treeAncestorFree` 並且以下圖作為範例來實作 ```graphviz digraph { node [shape=circle] 0 -> {1, 2} [arrowhead=none] 1 -> {3, 4} [arrowhead=none] 2 -> {5, 6} [arrowhead=none] } ``` ### AAA 本題的部分是要補完樹狀結構元素的 `struct`,根據 ```cpp typedef struct { AAA; int max_level; } TreeAncestor; ``` 的部分我們可以知道這個元素的結構包括一個 int 變數 `max_level` 用記錄從這個節點向上還有多少層元素。 接著我們可以看到 `treeAncestorCreate` 函式中關於節點初始化的部分 ```cpp TreeAncestor *obj = malloc(sizeof(TreeAncestor)); obj->parent = malloc(n * sizeof(int *)); ``` 從這邊我們可以看到 `TreeAncestor *` 這個變數 `obj` 的結構中有 `parent` 這個 field,並從 `malloc` 的參數得知,我們取得了一段可以存取 `n` 個 `int *` 型態的變數的空間,所以根據上述的線索我們可以推測在 AAA 中缺少的 field 應該是 > AAA = `int **parent` ### BBB 根據 BBB 所在位置的程式碼 ```cpp for (int j = 1;; j++) { int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][BBB] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } ``` 我們可以從三元運算子 else 的條件看到,如果 `obj->parent[i][BBB] == -1` 的條件不成立時,則會將 `obj->parent[obj->parent[i][j - 1]][j - 1]` 的數值賦值到 `obj->parent[i][j]` 中。 因此我們可以判斷 BBB 的部分是在確認是否存在 `obj->parent[i][BBB]` 亦即該變數的數值不等於 `-1`,才能做 `obj->parent[obj->parent[i][j - 1]][j - 1]` 取值的運算。 因為根據 ISO/IEC 9899 6.5.2.1 Array subscripting 的描述 > The definition of the subscript operator [] is that **E1[E2]** is identical to **(*((E1)+(E2)))**. 若我們嘗試取得一個 array 編號 -1 的元素時,根據上述的概念,我們實際上會存取到在 array 第 0 個元素之前的資料,除了程式將無法按照我們預期的執行之外,也會造成記憶體存取上的疑慮,因為我們正在對程式宣告的記憶體空間以外的其他位址進行操作。 因此為了避免這樣的問題,我們才需要事先確認 `obj->parent[i][BBB] == -1` 的情況是否發生 所以 BBB 的答案就會是 > BBB = `(-1)` ### CCC 為了了解這題的實作觀點,我們首先可以看到 `treeAncestorCreate` 函式中針對 `max_level` 的計算 ```cpp TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize) { TreeAncestor *obj = malloc(sizeof(TreeAncestor)); obj->parent = malloc(n * sizeof(int *)); int max_level = 32 - __builtin_clz(n) + 1; ... for (int j = 1;; j++) { int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][j + BBB] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } obj->max_level = max_level - 1; return obj; } ``` 從這個部分我們可以知道 `int max_level = 32 - __builtin_clz(n) + 1` 這個部分本質上就是在做 $log_2 \ n + 1$ 的動作,也就是在計算這個二元樹的最多會有多大的 level。 在本題中,因為共有 7 個節點,所以最多會有 3 個 level (第 0 ~ 2 層),而最後 `+1` 的部分是為了保留終止的數值 `-1`。 因此我們可以知道本題的結構在 `obj->parent` 中保存的狀態應該如下表所示 ``` [0]: -1 -1 -1 -1 [1]: 0 -1 -1 -1 [2]: 0 -1 -1 -1 [3]: 1 0 -1 -1 [4]: 1 0 -1 -1 [5]: 2 0 -1 -1 [6]: 2 0 -1 -1 ``` 而接著在 `treeAncestorGetKthAncestor` 中,也就是 CCC 所在的位置 ```cpp int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) { int max_level = obj->max_level; for (int i = 0; i < max_level && node != -1; ++i) if (k & (CCC)) node = obj->parent[node][i]; return node; } ``` 這邊所採取的方式是將給定的 `k` 利用 `i` 拆成二的次方的級數 例如我們給定 $$ k = 7 $$ 則透過 for 迴圈,我們應該要將其拆成 $$ \begin{align} k &= 1 + 2 + 4 \\\\ &= 2^0 + 2^1 + 2^2 \end{align} $$ 接著將這些數字分別帶入 `obj->parent` 的陣列中找尋對應的節點 所以根據上述的條件,CCC 應該為 > CCC = `1 << i` ### 修改實作 從 `CCC` 中我們可以發現在尋找第 k 個祖先節點的時候我們是透過 2 的冪次逐漸求得我們所要的答案,但是因為這些資訊在 `obj->parent` 中全部都有記錄,所以我們也可以利用下列的方式來取得答案 ```cpp int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) { int max_level = obj->max_level; if (k > max_level) k = max_level; node = obj->parent[node][k - 1]; return node; } ``` 再來因為我們剛剛在解釋 `BBB` 的時候,有提到每個節點的最後面都有一個 `-1` 的數值作為終止的標記。 但是在 `treeAncestorCreate` 在最後有這麼一行操作 ```cpp obj->max_level = max_level - 1; ``` 而在 `treeAncestorGetKthAncestor` 函式中是如此操作這個陣列 ```cpp for (int i = 0; i < max_level && node != -1; ++i) if (k & (CCC)) node = obj->parent[node][i]; ``` 從這邊我們就可以看到在這些操作中,我們顯然根本不會使用到在 `treeAncestorCreate` 的時候考慮到的最後一個終止的數值。因為我們在設定完最後一個位置之後,有將邊界做調整,對應到 for 迴圈的邊界,所以在正常使用 `max_level` 作為邊界來操作的時候,我們是不會用到陣列的最後一個 `-1` 的。 因此我們可以再針對這個實作做以下的調整 ```diff TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize) { /* Initialize the variables */ TreeAncestor *obj = malloc(sizeof(TreeAncestor)); obj->parent = malloc(n * sizeof(int *)); - int max_level = 32 - __builtin_clz(n) + 1; + int max_level = 32 - __builtin_clz(n); for (int i = 0; i < n; i++) { obj->parent[i] = malloc(max_level * sizeof(int)); for (int j = 0; j < max_level; j++) obj->parent[i][j] = -1; } for (int i = 0; i < parentSize; i++) obj->parent[i][0] = parent[i]; /* Construct the relations of each parents */ for (int j = 1;; j++) { int quit = 1; for (int i = 0; i < parentSize; i++) { obj->parent[i][j] = obj->parent[i][j - 1] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; if (obj->parent[i][j] != -1) quit = 0; } if (quit) break; } - obj->max_level = max_level - 1; + obj->max_level = max_level; return obj; } ``` ## 測驗 `3` 本題嘗試使用 bitwise 的方式實作一個滿足以下條件的 Fizzbuzz 題目 - 從 1 數到 n,如果是 3 的倍數,印出 "Fizz" - 如果是 5 的倍數,印出 "Buzz" - 如果是 15 的倍數,印出 "FizzBuzz" - 如果都不是,就印出數字本身 透過給定數值的規律來控制輸出字串的形式 ```cpp #define MSG_LEN 8 static inline bool is_divisible(uint32_t n, uint64_t M) { return n * M <= M - 1; } static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1; static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1; int main(int argc, char *argv[]) { for (size_t i = 1; i <= 100; i++) { uint8_t div3 = is_divisible(i, M3); uint8_t div5 = is_divisible(i, M5); unsigned int length = (2 << div3) << div5; char fmt[MSG_LEN + 1]; strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` :::info :bulb: 關於 `is_divisable` 的解釋可以參考 [2020q3 Homework2 (quiz2)](/pzCs1jd9TQGtTcjG-_eWzg) ::: 所以根據 `is_divisable` 函式的定義,`div3` 與 `div5` 只有 `0` 或 `1` 兩種結果,因此我們只要專注在如何用 `div3` 與 `div5` 湊出對應的字串即可。 因此我們可以考慮以下的情況 考慮給定的數字是 3 的倍數,亦即 `div3 = 1` 且 `div5 = 0` 因為我們要輸出的字串是 `Fuzz`,所以需要先透過 `(MSG_LEN >> KK1) >> (KK2 << KK3)` 計算出 `0` 假設 `KK1` 的答案是 `div3` 而 `KK2` 的答案是 `div5`,則表示 `(8 >> 1) >> (0 << KK3)` 得到 `4` 考慮到 8 這個數字至少要位移 4 次後才會是 0 ,所以可以組合出最大的可能就會是 > KK1 = `div5` > [color=purple] > KK2 = `div3` (或 `2`) > [color=purple] > KK3 = `2` (或 `div3`) > [color=purple] 我們可以帶入 `div3 = 0` 且 `div5 = 1` 也就是給定數字是 5 的倍數的情況來驗算 考慮給定 `div3 = 0` 與 `div5 = 1` 的 `(MSG_LEN >> div5) >> (div3 << 2)` 與 `(MSG_LEN >> div5) >> (2 << div3)` 兩種情況 代入數字後兩式就會變成 `(8 >> 1) >> (0 << 2) = 4` 與 `(8 >> 1) >> (2 << 0) = 1` 所以根據計算的結果我們就可以知道正確的組合是 > KK1 = `div5` > KK2 = `div3` > KK3 = `2` ### `naive.c` 實作的問題 根據題目給定的 `naive.c` 版本的實作 ```cpp #include <stdio.h> int main() { for (unsigned int i = 1; i < 100; i++) { if (i % 3 == 0) printf("Fizz"); if (i % 5 == 0) printf("Buzz"); if (i % 15 == 0) printf("FizzBuzz"); if ((i % 3) && (i % 5)) printf("%u", i); printf("\n"); } return 0; } ``` 在給定數字 `i` 是 3 或是 5 的倍數的時候可以正常印出規定的字串形式,但是因為上述程式碼在邏輯上採用的是個別判斷是否為 3 或 5 的倍數的手法,所以在遇到 `i = 15 * n (n >= 1)` 的情況的時候會輸出以下的字串 ``` i = 1, 1 i = 2, 2 i = 3, Fizz i = 4, 4 i = 5, Buzz i = 6, Fizz i = 7, 7 i = 8, 8 i = 9, Fizz i = 10, Buzz i = 11, 11 i = 12, Fizz i = 13, 13 i = 14, 14 i = 15, FizzBuzzFizzBuzz i = 16, 16 ``` 從上列的輸出可以看到在判斷的邏輯上,若 `i` 是 15 的倍數也就表示其是 3 與 5 的倍數,所以在這個實作中,我們不需要額外針對 `i` 是 15 的倍數的情況在處理,因為我們在判斷其為 3 的倍數的時候就會輸出 `Fuzz`;判斷其為 5 的倍數的時候就會輸出 `Buzz` 了 所以我們可以將 `naive.c` 的實作進行修改 ```diff #include <stdio.h> int main() { for (unsigned int i = 1; i < 100; i++) { if (i % 3 == 0) printf("Fizz"); if (i % 5 == 0) printf("Buzz"); - if (i % 15 == 0) printf("FizzBuzz"); if ((i % 3) && (i % 5)) printf("%u", i); printf("\n"); } return 0; } ``` 省去多餘的邏輯判斷。 ## 測驗 `4` 本題的用意在於使用 `__builtin_ffs` 來減少整數除法的開銷 考慮到 `PAGE_QUEUES` 的數值範圍如下 - (1 or 2 or 3 or 4) - (5 or 6 or 7 or 8) × (2^n^), n from 1 to 14 用以進行以下的運算 ```cpp #include <stdint.h> size_t blockidx = offset / PAGE_QUEUES; ``` 因為 `PAGE_QUEUES` 的數值最大可能是 $8 \times 2^{14}$ 所以若不進行化簡而直接做整數除法的話會運算的成本,因此我們可以使用 `__builtin_ffs` 將被除數與除數先行化簡 ```cpp #include <stdint.h> #define ffs32(x) ((__builtin_ffs(x)) - 1) size_t blockidx; size_t divident = PAGE_QUEUES; blockidx = offset >> ffs32(divident); divident >>= ffs32(divident); switch (divident) { CASES } ``` 由於 `PAGE_QUEUES` 的範圍已知,所以如果經過 `ffs` 的化簡後則可能的除數剩下 $$ 1,\ 3,\ 5,\ 7 $$ 四種可能,除去被除數為 1 的情況,`CASES` 至少要包括 > (b) 3 > (d) 5 > (f) 7 三個情況才行。 ## 參考資料 - [2020q3 第 4 週測驗題](https://hackmd.io/@sysprog/2020-quiz4) - [Hamming distance - Wikipedia](https://en.wikipedia.org/wiki/Hamming_distance) ###### tags: `sysprog2020`