<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>MidnightSun &apos;s Blog</title><description>Astro</description><link>https://m1dnightsun.github.io/</link><language>zh_CN</language><item><title>Day48-单调栈 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/monotonicstack/day48_monotonic_stack_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/monotonicstack/day48_monotonic_stack_part1/</guid><description>单调栈，每日温度，下一个更大元素I，下一个更大元素II</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;单调栈&lt;/h2&gt;
&lt;p&gt;单调栈是一种栈结构，但有特殊要求：&lt;/p&gt;
&lt;p&gt;栈里的元素是单调递增或者单调递减排列的。&lt;/p&gt;
&lt;p&gt;通常分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单调递增栈：栈内元素从栈底到栈顶是递增的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单调递减栈：栈内元素从栈底到栈顶是递减的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：这里的&quot;单调&quot;指的是栈内元素大小关系，而不是压入栈的顺序。&lt;/p&gt;
&lt;p&gt;核心思想：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;维护一个栈，在遍历数组时动态保持栈的单调性，一旦遇到破坏单调性的元素，就弹出栈顶，处理对应关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;每日温度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/daily-temperatures/&quot;&gt;739. 每日温度&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;用单调递减栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈中存放下标，对应的温度是单调递减的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当新温度比栈顶元素温度高时，说明找到了栈顶元素的下一个更大温度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体实现思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化一个空栈 &lt;code&gt;stk&lt;/code&gt;，和一个与 &lt;code&gt;temperatures&lt;/code&gt; 长度相同的 &lt;code&gt;result&lt;/code&gt; 数组，初始为全 0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从左到右遍历 &lt;code&gt;temperatures&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当前温度大于栈顶所代表的温度，说明找到了更高温度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弹出栈顶元素，更新 &lt;code&gt;result[栈顶下标] = 当前下标 - 栈顶下标&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后继续比较，直到栈为空或栈顶温度大于当前温度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前下标压入栈中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后栈中剩下的元素没有更高温度，对应的 &lt;code&gt;answer[i] = 0&lt;/code&gt;（初始就是 0，不需要再动）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; dailyTemperatures(vector&amp;lt;int&amp;gt;&amp;amp; temperatures) {
        vector&amp;lt;int&amp;gt; result(temperatures.size(), 0); // 初始化结果数组，默认值为0
        stack&amp;lt;int&amp;gt; stk; // 单调栈，存储索引

        for (int i = 0; i &amp;lt; temperatures.size(); i++) {
            // 当栈不为空且当前温度大于栈顶温度时，说明找到了更高的温度
            while (!stk.empty() &amp;amp;&amp;amp; temperatures[i] &amp;gt; temperatures[stk.top()]) {
                int index = stk.top(); // 获取栈顶索引
                stk.pop(); // 弹出栈顶元素
                result[index] = i - index; // 计算天数差并存入结果数组
            }
            stk.push(i); // 将当前索引压入栈中
        }
        return result; // 返回结果数组
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;下一个更大元素 I&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/next-greater-element-i/&quot;&gt;739. 下一个更大元素 I&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;预处理 nums2 中每个元素的下一个更大元素，然后查询 nums1。&lt;/p&gt;
&lt;p&gt;用单调递减栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈中存放 nums2 的元素。&lt;/li&gt;
&lt;li&gt;遇到比栈顶大的元素时，栈顶元素的下一个更大元素就是当前元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用一个哈希表 map，记录每个元素对应的下一个更大元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历 &lt;code&gt;nums2&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;栈为空或者当前元素小于等于栈顶元素，入栈。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前元素大于栈顶元素，弹栈，并在 &lt;code&gt;map&lt;/code&gt; 中记录：&lt;code&gt;map[栈顶元素] = 当前元素&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历 &lt;code&gt;nums1&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查找每个元素对应的下一个更大元素，如果没有则返回 -1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; nextGreaterElement(vector&amp;lt;int&amp;gt;&amp;amp; nums1, vector&amp;lt;int&amp;gt;&amp;amp; nums2) {
        unordered_map&amp;lt;int, int&amp;gt; nextGreaterMap; // 存储每个元素的下一个更大元素
        stack&amp;lt;int&amp;gt; stk; // 单调栈，存储索引

        for (int i = 0; i &amp;lt; nums2.size(); i++) {
            // 当栈不为空且当前元素大于栈顶元素时，说明找到了下一个更大元素
            while (!stk.empty() &amp;amp;&amp;amp; nums2[i] &amp;gt; nums2[stk.top()]) {
                nextGreaterMap[nums2[stk.top()]] = nums2[i]; // 存储下一个更大元素
                stk.pop(); // 弹出栈顶元素
            }
            stk.push(i); // 将当前索引压入栈中
        }

        vector&amp;lt;int&amp;gt; result; // 存储结果
        for (int num : nums1) {
            result.push_back(nextGreaterMap.count(num) ? nextGreaterMap[num] : -1); // 如果没有下一个更大元素，则返回-1
        }
        return result; // 返回结果数组
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;下一个更大元素 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/next-greater-element-ii/&quot;&gt;503. 下一个更大元素 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;处理方式和上题差不多，只是需要循环两遍数组来模拟“环形”。&lt;/p&gt;
&lt;p&gt;用单调递减栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遍历两遍数组（2n长度），用取模操作 &lt;code&gt;i % n&lt;/code&gt; 访问元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化一个栈，存放下标。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化一个数组 &lt;code&gt;answer&lt;/code&gt;，全设为 -1。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遍历 &lt;code&gt;2 * n&lt;/code&gt; 次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当前元素大于栈顶元素代表找到了下一个更大的元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;弹出栈，更新结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只在第一次遍历（i &amp;lt; n）时压栈，避免重复压入元素。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终得到所有答案。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; nextGreaterElements(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        vector&amp;lt;int&amp;gt; result(n, -1); // 初始化结果数组，默认值为-1
        stack&amp;lt;int&amp;gt; stk; // 单调栈，存储索引

        for (int i = 0; i &amp;lt; 2 * n; i++) {
            int index = i % n; // 处理循环数组的索引
            // 当栈不为空且当前元素大于栈顶元素时，说明找到了下一个更大元素
            while (!stk.empty() &amp;amp;&amp;amp; nums[index] &amp;gt; nums[stk.top()]) {
                result[stk.top()] = nums[index]; // 存储下一个更大元素
                stk.pop(); // 弹出栈顶元素
            }
            if (i &amp;lt; n) { // 只在第一次遍历时将索引压入栈中
                stk.push(index); // 将当前索引压入栈中
            }
        }
        return result; // 返回结果数组
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day45-动态规划 part12</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day45_dynamic_programing_part12/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day45_dynamic_programing_part12/</guid><description>动态规划，不同的子序列，两个字符串的删除操作，编辑距离</description><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;115. 不同的子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/distinct-subsequences/&quot;&gt;115. 不同的子序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义一个二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示 &lt;code&gt;s&lt;/code&gt; 的前 &lt;code&gt;i&lt;/code&gt; 个字符中，组成 &lt;code&gt;t&lt;/code&gt; 的前 &lt;code&gt;j&lt;/code&gt; 个字符的子序列个数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;s[i - 1] == t[j - 1]&lt;/code&gt;，那么可以有两种选择：
&lt;ul&gt;
&lt;li&gt;要用 &lt;code&gt;s[i-1]&lt;/code&gt; 来匹配 &lt;code&gt;t[j-1]&lt;/code&gt;：即 &lt;code&gt;dp[i-1][j-1]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不用 &lt;code&gt;s[i-1]&lt;/code&gt;，直接继承前面的状态：即 &lt;code&gt;dp[i-1][j]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因此递推公式为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (s[i - 1] == t[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
    dp[i][j] = dp[i - 1][j];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0][0] = 1&lt;/code&gt;，空字符串可以匹配空字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i][0] = 1&lt;/code&gt;，任何字符串都可以通过删除全部字符得到空字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[0][j] = 0&lt;/code&gt;（&lt;code&gt;j &amp;gt; 0&lt;/code&gt;），空字符串无法组成非空字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;long long&amp;gt;&amp;gt; dp(s.size() + 1, vector&amp;lt;long long&amp;gt;(t.size() + 1, 0));
dp[0][0] = 1;
for (int i = 1; i &amp;lt;= s.size(); i++) dp[i][0] = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;i = 1&lt;/code&gt; 遍历到 &lt;code&gt;s.size()&lt;/code&gt;，&lt;code&gt;j = 1&lt;/code&gt; 遍历到 &lt;code&gt;t.size()&lt;/code&gt;，先遍历 &lt;code&gt;s&lt;/code&gt;，再遍历 &lt;code&gt;t&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= s.size(); i++) {
    for (int j = 1; j &amp;lt;= t.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numDistinct(string s, string t) {
        vector&amp;lt;vector&amp;lt;long long&amp;gt;&amp;gt; dp(s.size() + 1, vector&amp;lt;long long&amp;gt;(t.size() + 1, 0));
        for (int i = 0; i &amp;lt;= s.size(); i++) dp[i][0] = 1;
        
        for (int i = 1; i &amp;lt;= s.size(); i++) {
            for (int j = 1; j &amp;lt;= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.size()][t.size()];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;583. 两个字符串的删除操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/delete-operation-for-two-strings/&quot;&gt;583. 两个字符串的删除操作&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示将 &lt;code&gt;word1&lt;/code&gt; 的前 &lt;code&gt;i&lt;/code&gt; 个字符和 &lt;code&gt;word2&lt;/code&gt; 的前 &lt;code&gt;j&lt;/code&gt; 个字符变得相同所需要删除的最少步数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;word1[i-1] == word2[j-1]&lt;/code&gt;，那么不需要删除字符，继承前面的状态：&lt;pre&gt;&lt;code&gt;dp[i][j] = dp[i-1][j-1];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;word1[i-1] != word2[j-1]&lt;/code&gt;，要么删 &lt;code&gt;word1[i-1]&lt;/code&gt;，要么删 &lt;code&gt;word2[j-1]&lt;/code&gt;，取最小值再加一：&lt;pre&gt;&lt;code&gt;dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][0] = i&lt;/code&gt;，将 &lt;code&gt;word1&lt;/code&gt; 删除成空字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[0][j] = j&lt;/code&gt;，将 &lt;code&gt;word2&lt;/code&gt; 删除成空字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(word1.size() + 1, vector&amp;lt;int&amp;gt;(word2.size() + 1, 0));
for (int i = 0; i &amp;lt;= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j &amp;lt;= word2.size(); j++) dp[0][j] = j;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;i = 1&lt;/code&gt; 遍历到 &lt;code&gt;word1.size()&lt;/code&gt;，&lt;code&gt;j = 1&lt;/code&gt; 遍历到 &lt;code&gt;word2.size()&lt;/code&gt;，从前向后遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= word1.size(); i++) {
    for (int j = 1; j &amp;lt;= word2.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minDistance(string word1, string word2) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(word1.size() + 1, vector&amp;lt;int&amp;gt;(word2.size() + 1, 0));
        for (int i = 0; i &amp;lt;= word1.size(); i++) dp[i][0] = i;
        for (int j = 0; j &amp;lt;= word2.size(); j++) dp[0][j] = j;

        for (int i = 1; i &amp;lt;= word1.size(); i++) {
            for (int j = 1; j &amp;lt;= word2.size(); j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
                }
            }
        }

        return dp[word1.size()][word2.size()];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;72. 编辑距离&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/edit-distance/&quot;&gt;72. 编辑距离&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示将 &lt;code&gt;word1&lt;/code&gt; 的前 &lt;code&gt;i&lt;/code&gt; 个字符转换成 &lt;code&gt;word2&lt;/code&gt; 的前 &lt;code&gt;j&lt;/code&gt; 个字符所使用的最少编辑操作次数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;编辑操作包括：插入、删除、替换。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;word1[i-1] == word2[j-1]&lt;/code&gt;，则不需要任何操作：&lt;pre&gt;&lt;code&gt;dp[i][j] = dp[i-1][j-1];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;word1[i-1] != word2[j-1]&lt;/code&gt;，则考虑三种情况：
&lt;ul&gt;
&lt;li&gt;插入：&lt;code&gt;dp[i][j-1] + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除：&lt;code&gt;dp[i-1][j] + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;替换：&lt;code&gt;dp[i-1][j-1] + 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;取三者最小值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = min({dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][0] = i&lt;/code&gt;，把 &lt;code&gt;word1&lt;/code&gt; 变成空字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[0][j] = j&lt;/code&gt;，把空字符串变成 &lt;code&gt;word2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(word1.size() + 1, vector&amp;lt;int&amp;gt;(word2.size() + 1, 0));
for (int i = 0; i &amp;lt;= word1.size(); i++) dp[i][0] = i;
for (int j = 0; j &amp;lt;= word2.size(); j++) dp[0][j] = j;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;i = 1&lt;/code&gt; 遍历到 &lt;code&gt;word1.size()&lt;/code&gt;，&lt;code&gt;j = 1&lt;/code&gt; 遍历到 &lt;code&gt;word2.size()&lt;/code&gt;，先遍历 &lt;code&gt;word1&lt;/code&gt; 再遍历 &lt;code&gt;word2&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= word1.size(); i++) {
    for (int j = 1; j &amp;lt;= word2.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minDistance(string word1, string word2) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(word1.size() + 1, vector&amp;lt;int&amp;gt;(word2.size() + 1, 0));
        for (int i = 0; i &amp;lt;= word1.size(); i++) dp[i][0] = i;
        for (int j = 0; j &amp;lt;= word2.size(); j++) dp[0][j] = j;

        for (int i = 1; i &amp;lt;= word1.size(); i++) {
            for (int j = 1; j &amp;lt;= word2.size(); j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1});
                }
            }
        }

        return dp[word1.size()][word2.size()];
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day46-动态规划 part13</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day46_dynamic_programing_part13/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day46_dynamic_programing_part13/</guid><description>动态规划，回文子串，最长回文子序列</description><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;647. 回文子串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/palindromic-substrings/&quot;&gt;647. 回文子串&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义一个二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示字符串 &lt;code&gt;s&lt;/code&gt; 中从下标 &lt;code&gt;i&lt;/code&gt; 到下标 &lt;code&gt;j&lt;/code&gt; 这一子串是否是回文子串。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果是回文子串，则 &lt;code&gt;dp[i][j] = true&lt;/code&gt;，否则为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;s[i] == s[j]&lt;/code&gt;，并且：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;j - i &amp;lt;= 1&lt;/code&gt;，说明子串长度是 1 或 2，直接就是回文子串；&lt;/li&gt;
&lt;li&gt;或者 &lt;code&gt;dp[i+1][j-1] == true&lt;/code&gt;，说明去掉两端字符后，中间子串是回文，那么整个也是回文。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因此递推公式为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (s[i] == s[j]) {
    if (j - i &amp;lt;= 1) dp[i][j] = true;
    else dp[i][j] = dp[i + 1][j - 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;初始时，&lt;code&gt;dp[i][i] = true&lt;/code&gt;，单个字符本身就是回文子串。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt; dp(s.size(), vector&amp;lt;bool&amp;gt;(s.size(), false));
for (int i = 0; i &amp;lt; s.size(); i++) {
    dp[i][i] = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;因为 &lt;code&gt;dp[i][j]&lt;/code&gt; 依赖于 &lt;code&gt;dp[i+1][j-1]&lt;/code&gt;，所以 &lt;code&gt;i&lt;/code&gt; 要从大到小遍历，&lt;code&gt;j&lt;/code&gt; 要从小到大遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = s.size() - 1; i &amp;gt;= 0; i--) {
    for (int j = i + 1; j &amp;lt; s.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt; dp(n, vector&amp;lt;bool&amp;gt;(n, false));
        int result = 0;

        for (int i = n - 1; i &amp;gt;= 0; i--) {
            for (int j = i; j &amp;lt; n; j++) {
                if (s[i] == s[j]) {
                    if (j - i &amp;lt;= 1) dp[i][j] = true;
                    else dp[i][j] = dp[i + 1][j - 1];
                }
                if (dp[i][j]) result++;
            }
        }
        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;516. 最长回文子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-palindromic-subsequence/&quot;&gt;516. 最长回文子序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义一个二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示字符串 &lt;code&gt;s&lt;/code&gt; 中从下标 &lt;code&gt;i&lt;/code&gt; 到下标 &lt;code&gt;j&lt;/code&gt; 的子串内，&lt;strong&gt;最长回文子序列的长度&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;s[i] == s[j]&lt;/code&gt;，那么两端可以构成回文，长度加 2：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = dp[i+1][j-1] + 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;s[i] != s[j]&lt;/code&gt;，则取删除左边或者右边元素后能得到的最长回文子序列长度的最大值：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][i] = 1&lt;/code&gt;，每个单独的字符，最长回文子序列长度就是 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(s.size(), vector&amp;lt;int&amp;gt;(s.size(), 0));
for (int i = 0; i &amp;lt; s.size(); i++) {
    dp[i][i] = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;因为 &lt;code&gt;dp[i][j]&lt;/code&gt; 依赖于 &lt;code&gt;dp[i+1][j-1]&lt;/code&gt;、&lt;code&gt;dp[i+1][j]&lt;/code&gt; 和 &lt;code&gt;dp[i][j-1]&lt;/code&gt;，所以 &lt;code&gt;i&lt;/code&gt; 要从大到小遍历，&lt;code&gt;j&lt;/code&gt; 从小到大遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = s.size() - 1; i &amp;gt;= 0; i--) {
    for (int j = i + 1; j &amp;lt; s.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(n, 0));
        for (int i = 0; i &amp;lt; n; i++) {
            dp[i][i] = 1;
        }

        for (int i = n - 1; i &amp;gt;= 0; i--) {
            for (int j = i + 1; j &amp;lt; n; j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day44-动态规划 part11</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day44_dynamic_programing_part11/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day44_dynamic_programing_part11/</guid><description>动态规划，最长公共子序列，不相交的线，最大子序和，判断子序列</description><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;最长公共子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-common-subsequence/&quot;&gt;1143. 最长公共子序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;定义一个二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示 &lt;code&gt;text1&lt;/code&gt; 的前 &lt;code&gt;i&lt;/code&gt; 个字符与 &lt;code&gt;text2&lt;/code&gt; 的前 &lt;code&gt;j&lt;/code&gt; 个字符的最长公共子序列长度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意：这里的“前 &lt;code&gt;i&lt;/code&gt; 个字符”不包括 &lt;code&gt;text1[i]&lt;/code&gt;，即 &lt;code&gt;dp&lt;/code&gt; 的下标是从 1 开始表示字符串前缀长度。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;text1[i - 1] == text2[j - 1]&lt;/code&gt;，说明两个字符串当前字符相等，那么 &lt;code&gt;dp[i][j] = dp[i - 1][j - 1] + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;否则，&lt;code&gt;dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (text1[i - 1] == text2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;i == 0&lt;/code&gt; 或 &lt;code&gt;j == 0&lt;/code&gt; 时，即其中一个字符串为空，最长公共子序列为 0&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(text1.size() + 1, vector&amp;lt;int&amp;gt;(text2.size() + 1, 0));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;我们从 &lt;code&gt;i = 1&lt;/code&gt; 遍历到 &lt;code&gt;text1.size()&lt;/code&gt;，&lt;code&gt;j = 1&lt;/code&gt; 遍历到 &lt;code&gt;text2.size()&lt;/code&gt;，并根据 &lt;code&gt;text1[i - 1]&lt;/code&gt; 和 &lt;code&gt;text2[j - 1]&lt;/code&gt; 进行转移。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= text1.size(); i++) {
    for (int j = 1; j &amp;lt;= text2.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(m + 1, vector&amp;lt;int&amp;gt;(n + 1, 0));
        for (int i = 1; i &amp;lt;= m; i++) {
            for (int j = 1; j &amp;lt;= n; j++) {
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1035. 不相交的线&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/uncrossed-lines/&quot;&gt;1035. 不相交的线&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 表示 A 的前 &lt;code&gt;i&lt;/code&gt; 个元素与 B 的前 &lt;code&gt;j&lt;/code&gt; 个元素之间，最多可以连接多少条不相交的线。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;本质上与最长公共子序列类似：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;A[i-1] == B[j-1]&lt;/code&gt;，则可以连接一条线：&lt;code&gt;dp[i][j] = dp[i-1][j-1] + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;否则：&lt;code&gt;dp[i][j] = max(dp[i-1][j], dp[i][j-1])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][0] = 0&lt;/code&gt;，表示 B 为空时无法连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[0][j] = 0&lt;/code&gt;，表示 A 为空时无法连接&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(A.size() + 1, vector&amp;lt;int&amp;gt;(B.size() + 1, 0));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从前往后遍历 A 和 B：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= A.size(); i++) {
    for (int j = 1; j &amp;lt;= B.size(); j++) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxUncrossedLines(vector&amp;lt;int&amp;gt;&amp;amp; A, vector&amp;lt;int&amp;gt;&amp;amp; B) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(A.size() + 1, vector&amp;lt;int&amp;gt;(B.size() + 1, 0));
        for (int i = 1; i &amp;lt;= A.size(); i++) {
            for (int j = 1; j &amp;lt;= B.size(); j++) {
                if (A[i - 1] == B[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[A.size()][B.size()];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;53. 最大子序和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-subarray/&quot;&gt;53. 最大子序和&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示以 &lt;code&gt;nums[i]&lt;/code&gt; 结尾的连续子数组的最大和。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;我们有两种选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要么将 &lt;code&gt;nums[i]&lt;/code&gt; 加到 &lt;code&gt;dp[i-1]&lt;/code&gt; 上，表示继续累加；&lt;/li&gt;
&lt;li&gt;要么舍弃前面的子数组，从 &lt;code&gt;nums[i]&lt;/code&gt; 重新开始。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = max(dp[i - 1] + nums[i], nums[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0] = nums[0]&lt;/code&gt;，表示以第一个元素结尾的最大子数组和就是它自己。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(nums.size());
dp[0] = nums[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从前往后遍历即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt; nums.size(); i++) {
    dp[i] = max(dp[i - 1] + nums[i], nums[i]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxSubArray(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        vector&amp;lt;int&amp;gt; dp(nums.size());
        dp[0] = nums[0];
        int result = dp[0];
        for (int i = 1; i &amp;lt; nums.size(); i++) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            result = max(result, dp[i]);
        }
        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;392. 判断子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/is-subsequence/&quot;&gt;392. 判断子序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;这道题不一定要用动态规划，用双指针更加简单直观。&lt;/p&gt;
&lt;h4&gt;2. 算法核心思路（双指针法）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;指针 &lt;code&gt;i&lt;/code&gt; 指向 &lt;code&gt;s&lt;/code&gt;，&lt;code&gt;j&lt;/code&gt; 指向 &lt;code&gt;t&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;s[i] == t[j]&lt;/code&gt;，说明匹配上了，&lt;code&gt;i++&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;否则 &lt;code&gt;j++&lt;/code&gt;，继续匹配下一个字符。&lt;/li&gt;
&lt;li&gt;如果最后 &lt;code&gt;i == s.size()&lt;/code&gt;，说明 &lt;code&gt;s&lt;/code&gt; 是 &lt;code&gt;t&lt;/code&gt; 的子序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int i = 0, j = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;只需要从左往右遍历两个字符串。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool isSubsequence(string s, string t) {
        int i = 0, j = 0;
        while (i &amp;lt; s.size() &amp;amp;&amp;amp; j &amp;lt; t.size()) {
            if (s[i] == t[j]) i++;
            j++;
        }
        return i == s.size();
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day43-动态规划 part10</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day43_dynamic_programing_part10/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day43_dynamic_programing_part10/</guid><description>动态规划，最长递增子序列，最长连续递增子序列，最长重复子序列</description><pubDate>Wed, 23 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;最长递增子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-increasing-subsequence/&quot;&gt;300. 最长递增子序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;我们可以定义一个一维的dp数组 &lt;code&gt;dp[i]&lt;/code&gt;，表示以 &lt;code&gt;nums[i]&lt;/code&gt; 结尾的最长递增子序列的长度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;位置 &lt;code&gt;i&lt;/code&gt; 的最长递增子序列的长度等于 &lt;code&gt;j&lt;/code&gt; 从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;i - 1&lt;/code&gt; 的所有位置中，&lt;code&gt;nums[j] &amp;lt; nums[i]&lt;/code&gt; 的最大值加 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此我们可以得到递推公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (nums[j] &amp;lt; nums[i]) {
    dp[i] = max(dp[i], dp[j] + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;p&gt;对于每一个位置 &lt;code&gt;i&lt;/code&gt;，我们都可以认为它本身就是一个递增子序列，因此我们可以将 &lt;code&gt;dp[i]&lt;/code&gt; 初始化为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(nums.size(), 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;在递推公式中，我们需要用到 &lt;code&gt;j&lt;/code&gt; 从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;i - 1&lt;/code&gt; 的所有位置，因此我们需要先遍历 &lt;code&gt;i&lt;/code&gt;，再遍历 &lt;code&gt;j&lt;/code&gt;，并且是从前往后遍历。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; nums.size(); i++) {
    for (int j = 0; j &amp;lt; i; j++) {
        if (nums[j] &amp;lt; nums[i]) {
            dp[i] = max(dp[i], dp[j] + 1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int lengthOfLIS(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        vector&amp;lt;int&amp;gt; dp(nums.size(), 1); // 初始化为1
        for (int i = 0; i &amp;lt; nums.size(); i++) {
            for (int j = 0; j &amp;lt; i; j++) {
                if (nums[j] &amp;lt; nums[i]) { // 如果nums[j] &amp;lt; nums[i]，说明可以组成递增子序列
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最长连续递增序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-continuous-increasing-subsequence/&quot;&gt;674. 最长连续递增序列&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;我们可以定义一个一维的dp数组 &lt;code&gt;dp[i]&lt;/code&gt;，表示以 下标 &lt;code&gt;i&lt;/code&gt; 结尾的最长连续递增子序列的长度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;如果 &lt;code&gt;nums[i] &amp;gt; nums[i - 1]&lt;/code&gt;，那么以 &lt;code&gt;nums[i]&lt;/code&gt; 结尾的最长连续递增子序列的长度等于以 &lt;code&gt;nums[i - 1]&lt;/code&gt; 结尾的最长连续递增子序列的长度加 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;即：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = dp[i - 1] + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是连续递增，所以我们只需要判断 &lt;code&gt;nums[i] &amp;gt; nums[i - 1]&lt;/code&gt; 即可，不再需要判断 &lt;code&gt;j&lt;/code&gt; 从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;i - 1&lt;/code&gt; 的所有位置，也就是只用一层循环即可。&lt;/p&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;p&gt;和之前求最长递增子序列一样，以下标 &lt;code&gt;i&lt;/code&gt; 结尾的最长连续递增子序列的长度为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(nums.size(), 1); // 初始化为1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;在递推公式中，我们只需要用到 &lt;code&gt;i - 1&lt;/code&gt; 的位置，因此我们只需要从前往后遍历即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt; nums.size(); i++) {
    if (nums[i] &amp;gt; nums[i - 1]) { // 如果nums[i] &amp;gt; nums[i - 1]，说明可以组成递增子序列
        dp[i] = dp[i - 1] + 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findLengthOfLCIS(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (nums.size() == 0) return 0; // 如果数组为空，返回0
        vector&amp;lt;int&amp;gt; dp(nums.size(), 1); // 初始化为1
        for (int i = 1; i &amp;lt; nums.size(); i++) {
            if (nums[i] &amp;gt; nums[i - 1]) { // 如果nums[i] &amp;gt; nums[i - 1]，说明可以组成递增子序列
                dp[i] = dp[i - 1] + 1;
            }
        }
        return *max_element(dp.begin(), dp.end()); // 返回dp的最大值
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最长重复子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/longest-common-subsequence/&quot;&gt;1143. 最长公共子序列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们可以使用一个二维的dp数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示 &lt;code&gt;AA[0...i]&lt;/code&gt; 和 &lt;code&gt;B[0...j]&lt;/code&gt; 的最长公共子序列的长度。&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; ：以下标 &lt;code&gt;i - 1&lt;/code&gt; 为结尾的 &lt;code&gt;A&lt;/code&gt;，和以下标 &lt;code&gt;j - 1&lt;/code&gt; 为结尾的 &lt;code&gt;B&lt;/code&gt;，最长重复子数组长度为 &lt;code&gt;dp[i][j]&lt;/code&gt;。 （“以下标 &lt;code&gt;i - 1&lt;/code&gt; 为结尾的 &lt;code&gt;A&lt;/code&gt;” 标明一定是以 &lt;code&gt;A[i-1]&lt;/code&gt; 为结尾的字符串 ）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;根据 dp数组的定义，&lt;code&gt;dp[i][j]&lt;/code&gt; 由 &lt;code&gt;dp[i - 1][j - 1]&lt;/code&gt; 递推而来。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;A[i - 1] == B[j - 1]&lt;/code&gt; 时，&lt;code&gt;dp[i][j] = dp[i - 1][j - 1] + 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;p&gt;根据 dp数组的定义，&lt;code&gt;dp[i][j]&lt;/code&gt; 由 &lt;code&gt;dp[i - 1][j - 1]&lt;/code&gt; 递推而来，&lt;code&gt;dp[i][0]&lt;/code&gt; 和 &lt;code&gt;dp[0][j]&lt;/code&gt; 实际上是没有意义的，但是在递推的过程中需要用到，因此我们可以将 &lt;code&gt;dp[i][0]&lt;/code&gt; 和 &lt;code&gt;dp[0][j]&lt;/code&gt; 初始化为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(A.size() + 1, vector&amp;lt;int&amp;gt;(B.size() + 1, 0)); // 初始化为0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;在递推公式中，我们需要用到 &lt;code&gt;i - 1&lt;/code&gt; 和 &lt;code&gt;j - 1&lt;/code&gt; 的位置，因此我们需要先遍历 &lt;code&gt;i&lt;/code&gt;，再遍历 &lt;code&gt;j&lt;/code&gt;，并且是从前往后遍历。先遍历 &lt;code&gt;A&lt;/code&gt;，再遍历 &lt;code&gt;B&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= text1.size(); i++) {
            for (int j = 1; j &amp;lt;= text2.size(); j++) {
                if (text1[i - 1] == text2[j - 1]) { // 如果当前字符相等，则最长公共子序列长度加1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else { // 如果当前字符不相等，则取前一个状态的最大值
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(text1.size() + 1, vector&amp;lt;int&amp;gt;(text2.size() + 1, 0)); // dp[i][j]表示text1前i个字符和text2前j个字符的最长公共子序列长度
        for (int i = 1; i &amp;lt;= text1.size(); i++) {
            for (int j = 1; j &amp;lt;= text2.size(); j++) {
                if (text1[i - 1] == text2[j - 1]) { // 如果当前字符相等，则最长公共子序列长度加1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else { // 如果当前字符不相等，则取前一个状态的最大值
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[text1.size()][text2.size()]; // 返回最长公共子序列长度
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day42-动态规划 part09</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day42_dynamic_programing_part9/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day42_dynamic_programing_part9/</guid><description>动态规划，买卖股票 IV， 买卖股票含冷冻期</description><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;买卖股票 IV&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/&quot;&gt;188. 买卖股票的最佳时机 IV&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题和之前的买卖股票类似，都是在给定的交易次数内求最大利润。&lt;/p&gt;
&lt;p&gt;我们同样可以使用一个二维的dp数组来表示状态。&lt;/p&gt;
&lt;p&gt;用 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天对股票的持有状态为 &lt;code&gt;j&lt;/code&gt; 时的最大利润。&lt;code&gt;j&lt;/code&gt; 的状态可以表示为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;j = 0&lt;/code&gt;：表示不操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 1&lt;/code&gt;：表示第一次持有股票&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 2&lt;/code&gt;：表示第一次卖出股票&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 3&lt;/code&gt;：表示第二次持有股票&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 4&lt;/code&gt;：表示第二次卖出股票&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以发现除了 &lt;code&gt;j = 0&lt;/code&gt; 以外，当 &lt;code&gt;j&lt;/code&gt; 为奇数时表示持有股票，为偶数时表示不持有股票。&lt;/p&gt;
&lt;p&gt;因为有 &lt;code&gt;k&lt;/code&gt; 次交易，所以二维的dp数组的大小可以定义为 &lt;code&gt;2 * k + 1&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;根据上面的分析，我们可以定义一个二维的dp数组 &lt;code&gt;dp[i][j]&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天对股票的持有状态为 &lt;code&gt;j&lt;/code&gt; 时的最大利润。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;到达 &lt;code&gt;dp[i][1]&lt;/code&gt; （第一次持有股票）的状态有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天不买入股票，保持原有状态 &lt;code&gt;dp[i-1][1]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天买入股票，说明在第 &lt;code&gt;i - 1&lt;/code&gt; 天不持有股票，买入股票后的所持金额为 &lt;code&gt;dp[i-1][0] - prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在两者中选择最大的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理到达 &lt;code&gt;dp[i][2]&lt;/code&gt; （第一次不持有股票）的状态有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天不卖出股票，保持原有状态 &lt;code&gt;dp[i-1][2]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天卖出股票，说明在第 &lt;code&gt;i - 1&lt;/code&gt; 天持有股票，卖出股票后的所持金额为 &lt;code&gt;dp[i-1][1] + prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在两者中选择最大的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理类比以上的状态，我们可以得到其他状态的递推公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = 0; j &amp;lt; 2 * k - 1; j += 2) {
    dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
    dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;我们可以发现 &lt;code&gt;dp[i][j]&lt;/code&gt; 的都是由 &lt;code&gt;dp[i-1][j]&lt;/code&gt; 和 &lt;code&gt;dp[i-1][j+1]&lt;/code&gt; 递推而来，因此我们可以先初始化 &lt;code&gt;dp[0][j]&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第0天没有操作，所以 &lt;code&gt;dp[0][0] = 0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第0天第一次持有股票，即首日就买入股票，所以 &lt;code&gt;dp[0][1] = -prices[0]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第0天第一次不持有股票，即首日就买入卖出，所以 &lt;code&gt;dp[0][2] = 0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第0天第二次持有股票，所以 &lt;code&gt;dp[0][3] = -prices[0]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第0天第二次不持有股票，所以 &lt;code&gt;dp[0][4] = 0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以在这里我们可以当 &lt;code&gt;j&lt;/code&gt; 为偶数时，&lt;code&gt;dp[0][j] = 0&lt;/code&gt;，当 &lt;code&gt;j&lt;/code&gt; 为奇数时，&lt;code&gt;dp[0][j] = -prices[0]&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = 1; j &amp;lt; 2 * k; j += 2) {
    dp[0][j] = -prices[0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;我们可以发现 &lt;code&gt;dp[i][j]&lt;/code&gt; 的都是由 &lt;code&gt;dp[i-1][j]&lt;/code&gt; 和 &lt;code&gt;dp[i-1][j+1]&lt;/code&gt; 递推而来，因此遍历顺序是从前往后&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxProfit(int k, vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        if (prices.size() == 0) return 0;
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(prices.size(), vector&amp;lt;int&amp;gt;(2 * k + 1, 0)); // 二维dp数组，大小为2*k+1
        for (int j = 1; j &amp;lt; 2 * k; j += 2) { // 初始化dp数组
            dp[0][j] = -prices[0];
        }
        for (int i = 1; i &amp;lt; prices.size(); i++) {
            for (int j = 0; j &amp;lt; 2 * k - 1; j += 2) {
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        }
        return dp[prices.size() - 1][2 * k];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最佳买卖股票时机含冷冻期&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/&quot;&gt;309. 最佳买卖股票时机含冷冻期&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;我们可以定义一个二维的dp数组来表示状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 第i天状态为j，所剩的最多现金为dp[i][j]。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;具体可以区分出如下四个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态一：持有股票状态（今天买入股票，或者是之前就买入了股票然后没有操作，一直持有）&lt;/li&gt;
&lt;li&gt;不持有股票状态，这里就有两种卖出股票状态
&lt;ul&gt;
&lt;li&gt;状态二：保持卖出股票的状态（两天前就卖出了股票，度过一天冷冻期。或者是前一天就是卖出股票状态，一直没操作）&lt;/li&gt;
&lt;li&gt;状态三：今天卖出股票&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;状态四：今天为冷冻期状态，但冷冻期状态不可持续，只有一天&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/518d5baaf33f4b2698064f8efb42edbf.png&quot; alt=&quot;四种状态&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以将 &lt;code&gt;j&lt;/code&gt; 状态表示为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;j = 0&lt;/code&gt;：状态1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 1&lt;/code&gt;：状态2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 2&lt;/code&gt;：状态3&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j = 3&lt;/code&gt;：状态4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;冷冻期的前一天，只能是 「今天卖出股票」状态，如果是 「不持有股票状态」那么就很模糊，因为不一定是 卖出股票的操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;到达到买入股票状态（状态一）即：&lt;code&gt;dp[i][0]&lt;/code&gt;，有两个具体操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前一天持有股票，保持不变，&lt;code&gt;dp[i-1][0]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前一天不持有股票，今天买入股票，那么又会有两种情况：
&lt;ul&gt;
&lt;li&gt;前一天是冷冻期（状态四），那么今天买入股票的金额为 &lt;code&gt;dp[i-1][3] - prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前一天是卖出股票（状态二），那么今天买入股票的金额为 &lt;code&gt;dp[i-1][1] - prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以 &lt;code&gt;dp[i][0]&lt;/code&gt; 的状态转移方程为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;到达到卖出股票状态（状态二）即：&lt;code&gt;dp[i][1]&lt;/code&gt;，有两个具体操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前一天就是卖出股票的状态，保持不变，&lt;code&gt;dp[i-1][1]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前一天是冷冻期（状态四），那么今天卖出股票的金额为 &lt;code&gt;dp[i-1][3]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;到达今天卖出股票状态（状态三）即：&lt;code&gt;dp[i][2]&lt;/code&gt;，就只有一个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前一天一定是持有股票状态（状态一），那么今天卖出股票的金额为 &lt;code&gt;dp[i-1][0] + prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][2] = dp[i - 1][0] + prices[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;到达冷冻期状态（状态四）即：&lt;code&gt;dp[i][3]&lt;/code&gt;，就只有一个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;昨天一定是卖出股票状态（状态三），今天冷冻期的金额为 &lt;code&gt;dp[i-1][2]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][3] = dp[i - 1][2];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我们可以得到状态转移方程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;对于第0天的状态，我们可以直接初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态一：&lt;code&gt;dp[0][0] = -prices[0]&lt;/code&gt;，表示第0天买入股票。&lt;/li&gt;
&lt;li&gt;状态二：&lt;code&gt;dp[0][1] = 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;状态三：&lt;code&gt;dp[0][2] = 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;状态四：&lt;code&gt;dp[0][3] = 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;我们可以发现 &lt;code&gt;dp[i][j]&lt;/code&gt; 的都是由 &lt;code&gt;dp[i-1][j]&lt;/code&gt; 和 &lt;code&gt;dp[i-1][j+1]&lt;/code&gt; 递推而来，因此遍历顺序是从前往后&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        int n = prices.size();
        if (n == 0) return 0;
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(4, 0));
        dp[0][0] -= prices[0]; // 持股票
        for (int i = 1; i &amp;lt; n; i++) {
            dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        return max(dp[n - 1][3], max(dp[n - 1][1], dp[n - 1][2]));
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day41-动态规划 part08</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day41_dynamic_programing_part8/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day41_dynamic_programing_part8/</guid><description>动态规划，买卖股票 I &amp; II &amp; III</description><pubDate>Mon, 21 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;买卖股票 I&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/&quot;&gt;121. 买卖股票的最佳时机&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;我们可以用一个二维的dp数组来表示状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天持有股票的最大利润，&lt;code&gt;j&lt;/code&gt; 代表持有状态，&lt;code&gt;0&lt;/code&gt; 代表持有，&lt;code&gt;1&lt;/code&gt; 代表不持有。
&lt;code&gt;dp[i][0]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天持有股票的最大利润，&lt;code&gt;dp[i][1]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天不持有股票的最大利润。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果第 &lt;code&gt;i&lt;/code&gt; 天持有股票，即 &lt;code&gt;dp[i][0]&lt;/code&gt;，那么有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i-1&lt;/code&gt; 天也持有股票，即状态和第 &lt;code&gt;i - 1&lt;/code&gt; 天一样，所持金额为 &lt;code&gt;dp[i-1][0]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i-1&lt;/code&gt; 天不持有股票，说明在第 &lt;code&gt;i&lt;/code&gt; 天买入了股票，所得所持现金就是买入今天的股票后所得现金，也就是 &lt;code&gt;-prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么 &lt;code&gt;dp[i][0]&lt;/code&gt; 应当选择最大的所持金额：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][0] = max(dp[i - 1][0], -prices[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果第 &lt;code&gt;i&lt;/code&gt; 天不持有股票，即 &lt;code&gt;dp[i][1]&lt;/code&gt;，那么有两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i-1&lt;/code&gt; 天也不持有股票，即状态和第 &lt;code&gt;i - 1&lt;/code&gt; 天一样，所持金额为 &lt;code&gt;dp[i-1][1]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i-1&lt;/code&gt; 天持有股票，说明在第 &lt;code&gt;i&lt;/code&gt; 天卖出了股票，所得所持现金就是卖出今天的股票后所得现金，那么所持金就要加上今天卖出的股票的价格，也就是 &lt;code&gt;dp[i-1][0] + prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样的，&lt;code&gt;dp[i][1]&lt;/code&gt; 应当选择最大的所持金额：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;我们可以发现 &lt;code&gt;dp[i][0]&lt;/code&gt; 和 &lt;code&gt;dp[i][1]&lt;/code&gt; 的都是由 &lt;code&gt;dp[i-1][0]&lt;/code&gt; 和 &lt;code&gt;dp[i-1][1]&lt;/code&gt; 递推而来，因此我们可以先初始化 &lt;code&gt;dp[0][0]&lt;/code&gt; 和 &lt;code&gt;dp[0][1]&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0][0]&lt;/code&gt; 表示第 &lt;code&gt;0&lt;/code&gt; 天持有股票的最大利润，也就是第 &lt;code&gt;0&lt;/code&gt; 天就买入了股票，所持金额为 &lt;code&gt;-prices[0]&lt;/code&gt;（我们假设初始现金为0）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[0][1]&lt;/code&gt; 表示第 &lt;code&gt;0&lt;/code&gt; 天不持有股票的最大利润，也就是第 &lt;code&gt;0&lt;/code&gt; 天不买入股票，所持金额为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;显然我们的遍历顺序是从前往后遍历的。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        if (prices.size() &amp;lt;= 1) return 0;
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(prices.size(), vector&amp;lt;int&amp;gt;(2, 0));
        dp[0][0] = -prices[0]; // 第 0 天持有股票
        dp[0][1] = 0; // 第 0 天不持有股票

        for (int i = 1; i &amp;lt; prices.size(); i++) {
            dp[i][0] = max(dp[i - 1][0], -prices[i]); // 持有股票
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); // 不持有股票
        }
        return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;买卖股票 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/&quot;&gt;122. 买卖股票的最佳时机 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题和买卖股票 I 的区别在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;买卖股票 I 只能进行一次交易，而买卖股票 II 可以进行多次交易。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;唯一不同的地方，就是推导 &lt;code&gt;dp[i][0]&lt;/code&gt; 的时候，第i天买入股票的情况。&lt;/p&gt;
&lt;p&gt;因为一只股票可以买卖多次，所以当第i天买入股票的时候，所持有的现金可能有之前买卖过的利润。&lt;/p&gt;
&lt;p&gt;如果第 &lt;code&gt;i&lt;/code&gt; 天买入股票，所得现金就是昨天不持有股票的所得现金 &lt;strong&gt;减去&lt;/strong&gt; 今天的股票价格 即：&lt;code&gt;dp[i - 1][1] - prices[i]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;整体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        // 动态规划
        int n = prices.size();
        if (n &amp;lt; 2) return 0;
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(2, 0));
        dp[0][0] = -prices[0]; // 第 0 天持有股票
        dp[0][1] = 0; // 第 0 天不持有股票

        for (int i = 1; i &amp;lt; n; i++) {
            // 持有股票，在前一天不持有股票的基础上买入
            // 那么今天的利润就是前一天不持有股票的利润减去今天的价格
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); 
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); // 不持有股票
        }
        return dp[n - 1][1]; // 返回最后一天不持有股票的最大利润
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;买卖股票 III&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/&quot;&gt;123. 买卖股票的最佳时机 III&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;和买卖股票 I, II 类似，我们可以用一个二维的dp数组来表示状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天持有股票的最大利润，&lt;code&gt;j&lt;/code&gt; 代表持有状态，&lt;code&gt;0&lt;/code&gt; 代表不操作，&lt;code&gt;1&lt;/code&gt; 代表第一次持有，&lt;code&gt;2&lt;/code&gt; 代表第一次不持有，&lt;code&gt;3&lt;/code&gt; 代表第二次持有，&lt;code&gt;4&lt;/code&gt; 代表第二次不持有。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][0]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天不操作的最大利润&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i][1]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天第一次持有股票的最大利润&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i][2]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天第一次不持有股票的最大利润&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i][3]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天第二次持有股票的最大利润&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i][4]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 天第二次不持有股票的最大利润&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于第 &lt;code&gt;i&lt;/code&gt; 天不操作的最大利润，即 &lt;code&gt;dp[i][0]&lt;/code&gt;，那么 &lt;code&gt;dp[i][0]&lt;/code&gt; 就是前一天的最大利润 &lt;code&gt;dp[i - 1][0]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 &lt;code&gt;dp[i][1]&lt;/code&gt; 第 &lt;code&gt;i&lt;/code&gt; 天第一次持有股票的最大利润, 我们可以有两个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天买入股票，那么所持现金就是前一天不操作的最大利润减去今天的股票价格，即 &lt;code&gt;dp[i - 1][0] - prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天不操作，那么所持现金就是前一天第一次持有股票的最大利润，即 &lt;code&gt;dp[i - 1][1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么 &lt;code&gt;dp[i][1]&lt;/code&gt; 应当选择最大的所持金额：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 &lt;code&gt;dp[i][2]&lt;/code&gt; 第 &lt;code&gt;i&lt;/code&gt; 天第一次不持有股票的最大利润, 类似的我们可以有两个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天卖出股票，那么所持现金就是前一天第一次持有股票的最大利润加上今天的股票价格，即 &lt;code&gt;dp[i - 1][1] + prices[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;i&lt;/code&gt; 天不操作，那么所持现金就是前一天第一次不持有股票的最大利润，即 &lt;code&gt;dp[i - 1][2]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么 &lt;code&gt;dp[i][2]&lt;/code&gt; 应当选择最大的所持金额：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同理，我们可以推导出 &lt;code&gt;dp[i][3]&lt;/code&gt; 和 &lt;code&gt;dp[i][4]&lt;/code&gt; 的递推公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][3] = max(dp[i - 1][2] - prices[i], dp[i - 1][3]);
dp[i][4] = max(dp[i - 1][3] + prices[i], dp[i - 1][4]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;第0天没有操作，就是0，即：&lt;code&gt;dp[0][0] = 0&lt;/code&gt;;
第0天做第一次买入的操作，&lt;code&gt;dp[0][1] = -prices[0]&lt;/code&gt;;
第0天做第一次卖出的操作, 可以理解为第一天就买入并卖出，&lt;code&gt;dp[0][2] = 0&lt;/code&gt;;
第0天做第二次买入的操作，相当于第一天买入并卖出，再买入，&lt;code&gt;dp[0][3] = -prices[0]&lt;/code&gt;;
第0天做第二次卖出的操作, 可以理解为第一天买入并卖出，之后再买入并卖出，&lt;code&gt;dp[0][4] = 0&lt;/code&gt;;&lt;/p&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从递推公式我们可以看出，&lt;code&gt;dp[i][0]&lt;/code&gt; 依赖于 &lt;code&gt;dp[i-1][0]&lt;/code&gt;，因此我们需要从前往后遍历。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        if (prices.size() &amp;lt; 2) return 0;
        int n = prices.size();
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(n, vector&amp;lt;int&amp;gt;(5, 0)); // dp[i][j]表示第i天第j种状态的最大利润
        dp[0][0] = 0; // 不操作，即为0
        dp[0][1] = -prices[0]; // 第0天买入股票
        dp[0][2] = 0; // 第0天卖出股票，即为0
        dp[0][3] = -prices[0]; // 第0天第二次买入股票
        dp[0][4] = 0; // 第0天第二次卖出股票，即为0

        for (int i = 1; i &amp;lt; n; i++) {
            dp[i][0] = dp[i - 1][0]; // 不操作
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 第i天第一次持有股票
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); // 第i天第一次不持有股票
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); // 第i天第二次持有股票
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); // 第i天第二次不持有股票
        }
        // return max(dp[n - 1][2], dp[n - 1][4]); // 返回最后一天的最大利润
        return dp[n - 1][4]; // 返回最后一天第二次卖出股票的最大利润
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day39-动态规划 part07</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day39_dynamic_programing_part7/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day39_dynamic_programing_part7/</guid><description>动态规划，打家劫舍 I &amp; II &amp; III</description><pubDate>Sun, 20 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;打家劫舍 I&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/house-robber/&quot;&gt;198. 打家劫舍&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示 偷前 &lt;code&gt;i&lt;/code&gt; 个房子能获得的最大金额&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果偷第 &lt;code&gt;i&lt;/code&gt; 个房子，那么前 &lt;code&gt;i-1&lt;/code&gt; 个房子不能偷，因此 &lt;code&gt;dp[i] = dp[i-2] + nums[i]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果不偷第 &lt;code&gt;i&lt;/code&gt; 个房子，那么前 &lt;code&gt;i&lt;/code&gt; 个房子能获得的最大金额为 &lt;code&gt;dp[i-1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此递推公式为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[0] = nums[0]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[1] = max(nums[0], nums[1])&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;由于 &lt;code&gt;dp[i]&lt;/code&gt; 依赖于 &lt;code&gt;dp[i-1]&lt;/code&gt; 和 &lt;code&gt;dp[i-2]&lt;/code&gt;，因此我们需要从前往后遍历。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if (n == 0) return 0;
        if (n == 1) return nums[0];

        vector&amp;lt;int&amp;gt; dp(n);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);

        for (int i = 2; i &amp;lt; n; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }

        return dp[n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;打家劫舍 II&lt;/h2&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;和打家劫舍 I 类似，但因为房子是环形的，所以我们&lt;strong&gt;不能同时偷第一个和最后一个房子&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们将问题拆解成两个子问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;偷第 &lt;code&gt;0 ~ n-2&lt;/code&gt; 间房屋（不偷最后一间）&lt;/li&gt;
&lt;li&gt;偷第 &lt;code&gt;1 ~ n-1&lt;/code&gt; 间房屋（不偷第一间）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个子问题都是打家劫舍 I 的线性版本。&lt;/p&gt;
&lt;p&gt;我们设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示从第 &lt;code&gt;i&lt;/code&gt; 间房屋开始偷，到当前能偷到的最大金额&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;与打家劫舍 I 相同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;对于子问题 &lt;code&gt;[start ~ end]&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[start] = nums[start]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[start + 1] = max(nums[start], nums[start + 1])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;start + 2&lt;/code&gt; 开始到 &lt;code&gt;end&lt;/code&gt;，依次计算 &lt;code&gt;dp[i]&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;5. 整体代码（包含空间优化）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    int robRange(vector&amp;lt;int&amp;gt;&amp;amp; nums, int start, int end) {
        if (start == end) return nums[start];
        int pre2 = nums[start];
        int pre1 = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i &amp;lt;= end; ++i) {
            int cur = max(pre1, pre2 + nums[i]);
            pre2 = pre1;
            pre1 = cur;
        }
        return pre1;
    }

public:
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        return max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;打家劫舍 III&lt;/h2&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;本题的结构是&lt;strong&gt;二叉树&lt;/strong&gt;，不能用下标作为状态，因此不能直接使用数组。&lt;/p&gt;
&lt;p&gt;我们使用&lt;strong&gt;树形动态规划&lt;/strong&gt;，对每一个节点 &lt;code&gt;root&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记录两个状态：
&lt;ul&gt;
&lt;li&gt;偷当前节点：&lt;code&gt;dp[root][1]&lt;/code&gt;（当前节点值 + 左右子树不偷的结果）&lt;/li&gt;
&lt;li&gt;不偷当前节点：&lt;code&gt;dp[root][0]&lt;/code&gt;（左右子树偷或不偷的最大值之和）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于树是结构体，不能直接建立数组，用函数返回 &lt;code&gt;pair&amp;lt;int, int&amp;gt;&lt;/code&gt; 实现。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;设当前节点为 &lt;code&gt;node&lt;/code&gt;，左右子树为 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt;，对应状态分别为 &lt;code&gt;(l0, l1)&lt;/code&gt; 和 &lt;code&gt;(r0, r1)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[node][1] = node-&amp;gt;val + l0 + r0      // 偷当前节点
dp[node][0] = max(l0, l1) + max(r0, r1) // 不偷当前节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;后序遍历中，递归到空节点时返回 &lt;code&gt;{0, 0}&lt;/code&gt; 表示偷与不偷都为0。&lt;/p&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;后序遍历&lt;/strong&gt;（从叶子到根），因为我们在计算当前节点状态时需要用到左右子树的状态。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    pair&amp;lt;int, int&amp;gt; dfs(TreeNode* root) {
        if (!root) return {0, 0};

        auto left = dfs(root-&amp;gt;left);
        auto right = dfs(root-&amp;gt;right);

        int rob = root-&amp;gt;val + left.second + right.second;
        int notRob = max(left.first, left.second) + max(right.first, right.second);

        return {rob, notRob};
    }

public:
    int rob(TreeNode* root) {
        auto res = dfs(root);
        return max(res.first, res.second);
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day38-动态规划 part06</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day38_dynamic_programing_part6/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day38_dynamic_programing_part6/</guid><description>动态规划，完全背包问题，零钱兑换，完全平方数，单词拆分</description><pubDate>Sat, 19 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;零钱兑换&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/coin-change/&quot;&gt;322. 零钱兑换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题与&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/dp/day37_dynamic_programing_part5/#%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2-ii&quot;&gt;零钱兑换 II&lt;/a&gt;的区别在于：本题在于求最少的硬币数量，而零钱兑换 II 是求组合数。&lt;/p&gt;
&lt;p&gt;转换为背包问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;背包容量：&lt;code&gt;amount&lt;/code&gt;，即目标金额。&lt;/li&gt;
&lt;li&gt;物品：&lt;code&gt;coins&lt;/code&gt;，即硬币的面值。&lt;/li&gt;
&lt;li&gt;物品的体积：&lt;code&gt;coins[i]&lt;/code&gt;，即硬币的面值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt;：表示背包容量为 &lt;code&gt;i&lt;/code&gt; 时，所需的最少硬币数量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;要凑满 &lt;code&gt;j - coins[i]&lt;/code&gt; 的金额，最少需要 &lt;code&gt;dp[j - coins[i]]&lt;/code&gt; 个硬币。&lt;/li&gt;
&lt;li&gt;那么要凑满 &lt;code&gt;j&lt;/code&gt; 的金额，最少需要 &lt;code&gt;dp[j - coins[i]] + 1&lt;/code&gt; 个硬币，也就是 &lt;code&gt;dp[j]&lt;/code&gt; 的值。&lt;/li&gt;
&lt;li&gt;不放硬币 &lt;code&gt;i&lt;/code&gt;：背包容量为 &lt;code&gt;j&lt;/code&gt;，最少需要 &lt;code&gt;dp[j]&lt;/code&gt; 个硬币。&lt;/li&gt;
&lt;li&gt;我们的目标是求最少的硬币数量，因此我们需要取最小值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此递推公式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = min(dp[j], dp[j - coins[i]] + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;首先凑满0的金额需要0个硬币，因此 &lt;code&gt;dp[0] = 0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;但因为递推公式的性质，我们要求的是最小值，因此我们需要初始化 &lt;code&gt;dp[j]&lt;/code&gt; 为一个较大的数。可以用 &lt;code&gt;INT_MAX&lt;/code&gt; 来表示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;本题求的是最少的硬币数量，与排列组合问题无关，一次我们的遍历顺序可以先背包或者先物品。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= amount; ++i) {
    for (int j = 0; j &amp;lt; coins.size(); ++j) {
        if (i &amp;gt;= coins[j]) {
            dp[i] = min(dp[i], dp[i - coins[j]] + 1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    void printDp(const vector&amp;lt;int&amp;gt;&amp;amp; dp) {
        for (auto _dp : dp) {
            if (_dp == INT_MAX) cout &amp;lt;&amp;lt; &quot;INF &quot;;
            else cout &amp;lt;&amp;lt; _dp &amp;lt;&amp;lt; &apos; &apos;;
        }
        cout &amp;lt;&amp;lt; endl;
    }

public:
    int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
        vector&amp;lt;int&amp;gt; dp(amount + 1, INT_MAX);
        dp[0] = 0;
        
        for (auto coin : coins) { // 先遍历物品
            // cout &amp;lt;&amp;lt; &quot;coin = &quot; &amp;lt;&amp;lt; coin &amp;lt;&amp;lt; endl;
            for (auto j = coin; j &amp;lt;= amount; j++) { // 再遍历背包
                // 边界处理，可能出现 INT_MAX + 1 溢出
                // dp[j - coin] == INT_MAX，代表此时尚未找到能凑出 j - coin 金额的方案。
                if (dp[j - coin] != INT_MAX) { 
                    dp[j] = min(dp[j], dp[j - coin] + 1);
                }
            }
            // printDp(dp);
            // cout &amp;lt;&amp;lt; &quot;----------&quot; &amp;lt;&amp;lt; endl;
        }
        return dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里因为我们的 &lt;code&gt;dp[j]&lt;/code&gt; 是从 &lt;code&gt;dp[j - coin]&lt;/code&gt; 推算出来的，也就是说，我们要知道凑成金额为 &lt;code&gt;j - coin&lt;/code&gt; 的最小硬币数，才能推算出凑成金额为 &lt;code&gt;j&lt;/code&gt; 的最小硬币数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;dp[j - coin]&lt;/code&gt; 是 &lt;code&gt;INT_MAX&lt;/code&gt;，即说明我们还没有找到凑成金额为 &lt;code&gt;j - coin&lt;/code&gt; 的方案，那么我们就不能用 &lt;code&gt;dp[j - coin] + 1&lt;/code&gt; 来推算出 &lt;code&gt;dp[j]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;完全平方数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/perfect-squares/&quot;&gt;279. 完全平方数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们可以把这题解释为：我们可以使用任意个数的完全平方数，去凑成一个目标值 &lt;code&gt;n&lt;/code&gt;，然后求出最少的完全平方数的个数。&lt;/p&gt;
&lt;p&gt;因此我们可以把这题转换为背包问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;背包容量：&lt;code&gt;n&lt;/code&gt;，即目标值。&lt;/li&gt;
&lt;li&gt;物品：&lt;code&gt;1, 4, 9, 16, ...&lt;/code&gt;，即完全平方数。&lt;/li&gt;
&lt;li&gt;物品的体积：&lt;code&gt;i * i&lt;/code&gt;，即完全平方数的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[j]&lt;/code&gt;：表示背包容量（凑成总和）为 &lt;code&gt;j&lt;/code&gt; 时，所需的最少完全平方数的个数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;要凑成总和 &lt;code&gt;j&lt;/code&gt;，当前遍历到的完全平方数为 &lt;code&gt;i * i&lt;/code&gt;，那么我们可以选择放入这个完全平方数，或者不放入。
&lt;ul&gt;
&lt;li&gt;放入 &lt;code&gt;i * i&lt;/code&gt;：那么我们需要凑成 &lt;code&gt;j - i * i&lt;/code&gt; 的总和，最少需要 &lt;code&gt;dp[j - i * i] + 1&lt;/code&gt; 个完全平方数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不放入 &lt;code&gt;i * i&lt;/code&gt;：那么我们需要凑成 &lt;code&gt;j&lt;/code&gt; 的总和，最少需要 &lt;code&gt;dp[j]&lt;/code&gt; 个完全平方数。&lt;/li&gt;
&lt;li&gt;我们的目标是求最少的完全平方数的个数，因此我们需要取最小值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此递推公式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = min(dp[j], dp[j - i * i] + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现这里的解法其实就和&lt;a href=&quot;#%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2&quot;&gt;零钱兑换&lt;/a&gt;问题是一样的。&lt;/p&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;首先凑满和为 0 的完全平方数的个数为 0，因此 &lt;code&gt;dp[0] = 0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;但因为递推公式的性质，我们要求的是最小值，因此我们需要初始化 &lt;code&gt;dp[j]&lt;/code&gt; 为一个较大的数。可以用 &lt;code&gt;INT_MAX&lt;/code&gt; 来表示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此初始化 &lt;code&gt;dp&lt;/code&gt; 数组为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[0] = 0; // 凑成总和为0的完全平方数的个数为0
for (int j = 1; j &amp;lt;= n; ++j) {
    dp[j] = INT_MAX; // 凑成总和为j的完全平方数的个数为无穷大
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;本题求的是最少的完全平方数的个数，与排列组合问题无关，一次我们的遍历顺序可以先背包或者先物品。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    void printDp(const vector&amp;lt;int&amp;gt;&amp;amp; dp) {
        for (auto _dp : dp) {
            if (_dp == INT_MAX) {
                cout &amp;lt;&amp;lt; &quot;INF&quot; &amp;lt;&amp;lt; &quot; &quot;;
            } else {
                cout &amp;lt;&amp;lt; _dp &amp;lt;&amp;lt; &quot; &quot;;
            }
        }
        cout &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; &quot;--------------&quot; &amp;lt;&amp;lt; endl;
    }

public:
    int numSquares(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, INT_MAX);
        dp[0] = 0; // 初始化：和为0需要0个完全平方数

        for (int i = 1; i * i &amp;lt;= n; i++) { // 遍历物品（完全平方数）
            for (int j = i * i; j &amp;lt;= n; j++) { // 遍历背包容量
                if (dp[j - i * i] != INT_MAX) {
                    dp[j] = min(dp[j], dp[j - i * i] + 1);
                }
            }
            // printDp(dp);
        }
        return dp[n];
    }
};  
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单词拆分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/word-break/&quot;&gt;139. 单词拆分&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题是一个“字符串切割 + 判断的”问题，可以类比成&lt;strong&gt;字符串长度为背包容量&lt;/strong&gt;，&lt;strong&gt;字典中的单词为物品&lt;/strong&gt;，判断能否恰好组成字符串。&lt;/p&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt;：表示字符串 &lt;code&gt;s[0...i-1]&lt;/code&gt; 是否可以被拆分成字典中的单词。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;我们枚举每一个位置 &lt;code&gt;i&lt;/code&gt;，再枚举 &lt;code&gt;j（j &amp;lt; i）&lt;/code&gt;，看 &lt;code&gt;s[j:i]&lt;/code&gt; 是否是字典中的单词，如果是且 &lt;code&gt;dp[j] == true&lt;/code&gt;，那么 &lt;code&gt;dp[i] = true&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dp[i] = dp[j] &amp;amp;&amp;amp; (s[j:i] ∈ wordDict)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;从递推公式中可以看出，&lt;code&gt;dp[i]&lt;/code&gt; 的状态依靠 &lt;code&gt;dp[j]&lt;/code&gt; 是否为 &lt;code&gt;true&lt;/code&gt;，那么 &lt;code&gt;dp[0]&lt;/code&gt; 就是递推的基础，&lt;code&gt;dp[0]&lt;/code&gt;一定要为 &lt;code&gt;true&lt;/code&gt;，否则递推下去后面都是 &lt;code&gt;false&lt;/code&gt; 了。&lt;/p&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;题目中说是拆分为一个或多个在字典中出现的单词，所以这是完全背包。还要讨论两层for循环的前后顺序。&lt;/p&gt;
&lt;p&gt;如果求组合数就是外层for循环遍历物品，内层for遍历背包。&lt;/p&gt;
&lt;p&gt;如果求排列数就是外层for遍历背包，内层for循环遍历物品。&lt;/p&gt;
&lt;p&gt;而本题我们要求的其实是排列，我们可以拿以下例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s = &quot;applepenapple&quot;
wordDict = [&quot;apple&quot;, &quot;pen&quot;]

那么我们在物品中选择 &quot;apple&quot; + &quot;pen&quot; + &quot;apple&quot;，才能组成 &quot;applepenapple&quot;。

如果选择 &quot;pen&quot; + &quot;apple&quot; + &quot;apple&quot;，虽然单词都在字典中，但是不能组成 &quot;applepenapple&quot;。

因此这里强调了一个单词之间顺序的问题。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们的遍历顺序应该是：外层for循环遍历背包，内层for循环遍历物品。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= s.size(); i++) { // 遍历背包容量，从1~字符串长度
    for (int j = 0; j &amp;lt; i; j++) { // 遍历物品（单词）
        string word = s.substr(j, i - j); // 从j到i的子串
        // 如果当前子串在字典中，并且前面部分可以拆分成单词
        // dp[j] == true 代表前面部分可以拆分成单词
        // dp[i] == true 代表当前子串可以拆分成单词
        if (set.find(word) != set.end() &amp;amp;&amp;amp; dp[j]) {
            dp[i] = true;
        }
    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:    
    void printDp(const vector&amp;lt;bool&amp;gt;&amp;amp; dp) {
        for (auto _dp : dp) {
            cout &amp;lt;&amp;lt; _dp &amp;lt;&amp;lt; &quot; &quot;;
        }
        cout &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; &quot;--------------&quot; &amp;lt;&amp;lt; endl;
    }

public:
    bool wordBreak(string s, vector&amp;lt;string&amp;gt;&amp;amp; wordDict) {
        unordered_set&amp;lt;string&amp;gt; set(wordDict.begin(), wordDict.end());

        vector&amp;lt;bool&amp;gt; dp(s.size() + 1, false);
        dp[0] = true; // 空字符串可以被拆分成字典中的单词

        for (int i = 1; i &amp;lt;= s.size(); i++) { // 遍历背包容量，从1~字符串长度
            for (int j = 0; j &amp;lt; i; j++) { // 遍历物品（单词）
                string word = s.substr(j, i - j); // 从j到i的子串
                // 如果当前子串在字典中，并且前面部分可以拆分成单词
                // dp[j] == true 代表前面部分可以拆分成单词
                // dp[i] == true 代表当前子串可以拆分成单词
                if (set.find(word) != set.end() &amp;amp;&amp;amp; dp[j]) {
                    dp[i] = true;
                }
            }
            // printDp(dp);
        }
        return dp[s.size()];
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day37-动态规划 part05</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day37_dynamic_programing_part5/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day37_dynamic_programing_part5/</guid><description>动态规划，完全背包问题，零钱兑换 II，组合总和 IV，爬楼梯</description><pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;完全背包问题&lt;/h2&gt;
&lt;p&gt;完全背包与0-1背包问题非常相似，不同之处在于：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每种物品可以选择无限次，而不是只能选一次。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设有 &lt;code&gt;N&lt;/code&gt; 件物品和一个容量为 &lt;code&gt;W&lt;/code&gt; 的背包。每件物品有两个属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;体积：&lt;code&gt;v[i]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;价值：&lt;code&gt;w[i]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目标是从这些物品中选取若干件（每种物品可以选无限多次），使得在不超过背包容量的前提下，获得的最大总价值是多少。&lt;/p&gt;
&lt;h3&gt;完全背包的二维dp表示&lt;/h3&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[i][j]&lt;/code&gt;：表示从下标为 &lt;code&gt;[0-i]&lt;/code&gt; 的物品，每个物品可以取无限次，放进容量为 &lt;code&gt;j&lt;/code&gt; 的背包，价值总和最大是多少。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;不放物品 &lt;code&gt;i&lt;/code&gt;：背包容量为 &lt;code&gt;j&lt;/code&gt;，最大价值为 &lt;code&gt;dp[i-1][j]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;放物品 &lt;code&gt;i&lt;/code&gt;：背包空出物品 &lt;code&gt;i&lt;/code&gt; 的体积 &lt;code&gt;v[i]&lt;/code&gt;，剩余容量为 &lt;code&gt;j - v[i]&lt;/code&gt;，&lt;code&gt;dp[i][j - v[i]]&lt;/code&gt; 表示背包容量 为 &lt;code&gt;j - v[i]&lt;/code&gt; 时的最大价值。那么放入物品 &lt;code&gt;i&lt;/code&gt; 后的最大价值为：&lt;code&gt;dp[i][j - v[i]] + w[i]&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;放入物品 &lt;code&gt;i&lt;/code&gt; 后，背包容量为 &lt;code&gt;j - v[i]&lt;/code&gt;，但是物品 &lt;code&gt;i&lt;/code&gt; 还可以继续放入背包中，因此我们还是在考虑物品 &lt;code&gt;i&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此递推公式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里与0-1背包的递推公式不同的是：&lt;code&gt;dp[i][j - v[i]]&lt;/code&gt; 这里的 &lt;code&gt;i&lt;/code&gt; 没有减去1，也就是说物品 &lt;code&gt;i&lt;/code&gt; 还可以继续放入背包中。&lt;/p&gt;
&lt;p&gt;0 - 1背包的递推公式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;p&gt;首先从 &lt;code&gt;dp[i][j]&lt;/code&gt; 的定义出发，如果背包容量 &lt;code&gt;j&lt;/code&gt; 为0的话，即 &lt;code&gt;dp[i][0]&lt;/code&gt;，无论是选取哪些物品，背包价值总和一定为0。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://camo.githubusercontent.com/5e1e1bbf46e6dec87adfee5640d49ee0cff19b239fd936813d15d92d75ff5488/68747470733a2f2f66696c652e6b616d61636f6465722e636f6d2f706963732f323032313031313031303330343139322e706e67&quot; alt=&quot;初始化1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;状态转移方程中： &lt;code&gt;dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i])&lt;/code&gt; 可以看出，&lt;code&gt;dp[i][j]&lt;/code&gt; 依赖于 &lt;code&gt;dp[i-1][j]&lt;/code&gt; 和 &lt;code&gt;dp[i][j - v[i]]&lt;/code&gt;。可以看出有一个方向 &lt;code&gt;i&lt;/code&gt; 是由 &lt;code&gt;i-1&lt;/code&gt; 推导出来，那么 &lt;code&gt;i&lt;/code&gt; 为0的时候就一定要初始化。
因此我们需要初始化 &lt;code&gt;dp[0][j]&lt;/code&gt;，即当没有物品时，背包的最大价值为0。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dp[0][j]&lt;/code&gt;，即：存放编号0的物品的时候，各个容量的背包所能存放的最大价值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;那么很明显当 &lt;code&gt;j &amp;lt; v[0]&lt;/code&gt; 时，&lt;code&gt;dp[0][j]&lt;/code&gt; 应该为0，因为背包容量不够放下物品0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 &lt;code&gt;j &amp;gt;= v[0]&lt;/code&gt; 时，&lt;code&gt;dp[0][j]&lt;/code&gt; 应该为 &lt;code&gt;w[0]&lt;/code&gt;，因为背包容量足够放下物品0。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;j &amp;gt;= v[0]&lt;/code&gt; ，&lt;code&gt;dp[0][j]&lt;/code&gt; 如果能放下 &lt;code&gt;v[0]&lt;/code&gt; 的话，就一直装，每一种物品有无限个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此我们可以初始化 &lt;code&gt;dp[0][j]&lt;/code&gt; 为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(W + 1, 0);
for (int j = 0; j &amp;lt;= W; ++j) {
    if (j &amp;gt;= v[0]) {
        dp[j] = w[0];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;在0 - 1背包中，先遍历物品，再遍历背包容量，或者是先遍历背包容量，再遍历物品，都是可行的，因为这是由推导顺序决定的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 先遍历物品，再遍历背包容量
for (int i = 0; i &amp;lt; N; ++i) {
    for (int j = 1; j &amp;lt;= W; ++j) {
        if (j &amp;lt; v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 先遍历背包容量，再遍历物品
for (int j = 0; j &amp;lt;= W; ++j) {
    for (int i = 1; i &amp;lt; N; ++i) {
        if (j &amp;lt; v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; knapsack(int W, vector&amp;lt;int&amp;gt;&amp;amp; v, vector&amp;lt;int&amp;gt;&amp;amp; w) {
    int N = v.size();
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(N + 1, vector&amp;lt;int&amp;gt;(W + 1, 0)); // dp[i][j]表示从下标为[0-i]的物品，每个物品可以取无限次，放进容量为j的背包，价值总和最大是多少。
    
    for (int j = 0; j &amp;lt;= W; ++j) {
        if (j &amp;gt;= v[0]) {
            dp[0][j] = w[0];
        }
    }

    for (int i = 1; i &amp;lt; N; ++i) {
        for (int j = 0; j &amp;lt;= W; ++j) {
            if (j &amp;lt; v[i]) {
                dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
            }
        }
    }

    return dp[N - 1]; // 返回最后一行，即所有物品的最大价值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完全背包的一维dp表示&lt;/h3&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[j]&lt;/code&gt;：表示从下标为 &lt;code&gt;[0-N]&lt;/code&gt; 的物品，每个物品可以取无限次，放进容量为 &lt;code&gt;j&lt;/code&gt; 的背包，价值总和最大是多少。&lt;/p&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;在上面的二维dp表示中，&lt;code&gt;dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i])&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; N; ++i) {
    for (int j = 1; j &amp;lt;= W; ++j) {
        if (j &amp;lt; v[i]) {
            dp[i][j] = dp[i-1][j]; // 背包容量不够放下物品i
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]); // 放下物品i
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将上一层 &lt;code&gt;dp[i-1]&lt;/code&gt; 的那一层拷贝到 当前层 &lt;code&gt;dp[i]&lt;/code&gt; ，那么 递推公式由：&lt;code&gt;dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])&lt;/code&gt; 变成： &lt;code&gt;dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以我们可以将二维dp表示转化为一维dp表示。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dp[j] = max(dp[j], dp[j - v[i]] + w[i]);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里的 &lt;code&gt;j&lt;/code&gt; 是从 &lt;code&gt;W&lt;/code&gt; 到 &lt;code&gt;v[i]&lt;/code&gt; 递减的，表示当前背包容量为 &lt;code&gt;j&lt;/code&gt;，如果放入物品 &lt;code&gt;i&lt;/code&gt;，那么剩余容量为 &lt;code&gt;j - v[i]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(W + 1, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;在完全背包中，对于一维dp数组来说，其实两个for循环嵌套顺序是无所谓的，因为 &lt;code&gt;dp[j]&lt;/code&gt; 是根据 下标 &lt;code&gt;j&lt;/code&gt; 之前所对应的 &lt;code&gt;dp[j]&lt;/code&gt; 计算出来的。 只要保证下标j之前的 &lt;code&gt;dp[j]&lt;/code&gt; 都是经过计算的就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 先遍历物品，再遍历背包容量
for (int i = 0; i &amp;lt; N; ++i) {
    for (int j = v[i]; j &amp;lt;= W; ++j) { // 从v[i]开始遍历
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 先遍历背包容量，再遍历物品
for (int j = 0; j &amp;lt;= W; ++j) {
    for (int i = 0; i &amp;lt; N; ++i) { // 从0开始遍历
        if (j &amp;gt;= v[i]) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 放下物品i
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;零钱兑换 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/coin-change-2/&quot;&gt;518. 零钱兑换 II&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 状态定义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示金额为 &lt;code&gt;i&lt;/code&gt; 的组合数。&lt;/p&gt;
&lt;h4&gt;2. 状态转移方程&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[i] = dp[i] + dp[i - coins[j]]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;意思是：对于当前硬币 &lt;code&gt;coin&lt;/code&gt;，凑成金额 &lt;code&gt;j&lt;/code&gt; 的方法数 = 凑成金额 &lt;code&gt;j - coin&lt;/code&gt; 的方法数之和。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示当前金额为 &lt;code&gt;i&lt;/code&gt; 的组合数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[i - coins[j]]&lt;/code&gt; 表示当前金额为 &lt;code&gt;i - coins[j]&lt;/code&gt; 的组合数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[0] = 1&lt;/code&gt;，表示凑成金额为0的组合数为1（即不选任何数字）。&lt;/p&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;因为纯完全背包求得装满背包的最大价值是多少，和凑成总和的元素有没有顺序没关系，即：有顺序也行，没有顺序也行。&lt;/p&gt;
&lt;p&gt;而本题要求凑成总和的组合数，元素之间明确要求没有顺序。所以纯完全背包是能凑成总和就行，不用管怎么凑的。本题是求凑出来的方案个数，且每个方案个数是组合数。&lt;/p&gt;
&lt;p&gt;正确的遍历顺序应该是：先遍历硬币，再遍历容量(金额)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int coin : coins) {
    for (int j = coin; j &amp;lt;= amount; ++j) {
        dp[j] += dp[j - coin];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时先遍历硬币，就是求组合。如果先遍历的是容量，那么就会出现重复计算的情况，也就是求的是排列。&lt;/p&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int change(int amount, vector&amp;lt;int&amp;gt;&amp;amp; coins) {
        vector&amp;lt;int&amp;gt; dp(amount + 1, 0);
        dp[0] = 1; // 凑出金额为0的组合数是1（空集）

        for (int coin : coins) {           // 遍历每种硬币（物品）
            for (int j = coin; j &amp;lt;= amount; ++j) { // 遍历容量，从小到大
                if (dp[j] &amp;lt; INT_MAX - dp[j - coin]) { //防止相加数据超int
                    dp[j] += dp[j - coin];
                }
            }
        }
        return dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果求组合数就是外层for循环遍历物品，内层for遍历背包。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果求排列数就是外层for遍历背包，内层for循环遍历物品。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;组合总和 IV&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/combination-sum-iv/&quot;&gt;377. 组合总和 IV&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;因为本题的描述中，元素相同但顺序不同的组合算作不同的组合，所以这其实是一个排列问题。&lt;/p&gt;
&lt;p&gt;所以本题的思路和零钱兑换 II 是一样的，只不过本题是求排列数。&lt;/p&gt;
&lt;p&gt;遍历顺序是：先遍历背包，再遍历物品。&lt;/p&gt;
&lt;p&gt;这里就直接给出代码了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int combinationSum4(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        // 求的是排列
        vector&amp;lt;unsigned int&amp;gt; dp(target + 1, 0);
        dp[0] = 1; // 凑出金额为0的组合数是1（空集）

        for (int i = 1; i &amp;lt;= target; ++i) { // 遍历容量，从小到大
            for (int num : nums) {           // 遍历每种硬币（物品）
                if (i &amp;gt;= num) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;爬楼梯 - 完全背包问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/climbing-stairs/&quot;&gt;70. 爬楼梯&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们将爬楼梯的规则稍微改一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次可以爬 1 ~ 无限 步。&lt;/li&gt;
&lt;li&gt;也就是每次可以选择爬 1 步，2 步，3 步，...，n 步。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题：有多少种不同的方式可以爬到楼梯的顶部？&lt;/p&gt;
&lt;p&gt;那么这里就变成了一个完全背包问题了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跳多少阶楼梯 -&amp;gt; 背包容量&lt;/li&gt;
&lt;li&gt;爬楼梯的方式 -&amp;gt; 物品&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;背包问题&lt;/th&gt;
&lt;th&gt;爬楼梯&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;背包容量 n&lt;/td&gt;
&lt;td&gt;要达到的目标楼梯高度 n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;物品&lt;/td&gt;
&lt;td&gt;每次可以选择的步数 1, 2, ..., n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;物品体积&lt;/td&gt;
&lt;td&gt;每次爬的步数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;物品价值&lt;/td&gt;
&lt;td&gt;这里我们不是求最大价值，而是求组合数（方案数）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;1. 状态定义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示爬到第 &lt;code&gt;i&lt;/code&gt; 层楼梯的方式数。&lt;/p&gt;
&lt;h4&gt;2. 状态转移方程&lt;/h4&gt;
&lt;p&gt;装满背包有几种方法，递推公式一般都是 &lt;code&gt;dp[i] += dp[i - nums[j]]&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;本题呢，&lt;code&gt;dp[i]&lt;/code&gt; 有几种来源，&lt;code&gt;dp[i - 1]，dp[i - 2]，dp[i - 3]&lt;/code&gt; 等等，即：&lt;code&gt;dp[i - j]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那么递推公式为：&lt;code&gt;d&lt;/code&gt;p[i] += dp[i - j]`&lt;/p&gt;
&lt;h4&gt;3. 初始化&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[0] = 1&lt;/code&gt;，表示爬到第0层楼梯的方式数为1（即不爬）。&lt;/p&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;这是背包里求排列问题，即：1、2 步 和 2、1 步都是上三个台阶，但是这两种方法是不同的，所以我们需要先遍历背包，再遍历物品。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= n; ++i) { // 遍历背包
    for (int j = 1; j &amp;lt;= m; ++j) { // 遍历物品
        if (i &amp;gt;= j) {
            dp[i] += dp[i - j];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 整体代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int climbStairs(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, 0);
        dp[0] = 1; // 爬到第0层楼梯的方式数为1（即不爬）

        for (int i = 1; i &amp;lt;= n; ++i) { // 遍历背包
            for (int j = 1; j &amp;lt;= i; ++j) { // 遍历物品
                if (i &amp;gt;= j) {
                    dp[i] += dp[i - j];
                }
            }
        }
        return dp[n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day36-动态规划 part04</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day36_dynamic_programing_part4/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day36_dynamic_programing_part4/</guid><description>动态规划，最后一块石头的重量II，目标和，一和零</description><pubDate>Wed, 16 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;最后一块石头的重量II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/last-stone-weight-ii/&quot;&gt;1049. 最后一块石头的重量 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题中要求的是两两石头相撞之后的重量差的最小值。&lt;/p&gt;
&lt;p&gt;这也就意味着，我们应该尽可能让石头分成重量相等的两堆，那么相撞之后剩下的石头就是最小的重量。&lt;/p&gt;
&lt;p&gt;因此这也和&lt;a href=&quot;https://leetcode.cn/problems/partition-equal-subset-sum/&quot;&gt;分割等和子集&lt;/a&gt;是相似的，也就是一个0 - 1背包问题。&lt;/p&gt;
&lt;p&gt;唯一的区别是：分割等和子集是求是否可以分割成两堆相等的重量，如果凑不成就返回false，否则返回true。
而这个题是求最小的重量差，因此可以两堆石头的重量不相等。&lt;/p&gt;
&lt;p&gt;转换成背包问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;物品就是石头，物品的重量为 &lt;code&gt;stone[i]&lt;/code&gt;，物品的价值为 &lt;code&gt;stone[i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;物品的数量为 &lt;code&gt;n&lt;/code&gt;，背包的容量为 &lt;code&gt;sum(stone) / 2&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp][j]&lt;/code&gt;：表示容量为 &lt;code&gt;j&lt;/code&gt; 的背包，最大可以装入的石头重量为 &lt;code&gt;dp[j]&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“最多可以装的价值为 &lt;code&gt;dp[j]&lt;/code&gt;” 等同于 “最多可以背的重量为 &lt;code&gt;dp[j]&lt;/code&gt;”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2. 确定递推公式&lt;/h4&gt;
&lt;p&gt;0 - 1背包问题的递推公式为：&lt;code&gt;dp[j] = max(dp[j], dp[j - weight[i]] + value[i])&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于本题来说，则转变成了：&lt;code&gt;dp[j] = max(dp[j], dp[j - stone[i]] + stone[i])&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;3. 初始化dp数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;首先，&lt;code&gt;dp[0] = 0&lt;/code&gt;，表示背包容量为0时，最大可以装入的石头重量为0。&lt;/li&gt;
&lt;li&gt;dp数组定义为多大：
&lt;ul&gt;
&lt;li&gt;题中给出的石头数量范围是 &lt;code&gt;1 &amp;lt;= stone.length &amp;lt;= 30&lt;/code&gt;，重量范围是 &lt;code&gt;1 &amp;lt;= stone[i] &amp;lt;= 100&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;那么在极端情况下，石头的数量为30，重量为100，那么总重量为3000。&lt;/li&gt;
&lt;li&gt;我们要求的是背包容量为在总重量的一半，因此我们可以将背包的容量定义为 &lt;code&gt;1501&lt;/code&gt;的长度，也就是 &lt;code&gt;vector&amp;lt;int&amp;gt; dp(1501, 0)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 确定遍历顺序&lt;/h4&gt;
&lt;p&gt;在使用一维dp数组时，外层应该遍历的是物品，内层循环应该是倒序遍历背包容量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; n; ++i) {                  // 遍历每一个物品
    for (int j = sum / 2; j &amp;gt;= stone[i]; --j) { // 容量从大到小倒序遍历
        dp[j] = max(dp[j], dp[j - stone[i]] + stone[i]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 推导dp数组样例&lt;/h4&gt;
&lt;p&gt;假设我们的输入：&lt;code&gt;stones = [2,7,4,1,8,1]&lt;/code&gt;, &lt;code&gt;sum = 23&lt;/code&gt;，那么我们可以将背包的容量定义为 &lt;code&gt;sum / 2 = 11&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;i = 0: 用stone[0], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
i = 1: 用stone[1], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 7, 7, 9, 9, 9]
i = 2: 用stone[2], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 7, 7, 9, 9, 11]
i = 3: 用stone[3], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 7, 7, 9, 9, 11]
i = 4: 用stone[4], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 7, 7, 9, 9, 11]
i = 5: 用stone[5], 遍历背包: dp[0...11]: [0, 0, 2, 2, 2, 2, 2, 7, 7, 9, 9, 11]

我们得到最终结果：dp[11] = 11，说明我们可以将石头分成两堆，重量分别为11和12，那么此时的差值为1，也就是最小的差值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. 返回结果&lt;/h4&gt;
&lt;p&gt;最后 &lt;code&gt;dp[target]&lt;/code&gt; 里表示的就是容量为 &lt;code&gt;target&lt;/code&gt; 的背包，最大可以装入的石头重量。
那么分成两堆石头的话，剩下的石头重量就是 &lt;code&gt;sum - dp[target]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在计算 &lt;code&gt;target&lt;/code&gt; 的时候，&lt;code&gt;target = sum / 2&lt;/code&gt; 因为是向下取整，所以 &lt;code&gt;sum - dp[target]&lt;/code&gt; 一定是大于等于&lt;code&gt;dp[target]&lt;/code&gt; 的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么相撞之后剩下的石头最小重量就是：&lt;code&gt;(sum - dp[target]) - dp[target]&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;7. 代码实现&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int lastStoneWeightII(vector&amp;lt;int&amp;gt;&amp;amp; stones) {
        vector&amp;lt;int&amp;gt; dp(1501, 0); // dp[i]表示容量为i的背包能装的最大重量
        int sum = accumulate(stones.begin(), stones.end(), 0); // 计算所有石头的总重量
        int target = sum / 2; // 背包的最大容量为总重量的一半

        for (int i = 0; i &amp;lt; stones.size(); i++) { // 遍历每个石头
            for (int j = target; j &amp;gt;= stones[i]; j--) { // 从后往前遍历每个容量
                // 如果当前容量j大于等于当前石头的重量stones[i]，则可以选择放入这个石头
                // 更新dp[j]为放入当前石头后的最大重量
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        // 分成两堆石头，一堆是dp[target]，另一堆是sum - dp[target]
        // 返回两堆石头的重量差的绝对值，即为最后剩下的石头的重量
        return abs(dp[target] - (sum - dp[target]));
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8. 复杂度分析 &amp;amp; 总结&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n * target)，其中n为石头的数量，target为背包的容量。&lt;/li&gt;
&lt;li&gt;空间复杂度：O(target)，使用了一维dp数组来存储每个容量的最大重量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本题其实和分割等和子集几乎是一样的，只是最后对 &lt;code&gt;dp[target]&lt;/code&gt; 的处理方式不同。&lt;/p&gt;
&lt;p&gt;割等和子集相当于是求背包是否正好装满，而本题是求背包最多能装多少。&lt;/p&gt;
&lt;h2&gt;目标和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/target-sum/&quot;&gt;494. 目标和&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;回溯暴力解法&lt;/h3&gt;
&lt;p&gt;这里其实和&lt;a href=&quot;https://leetcode.cn/problems/combination-sum/&quot;&gt;组合总和&lt;/a&gt;的思路是一样的，只不过我们可以在每次选择数字的时候选择加号或者减号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    int dfs(vector&amp;lt;int&amp;gt;&amp;amp; nums, int i, int target) {
        if (i == nums.size()) { // 到达数组末尾
            // 如果目标值为0，说明找到了一个符合条件的组合，返回1；否则返回0
            return target == 0 ? 1 : 0;
        }
        // 递归调用，分别考虑加上当前数字和减去当前数字的两种情况
        return dfs(nums, i + 1, target - nums[i]) + dfs(nums, i + 1, target + nums[i]);
    }

public:
    int findTargetSumWays(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        // 通过递归的方式计算所有可能的组合
        return dfs(nums, 0, target);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是使用回溯暴力求解法的时间复杂度是O(2^n)，其中n为数组的长度。
在力扣上提交的数据为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;141/141 cases passed (948 ms)&lt;/li&gt;
&lt;li&gt;Your runtime beats 11.67 % of cpp submissions&lt;/li&gt;
&lt;li&gt;Your memory usage beats 97.37 % of cpp submissions (11.3 MB)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态规划&lt;/h3&gt;
&lt;p&gt;本题可以通过数学推导转换为 0-1 背包问题：&lt;/p&gt;
&lt;p&gt;我们将数组划分为两个子集:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一个子集 &lt;code&gt;P&lt;/code&gt; 的数添加 &lt;code&gt;+&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另一个子集 &lt;code&gt;N&lt;/code&gt; 的数添加 &lt;code&gt;-&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目标是使得最终表达式等于 &lt;code&gt;target&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sum(P) - sum(N) = target // P表示加号的数，N表示减号的数
sum(P) + sum(N) = sum // sum表示所有数的和
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两式相加可以得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2 * sum(P) = target + sum
            ↓
sum(P) = (target + sum) / 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以问题转化为：从数组中选一些数字，使它们的和等于 &lt;code&gt;(target + sum) / 2&lt;/code&gt;，每个数字只能用一次，一共有多少种选法。&lt;/p&gt;
&lt;h4&gt;1. 前置判断（剪枝）&lt;/h4&gt;
&lt;p&gt;首先我们要判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int sum = accumulate(nums.begin(), nums.end(), 0);
if ((target + sum) % 2 != 0 || abs(target) &amp;gt; sum) return 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;(target + sum)&lt;/code&gt; 必须为偶数，否则 P 不为整数，不能划分&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;target &amp;gt; sum&lt;/code&gt;，显然无法达到目标&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 确定dp数组以及下标的含义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dp[j]&lt;/code&gt;：表示和为 &lt;code&gt;j&lt;/code&gt; 的子集个数（从 nums 中选，元素不可重复）&lt;/p&gt;
&lt;h4&gt;3. 确定递推公式&lt;/h4&gt;
&lt;p&gt;对于当前数字 &lt;code&gt;num&lt;/code&gt;，我们可以选择放入背包或者不放入背包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不选当前数字：&lt;code&gt;dp[j]&lt;/code&gt; 保持原样&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = dp[j]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;选当前数 &lt;code&gt;num&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;那么必须从一个“原本和为 &lt;code&gt;j - num&lt;/code&gt; 的子集”中，加上当前 &lt;code&gt;num&lt;/code&gt;，才能变成和为 j。&lt;/li&gt;
&lt;li&gt;所以我们有：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = dp[j] + dp[j - num]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;表示：我们把所有“原本和为 &lt;code&gt;j - num&lt;/code&gt; 的子集”，都加上 &lt;code&gt;num&lt;/code&gt;，得到新的和为 &lt;code&gt;j&lt;/code&gt; 的子集。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终状态转移方程为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = target; j &amp;gt;= num; --j) {
    dp[j] += dp[j - num];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[j - num]&lt;/code&gt; 表示之前已有多少种方法能组成 &lt;code&gt;j - num&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把当前 &lt;code&gt;num&lt;/code&gt; 加进去就能组成 &lt;code&gt;j&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 初始化dp数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0] = 1&lt;/code&gt;：表示和为0的子集只有一种，就是不选任何数字&lt;/li&gt;
&lt;li&gt;其余 &lt;code&gt;dp[j] = 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. 确定遍历顺序&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;外层遍历每个物品 &lt;code&gt;i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内层倒序遍历容量 &lt;code&gt;j&lt;/code&gt;，从 &lt;code&gt;target&lt;/code&gt; 到 &lt;code&gt;num&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6. 代码实现&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findTargetSumWays(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0); // 计算所有数字的总和
        if (sum &amp;lt; target || (sum - target) % 2 != 0) return 0; // 如果目标不可能达到，返回0
        int s = (sum - target) / 2; // 将问题转化为背包问题，求子集和为s的组合数

        vector&amp;lt;int&amp;gt; dp(s + 1, 0); // dp[i]表示和为i的组合数
        dp[0] = 1; // 和为0的组合数为1（即不选任何数字）

        for (int num : nums) { // 遍历每个数字
            for (int j = s; j &amp;gt;= num; j--) { // 从后往前遍历每个容量
                dp[j] += dp[j - num]; // 更新dp[j]为当前数字num的组合数
            }
        }
        return dp[s]; // 返回和为s的组合数
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一和零&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/ones-and-zeroes/&quot;&gt;474. 一和零&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. 状态定义&lt;/h4&gt;
&lt;p&gt;我们使用二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i][j]&lt;/code&gt; 表示在限定 &lt;code&gt;i&lt;/code&gt; 个 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt; 个 &lt;code&gt;1&lt;/code&gt; 的容量下，最多可以选择多少个字符串。&lt;/li&gt;
&lt;li&gt;初始时所有 &lt;code&gt;dp[i][j] = 0&lt;/code&gt;，表示一个字符串都不选。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 状态初始化&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;初始化条件为 &lt;code&gt;dp[0][0] = 0&lt;/code&gt;，含义是：不使用任何 &lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;1&lt;/code&gt;，最多能选 0 个字符串；&lt;/li&gt;
&lt;li&gt;其余 &lt;code&gt;dp[i][j]&lt;/code&gt; 也初始化为 0，表示在该容量下尚未选择任何字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 状态转移方程&lt;/h4&gt;
&lt;p&gt;对于每个字符串 &lt;code&gt;s ∈ strs&lt;/code&gt;，我们统计它的 &lt;code&gt;0&lt;/code&gt; 和 &lt;code&gt;1&lt;/code&gt; 数量分别为 &lt;code&gt;zeros&lt;/code&gt; 和 &lt;code&gt;ones&lt;/code&gt;。然后进行如下状态更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = m; i &amp;gt;= zeros; --i) {
    for (int j = n; j &amp;gt;= ones; --j) {
        dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不选当前字符串 &lt;code&gt;s&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;dp[i][j]&lt;/code&gt; 保持不变；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选当前字符串 &lt;code&gt;s&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;要消耗 &lt;code&gt;zeros&lt;/code&gt; 个 0 和 &lt;code&gt;ones&lt;/code&gt; 个 1；&lt;/li&gt;
&lt;li&gt;那么剩余容量为 &lt;code&gt;i - zeros&lt;/code&gt; 和 &lt;code&gt;j - ones&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;对应的最大选择数为 &lt;code&gt;dp[i - zeros][j - ones]&lt;/code&gt;，加上当前字符串，变为 &lt;code&gt;dp[i - zeros][j - ones] + 1&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;两种方案取最大值，表示当前容量下最多可选字符串数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遍历顺序&lt;/h4&gt;
&lt;p&gt;必须从大到小（倒序）枚举 &lt;code&gt;i&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt;，这是因为每个字符串只能使用一次（符合 0-1 背包特性）。如果从小到大遍历，会导致同一个字符串被多次选入。&lt;/p&gt;
&lt;h4&gt;5. 最终结果&lt;/h4&gt;
&lt;p&gt;状态转移结束后，最终结果为 &lt;code&gt;dp[m][n]&lt;/code&gt;，表示在不超过 &lt;code&gt;m&lt;/code&gt; 个 0 和 &lt;code&gt;n&lt;/code&gt; 个 1 的条件下，最多可以选择多少个字符串。&lt;/p&gt;
&lt;h4&gt;6. 代码实现&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findMaxForm(vector&amp;lt;string&amp;gt;&amp;amp; strs, int m, int n) {
        // 初始化二维 dp 数组，dp[i][j] 表示在 i 个 0 和 j 个 1 的容量限制下，最多可选字符串数量
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(m + 1, vector&amp;lt;int&amp;gt;(n + 1, 0));
        // 遍历每个字符串，相当于遍历每个“物品”
        for (const string&amp;amp; s : strs) {
            // 统计当前字符串中的 0 和 1 的个数
            int zeros = 0, ones = 0;
            for (char c : s) {
                if (c == &apos;0&apos;) zeros++;
                else if (c == &apos;1&apos;) ones++;
            }
            // 进行二维 0-1 背包状态更新，必须倒序遍历（保证每个字符串最多只被使用一次）
            for (int i = m; i &amp;gt;= zeros; --i) {
                for (int j = n; j &amp;gt;= ones; --j) {
                    // 状态转移：选 or 不选当前字符串
                    dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1);
                }
            }
        }
        // 返回最大可选字符串数量
        return dp[m][n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;状态表变化过程（示例）&lt;/h4&gt;
&lt;p&gt;以如下数据为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strs = [&quot;10&quot;, &quot;0&quot;, &quot;1&quot;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m = 1&lt;/code&gt;（最多 1 个 0），&lt;code&gt;n = 1&lt;/code&gt;（最多 1 个 1）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;初始化状态表（dp）：&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i \ j&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h5&gt;处理字符串 &lt;code&gt;&quot;10&quot;&lt;/code&gt;：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zeros = 1&lt;/code&gt;，&lt;code&gt;ones = 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;i=1&lt;/code&gt; 到 &lt;code&gt;zeros=1&lt;/code&gt;，&lt;code&gt;j=1&lt;/code&gt; 到 &lt;code&gt;ones=1&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[1][1] = max(dp[1][1], dp[0][0] + 1) = max(0, 1) = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新后的表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i \ j&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;处理字符串 &lt;code&gt;&quot;0&quot;&lt;/code&gt;：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zeros = 1&lt;/code&gt;，&lt;code&gt;ones = 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[1][1] = max(1, dp[0][1] + 1) = max(1, 1) = 1
dp[1][0] = max(0, dp[0][0] + 1) = max(0, 1) = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新后的表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i \ j&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;处理字符串 &lt;code&gt;&quot;1&quot;&lt;/code&gt;：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zeros = 0&lt;/code&gt;，&lt;code&gt;ones = 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[1][1] = max(1, dp[1][0] + 1) = max(1, 2) = 2
dp[0][1] = max(0, dp[0][0] + 1) = max(0, 1) = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终状态表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i \ j&lt;/th&gt;
&lt;th&gt;0&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h5&gt;最终结果：&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;dp[1][1] = 2&lt;/code&gt;，最多可以选 2 个字符串，比如：&lt;code&gt;&quot;0&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;1&quot;&lt;/code&gt;，或 &lt;code&gt;&quot;10&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;1&quot;&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>Day35-动态规划 part03</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day35_dynamic_programing_part3/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day35_dynamic_programing_part3/</guid><description>动态规划，0-1背包问题，分割等和子集</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0-1背包问题&lt;/h2&gt;
&lt;p&gt;0 - 1 背包问题定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有 &lt;code&gt;N&lt;/code&gt; 个物品，每个物品有一个 体积（或重量）&lt;code&gt;w[i]&lt;/code&gt; 和 价值 &lt;code&gt;v[i]&lt;/code&gt;。现在给你一个容量为 &lt;code&gt;C&lt;/code&gt; 的背包（不能超过容量），请你从这 &lt;code&gt;N&lt;/code&gt; 个物品中选出若干个，使得在不超过背包容量的前提下，总价值最大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;“01”表示每个物品只能选择一次（要么选，要么不选）&lt;/p&gt;
&lt;h3&gt;二维 dp 解法&lt;/h3&gt;
&lt;h4&gt;状态定义：&lt;/h4&gt;
&lt;p&gt;定义二维数组 &lt;code&gt;dp[i][j]&lt;/code&gt;，表示&lt;strong&gt;使用第 &lt;code&gt;0&lt;/code&gt; 到第 &lt;code&gt;i&lt;/code&gt; 件物品&lt;/strong&gt;，在背包容量为 &lt;code&gt;j&lt;/code&gt; 的限制下所能达到的最大价值。&lt;/p&gt;
&lt;h4&gt;状态转移方程：&lt;/h4&gt;
&lt;p&gt;对于第 &lt;code&gt;i&lt;/code&gt; 件物品（体积为 &lt;code&gt;weight[i]&lt;/code&gt;，价值为 &lt;code&gt;value[i]&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不选&lt;/strong&gt;：&lt;code&gt;dp[i][j] = dp[i - 1][j]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果我们&lt;strong&gt;不选择第 &lt;code&gt;i&lt;/code&gt; 件物品&lt;/strong&gt;，那么当前背包容量为 &lt;code&gt;j&lt;/code&gt; 时的最大价值就是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从前 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;i-1&lt;/code&gt; 这些物品中选取&lt;/strong&gt;，在容量限制为 &lt;code&gt;j&lt;/code&gt; 的条件下所能达到的最大价值。&lt;/p&gt;
&lt;p&gt;换句话说，我们&lt;strong&gt;跳过第 &lt;code&gt;i&lt;/code&gt; 件物品，不把它放入背包中&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;是否还需要我把整段上下文整合输出？如果你要用于文档用途，我可以重新排好完整段落。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选&lt;/strong&gt;：&lt;code&gt;dp[i][j] = dp[i - 1][j - weight[i]] + value[i]&lt;/code&gt;（前提是 &lt;code&gt;j &amp;gt;= weight[i]&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;如果我们选择了第 &lt;code&gt;i&lt;/code&gt; 件物品：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会消耗 &lt;code&gt;weight[i]&lt;/code&gt; 的容量，背包剩余空间变成 &lt;code&gt;j - weight[i]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从前 &lt;code&gt;i-1&lt;/code&gt; 件物品中在剩余容量内选取的最大价值为 &lt;code&gt;dp[i - 1][j - weight[i]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;加上当前物品的价值 &lt;code&gt;value[i]&lt;/code&gt;，就是选当前物品的总价值&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：只有当 &lt;code&gt;j &amp;gt;= weight[i]&lt;/code&gt; 时，背包才放得下这件物品&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此状态转移方程为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;初始化：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0][j]&lt;/code&gt; 表示只使用第 0 件物品，在容量为 &lt;code&gt;j&lt;/code&gt; 时的最大价值：
&lt;ul&gt;
&lt;li&gt;若 &lt;code&gt;j &amp;lt; weight[0]&lt;/code&gt;，则 &lt;code&gt;dp[0][j] = 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;j &amp;gt;= weight[0]&lt;/code&gt;，则 &lt;code&gt;dp[0][j] = value[0]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以统一初始化方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = 0; j &amp;lt;= N; ++j) {
    dp[0][j] = (j &amp;gt;= weight[0]) ? value[0] : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;最终答案：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[M - 1][N]&lt;/code&gt;，表示使用前 &lt;code&gt;M&lt;/code&gt; 件物品（编号 &lt;code&gt;0&lt;/code&gt;~&lt;code&gt;M-1&lt;/code&gt;），在容量为 &lt;code&gt;N&lt;/code&gt; 时的最大价值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;示例（模板）：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(M, vector&amp;lt;int&amp;gt;(N + 1, 0));

// 初始化第0件物品
for (int j = 0; j &amp;lt;= N; ++j) {
    if (j &amp;gt;= weight[0]) dp[0][j] = value[0];
}

for (int i = 1; i &amp;lt; M; ++i) {
    for (int j = 0; j &amp;lt;= N; ++j) {
        if (j &amp;lt; weight[i])
            dp[i][j] = dp[i - 1][j];
        else
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一维 dp 解法&lt;/h3&gt;
&lt;h4&gt;状态定义：&lt;/h4&gt;
&lt;p&gt;定义一维数组 &lt;code&gt;dp[j]&lt;/code&gt;，表示在背包容量为 &lt;code&gt;j&lt;/code&gt; 时，可获得的最大价值（考虑过的所有物品来自 &lt;code&gt;weight[0..i]&lt;/code&gt;）。&lt;/p&gt;
&lt;h4&gt;状态转移方程：&lt;/h4&gt;
&lt;p&gt;对于第 &lt;code&gt;i&lt;/code&gt; 件物品（体积 &lt;code&gt;weight[i]&lt;/code&gt;，价值 &lt;code&gt;value[i]&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前容量 &lt;code&gt;j &amp;gt;= weight[i]&lt;/code&gt;，可以选择该物品，状态转移为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;j &amp;lt; weight[i]&lt;/code&gt;，不能选该物品，&lt;code&gt;dp[j]&lt;/code&gt; 保持不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：为防止一个物品被重复使用，必须&lt;strong&gt;倒序枚举容量 &lt;code&gt;j&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;初始化：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0] = 0&lt;/code&gt;：表示容量为 0 时，最大价值只能是 0&lt;/li&gt;
&lt;li&gt;其余 &lt;code&gt;dp[j]&lt;/code&gt; 也初始化为 &lt;code&gt;0&lt;/code&gt;（未放入任何物品）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;遍历顺序：&lt;/h4&gt;
&lt;p&gt;在一维 dp 中，为防止每个物品被选多次，遍历顺序必须如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; M; ++i) {                  // 遍历每一个物品
    for (int j = N; j &amp;gt;= weight[i]; --j) {     // 容量从大到小倒序遍历
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;外层循环表示依次考虑物品 &lt;code&gt;0 ~ M-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内层倒序循环容量，从 &lt;code&gt;N&lt;/code&gt; 到 &lt;code&gt;weight[i]&lt;/code&gt;，以确保当前物品不会在同一轮中被重复使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;最终答案：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[N]&lt;/code&gt; 表示背包容量为 &lt;code&gt;N&lt;/code&gt; 时所能取得的最大价值&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;示例（模板）：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; dp(N + 1, 0);

for (int i = 0; i &amp;lt; M; ++i) {
    for (int j = N; j &amp;gt;= weight[i]; --j) {
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分割等和子集&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/partition-equal-subset-sum/&quot;&gt;416. 分割等和子集&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们想把数组分成两个子集，使得两个子集的和相等。&lt;/p&gt;
&lt;p&gt;等价于：在数组中选出一些元素，使它们的总和为整个数组和的一半。&lt;/p&gt;
&lt;p&gt;记数组总和为 &lt;code&gt;sum&lt;/code&gt;，只要能选出若干个数使它们的和为 &lt;code&gt;sum / 2&lt;/code&gt;，那么剩下的就是另外一半。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组元素都是正整数&lt;/li&gt;
&lt;li&gt;每个数只能选一次（&lt;strong&gt;0-1 背包&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;不求最大价值，而是判断是否能达到某个“目标和” → &lt;strong&gt;判断型背包&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;举例：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;nums = [1, 5, 11, 5]
sum = 22 → target = 11

我们要判断：能否从数组中选出若干个数，使它们的和恰好为 11？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这正是一个 &lt;strong&gt;0-1 背包问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个数是一个“物品”，&lt;/li&gt;
&lt;li&gt;数值是物品的“体积”，&lt;/li&gt;
&lt;li&gt;背包容量为 &lt;code&gt;target = sum / 2&lt;/code&gt;，&lt;/li&gt;
&lt;li&gt;每个物品只能选择一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态规划解法（一维数组）&lt;/h3&gt;
&lt;h4&gt;状态定义：&lt;/h4&gt;
&lt;p&gt;定义 &lt;code&gt;dp[j]&lt;/code&gt; 表示：&lt;strong&gt;是否能选出若干个数，使它们的和恰好为 &lt;code&gt;j&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[j] = true&lt;/code&gt;：能选出和为 &lt;code&gt;j&lt;/code&gt; 的子集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[j] = false&lt;/code&gt;：不能选出&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;状态转移方程：&lt;/h4&gt;
&lt;p&gt;遍历数组中每个数 &lt;code&gt;num&lt;/code&gt;，从容量 &lt;code&gt;target&lt;/code&gt; 倒序遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int j = target; j &amp;gt;= num; --j) {
    dp[j] = dp[j] || dp[j - num];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不选当前数字：&lt;code&gt;dp[j]&lt;/code&gt; 保持原样&lt;/li&gt;
&lt;li&gt;选当前数字：如果 &lt;code&gt;dp[j - num]&lt;/code&gt; 是 true，则 &lt;code&gt;dp[j]&lt;/code&gt; 也为 true&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;初始化：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[0] = true&lt;/code&gt;：容量为 0 的背包一定可以被“什么都不选”填满&lt;/li&gt;
&lt;li&gt;其余 &lt;code&gt;dp[j] = false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;遍历顺序：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;外层遍历每个物品 &lt;code&gt;i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内层倒序遍历容量 &lt;code&gt;j&lt;/code&gt;，从 &lt;code&gt;target&lt;/code&gt; 到 &lt;code&gt;num&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;倒序是为了确保每个物品最多只能使用一次（0-1背包的特性）&lt;/p&gt;
&lt;h4&gt;最终结果：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;判断 &lt;code&gt;dp[target]&lt;/code&gt; 是否为 &lt;code&gt;true&lt;/code&gt;，表示能否恰好选出和为 &lt;code&gt;sum / 2&lt;/code&gt; 的子集&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;整体代码如下：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canPartition(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2 != 0) return false; // 总和为奇数，肯定不能平分

        int target = sum / 2;
        vector&amp;lt;bool&amp;gt; dp(target + 1, false);
        dp[0] = true; // 初始条件

        for (int num : nums) {
            for (int j = target; j &amp;gt;= num; --j) {
                dp[j] = dp[j] || dp[j - num];
            }
        }

        return dp[target];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;时间复杂度：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：&lt;code&gt;O(N * target)&lt;/code&gt;，其中 &lt;code&gt;N&lt;/code&gt; 为数组长度，&lt;code&gt;target = sum / 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;空间复杂度：&lt;code&gt;O(target)&lt;/code&gt;，使用一维 &lt;code&gt;dp&lt;/code&gt; 数组优化空间&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day34-动态规划 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day34_dynamic_programing_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day34_dynamic_programing_part2/</guid><description>动态规划，不同路径，不同路径 II，整数拆分，不同的二叉搜索树</description><pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;不同路径&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/unique-paths/&quot;&gt;62. 不同路径&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;动态规划&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义 dp 数组：&lt;code&gt;dp[i][j]&lt;/code&gt; 表示从 &lt;code&gt;(0, 0)&lt;/code&gt; 到 &lt;code&gt;(i, j)&lt;/code&gt; 的路径数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最终我们需要求的是右下角的路径数，即 &lt;code&gt;dp[m - 1][n - 1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定递推公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们要到达 &lt;code&gt;(i, j)&lt;/code&gt;，只能从上面 &lt;code&gt;(i - 1, j)&lt;/code&gt; 或者左边 &lt;code&gt;(i, j - 1)&lt;/code&gt; 来。&lt;/li&gt;
&lt;li&gt;所以 &lt;code&gt;dp[i][j] = dp[i - 1][j] + dp[i][j - 1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定初始条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果我们只有一行或者一列，那么只有一种路径。&lt;/li&gt;
&lt;li&gt;所以 &lt;code&gt;dp[0][j] = 1&lt;/code&gt; 和 &lt;code&gt;dp[i][0] = 1&lt;/code&gt;，因此我们可以初始化第一行和第一列为 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;不过也可以将dp数组直接初始化为 &lt;code&gt;1&lt;/code&gt;，因为只有第一行和第一列的值是 &lt;code&gt;1&lt;/code&gt;，其他的值会在后续的计算中被覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定计算顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;m - 1&lt;/code&gt;，从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;n - 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;即从左上角到右下角。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[m - 1][n - 1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int uniquePaths(int m, int n) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(m, vector&amp;lt;int&amp;gt;(n, 0));
        for (int i = 0; i &amp;lt; m; i++) {
            dp[i][0] = 1; // 第一列只能从上方到达
        }
        for (int j = 0; j &amp;lt; n; j++) {
            dp[0][j] = 1; // 第一行只能从左方到达
        }
        // 其实可以全都初始化为1 
        // vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(m, vector&amp;lt;int&amp;gt;(n, 1)); 
        for (int i = 1; i &amp;lt; m; i++) {
            for (int j = 1; j &amp;lt; n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1]; // 返回右下角的路径数
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数学方法&lt;/h3&gt;
&lt;p&gt;其实也就是组合数学的问题。&lt;/p&gt;
&lt;p&gt;从左上角走到右下角需要走 &lt;code&gt;m - 1&lt;/code&gt; 步“下”，&lt;code&gt;n - 1&lt;/code&gt; 步“右”，总共 &lt;code&gt;m + n - 2&lt;/code&gt; 步。
在这些步中选出 &lt;code&gt;m - 1&lt;/code&gt; 步向下，其余为向右，所以总路径数是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;C(m + n - 2, m - 1)&lt;/code&gt; 或者 &lt;code&gt;C(m + n - 2, n - 1)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其中 &lt;code&gt;C(n, k)&lt;/code&gt; 表示从 &lt;code&gt;n&lt;/code&gt; 个元素中选出 &lt;code&gt;k&lt;/code&gt; 个元素的组合数，计算公式为：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;C(m+n-2, m-1) = (m+n-2)! / ((m-1)! * (n-1)!)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int uniquePaths(int m, int n) {
        long long res = 1;
        for (int i = 1; i &amp;lt;= m - 1; ++i) {
            res = res * (n - 1 + i) / i;
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不同路径 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/unique-paths-ii/&quot;&gt;63. 不同路径 II&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;定义 dp 数组：这里的定义其实和&lt;a href=&quot;#%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84&quot;&gt;上面&lt;/a&gt;是一样的&lt;code&gt;dp[i][j]&lt;/code&gt; 表示从 &lt;code&gt;(0, 0)&lt;/code&gt; 到 &lt;code&gt;(i, j)&lt;/code&gt; 的路径数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最终我们需要求的是右下角的路径数，即 &lt;code&gt;dp[m - 1][n - 1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定递推公式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们要到达 &lt;code&gt;(i, j)&lt;/code&gt;，只能从上面 &lt;code&gt;(i - 1, j)&lt;/code&gt; 或者左边 &lt;code&gt;(i, j - 1)&lt;/code&gt; 来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是当 &lt;code&gt;(i, j)&lt;/code&gt; 是障碍物时，&lt;code&gt;dp[i][j] = 0&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此我们要有一个先决条件：&lt;code&gt;if (obstacleGrid[i][j] == 0)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所以在这个条件下 &lt;code&gt;dp[i][j] = dp[i - 1][j] + dp[i][j - 1]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定初始条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果我们只有一行或者一列，那么只有一种路径。&lt;/li&gt;
&lt;li&gt;与之前不同的是，如果第一行或者第一列有障碍物，那么包括障碍物在内之后的路径都是不能到达的。
&lt;ul&gt;
&lt;li&gt;所以我们需要在初始化时判断是否有障碍物，如果有障碍物，那么后面的路径数都初始化为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定计算顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;m - 1&lt;/code&gt;，从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;n - 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;即从左上角到右下角。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int uniquePathsWithObstacles(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();

        if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) return 0; // 起点或终点有障碍物
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dp(m, vector&amp;lt;int&amp;gt;(n, 0));
        for (int i = 0; i &amp;lt; m &amp;amp;&amp;amp; obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1; // 第一列只能从上方到达，如果有障碍物则不初始化
        }
        for (int j = 0; j &amp;lt; n &amp;amp;&amp;amp; obstacleGrid[0][j] == 0; j++) {
            dp[0][j] = 1; // 第一行只能从左方到达，如果有障碍物则不初始化
        }

        for (int i = 1; i &amp;lt; m; i++) {
            for (int j = 1; j &amp;lt; n; j++) {
                if (obstacleGrid[i][j] == 0) { // 递推前先判断当前有没有障碍物，如果没有障碍物
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 上方和左方的路径数之和
                } else {
                    dp[i][j] = 0; // 有障碍物则路径数为0
                }
            }
        }
        return dp[m - 1][n - 1]; // 返回右下角的路径数
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;整数拆分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/integer-break/&quot;&gt;343. 整数拆分&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;状态定义：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;dp[i] 表示将正整数 i 拆分成至少两个正整数之和后，这些数的最大乘积
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;状态转移方程：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对 i 来说，可以拆成 &lt;code&gt;j + (i - j)&lt;/code&gt;，其中 `1 &amp;lt;= j &amp;lt; i。&lt;/p&gt;
&lt;p&gt;那么我们有两种选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不继续拆 &lt;code&gt;i - j&lt;/code&gt;，也就是说乘积为 &lt;code&gt;j * (i - j)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继续拆 &lt;code&gt;i - j&lt;/code&gt;，也就是说乘积为 &lt;code&gt;j * dp[i - j]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以状态转移为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[0] = 0&lt;/code&gt;，实际上没有意义&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[1] = 0&lt;/code&gt;，实际上没有意义&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[2] = 1&lt;/code&gt;，表示拆分成 &lt;code&gt;1 + 1&lt;/code&gt;，乘积为 &lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;遍历顺序：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;外层从 &lt;code&gt;2&lt;/code&gt; 到 &lt;code&gt;n&lt;/code&gt;，内层从 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;i - 1&lt;/code&gt;，也可以从 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;i / 2&lt;/code&gt;，因为 &lt;code&gt;j&lt;/code&gt; 和 &lt;code&gt;i - j&lt;/code&gt; 是对称的。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int integerBreak(int n) {
        if (n &amp;lt;= 2) return 1; // 1和2只能拆分成1
        vector&amp;lt;int&amp;gt; dp(n + 1, 0); // dp[i]表示整数i的最大乘积
        dp[0] = 0;
        dp[1] = 0;
        dp[2] = 1;

        for (int i = 3; i &amp;lt;= n; i++) {
            for (int j = 1; j &amp;lt;= i / 2; j++) {
                // j * (i - j)表示拆分成j和i-j的乘积
                // j * dp[i - j]表示拆分成j和i-j的乘积，i-j可以继续拆分
                // 取两者的最大值
                // 同时因为固定了i，我们还要在当前取dp[i]的最大值
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
            }
        }
        return dp[n]; // 返回整数n的最大乘积  
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;不同的二叉搜索树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/unique-binary-search-trees/&quot;&gt;96. 不同的二叉搜索树&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;状态定义：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;我们定义 dp[i] 表示 i 个节点能组成的不同二叉搜索树的个数。
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;状态转移方程：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设当前总共有 &lt;code&gt;i&lt;/code&gt; 个节点，我们枚举每一个数 &lt;code&gt;j&lt;/code&gt; 当作根节点，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左子树节点数为 &lt;code&gt;j - 1&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右子树节点数为 &lt;code&gt;i - j&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么以 &lt;code&gt;j&lt;/code&gt; 为根的 BST 数量为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[j - 1] * dp[i - j]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对所有可能的根节点求和：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dp[i] = dp[0] * dp[i - 1] + dp[1] * dp[i - 2] + ... + dp[i - 1] * dp[0]
/*这其实是 卡特兰数 的定义*/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[0] = 1&lt;/code&gt;：空树也算一种&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[1] = 1&lt;/code&gt;：只有一个节点，只有一种 BST&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;遍历顺序：
节点数为 &lt;code&gt;i&lt;/code&gt; 的状态是依靠 &lt;code&gt;i&lt;/code&gt; 之前节点数的状态。
外层从 &lt;code&gt;2&lt;/code&gt; 到 &lt;code&gt;n&lt;/code&gt;，内层从 &lt;code&gt;1&lt;/code&gt; 到 &lt;code&gt;i&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numTrees(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, 0); // dp[i]表示i个节点的二叉搜索树的个数
        dp[0] = 1; // 0个节点的二叉搜索树的个数为1（空树）
        dp[1] = 1; // 1个节点的二叉搜索树的个数为1（只有一个节点）

        for (int i = 2; i &amp;lt;=n; i++) {
            for (int j = 1; j &amp;lt;= i; j++) {
                // j作为根节点，左子树有j-1个节点，右子树有i-j个节点
                dp[i] += dp[j - 1] * dp[i - j]; // 左子树和右子树的组合
            }
        }
        return dp[n]; // 返回n个节点的二叉搜索树的个数
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day32-动态规划 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/dp/day32_dynamic_programing_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/dp/day32_dynamic_programing_part1/</guid><description>动态规划，斐波那契数列，爬楼梯，使用最小花费爬楼梯</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;动态规划&lt;/h2&gt;
&lt;p&gt;动态规划（Dynamic Programming，简称DP）是一种用于解决&lt;strong&gt;最优化问题&lt;/strong&gt;的算法设计方法，其核心思想是&lt;strong&gt;将原问题划分为若干子问题，先解决子问题，并利用子问题的解构造原问题的解&lt;/strong&gt;，从而避免重复计算，提高效率。&lt;/p&gt;
&lt;p&gt;动态规划适用于满足以下两个条件的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最优子结构（Optimal Substructure）&lt;/strong&gt;&lt;br /&gt;
原问题的最优解可以由子问题的最优解构成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重叠子问题（Overlapping Subproblems）&lt;/strong&gt;&lt;br /&gt;
原问题在递归求解过程中会反复遇到相同的子问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;动态规划五步曲：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定义状态（确定dp数组（dp table）以及下标的含义）&lt;/strong&gt;：用一个或多个变量来描述子问题的结构，一般以 &lt;code&gt;dp[i]&lt;/code&gt; 或 &lt;code&gt;dp[i][j]&lt;/code&gt; 表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定状态转移方程（确定递推公式）&lt;/strong&gt;：描述如何通过已知状态计算出新的状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定初始条件（dp数组如何初始化）&lt;/strong&gt;：即基础状态的值，作为递推的起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定计算顺序（确定遍历顺序）&lt;/strong&gt;：通常是从小到大递推。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回最终结果（举例推导dp数组）&lt;/strong&gt;：即问题的答案对应的状态值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;斐波那契数列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/fibonacci-number/&quot;&gt;509. 斐波那契数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;用递归的方式来实现斐波那契数列的实现其实很简单，直接递归调用 &lt;code&gt;fib(n - 1) + fib(n - 2)&lt;/code&gt; 即可。
如果使用动态规划的方式来实现，我们按照动态规划五步曲来实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 个斐波那契数的值。&lt;/li&gt;
&lt;li&gt;递推公式：&lt;code&gt;dp[i] = dp[i - 1] + dp[i - 2]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;初始化dp数组：&lt;code&gt;dp[0] = 0, dp[1] = 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遍历顺序：从 &lt;code&gt;2&lt;/code&gt; 到 &lt;code&gt;n&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;返回结果：&lt;code&gt;dp[n]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我们动态规划的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int fib(int n) {
        // 动态规划
        if (n &amp;lt;= 1) return n;
        // dp[i] 表示第 i 个斐波那契数的值
        // 
        vector&amp;lt;int&amp;gt; dp(n + 1, 0); // 
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i &amp;lt;= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
        
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么dp数组要初始化为 dp(n + 1, 0)&lt;/h3&gt;
&lt;p&gt;因为我们需要计算到 &lt;code&gt;n&lt;/code&gt;，所以我们需要一个大小为 &lt;code&gt;n + 1&lt;/code&gt; 的数组来存储从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;n&lt;/code&gt; 的斐波那契数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;假设 n = 5
F(0) = 0  
F(1) = 1  
F(2) = F(1) + F(0)  
F(3) = F(2) + F(1)  
F(4) = F(3) + F(2)  
F(5) = F(4) + F(3)

从 0 到 5 一共需要 6 个数，所以 dp 数组的大小为 n + 1。
数组必须有 n + 1 个元素，才能保证 dp[0] ~ dp[n] 都能合法访问
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;爬楼梯&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/climbing-stairs/&quot;&gt;70. 爬楼梯&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们可以把这个问题简化一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设我们要爬到第3层&lt;/li&gt;
&lt;li&gt;我们可以从第2层爬上来（走一步）&lt;/li&gt;
&lt;li&gt;或者从第1层爬上来（走两步）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我们可以推导出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为爬到第 &lt;code&gt;n&lt;/code&gt; 层楼梯有两种方式：&lt;/li&gt;
&lt;li&gt;从第 &lt;code&gt;n - 1&lt;/code&gt; 层楼梯爬上来（走一步）&lt;/li&gt;
&lt;li&gt;从第 &lt;code&gt;n - 2&lt;/code&gt; 层楼梯爬上来（走两步）&lt;/li&gt;
&lt;li&gt;从而爬到第 &lt;code&gt;n&lt;/code&gt; 层楼梯的方式有 &lt;code&gt;f(n) = f(n - 1) + f(n - 2)&lt;/code&gt;种方式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们可以定义 &lt;code&gt;dp&lt;/code&gt; 数组 &lt;code&gt;dp[i]&lt;/code&gt;，用来表示爬到第 &lt;code&gt;i&lt;/code&gt; 层楼梯的方式有多少种。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;递推公式：&lt;code&gt;dp[i] = dp[i - 1] + dp[i - 2]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这题的思路其实也就转换成了和&lt;a href=&quot;#%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97&quot;&gt;斐波那契数列&lt;/a&gt;一样了。&lt;/p&gt;
&lt;p&gt;接下来是定义初始条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示第 &lt;code&gt;i&lt;/code&gt; 层楼梯的方式&lt;/li&gt;
&lt;li&gt;因为题中描述了 &lt;code&gt;n&lt;/code&gt; 是从 &lt;code&gt;1&lt;/code&gt; 开始的，因此我们直接从 &lt;code&gt;1&lt;/code&gt; 开始定义&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[1] = 1&lt;/code&gt;，爬到第 1 层楼梯只有一种方式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[2] = 2&lt;/code&gt;，爬到第 2 层楼梯有两种方式：
&lt;ul&gt;
&lt;li&gt;走一步到第 1 层，再走一步到第 2 层&lt;/li&gt;
&lt;li&gt;一次性走两步到第 2 层&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[如果一定要初始化dp[0]]
由于到达第0层楼梯在题中并没有实际意义，所以我们可以根据题意，“到达第 2 层有两种方式”，反推出dp[0] = 1
:::&lt;/p&gt;
&lt;p&gt;遍历顺序：因为我们是一层一层往上爬的，所以很自然是从前往后遍历，再根据题意，所以我们从 &lt;code&gt;3&lt;/code&gt; 开始遍历到 &lt;code&gt;n&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以我们的整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int climbStairs(int n) {
        if (n &amp;lt;= 2) return n; // 如果台阶数小于等于2，直接返回n
        vector&amp;lt;int&amp;gt; dp(n + 1, 0); // dp[i]表示到达第i级台阶的方法数
        dp[1] = 1; // 到达第1级台阶的方法数为1
        dp[2] = 2; // 到达第2级台阶的方法数为2
        for (int i = 3; i &amp;lt;= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2]; // 到达第i级台阶的方法数为到达第i-1级台阶和第i-2级台阶的方法数之和
        }
        return dp[n]; // 返回到达第n级台阶的方法数
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用最小花费爬楼梯&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/min-cost-climbing-stairs/&quot;&gt;746. 使用最小花费爬楼梯&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;确定转移状态方程 &lt;code&gt;dp[i]&lt;/code&gt; 和下标 &lt;code&gt;i&lt;/code&gt; 的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 表示到达第 &lt;code&gt;i&lt;/code&gt; 级台阶的最小花费。
根据题意，我们只能选择跳 1 级或 2 级台阶，所以当我们需要跳到第 &lt;code&gt;i&lt;/code&gt; 级台阶时：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从第 &lt;code&gt;i - 1&lt;/code&gt; 级台阶跳上来，花费 &lt;code&gt;cost[i - 1]&lt;/code&gt; 的费用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i] = dp[i - 1] + cost[i - 1]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从第 &lt;code&gt;i - 2&lt;/code&gt; 级台阶跳上来，花费 &lt;code&gt;cost[i - 2]&lt;/code&gt; 的费用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i] = dp[i - 2] + cost[i - 2]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然我们可以从第 &lt;code&gt;i - 1&lt;/code&gt; 级台阶跳上来，也可以从第 &lt;code&gt;i - 2&lt;/code&gt; 级台阶跳上来，有两种选择，根据题意我们需要选择最小的费用，所以我们可以得到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初始化 &lt;code&gt;dp&lt;/code&gt; 数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;题意中：“你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯”  也就是说到达第 0 个台阶是不花费的，但从第 0 个台阶 往上跳的话，需要花费 &lt;code&gt;cost[0]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;所以我们可以定义 &lt;code&gt;dp[0] = 0&lt;/code&gt;，表示到达第 0 个台阶的费用为 0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dp[1] = 0&lt;/code&gt;，表示到达第 1 个台阶的费用为 0。&lt;/li&gt;
&lt;li&gt;也就是默认第一步是不花费体力的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们的整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minCostClimbingStairs(vector&amp;lt;int&amp;gt;&amp;amp; cost) {
        int n = cost.size(); // 台阶数
        vector&amp;lt;int&amp;gt; dp(n + 1, 0); // dp[i]表示到达第i级台阶的最小花费
        dp[0] = 0; // 从下标为 0 或下标为 1 的台阶开始，因此支付费用为0
        dp[1] = 0;
        for (int i = 2; i &amp;lt;= n; i++) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); // 到达第i级台阶的最小花费为到达第i-1级台阶和第i-2级台阶的最小花费之和
        }
        return dp[n]; // 返回到达第n级台阶的最小花费
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important[其实还是要理解题意]
这里其实理解题意比较重要，跳到第 0 级台阶和第 1 级台阶是不需要花费体力的，所以我们可以直接将 &lt;code&gt;dp[0]&lt;/code&gt; 和 &lt;code&gt;dp[1]&lt;/code&gt; 初始化为 0。
:::&lt;/p&gt;
</content:encoded></item><item><title>Day31-贪心算法 part05</title><link>https://m1dnightsun.github.io/posts/programmercarl/greedy/day31_greedy_algorithm_part5/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/greedy/day31_greedy_algorithm_part5/</guid><description>贪心算法，合并区间，单调递增的数字，监控二叉树</description><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;合并区间&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/merge-intervals/&quot;&gt;56. 合并区间&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题的本质其实还是判断重叠区间的问题。&lt;/p&gt;
&lt;p&gt;我们可以先将区间按照起始位置进行排序，然后遍历区间&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前区间的起始位置小于等于上一个区间的结束位置，则说明两个区间重叠，需要进行合并。&lt;/li&gt;
&lt;li&gt;否则，说明两个区间不重叠，可以直接将当前区间加入结果中。&lt;/li&gt;
&lt;li&gt;合并区间的方式是将上一个区间的结束位置更新为两个区间的结束位置的最大值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此整体代码不难&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; merge(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; intervals) {
        if (intervals.size() &amp;lt; 2) return intervals; // 如果区间数量小于2，直接返回
        sort(intervals.begin(), intervals.end(), [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
            return a[0] &amp;lt; b[0]; // 按照区间的起始位置排序
        });
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; merged; // 存储合并后的区间
        merged.push_back(intervals[0]); // 将第一个区间加入结果
        for (int i = 1; i &amp;lt; intervals.size(); i++) {
            // 如果当前区间的起始位置小于等于上一个区间的结束位置，进行合并
            if (intervals[i][0] &amp;lt;= merged.back()[1]) {
                merged.back()[1] = max(merged.back()[1], intervals[i][1]); // 更新结束位置
            } else {
                merged.push_back(intervals[i]); // 否则，直接加入结果
            }
        }
        return merged; // 返回合并后的区间
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;单调递增的数字&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/monotone-increasing-digits/&quot;&gt;738. 单调递增的数字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里的思路是我们从后往前遍历数组，如果当前的数字小于前一个数字(&lt;code&gt;s[i] &amp;lt; s[i - 1]&lt;/code&gt;)，那么我们记录下当前这个位置 &lt;code&gt;flag&lt;/code&gt;，并将前一个数字减1, 重复这个过程知道遍历结束。&lt;/p&gt;
&lt;p&gt;然后我们将 &lt;code&gt;flag&lt;/code&gt; 位置之后的数字全部改为9，最后返回结果。&lt;/p&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    bool check(int n) {
        string s = to_string(n); // 将数字转换为字符串
        for (int i = 1; i &amp;lt; s.size(); i++) { 
            // 如果当前数字小于前一个数字，返回false
            if (s[i] &amp;lt; s[i - 1]) {
                return false;
            }
        }
        return true;
    }
public:
    int monotoneIncreasingDigits(int n) {
        if (check(n)) return n; // 如果已经是单调递增的数字，直接返回
        string s = to_string(n);
        int flag = s.size(); // 标记位置
        for (int i = s.size() - 1; i &amp;gt; 0; i--) { // 从后往前遍历
            // 如果当前数字小于前一个数字，更新标记位置
            if (s[i] &amp;lt; s[i - 1]) {
                flag = i;
                s[i - 1]--; // 前一个数字减1
            }
        }
        // 将标记位置之后的数字全部改为9
        for (int i = flag; i &amp;lt; s.size(); i++) {
            s[i] = &apos;9&apos;;
        }
        return stoi(s); // 将字符串转换为整数并返回
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的是只有从后向前遍历才能重复利用上次比较的结果。&lt;/p&gt;
&lt;h2&gt;监控二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-cameras/&quot;&gt;968. 监控二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;#TODO&lt;/p&gt;
</content:encoded></item><item><title>Day30-贪心算法 part04</title><link>https://m1dnightsun.github.io/posts/programmercarl/greedy/day30_greedy_algorithm_part4/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/greedy/day30_greedy_algorithm_part4/</guid><description>贪心算法，引爆气球，无重叠区间，划分字母区间</description><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;用最少数量的箭引爆气球&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/&quot;&gt;452. 用最少数量的箭引爆气球&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为了尽量使气球重叠，我们可以将气球的右端点进行排序。
我们可以从第一个气球开始，记录当前的箭头位置，然后遍历后面的气球，如果当前气球的左边界大于箭头位置，则需要增加一支箭头，并更新箭头位置为当前气球的右边界。&lt;/p&gt;
&lt;p&gt;如果当前气球的左边界小于等于箭头位置，则说明当前气球可以被当前箭头引爆，所以不需要增加箭头。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部最优解：
&lt;ul&gt;
&lt;li&gt;每次选择右端点最小的气球进行引爆&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;points = [[10,16],[2,8],[1,6],[7,12]]
排序后：
points = [[1,6],[2,8],[7,12],[10,16]]
    
           end        
           ↓
     1 --- 6
     
            2 --- 8       左端点小于end，不需要增加箭头
                        
                      end
                       ↓
                7 --- 12     此时的end为6，左端点大于end，需要增加箭头，并更新end为12

                    10 --- 16   此时左端点小于end，不需要增加箭头
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findMinArrowShots(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; points) {
        if (points.empty()) return 0; // 如果没有气球，返回0
        // 按照气球的结束位置进行排序
        sort(points.begin(), points.end(), [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
            return a[1] &amp;lt; b[1];
        });
        
        int arrows = 1; // 至少需要一支箭
        int end = points[0][1]; // 第一个气球的右边界

        for (int i = 1; i &amp;lt; points.size(); i++) {
            // 如果当前气球的开始位置大于上一个气球的结束位置，增加箭的数量
            if (points[i][0] &amp;gt; end) {
                arrows++;
                end = points[i][1]; // 更新结束位置
            }
        }

        return arrows;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然这里也可以依据左边界进行排序。&lt;/p&gt;
&lt;p&gt;我们同样的使用一个 &lt;code&gt;end&lt;/code&gt; 变量来记录当前箭头的结束位置，也就是当前箭头可以引爆的气球的右边界。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果当前气球的左边界大于 &lt;code&gt;end&lt;/code&gt;，说明当前气球不能被当前箭头引爆，需要增加一支箭头，并更新 &lt;code&gt;end&lt;/code&gt; 为当前气球的右边界。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前气球的左边界小于等于 &lt;code&gt;end&lt;/code&gt;，说明当前气球可以被当前箭头引爆，所以不需要增加箭头。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时我们需要更新 &lt;code&gt;end&lt;/code&gt; 为当前气球的右边界和 &lt;code&gt;end&lt;/code&gt; 的最小值，因为我们要尽量让气球重叠。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findMinArrowShots(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; points) {
        if (points.empty()) return 0; // 如果没有气球，返回0
        // 按照气球的结束位置进行排序
        sort(points.begin(), points.end(), [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
            return a[0] &amp;lt; b[0];
        });
        
        int arrows = 1; // 至少需要一支箭
        int end = points[0][1]; // 记录第一个气球的结束位置
        for (int i = 0; i &amp;lt; points.size(); i++) {
            // 如果当前气球的开始位置大于上一个气球的结束位置，说明需要新的箭
            if (points[i][0] &amp;gt; end) {
                arrows++; // 增加箭的数量
                end = points[i][1]; // 更新结束位置为当前气球的结束位置
            } else {
                // 如果当前气球的开始位置小于等于上一个气球的结束位置，更新结束位置为二者的最小值
                end = min(end, points[i][1]);
            }
        }
        return arrows;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;无重叠区间&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/non-overlapping-intervals/&quot;&gt;435. 无重叠区间&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里的整体思路其实和&lt;a href=&quot;#%E6%97%A0%E9%87%8D%E5%8F%A0%E5%8C%BA%E9%97%B4&quot;&gt;无重叠区间&lt;/a&gt;的思路是一样的，我们同样可以根据结束位置进行排序。
我们可以使用一个 &lt;code&gt;end&lt;/code&gt; 变量来记录当前区间的结束位置，也就是当前区间的右边界。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果当前区间的开始位置大于等于 &lt;code&gt;end&lt;/code&gt;，则说明无重叠，此时我们要更新 &lt;code&gt;end&lt;/code&gt; 为当前区间的右边界。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前区间的开始位置小于 &lt;code&gt;end&lt;/code&gt;，则说明有重叠，此时我们要更新 &lt;code&gt;end&lt;/code&gt; 为当前区间的右边界和 &lt;code&gt;end&lt;/code&gt; 的最小值，并使 &lt;code&gt;count++&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;intervals = [[1,2],[2,3],[3,4],[1,3]]
排序后：
intervals = [[1,2],[1,3],[2,3],[3,4]]
    
         end = 2        
          ↓
    1 ---  2

    1 ----------- 3       左边界小于end，count++，相当于删除了[1,3]，end更新为2（2和3的最小值）
                        
         end = 2
          ↓          
          2 ----- 3     此时的end为2，左边界等于end，说明没有重叠（指[1, 2]），并更新end为右边界3

                  3 --- 4   此时左边界大于end，说明没有重叠，更新end为4
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int eraseOverlapIntervals(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; intervals) {
        if (intervals.size() &amp;lt; 2) return 0; // 如果区间数量小于2，返回0

        sort(intervals.begin(), intervals.end(), [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b){return a[1] &amp;lt; b[1];});

        int count = 0;
        int end = intervals[0][1]; // 记录第一个区间的结束位置

        for (int i = 1; i &amp;lt; intervals.size(); i++) {
            // 如果当前区间的开始位置大于等于上一个区间的结束位置，说明没有重叠
            // 更新结束位置为当前区间的结束位置
            if (intervals[i][0] &amp;gt;= end) {
                end = intervals[i][1]; // 更新结束位置为当前区间的结束位置
            } else {
                count++; // 如果当前区间与上一个区间重叠，增加计数
                end = min(end, intervals[i][1]); // 更新结束位置为二者的最小值
            }
        }
        return count;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;划分字母区间&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/partition-labels/&quot;&gt;763. 划分字母区间&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题的题意可以分解为，给定任意一个字符 &lt;code&gt;ch&lt;/code&gt;，该区间需要包含所有的 &lt;code&gt;ch&lt;/code&gt; 字符。&lt;/p&gt;
&lt;p&gt;我们可以使用一个 &lt;code&gt;map&lt;/code&gt; 来记录每一个字符的最后出现位置。（当然也可以使用一个数组来记录 &lt;code&gt;int LastIndex[26] = -1;&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;使用一个 &lt;code&gt;start&lt;/code&gt; 变量来记录当前区间的开始位置，&lt;code&gt;end&lt;/code&gt; 变量来记录当前区间的结束位置。&lt;/p&gt;
&lt;p&gt;在每次遍历中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新 &lt;code&gt;end&lt;/code&gt; 为当前字符的最后出现位置和 &lt;code&gt;end&lt;/code&gt; 的最大值。&lt;/li&gt;
&lt;li&gt;如果当前字符的下标等于 &lt;code&gt;end&lt;/code&gt;，说明当前区间已经划分完成，此时我们可以将 &lt;code&gt;start&lt;/code&gt; 更新为 &lt;code&gt;i + 1&lt;/code&gt;，并将 &lt;code&gt;end&lt;/code&gt; 重置为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; partitionLabels(string s) {
        unordered_map&amp;lt;char, int&amp;gt; lastIndex; // 记录每个字符最后出现的位置
        for (int i = 0; i &amp;lt; s.size(); i++) {
            lastIndex[s[i]] = i; // 更新字符的最后出现位置
        }

        vector&amp;lt;int&amp;gt; result; // 存储划分的区间长度
        int start = 0; // 当前区间的起始位置
        int end = 0; // 当前区间的结束位置

        for (int i = 0; i &amp;lt; s.size(); i++) {
            end = max(end, lastIndex[s[i]]); // 更新当前区间的结束位置
            if (i == end) { // 如果当前索引等于结束位置，说明可以划分一个区间
                result.push_back(end - start + 1); // 计算区间长度并加入结果
                start = i + 1; // 更新起始位置为下一个字符
            }
        }
        return result; 
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这三题的思路其实都是一个重叠区间的问题，&lt;a href=&quot;#%E7%94%A8%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E7%9A%84%E7%AE%AD%E5%BC%95%E7%88%86%E6%B0%94%E7%90%83&quot;&gt;引爆气球&lt;/a&gt; 和 &lt;a href=&quot;#%E6%97%A0%E9%87%8D%E5%8F%A0%E5%8C%BA%E9%97%B4&quot;&gt;无重叠区间&lt;/a&gt; 是典型的区间选择问题，而&lt;a href=&quot;#%E5%88%92%E5%88%86%E5%AD%97%E6%AF%8D%E5%8C%BA%E9%97%B4&quot;&gt;划分字母区间&lt;/a&gt;是利用区间合并来进行划分的问题。都通过“优先选择最有利的区间”实现了贪心最优化。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题号&lt;/th&gt;
&lt;th&gt;本质问题&lt;/th&gt;
&lt;th&gt;贪心点&lt;/th&gt;
&lt;th&gt;排序依据&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;435&lt;/td&gt;
&lt;td&gt;最少移除重叠区间&lt;/td&gt;
&lt;td&gt;每次保留最早结束的区间&lt;/td&gt;
&lt;td&gt;按 end 升序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;452&lt;/td&gt;
&lt;td&gt;最少箭数射爆气球&lt;/td&gt;
&lt;td&gt;每次一箭射最右的可覆盖区间&lt;/td&gt;
&lt;td&gt;按 end 升序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;763&lt;/td&gt;
&lt;td&gt;最多不重叠分区&lt;/td&gt;
&lt;td&gt;当前分区的最大右边界到达时划分&lt;/td&gt;
&lt;td&gt;字符最远出现位置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>Day29-贪心算法 part03</title><link>https://m1dnightsun.github.io/posts/programmercarl/greedy/day29_greedy_algorithm_part3/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/greedy/day29_greedy_algorithm_part3/</guid><description>贪心算法，加油站，分发糖果，柠檬水找零，根据身高重建队列</description><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;加油站&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/gas-station/&quot;&gt;134. 加油站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们可以设定一个净增的油量，也就是每次到达加油站获取的油量，减去下一次所需要的油量。&lt;/p&gt;
&lt;p&gt;也就是说 &lt;code&gt;total = gas[i] - cost[i]&lt;/code&gt;，然后对 &lt;code&gt;total&lt;/code&gt; 做一个累加，如果 &lt;code&gt;total&lt;/code&gt; 小于 0，则说明无论从哪里开始都无法到达终点。&lt;/p&gt;
&lt;p&gt;反之，如果 &lt;code&gt;total&lt;/code&gt; 大于 0，则我们先不管是从哪里出发的，一定存在一个点 &lt;code&gt;start&lt;/code&gt;，使得从 &lt;code&gt;start&lt;/code&gt; 出发可以到达终点。&lt;/p&gt;
&lt;p&gt;我们可以定义一个变量 &lt;code&gt;total&lt;/code&gt;，用来计算从第 &lt;code&gt;0&lt;/code&gt; 个加油站出发到达终点的净油量。
同时定义一个变量 &lt;code&gt;cur&lt;/code&gt;，用来表示当前的净油量。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;cur&lt;/code&gt; 小于 0，则说明我们已经无法到达下一个加油站了，所以我们需要将 &lt;code&gt;start&lt;/code&gt; 更新为 &lt;code&gt;i + 1&lt;/code&gt;，并将 &lt;code&gt;cur&lt;/code&gt; 重置为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int canCompleteCircuit(vector&amp;lt;int&amp;gt;&amp;amp; gas, vector&amp;lt;int&amp;gt;&amp;amp; cost) {
        int total = 0; // 总的净油量
        int cur = 0;   // 当前段的净油量
        int start = 0; // 起始加油站索引

        for (int i = 0; i &amp;lt; gas.size(); i++) {
            total += gas[i] - cost[i]; // 计算总净油量
            cur += gas[i] - cost[i];   // 计算当前段净油量

            if (cur &amp;lt; 0) { // 当前段净油量不足以到达下一站
                start = i + 1; // 更新起始加油站为下一站
                cur = 0;       // 重置当前段净油量
            }
        }
        // 如果总净油量为正，则说明可以绕行一圈，否则返回 -1
        return total &amp;gt;= 0 ? start : -1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;gas  =  [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
net = gas[i] - cost[i] = [-2, -2, -2, 3, 3]

站点:     0     1     2     3     4
         ↓     ↓     ↓     ↓     ↓
净油量: -2   -2   -2   +3   +3

cur:   &amp;lt;---当前段累计油量差

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我们只关心有没有一个起点 start，从这个起点出发，走一圈能不能回到起点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cur&lt;/code&gt; 每次失败就重置成 0，表示当前这段肯定不能作为起点，那我们就跳过。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后如果 &lt;code&gt;total &amp;gt;= 0&lt;/code&gt;，说明全局油量足够绕一圈，这时候 start 一定就是那个唯一可行的起点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分发糖果&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/candy/&quot;&gt;135. 分发糖果&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题目要求分发糖果，规则如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个孩子至少分配一个糖果。&lt;/li&gt;
&lt;li&gt;相邻的孩子中，评分更高的孩子必须获得更多的糖果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们可以初始化一个数组将每人分得1个糖果，然后分两次遍历数组：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从左到右遍历&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前孩子的评分比左边的孩子高，则当前孩子的糖果数应该比左边孩子多 1。&lt;/li&gt;
&lt;li&gt;这样可以保证从左到右的规则满足。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从右到左遍历&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前孩子的评分比右边的孩子高，则当前孩子的糖果数应该比右边孩子多 1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同时需要取当前糖果数和右边孩子糖果数 +1 的最大值&lt;/strong&gt;，以确保不覆盖从左到右遍历的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;计算总糖果数&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遍历糖果数组，将所有孩子的糖果数累加即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int candy(vector&amp;lt;int&amp;gt;&amp;amp; ratings) {
        int n = ratings.size();
        vector&amp;lt;int&amp;gt; candies(n, 1); // 每个孩子至少分配一个糖果

        // 从左到右遍历，确保右边评分更高的孩子糖果更多
        for (int i = 1; i &amp;lt; n; i++) {
            if (ratings[i] &amp;gt; ratings[i - 1]) { // 当前孩子评分高于左边孩子
                candies[i] = candies[i - 1] + 1; // 当前孩子糖果数比左边孩子多 1
            }
        }
        // 从右到左遍历，确保左边评分更高的孩子糖果更多
        for (int i = n - 2; i &amp;gt;= 0; i--) { // 从倒数第二个孩子开始遍历
            if (ratings[i] &amp;gt; ratings[i + 1]) { // 当前孩子评分高于右边孩子
                // 如果 ratings[i] &amp;gt; ratings[i + 1]，此时candy[i]（第i个小孩的糖果数量）就有两个选择
                // 一个是candy[i + 1] + 1（从右边这个加1得到的糖果数量）
                // 一个是candyVec[i]（之前比较右孩子大于左孩子得到的糖果数量）
                // 那么我们取局部最优，也就是取两者的最大值
                candies[i] = max(candies[i], candies[i + 1] + 1);
            }
        }
        // 计算糖果总数
        return accumulate(candies.begin(), candies.end(), 0);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此这题其实利用了两次贪心策略，第一次是从左到右，每次保证右边的孩子比左边的孩子多 1，第二次是从右到左，每次保证左边的孩子比右边的孩子多 1，同时还要考虑在左到右过程中可能已经被加过的糖果，因此我们取两者的最大值。&lt;/p&gt;
&lt;h2&gt;柠檬水找零&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/lemonade-change/&quot;&gt;860. 柠檬水找零&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们根据题意，对于客户支付的情况一共有三种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;支付5：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不需要找零，并收下5美元&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支付10：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只能找零5美元，并收下10美元&lt;/li&gt;
&lt;li&gt;如果没有5美元的零钱，则无法找零，返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支付20：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要找零15美元
&lt;ul&gt;
&lt;li&gt;如果有10美元和5美元的零钱，则找零10美元和5美元&lt;/li&gt;
&lt;li&gt;如果没有10美元的零钱，则需要找零3个5美元&lt;/li&gt;
&lt;li&gt;如果都不满足，则无法找零，返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果没有5美元的零钱，则无法找零，返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们的局部最优情况在于，当支付20美元时，优先找10美元和5美元的零钱，这样可以保证后续支付5美元和10美元时有足够的零钱可找，因为5美元能够用来找零的情况更多。因此遇到账单20，优先消耗美元10，完成本次找零。&lt;/p&gt;
&lt;p&gt;所以我们可以维护两个变量 &lt;code&gt;five&lt;/code&gt; 和 &lt;code&gt;ten&lt;/code&gt;，分别表示5美元和10美元的零钱数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool lemonadeChange(vector&amp;lt;int&amp;gt;&amp;amp; bills) {
        int five = 0, ten = 0; // 记录5美元和10美元的数量

        for (int bill : bills) {
            if (bill == 5) {
                five++; // 收到5美元，不需要找零
            } 
            else if (bill == 10) {
                if (five &amp;gt; 0) { // 找零1张5美元，并收下10美元
                    five--;
                    ten++;
                } 
                else {
                    return false; // 无法找零
                }
            } 
            else if (bill == 20) {
                if (ten &amp;gt; 0 &amp;amp;&amp;amp; five &amp;gt; 0) { // 优先找零1张10美元和1张5美元
                    ten--;
                    five--;
                } 
                else if (five &amp;gt;= 3) { // 否则找零3张5美元
                    five -= 3;
                } 
                else {
                    return false; // 无法找零
                }
            }
        }
        return true; // 所有顾客都成功找零
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;根据身高重建队列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/queue-reconstruction-by-height/&quot;&gt;406. 根据身高重建队列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题的题意其实是，需要重新构造一个队列，使得每个人的前面有 &lt;code&gt;k&lt;/code&gt; 个人的身高大于等于他。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result = [ ?, ?, ?, ?, ?, ? ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使得对于每一个人在这个 result 队列中:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;他前面有正好 k 个“身高 ≥ 他身高”的人
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以先将所有人按照身高从高到低排序，如果身高相同，则按照 &lt;code&gt;k&lt;/code&gt; 从小到大排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; reconstructQueue(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; people) {
        // 按身高降序排序，如果身高相同则按 k 值升序排序
        sort(people.begin(), people.end(), [](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
            return a[0] &amp;gt; b[0] || (a[0] == b[0] &amp;amp;&amp;amp; a[1] &amp;lt; b[1]);
        });

        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; result;
        // 按照 k 值插入到结果队列中
        for (const auto&amp;amp; person : people) {
            result.insert(result.begin() + person[1], person);
        }

        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day28-贪心算法 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/greedy/day28_greedy_algorithm_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/greedy/day28_greedy_algorithm_part2/</guid><description>贪心算法，买卖股票的最佳时机 II，跳跃游戏I &amp;&amp; II，K次取反后最大化的数组和</description><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;买卖股票的最佳时机 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/&quot;&gt;122. 买卖股票的最佳时机 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里的想法其实容易陷入一个误区，就是我们一般都会认为在最低点买入，在最高点卖出，这样就能获得最大的收益。&lt;/p&gt;
&lt;p&gt;而其实我们可以在每一个上升的区间内进行交易，来获得最大的收益。&lt;/p&gt;
&lt;p&gt;当我们把利润分解为以每天为单位的利润时，我们就可以把每一天的利润都加起来，来获得最大的收益。&lt;/p&gt;
&lt;p&gt;局部最优解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收集每天的正利润
全局最优解：&lt;/li&gt;
&lt;li&gt;收集所有的正利润&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们在第 &lt;code&gt;i&lt;/code&gt; 天买入，第 &lt;code&gt;j&lt;/code&gt; 天卖出，那么我们可以将利润分解为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;利润 = 第 j 天的价格 - 第 i 天的价格
     = (第 j 天的价格 - 第 (j-1) 天的价格) + (第 (j-1) 天的价格 - 第 (j-2) 天的价格) + ... + (第 i+1 天的价格 - 第 i 天的价格)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以其实我们只要判断当前的价格是否比前一天的价格高，如果是，就将差值加入到利润中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxProfit(vector&amp;lt;int&amp;gt;&amp;amp; prices) {
        if (prices.size() &amp;lt; 2) return 0;
        int sum = 0;
        for (int i = 1; i &amp;lt; prices.size(); i++) {
            // 如果今天的价格比昨天高，则计算利润
            if (prices[i] &amp;gt; prices[i - 1]) {
                sum += prices[i] - prices[i - 1];
            }
        }
        return sum;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跳跃游戏&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/jump-game/&quot;&gt;55. 跳跃游戏&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里重要的不是每次跳多少步，而是跳跃的范围能否覆盖最后一个元素。&lt;/p&gt;
&lt;p&gt;我们可以定义一个变量 &lt;code&gt;maxReach&lt;/code&gt;，表示当前能跳到的最远位置。
&lt;code&gt;i + nums[i]&lt;/code&gt; 表示当前下标 &lt;code&gt;i&lt;/code&gt; 能跳到的最远位置。
如果当前的下标 &lt;code&gt;i&lt;/code&gt; 大于 &lt;code&gt;maxReach&lt;/code&gt;，说明无法到达最后一个元素，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;55%E8%B7%B3%E8%B7%83%E9%97%AE%E9%A2%98.png&quot; alt=&quot;跳跃问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么我们只要在遍历数组的过程中，如果遍历到尾了，那么返回 &lt;code&gt;true&lt;/code&gt;就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canJump(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (nums.size() == 1) return true; // 只有一个元素，返回true
        int maxReach = 0;
        for (int i = 0; i &amp;lt; nums.size(); i++) {
            if (maxReach &amp;lt; i) { // 如果跳跃的范围到不了当前的下标
                return false; // 直接返回false
            }
            else { // 否则更新当前跳跃的最大范围
                maxReach = max(maxReach, i + nums[i]);
            }
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这道题目关键点在于不用拘泥于每次究竟跳几步，而是看覆盖范围，覆盖范围内一定是可以跳过来的，不用管是怎么跳的。&lt;/p&gt;
&lt;h2&gt;跳跃游戏 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/jump-game-ii/&quot;&gt;45. 跳跃游戏 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题的核心是用贪心思想模拟 BFS 的层次遍历，每次“跳跃”都尽可能跳得远。&lt;/p&gt;
&lt;p&gt;我们定义以下几个变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;curEnd&lt;/code&gt;：当前这一跳所能到达的最远位置（当前“层”的边界）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;curFarthest&lt;/code&gt;：下一跳所能到达的最远位置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jumps&lt;/code&gt;：跳跃次数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把每次跳跃看作 BFS 中的一“层”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;curEnd&lt;/code&gt; 是当前层的最远边界，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;curFarthest&lt;/code&gt; 是我们能探索到的下一层最远边界，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一旦我们走到了 &lt;code&gt;curEnd&lt;/code&gt;，说明该跳一次了（从当前层跳到下一层），此时就增加一次跳跃次数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int jump(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (nums.size() == 1) return 0;
        int jumps = 0;
        int curEnd = 0;
        int curFarthest = 0;
        int n = nums.size();
        for (int i = 0; i &amp;lt; n - 1; ++i) {
            curFarthest = max(curFarthest, i + nums[i]);  // 能跳的最远
            if (i == curEnd) {  // 到了当前这跳的最远位置
                jumps++;            // 需要跳一次
                curEnd = curFarthest;  // 更新下一跳的边界
            }
        }
        return jumps;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以把这个过程想象成一个 BFS 的过程：&lt;/p&gt;
&lt;p&gt;假定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nums = [2, 3, 1, 1, 4]
index:  0   1   2   3   4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换成树结构，格式是：节点(值) → 子节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         0(2)
        /   \
     1(3)   2(1)
    / | \     \
 3(1) 4(4)     3(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最短路径是：0 → 1 → 4，即跳跃次数为2。&lt;/p&gt;
&lt;h2&gt;K次取反后最大化的数组和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/&quot;&gt;1005. K 次取反后最大化的数组和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们的目标是尽可能多地把负数变成正数，因为正数才对总和有贡献。&lt;/p&gt;
&lt;p&gt;步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;把数组按绝对值从大到小排序；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从前往后遍历，如果当前是负数且还有操作次数，就取反；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果最后还剩奇数个 &lt;code&gt;k&lt;/code&gt;，对最后一个元素（绝对值最小的）再取反一次；如果是偶数个 &lt;code&gt;k&lt;/code&gt;，则不需要再取反，因为两个负号抵消了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int largestSumAfterKNegations(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        // 按照绝对值从大到小排序
        sort(nums.begin(), nums.end(), [](int a, int b) { //使用lamda表达式进行排序
            return abs(a) &amp;gt; abs(b);
        });
        for (int i = 0; i &amp;lt; nums.size(); ++i) {
            if (nums[i] &amp;lt; 0 &amp;amp;&amp;amp; k &amp;gt; 0) {
                nums[i] = -nums[i];  // 取反负数
                --k;
            }
        }
        // 如果 k 还剩奇数次，对最小的那个数取反（一定是最后一个）
        if (k % 2 == 1) {
            nums[nums.size() - 1] = -nums[nums.size() - 1];
        }
        return accumulate(nums.begin(), nums.end(), 0);
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day27-贪心算法 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/greedy/day27_greedy_algorithm_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/greedy/day27_greedy_algorithm_part1/</guid><description>贪心算法，分发饼干，摆动序列，最大子数组和</description><pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;贪心算法基础&lt;/h2&gt;
&lt;p&gt;贪心算法（Greedy Algorithm）是一种在每一步选择中都采取当前状态下最优或最有利的选择，从而希望通过一系列局部最优的选择，最终得到全局最优解的方法。&lt;/p&gt;
&lt;p&gt;贪心算法的核心思想是利用局部最优解推导出全局最优解。它通常用于解决一些最优化问题，如最小生成树、最短路径、活动选择等。贪心算法的关键在于选择标准和选择过程，选择标准决定了每一步的选择，而选择过程则是根据当前状态进行决策。&lt;/p&gt;
&lt;p&gt;它的工作方式是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从问题的某个初始状态出发；&lt;/li&gt;
&lt;li&gt;在每一步中都做出一个局部最优选择（即当前看来最好的选择）；&lt;/li&gt;
&lt;li&gt;求出每一步的的最优解；&lt;/li&gt;
&lt;li&gt;最终得到一个可行解， &lt;strong&gt;但不一定是最优解&lt;/strong&gt;，这取决于问题本身是否适合贪心策略。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;贪心算法并不总是适用，它适用于具有“贪心选择性质”和“最优子结构”的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;贪心选择性质：可以通过局部最优的选择构造出全局最优解；&lt;/li&gt;
&lt;li&gt;最优子结构：一个问题的最优解包含其子问题的最优解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分发饼干&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/assign-cookies/&quot;&gt;455. 分发饼干&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们可以对两个数组进行排序，然后在每一次试行中，将当前最小的饼干分配给当前最小的孩子，如果当前最小的饼干不能满足当前最小的孩子，那么就将下一个饼干分配给当前最小的孩子。&lt;/p&gt;
&lt;p&gt;所以这里的代码其实很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findContentChildren(vector&amp;lt;int&amp;gt;&amp;amp; g, vector&amp;lt;int&amp;gt;&amp;amp; s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int count = 0;
        int i = 0, j = 0;
        while (i &amp;lt; g.size() &amp;amp;&amp;amp; j &amp;lt; s.size()) {
            if (g[i] &amp;lt;= s[j]) { // 当前饼干值满足当前胃口值
                count++;
                i++;
                j++;
            }
            else { // 当前饼干值不满足当前胃口值，小孩不动，饼干前移
                j++;
            }
        }
        return count;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者是，尽量使用大的饼干去满足胃口大的小孩，其思路是一样的。&lt;/p&gt;
&lt;h2&gt;摆动序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/wiggle-subsequence/&quot;&gt;376. 摆动序列&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;贪心算法&lt;/h2&gt;
&lt;p&gt;局部最优：删除单调坡度上的节点（不包括单调坡度两端的节点），那么这个坡度就可以有两个局部峰值。&lt;/p&gt;
&lt;p&gt;整体最优：整个序列有最多的局部峰值，从而达到最长摆动序列。&lt;/p&gt;
&lt;p&gt;在计算是否有峰值的时候，计算 &lt;code&gt;prediff(nums[i] - nums[i-1])&lt;/code&gt; 和 &lt;code&gt;curdiff(nums[i+1] - nums[i])&lt;/code&gt;，如果 &lt;code&gt;prediff &amp;lt; 0 &amp;amp;&amp;amp; curdiff &amp;gt; 0&lt;/code&gt; 或者 &lt;code&gt;prediff &amp;gt; 0 &amp;amp;&amp;amp; curdiff &amp;lt; 0&lt;/code&gt; 此时就有波动就需要统计。&lt;/p&gt;
&lt;p&gt;那么这里有三种情况需要考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上下坡中有平坡&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20230106170449.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://file.kamacoder.com/pics/20230106172613.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在图中，当 &lt;code&gt;i&lt;/code&gt; 指向第一个 2 的时候，&lt;code&gt;prediff &amp;gt; 0 &amp;amp;&amp;amp; curdiff = 0&lt;/code&gt; ，当 &lt;code&gt;i&lt;/code&gt; 指向最后一个 2 的时候 &lt;code&gt;prediff = 0 &amp;amp;&amp;amp; curdiff &amp;lt; 0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果我们采用，删左面三个 2 的规则，那么 当 &lt;code&gt;prediff = 0 &amp;amp;&amp;amp; curdiff &amp;lt; 0&lt;/code&gt; 也要记录一个峰值，因为他是把之前相同的元素都删掉留下的峰值。&lt;/p&gt;
&lt;p&gt;所以我们记录峰值的条件应该是： &lt;code&gt;(preDiff &amp;lt;= 0 &amp;amp;&amp;amp; curDiff &amp;gt; 0) || (preDiff &amp;gt;= 0 &amp;amp;&amp;amp; curDiff &amp;lt; 0)&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数组首位两端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20201124174357612.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为我们在计算 &lt;code&gt;prediff(nums[i] - nums[i-1])&lt;/code&gt; 和 &lt;code&gt;curdiff(nums[i+1] - nums[i])&lt;/code&gt; 的时候，至少需要三个数字才能计算，而数组只有两个数字。&lt;/p&gt;
&lt;p&gt;这里我们可以写死，就是如果只有两个元素，且元素不同，那么结果为 2。&lt;/p&gt;
&lt;p&gt;因此我们可以规定，最前面还有一个数字，他的值等于 &lt;code&gt;nums[0]&lt;/code&gt;，这样它就有坡度了即 &lt;code&gt;preDiff = 0&lt;/code&gt;。
针对以上情形，result 初始为 1（默认最右面有一个峰值），此时 &lt;code&gt;curDiff &amp;gt; 0 &amp;amp;&amp;amp; preDiff &amp;lt;= 0&lt;/code&gt;，那么 &lt;code&gt;result++&lt;/code&gt;（计算了左面的峰值），最后得到的 &lt;code&gt;result&lt;/code&gt; 就是 2（峰值个数为 2 即摆动序列长度为 2）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单调坡中有平坡&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20230108171505.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中，我们可以看出，如果用情况1在三个地方记录峰值，但其实结果因为是 2，因为单调中的平坡不能算峰值（即摆动）。&lt;/p&gt;
&lt;p&gt;我们只需要在这个坡度摆动变化的时候，更新 &lt;code&gt;prediff&lt;/code&gt; 就行，这样 &lt;code&gt;prediff&lt;/code&gt; 在 单调区间有平坡的时候就不会发生变化.&lt;/p&gt;
&lt;p&gt;所以整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int wiggleMaxLength(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (nums.size() &amp;lt;= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值，默认最前面还有一个数等于nums[0]
        int result = 1;  // 记录峰值个数，序列默认序列最右边有一个峰值
        for (int i = 0; i &amp;lt; nums.size() - 1; i++) { // 到倒数第二个
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((preDiff &amp;lt;= 0 &amp;amp;&amp;amp; curDiff &amp;gt; 0) || (preDiff &amp;gt;= 0 &amp;amp;&amp;amp; curDiff &amp;lt; 0)) {
                result++;
                preDiff = curDiff; // 注意这里，只在摆动变化的时候更新prediff
            }
        }
        return result;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;贪心 + 动态规划&lt;/h3&gt;
&lt;p&gt;我们维护两个变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;up[i]&lt;/code&gt;：以第 &lt;code&gt;i&lt;/code&gt; 个元素结尾，最后一个波动是上升时的最长摆动序列长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;down[i]&lt;/code&gt;：以第 &lt;code&gt;i&lt;/code&gt; 个元素结尾，最后一个波动是下降时的最长摆动序列长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up[i]&lt;/code&gt; 和 &lt;code&gt;down[i]&lt;/code&gt; 的初始值都为1，因为单个元素本身就是一个摆动序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但我们可以把它进一步简化成两个变量 &lt;code&gt;up&lt;/code&gt; 和 &lt;code&gt;down&lt;/code&gt;，因为我们只关心上一个状态（不需要保存整个数组）。&lt;/p&gt;
&lt;p&gt;具体思路&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;nums[i] &amp;gt; nums[i-1]&lt;/code&gt;，说明当前是一个上升，那么我们希望它接在一个下降后面，才能构成「摆动」。
&lt;ul&gt;
&lt;li&gt;所以：&lt;code&gt;up = down + 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;nums[i] &amp;lt; nums[i-1]&lt;/code&gt;，说明当前是一个下降，希望它接在一个上升后面：
&lt;ul&gt;
&lt;li&gt;所以：&lt;code&gt;down = up + 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果两者相等，那就忽略它（不会更新任何一个）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们以题中给出的例子 &lt;code&gt;nums = [1, 7, 4, 9, 2, 5]&lt;/code&gt; 为例，输入：&lt;code&gt;nums = [1, 7, 4, 9, 2, 5]&lt;/code&gt;，&lt;code&gt;i&lt;/code&gt; 从 1 开始：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i&lt;/th&gt;
&lt;th&gt;nums[i]&lt;/th&gt;
&lt;th&gt;nums[i]-nums[i-1]&lt;/th&gt;
&lt;th&gt;up&lt;/th&gt;
&lt;th&gt;down&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;6  ↑&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;-3 ↓&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;5  ↑&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;-7 ↓&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3  ↑&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int wiggleMaxLength(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
    if (nums.size() &amp;lt; 2) return nums.size(); // 只有一个元素，返回1
    int up = 1, down = 1; // up和down的初始值都为1，因为单个元素本身就是一个摆动序列
    for (int i = 1; i &amp;lt; nums.size(); ++i) { // 从第二个元素开始遍历
        if (nums[i] &amp;gt; nums[i - 1]) up = down + 1; // 当前是一个上升，那么我们希望它接在一个下降后面，才能构成「摆动」
        else if (nums[i] &amp;lt; nums[i - 1]) down = up + 1; // 当前是一个下降，希望它接在一个上升后面
    }
    // 整个过程中并不知道最终的波动是“升”还是“降”
    // 所以我们需要记录两种情况的最大长度
    // 最终返回 max(up, down)，才是整个序列中最长的摆动子序列长度。
    return max(up, down);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最大子数组和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-subarray/&quot;&gt;53. 最大子数组和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;局部最优：当前“连续和”为负数的时候立刻放弃，从下一个元素重新计算“连续和”，因为负数加上下一个元素 “连续和”只会越来越小。&lt;/p&gt;
&lt;p&gt;因此我们可以遍历 &lt;code&gt;nums&lt;/code&gt; 数组，计算当前的连续和，如果当前的连续和小于 0，那么就将当前的连续和置为 0, 并且从下一个元素重新计算连续和。&lt;/p&gt;
&lt;p&gt;同时我们还需要维护一个变量 &lt;code&gt;maxSum&lt;/code&gt;，用来记录当前的最大连续和，因此不会出现类似错过某一个最大连续和的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxSubArray(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int maxSum = INT_MIN; // 记录当前的最大连续和
        int curSum = 0; // 记录当前的连续和
        for (int i = 0; i &amp;lt; nums.size(); i++) {
            curSum += nums[i]; // 当前的连续和
            maxSum = max(maxSum, curSum); // 更新最大连续和
            if (curSum &amp;lt; 0) { // 如果当前的连续和小于0，那么就放弃当前的连续和，从下一个元素重新计算
                curSum = 0;
            }
        }
        return maxSum;
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day26-回溯算法 part05</title><link>https://m1dnightsun.github.io/posts/programmercarl/backtracking/day26_backtracking_part5/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/backtracking/day26_backtracking_part5/</guid><description>回溯算法，重新安排行程，N 皇后，解数独</description><pubDate>Sun, 06 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;重新安排行程&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/reconstruct-itinerary/&quot;&gt;332. 重新安排行程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;题意：现在我们一堆「机票」，每张机票都是一个 出发机场 -&amp;gt; 到达机场，比如 [&quot;JFK&quot;, &quot;SFO&quot;] 表示从 JFK 飞到 SFO。&lt;/p&gt;
&lt;p&gt;要求你用所有机票安排一个「完整的行程」，而且：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;必须从 &quot;JFK&quot; 出发。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果有多种行程方案可以使用完所有机票，就选字典序最小的那个（比如 A &amp;lt; B &amp;lt; C...）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这本质是一个 &lt;strong&gt;图的欧拉路径问题&lt;/strong&gt;（所有边恰好走一次），并结合了 字典序优先遍历（DFS + 小顶堆）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;把机票变成一张图（邻接表），从出发点 &quot;JFK&quot; 开始 DFS。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次从当前机场出发，优先访问「按字典序最小」的下一站。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回溯构建路径。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用回溯 + map映射&lt;/h3&gt;
&lt;p&gt;首先我们要构建机场的和机场-机票的映射关系，这里我们可以使用 &lt;code&gt;unordered_map&lt;/code&gt; 来存储机场和各机场的机票数量的关系。&lt;/p&gt;
&lt;p&gt;各机场的机票数量可以使用 &lt;code&gt;map&amp;lt;string, int&amp;gt;&lt;/code&gt; 来存储，&lt;code&gt;key&lt;/code&gt; 为机场名，&lt;code&gt;value&lt;/code&gt; 为机票数量。&lt;/p&gt;
&lt;p&gt;所以我们的这个映射关系可以使用 &lt;code&gt;unordered_map&amp;lt;string, map&amp;lt;string, int&amp;gt;&amp;gt;&lt;/code&gt; 来存储。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unordered_map&amp;lt;string, map&amp;lt;string, int&amp;gt;&amp;gt; targets;

// 初始化target
for (auto&amp;amp; ticket : tickets) {
    targets[ticket[0]][ticket[1]]++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;targets[出发机场][到达机场] = 这条航线剩余多少张票

例如：tickets = [[&quot;JFK&quot;, &quot;ATL&quot;], [&quot;JFK&quot;, &quot;SFO&quot;], [&quot;ATL&quot;, &quot;SFO&quot;]]

初始化之后会变成：

targets = {
    &quot;JFK&quot;: {
        &quot;ATL&quot;: 1,
        &quot;SFO&quot;: 1
    },
    &quot;ATL&quot;: {
        &quot;SFO&quot;: 1
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后是回溯函数的实现，回溯函数的参数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool backTacking(int ticketNum, vector&amp;lt;string&amp;gt;&amp;amp; path)

- ticketNum：表示当前已经使用的机票数量
- path：表示当前的路径

回溯函数的返回值为 bool，表示是否找到了一条完整的路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回溯的终止条件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (ticketNum + 1== path.size()) {
    return true;
}

当机票数量加1等于搜索的路径长度时，说明找到了完整的路径。换一种说法就是找到了一条欧拉路径。
假如最后我们的路径是：[&quot;JFK&quot;, &quot;SFO&quot;, &quot;ATL&quot;]，那么机票数量 ticketNum = 2，路径长度 path.size() = 3。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单层搜索的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (auto&amp;amp; target : targets[path.back()]) {
    // path.back(): path中的最后一个元素，也就是当前遍历到的机场
    // target[path.back()]: 当前机场的每条航线和机票数量，也就是遍历当前机场的每条航线
    // type(tar[path.back()]): map&amp;lt;string, int&amp;gt;
    if (target.second &amp;gt; 0) { // 如果遍历的当前航线机票还有剩余
        path.push_back(target.first); // 选择目的地，加入到path中
        target.second--; // 机票数量 - 1
        if (backTacking(ticketNum, path)) return true; // 如果递归刚加入的机场返回true，则继续向上返回
        target.second++; // 回溯 机票数量 + 1
        path.pop_back(); // 撤销目的地选择
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们以初始化后的targets为例，可以有以下树状图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;targets = {
    &quot;JFK&quot;: {
        &quot;ATL&quot;: 1,
        &quot;SFO&quot;: 1
    },
    &quot;ATL&quot;: {
        &quot;SFO&quot;: 1
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%80%9D%E8%B7%AF1%E5%9B%9E%E6%BA%AF.png&quot; alt=&quot;思路1回溯&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    unordered_map&amp;lt;string, map&amp;lt;string, int&amp;gt;&amp;gt; targets;
    vector&amp;lt;string&amp;gt; ans;

    bool backTacking(int ticketNum, vector&amp;lt;string&amp;gt;&amp;amp; path) {
        if (ticketNum + 1 == path.size()) { // 如果机票数量 + 1 == 机场数量，说明找到了一条可行的路径，也就是找到了一条欧拉路径
            return true;
        }
        for (auto&amp;amp; target : targets[path.back()]) {
            // path.back(): path中的最后一个元素，也就是当前遍历到的机场
            // target[path.back()]: 当前机场的每条航线和机票数量，也就是遍历当前机场的每条航线
            // type(tar[path.back()]): map&amp;lt;string, int&amp;gt;
            if (target.second &amp;gt; 0) { // 如果遍历的当前航线机票还有剩余
                path.push_back(target.first); // 选择目的地
                target.second--; // 机票数量 - 1
                if (backTacking(ticketNum, path)) return true; // 如果递归刚加入的机场返回true，则继续向上返回
                target.second++; // 回溯 机票数量 + 1
                path.pop_back(); // 撤销目的地选择
            }
        }
        return false; // 否则返回false，说明没有找到
    }
public:
    vector&amp;lt;string&amp;gt; findItinerary(vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt;&amp;amp; tickets) {
        targets.clear();
        ans.clear();
        // 初始化targets
        for (const auto ticket : tickets) {
            // 初始化unordered_map
            // ticket[0]: 机场名
            // ticket[1]: 该机场对应的航班以及机票数量，是一个map&amp;lt;string, int&amp;gt;
            // 相当于从初始化有向图
            targets[ticket[0]][ticket[1]]++;
        }
        ans.push_back(&quot;JFK&quot;);
        backTacking(tickets.size(), ans);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用链表 + 优先级队列&lt;/h3&gt;
&lt;p&gt;这种做法其实是基于有向图的 DFS + 小顶堆（优先级队列）来实现的。属于是一种 &lt;strong&gt;Hierholzer 算法&lt;/strong&gt;的变种，用来找欧拉路径，但加入了一个要求：每次访问字典序最小的边。&lt;/p&gt;
&lt;p&gt;Hierholzer 算法主要思路是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在图中从起点开始一路走下去，每次都选择字典序最小的下一站，走完一条路径后“回溯地插入”路径到最终结果中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据结构的选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用优先队列来存储每个机场的目的地
unordered_map&amp;lt;string, priority_queue&amp;lt;string, vector&amp;lt;string&amp;gt;, greater&amp;lt;string&amp;gt;&amp;gt;&amp;gt; graph;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;key&lt;/code&gt; 是出发机场&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;value&lt;/code&gt; 是一个小顶堆（优先队列），每次能快速拿到字典序最小的目的地&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的含义是：建立一个从出发机场 &lt;code&gt;string&lt;/code&gt; 到多个目的地 &lt;code&gt;string&lt;/code&gt; 的映射，并且这些目的地是按字母排序的优先级队列（小顶堆），这样可以随时快速找到“字典序最小的下一个目的地”。&lt;/p&gt;
&lt;p&gt;第一层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unordered_map&amp;lt;string, ...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这是一张哈希表（无序 map），&lt;/li&gt;
&lt;li&gt;key 是 出发机场（比如 &quot;JFK&quot;、&quot;LHR&quot; 等）；&lt;/li&gt;
&lt;li&gt;value 是从这个机场出发所能到达的所有机场的集合（你要用优先队列来维护这些目的地）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;priority_queue&amp;lt;string, vector&amp;lt;string&amp;gt;, greater&amp;lt;string&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 C++ 的 小顶堆写法，它的含义是：一个会自动维护“字典序最小”的字符串在队首的优先队列。&lt;/p&gt;
&lt;p&gt;默认 &lt;code&gt;priority_queue&lt;/code&gt; 是大顶堆，也就是“最大的在前面”
所以我们要自己写成小顶堆（最小字母在前）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vector&amp;lt;string&amp;gt;&lt;/code&gt; 是底层容器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;greater&amp;lt;string&amp;gt;&lt;/code&gt; 是比较函数（实现小顶堆）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 优先级队列数据结构预览
graph[&quot;JFK&quot;] = {&quot;ATL&quot;, &quot;SFO&quot;, &quot;LHR&quot;} // 自动按字母顺序管理
graph[&quot;LHR&quot;] = {&quot;SFO&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;/// 使用链表来保存最终结果
list&amp;lt;string&amp;gt; itinerary;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用链表 &lt;code&gt;list&lt;/code&gt; 来保存最终结果，方便在路径前面插入（从后往前构建路径）&lt;/p&gt;
&lt;p&gt;深度优先搜索（DFS）过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Hierholzer 算法的 DFS 过程
void dfs(const string&amp;amp; airport) {
        auto&amp;amp; pq = graph[airport]; // 引用当前机场对应的优先队列
        // 当还有目的地没访问时，一直往下走
        while (!pq.empty()) {
            string next = pq.top(); // 拿出字典序最小的目的地
            pq.pop(); // 模拟用掉这张票
            dfs(next); // 递归访问下一个目的地
        }
        // 所有路径走完后，把当前节点插到最前面
        itinerary.push_front(airport); // 回溯插入
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后整体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
   // 图结构：每个出发地 -&amp;gt; 所有目的地的小顶堆（按字母排序）
   unordered_map&amp;lt;string, priority_queue&amp;lt;string, vector&amp;lt;string&amp;gt;, greater&amp;lt;string&amp;gt;&amp;gt;&amp;gt; graph;

   // 用链表保存最终行程，方便在前面插入（欧拉路径是回溯构建的）
   list&amp;lt;string&amp;gt; itinerary;

   // 深度优先遍历构建行程
    void dfs(const string&amp;amp; airport) {
        // 引用当前机场对应的优先队列
        auto&amp;amp; pq = graph[airport];

        // 当还有目的地没访问时，一直往下走
        while (!pq.empty()) {
            string next = pq.top(); // 拿出字典序最小的目的地
            pq.pop();               // 模拟用掉这张票
            dfs(next);              // 递归访问下一站
        }

        // 所有路径走完后，把当前节点插到最前面
        itinerary.push_front(airport);  // 回溯时构建路径
    }
public:
    vector&amp;lt;string&amp;gt; findItinerary(vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt;&amp;amp; tickets) {
        // 构建图
        for (const auto&amp;amp; ticket : tickets) {
            graph[ticket[0]].push(ticket[1]); // 加入小顶堆
        }

        // 从 JFK 出发执行 DFS
        dfs(&quot;JFK&quot;);

        // 把链表转换成 vector 输出结果
        return vector&amp;lt;string&amp;gt;(itinerary.begin(), itinerary.end());
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体的思想：&lt;/p&gt;
&lt;p&gt;用一个“出发机场 → 小顶堆目的地”的图结构，配合 Hierholzer 算法从 &quot;JFK&quot; 出发递归走图，每次优先访问字典序最小的目的地，构建出字典序最小的欧拉路径。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;做的事情&lt;/th&gt;
&lt;th&gt;用到的结构&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1️&lt;/td&gt;
&lt;td&gt;把机票构造成一张&lt;strong&gt;有向图&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unordered_map&amp;lt;string, 优先级队列(小顶堆)&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2️&lt;/td&gt;
&lt;td&gt;用 DFS 找一条「覆盖所有边」的路径（Hierholzer 算法）&lt;/td&gt;
&lt;td&gt;递归 + 小顶堆控制顺序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3️&lt;/td&gt;
&lt;td&gt;每当走完当前点所有边，就把它「插入路径前面」&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&amp;lt;string&amp;gt;&lt;/code&gt; 反向构造路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4️&lt;/td&gt;
&lt;td&gt;最终返回完整路径&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt; → &lt;code&gt;vector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;N 皇后&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/n-queens/&quot;&gt;51. N 皇后&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在国际象棋的规则中，皇后可以在同一行、同一列和同一对角线上攻击其他棋子。所以本题的题意可以转换成：
给定一个整数 n，要求在 n x n 的棋盘上放置 n 个皇后，使得：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每行只能有一个皇后&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每列只能有一个皇后&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每条对角线（↘ / ↙）也不能有两个皇后&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;攻击范围矩阵 + 回溯&lt;/h3&gt;
&lt;p&gt;每次在某个位置放皇后，就把它能攻击到的位置都标记为 &lt;code&gt;true&lt;/code&gt;（即“危险”）； 然后递归下一行； 回溯时撤销刚刚这一步设置的“危险区域”，恢复原状态。&lt;/p&gt;
&lt;p&gt;首先我们可以定义两个全局变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ans&lt;/code&gt;：用于存储所有的解&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vector&amp;lt;string&amp;gt; board&lt;/code&gt;：用于存储当前的棋盘路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回溯函数的参数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void backTracking(int n, int row, vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt;&amp;amp; danger)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;：表示棋盘的大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;row&lt;/code&gt;：表示当前行数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;danger&lt;/code&gt;：表示当前危险的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;递归终止的条件是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (row == n) {
    ans.push_back(board); // 当皇后数量等于n时（等于行数或列数）说明遍历结束
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单层回溯逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从第 0 行开始，尝试在每一列放皇后，后记为 &lt;code&gt;row&lt;/code&gt; 为行数，&lt;code&gt;i&lt;/code&gt; 为列数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前行的 &lt;code&gt;danger[row][i]&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，说明当前位置是危险的，跳过&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前位置是安全的，即 &lt;code&gt;danger[row][i]&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化 &lt;code&gt;changed&lt;/code&gt;：用于记录 &lt;code&gt;danger&lt;/code&gt; 矩阵中有哪些元素被修改过，标记其位置&lt;/li&gt;
&lt;li&gt;放皇后：在 &lt;code&gt;board[row][col]&lt;/code&gt; 放 &lt;code&gt;Q&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;flip()&lt;/code&gt;：标记所有攻击范围为 &lt;code&gt;true&lt;/code&gt;，并记录 &lt;code&gt;changed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;递归下一行；&lt;/li&gt;
&lt;li&gt;回溯时恢复：撤销并记录 &lt;code&gt;board[row][col] = &apos;.&apos;&lt;/code&gt;，调用 &lt;code&gt;unflip()&lt;/code&gt; 恢复 &lt;code&gt;danger&lt;/code&gt; 矩阵状态；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后的整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ans;   // 所有解
    vector&amp;lt;string&amp;gt; board; // 当前棋盘路径

     // 标记攻击范围：行、列、主对角线、副对角线
     void flip(vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt;&amp;amp; danger, int i, int j, vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;amp; changed) {
        int n = danger.size();
        // 行
        for (int col = 0; col &amp;lt; n; ++col) {
            if (!danger[i][col]) { // 如果该位置尚未被标记为危险
                danger[i][col] = true; // 标记为危险
                changed.emplace_back(i, col); // 记录此次改动
            }
        }
        // 列
        for (int row = 0; row &amp;lt; n; ++row) {
            if (!danger[row][j]) { // 如果该位置尚未被标记为危险
                danger[row][j] = true; // 标记为危险
                changed.emplace_back(row, j); // 记录此次改动
            }
        }
        // -n 到 +n 是为了确保无论皇后在棋盘哪个位置，都能完整枚举它对应的整条对角线上的所有格子；然后通过边界检查来保证不越界。

        // 右对角线 ↘
        for (int d = -n; d &amp;lt;= n; ++d) { 
            int row = i + d;  // 当前对角线上的行坐标（沿 ↘ 方向增加）
            int col = j + d;  // 当前对角线上的列坐标（列也一样增加）
            // 保证当前(row, col)在棋盘范围内，且这个位置还没有被标记为危险
            if (row &amp;gt;= 0 &amp;amp;&amp;amp; row &amp;lt; n &amp;amp;&amp;amp; col &amp;gt;= 0 &amp;amp;&amp;amp; col &amp;lt; n &amp;amp;&amp;amp; !danger[row][col]) {
                danger[row][col] = true; // 标记为危险（不可放皇后）
                changed.emplace_back(row, col); // 记录这个位置，以便回溯还原
            }
        }
        // 左对角线 ↙
        for (int d = -n; d &amp;lt;= n; ++d) {
            int row = i + d; // 当前对角线上的行坐标（向下走）
            int col = j - d; // 列坐标反向移动（↙ 的方向）
            // 保证当前(row, col)在棋盘范围内，且这个位置还没有被标记为危险
            if (row &amp;gt;= 0 &amp;amp;&amp;amp; row &amp;lt; n &amp;amp;&amp;amp; col &amp;gt;= 0 &amp;amp;&amp;amp; col &amp;lt; n &amp;amp;&amp;amp; !danger[row][col]) {
                danger[row][col] = true; // 标记为危险
                changed.emplace_back(row, col); // 记录用于撤销
            } 
        }
    }

    void unflip(vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt;&amp;amp; danger, const vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;amp; changed) {
        // 将 flip() 中记录的所有变更位置恢复为 false，撤销当前层的影响
        for (const auto&amp;amp; [i, j] : changed) {
            danger[i][j] = false;
        }
    }

    void backTracking(int n, int row, vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt;&amp;amp; danger) {
        if (row == n) { // 当皇后数量等于n时（等于行数或列数）说明遍历结束
            ans.push_back(board);
            return;
        }

        for (int i = 0; i &amp;lt; n; i++) {
            if (danger[row][i]) {
                continue;
            }
            board[row][i] = &apos;Q&apos;;
            vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; changed;
            // 更改danger的当前行，当前列，两条对角线的布尔值
            flip(danger, row, i, changed);
            // 递归下一行
            backTracking(n, row + 1, danger);
            // 回溯danger的当前行，当前列，两条对角线的布尔值
            unflip(danger, changed);
            board[row][i] = &apos;.&apos;;
        }
    }

public:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; solveNQueens(int n) {
        vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt; danger(n, vector&amp;lt;bool&amp;gt;(n, false)); // 用于记录当前危险的位置
        board = vector&amp;lt;string&amp;gt;(n, string(n, &apos;.&apos;)); // 初始化棋盘路径
        backTracking(n, 0, danger);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这种方法的难点在于如何标记和撤销危险区域，也就是 &lt;code&gt;flip()&lt;/code&gt; 和 &lt;code&gt;unflip()&lt;/code&gt; 函数的实现。&lt;/p&gt;
&lt;p&gt;不过这种方法效率并不高，有一点绕弯路的意思。&lt;/p&gt;
&lt;h3&gt;不使用攻击标记&lt;/h3&gt;
&lt;p&gt;如果使用以上的方法的话，时间复杂度是 O(N^3)，空间复杂度是 O(N^2)。相对来说是耗时的。我们可以直接在回溯的逻辑中，直接判断当前位置放皇后是否合法。&lt;/p&gt;
&lt;p&gt;也就是在递归逻辑中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (isValid(row, i, board)) {
    board[row][i] = &apos;Q&apos;; // 放皇后
    backTracking(n, row + 1, board); // 递归下一行
    board[row][i] = &apos;.&apos;; // 回溯，并放置 &apos;.`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重点就是如何实现 &lt;code&gt;isValid()&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isValid(int row, int col, const vector&amp;lt;string&amp;gt;&amp;amp; board) {
    // 检查当前列是否有皇后
    // 因为是要在每一行中放置一个皇后，所以可以不需要对行进行判断
    // 当前这一行是第一次处理，行里还没有任何皇后；
    // 前面的所有皇后都在上面的行中；
    // 因为我们是一行一行往下放，只需要检查 0 到 row-1 行
    for (int i = 0; i &amp;lt; row; ++i) {
        if (board[i][col] == &apos;Q&apos;) return false; // 当前列有皇后
    }
    // 检查主对角线 ↘
    for (int i = row - 1, j = col - 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;gt;= 0; --i, --j) {
        if (board[i][j] == &apos;Q&apos;) return false; // 主对角线有皇后
    }
    // 检查副对角线 ↙
    for (int i = row - 1, j = col + 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;lt; board.size(); --i, ++j) {
        if (board[i][j] == &apos;Q&apos;) return false; // 副对角线有皇后
    }
    return true; // 没有冲突，可以放皇后
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以整体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ans;   // 所有解
    vector&amp;lt;string&amp;gt; board; // 当前棋盘路径

    bool isValid(int row, int col, const vector&amp;lt;string&amp;gt;&amp;amp; board) {
        // 检查当前列是否有皇后
        // 因为是要在每一行中放置一个皇后，所以可以不需要对行进行判断
        // 当前这一行是第一次处理，行里还没有任何皇后；
        // 前面的所有皇后都在上面的行中；
        // 因为我们是一行一行往下放，只需要检查 0 到 row-1 行
        for (int i = 0; i &amp;lt; row; ++i) {
            if (board[i][col] == &apos;Q&apos;) return false; // 当前列有皇后
        }
        // 检查主对角线 ↘
        for (int i = row - 1, j = col - 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;gt;= 0; --i, --j) {
            if (board[i][j] == &apos;Q&apos;) return false; // 主对角线有皇后
        }
        // 检查副对角线 ↙
        for (int i = row - 1, j = col + 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;lt; board.size(); --i, ++j) {
            if (board[i][j] == &apos;Q&apos;) return false; // 副对角线有皇后
        }
        return true; // 没有冲突，可以放皇后
    }

    void backTracking(int n, int row, vector&amp;lt;string&amp;gt;&amp;amp; board) {
        if (row == n) {
            ans.push_back(board);
            return;
        }

        for (int col = 0; col &amp;lt; n; col++) {
            if (isValid(row, col, board)) { // 如果这个位置放置皇后合法
                board[row][col] = &apos;Q&apos;; // 放置皇后
                backTracking(n, row + 1, board); // 递归下一行
                board[row][col] = &apos;.&apos;; // 回溯标记为 &apos;.&apos;
            }
        }
    }

public:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; solveNQueens(int n) {
        board = vector&amp;lt;string&amp;gt;(n, string(n, &apos;.&apos;)); // 初始化棋盘路径
        backTracking(n, 0, board);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法的时间复杂度是 O(N^2)，空间复杂度是 O(N)。相对来说是比较高效的。&lt;/p&gt;
&lt;h2&gt;解数独&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sudoku-solver/&quot;&gt;37. 解数独&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里于之前的 N 皇后不同的是，要做的是一个二维递归。因为在棋盘的每一个位置都要放置一个数字，并检查数字是否合法，解数独的树形结构要比N皇后更宽更深。&lt;/p&gt;
&lt;p&gt;所以本质上还是一个回溯问题。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归函数的参数：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;bool backTracking(vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt;&amp;amp; board)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们需要搜索一个满足条件的答案，所以我们需要一个布尔返回值，当找到了解时就立刻返回。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归终止条件：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里其实可以不用写，因为我们在每次递归时都会检查当前的棋盘是否满足条件，如果满足条件就返回 &lt;code&gt;true&lt;/code&gt;，否则会继续递归或者是 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单层回溯逻辑：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里我们需要用到二维递归，因为每一个位置都要放置一个数字，所以我们需要两个循环来遍历每一个位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 0; i &amp;lt; board.size(); i++) {
    for (int j = 0; j &amp;lt; board[0].size(); j++) {
        if (board[i][j] == &apos;.&apos;) { // 遇到 &apos;.&apos;时进行处理
            for (char c = &apos;1&apos;; c &amp;lt;= &apos;9&apos;; c++) {
                if (isValid(i, j, c, board)) {
                    board[i][j] = c; // 放置数字
                    if (backTracking(board)) return true;
                    board[i][j] = &apos;.&apos;; // 回溯
                }
            }
            // 如果上述for循环已经走完了，说明没有合适的数字填在此处，等于数独无解返回false
            return false; 
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后还需要一个 &lt;code&gt;isValid()&lt;/code&gt; 函数来判断当前数字是否合法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isValid(int row, int col, char ch, vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt;&amp;amp; board) {
    // 行中是否有该数字
    for (int i = 0; i &amp;lt; 9; i++) {
        if (board[row][i] == ch) return false;
    }

    // 列中是否有该数字
    for (int j = 0; j &amp;lt; 9; j++) {
        if (board[j][col] == ch) return false;
    }
    // 九宫格中是否有该数字
    //把 0~2 映射成 0，3~5 映射成 1，6~8 映射成 2
    // 再将这个 “宫格编号” 映射成其实际起点坐标：0 → 0，1 → 3，2 → 6
    int blockRow = (row / 3) * 3; // 九宫格行起始位置
    int blockCol = (col / 3) * 3; // 九宫格列起始位置
    for (int i = blockRow; i &amp;lt; blockRow + 3; i++) {
        for (int j = blockCol; j &amp;lt; blockCol + 3; j++) {
            if (board[i][j] == ch) return false;
        }
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后整体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt; ans;

    bool isValid(int row, int col, char ch, vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt;&amp;amp; board) {
        // 行中是否有该数字
        for (int i = 0; i &amp;lt; 9; i++) {
            if (board[row][i] == ch) return false;
        }

        // 列中是否有该数字
        for (int j = 0; j &amp;lt; 9; j++) {
            if (board[j][col] == ch) return false;
        }
        // 九宫格中是否有该数字
        //把 0~2 映射成 0，3~5 映射成 1，6~8 映射成 2
        // 再将这个 “宫格编号” 映射成其实际起点坐标：0 → 0，1 → 3，2 → 6
        int blockRow = (row / 3) * 3; // 九宫格行起始位置
        int blockCol = (col / 3) * 3; // 九宫格列起始位置
        for (int i = blockRow; i &amp;lt; blockRow + 3; i++) {
            for (int j = blockCol; j &amp;lt; blockCol + 3; j++) {
                if (board[i][j] == ch) return false;
            }
        }
        return true;
    }

    bool backTracking(vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt;&amp;amp; board) {
        // 这里可以不需要返回值
        for (int i = 0; i &amp;lt; board.size(); i++) {
            for (int j = 0; j &amp;lt; board[0].size(); j++) {
                if (board[i][j] == &apos;.&apos;) { // 遇到 &apos;.&apos;时进行处理
                   for (char c = &apos;1&apos;; c &amp;lt;= &apos;9&apos;; c++) {
                        if (isValid(i, j, c, board)) {
                        board[i][j] = c; // 放置数字
                        if (backTracking(board)) return true;
                        board[i][j] = &apos;.&apos;; // 回溯
                        }
                    }
                    // 如果上述for循环已经走完了，说明没有合适的数字填在此处，等于数独无解返回false
                    return false; 
                }
            }
        }
        return true;
    }

public:
    void solveSudoku(vector&amp;lt;vector&amp;lt;char&amp;gt;&amp;gt;&amp;amp; board) {
        backTracking(board);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;回溯算法的框架其实基本上是固定的一个写法，即使是二维递归也可以套用这个框架。总体的思路都是一致的，当满足条件，则进行下一层递归。&lt;/p&gt;
&lt;p&gt;最后再回顾一下回溯算法的框架：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void backTracking(参数) {
    if (满足终止条件) { // 根据情况可能没有终止条件的情况
        // 处理结果
        return;
    }
    for (选择：所有可能的选择) {
        // 做选择
        backTracking(参数); // 递归进入下一层
        // 撤销选择
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后当要搜索某一个路径的时候，可以用 &lt;code&gt;bool&lt;/code&gt; 类型的返回值，如果是搜索所有的情况，则可以不需要返回值，也就是 &lt;code&gt;void&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>Day25-回溯算法 part04</title><link>https://m1dnightsun.github.io/posts/programmercarl/backtracking/day25_backtracking_part4/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/backtracking/day25_backtracking_part4/</guid><description>回溯算法，非递减子序列，全排列，全排列 II</description><pubDate>Sat, 05 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;非递减子序列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/non-decreasing-subsequences/&quot;&gt;491. 非递减子序列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里是要求所有的非递减子序列，和之前的组合问题类似，但这里不能对数组进行排序。&lt;/p&gt;
&lt;p&gt;所以这里的去重逻辑和之前都不一样。&lt;/p&gt;
&lt;p&gt;首先是和之前的子集问题一样，可以不加终止条件，不过按照题意至少拥有两个元素，所以当我们的 &lt;code&gt;path&lt;/code&gt; 大于等于 2 时，才将其加入到 &lt;code&gt;ans&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;单层的搜索逻辑：
&lt;img src=&quot;https://file.kamacoder.com/pics/20201124200229824-20230310131640070.png&quot; alt=&quot;单层搜索逻辑&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果我们转换成树状图，可以发现，在同一层（也就是 &lt;code&gt;for&lt;/code&gt; 循环中）中，已经用到的元素就不能再用了，而我们可以使用一个 &lt;code&gt;set&lt;/code&gt; 来存储已经用过的元素，来达到去重的效果。&lt;/p&gt;
&lt;p&gt;整体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;

    void backTracking (vector&amp;lt;int&amp;gt;&amp;amp; nums, int startIndex) {
        if (path.size() &amp;gt;= 2) { // 当path长度大于等2时才加入结果集
            ans.push_back(path);
        }
        
        unordered_set&amp;lt;int&amp;gt; used; // 使用set来对同一层的元素进行去重
        for (int i = startIndex; i &amp;lt; nums.size(); i++) {
            // 当path不为空且当层遍历的元素小于path中最后一个元素，就跳过
            if (!path.empty() &amp;amp;&amp;amp; nums[i] &amp;lt; path.back()) continue;
            // 当遍历的元素存在set中，说明之前已经用过该元素，跳过
            if (used.find(nums[i]) != used.end()) continue;
            used.insert(nums[i]);
            path.push_back(nums[i]);
            backTracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; findSubsequences(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        path.clear();
        ans.clear();
        backTracking(nums, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全排列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/permutations/&quot;&gt;46. 全排列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;由于是求排列，因此在每一层的 &lt;code&gt;for&lt;/code&gt; 循环中，我们都要遍历所有的元素，除了已经使用过的元素。&lt;/p&gt;
&lt;p&gt;因此处理的逻辑其实可以和&lt;a href=&quot;#%E9%9D%9E%E9%80%92%E5%87%8F%E5%AD%90%E5%BA%8F%E5%88%97&quot;&gt;非递减子序列&lt;/a&gt;一样，使用一个 &lt;code&gt;set&lt;/code&gt; 来存储已经使用过的元素。或者是使用 &lt;code&gt;find&lt;/code&gt; 来判断是否已经使用过。&lt;/p&gt;
&lt;p&gt;总体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;

    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if (path.size() == nums.size()) { // 当路径长度等于数组长度时，找到一个排列
            ans.push_back(path);
            return;
        }
        // 注意因为求排列，所以i从0开始，以遍历所有的元素（除了已经使用过的）
        for (int i = 0; i &amp;lt; nums.size(); i++) { // 遍历每个数字
            if (find(path.begin(), path.end(), nums[i]) != path.end()) continue; // 如果数字已经在路径中，跳过
            path.push_back(nums[i]); // 选择数字
            backTracking(nums); // 递归
            path.pop_back(); // 撤销选择
        }
    }

public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; permute(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        path.clear();
        ans.clear();
        backTracking(nums);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然这里也可以使用一个 &lt;code&gt;used&lt;/code&gt; 数组来标记是否使用过，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;

    // 使用used数组的方式
    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; nums, vector&amp;lt;bool&amp;gt;&amp;amp; used) {
        if (path.size() == nums.size()) { // 当路径长度等于数组长度时，找到一个排列
            ans.push_back(path);
            return;
        }
        // 注意因为求排列，所以i从0开始，以遍历所有的元素（除了已经使用过的）
        for (int i = 0; i &amp;lt; nums.size(); i++) {
            if (used[i] == true) continue; // 如果数字已经在路径中，跳过
            used[i] = true; // 标记为已使用
            path.push_back(nums[i]);
            backTracking(nums, used);
            path.pop_back();
            used[i] = false; // 回溯时撤销标记
        }
    }

public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; permute(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        path.clear();
        ans.clear();
        vector&amp;lt;bool&amp;gt; used(nums.size(), false); // 初始化used数组
        backTracking(nums, used);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全排列 II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/permutations-ii/&quot;&gt;47. 全排列 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里与&lt;a href=&quot;#%E5%85%A8%E6%8E%92%E5%88%97&quot;&gt;全排列&lt;/a&gt;的区别在于，数组中可能存在重复的元素，因此我们需要对同一层的元素进行去重。&lt;/p&gt;
&lt;p&gt;去重还一定要对元素进行排序，这样我们才方便通过相邻的节点来判断是否重复使用了。&lt;/p&gt;
&lt;p&gt;我们对同一树层，前一位（也就是 &lt;code&gt;nums[i-1]&lt;/code&gt;  ）如果使用过，那么就进行去重。&lt;/p&gt;
&lt;p&gt;总体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;

    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; nums, vector&amp;lt;bool&amp;gt;&amp;amp; used) {
        if (path.size() == nums.size()) { // 当路径长度等于数组长度时，找到一个排列
            ans.push_back(path);
            return;
        }

        for (int i = 0; i &amp;lt; nums.size(); i++) { // 遍历每个数字
            // used[i - 1] == true，说明同一树枝nums[i - 1]使用过
            // used[i - 1] == false，说明同一树层nums[i - 1]使用过
            // 如果同一树层nums[i - 1]使用过则直接跳过
            if (used[i]) continue; // 如果数字已经被使用，跳过
            if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] &amp;amp;&amp;amp; !used[i - 1]) continue; 
            used[i] = true; // 标记数字为已使用
            path.push_back(nums[i]); // 选择数字
            backTracking(nums, used); // 递归
            path.pop_back(); // 撤销选择
            used[i] = false; // 取消标记
        }
    }

public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; permuteUnique(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        path.clear();
        ans.clear();    
        vector&amp;lt;bool&amp;gt; used(nums.size(), false); // 用于标记数字是否被使用
        sort(nums.begin(), nums.end()); // 排序以便去重
        backTracking(nums, used);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[去重条件]
对于这个去重条件 &lt;code&gt;if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] &amp;amp;&amp;amp; !used[i - 1]) continue;&lt;/code&gt;，我们可以理解为：
&lt;strong&gt;在当前层中，如果我们遇到了两个值相同的元素，而前一个还没被用（说明它在当前层的某个位置被跳过了），那我们就不应该再用当前这个重复值了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果在当前层我们还没选过 &lt;code&gt;nums[i - 1]&lt;/code&gt;，那就不应该再选 &lt;code&gt;nums[i]&lt;/code&gt;（因为它俩相等）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果我们已经选过了 &lt;code&gt;nums[i - 1]&lt;/code&gt;，说明当前这条路径在“不同树枝”上，可以选 &lt;code&gt;nums[i]&lt;/code&gt;。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在去重的代码中，我们可以看到 &lt;code&gt;used[i - 1]&lt;/code&gt; 的判断条件，这里是为了避免在同一树层中，使用了相同的元素。&lt;/p&gt;
&lt;p&gt;如果改成 &lt;code&gt;used[i - 1] == true&lt;/code&gt;， 也是正确的。如果要对树层中前一位去重，就用 &lt;code&gt;used[i - 1] == false&lt;/code&gt;，如果要对树枝前一位去重用 &lt;code&gt;used[i - 1] == true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于排列问题，树层上去重和树枝上去重，都是可以的，但是树层上去重效率更高。&lt;/p&gt;
&lt;p&gt;这里以随想录中的所举的例子 &lt;code&gt;[1, 1, 1]&lt;/code&gt; 为例&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E6%A0%91%E5%B1%82%E5%8E%BB%E9%87%8D%E4%B8%8E%E6%A0%91%E6%9E%9D%E5%8E%BB%E9%87%8D%5D(https://programmercarl.com/0047.%E5%85%A8%E6%8E%92%E5%88%97II.html#%E6%8B%93%E5%B1%95)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;树层上去重：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] &amp;amp;&amp;amp; !used[i - 1]) continue; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20201124201406192.png&quot; alt=&quot;树层去重&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;树枝上去重：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] &amp;amp;&amp;amp; used[i - 1]) continue; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20201124201431571.png&quot; alt=&quot;树枝去重&quot; /&gt;&lt;/p&gt;
&lt;p&gt;树层上对前一位去重非常彻底，效率很高，树枝上对前一位去重虽然最后可以得到答案，但是做了很多无用搜索。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;如果我们在去重操作中不写这个条件，&lt;code&gt;!used[i - 1]&lt;/code&gt; 或者 &lt;code&gt;used[i - 1]&lt;/code&gt;，直接写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1]) continue; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实并不行，一定要加上 &lt;code&gt;used[i - 1] == false&lt;/code&gt; 或者 &lt;code&gt;used[i - 1] == true&lt;/code&gt;，因为 &lt;code&gt;used[i - 1]&lt;/code&gt; 要一直是 &lt;code&gt;true&lt;/code&gt; 或者一直是 &lt;code&gt;false&lt;/code&gt; 才可以，而不是一会是 &lt;code&gt;true&lt;/code&gt; 一会又是 &lt;code&gt;false&lt;/code&gt;, 所以这个条件要写上。&lt;/p&gt;
</content:encoded></item><item><title>Day24-回溯算法 part03</title><link>https://m1dnightsun.github.io/posts/programmercarl/backtracking/day24_backtracking_part3/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/backtracking/day24_backtracking_part3/</guid><description>回溯算法，复原IP地址，子集，子集II</description><pubDate>Fri, 04 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;复原IP地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/restore-ip-addresses/&quot;&gt;93. 复原IP地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们需要将一个字符串分割成4段合法的IP地址段，问题就变成了在字符串中寻找三个切割点，将整个字符串分为四段，每段都要符合合法条件。&lt;/p&gt;
&lt;p&gt;其实与组合问题的思路是一样的，-字符串切割与组合问题的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;组合问题：选一个字母后，在剩余字母中继续选&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切割问题：切掉一段后，在剩余部分继续切下一段&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们把这个问题转换成树形图，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20201123203735933.png&quot; alt=&quot;IP树形图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先我们先定义两个全局变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;string&amp;gt; path;
vector&amp;lt;string&amp;gt; ans;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回溯三部曲：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;递归参数
因为我们在递归过程中需要知道从哪个地方进行下一层递归，所以我们需要一个 &lt;code&gt;startIndex&lt;/code&gt; 参数来表示当前的起始位置。同时需要一个 &lt;code&gt;dotCount&lt;/code&gt; 参数来表示当前的点的数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 参数：字符串s，字符串起始位置startIndex，&apos;.&apos;的个数dotCount
void backTracking(const string&amp;amp; s, int startIndex, int dotCount)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归终止条件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;dotCount&lt;/code&gt; 等于 4 时，说明我们已经找到了 4 段合法的 IP 地址段&lt;/li&gt;
&lt;li&gt;并且当 &lt;code&gt;startIndex&lt;/code&gt; 等于字符串的长度时，说明我们已经遍历完了整个字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时可以将 &lt;code&gt;path&lt;/code&gt; 中的字符串加入到 &lt;code&gt;ans&lt;/code&gt; 中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (dotCount == 4 &amp;amp;&amp;amp; startIndex == s.size()) {
        ans.push_back(path[0] + &quot;.&quot; + path[1] + &quot;.&quot; + path[2] + &quot;.&quot; + path[3]);
        return;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单层搜索逻辑&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次分割的字符串最多只有三位，所以可以控制 &lt;code&gt;i - startIndex &amp;lt;= 2&lt;/code&gt;，也就是每段最多三位数。&lt;/li&gt;
&lt;li&gt;重要的地方是如何切割字符串，&lt;code&gt;s.substr(startIndex, i - startIndex + 1)&lt;/code&gt; 表示从 &lt;code&gt;startIndex&lt;/code&gt; 开始，取 &lt;code&gt;i - startIndex + 1&lt;/code&gt; 个字符，也就是取 &lt;code&gt;s[startIndex...i]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;我们将切割的字符串放入 &lt;code&gt;path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;进入下一层递归，&lt;code&gt;i + 1&lt;/code&gt; 表示下一个起始位置，&lt;code&gt;dotCount + 1&lt;/code&gt; 表示点的数量加 1。&lt;/li&gt;
&lt;li&gt;回溯时将 &lt;code&gt;path&lt;/code&gt; 中的最后一个元素删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 每段最多3位，i是当前段的终止位置
    for (int i = startIndex; i &amp;lt; s.size() &amp;amp;&amp;amp; i - startIndex &amp;lt;= 2; ++i) {
        if (isValid(s, startIndex, i)) {
            // 将当前分割的字符串先放入path中：[startIndex, i]
            path.push_back(s.substr(startIndex, i - startIndex + 1)); // 从startIndex开始, 取i - startIndex + 1个字符，也就是取s[startIndex...i]
            backTracking(s, i + 1, dotCount + 1);
            path.pop_back(); // 回溯
        } else {
            break; // 剪枝
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;isValid() 函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;段位以0为开头的数字不合法&lt;/li&gt;
&lt;li&gt;段位里有非正整数字符不合法&lt;/li&gt;
&lt;li&gt;段位如果大于255了不合法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;bool isValid(const string&amp;amp; s, int start, int end) {
    if (start &amp;gt; end) return false;
    if (s[start] == &apos;0&apos; &amp;amp;&amp;amp; start != end) return false; // 有前导 0
    int num = 0;
    for (int i = start; i &amp;lt;= end; i++) {
        if (s[i] &amp;gt; &apos;9&apos; || s[i] &amp;lt; &apos;0&apos;) { // 遇到非数字字符不合法
            return false;
        }
        num = num * 10 + (s[i] - &apos;0&apos;);
        if (num &amp;gt; 255) { // 如果大于255了不合法
            return false;
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;string&amp;gt; path;
    vector&amp;lt;string&amp;gt; ans;

    bool isValid(const string&amp;amp; s, int start, int end) {
        if (start &amp;gt; end) return false;
        if (s[start] == &apos;0&apos; &amp;amp;&amp;amp; start != end) return false; // 有前导 0
        int num = 0;
        for (int i = start; i &amp;lt;= end; i++) { // 如果该字符串转换成整数的数值&amp;gt;255返回false
            if (!isdigit(s[i])) return false;
            num = num * 10 + (s[i] - &apos;0&apos;);
            if (num &amp;gt; 255) return false;
        }
        return true;
    }
public:
    // 参数：字符串s，字符串起始位置startIndex，&apos;.&apos;的个数dotCount
    void backTracking(const string&amp;amp; s, int startIndex, int dotCount) {
        // 如果已经切了4段，检查是否用完字符串
        // 并且已经切割到最后一个字符串
        if (dotCount == 4 &amp;amp;&amp;amp; startIndex == s.size()) {
            ans.push_back(path[0] + &quot;.&quot; + path[1] + &quot;.&quot; + path[2] + &quot;.&quot; + path[3]);
            return;
        }
        
        // 每段最多3位，i是当前段的终止位置
        for (int i = startIndex; i &amp;lt; s.size() &amp;amp;&amp;amp; i - startIndex &amp;lt;= 2; ++i) {
            if (isValid(s, startIndex, i)) {
                // 将当前分割的字符串先放入path中：[startIndex, i]
                path.push_back(s.substr(startIndex, i - startIndex + 1)); // 从startIndex开始, 取i - startIndex + 1个字符，也就是取s[startIndex...i]
                backTracking(s, i + 1, dotCount + 1);
                path.pop_back(); // 回溯
            } else {
                break; // 剪枝
            }
        }
    }

    vector&amp;lt;string&amp;gt; restoreIpAddresses(string s) {
        ans.clear();
        if (s.size() &amp;lt; 4 || s.size() &amp;gt; 12) return ans;
        backTracking(s, 0, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;子集&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/subsets/&quot;&gt;78. 子集&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果把 子集问题、组合问题、分割问题都抽象为一棵树的话，那么组合问题和分割问题都是收集树的叶子节点，而子集问题是找树的所有节点。&lt;/p&gt;
&lt;p&gt;因此这里的差别就体现在什么时候收集结果。以 &lt;code&gt;nums = [1,2,3]&lt;/code&gt; 为例把求子集抽象为树型结构，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/pics/78.%E5%AD%90%E9%9B%86.png&quot; alt=&quot;子集树&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以发现，我们每进行一次递归，就要把当前的 &lt;code&gt;path&lt;/code&gt; 加入到结果中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
public:
    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; nums, int startIndex) {
        ans.push_back(path); // 刚进入递归时，就收集结果
        if (startIndex &amp;gt;= nums.size()) { // 可以不写终止条件，因为满足该条件时，以下for循环不会进行
            return;
        }

        for (int i = startIndex; i &amp;lt; nums.size(); i++) {
            path.push_back(nums[i]);
            backTracking(nums, i + 1); // 不重复，从i + 1开始
            path.pop_back();
        }
    }
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; subsets(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        ans.clear();
        path.clear();
        backTracking(nums, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;子集II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/subsets-ii/&quot;&gt;90. 子集 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;次问题是一个综合了&lt;a href=&quot;#%E5%AD%90%E9%9B%86&quot;&gt;子集&lt;/a&gt;和&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/backtracking/day23_backtracking_part2/#%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8Cii&quot;&gt;组合总和II&lt;/a&gt;的问题，无非就是在子集问题上加了一个去重的操作。&lt;/p&gt;
&lt;p&gt;代码其实不难，主要是要注意去重的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
public:
    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; nums, int startIndex) {
        ans.push_back(path); // 每进入一次递归就要收集结果
        if (startIndex &amp;gt;= nums.size());

        for (int i = startIndex; i &amp;lt; nums.size(); i++) {
            if (i &amp;gt; startIndex &amp;amp;&amp;amp; nums[i] == nums[i - 1]) continue; // 去重
            path.push_back(nums[i]);
            backTracking(nums, i + 1);
            path.pop_back();
        }
    }
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; subsetsWithDup(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        path.clear();
        ans.clear();
        sort(nums.begin(), nums.end()); // 针对去重需要排序
        backTracking(nums, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对于组合问题和分割问题，我们都是从树的叶子节点收集结果，最后是当满足一定条件才收集结果。&lt;/p&gt;
&lt;p&gt;而对于子集问题，我们是从树的所有节点收集结果，也就是每进行一次递归都要收集一次结果。&lt;/p&gt;
</content:encoded></item><item><title>Day23-回溯算法 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/backtracking/day23_backtracking_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/backtracking/day23_backtracking_part2/</guid><description>回溯算法，组合总和，组合总和II，分割回文串</description><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;组合总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum/&quot;&gt;39. 组合总和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里和&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/backtracking/day22_backtracking_part1/#%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8Ciii&quot;&gt;组合总和III&lt;/a&gt;
的区别在于，本题的数组中的元素是可以重复使用的，也就是说在 &lt;code&gt;for&lt;/code&gt; 循环中的回溯递归过程中，选取元素的位置可以从当前的 &lt;code&gt;i&lt;/code&gt; 开始，而不是 &lt;code&gt;i + 1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里对于组合问题，什么时候需要用到 &lt;code&gt;start&lt;/code&gt; 标识来控制元素的选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果是用一个数组（或集合）中的元素来进行组合，那么就需要用到 &lt;code&gt;start&lt;/code&gt; 来控制元素的选择。例如&lt;a href=&quot;https://leetcode-cn.com/problems/combinations/&quot;&gt;77. 组合&lt;/a&gt;中的 &lt;code&gt;start&lt;/code&gt; 就是用来控制元素的选择的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是多个数组（或集合）中的元素来进行组合，那么就不需要用到 &lt;code&gt;start&lt;/code&gt; 来控制元素的选择。例如&lt;a href=&quot;https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/&quot;&gt;17. 电话号码的字母组合&lt;/a&gt;，各个集合之间的元素互不影响，所以不需要用到 &lt;code&gt;start&lt;/code&gt; 来控制元素的选择。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们可以有以下的思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path; // 全局变量，存储当前遍历的数字
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans; // 全局变量，存储所有的组合
public:
    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target, int start) {
        if (target &amp;lt; 0) return; // 重要：如果target小于0，说明当前路径不符合条件，直接返回
        if (target == 0) { // 如果target等于0，说明当前路径符合条件
            ans.push_back(path);
            return;
        }
        for (int i = start; i &amp;lt; candidates.size() &amp;amp;&amp;amp; candidates[i] &amp;lt;= target; i++) { //一旦 candidates[i] &amp;gt; target，就不会再往后递归，这里的数组一定是有序的
            path.push_back(candidates[i]);
            backTracking(candidates, target - candidates[i], i); // 这里的 i 而不是 i + 1，表示可以重复使用当前元素
            path.pop_back();
        }
    }
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combinationSum(vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target) {
        // 剪枝操作一定要对数组进行排序
        sort(candidates.begin(), candidates.end());
        backTracking(candidates, target, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的剪枝操作，没有排序时，&lt;code&gt;candidates[i] &amp;gt; target&lt;/code&gt; 不代表后面的都更大，所以不能剪枝，否则就会漏掉组合。&lt;/p&gt;
&lt;h2&gt;组合总和II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum-ii/&quot;&gt;40. 组合总和 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个问题其实和之前的组合问题区别不大，主要是需要对重复的元素进行去重处理。&lt;/p&gt;
&lt;p&gt;所以这里就直接写出代码了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; path;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
public:
    void backTracking(vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target, int start) {
        if (target &amp;lt; 0) return;
        if (target == 0) {
            ans.push_back(path);
            return;
        }
        for (int i = start; i &amp;lt; candidates.size() &amp;amp;&amp;amp; candidates[i] &amp;lt;= target; i++) {
            // start是在该层的第一个选择，所以要先判断i&amp;gt;start，相当于后续子数组的“0”位置
            if (i &amp;gt; start &amp;amp;&amp;amp; candidates[i] == candidates[i - 1]) continue; // 去重操作
            path.push_back(candidates[i]);
            backTracking(candidates, target - candidates[i], i + 1); // 从下一个元素开始，避免重复使用
            path.pop_back();
        }
    }

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combinationSum2(vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backTracking(candidates, target, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分割回文串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/palindrome-partitioning/&quot;&gt;131. 分割回文串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;字符串的切割问题和之前的组合问题类似，例如对于字符串 &lt;code&gt;abcdef&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;组合问题：选取一个 &lt;code&gt;a&lt;/code&gt; 之后，在 &lt;code&gt;bcdef&lt;/code&gt; 中再去选取第二个，选取 &lt;code&gt;b&lt;/code&gt;之后在 &lt;code&gt;cdef&lt;/code&gt; 中再选取第三个.....。
切割问题：切割一个 &lt;code&gt;a&lt;/code&gt; 之后，在 &lt;code&gt;bcdef&lt;/code&gt; 中再去切割第二段，切割b之后在 &lt;code&gt;cdef&lt;/code&gt; 中再切割第三段.....。&lt;/p&gt;
&lt;p&gt;可以抽象为以下的树形图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/pics/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg&quot; alt=&quot;字符串切割&quot; /&gt;&lt;/p&gt;
&lt;p&gt;递归用来纵向遍历，for循环用来横向遍历，切割线（就是图中的红线）切割到字符串的结尾位置，说明找到了一个切割方法。&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2%5D(https://programmercarl.com/0131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从树形结构的图中可以看出：切割线切到了字符串最后面，说明找到了一种切割方法，此时就是本层递归的终止条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (start == s.size()) {
    ans.push_back(path);
    return;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于如何分割：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从 &lt;code&gt;start&lt;/code&gt; 开始，枚举所有的子串 &lt;code&gt;s[start..i]&lt;/code&gt;，判断是否是回文串。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是回文串，就将 &lt;code&gt;s[start..i]&lt;/code&gt; 加入到当前路径 &lt;code&gt;path&lt;/code&gt; 中，然后递归分割剩下的子串 &lt;code&gt;s[i+1..end]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不是回文串，就继续枚举下一个子串 &lt;code&gt;s[start..i+1]&lt;/code&gt;，直到 &lt;code&gt;i&lt;/code&gt; 到达字符串的末尾。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归结束后，回溯到上一个状态，移除上一次加入的子串 &lt;code&gt;s[start..i]&lt;/code&gt;，继续枚举下一个子串 &lt;code&gt;s[start..i+1]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直到所有的子串都枚举完毕，返回所有的分割方案。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以有以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; ans;  // 最终结果，包含所有分割方案
    vector&amp;lt;string&amp;gt; path;         // 当前递归路径下的分割结果

    // 判断 s[left..right] 是否是回文串
    bool isPalindrome(const string&amp;amp; s, int left, int right) {
        while (left &amp;lt; right) {
            if (s[left++] != s[right--]) return false;  // 有不相等的字符就不是回文
        }
        return true;  // 所有字符相等，说明是回文
    }

    void backtrack(const string&amp;amp; s, int start) {
        // 如果 start 到达字符串末尾，说明已经成功分割一组，加入结果
        if (start == s.size()) {
            ans.push_back(path);
            return;
        }

        // 枚举所有从 start 开始的子串
        for (int i = start; i &amp;lt; s.size(); ++i) {
            // 判断 s[start..i] 是否是回文串
            if (isPalindrome(s, start, i)) {
                // 是回文串就加入当前路径
                path.push_back(s.substr(start, i - start + 1));
                // 递归分割剩下的子串（从 i+1 开始）
                backtrack(s, i + 1);
                // 回溯，移除上一次加入的子串
                path.pop_back();
            }
        }
    }

public:
    // 主函数，调用回溯
    vector&amp;lt;vector&amp;lt;string&amp;gt;&amp;gt; partition(string s) {
        backtrack(s, 0);  // 从索引 0 开始回溯
        return ans;       // 返回所有合法的分割方案
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实也可以使用动态规划来解决这个问题，不过当前阶段就先不使用了。&lt;/p&gt;
</content:encoded></item><item><title>Day22-回溯算法 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/backtracking/day22_backtracking_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/backtracking/day22_backtracking_part1/</guid><description>回溯算法，组合问题，组合总和III，电话号码字母组合</description><pubDate>Wed, 02 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;回溯算法&lt;/h2&gt;
&lt;p&gt;回溯算法就是在尝试一条路径的过程中不断深入，如果发现不能达到目标，就回退一步，换另一种选择继续尝试，直到找到解或者穷尽所有解。&lt;/p&gt;
&lt;p&gt;它本质上是一个 &lt;strong&gt;递归过程 + 状态恢复&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;回溯算法与 &lt;code&gt;DFS&lt;/code&gt; 的关系：回溯算法是深度优先搜索（DFS）的一种特例。但回溯算法更强调“撤销操作”，是“DFS + 剪枝 + 撤销”的过程。&lt;/p&gt;
&lt;p&gt;回溯法，一般可以解决如下几种问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;组合问题：N个数里面按一定规则找出k个数的集合&lt;/li&gt;
&lt;li&gt;切割问题：一个字符串按一定规则有几种切割方式&lt;/li&gt;
&lt;li&gt;子集问题：一个N个数的集合里有多少符合条件的子集&lt;/li&gt;
&lt;li&gt;排列问题：N个数按一定规则全排列，有几种排列方式&lt;/li&gt;
&lt;li&gt;棋盘问题：N皇后，解数独等等&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;回溯算法通用模板(C++伪代码)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void backtrack(参数) {
    if (满足结束条件) {
        保存结果;
        return;
    }

    for (每一个可选选择) {
        if (不合法) continue;

        做出选择;
        backtrack(进入下一层); // 递归
        撤销选择; // 回溯
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组合&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combinations/&quot;&gt;77. 组合&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里的关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数字之间的顺序不重要，只要组合内的数字集合相同就认为是相同的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要求输出所有可能的组合，这要求遍历所有可能的选择。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要使用 &lt;code&gt;for&lt;/code&gt; 循环来实现，可能会导致当 &lt;code&gt;k&lt;/code&gt; 较大时，循环的次数会非常多，导致时间复杂度过高。
因此我们可以使用回溯算法来实现。&lt;/p&gt;
&lt;p&gt;我们可以有以下思路：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 参数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;路径（path）：当前已经选择的数字集合，代表当前的决策过程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;候选列表：在数字 1 到 n 中，当前还可以选择的数字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;起点（start）：为了避免重复组合，我们规定每次递归选择数字时只能从当前数字之后的数字中进行选择。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如，第一次选择从 1 开始；如果选了 1，那么接下来只能从 2 开始，这样保证了组合的顺序和唯一性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 终止条件&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当路径的长度等于 k 时，说明当前构建的组合已经完整，此时将当前的路径记录下来，然后返回上层，进行其他分支的探索。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终止条件：if (path.size() == k)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 递归过程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;循环选择：从当前起点 start 到 n 进行遍历，每一次循环：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;选择数字 i：将 i 加入当前路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归调用：更新起点为 i+1，因为后续的选择只考虑比 i 大的数字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;撤销选择：递归返回后，将 i 从路径中移除，以便探索其他可能的数字。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个过程对应于在一棵决策树中深度优先地遍历所有可能的路径。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据&lt;a href=&quot;#%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95&quot;&gt;回溯算法的通用模板&lt;/a&gt;，我们可以实现如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; path; // 全局变量存储组合
vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans; // 全局变量存储所有的组合

void backTracking(int n, int k, int start, vector&amp;lt;int&amp;gt;&amp;amp; path) {
    if (path.size() == k) { // 如果path长度等于k，那么存储结果并返回
        ans.push_back(path);
        return;
    }
    // 单层搜索逻辑
    for (int i = start; i &amp;lt;= n; i++) { // i == n 是有意义的
        path.push_back(i); // 将当前数字 i 添加到组合中
        backTracking(n, k, i + 1, path); // 递归：将起点更新为 i+1，表示后续只选择比 i 大的数字
        path.pop_back(); // 回溯：将 i 移除，恢复状态，以便尝试其他选择
    }
    return;
}
vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combine(int n, int k) {
    backTracking(n, k, 1, path); // 根据题意这里是从1开始
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[剪枝操作]
在 &lt;code&gt;for&lt;/code&gt; 循环中，我们可以进行剪枝操作，避免不必要的递归调用。
例如，如果当前路径的长度加上剩余的数字数量小于 k，那么就没有必要继续递归下去，因为不可能满足条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (path.size() + (n - i + 1) &amp;lt; k) { // 当剩余数字数量小于 k 时，剪枝
    break; // 剪枝操作
}

path_size(): 当前路径的长度
(n - i + 1): 剩余数字的数量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;我们也可以将这个问题抽象成一个树的遍历问题&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E7%BB%84%E5%90%88%5D(https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个节点代表一个数字的选择。&lt;/li&gt;
&lt;li&gt;每个节点的子节点代表在当前选择的基础上，继续选择下一个数字。&lt;/li&gt;
&lt;li&gt;叶子节点代表一个完整的组合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20201123195223940.png&quot; alt=&quot;抽象成树的问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每次从集合中选取元素，可选择的范围随着选择的进行而收缩，调整可选择的范围。&lt;/p&gt;
&lt;p&gt;图中可以发现 &lt;code&gt;n&lt;/code&gt; 相当于树的宽度，&lt;code&gt;k&lt;/code&gt; 相当于树的深度。&lt;/p&gt;
&lt;h2&gt;组合总和III&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum-iii/&quot;&gt;216. 组合总和 III&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里和&lt;a href=&quot;#%E7%BB%84%E5%90%88&quot;&gt;组合问题&lt;/a&gt;是相似的回溯算法，只是添加了一些限制条件，我们可以用相同的思路来写以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; path; // 全局变量存储组合
vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans; // 全局变量存储所有的组合

void backTracking(int n, int k, int start, vector&amp;lt;int&amp;gt;&amp;amp; path) {
    if (n == 0 &amp;amp;&amp;amp; path.size() == k) { // 如果相加之和被减到0, 且path的大小等于k，说明满足条件
        ans.push_back(path);
        return;
    }
    // 单层搜索逻辑
    // 剪枝操作 i + (k - path.size()) + 1 &amp;lt;= 9，和之前的剪枝是一个原理
    for (int i = start; i &amp;lt;= 9; i++) {
        path.push_back(i); // 将当前数字 i 添加到组合中
        backTracking(n - i, k, i + 1, path); // 隐藏的回溯逻辑，n的值没有被改变
        path.pop_back(); // 回溯：将 i 移除，恢复状态，以便尝试其他选择
    }
}

vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combinationSum3(int k, int n) {
    backTracking(n, k, 1, path); // 根据题意这里是从1开始
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[剪枝操作]
在 &lt;code&gt;for&lt;/code&gt; 循环中，进行剪枝避免不必要的递归调用。
例如，如果当前路径的长度加上剩余的数字数量小于 &lt;code&gt;k&lt;/code&gt;，就没有必要继续递归下去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (path.size() + (n - i + 1) &amp;lt; k) { // 当剩余数字数量小于 k 时，剪枝
    break; // 剪枝操作
}
path_size(): 当前路径的长度
(n - i + 1): 剩余数字的数量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;总体来说和之前的组合问题是一个类型。&lt;/p&gt;
&lt;h2&gt;电话号码字母组合&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/&quot;&gt;17. 电话号码的字母组合&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是一个排列问题，不同于组合（无序），本题要求输出所有有顺序的字母排列组合，每一位数字都有自己的字母选择。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;23&quot; -&amp;gt; 两位数字
→ 第1位可选 [&apos;a&apos;,&apos;b&apos;,&apos;c&apos;]
→ 第2位可选 [&apos;d&apos;,&apos;e&apos;,&apos;f&apos;]
目标是从每一位中选出一个字母，组成一个字符串，最终生成所有可能结果。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以把问题想成一个多叉树：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每一层表示一个数字；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每一层的分支是当前数字可选的字母；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们从第一层一路向下选择，直到路径长度和输入数字长度相等，得到一个完整组合。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;我们以“23”为例
                  &quot; &quot;
        /          |          \
        a          b           c                 ← 第一个数字 2 的字母
      / | \      / | \      /  |  \
     d  e  f    d  e  f    d   e   f             ← 第二个数字 3 的字母

从根节点开始，每层做一个选择，最终到达叶子节点，就完成一个组合。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据前面的经验我们可以写出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    string path;
    vector&amp;lt;string&amp;gt; ans;
    // 数字到字母的映射
    vector&amp;lt;string&amp;gt; phoneMap = {
        &quot;&quot;,     // 0
        &quot;&quot;,     // 1
        &quot;abc&quot;,  // 2
        &quot;def&quot;,  // 3
        &quot;ghi&quot;,  // 4
        &quot;jkl&quot;,  // 5
        &quot;mno&quot;,  // 6
        &quot;pqrs&quot;, // 7
        &quot;tuv&quot;,  // 8
        &quot;wxyz&quot;  // 9
    };
public:
    // 回溯函数：用于从 digits[index] 开始，尝试所有可能的组合
    void backTracking(const string &amp;amp;digits, int index) { // index是传入字符串的下标
        // 终止条件：当 index 等于 digits 的长度，说明所有数字都处理完
        if (index == digits.size()) {
            // 到达末尾，把当前路径保存
            if (!path.empty()) ans.push_back(path);
            return;
        }
        // 当前要处理的数字字符，ASCII 值处理方式
        int digit = digits[index] - &apos;0&apos;;
        // 遍历该数字对应的所有字母
        for (char c : phoneMap[digit]) {
            path.push_back(c);
            backTracking(digits, index + 1); // 递归下一层
            path.pop_back();
        }
        return;
    }

    vector&amp;lt;string&amp;gt; letterCombinations(string digits) {
        backTracking(digits, 0); // 从第0位开始处理
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day21-二叉树 part08</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day21_binarytree_part8/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day21_binarytree_part8/</guid><description>二叉树，修建二叉搜索树，将有序数组转换为平衡二叉搜索树，把二叉搜索树转换为累加树</description><pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;修剪二叉搜索树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/trim-a-binary-search-tree/&quot;&gt;669. 修剪二叉搜索树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本题可以使用递归来完成，利用&lt;strong&gt;二叉搜索树的特性&lt;/strong&gt;，我们可以有效地剪枝不符合条件的子树。主要的处理逻辑分为以下几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;递归终止条件&lt;/strong&gt;&lt;br /&gt;
当当前节点为空时，说明已经遍历到空节点，直接返回 &lt;code&gt;nullptr&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当前节点的值小于 low&lt;/strong&gt;&lt;br /&gt;
说明当前节点及其左子树所有节点的值都小于 &lt;code&gt;low&lt;/code&gt;，根据 BST 的性质，左子树上的所有节点也都不符合条件，因此可以&lt;strong&gt;直接舍弃左子树和当前节点&lt;/strong&gt;，递归修剪右子树，并将修剪结果返回。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当前节点的值大于 high&lt;/strong&gt;&lt;br /&gt;
说明当前节点及其右子树所有节点的值都大于 &lt;code&gt;high&lt;/code&gt;，同理可以&lt;strong&gt;舍弃右子树和当前节点&lt;/strong&gt;，递归修剪左子树，并将修剪结果返回。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;当前节点的值在区间 [low, high] 之内&lt;/strong&gt;&lt;br /&gt;
当前节点是合法的，我们保留该节点，并继续分别修剪它的左右子树。将修剪后的左子树接到当前节点的 &lt;code&gt;left&lt;/code&gt; 指针，将修剪后的右子树接到当前节点的 &lt;code&gt;right&lt;/code&gt; 指针。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最终返回当前节点&lt;/strong&gt;&lt;br /&gt;
经过上述判断和修剪后，当前节点要么是合法节点，要么已经替换为左右子树中修剪后合法的部分，最终返回该节点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::important
不能在当前节点值不在 [low, high] 的时候直接 return nullptr，而是应该继续处理它的左子树或右子树。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果当前节点值小于 &lt;code&gt;low&lt;/code&gt; ，那么它的左子树所有值一定也小于 &lt;code&gt;low&lt;/code&gt;，可以舍弃，但右子树可能有合法值，需要递归处理右子树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同理，如果当前节点值大于 &lt;code&gt;high&lt;/code&gt;，左子树可能有合法值，不能直接舍弃整个子树。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;例如一棵树的结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      3
     / \
    3
   / \
  0   4
   \
    2
   /
  1

L = 1, R = 3
遇到节点 0 时判断其不在范围内，直接返回 nullptr，但 0 的右子树中有 2 和 1，是合法的节点，却被错误舍弃了。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们可以根据上面的逻辑写出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* trimBST(TreeNode* root, int low, int high) {
    // 1. 递归终止条件，如果当前节点为空，直接返回空
    if (!root) return nullptr;
    // 2. 如果当前节点的值小于 low，说明当前节点及其左子树都不符合要求，那么我们需要递归修建右子树
    if (root-&amp;gt;val &amp;lt; low) {
        TreeNode* right = trimBST(root-&amp;gt;right, low, high);
        return right;
    }
    // 3. 如果当前节点的值大于high，说明当前节点及其右子树不符合要求，那么需要递归修剪其左子树
    if (root-&amp;gt;val &amp;gt; high) {
        TreeNode* left = trimBST(root-&amp;gt;left, low, high);
        return left;
    }

    // 将向左递归的结果用root的左孩子接住
    root-&amp;gt;left = trimBST(root-&amp;gt;left, low, high);
    // 将向右递归的结果用root的右孩子接住
    root-&amp;gt;right = trimBST(root-&amp;gt;right, low, high);

    return root;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;将有序数组转换为平衡二叉搜索树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/&quot;&gt;108. 将有序数组转换为二叉搜索树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原标题是“将有序数组转换为二叉搜索树”，但实际上要注意是将有序数组转换为&lt;strong&gt;平衡二叉搜索树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;首先平衡树的定义是：对于每个节点，左子树和右子树的高度差不超过 1。然后同时保证这棵树是一棵二叉搜索树。&lt;/p&gt;
&lt;p&gt;在构造二叉树时，一般都可以默认使用数组的中间元素作为根节点。其实本质就是寻找数组的分割点，以分割点作为根节点，左边的元素作为左子树，右边的元素作为右子树。&lt;/p&gt;
&lt;p&gt;核心思路：分治 + 递归&lt;/p&gt;
&lt;p&gt;将升序数组转换为平衡 BST，本质上就是要保持结构对称：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每次选择数组的“中间元素”作为根节点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左半部分构成左子树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右半部分构成右子树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对左右子数组递归执行相同操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们可以有以下的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义递归函数 &lt;code&gt;buildBST(nums, left, right)&lt;/code&gt;，表示构造从 &lt;code&gt;nums[left]&lt;/code&gt; 到 &lt;code&gt;nums[right]&lt;/code&gt; 的子数组对应的平衡 BST&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;left &amp;gt; right&lt;/code&gt;，说明区间无效，返回空指针（递归终止条件）&lt;/li&gt;
&lt;li&gt;计算中间索引：&lt;code&gt;mid = (left + right) / 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建根节点：&lt;code&gt;TreeNode* root = new TreeNode(nums[mid])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;递归构造左子树：&lt;code&gt;root-&amp;gt;left = buildBST(nums, left, mid - 1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;递归构造右子树：&lt;code&gt;root-&amp;gt;right = buildBST(nums, mid + 1, right)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;返回当前构建的 &lt;code&gt;root&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终调用 &lt;code&gt;buildBST(nums, 0, nums.size() - 1)&lt;/code&gt; 得到整棵树的根节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 从 nums[left] 到 nums[right] 构造平衡 BST
TreeNode* buildBST(const vector&amp;lt;int&amp;gt;&amp;amp; nums, int left, int right) {
    // 递归终止条件：区间为空时返回空指针
    if (left &amp;gt; right) return nullptr;
    // 取中间位置作为当前子树的根节点，保持平衡性
    int mid = left + (right - left) / 2; // 防止整型溢出
    TreeNode* root = new TreeNode(nums[mid]); // 创建根节点

    root-&amp;gt;left = buildBST(nums, left, mid - 1); // 递归构建左子树，左半部分 [left, mid - 1]
    root-&amp;gt;right = buildBST(nums, mid + 1, right); // 递归构建右子树，右半部分 [mid + 1, right]

    return root;
}

TreeNode* sortedArrayToBST(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
    return buildBST(nums, 0, nums.size() - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;把二叉搜索树转换为累加树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/convert-bst-to-greater-tree/&quot;&gt;538. 把二叉搜索树转换为累加树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;累加树的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对每个节点，它的新值 = 原值 + 所有 &lt;strong&gt;大于它的节点值之和&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每个节点都会“获得”所有比它大的节点值的“加成”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原本的 BST 被“累加”成了一个新的树结构&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以把二叉搜素树看作一个“有序的数组”，用中序遍历来得到这个数组，以这个数组作为例子。&lt;/p&gt;
&lt;p&gt;假设原始 BST：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      5
     / \
    2   13

中序遍历（左→中→右）得到的序列是：2, 5, 13（升序）  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们希望变成累加树：&lt;/p&gt;
&lt;p&gt;遍历顺序换成&lt;strong&gt;右→中→左&lt;/strong&gt;（从大到小）&lt;br /&gt;
初始 &lt;code&gt;sum = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开始反向遍历：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问 13&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sum += 13 → sum = 13&lt;/li&gt;
&lt;li&gt;节点值 = 13&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问 5&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sum += 5 → sum = 18&lt;/li&gt;
&lt;li&gt;节点值 = 18&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问 2&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sum += 2 → sum = 20&lt;/li&gt;
&lt;li&gt;节点值 = 20&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结果树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      18
     /  \
   20    13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这个树理解为一个“从右向左”不断“累加贡献值”的结构，累加树就非常好理解了。&lt;/p&gt;
&lt;p&gt;所以我们需要实现一个类似数组的“倒序遍历”二叉搜索树，并在遍历的过程中进行累加。从树中可以看出累加的顺序是右中左，那么这个“倒序遍历”其实就是&lt;strong&gt;反向中序遍历&lt;/strong&gt;。所以我们需要反中序遍历这个二叉树，然后顺序累加就可以了。&lt;/p&gt;
&lt;p&gt;在数组中，我们可以用刷换那个指针来实现反向遍历，在二叉搜索树中，我们也可以使用双指针来实现累加的过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用sum来记录当前的累加值，遍历到每个节点时，先把sum加到当前节点的值上，然后再更新sum。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int sum = 0; // 用于记录累加值
void traverse(TreeNode* root) {
    if (root == nullptr) return;
    // 先访问右子树（较大的节点）
    traverse(root-&amp;gt;right); // 右
    // 累加当前节点值
    // 中
    sum += root-&amp;gt;val;
    root-&amp;gt;val = sum;
    // 再访问左子树（较小的节点）
    traverse(root-&amp;gt;left); // 左
}

TreeNode* convertBST(TreeNode* root) {
    traverse(root);
    return root;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day20-二叉树 part07</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day20_binarytree_part7/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day20_binarytree_part7/</guid><description>二叉树，二叉搜索树的最近公共祖先，二叉搜索树中的插入操作，二叉搜索树中的删除操作</description><pubDate>Mon, 31 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;二叉搜索树的最近公共祖先&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/&quot;&gt;235. 二叉搜索树的最近公共祖先&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;二叉搜索树（BST）的特性，对于任意一个节点 &lt;code&gt;root&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;左子树所有值 &amp;lt; &lt;code&gt;root-&amp;gt;val&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右子树所有值 &amp;gt; &lt;code&gt;root-&amp;gt;val&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以根据二叉搜索树的这个特性，我们可以有以下的思路：&lt;/p&gt;
&lt;p&gt;我们从根节点 root 开始，比较 p 和 q 的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;情况一：&lt;code&gt;p.val&lt;/code&gt; &amp;lt; &lt;code&gt;root.val&lt;/code&gt; &amp;amp;&amp;amp; &lt;code&gt;q.val&lt;/code&gt; &amp;lt; &lt;code&gt;root.val&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;→ 说明 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 都在左子树 → 往左走&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况二：&lt;code&gt;p.val&lt;/code&gt; &amp;gt; &lt;code&gt;root.val&lt;/code&gt; &amp;amp;&amp;amp; &lt;code&gt;q.val&lt;/code&gt; &amp;gt; &lt;code&gt;root.val&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;→ 说明 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 都在右子树 → 往右走&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;情况三：&lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 分别在两边&lt;/p&gt;
&lt;p&gt;→ 当前 &lt;code&gt;root&lt;/code&gt; 就是最近公共祖先&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[为什么当 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 分别在两边时，当前 &lt;code&gt;root&lt;/code&gt; 就是最近公共祖先]
在 BST 中，每个节点都有如下性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;所有 左子树节点 &amp;lt; 当前节点值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有 右子树节点 &amp;gt; 当前节点值&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设我们在当前节点 &lt;code&gt;root&lt;/code&gt; 发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;p &amp;lt; root-&amp;gt;val&lt;/code&gt; &amp;amp;&amp;amp; &lt;code&gt;q &amp;gt; root-&amp;gt;val&lt;/code&gt; 或者相反&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;说明它们分布在两边&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;→ 如果再往上走一层，&lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 仍然是它的子孙，但距离更远&lt;/p&gt;
&lt;p&gt;→ 如果再往下走一层，那一边就不包含两个节点了&lt;/p&gt;
&lt;p&gt;所以，“第一次分叉的地方”一定是最近公共祖先。
:::&lt;/p&gt;
&lt;p&gt;因此我们可以写出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    // 如果 p 和 q 都比 root 小，说明 LCA 在左子树
    if (p-&amp;gt;val &amp;lt; root-&amp;gt;val &amp;amp;&amp;amp; q-&amp;gt;val &amp;lt; root-&amp;gt;val) {
        return lowestCommonAncestor(root-&amp;gt;left, p, q);
    }
    // 如果 p 和 q 都比 root 大，说明 LCA 在右子树
    else if (p-&amp;gt;val &amp;gt; root-&amp;gt;val &amp;amp;&amp;amp; q-&amp;gt;val &amp;gt; root-&amp;gt;val) {
        return lowestCommonAncestor(root-&amp;gt;right, p, q);
    }
    // 否则，说明分布在两边，当前 root 就是最近公共祖先
    else {
        return root;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉搜索树中的插入操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/&quot;&gt;701. 二叉搜索树中的插入操作&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们要做的事很简单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从根节点出发，根据大小关系，递归或迭代地走到合适的位置，然后插入新节点。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当前节点不为空：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;val &amp;lt; root-&amp;gt;val&lt;/code&gt;，递归插入到左子树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;val &amp;gt; root-&amp;gt;val&lt;/code&gt;，递归插入到右子树&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前节点为空（走到叶子外部了）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建新节点并返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以可以写出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* insertIntoBST(TreeNode* root, int val) {
    // 如果当前节点为空，说明找到了插入位置
    if (root == nullptr) {
    return new TreeNode(val);
    }
    // 如果插入值小于当前节点，插入到左子树
    if (val &amp;lt; root-&amp;gt;val) {
    root-&amp;gt;left = insertIntoBST(root-&amp;gt;left, val);
    }
    // 如果插入值大于当前节点，插入到右子树
    else if (val &amp;gt; root-&amp;gt;val) {
    root-&amp;gt;right = insertIntoBST(root-&amp;gt;right, val);
    }
    // 返回当前节点（整棵树最终返回 root）
    return root;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么不需要“重新排序”或者“重构整棵树”，只需要在正确的位置插入就行？]
我们在“每一步决策”里都遵守了 BST 的基本规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;val&lt;/code&gt; &amp;lt; 当前节点 → 插入左子树&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;val&lt;/code&gt; &amp;gt; 当前节点 → 插入右子树&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样保证了“局部有序性”：插入后，每一层都仍然满足 BST 条件，再加上 BST 是递归结构，每个子树也是一棵 BST，所以这种局部有序性会自动延续下去。&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;只要遵守 “小左大右” 的递归结构，BST 的性质就保持住了&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h2&gt;二叉树中的删除操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/delete-node-in-a-bst/&quot;&gt;450. 二叉搜索树中的删除操作&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;相比插入节点相比，删除的逻辑更复杂一些。&lt;/p&gt;
&lt;p&gt;递归函数的终止条件是：当当前节点为空时，返回空指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (root == nullptr) return nullptr;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们大致可以分为三种情况，也可以看作是五种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;未找到节点，直接返回空指针&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除的节点是叶子节点，直接删除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仅有右子树存在，右子树补位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仅有左子树存在，左子树补位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左右子树都存在，找到右子树的最小节点（后继），将当前节点的左子树接到它的左侧&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这里的最小节点是指：在右子树中，最左边的节点（也就是最小值）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之后将当前节点的左子树接到它的左侧&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后返回当前节点的右子树为新的根节点&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下为第五种情况的动画演示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.gif&quot; alt=&quot;删除节点演示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据上面的逻辑，我们可以写出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* deleteNode(TreeNode* root, int key) {
    if (root == nullptr) return nullptr; // 情况 1：未找到节点，直接返回空

    if (key &amp;lt; root-&amp;gt;val) {
        // 说明目标值在左子树中，递归删除
        root-&amp;gt;left = deleteNode(root-&amp;gt;left, key);
    } 
    else if (key &amp;gt; root-&amp;gt;val) {
        // 说明目标值在右子树中，递归删除
        root-&amp;gt;right = deleteNode(root-&amp;gt;right, key);
    } 
    else {
        // 情况 2~5：找到目标节点，开始分类处理

        // 情况 2：叶子节点，直接删除
        if (root-&amp;gt;left == nullptr &amp;amp;&amp;amp; root-&amp;gt;right == nullptr) {
            delete root;
            return nullptr;
        }
        // 情况 3：仅右子树存在，右子树补位
        else if (root-&amp;gt;left == nullptr) {
            TreeNode* retNode = root-&amp;gt;right;
            delete root;
            return retNode;
        }
        // 情况 4：仅左子树存在，左子树补位
        else if (root-&amp;gt;right == nullptr) {
            TreeNode* retNode = root-&amp;gt;left;
            delete root;
            return retNode;
        }
        // 情况 5：左右子树都存在
        // 找到右子树的最小节点（后继），将当前节点的左子树接到它的左侧
        else {
            TreeNode* successor = findMin(root-&amp;gt;right);
            successor-&amp;gt;left = root-&amp;gt;left;
            TreeNode* temp = root;
            root = root-&amp;gt;right;  // 把 root-&amp;gt;right 提上来作为新的根
            delete temp;
            return root;
        }
    }
    return root;
}

// 辅助函数：查找某个子树的最小节点（一直往左走）
TreeNode* findMin(TreeNode* node) {
    while (node-&amp;gt;left != nullptr) {
        node = node-&amp;gt;left;
    }
    return node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;操作二叉搜索树的关键在于始终遵循“小于放左，大于放右”的结构规律。无论是查找、插入还是删除，本质都是在利用这一规则不断缩小问题规模。&lt;/p&gt;
&lt;p&gt;二叉搜索树添加节点只需要在叶子上添加就可以的，不涉及到结构的调整，而删除节点操作涉及到结构的调整。&lt;/p&gt;
</content:encoded></item><item><title>Day18-二叉树 part06</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day18_binarytree_part6/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day18_binarytree_part6/</guid><description>二叉树，二叉树搜索树的最小绝对值差， 二叉搜索树中的众数, 二叉树的最近公共祖先</description><pubDate>Sun, 30 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;二叉树搜索树的最小绝对值差&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/&quot;&gt;530.二叉搜索树的最小绝对值差&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;利用二叉搜素树的特性&lt;/h3&gt;
&lt;p&gt;这里的思路与&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/binarytree/day17_binarytree_part5/#%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91&quot;&gt;验证二叉搜索树&lt;/a&gt;其实是一致的，需要明确的是，二叉搜索树进行中序遍历的时候一定是一个递增的序列。&lt;/p&gt;
&lt;p&gt;所以可以利用这个特性来求解最小绝对值差。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void traversal(TreeNode* node, vector&amp;lt;int&amp;gt;&amp;amp; nums) { // 中序遍历搜索二叉树，返回有序数组
    if (!node) return;
    if (node-&amp;gt;left) traversal(node-&amp;gt;left, nums); // 左
    nums.push_back(node-&amp;gt;val); // 中
    if (node-&amp;gt;right) traversal(node-&amp;gt;right, nums); // 右
}
int getMinimumDifference(TreeNode* root) {
    vector&amp;lt;int&amp;gt; vec;
    if (!root) return 0;
    traversal(root, vec);
    // 求数组中的最小绝对值差
    int ans = INT_MAX;
    for (int i = 1; i &amp;lt; vec.size(); i++) {
        int temp = abs(vec[i] - vec[i - 1]); // -&amp;gt; temp = min(temp, abs(vec[i] - vec[i - 1]));
        if (temp &amp;lt; ans) ans = temp;
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二叉搜索树的双指针遍历&lt;/h3&gt;
&lt;p&gt;这里在遍历的时候可以定义两个指针 &lt;code&gt;cur&lt;/code&gt; 和 &lt;code&gt;pre&lt;/code&gt;，&lt;code&gt;cur&lt;/code&gt; 指向当前节点，&lt;code&gt;pre&lt;/code&gt; 指向前一个节点。&lt;/p&gt;
&lt;p&gt;然后在遍历的时候，&lt;code&gt;cur&lt;/code&gt; 和 &lt;code&gt;pre&lt;/code&gt; 之间的差值就是当前节点和前一个节点之间的差值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int ans = INT_MAX; // 定义全局变量 ans，初始值为 INT_MAX
void traversal (TreeNode* cur, TreeNode*&amp;amp; pre) { // 这里 pre 是引用类型
    if (!cur) return;
    if (cur-&amp;gt;left) traversal(cur-&amp;gt;left, pre);
    if (pre) { // 当pre为空的时候不会进行这一步
        ans = min(ans, cur-&amp;gt;val - pre-&amp;gt;val); // 计算差值
    }
    pre = cur; // 更新pre指针
    if (cur-&amp;gt;right) traversal(cur-&amp;gt;right, pre);
}
int getMinimumDifference(TreeNode* root) {
    if (!root) return 0;
    TreeNode *pre = nullptr;
    traversal(root, pre);
    return ans;
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么pre要使用引用类型]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cur&lt;/code&gt; 的作用
&lt;code&gt;cur&lt;/code&gt; 是当前递归函数正在处理的节点。它是一个普通的指针，因为每次递归调用时，&lt;code&gt;cur&lt;/code&gt; 都会被赋值为树中的下一个节点（左子节点或右子节点）。递归调用时，&lt;code&gt;cur&lt;/code&gt; 的值会被复制到新的栈帧中，因此不需要使用引用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pre&lt;/code&gt; 的作用
&lt;code&gt;pre&lt;/code&gt; 是用来记录当前节点的前一个节点，以便计算两个节点值之间的差值。由于递归函数需要在不同的递归栈帧之间共享和更新 &lt;code&gt;pre&lt;/code&gt; 的值，因此必须使用引用类型 &lt;code&gt;TreeNode*&amp;amp;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果 &lt;code&gt;pre&lt;/code&gt; 不使用引用类型，而是普通指针 &lt;code&gt;TreeNode*&lt;/code&gt;，那么在递归调用中对 &lt;code&gt;pre&lt;/code&gt; 的修改只会影响当前栈帧的局部变量，而不会影响外层递归栈帧中的 &lt;code&gt;pre&lt;/code&gt;。这会导致 &lt;code&gt;pre&lt;/code&gt; 无法正确地在整个递归过程中保持更新。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么 cur 不需要引用
&lt;code&gt;cur&lt;/code&gt; 是递归函数的输入参数，每次递归调用时都会传入新的节点值。即使不使用引用，递归调用时 &lt;code&gt;cur&lt;/code&gt; 的值也会正确传递到下一个栈帧中，因此不需要用引用
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二叉搜索树中的众数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/&quot;&gt;501.二叉搜索树中的众数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里直观的思路是使用一个哈希表来存储每个节点的值和出现的次数，然后遍历哈希表，找出出现次数最多的值。&lt;/p&gt;
&lt;p&gt;首先递归遍历二叉树（何种遍历顺序都无所谓），统计每个节点的值出现的次数，存入 &lt;code&gt;map&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;之后遍历 &lt;code&gt;map&lt;/code&gt;，找出出现次数最多的值。&lt;/p&gt;
&lt;p&gt;这里就不多介绍这种一般的解法了。&lt;/p&gt;
&lt;p&gt;下面使用和&lt;a href=&quot;#%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E5%8F%8C%E6%8C%87%E9%92%88%E9%81%8D%E5%8E%86&quot;&gt;二叉树中的双指针遍历&lt;/a&gt;一样的思路，使用 &lt;code&gt;pre&lt;/code&gt; 和 &lt;code&gt;cur&lt;/code&gt; 来遍历二叉树。可以遍历两遍二叉树，一遍遍历找出最大值，另一遍遍历找出众数。&lt;/p&gt;
&lt;p&gt;不过这里介绍的还是只遍历一次二叉树的写法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int count = 0; // 定义全局变量 count，初始值为 0
int maxCount = 0; // 定义全局变量 maxCount，初始值为 0
vector&amp;lt;int&amp;gt; ans; // 定义全局变量 ans，存储众数
void traversal(TreeNode* cur, TreeNode*&amp;amp; pre) {
    if (!cur) return;
    if (cur-&amp;gt;left) traversal(cur-&amp;gt;left, pre); // 左

    if (!pre) count = 1; // 当遍历到第一个节点时，还没有pre此时count就直接等于1
    else if (cur-&amp;gt;val == pre-&amp;gt;val) count++; // 当前后节点的数值相等时，count加1
    else count = 1; // 否则，前后数值不相等了，重新让count等于1
    pre = cur; // 赋值pre指针

    if (count &amp;gt; maxCount) { // 当count的值大于maxCount时
        maxCount = count; // 更新maxCount
        ans.clear(); // 此时因为更新maxCount，结果集中的就不是出现最多的数了，因为要清空结果集
        ans.push_back(cur-&amp;gt;val); // 将新的值放入结果集中
    } else if (count == maxCount) { // 否则当count和maxCount相等时
        ans.push_back(cur-&amp;gt;val); // 加入结果集
    }

    if (cur-&amp;gt;right) traversal(cur-&amp;gt;right, pre); // 右
    return;
}
vector&amp;lt;int&amp;gt; findMode(TreeNode* root) {
    if (!root) return ans;
    TreeNode *pre = nullptr;
    traversal(root, pre);
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[更新结果集]
频率 &lt;code&gt;count&lt;/code&gt; 大于 &lt;code&gt;maxCount&lt;/code&gt; 的时候，不仅要更新 &lt;code&gt;maxCount&lt;/code&gt;，而且要清空结果集，因为结果集之前的元素都失效了。
:::&lt;/p&gt;
&lt;h2&gt;二叉树的最近公共祖先&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/&quot;&gt;236.二叉树的最近公共祖先&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里涉及到二叉树的一个自底向上处理的一个思路，当要向上处理的时候，可以使用后序遍历的方式来解决。&lt;/p&gt;
&lt;p&gt;在查找公共祖先的时候有以下的几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果找到一个节点，发现左子树出现结点 &lt;code&gt;p&lt;/code&gt;，右子树出现节点 &lt;code&gt;q&lt;/code&gt;，或者左子树出现结点 &lt;code&gt;q&lt;/code&gt;，右子树出现节点 &lt;code&gt;p&lt;/code&gt;，那么该节点就是节点 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 的最近公共祖先&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20220922173502.png&quot; alt=&quot;二叉树公共节点1&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;节点本身 &lt;code&gt;p(q)&lt;/code&gt;，它拥有一个子孙节点 &lt;code&gt;q(p)&lt;/code&gt;, 那么该节点就是节点 &lt;code&gt;p&lt;/code&gt; 和 &lt;code&gt;q&lt;/code&gt; 的最近公共祖先&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20220922173530.png&quot; alt=&quot;二叉树公共节点2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;判断的逻辑是：如果递归遍历遇到 &lt;code&gt;q&lt;/code&gt;，就将 &lt;code&gt;q&lt;/code&gt; 返回，遇到 &lt;code&gt;p&lt;/code&gt; 就将 &lt;code&gt;p&lt;/code&gt; 返回，那么如果左右子树的返回值都不为空，说明此时的中节点，一定是 &lt;code&gt;q&lt;/code&gt; 和 &lt;code&gt;p&lt;/code&gt; 的最近祖先。&lt;/p&gt;
&lt;p&gt;其实情况一和情况二代码实现过程都是一样的，也可以说，实现情况一的逻辑，顺便包含了情况二。因为遇到 &lt;code&gt;q&lt;/code&gt; 或者 &lt;code&gt;p&lt;/code&gt; 就返回，这样也包含了 &lt;code&gt;q&lt;/code&gt; 或者 &lt;code&gt;p&lt;/code&gt; 本身就是公共祖先的情况。&lt;/p&gt;
&lt;p&gt;完整的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 如果当前节点为空，说明递归到底了，返回空
        if (root == nullptr) {
            return nullptr;
        }

        // 如果当前节点就是 p 或 q，那么说明找到了其中一个
        // 直接返回这个节点（不再继续往下找）
        if (root == p || root == q) {
            return root;
        }

        // 在左子树中递归查找 p 和 q
        TreeNode* leftResult = lowestCommonAncestor(root-&amp;gt;left, p, q);

        // 在右子树中递归查找 p 和 q
        TreeNode* rightResult = lowestCommonAncestor(root-&amp;gt;right, p, q);

        // 情况一：如果左子树和右子树都不为空，说明 p 和 q 分别出现在两边
        // 此时当前 root 就是最近公共祖先
        if (leftResult != nullptr &amp;amp;&amp;amp; rightResult != nullptr) {
            return root;
        }

        // 情况二：如果只有左子树不为空，说明 p 和 q 都在左边
        if (leftResult != nullptr) {
            return leftResult;
        }

        // 情况三：如果只有右子树不为空，说明 p 和 q 都在右边
        if (rightResult != nullptr) {
            return rightResult;
        }

        // 情况四：左右子树都没找到，返回空
        return nullptr;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;求最小公共祖先，需要从底向上遍历，那么二叉树，只能通过后序遍历（即：回溯）实现从底向上的遍历方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在回溯的过程中，必然要遍历整棵二叉树，即使已经找到结果了，依然要把其他节点遍历完，因为要使用递归函数的返回值（也就是代码中的 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt;）做逻辑判断。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要理解如果返回值 &lt;code&gt;left&lt;/code&gt; 为空，&lt;code&gt;right&lt;/code&gt; 不为空为什么要返回 &lt;code&gt;right&lt;/code&gt;，为什么可以用返回 &lt;code&gt;right&lt;/code&gt; 传给上一层结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在确定单层递归逻辑的时候，要关注函数是否有返回值，是因为回溯的过程需要递归函数的返回值做判断，也可以从是否要遍历整棵树来做判断。&lt;/p&gt;
&lt;p&gt;:::tip[二叉树-递归函数返回值]
是否需要返回值，取决于是否需要从子树返回“信息”给父节点来“做决策”。
如果要“从子树拿信息回来” —— 要返回值；
如果只是“走一遍树做操作” —— 不需要返回值。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题类型&lt;/th&gt;
&lt;th&gt;是否需要返回值&lt;/th&gt;
&lt;th&gt;典型例子&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;查找、判断、返回某个节点&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;最近公共祖先、查找值、路径求和&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;纯遍历或修改树结构&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;遍历打印、翻转、修改值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:::&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果递归函数有返回值，如何区分要搜索一条边，还是搜索整个树呢？&lt;/p&gt;
&lt;p&gt;是否遍历整个树，取决于是否需要从“多个子树中综合信息”，还是在**某一条路径上找到目标后立刻返回，不再继续搜索。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索一条边（路径）的写法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (递归函数(root-&amp;gt;left)) return true;
if (递归函数(root-&amp;gt;right)) return true;
return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;搜索整个树写法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;auto left = 递归函数(root-&amp;gt;left);    // 左子树递归
auto right = 递归函数(root-&amp;gt;right);  // 右子树递归
// 根据 left 和 right 的返回值判断逻辑
return 结果; 
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day17-二叉树 part05</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day17_binarytree_part5/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day17_binarytree_part5/</guid><description>二叉树，二叉搜索树，最大二叉树，合并二叉树，验证二叉搜索树</description><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;最大二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/maximum-binary-tree/&quot;&gt;654.最大二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们可以参考使用前序+中序数组来构造二叉树的思路来写，首先根节点就是数组中的最大值，然后根据最大值的下标，我们可以区分左和右区间。&lt;/p&gt;
&lt;p&gt;假设最大值下标是 &lt;code&gt;maxIndex&lt;/code&gt;，那么左区间就是 &lt;code&gt;[left, maxIndex)&lt;/code&gt;，右区间就是&lt;code&gt;[maxIndex+1, right)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后在区间中分别递归调用，直到区间的左边界大于等于右边界。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* traversal(vector&amp;lt;int&amp;gt;&amp;amp; nums, int left, int right) { // 传入数组的左边界和有边界
    if (left &amp;gt;= right) { // 如果左边界 &amp;gt;= 右边界， 那么就返回空节点
        return nullptr;
    }
    // 找到最大值和下标
    int maxIndex = left; // 初始化最大值的下标是数组区间的最左边的值的下标
    for (int i = left + 1; i &amp;lt; right; i++) { // 寻找最大值的下标，从左边界的下一个开始
        if (nums[i] &amp;gt; nums[maxIndex]) {
            maxIndex = i;
        }
    }
    // 前序遍历
    TreeNode* node = new TreeNode(nums[maxIndex]); // 定义新节点，最大值
    // 左区间 [left, maxindex)
    node-&amp;gt;left = traversal(nums, left, maxIndex); // 左
    // 右区间 [maxIndex + 1, right)
    node-&amp;gt;right = traversal(nums, maxIndex + 1, right); // 右

    return node;
}

TreeNode* constructMaximumBinaryTree(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
    if (nums.empty()) return nullptr;
    int left = 0, right = nums.size(); // 因为需要左开右闭区间，所以是[0, nums.size())
    return traversal(nums, left, right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我觉得这种方式更好理解一些所以采用的这种写法，如果要完全按照之前的思路分成两个数组来做也是可以的，就是稍微麻烦一点。&lt;/p&gt;
&lt;h2&gt;合并二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/merge-two-binary-trees/&quot;&gt;617.合并二叉树&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;递归实现&lt;/h3&gt;
&lt;p&gt;这题的思路其实不需要想的太复杂，遍历两棵树的逻辑其实和遍历一棵树是一样的，只不过需要传入两个树的根节点。&lt;/p&gt;
&lt;p&gt;所以我们的递归函数需要输入的是两个树的根节点， 返回合并后的根节点。&lt;/p&gt;
&lt;p&gt;然后要确定递归函数的终止条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果两个树都是空的，返回空节点&lt;/li&gt;
&lt;li&gt;如果树1是空的，那么合并的只有树2，也就是可以返回树2的根节点&lt;/li&gt;
&lt;li&gt;如果树2是空的，那么合并的只有树1，也就是可以返回树1的根节点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之后是确定单层的递归逻辑，当两个树都不为空时，我们就可以新建一个节点，值为两棵树的根节点之和。
然后递归调用左子树和右子树的合并函数，分别传入两棵树的左子树和右子树。&lt;/p&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* mergeTrees(TreeNode* ro root1, TreeNode* root2) {
    if (ro root1 == nullptr &amp;amp;&amp;amp; root2 == nullptr) { // 如果两个树都是空的，返回空
        return nullptr;
    }
    if (ro root1 == nullptr) { // 如果树1是空的，返回树2
        return root2;
    }
    if (root2 == nullptr) { // 如果树2是空的，返回树1
        return root1;
    }
    // 如果两个树都不为空，那么就合并
    TreeNode* node = new TreeNode(ro root1-&amp;gt;val + root2-&amp;gt;val); // 新建一个节点，值为两棵树的根节点之和
    node-&amp;gt;left = mergeTrees(ro root1-&amp;gt;left, root2-&amp;gt;left); // 左子树合并
    node-&amp;gt;right = mergeTrees(ro root1-&amp;gt;right, root2-&amp;gt;right); // 右子树合并

    return node; // 返回合并后的节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这里的遍历顺序用三种之一都可以，不过用前序遍历更好理解。&lt;/p&gt;
&lt;h3&gt;迭代实现&lt;/h3&gt;
&lt;p&gt;对于同时操作两棵树，我们可以使用队列，将两棵树的节点同时加入队列进行比较，最后我们可以返回树1的根节点。&lt;/p&gt;
&lt;p&gt;同样的，我们有以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果两棵树左节点都不为空，加入队列&lt;/li&gt;
&lt;li&gt;如果两棵树右节点都不为空，加入队列&lt;/li&gt;
&lt;li&gt;root1的左节点 为空 root2左节点不为空，就赋值过去&lt;/li&gt;
&lt;li&gt;root1的右节点 为空 root2右节点不为空，就赋值过去&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后我们只需要返回树1的根节点即可（因为修改的是root1）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
    if  root1 == NULL) return root2;
    if (root2 == NULL) return root1;
    queue&amp;lt;TreeNode*&amp;gt; que;
    que.push root1);
    que.push(root2);
    while(!que.empty()) {
        TreeNode* node1 = que.front(); que.pop();
        TreeNode* node2 = que.front(); que.pop();
        // 此时两个节点一定不为空，val相加
        node1-&amp;gt;val += node2-&amp;gt;val;

        // 如果两棵树左节点都不为空，加入队列
        if (node1-&amp;gt;left != NULL &amp;amp;&amp;amp; node2-&amp;gt;left != NULL) {
            que.push(node1-&amp;gt;left);
            que.push(node2-&amp;gt;left);
        }
        // 如果两棵树右节点都不为空，加入队列
        if (node1-&amp;gt;right != NULL &amp;amp;&amp;amp; node2-&amp;gt;right != NULL) {
            que.push(node1-&amp;gt;right);
            que.push(node2-&amp;gt;right);
        }

        //  root1的左节点 为空 t2左节点不为空，就赋值过去
        if (node1-&amp;gt;left == NULL &amp;amp;&amp;amp; node2-&amp;gt;left != NULL) {
            node1-&amp;gt;left = node2-&amp;gt;left;
        }
        //  root1的右节点 为空 t2右节点不为空，就赋值过去
        if (node1-&amp;gt;right == NULL &amp;amp;&amp;amp; node2-&amp;gt;right != NULL) {
            node1-&amp;gt;right = node2-&amp;gt;right;
        }
    }
    return root1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉搜索树中的搜索&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/search-in-a-binary-search-tree/&quot;&gt;700.二叉搜索树中的搜索&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;递归实现&lt;/h3&gt;
&lt;p&gt;因为二叉搜索树的性质是左子树的值小于根节点的值，右子树的值大于根节点的值，所以我们可以根据这个性质来进行搜索。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前节点的值等于目标值，返回当前节点&lt;/li&gt;
&lt;li&gt;如果当前节点的值大于目标值，搜索左子树&lt;/li&gt;
&lt;li&gt;如果当前节点的值小于目标值，搜索右子树&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据这个逻辑我们可以有以下递归的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* searchBST(TreeNode* root, int val) {
    if (!root || root-&amp;gt;val == val) return root; // 1. 如果当前节点为空或者当前节点的值等于目标值，返回当前节点
    // 2. 如果当前节点的值大于目标值，搜索左子树
    if (root-&amp;gt;val &amp;gt; val) return searchBST(root-&amp;gt;left, val);
    // 3. 如果当前节点的值小于目标值，搜索右子树
    if (root-&amp;gt;val &amp;lt; val) return searchBST(root-&amp;gt;right, val);
    // 都没有找到，返回空节点
    return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迭代实现&lt;/h3&gt;
&lt;p&gt;迭代的写法相对就简单了，因为二叉搜索树的特殊性，也就是节点的有序性，可以不使用辅助栈或者队列就可以写出迭代法。并且对于二叉搜索树，不需要回溯的过程，因为节点的有序性就帮我们确定了搜索的方向。&lt;/p&gt;
&lt;p&gt;从根节点开始，如果当前节点的值等于目标值，返回当前节点；如果当前节点的值大于目标值，搜索左子树；如果当前节点的值小于目标值，搜索右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* searchBST(TreeNode* root, int val) {
    TreeNode* node = root; // 定义一个节点，指向根节点
    while (node != nullptr) { // 如果节点不为空
        if (node-&amp;gt;val == val) { // 如果节点的值等于目标值，返回该节点
            return node;
        } else if (node-&amp;gt;val &amp;lt; val) { // 如果节点的值小于目标值，去右子树查找
            node = node-&amp;gt;right;
        } else { // 如果节点的值大于目标值，去左子树查找
            node = node-&amp;gt;left;
        }
    }
    return nullptr; // 如果没有找到，返回空
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证二叉搜索树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/validate-binary-search-tree/&quot;&gt;98.验证二叉搜索树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在二叉搜索树中，每个节点的值都大于其左子树的所有节点的值，并且小于其右子树的所有节点的值。&lt;/p&gt;
&lt;p&gt;因此使用中序遍历的方式来遍历二叉搜索树，得到的结果应该是一个升序的数组。&lt;/p&gt;
&lt;p&gt;所以我们可以使用这个特性，将中序遍历的结果存储在一个数组中，然后判断这个数组是否是升序的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void inorder(TreeNode* root, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
    if (root == nullptr) { // 如果节点为空，返回
        return;
    }
    inorder(root-&amp;gt;left, nums); // 左子树
    nums.push_back(root-&amp;gt;val); // 中序遍历，先左子树，再根节点，最后右子树
    inorder(root-&amp;gt;right, nums); // 右子树
}
bool isValidBST(TreeNode* root) {
    vector&amp;lt;int&amp;gt; nums; // 定义一个数组，存储中序遍历的结果
    inorder(root, nums); // 中序遍历
    for (int i = 1; i &amp;lt; nums.size(); i++) { // 遍历数组
        if (nums[i] &amp;lt;= nums[i - 1]) { // 如果当前值小于等于前一个值，返回false
            return false;
        }
    }
    return true; // 如果没有返回false，说明是二叉搜索树，返回true
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day16-二叉树 part04</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day16_binarytree_part4/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day16_binarytree_part4/</guid><description>二叉树，找树左下角的值，路径总和，从中序与后序遍历序列构造二叉树</description><pubDate>Thu, 27 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;找树左下角的值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-bottom-left-tree-value/&quot;&gt;513.找树左下角的值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里如果使用层序遍历那么就很轻松，只要记录最后一层的第一个节点的值即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int findBottomLeftValue(TreeNode* root) {
    if (!root) return 0;
    queue&amp;lt;TreeNode*&amp;gt; que;
    que.push(root);
    int ans;
    while (!que.empty()) {
        int size = que.size();
        vector&amp;lt;int&amp;gt; vec;
        while (size--) {
            TreeNode* node = que.front();
            que.pop();
            vec.push_back(node-&amp;gt;val);
            if (node-&amp;gt;left) que.push(node-&amp;gt;left);
            if (node-&amp;gt;right) que.push(node-&amp;gt;right);
        }
        ans = vec[0];
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以使用递归遍历的方式，我们只要找到深度最大的叶子节点，然后要找左节点的话，我们只需要使用前序遍历（或者是中序，后序都行，因为左一直在右的前面）来保证优先搜索左边节点，然后记录深度最大的叶子节点，此时就是树的最后一行最左边的值。&lt;/p&gt;
&lt;p&gt;在递归过程中，每当深度大于当前最大深度时，我们就更新最大深度和最左边的值。所以可以定义两个全局变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int maxDepth = INT_MIN; // 记录最大深度
int ans = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在找最大深度的时候，递归的过程中依然要使用回溯。&lt;/p&gt;
&lt;p&gt;我们可以有以下递归代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int maxDepth = INT_MIN; // 全局变量记录最大深度
int result = 0; // 全局变量记录最左边的值
void traversal(TreeNode* node, int depth) { // 1. 传入根节点，和当前深度
    // 2. 递归终止条件，遇到叶子节点
    if (!node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;right) { // 找到叶子节点，比较当前深度是否大于最大深度
        if (depth &amp;gt; maxDepth) { // 如果当前深度大于最大深度，更新最大深度和结果
            maxDepth = depth;
            result = node-&amp;gt;val;
        }
    }
    // 3. 单层递归逻辑
    if (node-&amp;gt;left) { // 左
        depth++;
        traversal(node-&amp;gt;left, depth); // -&amp;gt; traversal(node-&amp;gt;left, depth + 1);
        depth--; // 回溯
    }
    if (node-&amp;gt;right) { // 右
        depth++;
        traversal(node-&amp;gt;right, depth); // -&amp;gt; traversal(node-&amp;gt;right, depth + 1);
        depth--; // 回溯
    }
    return;
}

int findBottomLeftValue(TreeNode* root) {
    traversal(root, 0);
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是这里的遍历顺序其实只需要保证左节点在右节点前面就行，因为没有对中间节点的处理过程。&lt;/p&gt;
&lt;h2&gt;路径总和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/path-sum/&quot;&gt;112.路径总和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里和上面一样因为没有中间节点的处理逻辑，所以用什么遍历顺序都可以。&lt;/p&gt;
&lt;p&gt;关于递归函数什么时候需要返回值，可以根据需不需要处理递归返回值来进行判断。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果需要搜索整棵二叉树且不用处理递归返回值，递归函数就不要返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果需要搜索整棵二叉树且需要处理递归返回值，递归函数就需要返回值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果要搜索其中一条符合条件的路径，那么递归一定需要返回值，因为遇到符合条件的路径了就要及时返回。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里进行统计的值，我们可以使用减法的方式来统计，当遍历到叶子节点是，如果当前总和为0，那么就是找到了一条符合条件的路径。&lt;/p&gt;
&lt;p&gt;这样的做法要比使用加法更简洁，很多时候使用减法的做法会更简单。&lt;/p&gt;
&lt;p&gt;整体的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; bool dfs(TreeNode* node, int sum) { // 1. 返回值：是否找到一条符合条件的路径，参数：当前节点，当前总和
    if (!node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;right) { // 2. 递归终止条件，遇到叶子节点
        if (sum == 0) return true; // 如果当前总和为0，那么就是找到了一条符合条件的路径
        else return false; // 否则返回false
    }
    // 3. 单层递归逻辑
    if (node-&amp;gt;left) { // 左
        sum -= node-&amp;gt;left-&amp;gt;val; // 先减去当前节点的值
        if (dfs(node-&amp;gt;left, sum)) return true; // -&amp;gt; if(dfs(node-&amp;gt;left, sum - node-&amp;gt;left-&amp;gt;val))
        sum += node-&amp;gt;left-&amp;gt;val; // 回溯， 加上当前节点的值
    }

    if (node-&amp;gt;right) {
        sum -= node-&amp;gt;right-&amp;gt;val; // 先减去当前节点的值
        if (dfs(node-&amp;gt;right, sum)) return true; // -&amp;gt; if(dfs(node-&amp;gt;right, sum - node-&amp;gt;right-&amp;gt;val))
        sum += node-&amp;gt;right-&amp;gt;val; // 回溯， 加上当前节点的值
    }
    return false;
}
bool hasPathSum(TreeNode* root, int targetSum) {
    if (!root) return false;
    return dfs(root, targetSum - root-&amp;gt;val); // 这里要提前减去root的值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是回溯问题，先减去当前节点的值，然后递归，递归完之后再加上当前节点的值。&lt;/p&gt;
&lt;h2&gt;从中序与后序遍历序列构造二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/&quot;&gt;106.从中序与后序遍历序列构造二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在二叉树遍历顺序中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中序遍历：左 -&amp;gt; 中 -&amp;gt; 右&lt;/li&gt;
&lt;li&gt;后序遍历：左 -&amp;gt; 右 -&amp;gt; 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以在后序遍历中，最后一个节点一定是根节点，然后在中序遍历中，根节点的左边是左子树，右边是右子树。&lt;/p&gt;
&lt;p&gt;因此可以根据这个规则，来切割中序数组，分成左右子树&lt;/p&gt;
&lt;p&gt;然后再根据左右子树在后序遍历中的位置，来切割后序数组，分成左右子树&lt;/p&gt;
&lt;p&gt;当后序数组为空时，就返回空节点。&lt;/p&gt;
&lt;p&gt;所以大体的思路可以分为以下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果数组大小为零的话，说明是空节点了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不为空，那么取后序数组最后一个元素作为节点元素。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找到后序数组最后一个元素在中序数组的位置，作为切割点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切割中序数组，切成中序左数组和中序右数组 （顺序别搞反了，一定是先切中序数组）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切割后序数组，切成后序左数组和后序右数组&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归处理左区间和右区间&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么难点就在于如何切分中序数组和后序数组。&lt;/p&gt;
&lt;p&gt;在切割的过程中会产生四个区间，分别是中序左区间，中序右区间，后序左区间，后序右区间。&lt;/p&gt;
&lt;p&gt;切割点在后序数组的最后一个元素，就是用这个元素来切割中序数组的，所以必要先切割中序数组。&lt;/p&gt;
&lt;p&gt;中序数组相对比较好切，找到切割点（后序数组的最后一个元素）在中序数组的位置，然后切割：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 4. 切割中序数组
// 左子树区间 [0, delimiterIndex)
vector&amp;lt;int&amp;gt; leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); 
// 右子树区间 [delimiterIndex + 1, end)
vector&amp;lt;int&amp;gt; rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end()); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于后序数组，首先需要舍弃最后一个元素，因为最后一个元素是根节点已经被使用了，然后再找到切割点在中序数组。后序数组没有明确的切割元素来进行左右切割，不像中序数组有明确的切割点，切割点左右分开就可以了。&lt;/p&gt;
&lt;p&gt;此时有一个很关键的点，就是中序数组大小一定是和后序数组的大小相同的，那么我们可以根据中序数组的大小来切割后序数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// postorder 舍弃末尾元素
postorder.resize(postorder.size() - 1);

// 5. 切割后序数组
// 依然左闭右开，注意这里使用了左中序数组大小作为切割点
// [0, leftInorder.size)
vector&amp;lt;int&amp;gt; leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector&amp;lt;int&amp;gt; rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时完整的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* traversal(vector&amp;lt;int&amp;gt;&amp;amp; inorder, vector&amp;lt;int&amp;gt;&amp;amp; postorder) {
    // 1. 当后序数组为空，返回空节点 
    if (postorder.empty()) return nullptr; 
    // 2. 后序遍历数组最后一个元素，就是当前的中间节点
    int rootValue = postorder[postorder.size() - 1];
    TreeNode* root = new TreeNode(rootValue);

    // 当后序数组的大小为1时，也就是叶子节点，返回这个节点
    if (postorder.size() == 1) return root;

    // 3. 找到中序遍历的切割点
    int delimiterIndex;
    for (delimiterIndex = 0; delimiterIndex &amp;lt; inorder.size(); delimiterIndex++) {
        if (inorder[delimiterIndex] == rootValue) break;
    }
    // 4. 切割中序数组
    // 左子树区间 [0, delimiterIndex)
    vector&amp;lt;int&amp;gt; leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); 
    // 右子树区间 [delimiterIndex + 1, end)
    vector&amp;lt;int&amp;gt; rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end()); 

    // postorder 舍弃末尾元素
    postorder.resize(postorder.size() - 1);

    // 5. 切割后序数组
    // 依然左闭右开，注意这里使用了左中序数组大小作为切割点
    // [0, leftInorder.size)
    vector&amp;lt;int&amp;gt; leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
    // [leftInorder.size(), end)
    vector&amp;lt;int&amp;gt; rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

    // 6. 递归处理左区间和右区间
    root-&amp;gt;left = traversal(leftInorder, leftPostorder);
    root-&amp;gt;right = traversal(rightInorder, rightPostorder);

    return root;
}

TreeNode* buildTree(vector&amp;lt;int&amp;gt;&amp;amp; inorder, vector&amp;lt;int&amp;gt;&amp;amp; postorder) {
    if (inorder.size() == 0 || postorder.size() == 0) return NULL;
    return traversal(inorder, postorder);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;a href=&quot;https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/&quot;&gt;105.从前序与中序遍历序列构造二叉树&lt;/a&gt;也是类似的思路，只是切割点在前序数组的第一个元素。因为前序遍历是中左右，所以前序数组的第一个元素就是根节点。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;关于递归函数的写法，什么时候需要返回值，传入的参数是什么，有些还是不太清楚，需要多练习。&lt;/p&gt;
&lt;p&gt;像这种查找路径或者是涉及到一些需要深度来判断的问题，我们需要多留意回溯的问题，有递归就一定有回溯。&lt;/p&gt;
&lt;p&gt;从中序数组和后序数组，或者是从前序数组和中序数组都可以构造二叉树，因为可以根据中间节点来区分左右子树。
但是从前序数组和后序数组是无法构造二叉树的，因为前序和后序数组都没有明确的切割点来区分左右子树。&lt;/p&gt;
</content:encoded></item><item><title>Day15-二叉树 part03</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day15_binarytree_part3/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day15_binarytree_part3/</guid><description>二叉树，平衡二叉树，二叉树的所有路径，左子叶之和，完全二叉树的节点个数</description><pubDate>Wed, 26 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;平衡二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/balanced-binary-tree/&quot;&gt;110.平衡二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;平衡二叉树的定义是：对于树中的每个节点，其左右子树的高度差的绝对值不超过1。&lt;/p&gt;
&lt;p&gt;既然要求的是高度，那么可以使用后序遍历的方式，&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;明确递归函数的参数和返回值&lt;/p&gt;
&lt;p&gt;参数：当前传入节点。 返回值：以当前传入节点为根节点的树的高度。&lt;/p&gt;
&lt;p&gt;如果当前传入节点为根节点的二叉树已经不是二叉平衡树了，还返回高度的话就没有意义了。&lt;/p&gt;
&lt;p&gt;所以如果已经不是二叉平衡树了，可以返回-1 来标记已经不符合平衡树的规则了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确终止条件&lt;/p&gt;
&lt;p&gt;递归的过程中依然是遇到空节点了为终止，返回0，表示当前节点为根节点的树高度为0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确单层递归的逻辑&lt;/p&gt;
&lt;p&gt;如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢？当然是其左子树高度和其右子树高度的差值。&lt;/p&gt;
&lt;p&gt;分别求出其左右子树的高度，然后如果差值小于等于1，则返回当前二叉树的高度，否则返回-1，表示已经不是二叉平衡树了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么整体代码可以如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int checkHeight(TreeNode* root) {
    if (!root) return 0;
    int left = checkHeight(root-&amp;gt;left); // 左
    if (left == -1) return -1; // 如果左子树已经不是平衡树了，直接返回-1

    int right = checkHeight(root-&amp;gt;right); // 右
    if (right == -1) return -1; // 如果右子树已经不是平衡树了，直接返回-1

    if (abs(left - right) &amp;gt; 1) return -1; // 如果当前节点的左右子树高度差大于1，说明不是平衡树，直接返回-1
    return max(left, right) + 1; // 是平衡树的情况下返回当前节点为根节点的高度
}

bool isBalanced(TreeNode* root) {
    return checkHeight(root) != -1; // 只需要比较是否等于-1即可
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树的所有路径&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-paths/&quot;&gt;257.二叉树的所有路径&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里要求的是从根节点到叶子节点的所有路径，所以很自然可以想到使用前序遍历。因为前序遍历是中左右，符合从根节点到叶子节点的路径。&lt;/p&gt;
&lt;p&gt;并且在这里涉及到回溯，每当找完一条路径（也就是访问到叶子节点的时候），需要回退到上一个节点，继续遍历其他路径。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;明确递归函数的参数和返回值&lt;/p&gt;
&lt;p&gt;参数：当前传入节点，一个用来存储路径的 &lt;code&gt;path&lt;/code&gt;，一个用来存储所有路径的结果 &lt;code&gt;result&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void traversal(TreeNode* node, vector&amp;lt;int&amp;gt;&amp;amp; path, vector&amp;lt;string&amp;gt;&amp;amp; result)
// 中，因为最后一个节点也要加入到path中
path.push_back(node-&amp;gt;val);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确终止条件&lt;/p&gt;
&lt;p&gt;递归的过程中遇到叶子节点的时候，那么需要把 &lt;code&gt;path&lt;/code&gt;中存储的节点记录下来保存到 &lt;code&gt;result&lt;/code&gt;中，然后返回。在这里可以不需要判断当前节点是否为空。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;right) { // 终止条件：遇到叶子节点
        string sPath;
        for (int i = 0; i &amp;lt; path.size() - 1; i++) { // 遍历到倒数第二个
            sPath += to_string(path[i]);
            sPath += &quot;-&amp;gt;&quot;;
        }
        sPath += to_string(path[path.size() - 1]); // 最后一个节点不需要加-&amp;gt;
        result.push_back(sPath);
        return;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确单层递归的逻辑&lt;/p&gt;
&lt;p&gt;然后是递归和回溯的过程，上面说过没有判断是否为空，那么在这里递归的时候，如果为空就不进行下一层递归了。&lt;/p&gt;
&lt;p&gt;需要注意的是回溯和递归是一一对应的，因此在每次递归之后，需要回溯到上一个节点。&lt;/p&gt;
&lt;p&gt;所以递归前要加上判断语句，下面要递归的节点是否为空，整体如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (node-&amp;gt;left) {
    traversal(node-&amp;gt;left, path, result);
    path.pop_back(); // 回溯
}
if (node-&amp;gt;right) {
    traversal(node-&amp;gt;right, path, result);
    path.pop_back(); //回溯
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;string&amp;gt; binaryTreePaths(TreeNode* root) {
    vector&amp;lt;string&amp;gt; ans;
    vector&amp;lt;int&amp;gt; path;
    if (!root) return ans;
    traversal(root, path, ans);
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;左子叶之和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sum-of-left-leaves/&quot;&gt;404.左叶子之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里提到的是求左子叶之和，我们可以使用后序遍历的方式，将左右子树的左子叶之和返回给上一层。&lt;/p&gt;
&lt;p&gt;那么如何判断一个节点是左子叶呢？&lt;/p&gt;
&lt;p&gt;:::tip
如果一个节点的左节点不为空，且该左节点的左右节点都为空，那么这个节点就是左子叶。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;明确递归函数的参数和返回值&lt;/p&gt;
&lt;p&gt;参数：当前传入节点。 返回值：当前节点的左子叶之和。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int traversal (TreeNode* node)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确终止条件&lt;/p&gt;
&lt;p&gt;如果遍历到空节点，那么左叶子值一定是0。
当前遍历的节点是叶子节点，那其左叶子也必定是0。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!node) return 0;
if (!node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;right) return 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;明确单层递归的逻辑&lt;/p&gt;
&lt;p&gt;根据后序遍历，先往左递归，因为最后的结果一定会出现在往左递归的过程中，所以结果要在这一过程中保存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int left = traversal(node-&amp;gt;left);
if (node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;left-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;left-&amp;gt;right) {
    left = node-&amp;gt;left-&amp;gt;val;
}
int right = traversal(node-&amp;gt;right);

return left + right;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int traversal (TreeNode* node) {
    if (!node) return 0;
    if (!node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;right) return 0;

    int left = traversal(node-&amp;gt;left); // 左
    if (node-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;left-&amp;gt;left &amp;amp;&amp;amp; !node-&amp;gt;left-&amp;gt;right) {
        left = node-&amp;gt;left-&amp;gt;val; 
    }
    int right = traversal(node-&amp;gt;right); // 右
    
    return left + right; // 中
}
int sumOfLeftLeaves(TreeNode* root) {
    return traversal(root);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完全二叉树的节点个数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/count-complete-tree-nodes/&quot;&gt;222.完全二叉树的节点个数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里可以简单地使用后序遍历的方式，分别求出左右子树的节点个数，然后加上根节点，就是整个完全二叉树的节点个数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int traversal(TreeNode* node) {
    if (!node) return 0;

    int left = traversal(node-&amp;gt;left); // 左
    int right = traversal(node-&amp;gt;right); // 右

    int res = left + right + 1; // 中
    return res;
}
int countNodes(TreeNode* root) {
    if (!root) return 0;
    return traversal(root);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完全二叉树中，除了最底层节点可能没填满外，其余每层节点数都达到最大值，并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 &lt;code&gt;h&lt;/code&gt; 层，则该层包含 &lt;code&gt;1~ 2^(h-1)&lt;/code&gt;  个节点。&lt;/p&gt;
&lt;p&gt;完全二叉树只有两种情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情况一：就是满二叉树，可以直接用 &lt;code&gt;2^树深度 - 1&lt;/code&gt; 来计算，注意这里根节点深度为1。&lt;/li&gt;
&lt;li&gt;情况二：最后一层叶子节点没有满。分别递归左孩子，和右孩子，递归到某一深度一定会有左孩子或者右孩子为满二叉树，然后依然可以按照情况1来计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里的关键在于如何去判断一个左子树或者右子树是不是满二叉树。&lt;/p&gt;
&lt;p&gt;在完全二叉树中，如果递归向左遍历的深度等于递归向右遍历的深度，那说明就是满二叉树。如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20220829163709.png&quot; alt=&quot;完全二叉树1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在完全二叉树中，如果递归向左遍历的深度不等于递归向右遍历的深度，则说明不是满二叉树&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20220829163709.png&quot; alt=&quot;完全二叉树2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;判断其子树是不是满二叉树，如果是则利用公式计算这个子树（满二叉树）的节点数量，如果不是则继续递归，那么 在递归三部曲中，第二部：终止条件的写法应该是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (root == nullptr) return 0; 
// 开始根据左深度和右深度是否相同来判断该子树是不是满二叉树
TreeNode* left = root-&amp;gt;left;
TreeNode* right = root-&amp;gt;right;
int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的，为了下面求指数方便
while (left) {  // 求左子树深度
    left = left-&amp;gt;left;
    leftDepth++;
}
while (right) { // 求右子树深度
    right = right-&amp;gt;right;
    rightDepth++;
}
if (leftDepth == rightDepth) {
    return (2 &amp;lt;&amp;lt; leftDepth) - 1; // 注意(2&amp;lt;&amp;lt;1) 相当于2^2，返回满足满二叉树的子树节点数量
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三部，单层递归的逻辑：（可以看出使用后序遍历）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int leftTreeNum = countNodes(root-&amp;gt;left);       // 左
int rightTreeNum = countNodes(root-&amp;gt;right);     // 右
int result = leftTreeNum + rightTreeNum + 1;    // 中
return result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int countNodes(TreeNode* root) {
    if (root == nullptr) return 0;
    TreeNode* left = root-&amp;gt;left;
    TreeNode* right = root-&amp;gt;right;
    int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的，为了下面求指数方便
    while (left) {  // 求左子树深度
        left = left-&amp;gt;left;
        leftDepth++;
    }
    while (right) { // 求右子树深度
        right = right-&amp;gt;right;
        rightDepth++;
    }
    if (leftDepth == rightDepth) {
        return (2 &amp;lt;&amp;lt; leftDepth) - 1; // 注意(2&amp;lt;&amp;lt;1) 相当于2^2，所以leftDepth初始为0
    }
    return countNodes(root-&amp;gt;left) + countNodes(root-&amp;gt;right) + 1;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Day14-二叉树 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day14_binarytree_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day14_binarytree_part2/</guid><description>二叉树，翻转二叉树，对称二叉树，二叉树的最大深度，二叉树的最小深度</description><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;翻转二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/invert-binary-tree/&quot;&gt;226.翻转二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于二叉树的翻转，其实用前序遍历和后序遍历都能实现，但是用中序遍历的话会使一些节点的左右子树翻转两次。&lt;/p&gt;
&lt;p&gt;如果使用中序遍历的话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      4                   4
    /   \  数次遍历后    /   \
   2     7    ---&amp;gt;     7     2  --&amp;gt; 此时再遍历右子树会使以2位根节点的子树再次翻转
  / \   / \           / \   / \
 1   3 6   9         9   6 3   1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以实现其实很简单，在遍历的时候只要交换左右子树然后递归遍历左右子树即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* invertTree(TreeNode* root) {
    if (!root) return root;
    if (!root-&amp;gt;left &amp;amp;&amp;amp; !root-&amp;gt;right) return root; // 递归终止条件，是叶子节点直接返回
    // swap(root-&amp;gt;left, root-&amp;gt;right);
    // 中
    TreeNode* node = root-&amp;gt;right;
    root-&amp;gt;right = root-&amp;gt;left;
    root-&amp;gt;left = node;

    if (root-&amp;gt;left) invertTree(root-&amp;gt;left); // 左
    if (root-&amp;gt;right) invertTree(root-&amp;gt;right); // 右
    return root;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用迭代的同意写法方式来实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TreeNode* invertTree(TreeNode* root) { // 前序遍历
    stack&amp;lt;TreeNode*&amp;gt; stk;
    if (root) stk.push(root);
    while (!stk.empty()) {
        TreeNode* node = stk.top();
        if (node) {
            stk.pop();
            
            if (node-&amp;gt;right) stk.push(node-&amp;gt;right); // 右
            if (node-&amp;gt;left) stk.push(node-&amp;gt;left); // 左
            // 中
            stk.push(node);
            stk.push(nullptr);
        }
        else {
            stk.pop();
            node = stk.top();
            stk.pop();
            swap(node-&amp;gt;left, node-&amp;gt;right);
        }
    }
    return root;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对称二叉树&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/symmetric-tree/&quot;&gt;101.对称二叉树&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;要判断一个二叉树是否为对称二叉树，需要判断左右子树节点是否对称：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果左子树节点和右子树节点都为空，那么说明左右对称，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果左右子树有其中一个为空，则说明不对称，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果左右子树节点都不为空，那么需要判断左右子树的值是否相等，且左子树的左节点和右子树的右节点对称，左子树的右节点和右子树的左节点对称。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;递归写法&lt;/h3&gt;
&lt;p&gt;那么在递归函数中需要传入两个节点（t1, t2分别表示左右子树）来判断，根据上面的条件可以写出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 左右子树节点都为空
if (!t1 &amp;amp;&amp;amp; !t2) return true;
// 2. 左右子树有其中一个为空
if (!t1 || !t2) return false; // 如果上面条件满足那么就会直接返回 true 不会进行这个判断。
// 3. 左右子树节点都不为空
if (t1-&amp;gt;val != t2-&amp;gt;val) return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以可以有以下的递归函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isMirro(TreeNode* t1, TreeNode* t2) {
    if (!t1 &amp;amp;&amp;amp; !t2) return true;
    if (!t1 || !t2) return false;

    return ((t1-&amp;gt;val == t2-&amp;gt;val) 
            &amp;amp;&amp;amp; isMirro(t1-&amp;gt;left, t2-&amp;gt;right) // 左子树的左节点和右子树的右节点对称
            &amp;amp;&amp;amp; isMirro(t1-&amp;gt;right, t2-&amp;gt;left)); // 左子树的右节点和右子树的左节点对称
}

bool isSymmetric(TreeNode* root) { 
    if (!root) return false;
    return isMirro(root-&amp;gt;left, root-&amp;gt;right); // 传入左右子树节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迭代写法&lt;/h3&gt;
&lt;p&gt;在这里我们可以使用队列来实现迭代的写法，队列中存放的是左右子树的节点，每次取出两个节点进行比较，然后将左子树的左节点和右子树的右节点入队，左子树的右节点和右子树的左节点入队。&lt;/p&gt;
&lt;p&gt;使用队列的图示如下&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91%5D(https://programmercarl.com/0101.%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/101.%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91.gif&quot; alt=&quot;使用队列判断对称二叉树&quot; /&gt;&lt;/p&gt;
&lt;p&gt;迭代写法的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isSymmetric(TreeNode* root) { // 迭代写法
    if (!root) return true;
    queue&amp;lt;TreeNode*&amp;gt; que;
    que.push(root-&amp;gt;left);
    que.push(root-&amp;gt;right);

    while (!que.empty()) {
        TreeNode* t1 = que.front(); // 取出两个节点
        que.pop();
        TreeNode* t2 = que.front();
        que.pop();

        if (!t1 &amp;amp;&amp;amp; !t2) continue; // 1. 左右子树节点都为空
        if (!t1 || !t2 || t1-&amp;gt;val != t2-&amp;gt;val) return false; // 2. 左右子树有其中一个为空或者值不相等

        que.push(t1-&amp;gt;left); // 将左子树的左节点和右子树的右节点入队
        que.push(t2-&amp;gt;right);
        que.push(t1-&amp;gt;right);// 将左子树的右节点和右子树的左节点入队
        que.push(t2-&amp;gt;left);
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树的最大深度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/&quot;&gt;104.二叉树的最大深度&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果求的是二叉树的深度，那么可以用前序遍历，如果求的是二叉树的高度，那么可以用后序遍历。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二叉树节点的深度：指从根节点到该节点的最长简单路径边的条数或者节点数（取决于深度从0开始还是从1开始）&lt;/li&gt;
&lt;li&gt;二叉树节点的高度：指从该节点到叶子节点的最长简单路径边的条数或者节点数（取决于高度从0开始还是从1开始）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而二叉树的最大深度就是二叉树的高度。&lt;/p&gt;
&lt;p&gt;所以这题可以很轻松的解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int maxDepth(TreeNode* root) {
        if (!root) return 0; // 如果是空节点，返回高度0

        int left = maxDepth(root-&amp;gt;left); // 左，递归求左子树的高度
        int right = maxDepth(root-&amp;gt;right); // 右，递归求右子树的高度
        return max(left, right) + 1; // 中，返回左右子树的最大高度加上根节点的高度
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然还可以使用层序遍历来解决，遍历的层数就是二叉树的高度，也就是最大深度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int maxDepth(TreeNode* root) {
    int depth = 0;
    if (!root) return depth;
    queue&amp;lt;TreeNode*&amp;gt; que;
    que.push(root);
    while (!que.empty()) { // 层序遍历
        depth++; // 每遍历一层，深度加1
        int size = que.size();
        while (size--) {
            TreeNode* node = que.front();
            que.pop();

            if (node-&amp;gt;left) que.push(node-&amp;gt;left);
            if (node-&amp;gt;right) que.push(node-&amp;gt;right);
        }
    }
    return depth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树的最小深度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/&quot;&gt;111.二叉树的最小深度&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里和&lt;a href=&quot;#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6&quot;&gt;二叉树的最大深度&lt;/a&gt;不同的是，最小深度是从根节点到最近叶子节点的最短路径上的节点数量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个节点是空节点，那么它的最小深度为0。&lt;/li&gt;
&lt;li&gt;如果一个节点只有左子树，右子树为空，那么它的最小深度为左子树的最小深度+1。&lt;/li&gt;
&lt;li&gt;如果一个节点只有右子树，左子树为空，那么它的最小深度为右子树的最小深度+1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此这里是与最大深度的本质区别，如果按照最大深度的写法，那么可能会返回空子树的深度。&lt;/p&gt;
&lt;p&gt;同样是后序遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int minDepth(TreeNode* root) {
    if (!root) return 0;

    int left = minDepth(root-&amp;gt;left); // 左
    int right = minDepth(root-&amp;gt;right); // 右

    if (!root-&amp;gt;left &amp;amp;&amp;amp; root-&amp;gt;right) { // 如果左子树为空，右子树不为空
        return right + 1; // 返回右子树的最小深度+1
    }
    if (root-&amp;gt;left &amp;amp;&amp;amp; !root-&amp;gt;right) { // 如果右子树为空，左子树不为空
        return left + 1; // 返回左子树的最小深度+1
    }

    return min(left, right) + 1; // 返回左右子树的最小深度+1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;二叉树的翻转，可以使用前序遍历和后序遍历，也可以使用迭代的方式实现。&lt;/li&gt;
&lt;li&gt;对称二叉树，要同时判断左右子树的相应左右节点是否对称。&lt;/li&gt;
&lt;li&gt;对于于求二叉树的高度，用前序最合适&lt;/li&gt;
&lt;li&gt;对于求二叉树的深度，用后序最合适&lt;/li&gt;
&lt;li&gt;对于二叉树的最小深度，需要注意左右子树为空的情况。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day13-二叉树 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/binarytree/day13_binarytree_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/binarytree/day13_binarytree_part1/</guid><description>二叉树，二叉树的各种遍历方式</description><pubDate>Mon, 24 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;二叉树&lt;/h2&gt;
&lt;p&gt;二叉树是一种树形结构，它的每个节点最多有两个子节点，分别是左子节点和右子节点。&lt;/p&gt;
&lt;p&gt;二叉树的存储方式一般是链式存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用数组来存储二叉树，假设父节点的下标是 &lt;code&gt;i&lt;/code&gt;，那么左子节点的下标是 &lt;code&gt;2*i+1&lt;/code&gt;，右子节点的下标是 &lt;code&gt;2*i+2&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;二叉树的递归遍历&lt;/h2&gt;
&lt;p&gt;二叉树的遍历方式主要有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;深度优先搜索（DFS）：前序遍历、中序遍历、后序遍历（递归，迭代）&lt;/li&gt;
&lt;li&gt;广度优先搜索（BFS）：层序遍历（跌代）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们先看深度优先搜索的递归实现，也就是三种常见的遍历方式。
这里遍历的顺序其实就是根节点的访问顺序。&lt;/p&gt;
&lt;p&gt;关于递归的写法，我们可以确定三个要素&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86%5D(https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定递归函数的参数和返回值：确定哪些参数是递归的过程中需要处理的，那么就在递归函数里加上这个参数， 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定终止条件：写完了递归算法, 运行的时候，经常会遇到栈溢出的错误，就是没写终止条件或者终止条件写的不对，操作系统也是用一个栈的结构来保存每一层递归的信息，如果递归没有终止，操作系统的内存栈必然就会溢出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单层递归的逻辑：确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;前序遍历&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-preorder-traversal/&quot;&gt;二叉树的前序遍历&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;按照递归写法的思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确定递归函数的参数和返回值：这里要存储遍历的结果，所以要传入一个 &lt;code&gt;vector&amp;lt;int&amp;gt;&lt;/code&gt; 来存储结果，返回值是 &lt;code&gt;void&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定终止条件：当节点为空时，直接返回。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单层递归的逻辑：先访问根节点，再递归访问左子树，再递归访问右子树。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void traversal(TreeNode* root, vector&amp;lt;int&amp;gt;&amp;amp; vec) { // 1. 确定递归函数的参数和返回值
    if (!root) return; // 2. 确定终止条件
    // 3. 单层递归的逻辑
    vec.push_back(root-&amp;gt;val); // 中
    if (root-&amp;gt;left) traversal(root-&amp;gt;left, vec); // 左
    if (root-&amp;gt;right) traversal(root-&amp;gt;right, vec); // 右
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确定好递归函数之后，遍历的函数就很好写了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; preorderTraversal(TreeNode* root) {
    if (!root) return {};
    vector&amp;lt;int&amp;gt; ans;
    traversal(root, ans);
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中序遍历&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-inorder-traversal/&quot;&gt;二叉树的中序遍历&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;依照之前的思路，我们只需要在前序遍历中的代码调整一下遍历顺序即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void traversal(TreeNode* root, vector&amp;lt;int&amp;gt;&amp;amp; vec) { // 1. 确定递归函数的参数和返回值
    if (!root) return; // 2. 确定终止条件
    // 3. 单层递归的逻辑
    if (root-&amp;gt;left) traversal(root-&amp;gt;left, vec); // 左
    vec.push_back(root-&amp;gt;val); // 中
    if (root-&amp;gt;right) traversal(root-&amp;gt;right, vec); // 右
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-postorder-traversal/&quot;&gt;二叉树的后序遍历&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;写法同上，只需要调整遍历顺序即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void traversal(TreeNode* root, vector&amp;lt;int&amp;gt;&amp;amp; vec) { // 1. 确定递归函数的参数和返回值
    if (!root) return; // 2. 确定终止条件
    // 3. 单层递归的逻辑
    if (root-&amp;gt;left) traversal(root-&amp;gt;left, vec); // 左
    if (root-&amp;gt;right) traversal(root-&amp;gt;right, vec); // 右
    vec.push_back(root-&amp;gt;val); // 中
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树的迭代遍历&lt;/h2&gt;
&lt;p&gt;在二叉树的迭代遍历中，我们需要借助栈来实现。
递归的实现方式是隐式地使用了系统栈，而迭代的实现方式是显式地使用一个栈来模拟系统栈。&lt;/p&gt;
&lt;h3&gt;前序遍历&lt;/h3&gt;
&lt;p&gt;在前序遍历中，遍历的顺序是中左右，所以我们先将根节点入栈，然后处理根节点，&lt;strong&gt;再将右子节点入栈，再将左子节点入栈&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::tip[前序遍历的入栈顺序]
先入栈右节点，再入栈左节点，这样在出栈的时候就是先左后右的顺序。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; preorderTraversal(TreeNode* root) {
    // 迭代法
    stack&amp;lt;TreeNode*&amp;gt; stk;
    vector&amp;lt;int&amp;gt; ans;
    if (!root) return ans;
    stk.push(root); // 先入栈根节点
        
    while (!stk.empty()) {
        // 处理中节点
        TreeNode* node = stk.top(); 
        stk.pop();
        ans.push_back(node-&amp;gt;val);

        if (node-&amp;gt;right) stk.push(node-&amp;gt;right); // 右
        if (node-&amp;gt;left) stk.push(node-&amp;gt;left); // 左
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历&lt;/h3&gt;
&lt;p&gt;先序遍历是中左右，后序遍历是左右中，那么我们只需要调整一下先序遍历的代码顺序，就变成中右左的遍历顺序，然后在反转result数组，输出的结果顺序就是左右中了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前序遍历-&amp;gt;中左右&lt;/li&gt;
&lt;li&gt;调整代码顺序-&amp;gt;中右左&lt;/li&gt;
&lt;li&gt;反转结果集-&amp;gt;左右中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此可以根据前序遍历的代码稍作调整即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; postorderTraversal(TreeNode* root) {
    // 迭代法
    stack&amp;lt;TreeNode*&amp;gt; stk;
    vector&amp;lt;int&amp;gt; ans;
    if (!root) return ans;
    stk.push(root); // 先入栈根节点
        
    while (!stk.empty()) {
        // 处理中间节点
        TreeNode* node = stk.top();
        stk.pop();
        ans.push_back(node-&amp;gt;val);

        if (node-&amp;gt;left) stk.push(node-&amp;gt;left); // 左
        if (node-&amp;gt;right) stk.push(node-&amp;gt;right); // 右
    }
    reverse(ans.begin(), ans.end()); // 反转结果集
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中序遍历&lt;/h3&gt;
&lt;p&gt;在这里需要注意的是，之前的前序和后序遍历的代码在中序遍历中不能通用，因为之前的代码的处理过程是：先访问中间节点，处理的也是中间节点，&lt;strong&gt;因为要访问的元素和要处理的元素都是同一个&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而中序遍历的顺序是左中右，我们需要一步一步访问到最左下的节点，然后再处理中间节点，这造成了访问的元素和处理的元素不是同一个。&lt;/p&gt;
&lt;p&gt;那么在使用迭代法写中序遍历，就需要借用指针的遍历来帮助访问节点，栈则用来处理节点上的元素。&lt;/p&gt;
&lt;p&gt;所以在中序遍历中，我们需要先将根节点入栈，然后一直往左走，直到走到最左下的节点，然后处理这个节点，再处理右子节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; inorderTraversal(TreeNode* root) {
    // 迭代法
    stack&amp;lt;TreeNode*&amp;gt; stk;
    vector&amp;lt;int&amp;gt; ans;
    TreeNode* node = root; // 指针遍历
    while (node || !stk.empty()) { // 当node不为空或者栈不为空时
        while (node) { // 一直往左走
            stk.push(node);
            node = node-&amp;gt;left;
        }
        // 处理中间节点
        node = stk.top(); 
        stk.pop(); 
        ans.push_back(node-&amp;gt;val);

        node = node-&amp;gt;right; // 处理右子节点
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迭代遍历的统一写法&lt;/h3&gt;
&lt;p&gt;这里我们以中序遍历为例，来解决上述提到的访问元素和处理元素不一致的问题。&lt;/p&gt;
&lt;p&gt;我们可以&lt;strong&gt;将访问的节点放入栈中，把要处理的节点也放入栈中但是要做标记&lt;/strong&gt;，这样在处理的时候就可以知道这个节点是要处理的节点还是要访问的节点。&lt;/p&gt;
&lt;p&gt;这里有两种标记的方式&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%BF%AD%E4%BB%A3%E9%81%8D%E5%8E%86%5D(https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E7%BB%9F%E4%B8%80%E8%BF%AD%E4%BB%A3%E6%B3%95.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^2&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;就是要处理的节点放入栈之后，紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加一个 &lt;code&gt;boolean&lt;/code&gt; 值跟随每个节点，&lt;code&gt;false&lt;/code&gt;(默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次，&lt;code&gt;true&lt;/code&gt; 表示该节点的位次之前已经安排过了，可以收割节点了。 这种方法可以叫做 &lt;code&gt;boolean&lt;/code&gt; 标记法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 空指针标记法
vector&amp;lt;int&amp;gt; inorderTraversal(TreeNode* root) {
    vector&amp;lt;int&amp;gt; result;
    stack&amp;lt;TreeNode*&amp;gt; st;
    if (root != NULL) st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top();
        if (node != NULL) {
            st.pop(); // 将该节点弹出，避免重复操作，下面再将右中左节点添加到栈中
            if (node-&amp;gt;right) st.push(node-&amp;gt;right);  // 添加右节点（空节点不入栈）

            st.push(node);                          // 添加中节点
            st.push(NULL); // 中节点访问过，但是还没有处理，加入空节点做为标记。

            if (node-&amp;gt;left) st.push(node-&amp;gt;left);    // 添加左节点（空节点不入栈）
        } else { // 只有遇到空节点的时候，才将下一个节点放进结果集
            st.pop();           // 将空节点弹出
            node = st.top();    // 重新取出栈中元素
            st.pop();
            result.push_back(node-&amp;gt;val); // 加入到结果集
        }
    }
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// boolean 标记法
vector&amp;lt;int&amp;gt; inorderTraversal(TreeNode* root) {
    // 迭代法
    stack&amp;lt;pair&amp;lt;TreeNode*, bool&amp;gt;&amp;gt; stk;
    vector&amp;lt;int&amp;gt; ans;
    if (!root) return ans;
    stk.push({root, false}); // 先入栈根节点
        
    while (!stk.empty()) {
        auto [node, flag] = stk.top(); // stk.top() 返回的是一个pair
        stk.pop();
        if (flag) ans.push_back(node-&amp;gt;val); // 处理节点
        else { // 访问节点
            if (node-&amp;gt;right) stk.push({node-&amp;gt;right, false}); // 右
            stk.push({node, true}); // 标记
            if (node-&amp;gt;left) stk.push({node-&amp;gt;left, false}); // 左
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树的层序遍历&lt;/h2&gt;
&lt;p&gt;层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和之前的都不太一样。&lt;/p&gt;
&lt;p&gt;需要借用一个辅助数据结构即队列来实现，队列先进先出，符合一层一层遍历的逻辑，而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; levelOrder(TreeNode* root) {
    queue&amp;lt;TreeNode*&amp;gt; que;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
    if (!root) return ans;
    que.push(root);
    while (!que.empty()) { // 当队列不为空时
        int size = que.size();  // 记录当前层的节点个数，que.size()是动态变化的
        vector&amp;lt;int&amp;gt; vec; // 存储当前层的节点值
        for (int i = 0; i &amp;lt; size; i++) {    // 遍历当前层的节点
            TreeNode* node = que.front(); // 取出队首元素
            que.pop();
            vec.push_back(node-&amp;gt;val); // 存储当前层的节点值

            if (node-&amp;gt;left) que.push(node-&amp;gt;left); // 将左子节点入队
            if (node-&amp;gt;right) que.push(node-&amp;gt;right); // 将右子节点入队
        }
        ans.push_back(vec);
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;总的来说递归的写法看起来还是简单易懂，不过使用迭代的写法，就需要思考很多，希望自己能熟练掌握这种迭代写法的。&lt;/p&gt;
&lt;p&gt;层序遍历的应用相当广泛，后序自己也会把随想录中提到的题目都做一遍。&lt;/p&gt;
</content:encoded></item><item><title>Day11-栈与队列 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/stack-queue/day11_stack-part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/stack-queue/day11_stack-part2/</guid><description>栈，队列，逆波兰表达式，滑动窗口最大值，前k个高频元素</description><pubDate>Sat, 22 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;逆波兰表达式&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/&quot;&gt;逆波兰表达式&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;逆波兰表达式&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;逆波兰表达式是一种后缀表达式，所谓后缀就是指算符写在后面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平常使用的算式则是一种中缀表达式，如 &lt;code&gt;( 1 + 2 ) * ( 3 + 4 )&lt;/code&gt; 。&lt;/li&gt;
&lt;li&gt;该算式的逆波兰表达式写法为 &lt;code&gt;( ( 1 2 + ) ( 3 4 + ) * )&lt;/code&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;逆波兰表达式主要有以下两个优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;去掉括号后表达式无歧义，上式即便写成 &lt;code&gt;1 2 + 3 4 + *&lt;/code&gt; 也可以依据次序计算出正确结果。&lt;/li&gt;
&lt;li&gt;适合用栈操作运算：遇到数字则入栈；遇到算符则取出栈顶两个数字进行计算，并将结果压入栈中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里其实与二叉树的后序遍历（左右中）是差不多的。&lt;/p&gt;
&lt;p&gt;在这里我们可以直接使用栈来解决，思路就是将数字入栈，然后遇到操作符，弹出两个数字进行运算，再将结果入栈。&lt;/p&gt;
&lt;p&gt;图示如下&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC%5D(https://programmercarl.com/0150.%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://camo.githubusercontent.com/9f7f3d3cc8df9823f36cb8566502a3c263476e49ca6b87bea9a3503d2c928eaa/68747470733a2f2f636f64652d7468696e6b696e672e63646e2e626365626f732e636f6d2f676966732f3135302e2545392538302538362545362542332541322545352538352542302545382541312541382545382542452542452545352542432538462545362542312538322545352538302542432e676966&quot; alt=&quot;逆波兰表达式&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int f (int a, int b, string op) {
    if (op == &quot;+&quot;) return a + b;
    if (op == &quot;-&quot;) return a - b;
    if (op == &quot;*&quot;) return a * b;
    if (op == &quot;/&quot;) return a / b;
    return 0;
}
int evalRPN(vector&amp;lt;string&amp;gt;&amp;amp; tokens) {
    stack&amp;lt;int&amp;gt; stk;
    for (int i = 0; i &amp;lt;= tokens.size() - 1; i++){
        // 当遇到操作符的时候
        if (tokens[i] == &quot;+&quot; || tokens[i] == &quot;-&quot; || tokens[i] == &quot;*&quot; || tokens[i] == &quot;/&quot;) {
            // 注意栈的弹出顺序，先弹出b再弹出a
            int b = stk.top();
            stk.pop();
            int a = stk.top();
            stk.pop();
            stk.push(f(a, b, tokens[i]));
        }
        else {
            stk.push(stoi(tokens[i])); // 这里要注意将字符串转为整数
        }
    }
    int ans = stk.top();
    stk.pop();
    return ans;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;唯一要注意的就是弹出元素的顺序，先弹出的是第二个操作数，之后才是第一个。&lt;/p&gt;
&lt;h2&gt;滑动窗口最大值&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sliding-window-maximum/&quot;&gt;239. 滑动窗口最大值&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;暴力求解&lt;/h3&gt;
&lt;p&gt;最直观的解法是使用 &lt;code&gt; max_element()&lt;/code&gt; 直接返回每次滑动窗口的最大值，然而这种方法会超时，时间复杂度为 $O(nk)$，&lt;code&gt;k&lt;/code&gt; 是窗口大小，遍历数组 &lt;code&gt;n - k + 1&lt;/code&gt; 次，&lt;code&gt;max_element()&lt;/code&gt; 遍历k次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; maxSlidingWindow(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) { // 暴力求解超时
    vector&amp;lt;int&amp;gt; ans;
    for (int i = 0; i + k &amp;lt;= nums.size(); i++) {
        ans.push_back(*max_element(nums.begin() + i, nums.begin() + i + k));
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单调队列&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC%5D(https://programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html#%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97)&quot;&gt;^2&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这里我们可以使用单调队列来解决，放进去窗口里的元素，然后随着窗口的移动，队列也一进一出，每次移动之后，队列告诉我们里面的最大值是什么。&lt;/p&gt;
&lt;p&gt;我们需要的队列有以下操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyQueue {
public:
    void pop(int value) {
    }
    void push(int value) {
    }
    int front() {
        return que.front();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在每次移动滑动窗口的时候，调用 &lt;code&gt;pop()&lt;/code&gt; 和 &lt;code&gt;push()&lt;/code&gt; 方法，然后 &lt;code&gt;front()&lt;/code&gt; 方法返回队列的最大值。&lt;/p&gt;
&lt;p&gt;这里有个思路是：&lt;strong&gt;没有必要维护窗口里的所有元素，只需要维护有可能成为窗口里最大值的元素就可以了，同时保证队列里的元素数值是由大到小的&lt;/strong&gt;。那么这个维护元素单调递减的队列就叫做单调队列，即单调递减或单调递增的队列。&lt;/p&gt;
&lt;p&gt;以下图示展示了单调队列的过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.gif&quot; alt=&quot;单调队列1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设计单调队列的时候，&lt;code&gt;pop&lt;/code&gt;，和 &lt;code&gt;push&lt;/code&gt; 操作要保持如下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pop(value)&lt;/code&gt;：如果窗口移除的元素 &lt;code&gt;value&lt;/code&gt; 等于单调队列的出口元素，那么队列弹出元素，否则不用任何操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push(value)&lt;/code&gt;：如果 &lt;code&gt;push&lt;/code&gt; 的元素 &lt;code&gt;value&lt;/code&gt; 大于入口元素的数值，那么就将队列入口的元素弹出，直到 &lt;code&gt;push&lt;/code&gt; 元素的数值小于等于队列入口元素的数值为止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在保持以上规则的前提下，每次滑动窗口移动时，我们只需要调用 &lt;code&gt;front()&lt;/code&gt; 方法就可以得到窗口的最大值。&lt;/p&gt;
&lt;p&gt;下面是一个更清晰展示单调队列的过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC-2.gif&quot; alt=&quot;单调队列2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么使用哪种队列呢，答案是 &lt;code&gt;deque&lt;/code&gt;，因为 &lt;code&gt;deque&lt;/code&gt; 可以在队列的两端进行插入和删除操作。&lt;/p&gt;
&lt;p&gt;那么根据以上规则，我们可以实现 &lt;code&gt;MyQueue&lt;/code&gt; 类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyQueue {
    public:
    deque&amp;lt;int&amp;gt; que;
    // 每次pop()之前，首先要判断队列是否为空，然后比较当前要弹出的数值时候等于队列的第一个元素，如果是则弹出
    void pop(int value) {
        if (!que.empty() &amp;amp;&amp;amp; value == que.front()) {
            que.pop_front();
        }
    }
    // 如果push()的数值大于入口元素的数值，那么就将队列入口的元素弹出，直到push()元素的数值小于等于队列入口元素的数值为止
    // 这样可保持队列里的元素是单调的
    void push(int value) {
        while (!que.empty() &amp;amp;&amp;amp; value &amp;gt; que.back()) {
            que.pop_back();
        }
        que.push_back(value);
    }

    int front() {
        return que.front();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是遍历数组，实现求解滑动窗口的最大值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; maxSlidingWindow(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) { 
    MyQueue que;
    vector&amp;lt;int&amp;gt; ans;
    for (int i = 0; i &amp;lt; k; i++) { // 先把前k个元素push进队列
        que.push(nums[i]);
    }
    ans.push_back(que.front()); // 记录前k个元素的最大值 
    for (int i = k; i &amp;lt; nums.size(); i++) { 
        que.pop(nums[i - k]); // 滑动窗口移除最前面元素
        que.push(nums[i]); // 滑动窗口前加入最后面的元素
        ans.push_back(que.front()); // 记录对应的最大值
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前k个高频元素&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/top-k-frequent-elements/&quot;&gt;347. 前K个高频元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们可以先用一个哈希表来统计每个元素出现的频率，然后再使用优先级队列来维护前 &lt;code&gt;k&lt;/code&gt; 个高频元素。&lt;/p&gt;
&lt;p&gt;优先级队列（priority_queue）底层通常基于堆实现，能在 &lt;code&gt;O(log n)&lt;/code&gt; 的时间复杂度内插入或取出优先级最高的元素。默认情况下是大顶堆，即每次取出的都是值最大的元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; topKFrequent(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        unordered_map&amp;lt;int, int&amp;gt; map;
        for (int i = 0; i &amp;lt; nums.size(); i++) {
            map[nums[i]]++;
        }
        // 优先队列，大顶堆
        priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; pq;
        for (auto it = map.begin(); it != map.end(); it++) { // 遍历map，将map中的元素push进优先队列
            pq.push(make_pair(it-&amp;gt;second, it-&amp;gt;first)); // 优先队列中存储的是pair&amp;lt;int, int&amp;gt;，第一个元素是频率，第二个元素是元素值
        }
        vector&amp;lt;int&amp;gt; ans;
        for (int i = 0; i &amp;lt; k; i++) { // 取出前k个元素
            ans.push_back(pq.top().second);
            pq.pop();
        }
        return ans;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是，优先队列默认是大顶堆，所以我们要将频率作为第一个元素，元素值作为第二个元素，这样就可以取出前 &lt;code&gt;k&lt;/code&gt; 个高频元素。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;今天的这个&lt;a href=&quot;#%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC&quot;&gt;单调队列&lt;/a&gt;还是有点难以理解，不过难的并不是 &lt;code&gt;myQueue&lt;/code&gt; 类的实现，在下面的遍历逻辑里，饶了半天有点没绕明白。&lt;/p&gt;
&lt;p&gt;最后要知道什么时候用优先级队列，什么时候用单调队列&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先级队列：求最大值或最小值&lt;/li&gt;
&lt;li&gt;单调队列：求最大值或最小值，同时还要保持队列里的元素是单调的&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day10-栈与队列 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/stack-queue/day10_stack-part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/stack-queue/day10_stack-part1/</guid><description>栈，队列</description><pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;栈，队列&lt;/h2&gt;
&lt;p&gt;已经知道的是栈是一种后进先出（LIFO）的数据结构，而队列是一种先进先出（FIFO）的数据结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%A0%88%E4%B8%8E%E9%98%9F%E5%88%97.png&quot; alt=&quot;栈与队列&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过在C++中，栈和队列的实现都是一种&lt;strong&gt;容器适配器(Container Adapte)&lt;/strong&gt;，而非容器。容器适配器是对已有容器（比如 &lt;code&gt;deque&lt;/code&gt; 或 &lt;code&gt;vector&lt;/code&gt;）的一种封装，用来提供不同的接口和使用方式。&lt;/p&gt;
&lt;p&gt;:::tip[容器和容器适配器]
在 STL 中，容器分为两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标准容器（Standard Containers）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如：&lt;code&gt;vector&lt;/code&gt;、&lt;code&gt;deque&lt;/code&gt;、&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;map&lt;/code&gt;、&lt;code&gt;unordered_map&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;这些容器拥有完整的接口，比如随机访问、插入、删除等。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;容器适配器（Container Adapters）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如：stack、queue、priority_queue&lt;/li&gt;
&lt;li&gt;它们是对标准容器的一种“限制使用接口的包装器”，让容器只能以特定的方式使用。&lt;/li&gt;
&lt;li&gt;例如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stack&lt;/code&gt; 能后进先出（LIFO）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queue&lt;/code&gt; 先进先出（FIFO）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority_queue&lt;/code&gt; 最大堆（默认）
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器适配器内部使用一个标准容器作为基础，比如 &lt;code&gt;deque&lt;/code&gt; 或 &lt;code&gt;vector&lt;/code&gt;，但不暴露全部接口，而是提供受限制的操作方式。在C++中，栈的默认底层实现是 &lt;code&gt;deque&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stack&amp;gt;

std::stack&amp;lt;int&amp;gt; stk1;                // 默认底层容器是 deque
std::stack&amp;lt;int, std::vector&amp;lt;int&amp;gt;&amp;gt; stk2; // 指定使用 vector 作为底层容器

// 无法对 stack 做 begin() 或 end()，因为这些操作在适配器中被屏蔽了
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用栈实现队列&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/implement-queue-using-stacks/&quot;&gt;232.用栈实现队列&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果只使用一个栈，是无法做到模拟队列的先进先出的，但如果使用两个栈，一个用来入栈，一个用来出栈，就可以实现模拟队列的先进先出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyQueue {
public:
    stack&amp;lt;int&amp;gt; stk_in;
    stack&amp;lt;int&amp;gt; stk_out;

    MyQueue() {
 
    }
    
    void push(int x) {
        stk_in.push(x); // 直接push进stk_in中
    }
    
    int pop() {
        if (stk_out.empty()) { 
            // 首先需要判断stk_out中是否有元素，如果没有则需要把stk_in中的元素全部放入stk_out中
            // 否则直接弹出stk_out栈顶元素
            while (!stk_in.empty()) { // stk_out入栈是一个持续的过程
                stk_out.push(stk_in.top());
                stk_in.pop();
            }
        }
        int result = stk_out.top();
        stk_out.pop();
        return result;
    }
    
    int peek() {
        int result = this-&amp;gt;pop(); // 复用pop()方法，弹出栈顶元素
        stk_out.push(result); // 使用pop()方法将stk_out栈顶弹出，还要再push回去
        return result;
    }
    
    bool empty() {
        return stk_in.empty() &amp;amp;&amp;amp; stk_out.empty();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[关于 &lt;code&gt;peek()&lt;/code&gt; 方法]
根据之前写python的经验，python中的 &lt;code&gt;pop()&lt;/code&gt; 方法不仅会弹出栈顶元素，还会返回这个元素，而C++中的 &lt;code&gt;pop()&lt;/code&gt; 方法只会弹出栈顶元素，不会返回这个元素。所以在 &lt;code&gt;peek()&lt;/code&gt; 方法中，需要调用 &lt;code&gt;pop()&lt;/code&gt; 方法，再将弹出的元素 &lt;code&gt;push()&lt;/code&gt; 回去。
:::&lt;/p&gt;
&lt;h2&gt;用队列实现栈&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/implement-stack-using-queues/&quot;&gt;225. 用队列实现栈&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;使用两个队列来实现栈&lt;/h3&gt;
&lt;p&gt;这里我们可以沿用上面的思路，用两个队列来模拟栈的实现（其实也是题中的要求使用两个队列）。&lt;/p&gt;
&lt;p&gt;这里的主要思路是，用两个队列 &lt;code&gt;q1&lt;/code&gt; 和 &lt;code&gt;q2&lt;/code&gt;，&lt;code&gt;q1&lt;/code&gt; 用来存储栈中的元素，&lt;code&gt;q2&lt;/code&gt; 用来辅助操作。
并且很重要的一点是，要保证 &lt;code&gt;q1.fornt()&lt;/code&gt; 一定是栈顶元素。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyStack { // 使用两个队列来实现栈
public:
    queue&amp;lt;int&amp;gt; q1; // q1 主栈
    queue&amp;lt;int&amp;gt; q2; // q2 用来临时存放q1的元素

    MyStack() {

    }
    
    void push(int x) { // 始终保证q1.front()是栈顶元素
        q2.push(x);  // 将新元素放入q2中
        while (!q1.empty()) { // 如果q1不为空，将q1中的元素放入q2中，这样保证q2.fornt()是栈顶元素
            q2.push(q1.front());
            q1.pop();
        }
        // swap q1 和 q2，使得 q1 始终是主栈
        swap(q1, q2);
    }
    
    int pop() {
        int result = q1.front(); // 因为q1.front()是栈顶元素，所以直接pop()，下一个元素自动成为栈顶元素
        q1.pop();
        return result;
    }
    
    int top() {
        return q1.front(); // 同pop()
    }
    
    bool empty() { // 只需返回q1是否为空即可
        return q1.empty();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们只使用其中一个队列来维护栈顶元素，始终保持 &lt;code&gt;q1&lt;/code&gt; 的前端是栈顶元素。&lt;/p&gt;
&lt;h3&gt;仅使用一个队列来实现栈&lt;/h3&gt;
&lt;p&gt;这里的思路其实和上面是一样的，我们只需要在 &lt;code&gt;push()&lt;/code&gt; 的时候，把新元素插入队尾，然后把前面的所有元素都重新入队，这样新元素就变成队首了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyStack { // 仅使用一个队列来实现栈
public:
    queue&amp;lt;int&amp;gt; q;

    MyStack() {}

    void push(int x) {
        q.push(x);
        int n = q.size(); // 这里一定要提前保存队列的大小，因为在循环中q的size是在不断改变的
        // 把前 n-1 个元素重新入队
        for (int i = 0; i &amp;lt; n - 1; ++i) {
            q.push(q.front());
            q.pop();
        }
    }
    // 剩余操作不变
    int pop() {
        int result = q.front();
        q.pop();
        return result;
    }

    int top() {
        return q.front();
    }

    bool empty() {
        return q.empty();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与随想录中的实现不同的是，这里只需要在 &lt;code&gt;push()&lt;/code&gt; 方法里实现入栈的逻辑就行了，维护队列的第一个元素始终是栈顶元素即可。&lt;/p&gt;
&lt;h2&gt;有效的括号&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/valid-parentheses/&quot;&gt;20. 有效的括号&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;括号匹配是一个使用栈来解决的经典问题，&lt;/p&gt;
&lt;p&gt;我们可以模拟出当括号不匹配时的几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当字符串遍历完了，但是栈不为空，说明还存在有未匹配的括号，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遍历字符串的过程中，发现栈里没有要匹配的字符，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;遍历字符串的过程中，栈已经空了，但是还有右括号，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种方法使用的是直接入栈左括号的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isPair(char a, char b) { // 定义一个判断括号是否匹配的函数
        return (a == &apos;(&apos; &amp;amp;&amp;amp; b == &apos;)&apos; 
                || a == &apos;[&apos; &amp;amp;&amp;amp; b == &apos;]&apos; 
                || a == &apos;{&apos; &amp;amp;&amp;amp; b == &apos;}&apos;);
    }

    bool isValid(string s) {
        stack&amp;lt;char&amp;gt; stk;
        int n = s.size() - 1;
        for (int i = 0; i &amp;lt;= n; i++) {
            if (s[i] == &apos;(&apos; || s[i] == &apos;[&apos; || s[i] == &apos;{&apos;) {
                stk.push(s[i]);
                cout &amp;lt;&amp;lt; stk.top();
            }
            else {
                if (stk.empty() || !isPair(stk.top(), s[i])) { // 如果是空栈或者不匹配，返回false
                    return false;
                }
                else { // 否则匹配，弹出栈顶元素
                    stk.pop();
                }
            }
        }
        return stk.empty();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种方法使用的是直接入栈右括号的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isValid(string s) { // 入栈右括号的方式
    stack&amp;lt;char&amp;gt; stk;
    int n = s.size() - 1;
    for (int i = 0; i &amp;lt;= n; i++ ) {
        if (s[i] == &apos;(&apos;) stk.push(&apos;)&apos;);
        else if (s[i] == &apos;[&apos;) stk.push(&apos;]&apos;);
        else if (s[i] == &apos;{&apos;) stk.push(&apos;}&apos;);
        // 1. 匹配到右括号，但是stk已经是空的，说明没有左括号与其匹配
        // 2. 匹配到右括号，但是s[i] != stk.top() 说明没有右括号与其匹配
        else if (stk.empty() || s[i] != stk.top()) return false;
        else stk.pop();
    }
    return stk.empty();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以在匹配左括号的时候，右括号先入栈，就只需要比较当前元素和栈顶相不相等就可以了，比左括号先入栈代码实现要简单的多了。&lt;/p&gt;
&lt;h2&gt;删除字符串中的所有相邻重复项&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/remove-all-adjacent-duplicates-in-string/&quot;&gt;1047. 删除字符串中的所有相邻重复项&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题其实还算比较简单，不过因为随想录的验证码出了些问题，所以就自己写了写简单的思路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string removeDuplicates(string s) {
    stack&amp;lt;char&amp;gt; stk;
    int n = s.size() - 1;
    for (int i = 0; i &amp;lt;= n; i++) {
        if (!stk.empty() &amp;amp;&amp;amp; stk.top() == s[i]) {
            stk.pop();
        }
        else stk.push(s[i]);
    }
    string ans;
    // 将stk中的元素存入ans中，这里还是倒序
    while (!stk.empty()) {
        ans += stk.top();
        stk.pop();
    }
    // 反转ans
    reverse(ans.begin(), ans.end());
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难理解，只要栈不为空的时候，且栈顶元素和当前元素相等，就弹出栈顶元素，否则入栈。&lt;/p&gt;
&lt;p&gt;不过这里也只是删除相邻的重复项并不是所有重复项，切勿想得太复杂。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;不太难，&lt;a href=&quot;#%E7%94%A8%E9%98%9F%E5%88%97%E5%AE%9E%E7%8E%B0%E6%A0%88&quot;&gt;用队列实现栈&lt;/a&gt;中的这两个写法我觉得可以提一个PR，因为只要处理 &lt;code&gt;push()&lt;/code&gt; 的逻辑就行了，看起来更清晰一点。&lt;/p&gt;
</content:encoded></item><item><title>Day09-字符串 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/string/day09_string_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/string/day09_string_part2/</guid><description>字符串，反转字符串中的单词，旋转字符串</description><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;反转字符串中的单词&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-words-in-a-string/&quot;&gt;151.反转字符串中的单词&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;用栈实现&lt;/h3&gt;
&lt;p&gt;其实一开始想到这题的时候，用栈道思路会比较清楚，用LIFO就很好理解，不过空间复杂度为 &lt;code&gt;O(n)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string reverseWords(string s) {
    stack&amp;lt;string&amp;gt; st;
    string word, ans;

    //遍历字符串，按单词存入栈中
    for (int i = 0; i &amp;lt; s.size(); i++) {
        if (s[i] != &apos; &apos;) { // 如果遇到的是字符，添加到word里
            word += s[i];
        }
        else if (!word.empty()) { //遇到空格且word为不为空，将word压入栈中
            st.push(word);
            word.clear();
        }
    }
    // 如果遍历完了，还有单词，即word不为空
    if (!word.empty()) st.push(word);
    // 从栈中弹出单词，形成结果
    while (!st.empty()) {
        ans += st.top();
        st.pop();
        if (!st.empty()) ans += &quot; &quot;;  // 仅在不是最后一个单词时添加空格
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;code&gt;O(n)&lt;/code&gt;，&lt;code&gt;n&lt;/code&gt; 为字符串的长度
空间复杂度：&lt;code&gt;O(n)&lt;/code&gt;，最坏情况下，栈中存储了 &lt;code&gt;n&lt;/code&gt; 个字符&lt;/p&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么要用 else if 而不是 if ：因为这里不是 default 的情况，当遇到空格时，还需要判断当前 word 是否为空&lt;/li&gt;
&lt;li&gt;在遍历结束之后，还需要再判断一次 word 是否为空，如果不为空，将 word 压入栈中：这是因为在遍历结束之后，我们可能进行了将字符添加到word中，但是最后没有遇到空格，因此我们还剩最后一个word没有入栈，所以需要单独处理一次。&lt;/li&gt;
&lt;li&gt;在出栈道过程中，如果栈为空，说明已经弹出了最后一个单词，不需要再添加空格。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;在原字符串上来实现&lt;/h3&gt;
&lt;p&gt;这题的关键问题其实在于如何去除多余的空格，要判断的情况很多，但又没有像 python 那样方便的库函数。
因此我们需要自己实现一个去除多余空格的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void removeExtraSpaces(string &amp;amp;s) {
    int n = s.size();
    int slow = 0, fast = 0;
    // 1. 去除字符串前面的空格
    while (fast &amp;lt; n &amp;amp;&amp;amp; s[fast] == &apos; &apos;) fast++;
    // 1.5 去除中间的空格
    while (fast &amp;lt; n) {
        // 2. 复制非空格字符，参考27.移除元素
        if (s[fast] != &apos; &apos;) {
            s[slow++] = s[fast];
        }
        // 3.确保中间只有一个空格
        else if (slow &amp;gt; 0 &amp;amp;&amp;amp; s[slow - 1] != &apos; &apos;) { // 当slow的前一位不是空格，也就是字符的时候，才添加空格
            s[slow++] = &apos; &apos;;
        }
        fast++;
    }
    // 4. 去除字符串末尾的空格
    if (slow &amp;gt; 0 &amp;amp;&amp;amp; s[slow - 1] == &apos; &apos;) slow--;
    // 5. 重新设置字符串大小
    s.resize(slow);
    cout&amp;lt;&amp;lt; s&amp;lt;&amp;lt;endl; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在不使用库函数和辅助空间的前提下，我们可以使用两次反转来实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次将整个字符串反转&lt;/li&gt;
&lt;li&gt;第二次将每个单词反转&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此我们整体的处理逻辑可以分以下几步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;去除多余空格&lt;/li&gt;
&lt;li&gt;将整个字符串反转&lt;/li&gt;
&lt;li&gt;将每个单词反转&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;反转过程的如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string reverseWords(string s) {
    removeExtraSpaces(s); // 去除多余空格
    reverse(s.begin(), s.end()); // 反转整个字符串

    int start = 0; // 定义一个start指向单词的起始位置
    for (int i = 0; i &amp;lt;= s.size(); i++){
        if ( i == s.size() || s[i] == &apos; &apos;) { // 遇到字符串结束或者当前字符为空格
            reverse(s.begin() + start, s.begin() + i); // 反转区间[s.begin() + start, s.begin() + i]
            start = i + 1; // start 移动到下一位
        }
    }
    return s;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;：：：tip
复杂的其实是空格的处理逻辑。
：：：&lt;/p&gt;
&lt;h2&gt;右旋字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1065&quot;&gt;右旋字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个其实用pyhton来做就非常简单了，伟大&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return s[-k:] + s[:-k]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++没有类似python的切片操作，不过根据&lt;a href=&quot;#%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%8D%95%E8%AF%8D&quot;&gt;反转字符串中单词&lt;/a&gt;的思路，我们也可以使用两次反转字符串来实现。&lt;/p&gt;
&lt;p&gt;图示如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%8F%B3%E6%97%8B%E5%AD%97%E7%AC%A6%E4%B8%B2.png&quot; alt=&quot;右旋字符串&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;iostream&amp;gt;
# include &amp;lt;string&amp;gt;
using namespace std;

void reverse(string&amp;amp; s, int begin, int end) {
    while (begin &amp;lt; end) {
        swap(s[begin], s[end]);
        begin++;
        end--;
    }
}

int main() {
    int k;
    cin &amp;gt;&amp;gt; k;

    string s;
    cin &amp;gt;&amp;gt; s;

    if (s.size() &amp;lt;= 1) {
        cout &amp;lt;&amp;lt; s;
        return 0;
    }
    reverse(s, 0, s.size() - 1);
    reverse(s, 0, k - 1);
    reverse(s, k, s.size() - 1);

    cout &amp;lt;&amp;lt; s &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;双指针法在数组，字符串，链表中很常用，需要熟练掌握。&lt;/p&gt;
&lt;p&gt;在反转数组中，还是要提高自己的逻辑理解能力，反转很容易，但是各种处理的逻辑却很复杂。&lt;/p&gt;
&lt;p&gt;今天本来应该还有个KMP算法，但是内容有些多，KMP又比较复杂，今天这里先鸽一会。&lt;/p&gt;
</content:encoded></item><item><title>Day08-字符串 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/string/day08_string_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/string/day08_string_part1/</guid><description>字符串，反转字符串，反转字符串II，替换数字</description><pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;反转字符串&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-string/&quot;&gt;344.反转字符串&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题其实用双指针非常容易解决，但是用递归的话说实话还没有什么头绪。&lt;/p&gt;
&lt;p&gt;双指针的解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void reverseString(vector&amp;lt;char&amp;gt;&amp;amp; s) {
    int left = 0;
    int right = s.size() - 1;
    while (left &amp;lt; right) { // 当left与right相等时，说明已经遍历完了
        swap(s[left], s[right]);
        left++;
        right--;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于递归写法，我们可以使用一个辅助函数 &lt;code&gt;reverse()&lt;/code&gt;,这个函数接受两个参数，一个是字符串，一个是左右指针。&lt;/p&gt;
&lt;p&gt;递归的终止条件是 &lt;code&gt;left &amp;gt;= right&lt;/code&gt;，说明已经遍历完了，直接返回即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 递归写法
void reverse(vector&amp;lt;char&amp;gt;&amp;amp; s, int left, int right) {
    if (left &amp;gt;= right) return; // 递归终止条件
    swap(s[left], s[right]); // 交换
    reverse(s, left + 1, right - 1); // 递归s[left + 1], s[right - 1]
}
void reverseString(vector&amp;lt;char&amp;gt;&amp;amp; s) {
    reverse(s, 0, s.size() - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;反转字符串II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-string-ii/&quot;&gt;541.反转字符串II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里的重点其实是理解题意，用不用库函数都不是重点。&lt;/p&gt;
&lt;p&gt;我们可以画个图来帮助理解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2II.png&quot; alt=&quot;反转字符串II&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以可以写出以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string reverseStr(string s, int k) {
    for (int i = 0; i &amp;lt; s.size(); i += 2 * k) { // i每次增加2k
        if (i + k &amp;lt;= s.size()) { // 如果剩余字符大于等于k个，但是小于2k个，则反转前k个字符
            reverse(s.begin() + i, s.begin() + i + k);
        } else { // 如果剩余字符小于k个，则反转所有字符
            reverse(s.begin() + i, s.end());
        }
    }
    return s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[题外话]
其实这里的 &lt;code&gt;if (i + k &amp;lt;= s.size())&lt;/code&gt; 也可以不写等于，直接让 &lt;code&gt;else&lt;/code&gt; 去判断，虽然意思都是一样的：当剩余字符正好等于 &lt;code&gt;k&lt;/code&gt; 个，反转剩余所有字符的逻辑其实是一样的。只是这种写法没有按照题意而已，依照题意还是写 &lt;code&gt;&amp;lt;=&lt;/code&gt; 比较好。
:::&lt;/p&gt;
&lt;h2&gt;替换数字&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1064&quot;&gt;替换数字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题最直接的思路就是遍历字符串，如果是数字就替换，不是就跳过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;iostream&amp;gt;
# include &amp;lt;vector&amp;gt;
using namespace std;

int main() {
    string s, ans;
    cin &amp;gt;&amp;gt; s;
    for (char c : s) {
        if (c &amp;gt;= &apos;a&apos; &amp;amp;&amp;amp; c &amp;lt;= &apos;z&apos;)
        ans += c;
        else {
            ans += &quot;number&quot;;
            continue;
        }
    }
    cout &amp;lt;&amp;lt; ans;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这里也可以不使用额外空间，直接在原字符串上进行操作，但是要先将原字符串扩容。&lt;/p&gt;
&lt;p&gt;例如 字符串 &lt;code&gt;a5b&lt;/code&gt; 的长度为3，那么将数字字符变成字符串 &lt;code&gt;number&lt;/code&gt; 之后的字符串为 &lt;code&gt;anumberb&lt;/code&gt; 长度为 8。&lt;/p&gt;
&lt;p&gt;然后从后向前替换数字字符，也就是双指针法，过程如下：&lt;code&gt;i&lt;/code&gt; 指向新长度的末尾，&lt;code&gt;j&lt;/code&gt; 指向旧长度的末尾[^1]。
[^1]: &lt;a href=&quot;https://programmercarl.com/kamacoder/0054.%E6%9B%BF%E6%8D%A2%E6%95%B0%E5%AD%97.html#%E6%80%9D%E8%B7%AF&quot;&gt;代码随想录-替换数字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://file.kamacoder.com/pics/20231030173058.png&quot; alt=&quot;替换数字&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从后向前填充，避免每次添加元素都要将添加元素之后的所有元素整体向后移动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# include &amp;lt;iostream&amp;gt;
# include &amp;lt;vector&amp;gt;
using namespace std;

int main() {
    string s;
    cin &amp;gt;&amp;gt; s;
    int count = 0;
    for (char c : s) {
        if (c &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; c &amp;lt;= &apos;9&apos;) count++;

    }
    int left = s.size() - 1;
    s.resize(s.size() + count * 5); // 扩容数组
    int right = s.size() - 1;
    
    while (left &amp;gt;= 0) { // 从后向前遍历
        if (s[left] &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; s[left] &amp;lt;= &apos;9&apos;) { // 遇到数字从向前填充&quot;number&quot;
            s[right--] = &apos;r&apos;;
            s[right--] = &apos;e&apos;;
            s[right--] = &apos;b&apos;;
            s[right--] = &apos;m&apos;;
            s[right--] = &apos;u&apos;;
            s[right--] = &apos;n&apos;;
        }
        else {
            s[right--] = s[left]; // 遇到字母直接填充
        }
        left--;
    }
    cout &amp;lt;&amp;lt; s;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;今天的任务总的来说比较简单，没有太大难度，不过这个填充数字的在原字符串上操作的思路确实还是要多思考一会，多看几遍图解其实就也还好。&lt;/p&gt;
&lt;p&gt;这两天估计要特别忙了，训练营说不好可能要鸽几天。&lt;/p&gt;
</content:encoded></item><item><title>Day07-哈希表 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/hash-table/day07_hashtable_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/hash-table/day07_hashtable_part2/</guid><description>哈希表，双指针，四数相加，赎金信，三数之和，四数之和</description><pubDate>Tue, 18 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;四数相加&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/4sum-ii/&quot;&gt;454.四数相加II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这题的思考方式其实和之前写过的&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/hash-table/day06_hashtable_part1/#2-%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D&quot;&gt;有效的字母异位词&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;四数相加和四数之和是有区别的，四数之和是在同一个数组中要求四个数的和等于目标值。&lt;/p&gt;
&lt;p&gt;其实一开是也想到了将数组两两分组，然后用哈希表来解决，但是想了想好像这个时间复杂度都已经O(n^2)了，总不至此吧。&lt;/p&gt;
&lt;p&gt;事实上，这个解法是正确的，后续也查阅过貌似无法再优化时间复杂度到O(n^2)以下。&lt;/p&gt;
&lt;p&gt;整体思路与之前的题目类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int fourSumCount(vector&amp;lt;int&amp;gt;&amp;amp; nums1, vector&amp;lt;int&amp;gt;&amp;amp; nums2, vector&amp;lt;int&amp;gt;&amp;amp; nums3, vector&amp;lt;int&amp;gt;&amp;amp; nums4) {
    // 哈希表，两两数组分别处理
    unordered_map&amp;lt;int, int&amp;gt; map;
    int count = 0;
    // 将a+b映射到map中
    for (int a : nums1) {
        for (int b : nums2) {
            map[a+b]++;
        }
    }
    // 在map中查找0 - (c + d)
    for (int c : nums3) {
        for (int d : nums4) {
            int target = - (c + d);
            if (map.find(target) != map.end()) {
                count += map[target]; // 这里注意count是要加上所有符合条件的元组
            }
        }
    }
    return count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的是，要求的是所有符合条件的元组，所以在进行哈希表查询的时候，&lt;code&gt;count&lt;/code&gt; 要加上所有符合条件的元组，也就是 &lt;code&gt;map[target]&lt;/code&gt; 的值。&lt;/p&gt;
&lt;h2&gt;赎金信&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/ransom-note/&quot;&gt;383.赎金信&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这道题的思路其实和前面的差不多，都是通过建立哈希表，然后从另一个字符串中查找是否存在。&lt;/p&gt;
&lt;p&gt;用map的实现（无论是将magazine还是ransomNote做成哈希表都可以实现）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 将magazine做成哈希表
bool canConstruct(string ransomNote, string magazine) {
    unordered_map&amp;lt;char, int&amp;gt; map;
    for (char c : magazine) {
        map[c]++;
    }
    for (char c : ransomNote) {
        if (map.find(c) != map.end()) { // 如果在哈希表中找到了ransomNote中的字符
            if (map[c] &amp;lt;= 0) return false; // 如果哈希表中的值小于等于0，说明ransomNote中不存在这个字符
            else map[c]--; // 否则哈希表中的值减1，也就是找到了
        }
        else return false; // 如果在哈希表中没有找到ransomNote中的字符，直接返回false
    }
    return true;
}

// 如果将ransomNote做成哈希表
bool canConstruct(string ransomNote, string magazine) {
    unordered_map&amp;lt;char, int&amp;gt; map;
    for (char c :  ransomNote) { // 将ransomNote做成哈希表
        map[c]++;
    }
    for (char c : magazine) { // 在magazine中查找
        if (map.find(c) != map.end()) { // 如果在哈希表中找到了magazine中的字符
            if (map[c] &amp;lt;= 0) continue; // 如果哈希表中的值小于等于0，则说明magazine中已经满足了ransomNote中的字符，直接跳过
            else map[c]--; // 否则哈希表中的值减1
        }
    }
    //检查map中键的值是否都为0
    for (auto&amp;amp; pair : map) {
        if (pair.second != 0)return false;
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在本题的情况下，使用map的空间消耗要比数组大一些的，因为map要维护红黑树或者哈希表，而且还要做哈希函数，是费时的，数据量大的话就能体现出来差别了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用数组来实现
bool canConstruct(string ransomNote, string magazine) {
    int count[26] = {0};

    for (char c : magazine) {
        count[c - &apos;a&apos;]++;
    }

    for (char c : ransomNote) {
        count[c - &apos;a&apos;]--;
        if (count[c - &apos;a&apos;] &amp;lt; 0) return false;
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三数之和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/3sum/&quot;&gt;15.三数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其实这道题用哈希表来解决是不太合适的，因为涉及到复杂的去重操作，而且时间复杂度也不会比排序+双指针更优。&lt;/p&gt;
&lt;p&gt;这里我们可以使用双指针的解法：&lt;/p&gt;
&lt;p&gt;我们首先将数组进行排序，同时定义 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt; 指针，然后遍历数组，对于每一个元素，我们都将其作为第一个元素，然后在剩下的元素中使用双指针来寻找另外两个元素。&lt;/p&gt;
&lt;p&gt;依然还是在数组中找到 &lt;code&gt;abc&lt;/code&gt; 使得 &lt;code&gt;a + b +c = 0&lt;/code&gt;，我们这里相当于 &lt;code&gt;a = nums[i]&lt;/code&gt;，&lt;code&gt;b = nums[left]&lt;/code&gt;，&lt;code&gt;c = nums[right]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;接下来如何移动 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt;， 如果 &lt;code&gt;nums[i] + nums[left] + nums[right] &amp;gt; 0&lt;/code&gt; 就说明 此时三数之和大了，因为数组是排序后了，所以 &lt;code&gt;right&lt;/code&gt; 下标就应该向左移动，这样才能让三数之和小一些。&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;nums[i] + nums[left] + nums[right] &amp;lt; 0&lt;/code&gt; 说明 此时 三数之和小了，&lt;code&gt;left&lt;/code&gt; 就向右移动，才能让三数之和大一些，直到 &lt;code&gt;left&lt;/code&gt; 与 &lt;code&gt;right&lt;/code&gt; 相遇为止。&lt;/p&gt;
&lt;p&gt;总体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; threeSum(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
    sort(nums.begin(), nums.end()); // 对数组排序
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
    if (nums[0] &amp;gt; 0 || nums[nums.size() - 1] &amp;lt; 0) return {};
    for (int i = 0; i &amp;lt; nums.size() - 1; i++) {
        if (i &amp;gt;0 &amp;amp;&amp;amp; nums[i] == nums[i - 1]) continue; // 对i进行去重操作，注意i是与上一次比较
        int left = i + 1;
        int right = nums.size() - 1;

        while (left &amp;lt; right) {
            if (nums[i] + nums[left] + nums[right] &amp;gt; 0) right--;
            else if (nums[i] + nums[left] + nums[right] &amp;lt; 0) left ++;
            else {
                ans.push_back({nums[i], nums[left], nums[right]}); // 保存结果集
                while (left &amp;lt; right &amp;amp;&amp;amp; nums[left] == nums[left + 1]) left++; // 对left去重
                while (left &amp;lt; right &amp;amp;&amp;amp; nums[right] == nums[right - 1]) right--; // 对right去重
                // 去重结束后，再移动一位
                left++; 
                right--;
            }
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：O(n^2)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; 遍历一次数组，时间复杂度为O(n)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt; 使用双指针，时间复杂度为O(n)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于去重的思考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 &lt;code&gt;a&lt;/code&gt; 的去重，要判断 &lt;code&gt;nums[i] == nums[i - 1]&lt;/code&gt;，因为 &lt;code&gt;i&lt;/code&gt; 是与上一次比较的。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;b&lt;/code&gt; 的去重，要判断 &lt;code&gt;nums[left] == nums[left + 1]&lt;/code&gt;，因为 &lt;code&gt;left&lt;/code&gt; 是与上一次比较的。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;c&lt;/code&gt; 的去重，要判断 &lt;code&gt;nums[right] == nums[right - 1]&lt;/code&gt;，因为 &lt;code&gt;right&lt;/code&gt; 是与上一次比较的。&lt;/li&gt;
&lt;li&gt;最后因为去重是持续进行的，所以要使用 &lt;code&gt;while&lt;/code&gt; 循环，直到不重复为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四数之和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/4sum/&quot;&gt;18.四数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;做了三数之和之后，其实四数之后就很好理解了，无疑是在外面再套一层循环。&lt;/p&gt;
&lt;p&gt;主要的问题还是在剪枝和去重的操作上。&lt;/p&gt;
&lt;p&gt;这里剪枝操作不同于&lt;a href=&quot;#%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C&quot;&gt;三数之和&lt;/a&gt;的地方在于，数据中可能存在负数，所以之前的条件不能照搬。
如果要进行剪枝，我们要使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; if (nums[a] &amp;gt; target &amp;amp;&amp;amp; nums[a] &amp;gt; 0) break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了三数之和的基础上，很容易写出以下代码，不过这里的与随想录的代码不同的是，我使用的是从两侧靠拢的思路：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C.png&quot; alt=&quot;四数之和思路&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; fourSum(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    sort(nums.begin(), nums.end());
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans = {};
    // 这里不能像三数之和那样做剪枝操作，因为可能存在负数
    // if (nums[0] &amp;gt; target || nums[nums.size() - 1] &amp;lt; target) return {};
    for (int a = 0; a &amp;lt; nums.size(); a++) {
        if (a &amp;gt; 0 &amp;amp;&amp;amp; nums[a] == nums[a - 1]) continue; // 对a进行去重
        for (int d = nums.size() - 1; d &amp;gt; a + 2; d--) {
            if (d &amp;lt; nums.size() - 1 &amp;amp;&amp;amp; nums[d] == nums[d + 1]) continue; // 对d进行去重
            int b = a + 1;
            int c = d - 1;
            while (b &amp;lt; c) {
                // sum = nums[a] + nums[b] + nums[c] + nums[d];
                // 这里应该使sum的类型为long long，int类型可能会溢出
                long long sum = (long long)nums[a] + nums[b] + nums[c] + nums[d];
                if (sum &amp;gt; target) c--;
                else if (sum &amp;lt; target) b++;
                else {
                    ans.push_back({nums[a], nums[b], nums[c], nums[d]});
                    while (b &amp;lt; c &amp;amp;&amp;amp; nums[b] == nums[b + 1]) b++; // 对b进行去重
                    while (b &amp;lt; c &amp;amp;&amp;amp; nums[c] == nums[c - 1]) c--; // 对c进行去重
                    // 去重结束后，再移动一位
                    b++;
                    c--;
                }   
            }
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要注意的是题中给出的条件是 &lt;code&gt;-10^9 &amp;lt;= nums[i] &amp;lt;= 10^9&lt;/code&gt;，&lt;code&gt;-109 &amp;lt;= target &amp;lt;= 109&lt;/code&gt;，在计算的时候如果sum的类型是 &lt;code&gt;int&lt;/code&gt;，那么可能会导致结果溢出，保险起见将 &lt;code&gt;sum&lt;/code&gt; 设置为 &lt;code&gt;long long&lt;/code&gt; 类型（在提交代码的时候不通过也是因为这个原因）。&lt;/p&gt;
&lt;p&gt;时间复杂度为O(n^3)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; 遍历一次数组，时间复杂度为O(n)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt; 遍历一次数组，时间复杂度为O(n)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b&lt;/code&gt; 和 &lt;code&gt;c&lt;/code&gt; 使用双指针，时间复杂度为O(n)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这里的两道题其实都没有使用哈希表，而是使用了排序+双指针的方法。&lt;/p&gt;
&lt;p&gt;时间复杂度高的算法，就不需要再感到意外了，不能因为是个O(n^2)的算法就默认不是最优解。&lt;/p&gt;
&lt;p&gt;会了三数之和，四数之和感觉通透了，没看视频也能自己写出来了&lt;s&gt;除了sum的类型溢出问题整了好久。&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>Day06-哈希表 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/hash-table/day06_hashtable_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/hash-table/day06_hashtable_part1/</guid><description>哈希表，有效的字母异位词，两个数组的交集，快乐数，两数之和</description><pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;哈希表&lt;/h2&gt;
&lt;h3&gt;哈希表的概念&lt;/h3&gt;
&lt;p&gt;哈希表（Hash Table），也叫做散列表，是一种键值对（key-value）存储的数据结构，能够在平均O(1)的时间复杂度下进行查找，插入和删除的操作，广泛运用于数据库，缓存系统，计数统计等场景。&lt;/p&gt;
&lt;p&gt;哈希表的核心思想是利用 哈希函数（Hash Function） 将键（Key）映射到数组的某个位置（索引），从而 快速存取数据。&lt;/p&gt;
&lt;h3&gt;哈希表的基本组成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;键（Key）：用于唯一标识数据的值，如 &quot;apple&quot;、123、user_id。&lt;/li&gt;
&lt;li&gt;值（Value）：键对应的存储数据，如 &quot;fruit&quot;、&quot;张三&quot;、56.7。&lt;/li&gt;
&lt;li&gt;哈希函数（Hash Function）：将键转换为数组索引的函数。&lt;/li&gt;
&lt;li&gt;哈希桶（Bucket）：用于存储键值对的数据单元。&lt;/li&gt;
&lt;li&gt;冲突解决机制：当不同的键被映射到相同的位置时，如何处理（如链地址法、开放寻址法等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;哈希函数&lt;/h3&gt;
&lt;p&gt;哈希函数[^1]用于将任意的键值映射到一个固定的索引值，一个好的哈希函数应该具有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;均匀性（Uniformity）：不同的键应该尽可能均匀地分布到哈希表中。&lt;/li&gt;
&lt;li&gt;确定性（Deterministic）：相同的输入必须产生相同的输出。&lt;/li&gt;
&lt;li&gt;高效性（Efficiency）：计算哈希值的时间开销要尽可能小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Hash_function.svg/1280px-Hash_function.svg.png&quot; alt=&quot;哈希函数&quot; /&gt;&lt;/p&gt;
&lt;p&gt;[^1]: &lt;a href=&quot;https://en.wikipedia.org/wiki/Hash_function&quot;&gt;Hash function - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;哈希碰撞&lt;/h3&gt;
&lt;p&gt;哈希碰撞（Hash Collision）是指两个不同的键映射到相同的索引，常见的解决哈希碰撞的方法有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开放寻址法：当发生冲突时，寻找下一个空闲位置存储数据，如果还是冲突，继续寻找下一个空闲位置。
当然，开放寻址法也有多种实现方式，如&lt;strong&gt;线性探测&lt;/strong&gt;、&lt;strong&gt;二次探测&lt;/strong&gt;、&lt;strong&gt;双重散列&lt;/strong&gt;等，图中是线性探测的示例。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
开放寻址法的优点是不需要额外的链表存储，缺点是当负载因子较大时，查找性能会下降。
:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%BC%80%E6%94%BE%E5%AF%BB%E5%9D%80%E6%B3%95.png&quot; alt=&quot;开放寻址法&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;链地址法：也叫拉链法，每个哈希桶存储一个链表（或其他数据结构），当多个键映射到相同索引时，放入链表中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
链地址法的有点是实现简单，易于扩展，缺点是当链表过长时，查找性能会下降（退化为 &lt;code&gt;O(n)&lt;/code&gt;）。
:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%93%BE%E5%9C%B0%E5%9D%80%E6%B3%95.png&quot; alt=&quot;链地址法&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;常见的哈希结构&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E5%B8%B8%E8%A7%81%E7%9A%84%E4%B8%89%E7%A7%8D%E5%93%88%E5%B8%8C%E7%BB%93%E6%9E%84%5D(https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E4%B8%89%E7%A7%8D%E5%93%88%E5%B8%8C%E7%BB%93%E6%9E%84)&quot;&gt;^2&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;在哈希表中，常用的数据结构有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组&lt;/li&gt;
&lt;li&gt;set（集合）&lt;/li&gt;
&lt;li&gt;map（映射）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​在 C++ 标准模板库（STL）中，&lt;code&gt;set&lt;/code&gt; 和 &lt;code&gt;map&lt;/code&gt; 是两种常用的关联容器，用于存储具有特定规则的数据。​它们的底层实现通常采用 红黑树，这是一种自平衡的二叉搜索树，确保了元素的有序性和操作的高效性。&lt;/p&gt;
&lt;p&gt;在C++中，&lt;code&gt;set&lt;/code&gt; 和 &lt;code&gt;map&lt;/code&gt; 分别提供以下三种数据结构，其底层实现以及优劣如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;集合&lt;/th&gt;
&lt;th&gt;底层实现&lt;/th&gt;
&lt;th&gt;是否有序&lt;/th&gt;
&lt;th&gt;数值是否可以重复&lt;/th&gt;
&lt;th&gt;能否更改数值&lt;/th&gt;
&lt;th&gt;查询效率&lt;/th&gt;
&lt;th&gt;增删效率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;红黑树&lt;/td&gt;
&lt;td&gt;有序&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::multiset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;红黑树&lt;/td&gt;
&lt;td&gt;有序&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::unordered_set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;哈希表&lt;/td&gt;
&lt;td&gt;无序&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;std::unordered_set&lt;/code&gt; 底层实现为哈希表，&lt;code&gt;std::set&lt;/code&gt; 和 &lt;code&gt;std::multiset&lt;/code&gt; 的底层实现是红黑树，红黑树是一种平衡二叉搜索树，所以 &lt;code&gt;key&lt;/code&gt; 值是有序的，但 &lt;code&gt;key&lt;/code&gt; 不可以修改，改动 &lt;code&gt;key&lt;/code&gt; 值会导致整棵树的错乱，所以只能删除和增加。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;映射&lt;/th&gt;
&lt;th&gt;底层实现&lt;/th&gt;
&lt;th&gt;是否有序&lt;/th&gt;
&lt;th&gt;数值是否可以重复&lt;/th&gt;
&lt;th&gt;能否更改数值&lt;/th&gt;
&lt;th&gt;查询效率&lt;/th&gt;
&lt;th&gt;增删效率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;红黑树&lt;/td&gt;
&lt;td&gt;key有序&lt;/td&gt;
&lt;td&gt;key不可重复&lt;/td&gt;
&lt;td&gt;key不可修改&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;td&gt;O(logn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::multimap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;红黑树&lt;/td&gt;
&lt;td&gt;key有序&lt;/td&gt;
&lt;td&gt;key可重复&lt;/td&gt;
&lt;td&gt;key不可修改&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;std::unordered_map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;哈希表&lt;/td&gt;
&lt;td&gt;key无序&lt;/td&gt;
&lt;td&gt;key不可重复&lt;/td&gt;
&lt;td&gt;key不可修改&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;std::unordered_map&lt;/code&gt; 底层实现为哈希表，&lt;code&gt;std::map&lt;/code&gt; 和 &lt;code&gt;std::multimap&lt;/code&gt; 的底层实现是红黑树。同理，std::map 和 &lt;code&gt;std::multimap&lt;/code&gt; 的 &lt;code&gt;key&lt;/code&gt; 也是有序的。&lt;/p&gt;
&lt;p&gt;当我们要使用集合来解决哈希问题的时候，优先使用 &lt;code&gt;unordered_set&lt;/code&gt;，因为它的查询和增删效率是最优的，如果需要集合是有序的，那么就用 &lt;code&gt;set&lt;/code&gt;，如果要求不仅有序还要有重复数据的话，那么就用 &lt;code&gt;multiset&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那么再来看一下 &lt;code&gt;map&lt;/code&gt; ，在 &lt;code&gt;map&lt;/code&gt; 是一个 &lt;code&gt;key&lt;/code&gt; &lt;code&gt;value&lt;/code&gt; 的数据结构，&lt;code&gt;map&lt;/code&gt; 中，对 &lt;code&gt;key&lt;/code&gt; 是有限制，对 &lt;code&gt;value&lt;/code&gt; 没有限制的，因为 &lt;code&gt;key&lt;/code&gt; 的存储方式使用红黑树实现的。&lt;/p&gt;
&lt;p&gt;其他语言例如：java里的 &lt;code&gt;HashMap&lt;/code&gt; ，&lt;code&gt;TreeMap&lt;/code&gt; 都是一样的原理。可以灵活贯通。&lt;/p&gt;
&lt;p&gt;虽然 &lt;code&gt;std::set&lt;/code&gt; 和 &lt;code&gt;std::multiset&lt;/code&gt; 的底层实现基于红黑树而非哈希表，它们通过红黑树来索引和存储数据。不过给我们的使用方式，还是哈希法的使用方式，即依靠键 &lt;code&gt;key&lt;/code&gt; 来访问值 &lt;code&gt;value&lt;/code&gt;。所以使用这些数据结构来解决映射问题的方法，我们依然称之为哈希法。&lt;code&gt;std::map&lt;/code&gt; 也是一样的道理。&lt;/p&gt;
&lt;h2&gt;有效的字母异位词&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/valid-anagram/&quot;&gt;242.有效的字母异位词&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;数组实现&lt;/h3&gt;
&lt;p&gt;这里要注意的是，题中给出的是小写字母，所以这个数据是比较小的，我们可以使用数组来实现哈希表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isAnagram(string s, string t) {
    // 用数组来实现哈希表
    int hash[26] = {0};
    // 遍历s
    for (char c : s) { 
        hash[c - &apos;a&apos;]++; 
    }
    // 遍历t
    for (char c : t) {
        hash[c - &apos;a&apos;]--;
    }
    // 检查数组是否全部为0
    for (int i = 0; i &amp;lt; 26; i++) {
        if (hash[i] != 0) return false;
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里其实掌握了 &lt;code&gt;hash[c - &apos;a&apos;]&lt;/code&gt; 这个概念，就很好理解了，&lt;code&gt;c - &apos;a&apos;&lt;/code&gt; 代表的就是当前字符在数组中的位置，然后做递增操作就能得到这个字符出现的次数。&lt;/p&gt;
&lt;p&gt;然后通过相同的操作再对另一个字符串做递减操作，最后检查数组是否全部为0，如果不为0，说明两个字符串不是字母异位词。&lt;/p&gt;
&lt;p&gt;顺便附上随想录中的图解&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%A8%E5%BC%82%E4%BD%8D%E8%AF%8D%5D(https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html#%E6%95%B0%E7%BB%84%E5%AE%9E%E7%8E%B0)&quot;&gt;^3&lt;/a&gt;，这样更好理解。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.gif&quot; alt=&quot;数组实现哈希表&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;map实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;bool isAnagram(string s, string t) {
    // 用map实现
    unordered_map&amp;lt;char, int&amp;gt; count;

    for (char c : s) count[c]++;
    for (char c : t) {
        count[c]--;
        if (count[c] &amp;lt; 0) return false;
    }

    // 检查map元素
    for (const auto&amp;amp; pair : count) { // 自动推导pair类型
        if (pair.second != 0) return false; // second指的是定义的count类型的第二个元素
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体思路其实是一致的，只是这个题目的数据量小，适合用数组。&lt;/p&gt;
&lt;p&gt;最后这里并不适用 &lt;code&gt;set&lt;/code&gt;，因为 &lt;code&gt;set&lt;/code&gt; 是用来存储不重复的元素的，而这里是要统计每个元素的个数。&lt;/p&gt;
&lt;h2&gt;两个数组的交集&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/intersection-of-two-arrays/&quot;&gt;349.两个数组的交集&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;:::tip[什么时候用set什么时候用数组？]
当一个问题中的数据量比较小，且数据量有限时，我们可以使用数组来实现哈希表。而当数据量比较大时，或者是数据量很分散的时候我们可以使用 &lt;code&gt;set&lt;/code&gt; 来实现哈希表。例如 &lt;code&gt;[1, 2, 10000]&lt;/code&gt;,这个数据很分散，如果用数组来实现哈希表，会浪费很多空间(需要开辟一个长度10000的数组)。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; intersection(vector&amp;lt;int&amp;gt;&amp;amp; nums1, vector&amp;lt;int&amp;gt;&amp;amp; nums2) {
    // 用set实现哈希表
    unordered_set&amp;lt;int&amp;gt; set(nums1.begin(), nums1.end()); // 将nums1中的元素放入set中
    unordered_set&amp;lt;int&amp;gt; ans;

    for (int i : nums2) {
        if (set.find(i) != set.end()) { // 判断nums2中的元素在set中是否存在，否则返回set.end()
            ans.insert(i); // 将该元素插入到ans中
        }
    }
    return vector&amp;lt;int&amp;gt;(ans.begin(), ans.end()); // 将ans转换为vector返回
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快乐数&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/happy-number/&quot;&gt;202.快乐数&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;哈希表解法&lt;/h3&gt;
&lt;p&gt;这里可以这么思考，如果一个数不是快乐数，那么它不是快乐数的理由是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在验证快乐数的过程中，某次计算的结果出现了重复，从而导致无限循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我们只要记录每次计算的结果，如果这个结果重复了，那么之后的计算必然是陷入无限循环。所以我们的思路就是使用一个哈希表来记录每次计算的结果，如果结果重复，那么就返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;那么我们就可以使用 &lt;code&gt;unordered_set&lt;/code&gt; 来实现哈希表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int getsum (int n) {
    int sum = 0;
    while (n) {
        sum += (n % 10) * (n % 10); // 计算每一位的平方和
        n = n / 10;
    }
    return sum;
}
bool isHappy(int n) {
    unordered_set&amp;lt;int&amp;gt; all_sum;
    int cur_sum = 0;
    while (true) {
        cur_sum = getsum(n); // 计算平方和
        if (cur_sum == 1) return true; // 如果平方和为1，返回true
        if (all_sum.find(cur_sum) != all_sum.end()) return false; // 如果平方和重复，返回false
        else all_sum.insert(cur_sum);
        n = cur_sum;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度: &lt;code&gt;O(logn)&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getsum(n)&lt;/code&gt; 计算平方和的时间复杂度是 &lt;code&gt;O(log n)&lt;/code&gt;（数字有 log n 位，每位计算 x^2）。&lt;/li&gt;
&lt;li&gt;最坏情况 下，&lt;code&gt;n&lt;/code&gt; 可能进入一个循环，导致 &lt;code&gt;seen.insert(n)&lt;/code&gt; 运行 &lt;code&gt;O(log n)&lt;/code&gt; 次。&lt;/li&gt;
&lt;li&gt;综合复杂度：&lt;code&gt;O(logn)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度: &lt;code&gt;O(logn)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快慢指针解法&lt;/h3&gt;
&lt;p&gt;之前在链表中的&lt;a href=&quot;https://m1dnightsun.github.io/MidnightSun-Blog/posts/programmercarl/linkedlist/day04_linkedlist_part2/#2-%E5%A6%82%E4%BD%95%E7%A1%AE%E5%AE%9A%E7%8E%AF%E7%9A%84%E5%85%A5%E5%8F%A3floyd-%E5%88%A4%E5%9C%88%E6%B3%95&quot;&gt;环形链表&lt;/a&gt;中介绍过Flyod判圈算法，这里也可以使用快慢指针来解决这个问题。&lt;/p&gt;
&lt;p&gt;快慢指针的方法如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快指针 &lt;code&gt;fast&lt;/code&gt; 每次走两步，即 &lt;code&gt;fast = getsum(getsum(fast))&lt;/code&gt;，每次运算两次 &lt;code&gt;getsum()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;慢指针 &lt;code&gt;slow&lt;/code&gt; 每次走一步，即 &lt;code&gt;slow = getsum(slow)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后会出现两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;fast&lt;/code&gt; 和 &lt;code&gt;slow&lt;/code&gt; 相遇，说明存在循环，返回 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;fast&lt;/code&gt; 等于1，说明是快乐数，返回 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们假设 &lt;code&gt;n = 19&lt;/code&gt;，那么计算过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n = 19 → 82 → 68 → 100 → 1  ✅（快乐数）

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;n = 2&lt;/code&gt;，那么计算过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n = 2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 ❌（不是快乐数）
        ↑                                       ↑
        └───────────────────────────────────────┘
        存在循环，那么就会演变成fast追slow，最终相遇 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双指针方法的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int getsum (int n) {
    int sum = 0;
    while (n) {
        sum += (n % 10) * (n % 10); // 计算每一位的平方和
        n = n / 10;
    }
    return sum;
}
bool isHappy(int n) {
    int slow = n, fast = getsum(n);
    while (fast != 1 &amp;amp;&amp;amp; slow != fast) { // 当fast等于1或者slow等于fast时，退出循环
        slow = getsum(slow);
        fast = getsum(getsum(fast));
    }
    return fast == 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;两数之和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/two-sum/&quot;&gt;1.两数之和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当我们需要查询一个元素是否出现过，或者一个元素是否在集合里的时候，就要第一时间想到哈希法。&lt;/p&gt;
&lt;p&gt;在这个题目中，我们可以使用哈希表来存储每个元素的索引，然后在遍历的过程中，查询 &lt;code&gt;target - nums[i]&lt;/code&gt; 是否在哈希表中，如果在，那么就返回这两个元素的索引。&lt;/p&gt;
&lt;p&gt;在这个情形中，我们不仅要保存元素的值，还需要保存元素的索引，所以我们可以使用 &lt;code&gt;map&lt;/code&gt;。
而我们不需要元素有序，所以我们可以使用 &lt;code&gt;unordered_map&lt;/code&gt; 来实现哈希表。&lt;/p&gt;
&lt;p&gt;:::tip[map中 key 和 value 分别表示什么]
判断元素是否出现，这个元素就要作为 &lt;code&gt;key&lt;/code&gt;，所以数组中的元素作为 &lt;code&gt;key&lt;/code&gt;，有 &lt;code&gt;key&lt;/code&gt; 对应的就是 &lt;code&gt;value&lt;/code&gt;，&lt;code&gt;value&lt;/code&gt; 用来存下标。
所以 map中的存储结构为 {&lt;code&gt;key&lt;/code&gt;：&lt;code&gt;数据元素&lt;/code&gt;，&lt;code&gt;value&lt;/code&gt;：&lt;code&gt;数组元素对应的下标&lt;/code&gt;}。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; twoSum(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    // key：target-nums[i]，value: 下标i
    unordered_map&amp;lt;int, int&amp;gt; map;
    vector&amp;lt;int&amp;gt; ans;
    for (int i = 0; i &amp;lt; nums.size(); i++) {
        if (map.find(target - nums[i]) != map.end()) { // 如果找到了
            int a = map[target - nums[i]]; // 也可以是map.find(target - nums[i])-&amp;gt;second
            int b = i;
            ans.push_back(a);
            ans.push_back(b);
            return ans;
        }
        map.insert(pair&amp;lt;int, int&amp;gt;(nums[i], i));
    }
    return {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本题其实有四个重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么会想到用哈希表：因为要查询元素是否出现过&lt;/li&gt;
&lt;li&gt;哈希表为什么用map：因为不仅要存储元素，也要存储元素的下标&lt;/li&gt;
&lt;li&gt;本题map是用来存什么的：存储的是 &lt;code&gt;target - nums[i]&lt;/code&gt; 和 &lt;code&gt;i&lt;/code&gt; 的对应关系，类比python中的{&lt;code&gt;target - nums[i]&lt;/code&gt;：&lt;code&gt;i&lt;/code&gt;}&lt;/li&gt;
&lt;li&gt;map中的key和value用来存什么的：key存储的是 &lt;code&gt;target - nums[i]&lt;/code&gt;，value存储的是 &lt;code&gt;i&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这四点想清楚了，才算是理解透彻了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;我们需要养成一个习惯，当遇到需要查询元素是否出现过，或者一个元素是否在集合里的时候，就要第一时间想到哈希法。&lt;/p&gt;
&lt;p&gt;然后清楚什么时候用数组，什么时候用集合，什么时候用映射。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当数据量比较小，且数据量有限时，我们可以使用数组来实现哈希表。&lt;/li&gt;
&lt;li&gt;当数据量比较大时，或者是数据量很分散的时候我们可以使用 &lt;code&gt;set&lt;/code&gt; 来实现哈希表。&lt;/li&gt;
&lt;li&gt;当我们需要存储键值对的时候，我们可以使用 &lt;code&gt;map&lt;/code&gt; 来实现哈希表。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day04-链表 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/linkedlist/day04_linkedlist_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/linkedlist/day04_linkedlist_part2/</guid><description>链表， 交换链表节点，删除倒数第n个节点，链表相交，环形链表</description><pubDate>Sat, 15 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;两两交换链表中的节点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/swap-nodes-in-pairs/description/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;遍历链表&lt;/h3&gt;
&lt;p&gt;在这种类型的题中，通常可以使用虚拟头节点的方法来做。这样可以避免对头节点的特殊处理。&lt;/p&gt;
&lt;p&gt;在遍历链表时，要注意遍历结束的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当链表节点为奇数个时，此时 &lt;code&gt;cur-&amp;gt;next-&amp;gt;next&lt;/code&gt; 为空&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当链表节点为偶数个时，此时 &lt;code&gt;cur-&amp;gt;next&lt;/code&gt; 为空&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E5%BE%AA%E7%8E%AF%E7%BB%88%E6%AD%A2%E6%9D%A1%E4%BB%B6.png&quot; alt=&quot;循环终止条件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在交换节点的过程中，画出图会更好地理解交换的进行过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E4%BA%A4%E6%8D%A2%E6%AD%A5%E9%AA%A40.png&quot; alt=&quot;交换的初始状态&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了更清晰的表达交换的过程，我们把要交换的两个节点分别记为 &lt;code&gt;first&lt;/code&gt; 和 &lt;code&gt;second&lt;/code&gt;，此时 &lt;code&gt;cur&lt;/code&gt; 指向的应该是 &lt;code&gt;first&lt;/code&gt;，交换的过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;first-&amp;gt;next&lt;/code&gt; 指向 &lt;code&gt;second-&amp;gt;next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E4%BA%A4%E6%8D%A2%E6%AD%A5%E9%AA%A41.png&quot; alt=&quot;步骤1&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;second-&amp;gt;next&lt;/code&gt; 指向 &lt;code&gt;first&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E4%BA%A4%E6%8D%A2%E6%AD%A5%E9%AA%A42.png&quot; alt=&quot;步骤2&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;cur-&amp;gt;next&lt;/code&gt; 指向 &lt;code&gt;second&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E4%BA%A4%E6%8D%A2%E6%AD%A5%E9%AA%A43.png&quot; alt=&quot;步骤3&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;交换完成，将 &lt;code&gt;cur&lt;/code&gt; 指向 &lt;code&gt;first&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0-%E4%BA%A4%E6%8D%A2%E6%AD%A5%E9%AA%A44.png&quot; alt=&quot;步骤4&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ListNode* swapPairs(ListNode* head) {
    if (!head || !head-&amp;gt;next) return head;

    ListNode *dummyhead = new ListNode(0, head);
    ListNode *cur = dummyhead; // 创建虚拟头节点，避免对头节点做特殊处理。

    while (cur-&amp;gt;next &amp;amp;&amp;amp; cur-&amp;gt;next-&amp;gt;next) {
        ListNode *first = cur-&amp;gt;next; // 交换的第一个节点
        ListNode *second = cur-&amp;gt;next-&amp;gt;next; // 交换的第二个节点
        // 交换节点
        first-&amp;gt;next = second-&amp;gt;next; // 步骤1
        second-&amp;gt;next = first; // 步骤2
        cur-&amp;gt;next = second; // 步骤3
        cur = first;// 步骤4，将cur移到下一次要交换的节点前
    }
    ListNode *ans = dummyhead-&amp;gt;next;
    delete dummyhead;
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在不清楚的地方，把图画一下就很好理解了。&lt;/p&gt;
&lt;h3&gt;递归的实现&lt;/h3&gt;
&lt;p&gt;这里也可以使用递归的方法来来实现，这里我们递归交换后续节点，并将其连接到 &lt;code&gt;head&lt;/code&gt; 的后面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* swapPairs(ListNode* head) {
    //递归终止条件
    if (!head || !head-&amp;gt;next) return head;
    //单层递归逻辑
    ListNode *newhead = head-&amp;gt;next; // // 交换后的新头节点
    ListNode *remaining = head-&amp;gt;next-&amp;gt;next; // 剩余未交换部分

    head-&amp;gt;next = swapPairs(remaining); // 递归交换后续节点，并让head连接到交换后的新链表
    newhead-&amp;gt;next = head; // 交换当前两个节点

    return newhead; // 返回新的头节点
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该递归逻辑如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始链表: 1 -&amp;gt; 2 -&amp;gt; 3 -&amp;gt; 4
swapPairs(1)

// 递归调用：
swapPairs(1)
    ├── swapPairs(3)
    │    ├── swapPairs(NULL) → 直接返回 3
    │    ├── 交换 3 和 4 → 返回 4 -&amp;gt; 3
    ├── 交换 1 和 2 → 返回 2 -&amp;gt; 1 -&amp;gt; 4 -&amp;gt; 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除链表的倒数第N个节点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/&quot;&gt;19. 删除链表的倒数第 N 个结点&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这一题其实还是有点印象的，那就是使用双指针的思路，不过还需要注意使用头节点。&lt;/p&gt;
&lt;p&gt;:::tip[快慢指针的用法]
定义两个指针，先让 &lt;code&gt;fast&lt;/code&gt; 提前走n步，然后再两个指针一起走，直到 &lt;code&gt;fast-&amp;gt;next&lt;/code&gt; 为空，那么此时 &lt;code&gt;slow&lt;/code&gt; 一定&lt;strong&gt;指向要删除节点的上一个节点&lt;/strong&gt;，也就是&lt;strong&gt;目标节点的前驱节点&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先提前让&lt;code&gt;fast&lt;/code&gt;往前走n步&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%88%A0%E9%99%A4%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B91.png&quot; alt=&quot;删除倒数第n个节点1&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直到 &lt;code&gt;fast-&amp;gt;next&lt;/code&gt; 为空，那么此时 &lt;code&gt;slow&lt;/code&gt; 就是要删除节点的前驱节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%88%A0%E9%99%A4%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B92.png&quot; alt=&quot;删除倒数第n个节点2&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;假设链表为：
1 -&amp;gt; 2 -&amp;gt; 3 -&amp;gt; 4 

n = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode *dummyhead = new ListNode(0, head); // 创建虚拟头节点
    ListNode *fast = dummyhead; //定义快慢指针
    ListNode *slow = dummyhead;

    while (n-- &amp;amp;&amp;amp; fast) { //先让fast走n步
        fast = fast-&amp;gt;next;
    }
    while (fast-&amp;gt;next) { // 当fast的下一个节点为空时，slow找到要删除节点的前一个节点
        fast = fast-&amp;gt;next;
        slow = slow-&amp;gt;next;
    }
    // 基本操作
    ListNode *temp = slow-&amp;gt;next;
    slow-&amp;gt;next = slow-&amp;gt;next-&amp;gt;next;
    delete temp;
    ListNode *ans = dummyhead-&amp;gt;next;
    delete dummyhead;
    return ans;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要注意的问题就是对于要删除的节点，要操作的一定是该节点的前驱节点。&lt;/p&gt;
&lt;h2&gt;链表相交&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/intersection-of-two-linked-lists/description/&quot;&gt;160. 相交链表&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;哈希表方法&lt;/h3&gt;
&lt;p&gt;首先最直观的一个想法是使用哈希表&lt;/p&gt;
&lt;p&gt;我们先遍历链表 &lt;code&gt;A&lt;/code&gt;, 将 &lt;code&gt;A&lt;/code&gt; 中的元素记录到哈希表中，然后再遍历链表 &lt;code&gt;B&lt;/code&gt;，如果在哈希表中找到了相同的节点，那么这个节点就是相交的节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    // 使用哈希表的方法
    // 时间复杂度：O(n+m)，其中 n 和 m 分别为两个链表的长度。
    // 空间复杂度：O(n)，其中 n 是链表 A 的长度。
    unordered_set&amp;lt;ListNode*&amp;gt; set;
    while (headA) {
        set.insert(headA);
        headA = headA-&amp;gt;next;
    }
    while (headB) {
        if (set.find(headB) != set.end()) {
            return headB;
        }
        headB = headB-&amp;gt;next;
    }
    return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;随想录中的解法&lt;/h3&gt;
&lt;p&gt;我们可以分别求出两个链表的长度，然后让较长的链表先走 &lt;code&gt;lenA - lenB&lt;/code&gt; 步，然后两个链表一起走，直到找到相交的节点。&lt;/p&gt;
&lt;p&gt;因为从短的链表开始出发，如果两个链表相交，那么就会存在一个相同的节点，使得两个链表同时到达。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    // 随想录中的方法
    int lenA = 0, lenB = 0;
    ListNode *cur = headA;
    while (cur) { // A的长度
        cur = cur-&amp;gt;next;
        lenA++;
    }
    cur = headB;
    while (cur) { // B的长度
        cur = cur-&amp;gt;next;
        lenB++;
    }
    ListNode *longL;
    ListNode *shortL;
    if (lenA &amp;gt;= lenB) { // 依照长度定义长链表和短链表
        longL = headA;
        shortL = headB;
    }
    else {
        longL = headB;
        shortL = headA;
    }
    int gap = abs(lenA - lenB); 
    while (gap--) { // 长链表先走gap步
        longL = longL-&amp;gt;next;
    }

    while(longL &amp;amp;&amp;amp; shortL) {
        if (longL == shortL) return longL; // 注意是指针相等而不是值相等
        longL = longL-&amp;gt;next;
        shortL = shortL-&amp;gt;next;
    }
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的就是在判断相交的时候一定是判断指针相等而不是值相等。&lt;/p&gt;
&lt;h3&gt;双指针法&lt;/h3&gt;
&lt;p&gt;双指针的思路，使用两个指针 p1 和 p2 分别遍历两个链表：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;p1&lt;/code&gt; 从 &lt;code&gt;headA&lt;/code&gt; 开始，&lt;code&gt;p2&lt;/code&gt; 从 &lt;code&gt;headB&lt;/code&gt; 开始。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;p1&lt;/code&gt; 走到 &lt;code&gt;nullptr&lt;/code&gt;，则跳到 &lt;code&gt;headB&lt;/code&gt; 继续走；&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;p2&lt;/code&gt; 走到 &lt;code&gt;nullptr&lt;/code&gt;，则跳到 &lt;code&gt;headA&lt;/code&gt; 继续走。&lt;/li&gt;
&lt;li&gt;如果两个链表相交，最终 &lt;code&gt;p1&lt;/code&gt; 和 &lt;code&gt;p2&lt;/code&gt; 会在相交点相遇。&lt;/li&gt;
&lt;li&gt;如果没有相交，最终 &lt;code&gt;p1 == p2 == nullptr&lt;/code&gt;，返回 &lt;code&gt;nullptr&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    if (!headA || !headB) return nullptr; // 如果有空链表，则不可能相交
    ListNode *p1 = headA, *p2 = headB;
    while (p1 != p2) {  
        // p1 走完 A 就去 B，p2 走完 B 就去 A
        p1 = (p1 == nullptr) ? headB : p1-&amp;gt;next;
        p2 = (p2 == nullptr) ? headA : p2-&amp;gt;next;
    }
    return p1; // 如果相交，p1 == p2（相交点）；如果无交点，p1 == nullptr
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比哈希表方法，可以做到 &lt;code&gt;O(1)&lt;/code&gt; 的空间复杂度，而哈希表的空间复杂度为 &lt;code&gt;O(n)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面可以说明一下为什么这种方法可以找到相交的节点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;两个指针最终都会走 A + B 的步数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;假设：headA 长度 n，headB 长度 m，相交点前分别是 a 和 b（相交部分长度 c）。&lt;/li&gt;
&lt;li&gt;p1 走 A → B，共走 a + c + b 步。&lt;/li&gt;
&lt;li&gt;p2 走 B → A，共走 b + c + a 步。&lt;/li&gt;
&lt;li&gt;最终，两者都会在相交点相遇，或者都变成 nullptr（无交点）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果没有交点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;p1 和 p2 都会走 n + m 步，最终都等于 nullptr，所以返回 nullptr。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4-%E5%8F%8C%E6%8C%87%E9%92%88.png&quot; alt=&quot;链表相交-双指针&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心思路在于两个指针 &lt;code&gt;p1&lt;/code&gt; 和 &lt;code&gt;p2&lt;/code&gt; 走相同的距离，既然两个指针都走了相同的距离，那么他们最终一定会在相交的节点相遇。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4-%E5%8F%8C%E6%8C%87%E9%92%88%E6%BC%94%E7%A4%BA1.png&quot; alt=&quot;链表相交-双指针&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4-%E5%8F%8C%E6%8C%87%E9%92%88%E6%BC%94%E7%A4%BA2.png&quot; alt=&quot;链表相交-双指针&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;环形链表II&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/linked-list-cycle-ii/description/&quot;&gt;142. 环形链表 II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于这一题，与其说是算法题，其实多少还设计到一些数学知识。&lt;/p&gt;
&lt;p&gt;解决这个问题，我们需要拆分成两个步骤。&lt;/p&gt;
&lt;h3&gt;如何确定有环&lt;/h3&gt;
&lt;p&gt;搜了一下，这个其实有个专有名词，叫&lt;strong&gt;Floyd 判圈法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::tip[Floyd 判圈法]
Floyd 判圈法（Floyd’s Cycle Detection Algorithm）是一种 使用快慢指针判断链表是否有环 的算法，也叫 龟兔赛跑算法（Tortoise and Hare Algorithm）。
:::&lt;/p&gt;
&lt;p&gt;可以使用快慢指针法，分别定义 &lt;code&gt;fast&lt;/code&gt; 和 &lt;code&gt;slow&lt;/code&gt; 指针，从头结点出发，&lt;code&gt;fast&lt;/code&gt;指针每次移动两个节点，&lt;code&gt;slow&lt;/code&gt;指针每次移动一个节点，如果 &lt;code&gt;fast&lt;/code&gt; 和 &lt;code&gt;slow&lt;/code&gt;指针在途中相遇 ，说明这个链表有环。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快指针 fast 每次走 2 步，慢指针 slow 每次走 1 步。&lt;/li&gt;
&lt;li&gt;如果 fast 和 slow 相遇，则链表有环。&lt;/li&gt;
&lt;li&gt;如果 fast 走到 nullptr，说明链表无环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为fast是走两步，slow是走一步，其实相对于slow来说，fast是一个节点一个节点的靠近slow的，所以fast一定可以和slow重合。&lt;/p&gt;
&lt;p&gt;动画演示如下&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II%5D(https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html#%E6%80%9D%E8%B7%AF)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif&quot; alt=&quot;确定环形链表-快慢指针&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里还有一个明确的地方：慢指针和快指针的相遇，一定是在&lt;strong&gt;第一圈&lt;/strong&gt;内。&lt;/p&gt;
&lt;h3&gt;如何确定环的入口（Floyd 判圈法）&lt;/h3&gt;
&lt;p&gt;假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;head → 环入口 的距离为 &lt;code&gt;a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;环入口 → 相遇点 的距离为 &lt;code&gt;b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;环的总长度为 &lt;code&gt;c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当快慢指针相遇时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;慢指针 走的路程：&lt;code&gt;a + b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;快指针 走的路程：&lt;code&gt;a + b + k * c&lt;/code&gt;（比 &lt;code&gt;slow&lt;/code&gt; 多走 &lt;code&gt;k&lt;/code&gt; 圈环）&lt;/li&gt;
&lt;li&gt;由于 &lt;code&gt;fast&lt;/code&gt; 每次走 2 步，&lt;code&gt;slow&lt;/code&gt; 走 1 步，因此：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
2(a + b) = a + b + k \cdot c
$$&lt;/p&gt;
&lt;p&gt;化简可以得到：&lt;/p&gt;
&lt;p&gt;$$
a + b = k \cdot c
$$&lt;/p&gt;
&lt;p&gt;说明 从 &lt;code&gt;head&lt;/code&gt; 走 &lt;code&gt;a&lt;/code&gt; 步，等价于从相遇点继续走 &lt;code&gt;c - b&lt;/code&gt; 步到达环入口（也就是要求的 &lt;code&gt;x&lt;/code&gt; 距离），也就是图中黄色区域的距离。&lt;/p&gt;
&lt;p&gt;也就是说让 &lt;code&gt;slow&lt;/code&gt; 从 &lt;code&gt;head&lt;/code&gt; 出发，&lt;code&gt;fast&lt;/code&gt; 从相遇点出发，每次走一步，最终相遇的点就是环的入口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II-%E5%9B%BE%E7%A4%BA.png&quot; alt=&quot;环形链表II-图示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode *detectCycle(ListNode *head) {
    if (!head) return nullptr;
    ListNode *fast = head; // 定义快慢指针
    ListNode *slow = head;

    while (fast &amp;amp;&amp;amp; fast-&amp;gt;next) { // 因为要访问fast-&amp;gt;next-&amp;gt;next所以这里也不能为空
        fast = fast-&amp;gt;next-&amp;gt;next; // fast一次走2
        slow = slow-&amp;gt;next; // slow一次走1

        if (slow == fast) { // 当slow和fast相等的时候说明相遇了
            ListNode *index1 = slow; // 此时一个指针从slow/fast出发，另一个从head出发
            ListNode *index2 = head;

            while (index1 != index2) { // 当index1==index2时，说明找到了入口
                index1 = index1-&amp;gt;next;
                index2 = index2-&amp;gt;next;
            }
            return index1;
        }
    }
    return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;今天的内容还算比较多，其实大部分的时间还是用来写文档了，全文也并没有照抄随想录，因为想着留给自己一些更深的印象。&lt;/p&gt;
&lt;p&gt;虽然有些内容是问了问gpt贴了过来，大AI时代嘛，当然在背后也是有自己好好去理解这些内容的。我觉得不失为一种有效的学习方式。&lt;/p&gt;
</content:encoded></item><item><title>Day03-链表 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/linkedlist/day03_linkedlist_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/linkedlist/day03_linkedlist_part1/</guid><description>链表基础，移除链表元素，设计链表，反转链表</description><pubDate>Fri, 14 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;链表基础&lt;/h2&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;链表（Linked List）是一种&lt;strong&gt;线性数据结构&lt;/strong&gt;，由一系列的节点（Node）组成，每个节点包含&lt;strong&gt;数据&lt;/strong&gt;和&lt;strong&gt;指向下一个节点的指针&lt;/strong&gt;。相较于数组，链表的特点是&lt;strong&gt;动态存储、插入和删除操作更高效&lt;/strong&gt;，但&lt;strong&gt;随机访问速度较慢&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;链表的种类&lt;/h3&gt;
&lt;p&gt;链表可以分为以下几种类型&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-%E5%85%B3%E4%BA%8E%E9%93%BE%E8%A1%A8%EF%BC%8C%E4%BD%A0%E8%AF%A5%E4%BA%86%E8%A7%A3%E8%BF%99%E4%BA%9B%EF%BC%81%5D(https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html#%E5%85%B3%E4%BA%8E%E9%93%BE%E8%A1%A8-%E4%BD%A0%E8%AF%A5%E4%BA%86%E8%A7%A3%E8%BF%99%E4%BA%9B)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单链表（Singly Linked List）&lt;/strong&gt;：每个节点存储一个指向下一个节点的指针。
&lt;img src=&quot;%E5%8D%95%E9%93%BE%E8%A1%A8.png&quot; alt=&quot;单链表&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双向链表（Doubly Linked List）&lt;/strong&gt;：每个节点存储&lt;strong&gt;前后两个指针&lt;/strong&gt;（指向前驱和后继）。
&lt;img src=&quot;%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8.png&quot; alt=&quot;双向链表&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环链表（Circular Linked List）&lt;/strong&gt;：链表的&lt;strong&gt;最后一个节点&lt;/strong&gt;指向&lt;strong&gt;头节点&lt;/strong&gt;，形成循环。
&lt;img src=&quot;%E5%BE%AA%E7%8E%AF%E9%93%BE%E8%A1%A8.png&quot; alt=&quot;循环链表&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双向循环链表（Doubly Circular Linked List）&lt;/strong&gt;：结合双向链表和循环链表的特性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;链表的基本操作&lt;/h3&gt;
&lt;p&gt;链表的核心操作包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插入（Insertion）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除（Deletion）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找（Search）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遍历（Traversal）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于这部分的内容，会在后面的内容进行更详细的讲解。&lt;/p&gt;
&lt;h3&gt;链表与数组的对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;插入/删除（时间复杂度）&lt;/th&gt;
&lt;th&gt;查询（时间复杂度）&lt;/th&gt;
&lt;th&gt;适用的场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;数组&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据量固定，频繁查询，较少的增删操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数据量不固定，频繁的增删操作，较少的查询&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;移除链表元素&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/remove-linked-list-elements/description/&quot;&gt;203.移除链表元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于删除链表中的节点，一般的步骤是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%E7%A4%BA%E4%BE%8B.png&quot; alt=&quot;删除链表节点&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::important[删除过程中要注意的问题]
需要相当注意的是，在删除一个节点时，操作的对象一定是&lt;strong&gt;前一个节点&lt;/strong&gt;，而不是当前节点。因为在删除才做中，一定需要使被删除节点的前一个节点的&lt;code&gt;next&lt;/code&gt;指针指向被删除节点的下一个节点。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* temp = cur-&amp;gt;next;
cur-&amp;gt;next = cur-&amp;gt;next-&amp;gt;next;
delete temp; // 在C++中，需要手动释放内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在移除链表元素时，可以有几种方法：&lt;/p&gt;
&lt;h3&gt;直接在原链表上操作&lt;/h3&gt;
&lt;p&gt;这里需要注意的是，如果头节点是要删除的节点，那么需要特殊处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* removeElements(ListNode* head, int val) {
    while (head != nullptr &amp;amp;&amp;amp; head-&amp;gt;val == val) { // 这里要使用while而不是if，因为可能有多个连续的节点都是要删除的节点
        ListNode* temp = head;
        head = head-&amp;gt;next;
        delete temp;
    }

    // 定义前驱节点
    ListNode* cur = head;
    while (cur != nullptr &amp;amp;&amp;amp; cur-&amp;gt;next != nullptr) { 
        if (cur-&amp;gt;next-&amp;gt;val == val) {
            ListNode* temp = cur-&amp;gt;next;
            cur-&amp;gt;next = cur-&amp;gt;next-&amp;gt;next;
            delete temp;
        }
        else {
            cur = cur-&amp;gt;next;
        }
    }
    return head;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用虚拟头节点&lt;/h3&gt;
&lt;p&gt;使用虚拟头节点可以简化代码逻辑，避免对头节点的特殊处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyhead = new ListNode(0, head); // 创建虚拟头节点
        ListNode* cur = dummyhead; // 定义前驱节点
        while (cur-&amp;gt;next) { 
            if (cur-&amp;gt;next-&amp;gt;val == val) {
                ListNode* temp = cur-&amp;gt;next;
                cur-&amp;gt;next = cur-&amp;gt;next-&amp;gt;next;
                delete temp;
            }
            else cur = cur-&amp;gt;next;
        }
        head = dummyhead-&amp;gt;next;
        delete dummyhead;
        return head;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归&lt;/h3&gt;
&lt;p&gt;递归删除链表元素是利用递归函数的调用栈，逐层返回时执行删除操作，从而达到删除链表节点的目的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* removeElements(ListNode* head, int val) {
    if (head == nullptr) { // 递归终止条件
        return head;
    }
    head-&amp;gt;next = removeElements(head-&amp;gt;next, val); // 递归调用
    return head-&amp;gt;val == val ? head-&amp;gt;next : head; // 如果当前节点的值等于 val，则删除当前节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设计链表&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/design-linked-list/description/&quot;&gt;707.设计链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里我们需要设计一个链表的数据结构，支持以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取链表的第&lt;code&gt;index&lt;/code&gt;个节点的值。&lt;/li&gt;
&lt;li&gt;在链表的头部插入一个新节点。&lt;/li&gt;
&lt;li&gt;在链表尾部插入一个新节点。&lt;/li&gt;
&lt;li&gt;在链表中的第&lt;code&gt;index&lt;/code&gt;个节点前添加一个新节点。&lt;/li&gt;
&lt;li&gt;删除链表中的第&lt;code&gt;index&lt;/code&gt;个节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;class MyLinkedList {
public:

    struct ListNode { // 定义链表节点
        int val;
        ListNode* next;
        ListNode(int val): val(val), next(nullptr){} // 构造函数
    };
    
    MyLinkedList() {
        _size = 0; // 初始化链表长度
        _dummyhead = new ListNode(0); // 创建虚拟头节点
    }
    
    int get(int index) { // 获取第index个节点的值
        // 注意下标问题，这里的index是从0开始的，例如只有一个元素，那么它的index(0)=size-1
        if (index &amp;lt; 0 || index &amp;gt; (_size - 1)) return -1; // index是从0开始的，所以判断条件是size-1
        ListNode* cur = _dummyhead-&amp;gt;next; 
        while (index--){
            cur = cur-&amp;gt;next;
        }
        return cur-&amp;gt;val;
    }
    
    void addAtHead(int val) { // 在头部添加节点
        ListNode* new_node = new ListNode(val); // 创建新节点
        new_node-&amp;gt;next = _dummyhead-&amp;gt;next; // 新节点直接指向虚拟头节点的下一个节点
        _dummyhead-&amp;gt;next = new_node; // 虚拟头节点指向新节点，新节点成为新的头节点
        _size++; // 链表长度+1
    }
    
    void addAtTail(int val) { // 在尾部添加节点
        ListNode* new_node = new ListNode(val); // 创建新节点
        ListNode* cur = _dummyhead; // 创建前驱节点，指向虚拟头节点
        while (cur-&amp;gt;next != nullptr) { // 遍历到链表尾
            cur = cur-&amp;gt;next;
        }
        cur-&amp;gt;next = new_node; // 前驱节点指向新节点
        _size++; // 链表长度-1
    }
    
    void addAtIndex(int index, int val) { // 在第index个节点前添加节点
        if (index &amp;gt; _size || index &amp;lt; 0) {return;} // 若index不合法，如果index=size，则说明在末尾添加
        //如果index=0，则说明在头部添加
        ListNode* new_node = new ListNode(val); // 创建新节点
        ListNode* cur = _dummyhead; // 创建前驱节点，指向虚拟头节点
        while (index--) { // 遍历到index位置，例如index=0，即在头节点插入
            cur = cur-&amp;gt;next;
        }
        new_node-&amp;gt;next = cur-&amp;gt;next; // 新节点指向前驱节点的下一节点
        cur-&amp;gt;next = new_node; // 前驱节点指向新节点
        _size++; // 链表长度+1
    }
    
    void deleteAtIndex(int index) { // 删除第index个节点
        if (index &amp;gt;= _size || index &amp;lt; 0) {return;} //若index不合法
        //eg. 若size=1，index=1，那要删除的其实是第二个节点（不存在），所以不合法
        ListNode* cur = _dummyhead; // 创建前驱节点指向虚拟头节点
        while (index--) { // 遍历到index节点
            cur = cur-&amp;gt;next;
        }   
        ListNode* temp = cur-&amp;gt;next; // 将要删除的节点保存到临时节点
        cur-&amp;gt;next = cur-&amp;gt;next-&amp;gt;next; // 前驱节点指向要删除节点的下一个节点
        delete temp; // 删除临时节点
        //delete命令指示释放了tmp指针原本所指的那部分内存，
        //被delete后的指针tmp的值（地址）并非就是NULL，而是随机值。也就是被delete后，
        //如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
        //如果之后的程序不小心使用了tmp，会指向难以预想的内存空间
        temp=nullptr;
        _size--;       
    }

private:
    int _size; // 链表长度
    ListNode* _dummyhead; // 虚拟头节点
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important[添加节点的顺序问题]
和删除链表中元素类似，在链表中添加节点的时候，一定要注意操作的顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新节点指向前驱节点的下一个节点&lt;/li&gt;
&lt;li&gt;前驱节点指向新节点
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;反转链表&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-linked-list/description/&quot;&gt;206.反转链表&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;反转链表是一个对考察基础数据结构操作能力很好的问题。&lt;/p&gt;
&lt;h3&gt;双指针解法&lt;/h3&gt;
&lt;p&gt;在这里可以定义一个前驱节点&lt;code&gt;pre&lt;/code&gt;和一个当前节点&lt;code&gt;cur&lt;/code&gt;：
&lt;img src=&quot;%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A81.png&quot; alt=&quot;翻转链表1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后让&lt;code&gt;cur&lt;/code&gt;指向&lt;code&gt;pre&lt;/code&gt;
&lt;img src=&quot;%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A82.png&quot; alt=&quot;翻转链表2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;cur&lt;/code&gt;为空时，说明已经遍历完链表，此时&lt;code&gt;pre&lt;/code&gt;指向的就是反转后的链表头节点。
&lt;img src=&quot;%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A83.png&quot; alt=&quot;翻转链表3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用双指针的写法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ListNode* reverseList(ListNode* head) {
        if (head == nullptr) return head; 
        ListNode* cur = head; // 定义cur指向头节点
        ListNode* pre = nullptr; //定义pre指向空节点
        ListNode* temp; // 定义临时节点用于存储cur的下一个节点
        while (cur) { // 当cur指向空时，说明遍历结束
            temp = cur-&amp;gt;next; // 存储cur的下一个节点
            cur-&amp;gt;next = pre; // 翻转操作
            pre = cur; // 将pre向前移
            cur = temp; //将cur向后移
        }
        return pre;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里需要注意的就是操作的顺序，一定是先存储&lt;code&gt;cur&lt;/code&gt;的下一个节点，然后再进行翻转操作，先将&lt;code&gt;pre&lt;/code&gt;向前移，再将&lt;code&gt;cur&lt;/code&gt;向后移。&lt;/p&gt;
&lt;h3&gt;递归解法&lt;/h3&gt;
&lt;p&gt;可以依据双指针的思路，写出递归的解法。和双指针法是一样的逻辑，同样是当cur为空的时候循环结束，不断将cur指向pre的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 递归写法
ListNode* reverse(ListNode* pre, ListNode* cur) {
    if (cur == nullptr) return pre; // 递归终止条件，终止之后，返回的是pre
    // 单层递归
    ListNode* temp = cur-&amp;gt;next;
    cur-&amp;gt;next = pre; // 翻转操作
    // 进入下一层递归
    // pre = cur;
    // cur = temp; 
    return reverse(cur, temp); // 按照双指针的解法来写
}

ListNode* reverseList(ListNode* head) {
    return reverse(nullptr, head); //按照双指针解法，pre指向空节点，cur指向头节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比双指针解法的过程，来写递归的思路会更清晰一些&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;链表的操作自己理解起来还是比较清晰的，不过在写代码的过程中，还是不太熟练，还是要更多的时间来练习。
最后，递归这个东西还是比较抽象的，希望后面能够在更多的题目中，更加熟练的掌握递归的写法。&lt;/p&gt;
</content:encoded></item><item><title>Day02-数组 part02</title><link>https://m1dnightsun.github.io/posts/programmercarl/array/day02_array_part2/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/array/day02_array_part2/</guid><description>滑动窗口, 螺旋矩阵, 前缀和</description><pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;滑动窗口-最小子数组&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-size-subarray-sum/description/&quot;&gt;209.长度最小的子数组&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;首先解决这个问题最简单的方法就是暴力求解。&lt;/p&gt;
&lt;p&gt;使用两个循环，外层循环遍历数组，内层循环计算以当前元素为起点的子数组和。然后比较子数组和是否大于等于目标值，如果是则更新最小长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int minSubArrayLen(int target, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        // 一般解法, 超时
        int ans = INT_MAX;
        int n = nums.size() - 1;
        for (int i = 0; i &amp;lt;=  n; i++) {
            int sum = 0;
            for (int j = i; j &amp;lt;= n; j++){
                sum += nums[j];
                if (sum &amp;gt;= target) {
                    ans = min(ans, (j-i+1));
                    break; //即使提前中断内循环，时间复杂度也是O(n^2)，结果上来说还是超时
                }
            }
        }
        return ans == INT_MAX ? 0 : ans;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note[滑动窗口]
滑动窗口的思路是维护一个窗口，使得窗口内的元素和满足某种条件。
:::
在这个问题中，实现滑动窗口需要考虑三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;滑动窗口内是什么
&lt;ul&gt;
&lt;li&gt;窗口其实就是满足条件(&lt;code&gt;subArraySum &amp;gt;= target&lt;/code&gt;)的一段区间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如何移动滑动窗口的起始位置
&lt;ul&gt;
&lt;li&gt;如果当前窗口的值大于等于&lt;code&gt;target&lt;/code&gt;了，窗口就要向前移动了（也就是该缩小了）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如何移动滑动窗口的结束位置
&lt;ul&gt;
&lt;li&gt;窗口的结束位置就是遍历数组的指针，也就是循环里的&lt;code&gt;end&lt;/code&gt;索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而关键在于如何移动窗口的起始位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (sum &amp;gt;= target) { 
    // 每次加入新元素后，检查 sum 是否大于等于目标值 s。如果是，则计算当前子数组的长度，
    //并与已记录的最小长度进行比较，取较小值。
    //然后，移动起始指针 i，并从 sum 中减去 nums[i]，以缩小窗口，直到 sum 小于 s
    //随后start向前移动一位
    subLength = end - start  + 1;
    ans = min(ans, subLength); 
    sum -= nums[start++]; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;滑动窗口的过程，其实类似向前蠕动的蛇，每次向前移动一格，然后检查是否满足条件，满足条件则记录当前窗口的长度，然后继续向前移动。&lt;/p&gt;
&lt;p&gt;完整的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int minSubArrayLen(int target, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        // 使用滑动窗口
        int sum = 0;
        int ans = INT_MAX;
        int start = 0, end = 0; //滑动窗口的起始和终止位置
        int subLength = 0; //滑动窗口的长度
        while (end &amp;lt; nums.size()) { // 移动结束指针，将nums[end]加入子数组
            sum += nums[end];
            while (sum &amp;gt;= target) { //注释如上
                subLength = end - start  + 1;
                ans = min(ans, subLength); 
                sum -= nums[start++]; 
            }
            end++; //使用的while的写法，最后要将end++
        }
        return ans == INT_MAX? 0 : ans;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[时间复杂度]
在滑动窗口方法中，每个元素最多被访问两次（一次被右指针访问，另一次被左指针访问），因此时间复杂度为 O(n)。
:::&lt;/p&gt;
&lt;h2&gt;螺旋矩阵&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/spiral-matrix/&quot;&gt;54.螺旋矩阵&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/spiral-matrix-ii/&quot;&gt;59.螺旋矩阵II&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;随想录里面讲到的题目是59.螺旋矩阵II，本质上是同一个类型，59只是将一般矩阵换成了方阵，都是了解循环的边界处理条件。&lt;/p&gt;
&lt;p&gt;这里我将两题都做了，然后以54.螺旋矩阵为例，以及这种题的两种思路。&lt;/p&gt;
&lt;h3&gt;随想录中的思路&lt;/h3&gt;
&lt;p&gt;要模拟螺旋矩阵的遍历，就是从四个方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遍历上行从左到右&lt;/li&gt;
&lt;li&gt;遍历右列从上到下&lt;/li&gt;
&lt;li&gt;遍历下行从右到左&lt;/li&gt;
&lt;li&gt;遍历左列从下到上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%9E%BA%E6%97%8B%E6%95%B0%E7%BB%84%E9%81%8D%E5%8E%86.png&quot; alt=&quot;螺旋数组遍历&quot; /&gt;
&amp;lt;!-- &amp;lt;img src=&quot;螺旋数组遍历1.png&quot; title=&quot;图片title&quot; width=&quot;50%&quot;&amp;gt; --&amp;gt;&lt;/p&gt;
&lt;p&gt;在这里，对于每一次遍历，都是最后一个元素不处理，留给下一次遍历处理，这也是坚持了每条边左闭右开的原则。&lt;/p&gt;
&lt;p&gt;由于在螺旋遍历过程中，每次遍历都会缩小矩阵的范围。当行数和列数的差值为奇数时，最终可能会剩下一行或一列未被遍历，因此也许要对这集中情况进行处理。&lt;/p&gt;
&lt;p&gt;在59.螺旋矩阵II中，只是将矩阵换成了方阵，因此最多只会出现剩余一个元素的情况。
依照随想录给出的思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; spiralOrder(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix) {
    int startx = 0, starty = 0;
    int m = matrix.size();
    int n = matrix[0].size();
    int loop = min(m, n) / 2;
    int offset = 1;
    int i, j;
    vector&amp;lt;int&amp;gt; ans;
    while (loop--) {
        i = startx;
        j = starty;
        for (j = starty; j &amp;lt; n - offset; j++) ans.push_back(matrix[i][j]); //记录当前循环上面的行          
        for (i = startx; i &amp;lt; m - offset; i++) ans.push_back(matrix[i][j]); //记录当前循环右边的列    
        for ( ; j &amp;gt; starty; j--) ans.push_back(matrix[i][j]); //记录当前循环下面的行 
        for( ; i &amp;gt; startx; i--) ans.push_back(matrix[i][j]); //记录当前循环左边的列
        startx++;
        starty++;
        offset++;
    }
        // 如果循环次数是奇数，那么需要把中间的元素加入结果
    if (min(m, n) % 2 == 1) {
        if (m &amp;lt; n) {
            // 中间剩余一行
            for (j = starty; j &amp;lt; n - starty; j++) {
                ans.push_back(matrix[startx][j]);
            }
        } else if (m &amp;gt; n) {
            // 中间剩余一列
            for (i = startx; i &amp;lt; m - startx; i++) {
                ans.push_back(matrix[i][starty]);
            }
        } else {
            // 中间剩余一个元素
            ans.push_back(matrix[startx][starty]);
        }
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;逐层模拟&lt;/h3&gt;
&lt;p&gt;我们可以通过定义上下左右四个边界来模拟螺旋矩阵的遍历。确保了对所有情况的处理，包括奇数维度的矩阵。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化边界：定义 left、right、top、bottom 四个变量，分别表示当前层的左、右、上、下边界。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;循环遍历：在 left &amp;lt;= right 且 top &amp;lt;= bottom 的条件下，进行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;从左到右遍历上边界：从 left 到 right，将元素添加到结果中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从上到下遍历右边界：从 top + 1 到 bottom，将元素添加到结果中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从右到左遍历下边界（需要确保当前层至少有两行两列）：从 right - 1 到 left，将元素添加到结果中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从下到上遍历左边界（需要确保当前层至少有两行两列）：从 bottom - 1 到 top + 1，将元素添加到结果中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缩小边界：在完成一层的遍历后，缩小边界范围，进入下一层的遍历。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; spiralOrder(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix) {
    vector&amp;lt;int&amp;gt; ans;
    int m = matrix.size(), n = matrix[0].size();

    int left = 0, right = n - 1, top = 0, bottom = m - 1; // 定义上下左右边界，逐层遍历

    while (left &amp;lt;= right &amp;amp;&amp;amp; top &amp;lt;= bottom) { //当对应边界还未交错时，进行处理
        // 列j: 从左边界处理到右边界，行此时为上边界top
        for (int j = left; j &amp;lt;= right; ++j) ans.push_back(matrix[top][j]);
        // 行i: 从上边界处理到下边界，列此时为右边界right
        // 因为上边界处理完，所以i=top+1
        for (int i = top + 1; i &amp;lt;= bottom; ++i) ans.push_back(matrix[i][right]);

        if (left &amp;lt; right &amp;amp;&amp;amp; top &amp;lt; bottom) { // 当矩阵至少拥有两行两列时，处理逻辑与上右不一样
            // 列j：从右边界处理到左边界，行此时为下边界bottom
            //因为右边界处理完，所以j = right - 1，同时不访问左下角元素
            for (int j = right - 1; j &amp;gt; left; --j) ans.push_back(matrix[bottom][j]);
            // 行i：从下边界处理到上边界，列此时为左边界left
            for (int i = bottom; i &amp;gt; top; --i) ans.push_back(matrix[i][left]);
        }
        ++left; --right; ++top; --bottom; //处理边界
    }
    return ans; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important[边界处理]
与随想录中不同的是，在这种处理方法中，处理下边界和左边界的循环条件与上边界和右边界的循环条件有所不同，主要原因在于避免重复访问元素。
:::&lt;/p&gt;
&lt;h2&gt;区间和&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1070&quot;&gt;kamacoder-58.区间和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里涉及到的一个数据中的技巧：前缀和(Prefix Sum)。&lt;/p&gt;
&lt;p&gt;:::note[前缀和（Prefix Sum）简介]
前缀和是一种用于高效计算&lt;strong&gt;子数组区间和&lt;/strong&gt;的数据结构。它的核心思想是&lt;strong&gt;预先计算数组的前缀和数组&lt;/strong&gt;，然后通过&lt;strong&gt;O(1) 时间复杂度&lt;/strong&gt;查询任意区间的和。
:::&lt;/p&gt;
&lt;h3&gt;前缀和的定义&lt;/h3&gt;
&lt;p&gt;对于一个数组 &lt;code&gt;A&lt;/code&gt; ，定义其前缀和数组 &lt;code&gt;S&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;S[i] = A[0] + A[1] + ... + A[i-1]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;S[i]&lt;/code&gt; 表示从索引 &lt;code&gt;0&lt;/code&gt; 到索引 &lt;code&gt;i-1&lt;/code&gt; （不含 &lt;code&gt;i&lt;/code&gt; ）的元素总和。&lt;/li&gt;
&lt;li&gt;通过前缀和，可以 快速求解子数组&lt;code&gt;[L, R]&lt;/code&gt;的和：
&lt;code&gt;SUM(L, R) = S[R+1] - S[L]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样，计算 &lt;strong&gt;任意区间和&lt;/strong&gt; 仅需 &lt;strong&gt;O(1)&lt;/strong&gt; 时间，而不是直接遍历数组求和的 &lt;strong&gt;O(n)&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;计算前缀和&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;(1) 构建前缀和数组&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

vector&amp;lt;int&amp;gt; computePrefixSum(const vector&amp;lt;int&amp;gt;&amp;amp; A) {
    int n = A.size();
    vector&amp;lt;int&amp;gt; S(n + 1, 0); // S[0] = 0，保证计算区间时不会越界
    for (int i = 1; i &amp;lt;= n; i++) {
        S[i] = S[i - 1] + A[i - 1]; // S[i] 存储 A[0] 到 A[i-1] 的和
    }
    return S;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;code&gt;O(n)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) 快速计算区间和&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int rangeSum(const vector&amp;lt;int&amp;gt;&amp;amp; S, int L, int R) {
    return S[R + 1] - S[L]; // O(1) 计算子数组 [L, R] 的和
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
    vector&amp;lt;int&amp;gt; A = {2, 3, 5, 7, 11, 13};
    vector&amp;lt;int&amp;gt; S = computePrefixSum(A);

    cout &amp;lt;&amp;lt; &quot;Sum of A[1] to A[3]: &quot; &amp;lt;&amp;lt; rangeSum(S, 1, 3) &amp;lt;&amp;lt; endl; // 3 + 5 + 7 = 15
    cout &amp;lt;&amp;lt; &quot;Sum of A[2] to A[5]: &quot; &amp;lt;&amp;lt; rangeSum(S, 2, 5) &amp;lt;&amp;lt; endl; // 5 + 7 + 11 + 13 = 36
}

Output:
Sum of A[1] to A[3]: 15
Sum of A[2] to A[5]: 36
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预处理&lt;/strong&gt;（计算前缀和）：&lt;code&gt;O(n)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询区间和&lt;/strong&gt;：&lt;code&gt;O(1)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的就是这里的前缀和数组的长度是&lt;code&gt;n+1&lt;/code&gt;，因为&lt;code&gt;S[0] = 0&lt;/code&gt;，这样可以保证计算区间和时不会越界，这一点与kamacode中的解法有些区别。&lt;/p&gt;
&lt;h2&gt;开发商购买土地问题&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://kamacoder.com/problempage.php?pid=1044&quot;&gt;kamacoder-44.开发商购买土地&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里其实也是一个可以使用前缀和来解决问题，对于我来说前缀和的思路可能更好理解。关键就是无论对于哪种分割方向来说，都是是找到一个index，使得区间&lt;code&gt;[0, index]&lt;/code&gt;的总和与区间&lt;code&gt;(index, L]&lt;/code&gt;的总和差值最小。
&lt;img src=&quot;%E5%9C%9F%E5%9C%B0%E5%88%92%E5%88%86%E6%BC%94%E7%A4%BA.png&quot; alt=&quot;土地划分演示&quot; /&gt;&lt;/p&gt;
&lt;p&gt;代码写的有些冗长，但是思路是清晰的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;climits&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int main() {
  int n, m;
  cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;
  // 初始化矩阵
  vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; vec(n, vector&amp;lt;int&amp;gt;(m, 0));
  for (int i = 0; i &amp;lt; n; i++) {
    for (int j = 0; j &amp;lt; m; j++) {
      cin &amp;gt;&amp;gt; vec[i][j];
    }
  }
    // 计算横向总和
    vector&amp;lt;int&amp;gt; total_h(n, 0);
    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; m; j++) {
            total_h[i] += vec[i][j];
        }
    }
    // 计算横向前缀和
    vector&amp;lt;int&amp;gt; p_h(n + 1, 0);
    for (int i = 1; i &amp;lt;= n; i++) {
        p_h[i] = p_h[i - 1] + total_h[i - 1];
    }
    // 计算纵向总和
    vector&amp;lt;int&amp;gt; total_v(m, 0);
    for (int j = 0; j &amp;lt; m; j++) {
        for (int i = 0; i &amp;lt; n; i++) {
            total_v[j] += vec[i][j];
        }
    }
    // 计算纵向前缀和
    vector&amp;lt;int&amp;gt; p_v(m + 1, 0);
    for (int i = 1; i &amp;lt;= m; i++) {
        p_v[i] = p_v[i - 1] + total_v[i - 1];
    }
  // 无论对于哪种分割方向来说，都是是找到一个index，使得区间[0, index]的总和与区间(index, L]的总和差值最小。
  int ans = INT_MAX;
  int block_A, block_B;
  // 对于横向
  for (int idx = 1; idx &amp;lt; n; idx++) {
    // 分给A
    block_A = p_h[idx];
    // 分给B
    block_B = p_h[n] - p_h[idx];
    ans = min(ans, abs(block_A - block_B));
  }
  // 对于纵向
  for (int idx = 1; idx &amp;lt; m; idx++) {
    // 分给A
    block_A = p_v[idx];
    // 分给B
    block_B = p_v[m] - p_v[idx];
    ans = min(ans, abs(block_A - block_B));
  }
  cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的是
&amp;lt;!-- ```cpp
block_A = p_h[idx]; // 其实这里的内容是p_h[idx] - p_h[0], 由于p_h[0] = 0，所以这里可以直接写成p_h[idx]
block_B = p_h[n] - p_h[idx];&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这里的`p_h[n]`是整个矩阵的和，`p_h[idx]`是前`idx`行的和，所以`p_h[n] - p_h[idx]`就是剩下的行的和。
也就是说，`p_h[idx]`只属于A，而不会同时算入B。 --&amp;gt;

以横向为例，在计算的过程中，我们使用了下面的方法来计算前缀和：
```cpp
for (int i = 1; i &amp;lt;= n; i++) {
    p_h[i] = p_h[i - 1] + total_h[i - 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 &lt;code&gt;idx=2&lt;/code&gt;，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p_h[2] = total_h[0] + total_h[1]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_A = p_h[2] - p_h[0]&lt;/code&gt; -&amp;gt; A 包含第 0, 1 行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_B = p_h[n] - p_h[2]&lt;/code&gt; -&amp;gt; B 包含第 2, 3, ..., n-1 行&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;p_h[n] = total_h[0] + total_h[1] + total_h[2] + ... + total_h[n-1];
p_h[n] - p_h[2] = total_h[2] + total_h[3] + ... + total_h[n-1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B 仅包含 idx 及之后的行（即 从 &lt;code&gt;idx&lt;/code&gt; 开始到 &lt;code&gt;n-1&lt;/code&gt;）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;弄清楚了前缀和的下标问题，其实就很好理解了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;今天的内容说实话有点多，滑动窗口的应用，螺旋矩阵的遍历，以及前缀和的应用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;滑动窗口通过动态调整子数组的范围，可以有效解决子数组问题，如最小子数组和。&lt;/li&gt;
&lt;li&gt;螺旋矩阵的遍历方法包括模拟四个方向的移动和逐层缩小边界，确保了矩阵所有元素的正确访问，需要注意的是边界的处理。&lt;/li&gt;
&lt;li&gt;前缀和作为高效计算区间和的技巧，能够在 O(1) 时间内查询子数组和，并用于优化矩阵分割问题，如开发商购买土地问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实还有很多相关题没看，等到有时间再自己看看吧。&lt;/p&gt;
&lt;p&gt;摸了摸了摸了！&lt;/p&gt;
</content:encoded></item><item><title>Day01-数组 part01</title><link>https://m1dnightsun.github.io/posts/programmercarl/array/day01_array_part1/</link><guid isPermaLink="true">https://m1dnightsun.github.io/posts/programmercarl/array/day01_array_part1/</guid><description>数组基础，二分查找，双指针</description><pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数组基础&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;数组是一种存放在连续空间内的相同类型的集合&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;连续存储：在内存中占据连续的地址空间。&lt;/li&gt;
&lt;li&gt;固定长度：数组的长度在定义时就已经确定，不可动态调整（某些语言例如python的列表可以动态扩展）。&lt;/li&gt;
&lt;li&gt;随机访问：支持O(1)的索引访问。&lt;/li&gt;
&lt;li&gt;相同数据类型：数组中的元素类型必须相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;静态数组不能直接插入新元素，必须创建一个更大的数据复制元素。
同理数组也无法“删除”一个元素，所谓“删除”，指的是用原来的元素进行覆盖。&lt;/p&gt;
&lt;h2&gt;二分查找&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-search/description/&quot;&gt;704.二分查找&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是一个典型的二分查找问题，给定一个有序数组，查找目标值的索引。
需要注意的是，二分查找的边界条件，以及mid的计算方法。&lt;/p&gt;
&lt;p&gt;在通常的情况下，我们可以有两种写法：
:::note[1. 左闭右闭区间]
对于第一种写法：区间的定义这就决定了二分法的代码应该如何写，因为定义target在[left, right]区间，所以有如下两点：
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;while (left &amp;lt;= right) 要使用 &amp;lt;= ，因为left == right是有意义的，所以使用 &amp;lt;=&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;if (nums[middle] &amp;gt; target) right 要赋值为 middle - 1，因为当前这个nums[middle]一定不是target，那么接下来要查找的左区间结束下标位置就是 middle - 1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int search(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int left = 0, right = nums.size() - 1;
        while (left &amp;lt;= right) { //因为区间是左闭右闭，因此left与right相等是有意义的，所以使用&amp;lt;=
            int mid = left + (right - left) / 2;
            if (nums[mid] &amp;gt; target) { // target在左区间， 所以[left, mid - 1]，nums[mid]已经被排除，所以mid - 1
                right = mid - 1;
            }
            else if (nums[mid] &amp;lt; target) { // target在右区间，所以[mid + 1, right]
                left = mid + 1;
            }
            else {
                return mid;
            }
        }
        return -1;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note[2. 左闭右开区间]
如果说定义 target 是在一个在左闭右开的区间里，也就是[left, right) ，那么二分法的边界处理方式则截然不同。有如下两点：
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;while (left &amp;lt; right)，这里使用 &amp;lt; ,因为left == right在区间[left, right)是没有意义的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;if (nums[middle] &amp;gt; target) right 更新为 middle，因为当前nums[middle]不等于target，去左区间继续寻找，而寻找区间是左闭右开区间，所以right更新为middle，即：下一个查询区间不会去比较nums[middle]&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int search_v2(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int left = 0, right = nums.size(); // 因为是左闭右开，因此right = nums.size()
        while (left &amp;lt; right) { // 因为区间是左闭右开，因此left与right相等是没有意义的，所以使用&amp;lt;
            int mid = left + (right - left) / 2;
            if (nums[mid] &amp;gt; target) { //target在左区间，所以[left, mid)，因为左闭右开，下一次查找为[left, mid), nums[mid]并不会被再次访问
                right = mid;
            }
            else if (nums[mid] &amp;lt; target) { //target在有区间，所以[mid + 1, right)
                left = mid + 1;
            }
            else {
                return mid;
            }
        }
        return -1;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;循环条件&lt;/strong&gt;：根据区间的定义，循环条件有所不同：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;左闭右闭区间&lt;/strong&gt;：使用 &lt;code&gt;while (left &amp;lt;= right)&lt;/code&gt;，因为当 &lt;code&gt;left == right&lt;/code&gt; 时，区间 &lt;code&gt;[left, right]&lt;/code&gt; 仍然有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;左闭右开区间&lt;/strong&gt;：使用 &lt;code&gt;while (left &amp;lt; right)&lt;/code&gt;，因为当 &lt;code&gt;left == right&lt;/code&gt; 时，区间 &lt;code&gt;[left, right)&lt;/code&gt; 已为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;对数组元素进行操作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/remove-element/&quot;&gt;27.移除元素&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;:::important[数组的特性]
数组中的元素无法直接删除，只能通过覆盖的方式进行删除。
:::
最直观的想法是，遍历数组，如果遇到目标值，就将后面的元素向前移动一位，然后数组长度减一。
这里如果使用暴力解法，时间复杂度为O(n^2), 空间复杂度为O(1)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int removeElement(vector&amp;lt;int&amp;gt;&amp;amp; nums, int val) {
        int n = nums.size();
        int i = 0;
        while (i &amp;lt; n) {
            if (nums[i] == val) {
                for (int j = i + 1; j &amp;lt; n; ++j) { 
                    nums[j - 1] = nums[j]; //将后面的元素向前移动一位
                }
                --n; // 数组长度减一
            } else {
                ++i;
            }
        }
        return n;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[双指针]
双指针法（快慢指针法）可以将时间复杂度降低到O(n)，空间复杂度为O(1)。
:::&lt;/p&gt;
&lt;p&gt;双指针的思路实际上是用一层for循环做了暴力解法中两层for循环的操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快指针：寻找新数组的元素 ，新数组就是不含有目标元素的数组&lt;/li&gt;
&lt;li&gt;慢指针：指向更新 新数组下标的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 通过将快指针指向的元素赋值给慢指针指向的元素，实现元素的覆盖
int removeElement(vector&amp;lt;int&amp;gt;&amp;amp; nums, int val) {
        int slow = 0;
        for (int fast = 0; fast &amp;lt; nums.size(); fast++) { // 定义快指针，遍历数组
            if (nums[fast] != val) { 
                // 如果快指针不等于要删除的元素，那么将快指针指向的元素赋值给慢指针指向的元素，然后慢指针向后移动一位
                // 如果快指针等于要删除的元素，那么快指针向后移动一位，慢指针不动
                nums[slow++] = nums[fast];
            }
        }
        return slow;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动画演示如下&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0%5D(https://programmercarl.com/0027.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0.html#%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95)&quot;&gt;^1&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95.gif&quot; alt=&quot;快慢指针动画演示&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;双指针&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/squares-of-a-sorted-array/&quot;&gt;977.有序数组的平方&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;一般解法&lt;/h3&gt;
&lt;p&gt;最简单的想法是，遍历数组，将每个元素平方，然后排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; sortedSquares(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        for (int i = 0; i &amp;lt; nums.size(); i++) { //时间复杂度为O(n)
            nums[i] = nums[i] * nums[i];
        }
        sort(nums.begin(), nums.end()); // 这里使用的是快排，时间复杂度为O(nlogn)
        return nums;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双指针解法&lt;/h3&gt;
&lt;p&gt;:::tip[有序数组的特性]
首先数组是有序的，但因为存在负数，所以平方后的数组不一定是有序的。根据数组有序的特性，左右两端的平方值其中之一一定为最大，所以我们可以使用双指针，从两端开始遍历，将较大的平方值放入结果数组中。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; sortedSquares(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int left = 0;
        int right = nums.size() - 1;
        vector&amp;lt;int&amp;gt; res(nums.size());
        int index = res.size() - 1; // 从后往前填充结果数组

        while (left &amp;lt;= right) { // 这里使用的是左闭右闭
            int square_left = nums[left] * nums[left];
            int square_right = nums[right] * nums[right];

            if (square_left &amp;lt; square_right) { //当左边的平方小于右边的平方时，将右边的平方放入结果数组中
                res[index--] = square_right;
                right--;
            } else {                          //当左边的平方大于等于右边的平方时，将左边的平方放入结果数组中
                res[index--] = square_left;
                left++;
            }
        }
        return res;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其动画演示如下&lt;a href=&quot;%5B%E4%BB%A3%E7%A0%81%E9%9A%8F%E6%83%B3%E5%BD%95-977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9%5D(https://programmercarl.com/0977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.html)&quot;&gt;^2&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://code-thinking.cdn.bcebos.com/gifs/977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.gif&quot; alt=&quot;有序数组平方动画演示&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Day1总归是结束了，大概会是个好的开始吧。&lt;/p&gt;
&lt;p&gt;其实自己之前只会写python，这次使用C++一个很大的目的还是想让自己在做题的过程中，熟悉C++的用法，多掌握一些东西倒也不是坏事。&lt;/p&gt;
&lt;p&gt;训练营第一天的内容其实很快就做完了，大把时间还是用来想怎么写markdown，这个博客还在刚刚完成部署的阶段，后日会慢慢完善吧。&lt;/p&gt;
&lt;p&gt;要学的东西还是太多了。&lt;/p&gt;
</content:encoded></item></channel></rss>