初始动态规划:如何巧妙解决“双十一”购物时的凑单问题?
淘宝有“双十一”购物节有促销活动,比如“满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;
}