觉得在用回溯解决01背包问题之前最好先解决一下子集和问题。
问题描述:有 n n n个不同正整数组成的集合W = { w 1 , w 2 , w 3 , . . . , w n w_1,w_2,w_3,...,w_n w1,w2,w3,...,wn } 和一个正整数Z,要求找出W中所有和为Z的子集。
例如 W = { 11, 13, 24, 7 },Z = 31时,满足要求的子集为{ 11, 13, 7 }和{ 24, 7 }
本题的解法其实就是树的先序遍历+剪枝。
对于集合中的所有整数,都只有选或不选两种可能,以树的第1层表示第1个整数,第2层表示第2个整数,…,直到第n层,就可以得到下面这棵解的空间树。
对于空间树中每个结点,往左延伸表示选这个整数,同时标记置为1;往右延伸表示不选这个整数,标记置为0,这样最下面的每个叶子结点都代表了一种组合。
树的遍历过程用个变量保存已选整数之和,这样每遍历到一个叶子结点就可以根据变量的值判断这条路径上的结点是不是一组解。
很明显上述这样肯定是能够得到结果,但随着集合长度的增加空间树以指数级增长。但也并非每条路径都需要走到叶子结点才可以判定是否为解。
假如给每个结点增加两个属性 tw 和 rw,tw 表示前面已选整数之和,rw表示后面剩下的整数的和,那么有:
约束函数:tw + w i w_i wi ≤ Z,作用是选取满足条件的一个解。
限界函数:tw + rw - w i w_i wi >= Z,也可以是 tw + rw > Z,作用是剪除不可能存在解的结点。
约束函数和限界函数统称剪枝函数,可以看出剪枝函数其实就是在限定可行解的空间。
上面的空间树经过剪枝之后的状态(限界函数用的 tw + rw - w i w_i wi >= Z ):
java代码
public class SubSetSum {
public static void search(int[] W, int Z) {
int n = W.length - 1;
int[] track = new int[W.length];
int tw = 0; // tw表示考虑第i个整数时前面已选取的整数和
int rw = 0; // rw表示考虑第i个整数时第i个及后面所有的整数和,初始为集合元素总和
for (int i = 1; i < W.length; i++)
rw += W[i];
dfs(W, Z, n, tw, rw, track, 1);
}
private static void dfs(int[] W, int Z, int n, int tw, int rw, int[] track, int i) {
// 到达一个叶子结点,根据结点中tw的值判定是否为解
if (i > n) {
if (tw == Z)
print(track, W);
} else { // 没有找完所有整数
// 左孩子结点剪枝,选取满足条件的整数W[i]
if (tw + W[i] <= Z) {
track[i] = 1; // 表示选取第i个整数
dfs(W, Z, n, tw + W[i], rw - W[i], track, i + 1); // 判断下一个整数
}
// 有孩子结点剪枝,剪出不可能存在解的结点
if (tw + rw - W[i] >= Z) {
track[i] = 0; // 不选取第i个整数
dfs(W, Z, n, tw, rw - W[i], track, i + 1); // 判断下一个整数
}
}
}
private static void print(int[] track, int[] W) {
for (int i = 1; i < track.length; i++) {
if (track[i] == 1) {
System.out.print(W[i] + " ");
}
}
System.out.println();
}
public static void main(String[] args) {
int[] set = { 0, 11, 24, 13, 7 }; // 数组的下标0位置不用
search(set, 31);
}
}
时间复杂度因存在剪枝而不确定,因为传入数组中整数的顺序不同剪枝后的状态也不同,则遍历到的结点个数也不一样。但由于 n n n个整数的解空间树总结点数为 2 n + 1 − 1 2^{n+1}-1 2n+1−1个,所以对应的最坏时间复杂度 O ( 2 n ) Ο(2^n) O(2n)。
如果存在解,则一定会遍历到底层叶子结点,递归过程中函数压栈,空间复杂度 O ( l o g 2 n ) Ο(log_ 2n) O(log2n)。
同时应该注意到: