# 2020q3: quiz4
###### tags: `sysprog-2020`
> [作業說明](https://hackmd.io/@sysprog/r1z0WPWLD)
> [題目](https://hackmd.io/@sysprog/2020-quiz4)
## 開發環境
```shell
$ uname -a
Linux jonec76-Aspire-M7720 5.4.0-47-generic #51-Ubuntu SMP GNU/Linux
```
## 參考解答
OP = ^
AAA = int **
BBB = -1
CCC = 1 << i
KK1 = div5
KK2 = div3
KK3 = 2
## Q1
- 解題想法
```c=1
int hammingDistance(int x, int y) {
return __builtin_popcount(x ^ y);
}
```
可以先觀察一下圖上的點,如果兩點之間 bit 中只有 1 位不相同的話,那可以知道兩個點的距離只差 1。根據這個原則,只要找出兩個數字有多少個 bit 不同即可。
那如何找出呢?這邊可以使用 XOR 找出,因為 XOR 的特性剛好可以使不同 bit (`0^1`)出來的結果是 1,若是相同的話就會變成 0。
接著,再使用 `__builtin_popcount` 計算出有多少個 1,即可知道答案了。
### 延伸問題
1. 解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼
閱讀了 [Other-Builtins](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 的說明文件,裡頭說到
> Built-in Function: int __builtin_popcount (unsigned int x)
Returns the number of 1-bits in x.
需要注意傳入的參數要為 `unsigned int` 的型態,改寫如下
1. 暴力法
遍歷所有的 bit 並且紀錄 1 的個數。
```c=1
int _popcount(unsigned int x) {
int count = 0;
while(x){
count += (x & 1);
x >>= 1;
}
return count;
}
```
2. 二補數減法特性
閱讀 [详解二进制数中1的个数](http://github.tiankonguse.com/blog/2014/11/16/bit-count-more.html) 裡頭提到的方法,覺得蠻神奇的。
在二補數做減 `1` 時,從最右邊的 `1` 值開始往右,`1` 會變 `0` 且 `0` 會變 `1` 。舉個例子,可以看到 `101100 - 1 = 101011`
利用這個特性,再將減去後的值 `101011` 回去跟原本的值 `101100` 做 `&`,就會消去最低位 `1` 變成 `0` 了。這樣的方法不像 (1) 一樣,要遍歷所有的 bit,時間複雜度是取決於該值有多少個 `1`
```c=1
int _popcount_v2(unsigned int x) {
int count = 0;
while(x){
x &= (x - 1);
count ++;
}
return count;
}
```
3. log 法
以算法 (2) 來說,時間複雜度會依據有多少個 1 而定,也就是說當所有 bit 幾乎為 1 時,複雜度會跟 (1) 的遍歷法是差不多的。log 法的想法,跟 merge sort 提到的 divide & conquer 的切法很像,差別在於所有的子問題可以同時處理(直接相加即可,不用遞迴各個子問題),bit n 數目則時間複雜度是 log(n)。
```c=1
int _popcount_log(unsigned int x) {
uint32_t mask[]= {0x55555555,0x33333333, 0x0F0F0F0F, 0x00FF00FF, 0x0000FFFF};
for(int i=0;i<5;i++)
x=(x & mask[i]) + ((x>>(1<<i)) & mask[i]);
return x;
}
```
使用 `mask` 是為了在兩值相加時將高位的項目(沒有用到的部分)移除,`mask` 的用法就是拿來將相鄰特定 bit 數目增加的,隨著不同的 iteration 有不同的相加方法,順序是相鄰的 1 -> 2 -> 4 -> 8 -> 16 位相加後,即可得到所有的 1 的個數。
2. 練習 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時
- Submission code
- [GitHub](https://github.com/Jonec76/sysprog2020-final/blob/master/477.c)
- Submission Detail
- runtims: 32ms (80.56 %)
memory: 6.7Mb (91.67 %)
![](https://i.imgur.com/rtjMr66.png)
- 解題想法
在 477 題中,要求算出所有數值之間兩兩 hamming distance 並且加總。
自己嘗試過使用暴力法,也就是使用 for 迴圈跑過所有的可能組合 ( 時間 $O(n^2)$ ),並對每個組合做 `XOR` 得到一值,計算該值中的所有 1 數目,此方法會使程式執行時間 time limit。
觀察後,得到一個新的想法。能使 total hamming distance 增加,只有當 bit 值為 `0` 對上 bit 值為 `1` 時。若在某特定 bit 值下,算出有 a 個值為 `1` 且 b 個值為 `0`,那 $a*b$ 就是該 bit 值下的 total hamming distance,而計算所有 bit 之後就可得到答案。這樣的時間複雜度為 : 跑過所有數 $O(n)$ $*$ 該陣列中最大值 `max_value` 的最高 bit 數 $O(log(max\_value))$ = $O(n * log(max\_value))$
- 程式碼優化
因為程式碼執行 `if/else` 的部分會有 branch prediction cost,而第一版的程式碼是希望算出 bit 值為 `0` 跟 `1` 的各自數目,如下
```c=1
if(nums[i] & 1)
bit_1_ctr++;
else
bit_0_ctr++;
// ...
total_ham_distance += bit_0_ctr * bit_1_ctr;
```
之後更改這部分,改成只計算 `1` 的數目
```c=1
bit_1_ctr += (nums[i] & 1);
// ...
total_ham_distance += (numsSize - bit_1_ctr) * bit_1_ctr;
```
在 runtime 上從 `38.89%` 上升到 `80.56%`
3. 研讀[錯誤更正碼](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) 及 [Hamming codes](https://www.youtube.com/watch?v=b3NxrZOu_CE&feature=youtu.be), Part II,你可適度摘錄影片中的素材,但應當清楚標註來源
---
## Q2
- 解題方向
在解 AAA 時,可以從下方這行程式碼得出
```c=1
obj->parent = malloc(n * sizeof(int *));
```
表示 `obj->parent` 是個具有 n 個 `int *` 的 pointer,如此即可知道應為 `int **` 型態。
在解 BBB 之前,可以先了解此題題目目的,參考下方程式碼
```c=1
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;
}
```
找出 `max_level` 的方法蠻有趣的,背後含義是做了 $\lfloor log\ n\rfloor$。一開始想說應該是想要找出 n 最大會在哪個 level,但是!之後再看一次題目才知道不是這樣子(需要配合 `treeAncestorGetKthAncestor` 來看)
這邊就是建立每個點的 parent table,用來記錄該點之上的 "幾個" parent(不是所有都紀錄!),並且對每個 node 的 parent table 做初始化。
```c=1
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
```
接著可以看到下方 BBB 的填空
```c=1
for (int i = 0; i < parentSize; i++) {
obj->parent[i][j] = obj->parent[i][j + BBB] == -1
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
if (obj->parent[i][j] != -1) quit = 0;
}
```
若是倒著推回來的話,可以先思考一下怎樣的情況,parent 會是 `-1`?
同時,又可以看到需要根據該點之外的其他 parent ,來更新目前的 parent。此時就可以推測出,如果在 level `j - 1` 已經是 `-1`,也就是沒有任何 node 在 level `j - 1` 之上,那 `j` 一定也不會有 parent。
接著,看到最後 CCC 選項
```c=1
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 & (CCC))
node = obj->parent[node][i];
return node;
}
```
上述程式碼蠻有趣的,是要拿第 k 個 parent。當初看到此題時,想說就是 n 往上數到第 k 個 node 並且回傳。但事實上並非如此,以下圖為例子
```graphviz
digraph skew_BST {
size="10,3!"
node [fontname="Arial"];
0 -> 1;
1 -> 2;
2 -> 3;
3 -> 4;
4 -> 5;
5 -> 6;
}
```
以例子來看,在 node 6 的 parent table 應為下圖,parent[i] 紀錄的是往上數第 $2^i$ 個點(如果存在)
```graphviz
digraph hash_table {
nodesep = .05;
rankdir = LR;
node[shape = record, width = .1, height = .1];
node0[label = "<f9> 6", height = 0.5];
node [width = 1.5];
node9[label = "{ 5|4|2}"];
node0:f9 -> node9;
}
```
why? 由下方程式碼可以看出
```c=1
for (int j = 1;; j++) {
int quit = 1;
for (int i = 0; i < parentSize; i++) {
obj->parent[i][j] = obj->parent[i][j + BBB] == -1
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
```
以 `parent[5][1]` (node 6 往上 2 格) 來看,那就會被更新為 `parent[4][0]` (node 5 往上 1 格)。
以 `parent[5][2]` (node 6 往上 4 格) 來看,那就會被更新為 `parent[3][1]` (node 4 往上 2 格)。
這樣的紀錄方法,將會使得 `parent[i]` 紀錄的是往上數第 $2^i$ 個點。對於這樣設計的原因,自己思考後是認為這樣的設計在處理 balanced tree 和 skewed tree 時都能減少 partent table size,不必將該點之上所有的 parent 都存入。
既然如此,再回到 CCC 選項。若 node=6, k=5 時,也就是要找出 node=6 往上數第 5 個的 node。這邊以 `treeAncestorGetKthAncestor` 來看,跑法會是
1. 取出 node 6 的第 $2^0$ parent,也就是 5
2. 5 再取出第 $2^2$ 點,也就是 0
打破原先那直觀的觀念(往上走 5 步),而是用走 "2 的冪次方" 的方法取點。
### 延伸問題:
1. 解釋上述程式碼運作原理,指出可改進的地方,並實作對應的程式碼;
- 優化記憶體
- [**演(1)**] 原始程式碼
Runtime: 288 ms (47.47 %)
Memory: 70.3 MB (10%,吊車尾超出排名圖表)
![](https://i.imgur.com/QIgM8qE.png)
可看出原始程式碼還有相當多的空間能夠進步,以下提出若干個調整
- [**演(2)**]
Runtime: 280 ms (63.16 %)
Memory: 68 MB (89.47 %)
![](https://i.imgur.com/SWAT81Z.png)
在 [**演(1)**] 中,分配記憶體的方式如下:
```c=1
int max_level = 32 - __builtin_clz(n) + 1;
for (int i = 0; i < n; i++) {
obj->parent[i] = malloc(max_level * sizeof(int));
// ...
}
```
可以看到每個節點的 `parent_size` 會被分配到 $\lfloor log\ n\rfloor$ 的大小空間。試想如果是一棵 skewed tree 的話,那會浪費相當多的空間(越靠近 root 的 node,depth 小但是一樣要分配 max depth 的空間),於是做了以下的優化:
若以 n=8 的 skewed tree 去設想 [**演(2)**] ,建出來的 table 如下表
```graphviz
digraph hash_table {
nodesep = .05;
rankdir = LR;
node[shape = record, width = .1, height = .1];
node7:f7 -> n7;
node6:f6 -> n6;
node5:f5 -> n5;
node4:f4 -> n4;
node3:f3 -> n3;
node2:f2 -> n2;
node1:f1 -> n1;
node0:f0 -> n0;
node0[label = "<f0> 0", height = 0.5];
node1[label = "<f1> 1", height = 0.5];
node2[label = "<f2> 2", height = 0.5];
node3[label = "<f3> 3", height = 0.5];
node4[label = "<f4> 4", height = 0.5];
node5[label = "<f5> 5", height = 0.5];
node6[label = "<f6> 6", height = 0.5];
node7[label = "<f7> 7", height = 0.5];
n0[label = "{-1}"];
n1[label = "{0}"];
n2[label = "{1|0}"];
n3[label = "{2|1}"];
n4[label = "{3|2|0}"];
n5[label = "{4|3|1}"];
n6[label = "{5|4|2}"];
n7[label = "{6|5|3|0}"];
}
```
可以看出,該圖每個點的 size 就是該點的最大 parent size,不可能找到其他 tree 比 skewed tree 的深度還要更大,若是依據這樣的想法,那可以節省掉 <br> **[演(1)]** 將所有點都分配了 $\lfloor log\ n\rfloor$ 的空間。
提交 [**演(2)**] 可以得到下方結果
- [**演(3)**]
Runtime: 264 ms (97.37%)
Memory: 65 MB (100%,超過圖表範圍)
![](https://i.imgur.com/Sc0BXwB.png)
[**演(2)**] 確實減少配置記憶體空間,但仍有能改善的地方,可以看到以下的例子
```graphviz
digraph skew_BST {
size="10,3!"
node [fontname="Arial"];
0 -> 1;
0 -> 2;
0 -> 3;
0 -> 4;
0 -> 5;
0 -> 6;
}
```
若用 [**演(2)**] 的方法配置記憶體,因為大部分的點 depth=1,在 node 2~6 會有些許的浪費,那是否還有辦法能節省這些空間呢?
於是在 [**演(3)**] 裡頭使用了 `depth` 表格,依據每個 node 的深度精確的配置大小。
```c=1
typedef struct {
int *depth; // For record each depth of node
int **parent;
int max_depth;
int n;
} TreeAncestor;
for (int i = 1; i < n; i++) {
obj->depth[i] = obj->depth[parent[i]]+1;
// ...
int node_level = 32 - __builtin_clz(obj->depth[i]);
obj->parent[i] = malloc(node_level * sizeof(int));
}
```
更新 table 的部份如下:
```c=1
int bit_idx = 1;
int max_bit = 32 - __builtin_clz(max_depth) + 1;
while(bit_idx < max_bit){
int start_idx = (1 << bit_idx);
for(int j=start_idx;j<parentSize;j++){
if(32 - __builtin_clz(obj->depth[j]) <= bit_idx)
continue;
obj->parent[j][bit_idx] = obj->parent[obj->parent[j][bit_idx - 1]][bit_idx - 1];
}
bit_idx++;
}
```
`bit_idx` 會從 1 開始增加,若 node 的深度 大於 $2^{bit\_idx}$ 則更新其 parent,否則就 `continue`。多出 `depth` 表格能夠省略掉設定 `-1` 的過程,因為每個 parent array 都是剛好符合該深度,不會有多餘要設為 `-1` 的過程。
而如果在 `treeAncestorGetKthAncestor` 要找的超過深度的話,再回傳 `-1` 即可
```c=
int max_depth = obj->max_depth;
if(obj->depth[node] < k)
return -1;
```
- 優化執行時間
在 [**演(1)**] 中,更新 `obj->parent` 的演算法中,由於 `obj->parent` 的空間很大 ($O(n * log(n))$),需要更新每格,程式碼如下
```c=1
for (int j = 1;; j++) {
int quit = 1;
for (int i = 0; i < parentSize; i++) {
// ...
}
}
```
而改善了空間配置之後,在更新 `obj->parent` 的程式碼如下
```c=1
int bit_idx = 1;
int max_bit = 32 - __builtin_clz(max_depth) + 1;
while(bit_idx < max_bit){
int start_idx = (1 << bit_idx);
for(int j=start_idx;j<parentSize;j++){
if(32 - __builtin_clz(obj->depth[j]) <= bit_idx)
continue;
obj->parent[j][bit_idx] = obj->parent[obj->parent[j][bit_idx - 1]][bit_idx - 1];
}
bit_idx++;
}
```
首先,改成跑過 `max_depth` 的 bit 數目就好,別跑過 `n` 的 bit 數。接著,若要更新 node i 的第 j 格 obj->parent[i][j],則 i 從 $2^j$ 開始跑,不要從 `0` 開始,因為只有 node $2^j$ 才有可能有深度為 `j` 的大小。
舉個例子:
```graphviz
digraph hash_table {
nodesep = .05;
rankdir = LR;
node[shape = record, width = .1, height = .1];
node4:f4 -> n4;
node3:f3 -> n3;
node2:f2 -> n2;
node1:f1 -> n1;
node0:f0 -> n0;
node0[label = "<f0> 0", height = 0.5];
node1[label = "<f1> 1", height = 0.5];
node2[label = "<f2> 2", height = 0.5];
node3[label = "<f3> 3", height = 0.5];
node4[label = "<f4> 4", height = 0.5];
n0[label = "{-1}"];
n1[label = "{0}"];
n2[label = "{1|0}"];
n3[label = "{2|1}"];
n4[label = "{3|2|0}"];
}
```
若要更新 `j=2`,那就從 $2^j=4$ 開始更新,為何呢?
因為在 node=4 之下的 node,不可能會有 `j=2` 的可能,所以這段可以省略掉 (但是 [**演(1)**] 是每次都從頭 node=0 更新,會浪費一些時間)
另外,在 `treeAncestorGetKthAncestor` 也能加速,在 [**演(1)**] 中會透過 k 的每個 bit 去找出來答案,但有些時候 k 其實比點的深度還要大,都要花一些時間遇到 `-1` 之後才知道。透過 `depth` 表格,能夠在一開始函式獲得 `k` 的時候就立刻判斷,如果 `k` 大於深度就可以直接回傳 `-1`,節省許多時間
```c=
int max_depth = obj->max_depth;
if(obj->depth[node] < k)
return -1;
```
2. 在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析;
3. 在 [LeetCode 1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者;
- Runtime: 264 ms (97.37%)
- Memory: 65 MB (100%,超過圖表範圍)
- [Github](https://github.com/Jonec76/sysprog2020-final/blob/master/1483.c)
![](https://i.imgur.com/Sc0BXwB.png)
---
## Q3
- 程式碼分析
```c=1
if (i % 3 == 0) printf("Fizz");
if (i % 5 == 0) printf("Buzz");
if (i % 15 == 0) printf("FizzBuzz");
if ((i % 3) && (i % 5)) printf("%u", i);
```
此題的原意其實很簡單,目的就是根據以上的程式碼印出。
接著此題希望使用 `strncpy` 的方式,來複製出原始字串的 substring。複製的參數是
1. start 字串的起始位置
2. 複製的長度
複製程式碼如下:
```c=1
#define MSG_LEN 8
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[start], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
```
- 解題想法
```c=1
strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);
```
此題基本上就是要把數字湊出來,在 [quiz2-Q4](https://hackmd.io/@jonec76/2020q3-quiz2#Q4) 中分析過 `is_divisible` 的原理。所以可以知道此題的 `div3` 、 `div5` 就會表示出目前的數可否被相對 `3` 或 `5` 整除,可以的話就得 `1`,否則 `0`。
以下為 output 合併 `strncpy` 參數的 table:
|value|output|start index|length|
|---|---|---|---|---|
|n 整除 3|"Fizz"|0|4|
|n 整除 5|"Buzz"|4|4|
|n 整除 15|"FizzBuzz"|0|8|
|以上皆非|n|0|0|
湊了幾組後即可得到答案。
這邊在課堂上寫的時候發生了粗心的情況,順便記錄一下。當時把 `div3 << 2` 跟 `2 << div3` 寫相反,當 `div3=1` 時的確值是一樣,但是缺少考慮了 `div3=0` 的 case。
## Q4
- 程式碼
```c=1
#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
}
```
- 解題想法
因為 `PAGE_QUEUES` 已知,此題希望能夠不要使用到除法,來完成 `size_t blockidx = offset / PAGE_QUEUES;` 的運算。
使用 `ffs32(x)` ,可以知道 1+(x 的最低 bit index)。若用數學式表達,可看到下方
$page\_queues = x * 2^i,\ where\ x\ is\ odd,\ i=0,1,...$
從所求分解回推:
$\ =(
{\dfrac{offset}{page\_queues}}
)$
$\ =(
{\dfrac{offset}{x * 2^i}}
)$
接著依據程式碼的算式
```c=1
blockidx = offset >> ffs32(divident);
divident >>= ffs32(divident);
```
可得:
$\ =(
{\dfrac{blockidx}{divident}}
)$
看到 `switch/case` 的要求,也就是找出所有 `divident` 的可能數,並且讓 `blocksize` 除以一個常數。
`divident` 的所有可能數,因為做了 `ffs32(divident)`,也就是對 0~8 的每個數除掉 2 的冪次方直到變奇數為止。所以所有可能為 `3`, `5`, `7`。
### 延伸問題:
1. 解釋程式運算原理,可搭配 Microsoft Research 的專案 snmalloc 來解說;
- 對應的原始程式碼 src/mem/sizeclass.h
2. 練習 [LeetCode 51. N-Queens](https://leetcode.com/problems/n-queens/),應選擇 C 語言並善用上述的 __builtin_ffs,大幅降低記憶體開銷;
- [**演(1)**] bitwise 解法 (未優化)
[Github](https://github.com/Jonec76/sysprog2020-final/blob/master/51.c)
Runtime: 52 ms (16.81 %)
Memory: 8.3 MB (39.45 %)
![](https://i.imgur.com/aHWQevl.png)
(因為 `c` 的解答說明不清楚,所以改成用 `cpp` 寫)
自己試著用 bit 的觀點,來解皇后問題,想法如下:
![](https://i.imgur.com/dRW9BvP.png)
每個棋子的「米」字方向上,都不能有任何其他的棋子存在。而 n=a 的時候要找出 a-Queen 的解答。試想若將上面的圖,用二進制來表示的話可以得到
```
0100
0001
1000
0010
```
由上而下就是 「4, 1, 8, 2」。
觀察了一下,只要找出 $2^n$ 內的所有 2 的冪次方(水平方向只有 一個 bit 為 1),且不重複(垂直方向只有一個 bit 為 1),就可以滿足 "「十」字方向不能有任何棋子" 的條件了。
接著,再把「4, 1, 8, 2」用「2, 0, 3, 1」的表示,以簡化想法。整個演算法就是找出 「0, 1, 2, 3」的所有排列,並且拿掉 "斜對角有棋子" 的情況(例:「0, 1, 2, 3」)就是答案了。
程式碼分析如下:
1. 找出 0~n 的所有排列
```c=1
int* nums = (int*)malloc(n * sizeof(int));
for(int i=0;i<n;i++)
nums[i] = i;
permute(nums, 0, n-1, ans);
// ...
for (i = l; i <= r; i++){
// ...
swap((a+l), (a+i));
permute(a, l+1, r, ans);
swap((a+l), (a+i)); //backtrack
}
```
2. 挪去對角線上有棋子的 case
```c=1
void permute(int *a, int l, int r, vector<vector<string> >& ans){
if (l == r){
bool skip = false;
for(int j=0;j<l+1;j++){
int diff = 1;
for(int k=j+1;k<l+1;k++){
if((a[j] - a[k]) == diff || (a[j] - a[k]) == -diff)
return;
diff++;
}
}
// ...
}
}
```
這部分花了 $O(n^2)$ 的時間。以例子來看,若得到 "0, 4, 2, 3" ,則會在 `j=0`, `k=2` 的時候,也就是 `a[j]` 為 `4` 且 `a[k]` 為 `2` 時因為不滿足對角項而 `return`
- [**演(2)**] bitwise 解法 (優化執行時間)
[Github](https://github.com/Jonec76/sysprog2020-final/blob/master/51_v2.cpp)
Runtime: 8 ms (74.41 %)
Memory: 8.3 MB (39.45 %)
![](https://i.imgur.com/WROF73C.png)
可以看到 [**演(1)**] 的執行時間表現不佳,思考之後發現說,在做排列的遞迴時,其實若知道該 substring 中有 "斜對角有棋子" 的話,那就可以直接停止了,這樣可以減少需多重複檢查,不必將所有的可能性都跑完。
參考 [geek-permutate](https://www.geeksforgeeks.org/write-a-c-program-to-print-all-permutations-of-a-given-string/) 中的圖
![](https://i.imgur.com/hIJrbD9.gif)
可以發現 `permutate` 在往下遞迴時,會將 `l` 的值加 `1`去遞迴右邊的子字串。如圖中紅色的部分(`l` 的左邊)越來越多,在紅色的部分是 fixed ,也就是不會被 swap 了,所以說若在這 fix 的部分已經知道不符合 "斜對角不能有棋子" 的話,那就可以直接不用進入右邊子字串的遞迴。
程式碼如下:
```c=1
for (i = l; i <= r; i++){
bool skip = false;
if(l>1){
for(int j=1;j<l;j++)
if(((a[l-1] - a[l-1-j]) == j)||((a[l-1] - a[l-1-j]) == -j))
skip = true;
}
if(skip)
continue;
swap((a+l), (a+i));
permute(a, l+1, r, ans);
swap((a+l), (a+i)); //backtrack
}
```
有趣的是,這樣使用 bitwise 想法的做法,跟原本經典的解法在時間、空間使用率上是一樣的,所以在 [**演(3)**] 中試著用 `__builtin_ffs` 優化經典的解法。
- [**演(3)**] 經典解法 (優化執行時間)
Runtime: 4 ms (97.37 %)
Memory: 8.3 MB (39.45 %)
![](https://i.imgur.com/bqpNrcK.png)
先看到原本經典的皇后問題解法:
```c=1
for (int i = 0; i < n; i++) {
/* Check if the queen can be placed on
board[i][col] */
if (isSafe(board, i, col, n)) {
/* Place this queen in board[i][col] */
board[i][col] = 1;
/* recur to place rest of the queens */
solvenQUtil(board, col + 1, n, ans);
/* If placing queen in board[i][col]
doesn't lead to a solution, then
remove queen from board[i][col] */
board[i][col] = 0; // BACKTRACK
}
}
/* A utility function to check if a queen can
be placed on board[row][col]. Note that this
function is called when "col" queens are
already placed in columns from 0 to col -1.
So we need to check only left side for
attacking queens */
bool isSafe(int** board, int row, int col, int n) {
int i, j;
/* Check this row on left side */
for (i = 0; i < col; i++)
if (board[row][i])
return false;
/* Check upper diagonal on left side */
for (i = row, j = col; i >= 0 && j >= 0; i--, j--)
if (board[i][j])
return false;
/* Check lower diagonal on left side */
for (i = row, j = col; j >= 0 && i < n; i++, j--)
if (board[i][j])
return false;
return true;
}
```
該解法是給定一張 N*N 的 table `board`,並且從最左上角開始放棋子,若放得下 (`isSafe`) 表示該棋子滿足問題的所有條件,則 `col+1` 往下一層遞迴繼續放。由左而右的放棋子有個優點,就是在 `isSafe` 裡頭檢查棋子時,因為該行的右半邊不會有任何棋子,所以只要檢查該棋子的左半部。
接著,再看看如何嘗試使用 `__builtin_ffs` 進行優化:
```c=1
for (int i = 0; i < n; i++) {
/* Check if the queen can be placed on
board[i][col] */
if (isSafe(board, i, col, n)) {
// board[i][col] = 1;
board[i] = 1 << (n-col-1);
solvenQUtil(board, col + 1, n, ans);
// board[i][col] = 0;
board[i] = 0; // BACKTRACK
}
}
bool isSafe(int** board, int row, int col, int n) {
int i, j;
/* Check this row on left side */
// if (board[row][i])
if(board[row] > (1<<(n-col-1)))
return false;
// /* Check upper diagonal on left side */
for (i = row, j = col; i >= 0 && j >= 0; i--, j--)
// if (board[i][j])
if(board[i] == 1<<(n-j-1))
return false;
// /* Check lower diagonal on left side */
for (i = row, j = col; j >= 0 && i < n; i++, j--)
// if (board[i][j])
if(board[i] == 1<<(n-j-1))
return false;
return true;
}
```
原本是使用 N*N 的大小儲存 `board`,在優化後使用 N*1 的大小來儲存。想法是將每個 row 的 `N` 格,用 2 進位的方式合併成一個十進位的數字。舉例來說
```c=1
// board[i][col] = 1;
board[i] = 1 << (n-col-1);
```
若原本是要在第 0 個 col 設為 1,那可以看成是值 $2^{n-1}$ 的意思。
而想要把 `board[i]` 的值轉換成對應到哪個 index 應該為 `Q`,就可以用到 `__builtin_ffs` 了
```c=1
for (int i = 0; i < n; i++) {
string tmp = dots;
tmp[__builtin_ffs(board[i])-1] = 'Q';
sol.push_back(tmp);
}
```
在記憶體使用上,仍然沒有大幅的增長,但是確實改善了執行時間。
:::warning
TODO: 透過 non-recursive 手段改寫 n-queen 程式碼
:::