背包问题(动态规划)

DP大法,玄妙无比。

动态规划的原理

动态规划(dyanmic programming)与分治方法相似,都是通过组合子问题的解来求解原问题。 分治方法:将问题划分为互不相交的子问题,递归求解子问题,再将姐组合起来,求出原问题的解。 与之相反,动态规划应用于子问题重叠情况,即不同子问题具有公共的子子问题(将子子问题存入表格,只求解一次) 刻画最优解的结构特征。 利用计算信息构造一个最优解。 “动态规划算法通常利用重叠子问题性质:对每一个子问题求解一次,将解存入表中,再次需要直接查表。 ” 适合动态规划方法求解的最优化问题应具备两个要素:最优子结构和问题重叠。 “如果一个问题的最优解包含其子问题的最优解,我们称此问题具有最优子结构性质。

01背包问题

传送门

解题思路:
二维数组版

  1. 状态 f[i][j]定义:前 i 个物品,背包容量 j 下的最优解(最大价值):

    当前的状态依赖于之前的状态,可以理解为从初始状态 f[0][0] = 0 开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态 f[i][j] 不断由之前的状态更新而来。

  2. 当前背包容量不够( j < v[ i ] ),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:
    对应代码:f [i][j] = f [i - 1][j]

  3. 当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:

    f [i][j] = f [i - 1][j - v[i]] + w[i]

    不选f[i][j] = f[i - 1][j]

    我们的决策是如何取到最大价值,因此以上两种情况取 max() 。

代码如下

#include<iostream>
using namespace std;
int n,m;
int f[1010][1010];
int v[1010],w[1010];
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>=1;j--) //for(int j=1;j<=m;j++) 也可以
        {
            //if(j<v[i])
            f[i][j]=f[i-1][j];
            
            if(j>=v[i])
            f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    cout<<f[n][m];
    return 0;
}

优化版–> 一维滚动数组
将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。

为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。

  1. 状态f[j]定义:N 件物品,背包容量 j 下的最优解。
  2. 注意枚举背包容量j必须从m开始。(逆序)
  3. 为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]f[i -1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

例如,一维状态第i轮对体积为 3 的物品进行决策,则f[7]f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]

简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。

状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i]

代码如下

#include<iostream>
using namespace std;
int n,m;
int f[1010];
int v[1010],w[1010];
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]);

    cout<<f[m];
    return 0;
}

注意 f[j-v[i]]==f[i-1][j-v[i]],更新是用上一轮没更新的状态,不是用当前的状态更新。

完全背包问题

传送门

在这里插入图片描述

朴素版 O(n*n) 有可能超时

#include<iostream>
using namespace std;

const int N = 3010;

int n, m;
int dp[N][N], v[N], w[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 = 0; j <= m; j ++ )
            for(int k = 0; k * v[i] <= j; k ++ )
                dp[i][j]=max(dp[i][j], dp[i-1][j-k*v[i]]+k*w[i]);
                
    cout << dp[n][m] << endl;
    return 0}

所以我们要改进
f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2 * v] + w, f[i - 1][j - 3 * v] + 2 * w, .....)
而我们需要的f[i][j]的状态表示是:
f[i][j]= max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2 * v] + 2 * w, f[i - 1][j - 3 * v] + 3 * w);
将每一项一一比对,我们可以得到下列状态表示:

f[i][j] = max( f[i - 1][j], f[i][j - v] +w );

二维版

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

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )
    {
        int v, w;
        cin >> v >> w;
        for(int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            
            if(j >= v)
                f[i][j] = max(f[i][j], f[i][j - v] + w);
        }
    }
    cout << f[n][m] << endl;
    
    return 0}

优化版

#include<iostream>
using namespace std;
int n,m;
int f[1010];
int v[1010],w[1010];
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=v[i];j<=m;j++)
            f[j]=max(f[j],f[j-v[i]]+w[i]);
            
    cout<<f[m];
    
    return 0;
}

注意f[j-v[i]]==f[i][j-v[i]]

完全背包一维的滚动数组代码和 01背包 的代码只有一处区别,即 01背包的j从大到小遍历,而 完全背包 的j从小到大遍历,两者的含义不同。

多重背包问题 I

传送门

思路和 01背包相似

#include<iostream>
using namespace std;
const int N=110;
int s[N],v[N],w[N];
int f[N][N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>> v[i] >> w[i]>> s[i];
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for( int k=0; k<= s[i] && k * v[i]<= j; k++ )
                f[i][j]=max( f[i][j],f[i-1][j-v[i]*k]+w[i]*k );
    
    cout<<f[n][m];            
    return 0;
}

或者

#include <iostream>
using namespace std;
int f[1005];
int main()
{
    int n, m ;
    cin >> n >> m;
    for(int l=1;l<=n;l++)
    {
        int v, w, k;
        cin >> v >> w >> k;
        for (int i = 1; i <= k; i++)
            for (int j = m; j >= v; j--)
                f[j] = max(f[j], f[j - v] + w);
    }
    cout << f[m];
}

多重背包问题 II

传送门

由于时间限制,必须要优化,否则就 TLE 。

二进制优化!
以 2013 为例
把2013分组为 1,2,4,8,,,512 ,任意几组相加,可得到1~2013,从原本0 ~ 2013 遍历,变成 0 ~ 9遍历,,遍历次数就减少,时间变成log(2013)

在这里插入图片描述

#include<iostream>
#include<algorithm>
using namespace std;
const int N=25000;
int v[N],w[N];
int f[N];
int main()
{
    int n,m;
    cin>>n>>m;
    int cnt=0;
    for(int i=1;i<=n;i++)
    {
        int a,b,s;
        cin>>a>>b>>s;
        int k=1;
        while(k<=s)
        {
            cnt++;
            v[cnt]=a*k;
            w[cnt]=b*k;
            s-=k;
            k*=2;
        }
        if(s>0)
        {
            cnt++;
            v[cnt]=a*s;
            w[cnt]=b*s;
        }
    }
    
    n=cnt;
    
    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];        
    return 0;
}

分组背包问题

传送门

一、状态表示:f[i][j]

  1. 集合:从前i组物品中选,且总体积不超过j的所有方案的集合.
  2. 属性:最大值

二、状态计算:

  1. 思想-----集合的划分
  2. 集合划分依据:根据从第i组物品中选哪个物品进行划分.
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k])

代码如下

#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int v[N][N],w[N][N];
int f[N],s[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        for(int j=0;j<s[i];j++)
        cin>>v[i][j]>>w[i][j];
    }
    
    for(int i=1;i<=n;i++)
        for(int j=m;j>=0;j--)
            for(int k=0;k<=s[i];k++)
                if(v[i][k]<=j)
                f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    cout<<f[m];            
    return 0;
}

欢迎点赞与评论~
记得收藏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值