--- tags: Linux2020 --- # 2020q3 Homework4 (quiz4) contributed by < `YLowy` > ## [測驗 1](https://hackmd.io/@sysprog/2020-quiz4#%E6%B8%AC%E9%A9%97-1) [Hamming Distance](https://leetcode.com/problems/hamming-distance/) 兩個整數間的 Hamming distance 為其二進位的每個位元的差 Example : 1 (0 0 0 1) 4 (0 1 0 0) hamming distance = 2 | A $\oplus$ B | B = 0 | B = 1 | | -------- | -------- | -------- | | A = 0 | 0 | 1 | | A = 1 | 1 | 0 | `A ^ B` 就為二進位中兩數字的每個位元的差,再透過 popcount() 就可以得到答案 ```cpp int hammingDistance(int x, int y) { return __builtin_popcount(x ^ y); } ``` ### 延伸問題 1. 解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼 其實看到這題挺眼熟的,原來 3 個月前有碰到過 ![](https://i.imgur.com/1WwNKSo.png) 當時寫法也是相似不過當時還不知道 gcc extension popcount() ```c= int hammingDistance(int x, int y){ unsigned int Z; Z=x^y; int result= 0,i; for(i=0;i<32;i++){ result+= (Z>>i)&1; } return result; } ``` 透過計數每一個位元得到結果 3. 練習 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時 一開始想法很簡單,兩兩互相取 Hamming Distance 再除以 2 ```c= int totalHammingDistance(int* nums, int numsSize){ int total = 0,i,j; for(i=0;i<numsSize;i++) for(j=0;j<numsSize;j++) total +=__builtin_popcount(nums[i] ^ nums[j]); return total>>1; } ``` 不過超出時間 ![](https://i.imgur.com/pUQakOM.png) 於是想說用減少一半計算量的方法修改 ```c= int totalHammingDistance(int* nums, int numsSize){ int total = 0,i,j; for(i=0;i<numsSize;i++) for(j=i;j<numsSize;j++) total +=__builtin_popcount(nums[i] ^ nums[j] ); return total; } ``` ![](https://i.imgur.com/XrXYtnK.png) 快,還需要再快 似乎從 pop_count 角度出發,時間複雜度就被限制在 O($n^{2}$) 透過觀察可以看看能不能從 " bit " 角度去觀察 Total Hamming Distance ```graphviz digraph A{ b [shape=record fontsize=24 label="{ n 'th bit|input 7| input 5| input 10|input 17 }|{ 4|0| 0| 0|1 }|{ 3|0| 0| 1 |0}|{ 2|1| 1| 0 |0}|{ 1|1| 0| 0 |0}|{ 0|1| 1| 1 |1}"] } ``` 1. 第 0 個 bit 觀察 : 全部都是 1 -> Hamming Distance = 0 2. 第 1 個 bit 觀察 : 1 有一個,其餘皆為 0 可以直接用單個 bit 判斷 Hamming Distance (1)* (numsize - 1) 3. 第 2 個 bit 觀察 : 1 有兩個,其餘皆為 0 Hamming Distance (2)* (numsize - 2) 以 bit 找規則似乎就找到了 : > 所有 Number 中從第 0 個 bit 開始計算全部該 bit 的 Hamming Distance ,在全部累加起來就是 Total Hamming Distance 之後程式改寫 : ```c= int totalHammingDistance(int* nums, int numsSize) { int bit_1_counter =0; int total=0; for(int i=0;i<32;i++){ bit_1_counter=0; // count how many '1' in nums[bit 'i'] in total for(int j=0;j<numsSize;j++){ bit_1_counter+=nums[j] & 1; nums[j]>>=1; } total+=bit_1_counter*(numsSize-bit_1_counter); } return total; } ``` ![](https://i.imgur.com/CacC7Xv.png) 4. 研讀[錯誤更正碼簡介](https://w3.math.sinica.edu.tw/math_media/d184/18404.pdf)及[抽象代數的實務應用](https://www.math.sinica.edu.tw/www/file_upload/summer/crypt2017/data/2017/%E4%B8%8A%E8%AA%B2%E8%AC%9B%E7%BE%A9/[20170710][%E6%8A%BD%E8%B1%A1%E4%BB%A3%E6%95%B8%E7%9A%84%E5%AF%A6%E5%8B%99%E6%87%89%E7%94%A8].pdf),摘錄其中 Hamming Distance 和 Hamming Weight 的描述並重新闡述,舉出實際的 C 程式碼來解說 >搭配閱讀 [Hamming codes, Part I](https://www.youtube.com/watch?v=X8jsijhllIA&feature=youtu.be&ab_channel=3Blue1Brown) 及 [Hamming codes, Part II](https://www.youtube.com/watch?v=b3NxrZOu_CE&feature=youtu.be&ab_channel=3Blue1Brown),你可適度摘錄影片中的素材,但應當清楚標註來源 Hamming Distance : 兩個向量中有多少 bit 不同 Hamming Weight : 向量中有多少 bit 為 '1' ( 非 0 ) 對於以此可以定義 minimum distance 以及 minimum weight 考慮到所有 Domain 中,最小值的 Hamming Distance 、Hamming Weight 會是相同的 在 (7,4) Hamming code 中 minimum distance , minimum weight 皆等於 3 並且 codeword 可有 7 個 在任何 Hamming code 中,如果想改其中 t 個錯誤,則該 它的 minimum distance 至少要2t+ 1 ![](https://i.imgur.com/hdh9do0.png) Hamming Distance 說明了 Hamming code 理論上改錯最大值 (15, 7) BCH code 想成 minimum distance = 5 的 Hamming code BCH code 實作上十分簡樸優雅,可以非常快速的找到更正出現的一個錯誤,並且可以偵測到如果出現兩個錯誤時的情況 ![](https://i.imgur.com/0CwzIQQ.png) 圖片來源 : [Hamming codes, h■w to ov■rco■e n■ise.](https://www.youtube.com/watch?v=X8jsijhllIA&feature=youtu.be&ab_channel=3Blue1Brown) 0 : 用來判斷錯兩個的情況 1 : {1,5,9,13} {3,7,11,15} 該兩行的錯誤更正碼,使其為偶數 2 : {2,6,10,14} {3,7,11,15} 該兩行的錯誤更正碼,使其為偶數 4 : {4,5,6,7} {12,13,14,15} 該兩行的錯誤更正碼,使其為偶數 8 : {8,9,10,11} {12,13,14,15} 該兩行的錯誤更正碼,使其為偶數 #### 編碼 給定 11 bit 資料,封裝進此陣列中,在預設 0 , 1 , 2 , 4 , 8 皆為 0 下算出這些錯誤更正碼 uint16_t hammingcode = 0b000x0xxx0xxxxxxx (x 為資料 0 or 1) 以 1 為例 : bit 1 = Hamming Weight(hammingcode & 0b0101010101010101) mod 2 用 C 語言寫起來會長 : ```c= hammingcode |=(((__builtin_popcount(hammingcode & 0b0101010101010101)&1))<<14); ``` bit 2, 4, 8 也是類似方法,最後再由計算完的整體進行最後的錯誤更正碼並指派給 bit 0 #### 解碼 異常簡單,用 4 個判斷即可,讓我對這個編碼感到驚艷的是找到錯誤位置就是該四個判斷的分支,設計的非常優雅 現在收到一組資料 hammingcode 現在想判斷錯誤點的最低 bit : ```c= bool bit1 = ((__builtin_popcount(hammingcode & 0b0101010101010101)&1)?1:0; ``` 當然如果 4 個判斷都是 0 的情況下表示沒有錯誤 基本原理如上述所說,往後延伸應用包含像是分散式取block避免連續區域毀損、( 63 , 57 ) Hamming code #### Example [ LoRa-SDR/LoRaCodes.hpp ](https://github.com/myriadrf/LoRa-SDR/blob/master/LoRaCodes.hpp) LoRa 在物流網系統中為一種通訊系統,低耗電以及訊號覆蓋範圍大可以運用在中長距離通訊 [錯誤更正碼簡介](https://w3.math.sinica.edu.tw/math_media/d184/18404.pdf) 文中提到的(7,4)Hamming 實作如下 : ```cpp= /*********************************************************************** * Encode a 4 bit word into a 7 bits with parity. * Non standard version used in sx1272. **********************************************************************/ static inline unsigned char encodeHamming74sx(const unsigned char x) { auto d0 = (x >> 0) & 0x1; auto d1 = (x >> 1) & 0x1; auto d2 = (x >> 2) & 0x1; auto d3 = (x >> 3) & 0x1; unsigned char b = x & 0xf; b |= (d0 ^ d1 ^ d2) << 4; b |= (d1 ^ d2 ^ d3) << 5; b |= (d0 ^ d1 ^ d3) << 6; return b; } ``` ```cpp= /*********************************************************************** * Decode 7 bits into a 4 bit word with single bit correction. * Non standard version used in sx1272. * Set error to true when a parity error was detected **********************************************************************/ static inline unsigned char decodeHamming74sx(const unsigned char b, bool &error) { auto b0 = (b >> 0) & 0x1; auto b1 = (b >> 1) & 0x1; auto b2 = (b >> 2) & 0x1; auto b3 = (b >> 3) & 0x1; auto b4 = (b >> 4) & 0x1; auto b5 = (b >> 5) & 0x1; auto b6 = (b >> 6) & 0x1; auto p0 = (b0 ^ b1 ^ b2 ^ b4); auto p1 = (b1 ^ b2 ^ b3 ^ b5); auto p2 = (b0 ^ b1 ^ b3 ^ b6); auto parity = (p0 << 0) | (p1 << 1) | (p2 << 2); if (parity != 0) error = true; switch (parity) { case 0x5: return (b ^ 1) & 0xf; case 0x7: return (b ^ 2) & 0xf; case 0x3: return (b ^ 4) & 0xf; case 0x6: return (b ^ 8) & 0xf; case 0x0: case 0x1: case 0x2: case 0x4: return b & 0xF; } return b & 0xf; } ``` ![](https://i.imgur.com/wbbzAM4.png) 可以非常簡單看出 Hamming 應用 5. 研讀 [Reed–Solomon error correction](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction),再閱讀 Linux 核心原始程式碼的 [lib/reed_solomon](https://github.com/torvalds/linux/tree/master/lib/reed_solomon) 目錄,抽離後者程式碼為單獨可執行的程式,作為 ECC 的示範。 [Reed-Solomon Library Programming Interface](https://www.kernel.org/doc/html/latest/core-api/librs.html) [里德所羅門碼的編 / 解碼方式](https://csie.ntut.edu.tw/labaspl/edu/Convolution_RS.pdf) page 23 ~ 38 Reed–Solomon encode: 1. 編碼 : 如果想傳送 km 個 bit 也就是所謂 k 個 block ( m 個 bit 且Domain ∈ GF(${2^m}$) ) , 後面附上的 parity (額外附加) 可以協助修正傳送資料。而能修改的資料(block 數量) 的最大值為 $\dfrac {n-k}{2}$ 傳出的資料計算 : $S(x)= p(x) * {x^t}$ - $S_r$$(x)$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 3 | 2 | 1 | 382 | 191 | 487 | 474 | 2. 傳送過程中產生些錯誤 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 3 | 2 | 123 | 456 | 191 | 487 | 474 | 3. 接收端收到錯誤資料要進行兩件事 > 1. 找出錯誤位置 > The syndromes are calculated by evaluating r at powers of α > 2. 在錯誤位置找到正確的值 [Forney algorithm](https://en.wikipedia.org/wiki/Forney_algorithm) 方法將正確值帶回原本資料 參考 [Reed-Solomon Library Programming Interface](https://www.kernel.org/doc/html/latest/core-api/librs.html) 若是要實作該 de/encode 需要先 create 且 Inititalize ```cpp rs_decoder = init_rs (10, 0x409, 0, 1, 6); ``` 對照到 linux kernel [lib/reed_solomon/reed_solomon.c](https://github.com/torvalds/linux/blob/master/lib/reed_solomon/reed_solomon.c) init_rs 存在不同 initialize 行為 (init_rs_XXX ),但是均為帶入預設參數進 init_rs_internal 中 ***initrsinternal() 目的在於指派 rs control ,找尋對應的 codec 或者 建立的新的,一個系統中未必只由一種 codec*** 先定義 rs_codec 結構 ```cpp struct rs_codec { int mm; //徵狀值 (Syndromes) 每個 Syndromes 的bit 數 ,也決定GF(X) int nn; //block 有多少 Syndromes (= (1<<mm)-1) uint16_t *alpha_to; // 用以計算log uint16_t *index_of; // 用以取log uint16_t *genpoly; //生成函數 int nroots; //多少位額外的 parity ,同時也代表生成函數的 root 數量 int fcr; //First consecutive root, index form int prim; //Primitive element, index form int iprim; //prim-th root of 1, index form int gfpoly; //原本的生成函數 g(x) 已就是 parity 決定之函數 int (*gffunc)(int); //Function to generate the field, if non-canonical representation int users; //使用者,這邊我認為應該是對於使用的 Thread ,實作中有使用到 mutex_lock struct list_head list; // Linux Kernel 雙向鏈結的封裝,定義在linux/list.h中 }; ``` >>[Linux鏈結串列struct list_head 研究](https://myao0730.blogspot.com/2016/12/linux.html) 還挺特別的 >>![](https://i.imgur.com/5ELCzji.png) ```c= #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) ``` reed_solomon.c 有宣告此 List: ```c=53 static LIST_HEAD(codec_list); ``` ```c=231 static struct rs_control *init_rs_internal(int symsize, int gfpoly, int (*gffunc)(int), int fcr, int prim, int nroots, gfp_t gfp) { struct list_head *tmp; struct rs_control *rs; unsigned int bsize; /* Sanity checks */ if (symsize < 1) return NULL; if (fcr < 0 || fcr >= (1<<symsize)) return NULL; if (prim <= 0 || prim >= (1<<symsize)) return NULL; if (nroots < 0 || nroots >= (1<<symsize)) return NULL; ``` 上述檢查輸入合理性與否 ```c=248 /* * The decoder needs buffers in each control struct instance to * avoid variable size or large fixed size allocations on * stack. Size the buffers to arrays of [nroots + 1]. */ ``` [Reed-Solomon Library Programming Interface](https://www.kernel.org/doc/html/latest/core-api/librs.html) >The encoder calculates the Reed-Solomon code over the given data length and stores the result in the parity buffer. Note that the parity buffer must be initialized before calling the encoder. 故先在 create 與 initialize 中先設定好 ```c=255 bsize = sizeof(uint16_t) * RS_DECODE_NUM_BUFFERS * (nroots + 1); rs = kzalloc(sizeof(*rs) + bsize, gfp); if (!rs) return NULL; mutex_lock(&rslistlock); /* Walk through the list and look for a matching entry */ list_for_each(tmp, &codec_list) { struct rs_codec *cd = list_entry(tmp, struct rs_codec, list); ``` [kmalloc](https://www.kernel.org/doc/htmldocs/kernel-api/API-kmalloc.html) >bsize = sizeof(uint16_t) * RS_DECODE_NUM_BUFFERS * (nroots + 1) >sizeof(uint16_t) * RS_DECODE_NUM_BUFFERS = 64 ,剛好就是 64 位元架構的大小 ` RS_DECODE_NUM_BUFFERS //RS_DECODE_NUM_BUFFERS = 8 ` >void *kmalloc(size_t size, gfp_t flags) gfp -> get_free_pages rs = kzalloc(sizeof(*rs) + bsize, gfp) 可以將 buffer 與 rs 接在一起 kmalloc 是 kernel 中最常用的一種記憶體分配方式,它通過呼叫kmem_cache_alloc函數來實現。 de/encode 通常不會少量次數,在 cache 中肯定快些,但是要注意 cache page 大小 kzalloc 會在取得記憶體後預先填入 0 flags argument 運在在多種情況 EG: >>GFP_ATOMIC : 用於interrupt handler 或任何行程外環境的程式..絕不休眠 >>GFP_KERNEL : 一般核心記憶體配置..有可能休眠 >>... > mutex_lock(&rslistlock) 其他 Thread 若也想做 encode initialization 會被擋住,確保對於該初始化行為是正確不被干擾的 >list_for_each(pos, head) >`#define list_for_each(pos, head) for (pos = (head)->next; pos != (head); pos = pos->next)` LIST_HEAD 串接 codec 的 list ```cpp=265 if (symsize != cd->mm) continue; if (gfpoly != cd->gfpoly) continue; if (gffunc != cd->gffunc) continue; if (fcr != cd->fcr) continue; if (prim != cd->prim) continue; if (nroots != cd->nroots) continue; /* We have a matching one already */ cd->users++; rs->codec = cd; goto out; } ``` >找尋適訂的 codec 這邊他寫的條件判斷挺特別的,透過 continue 讓判別錯的回到 for判斷式找下一個 codec ```c=284 /* Create a new one */ rs->codec = codec_init(symsize, gfpoly, gffunc, fcr, prim, nroots, gfp); if (!rs->codec) { kfree(rs); rs = NULL; } out: mutex_unlock(&rslistlock); return rs; } ``` >如果一開始 for 迴圈就離開表示該 codec list 並未存在任何 codec ,就可以用原本尋找的條件自行創一個 codec >並且找到指定的 codec 之後 unlock mutex_lock >找到所需的 codec 再回傳 :::success 這裡的實作挺像物件導向設計模式的簡單工廠模式,有需要再做。 ::: 建立 rs_codec 以及初始化 ( 這裡幾乎看不懂了zzz ) ```c=70 static struct rs_codec *codec_init(int symsize, int gfpoly, int (*gffunc)(int), int fcr, int prim, int nroots, gfp_t gfp) { int i, j, sr, root, iprim; struct rs_codec *rs; rs = kzalloc(sizeof(*rs), gfp); if (!rs) return NULL; INIT_LIST_HEAD(&rs->list); ``` 對 cache 建立空間,並且初始化 LIST_HEAD ```c=81 rs->mm = symsize; rs->nn = (1 << symsize) - 1; rs->fcr = fcr; rs->prim = prim; rs->nroots = nroots; rs->gfpoly = gfpoly; rs->gffunc = gffunc; /* Allocate the arrays */ rs->alpha_to = kmalloc_array(rs->nn + 1, sizeof(uint16_t), gfp); if (rs->alpha_to == NULL) goto err; rs->index_of = kmalloc_array(rs->nn + 1, sizeof(uint16_t), gfp); if (rs->index_of == NULL) goto err; rs->genpoly = kmalloc_array(rs->nroots + 1, sizeof(uint16_t), gfp); if(rs->genpoly == NULL) goto err; ``` 對照一開始輸入的參數建立 codec 物件 ```c=101 /* Generate Galois field lookup tables */ rs->index_of[0] = rs->nn; /* log(zero) = -inf */ rs->alpha_to[rs->nn] = 0; /* alpha**-inf = 0 */ if (gfpoly) { sr = 1; for (i = 0; i < rs->nn; i++) { rs->index_of[sr] = i; rs->alpha_to[i] = sr; sr <<= 1; if (sr & (1 << symsize)) sr ^= gfpoly; sr &= rs->nn; } } else { sr = gffunc(0); for (i = 0; i < rs->nn; i++) { rs->index_of[sr] = i; rs->alpha_to[i] = sr; sr = gffunc(sr); } } ``` >[An Introduction to Galois Fields and Reed-Solomon Coding](https://people.cs.clemson.edu/~westall/851/rs-code.pdf) 先前提到的 GF(${2^m}$) >gfpoly (Generator polynomial) 代表其生成多項式 [參考來源](http://apwcs2014.nsysu.edu.tw/course/pdfdownload/95_2/%E9%8C%AF%E8%AA%A4%E6%9B%B4%E6%AD%A3%E7%A2%BC/CC-05-BCH.pdf) >若是一開始呼叫 init_rs_non_canonical 此 rs_control 則會使用到 gffunc >>gffunc : pointer to function to generate the next field element, or the multiplicative identity element if given 0. Used instead of gfpoly if gfpoly is 0 ```c=121 /* If it's not primitive, exit */ if(sr != rs->alpha_to[0]) goto err; /* Find prim-th root of 1, used in decoding */ for(iprim = 1; (iprim % prim) != 0; iprim += rs->nn); /* prim-th root of 1, index form */ rs->iprim = iprim / prim; ``` >不太了解 卡一個 for 在這裡 >prim : Primitive element, index form >iprim : prim-th root of 1, index form ```c=129 /* Form RS code generator polynomial from its roots */ rs->genpoly[0] = 1; for (i = 0, root = fcr * prim; i < nroots; i++, root += prim) { rs->genpoly[i + 1] = 1; /* Multiply rs->genpoly[] by @**(root + x) */ for (j = i; j > 0; j--) { if (rs->genpoly[j] != 0) { rs->genpoly[j] = rs->genpoly[j -1] ^ rs->alpha_to[rs_modnn(rs, rs->index_of[rs->genpoly[j]] + root)]; } else rs->genpoly[j] = rs->genpoly[j - 1]; } /* rs->genpoly[0] can never be zero */ rs->genpoly[0] = rs->alpha_to[rs_modnn(rs, rs->index_of[rs->genpoly[0]] + root)]; } ``` 計算 generator polynomial 可是我看不懂為啥是這樣 ```c=148 /* convert rs->genpoly[] to index form for quicker encoding */ for (i = 0; i <= nroots; i++) rs->genpoly[i] = rs->index_of[rs->genpoly[i]]; rs->users = 1; list_add(&rs->list, &codec_list); return rs; err: kfree(rs->genpoly); kfree(rs->index_of); kfree(rs->alpha_to); kfree(rs); return NULL; } ``` :::success initial_codec 有點... ::: **encode** ```c=315 #ifdef CONFIG_REED_SOLOMON_ENC8 /** * encode_rs8 - Calculate the parity for data values (8bit data width) * @rsc: the rs control structure * @data: data field of a given type * @len: data length * @par: parity data, must be initialized by caller (usually all 0) * @invmsk: invert data mask (will be xored on data) * * The parity uses a uint16_t data type to enable * symbol size > 8. The calling code must take care of encoding of the * syndrome result for storage itself. */ int encode_rs8(struct rs_control *rsc, uint8_t *data, int len, uint16_t *par, uint16_t invmsk) { #include "encode_rs.c" } EXPORT_SYMBOL_GPL(encode_rs8); #endif ``` >原本只有看到 encode_rs.c 裡面一個 {......} 連函數名稱都沒有,原來可以直接這樣用 lib/reed_solomon/encode_rs.c ```c=12 { struct rs_codec *rs = rsc->codec; int i, j, pad; int nn = rs->nn; int nroots = rs->nroots; uint16_t *alpha_to = rs->alpha_to; uint16_t *index_of = rs->index_of; uint16_t *genpoly = rs->genpoly; uint16_t fb; uint16_t msk = (uint16_t) rs->nn; ``` >檢查輸入 >invmsk (invert data mask) XOR on data, not on parity >這裡用意為辨別輸入為 RS 8 或者 RS 16 利用此種 bitwise 方式操作 >par 為 parity data ```c=22 /* Check length parameter for validity */ pad = nn - nroots - len; if (pad < 0 || pad >= nn) return -ERANGE; for (i = 0; i < len; i++) { fb = index_of[((((uint16_t) data[i])^invmsk) & msk) ^ par[0]]; /* feedback term is non-zero */ if (fb != nn) { for (j = 1; j < nroots; j++) { par[j] ^= alpha_to[rs_modnn(rs, fb + genpoly[nroots - j])]; } } /* Shift */ memmove(&par[0], &par[1], sizeof(uint16_t) * (nroots - 1)); if (fb != nn) { par[nroots - 1] = alpha_to[rs_modnn(rs, fb + genpoly[0])]; } else { par[nroots - 1] = 0; } } return 0; } ``` ... ## 測驗 2 [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/JI25RwH.png) input : ```c= ["TreeAncestor","getKthAncestor","getKthAncestor","getKthAncestor"] [[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]] ``` output ``` [null,1,0,-1] ``` C 程式實作 建構一個題目所提供的程式可以有兩種方式 : 1. 建立 tree 的時候一起建立個 table 以記錄該點以及每點父節點,優點是存取速度快 O(1) ,而缺點是存在節點上限,且不能無限動態新增 2. structure 裡面新增 parent_node 指向父節點,但在找尋時候需要逐一向上找,時間複雜度 worst case 為 O(n) , 而平均情況為 O( log n ) 本實作使用第一種 : 建立記錄節點的 table 首先要考慮其大小,在已知 n 個節點,在 worst case 情況下會形成一條 list , 在最差情況下每個 node 對應的 table 需要 n-1 個紀錄空間大小,也就是說理論上我們需要建立 n * ( n-1 ) 個 空間的 Table 但是考量到空間成本效益,除非是例如 Heap 這樣的資料結構,Table 空間利用率可能會很差,~~所以此處是利用 "只記錄第${2^x}$ 個祖先" 方式記錄在表中,剩下的透過計算算出的折衷方式。~~ ```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 *)); //上述這行可以先確定 parent 為 a pointer to pointer to integer 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; } ``` 這邊可以想像我們創建一個 table 以記錄每個點的 parent ,而每個 node 可以記錄 max_level 個 parents 取得空間後先在每個欄位預設填入 -1 | node | parent 1 | parent 2 | parent 3 | | ---- | -------- | -------- | -------- | | 0 | XX | XX | XX | | 1 | XX | XX | XX | | 2 | XX | XX | XX | | ... | XX | XX | XX | | n | XX | XX | XX | --- ```c=20 for (int i = 0; i < parentSize; i++) obj->parent[i][0] = parent[i]; ``` 這邊將每個 node 所對應的 table 第一個位置填入第一個父節點 ```c=22 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; } ``` ~~22-31 for 迴圈中主要是填入每個節點的 ${2^j}$ 個parent node ~~ 值得注意 25 行 ```c=25 obj->parent[i][j] = obj->parent[i][j + (-1)] == -1 ? -1 : obj->parent[obj->parent[i][j - 1]][j - 1]; ``` ~~這邊是先檢查 第 ${2^{j-1}}$ 個祖先存在與否~~ | 第${2^0}$個祖先 | 第${2^1}$個祖先 | 第${2^2}$個祖先 | 第${2^3}$個祖先 | 第${2^4}$個祖先 | | ---------- | ---------- | ---------- | ---------- | ---------- | --- | --- | --- | --- |:---:| --- | | XXX | XXX | XXX | XXX | XXX | ~~挺特別的設計為第 ${2^{x+1}}$ 個祖先就是 第${2^x}$ 個祖先的第${2^x}$ 個祖先~~ 透過上述程式設計成為可以 O(1) 查詢的 Table ```c=35 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); } ``` 注意到這裡反而覺得特別,treeAncestorGetKthAncestor() 直接取 Table 的值 後來發現原本想法錯了,此處建立 table 時候會根據前面所建立的資料建立 table 所以並不需要親自探訪 也就是說 treeAncestorCreate() 裡面我們可以想說有張表格 | 第 X 個節點 | parent[X][0] | parent[X][1] | | -------- | -------- | -------- | | 0 | -1| -1 | | 1 | 0 | -1 | | 2 | 0 | -1 | | 3 | 2 | -1 | treeAncestorCreate() 建立時就會知道要放在哪個父點 (int *parent) 下,並把父點紀錄在 parent[X][0] 中,當要建立下個資料時,其實會透過知道本身父點位置再去找尋本身父點,用自己本身的父點建立祖先 Table 的值,有點像可是又不是動態規劃的方式。 透過這個方法可以讓 table 直接查對應值就可以達到找到的祖先點,而且確保為 Heap 結構情況時資料空間深度只需要 O(log n),而且 create table 實作速度快 O(1) ### 延伸問題 1. 在 `treeAncestorCreate` 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析 首先在 leetcode 中 "可能" 建立 tree 時候就確定為 Heap 架構了,如果考慮 tree 的 worst case 情況應該需要 n*(n-1) 的空間大小,而如果資料結構只存在為 heap ,我們可以透過計算階層計算算出該節點對應表格位置,畢竟像是 `obj->parent[0][i] ` i=1,2,3,... 都只會是 -1 ,而且沒意義。 原先 table | 第 X 個節點 | parent[X][0] | parent[X][1] | | -------- | -------- | -------- | | 0 | -1| -1 | | 1 | 0 | -1 | | 2 | 0 | -1 | | 3 | 1 | -1 | 我們可以想成 | Table[0] | Table[1] | Table[2] | Table[3] | Table[4] | Table[5] | Table[6] | Table[7] | | -------- | -------- | -------- | -------- | -------- | -------- | -------- |:--------:| | -1 | 0 | -1 | 0 | -1 | 1 | 0 | -1 | 以此類推 該 x 節點資料位置為Table[${1+2*2+3*2*2+4*2*2*2...}$] 而讀取到 -1 表示無父點 2. 在 LeetCode 1483. Kth Ancestor of a Tree Node 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者; 原先部分 ![](https://i.imgur.com/vFuXFWD.png) 其實也很明顯,最後的 "-1" 只是為了判斷停止點浪費了 改動部分 : int max_level = 32 - __builtin_clz(n); ... for (int j = 1; j<max_level; j++) obj->max_level = max_level; 更改完程式碼: ```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 *)); //上述這行可以先確定 parent 為 a pointer to pointer to integer 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]; for (int j = 1; j<max_level; 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; 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); } ``` ![](https://i.imgur.com/PoGtdcl.png) 改善空間結果連同時間一起改善了,此生或許再無悲喜 ## 測驗 3 [Fizz buzz](https://en.wikipedia.org/wiki/Fizz_buzz) 從 1 數到 n,如果是 3的倍數,印出 “Fizz” 如果是 5 的倍數,印出 “Buzz” 如果是 15 的倍數,印出 “FizzBuzz” 如果都不是,就印出數字本身 ```c = #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 題意: ``` string literal: "fizzbuzz%u" offset: 0 4 8 ``` 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; 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; } ``` ( FizzBuzz optimization)[https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82853] 分析 : i div3 div5 > length ``` 1 0 0 > 2 2 0 0 > 2 3 1 0 > 4 4 0 0 > 2 5 0 1 > 4 6 1 0 > 4 7 0 0 > 2 8 0 0 > 2 9 1 0 > 4 10 0 1 > 4 11 0 0 > 2 12 1 0 > 4 13 0 0 > 2 14 0 0 > 2 15 1 1 > 8 ``` length 決定該輸出字串長度 "%u" : 2 "fizz" : 4 "buzz" : 4 "fizzbuzz" : 8 故再來希望知道 " 從哪個位置開始 " `&"FizzBuzz%u" [0]` : 若 length = 4 => "Fizz" 若 length = 8 => "FizzBuzz" `&"FizzBuzz%u" [4]` : 若 length = 4 => "Buzz" `&"FizzBuzz%u" [8]` : 若 length = 2 => "%u" 考慮程式碼中,對於字串 "fizzbuzz%u" 的位移 ```c= &"FizzBuzz%u"[(MSG_LEN >> div5) >> (div3 << 2)] ``` 1. 非 3,5 倍數,offset = 8 (MSG_LEN >> 0) >> (0 << 2) = MSG_LEN >> 0 = 8 2. 為 5 倍數,但是非 3 的倍數,offset = 4 (MSG_LEN >> 1) >> (0 << 2) = MSG_LEN >> 1 = 4 3. 為 3 倍數,但是非 5 的倍數,offset = 4 (MSG_LEN >> 0) >> (1 << 2) = MSG_LEN >> 4 = 0 再透過 length 判斷是否為 15 倍數 ### 延伸問題 1. 評估 naive.c 和 bitwise.c 效能落差 fizzbuzz count 測試 每次讓 fizzbuzz 判斷 1000 數 (0-999) 各測試 100 次 naive.c: ![](https://i.imgur.com/7sE3joo.png) bitwise.c: ![](https://i.imgur.com/6y3PuJW.png) 挺意外的其實都差不多一樣,就算算更多次的數字其實也都差不多。 使用 bitwise 造成預期的效能提升並沒有出現。 2. 分析 [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/) 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless; 參照[fastmod: A header file for fast 32-bit division remainders on 64-bit hardware](https://github.com/lemire/fastmod) ## 測驗 4 offset / PAGE_QUEUES 考慮到整數 PAGE_QUEUES 可能有以下數值 : > 1. (1 or 2 or 3 or 4) > 2. (5 or 6 or 7 or 8) × (${2^n}$), n from 1 to 14 給定 size_t offset,試著將原本運算 : ``` #include <stdint.h> size_t blockidx = offset / PAGE_QUEUES; ``` 因為 PAGE_QUEUES 的數值最大可能是 8*${2^n}$ 所以若不進行化簡而直接做整數除法的話會運算的成本,因此我們可以使用 __builtin_ffs 將被除數與除數先行化簡 由於 PAGE_QUEUES 的數值分佈已知,在整數除法時,可思考加速運算的機制。 需要注意,某些硬體平台缺乏整數除法指令 (如 Arm Cortex-A8),即使 Intel 公司出品的 Core 處裡器 Sandy Bridge 微架構中,針對 64 位元整數的除法運算,會佔用 40 到 103 個處理器週期,運算成本仍屬沈重。 我們可預先進行計算,從而避免整數除法指令的使用: (假設在 x86_64 硬體執行 GNU/Linux 並透過 gcc/clang 編譯) ```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 } ``` ```c= case 2: blockidx /= 2; break; ``` 這樣的組合,請用最少的 case-break 敘述做出同等 `size_t blockidx = offset / PAGE_QUEUES;` 效果的程式碼。 >— Built-in Function: >int __builtin_ffs (unsigned int x) >Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero. ```c= for(int i=0;i<10;i++) printf("__builtin_ffs (%d) = %d \n ",i,__builtin_ffs (i)); ``` 先從 gcc 擴充函數了解 : ``` __builtin_ffs (0) = 0 __builtin_ffs (1) = 1 __builtin_ffs (2) = 2 __builtin_ffs (3) = 1 __builtin_ffs (4) = 3 __builtin_ffs (5) = 1 __builtin_ffs (6) = 2 __builtin_ffs (7) = 1 __builtin_ffs (8) = 4 __builtin_ffs (9) = 1 ``` 我們可以知道最後幾個 0 也就是可以得知該數含有 ${2^n}$ 的因數 這題目與 [Quiz 3 Question 4](https://hackmd.io/0dQsfZYVQaCf9H0_W2weQA#%E6%B8%AC%E9%A9%97-4) 觀念很像,再計算時可以先提取容易觀察計算的 ${2^n}$ | divident | binary | after __builtin_ffs(divident) | |:--------:|:------:|:-----------------------------:| | 1 | 0001 | 0001 = 1 | | 2 | 0010 | 0001 = 1 | | 3 | 0011 | 0011 = 3 | | 4 | 0100 | 0001 = 1 | | 5 | 0101 | 0101 = 5 | | 6 | 0110 | 0011 = 3 | | 7 | 0111 | 0111 = 7 | | 8 | 1000 | 0001 = 1 | 列舉每種可能之後會發現其實需要額外判斷的 case 其實只有 3, 5, 7 三數而已 ```c= case 3: blockidx /= 3; break; case 5: blockidx /= 5; break; case 7: blockidx /= 7; break; ``` ### 延伸問題 1. 解釋程式運算原理,可搭配 Microsoft Research 的專案 [snmalloc](https://github.com/microsoft/snmalloc) 來解說對應的原始程式碼 src/mem/sizeclass.h 2. 練習 LeetCode 51. [N-Queens](https://leetcode.com/problems/n-queens/),應選擇 C 語言並善用上述的 __builtin_ffs,大幅降低記憶體開銷 N-Queen Problem 算是挺經典的問題,不過過去一直沒有機會碰到 關於此題全部整理在 [YNQueens](https://hackmd.io/@YLowy/r1wIe9-DD)