埃及分数(迭代深搜+重定义运算符(其实可以不用,顺便介绍一下),附模板)
题目描述
在古埃及,人们使用单位分数的和(形如1/a的, a是自然数)表示一切有理数。如:2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为加数中有相同的。对于一个分数a/b,表示方法有很多种,但是哪种最好呢?首先,加数少的比加数多的好,其次,加数个数相同的,最小的分数越大越好。
如:
19/45=1/3 + 1/12 + 1/180
19/45=1/3 + 1/15 + 1/45
19/45=1/3 + 1/18 + 1/30
19/45=1/4 + 1/6 + 1/180
19/45=1/5 + 1/6 + 1/18.
最好的是最后一种,因为1/18比1/180,1/45,1/30,1/180都大。
给出\(a,b\)(\(0<a<b<1000\)),编程计算最好的表达方式。
输入格式
a b
输出格式
若干个数,自小到大排列,依次是单位分数的分母。
OK先来讲讲迭代深搜是什么鬼
迭代深搜
基础还是我们的老朋友深搜(\(DFS\))同学,但是它却又进行了改造,使得这个方法适用于一些本来不好处理的题目,比如本题。
如果我们暴搜的话,首先是深搜,很明显一个分数可以拆成一个单位分数(即分子为1的分数)加上另一个,然后如果另一个的分数不是单位分数,它就又可以拆,拆完再拆\(……\),这样最后就有可能会出现很多很多分母超大的单位分数
(比如\(……+\frac{1}{22535253257}+\frac{1}{2266662538797}+\frac{1}{32565687858557}+……\)(好吧我瞎编的))相加,那个复杂度肯定是接受不了的,方案不成立。
那么广搜捏?
这题求单位分数最少的方案,确实很容易想到广搜,但是\(……\)
我拿个测试点出来
input
768 769
output
2 3 7 49 476 7686924
没错,你需要枚举单位分数的分母到7686924,实际上在运行过程中会更大,所以广搜扩展节点时也会超时
于是乎,迭代深搜应运而生
其实思路不难,就是先枚举深度或步数,然后在限定深度或步数的情况下深搜,这样就不会出现一直搜下去的情况,也因为有了限制,深搜的步数也会比广搜少,没错,迭代深搜专治各种搜索的疑难杂症,只要你觉得一道题深搜限制太小,广搜节点会扩展很多,二话不说就用迭代深搜,以下是简单的模板:
#include<bits/stdc++.h> using namespace std; int deepth;//深度 void dfs(int t){//t表示步数或深度 if(t==deepth){//已经到达制定深度 if(……){//如果符合条件 …… //记录结果 } return; } for(……){ } dfs(……);//继续搜,具体视情况定 } int main(){ for(deepth=1;;deepth++){ //枚举深度,找到目标情况就掐掉,否则枚举下一个深度初始深度视题目而定 dfs(1); } return 0; }
以埃及分数这道题为例,我们枚举的深度就是结果的单位分数的个数,而且每个单位分数的分母要求递增,也就是说单位分数要递减,所以又多了一个限制就是深搜到第t个单位分数时,这个单位分数明显要大于预定情况下剩余的单位分数的平均数,而这个预定情况下剩余的单位分数的和只需要用原分数减去已经搜到的单位分数(假设这个结果叫\(tmp\)),预计剩下的单位分数的个数也很好求,假设当前正在搜第t个单位分数,那么剩下的单位分数的个数就是\((deepth-t+1)\) ,那么当前搜的这个单位分数必须小于\(tmp\)且大于\(\frac{tmp}{deepth-t+1}\) ,这样就可以大大减小复杂度防止超时。
$ $
$ $
$ $
$ $
好了,接下来讲讲
重定义运算符
其实学一下就会用(而且可以用来装逼) 操作起来也不难
就以这题为例,因为精度问题,所以在比较分数时不能转换成小数再比较,所以需要手写一系列分数运算和比较的子函数,为了显得有逼格,就可以用重定义运算符猥琐一波
我先用一个pair<int,int>
来存分数,first存分子,second存分母
注:pair为C++内置的数据结构,你只需要加#include<iostream>
和using namespace std;
,然后在 \(<,>\) 的逗号的左边填一个你需要的第一个元的数据类型,右边填一个你需要的第二个元的数据类型比如说我搞一个 pair<int,int>a
,那么我要访问第一元就是a.first,第二元就是a.second,要把它赋值成第一元为x,第二元为y可以直接a.first=x;a.second=y;或者a=make_pair(x,y);
然后重定义运算符就是把一个符号作为子函数的名称,而且用的时候直接把符号旁边的变量传递进去
举个栗子,我们写个分数减法
#include<bits/stdc++.h> #define P pair<LL,LL> #define f(x) x.first #define s(x) x.second #define mp(x,y) make_pair(x,y) #define LL long long using namespace std; //这波define极骚 LL gcd(LL a,LL b){//最大公约数 return b==0?a:gcd(b,a%b); } void yf(P &x){//约分 LL g=gcd(f(x),s(x)); f(x)/=g;s(x)/=g; } P operator -(P a,P b){//这里的P是个数据类型,规则和子函数一样,然后那个operator必须加 LL g=gcd(s(a),s(b)); P c; f(c)=f(a)*s(b)/g-f(b)*s(a)/g; s(c)=s(a)*s(b)/g; yf(c); return c; } int main(){ //使用 P a,b,c; cin>>f(a)>>s(a)>>f(b)>>s(b); c=a-b; cout<<f(c)<<" "<<s(c); }
OK这是个玄学代码,但你理解了就会觉得很方便(一本正经)
最后附上代码
#include<bits/stdc++.h> #define P pair<LL,LL> #define f(x) x.first #define s(x) x.second #define mp(x,y) make_pair(x,y) #define LL long long using namespace std; LL deepth;//单位分数个数 LL a,b,ans[10001],h[10001],db=10000000; bool flag; LL gcd(LL a,LL b){//最大公约数 return b==0?a:gcd(b,a%b); } void yf(P &x){//约分 LL g=gcd(f(x),s(x)); f(x)/=g;s(x)/=g; } P operator -(P a,P b){//分数减法 LL g=gcd(s(a),s(b)); P c; f(c)=f(a)*s(b)/g-f(b)*s(a)/g; s(c)=s(a)*s(b)/g; yf(c); return c; } P operator /(P a,LL b){//分数除一个整数(用于算分数的平均数) P c=a; s(c)*=b; yf(c); return c; } bool operator >(P a,P b){//分数判断大小 LL g=gcd(s(a),s(b)); return (f(a)*s(b))>(f(b)*s(a)); } void dfs(LL t,P tmp){//当前正在搜第t个单位分数,剩下的单位分数的和为tmp yf(tmp); if(t==deepth){ if(f(tmp)==1&&s(tmp)<db&&s(tmp)>h[t-1]){ //满足条件且结果更优(即最后一个单位分数分母最小) flag=1; //搜到结果的标记 db=s(tmp); for(LL j=1;j<=t-1;j++) { ans[j]=h[j]; } ans[t]=s(tmp); //记录当前结果 } return; } //以下为枚举当前单位分数的分母 LL j=h[t-1]+1;//这样保证了分母的递增 P bj=tmp/(deepth-t+1);//剩余的单位分数的平均值 while(mp(1,j)>tmp)j++; //保证当前的单位分数不超过剩余的单位分数的和 for(j=j;mp(1,j)>bj;j++){ //这里都是合法的情况,就搜下去 h[t]=j; dfs(t+1,tmp-mp(1,j)); } } int main(){ cin>>a>>b; P s=mp(a,b); yf(s); if(f(s)==1){//特判它本身就是单位分数 cout<<s(s); return 0; } for(deepth=1;;deepth++){//枚举深度 h[0]=1; dfs(1,s); if(flag){//已经找到结果 cout<<ans[1]; for(LL i=2;i<=deepth;i++) cout<<" "<<ans[i]; return 0; } } return 0; }
$ $
$ $
$ $