DP入门

1,787 阅读6分钟

动态规划

 1.什么是动态规划

  简而言之动态规划就是把原问题视作若干个重叠的子问题的逐层递进,每个子问题的求解过程都构成一个“阶段“。在完成前一个阶段的计算后,动态规划才会执行下一个阶段的计算。

 2.什么时候要使用动态规划

  动态规划一般用来求解最优化问题(比如:最大值、最小值等),如果该问题能够分解为若干个子问题,且若干个子问题中也存在重叠子问题,则采用动态规划。

名词解析:重叠子问题就是一个解决方案中,可能存在很多子问题,但是不同的子问题确很少,少量的子问题被重复处理的很多次就叫做重叠子问题。

案例:递归求解Fibonacci数列

代码如下:

    int fib(int x){
        if(x == 1 || x == 2) return 1;
        else return fib(x - 1) + fib(x - 2);
    }

我们以来手动模拟一下求f(3)、f(4)、f(5)

fib

  我们发现每次递归求这个数的时候,要想求得第n个数的数值是多少就要先求f(n - 1) + f(n - 2),而f(n - 1) 等于n - 1的前两项之和,随着n不断增大,每次求f(n)的时候都会产生大量的重叠子问题,求解f(5)时,就重复计算了f(3),显然就是浪费时间,以 n = 40 为例,这里我们记录了 fib 函数总共调用的次数以及运算总共耗时,仅仅是计算第 40 项,fib 函数调用的次数高达3亿多次,于是我们就想到了,存储它每个子问题的结果,以后再次需要的时候,就直接访问,不再重复计算,代码如下:

    int f[100010];
        int fib(int x) {
            if(x == 0)return 0;
            if(x == 1 || x == 2){
            f[x] = 1;
            return f[x];
        }else if(f[x] == 0){
            f[x] = fib(x - 1) + fib(x - 2);
        }
        return f[x];
        }
    //题目链接:https://leetcode-cn.com/problems/fibonacci-number/

采用递归求解与动态规划求解(可能数据有点水,不然动态规划应该会快很多)

fibtest

DP(动态规划)就是将通过已知的结果去推导未知的结果,此方法也叫刷表法,下面给大家介绍一个DP分析方法——闫式DP分析法(闫雪灿大佬发明的,并创办了Acwing)


例题1:最长上升子序列

题目描述

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000

-10^9 ≤数列中的数≤ 10^9

样例
输入样例:
7
3 1 2 1 8 5 6
输出:
4

(动态规划)

LIS

以集合的方式理解DP:最主要的就是状态表示与状态计算

状态表示——集合:表示以第i个数结尾的上升子序列

	——属性:存储的为最大值(一般为最大值、最小值、数量这三个任意一个)

f[i]就表示为第i个数结尾的最大的上升子序列的个数

状态计算:要求f[i],你就要知道前i - 1 个数有多少个数比a[i]小,如果比a[i]小那么f[i]的结果就要+1,于是得到转移方程为:f[i] = max(f[i],f[j] + 1)

C++ 代码

    #include <iostream>
    #include <algorithm>
    using namespace std;
    const int N = 1010;
    int f[N],a[N],n;
    int main(){
        cin >> n;
        for(int i = 1; i <= n; i ++) cin >> a[i];
        
        for(int i = 1; i <= n; i ++){
            f[i] = 1;					//表示只选第i个
            for(int j =1 ; j < i; j ++)		//从 j 到 i -1 枚举
                if(a[i] > a[j]) f[i] = max(f[i], f[j] + 1); //如果满足前面的数比 a[i] 小就更新f[i]
        }
        
        int res = 0;
        for(int i = 0; i<=n; i ++) res = max(res,f[i]);//找出最大值
        cout << res;
        return 0;
    }

例题1:最长上升子序列

题目描述

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数N和M。

第二行包含一个长度为N的字符串,表示字符串A。

第三行包含一个长度为M的字符串,表示字符串B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000

样例

输入:
4 5
acbd
abedc
输出:
3

(动态规划)

LCS
  这里的状态f[i,j]表示所有在第一个序列中的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列。

——属性为:max

  这里难点在于状态计算的划分区间,我们通过选与不选将它分成4种情况:

00表示a[i],b[j]都不选:显然这种情况很好计算,就是f[i-1,j-1]的值,因为新增a[i],b[j]都没用

01表示不选a[i]选a[j]:f[i-1,j]

10表示选a[i]不选a[j]:f[i,j-1]

11表示a[i],b[j]都选:它的计算是f[i-1,j -1] + 1 ,表示在之前的基础之上在加一

难点是第二种和第三种情况:因为f[i - 1,j]和第二种情况并不相等,第二种情况一定是f[i - 1,j]的子集,而f[i - 1,j]又是f[i,j]的子集,所以我们才可以使用f[i-1,j]来代替第二种情况的取值,第三种情况类似,不过再写的时候一般都会忽略第一种情况,因为第一种情况又是第二种和第三种情况的子集~~~

C++ 代码

    #include <iostream>
    #include <algorithm>
    using namespace std;
    const int N = 1010;
    int f[N][N];
    char a[N],b[N];
    int n,m;
    int main(){
        cin >> n >> m;
        scanf("%s%s",a + 1, b + 1);	// a+1 ,b+1 表示读入从1号下标开始
        
        for(int i = 1; i <= n; i ++){	//枚举a[i]
            for(int j = 1; j <= m; j++){//枚举b[j]
                f[i][j] = max(f[i - 1][j],f[i][j - 1]);//表示 00 01 10的情况
                if(a[i] == b[j]) f[i][j] = max(f[i][j],f[i-1][j-1] + 1);//当a[i] == b[j] 表示 11这种情况
            }
        }
        cout << f[n][m];		//结果
        return 0;
    }

这就是最简单的几个DP问题,后面有机会将背包问题整理出来:

  • 0/1背包 : 每个物品只能用一次
  • 完全背包: 每个物品有无数多次
  • 多重背包: 每个物品有固定的次数
  • 分组背包: 每组物品至少选一个