【蓝桥杯C/C++】专题五:DFS深度优先搜索
專題五:DFS深度優(yōu)先搜索
目錄
- 專題五:DFS深度優(yōu)先搜索
- 前言
- 什么是回溯法
- 如何理解回溯法
- 回溯法解決的問題
- 回溯法模板
- 1 、回溯函數(shù)模板返回值以及參數(shù)
- 2、 回溯函數(shù)終止條件
- 3 、回溯搜索的遍歷過程
- 回溯算法模板框架代碼如下
- 遞歸實現(xiàn)指數(shù)型枚舉
- 題目
- 代碼及注釋
- 方法一:遞歸枚舉法(子集)
- 方法二:遞歸填坑法(每個數(shù)字選與不選)
- 題解
- 遞歸實現(xiàn)排列型枚舉
- 題目
- 代碼及注釋
- 題解
- 遞歸實現(xiàn)組合型枚舉
- 題目
- 代碼及注釋
- 題解
- 迷宮問題
- 題目
- 代碼及注釋
- 題解
- 01背包問題
- 題目
- 代碼及注釋
- 題解
- 八皇后
- 題目
- 代碼及注釋
- 題解
- 方格分割
- 題目
- 代碼及注釋
- 題解
- 組隊
- 題目
- 代碼及注釋
- 題解
- 總結(jié)
前言
本專題將講解算法競賽中最常用的算法dfs深度優(yōu)先搜索,也叫回溯搜索法或者“暴力搜索”法,也就是說在比賽的時候就算遇到?jīng)]有思路的題,也可以用遞歸實現(xiàn)暴力搜索來騙分。有的同學(xué)可能會過多的去糾結(jié)一些概念,比如遞歸、暴力搜索、回溯法、dfs等,其實我們大可不必去糾結(jié),因為dfs和回溯搜索法本身就是一個算法,是用遞歸操作來實現(xiàn)的,而“暴力搜索”則是民間賦予的稱號!! 以下內(nèi)容,我統(tǒng)稱為回溯法(我最喜歡的名字)!
什么是回溯法
回溯法也可以叫做回溯搜索法,它是一種搜索的方式。
回溯的本質(zhì)是窮舉,窮舉所有可能,然后選出我們想要的答案,也就是暴力搜索。
如何理解回溯法
回溯法解決的問題都可以抽象為樹形結(jié)構(gòu),是的,我指的是所有回溯法的問題都可以抽象為樹形結(jié)構(gòu)!
因為回溯法解決的都是在集合中遞歸查找子集,集合的大小就構(gòu)成了樹的寬度,遞歸的深度,都構(gòu)成的樹的深度。
遞歸就要有終止條件,所以必然是一棵高度有限的樹(N叉樹)。
遞歸里面嵌套著循環(huán),為單層搜索邏輯。
回溯法解決的問題
回溯法,一般可以解決如下幾種問題:
- 組合問題:N個數(shù)里面按一定規(guī)則找出k個數(shù)的集合
- 切割問題:一個字符串按一定規(guī)則有幾種切割方式
- 子集問題:一個N個數(shù)的集合里有多少符合條件的子集
- 排列問題:N個數(shù)按一定規(guī)則全排列,有幾種排列方式
- 棋盤問題:N皇后,解數(shù)獨等等
回溯法模板
1 、回溯函數(shù)模板返回值以及參數(shù)
在回溯算法中,我的習(xí)慣是函數(shù)起名字為backtracking,這個起名大家隨意。
回溯算法中函數(shù)返回值一般為void。
再來看一下參數(shù),因為回溯算法需要的參數(shù)可不像二叉樹遞歸的時候那么容易一次性確定下來,所以一般是先寫邏輯,然后需要什么參數(shù),就填什么參數(shù)。
void backtracking(參數(shù))2、 回溯函數(shù)終止條件
什么時候達到了終止條件,樹中就可以看出,一般來說搜到葉子節(jié)點了,也就找到了滿足條件的一條答案,把這個答案存放起來,并結(jié)束本層遞歸。
所以回溯函數(shù)終止條件偽代碼如下:
if (終止條件) {存放結(jié)果;return; }3 、回溯搜索的遍歷過程
在上面我們提到了,回溯法一般是在集合中遞歸搜索,集合的大小構(gòu)成了樹的寬度,遞歸的深度構(gòu)成的樹的深度。
分享一張代碼隨想錄的圖
回溯函數(shù)遍歷過程偽代碼如下:
for循環(huán)就是遍歷集合區(qū)間,可以理解一個節(jié)點有多少個孩子,這個for循環(huán)就執(zhí)行多少次。
backtracking這里自己調(diào)用自己,實現(xiàn)遞歸。
大家可以從圖中看出for循環(huán)可以理解是橫向遍歷,backtracking(遞歸)就是縱向遍歷,這樣就把這棵樹全遍歷完了,一般來說,搜索葉子節(jié)點就是找的其中一個結(jié)果了。
回溯算法模板框架代碼如下
void backtracking(參數(shù)) {if (終止條件) {存放結(jié)果;return;}for (選擇:本層集合中元素(樹中節(jié)點孩子的數(shù)量就是集合的大小)) {處理節(jié)點;backtracking(路徑,選擇列表); // 遞歸回溯,撤銷處理結(jié)果} }下面將會列舉一些例題以及真題。后續(xù)會繼續(xù)出一期專門的練習(xí)題!
遞歸實現(xiàn)指數(shù)型枚舉
題目
代碼及注釋
方法一:遞歸枚舉法(子集)
#include<iostream> #include<cstring> #include<algorithm> #include<vector> using namespace std; int n;vector<vector<int>> result;vector<int> path;void backtracking(int n, int startIndex) {result.push_back(path); // 收集子集,要放在終止添加的上面,否則會漏掉自己if (startIndex > n) { // 終止條件可以不加return;}for (int i = startIndex; i <=n; i++) {path.push_back(i);backtracking(n, i + 1);path.pop_back();}}int main(){cin>>n;backtracking(n,1);//記住二維向量的輸出方式!!for(int i=0 ; i <result.size(); i ++ )//把所有方案輸出 {for(int j=0;j<result[i].size();j++){printf("%d ",result[i][j]);}puts("");}}📌本解法需要注意:子集必須再遞歸終止前收集 二維向量輸出的方法要會
方法二:遞歸填坑法(每個數(shù)字選與不選)
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm>using namespace std;const int N = 16;int n; int st[N]; // 狀態(tài),記錄每個位置當(dāng)前的狀態(tài):0表示還沒考慮,1表示選它,2表示不選它void dfs(int u) {if (u > n){for (int i = 1; i <= n; i ++ )if (st[i] == 1)printf("%d ", i);printf("\n");return;}st[u] = 2;dfs(u + 1); // 第一個分支:不選st[u] = 0; // 恢復(fù)現(xiàn)場st[u] = 1;dfs(u + 1); // 第二個分支:選st[u] = 0; }int main() {cin >> n;dfs(1);return 0; }題解
遞歸填與不填,關(guān)鍵在于先畫出遞歸搜索樹,思路也就顯而易見了。
遞歸實現(xiàn)排列型枚舉
題目
代碼及注釋
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm>using namespace std;const int N = 10;//題目中N的范圍是9,但我們?nèi)绻聵讼霃?開始,那么多用一個,開10 int n; //此題可以發(fā)現(xiàn)在搜索的時候還要保證每個數(shù)只搜索一次,要判斷當(dāng)前這個位置可以用的數(shù)有哪些, //因此還要存一個新的狀態(tài)used:表示每個數(shù)有沒有被用過 int state[N]; // 用st[]表示當(dāng)前的狀態(tài):0 表示還沒放數(shù),1~n表示放了哪個數(shù) bool used[N]; // true表示用過,false表示還未用過//注意變量如果定義成全局變量的話,初值會自動賦成0,如果定義成隨機變量的話,初值是一個隨機值 void dfs(int u) {if (u > n) // 邊界:枚舉完了最后一位,{for (int i = 1; i <= n; i ++ ) printf("%d ", state[i]); // 打印方案:只需要把當(dāng)前每個位置輸出出來 puts("");return;}// 依次枚舉每個分支,即當(dāng)前位置可以填哪些數(shù)for (int i = 1; i <= n; i ++ )//從小到大枚舉 if (!used[i])//如果當(dāng)前位置是沒有用過的,表示當(dāng)前位置可以填這個數(shù),成為一個分支,等價于used[i]==false{state[u] = i;//標記,更新狀態(tài) used[i] = true;//標記,更新狀態(tài),因為此題遞歸時需要兩個狀態(tài)表示 dfs(u + 1);//遞歸下一步 // 恢復(fù)現(xiàn)場,兩個狀態(tài)都要恢復(fù) state[u] = 0; //當(dāng)然,在這里state[]狀態(tài)數(shù)組其實可以不用恢復(fù),因為會直接覆蓋掉,但是為了更好的展現(xiàn)算法流程,方便初學(xué)者理解最好加上 used[i] = false;} }int main() {scanf("%d", &n);dfs(1);//從前往后枚舉,函數(shù)里面寫一個參數(shù),表示當(dāng)前枚舉到第幾位了 //因為state[]和used是全局變量了,所以不需要寫到函數(shù)參數(shù)里面 return 0; }題解
對于全排列問題我們可以這樣想,第1個位置可以放1~n任意一個數(shù),第2個位置可以放除了放在第1個位置的數(shù)以外的任何一個數(shù),以此類推。因此我們可以畫出一個遞歸搜索樹,用map[]來表示儲存當(dāng)前排列。DFS函數(shù)要記住當(dāng)前處理的是第index個位置,從1到n進行遍歷,看看這個數(shù)是否可以放在第index個位置,需要有一個判重數(shù)組hashtable[x]來記錄x是否在排列里面。
遞歸實現(xiàn)組合型枚舉
題目
代碼及注釋
#include<iostream> #include<cstdio> using namespace std; const int N=30; int way[N]; int n,m; void dfs(int u,int start) {//剪枝if(u+n-start<m) return;//正在選第u個數(shù),已經(jīng)選了u-1個數(shù),還能選n-start+1個數(shù)if(u>m) {for(int i=1;i<=m;i++)printf("%d ",way[i]);printf("\n");return;}for(int i=start;i<=n;i++){way[u]=i;dfs(u+1,i+1);//way[u]=0;}} int main() {cin>>n>>m;dfs(1,1);return 0; }題解
DFS的思路是這個樣子的,假設(shè)當(dāng)前處理的是第index個位置,這個位置可以放置start~n其中任意一個數(shù)。接著處理第index+1個位置,這個位置可以放置的最小數(shù)是前一位數(shù)的下一個數(shù)。即i+1~n
迷宮問題
題目
代碼及注釋
#include<bits/stdc++.h> using namespace std; const int N=100;int dx[4]={0,0,-1,1};//定義上下左右四個方向 int dy[4]={1,-1,0,0};int n,m,t; int sx,sy,fx,fy,l,r; int ans; bool visited[N][N]; int ditu[N][N]; bool check(int x,int y) {if(x<1||x>n||y<1||y>m) return false;//下標越界 if(ditu[x][y]) return false;//有障礙物 if(visited[x][y]) return false;//已經(jīng)訪問過該點 return true; }void dfs(int x,int y) {if(x==fx&&y==fy){ans++;return ;}for(int i=0;i<4;i++){int newx=x+dx[i];//走到下一個點 int newy=y+dy[i];if(check(newx,newy)){visited[x][y]=true;dfs(newx,newy);visited[x][y]=false;}} }int main() {cin>>n>>m>>t;cin>>sx>>sy>>fx>>fy;while(t--){cin>>l>>r;ditu[l][r]=1;}dfs(sx,sy);cout<<ans<<endl;return 0;}題解
迷宮問題是拿來練習(xí)DFS與BFS很經(jīng)典的題目。迷宮問題有很多種問法,比如迷宮從起點到終點有沒有路徑,有幾條,最短路徑是多少。
求從起點到終點的方案數(shù)顯而易見也是要用DFS,遍歷所有的情況。我們要考慮這樣一個問題,迷宮里的某點(x,y)是否要被訪問呢。當(dāng)這點是障礙物肯定不能訪問,該點不在迷宮里面也不能訪問,該點訪問過了那就不能訪問了。(題目中有每個方格最多經(jīng)過一次)。因此我們需要一個check()函數(shù)來判斷某一點是否合法。合法我們就去訪問該點。
其實這個過程就是一個剪枝的過程,根據(jù)題目條件限制,剪掉一些不可能存在解的分支。
另外我們該如何知道某點是障礙點呢,可以設(shè)置一個map數(shù)組來表示該迷宮。
當(dāng)map[x][y]==1時表示該點是障礙點map[x][y]==0表示該點是正常點
01背包問題
題目
代碼及注釋
#include<bits/stdc++.h> using namespace std;const int N=1010; int n,m; int w[N], c[N]; int a[N][N]; int ans; //由于記錄了index不會出現(xiàn)重復(fù)遍歷的問題,不需要額外的標記數(shù)組 void dfs(int index,int sumv,int sumc) {if(index==n){if(sumv<=m){ans=max(ans,sumc);}return ;} //只有兩種情況不需要for循環(huán)了dfs(index+1,sumv+w[index],sumc+c[index]);//選第i個物品 dfs(index+1,sumv,sumc);//不選第i個物品 }int main() {cin>>n>>m;for (int i = 0; i < n; i++){cin >> w[i] >> c[i];}dfs(0,0,0);cout<<ans<<endl;return 0;}題解
第i件物品無非就是選和不選兩種情況,在搜索的過程中DFS函數(shù)必須要記錄當(dāng)前處理的物品編號index,當(dāng)前背包的容量sumW,當(dāng)前的總價值sumC。
當(dāng)不選第index個物品時,那么sumW,sumC是不變的,接著處理第index+1個物品,也就是DFS(index+1, sumW, sumC)。
當(dāng)選擇第index個物品時,sumW變成sumW+w[index],sumC變成sumC+v[index],接著處理第index+1個物品,也就是DFS(index+1, sumW+w[index],sumC+v[index])。邊界條件也就是把最后一件物品也處理完了,即index=n(注意默認index從0開始)。
當(dāng)一條分支結(jié)束了該干什么呢,很簡單呀就是判斷該分支最終滿不滿足總重量不大于背包容量。即sumW<=v。滿足的話我們就更新價值maxvalue,即maxvalue=max(maxvalue,sumC)
八皇后
題目
代碼及注釋
#include<bits/stdc++.h>using namespace std; int n; const int N=100; int a[100],b[100],c[100],d[100]; int ans;bool check(int i,int j) {if(!b[j]&&!c[j-i+n]&&!d[i+j]) return true;//注意這里的對角線表達式只能為這個 return false; }void dfs(int i) {if(i>n){ans++;if(ans<=3){for(int i=1;i<=n;i++){cout<<a[i]<<" ";}cout<<endl;}return ;}for(int j=1;j<=n;j++)//枚舉一行中所有列的棋子 {if(check(i,j)){a[i]=j;b[j]=1;c[j-i+n]=1;d[i+j]=1;dfs(i+1);b[j]=0;c[j-i+n]=0;d[i+j]=0;}} }int main() {cin>>n;dfs(1);//從第一行開始枚舉 cout<<ans<<endl;return 0;}題解
這道題DFS的思路還是比較清晰的,每行有且只有一個棋子,那么DFS可以記錄下當(dāng)前處理的是第幾行的棋子。假設(shè)當(dāng)前處理的是第i行的棋子,那么要枚舉處在該行的棋子位置,判斷哪個是合法的。
什么樣的位置算是合法的呢,這個位置的列還有左對角線,右對角線位置都不能有棋子。那么又該如何表示這些位置呢?我們采用一維數(shù)組來分別表示列,左對角線,右對角線。列很好表示就是b[j],左對角線我們可以發(fā)現(xiàn)行減去列的絕對值是恒定的,即c[i-j+n],右對角線行加列是恒定的。即d[i+j]。
方格分割
題目
代碼及注釋
#include<bits/stdc++.h> using namespace std;int maze[7][7];//表示已經(jīng)訪問過的點,1表示已經(jīng)訪問的 int dx[4]={0,1,0,-1}; int dy[4]={-1,0,1,0};int ans;void dfs(int x,int y) {if(x==0||y==0||x==6||y==6){ans++;return ;}for(int i=0;i<4;i++){int a=x+dx[i],b=y+dy[i];if(maze[a][b]!=1){maze[a][b]=1;maze[6-a][6-b]=1;dfs(a,b);maze[a][b]=0;//回溯maze[6-a][6-b]=0;}} }int main() {maze[3][3]=1;//中心點 標記已經(jīng)訪問dfs(3,3);cout<<ans/4<<endl;//旋轉(zhuǎn)對稱只算一種方式return 0; }題解
本題需要發(fā)現(xiàn)一個規(guī)律:分割成的兩部分一定是中心對稱的,也就是從中心點開始上下左右搜索的結(jié)果就是答案,但是需要記錄已經(jīng)搜索過的點,以及旋轉(zhuǎn)對稱只算一種方式。
畫個圖會清晰很多,寫出點的坐標,并算出中心對稱的坐標。
組隊
題目
代碼及注釋
#include<bits/stdc++.h> using namespace std;int maze[20][20]; bool visited[20]; int ans; void dfs(int index,int sum) {if(index==5)//枚舉當(dāng)前是第幾位,固定列{ans=max(ans,sum);return ;}for(int i=0;i<20;i++)//枚舉每一行{if(maze[i][index]!=0&&!visited[i]){visited[i]=true;dfs(index+1,sum+maze[i][index]);visited[i]=false;a}} }int main() {for(int i=0;i<20;i++)for(int j=0;j<5;j++)cin>>maze[i][j];dfs(0,0); }題解
本題是經(jīng)典的dfs模型,可以看成是排列問題,對于一號位來說有20種選擇,對于二號位來說有19種選擇,也就是說可以維護當(dāng)前正在選擇第index位,每一次dfs更新一個最大值。
縱向遞歸:index代表第幾位
橫向枚舉:一共有20個選手,for循環(huán)枚舉。
參數(shù):index sum
回溯數(shù)組:visited[N] 表示已經(jīng)訪問過的選手
總結(jié)
本文主要講解了回溯搜素算法的原理、模板以及具體的代碼與例題。需要大家熟練掌握算法,多做練習(xí)題,才能在比賽中靈活運用該算法。預(yù)祝各位考出好成績!!
總結(jié)
以上是生活随笔為你收集整理的【蓝桥杯C/C++】专题五:DFS深度优先搜索的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql分组查询学生平均年龄_8.21
- 下一篇: 动图图解!既然IP层会分片,为什么TCP