哈喽,大家好鸭,这里是东东,这是我的第一篇博客,如有什么不足和错误,欢迎大家指正,想直接看题目解析的可以直接跳转至题解标题,我先来bb两句 ,作为一个写博客的新人,先说几句吧。
前言:
这里是东东,一个喜欢代码但是技术仍待提高的大三学生,平时也很享受和同学讨论问题,互相交流。我的代码风格应该算是比较工整的?喜欢使用调用函数的方式实现功能,所以我的解析里会出现很多函数(雾)。写博客也是空闲时间才会写个一两篇,作为巩固知识和与广大友友交流也是不错的一种方式。我每次写解析我会尽量做到详细,按思考步骤一步一步来,希望能给大家带来帮助(但相应的我的博客也会比较长)。现在我在积极的刷题,为蓝桥杯作准备,会挑一些我觉得很好的类型的题来出博客,不定期更新,但每次更新我都会尽量写详细。难免有疏漏或者没有写详细的地方也欢迎大家在评论区留言给我,我看到应该就会回的吧(心虚),主要是我也没有那么经常上博客 。
题解:
以下是题目,想看网站原版可以点击这里。
这里给出官方其中一个示例:
先来分析一下题目。有的同学可能刚开始接触这类题并不知道怎么做(包括我这个小白),会把这道题想得比较复杂,想着有没有比较灵巧的方法可以快速解决这道题。但其实这道题就是一道普通的BFS广搜题,看到求最小次数以及限制较小的广大搜索空间就可以往朴素的BFS靠了。我们从"0000"状态开始搜索,每次旋转一个拨轮(正向/反向),遇到“死锁”返回,直到搜索到目标状态,返回搜索的步数即可。
这里介绍一种比普通BFS还要高效的方法——双向广搜,使用的条件是知道搜索的初始状态与结束状态,可以大大缩小解空间,提高你的搜索速度。相较于普通BFS,双向BFS顾名思义,也就是从起点和终点一起开始搜索,如果搜索中途相遇了,停止搜索,返回结果。拿官方示例举例子,也就是分别从“0000”和“0202”开始搜索,假设两者都搜索到了“1200”这个状态,就可以返回了。
双向广搜的在普通BFS的基础上需要多出一个队列和map,以下是准备工作的代码:
typedef map<string,int> MSI; //需要用到map,这里简化map<string,int>变量表达形式
typedef pair<string,int> PSI; //map用于存放节点的访问情况以及步数,这里用unordered_map也可
class Solution {
public:
queue<string> qstart; //起始队列
queue<string> qend; //结尾队列
MSI mstart;
MSI mend;
int result; // 返回的结果
unordered_set<string> dead; //用于快速确定是否属于死亡密码
int openLock(vector<string>& deadends, string target) {
if (target.compare("0000") == 0) //特殊情况判断,判断起点是否为终点
return 0;
qstart.push("0000"); //把起点终点分别放入队列和map
qend.push(target);
mstart.insert(PSI("0000",0));
mend.insert(PSI(target,0));
dead.insert(deadends.begin(), deadends.end());//将死亡密码转存到查找效率更高的set里
}
}
初始化做完了,接下来我们就开始搜索了,我们需要先知道哪个队列比较短,短的我们先搜。这里search起到的作用是进行一层的搜索,如果搜索到了首尾交点,返回步数。
// 当两个队列都不为空的时候
while (!qstart.empty() && !qend.empty()) {
result=-1;
if (qstart.size() < qend.size()) {
result=search(qstart, mstart, mend);
}
else {
result=search(qend, mend, mstart);
}
if(result!=-1)break; //如果搜索到了数据,退出循环
}
return result;
这里介绍一下search函数,第一个参数cur是当前搜索的队列,self是cur队列对应的map,other是另一个队列的map。这里的cur和self一定要是引用,因为我们后续需要改变cur和self,如果不加&的话改动不会生效。
接下来我们就需要完善search函数的功能了:先写出两层循环,第一个代表我要处理当前队列里的所有字符串,第二个就是对当前字符串(item)的每一个字符做处理,其中change函数起到的作用就是拧旋钮,末尾数字为0代表逆时针旋转拨轮,1则是顺时针,p是处理的位置。我们就得到了两个新字符串。
int search(queue<string>& cur, MSI& self, MSI other) {
for (int i=0;i<cur.size();i++) { // 队列中的每个字符串
string item=cur.front();
int step=self[item];
for (int p = 0; p < 4; p++) { // 字符串的每个字符循环
string newstring[2];
newstring[0] = change(item, p, 0);
newstring[1] = change(item, p, 1);
}
cur.pop();
}
return -1;
}
先跳过change的实现,我们得到了在p位置处理后的字符串后,需要进行入队判断以及是否达成结束条件判断。入队判断需要满足在self(也就是当前队列的map)中没有数据,即未曾访问过,同时还不能在死亡密码中,我们让check函数检查string是否在死亡密码中。而达成结束条件需要我们在other(也就是另一队列的map)中能找到当前字符串的数据,也就是意味着起始点和结束点相交了,就可以返回步数了。
bool check(string a){
if (dead.count(a))
return false;
return true;
}
综合上述,我们就可以写出search函数的整体框架了:
int search(queue<string>& cur, MSI& self, MSI other) {
for (int i=0;i<cur.size();i++) { // 队列中的每个字符串
string item=cur.front();
int step=self[item];
for (int p = 0; p < 4; p++) { // 字符串的每个字符循环
string newstring[2];
newstring[0] = change(item, p, 0);
newstring[1] = change(item, p, 1);
for(int m=0;m<2;m++){
//满足结束条件,两者相交
if(other.find(newstring[m])!=other.end()){
int step2=other[newstring[m]];
return step+step2+1; //从cur到下一步需要+1
}
//满足入队条件,self里无数据,不在死亡密码中
if ((self.find(newstring[m])==self.end())&&(check (newstring[m]))){
self.insert(PSI(newstring[m],step+1)); //插入map时,步数+1
cur.push(newstring[m]); //入队
}
}
}
cur.pop();
}
return -1; //所有搜索都结束没有找到结束条件,返回-1
}
最后我们再来看看change的实现,也很简单,就是单纯的条件判断,就不多作解释了:
string change(string prestring, int p, int mode) {
int tempint;
if (mode == 0)
tempint=-1;
else
tempint=1;
int changenum=(prestring[p]-'0')+tempint; //将char转化成int
if (changenum<0)
changenum=9;
if (changenum==10)
changenum=0;
string tempstring=prestring;
tempstring[p]=(changenum+'0'); //将int转化成char
return tempstring;
}
最后不要忘记判断一下“0000”是否在死亡密码中哦,我第一次做的时候就被这个坑了o(╥﹏╥)o,放一下完整的代码,完工(*^▽^*),复杂度是低的那一档,但是用时不是最佳的(105ms),因为要我是按照一步一步思路来的,会额外调用不少函数;这里用unordered_map可能会更快点,想试试的友友们也可以尝试哦。
typedef map<string, int> MSI;
typedef pair<string, int> PSI;
class Solution {
public:
queue<string> qstart;
queue<string> qend;
MSI mstart;
MSI mend;
int result; // 返回的结果
unordered_set<string> dead;
string change(string prestring, int p, int mode) {
int tempint;
if (mode == 0)
tempint=-1;
else
tempint=1;
int changenum=(prestring[p]-'0')+tempint;
if (changenum<0)
changenum=9;
if (changenum==10)
changenum=0;
string tempstring=prestring;
tempstring[p]=(changenum+'0');
return tempstring;
}
bool check(string a){
if (dead.count(a))
return false;
return true;
}
int search(queue<string>& cur, MSI& self, MSI other) {
// 队列中的每个字符串循环
for (int i=0;i<cur.size();i++) {
string item=cur.front(); // 字符串的每个字符循环
int step=self[item];
for (int p = 0; p < 4; p++) {
string newstring[2];
newstring[0] = change(item, p, 0);
newstring[1] = change(item, p, 1);
for(int m=0;m<2;m++){
if(other.find(newstring[m])!=other.end()){
int step2=other[newstring[m]];
return step+step2+1;
}
if ((self.find(newstring[m])==self.end())&&(check (newstring[m]))){
self.insert(PSI(newstring[m],step+1));
cur.push(newstring[m]);
}
}
}
cur.pop();
}
return -1;
}
int openLock(vector<string>& deadends, string target) {
dead.insert(deadends.begin(), deadends.end());
if (target.compare("0000") == 0)
return 0;
if (!check("0000")){
return -1;
}
qstart.push("0000");
qend.push(target);
mstart.insert(PSI("0000",0));
mend.insert(PSI(target,0));
// 当两个队列都不为空的时候
while ((!qstart.empty()) && (!qend.empty())) {
result=-1;
if (qstart.size() < qend.size()) {
result=search(qstart, mstart, mend);
} else {
result=search(qend, mend, mstart);
}
if(result!=-1)break;
}
return result;
}
};
题外话:作者第一次写的时候用的正是双向广搜,但是运行时间却达到了1000ms多,很是不解,对比了其他双向广搜的作者发现原因是我检查是否在死亡密码中仍然用的是vector进行一一遍历,导致我的运行时间非常的差,所以这里推荐将原先给的vector deadends用unordered_map重新存一下,这两个的查找效率是天壤之别的。