公平分发饼干[标准回溯+剪枝 || 二进制枚举集合法+二维消耗动规 + (状压--优化)]

359 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,公平分发饼干[标准回溯+剪枝 || 二进制枚举集合法+二维消耗动规 + (状压--优化)] - 掘金 (juejin.cn)

前言

集合划分问题,可以采用标准和回溯+剪枝来解决,也可用二进制枚举集合法来完成题解,体验二进制枚举子集合的魅力。

一、公平分发饼干

image.png

二、题解

1、回溯+剪枝

target:把cookies分为k组,要求分得非常平均,即最大值与最小值之差最小。

M1:可暴力DFS枚举+剪枝,每包饼干可分给任意一个孩子。每包即n包;任意一个孩子即组合Ck1,所以有k^n次方种可能。

技巧:可以从大包饼干开始分发,从而快速剪枝,让递归深度变浅。

剪枝1:当最大值已经大于min时,就不用再分了。

剪枝2:当饼干包数小于还没分发的孩子数时,也不用再分了。

剪枝3:第一包饼干分谁都行,就分一个就可以了,分给bucket[0],其它就不分了。

public class DistributeCookies {
    /*
    target:把cookies分为k组,要求分得非常平均,即最大值与最小值之差最小。
    M1:可暴力DFS枚举+剪枝,每包饼干可分给任意一个孩子。每包即n包;任意一个孩子即组合Ck1,所以有k^n次方种可能。
     */
    public int distributeCookies(int[] cookies, int k) {
        // 先发饼干较多的包,以便快速剪枝。
        Arrays.sort(cookies);
        dfs(new int[k], cookies.length - 1, cookies);
        return min;
    }

    int min = 1 << 30;

    private void dfs(int[] bucket, int cur, int[] cookies) {
        int k = bucket.length, n = cookies.length;
        // 递归终止条件,饼干分完了。
        if (cur == -1) {
            // 分配完毕,计算最大值。
            int max = 1 << 31;
            for (int i : bucket) max = Math.max(max, i);
            min = Math.min(max, min);
            return;
        }
        // 剪枝1:当最大值已经大于min时,就不用再分了。
        for (int i : bucket) if (i >= min) return;
        // 剪枝2:当饼干包数小于还没分发的孩子数时,也不用再分了。
        int cnt = 0;
        for (int i : bucket) cnt += i == 0 ? 1 : 0;
        if (cnt > cur + 1) return;
        // 标准回溯
        for (int i = 0; i < k; i++) {
            // 剪枝3:第一包饼干分谁都行,就分一个就可以了,分给bucket[0],其它就不分了。
            if (cur == n - 1 && i > 0) return;
            //开始暴力搜索
            bucket[i] += cookies[cur];
            dfs(bucket, cur - 1, cookies);
            bucket[i] -= cookies[cur];
        }
    }

}

2、二进制枚举+动规

target:把cookies分为k组,可以先分成k - 1组,求得k - 1组对cookies所有子集划分成k-1组的最小不公平程度。

状态f[i][j]定义:二进制表示集合j分成i组所有分发的最小值。

即f[i][j] = min(f[i][j],max(f[i-1][j^s],sum[s])),注:初始化f[i][j] = 1 << 30,要去min,不能用默认值0.

注:二进制枚举集合法:一个二进制,第i位为1表示取第cookies中第i个值放入集合。

// 二进制枚举集合 + 二维动规消费法 + 状压(优化)
class DistributeCookies2 {
    /*
    target:把cookies分为k组,可以先分成k - 1组,求得k - 1组对cookies所有子集划分成k-1组的最小不公平程度。
    状态f[i][j]定义:二进制表示集合j分成i组所有分发的最小值。
    即f[i][j] = min(f[i][j],max(f[i-1][j^s],sum[s])),注:初始化f[i][j] = 1 << 30,要去min,不能用默认值0.
    注:二进制枚举集合法:一个二进制,第i位为1表示取第cookies中第i个值放入集合。
     */
    public int distributeCookies(int[] cookies, int k) {
        int n = cookies.length;
        int[] sum = new int[1 << n];
        // 为sum赋值,sum有1<<n位,可以通过动规来求,确定最高位i,加上以前求到的sum[所有低位和]
        for (int i = 0; i < n; i++) {//确定最高位i
            for (int j = 0; j < 1 << i; j++) {//低位,即不超过1 << i,和为sum[j]
                sum[(1 << i) | j] = sum[j] + cookies[i];//高低位合并
            }
        }
        // 动规。
        int[][] f = new int[k][];
        f[0] = Arrays.copyOfRange(sum, 0, 1 << n);
        for (int i = 1; i < k; i++) {
            f[i] = new int[1 << n];
            for (int j = 0; j < 1 << n; j++) {
                //枚举j的所有子集
                f[i][j] = 1 << 30;
                for (int s = j; s > 0; s = (s - 1) & j) {
                    f[i][j] = Math.min(f[i][j], Math.max(f[i - 1][j ^ s], sum[s]));
                }
            }
        }
        return f[k - 1][(1 << n) - 1];
    }

    // 状态压缩,f[i][j] 只和 f[i-1][j之前的集合有关],可一维 + 倒序枚举。
    public int distributeCookies2(int[] cookies, int k) {
        int n = cookies.length;
        int[] sum = new int[1 << n];
        // 为sum赋值,sum有1<<n位,可以通过动规来求,确定最高位i,加上以前求到的sum[所有低位和]
        for (int i = 0; i < n; i++) {//确定最高位i
            for (int j = 0; j < 1 << i; j++) {//低位,即不超过1 << i,和为sum[j]
                sum[(1 << i) | j] = sum[j] + cookies[i];//高低位合并
            }
        }
        // 动规。
        int[] f = Arrays.copyOfRange(sum, 0, 1 << n);
        for (int i = 1; i < k; i++) {
            for (int j = (1 << n) - 1; j >= 0; j--) {
                //枚举j的所有子集
                for (int s = j; s > 0; s = (s - 1) & j) {
                    f[j] = Math.min(f[j], Math.max(f[j ^ s], sum[s]));
                }
            }
        }
        return f[(1 << n) - 1];
    }
}

总结

1)回溯+剪枝经典操作。

2)二进制枚举+动态规划。

参考文献

[1] LeetCode 公平分发饼干