题目有难度。主要难点在于DFS+剪枝。
大致题意为:给定n个正整数,要求将这些正整数整合起来,使得每个整合组的和相同,并要求出该和的最小值。(正整数<=50 && 0<n<=64)
典型的搜索题。分析如下:
题目要求最小的数字和,这里记max为n个数中的最大值,sum为n个数的和,result为满足题意的最小和。那么result必定在max——result之间。这样最暴力的方法为枚举max到result之间的每一个数字,从小到大,判断是否可以将题目中的n个数字组合成若干组,且每组数字和为枚举的数,那么就说明满足题目要求,答案即为该数。
思路其实很明确。主要是如何判断一个数是否满足题目要求可以将n个数划分为如干组,并且每组数字的和即为该数。首先要满足这点,那么该数必须能整除Sum(很明显)。这是第一步剪枝。那么判断的主体如何入手?这里提供两种方法。
1)令x为要判断的数,sum/x=num为划分的组数,那么n个数必定属于这num个组其中一个,也就是说n个数中每个数都有num种可能,那么一共就有n^num种可能情况,当然这是没有剪枝的情形下的估算。而结果的判定则是判断这num个组的和是否相等,即都为x。若为则说明可行,否不可行。
这种思路简单,但是效率太低,假设n比较大,而sum/x也比较大,那么n^num将是非常巨大的数字,即使剪枝也TLE。
下面是其代码(TLE),仅供参考:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 70
#define Maxx 3210
int record[Max];
int index[Maxx];
int n,range,point;
bool dfs(int pivot){
if(pivot==n+1){
for(int i=1;i<=range;i++)
if(index[i]!=point)
return false;
return true;
}
for(int i=1;i<=range;i++){
if(index[i]+record[pivot]<=point){
int Count=index[i];
for(int j=pivot;j<=n;j++)
Count+=record[j];
if(Count<point)
continue;
int temp=index[i];
index[i]+=record[pivot];
if(dfs(pivot+1))
return true;
index[i]=temp;
}
}
return false;
}
int main(){
while(scanf("%d",&n),n){
int i,left=-1,Sum=0,min_value=Max;
for(i=1;i<=n;i++){
scanf("%d",&record[i]);
if(record[i]>left) left=record[i];
if(record[i]<min_value) min_value=record[i];
Sum+=record[i];
}
for(i=left;i<=Sum;i++)
if(Sum%i==0){
memset(index,0,sizeof(index));
point=i;
range=Sum/i;
if(dfs(1))
break;
}
printf("%d\n",i);
}
return 0;
}
2)针对上面简单好实现但是效率低的算法,这里提供一个比较难懂但更高效的算法(一般都是这样,简单算法则低效率,高效算法则复杂)。
同样令x为要判断的数,那么可以不断的试探,拼凑数子使它们和为x,若可以恰好拼凑完所有数,则说明该数可行;相反,若还剩余某些数字无法成功拼凑则不可行。这样说比较抽象。举个例子吧!就拿样例说明:
4
1 2 3 4
其中最大的数为4,则从4开始判断。
首先试探1,1+2=3<=4,可能成功,继续试探
1+2+3=6>4不可性,舍弃3,同理舍弃4 ——说明1与2不能成为一组
然后是1与3,1+3=4<=4,可能成功,已经成为一组
继续试探2, 2+4=6>4不可行,说明1与3不能在一组,舍弃该方案
最后是1与4,大于4直接舍弃该方案,故为4的情况不可性,应该舍弃。
继续判断5、6、……Sum
如上即为试探法的基本思想。
下面进入主题:如何编写实现上述思想的dfs代码,如何优化剪枝?
首先主函数中可有两点优化:
1)上面已经说过,x必整除sum,且该数范围为max——sum之间
2)若x——sum-x之间存在最小数,那么其必定也是x——sum的最小数,否则最小数即为sum
dfs实现中优化剪枝有三处:
1)首先在主函数中按降序排序n个数,由于大数灵活度低,小数灵活度高,故应该降序排序,这样在搜索时会减少时间,同样的代码降序16MS,升序则TLE。在dfs中当判定前一个数不在同一组时,则与之相同的数可均舍弃
2)在枚举时,由于是从前往后枚举,那么当前面已经判定不是同一组时,递归时,则直接从该数后枚举
3)若第一个数,无论怎么方式都无法组合时,则说明该方案x不可性,直接结束dfs,返回false
详细见代码:164K+16ms
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#define Max 70
using namespace std;
int record[Max]; //记录n个数
bool flag[Max]; // 标记n个数中是否已经组合在其中一组中,true为组合,false为还没有组合
int n,point; //point记录要判断的数
bool cmp(const int a,const int b){ //sort函数的cmp
return a>b;
}
/*int cmp(const void *p,const void *q){ // qsort函数的cmp
return *(int*)q-*(int*)p;
}*/
bool dfs(int left,int num,int cost){ //dfs模拟试探过程,判断该数是否可行,其中left为枚举起点,num为正在组合的组的数字之和,cost为已经组合的数字的个数
if(cost==n) // 若已经组合的数字个数为n,则说明该方案可行,这里提醒一点:由于事前已经判断该数可以被sum整除,所以这里的条件可由 //if(cost==n && num==0)变成if(cost==n) 因为只要该条件满足,加上事前已经保证x可被sum整除,那么后面条件num==0必定自动满足,不需要另外判断,可直接返回true
return true;
int former=-1; // 设置比较变量
for(int i=left;i<=n;i++){ //从left开始枚举试探
if(flag[i] || former==record[i]) // 若没有第i个数已经组合,并且与已经判断不能组合的数相同,则跳过,枚举下一个
continue;
flag[i]=true; // 置标记为已经组合,为dfs递归下一层铺设,下一层则不能再组合该数了
if(record[i]+num<point){ // 若相加仍小于x(要判断的数)
if(dfs(i+1,num+record[i],cost+1)) // 组合该数,继续试探下一个数,从i+1开始
return true;
}
else if(record[i]+num==point){ //若相加等于x,则组合该数,继续试探下一个数,但要从1开始试探,且组合数要成为0(思考)
if(dfs(1,0,cost+1))
return true;
}
flag[i]=false; //回溯,若不能组合,则设置为还没组合,等待下一次可能的组合
former=record[i]; //标记此次没有组合成功的数,若下一个数与其相等,则直接跳过
if(num==0) // 若第一个数无论怎么组合都没成功,则说明该方案不可行,直接跳出循环,结束dfs
break;
}
return false; // 若都不能组合,则说明不可行
}
int main(){
while(scanf("%d",&n),n){
int i,left=-1,Sum=0;
for(i=1;i<=n;i++){
scanf("%d",&record[i]);
Sum+=record[i]; //求和
}
memset(flag,0,sizeof(flag)); //初始化为全0,即都为组合
sort(record+1,record+1+n,cmp); //从大到小排序,测试了一下,由于本题排序数据量比较小,故sort与qsort没有什么区别,但是sort在大数据量时明显要快于qsort
//qsort(record+1,n,sizeof(record[0]),cmp);
left=record[1]; // 最大数
for(i=left;i<=Sum-i;i++) // 优化1
if(Sum%i==0){ // 优化2
point=i;
//memset(flag,0,sizeof(flag));
if(dfs(1,0,0)) // 若dfs失败,则flag变成全0(回溯造成),故无需重设置全0
break;
}
if(i<=Sum-i) // 若在i——sum-i范围内找到最小数,则为结果
printf("%d\n",i);
else //否则结果为sum
printf("%d\n",Sum);
}
return 0;
}