动态规划思想
动态规划的基本思想是把问题拆分成一个个相互联系的子问题,先求解子问题,最后将子问题合并形成最终的求解。其实质上是通过开辟记录表,记录已求解过的结果,当再次需要求解的时候,可以直接到那个记录表中去查找,从而避免重复计算子问题来达到降低时间复杂度的效果。实际上是一个空间换时间的算法。动态规划,通常可以把指数级的复杂度降低到多项式级别。
动态规划的两个重要性质:
1.子问题重叠性质:解决冗余,它对于重复出现的子问题,会在第一次求解后使用一个表储存下来,供之后使用,本质上也是一个空间换时间的思想。
举个例子:问1+2+3+4+5等于几,答案是15,那么我再后面再写个+6,答案呢,21,一下就算出来了,原因就在于我们已经有了前面计算的基础,只需要再前面的基础之上再+6就可以了,而如果直接问你1+2+3+4+5+6等于几,肯定是要慢很多的。
2.最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,则称为其具有最优子结构性质。它为动态规划解决问题提供了重要的线索。
应用:
在实际中应用动态规划是难点,关键在于动态规划虽然有诸多特点,但在实际问题中却没有一个通用的方法,它会因具体问题的不同而不同。
通常判断一个问题是否可以使用动态规划来解决,关键之一是判断有没有重复的子问题,除了我上面举得那个简单的例子之外,还有一个经典案例,就是斐波那契数列,都知道它的公式为f(n) = f(n-1) + f(n-2),也就是说我每求一个数都得去找他前两个数的值,而前两个数也是这么算来的所以斐波那契数列一般对第n项求值都是使用递归,然而递归是很低效的,但是如果我们在这里使用动态规划,使用一个value[i]的数组把每一项i计算之后的值都存在这个数组里,那么在之后的计算中就不必再进行冗余的计算,这就是动态规划的关键。(还有一个经典例子是寻找最长公共子字符串)
另外一个关键是在于有没有最优解结构。经典案例就是01背包问题:
假设中保险箱有 5 件宝物,大小分别是 3、4、7、8 和 9,其价值分别是 4、5、10、11、13,而且背包的容量为 i,要求在不超过背包容量的情况下取得最大价值的宝物。
这个问题就是一个典型的求最优解的问题,这个问题的解决思路就是把它拆分为求解一个个子问题,并且保证子问题的解也是最优的:
1.首先将第一个宝物放入,讨论背包大小为1-i时背包中的价值情况,求得一个最大价值;
2.之后再加入第二个宝物,讨论背包大小为1-i时背包中的价值情况,求得一个最大价值;
3.加入第三个宝物。。。
。。
直到宝物全部加入。
代码如下:
static void GetMaxValue(int capacity)
{
int[] size = new int[] { 3, 4, 7, 8, 9 };
int[] values = new int[] { 4, 5, 10, 11, 13 };
int[] totval = new int[capacity+1];//储存背包容量为0-capacity时的最高价值
int n = values.Length;//宝物数量
for (int j = 0; j <= n - 1; j++)//遍历每一件宝物
{
for (int i = 0; i <= capacity; i++)//遍历不同大小的情况
{
if (i >= size[j])//当背包容量足以放得下宝物时,计算当前容量的最高价值
{
int temp = totval[i - size[j]] + values[j];//把当前宝物放入背包,再在totval数组找一个剩下的容量的最大价值(这里用到的就是子问题的重叠性)
if (totval[i] < temp)//当数组中的这个值不为最优解了,就替换它
{
totval[i] = totval[i - size[j]] + values[j];
}
}
}
}
Console.WriteLine("The maximum value is: " + totval[capacity])
}