Java版高级数据结构算法 - 回溯算法之子集树(解决常见面试问题)

知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!

子集树

解空间就是问题所有解的可能取值构成的空间,一个问题的解往往包含了得到这个解的每一步,就是对应解空间树中一条从根节点到叶子节点的路径。

子集树就代表了一类问题的解空间,它并不是真实存在的数据结构,也就是说并不是真的有一颗这样的树,只是抽象出来的解的空间树。

当问题求解的结果是集合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 &lt; = c 1 + c 2 \sum_{i=1}^{n}wi&lt;=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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值