最简单的dp问题

01背包问题

https://www.acwing.com/problem/content/2/

视频讲解:https://www.acwing.com/video/214/

问题分析

该问题有n件物品,每件物品有两个属性,要求再体积V的限制下能够得到最大价值的方法,以及所能得到的最大价值

思路:

在了解dp之前,第一想法是dfs或者bfs,但是有两个缺点,一是时间复杂度太高,二是太笨,时间复杂度是O(N!),对于1000的数据范围显然是行不通的

然后又分析了贪心,虽然可行,但往往不是最优解,而是近似的最优解,同样也行不通

所以,dp问题有点像dfs或者bfs又或者贪心问题,但又不能很好的解决的,而且可以通过集合的方法表示,基本就是dp问题了

什么时候用dp?

1.能通过dfs或bfs的思路给出解,但是无法成功解决问题的(尤其注意数据范围)

2.求有限集的某些属性,例如最值(如:最多,最少,value等等)

3.能够用一组数字表示状态,如本题中的f(i ,j)表示计算前i件物品最大体积为j时的最大价值

dp问题分析法:

第一步:状态表示(一般是二维的):

(1)明确所求:限制条件是啥(体积V),题目要求的是啥(最大价值value),我们要做啥(选择某几件物品,使得在限制条件内得出最大价值value)

(2)集合,我们对这组数据的操作可以表示为一个集合,用f(i, j)表示,每个集合可以分成两个子集f(i - 1, j)和f(i - 1, j - a(i))。通俗点就是在对前i - 1个数据操作(就是选不选的问题)完之后有两条路可走,不选第i件或选第i件

(3)操作,f(i, j) 这里要做出的选择就是在满足限制条件的情况下,是否对i进行操作。

(4)表示,f(i, j)表示的是对前i个物品进行操作集合,同时也是对最优操作的记录。

第二步:状态计算:

有了第一步的铺垫,我们可以看到当计算f(i, j)时需要做的是在满足限定条件时找出选择前i件物品时的最优解max(f(i - 1, j), f(i - 1,j - value(i)));其中f(i - 1, j)表示的是不选第i件并且总体积为j时的最优选择;f(i - 1,j - value(i))表示的是选择第i件物品并且总体积为j的最优选择(要注意这一步操作是要满足限制条件的)。

由此,我们可以得到dp问题的核心思想:状态转移

if(满足条件) f(i, j) = max(f(i - 1, j), f(i - 1,j - value(i)))

给出代码:

#include<iostream>

using namespace std;

const int N = 1010;

int f[N][N];
pair<int, int> b[N];

int main()
{
    int n, v;
    cin >> n >> v;
    for(int i = 1; i <= n; i ++)
        cin >> b[i].first >> b[i].second;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= v; j ++)
        {
            if(b[i].first <= j)
                f[i][j] = b[i].second + f[i - 1][j - b[i].first];
            f[i][j] = max(f[i][j], f[i - 1][j]);
        }
    cout << f[n][v];
    return 0;
}
//f[i][j] = 选第i个物品并且体积为j时的价值

在这里我们可以进行一步优化,类似与滚动数组。可以看到每次得到的结果f(i, j)都是基于对f(i - 1, **)的计算,所以我们可以去掉f的某一层之后改变某些地方使之与原来的代码等价。

观察核心部分

for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= v; j ++)
        {
            if(b[i].first <= j)
                f[i][j] = b[i].second + f[i - 1][j - b[i].first];
            f[i][j] = max(f[i][j], f[i - 1][j]);
        }

去掉i层之后可以看到

for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= v; j ++)
        {
            if(b[i].first <= j)
                f[j] = b[i].second + f[j - b[i].first];
                //f[i][j] = b[i].second + f[i - 1][j - b[i].first];
            f[j] = max(f[j], f[j]);
            //f[i][j] = max(f[i][j], f[i - 1][j]);
        }

进行分析:

f[j] = b[i].second + f[j - b[i].first];
//在第i次循环的时候,等号后面等价于b[i].second + f[i][j - b[i].first]
//与修改前的等价不等价,但是如果将j从后往前循环的话就可以等价
f[j] = max(f[j], f[j]);
//这一句无用

于是代码就可以化简为:

for(int i = 1; i <= n; i ++)
        for(int j = v; j; j --)
            if(b[i].first <= j)
                f[j] = b[i].second + f[j - b[i].first];

最终代码:

#include<iostream>

using namespace std;

const int N = 1010;

int f[N];
pair<int, int> b[N];

int main()
{
    int n, v;
    cin >> n >> v;
    for(int i = 1; i <= n; i ++)
        cin >> b[i].first >> b[i].second;
    for(int i = 1; i <= n; i ++)
        for(int j = v; j; j --)
            if(b[i].first <= j)
                f[j] = max(f[j], b[i].second + f[j - b[i].first]);
    cout << f[v];
    return 0;
}
//f[i][j] = 选第i个物品并且体积为j时的价值
posted @ 2021-02-24 19:20  kanbujian55  阅读(97)  评论(0)    收藏  举报