有時候,我們可以將計算好的值進行存儲,需要的時候取出,這樣可以大大降低計算量,用空間代替時間。
我們從一個問題出發,農夫John和他的朋友們一同去參加Cownty展會,這個展會的門票是50元,排隊購票的人
有2n個人,其中n個人拿著100元的鈔票,另外n個人拿著50元的鈔票,農夫john想知道在這種情況下著2n個人共有
多少種排隊的方式,使得售票處在不準備零錢的情況下,也能把票賣給這2n個人,而不會出現找不開錢的局面。
這是一個經典的組合問題,最後可以通過求解catalan數一步解決,我們這裡通過深入搜索,寫通項和存儲計算三種方法來實現。
通過實現,存儲計算的這種方法最好,但需要對計算的順序精心設計。
在動態規劃等很多場合都會用到將計算存儲在一個數組中,此前的關於pfordelta的實現中也出現了將計算存儲在數組中的方式,這是很常見的技巧,該問題前兩種方法只所以慢一方面是因為大量重複冗餘的計算,另一方面是過多的跳轉,通過第三種方法改寫後,計算的效率大大提高了,沒有組合數學背景的同學仔細研讀,應該也可以讀懂,該題來自吳文虎的《程序設計中的組合數學》,實現也部分參考了這本書,但有較多不同。
#include <stdio.h>
#if defined(__i386__)
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile("rdtsc" : "=A"(x));
return x;
}
#elif defined(__x86_64__)
static __inline__ unsigned long long rdtsc(void)
{
unsigned hi, lo;
__asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi));
return ((unsigned long long)lo) | (((unsigned long long)hi) << 32);
}
#endif
int all_n = 20;
int five_cnt = 10;
int ten_cnt = 10;
int dfs(int i, int change_cnt, int fi, int tj,
int& all_cases_cnt) //fi表示拿50元鈔票的人數,tj表示拿100元鈔票的人數
{
if (i == 1) {
if (change_cnt == 1 && tj == 1 && fi == 0) {
all_cases_cnt += 1;
tj -= 1;
} else if (change_cnt == 0 && tj == 0 && fi == 1) {
all_cases_cnt += 1;
fi -= 1;
}
} else {
for (int k = 0; k < 2; ++k) {
if (k == 0) {
if (fi >= 1) {
dfs(i - 1, change_cnt + 1, fi - 1, tj, all_cases_cnt);
}
}
if (k == 1) {
if (tj >= 1) {
if (change_cnt > 0) {
dfs(i - 1, change_cnt - 1, fi, tj - 1, all_cases_cnt);
}
}
}
}
}
};
int f(int m, int n)
{
if (m < n) { //50元的人數少於100元的人數必然無解
return 0;
} else if (n ==
0) { //100元的人已經不存在了,後續的都是50元的算1種
return 1;
} else {
return f(m, n - 1) + f(m - 1,
n); //否則方案數為先用100元的,再用50元的方案之和.
}
}
int fast_f(int m, int n)
{
int fast[m + 1][n + 1];
for (int i = 0; i <= n;
++i) { //直接改寫自f函數的第一個條件分支
for (int j = 0; j < i && j <= m; ++j) {
fast[j][i] = 0;
}
}
for (int i = 0; i <= m;
++i) { //直接改寫自f函數的第二個分支,計算可以合併到上一個循環中,這裡為清晰起見,拆開。
fast[i][0] = 1;
}
for (int i = 1 ; i <= m;
++i) { //這一步的計算有很高的技巧,每一層的計算依賴的另外兩個數恰好都是已經計算好的
for (int j = 1; j <= i && j <= n; ++j) {
fast[i][j] = fast[i][j - 1] + fast[i - 1][j];
}
}
return fast[m][n - 1] + fast[m - 1][n];
}
int main(void)
{
int all_cases_cnt = 0;
int start = rdtsc();
dfs(all_n, 0, five_cnt, ten_cnt, all_cases_cnt);
int end = rdtsc();
printf("方案數%d,cycle:%d/n", all_cases_cnt, end - start);
start = rdtsc();
all_cases_cnt = f(five_cnt, ten_cnt);
end = rdtsc();
printf("方案數%d,cycle:%d/n", all_cases_cnt, end - start);
start = rdtsc();
all_cases_cnt = fast_f(five_cnt, ten_cnt);
end = rdtsc();
printf("方案數%d,cycle:%d/n", all_cases_cnt, end - start);
return 0;
}
關於購票問題其實是一個組合數學的問題,有通解可以直接求出。
我們假定X軸為手持50元的人,Y軸為手持100元的人,那麼一個正確的解等價於從(0,0)到(n,n)的格路問題,每次只能走一格,要麼X加1,要麼Y加1,如下所示的一條紅線為一個8個人的解,即{50,50,100,100,50,50,100,100},先來2個50元的購票者,在來2個100元的購票者,與不例舉。
由格路問題的定義,從(0,0)到(n,n)的方案數如下圖,但同時要扣除跨過y=x這條對角線的解方案,這等價於從(1,-1)到(n,n)的方案數。因此解得結果為兩數之差,這個值就是Catalan數,因此沒有必要向我們上面給出的方法,而是直接計算即可,在求解階層的實際計算中為了防止大數溢出,可以遍歷一遍找到乘積中2和5的個數,湊成1對,結果末尾就加個0,這樣可以避免一定程度的溢出。
當然本文主要是為了介紹存儲計算的技巧,借用了這個例子,上文中我們用存儲計算的方法把著每一個方案都枚舉出來。存儲計算就介紹到這裡。