LeetCode/青蛙过河
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表stones(用单元格序号升序表示,请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时,青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格1跳至单元格2)。
如果青蛙上一步跳跃了k个单位,那么它接下来的跳跃距离只能选择为k-1、k 或 k+1个单位。另请注意,青蛙只能向前方(终点的方向)跳跃。
1. 回溯法(超时)
遍历每个位置能跳到的下一位置,对满足条件的位置进行递归回溯
回溯法(超时)
class Solution {
public:
bool flag=false;
int n;
bool canCross(vector<int>& stones) {
n= stones.size();
trackback(stones,0,0);
return flag;
}
void trackback(vector<int>& stones,int pos,int lastjump){
if(pos==n-1) flag=true;
int maxjump = lastjump+1;
int distance;
for(int nextpos=pos+1;nextpos<n;nextpos++){
distance = stones[nextpos]-stones[pos];
if(distance>maxjump) return;
if(abs(distance-lastjump)<=1) trackback(stones,nextpos,distance);
}
}
};
上面算法中,还需要花费大量的时间从当前位置往后遍历去寻找下一块合适石头所在的数组位置
用哈希表存储石头实际位置,每次只用对能跳的三个位置进行判断是否是石头实际位置即可
一维哈希优化(超时)
class Solution {
public:
bool flag=false;
unordered_set<int> set_;
int n;
bool canCross(vector<int>& stones) {
n= stones.size();
for (auto i : stones)
set_.insert(i);
trackback(stones,0,0);
return flag;
}
void trackback(vector<int>& stones,int pos,int lastjump){
if(pos==stones.back()) flag=true;
if(flag==true) return;
for(int i=lastjump+1;i>=lastjump-1;i--){
if(i==0) break;
if(set_.count(pos+i)){
trackback(stones,pos+i,i);
}
}
}
};
2. 记忆化搜索?(回溯法+备忘录)
哪怕使用了上述优化,但递归中的重复还是没有避免,所以我们将递归过程的所有状态存储起来,
避免重复计算,减少递归,用数组下标表示位置,该位置的哈希表,表示该位置下某个跳跃距离是否最终能达到终点
相比二维数组更灵活和节省空间vector<unordered_map<int, bool>> rec
官方写法(反向递归更新备忘录)
class Solution {
public:
vector<unordered_map<int, int>> rec;
bool dfs(vector<int>& stones, int i, int lastDis) {
if(i==stones.size()-1) return true; //可达返回真
//下面一句为动态规划的精髓
if (rec[i].count(lastDis)) return rec[i][lastDis];//已经记录过的状态返回对应值,减少递归运算
for (int curDis = lastDis - 1; curDis <= lastDis + 1; curDis++) {//每次可选跳跃距离
if (curDis > 0) {//只能前进
//查询下一合适位置的对应数组下标
int j = lower_bound(stones.begin(), stones.end(), curDis + stones[i]) - stones.begin();
//判断该数是否满足要求,同时继续进行递归更新
if (j != stones.size() && stones[j] == curDis + stones[i] && dfs(stones, j, curDis)) {
//更新为真的参数,一般不为真
return rec[i][lastDis] = true;
}
}
}
return rec[i][lastDis] = false;//该位置和特定跳跃距离赋值为假,更新参数表
}
bool canCross(vector<int>& stones) {
int n = stones.size();
rec.resize(n);//分配空间
return dfs(stones, 0, 0);
}
};
官方写法的框架已经给出,但还存在很多可以优化的地方
对于每次递归的三个跳跃位置,判断其是否能到石头上,以及对应的石头位置存在数组哪个位置
可以通过哈希表事前存储,减少后面的判断查找
另外,没有必要完全更新完dp备忘录参数表,只需要更新false,设一个全局flag,当碰到为真置真并剪去所有分支即可
最后,从大的跳跃距离往小的遍历可以大大提升性能
性能最好的记忆化搜索
class Solution {
public:
vector<unordered_map<int, bool>> rec;
bool flag=false;
unordered_map<int,int> search_map;//哈希表减少查找
bool backtrack(vector<int>& stones,int i,int lastDis) {
if(flag==true) return true;//找到即可减去所有递归分支
if (i == stones.size() - 1){flag=true; return true;} //边界条件
if (rec[i].count(lastDis)) return rec[i][lastDis];
//减少递归,因为可能会有多个分支到达同一个位置状态,即重复遍历,也是回溯法超时的原因
for (int curDis = lastDis + 1; curDis >= lastDis - 1; curDis--) {
if (curDis < 1) continue;//必须向前走
//查找下一跳的数组位置
if(search_map.count(curDis+stones[i])) //这里用哈希表减少查找
backtrack(stones,search_map[curDis+stones[i]],curDis);
}
if(flag==true) return true; //出循环判断是否找到
return rec[i][lastDis] = false;//遍历完,该位置状态为假
}
bool canCross(vector<int>& stones) {
int n = stones.size();
rec.resize(n);//数组分配空间
for(int i=0;i<n;i++)
search_map[stones[i]]=i;
return backtrack(stones, 0, 0);
}
};
3. 真动态规划(不大懂)
dp[i][k]表示青蛙能否达到数组位置为i石块、上一次跳跃距离为k这个组合状态,返回值为布尔值
状态转移方程:dp[i][k]=dp[j][k−1]||dp[j][k]||dp[j][k+1] j表示上一次所在石子数组位置
点击查看代码
class Solution {
public:
bool canCross(vector<int>& stones) {
int n = stones.size();
vector<vector<int>> dp(n, vector<int>(n));
dp[0][0] = true;
for (int i = 1; i < n; ++i) {
if (stones[i] - stones[i - 1] > i) {
return false;
}
}
for (int i = 1; i < n; ++i) {
for (int j = i - 1; j >= 0; --j) {
int k = stones[i] - stones[j];
if (k > j + 1) {
break;
}
dp[i][k] = dp[j][k - 1] || dp[j][k] || dp[j][k + 1];
if (i == n - 1 && dp[i][k]) {
return true;
}
}
}
return false;
}
};