刷题记录——动态规划

1.《过马卒》一道入门dp

刷题记录——动态规划_第1张图片

刷题记录——动态规划_第2张图片

借着本题还玩了一晚上象棋(bushi

本蒟蒻终于(复述)了一遍佬的答案,思路是这样的

理解题目

在过河卒问题里,棋盘上有一个卒和一匹马。卒只能向下或者向右移动,马会控制它所在位置以及按照 “日” 字规则能跳到的位置,卒不能经过马控制的点。

我们的目标是计算卒从棋盘左上角走到右下角有多少种不同的路径。

检查点是否被马控制的函数 check

根据马走 “日” 字的规则,马控制的点满足两个条件:

一是两点横纵坐标差值的绝对值之和为 3,二是横纵坐标差值绝对值的最大值为 2。同时满足这两个条件的点就是马控制的点,返回 true,否则返回 false

动态规划计算路径数量

动态规划状态转移方程

在过河卒问题里,设 f[i][j] 表示卒走到坐标 (i, j) 位置的路径数量,也就是说,卒只能从上方 (i - 1, j) 或者左方 (i, j - 1) 走到当前位置 (i, j),那么到达 (i, j) 的路径数量就是到达上方点和左方点的路径数量之和。状态转移方程为 

f[i][j] = f[i - 1][j] + f[i][j - 1](前提是该点没有被马控制)。

坐标加 2 避免边界问题

终点坐标和马的坐标,然后都加 2 是为了避免边界处理的麻烦,相当于给棋盘的最上面和最左边空出两行两列。

假设我们不把坐标加上 2,直接从 (0, 0) 开始计算。当卒在棋盘的最左边一列(即 j = 0)时,在计算 f[i][0] 时,根据状态转移方程需要用到 f[i][0 - 1],也就是 f[i][-1],这就出现了数组越界的情况,因为数组下标不能为负数。

如果坐标只加 1,在处理棋盘最左上角的实际起点位置(加 1 后变为 (1, 1) )时,还是会面临边界问题。例如,当计算 f[1][1] 时,根据状态转移方程需要用到 f[1 - 1][1] (即 f[0][1] )和 f[1][1 - 1] (即 f[1][0] ),这样就出现了数组越界的情况,因为数组下标为 0 可能不在我们有效的棋盘范围内。

空间优化:二维数组转化为一维数组——滚动数组

这个地方我觉得语言描述很难看懂,也可能是本人语言能力和理解力不行,其实写一遍两层for循环就知道咋回事了,外循环是行,内循环是列,二维数组转化为一维数组:f [ j ]表示到 j 列的路径数,每增加一行,更新一遍f [ j ],即

f [ j ] = f [ j ] + f [ j - 1 ]

滚动数组其实就是复用

这里举例子说明。

例如从(0,0)走到(2,2)(坐标加2后,即从(2,2)走到(4,4) )

初始f [ 2 ]=1

刷题记录——动态规划_第3张图片

计算第 2 行(i = 2),f [ 2 ]=1, f [ 3 ]=1, f [ 4 ]=1

刷题记录——动态规划_第4张图片

 计算第 3 行(i = 3),f [ 2 ]=1, f [ 3 ]=2, f [ 4 ]=3

刷题记录——动态规划_第5张图片

计算第 4 行(i = 4) f [ 2 ]=1, f [ 3 ]=3, f [ 4 ]=6

刷题记录——动态规划_第6张图片

#include 
using namespace std;
#define ll long long

int dx, dy, mx, my;
ll f[25];

bool check(int x, int y){
    if (x==mx && y==my) return 1;
    return (abs(x-mx)+abs(y-my)==3 && max(abs(x-mx), abs(y-my))==2);
}

int main(){
    cin >> dx >>dy >>mx >>my;
    dx += 2;
    dy += 2;
    mx += 2;
    my += 2;
    f[2]=1;
    for (int i=2; i<=dx; i++){
        for (int j=2; j<=dy; j++){
            if (check(i, j))  {
                f[j]=0;
                continue;     //直接跳到for循环的下一次迭代
            }
            f[j] += f[j-1];
        }
    }
    cout << f[dy] <

2.最长上升子序列(一道dp板子题)

B3637

刷题记录——动态规划_第7张图片

刷题记录——动态规划_第8张图片

思路很简单,两层for循环,但本蒟蒻还是看的题解才写出来...

#include 
using namespace std;

int main(){
    int a[5010], n;
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> a[i];
    }
    vector f(n+1, 1);
    for (int i=1; i<=n; i++){
        for (int j=1; j<=i; j++){
            if (a[j]result)    result = f[i];
    }
    cout << result <

3.最大子段和

刷题记录——动态规划_第9张图片

本题仍然参考大佬的题解

  • 第一个数为一个有效序列
  • 如果一个数加上上一个有效序列得到的结果比这个数大,那么该数也属于这个有效序列。
  • 如果一个数加上上一个有效序列得到的结果比这个数小,那么这个数单独成为一个新的有效序列
#include 
using namespace std;

int n, a[200010], b[200010];
//b[i]表示循环到i时,第i个数所在的有效序列的和
int main(){
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> a[i];
    }
    for (int i=1; i<=n; i++){
        if (b[i-1]+a[i]>a[i])  b[i]=b[i-1]+a[i];
        else  b[i]=a[i];
    }
    int result =b[1];
    for (int i=1; i<=n; i++){
        if (b[i]>result)    result = b[i];
    }
    cout << result<< endl;
    return 0;
}

大佬还对空间复杂度进行了优化,直接O(1)了,鼠鼠拜服,学习一下这种写法。

就是把a数组和b数组都优化成1个变量,并且是在求数组b[ ] 的过程中找最值。

#include 
using namespace std;

int n, a, b, ans=-10010;
int main(){
    cin >> n;
    for (int i=1; i<=n; i++){
        cin >> a;
        if (i==1) b=a;
        else  b = max(a, a+b);
        ans = max(ans, b);
    }
    cout << ans<< endl;
    return 0;
}

4.

P3399

刷题记录——动态规划_第10张图片

刷题记录——动态规划_第11张图片

写到本题的时候本蒟蒻还没悟怎么用动态规划思想解题,然后又又又看题解里大佬说推状态转移方程,动态规划思想的核心难道是推推状态转移方程(?,2025年AI已经发展到如此规划,本蒟蒻仍然学习不好好利用有效工具,罪过,下面是豆包老师的总结:

动态规划(Dynamic Programming,简称 DP)是一种通过把原问题分解为相对简单的子问题,并保存子问题的解来避免重复计算,从而解决复杂问题的算法策略。下面详细解释动态规划的思想、状态转移方程以及如何刷题做到举一反三。

动态规划思想核心

动态规划主要基于两个核心特性:最优子结构和重叠子问题。!!!

最优子结构

一个问题的最优解可以由其子问题的最优解组合而成。也就是说,大问题的最优解包含了小问题的最优解。例如在过河卒问题中,到达某个点的最优路径数量(这里路径数量的最优就是最多路径)可以由到达其上方点和左方点的路径数量推导出来,即 f[i][j] = f[i - 1][j] + f[i][j - 1] ,这体现了最优子结构的特性。

重叠子问题

在求解问题的过程中,很多子问题会被重复计算。动态规划通过保存这些子问题的解(通常使用数组),避免了重复计算,从而提高了算法的效率。比如在斐波那契数列问题中,计算 F(n) 时会多次用到 F(n - 1) 和 F(n - 2) ,使用动态规划可以把已经计算过的 F(k) 的值保存起来,下次再需要时直接使用,而不是重新计算。

状态转移方程

状态转移方程是动态规划的关键,它描述了如何从子问题的解推导出原问题的解。推导状态转移方程一般可以按以下步骤进行:

定义状态

明确问题的状态表示,也就是用什么来描述子问题。例如在背包问题中,状态可以定义为 dp[i][j] ,表示前 i 个物品放入容量为 j 的背包中所能获得的最大价值。

分析状态转移

考虑状态之间的转移关系,即如何从已知的状态推导出未知的状态。这需要分析问题的规则和约束条件。比如在过河卒问题中,由于卒只能向右或向下走,所以到达 (i, j) 点的路径数量等于到达 (i - 1, j) 点和 (i, j - 1) 点的路径数量之和,得到状态转移方程 f[i][j] = f[i - 1][j] + f[i][j - 1] 。

边界条件

确定状态转移的边界情况,也就是最简单的子问题的解。例如在过河卒问题中,起点的路径数量为 1,即 f[2][2] = 1 ;在斐波那契数列问题中,F(0) = 0 ,F(1) = 1 。

111111111111111111111

下面回到本题

显然,状态转移方程:

f[i][j] = min(f[i][j - 1], f[i - 1][j - 1] + D[i] * C[j])

  • f[i][j - 1] 的含义:它表示在第 j - 1 天已经到达了第 i 号城市,而在第 j 天选择休息,不进行移动。由于休息不会产生额外的疲劳度,所以在第 j 天到达第 i 号城市的最少疲劳度就等于第 j - 1 天到达该城市的最少疲劳度。
  • f[i - 1][j - 1] + D[i] * C[j] 的含义f[i - 1][j - 1] 表示在第 j - 1 天到达了第 i - 1 号城市的最少疲劳度。D[i] 是第 i - 1 号城市到第 i 号城市的距离,C[j] 是第 j 天的气候恶劣值,那么 D[i] * C[j] 就是在第 j 天从第 i - 1 号城市移动到第 i 号城市所产生的疲劳度。所以 f[i - 1][j - 1] + D[i] * C[j] 表示在第 j 天从第 i - 1 号城市移动到第 i 号城市的总疲劳度。

#include 
using namespace std;

int n, m, d[1010], c[1010], f[1010][1010];
const int INF=2139063143;

int main(){
    memset(f,0x7f,sizeof(f));
    cin >> n>> m;
    for (int i=1; i<=n; i++){
        cin >> d[i];
    }
    for (int i=1; i<=m; i++){
        cin >> c[i];
    }
    for (int i=0;i<=m;i++) f[0][i]=0;  //第i天在第0城市
    for (int i=1; i<=n; i++){
        for (int j=i; j<=m-(n-i); j++){
            f[i][j] = min (f[i][j-1], f[i-1][j-1]+d[i]*c[j]);
        }
    }
    int ans=f[n][n];
    for (int i=n; i<=m; i++){
        if (f[n][i]

你可能感兴趣的:(动态规划,算法)