設想我們現在身處一個巨大的迷宮中,我們只能自己想辦法走出去,下面是一種看上去很盲目但實際上會很有效的方法。
以當前所在位置為起點,沿着一條路向前走,當碰到岔道口時,選擇其中一個岔路前進。如果選擇的這個岔路前方是一條死路,就退回到這個岔道口,選擇另一個岔路前進。如果岔路口存在新的岔道口,那么仍然按上面的方法枚舉新岔道口的每一條岔道。這樣,只要迷宮存在出口,那么這個方法一定能夠找到它。
也就是說,當碰到岔道口時,總是以“深度”作為前進的關鍵詞,不碰到死胡同就不回頭,因此這種搜索的方式稱為深度優先搜索(DFS)。
接下來講解一個例子。
有 n 件物品,每件物品的重量為 w[i],價值為 c[i]。現在需要選出若干件物品放入一個容量為 V 的背包中,使得在選入背包的物品重量和不超過容量 V 的前提下,讓背包中物品的價值之和最大,求最大價值。(1≤n≤20)
在這個問題中,對每件物品都有選或者不選兩種選擇,而這就是所謂的“岔道口”。那么什么是“死胡同”呢?題目要求選擇的物品重量總和不能超過 V,因此一旦選擇的物品重量總和超過 V,就會到達“死胡同”,需要返回最近的“岔道口”。
DFS 函數的參數中必須記錄當前處理的物品編號 index,和在處理當前物品之前,已選物品的總重量 sumW 與 總價值 sumC。於是 DFS 函數如下:
void DFS(int index, int sumW, int sumC) {...}
思路:
- 如果選擇不放入 index 號物品,那么 sumW 與 sumC 就將不變,接下來處理 index+1 號物品,即前往 DFS(index+1, sumW, sumC) 這條分支;
- 如果選擇放入 index 號物品,那么 sumW=sumW+w[index], sumC=sumC+c[index],接着處理 index+1 號物品,即前往 DFS(index+1, sumW+w[index], sumC+c[index]) 這條分支;
- 一旦 index 增長到了 n,則說明已經把 n 件物品處理完畢。此時記錄的 sumW 和 sumC 就是所選物品的總重量和總價值。如果 sumW 不超過 V 且 sumC 大於記錄的最大總價值 maxValue,就說明當前的這種選擇方案可以得到更大的價值,於是用 sumC 更新 maxValue。
代碼如下:
1 /* 2 搜索_DFS 3 有 n 件物品,每件物品的重量為 w[i],價值為 c[i]。現在需要選出若干件物品 4 放入一個容量為 V 的背包中,使得在選入背包的物品重量和不超過容量 V 的前提下, 5 讓背包中物品的價值之和最大,求最大價值。(1≤n≤20) 6 */ 7 8 #include <stdio.h> 9 #include <string.h> 10 #include <math.h> 11 #include <stdlib.h> 12 #include <time.h> 13 #include <stdbool.h> 14 15 #define maxn 30 16 int n, V, maxValue; // 物品減數,背包容量,最大價值 17 int w[maxn], c[maxn]; // 每件物品的重量,價值 18 19 // index 當前處理的物品編號,sumW 和 sumC 為當前總重量和總價值 20 void DFS(int index, int sumW, int sumC) { 21 if(index == n) { // 已經把 n 件物品處理完畢(死胡同) 22 if(sumW <= V && sumC > maxValue) { 23 maxValue = sumC; // 有更好的選擇 24 } 25 return; 26 } 27 // 岔道口 28 DFS(index+1, sumW, sumC); // 不選 Index 號物品 29 DFS(index+1, sumW+w[index], sumC+c[index]); // 選 index 號物品 30 } 31 32 int main() { 33 scanf("%d %d", &n, &V); 34 int i; 35 for(i=0; i<n; ++i) { 36 scanf("%d", &w[i]); // 每件物品的重量 37 } 38 for(i=0; i<n; ++i) { 39 scanf("%d", &c[i]); // 每件物品的價值 40 } 41 DFS(0, 0, 0); 42 printf("%d\n", maxValue); 43 44 return 0; 45 }
在上述代碼中,總是把 n 件物品的選擇全部確定之后才去更新最大價值,但是事實上忽視了背包容量不超過 V 這個特點。也就是說,完全可以把對 sumW 的判斷加入“岔道口”中,只有當 sumW ≤ V 時才進入岔道,這樣效率會高很多。代碼如下:
1 // index 當前處理的物品編號,sumW 和 sumC 為當前總重量和總價值 2 void DFS1(int index, int sumW, int sumC) { 3 if(index == n) { // 已經把 n 件物品處理完畢(死胡同) 4 return; 5 } 6 // 岔道口 7 DFS(index+1, sumW, sumC); // 不選 Index 號物品 8 // 只有加入 index 物品后總重量小於 V 才可以繼續 9 if(sumW + w[index] <= V) { 10 if(sumC + c[index] > maxValue) { 11 maxValue = sumC + c[index]; 12 } 13 DFS(index+1, sumW+w[index], sumC+c[index]); // 選 Index 號物品 14 } 15 }
再來看另外一個問題。
給定 N 個整數(可能有負數),從中選擇 K 個數,使得這 K 個數之和恰好等於一個給定的整數 X;如果有多種方案,選擇它們中元素平方和最大的一個。
與之前的問題類似,此處仍然需要記錄當前處理的整數編號 index;由於要求恰好選擇 K 個數,因此需要一個參數 nowK 來記錄當前已經選擇的數的個數;另外,還需要參數 sum 和 sumSqu 分別記錄當前已選整數之和與平方和。於是 DFS 函數如下:
void DFS(int index, int nowK, int sum, int sumSqu) {...}
思路:
- 需要一個數組 temp,用以存放當前選擇的整數。
- 當試圖進入“選 index 號數”這條分支時,就把 A[index] 加入 temp 中;
- 當這條分支結束時,就還原 temp 數組,使他不影響“不選 index 號數”這條分支。
- 如果當前已選擇了 K 個數,且這 K 個數之和恰為 x 時,就將平方和與已有的最大平方和 maxValue 作比較,如果更大,更新 maxValue 和數組 ans。
代碼如下:
1 /* 2 DFS_N個整數選K個 3 給定 N 個整數(可能有負數),從中選擇 K 個數,使得這 K 個數之和 4 恰好等於一個給定的整數 X;如果有多種方案,選擇它們中元素平方和最大的一個。 5 */ 6 7 #include <stdio.h> 8 #include <string.h> 9 #include <math.h> 10 #include <stdlib.h> 11 #include <time.h> 12 #include <stdbool.h> 13 14 #define maxn 30 15 // 序列A中n個數選k個數使得和為x,最大平方和為maxSumSqu 16 int n, k, x, maxSumSqu=-1, A[maxn]; 17 int temp[maxn]={0}, ans[maxn]={0}; // 臨時方案,平方和最大的方案 18 19 void DFS(int index, int nowK, int sum, int sumSqu) { 20 if(nowK == k && sum == x) { // 找到K個數和為x 21 if(sumSqu > maxSumSqu) { // 更優方案 22 maxSumSqu = sumSqu; // 更新 maxValue 和數組 ans 23 int i; 24 for(i=0; i<k; ++i) { 25 ans[i] = temp[i]; 26 } 27 } 28 } 29 // 已經處理完n個數,選擇超過k個數,和大於x 30 if(index==n || nowK>k || sum>x) return; 31 // 選 index 號數 32 temp[nowK] = A[index]; 33 DFS(index+1, nowK+1, sum+A[index], sumSqu+A[index]*A[index]); 34 temp[nowK] = 0; 35 // 不選 index 號數 36 DFS(index+1, nowK, sum, sumSqu); 37 } 38 39 int main() { 40 scanf("%d %d %d", &n, &k, &x); 41 int i; 42 for(i=0; i<n; ++i) { 43 scanf("%d", &A[i]); // n個數 44 } 45 DFS(0, 0, 0, 0); 46 for(i=0; i<k; ++i) { // 最優方案 47 printf("%d ", ans[i]); 48 } 49 printf("%d\n", maxSumSqu); // 最優方案的平方和 50 51 return 0; 52 }
