背包问题Ⅱ与二分问题

今天我对背包问题有了更深的理解,我一定要写下来,巩固自己的思路

并且,遇到新的难题二分,不管了,干就完了!!!

完全背包

以今天写的代码展开详细描述与解释,并附上题目

cpp 复制代码
#define N 1001
int n, v, W[N], V[N];
int main()
{
	cin >> n >> v;
	for (int i = 1; i <= n; i++)
	{
		cin >> V[i] >> W[i];
	}
	int dp[N][N];
	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])
				dp[i][j] = max(dp[i][j], dp[i][j - V[i]] + W[i]);
		}
	}
	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] && dp[i][j - V[i]] != -1)
				dp[i][j] = max(dp[i][j], dp[i][j - V[i]] + W[i]);
		}
	}
	cout << (dp[n][v] == -1 ? 0 : dp[n][v]) << endl;
	return 0;
}

这是一道在牛客上写的题目,所以使用全局的变量更加方便,根据输入要求,我直接把数组设置的足够大,那么就不用考虑越界的问题

题目分为两道,第一道就是中规中矩的动态规划,但是要区别与01背包;第二道需要一些数学推导,简化代码

  • 第一题,我们要确定下来状态转移方程dp[i][j]:意为在前i个物品中,所挑选的物品中总体积不超过j的情况下,所能拥有的最大价值
  1. 这里主要区分与01背包的是dp[i][j] = max(dp[i][j], dp[i][j - V[i]] + W[i])表达式;这里01背包中是dp[i][j] = max(dp[i][j], dp[i-1][j - V[i]] + W[i]);因为完全背包和01背包的本质区别是01背包中物品都只有一个,然而完全背包中物品确实可以任意多个;所以以我的话来讲就是在01背包中,你选了当前物品,就不能再次选择了,就只能去前i-1个物品中找合适的选项;在完全背包中,你选了当前物品,还可以再次选择了,就可以再去前i个物品中找合适的选项;
  • 第二题,我们要确定下来状态转移方程dp[i][j]:意为在前i个物品中,所挑选的物品中总体积恰好为j的情况下,所能拥有的最大价值
  • 这题中最重要的是一个数学公式推导,还有初始化与返回值问题
  1. 初始化:主要讨论第一行与第一列,dp[0][0]就是0,不用质疑;当i=0时,要讨论从0个物品中挑选出j体积的情况,是不可能出现的,所以要把这些位置置为-1(表示不存在这种情况);当j=0,要讨论从i(从1开始)个物品中挑选出0体积的情况,只要不选即可,所以情况存在只要置为0即可
  2. 数学公式推导如下

3.因为最终结果可能为-1,所以需要特判一下

完全背包利用滚动数组优化后的代码

cpp 复制代码
#define N 1001
int n, v, W[N], V[N];
int main()
{
	cin >> n >> v;
	for (int i = 1; i <= n; i++)
	{
		cin >> V[i] >> W[i];
	}
	int dp[N];
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j >= V[i]; j++)
		{
			dp[j] = max(dp[j], dp[j - V[i]] + W[i]);
		}
	}
	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 = 0; j >= V[i] && dp[j - V[i]] != -1; j++)
		{
			dp[j] = max(dp[j], dp[j - V[i]] + W[i]);
		}
	}
	cout << (dp[v] == -1 ? 0 : dp[v]) << endl;
	return 0;
}

01背包利用滚动数组优化后的代码

cpp 复制代码
#define N 1010 
int n, v, M[N], V[N];
int dp[N];

int main()
{
    cin >> n >> v;
    for (int i = 1; i <= n; i++)
    {
        cin >> V[i] >> M[i];
    }
    //第一问
    for (int i = 1; i <= n; i++)
    {
        for (int j = v; j >= V[i]; j--)
        {
            dp[j] = max(dp[j], M[i] + dp[j - V[i]]);
        }
    }
    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; j >= V[i] && dp[j - V[i]] != -1; j--)
        {
            dp[j] = max(dp[j], M[i] + dp[j - V[i]]);
        }
    }
    cout << (dp[v] == -1 ? 0 : dp[v]) << endl;;
    return 0;
}

如上可知,两份代码有一些细微差别

即遍历顺序:

  • 01背包从右往左:

因为需要避免重复选取同一个物品;我们想象下,如果从左往右遍历,dp[j]与dp[j-v[i]]的关系;如若有一个物品体积为2,我们一开始初始化了dp[2],但是当j=4时,会使得出现dp[j]==dp[j-v[i]]的情况(即dp[2]==dp[4-2]);此时就违背了01背包的原则-->物品只能选一次;

所以应该从右往左遍历,这样我们就可以确保不会重复使用到第i个物品

  • 完全背包从左往右遍历:

完全背包则和01背包相反,可以重复使用第i个物品,所以因该是从左往右遍历,详情见上

至此,01背包和完全背包已经完全解释清楚!!!撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★*

二分算法

二分算法是一类模板题,有规范的解题思路,所以更应该把思路理清,不为了一劳永逸,而是加深巩固,方便日后复习

以之前写的代码展开详细描述与解释,并附上题目

cpp 复制代码
class Solution 
{
public:
    vector<int> searchRange(vector<int>& nums, int target) 
    {
        if(nums.size()==0)
        {
            return {-1,-1};
        }  
        int begin=0;
        int n=nums.size();
        int left=0,right=n-1;
        while(left<right)
        {
            int mid=left+(right-left)/2;
            if(nums[mid]<target)
            {
                left=mid+1;
            }
            else
            {
                right=mid;
            }
        }
        if(nums[left]!=target)
        {
            return {-1,-1};
        }
        else
        {
            begin=left;
        }
        left=0,right=n-1;
        while(left<right)
        {
            int mid=left+(right-left+1)/2;
            if(nums[mid]<=target)
            {
                left=mid;
            }
            else
            {
                right=mid-1;
            }
        }
        return {begin,right};
    }
};

二分查找算法的本质-->二段性,即遇到有二段性的题目,可以优先尝试使用二分算法

  • 确定左右边界:在题目中会分为两种左右

①left=mid+1,right=mid//②left=mid,right=mid-1

在这种情况下,所选中的x是小于target与x大于等于target//x是小于等于target与x大于target;判断left是否+1或者right是否-1;即看这个x是否可能是所需答案,如若x不是所需答案,需要+1//-1操作跳过这个数

  • 确定中点mid:在题目中会分为两种左右

①mid=left+(right-left)/2 || mid=left+(right-mid+1)/2

如何区分这两种情况呢?很简单,我只需要观察right是否-1即可;如若-1,则括号中需要+1;反之则不要,总结如下图

  • 算法进行的前提是left<right
  • 总结:当x>t时,right一直向left靠拢;当x<t时,left一直向right靠拢
  • 回到开头我们可知,对于x的划分是基于二段性的;即当我们求左端点时,数组会被划分成小于t与大于等于t两个部分;当我们求右端点时,数组会被划分成大于t与小于等于t两个部分

最后感谢您的观看!!!

相关推荐
Code Slacker2 小时前
LeetCode Hot100 —— 普通数组(面试纯背版)(五)
数据结构·c++·算法·leetcode·面试
sin_hielo2 小时前
leetcode 3573(买卖股票问题,状态机dp)
数据结构·算法·leetcode
flashlight_hi2 小时前
LeetCode 分类刷题:110. 平衡二叉树
javascript·算法·leetcode
式5162 小时前
线性代数(九)线性相关性、基与维数
线性代数·算法·机器学习
啊阿狸不会拉杆2 小时前
《数字图像处理》第7章:小波变换和其他图像变换
图像处理·人工智能·python·算法·机器学习·计算机视觉·数字图像处理
炽烈小老头2 小时前
【 每天学习一点算法 2025/12/17】验证二叉搜索树
学习·算法
用户271995372132 小时前
基于Label Studio 集成视觉大模型Qwen2-VL和yolo实现自动标注
算法
智者知已应修善业2 小时前
【删除有序数组中的重复项 II之O(N)算法】2024-1-31
c语言·c++·经验分享·笔记·算法
patrickpdx3 小时前
leetcode:环形链表
算法·leetcode·链表