【基础算法笔记】dp(背包问题)

在acwing上学习算法时的一点思考与总结。


如何理解dp

思路:动态规划解决问题的方式就是将一个大问题分解成多个子问题。每个子问题的决策都会影响下一个子问题的决策,也就是i状态会受到i-1状态的影响。根据这个特点,我们可以写出状态转移方程。状态转移方程的意义就在于不断更新f[ i ][ j ] 的结果。

思考:dp的优势就在于当考虑i状态时,i-1前面的状态就可以不用考虑了,他们的最优解已经被计算出来存在f[ i ][ j ]里了。所以我们只要考虑i和i-1状态的关系,然后更新i状态下的最优解就可以了。就这样一直往下推进,直到i到边界。

f[ i ][ j ]有两层含义:一个大集合(存储了所有不同状态下的结果) + 属性(最大值最小值)

dp代码思路:首先想清楚用几维的f来表示状态,以及对应的i和j分别表示什么,比如在最长上升序列中只需要用一维f[ i ]就表示数组中以w[ i ]结尾的最长上升序列。在背包问题中f[i][j]表示前i个物品,体积不超过j时的最大价值。然后根据状态的前后影响关系写出状态转移方程。

干讲还是有点抽象,下面结合几个例题。

01背包问题

思路:因为限制条件(有限体积)的存在,我们每次放一个物品到背包里都会影响下一个物品是否能放,也就是i状态会受到i-1状态的影响。因此我们要用dp来找出所有符合条件的结果,把他放在集合f[ i ][ j ]中,比如f[ 3 ][ 4 ]表示从前三个物品中取并且体积不超过4时的最大价值。最后通过遍历这个集合直接输出最大值。

二维

#include<iostream>

using namespace std;
const int N = 1010;
int n, m;
int f[N][N]; //表示前i个物品,体积小于等于j时的最大价值
int v[N], w[N]; //v表示体积,w表示权重(价值)

int main()
{
    cin>>n>>m;
    for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
    
    for(int i = 1; i <= n; i ++) //遍历前i个物品
        for(int j = 1; j <= m; j++) //体积
        {    
            f[i][j] = f[i-1][j]; //表示不放物品
            if(j >= v[i]) //j要大于等于当前放置物体的体积
                f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]); // 从1 ~ i-1中物品选,体积为j-v[i]时的最大价值加上当前物品的价值。
        }
    int res = 0;
    for(int i = 0; i <= m; i ++) res = max(res,f[n][i]); //遍历取四个物品时的最大价值
    cout<<res;
}

优化为一维后的代码

解释一下为什么j是从最大体积开始往下遍历:

首先我们要知道这道背包问题的每个物品只能取一次,也就是说f[i]的状态计算必须由f[ i - 1][  j ]而来(f[ i - 1]表示前i-1个物品不超过j的最大价值)。

综述:i是由i-1得到的,而此时若j还是由小到大,那么i更新是就会是以在i循环内更新的j来进行更新比j大的数,但这不符合我们二维分析时的情况,因此我们需要从小到大,这样大j在更新时,是用的仍然是小j在i-1时更新的值。

#include<iostream>

using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin>>n>>m;
    for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
    
    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]);
    
    int res =0;
    for(int i = 0; i <= m; i ++) res = max(res, f[i]);
    cout<<res;
}

完全背包问题

差异就在于此题里,物品有无限个,所以j就要从v[ i ]开始遍历到最大体积。

#include<iostream>
using namespace std;
const int N = 1010;
int n,m;
int v[N], w[N], f[N];

int main()
{
    cin>>n>>m;
    for(int i = 0; i < n; i ++) cin>>v[i]>>w[i];
    for(int i = 0; i < n; i ++)
        for(int j = v[i]; j <= m; j ++) //从v[i]遍历到最大体积
        {
            f[j] = max(f[j], f[j-v[i]]+w[i]);
        }
    cout<<f[m];
}

多重背包问题

思路:将多重背包问题拆成01背包问题来做。假设一件物品的体积价值和数量分别是1 2 3,就可以拆成(1,2) 或 (2,4)或(3,6) 相当于是等价替换成三件新的物品,每个物品只能取一次,又回到了01背包问题。

#include<iostream>

using namespace std;
const int N = 105;
int n,m;
int v[N], w[N], s[N], f[N];

int main()
{
    cin>>n>>m;
    for(int i = 0; i < n; i ++) cin>>v[i]>>w[i]>>s[i];
    
    for(int i = 0; i < n; i ++)
        for(int j = m; j >= v[i]; j --)
           //下面这两代码合并起来看,和01背包的做法是一样的
            for(int k = 0; k <= s[i] && k*v[i] <= j; k ++)
                f[j] = max(f[j],f[j-k*v[i]]+k*w[i]);
    cout<<f[m];
}

多重背包问题(plus)

思路:也是拆成01背包问题来做,只不过不能再像上面那样循环一遍来拆分,一定会超时的。所以就用二进制的方法来拆分。因为二进制可以表达所有整数,比如一个在[ 0 , 255 ]的数字,它可以用1 2 4 8 16 32 64 128组合表示,所以时间复杂度就从 O(n^3)降到O(n^2logS)

#include<iostream>

using namespace std;
const int N = 12010, M = 2010; 
int a, b, n, m, s;
int v[N], w[N], f[M];

int main()
{
    int cnt = 0;
    cin>>n>>m;
    for(int i = 1; i <= n; i ++)
    {
        cin>>a>>b>>s;
        int k = 1;
        
        while( k <= s)
        {   
            cnt ++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s = s - k;
            k *= 2;
        }
        if(s > 0) //将剩下的s进行处理
        {
            cnt ++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    
    n = cnt; //更新n的个数
    
    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]);

    cout<<f[m];
}

猜你喜欢

转载自blog.csdn.net/Radein/article/details/134700769