记忆化搜索学习(1)滑雪

动态规划 - 记忆化搜索

记忆化搜索 = 深度优先搜索的实现 + 动态规划的思想

​ 从一个题目入手, 看洛谷P1434滑雪

题目描述
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(1≤R,C≤100)。下面是R行,每行有C个数,代表高度(两个数字之间用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

​ 看到这个题目,第一想法就是 暴力搜索,于是写下如下代码

#include <iostream>
using namespace std;

int array[1001][1001];
int row, column, ans=0;

int dir[][2] = {
    {-1, 0}, // 上
    {1, 0},  // 下
    {0, -1}, // 左
    {0, 1}   // 右
};

int max(const int& a, const int& b)
{
    return ((a>b)? a:b);
}
 
int dfs(const int& x, const int& y)
{
    
}
int main()
{
    cin >> row >> column;
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
        {
            cin >> array[i][j];
        }
    }
    
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
            ans = max(ans, dfs(i, j));
    }
    
    cout << ans;
    
    return 0;
}

​ 程序主要流程写好了,接下来写dfs

int dfs(const int& x, const int& y)
{
    int result = 1;
	for(int i=0; i<4; i++)
    {
        int tx = x+dir[i][0];
        int ty = y+dir[i][1];
        
        result = max(result, dfs(tx, ty)+1);
    }
    
    return result;
}

dfs的大致流程写出来了,但是还要加上一些东西,我们要保证输入参数的合法化。

bool check(const int& x, const int& y)
{
    return (x>=0 && x<row && y>=0 && y<column);
}

​ 输入的参数合法以后,我们就要加上判断了,因为我们不是所有点都要的,我们只想经过比我们现在的点小的点,所以我们判断值可不可以用。所以写出新的dfs如下

int dfs(const int& x, const int& y)
{
    int result = 1;
	for(int i=0; i<4; i++)
    {
        int tx = x+dir[i][0];
        int ty = y+dir[i][1];
        if(check(tx, ty) && array[x][y] > array[tx][ty])
        	result = max(result, dfs(tx, ty)+1);
    }
    
    return result;
}

​ dfs 写好以后新程序如下

#include <iostream>
using namespace std;

int array[1001][1001];
int row, column, ans=0;

int dir[][2] = {
    {-1, 0}, // 上
    {1, 0},  // 下
    {0, -1}, // 左
    {0, 1}   // 右
};

// 判断参数的合法性
bool check(const int& x, const int& y)
{
    return (x>=0 && x<row && y>=0 && y<column);
}

int max(const int& a, const int& b)
{
    return ((a>b)? a:b);
}
   

int dfs(const int& x, const int& y)
{
    int result = 1;
	for(int i=0; i<4; i++)
    {
        int tx = x+dir[i][0];
        int ty = y+dir[i][1];
        if(check(tx, ty) && array[x][y] > array[tx][ty])
        	result = max(result, dfs(tx, ty)+1);
    }
    
    return result;
}

int main()
{
    cin >> row >> column;
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
        {
            cin >> array[i][j];
        }
    }
    
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
            ans = max(ans, dfs(i, j));
    }
    
    cout << ans;
    
    return 0;
}

​ 以上用dfs,就把这个程序写好了。但是这个效率很慢,为什么这么说呢,

在这里插入图片描述

​ 看这个图,我们对这个数组执行我们的程序。如果从 9 出发,按照我们的程序我们会9的上下左右各走一遍,如果走左边会走到8 对 8来说,从8开始的dfs中一定会有一条经过 6 - 5 - 4 - 3 - 2 - 1。 我们继续看 9 ,6在9的下边,9也会试着从下边走, 那么对于下边的 6 来说,从6开始的dfs中也一定会有一条经过 6 - 5 - 4 - 3 - 2 - 1。看到这里,你是不是发现问题了,我们重复走了很多路。试想一下如果数据量很大,那么我们这样的dfs 又会重复多少次,效率也就会很低。

​ 那么有没有解决办法,我们想一下,我们需要的是最长路径,那么假设我们从任一点出发,最长路径是不是该点上下左右四个方向的点中,可以走最长的点走的路径加1。 比如,9这个点,它的最长路径,是它附近 8, 6, 4, 2 分别可到达最远距离最大的距离再加上1。假设 8的最远路径通过的点为 f(8) ,6为f(6)。 那么f(8) = 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 。 f(6) = 6 - 5 - 4 - 3 - 2 - 1。那么f(8) = 8 - 7 - f(6)。 所以我们上面的问题就是计算f(8)时,计算了 f(6)。但是后面遇到从 6 开始的点后,又计算了 f(6)。

​ 问题清楚后,我们想一下有没有办法 对 每个位置 所能达到的最远路径,都只计算一次,问题就解决了。我们可以考虑将 f(i)计算出来的值存储到哈希数组h(i) 中,当第二次要访问f(i) 时,直接取 h(i)的值即可,这样每次计算 f(i)的时间复杂度变成了O(1),总共需要计算 ,总的时间复杂度就变成了O(n) 。

​ 那么这种用哈希数组来记录运算结果,避免重复运算的方法就是记忆化搜索。

​ 解决办法想好后,我们来想如何实现,我们是一个二维数组,所以我们的哈希数组也要是二维数组。那么最开始数组一定是没有值的,所以我们最开始给数组每个点都赋值 - 1, 之后判断,如果不是-1,表示我们之前计算了,直接返回该点的值即可。反之则计算。那么最终代码如下:

#include <iostream>
using namespace std;

int array[1001][1001];
int h[1001][1001];	// hash数组
int row, column, ans=0;

int dir[][2] = {
    {-1, 0}, // 上
    {1, 0},  // 下
    {0, -1}, // 左
    {0, 1}   // 右
};

// 判断参数的合法性
bool check(const int& x, const int& y)
{
    return (x>=0 && x<row && y>=0 && y<column);
}

int max(const int& a, const int& b)
{
    return ((a>b)? a:b);
}

int dfs(const int& x, const int& y)
{
    int& result = h[x][y];// 记忆化剪枝 ,若该点最远路径已经计算过,直接返回。
    if(result != -1)
        return result;
    result = 1;
	for(int i=0; i<4; i++)
    {
        int tx = x+dir[i][0];
        int ty = y+dir[i][1];
        if(check(tx, ty) && array[x][y] > array[tx][ty])
        {
            result = max(result, dfs(tx, ty)+1);
        }
        	
    }
    
    return result;
}
int main()
{
    cin >> row >> column;
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
        {
            cin >> array[i][j];
            h[i][j] = -1;
        }
    }
    
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<column; j++)
            ans = max(ans, dfs(i, j));
    }
    
    cout << ans;
    
    return 0;
}

到此,这个题目就结束了。最后我们总结一下,记忆化搜索的步骤,这个总结是我看大佬写的夜深人静写算法系列看到的。这里分享给大家。

总共可以归纳为以下四步:
  1)合法性剪枝
  2)偏序关系剪枝
  3)记忆化剪枝
  4)递归计算结果并返回

1)合法性剪枝
  • 因为在递归计算的时候,我们必须保证传入参数的合法性,所以这一步是必要的,比如坐标为负数之类的判断;
2)偏序关系剪枝
  • 偏序关系其实就是代表了状态转移的方向,例如【例题2】中,只允许值大的往值小的方向走,这就是一种偏序关系;如果不满足偏序关系的就不能继续往下搜索了;
3)记忆化剪枝
  • 记忆化剪枝就是去对应的哈希数组判断这个状态是否曾经已经计算过,如果计算过则直接返回,时间复杂度 ;
4)递归计算结果并返回
  • 这一步就是深度优先搜索的递归过程,也是递归子问题取最优解的过程,需要具体问题具体分析;
5、记忆化搜索的优点
1、忽略边界判断
  • 状态转移的时候完全不需要进行边界判断,只需要在递归调用的出口进行统一判断即可,这样使得代码更加简洁清晰;
2、编码方便
  • 相比动态规划而言,不用去关心子问题的枚举顺序问题,也不用管数组下标越界问题,只要按照深度优先搜索的思路,把代码框架写好,再加入记忆化部分的代码即可,实现方便,手到擒来;
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值