LQ训练营(C++)学习笔记_深度优先搜索
深度優先搜索
- 三、深度優先搜索
- 1、普通深度優先搜索
- 1.1 迷宮問題描述
- 1.2 代碼實現
- 2、抽象深度優先搜索問題
- 2.1 和為K問題
- 2.1.1 問題描述
- 2.1.2 解題思路
- 2.1.3 代碼實現
- 2.2 八皇后問題
- 2.2.1 問題描述
- 2.2.2 解題思路
- 2.2.3 代碼實現
- 3、剪枝深度優先搜索問題
- 3.1 基本概念
- 3.1.1最優性剪枝
- 3.1.2 重復性剪枝
- 3.1.3問題描述及代碼實現
- 3.1.4奇偶性剪枝
- 4、迷宮問題
- 4.1 問題描述
- 4.2 問題分析
- 4.3 代碼實現
- 5、炸彈問題
- 5.1 問題描述
- 5.2 解題思路
- 5.3 代碼實現
三、深度優先搜索
1、普通深度優先搜索
1.1 迷宮問題描述
迷宮問題的解法要用到DFS,對上下左右四個方向進行嘗試,如果沿某個方向不能走到終點,就原路返回,繼續嘗試其它方向,知道走出迷宮。
首先找到起點S,走到每個點時,按照左、下、右、上的順序嘗試。每次走到下一個點以后,就把這個點當作起點S,繼續按照順序嘗試。如果每個點上下左右四個方向都嘗試了,便回到走到這個點之前的點,這一步稱為回溯。繼續嘗試其它方向,直到所有點都嘗試過上下左右四個方向。
程序
首先處理好邊界條件,什么時候結束搜索?如果搜索到了終點,自然要結束搜索,邊界條件是maze[x][y] == ‘T’
1.2 代碼實現
#include<Iostream> #include<String> Using namespace std; int n,m; string maze[110]; bool vis[110][110];//定義vis bool in(int x,int y){return 0 <= x && 0 <= y && y < m; } bool dfs(int x,int y){If(maze[x][y] == ‘T’){//邊界條件,表示到達終點結束搜索Retrun true;}vis[x][y]=1;//用VIS數組標記當前走過的點maze[x][y]=‘m’;//把走過的點用字符‘m’標記int tx=x-1,ty=y;//向上走if(in(tx,ty)&&maze[tx][ty]!=‘*’&&!vis[tx][ty]){//如果不是‘*’且沒有訪問,就繼續在該位置進行搜索if(dfs(tx,ty)){//如果該位置能搜到解,直接返回trueretrun true;}}tx=x,ty=y-1;if(int(tx,ty)&&maze[tx][ty]!=‘*’&&!vis[tx][ty]){If(dfs(tx,ty)){Retrun true;}}tx=x+1,ty=y;if(int(tx,ty)&&maze[tx][ty]!=‘*’&&!vis[tx][ty]){If(dfs(tx,ty)){Retrun true;}}tx=x,ty=y+1;if(int(tx,ty)&&maze[tx][ty]!=‘*’&&!vis[tx][ty]){If(dfs(tx,ty)){Retrun true;}}//如果四個方向都搜索完也沒找到路徑,函數結束運行,返回到上一層遞歸,并且把剛才訪問這個位置時做的標記全部撤銷。vis[x][y]=0;maze[x][y]=‘.’;return false; } int main(){cin>>n>>m;//輸入迷宮地圖for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(maze[i][j] == ‘s’){x=i,y=j;}}}if(dfs(x,y)){for(int i=0;i<n;i++){cout<<maze[i]<<endl;}}else{cout<<“NO!<<endl”} }2、抽象深度優先搜索問題
2.1 和為K問題
2.1.1 問題描述
給定N個整數,要求選出K個數,使得選出的K個數和為SUM
2.1.2 解題思路
對于每一個數,枚舉選或者不選兩種情況,可以用DFS思想完成這樣的枚舉過程。在搜索過程中,用S來記錄當前選擇的數值總和,K記錄選擇的數的個數,deep表示當前正在枚舉第幾個數是否選擇。
在第一層DFS時,可以枚舉是否選第一個數,如果選第一個數則讓S加上第一個數且k加一,DFS進入下一層;否則DFS直接進入下一層。
這里還需要借助全局變量、參數或修改數組中的元素的值等方式來標識出當前的層數。
在第二層,對第二個數做同樣的處理,DFS的過程中記錄已經選取的數的個數,如果已經選取了K個數,判斷S的值是否等于SUM。
對于每一層,都有選和不選兩個選擇。不同的選擇,都會使得搜索進入完全不同的分支繼續搜索。
可以根據搜索狀態構建一張抽象的圖,圖上的一個頂點就是一個狀態,而圖上的邊就是狀態之間的轉移關系。
2.1.3 代碼實現
#include<iostream> using namespace std; int n,k,sum,ans; int a[40]; //i表示當前正在選擇第幾個數,cnt表示選取了幾個數,s表示選取的數的和 void dfs(int i,int cnt,int s){if(i==n){//邊界條件,對所有的數都進行了選擇if(cnt==k && s==sum){//判斷選出來的數的個數是否等于k,和是否為sumans++;}return 0;}dfs(i+1,cnt,s);//不選取第i個數,cnt和s都不會變化dfs(i+1,cnt+1,s+a[i]);//選取第i個數,cnt+1,s+a[i] } int main(){cin>>n>>k>>sum;for(int i=0;i<n;i++){cin>>a[i];}ans=0;//初始化ans為0dfs(0,0,0);//將所有參數初始化為0cout<<ans<<endl;return 0; }2.2 八皇后問題
2.2.1 問題描述
在8??8的國際象棋上擺上8個皇后,使其不能相互攻擊,即任意兩個皇后都不能處于同一行、同一列或同一斜線上,問有多少種擺法。
2.2.2 解題思路
解決這個問題,可以對每一行的八個位置一個一個進行嘗試,如果可以放下,就填下一行,直到最后一行,這就是一個可行的方案。
2.2.3 代碼實現
#include<iostream> using namespace std; int ans = 0; bool col[10],x1[20],x2[20];//標記列和兩條對角線 bool check(int r,int i){//對應的列和兩條對角線沒有被占用就可以放置return !col[i] && !x1[r+i] && !x2[r-i+8]; } void dfs(int r){if(r==8){//遞歸出口,如果填完前八行,代表是一種可行方案ans++;return;}for(int i=0;i<8;i++){//對第r行的每一列進行一一嘗試if(check(r,i)){//判斷是否能放置col[i]=x1[r+i]=x2[r-i+8]=true;//r-i可能為負值,加上偏移量dfs(r+1);col[i]=x1[r+i]=x2[r-i+8]=false;}} } int main(){dfs(0);cout<<ans<<endl;return 0; }3、剪枝深度優先搜索問題
3.1 基本概念
剪枝就是通過一些判斷,砍掉搜索樹上不必要的子樹。如某個結點對應的子樹的狀態都不是我們要的結果,就沒必要對這個分支進行搜索,砍掉這個子樹就是剪枝。
3.1.1最優性剪枝
對于求最優解的一類問題,通常可以用最優性剪枝,比如在求解迷宮問題最短路徑時,如果發現當前的步數已經超過了當前最優解,那從當前狀態開始的搜索都是多余的,因為這樣搜索下去永遠都搜不到更優的解。通過這樣的剪枝,可以省去大量冗余的計算。此外,在搜索是否有可行解的過程中,一旦找到了一組可行解,后面的搜索都不必再進行了,這算是最優性剪枝的一個特例。
3.1.2 重復性剪枝
對于某一些特定的搜索方式,一個方案可能會被搜索很多次,這樣是沒有必要的。比如在這個問題中:給定n個整數,要求選出K個數,使得選出的K個數的和為sum。如果搜索方法是每次從剩下的數中選出一個數,一直搜到第k層,那么1,2,3這個選取方法能被搜到6次,這是沒必要的,因為我們只關注選出來的數的和,而不會關注選出來的數的順序,所以這里可以用重復性剪枝。若規定選出的數的位置是遞增的,在搜索的時候,用一個參數記錄上一次選取的數的位置,那么此次選擇,從這個數之后開始選取,這樣最后選出來的方案也就不會重復。
3.1.3問題描述及代碼實現
從0、1、2、···、29這30個數中選擇8個數,使得和值為200.
#include<iostream> using namespace std; int n,k,sum,ans; int a[40]; bool xuan[40]; void dfs(int s,int cnt,int p){if(s>sum || cnt>k){return;}if(s==sum && cnt==k){ans++;}for(int i=pos;i<n;i++){//直接從pos的位置選擇數if(!xuan[i]){xuan[i]=1;dfs(s+a[i],cnt+1);xuan[i]=0;}} } int main(){n=30;K=8;sum=200;for(int i=0;i<30;i++){a[i]=i+1;}ans=0;dfs(0,0,0);cout<<ans<<endl;return 0; }3.1.4奇偶性剪枝
4、迷宮問題
4.1 問題描述
有一個n*m大小的迷宮。其中字符’表示’起點,字符‘D’表示出口,字符’X’表示墻壁。字符’.‘表示空地,從’S’出發走到’D’,每次只能向上下左右相鄰的位置移動,并且不能 走出地圖,也不能走進墻壁。
每次移動消耗1時間,走過的路都會塌陷,因此不能走回頭路或者原地不動。現在已知出口的大門會在T時間打開,從起點能否逃離迷宮。
數據范圍n,m<=10,T<=50。
4.2 問題分析
分析:我們需要用dfs搜索每一條路線,并且只需搜到T時間就可以了(這是一個可行性剪枝)
奇偶性剪枝:
如上圖所示,將n*m的網格染成黑白兩色。我們記錄每一個格子的行列數之和為x,如果x為偶數,那么格子就是白色,反之為奇數時就為黑色。容易發現相鄰的兩個格子顏色肯定不一樣,也就是說每走一步顏色都會不一樣。更普遍的結論:走偶數步不會改變顏色,走奇數步會改變顏色。
那么如果起點和終點顏色一樣,而T是奇數的話,就不可能逃離迷宮。同理,如果起點和終點顏色不一樣,而T是偶數的話也不能逃離。遇到這兩種情況時,就不用進行DFS了,直接輸出“NO”。
這里用一個通用公式:
(x1+y1+x2+y2+T)%2!=0 該公式為真,就不可能逃出迷宮
4.3 代碼實現
#include<iostream> using namespace std; int n,m,T; char mat[10][10]; bool vis[10][10]; int dx[4]={0,0,-1,1}; int dy[4]={1,-1,0,0}; bool ok; void dfs(int x,int y,int t){if(ok)return;if(t==T){if(mat[x][y]=='D'){ok=true;} return;}vis[x][y]=true;for(int i=0;i<4;i++){int tx=x+dx[i];int ty=y+dy[i];if(tx<0||ty>=n||ty<0||ty>=m||mat[tx][ty]=='X'||vis[tx][ty]){continue;}dfs(tx,ty,t+1);}vis[x][y]=false; } int main(){cin>>n>>m>>T;for(int i=0;i<n;i++){cin>>mat[i];}int sx,sy,ex,ey;for(int i=0;i<n;i++){for(int j=0;j<m;j++){if(mat[i][j]=='S'){sx=i;sy=j;}if(mat[i][j]=='D'){ex=i;ey=j;}}}if((sx+sy+ex+ey+T)%2!=0){//利用奇偶性剪枝判斷是否能夠到達迷宮 cout<<"NO"<<endl;}else{ok=false;dfs(sx,sy,0);//搜索 if(ok)cout<<"YES"<<endl;elsecout<<"NO"<<endl;} }5、炸彈問題
5.1 問題描述
在一個n*m的方格地圖上,某些方格上放置著炸彈。手動引爆一個炸彈以后,炸彈會把其所在的行和列的所有炸彈引爆,被引爆的炸彈又能引爆其他炸彈,這樣連鎖下去。
現在為了引爆地圖上的所有炸彈,需要手動引爆其中一些炸彈,為了把危險程度降到最低,請算出最少手動引爆多少個炸彈可以把地圖上的所有炸彈引爆。
輸入格式
第一行輸入兩個正數n,m,用空格隔開。
接下來n行,每行輸入一個長度為m的字符串,表示地圖信息。0表示沒有炸彈,1表示炸彈。
數據約定:
對于60%的數據:1<=n,m<=100;
對于100%的數據:1<=n;m<=1000;
輸出格式
輸出一個證書,表示最少需要手動引爆的炸彈數。
樣例輸入
5 5
00010
00010
01001
10001
01000
樣例輸出
2
5.2 解題思路
本題首先不要糾結炸彈的個數,要把可以連鎖引爆的炸彈當做一個集合,每個集合分別引爆一個炸彈,便能將所有炸彈引爆。因此本題將手動引爆最少的炸彈個數,轉化為求炸彈的集合數。
遞歸參數:地圖坐標x,y
遞歸邊界:當前行列是否被訪問過
遞歸體:遍歷所在行,列,如果有炸彈,繼續引爆
剪枝:visx[], visy[], mp[x][y] = ‘0’,以保證行列只訪問一次
5.3 代碼實現
#include<stdio.h> #include<iostream> using namespace std; int n, m; char mat[1005][1005]; bool row[1005], col[1005]; //x,y坐標 void boom(int x, int y) {mat[x][y] = 0;//引爆炸彈,修改防止重復訪問 //遞歸邊界 //遞歸體//訪問同一行if(!row[x]){row[x] = true;//標記行被訪問 for(int i = 0; i < m; ++i){if(mat[x][i] == '1')//同一行存在炸彈 {boom(x, i);}}}//訪問同一列if(!col){col[y] = true;for(int i = 0; i < n; ++i){if(mat[i][y] == '1')//同一列存在炸彈 {boom(i, y);}} } } int main() {scanf("%d%d", &n, &m);for(int i = 0; i < n; i++){scanf("%s", mat[i]);}int cnt=0;for(int i = 0; i < n; ++i){for(int j = 0; j < m; ++j){if(mp[i][j] == '1'){++cnt;boom(i, j);}}}printf("%d\n", cnt);return 0; }總結
以上是生活随笔為你收集整理的LQ训练营(C++)学习笔记_深度优先搜索的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一加中国区总裁:8GB内存该被淘汰了 否
- 下一篇: 一加Ace 2原神定制礼盒4月登场!香菱