最基礎(chǔ)的"窮竭搜索"

[最基礎(chǔ)的"窮竭搜索"](http://naotu.baidu.com/file/494b7f6913e6bf43c92d0f2fd08befa6?token=bd529023cef0b4f6)

窮竭搜索是將所有的可能性羅列出來(lái),在其中尋找答案的方法。主要有深度優(yōu)先搜索和廣度優(yōu)先搜索這兩種方法


1.遞歸函數(shù)

在一個(gè)函數(shù)中再次調(diào)用該函數(shù)自身的行為叫做遞歸,這樣的函數(shù)被稱作遞歸函數(shù)。
階乘:

int fact(int n) {
  if (n == 0) return 1; 
  return n * fact(n - 1);
}

斐波那契數(shù)列:

int memo[MAX_N + 1];

int fib(int n) {
  if (n <= 1) return n;
  if (memo[n] != 0) return memo[n];
  return memo[n] = fib(n - 1) + fib(n - 2);
}

2.棧

C++的標(biāo)準(zhǔn)庫(kù)中,stack::pop完成的僅僅是移除最頂端的數(shù)據(jù)。如果要訪問(wèn)最頂端的數(shù)據(jù),需要使用stack::top函數(shù)(這個(gè)操作通常也被稱為peek)。

遞歸函數(shù)的遞歸過(guò)程也可以改用棧上的操作來(lái)實(shí)現(xiàn)?,F(xiàn)實(shí)中需要如此改寫的場(chǎng)合并不多,不過(guò)作為使用棧的練習(xí)試試看也是不錯(cuò)的。以下是使用stack的例子:

#include <stack>
#include <cstdio>

using namespace std;

int main() {
  stack<int> s;            // 聲明存儲(chǔ)int類型數(shù)據(jù)的棧
  s.push(1);               // {} → {1}
  s.push(2);               // {1} → {1,2}
  s.push(3);               // {1,2} → {1,2,3}
  printf("%d\n", s.top()); // 3
  s.pop();                 // 從棧頂移除 {1,2,3}→{1,2}
  printf("%d\n", s.top()); // 2
  s.pop();                 // {1,2} → {1}
  printf("%d\n", s.top()); // 1
  s.pop();                 // {1} → {}
  return 0;
}

3.隊(duì)列

在C++中queue::front是用來(lái)訪問(wèn)最底端數(shù)據(jù)的函數(shù)。以下是使用queue的例子:

#include <queue>
#include <cstdio>

using namespace std;

int main() {
  queue<int> que;              // 聲明存儲(chǔ)int類型數(shù)據(jù)的隊(duì)列
  que.push(1);                 // {} → {1}
  que.push(2);                 // {1} → {1,2}
  que.push(3);                 // {1,2} → {1,2,3}
  printf("%d\n", que.front()); // 1
  que.pop();                   // 從隊(duì)尾移除 {1,2,3}→{2,3}
  printf("%d\n", que.front()); // 2
  que.pop();                   // {2,3} → {3}
  printf("%d\n", que.front()); // 3
  que.pop();                   // {3} → {}
  return 0;
}


4.深度優(yōu)先搜索

深度優(yōu)先搜索(DFS,Depth-First Search)是搜索的手段之一。它從某個(gè)狀態(tài)開始,不斷地轉(zhuǎn)移狀態(tài)直到無(wú)法轉(zhuǎn)移,然后回退到前一步的狀態(tài),繼續(xù)轉(zhuǎn)移到其他狀態(tài),如此不斷重復(fù),直至找到最終的解。例如求解數(shù)獨(dú),首先在某個(gè)格子內(nèi)填入適當(dāng)?shù)臄?shù)字,然后再繼續(xù)在下一個(gè)格子內(nèi)填入數(shù)字,如此繼續(xù)下去。如果發(fā)現(xiàn)某個(gè)格子無(wú)解了,就放棄前一個(gè)格子上選擇的數(shù)字,改用其他可行的數(shù)字。根據(jù)深度優(yōu)先搜索的特點(diǎn),采用遞歸函數(shù)實(shí)現(xiàn)比較簡(jiǎn)單。


部分和問(wèn)題
給定整數(shù)a1、a2、…、an,判斷是否可以從中選出若干數(shù),使它們的和恰好為k。
樣例1

n=4
a={1,2,4,7}
k=13

輸入

n=4
a={1,2,4,7}
k=13

輸出

Yes (13 = 2 + 4 + 7)

樣例2

輸入

n=4
a={1,2,4,7}
k=15

輸出

No
左加右不加
// 輸入
int a[MAX_N];
int n, k;

// 已經(jīng)從前i項(xiàng)得到了和sum,然后對(duì)于i項(xiàng)之后的進(jìn)行分支
bool dfs(int i, int sum) {
  // 如果前n項(xiàng)都計(jì)算過(guò)了,則返回sum是否與k相等
  if (i == n) return sum == k; 

  // 不加上a[i]的情況
  if (dfs(i + 1, sum)) return true;

  // 加上a[i]的情況
  if (dfs(i + 1, sum + a[i])) return true;

  // 無(wú)論是否加上a[i]都不能湊成k就返回false
  return false;
}

void solve() {
  if (dfs(0, 0)) printf("Yes\n");
  else printf("No\n");
}

Lake Counting (POJ No.2386)
有一個(gè)大小為N×M的園子,雨后積起了水。八連通的積水被認(rèn)為是連接在一起的。請(qǐng)求出園子里總共有多少水洼?(八連通指的是下圖中相對(duì)W的*的部分)
輸入

N=10, M=12
園子如下圖('W'表示積水,'.'表示沒(méi)有積水)


W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.


輸出

3
從任意的W開始,不停地把鄰接的部分用'.'代替。1次DFS后與初始的這個(gè)W連接的所有W就都被替換成了'.',因此直到圖中不再存在W為止,總共進(jìn)行DFS的次數(shù)就是答案了。8個(gè)方向共對(duì)應(yīng)了8種狀態(tài)轉(zhuǎn)移,每個(gè)格子作為DFS的參數(shù)至多被調(diào)用一次,所以復(fù)雜度為O(8×N×M)=O(N×M)。

// 輸入
int N, M;
char field[MAX_N][MAX_M + 1]; // 園子

// 現(xiàn)在位置(x,y)
void dfs(int x, int y) {
  // 將現(xiàn)在所在位置替換為.
  field[x][y] = '.';

  // 循環(huán)遍歷移動(dòng)的8個(gè)方向
  for (int dx = -1; dx <= 1; dx++) {
    for (int dy = -1; dy <= 1; dy++) {
      // 向x方向移動(dòng)dx,向y方向移動(dòng)dy,移動(dòng)的結(jié)果為(nx,ny)
      int nx = x + dx, ny = y + dy;
     // 判斷(nx,ny)是不是在園子內(nèi),以及是否有積水
      if (0 <= nx && nx < N && 0 <= ny && ny < M && field[nx][ny] == 'W') dfs(nx, ny);
    }
  }
  return ;
}

void solve() {
  int res = 0;
  for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
      if (field[i][j] == 'W') {
        // 從有W的地方開始dfs
        dfs(i, j);
        res++;
      }
    }
  }
  printf("%d\n", res);
}

5.寬度優(yōu)先搜索

與深度優(yōu)先搜索的不同之處在于搜索的順序,寬度優(yōu)先搜索總是先搜索距離初始狀態(tài)近的狀態(tài)。也就是說(shuō),它是按照開始狀態(tài)→只需1次轉(zhuǎn)移就可以到達(dá)的所有狀態(tài)→只需2次轉(zhuǎn)移就可以到達(dá)的所有狀態(tài)→……這樣的順序進(jìn)行搜索。對(duì)于同一個(gè)狀態(tài),寬度優(yōu)先搜索只經(jīng)過(guò)一次,因此復(fù)雜度為O(狀態(tài)數(shù)×轉(zhuǎn)移的方式)。

深度優(yōu)先搜索(隱式地)利用了棧進(jìn)行計(jì)算,而寬度優(yōu)先搜索則利用了隊(duì)列。搜索時(shí)首先將初始狀態(tài)添加到隊(duì)列里,此后從隊(duì)列的最前端不斷取出狀態(tài),把從該狀態(tài)可以轉(zhuǎn)移到的狀態(tài)中尚未訪問(wèn)過(guò)的部分加入隊(duì)列,如此往復(fù),直至隊(duì)列被取空或找到了問(wèn)題的解。通過(guò)觀察這個(gè)隊(duì)列,我們可以就知道所有的狀態(tài)都是按照距初始狀態(tài)由近及遠(yuǎn)的順序被遍歷的。


迷宮的最短路徑
給定一個(gè)大小為N×M的迷宮。迷宮由通道和墻壁組成,每一步可以向鄰接的上下左右四格的通道移動(dòng)。請(qǐng)求出從起點(diǎn)到終點(diǎn)所需的最小步數(shù)。請(qǐng)注意,本題假定從起點(diǎn)一定可以移動(dòng)到終點(diǎn)。

輸入

N=10, M=10(迷宮如下圖所示。'#','.','S','G'分別表示墻壁、通道、起點(diǎn)和終點(diǎn))

#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#

輸出

22
const int INF = 100000000;

// 使用pair表示狀態(tài)時(shí),使用typedef會(huì)更加方便一些
typedef pair<int, int> P;

// 輸入
char maze[MAX_N][MAX_M + 1];  // 表示迷宮的字符串的數(shù)組
int N, M;
int sx, sy;                   // 起點(diǎn)坐標(biāo)
int gx, gy;                   // 終點(diǎn)坐標(biāo)

int d[MAX_N][MAX_M];          // 到各個(gè)位置的最短距離的數(shù)組

// 4個(gè)方向移動(dòng)的向量
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};

// 求從(sx, sy)到(gx, gy)的最短距離
// 如果無(wú)法到達(dá),則是INF
int bfs() {
  queue<P> que; 
  // 把所有的位置都初始化為INF
  for (int i = 0; i < N; i++)
    for (int j = 0; j < M; j++) d[i][j] = INF;
  // 將起點(diǎn)加入隊(duì)列,并把這一地點(diǎn)的距離設(shè)置為0
  que.push(P(sx, sy));
  d[sx][sy] = 0;

  // 不斷循環(huán)直到隊(duì)列的長(zhǎng)度為0
  while (que.size()) {
    // 從隊(duì)列的最前端取出元素
    P p = que.front(); que.pop();
    // 如果取出的狀態(tài)已經(jīng)是終點(diǎn),則結(jié)束搜索
    if (p.first == gx && p.second == gy) break;

    // 四個(gè)方向的循環(huán)
    for (int i = 0; i < 4; i++) {
      // 移動(dòng)之后的位置記為(nx, ny)
      int nx = p.first + dx[i], ny = p.second + dy[i];

      // 判斷是否可以移動(dòng)以及是否已經(jīng)訪問(wèn)過(guò)(d[nx][ny]!=INF即為已經(jīng)訪問(wèn)過(guò))
      if (0 <= nx && nx < N && 0 <= ny && ny < M && maze[nx][ny] != '#' &&
          d[nx][ny] == INF) {
        // 可以移動(dòng)的話,則加入到隊(duì)列,并且到該位置的距離確定為到p的距離+1
        que.push(P(nx, ny));
        d[nx][ny] = d[p.first][p.second] + 1;
      }
    }
  }
  return d[gx][gy];
}

void solve() {
  int res = bfs();
  printf("%d\n", res);
}

寬度優(yōu)先搜索與深度優(yōu)先搜索一樣,都會(huì)生成所有能夠遍歷到的狀態(tài),因此需要對(duì)所有狀態(tài)進(jìn)行處理時(shí)使用寬度優(yōu)先搜索也是可以的。但是遞歸函數(shù)可以很簡(jiǎn)短地編寫,而且狀態(tài)的管理也更簡(jiǎn)單,所以大多數(shù)情況下還是用深度優(yōu)先搜索實(shí)現(xiàn)。反之,在求取最短路時(shí)深度優(yōu)先搜索需要反復(fù)經(jīng)過(guò)同樣的狀態(tài),所以此時(shí)還是使用寬度優(yōu)先搜索為好。

寬度優(yōu)先搜索會(huì)把狀態(tài)逐個(gè)加入隊(duì)列,因此通常需要與狀態(tài)數(shù)成正比的內(nèi)存空間。反之,深度優(yōu)先搜索是與最大的遞歸深度成正比的。一般與狀態(tài)數(shù)相比,遞歸的深度并不會(huì)太大,所以可以認(rèn)為深度優(yōu)先搜索更加節(jié)省內(nèi)存。

此外,也有采用與寬度優(yōu)先搜索類似的狀態(tài)轉(zhuǎn)移順序,并且注重節(jié)約內(nèi)存占用的迭代加深深度優(yōu)先搜索(IDDFS,Iterative Deepening Depth-First Search)。IDDFS是一種在最開始將深度優(yōu)先搜索的遞歸次數(shù)限制在1次,在找到解之前不斷增加遞歸深度的方法


6.特殊狀態(tài)的枚舉

雖然生成可行解空間多數(shù)采用深度優(yōu)先搜索,但在狀態(tài)空間比較特殊時(shí)其實(shí)可以很簡(jiǎn)短地實(shí)現(xiàn)。比如,C++的標(biāo)準(zhǔn)庫(kù)中提供了next_permutation這一函數(shù),可以把n個(gè)元素共n!種不同的排列生成出來(lái)。又或者,通過(guò)使用位運(yùn)算,可以枚舉從n個(gè)元素中取出k個(gè)的狀態(tài)或是某個(gè)集合中的全部子集等。

bool used[MAX_N];
int perm[MAX_N];

// 生成{0,1,2,3,4,...,n-1}的n!種排列

void permutation1(int pos, int n) {
  if (pos == n) {
    /*
     * 這里編寫需要對(duì)perm進(jìn)行的操作
     */
    return ;
  }

  // 針對(duì)perm的第pos個(gè)位置,究竟使用0~n-1中的哪一個(gè)進(jìn)行循環(huán)
  for (int i = 0; i < n; i++) {
    if (!used[i]) {
      perm[pos] = i;
      // i已經(jīng)被使用了,所以把標(biāo)志設(shè)置為true
      used[i] = true;
      permutation1(pos + 1, n);
      // 返回之后把標(biāo)志復(fù)位
      used[i] = false;
    }
  }
  return ;
}

#include <algorithm>

// 即使有重復(fù)的元素也會(huì)生成所有的排列
// next_permutation是按照字典序來(lái)生成下一個(gè)排列的
int perm2[MAX_N];

void permutation2(int n) {
  for (int i = 0; i < n; i++) {
    perm2[i] = i;
  }
  do {
    /*
     * 這里編寫需要對(duì)perm2進(jìn)行的操作
     */
  } while (next_permutation(perm2, perm2 + n));
// 所有的排列都生成后,next_permutation會(huì)返回false
  return ;
}

7.剪枝

顧名思義,窮竭搜索會(huì)把所有可能的解都檢查一遍,當(dāng)解空間非常大時(shí),復(fù)雜度也會(huì)相應(yīng)變大。
比如n個(gè)元素進(jìn)行排列時(shí)狀態(tài)數(shù)總共有n!個(gè),復(fù)雜度也就成了O(n!)。這樣的話,即使n=15計(jì)算也很難較早終止。這里簡(jiǎn)單介紹一下此類情形要如何進(jìn)行優(yōu)化。
深度優(yōu)先搜索時(shí),有時(shí)早已很明確地知道從當(dāng)前狀態(tài)無(wú)論如何轉(zhuǎn)移都不會(huì)存在解。這種情況下,不再繼續(xù)搜索而是直接跳過(guò),這一方法被稱作剪枝。
我們回想一下深度優(yōu)先搜索的例題“部分和問(wèn)題”。這個(gè)問(wèn)題中的限制條件如果變?yōu)?≤ai≤108,那么在遞歸中只要sum超過(guò)k了,此后無(wú)論選擇哪些數(shù)都不可能讓sum等于k,所以此后沒(méi)有必要繼續(xù)搜索。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 遞歸函數(shù) 棧 隊(duì)列 深度優(yōu)先搜索 寬度優(yōu)先搜索 2.1.1 遞歸函數(shù) 在一個(gè)函數(shù)中再次調(diào)用該函數(shù)本身的行為叫做遞歸...
    Nathanpro閱讀 669評(píng)論 0 3
  • 歸去來(lái)兮。 1.1 說(shuō)明 本篇為《挑戰(zhàn)程序設(shè)計(jì)競(jìng)賽(第2版)》[http://www.ituring.com.cn...
    尤汐Yogy閱讀 14,921評(píng)論 0 160
  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學(xué)在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,397評(píng)論 0 12
  • 我,大琴身高不足1米六,體重及超160。有一枚鮮肉丈夫,他對(duì)大琴寵愛(ài)有家,大琴是枚懶姑娘,卻愛(ài)吃……有時(shí)候不想動(dòng)手...
    玥玥的牙兒閱讀 257評(píng)論 1 1
  • 可能想要記錄點(diǎn)什么,到處都是電腦,方顯筆跡的彌足珍貴。有一段時(shí)間會(huì)有寫日記的習(xí)慣,但是并沒(méi)有天天都寫,因?yàn)椴⒉?..
    有楓葉的地方閱讀 235評(píng)論 0 1

友情鏈接更多精彩內(nèi)容