# 演算法課程題解 - DFS
# UVa 441
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?441
給一個正整數 $k$ 以及 $k$ 個由小到大排序好的數字,輸出所有序列長度為 $6$ 的組合,所有組合皆由小到大輸出
$6 < k < 13$
## 想法 By Koios
因為題目要求組合而非排列,所以說像是 $(1, 2, 3)$ 和 $(2, 1, 3)$ 會被視為相同的
所以我們可以這麼想,這些會被視為相同的組合在由小到大排序過後都是一樣的
所以我們可以直接找那個由小到大嚴格遞增的那個,在上例中就是 $(1, 2, 3)$
既然 $k$ 不大,我們就可以考慮使用 DFS 去枚舉出所有的狀況
並且為了保持每個組合的遞增性,記錄上次最後存取的元素位置,下次就只能從那個位置之後繼續枚舉
例如說給定的序列是 $\{1, 2, 3, 4\}$
現在枚舉到 $\{1, 3\}$ 的時候,下次就不需要再看 $3$ 以前的數字了,只需要看 $3$ 以後的數字
如此一來,我們就可以在維持序列遞增性的前提下去枚舉出所有情況了!
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<algorithm>
using namespace std;
int n, m, tmp[6], arr[15], res[10];
void dfs(int depth, int start){
// 已經找好六個數字了
if(depth == 6){
// 把結果複製到 tmp
for(int i=0 ; i<6 ; i++){
tmp[i] = res[i];
}
for(int i=0 ; i<6 ; i++){
if(i) cout<<" ";
cout<<tmp[i];
}
cout<<"\n";
return;
}
// 從 start 開始枚舉
for(int i=start ; i<n ; i++){
res[depth] = arr[i];
dfs(depth+1, i+1);
}
}
int main(){
bool out=false;
while(cin>>n && n){
if(out) cout<<"\n";
else out=true;
for(int i=0 ; i<n ; i++){
cin>>arr[i];
}
dfs(0, 0);
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(n)$
DFS 每一層最多會有 $n$ 個選擇,總共有 $6$ 層
每筆測資 DFS 時間複雜度為 $O(n^6)$
每筆測資總時間複雜度約為 $O(n^7)$
# UVa 216
http://domen111.github.io/UVa-Easy-Viewer/?216
在平面座標上有 $n$ 個點,我們要用 $n-1$ 條網路線連接這 $n$ 個點
為了安裝方便,兩台電腦之間會多預留 $16$ 呎的網路線
是否能找到一種連接方式,使得電腦都有網路線連接,並且網路線總長度最少
$2 \leq n \leq 8$
## 想法 By Koios
這一題的 $n$ 很小,所以我們可以考慮比較暴力的作法
任意選擇一個起點,然後往下任意選擇下一個點,一直做到每個點都選擇到了,然後計算長度,把當前最短的長度所經過的點都記錄下來
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<iomanip>
#include<cmath>
using namespace std;
int n, Case=1, x[10], y[10], tmpx[10], tmpy[10], rx[10], ry[10];
bool used[10];
double tot, min_dis;
// 計算兩點之間距離
double dist(int p, int q, int m, int n){
return sqrt(pow(p-m, 2) + pow(q-n, 2));
}
// path 紀錄當前網路線總長度
void dfs(int depth, double path){
if(depth == n){
if(path < min_dis){
min_dis = path;
// 紀錄當前長度最短的路徑
for(int i=0 ; i<n ; i++){
rx[i] = tmpx[i];
ry[i] = tmpy[i];
}
}
return;
}
for(int i=0 ; i<n ; i++){
if(!used[i]){
used[i] = true;
tmpx[depth] = x[i];
tmpy[depth] = y[i];
if(depth == 0) dfs(depth+1, 0);
else dfs(depth+1, path+dist(tmpx[depth], tmpy[depth], tmpx[depth-1], tmpy[depth-1])+16);
used[i] = false;
}
}
}
int main(){
while(cin>>n && n){
// 初始化最小值是一個極大值
min_dis=2147483647;
for(int i=0 ; i<n ; i++){
cin>>x[i]>>y[i];
used[i] = false;
}
dfs(0, 0);
cout<<"**********************************************************\n";
cout<<"Network #"<<Case++<<"\n";
for(int i=1 ; i<n ; i++){
cout<<"Cable requirement to connect ("<<rx[i-1]<<","<<ry[i-1]<<") to ("<<rx[i]<<","<<ry[i]<<") is "<<fixed<<setprecision(2)<<dist(rx[i-1], ry[i-1], rx[i], ry[i])+16<<" feet.\n";
}
cout<<"Number of feet of cable required is "<<min_dis<<".\n";
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(n)$
DFS 每一層最多有 $n$ 種選擇,總共有 $n$ 層,並且最後一層需要 $O(n)$ 的時間複製當前答案
每筆測資 DFS 時間複雜度為 $O(n^n \times n) = O(n^{n+1})$
每筆測資總時間複雜度約為 $O(n^{n+2})$
# UVa 11084
http://domen111.github.io/UVa-Easy-Viewer/?11084
給一個只由數字組成的字串 $s$ 以及一個數字 $t$,問在 $s$ 的所有排列當中有多少個數字可以被 $t$ 整除
$1 \leq |s| \leq 10\\
1 \leq d \leq 10^4$
## 想法 By Koios
由於這題的 $|s|$ 不大,可以考慮比較暴力的做法
直接利用 DFS 去枚舉出所有的排列,接著去計算是否能整除
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int t, d, n;
long long ans=0;
bool used[15];
string s;
void dfs(int depth, string res){
if(depth == n){
long long cnt=0;
for(int i=0 ; i<n ; i++){
cnt*=10;
cnt+=res[i]-'0';
}
if(cnt % d == 0) ans++;
return;
}
bool num_used[15];
for(int i=0 ; i<15 ; i++) num_used[i] = false;
for(int i=0 ; i<n ; i++){
if(!used[i] && !num_used[s[i]-'0']){
used[i] = true;
num_used[s[i]-'0'] = true;
dfs(depth+1, res+s[i]);
used[i] = false;
}
}
}
int main(){
cin>>t;
while(t--){
ans=0;
for(int i=0 ; i<15 ; i++){
used[i] = false;
}
cin>>s>>d;
n = s.size();
dfs(0, "");
cout<<ans<<"\n";
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(|s|)$
DFS 每一層最多有 $|s|$ 種選擇,並且有 $|s|$ 層,令 $n = |s|$
每筆測資 DFS 時間複雜度為 $n^n$
總時間複雜度為 $O(tn^{n+1})$
# UVa 10776
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10776
本題有多筆測資,每筆測資包含一個字串 $s$ 以及一個數字 $r$,求以 $s$ 為集合,所有長度為 $r$ 的字串組合,每個輸出字串由小到大排列
## 想法 By Koios
因為要求的是組合,我們可以去枚舉答案的第 $i$ 格要是什麼,並且過去枚舉過的字就不能再使用
為了避免重複枚舉到重複的字串,一開始要先把字串由小到大排序好
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<algorithm>
using namespace std;
string s, res[35];
int n;
void DFS(int depth, int start){
// 枚舉完 n 個字了
if(depth == n){
for(int i=0 ; i<n ; i++){
cout<<res[i];
}
cout<<"\n";
return;
}
// 第 i 個字不能重複
// 用 used 紀錄用過的字
bool used[26];
for(int i=0 ; i<26 ; i++){
used[i] = false;
}
// 前面娶過的字不會再被用到
// 從 start 開始枚舉
for(int i=start ; i<s.size() ; i++){
if(!used[s[i]-'a']){
used[s[i]-'a']=true;
res[depth] = s[i];
DFS(depth+1, i+1);
}
}
}
int main(){
while(cin>>s>>n){
sort(s.begin(), s.end());
DFS(0, 0);
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(|s|)$,令 $n = |s|$
每筆測資排序時間複雜度為 $O(nlogn)$
DFS 每一層最多有 $|s|$ 種選擇,總共有 $|s|$ 層
每筆測資 DFS 時間複雜度為 $O(n^n)$
每筆測資總時間複雜度為 $O(nlogn + n^{n+1})$
# UVa 167
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?167
在一個 $8 \times 8$ 的西洋棋棋盤上要放上 $8$ 個皇后,並且所有皇后之間彼此不能在對方的攻擊範圍內,也就是經典的 $8$ 皇后問題
現在在棋盤上的每個格子都有一個數字,求所有符合 $8$ 皇后的情況下,所有皇后所在的格子數字總和最大是多少
## 想法 By Koios
因為棋盤大小固定只有 $8 \times 8$,可以考慮用比較暴力的方式去做
枚舉每一橫排的皇后可以存在的位置,直到所有橫排都枚舉完後記錄當前的數字總和並記錄下最大的情況
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<iomanip>
using namespace std;
int arr[8][8], path_x[8], path_y[8], k, ans;
void dfs(int depth){
if(depth == 8){
int tot=0;
// 計算當前數字總和
for(int i=0 ; i<8 ; i++){
tot += arr[path_x[i]][path_y[i]];
}
// 找出最大值
ans=max(ans, tot);
return;
}
// 對於橫排的 8 個位置枚舉
for(int i=0 ; i<8 ; i++){
bool cango = true;
// 檢查 depth 排以前的皇后與現在的位置是否衝突
for(int j=0 ; j<depth ; j++){
// 檢查是否在同一列上
if(path_y[j] == i){
cango=false;
break;
}
// 檢查是否在同一斜線上
if(abs(path_x[j]-depth) == abs(path_y[j]-i)){
cango=false;
break;
}
}
if(cango){
path_x[depth] = depth;
path_y[depth] = i;
dfs(depth+1);
}
}
}
int main(){
cin>>k;
while(k--){
ans=0;
for(int i=0 ; i<8 ; i++){
for(int j=0 ; j<8 ; j++){
cin>>arr[i][j];
}
}
dfs(0);
// 輸出格式須符合總長度為 5,不足則以空格補齊
cout<<setw(5)<<ans<<"\n";
}
return 0;
}
```
### 時間複雜度分析
令 $n = 8$
每筆測資輸入時間複雜度為 $O(n^2)$
DFS 每層最多有 $8$ 種選擇,總共有 $8$ 層
每筆測資 DFS 時間複雜度為 $O(n^n)$
總時間複雜度為 $O(kn^{n+2})$
# UVa 10957
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10957
給一個數獨的盤面,用數字 $0$ ~ $9$ 表示, $0$ 表示該格目前沒有數字
問這個盤面是 **有唯一解**、**有多種解**、**沒有解**,或是一開始的盤面即不符規則
## 想法 By Koios
對於每一個當前沒有數字的格子枚舉,把所有符合規則的盤面枚舉出來,計算可行的數量
至於判斷一個盤面是否合法,可以直觀的使用數獨的一般規則來判斷,也就是說
1. 一個 $3 \times 3$ 的方格當中只能有 $1$~$9$ 每個數字各一個
2. 每一行方格當中只能有 $1$~$9$ 每個數字各一個
3. 每一列方格當中只能有 $1$~$9$ 每個數字各一個
可以用三個陣列分別記錄上面三種情況下每個數字出現的次數,只要出現過就不能再枚舉
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int R, ans, Case=1;
int arr[9][9], small_sq[9][10], row[9][10], col[9][10], nxt_x[90], nxt_y[90];
// 先計算三種情況每個數字出現的數量
// 順便先判斷出一開始即不符合的情況
bool check(){
bool res=true;
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<10 ; j++){
small_sq[i][j] = row[i][j] = col[i][j] = 0;
}
}
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<9 ; j++){
if(arr[i][j] != 0){
// 3*3 方格從左至右、從上至下依序編號 1~9
small_sq[3*(i/3)+(j/3)][arr[i][j]]++;
row[i][arr[i][j]]++;
col[j][arr[i][j]]++;
if(small_sq[3*(i/3)+(j/3)][arr[i][j]]>1 || row[i][arr[i][j]] > 1 || col[j][arr[i][j]] > 1)
res=false;
}
}
}
return res;
}
void DFS(int depth){
if(depth == R){
ans++;
if(ans > 1){
return;
}
}
int i = nxt_x[depth];
int j = nxt_y[depth];
for(int k=1 ; k<=9 ; k++){
if(small_sq[3*(i/3)+(j/3)][k]>0 || row[i][k] > 0 || col[j][k] > 0)
continue;
small_sq[3*(i/3)+(j/3)][k]++;
row[i][k]++;
col[j][k]++;
DFS(depth+1);
small_sq[3*(i/3)+(j/3)][k]--;
row[i][k]--;
col[j][k]--;
}
}
int main(){
while(cin>>arr[0][0]){
// R 記錄空格子的數量
// nxt_x, nxt_y 記錄空格的座標
if(arr[0][0] == 0) nxt_x[0]=0, nxt_y[0]=0, R = 1;
else R = 0;
ans=0;
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<9 ; j++){
if(i==0 && j==0) continue;
cin>>arr[i][j];
if(arr[i][j] == 0) nxt_x[R]=i, nxt_y[R]=j, R++;
}
}
if(check() == false){
cout<<"Case "<<Case++<<": Illegal.\n";
}
else{
DFS(0);
if(ans == 1){
cout<<"Case "<<Case++<<": Unique.\n";
}
else if(ans > 1){
cout<<"Case "<<Case++<<": Ambiguous.\n";
}
else{
cout<<"Case "<<Case++<<": Impossible.\n";
}
}
}
return 0;
}
```
### 時間複雜度分析
令 $n = 9$
每筆測資輸入時間複雜度為 $O(n^2)$
每筆測資預處理三種情況時間複雜度為 $O(3n^2)$
DFS 每層最多有 $9$ 種選擇,最多有 $81$ 層
每筆測資 DFS 時間複雜度為 $O(n^{n^2})$
每筆測資總時間複雜度最差為 $O(4n^2 + n^{n^2})$
# TIOJ 1235
## 題目
https://tioj.ck.tp.edu.tw/problems/1235
跟數獨問題相同,不過這次從數字改成英文字母,保證一定有解的前提下,輸出每個空格應該是哪個對應的英文字母
**無論那一行是否有空格,都需要有一個換行**
## 想法 By Koios
跟數獨問題相同,我們可以枚舉每個空格可行的數字,枚舉到每個格子都填滿就可以輸出答案了
為了方便處理,我們可以把每個英文字母對應到一個數字,接下來就跟數獨問題一樣了
跟數獨一樣有三種情況
1. 一個 $3 \times 3$ 的方格當中只能有 $1$~$9$ 每個數字各一個
2. 每一行方格當中只能有 $1$~$9$ 每個數字各一個
3. 每一列方格當中只能有 $1$~$9$ 每個數字各一個
先計算出每種情況中每個數字出現的數字,就可以用來判斷某個數字是否能被放在空格當中
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int R, ans, Case=1;
int small_sq[9][10], row[9][10], col[9][10], nxt_x[90], nxt_y[90];
char arr[9][9];
// 建立英文與數字的對應表
string color="ROYGBIPLW";
int color_num[30];
char num_color[10];
bool found=false;
void init(){
// 統計三種情況中每個數字出現的次數
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<10 ; j++){
small_sq[i][j] = row[i][j] = col[i][j] = 0;
}
}
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<9 ; j++){
if(arr[i][j] != '*'){
small_sq[3*(i/3)+(j/3)][color_num[arr[i][j]-'A']]++;
row[i][color_num[arr[i][j]-'A']]++;
col[j][color_num[arr[i][j]-'A']]++;
}
}
}
}
void DFS(int depth){
if(depth == R){
for(int i=0, k=0, x, y ; i<9 ; i++){
for(int j=0 ; j<9 ; j++){
x = nxt_x[k];
y = nxt_y[k];
if(i==x && j==y){
cout<<arr[i][j];
k++;
}
}
cout<<'\n';
}
found=true;
}
int i = nxt_x[depth];
int j = nxt_y[depth];
for(int k=1 ; k<=9 && !found ; k++){
if(small_sq[3*(i/3)+(j/3)][k]>0 || row[i][k] > 0 || col[j][k] > 0)
continue;
small_sq[3*(i/3)+(j/3)][k]++;
row[i][k]++;
col[j][k]++;
arr[i][j] = num_color[k];
DFS(depth+1);
small_sq[3*(i/3)+(j/3)][k]--;
row[i][k]--;
col[j][k]--;
arr[i][j] = '*';
}
}
int main(){
// 建立英文與數字的對應表
for(int i=1 ; i<=color.size() ; i++){
num_color[i] = color[i-1];
color_num[color[i-1]-'A'] = i;
}
R=0;
ans=0;
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<9 ; j++){
cin>>arr[i][j];
if(arr[i][j] == '*') nxt_x[R]=i, nxt_y[R]=j, R++;
}
}
init();
DFS(0);
return 0;
}
```
# TOJ 362
## 題目
https://toj.tfcis.org/oj/pro/362/
現在有一副撲克牌,分成 $4$ 堆,每一堆都有 $13$ 張牌
現在我們要從 $4$ 個牌堆當中每次選兩張相同的拿出,最後是否有辦法把全部的牌拿完?
## 想法 By Koios
我們可以枚舉在每一層當前的 $4$ 張牌當中要取哪兩張,把所有的情況列出,如果能將全部的牌取完就表示有答案,否則沒有
至於要如何知道牌堆中哪張牌是最上方的,我們可以用一個陣列去紀錄 $4$ 個牌堆最上方的牌是牌堆中的第幾張,如此一來要枚舉的時候就只需要改變紀錄的 index 即可
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int arr[4][13];
int top[4];
bool found=false, ans=false;
void DFS(int depth){
if(depth == 26){
ans=true;
found=true;
return;
}
for(int i=0 ; i<4 ; i++){
for(int j=i+1 ; j<4 && !found ; j++){
if(top[i]<13 && top[j]<13 && arr[i][top[i]] == arr[j][top[j]]){
top[i]++;
top[j]++;
DFS(depth+1);
top[i]--;
top[j]--;
}
}
}
}
int main(){
for(int i=0 ; i<4 ; i++){
top[i] = 0;
for(int j=0 ; j<13 ; j++){
cin>>arr[i][j];
}
}
DFS(0);
if(ans){
cout<<"YES\n";
}
else{
cout<<"NO\n";
}
return 0;
}
```
### 時間複雜度分析
令 $n = 4, m = 13$
輸入時間複雜度為 $O(nm)$
DFS 每層最多有 $4$ 種選擇,總共有 $26$ 層
DFS 時間複雜度為 $O(n^{2m})$
總時間複雜度為 $O(nm + n^{2m})$
# UVa 124
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?124
給你幾個不重複的小寫英文字母,告訴你某些字母之間的大小關係,把所有從小到大的排列列出來
如果沒有告知某個元素的大小關係,那可以任意擺放
## 想法1 By Koios
我們可以紀錄兩個元素之間的關係,兩兩元素間的關係可以分成 `a<b` `a>b` `未定義` 三種
只要當前枚舉的元素跟前面的元素彼此之間不衝突,也就是說並不是 `a>b` 的狀態就可以枚舉下去
因為要找出所有情況,所以枚舉方式是採用 DFS
### 程式碼
```cpp=
#include<iostream>
#include<sstream>
using namespace std;
int var_nums, tbl[26];
int less_than[26][26]; // 0: undefine, 1: less, 2: larger or others
string vars, cons;
char v, x, y, res[26];
bool out=false, used_vars[26];
void DFS(int depth){
if(depth == var_nums){
for(int i=0 ; i<depth ; i++){
cout<<res[i];
}
cout<<"\n";
return;
}
for(int i=0 ; i<26 ; i++){
if(tbl[i]){
bool less=true;
for(int j=0 ; j<depth ; j++){
// 只要出現大於的情況就避開
if(less_than[res[j]-'a'][i]==2){
less=false;
}
}
if(less){
res[depth] = i+'a';
DFS(depth+1);
}
}
}
}
int main(){
while(getline(cin, vars)){
if(out) cout<<'\n';
else out=true;
// 初始化
var_nums=0;
for(int i=0 ; i<26 ; i++){
tbl[i] = 0;
used_vars[i] = false;
for(int j=0 ; j<26 ; j++){
less_than[i][j] = 2;
}
}
stringstream ss;
ss<<vars;
while(ss>>v){
tbl[v-'a']++;
var_nums++;
used_vars[v-'a'] = true;
}
// 預設把所有元素之間的關係都設成未定義
for(int i=0 ; i<26 ; i++){
if(!tbl[i]) continue;
for(int j=0 ; j<26 ; j++){
if(!tbl[j]) continue;
if(i != j)
less_than[i][j] = 0;
}
}
getline(cin, cons);
// 重設 stringstream
ss.str("");
ss.clear();
ss<<cons;
while(ss>>x>>y){
less_than[x-'a'][y-'a'] = 1;
less_than[y-'a'][x-'a'] = 2;
}
DFS(0);
}
return 0;
}
```
## 想法2 By Koios
既然題目給的是兩兩之間的關係,那麼就可以想到用一個圖來表示
例如題目的第一筆測資
![](https://i.imgur.com/jxqzSfj.png)
那麼該怎麼枚舉呢?隨便選一個點走下去嗎?
從上圖可以發現到,因為 $G$ 跟其他點的大小關係是未定義的,所以我們永遠走不到它,所以要換個想法
如果當前連到點 $x$ 的線數量是 $0$ ,就表示我們可以選擇這個點
在選擇 $x$ 之後,我們需要把 $x$ 以及 $x$ 所能連接到的所有點的邊砍斷
如此一來,與 $x$ 相連的所有點在下一次都是可以枚舉的點了!
並且我們不會忽略那些未定義的點
這個方法被稱為拓樸排序,也會用到 DFS 的概念
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<sstream>
using namespace std;
string vars, cons;
char v, x, y, res[26];
bool out=false, nxt[26][26];
int var_nums, tbl[26], in[26];
void DFS(int depth){
if(depth == var_nums){
for(int i=0 ; i<depth ; i++){
cout<<res[i];
}
cout<<"\n";
return;
}
for(int i=0 ; i<26 ; i++){
if(tbl[i] && in[i]==0){
res[depth] = i+'a';
// 刪除當前以及下一層點的邊
// 刪除當前是為了避免下次被重複枚舉
in[i]--;
for(int j=0 ; j<26 ; j++){
if(tbl[j] && nxt[i][j]){
in[j]--;
}
}
DFS(depth+1);
in[i]++;
for(int j=0 ; j<26 ; j++){
if(tbl[j] && nxt[i][j]){
in[j]++;
}
}
}
}
}
int main(){
while(getline(cin, vars)){
if(out) cout<<"\n";
else out=true;
// 初始化
var_nums=0;
for(int i=0 ; i<26 ; i++){
tbl[i] = in[i] = 0;
for(int j=0 ; j<26 ; j++){
nxt[i][j] = false;
}
}
stringstream ss;
ss<<vars;
while(ss>>v){
tbl[v-'a']=1;
var_nums++;
}
getline(cin, cons);
ss.str("");ss.clear();
ss<<cons;
while(ss>>x>>y){
// 連結到 y 的邊 +1
in[y-'a']++;
// 記錄下邊是存在的
nxt[x-'a'][y-'a'] = true;
}
DFS(0);
}
return 0;
}
```
# UVa 258
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?258
給一個 $n \times m$ 的方格,在最外圍保證只有兩個點是 `.`,表示起點以及終點,其餘都是 `*`
起點會有一束光源源不絕射入,如果遇到地圖當中的鏡子 `/` 或是 `\` 就會被反射,這裡的鏡子都會是以 $45^{\circ}$ 放置,也就是說光會轉向 $90^{\circ}$
每個鏡子都可以選擇要是 `/` 或是 `\`,輸出其中一組可以使光照射出終點的盤面
## 想法 By Koios
首先先找到起點以及終點,因為是光所以其實起點終點相反也沒關係
再來對於光來說方向也是重要的狀態,所以也同時先找到起點光射入的方向
找到之後嘗試跟著方向走,如果遇到鏡子就分成兩種情況
1. 跟著鏡子反射,然後固定這個鏡子的方向
2. 如果之前沒有光線經過這個鏡子,那我們就嘗試轉方向
這裡很重要的是要記錄這個鏡子有沒有被光線走過,如果有光線經過而我們又調整鏡子的方向,可能會導致光線不會走到現在的點上
那麼一直走到出口就找到答案了
因為每次都是一路走到底,所以可以用 DFS 去嘗試每種走法
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
// 0: up , 1: right, 2: down, 3: left
int n ,m, start_x, start_y, end_x, end_y, start_dir, end_dir, nx, ny, n_dir;
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};
char arr[55][55];
bool used[55][55], out=false, found;
void DFS(int x, int y, int dir){
if(found) return;
if(x==end_x && y==end_y){
found=true;
for(int i=0 ; i<n ; i++){
for(int j=0 ; j<m ; j++){
cout<<arr[i][j];
}
cout<<"\n";
}
return;
}
if(arr[x][y] == '.'){
nx = x + dx[dir];
ny = y + dy[dir];
DFS(nx, ny, dir);
}
else if(arr[x][y] == '*'){
return;
}
else{
if(arr[x][y] == '/'){
bool tmp=used[x][y];
used[x][y] = true;
n_dir = dir^1;
nx = x + dx[n_dir];
ny = y + dy[n_dir];
DFS(nx, ny, n_dir);
used[x][y] = tmp;
// spin mirror
if(!used[x][y]){
used[x][y] = true;
arr[x][y] = '\\';
n_dir = 3-dir;
nx = x + dx[n_dir];
ny = y + dy[n_dir];
DFS(nx, ny, n_dir);
arr[x][y] = '/';
used[x][y] = false;
}
}
else{
bool tmp=used[x][y];
used[x][y] = true;
n_dir = 3-dir;
nx = x + dx[n_dir];
ny = y + dy[n_dir];
DFS(nx, ny, n_dir);
used[x][y] = tmp;
// spin mirror
if(!used[x][y]){
used[x][y] = true;
arr[x][y] = '/';
n_dir = dir^1;
nx = x + dx[n_dir];
ny = y + dy[n_dir];
DFS(nx, ny, n_dir);
arr[x][y] = '\\';
used[x][y] = false;
}
}
}
}
int main(){
while(cin>>m>>n && (m!=-1 && n!=-1)){
if(out) cout<<"\n";
else out=true;
found=false;
start_x = start_y = end_x = end_y = -1;
for(int i=0 ; i<n ; i++){
for(int j=0 ; j<m ; j++){
used[i][j] = false;
cin>>arr[i][j];
if((i==0 || i==n-1 || j==0 || j==m-1) && arr[i][j]=='.'){
if(start_x == -1){
start_x = i;
start_y = j;
if(i==0) start_dir = 2;
else if(i==n-1) start_dir = 0;
else if(j==0) start_dir = 1;
else start_dir = 3;
}
else{
end_x = i;
end_y = j;
}
}
}
}
DFS(start_x, start_y, start_dir);
}
return 0;
}
```
### 時間複雜度分析
每筆輸入時間複雜度為 $O(nm)$
DFS 每層最多有 $2$ 種選擇,最多有 $nm$ 層
DFS 時間複雜度為 $O(2^{nm})$
每筆測資總時間複雜度為 $O(nm + 2^{nm})$
# UVa 291
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?291
如下圖所示,有編號 $1$~$5$ 的五個點,假設每一條邊都是雙向的
列出所有從 $1$ 開始每條邊最多只經過一次,並且每個點都有走到的情況,也就是一筆劃問題
![](https://i.imgur.com/6aqms5F.png)
## 想法 By Koios
要枚舉有哪些情況,就必須要先知道每個點可以連接到哪些點
我們可以預處理一張表,紀錄 $a$ 到 $b$ 是否有邊連通
接下來要符合一條邊的情況,就必須記錄每條邊是否被走過
一樣的,可以用一個表格紀錄 $a$ 到 $b$ 之間的邊是否被走過
最後就從 $1$ 點開始對於每個可以走的點都走過一次,枚舉出所有情況即可
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
bool nxt[6][6];
int res[10], vis[6][6];
void DFS(int depth, int last){
if(depth == 9){
for(int i=0 ; i<9 ; i++){
cout<<res[i];
}
cout<<"\n";
return;
}
for(int i=1 ; i<=6 ; i++){
// 確認 last 到 i 之間的邊是否走過
if(!vis[last][i] && !vis[i][last] && nxt[last][i]){
vis[last][i] = vis[i][last] = true;
res[depth] = i;
DFS(depth+1, i);
vis[last][i] = vis[i][last] = false;
}
}
}
int main(){
for(int i=0 ; i<6 ; i++){
for(int j=0 ; j<6 ; j++){
if(i==0) nxt[i][j] = true;
else nxt[i][j] = false;
vis[i][j] = false;
}
}
// 建立兩個點之間是否有邊的表
nxt[1][2] = nxt[1][3] = nxt[1][5] = true;
nxt[2][1] = nxt[2][3] = nxt[2][5] = true;
nxt[3][1] = nxt[3][2] = nxt[3][4] = nxt[3][5] = true;
nxt[4][3] = nxt[4][5] = true;
nxt[5][1] = nxt[5][2] = nxt[5][3] = nxt[5][4] = true;
res[0]=1;
DFS(1, 1);
return 0;
}
```
### 時間複雜度分析
DFS 每層最多 $4$ 種選擇,總共有 $8$ 層,時間複雜度為 $O(4^8)$
# UVa 399
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?399
有一張由 $d \times d$ 塊 $w \times h$ 大小的拼圖,每一塊拼圖的 上、左、下、右 都分別有一個數字
當兩塊拼圖之間的數字只差一個負號的時候(例如: `5` 和 `-5`)這兩塊拼圖就可以放在一起
此外,在四周的拼圖邊上數字都必須是 $0$
舉例來說,我們有這些拼圖
![](https://i.imgur.com/JMa4VJj.png)
那麼可行的拼法為
![](https://i.imgur.com/khq6BXR.png)
## 想法 By Koios
因為拼圖有許多拼法,也許兩塊可以組合在一起,但是卻無法拼成完整的拼圖,因此可以考慮窮舉每個當前可行的情況,如果一直拚到最後一塊也沒問題,那就找到答案了
從左至右,從上至下枚舉每一個空格可以放哪些拼圖,檢查邊界、四周的拼圖
除了輸入和判斷比較繁複以外,基本都和 DFS 是相同的概念
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, d, h, w, res[105][105];
bool used[105], found;
string s;
struct puzzle{
// 拼圖的每一行
string line[30];
// 上左下右四個數字
int above;
int left;
int below;
int right;
}puzzles[105];
bool feasible(int id, int x, int y){
// 當前在邊界,要判斷 0
if(x==1 && puzzles[id].above != 0){
return false;
}
if(y==1 && puzzles[id].left != 0){
return false;
}
if(x==d && puzzles[id].below != 0){
return false;
}
if(y==d && puzzles[id].right != 0){
return false;
}
//right
if(res[x][y+1]!=-1 && puzzles[res[x][y+1]].left != -puzzles[id].right){
return false;
}
//left
if(res[x][y-1]!=-1 && puzzles[res[x][y-1]].right != -puzzles[id].left){
return false;
}
//above
if(res[x-1][y]!=-1 && puzzles[res[x-1][y]].below != -puzzles[id].above){
return false;
}
//below
if(res[x+1][y]!=-1 && puzzles[res[x+1][y]].above != -puzzles[id].below){
return false;
}
return true;
}
void DFS(int depth){
if(depth == d*d){
found=true;
for(int i=1 ; i<=d ; i++){
for(int k=0 ; k<h ; k++){
for(int j=1 ; j<=d ; j++){
cout<<puzzles[res[i][j]].line[k];
}
cout<<"\n";
}
}
return;
}
// x, y 都從 1 開始
// 這樣 res 四周都會是 -1,判斷邊界就不會超出範圍
int x=depth/d+1;
int y=depth%d+1;
for(int i=0 ; i<d*d && !found ; i++){
if(!used[i] && feasible(i, x, y)){
used[i] = true;
res[x][y] = i;
DFS(depth+1);
used[i] = false;
res[x][y] = -1;
}
}
}
int main(){
cin>>n;
for(int t=0 ; t<n ; t++){
if(t) cout<<"\n";
// 初始化
for(int i=0 ; i<30 ; i++){
used[i] = false;
}
for(int i=0 ; i<105 ; i++){
for(int j=0 ; j<105 ; j++){
res[i][j] = -1;
}
}
found = false;
// 輸入
// 要特別注意到,要用 getline 之前如果有 cin,後面都需要一個 getchar 去接收換行
cin>>d>>h>>w;
getchar();
for(int i=0 ; i<d*d ; i++){
// 每塊拼圖之間有一個空行隔開
if(i) getchar();
for(int j=0 ; j<h ; j++){
getline(cin, s);
puzzles[i].line[j] = s;
}
cin>>puzzles[i].above>>puzzles[i].left>>puzzles[i].below>>puzzles[i].right;
getchar();
}
DFS(0);
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(d^2hw)$
DFS 每層最多有 $d^2$ 種選擇,共 $d^2$ 層
每筆測資 DFS 時間複雜度為 $O(d^{2^{d^{2}}})$
每筆測資輸出時間複雜度為 $O(d^2hw)$
總時間複雜度為 $O(t(2d^2hw + d^{2^{d^{2}}}))$
# UVa 598
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?598
給有一群字串,輸出這群字串的所有組合
## 想法 By Koios
這題跟問數字 $1$~$n$ 的所有組合是相同的問題,只是從數字變成字串而已,但是概念相同
因為求的是組合,所以過去出現過的組合就不能再以不同排列出現
也就是說要是我們已經枚舉過包含元素 $A$ 的所有組合,那下次元素 $A$ 就必定不能再使用
所以可以用一個變數 $start$ 紀錄上次最後用到哪個元素,下次枚舉就從 $start+1$ 開始即可
這題麻煩的是輸入有些複雜,可以使用 `stringstream` 處理輸入是 `a` 或是 `a b` 的情況
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<sstream>
using namespace std;
int m, a, b, R, res[20];
string op, s, arr[20];
bool used[20];
void DFS(int depth, int start, int end){
if(depth == end){
for(int i=0 ; i<end ; i++){
if(i) cout<<", ";
cout<<arr[res[i]];
}
cout<<"\n";
return;
}
for(int i=start ; i<R ; i++){
res[depth]=i;
DFS(depth+1, i+1, end);
}
}
int main(){
// 輸入
cin>>m;
while(getchar() != '\n');
while(getchar() != '\n');
for(int t=0 ; t<m ; t++){
if(t) cout<<"\n";
R = 0;
getline(cin, op);
while(getline(cin, s) && s[0]!='\0'){
arr[R] = s;
R++;
}
// 判斷所求是哪種
if(op[0] == '*'){
a=1, b=R+1;
}
else{
stringstream ss;
ss<<op;
ss>>a;
ss>>b;
// 判斷有沒有 b
if(ss.fail()){
b=a+1;
}
else{
b++;
}
}
for(int i=a ; i<b ; i++){
if(i>a) cout<<"\n";
cout<<"Size "<<i<<"\n";
DFS(0, 0, i);
}
cout<<"\n";
}
return 0;
}
```
# UVa 639
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?639
給一個最多為 $4 \times 4$ 的西洋棋棋盤盤面,求最多能放上多少城堡,使得城堡之間不會互相攻擊到
盤面上可能會有牆壁,有牆壁的阻隔就不會互相攻擊到
## 想法 By Koios
枚舉每個空格是否要放城堡,找出城堡數量最多的
對於每個空格都可以選擇放與不放,如果要放則需要先檢查當前的盤面是否會衝突
如此一來就可以找到最大值
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n;
char arr[5][5]; // . 表示空格 ; * 表示城堡 ; X 表示牆壁
bool feasible(int x, int y){
//above
for(int i=x-1 ; i>=0 ; i--){
if(arr[i][y] == 'X') break;
else if(arr[i][y] == '*') return false;
}
//left
for(int i=y-1 ; i>=0 ; i--){
if(arr[x][i] == 'X') break;
else if(arr[x][i] == '*') return false;
}
//below
for(int i=x+1 ; i<n ; i++){
if(arr[i][y] == 'X') break;
else if(arr[i][y] == '*') return false;
}
//right
for(int i=y+1 ; i<n ; i++){
if(arr[x][i] == 'X') break;
else if(arr[x][i] == '*') return false;
}
return true;
}
int DFS(int x, int y){
if(x == n || y == n){
return 0;
}
if(arr[x][y] == '.'){
if(feasible(x, y)){
int Max=0;
// 放城堡
arr[x][y] = '*';
if(y == n-1){
Max = 1+DFS(x+1, 0);
}
else{
Max = 1+DFS(x, y+1);
}
// 不放城堡
arr[x][y] = '.';
if(y == n-1){
Max = max(Max, DFS(x+1, 0));
}
else{
Max = max(Max, DFS(x, y+1));
}
return Max;
}
}
if(y == n-1){
return DFS(x+1, 0);
}
else{
return DFS(x, y+1);
}
}
int main(){
while(cin>>n && n){
for(int i=0 ; i<n ; i++){
for(int j=0 ; j<n ; j++){
cin>>arr[i][j];
}
}
cout<<DFS(0, 0)<<"\n";
}
return 0;
}
```
### 時間複雜度分析
輸入時間複雜度為 $O(n^2)$
DFS 每層最多有 $2$ 種選擇,共 $n^2$ 層
DFS 時間複雜度為 $O(2^{n^2})$
總時間複雜度為 $O(n^2 + 2^{n^2})$
# UVa 989
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?989
跟數獨問題相同,不過這次的版面大小可以從 $1 \times 1$ 到 $3 \times 3$
## 想法 By Koios
記錄小正方形、行、列每個數字是否出現過,透過 DFS 枚舉每種可能
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, R, arr[9][9], nx[81], ny[81];
bool found, out=false, small_sq[9][10], row[9][10], col[9][10];
void init(){
for(int i=0 ; i<9 ; i++){
for(int j=0 ; j<10 ; j++){
small_sq[i][j] = false;
row[i][j] = false;
col[i][j] = false;
}
}
for(int i=0 ; i<n*n ; i++){
for(int j=0 ; j<n*n ; j++){
if(arr[i][j] != 0){
col[j][arr[i][j]]=true;
row[i][arr[i][j]]=true;
small_sq[n*(i/n) + j/n][arr[i][j]]=true;
}
}
}
}
void DFS(int depth){
if(depth == R){
found = true;
for(int i=0 ; i<n*n ; i++){
for(int j=0 ; j<n*n ; j++){
if(j) cout<<' ';
cout<<arr[i][j];
}
cout<<'\n';
}
return;
}
int x = nx[depth];
int y = ny[depth];
for(int i=1 ; i<=n*n && !found ; i++){
if(!col[y][i] && !row[x][i] && !small_sq[n*(x/n) + y/n][i]){
col[y][i] = true;
row[x][i] = true;
small_sq[n*(x/n) + y/n][i] = true;
arr[x][y] = i;
DFS(depth+1);
col[y][i] = false;
row[x][i] = false;
small_sq[n*(x/n) + y/n][i] = false;
arr[x][y] = 0;
}
}
}
int main(){
while(cin>>n){
if(out) cout<<"\n";
else out=true;
R = 0;
found = false;
for(int i=0 ; i<n*n ; i++){
for(int j=0 ; j<n*n ; j++){
cin>>arr[i][j];
if(arr[i][j] == 0){
nx[R] = i;
ny[R] = j;
R++;
}
}
}
init();
DFS(0);
if(!found){
cout<<"NO SOLUTION\n";
}
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(n^2 \times n^2)$
DFS 每次最多有 $n^2$ 種選擇,最多有 $n^2 \times n^2$ 層
每筆測資 DFS 時間複雜度為 $O(n^{2^{n^{4}}})$
每筆測資總時間複雜度為 $O(n^4 + n^{2^{n^{4}}})$
###### tags: `SCIST 演算法 題解`