给定: 整数数组 值K,M
问题: 找出从给定数组的所有K个元素子集中可以获得的最大总和,使其小于值M?
是否存在非动态规划解决此问题的方法? 或者只有dp [i] [j] [k]可以解决这种类型的问题! 你能解释一下算法吗。
给定: 整数数组 值K,M
问题: 找出从给定数组的所有K个元素子集中可以获得的最大总和,使其小于值M?
是否存在非动态规划解决此问题的方法? 或者只有dp [i] [j] [k]可以解决这种类型的问题! 你能解释一下算法吗。
dp [i] [j] [k]
= true,如果存在一个大小为 k
的子集,其和为输入数组的前 i
个元素之和为 j
。 dp [0] [0] [0] = true
。 i
个元素的大小 k
的子集是否使用了 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
的子集,由一个总和超过M
的大小为K-1
的子集以及另一个足够负面的A []
元素组成,从而总体总和不再超过M
。同样地,我们的大小为K-1
的子集可能会相加得到一些极为负面的数字,然后与A []
中足够正面的元素相加得到M
。为使我们的算法在两种情况下仍能正常工作,我们需要将第二个维度从M
增加到所有A []
中正元素之和与负元素之和(即A []
的所有元素的绝对值的和)之差。
至于是否存在非动态规划解决方案,当然有原始指数时间暴力解决方案和优化指数常数的变体等。
除此之外?您的问题与子集和密切相关,而大名鼎鼎的NP完全问题的文献则相当广泛。作为一般原则,算法可以有各种形状和大小-我不难想象做例如随机化、逼近、(只需选择误差参数足够小!)简单地将其转换为其他NP完全问题(将您的问题转换为一个巨大布尔电路并运行SAT求解器)。是的,这些都是不同的算法。它们比动态规划解决方案更快吗?其中一些可能会。是否像标准算法材料中的培训那样简单易懂或实现呢?可能不是。
这是背包或子集问题的变体,在时间(以指数级增长的空间要求为代价)上,动态规划是正确解决此问题的最有效方法。请查看Is this variant of the subset sum problem easier to solve? 以获取类似于您的问题的问题。
dp [i] [j]
= true
,如果存在长度为i
且总和为j
的子集,则为true
,否则为false
。思路是dp [][ ]
将编码每个可能长度的所有可能子集的总和。然后,我们可以简单地找到最大的j <= M
,使得dp [K] [j]
为true
。我们的基本情况是dp [0] [0] = true
,因为我们始终可以通过选择大小为0的子集来使总和为0。n
个值计算了dp [][ ]
的值。要查找数组的前n +1
个值的所有可能子集,我们只需取n +1
个值并将其添加到我们之前看到的所有子集中即可。更具体地说,我们有以下代码: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
values[0] ... values[Z-1]
中选取一个子集,使得元素之和最大,但小于M - values[Z]
。values[X]
的任何最大值,因为所有K
个元素的子集的最大总和仍小于M
。同样,我们也不需要检查任何大于values[Y]
的值,因为所有那些具有K
个元素的子集的最小总和已经大于M。明白我的意思吗? - JohnFelix说得没错,这是背包问题的一个特例。他的动态规划算法需要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
包含[0,2,3],对应于元素1、5、6。
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();
}
}