临近研究生毕业,赶忙刷题,刷到力扣79-单词搜索。这不就是回溯算法,结果一直说我超时,花了几小时,终于弄清楚了真正原因,望各位也能避坑。
单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
用例1
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出: true
图1 单词矩阵
我的代码
想着这是普通的回溯算法题,用c++很快就写了如下版本。
class Solution {
public:
vector<vector<char>> board;
string word;
bool bt4exist(int i, int j, int k){
/* end */
if(k== word.length()){return true;
}else{
vector<vector<int>> dirts= {{1, 0}, {-1, 0}, {0, -1}, {0, 1}};
for(int c= 0; c< dirts.size(); c++){
int x= i+ dirts[c][0], y= j+ dirts[c][1];
if(x>= 0&& x< board.size()&& y>= 0&& y< board[0].size()&& board[x][y]== word[k]){
board[x][y]= '$';
if(bt4exist(x, y, k+ 1)){return true;}
board[x][y]= word[k];
}
}
}
}
bool exist(vector<vector<char>>& board, string word) {
this->board= board;this->word= word;
for(int i= 0; i< board.size(); i++){
for(int j= 0; j< board[0].size(); j++){
if(board[i][j]== word[0]){
board[i][j]= '$';
if(bt4exist(i, j, 1)){return true;}
board[i][j]= word[0];
}
}
}
}
};
结果是正确的,可是时间超过了。看了大神们的写法(代码如下)。
class Solution {
public:
vector<vector<char>> board;
string word;
bool bt4exist(int x0, int y0, int k){
if(x0< 0|| x0>= board.size()|| y0< 0|| y0>= board[0].size()|| board[x0][y0]!= word[k]){return false;
}else{
if(k== word.length()- 1){return true;
}else{
board[x0][y0]= '$';
bool flag= bt4exist(x0- 1, y0, k+ 1)|| bt4exist(x0+ 1, y0, k+ 1)|| bt4exist(x0, y0- 1, k+ 1)|| bt4exist(x0, y0+ 1, k+ 1);
board[x0][y0]= word[k];
return flag;
}
}
}
bool exist(vector<vector<char>>& board, string word) {
this->board= board; this->word= word;
int n= board.size(), m= board[0].size();
for(int i= 0; i< n; i++){
for(int j= 0; j< m; j++){
if(bt4exist(i, j, 0)){return true;}
}
}
return false;
}
};
这不就是判断先后的差异嘛?我采用先判断,还减少程序调用呢?计算了下下述用例,两个程序花的时间:
用例2
输入: [["A","A","A","A","A","A"],["A","A","A","A","A","A"],["A","A","A","A","A","A"],["A","A","A","A","A","A"],["A","A","A","A","A","A"],["A","A","A","A","A","A"]], word = "AAAAAAAAAAAAAAB"
输出: false
图2 先判断的程序的时间开销(秒为单位)
图3 后判断的程序的时间开销(秒为单位)
不比不知道,一比真吓一跳。这差了几十倍的时间啊。
为什么会多出这么多时间开销呢?
思考ing....
- 先判断的方式可以减少函数调用,后判断的写法简单。即便你不先判断,后面调用函数还得判断,因此不是判断的开销。
- 难道是逻辑或的短路特性?我return也会结束分支,因此不是逻辑或的开销。
最后,就连文心一言都去问了。
图4 文心一言的结果
不得不说,文心一言真的很强,说的很有道理,但是都没用。\
罪魁祸首
控制变量法,循环,局部变量等,能展开的就展开,1 hours later....
class Solution {
public:
vector<vector<int>> dirts= {{1, 0}, {-1, 0}, {0, -1}, {0, 1}};
vector<vector<char>> board;
string word;
bool bt4exist(int x0, int y0, int k){
if(k== word.length()){return true;
}else{
bool flag= false;
if(!flag&& check_x(x0+ -1)&& check_y(y0+ 0)&& board[x0+ -1][y0+ 0]== word[k]){char temp= board[x0+ -1][y0+ 0];board[x0+ -1][y0+ 0]= '$';flag= bt4exist(x0+ -1, y0+ 0, k+ 1);board[x0+ -1][y0+ 0]= temp;}
if(!flag&& check_x(x0+ 1)&& check_y(y0+ 0)&& board[x0+ 1][y0+ 0]== word[k]){char temp= board[x0+ 1][y0+ 0];board[x0+ 1][y0+ 0]= '$';flag= bt4exist(x0+ 1, y0+ 0, k+ 1);board[x0+ 1][y0+ 0]= temp;}
if(!flag&& check_x(x0+ 0)&& check_y(y0+ -1)&& board[x0+ 0][y0+ -1]== word[k]){char temp= board[x0+ 0][y0+ -1];board[x0+ 0][y0+ -1]= '$';flag= bt4exist(x0+ 0, y0+ -1, k+ 1);board[x0+ 0][y0+ -1]= temp;}
if(!flag&& check_x(x0+ 0)&& check_y(y0+ 1)&& board[x0+ 0][y0+ 1]== word[k]){char temp= board[x0+ 0][y0+ 1];board[x0+ 0][y0+ 1]= '$';flag= bt4exist(x0+ 0, y0+ 1, k+ 1);board[x0+ 0][y0+ 1]= temp;}
return flag;
}
}
bool exist(vector<vector<char>>& board, string word) {
this->board= board; this->word= word;
int n= board.size(), m= board[0].size();
for(int i= 0; i< n; i++){
for(int j= 0; j< m; j++){
if(board[i][j]== word[0]){
char temp= board[i][j];
board[i][j]= '$';
bt4exist(i, j, 1);
board[i][j]= temp;
}
}
}
return false;
}
}
终于,我发现上述程序跑的跟后判断的几乎一样快了。原来罪魁祸首是局部变量dirts。
那为什么去掉局部变量,程序就变快很多了呢?
首先,递归程序里有局部变量有一定销毁的开销,这是主要原因。其次,我们程序里会多次用到dirts,局部变量放在栈区,内存里;循环固定4次,把它加载到cache,它生命周期就到了,这写法不符合程序访问的局部性原理。
好,那我们针对代码进行如下微调即可。
class Solution {
public:
vector<vector<int>> dirts= {{1, 0}, {-1, 0}, {0, -1}, {0, 1}};
vector<vector<char>> board;
string word;
bool bt4exist(int i, int j, int k){
/* end */
if(k== word.length()){return true;
}else{
for(int c= 0; c< dirts.size(); c++){
int x= i+ dirts[c][0], y= j+ dirts[c][1];
if(x>= 0&& x< board.size()&& y>= 0&& y< board[0].size()&& board[x][y]== word[k]){
board[x][y]= '$';
if(bt4exist(x, y, k+ 1)){return true;}
board[x][y]= word[k];
}
}
}
}
bool exist(vector<vector<char>>& board, string word) {
this->board= board;this->word= word;
for(int i= 0; i< board.size(); i++){
for(int j= 0; j< board[0].size(); j++){
if(board[i][j]== word[0]){
board[i][j]= '$';
if(bt4exist(i, j, 1)){return true;}
board[i][j]= word[0];
}
}
}
}
};
总结
写个回溯算法,收获还不少,总结如下:
- 递归调用中减少局部变量的定义
- 扩展经常访问的局部变量为全局变量可以提高cache的命中率