1. 程式人生 > >UVA12558 Egyptian Fractions (HARD version)

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;
}