# 2020q3 Homework4 (quiz4)
contributed by < `Sisker1111` >
###### tags: `進階電腦系統理論與實作`
> [測驗題](https://hackmd.io/@sysprog/2020-quiz4)
>
## 測驗 1
LeetCode [461. Hamming Distance](https://leetcode.com/problems/hamming-distance/) 提及,兩個整數間的 [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) 為其二進位的每個位元的差。請計算輸入參數兩整數 x 與 y 的 [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance),例如整數 1 的二進位為 0 0 0 1,而整數 4 的二進位為 0 1 0 0,則 1 與 4 的 [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) 為 2。
運用第 3 周測驗 提到的 __builtin_popcount (gcc extension 之一),我們可實作如下:
```cpp=
int hammingDistance(int x, int y) {
return __builtin_popcount(x OP y);
}
```
Hamming Distance 是要找出兩個二進位數字不同的 bits 有幾個,已知`__builtin_popcount`會 return `bit = 1`的數量,也就是說,若兩數某一 bit 不同,我們要 reset 為 1,否則就 reset 為 0,很明顯的,`OP = ^`.
## 測驗 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 個節點
```c=
#include <stdlib.h>
typedef struct {
AAA;
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 + 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 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;
}
void treeAncestorFree(TreeAncestor *obj)
{
for (int i = 0; i < obj->n; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
`treeAncestorCreate`
* 對於 n 個節點,最差情形下是有 n - 1 代的祖先,可以想到如果把每個節點從第 0 個到第 n - 1 個祖先都紀錄下來,就可以在 treeAncestorGetKthAncestor 時有 O(1) 的時間效率
* 然而這需要建立 sizeof(int) * (n * n - 1) 的空間,當 n 越大時越不可行,如果不預先建立查詢的表,每一次 treeAncestorGetKthAncestor 就變成要承受時間成本.
* 因此,一個時間和空間的 trade-off,就是僅記錄每個點的第 1、2、4、…2^n 個祖先是誰
回到程式碼:
1. 從 11~15 行可知,`obj->parent`是一個二維陣列,因此 struct裡的`AAA`應要是一個可以動態 malloc 二維空間的變數,故選`(b)` `int **parent`
2. 接著看 19~34 行,已知`parent[i]`是節點 i 的父節點,樹的根節點是編號為 0 的節點,可以大致判斷出`obj->parent[i][0]`是存放節點 i 的父節點,`obj->parent[i][1]`是存放節點 i 父節點的父節點......以此類推.
* 第 25 行在判斷說,如果 i 不存在父節點,則節點 i 父節點的父節點也不存在,因此`BBB = -1`.
* 如果存在的話,舉例來說,a 的第 $2^j$ 個 祖先就是第 $2^{j-1}$ 個祖先的第 $2^{j-1}$ 個祖先($2^{j-1}$+$2^{j-1}$ = $2^j$).
3. Code 36~43 行是要找出節點 node 的第 k 的祖先,k 可以拆成 $2^j$ 的組合,例如第 6 個祖先可以拆成是搜尋第 4 個祖先的第 2 個祖先,因此答案為`(f)` `1 << i`
### 練習 LeetCode [LeetCode 1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/)
```c=
```
* Runtime : 0 ms, faster than 100.00% of C++ online submissions.
Memory Usage : 6.1 MB, less than 40.99% of ...
## 測驗 3
考慮貌似簡單卻蘊含實作深度的 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 題目:
* 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
* 如果是 5 的倍數,印出 “Buzz”
* 如果是 15 的倍數,印出 “FizzBuzz”
* 如果都不是,就印出數字本身
觀察 printf 的(格式)字串,可分類為以下三種:
1. 整數格式字串 "%d" : 長度為 2 B
2. “Fizz” 或 “Buzz” : 長度為 4 B
3. “FizzBuzz” : 長度為 8 B
考慮下方程式碼:
```cpp
#define MSG_LEN 8
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[start], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
```
我們若能精準從給定輸入 i 的規律去控制 start 及 length,即可符合 FizzBuzz 題意:
```cpp
string literal: "fizzbuzz%u"
offset: 0 4 8
```
以下是利用 bitwise 和上述技巧實作的 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;
}
```
本來想藉由控制"擷取字串的長度"及"字串的起始位置",來印出不同 case 的結果:
程式碼前半在 [quiz2](https://hackmd.io/@sysprog/2020-quiz2) 已經看過,是用來判斷該數是否能被 3 or 5 來整除,接著看向 Code 第 17 行:
這邊得用 4 個 case 來做刪去法,才比較好判斷答案是那些:
1. 某數 x 不會被 3 整除且不會被 5 整除,為了印出`%d`,那麼應該從該字串的第 8 個位置開始擷取 2B 字串,那麼 KK1 中 ( c )、( d ),KK2 中 ( b )、( c ) 就一定不能是答案.
2. 某數 x 會被 3 整除且不會被 5 整除,為了印出`Fizz`,那麼應該從該字串的第 0 個位置開始擷取 4B 字串,若要把 8 變成 0 必須達成 `>> 4`,KK1 為 ( a )、( b )都有可能,這邊先假設`KK1 = div5`.
3. 某數 x 不會被 3 整除且會被 5 整除,為了印出`Buzz`,那麼應該從該字串的第 4 個位置開始擷取 4B 字串,已知`KK1 = div5`,`(MSG_LEN >> 1) = 4` ,`(KK2 << KK3)`必須為 0,KK2 中的 ( e )不可能是答案.
4. 某數 x 會被 3 整除且會被 5 整除,為了印出`FizzBuzz`,那麼應該從該字串的第 0 個位置開始擷取 8B 字串,已知`(MSG_LEN >> div5) = 4`,KK2 的 (a) 不可能是答案,因此 KK2 只剩 `(d)` `div3`一種可能,要滿足` div3 << KK3`的結果可以使得`(MSG_LEN >> 1) >> (1 << KK3) = 0`,KK3 只有`(e)` `(2)`能滿足且能同時滿足以上 4 種 case.
5. 重回 Step.2 假設 `KK1 = div3`,會發現找不到一組解能同時滿足 4 種 case.
## 4. 測驗 4
考慮到整數 PAGE_QUEUES 可能有以下數值:
* (1 or 2 or 3 or 4)
* (5 or 6 or 7 or 8) x (2^n), n from 1 to 14
給定 `size_t offset`,試著將原本運算:
```cpp
#include <stdint.h>
size_t blockidx = offset / PAGE_QUEUES;
```
由於 `PAGE_QUEUES` 的數值分佈已知,在整數除法時,可思考加速運算的機制。需要注意,某些硬體平台缺乏整數除法指令 (如 Arm Cortex-A8),即使 Intel 公司出品的 Core 處裡器 [Sandy Bridge 微架構](https://en.wikipedia.org/wiki/Sandy_Bridge)中,針對 64 位元整數的除法運算,會佔用 40 到 103 個處理器週期,運算成本仍屬沈重。
> 來源: [Agner Fog’s instruction tables](https://www.agner.org/optimize/instruction_tables.pdf),第 180 頁
於是我們可預先進行計算,從而避免整數除法指令的使用: (假設在 x86_64 硬體執行 GNU/Linux 並透過 gcc/clang 編譯)
```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
}
```
其中 `CASES` 可能形如:
```cpp
case 2: blockidx /= 2;
break;
```
這樣的組合,請用最少的 case-break 敘述做出同等 `size_t blockidx = offset / PAGE_QUEUES;` 效果的程式碼。
* 摘自 Built-in Functions Provided by GCC:
>Built-in Function: int `__builtin_ffs (int x)`
Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
觀察 `PAGE_QUEUES`,可以知道其數值都可以表示成 k x $2^n$,其中 k 是介於 1 到 8 之間,n 則介於 0 到 14 之間,回想 [quiz2](https://hackmd.io/@sysprog/2020-quiz2) `測驗 3`,對於 $2^n$ 的除法運算可以轉為 bitwise operator,因此:
* `ffs32(x)`的作用就是找到 $2^n$ 中的 n 是多少.
* `blockidx = offset >> ffs32(divident);`用來將 offset/=$2^n$.
* `divident >>= ffs32(divident);`用來將`PAGE_QUEUES`/=$2^n$.
* 因此 switch case 只要針對前面的 k 去計算即可,需要特別處理的 k 值只需要有 `k=3` `k=5` `k=7`,其餘`k=4` `k=6` `k=8` 都已經被包含在前幾個 case 之中.