3

我正在尝试实现 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();
}

4

1 回答 1

2

我会使用一个阶段来实现这个算法,添加数据结构来跟踪与每个总和相关的子集。在 C++ 中:

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <list>
#include <memory>
#include <vector>

namespace {

using Number = std::uint64_t;

class Subset {
 public:
  Subset() : numbers_{}, sum_{} {}
  explicit Subset(const Number number) : numbers_{number}, sum_{number} {}

  Subset(Subset&&) = default;
  Subset& operator=(Subset&&) = default;

  const std::list<Number>& numbers() const { return numbers_; }

  Number sum() const { return sum_; }

  void Merge(Subset other) {
    numbers_.splice(numbers_.end(), other.numbers_);
    sum_ += other.sum_;
  }

 private:
  Subset(const Subset&) = delete;
  Subset& operator=(const Subset&) = delete;

  std::list<Number> numbers_;
  Number sum_;
};

std::ostream& operator<<(std::ostream& stream, const Subset& subset) {
  stream << '[';
  if (!subset.numbers().empty()) {
    auto it{subset.numbers().begin()};
    stream << *it;
    for (++it; it != subset.numbers().end(); ++it) {
      stream << ',' << *it;
    }
  }
  stream << ']';
  return stream;
}

struct SubsetSumGreater {
  bool operator()(const Subset& a, const Subset& b) const {
    return a.sum() > b.sum();
  }
};

class Partition {
 public:
  Partition(const Number number, const std::size_t k) : subsets_(k) {
    assert(k > 0);
    subsets_.front().Merge(Subset{number});
  }

  Partition(Partition&&) = default;
  Partition& operator=(Partition&&) = default;

  const std::vector<Subset>& subsets() const { return subsets_; }

  Number difference() const {
    return subsets_.front().sum() - subsets_.back().sum();
  }

  void Merge(Partition other) {
    assert(subsets_.size() == other.subsets_.size());
    auto it{subsets_.begin()};
    auto other_it{other.subsets_.rbegin()};
    while (it != subsets_.end() && other_it != other.subsets_.rend()) {
      it->Merge(std::move(*other_it));
      ++it;
      ++other_it;
    }
    std::sort(subsets_.begin(), subsets_.end(), SubsetSumGreater{});
  }

 private:
  Partition(const Partition&) = delete;
  Partition& operator=(const Partition&) = delete;

  std::vector<Subset> subsets_;
};

std::ostream& operator<<(std::ostream& stream, const Partition& partition) {
  stream << '[';
  if (!partition.subsets().empty()) {
    auto it{partition.subsets().begin()};
    stream << *it;
    for (++it; it != partition.subsets().end(); ++it) {
      stream << ',' << *it;
    }
  }
  stream << ']';
  return stream;
}

struct DifferenceLess {
  bool operator()(const Partition& a, const Partition& b) const {
    return a.difference() < b.difference();
  }
};

Partition KarmarkarKarp(const std::vector<Number>& numbers,
                        const std::size_t k) {
  assert(!numbers.empty());
  assert(k > 0);
  std::vector<Partition> heap;
  heap.reserve(numbers.size());
  for (const Number number : numbers) {
    heap.emplace_back(number, k);
  }
  std::make_heap(heap.begin(), heap.end(), DifferenceLess{});
  while (heap.size() > 1) {
    std::pop_heap(heap.begin(), heap.end(), DifferenceLess{});
    Partition partition{std::move(heap.back())};
    heap.pop_back();
    std::pop_heap(heap.begin(), heap.end(), DifferenceLess{});
    heap.back().Merge(std::move(partition));
    std::push_heap(heap.begin(), heap.end(), DifferenceLess{});
  }
  return std::move(heap.front());
}

}  // namespace

int main() { std::cout << KarmarkarKarp({1, 7, 5, 10, 9, 3}, 3) << '\n'; }
于 2021-07-17T23:33:45.537 回答