题目
一只青蛙想要过河。 假定河流被等分为 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
分析
动规的题目,这个题的状态应该就是所处的位置和能跳的距离.按照这个来定.
查找下一个石头有三种找法
- 遍历石头,找dis-1<=stones[i]-stones[now]<=dis+1的 O(n)
- 通过距离,在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返回值一一对应 - 事先将所有的石头都放到一个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这种,正常的剪肯定剪不了多少,剪不了还是要回溯,回溯就太慢了.