数据结构和算法 十九、动态规划

本文深入探讨了动态规划这一算法思想,通过斐波那契数列的例子展示了自上而下(记忆化搜索)和自下而上(表格填充)两种方法。动态规划通过解决子问题并存储结果,避免了重复计算,从而提高了效率。文章还介绍了最长公共子序列问题的动态规划解决方案,包括递归记忆和制表法的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、动态规划概述

        动态规划(Dynamic Programming),这是一种解决棘手问题的方法,它将问题分成小问题,并先着手解决这些小问题。然后存储它们的解决方案以避免重复计算。这种简单的优化将时间复杂度从指数降低到多项式。例如,如果我们为Fibonacci Numbers编写简单的递归解,我们会得到指数时间复杂度,如果我们通过存储子问题的解来优化它,时间复杂度会降低到线性。

“1+1+1+1+1+1+1+1 = ?”。
“八!”
在左边写下另一个“1+”。
“那个怎么样?”
“九!” “你怎么知道这么快就九点了?”
“因为又加了一个!”
“所以不需要重新计算,因为记得有八个!

二、动态规划的不同方法

        有两种不同的方法来存储值,以便可以重用子问题的值。在这里,将讨论解决动态规划 (DP) 问题的两种模式: 

        记忆:自上而下

        制表:自下而上

1、自上而下(记忆法)

        自上而下的方法使用记忆技术。它由递归和缓存组成。“递归”表示重复调用函数的计算过程,而“缓存”表示存储中间结果。

        斐波那契数列示例(普通递归)

public int CalculateFibonacci(int n) {
    if(n < 2)
      return n;
    return CalculateFibonacci(n-1) + CalculateFibonacci(n-2);
  }

         斐波那契数列示例(递归记忆)

public int CalculateFibonacci(int n) {
    int memoize[] = new int[n+1];
    return CalculateFibonacciRecursive(memoize, n);
  }

  public int CalculateFibonacciRecursive(int[] memoize, int n) {
    if(n < 2)
      return n;

    // if we have already solved this subproblem, simply return the result from the cache
    if(memoize[n] != 0)
      return memoize[n];

    memoize[n] = CalculateFibonacciRecursive(memoize, n-1) + CalculateFibonacciRecursive(memoize, n-2);
    return memoize[n];
  }

2、自下而上(制表法)

        这种方法使用制表技术来实现动态规划解决方案。它解决了与以前相同的问题,但没有递归。在这种方法中,递归被迭代替换。制表法是通过填写一个 n 维表来完成的。根据表中的结果,然后计算顶部/原始问题的解决方案。

        斐波那契数列示例(制表法)

static int fib(int n)
    {
    /* Declare an array to store Fibonacci numbers. */
    int f[] = new int[n+2]; // 1 extra to handle case, n = 0
    int i;
      
    /* 0th and 1st number of the series are 0 and 1*/
    f[0] = 0;
    f[1] = 1;
     
    for (i = 2; i <= n; i++)
    {
       /* Add the previous 2 numbers in the series
         and store it */
        f[i] = f[i-1] + f[i-2];
    }
      
    return f[n];
    }
      
    public static void main (String args[])
    {
        int n = 9;
        System.out.println(fib(n));
    }

        斐波那契数列示例(制表法的优化方法)

        我们可以通过存储前两个数字来优化上一个方法中使用的空间,因为这就是我们获得下一个斐波那契数列所需的全部内容。 

static int fib(int n)
    {
        int a = 0, b = 1, c;
        if (n == 0)
            return a;
        for (int i = 2; i <= n; i++)
        {
            c = a + b;
            a = b;
            b = c;
        }
        return b;
    }
 
    public static void main (String args[])
    {
        int n = 9;
        System.out.println(fib(n));
    }

三、更多示例

1、最长公共子序列

        最长公共子序列(Longest Common Subsequence),简称LCS 问题,是说给定两个序列,找出它们中存在的最长子序列的长度。子序列是以相同的相对顺序出现的序列,但不一定是连续的。例如,“abc”、“abg”、“bdf”、“aeg”、“acefg”、..等是“abcdefg”的子序列。 

输入序列“ABCDGH”和“AEDFHR”的 LCS 为长度为 3 的“ADH”。 
输入序列“AGGTAB”和“GXTXAYB”的 LCS 为长度为 4 的“GTAB”。 

        它是一个经典的计算机科学问题,是比较两个文件之间差异的程序的基础,并在生物信息学中有应用。

(1)简单递归实现

        朴素递归方法的时间复杂度在最坏情况下为 O(2^n),最坏情况发生在 X 和 Y 的所有字符不匹配时,即 LCS 的长度为 0。 

public class LongestCommonSubsequence
{

    /* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char[] X, char[] Y, int m, int n )
{
	if (m == 0 || n == 0)
	return 0;
	if (X[m-1] == Y[n-1])
	return 1 + lcs(X, Y, m-1, n-1);
	else
	return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n));
}

/* Utility function to get max of 2 integers */
int max(int a, int b)
{
	return (a > b)? a : b;
}

public static void main(String[] args)
{
	LongestCommonSubsequence lcs = new LongestCommonSubsequence();
	String s1 = "AGGTAB";
	String s2 = "GXTXAYB";

	char[] X=s1.toCharArray();
	char[] Y=s2.toCharArray();
	int m = X.length;
	int n = Y.length;

	System.out.println("Length of LCS is" + " " +
								lcs.lcs( X, Y, m, n ) );
}

}

(2)递归记忆实现(自上而下)

        时间复杂度:O(mn) 忽略递归堆栈空间

/* A Top-Down DP implementation of LCS problem */
#include <bits/stdc++.h>
using namespace std;

/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs(char* X, char* Y, int m, int n,
		vector<vector<int> >& dp)
{
	if (m == 0 || n == 0)
		return 0;
	if (X[m - 1] == Y[n - 1])
		return dp[m][n] = 1 + lcs(X, Y, m - 1, n - 1, dp);

	if (dp[m][n] != -1) {
		return dp[m][n];
	}
	return dp[m][n] = max(lcs(X, Y, m, n - 1, dp),
						lcs(X, Y, m - 1, n, dp));
}

/* Driver code */
int main()
{
	char X[] = "AGGTAB";
	char Y[] = "GXTXAYB";

	int m = strlen(X);
	int n = strlen(Y);
	vector<vector<int> > dp(m + 1, vector<int>(n + 1, -1));
	cout << "Length of LCS is " << lcs(X, Y, m, n, dp);

	return 0;
}

(3)制表法实现(自下而上)

        上述实现的时间复杂度为 O(mn),比朴素递归实现的最坏情况时间复杂度要好得多。 

/* Dynamic Programming Java implementation of LCS problem */
public class LongestCommonSubsequence
{

/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char[] X, char[] Y, int m, int n )
{
	int L[][] = new int[m+1][n+1];

	/* Following steps build L[m+1][n+1] in bottom up fashion. Note
		that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] */
	for (int i=0; i<=m; i++)
	{
	for (int j=0; j<=n; j++)
	{
		if (i == 0 || j == 0)
			L[i][j] = 0;
		else if (X[i-1] == Y[j-1])
			L[i][j] = L[i-1][j-1] + 1;
		else
			L[i][j] = max(L[i-1][j], L[i][j-1]);
	}
	}
return L[m][n];
}

/* Utility function to get max of 2 integers */
int max(int a, int b)
{
	return (a > b)? a : b;
}

public static void main(String[] args)
{
	LongestCommonSubsequence lcs = new LongestCommonSubsequence();
	String s1 = "AGGTAB";
	String s2 = "GXTXAYB";

	char[] X=s1.toCharArray();
	char[] Y=s2.toCharArray();
	int m = X.length;
	int n = Y.length;

	System.out.println("Length of LCS is" + " " +
								lcs.lcs( X, Y, m, n ) );
}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坐望云起

如果觉得有用,请不吝打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值