背包问题

5 篇文章 0 订阅
3 篇文章 0 订阅

v[i]:价值
w[i]:重量
n:物品总数
m:背包容量

01背包问题

:一般是给两个值一个是价值,一个是重量,然后背包有承重上限,在承重上线下,问最多可以装多少价值的东西。01的意思就是每个物体只有一个,只能装一次。
状态转移方程为:
dp[n]=max(dp[n],dp[n-1]+v[i]);
初始条件:
dp[i]=0;

如果题目给的价值有负数,那么非0下标就要初始化为负无穷了。例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么取最大值的时候,就会取0而不是-2了,所以要初始化为负无穷。

先要这样想:dp[i][j](i是有前i个物品,j是当前能装物品重量)(总的意思是在容量为j,有i个物品的最大价值)

for(int i=1;i<=n;i++)//从第一个物品到第n个物品讨论
//循环可以交换位置
{
	for(int j=1;j<=m&&j>w[i];j++)
	{
		dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
	}
}

优化一下,用到了滚动数组,因为这里每一层i都是和上一层i-1去比,每用一层i就要刷新一下这i层的数据,而下面的完全背包是与本层去比

for(int i=1;i<=n;i++)//n是物品个数,m是总承重重量
{//循环不可以交换位置,因为需要保证每个物品只添加一次
	for(int j=m;j>=w[i];j--)//这里就要从后往前递推,避免数据被覆盖
	{//滚动数组
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
}

一维数组为什么第二层循环是逆序:
如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒叙遍历,就可以保证物品只放入一次呢?
倒叙就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15  //(dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了

例题:

试题 算法提高 01背包
资源限制
时间限制:1.0s 内存限制:256.0MB
问题描述
  给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个.
输入格式
  输入的第一行包含两个整数n, m,分别表示物品的个数和背包能装重量。
  以后N行每行两个数Wi和Vi,表示物品的重量和价值
输出格式
  输出1行,包含一个整数,表示最大价值。
样例输入
3 5
2 3
3 5
4 7
样例输出
8
数据规模和约定
  1<=N<=200,M<=5000.

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int w[5000];//重量
int v[5000];//价值
int dp[5000];//dp[i]为当前i重量的最大价值
int main()
{
	int n,m;
	cin>>n>>m;
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=w[i];j--)
		{
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	cout<<dp[m]<<endl;
} 

完全背包问题

意思是有每个物品有无限个,选出给定重量的最大价值

for(int i=1;i<=n;i++)//这个算法比较捞,用来3层循环
{
	for(int j=m;j>=w[i];j--)
	{
		for(int k=0;k<=j/w[i];k++)//相对于01背包加了一个可以达到最大数量的循环
		{
			dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
		}
	}
}

要优化

/*01背包*/dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//与上一层比较
//用的是上一层数据,所以要倒着第二层循环
/*完全背包*/dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);//是与本层比较
//用的是本层数据,新的数据,所以要顺着循环

状态方程是和01背包一样的,但是第二层循环方向不一样。

for(int i=1;i<=n;i++)
{
	for(int j=w[i];j<=m;j++)//与01背包的区别
	{
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
}

2649 完全背包
1.0 秒 131,072.0 KB 10 分 2级题
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是v[i],价值是c[i]。

现在请你选取一些物品装入背包,使这些物品的体积总和不超过背包容量,且价值总和最大。

其中1<=N<=100,1<=V<=50000,1<=v[i],c[i]<=10000。

输入
第一行输入两个数N,V,分别表示物品种类数和背包容积;
之后N行,每行两个数v[i],c[i],分别表示第i种物品的体积和价值;
输出
输出一个数,表示最大的价值
输入样例
2 11
2 3
6 14
输出样例
20

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int w[5000],v[5000],dp[5000];
int main()
{
	int n,m;
	cin>>n>>m;
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=w[i];j<=m;j++)
		{
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	cout<<dp[m]<<endl;
} 

多重背包

相对与01背包和完全背包,不同在于每个物体给定有限个个数
我们设这个每个不同物品的个数为 s[i]
状态转移方程式:dp[j]=max(dp[j],dp[j-kv[i]]+kw[i])
朴素的算法还是要仿照01背包
和完全背包的未优化前的三重循环相似,只是k的限制不一样

for(int i=1;i<=n;i++)
{
	for(int j=m;j>=w[i];j--)
	{
		for(int k=0;k<=s[i]&&j>=k*v[i];k++)
		{//从01背包的状态转移方程式,去增加i个物品拿k个的循环
			dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
		}
	}
}
二进制拆分思想
将第i个物品拆分成若干件物品,每件物品的体积和价值乘以一个拆分系数(1,21,22,…2k-1,Si-2k+1(最后这一项可能不是2的次方,是剩余数,与其它数加起来等于i))就可以转换为01背包的问题来求解,例如:Si=12,拆分系数为1,2,4,5转化为4件01背包的物品:(Vi,Wi),(2Vi,2Wi)(4Vi,4Wi)(5Vi,5Wi
如一个物品有12件,拆分系数为1,2,4,5

在这里插入图片描述

设sum为二进制拆分成为了多少个数拆分
vv[i],ww[i]分别为拆分后每个数的价值和重量

for(i=1;i<sum;i++)
{
	for(j=m;j>=ww[i];j--)
	{
		dp[j]=max(dp[j],dp[j-ww[i]]+vv[i])
	}	
} 

求背包问题的排列和组合数

核心状态转移方程:

dp[j]+=dp[j-w[i]];

排列组合只是内外循环换个位置就行
(组合不强调元素之间的顺序,排列强调元素之间的顺序)
有顺序的排列数(外层循环是遍历背包容量,内层循环是遍历物品)
如完全背包的排列数:(如下例三)

for(int j=w[i];j<=m;j++)
{
	for(int i=1;i<=n;i++)
	{
		dp[j]+=dp[j-w[i]];
	}
}

无顺序的组合数(外层循环是遍历物品,内层循环是遍历背包容量)
如完全背包的组合数:(如下例二)

for(int i=1;i<=n;i++)
{
	for(int j=w[i];j<=m;j++)
	{
		dp[j]+=dp[j-w[i]];
	}
}

01背包组合数状态转移方程为

for(int i=1;i<=n;i++)
{
	for(int j=m;j>=w[i];j--)
	{
		dp[j]+=dp[j-w[i]];
	}
}

例一(01背包组合数)
leetcode494.目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n=nums.size();
        int sum=0;
        for(int i=0;i<n;i++)//找所有数和
        {
            sum+=nums[i];
        }
        int l=(sum+target)/2;//找加或减的值,如示例得到3一定是4-1;
        vector<int> dp(l+1);
        if((sum+target)%2==1)//除不尽就不可能得到target
        {
            return 0;
        }
        dp[0]=1;
        for(int i=0;i<n;i++)
        {
            for(int j=l;j>=nums[i];j--)//01背包完全装满的方式数的固定套路
            {
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[l];
    }
};

例二:(完全背包组合数)
leetcode518.零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:

输入: amount = 10, coins = [10]
输出: 1

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

例三(完全背包排列数)
leetcode377.组合总数IV
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:

输入:nums = [9], target = 3
输出:0

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1);
        dp[0] = 1;
        for (int i = 1; i <= target; i++) {
            for (int num : nums) {
                if (num <= i && dp[i - num] < INT_MAX - dp[i]) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值