贪心算法与集合划分问题详解
集合划分问题是组合优化中的经典问题,其核心目标是将元素集合划分为若干满足特定条件的子集。本文将深入探讨贪心算法在集合划分中的应用,涵盖算法原理、适用场景、Java实现细节及优化策略。
一、集合划分问题定义
1.1 基础概念
给定一个集合 S = {x₁, x₂, ..., xₙ}
,需要将其划分为 k
个子集 {S₁, S₂, ..., Sₖ}
,满足:
- 完备性:所有子集的并为原集合
- 互斥性:任意两个子集交集为空
- 特定约束:如子集和相等、子集大小相近等
1.2 常见变种问题
- 等和划分:将集合划分为两个子集,使得两者和相等(如LeetCode 416)
- 最小最大子集和:划分至k个子集,使最大子集和最小(如LeetCode 698)
- 平衡划分:使子集元素数量或属性差异最小
- 多维度划分:同时考虑多个属性(如重量+体积)
1.3 应用场景
- 服务器负载均衡
- 分布式文件存储
- 生产线任务调度
- 数据处理分片
二、贪心算法策略设计
2.1 基本贪心策略
核心思想:通过局部最优选择逐步逼近全局最优解
通用步骤:
- 排序预处理:按关键属性(如数值大小)排序
- 分配策略:依次将元素分配到当前最优子集
- 终止条件:所有元素分配完成
2.2 典型分配策略
问题类型 | 排序方式 | 分配策略 |
---|---|---|
等和划分 | 降序排序 | 优先填充大元素 |
最小最大子集和 | 降序排序 | 当前总和最小的子集优先 |
平衡数量划分 | 无需排序 | 轮询分配 |
2.3 正确性分析
- 等和划分:当总和为偶数且无超大元素时有效
- 最小最大和:提供近似解,近似比通常为2
- NP-Hard证明:多数划分问题属于NP-Hard,贪心提供可行近似解
三、等和划分问题详解
3.1 问题定义
给定非空数组 nums
,判断是否能将其划分为两个子集,使得两个子集的和相等。
3.2 贪心算法实现
public class BalancedPartition {
// 辅助类:记录子集状态
static class Subset {
int sum = 0;
List<Integer> elements = new ArrayList<>();
}
public static boolean canPartition(int[] nums) {
int total = Arrays.stream(nums).sum();
if (total % 2 != 0) return false;
int target = total / 2;
Arrays.sort(nums); // 升序排序
reverse(nums); // 自定义降序
Subset[] subsets = new Subset[2];
subsets[0] = new Subset();
subsets[1] = new Subset();
for (int num : nums) {
// 选择当前总和较小的子集
int idx = (subsets[0].sum <= subsets[1].sum) ? 0 : 1;
if (subsets[idx].sum + num > target) {
// 无法放入则尝试另一个子集
idx = 1 - idx;
if (subsets[idx].sum + num > target) return false;
}
subsets[idx].sum += num;
subsets[idx].elements.add(num);
}
return subsets[0].sum == subsets[1].sum;
}
private static void reverse(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int temp = arr[left];
arr[left++] = arr[right];
arr[right--] = temp;
}
}
public static void main(String[] args) {
int[] nums1 = {1, 5, 11, 5};
System.out.println(canPartition(nums1)); // true
int[] nums2 = {1, 2, 3, 5};
System.out.println(canPartition(nums2)); // false
}
}
3.3 算法分析
- 时间复杂度:O(n log n)(排序耗时)
- 空间复杂度:O(n)(存储子集信息)
- 局限性:无法处理存在单个元素超过总和一半的情况
四、最小最大子集和问题
4.1 问题定义
给定数组 nums
和整数 k
,将其划分为 k
个连续非空子集,使得最大子集和最小。
4.2 贪心策略实现
public class MinMaxSubsetSum {
public static int minMaxSum(int[] nums, int k) {
Arrays.sort(nums);
reverse(nums);
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int i = 0; i < k; i++) minHeap.offer(0);
for (int num : nums) {
int curr = minHeap.poll();
curr += num;
minHeap.offer(curr);
}
int max = Integer.MIN_VALUE;
while (!minHeap.isEmpty()) max = Math.max(max, minHeap.poll());
return max;
}
private static void reverse(int[] arr) {
// 同前文实现
}
public static void main(String[] args) {
int[] nums = {7, 2, 5, 10, 8};
System.out.println(minMaxSum(nums, 2)); // 输出18([7,2,5]和[10,8])
}
}
4.3 关键逻辑解析
- 降序排序:优先处理大元素
- 最小堆维护子集和:每次选择当前和最小的子集
- 近似比证明:该策略结果不超过最优解的2倍
五、多维约束划分问题
5.1 问题描述
考虑元素的多个属性(如重量、体积、价值),需同时满足多个约束条件。
5.2 装箱问题变种
class Item {
int weight;
int volume;
public Item(int w, int v) {
weight = w;
volume = v;
}
}
public class MultiDimBinPacking {
public static int minBins(Item[] items, int maxWeight, int maxVolume) {
Arrays.sort(items, (a, b) ->
Integer.compare(b.weight + b.volume, a.weight + a.volume));
List<Bin> bins = new ArrayList<>();
for (Item item : items) {
boolean placed = false;
// 尝试放入已有箱子
for (Bin bin : bins) {
if (bin.canAdd(item, maxWeight, maxVolume)) {
bin.addItem(item);
placed = true;
break;
}
}
// 创建新箱子
if (!placed) {
Bin newBin = new Bin();
newBin.addItem(item);
bins.add(newBin);
}
}
return bins.size();
}
static class Bin {
int currentWeight = 0;
int currentVolume = 0;
boolean canAdd(Item item, int maxW, int maxV) {
return currentWeight + item.weight <= maxW
&& currentVolume + item.volume <= maxV;
}
void addItem(Item item) {
currentWeight += item.weight;
currentVolume += item.volume;
}
}
}
5.3 策略分析
- 复合排序:根据权重和体积的综合指标排序
- 首次适应策略:遍历现有容器尝试放置
- 复杂度:O(n²) 时间复杂度,适用于中小规模数据
六、性能优化技巧
6.1 数据结构优化
使用TreeSet加速查找:
TreeSet<Bin> bins = new TreeSet<>(Comparator
.comparingInt((Bin b) -> b.currentWeight)
.thenComparingInt(b -> b.currentVolume));
// 查找可放置的bin
Bin candidate = bins.floor(searchKey);
6.2 并行处理
利用Java Stream API并行化:
Arrays.stream(items)
.parallel()
.sorted(comparator)
.forEach(item -> {
// 分配逻辑
});
6.3 缓存优化
预处理常用计算:
int[] prefixSum = new int[nums.length + 1];
for (int i=0; i<nums.length; i++) {
prefixSum[i+1] = prefixSum[i] + nums[i];
}
七、正确性证明与反例分析
7.1 等和划分反例
输入:[3, 3, 3, 3]
贪心输出:[[3,3], [3,3]]
(正确)
输入:[4, 4, 4, 6]
贪心失败:需要动态规划
7.2 最小最大和证明
- 最大元素必属于某个子集
- 贪心结果
G ≤ 2 * OPT
- 实例:
[9,8,7,6,5,4,3,2,1]
, k=3
贪心解:19,最优解:17
八、测试用例设计
8.1 常规测试
// 等和划分测试
@Test
void testBalancedPartition() {
assertTrue(canPartition(new int[]{1,5,11,5}));
assertFalse(canPartition(new int[]{1,2,3,5}));
}
// 最小最大和测试
@Test
void testMinMaxSum() {
assertEquals(18, minMaxSum(new int[]{7,2,5,10,8}, 2));
}
8.2 边界测试
// 单个元素测试
@Test
void testSingleElement() {
assertFalse(canPartition(new int[]{5}));
}
// 空输入测试
@Test
void testEmptyInput() {
assertEquals(0, minMaxSum(new int[]{}, 0));
}
8.3 性能测试
// 生成10^5个元素的大规模测试
int[] bigData = new int[100000];
Arrays.fill(bigData, 1);
long start = System.currentTimeMillis();
assertTrue(canPartition(bigData));
System.out.println("Time cost: " + (System.currentTimeMillis()-start) + "ms");
九、实际应用案例
9.1 云计算资源分配
- 需求:将虚拟机实例分配到物理机,最小化使用主机数量
- 策略:
- 按虚拟机资源需求(CPU+内存)降序排序
- 使用首次适应递减算法分配
9.2 物流装箱优化
- 需求:装车时同时考虑货物重量和体积
- 实现:
public class CargoOptimizer { // 类似多维划分实现 }
9.3 分布式计算
- 场景:将大数据作业分片到计算节点
- 优化:根据节点处理能力动态调整划分策略
十、总结
10.1 算法选择指南
问题类型 | 推荐算法 | 时间复杂度 | 适用场景 |
---|---|---|---|
小规模精确划分 | 动态规划 | O(n*sum) | 元素较少 |
大规模近似划分 | 贪心算法 | O(n log n) | 实时性要求高 |
多约束复杂划分 | 元启发式算法 | - | 复杂工业场景 |