[每日一题] 128. 青蛙过河(数组、记忆化搜索、递归、剪枝)

文章目录

    • 1. 题目来源
    • 2. 题目说明
    • 3. 题目解析
          • 方法一:哈希表、记忆化搜索、递归解法
          • 方法二:迭代解法
          • 方法三:回溯法+贪心策略+剪枝

1. 题目来源

链接:青蛙过河
来源:LeetCode

2. 题目说明

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

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

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

请注意:

石子的数量 ≥ 2 且 < 1100;
每一个石子的位置序号都是一个非负整数,且其 < 2 31 2^{31} 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个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。

3. 题目解析

方法一:哈希表、记忆化搜索、递归解法

首先要理解青蛙跳到某个石头上可能有多种跳法,由于这道题只是让判断青蛙是否能跳到最后一个石头上,并没有让我们返回所有的路径,这样就降低了一些难度。下面为递归做法思路:

  • 使用记忆化搜索,维护一个哈希表,建立青蛙在 pos 位置和拥有 jump 跳跃能力时是否能跳到对岸
  • 为了能用一个变量同时表示 pos 和 jump,可以将 jump 左移很多位并或上 pos,由于题目中对于位置大小有限制,所以不会产生冲突
  • 首先判断 pos 是否已经到最后一个石头了,是的话直接返回 true
  • 然后查看当前这种情况是否已经出现在哈希表中,是的话直接从哈希表中取结果
  • 如果没有,就遍历余下的所有石头,对于遍历到的石头,计算到当前石头的距离 dist
  • 如果 dist 小于 jump - 1,接着遍历下一块石头
  • 如果 dist 大于 jump + 1,说明无法跳到下一块石头,m[key] 赋值为 false,并返回 false
  • 如果在青蛙能跳到的范围中,调用递归函数,以新位置 i 为 pos,距离 dist 为 jump,如果返回 true 了,即给 m[key] 赋值为 true,并返回 true
  • 如果结束遍历给 m[key] 赋值为 false,并返回 false

参见代码如下:

// 执行用时 :40 ms, 在所有 C++ 提交中击败了94.12%的用户
// 内存消耗 :13.9 MB, 在所有 C++ 提交中击败了81.25%的用户

class Solution {
public:
    bool canCross(vector<int>& stones) {
        unordered_map<int, bool> m;
        return help(stones, 0, 0, m);
    }

    bool help(vector<int>& stones, int pos, int jump, unordered_map<int, bool>& m) {
        int n = stones.size(), key = pos | jump << 11;
        if (pos >= n - 1) return true;
        if (m.count(key)) return m[key];
        for (int i = pos + 1; i < n; ++i) {
            int dist = stones[i] - stones[pos];
            if (dist < jump - 1) continue;
            if (dist > jump + 1) return m[key] = false;
            if (help(stones, i, dist, m)) return m[key] = true;
        }
        return m[key] = false;
    }
};
方法二:迭代解法

也可以用迭代的方法来解,思路如下:

  • 用一个哈希表来建立每个石头和在该位置上能跳的距离之间的映射
  • 建立一个一维 dp 数组,其中 dp[i] 表示在位置为 i 的石头青蛙的弹跳力(只有青蛙能跳到该石头上,dp[i]才大于0)
  • 由于题目中规定了第一个石头上青蛙跳的距离必须是 1,为了跟后面的统一,对青蛙在第一块石头上的弹跳力初始化为 0 (虽然为 0,但是由于题目上说青蛙最远能到其弹跳力 +1 的距离,所以仍然可以到达第二块石头)。
  • 用变量 k 表示当前石头,然后开始遍历剩余的石头
  • 对于遍历到的石头 i,来找到刚好能跳到 i 上的石头 k,如果 i 和 k 的距离大于青蛙在 k 上的弹跳力 +1,则说明青蛙在 k 上到不了 i,则 k 自增 1
  • 从 k 遍历到 i,如果青蛙能从中间某个石头上跳到 i 上,则更新石头 i 上的弹跳力和最大弹跳力
  • 这样当循环完成后,只要检查最后一个石头上青蛙的最大弹跳力是否大于0即可

参见代码如下:

// 执行用时 :140 ms, 在所有 C++ 提交中击败了66.29%的用户
// 内存消耗 :35.5 MB, 在所有 C++ 提交中击败了36.81%的用户

class Solution {
public:
    bool canCross(vector<int>& stones) {
        unordered_map<int, unordered_set<int>> m;
        vector<int> dp(stones.size(), 0);
        m[0].insert(0);
        int k = 0;
        for (int i = 1; i < stones.size(); ++i) {
            while (dp[k] + 1 < stones[i] - stones[k]) ++k;
            for (int j = k; j < i; ++j) {
                int t = stones[i] - stones[j];
                if (m[j].count(t - 1) || m[j].count(t) || m[j].count(t + 1)) {
                    m[i].insert(t);
                    dp[i] = max(dp[i], t);
                }
            }
        }
        return dp.back() > 0;
    }
};
方法三:回溯法+贪心策略+剪枝

和上述几种方法大致思路一样:由于当前可跳的步长取决于上一次调到次石头上的步长,所以将上一次可达此石头的步长保存,然后根据上一次的到达此石头的步长集合选择当前可跳的步长。

// 执行用时 :616 ms, 在所有 C++ 提交中击败了15.61%的用户
// 内存消耗 :38.9 MB, 在所有 C++ 提交中击败了14.94%的用户
class Solution {
public:
	bool canCross(vector<int>& stones) {
		//第一步只能跳一个不长
		if (stones[1] != stones[0] + 1) {
			return false;
		}
		int endStone = *(--stones.end());//尾端石头
		set<int> stonesSet(++stones.begin(), stones.end());//将vector容器转换为set容器
		map<int, set<int> > myMap;//myMap[i]标记i可跳的不长
		myMap[*stonesSet.begin()].insert(1);//初始只能跳一步到第二个位置
        //顺序访问所有石头
		for (auto stone : stonesSet) {
            //根据上一次到达此地的步长集合,计算下一步可跳的步长
			for (auto nextStone : myMap[stone]) {
                //步长nextStone - 1
				if (nextStone > 1 && stonesSet.find(stone + nextStone - 1) != stonesSet.end()) {
                    //如果跳nextStone - 1后到达的石头在stonesSet中,说明stone + nextStone - 1这块石头可由不长nextStone - 1到达
					myMap[stone + nextStone - 1].insert(nextStone - 1);
				}
                //步长nextStone
				if (stonesSet.find(stone + nextStone) != stonesSet.end()) {
                    //如果跳nextStone 后到达的石头在stonesSet中,说明stone + nextStone这块石头可由不长nextStone到达
					myMap[stone + nextStone].insert(nextStone);
				}
                //步长nextStone + 1
				if (stonesSet.find(stone + nextStone + 1) != stonesSet.end()) {
                    //如果跳nextStone + 1后到达的石头在stonesSet中,说明stone + nextStone + 1这块石头可由不长nextStone + 1到达
					myMap[stone + nextStone + 1].insert(nextStone + 1);
				}
                //如果已经达到了endStone
				if (myMap.count(endStone) > 0) {
					return true;
				}
			}
		}
		return false;
	}
};

剪枝:

// 执行用时 :24 ms, 在所有 C++ 提交中击败了96.83%的用户
// 内存消耗 :12.3 MB, 在所有 C++ 提交中击败了86.81%的用户
class Solution {
private:
	set<int> stonesSet;
public:
    //从nowStone开始搜索能否到达last,beforeStep为到达nowStone的步长
	bool jump(int nowStone, int beforeStep, int last) {
        //如果已经达到了终点
		if ((nowStone + beforeStep + 1) == last || (nowStone + beforeStep) == last || (nowStone + beforeStep - 1) == last){
            return true;
        }
        //采取贪心策略,每次都先选择beforeStep + 1步长
		if (stonesSet.find(nowStone + beforeStep + 1) != stonesSet.end() && jump(nowStone + beforeStep + 1, beforeStep + 1, last)) {
			return true;
		}
        //再beforeStep步长
		if (stonesSet.find(nowStone + beforeStep) != stonesSet.end()&& jump(nowStone + beforeStep, beforeStep, last)) {
			return true;
		}
        //最后beforeStep - 1步长
		if (beforeStep > 1 && stonesSet.find(nowStone + beforeStep - 1) != stonesSet.end()&& jump(nowStone + beforeStep - 1, beforeStep - 1, last)) {
			return true;
		}
		return false;
	}
	bool canCross(vector<int>& stones) {
		if (stones[1] != 1) return false;
		int last = stones.back();
		for (int i = 1; i < stones.size(); ++i) {
			if (i > 3 && stones[i - 1] * 2 < stones[i]){//剪枝算法,stones[i - 1]最多是有步长stones[i - 1]到达
                //stones[i - 1] * 2 < stones[i],说明无论如何stones[i]都不可达
                return false;
            }
			stonesSet.insert(stones[i]);
		}
		return jump(1, 1, last);
	}
};

你可能感兴趣的:(每日一题,算法编程题,数组,记忆化搜索,递归,剪枝)