--- tags: 進階電腦系統理論與實作, NCKU Linux Kernel Internals, 作業系統 --- # 2020q3 Homework8 (quiz8) contributed by < `RusselCK` > ###### tags: `RusselCK` > [ dynamic programming & bitwise 操作 & linked list & 非連續記憶體操作 練習題](https://hackmd.io/@sysprog/2020-quiz8) ## Q1. LeetCode [1494. Parallel Courses II](https://leetcode.com/problems/parallel-courses-ii/) Given * the integer `n` representing **the number of courses** at some university labeled from `1` to `n` * the array dependencies where `dependencies[i] = [xi, yi]` represents a **prerequisite relationship** * that is, the course `xi` must be taken before the course `yi`. * **In one semester you can take at most `k` courses** as long as you have taken all the prerequisites for the courses you are taking. Return **the minimum number of semesters** to take all courses. Example: ![](https://i.imgur.com/JLdYvVi.png) ``` Input: n = 5, dependencies = [[2,1],[3,1],[4,1],[1,5]], k = 2 Output: 4 ``` ### 遞迴解 (**`recursive-nos1.c`**) ```c= #include <string.h> #include <limits.h> #define min(a, b) (((a) < (b)) ? (a) : (b)) static void solve(int i, int s, int k, int n, int s0, int s1, int *dp) { if ((k == 0) || (i == n)) { dp[s0 | s] = min(dp[s0 | s], dp[s0] + 1); return; } solve(i + 1, s, k, n, s0, s1, dp); if ((s1 >> i) & 1) solve(i + 1, s | 1 << i, k - 1, n, s0, s1, dp); } 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; 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]; } ``` `#25` : `pre[cap]` : 課程 `cap` 的前導課程的 bit mask * `cap` = $2^n$ * 在 `pre[]` 中代表課程的 index ( 實際上只會用到 `0` ~ `n-1` ) * 在 `dp[]` 中代表已修過的課程的 bit mask * `#26` : (初始) 全為 `0` * for example : | `pre[0]` | `0...0 1110` | |:-----------:|:------------:| | `pre[4]` | `0...0 0001` | | `pre[o.w.]` | `0` | `#30` : ==`dp[cap]` : 完成`cap` 所需要的最少學期數== * `#31~33` : (初始) `dp[0] = 0`,其餘為 `INT_MAX` (infinity 的實作) * `s0` : 已修過的課程的 bit mask * `s1` : 目前可選擇的新課程 `#39~42` : 求`s1` * `((s0 >> i) & 1)` : 課程`i`是否已經修過了 * `(pre[i] & s0) == pre[i])` : 課程`i`的先修是否已經修完了 * `#42` : 若課程`i`滿足可選擇的條件,則加入`s1` `#44` : 從 `s1` 中找出至多 $k$ 門可選擇的課程,其 bit mask 為 `s` (從課程 `i = 0` 開始找起) > `solve(int i = 0, int s = 0, int k, int n, int s0, int s1, int *dp)` 若還有可以選擇課程的 quota 且 還有課程可以檢查 (即 `!((k == 0) || (i == n))`) : * `#12` : `solve(i + 1, s, k, n, s0, s1, dp);` >完成 dp 的精神 -- 建置表格 > 從 `s1` 中找出至多 $k$ 門可選擇的課程,其 bit mask 為 `s` (從課程 `i+1` 開始找起) * `#14~15` : 若 課程`i` 在 `s1` 的選項中 (即 `(s1 >> i) & 1`),則將課程`i`納入`s`,並減少一個可選擇的 quota (即 `k - 1`),接著找尋下一個可選擇的課程 (從課程 `i + 1` 開始找起) > `solve(i + 1, s | 1 << i, k - 1, n, s0, s1, dp);` 若沒有可以選擇課程的 quota 或 沒有課程可以檢查 (即 `((k == 0) || (i == n))`) : * `#8` : ==`dp[s0 | s] = min(dp[s0 | s], dp[s0] + 1);`== `#47` : `dp[cap - 1];` 即為修完所有課程至少需要的學期數 ### 非遞迴解 (**`iterative-nos1.c`**) ```c= #define min(a, b) (((a) < (b)) ? (a) : (b)) 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]; } ``` `#9` :`prerequisite[n];` : 課程 `n` 的前導課程的 bit mask * 因為題目限制 `1 <= n <= 15`,因此使用 `uint16_t` 就足夠了 `#15` : `dp[cap]` : 完成`cap` 所需要的最少學期數 * 因為題目限制 `1 <= k <= n <= 15`,因此使用 `uint8_t` 就足夠了 > 甚至還太多,4 bits 就可以了 * `#16` : `memset(dp, 1 << 6, cap);` * `1 << 6` 可以換成 `14`,只要是 bit mask 有效範圍以外的數值即可 > leetcode 的測試的答案最多只到 13 ,所以最小換到 `13` 也能過 :::warning - [x] 理論上要寫成 `memset(dp, 1 << 6, sizeof(dp));`,但為何 `memset(dp, 1 << 6, cap);` 也可以? * 因為 `sizeof(dp)` = `cap` * 8 bits = `cap` * 1 bytes = **`cap` bytes** ::: :::info * [**`void *memset(void *s, int c, size_t n);`**](https://www.man7.org/linux/man-pages/man3/memset.3.html) * fills the first `n` **bytes** of the memory area pointed to by `s` with the constant byte `c`. ::: `#20~24` : 求 `s1` (目前可選擇的新課程) * `i` = `s0` (某個已修過的課程的 bit mask) * `ready_for_take` = `s1` `#26~30` : 非遞迴版本的 `solve(int i = 0, int s = 0, int k, int n, int s0, int s1, int *dp);` * `#26` : `ready_for_take &= ~i;` : 在尚未修完的課程中,所有符合前導規定且可選擇的新課程的 bitmask * `#27` : 由 `j = ready_for_take & (j - 1)` 來一個一個找尋選課數量不超過 `k` 的 bitmask ### 改良版的非遞迴解 (**`iterative-nos2.c`**) ```c= #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)); // INIT = 0x5F 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]) // COND = !(i & (1 << 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]; } ``` `#20`: `memset(dp, INIT, sizeof(dp));` * 同前面所述,**INIT** 只要大於 `14` 即可 * 從選向上來看,**INIT = `0x5F`** `#23`: `count[cap];` : `cap` 代表的修課數量 :::warning * `#26~28` 換成 `count[i] = __builtin_popcount(i);` 可以更快 ::: `#37~40` : 求 `s1` (目前可選擇的新課程) * `b` = `s1` * `i` = `s0` (某個已修過的課程的 bit mask) * `j` (課程編號) * 做法與第一種方法相同,故 **COND = `!(i & (1 << j))`** `#41~42` : 若 `b` 可選的數量不超過 `k`,代表 `b` 挑出的課程都能用 1 學期就修完 :::warning * 這個 `if...else...` 可以省去第二種方法中一些多餘的執行步驟 ::: `#43~48` : 反之,就要從 `b` 中挑出至多 `k` 個課程的 bit mask `j` ## Q1. 進階題 ### LeetCode [1595. Minimum Cost to Connect Two Groups of Points](https://leetcode.com/problems/minimum-cost-to-connect-two-groups-of-points/) ## Q2. [Skip list](https://en.wikipedia.org/wiki/Skip_list)