前言
鄙人近日看到一个算法题,觉得非常有趣,题目为:任意一些数字分为三组,使得每组之和尽量相等。题目看似简单,但是要设计出能够经得住不限数量、不限大小、不限顺序的任意数字的测试的算法并且写成代码实现,并不那么容易。
鄙人想了多种方法解决这个问题。
第一种是将任意数字分成3组的所有可能的组合全部求出存入集合,然后遍历集合找出和差距最小的3组的组合。这种方法不仅编码复杂,程序的计算量也太大。鄙人亲测,写出来后只能计算11个数字的分组,数字超过10个的话程序就卡死。
第二种是先将数组从小到大排序,然后取出最后一个数字成一个数组,再遍历剩下的数字,找到能加入该数组能使得新数组之和与均值的差距变得最小的那个数字,将其加入数组中。再使用递归找到下一个能加入新数组后使得新数组之和与均值的差距变得最小的数字然后加入数组……不断递归,直到找不到这样的数字为止,就完成了第一个数组的创建。再依次完成第二个和第三个数组。这种方法的计算量比第一种小很多,但是结果在多种数字测试下不总是准确。
第三种方法是鄙人最后想到的,也是最好的一种方法,不仅计算结果准确无误,而且计算量也不大,能够经得起大量数字的测试。本篇博客主要讲这种方法的算法和代码。具体为先计算这些数字之和并且除以3得到的值成为均值,再将这些数字任意分为三组。取出和最大的数组(下称较大数组)与和最小的数组 (下称较小数组),将其中的数字分别从小到大进行排序,计算这两个数组与均值的偏差之和,成为总偏差。通过双重for循环遍历这两个数组,较大数组在外部的for循环先取出一个数字,然后进行判断这个数字是否在在直接加入较小数组之后能让两个数组与均值的总偏差变小,可以的话则将其直接放入较小数组之中。这是第一种数字交换方法。如果第一种数字交换方法不能使得两数组与均值的总偏差变小的话,再进行下面的判断。从小到大遍历较小数组,依次取值,每次取值之后判断已经取出的所有数字是否可以与较大数组当前取出的数字进行交换,使得交换后两个数组之和与均值的总偏差变小。这是第二种交换方式。每次交换之后,使用递归方法进入下一轮的遍历与交换,并且结束后面的循环。
当较大数组与较小数组经过一次以上方法处理之后,判断两个数组是否有数字变化,有的话在新的数组中重新选取和最大与和最小的两个数组进行以上方法的遍历与交换。如果数组的数字没有变化,说明和最大与和最小的两个数组已经没有数字可以交换,能使得两数组之和离均值的偏差减小,这时候说明3个组合已经达到最佳状态。
算法
1、计算数字的总和除以3的值,称为均值。将这些数字任意分为三组,并且按照从小到大的顺序进行排序。
2、从中取出和最大和最小两个数组,计算两组和与均值的总偏差。
3、遍历和最大的数组,依次取出数字后判断其是否其值是否比总偏差的1/2小,小的话直接将其赠给和最小的数组。
4、如果在第3步没有发现可以直接赠予的数字,遍历和最小的数组,将其中的数字从小到大开始累加,直到累加值可以与最大数组中的数字进行交换为止,即交换后使得两个数组与均值的总偏差变小。然后将最大数组中的数字和最小数组中的数字进行1对N交换。
5、每次交换完成后,结束当前循环,通过递归进入下一次寻找和交换。
6、如果一次方法运行结束后没有进行数字交换,则表示该两个数组已经达到最佳状态。
7、再次从得到的3个新的数组中取出和最大与和最小的两个组,使用递归来重新开始遍历和数字交换。
8、如果一轮遍历结束后,两个数组中的数字没有发生变化,则说明和最大的数组与和最小的数组已经没有数字可以交换,3个数组已经达到理想状态,即和尽量相等的状态。
程序代码
package myPractice;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class Grouping {
static List myGather=new ArrayList();
//准备方法,进行两个数组直接的数字交换,使得交换之后两组数据整体上更加接近均值
public static void displace(int[] numbers1,int[]numbers2,int aver,List resultlist){
int sum1=0;
int sum2=0;
int isDisplace=0;
for(int i1=0;i1<numbers1.length;i1++){
sum1=sum1+numbers1[i1];
}
for(int i2=0;i2<numbers2.length;i2++){
sum2=sum2+numbers2[i2];
}
//只有当较小数字组合的和小于均值时,才有必要交换。如果两组数字和均大于均值,则交换不能使其整体上离均值的偏差较小。
if(sum2<=aver){
//计算两组数字离均值的总偏差
int deviation=Math.abs(sum1-aver)+Math.abs(sum2-aver);
for(int i3=0;i3<numbers1.length;i3++){
//如果较大组合中有个比较小的数字的值比总偏差的一般还小,则可以将这个数字直接给予较小组合。
if(numbers1[i3]<=deviation/2){
//isDisplay自增用来标记进行了交换
isDisplace++;
//重新定义两个数组,用来存储交换后的数字组合
int[]numbers1_1=new int[numbers1.length-1];
int[]numbers2_2=new int[numbers2.length+1];
int c1=0;
int c2=0;
//对新数组进行赋值
for(int d1=0;d1<numbers1.length;d1++){
if(d1!=i3){
numbers1_1[c1]=numbers1[d1];
c1++;
}else{
numbers2_2[c2]=numbers1[d1];
c2++;
}
}
for(int d2=0;d2<numbers2.length;d2++){
numbers2_2[c2]=numbers2[d2];
c2++;
}
//对新数组的数字进行从小到大的排序,方便下一轮计算
Arrays.sort(numbers1_1);
Arrays.sort(numbers2_2);
//交换一次结束后,应该使用递归方法进行下一轮交换,并结束当前循环
displace(numbers1_1,numbers2_2,aver,resultlist);
break;
}
int diff=0;
int a=0;
List list=new ArrayList();
//遍历较小组合,计算两个组合中是否具有数字可以交换
for(int i4=numbers2.length-1;i4>=0;i4--){
//如果较小组合中取出的数字比较大组合中的数字还大,则没有必要交换,否则会导致结果偏差更大。
if(numbers2[i4]>=numbers1[i3]){
continue;
}else{
//计算可能用来交换的较大组合的数字和较小数字组合的数字的差值
if(a==0){
diff=numbers1[i3]-numbers2[i4];
}else{
diff=diff-numbers2[i4];
}
//如果差值大于0,并且小于之前的两组数字离均值的偏差值,说明可以交换
if(diff>0){
a++;
list.add(i4);
if(diff<=deviation/2){
isDisplace++;
//下面的代码用以进行数字交换
int[]numbers1_1=new int[numbers1.length-1+a];
int[]numbers2_2=new int[numbers2.length+1-a];
int c1=0;
int c2=0;
for(int d1=0;d1<numbers1.length;d1++){
if(d1!=i3){
numbers1_1[c1]=numbers1[d1];
c1++;
}else{
numbers2_2[c2]=numbers1[d1];
c2++;
}
}
for(int d2=0;d2<numbers2.length;d2++){
int isDis=0;
for(int i5=0;i5<list.size();i5++){
int b=(int) list.get(i5);
if(d2==b){
numbers1_1[c1]=numbers2[d2];
isDis++;
c1++;
}
}
if(isDis==0){
numbers2_2[c2]=numbers2[d2];
c2++;
}
}
Arrays.sort(numbers1_1);
Arrays.sort(numbers2_2);
//一次交换之后,需要使用递归进入下一轮交换,并且结束当前循环
displace(numbers1_1,numbers2_2,aver,resultlist);
break;
}
}else{
diff=diff+numbers2[i4];
}
}
}
}
}
//如果这个方法运行到最后,仍然没有找到可以交换的数字,则说明这两组已经达到最佳状态
//将结果存储,然后重新选出两个较大组合较小组,进行遍历和交换。
if(isDisplace==0){
resultlist.add(numbers1);
resultlist.add(numbers2);
}
}
public static int getSum(int[] arr){
int sum=0;
for(int i=0;i<arr.length;i++){
sum=sum+arr[i];
}
return sum;
}
//此方法用来接收3个数组,从中选取数组和最大和最小的两个组,调用display方法进行数字交换
public static void getResult(int[]numbers1,int[]numbers2,int[]numbers3,int aver){
//数字交换需要使用较大组和较小组来进行,因此需要保证numbers1为和最小的组,numbers3为和最大的组
if(getSum(numbers1)>getSum(numbers2)){
int[]numberss=numbers1.clone();
numbers1=numbers2.clone();
numbers2=numberss.clone();
}
if(getSum(numbers1)>getSum(numbers3)){
int[]numberss=numbers1.clone();
numbers1=numbers3.clone();
numbers3=numberss.clone();
}
//如果两组和相同,选取数组中最小值在两组中最小的那一组进行交换,使计算结果更准确
if(getSum(numbers1)==getSum(numbers2)){
if(getMin(numbers1)>getMin(numbers2)){
int[]numberss=numbers1.clone();
numbers1=numbers2.clone();
numbers2=numberss.clone();
}
}
if(getSum(numbers3)<getSum(numbers2)){
int[]numberss=numbers2.clone();
numbers2=numbers3.clone();
numbers3=numberss.clone();
}
if(getSum(numbers3)<getSum(numbers1)){
int[]numberss=numbers1.clone();
numbers1=numbers3.clone();
numbers3=numberss.clone();
}
if(getSum(numbers2)==getSum(numbers3)){
if(getMin(numbers2)<getMin(numbers3)){
int[]numberss=numbers2.clone();
numbers2=numbers3.clone();
numbers3=numberss.clone();
}
}
List resultlist=new ArrayList();
//调用display方法进行数字交换
displace(numbers3,numbers1,aver,resultlist);
int[]numbers1_1=(int[]) resultlist.get(0);
int[]numbers3_1=(int[]) resultlist.get(1);
//如果交换后两组数字均无变化,说明已经达到理想状态
if(Arrays.equals(numbers1, numbers3_1)&&Arrays.equals(numbers3,numbers1_1)){
myGather.add((int[])resultlist.get(0));
myGather.add((int[])resultlist.get(1));
myGather.add(numbers2);
}else{
//如果交换后两组数字有变化,则使用递归进行下一轮选组和交换
getResult(numbers1_1,numbers2,numbers3_1,aver);
}
}
//这是一个求数组最小值的方法
public static int getMin(int[] arr){
int x = 0;
for(int i=0;i<arr.length;i++){
if(i==0){
x=arr[i];
}else{
if(x>arr[i]){
x=arr[i];
}
}
}
return x;
}
//下面main方法用来接收用户输入的值,并且计算结果
public static void main(String[] args) {
// TODO Auto-generated method stub
int[]numbers=null;
try{
Scanner input=new Scanner(System.in);
System.out.println("请输入一组数字,如您可以输入“1,2,3,4,5”");
String numberString=input.next();
String[]numStr=null;
numStr=numberString.split(",");
numbers=new int[numStr.length];
System.out.println("您输入的数字分别为:");
for(int a=0;a<numStr.length;a++){
numbers[a]=Integer.valueOf(numStr[a]);
System.out.print(numbers[a]+" ");
}
System.out.println();
int sum=getSum(numbers);
int aver=sum/3;
System.out.println("正在计算最佳分配方法...");
int count=numbers.length/3;
int[]numbers1=new int[count];
int[]numbers2=new int[count];
int[]numbers3=new int[numbers.length-count-count];
int c1=0,c2=0,c3=0;
for(int i=0;i<count;i++){
numbers1[c1]=numbers[i];
c1++;
}
for(int i=count;i<count*2;i++){
numbers2[c2]=numbers[i];
c2++;
}
for(int i=count*2;i<numbers.length;i++){
numbers3[c3]=numbers[i];
c3++;
}
getResult(numbers1,numbers2,numbers3,aver);
System.out.println("计算完成,3组分别为:");
for(int i=0;i<myGather.size();i++){
int[]numbersRe=(int[]) myGather.get(i);
for(int j=0;j<numbersRe.length;j++){
System.out.print(numbersRe[j]+" ");
}
System.out.print("(本组之和为:"+getSum(numbersRe)+")");
System.out.println();
}
}catch(Exception e){
System.out.println("您输入的格式有误!");
}
}
}
数据测试
鄙人输入多组任意数字进行测试,均表明此算法的运算结果可靠性无疑: