DFS 深搜專題 入門典例 -- 凌宸1642
深度優先搜尋 是一種 列舉所有完整路徑以遍歷所有情況的搜尋方法 ,使用 遞迴 可以很好的實現 深度優先搜尋。
1 最大價值
題目描述
有 n 件物品,每件物品的重量為 w[i] , 價值為 c[i] 。現在需要選出若干件物品放入一個容器為 V 的揹包中,使得在選入揹包的物品重量和不超過容量 V 的前提下 ,讓揹包中的物品的價值之和最大,求最大價值。(1 ≤ n≤ 20 )
輸入描述:
第一行輸入物品總數 n 和 揹包容量 v 。
第二行輸入 n 個整數, 分別表示 n 件物品各自的重量。
第三行輸入 n 個整數, 分別表示 n 件物品各自的價值。
輸出描述:
對於每個測試用例,在一行中輸出最大價值。
樣例輸入:
5 8 3 5 1 2 2 4 5 2 1 3
樣例輸出:
10
#include<bits/stdc++.h>
using namespace std ;
#define MAX 30
int n , v , maxValue = 0 ; // 物品件數 n,揹包容量 v,最大價值 maxValue
int w[MAX] , c[MAX] ; // w[i]為每件物品的重量, c[i]每件物品的價值
// dfs ,index 表示當前處理物品的編號 , sumW sumC 分別為當前 總重量 和 總價值
void dfs(int index , int sumW , int sumC){
if(index == n) { // 已經完成對 n 件物品的處理
if(sumW <= v && sumC > maxValue ) maxValue = sumC ; // 如果此時最大價值符合題意,則更新
return ;
}
// 岔道口
dfs(index + 1 , sumW , sumC) ; // 不選擇第 index 件物品
dfs(index + 1 , sumW + w[index] , sumC + c[index]) ; // 選擇了第 index 件物品
}
int main(){
cin >> n >> v ; // 輸入 物品數 n 和 揹包容量 v
for(int i = 0 ; i < n ; i ++) cin >> w[i] ; // 輸入 n 件物品的 重量
for(int i = 0 ; i < n ; i ++) cin >> c[i] ; // 輸入 n 件物品的 價值
dfs(0 , 0 , 0) ; // 呼叫 dfs 進行深搜
cout << maxValue << endl ; // 深搜結束後, maxValue 中存有最大價值,直接輸出
return 0;
}
// 對此 dfs 進行剪枝後 ,剪枝是在保證演算法正確的情況下,通過題目條件限制來減少 DFS 計算量的方法
void dfs(int index , int sumW , int sumC){
if(index == n) return ; //完成對 n 件物品的選擇
dfs(index + 1 , sumW , sumC) ; // 未選擇第 index 件物品
if(sumW + w[index] <= v ) { // 只有當選擇第 index 件物品之後,容量不會超過 v ,才進行選擇
if(sumC + c[index] > maxValue)
maxValue = sumC + c[index] ; // 更新最大價值 maxValue
dfs(index + 1 , sumW + w[index] , sumC + c[index]) ; // 選擇第 index 件物品
}
}
2 最優方案
題目描述:
給定一個序列,列舉這個序列的所有子序列(可以不連續)。例如對序列{1,2,3}來說,他的所有子序列為{1},{2},{3},{1,2},{1,3},{1,2,3}。列舉所有子序列的目的很明顯——可以從中選擇一個“最優”子序列,使它的某個特徵是所有子序列中最優的;如果有需要,還可以將這個最優子序列儲存下來。顯然,這個問題也可以等價於 列舉從 N 個整數中選擇 K 個數的所有方案。
給定
N
個整數(可能有負數),從中選擇K
個數,使得這K
個數的和恰好等於給定的一個整數X
; 如果有多種方案,則選擇他們當中平方和最大的一個。資料保證這樣的方案唯一。例如,從 4 個整數{2,3,3,4}中選擇兩個數,使他們的和為 6 。顯然有兩種方案{2,4} 和{3,3} ,其中平方和最大的方案為{2,4}。輸入描述:
第一行輸入
3
個正整數 ,依次輸入序列的長度N
,從中選擇數的數量K
,和恰好等於的和X
。 第二行輸入
N
個整數(可能有負數),表示序列中的數。輸出描述:
輸出分兩行,第一行輸出
K
個數 , 表示最優序列中的元素。元素之間用空格分隔,且行末沒有於的空格 第二行輸出 最優方案的平方和。
樣例輸入:
4 2 6 2 3 3 4
樣例輸出:
2 4 20
#include<bits/stdc++.h>
using namespace std ;
#define MAX 100010
int n , k , x , maxSumSqu = - 1 , a[MAX] ;//分別表示,序列長度,選擇數量,恰好的和,最大平方和,序列
vector<int> temp , ans ; // temp 存放臨時方案,ans 存放平方和最大的方案
// dfs引數列表分別表示 處理第 index 個數,現在已選 nowK 個數,當前已選數字的和 sum 及其平方和 sumSqu
void dfs(int index , int nowK , int sum , int sumSqu){
// 找到第 k 個數的和為 x
if(nowK == k && sum == x){
if(sumSqu > maxSumSqu){ // 如果比當前的更優
maxSumSqu = sumSqu ; // 更新最大平方和
ans = temp ; // 更新最優方案
}
return ;
}
// 已經處理完 n 個數,或者超過 k 個數,或者和超過 x,返回
if(index == n || nowK > k || sum > x) return ;
// 選第 index 個數
temp.push_back(a[index]) ;// 將第 index 個數 入棧,然候搜尋下一個數,注意引數各個值的變化
dfs(index + 1 , nowK + 1 , sum + a[index] , sumSqu + a[index] * a[index]) ;
// 不選第 index 個數
temp.pop_back() ; // 先將第 index 個數 出棧( 還原現場 )
dfs(index + 1 , nowK , sum , sumSqu) ; // 繼續搜尋下一個數
}
int main(){
//cin >> n >> k >> x ;
scanf("%d%d%d" , &n , &k , &x) ;// 依次輸入序列的長度 n , 選擇數的數量 k ,恰好的和 x
for(int i = 0 ; i < n ; i ++) scanf("%d" , &a[i]) ; // 輸入序列
dfs(0 , 0 , 0 , 0) ; // 呼叫 dfs
for(int i = 0 ; i < ans.size() ; i ++){ // dfs 後 , ans中儲存的是最優子序列,遍歷輸出
//cout<< ans[i]<<" " ;
printf("%d%c" , ans[i] , (i == ans.size() - 1)?'\n':' ') ;
}
printf("%d\n" , maxSumSqu) ; // 輸出最優子序列的 平方和
// cout<<endl<<maxSumSqu<<endl ;
return 0 ;
}
3 全排列
題目描述:
排列與組合是常用的數學方法。先給一個正整數 ( 1 < = n < = 10 ) 。例如n=3,所有組合,並且按字典序輸出:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
輸入描述:
在一行中輸入正整數 n ( 1 < = n < = 10 ) 。
輸出描述:
按字典序輸出 n 的全排列。每行輸出一個排列,數字間用空格分隔,且行末沒有多與的空格
樣例輸入:
3
樣例輸出:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
#include<bits/stdc++.h>
using namespace std ;
#define MAX 20
bool flag[20] = { false } ; // 是否被訪問,初值為 false
int num[20] ; // 儲存排列
int n ;
// dfs 引數 index 表述處理排列的第 index 個數
void dfs(int index){
if(index == n + 1){ // 如果 index 等於 n + 1 ,表明已經完成了一次排列 ,則當前排列
// printf("%d" , num[1]) ; 輸出第一個元素
// for(int i = 2 ; i <= n ; i ++)printf(" %d" ,num[i]); 輸出空格和後面 n - 1 個元素
// printf("\n") ; 當前排列全部輸出,換行
// 上述四條語句,等價於如下語句
for(int i = 1 ; i <= n ; i ++) printf("%d%c" , num[i] , (i == n)?'\n':' ');
return ;
}
for(int i = 1 ; i <= n ; i ++){ // n 的全排列,就是將 1 - n 都訪問一遍 , 並存在對應位置
if(flag[i] == false){ // 如果 i 還未訪問
num[index] = i ; // 將 i 的值存入 num 陣列第 index 位置
flag[i] = true ; // 將 i 的訪問標誌置為 true
dfs(index + 1) ; // 進行下一個位置的 dfs 搜尋
flag[i] = false ; // 還原現場
}
}
}
int main(){
scanf("%d" , &n) ; // 輸入正整數 n
dfs(1) ; // 呼叫 dfs
return 0;
}
4 組合的輸出
題目描述:
排列與組合是常用的數學方法,其中組合就是從
n
個元素中抽出r
個元素(不分順序且r ≤ n
),我們可以簡單地將n
個元素理解為自然數1,2,…,n
,從中任取r
個數。 例如n = 5 ,r = 3
,所有組合為:1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5
輸入描述:
在一行中輸入兩個自然數
n
,r
( 1 < n < 21,1 ≤ r ≤ n )
。輸出描述:
輸出所有的組合,每一個組合佔一行且其中的元素按由小到大的順序排列,所有的組合也按字典順序。每行中的元素之間用空格分隔,並且行末沒有多餘的空格。
樣例輸入:
5 3
樣例輸出:
1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5
#include<bits/stdc++.h>
using namespace std ;
int n , r ;
vector<int> temp ; // temp 儲存當前已選的數
// index 為當前處理的數的下標, nowR是已經有多少個數
void dfs(int index , int nowR){
if(nowR == r){ // 如果已選 k 個數,那麼輸出當前選擇的 k 個數
// printf("%d" ,temp[0]);
// for(int i = 1 ; i < r ; i ++) printf(" %d" , temp[i]);
// printf("\n");
for(int i = 0 ; i < r ; i ++) printf("%d%c" , temp[i] , (i == r - 1)?'\n':' ');
return ;
}
// 如果已經處理完 n 個數 或者選擇數的數量大於 k
if(index == n + 1 || nowR > r) return ;
// 選第 index 個數
temp.push_back(index) ; // 將 index 入棧
dfs(index + 1 , nowR + 1) ; // 繼續 dfs 深搜 注意引數的變化
// 不選第 index 個數
temp.pop_back() ; // 先將 index 出棧
dfs(index + 1 , nowR) ; // 繼續 dfs 深搜 注意引數的變化
}
int main(){
scanf("%d%d" , &n, &r) ;
dfs(1 , 0) ; // 呼叫 dfs 下標解釋為 從 1 開始搜尋,當前選擇數的數量為 0
return 0 ;
}
5 組合+判斷素數
題目描述:
已知
n
個整數b1,b2,…,bn
。以及一個整數k(k<n)
。 從n
個整數中任選k
個整數相加,可分別得到一系列的和。例如當 n = 4,k=3,4 個整數分別為 3,7,12,19 時,可得全部的組合與它們的和為:3+7+12=22 3+7+19=29 7+12+19=38 3+12+19=34。
現在,要求你計算出和為素數共有多少種。例如上例,只有一種的和為素數:3+7+19=29。
輸入描述:
第一行兩個整數:`n` , `k` `(1 ≤ n ≤ 20,k < n)`
第二行
n
個整數:x1,x2,… , xn (1 ≤ xi ≤ 5000000)
輸出描述:
一個整數(滿足條件的方案數)。
樣例輸入:
4 3 3 7 12 19
樣例輸出:
1
#include<bits/stdc++.h>
using namespace std ;
int num[22] ; // 儲存選擇的數
int n , k , ans = 0 ; // ans 用來儲存滿足要求的方案數的數量
// 判斷一個數是否為素數的演算法 就不必再贅述了
bool isPrime(int x){
if(n <= 1 || n == 2) return false ;
for(int i = 2 ; i <= sqrt(x) ; i ++){
if(x % i == 0)
return false ;
}
return true ;
}
// dfs 引數說明:分別表示 處理第 index 個數,當前已選 nowK 個數 ,當前已選數字的和
void dfs(int index , int nowK ,int sum){
if(nowK == k){ // 如果已選 k 個數
if(isPrime(sum)) // 判斷當前 k 個數的和 是否為素數,
ans ++ ; // 如果是,則方案數 自增 1
return ;
}
// 已經處理了 n 個數 , 或者 所選數的數量大於 k ,則返回
if(index == n + 1 || nowK > k) return ;
// 選第 index 個數 ;注意 對應引數變化
dfs(index + 1 , nowK + 1 , sum + num[index]) ;
// 不選第 index 個數 ;
dfs(index + 1 , nowK , sum) ;
}
int main(){
scanf("%d%d" , &n ,&k) ; // 輸入 n 和 k
for(int i = 1 ; i <= n ; i ++) scanf("%d" ,&num[i]) ; // 輸入 n 個整數
dfs(1 , 0 , 0) ; // dfs 呼叫, 處理第 1 個數 當前所選數的數量為 0 當前所選數字的和為 0
printf("%d\n" , ans) ; // 輸出滿足要去的結果數
return 0;
}
6 N 皇后問題
題目描述:
會下國際象棋的人都很清楚:皇后可以在橫、豎、斜線上不限步數地吃掉其他棋子。如何將 8 個皇后放在棋盤上(有 8 * 8 個方格),使它們誰也不能被吃掉!這就是著名的八皇后問題。
輸入描述:
一個整數
n( 1 ≤ n ≤ 10 )
。輸出描述:
每行輸出對應一種方案,按字典序輸出所有方案。每種方案順序輸出皇后所在的列號,相鄰兩數之間用空格隔開,行末沒有多與的空格。如果一組可行方案都沒有,輸出
no solute!
。樣例輸入:
4
樣例輸出:
2 4 1 3 3 1 4 2
#include<bits/stdc++.h>
using namespace std ;
int n , temp[12] = { 0 } ,cnt = 0 ; // temp 用來儲存當前擺放皇后的列標 , cnt 用來記錄解的個數
bool p[12] = { false } ; // 列表的訪問陣列 , 初值為 false 代表都未被訪問
// dfs 引數 index 表示當前處理 第 index 列
void dfs(int index){
if(index == n + 1){ // 已經處理完 n 列 , 輸出當前 temp 中儲存的擺放順序
cnt ++ ;
//printf("%d" ,temp[1]);
//for(int i = 2 ; i <= n ; i ++) printf(" %d" , temp[i]);
//printf("\n");
for(int i = 1 ; i <= n ; i ++) printf("%d%c" , temp[i] , (i == n)?'\n':' ') ;
return ;
}
// 對於 擺放皇后的位置,遍歷第 i 到 n 列
for(int i = 1 ; i <= n ; i ++){
if(p[i] == false){ // 如果當前列沒有皇后
bool flag = true ; // 定義標誌位,看是否對角線上有皇后
for(int pre = 1 ; pre < index ; pre ++){ // 遍歷已經擺放好的皇后
// 回溯
if(abs(index - pre) == abs(i - temp[pre])){ // 如果在某一對角線有衝突
flag = false ; // 標誌位置為 false ,並跳出迴圈
break;
}
}
if(flag){ // 如果對角線沒有衝突
temp[index] = i ; // 將當前列標複製給 temp 的第 index ,即在當前列放一個皇后
p[i] = true ; // 並置當前位置的訪問位 為true 表示已經訪問過
dfs(index + 1) ; // 繼續 dfs ,擺放下一個位置的皇后
p[i] = false ; // 還原現場
}
}
}
}
int main(){
scanf("%d" , &n) ; // 輸入 n 皇后 的規模 n
dfs(1) ; // 從 第 1 列開始 dfs
if(cnt == 0) printf("no solute!\n") ; // 如果解的數目為 0 即無解,按題目要求輸出 no solute!
return 0;
}
7 出棧序列統計
題目描述:
棧是常用的一種資料結構,有
n
個元素在棧頂端一側等待進棧,棧頂端另一側是出棧序列。你已經知道棧的操作有兩種:push
和pop
,前者是將一個元素進棧,後者是將棧頂元素彈出。現在要使用這兩種操作,由一個操作序列可以得到一系列的輸出序列。請你程式設計求出對於給定的n,計算並輸出由運算元序列1,2,… ,n
,經過一系列操作可能得到的輸出序列總數。輸入描述:
一個整數
n (1 ≤ n ≤ 15)
.輸出描述:
一個整數,即可能輸出序列的總數目。
樣例輸入:
3
樣例輸出:
5
#include<bits/stdc++.h>
using namespace std ;
int n , cnt = 0 ;
// dfs 引數解析,in 代表棧中有多少個元素 , out 代表還有多少元素需要入棧
void dfs(int in , int out){
if(out == 0){ // 如果以及沒有需要進棧的元素,那麼出棧順序總數目 + 1
cnt ++ ;
return ;
}
if(in == 0) dfs(in + 1,out - 1) ; // 如果棧中沒有元素,則只能進行入棧操作
else if (out != 0){ // 如果棧中有元素,也存在待入棧的元素
dfs(in + 1 , out - 1 ) ; // 進行入棧操作
dfs(in - 1 , out) ; // 進行出棧操作
}
}
int main(){
scanf("%d" , &n) ;
dfs(0 , n) ; // 呼叫 dfs 初始棧內元素個數為 0 ,需要入棧元素為 n
printf("%d\n" , cnt) ; // 輸出解的個數
return 0 ;
}
8 走迷宮
題目描述:
有一個n * m格的迷宮(表示有 n 行、m 列),其中有可走的也有不可走的,如果用 1 表示可以走,0 表示不可以走,檔案讀入這 n * m個資料和起始點、結束點(起始點和結束點都是用兩個資料來描述的,分別表示這個點的行號和列號)。現在要你程式設計找出所有可行的道路,要求所走的路中沒有重複的點,走時只能是上下左右四個方向。如果一條路都不可行,則輸出相應資訊(用-l表示無路)。 請統一用 左上右下的順序擴充,也就是 (0,-1),(-1,0),(0,1),(1,0)
輸入描述:
第一行是兩個數
n,m ( 1 < n , m < 15 )
,接下來是m行n列由1和0組成的資料,最後兩行是起始點和結束點。輸出描述:
所有可行的路徑,描述一個點時用(x,y)的形式,除開始點外,其他的都要用
->
表示方向。 如果沒有一條可行的路則輸出-1
。樣例輸入:
5 6 1 0 0 1 0 1 1 1 1 1 1 1 0 0 1 1 1 0 1 1 1 1 1 0 1 1 1 0 1 1 1 1 5 6
樣例輸出:
(1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(3,4)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(2,4)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,4)->(4,4)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(3,4)->(2,4)->(2,5)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(3,4)->(3,5)->(4,5)->(5,5)->(5,6) (1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(4,3)->(4,4)->(4,5)->(5,5)->(5,6)
提示資訊:
用一個a陣列來存放迷宮可走的情況,另外用一個陣列b來存放哪些點走過了。每個點用兩個數字來描述,一個表示行號,另一個表示列號。對於某一個點(x,y),四個可能走的方向的點描述如下表:
2
1 x,y 3
4
對應的位置為:(x, y-1),(x-1, y),(x, y+1),(x+1, y)。所以每個點都要試探四個方向,如果沒有走過(陣列b相應的點的值為0)且可以走(陣列a相應點的值為1)同時不越界,就走過去,再看有沒有到達終點,到了終點則輸出所走的路,否則繼續走下去。
這個查詢過程用search來描述如下:
procedure search(x, y, b, p);{x,y表示某一個點,b是已經過的點的情況,p是已走過的路}
begin
for i:=1 to 4 do{分別對4個點進行試探}
begin
先記住當前點的位置,已走過的情況和走過的路;
如果第i個點(xi,yi)可以走,則走過去;
如果已達終點,則輸出所走的路徑並置有路可走的資訊,
否則繼續從新的點往下查詢search(xi,yi,b1,p1);
end;
end;
有些情況很明顯是無解的,如從起點到終點的矩形中有一行或一列都是為0的,明顯道路不通,對於這種情況要很快地“剪掉”多餘分枝得出結論,這就是搜尋裡所說的“剪枝”。從起點開始往下的一層層的結點,看起來如同樹枝一樣,對於其中的“枯枝”——明顯無用的節點可以先行“剪掉”,從而提高搜尋速度。
#include<bits/stdc++.h>
using namespace std ;
#define MAX 20
// 下列變數代表迷宮的維數, 解的數量,起始座標和出口座標
int n , m , num = 0 , startX , startY , endX , endY ;
int labyrinth[MAX][MAX] = { 0 } ; // 儲存迷宮的二維陣列
int dx[4] = {0 , -1 , 0 , 1 } ;
int dy[4] = {-1 , 0 , 1 , 0 } ; // 表示上下左右的擴充套件
// 代表所有是解的路徑上的點的數量(除終點)
vector<pair<int , int > > road ; //儲存路線的向量,每個元素的有x、y座標
// x ,y 代表當前處理的座標點,
void dfs(int x , int y ){
road.push_back(make_pair(x , y )) ; // 當前座標的賦值
if(x == endX && y == endY){ // 如果已經走到出口位置,輸出這條路徑
for(int i = 0 ; i < road.size() ; i ++){
printf("(%d,%d)" , road[i].first , road[i].second) ;
if(i != road.size() - 1) printf("->") ;
}
printf("\n") ;
num ++ ;// 解的數量加 1
return ;
}
// 遍歷當前座標的左,上,右,下位置的點
for(int j = 0 ; j < 4 ; j ++){
// 如果進行 左,上,右,下 擴充套件後,得到的點依舊未被訪問且 在迷宮的範圍內
if(labyrinth[x + dx[j]][y + dy[j]] == 1 && 1 <= x + dx[j] <= m && 1 <= y + dy[j] <= n){
labyrinth[x][y] = 0 ; //當前點以遍歷,不可訪問
dfs(x + dx[j] , y + dy[j]) ; // 選擇某個位置(左,上,右,下),繼續遍歷搜尋
road.pop_back() ; // 當前元素出棧
labyrinth[x][y] = 1 ; // 還原現場
}
}
}
int main(){
scanf("%d%d" , &m , &n) ;// 輸入迷宮的行與列的值
for(int i = 1 ; i <= m ; i ++){
for(int j = 1 ; j <= n ; j ++){
scanf("%d" , &labyrinth[i][j]) ; // 輸入迷宮
}
}
scanf("%d%d" , &startX , &startY) ; // 輸入起始點座標
scanf("%d%d" , &endX , &endY) ;// 輸入終點座標
dfs(startX , startY) ; // 從輸入的起點開始 dfs
if(num == 0) printf("-1\n") ; // 如果找不到路徑,則輸出 -1
return 0 ;
}