编程之美1.6 饮料供货[动态规划vs贪心算法]

饮料供货是一个求最优解问题。需要在给定最大容量V的前提下,从不同容量不同满意度的饮料中选择满意度最大的集合。

1. 动态规划

动态规划是最常用的解决最优化问题的方法,很容易应用到本题的需求中。用f[V,i]表示从第i,i+1,i+2,...,n-1种饮料中,算出总量为V的方案中满意度之和的最大值。

动态规划方程为:f[V,i] = max{k*Hi + f[V-k*Vi, i+1]}

弄清解法之后,我们不妨来分析一下问题的最优子结构。

根据《算法导论》描述,最优子结构有两种变形:

     1) 有多少个问题在原问题的最优解中,

     2) 在决定一个最优解中使用哪些子问题时有多少个选择。

在饮料供货问题中,一个最优解只使用了一个子问题,但为了确定最优解,我们必须考虑Ci(第i中饮料可选的最大数量)种选择。

 

2. 贪心算法

书中提到第二种解法,贪心算法。由于所有饮料的容量都是2的整数幂,这就给贪心创造了条件。

假设给定的容量为V,我们可以把V写成二进制的形式,不妨设V=7,二进制的写法为111。

接下来我们就从最低位开始取:

第一步,取第一个1:拿出容量为1满意度最大的饮料,

第二步,取第二个1:使用剩余容量为1的饮料构造容量为2的饮料,比如(1,2)(1,3)构造出(2,5),并从新构造的和原有容量为2的所有饮料中选出满意度最大的。

第三步,取第三个1:递归构造容量为4的饮料,并从新构造的和原有容量为4的所有饮料中选出满意度最大的。

 

3.定义饮料类

首先需要定义一个饮料的类:

    struct Drink
    {
        int Volume;
        int TotalCount;
        int Happiness;
        Drink(int v, int t, int h)
        {
            Volume = v;
            TotalCount = t;
            Happiness = h;
        }
    };

建立一个存储饮料的全局动态数组,用于存放所有饮料,由于同时兼顾动态规划和贪心算法两种方法,我们使用了一个二维数组。

drink[i][j]表示容量为2^i的第j个饮料。

    vector<vector<Drink> > drinks;
    void initialize()
    {
        ifstream fin("drink.txt");
        int v,t,h;
        
        while(fin>>v>>t>>h)
        {
            while(drinks.size()<=v)
            {
                drinks.push_back(vector<Drink>());
            }
            drinks[v].push_back(Drink((int)pow(2,v),t,h));
        }
        fin.close();
    }

创建样例输入drink.txt。

0 2 2
0 2 3
0 3 5
0 3 4
1 2 6
1 3 5
1 4 4
2 1 18
2 2 12

 接下来使用两种方法求解当V=7时的最高满意度值。

 

4.动态规划解法

    const int MAXV=101;
    const int MAXT=101;
    int opt[MAXV][MAXT];
     
    // 动态规划
    int GetMaxHappinessByDP1(int V)
    {
        int maxHappiness = 0;
        vector<Drink> temp;
        for (int i=0; i<drinks.size(); ++i)
            for(int j=0; j<drinks[i].size(); ++j)
                temp.push_back(drinks[i][j]);
     
        int T = temp.size();
     
        // init
        for(int i=0; i<=T; ++i)
            opt[0][i] = 0;
        for (int i=0; i<=V; ++i)
            opt[i][T] = 0;
     
        for (int i=T-1; i>=0; --i)
        {
            for(int j=0; j<=V; ++j)
            {
                opt[j][i] = 0;
                for (int k=0; k<=temp[i].TotalCount; ++k)
                {
                    int v = temp[i].Volume*k;
                    int h = temp[i].Happiness*k;
                    if (v<=j && opt[j-v][i+1]+h>opt[j][i])
                    {
                        opt[j][i] = opt[j-v][i+1]+h;
                    }
                }
            }
        }
        return opt[V][0];
    }


动态规划数组输出结果:

V
    

0
    

1
    

2
    

3
    

4
    

5
    

6
    

7
    

8
    

9

0
    

0
    

0
    

0
    

0
    

0
    

0
    

0
    

0
    

0
    

0

1
    

5
    

5
    

5
    

4
    

0
    

0
    

0
    

0
    

0
    

0

2
    

10
    

10
    

10
    

8
    

6
    

5
    

4
    

0
    

0
    

0

3
    

15
    

15
    

15
    

12
    

6
    

5
    

4
    

0
    

0
    

0

4
    

19
    

19
    

19
    

18
    

18
    

18
    

18
    

18
    

12
    

0

5
    

23
    

23
    

23
    

22
    

18
    

18
    

18
    

18
    

12
    

0

6
    

28
    

28
    

28
    

26
    

24
    

23
    

22
    

18
    

12
    

0

7
    

33
    

33
    

33
    

30
    

24
    

23
    

22
    

18
    

12
    

0

 只需要读取opt[7][0]即可获得最高满意度为33。

 

5.贪心解法

    int TakeOutMax( int k)
    {
        int maxHappiness = 0;
        if (k < 0) return 0;
        
        if (k > 0){
            int t =k/2;
            int h = TakeOutMax(t);
            if (h>0){
                drinks[t].push_back(Drink((int)pow(2,t), 1, h));
                h = TakeOutMax(t);
                if (h>0)
                {
                    drinks[t].push_back(Drink((int)pow(2,t), 1, h));
                }
            }
            int p1, p2;
            p1 = p2 = -1;
            for (int i=0; i<drinks[t].size(); i++)
            {
                for (int j=i+1; j<drinks[t].size(); ++j)
                {
                    if (drinks[t][i].TotalCount>0 && drinks[t][j].TotalCount>0 && drinks[t][i].Happiness+drinks[t][j].Happiness>maxHappiness)
                    {
                        maxHappiness = drinks[t][i].Happiness+drinks[t][j].Happiness;
                        p1 = i;
                        p2 = j;
                    }
                }
            }
            if (p1 >-1 && p2 > -1)
            {
                drinks[t][p1].TotalCount--;
                drinks[t][p2].TotalCount--;
                drinks[k].push_back(Drink((int)pow(2,k), 1, maxHappiness));
            }
        }
        int p=-1;
        maxHappiness = 0;
        for (int i=0; i<drinks[k].size(); ++i)
        {
            if (drinks[k][i].TotalCount>0 && drinks[k][i].Happiness>maxHappiness)
            {
                maxHappiness = drinks[k][i].Happiness;
                p = i;
            }
        }
        if (p >=0 ){
            drinks[k][p].TotalCount--;
        }
        return maxHappiness;
    }
     
    // 贪心算法
    int GetMaxHappinessByGreed(int V)
    {
        int k = V;
        int i=0;
        int happiness = 0;
        while(k)
        {
            if (k & 1) happiness += TakeOutMax(i);
            k >>= 1;
            i++;
        }
        return happiness;
    }

贪心解法中TakeOutMax()函数会改变drinks数组。

最后的返回值是一样的:33。

 

6.总结

1)动态规划解法中,前两层循环的次序可以颠倒。也就是在生成的动态数组中,我们可以从上往下一行一行地填,或者从右往左一列一列地填都不影响最后结果,最关键的是需要事先把边界值设定好,最上面一行全为0,最右边一列全为0。

2)贪心算法的思路其实并没有动态规划清晰,但也容易想通,算法的复杂度我没有仔细研究,欢迎大家讨论。
---------------------
作者:lichaoyin
来源:CSDN
原文:https://blog.csdn.net/lichaoyin/article/details/9983883
版权声明:本文为博主原创文章,转载请附上博文链接!

posted @ 2019-07-19 11:32  天涯海角路  阅读(272)  评论(0)    收藏  举报