【算法】藍橋杯dfs深度優先搜索之湊算式總結

導航

本文               → 《【算法】藍橋杯dfs深度優先搜索之湊算式總結》java

相關文章        →《【算法】藍橋杯dfs深度優先搜索之排列組合總結》算法

                      →《【算法】藍橋杯dfs深度優先搜索之圖連通總結》數組

前言

曾幾什麼時候這個詞如今用正適合不過了。數據結構

曾幾什麼時候我仍是對dfs算法一臉懵x的狀態,雖然說大二的時候學過數據結構,可是那一學期被我荒廢了......詳情等我畢業了再說吧。app

還有四天就要考藍橋杯了,我分析了近四年的藍橋杯(B組)題型,分類純屬我的理解,並不是官方宣佈,以下表:函數

注:測試

如下DFS表明Depth First Search首字母縮寫,即深度優先搜索,簡稱深搜spa

如下DP表明Dynamic Programming首字母縮寫,即動態規劃,沒有簡稱 : ).net

 

2015年(第六屆)藍橋杯B組省賽
分類 題號 總分
水題 1(3分)、2(5分)、4(11分) 19分
DFS/爆破 3(9分)、5(15分)、7(21分) 45分

冒泡設計

(加法乘法)

6(17分) 17分

取餘

(飲料換購)

8(13分) 13分
矩陣 9(25分) 25分
DP 10(31分) 31分
2016年(第七屆)藍橋杯B組省賽
分類 題號 總分
遞推 1(3分)、2(5分) 8分
函數調用 4(11分) 11分
DFS/爆破

3(9分)、5(13分)、

6(15分)、7(19分)、

8(21分)

77分
DP 9(23分) 23分
數據結構 10(31分) 31分
2017年(第八屆)藍橋杯B組省賽
分類 題號 總分
文件處理 1(5分)、3(13分) 18分
DFS/爆破 2(11分)、4(17分) 28分
水題 5(7分) 7分
DP 6(9分)、8(21分) 30分
日期問題 7(19分) 19分
二分問題 9(23分) 23分
前綴和 10(25分) 25分
2018年(第九屆)藍橋杯B組省賽
分類 題號 總分
水題 1(5分)、2(7分) 12分
複數 3(13分) 13分
排序 5(9分)、6(11分) 20分
找規律 7(19分) 19分
尺取法 8(21分) 21分
DFS/爆破 9(23分) 23分
DP 4(17分)、10(25分) 42分

能夠看到每一年藍橋杯都會考察dfs類型的題,並且分值在23-77分之間不等,雖然說分值成逐年降低的趨勢,但怎麼說也有20+分,藍橋杯滿分150分,普通人(我就是普通人中之一)總共能拿到的分數大概在80分左右,而DFS就包括在這80分以內,因此必定要掌握好。

以上是對題型及分值的分析,接下來分析一下爲什麼藍橋杯總要考dfs算法?首先看一則人物百科:

再來看一下「深度優先搜索」的百度詞條:

還有一點值得提出,這位大神是中國政府外聘千人計劃之一,目前好像在中國清北或港大其中一個學校任教。

藍橋杯做爲一個算法類競賽,再退一步講,怎麼說也得給老前輩一個面子吧。

綜上所述,藍橋杯考dfs算法的機率賽過千足金含金量的比率!


正文

下文的大部分靈感均來自於【CSDN】梅森上校《JAVA版本:DFS算法題解兩個例子(走迷宮和求排列組合數)》

下面會貼出藍橋杯第六屆B組省賽第3題,第5題;第七屆B組省賽第3題,第8題;第八屆B組省賽第2題,共5道題。

由於它們都是:湊算式

【第一道題】

【分析】

總共有8個不一樣的字,每一個字都表明不一樣的數,申請一個一維數組a[8]存儲這8個不一樣的數。

a[8]對應有下標index:0至7

因此dfs方法定義爲

public static void dfs(int index)

而後設計結束條件,就是index等於8,越界了,表明找夠了8個數,嘗試進行判斷是否構成等式。

if(index == 8) { int sum = (a[0]*1000+a[1]*100+a[2]*10+a[3]) + (a[4]*1000+a[5]*100+a[6]*10+a[1]); if(sum == a[4]*10000+a[5]*1000+a[2]*100+a[1]*10+a[7]) System.out.println(a[4]+""+a[5]+""+a[6]+""+a[1]); return; }

若是index沒有越界,a[]數組沒存夠8個數,就進行搜索。

由於從0至9搜索,且每一個數不能重複,因此每訪問一個都須要標記一下,visited[i] 爲 0 表明未訪問,爲 1 表明已訪問。

for(int i=0; i<=9; i++) {
    if(visited[i] == 0) {// 表明沒被訪問過,能夠訪問
        visited[i] = 1; // 訪問 i, 作標記
        a[index] = i; // 往數組裏放數
        dfs(index+1); // 枚舉下一種狀況
        visited[i] = 0; // 回溯, 清除標記
    }
}

經過數學分析,祥字不可能爲0,不然沒法進位,三字必爲1,由於十進制進位只能進1,因此最終搜索代碼以下:

for(int i=0; i<=9; i++) { if(index == 0 && i == 0) {// 祥字不可能爲0,不然就沒法進位了 continue; } if(index == 4 && i != 1) {// 三字必爲1,由於十進制進位只能進1 continue; } if(visited[i] == 0) {// 表明沒被訪問過,能夠訪問 visited[i] = 1; // 訪問 i, 作標記 a[index] = i; dfs(index+1); visited[i] = 0; // 回溯, 清除標記 } }

【完整代碼】

  

 1 /**
 2  *  3  * 祥瑞生輝  4  * + 三羊獻瑞  5  * ----------  6  * 三羊生瑞氣  7  *  8  * 抽象爲以下:  9  * 10  * a[0]a[1]a[2]a[3] 11  * + a[4]a[5]a[6]a[1] 12  *------------------- 13  * a[4]a[5]a[2]a[1]a[7] 14  * 15  */
16 public class Main { 17     static int[] a = new int[8]; // 總共有8個不一樣的字,表明8個不一樣的數
18     static int[] visited = new int[] {0,0,0,0,0,0,0,0,0,0}; // 10個0, 標記0-9十個數是否被訪問過
19     public static void dfs(int index) { 20         // index爲8,越界,表明8個數找夠了,同時也是遞歸結束條件
21         if(index == 8) { 22             int sum = (a[0]*1000+a[1]*100+a[2]*10+a[3]) + (a[4]*1000+a[5]*100+a[6]*10+a[1]); 23             if(sum == a[4]*10000+a[5]*1000+a[2]*100+a[1]*10+a[7]) 24                 System.out.println(a[4]+""+a[5]+""+a[6]+""+a[1]); 25             return; 26         } else {// 不夠8個數,0-9十個數,深搜
27             for(int i=0; i<=9; i++) { 28                 if(index == 0 && i == 0) {// 祥字不可能爲0,不然就沒法進位了
29                     continue; 30  } 31                 if(index == 4 && i != 1) {// 三字必爲1,由於十進制進位只能進1
32                     continue; 33  } 34                 if(visited[i] == 0) {// 表明沒被訪問過,能夠訪問
35                     visited[i] = 1; // 訪問 i, 作標記
36                     a[index] = i; 37                     dfs(index+1); 38                     visited[i] = 0; // 回溯, 清除標記
39  } 40  } 41  } 42         
43  } 44 
45     public static void main(String[] args) { 46         dfs(0); 47  } 48 
49 }

 

沒看懂這道題?不要緊。繼續往下看。

【第二道題】

【分析】

這是一道填空題,基本代碼已經給出來了,我只是借題發揮,講解一下dfs算法。

上一題是8個數,咱們設置了一個a[8],這道題是9個數,因此設置一個a[9]

一樣須要一個index:0至8

這道題和上一道題的區別在於題目用了dfs()方法多了一個數組參數,以下

public static void dfs(int[] a, int index)

結束條件和上一題相似,當index爲9時,越界。

if(index == 9) { 
    int m = a[0]*1000+a[1]*100+a[2]*10+a[3];
    int n = a[4]*10000+a[5]*1000+a[6]*100+a[7]*10+a[8];
    if(m*3 == n) 
        System.out.println(m+" "+n);
    return;
}

若是index不爲9,表明還沒湊夠9個數,深搜。上一題深搜以前是作標記,深搜以後重置標記,這一題相似,深搜以前交換,深搜以後重置交換,代碼以下:

for(int i=index; i<a.length; i++){
    int t=a[index]; a[index]=a[i]; a[i]=t; // 交換a[index]和a[i]
    dfs(a,index+1);
    t=a[index]; a[index]=a[i]; a[i]=t; // 再次交換a[index]和a[i]
}

這道填空題須要注意的是,不要徹底照抄以至於連 int 都抄上了,致使 t 被聲明兩次,報錯。

【完整代碼】

 1 public class Main {  2 
 3     public static void dfs(int[] a, int index) {  4                 // 結束條件
 5         if(index == 9) {  6             int m = a[0]*1000+a[1]*100+a[2]*10+a[3];  7             int n = a[4]*10000+a[5]*1000+a[6]*100+a[7]*10+a[8];  8             if(m*3 == n)  9                 System.out.println(m+" "+n); 10             return; 11  } 12         
13         for(int i=index; i<a.length; i++){ 14             int t=a[index]; a[index]=a[i]; a[i]=t; // 交換a[index]和a[i]
15             dfs(a,index+1); 16             t=a[index]; a[index]=a[i]; a[i]=t; // 再次交換a[index]和a[i]
17  } 18  } 19     public static void main(String[] args) { 20             int[] a = new int[]{1,2,3,4,5,6,7,8,9}; 21         dfs(a,0); 22  } 23 
24 }

 

又沒看懂?不要緊。繼續往下看。

【第三道題】

此題和第二道題相似,都是1-9九個數字,不過這裏要注意的是,第二題用乘法代替了除法,這道題雖說也能夠改爲乘法,可是會相對麻煩一點,乾脆就直接用除法,一旦涉及到除法,就有可能產生浮點數,因此咱們在聲明數組的時候不能再聲明爲int了,必須聲明爲double。

index仍是int的,取值範圍爲0至8

dfs方法定義以下:

public static void dfs(double[] a,int index)

結束條件就是index==9,越界

if(index == 9) {
    if(a[0]+a[1]/a[2]+(a[3]*100+a[4]*10+a[5])/(a[6]*100+a[7]*10+a[8]) == 10)
        count++;
    return;
}

當index不夠9的時候,進行深搜

for(int i=index; i<a.length; i++) {
    double t = a[index]; a[index]=a[i]; a[i] = t;
    dfs(a,index+1);
    t = a[index]; a[index]=a[i]; a[i] = t;				
}

【完整代碼】

 1 public class 湊算式交換法dfs {  2     
 3     public static int count = 0;  4     public static void dfs(double[] a,int index) {  5         // 結束條件
 6         if(index == 9) {  7             if(a[0]+a[1]/a[2]+(a[3]*100+a[4]*10+a[5])/(a[6]*100+a[7]*10+a[8]) == 10)  8                 count++;  9             return; 10         } else { 11             for(int i=index; i<a.length; i++) { 12                 double t = a[index]; a[index]=a[i]; a[i] = t; 13                 dfs(a,index+1); 14                 t = a[index]; a[index]=a[i]; a[i] = t; 15  } 16  } 17  } 18     
19     public static void main(String[] args) { 20         double[] a = new double[] {1.00,2.00,3.00,4.00,5.00,6.00,7.00,8.00,9.00}; 21         dfs(a,0); 22  System.out.println(count); 23  } 24 
25 }
 1 public class 湊算式交換法dfs {  2     
 3     public static int count = 0;  4     public static void dfs(double[] a,int index) {  5         // 結束條件
 6         if(index == 9) {  7             if(a[0]+a[1]/a[2]+(a[3]*100+a[4]*10+a[5])/(a[6]*100+a[7]*10+a[8]) == 10)  8                 count++;  9             return; 10         } else { 11             for(int i=index; i<a.length; i++) { 12                 double t = a[index]; a[index]=a[i]; a[i] = t; 13                 dfs(a,index+1); 14                 t = a[index]; a[index]=a[i]; a[i] = t; 15  } 16  } 17  } 18     
19     public static void main(String[] args) { 20         double[] a = new double[] {1.00,2.00,3.00,4.00,5.00,6.00,7.00,8.00,9.00}; 21         dfs(a,0); 22  System.out.println(count); 23  } 24 
25 }

 

固然,這道題也能夠像第一題「三羊獻瑞」那樣,使用遞歸以前標記法

【標記法完整代碼】

 1 public class 湊算式標記dfs {  2     public static double[] a = new double[9];  3     public static int[] visited = new int[] {0,0,0,0,0,0,0,0,0}; // 9個0
 4     public static int count = 0;  5     public static void dfs(int index) {  6         // 結束條件
 7         if(index == 9) {  8             if(a[0]+a[1]/a[2]+(a[3]*100+a[4]*10+a[5])/(a[6]*100+a[7]*10+a[8]) == 10)  9                 count++; 10             return; 11         } else { 12             for(int i=1; i<=9; i++) { 13                 if(visited[i-1] == 0) { // 由於i從1開始,因此須要 -1
14                     visited[i-1] = 1; // 標記
15                     a[index] = (double)i; 16                     dfs(index+1); 17                     visited[i-1] = 0; // 清除標記
18  } 19  } 20  } 21  } 22     
23     public static void main(String[] args) { 24         dfs(0); 25  System.out.println(count); 26  } 27 
28 }

 

對visited初始化的說明:

Java語言int型數組初始化時會自動賦值爲0,可是我爲了直觀表達,因此選擇了顯式初始化。

今天已經深夜1:22,還有一個四平方和沒有寫,明天再寫

我必定會回來的......


我又回來了,繼續看題。

【第四道題】

經過上面的三道題,我想如今的dfs的套路應該已經有一些印象了吧。

由於是四平方,要存四個數,因此咱們思考都不用思考,先整個a[4]

同理,須要一個index,0至3

由於前幾道題的程序都不須要咱們進行輸入,這道題須要輸入一個num,並且還須要進行等式判斷,因此須要做爲dfs()方法的一個參數,這樣dfs()方法定義以下:

public static void dfs(int index, int num)

遞歸結束條件爲index==4,越界,表明a[]數組存夠4個數了,能夠進行等式判斷了,代碼以下:

if(index == 4) { if(num == (int)(Math.pow(a[0], 2)+Math.pow(a[1], 2) +Math.pow(a[2], 2)+Math.pow(a[3], 2))) { System.out.println(a[0]+" "+a[1]+" "+a[2]+" "+a[3]); } return; }

若是index不等於4,表明4個數還沒找夠,深搜。這裏要注意3件事:

一是深搜的範圍,最大範圍是三個0和輸入數字的開根號。

二是Java中Math.sqrt()方法返回值是double,最好轉爲int,讓兩個int進行比較。

三是遞歸以前要不要留下已訪問的標記,或者交換?從給出的輸入樣例能夠看出來,有0 0 1 2 、 0 2 2 2 這樣的輸出,因此說數字是能夠重複使用的,那麼就不必進行標記或者交換了。這也是這道題和前三道題區別之一。

綜上,搜索代碼以下

for(int i=0; i<=(int)Math.sqrt(num); i++) { a[index] = i; dfs(index+1,num); }

【完整代碼】

 1 import java.util.Scanner;  2 
 3 public class 四平方和dfs {  4     
 5     public static int[] a = new int[4];  6 
 7     public static void dfs(int index, int num) {  8     // 結束條件
 9         if(index == 4) { 10             if(num == (int)(Math.pow(a[0], 2)+Math.pow(a[1], 2) 11                                 +Math.pow(a[2], 2)+Math.pow(a[3], 2))) { 12                 System.out.println(a[0]+" "+a[1]+" "+a[2]+" "+a[3]); 13  } 14             return; 15  } 16         // 搜索
17         for(int i=0; i<=(int)Math.sqrt(num); i++) { 18             a[index] = i; 19             dfs(index+1,num); 20  } 21  } 22     
23     public static void main(String[] args) { 24         Scanner sc = new Scanner(System.in); 25         int num = sc.nextInt(); 26         dfs(0,num); 27 }

 

【等等,這道題還沒完】

畢竟是一道23分的大題,尊重一下出題人嘛,怎麼會讓你這麼輕鬆的拿分呢?

若是按照上面的代碼,輸入5,會輸出下面的結果

5
0 0 1 2
0 0 2 1
0 1 0 2
0 1 2 0
0 2 0 1
0 2 1 0
1 0 0 2
1 0 2 0
1 2 0 0
2 0 0 1
2 0 1 0

顯然,人家只要第一行。在趙壯同窗的指點下,直接在輸出下面加一句System.exit(0);就能夠了。

System.out.println(a[0]+" "+a[1]+" "+a[2]+" "+a[3]); System.exit(0); // 加上這句,打印完直接結束程序

【再等最後一下,真的】

雖說上面的代碼可以實現輸出第一行,可是深搜畢竟是一種盲目的遞歸,進行大量無用的嘗試,最終致使超時。我僅僅是拿這道題讓你們更深刻了的理解一下dfs這個算法。就上面的代碼,我測試了一下 773535 這個輸入,Eason的《很久不見》循環播放三遍了都還沒跑出來,這要是提交了藍橋比賽,那就GG了......

其實深搜大部分均可以用for循環暴力破解代替,這樣能夠減小一下盲目性,好比這道題的解法能夠參考下面這篇文章:

【CSDN】有夢就不怕痛《藍橋杯 四平方和》

Java版代碼以下(運行飛通常的流暢):

 1 import java.util.Scanner;  2 
 3 public class 四平方和爆破 {  4 
 5     public static void main(String[] args) {  6         int n, a, b, c, d;  7         Scanner sc = new Scanner(System.in);  8         n = sc.nextInt();  9         for (a = 0; a < 3000; ++a) {// 3000² = 900萬 > 500萬
10             for (b = a; b < 3000; ++b) { 11                 for (c = b; c < 3000; ++c) { 12                     d = (int)Math.sqrt(n - a * a - b * b - c * c); 13                     if (n == a * a + b * b + c * c + d * d) { 14                         if (c > d) { 15                             int temp = d; 16                             d = c; 17                             c = temp; 18  } 19                         System.out.println(a+" "+b+" "+c+" "+d); 20                         return; // 找到以後就return,讓全部循環結束
21  } 22  } 23  } 24  } 25  } 26 }

 

好了,我已經逐個分析了四道題了,原本已經快要分析吐了,不想再分析了。可是後來想了想小王子裏面那句話:「每一個大人都曾經是個孩子,可又有幾我的記得呢?」還有我國晉惠帝說的一句千古名言:「何不食肉糜?」

下面這道題就算是對「湊算式」這類題型的一個總結吧。

【第五道題】

有了上面四道題的經驗,看到這種湊算式的題,上來想都不用想:

第一件事,就是設數組

第二件事,數字可否重複使用?能,略過這條;不能,設標記數組,或者交換(我我的偏向設標記數組)

第三件事,定義dfs()方法

第四件事,判斷遞歸結束條件,一般是index越界,並進行等式判斷

第五件事,還未湊齊數,深度優先搜索

第六件事,寫main()方法

完事

【完整代碼】

 1 public class 紙牌三角形dfs {  2 
 3     public static int[] a = new int[9];  4     public static int[] visited = new int[] {0,0,0,0,0,0,0,0,0};  5     public static int count = 0;  6     public static void dfs(int index) {  7         // 結束條件
 8         if(index == 9) {  9             if(a[0]+a[1]+a[3]+a[5] == a[0]+a[2]+a[4]+a[8] &&
10                 a[0]+a[1]+a[3]+a[5] == a[5]+a[6]+a[7]+a[8]) { 11                 count++; 12  } 13             return; 14  } 15         // 搜索
16         for(int i=1; i<=9; i++) { 17             if(visited[i-1] == 0) { // i從1開始,因此要i-1
18                 visited[i-1] = 1; 19                 a[index] = i; 20                 dfs(index+1); 21                 visited[i-1] = 0; 22  } 23  } 24  } 25     
26     public static void main(String[] args) { 27         dfs(0); 28         System.out.println(count/6); 29  } 30 
31 }

 

這道題有個坑就是要去重,由於題中說明「旋轉,鏡像後相同的算一種」,因此最後要count/6.這個純靠經驗了,有過旋轉經驗的就知道,每一個數都有機會佔在頂點上,全部數固定後,轉動的話,等於每一個數計算了3次,因此要除以3;對於鏡像,一樣是靠經驗,若是你知道從鏡子裏看左右會對換的話,就會知道要除以2,總共除以6.

在沒作這道題以前,我是不知道鏡像是什麼樣的,所以我還特地拿個鏡子照了照。

 

 

 

這篇文章就到此結束吧,我要開一個新坑了,明天應該能發佈新文章:

《【算法】藍橋杯dfs深度優先搜索之排列組合總結》

 

若是看完這5道題還不會這種「湊算式」的題,請發QQ郵箱:424171723@qq.com 或 直接加我QQ,進行手把手教學。

 


【參考文章】

  1. 【CSDN】梅森上校《JAVA版本:DFS算法題解兩個例子(走迷宮和求排列組合數)》
  2. 【CSDN】winycg《2015藍橋杯 三羊獻瑞(回溯法dfs)》
  3. 【CSDN】有夢就不怕痛《藍橋杯 四平方和》
  4. 還有一些CSDN、博客園、簡書.......因爲瀏覽篇幅太多,因此一些未列在其中,表示抱歉
相關文章
相關標籤/搜索