我正在尝试实现 Karmarkar-Karp 启发式数字分区算法的 k 分区版本。但我在第二阶段苦苦挣扎,从结果差异集重建数字分区。
我能找到的用一些伪代码彻底描述第二阶段的唯一来源是论文的第 58 页:通过多标准图分区实现多物理场仿真的负载平衡。
给定多重集 S = [1,7,5,10,9,3] 将其划分为三个 (k=3) 子集,该算法经历了 2 个阶段。
阶段1:
它首先按降序对 S 进行排序:
S = [10,9,7,5,3,1]
然后,将 S 中的每个数字转换为大小为 k 的元组,第一个条目是数字本身,其他条目为零:
M = [[10,0,0],[9,0,0],[7,0,0],[5,0,0],[3,0,0],[1,0,0] ]
这将创建矩阵 M。
在每一步,(a, b, c) 和 (x, y, z) 从 M 中删除,并在适当位置形成一个新元组:E := (a + x, b + y, c + z)。元组按其最小值归一化并按降序排序,然后按顺序将其插入 M 中。在每个步骤中,算法通过将旧值([a, x], [b, y], [c, z])和用于归一化的数字放入两个堆栈中来记忆旧值数组。
M = [[10,0,0],[9,0,0],[7,0,0],[5,0,0],[3,0,0],[1,0,0]]
Old values: []
Norm. numbers: []
M = [[10,9,0],[7,0,0],[5,0,0],[3,0,0],[1,0,0]]
Old values: [([10,0],[0,0],[0,9])]
Norm. numbers: [0]
M = [[5,0,0],[3,0,0],[3,2,0],[1,0,0]]
Old values: [([10,0],[9,0],[0,7]), ([10,0],[0,0],[0,9])]
Norm. numbers: [7,0]
M = [[5,3,0],[3,2,0],[1,0,0]]
Old values: [([5,0],[0,0],[0,3]), ([10,0],[9,0],[0,7]), ([10,0],[0,0],[0,9])]
Norm. numbers: [0,7,0]
M = [[2,2,0],[1,0,0]]
Old values: [([5,0],[3,2],[0,3]), ([5,0],[0,0],[0,3]), ([10,0],[9,0],[0,7]), ([10,0],[0,0],[0,9])]
Norm. numbers: [3,0,7,0]
M = [[1,1,0]]
Old values: [([2,0],[2,0],[0,1]), ([5,0],[3,2],[0,3]), ([5,0],[0,0],[0,3]), ([10,0],[9,0],[0,7]), ([10,0],[0,0],[0,9])]
Norm. numbers: [1,3,0,7,0]
阶段2:
第二阶段从产生的差异集、记忆的旧值和归一化数字构建分区。
在每一步,它都会弹出记忆的旧值数组和规范化编号:
M = [[1],[1],[0]]
Old values: [[2,0],[2,0],[0,1]]
Norm. number: 1
对于旧值数组中的每个元组,它计算一个要在 M 中搜索的值:
[2,0]: 2 + 0 - 1 = 1
然后在 M 中搜索包含该值的分区并将该值替换为元组:
[[2,0],[1],[0]]
[2,0]: 2 + 0 - 1 = 1
=> [[2,0],[2,0],[0]]
在每次迭代中,它永远不会将两个元组放入同一个分区。所以 [0,1] 进入最后一个分区:
[0,1]: 0 + 1 - 1 = 0
=> [[2,0],[2,0],[0,1]]
等等:
M = [[2,0],[2,0],[0,1]]
Old values: [[5,0],[3,2],[0,3]]
Norm. number: 3
M = [[0,0,5],[0,3,2],[1,0,3]]
Old values: [[5,0],[0,0],[0,3]]
Norm. number: 0
问题来了:
M = [[0,0,0,5],[3,2,0,0],[1,0,0,3]]
Old values: [[10,0],[9,0],[0,7]]
Norm. number: 7
[10,0]: 10 + 0 - 7 = 3
=> [[0,0,0,5],[10,0,2,0,0],[1,0,0,3]]
[9,0]: 9 + 0 - 7 = 2
=> ???
在这次迭代中,我已经将 [10,0] 元组放入了 [3,2,0,0] 分区,所以我不能再将另一个元组放入其中。但是 M 没有另一个包含值 2 的分区。
如果在出现这种情况时允许将多个元组放入同一个分区,那么我得到的分区远非最佳:
[[0,0,0,0,5,7],[0,0,0,0,0,0,10,9],[1,0,0,3]]
我想我可以使用最大二分匹配来解决这个问题,但感觉就像一个肮脏的黑客。
我错过了一些重要的步骤还是有更好的方法来重建分区?
结论:
David Eisenstat 在 C++ 中提供了该算法的优雅且快速的 OOP 实现。
但是对于我的工作,我需要用 PHP 重写它。实现的直接翻译被证明是内存效率低且速度慢。由于将每个数字包装到 Subset 对象中,因此内存效率低下。而且它很慢,因为在 PHP 中对对象数组进行排序比对数字数组进行排序要慢一个数量级。例如,使用这种方法将 500000 个数字的数组划分为 100 个子集需要 138 秒,而我的贪心算法的实现只需要 9 秒。
因此,在分析器中使用两天后,我使用数组重写了 PHP 实现。它看起来很难看,但是当 k 低时它比我实现的贪婪算法更好,而当 k 高时它不会那么慢:
K: 2
Greedy: 330 (seconds)
Karmarkar–Karp: 6
K: 4
Greedy: 177
Karmarkar–Karp: 6
K: 8
Greedy: 85
Karmarkar–Karp: 6
K: 16
Greedy: 43
Karmarkar–Karp: 8
K: 32
Greedy: 21
Karmarkar–Karp: 11
K: 64
Greedy: 11
Karmarkar–Karp: 20
K: 128
Greedy: 8
Karmarkar–Karp: 33
K: 256
Greedy: 3
Karmarkar–Karp: 61
我希望 David Eisenstat 的答案和我的解决方案都证明是有用的。
<?php
function greedy(array $array, int $k) : array
{
$result = new \Ds\PriorityQueue();
for ($i = 0; $i < $k; $i++)
{
$result->push([], 0);
}
sort($array, SORT_NUMERIC);
while (!empty($array))
{
$number = array_pop($array);
$smallestSumArray = $result->pop();
$smallestSumArray [] = $number;
$sum = array_sum($smallestSumArray);
$result->push($smallestSumArray, -$sum);
}
return $result->toArray();
}
function karmarkarKarp(array $array, int $k) : array
{
$id = PHP_INT_MIN;
$heap = new \Ds\PriorityQueue();
$idToNumbers = new \Ds\Map();
$idToSum = new \Ds\Map();
/**
* Convert every number into an ID that is connected with a numbers array using $idToNumbers map
* and with a sum using $idToSum map
*/
foreach ($array as $number)
{
$idToNumbers[$id] = new \Ds\Stack([$number]);
$idToSum[$id] = $number;
$heap->push([$id], $number);
++$id;
}
//Partitioning:
$sumToId = [];
while ($heap->count() > 1)
{
/** @var array $a */
$a = $heap->pop();
/** @var array $b */
$b = $heap->pop();
for ($i = 0; $i < $k; $i++)
{
$reverseI = $k - 1 - $i;
if (!isset($a[$i]) && !isset($b[$reverseI])) // Instead of filling k-tuple with zeroes just check that a position is set
{
continue;
}
if (!isset($a[$i]) || !isset($b[$reverseI]))
{
$Ai = $a[$i] ?? $b[$reverseI];
unset($a[$i], $b[$reverseI]);
$sum = $idToSum[$Ai];
isset($sumToId[$sum]) ? $sumToId[$sum] []= $Ai : $sumToId[$sum] = [$Ai];
continue;
}
/** @var int $Ai */
$Ai = $a[$i];
/** @var int $Bk */
$Bk = $b[$reverseI];
unset($a[$i], $b[$reverseI]);
$aNumbers = $idToNumbers[$Ai];
$bNumbers = $idToNumbers[$Bk];
while (!$bNumbers->isEmpty())
{
$aNumbers->push($bNumbers->pop());
}
$sum = $idToSum[$Ai] + $idToSum[$Bk];
$idToSum[$Ai] = $sum;
isset($sumToId[$sum]) ? $sumToId[$sum] []= $Ai : $sumToId[$sum] = [$Ai];
$idToNumbers->remove($Bk);
$idToSum->remove($Bk);
}
krsort($sumToId); // It's faster than using usort() to sort $a by sums in $idToSum map
$a = array_merge(...$sumToId);
$sumToId = [];
$difference = $idToSum[$a[0]] - (isset($a[$k -1]) ? $idToSum[$a[$k -1]] : 0);
$heap->push($a, $difference);
}
//Preparing the resulting array:
/** @var array $last */
$last = $heap->pop();
array_walk($last, function (&$item) use ($idToNumbers) {
/** @var \Ds\Stack $numbersStack */
$numbersStack = $idToNumbers[$item];
$item = [];
while (!$numbersStack->isEmpty())
{
$item []= $numbersStack->pop();
}
});
return $last;
}
$randomArray = [];
for ($i = 0; $i < 500000; $i++)
{
$randomArray []= random_int(1, 6000);
}
for ($k = 2; $k <= 256; $k *= 2)
{
echo PHP_EOL . 'K: ' . $k;
$time = time();
$result = greedy($randomArray, $k);
echo PHP_EOL . 'Greedy: ' . (time() - $time);
gc_mem_caches();
$time = time();
$result = karmarkarKarp($randomArray, $k);
echo PHP_EOL . 'Karmarkar–Karp: ' . (time() - $time) . PHP_EOL;
gc_mem_caches();
}