PAT 1103 Integer Factorization (30分) dfs(可行性剪枝+最有性剪枝)附数据生成器和对拍程序 Apare_xzc

PAT 1103 Integer Factorization (30分)

dfs+剪枝

附数据生成器和对拍程序


题目链接:

1103 Integer Factorization(30分)

题面:


题意:

  输入为3个正整数N, K, P, 现在要将 N 分解成 K 个数的 P 次方之和,要求这K个数组成的数列是非递增的数列。
  如果没有满足的拆分方案,输出一行“Impossilbe”, 如果有多组拆分方案,输出K个的和最大的方案,如果仍有多组方案,输出字典序最大的方案。


数据范围:

1 <= N <= 400
1 <= K <= N
2 <= P <= 7
(N,K,P 为正整数)


样例:

Sample Input 1

169 5 2

Sample Output 1

169 = 6^2 + 6^2 + 6^2 + 6^2 + 5^2

Sample Input 2

169 167 3

Sample Output 2

Impossible

分析与思路:

   因为是把正整数拆分成K个正整数的P次方之和,可能会有凑不出来的情况。那么我们如何知道凑不出来呢?
  我们先分析边界条件,如果K个数都取最小值1, 还比N大,那一定无解。但是题目条件已经保证了K<=N, 所以我们并不能直接判断出Impossible的情况。所以我们只能搜索,如果搜不到可行解,那么就是无解。

  我们不妨做一个简单的思考,如果K个数一样大,那么一定是最优解(想想为什么)

  题目要求输出K个数之和最大的可行解,如果和相同,输出字典序最大的式子。所以我们不妨按字典序从大到小进行搜索,如果sum_of_k比之前的可行解大,再更新答案

  由于是非递增的数列,所以我们在搜索的时候,只要先确定第一个数的大小。那么第一个数的上界和下界怎么确定呢?

  我们可以分析一下,如果后面的K-1个数都去最大值,即都和第一个数一样,他们的P次方和还是小于N, 这样肯定是无解,所以我们的下界down要满足 down^p * K >= N , 循环或者二分即可以找到最大的下界; 如果后面的K-1个数都取最小值,即都取1,他们的P次方之和还是小于N, 可定也无解。所以上届up要满足up^p + K - 1 <= N, 可以二分找到up的最小值。于是我们确定了第一个数的上届up和下界down.

  接下来我们就可以开始搜索了。搜索的时候,我们自然还是要从往小填数来保证字典序最大的需要,填数的上届和下界的计算方法同第一个数。

  关于剪枝, 我们可以进行可行性剪枝和最有性剪枝:

  1. 可行性剪枝:
       如果后面都放最大值都凑不够N, 就剪掉
       如果后面都放1都比N大,也剪掉
  2. 最优性剪枝:
       如果后面都放最大值,最后K个数的和还是小于当前的最大和,也剪掉

   这样搜索的效率就会比较高


我的AC代码(15ms)

#include <bits/stdc++.h>
using namespace std;
int Pow[21][8]; //Pow[x][j]中存储的是x^j 
long long a[21]; //a[i]表示Pow[i][p] 
int N,k,p; //题目输入,待分解的正整数N,要拆分成K个数的p次方之和 
int ans[405]; //搜索中用于存储拆分方案的数组 
int res[405]; //用于记录最终答案的数组 
int MaxSum; //拆分后x1,x2,x3...xk最大的和 
bool flag; //标记是否搜到了可行的拆分方案 
void dfs(int index,int pos,int remain,int tmpSum)
{ // 
	if(pos==k+1) //说明搜到了一种可行的拆分方案 
	{
		flag = true; //标记搜到了解 
		if(tmpSum>MaxSum) //更新最大的和 
		{ //因为是按字典序从大往小遍历的,所以
		//tmpSum==MaxSum并不需要更新答案 
			MaxSum = tmpSum; //更新K个数的最大和,并记录答案 
			for(int i=1;i<=k;++i) res[i] = ans[i];
		}
		return;
	}
	ans[pos] = index; //第pos个数是index 
	tmpSum += index; //当前pos个数的总和为tmpSum 
	remain -= a[index]; //remain代表N减去之前Pos个数的p次方和剩余 
	
  ///可行性剪枝: 
	if(1ll*(k-pos)*a[1]>remain) return; 
	//后面从第pos个到第K个数都放1也会使最后最后的p次方之和大于N 
	if(1ll*(k-pos)*a[index]<remain) return; //
	//后面从第pos个到第K个数都放index^p也会使最后最后的p次方之和大于N 
	
  ///最优性剪枝: 
	if(1ll*(k-pos)*index+tmpSum<MaxSum) return;
	////后面从第pos个到第K个数都放index^p得到的K个数的和也小于之前搜到的最大值 
	
	for(int t=index;t>=1;--t)
	{//从index往1遍历,保证了字典序大的先搜索 
		dfs(t,pos+1,remain,tmpSum);
	}
}
int main()
{
// freopen("in.txt","r",stdin); 
// freopen("out2.txt","w",stdout);
	//因为p>=2,N<=400,所以填的数最大为sqrt(400)=20 
	for(int i=1;i<=20;++i) //预处理1-20的2-7次方 
		Pow[i][0] = 1;
	for(int i=1;i<=20;++i)
		for(int j=1;j<=7;++j)
			Pow[i][j] = Pow[i][j-1]*i;
	while(scanf("%d%d%d",&N,&k,&p)!=EOF)
	{  //多组输入 
// printf("%d %d %d\n",N,k,p); //用于调试 
		map<int,int> mp; //mp中存放1-20的p次方 
		for(int i=1;i<=20;++i)
			a[i] = Pow[i][p],mp[a[i]]++;
		if(N%k==0&&mp.count(N/k)) //注意判断键的存在性要用count
	    //打codeforces的时候吃过两次亏,用[]访问直接MLE 
		{ //K个数都相同和一定最大
			int y = N/k; //存在x使得x^p * K = N 
			int x = lower_bound(a,a+21,y)-a; //二分查找确定x的值
			//输出答案,行末无空格 
			printf("%d = %d^%d",N,x,p);
			for(int i=2;i<=k;++i) 
				printf(" + %d^%d",x,p);
			puts("");
			continue;
		}
		MaxSum = 0;
		//up和down是第一位的上界和下界 
		int up = 0; //a[up] <= N-K+1的最大值 
		int down = 0; //a[down]*K>=n;的最小值 
		for(int i=1;i<=20;++i)
			if(a[i]<=N-k+1) up = i;	
		for(int i=1;i<=20;++i)
		{ 
			if(a[i]*k>=N)
			{ 
				down = i;break;
			} 
		} 
		flag = false;
		for(int st=up;st>=down;--st)
		{ 
			dfs(st,1,N,0);
		}	
		if(!flag) puts("Impossible");
		else
		{
			printf("%d = %d^%d",N,res[1],p); 
			for(int i=2;i<=k;++i)
				printf(" + %d^%d",res[i],p);
			puts("");
		}
	}
	return 0;
} 


数据生成器:

  由于这道题N,K,P范围都不大,所以我们可以穷尽所有的合法输入。只要保证:

1 <= N <= 400
1 <= K <= N
2 <= P <= 7
(N,K,P 为正整数)

即可,所以数据生成也非常好写,只需要三个for加文件输出即可

数据生成器代码

CreateDate.cpp

//Create Date.cpp
//Author xzc
//2020.1.31 
#include <bits/stdc++.h>
using namespace std;
int main()
{
	freopen("in.txt","w",stdout);
	for(int N=1;N<=400;++N)
	{
		for(int k=1;k<=N;++k)
		{
			for(int p=2;p<=7;++p)
				cout<<N<<" "<<k<<" "<<p<<endl;
		}
	}
	return 0;
}

运行完CreateDate.cpp之后,我们得到一个较大的输入文件in.txt, 可以用notepad++或者其它文本编辑器打开来查看


生成标准答案:

  因为这个题也没什么其它的更稳妥,更暴力的方法生成答案,所以,我们可以用已经AC了的正确代码来计算出 (400+1)*400/2*6 = 481200组N,P,K的输入的解
  我用我的AC代码,改成多组输入,然后用freopen()输入输出重定向,得到了所有输入的解,用时119秒

可以得到一个更大的输出文件,由于为了对排方便,我在每个样例的答案前一行都输出了N,K,P的值。输出文件out.txt的内容展示如下:


对拍程序:judge.cpp

  比较输出文件和标准答案的异同,如果不同,得到相应的输入和错误输出,并且统计出错的个数。
  因为我进行了比较好的剪枝,所以程序跑得飞快,119秒就跑完了481200组输入的数据,但是你的程序如果没有剪枝,或者cin,cout,或者滥用STL,或者该二分查找的非要for遍历等等,可能根本不能在可以接受的时间内跑完所有的合法输入,所以,我们人性化一点儿,没有直接比较两个文件到文件尾,我们设置了比较的行数。

#include <bits/stdc++.h>
using namespace std;
string ask1,res1,ask2,res2;
int main()
{
	ifstream Ans("out.txt"); //标程生成的标准答案 
	ifstream Out("MyOut.txt"); //你的输出 
// freopen("Judge_result.txt","w",stdout); 
	int cnt = 0;
	for(int i=1;i<=962400;i+=2)
	{
		getline(Ans,ask1);
		getline(Ans,res1);
		getline(Out,ask2);
		getline(Out,res2);
		if(res1!=res2)
		{
			cout<<ask1<<endl;
			cout<<"ans is: "<<res1<<endl;
			cout<<"Myout is: "<<res2<<endl;
			cout<<endl;
			++cnt;
		}
	}
	Ans.close();
	Out.close();
	if(cnt==0) cout<<"AC"<<endl;
	else cout<<"cnt = "<<cnt<<endl;
	
	return 0;	
} 


评测结果显示:


AC愉快~

全部评论

相关推荐

程序员猪皮:看不到八股什么意思
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务