虾扯编程4——动态规划与背包问题
5秒导读:本期介绍动态规划算法,以及经典的背包问题,算法导论中的装配线问题
注:可以直接cv的代码在最后。
最近《极简微积分》写的有些多,下几期我们都不谈数学,说说AI的东西。
玩ai37,讲到隐马尔可夫模型用于分词,一个句子“咖啡味道不错”的分词就是找到其背后隐藏的状态序列“be/be/be(咖啡,味道,不错)”。假设我们已经有了初始分布,状态转移矩阵,观测状态分布,找出句子对应状态的最大概率,不就是分词么?
OK,找出最大概率的算法叫做维特比算法,但是在说维特比算法之前我们先说一个玩编程的小伙伴都知道的“动态规划”。
动态规划对应的问题通常有很多可行解,每个解有一个值,而动态规划算法要做的就是找到“最优解(通常就是最大/最小值)”
“动态规划”或许真的有那么点点绕,不过我们还是有重点可以抓,第一“把问题分解为更小,更简单的子问题”,第二“要找最优解,所以每一个子问题都得是最优解”,第三“利用之前的最优解,获取现在的最优解”
好吧,抽象的东西到此为止,我们还是看个具体例子——最最典型的背包问题入手。
问题描述如下:背包最大容量为M,N个物品每个只有一个,物品的体积从W1到Wn,价值从V1到Vn,请问怎么装,才能带走价值最高的物品组合。
我们再具体一点,背包容量为8,物品体积价值如下表,请问该怎么装?
用背包问题对应看动态规划,首先更小的子问题是什么?当然我么从最最简单的情况也就是,只有第一个物品开始,背包容量从1到8。
用二维数组maxValue[i][j],表示只有前i个物品,背包容量为j时的最优解。只有第一个物品,背包容量从1到8,这时候背包容量小于2最优解是0,大于2就是3,这当然没啥好解释的。ok,我们让情况复杂一些,现在有第一第二个物品,我们要做的事利用之前的最优解,获取当前的最优解,多了一个物品,2种选择,要么放进去,要么放进去,不放进去时最优解为maxValue[1][j],放进去时最优解为把第二个物品需要的空间腾出来,剩下的空间放第一个物品的最优解加上放入的第二个物品的价值maxValue[1][j-w[2]]+v[2],从中选择选一个大的出来就是有2个物品时的最优解——maxValua[2][j],那么有i个物品呢?以此类推。
maxValue[n-1][j]和maxValue[i-1][j-w[i]]+v[i],中选一个大的(这里之所以能这么做的关键在于maxValue中每一个元素都是子问题的最优解)。
下面给出完整实现代码java实现,其实上面的描述一看就好,不要去绕,记住分解为更小更简单的子问题,找子问题最优解(通常是简单到没得选择之时的解),利用子问题最优解递推新问题最优解才是真的
对于完全背包(每个物品都有无数个),其实只需要改一点点:
对比一下只有第一个物品时的最优解(再重复下,第一个索引1代表只有第1个物品,第二个索引0-8代表背包容量为0-8时的最优解)每个物品只有一个:
完全背包,第一个物品最多放8/2=4个。最大价值为12
说完背包问题,我们说一个稍微复杂些的,来自算法导论的例子,6个步骤完成一次装配,2条装配线,每一条都可以完成每一个步骤,装配一个东西可以随时换线,但是换线需要时间,装配时间与换线时间如下图,请问如何装配一个最快:
对于这个问题,首先我们划分子问题,最快速度完成的装配(下图红圈),不是从“1”,就是从“2”完成的,其中的“1,2”代表走到那个位置时的最优解,而“1”不是从“3”就是从“4”走过来的,“2”也是(这好像是废话),但是如果从起点“x”和“y”看,问题就简单了,一步步走,选出每一个最优解不就好了?
现在我们用fast[i][j]表示在第i条线,完成第j个步骤的最优解。下图为每一个fast[i][j]的值
直接给出完整java实现:
最后啰嗦一句,代码是死的,人是活的,不必在乎代码具体什么样子,关键记住算法到底在做什么,最后附上算法导论对动态规划的总结
public int maxKnapsack() {
int[] w = { 0 , 2 , 3 , 4 , 5 }; //商品的体积
int[] v = { 0 , 3 , 4 , 5 , 6 }; //商品的价值
int space = 8;
int [][] maxvalue=new int[5][9];
for(int i=1;i<=4;i++){
for(int j=1;j<=space;j++){
if(j< w[i]){maxvalue[i][j] = maxvalue[i-1][j];
//背包装不下,所以最优解只能等同只有前i-1个的最优解
}
else{
maxvalue[i][j] = max(maxvalue[i-1][j],maxvalue[i-1][j-w[i]] + v[i] );
//腾出空间放入第i个,与只有前i-1个比较,选择较大的做为最优解
}
}
}
return maxvalue[4][8];
}
public int fast() {
int[][] fast = new int[2][6];
int S[][] = {{9, 9, 3, 4, 8, 4}, {12, 5, 6, 4, 5, 7}};
int T[][][] ={
{{0 ,0 ,0 ,0 ,0},{2, 1, 2, 2, 1} },{{2, 3, 1, 3, 4},{0 ,0 ,0 ,0 ,0} },
//换线的时间,第一个索引为线编号,第2,3个索引表示第n个步骤,换到第m条线用时
//自己到自己也就是索引1=索引2用时全部为0
};
for (int j = 0; j < 6; j++) {
for (int i = 0; i < 2; i++) {
if (j == 0) {
//第一个步骤没得选择
fast[i][j] = S[i][0];
} else {
int min = 999;
for (int k = 0; k < 2; k++) {//现在是第i条线,第j个步骤的最优解,
//这个步骤反正是从其他步骤最优解来的
//所以全部循环一遍,只有两条线其实一层循环加if else就够了
//这里3层循环是为了处理任意条线。另外if写多了也难看
int time = T[i][k][j-1] + S[i][j];
if (min > fast[k][j - 1] + time){
min = fast[k][j - 1] + time;
}
}
fast[i][j] = min;
}
}
}
return min(fast[0][5]+3 , fast[1][5]+2 );
}