web-dev-qa-db-fra.com

Comment diviser un ensemble en deux sous-ensembles de sorte que la différence entre la somme des nombres dans deux ensembles soit minime?

Étant donné un ensemble de nombres, divisez les nombres en deux sous-ensembles de sorte que la différence entre la somme des nombres dans deux sous-ensembles soit minime.

C'est l'idée que j'ai, mais je ne suis pas sûr que ce soit une bonne solution:

  1. Trier le tableau
  2. Prenez les 2 premiers éléments. Considérez-les comme 2 ensembles (chacun ayant 1 élément)
  3. Prenez l'élément suivant du tableau.
  4. Décidez dans quel ensemble cet élément doit aller (en calculant la somme => il doit être minimum)
  5. Répéter

Est-ce la bonne solution? Pouvons-nous faire mieux?

48
maxpayne

La version décision du problème que vous décrivez est un problème NP-complete et s'appelle le problème de partition . Il existe un certain nombre de approximations qui fournissent, dans de nombreux cas, des solutions optimales ou, du moins, assez bonnes.

L'algorithme simple que vous avez décrit est une façon dont les enfants de terrain de jeu choisiraient des équipes. Cet algorithme gourmand fonctionne remarquablement bien si les nombres dans l'ensemble sont de l'ordre de grandeur similaire.

L'article ( Le problème le plus simple et le plus difficile , par American Scientist , donne une excellente analyse du problème. Vous devriez le lire et le lire!

35
tskuzzy

Non, ça ne marche pas. Il n'y a pas de solution temporelle polynomiale (sauf si P = NP). Le mieux que vous puissiez faire est simplement de regarder tous les différents sous-ensembles. Jetez un oeil au problème de somme de sous-ensemble .

Considérez la liste [0, 1, 5, 6]. Vous réclamerez {0, 5} et {1, 6}, quand la meilleure réponse est en fait {0, 1, 5} et {6}.

7
sshannin

Approche des combinaisons sur les combinaisons:

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

Il s'agit d'une variation du problème de la somme des sous-ensembles et des sacs à dos. Dans le problème de somme de sous-ensemble, étant donné n entiers positifs et une valeur k et nous devons trouver la somme de sous-ensemble dont la valeur est inférieure ou égale à k. Dans le problème ci-dessus, nous avons donné un tableau, ici nous devons trouver le sous-ensemble dont la somme est inférieure ou égale à total_sum (somme des valeurs du tableau). Ainsi, la somme du sous-ensemble peut être trouvée en utilisant une variation de l'algorithme du sac à dos, en prenant les bénéfices comme des valeurs de tableau données. Et la réponse finale est total_sum-dp [n] [total_sum/2]. Jetez un œil au code ci-dessous pour une compréhension claire.

#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
palle sai krishna

L'approche récursive consiste à générer toutes les sommes possibles à partir de toutes les valeurs du tableau et à vérifier quelle solution est la plus optimale. Pour générer des sommes, nous incluons le ième élément dans l'ensemble 1 ou nous n'incluons pas, c'est-à-dire l'incluons dans l'ensemble 2.

La complexité temporelle est O (n * somme) pour le temps et l'espace.

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

Cela peut être résolu en utilisant BST.
Commencez par trier le tableau, dites arr1
Pour commencer, créez un autre arr2 avec le dernier élément de arr1 (supprimez cet ele de arr1)

Maintenant: répétez les étapes jusqu'à ce qu'aucun échange ne se produise.

  1. Vérifiez arr1 pour un élément qui peut être déplacé vers arr2 en utilisant BST de telle sorte que le diff soit moins MIN diff trouvé jusqu'à présent.
  2. si nous trouvons un élément, déplacez cet élément vers arr2 et passez à l'étape1 à nouveau.
  3. si nous ne trouvons aucun élément dans les étapes ci-dessus, faites les étapes 1 et 2 pour arr2 et arr1. c'est-à-dire maintenant vérifier si nous avons un élément dans arr2 qui peut être déplacé vers arr1
  4. continuez les étapes 1 à 4 jusqu'à ce que nous n'ayons pas besoin de swap ..
  5. nous obtenons la solution.

Exemple Java Code:

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

Un petit changement: inversez l'ordre - commencez par le plus grand nombre et descendez. Cela minimisera l'erreur.

0
Ed Staub

Non, votre algorithme est erroné. Votre algo suit une approche gourmande. J'ai implémenté votre approche et elle a échoué sur ce cas de test: (Vous pouvez essayer ici )

Un algorithme gourmand:

#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;
}

Cas de test:

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

La raison pour laquelle l'algorithme gourmand échoue est qu'il ne prend pas en compte les cas où la prise d'un élément plus grand dans l'ensemble de somme plus grand actuel et plus tard un élément beaucoup plus petit dans l'ensemble de somme plus grand peut donner de bien meilleurs résultats. Il essaie toujours de minimiser la différence actuelle sans explorer ou connaître d'autres possibilités, tandis que dans une solution correcte, vous pouvez inclure un élément dans un ensemble plus grand et inclure un élément beaucoup plus petit plus tard pour compenser cette différence, comme dans le cas de test ci-dessus.

Solution correcte:

Pour comprendre la solution, vous devrez comprendre tous les problèmes ci-dessous dans l'ordre:

Mon code (même logique que 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

Triez-vous votre sous-ensemble par ordre décroissant ou croissant?

Pensez-y comme ceci, le tableau {1, 3, 5, 8, 9, 25}

si vous deviez diviser, vous auriez {1,8,9} = 18 {3,5,25} = 33

S'il était trié par ordre décroissant, cela fonctionnerait beaucoup mieux

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

Votre solution est donc fondamentalement correcte, elle doit simplement vous assurer de prendre les valeurs les plus élevées en premier.

EDIT: Lire le post de tskuzzy. Le mien ne fonctionne pas

0
Collecter