
本文旨在解决如何从给定数字集合中,为目标数字寻找一个最优的单类型构成组合。针对传统贪婪算法在处理非精确整除时可能出现的局限性,我们提出一种改进方法。该方法通过独立计算每个可用构成数能提供的最大倍数及其剩余量,并对结果进行智能排序,以优先选择余数最小且使用次数合理的组合,从而实现更精准的数字分解近似。
在许多实际应用场景中,我们可能需要将一个目标数值分解为由一组预定义数值(构成数)的组合。例如,在库存管理或资金分配中,我们需要从一系列固定面额或尺寸的物品中,尽可能精确地凑出或接近一个目标总量。一个常见的挑战是,当目标数值无法被构成数精确整除时,如何找到一个“最接近”的组合,或者说,一个剩余量最小的组合。
传统贪婪算法的局限性
一种直观的解决方案是采用贪婪算法:总是优先使用当前能使用的最大构成数,并尽可能多地使用它,然后用剩余的量继续这个过程。让我们看一个PHP示例:
= $size) {
$times = floor($remainingAmount / $size);
$result[$size] = $times;
$remainingAmount -= $times * $size;
} else {
$result[$size] = 0;
}
}
echo ''; print_r($result); echo '
';
?>对于 $amount = 3000,上述代码的输出将是:
Array
(
[1300] => 2
[1200] => 0
[1100] => 0
[1000] => 0
[950] => 0
[900] => 0
[800] => 0
[700] => 0
)这里,1300 * 2 = 2600,剩余 400。虽然 1300 * 2 是一个有效的组合,但我们知道 1000 * 3 = 3000 是一个更好的选择,因为它没有剩余量。这表明简单的贪婪算法可能无法找到全局最优的近似解,尤其是在寻求最小余数时。其主要原因在于,贪婪算法一旦选择了一个构成数并减去了相应的量,就不会回头考虑其他可能更好的组合。
立即学习“PHP免费学习笔记(深入)”;
优化算法:基于最小余数和使用次数的排序
为了克服贪婪算法的局限性,我们可以采用一种不同的策略:不立即减少目标数值,而是针对每个可用的构成数,独立地计算它能提供的最大倍数以及此时产生的剩余量。然后,我们对这些计算结果进行排序,以找到最优的近似解。这里的“最优”定义为:首先余数最小,如果余数相同,则使用次数较少的方案(或根据具体需求选择使用次数较多的方案)。
算法步骤
-
遍历所有构成数: 对于给定的目标数值 $amount 和构成数数组 $sizes 中的每一个 $size。
-
计算倍数和余数:
- 计算当前 $size 能整除 $amount 的最大次数 ($times = floor($amount / $size))。
- 计算此时的剩余量 ($remainder = $amount - $times * $size)。
-
存储结果: 将 $size、$times 和 $remainder 作为一个临时结果存储起来。
-
排序结果: 对所有临时结果进行排序。
-
主要排序键: $remainder,按升序排列(最小余数优先)。
-
次要排序键: $times,按升序或降序排列,具体取决于业务需求。例如,如果希望在余数相同的情况下优先选择使用次数较少的(更“紧凑”的)组合,则按升序排列。如果希望优先选择使用次数较多的(可能更“分散”的)组合,则按降序排列。在我们的示例中,我们选择升序。
PHP实现
$size,
'times' => $times,
'remainder' => $remainder
];
}
// 使用 usort 进行自定义排序
usort($results, static function ($item1, $item2): int {
// 首先按 'remainder' 升序排序
$comparison = $item1['remainder'] <=> $item2['remainder'];
// 如果 'remainder' 相同,则按 'times' 升序排序
// 这样在余数相同的情况下,会优先选择使用次数较少的方案
return $comparison === 0 ? $item1['times'] <=> $item2['times'] : $comparison;
});
echo ''; print_r($results); echo '
';
?>输出分析
运行上述代码,输出结果如下:
Array
(
[0] => Array
(
[size] => 1000
[times] => 3
[remainder] => 0
)
[1] => Array
(
[size] => 950
[times] => 3
[remainder] => 150
)
[2] => Array
(
[size] => 700
[times] => 4
[remainder] => 200
)
[3] => Array
(
[size] => 900
[times] => 3
[remainder] => 300
)
[4] => Array
(
[size] => 1300
[times] => 2
[remainder] => 400
)
[5] => Array
(
[size] => 1200
[times] => 2
[remainder] => 600
)
[6] => Array
(
[size] => 800
[times] => 3
[remainder] => 600
)
[7] => Array
(
[size] => 1100
[times] => 2
[remainder] => 800
)
)从输出中可以看到,排序后的第一个元素 [0] 提供了最佳结果:size 为 1000,times 为 3,remainder 为 0。这正是我们期望的 1000 * 3 = 3000 的完美组合。
值得注意的是,在 remainder 均为 600 的情况下,[5] 元素 (1200 * 2) 比 [6] 元素 (800 * 3) 靠前,这是因为我们设置了次要排序键为 times 的升序,即在余数相同的情况下,优先选择使用次数更少的方案 (2
注意事项与扩展
-
问题定义: 本文提供的解决方案旨在找到一个单类型构成数(例如 X * N)来最佳近似目标数值。如果您的需求是寻找多种不同类型构成数的组合(例如 A * N1 + B * N2 + C * N3)来达到目标或最小余数,那么这属于更复杂的背包问题或找零问题范畴,通常需要动态规划或回溯算法来解决。本教程的方案不适用于这类多类型组合问题。
-
性能考量: 对于较小的构成数数组,这种遍历和排序的方法效率很高。如果构成数数组非常庞大,可能需要考虑更优化的数据结构或算法。
-
浮点数处理: 如果目标数值或构成数是浮点数,需要特别注意浮点数比较的精度问题,可能需要使用 bcmath 扩展进行精确计算,而不是直接使用 floor 和减法。
-
排序规则: 次要排序键(times)的选择应根据具体业务需求调整。例如,如果更倾向于使用尽可能多的构成数来分解,即使余数相同,可以将 times 设置为降序排序。
-
无解情况: 如果目标数值为负数或构成数数组为空,代码需要添加额外的逻辑进行处理,以避免错误或产生无意义的结果。
总结
通过对每个构成数独立进行计算,并结合多级排序策略,我们能够有效地找到给定目标数值的最佳单类型构成组合,尤其是在存在非精确整除情况时。这种方法比简单的贪婪算法更具鲁棒性,能够提供更符合“最小余数”或“最接近”原则的解决方案,为资源分配、库存优化等场景提供了实用的技术支持。











