由来
简单例子,斐波那契数列求和,最简单,直接递归,但有大量重复计算
优化:记忆搜索,存储计算过的值
问题:递归层数多时栈溢出
动态规划
去除递归
对比记忆搜索:规定了计算顺序
基本性质
最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
动态规划的核心思想是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
动态规划的一般步骤
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
最主要的是确定存储结构、子问题和父问题的关系、停止条件
线性模型
一维
斐波那契
最长上升子序列
构建一维数组,存该位置最长序列长度,O(n2)
class Solution(object):
def lengthOfLIS(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
a=[1]*len(nums)
if len(nums)==0:
return 0
for i in range(1,len(nums)):
for j in range(0,i):
if(nums[i]>nums[j]):
a[i]=max(a[i],a[j]+1)
return max(a)
长度和序列输出,从前到后求解,从后到前输出序列
public static int[] getLIS(int[] A) {
// dp列表构建
List<Integer> list = new ArrayList<>();
int[] dp = new int[A.length];
dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for(int j = 0; j < i; j++){
if(A[j] < A[i]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
// 取最大值
int maxIndex = dp.length - 1;
for (int i = dp.length - 2; i >= 0; i--) {
if(dp[i] > dp[maxIndex]){
maxIndex = i;
}
}
// 序列恢复
list.add(A[maxIndex]);
for (int i = maxIndex - 1; i >= 0; i--) {
if(A[maxIndex] > A[i] && dp[maxIndex] == dp[i] + 1){
list.add(A[i]);
maxIndex = i;
}
}
//反序为升序
int[] nums = new int[list.size()];
for(int i = 0; i < nums.length; i++){
nums[nums.length - 1 - i] = list.get(i);
}
return nums;
}
长度和序列输出,从后到前求解,从前到后输出序列
def lis(arr):
n = len(arr)
m = [0]*n
for x in range(n-2,-1,-1):
for y in range(n-1,x,-1):
if arr[x] < arr[y] and m[x] <= m[y]:
m[x] += 1
max_value = max(m)
result = []
for i in range(n):
if m[i] == max_value:
result.append(arr[i])
max_value -= 1
return result
O(nlgn)
这个算法其实已经不是DP了,有点像贪心。至于复杂度降低其实是因为这个算法里面用到了二分搜索。本来有N个数要处理是O(n),每次计算要查找N次还是O(n),一共就是O(n^2);现在搜索换成了O(logn)的二分搜索,总的复杂度就变为O(nlogn)了
开一个栈,每次取栈顶元素top和读到的元素temp做比较,如果temp > top 则将temp入栈;如果temp < top则二分查找栈中的比temp大的第1个数,并用temp替换它。 最长序列长度即为栈的大小top。
虽然有些时候这样得不到正确的序列了,但最后算出来的个数是没错的
class Solution(object):
def lengthOfLIS(self, nums):
a = []
if len(nums) == 0:
return 0
a.append(nums[0])
for i in range(1, len(nums)):
if nums[i] > a[-1]:
a.append(nums[i])
else:
if nums[i] < a[0]:
a[0] = nums[i]
else:
position = self.binarySearch(a, nums[i], 0, len(a) - 1)
a[position] = nums[i]
return len(a)
def binarySearch(self, a, number, left, right):
if left == right:
return left
while left < right:
mid = (left + right) / 2
if mid == left or mid == right:
if number > a[left]:
return right
else:
return left
if number < a[mid]:
return self.binarySearch(a, number, left, mid)
else:
return self.binarySearch(a, number, mid, right)
长度,序列,对每个值标记在dp中位置
dp[0] = s[0];
len = 0;
for(i = 1; i < N; i++)
{
if(s[i] > dp[len])
dp[++len] = s[i];//加在数列的最后
else
*lower_bound(dp, dp+len, s[i])=s[i];//用二分法找dp中第一个大于s[i]的数,要是不会用循环也是可以的。
}
len++;
dp[0]=s[0];
len=1;
pos[0]=len;
for(i=1; i<N; i++)
{
if(s[i]>dp[len])
{
dp[++len]=s[i];
pos[i]=len;//记录s[i]在dp数组中出现的位置
}
else
{
int m=lower_bound(dp+1, dp+len+1, s[i])-dp;
dp[m]=s[i];
pos[i]=m;
}
}
for(i=N-1; i>=0; i--)
{
if(!len)
break;
if(pos[i]==len)
{
ans[len]=i;
len--;
}
}
二维
如0-1背包,都是从小到
区间模型
对于每个问题,都是由子区间推导过来的,我们称之为区间模型
树模型
在数据结构树上面进行最求最优解、最大值等问题
找零钱问题
dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]]
矩阵最小路径
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + m[i][j]
最长公共子序列
第一个,第一行,第一列,每个填表
相等则为对角加1,否则为上或左大的
最后一个为长度,递归求序列
最长公共子串
需要连续
最长回文子字符串
P[i,i]=1
P[i,j]=P[i+1,j-1],if(s[i]==s[j])
=0 ,if(s[i]!=s[j])
0-1背包问题
二维
第一,包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
第二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{ V(i-1,j),V(i-1,j-w(i))+v(i) }
其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i)但价值增加了v(i);
一维
设f[j]表示重量不超过j公斤的最大价值 可得出状态转移方程
f[j]=max{f[j], f[j−a[i]]+b[i]}
for(int i=1;i<=n;i++){
for(int j=m;j>=a[i];j--)
f[j]=max(f[j], f[j-a[i]]+b[i]);
}
最长整除子序列
类似最长升序子序列
寻找和为定值的多个数
0-1背包