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

前言

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

一、公平分发饼干

在这里插入图片描述

二、题解

1、回溯+剪枝

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、二进制枚举+动规

// 二进制枚举集合 + 二维动规消费法 + 状压(优化)
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 公平分发饼干

附录–标准回溯

1、火柴拼正方形

在这里插入图片描述

package everyday;

import java.util.Arrays;

// 火柴拼正方形。
public class Makesquare {
    /*
    target:说白了就是将数组分成相等的4份,数字不可拆分。
    M1:暴力枚举+剪枝。
     */
    public boolean makesquare(int[] matchsticks) {
        // 如果不能取整,那么肯定不能分成4份相等长度的火柴组。
        int total = Arrays.stream(matchsticks).sum();
        if ((total & 0x3) != 0) return false;
        // 技巧:将数组排序,先枚举大的,方便后面快速超过平均值来剪掉一大份枝丫。
        Arrays.sort(matchsticks);
        // 枚举。
        dfs(new int[4], matchsticks.length - 1, matchsticks, total >>> 2);
        System.out.println(1);
        // 返回是否可以平均分为4份。
        return flag;
    }

    boolean flag = false;// 表示能否分成平均四份。

    private void dfs(int[] bucket, int cur, int[] matchsticks, int avg) {
        // 都剩一根火柴了,还需要分配,说明前面就没有avg = 3,直接返回。
        // bug1:都剩一根火柴还没avg=3是bug存在,因为是先判断avgCnt,再for循环使用matchsticks[i],所以avgCnt = 2
        // if (cur == 0) return;
        if (cur == -1) return;
        // 剪枝1:已经可以平均分了,就不用再枚举了。
        if (flag) return;
        // 剪枝2:当有的组火柴长度超过平均值avg了,则不用继续分配下去了。
        int max = 1 << 31, avgCnt = 0, zeroCnt = 0;
        for (int i : bucket) {
            max = Math.max(max, i);
            if (i == avg) avgCnt++;
            if (i == 0) zeroCnt++;
        }
        if (max > avg) return;
        // 剪枝3:当有3组火柴都分到平均值了,第4组不用分都是avg,毕竟total & 0x3 == 0.
        if (avgCnt == 3) {
            flag = true;
            return;
        }
        // 剪枝4:当火柴个数小于还未分配到的组数,则不用再分了。
        if (zeroCnt > cur + 1) return;
        // 标准回溯分发火柴。
        for (int i = 0; i < bucket.length; i++) {
            // 剪枝5:第1根火柴分给任意一组都行,为了将回溯根从4个减到0个,所以第一个火柴只分配给bucket[0]。
            if (cur == matchsticks.length - 1 && i > 0) return;
            // 回溯分发。
            bucket[i] += matchsticks[cur];
            dfs(bucket, cur - 1, matchsticks, avg);
            bucket[i] -= matchsticks[cur];
        }
    }

    public static void main(String[] args) {
        new Makesquare().makesquare(new int[]{5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3});
    }

}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值