【动规】背包问题

01背包问题:

一. 01背包(牛客网)


01背包问题的本质就是一个商品选不选,同时注意背包的容量问题,那么状态表示一般用二维,一维来表示选不选一维来表示容量,要注意背包体积是否有剩余,完全背包问题要注意体积是否刚好合适,初始化也是采用多增加一行一列来避免越界问题

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
 
const int N=1010;
int n,V,v[N],w[N];
int dp[N][N];
int main() {
    //获取数据
    cin>>n>>V;
    for(int i=1;i<=n;i++)
    cin>>v[i]>>w[i];
    //解决第一问
    for(int i=1;i<=n;i++)
        for(int j=0;j<=V;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(j-v[i]>=0)
            dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }
    //第一问答案输出
    cout<<dp[n][V]<<endl;
 
    //解决第二问
    memset(dp,0,sizeof(dp));
    //初始化第一行为-1的情况
    for(int i=1;i<=V;i++) dp[0][i]=-1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=V;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(j-v[i]>=0&&dp[i-1][j-v[i]]!=-1)
            dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }
    cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;
    return 0;
}
  • 优化版本:
    已知填表需要用到上一行的信息,用完之后就没有了,所以可以极限一点,只用一行一维数组来存储,但是遍历顺序要改为从后往前,因为一维数组中,dp[j] 既存储 i-1 层的旧状态,又要存储 i 层的新状态。正序遍历 j 时,较小的 j 会先被更新为 i 层状态(选了当前物品)。当计算较大的 j 时,j - v[i] 可能指向已经更新过的 i 层状态(而非原始的 i-1 层),导致同一物品被多次计入(违背 01 背包 "每个物品只能选一次" 的规则),而逆序遍历从大的位置开始覆盖,能保证只使用上一层的旧状态
  • 方法:
    所有的背包问题都可以进行空间上的优化
    1.直接在原始代码上修改,删除第一维数组即可
    2.改变j的遍历顺序
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
 
const int N=1010;
int n,V,v[N],w[N];
int dp[N][N];
int main() {
    //获取数据
    cin>>n>>V;
    for(int i=1;i<=n;i++)
    cin>>v[i]>>w[i];
    //解决第一问
    for(int i=1;i<=n;i++)
        for(int j=0;j<=V;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(j-v[i]>=0)
            dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }
    //第一问答案输出
    cout<<dp[n][V]<<endl;
 
    //解决第二问
    memset(dp,0,sizeof(dp));
    //初始化第一行为-1的情况
    for(int i=1;i<=V;i++) dp[0][i]=-1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=V;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(j-v[i]>=0&&dp[i-1][j-v[i]]!=-1)
            dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }
    cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;
    return 0;
}

二. 分割等和子集


可以将该问题转化为背包问题,选每个数就相当于在选商品,等和子集就相当于背包容量,注意容量要是偶数才能划分否则直接返回false,注意初始化第一列可以的情况

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n=nums.size();
        int sum=0;
        for(auto e:nums) sum+=e;
        if(sum%2==1) return false;
        int s=sum/2;
        vector<vector<bool>>dp(n+1,vector<bool>(s+1));
        //初始化
        for(int i=1;i<=n;i++) dp[i][0]=1;
        //填表
        for(int i=1;i<=n;i++)
            for(int j=1;j<=s;j++)
                {
                    dp[i][j]=dp[i-1][j];
                    if(j>=nums[i-1]) dp[i][j]=dp[i][j]||dp[i-1][j-nums[i-1]];
                }
        return dp[n][s];
    }
};

空间优化:

1.删掉第⼀维;

2.修改第⼆层循环的遍历顺序即可,从后往前

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n=nums.size();
        int sum=0;
        for(auto e:nums) sum+=e;
        if(sum%2==1) return false;
        int s=sum/2;
        vector<bool>dp(s+1);
        //初始化
        dp[0]=1;
        //填表
        for(int i=1;i<=n;i++)
            for(int j=s;j>=nums[i-1];j--)
                {
                    dp[j]=dp[j]||dp[j-nums[i-1]];
                }
        return dp[s];
    }
};

三. (494.) 目标和


关键点在于如何转化成背包问题,后面的分析思路就比较统一,本质就是在数组中选一些数能否等于目标值。

注意初始化:

由于需要用到上⼀行的数据,因此我们可以先把第⼀行初始化。第⼀行表示不选择任何元素,要凑成目标和 j 。只有当目标和为 0 的时候才能做到,因此第⼀

行仅需初始化第⼀个元素 dp[0][0] = 1

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s=0,sum=0;
        int n=nums.size();
        for(auto e:nums) s+=e;
        sum=(s+target)/2;
        if(sum<0||(s+target)%2==1)return 0;
        vector<vector<int>>dp(n+1,vector<int>(sum+1));
        dp[0][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=sum;j++)
                {
                    dp[i][j]=dp[i-1][j];
                    if(j>=nums[i-1]) dp[i][j]+=dp[i-1][j-nums[i-1]];
                }
        return dp[n][sum];
    }
};

空间优化:

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int s=0,sum=0;
        int n=nums.size();
        for(auto e:nums) s+=e;
        sum=(s+target)/2;
        if(sum<0||(s+target)%2==1)return 0;
        vector<int>dp(sum+1);
        dp[0]=1;
        for(int i=1;i<=n;i++)
            for(int j=sum;j>=nums[i-1];j--)
                {
                    dp[j]+=dp[j-nums[i-1]];
                }
        return dp[sum];
    }
};

四. (1049.) 最后一块石头的重量 II

可以将问题转化为01背包问题,已知任意两块石头放在一起重量相同会被丢弃,重量不同会进行差值计算并保留,也就是说任意两块石头重量相同部分会被丢弃,重量差异部分相当于在原数据前加上加减符号,所以可以将该题转化为上题目标和一样的思路

当所有元素的和固定时,分成的两部分越接近数组总和的⼀半,两者的差越小。因此问题就变成了:在数组中选择⼀些数,让这些数的和尽量接近 sum / 2 ,如果把数看成物品,每个数的值看成体积和价值,问题就变成了01 背包问题

cpp 复制代码
class Solution {
public:
    int lastStoneWeightII(vector<int>& s) {
        int sum=0;
        int n=s.size();
        for(auto e:s) sum+=e;
        int aim=sum/2;

        vector<vector<int>>dp(n+1,vector<int>(aim+1,0));
        for(int i=1;i<=n;i++)
            for(int j=0;j<=aim;j++)
            {
                dp[i][j]=dp[i-1][j];
                if(j>=s[i-1]) dp[i][j]=max(dp[i][j],dp[i-1][j-s[i-1]]+s[i-1]);
            }
            return sum-2*dp[n][aim];
    }
};
  • 空间优化版本:
cpp 复制代码
class Solution {
public:
    int lastStoneWeightII(vector<int>& s) {
        int sum=0;
        int n=s.size();
        for(auto e:s) sum+=e;
        int aim=sum/2;

        vector<int>dp(aim+1,0);
        for(int i=1;i<=n;i++)
            for(int j=aim;j>=s[i-1];j--)
            {
             dp[j]=max(dp[j],dp[j-s[i-1]]+s[i-1]);
            }
            return sum-2*dp[aim];
    }
};

完全背包问题:

五. 完全背包


  • 关于空间优化01背包和完全背包的区别:
    都是通过空间覆盖来减少空间消耗,区别在于遍历顺序,遍历顺序的本质是状态依赖方向。01背包怕重复选,所以从右往左保护旧状态,完全背包需要重复选,所以从左往右利用新状态
cpp 复制代码
#include <iostream>
#include<cstring>
using namespace std;
 
const int N=1010;
int dp[N][N],v[N],w[N];
int main() {
    //获取输入
    int n, V;
    cin>>n>>V;
    for(int i=0;i<n;i++) cin>>v[i]>>w[i];
    //第一题
    for(int i=1;i<=n;i++)
        for(int j=0;j<=V;j++)
        {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i-1])dp[i][j]=max(dp[i][j],dp[i][j-v[i-1]]+w[i-1]);
        }
        cout<<dp[n][V]<<endl;
 
        //第二问
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=V;i++)dp[0][i]=-1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=V;j++)
            {
                dp[i][j]=dp[i-1][j];
                if(j>=v[i-1]&&dp[i][j-v[i-1]]!=-1)
                dp[i][j]=max(dp[i][j],dp[i][j-v[i-1]]+w[i-1]);
            }
            cout<<(dp[n][V]==-1?0:dp[n][V])<<endl;
    return 0;
}
  • 空间优化版本:
cpp 复制代码
#include <iostream>
#include<cstring>
using namespace std;
 
const int N=1010;
int dp[N],v[N],w[N];
int main() {
    //获取输入
    int n, V;
    cin>>n>>V;
    for(int i=0;i<n;i++) cin>>v[i]>>w[i];
    //第一题
    for(int i=1;i<=n;i++)
        for(int j=v[i-1];j<=V;j++)
        {
            dp[j]=max(dp[j],dp[j-v[i-1]]+w[i-1]);
        }
        cout<<dp[V]<<endl;
 
        //第二问
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=V;i++)dp[i]=-1;
        for(int i=1;i<=n;i++)
            for(int j=v[i-1];j<=V;j++)
            {
                if(dp[j-v[i-1]]!=-1)
                dp[j]=max(dp[j],dp[j-v[i-1]]+w[i-1]);
            }
            cout<<(dp[V]==-1?0:dp[V])<<endl;
    return 0;
}

六. (322.) 零钱兑换


cpp 复制代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n=coins.size();
        vector<vector<int>>dp(n+1,vector<int>(amount+1));
        //初始化
        for(int i=1;i<=amount;i++) dp[0][i]=0x3f3f3f3f;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=amount;j++)
            {
                dp[i][j]=dp[i-1][j];
                if(j>=coins[i-1]) dp[i][j]=min(dp[i][j],dp[i][j-coins[i-1]]+1);
            }
        return dp[n][amount]>=0x3f3f3f3f?-1:dp[n][amount];
    }
};

空间优化:

cpp 复制代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n=coins.size();
        vector<int>dp(amount+1);
        //初始化
        for(int i=1;i<=amount;i++) dp[i]=0x3f3f3f3f;
        for(int i=1;i<=n;i++)
            for(int j=coins[i-1];j<=amount;j++)
            {
                dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
            }
        return dp[amount]>=0x3f3f3f3f?-1:dp[amount];
    }
};

七. (518.) 零钱兑换 II


cpp 复制代码
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n=coins.size();
        vector<vector<unsigned long long>>dp(n+1,vector<unsigned long long>(amount+1));
        dp[0][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=amount;j++)
                {
                    dp[i][j]=dp[i-1][j];
                    if(j>=coins[i-1]) dp[i][j]+=dp[i][j-coins[i-1]];
                }
        return dp[n][amount];
    }
};

八. (279.) 完全平方数


cpp 复制代码
class Solution {
public:
    int numSquares(int n) {
        int m=sqrt(n);
        vector<int>dp(n+1,0x3f3f3f3f);
        dp[0]=0;
        for(int i=1;i<=m;i++)
                for(int j=i*i;j<=n;j++)
                    {
                        dp[j]=min(dp[j],dp[j-i*i]+1);
                    }
        return dp[n];
    }
};

二维费用的背包问题:

九. (474.) ⼀和零


cpp 复制代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int k=strs.size();
        vector<vector<vector<int>>>dp(k+1,vector<vector<int>>(m+1,vector<int>(n+1)));
        for(int i=1;i<=k;i++)
            {
                //统计0(a)、1(b)个数
                int a=0,b=0;
                for(auto e:strs[i-1])
                    {
                        if(e=='0') a++;
                        else b++;
                    }
            for(int j=0;j<=m;j++)
                for(int x=0;x<=n;x++)
                    {
                        dp[i][j][x]=dp[i-1][j][x];
                        if(j>=a&&x>=b)
                            dp[i][j][x]=max(dp[i][j][x],dp[i-1][j-a][x-b]+1);
                    }
            }
        return dp[k][m][n];
    }
};

空间优化:

所有的背包问题,都可以进⾏空间上的优化

对于⼆维费⽤的 01 背包类型的,优化策略是:

i. 删掉第⼀维;

ii. 修改第⼆层以及第三层循环的遍历顺序即可

cpp 复制代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int k=strs.size();
        vector<vector<int>>dp(m+1,vector<int>(n+1));
        for(int i=1;i<=k;i++)
            {
                //统计0(a)、1(b)个数
                int a=0,b=0;
                for(auto e:strs[i-1])
                    {
                        if(e=='0') a++;
                        else b++;
                    }
            for(int j=m;j>=a;j--)
                for(int x=n;x>=b;x--)  
                        dp[j][x]=max(dp[j][x],dp[j-a][x-b]+1);
            }
        return dp[m][n];
    }
};

十. (879.) 盈利计划


第三维盈利越多越好肯定,所以k-price[i-1]可能会小于0,但是数组下标不能用负数来表示,所以用0来统一表示下标为负数的情况,取max如果盈利目标-当前盈利小于0就取0,大于0就取本身

cpp 复制代码
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {  
        const int mod=1e9+7;
        int m=group.size();
        vector<vector<vector<int>>>dp(m+1,vector<vector<int>>(n+1,vector<int>(minProfit+1)));
        //初始化,选择为空集的情况
        for(int i=0;i<=n;i++) dp[0][i][0]=1;
        //填表
        for(int i=1;i<=m;i++)
            for(int j=0;j<=n;j++)
                for(int k=0;k<=minProfit;k++)
                    {
                        dp[i][j][k]=dp[i-1][j][k];
                        if(j>=group[i-1])
                            dp[i][j][k]+=dp[i-1][j-group[i-1]][max(0,k-profit[i-1])];
                        dp[i][j][k]%=mod;
                    }
        return dp[m][n][minProfit];
    }
};

空间优化:

cpp 复制代码
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {  
        const int mod=1e9+7;
        int m=group.size();
        vector<vector<int>>dp(n+1,vector<int>(minProfit+1));
        //初始化,选择为空集的情况
        for(int i=0;i<=n;i++) dp[i][0]=1;
        //填表
        for(int i=1;i<=m;i++)
            for(int j=n;j>=group[i-1];j--)
                for(int k=minProfit;k>=0;k--)
                    {
                        dp[j][k]+=dp[j-group[i-1]][max(0,k-profit[i-1])];
                        dp[j][k]%=mod;
                    }
        return dp[n][minProfit];
    }
};

似包非包问题:

十一.(377.) 组合总和 Ⅳ


  • 背包问题本质求的是组合问题,是无序的,该题是排列问题,是有序的,不能用背包问题的思路来解决,用常规的动规思路来解决.dp[i]通过最后一个元素的位置,依次遍历整个原数组,若小于当前索引值就把dp[i-1]的情况累加起来
cpp 复制代码
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<double>dp(target+1);
        dp[0]=1;
        for(int i=1;i<=target;i++)
             for(auto e:nums)
            if(i>=e) dp[i]+=dp[i-e];
        return dp[target];
            
    }
}; 

十二. (96.) 不同的二叉搜索树


从1到当前下标位置依次遍历每一个元素作为根节点时有多少个二叉树。

外层循环 i 表示 "当前要组成的 BST 有 i 个节点(用 1到i 这 i 个数)";内层循环 j 表示 "选择 j 作为当前 BST 的 根节点"(j 必须是 1到i 中的某个数,因为 BST 的节点值是 1~i 连续的),0不能当作根节点,与题目要求不符,不是合法节点值,也会造成左子树计算越界

cpp 复制代码
class Solution {
public:
    int numTrees(int n) {
        vector<int>dp(n+1);
        dp[0]=1;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=i;j++)
            dp[i]+=dp[j-1]*dp[i-j];
        return dp[n];
    }
};

总结:

  • 关于状态转移方程的优化:
  • 01背包:
    商品只能选一次,依赖上一层的商品状态,所以状态转移方程一般形如:
    dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i])
  • 完全背包:
    可以重复选择商品,所以可以依赖当前层状态,由于重复选择所以无法穷尽状态转移方程,可采用数学等价替换的方式来优化表达式。一般完全背包的优化方式如下:
  • 关于空间优化:
  • 01背包:
    将第一维去掉,利用滚动数组的方式来覆盖计算。改变遍历顺序,从右往左,因为只能选一次商品,依赖上一层的状态,如果从左往右计算可能导致状突被提前覆盖,从右往左就不会
  • 完全背包:
    将第一维去掉,利用滚动数组的方式来覆盖计算。遍历顺序可以不用改变,因为可以重复选商品,所以不怕当前层元素被覆盖
相关推荐
犯困的土子哥2 小时前
C++:哈希表
c++·哈希算法
Code Warrior3 小时前
【Linux】Socket 编程预备知识
linux·网络·c++
智者知已应修善业3 小时前
【c语言蓝桥杯计算卡片题】2023-2-12
c语言·c++·经验分享·笔记·算法·蓝桥杯
littlepeanut.top3 小时前
C++中将FlatBuffers序列化为JSON
开发语言·c++·json·flatbuffers
hansang_IR3 小时前
【题解】洛谷 P2330 [SCOI2005] 繁忙的都市 [生成树]
c++·算法·最小生成树
Croa-vo3 小时前
PayPal OA 全流程复盘|题型体验 + 成绩反馈 + 通关经验
数据结构·经验分享·算法·面试·职场和发展
AndrewHZ4 小时前
【图像处理基石】 怎么让图片变成波普风?
图像处理·算法·计算机视觉·风格迁移·cv
无极小卒4 小时前
如何在三维空间中生成任意方向的矩形内部点位坐标
开发语言·算法·c#
FMRbpm4 小时前
链表中出现的问题
数据结构·c++·算法·链表·新手入门