今天我对背包问题有了更深的理解,我一定要写下来,巩固自己的思路
并且,遇到新的难题二分,不管了,干就完了!!!
完全背包
以今天写的代码展开详细描述与解释,并附上题目

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的情况下,所能拥有的最大价值
- 这里主要区分与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的情况下,所能拥有的最大价值
- 这题中最重要的是一个数学公式推导,还有初始化与返回值问题
- 初始化:主要讨论第一行与第一列,dp[0][0]就是0,不用质疑;当i=0时,要讨论从0个物品中挑选出j体积的情况,是不可能出现的,所以要把这些位置置为-1(表示不存在这种情况);当j=0,要讨论从i(从1开始)个物品中挑选出0体积的情况,只要不选即可,所以情况存在只要置为0即可
- 数学公式推导如下
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两个部分
最后感谢您的观看!!!


