动态规划--每日一练(记忆化搜索)

P1434 [SHOI2002] 滑雪-->y总视频讲解

目录

P1434 [SHOI2002] 滑雪-->y总视频讲解

1.题目描述

2.解题思路

📍状态表示:

📍状态计算: 

📍记忆化搜索的本质及其步骤

3.代码展示

4.什么时候适合使用记忆化搜索 ?

P4017 最大食物链计数

1.题目描述

2.解题思路

📍解题方向

📍核心思路

3.代码展示 

1.题目描述

Michael 喜欢滑雪。这并不奇怪,因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael 想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:

1   2   3   4   5
16  17  18  19  6
15  24  25  20  7
14  23  22  21  8
13  12  11  10  9

一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度会减小。在上面的例子中,一条可行的滑坡为 24−17−16−1(从 24 开始,在 1 结束)。当然 25-24-23-…-3-2-1 更长。事实上,这是最长的一条。

输入格式

输入的第一行为表示区域的二维数组的行数 R 和列数 C。下面是 R 行,每行有 C 个数,代表高度(两个数字之间用 1 个空格间隔)。

输出格式

输出区域中最长滑坡的长度。

输入输出样例

输入 

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出

25

说明/提示

对于 100% 的数据,1≤R,C≤100。

2.解题思路

本题考点:求矩阵中的最长递增/减路径。

核心思想:适用记忆化搜索,记忆化搜索的好处在于代码较为简洁,思路较为清晰。

📍状态表示:

dp[ i ][ j ]表示从( i , j )点出发的最大路径 。

📍状态计算: 

当前点的状态由上下左右四个状态推导而来。

如果上下左右的点的值 < 当前点的值,则dp[ i ][ j ]=max(dp[ i ][ j ] , dp[ i +/- 1][ j +/- 1 ]  + 1 ).

如果上下左右的点的值 >= 当前点的值,则dp[ i ][ j ]值不变。

📍记忆化搜索的本质及其步骤

记忆化搜索的本质:递归的计算每一个点的最优解,即一次递归函数计算一个点。

记忆化搜索步骤:

  1. 递归当前点(i , j ),如果当前的点已被递归调用过则直接返回,否则继续计算当前点的最优解。
  2. 初始化当前点的dp值。
  3. 根据状态转移方程计算当前点的最优解并返回。

3.代码展示

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#define int long long
using namespace std;

const int N = 305;
int r, c;
int grid[N][N], dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
int dp[N][N];

int f(int x, int y) {   //递归函数:用来计算从当前点出发的最长路径

    if (dp[x][y] != -1) return dp[x][y];  //1.如果当前点已存在最优解,则返回。

    dp[x][y] = 1;//2.初始化当前点的dp数组。
    
    //3.状态转移计算
    for (int i = 0; i < 4; i++) {

        int nx = x + dx[i], ny = y + dy[i];
        if (nx >= 1 && nx <= r && ny >= 1 && ny <= c && grid[nx][ny] < grid[x][y]) {
            dp[x][y] = max(dp[x][y], f(nx, ny) + 1);  //⚠️⚠️⚠️注意错误写法:dp[x][y] = max(dp[x][y], dp[nx][ny] + 1);
            //修正后的代码通过递归调用 f(nx, ny) 确保在使用 (nx, ny) 的路径长度前,该值已经被正确计算并存储在 dp 中。
        }
    } 
    return dp[x][y];  //3.返回最优解
}
signed main() {

    cin >> r >> c;
    for (int i = 1; i <= r; i++) {
        for (int j = 1; j <= c; j++) {
            cin >> grid[i][j];
        }
    }
    int res = -1e18;
    fill(dp[0], dp[0] + N * N, -1);//初始化dp数组为-1,用于判断某个点是否已存在最优解📍
    for (int i = 1; i <= r; i++) {
        for (int j = 1; j <= c; j++) {
            res = max(res, f(i, j));   //f(i,j)表示用递归函数计算从当前点出发的最长路径。
        }
    }
    cout << res << endl;
    return 0;
    
}

4.什么时候适合使用记忆化搜索 ?

当状态转移方向复杂(如网格中任意方向移动(滑雪)、状态依赖无固定顺序(食物链)等)时,记忆化搜索(Memoization Search) 成为一种高效的解题方法,其核心优势在于 通过递归自然处理复杂转移逻辑,并利用缓存避免重复计算。以下从原理、适用场景、优势三方面详细解析:

一、状态转移复杂的典型场景

复杂的状态转移通常具有以下特点

  1. 转移方向不固定

    • 例如在网格问题中,每个状态(如坐标 \((i,j)\))可能向上下左右、甚至任意方向转移(如「最长递增路径」问题,每个点可向四周值更小的点转移)。
    • 传统动态规划(DP)需要预先定义转移顺序(如按行 / 列遍历),但复杂转移方向可能导致无法用简单顺序覆盖所有依赖关系。
  2. 状态依赖无明确顺序

    • 某些问题中,状态 A 的最优解可能依赖状态 B,而状态 B 的最优解又可能反过来依赖状态 A(但实际问题中通常保证无环,如 DAG)。此时传统 DP 的「递推顺序」难以确定,而递归天然支持逆向推导。

二、记忆化搜索的核心原理

记忆化搜索本质是 递归 + 缓存(Cache) 的结合,其工作流程如下:

 
  1. 递归定义状态转移

    • 以目标状态为起点,递归地向所有可能的前驱状态展开。例如,计算网格中 \((i,j)\) 的最长路径时,递归查询其四周值更小的点的最长路径。
    • 关键:无需预先定义转移顺序,递归的调用栈会自动处理依赖关系(子状态先计算,父状态后计算)。
  2. 缓存中间结果

    • 用数组、哈希表等结构记录已计算过的状态的结果。当同一个状态被多次访问时,直接返回缓存结果,避免重复计算。
    • 核心优化:将指数级的递归复杂度降为多项式级(与状态总数相关)。

P4017 最大食物链计数

1.题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 1 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 80112002 的结果。

输入格式

第一行,两个正整数 n、m,表示生物种类 n 和吃与被吃的关系数 m。

接下来 m 行,每行两个正整数,表示被吃的生物A和吃A的生物B。

输出格式

一行一个整数,为最大食物链数量模上 80112002 的结果。

输入输出样例

输入 

5 7
1 2
1 3
2 3
3 5
2 5
4 5
3 4

输出 

5

说明/提示

各测试点满足以下约定:

【补充说明】

数据中不会出现环,满足生物学的要求。(感谢 @AKEE )

2.解题思路

📍解题方向

本题的状态转移复杂,是一种状态依赖无固定顺序的关系,所以用记忆化搜索解决。

📍核心思路

  • 状态表示:dp[ i ]表示以i为食物链顶端的最大的食物链数量。
  • 状态计算:当 i 为捕食者时,循环遍历 i 的每一个食物 v,累加 f( v ) 得出dp[ i ]的总食物链的数量,即dp[ i ]=( dp[ i ] + f ( v ) )%MOD。
  • 记忆化搜索的步骤:
  1. 如果 i 点已经计算过最大的食物链的数量,则直接返回dp[ i ];如果 i 点是生产者,则返回dp[ i ]=1;如果 i 点是捕食者,则计算 i 点的最大食物链数量。
  2. 循环遍历 i 的每一个食物 v , 累加 f( v ) 得出dp[ i ]的总食物链的数量,即dp[ i ]=( dp[ i ] + f ( v ) )%MOD。
  3. 返回捕食者 i 的最大食物链数量。

3.代码展示 

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#define int long long
using namespace std;

const int N = 5e5 + 10, MOD = 80112002;
int n, m;
int in[N], out[N];  // 记录每个节点的入度和出度
vector<int> eat[N]; // eat[i]存储i吃的生物(即i的猎物)
int dp[N];          // dp[i]表示以i为起点的最大食物链数量

// 记忆化搜索函数
int dfs(int u) {
    if (dp[u] != -1) return dp[u];  // 已计算过,直接返回

    // 如果是生产者(没有猎物),食物链数量为1
    if (eat[u].size() == 0) {
        return dp[u] = 1;
    }

    // 否则,递归计算所有猎物的食物链数量之和
    dp[u] = 0;
    for (int v : eat[u]) {
        dp[u] = (dp[u] + dfs(v)) % MOD;
    }
    return dp[u];
}

signed main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int a, b;
        cin >> a >> b;  // a被b吃,即b吃a
        eat[b].push_back(a);  // b的猎物包括a
        in[a]++;   // a的入度+1(被b吃)
        out[b]++;  //b的出度+1(吃a)
    }

    // 初始化dp数组为-1,表示未计算
    fill(dp + 1, dp + n + 1, -1);

    // 遍历所有顶级消费者(出度为0的节点),累加食物链数量
    int res = 0;
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0) { // 顶级消费者是入度为0的节点
            res = (res + dfs(i)) % MOD;
        }
    }

    cout << res << endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值