动态规划1(一维 — 跳台阶&&最长上升子序列)

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]表示的问题是什么。一般来说,这个问题定义清楚,就成功了大半。
状态转移方程:转态转移就是根据子问题(上一阶段)状态和决策来导出本问题(当前阶段)的状态,确定了决策方法,就可以写出转态转移方程。
边界条件:状态转移方程是一个递推式,需要一个触发的边界条件来最终解出动态规划问题。
参考资料

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值