
在数据分析、实验设计或样本分配等场景中,我们经常需要将一个包含m个元素的原始数据集(超集)划分为n个互不重叠、且大小预定的子集。一个常见的优化目标是使每个子集的统计特性(例如均值)尽可能地与原始超集的特性保持一致。具体来说,给定一个超集 s 及其包含的 m 个元素,以及 n 个预期的子集大小 x0, x1, ..., xn-1(其中 sum(xi) == m),目标是创建这些子集,使得每个子集的均值与超集的均值最为接近。我们通常通过最小化所有子集均值与超集均值之间绝对差异的总和来量化这一目标。
这是一个典型的组合优化问题,其挑战在于:
这种类型的分配问题可以被归类为“集合划分问题”(Set Partitioning Problem)的一个变种,其中加入了特定的目标函数(均值优化)和额外的约束。混合整数线性规划(MILP)提供了一种强大的框架来精确解决这类问题。
我们的目标是使每个子集的均值 mean(subset_s) 尽可能接近超集的均值 mean(superset)。这等价于使每个子集的总和 sum(subset_s) 尽可能接近 subset_size_s * mean(superset)。因此,我们可以定义目标函数为最小化所有子集总和与目标总和之间绝对差异的总和:
$$ \min \sum{s=0}^{N-1} | \sum{i \in \text{subset}_s} \text{element}_i - (\text{size}_s \times \text{mean}(\text{superset})) | $$
我们引入二元决策变量 x_s_i: x_s_i = 1 如果超集中的第 i 个元素被分配到第 s 个子集中。 x_s_i = 0 否则。
PuLP 是一个用 Python 编写的线性规划建模工具,它允许用户以直观的方式定义优化问题,并调用各种求解器(如CBC、GLPK、Gurobi等)来解决。
以下是一个使用 PuLP 解决上述问题的示例代码:
from statistics import mean
import pulp
def solve_subset_partitioning(superset_elements, subset_sizes):
"""
使用 PuLP 解决基于均值优化的数据集子集划分问题。
Args:
superset_elements (list): 超集中的所有元素列表。
subset_sizes (list): N个子集各自的目标大小列表。
Returns:
tuple: (list of lists, list of floats) 分割后的子集元素列表和每个子集的均值。
"""
N = len(subset_sizes)
M = len(superset_elements)
# 验证输入
if sum(subset_sizes) != M:
raise ValueError("所有子集大小之和必须等于超集元素总数。")
# 计算超集均值
superset_mean = mean(superset_elements)
# 创建 PuLP 问题实例
set_partitioning_model = pulp.LpProblem("Set_Partitioning_Model", pulp.LpMinimize)
# 决策变量:x_s_i = 1 如果超集中的第 i 个元素被分配到第 s 个子集中
# covering[s] 是一个列表,其中包含子集 s 的 M 个二元变量
covering = {}
for s in range(N):
vals = []
for i, v in enumerate(superset_elements):
vals.append(
pulp.LpVariable(
f"x_set_{s}_element_idx_{i:>02}_val_{v}",
lowBound=0, # 0
upBound=1, # 1
cat=pulp.LpBinary, # 二进制变量
)
)
covering[s] = vals
# 辅助变量:用于处理绝对误差
abs_sum_errs = []
for s_i in range(N):
abs_sum_errs.append(pulp.LpVariable(f"set_{s_i}_sum_error_abs", lowBound=0))
# 目标函数:最小化所有子集绝对误差之和
set_partitioning_model += pulp.lpSum(abs_sum_errs), "Minimize_Absolute_Sum_Errors"
# 添加约束
for s_i, st_vars in covering.items():
# 计算每个子集的实际总和
current_set_sum = pulp.lpSum([p * superset_elements[i] for i, p in enumerate(st_vars)])
# 计算每个子集的目标总和 (子集大小 * 超集均值)
target_set_sum = subset_sizes[s_i] * superset_mean
# 定义子集总和误差变量
set_sum_err = pulp.LpVariable(f"set_{s_i}_sum_error")
set_partitioning_model += set_sum_err == current_set_sum - target_set_sum, f"Set_{s_i}_Sum_Error_Definition"
# 绝对值线性化约束
set_partitioning_model += abs_sum_errs[s_i] >= set_sum_err, f"Abs_Error_Upper_Bound_Pos_{s_i}"
set_partitioning_model += abs_sum_errs[s_i] >= -set_sum_err, f"Abs_Error_Upper_Bound_Neg_{s_i}"
# 约束1: 每个子集的大小必须符合预设
for s_i, st_vars in enumerate(covering.values()):
set_partitioning_model += pulp.lpSum(st_vars) == subset_sizes[s_i], f"Set_{s_i}_Size_Constraint"
# 约束2: 超集中的每个元素只能被使用一次
# zip(*covering.values()) 将所有子集的变量列表转置,以便按元素索引迭代
for i, element_vars in enumerate(zip(*covering.values())):
set_partitioning_model += (
pulp.lpSum(element_vars) == 1,
f"Element_{i}_Used_Once_Constraint",
)
# 求解模型
set_partitioning_model.solve(pulp.PULP_CBC_CMD(msg=False)) # 使用默认的CBC求解器,静默模式
# 提取结果
if pulp.LpStatus[set_partitioning_model.status] == "Optimal":
result_subsets = []
result_means = []
for s_i, st_vars in covering.items():
current_subset_elements = [
superset_elements[i] for i, var in enumerate(st_vars) if var.value() == 1
]
result_subsets.append(current_subset_elements)
result_means.append(mean(current_subset_elements))
return result_subsets, result_means
else:
print(f"未能找到最优解。状态: {pulp.LpStatus[set_partitioning_model.status]}")
return [], []
# 示例 1:完美分配
print("--- 示例 1:完美分配 ---")
superset1 = [100]*5 + [101]*10 + [102]*5
subset_sizes1 = [2, 4, 14]
subsets1, means1 = solve_subset_partitioning(superset1, subset_sizes1)
print(f"超集均值: {mean(superset1)}")
for i, (subset, mean_val) in enumerate(zip(subsets1, means1)):
print(f"子集 {chr(65+i)} ({len(subset)} 元素): {subset}, 均值: {mean_val}")
# 预期输出:所有子集均值均为 101
# 示例 2:最佳拟合
print("\n--- 示例 2:最佳拟合 ---")
superset2 = [100]*5 + [103]*10 + [104]*5
subset_sizes2 = [2, 4, 14]
subsets2, means2 = solve_subset_partitioning(superset2, subset_sizes2)
print(f"超集均值: {mean(superset2)}")
for i, (subset, mean_val) in enumerate(zip(subsets2, means2)):
print(f"子集 {chr(65+i)} ({len(subset)} 元素): {subset}, 均值: {mean_val}")
# 预期输出:子集均值尽可能接近 102.5代码解析:
当精确求解(如MILP)因问题规模过大而变得不可行时,启发式算法提供了一种快速获得近似解的方法。Karmarkar-Karp 算法(也称为最大差分法)是解决数集划分问题的一种著名启发式算法,其目标是将一个数集划分为 k 个子集,使这些子集的和尽可能接近,即最小化最大子集和与最小子集和之间的差异。
优点: 速度快,易于实现。 局限性:
因此,Karmarkar-Karp 算法不适用于严格满足本教程最初提出的所有约束条件。然而,作为一种通用的数集划分启发式方法,它在某些场景下仍有其价值,例如当我们只需要大致平衡子集总和,而对子集大小没有严格要求时。
以下是一个使用 numberpartitioning 库实现 Karmarkar-Karp 算法的示例:
from statistics import mean
from numberpartitioning import karmarkar_karp
# 示例 2 的超集数据
superset = [100, 100, 100, 100, 100, 103, 103, 103, 103, 103, 103, 103, 103, 103, 103, 104, 104, 104, 104, 104]
print("\n--- 启发式方法:Karmarkar-Karp ---")
print("超集均值:", mean(superset))
# 使用 Karmarkar-Karp 划分成 3 个部分
# 注意:此方法不接受预设的子集大小
for p in karmarkar_karp(superset, num_parts=3).partition:
print(f"子集 ({len(p)} 元素): {p}, 均值: {mean(p)}")
从输出可以看出,Karmarkar-Karp 算法生成的子集大小不固定,且其均值与超集均值的接近程度可能不如 MILP 得到的精确解。
尽管 MILP 可以提供最优解,但其计算复杂度是 NP-hard 的。对于大规模问题,求解时间可能过长。在实际应用中,我们需要根据具体情况权衡求解精度和计算效率。
问题规模:
优化策略:
以上就是基于均值优化的数据集子集划分:混合整数规划与启发式方法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号