动态规划 入门篇(一)

入门篇(一)

        在入门篇(一),简单介绍一部分概念,重点是在例子上,在题目中体会动态规划尤为重要。

 

一、概念 

动态规划(DP)是求最优解的一种常用解法,但是理解起来还是有点麻烦。

首先来讲讲几个概念:

 

1.状态:将求解过程细化的一种表示,通常是一个数组,也有可能是一个序列。

 

2.无后效性:当前状态只受前面状态的影响,不受未发生状态的影响。如,用某a[i]表示某一状态,其中用i的大小表示状态的先后顺序,a[i]只受a[j]的影响,j小于等于i。

 

3.状态转移:通过某种表达式,使得后一状态可以由前一个或一系列状态表示。通常会用到sum,max,min之类的函数。

 

4.最优子结构:当前问题的最优解包含了子问题的最优解。这个是保证能够进行状态转移的重要条件。

 

5.边界:顾名思义。DP在处理边界的时候要小心。

 

 

很多人讲动态规划的时候会提到记忆化搜索,其实这就是两个不同的东西。这里不必再提。

动态规划和贪心,贪心和动态规划的最大区别在于,贪心取的值是当前最优,动态规划取的是全局最优。

概念讲就讲这么多,动态规划不实践,不做题,有点难理解。一定要动手!

在做题的时候思考、体会。重点看定义状态,状态转移。代码里注意看状态转移的实现先后实现,和边界的处理。

 

 

二、入门实例

这里的入门实例都是dp里面非常简单常见的几个例子。对其中的某一个或几个会着重的讲。这里都是用python来实现。

 

2.1 矩阵取数游戏

给定一个n阶的正矩阵,从左上角走到右下角,每次走一步,只能向右或者向下走,求路过的元素的最大和。

 

    

                 图一

分析:

在图中,在位置(3,4)是从左边来的2得到的 是贪心,选择了当前最优,却错过了整体最优,得不到这个题目的答案。

往上1来的是动态规划。我们从图上容易看到,往上走才是正解。那为什么向上就是正解?

 

        这里就涉及到了状态转移。如果是贪心,它会考虑a[3][4]=2大于a[2][4]=1,选择a[3][4]=2作为路径是贪心的思想; 如果是动态规划,在求(3,4)是从上还是左边来的,会做一个比较:从上(2,4)位置的时候,从起点(1,1)到(2,4)它最大的和 从起点(1,1)到(3,3)它的最大和谁大,就从选择那一条路。这里为了方便,我们用一个数组dp[i][j]表示从(1,1)到位置(i,j)路过的元素最大和。即我们在求dp[3][4]的时候,我们考虑,由于题目规定是从起点向右、向下出发,(3,4)它可能是的上一个位置可能是(3,3)或(2,4),只要比较dp[2][4]和dp[3][3]的大小即可得到它是从哪里来的,再加上本身节点的值就是从起点到这个节点的最大元素和。表达式可以写出dp[3][4]=max(dp[2][4] , dp[3][3])+a[3][4]。

 

        更为普遍的表达式dp[i][j]=max(dp[i-1][j] , dp[i][j-1])+a[i][j],初始状态dp[1][1]=a[i][j]。

        到这里,矩阵取数游戏的状态转移方程就讲完了。

       状态转移方程,重点在这个转移上,是怎么转移的,转移给谁了。

       我们看看代码怎么实现这个转移的。

 

def maxMatrix(a):
    result = [[0 for col in range(len(a[row-1]) + 1)]for row in range(len(a) + 1)]
    for i in range(len(result))[1:]:
        for j in range(len(result[i]))[1:]:
            result[i][j] = max(result[i - 1][j], result[i][j - 1]) + a[i - 1][j - 1]
    return result[len(result) - 1][len(result[len(result)-1])-1]

if __name__=='__main__':
    n = int(input())
    a = []
    for i in range(n):
        tmp = input().split()
        tmp = [int(t) for t in tmp]
        a.append(tmp)
    print(maxMatrix(a))

 

input:
4
1 2 1 4
1 1 1 1
1 1 2 3
1 1 1 1

output:
13

 

 

 

 

 

2.2 数字三角形

在一个数字三角形中,从顶至底走,每一步可沿着左斜线和右斜线走,走至底部,途经的路径之和做大是多少?

 

           图二

分析:

这个和2.1是不是很相似,我们把它放入数组中,正常的看,如下图:

 

问题看可以看成,它只能往下或者往右下走,同时,注意边界,在第一列的时候它只能从上方得到,在最右边一列的时候,它只能从右上方来。

状态转移公式,很容易得到

令s[i][j]表示在第i行、j列的时候它路径的最大和。

这里的i,j默认是不数组越界的。

 

好了,根据状态转移方程 ,很容易得到代码

 

def caculateTriangle(a):
    s=[[0 for j in i] for i in a]
    s[0][0]=a[0][0]
    for i in range(len(a))[1:]:
        for j in range(len(a[i])):
            if(j == 0):
                s[i][j]=s[i-1][j]+a[i][j]
                continue
            if(j == i):
                s[i][j]=s[i-1][j-1]+a[i][j]
                continue
            s[i][j]=a[i][j]+max(s[i-1][j-1],s[i-1][j])
    print(s)
    return max(s[len(s)-1])

if __name__ == '__main__':
    a=[[7],[3,8],[8,1,0],[2,7,4,4],[4,5,2,6,5]]
    print(caculateTriangle(a))

 

2.3 连续子序列最大和(最大子段和)

给定一个序列,求这个序列的某个连续子序列,它的和最大为多少。如[31,-41,59,26,-53,58,97,-93,-23,84],

 

分析:

求这个题目方法有很多,这里只简单讲几种,重点在动态规划。

        方法一:直接枚举,暴力尝试,一层循环确定序列起点,一层循环定序列终点,一层循环求和,大致会如下写:

for(int i = 1; i <= n; i++)
{
    for(int j = i; j <= n; j++)
    {
        int sum = 0;
        for(int k = i; k <= j; k++)
            sum += a[k];
            
        max = Max(max, sum);
    }
}

这个时间复杂度O(n^3),效果太差。 也许你想到了,第三层循环求和其实是没有必要的,这里看看方法二。

        

方法二:利用一个求和数组。

用sums[i]表示从0...i的和。一旦要求i+1...j的和的到时候直接用,sums[j]-sums[i]。

其实这种方法更为实用的是只用一个数表示i...j的和。在第二层循环的时候直接累加。大致可以这样写。

for(int i = 1; i <= n; i++)
{
    int sum = 0;
    
    for(int j = i; j <= n; j++)
    {
        sum += a[j];
        max = Max(max, sum);
    }
}

这样时间复杂度降低到了O(n^2),继续优化。

 

方法三:分治。

常见的分支手法在这不太适用,这里不太好确定界限。

首先,将序列分成前后两部分,分别求其前、后两部分最大连续子序列,这个最大序列也有可能跨前后两部分,所以第三中情况就是跨段,以中心点为轴,向前、向后分别求最大子序列和,最后求三中情况的最大值即可。

这种方法时间复杂度是O(nlogn)

 

方法四:DP

终于到了动态规划了。其实在使用动态规划的时候,很多人都是做过了大量的题目之后,把遇到的问题,转换为已经遇到的问题来解。

定义状态,s[i]为以第i个元素结尾的最大序列和。

转移状态,s[i]和s[i-1]的有什么关系?考虑,当第i个元素可以是上一个序列的最后一个元素的时候,从s[i]的定义上,我们可以得到s[i]=s[i-1]+a[i],条件是这个序列没有从头开始计数;另一种状态是s[i]所属于的序列重新开始定义,s[i]=a[i]。 我们已经可以将当前状态转移到另两个状态中去了,但是我们还没有得到具体的条件。重新考虑所求问题,求的是“最大和”,那条件就是比较两种状态下的值的大小即可。

状态转移公式:s[i]=max(s[i-1]+a[i] , a[i]),s[1]=a[1].

代码如下:

 

def getMaxSeqSum(a):
    a.insert(0,0)  # 为了和表达式一致,在首位置插入一个0,使得下标从1开始
    s=[0 for i in range(len(a))]
    for i in range(len(a)):
        s[i]=max(s[i-1]+a[i],a[i])
    return max(s)

if __name__=='__main__':
    a=[31,-41,59,26,-53,58,97,-93,-23,84]
    print(getMaxSeqSum(a))    

这个方法下时间复杂度已经是O(n)了,已经很好了,我们考虑一下,能不能再优化一下。时间上已经很优了,考虑考虑空间。

仔细观察,每次在使用s[i]的时候,只使用了上一个状态s[i-1],没有使用更早之前的状态,可以只使用一个变量存上一个状态。

这样状态转移公式就变成了: s=max(s+a[i] , a[i]) 。在使用了这个公式之后,s只存了当前的状态,我们如果不适用另一个变量存s的最大值,我们最终就只能得到,以最后一个元素结尾的最大子序列和。 其实很多其他问题,并不需要再使用一个其他的变量来存最大值,最后一个因为很多问题最后一个状态就是所求答案。

 

上这个改进方案的代码:

 

def getMaxSeqSum(a):
    s,result=0,0
    for i in range(len(a)):
        s=max(s+a[i],a[i])
        if(s>result):
            result=s
    return result

if __name__=='__main__':
    a=[31,-41,59,26,-53,58,97,-93,-23,84]
    print(getMaxSeqSum(a))


****************************************************************************************************************************************************************************************

先列举这几个例子吧,这几个例子都是DP里面入门的简单实例。好好体会一下状态转移。

过两天有时间了,再写个入门(二)。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值