总和小于 M 的大小为 K 的子集的最大总和

Posted

技术标签:

【中文标题】总和小于 M 的大小为 K 的子集的最大总和【英文标题】:maximum sum of a subset of size K with sum less than M 【发布时间】:2013-08-06 15:49:50 【问题描述】:

鉴于: 整数数组 值 K,M

问题: 找出我们可以从给定数组的所有 K 元素子集中获得的最大和,使得和小于值 M?

是否有可用的非动态规划解决方案来解决这个问题? 或者如果只有 dp[i][j][k] 只能解决这类问题! 你能解释一下算法吗?

【问题讨论】:

我无法理解您的问题,您能否在此处提供一个示例。这是我的理解, [3,5,2,6,1,8,15,18,4] 如果 K 是 3 M 是 15 那么我可以选择 8,2,4 ,答案应该是什么? @AKS 问题是......我们只能制作 K 个长度的子集......并且在这些 K 个长度的子集中,哪个子集给出最大的总和......在总和小于 M 的条件下 好的,所以根据我给出的示例,如果 k 为 3,则在长度为 3 的所有子集中,其总和最大但小于 15。谢谢!! 这里讨论这个问题cs.dartmouth.edu/~ac/Teach/CS105-Winter05/Notes/… @darksky 我不认为这是我问的。它是相同的变体,但不完全是所要求的。 【参考方案1】:

许多人正确地评论说,多年前使用动态编程的以下答案错误地编码了允许数组元素多次出现在“子集”中的解决方案。幸运的是,基于 DP 的方法仍有希望。

如果输入数组的第一个 i 元素的大小为 k 子集,则让 dp[i][j][k] = true 总和为 j

我们的基本情况是dp[0][0][0] = true

现在,第一个i 元素的大小k 子集使用a[i + 1],或者不使用a[i + 1],从而重复出现

dp[i + 1][j][k] = dp[i][j - a[i + 1]][k - 1] OR dp[i][j][k]

把所有东西放在一起:

given A[1...N]
initialize dp[0...N][0...M][0...K] to false
dp[0][0][0] = true
for i = 0 to N - 1:
    for j = 0 to M:
        for k = 0 to K:
            if dp[i][j][k]:
                dp[i + 1][j][k] = true
            if j >= A[i] and k >= 1 and dp[i][j - A[i + 1]][k - 1]:
                dp[i + 1][j][k] = true
max_sum = 0
for j = 0 to M:
    if dp[N][j][K]:
        max_sum = j
return max_sum

给出O(NMK)时间和空间复杂度。

退一步,我们在这里隐含地做了一个假设,即A[1...i] 都是非负的。使用负数,初始化第二个维度0...M 是不正确的。考虑一个大小为K 的子集,该子集由一个大小为K - 1 的子集组成,其总和超过M 和另一个足够负的A[] 元素,使得总和不再超过M。类似地,我们的大小K - 1 子集可以求和到某个极负数,然后用足够正的元素A[] 求和到M。为了让我们的算法在这两种情况下仍然有效,我们需要将第二维从M 增加到A[] 中所有正元素的总和与所有负元素的总和(绝对值的总和)之间的差值。 A[] 中所有元素的值)。

至于是否存在非动态规划解,当然有朴素的指数时间蛮力解和优化指数中的常数因子的变体。

除此之外?好吧,您的问题与子集和密切相关,而且大名鼎鼎的 NP 完全问题的文献相当广泛。作为一般原则,算法可以有各种形状和大小——我不能想象说,随机化,近似,(只需选择足够小的误差参数!)对其他 NP 完全问题的简单旧约化(将您的问题转换为巨大的布尔电路并运行 SAT 求解器)。是的,这些是不同的算法。它们比动态编程解决方案更快吗?其中一些,可能。它们是否易于理解或实施,无需说超出标准介绍算法材料的培训?应该不会吧。

这是背包问题或子集问题的变体,在时间方面(以随着输入大小的增长而呈指数增长的空间需求为代价),动态规划是正确解决此问题的最有效方法问题。请参阅Is this variant of the subset sum problem easier to solve? 了解与您类似的问题。

但是,由于您的问题并不完全相同,我还是会提供一个解释。如果有长度为i 的子集等于j,则设dp[i][j] = true,如果没有,则设false。这个想法是dp[][] 将对每个可能长度的所有可能子集的总和进行编码。然后我们可以简单地找到最大的j <= M,使得dp[K][j]true。我们的基本情况 dp[0][0] = true 因为我们总是可以通过选择一个大小为 0 的子集来生成总和为 0 的子集。

重复也相当简单。假设我们使用数组的第一个 n 值计算了 dp[][] 的值。要查找数组的第一个 n+1 值的所有可能子集,我们可以简单地将 n+1_th 值添加到我们之前见过的所有子集中。更具体地说,我们有以下代码:

initialize dp[0..K][0..M] to false
dp[0][0] = true
for i = 0 to N:
    for s = 0 to K - 1:
        for j = M to 0:
            if dp[s][j] && A[i] + j < M:
                dp[s + 1][j + A[i]] = true
for j = M to 0:
    if dp[K][j]:
        print j
        break

【讨论】:

您的解决方案是正确的。小错误:“如果 dp[s][j] && A[i] + j >= M”应该是“如果 dp[s][j] && A[i] + j 这个解决方案中的 A 是什么?文章中没有提到任何地方。 这里假设你可以在同一个子集中重复A的元素,这不符合子集的定义,即(1, 1)不是(1, 2, 3的子集)。 确实@andresp 是正确的。建议的解决方案是不正确的。您需要跟踪子集中使用了哪些元素。 你们是对的,该解决方案允许您多次使用一个项目。我要记下这个答案【参考方案2】:

我们正在寻找K 元素的子集,其中元素的总和最大,但小于M

我们可以在子集中的最大元素上设置边界[X, Y],如下所示。

首先我们对 (N) 个整数 values[0] ... values[N-1] 进行排序,其中元素 values[0] 是最小的。

下界X 是最大的整数

values[X] + values[X-1] + .... + values[X-(K-1)] &lt; M.

(如果XN-1,那么我们找到了答案。)

上限Y 是小于N 的最大整数

values[0] + values[1] + ... + values[K-2] + values[Y] &lt; M.

通过这个观察,我们现在可以为最高项Z 的每个值绑定第二高项,其中

X &lt;= Z &lt;= Y.

我们可以使用完全相同的方法,因为问题的形式完全相同。减少的问题是找到K-1 元素的子集,取自values[0] ... values[Z-1],其中元素的总和最大,但小于M - values[Z]

一旦我们以相同的方式绑定了 那个 值,我们就可以为两个最高值中的每一对设置第三大值的界限。以此类推。

这给了我们一个树形结构来搜索,希望搜索的组合比 N 选择 K 少得多。

【讨论】:

这是否满足了它是一个子集的需求!我认为解决方案就好像它是一个子数组..子集可以是任何子序列!不过我会试试你的解决方案.. 是的,它确实满足它是一个子集。我对元素进行了排序,以便可以对该子集中满足您条件的最大元素的值设置界限。不需要将任何低于values[X] 的值用作最大值,因为具有K 元素的所有子集的最大值 总和仍小于M。同样,不需要检查任何大于values[Y] 的值,因为所有具有K 元素的子集的最小总和已经大于 比@987654343 @。明白我的意思了吗?【参考方案3】:

Felix 说得对,这是背包问题的一个特例。他的动态规划算法需要 O(K*M) 大小和 O(K*K*M) 时间量。我相信他对变量 N 的使用确实应该是 K。

有两本书专门讨论背包问题。最新的,由 Kellerer、Pferschy 和 Pisinger [2004, Springer-Verlag, ISBN 3-540-40286-1] 在他们的第 76 页图 4.2 中给出了一种改进的动态规划算法,它采用 O( K+M) 空间和 O(KM) 时间,相比 Felix 给出的动态规划算法,这是一个巨大的减少。请注意,本书算法的最后一行有一个错字,应该是 c-bar := c-bar - w_(r(c-bar))。

我的 C# 实现如下。我不能说我已经对它进行了广泛的测试,我欢迎对此提供反馈。我用BitArray实现了书中算法给出的集合的概念。在我的代码中,c 是容量(在原始帖子中称为 M),我使用 w 而不是 A 作为保存权重的数组。

它的一个使用例子是:

int[] optimal_indexes_for_ssp = new SubsetSumProblem(12, new List<int>  1, 3, 5, 6 ).SolveSubsetSumProblem();

其中数组optimal_indexes_for_ssp 包含对应于元素1、5、6 的[0,2,3]。

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;

public class SubsetSumProblem

    private int[] w;
    private int c;

    public SubsetSumProblem(int c, IEnumerable<int> w)
    
      if (c < 0) throw new ArgumentOutOfRangeException("Capacity for subset sum problem must be at least 0, but input was: " + c.ToString());
      int n = w.Count();
      this.w = new int[n];
      this.c = c;
      IEnumerator<int> pwi = w.GetEnumerator();
      pwi.MoveNext();
      for (int i = 0; i < n; i++, pwi.MoveNext())
        this.w[i] = pwi.Current;
    

    public int[] SolveSubsetSumProblem()
    
      int n = w.Length;
      int[] r = new int[c+1];
      BitArray R = new BitArray(c+1);
      R[0] = true;
      BitArray Rp = new BitArray(c+1);
      for (int d =0; d<=c ; d++) r[d] = 0;
      for (int j = 0; j < n; j++)
      
        Rp.SetAll(false);
        for (int k = 0; k <= c; k++)
          if (R[k] && k + w[j] <= c) Rp[k + w[j]] = true;
        for (int k = w[j]; k <= c; k++) // since Rp[k]=false for k<w[j]
          if (Rp[k])
          
            if (!R[k]) r[k] = j;
            R[k] = true;
          
      
      int capacity_used= 0;
      for(int d=c; d>=0; d--)
        if (R[d])
        
          capacity_used = d;
          break;
        
      List<int> result = new List<int>();
      while (capacity_used > 0)
      
        result.Add(r[capacity_used]);
        capacity_used -= w[r[capacity_used]];
       ;
      if (capacity_used < 0) throw new Exception("Subset sum program has an internal logic error");
      return result.ToArray();
    

【讨论】:

以上是关于总和小于 M 的大小为 K 的子集的最大总和的主要内容,如果未能解决你的问题,请参考以下文章

使用每一行的二维数组中小于或等于 k ​​的最大可能总和

具有固定子集大小的 Sum-subset

不能被 K 整除的两个和的最大子集

动态规划算法:硬币的最大总和(小于或等于k)

为啥符号大小的总和远小于“.text”部分的大小?

子三角形中最大元素的总和