# 2020q3 Homework4 (quiz4)
contributed by < `Rsysz` >
###### tags: `sysprog`
> [測驗題](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` 取兩者不同的位數用來計算距離,因此OP = `(c) ^`
### 延伸問題
```cpp
int hammingDistance(int x, int y) {
int num = x ^ y;
num = (num & 0x55555555) + ((num >> 1) & 0x55555555);
num = (num & 0x33333333) + ((num >> 2) & 0x33333333);
num = (num & 0x0F0F0F0F) + ((num >> 4) & 0x0F0F0F0F);
num = (num & 0x00FF00FF) + ((num >> 8) & 0x00FF00FF);
num = (num & 0x0000FFFF) + ((num >> 16) & 0x0000FFFF);
return num;
```
![](https://i.imgur.com/CNlHpXD.png)
如同 `quiz3` 一樣,每個 `byte` 獨立計算 `1` 的數量再予以加總輸出
[Leetcode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/)
最簡單的寫法就是每兩兩計算一次 `hammingDistance` ,然後很不意外的 `TLE` 了,代表更有效率的解法是存在的,因此我們先把範例中的數值列出以表格做觀察
| Number | Bits |
| -------- | -------- |
| 4 | 0100 |
| 14 | 1110 |
| 2 | 0010 |
`hammingDistance` 就是求取兩兩不同的 `bit` 數目的合,因此可以發現若將每位 `bit` 裡 `0` 與 `1` 的數目分別加總並相乘,剛好會等於 `totalHammingDistance`
$2_0 \times 1_1 + 1_0 \times 2_1 + 1_0 \times 2_1 = 6$ 因此我們可以將程式改寫為這樣
```cpp
int totalHammingDistance(int* nums, int numsSize){
int res = 0;
for(int j = 0, c = 0; j < 30; j++, c = 0){
for(int i = 0; i < numsSize; nums[i] >>= 1, i++)
c += (nums[i] & 1);
res += c * (numsSize - c);
}
return res;
}
```
![](https://i.imgur.com/v6U6pQQ.png)
#### ECC, Hamming Distance & Hamming Weight
因雜訊,碟片損傷等,資料傳輸過程中需要面對部分資訊遺失或錯誤的風險,因此錯誤更正碼應運而生,錯誤更正碼的精神在如何確保資訊正確的情況下,盡量減少 `redundant bits` 的使用。
如下圖所示以 (15, 11) `Hamming code` 為例,透過 `1, 2, 4, 8` , 4 個 `redundant bits` 也就是 `Q1` ~ `Q4` 的相互比較確認 `1bit` 錯誤發生的地方,可以對 `15 bits` 內發生的 `1 bit` 進行糾正,但若發生兩個 `bits` 以上的錯誤便無法偵測
![](https://i.imgur.com/TFbhtn1.png)
但若使用 `0` 號 bit 檢查整體的 1-bits 是否為偶數,雖然無法進行更正,但能夠檢測出兩個 `bits` 以上的錯誤,是為 `extended Hamming code`
![](https://i.imgur.com/cFnn6MD.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 個節點
![](https://i.imgur.com/WsOPG3W.png)
Input:
```
["TreeAncestor","getKthAncestor","getKthAncestor","getKthAncestor"]
[[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]]
```
Output:
```
[null,1,0,-1]
```
```cpp
#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);
}
```
本題透過建立 `Ancestor Table` 儲存每個節點的==每$2^n$以上父代==直到 `root` 也就是 `0` ,因此可以判斷 `parent` 為二維陣列,AAA = `(b) int **parent`
```cpp
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
```
程式運行到這行之後便會建立如下圖所示的 `Ancestor Table` , `[i]` 為 `row` 而 `[j]` 為 `column`
|parent[i][j]|[0]|[1]|[2]|[3]|
|-|-|-|-|-|
|[0]|-1|-1|-1|-1|
|[1]|0|-1|-1|-1|
|[2]|0|-1|-1|-1|
|[3]|1|-1|-1|-1|
|[4]|1|-1|-1|-1|
|[5]|2|-1|-1|-1|
|[6]|2|-1|-1|-1|
但這張 `Ancestor Table` 缺少==祖父==輩以上節點的資訊,若以此進行查詢將會花費過多的時間,因此透過下方程式碼更新 `Ancestor Table` ,BBB = `(b) (-1)`
```cpp
...
obj->parent[i][j] = obj->parent[i][j + BBB] == -1
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
...
```
|parent[i][j]|[0]|[1]|[2]|[3]|
|-|-|-|-|-|
|[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|
以 `GetKthAncestor(5, 2)` 為例,目標在於找尋 `5` 節點的上 `2` 代,也就是上$2^1$代,觀察 `node = obj->parent[node][i]` 與上方 `Ancestor Table` ,因 `Table` 內直接存有目標節點,又$2^i = 2^1$因此最有效率的找法就是直接令 `node = parent[5][1]`
代表 `if (k & (CCC))` 必須在 `i = 1` 時成立,其餘則不成立,以 `Bits` 型式觀察 `2` ,可以得到
CCC = `(f) 1 << i`。
| Number | Bits |
| -------- | -------- |
| 2 | 0010 |
```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;
}
```
### 延伸問題
```cpp
typedef struct {
int **parent;
int max_level;
} TreeAncestor;
TreeAncestor* treeAncestorCreate(int n, int* parent, int parentSize) {
TreeAncestor *obj = malloc(sizeof(TreeAncestor));
obj->max_level = 32 - __builtin_clz(n);
obj->parent = malloc(obj->max_level * sizeof(int *));
for(int i = 1; i < obj->max_level; i++)
obj->parent[i] = malloc(n * sizeof(int));
obj->parent[0] = parent;
for(int i = 1; i < obj->max_level; ++i){
for(int j = 0; j < n; ++j)
obj->parent[i][j] = obj->parent[i - 1][j] == -1 ? -1 : obj->parent[i-1][obj->parent[i-1][j]];
}
return obj;
}
int treeAncestorGetKthAncestor(TreeAncestor* obj, int node, int k) {
for (int i = 0; i < obj->max_level && node != -1; ++i)
if (k & (1 << i))
node = obj->parent[i][node];
return node;
}
void treeAncestorFree(TreeAncestor* obj) {
for (int i = 0; i < obj->max_level; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
![](https://i.imgur.com/PJm2xAa.png)
將原來 `parent[i][j]` 型式翻轉為 `parent[j][i]` ,這個小改動可以使得原來
```cpp
...
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];
...
```
改動為
```cpp
...
obj->parent[0] = parent;
...
```
減少存取陣列元素的次數,也可以避免重複對陣列寫入數值,此外因原始程式碼中沒有對
```cpp
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;
}
```
中的 `j` 設邊界條件,此舉雖然減少了平衡樹 `assign` 的數量,但也因此需要對 `max_level` 額外加 `1` 來避免 `j` 的越界存取。
舉例來說,以 `[5, [-1, 0, 1, 2, 3]]` 代入,若 `max_level` 沒有加 `1` 的話, `j` 便會存取到 ==Highligh== 部分的記憶體,從而導致 `heap-buffer-overflow`。
|parent[i][j]|[0]|[1]|[2]|==[3]==
|-|-|-|-|-|
|[0]|-1|-1|-1|==-1==|
|[1]|0|-1|-1|==-1==|
|[2]|1|0|-1|==-1==|
|[3]|2|1|-1|==-1==|
|[4]|3|2|0|==-1==|
## 3.
貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
* 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
* 如果是 5 的倍數,印出 “Buzz”
* 如果是 15 的倍數,印出 “FizzBuzz”
* 如果都不是,就印出數字本身
```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;
}
```
觀察 `printf` 的(格式)字串,可分類為以下三種:
1. 整數格式字串 "%d" : 長度為 2 B
2. “Fizz” 或 “Buzz” : 長度為 4 B
3. “FizzBuzz” : 長度為 8 B
```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;
}
```
`is_divisible` : 透過先前 [quiz2](https://hackmd.io/@sysprog/2020-quiz2#%E6%B8%AC%E9%A9%97-4) 學過的 `fast divide` 技巧,快速判斷 `i` 是否為 `3` 或 `5` 的倍數。
length有三種情形
* 當 `i` 是 `15` 的倍數, `length = 8`
* 當 `i` 是 `3` 或 `5` 的倍數,且非 `15` 的倍數, `length = 4`
* 當 `i` 非 `3` 或 `5` 的倍數, `length = 2`
且根據前面提到的印出值因此,當 `i` 有因數 `3` 時 `MSG_LEN` 要右移 `3` 以上,當 `i` 有因數 `5` 時 `MSG_LEN` 要右移 `1` ,答案裡所有組合只有 KK1 = `(a) div5`, KK2 = `(d) div3`, KK3 = `(c) 2` 能達到目的。
### 延伸問題
將兩者 `printf` 修改為 `sprintf` 後以 `sudo perf stat` 量測兩者的差異,因為兩者最大的差異應是,進行大數除法所耗費的時間,因此將輸入 `i` 設定為從30億到40億
但沒想到 `naive` 耗費的時間遠小於 `bitwise` ,然而在 `branch-misses` 上因 `bitwise` 是使用 `branchless` 的寫法,因此有所改善。
```bash
Performance counter stats for './naive':
3,7177.70 msec task-clock # 1.000 CPUs utilized
130 context-switches # 0.003 K/sec
0 cpu-migrations # 0.000 K/sec
49 page-faults # 0.001 K/sec
1542,8953,8117 cycles # 4.150 GHz
4501,7560,0779 instructions # 2.92 insn per cycle
756,0705,1347 branches # 2033.667 M/sec
4,8374,4583 branch-misses # 0.64% of all branches
37.178599565 seconds time elapsed
37.177932000 seconds user
0.000000000 seconds sys
```
```bash
Performance counter stats for './bitwise':
5,3985.27 msec task-clock # 1.000 CPUs utilized
167 context-switches # 0.003 K/sec
1 cpu-migrations # 0.000 K/sec
50 page-faults # 0.001 K/sec
2241,7260,7612 cycles # 4.152 GHz
6612,6007,9371 instructions # 2.95 insn per cycle
1122,7667,1320 branches # 2079.765 M/sec
4,7134,9005 branch-misses # 0.42% of all branches
53.986089902 seconds time elapsed
53.985592000 seconds user
0.000000000 seconds sys
```
## 4.
考慮到整數 `PAGE_QUEUES` 可能有以下數值:
$1,2,3,4,5,6,7,8\times (2^n)$, n from 1 to 14
給定 `size_t offset` ,試著將原本運算:
```cpp
#include <stdint.h>
size_t blockidx = offset / PAGE_QUEUES;
```
由於 `PAGE_QUEUES` 的數值分佈已知,在整數除法時,可思考加速運算的機制。
我們可預先進行計算,從而避免整數除法指令的使用: (假設在 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;` 效果的程式碼。
因 `PAGE_QUEUES` 為某數$\times 2^n$因此,可將整體$2^n$部分先行提出並先進行除法再除以剩下的數加速運算過程。
首先透過 `offset >> ffs32(divident)` 將, `PAGE_QUEUES` 數值中 $2^n$ 先行除去,再以 switch-case 除去剩下的部分,因$3, 5, 6, 7非2^n$,可提出,又 $6 可以拆為 2 \times 3$ 因此最終剩下$3, 5, 7$
CASES = `(b) 3`, `(d) 5`, `(f) 7`
### 延伸問題
`N-Queens` 問題是非常有名的題目,早在高中時期就聽過有這東西,不過懶散的我一直沒去深度鑽研這題目的奧妙,回歸正題首先我們知道這題有三個需要回傳的數值,分別是:
* `int *returnSize` : 代表回傳陣列 `ans` 的長度。
* `int **returnColumnSizes` : 代表回傳陣列 `ans[i]` , `0 < i < returnSize` 的長度。
* `char ***ans` : 回傳陣列本身。
此外還要求每個陣列都必須使用 `malloc` 取得,參考[RinHizakura](https://hackmd.io/@RinHizakura/BkoGM5V8v)大神的筆記可以知道, `n > 18` 時解的數量會超過 `int` 能容納的上限,因此可以將 `sizes[]` 先行列出以確保 `malloc` 到足夠的空間,避免重複呼叫 `realloc` 。
為了加速運算,使用 `Bitwise` 操作
* `upperlim` : 代表棋盤大小
* `vd` : 代表垂直上的限制
* `ld` : 代表右上到左下斜線上的限制
* `rd` : 代表左上到右下斜線上的限制
如下圖所示 藍色是 `ld` ,紅色是 `rd`透過由下往上不斷遞迴擺放,根據棋盤限制,窮舉出每種解並放入 `ans` 。
![](https://i.imgur.com/CKphPD8.png)
![](https://i.imgur.com/RqpY8bL.png)
```cpp
static int N;
static int sol;
static int upperlim;
static int sizes[] = {1, 1, 0, 0, 2, 10, 4,
40, 92, 352, 724, 2680, 14200, 73712,
365596, 2279184, 14772512, 95815104, 666090624};
static char ***ans;
static char **ret;
//vd: Vertical
//ld: Left slash
//rd: Right slash
void solve(int level, int vd, int ld, int rd){
if (level == N){
for (int i = 0; i < N; i++)
memcpy(ans[sol][i], ret[i], N + 1);
sol++;
return;
}
int limit = (vd | ld | rd) ^ upperlim;
int pos;
while(limit){
pos = limit & (-limit); //LSB
limit = limit & (limit - 1); //cutLSB
ret[level][N - __builtin_ffs(pos)] = 'Q';
solve(level + 1, vd | pos, ((ld | pos) >> 1) & upperlim, ((rd | pos) << 1) & upperlim);
ret[level][N - __builtin_ffs(pos)] = '.';
}
}
char *** solveNQueens(int n, int *returnSize, int **returnColumnSizes){
*returnSize = sizes[n];
*returnColumnSizes = malloc(sizes[n] * sizeof(int));
for (int i = 0; i < sizes[n]; i++)
(*returnColumnSizes)[i] = n;
N = n;
sol = 0;
upperlim = 1;
ans = malloc(sizes[n] * sizeof(char **));
for (int i = 0; i < sizes[n]; i++)
ans[i] = malloc(n * sizeof(char *));
for (int i = 0; i < sizes[n]; i++){
for(int j = 0; j < n; j++)
ans[i][j] = malloc((n + 1) * sizeof(char)); // 1 for '\0'
}
ret = malloc(n * sizeof(char *));
for(int i = 0; i < n; i++){
ret[i] = malloc((n + 1) * sizeof(char));
for(int j = 0; j < n; j++)
ret[i][j] = '.';
ret[i][n] = '\0';
}
upperlim = (upperlim << n) - 1;
solve(0, 0, 0, 0);
for (int i = 0; i < n; i++)
free(ret[i]);
free(ret);
return ans;
}
```
![](https://i.imgur.com/q5AmMDl.png)
[Leetcode Runtime Error](https://support.leetcode.com/hc/en-us/articles/360011834174-I-encountered-Wrong-Answer-Runtime-Error-for-a-specific-test-case-When-I-test-my-code-using-this-test-case-it-produced-the-correct-output-Why-)
另外比較值得注意的是如果有使用全域或以靜態宣告的變數時,必須在==每次進入函數==時對其初始化,否則就會遇上![](https://i.imgur.com/AjWWEdE.png)等問題,因為 `Leetcode` 使用同一個實體對所有 `test case` 做測試,而這些變數會影響到下個 `test cas` 的運行結果。