动态规划之重叠子问题属性(Overlapping Subproblems Property)

Dynamic Programming is an algorithmic paradigm that solves a given complex problem by breaking it into subproblems and stores the results of subproblems to avoid computing the same results again. Following are the two main properties of a problem that suggest that the given problem can be solved using Dynamic programming.

动态规划是一种典型的算法,它可以通过将一个给定的复杂问题分割为子问题,然后将子问题的结果进行存储以免再次计算相同的结果。当问题具有下面两个属性(特征)的时候,那么意思就是说这个问题可以用动态规划的思想来解决。

1) Overlapping Subproblems
2) Optimal Substructure

1) 重叠的子问题
2) 最优的子结构

1) Overlapping Subproblems:
Like Divide and Conquer, Dynamic Programming combines solutions to sub-problems. Dynamic Programming is mainly used when solutions of same subproblems are needed again and again. In dynamic programming, computed solutions to subproblems are stored in a table so that these don’t have to recomputed. So Dynamic Programming is not useful when there are no common (overlapping) subproblems because there is no point storing the solutions if they are not needed again. For example, Binary Search doesn’t have common subproblems. If we take example of following recursive program for Fibonacci Numbers, there are many subproblems which are solved again and again.

像分治算法一样,动态规划结合了子问题的解决方案。动态规划主要用在当一遍又一遍地解决相同的子问题。在动态规划中,子问题的计算结果存储在一个表中,这样的话这些问题就没必要再计算了。所以动态规划在非一般(重叠)子问题的情况下不是那么有用,因为找不到啥存储结果的提升当这个结果不再需要了,例如,二分查询就不具有一般子问题。如果我们用斐波那契数最为递归编程的例子,那么就有许多的问题需要一遍又一遍地解决。

/* simple recursive program for Fibonacci numbers */
int fib(int n)
{
   if ( n <= 1 )
      return n;
   return fib(n-1) + fib(n-2);
}

Recursion tree for execution of 
fib(5)

fib(5) 执行的递归树

                              
                         fib(5)
                     /             \
               fib(4)                fib(3)
             /      \                /     \
         fib(3)      fib(2)         fib(2)    fib(1)
        /     \        /    \       /    \
  fib(2)   fib(1)  fib(1) fib(0) fib(1) fib(0)
  /    \
fib(1) fib(0)

We can see that the function f(3) is being called 2 times. If we would have stored the value of f(3), then instead of computing it again, we would have reused the old stored value. There are following two different ways to store the values so that these values can be reused.

我们可以看到函数f(3)被调用了两次。如果我们可以存储f(3)的值,那就不再计算了,我们可以拒绝旧的存储数值。以下有两种方法可以存值一遍这些值可以被拒绝

a) Memoization (Top Down): 
b) Tabulation (Bottom Up):

a) 记忆法 (自顶向下): 
b) 制表法 (自底向上):

a) Memoization (Top Down): The memoized program for a problem is similar to the recursive version with a small modification that it looks into a lookup table before computing solutions. We initialize a lookup array with all initial values as NIL. Whenever we need solution to a subproblem, we first look into the lookup table. If the precomputed value is there then we return that value, otherwise we calculate the value and put the result in lookup table so that it can be reused later.

a) 记忆法 (至顶向下): 对一个问题进行记忆编程类似于用一小改动的递归版本,它在计算之前要查看一个查询表。我们可以用NIL来初始化这个查询表。无论什么时候我们需要一个子问题的结果了,我们首先先去查询表中看看。如果要计算的值已经在表中了,那么就直接返回这个值,否则就继续计算,然后把结果存放在查询表中,这样就可以在后面用了。

Following is the memoized version for nth Fibonacci Number.

下面是记忆法版本的斐波那契数。

/* C/C++ program for memoized version for nth Fibonacci number */
#include<stdio.h>
#define NIL -1
#define MAX 100
 
int lookup[MAX];
 
/* Function to initialize NIL values in lookup table */
void _initialize()
{
  int i;
  for (i = 0; i < MAX; i++)
    lookup[i] = NIL;
}
 
/* function for nth Fibonacci number */
int fib(int n)
{
   if (lookup[n] == NIL)
   {
      if (n <= 1)
         lookup[n] = n;
      else
         lookup[n] = fib(n-1) + fib(n-2);
   }
 
   return lookup[n];
}
 
int main ()
{
  int n = 40;
  _initialize();
  printf("Fibonacci number is %d ", fib(n));
  return 0;
}

b) Tabulation (Bottom Up): The tabulated program for a given problem builds a table in bottom up fashion and returns the last entry from table.

b) 制表法 (至底向上):对于一个给定的问题,制表法编程就是用自底向上的方法建立一个表,然后返回表中最新的记录。

/* C program for tabulated version */
#include<stdio.h>
int fib(int n)
{
  int f[n+1];
  int i;
  f[0] = 0;   f[1] = 1; 
  for (i = 2; i <= n; i++)
      f[i] = f[i-1] + f[i-2];
 
  return f[n];
}
  
int main ()
{
  int n = 9;
  printf("Fibonacci number is %d ", fib(n));
  return 0;
}

Output:

Fibonacci number is 34 

Both tabulated and Memoized store the solutions of subproblems. In Memoized version, table is filled on demand while in tabulated version, starting from the first entry, all entries are filled one by one. Unlike the tabulated version, all entries of the lookup table are not necessarily filled in memoized version. For example, memoized solution of LCS problem doesn’t necessarily fill all entries.

制表法与记忆法都是存储子问题的结果。在记忆法中,只有在需要的时候才填充表,但是在制表法中,从第一个记录开始,所有的记录依次填充。与制表法不同,记忆法查询表中的所有记录不是必须都要被填充的。例如LCS问题的记忆法解法就不需要吧所有的记录都填充进来。

To see the optimization achieved by memoized and tabulated versions over the basic recursive version, see the time taken by following runs for 40th Fibonacci number.

为了看看我们用记忆法与制表法在最基本的递归版本上的优化结果,试试下面的几个计算第40个斐波那契数所花费的时间。

Simple recursive program
Memoized version
tabulated version

Also see method 2 of Ugly Number post for one more simple example where we have overlapping subproblems and we store the results of subproblems.

We will be covering Optimal Substructure Property and some more example problems in future posts on Dynamic Programming.

Try following questions as an exercise of this post.
1) Write a memoized version for LCS problem. Note that the tabular version is given in the CLRS book.
2) How would you choose between Memoization and Tabulation?

下面两个问题作为这一部分的练习。
1) 用记忆法解决LCS,注意制表法已经在CLRS中给出。
2) 这两种方法你怎样去选择?

    原文作者:动态规划
    原文地址: https://blog.csdn.net/sinat_36246371/article/details/52803931
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞