我正在处理一个类似的采访问题:
给定一个整数和和的数组, 检查是否有任何组合加起来 总和。
当他们想要尝试一组的所有可能组合时,会使用什么编程技术?
即使这不是解决这个问题的最佳解决方案,但我遇到了一些问题,我需要生成或使用列表的所有组合执行某些操作,并且我想知道如何处理它。
答案 0 :(得分:10)
一个方便的见解是要意识到从0
到(2^N)-1
的所有数字的二进制表示实际上是N
个不同项中可能组合的一组位掩码。例如,对于N=3
(3个项目),因此(2^3)-1 = 7
:
0: 000 = none
1: 001 = third item
2: 010 = second item
3: 011 = second and third items
4: 100 = first item
5: 101 = first and third items
6: 110 = first and second items
7: 111 = all 3 items
这使得以设定顺序循环所有可能的选择非常容易(因此不可能跳过或双重访问任何可能的选择)。
答案 1 :(得分:3)
有多种方法可以解决这个问题。一个是其他人发布的经典DP解决方案。我将发布一个仅使用O(S)内存的解决方案,其中S是数组中所有整数的总和(也可以更改为所需的总和),另一个使用非常有效的随机算法即使是数十万个任意大小的数字,甚至是理性数字和负数,都要经受考验。
O(nS)时间和O(S)内存中的DP解决方案:
//let F[i] = 1 if we can get sum i and 0 otherwise
F[0] = 1; // we can always make sum 0
for ( int i = 1; i <= n; ++i )
for ( int j = S; j >= numbers[i]; --j )
F[j] |= F[j - numbers[i]]; // basically, if F[j - numbers[i]] == 1, then we
// can add numbers[i] to make F[j] 1, otherwise
// we can't. A bitwise or operation will save us
// an if/else structure basically.
随机算法的伪代码: 让使用=你总和的数字列表。 让Unused =你不要求和的数字列表。 设tmpsum = 0。 设S =您希望达到的所需金额。
for ( each number x you read )
toss a coin:
if it's heads and tmpsum < S
add x to Used
else
add x to Unused
while ( tmpsum != S )
if tmpsum < S
MOVE one random number from Unused to Used
else
MOVE one random number from Used to Unused
print the Used list, containing the numbers you need to add to get S
这将比动态编程解决方案快得多,特别是对于随机输入。唯一的问题是你无法可靠地检测到何时没有解决方案(你可以让算法运行几秒钟,如果它没有完成,假设没有解决方案)并且你不能确定你会得到解决方案选择的元素数量最少。同样,您可以添加一些逻辑以使算法继续运行并尝试找到具有较少元素的解决方案,直到满足某些停止条件,但这会使其变慢。但是,如果您只对一个有效的解决方案感兴趣并且您有很多数字并且所需的总和可能非常大,那么这可能比DP算法更好。
这种方法的另一个优点是它也可以用于负数和有理数而不需要修改,这对DP解决方案来说是不正确的,因为DP解决方案涉及使用部分和作为数组索引,索引只能是自然的数字。当然,您可以使用哈希表,但这会使DP解决方案更慢。
要生成所有组合,您应该查找回溯:http://en.wikipedia.org/wiki/Backtracking
对于这个问题,您需要使用以下内容:
void back(int k)
{
if ( k > numElements )
{
// add all the nums[i] for which st[i] == 1 and check
// if their sum is what you desire, then return;
}
for ( int i = 0; i <= 1; ++i )
{
st[k] = i;
back(k + 1);
}
}
您应该在纸上运行少量元素以查看其工作原理。您可以通过计算总和来优化它,从而避免最终求和。这是一般的想法。
答案 2 :(得分:2)
这不能回答你的“组合”问题,但它可能是问题的最佳解决方案:P
这是您必须搜索N个总和的子集sum problem问题。
子集和有一个使用动态编程的伪多项式算法:
来自此link
的伪代码Subset-Sum-Solver[S = w1,w2, . . . ,wn,B]
1 Initialize M[0..n, 0..B] everywhere False apart from M[0, 0] = True
2 for i from 1 to n
do
3 for w from 0 to B
do
4 M[i,w] = M[i − 1,w] _M[i − 1,w − wi]
(any reference outside the array returns false)
5 Output M[n,B]
其中B是和,S是数字集,n是S的基数(S中的元素数),M是n×B矩阵。该算法为O(nB)
在面试问题的情况下,对每个总和执行此操作,您将获得一个O(nmB)算法,其中m是您必须测试的总和数。
问题有点模棱两可,用于获取子集的整数数组也是相同的数组?即,数组A中的整数子集是否也加到数组A中的一个整数?在那种情况下,那么算法是O(n ^ 2B),因为n == m
答案 3 :(得分:1)
这里需要一些术语。 组合用于指从一组k
项中挑选n
项,其中k
项的顺序无关紧要。从一组k
项中挑选n
项的相关概念,其中k
项的顺序很重要,被称为置换强>
你最初谈到的是:
给定一个整数和和的数组,检查是否有任何组合加起来。
是另一回事 - 这里没有固定的k
:你对原始项目的任何大小的子集感兴趣。
集合S的所有子集的集合称为S的 power-set ,并且它包含的成员数量有一个非常简单的公式。我将把它作为一个练习 - 一旦你完成了练习,如何通过一组动作组的成员进行枚举应该是相对明显的。
(提示:{ 1, 2 }
的权力集是{ {}, { 1 }, { 2 }, { 1, 2 } }
)
答案 4 :(得分:0)
递归。伪代码就是这样的:
function f(set,currentelement,selectedelements,sum,wantedsum)
{
for (thiselement=currentelement+1 to lastelement)
{
if (sum+thiselement==wantedsum) print out selectedelements+thiselement;
if (sum+thiselement<wantedsum)
{
f(set,thiselement,selectedelements+thiselement,sum+thiselement,wantedsum);
}
}
答案 5 :(得分:0)
这听起来像是经典的递归问题。你从第一个元素开始,考虑数组的其余部分;对于每个元素,无论是被挑选还是不被挑选。基本情况是起始索引大于数组的长度。像
这样的东西public static bool canSum(int start, int[] array, int sum)
{
if (start >= array.Length)
return sum == 0;
return canSum(start + 1, array, sum - array[start]) || canSum(start + 1, array, sum);
}
答案 6 :(得分:0)
如果您有正整数和负整数,那么您将遇到组合爆炸,无论您选择哪种算法,每增加一个数组长度都会减慢一个固定的百分比。 (如果只有正整数,则一旦超过目标总和,就可以限制搜索。)
边界问题:您是否也允许重复使用整数?
您应该搜索'组合算法'。如果你想深入研究问题,Knuths'tome-in-progress可以帮助你很多。
答案 7 :(得分:0)
我看到两个选项:
答案 8 :(得分:0)
如果你选择计算一个powerset,它可以很容易地以功能方式完成。
在Haskell中,有一个子序列函数基本上返回任何集合的powerset,作为列表列表。
或者你可以自己写一下
powerSet :: [a] -> [[a]]
powerSet [] = [[]]
powerSet x:xs = map (:x) (powerSet xs) ++ (powerSet xs)