动态规划笔记

动态规划知识点

背包问题

动态规划,算法课的时候其实就学过,但是在ACM中的应用远比算法课学的01背包等问题要多。

01背包问题

N 件物品和一个容量是 V的背包。每件物品只能使用一次。

i件物品的体积是 vi,价值是 wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 v**i,w**i,用空格隔开,分别表示第 i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

简单理解题意,就是有一堆东西,他们有两个属性,一个是体积,一个是价值。

我们还有一个背包,背包只有一个属性,就是体积

要把选择一些东西装入背包中,每个东西只能选1次或者不选(这就是所谓的01)

最终目的是,在物品总体积不超过背包总体积的情况下,使得背包中的价值最大

分析dp的方法:状态表示(集合,属性) 状态计算

状态表示

集合:f[i,j],表示从前i个物品中取,总体积不超过j,总价值

属性:总价值的最大值

状态计算

状态实际上就是针对上面我们分析出来的集合进行拆分,拆分成能够计算的更小的状态子集

对于一个f[i,j]我们可以认为他由两个子集构成,选第i个物品,不选第i个物品

如果不选第i个物品,f[i,j]=f[i-1,j]

如果选第i个物品,用子集来表示就是f[i,j]=f[i-1,j-v[i]]+w[i] 意思就是去掉这个物品的最大价值,加上这个物品的价值,就是现在的价值

综上我们有f[i,j]=max(f[i1,j],f[i1,jv[i]]+w[i])

01背包一维优化

上述式子已经足以解决这个问题,但f[i,j]是一个二维矩阵,满足以下条件的状态转移方程可以优化为一维

  • f[i,]仅仅依赖f[i-1,]
  • f[i,j]依赖的f[i,g(j)]中的g(j)均在j的同侧
    • 如果在j的右侧,就对j顺序遍历
    • 如果在j的左侧,就对j逆序遍历
    • 上述两条也不一定,需要具体问题具体分析,在01背包问题中是这样的

本质上是针对一个数组的重复利用,计算完f[i-1,]后,可以原地利用f[i-1,]的元素来计算下一级f[i,]的元素,那么这一维其实可以去掉

遍历顺序可以这样理解,以f[i,j]=max(f[i1,j],f[i1,jv[i]]+w[i])为例,

我们通过一维优化把它优化成f[j]=max(f[j],f[jv[i]]+w[i])以后

f[j]会依赖f[j-v[i]]也就是j左侧,那么在更新第i层的时候,j左边的应该还是i-1层的数据,没有被改动,因此从后向前遍历。

代码中的一些注意点

i和j都是从1开始考虑,这是针对v和w而言,但是在对f进行状态转移的时候,要处理j=v[i],因为当j=v[i]时,右侧第二个式子的含义就是只选第i个物品的方案。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int v[N];
int w[N];
int f[N];
int main()
{
	int n,m;
	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--)
//		{//j=0需要初始化 
//			f[j]=max(f[j],f[j-v[i]]+w[i]);
//		}
		for(int j=m;j>=1;j--)
		{
			//这里要考虑0是因为,j=v[i]时,f[0]=0,第二个式子代表的就是只选第i个物品 
			if(j>=v[i])
			{
				f[j]=max(f[j],f[j-v[i]]+w[i]);
			}
		}
	}
	cout<<f[m];
}

完全背包问题

N 种物品和一个容量是 V的背包,每种物品都有无限件可用。

i种物品的体积是 v**i,价值是 w**i

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 v**i,w**i,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

可以看到,完全背包与01背包的不同仅仅在于对于每一种物品,完全背包问题不限制使用的个数,我们用同样的dp分析方式来思考这个问题

状态表示

集合:f[i,j],表示从前i个物品中取,总体积不超过j,总价值

属性:总价值的最大值

状态计算

01背包问题中,我们是分为0,1两种状态,这里由于不限制数量,我们应该对选择数量k进行枚举

对于f[i,j]:

  • 选0个第i类物品,为f[i-1,j]
  • 选1个第i类物品,为f[i-1,j-v[i]]+w[i]
  • ……
  • 选k个第i类物品,为f[i-1,j-k*v[i]]+k*w[i]

因此f[i,j]=maxk(f[i1,j],f[i1],jv[i]+w[i],,f[i1,jkv[i]]+kw[i]),其中k=jv[i]

到此位置应该用暴力的方法可以解决,后面针对这个式子进行优化

f[i,jv[i]]=maxk(f[i1,jv[i]],f[i1],j2v[i]+w[i],,f[i1,j(k+1)v[i]]+(k+1)w[i]),其中k=jv[i]v[i]

注意k和项数的变化,这个式子就是上面式子去掉第一项f[i-1,j]

合并这两个式子,我们可以得到:

f[i,j]=max(f[i1,j],f[i,jv[i]])

右侧的第一项就是上面式子少掉的一项,第二项就是上面的式子本身

在进一步优化成一维f[j]=max(f[j],f[jv[i]])需要注意的是,此时f[j]左侧第二项本身是第i轮的产物,而f[j]就是f[j]本身,因此完全背包与01背包的差别就是在枚举j的时候从前向后枚举

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int v[N];
int w[N];
int f[N];
int main()
{
	int n,m;
	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];
}

多重背包问题

N 种物品和一个容量是 V的背包。

i种物品最多有 s**i 件,每件体积是 v**i,价值是 w**i

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品种数和背包容积。

接下来有 N行,每行三个整数 v**i,w**i,s**i,用空格隔开,分别表示第 i种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

提示:

本题考查多重背包的二进制优化方法。

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

多重背包问题的问题情景与之前两个又有所区别。

多重的意思就是对每种物品的个数都有限制si,因此需要进一步讨论。

状态表示

集合:f[i,j],表示从前i个物品中取,每个物品不超过si个,总体积不超过j,总价值

属性:最大值

状态计算

朴素的想法就还是针对每类物品,取0-si个进行讨论

状态转移方程就是f[i,j]=maxk(f[i1,jv[i]k]+w[i]k),其中k=0,1,2,,s[i]

这中思路就和完全背包是类似的,但是复杂度是O(N*V*S)

下面一种方法可以优化成O(N*V*log(s))

基本的思想是针对枚举进行优化,考虑到任何一个整数都可以被若干个2的某次方加和得到

这样我们就可以不用枚举s,转而把对s的枚举拆解若干个2的某次方,对s的枚举也就转化为对这些2的某次方的枚举

更近一步,我们把这些2的某次方打包成一个整体,就可以看成新的物体,这些新物体只能选0或1次,从而转化为一个01背包问题。

一个例子

s=200

拆解为

1 2 4 8 16 32 64 73 最后的73是200-(前面数字之和)

在原来的方法中,对于200个物品,相当于是对s从0到200进行枚举,在这些里面选一个满足条件的最小的

从循环的角度就是 N->V->S 选定一些物品,选定一个体积,对物品个数枚举

现在其实是扩大了N,把对里面的S筛选整合成一些新的物体,然后对新的物体进行筛选

循环的角度就是 N*logS->V 把物品个数转化为新物品,但是每个物品只能选1次或0次,放到外层,去掉最内层的枚举

感觉这一块理解的仍然不是很到位,从问题和代码的角度目前是这样理解的

于是我们就只需要先把每一类的si个物品,转化为相应的logsi + 1个捆绑好的商品

一般形式是1 2 4 8 16 32 64 73

作为新的商品,然后对这些商品进行01背包即可

所以代码步骤就是

  • 根据输入物品的价值,重量,个数。将其打包重组,这里需要用一个idx来指示重组数组的下标

  • 重组的方法是先计算出最后一个k,算出前面k个次幂,最后填上最后一个s-2^(k+1)

  • idx作为01背包中的n,进行01背包的处理

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=12000;
    
    int v[N],w[N];
    int f[N];
    
    
    int main()
    {
    	int n,m;
    	cin>>n>>m;
    	int idx=0;
    	for(int i=1;i<=n;i++)
    	{
    		int a,b,s;
    		cin>>a>>b>>s;
    		int k=(int)((log(s)/log(2)));
    //		cout<<"k:"<<k<<endl;
    		for(int j=1;j<=k;j++)
    		{
    			idx++; 
    			v[idx]=pow(2,j-1)*a;
    			w[idx]=pow(2,j-1)*b;
    //			cout<<v[idx]<<endl;
    //			idx++;
    		}
    		//这里漏考虑最后一个填补的东西
    		//这里填补的对象应该是前面所有数字之和,用s去减 
    		if((s-pow(2,k)+1)>0)
    		{
    			idx++;
    			v[idx]=(s-pow(2,k)+1)*a;
    			w[idx]=(s-pow(2,k)+1)*b;
    //			idx++;
    		}
    		
    	}
    	n=idx;
    	//这个也漏了 
    	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];
    }
    

分组背包问题

N 组物品和一个容量是 V的背包。

每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vij,价值是 wij,其中 i 是组号,j是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式

第一行有两个整数 NV,用空格隔开,分别表示物品组数和背包容量。

接下来有 N组数据:

  • 每组数据第一行有一个整数 S**i,表示第 i 个物品组的物品数量;
  • 每组数据接下来有 S**i 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100

0<S**i≤100
0<vij,wij≤100

输入样例

3 5
2
1 2
2 4
1
3 4
1
4 5

输出样例:

8
posted @   zcaoyao  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示