Dynamic Programming的基本思想
- 将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
- 有些子问题被重复计算了很多次。我们可以用一个表来记录所有已解的子问题的答案(打表),在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间,这就是动态规划法的基本思路
- 适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的,如下例dp[i ]依赖于dp[i - 1]和dp[i - 2]
跳台阶
例题原题链接
一个楼梯共有n级台阶,每次可以走一级或者两级,问从第0级台阶走到第n级台阶一共有多少种方案。
输入格式
共一行,包含一个整数n
输出格式
共一行,包含一个整数,表示方案数。
数据范围
1 ≤ n ≤ 15
输入样例:
5
输出样例:
8
分析:
跳到第n个台阶有两种跳法:(从后往前分析)
- 从倒数第二个台阶即第n - 1个台阶开始,跳一个台阶就可以到第n个台阶
- 从倒数第三个台阶即第n - 2个台阶开始,一次跳2个台阶或者一次跳一个台阶(跳两次)也可以到第n个台阶
- 设dfs(n)表示跳到第n个台阶的方案数:可得到状态转移方程—dfs(n) = dfs(n - 1) + dfs(n - 2)
AC代码—递归写法
#include <bits/stdc++.h>
using namespace std;
//由数据范围n <= 15知可以递归求解(不会TLE)
int dfs(int x)
{
if (x == 1) return 1;//递归出口:跳到第1个台阶只有1种方案
if (x == 2) return 2;//递归出口:跳到第2个台阶有2种方案
return dfs(x - 1) + dfs(x - 2);
}
int main()
{
int n;
while (cin >> n) cout << dfs(n) << endl;//输出跳到第n个台阶的方案数
return 0;
}
AC代码—DP写法:
#include <bits/stdc++.h>
using namespace std;
int dp[20];//打表
int main()
{
dp[1] = 1;
dp[2] = 2;
int n;
while (cin >> n)
{
//初始化(从后往前推,从前往后计算,效率O(N))
for (int i = 3;i <= n;i++) dp[i] = dp[i - 1] + dp[i - 2];
cout << dp[n] << endl;
}
return 0;
}
最长上升子序列
- 最长上升子序列(Longest Increasing Subsequence—LIS)
- sequence 顺序,序列
- Subsequence 子序列
设有一个正整数的序列:b1,b2…,bn,对于下标1 < 2 < …< i,有b1 <= b2 <= …. <= bi,则称存在一个长度为 i 的不下降序列(注意:此处并非严格上升)。
例如,下列数列13 7 9 16 38 24 37 18 44 19 21 22 63 15对于下标i=1,i=4,i=5,i=9,i=13,且满足13 < 16 < 38 < 44 < 63,则存在长度为5的不下降序列。但是还存在其它的不下降序列。
问题:当b1,b2,…,bn给出后,求出最长的不下降序列。
分析
- 最长不降序列必然以某个数为结束
- 上题数列中有14个数,若我们分别求出以每个数结束的最长不降序列的长度(子问题),得到里面的最大值,就是问题的答案:这个数列的最长不降序列的长度
- 设F( i )表示以第 i 个元素结束的最长不降序列的长度。
- F(4) = 16,由于F(1) = 13,F(2) = 7,F(3) = 9,均小于F(4),满足不降。则会发现F(4)与F(3)、F(2)、F(1)均有联系,所以以第4个元素结束的最长不降序列的长度 = max{以第3个元素结束的最长不降序列的长度,以第2个元素结束的最长不降序列的长度,以第1个元素结束的最长不降序列的长度} + 1

- 同理,F(8) = 18,由于F(1) = 13,F(2) = 7,F(3) = 9,F(4) = 16均小于F(4),满足不降。而F(5) = 38、F( 6 ) = 24、F(7) = 37,均大于F(8),则会发现F(8)与F(4)、F(3)、F(2)、F(1)均有联系,所以以第8个元素结束的最长不降序列的长度 = max{以第 4 个元素结束的最长不降序列的长度,以第3个元素结束的最长不降序列的长度,以第2个元素结束的最长不降序列的长度,以第1个元素结束的最长不降序列的长度} + 1

- 由多个例子,总结出LIS状态转移方程:(设元素存储在a数组中)
当a[j] <= a[i]且1 <= j <= i - 1时,有F(i) = max{F(j)} + 1
下面给出A和F数组,模拟以上过程求出F数组的值

- 首先,F(1)之前没有与其相关的子问题,故F(1) = 1
- 在计算F(2)时,F(2)表示以A数组中第2个元素22结束的最长不降序列的长度,由于A[2] > A[1],故F(2)的值可以在F(1)的基础上加1,即F(2) =F(1) + 1 = 2
- 在计算F(3)时,F(3)表示以A数组中第3个元素5结束的最长不降序列的长度,由于由于A[2] > A[3],A[1] > A[3],故F(1)和F(2)与F(3)的值无关,F(3)之前没有与其相关的子问题,故F(3) = 1
- 在计算F(4)时,F(4)表示以A数组中第4个元素18结束的最长不降序列的长度,由于由于A[1] > A[4],A[2] > A[4],故F(1)和F(2)与F(4)的值无关,故F(4)的值可以在F(3)的基础上加1,即F(4) = F(3) + 1 = 2
- 在计算F(5)时,F(5)表示以A数组中第5个元素2结束的最长不降序列的长度,由于由于A[1] > A[5],A[2] > A[5],A[3] > A[5],A[4] > A[5],所以第5个数不能接在前四个数的任何一个后面形成不降序列。故F(5)和F(1) ~ F(4)均无关,F(5)之前没有与其相关的子问题,故F(5) = 1
- 在计算F(6)时,F(6)表示以A数组中第6个元素11结束的最长不降序列的长度,由于由于A[6] > A[3],A[6] > A[5],所以第6个数可以拼接在第3或者第5个数后面形成不降序列。换句话说,F(6)与F(3)、F(5)这两个子问题相关,故F(6) = max{F(3),F(5)} + 1 = 2
- 在计算F(7)时,F(7)表示以A数组中第7个元素29结束的最长不降序列的长度,由于由于A[1] ~ A[6]均小于A[7],所以第7个数可以拼接在前6个数的任意一个后面形成不降序列。换句话说,F(7)与F(1) ~ F(6)这6个已经解决的子问题相关,故F(7) = max{F(1),F(2),F(3),F(4),F(5),F(6)} + 1 = 3
- … …
- 以此类推,得到最后的F(i)数组
实现上述过程的代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N];//f[i]的值表示以第i个元素结束的最长不降序列的长度
int a[N];//存储数据
int ans;//这个数列的最长不降序列的长度
int main()
{
int n;
ans = 0;
while (scanf("%d",&n) != EOF)
{
memset(f,0,sizeof f);//初始化
memset(a,0,sizeof a);
for (int i = 1;i <= n;i++) scanf("%d",&a[i]);//接收数据
for (int i = 1;i <= n;i++)//i:从1 ~ n推导f[i]
{
for (int j = 1;j <= i - 1;j++)//j:寻找前i - 1个子问题
{
if (a[j] <= a[i])//当a数组满足a[j]<=a[i]时,表示可以拼接序列
f[i] = max(f[i],f[j]);//找到f数组中前i-1个位置的最大值
}
f[i]++;//最大值 + 1
ans = max(ans,f[i]);
}
for (int i = 1;i <= n;i++) printf("%d ",f[i]);
printf("\n");
printf("%d",ans);
}
return 0;
}
输入数据
14
19 22 5 18 2 11 29 16 1 6 21 21 25 3
得到
1 2 1 2 1 2 3 3 1 2 4 5 6 2
6
根据这个例子总结一下动态规划解题三步骤:
①定义动态规划求解对象:简单的说就是定义dp[i]表示的问题是什么。一般来说,这个问题定义清楚,就成功了大半。
②状态转移方程:转态转移就是根据子问题(上一阶段)状态和决策来导出本问题(当前阶段)的状态,确定了决策方法,就可以写出转态转移方程。
③边界条件:状态转移方程是一个递推式,需要一个触发的边界条件来最终解出动态规划问题。
参考资料
232

被折叠的 条评论
为什么被折叠?



