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