diff --git a/README.md b/README.md index d844c3b4..24ef765b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) * [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) * [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) +* [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) +* [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) * 精选链表相关的面试题 * 精选字符串相关的面试题 * 精选栈与队列相关的面试题 @@ -428,6 +430,7 @@ int countNodes(TreeNode* root) { |[0450.删除二叉搜索树中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0450.删除二叉搜索树中的节点.md) |树 |中等|**递归**| |[0454.四数相加II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0454.四数相加II.md) |哈希表 |中等| **哈希**| |[0459.重复的子字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0459.重复的子字符串.md) |字符创 |简单| **KMP**| +|[0486.预测赢家](https://github.com/youngyangyang04/leetcode/blob/master/problems/0486.预测赢家.md) |动态规划 |中等| **递归** **记忆递归** **动态规划**| |[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法**| |[0541.反转字符串II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0541.反转字符串II.md) |字符串 |简单| **模拟**| |[0575.分糖果](https://github.com/youngyangyang04/leetcode/blob/master/problems/0575.分糖果.md) |哈希表 |简单|**哈希**| @@ -437,6 +440,7 @@ int countNodes(TreeNode* root) { |[0701.二叉搜索树中的插入操作](https://github.com/youngyangyang04/leetcode/blob/master/problems/0701.二叉搜索树中的插入操作.md) |树 |简单|**递归** **迭代**| |[0705.设计哈希集合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0705.设计哈希集合.md) |哈希表 |简单|**模拟**| |[0707.设计链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0707.设计链表.md) |链表 |中等|**模拟**| +|[0841.钥匙和房间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0841.钥匙和房间.md) |孤岛问题 |中等|**bfs** **dfs**| |[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |栈 |简单|**栈**| |[剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**双指针**| |[面试题02.07.链表相交](https://github.com/youngyangyang04/leetcode/blob/master/problems/面试题02.07.链表相交.md) |链表 |简单|**模拟**| diff --git a/pics/486.预测赢家.png b/pics/486.预测赢家.png new file mode 100644 index 00000000..03ddfaa8 Binary files /dev/null and b/pics/486.预测赢家.png differ diff --git a/pics/486.预测赢家1.png b/pics/486.预测赢家1.png new file mode 100644 index 00000000..dbddef26 Binary files /dev/null and b/pics/486.预测赢家1.png differ diff --git a/pics/486.预测赢家2.png b/pics/486.预测赢家2.png new file mode 100644 index 00000000..7d26de9a Binary files /dev/null and b/pics/486.预测赢家2.png differ diff --git a/pics/486.预测赢家3.png b/pics/486.预测赢家3.png new file mode 100644 index 00000000..506778c4 Binary files /dev/null and b/pics/486.预测赢家3.png differ diff --git a/pics/486.预测赢家4.png b/pics/486.预测赢家4.png new file mode 100644 index 00000000..8a679b74 Binary files /dev/null and b/pics/486.预测赢家4.png differ diff --git a/pics/841.钥匙和房间.png b/pics/841.钥匙和房间.png new file mode 100644 index 00000000..3bfdeea4 Binary files /dev/null and b/pics/841.钥匙和房间.png differ diff --git a/problems/0486.预测赢家.md b/problems/0486.预测赢家.md new file mode 100644 index 00000000..0cbc0752 --- /dev/null +++ b/problems/0486.预测赢家.md @@ -0,0 +1,279 @@ +## 题目地址 + +## 思路 + +在做这道题目的时候,最直接的想法,就是计算出player1可能得到的最大分数,然后用数组总和减去player1的得分就是player2的得分,然后两者比较一下就可以了。 + +那么问题是如何计算player1可能得到的最大分数呢。 + +## 单独计算玩家得分 + +以player1选数字的过程,画图如下: + + + +可以发现是一个递归的过程。 + +按照递归三部曲来: + +1. 确定递归函数的含义,参数以及返回值。 + +定义函数getScore,就是用来获取玩家1的最大得分。 参数为start 和 end 代表获取[start, end]这个区间的最大值,当然还需要传入nums。 + +返回值就是玩家1的最大得分。 + +代码如下: + +``` +int getScore(vector& nums, int start, int end) { +``` + + +2. 确定终止条件 + +当start == end的时候,玩家A的得分就是nums[start],代码如下: +``` + if (start == end) { + return nums[start]; + } +``` + +3. 确定单层递归逻辑 + +玩家1的得分,等于集合左元素的数值+ 玩家2选择后集合的最小值(因为玩家2也是最聪明的) + + +而且剩余集合中的元素数量为2,或者大于2,的处理逻辑是不一样的! + +如图:当集合中的元素数量大于2,那么玩家1先选,玩家2依然有选择的权利。 + +所以代码如下: +``` + if ((end - start) >= 2) { + selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); + selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); + } +``` + + +如图:当集合中的元素数量等于2,那么玩家1先选,玩家2没得选。 + + +所以代码如下: +``` + if ((end - start) == 1) { + selectLeft = nums[start]; + selectRight = nums[end]; + } +``` + +单层递归逻辑代码如下: + +``` + int selectLeft, selectRight; + if ((end - start) >= 2) { + selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); + selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); + } + if ((end - start) == 1) { + selectLeft = nums[start]; + selectRight = nums[end]; + } + + return max(selectLeft, selectRight); +``` + +这些可以写出这道题目整体代码如下: + +``` +class Solution { +private: +int getScore(vector& nums, int start, int end) { + if (start == end) { + return nums[start]; + } + int selectLeft, selectRight; + if ((end - start) >= 2) { + selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); + selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); + } + if ((end - start) == 1) { + selectLeft = nums[start]; + selectRight = nums[end]; + } + + return max(selectLeft, selectRight); +} +public: + bool PredictTheWinner(vector& nums) { + int sum = 0; + for (int i : nums) { + sum += i; + } + int player1 = getScore(nums, 0, nums.size() - 1); + int player2 = sum - player1; + return player1 >= player2; + } +}; +``` + +可以有一个优化,就是把重复计算的数值提取出来,如下: +``` +class Solution { +private: +int getScore(vector& nums, int start, int end) { + int selectLeft, selectRight; + int gap = end - start; + if (gap == 0) { + return nums[start]; + } else if (gap == 1) { // 此时直接取左右的值就可以 + selectLeft = nums[start]; + selectRight = nums[end]; + } else if (gap >= 2) { // 如果gap大于2,递归计算selectLeft和selectRight + // 计算的过程为什么用min,因为要按照对手也是最聪明的来计算。 + int num = getScore(nums, start + 1, end - 1); + selectLeft = nums[start] + + min(getScore(nums, start + 2, end), num); + selectRight = nums[end] + + min(num, getScore(nums, start, end - 2)); + } + return max(selectLeft, selectRight); +} +public: + bool PredictTheWinner(vector& nums) { + int sum = 0; + for (int i : nums) { + sum += i; + } + int player1 = getScore(nums, 0, nums.size() - 1); + int player2 = sum - player1; + // 如果最终两个玩家的分数相等,那么玩家 1 仍为赢家,所以是大于等于。 + return player1 >= player2; + } +}; +``` + +## 计算两个玩家的差值 + +以上是单独计算出两个选手的得分,逻辑上直观,但是代码确实比较冗余。 + +因为就我们要求的结果其实就是两个选手的胜负,那么不用两个选手的得分,而是把问题转换为两个选手所拿元素的差值。 + +代码如下: + +``` +class Solution { +private: +int getScore(vector& nums, int start, int end) { + if (end == start) { + return nums[start]; + } + int selectLeft = nums[start] - getScore(nums, start + 1, end); + int selectRight = nums[end] - getScore(nums, start, end - 1); + return max(selectLeft, selectRight); +} +public: + bool PredictTheWinner(vector& nums) { + return getScore(nums, 0, nums.size() - 1) >=0 ; + } +}; +``` + +计算的过程有一些是冗余的,在递归的过程中,可以使用一个memory数组记录一下中间结果,代码如下: + +``` +class Solution { +private: +int getScore(vector& nums, int start, int end, int memory[21][21]) { + if (end == start) { + return nums[start]; + } + if (memory[start][end]) return memory[start][end]; + int selectLeft = nums[start] - getScore(nums, start + 1, end, memory); + int selectRight = nums[end] - getScore(nums, start, end - 1, memory); + memory[start][end] = max(selectLeft, selectRight); + return memory[start][end]; +} +public: + bool PredictTheWinner(vector& nums) { + int memory[21][21] = {0}; // 记录递归中中间结果 + return getScore(nums, 0, nums.size() - 1, memory) >= 0 ; + } +}; +``` + +此时效率已经比较高了 + + +那么在看一下动态规划的思路。 + + +## 动态规划 + + +定义一个二维数组,先明确是用来干什么的,dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j)。 + +假如玩家1先取左端 nums[i],那么玩家2能赢对方的差值是dp[i+1][j] ,如果玩家1先取取右端 nums[j],玩家2能赢对方的差值就是dp[i][j-1], + +那么 不难理解如下公式: + +`dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); ` + + +确定了状态转移公式之后,就要想想如何遍历。 + +一些同学确定的方程,却不知道该如何遍历这个遍历推算出方程的结果,我们来看一下。 + +首先要给dp[i][j]进行初始化,首先当i == j的时候,nums[i]就是dp[i][j]的值。 + +代码如下: + +``` +// 当i == j的时候,nums[i]就是dp[i][j] +for (int i = 0; i < nums.size(); i++) { + dp[i][i] = nums[i]; +} +``` + +接下来就要推导公式了,首先要知道最终求是dp[0][nums.size() - 1]是否大于等于0,也就是求dp[0][nums.size() - 1] 至关重要。 + +从下图中,可以看出在推导方程的时候一定要从右上角向下推导,而且矩阵左半部分根本不用管! + + + +按照上图中的规则,不难列出推导公式的循环方式如下: + +``` +for(int i = nums.size() - 2; i >= 0; i--) { + for (int j = i + 1; j < nums.size(); j++) { + dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); + } +} + +``` + +最后整体动态规划的代码: + +## + +``` +class Solution { +public: + bool PredictTheWinner(vector& nums) { + // dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j) + int dp[22][22] = {0}; + // 当i == j的时候,nums[i]就是dp[i][j] + for (int i = 0; i < nums.size(); i++) { + dp[i][i] = nums[i]; + } + for(int i = nums.size() - 2; i >= 0; i--) { + for (int j = i + 1; j < nums.size(); j++) { + dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); + } + } + return dp[0][nums.size() - 1] >= 0; + } +}; +``` + + diff --git a/problems/0541.反转字符串II.md b/problems/0541.反转字符串II.md index f0406b43..ca31a744 100644 --- a/problems/0541.反转字符串II.md +++ b/problems/0541.反转字符串II.md @@ -1,20 +1,43 @@ -## 题目地址 +# 题目地址 https://leetcode-cn.com/problems/reverse-string-ii/ -## 思路 +> 简单的反转还不够,我要花式反转 -先做0344.反转字符串,在做这道题目更好一些 +# 题目:541. 反转字符串II -for循环中i 每次移动 2 * k,然后判断是否需要有反转的区间 +给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 + +如果剩余字符少于 k 个,则将剩余字符全部反转。 + +如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 + +示例: + +输入: s = "abcdefg", k = 2 +输出: "bacdfeg" + +# 思路 + +这道题目其实也是模拟,实现题目中规定的反转规则就可以了。 + +一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 + +其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 + +因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 + +**所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。** 性能如下: -## C++代码 +那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 -使用C++库里的反转函数reverse +# C++代码 + +使用C++库函数reverse的版本如下: ``` class Solution { @@ -35,13 +58,14 @@ public: }; ``` -自己实现反转函数 +那么我们也可以实现自己的reverse函数,其实和题目[344. 反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)道理是一样的。 + +下面我实现的reverse函数区间是左闭右闭区间,代码如下: ``` class Solution { public: void reverse(string& s, int start, int end) { - int offset = (end - start + 1) / 2; - for (int i = start, j = end; i < start + offset; i++, j--) { + for (int i = start, j = end; i < j; i++, j--) { swap(s[i], s[j]); } } diff --git a/problems/0841.钥匙和房间.md b/problems/0841.钥匙和房间.md new file mode 100644 index 00000000..9a9e6a79 --- /dev/null +++ b/problems/0841.钥匙和房间.md @@ -0,0 +1,82 @@ +# 题目地址 +https://leetcode-cn.com/problems/keys-and-rooms/ + +## 思路 + +其实这道题的本质就是判断各个房间所连成的有向图,是否存在孤岛,如果有孤岛,说明不用访问所有的房间。 + +如图所示: + + + +示例1就可以访问所有的房间,因为通过房间里的key将房间连在了一起。 + +示例2中,就不能访问所有房间,从图中就可以看出,房间2是一个孤岛,我们从0出发,无论怎么遍历,都访问不到房间2。 + +认清本质问题之后,**就知道孤岛问题,使用 广度优先搜索(BFS) 还是 深度优先搜索(DFS) 都是可以的。** + +代码如下: + +## BFS C++代码 + +``` +class Solution { +bool bfs(const vector>& rooms) { + vector visited(rooms.size(), 0); // 标记房间是否被访问过 + visited[0] = 1; // 0 号房间开始 + queue que; + que.push(0); // 0 号房间开始 + + // 广度优先搜索的过程 + while (!que.empty()) { + int key = que.front(); que.pop(); + vector keys = rooms[key]; + for (int key : keys) { + if (!visited[key]) { + que.push(key); + visited[key] = 1; + } + } + } + // 检查房间是不是都遍历过了 + for (int i : visited) { + if (i == 0) return false; + } + return true; + +} +public: + bool canVisitAllRooms(vector>& rooms) { + return bfs(rooms); + } +}; +``` + +## DFS C++代码 + +``` +class Solution { +private: + void dfs(int key, const vector>& rooms, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = 1; + vector keys = rooms[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(key, rooms, visited); + } + } +public: + bool canVisitAllRooms(vector>& rooms) { + vector visited(rooms.size(), 0); + dfs(0, rooms, visited); + //检查是否都访问到了 + for (int i : visited) { + if (i == 0) return false; + } + return true; + } +}; +```