初始动态规划:如何巧妙解决“双十一”购物时的凑单问题?

初始动态规划:如何巧妙解决“双十一”购物时的凑单问题?

淘宝有“双十一”购物节有促销活动,比如“满200减50”,如果你女朋友购物车中有n个想买的东西,从中挑选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度的接近满减条件

动态规划学习路线

动态规划比较适合用来求解最优问题,比如求最大值、最小值

第一节:通过两个经典的动态规划问题模型,展示我们为什么需要动态规划,以及动态规划解题方法是如何演化出来的

第二节:总结动态规划适合解决问题的特征,以及动态规划解题思路

第三节:实战解决经典动态规划问题

0-1背包问题

//回溯算法实现。注意:把输入的变量都定义成了成员变量
private int maxW = Integer.MIN.VALUE;  //结果放到maxW中
private int[] weight = {2,3,4,6,3}  //物品重量
private int n = 5 ;//物品个数
private int w = 9 ;//背包承受的最大重量
public void f(int i , int cw){  //调用f(0,0)
	if(cw == w || i == n){   //cw == w 表示装满了,i == n表示物品都考察完了
		if(cw > maxW) maxW = cw ;
		return;
	}
	f(i+1,cw);  //选择不装第i个物品
	if(cw + weight[i] <= w){
		f(i+1,cw + weight[i]);  //选择装第i个物品
	}
}

假设背包的最大承载重量是9,有5个不同物品,分别是2,2,4,6,3,把例子的回溯求解过程用递归树画出来,则是:

​ f(0,0)

​ f(1,0) f(1,2)

​ f(2,0) f(2,2) f(2,2) f(2,4)

​ f(3,0) f(3,4) f(3,2) f(3,6) f(3,2) f(3,6) f(3,4) f(3,8)

递归树中每个节点表示一种状态用(i,cw)表示,i表示将要决策第几个物品是否装入背包,cw表示当前背包中的总重量,比如(2,2)表示将要决策第2个物品是否装入背包,在决策前,背包中物品的总重量是2,

递归树中,有些问题被重复计算了两次,可以借助递归那一节将的“备忘录”解决,记录以及计算好了的f(i,cw),避免冗余

private int maxW = Integer.MIN_VALUE;  //结果放到maxW中
private int[]  weight = {2,2,4,6,3} ;   //物品重量
private int n = 5 ;//物品个数
private int w = 9;//背包承受的最大重量
private boolean[][] mem = new boolean[5][10];  //备忘录,默认值是false
public void f(int i , int cw){  //调用f(0,0)
	if(cw == w || i == n){   //cw == w表示装满了,i == n表示物品都考察完了
		if(cw > maxW) maxW = cw;
		return;
	}
	if(mem[i][cw])  return;   //重复状态
	mem[i][cw] = true;   //记录(i,cw)这个状态
	f(i+1,cw) ; //选择不装第i个物品
	if(cw +weight[i] <= w){
		f(i+1,cw +weight[i]);   //选择装第i个物品
	}
}

那么动态规划的做法是:将整个求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中,每个物品决策(放入或者不放入背包)完之后,背包中的物品重量会有多种情况,即会达到多种不同的状态,对应到递归树中,就是有很多不同的节点,把每一层重复的状态合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。可以通过合并每一层的重复的状态,保证每一层不同的状态个数都不超过W

用一个二维数组states[n][w+1],记录每层可以达到的不同状态,第0个(下标从0开始编号)物品的重量是2,要么装入背包,要么不装入背包,决策完之后,会对应背包的两种状态,背包中的物品的总重量是0或2,用states[0][0]=true 和states[0][2]=true来表示两种状态

第1个物品的重量也是2,基于之前的背包状态,在这个物品决策完之后不同的状态有3个,背包中的物品总重量分别是0(0+0) , 2(0+2 or 2+0) ,4(2+2)用states[1][0]=true ,states[1][2]=true,states[1][4]=true来表示三种状态

以此类推,直到考察完所有物品后,整个states数组计算出来,0表示false,1表示true,只需要在最后一层找到值为true的最接近w=9的值,就是背包中物品总重量最大值

weight:物品重量      n:物品个数 , w:背包可承载重量
public int knapsack(int[] weight , int n ,int w){
	boolean[][] states = new boolean[n][w+1];  //默认值false
	states[0][0] = true;   //第一行的数据要特殊处理,可以利用哨兵优化
	if(weight[0] <= w){
		states[0][weight[0]]  = true;
	}
	for(int i = 1; i < n ; ++i){  //动态规划状态转移
		for(int j = 0 ; j <= w ; ++j){   // 不把第i个物品放入背包
			if(states[i -1][j]  == true) states[i][j] = states[i -1][j];
		}
		for(int j = 0 ;j <= w-weight[i] ; ++j){   //把第i个物品放入背包
			if(states[i-1][j]  == true) states[i][j+weight[i]]  =true;
		}
	}
	for(int i = w ; i >= 0 ;--i) {   //输出结果
		if(states[n-1][i]  == true)  return i;
	}
	return 0 ;
}

把问题分为多个阶段,每个阶段对应一个决策,记录每一个阶段可达的状态集合,然后通过当前阶段的状态集合来推导下一个阶段的状态集合

尽管动态规划的执行效率比较高,但是需要额外申请一个n乘以w+1的二维数组,可以降低空间消耗吗?

只需要一个大小为w+1的一维数组可以解决这个问题

public static int knapsack2(int[] items , int n , int w){
	boolean[] states = new boolean[w+1];//默认值false
	states[0] = true;   //第一行的数据要特殊处理,可以利用哨兵优化
	if(items[0] <= w){
		states[items[0]] = true;
	}
	for(int i = 1 ; i < n ; ++i){    //动态规划
		for(int j = w-items[i] ; j >= 0 ; --j){   //把第i个物品放入背包
			if(states[j] == true)  states[j+items[i]] = true;
		}
	}
	for(int i = w ; i >= 0 ; --i){   //输出结果
		if(states[i]  == true) return i ;
	}
	return 0 ;
}

0-1 背包问题升级版

引入物品价值这一变量,选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少?

利用回溯算法解决:

private int maxV = Integer.MIN_VALUE;//结果放到maxV中
private int[] items={2,2,4,6,3}; //物品的重量
private int[] value={3,4,8,9,6};//物品的价值
private int n = 5;//物品个数
private int w = 9 ;//背包承受的最大重量
public void f(int i , int cw,int cv){  //调用f(0,0,0)
	if(cw == w || i == n){//cw == w 表示装满了,i==n表示物品都考察完了
		if(cv > maxV) maxV = cv;
		return;
	}
	f(i+1,cw,cv);  //选择不装第i个物品
	if(cw + weight[i] <= w){
		f(i+1,cw +weight[i],cv+value[i];  //选择装第i个物品)
	}
}

需要3个变量(i,cw,cv)来表示一个状态,i表示即将决策第i个物品是否装入背包,cw表示当前背包中物品的总重量,cv表示当前背包种物品的总价值

有几个节点的i和cw是完全相同的,比如f(2,2,4) & f(2,2,3),总重量一样的情况下,f(2,2,4)这种状态对应的物品总价值更高,舍弃f(2,2,3),沿着f(2,2,4)这条决策走下去

用动态规划:

还是把整个求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中,每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态

用一个二维数组states[n][w+1],来记录每层可以达到的不同状态,这里数组存储的是当前状态对应的是最大总价值,把每一层中(i,cw)重复的状态合并,只记录cv值最大的状态,然后基于这个状态来推导下一层的状态

public static int knapsack3(int[] weight,int[] value,int n ,int w){
	int[][] states = new int[n][w+1];
	for(int i = 0 ; i < n ; ++i){   //初始化states
		for(int j = 0 ; j < w+1;++j){
			states[i][j] = -1;
		}
	}
	states[0][0] = 0;
	if(weight[0] <= w){
		states[0][weight[0]] = value[0];
	}
	for(int i = 1;i < n ;++i){   //动态规划,状态转移
		for(int j = 0 ; j <= w;++j){   //不选择第i个物品
			if(states[i-1][j] >= 0) states[i][j] = states[i-1][j];
		}
		for(int j = 0;j <= w-weight[i] ;++j){   //选择第i个物品
			if(states[i-1][j] >= 0 ){
				int v = states[i-1][j] + value[i];
				if(v > states[i][j+weight[i]]){
					states[i][j+weight[i]] = v;
				}
			}
		}
	}
	
	//找出最大值
	int maxvalue = -1;
	for(int j = 0 ; j <= w ;++j){
		if(states[n-1][j] > maxvalue) maxvalue = states[n-1][j];
	}
	return maxvalue;
}

对杨辉三角进行一些改造,每个位置的数字可以随意填写,经过某个数字只能到达下面一层相邻的两个数字,如果你在第一层往下移动,把移动到最底层所经过的所有数字之和,定义为路径的长度,编程求出从最高层移动到最底层的最短路径长度

​ 5

​ 7 8

​ 2 3 4

​ 4 9 6 1

​ 2 7 9 4 5

int[][] matrix={{5},{7,8},{2,3,4},{4,9,6,1},{2,7,9,4,5}};

public int yanghuiTriangle(int[][] matrix){
	int[][] state = new int[matrix.length][matrix.length];
	state[0][0] = matrix[0][0];
	for(int i = 1 ; i < matrix.length ; i++){
		for(int j =0 ; j < matrix[i].length;j++){
			if(j == 0)  state[i][j] = state[i -1][j] + matrix[i][j;]
			else if(j == matrix[i].length -1)  state[i][j ] = state[i -1][j - 1] + matrix[i][j];
			else{
				int top1 = state[i- 1][j -1];
				int top2 = state[i - 1][j];
				state[i][j] = Math.min(top1,top2) + matrix[i][j];
			}
		}
	}
	
	int minDis = Integer.MAX_VALUE;
	for(int i = 0 ; i < matrix[matrix.length - 1] . length;i++){
		int distance = state[matrix.length -1][i];
		if(distance < minDis)  minDis = distance;
	}
	return minDis;
}

发布了75 篇原创文章 · 获赞 9 · 访问量 9181

猜你喜欢

转载自blog.csdn.net/ywangjiyl/article/details/104717901