力扣79-揭秘巨慢的程序

100 阅读3分钟

临近研究生毕业,赶忙刷题,刷到力扣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

单词矩阵.png

图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

image.png

图2 先判断的程序的时间开销(秒为单位)

image.png

图3 后判断的程序的时间开销(秒为单位)

不比不知道,一比真吓一跳。这差了几十倍的时间啊。
为什么会多出这么多时间开销呢?
思考ing....

  • 先判断的方式可以减少函数调用,后判断的写法简单。即便你不先判断,后面调用函数还得判断,因此不是判断的开销。
  • 难道是逻辑或的短路特性?我return也会结束分支,因此不是逻辑或的开销。

最后,就连文心一言都去问了。

image.png

图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的命中率