Dynamic programming动态规划算法

1.什么是动态规划算法?

动态规划算法是指将复杂问题拆分为简单问题,并存储简单问题的结果,避免重复计算。

也就是说动态规划算法需要满足以下3个特点:

  • 1.可以将问题分解为相似的子问题,并且子问题有重复
  • 2.每一个子问题只解决一次
  • 3.存储子问题解决后的结果

 

2.什么样的问题可以用动态规划解决?

可以用动态规划解决的问题主要有如下3个特征:

  • 1. 重复的子问题
  • 2. 最优子结构
  • 3. 无后效性

重复子问题是指:原问题分解得到的子问题中有完全相同的子问题

最优子结构就是指:原问题的最优解可以通过子问题的最优解的解决而解决

无后效性是指:原问题的解可以通过子问题的解获得,而不用管子问题的解是如何获得的 或者说 如果给定某一阶段的状态,则                          在这一阶段以后过程的发展不受这阶段以前各段状态的影响。

有点抽象,下面以例子进行说明

2.1以斐波那契数列(Fibonacci)为例子

从n=0开始的Fibonacci数列:

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

下面我们要计算Fibonacci的fib(n)如何计算呢?

首先我们看要计算fib(n)能否拆分为子问题,在这里很明显

fib(n) = fib(n-1) + fib(n-2)

那么我们要求fib(5),如何求解呢?

我们通过递归来求解:

int fib(int n) 
{ 
    if ( n <= 1 ) 
    {
        return n; 
    }
    return fib(n-1) + fib(n-2); 
}
                         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)

很明显,在求解过程中,我们需要求解fib(3)两次,fib(2)三次。。。。

求解了相同的子问题,每次求解子问题都需要耗费时间,那为什么不将子问题的结果存储起来,再用到相同子问题的时候直接取出来结果就可以,而不用再次计算,从而节省了时间(用空间来换取时间);而这正是动态规划的意义所在,降低程序时间复杂度,提高程序运行效率

在斐波那契数列求解fib(5)只需求解fib(4)和fib(3),最优子结构这一特征并不明显,但我们不用管fib(4)和fib(3)是怎么求解出来的,直接拿过来用就行了,满足无后效性。

所以,可以用动态规划方法来求解。

动态规划中有两种不同的方法来存储子问题的结果:

  • 1.自上而下

针对问题的自上而下存储子问题结果的程序类似于递归版本,只有一个小修改,它在计算解决方案之前会查找存储表。 我们初始化一个查找数组,所有初始值都为INT_MAX。 每当我们需要子问题的解决方案时,我们首先查看查找表。 如果预先计算的值在那里,那么我们返回该值,否则,我们计算该值并将结果放在查找表中,以便以后可以重用它。

vector<int> look_up(n+1,INT_MAX);//查找表一个包含n+1个元素,因为有fib(0),全部初始化为INT_MAX,不为INT_MAX说明该子问题已经被解决过


int fib(int n) 
{ 
	if (look_up[n] == INT_MAX)//fib(n)还未被求解过 
	{ 
		if (n <= 1) 
			lookup[n] = n; 
		else
			lookup[n] = fib(n - 1) + fib(n - 2); //求解出来后保存到look_up
    } 

    return look_up[n]; //否则,fib(n)已经求解过,再次用的时直接取出返回即可
} 

 

  •     2.  自下而上

自底部向上,一般以迭代的方式进行,给定问题以自下而上的方式构建表,并返回表中的最后一个值。 例如,对于相同的斐波纳契数,我们首先计算fib(0)然后计算fib(1)然后计算fib(2)然后计算fib(3),依此类推。 通过建立fib(0)和fib(1)而建立出fib(2)

//迭代的方式自下而上
int fib(int n) 
{ 
    vector<int> look_up(n+1,INT_MAX);
    int i; 
    look_up[0] = 0; 
    look_up[1] = 1; 
    for (i = 2; i <= n; i++)
    { 
	    look_up[i] = look_up[i-1] + look_up[i-2]; 
    }
    return look_up[n]; 
} 

进一步优化:

通过上述程序,我们发现我们求解的目的是fib(n),但是我们保留了大量的中间结果,这是没必要的,我们只需要保留要求解fib(i)所需要的前两个状态fib(i-1)与fib(i-2)

所以,我们可以用两个变量来代表fib(i)之前的两个状态,减少所用的存储空间

//迭代的方式自下而上,不保留中间结果进行优化
int fib(int n) 
{ 
    
    int i; 
    pre1 = 0; 
    pre2 = 1; 
    res = 0;//fib(n)最后返回结果
    for (i = 1; i <= n; i++)
    { 
	    res = pre1 + pre2; 
        pre1 = pre2;
        pre2 = res;
    }
    return res; 
} 

 

大多数动态规划问题的解题思路可以按以下步骤进行:

  • 找到递归关系(如何找其实就是如何拆分问题,下面会讨论)
  • 写出递归程序

然后选择一种方法优化,熟练之后可以省略写递归程序这一步

  • 递归程序+存储子问题结果   自上而下
  • 迭代程序+存储子问题结果   自下而上
  • 迭代程序  +  N个变量           自下而上

2.2以最短路径为例

我们以最短路径为例主要说明最优子结构特性

最优子结构特性是指:原问题的最优解可以通过子问题的最优解推出来

以下图有向图为例,求解节点q到节点t的最短路径,很明显是 q -> r -> t或者 q -> s ->t

 

若节点r在最短路径中,那么q -> t的最短路径 就等于 q->r 和 r->t 的最短路径,拆分为了两个子问题的最优解,求出子问题的最优解,那么原问题的最优解就可以推出来

即最短路径问题满足最优子结构特性

但是并不是所有的问题均满足最优子结构特性,比如最长路径问题,还是以上图为例,

我们要求q -> t的最长路径,是q -> r -> t或者 q -> s ->t, 

若已知r在最长路径中,那么原问题是不能拆分为 q->r 和 r->t的最长路径的组合的,

因为q->r的最长路径是q->s->t->r,  而r->t的最长路径是r->q->s->t,两者结合并不能推出原问题的最优解

所以不符合最优子结构问题

3.怎么解决动态规划问题?

解决动态规划问题的关键在于如何拆分问题,这是解决动态规划问题的关键点

解决动态规划问题时可以按以下思路进行尝试:

  • 将当前要解决的问题 i 视为当前状态,我们的目标是求出 f(i)  ;比如我们求斐波那契数列的第n个数,n就是当前的状态,f(n)是我们问题的目标
  • 找目标f(i)与那些状态有关,假设与状态x有关,找出f(x)与f(i)之间的状态转移关系;  比如斐波那契数列目标f(n)与n-1状态和n-2状态有关,找出f(n)与f(n-1)与f(n-2)之间的状态转移关系, f(n) = f(n-1) + f(n-2)
  • 有了状态转移关系就可以很容易运用
  • 动态规划方法来解决问题

以例子来进行实战解决动态规划问题:

  • 上楼梯问题

问题描述:你正在爬楼梯。 你需要n步才能达到顶峰,每次你可以爬1或2步, 您可以通过多少不同的方式登顶?

设当期状态为 i,i 表示i步才能到达的地方,我们由 f(i) 种方式到达 i 处,下面我们来看看 f(i) 与哪些状态有关,由于每次只能爬一步或者两步,只要到达了 i-1 处或者 i-2  处,我们就可以轻松到达 i 处,所以 f(i) 的状态与 f(i-1) 和 f(i-2)有关,所以状态转移关系为:

                                   f(i) = f(i-1) + f(i-2)

发现没有,这其实就是上面提到的 斐波那契数列,所以程序不再赘述

  • 抢劫问题

问题描述:

假设你是一个专业的强盗,计划在街上抢劫房屋。 每个房子都有一定数量的钱存在,阻止你抢劫他们的唯一限制是邻近的房子连接了安全系统,如果两个相邻的房子在同一个晚上被打破,它将自动联系警察。给出一个数组nums包括这条街道上房子包含的金额数,确定你今晚可以抢劫的最高金额

比如:[1,2,3,1]  则在不触发安全系统的前提下你抢的金额最大为 1 + 3 = 4

现在 假设你已经来到了第 i 间房子处,设你 抢到第 i 间房子处时,总最大金额为 f(i)

为了不触发安全系统,且为了达到最大净额,f(i) 的状态与 f(i-1) 和 f(i - 2)有关,假如你已经抢了第 i-1 间房子,那么你肯定不能抢第 i 间房, f(i) = f(i-1);但若你抢的是第 i-2 间房,那么你可以抢第 i 间房,总金额 f(i) = f(i-2) + nums[i] 

nums[i] 表示从第 i 间房抢的金额

所以状态转移关系应该为:

                                         f(i) = max( f(i-1), f(i-2) + nums[i])

自上而下的递归程序为:

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        vector<int> memo(nums.size(),INT_MAX);
        return rob(nums,nums.size()-1,memo);    
    }
private:
    int rob(vector<int>& nums,int n,vector<int>& memo)
    {   
        
        if(n < 0)
        {
            return 0;
        }//递归结束条件
        
        if(memo[n] != INT_MAX)
        {
            return memo[n];
        }
            
        int result =  max(rob(nums,n-1,memo),rob(nums,n-2,memo)+nums[n]);
        memo[n] = result;
        return result;
    }
    
  • 必谈的最长上升子序列(LIS)问题

给定长度为n的序列a,从a中抽取出一个子序列,这个子序列需要单调递增。问最长的上升子序列(LIS)的长度。  

e.g. 1,7,2,8,3,4   中的最长上升子序列为 1 2 3 4 ,所以长度为4

现在我们开始拆分问题:假设我们当前状态为 i,表示序列a的下标,则我们一定可以找到一个以a[i]为结尾的上升子序列,(即在这个上升序列中必须以a[i]为结尾,无论它是否是最长的)

此时LIS为f(i);下面找f(i)与哪些状态有关,

很明显,f(i)与a[i]之前的序列的最长上升序列有关;设之前的最大上升序列为f(p),p为a的下标,p可以是i之前的任意下标,而且f(p)是以a[p]为结尾的最长子序列

那么如果a[p] < a[i],那么序列最长上升序列+1

所以写成状态转移方程:

                                     f(i) =  max(f(p))  + 1;       0=< p < i

所以,程序如下:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size() == 0)
        {
            return 0;
        }
        vector<int> look_up(nums.size()+1,1);
        for(int i = 0;i < nums.size();i++)//表示求以a[i]结尾的序列的最长子序列长度
        {
            for(int p = 0;p < i;p++)//将a[i]依次拼接在a[p]后面,判断最长子序列长度是否变化
            {
                if(nums[p] < nums[i])
                {
                    look_up[i] = max(look_up[i],look_up[p]+1);//选出拼接在a[p]后面的最大长度值
                }   
            }
        }
        int ans = 1;
        //look_up[i]中求的都是以a[i]为结尾的最长上升子序列长度,所以需要遍历找出最长的上升子序列长度
        for(int j = 0;j < nums.size();j++)
        {
            ans = max(ans,look_up[j]);
        }
        
        
        return ans;
    }
    
};

 

 

 

 

参考: https://www.geeksforgeeks.org/overlapping-subproblems-property-in-dynamic-programming-dp-1/

           https://www.zhihu.com/question/23995189/answer/35429905

           https://www.zhihu.com/question/23995189

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值