# 2020q3 Homework4 (quiz4) contributed by < `ccs100203` > ###### tags: `linux2020` > [第 4 週測驗題](https://hackmd.io/@sysprog/2020-quiz4) :::info TODO 延伸問題 1、2、3、4 ::: ## 測驗 1 [LeetCode 461. Hamming Distance](https://leetcode.com/problems/hamming-distance/) ```cpp= int hammingDistance(int x, int y) { return __builtin_popcount(x ^ y); } ``` - 這題要求 hamming distance,就是判斷兩組數字總共相差了多少 bit 觀察以下的 table,可以看出這就是使用 XOR 的情況,所以使用 XOR 將所有不同的 bit 設成 1,其餘為 0。最後用 `__builtin_popcount` 計算總共有多少 1 就可以知道答案。 | diff | A | B | | :--: | :--: | :--: | | 0 | 0 | 0 | | 1 | 0 | 1 | | 1 | 1 | 0 | | 0 | 1 | 1 | ### 延伸問題 #### [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/) 題目會給定一條 array,算出所有數字之間 hamming distance 最直覺的做法就是暴力解,對每個數字做 `__builtin_popcount(x ^ y)`,但這毫不易外的 TLE。 所以我採取的方法是,比較每一組 bit,每挑出一對 0 跟 1,hamming distance 就會 +1,所以得出以下結論,我只要找出每組 bit 中有多少 0 跟 1,運用組合的方式挑出所有答案。 $${n \choose 1} * {numsSize - n \choose 1}$$ ```cpp= int totalHammingDistance(int* nums, int numsSize){ int ans = 0; for(int i=0; i<32; ++i){ int num = 0; for(int j=0; j<numsSize; ++j){ num += (nums[j] & 0x1); nums[j] >>=1; } ans += num * (numsSize - num); } return ans; } ``` ![](https://i.imgur.com/3OexKie.png) ## 測驗 2 [LeetCode 1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) > 給你一棵樹,樹上有 n 個節點,編號自 0 到 n−1。樹以父節點陣列的形式提供,其中 parent[i] 是節點 i 的父節點。樹的根節點是編號為 0 的節點。請設計 treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) 函式,回傳節點 node 的第 k 個祖先節點。若不存在這樣的祖先節點,回傳 -1。樹節點的第 k 個祖先節點是從該節點到根節點路徑上的第 k 個節點 ```cpp= #include <stdlib.h> typedef struct { int **parent; int max_level; } TreeAncestor; 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 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]; 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; return obj; } 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 & (1<<i)) node = obj->parent[node][i]; return node; } void treeAncestorFree(TreeAncestor *obj) { for (int i = 0; i < obj->n; i++) free(obj->parent[i]); free(obj->parent); free(obj); } ``` - `treeAncestorCreate` 會建立出一個 n * max_level 的 TreeAncestor array,此 array 的用途為記錄 node 的 parent,比較特別的是這邊使用 $2^n$ 的方式去記錄,假設 n = 7,那只會記錄 3 個 parent,分別是第 $2^0$、$2^1$、$2^2$ 的 parent,其他的 parent 只需要利用 1、2、4 這三個值的組合就可以得到。這也是 max_level 的用處,就是存放 $\lfloor{\log_{2}{n}}\rfloor + 1$,代表 array 需要多少個 column。 - `int max_level = 32 - __builtin_clz(n) + 1;`實際上 `max_level` 宣告的時候是 $(\lfloor{\log_{2}{n}}\rfloor + 1) + 1$,最後的 + 1 我認為是浪費空間,他的存在原因為下方第 22 行的 for 並沒有寫終止條件。之後會對這個部分做修正。 - `quit` 就是用來當第 22 行 for 迴圈的終止條件,當有一個 column 全部為 -1 時就會 `break`。 - `treeAncestorGetKthAncestor` 這邊就是去做查表,利用 `if (k & (1<<i))` 去做到查詢的作用。 假設 k = 5,二進位為 0101,利用 $2^0$ + $2^2$ 去組合出 5。那麼答案就會是 `parent[ parent[node][1 << 0] ][1 << 2]` ### 延伸問題 #### 程式碼改進 ```cpp=13 int max_level = 32 - __builtin_clz(n); ``` ```cpp=22 for (int j = 1; j<max_level; j++) ``` - 將多餘的 + 1 移除掉,並且在 22 行新增終止條件,有效降低其記憶體空間的浪費。 - 原先版本繳交後占用 69.3MB,新版則是占用 67.8 MB。 #### 執行時間領先 75% 提交者 在這邊遇到一個問題,繳交同一份程式碼會有各種不同的執行時間。 1. ![](https://i.imgur.com/dLM0S0H.png) 2. ![](https://i.imgur.com/WWdakz2.png) ## 測驗 3 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目: - 從 1 數到 n,如果是 3的倍數,印出 “Fizz” - 如果是 5 的倍數,印出 “Buzz” - 如果是 15 的倍數,印出 “FizzBuzz” - 如果都不是,就印出數字本身 先從這邊來看,此題的關鍵在於如何精準的控制 `start` 以及 `length`,藉由他們去印出答案。 ```cpp #define MSG_LEN 8 char fmt[MSG_LEN + 1]; strncpy(fmt, &"FizzBuzz%u"[start], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); string literal: "fizzbuzz%u" offset: 0 4 8 ``` 以下為完整程式碼: ```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 >> div5) >> (div3 << 2)], length); fmt[length] = '\0'; printf(fmt, i); printf("\n"); } return 0; } ``` - `unsigned int length = (2 << div3) << div5;` 若能整除 3 或 5 則為 4,兩者同時符合則為 8,剩下就是印數字本身所以長度為 2。 - `is_divisible` 可以參考 [2020q3 Homework2 (quiz2)](https://hackmd.io/@cccccs100203/linux2020-quiz2#%E6%B8%AC%E9%A9%97-4) 的解釋 - `&"FizzBuzz%u"[ (MSG_LEN >> KK1) >> (KK2 << KK3) ]` 括號內的動作就是在判定 `start` 的位置,用來當 array 的 index。 - 用一個表格表示 `start` 的可能性: | div3 | div5 | start | | :-: | :-: | :-: | | 0 | 0 | 8 | | 1 | 0 | 0 | | 0 | 1 | 4 | | 1 | 1 | 0 | - 只要 `div3` 為 1, `start` 就應為 0,所以可判斷出 `(MSG_LEN >> KK1) >> (div3 << 2)`。再來藉由 `(MSG_LEN >> div5)` 去處理剩下的情況也就不難想到。 ## 測驗 4 考慮到整數 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; ``` 將原本的運算轉換為下列形式 ```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` 範圍,所以在 5~6 行先做 shift 就是為了降低除法的運算成本。可以藉由 `ffs32(x)` 得到 `divident` 有多少 tail zero,將這些 `/ 2` 的部分用 shift 取代掉。 - 在 shift 過後 `divident` 只會剩下 1、3、5、7 四種數值,所以最後的 `CASES` 就必須包含 3、5、7。 #### 延伸問題 [LeetCode 51. N-Queens](https://leetcode.com/problems/n-queens/) - 必須運用 `__builtin_ffs`