持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,公平分发饼干[标准回溯+剪枝 || 二进制枚举集合法+二维消耗动规 + (状压--优化)] - 掘金 (juejin.cn)
前言
集合划分问题,可以采用标准和回溯+剪枝来解决,也可用二进制枚举集合法来完成题解,体验二进制枚举子集合的魅力。
一、公平分发饼干
二、题解
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 公平分发饼干