IDDFS学习笔记-埃及分数
? ? ? ? 迭代加深搜索算法(iterative deepening depth-first search (IDS or IDDFS)) 可以被視作是廣度優先搜索算法(BFS)和深度優先搜索算法(DFS)的結合。常用于解決在廣度很深度上都無限的問題,最經典的便是埃及分數:
在古埃及,人們使用單位分數的和(形如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),編程計算最好的表達方式。
Input:
a b
Output:
一個等式
Sample Input:
3 4
Sample Output:
3/4 = 1/2 + 1/4? ??
? ? ? ? 埃及分數問題的廣度(單位分數的大小)和深度(單位分數的求和數量)都是無限的。在單純的DFS問題中,很多時候剪枝技術只是一種優化,但在IDDFS中,剪枝直接限定了合理有效的邊界,使搜索在有限的范圍內進行,使得回溯和搜索都成為了可能。因此在IDDFS中,如何剪枝成為了不可繞過的重要話題,直接決定了是否能合理地解決問題。
? ? ? ? 簡要回顧以下IDDFS的思想:逐漸增加DFS的遞歸深度,同時每一層都采用BFS進行搜索,可以說IDDFS就是用DFS串起來的BFS,又或者用筆者在DFS筆記中的類比,DFS是”動態循環“,普通的DFS的循環體只是一個簡單判斷,而IDDFS中的循環體就是一個BFS。
? ? ? ? 本題存在兩個維度的最優解情況:1、和式中的分數最少,2、和式中的最小的分數最大(分母最小),其中2是在1的條件下進行判斷。考慮到IDDFS中DFS和BFS的從屬關系,自然1應當由DFS處理,而2則是BFS。
? ? ? ? 接下來具體分析一下題目,首先本題采用分數作為結果,為了保證過程沒有誤差,采用浮點型進行計算是錯誤且復雜的,會牽扯到小數和分數的轉換問題。在本題中,對于分數而言分子和分母成對出現,不如用pair<int,int>來儲存它們,first是分子,second是分母。在此基礎上,需要重新定義這種特殊類型的分數的求和函數:
int gcd(int a,int b) //輾轉相除法求公約數 {return b==0 ? a : gcd(b,a%b);} pair<int,int> sum(pair<int,int> a, pair<int,int> b){pair<int,int> result;int c = a.first*b.second+b.first*a.second;int d = a.second*b.second;int g = abs(gcd(d,c));c /= g; d /= g;result.first = c; result.second = d;return result; }? ? ? ? 有了求和方法之后似乎分數運算變得可行,如果要比大小的話只需要將其中一個的first*(-1)傳入,再比較結果的符號即可。同時本題需要輸出一個和式,這個和式再DFS的過程中也需要被跟蹤,所以不妨先放出兩個字符串和整型互轉函數:
string ItS(int num){string str;stringstream ss;ss<<num;ss>>str;return str; } int StI(string str){int num;stringstream ss;ss<<str;ss>>num;return num; }? ? ? ? 準備好基本工具后我們來理一下解題思路:先試2個分數的和,再試3個分數的和,再...直到在第n個分數的和找到了我們想要的結果a/b,便解決了最優解條件1,再在這一層中繼續尋找別的可能解且不再深入,最后根據最優解條件2排序,輸出最優解。
? ? ? ? 從深度上說,IDDFS中DFS的部分往往需要兩個基本參數(now和deep),now是當前的遞歸深度,deep是最深的遞歸深度,一旦now=deep則返回,deep+1后再繼續,deep則有主函數給出。所以IDDFS的遞歸思路是從0-1,再0-2,再0-3...直到解決問題。
? ? ? ? 埃及分數是恒有解問題,這一點在題目中也有暗示(沒有impossible的情況),且大部分IDDFS的問題在深度上的無限正體現在這一點上。因此我們不用擔心由于深度無限而無法跳出,我們總會找到合適的解,并且事實上它不會在深度上花費太多。
? ? ? ? 而在廣度上,我們需要不斷地求和,并記錄這些和的結果,運用到下一層的BFS中去,為了簡單說明這個問題的思路,我們將算法中位于+號左邊的稱為“頭”,位于+號右邊的稱為“尾”,作為暴力算法,遍歷求和的思路便是將頭作為外循環,尾作為內循環,儲存求和結果,下面用一個代碼解釋這個思路:
for(int head=0;head<10;head++){for(int tail = head+1;tail<10;tail++){int sum = head + tail;} }? ? ? ? 在IDDFS中,這個head來自于上一層的BFS中,而這個tail正是當前層的BFS給出的遍歷,而sum則是當前層BFS的搜索用元。DFS則在其中擔任傳遞head的作用,并且決定傳遞幾層。
? ? ? ? 了解了BFS和DFS再IDDFS中分別擔任的角色和它們互相作用的原理關系,接下來我們需要解決能使整個解決過程變得可行,變得不再無窮的核心問題——如何剪枝。
? ? ? ? 事實上在這里稱剪枝或許并不準確,畢竟剪枝是DFS中用于避免無效遞歸的手段,而在IDDFS中,實際上我們更關注給BFS進行“劃界”,使得BFS在一個合理的范圍內搜索,剩余的無窮的無效范圍是不必要的。
? ? ? ? 在本題中的“劃界”方法是:用遞增順序枚舉分數,如果接下來的遞歸層數(deep-now)全加上目前這個分數(1/n)仍然小于目標值(a/b),說明剩下的分數枚舉是無效的,可以直接結束枚舉,劃出右界,同時如果當前分數比目標分數大,則同樣不需要枚舉,劃出左界。給出檢測函數:
int check(pair<int,int>now, int after, int deep){//after是小于當前枚舉分數的最大分數的分母,pair<int,int> p; //deep為遞歸深度,同時也是求和寬度 pair<int,int>temp;p.first = deep; p.second = after;temp.first = a*(-1); temp.second = b;if(sum(now,temp).first>0) return 2; //左界以左p = sum(now,p);p = sum(p,temp);return(p.first>=0); //右界以右 }? ? ? ? 之所以check函數不用bool型而是用int型,是因為需要區分左界以左,右界以右和在范圍內三種情況,在住主函數中需要另外處理它們,保證主函數給出的head是左界,使IDDFS效率更高。
? ? ? ? 有了IDDFS的基礎模塊check劃界后,我們繼續解決題目中的具體問題。
? ? ? ? 首先是記錄每個head的加合路徑,這個路徑將再最后被調用輸出,簡單寫一個字符串函數,同時用map<pair<int,int>,stirng>來儲存它們:
string road(string pre, pair<int,int> next){string str;str = pre + " + " + ItS(next.first) + "/" + ItS(next.second);return str; }? ? ? ? 另外,在BFS中tail的左值取決于head的road中的最后一個分數,并且如果最后目標值有多解,我們還需要比較它們最后一個分數的大小,所以需要一個提取最后一個分數分母的函數:
int GetStringTail(string str){ //找到分數加和式中的最后一個分母 int num;string st;for(string::reverse_iterator it=str.rbegin();it!=str.rend();it++){if((*it)=='/') break; st+=(*it); }reverse(st.begin(),st.end());stringstream ss;ss<<st;ss>>num;return num; }? ? ? ? ? 最后還有一些細節:在讀入目標分數后,首先進行化簡,不然分母是錯誤的。如果目標分數的分子是1(已經是埃及分數),避免麻煩,直接輸出即可。
? ? ? ? 整合它們!
#include<bits/stdc++.h> using namespace std; int a, b; bool findthedeep; map<pair<int,int>,string> m; vector<string> result; int gcd(int a,int b) //輾轉相除法求公約數 { return b==0 ? a : gcd(b,a%b);} pair<int,int> sum(pair<int,int> a, pair<int,int> b){pair<int,int> result;int c = a.first*b.second+b.first*a.second;int d = a.second*b.second;int g = abs(gcd(d,c));c /= g; d /= g;result.first = c; result.second = d;return result; } string ItS(int num){string str;stringstream ss;ss<<num;ss>>str;return str; } int StI(string str){int num;stringstream ss;ss<<str;ss>>num;return num; } int check(pair<int,int>now, int after, int deep){ //after是小于當前枚舉分數的最大分數的分母,pair<int,int> p; //deep為遞歸深度,同時也是求和寬度 pair<int,int>temp;p.first = deep; p.second = after;temp.first = a*(-1); temp.second = b;if(sum(now,temp).first>0) return 2;p = sum(now,p);p = sum(p,temp);return(p.first>=0); } string road(string pre, pair<int,int> next){string str;str = pre + " + " + ItS(next.first) + "/" + ItS(next.second);return str; } int GetStringTail(string str){ //找到分數加和式中的最后一個分母 int num;string st;for(string::reverse_iterator it=str.rbegin();it!=str.rend();it++){if((*it)=='/') break; st+=(*it); }reverse(st.begin(),st.end());stringstream ss;ss<<st;ss>>num;return num; } void IDDFS(pair<int,int> top, int now, int deep){ if(now == deep) return;queue<pair<int,int>> q; int k = 0; int s = GetStringTail(m[top]);for(int i=s+1;;i++){if(check(top,i,deep-now) != 1) break;pair<int,int> num;num.first = 1; num.second = i;q.push(num);k++;}pair<int,int> head, next;while(k--){head = q.front();q.pop();next = sum(top,head);q.push(next);m[next] = road(m[top],head);if(next.first == a && next.second == b){result.push_back(m[next]);findthedeep = 1;}}while(!q.empty()){IDDFS(q.front(),now+1,deep);q.pop();}return; } int main(){cin>>a>>b;cout<<a<<"/"<<b<<"=";int c = gcd(a,b);a /= c; b /= c; //化簡 if(a==1){cout<<a<<"/"<<b;return 0;}queue<pair<int,int>> q;pair<int,int> top;for(int i=2;;i++){for(int t=2;;t++){top.first = 1; top.second = t;if(check(top,top.second+1,i) == 0) break;if(check(top,top.second+1,i) == 2) continue;q.push(top);}while(!q.empty()){m.clear();top = q.front();q.pop();m[top] = "1/" + ItS(top.second); IDDFS(top,1,i); }if(findthedeep) break;}int max = 0;string res;for(int i=0;i<result.size();i++){int num = GetStringTail(result[i]);if(max>num || max==0){max = num;res = result[i];}}cout<<res<<endl;return 0; }? ? ? ? 最后的補充:在IDDFS中,實際上是對DFS和BFS的配合使用,主要分為以下3個要點:
? ? ? ? 1、BFS:依然是找到核心隊列,注意結合DFS的遞歸深度劃分左右界。
? ? ? ? 2、DFS:依然是找到核心遞歸,注意逐步加深遞歸深度。
? ? ? ? 3、DFS給BFS傳遞枚舉head,BFS負責找到最優解。
? ? ? ? 4、IDDFS的核心是變化深度上限的DFS,BFS很多時候是被忽視甚至是可以代替的。
? ? ? ? 5、IDDFS要注意逐漸加深的深度的數據意義,能夠幫助快速架構算法邏輯。
總結
以上是生活随笔為你收集整理的IDDFS学习笔记-埃及分数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: T20服务器raid新增硬盘,戴尔T20
- 下一篇: 山东大学计算机考研资料汇总