P1434 [SHOI2002] 滑雪-->y总视频讲解
目录
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 ]值不变。
📍记忆化搜索的本质及其步骤
记忆化搜索的本质:递归的计算每一个点的最优解,即一次递归函数计算一个点。
记忆化搜索步骤:
- 递归当前点(i , j ),如果当前的点已被递归调用过则直接返回,否则继续计算当前点的最优解。
- 初始化当前点的dp值。
- 根据状态转移方程计算当前点的最优解并返回。
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) 成为一种高效的解题方法,其核心优势在于 通过递归自然处理复杂转移逻辑,并利用缓存避免重复计算。以下从原理、适用场景、优势三方面详细解析:
一、状态转移复杂的典型场景
复杂的状态转移通常具有以下特点
转移方向不固定
- 例如在网格问题中,每个状态(如坐标 \((i,j)\))可能向上下左右、甚至任意方向转移(如「最长递增路径」问题,每个点可向四周值更小的点转移)。
- 传统动态规划(DP)需要预先定义转移顺序(如按行 / 列遍历),但复杂转移方向可能导致无法用简单顺序覆盖所有依赖关系。
状态依赖无明确顺序
- 某些问题中,状态 A 的最优解可能依赖状态 B,而状态 B 的最优解又可能反过来依赖状态 A(但实际问题中通常保证无环,如 DAG)。此时传统 DP 的「递推顺序」难以确定,而递归天然支持逆向推导。
二、记忆化搜索的核心原理
记忆化搜索本质是 递归 + 缓存(Cache) 的结合,其工作流程如下:
递归定义状态转移
- 以目标状态为起点,递归地向所有可能的前驱状态展开。例如,计算网格中 \((i,j)\) 的最长路径时,递归查询其四周值更小的点的最长路径。
- 关键:无需预先定义转移顺序,递归的调用栈会自动处理依赖关系(子状态先计算,父状态后计算)。
缓存中间结果
- 用数组、哈希表等结构记录已计算过的状态的结果。当同一个状态被多次访问时,直接返回缓存结果,避免重复计算。
- 核心优化:将指数级的递归复杂度降为多项式级(与状态总数相关)。
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。
- 记忆化搜索的步骤:
- 如果 i 点已经计算过最大的食物链的数量,则直接返回dp[ i ];如果 i 点是生产者,则返回dp[ i ]=1;如果 i 点是捕食者,则计算 i 点的最大食物链数量。
- 循环遍历 i 的每一个食物 v , 累加 f( v ) 得出dp[ i ]的总食物链的数量,即dp[ i ]=( dp[ i ] + f ( v ) )%MOD。
- 返回捕食者 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;
}