动态规划的总结与思考

本文深入探讨了动态规划的概念,介绍了动态规划的基本思想、状态表示和状态转移方程,并通过爬楼梯问题举例说明。文章进一步阐述了背包问题的分类(01背包、完全背包、多重背包)及优化策略,同时还涉及线性DP、序列性DP,如最长上升子序列问题的单调性优化。此外,还讨论了状态机模型和区间DP的应用,如石子合并和戳气球问题。最后,提到了Floyd和Bellman-Ford算法在求解最短路径问题中的应用。
摘要由CSDN通过智能技术生成

point 1 .动态规划是什么?用来解决什么问题?主要有哪几种问题?

这里引用leetcode上的一段话:动态规划(英语:Dynamic programming,简称DP)是一种在数学,管理科学,计算机科学,经济学和生物信息学中使用的,通过把原问题分解为相对简单子问题的方式求解复杂问题的方法。

所谓dp,个人感觉就是一个数组记录状态然后记忆化求解的过程,在这个过程中,有两点很重要,第一是状态表示,就是用一个数组来记录当前的状态是什么样的,第二就是状态转移,即是列出从上一个状态转移到目前状态的方程。这两点做完后我们基本就告成了一大半,最后就是处理下边界情况和一些初始状态,然后等着ac就可以了~

下面以很经典的爬楼梯问题为例子,如题:

你要爬n阶楼梯,每次你可以爬1阶或2阶,请问你有多少种方案可以爬上去?

我们首先来分析一下所谓的状态表示,这一题我们可以用dp[n]来表示你爬上n阶楼梯的所有方案数,即也是本题所谓的状态表示。这里注意到dp是个数组,所以我们之前的所有方案都会被记录在这个数组里,所以我们想要什么数据直接从里面找就可以了,这也就是所谓的记忆化。然后我们来考虑状态转移方程,回顾状态转移方程,即由前面的状态去更新当前的状态,而前面状态的正确结果我们已经得到了,所以这个更新类似于递归,我们可以由此得到所求状态的正确答案。

考虑一下本题,上到第n层可以由第n - 1层爬一步上来,也可以由第n - 2层爬两步上来,而上到第n - 1层的所有方案数和上到第n - 2层的所有方案数必定没有重合,故上到第n层的方案数是两者之和,故状态转移方程为dp[n] = dp[n - 1] + dp[n - 2]。这时候大半已经完成了,只需要考虑下边界即可。注意到数组下标必须为非负数,故dp[1]与dp[0]需要特殊处理,而这两个很容易得到都是1。故写出代码

vector<int> dp(n + 1);
dp[0] = 1, dp[1] = 1;

for (int i = 2; i <= n; i ++ )
dp[i] = dp[i - 1] + dp[i - 2];

return dp[n];

好了,引入完毕,正式进入专题。这篇文章总结dp问题有以下几种:背包问题,线性dp,区间dp,状态机模型,还会写一些关于图论的dp思想,如flyod等,更深一些的dp本小白也不咋了解就不班门弄斧了hh。对于本文中的dp算法的一些优化以及单调队列优化斜率优化会另外发文讨论。本文题源主要来自leetcode与acwing,我也是初步接触dp并对其感兴趣的小白,也是第一次写这种东西,如有不对的地方,还请大家包涵并指出~(有时候dp和f会写串qaq)

point2. 背包问题

背包问题作为很典型的dp,其书写模式比较固定,但是有些题需要你分析出来他是这个问题。这里将背包问题分为一维和二维,题型的话有01背包,完全背包,多重背包,分组背包等以及它们的空间优化思路,参考了yxc大佬的解法,在此表达感谢~

1.01背包

顾名思义,就是每个物品只能用一次

来思考状态表示。我们用dp[i][j]来表示从前i个物品里选且体积为j的物品的最大价值,那么我们要求的答案就是dp[N][V]。再来考虑状态转移方程,我所理解的状态转移方程即是找前i - 1个与前i个的不同点,即从i - 1到i会发生什么事。我们可以从选不选第i个物品入手,将所有情况分为两类:选第i个物品与不选第i个物品(注意情况要做到不重不漏)

不选第i个物品:所能得到的最大价值即从前i - 1个里面选出体积为j的物品,即dp[i - 1][j]。

选第i个物品:所能得到的最大价值即前i - 1个物品需要给第i个留一下它的体积空间,然后再加上它的价值,即dp[i - 1][j - v[i]] + w[i]。

dp[i][j]即是在里面取最大值。

参考代码如下(f写顺手了,请自动过滤成dp hh)

vector<vector<int>> dp(n + 1, vector<int> (m + 1));

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= m; j ++ )
    {
        if (j < v[i])
        f[i][j] = f[i - 1][j];
        else
        f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]);
    }
}

return f[n][m];

下面我们考虑空间优化,注意到我们的dp[i][j]可以求出任意状态关于i,j的最优解,但是我们仅需要关于j的最优解,故可以将二维数组优化成一维。但是简单地去掉一维然后照抄代码肯定是不够的,必须将j这一维度改为逆序。解释一下为什么需要改成逆序,首先需要明确每个物品只能用一次,故我们i只能由i - 1更新而来,否则会出现一个物品使用两次的情况。举个例子,假如我们换成一维且j是正序,而且第四个物品的体积为3,此时dp[4][7]可能会由dp[4][4]更新而来,这样第四个物品会重复使用,而我们逆序更新则不会出现这种情况(一定要与后面的完全背包的空间优化作区别),因为逆序物品的体积由大到小,选的物品个数却是不减的,故i必定只能由i - 1更新而来。参考代码如下:

vector<int> dp(m + 1)

for (int i = 1; i <= n; i ++ )
{
    for (int j = m; j >= v[i]; j -- )
    f[j] = max(f[j], f[j - v[i]] + w[i]);
}  

return f[m];

2.完全背包

和01背包同样的状态表示,区别在于每个物品的使用次数无限,下面来分析状态转移方程。

同样分为第i组物品选或者不选,但是第i种物品可以选任意多个,故f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i], .... f[i - 1][j - k * v[i]] + k * w[i]) 但是这么做时间复杂度较高,考虑优化,如下图所示

这样改进后可以将复杂度降为O(n * m)。

与一维背包相同,我们考虑作空间优化,这里注意到每个物品是无限使用的,所以与01背包不同,我们的j需要正序更新,逆序更新会产生什么后果呢?就是把取法限制在了每种物品只能取一个,换句话说逆序的i只会被i - 1更新,而我们这里的i是可以由i更新的 ,所以会少一些情况。参考代码如下:

vector<int> dp(m + 1)

for (int t = 1; t <= n; t ++ )
{
    for (int j = v[t]; j <= m; j ++ )
    dp[j] = max(dp[j], dp[j - v[t]] + w[t]);
}

return dp[m];

3.多重背包及其优化

多重背包用暴力一点的做法就是枚举第i种物品所能取的个数,然后取一个最大值。空间优化可能有点困难,因为第i组物品可能有一个也可能有多个,所以我们先不考虑空间优化,参考

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值