1. 题目来源
链接:前K个高频元
来源:LeetCode
2. 题目说明
一只青蛙想要过河。 假定河流被等分为 x 个单元格,并且在每一个单元格内都有可能放有一石子(也有可能没有)。 青蛙可以跳上石头,但是不可以跳入水中。
给定石子的位置列表(用单元格序号升序表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一个石子上)。 开始时, 青蛙默认已站在第一个石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格1跳至单元格2)。
如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
请注意:
石子的数量 ≥ 2 且 < 1100;
每一个石子的位置序号都是一个非负整数,且其 < ;
第一个石子的位置永远是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:
示例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);
}
};
来源:CSDN
作者:Y_puyu
链接:https://blog.csdn.net/yl_puyu/article/details/104221298