一連の数値{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)
説明
これは古典的な問題の1つです。考えは、現在の数で可能な合計の数を見つけることです。確かに、合計を0にする方法は1つだけです。最初は、1つの数値しかありません。ターゲット(ソリューション内の変数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-1は0になり、0に対応する要素は1です)。したがって、配列は次のようになります(要素ゼロには1が含まれることを忘れないでください)
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
ここで、2番目の数値は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です。
ダイナミックプログラミングを使用できます。 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
は次のとおりです。
これは、入力である整数配列またはセットのすべての可能なサブセットを検索し、次にfiltering
がtarget
を与えてeになるものを見つけるための古典的なバックトラッキング問題です。
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}
に合計されるサブセットのコンマ区切り値を出力します
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で尋ねられ、答えられました: 与えられた合計に達する数字のすべての可能な組み合わせを見つける
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ソリューションは問題に当てはまります。
あなたが行うことができる1つの最適化は、その合計を構成する実際のセットではなく、特定の合計に対して存在するソリューションの数を数えることです...
これは、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]]になります
大量の入力(25〜30)がある場合の効率的なソリューションを次に示します。
次の2つの方法で効率を向上させました。
このソリューションは、負の数、小数、および入力値の繰り返しで機能します。ほとんどの言語で浮動小数点10進数演算がうまく機能しないため、入力を小数点以下数桁に設定するか、予期しない動作が発生する可能性があります。
私の古い2012年時代のデスクトップコンピューターでは、指定されたコードは、javascript/node.jsでは.8秒、C#では.4秒で25の入力値を処理します。
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)
次のソリューションは、特定の合計を提供するサブセットの配列も提供します(ここでは合計= 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]]
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)で解決できます。サブセット合計の最適な部分構造は次のとおりです。
SubsetSum(A、n、sum)= SubsetSum(A、n-1、sum)|| SubsetSum(A、n-1、sum-set [n-1])
SubsetSum(A、n、sum)= 0、sum> 0およびn == 0の場合SubsetSum(A、n、sum)= 1、sum == 0の場合
ここで[〜#〜] a [〜#〜]は要素の配列、nは配列Aの要素数、sumはサブセット内の要素の合計。
このdpを使用して、合計のサブセットの数を求めることができます。
サブセット要素を取得するには、次のアルゴリズムを使用できます。
SubsetSum(A、n、sum)を呼び出してdp [n] [sum]を埋めた後、dp [n] [sum]から再帰的に走査します。移動するセルの場合、到達する前にパスを保存し、要素の2つの可能性を検討します。
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);
}
}