知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!
文章目录
子集树
解空间就是问题所有解的可能取值构成的空间,一个问题的解往往包含了得到这个解的每一步,就是对应解空间树中一条从根节点到叶子节点的路径。
子集树就代表了一类问题的解空间,它并不是真实存在的数据结构,也就是说并不是真的有一颗这样的树,只是抽象出来的解的空间树。
当问题求解的结果是集合S的某一子集的时候,其对应的解空间就是一颗子集树,时间复杂度是 O ( 2 n ) O(2^n) O(2n),看下面的这段代码:
@Test
public void test01(){
int[] arr = {1,2,3};
backstrace01(arr, 0, arr.length);
}
private void backstrace01(int[] arr, int i, int length) {
if(i == length){
System.out.println("hello world!");
} else {
backstrace01(arr, i+1, length);
backstrace01(arr, i+1, length);
}
}
上面的代码运行以后,会输出多少次hello world!呢?上面的backstrace01函数的递归调用就是一个二叉树的遍历过程,如果给节点的左树枝标识1,右树枝标识0,那么这颗树就称作子集树,看下面的代码和图示:
@Test
public void test01(){
int[] arr = {1,2,3};
int[] brr = new int[arr.length];
backstrace02(arr, brr, 0, arr.length);
}
private void backstrace02(int[] arr, int[] brr, int i, int length) {
if(i == length){
for (int j=0; j<length; ++j){
if(brr[j] == 1){
System.out.print(arr[j] + " ");
}
}
System.out.println();
} else {
brr[i] = 1;
backstrace02(arr, brr, i+1, length);
brr[i] = 0;
backstrace02(arr, brr, i+1, length);
}
}
上面代码中用brr做了辅助数组,来记录遍历过程中,是遍历子集树节点的左孩子还是右孩子,如图:
运行上面的代码,可以看出来,打印出来了序列{1,2,3}的所有的子集情况,也就是从根节点到某一个叶子节点的路径就代表了问题的一个解,可以根据上面的图梳理出所有子集结果。
一组整数序列,选择其中的一部分整数,让选择的整数和序列中剩下的整数的和的差值最小
这个问题是笔试面试中经常出现的,是典型的求子集的问题,可以用子集树完美解决,代码如下:
// 原始的整形数组序列
static int[] arr = {12,32,8,15,26,7,6,258};
// 定义一个获取子集的辅助数组
static int[] brr = new int[arr.length];
// 存储到目前位置,最合适的子集数组
static int[] bestx = new int[arr.length];
// 记录剩下的整数的和
static int r = 0;
// 记录已选择的整数的和
static int cv = 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, brr, 0, arr.length);
System.out.println("min:" + min);
for (int i = 0; i < bestx.length; i++) {
if(bestx[i] == 1){
System.out.print(arr[i] + " ");
}
}
System.out.println();
}
private static void backstrace(int[] arr, int[] brr, int i, int length) {
if(i == length){
int ret = Math.abs(cv - r);
if(ret < min){
min = ret;
// brr帮你携带了当前的子集, bestx
for (int j = 0; j < brr.length; j++) {
bestx[j] = brr[j];
}
}
} else {
r -= arr[i];
cv += arr[i];
brr[i] = 1;
backstrace(arr, brr, i+1, length); // 表示选择了左孩子节点
r += arr[i];
cv -= arr[i];
brr[i] = 0;
backstrace(arr, brr, i+1, length); // 标识不选择左孩子节点
}
}
完没解决,看下面这个相似的问题。
一组2n个整数序列,选择其中n个整数,和序列中剩下的n个整数的和的差值最小
这个问题和上面的问题几乎一样,就是对选择的子集的个数做了限制,代码解决如下:
// 原始的整形数组序列
static int[] arr = {12,32,8,15,26,7,6,258};
// 定义一个获取子集的辅助数组
static int[] brr = new int[arr.length];
// 存储到目前位置,最合适的子集数组
static int[] bestx = new int[arr.length];
// 记录剩下的整数的和
static int r = 0;
// 记录已选择的整数的和
static int cv = 0;
static int min = Integer.MAX_VALUE;
static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < arr.length; i++) {
r += arr[i];
}
backstrace(arr, brr, 0, arr.length);
System.out.println("min:" + min);
for (int i = 0; i < bestx.length; i++) {
if(bestx[i] == 1){
System.out.print(arr[i] + " ");
}
}
System.out.println();
}
private static void backstrace(int[] arr, int[] brr, int i, int length) {
if(i == length){
if(count != length/2){
return;
}
int ret = Math.abs(cv - r);
if(ret < min){
min = ret;
// brr帮你携带了当前的子集, bestx
for (int j = 0; j < brr.length; j++) {
bestx[j] = brr[j];
}
}
} else {
if(count < length/2){ // 适当的对子集树的遍历进行剪枝操作
count++;
r -= arr[i];
cv += arr[i];
brr[i] = 1;
backstrace(arr, brr, i+1, length); // 表示选择了左孩子节点
count--;
r += arr[i];
cv -= arr[i];
}
brr[i] = 0;
backstrace(arr, brr, i+1, length);
}
}
上面代码添加了一个count变量,用来控制子集元素的个数选取;另外注意在子集树遍历代码中适当添加剪枝操作,可以减少遍历不必要的树枝,提高子集树的遍历效率。
解决0-1背包问题
0-1背包的问题描述是这样的 ,假设有n个物品,它们的重量分别是W1, W2, W3… Wn,它们的价值分别是V1, V2, V3… Vn,有一个背包,其容量限制是C,问怎么样装入物品,能使背包的价值最大化。
0-1背包可以用动态规划来解决,是一种空间换时间的方法,肯定提高了算法效率。但是这个问题的结果也是原物品的一个子集,用子集树遍历来解决,然后添加适当的剪枝操作,提高子集的遍历效率,代码如下:
// 所有商品的价值
static int[] v = {12,4,60,8,13};
// 所有商品的重量
static int[] w = {8,6,9,4,7};
// 定义背包的总容量
static int c = 25;
// 记录当前子集的辅助数组
static int[] x = new int[v.length];
// 记录当前最优解的子集
static int[] bestx = new int[v.length];
// 已选择商品的总重量
static int cw = 0;
// 已选择商品的总价值
static int cv = 0;
// 当前选择物品价值的最优解
static int bestv = 0;
// 剩余物品的价值总和
static int r = 0;
public static void main(String[] args) {
for (int i = 0; i < v.length; i++) {
r += v[i];
}
backStace(v, w, 0, v.length);
System.out.println("bestv:" + bestv);
System.out.println("bestx:" + Arrays.toString(bestx));
}
private static void backStace(int[] v, int[] w, int i, int length) {
if(i == length){
// 添加了剪操作以后,应该只有价值更大的商品选择才会遍历到
System.out.println("cv:" + cv);
// 判断取得当前价值最高的商品
if(cv > bestv){
bestv = cv;
for (int j = 0; j < length; j++) {
bestx[j] = x[j];
}
}
} else {
r -= v[i];
if(cw + w[i] <= c){ // 子集树的剪枝操作
cw += w[i];
cv += v[i];
x[i] = 1;
backStace(v, w, i+1, length);
cw -= w[i];
cv -= v[i];
}
if(cv + r > bestv){ // 子集树的剪枝操作
x[i] = 0;
backStace(v, w, i+1, length);
}
r += v[i];
}
}
从一组整数数组中选择n个元素,让其和等于指定的值
这个面试问题,也是典型的一个可以利用回溯子集树解决的问题,代码如下:
//数组序列
static int[] arr = {12,45,8,91,36,79,83,52,31};
// 辅助数组
static int[] x = new int[arr.length];
// 记录选择的数字的和
static int cv = 0;
// 记录剩下的数组元素的和
static int r = 0;
// 记录数组元素的总和
static int sum = 234;
public static void main(String[] args) {
for (int i = 0; i < arr.length; i++) {
r += arr[i];
}
backstrace(arr, x, 0, arr.length);
}
private static void backstrace(int[] arr, int[] x, int i, int length) {
if(i == length){
if(cv == sum){
for (int j = 0; j < x.length; j++) {
if(x[j] == 1){
System.out.print(arr[j] + " ");
}
}
System.out.println();
}
} else {
r -= arr[i];
if(cv + arr[i] <= sum){ // 剪枝
cv += arr[i];
x[i] = 1;
backstrace(arr, x, i+1, length);
cv -= arr[i];
}
if(cv + r >= sum){ // 剪枝
x[i] = 0;
backstrace(arr, x, i+1, length);
}
r += arr[i];
}
}
装载问题
有一批共n个集装箱要装上2艘载重量分别是c1和c2的轮船,其中集装箱i的重量为wi,且满足
∑
i
=
1
n
w
i
<
=
c
1
+
c
2
\sum_{i=1}^{n}wi<=c1+c2
∑i=1nwi<=c1+c2 ,是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。
实际上这个装载问题,本质上还是一个0-1背包的问题,题目中已经说了所有集装箱的重量之和不会超过轮船的总容量,因此只需要找出一艘轮船的的最优装载就可以了,剩下的集装箱直接装入第二艘轮船就可以了,代码如下:
// 集装箱的重量
static int[] w = {10,8,12,5,15,20};
// 轮船1的容量
static int c1 = 50;
// 轮船2的容量
static int c2 = 20;
// 记录当前子集的辅助数组
static int[] x = new int[w.length];
// 记录当前最优解的子集
static int[] bestx = new int[w.length];
// 已选择集装箱的总重量
static int cw = 0;
// 当前已选择的集装箱重量的最优解
static int bestw = 0;
// 剩余没选择的集装箱的重量总和
static int r = 0;
private static void backStace(int[] w, int i, int length) {
if(i == length){
// 更新轮船c1能装入的集装箱子集的最优解
if(cw > bestw){
bestw = cw;
for (int j = 0; j < length; j++) {
bestx[j] = x[j];
}
}
} else {
r -= w[i];
if(cw + w[i] <= c1){ // 子集树的剪枝操作
cw += w[i];
x[i] = 1;
backStace(w, i+1, length);
cw -= w[i];
}
if(cw + r > bestw){ // 子集树的剪枝操作
x[i] = 0;
backStace(w, i+1, length);
}
r += w[i];
}
}
public static void main(String[] args) {
// 初始化r为所有集装箱的总重量
for (int i = 0; i < w.length; i++) {
r += w[i];
}
backStace(w, 0, w.length);
// 如果w1+w2+...+wn和c1+c2比较接近,有可能不存在合适的装载方案
int sum = 0;
for (int i = 0; i < bestx.length; i++) {
if(bestx[i] == 0){
sum += w[i];
}
}
if(sum > c2){
System.out.println("没有合适的装载方案!");
return;
}
// 输出轮船1的装载方案
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();
// 输出轮船2的装载方案
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();
}