埃及分數問題 【IDA*】
埃及分數問題
- 題意
- 分析
- 思路
- 參考
- 程式碼
題意
在古埃及,人們使用單位分數的和(即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
分析
本題可以用dfs回溯來求解。但是由於本題沒有指明等式數目即深度,如果dfs搜尋的話是沒有上限的,換句話說,如果用寬度優先遍歷,連一層都擴充套件不完(因為每一層都是無限大的)。所以需要列舉深度,直到找到跳出即可,即迭代深度搜索。
深度上限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),只需要像剛才
那樣設計出樂觀估價函式,想清楚在什麼情況下不可能在當前的深度限制下出解即可。
思路
(1)列舉深度maxd
(2)dfs搜尋:從滿足1/c<=a/b的最小c即b/a+1開始列舉分母,深度d為表示第幾個分數;每次計算的是a/b - 1/i = a2/b2 然後將a2,b2再作為a,b進行遞迴。
(3)剪枝:if(bb * (maxd+1-d) <= i*aa) break;//剪枝:如果剩下的maxd+1-d個分數全部都是1/i,加起來仍然不超過aa/bb,則無解! (maxd+1-d)/i <= aa/bb
參考
入門經典-埃及分數-P
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
const int maxn = 1000;
LL ans[maxn],v[maxn];
int maxd,kcase;
int gcd(LL a,LL b){//最大公約數,用於約分
LL temp;
while(b){temp = b;b = a % b;a = temp;}
return a;
}
LL get_first(LL a, LL b){//滿足1/c<=a/b的最小c
return b/a + 1;
}
bool better(int d){//更新最優值:如果當前解v比目前最優解ans更優,更新ans
for(int i=d;i>=0;i--)
if(v[i] != ans[i])
return ans[i] == -1 || v[i] < ans[i];
return false;
}
//當前深度為d,分母不能小於from,分數之和恰好為aa/bb
bool dfs(int d,int from,LL aa,LL bb){
if(d == maxd){//深度達到當前列舉個數
if(bb % aa) return false;//aa/bb必須是埃及分數
v[d] = bb/aa;//儲存分母
if(better(d)) memcpy(ans,v,sizeof(LL)*(d+1));//更新最優解
return true;//返回成功
}
bool ok = false;//用於返回本次遞迴的結果
from = max((LL)from,get_first(aa,bb));//列舉的起點
for(int i=from; ;i++){
if(bb * (maxd+1-d) <= i*aa) break;//剪枝:如果剩下的maxd+1-d個分數全部都是1/i,加起來仍然不超過aa/bb,則無解! (maxd+1-d)/i <= aa/bb
v[d] = i;//儲存分母
//計算aa/bb - 1/i,設結果為a2/b2
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;}
void solve(int a,int b){
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=",++kcase,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);
}
int main()
{
int a,b;
while(scanf("%d%d",&a,&b)!=EOF)
solve(a,b);
return 0;
}