给定一组数字:{1,3,2,5,4,9},找到与特定值相加的子集数(例如,本例中为9)。
这类似于子集求和问题,略有不同,不是检查集合是否有一个总和为9的子集,我们必须找到这样的子集的数量。我正在遵循子集求和问题here的解决方案。但我想知道如何修改它以返回子集的数量。
def total_subsets_matching_sum(numbers, sum):
array = [1] + [0] * (sum)
for current_number in numbers:
for num in xrange(sum - current_number, -1, -1):
if array[num]:
array[num + current_number] += array[num]
return array[sum]
assert(total_subsets_matching_sum(range(1, 10), 9) == 8)
assert(total_subsets_matching_sum({1, 3, 2, 5, 4, 9}, 9) == 4)
说明
这是经典问题之一。我们的想法是找到当前数字的可能总和数。确实如此,只有一种方法可以将总和加到0.开始时,我们只有一个数字。我们从目标开始(解决方案中的变量Maximum)并减去该数字。如果可以得到该数字的总和(对应于该数字的数组元素不为零),则将其添加到与当前数字对应的数组元素中。这个程序会更容易理解
for current_number in numbers:
for num in xrange(sum, current_number - 1, -1):
if array[num - current_number]:
array[num] += array[num - current_number]
当数字为1时,只有一种方法可以得出1的总和(1-1变为0,对应0的元素为1)。所以数组就像这样(记住元素零将有1)
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
现在,第二个数字是2.我们开始从9减去2并且它无效(因为7的数组元素是零,我们跳过它)我们一直这样做直到3.当它的3,3 - 2是1和数组元素对应于1是1并且我们将它添加到3的数组元素中。当它的2,2-2变为0并且我们将值0对应于2的数组元素。在此迭代之后,数组看起来像这样
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
我们一直这样做,直到我们处理完所有数字,每次迭代后数组看起来像这样
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
[1, 1, 1, 2, 1, 1, 1, 0, 0, 0]
[1, 1, 1, 2, 2, 2, 2, 2, 1, 1]
[1, 1, 1, 2, 2, 3, 3, 3, 3, 3]
[1, 1, 1, 2, 2, 3, 4, 4, 4, 5]
[1, 1, 1, 2, 2, 3, 4, 5, 5, 6]
[1, 1, 1, 2, 2, 3, 4, 5, 6, 7]
[1, 1, 1, 2, 2, 3, 4, 5, 6, 8]
在最后一次迭代之后,我们会考虑所有数字和获取目标的方式的数量将是与目标值对应的数组元素。在我们的例子中,最后一次迭代后的Array [9]是8。
public class SumOfSubSet {
public static void main(String[] args) {
// TODO Auto-generated method stub
int a[] = {1,2};
int sum=0;
if(a.length<=0) {
System.out.println(sum);
}else {
for(int i=0;i<a.length;i++) {
sum=sum+a[i];
for(int j=i+1;j<a.length;j++) {
sum=sum+a[i]+a[j];
}
}
System.out.println(sum);
}
}
}
我的回溯解决方案: - 对数组进行排序,然后应用回溯。
void _find(int arr[],int end,vector<int> &v,int start,int target){
if(target==0){
for(int i = 0;i<v.size();i++){
cout<<v[i]<<" ";
}
cout<<endl;
}
else{
for(int i = start;i<=end && target >= arr[i];i++){
v.push_back(arr[i]);
_find(arr,end,v,i+1,target-arr[i]);
v.pop_back();
}
}
}
虽然可以直接找到它们是否是与目标相加的子集,但是当您需要跟踪所考虑的部分子集时,实现会变得棘手。
如果您使用链接列表,哈希集或任何其他通用集合,您可能会想要在包含该项目的调用之前将项目添加到此集合,然后在排除该项目的调用之前将其删除。这不能按预期工作,因为添加将发生的堆栈帧与将发生删除的堆栈帧不同。
解决方案是使用字符串来跟踪序列。追加到字符串可以在函数调用中内联完成;从而保持相同的堆栈帧,然后您的答案将与原始的hasSubSetSum递归结构完美匹配。
import java.util.ArrayList;
公共类解决方案{
public static boolean hasSubSet(int [] A, int target) {
ArrayList<String> subsets = new ArrayList<>();
helper(A, target, 0, 0, subsets, "");
// Printing the contents of subsets is straightforward
return !subsets.isEmpty();
}
private static void helper(int[] A, int target, int sumSoFar, int i, ArrayList<String> subsets, String curr) {
if(i == A.length) {
if(sumSoFar == target) {
subsets.add(curr);
}
return;
}
helper(A, target, sumSoFar, i+1, subsets, curr);
helper(A, target, sumSoFar + A[i], i+1, subsets, curr + A[i]);
}
public static void main(String [] args) {
System.out.println(hasSubSet(new int[] {1,2,4,5,6}, 8));
}
}
子集和问题可以使用动态编程在O(sum * n)中求解。子集和的最优子结构如下:
子集和(A,n,sum)=子集和(A,n-1,和)||子集和(A,n-1,和集[n-1])
子集Sum(A,n,sum)= 0,如果sum> 0且n == 0子集Sum(A,n,sum)= 1,如果sum == 0
这里A是元素数组,n是数组A的元素数,sum是子集中元素的总和。
使用此dp,您可以求解总和的子集数。
为了获取子集元素,我们可以使用以下算法:
在通过调用SubsetSum(A,n,sum)填充dp [n] [sum]之后,我们从dp [n] [sum]递归地遍历它。对于遍历的单元格,我们在到达它之前存储路径并考虑元素的两种可能性。
1)元素包含在当前路径中。
2)元素不包含在当前路径中。
每当sum变为0,我们就停止递归调用并打印当前路径。
void findAllSubsets(int dp[], int A[], int i, int sum, vector<int>& p) {
if (sum == 0) {
print(p);
return;
}
// If sum can be formed without including current element
if (dp[i-1][sum])
{
// Create a new vector to store new subset
vector<int> b = p;
findAllSubsets(dp, A, i-1, sum, b);
}
// If given sum can be formed after including
// current element.
if (sum >= A[i] && dp[i-1][sum-A[i]])
{
p.push_back(A[i]);
findAllSubsets(dp, A, i-1, sum-A[i], p);
}
}
以下解决方案还提供了提供特定总和的子集数组(此处sum = 9)
array = [1, 3, 4, 2, 7, 8, 9]
(0..array.size).map { |i| array.combination(i).to_a.select { |a| a.sum == 9 } }.flatten(1)
返回返回9的子集的子集
=> [[9], [1, 8], [2, 7], [3, 4, 2]]
当有大量输入(即25到30)时,这是一个有效的解决方案
我以两种方式提高了效率:
此解决方案适用于负数,小数和重复输入值。由于浮点十进制数学在大多数语言中的工作方式很糟糕,您需要将输入设置为仅保留几个小数位,否则可能会出现一些不可预测的行为。
在我的旧版2012年台式计算机上,给定代码在javascript / node.js中处理大约0.8秒内的25个输入值,在C#中处理3.4秒。
let numbers = [-0.47, -0.35, -0.19, 0.23, 0.36, 0.47, 0.51, 0.59, 0.63, 0.79, 0.85,
0.91, 0.99, 1.02, 1.17, 1.25, 1.39, 1.44, 1.59, 1.60, 1.79, 1.88, 1.99, 2.14, 2.31];
let target = 24.16;
displaySubsetsThatSumTo(target, numbers);
function displaySubsetsThatSumTo(target, numbers)
{
let wheel = [0];
let resultsCount = 0;
let sum = 0;
const start = new Date();
do {
sum = incrementWheel(0, sum, numbers, wheel);
//Use subtraction comparison due to javascript float imprecision
if (sum != null && Math.abs(target - sum) < 0.000001) {
//Found a subset. Display the result.
console.log(numbers.filter(function(num, index) {
return wheel[index] === 1;
}).join(' + ') + ' = ' + target);
resultsCount++;
}
} while (sum != null);
const end = new Date();
console.log('--------------------------');
console.log(`Processed ${numbers.length} numbers in ${(end - start) / 1000} seconds (${resultsCount} results)`);
}
function incrementWheel(position, sum, numbers, wheel) {
if (position === numbers.length || sum === null) {
return null;
}
wheel[position]++;
if (wheel[position] === 2) {
wheel[position] = 0;
sum -= numbers[position];
if (wheel.length < position + 2) {
wheel.push(0);
}
sum = incrementWheel(position + 1, sum, numbers, wheel);
}
else {
sum += numbers[position];
}
return sum;
}
public class Program
{
static void Main(string[] args)
{
double[] numbers = { -0.47, -0.35, -0.19, 0.23, 0.36, 0.47, 0.51, 0.59, 0.63, 0.79, 0.85,
0.91, 0.99, 1.02, 1.17, 1.25, 1.39, 1.44, 1.59, 1.60, 1.79, 1.88, 1.99, 2.14, 2.31 };
double target = 24.16;
DisplaySubsetsThatSumTo(target, numbers);
}
private static void DisplaySubsetsThatSumTo(double Target, double[] numbers)
{
var stopwatch = new System.Diagnostics.Stopwatch();
bool[] wheel = new bool[numbers.Length];
int resultsCount = 0;
double? sum = 0;
stopwatch.Start();
do
{
sum = IncrementWheel(0, sum, numbers, wheel);
//Use subtraction comparison due to double type imprecision
if (sum.HasValue && Math.Abs(sum.Value - Target) < 0.000001F)
{
//Found a subset. Display the result.
Console.WriteLine(string.Join(" + ", numbers.Where((n, idx) => wheel[idx])) + " = " + Target);
resultsCount++;
}
} while (sum != null);
stopwatch.Stop();
Console.WriteLine("--------------------------");
Console.WriteLine($"Processed {numbers.Length} numbers in {stopwatch.ElapsedMilliseconds / 1000.0} seconds ({resultsCount} results). Press any key to exit.");
Console.ReadKey();
}
private static double? IncrementWheel(int Position, double? Sum, double[] numbers, bool[] wheel)
{
if (Position == numbers.Length || !Sum.HasValue)
{
return null;
}
wheel[Position] = !wheel[Position];
if (!wheel[Position])
{
Sum -= numbers[Position];
Sum = IncrementWheel(Position + 1, Sum, numbers, wheel);
}
else
{
Sum += numbers[Position];
}
return Sum;
}
}
-0.35 + 0.23 + 0.36 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
0.23 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.47 + 0.23 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.19 + 0.36 + 0.51 + 0.59 + 0.63 + 0.79 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.47 + -0.19 + 0.36 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
0.23 + 0.47 + 0.51 + 0.63 + 0.85 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
--------------------------
Processed 25 numbers in 0.823 seconds (6 results)
您可以使用动态编程。 Algo复杂度为O(Sum * N)并使用O(Sum)存储器。
这是我在C#中的实现:
private static int GetmNumberOfSubsets(int[] numbers, int sum)
{
int[] dp = new int[sum + 1];
dp[0] = 1;
int currentSum =0;
for (int i = 0; i < numbers.Length; i++)
{
currentSum += numbers[i];
for (int j = Math.Min(sum, currentSum); j >= numbers[i]; j--)
dp[j] += dp[j - numbers[i]];
}
return dp[sum];
}
注意:由于子集的数量可能具有值2 ^ N,因此很容易溢出int类型。
Algo仅适用于正数。
这是一个Java Solution
:
这是一个经典的Back跟踪问题,用于查找整数数组的所有可能子集或设置为输入,然后filtering
那些总和为e的给定target
import java.util.HashSet;
import java.util.StringTokenizer;
/**
* Created by anirudh on 12/5/15.
*/
public class findSubsetsThatSumToATarget {
/**
* The collection for storing the unique sets that sum to a target.
*/
private static HashSet<String> allSubsets = new HashSet<>();
/**
* The String token
*/
private static final String token = " ";
/**
* The method for finding the subsets that sum to a target.
*
* @param input The input array to be processed for subset with particular sum
* @param target The target sum we are looking for
* @param ramp The Temporary String to be beefed up during recursive iterations(By default value an empty String)
* @param index The index used to traverse the array during recursive calls
*/
public static void findTargetSumSubsets(int[] input, int target, String ramp, int index) {
if(index > (input.length - 1)) {
if(getSum(ramp) == target) {
allSubsets.add(ramp);
}
return;
}
//First recursive call going ahead selecting the int at the currenct index value
findTargetSumSubsets(input, target, ramp + input[index] + token, index + 1);
//Second recursive call going ahead WITHOUT selecting the int at the currenct index value
findTargetSumSubsets(input, target, ramp, index + 1);
}
/**
* A helper Method for calculating the sum from a string of integers
*
* @param intString the string subset
* @return the sum of the string subset
*/
private static int getSum(String intString) {
int sum = 0;
StringTokenizer sTokens = new StringTokenizer(intString, token);
while (sTokens.hasMoreElements()) {
sum += Integer.parseInt((String) sTokens.nextElement());
}
return sum;
}
/**
* Cracking it down here : )
*
* @param args command line arguments.
*/
public static void main(String[] args) {
int [] n = {24, 1, 15, 3, 4, 15, 3};
int counter = 1;
FindSubsetsThatSumToATarget.findTargetSumSubsets(n, 25, "", 0);
for (String str: allSubsets) {
System.out.println(counter + ") " + str);
counter++;
}
}
}
它给出了与目标相加的子集的空格分隔值。
将打印出25
中总和为{24, 1, 15, 3, 4, 15, 3}
的子集的commma分隔值
1) 24 1
2) 3 4 15 3
3) 15 3 4 3
同一站点geeksforgeeks还讨论了输出总和为特定值的所有子集的解决方案:http://www.geeksforgeeks.org/backttracking-set-4-subset-sum/
在您的情况下,您只需要计算它们,而不是输出集。请务必在同一页面中检查优化版本,因为它是NP-complete问题。
这个问题也曾在stackoverflow中被提出并回答,但未提及它是一个子集和问题:Finding all possible combinations of numbers to reach a given sum
这是我在ruby中的程序。它将返回数组,每个数组保持子序列求和到提供的目标值。
array = [1, 3, 4, 2, 7, 8, 9]
0..array.size.times.each do |i|
@ary.combination(i).to_a.each { |a| print a if a.inject(:+) == 9}
end
我用java解决了这个问题。这个解决方案非常简单。
import java.util.*;
public class Recursion {
static void sum(int[] arr, int i, int sum, int target, String s)
{
for(int j = i+1; j<arr.length; j++){
if(sum+arr[j] == target){
System.out.println(s+" "+String.valueOf(arr[j]));
}else{
sum(arr, j, sum+arr[j], target, s+" "+String.valueOf(arr[j]));
}
}
}
public static void main(String[] args)
{
int[] numbers = {6,3,8,10,1};
for(int i =0; i<numbers.length; i++){
sum(numbers, i, numbers[i], 18, String.valueOf(numbers[i]));
}
}
}
通常的DP解决方案适用于该问题。
您可以做的一个优化是,计算特定总和存在多少解决方案的数量,而不是构成该总和的实际集合...
这是我在JS中的动态编程实现。它将返回一个数组数组,每个数组保持子序列求和到提供的目标值。
function getSummingItems(a,t){
return a.reduce((h,n) => Object.keys(h)
.reduceRight((m,k) => +k+n <= t ? (m[+k+n] = m[+k+n] ? m[+k+n].concat(m[k].map(sa => sa.concat(n)))
: m[k].map(sa => sa.concat(n)),m)
: m, h), {0:[[]]})[t];
}
var arr = Array(20).fill().map((_,i) => i+1), // [1,2,..,20]
tgt = 42,
res = [];
console.time("test");
res = getSummingItems(arr,tgt);
console.timeEnd("test");
console.log("found",res.length,"subsequences summing to",tgt);
console.log(JSON.stringify(res));
红宝石
此代码将拒绝空数组并返回带有值的正确数组。
def find_sequence(val, num)
b = val.length
(0..b - 1).map {|n| val.uniq.combination(n).each.find_all {|value| value.reduce(:+) == num}}.reject(&:empty?)
end
val = [-10, 1, -1, 2, 0]
num = 2
输出将是[[2],[2,0],[ - 1,1,2],[ - 1,1,2,0]]