入门篇(一)
在入门篇(一),简单介绍一部分概念,重点是在例子上,在题目中体会动态规划尤为重要。
一、概念
动态规划(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里面入门的简单实例。好好体会一下状态转移。
过两天有时间了,再写个入门(二)。