[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 24−17−16−1(从 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 1≤R,C≤100。
题目分析
读完题目我们知道,这是一段关于搜索的题目。大家可能下意识就想到了广度优先搜索 和 深度优先搜索 算法。分析题目我们发现,如果说遍历每一个点都要进行一次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的模型。先确定算法的流程:
- 初始化备忘录dp二维表,dp[i][j]代表以(i, j)为起点时的最大路径,每个位置的初始值为0
- 建立dfs函数,函数形参为起始点
- 若dp表有更新,则直接返回dp[i][j]
- 在dfs函数内,遍历传入点的每一个合法点,递归调用dfs函数进行子节点的计算,将计算结果+1与dp[i][j]进行比较,使dp[i][j]取得较大值
- 针对会出现一种特殊情况:也就是当前节点没有子节点。这也就代表:**当前节点是这段路的终点。**我们直接对当前点位的dp值设置为1(算上自己路径长度只有1)
- 返回当前节点的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)