chapter9-搜尋

paopaotangzu發表於2024-03-13

搜尋是一種有目的地列舉問題的解空間中部分或全部情況,進而找到解的方法。它的定義是:

起始狀態經過一系列的狀態轉移抵達目標狀態,我們一般用搜尋樹(Search Tree)來表示狀態轉移

搜尋一般包括4個部分:

  • 1、狀態空間,也叫解空間;

  • 2、狀態轉移;

  • 3、起始狀態;

  • 4、目標狀態。

如何構思上面的四個部分呢,比如狀態空間是什麼,舉個例子:
搜尋舉例1.jpg

搜尋舉例2.jpg

1.Search Trees

以尋找從點A到點E的路徑問題為例,建立一顆搜尋樹,建好搜尋樹之後,還需要確定搜尋策略,即先擴充套件樹中的哪個結點,搜尋策略有寬度優先搜尋、深度優先搜尋兩種。

搜尋樹.jpg

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)

然後建立一顆搜尋樹:
搜尋樹抓牛.jpg

確定搜尋策略:抓到牛並且花費時間最少,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

然後建立一顆搜尋樹:
倍數搜尋樹.jpg

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

像象棋一樣,馬這個棋字的行走規則是“日”字。
馬走日.jpg

同樣首先分析搜尋問題的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) 在哪點終止不確定,但格子數確定

畫出搜尋樹:
騎士搜尋樹.jpg

題目要求是找到一條路徑能夠遍歷到棋盤的每一格,相當於要求搜尋樹的深度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搜尋策略
拼成正方形.jpg

因為每個木棍只能使用一次,故要一個visit陣列記錄是否使用過這根木棍。

留個坑,DFS啊~

相關文章