---
tags: 進階電腦系統理論與實作
---
# 2020q3 Homework8 (quiz8)
contributed by < `RinHizakura` >
> [第 8 週測驗題](https://hackmd.io/@sysprog/2020-quiz8)
> [GitHub](https://github.com/RinHizakura/NCKU-System-Software-Quiz/tree/master/quiz8)
## 測驗 `1`
### 程式原理
解說遞迴版本的程式思維。
```cpp
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
const size_t cap = 1 << n;
int pre[cap];
memset(pre, 0, sizeof(pre));
for (int i = 0; i < dependenciesSize; i++)
pre[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
int dp[cap];
for (int i = 1; i < cap; i++)
dp[i] = INT_MAX;
dp[0] = 0;
```
* 初始化 `pre` 是根據課號 `dependencies[i][1]` 需要先修完課號 `dependencies[i][1]` 去設定 bitmask(-1 是因為課號會從 1 開始,但我們從第 0 bit 開始設定 bitmask)
* 初始化 `dp`
```cpp
for (int s0 = 0; s0 < cap; s0++) {
if (dp[s0] == INT_MAX)
continue;
int s1 = 0;
for (int i = 0; i < n; i++)
if (!((s0 >> i) & 1) && ((pre[i] & s0) == pre[i]))
s1 |= 1 << i;
solve(0, 0, k, n, s0, s1, dp);
}
return dp[cap - 1];
}
```
* 對照題目說明,`s0` 是一個 bitmask,表示當前可以選修的課程,數字的第 n 個 bit 為 0 表示考慮選修第 n 堂課
* `dp[n]` 表示要同時修過 n 中 bit 為 1 課程數需要多少學期
* 先思考 `s0 = 0` 的時候,所有的 bit 都為 0,這表示所有的課程都是可以考慮的,而事實上可以選得課無疑是那些不需要前置要求的
* 因此,內層的迴圈會調整 `s1` ,根據每堂課的前置是否已經修完(`(pre[i] & s0) == pre[i])`),以及 `s0` 限制考慮的課程(`!((s0 >> i) & 1)`)決定目前可以被選擇的課程
* 則對照 `solve()` 的實作, `if ((k == 0) || (i == n))` 是遞迴到已經選了足夠數量的課、或者考慮每一堂課的可選與否後,更新 `dp`
* `solve(i + 1, s, k, n, s0, s1, dp);` 表示判斷第 i + 1 堂課是否可選
* `if ((s1 >> i) & 1) solve(i + 1, s | 1 << i, k - 1, n, s0, s1, dp);` 表示判斷若第 i 堂課可選,且此時 k > 0,因此再找找還有沒有其他可選
* 下一個遞迴時,`s0 = 1` 等於是課號 1 已經修完,因此 1 以外都能選,如此遍歷所有狀態的解後,則 `dp[cap - 1]` 也就是所有課號的表示 bits 皆為 1 時的數值,就是我們所求
改寫為非遞迴版本的程式則如下。
```cpp=
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
uint16_t prerequisite[n];
memset(prerequisite, 0, sizeof(prerequisite));
for (int i = 0; i < dependenciesSize; ++i)
prerequisite[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
const int cap = 1 << n;
uint8_t dp[cap];
memset(dp, 1 << 6, cap);
dp[0] = 0;
for (uint32_t i = 0; i < cap; ++i) {
uint32_t ready_for_take = 0;
for (uint32_t j = 0; j < n; ++j) {
if ((prerequisite[j] & i) == prerequisite[j])
ready_for_take |= 1 << j;
}
ready_for_take &= ~i;
for (uint32_t j = ready_for_take; j; j = ready_for_take & (j - 1)) {
if (__builtin_popcount(j) <= k)
dp[i | j] = min(dp[i | j], dp[i] + 1);
}
}
return dp[cap - 1];
}
```
* 15 行前面的操作與遞迴版本大致相同
* 17 行的 for 迴圈中, `ready_for_take` 根據 `i` (對應遞迴版本的 `s0`) 以及 `ready_for_take &= ~i` 操作,調整成可以選的課為 1 的 mask
* 則 25 行的 for 迴圈會遍歷 `ready_for_take` 的所有可能子集合
* 所謂子集合是指維持 `ready_for_take` 為 0 的 bit,而為 1 的 bit 可以為 0 或 1 的所有排列組合,這是因為對於 `dp` 的更新只要考慮從可以選的課中選出其中幾堂是否可行即可
* 因此 `__builtin_popcount(j)` 等於是判斷當前的選擇 `j` 1 數量,對應選課數量,是否符合學期只能選 k 堂的規定
最後,來看一個優化的版本。
```cpp=
#define min(a, b) (((a) < (b)) ? (a) : (b))
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
const size_t cap = 1 << n;
/* pre[i]: the bit representation of all dependencies of course i */
uint16_t pre[cap];
memset(pre, 0, sizeof(pre));
for (int i = 0; i < dependenciesSize; i++)
pre[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
/* i is the bit representation of a combination of courses.
* dp[i] is the minimum days to complete all the courses.
*/
uint8_t dp[cap];
memset(dp, INIT, sizeof(dp));
dp[0] = 0;
uint8_t count[cap];
memset(count, 0, sizeof(count));
for (uint32_t i = 0; i < cap; i++) {
for (uint32_t j = 0; j < n; j++)
if (i & (1 << j))
count[i]++;
}
for (uint32_t i = 0; i < cap; i++) {
uint32_t b = 0;
/* figure out "b", the bit representation of the all courses
* that we can study now. Since we have i and pre[j], we know
* course j can be studied if i contains all its prerequisites.
*/
for (uint32_t j = 0; j < n; j++) {
if (COND && (pre[j] & i) == pre[j])
b |= 1 << j;
}
if (count[b] <= k) {
dp[i | b] = min(dp[i | b], dp[i] + 1);
} else {
for (uint32_t j = b; j; j = (j - 1) & b) {
if (count[j] == k)
dp[i | j] = min(dp[i | j], dp[i] + 1);
}
}
}
return dp[cap - 1];
}
```
* 因為 n 堂課在有解的情況下最多就修 n 個學期,而題目中說 n <= 15,所以只要把 `dp` 中的值(`dp[0]` 以外)初始化成大於 15 的任意數即可。因此 `INIT = (a) 0x5F`
* `count` 預先計算每個 bitmask 的 popcnt
* 所以 25 行的迴圈其實還可以透過以下的調整優化
```cpp
for (uint32_t i = 0; i < cap; i++) {
count[i] = __builtin_popcount(i);
}
```
* 31 行的 for 迴圈對應前兩個版本更新 `dp` 的外層迴圈
* 則 37 行依據前面的邏輯,可以知道 `b` 的作用就是可以選擇的課程對應 bit 為 1 的 mask,因此 `COND = !(i & (1 << j))`
* 41 行的判斷可以看到,如果當前的 mask `b` 1 數量沒有超過可選課的上限,其實並不需要再透過迴圈去檢查其子集合
* 想像 n = 15 、 k = 15 、 bitmask 有 15 個 1 的情況下,如果用上一個程式的邏輯,迴圈會執行 $2^{15}$ 個 iteration,但透過這個額外判斷式僅需要一行程式
### [1595. Minimum Cost to Connect Two Groups of Points](https://leetcode.com/problems/minimum-cost-to-connect-two-groups-of-points/)
先不思考極致的實作效率,嘗試寫出可以正確算出結果的程式碼。
解題的思路是:對於兩群的點,如果要滿足題中的 connected 條件,則對於每個點,勢必都會最少選中自己最小 cost 的 edge。因此,我們可以想成先針對 group 2,選出其中每個點的最小 edge。如此一來,我們可以先保證 group 2 中所有的點都已經有連到 group 1 中的某個點。接下來,就可以利用 dynamic programming 決定 group 1 中的點如何挑選 edge 了。
```c=
#define min(a, b) (((a) < (b)) ? (a) : (b))
int connectTwoGroups(int **cost, int costSize, int *costColSize)
{
// size of first group
const int size1 = costSize;
// size of second group
const int size2 = costColSize[0];
// size of mask
const int mask_size = (1 << size2) - 1;
int min_table[size2];
// init dp table with all zero
unsigned int dp[size1 + 1][mask_size + 1];
// for the first round, pick up all min edge for each node in group 2
for (int j = 0; j < size2; j++) {
int min = 200;
for (int i = 0; i < size1; i++) {
if (cost[i][j] < min) {
min = cost[i][j];
}
}
min_table[j] = min;
}
// then update dp table:
// count cost for each mask for choosing jth node
// which the jth bit of mask is set to 1
memset(dp[0], 0, sizeof(int) * (mask_size + 1));
for (int m = 0; m <= mask_size; m++) {
for (int j = 0; j < size2; j++) {
if (m & (1 << j))
dp[0][m] += min_table[j];
}
}
// now, we already find the edge which let all the node of group2 connect to
// group1 for the next step, we should check the node in group1 now
// for dp[i][m], it means the min cost when node from 1 to i in group 1
// connect with node in group2 of the mask's jth bit of mask is set to 1
for (int i = 1; i <= size1; i++) {
for (int m = 0; m <= mask_size; m++) {
dp[i][m] = INT_MAX;
for (int j = 0; j < size2; j++) {
// if j is already the min edge of node in group2, the min cost
// should consider the combination without node j in group add
// this edge
if (m & (1 << j)) {
dp[i][m] = min(dp[i][m],
cost[i - 1][j] + dp[i - 1][m & ~(1 << j)]);
}
// or we just consider add this edge directly
else {
dp[i][m] = min(dp[i][m], cost[i - 1][j] + dp[i - 1][m]);
}
}
}
}
return dp[size1][mask_size];
}
```
* 第 16 行中,計算 group2 每個點 `j` 的最小 edge,放進 `min_table`
* 18 行的 `int min = 200;` 是因為題中限制最大 cost 為 100,隨便設個比 100 大的數字即可
* `dp[i][m]` 所代表的是對於 group1 的第 1 到第 i 個點,與 bitmask `m` 中位元為 1 的所有 group2 點形成 connected 的最小 cost
* 則透過 `min_table` 首先更新 `dp[0][m]`,其函義為不考慮挑選 group1 哪些點的情況下,根據 bitmask `m` 選擇的點形成 connected 的最小 cost
* 因此 33 行的 `if (m & (1 << j))` 在判斷該 mask 第 j 個 bit (從 0 開始算) 是否為 1,得知該 mask 對應的點有哪些
* 43 行的迴圈中
* 如果已知從 group1 的第 1 到第 i - 1 個點與 mitbask `m` 中 bit 為 1 對應的 group2 中的所有點形成 connected 的最小值
* 思考加入 group1 中第 i 個點要形成 connected,表示要找到 group2 的一個點 j 形成連線:
* 如果在第 1 到第 i - 1 個點時,已經和第 j 個點形成 connected,則把第 j 個點上的 edge 拆掉,替換成第 i 個點連到第 j 個點,選擇這個 j 是否會有更小的 cost?
* 如果在第 1 到第 i - 1 個點時,尚未和第 j 個點形成 connected,則把第 i 個點連接到第 j 個點上,選擇這個 j 是否有更小的 cost?
* 把前面的兩個狀態寫成程式就是迴圈中的 if else 敘述
* 特別注意到對於 group1 中的點 i 是從 1 開始數,對於 group2 的點 j 則是從 0 開始數,只是為了撰寫程式方便
如下,為在 leetcode 上提交並正確的結果。

## 測驗 `2`
### 程式原理
逐行來看幾個關鍵 macro 的作用。
#### `LIST_INIT`
```cpp
#define LIST_INIT(d) \
do { \
LIST_ALLOC(d, sizeof(skiplist)); \
LIST_ALLOC(d->head, (sizeof(list_node_t) + ((sizeof(list_node_t *)) * \
(LIST_MAX_HEIGHT - 1)))); \
d->height = 0; \
} while (0)
```
* 初始化 skiplist 的結構 `d`
* 第一個 `LIST_ALLOC` 分配出 `d` 本身需要的空間
* 第二個 `LIST_ALLOC` 分配出 `d` 底下的 `*head`,注意到 `((sizeof(list_node_t *)) * (LIST_MAX_HEIGHT - 1))` 表示 `*head` 底下有 `(LIST_MAX_HEIGHT - 1)` 個不同高度的 `list_node_t`
#### `LIST_INSERT`
```cpp
#define LIST_INSERT(d, k, l, v) \
do { \
custom_t vl; \
list_node_t *u[LIST_MAX_HEIGHT]; \
LIST_FIND(d, k, l, vl, u); \
if (vl) \
break; \
int h; \
LIST_HEIGHT(d, k, l, h); \
if (h > d->height) { \
HHH; \
u[h - 1] = d->head; \
} \
list_node_t *n; \
LIST_NODE_INIT(n, k, l, v, h); \
while (--h >= 0) { \
n->next[h] = u[h]->next[h]; \
u[h]->next[h] = n; \
} \
} while (0)
```
* 插入 key 為 `k`,key 的長度為 `l` 的數值 `v`
* 則首先透過 `LIST_FIND` 走訪 `d`,如果 `vl != 0` 表示該 key 已經存在,不能再 insert 相同的 key
* 否則透過 `LIST_HEIGHT` 計算出這個節點的 level 該為多少
* 如果 `h > d->height` 表示預設定的 height 已經超過目前的最大 height,則此時節點 level 是最大的 height + 1 即可,因此 `HHH = (d) h = ++(d->height)` (調整最大高度後,再用該高度初始化新節點)
* 否則就利用此高度產生對應空間的節點
* 此前的 `u` 紀錄著新節點對應每個 level 的 parent,因此對每個 level 去做插入即可
#### `LIST_FIND`
```cpp
#define LIST_FIND(d, k, l, v, u) \
do { \
list_node_t *iterator = d->head, **ud = u; \
for (int i = d->height - 1; i >= 0; --i) { \
while (iterator->next[i] && \
memcmp(k, iterator->next[i]->key, l) > 0) \
iterator = iterator->next[i]; \
if (ud) \
ud[i] = iterator; \
} \
list_node_t *n = iterator->next[0]; \
v = (n && l == n->key_len && !memcmp(k, n->key, l)) ? n->val : 0; \
} while (0)
```
* `iterator` 從 `d->head` 開始搜尋
* 從最高的 level (`d->height`) 開始,檢查走訪到的節點是否為 null,且比較該節點的 key 是否大於要找的 key
* 如果是,則往同一層的下一個節點繼續走訪
* 如果 `u` 非 NULL,則把這層走到的最後一個節點放進 ud 中
* 遞迴走訪直到最後一層
* 則判斷最後一個走訪的節點是否存在、且 key 長度等於要搜尋的 key 長度、內容也相同
* 如果是,把 `v` 設成該 key 對應之 value
* 否則把 `v` 設為 0
#### `LIST_ITERATE`
```cpp
#define LIST_ITERATE(d, callback) \
do { \
assert(callback); \
list_node_t *iterator = d->head; \
while (iterator->next[0]) { \
III; \
} \
} while (0)
```
* iterate 的部份就很單純,因為遞迴 level 0 就可以走訪所有節點,而透過 `callback` 去操作每個節點,因此 `III = iterator = iterator->next[0]; callback(iterator)`
### [1206. Design Skiplist](https://leetcode.com/problems/design-skiplist/)
這題要求設計 skiplist 一系列的 function,我們可以根據原測驗題的思維,調整至符合題目的要求。
```cpp
#define MAX_HEIGHT 8
typedef struct __list_node {
uint8_t height;
int val;
int ref;
struct __list_node *next[1];
} list_node_t;
typedef struct {
uint8_t height;
list_node_t *head;
} Skiplist;
Skiplist* skiplistCreate() {
Skiplist *l = malloc(sizeof(Skiplist));
l->height = 0;
int s = sizeof(list_node_t) + ((sizeof(list_node_t *)) * (MAX_HEIGHT));
list_node_t *n = malloc(s);
memset(n, 0, s);
n->val = -1;
l->head = n;
return l;
}
int _skiplistSearch(Skiplist* obj, int target, list_node_t **u){
list_node_t *iterator = obj->head, **ud = u;
for (int i = obj->height - 1; i >= 0; --i) {
while (iterator->next[i] && iterator->next[i]->val < target)
iterator = iterator->next[i];
if (ud)
ud[i] = iterator;
}
list_node_t *n = iterator->next[0];
if(!n)
return -1;
if(n->val == target){
return target;
}
return -1;
}
bool skiplistSearch(Skiplist* obj, int target) {
if (_skiplistSearch(obj, target, NULL) >= 0)
return true;
return false;
}
void skiplistAdd(Skiplist* obj, int num) {
list_node_t *u[MAX_HEIGHT];
int search = _skiplistSearch(obj, num, u);
if (search != -1){
u[0]->next[0]->ref++;
return;
}
// random height
int h = random() % (MAX_HEIGHT) + 1;
if(h > obj->height){
h = ++(obj->height);
u[h - 1] = obj->head;
}
list_node_t *n;
n = malloc(sizeof(list_node_t) + ((sizeof(list_node_t *)) * h));
n->height = h;
n->val = num;
n->ref = 1;
while (--h >= 0) {
n->next[h] = u[h]->next[h];
u[h]->next[h] = n;
}
}
bool skiplistErase(Skiplist* obj, int num) {
list_node_t *u[MAX_HEIGHT];
int search = _skiplistSearch(obj, num, u);
if (search == -1)
return false;
list_node_t *tmp = u[0]->next[0];
if(tmp->ref > 1){
tmp->ref--;
return true;
}
for (int i = obj->height - 1; i >= 0; --i) {
if(u[i]->next[i] == tmp){
u[i]->next[i] = u[i]->next[i]->next[i];
}
}
free(tmp);
return true;
}
void skiplistFree(Skiplist* obj) {
list_node_t *iterator = obj->head;
if(iterator->next[0])
iterator = iterator->next[0];
else
goto FREE;
while (iterator) {
list_node_t *tmp = iterator->next[0];
free(iterator);
iterator = tmp;
}
FREE:
free(obj->head);
free(obj);
}
```
* `skiplistCreate` 建立所需要的空間,包含 skiplist 本身以及其 head node
* `_skiplistSearch` 類似原題目的 find 設計,會搜尋 target 並且把 target 節點在每一個 level 的 parent 記錄起來
* 搜尋失敗回傳的是 -1,是因為節點的值有可能是 0,需區分開來
* 則 `skiplistSearch` 會透過 `_skiplistSearch` 進行搜尋,根據題意,找到回傳 true,否則回傳 false
* `skiplistAdd` 的結構則類似原測驗題,這裡就不多解釋
* 值得注意的是額外的 `ref` 欄位操作,這是因為題目中允許插入相同值的節點
* 我們當然也可以重新建立一個節點並找尋合適的位置插入,但可能對於空間與時間都不是很好的選擇
* 因此,對於重複值的插入,這裡的方法是僅將 reference count 加一,而不產生額外節點
* `skiplistErase` 只是把 `skiplistAdd` 的行為反過來思考,找出節點該在的位置,並判斷是否確實存在
* 如果存在,但 reference count >= 2,表示這次的 erase 不必刪除節點,因為還有其他的
* 否則就刪除,方法是將刪除目標在每一個 level 的 parent 和 child 接在一起,然後釋放刪除目標的節點
為了比較 Skiplist 的效益,我把之前 [quiz1](https://hackmd.io/@RinHizakura/BysgszHNw) 的內容稍做改寫,並加入測試的機制,先確認無論是 Skiplist 或是改寫的一般 linked-list 的行為符合預期後,進一步測試兩者的效率。(詳細程式請參考 [RinHizakura/Skiplist](https://github.com/RinHizakura/Skiplist))