UVA12558 Egyptian Fractions (HARD version)
題目大意
迭代加深搜尋是一個應用範圍很廣的演算法,不僅可以像回溯法那樣找一個解,也可以像狀態空間搜尋那樣找一條路徑。
埃及分數問題。
在古埃及,人們使用單位分數的和(即1/a,a是自然數)表示一切有理數。例如,2/3=1/2+1/6,但不允許2/3=1/3+1/3,因為在加數中不允許有相同的。對於一個分數a/b,表示方法有很多種,其中加數少的比加數多的好,如果加數個數相
同,則最小的分數越大越好。
例如,19/45=1/5+1/6+1/18是最優方案。
輸入整數a,b(0<a<b<500),試程式設計計算最佳表示式。
題解
詳細可以檢視lrj書。本題題解出自劉汝佳演算法競賽入門經典
這道題目理論上可以用回溯法求解,但是解答樹非常“恐怖”——不僅深度沒有明顯的上界,而且加數的選擇在理論上也是無限的。換句話說,如果用寬度優先遍歷,連一層都擴充套件不完(因為每一層都是無限大的)。
解決方案是採用迭代加深搜尋(iterative deepening):從小到大列舉深度上限maxd,每次執行只考慮深度不超過maxd的結點。這樣,只要解的深度有限,則一定可以在有限時間內列舉到。
對於可以用回溯法求解但解答樹的深度沒有明顯上限的題目,可以考慮使用迭代加深搜尋(iterative deepening)。深度上限maxd還可以用來“剪枝”。按照分母遞增的順序來進行擴充套件,如果擴充套件到i層時,前i個分數之和為c/d,而第i個分數為1/e,則接下來至少還需要(a/b-c/d)/(1/e)個分數,總和才能達到a/b。例如,當前搜尋到19/45=1/5+1/100+…,則後面的分數每個最大為1/101,至少需要(19/45-1/5) / (1/101) =23項總和才能達到19/45,因此前22次迭代是根本不會考慮這棵子樹的。這裡的關鍵在於:可以估計至少還要多少步才能出解。
注意,這裡的估計都是樂觀的,因為用了“至少”這個詞。說得學術一點,設深度上限為maxd,當前結點n的深度為g(n),樂觀估價函式為h(n),則當g(n)+h(n)>maxd時應該剪枝。這樣的演算法就是IDA*。當然,在實戰中不需要嚴格地在程式碼裡寫出g(n)和h(n),只需要像剛才那樣設計出樂觀估價函式,想清楚在什麼情況下不可能在當前的深度限制下出解即可。
#include<cstdio> #include<cstring> #include<algorithm> #include<set> using namespace std; typedef long long LL; const int maxn = 1005; LL v[maxn], ans[maxn]; int maxd, kase; set<int> have; LL gcd(LL a, LL b){ return b == 0 ? a : gcd(b, a % b); } bool better(int d){ for(int i = d; i >= 0; --i) if(v[i] != ans[i]) return ans[i] == -1 || v[i] < ans[i]; return false; } int get_first(LL a, LL b){ return b / a + 1; } bool dfs(int d, int from, LL aa, LL bb){ if(d == maxd){ if(bb % aa || have.count(bb / aa)) return false; v[d] = bb / aa; if(better(d)) memcpy(ans, v, sizeof(LL) * (d + 1)); return true; } bool ok = false; from = max(from, get_first(aa, bb)); for(int i = from; ; ++i){ if(i * aa >= (maxd - d + 1) * bb) break; if(have.count(i)) continue; v[d] = i; LL b2 = bb * i; LL a2 = aa * i - bb; LL g = gcd(a2, b2); if(dfs(d + 1, i + 1, a2 / g, b2 / g)) ok = true; } return ok; } int main(){ freopen("data.in", "r", stdin); int a, b, t, n; scanf("%d", &t); while(t--){ scanf("%d%d%d", &a, &b, &n); have.clear(); for(int i = 0; i < n; ++i){ int x; scanf("%d", &x); have.insert(x); } int ok = 0; for(maxd = 1; ;maxd++){ memset(ans, -1, sizeof(ans)); if(dfs(0, get_first(a, b), a, b)) { ok = 1; break; } } printf("Case %d: %d/%d=", ++kase, a, b); if(ok){ for(int i = 0; i < maxd; ++i){ printf("1/%lld+", ans[i]); } printf("1/%lld\n", ans[maxd]); }else printf("%d/%d\n", a, b); } return 0; }