回溯法的设计思想:
回溯法从根节点出发,按照深度优先策略遍历解空间树,搜索满足约束条件的解。在搜索至树中任一节点时,先判断该节点对应的部分解是否满足约束条件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];
}
}
}