什么是搜索算法
搜索算法是利用計算機的高性能來有目的的窮舉一個問題解空間的部分或所有的可能情況,從而求出問題的解的一種方法。現階段一般有枚舉算法、深度優先搜索、廣度優先搜索、A*算法、回溯算法、蒙特卡洛樹搜索、散列函數等算法。在大規模實驗環境中,通常通過在搜索前,根據條件降低搜索規模;根據問題的約束條件進行剪枝;利用搜索過程中的中間解,避免重復計算這幾種方法進行優化。
搜索方式
首先給大家掃個盲在搜索中,不僅僅只有常見的遞歸式搜索,也存在着一部分正向迭代式搜索,但是在真正的使用中遞歸式搜索占到了絕大多數,基本上所有的遞歸式搜索用是遞歸都可以實現只不過代價比較大
比如我們想要求出數字 1 - 3之間所有數字的全排列
這個問題很簡單,簡單到不想用手寫。。還是寫一下吧
對於這個問題我們只用三重循環就可以完全搞定它
int n = 3;
for(int i = 1;i <= n ;i ++)
{
for(int j = 1;j <= n ;j ++)
{
for(int k = 1;k <= n; k++)
{
if(i != j && i != k && j != k)
{
printf("%d %d %d\n",i ,j ,k);
}
}
}
}
這個時候有同學就會問了,既然用遞推就可以實現我們的搜索那么我們為什么還要費勁的去寫遞歸呢?
原因是之前舉的哪一個例子規模很小,如果此時我們講n 換成10 我們需要枚舉 1 - 10的全排列那么你用遞推的話代碼大致式這樣的
int n = 3;
for(int i = 1;i <= n ;i ++)
{
for(int j = 1;j <= n ;j ++)
{
for(int k = 1;k <= n; k++)
{
for(int o = 1; 0 <= n ;o++)
{
for(int p = 1;p <= n ; p++)
{
for()
{
for()
.......不寫了 我吐了
}
}
}
}
}
}
首先不說你有沒有心情實現,光是變量的字母引用 就夠你喝一壺了 這里n = 10 還沒超過26,那如果超過了26,你豈不是要把漢字搬來了....
這里就暴露出一個問題。我們的迭代式搜索存在着一個很大的局限性,那就是所能夠完成的規模有限,而且代碼十分冗余。此時就展現出我們遞歸搜索的強大了
void serch(int now)
{
if(now == n)
{
for(int i = 0;i < n; i++)
printf("%d",a[i]);
return;
}else{
for(int i = 1;i <= n;i ++)
{
if(!vis[i]){
a[now] = i;vis[i] = 1;
dfs(now+1);vis[i] = 0;
}
}
}
}
同樣的實現1-10的全排列 我們利用遞歸很簡單的就實現了,但是遞推卻難以實現。
既然我們已經知道了遞歸式搜索的好處那么我們就來先了解遞歸搜索的靈魂遞歸樹
遞歸樹的引入
我們會發現無論是我們的正向窮舉算法還是我們的遞歸式搜索枚舉都可以產生一棵搜索樹,而這顆搜索樹也通常是輔助我們解決問題的關鍵,當然正向遞推枚舉會比遞歸枚舉要快的多,其差別之一就是迭代式的搜索算法不會用到系統棧,而遞歸的搜索會大量的調用系統棧。
在我們寫遞歸搜索算法得時候通常會有一個小問題,我該在什么時候讓他自身調用自身去進行深層遞歸呢
還是我們之前得那個問題 在得出所有1 - 3 的全排列
首先我們思考 既然是 1 - 3 的全部排列那說明所有的位置只能是有三個 :_ _ _ 我們在這三個位置上填充數字即可每一個位置都可以填充 1 - 3這三個數字
於是我們可以得到如下結構的一顆樹:
由於畫圖技術爛沒能把樹畫完望大家諒解
根據上圖我們發現雖然上圖是一個完整的遞歸樹名單時並不是我們想要的遞歸樹,因為我們需要讓一個排列中的樹字互相都不重復。那么我們就需要進行一些操作把我們不需要的分支給去掉
於是我們得到
這種將不需要的分支給去掉的操作我們將其稱之為剪枝操作
這就是一個簡單的遞歸搜索程序下面把代碼貼出來,同學們細心觀察一下我們的剪枝操作究竟在哪里體現的呢
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 10;
int n,a[N];bool vis[N];
void dfs(int now)
{
if(now == n)
{
for(int i = 0;i < n; i++)
cout << a[i] << " ";
cout << endl;
}
for(int i = 1;i <= n;i ++)
{
if(!vis[i])
{
a[now] = i;vis[i] = true;
dfs(now+1);vis[i] = false;
}
}
}
int main()
{
cin>> n;dfs(0);
return 0;
}
回溯
一、什么是回溯算法
回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
回溯算法實際上一個類似枚舉的深度優先搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回(也就是遞歸返回),嘗試別的路徑。
二、遞歸函數的參數的選擇,要遵循四個原則
1、必須要有一個臨時變量(可以就直接傳遞一個字面量或者常量進去)傳遞不完整的解,因為每一步選擇后,暫時還沒構成完整的解,這個時候這個選擇的不完整解,也要想辦法傳遞給遞歸函數。也就是,把每次遞歸的不同情況傳遞給遞歸調用的函數。
2、可以有一個全局變量,用來存儲完整的每個解,一般是個集合容器(也不一定要有這樣一個變量,因為每次符合結束條件,不完整解就是完整解了,直接打印即可)。
3、最重要的一點,一定要在參數設計中,可以得到結束條件。一個選擇是可以傳遞一個量n,也許是數組的長度,也許是數量,等等。
4、要保證遞歸函數返回后,狀態可以恢復到遞歸前,以此達到真正回溯。
相信大家已經知道了遞歸樹的含義那么,接下來我們來給大家說說回溯的過程。
如果大家仔細觀察上面的代碼大家可能會疑惑我的搜索的代碼中為什么會出現
a[now] = i;
vis[i] = true;
dfs(now+1);
vis[i] = false;
大家可能會疑惑為什么我將vis[i] 設置成為了 true 之后在執行完遞歸調用后為什么又多了一步又將vis[i] = false,而這看似不起眼的關鍵的一步就是我們dfs算法中常用的回溯,也叫做恢復現場
其實原來的代碼應該本來應該是這樣
a[now] = i;
vis[i] = true;
dfs(now+1);
vis[i] = false;
a[now] = 0;
也就是說 我們在進入下一個遞歸調用之前我們對我們的局部變量或者全局變量進行了某些操作,我們在這個遞歸嗲用之前我們要保證它恢復至原來的狀態
拿我們以上的狀態舉例
我們從root -> 1的這一個分支看
從1在往下共有兩個分支可以走 -->如果我們走了2的話那么勢必會對a數組做出改動但是我們3的路徑還沒走,為了確保我們走了第二個路徑的結果對第三個路徑的結果不產生影響我們在走完第二個路徑分治后密鑰將狀態恢復為進入2之前的狀態,這樣才能做到路徑之間互不干擾。協同搜索出來所有結果
深度優先搜索(dfs)
掌握了遞歸樹和回溯之后我們就可以來了解深度優先搜索這個概念啦,深度優先搜索大部分時候就是以上兩種情況的結合使用(其實回溯就是一個搜索算法)反正本蒟蒻認為以上幾種算法大部分時候時候邊界模糊,其實dfs就是一個普適的搜索模板所以一定要掌握好。學好搜索你可以解決很多的問題。
深度優先搜索的來源
深度優先搜索是一種在開發爬蟲早期使用較多的方法。它的目的是要達到被搜索結構的葉結點(即那些不包含任何超鏈的HTML文件) 。在一個HTML文件中,當一個超鏈被選擇后,被鏈接的HTML文件將執行深度優先搜索,即在搜索其余的超鏈結果之前必須先完整地搜索單獨的一條鏈。深度優先搜索沿着HTML文件上的超鏈走到不能再深入為止,然后返回到某一個HTML文件,再繼續選擇該HTML文件中的其他超鏈。當不再有其他超鏈可選擇時,說明搜索已經結束。
深度優先搜索框架
下面給出我常用的深度優先搜索算法的模板
void dfs(根據需要傳遞相應的參數)
{
if(滿足條件)退出當前函數
if(剪枝)剪掉該分支
如果以上都不滿足就枚舉情況進行遞歸
for(......)
{
狀態標記
dfs(...)
狀態恢復
}
}
下面從一道題目給大家引入深度優先搜索在圖上的應用
題目傳送門
我的解法C++
#include <iostream>
using namespace std;
int n, m; //行,列
int t;//障礙總數
int map[6][6];
int zx, zy;//障礙物的坐標
int sx, sy;//起始坐標
int tx, ty;//終點坐標
int step;
int _next[4][2] = { { -1, 0 },{ 0, 1 },{ 0, -1 },{ 1, 0 } };
//判斷這個坐標是否在map里
bool inmap(int x, int y)
{
return (x >= 1 && x <= n && y >= 1 && y <= n);
}
void dfs(int x, int y)
{
if (x == tx && y == ty)
{
step++;return;
}
for (int i = 0; i < 4; i++)
{
int nx = x + _next[i][0];
int ny = y + _next[i][1];
if (inmap(nx, ny) == 1 && map[nx][ny] != 1 && map[nx][ny] != 2)
{
map[nx][ny] = 2;
dfs(nx, ny);
map[nx][ny] = 0; // 恢復現場
}
}
}
int main()
{
cin >> n >> m >> t;
cin >> sx >> sy;//輸入起始坐標
cin >> tx >> ty;//輸入終點坐標
for (int i = 0; i < t; i++)
{
cin >> zx >> zy;
map[zx][zy] = 1;//障礙物設置成一
}
map[sx][sy] = 2;
dfs(sx, sy);
cout << step << endl;
}
我的解法Java
import java.util.Scanner;
public class Main{
static int n , m , t ,zx ,zy,sx,sy,tx,ty,step;
static int map[][] = new int[6][6];
static int dir[][] = { { -1, 0 },{ 0, 1 },{ 0, -1 },{ 1, 0 } };
public static boolean inmap(int x,int y) {
return (x >= 1 && x <= n && y >= 1 && y <= n);
}
public static void dfs(int x,int y) {
if(x == tx && y == ty){
step++;return;
}
for(int i = 0;i < 4 ;i ++) {
int nx = x + dir[i][0];
int ny = y + dir[i][1];
if(inmap(nx,ny) && map[nx][ny] != 1 && map[nx][ny] != 2){
map[nx][ny] = 2;
dfs(nx, ny);
map[nx][ny] = 0;
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();m = sc.nextInt();t = sc.nextInt();
sx = sc.nextInt();sy = sc.nextInt();
tx = sc.nextInt();ty = sc.nextInt();
for(int i = 0;i < t;i ++) {
zx = sc.nextInt();zy = sc.nextInt();
map[zx][zy] = 1;
}
map[sx][sy] = 2;
dfs(sx,sy);
System.out.println(step);
}
}