动态规划(一)

一、从递推到动规(一)


引:
编写程序:如果有一对小兔,每一个月都剩下一对小兔,而所生下的每一对小兔在出生后的第三个月都生下一对小兔。那么,由一对兔子开始,满一年时一共可以繁殖成多少对兔子?

在这里插入图片描述
得到了递推方程 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2) f(n)=f(n1)+f(n2)

推算:
第一个月 -> 一个兔子
第二个月 -> 两只兔子(第一个月生的)
第三个月 -> 三只兔子 (二月的两只兔子 + 一月成熟兔子生的兔子)
第四个月 -> 五只兔子 (三月的三只兔子 + 二月成熟兔子生的兔子)
。。。
以此递归得出
在这里插入图片描述
代码演示为:

#include<stdio.h>

int f(int n) {
    switch (n) {
        case 1: return 1;
        case 2: return 2;
        default : return f(n - 1) + f(n - 2);
    }
}
int main() {
    int n;
    scanf("%d", &n);
    printf("%d", f(n));
    return 0;
}

思考:
当n为40会出现什么问题?
答:会出现两个问题
一:程序运行效率问题
(递推过程加记忆化 或 改成逆向递推求解)
正向基于递归,逆向基于循环
二:程序计算结果超过整型表示范围,结果溢出错误 (改成大整数求解)

出现了重复计算的问题:
在这里插入图片描述

一、确定递推状态

注意: 这是学习递推问题的重中之重!!!!!
递推状态 && 状态定义 状态定义:数学符号 + 对该数学符号的描述 如何做递推状态?

  1. 确定状态的维数
  2. 确定函数的因变量是什么?(通常为求解的量)
  3. 影响因变量的值 -> 自变量
  4. 做自变量到因变量的状态映射 确定递推状态
    例:
    f ( x ) = y f(x) = y f(x)=y
    y : 问题中的求解量也是我们所谓的因变量
    x:问题中直接影响求解量的部分,也是我们所谓的自变量
    本质:就是寻找问题中的自变量与因变量

二、确定递推公式

本质:分析状态中的容斥关系
容斥原理:
求整体时,(容)先把部分面加相加,(斥)减去相交的部分
假设两个圆,两个圆覆盖的总面积为 圆一面积 + 圆二面积 - 两圆相交的面积
当该符号可以自己表示自己的时候就算推导出来了 (f(n) f(n + 1) 都算一个符号)
例:
f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2) f(n)=f(n1)+f(n2)
f ( n − 1 ) , f(n - 1), f(n1) 代表n - 1个兔子数量,恰巧等于第n个月的成年兔子数量
f ( n − 2 ) , f(n - 2), f(n2) 代表n - 2个兔子数量,恰巧等于第n个月的幼年兔子数量
所谓的推导,就是推导上面的这两句话的内容

三、程序实现

  1. 递归 + 记忆化
  2. 循环实现

练习题:

练习一:爬楼梯
一个人每次只能走2节楼梯或者3节楼梯,问走到第N节楼梯一共有多少种方法


练习二:墙壁涂色
给一个环形的墙壁涂颜色,颜色一共有三种,,分别是红、黄、蓝,墙壁被竖直地划分成wallsize个部分,相邻的部分颜色不能相同,请你写出函数paintWallCounts计算出一共有多少种给房间上色的方案


练习三:钱币问题
现在给你1,2,5,10,20,50,100,200元若干张,问你想要凑足N元钱,一共有多少种不用的方法?
注意(1,1,2)和(1,2,1)属于同一种方法


爬楼梯:

因变量:方法数

  1. 确定递归状态
    f ( n ) f(n) f(n)
    自变量:上的楼梯数
    f ( n ) f(n) f(n) 代表走到第n节台阶的方法总数
  2. 确定递归公式
    分类:
    最后一步走两步 :从n - 2 级台阶走两步
    最后一步走三步 :从n - 3 级台阶走三步
    f ( n ) = f ( n − 2 ) + f ( n − 3 ) f(n) = f(n - 2) + f(n - 3) f(n)=f(n2)+f(n3)

墙壁涂色
方法一: 通用

  1. 确定递推状态
    因变量:方法总数
    自变量:墙壁数量
    解题技巧相关量:头部颜色,尾部颜色 ( 便于最后挑出首尾颜色相同的方案 )
    f(n, i, j) n块墙壁,头部涂第i种颜色,尾部涂第j种颜色的方案组数
  2. 确定递推公式
    一共n块,首颜色为i,尾颜色为j的数量,为n - 1块时,首颜色为i 尾颜色不是j的数量之和
    f ( n , i , j ) = ∑ k f ( n − 1 , i , k ) ( k ≠ j ) f(n, i, j) = \sum_k f(n - 1, i, k) (k \ne j) f(n,i,j)=kf(n1,i,k)(k=j)
    首尾颜色不同
    答案则为 ∑ i ∑ j f ( n , i , j ) ( i ≠ j ) \sum _i \sum_j f(n, i, j) (i \ne j) ijf(n,i,j)(i=j)

方法二:

  1. 确定递推状态
    假设一共三种颜色1, 2 ,3
    思考: f ( n , 1 , 3 ) f(n, 1, 3) f(n,1,3) f ( n , 2 , 3 ) f(n, 2, 3) f(n,2,3)的值谁大?
    不难想,这两个值一样大
    所以让初始状态颜色固定,保留尾状态 f ( n , j ) f(n, j) f(n,j) ,最后一共 j j j 种方案
  2. 确定推导公式
    f ( n , j ) = ∑ k f ( n − 1 , k ) ( j ≠ k ) f(n, j) = \sum_k {f(n - 1, k)}(j \ne k) f(n,j)=kf(n1,k)(j=k)
    答案则为 ∑ k f ( n , j ) × m ( j ≠ k , m 为 总 颜 色 数 ) \sum_k{f(n, j)} \times m (j \ne k, m为总颜色数) kf(n,j)×m(j=km)

方法三:本题最优

  1. 确定递推状态
    设一共有m种颜色
    假设 f ( n ) f(n) f(n) 为n块首尾不同颜色墙块
  2. 确定推到公式
    则我们看第 n − 1 n - 1 n1 块墙面与第 1 1 1 块墙面
    若第 n − 1 n - 1 n1 块与第 1 1 1 块墙面颜色相同 则第 n n n 块有 m − 1 m - 1 m1 种选择
    而这个时候 n − 1 n - 1 n1 块与 第 1 1 1 块颜色相同,那么 n − 2 n - 2 n2 和第 1 1 1块颜色不同 f ( n − 2 ) f(n - 2) f(n2)
    否则有 m − 2 m - 2 m2种选择
    f ( n ) = ( m − 1 ) ∗ f ( n − 2 ) + ( m − 2 ) ∗ f ( n − 1 ) f(n) = (m - 1) * f(n - 2) + (m - 2) * f(n - 1) f(n)=(m1)f(n2)+(m2)f(n1)
  3. 代码演示
    (题目需要大整数运算)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <ostream>
using namespace std;

class BigInt : public vector<int> {
public :
    BigInt() {
        push_back(0);
    }
    BigInt(int x) {
        push_back(x);
        proccess_digit();
    }

    BigInt operator*(int x) {
        BigInt ret(*this);
        ret *= x;
        return ret;
    }

    void operator*=(int x) {
        for (int i = 0; i < size(); i++) at(i) *= x;
        proccess_digit();
        return ;
    }

    void operator+=(const BigInt &num) {
        for (int i = 0; i < num.size(); i++) {
            if (i == size())push_back(num[i]);
            else at(i) += num[i];
        }
        proccess_digit();
        return ;
    }

    BigInt operator+(const BigInt &num) {
        BigInt ret(*this);
        ret += num;
        return ret;
    }

    void proccess_digit() {//处理进位过程
        for (int i = 0; i < size(); i++) {
            if (at(i) < 10) continue;
            if (i + 1== size()) push_back(0);//扩位
            at(i + 1) += at(i) / 10;
            at(i) %= 10;
        }
        return ;
    }
};

ostream &operator<<(ostream &out, const BigInt &num) {
    for (int i = num.size() - 1; i >= 0; --i) {
        out << num[i];
    }
    return out;
}

int main() {
    int n, k;
    cin >> n >> k;
    //利用滚动数组
    BigInt f[3] = {0};//f[n] = f[n - 1] * (k - 2) + f[n - 2] * (k - 1)
    f[1] = k;
    f[2] = k * (k - 1);
    f[0] = k * (k - 1) * (k - 2);
    for (int i = 4; i <= n; i++) {
        f[i % 3] = f[(i - 1) % 3] * (k - 2) + f[(i - 2) % 3] * (k - 1);
    }
    cout << f[n % 3] << endl;

    return 0;
}

钱币问题
6. 确定递推状态
f ( n , m ) f(n, m) f(n,m): 使用前n种拼凑目标金额m
自变量:目标金额 + 使用的钱币种类
7. 确定递归公式
分类:
以是否使用第n种钱币作为分类
没用第n种 f ( n − 1 , m ) f(n - 1, m) f(n1,m)
一定用了第n种 f ( n , m − v a l ( n ) ) f(n, m - val(n)) f(n,mval(n)) 把该钱币的金额,从总金额种拿走
f ( n , m ) = f ( n − 1 , m ) + f ( n , m − v a l ( n ) ) f(n, m) = f(n - 1, m) + f(n, m - val(n)) f(n,m)=f(n1,m)+f(n,mval(n))
8. 程序实现
当只选择一种钞票:只有一种选择
当前选择的最大面额钞票大于手中拥有的钞票,选择的可能和不选这种钞票的数量相同
其余为推导公式

代码演示:

#include<iostream>
#include <stdio.h>
using namespace std;


int method[25][10005] = {0};
int val[25] = {0};
int f(int n, int m) {
    for (int j = 1; j <= m; j++) {
        for (int i = 1; i <= n; i++) {
            method[i][j] = method[i - 1][j] % 9973;
            if (j >= val[i])  {
                method[i][j] += method[i][j - val[i]];//执行递推公式
            }
        }
    }
    return method[n][m] % 9973;//返回选择n张纸币凑m元的方法
}

int main() {
    for (int i = 0; i < 25; i++) {
        method[i][0] = 1;
    }
    int n, m;
    while(~scanf("%d%d", &n, &m)) {
        for (int i = 1; i <= n; i++) {
            scanf("%d", &val[i]);
        }
        printf("%d\n", f(n, m));
    }
    return 0;
}

二、从递推到动规(二)

引:数字三角形
编写程序:有一个由数字组成的三角形,站在上一层的某个点,只能到达其下方左右的两个点。现在请找到一条从上到下的路径,使得路径上的所有数字相加之和最大。

  1. 确定递推状态:(状态定义的不同,得出的递推公式不同,状态定义不止是一个符号,还是代码的思维)

    1. f ( i , j ) f(i, j) f(i,j) 代表从底边走到(i , j) 点的最大值
    2. f ( i , j ) f(i, j) f(i,j) 代表从顶点走到(i , j) 点的最大值
  2. 确定递推公式:(状态转移)

    1. f ( i , j ) = max ⁡ [ f ( i + 1 , j ) , f ( i + 1 , j + 1 ) ] + v a l ( i , j ) f(i, j) = \max[f(i + 1, j), f(i + 1, j + 1)] + val(i , j) f(i,j)=max[f(i+1,j),f(i+1,j+1)]+val(i,j)
    2. f ( i , j ) = max ⁡ [ f ( i − 1 , j ) , f ( i − 1 , j − 1 ) ] + v a l ( i , j ) f(i, j) = \max[f(i - 1 , j), f(i - 1, j - 1)] + val(i, j) f(i,j)=max[f(i1,j),f(i1,j1)]+val(i,j)
  3. 递推公式解读:

    1. 在这里插入图片描述
    2. 在这里插入图片描述
  4. 两种方法的对比:
    本质:两种状态定义的方式的对比

    1. 第一种:不用做边界判断,最终结果直接存储在 f ( 0 , 0 ) f(0, 0) f(0,0)
    2. 第二种:需要做边界判断, 最终结果存储在一组数据中
    3. 结论:第一种要比第二种优秀。
  5. 代码演示

#include <iostream>
#include <stdio.h>
using namespace std;

int Tri[1005][1005];
int f[1005][1005];//代表从边走到(i, j)点的最大值

int method_1(int n) {
    int f[1005][1005] = {0};//代表从边走到(i, j)点的最大值
    for (int i = n - 1; i >= 0; i--) {
        f[n - 1][i] = Tri[n - 1][i];
    }
    for (int i = n - 2; i >= 0; i--) {
        for (int j = i; j >= 0; j--) {
            f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + Tri[i][j];
        }
    }
    return f[0][0];
}

int method_2(int n) {
    int f[1005][1005] = {Tri[0][0]};//代表从顶点走到(i, j)点的最大值
    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            if (j == 0) f[i][j] = f[i - 1][j] + Tri[i][j];//边界判断
            else f[i][j] = max (f[i - 1][j - 1], f[i - 1][j]) + Tri[i][j];
        }
    }
    int ans = f[n - 1][0];
    for (int i = 1; i < n ; i++) {
        if (f[n - 1][i] > ans)
        ans = f[n - 1][i];
    }//找最大值
    return ans;
}
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            scanf("%d", &Tri[i][j]);
        }
    }
    printf("%d\n", method_1(n));
    return 0;

}

这个问题是最简单的动态规划问题
像递推问题

动态规划是递推问题种的特殊问题
动态规划特点:最优化求解
普通递推问题:求方案问题

数学归纳法

  1. 验证 k 0 k_0 k0 成立
  2. 验证如果 k i k_i ki 成立, 那么 $k_{i + 1} $ 也成立
  3. 联合 步骤1 和 步骤2 ,证明 k 0 − > k n k_0 -> k_n k0>kn 成立

如何求解动态规划问题?

  1. 确定动规状态
    例如: f ( i , j ) f(i, j) f(i,j) 代表从底边走到(i,j)点所能获得的最大值
  2. 确定状态转转移方程,理解:转移、决策
    例如: f ( i , j ) = max ⁡ { f ( i + 1 , j + 1 ) f ( i + 1 , j ) + v a l ( i , j ) f(i, j) = \max \{_{f(i + 1, j + 1)}^{f(i + 1, j)} + val(i, j) f(i,j)=max{f(i+1,j+1)f(i+1,j)+val(i,j)
  3. 正确性证明:求助于数学归纳法
  4. 程序实现
  5. 所谓的转移,把所有决定 f ( i , j ) f(i, j) f(i,j)最优值的状态,放入到决策过程中

附加内容:拓扑序
图形结构是最最抽象的数据结构,必须理解成思维逻辑结构

  1. 拓扑序是一种图形结构上的依赖顺序。一个图的拓扑序不唯一
  2. 拓扑序的本质作用:是把图形结构上编程一个一维序列
  3. 图形结构不能用循环遍历的,一维序列可以
  4. 所有递推问题的更新过程,本质上满足拓扑序

动态规划求解问题的过程种,状态之间的求解顺序,必须满足拓扑序


练习题

练习题1:最长上升子序列问题
有一个数字序列,求期中最长上升子序列的长度


练习题2:最长公共子序列
给出两个字符串,求其两个的最长公共子序列长度。


最长上升子序列问题

  1. 状态定义:
    f ( i ) f(i) f(i) 以i结尾的最长上升序列长度
  2. 状态转移方程
    f ( i ) = max ⁡ ( f ( j ) ) + 1 : v a l ( i ) > v a l ( j ) f(i) = \max (f(j)) + 1 : val(i) > val(j) f(i)=max(f(j))+1:val(i)>val(j)
    状态转移的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    后续重点:优化转移过程
#include<iostream>
#include <stdio.h>
using namespace std;
#define MAX_N 1000005

int arr[MAX_N];
int dp[MAX_N];
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", arr + i);
    int ans = 0;
    for (int i = 0; i < n; i++) {
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (arr[j] >= arr[i]) continue;
            dp[i] = max(dp[i], dp[j]);
        }
        ans = max(ans, dp[i]);
        }
        cout << ans << endl;
        return 0;
    }
}

最长公共子序列

  1. 状态定义:
    f ( i , j ) f(i, j) f(i,j) :第一串前i位,和第二串前j位最长的公共子序列
  2. 状态转移方程
    f ( i , j ) = { max ⁡ f ( i − 1 , j ) , f ( i , j − 1 ) v a l ( i ) ≠ v a l ( j ) f ( i − 1 , j − 1 ) v a l ( i ) = v a l ( j ) f(i, j) = \left\{\begin{aligned} & \max{f(i - 1, j), f(i, j - 1)} &val(i) \ne val(j)\\ & f(i - 1, j - 1) &val(i) = val(j) \end{aligned}\right . f(i,j)={maxf(i1,j),f(i,j1)f(i1,j1)val(i)=val(j)val(i)=val(j)
  3. 时间复杂度 O ( n × m ) O(n \times m) O(n×m)
  4. 代码演示
#include<iostream>
#include <string>
using namespace std;
#define MAX_N 1000
string s1, s2;
int dp[MAX_N + 5][MAX_N + 5];

int main() {
    cin >> s1 >> s2;
    for (int i = 1; i <= s1.size(); i++) {
        for (int j = 1; j <= s2.size(); j++) {
            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            if (s1[i - 1] == s2[j - 1]) {//所有可能进行决策
                dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
            }
        }
    }
    cout << dp[s1.size()][s2.size()] << endl;

    return 0;
}

递推问题的求解方向:
4. 我从哪里来?
例如:数字三角形,兔子繁殖问题,钱币问题,墙壁涂色
5. 我从哪里去?
例如:杂物(P1113)、神经网络(P1038)、旅行计划(P1137)…(洛谷题目)

课后作业题:
1.切割回文
2. 0/1背包
3. 完全背包
4. 多重背包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值