记住求过的解来节省时间
记住求解的方式有两种:①自顶向下的备忘录法 ②自底向上。
只要问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解,则可以考虑用动态规划解决。动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略
动态规划和分治法区别:分治法中的各个子问题是独立的(即不包含公共的子子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。
如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题。解决上述问题的办法是利用动态规划。
例子1:最大子数组和问题
问题
一个有N个整数元素的一维数组(A[0],A[1],…A[N-1]),这个数组有很多子数组,求子数组和的最大值?注意:子数组必须是连续的、不需要返回子数组的具体位置、数组中包含:正、负、零整数、子数组不能空。
穷举法:
-
int MaxSubStringSum(int *A,int n)
-
{
-
int MaxSum = A[0];
-
int sum = 0;
-
for (int i = 0;i < n;i++)
-
{
-
sum = 0;
-
for (int j = i;j < n;j++)
-
{
-
sum += A[j];
-
MaxSum = max(MaxSum,sum);
-
-
}
-
}
-
return MaxSum;
-
}
穷取法最为直接,当然耗时也较多,时间复杂度为O(n^2);
我们利用穷举法虽然简单易懂,但是其时间复杂度很大,我们试着优化。
假设已经知道(A[1],……A[N-1])中和最大的子数组和为MaxSum[1],并且知道,(A[1],……A[N-1])中包含A[1]的和最大的子数组为TempMaxSum[1]。我们就可以把(A[0],……A[N-1])求和最大子数组问题转换为,MaxSum[0] = max{A[0],A[0]+TempMaxSum[1],MaxSum[1]}。
-
int MaxSubStringSum(int *A,int n)
-
{
-
int MaxSum = A[0];
-
int TempMaxSum = A[0];
-
for(int i = 1;i < n;i++)
-
{
-
TempMaxSum = max(A[i],TempMaxSum + A[i]);
-
MaxSum = max(MaxSum,TempMaxSum);
-
}
-
return MaxSum;
-
}
2.求斐波拉契数列Fibonacci
Fibonacci (n) = 1; n = 0
Fibonacci (n) = 1; n = 1
Fibonacci (n) = Fibonacci(n-1) + Fibonacci(n-2)
递归算法实现:
public int fib(int n)
{
if(n<=0)
return 0;
if(n==1)
return 1;
return fib( n-1)+fib(n-2);
}
//输入6
//输出:8
由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。下面就看看动态规划的两种方法怎样来解决斐波拉契数列Fibonacci 数列问题。
①自顶向下的备忘录法
public static int Fibonacci(int n)
{
if(n<=0)
return n;
int []Memo=new int[n+1];
for(int i=0;i<=n;i++)
Memo[i]=-1;
return fib(n, Memo);
}
public static int fib(int n,int []Memo)
{
if(Memo[n]!=-1)
return Memo[n];
//如果已经求出了fib(n)的值直接返回,否则将求出的值保存在Memo备忘录中。
if(n<=2)
Memo[n]=1;
else Memo[n]=fib( n-1,Memo)+fib(n-2,Memo);
return Memo[n];
}
②自底向上的动态规划
备忘录法还是利用了递归,上面算法不管怎样,计算fib(6)的时候最后还是要计算出fib(1),fib(2),fib(3)……,那么何不先计算出fib(1),fib(2),fib(3)……,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。