【问题标题】:How to divide a set of numbers into two sets such that the difference of their sum is minimum如何将一组数字分成两组,以使它们的总和之差最小
【发布时间】:2015-09-22 06:02:39
【问题描述】:

如何编写一个 Java 程序将一组数字分成两组,以使它们各自数字之和的差最小。

例如,我有一个包含整数的数组 - [5,4,8,2]。我可以把它分成两个数组——[8,2] 和 [5,4]。假设给定的一组数字,可以有一个唯一的解决方案,就像上面的例子一样,如何编写一个Java程序来实现这个解决方案。即使我能够找出最小可能的差异,那也很好。 假设我的方法接收一个数组作为参数。该方法必须首先将接收到的数组分成两个数组,然后将其中包含的整数相加。此后,它必须返回它们之间的差异,使差异尽可能小。

P.S.- 我已经环顾四周,但找不到任何具体的解决方案。这里似乎给出了最可能的解决方案-divide an array into two sets with minimal difference。但我无法从该线程中收集到如何编写 Java 程序来获得问题的明确解决方案。

编辑:

看了@Alexandru Severin 的评论后,我尝试了一个java程序。它适用于一组数字 [1,3,5,9],但不适用于另一组 [4,3,5,9,11]。下面是程序。请提出更改建议:-

 import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FindMinimumDifference {
public static void main(String[] args) {
    int[] arr= new int[]{4,3,5,9, 11};  
    FindMinimumDifference obj= new FindMinimumDifference();
    obj.returnMinDiff(arr);
}

private int  returnMinDiff(int[] array){


    int diff=-1;
    Arrays.sort(array);
    List<Integer> list1= new ArrayList<>();
    List<Integer> list2= new ArrayList<>();
    int sumOfList1=0;
    int sumOfList2=0;
    for(int a:array){
        for(Integer i:list1){
            sumOfList1+=i;
        }
        for(Integer i:list2){
            sumOfList2+=i;
        }
        if(sumOfList1<=sumOfList2){
        list1.add(a);
        }else{
            list2.add(a);
        }
    }

    List<Integer> list3=new ArrayList<>(list1);   
    List<Integer> list4= new ArrayList<>(list2);   
    Map<Integer, List<Integer>> mapOfProbables= new HashMap<Integer, List<Integer>>();
    int probableValueCount=0;
    for(int i=0; i<list1.size();i++){  
        for(int j=0; j<list2.size();j++){
            if(abs(list1.get(i)-list2.get(j))<
abs(getSumOfEntries(list1)-getSumOfEntries(list2))){
                List<Integer> list= new ArrayList<>();
                list.add(list1.get(i));
                list.add(list2.get(j));    
                mapOfProbables.put(probableValueCount++, list);
            }
        }
    }
    int minimumDiff=abs(getSumOfEntries(list1)-getSumOfEntries(list2));
    List resultList= new ArrayList<>();
    for(List probableList:mapOfProbables.values()){  
        list3.remove(probableList.get(0));
        list4.remove(probableList.get(1));
        list3.add((Integer)probableList.get(1));
        list4.add((Integer)probableList.get(0));
        if(minimumDiff>abs(getSumOfEntries(list3)-getSumOfEntries(list4))){ 
// valid exchange 
                minimumDiff=abs(getSumOfEntries(list3)-getSumOfEntries(list4));
                resultList=probableList;
        }

    }

    System.out.println(minimumDiff);

    if(resultList.size()>0){
        list1.remove(resultList.get(0));
        list2.remove(resultList.get(1));
        list1.add((Integer)resultList.get(1));
        list2.add((Integer)resultList.get(0));
    }

    System.out.println(list1+""+list2);  // the two resulting set of 
// numbers with modified data giving expected result

    return minimumDiff;
}

private static int getSumOfEntries(List<Integer> list){
    int sum=0;
    for(Integer i:list){
        sum+=i;
    }
    return sum;
}
private static int abs(int i){
    if(i<=0) 
        i=-i;
    return i;
}
}

【问题讨论】:

  • @LuigiCortese:我试图首先对数组进行排序。然后开始将一个数字放入一组,另一个数字放入另一组,依此类推,直到结束。但它似乎没有用。

标签: java arrays partition


【解决方案1】:

这是分区问题https://en.wikipedia.org/wiki/Partition_problem的变体

如果您想要最佳解决方案,您必须测试每个可能的输出集组合。这对于小集合可能是可行的,但对于大输入是不可行的。

一个很好的近似值是我在下面介绍的贪心算法。

当集合中的数字为 大小与其基数大致相同或更小,但不是 保证产生最好的分区。

首先,您需要将输入放入可排序的集合中,例如 List。

1) 对输入集合进行排序。

2) 创建 2 个结果集。

3) 遍历排序后的输入。如果索引甚至将项目放入结果 1,否则将项目放入结果 2。

  List<Integer> input = new ArrayList<Integer>();
  Collections.sort(input);
  Set<Integer> result1 = new HashSet<Integer>();
  Set<Integer> result2 = new HashSet<Integer>();
  for (int i = 0; i < input.size(); i++) {
      if (i % 2 == 0) {// if i is even
          result1.add(input.get(i));
      } else {
          result2.add(input.get(i));
      }
  }

【讨论】:

  • 感谢您的回答。但是通过这种方式, [5,4,8,2] 不会分为 [5] 和 [2,4,8] 吗?然后差变成 9。
  • 没有。测试是否在项目的索引(位置)而不是项目的值上。输入将排序为 [2,4,5,8],输出将是 [2,5] 和 [4,8]
  • 请注意,此算法将仅提供解决方案的近似值,而不是最佳解决方案。它适用于较大的输入。
  • 这不是正确的答案。对于这个问题,这是一个非常糟糕的解决方案。
  • 这就是为什么我说它是一个近似值。如果您想要最佳解决方案,您必须测试每个可能的输出集组合,这是一个 NP 完全问题。
【解决方案2】:

看来你对算法比对代码更感兴趣。所以,这是我的伪代码:-

int A[];//This contains your elements in sorted (descending) order
int a1[],a2[];//The two sub-arrays
int sum1=0,sum2=0;//These store the sum of the elements of the 2 subarrays respectively
for(i=0;i<A.length;i++)
{
//Calculate the absolute difference of the sums for each element and then add accordingly and thereafter update the sum
    if(abs(sum1+A[i]-sum2)<=abs(sum2+A[i]-sum1))
           {a1.add(A[i]);
            sum1+=A[i];}
    else
           {a2.add(A[i]);
            sum2+=A[i];}
}

这适用于所有整数,无论​​是正数还是负数。

【讨论】:

  • 在示例集上尝试了此代码 - [1,3,5,9] 它不起作用。我有两个数组- [1,5] 和 [3,9]
  • 对初始数组进行降序排序。
  • 现在 [4,3,5,9,11] 失败。我得到 [11,4] [9,5,3],这是错误的。但奇怪的是,对于许多其他集合,它给出了正确的输出!稍作修正可能会使其完美运行。
【解决方案3】:

首先,对数组进行排序,然后将第一个成员放在组中,然后将第二个成员放在另一个伤口中是行不通的,原因如下:

给定输入[1,2,3,100]。 结果将是:[1,3][2,100],显然是错误的。 正确答案应该是:[1,2,3][100]

你可以在 google 上找到很多针对这个问题的优化算法,但是由于我假设你是初学者,所以我会尝试给你一个可以实现的简单算法:

  1. 对数组进行排序
  2. 从最高值到最低值迭代
  3. 对于每次迭代,计算每个组的总和,然后将元素添加到总和最小的组中

在循环结束时,您应该有两个相当平衡的数组。示例:

Array: [1,5,5,6,7,10,20]
i1: `[20] []`
i2: `[20] [10]`
i3: `[20] [10,7]`
i4: `[20] [20,7,6]`
i5: `[20,5] [10,7,6]`
i6: `[20,5] [10,7,6,5]`
i7: `[20,5,1] [10,7,6,5]`

总和为2628。如您所见,我们可以进一步优化解决方案,如果我们交换 56 导致 [20,6,1][20,7,5,5] 总和是相等的。

对于这一步,您可以:

  1. 找到所有元素组(x,y),其中x在group1中,y在group2中,|x-y| &lt; |sum(group1) - sum(group2)|
  2. 循环所有组并尝试将xy 交换,直到获得最小差异
  3. 每次交换后检查总和最大的组中的最小值是否大于组的差异,如果是,则将其转移到另一组

此算法将始终返回最佳解决方案,并且比贪婪方法要好得多。然而,它在复杂性、速度和内存方面并不是最佳的。如果需要它用于非常大的数组并且资源有限,则最佳算法可能会因速度/内存比率和可接受的错误百分比而异。

【讨论】:

  • 嗨 Alexa,非常感谢您的回答。在查看了您的解决方案后,我觉得它适用于大多数情况(假设没有重复的数字和唯一的解决方案),所以我想试一试。但似乎除了您提到的最后两个步骤之外,还需要更多步骤。我用一个java程序试过了,它确实适用于一组特定的否。但又失败了。以下程序,为 [1,3,5,9] 工作,但为 [4,3,5,9,11] 失败。您能否看看下面的程序并提出我在做什么错误。将其作为单独的评论。
  • 将程序添加为问题本身的编辑。
【解决方案4】:

对于这个问题,假设我们可以将数组分成两个子数组,使得它们的和相等。 (即使认为它们不相等,它也会起作用)

因此,如果数组中元素的总和为 S。您的目标是找到总和为 S/2 的子集。你可以为此编写一个递归函数。

  int difference = Integer.MAX_VALUE;

  public void recursiveSum(int[] array, int presentSum, int index,Set<Integer> presentSet){
       if(index == array.length){
          if(Math.abs(presentSum - (S/2)) < difference)){
             difference = Math.abs(presentSum - (S/2);
             // presentSet is your answer
             return;
          }
       }
       recursiveSum(array,presentSum,index+1,presentSet); // don't consider the present element in the final solution
       presentSet.add(array[index]);
       recursiveSum(array,presentSum + array[index],index+1,presentSet); //consider the present element in the final solution

 }

您也可以为此编写等效的O(N^2) 动态编程代码。 我只是在演示这个想法。

因此,当您找到该集合的总和为 S/2 时,您会自动将数组分成总和相同的两部分(此处为 S/2)。

【讨论】:

    【解决方案5】:

    我似乎已经找到了完美的解决方案。下面的Java程序完美运行。唯一的假设是,给定的问题有唯一的解决方案(只有一个解决方案)。这个假设意味着 - 只有非零数。我把程序放在下面。我要求每个人都告诉程序是否会在某些情况下失败,或者是否可以以某种方式改进/优化。 感谢 Alexandru Severin 先生的算法作为此线程中的答案之一发布。

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class FindMinimumDifference {
    
    static List<Integer> list1= new ArrayList<>();
    static List<Integer> list2= new ArrayList<>();
    
    public static void main(String[] args) {
        int[] arr= new int[]{3,-2,9,7};  
        // tested for these sample data:- [1,5,9,3] ; [4,3,5,9,11] ; 
    //[7,5,11,2,13,15,14] ; [3,2,1,7,9,11,13] ; 
        //[3,1,0,5,6,9] ; [6,8,10,2,4,0] ; [3,1,5,7,0] ; [4,-1,5,-3,7] ; [3,-2,9,7]
    
        System.out.println("the minimum possible difference is: "+returnMinDiff(arr));
        System.out.println("the two resulting set of nos. are: "+list1+" and "+list2);
    }
    
    private static int  returnMinDiff(int[] array){
        int diff=-1;
        Arrays.sort(array);
    
        for(int a:array){
            int sumOfList1=0;
            int sumOfList2=0;
    
            for(Integer i:list1){
                sumOfList1+=i;
            }
            for(Integer i:list2){
                sumOfList2+=i;
            }
            if(sumOfList1<=sumOfList2){
            list1.add(a);
            }else{
                list2.add(a);
            }
        }
    
        List<Integer> list3=new ArrayList<>(list1);   
        List<Integer> list4= new ArrayList<>(list2); 
        if(list3.size()!=list4.size()){     // both list should contain equal no. of entries. 
            //If not, add 0 to the list having lesser no. of entries
            if(list3.size()<list4.size()){
                list3.add(0);
            }else{
                list4.add(0);
            }
        }
        Map<Integer, List<Integer>> mapOfProbables= new HashMap<Integer, List<Integer>>();
        int probableValueCount=0;
        for(int i=0; i<list3.size();i++){  
            for(int j=0; j<list4.size();j++){
                if(abs(list3.get(i)-list4.get(j))
       <abs(getSumOfEntries(list3)-getSumOfEntries(list4))){
                    List<Integer> list= new ArrayList<>();
                    list.add(list3.get(i));
                    list.add(list4.get(j));    
                    mapOfProbables.put(probableValueCount++, list);
                }
            }
        }
        int minimumDiff=abs(getSumOfEntries(list1)-getSumOfEntries(list2));
        List resultList= new ArrayList<>();
        for(List probableList:mapOfProbables.values()){ 
            list3=new ArrayList<>(list1);   
            list4= new ArrayList<>(list2);
            list3.remove(probableList.get(0));
            list4.remove(probableList.get(1));
            list3.add((Integer)probableList.get(1));
            list4.add((Integer)probableList.get(0));
            if(minimumDiff>abs(getSumOfEntries(list3)-getSumOfEntries(list4))){ // valid exchange 
                    minimumDiff=abs(getSumOfEntries(list3)-getSumOfEntries(list4));
                    resultList=probableList;
            }
    
        }
    
        if(resultList.size()>0){   // forming the two set of nos. whose difference of sum comes out to be minimum
            list1.remove(resultList.get(0));
            list2.remove(resultList.get(1));
            if(!resultList.get(1).equals(0) )   // (resultList.get(1).equals(0) && !list1.contains(0))
                list1.add((Integer)resultList.get(1));
            if(!resultList.get(0).equals(0) || (resultList.get(0).equals(0) && list2.contains(0)))
                list2.add((Integer)resultList.get(0));
        }
    
        return minimumDiff; // returning the minimum possible difference
    }
    
    private static int getSumOfEntries(List<Integer> list){
        int sum=0;
        for(Integer i:list){
            sum+=i;
        }
        return sum;
    }
    private static int abs(int i){
        if(i<=0) 
            i=-i;
        return i;
    }
    }
    

    【讨论】:

    • 为什么你认为它不适用于zero or repeted numbers?我只是用[0,0,2,2,3] 运行它,结果是[0,0,3] [2,2]。我没有看到任何错误或更好的解决方案
    • @AlexandruSeverin:感谢您检查代码。我没有检查重复的数字。我刚刚认为重复的数字或 0 都会导致多个解决方案。但看来我错了。重复的数字并不一定意味着非唯一的解决方案。而且这个程序似乎也适用于重复的数字(我感觉真的很棒!!):) 但是 0 的存在意味着会有两个解决方案。无论您将 0 放在哪个集合中,都没有关系。因此,如果假设唯一解,则不能考虑 0。感谢所有的大力帮助。我猜这个程序看起来不错
    • 我假设您需要a 最佳解决方案,如果您需要all 可能的解决方案,您可以为每个步骤做出最小差异的解决方案列表,如果您找到更好的解决方案,请重置列表。如果您找到相等的差异解决方案,请将其添加到列表中。然后对迭代找到的每个解决方案应用下一步
    • @AlexandruSeverin 我同意,现在可以很容易地找到满足条件(最小差异)的所有可能的解决方案集。我将尝试调整相同的代码以实现相同的效果。但无论如何,这不是主要问题。我遇到的问题,明确提到假设独特的解决方案。在非唯一解的情况下,该程序将简单地获取它获得的第一个解,并忽略后面可能的解。但是,是的,感谢您的提示。每当我有空时,我都会尝试修改代码以获得所有可能的解决方案。
    • 输入错误答案 [616,202,595,876,388,120,238,296] 输出 = 87 预期 = 15
    猜你喜欢
    • 2015-04-07
    • 2020-04-07
    • 1970-01-01
    • 2021-05-02
    • 1970-01-01
    • 1970-01-01
    • 2012-01-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多