---
# System prepended metadata

title: '[AIdrifter CS 浮生筆錄](https://hackmd.io/@iST40ExoQtubds5LhuuaAw/rypeUnYSb?type=view#Competitive-Programmer%E2%80%99s-Handbookb) <br>Competitive Programming''s Handbook <br> Ch5 :Complete Search'

---

# [AIdrifter CS 浮生筆錄](https://hackmd.io/@iST40ExoQtubds5LhuuaAw/rypeUnYSb?type=view#Competitive-Programmer%E2%80%99s-Handbookb) <br>Competitive Programming's Handbook <br> Ch5 :Complete Search
# Complete Search 窮舉法
- 常見有暴搜法 (brute force)，列舉 (straight-forward))
- 如果搞不定在讓**greedy algorithms** or **dynamic programming**出馬。



## Generating subsets
先從如何簡單產生set有可能開始 - **組合**。
for example, the subsets of $\{0,1,2\}$ are
$\emptyset$, $\{0\}$, $\{1\}$, $\{2\}$, $\{0,1\}$, $\{0,2\}$, $\{1,2\}$ and $\{0,1,2\}$.
### Recursive
- `n = 3`, k自0開始。
```cpp
void search(int k) {
    if (k == n) {
        // process subset
    } else {
        search(k+1);
        subset.push_back(k);
        search(k+1);
        subset.pop_back();
    }
}
```
```cpp
int n = 3;
vector<int> subset;
void search(int k) {
    cout<<" search(" << k<< ")";
    if (k == n) {
        cout<<endl;
        for(auto s: subset)
            cout<<s<<" ";
        cout<<endl;
    } else {
        search(k+1);
        cout<<" push(" << k<< ")";
        subset.push_back(k);
        search(k+1);
        cout<<" pop()";
        subset.pop_back();
    }
}
```
```shell
 search(0) search(1) search(2) search(3)

 push(2) search(3)
2
 pop() push(1) search(2) search(3)
1
 push(2) search(3)
1 2
 pop() pop() push(0) search(1) search(2) search(3)
0
 push(2) search(3)
0 2
 pop() push(1) search(2) search(3)
0 1
 push(2) search(3)
0 1 2
 pop() pop() pop()

```
![](https://i.imgur.com/wuh9omQ.png)


### Bit representation
For example, the bit representation of 25(b)
is 11001, which corresponds to the subset $\{0,3,4\}$.
```cpp
for (int b = 0; b < (1<<n); b++) {
    vector<int> subset;
    for (int i = 0; i < n; i++) {
        if (b&(1<<i)) subset.push_back(i);
    }
}
```
## Generating permutations
### Recursive
```cpp
void search() {
    if (permutation.size() == n) {
        // process permutation
    } else {
        for (int i = 0; i < n; i++) {
            if (chosen[i]) continue;
            chosen[i] = true;
            permutation.push_back(i);
            search();
            chosen[i] = false;
            permutation.pop_back();
        }
    }
}
```
### next_permutation
```cpp
vector<int> permutation;
for (int i = 0; i < n; i++) {
    permutation.push_back(i);
}
do {
    // process permutation
} while (next_permutation(permutation.begin(),permutation.end()));
```

## Backtracking 回溯法
- 回溯算法知道回頭：當一條路不同的時候，它會退**回到上一個岔路**，而不是從頭開始。

Consider the problem of calculating the number
of ways $n$ queens can be placed on an $n \times n$ chessboard so that
no two queens attack each other.
For example, when $n=4$, there are two possible solutions:

![](https://i.imgur.com/2LtETLg.png)

At the bottom level, the three first configurations are illegal(在第二排，前三個皇后都互吃), because the queens attack each other. However, the fourth configuration is valid and it can beextended to a complete solution by placing two more queens to the board(在第二排的第4個是合法的，他還可以變最後解). There is only one way to place the two remaining queens

![](https://i.imgur.com/ozYwJhi.png)

```cpp
void search(int y) {
    if (y == n) {
        count++;
        return;
    }
    for (int x = 0; x < n; x++) {
        if (column[x] || diag1[x+y] || diag2[x-y+n-1]) continue;
        column[x] = diag1[x+y] = diag2[x-y+n-1] = 1;
        search(y+1);
        column[x] = diag1[x+y] = diag2[x-y+n-1] = 0;
    }
}
```
he array $\texttt{column}$ keeps track of columns that contain a queen,
and the arrays $\texttt{diag1}$ and $\texttt{diag2}$ keep track of diagonals.
It is not allowed to add another queen to a column or diagonal that already contains a queen. 
For example, the columns and diagonals of the $4 \times 4$ board are numbered as follows:

![](https://i.imgur.com/t6ufJWO.png)



## Pruning the search
- 回溯算法：當一條路不同的時候，它會退**回到上一個岔路**，而不是從頭開始；
- 回溯算法 + **剪枝**：當發現一條路有問題的時候，**它就會放棄走下去**，這樣就可以避免走一些不必要的路。

![](https://i.imgur.com/mBeVX2X.png)

原文網址：https://kknews.cc/news/mkr6gv9.html


We can often optimize backtracking by pruning the search tree.
The idea is to add ''intelligence'' to the algorithm
so that it will notice as soon as possible
**if a partial solution cannot be extended to a complete solution.**

Let us consider the problem
of calculating the number of paths
in an $n \times n$ grid from the upper-left corner
to the lower-right corner
從左上到右下， 在7*7的gird下，如果又不要**碰撞**，一共有111712的走法。
下面會介紹各種解法。
![](https://i.imgur.com/Ip1NmYg.png)
### Basic Algorithm
- 直接用backtracking 硬幹。
    - running time: 483 seconds
    - number of recursive calls: 76 billion
### Optimization 1
利用對稱的概念，$down = right$ , $left = up$。
In any solution, we first move one step down or right.
There are always two paths that are symmetric
![](https://i.imgur.com/sDbbZ1c.png)
- running time: 244 seconds
- number of recursive calls: 38 billion

### Optimization 2
如果沒有走完全程，直接到終點(右下)，直接放棄這個解，不用再亂走。

![](https://i.imgur.com/bzAkdTl.png)
- running time: 119 seconds
- number of recursive calls: 20 billion
### Optimization 3
如果撞到牆壁後，把gird拆成兩邊，而另外一邊無法訪問完，直接pruning這個解。

![](https://i.imgur.com/SMTLp6y.png)
- running time: 1.8 seconds
- number of recursive calls: 221 million

### Optimization 4
如果撞到自己走過的路(和貪食蛇很像)，直接pruning當下解。

![](https://i.imgur.com/k9k2ktG.png)

- running time: 0.6 seconds
- number of recursive calls: 69 million

### Conclusion
- the running time of the original algorithm was 483 seconds, and now after the optimizations, the running time is only 0.6 seconds
- This is a usual `phenomenon` in backtracking, because the search tree is usually large and even simple observations can effectively prune the search.
- 對於search tree來說，越前面的step如果可以控制，可以少走很多冤路，所以盡量想辦法往前pruning。
  Especially useful are optimizations that occur during the **first steps** of the algorithm,
i.e., at the top of the search tree.

## 剪枝法 vs 回朔法 vs 分支與限制
- 回溯 Backtracking
    - 採用 “深先搜尋法” (Depth-First Search; DFS)  對狀態空間樹中每一個節點進行檢查
    - 為**遞迴**的應用概念，因此可利用**Stack**保存走訪過程中間所走過的點。

- 分枝與限制 Branch and Bound
    - 策略採用 “廣先搜尋法” (Breadth-First Search; BFS) 對狀態空間樹中每一個節點進行檢查
    - 為**迴圈**的應用概念，因此可利用**Queue**保存走訪過程中間所走過的點


上述的兩個策略，皆透過 “邊界函數” (Bounding Function) 來刪除一些不必要的子樹搜尋動作，以提昇搜尋效率。

- 剪枝
    - 當搜尋到 “不可行” 的節點 (即：沒前途(nonpromising)節點) 時，則不用再去搜尋該節點以下之所有分枝節點。 此即為修剪 (**Pruning**)
    - 當搜尋到 “可行” 的節點 (即：有前途(promising)節點) 時，則可以再續繼往下搜尋該節點以下之分枝節點。
    - 可以得到修剪過的狀態空間樹 (Pruned State Space Tree)。


## Meet in the middle
**把 seach sapce 拆成兩塊，最後在合併**
is a technique
where the search space is divided into
two parts of about equal size.
A separate search is performed
for both of the parts,
and finally the results of the searches are combined.

可以有效把exponential下降，是個非常好的方法。
we can turn a factor of $2^n$
into a factor of $2^{n/2}$ using the meet in the
middle technique.

### Question: Does the sum exist in array ?
題目給一陣列$[2,4,5,9]$，令$x=15$，要如何判斷陣列中元素可否組成 $15$ 呢?
$15 = 2 + 4 + 9$
如果採用列舉出set的全部組合(Complete Search)，需要花掉$O(2^n)$的時間。
For example, given the list $[2,4,5,9]$ and $x=15$,
we can choose the numbers $[2,4,9]$ to get $2+4+9=15$.
However, if $x=10$ for the same list,
it is not possible to form the sum.
A simple algorithm to the problem is to
go through all subsets of the elements and
check if the sum of any of the subsets is $x$.
The running time of such an algorithm is $O(2^n)$,
because there are $2^n$ subsets.

**Meet in the middle** : 把陣列拆成$A=[2,4]$ and $B=[5,9]$.創造兩陣列 :
$S_A=[0,2,4,6]$ and $S_B=[0,5,9,14]$
再從兩陣列中去找組合，最後找到 $S_A$ ($6 = 2 +4$) + $S_B$($9$) = 15
時間複雜度從 $O(2^n)$ 降成 $2*O(2^{n/2})$
# Reference
backtracking:
http://programming-study-notes.blogspot.com/2014/03/backtracking.html

Complete Search:
https://m80126colin.github.io/blog/articles/%E7%BF%BB%E8%AD%AF/usaco/usaco-1-2-1-text/

回朔法:
https://kknews.cc/zh-tw/news/mkr6gv9.html

# Note
類似 $n!$ 種問題還有這些:
- N皇后(N-Queen)問題
- 旅行銷售員問題(Traveling Salesman Problem; TSP)
- 漢米爾頓迴路(Hamiltonian Circuits)
- 圖形著色(Graph-Coloring)


