动态规划和贪婪算法是非常相似的,只是贪婪算法每一步都寻找当前情况下最好的下一步算法,选好了就不能改变,而动态规划每次执行下一步时都会重新规划之前的步数,保证当前步加上之前的步数的结果为最好。
下面我们将以01背包问题来介绍动态规划算法:
从前往后递推的动态规划算法请参考这篇博客:
https://blog.csdn.net/Du_Shuang/article/details/81985214
下面将介绍从后往前递推的动态规划算法:
01背包问题之前介绍过,上面的博客也说的很清楚,就不重新介绍了。
假设f(i,y)表示剩余容量为y,剩余物品为i,i+1,…n的背包问题的最优解的值,即
当判断最后一个物品是否应该放入背包时,如果物品n的体积大于背包剩余容量,那么物品n不能被放入背包于是f(n,y)=0,否则物品n被放入背包,这时最优解的值为 pn p n 即物品n的价值。
判断第i个物品是否应该放入背包时,如果这时的背包剩余容量小于物品i的体积则不放入该物品,此时f(i,y)=f(i+1,y),否则我们将决定是否将该物品放入背包,如果不放入那么我们的f(i,y)=f(i+1,y),如果放入那么我得重新规划后面的物品是否应该放入背包,因为放入物品i后后面的物品可能就放不下了,我们判断的依据是剩余价值的大小,放入大我们就选择放入,否则就不放入。
假设n=3,w=[100,14,10],p=[20,18,15],c=116,分别为物品数量,重量,价值,和背包容量。
利用上面两个递推式对上述问题进行求解。
首先求边界即第一个递推式。
利用第二个递推式有:
以上其实就是从后往前,将每一步的情况都进行了列举。最后我们求f(1,y)的时候并不需要将每种情况都列出来,因为我们已经知道了y的值,所以可以直接将y带入递推公式去进行求解。
接下来我们求解到底我们将哪些物品放入了背包。
如果我们没有将物品1放入背包那么有f(1,116)=f(2,116)这里f(2,116)=33不等于f(1,116)=28,所以第一个物品放入了背包
f(2,116-100)不等于f(3,16)所以第二个物品放入了背包,f(3,2)=0,所以物品3没有放入背包。
应用动态规划求解的步骤如下:
1.证实最优原则是适用的。
2.建立动态规划的递归方程式。
3.求解动态规划的递归方程式以获得最优解。
4.沿着最优解的生成过程进行回溯。
存在的问题:
编写一个简单的递归程序来求解动态规划递归方程是一件很诱人的事。然而,如果不能避免重复计算,递归程序的复杂度将非常可观,动态递归方程也可以用迭代方式来求解,这时就自然避免了重复计算,实际上迭代程序与避免了重复计算的递归程序有同样的复杂度,但是迭代不需要额外的栈空间,因此将运行的更快。
//背包问题的递归函数
int f(int i,int theCapacity)
{//返回f(i,theCapacity)的值
if(i==numberOfObjects)
return (theCapacity<weight[numberOfObjects]?0:profit[numberOfObjects]);//相当于之前递推公式的第一个公式,边界公式
if(theCapacity<weight[i])
return f(i+1,theCapacity);
return
max(f(i+1,theCapacity),f(i+1,theCapacity-weight[i])+profit[i]);
}
假定n=5,p=[6,3,5,4,6],w=[2,2,6,5,4]且c=10.
为了求f(1,10)的值,用f(1,10)调用递归函数f,其返回值便是f(1,10)的值。
首先求f(2,10)(不放入)和f(2,8)(放入)
然后再在两种情况下分别考虑放入和不放入。
如下如所示:
其实就是将所有情况都计算一遍然后取其最大值,所以动态规划其实就是枚举法。
但是利用上述的递归公式我们发现,图中的阴影部分计算重复了,重复部分大概是计算量的1/5左右。
所以我们要做的就是避免重复的计算,那么我们该怎么做呢?我们采取记账的方法将计算过的f(i,y)都保存在一个列表里面,当碰到已经计算过的数据时,直接提出,否则重新进行计算。该列表的元素是一个三元组(i,y,f(i,y))
int f(int i,int theCapacity)
{//返回f(i,theCapacity)。不重复计算f的值
//检查是否已经计算过
if(fArray[i][theCapacity]>=0)//我们将fArray所有的数据全部初始化为-1,然后用计算的值取代-1,如果已计算那么该值大于0
return fArray[i][theCapacity];
//还没有计算过
if(i==numberOfObjects)
{//使用第一个递推公式,计算并存储f(i,theCapacity)
fArray[i][theCapacity]=(theCapacity<weight[numberOfObjects]?0:profit[numberOfObjects]);
return fArray[i][theCapacity];
}
//使用公式(19-2)
if(theCapacity<weight[i])
//物品i不合适
fArray[i][theCapacity]=f(i+1,theCapacity);
else
//物品i合适,尝试两种可能
fArray[i][theCapacity]=max(f(i+1,theCapacity),f(i+1,theCapacity-weight[i])+profit[i]);
return fArray[i][theCapacity];
}
上述代码与之前的代码其实是一样的,只是将每次求得的数据存储到了一个二维数组里面,然后再计算新数据之前,判断该数据之前是否已经计算过,如果计算过则直接取出,否则进行计算。
虽然以上递归方法没有进行重复计算,但是仍然会浪费栈资源,所以下面实现一个迭代的动态规划程序:
void knapsack(int *profit,int *weight,int numberOfObjects,int knapsackCapacity,int **f)
{//用迭代算法求解动态规划的递归方程
//计算f[1][knapsackCapacity]和f[i][y]
//2<=i<=numberOfObjects,0<=y<=knapsackCapacity
//profit[1:numberOfObjects]给出物品的价值
//weight[1:numberOfObjects]给出物品的重量
//初始化f[numberOfObjects][]
int yMax=min(weight[numberOfObjects]-1,knapsackCapacity);//取最后一个物品的重量-1和背包容量的小值
for(int y=0;y<=yMax;y++)
f[numberOfObjects][y]=0;//当剩余容量小于最后一个物品的体积时,最优价值为0;
for(int y=weight[numberOfObjects];y<=knapsackCapacity;y++)
f[numberOfObjects][y]=profit[numberOfObjects];
//其实上述两个for循环就是实现第一个递推公式即边界条件
//计算f[i][y],1<i<numberOfObjects
for(int i=numberOfObjects-1;i>1;i--)
{
yMax=min(weight[i]-1,knapsackCapacity);
for(int y=0;y<=yMax;y++)//如果当前容量y小于物品i的体积则不将其放入背包则其最优值等于f[i+1][y]
f[i][y]=f[i+1][y];
for(int y=weight[i];y<=knapsackCapacity;y++)
f[i][y]=max(f[i+1][y],f[i+1][y-weight[i]]+profit[i]);
}
//计算f[1][knapsackCapacity]
f[1][knapsackCapacity]=f2[knapsackCapacity];
if(knapsackCapacity>=weight[1])
f[1][knapsackCapacity]=max(f[1][knapsackCapacity],f[2][knapsackCapacity-weight[1]+profit[1]]);
}
以上就是将我们最开始讲的那个背包例子实现了,首先两个for循环求第一个递推式的结果,然后两个for循环求第二个递推式的所有可能情况,然后将f(1,y)带入直接利用之前求得的表格数据去计算最终结果。
这与我们放的链接的博客讲的方法是一致的只是递推方向反了而已。
需要注意的就是为何要取capacity和weight[i]-1的小值作为ymax。当capacity大于weight[i]-1时我们有两种情况,第一种情况剩余容量小于weight[i]-1,这时最优值为f([i+1,y]),第二种情况剩余容量的值在两者之间这时最优值为f([i+1],y-weight[i]),当capacity小于weight[i]-1时就只会有一种情况就是放不下这个物品,与是f([i,y])=f([i+1,y]).
接下来我们就得用迭代回溯法来计算到底哪些物品被放入了背包,哪些物品没有放入背包。
void trackback(int **f,int *weight,int numberOfObjects,int knapsackCapacity,int *x)
{//计算解向量
for(int i=1;i<numberOfObjects;i++)
if(f[i][knapsackCapacity]=f[i+1][knapsackCapacity])//如果f(i,y)=f(i+1,y)那么该物品就没有被放入背包
//不包括物品
x[i]=0;
else
{//包括物品i
x[i]=1;
knapsackCapacity-=weight[i];//如果不等于则包含物品i,且放置下一个物品的剩余容量减少weight[i]
}
x[numberOfObjects]=(f[numberOfObjects][knapsackCapacity]>0)?1:0;
//由于没有f[numberOfObjects+1][knapsackCapacity]所以最后一个物品额外进行判断。
}
其实就是简单的从前往后通过两个条件进行判断该物品有没有被加入背包。