对加权元素进行分区,限制总分区权重

时间:2019-06-28 14:39:05

标签: javascript algorithm

给出大量的正整数“权重”,例如[ 2145, 8371, 125, 10565, ... ]和一个正整数“权重限制”,例如15000,我要按照以下条件将权重划分为一个或多个较小的数组:

  1. 我要尽量减少分区数。
  2. 没有一个分区的总和不能超过重量限制。 (请注意,单个重量不会超过此限制。)

我怀疑这个问题的复杂程度很高。作为答案,我感兴趣:

  1. 最佳解决方案
  2. 非最佳选择,但运行速度很快(近似)的解决方案

当前非最佳方法:(基本贪婪算法; JavaScript)

function minimizePartitions(weights, weightLimit) {
  let currentPartition = [];
  let currentSum = 0;
  let partitions = [ currentPartition ];
  
  for (let weight of weights) {
    if (currentSum + weight > weightLimit) {
      currentPartition = [];
      currentSum = 0;
      partitions.push(currentPartition);
    }
    
    currentPartition.push(weight);
    currentSum += weight;
  }
  
  return partitions;
}

let weights = [3242, 987, 1222, 7299, 400, 10542, 10678, 513, 3977];
console.log(minimizePartitions(weights, 15000));

3 个答案:

答案 0 :(得分:6)

这是bin-packing problem,已知是NP硬性的。

为了快速近似,我建议从最大到最小进行排序,然后将每个元素放入最合适的容器中。

答案 1 :(得分:3)

下面是一种实现快速但近似图像解决方案的方法。

让我们按重量对项目进行排序。

具有最大分区权重 W 时,分区数的下限为 P = sum(所有件的权重)/ W

让我们最初创建尽可能多的分区,然后将最重到最轻的项目分配给每个分区,并尝试将每个最重的项目仍可以放入其中。

如果剩余一些项(除非我们找到完美的组合,否则它们将保留),通过运行相同算法将它们放入另一个分区(甚至多个)。请注意,此类溢出分区将具有最轻的项目。

以上内容以线性时间运行(尽管上述按重量排序的项目是对数线性的)。

根据权重的分布以及限制的严格程度,我们可以考虑进行优化。

如果我们的数字 P 略高于最接近的整数(例如3.005),并且我们有 P + 1 个分区,我们就不希望减少它们的数量,并且可以停止。

如果 P 在某种程度上低于最接近的整数(例如2.77),并且我们有 P + 2 个分区,我们可以希望从内部一个溢出分区中推送项目其他分区中的空闲空间。

为此,我们可以在分区之间交换元素,尝试最大化其中一个的空闲空间(并最小化另一个),然后将溢出分区中的某些项放到该空闲空间中。交换步骤是必需的,否则项目将在第一次运行时恰好适合该分区,而不会进入溢出分区。如果不再有可能产生足够大的可用空间的交换,则此优化阶段应停止。

这部分是高度非线性的(在此粗略的描述中不会对其进行分析)。它可能受到运行时间的限制(不要尝试优化超过1秒的时间),并且取决于大小的分布。幸运的是,此步骤应该能够经常消除松散的溢出分区。

希望这会有所帮助。

答案 2 :(得分:3)

最佳

这是蛮力方法的实现,该方法会生成权重的所有可能分区,其中分区满足约束条件,然后在迭代分区时跟踪理想解决方案。

它总是产生理想的解决方案,但是在Node.js中进行测试,仅在9个值的数组上在我的计算机上运行大约需要50秒。

很公平的警告,运行此可能会使您的浏览器崩溃。

// adapted from https://stackoverflow.com/a/31145957/1541563
function nextPermutation (array, compare) {
  let i = array.length - 1;

  while (i > 0 && compare(array[i - 1], array[i]) >= 0) {
    i--;
  }

  if (i === 0) return false;

  let j = array.length - 1;

  while (compare(array[j], array[i - 1]) <= 0) {
    j--;
  }

  [array[i - 1], array[j]] = [array[j], array[i - 1]];

  let k = array.length - 1;

  while (i < k) {
    [array[i], array[k]] = [array[k], array[i]];
    i++;
    k--;
  }

  return true;
}

function * permutations (array, compare) {
  array.sort(compare);

  do {
    yield [...array];
  } while (nextPermutation(array, compare));
}

function * partitions (array, predicate) {
  if (predicate(array)) yield [array];

  const end = array.length - 1;

  for (let i = 1; i < end; i++) {
    for (const a of partitions(array.slice(0, i), predicate)) {
      for (const b of partitions(array.slice(i), predicate)) {
        yield [...a, ...b];
      }
    }
  }
}

function * partitionsOfPermutations (array, predicate, compare) {
  for (const permutation of permutations(array, compare)) {
    yield * partitions(permutation, predicate);
  }
}

function idealPartition (array, predicate, comparePartitions, compareValues) {
  const iterator = partitionsOfPermutations(array, predicate, compareValues);
  let ideal = iterator.next().value;

  for (const partition of iterator) {
    if (comparePartitions(ideal, partition) > 0) {
      ideal = partition;
    }
  }

  return ideal;
}

const weights = [3242, 987, 1222, 7299, 400, 10542, 10678, 513, 3977];

const limit = 15000;

function constraint (weights) {
  return weights.reduce(
    (sum, weight) => sum + weight,
    0
  ) <= limit;
}

function minPartition (a, b) {
  return a.length - b.length;
}

function minValue (a, b) {
  return a - b;
}

const solution = idealPartition(
  weights,
  constraint,
  minPartition,
  minValue
);

console.log(solution);
console.log((performance.now() / 1000).toFixed(2), 'seconds');

如果没有给定约束的解决方案,则返回值为undefined。在这种情况下,它将返回:

[ [ 400, 513, 987, 1222, 3242, 7299 ],
  [ 10542 ],
  [ 3977, 10678 ] ]

使用动态编程,绝对有可能对这种蛮力算法进行改进。不过,我会将其留给读者练习。

这种方法的优点在于,它足以应付大量理想的分区问题。

非最佳逼近

如果您为理想的分区指定了截止条件,则该程序可以在发现“足够好”的分区时提前终止。这取决于选择的谓词,速度非常快。 对于此特定输入,它可以在不到一秒钟的时间内返回理想的解决方案:

// adapted from https://stackoverflow.com/a/31145957/1541563
function nextPermutation (array, compare) {
  let i = array.length - 1;

  while (i > 0 && compare(array[i - 1], array[i]) >= 0) {
    i--;
  }

  if (i === 0) return false;

  let j = array.length - 1;

  while (compare(array[j], array[i - 1]) <= 0) {
    j--;
  }

  [array[i - 1], array[j]] = [array[j], array[i - 1]];

  let k = array.length - 1;

  while (i < k) {
    [array[i], array[k]] = [array[k], array[i]];
    i++;
    k--;
  }

  return true;
}

function * permutations (array, compare) {
  array.sort(compare);

  do {
    yield [...array];
  } while (nextPermutation(array, compare));
}

function * partitions (array, predicate) {
  if (predicate(array)) yield [array];

  const end = array.length - 1;

  for (let i = 1; i < end; i++) {
    for (const a of partitions(array.slice(0, i), predicate)) {
      for (const b of partitions(array.slice(i), predicate)) {
        yield [...a, ...b];
      }
    }
  }
}

function * partitionsOfPermutations (array, predicate, compare) {
  for (const permutation of permutations(array, compare)) {
    yield * partitions(permutation, predicate);
  }
}

function idealPartition (array, predicate, comparePartitions, compareValues, cutoff) {
  const iterator = partitionsOfPermutations(array, predicate, compareValues);
  let ideal = iterator.next().value;

  for (const partition of iterator) {
    if (comparePartitions(ideal, partition) > 0) {
      if (cutoff(ideal = partition)) return ideal;
    }
  }

  return ideal;
}

const weights = [3242, 987, 1222, 7299, 400, 10542, 10678, 513, 3977];

const limit = 15000;

function constraint (weights) {
  return weights.reduce(
    (sum, weight) => sum + weight,
    0
  ) <= limit;
}

function minPartition (a, b) {
  return a.length - b.length;
}

function minValue (a, b) {
  return a - b;
}

// we already know the solution to be size 3
const average = Math.ceil(
  weights.reduce(
    (sum, weight) => sum + weight,
    0
  ) / limit
);

function isSolution (partition) {
  return partition.length === average;
}

const solution = idealPartition(
  weights,
  constraint,
  minPartition,
  minValue,
  isSolution
);

console.log(solution);
console.log((performance.now() / 1000).toFixed(2), 'seconds');