长江游乐俱乐部在长江上设置了n个游艇出租站,游客可以在这些游艇出租站用游艇,并在下游任何一个游艇出租站归还游艇,游艇出租站i到j之间的租金是rent(i,j),其中1<=i<j<=n。试设计一个算法使得游客租用的费用最低。
这是一道典型的动态规划问题。解题的思路是,既然要得到最小的花费,那么就从最底层开始,逐层向上计算每两个站之间的最小花费,并记录在数组中,有记录的就不必再算了。其中两个站可能是相邻的,也可能不是相邻的。然后要得到方案,就需要对费用进行递归检测,比如:如果J、K两站之间的最小费用为M,但是J、K两站之间的某一站P满足:JP最小费用+PK最小费用等于JK最小费用,同时JK最小费用不等于由J直接到K的费用,则这个P站肯定是其中一个租船点。如果JK最小费用等于由J直接到K的费用,那么J和K必定是两个租船点,且其中没有租船点。在递归的过程中将这些租船点记录下来,就得到了最优方案。
代码如下:
#include <iostream> #include <memory.h> #include <stdio.h> #define N 200 using namespace std; int p[N][N]; //p[i][j]为i到j的最小费用 int r[N][N]; //存储数据 int mark[N][N]; //记录是否已经计算过最小费用,避免重复计算 int smallestFee(int start, int ende); //求最小费用 void findPath(int start, int ende); //求最小费用方案 int answer[N]; int main() { int n; while(1 == scanf("%d", &n)) //读取数据 { memset(answer, 0, sizeof(answer)); memset(mark, 0, sizeof(mark)); for(int i = 1; i <= n - 1; ++i) { for(int j = i + 1; j <= n; ++j) scanf("%d", &r[i][j]); } smallestFee(1, n); printf("Smallest cost: %d\n", p[1][n]); //输出最小费用 //找出最优解 findPath(1, n); printf("Path: 1->"); for(int i = 1; i < n; ++i) if(answer[i] != 0) printf("%d->", answer[i]); printf("%d\n\n", n); while(getchar() != '\n') //剔除空格,为下一次测试做准备 continue; } return 0; } int smallestFee(int start, int ende) { if(start + 1 == ende) //分解到只剩下2个站 { p[start][ende] = r[start][ende]; mark[start][ende] = 1; return r[start][ende]; } p[start][ende] = r[start][ende]; //假设直接从start到ende为最小花费 int k, x1, x2; for(k = start + 1; k < ende; ++k) //找从start到ende的最小花费 { //计算过最小费用则不用再次计算 if(mark[start][k] != 1) x1 = smallestFee(start, k); else x1 = p[start][k]; if(mark[k][ende] != 1) x2 = smallestFee(k, ende); else x2 = p[k][ende]; if(p[start][ende] > x1 + x2) p[start][ende] = x1 + x2; } mark[start][ende] = 1; //已经计算过标记为1 return p[start][ende]; } void findPath(int start, int ende) { if((start + 1 == ende) || (p[start][ende] == r[start][ende])) //寻找到相邻位置,或者已经是最便宜,记录位置 { answer[ende] = ende; return; } for(int k = start + 1; k < ende; ++k) { if(p[start][k] + p[k][ende] == p[start][ende]) { findPath(start, k); findPath(k, ende); return; //找到了直接返回 } } return; }
然而上面这种简单的带备忘的朴素分治算法,效率并不高,最差劲的是在求最小花费的过程中,不能同时构建最优方案。下面给出一种更好的方法。
这种方法假定第一次还的位置为k,从1直接到k是最优的(k从2到n循环)。然后递归调用此方法,求得k到n的最优方案和花费。这种方法是一种自顶向下的计算方法,也用到了备忘录。待会儿再给出一种非递归的自底向上的方法。
#include <iostream> #include <memory.h> #include <stdio.h> #define N 200 using namespace std; int r[N][N]; //存储数据 int p[N][N]; //记录最小花费 int smallestFee(int start, int n); //求最小费用 int answer[N]; int main() { int n; while(1 == scanf("%d", &n)) //读取数据 { memset(answer, 0, sizeof(answer)); memset(p, 0, sizeof(p)); for(int i = 1; i <= n - 1; ++i) { for(int j = i + 1; j <= n; ++j) scanf("%d", &r[i][j]); } printf("Smallest cost: %d\n", smallestFee(1, n)); //输出最小费用 //输出方案 printf("Path: 1->"); for(int i = 2; i < n; ++i) if(answer[i] != 0) printf("%d->", answer[i]); printf("%d\n\n", n); while(getchar() != '\n') //剔除后续字符,为下一次输入做准备 continue; } return 0; } /*自顶向下递归计算*/ int smallestFee(int start, int n) { if(start == n) { p[start][n] = r[start][n]; return 0; } int smallest = 1 << 10; int x; for(int k = start + 1; k <= n; ++k) { int temp = r[start][k]; if(p[k][n] != 0) //如果计算过就不必再计算 temp += p[k][n]; else temp += smallestFee(k, n); if(temp < smallest) { smallest = temp; x = k; } } answer[x] = x; //记录最优的归还站点位置 p[start][n] = smallest; //记录最优花费 return smallest; }
下面给出一种非递归的方法,很容易知道时间复杂度也为O(n²),但是不需要备忘录,其在求解最小费用时,同时能够构造最优解。这是一种自底向上的方法。假若把n个站从左到右排成一排,左边为1(起点),右边为n(终点)。先计算1到k的最优值,在根据1到k的最优值,计算1到k+1的最优值,最后得到1到n的最优值。
代码如下:
#include <iostream> #include <memory.h> #include <stdio.h> #define N 200 using namespace std; int r[N][N]; //存储费用数据 int p[N][N]; //记录最小花费 int smallestFee(int start, int n); //求最小费用 int answer[N]; int main() { int n; while(1 == scanf("%d", &n)) //读取数据 { memset(answer, 0, sizeof(answer)); memset(p, 0, sizeof(p)); for(int i = 1; i <= n - 1; ++i) { for(int j = i + 1; j <= n; ++j) scanf("%d", &r[i][j]); } printf("Smallest cost: %d\n", smallestFee(1, n)); //输出最小费用 //输出方案 printf("Path: 1->"); for(int i = 2; i < n; ++i) if(answer[i] != 0) printf("%d->", answer[i]); printf("%d\n\n", n); while(getchar() != '\n') //剔除后续字符,为下一次输入做准备 continue; } return 0; } /*自底向上计算*/ int smallestFee(int start, int n) { //这两层循环,由左至右,计算了在下次停靠归还游艇之前的最小费用 for(int i = 2; i <= n; ++i) { int x = 2 << 10; int temp; for(int j = 1; j < i; ++j) { //如果从1到j站的最小费用加上从j直接到i站的费用比之前的最优方案更优,则选择这种方案 if(r[j][i] + p[1][j] < x) { x = r[j][i] + p[1][j]; temp = j; } } p[1][i] = x; //记录最少花费 answer[temp] = temp; //记录最优方案的归还站点 } return p[start][n]; }