leetcode 403青蛙过河

本文讨论了LeetCode 403题——青蛙过河的问题,通过分析和展示不同代码实现(包括超时代码和通过代码),探讨了动态规划和回溯策略在解决该问题上的应用。代码4利用状态压缩优化了空间复杂度,而代码5通过剪枝提高了回溯效率。代码6则是官方的动态规划解决方案,通过记录到达每个石子的前一步骤长来完成状态转移。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

一只青蛙想要过河。 假定河流被等分为 x 个单元格,并且在每一个单元格内都有可能放有一石子(也有可能没有)。 青蛙可以跳上石头,但是不可以跳入水中。

给定石子的位置列表(用单元格序号升序表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一个石子上)。 开始时, 青蛙默认已站在第一个石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格1跳至单元格2)。

如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。

请注意:

石子的数量 ≥ 2 且 < 1100;
每一个石子的位置序号都是一个非负整数,且其 < 231;
第一个石子的位置永远是0。

示例 1:

[0,1,3,5,6,8,12,17]

总共有8个石子。
第一个石子处于序号为0的单元格的位置, 第二个石子处于序号为1的单元格的位置,
第三个石子在序号为3的单元格的位置, 以此定义整个数组…
最后一个石子处于序号为17的单元格的位置。

返回 true。即青蛙可以成功过河,按照如下方案跳跃:
跳1个单位到第2块石子, 然后跳2个单位到第3块石子, 接着
跳2个单位到第4块石子, 然后跳3个单位到第6块石子,
跳4个单位到第7块石子, 最后,跳5个单位到第8个石子(即最后一块石子)。

示例 2:

[0,1,2,3,4,8,9,11]

返回 false。青蛙没有办法过河。
这是因为第5和第6个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/frog-jump

分析

动规的题目,这个题的状态应该就是所处的位置和能跳的距离.按照这个来定.
查找下一个石头有三种找法

  1. 遍历石头,找dis-1<=stones[i]-stones[now]<=dis+1的 O(n)
  2. 通过距离,在stones里找stones[i]=dis||dis+1||dis-1的,这种一般是二分查找 O(log(n))
    bool check(vector& stones,int now,int jump,vector<vector>& memo)
    check的返回值就是对于从now索引的石头开始跳,现在跳的距离是jump的能不能到最后一块石头,memo的值也和check返回值一一对应
  3. 事先将所有的石头都放到一个set里,这样就可以在now,jump的时候查找set里是否有set.count(stones[now]+jump),来确定是否有需要的石头.

jumpsize的大小不会超过1001,因为一共有1000块石头,每一步就算只跳到下一块,那么最多跳1000下,一下最多jumpsize增加1,所以jumpsize最多为1001.就可以在一个stones.size()大小的存储了

这题我做吐了,写了两种都超时,又模仿着官方题解写了两种,又超时.

回溯的基本的思路:

 bool check(vector<int>& stones,int now,int jump,vector<vector<bool>>& memo)
    {
        if(memo[now][jump])return memo[now][jump];
        int tmp=search(now+1,sz-1,stones[now]+jump,stones);
        if(tmp!=-1&&check(stones,tmp,jump,memo)){
            memo[now][jump]=true;
            return true;
        }
        tmp=search(now+1,sz-1,stones[now]+jump-1,stones);
        if(tmp!=-1&&check(stones,tmp,jump-1,memo)){
            memo[now][jump-1]=true;
            return true;
        }
        tmp=search(now+1,sz-1,stones[now]+jump+1,stones);
        if(tmp!=-1&&check(stones,tmp,jump+1,memo)){
            memo[now][jump+1]=true;
            return true;
        }
        memo[now][jump]=now==sz-1;
        return memo[now][jump];

首先看备忘录里有没有这个值,之后尝试三种步子大小.如果有一种成功了,就return true走掉了.三种都没有成功的就说明它没有下一步了,检查它是否是最后一块石头(这个也可以在开头检查)

超时代码

代码1

class Solution {
public:
    bool res=false;
    int sz;
    int find(int left,int target,vector<int>&stones)
    {
        int right=sz-1;
        while(left<=right)
        {
            int mid=left+(right-left)/2;
            if(stones[mid]==target) return mid;
            else if(stones[mid]>target) right=mid-1;
            else left=mid+1;
        }
        return -1;
    }
    void backtrace(int now,int step,vector<int>& stones)
    {
        //找到下一个石头
        if(res) return;
        int tmp=find(now+1,stones[now]+step,stones);
        if(tmp==-1) return;
        if(tmp==sz-1) {res=true; return;}
        backtrace(tmp,step+1,stones);
        backtrace(tmp,step,stones);
        if(step-1>0)
            backtrace(tmp,step-1,stones);
    }
    bool canCross(vector<int>& stones) {
        sz=stones.size();;
        backtrace(0,1,stones);
        return res;
    }
};

这个写的就是单纯的回溯(过了16个),这个超时我认的
由于dp就是带备忘的回溯,所以我就加了个数组记录

代码2

class Solution {
public:
    int sz;
    bool check(vector<int>& stones,int now,int jump,vector<vector<bool>>& memo)
    {
        if(memo[now][jump])return memo[now][jump];
        for(int i=now+1;i<sz;i++)
        {
            int dis=stones[i]-stones[now];
            if(dis>=jump-1&&dis<=jump+1)
            {
                if(check(stones,i,dis,memo))
                {
                    memo[now][dis]=true;
                    return true;
                }
            }
        }
        memo[now][jump]=now==sz-1;
        return memo[now][jump];
    }
    bool canCross(vector<int>& stones) {
        sz=stones.size();
        vector<vector<bool>> memo(sz,vector<bool>(sz,false));
        return check(stones,0,0,memo);   
    }
};

这个跟官方的题解写的差不多啊.为啥还是超时只过16个
嗯,我不在数组里遍历了,再优化下,改成二分查找,这样就是找三个指定的值了

代码3

class Solution {
public:
    int sz;
    int search(int left,int right,int tar,vector<int> &stones)
    {
        while(left<=right)
        {
            int mid=left+(right-left)/2;
            if(stones[mid]==tar) return mid;
            else if (stones[mid]<tar) left=mid+1;
            else right=mid-1;
        }
        return -1;
    }
    bool check(vector<int>& stones,int now,int jump,vector<vector<bool>>& memo)
    {
        if(memo[now][jump])return memo[now][jump];
        int tmp=search(now+1,sz-1,stones[now]+jump,stones);
        if(tmp!=-1&&check(stones,tmp,jump,memo)){
            memo[now][jump]=true;
            return true;
        }
        tmp=search(now+1,sz-1,stones[now]+jump-1,stones);
        if(tmp!=-1&&check(stones,tmp,jump-1,memo)){
            memo[now][jump-1]=true;
            return true;
        }
        tmp=search(now+1,sz-1,stones[now]+jump+1,stones);
        if(tmp!=-1&&check(stones,tmp,jump+1,memo)){
            memo[now][jump+1]=true;
            return true;
        }
        memo[now][jump]=now==sz-1;
        return memo[now][jump];
    }
    bool canCross(vector<int>& stones) {
        sz=stones.size();
        vector<vector<bool>> memo(sz+1,vector<bool>(sz+1,false));
        return check(stones,0,0,memo);   
    }
};

做到这我快哭了,咋还是超时只过16个.这我改的跟没优化的一样…

通过代码

下面每一个通过对代码都有一些额外的小技巧,我是想不到,也是参考别人的

代码4

class Solution {
public:
    int sz;
    unordered_map<int,bool> memo;
    bool check(vector<int>& stones,int now,int jump)
    {
        int key = now | jump << 11;
        if(now>=sz-1) return true;
        if(memo.count(key)) return memo[key];
        for(int i=now+1;i<sz;i++)
        {
            int dis=stones[i]-stones[now];
            if(dis>=jump-1&&dis<=jump+1)
            {
                if(check(stones,i,dis))
                {
                    return memo[key]=true;
                }
            }
            else if(dis>jump+1) break;
        }
        return memo[key]=false;
    }
    bool canCross(vector<int>& stones) {
        sz=stones.size();
        return check(stones,0,0);   
    }
};

这个代码是参考大佬的,亮点在于使用了状态的压缩,即将now和jump压缩成一个数,然后存在unordered_map里,在备忘录里提出的速度就比较快,还节省空间(使用map也能过,就是慢点)
菜鸡的疑问,这vector<vector> 就这么慢吗,
map.count(key) ->返回 map中key的个数,因为map中key唯一,所以有就返回1,没有就返回0
map.find(key) ->返回值为key的迭代器,有就返回正常迭代器,没有就返回map.end()

代码5

回溯,剪枝

//12ms,9MB
class Solution {
public:
    bool res=false;
    int sz;
    int find(int left,int target,vector<int>&stones)
    {
        int right=sz-1;
        while(left<=right)
        {
            int mid=left+(right-left)/2;
            if(stones[mid]==target) return mid;
            else if(stones[mid]>target) right=mid-1;
            else left=mid+1;
        }
        return -1;
    }
    void backtrace(int now,int step,vector<int>& stones)
    {
        //找到下一个石头
        if(res) return;
        int tmp=find(now+1,stones[now]+step,stones);
        if(tmp==-1) return;
        if(tmp==sz-1) {res=true; return;}
        backtrace(tmp,step+1,stones);
        backtrace(tmp,step,stones);
        if(step-1>0)
            backtrace(tmp,step-1,stones);
    }
    bool canCross(vector<int>& stones) {
        sz=stones.size();
        for (int i = 1; i < stones.size(); ++i) {
            if (i > 3 && stones[i - 1] * 2 < stones[i]){
                return false;
            }
        }
        backtrace(0,1,stones);
        return res;
    }
};

这个代码就是"代码1"加了剪枝,结果双百.
剪枝

for (int i = 1; i < stones.size(); ++i) {
            if (i > 3 && stones[i - 1] * 2 < stones[i]){
                return false;
            }
        }

这个剪枝就是根据第i块石头的时候最多跳i个单位(这是i及以前的石头能跳的最远距离),所以所以如果下一块石头在2*stones[i]外面,那就跳不到了,只能是false;我是真没想到!

代码6

//412ms,37MB
class Solution {
public:
    int sz;
    unordered_map<int,unordered_set<int>> map;
    bool canCross(vector<int>& stones) {
        sz=stones.size();
        for(int i=0;i<sz;i++)
        {
            map[stones[i]]=unordered_set<int>();

        }
        map[0].insert(0);
        for(int i=0;i<sz;i++)
        {
            for(const int &step:map[stones[i]])
            {
                for(int j=step-1;j<=step+1;++j)
                {
                    if(j>0&&map.count(stones[i]+j))
                    {
                        map[stones[i]+j].insert(j);
                    }
                }
            }
        }
        return map[stones[sz-1]].size()>0;
    }
};

这个算法是官方的最后一种动态规划的解法,具体就是对每一个stones[i]存入map,每个stones[i]对应一个set,里面记录到达stones[i]的上一个步长,在stones[i]进行状态转移的时候,就可以使用set里面的步长每一个的+1&&-1的步长进行下一块石头的尝试,如果有,就将现在的步长记入下一块石头的set里.
初始就是在第0(索引)块的时候步长为0,保证第一步就是只有step=1.
最终结果就是看最后一块石头的set里是否有东西,因为set就是记录到达的上一步长度,有就表示可以到.
tips:
map里套set的构造

map[stones[i]]=unordered_set<int>();

map里套set的set遍历

for(const int &step:map[stones[i]])

const不能少,否则报错

感觉还是最后一种方法是正道,回溯剪枝虽然快,但感觉是数据水,出的都是0,1,2,999这种,正常的剪肯定剪不了多少,剪不了还是要回溯,回溯就太慢了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值