自底向上的方法(动态规划)包括首先查看“较小的”子问题,然后使用较小问题的解决方案来解决较大的子问题。

自顶向下包括以“自然的方式”解决问题,并检查之前是否计算过子问题的解决方案。

我有点糊涂了。这两者有什么不同?


当前回答

下面是DP为基础的解决方案的编辑距离问题,这是自上而下的。我希望它也能帮助你理解动态规划的世界:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

你可以想象它在你家里的递归实现。如果你以前没有解决过类似的问题,这是非常好的和具有挑战性的。

其他回答

动态规划问题可以使用自底向上或自顶向下的方法来解决。

通常,自底向上方法使用制表技术,而自顶向下方法使用递归(带记忆)技术。

但是您也可以使用下面所示的递归使用自底向上和自顶向下的方法。

自底向上:从基本条件开始,递归地传递计算到现在的值。一般来说,这些是尾部递归。

int n = 5;
fibBottomUp(1, 1, 2, n);

private int fibBottomUp(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibBottomUp(j, i + j, count + 1, n);
}

自顶向下:从最后一个条件开始,递归地得到它的子问题的结果。

int n = 5;
fibTopDown(n);

private int fibTopDown(int n) {
    if (n <= 1) return 1;
    return fibTopDown(n - 1) + fibTopDown(n - 2);
}

下面是DP为基础的解决方案的编辑距离问题,这是自上而下的。我希望它也能帮助你理解动态规划的世界:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

你可以想象它在你家里的递归实现。如果你以前没有解决过类似的问题,这是非常好的和具有挑战性的。

让我们以斐波那契数列为例

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

换句话说,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

对于前5个斐波那契数

Bottom(first) number :1
Top (fifth) number: 5 

下面以递归斐波那契数列算法为例

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

现在如果我们用下面的命令执行这个程序

rcursive(5);

如果我们仔细研究算法,为了生成第五个数字,它需要第3和第4个数字。所以我的递归实际上是从top(5)开始,然后一直到bottom/lower numbers。这种方法实际上是自顶向下的方法。

为了避免多次进行相同的计算,我们使用动态编程技术。我们存储先前计算的值并重用它。这种技巧叫做记忆。动态规划中除了记忆之外还有更多的内容,在这里不需要讨论。

自顶向下

让我们重写原始算法并添加记忆技术。

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

我们像下面这样执行这个方法

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

这个解决方案仍然是自顶向下的,因为算法从顶部值开始,每一步都从底部得到我们的顶部值。

自底向上

但问题是,我们能不能从底部开始,比如从第一个斐波那契数开始,然后向上走。我们用这个技巧重写一下,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

现在,如果我们研究这个算法,它实际上是从较低的值开始,然后向上。如果我需要第5个斐波那契数,我实际上是在计算第1个,然后是第二个,然后是第三个,一直到第5个数字。这种技术实际上叫做自下而上技术。

最后二,算法完全满足动态规划的要求。但一个是自上而下的,另一个是自下而上的。两种算法具有相似的空间复杂度和时间复杂度。

自顶向下和自底向上DP是解决相同问题的两种不同方法。考虑一个记忆(自顶向下)vs动态(自底向上)编程解决方案来计算斐波那契数。

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

我个人觉得记忆更自然。你可以取一个递归函数,并通过一个机械的过程来记忆它(首先在缓存中查找答案,如果可能的话返回它,否则递归地计算它,然后在返回之前,你把计算保存在缓存中以备将来使用),而做自底向上的动态编程需要你编码一个计算解决方案的顺序,这样就不会在它所依赖的小问题之前计算“大问题”。

简单地说,自顶向下方法使用递归一次又一次地调用子问题,而自底向上方法使用单个而不调用任何子问题,因此它更有效。