70. 爬楼梯
题目链接
标签
记忆化搜索 数学 动态规划
一维动态规划
思路
本题是一道很基础的 动态规划 题,在动态规划中最重要的就是 状态转移方程,即从前一个状态转移到后一个状态的方程式。此外,初始状态也比较重要,初始状态就是无法使用状态转移方程的状态。
在本题中,爬楼梯有两个选项:爬 1
个台阶、爬 2
个台阶。这就意味着:对于爬 n
个台阶,如果知道爬 n - 1
个台阶 和 爬 n - 2
个台阶 的方法数量,那么分别 再爬 1
个台阶 和 再爬 2
个台阶 就能到达第 n
个台阶。也就是说,爬 n
个台阶的方法数量 为 爬 n - 1
个台阶的方法数量 加 爬 n - 2
个台阶的方法数量。假设爬 n
个台阶的方法数量为
f
(
n
)
f(n)
f(n),则有
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
f(n) = f(n - 1) + f(n - 2)
f(n)=f(n−1)+f(n−2),这就是状态转移方程。
如果用 int[] dp
来保存上 n
个台阶的方法数量,可以将数组的大小设置为 n + 1
,这样 dp[i]
就表示爬 i
个台阶的方法数量。其中 dp[0]
表示爬 0
个台阶的方法数,这个数为 1
,这是通过
f
(
2
)
=
f
(
1
)
+
f
(
0
)
f(2) = f(1) + f(0)
f(2)=f(1)+f(0) 计算出来的,由于
f
(
2
)
=
2
,
f
(
1
)
=
1
f(2) = 2, f(1) = 1
f(2)=2,f(1)=1,所以
f
(
0
)
=
1
f(0) = 1
f(0)=1,即 dp[0] = 1
。
接下来该对初始状态赋初始值了,在状态方程中,由于计算一个状态,要用前面的两个状态,所以要对两个初始状态进行赋值,即 dp[0] = dp[1] = 1
。
代码
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1]; // 状态数组
dp[0] = dp[1] = 1; // 对初始状态赋值
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
}
}
降维
思路
可以发现,在状态转移方程 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2) f(n)=f(n−1)+f(n−2) 中,对于 f ( n ) f(n) f(n),只需要使用 f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n−1),f(n−2) 即可。也就是说:不需要使用一个数组来保存所有状态,只需要使用两个变量来保存前两个状态,然后及时更新它们两个。这就将 一维数组 降维 两个变量 了。
代码
class Solution {
public int climbStairs(int n) {
int dp1 = 1, dp2 = 1;
for (int i = 2; i <= n; i++) {
int dp3 = dp1 + dp2;
dp1 = dp2;
dp2 = dp3;
}
return dp2;
}
}
77. 组合
题目链接
标签
回溯
思路
本题是比较简单的,属于基础的 回溯 题,像这样的回溯题有一个基本的模版:
- 提前初始化 用于储存结果的集合 和 用于回溯的栈,以及保存与回溯不紧密关联的变量。
- 从第一个元素开始回溯。
- 如果栈中已经有 要求数量 个元素,则将栈中的元素作为集合放入结果集合中,并直接返回;否则进行如下操作:
- 枚举所有可能的元素,进行如下操作:
- 标记当前元素已被遍历过了。
- 将当前元素放入栈中。
- 枚举下一个元素。
- 将当前元素从栈中移除。
- 取消对当前元素的标记。
此外,还可以在必要时进行 剪枝,即缩小枚举的元素数量(由于一些元素无法得到最终结果,才将其优化掉),从而降低时间复杂度。
在本题中:
- 可以不用标记当前元素是否被遍历过,直接 在回溯的方法中 传递 待枚举的区间的左端点,这也是一种 标记元素已被遍历 的方式,因为这种方式避免了 后续的方法 枚举 之前方法已经枚举过的值。
- 可以进行剪枝,既然传递 枚举区间的左端点
i
,且右端点n
已知,那么就可以计算区间内还有多少个元素,即n - i + 1
;此外,因为栈中的元素数量可以通过.size()
方法获取,所以通过k - stack.size()
获取需要的元素个数。如果 区间内的元素 不足以满足 需要的元素,那么就无需枚举更大的数字了。
代码
class Solution {
public List<List<Integer>> combine(int n, int k) {
// 初始化成员变量,保存与回溯不紧密关联的变量
this.n = n;
this.k = k;
dfs(1); // 从数字 1 开始遍历
return res;
}
private List<List<Integer>> res = new ArrayList<>(); // 存储结果的集合
private LinkedList<Integer> stack = new LinkedList<>(); // 用于回溯的栈
private int n; // 区间 [1, n] 的右端点
private int k; // 组合中数字的个数
private void dfs(int start) {
if (stack.size() == k) { // 如果栈中已经有 k 个数字
res.add(new ArrayList<>(stack)); // 则将这 k 个数字作为集合放入结果集合
return; // 并直接返回
}
// 枚举区间 [start, n] 内的数字
int need = k - stack.size(); // 还需要的数字的个数
for (int i = start; i <= n; i++) {
int rest = n - i + 1; // 剩余的数字的个数
if (need > rest) { // 如果剩余的数字不够需要的数字
break; // 则无需进行遍历,这是 剪枝
}
stack.push(i); // 将当前数字压入栈中
dfs(i + 1); // 从第 i + 1 个数开始遍历下一个数
stack.pop(); // 将当前数字从栈中移除
}
}
}
127. 单词接龙
题目链接
标签
广度优先搜索 哈希表 字符串
思路
这道题的基本思路不算难,只是中等难度罢了,这道题和 433. 最小基因变化 挺像的,都使用到了 枚举新字符串 + 广度优先搜索 + 防止重复遍历。看懂 433 题之后直接看代码就行了,区别不是很大。
代码
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 存储 单词 的集合,并将所有 单词 放入 集合 中
Set<String> wordSet = new HashSet<>(wordList);
if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
return 0; // 如果 集合中不含单词 或 不含 endWord,则无法转换,返回 0
}
final int len = beginWord.length(); // 字符串的长度
Set<String> vis = new HashSet<>(); // 存储 已遍历过 的单词
int step = 1; // 从 beginWord 到 endWord 需要转换的步数
LinkedList<String> queue = new LinkedList<>(); // 存储 待遍历 的单词
queue.offer(beginWord); // 先将 beginWord 放入队列,等待遍历
vis.add(beginWord); // 并将其放入已遍历过的单词集合
while (!queue.isEmpty()) { // 直到 没有待遍历的单词 才退出循环
int size = queue.size(); // 获取本次遍历的单词数量
for (int cnt = 0; cnt < size; cnt++) { // 循环 size 次,遍历 size 个单词
String curr = queue.poll(); // 取出待遍历的单词,也称作 当前单词
char[] currWord = curr.toCharArray();
// 枚举 从当前单词 发生一次变化 形成的 可能的 新单词
for (int i = 0; i < len; i++) {
char temp = currWord[i]; // 先保存当前单词的原本的第 i 个字符
for (char ch = 'a'; ch <= 'z'; ch++) { // 从 小写字符 中选取字符进行替换
if (ch == temp) { // 如果 当前单词的第 i 个字符 与 待替换字符 一样
continue; // 则无法产生新的单词,跳过接下来的替换
}
// 准备构建新的单词
currWord[i] = ch; // 将当前单词的第 i 个字符替换为 待替换字符
String newWord = new String(currWord); // 形成新的单词
if (vis.contains(newWord) // 如果新单词经遍历过了
|| !wordSet.contains(newWord)) { // 或者新单词不在单词集合中
continue; // 则跳过这个新单词
}
if (newWord.equals(endWord)) { // 如果 新单词 与 endWord 一致
return step + 1; // 则返回 之前的转换次数 step + 本次转换次数 1
}
queue.offer(newWord); // 将新单词放入队列,等待遍历
vis.add(newWord); // 并将其放入已遍历过的单词集合
}
currWord[i] = temp; // 将原有的单词还原回来
}
}
step++;
}
return 0; // 则转换,返回 0
}
}