# 2020q3 Homework4 (quiz4)
contributed by < `nelsonlai1` >
## 測驗 1
```c=
int hammingDistance(int x, int y) {
return __builtin_popcount(x OP y);
}
```
這題要算兩數間的 Hamming distance,而 XOR 後在原本相異的位元會變成 1,再用 popcount 就能算出 Hamming distance。
### 延伸問題
#### 1. 舉出不使用 gcc extension 的 C99 實作程式碼
```c=
int hammingDistance(int x, int y){
int result = 0;
for (int i = 0; i < 31; i++)
result += ((x >> i) & 1) ^ ((y >> i) & 1);
return result;
}
```
![](https://i.imgur.com/jE4s550.png)
#### 2. 練習 LeetCode 477. Total Hamming Distance
```c=
int totalHammingDistance(int* nums, int numsSize){
int result = 0, ones = 0;
for (int i = 0; i < 30 ; i++) {
for (int j = 0; j < numsSize; j++) {
ones += ((nums[j] >> i) & 1);
}
result += ones * (numsSize - ones);
ones = 0;
}
return result;
}
```
![](https://i.imgur.com/RM8xmqQ.png)
這題最直接的方法應該是計算每對數字的 Hamming distance 再加起來,不過這樣就會超時了,所以要換一種解法。
當兩個數在同一個位元上有差別時才會產生 Hamming distance,而當 x + y 個數在同一個位元上有 x 個 1,y 個 0 時,這個位元能造成的 Hamming distance 是 x * y。
題目敘述中提到陣列中的數不超過 10^9,所以只檢查 30 個位元。計算每個位元共有多少 1 跟 0,再相乘後加到結果中,就能得到答案。
#### 3. ECC & Hamming distance, Hamming weight
[錯誤更正碼簡介](https://w3.math.sinica.edu.tw/math_media/d184/18404.pdf)中提到在資料傳輸時,可能會有干擾訊號形成錯誤。而錯誤更正碼的精神是在傳輸前先對資料做處理,以這裡介紹的方法為例,是在原本的資料上加上幾個額外的 bits。如此一來,只要錯誤小於一定的數量,就能夠更正回來。
Hamming distance : 兩個數間不一樣的 bits 有幾個。
Hamming weight : 一個數裡面 1-bits 有幾個 (popcount)。
裡面提到 x 和 x' 的 Hamming distance 是 x + x' 的 Hamming weight,其實就是本題的做法(文章中的加法指的是 XOR 運算)
[Hamming codes, Part I](https://www.youtube.com/watch?v=X8jsijhllIA) 較容易理解,裡面提到一種叫做 Hamming code 的錯誤更正碼,可以改正一個錯誤 bits。
以 (15, 11)Hamming code (15 個 bits 中帶有 11 個有內容的 bits) 為例,
| 0 | 1 | 2 | 3 |
| - | - | - | - |
| 4 | 5 | 6 | 7 |
| 8 | 9 | 10| 11|
| 12| 13| 14| 15|
第 1, 2, 4, 8 bits 作為 redundant bits,檢查錯誤用,第 0 個 bit 無作用(或是在 extended Hamming code 中檢查整體的 1-bits 是否為偶數)。
第 1, 2, 4, 8 bits 的值依照底下這張圖的規則來填入值,分別計算 Q1 ~ Q4 中藍色區塊的 1-bits 數量並調整第 1, 2, 4, 8 bits 的值來讓每個藍色區塊中的 1-bits 為偶數。
![](https://i.imgur.com/tb1UZKh.png =200x)
當有一個錯誤發生時,便能透過檢查每個藍色區域中的 1-bits 數來找到錯誤發生的 bits。Q1, Q2 用來找到特定的 column,Q3, Q4 用來找特定的 row。
從圖中可以發現第 0 bit 不會被檢查到,所以無從判斷有沒有錯誤,為了解決第 0 bits 沒有用到的問題,extended Hamming code 會計算整體的 1-bits 數,並且調整第 0 bit 的值來讓整體的 1-bits 為偶數。這樣一來,如果有兩個或偶數個錯誤發生時,會觀察到整體的 1-bits 是偶數,但檢查藍色區域時會發現有錯,這時就能判斷傳輸時產生了偶數個錯誤。雖然不能更正錯誤,但可以發現錯誤並要求傳送端重新傳送。
[Hamming codes, Part II](https://www.youtube.com/watch?v=b3NxrZOu_CE) 延續 part I 的內容,並說明如何實際寫成程式碼。
![](https://i.imgur.com/l1OUznw.png =500x)
這張圖表示 0 ~ 15 (0000 ~ 1111) 可以透過每個 bit 是 1 或 0 來判斷位於上面 Q1 ~ Q4 的哪個位置,假設第 4 個 bits 是 1,則位於 Q1 的藍色區域,反之則位於褐色區域。其他 bits 以此類推。
當一段資料傳來時,將資料中的 1-bits 位置找出來,並且將其全部做 XOR,就可以得到有問題的那個 bits 的位置,如下圖 :
![](https://i.imgur.com/yusvQnd.png =500x)
而可以這樣做是因為 XOR 的特性,有偶數個 1-bits 時會輸出 0。
先從計算結果的第四個 bit 看起,第四個 bit 為 0 代表著 Q1 中藍色區域的 1-bits 是偶數,同時代表著有問題的位置在褐色區域,其他 bits 以此類推,所以這個結果正好是有問題的位置。
而如果現在要做 encode 也很類似,一樣將 1-bits 的位置做 XOR,接著調整第 1, 2, 4, 8 bits 的值來讓結果變成 0000。以影片中的 1010 為例子,將 1000 跟 0010 分別對 1 做 XOR(toggle 1 and 0) 就能讓結果變成 0000,因為如果要刪去一個做 XOR 的元素實際上也等同於再 XOR 一次,如圖所示 :
![](https://i.imgur.com/XXzu2tL.png =100x)
根據以上做法實作的 code :
解碼部分 :
```c=
uint16_t ecc(uint16_t n)
{
int flip = 0;
for (int i = 0; i < 16; i++) {
if ((n >> i) & 1)
flip ^= i;
}
return flip ? n ^ (1 << (15 - flip)) : n;
}
```
編碼 :
```c=
uint16_t encode(uint16_t n)
{
int mod = 0;
uint16_t result = n;
for (int i = 0; i < 16; i++) {
if ((n >> i) & 1)
mod ^= i;
}
while (mod != 0) {
result ^= (1 << (15 - (mod & -mod)));
mod ^= (mod & -mod);
}
return result;
}
```
main function :
```c=
int main()
{
uint16_t ori = 7777;
uint16_t noise, rec;
printf("original message : ");
for (int i = 12; i >=0; i--) {
if (i == 11 || i == 7)
continue;
printf("%d", (ori >> i) & 1);
}
printf("\n");
ori = encode(ori);
noise = ori ^ (1 << 2);
rec = ecc(noise);
printf("received message : ");
for (int i = 12; i >=0; i--) {
if (i == 11 || i == 7)
continue;
printf("%d", (rec >> i) & 1);
}
printf("\n");
}
```
結果 :
```
original message : 11101100001
received message : 11101100001
```
## 測驗2
>給你一棵樹,樹上有 n 個節點,編號自 0 到 n − 1。樹以父節點陣列的形式提供,其中
>`parent[i]` 是節點 i 的父節點。樹的根節點是編號為 0 的節>點。請設計
>`treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)`函式,回傳節點 node 的第
>k 個祖先節點。若不存在這樣的祖先節點,回傳 `-1`。樹節點的第 k 個祖先節點是從該節點到根
>節點路徑上的第 k 個節點
![](https://i.imgur.com/pL1Celk.png)
input 範例 :
```json
["TreeAncestor","getKthAncestor","getKthAncestor","getKthAncestor"]
[[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]]
```
其中 [-1,0,0,1,1,2,2] 代表第 0 到第 6 個 node 的 parent,也就是底下程式碼中的 `int *parent`
```c=
#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->max_level; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
`TreeAncestor` 這個 structure 中有兩個變數,分別是 `parent` 跟 `max_level`。
`parent` 是一個二維陣列,`parent [i][j]` 代表 i 這個 node 的第 $2^j$ 代祖先。
`max_level` 用來表示 j 的範圍大小,以 7 個節點為例,最大高度是 6,j 範圍是 0~2 ($6 = 2^1 + 2^2$),所以 `max_level` 是 3。
`TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize)` 中,
第 14~18 行將 `obj->parent` 做初始化,將每個都設成 -1。
19~20 行將輸入的 parent (第一代祖先)存在 `obj->parent[i][0]` 中。
22~31 行中,`obj->parent[i][j] = obj->parent[i][j - 1] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1];` 先判斷 i 節點 $2^{j-1}$ 代祖先是否不存在,不存在的話則 $2^j$ 代也不存在。反之 $2^j$ 代祖先則是 i 節點的 $2^{j-1}$ 代祖先的 $2^{j-1}$ 代祖先。
(這樣講可能有點拗口,可以想像你的曾曾祖父就是你的祖父的祖父。)
quit 則是來判斷若所有節點的第 $2^j$ 代祖先都不存在時,可以提前結束,因為之後的也會不存在。
有了以上概念後,`int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)` 也不難懂了。當要找 node 的第 k 個祖先時,會以類似二進位的方式來搜尋。如果要找第 7 代時,會以
node = node 第 $2^0$ 祖先 -> node = node 第 $2^1$ 祖先 -> node = node 第 $2^2$ 祖先的方式移動 node 最後找到目標節點。
### 延伸問題
在第 13 行計算 max_level 時多加了 1,是因為在第 22~31 行的迴圈使用 quit 作為終止條件,不這樣的話在 worst case (一條直線)下可能會存取到超過 array 範圍,然後就出錯了。因此第一個改動就是把第 22 行加上 `j < max_level`,就可以不用加 1 了。而這裡也可以順便把 `max_level` 這個變數直接用 `obj->max_level` 取代。
第二個可以改的地方是把 row, column 交換。原本需要 `malloc(n * sizeof(int *))` 和
`malloc(obj->max_level * sizeof(int))`,改完後變成 `malloc(obj->max_level * sizeof(int *))` 和 `malloc(n * sizeof(int))`。差別在於 int 大小是 4 bytes,而 int* 大小是 8 bytes。因為 max_level 會比 n 小,所以這樣就能省下一些空間。
另一個好處是可以把原本的
```
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
```
改成 `obj->parent[0] = parent;` 省下 for 迴圈的時間。
```c=
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));
for (int j = 0; j < n; j++)
obj->parent[i][j] = -1;
}
obj->parent[0] = parent;
for (int i = 1; i < obj->max_level; i++) {
int quit = 1;
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]];
if (obj->parent[i][j] != -1) quit = 0;
}
if (quit) break;
}
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/nRN1crp.png)
## 測驗3
```c=
#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) 的技巧判斷是否整除,當整除時回傳 1,其他則回傳 0。
length 用來決定要複製多長的字串,
當 i 是 3 或 5 的倍數,且非 15 的倍數時要複製四個字元 (Fizz or Buzz),
當 i 是 15 的倍數時要複製八個字元 (FizzBuzz),
其餘則複製兩個字元 (%u)。
所以這裡用 `length = (2 << div3) << div5;` 可以達到目的。
第 17 行可以從前面的敘述中知道,
當 i 是 3 的倍數時,start 要為 0。
當 i 是 5 的倍數且非 15 的倍數時,start 要為 4。
其餘時候 start 要為 8。
可以統整為,當有因數 3 時,MSG_LEN 要右移 3 “以上“,當有因數 5 時,要右移 1。所以 `KK1 = div5`, `KK2 = div3`, `KK3 = 2` 可以達到目的。
### 延伸問題
#### 1. 評估 naive.c 和 bitwise.c 效能落差
根據提示,我把 source code 改成以下 :
naive.c : (`if i % 15 == 0` 不用加)
```c=
int main() {
char buf[20];
for (unsigned int i = 1; i <= 100000000; i++) {
if (i % 3 == 0) sprintf(buf, "Fizz");
if (i % 5 == 0) sprintf(buf, "Buzz");
if ((i % 3) && (i % 5)) sprintf(buf, "%u", i);
}
return 0;
}
```
bitwise.c :
```c=
#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;
char buf[20];
int main(int argc, char *argv[]) {
for (size_t i = 1; i <= 100000000; 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';
sprintf(buf, fmt, i);
}
return 0;
}
```
結果 bitwise.c 的結果居然比較慢
```
Performance counter stats for './naive':
3584.62 msec task-clock # 1.000 CPUs utilized
18 context-switches # 0.005 K/sec
0 cpu-migrations # 0.000 K/sec
51 page-faults # 0.014 K/sec
148,7757,0325 cycles # 4.150 GHz
431,0389,3880 instructions # 2.90 insn per cycle
73,3560,5643 branches # 2046.408 M/sec
4543,6601 branch-misses # 0.62% of all branches
3.585121376 seconds time elapsed
3.584910000 seconds user
0.000000000 seconds sys
```
```
Performance counter stats for './bitwise':
5265.99 msec task-clock # 1.000 CPUs utilized
13 context-switches # 0.002 K/sec
0 cpu-migrations # 0.000 K/sec
49 page-faults # 0.009 K/sec
218,8159,0327 cycles # 4.155 GHz
642,2565,2212 instructions # 2.94 insn per cycle
110,4922,7345 branches # 2098.223 M/sec
4712,5830 branch-misses # 0.43% of all branches
5.266343370 seconds time elapsed
5.266260000 seconds user
0.000000000 seconds sys
```
## 測驗4
這題要加速運算 `size_t blockidx = offset / PAGE_QUEUES;` 這行指令。
其中 `PAGE_QUEUES` 可能的值是(1 or 2 or 3 or 4 or 5 or 6 or 7 or 8) × $2^n$, n from 1 to 14。
```c=
#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
}
```
`__builtin_ffs(x)` 這個 function 用來表示 x 二進位的最後一個 1 的位置。所以
`((__builtin_ffs(x)) - 1)` 的值等同於 x 的 trailing 0-bits。從第 5,6 行可以看到 blockidx 會先除以 divident(PAGE_QUEUES) 的 2^N 因數,接著 divident 也會把自己的 2^N 因數去除。
而最後的 `switch (divident)` 就是將其餘的因數分 case 做處理(也就是再除以剩下的因數)。由於 PAGE_QUEUES 的範圍是固定的,所以去除 2^N 因數後剩下的因數只有 3, 5, 7 而已。