一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
剑指 Offer 12. 矩阵中的路径
思路
(回溯) O(n^2 3^k)
深度优先搜索,我们定义这样一种搜索顺序,即先枚举单词的起点,然后依次枚举单词的每个字母。在这个过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。
递归函数设计:
bool dfs(vector<vector<char>>& board, string& word,int u,int x,int y)
u代表当前枚举到了目标单词word第u个位置。
x,y是当前搜索到的二维字符网格的横纵坐标。
搜索过程如下:
- 1、在二维字符网格中枚举每个单词的起点。
- 2、从该起点出发向四周搜索单词
word,并记录此时枚举到单词word的第u个位置 (u从0开始)。 - 3、如果当前搜索的位置
(x,y)的元素board[x][y] == word[u],则继续向四周搜索。 - 4、直到枚举到单词
word的最后一个字母返回ture,否则返回false。
递归边界:
- 1、当搜索过程出现当前位置
board[x][y] != word[u],说明当前路径不合法,返回false。 - 2、
u == word.size() - 1,成功搜索到单词末尾,返回true。
实现细节:
-
1、搜索过的位置继续搜索下一层时,需要对当前位置进行标识,表示已经搜索
-
2、可以使用偏移数组来简化代码。
时间复杂度分析: 单词起点一共有 n^2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O(n^2 3^k) 。
c++代码
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
for(int i = 0; i < board.size(); i++)
for(int j = 0; j < board[i].size(); j++)
if(dfs(board,word,0,i,j)) return true;
return false;
}
int dx[4] = {-1,0,1,0}, dy[4] = {0,1,0,-1}; //方向数组
bool dfs(vector<vector<char>>& board, string& word,int u,int x,int y)
{
if(board[x][y] != word[u]) return false;
if(u == word.size() - 1) return true;
char t = board[x][y];
board[x][y] = '.';
for(int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
//出界或者走到已经搜索过的位置
if(a < 0 || a >= board.size() || b < 0 || b >= board[0].size() || board[a][b] == '.') continue;
if(dfs(board,word,u+1,a,b)) return true;
}
board[x][y] = t;
return false;
}
};
剑指 Offer 13. 机器人的运动范围
思路
(BFS) O(nm) 这是一个典型的宽度优先搜索问题,我们从 (0, 0)点开始,每次朝上下左右四个方向扩展新的节点即可。
扩展时需要注意新的节点需要满足如下条件:
- 之前没有遍历过,这个可以用个
bool数组来判断; - 没有走出边界;
- 横纵坐标的各位数字之和小于等于k;
最后答案就是所有遍历过的合法的节点个数。
时间复杂度
每个节点最多只会入队一次,所以时间复杂度不会超过方格中的节点个数。最坏情况下会遍历方格中的所有点,所以时间复杂度就是 O(nm)。
细节:
如果把整个棋盘当做一个状态,那就需要回溯;如果把棋盘中的每个点当做状态,就不需要回溯。
c++代码
class Solution {
public:
int get_sum(pair<int, int> t){ //求出行坐标和列坐标的数位之和
int sum = 0;
while(t.first){
sum += t.first % 10;
t.first /= 10;
}
while(t.second){
sum += t.second % 10;
t.second /= 10;
}
return sum;
}
int movingCount(int m, int n, int k) {
if(!m || !n) return 0;
queue<pair<int,int>> q;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
vector<vector<bool>> st(m, vector<bool>(n, false)); //标记数组
q.push({0, 0});
int cnt = 0;
while(q.size()){
auto t = q.front();
q.pop();
if(st[t.first][t.second] || get_sum(t) > k) continue; //判断当前点是否可走
cnt++;
st[t.first][t.second] = true; // 标记为走过
for(int i = 0; i < 4; i++){
int a = t.first + dx[i], b = t.second + dy[i];
if(a >= 0 && a < m && b >=0 && b < n)
q.push({a, b});
}
}
return cnt;
}
};
剑指 Offer 14- I. 剪绳子
思路
(数学) O(n) 这道题目是数学中一个很经典的问题,下面我们给出证明:
首先把一个正整数 N 拆分成若干正整数只有有限种拆法,所以存在最大乘积。 假设 N=n1+n2+…+nk,并且 n1×n2×…×nk是最大乘积。
- 显然1不会出现在其中;
- 如果对于某个 i有 ni≥5,那么把 ni 拆分成 3+(ni−3),我们有 3(ni−3)=3ni−9>ni;
- 如果 ni=4,拆成 2+2乘积不变,所以不妨假设没有4;
- 如果有三个以上的2,那么 3×3>2×2×2,所以替换成3乘积更大;
综上,选用尽量多的3,直到剩下2或者4时,用2。
时间复杂度分析:
当 n比较大时,n 会被拆分成 ⌈n/3⌉ 个数,我们需要计算这么多次减法和乘法,所以时间复杂度是O(n)。
c++代码
class Solution {
public:
int cuttingRope(int n) {
if(n <= 3) return 1 * (n - 1); //拆成两段
int res = 1;
if(n % 3 == 1) res = 4, n -= 4; //选用尽量多的3,余下一个4,剪成2*2
else if(n % 3 == 2) res = 2 , n -= 2; //选用尽量多的3,余下一个2, 直接乘2
while(n) res *= 3, n -= 3;
return res;
}
};