搜尋是一種有目的地列舉問題的解空間中部分或全部情況,進而找到解的方法。它的定義是:
起始狀態經過一系列的狀態轉移抵達目標狀態,我們一般用搜尋樹(Search Tree)來表示狀態轉移
搜尋一般包括4個部分:
-
1、狀態空間,也叫解空間;
-
2、狀態轉移;
-
3、起始狀態;
-
4、目標狀態。
如何構思上面的四個部分呢,比如狀態空間是什麼,舉個例子:
1.Search Trees
以尋找從點A到點E的路徑問題為例,建立一顆搜尋樹,建好搜尋樹之後,還需要確定搜尋策略,即先擴充套件樹中的哪個結點,搜尋策略有寬度優先搜尋、深度優先搜尋兩種。
2.BFS
策略:每次優先處理當前所有未處理狀態中深度最淺的一個狀態,即處理最淺的結點。
採用這樣的搜尋策略,可以發現是一層一層從左到右的擴充套件,也就是先擴充套件的先處理,符合佇列先進先出特性。
2.1 Catch That Cow
題目描述:
Farmer John has been. informed of the location of a fugitive cow and wants to catch her immediately.He starts at a point N(0≤N≤100000) on a number line and the cow is at a point K (0≤K≤100000)on the same number line. Farmer John has two modes of transportation: walking and teleporting.
Walking:Farmer John can move from any point X to the pointsX- 1 orX+ 1 in a single minute.Teleporting: Farmer John can move from any point xto the point 2X in a single minute.
輸入:
Line 1: Two space-separated integers: N and K.
輸出:
Line 1: The least amount of time, in minutes, it takes for Farmer John to catch the fugitive cow.
搜尋類題目,首先寫出搜尋問題的4個部分:
-
1、狀態空間 (位置n, 時間t)
-
2、狀態轉移 (n-1, t+1), (n+1, t+1), (2n, t+1),共3種移動方式
-
3、起始狀態 (N, 0)
-
4、目標狀態 (K, lowestTime)
然後建立一顆搜尋樹:
確定搜尋策略:抓到牛並且花費時間最少,BFS逐層搜尋符合題目要求。同時注意到,對重複位置的狀態,為了提高搜尋的效率,設定visit陣列,不再對它進行擴充套件。
抓住那隻牛
//2024-03-08 搜尋 Catch that cow
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int MAXN = 1e5 + 10;
bool visit[MAXN];
struct Status {
int position;
int time;
Status(){}
Status(int p, int t): position(p), time(t) {}
};
int BFS(int n, int k) {
queue<Status> myQueue;
myQueue.push(Status(n, 0));//把搜尋樹的根結點壓入佇列
visit[n] = true;
while(!myQueue.empty()) { //逐層擴充套件狀態
Status current = myQueue.front();
if(current.position == k) {
return current.time;
}
myQueue.pop();
for(int i = 0; i < 3; ++i) {
Status next = current;
if(0 == i) {
next.position -= 1;
} else if(1 == i) {
next.position += 1;
} else {
next.position *= 2;
}
next.time += 1;
if(next.position < 0 || next.position > MAXN || visit[next.position]) {
continue;
}
myQueue.push(next);
visit[next.position] = true;
}
}
return -1;
}
int main()
{
int n, k;
while(cin >> n >> k) {
fill(visit, visit + MAXN, false);
printf("%d\n",BFS(n, k));
}
return 0;
}
2.2 Find The Multiple
為了縮小狀態空間,提高搜尋效率,我們轉換思路,搜尋由0、1組成的數字中,能否被給定的數n整除
首先,寫出搜尋的4個部分
-
1、狀態空間 0,1構成的數字number
-
2、狀體轉移 (number * 10),(number * 10 + 1)
-
3、初始狀態 1
-
4、目標狀態 number % n == 0
然後建立一顆搜尋樹:
Find the Multiple
//2024-03-12 Find the Mutiple 自寫
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
void BFS(int n) {
queue<long long> myQueue;
myQueue.push(1);
while(!myQueue.empty()) {
long long current = myQueue.front();
myQueue.pop();
if(current % n == 0) {
cout << current << endl;
return;
}
myQueue.push(current * 10);
myQueue.push(current * 10 + 1);
}
}
int main()
{
int n;
while(cin >> n) {
if(n == 0)
break;
BFS(n);
}
return 0;
}
寬度優先搜尋常被用來求解最優值問題,因為其搜尋到的狀態總是按照其某個關鍵字遞增。因此,一旦問題中出現最少、最短、、最優等關鍵字,就要考慮是否是寬度優先搜尋問題。
由於深度優先搜尋並沒有先入先出的特點,所以搜尋到需要的狀態時,該狀態不再像是寬度優先搜尋中的狀態一樣,具有某種最優的特性。因此,使用深度優先搜尋策略時,常常是為了知道問題是否有解。
3.DFS
策略:優先處理最深的結點,可以發現先擴充套件的狀態可能較後才處理,與棧的後入先出特性相符,但在實際操作中,往往使用遞迴的方式來實現DFS。
3.1 A Knight's Journey
像象棋一樣,馬這個棋字的行走規則是“日”字。
同樣首先分析搜尋問題的4個組成部分:
-
1、狀態空間:當前遊歷到的點的座標,以及總共走了多少格 (x, y, step)
-
2、狀態轉移:一共8種 (x-1, y-2, step+1)、(x+1, y-2, step+1)...
-
3、起始狀態: (0, 0, 1) 如果存在解,一定是從A1開始搜尋,使得遊歷的字典序最小
-
4、目標狀態: (x, y, sizeof board) 在哪點終止不確定,但格子數確定
畫出搜尋樹:
題目要求是找到一條路徑能夠遍歷到棋盤的每一格,相當於要求搜尋樹的深度height與格子數量相同,並且注意要設定visit陣列,之前已經訪問過的格子不能再進行擴充套件或計入樹高。
騎士的環球旅行
//2024-03-13 騎士的旅行
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int direction[8][2] = {
{-1, -2}, {1, -2}, {-2, -1}, {2, -1}, {-2, 1}, {2, 1}, {-1, 2}, {1, 2}
};
const int MAXN = 30; //棋盤格數+5安全冗餘
bool visit[MAXN][MAXN];
bool DFS(int x, int y, int step, string answer, int p, int q) {
if(step == p * q) {
cout << answer << endl << endl;
return true;
}
//步數沒到棋盤尺寸,就要進行擴充套件,有8種擴充套件方式
for(int i = 0; i < 8; ++i) {
int nx = x + direction[i][0];
int ny = y + direction[i][1];
if(nx < 0 || nx >= p || ny < 0 || ny >= q || visit[nx][ny]) {
continue;
}
visit[nx][ny] = true;
char col = ny + 'A';
char row = nx + '1';
if(DFS(nx, ny, step + 1, answer + col + row, p, q)) {
return true;
}
visit[nx][ny] = false; //不再訪問這個狀態,這個狀態下的所有擴充套件狀態均無解
}
return false;
}
int main()
{
int n;
scanf("%d", &n);
int caseNumber = 0;
while(n--) {
int p, q;
cin >> p >> q;
memset(visit, false, sizeof(visit));
cout << "Scenario #" << ++caseNumber << ":" << endl;
visit[0][0] = true;
if(DFS(0, 0, 1, "A1", p, q)) {//壓入初始狀態進行處理
continue;
} else {
cout << "impossible" << endl << endl;
}
}
return 0;
}
3.2 Square
給出若干根長度不一的木棍,問它們能否拼成一個正方形,涉及搜尋樹減枝,這個例子先留個坑。
首先看搜尋問題的4個部分:
-
1、狀態空間: (sum, number) sum指當前已拼湊木棍的長度,第二個number,指已經拼湊成為正方形邊長的個數
-
2、狀態轉移: (sum + stick[i], number),有可能發現狀態變異-> (0, number + 1)
-
3、初始狀態: (0, 0)
-
4、目標狀態: (0, 4)
畫出搜尋樹,由於擴充套件的狀態數目由木棍數目決定,所以不定:
每往下搜尋一層,就多用掉一根木根,所以可以判定木棍是否用完,以及最終拼湊出的邊長個數是否等於4,所以應該用DFS搜尋策略。
因為每個木棍只能使用一次,故要一個visit陣列記錄是否使用過這根木棍。
留個坑,DFS啊~