回溯法的設計思想:
回溯法從根節點出發,按照深度優先策略遍歷解空間樹,搜索滿足約束條件的解。在搜索至樹中任一節點時,先判斷該節點對應的部分解是否滿足約束條件I,是否超出目標函數的界,也就是判斷該節點是否包含問題的最優解,如果肯定不包含,則跳過對以該節點爲根的子樹的搜索(即所謂剪枝),返回上一層的節點,從其他子節點尋找通向最優解的路徑;否則,進入以該節點爲根的子樹,繼續按照深度優先策略搜索。
回溯法的搜索過程涉及的節點稱爲搜索空間,只是整個解空間樹的一部分,在搜索過程中,通常採用兩種策略避免無效搜索:
- 用約束條件剪去得不到可行解的子樹
- 用目標函數剪去得不到最優解的子樹
這兩類函數統稱剪枝函數。
需要注意的是:
問題的解空間樹是虛擬的,並不需要在算法運行時構造一棵真正的樹結構,只需要存儲從根節點到當前節點的路徑。
連續郵資問題
問題描述:
假設某國家發行了n種不同面值的郵票,並且規定每張信封上最多隻允許貼m張郵票。連續郵資問題要求對於給定的n和m,給出郵票面值的最佳設計,在1張信封上貼出從郵資1開始,增量爲1的最大連續郵資區間。 例如當n=5,m=4時,面值爲1,3,11,15,32的5種郵票可以貼出郵資的最大連續區間是1到70。
輸入樣例:
5 4
1 3 11 15 32
輸出樣例:
70
算法思路:
參考:https://www.jianshu.com/p/313d1e98c0d7
每張信封最多貼m張郵票,也就是說可能貼了m張,也可能貼了0張、1張等等。爲了便於計算,我們可以把未貼滿m張郵票看作貼了x張郵票和m-x張面值爲0的郵票,從而構造一棵完全多叉樹。若求所有可能的組合情況,解空間是(n+1)^m。
以n=5,m=4爲例,解空間爲完全多叉樹,圖中只以一條路徑爲例:
實際求解需要對其進行剪枝:解的約束條件是必須滿足當前解的郵資可以構成增量爲1的連續空間,所以在搜索至樹中任一節點時,先判斷該節點對應的部分解是否滿足約束條件,也就是判斷該節點是否包含問題的最優解,如果肯定不包含,則跳過對以該節點爲根的子樹的搜索,返回上一層的節點,從其他子節點尋找通向最優解的路徑;否則,進入以該節點爲根的子樹,繼續按照深度優先策略搜索。
求解過程:
- 讀入郵票面值數n,每張信封最多貼的郵票數m
- 讀入郵票面值數組nums[],除了正常面值,再加入一個值爲0的面值
- 循環求取區間最大值maxValue,maxValue初始設爲0
① 從0張郵票開始搜索解空間,如果當前未達到葉節點,且過程值temp=temp+nums[i]未達到當前記錄的區間最大值maxValue,則繼續向下搜索
② 若超過了區間最大值maxValue,則當前面值不是可行解,計算下一個面值nums[i+1]。若循環結束,當前節點的所有面值都無法滿足,則說明再往下搜索也不可能有可行解,這個時候回溯到上一節點
③ 若當前已搜索到葉節點,判斷當前路徑下的解temp是否滿足比當前記錄的區間最大值maxValue大1。若滿足,則更新區間最大值maxValue;若不滿足,回溯到上一節點 - 重複步驟3直到沒有滿足當前區間最大值maxValue+1的可行解,則當前記錄的maxValue就是區間最大值
算法實現:
public class Main {
// n表示郵票面值數,m表示每張信封最多貼的郵票數
private static int n;
private static int m;
// 郵票面值數組
private static int[] nums;
// 當前解是否可行
private static boolean accFlag;
// 記錄區間最大值
private static int maxValue = 0;
// 搜索過程中的郵資
private static int temp = 0;
public static void main(String[] args) {
// 讀取鍵盤輸入
Scanner sc = new Scanner(System.in);
while (true) {
n = sc.nextInt();
m = sc.nextInt();
// 多一個是假設有面值爲0的郵票
nums = new int[n + 1];
nums[0] = 0;
for (int i = 1; i < n + 1; i++) {
nums[i] = sc.nextInt();
}
solution(n, m, nums);
System.out.println("maxValue=" + maxValue);
}
}
public static void solution(int n, int m, int[] nums) {
// 求解區間最大值,一直搜索,直到確定最大值
while (true) {
accFlag = false;
// 每次都從0張郵票開始搜索解空間
// 當前求解的目標是判斷是否存在比當前區間最大值大1的解
search(0);
// 若存在
if (accFlag) {
// 連續區間增量加1
maxValue++;
} else {
break;
}
}
}
/**
* 搜索解空間,找到比當前區間最大值大1的解
*
* @param t 當前郵票數量
*/
public static void search(int t) {
// 葉節點,結束搜索
if (t == m) {
// 當前解可以將連續區間最大值加1
if (temp == maxValue + 1) {
accFlag = true;
}
// 此時已經是葉節點,若不是可行解,則需要回溯到t-1
return;
}
// 未結束,繼續向下搜索
// 遍歷所有面值
for (int i = 0; i < nums.length; i++) {
temp += nums[i];
// 如果當前未達到葉節點,且過程值未達到當前記錄的區間最大值,則繼續向下搜索
if (temp <= maxValue + 1) {
search(t + 1);
}
// 若超過了區間最大值,則當前面值不是可行解,需要回溯,計算下一個面值
temp -= nums[i];
}
}
}