问题描述:
长江游乐俱乐部在长江上设置了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²),但第二种更好一些,因为在计算的过程中就能构建最优方案,而且第一种还用另一个函数计递归求解了最优方案。
下面给出一种非递归的方法,很容易知道时间复杂度也为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];
}
后面两种方法的思路是在看《算法导论》动态规划章节的过程中得来的,如果文中有错误请批评指正。