子集树就是代表了一类问题的解空间,当所给问题是从n个元素的集合S中找出S满足某种性质的子集时,相应的解空间称为子集树。例如:n个物品的0-1背包问题/装在问题所对应的解空间是一棵子集树,这类子集树通常有2^n个叶结点,时间复杂度为O(2^n)
子集树的思想为深度优先算法,从根节点出发,不断访问符合条件的子节点,(如果不符合则进行剪枝并访问该节点的兄弟节点),当访问到叶节点的时候,进行输出或者计数工作,然后对叶节点的兄弟节点进行访问,直到访问结束,再访问叶节点的父节点的兄弟结点(回溯)
先看这样一个代码:
public class TestDemo {
public static void main(String[] args) {
int[] arr = {1,2,3};
backstrace(arr,0);
}
private static void backstrace(int[] arr,int i) {
if (i == arr.length) {
for (int j = 0; j < arr.length; j++) {
System.out.print (arr[j] + " ");
}
System.out.println ();
} else {
backstrace (arr, i + 1);//访问i节点的左孩子
backstrace (arr, i + 1);//访问i节点的右孩子
}
}
}
问题:打印结果是什么?
由图我们可以看到,这是一个简单的递归问题,深度遍历二叉树的思想,因此打印结果是 8个“1 2 3”
在上面代码的基础上,给节点的左树枝标识1,右树枝标识0,只打印左树枝的值:
代码:
public class ChildTree {
public static void main(String[] args) {
int[] arr = {1,2,3};
int[] x = new int[arr.length];
backstrace(arr,0,arr.length,x);
}
private static void backstrace(int[] arr, int i, int length, int[] x) {
if(i == length){
for (int j = 0; j < arr.length; j++) {
if (x[j] == 1) {//打印左树枝
System.out.print (arr[j] + " ");
}
}
System.out.println ();
}else {
//访问i节点的左孩子
x[i] = 1;
backstrace (arr,i+1,length, x);
//访问i节点的右孩子
x[i] = 0;
backstrace (arr,i+1,length, x);
}
}
}
我们使用x记录遍历过程中是遍历二叉树的左孩子还是右孩子,(1标记左孩子,0标记右孩子)根据x数组中存储的标记进行打印
运行结果:
1 2 3
1 2
1 3
1
2 3
2
3
结果显示为123三个数组成数组的所有子集(空子集无法显示),因此我们将这种树成为子集树
用子集树解决的问题
1.有一组整数,请选择一部分整数,让选择的,和剩下的整数,他们的和的差最小
/**
* 描述:有一组整数,请选择一部分整数,让选择的,和剩下的整数,他们的和的差最小
*/
public class ChildTree01 {
static int[] arr = {1,2,3,4,5,6};
static int[] x = new int[arr.length];//以选择的数
static int[] bestx = new int[arr.length];//标记数组
static int r = 0; // 没有选择的整数的和
static int min = Integer.MAX_VALUE;//记录最小差值
public static void main(String[] args) {
for (int i = 0; i < arr.length; i++) {
r += arr[i];
}
backstrace(arr, 0);
System.out.println("min:" + min);
System.out.println(Arrays.toString(bestx));
}
private static void backstrace(int[] arr, int i) {
if(i == arr.length){
int sum = 0;//记录已选整数的和
for (int j = 0; j < arr.length; j++) {
if(x[j] == 1){
sum += arr[j];
}
}
// sum
int ret = Math.abs(sum - r);//记录当前已选与未选整数和差的绝对值
if(ret < min){
min = ret;
//标记数组,选择标为1不选标为0
for (int j = 0; j < x.length; j++) {
bestx[j] = x[j];
}
}
} else {
r -= arr[i];//当前元素被选用,从未选用整数和中减去
x[i] = 1;
backstrace (arr, i + 1);//访问i节点的左孩子(选择)
r += arr[i];//当前元素选用,从未选用整数和中加上
x[i] = 0;
backstrace (arr, i + 1);//访问i节点的右孩子(不选择)
}
}
}
结果:
min:1
[1, 1, 1, 1, 0, 0]
2.解决0-1背包问题
描述:有一组物品,其重量分别为:w1,w2....,wn;其价值分别为:v1,v2....,vn;现在有一个容量为c的背包,问怎么装入物品,才能使背包的价值最大化
对于有n种可选物品的0/1背包问题,其解空间由长度为n的0-1向量组成,可用子集数表示。在搜索解空间树时,只要其左儿子结点是一个可行结点,搜索就进入左子树;当右子树中有可能包含最优解时就进入右子树搜索。再加入适当的剪枝操作,提高代码的效率
import java.util.Arrays;
/**
* 描述:
* 有一组物品,起重量分别为:
* w1,w2...,wn
* 其价值分别为:
* v1,v2....,vn
* 现在有一个容量为c的背包,问怎么装入物品,才能使背包的价值最大化
*/
public class childTreePackage {
static int[] w = {5,7,3,9,6,12};
static int[] v = {12,9,13,10,11,14};
static int c = 24;//背包容量
static int[] x = new int[w.length];//标记数组
static int[] bestx = new int[w.length];//最优解数组
static int cw = 0; // 已选择物品的重量
static int cv = 0; // 已选择物品的价值
static int bestv = Integer.MIN_VALUE; // 物品的最优价值
static int r = 0;//记录的是当前物品后面剩下的物品的总价值
public static void main(String[] args) {
for (int i = 0; i < v.length; i++) {
r += v[i];//先让r记录所有价值
}
backstrace(0);
System.out.println("bestv:" + bestv);
System.out.println(Arrays.toString(bestx));
}
private static void backstrace(int i) {
if(i == w.length){
if(cv > bestv){
bestv = cv; // 更新最优价值
for (int j = 0; j < x.length; j++) {
bestx[j] = x[j]; // 更新最优价值选择的物品的子集
}
}
} else {
r -= v[i];
if(cw + w[i] <= c){//当前重量符合要求(左子树剪枝)
cv += v[i];
cw += w[i];
x[i] = 1;
backstrace(i+1); // i节点的左孩子
cw -= w[i];
cv -= v[i];
}
// 当前已选择物品的总价值比最优价值大才有必要去右子树相当于剪枝
if(cv + r > bestv){ // r此时不包含物品i的价值
x[i] = 0;
backstrace(i+1); // i节点的右孩子
}
r += v[i];
}
}
}
3.轮船装载问题
描述:有一组物品,其重量分别是:w1,w2...wn,现在有两艘轮船,其容量分别是C1和C2,满足w1+w2+...+wn <= C1 + C2 问怎么装载物品,才能够把物品全部装上轮船
分析:装载问题,实际还是一个0-1背包的问题,题目中已经说了所有物品重量之和不超过轮船的总容量,因此只需要找出一艘轮船的的最优装载,剩下的物品直接装入第二艘轮船就可以了,代码如下:
public class childTree {
static int[] w = {12, 18, 21, 14, 9, 10};//物品的各自质量
static int c1 = 51;//c1轮船载重
static int c2 = 33;//c2轮船载重
static int[] x = new int[w.length];//标记数组
static int[] bestx = new int[w.length];//最优解子集
static int bestw = Integer.MIN_VALUE;//记录最优质量
static int cw = 0;//以选择物品
static int r = 0; //记录物品i后面未选择所有物品的总重量
private static void strace(int i) {
if (i == w.length) {
if (cw > bestw) {
bestw = cw;// 更新选择的物品
for (int j = 0; j < x.length; j++) {
bestx[j] = x[j]; // 更新最优重量c1选择的物品的子集
}
}
} else {
if (cw + w[i] <= c1) {
r -= w[i];
cw += w[i];
x[i] = 1;
strace (i + 1); //选择第i个节点
cw -= w[i];
}
if (cw + r > bestw) {
x[i] = 0; //满足条件才有必要去往右边的i节点
strace (i + 1); //没选择第i个节点
}
r += w[i];
}
}
public static void main(String[] args) {
for (int i = 0; i < w.length; i++) {
r += w[i];
}
strace (0);
int sum = 0;
//sum记录剩下的,即装入c2中的物品总重量
for (int i = 0; i < bestx.length; i++) {
if (bestx[i] == 0) {
sum += w[i];
}
}
if (sum > c2) {
System.out.println ("物品无法装载到轮船!");
return;
}
System.out.println ("C1:" + c1 + "装载物品:");
for (int i = 0; i < bestx.length; i++) {
if (bestx[i] == 1) {
System.out.print (w[i] + " ");
}
}
System.out.println ();
System.out.println ("C2:" + c2 + "装载物品:");
for (int i = 0; i < bestx.length; i++) {
if (bestx[i] == 0) {
System.out.print (w[i] + " ");
}
}
System.out.println ();
}
}
运行结果:
C1:51装载物品:
12 18 21
C2:33装载物品:
14 9 10