动态规划算法下的两个经典问题:投资问题和完全背包问题

中国蓬勃发展,国家日益昌盛。我们无不惊叹于变化之快。纵观五千年,又怎么想到现在的科技、饮食、房屋、衣裳发生了翻天覆地的变化。千秋万代,并未虚掷。
我们是新青年,国家的指挥棒交到了我们这一代。我们应该主动承担国家力量,民族力量。将祖国继续发扬光大。千载之后又会是一个不同的景色。
愿人族星火相传,奋飞不辍!

上一篇我们介绍了动态规划算法,我们直到动态规划是将原问题划分成有依赖关系的子问题,最后通过追踪解自底向上的归结原问题的解。这就是动态规划算法的实质内容

本篇主要介绍动态规划算法下的两个经典问题:投资问题和完全背包问题

1、投资问题

什么是投资问题

有m元钱,n项投资,fi(x):将x元投入第i个项目的效益。求使得的总效益最大的投资方案。

举个例子:现在有两个项目x是钱数(单位:元),fi(x):将x元钱投资到第i个项目产生的效益

注意:使用的是总共的x钱数投资两个项目,而不是分别投资。


将0元投资这两个项目,则最大收益就是0
将1元投资这两个项目,不难看出f1(1)+f2(0)=11,是最大收益
将2元投资这两个项目,不难看出f1(2)+f2(0)=12,是最大收益
将3元投资这两个项目,max(f1(0)+f2(3),f1(1)+f2(2),f1(2)+f2(1),f1(3)+f2(0))=f1(1)+f2(2)=16,是最大收益
同样的用4元或者5元投资这两个项目,所带来的最大收益分别是21和26

我们接下来对问题进行建模:
目标函数:利用所分配的投资产生最大效益
约束条件:是在总资金的条件下进行投资

下面我们来考虑用动态规划算法来解投资问题

动态规划算法来解投资问题

子问题的界定:由参数k和x界定
k:考虑对每个项目1,2,…,k的投资
x:投资总钱数不超过x的

接下来我们再看一下递推方程
设Fk(x):x元钱投给前k个项目的最大效益

我们可以这么想,假如我们要求Fk(x),即就是求x元钱投给前k个项目的最大效益。那不妨求p元钱(p<=x)投给前k-1个项目的最大效益Fk-1 ( p ) (p) (p),进而确定Fk(x)

我们进而是可以列出递推方程:

我们看到啊,Fk(x)的求解,就是去求用x-xk分配前k-1个项目所产生的最大效益。然而这个最大效益是在备忘录存着来。没错备忘录的作用就是存储最大的效益。
F1(x):就是在投资表中的用x钱投资第一个项目的收益

接下来我们看个例子:
这是一个投资——效益表

我们要先明确最小子问题是什么,然后才能从这个最小子问题开始算起;然后考虑计算顺序,保证后面的值在前面已经计算好。

这里我们看到第一个项目的最大收益就是投资对应的收益,即F1(0)=0,F1(1)=11,F1(2)=12,F1(3)=13,F1(4)=14,F1(5)=15。

我们能看到啊,前1个项目可以定为最小子问题,它的初值可以通过查表得到,不用计算。而后面的项目随着项目的增多,子问题的复杂性就会增强。因此我们根据项目序列的递增关系来计算,从而保证后面的值在前面已经计算好了。

我们通过上面的递推方程可以得知,用x-xk分配前k-1个项目所产生的最大效益越大以及x元钱投给前k个项目的最大效益越大,从而使得原问题的解达到最大。进而满足依赖关系。而对于xk为何值,这个是需要通过计算获取最优解来得到。

好,我们来继续看前两个项目的最大效益:
我们先看x=0,则最大效益是0
再来看xk=1,F2(1)=max{f2(1)+F1(1-1),f2(0)+F1(1-0)}=11
再来看xk=2,F2(2)=max{f2(2)+F1(2-2),f2(1)+F1(2-1),f2(0)+F1(2-0)}=12
再来看xk=3,F2(3)=max{f2(3)+F1(3-3),f2(2)+F1(3-2),f2(1)+F1(3-1),f2(0)+F1(3-0)}=16
同样的F2(4)=21,F2(5)=26
当然,这里得到的F2(1),F2(2),F2(3),F2(4),F2(5)要记录到备忘录里面。

那么如何去记录解?
我们用s数组来记录解。我们去记录在得到最大效益的时候,最后一个项目给了多少钱。
就如同上面的例子,在前两个项目的最大收益中。
xi(x):分配x元钱给前i个项目,在最大收益时,第i个项目得到了多少钱
x2(1):看到啊,F2(1)=f2(0)+F1(1-0)=11。此时,第2个项目得到了0元钱
x2(2):f2(0)+F1(2-0)=12。此时,第2个项目得到了0元钱
x2(3):f2(2)+F1(3-2)=16。此时,第2个项目得到了2元钱
同样的,我们也能得到x2(4)=3,x2(5)=4

ok,下面介绍一下如何追踪解
上面的投资问题的结果如图所示:

我们细想,原问题是用5元钱分配所有项目(这里就是4个项目),所得到的最大收益
这个最大收益是不是就是F4(5)(F4(5):用5元钱分配前4个项目得到的最大收益),那这个值就可以去衡量原问题的解。因此我们追踪解也要从x4(5)开始,自底向上追踪。

先看到x4(5)=1,说明达到最大收益的时候分配给最后一个项目,即第4个项目是1元钱。
那么第3个项目呢?
第3个项目就是x3(x),这个x就是5-1=4,就是用总共的5元钱-分配给第4个项目的钱数。
x3(5-1)=3。因此在得到最大收益时,分配给第3个项目3元钱。
同理x2(4-3)=0,x1(1-0)=1
也许你会问为什么要这么解?
我们看那个递推方程,我们既然知道F4(x5)=f4(x4)+F3(x-x4)。然而我们知道了在最大收益时,分配给第4个项目1元钱,这个可以通过代码啥的可以实现。则F3(4),这个通过查表即可得到41,此时分配给它的钱就是3。同样的也可以逆推出F2和F1中的x2和x1。就是通过前k-1个项目的最大收益+用剩下钱分配给第k个项目的收益。然而前k-1个项目的最大收益是保存在了我们的备忘录中,所以这个值不仅可以查到,而且它只计算了一次。没有重复计算。使得这个唯一确定的值+f4(x4)值就是最大收益。因此我们抛去f4(x4)的值,也就是前k个项目的最大收益F3(x-x4)。因此我们可以通过查表得到x3(x-x4)。故这样计算是合理的

对于代码分析,这里就不再分析了。大家可以通过算法逻辑来进行分析。
我们直接上代码:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int m=6,n=4;
		System.out.println("效益表如下:");
		System.out.println("x  f1(x)  f2(x)  f3(x)  f4(x)");
		int business[][]=new int[][] {{0,0,0,0},{11,0,2,20},{12,5,10,21},{13,10,30,22},{14,15,32,23},{15,20,40,24}};  //定义一个6*4的效益表
		for(int i=0;i<m;i++) {
			System.out.print(i+"    ");
			for(int j=0;j<n;j++)
				System.out.print(business[i][j]+"      ");
			System.out.println();
		}
		int temp[][]=new int[m][n];  //存放投资效益最大值
		int d[][]=new int[m][n];  //存放标记解
		
		int maxprofit=InvestBusiness(m, n, temp, d, business);
		System.out.println("最大利润是"+maxprofit);
//		for(int i=0;i<m;i++) {
//			System.out.print(i+"    ");
//			for(int j=0;j<n;j++)
//				System.out.print(temp[i][j]+"      ");
//			System.out.println();
//		}
		System.out.print("选取的合理规划是:");
		int t[]=Tracingsolution(m, n, d);
		for(int i=0;i<n;i++) {
			System.out.print("x"+(i+1)+"="+t[i]+"  ");
		}
		
	}
	
	//动态规划求解投资问题
		public static int InvestBusiness(int m,int n,int temp[][],int d[][],int b[][]) {
			int sum=-0x3f3f3f3f;
			//先考虑1个项目的情况(1个项目在0到m费用之间的效益)
			for(int i=0;i<m;i++) {
				temp[i][0]=b[i][0];
				d[i][0]=i;
			}
			
			//考虑多个(>=2)项目的情况(多个项目在0到m费用之间的效益)
			for(int i=1;i<n;i++)  //第二个项目开始
				for(int j=0;j<m;j++)
					for(int k=0;k<=j;k++) {
						sum=temp[j-k][i-1]+b[k][i];
						if(sum>temp[j][i]) {
							temp[j][i]=sum;
							d[j][i]=k;  //记最后一个项目给的钱数
						}
					}
			return temp[m-1][n-1];
		}
		
		//解的追踪
		public static int[] Tracingsolution(int m,int n,int d[][]) {
			int tracing[]=new int[n];  //将解寄存在数组里
			int s=m-1;
			tracing[n-1]=d[m-1][n-1];
			for(int i=n-2;i>=0;i--) {
			s=s-tracing[i+1];
			tracing[i]=d[s][i];
			}
			return tracing;
		}

接下来我们进行时间复杂度分析:

动态规划算法来解投资问题分析

我们看到代码,动态规划算法中加法和比较是在3层循环里面。
第1层循环是n的遍历,第2层循环是m的遍历,第3层循环也是m的遍历。我们不考虑它们遍历的个数,得出一个时间复杂度就是O(nm^2)。
再来看追踪解的过程,追踪解的过程是n的循环,而且就一层循环。所以是O(n)
即动态规划算法来解投资问题的时间复杂度就是O(nm^2)

这就是用动态归法算法解投资问题的内容

2、完全背包问题

什么是完全背包问题

问题描述:一个旅行者随身携带一个背包,可以放入背包的物品有n种,每种物品的重量和价值分别为wi,vi。如果背包的最大重量限制是b,每种物品可以放多个。怎么选择放入背包的物品使得背包的价值最大?
我们将上述变量设为正整数

来看一个例子:
放入背包的物品有4种,背包重量限制是10。
w1=2,w2=3,w3=4,w4=7
v1=1,v2=3,v3=5,v4=9
这里我们能通过蛮力法找到一个最大价值分配:
装第4号物品1个,再装第二号物品1个。总价值是12,且没有超重

我们进行问题建模:
目标函数:max(v1x1+v2x2+…+vixi):所产生背包最大价值量
约束条件:背包的限制重量,即不超过b的重量(<=b)

下面我们动态规划算法来解完全背包问题

动态规划算法解完全背包问题

我们子问题界定:有参数k和y决定
k:考虑对物品1,2,…,k的选择
y:背包总重量不超过y

我们来思考递推方程:
我们令Fk(y):装前k种物品,总重不超过y,背包达到的最大价值
我们想要去求Fk(y),那我们可以根据Fk-1(y)和Fk(y-wk)+vk的大小,谁大装谁
也就是说,y-wk的最大价值+本身的价值和与前k-1种物品的最大价值量进行比较,进而求得Fk(y)。
然后有几个初值需要考虑,物品种数为0的时候的最大价值量是0;重量限制为0,即没法装的时候此时最大价值量为0;y-wk<0的时候,此时装入的最大价值量是一个负无穷

那为什么是这么列呢?
我们可以这么想,有一个背包,里面通过计算y/w1。已经把第一种物品分配完毕,得到了在不同y的时候的最大价值量。

我们来装第2个物品,如果不能装,即就是第二个物品的重量超过了此时背包重量,那么最大价值量就是F1(y);如果能装,我们就要在y-w2这个基础上去考虑,因为y-w2,是可以装入第2号物品前提下的最大价值量,装进去价值量就会增加,然后再和此时的F1(y)进行比较,更新最大价值量

比如说:在k=2,y=5的时候,首先在y-w2=5-3=2前提下考虑,考虑在此时装入第2号物品时的价值量。再比如说y=6时,我们要先减去一个第2号物品的重量,然后再此时的价值量下考虑增加一个物品的最大价值量。

也就是说,如果能装。我们就把这个物品拿出去,回到y-wk此时的价值,直接在此时加上这个价值就行。为什么这么做,你想啊。我们要计算装第k个物品的最大价值。那根据动态规划算法最优子结构性质,减去这个质量的最优解是已经得到了,我们将这个最优解+此时的价值,再去比较,不就可以了。

好,我们分析完了之后。考虑最小子问题是什么,然后才能从这个最小子问题开始算起;然后考虑计算顺序,保证后面的值在前面已经计算好。

我们经过上面的递推方程,先处理第一号物品运算最为简单,随后再考虑第二号、第三号物品的时候运算会更为复杂。而且我们是按照物品种类考虑,先考虑第一号,再考虑第二号。所以就以第一号物品为最小子问题。

然后,我们再考虑追踪解
令ik(y):装前k种物品,总重不超过y,背包达到最大价值时装入物品的最大标号

我们根据上边的例子,来进行分析:
我们先考虑第1号物品。
y=1,不能装第1号物品,价值量为0
y=2,能装第1号物品,价值量为1,i1(2)=1
y=3,不能再装入1号物品,价值量为1,i1(3)=1
y=4,可以再装入1号物品,价值量为2,i1(4)=1
如此往复

下面考虑第2号物品
y=1,不能装入第2号物品,也不能装入第1号物品,价值量为0
y=2,不能装入第2号物品,能装入第1号物品,价值量为1,i2(2)=1
y=3,此时用3-w2=0,说明可以装入第2号物品,考虑F2(0),将F2(0)+v2与F1(3)进行比较,发现F2(0)+v2大。那么就记录它,i2(3)=2
y=5,此时用5-w2=2,考虑F2(2),将F2(2)+v2与F1(5)进行比较,发现F2(2)+v2大。那么就记录它,i2(5)=2
y=6,此时用6-w2=3,考虑F2(3),将F2(3)+v2与F1(6)进行比较,发现F2(3)+v2大。那么就记录它,i2(6)=2
如此往复

那如何追踪解?
在这里插入图片描述
也是一样的,我们可以从最后一个数据自底向上追踪解
i4(10)=4,那说明第4号物品可以装1个
10-w4=3
i4(3)=2,那说明第4号物品不能再装了,第3号物品也不能装,第2号物品是可以装的
3-w2=0
此时i2(0)=0,说明第1号不能再装了

为什么要这么做
从最后一个入手,我们发现F4(10)是由F4(3)+v4得到的。因此抛去第4号物品,剩下的承载量就是3了。那么计算F4(3),F4(3)是怎么产生的,这个是由F2(3)产生的,即此时物品最大标号是2。

编写代码可以参考上边的递推方程和算法分析
下面直接上代码:

public static void main(String[] args) {
		// TODO Auto-generated method stub
		Materia materia[]=new Materia[4];
		materia[0]=new Materia(2,1);
		materia[1]=new Materia(3,3);
		materia[2]=new Materia(4,5);
		materia[3]=new Materia(7,9);
		
		int maxWeight=10;
		int maxValue[][]=new int[materia.length][maxWeight+1];
		int d[][]=new int[4][maxWeight+1];
		
		int maxvalue=MaxloadingValue(materia, maxWeight, maxValue, d);
		for(int i=0;i<maxValue.length;i++) {
			for(int j=1;j<maxValue[i].length;j++) {
				System.out.print(maxValue[i][j]+"  ");
			}
			System.out.println();
		}
		System.out.println("背包的最大价值是:"+maxvalue);
		
		int solution[]=Tracingsolution(materia.length, maxWeight, d, materia);
		System.out.print("选取物品的最优解是:");
		for(int i=0;i<solution.length;i++) {
			System.out.print("x"+(i+1)+"="+solution[i]+"    ");
		}
	}
	
	//动态规划求解背包问题
	public static int MaxloadingValue(Materia materia[],int maxWeight,int maxValue[][],int d[][]) {
		int maxvalue=0,value,num=d[0][0];
		if(materia.length==0||maxWeight==0)
			return maxValue[materia.length-1][maxWeight];
		//第一个物品的装包情况
		for(int i=1;i<=maxWeight;i++) {
			value=(i/materia[0].weight)*materia[0].value;
			if(value>maxvalue) {
				num=1;
				maxvalue=value;
			}
			maxValue[0][i]=maxvalue;
			d[0][i]=num;
		}
		
		//对第i(i>1)个物品的装包情况
		for(int i=1;i<materia.length;i++) {
			for(int j=1;j<=maxWeight;j++) {
				num=d[i-1][j];
				if(j-materia[i].weight<0) 
					value=-0x3f3f3f3f;
				else
					value=maxValue[i][j-materia[i].weight]+materia[i].value;
				if(value>=maxValue[i-1][j]) {
					if(j-materia[i].weight>=0)
						num++;
					maxValue[i][j]=value;
				}
				else 
					maxValue[i][j]=maxValue[i-1][j];
				d[i][j]=num;
		}
	}
		return maxValue[materia.length-1][maxWeight];
  }
	
	//追踪解方法
	public static int[] Tracingsolution(int n,int maxWeight,int d[][],Materia materia[]) {
		int tracing[]=new int[n];  //将解寄存在数组里
		int num=0,j=n-1,y=maxWeight;
		while(y>0&&j>=0) {
			j=d[j][y]-1;  //拿到解数组中的最后一个值
			tracing[j]=++num;  //若是能取到该物品,则对应数组下标中的值+1
			y=y-materia[j].weight;
			while(y>0&&d[j][y]==j+1) {
				tracing[j]=++num;
				y=y-materia[j].weight;
			}
			num=0;
		}
		
		return tracing;
	}

public class Materia {
	public int weight;
	public int value;
	
	public Materia() {
		// TODO Auto-generated constructor stub
	}

	public Materia(int weight, int value) {
		super();
		this.weight = weight;
		this.value = value;
	}
	
	
}

接下来我们来看时间复杂度。

动态规划算法来解完全背包问题分析

我们通过代码,在动态规划算法里是有两层循环。第一层是n的循环,第二层是b的循环。我们得到了时间复杂度是O(nb),然后解的追踪是O(b^2)的时间复杂度

但是,大家细想下。b代表的是数字大小。如果b是更大的数的话。那么y的遍历循环就得从0到b。如果是素数测试,从1到n都要进行遍历寻找素数。那么这个复杂度可不是O(n2)。因为计算机存储数的大小是bit位存储,表达n需要logn位的比特。随着n的一次次增加,比特位也在一次次增加。那么计算量可就不是多项式关系了。因为输入规模是数大小的比特位,所以x=logn的这个x才是真正的输入规模,则n=2x。即素数测试是O(2^n),是一个指数量级

我们把这种看似是多项式算法,实际上时间复杂度与输入规模呈指数关系,而与数值呈多项式关系的算法成为伪多项式时间算法

正如本算法一样。n是个数,b是从1开始的数字大小,它的输入规模是logb。因此完全背包问题是一个伪多项式时间算法。它的时间复杂度是一个指数量级

我理解的伪多项式时间算法,就是有逐步递增的数字元素,且以此进入循环遍历的算法问题。

或者是说,从1到n循环遍历除以2。因为1到n的比特位逐步增长。所以使得大的数字(很多比特位)在计算时会变慢。就像你笔算从1到10000000除以13的时候,时间增长趋势是不一样的。

除了完全背包问题,当然也有最为熟悉的0-1背包问题
0-1背包问题:就是可以放入背包的物品有n种,每种物品的重量和价值分别为wi,vi。如果背包的最大重量限制是b,每种物品只能拿一个。要么拿,要么不拿。怎么选择放入背包的物品使得背包的价值最大

以上就是动态规划算法解完全背包问题的内容

      指薪修祜,永绥吉劭。矩步引领,俯仰廊庙。束带矜庄,徘徊瞻眺。《千字文》
  • 13
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完全背包问题经典动态规划问题之一。这里给出完全背包问题动态规划算法实现: 1. 定义状态 设 $f(i,j)$ 表示前 $i$ 个物品放入容量为 $j$ 的背包中所获得的最大价值。 2. 状态转移方程 对于每个物品 $i$,考虑将其放入背包中或不放入背包中两种情况: - 不放入背包中:此时 $f(i,j)=f(i-1,j)$,即前 $i-1$ 个物品放入容量为 $j$ 的背包中所获得的最大价值。 - 放入背包中:此时 $f(i,j)=f(i,j-w_i)+v_i$,即前 $i$ 个物品放入容量为 $j-w_i$ 的背包中所获得的最大价值再加上物品 $i$ 的价值 $v_i$。 综上所述,状态转移方程为: $$f(i,j)=\max\{f(i-1,j),f(i,j-w_i)+v_i\}$$ 3. 初始化 当背包容量为 $0$ 时,无论放入哪些物品,价值都为 $0$。因此,$f(i,0)=0$。 4. 算法实现 根据上述状态转移方程和初始化条件,可以编写完全背包问题动态规划算法实现: ```python def knapsack_complete(w, v, c): n = len(w) f = [[0] * (c + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(1, c + 1): f[i][j] = f[i - 1][j] if w[i - 1] <= j: f[i][j] = max(f[i][j], f[i][j - w[i - 1]] + v[i - 1]) return f[n][c] ``` 其中,$w$ 和 $v$ 分别表示物品的重量和价值,$c$ 表示背包的容量。首先定义一个 $n+1$ 行、$c+1$ 列的二维列表 $f$,并将其所有元素初始化为 $0$。然后,按照上述状态转移方程进行计算,最终返回 $f(n,c)$ 即可得到最大价值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值