web-dev-qa-db-ja.com

2つのセットの数値の合計の差が最小になるように、セットを2つのサブセットに分割する方法は?

数値のセットが与えられたら、2つのサブセットの数値の合計の差が最小になるように、数値を2つのサブセットに分割します。

これは私が持っているアイデアですが、これが正しい解決策であるかどうかはわかりません:

  1. 配列を並べ替える
  2. 最初の2つの要素を取得します。それらを2セットと見なします(それぞれ1つの要素を持ちます)
  3. 配列から次の要素を取得します。
  4. この要素をどのセットに入れるかを決定します(合計=>を計算することで最小になるはずです)
  5. 繰り返す

これは正しい解決策ですか?もっと良くできますか?

48
maxpayne

記述している問題の decision バージョンは NP-complete 問題であり、 パーティションの問題 。多くの場合、最適な、または少なくとも十分なソリューションを提供する多くの 近似 があります。

あなたが説明した簡単なアルゴリズムは、遊び場の子供たちがチームを選ぶ方法です。この 貪欲なアルゴリズム は、セット内の数値が同程度の大きさである場合に非常によく機能します。

American Scientistによる記事 The Easiest Hardest Problem は、問題の優れた分析を提供します。読んで読んでください!

35
tskuzzy

いいえ、それは機能しません。多項式時間解はありません(P = NPでない限り)。最善の方法は、すべての異なるサブセットを確認することです。 subset sum problem をご覧ください。

リスト[0, 1, 5, 6]{0, 5}および{1, 6}、ベストアンサーが実際に{0, 1, 5}および{6}

7
sshannin

組み合わせアプローチに対する組み合わせ:

import itertools as it

def min_diff_sets(data):
    """
        Parameters:
        - `data`: input list.
        Return:
        - min diff between sum of numbers in two sets
    """

    if len(data) == 1:
        return data[0]
    s = sum(data)
    # `a` is list of all possible combinations of all possible lengths (from 1
    # to len(data) )
    a = []
    for i in range(1, len(data)):
        a.extend(list(it.combinations(data, i)))
    # `b` is list of all possible pairs (combinations) of all elements from `a`
    b = it.combinations(a, 2)
    # `c` is going to be final correct list of combinations.
    # Let's apply 2 filters:
    # 1. leave only pairs where: sum of all elements == sum(data)
    # 2. leave only pairs where: flat list from pairs == data
    c = filter(lambda x: sum(x[0])+sum(x[1])==s, b)
    c = filter(lambda x: sorted([i for sub in x for i in sub])==sorted(data), c)
    # `res` = [min_diff_between_sum_of_numbers_in_two_sets,
    #           ((set_1), (set_2))
    #         ]
    res = sorted([(abs(sum(i[0]) - sum(i[1])), i) for i in c],
            key=lambda x: x[0])
    return min([i[0] for i in res])

if __== '__main__':
    assert min_diff_sets([10, 10]) == 0, "1st example"
    assert min_diff_sets([10]) == 10, "2nd example"
    assert min_diff_sets([5, 8, 13, 27, 14]) == 3, "3rd example"
    assert min_diff_sets([5, 5, 6, 5]) == 1, "4th example"
    assert min_diff_sets([12, 30, 30, 32, 42, 49]) == 9, "5th example"
    assert min_diff_sets([1, 1, 1, 3]) == 0, "6th example"
1
user913624

これは、ナップザックとサブセット合計の問題のバリエーションです。サブセット合計問題では、n個の正の整数と値kが与えられ、値がk以下であるサブセットの合計を見つける必要があります。上記の問題では配列を指定しましたが、ここでは合計がtotal_sum(配列値の合計)以下のサブセットを見つける必要があります。そのため、ナップザックアルゴリズムのバリエーションを使用して、与えられた配列値として利益を取ることにより、サブセットの合計を見つけることができます。最後の答えはtotal_sum-dp [n] [total_sum/2]です。明確に理解するには、以下のコードをご覧ください。

#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
        int n;
        cin>>n;
        int arr[n],sum=0;
        for(int i=1;i<=n;i++)
        cin>>arr[i],sum+=arr[i];
        int temp=sum/2;
        int dp[n+1][temp+2];
        for(int i=0;i<=n;i++)
        {
            for(int j=0;j<=temp;j++)
            {
                if(i==0 || j==0)
                dp[i][j]=0;
                else if(arr[i]<=j)
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-arr[i]]+arr[i]);
                else
                {
                dp[i][j]=dp[i-1][j];
                }
            }
        }
        cout<<sum-2*dp[n][temp]<<endl;
}
0

再帰的なアプローチは、配列のすべての値からすべての可能な合計を生成し、どのソリューションが最適かを確認することです。合計を生成するには、セット1にi番目のアイテムを含めるか、セット2に含めないでください。

時間と空間の両方の時間の複雑さはO(n * sum)です。

public class MinimumSubsetSum {

  static int dp[][];
  public static int minDiffSubsets(int arr[], int i, int calculatedSum, int totalSum) {

    if(dp[i][calculatedSum] != -1) return dp[i][calculatedSum];

    /**
     * If i=0, then the sum of one subset has been calculated as we have reached the last
     * element. The sum of another subset is totalSum - calculated sum. We need to return the
     * difference between them.
     */
    if(i == 0) {
      return Math.abs((totalSum - calculatedSum) - calculatedSum);
    }

    //Including the ith element
    int iElementIncluded = minDiffSubsets(arr, i-1, arr[i-1] + calculatedSum,
        totalSum);

    //Excluding the ith element
    int iElementExcluded = minDiffSubsets(arr, i-1, calculatedSum, totalSum);

    int res = Math.min(iElementIncluded, iElementExcluded);
    dp[i][calculatedSum] = res;
    return res;
  }

  public static void util(int arr[]) {
    int totalSum = 0;
    int n = arr.length;
    for(Integer e : arr) totalSum += e;
    dp = new int[n+1][totalSum+1];
    for(int i=0; i <= n; i++)
      for(int j=0; j <= totalSum; j++)
        dp[i][j] = -1;

    int res = minDiffSubsets(arr, n, 0, totalSum);
    System.out.println("The min difference between two subset is " + res);
  }


  public static void main(String[] args) {
    util(new int[]{3, 1, 4, 2, 2, 1});
  }

}
0
Juvenik

これは、BSTを使用して解決できます。
まずarr1と言う配列をソートします
arr1の最後の要素で別のarr2の作成を開始するには(このeleをarr1から削除します)

Now:スワップが発生しなくなるまで手順を繰り返します。

  1. BSTを使用してarr2に移動できる要素のarr1を確認し、差分がこれまでに検出されたMIN diffより小さくなるようにします。
  2. 要素が見つかった場合、この要素をarr2に移動し、もう一度step1に進みます。
  3. 上記の手順で要素が見つからない場合は、arr2およびarr1に対して手順1および2を実行します。つまり、arr2にarr1に移動できる要素があるかどうかを確認します
  4. スワップが不要になるまで手順1〜4を繰り返します。
  5. ソリューションを取得します。

サンプルJavaコード:

import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.List;

/**
 * Divide an array so that the difference between these 2 is min
 * 
 * @author shaikhjamir
 *
 */
public class DivideArrayForMinDiff {

    /**
     * Create 2 arrays and try to find the element from 2nd one so that diff is
     * min than the current one
     */

    private static int sum(List<Integer> arr) {

        int total = 0;
        for (int i = 0; i < arr.size(); i++) {
            total += arr.get(i);
        }

        return total;
    }

    private static int diff(ArrayList<Integer> arr, ArrayList<Integer> arr2) {
        int diff = sum(arr) - sum(arr2);
        if (diff < 0)
            diff = diff * -1;
        return diff;
    }

    private static int MIN = Integer.MAX_VALUE;

    private static int binarySearch(int low, int high, ArrayList<Integer> arr1, int arr2sum) {

        if (low > high || low < 0)
            return -1;

        int mid = (low + high) / 2;
        int midVal = arr1.get(mid);

        int sum1 = sum(arr1);
        int resultOfMoveOrg = (sum1 - midVal) - (arr2sum + midVal);
        int resultOfMove = (sum1 - midVal) - (arr2sum + midVal);
        if (resultOfMove < 0)
            resultOfMove = resultOfMove * -1;

        if (resultOfMove < MIN) {
            // lets do the swap
            return mid;
        }

        // this is positive number greater than min
        // which mean we should move left
        if (resultOfMoveOrg < 0) {

            // 1,10, 19 ==> 30
            // 100
            // 20, 110 = -90
            // 29, 111 = -83
            return binarySearch(low, mid - 1, arr1, arr2sum);
        } else {

            // resultOfMoveOrg > 0
            // 1,5,10, 15, 19, 20 => 70
            // 21
            // For 10
            // 60, 31 it will be 29
            // now if we move 1
            // 71, 22 ==> 49
            // but now if we move 20
            // 50, 41 ==> 9
            return binarySearch(mid + 1, high, arr1, arr2sum);
        }
    }

    private static int findMin(ArrayList<Integer> arr1) {

        ArrayList<Integer> list2 = new ArrayList<>(arr1.subList(arr1.size() - 1, arr1.size()));
        arr1.remove(arr1.size() - 1);
        while (true) {

            int index = binarySearch(0, arr1.size(), arr1, sum(list2));
            if (index != -1) {
                int val = arr1.get(index);
                arr1.remove(index);
                list2.add(val);
                Collections.sort(list2);
                MIN = diff(arr1, list2);
            } else {
                // now try for arr2
                int index2 = binarySearch(0, list2.size(), list2, sum(arr1));
                if (index2 != -1) {

                    int val = list2.get(index2);
                    list2.remove(index2);
                    arr1.add(val);
                    Collections.sort(arr1);

                    MIN = diff(arr1, list2);
                } else {
                    // no switch in both the cases
                    break;
                }
            }
        }

        System.out.println("MIN==>" + MIN);
        System.out.println("arr1==>" + arr1 + ":" + sum(arr1));
        System.out.println("list2==>" + list2 + ":" + sum(list2));
        return 0;
    }

    public static void main(String args[]) {

        ArrayList<Integer> org = new ArrayList<>();
        org = new ArrayList<>();
        org.add(1);
        org.add(2);
        org.add(3);
        org.add(7);
        org.add(8);
        org.add(10);

        findMin(org);
    }
}
0
Shaikh Jamir

1つの小さな変更:順序を逆にします-最大数から始めて、下に進みます。これにより、エラーが最小限に抑えられます。

0
Ed Staub

いいえ、アルゴリズムが間違っています。アルゴは貪欲なアプローチに従います。私はあなたのアプローチを実装しましたが、このテストケースで失敗しました: here

貪欲なアルゴリズム:

#include<bits/stdc++.h>
#define rep(i,_n) for(int i=0;i<_n;i++)
using namespace std;

#define MXN 55
int a[MXN];

int main() {
    //code
    int t,n,c;
    cin>>t;
    while(t--){
        cin>>n;
        rep(i,n) cin>>a[i];
        sort(a, a+n);
        reverse(a, a+n);
        ll sum1 = 0, sum2 = 0;
        rep(i,n){
            cout<<a[i]<<endl;
            if(sum1<=sum2) 
                sum1 += a[i]; 
            else 
                sum2 += a[i]; 
        }
        cout<<abs(sum1-sum2)<<endl;
    }
    return 0;
}

テストケース:

1
8 
16 14 13 13 12 10 9 3

Wrong Ans: 6
16 13 10 9
14 13 12 3

Correct Ans: 0
16 13 13 3
14 12 10 9

貪欲なアルゴリズムが失敗する理由は、現在のより大きな合計セットでより大きな要素を取得し、後でより大きな合計セットでより小さな要素を取得すると、より良い結果が得られる場合を考慮しないためです。上記のテストケースと同じように、正しい解決策では、より大きなセットに要素を含め、この差を補正するために後ではるかに小さな要素を含めることができますが、さらなる可能性を探ったり、知ることなく、常に電流の差を最小化しようとします。

正しい解決策:

ソリューションを理解するには、以下の問題をすべて理解する必要があります。

私のコード( this と同じロジック):

#include<bits/stdc++.h>
#define rep(i,_n) for(int i=0;i<_n;i++)
using namespace std;

#define MXN 55
int arr[MXN];
int dp[MXN][MXN*MXN];

int main() {
    //code
    int t,N,c;
    cin>>t;
    while(t--){
        rep(i,MXN) fill(dp[i], dp[i]+MXN*MXN, 0);

        cin>>N;
        rep(i,N) cin>>arr[i];
        int sum = accumulate(arr, arr+N, 0);
        dp[0][0] = 1;
        for(int i=1; i<=N; i++)
            for(int j=sum; j>=0; j--)
                dp[i][j] |= (dp[i-1][j] | (j>=arr[i-1] ? dp[i-1][j-arr[i-1]] : 0));

        int res = sum;

        for(int i=0; i<=sum/2; i++)
            if(dp[N][i]) res = min(res, abs(i - (sum-i)));

        cout<<res<<endl;
    }
    return 0;
}
0
Amit

サブセットを降順または昇順で並べ替えていますか?

このように考えると、配列{1、3、5、8、9、25}

分割する場合、{1,8,9} = 18 {3,5,25} = 33になります

降順でソートされた場合、はるかにうまくいくでしょう

{25,1} = 26 {9,8,5,3} = 25

したがって、ソリューションは基本的に正しいものであり、最大値を最初に取得する必要があります。

編集:tskuzzyの投稿を読んでください。鉱山は機能しません

0
Collecter