LeetCode 70, 77, 127


70. 爬楼梯

题目链接

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(n1)+f(n2),这就是状态转移方程。

如果用 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(n1)+f(n2) 中,对于 f ( n ) f(n) f(n),只需要使用 f ( n − 1 ) , f ( n − 2 ) f(n - 1), f(n - 2) f(n1),f(n2) 即可。也就是说:不需要使用一个数组来保存所有状态,只需要使用两个变量来保存前两个状态,然后及时更新它们两个。这就将 一维数组 降维 两个变量 了。

代码

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. 组合

题目链接

77. 组合

标签

回溯

思路

本题是比较简单的,属于基础的 回溯 题,像这样的回溯题有一个基本的模版:

  1. 提前初始化 用于储存结果的集合用于回溯的栈,以及保存与回溯不紧密关联的变量。
  2. 从第一个元素开始回溯。
  3. 如果栈中已经有 要求数量 个元素,则将栈中的元素作为集合放入结果集合中,并直接返回;否则进行如下操作:
  4. 枚举所有可能的元素,进行如下操作:
    1. 标记当前元素已被遍历过了。
    2. 将当前元素放入栈中。
    3. 枚举下一个元素。
    4. 将当前元素从栈中移除。
    5. 取消对当前元素的标记。

此外,还可以在必要时进行 剪枝,即缩小枚举的元素数量(由于一些元素无法得到最终结果,才将其优化掉),从而降低时间复杂度。

在本题中:

  • 可以不用标记当前元素是否被遍历过,直接 在回溯的方法中 传递 待枚举的区间的左端点,这也是一种 标记元素已被遍历 的方式,因为这种方式避免了 后续的方法 枚举 之前方法已经枚举过的值。
  • 可以进行剪枝,既然传递 枚举区间的左端点 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. 单词接龙

题目链接

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
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值