洛谷P1434 [SHOI2002] 滑雪 (难度:普及/省选- & python实现)

原题传送门((●’◡’●))

[SHOI2002] 滑雪

题目描述

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-17-16-1 2417161(从 24 24 24 开始,在 1 1 1 结束)。当然 25 25 25 24 24 24 23 23 23 … \ldots 3 3 3 2 2 2 1 1 1 更长。事实上,这是最长的一条。

输入格式

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

输出格式

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

样例 #1

样例输入 #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

样例输出 #1

25

提示

对于 100 % 100\% 100% 的数据, 1 ≤ R , C ≤ 100 1\leq R,C\leq 100 1R,C100

题目分析

读完题目我们知道,这是一段关于搜索的题目。大家可能下意识就想到了广度优先搜索 和 深度优先搜索 算法。分析题目我们发现,如果说遍历每一个点都要进行一次BFS 或 DFS,那时间复杂度是非常之恐怖的
┌(。Д。)┐所以我们在搜索过程中需要进行必要的剪枝,这里我们可以使用
<<<<<<<记忆化搜索>>>>>>>>

那么,何为记忆化搜索呢?记忆化搜索动态规划的一种。在《算法导论》中,将所有的动态规划归为了两种:带备忘的自顶向下法 && 自底向上法
来自《算法导论》对动态规划算法的分类
这里提到的 带备忘的自顶向下法(top-down with memoization) 就是我们所介绍的记忆化搜索。那么这里为什么不能用常规的线性的dp呢??

原因在于,在线性dp问题当中,当前的问题的子问题已经在之前被解决掉了。此时我们可以直接用子子问题的结果对子问题进行求解,最终得到最后的最优解的值。但是,在这道题目中,当我们对高度表进行遍历的时候,因为我们不知道我们遍历的每一个点都可以走到哪些位置,所以不能保证子子问题是否已经被求解了,甚至不知道是否还有子子问题。所以不适合使用线性DP。

搜索算法

我们知道,要使用记忆化搜索对高度表进行遍历搜索。那么,接下来的问题就是,选择何种方法对高度表进行搜索,如何实现搜索函数并在函数中正确的更新"备忘录" (dp)

说到搜索算法,我们首先想到的就是 广度优先搜索算法(BFS,Breadth First Search)深度优先搜索算法(DFS, Deepth First Search)。这两种方法如何选择呢?首先,不管是通过哪种算法,都可以计算得到当前点的最长路径。并且在正确计算之后都可以对DP表进行更新,以便后面的问题访问到的时候可以使用结果。

我们仔细剖析这两种算法。BFS使用的是循环进行搜索与计算,而DFS使用的是递归算法进行搜索与计算。我们发现:如果使用BFS对高度表进行搜索,那么最终只能得到当前搜索点的最大的路径长度。而对于DFS而言,他使用的是子节点的递归计算的结果对他进行计算,这就表明:DFS计算过程中不仅会对目标点进行路径的计算,还会把路径上的点的结果都进行计算。这样就可以大大的节约时间的开销,结合备忘录,可以保证每个点只被搜索一次。

记忆化搜索

在确定了使用DFS进行遍历搜索以后,下面的问题就是如何将DFS与备忘录(dp)进行结合。我们下面对DFS的设计进行拆解

首先,我们可以确定DFS的模型。先确定算法的流程:

  1. 初始化备忘录dp二维表,dp[i][j]代表以(i, j)为起点时的最大路径,每个位置的初始值为0
  2. 建立dfs函数,函数形参为起始点
  3. 若dp表有更新,则直接返回dp[i][j]
  4. 在dfs函数内,遍历传入点的每一个合法点,递归调用dfs函数进行子节点的计算,将计算结果+1与dp[i][j]进行比较,使dp[i][j]取得较大值
  5. 针对会出现一种特殊情况:也就是当前节点没有子节点。这也就代表:**当前节点是这段路的终点。**我们直接对当前点位的dp值设置为1(算上自己路径长度只有1)
  6. 返回当前节点的dp值

算法分析

对于每一种递归的函数,我们都需要考虑两个问题:递归条件基线条件
算法图解中对递归条件和基线条件的解释
分析算法我们可以知道:基线条件有两个:1是当前节点已经被计算过 2是对当前节点的计算已经完成
那么他的递归条件就是:当前节点还有未进行计算的子节点

搞清楚这两个条件以后,我们再去考虑计算的细节:为什么和dp值进行对比的时候要+1??
我们要知道,我们在和当前节点进行比较的dfs值是他的子节点的dfs值,就是他的子节点的最长的路。那么,选择这条路时还要加上起点,就是它本身!所以我们在比较时还要再+1,加上他本身的长度

代码实现

r, c = map(int, input().split())
snow = list()

for i in range(r):
    snow.append(list(map(int, input().split())))

dp = [[0] * c for _ in range(r)]
ans = 0

def dfs(pos):
    i, j = pos
    if dp[i][j]:
        return dp[i][j]
    end = True
    for x, y in [(i, j-1), (i, j+1), (i-1, j), (i+1, j)]:
        if 0<=x<=r-1 and 0<=y<=c-1 and snow[x][y] < snow[i][j]:
            end = False
            dp[i][j] = max(dp[i][j], dfs((x,y))+1)
    if end:
        dp[i][j] = 1
    return dp[i][j]

for i in range(r):
    for j in range(c):
        ans = max(ans, dfs((i,j)))

print(ans)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值