#1 2020/3/26 关于动态规划问题的入门总结(C++)

前几日,老师布置了一道LeetCode的题目(198. 打家劫舍),过后呢,我也看了与其同类型的(17.16.按摩师),现总结如下:

所谓动态规划问题,就这两道题讲,对于向我这样的初学者,重要的是理解动态规划的内涵、理解如何定义子问题、理解并写出状态转移方程。

以198. 打家劫舍为例(题目自查):

首先,要知道一个概念叫子问题。所谓子问题就是和原问题相似,但规模较小的问题。例如对于接下来讨论,记第 k个房子偷与不偷时的金额分别为f[k][1]和 f[k][0],则原问题是“第n个房子偷与不偷时的金额”,分别记为f[n][1]和f[n][0],那么我们将问题的规模缩小,子问题就是“第 n-1个房子偷与不偷时的金额”,分别记为f[n-1][1]和f[n-1][0],同时记第k个房子的钱数为m[k],至于为什么这样定义原问题和子问题,接下来讨论。

我们研究题目可以发现,从全部n个房子中能偷到的最大金额有两种可能,第一是第n个房子偷后有最大金额,第二是第n个房子不偷后有最大金额。又根据规定可以得出,如果偷第n个房子,则第n-1个房子不能偷;如果不偷第n个房子,则第n-1个房子可以偷也可以不偷。因此,可以将经过第n-1个房子后的金额分为两种,偷了第n-1个房子,金额记为f[n-1][1]和没偷第n-1个房子,金额记为f[n-1][0],当我们知道了第n-1个房子这两种状态下的金额时我们就可以轻易得出:当打算偷第n个房子时,f[n][1] = m[n]+f[n-1][0];而打算不偷第n个房子时,f[n][0] = f[n-1][0]与f[n-1][1]之间的最大值(因为题目中要求最大金额)。

到这一步,我们就把问题转化为了 “第n-1个房子偷与不偷时的金额”,但是我们仍然无法计算,所以我们需要向下递推。将上述的原理中的n替换为n-1,将n-1替换为n-2,则问题就又可以转化为“第n-2个房子偷与不偷时的金额”,以此类推,最后递推到“第1个房子偷与不偷时的金额”。此时,整个逻辑就完善了,我们只需要从底向上推导,就可以从k = 1根据上面的思路反推导至k = n,将上述思维进行代码化如下:

int dajiajieshejieyi(vector<int>& m)
    {
        if (m.empty())//注意:判断一定要在变量定义后,不然空的情况执行f[0][1] = m[0]语句错误,没有nums[0]的值
        {
            return 0;
        }
        vector<int[2]> f(m.size());//用来保存每一家偷或不偷的结果
        f[0][0] = 0;//初始化第1个房子不偷时的金额,为0
        f[0][1] = m[0];//初始化第1个房子偷时的金额,为m(0)

        for (size_t i = 1; i < m.size(); ++i)
        {                                                                                                                                                                                                               //max为求最大值函数,下面是状态转移方程
            f[i][0] = max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = f[i - 1][0] + f[i];
        }        

        return max(f[m.size() - 1][0], f[m.size() - 1][1]);//返回k = n 的结果
    }

以上的方法,使用了二位数组保存了所有运算过的偷与不偷的结果,空间复杂度较高,但我们观察可以发现,计算f(k)(0)和f(k)(1)时,只需要f[k-1][0]和f[k-1][1]即可,所以没有必要用数组,用两个变量存储f[k-1][0]和f[k-1][1]即可,代码如下:

int dajiajieshejiesan(vector<int>& m)
    {
        if (m.empty())
        {
            return 0;
        }
        int alltou0 = 0;//存储f[k-1][0],初始值为0,因为要在循环外返回所以定义在循环外
        int alltou1 = 0;//存储f[k-1][1],初始值为0
        for (size_t i = 0; i < m.size(); ++i)
        {
            int tou0 = max(alltou0, alltou1);//f[k][0]=max(f[k-1][0], f[k-1][1])
            int tou1 = alltou0 + m[i];//f(k)(1) = f(k-1)(0) + m(k)
            alltou0 = tou0;//保存f(k)(0)
            alltou1 = tou1;//保存f(k)(1)
        }
        return max(alltou1, alltou0);//返回k=n时的最大值。
    }

以上的两种方法,实际上都是计算出k-1时偷与不偷的情况并分别保存,用于计算k偷与不偷的情况。

这里还有另一种更简洁的思路,使用DP数组,也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。对于第k个房子来说,如果我们不偷的话,其值等于第k-1个房子偷与不偷的最大值;而如果我们偷的话,其值等于第k-1个房子不偷的值加上m(k),其中第k-1个房子不偷的值等于第k-2个房子偷与不偷的最大值,所以,计算f[k]需要第k-1和k-2个房子金额的最大值,以此类推至k=1,我们只需要知道第1个房子的最大值,并设第0个房子最大值为0,即可算出第k(k >= 2)个房子的最大值然后保存。这种思路的状态转移方程也就为 f[k] = max(f[k-1], m[k-1] + f[k-2]),因为k依赖于k-1和k-2计算,因此自底向上更为方便,代码如下:

int dajiajieshejiesi(vector<int>& m) {
    if (m.empty()) {//必须放在变量定义前,不然 f[1] = m[0]会出错
        return 0;
    }
    vector<int> f(m.size()+1, 0);
    f[0] = 0;//第0个房子,没房子最大值为0
    f[1] = m[0];//第一个房子,只有一个房子,最大值为m[0]
    for (int k = 2; k <= m.size(); ++k) {
        f[k] = max(f[k-1], m[k-1] + f[k-2]);//从第二个开始计算第k个的最大值
    }
    return f[m.size()];
}

同样的,这个思路也可以简化空间复杂度,计算f[k]时,只需要f[k-1]和f[k-2]即可,那么用两个变量保存其值即可,代码如下:

int dajiajieshejiewu(vector<int>& m) {
    int f1 = 0;
    int f2 = 0;
    for (int i  = 0; i < m.size(); ++i) {
        int k = max(f2, f1 + m[i]);
        f1 = f2;
        f2 = k;
    }                                                                                                                                                                                                         return f2;
}

就我见到的而言,上面的方法是最简洁的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值