动态规划
example1 Fibonacci
1 2 3 4 5 6 7 8
1 1 2 3 5 8 13 21 …
1 n=1,2
fib(n)
fib(n) = fib(n-1) + fib(n-2) other // overlap sub-problem
fibonacci问题的备忘录模式:保存已经计算出来的答案,无需再重新计算
example2
有八个任务,每个任务只能在指定的时间段做,红色数字代表任务的价值,要求所选的任务时间上不能重叠,求所有任务组合中价值最大的组合

i prev(i) opt(i)
1 [1-4] 0 5 [1]
2 [3-5] 0 5 [1]
3 [0-6] 0 8 [3]
4 [4-7] 1 9 [1, 4]
5 [3-8] 0 9 [1, 4]
6 [5-9] 2(选和它最接近的) 9 [1, 4]
7 [6-10] 3 10 [3, 7]
8 [8-11] 5 13 [1, 4, 8]
选 vi + opt(prev(i))
opt(i) max:
不选 opt(i-1)
选 4+opt(5)
opt(8)
不选 opt(7)
若第一次做8 ,前面还可以做prev(8) = 5
若第一次做7, 前面还可以做prev(7) = 3
prev(6) = 1
.
.
.
prev(2) = 0
example3 选出不相邻的最大和
arr 0 1 2 3 4 5 6
1 2 4 1 7 8 3
选 arr[i] + opt[i-2]
opt(i) max:
不选 arr[i-1]
递归出口:
opt(0) = arr[0]
opt(1) = arr[1] // 前两个只能选一个,选最大的
// python递归实现 和fibonacci一样,递归会产生很多重叠子问题
arr = [1, 2, 4, 1, 7, 8, 3]
def rec_opt(arr, i):
if i== 0:
return arr[0]
elif i == 1:
return max(arr[0], arr[1])
else:
A = rec_opt(arr , i-2) + arr[i]
B = rec_opt(arr, i-1)
return max(A, B)
rec_opt(arr, 6)
// python非递归实现
import numpy as np
def dp_opt(arr):
opt = np.zeros(len(arr))
opt[0] = arr[0]
opt[1] = max(arr[0], arr[1])
for i in range(2, len(arr)):
A = opt[i-2] + arr[i]
B = opt[i-1]
opt[i] = max(A, B)
return opt[ len(arr) -1 ]
example4
判断数组中是否存在和为s的组合

arr = [3, 34, 4, 12, 5, 2]
// 递归实现
def rec_subset(arr, i, s):
if s == 0:
return True
elif i == 0:
return arr[0] == s
elif arr[i] > s:
return rec_subset(arr, i-1, s)
else:
A = rec_subset(arr, i-1, s-arr[i])
B = rec_subset(arr, i-1, s)
return A or B
rec_subset(arr, len(arr)-1), 9)
//非递归实现
import numpy as np
def dp_subset(arr, S):
subset = np.zeros((len(arr), s+1), dtype=bool)
subset[:, 0] = True
subset[0, :] = False
subset[0, arr[0]] =True
for i in range(1, len(arr)):
for s in range(1, S+1):
if arr[i] > s:
subset[i, s] = subset[i-1, s]
else:
A = subset[i-1, s-arr[i]]
B = subset[i-1, s]
subset[i, s] = A or B
r, c = subset.shape
return subset[r-1, c-1]
dp_subset(arr, 9)
example5 硬币问题
现有若干枚硬币,硬币面值分别为1,5,10,20,50,100
要凑出价值为w,至少需要多少枚硬币。比如w = 666
思路一:每次选取面值最大的硬币(贪心思维,只考虑眼前情况)
666 = 6100 + 150 + 110 + 15 + 1*1
共需要6 + 1 + 1 + 1 + 1 = 10
但是当 硬币面值分别为 1、5、 11 问至少需要多少枚硬币,才能凑出15
按照贪心思维(每次选取价值最大的硬币)
15 = 111 + 41 至少需要5枚
然后,仅仅需要3枚价值5的硬币就可以凑出15
所以 ,贪心思维不靠谱
解决方案:
1、贪心算法、存在局限
2、暴力枚举
3、DP
DP分析:
若要凑出w=15,首先面临三种情况的选择
1、当选取了价值为1的硬币时,将面临凑出价值为14的情况
2、当选取了价值为5的硬币时,将面临凑出价值为10的情况
3、当选取了价值为11的硬币时,将面临凑出价值为4的情况
记凑出价值n 所需要的最少硬币数为f(n)
最优解 1 5 11
F(0) 0 x x x
F(1) 1 1+f(0)=1 x x
F(2) 2 1+f(1)=2 x x
F(3) 3 1+f(2)=3 x x
F(4) 4 1+f(3)=4 x x
F(5) 1 1+f(4)=5 1+f(0)=1 x
F(6) 2 1+f(5)=2 1+f(1)=2 x
F(7) 3 1+f(6)=3 1+f(2)=3 x
F(8) 4 1+f(7)=4 1+f(3)=4 x
F(9) 5 1+f(8)=5 1+f4)=5 x
F(10) 2 1+f(9)=6 1+f(5)=2 x
F(11) 1 1+f(10)=3 1+f(6)=3 1+f(0)=1
F(12) 2 1+f(11)=2 1+f(7)=4 1+f(1)=2
F(13) 3 1+f(12)=3 1+f(8)=5 1+f(2)=3
F(14) 4 1+f(13)=4 1+f(9)=6 1+f(3)=4
F(15) 3 1+f(14)=5 1+f(10)=3 1+f(4)=5
原理:
f(n)只与f(n-1), f(n-5), f(n-11)有关
f(n)=min{1 + f(n-1), 1+f(n-5), 1+f(n-11)}
总结:
最优子结构:大问题的最优解可以由小问题的最优解推出
无后效性:一旦f(n)确定,那么之后直接调用它的值就可以,不要再去关心f(n)的计算过程(就是无需再计算,可以由以前求得的值直接代入)
代码(C)
#include<cstdio>
#include<cstdlib>
using namespace std;
int min(int a, int b)
{
return a<b?a:b;
}
int main()
{
int dp[100], i;
dp[0]=0;
int cost;
for(i=1;i<=15;i++)
{
cost=999999;
if(i-1>=0) cost=min(cost, dp[i-1]+1);
if(i-5>=0) cost=min(cost, dp[i-5]+1);
if(i-11>=0) cost=min(cost, dp[i-11]+1);
dp[i]=cost;
printf(“dp[%d]=%d\n”, i, dp[i]);
}
return 0;
}
example6最长上升子序列(LIS)
给定长度为n的序列,从中选取一个子序列,这个子序列需要单调递增
求最长上升子序列的长度
eg:1, 5, 2, 3, 11, 7, 9
则LIS序列为:1,2,3,7,9,长度为5
设计状态
记f(x)为以a[x]结尾的LIS长度,那么LIS=max{f(x)}
如何导出f(x),f(x)从哪里来?
考虑比x小的每一个p,如果a[x]>a[p], 那么f(x)=f§+1
状态转移方程
f(x)=max{f§}+1
p<x,a[p]<a[x]
x: 1 2 3 4 5
a[x] 1 5 3 4 0
f(1)=1
f(2)=2
f(3)=2
f(4)=3
f(5)=1
LIS=3
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
using namespace std;
int a[100];
int dp[100];
int main()
{
int n;
scanf(“%d”, &n);
for(int i=1; i<=n ;i++)
scanf(“%d”, &a[i]);
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=1;j<i; j++)
{
if(a[j]<a[i])
dp[i] =max(dp[i], dp[j]+1);
}
printf(“dp[%d]=%d\n”, i, dp[i]);
}
return 0;
}
总结:
哪些题目适合用DP算法解决?如何设计好的DP算法:
1、满足最优子结构
大问题可以由小问题推出,大问题与小问题求解思路一致。
2、满足无后效性
一旦f(n)确定,后续我们直接调用它的值就可以,而不用关心它是怎么过来的
3、设计好状态
想办法把当前局面表达出来
4、设计好状态转移方程
可以从两个方面考虑,我从哪里来,或者我到哪里去
5、代码实现
example7最长公共子序列
s1=abcdaf
s2=abcdf
a b c d a f
a 1 1 1 1 1 1
b 1 2 2 2 2 2
c 1 2 3 3 3 3
d 1 2 3 4 4 4
f 1 2 3 4 4 5
s1 length n1
s2 length n2
if (s1.charAt(i) == s2.charAt(j)) {
dp[i][j] = d[i-1][j-1] +1
} else {
dp[i][j] = Math.max(d[i][j-1], d[i-1][j-1])
}
return dp[n1][n2]