回溯算法 - 子集和问题
(1)问题描述:子集和问题的一个实例为<data, num>。其中 data = {x1, x2, ......, xn} 是一个正整数的集合,targetValue 是一个正整数。子集和问题判定是否存在 data 的一个子集 data1,使得
x1 + x2 + ...... + xn = targetValue (x € data1)
(2)算法设计:使用回溯算法子集树来解决,对于给定的集合 data = {x1, x2, ......, xn} 和正整数 targetValue,计算 data 的一个子集 data1,满足【x1 + x2 + ...... + xn = targetValue (x € data1)】
(3)算法代码:
public class SubsetSum { /** * 目标值 */ private static Integer targetValue; /** * 当前所选元素之和 */ private static Integer sum = 0; /** * 数据个数 */ private static Integer num; /** * 未确定值 */ private static Integer indeterminacyValue = 0; /** * 数据数组 */ private static Integer[] data; /** * 数据存放【0:不存放 1:存放】 */ private static Integer[] store; /** * 初始化数据 */ private static void initData() { Scanner input = new Scanner(System.in); System.out.println("请输入目标值:"); targetValue = input.nextInt(); System.out.println("请输入数据个数:"); num = input.nextInt(); data = new Integer[num]; store = new Integer[num]; System.out.println("请输入各个数:"); for (int i = 0; i < data.length; i++) { data[i] = input.nextInt(); store[i] = 0; // 初始化都不存放 indeterminacyValue += data[i]; } } /** * 回溯查找 */ private static Boolean backtrack(int i) { if (sum == targetValue) { // 找到可行解,直接返回 true return true; } if (i == data.length) { // 找不到可行解,直接返回 false return false; } indeterminacyValue -= data[i]; // 计算还未确定数的总和 if (sum + data[i] <= targetValue) { // 当前 sum + data[i] <= targetValue 直接进入左子树 store[i] = 1; // 数据 i 存放,列入所选加数之中 sum += data[i]; if (backtrack(i + 1)) { // 继续深入下一层判定求和 return true; } sum -= data[i]; // 求解深入完毕,若不满足所求的解,需要回溯,恢复当前的 sum 起始值 } if (sum + indeterminacyValue >= targetValue) { // 剪枝函数【若当前 sum + 未确定的值 >= 目标值,才进入右子树深度搜索;否则没有任何意义】 store[i] = 0; // 数据 i 此时不存放,列入所选加数之中 if (backtrack(i + 1)) { return true; } } indeterminacyValue += data[i]; // 求解深入完毕,若不满足所求的解,需要回溯,恢复当前的 indeterminacyValue 起始值 return false; } /** * 输出 */ private static void print() { System.out.println("\n数据数组:"); Stream.of(data).forEach(element -> System.out.print(element + " ")); System.out.println(); System.out.println("数据存放:"); Stream.of(store).forEach(element -> System.out.print(element + " ")); System.out.println(); System.out.println("组成该目标值的数为:"); for (int i = 0; i < store.length; i++) { if (store[i] == 1) { System.out.print(data[i] + " "); } } System.out.println(); } public static void main(String[] args) { // 初始化数据 initData(); // 回溯查找 backtrack(0); // 输出 print(); } }
(4)输入输出:
请输入目标值: 10 请输入数据个数: 5 请输入各个数: 2 2 6 5 4 数据数组: 2 2 6 5 4 数据存放: 1 1 1 0 0 组成该目标值的数为: 2 2 6
(5)总结:子集和同样也完全体现了回溯法中子集树的核心思想,时间复杂度 O(2n) ,通过存与不存,判断是否剪枝,进入深度搜索一个解,一旦搜索到一个解直接返回即可;
建议:若肉眼看不太懂,可以在纸上根据我的代码思路,画一画走一遍求解的流程,便于理解代码,掌握回溯法子集树的核心思想;