算法 动态规划(DP) 白话解释朴素01揹包问题

先放题目(应该是洛谷里的样例题目吧,侵删~ )
洛谷-P1048 采药
采药
这个范例emmmm 我们重新编个范例输入,为了方便思考,我们先直接考虑花费时间和价值相等时的情况,也就是拿了多少价值就花掉多少时间

22 5
1 1
5 5
6 6
9 9
20 20

拿这个新编造的范例举例,问题也就是:

在时间22之内,要拿走下面的哪几个数,才使拿走的数的总和尽量更大?

编号 1 2 3 4 5
1 5 6 9 20

如果说贪心拿大的数,第一步先拿第5个数20,肉眼可以看的出来前面4个数之和为1+5+6+9=21,显然比20大,所以WA
如果说贪心拿小的数。。。。随便换一下样例就挂了(比如把5换成1)
贪心是不可能贪心了,,,动态规划就这几行代码,还是来看看怎么动态规划吧

尝试拆解成子问题

比如对于第5个数20,我们只可能进行两个操作:,或者不拿


如果选择拿的话,问题就变成这样:
在时间2内,拿走下面的哪几个数,才使拿走的数的总和尽量更大?

编号 1 2 3 4
1 5 6 9

这个子问题解决后(等下会将怎么解决这个子问题的),得出来的总和加上这一波的20,就是选择的答案。

这里子问题相比原问题有两个改变:

  1. 因为我们拿了20,也就花了20时间,时间就从原本的22变成了2
  2. 因为我们的第5个数已经拿走了,所以现在只用考虑前4个数了

如果选择不拿的话,问题就变成这样:
在时间22内,拿走下面的哪几个数,才使拿走的数的总和尽量更大?

编号 1 2 3 4
1 5 6 9

这个子问题解决后(这个子问题跟上面的差不多,就时间不一样,等下也会来解决掉的),得出来的答案就是选择不拿的答案。(都不拿了肯定当然就不加20了,相应的,这个子问题的时间也没有减20)


于是可以发现,无论这里的20选择还是不拿,问题都能变成一个关于前4个数的子问题,只是限制的时间不太一样

最后通过解决这个子问题,得到最终选择的答案(拿的子问题总和加上这波拿的20)和不拿的答案(不拿的子问题总和),比较这两个答案谁更大,就能得到这一波该选择还是不拿

我们希望通过关于前4个数的子问题来决定第5个数是拿还是不拿,但子问题中,限制的时间是由第5个数字拿不拿决定的。。。。于是喜闻乐见地套娃了


所以我们把关于前4个数的子问题变成这样:
在时间 t (t ∈ [1, n], n = 22) 内,拿走下面的哪几个数,才使拿走的数的总和尽量更大?

编号 1 2 3 4
1 5 6 9

这里t是变量,范围是1到22(22是范例里给的时间来着,无论咋样t最多肯定只有22了,也就是全部选不拿的情况),也就是把时间为1到22时都求一遍,这样第5个数无论是多少,都能取到或者不拿的结果(比如第5个数是范例中的20,那么对于拿的子问题,就有t=22-20=2,对于不拿的子问题,就有t=22,和上面推出来的一样,然后无论是2还是22都在t的范围内,也就都已经算好值等着拿了),也就是说,解出了这个前4个数的子问题的解,就必能解出前5个数的问题的解


同理的,对于前4个数的问题,也可以换成前3个数的子问题;前3个数的问题,也可以换成前2个数的子问题······最后,换成了前1个数的子问题,那就能直接算了(t 小于这个数时不够时间必取不了,总和就是0,t 不小于这个数时就只取这个数,因为只有一个数不取也没别的了,总和就等于这个数)

这时候我们求出了前1个数的问题的解,就能推前2个数的问题的解······最后,我们就能推出前5个问题的解,然后AC !


下面推下具体过程

先回顾下数据:

编号 1 2 3 4 5
1 5 6 9 20

前1个数的问题,对于每个 t 的答案如下

t 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
最大总和 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

第1个数是1,t>=1时,就都直接刷成了1

前2个数的问题,对于每个 t 的答案如下

t 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
最大总和 1 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3

第2个数是2,由前1个数的问题得到的解可以推出这份解。比如t=3时, 设f(i, j) 为前 i 个问题中 t 为 j 时的解,选择的答案是f(1, 1)+2=1+2=3,而选择不拿的答案为f(1, 3)=1,比较后决定在t=3时,要选择的操作,且最大总和为3。

······

实在太多,算了算了,最后引用大佬解释的两个概念和判断一个问题能否使用DP解决

【无后效性】  
一旦f(n)确定,“我们如何凑出f(n)”就再也用不着了。  
要求出f(15),只需要知道f(14),f(10),f(4)的值,而f(14),f(10),f(4)是如何算出来的,对之后的问题没有影响。  
“未来与过去无关”,这就是无后效性。 
(严格定义:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。)

【最优子结构】  
回顾我们对f(n)的定义:我们记“凑出n所需的最少钞票数量”为f(n).  
f(n)的定义就已经蕴含了“最优”。利用w=14,10,4的最优解,我们即可算出w=15的最优解。  
大问题的最优解可以由小问题的最优解推出,这个性质叫做“最优子结构性质”。

引入这两个概念之后,我们如何判断一个问题能否使用DP解决呢?  
能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。

作者:阮行止
链接:https://www.zhihu.com/question/23995189/answer/613096905

最后附上递推代码

#include <cstdio>
#include <algorithm>

using namespace std;

int main(int argc, char const *argv[])
{
    int t, m, times[128], values[128], bags[1024] = { 0 };
    int i, j, k;
    scanf("%d%d", &t, &m);
    for (i = 0; i < m; i++)
    {
        scanf("%d%d", &times[i], &values[i]);
    }
    for (i = 0; i < m; i++)
    {
        // 这里循环时要从后往前,每一次都要用上一轮的结果
        // 如果从前往后用的时候就变成这一轮的结果了
        for (j = t; j >= times[i]; j--)
        {
            // 这一轮的结果等于选择不拿(bags[j])或者选择拿(values[i] + bags[j - times[i]])
            // 各得到的结果的最大值
            bags[j] = max(bags[j], values[i] + bags[j - times[i]]);
        }
    }
    printf("%d", bags[t]);
    return 0;
}

抛砖引玉,大牛绕道~
如有错误,烦请指出~

参考文章

  1. 知乎:什么是动态规划(Dynamic Programming)?动态规划的意义是什么?——阮行止的回答
  2. 知乎:什么是动态规划(Dynamic Programming)?动态规划的意义是什么?——王勐的回答
  3. 知乎:0-1揹包问题的动态规划算法 ——Bat特白
  4. 入坑题 洛谷:P2392 kkksc03考前临时抱佛脚
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章