前言
鄙人近日看到一個算法題,覺得非常有趣,題目爲:任意一些數字分爲三組,使得每組之和儘量相等。題目看似簡單,但是要設計出能夠經得住不限數量、不限大小、不限順序的任意數字的測試的算法並且寫成代碼實現,並不那麼容易。
鄙人想了多種方法解決這個問題。
第一種是將任意數字分成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("您輸入的格式有誤!");
}
}
}
數據測試
鄙人輸入多組任意數字進行測試,均表明此算法的運算結果可靠性無疑: