【问题标题】:Partitioning weighted elements with a restriction on total partition weight对加权元素进行分区,并限制总分区权重
【发布时间】:2019-06-28 14:39:05
【问题描述】:

给定大量正整数“权重”,例如[ 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));

【问题讨论】:

  • 您希望将结果作为数组数组?请添加预期的输出。
  • 你能展示一下你尝试过的东西吗?一些开始,一些注意事项..
  • 你不能用贪心算法解决这个问题,这是一个NP-complete problem@GershomMaes
  • @Margon 我有一种感觉!有这个特定问题的名称吗?
  • @MaheerAli 在这种情况下需要有 3 个分区:[ [ 2, 2 ], [ 14999 ], [ 14999 ] ]

标签: javascript algorithm


【解决方案1】:

这是一个bin-packing problem,已知是 NP-hard。

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

【讨论】:

  • 对于最优解,线性规划
  • 排序最大 -> 最小,然后按顺序包装,尽可能紧密,听起来像是一个很好的近似解决方案。
  • 这一直困扰着我。谁能给我一个反例来说明为什么这并不总是产生理想的解决方案?
  • 这个有效! 100 的限制:[ 50, 33, 33, 33, 20, 20 ] 导致 [ [ 50, 33 ], [ 33, 33, 20 ], [ 20 ] ],但 [ [ 50, 20, 20 ], [ 33, 33, 33 ] ] 是最佳的。
  • 希望我能对有用的答案进行多次检查;这是我决定采用的解决方案。启发式方法似乎非常接近最优,并且代码运行速度很快。
【解决方案2】:

最佳

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

它总是会产生一个理想的解决方案,但是在 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');

【讨论】:

  • 我认为应该注意这个解决方案,因为有一些有趣的代码示例。也许记录它们会更好,但仍然是一本非常有趣的书。出于统计目的,在 i7 第 8 代(如果我没记错的话,7700)和 32 gigs 内存上运行,从该浏览器窗口开始,第一个例程需要 57.39 秒,而第二个例程需要 0.64 秒。
【解决方案3】:

一种非常快速但非常近似图像解决方案的方法可能如下。

让我们按重量排序。

在最大分区权重W的情况下,分区数的下界为P = sum(weights of all pieces) / W

让我们最初创建尽可能多的分区,并从最重到最轻分配项目,尝试在每个分区中放入它仍然可以容纳的最重的项目。

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

上面的运行时间是线性的(尽管前面按重量排序的项目是对数线性的)。

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

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

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

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

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

希望这会有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-18
    • 1970-01-01
    • 2014-11-26
    • 2019-03-19
    • 1970-01-01
    • 2019-01-22
    相关资源
    最近更新 更多