动态规划:游艇租用问题

问题描述:

长江游乐俱乐部在长江上设置了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];
}

后面两种方法的思路是在看《算法导论》动态规划章节的过程中得来的,如果文中有错误请批评指正。


你可能感兴趣的:(递归,动态规划,游艇租用问题)