一本通例题埃及分数—题解&&深搜的剪枝技巧总结
一、简述:
众所周知,深搜(深度优先搜索)的时间复杂度在不加任何优化的情况下是非常慢的,一般都是指数级别的时间复杂度,在题目严格的时间限制下难以通过。所以大多数搜索算法都需要优化。形象地看,搜索的优化过程就像将搜索树上没用的枝条剪下来,因此搜索的优化过程又叫剪枝。剪枝的实质就是通过判断决定是否要沿当前枝条走下去。
二、搜索的剪枝必需遵循三个原则:
1、正确性(不能把正解排除,要不然搜什么呢?)2、准确性(尽可能把不能通向正解的枝条剪去)3、高效性(因为在每个枝条上都要进行一次判断,如果判断的复杂度很高,也会相应地给总复杂度“加料”)
三、剪枝的常用入手点(上面说的都是套话,下面可是干货了)
1、搜索顺序(例如有些题要求不能重复且解中最大值在所有解的最大值中最小的最优解,可以按照从小到大设计搜索,否则其他顺序不仅搜索效率低,代码相应也会很繁琐);
解释一下为什么书中说不一样的搜索顺序,相应的搜索树的形态结构也不一样:当不加任何剪枝时,最直观的体现就是树从根节点开始分支不一样多。因为最终的可行解是一样多的(先不考虑最优性排除的非最优解),而只有搜索树搜到了叶节点才会搜到可行解。如果搜索深度也一样,那么两个搜索顺序对应的搜索树即为两个深度、叶节点树相同,但节点分支的递增顺序不同(可能是一个数较浅地方的分支较小,另一个反之...)。进行剪枝后,情况就不一样了:剪枝时,若剪去的枝条规模越大,那么剪枝也就越成功。那么,在一定的深度剪枝时,对更深的地方分支多的那颗搜索树剪枝的效果就要比对另一颗更深的地方分支少的搜索树剪枝的效果好。因此在剪枝判断很难继续优化的前提下,搜索顺序也会很大程度地影响剪枝的效果。
2、搜索对象:不一样的搜索对象,一般相应的复杂度也不一样。
3、可行性剪枝:当搜索到一个节点时,发现已经不能通向解,就立即回溯。更强一步:对以后的节点,只走能通向解的节点。
*上下界剪枝:有些题仔细分析会发现一个节点的分支是有一个区间,要靠我们分析出区间的上下界,叫上下界剪枝(可行性剪枝中非常常用的一类剪枝,以至于都有自己的名字了(滑稽))。
4、最优性剪枝:当搜索到一个节点时,发现当前状态已经比已得到的最优解差,这是无论向下走的策略再怎么好,也不能比已知解更优,所以立即回溯。更强一步:发现当前状态在未来不会优于已知最优解回溯。
5、排除等效冗杂/记忆化:当发现当前状态已经搜过(记忆化),或当前状态的等效状态已经搜过,就不往下搜了。
小总结:搜索时面临的状态其实都有多个“维度”,或是说多个方面(长度,体积,位置,顺序,密度,增长率至整体的每个基础的组成元素等等),这些维度常常出现在题目描述、给出的变量、要求求的解中。仔细阅读,找到这些维度,结合题目的约束条件或这些维度自己本身的性质,思考剪枝的入手点,根据可行性、最优性或界限列出有关当前花费的不等式,即为初步剪枝。初步剪枝不足以满足AK的可以再考虑列出有关未来花费的不等式(有时用到预处理),还可以再结合各维度之间的关系(例如圆半径与面积、位置与重力势能的关系等等,能懂就好)列出判断用不等式。这不就是推导不等式嘛。
看看例题吧:
条件少得可怜,抛过来一个问题就让我们做,显然是一道搜索题。但是我们不知道到底要用多少个加数,也不知道加数的分母要枚举到多大,这就是这道题的难点。
由于决定最优解的第一条件是加数的个数,所以我们可以先枚举加数的个数limit,尝试limit个单位分数将a/b分解(迭代加深搜索)。为了避免重复搜索,假设第一个单位分数>第二个单位份数>第三个单位分数>....;即第一个分母<第二个分母<第三个分母...这样我们确定的搜索顺序——个数从小到大,对于每个个数,分母从小到大。
设当前的分母为xk,a/b还剩rest没有分解完,因为1/xk+1/xk+1+..+1/xlimit=rest,1/xk>1/xk+1>...>1/xlimit,所以1/xk>rest/(limit-k+1),即xk<(limit-k+1)/limit。因为1/xk<rest,所以xk>1/rest。因为分母按从小到大的顺序搜索,所以xk有大于xk-1。这样就进行了一波强大的上下界剪枝。
当xk>=已知解的最大的分母maxans时,再进行下去也没有意义了(因为无论如何解都不会被这条路径更新了),所以xk<maxans。又进行了一波最优性剪枝。
TIP:对于分数的存储,最好不要算出值来存,因为浮点数总是有误差的,虽然一开始的误差很小,但在在大量的运算下,很容易就把误差放得很大,很大,很大(重要的事情说三遍),所以最好用分子分母存储法。但也注意分子分母可能也很大,很大,很大(重要的事情说三遍),因此别忘了数据类型开大点,能约分就约分(约分有时还能直接撞到最优解)。
AC代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<algorithm> 5 #include<cstring> 6 7 using namespace std; 8 9 int bj,len; 10 long long ans[100000],an[100000],maxans=0x7fffffffffffff; 11 12 long long gcd(long long a,long long b) 13 { 14 return a%b?gcd(b,a%b):b; 15 } 16 17 int ok(int k)//判断是否更优 18 { 19 if(!len) 20 { 21 len=k; 22 bj=1; 23 maxans=an[k]; 24 return 1; 25 } 26 if(an[k]<maxans) 27 { 28 maxans=an[k]; 29 return 1; 30 } 31 return 0; 32 } 33 34 void dfs(int limit,int lst,int k,long long zi,long long mu) 35 { 36 if(k==limit) 37 { 38 if(zi==1&&mu>an[k-1]) 39 { 40 an[k]=mu; 41 if(ok(k)) memcpy(ans,an,sizeof(long long) * (k+1)); 42 } 43 return; 44 } 45 long long li=(limit-k+1)*mu/zi-((limit-k+1)*mu%zi==0); 46 long long a1,b1; 47 for(int i=max((long long)lst+1,mu/zi+1);i<=li&&i<maxans;i++)//上下界剪枝+最优性剪枝 48 { 49 an[k]=i; 50 a1=mu*i;//模拟分数通分再加减的过程 51 b1=zi*i-mu; 52 long long g=gcd(b1,a1); 53 dfs(limit,i,k+1,b1/g,a1/g); 54 } 55 } 56 57 int main() 58 { 59 // freopen("fraction.in","r",stdin);//调试用 60 // freopen("fraction.out","w",stdout); 61 long long a,b; 62 cin>>a>>b; 63 long long g=gcd(a,b); 64 a/=g;b/=g; 65 if(a==1) 66 { 67 cout<<b; 68 return 0; 69 } 70 for(int i=2;;i++)//枚举加数个数 71 { 72 dfs(i,1,1,a,b); 73 if(bj) break; 74 } 75 for(int i=1;i<=len;i++) 76 printf("%lld ",ans[i]); 77 return 0; 78 }