问题描述
长江游艇俱乐部在长江上设置了 n 个游艇出租站,游客可以在这些游艇出租站租用游艇,并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为r(i,j),1≤i<j≤n。试设计一个算法,计算从游艇出租站 i 到出租站 j 所需的最少租金。
问题分析
长江游艇俱乐部在长江上设置了 n 个游艇出租站,游客可以在这些出租站租用游艇,并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为 r(i,j)。现在要求出从游艇出租站 1 到游艇出租站 n 所需的最少的租金。
当要租用游艇从一个站到另外一个站时,中间可能经过很多站点,不同的停靠站策略就
有不同的租金。那么我们可以考虑该问题,从第 1 站到第 n 站的最优解是否一定包含前 n−1的最优解,即是否具有最优子结构和重叠性。如果是,就可以利用动态规划进行求解。
如果我们穷举所有的停靠策略,例如一共有 10 个站点,当求子问题 4 个站点的停靠策略时,子问题有(1,2,3,4),(2,3,4,5),(3,4,5,6),(4,5,6,7),(5,6,7,8),(6,7,8,9),(7,8,9,10)。如果再求其子问题 3 个站点的停靠策略,(1,2,3,4)产生两个子问题:(1,2,3),(2,3,4)。(2,3,4,5)产生两个子问题:(2,3,4),(3,4,5)。如果再继续求解子问题,会发现有大量的子问题重叠,其算法时间复杂度为,暴力穷举的办法是很不可取的。
下面分析第 i 个站点到第 j 个站点(i,i+1,…,j)的最优解(最少租金)问题,考查是否具有最优子结构性质。
(1)分析最优解的结构特征
• 假设我们已经知道了在第 k 个站点停靠会得到最优解,那么原问题就变成了两个子
问题:(i,i+1,…,k)、(k,k+1,…,j)。如图 4-32 所示。
• 那么原问题的最优解是否包含子问题的最优解呢?
假设第 i 个站点到第 j 个站点(i,i+1,…,j)的最优解是 c,子问题(i,i+1,…,k)的最优解是 a,子问题(k,k+1,…,j)的最优解是 b,那么 c=a+b,无论两个子问题的停靠策略如何都不影响它们的结果,因此我们只需要证明如果 c是最优的,则 a 和 b 一定是最优的(即原问题的最优解包含子问题的最优解)。
(2)建立最优值的递归式
• 用 m[i][j]表示第 i 个站点到第 j 个站点(i,i+1,…,j)的最优值(最少租金),那 么两个子问题:(i,i+1,…,k)、(k,k+1,…,j)对应的最优值分别是 m[i][k]、 m[k][j]。
• 游艇租金最优值递归式:
(3)自底向上计算最优值,并记录
先求两个站点之间的最优值,再求 3 个站点之间的最优值,直到 n 个站点之间的最优值。
(4)构造最优解
上面得到的最优值只是第 1 个站点到第 n 个站点之间的最少租金,并不知道停靠了哪些站点,我们需要从记录表中还原,逆向构造出最优解。
算法设计
采用自底向上的方法求最优值,分为不同规模的子问题,对于每一个小的子问题都求最
优值,记录最优策略,具体策略如下。
(1)确定合适的数据结构
采用二维数组 r[][]输入数据,二维数组 m[][]存放各个子问题的最优值,二维数组 s[][]存放各个子问题的最优决策(停靠站点)。
(2)初始化
根据递推公式,可以把 m[i][j]初始化为 r[i][j],然后再找有没有比 m[i][j]小的值,如果有,则记录该最优值和最优解即可。初始化为:m[i][j]=r[i][j],s[i][j]=0,其中,i=1,2,…,n,j=i+1,i+2,…,n。
(3)循环阶段
• 按照递归关系式计算 3 个站点 i,i+1,j(j=i+2)的最优值,并将其存入 m[i][j],同时将最优策略记入 s[i][j],i=1,2,…,n−2。
• 按照递归关系式计算 4 个站点 i,i+1,i+2,j(j=i+3)的最优值,并将其存入 m[i][j],同时将最优策略记入 s[i][j],i=1,2,…,n−3。
• 以此类推,直到求出 n 个站点的最优值 m[1][n]。
(4)构造最优解
根据最优决策信息数组 s[][]递归构造最优解。s[1][n]是第 1 个站点到第 n 个站点(1,2,…,n)的最优解的停靠站点,即停靠了第 s[1][n]个站点,我们在递归构造两个子问题(1,2,…,k)和(k,k +1,…,n)的最优解停靠站点,一直递归到子问题只包含一个站点为止。
代码
#include<iostream>
using namespace std;
const int ms = 1000;
int r[ms][ms],m[ms][ms],s[ms][ms]; //i 到 j 站的租金
int n; //共有 n 个站点
void rent()
{
int i,j,k,d;
for(d=3;d<=n;d++) //将问题分为小规模为 d
{
for(i=1;i<=n-d+1;i++)
{
j=i+d-1;
for(k=i+1;k<j;k++) //记录每一个小规模内的最优解
{
int temp;
temp=m[i][k]+m[k][j];
if(temp<m[i][j])
{
m[i][j]=temp;
s[i][j]=k;
}
}
}
}
}
void print(int i,int j)
{
if(s[i][j]==0 )
{
cout <<j;
return ;
}
print(i,s[i][j]);
print(s[i][j],j);
}
int main()
{
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=i+1;j<=n;++j)
{
cin>>r[i][j];
m[i][j]=r[i][j];
}
rent();
cout <<m[1][n] << endl;
cout <<1;
print(1,n);
return 0;
}
算法复杂度分析
(1)时间复杂度:由程序可以得出:语句 temp=m[i][k]+m[k][j],它是算法的基本语句,
在 3 层 for 循环中嵌套,最坏情况下该语句的执行次数为 O(),print()函数算法的时间主要取决于递归,最坏情况下时间复杂度为 O(n)。故该程序的时间复杂度为 O()。
(2)空间复杂度:该程序的输入数据的数组为 r[][],辅助变量为 i、j、r、t、k、m[][]、s[][],空间复杂度取决于辅助空间,该程序的空间复杂度为 O()。
算法优化拓展
如果只是想得到最优值(最少的租金),则不需要 s[][]数组;m[][]数组也可以省略,直
接在 r[][]数组上更新即可,这样空间复杂度减少为 O(1)。