# [2020q3](http://wiki.csie.ncku.edu.tw/sysprog/schedule) 第 15 週測驗題 ###### tags: `sysprog2020` :::info 目的: 引導學員挑戰 LeetCode 和複習記憶體管理機制 ::: ==[作答表單](https://docs.google.com/forms/d/e/1FAIpQLSdIhhVeIOLInSPt_1TFjc92TamE4ZuqP4vbQyG6YCBWHzBjQw/viewform)== --- ### 測驗 `1` LeetCode [835. Image Overlap](https://leetcode.com/problems/image-overlap/) 給定二個影像 A 和 B,二者皆為大小相同的二維正方形矩陣,內部以二進位表示。我們轉換其中一個影像,向左、右、上,或下滑動任意數量的單位,並將其置於另一個影像之上,隨後,該轉換的重疊 (overlap) 指二個影像都有 1 的位置數量,注意:轉換不包含向任一方向旋轉。於是,最大可能的重疊為何? 範例 1: ![](https://i.imgur.com/OnF5wBs.png) * 輸入 $$ A = \left( \begin{array}{ccc} 1 & 1 & 0 \\ 0 & 1 & 0 \\ 0 & 1 & 0 \\ \end{array} \right) $$ $$ B = \left( \begin{array}{ccc} 0 & 0 & 0 \\ 0 & 1 & 1 \\ 0 & 0 & 1 \\ \end{array} \right) $$ * 輸出: * `3` * 解釋 * 將 A 向右移動 1 個單位,然後向下移動 1 個單位 ![](https://i.imgur.com/ONuIxjj.png) * 二張影像中都存在 `1` 的位置總數為 `3`,見下圖紅色標記 ![](https://i.imgur.com/N00Zxg7.png) 思路:找出所有 1 的位置,對兩個矩陣的所有這些位置進行求差,再統計這些差出現最多的次數。兩個坐標的差表示什麼?即是將其中一個坐標移動到另一個坐標需要移動的向量。於是,在走訪個別單元的過程中,我們找出 `A` 中所有值為 `1` 的坐標,移動到 `B` 中所有值為 `1` 的坐標需要移動的向量。於是乎,在這些向量中出現次數最多的向量,就是我們要求的整個矩陣應該移動的向量。 以下程式碼針對 LeetCode [835. Image Overlap](https://leetcode.com/problems/image-overlap/),提出可能的解法: ```cpp #include <string.h> #define max(a, b) (((a) > (b)) ? (a) : (b)) static inline int overlap_count(unsigned int *AA, unsigned int *BB, int size) { int res = 0; for (int i = 0; i < size; ++i) res += __builtin_popcount(AA[i] & BB[i]); return res; } static void translate(unsigned int *v, int vsize, int i, int j, unsigned int *res) { memcpy(res, v, sizeof(unsigned int) * vsize); int mask[32]; for (int k = 0; k < 32; k++) mask[k] = (1U << k) - 1; int n = vsize; if (i >= 0) { for (int k = 0; k < n; k++) res[k] >>= i; } else { i = -i; for (int k = 0; k < n; k++) res[k] = ((res[k] << i) & mask[n]); } if (j >= 0) { ITER1; for (int k = 0; k < j; k++) res[k] = 0; } else { j = -j; ITER2; for (int k = n - j; k < n; k++) res[k] = 0; } } int largestOverlap(int **img1, int img1Size, int *img1ColSize, int **img2, int img2Size, int *img2ColSize) { int n = img1Size; unsigned int A[n], B[n]; memset(A, 0, sizeof(unsigned int) * n); memset(B, 0, sizeof(unsigned int) * n); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (img1[i][j] == 1) A[i] |= 1 << j; if (img2[i][j] == 1) B[i] |= 1 << j; } } int res = 0; for (int i = -(n - 1); i <= (n - 1); ++i) { for (int j = -(n - 1); j <= (n - 1); ++j) { unsigned int AA[n]; translate(A, n, i, j, AA); res = max(res, overlap_count(AA, B, n)); } } return res; } ``` 請補完程式碼,使其符合 LeetCode 題目要求。 參考資料: * [LeetCode 835. Image Overlap](https://www.cnblogs.com/grandyang/p/10355589.html) ==作答區== ITER1 = ? * `(a)` `for (int k = 0; k < n - j; k++) res[k + j] = res[k]` * `(b)` `for (int k = 0; k <= n - j; k++) res[k + j] = res[k]` * `(c)` `for (int k = n - j - 1; k >= 0; k--) res[k + j] = res[k]` * `(d)` `for (int k = n - j; k >= 0; k--) res[k + j] = res[k]` ITER2 = ? * `(a)` `for (int k = j; k < n; k++) res[k + j] = res[k]` * `(b)` `for (int k = j; k < n; k++) res[k - j] = res[k]` * `(c)` `for (int k = j - 1; k < n; k++) res[k + j] = res[k]` * `(d)` `for (int k = j - 1; k < n; k++) res[k - j] = res[k]` :::success 延伸題目: 1. 解釋上述程式碼運作原理 2. 提出效率更高,且空間佔用更少的 C 程式碼實作 ::: --- ### 測驗 `2` LeetCode [1569. Number of Ways to Reorder Array to Get Same BST](https://leetcode.com/problems/number-of-ways-to-reorder-array-to-get-same-bst/) 給定一個陣列 `nums` 表示 $1$ 到 $n$ 的排列,依據元素在 `nums` 中的順序,依次插入一個初始為空的二元搜尋樹 (BST)。請你統計將 `nums` 重新排序後,滿足以下條件的個數: * 重排後得到的 BST 與 `nums` 原本數值順序得到的 BST 相同。 例如,給定 $nums = [2,1,3]$,我們得到一棵 `2` 為 root、`1` 為 left 及 `3` 為 right 的樹。 ![](https://i.imgur.com/zp2bTKD.png) 陣列 $[2,3,1]$ 也能得到相同的 BST,但 $[3,2,1]$ 會得到一棵不同的 BST 。 由於答案可能會很大,請將結果對 $10^9 + 7$ 取餘數。 * 為何是 $10^9 + 7$ 呢?請見 [Modulo 10^9+7 (1000000007)](https://www.geeksforgeeks.org/modulo-1097-1000000007/) 範例 1 ![](https://i.imgur.com/zp2bTKD.png) * 輸入 * $nums = [2,1,3]$ * 輸出 * `1` * 解釋 * 我們將 `nums` 重排,$[2,3,1]$ 可得到相同的 BST * 沒有其他得到相同 BST 的方法 範例 2 ![](https://i.imgur.com/EQRaUTp.png) * 輸入 * $nums = [3,4,5,1,2]$ * 輸出 * `5` * 解釋 * 下面 5 個陣列可得到相同的 BST * $[3,1,2,4,5]$ * $[3,1,4,2,5]$ * $[3,1,4,5,2]$ * $[3,4,1,2,5]$ * $[3,4,1,5,2]$ 範例 3 ![](https://i.imgur.com/pHEpvcY.png) * 輸入 * $nums = [1,2,3]$ * 輸出 * `0` * 解釋 * 沒有其他排列順序能得到相同的 BST 範例 4 ![](https://i.imgur.com/KnFWUNa.png) * 輸入 * $nums = [3,1,2,5,4,6]$ * 輸出 * `19` 範例 5 * 輸入 * $nums = [9,4,2,1,3,6,5,7,8,14,11,10,12,13,16,15,17,18]$ * 輸出 * `216212978` * 解釋 * 得到相同 BST 的個數是 `3216212999` * 將其對 $10^9 + 7$ 取餘後得到 `216212978` 根節點是陣列第一個數,分為左右兩個子樹,左右子樹之間的順序若不變即可 假設左子樹 $L$ 長度 $nL$,右子樹 $R$ 長度 $nR$,則有效組合數為 $C_{nL+nR}^{nL} \times f(L) \times f(R)$ ```cpp #include <string.h> static const int MOD = 1e9 + 7; /* prime */ static int recursive(int *nums, int nums_size, int N, DECL) { if (nums_size <= 2) return 1; int left[N + 1], left_size = 0, right[N + 1], right_size = 0; for (int i = 1; i < nums_size; i++) nums[i] > nums[0] ? (right[right_size++] = nums[i]) : (left[left_size++] = nums[i]); int l = recursive(left, left_size, N, cache), r = recursive(right, right_size, N, cache); return (((long) cache[nums_size - 1][left_size] * l % MOD) * r) % MOD; } int numOfWays(int *nums, int N) { int comb[N + 1][N + 1]; /* combination table */ memset(comb, 0, sizeof(comb)); comb[0][0] = 1; for (int i = 1; i <= N; i++) { comb[i][0] = 1; for (int j = 1; j <= N; j++) INIT; } return recursive(nums, N, N, comb) - 1; } ``` 參考資訊: * [花花醬 LeetCode 1569](https://zxi.mytechroad.com/blog/recursion/leetcode-1569-number-of-ways-to-reorder-array-to-get-same-bst/) 注意:`DECL` 若是 `int **cache`,會在 [undefined behavior sanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) (UBsan) 的執行階段遇到 [Load of misaligned address](https://stackoverflow.com/questions/47619944/load-of-misaligned-address-and-ubsan-finding) 請補完程式碼,使其符合 LeetCode 題目要求。 ==作答區== DECL = ? * `(a)` `int cache[N][N]` * `(b)` `int cache[N - 1][N - 1]` * `(c)` `int cache[N + 1][N]` * `(d)` `int (*cache)[N + 1]` * `(e)` `int (*cache)[N]` INT = ? * `(a)` `comb[i][j] = (comb[i - 1][j] + comb[i - 1][j - 1]) % MOD` * `(b)` `comb[i][j] = (comb[i - 1][j] + comb[i - 1][j - 1])` * `(c)` `comb[i][j] = (comb[i][j] + comb[i - 1][j - 1]) % MOD` * `(d)` `comb[i][j] = (comb[i][j] + comb[i - 1][j - 1])` * `(e)` `comb[i][j] = (comb[i][j - 1] + comb[i - 1][j - 1]) % MOD` * `(f)` `comb[i][j] = (comb[i][j - 1] + comb[i - 1][j - 1])` :::success 延伸題目: 1. 解釋上述程式碼運作原理 2. 提出效率更高,且空間佔用更少的 C 程式碼實作 ::: --- ### 測驗 `3` 在 Ubuntu/Debian Linux 中,可執行 `sudo apt install dictionaries-common` 以安裝辭典套件,於是可得到 `/usr/share/dict/words` 檔案,後者包含超過 10 萬個單字/詞: ```shell $ wc -l /usr/share/dict/words ``` 考慮以下 C 程式 (`strsearch.c`) 可從給定的詞典中檢索: ```cpp /* * Hash table is represented as `htable` array of size N (N = number of lines in * dict), where each element either points to the end of a singly-linked list or * is equal to zero. Lists are stored in pre-allocated array `clist` of size N. * * This implementation is memory-efficient and cache-friendly, requiring only * 12N + O(1) bytes of "real" memory which can be smaller than the size of * dict, sacrificing however for request processing speed, which is * O(req_size) in most cases, but may be up to O(dict_size) due to hash collision. */ #include <fcntl.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> /* 2 additional bytes for '\n' and '\0' */ #define MAX_REQUEST_SIZE (128 * 1024 * 1024 + 2) struct htable_entry { uint32_t ptr; }; struct collision_list { uint32_t fpos; uint32_t len; uint32_t prev; }; struct compr_collision_list { uint32_t fpos; uint32_t len; }; typedef struct htable_entry htable_entry_t; typedef struct collision_list collision_list_t; typedef struct compr_collision_list compr_collision_list_t; static char *dict; static htable_entry_t *htable; static collision_list_t *clist; static compr_collision_list_t *cclist; static size_t num_of_bytes, num_of_lines; static size_t num_of_buckets; static int clist_size = 1; static uint32_t hash(char *s, size_t len) { uint32_t hash = 5381; for (size_t i = 0; i < len; i++) hash = ((hash << 5) + hash) + (s[i] - 32); return hash % num_of_buckets; } static void htable_insert(size_t fpos, size_t len) { uint32_t h = hash(dict + fpos, len); clist[clist_size].fpos = fpos; clist[clist_size].len = len; clist[clist_size].prev = htable[h].ptr; htable[h].ptr = clist_size; ++clist_size; } static bool htable_lookup(char *s, size_t len) { uint32_t h = hash(s, len); uint32_t ptr = htable[h].ptr; uint32_t nextptr = htable[h + 1].ptr; for (; ptr < nextptr; ptr++) { if ((len == cclist[ptr].len) && !memcmp(s, dict + cclist[ptr].fpos, len)) return true; } return false; } int main(int argc, char **argv) { if (argc != 2) { printf("usage: %s dictionary_file\n", argv[0]); return 2; } /* Map dictionary file directly to memory for easier access from program */ int dictfd; if ((dictfd = open(argv[1], O_RDONLY)) < 0) return 1; struct stat st; fstat(dictfd, &st); num_of_bytes = st.st_size; if ((dict = mmap(0, num_of_bytes, PROT_READ, MAP_PRIVATE, dictfd, 0)) == MAP_FAILED) return 1; /* Count number of lines in file */ for (size_t i = 0; i < num_of_bytes; i++) { if (dict[i] == '\n') num_of_lines++; } num_of_lines++; num_of_buckets = num_of_lines; htable = calloc(num_of_buckets + 1, sizeof(htable_entry_t)); clist = malloc((num_of_lines + 1) * sizeof(collision_list_t)); size_t prev_start = 0; for (size_t i = 0; i < num_of_bytes; i++) { if (dict[i] == '\n') { htable_insert(prev_start, P1); prev_start = P2; } } /* Compress collision list and update ptrs in htable accordingly */ htable[num_of_buckets].ptr = num_of_lines; cclist = malloc((num_of_lines + 1) * sizeof(compr_collision_list_t)); size_t clines = 0; for (size_t i = 0; i < num_of_buckets; i++) { uint32_t ptr = htable[i].ptr; htable[i].ptr = clines; while (ptr) { cclist[clines].fpos = clist[ptr].fpos; cclist[clines].len = clist[ptr].len; ptr = clist[ptr].prev; clines++; } } free(clist); /* Ready to accept requests */ char *req_buf = malloc(MAX_REQUEST_SIZE); while (!feof(stdin)) { fgets(req_buf, MAX_REQUEST_SIZE, stdin); size_t len = strlen(req_buf); /* Exit on "exit" command */ if (!strncmp(req_buf, "exit\n", len)) break; printf("%s: %s\n", req_buf, htable_lookup(req_buf, len) ? "YES" : "NO"); } free(req_buf); free(htable); free(cclist); munmap(dict, num_of_bytes); close(dictfd); return 0; } ``` 給定測試輸入 (檔名: `inputs`) 如下: ``` gnu linux kernel X XX exit ``` 測試方式: ```shell $ ./strsearch /usr/share/dict/words < inputs ``` 預期執行結果: ``` gnu : YES linux : NO kernel : YES X : YES XX : NO ``` 請補完程式碼。 ==作答區== P1 = ? * `(a)` `prev_start + i` * `(b)` `prev_start + i + 1` * `(c)` `i - prev_start` * `(d)` `i - prev_start + 1` P2 = ? * `(a)` `i` * `(b)` `i + 1` * `(c)` `i - 1` :::success 延伸題目: 1. 解釋上述程式碼運作原理 2. 提出效率更高,且空間佔用更少的 C 程式碼實作 ::: ---