poj1011

这是一篇关于使用DFS和剪枝技术解决POJ1011整数划分问题的文章。文章指出,通过枚举数字并判断是否能将给定的正整数划分为若干组,每组和相等,从而找到最小的和。作者讨论了两种方法,一种是估算所有可能的组合,但由于效率低会导致超时;另一种是试探法,通过不断尝试拼凑数字,如果能成功划分所有数字则可行。文章强调了在DFS过程中对数字排序、剪枝优化的重要性,并提供了相关代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目有难度。主要难点在于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;
}


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值