1. 程式人生 > 其它 >搜尋——深搜+廣搜

搜尋——深搜+廣搜

技術標籤:演算法深度搜索廣度搜索c++

搜尋

蘊含遞迴的思想,通過不停的試探去尋求解
常用方法有:暴力的搜尋、深搜(DFS)、廣搜(BFS)

引入

像是圖的遍歷一樣,按照某個規則去訪問圖中的所有頂點,且每個頂點只被訪問一次。

深度優先遍歷(DFS)

就是要走就先走完一整條路,再返回去走別的可能的路。
舉個栗子:在這裡插入圖片描述
它的頂點訪問次序就是以下幾種:
V1>V2>V4>V8>V5>V3>V6>V7
V1>V2>V5>V8>V4>V3>V6>V7
V1>V2>V4>V8>V5>V3>V7>V6

V1>V2>V5>V8>V4>V3>V7>V6
V1>V3>V6>V7>V2>V4>V8>V5

方法:

1.訪問指定的起始頂點
2.若當前訪問的頂點的鄰接頂點有未被訪問的(通過標記陣列實現),則任選一個去訪問;反之,退回到最近訪問過的頂點;直到與起始點相通的全部頂點訪問完畢
3.若圖中還有頂點未被訪問,再選其中一個頂點作為起始頂點進行訪問,進行步驟2;反之,遍歷結束

演算法過程:

狀態A(我是誰,我在哪)
1.判斷當前狀態是否滿足題目需要,滿足則進行儲存,比較,輸出等操作
2.判斷當前狀態是否合法(當前狀態是否滿足題目要求,或者陣列是否越界),滿足繼續執行否則回到上次呼叫

3.往下走一層,遞迴呼叫dfs()

exp1:求出n的全排列

法一:全排列函式

next_permutation(start,end):求出當前排列的下一個排列
prev_permutation(start,end)求出當前排列的上一個排列
當前序列不存在下一個排列時函式返回false,否則返回true
程式碼如下:

#include <iostream>
#include <algorithm> //此標頭檔案包含全排列函式

using namespace std;

int arr[1000];
int main()
{
    int n;
    cin>>n;
    for(int i = 0; i < n; i++) arr[i] = i + 1;
    //採用do——while迴圈來輸出初始的序列
     do{
        for(int i = 0; i < n; i++) cout << arr[i] << " ";
        cout << endl;
     }while(next_permutation(arr, arr+n));
     return 0;
}

法二:採用深搜

把1~n的數字看成一個個的小球,而每個小球所處的位置看成一個個的盒子,且對於全排列來說每個盒子只能放1到n編號的球,每個盒子不能放之前放過的球。

模板:

1:首先將遍歷過的進行標記
2:將當前狀態賦值
3:遞迴搜尋
4:回到當前狀態以後,之前的標記要消除

程式碼如下:

#include <iostream>
#include <algorithm>

using namespace std;

int arr[1000];
int book[1000]; //用來表示此數字有沒有被用過,初始化為0表示沒用過
int n;
void dfs(int step)//step表示當前處於第step個盒子
{
    if(step==n+1) //先寫退出條件,讓答案輸出
    {
        for(int i  = 1; i <= n; i++) cout<< arr[i] << ' ';
        cout << endl;
        return;  //此題目一定要寫,不返回的話會一直搜尋下去然後卡死
    }//讓它返回到第n個盒子繼續處理
    for(int i = 1; i <= n; i++)
    {//遍歷盒子如果book[i]顯示為0的話表示沒用過,可放到盒子裡去
        if(!book[i])
        {
            book[i] = 1; //先讓他標記為1表示用過了
            arr[step] = i;//放到盒子裡去
            dfs(step+1);//繼續搜尋下一個地方
            book[i] = 0;//消除之前的標記
        }
    }
}
int main()
{
    cin>>n;
    dfs(1);
    return 0;
}

exp2:整數劃分

一個正整數可以劃分為多個正整數的和,比如n=6時: 6;1+5;2+4;3+3;2+2+2;1+1+4;1+2+3;1+1+1+3;1+1+2+2;1+1+1+1+2;1+1+1+1+1+1 共有十一種劃分方法。   
給出一個正整數,問有多少種劃分方法。

基本思路:正整數n可以看成n個小球,每一個小球代表數字一,觀察可知劃分即為將這n個小球放入k的盒子裡面,且每一個盒子不能為空,則k依次為從1到n,k=n時即為n個1相加,每次搜尋k個盒子

這裡如果只是簡單的搜尋,我們是不能AC的,因為會TLE,所以我們還要進行搜尋剪枝操作(也就是去除一些顯然不可能的情況)

程式碼如下:
1.最初始的程式碼,沒有剪枝肯定是過不去的

#include <iostream>
#include <cstdio>

using namespace std;

int arr[1000];
int n;
int sum;

void dfs(int step, int k)
{
    if(step == k+1) //退出條件, 第step個盒子變成k+1個
    {
        int num = 0;
        for(int i = 1; i <= n; i++)
            num += arr[i];
        if(num == n)
        {
            printf("%d=%d",n,arr[1]);
            for(int i = 2; i <= k; i++)
                printf("+%d",arr[i]);
            cout << endl;
            sum++;
        }
        return;
    }
    for(int i = 1; i <= n; i++)
    {
        arr[step] = i;//數字可以重複使用,沒有必要去標記
        if(arr[step] >= arr[step - 1])
        {//因為1 2 3和3 2 1是一樣的,所以要制定一個規則讓後一個數大於等於前一個數
            dfs(step + 1, k);
        }
    }
}

int main()
{
    cin>>n;
    for(int i = 1; i <= n; i++)
        dfs(1,i);
    cout << sum;
    return 0;
}

2.進行剪枝的程式碼

#include <iostream>
#include <cstdio>

using namespace std;

int arr[1000];
int n;
int sum;

void dfs(int step, int k, int m) //m表示剩餘數字的和即還沒有放進去的數字
{
    if(step == k+1)//超出盒子個數
    {
        if(m == 0)//並且數字全部放完
        {
            printf("%d=%d",n,arr[1]);
            for(int i = 2; i<=k; i++)
                printf("+%d",arr[i]);
            cout << endl;
            sum++;
        }
        return;
    }
    for(int i = arr[step - 1]; i <= m/(k-step+1); i++)
    {   //保證前面的數字小於後面的數字,每次從step-1開始列舉,且保證列舉的數字不能超過後面總和的平均數
        //例如1 2 3 4,為一組解,那麼在列舉arr[3]的時候,此時sum = 7,sum/(k-step+1)=3.5,則arr[3]不能超過3,否則arr[4]必定比arr[3]要小
        //不滿足前面的數字比後面的數字大,造成重複計算
        arr[step] = i;
        dfs(step+1, k, m-i);
    }
}
int main()
{
    cin>>n;
    arr[0]=1;//因為有step-1,會出現arr[0],所以將其設定為1
    for(int i = 1; i <= n; i++)
    {
        dfs(1, i, n);
    }
    cout << sum;
    return 0;
}

exp3:迷宮問題

定義一個n行n列的二維陣列
例如n=5時:
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
它表示一個迷宮,其中的1表示牆壁,0表示可以走的路,只能橫著走或豎著走,不能斜著走,要求程式設計序找出從左上角到右下角的最短路線。

基本思路:從起點出發,每次按照順序遍歷四個方向嘗試所有情況,如果合法就進入下一層
程式碼如下:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
ll n, m, min = 0x3fffffff;
ll arr[501][501], book[501][501];//book為標記陣列
ll dx[] = {0,1,0,-1};//x方向上的偏移量
ll dy[] = {1,0,-1,0};//y方向上的偏移量
void dfs(int x, int y, ll step)//x,y表示當前所在的點的橫縱座標,step表示當前的步數
{
    ll tx, ty, k;
    if (x == n - 1 && y == n - 1)//如果當前所在位置是終點,說明已經走完
    {
        if (step < min)//如果比最短路徑還要小,更新min
            min = step;
        return;//注意
    }
    for (k = 0; k < 4; k++) //跟新下一個座標
    {
        tx = x + dx[k];//利用此方法,算出下一個需要走的點的座標
        ty = y + dy[k];
        if (tx<0 || tx>n-1 || ty<0 || ty>n-1)//判斷邊界,如果越界就不執行
            continue;
        if (book[tx][ty] == 0 && arr[tx][ty] == 0) //當前沒有走過並且當前可以走不是牆
        {
            book[tx][ty] = 1;  //標記為走過
            dfs(tx, ty, step + 1);//搜素下個點
            book[tx][ty] = 0;  //恢復
        }
    }
}

int main(void)
{
    ll i, j;
    scanf("%lld", &n);
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
            scanf("%lld", &arr[i][j]);
    }
    book[0][0] = 1;//起點首先標記為走過
    dfs(0, 0, 0);//從起點開始搜尋
    printf("%lld\n", min);
    return 0;
}

廣度優先遍歷(BFS)

和深搜不同,它是一層一層地搜尋
舉個栗子:
在這裡插入圖片描述
它的搜尋順序有以下幾種:
V1>V2>V3>V4>V5>V6>V7>V8
V1>V3>V2>V7>V6>V5>V4>V8
V1>V3>V2>V5>V4>V7>V6>V8

方法:
從圖的某一結點出發,首先一次訪問該節點的所有鄰接點,再按這些頂點被訪問的先後次序依次訪問與他們相鄰接且未被訪問的所有的點,重複此過程,直至所有頂點均被訪問為止。

對於exp3的迷宮問題,用BFS來求解:
依次找出一步可以到達的點、兩步可到達的、三步…將每次走過的點加入佇列來進行擴充套件。

程式碼如下:

法一

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
ll n, m, min = 0x3fffffff;
ll arr[501][501], book[501][501];//book為標記陣列
ll dx[] = {0,1,0,-1};//x方向上的偏移量
ll dy[] = {1,0,-1,0};//y方向上的偏移量
void dfs(int x, int y, ll step)//x,y表示當前所在的點的橫縱座標,step表示當前的步數
{
    ll tx, ty, k;
    if (x == n - 1 && y == n - 1)//如果當前所在位置是終點,說明已經走完
    {
        if (step < min)//如果比最短路徑還要小,更新min
            min = step;
        return;//注意
    }
    for (k = 0; k < 4; k++) //跟新下一個座標
    {
        tx = x + dx[k];//利用此方法,算出下一個需要走的點的座標
        ty = y + dy[k];
        if (tx<0 || tx>n-1 || ty<0 || ty>n-1)//判斷邊界,如果越界就不執行
            continue;
        if (book[tx][ty] == 0 && arr[tx][ty] == 0) //當前沒有走過並且當前可以走不是牆
        {
            book[tx][ty] = 1;  //標記為走過
            dfs(tx, ty, step + 1);//搜素下個點
            book[tx][ty] = 0;  //恢復
        }
    }
}

int main(void)
{
    ll i, j;
    scanf("%lld", &n);
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
            scanf("%lld", &arr[i][j]);
    }
    book[0][0] = 1;//起點首先標記為走過
    dfs(0, 0, 0);//從起點開始搜尋
    printf("%lld\n", min);
    return 0;
}

法二:用STL寫

#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
typedef long long ll;
ll arr[501][501];
ll book[501][501];
ll dx[] = {0,1,0,-1};//x方向上的
ll dy[] = {1,0,-1,0};//y方向上的
struct f {//定義結構體
    int x;//表示橫座標
    int y;//縱座標
    int s;//走過的步數
};
queue<f>qu;//定義維護的佇列
ll n;
void bfs()
{
    struct  f a;
    a.x = 0;//設定起點位置並加入佇列
    a.y = 0;
    a.s = 0;
    qu.push(a);
    book[0][0] = 1;//首先將起點標記
    while (!qu.empty())//如果佇列不為空
    {
        int k = 0;
        struct f b = qu.front();//每次取隊首元素
        qu.pop();//取出之後就彈出
        for (int i = 0; i < 4; i++)
        {
            struct f c;
            c.x = b.x + dx[i];
            c.y = b.y + dy[i];
            c.s = b.s + 1;
            if (c.x<0 || c.x>n - 1 || c.y<0 || c.y>n - 1)
                continue;
            if (book[c.x][c.y] == 0 && arr[c.x][c.y] == 0)//如果這個點可以走且沒有標記過,就加入佇列
            {
                book[c.x][c.y] = 1;
                qu.push(c);
                if (c.x == n - 1 && c.y == n - 1)//如果是終點就輸出
                {
                    k = 1;
                    cout << c.s;
                    break;
                }
            }
        }
        if (k)break;
    }
}
int main(void)
{
    ll i, j;
    scanf("%lld", &n);
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
            scanf("%lld", &arr[i][j]);
    }
    bfs();
    return 0;
}

變式:若要打印出行走路徑則:

#include <iostream>
#include <algorithm>

using namespace std;
int arr[501][501];
int book[501][501];
int dx[] = { 0,1,0,-1 };//規定四個方向
int dy[] = { 1,0,-1,0 };
struct f {
    int x;
    int y;
    int s;
    int f;//記錄其父親的標號
}map[2500];
int n;
void print(int x)//函式功能:列印下標為x的點的座標
{
    if (map[x].f != -1)//如果其父親不是起點,就執行遞迴
    {
        print(map[x].f);
        printf("( %d , %d )\n", map[x].x, map[x].y);//列印的座標
    }
}
void bfs()
{
    int tail = 1, head = 1;//將起點入隊
    map[tail].x = 0, map[tail].y = 0;
    map[tail].s = 0, map[tail].f = -1;
    book[0][0] = 1;//標記起點
    tail++;
    while (head < tail)
    {
        int k = 0;
        for (int i = 0; i < 4; i++)//遍歷四個方向
        {
            int nx = map[head].x + dx[i];//計算下一個方向
            int ny = map[head].y + dy[i];
            if (nx<0 || nx>n - 1 || ny<0 || ny>n - 1)
                continue;
            if (book[nx][ny] == 0 && arr[nx][ny] == 0)
            {
                book[nx][ny] = 1;
                map[tail].x = nx;//更新佇列
                map[tail].y = ny;
                map[tail].s = map[head].s + 1;//等於父親步數加一
                map[tail].f = head;//記錄其父親
                tail++;
            }
            if (nx == n - 1 && ny == n - 1)//如果等於終點
            {
                k = 1;
                cout << map[tail - 1].s<<endl;//注意要減一
                print(head);//找到了,就開始列印路徑
                printf("( 0 , 0 )\n");
                break;
            }
        }   
        if (k == 1)
            break;
        head++;//四個方向探索完畢後,head++模擬出隊效果

    }
}
int main(void)
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
            scanf("%d", &arr[i][j]);
    }
    bfs();
    return 0;
}