探索动态规划的本质

动态规划是一种解决最优化问题的方法,核心在于状态和状态转移方程的定义。状态定义了问题的不同阶段,状态转移方程描述了各阶段之间的关系。例如,Fibonacci数列和最长递增子序列问题,通过状态和状态转移方程可以避免重复计算,降低时间复杂度。动态规划的本质并非记忆化存储,而是状态和状态间的转化关系。
摘要由CSDN通过智能技术生成

一、什么是动态规划:

dynamic programming is a method for solving a complex problem by breaking
it down into a collection of simpler subproblems, solving each of those subproblems
just once, and storing their solutions - ideally, using a memory-based data structure.1

以上定义摘自维基百科

下面则是百度百科里的解释:

动态规划常常适用于有重叠子问题最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),
再合并子问题的解以得出原问题的解。

通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给
定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重
复子问题的数目关于输入的规模呈指数增长时特别有用。[^2]

由此,我们可以简略的概括下:“动态规划”通过把原来较复杂的问题递归的分解
为一组较简单的子问题,并通过存储每个子问题的解,使得每个子问题只计算一次就可以解决原问题的思想。

这里的某一类问题通常情况下是指某些最优化问题。这类问题可以有很多可行解,每个解都有一个值
,我们希望寻找具有最优值(最小值或最大值)的解。[^3]

一言以蔽之——“动态规划”是解决某类问题的方法(或思想而不是算法)

根据以上定义,很多人可能会觉得动态规划的真谛就是通过递归或递推,用额外的空间记录下已解决的子问题的解
从而通过空间换时间来降低时间复杂度。其实不然,动态规划的本质是对每个阶段状态的定义以及当前状态与下一阶段*状态
关系*的定义(状态转移方程)。而所谓的“存储每个子问题的解”则是隐含的包含在状态关系里,那些“额外的记录空间”
则只是其表现形式而非其内涵。

二、什么是状态?什么是状态转移方程?

什么是状态?

我们先从最简单的Fibonacci数列谈起:

比如说我想计算第100个非波那契数,每一个非波那契数就是这个问题的一个状态,每求一个新数字只需要之前的两个状态。
所以同一个时刻,最多只需要保存两个状态,空间复杂度就是常数;每计算一个新状态所需要的时间也是常数且状态是线性
递增的,所以时间复杂度也是线性的。上面这种状态计算很直接,只需要依照固定的模式从旧状态计算出新状态就行
(a[i]=a[i-1]+a[i-2]),不需要考虑是不是需要更多的状态,也不需要选择哪些旧状态来计算新状态。

我们再来看一个动态规划的教学必备题:

给定一个数列,长度为N,求这个数列的最长上升(递增)子数列(LIS)的长度.以1 7 2 8 3 4为例。这个数列
的最长递增子数列是 1 2 3 4,长度为4;次长的长度为3, 包括 1 7 8; 1 2 3 等.

要解决这个问题,我们首先要定义这个问题和这个问题的子问题。有人可能会问了,题目都已经在这了,我们还
需定义这个问题吗?需要,原因就是这个问题在字面上看,找不出子问题,而没有子问题,这个题目就没办法解决。
所以我们来重新定义这个问题:

给定一个数列,长度为N,

设F(k)为:以数列中第k项结尾的最长递增子序列的长度.

求F(1)..F(N)中的最大值。

显然,这个新问题与原问题等价。而对于F(k)来讲,F(1)..F(k-1)都是F(k)的子问题:因为以第k项结尾的最长递
增子序列(下称LIS),包含着以第1..k-1中某项结尾的LIS。上述的新问题就可以叫做状态,定义中的“为数列中第
k项结尾的LIS的长度”,就叫做对状态的定义。

什么是状态转移方程?

上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程。

比如,对于LIS问题。
设F(k):以数列中第k项结尾的最长递增子序列的长度.
设A为题中数列,状态转移方程为:

F(1) = 1(根据状态定义导出边界情况)

F(k) = max(F(i)+1|A(k)>A(i),i∈(1..k-1))(k>1)

用文字解释一下是:以第k项结尾的LIS的长度是:保证第i项比第k项小的情况下,以第i项结尾的LIS长度加一的最大值,
取遍i的所有值(i小于k)。

状态转移方程就是带有条件的递推式

三、动态规划本质的探索:

对于上述LIS问题我们可以进一步探索。
Talk is cheap , show me the code:

#include <iostream>
using namespace std;

int lis(int A[], int n){
    int *d = new int[n];
    int len = 1;
    for(int i=0; i<n; ++i){
        d[i] = 1;
        for(int j=0; j<i; ++j)
            if(A[j]<=A[i] && d[j]+1>d[i])
                d[i] = d[j] + 1;
        if(d[i]>len) len = d[i];
    }
    delete[] d;
    return len;
}
int main(){
    int A[] = {
        5, 3, 4, 8, 6, 7
    };
    cout<<lis(A, 6)<<endl;
    return 0;
}

我们看到,这里多定义了一个数组d来存储每个阶段(状态)的最大值,即所谓的额外的存储空间,然而
这并不是动态规划的本质,通过上文的分析,我们知道,下一阶段的状态可由上一阶段的状态得到(我们已经定义了
状态和状态转移方程)。因此,额外的数组d是为了保存每个当前状态而开设的,如果没有状态及状态转移方程的定义,
数组d是没有任何意义的,换句话说额外的存储空间依附于前后状态的关系,因为有了状态关系才有了额外的存储空间。

再拿fibonacci数列举例:

定义fib(n):fibonacci数列第n项的值。

求解第10项fibonacci数列fib(10)

我们可以定义状态:

第n个状态为fib(n)的值。

状态转移方程:

fib(1)=1,fib(2)=1

fib(k) = fib(k-1)+fib(k-2) (k>2)

这里,我们可以通过多定义一个数组d来保存每个状态的值:

#include <iostram>
using namespace std;

int fib(int n){
  int *d;
  d[1] = d[2] = 1;
  for(int i=3;i<=n;i++){
    d[i] = d[i-1]+d[i-2];
  }
  return d[n];
}
int main(){
  cout<<fib(10)<<endl;
  return 0;
}

这里的数组d是为了保存每个状态而定义的,其实我们也可以不用开辟额外的存储空间来定义这个数组d:

#include <iostram>
using namespace std;

int fib(int n){
  int a,b;
  a = b =1;
  int t;
  for(int i=3;i<=n;i++){
    t = a+b;
    a = b;
    b = t;
  }
  return t;
}
int main(){
  cout<<fib(10)<<endl;
  return 0;
}

至于LIS问题能不能不使用额外的存储空间呢,答案是否定的。为什么,因为其状态转移方程的表示”F(k) = max(F(i)+1|A(k)>A(i),i∈(1..k-1))(k>1)”
注定了使用一个额外的数组会更方便点。

由此可见,动态规划的本质是对每个阶段状态的定义以及当前状态与下一阶段*状态
关系*的定义(状态转移方程),而不是所谓的记忆化存储。


  1. https://en.wikipedia.org/wiki/Dynamic_programming
    [^2]:摘自《算法导论》之“动态规划”
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值