diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6a3e68da..00000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index b25b102d..e94b4285 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ 👉 推荐 [Gitee同步](https://gitee.com/programmercarl/leetcode-master) > 1. **介绍** :本项目是一套完整的刷题计划,旨在帮助大家少走弯路,循序渐进学算法,[关注作者](#关于作者) -> 2. **正式出版** :[《代码随想录》](https://programmercarl.com/other/publish.html) 。 -> 3. **PDF版本** :[「代码随想录」算法精讲 PDF 版本](https://programmercarl.com/other/algo_pdf.html) 。 +> 2. **正式出版** :[《代码随想录》](https://programmercarl.com/qita/publish.html) 。 +> 3. **PDF版本** :[「代码随想录」算法精讲 PDF 版本](https://programmercarl.com/qita/algo_pdf.html) 。 > 4. **算法公开课** :[《代码随想录》算法视频公开课](https://www.bilibili.com/video/BV1fA4y1o715) 。 > 5. **最强八股文** :[代码随想录知识星球精华PDF](https://www.programmercarl.com/other/kstar_baguwen.html) 。 > 6. **刷题顺序** :README已经将刷题顺序排好了,按照顺序一道一道刷就可以。 @@ -14,7 +14,7 @@

- +

@@ -74,22 +74,9 @@ * 编程语言 * [C++面试&C++学习指南知识点整理](https://github.com/youngyangyang04/TechCPP) - * [C++语言基础课](https://kamacoder.com/course.php?course_id=1) - * [Java语言基础课](https://kamacoder.com/course.php?course_id=2) + * [编程语言基础课](https://kamacoder.com/courseshop.php) * [23种设计模式](https://github.com/youngyangyang04/kama-DesignPattern) -* 项目 - * [基于跳表的轻量级KV存储引擎](https://github.com/youngyangyang04/Skiplist-CPP) - * [Nosql数据库注入攻击系统](https://github.com/youngyangyang04/NoSQLAttack) - -* 编程素养 - * [看了这么多代码,谈一谈代码风格!](./problems/前序/代码风格.md) - * [力扣上的代码想在本地编译运行?](./problems/前序/力扣上的代码想在本地编译运行?.md) - * [什么是核心代码模式,什么又是ACM模式?](./problems/前序/什么是核心代码模式,什么又是ACM模式?.md) - * [刷题要不要用库函数](./problems/前序/刷力扣用不用库函数.md) - * [ACM模式如何构造二叉树](./problems/前序/ACM模式如何构建二叉树.md) - * [解密互联网大厂研发流程](./problems/前序/互联网大厂研发流程.md) - * 工具 * [一站式vim配置](https://github.com/youngyangyang04/PowerVim) * [保姆级Git入门教程,万字详解](https://mp.weixin.qq.com/s/Q_O0ey4C9tryPZaZeJocbA) @@ -103,13 +90,12 @@ * [BAT级别技术面试流程和注意事项都在这里了](./problems/前序/BAT级别技术面试流程和注意事项都在这里了.md) * 算法性能分析 - * [关于时间复杂度,你不知道的都在这里!](./problems/前序/关于时间复杂度,你不知道的都在这里!.md) - * [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md) - * [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md) - * [本周小结!(算法性能分析系列一)](./problems/周总结/20201210复杂度分析周末总结.md) - * [关于空间复杂度,可能有几个疑问?](./problems/前序/关于空间复杂度,可能有几个疑问?.md) + * [关于时间复杂度,你不知道的都在这里!](./problems/前序/时间复杂度.md) + * [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/算法超时.md) + * [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/递归算法的时间复杂度.md) + * [关于空间复杂度,可能有几个疑问?](./problems/前序/空间复杂度.md) * [递归算法的时间与空间复杂度分析!](./problems/前序/递归算法的时间与空间复杂度分析.md) - * [刷了这么多题,你了解自己代码的内存消耗么?](./problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md) + * [刷了这么多题,你了解自己代码的内存消耗么?](./problems/前序/内存消耗.md) ## 数组 @@ -190,8 +176,9 @@ ## 二叉树 + 题目分类大纲如下: -二叉树大纲 +二叉树大纲 1. [关于二叉树,你该了解这些!](./problems/二叉树理论基础.md) 2. [二叉树:二叉树的递归遍历](./problems/二叉树的递归遍历.md) @@ -233,7 +220,7 @@ 题目分类大纲如下: -回溯算法大纲 +回溯算法大纲 1. [关于回溯算法,你该了解这些!](./problems/回溯算法理论基础.md) 2. [回溯算法:77.组合](./problems/0077.组合.md) @@ -499,7 +486,7 @@ # 贡献者 -[点此这里](https://github.com/youngyangyang04/leetcode-master/graphs/contributors)查看LeetCode-Master的所有贡献者。感谢他们补充了LeetCode-Master的其他语言版本,让更多的读者收益于此项目。 +[点此这里](https://github.com/youngyangyang04/leetcode-master/graphs/contributors)查看LeetCode-Master的所有贡献者。感谢他们补充了LeetCode-Master的其他语言版本,让更多的读者受益于此项目。 # Star 趋势 @@ -507,24 +494,13 @@ # 关于作者 -大家好,我是程序员Carl,哈工大师兄,《代码随想录》作者,先后在腾讯和百度从事后端技术研发。对算法和C++后端技术有一定的见解,利用工作之余重新刷leetcode。 +大家好,我是程序员Carl,哈工大师兄,《代码随想录》作者,先后在腾讯和百度从事后端技术底层技术研发。 -加入「代码随想录」刷题小分队(微信群),可以扫下方二维码,加代码随想录客服微信。 +# PDF下载 -如果是已工作,备注:姓名-城市-岗位-组队刷题。如果学生,备注:姓名-学校-年级-组队刷题。**备注没有自我介绍不通过哦** +添加如下企业微信,会自动发送给大家PDF版本,顺便可以选择是否加入刷题群。 +添加微信记得备注,如果是已工作,备注:姓名-城市-岗位。如果学生,备注:姓名-学校-年级。**备注没有自我介绍不通过哦**
- -# 公众号 - -更多精彩文章持续更新,微信搜索:「代码随想录」第一时间围观,关注后回复:666,可以获得我的所有算法专题原创PDF。 - -**来看看就知道了,你会发现相见恨晚!** - - - -
- - diff --git a/pics/网站星球宣传海报.jpg b/pics/网站星球宣传海报.jpg index b0325d87..547d5704 100644 Binary files a/pics/网站星球宣传海报.jpg and b/pics/网站星球宣传海报.jpg differ diff --git a/problems/0001.两数之和.md b/problems/0001.两数之和.md index 80218cb5..580fa3e2 100644 --- a/problems/0001.两数之和.md +++ b/problems/0001.两数之和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1. 两数之和 @@ -349,6 +349,7 @@ function twoSum(nums: number[], target: number): number[] { index = helperMap.get(target - nums[i]); if (index !== undefined) { resArr = [i, index]; + break; } helperMap.set(nums[i], i); } @@ -537,8 +538,8 @@ int* twoSum(int* nums, int numsSize, int target, int* returnSize){ return NULL; } ``` +

- diff --git a/problems/0005.最长回文子串.md b/problems/0005.最长回文子串.md index b13f9ac3..a13daf1e 100644 --- a/problems/0005.最长回文子串.md +++ b/problems/0005.最长回文子串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -677,8 +677,8 @@ public class Solution { } ``` +

- diff --git a/problems/0015.三数之和.md b/problems/0015.三数之和.md index bf165788..ae218385 100644 --- a/problems/0015.三数之和.md +++ b/problems/0015.三数之和.md @@ -1,13 +1,11 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

-> 用哈希表解决了[两数之和](https://programmercarl.com/0001.两数之和.html),那么三数之和呢? - # 第15题. 三数之和 [力扣题目链接](https://leetcode.cn/problems/3sum/) @@ -938,8 +936,8 @@ object Solution { } } ``` +

- diff --git a/problems/0017.电话号码的字母组合.md b/problems/0017.电话号码的字母组合.md index cbd99f8d..a06ee72d 100644 --- a/problems/0017.电话号码的字母组合.md +++ b/problems/0017.电话号码的字母组合.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 17.电话号码的字母组合 @@ -260,7 +260,7 @@ class Solution { } - //每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuilder + //每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder StringBuilder temp = new StringBuilder(); //比如digits如果为"23",num 为0,则str表示2对应的 abc @@ -274,7 +274,7 @@ class Solution { String str = numString[digits.charAt(num) - '0']; for (int i = 0; i < str.length(); i++) { temp.append(str.charAt(i)); - //c + //递归,处理下一层 backTracking(digits, numString, num + 1); //剔除末尾的继续尝试 temp.deleteCharAt(temp.length() - 1); @@ -765,8 +765,8 @@ public class Solution } ``` +

- diff --git a/problems/0018.四数之和.md b/problems/0018.四数之和.md index 17715b2e..89bc2a8b 100644 --- a/problems/0018.四数之和.md +++ b/problems/0018.四数之和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 一样的道理,能解决四数之和 @@ -697,8 +697,8 @@ def four_sum(nums, target) return result end ``` +

- diff --git a/problems/0019.删除链表的倒数第N个节点.md b/problems/0019.删除链表的倒数第N个节点.md index c7a2cfcb..f0ef2366 100644 --- a/problems/0019.删除链表的倒数第N个节点.md +++ b/problems/0019.删除链表的倒数第N个节点.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -22,10 +22,12 @@ 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] + 示例 2: 输入:head = [1], n = 1 输出:[] + 示例 3: 输入:head = [1,2], n = 1 @@ -98,27 +100,32 @@ public: ### Java: ```java -public ListNode removeNthFromEnd(ListNode head, int n){ - ListNode dummyNode = new ListNode(0); - dummyNode.next = head; +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + //新建一个虚拟头节点指向head + ListNode dummyNode = new ListNode(0); + dummyNode.next = head; + //快慢指针指向虚拟头节点 + ListNode fastIndex = dummyNode; + ListNode slowIndex = dummyNode; - ListNode fastIndex = dummyNode; - ListNode slowIndex = dummyNode; + // 只要快慢指针相差 n 个结点即可 + for (int i = 0; i <= n; i++) { + fastIndex = fastIndex.next; + } + while (fastIndex != null) { + fastIndex = fastIndex.next; + slowIndex = slowIndex.next; + } - // 只要快慢指针相差 n 个结点即可 - for (int i = 0; i < n ; i++){ - fastIndex = fastIndex.next; + // 此时 slowIndex 的位置就是待删除元素的前一个位置。 + // 具体情况可自己画一个链表长度为 3 的图来模拟代码来理解 + // 检查 slowIndex.next 是否为 null,以避免空指针异常 + if (slowIndex.next != null) { + slowIndex.next = slowIndex.next.next; + } + return dummyNode.next; } - - while (fastIndex != null){ - fastIndex = fastIndex.next; - slowIndex = slowIndex.next; - } - - //此时 slowIndex 的位置就是待删除元素的前一个位置。 - //具体情况可自己画一个链表长度为 3 的图来模拟代码来理解 - slowIndex.next = slowIndex.next.next; - return dummyNode.next; } ``` @@ -187,16 +194,18 @@ func removeNthFromEnd(head *ListNode, n int) *ListNode { * @param {number} n * @return {ListNode} */ -var removeNthFromEnd = function(head, n) { - let ret = new ListNode(0, head), - slow = fast = ret; - while(n--) fast = fast.next; - while (fast.next !== null) { - fast = fast.next; - slow = slow.next - }; - slow.next = slow.next.next; - return ret.next; +var removeNthFromEnd = function (head, n) { + // 创建哨兵节点,简化解题逻辑 + let dummyHead = new ListNode(0, head); + let fast = dummyHead; + let slow = dummyHead; + while (n--) fast = fast.next; + while (fast.next !== null) { + slow = slow.next; + fast = fast.next; + } + slow.next = slow.next.next; + return dummyHead.next; }; ``` ### TypeScript: diff --git a/problems/0020.有效的括号.md b/problems/0020.有效的括号.md index 17fbe2be..d310f415 100644 --- a/problems/0020.有效的括号.md +++ b/problems/0020.有效的括号.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0024.两两交换链表中的节点.md b/problems/0024.两两交换链表中的节点.md index b2a830a7..23dba84b 100644 --- a/problems/0024.两两交换链表中的节点.md +++ b/problems/0024.两两交换链表中的节点.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 24. 两两交换链表中的节点 @@ -81,7 +81,7 @@ public: 上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。 -心想应该没有更好的方法了吧,也就$O(n)$的时间复杂度,重复提交几次,这样了: +心想应该没有更好的方法了吧,也就 $O(n)$ 的时间复杂度,重复提交几次,这样了: ![24.两两交换链表中的节点](https://code-thinking.cdn.bcebos.com/pics/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.png) @@ -181,6 +181,23 @@ class Solution { } ``` +```java +// 将步骤 2,3 交换顺序,这样不用定义 temp 节点 +public ListNode swapPairs(ListNode head) { + ListNode dummy = new ListNode(0, head); + ListNode cur = dummy; + while (cur.next != null && cur.next.next != null) { + ListNode node1 = cur.next;// 第 1 个节点 + ListNode node2 = cur.next.next;// 第 2 个节点 + cur.next = node2; // 步骤 1 + node1.next = node2.next;// 步骤 3 + node2.next = node1;// 步骤 2 + cur = cur.next.next; + } + return dummy.next; +} +``` + ### Python: ```python @@ -285,6 +302,21 @@ var swapPairs = function (head) { }; ``` +```javascript +// 递归版本 +var swapPairs = function (head) { + if (head == null || head.next == null) { + return head; + } + + let after = head.next; + head.next = swapPairs(after.next); + after.next = head; + + return after; +}; +``` + ### TypeScript: ```typescript diff --git a/problems/0027.移除元素.md b/problems/0027.移除元素.md index 2a2005d7..480800e9 100644 --- a/problems/0027.移除元素.md +++ b/problems/0027.移除元素.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 27. 移除元素 @@ -119,35 +119,6 @@ public: * 时间复杂度:O(n) * 空间复杂度:O(1) -```CPP -/** -* 相向双指针方法,基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素 -* 时间复杂度:O(n) -* 空间复杂度:O(1) -*/ -class Solution { -public: - int removeElement(vector& nums, int val) { - int leftIndex = 0; - int rightIndex = nums.size() - 1; - while (leftIndex <= rightIndex) { - // 找左边等于val的元素 - while (leftIndex <= rightIndex && nums[leftIndex] != val){ - ++leftIndex; - } - // 找右边不等于val的元素 - while (leftIndex <= rightIndex && nums[rightIndex] == val) { - -- rightIndex; - } - // 将右边不等于val的元素覆盖左边等于val的元素 - if (leftIndex < rightIndex) { - nums[leftIndex++] = nums[rightIndex--]; - } - } - return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素 - } -}; -``` ## 相关题目推荐 @@ -276,6 +247,24 @@ class Solution: ### Go: +```go +// 暴力法 +// 时间复杂度 O(n^2) +// 空间复杂度 O(1) +func removeElement(nums []int, val int) int { + size := len(nums) + for i := 0; i < size; i ++ { + if nums[i] == val { + for j := i + 1; j < size; j ++ { + nums[j - 1] = nums[j] + } + i -- + size -- + } + } + return size +} +``` ```go // 快慢指针法 // 时间复杂度 O(n) @@ -318,7 +307,6 @@ func removeElement(nums []int, val int) int { right-- } } - fmt.Println(nums) return left } ``` @@ -488,6 +476,32 @@ public class Solution { } ``` +###Dart: +```dart +int removeElement(List nums, int val) { + //相向双指针法 + var left = 0; + var right = nums.length - 1; + while (left <= right) { + //寻找左侧的val,将其被右侧非val覆盖 + if (nums[left] == val) { + while (nums[right] == val&&left<=right) { + right--; + if (right < 0) { + return 0; + } + } + nums[left] = nums[right--]; + } else { + left++; + } + } + //覆盖后可以将0至left部分视为所需部分 + return left; +} + +``` +

diff --git a/problems/0028.实现strStr.md b/problems/0028.实现strStr.md index 8d0cc525..e0cb123e 100644 --- a/problems/0028.实现strStr.md +++ b/problems/0028.实现strStr.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 @@ -564,6 +564,38 @@ public: ## 其他语言版本 ### Java: +```Java +class Solution { + /** + 牺牲空间,换取最直白的暴力法 + 时间复杂度 O(n * m) + 空间 O(n + m) + */ + public int strStr(String haystack, String needle) { + // 获取 haystack 和 needle 的长度 + int n = haystack.length(), m = needle.length(); + // 将字符串转换为字符数组,方便索引操作 + char[] s = haystack.toCharArray(), p = needle.toCharArray(); + + // 遍历 haystack 字符串 + for (int i = 0; i < n - m + 1; i++) { + // 初始化匹配的指针 + int a = i, b = 0; + // 循环检查 needle 是否在当前位置开始匹配 + while (b < m && s[a] == p[b]) { + // 如果当前字符匹配,则移动指针 + a++; + b++; + } + // 如果 b 等于 m,说明 needle 已经完全匹配,返回当前位置 i + if (b == m) return i; + } + + // 如果遍历完毕仍未找到匹配的子串,则返回 -1 + return -1; + } +} +``` ```Java class Solution { diff --git a/problems/0031.下一个排列.md b/problems/0031.下一个排列.md index 3cfb673a..48af8d0d 100644 --- a/problems/0031.下一个排列.md +++ b/problems/0031.下一个排列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -268,8 +268,8 @@ var nextPermutation = function(nums) { ``` +

- diff --git a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md index 22936fef..16adcdf1 100644 --- a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md +++ b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 34. 在排序数组中查找元素的第一个和最后一个位置 @@ -233,7 +233,7 @@ class Solution { if (index == -1) { // nums 中不存在 target,直接返回 {-1, -1} return new int[] {-1, -1}; // 匿名数组 } - // nums 中存在 targe,则左右滑动指针,来找到符合题意的区间 + // nums 中存在 target,则左右滑动指针,来找到符合题意的区间 int left = index; int right = index; // 向左滑动,找左边界 @@ -450,7 +450,7 @@ class Solution: return -1 index = binarySearch(nums, target) if index == -1:return [-1, -1] # nums 中不存在 target,直接返回 {-1, -1} - # nums 中存在 targe,则左右滑动指针,来找到符合题意的区间 + # nums 中存在 target,则左右滑动指针,来找到符合题意的区间 left, right = index, index # 向左滑动,找左边界 while left -1 >=0 and nums[left - 1] == target: left -=1 @@ -854,8 +854,8 @@ int* searchRange(int* nums, int numsSize, int target, int* returnSize){ } ``` +

- diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md index 80b7e40e..b5be9a5f 100644 --- a/problems/0035.搜索插入位置.md +++ b/problems/0035.搜索插入位置.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -313,18 +313,18 @@ func searchInsert(nums []int, target int) int { ```rust impl Solution { - pub fn search_insert(nums: Vec, target: i32) -> i32 { - let mut left = 0; - let mut right = nums.len(); - while left < right { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + use std::cmp::Ordering::{Equal, Greater, Less}; + let (mut left, mut right) = (0, nums.len() as i32 - 1); + while left <= right { let mid = (left + right) / 2; - match nums[mid].cmp(&target) { - Ordering::Less => left = mid + 1, - Ordering::Equal => return ((left + right) / 2) as i32, - Ordering::Greater => right = mid, + match nums[mid as usize].cmp(&target) { + Less => left = mid + 1, + Equal => return mid, + Greater => right = mid - 1, } } - ((left + right) / 2) as i32 + right + 1 } } ``` @@ -332,6 +332,7 @@ impl Solution { ### Python ```python +# 第一种二分法: [left, right]左闭右闭区间 class Solution: def searchInsert(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 @@ -348,6 +349,26 @@ class Solution: return right + 1 ``` +```python +# 第二种二分法: [left, right)左闭右开区间 +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) + + while (left < right): + middle = (left + right) // 2 + + if nums[middle] > target: + right = middle + elif nums[middle] < target: + left = middle + 1 + else: + return middle + + return right +``` + ### JavaScript ```js @@ -527,8 +548,8 @@ int searchInsert(int* nums, int numsSize, int target){ } ``` +

- diff --git a/problems/0037.解数独.md b/problems/0037.解数独.md index d96e59df..70d52e9e 100644 --- a/problems/0037.解数独.md +++ b/problems/0037.解数独.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ > 如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) @@ -87,7 +87,7 @@ bool backtracking(vector>& board) ![37.解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111720451790-20230310131822254.png) -在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归) +在树形图中可以看出我们需要的是一个二维的递归 (一行一列) **一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!** @@ -224,7 +224,7 @@ public: ### Java - +解法一: ```java class Solution { public void solveSudoku(char[][] board) { @@ -291,7 +291,73 @@ class Solution { } } ``` +解法二(bitmap标记) +``` +class Solution{ + int[] rowBit = new int[9]; + int[] colBit = new int[9]; + int[] square9Bit = new int[9]; + public void solveSudoku(char[][] board) { + // 1 10 11 + for (int y = 0; y < board.length; y++) { + for (int x = 0; x < board[y].length; x++) { + int numBit = 1 << (board[y][x] - '1'); + rowBit[y] ^= numBit; + colBit[x] ^= numBit; + square9Bit[(y / 3) * 3 + x / 3] ^= numBit; + } + } + backtrack(board, 0); + } + + public boolean backtrack(char[][] board, int n) { + if (n >= 81) { + return true; + } + + // 快速算出行列编号 n/9 n%9 + int row = n / 9; + int col = n % 9; + + if (board[row][col] != '.') { + return backtrack(board, n + 1); + } + + for (char c = '1'; c <= '9'; c++) { + int numBit = 1 << (c - '1'); + if (!isValid(numBit, row, col)) continue; + { + board[row][col] = c; // 当前的数字放入到数组之中, + rowBit[row] ^= numBit; // 第一行rowBit[0],第一个元素eg: 1 , 0^1=1,第一个元素:4, 100^1=101,... + colBit[col] ^= numBit; + square9Bit[(row / 3) * 3 + col / 3] ^= numBit; + } + if (backtrack(board, n + 1)) return true; + { + board[row][col] = '.'; // 不满足条件,回退成'.' + rowBit[row] &= ~numBit; // 第一行rowBit[0],第一个元素eg: 1 , 101&=~1==>101&111111110==>100 + colBit[col] &= ~numBit; + square9Bit[(row / 3) * 3 + col / 3] &= ~numBit; + } + } + return false; + } + + + boolean isValid(int numBit, int row, int col) { + // 左右 + if ((rowBit[row] & numBit) > 0) return false; + // 上下 + if ((colBit[col] & numBit) > 0) return false; + // 9宫格: 快速算出第n个九宫格,编号[0,8] , 编号=(row / 3) * 3 + col / 3 + if ((square9Bit[(row / 3) * 3 + col / 3] & numBit) > 0) return false; + return true; + } + +} + +``` ### Python ```python @@ -810,8 +876,8 @@ public class Solution } ``` +

- diff --git a/problems/0039.组合总和.md b/problems/0039.组合总和.md index 81558cc1..92c68562 100644 --- a/problems/0039.组合总和.md +++ b/problems/0039.组合总和.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -311,7 +311,7 @@ class Solution: for i in range(startIndex, len(candidates)): if total + candidates[i] > target: - continue + break total += candidates[i] path.append(candidates[i]) self.backtracking(candidates, target, total, i, path, result) @@ -660,8 +660,8 @@ public class Solution ``` +

- diff --git a/problems/0040.组合总和II.md b/problems/0040.组合总和II.md index 994b04b8..22cf726d 100644 --- a/problems/0040.组合总和II.md +++ b/problems/0040.组合总和II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 这篇可以说是全网把组合问题如何去重,讲的最清晰的了! @@ -806,8 +806,8 @@ public class Solution } } ``` +

- diff --git a/problems/0042.接雨水.md b/problems/0042.接雨水.md index 73d787b1..0484f830 100644 --- a/problems/0042.接雨水.md +++ b/problems/0042.接雨水.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -440,6 +440,33 @@ class Solution { } ``` +双指针优化 +```java +class Solution { + public int trap(int[] height) { + if (height.length <= 2) { + return 0; + } + // 从两边向中间寻找最值 + int maxLeft = height[0], maxRight = height[height.length - 1]; + int l = 1, r = height.length - 2; + int res = 0; + while (l <= r) { + // 不确定上一轮是左边移动还是右边移动,所以两边都需更新最值 + maxLeft = Math.max(maxLeft, height[l]); + maxRight = Math.max(maxRight, height[r]); + // 最值较小的一边所能装的水量已定,所以移动较小的一边。 + if (maxLeft < maxRight) { + res += maxLeft - height[l ++]; + } else { + res += maxRight - height[r --]; + } + } + return res; + } +} +``` + 单调栈法 ```java diff --git a/problems/0045.跳跃游戏II.md b/problems/0045.跳跃游戏II.md index c6433ea8..3eeec268 100644 --- a/problems/0045.跳跃游戏II.md +++ b/problems/0045.跳跃游戏II.md @@ -1,10 +1,10 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

-> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心里准备! +> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心理准备! # 45.跳跃游戏 II @@ -541,8 +541,8 @@ public class Solution } ``` +

- diff --git a/problems/0046.全排列.md b/problems/0046.全排列.md index 15e6ae16..638a2a7c 100644 --- a/problems/0046.全排列.md +++ b/problems/0046.全排列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 46.全排列 @@ -518,8 +518,8 @@ public class Solution } ``` +

- diff --git a/problems/0047.全排列II.md b/problems/0047.全排列II.md index 7f2c3638..56006a77 100644 --- a/problems/0047.全排列II.md +++ b/problems/0047.全排列II.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -554,8 +554,8 @@ public class Solution } ``` +

- diff --git a/problems/0051.N皇后.md b/problems/0051.N皇后.md index 1e108540..6ced679c 100644 --- a/problems/0051.N皇后.md +++ b/problems/0051.N皇后.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 51. N皇后 @@ -920,8 +920,8 @@ public class Solution } ``` +

- diff --git a/problems/0052.N皇后II.md b/problems/0052.N皇后II.md index 29c2b588..271484a4 100644 --- a/problems/0052.N皇后II.md +++ b/problems/0052.N皇后II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -306,8 +306,8 @@ class Solution { } } ``` +

- diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md index 74ff2ca4..551c39bf 100644 --- a/problems/0053.最大子序和.md +++ b/problems/0053.最大子序和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 53. 最大子序和 diff --git a/problems/0053.最大子序和(动态规划).md b/problems/0053.最大子序和(动态规划).md index 70ad7a84..38a3a118 100644 --- a/problems/0053.最大子序和(动态规划).md +++ b/problems/0053.最大子序和(动态规划).md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 53. 最大子序和 @@ -243,8 +243,8 @@ function maxSubArray(nums: number[]): number { ``` +

- diff --git a/problems/0054.螺旋矩阵.md b/problems/0054.螺旋矩阵.md index 85e6a936..4d54ccd6 100644 --- a/problems/0054.螺旋矩阵.md +++ b/problems/0054.螺旋矩阵.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0055.跳跃游戏.md b/problems/0055.跳跃游戏.md index 086fd64f..01fd9513 100644 --- a/problems/0055.跳跃游戏.md +++ b/problems/0055.跳跃游戏.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 55. 跳跃游戏 diff --git a/problems/0056.合并区间.md b/problems/0056.合并区间.md index f9d6f654..134a9028 100644 --- a/problems/0056.合并区间.md +++ b/problems/0056.合并区间.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 56. 合并区间 diff --git a/problems/0059.螺旋矩阵II.md b/problems/0059.螺旋矩阵II.md index 7f73bc48..c59ec033 100644 --- a/problems/0059.螺旋矩阵II.md +++ b/problems/0059.螺旋矩阵II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0062.不同路径.md b/problems/0062.不同路径.md index 207a66ee..b451704b 100644 --- a/problems/0062.不同路径.md +++ b/problems/0062.不同路径.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 62.不同路径 @@ -285,6 +285,24 @@ public: } ``` +状态压缩 +```java +class Solution { + public int uniquePaths(int m, int n) { + // 在二维dp数组中,当前值的计算只依赖正上方和正左方,因此可以压缩成一维数组。 + int[] dp = new int[n]; + // 初始化,第一行只能从正左方跳过来,所以只有一条路径。 + Arrays.fill(dp, 1); + for (int i = 1; i < m; i ++) { + // 第一列也只有一条路,不用迭代,所以从第二列开始 + for (int j = 1; j < n; j ++) { + dp[j] += dp[j - 1]; // dp[j] = dp[j] (正上方)+ dp[j - 1] (正左方) + } + } + return dp[n - 1]; + } +} +``` ### Python 递归 @@ -576,8 +594,8 @@ public class Solution +

- diff --git a/problems/0063.不同路径II.md b/problems/0063.不同路径II.md index 8c208ea8..6819c19f 100644 --- a/problems/0063.不同路径II.md +++ b/problems/0063.不同路径II.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 63. 不同路径 II @@ -145,7 +145,7 @@ public: int uniquePathsWithObstacles(vector>& obstacleGrid) { int m = obstacleGrid.size(); int n = obstacleGrid[0].size(); - if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0 + if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0 return 0; vector> dp(m, vector(n, 0)); for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1; @@ -759,8 +759,8 @@ public class Solution } ``` +

- diff --git a/problems/0070.爬楼梯.md b/problems/0070.爬楼梯.md index 67bbdd7b..1d0b192f 100644 --- a/problems/0070.爬楼梯.md +++ b/problems/0070.爬楼梯.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 70. 爬楼梯 @@ -519,8 +519,8 @@ impl Solution { } ``` +

- diff --git a/problems/0070.爬楼梯完全背包版本.md b/problems/0070.爬楼梯完全背包版本.md index 622b1117..07e0261e 100644 --- a/problems/0070.爬楼梯完全背包版本.md +++ b/problems/0070.爬楼梯完全背包版本.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 70. 爬楼梯(进阶版) @@ -211,10 +211,33 @@ func main() { ``` ### JavaScript: - +```javaScript +var climbStairs = function (n) { + let dp = new Array(n + 1).fill(0); + dp[0] = 1; + // 排列题,注意循环顺序,背包在外物品在内 + for (let j = 1; j <= n; j++) {//遍历背包 + for (let i = 1; i <= 2; i++) {//遍历物品 + if (j - i >= 0) dp[j] = dp[j] + dp[j - i]; + } + } + return dp[n]; +} +``` ### TypeScript: - +```typescript +var climbStairs = function (n: number): number { + let dp: number[] = new Array(n + 1).fill(0); + dp[0] = 1; + for (let j = 1; j <= n; j++) {//遍历背包 + for (let i = 1; i <= 2; i++) {//遍历物品 + if (j - i >= 0) dp[j] = dp[j] + dp[j - i]; + } + } + return dp[n]; +} +``` ### Rust: diff --git a/problems/0072.编辑距离.md b/problems/0072.编辑距离.md index 777b851c..b8de8bdc 100644 --- a/problems/0072.编辑距离.md +++ b/problems/0072.编辑距离.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 72. 编辑距离 diff --git a/problems/0077.组合.md b/problems/0077.组合.md index 103fb627..3a271ff8 100644 --- a/problems/0077.组合.md +++ b/problems/0077.组合.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 第77题. 组合 @@ -469,28 +469,58 @@ func dfs(n int, k int, start int) { ``` ### Javascript +未剪枝: + +```js +var combine = function (n, k) { + // 回溯法 + let result = [], + path = []; + let backtracking = (_n, _k, startIndex) => { + // 终止条件 + if (path.length === _k) { + result.push(path.slice()); + return; + } + // 循环本层集合元素 + for (let i = startIndex; i <= _n; i++) { + path.push(i); + // 递归 + backtracking(_n, _k, i + 1); + // 回溯操作 + path.pop(); + } + }; + backtracking(n, k, 1); + return result; +}; +``` 剪枝: ```javascript -let result = [] -let path = [] -var combine = function(n, k) { - result = [] - combineHelper(n, k, 1) - return result +var combine = function (n, k) { + // 回溯法 + let result = [], + path = []; + let backtracking = (_n, _k, startIndex) => { + // 终止条件 + if (path.length === _k) { + result.push(path.slice()); + return; + } + // 循环本层集合元素 + for (let i = startIndex; i <= _n - (_k - path.length) + 1; i++) { + path.push(i); + // 递归 + backtracking(_n, _k, i + 1); + // 回溯操作 + path.pop(); + } + }; + backtracking(n, k, 1); + return result; }; -const combineHelper = (n, k, startIndex) => { - if (path.length === k) { - result.push([...path]) - return - } - for (let i = startIndex; i <= n - (k - path.length) + 1; ++i) { - path.push(i) - combineHelper(n, k, i + 1) - path.pop() - } -} ``` ### TypeScript @@ -845,8 +875,8 @@ public class Solution } } ``` +

- diff --git a/problems/0077.组合优化.md b/problems/0077.组合优化.md index 9577d65f..0fa568af 100644 --- a/problems/0077.组合优化.md +++ b/problems/0077.组合优化.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -411,8 +411,8 @@ object Solution { } ``` +

- diff --git a/problems/0078.子集.md b/problems/0078.子集.md index 06547e3d..1415f2d2 100644 --- a/problems/0078.子集.md +++ b/problems/0078.子集.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 78.子集 @@ -466,8 +466,8 @@ public class Solution { ``` +

- diff --git a/problems/0084.柱状图中最大的矩形.md b/problems/0084.柱状图中最大的矩形.md index b836705a..1c4d7f59 100644 --- a/problems/0084.柱状图中最大的矩形.md +++ b/problems/0084.柱状图中最大的矩形.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 84.柱状图中最大的矩形 @@ -741,8 +741,8 @@ impl Solution { } ``` +

- diff --git a/problems/0090.子集II.md b/problems/0090.子集II.md index 6d618978..03bbd1dc 100644 --- a/problems/0090.子集II.md +++ b/problems/0090.子集II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 90.子集II @@ -659,8 +659,8 @@ public class Solution } ``` +

- diff --git a/problems/0093.复原IP地址.md b/problems/0093.复原IP地址.md index c662957a..73d5e3c3 100644 --- a/problems/0093.复原IP地址.md +++ b/problems/0093.复原IP地址.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -848,8 +848,8 @@ public class Solution ``` +

- diff --git a/problems/0096.不同的二叉搜索树.md b/problems/0096.不同的二叉搜索树.md index 15b99083..e0e77310 100644 --- a/problems/0096.不同的二叉搜索树.md +++ b/problems/0096.不同的二叉搜索树.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 96.不同的二叉搜索树 diff --git a/problems/0098.验证二叉搜索树.md b/problems/0098.验证二叉搜索树.md index 88e16282..023eeea5 100644 --- a/problems/0098.验证二叉搜索树.md +++ b/problems/0098.验证二叉搜索树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 98.验证二叉搜索树 @@ -806,8 +806,8 @@ public bool IsValidBST(TreeNode root) } ``` +

- diff --git a/problems/0100.相同的树.md b/problems/0100.相同的树.md index 56a6c884..7268b9f0 100644 --- a/problems/0100.相同的树.md +++ b/problems/0100.相同的树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -339,8 +339,8 @@ function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { +

- diff --git a/problems/0101.对称二叉树.md b/problems/0101.对称二叉树.md index 8442f0ab..063b5429 100644 --- a/problems/0101.对称二叉树.md +++ b/problems/0101.对称二叉树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 101. 对称二叉树 @@ -945,8 +945,8 @@ public bool IsSymmetric(TreeNode root) } ``` +

- diff --git a/problems/0102.二叉树的层序遍历.md b/problems/0102.二叉树的层序遍历.md index 4411b560..421c5dd9 100644 --- a/problems/0102.二叉树的层序遍历.md +++ b/problems/0102.二叉树的层序遍历.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 二叉树层序遍历登场! @@ -129,7 +129,7 @@ class Solution { return resList; } - //DFS--递归方式 + //BFS--递归方式 public void checkFun01(TreeNode node, Integer deep) { if (node == null) return; deep++; @@ -692,27 +692,29 @@ func levelOrderBottom(root *TreeNode) [][]int { #### Javascript: ```javascript -var levelOrderBottom = function(root) { - let res = [], queue = []; - queue.push(root); - while(queue.length && root!==null) { - // 存放当前层级节点数组 - let curLevel = []; - // 计算当前层级节点数量 - let length = queue.length; - while(length--) { - let node = queue.shift(); - // 把当前层节点存入curLevel数组 - curLevel.push(node.val); - // 把下一层级的左右节点存入queue队列 - node.left && queue.push(node.left); - node.right && queue.push(node.right); - } - // 从数组前头插入值,避免最后反转数组,减少运算时间 - res.unshift(curLevel); +var levelOrderBottom = function (root) { + let res = [], + queue = []; + queue.push(root); + while (queue.length && root !== null) { + // 存放当前层级节点数组 + let curLevel = []; + // 计算当前层级节点数量 + let length = queue.length; + while (length--) { + let node = queue.shift(); + // 把当前层节点存入curLevel数组 + curLevel.push(node.val); + // 把下一层级的左右节点存入queue队列 + node.left && queue.push(node.left); + node.right && queue.push(node.right); } - return res; + // 从数组前头插入值,避免最后反转数组,减少运算时间 + res.unshift(curLevel); + } + return res; }; + ``` #### TypeScript: @@ -1140,7 +1142,7 @@ impl Solution { ### 思路 -本题就是层序遍历的时候把一层求个总和在取一个均值。 +本题就是层序遍历的时候把一层求个总和再取一个均值。 C++代码: @@ -1295,26 +1297,26 @@ func averageOfLevels(root *TreeNode) []float64 { ```javascript var averageOfLevels = function(root) { - //层级平均值 - let res = [], queue = []; - queue.push(root); - - while(queue.length && root!==null) { - //每一层节点个数 - let length = queue.length; - //sum记录每一层的和 - let sum = 0; - for(let i=0; i < length; i++) { - let node = queue.shift(); - sum += node.val; - node.left && queue.push(node.left); - node.right && queue.push(node.right); - } - //每一层的平均值存入数组res - res.push(sum/length); + let res = [], + queue = []; + queue.push(root); + while (queue.length) { + // 每一层节点个数; + let lengthLevel = queue.length, + len = queue.length, + // sum记录每一层的和; + sum = 0; + while (lengthLevel--) { + const node = queue.shift(); + sum += node.val; + // 队列存放下一层节点 + node.left && queue.push(node.left); + node.right && queue.push(node.right); } - - return res; + // 求平均值 + res.push(sum / len); + } + return res; }; ``` @@ -1925,26 +1927,28 @@ func max(x, y int) int { #### Javascript: ```javascript -var largestValues = function(root) { - //使用层序遍历 - let res = [], queue = []; - queue.push(root); - - while(root !== null && queue.length) { - //设置max初始值就是队列的第一个元素 - let max = queue[0].val; - let length = queue.length; - while(length--) { - let node = queue.shift(); - max = max > node.val ? max : node.val; - node.left && queue.push(node.left); - node.right && queue.push(node.right); - } - //把每一层的最大值放到res数组 - res.push(max); - } - +var largestValues = function (root) { + let res = [], + queue = []; + queue.push(root); + if (root === null) { return res; + } + while (queue.length) { + let lengthLevel = queue.length, + // 初始值设为负无穷大 + max = -Infinity; + while (lengthLevel--) { + const node = queue.shift(); + // 在当前层中找到最大值 + max = Math.max(max, node.val); + // 找到下一层的节点 + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + res.push(max); + } + return res; }; ``` @@ -2805,21 +2809,23 @@ func maxDepth(root *TreeNode) int { * @param {TreeNode} root * @return {number} */ -var maxDepth = function(root) { - // 最大的深度就是二叉树的层数 - if (root === null) return 0; - let queue = [root]; - let height = 0; - while (queue.length) { - let n = queue.length; - height++; - for (let i=0; i - diff --git a/problems/0104.二叉树的最大深度.md b/problems/0104.二叉树的最大深度.md index 1f55f197..0f93cb0f 100644 --- a/problems/0104.二叉树的最大深度.md +++ b/problems/0104.二叉树的最大深度.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -77,7 +77,7 @@ return depth; 所以整体c++代码如下: ```CPP -class solution { +class Solution { public: int getdepth(TreeNode* node) { if (node == NULL) return 0; @@ -94,7 +94,7 @@ public: 代码精简之后c++代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(TreeNode* root) { if (root == null) return 0; @@ -110,7 +110,7 @@ public: 本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**) ```CPP -class solution { +class Solution { public: int result; void getdepth(TreeNode* node, int depth) { @@ -144,7 +144,7 @@ public: 注意以上代码是为了把细节体现出来,简化一下代码如下: ```CPP -class solution { +class Solution { public: int result; void getdepth(TreeNode* node, int depth) { @@ -183,7 +183,7 @@ public: c++代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(TreeNode* root) { if (root == NULL) return 0; @@ -232,7 +232,7 @@ public: c++代码: ```CPP -class solution { +class Solution { public: int maxDepth(Node* root) { if (root == 0) return 0; @@ -249,7 +249,7 @@ public: 依然是层序遍历,代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(Node* root) { queue que; @@ -278,7 +278,7 @@ public: 104.二叉树的最大深度 ```java -class solution { +class Solution { /** * 递归法 */ @@ -319,7 +319,7 @@ class Solution { ``` ```java -class solution { +class Solution { /** * 迭代法,使用层序遍历 */ @@ -369,7 +369,7 @@ class Solution { ``` ```java -class solution { +class Solution { /** * 迭代法,使用层序遍历 */ @@ -402,7 +402,7 @@ class solution { 递归法: ```python -class solution: +class Solution: def maxdepth(self, root: treenode) -> int: return self.getdepth(root) @@ -417,7 +417,7 @@ class solution: 递归法:精简代码 ```python -class solution: +class Solution: def maxdepth(self, root: treenode) -> int: if not root: return 0 @@ -1159,8 +1159,8 @@ public int MaxDepth(TreeNode root) ``` +

- diff --git a/problems/0106.从中序与后序遍历序列构造二叉树.md b/problems/0106.从中序与后序遍历序列构造二叉树.md index 0e0ab1d7..3518343f 100644 --- a/problems/0106.从中序与后序遍历序列构造二叉树.md +++ b/problems/0106.从中序与后序遍历序列构造二叉树.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -1242,8 +1242,8 @@ public TreeNode BuildTree(int[] inorder, int[] postorder) } ``` +

- diff --git a/problems/0108.将有序数组转换为二叉搜索树.md b/problems/0108.将有序数组转换为二叉搜索树.md index 9fa684cf..4804ccd3 100644 --- a/problems/0108.将有序数组转换为二叉搜索树.md +++ b/problems/0108.将有序数组转换为二叉搜索树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 构造二叉搜索树,一不小心就平衡了 @@ -334,6 +334,18 @@ class Solution: return root ``` +递归 精简(自身调用) +```python +class Solution: + def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]: + if not nums: + return + mid = len(nums) // 2 + root = TreeNode(nums[mid]) + root.left = self.sortedArrayToBST(nums[:mid]) + root.right = self.sortedArrayToBST(nums[mid + 1 :]) + return root +``` 迭代法 ```python @@ -549,8 +561,8 @@ public TreeNode Traversal(int[] nums, int left, int right) ``` +

- diff --git a/problems/0110.平衡二叉树.md b/problems/0110.平衡二叉树.md index 40fdcd14..dd05bdd6 100644 --- a/problems/0110.平衡二叉树.md +++ b/problems/0110.平衡二叉树.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -934,8 +934,8 @@ public int GetHeight(TreeNode root) } ``` +

- diff --git a/problems/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md index 6d1632d5..cd7096ac 100644 --- a/problems/0111.二叉树的最小深度.md +++ b/problems/0111.二叉树的最小深度.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 和求最大深度一个套路? @@ -64,7 +64,7 @@ 代码如下: -``` +```CPP int getDepth(TreeNode* node) ``` @@ -74,14 +74,14 @@ int getDepth(TreeNode* node) 代码如下: -``` +```CPP if (node == NULL) return 0; ``` 3. 确定单层递归的逻辑 这块和求最大深度可就不一样了,一些同学可能会写如下代码: -``` +```CPP int leftDepth = getDepth(node->left); int rightDepth = getDepth(node->right); int result = 1 + min(leftDepth, rightDepth); diff --git a/problems/0112.路径总和.md b/problems/0112.路径总和.md index d45be3bd..2beb8a7f 100644 --- a/problems/0112.路径总和.md +++ b/problems/0112.路径总和.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 112. 路径总和 @@ -1579,8 +1579,8 @@ public class Solution { // @lc code=end ``` +

- diff --git a/problems/0115.不同的子序列.md b/problems/0115.不同的子序列.md index 96ab2583..8682b88d 100644 --- a/problems/0115.不同的子序列.md +++ b/problems/0115.不同的子序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 115.不同的子序列 @@ -375,8 +375,8 @@ impl Solution { ``` +

- diff --git a/problems/0116.填充每个节点的下一个右侧节点指针.md b/problems/0116.填充每个节点的下一个右侧节点指针.md index 60ea9210..ca36ac6f 100644 --- a/problems/0116.填充每个节点的下一个右侧节点指针.md +++ b/problems/0116.填充每个节点的下一个右侧节点指针.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 116. 填充每个节点的下一个右侧节点指针 @@ -438,8 +438,8 @@ public class Solution ``` +

- diff --git a/problems/0121.买卖股票的最佳时机.md b/problems/0121.买卖股票的最佳时机.md index fb548cbc..e9aea0e6 100644 --- a/problems/0121.买卖股票的最佳时机.md +++ b/problems/0121.买卖股票的最佳时机.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 121. 买卖股票的最佳时机 @@ -287,9 +287,6 @@ class Solution { return dp[1]; } } -``` -```Java - ``` ### Python: diff --git a/problems/0122.买卖股票的最佳时机II.md b/problems/0122.买卖股票的最佳时机II.md index 69706e36..e255723d 100644 --- a/problems/0122.买卖股票的最佳时机II.md +++ b/problems/0122.买卖股票的最佳时机II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 122.买卖股票的最佳时机 II @@ -422,8 +422,8 @@ public class Solution } ``` +

- diff --git a/problems/0122.买卖股票的最佳时机II(动态规划).md b/problems/0122.买卖股票的最佳时机II(动态规划).md index 24c7f168..f0dff505 100644 --- a/problems/0122.买卖股票的最佳时机II(动态规划).md +++ b/problems/0122.买卖股票的最佳时机II(动态规划).md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 122.买卖股票的最佳时机II @@ -455,8 +455,8 @@ impl Solution { } ``` +

- diff --git a/problems/0123.买卖股票的最佳时机III.md b/problems/0123.买卖股票的最佳时机III.md index 18f19c51..d06b4f80 100644 --- a/problems/0123.买卖股票的最佳时机III.md +++ b/problems/0123.买卖股票的最佳时机III.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 123.买卖股票的最佳时机III diff --git a/problems/0127.单词接龙.md b/problems/0127.单词接龙.md index 6f893310..00d7d4cf 100644 --- a/problems/0127.单词接龙.md +++ b/problems/0127.单词接龙.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 127. 单词接龙 diff --git a/problems/0129.求根到叶子节点数字之和.md b/problems/0129.求根到叶子节点数字之和.md index ebb36071..90dfd061 100644 --- a/problems/0129.求根到叶子节点数字之和.md +++ b/problems/0129.求根到叶子节点数字之和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -382,8 +382,8 @@ int sumNumbers(struct TreeNode* root){ } ``` +

- diff --git a/problems/0130.被围绕的区域.md b/problems/0130.被围绕的区域.md index 1ddaaa7f..8ef8d5b2 100644 --- a/problems/0130.被围绕的区域.md +++ b/problems/0130.被围绕的区域.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 130. 被围绕的区域 @@ -792,8 +792,8 @@ impl Solution { } ``` +

- diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md index ca342d4b..822d4399 100644 --- a/problems/0131.分割回文串.md +++ b/problems/0131.分割回文串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 切割问题其实是一种组合问题! @@ -952,8 +952,8 @@ public class Solution ``` +

- diff --git a/problems/0132.分割回文串II.md b/problems/0132.分割回文串II.md index eb91a189..85e047f2 100644 --- a/problems/0132.分割回文串II.md +++ b/problems/0132.分割回文串II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -372,8 +372,8 @@ var minCut = function(s) { ``` +

- diff --git a/problems/0134.加油站.md b/problems/0134.加油站.md index c093023d..c88b43b1 100644 --- a/problems/0134.加油站.md +++ b/problems/0134.加油站.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 134. 加油站 @@ -654,8 +654,8 @@ public class Solution } ``` +

- diff --git a/problems/0135.分发糖果.md b/problems/0135.分发糖果.md index 210f4995..6805857e 100644 --- a/problems/0135.分发糖果.md +++ b/problems/0135.分发糖果.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 135. 分发糖果 @@ -401,8 +401,8 @@ public class Solution ``` +

- diff --git a/problems/0139.单词拆分.md b/problems/0139.单词拆分.md index a3d59ec7..816892d5 100644 --- a/problems/0139.单词拆分.md +++ b/problems/0139.单词拆分.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -394,7 +394,28 @@ class Solution: dp[j] = dp[j] or (dp[j - len(word)] and word == s[j - len(word):j]) return dp[len(s)] ``` +DP(剪枝) +```python +class Solution(object): + def wordBreak(self, s, wordDict): + + # 先对单词按长度排序 + wordDict.sort(key=lambda x: len(x)) + n = len(s) + dp = [False] * (n + 1) + dp[0] = True + # 遍历背包 + for i in range(1, n + 1): + # 遍历单词 + for word in wordDict: + # 简单的 “剪枝” + if len(word) > i: + break + dp[i] = dp[i] or (dp[i - len(word)] and s[i - len(word): i] == word) + return dp[-1] + +``` ### Go: diff --git a/problems/0141.环形链表.md b/problems/0141.环形链表.md index b1f42ba9..ac656576 100644 --- a/problems/0141.环形链表.md +++ b/problems/0141.环形链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 141. 环形链表 @@ -159,8 +159,8 @@ function hasCycle(head: ListNode | null): boolean { +

- diff --git a/problems/0142.环形链表II.md b/problems/0142.环形链表II.md index a643fd70..d97b160b 100644 --- a/problems/0142.环形链表II.md +++ b/problems/0142.环形链表II.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ diff --git a/problems/0143.重排链表.md b/problems/0143.重排链表.md index 0b3be9a0..87075431 100644 --- a/problems/0143.重排链表.md +++ b/problems/0143.重排链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 143.重排链表 diff --git a/problems/0150.逆波兰表达式求值.md b/problems/0150.逆波兰表达式求值.md index 663a68ea..d05f67bc 100644 --- a/problems/0150.逆波兰表达式求值.md +++ b/problems/0150.逆波兰表达式求值.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 这不仅仅是一道好题,也展现出计算机的思考方式 @@ -169,8 +169,12 @@ class Solution { ```python from operator import add, sub, mul -class Solution: - op_map = {'+': add, '-': sub, '*': mul, '/': lambda x, y: int(x / y)} +def div(x, y): + # 使用整数除法的向零取整方式 + return int(x / y) if x * y > 0 else -(abs(x) // abs(y)) + +class Solution(object): + op_map = {'+': add, '-': sub, '*': mul, '/': div} def evalRPN(self, tokens: List[str]) -> int: stack = [] @@ -186,18 +190,31 @@ class Solution: 另一种可行,但因为使用eval相对较慢的方法: ```python -class Solution: - def evalRPN(self, tokens: List[str]) -> int: +from operator import add, sub, mul + +def div(x, y): + # 使用整数除法的向零取整方式 + return int(x / y) if x * y > 0 else -(abs(x) // abs(y)) + +class Solution(object): + op_map = {'+': add, '-': sub, '*': mul, '/': div} + + def evalRPN(self, tokens): + """ + :type tokens: List[str] + :rtype: int + """ stack = [] - for item in tokens: - if item not in {"+", "-", "*", "/"}: - stack.append(item) + for token in tokens: + if token in self.op_map: + op1 = stack.pop() + op2 = stack.pop() + operation = self.op_map[token] + stack.append(operation(op2, op1)) else: - first_num, second_num = stack.pop(), stack.pop() - stack.append( - int(eval(f'{second_num} {item} {first_num}')) # 第一个出来的在运算符后面 - ) - return int(stack.pop()) # 如果一开始只有一个数,那么会是字符串形式的 + stack.append(int(token)) + return stack.pop() + ``` diff --git a/problems/0151.翻转字符串里的单词.md b/problems/0151.翻转字符串里的单词.md index 7a37ad07..b6b34be3 100644 --- a/problems/0151.翻转字符串里的单词.md +++ b/problems/0151.翻转字符串里的单词.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0160.相交链表.md b/problems/0160.相交链表.md index bf62ab30..d4422bd8 100644 --- a/problems/0160.相交链表.md +++ b/problems/0160.相交链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

同:[链表:链表相交](https://programmercarl.com/面试题02.07.链表相交.html)

diff --git a/problems/0188.买卖股票的最佳时机IV.md b/problems/0188.买卖股票的最佳时机IV.md index 2521749f..def69277 100644 --- a/problems/0188.买卖股票的最佳时机IV.md +++ b/problems/0188.买卖股票的最佳时机IV.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 188.买卖股票的最佳时机IV @@ -553,8 +553,8 @@ impl Solution { ``` +

- diff --git a/problems/0189.旋转数组.md b/problems/0189.旋转数组.md index d60612e9..b47ee4b9 100644 --- a/problems/0189.旋转数组.md +++ b/problems/0189.旋转数组.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 189. 旋转数组 @@ -201,8 +201,8 @@ function reverseByRange(nums: number[], left: number, right: number): void { +

- diff --git a/problems/0198.打家劫舍.md b/problems/0198.打家劫舍.md index 480222ef..032204bb 100644 --- a/problems/0198.打家劫舍.md +++ b/problems/0198.打家劫舍.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 198.打家劫舍 @@ -360,8 +360,8 @@ impl Solution { ``` +

- diff --git a/problems/0200.岛屿数量.广搜版.md b/problems/0200.岛屿数量.广搜版.md index 85471f73..00e4efd8 100644 --- a/problems/0200.岛屿数量.广搜版.md +++ b/problems/0200.岛屿数量.广搜版.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 200. 岛屿数量 @@ -408,9 +408,9 @@ impl Solution { } } ``` +``` +

-``` - diff --git a/problems/0200.岛屿数量.深搜版.md b/problems/0200.岛屿数量.深搜版.md index 83d295bd..46579203 100644 --- a/problems/0200.岛屿数量.深搜版.md +++ b/problems/0200.岛屿数量.深搜版.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 200. 岛屿数量 diff --git a/problems/0202.快乐数.md b/problems/0202.快乐数.md index 719672a2..409a7471 100644 --- a/problems/0202.快乐数.md +++ b/problems/0202.快乐数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -533,8 +533,8 @@ public class Solution { } } ``` +

- diff --git a/problems/0203.移除链表元素.md b/problems/0203.移除链表元素.md index d6d7e6c2..efcc6414 100644 --- a/problems/0203.移除链表元素.md +++ b/problems/0203.移除链表元素.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -497,27 +497,67 @@ func removeElements(_ head: ListNode?, _ val: Int) -> ListNode? { ```php /** - * Definition for singly-linked list. - * type ListNode struct { - * Val int - * Next *ListNode + * Definition for a singly-linked list. + * class ListNode { + * public $val = 0; + * public $next = null; + * function __construct($val = 0, $next = null) { + * $this->val = $val; + * $this->next = $next; + * } * } */ - // 虚拟头+双指针 -func removeElements(head *ListNode, val int) *ListNode { - dummyHead := &ListNode{} - dummyHead.Next = head - pred := dummyHead - cur := head - for cur != nil { - if cur.Val == val { - pred.Next = cur.Next - } else { - pred = cur + +//版本一(在原链表上直接删除): +class Solution { + + /** + * @param ListNode $head + * @param Integer $val + * @return ListNode + */ + function removeElements($head, $val) + { + if ($head == null) { + return null; } - cur = cur.Next + + $now = $head; + while ($now->next != null) { + if ($now->next->val == $val) { + $now->next = $now->next->next; + } else { + $now = $now->next; + } + } + if ($head->val == $val) { + return $head->next; + } + return $head; + } +} + +//版本二(虚拟头结点方式): +class Solution { + + /** + * @param ListNode $head + * @param Integer $val + * @return ListNode + */ + function removeElements($head, $val) + { + $dummyHead = new ListNode(0, $head); + $now = $dummyHead; + while ($now->next != null){ + if ($now->next->val == $val) { + $now->next = $now->next->next; + } else { + $now = $now->next; + } + } + return $dummyHead->next; } - return dummyHead.Next } ``` @@ -545,7 +585,7 @@ impl Solution { let mut dummyHead = Box::new(ListNode::new(0)); dummyHead.next = head; let mut cur = dummyHead.as_mut(); - // 使用take()替换std::men::replace(&mut node.next, None)达到相同的效果,并且更普遍易读 + // 使用take()替换std::mem::replace(&mut node.next, None)达到相同的效果,并且更普遍易读 while let Some(nxt) = cur.next.take() { if nxt.val == val { cur.next = nxt.next; @@ -664,8 +704,8 @@ public class Solution } ``` +

- diff --git a/problems/0205.同构字符串.md b/problems/0205.同构字符串.md index e07ab746..e416d9ce 100644 --- a/problems/0205.同构字符串.md +++ b/problems/0205.同构字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 205. 同构字符串 @@ -179,8 +179,8 @@ function isIsomorphic(s: string, t: string): boolean { +

- diff --git a/problems/0206.翻转链表.md b/problems/0206.翻转链表.md index 5a57939a..430bebe5 100644 --- a/problems/0206.翻转链表.md +++ b/problems/0206.翻转链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 diff --git a/problems/0207.课程表.md b/problems/0207.课程表.md new file mode 100644 index 00000000..dff0b18e --- /dev/null +++ b/problems/0207.课程表.md @@ -0,0 +1,65 @@ +

+ + + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +拓扑排序指的是一种 解决问题的大体思路, 而具体算法,可能是 广搜 可能是深搜。 + +大家可能发现 各式各样的解法,纠结哪个是拓扑排序? + +只要能在把 有向无环图 进行线性排序 的算法 都可以叫做 拓扑排序。 + +引用与任务调度,课程安排等等。 + + +「拓扑排序」是专门应用于有向图的算法; + +把一个 有向无环图 转成 线性的排序 就叫 拓扑排序。 + +拓扑排序(Kahn 算法,其实就是广度优先遍历的思路) + +这道题的做法同样适用于第 210 题。 + + +```CPP +class Solution { +public: + bool canFinish(int numCourses, vector>& prerequisites) { + vector inDegree(numCourses, 0); + unordered_map> umap; + for (int i = 0; i < prerequisites.size(); i++) { + + // prerequisites[i][0] 是 课程入度,prerequisites[i][1] 是课程出度 + // 即: 上课prerequisites[i][0] 之前,必须先上课prerequisites[i][1] + // prerequisites[i][1] -> prerequisites[i][0] + inDegree[prerequisites[i][0]]++;//当前课程入度值+1 + umap[prerequisites[i][1]].push_back(prerequisites[i][0]); // 添加 prerequisites[i][1] 指向的课程 + } + queue que; + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) que.push(i); // 所有入度为0,即为 开头课程 加入队列 + } + int count = 0; + while (que.size()) { + int cur = que.front(); //当前选的课 + que.pop(); + count++; // 选课数+1 + vector courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课 + if (courses.size()) { // 有后续课 + for (int i = 0; i < courses.size(); i++) { + inDegree[courses[i]]--; // 它的后续课的入度-1 + if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列 + } + } + } + if (count == numCourses) return true; + return false; + + } +}; +``` +

+ + + diff --git a/problems/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md index 5934d5e3..e399ac90 100644 --- a/problems/0209.长度最小的子数组.md +++ b/problems/0209.长度最小的子数组.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 209.长度最小的子数组 @@ -270,22 +270,21 @@ var minSubArrayLen = function(target, nums) { ```typescript function minSubArrayLen(target: number, nums: number[]): number { - let left: number = 0, right: number = 0; - let res: number = nums.length + 1; - let sum: number = 0; - while (right < nums.length) { - sum += nums[right]; - if (sum >= target) { - // 不断移动左指针,直到不能再缩小为止 - while (sum - nums[left] >= target) { - sum -= nums[left++]; - } - res = Math.min(res, right - left + 1); - } - right++; + let left: number = 0, + res: number = Infinity, + subLen: number = 0, + sum: number = 0; + for (let right: number = 0; right < nums.length; right++) { + sum += nums[right]; + while (sum >= target) { + subLen = right - left + 1; + res = Math.min(res, subLen); + sum -= nums[left]; + left++; } - return res === nums.length + 1 ? 0 : res; -}; + } + return res === Infinity ? 0 : res; +} ``` ### Swift: diff --git a/problems/0210.课程表II.md b/problems/0210.课程表II.md new file mode 100644 index 00000000..2d2e2429 --- /dev/null +++ b/problems/0210.课程表II.md @@ -0,0 +1,39 @@ + +```CPP +class Solution { +public: + vector findOrder(int numCourses, vector>& prerequisites) { + vector inDegree(numCourses, 0); + vector result; + unordered_map> umap; + for (int i = 0; i < prerequisites.size(); i++) { + + // prerequisites[i][0] 是 课程入度,prerequisites[i][1] 是课程出度 + // 即: 上课prerequisites[i][0] 之前,必须先上课prerequisites[i][1] + // prerequisites[i][1] -> prerequisites[i][0] + inDegree[prerequisites[i][0]]++;//当前课程入度值+1 + umap[prerequisites[i][1]].push_back(prerequisites[i][0]); // 添加 prerequisites[i][1] 指向的课程 + } + queue que; + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) que.push(i); // 所有入度为0,即为 开头课程 加入队列 + } + int count = 0; + while (que.size()) { + int cur = que.front(); //当前选的课 + que.pop(); + count++; // 选课数+1 + result.push_back(cur); + vector courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课 + if (courses.size()) { // 有后续课 + for (int i = 0; i < courses.size(); i++) { + inDegree[courses[i]]--; // 它的后续课的入度-1 + if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列 + } + } + } + if (count == numCourses) return result; + else return vector(); + } +}; +``` diff --git a/problems/0213.打家劫舍II.md b/problems/0213.打家劫舍II.md index ba996f2b..05ebd1ad 100644 --- a/problems/0213.打家劫舍II.md +++ b/problems/0213.打家劫舍II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 213.打家劫舍II diff --git a/problems/0216.组合总和III.md b/problems/0216.组合总和III.md index ac28f9fc..3d7f2d0c 100644 --- a/problems/0216.组合总和III.md +++ b/problems/0216.组合总和III.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -417,6 +417,7 @@ func dfs(k, n int, start int, sum int) { ``` ### JavaScript +- 未剪枝: ```js /** @@ -424,32 +425,74 @@ func dfs(k, n int, start int, sum int) { * @param {number} n * @return {number[][]} */ -var combinationSum3 = function(k, n) { - let res = []; - let path = []; - let sum = 0; - const dfs = (path,index) => { - // 剪枝操作 - if (sum > n){ - return - } - if (path.length == k) { - if(sum == n){ - res.push([...path]); - return - } - } - for (let i = index; i <= 9 - (k-path.length) + 1;i++) { - path.push(i); - sum = sum + i; - index += 1; - dfs(path,index); - sum -= i - path.pop() - } +var combinationSum3 = function (k, n) { + // 回溯法 + let result = [], + path = []; + const backtracking = (_k, targetSum, sum, startIndex) => { + // 终止条件 + if (path.length === _k) { + if (sum === targetSum) { + result.push(path.slice()); + } + // 如果总和不相等,就直接返回 + return; } - dfs(path,1); - return res + + // 循环当前节点,因为只使用数字1到9,所以最大是9 + for (let i = startIndex; i <= 9; i++) { + path.push(i); + sum += i; + // 回调函数 + backtracking(_k, targetSum, sum, i + 1); + // 回溯 + sum -= i; + path.pop(); + } + }; + backtracking(k, n, 0, 1); + return result; +}; +``` + +- 剪枝: + +```js +/** + * @param {number} k + * @param {number} n + * @return {number[][]} + */ +var combinationSum3 = function (k, n) { + // 回溯法 + let result = [], + path = []; + const backtracking = (_k, targetSum, sum, startIndex) => { + if (sum > targetSum) { + return; + } + // 终止条件 + if (path.length === _k) { + if (sum === targetSum) { + result.push(path.slice()); + } + // 如果总和不相等,就直接返回 + return; + } + + // 循环当前节点,因为只使用数字1到9,所以最大是9 + for (let i = startIndex; i <= 9 - (_k - path.length) + 1; i++) { + path.push(i); + sum += i; + // 回调函数 + backtracking(_k, targetSum, sum, i + 1); + // 回溯 + sum -= i; + path.pop(); + } + }; + backtracking(k, n, 0, 1); + return result; }; ``` diff --git a/problems/0222.完全二叉树的节点个数.md b/problems/0222.完全二叉树的节点个数.md index d93d2a33..8d7779f9 100644 --- a/problems/0222.完全二叉树的节点个数.md +++ b/problems/0222.完全二叉树的节点个数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 222.完全二叉树的节点个数 @@ -54,7 +54,7 @@ 1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。 代码如下: -``` +```CPP int getNodesNum(TreeNode* cur) { ``` @@ -62,7 +62,7 @@ int getNodesNum(TreeNode* cur) { 代码如下: -``` +```CPP if (cur == NULL) return 0; ``` @@ -70,7 +70,7 @@ if (cur == NULL) return 0; 代码如下: -``` +```CPP int leftNum = getNodesNum(cur->left); // 左 int rightNum = getNodesNum(cur->right); // 右 int treeNum = leftNum + rightNum + 1; // 中 @@ -893,8 +893,8 @@ public int CountNodes(TreeNode root) } ``` +

- diff --git a/problems/0225.用队列实现栈.md b/problems/0225.用队列实现栈.md index 6900e668..c7dc52f1 100644 --- a/problems/0225.用队列实现栈.md +++ b/problems/0225.用队列实现栈.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -1249,8 +1249,8 @@ impl MyStack { } ``` +

- diff --git a/problems/0226.翻转二叉树.md b/problems/0226.翻转二叉树.md index 8691953a..824968f0 100644 --- a/problems/0226.翻转二叉树.md +++ b/problems/0226.翻转二叉树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 226.翻转二叉树 @@ -1028,8 +1028,8 @@ public TreeNode InvertTree(TreeNode root) { } ``` +

- diff --git a/problems/0232.用栈实现队列.md b/problems/0232.用栈实现队列.md index 41933ca4..e8a3d2ec 100644 --- a/problems/0232.用栈实现队列.md +++ b/problems/0232.用栈实现队列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 diff --git a/problems/0234.回文链表.md b/problems/0234.回文链表.md index fef942fc..1356b7da 100644 --- a/problems/0234.回文链表.md +++ b/problems/0234.回文链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 234.回文链表 @@ -428,8 +428,8 @@ function reverseList(head: ListNode | null): ListNode | null { +

- diff --git a/problems/0235.二叉搜索树的最近公共祖先.md b/problems/0235.二叉搜索树的最近公共祖先.md index 2a11f9f4..597c2dff 100644 --- a/problems/0235.二叉搜索树的最近公共祖先.md +++ b/problems/0235.二叉搜索树的最近公共祖先.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 235. 二叉搜索树的最近公共祖先 @@ -547,8 +547,8 @@ public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) ``` +

- diff --git a/problems/0236.二叉树的最近公共祖先.md b/problems/0236.二叉树的最近公共祖先.md index 049f70c7..5e80e702 100644 --- a/problems/0236.二叉树的最近公共祖先.md +++ b/problems/0236.二叉树的最近公共祖先.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 本来是打算将二叉树和二叉搜索树的公共祖先问题一起讲,后来发现篇幅过长了,只能先说一说二叉树的公共祖先问题。 @@ -247,7 +247,7 @@ public: ### Java - +递归 ```Java class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { @@ -271,6 +271,47 @@ class Solution { } } +``` +迭代 +```Java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + int max = Integer.MAX_VALUE; + Stack st = new Stack<>(); + TreeNode cur = root, pre = null; + while (cur != null || !st.isEmpty()) { + while (cur != null) { + st.push(cur); + cur = cur.left; + } + cur = st.pop(); + if (cur.right == null || cur.right == pre) { + // p/q是 中/左 或者 中/右 , 返回中 + if (cur == p || cur == q) { + if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) { + return cur; + } + cur.val = max; + } + // p/q是 左/右 , 返回中 + if (cur.left != null && cur.left.val == max && cur.right != null && cur.right.val == max) { + return cur; + } + // MAX_VALUE 往上传递 + if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) { + cur.val = max; + } + pre = cur; + cur = null; + } else { + st.push(cur); + cur = cur.right; + } + } + return null; + } +} + ``` ### Python @@ -445,8 +486,8 @@ public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) } ``` +

- diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md index 19ac1261..23bf615b 100644 --- a/problems/0239.滑动窗口最大值.md +++ b/problems/0239.滑动窗口最大值.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0242.有效的字母异位词.md b/problems/0242.有效的字母异位词.md index 6eed90a7..ac03ddbb 100644 --- a/problems/0242.有效的字母异位词.md +++ b/problems/0242.有效的字母异位词.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 数组就是简单的哈希表,但是数组的大小可不是无限开辟的 @@ -390,8 +390,8 @@ object Solution { * [438.找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) +

- diff --git a/problems/0257.二叉树的所有路径.md b/problems/0257.二叉树的所有路径.md index 4c6c92c5..2d9292bc 100644 --- a/problems/0257.二叉树的所有路径.md +++ b/problems/0257.二叉树的所有路径.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 以为只用了递归,其实还用了回溯 @@ -40,7 +40,7 @@ 要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下: -``` +```CPP void traversal(TreeNode* cur, vector& path, vector& result) ``` @@ -48,7 +48,7 @@ void traversal(TreeNode* cur, vector& path, vector& result) 在写递归的时候都习惯了这么写: -``` +```CPP if (cur == NULL) { 终止处理逻辑 } @@ -59,7 +59,7 @@ if (cur == NULL) { **那么什么时候算是找到了叶子节点?** 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。 所以本题的终止条件是: -``` +```CPP if (cur->left == NULL && cur->right == NULL) { 终止处理逻辑 } @@ -102,7 +102,7 @@ if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 所以递归前要加上判断语句,下面要递归的节点是否为空,如下 -``` +```CPP if (cur->left) { traversal(cur->left, path, result); } diff --git a/problems/0279.完全平方数.md b/problems/0279.完全平方数.md index f7c06dbd..570632bd 100644 --- a/problems/0279.完全平方数.md +++ b/problems/0279.完全平方数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 279.完全平方数 @@ -219,7 +219,7 @@ class Solution { ### Python: -先遍历物品, 再遍历背包 +先遍历背包, 再遍历物品 ```python class Solution: def numSquares(self, n: int) -> int: @@ -234,7 +234,7 @@ class Solution: return dp[n] ``` -先遍历背包, 再遍历物品 +先遍历物品, 再遍历背包 ```python class Solution: def numSquares(self, n: int) -> int: @@ -271,7 +271,27 @@ class Solution: # 返回结果 return dp[n] +``` +```python +class Solution(object): + def numSquares(self, n): + # 先把可以选的数准备好,更好理解 + nums, num = [], 1 + while num ** 2 <= n: + nums.append(num ** 2) + num += 1 + # dp数组初始化 + dp = [float('inf')] * (n + 1) + dp[0] = 0 + # 遍历准备好的完全平方数 + for i in range(len(nums)): + # 遍历背包容量 + for j in range(nums[i], n+1): + dp[j] = min(dp[j], dp[j-nums[i]]+1) + # 返回结果 + return dp[-1] + ``` ### Go: @@ -389,7 +409,7 @@ function numSquares(n: number): number { }; ``` -## C +### C ```c #define min(a, b) ((a) > (b) ? (b) : (a)) diff --git a/problems/0283.移动零.md b/problems/0283.移动零.md index fc708844..4d8fd9a1 100644 --- a/problems/0283.移动零.md +++ b/problems/0283.移动零.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 283. 移动零:动态规划:一样的套路,再求一次完全平方数 @@ -172,8 +172,8 @@ void moveZeroes(int* nums, int numsSize){ +

- diff --git a/problems/0300.最长上升子序列.md b/problems/0300.最长上升子序列.md index 6d82eae1..f256d15c 100644 --- a/problems/0300.最长上升子序列.md +++ b/problems/0300.最长上升子序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 300.最长递增子序列 @@ -129,6 +129,7 @@ public: ```Java class Solution { public int lengthOfLIS(int[] nums) { + if (nums.length <= 1) return nums.length; int[] dp = new int[nums.length]; int res = 1; Arrays.fill(dp, 1); @@ -137,8 +138,8 @@ class Solution { if (nums[i] > nums[j]) { dp[i] = Math.max(dp[i], dp[j] + 1); } - res = Math.max(res, dp[i]); } + res = Math.max(res, dp[i]); } return res; } diff --git a/problems/0309.最佳买卖股票时机含冷冻期.md b/problems/0309.最佳买卖股票时机含冷冻期.md index 9dc35bdf..5a38111a 100644 --- a/problems/0309.最佳买卖股票时机含冷冻期.md +++ b/problems/0309.最佳买卖股票时机含冷冻期.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 309.最佳买卖股票时机含冷冻期 diff --git a/problems/0322.零钱兑换.md b/problems/0322.零钱兑换.md index 156b5ff3..0ed5cf68 100644 --- a/problems/0322.零钱兑换.md +++ b/problems/0322.零钱兑换.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 322. 零钱兑换 diff --git a/problems/0332.重新安排行程.md b/problems/0332.重新安排行程.md index 6c8a4814..bc453c51 100644 --- a/problems/0332.重新安排行程.md +++ b/problems/0332.重新安排行程.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。 @@ -379,6 +379,8 @@ class Solution { String targetLocation; //遍历从当前位置出发的机票 for (int i = 0; i < targetLocations.size(); i++) { + //去重,否则在最后一个测试用例中遇到循环时会无限递归 + if(i > 0 && targetLocations.get(i).equals(targetLocations.get(i - 1))) continue; targetLocation = targetLocations.get(i); //删除终点列表中当前的终点 targetLocations.remove(i); @@ -942,8 +944,8 @@ impl Solution { } ``` +

- diff --git a/problems/0337.打家劫舍III.md b/problems/0337.打家劫舍III.md index 61b9f99c..7aae5cbf 100644 --- a/problems/0337.打家劫舍III.md +++ b/problems/0337.打家劫舍III.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 337.打家劫舍 III diff --git a/problems/0343.整数拆分.md b/problems/0343.整数拆分.md index bbbd5c63..205b2201 100644 --- a/problems/0343.整数拆分.md +++ b/problems/0343.整数拆分.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 343. 整数拆分 @@ -296,7 +296,7 @@ class Solution: def integerBreak(self, n): if n == 2: # 当n等于2时,只有一种拆分方式:1+1=2,乘积为1 return 1 - if n == 3: # 当n等于3时,只有一种拆分方式:1+1+1=3,乘积为1 + if n == 3: # 当n等于3时,只有一种拆分方式:2+1=3,乘积为2 return 2 if n == 4: # 当n等于4时,有两种拆分方式:2+2=4和1+1+1+1=4,乘积都为4 return 4 @@ -516,8 +516,8 @@ public class Solution } ``` +

- diff --git a/problems/0344.反转字符串.md b/problems/0344.反转字符串.md index 44184c53..793c9af3 100644 --- a/problems/0344.反转字符串.md +++ b/problems/0344.反转字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0347.前K个高频元素.md b/problems/0347.前K个高频元素.md index 93d605f5..34d9f82c 100644 --- a/problems/0347.前K个高频元素.md +++ b/problems/0347.前K个高频元素.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 前K个大数问题,老生常谈,不得不谈 @@ -218,7 +218,7 @@ class Solution { ``` ### Python: - +解法一: ```python #时间复杂度:O(nlogk) #空间复杂度:O(n) @@ -246,6 +246,31 @@ class Solution: result[i] = heapq.heappop(pri_que)[1] return result ``` +解法二: +```python +class Solution: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # 使用字典统计数字出现次数 + time_dict = defaultdict(int) + for num in nums: + time_dict[num] += 1 + # 更改字典,key为出现次数,value为相应的数字的集合 + index_dict = defaultdict(list) + for key in time_dict: + index_dict[time_dict[key]].append(key) + # 排序 + key = list(index_dict.keys()) + key.sort() + result = [] + cnt = 0 + # 获取前k项 + while key and cnt != k: + result += index_dict[key[-1]] + cnt += len(index_dict[key[-1]]) + key.pop() + + return result[0: k] +``` ### Go: @@ -578,8 +603,8 @@ impl Solution { } ``` +

- diff --git a/problems/0349.两个数组的交集.md b/problems/0349.两个数组的交集.md index 9902fff8..e17e940f 100644 --- a/problems/0349.两个数组的交集.md +++ b/problems/0349.两个数组的交集.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0376.摆动序列.md b/problems/0376.摆动序列.md index 5c2241c8..9e6714ce 100644 --- a/problems/0376.摆动序列.md +++ b/problems/0376.摆动序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 376. 摆动序列 diff --git a/problems/0377.组合总和Ⅳ.md b/problems/0377.组合总和Ⅳ.md index 6f81bffe..e96f8dc6 100644 --- a/problems/0377.组合总和Ⅳ.md +++ b/problems/0377.组合总和Ⅳ.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 377. 组合总和 Ⅳ diff --git a/problems/0383.赎金信.md b/problems/0383.赎金信.md index ff5aafed..e8caa980 100644 --- a/problems/0383.赎金信.md +++ b/problems/0383.赎金信.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -133,7 +133,7 @@ class Solution { record[c - 'a'] -= 1; } - // 如果数组中存在负数,说明ransomNote字符串总存在magazine中没有的字符 + // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符 for(int i : record){ if(i < 0){ return false; @@ -465,8 +465,8 @@ bool canConstruct(char* ransomNote, char* magazine) { } ``` +

- diff --git a/problems/0392.判断子序列.md b/problems/0392.判断子序列.md index ebd567cb..92246e4f 100644 --- a/problems/0392.判断子序列.md +++ b/problems/0392.判断子序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 392.判断子序列 @@ -173,6 +173,63 @@ class Solution { } } ``` +> 修改遍历顺序后,可以利用滚动数组,对dp数组进行压缩 +```java +class Solution { + public boolean isSubsequence(String s, String t) { + // 修改遍历顺序,外圈遍历t,内圈遍历s。使得dp的推算只依赖正上方和左上方,方便压缩。 + int[][] dp = new int[t.length() + 1][s.length() + 1]; + for (int i = 1; i < dp.length; i++) { // 遍历t字符串 + for (int j = 1; j < dp[i].length; j++) { // 遍历s字符串 + if (t.charAt(i - 1) == s.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = dp[i - 1][j]; + } + } + System.out.println(Arrays.toString(dp[i])); + } + return dp[t.length()][s.length()] == s.length(); + } +} +``` +> 状态压缩 +```java +class Solution { + public boolean isSubsequence(String s, String t) { + int[] dp = new int[s.length() + 1]; + for (int i = 0; i < t.length(); i ++) { + // 需要使用上一轮的dp[j - 1],所以使用倒序遍历 + for (int j = dp.length - 1; j > 0; j --) { + // i遍历的是t字符串,j遍历的是dp数组,dp数组的长度比s的大1,因此需要减1。 + if (t.charAt(i) == s.charAt(j - 1)) { + dp[j] = dp[j - 1] + 1; + } + } + } + return dp[s.length()] == s.length(); + } +} +``` +> 将dp定义为boolean类型,dp[i]直接表示s.substring(0, i)是否为t的子序列 + +```java +class Solution { + public boolean isSubsequence(String s, String t) { + boolean[] dp = new boolean[s.length() + 1]; + // 表示 “” 是t的子序列 + dp[0] = true; + for (int i = 0; i < t.length(); i ++) { + for (int j = dp.length - 1; j > 0; j --) { + if (t.charAt(i) == s.charAt(j - 1)) { + dp[j] = dp[j - 1]; + } + } + } + return dp[dp.length - 1]; + } +} +``` ### Python: @@ -347,8 +404,8 @@ impl Solution { } ``` +

- diff --git a/problems/0404.左叶子之和.md b/problems/0404.左叶子之和.md index 3d0f5a8a..1ba71dc9 100644 --- a/problems/0404.左叶子之和.md +++ b/problems/0404.左叶子之和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 404.左叶子之和 @@ -669,8 +669,8 @@ public int SumOfLeftLeaves(TreeNode root) } ``` +

- diff --git a/problems/0406.根据身高重建队列.md b/problems/0406.根据身高重建队列.md index b0b02c14..b7e94543 100644 --- a/problems/0406.根据身高重建队列.md +++ b/problems/0406.根据身高重建队列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 406.根据身高重建队列 @@ -421,8 +421,8 @@ public class Solution ``` +

- diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md index 71e01ae3..4d2e6bf6 100644 --- a/problems/0416.分割等和子集.md +++ b/problems/0416.分割等和子集.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 416. 分割等和子集 @@ -755,8 +755,8 @@ public class Solution } } ``` +

- diff --git a/problems/0417.太平洋大西洋水流问题.md b/problems/0417.太平洋大西洋水流问题.md index 8fe0f1b4..b8448e93 100644 --- a/problems/0417.太平洋大西洋水流问题.md +++ b/problems/0417.太平洋大西洋水流问题.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md index b668e860..20961602 100644 --- a/problems/0435.无重叠区间.md +++ b/problems/0435.无重叠区间.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 435. 无重叠区间 @@ -494,8 +494,8 @@ public class Solution ``` +

- diff --git a/problems/0450.删除二叉搜索树中的节点.md b/problems/0450.删除二叉搜索树中的节点.md index 60dae7b9..f6057f44 100644 --- a/problems/0450.删除二叉搜索树中的节点.md +++ b/problems/0450.删除二叉搜索树中的节点.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 二叉搜索树删除节点就涉及到结构调整了 @@ -801,8 +801,8 @@ impl Solution { } ``` +

- diff --git a/problems/0452.用最少数量的箭引爆气球.md b/problems/0452.用最少数量的箭引爆气球.md index cd57f83b..318c3035 100644 --- a/problems/0452.用最少数量的箭引爆气球.md +++ b/problems/0452.用最少数量的箭引爆气球.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 452. 用最少数量的箭引爆气球 diff --git a/problems/0454.四数相加II.md b/problems/0454.四数相加II.md index a0bf84da..db9a9e43 100644 --- a/problems/0454.四数相加II.md +++ b/problems/0454.四数相加II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 需要哈希的地方都能找到map的身影 @@ -481,8 +481,8 @@ int fourSumCount(int* nums1, int nums1Size, int* nums2, int nums2Size, int* nums return count; } ``` +

- diff --git a/problems/0455.分发饼干.md b/problems/0455.分发饼干.md index b6f5bae5..9f59d3ad 100644 --- a/problems/0455.分发饼干.md +++ b/problems/0455.分发饼干.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 455.分发饼干 diff --git a/problems/0459.重复的子字符串.md b/problems/0459.重复的子字符串.md index 311e3a69..1028bf1e 100644 --- a/problems/0459.重复的子字符串.md +++ b/problems/0459.重复的子字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> KMP算法还能干这个 diff --git a/problems/0463.岛屿的周长.md b/problems/0463.岛屿的周长.md index 2e954e30..5261d6c2 100644 --- a/problems/0463.岛屿的周长.md +++ b/problems/0463.岛屿的周长.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -432,8 +432,8 @@ function islandPerimeter(grid: number[][]): number { } ``` +

- diff --git a/problems/0474.一和零.md b/problems/0474.一和零.md index af50fa5c..47b34a0f 100644 --- a/problems/0474.一和零.md +++ b/problems/0474.一和零.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 474.一和零 @@ -595,8 +595,8 @@ public class Solution } ``` +

- diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md index 1aa69a36..8f642a5f 100644 --- a/problems/0491.递增子序列.md +++ b/problems/0491.递增子序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 和子集问题有点像,但又处处是陷阱 @@ -206,7 +206,6 @@ public: ### Java ```Java -//using set, aligned with the unimproved method class Solution { List> result = new ArrayList<>(); List path = new ArrayList<>(); diff --git a/problems/0494.目标和.md b/problems/0494.目标和.md index 02edad4d..92616ed1 100644 --- a/problems/0494.目标和.md +++ b/problems/0494.目标和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -101,7 +101,7 @@ public: int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (S > sum) return 0; // 此时没有方案 - if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题 + if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要格外小心数值溢出的问题 int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和 // 以下为回溯法代码 @@ -173,9 +173,9 @@ dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法 * 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。 * 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。 -* 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包 -* 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包 -* 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包 +* 已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 容量为5的背包 +* 已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 容量为5的背包 +* 已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 容量为5的背包 那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。 @@ -650,8 +650,8 @@ public class Solution } ``` +

- diff --git a/problems/0496.下一个更大元素I.md b/problems/0496.下一个更大元素I.md index d97a3e84..54182d30 100644 --- a/problems/0496.下一个更大元素I.md +++ b/problems/0496.下一个更大元素I.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 496.下一个更大元素 I @@ -450,8 +450,8 @@ impl Solution { +

- diff --git a/problems/0501.二叉搜索树中的众数.md b/problems/0501.二叉搜索树中的众数.md index 20627d1a..93b3fb54 100644 --- a/problems/0501.二叉搜索树中的众数.md +++ b/problems/0501.二叉搜索树中的众数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 二叉树上应该怎么求,二叉搜索树上又应该怎么求? @@ -1051,8 +1051,8 @@ public class Solution ``` +

- diff --git a/problems/0503.下一个更大元素II.md b/problems/0503.下一个更大元素II.md index 6df83fb2..62066d85 100644 --- a/problems/0503.下一个更大元素II.md +++ b/problems/0503.下一个更大元素II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 503.下一个更大元素II @@ -295,8 +295,8 @@ impl Solution { } ``` +

- diff --git a/problems/0509.斐波那契数.md b/problems/0509.斐波那契数.md index 71c022bd..21b07802 100644 --- a/problems/0509.斐波那契数.md +++ b/problems/0509.斐波那契数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 509. 斐波那契数 @@ -475,8 +475,8 @@ public class Solution +

- diff --git a/problems/0513.找树左下角的值.md b/problems/0513.找树左下角的值.md index d897bba1..d69ceb6f 100644 --- a/problems/0513.找树左下角的值.md +++ b/problems/0513.找树左下角的值.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 513.找树左下角的值 diff --git a/problems/0516.最长回文子序列.md b/problems/0516.最长回文子序列.md index 44bdec1f..af36b94d 100644 --- a/problems/0516.最长回文子序列.md +++ b/problems/0516.最长回文子序列.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 516.最长回文子序列 @@ -298,8 +298,8 @@ impl Solution { ``` +

- diff --git a/problems/0518.零钱兑换II.md b/problems/0518.零钱兑换II.md index 59fdf6cd..255912d6 100644 --- a/problems/0518.零钱兑换II.md +++ b/problems/0518.零钱兑换II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -395,8 +395,8 @@ public class Solution } ``` +

- diff --git a/problems/0530.二叉搜索树的最小绝对差.md b/problems/0530.二叉搜索树的最小绝对差.md index 82b3f5d4..7fe64ad2 100644 --- a/problems/0530.二叉搜索树的最小绝对差.md +++ b/problems/0530.二叉搜索树的最小绝对差.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 利用二叉搜索树的特性搞起! @@ -669,8 +669,8 @@ public class Solution } ``` +

- diff --git a/problems/0538.把二叉搜索树转换为累加树.md b/problems/0538.把二叉搜索树转换为累加树.md index 7fcb5efd..b95b5854 100644 --- a/problems/0538.把二叉搜索树转换为累加树.md +++ b/problems/0538.把二叉搜索树转换为累加树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 538.把二叉搜索树转换为累加树 @@ -548,8 +548,8 @@ public class Solution ``` +

- diff --git a/problems/0541.反转字符串II.md b/problems/0541.反转字符串II.md index 80e662f9..3e304fab 100644 --- a/problems/0541.反转字符串II.md +++ b/problems/0541.反转字符串II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0583.两个字符串的删除操作.md b/problems/0583.两个字符串的删除操作.md index 7dbb8ef5..14a55631 100644 --- a/problems/0583.两个字符串的删除操作.md +++ b/problems/0583.两个字符串的删除操作.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 583. 两个字符串的删除操作 @@ -234,6 +234,25 @@ class Solution: return dp[-1][-1] ``` +> 版本 2 + +```python +class Solution(object): + def minDistance(self, word1, word2): + m, n = len(word1), len(word2) + + # dp 求解两字符串最长公共子序列 + dp = [[0] * (n+1) for _ in range(m+1)] + for i in range(1, m+1): + for j in range(1, n+1): + if word1[i-1] == word2[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 m + n - 2 * dp[-1][-1] +``` ### Go: ```go diff --git a/problems/0617.合并二叉树.md b/problems/0617.合并二叉树.md index 3478a2af..530350ac 100644 --- a/problems/0617.合并二叉树.md +++ b/problems/0617.合并二叉树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 617.合并二叉树 @@ -803,8 +803,8 @@ public TreeNode MergeTrees(TreeNode root1, TreeNode root2) } ``` +

- diff --git a/problems/0647.回文子串.md b/problems/0647.回文子串.md index 4887ff83..2011bee3 100644 --- a/problems/0647.回文子串.md +++ b/problems/0647.回文子串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 647. 回文子串 diff --git a/problems/0649.Dota2参议院.md b/problems/0649.Dota2参议院.md index db6b43df..1540a601 100644 --- a/problems/0649.Dota2参议院.md +++ b/problems/0649.Dota2参议院.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -283,8 +283,8 @@ function predictPartyVictory(senate: string): string { +

- diff --git a/problems/0654.最大二叉树.md b/problems/0654.最大二叉树.md index f54558a6..fed9b2b9 100644 --- a/problems/0654.最大二叉树.md +++ b/problems/0654.最大二叉树.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 654.最大二叉树 @@ -598,8 +598,8 @@ public TreeNode ConstructMaximumBinaryTree(int[] nums) } ``` +

- diff --git a/problems/0657.机器人能否返回原点.md b/problems/0657.机器人能否返回原点.md index ef587391..eccfef3a 100644 --- a/problems/0657.机器人能否返回原点.md +++ b/problems/0657.机器人能否返回原点.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 657. 机器人能否返回原点 @@ -181,8 +181,8 @@ var judgeCircle = function (moves) { ``` +

- diff --git a/problems/0669.修剪二叉搜索树.md b/problems/0669.修剪二叉搜索树.md index 6824c7e2..aef84659 100644 --- a/problems/0669.修剪二叉搜索树.md +++ b/problems/0669.修剪二叉搜索树.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -586,8 +586,8 @@ public TreeNode TrimBST(TreeNode root, int low, int high) ``` +

- diff --git a/problems/0673.最长递增子序列的个数.md b/problems/0673.最长递增子序列的个数.md index 0277f249..0366ee80 100644 --- a/problems/0673.最长递增子序列的个数.md +++ b/problems/0673.最长递增子序列的个数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 673.最长递增子序列的个数 @@ -360,8 +360,8 @@ var findNumberOfLIS = function(nums) { ``` +

- diff --git a/problems/0674.最长连续递增序列.md b/problems/0674.最长连续递增序列.md index ece62944..cebb552b 100644 --- a/problems/0674.最长连续递增序列.md +++ b/problems/0674.最长连续递增序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 674. 最长连续递增序列 @@ -186,7 +186,23 @@ public: return res; } ``` - +> 动态规划状态压缩 +```java +class Solution { + public int findLengthOfLCIS(int[] nums) { + // 记录以 前一个元素结尾的最长连续递增序列的长度 和 以当前 结尾的...... + int beforeOneMaxLen = 1, currentMaxLen = 0; + // res 赋最小值返回的最小值1 + int res = 1; + for (int i = 1; i < nums.length; i ++) { + currentMaxLen = nums[i] > nums[i - 1] ? beforeOneMaxLen + 1 : 1; + beforeOneMaxLen = currentMaxLen; + res = Math.max(res, currentMaxLen); + } + return res; + } +} +``` > 贪心法: ```Java diff --git a/problems/0684.冗余连接.md b/problems/0684.冗余连接.md index f5e84223..6d5f2bc4 100644 --- a/problems/0684.冗余连接.md +++ b/problems/0684.冗余连接.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 684.冗余连接 @@ -378,8 +378,8 @@ var findRedundantConnection = function(edges) { +

- diff --git a/problems/0685.冗余连接II.md b/problems/0685.冗余连接II.md index c07dda3a..3f489d82 100644 --- a/problems/0685.冗余连接II.md +++ b/problems/0685.冗余连接II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 685.冗余连接II @@ -619,8 +619,8 @@ var findRedundantDirectedConnection = function(edges) { +

- diff --git a/problems/0695.岛屿的最大面积.md b/problems/0695.岛屿的最大面积.md index 87b1b5bb..11b638d4 100644 --- a/problems/0695.岛屿的最大面积.md +++ b/problems/0695.岛屿的最大面积.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 695. 岛屿的最大面积 @@ -708,8 +708,8 @@ impl Solution { } } ``` +

- diff --git a/problems/0700.二叉搜索树中的搜索.md b/problems/0700.二叉搜索树中的搜索.md index 58ada3cb..9ec51524 100644 --- a/problems/0700.二叉搜索树中的搜索.md +++ b/problems/0700.二叉搜索树中的搜索.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 700.二叉搜索树中的搜索 @@ -507,8 +507,8 @@ public TreeNode SearchBST(TreeNode root, int val) } ``` +

- diff --git a/problems/0701.二叉搜索树中的插入操作.md b/problems/0701.二叉搜索树中的插入操作.md index 5cb0de99..6b9e5834 100644 --- a/problems/0701.二叉搜索树中的插入操作.md +++ b/problems/0701.二叉搜索树中的插入操作.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 701.二叉搜索树中的插入操作 diff --git a/problems/0704.二分查找.md b/problems/0704.二分查找.md index 31e89ae3..5604cd56 100644 --- a/problems/0704.二分查找.md +++ b/problems/0704.二分查找.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 704. 二分查找 @@ -501,19 +501,19 @@ func search(nums: [Int], target: Int) -> Int { ### **Rust:** -(版本一)左闭右开区间 +(版本一)左闭右闭区间 ```rust use std::cmp::Ordering; impl Solution { pub fn search(nums: Vec, target: i32) -> i32 { - let (mut left, mut right) = (0, nums.len()); - while left < right { + let (mut left, mut right) = (0_i32, nums.len() as i32 - 1); + while left <= right { let mid = (right + left) / 2; - match nums[mid].cmp(&target) { + match nums[mid as usize].cmp(&target) { Ordering::Less => left = mid + 1, - Ordering::Greater => right = mid, - Ordering::Equal => return mid as i32, + Ordering::Greater => right = mid - 1, + Ordering::Equal => return mid, } } -1 @@ -521,19 +521,19 @@ impl Solution { } ``` -//(版本二)左闭右闭区间 +//(版本二)左闭右开区间 ```rust use std::cmp::Ordering; impl Solution { pub fn search(nums: Vec, target: i32) -> i32 { - let (mut left, mut right) = (0, nums.len()); - while left <= right { + let (mut left, mut right) = (0_i32, nums.len() as i32); + while left < right { let mid = (right + left) / 2; - match nums[mid].cmp(&target) { + match nums[mid as usize].cmp(&target) { Ordering::Less => left = mid + 1, - Ordering::Greater => right = mid - 1, - Ordering::Equal => return mid as i32, + Ordering::Greater => right = mid, + Ordering::Equal => return mid, } } -1 @@ -828,8 +828,8 @@ class Solution { } ``` +

- diff --git a/problems/0707.设计链表.md b/problems/0707.设计链表.md index fecdbc3c..47771d28 100644 --- a/problems/0707.设计链表.md +++ b/problems/0707.设计链表.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 听说这道题目把链表常见的五个操作都覆盖了? diff --git a/problems/0714.买卖股票的最佳时机含手续费.md b/problems/0714.买卖股票的最佳时机含手续费.md index db398649..88b03d9d 100644 --- a/problems/0714.买卖股票的最佳时机含手续费.md +++ b/problems/0714.买卖股票的最佳时机含手续费.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 714. 买卖股票的最佳时机含手续费 @@ -360,8 +360,8 @@ object Solution { } ``` +

- diff --git a/problems/0714.买卖股票的最佳时机含手续费(动态规划).md b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md index 88ba9271..73714147 100644 --- a/problems/0714.买卖股票的最佳时机含手续费(动态规划).md +++ b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 714.买卖股票的最佳时机含手续费 @@ -322,8 +322,8 @@ impl Solution { } ``` +

- diff --git a/problems/0718.最长重复子数组.md b/problems/0718.最长重复子数组.md index e00b3ded..6c8e7101 100644 --- a/problems/0718.最长重复子数组.md +++ b/problems/0718.最长重复子数组.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 718. 最长重复子数组 diff --git a/problems/0724.寻找数组的中心索引.md b/problems/0724.寻找数组的中心索引.md index 9ed8535e..a66a4450 100644 --- a/problems/0724.寻找数组的中心索引.md +++ b/problems/0724.寻找数组的中心索引.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 724.寻找数组的中心下标 diff --git a/problems/0738.单调递增的数字.md b/problems/0738.单调递增的数字.md index 4a8a6e08..3d46d5ad 100644 --- a/problems/0738.单调递增的数字.md +++ b/problems/0738.单调递增的数字.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 738.单调递增的数字 diff --git a/problems/0739.每日温度.md b/problems/0739.每日温度.md index fdb11c63..45af5286 100644 --- a/problems/0739.每日温度.md +++ b/problems/0739.每日温度.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -482,8 +482,8 @@ impl Solution { ``` +

- diff --git a/problems/0743.网络延迟时间.md b/problems/0743.网络延迟时间.md new file mode 100644 index 00000000..e631951a --- /dev/null +++ b/problems/0743.网络延迟时间.md @@ -0,0 +1,1227 @@ + +# 743.网络延迟时间 + +https://leetcode.cn/problems/network-delay-time/description/ + + +有 n 个网络节点,标记为 1 到 n。 + +给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。 + +现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240229104105.png) + +提示: + +* 1 <= k <= n <= 100 +* 1 <= times.length <= 6000 +* times[i].length == 3 +* 1 <= ui, vi <= n +* ui != vi +* 0 <= wi <= 100 +* 所有 (ui, vi) 对都 互不相同(即,不含重复边) + +# dijkstra 精讲 + +本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。 + +接下来,我们来详细讲解最短路算法中的 dijkstra 算法。 + +dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。 + +需要注意两点: + +* dijkstra 算法可以同时求 起点到所有节点的最短路径 +* 权值不能为负数 + +(这两点后面我们会讲到) + +如本题示例中的图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240125162647.png) + +起点(节点1)到终点(节点7) 的最短路径是 图中 标记绿线的部分。 + +最短路径的权值为12。 + +其实 dijkstra 算法 和 我们之前讲解的prim算法思路非常接近,如果大家认真学过[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w),那么理解 Dijkstra 算法会相对容易很多。(这也是我要先讲prim再讲dijkstra的原因) + +dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。 + +这里我也给出 **dijkstra三部曲**: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +大家此时已经会发现,这和prim算法 怎么这么像呢。 + +我在[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)讲解中也给出了三部曲。 prim 和 dijkstra 确实很像,思路也是类似的,这一点我在后面还会详细来讲。 + +在dijkstra算法中,同样有一个数组很重要,起名为:minDist。 + +**minDist数组 用来记录 每一个节点距离源点的最小距离**。 + +理解这一点很重要,也是理解 dijkstra 算法的核心所在。 + +大家现在看着可能有点懵,不知道什么意思。 + +没关系,先让大家有一个印象,对理解后面讲解有帮助。 + +我们先来画图看一下 dijkstra 的工作过程,以本题示例为例: (以下为朴素版dijkstra的思路) + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) + +## 朴素版dijkstra + +### 模拟过程 + +----------- + +0、初始化 + +minDist数组数值初始化为int最大值。 + +这里在强点一下 **minDist数组的含义:记录所有节点到源点的最短路径**,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115421.png) + + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1 +* 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[4] = 4 + +可能有录友问:为啥和 minDist[2] 比较? + +再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理 + + +------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点到节点2距离最近,选节点2 + +2、该最近节点被标记访问过 + +节点2被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130121240.png) + +更新 minDist数组,即:源点(节点1) 到 节点6 、 节点3 和 节点4的距离。 + +**为什么更新这些节点呢? 怎么不更新其他节点呢**? + +因为 源点(节点1)通过 已经计算过的节点(节点2) 可以链接到的节点 有 节点3,节点4和节点6. + + +更新 minDist数组: + +* 源点到节点6的最短距离为5,小于原minDist[6]的数值max,更新minDist[6] = 5 +* 源点到节点3的最短距离为3,小于原minDist[3]的数值4,更新minDist[3] = 3 +* 源点到节点4的最短距离为6,小于原minDist[4]的数值max,更新minDist[4] = 6 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点距离哪些节点最近,怎么算的? + +其实就是看 minDist数组里的数值,minDist 记录了 源点到所有节点的最近距离,结合visited数组筛选出未访问的节点就好。 + +从 上面的图,或者 从minDist数组中,我们都能看出 未访问过的节点中,源点(节点1)到节点3距离最近。 + + +2、该最近节点被标记访问过 + +节点3被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130120434.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +更新 minDist数组: + +* 源点到节点4的最短距离为5,小于原minDist[4]的数值6,更新minDist[4] = 5 + +------------------ + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,有节点4 和 节点6,距离源点距离都是 5 (minDist[4] = 5,minDist[6] = 5) ,选哪个节点都可以。 + +2、该最近节点被标记访问过 + +节点4被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201111352.png) + +在上面的讲解中,每一步 我都是按照 dijkstra 三部曲来讲解的,理解了这三部曲,代码也就好懂的。 + +### 代码实现 + +本题代码如下,里面的 三部曲 我都做了注释,大家按照我上面的讲解 来看如下代码: + +```CPP +class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是领接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[k] = 0; // 起始点到自身的距离为0 + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + // 遍历每个节点,选择未被访问的节点集合中哪个节点到源点的距离最小 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] <= minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 标记该顶点已被访问 + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + + } + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### debug方法 + +写这种题目难免会有各种各样的问题,我们如何发现自己的代码是否有问题呢? + +最好的方式就是打日志,本题的话,就是将 minDist 数组打印出来,就可以很明显发现 哪里出问题了。 + +每次选择节点后,minDist数组的变化是否符合预期 ,是否和我上面讲的逻辑是对应的。 + +例如本题,如果想debug的话,打印日志可以这样写: + + +```CPP +class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是领接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[k] = 0; // 起始点到自身的距离为0 + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + // 遍历每个节点,选择未被访问的节点集合中哪个节点到源点的距离最小 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] <= minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 标记该顶点已被访问 + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + // 打印日志: + cout << "select:" << cur << endl; + for (int v = 1; v <= n; v++) cout << v << ":" << minDist[v] << " "; + cout << endl << endl;; + + + + } + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; + + +``` + +打印后的结果: + +``` +select:2 +1:1 2:0 3:1 4:2147483647 + +select:3 +1:1 2:0 3:1 4:2 + +select:1 +1:1 2:0 3:1 4:2 + +select:4 +1:1 2:0 3:1 4:2 +``` + +打印日志可以和上面我讲解的过程进行对比,每一步的结果是完全对应的。 + +所以如果大家如果代码有问题,打日志来debug是最好的方法 + +### 出现负数 + +如果图中边的权值为负数,dijkstra 还合适吗? + +看一下这个图: (有负权值) + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110217.png) + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为100,小于原minDist[2]的数值max,更新minDist[2] = 100 +* 源点到节点3的最短距离为1,小于原minDist[3]的数值max,更新minDist[4] = 1 + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点3最近,距离为1,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点3访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110711.png) + +-------------- + +至此dijkstra的模拟过程就结束了,根据最后的minDist数组,我们求 节点1 到 节点5 的最短路径的权值总和为 3,路径: 节点1 -> 节点3 -> 节点4 -> 节点5 + +通过以上的过程模拟,我们可以发现 之所以 没有走有负权值的最短路径 是因为 在 访问 节点 2 的时候,节点 3 已经访问过了,就不会再更新了。 + +那有录友可能会想: 我可以改代码逻辑啊,访问过的节点,也让它继续访问不就好了? + +那么访问过的节点还能继续访问会不会有死循环的出现呢?控制逻辑不让其死循环?那特殊情况自己能都想清楚吗?(可以试试,实践出真知) + +对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,**但最终会发现只是 拆了东墙补西墙**,对dijkstra的补充逻辑只能满足某特定场景最短路求解。 + +对于求解带有负权值的最短路问题,可以使用 Floyd 算法 ,我在后序会详细讲解。 + +## dijkstra与prim算法的区别 + +> 这里再次提示,需要先看我的 [prim算法精讲](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w) ,否则可能不知道我下面讲的是什么。 + +大家可以发现 dijkstra的代码看上去 怎么和 prim算法这么像呢。 + +其实代码大体不差,唯一区别在 三部曲中的 第三步: 更新minDist数组 + +因为**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**。 + +prim 更新 minDist数组的写法: + + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } +} +``` + +因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。 + +dijkstra 更新 minDist数组的写法: + +```CPP +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。 + +此时大家可能不禁要想 prim算法 可以有负权值吗? + +当然可以! + +录友们可以自己思考思考一下,这是为什么? + +这里我提示一下:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。 + + + +## 总结 + +本篇,我们深入讲解的dijkstra算法,详细模拟其工作的流程。 + +这里我给出了 **dijkstra 三部曲 来 帮助大家理解 该算法**,不至于 每次写 dijkstra 都是黑盒操作,没有框架没有章法。 + +在给出的代码中,我也按照三部曲的逻辑来给大家注释,只要理解这三部曲,即使 过段时间 对 dijkstra 算法有些遗忘,依然可以写出一个框架出来,然后再去调试细节。 + +对于图论算法,一般代码都比较长,很难写出代码直接可以提交通过,都需要一个debug的过程,所以 **学习如何debug 非常重要**! + +这也是我为什么 在本文中 单独用来讲解 debug方法。 + +本题求的是最短路径和是多少,**同时我们也要掌握 如何把最短路径打印出来**。 + +我还写了大篇幅来讲解 负权值的情况, 只有画图带大家一步一步去 看 出现负权值 dijkstra的求解过程,才能帮助大家理解,问题出在哪里。 + +如果我直接讲:是**因为访问过的节点 不能再访问,导致错过真正的最短路**,我相信大家都不知道我在说啥。 + +最后我还讲解了 dijkstra 和 prim 算法的 相同 与 不同之处, 我在图论的讲解安排中 先讲 prim算法 再讲 dijkstra 是有目的的, **理解这两个算法的相同与不同之处 有助于大家学习的更深入**。 + +而不是 学了 dijkstra 就只看 dijkstra, 算法之间 都是有联系的,多去思考 算法之间的相互联系,会帮助大家思考的更深入,掌握的更彻底。 + +本篇写了这么长,我也只讲解了 朴素版dijkstra,**关于 堆优化dijkstra,我会在下一篇再来给大家详细讲解**。 + + + +## 堆优化版本dijkstra + +> 本篇我们来讲解 堆优化版dijkstra,看本篇之前,一定要先看 我讲解的 朴素版dijkstra,否则本篇会有部分内容看不懂。 + +在上一篇中,我们讲解了朴素版的dijkstra,该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。 + +如果n很大的话,我们可以换一个角度来优先性能。 + +在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)(从点的角度来求最小生成树)、[Kruskal算法](https://mp.weixin.qq.com/s/rUVaBjCES_4eSjngceT5bw)(从边的角度来求最小生成树) + +这么在n 很大的时候,也有另一个思考维度,即:从边的数量出发。 + +当 n 很大,边 的数量 也很多的时候(稠密图),那么 上述解法没问题。 + +但 n 很大,边 的数量 很小的时候(稀疏图),是不是可以换成从边的角度来求最短路呢? + +毕竟边的数量少。 + +有的录友可能会想,n (节点数量)很大,边不就多吗? 怎么会边的数量少呢? + +别忘了,谁也没有规定 节点之间一定要有边连接着,例如有一万个节点,只有一条边,这也是一张图。 + +了解背景之后,再来看 解法思路。 + +### 图的存储 + +首先是 图的存储。 + +关于图的存储 主流有两种方式: 邻接矩阵和邻接表 + +#### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 链接 节点5 为有向图,节点2 指向 节点5,边的权值为6 (套在题意里,可能是距离为6 或者 消耗为6 等等) + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4,节点4指向节点1。 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的链接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点链接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点链接其他节点的数量。 +* 实现相对复杂,不易理解 + +#### 本题图的存储 + +接下来我们继续按照稀疏图的角度来分析本题。 + +在第一个版本的实现思路中,我们提到了三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +在第一个版本的代码中,这三部曲是套在一个 for 循环里,为什么? + +因为我们是从节点的角度来解决问题。 + +三部曲中第一步(选源点到哪个节点近且该节点未被访问过),这个操作本身需要for循环遍历 minDist 来寻找最近的节点。 + +同时我们需要 遍历所有 未访问过的节点,所以 我们从 节点角度出发,代码会有两层for循环,代码是这样的: (注意代码中的注释,标记两层for循环的用处) + +```CPP + +for (int i = 1; i <= n; i++) { // 遍历所有节点,第一层for循环 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 , 第二层for循环 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + +} +``` + +那么当从 边 的角度出发, 在处理 三部曲里的第一步(选源点到哪个节点近且该节点未被访问过)的时候 ,我们可以不用去遍历所有节点了。 + +而且 直接把 边(带权值)加入到 小顶堆(利用堆来自动排序),那么每次我们从 堆顶里 取出 边 自然就是 距离源点最近的节点所在的边。 + +这样我们就不需要两层for循环来寻找最近的节点了。 + +了解了大体思路,我们再来看代码实现。 + +首先是 如何使用 邻接表来表述图结构,这是摆在很多录友面前的第一个难题。 + +邻接表用 数组+链表 来表示,代码如下:(C++中 vector 为数组,list 为链表, 定义了 n+1 这么大的数组空间) + +```CPP +vector> grid(n + 1); +``` + +不少录友,不知道 如何定义的数据结构,怎么表示邻接表的,我来给大家画一个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +图中邻接表表示: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4 指向 节点1 + +大家发现图中的边没有权值,而本题中 我们的边是有权值的,权值怎么表示?在哪里表示? + +所以 在`vector> grid(n + 1);` 中 就不能使用int了,而是需要一个键值对 来存两个数字,一个数表示节点,一个数表示 指向该节点的这条边的权值。 + +那么 代码可以改成这样: (pair 为键值对,可以存放两个int) + +```CPP +vector>> grid(n + 1); +``` + +举例来给大家展示 该代码表达的数据 如下: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103904.png) + +* 节点1 指向 节点3 权值为 1 +* 节点1 指向 节点5 权值为 2 +* 节点2 指向 节点4 权值为 7 +* 节点2 指向 节点3 权值为 6 +* 节点2 指向 节点5 权值为 3 +* 节点3 指向 节点4 权值为 3 +* 节点5 指向 节点1 权值为 10 + +这样 我们就把图中权值表示出来了。 + +但是在代码中 使用 `pair` 很容易让我们搞混了,第一个int 表示什么,第二个int表示什么,导致代码可读性很差,或者说别人看你的代码看不懂。 + +那么 可以 定一个类 来取代 `pair` + +类(或者说是结构体)定义如下: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +这个类里有两个成员变量,有对应的命名,这样不容易搞混 两个int的含义。 + +所以 本题中邻接表的定义如下: + +```CPP +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +vector> grid(n + 1); // 邻接表 + +``` + +(我们在下面的讲解中会直接使用这个邻接表的代码表示方式) + +### 堆优化细节 + +其实思路依然是 dijkstra 三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +只不过之前是 通过遍历节点来遍历边,通过两层for循环来寻找距离源点最近节点。 这次我们直接遍历边,且通过堆来对边进行排序,达到直接选择距离源点最近节点。 + +先来看一下针对这三部曲,如果用 堆来优化。 + +那么三部曲中的第一步(选源点到哪个节点近且该节点未被访问过),我们如何选? + +我们要选择距离源点近的节点(即:该边的权值最小),所以 我们需要一个 小顶堆 来帮我们对边的权值排序,每次从小顶堆堆顶 取边就是权值最小的边。 + +C++定义小顶堆,可以用优先级队列实现,代码如下: + +```CPP +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 优先队列中存放 pair<节点编号,源点到该节点的权值> +priority_queue, vector>, mycomparison> pq; +``` + +(`pair`中 第二个int 为什么要存 源点到该节点的权值,因为 这个小顶堆需要按照权值来排序) + + +有了小顶堆自动对边的权值排序,那我们只需要直接从 堆里取堆顶元素(小顶堆中,最小的权值在上面),就可以取到离源点最近的节点了 (未访问过的节点,不会加到堆里进行排序) + +所以三部曲中的第一步,我们不用 for循环去遍历,直接取堆顶元素: + +```CPP +// pair<节点编号,源点到该节点的权值> +pair cur = pq.top(); pq.pop(); + +``` + +第二步(该最近节点被标记访问过) 这个就是将 节点做访问标记,和 朴素dijkstra 一样 ,代码如下: + +```CPP +// 2. 第二步,该最近节点被标记访问过 +visited[cur.first] = true; + +``` + +(`cur.first` 是指取 `pair` 里的第一个int,即节点编号 ) + +第三步(更新非访问节点到源点的距离),这里的思路 也是 和朴素dijkstra一样的。 + +但很多录友对这里是最懵的,主要是因为两点: + +* 没有理解透彻 dijkstra 的思路 +* 没有理解 邻接表的表达方式 + +我们来回顾一下 朴素dijkstra 在这一步的代码和思路(如果没看过我讲解的朴素版dijkstra,这里会看不懂) + +```CPP + +// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +其中 for循环是用来做什么的? 是为了 找到 节点cur 链接指向了哪些节点,因为使用邻接矩阵的表达方式 所以把所有节点遍历一遍。 + +而在邻接表中,我们可以以相对高效的方式知道一个节点链接指向哪些节点。 + +再回顾一下邻接表的构造(数组 + 链表): + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +假如 加入的cur 是节点 2, 那么 grid[2] 表示的就是图中第二行链表。 (grid数组的构造我们在 上面 「图的存储」中讲过) + +所以在邻接表中,我们要获取 节点cur 链接指向哪些节点,就是遍历 grid[cur节点编号] 这个链表。 + +这个遍历方式,C++代码如下: + +```CPP +for (Edge edge : grid[cur.first]) +``` + +(如果不知道 Edge 是什么,看上面「图的存储」中邻接表的讲解) + +`cur.first` 就是cur节点编号, 参考上面pair的定义: pair<节点编号,源点到该节点的权值> + +接下来就是更新 非访问节点到源点的距离,代码实现和 朴素dijkstra 是一样的,代码如下: + +```CPP +// 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } +} +``` + +但为什么思路一样,有的录友能写出朴素dijkstra,但堆优化这里的逻辑就是写不出来呢? + +**主要就是因为对邻接表的表达方式不熟悉**! + +以上代码中,cur 链接指向的节点编号 为 edge.to, 这条边的权值为 edge.val ,如果对这里模糊的就再回顾一下 Edge的定义: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +确定该节点没有被访问过,`!visited[edge.to]` , 目前 源点到cur.first的最短距离(minDist) + cur.first 到 edge.to 的距离 (edge.val) 是否 小于 minDist已经记录的 源点到 edge.to 的距离 (minDist[edge.to]) + +如果是的话,就开始更新操作。 + +即: + +```CPP +if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); // 由于cur节点的加入,而新链接的边,加入到优先级队里中 +} + +``` + +同时,由于cur节点的加入,源点又有可以新链接到的边,将这些边加入到优先级队里中。 + + +以上代码思路 和 朴素版dijkstra 是一样一样的,主要区别是两点: + +* 邻接表的表示方式不同 +* 使用优先级队列(小顶堆)来对新链接的边排序 + +### 代码实现 + +堆优化dijkstra完整代码如下: + +```CPP +class Solution { +public: + // 小顶堆 + class mycomparison { + public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } + }; + // 定义一个结构体来表示带权重的边 + struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 + }; + int networkDelayTime(vector>& times, int n, int k) { + + + std::vector> grid(n + 1); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + // p1 指向 p2,权值为 times[i][2] + grid[p1].push_back(Edge(p2, times[i][2])); + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的距离> + priority_queue, vector>, mycomparison> pq; + + pq.push(pair(k, 0)); + minDist[k] = 0; // 这个不要忘了 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现) + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } + } + + } + + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; + +``` + +* 时间复杂度:O(ElogE) E 为边的数量 +* 空间复杂度:O(N + E) N 为节点的数量 + +堆优化的时间复杂度 只和边的数量有关 和节点数无关,在 优先级队列中 放的也是边。 + +以上代码中,`while (!pq.empty())` 里套了 `for (Edge edge : grid[cur.first])` + +`for` 里 遍历的是 当前节点 cur 所连接边。 + +那 当前节点cur 所连接的边 也是不固定的, 这就让大家分不清,这时间复杂度究竟是多少? + +其实 `for (Edge edge : grid[cur.first])` 里最终的数据走向 是 给队列里添加边。 + +那么跳出局部代码,整个队列 一定是 所有边添加了一次,同时也弹出了一次。 + +所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。 + +(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总是是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出) + +所以 该算法整体时间复杂度为 O(ElogE) + +网上的不少分析 会把 n (节点的数量)算进来,这个分析是有问题的,举一个极端例子,在n 为 10000,且是有一条边的 图里,以上代码,大家感觉执行了多少次? + +`while (!pq.empty())` 中的 pq 存的是边,其实只执行了一次。 + +所以该算法时间复杂度 和 节点没有关系。 + +至于空间复杂度,邻接表是 数组 + 链表 数组的空间 是 N ,有E条边 就申请对应多少个链表节点,所以是 复杂度是 N + E + +## 拓展 + +当然也有录友可能想 堆优化dijkstra 中 我为什么一定要用邻接表呢,我就用邻接矩阵 行不行 ? + +也行的。 + +但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的。 + +如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。 + +这里我也给出 邻接矩阵版本的堆优化dijkstra代码: + +```CPP +class Solution { +public: + // 小顶堆(按照中的v 来从小到大排序) + class mycomparison { + public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } + }; + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是邻接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 [节点,源点到该节点的距离] + priority_queue, vector>, mycomparison> pq; + + pq.push(pair(k, 0)); + minDist[k] = 0; // 这个不要忘了 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1、选距离源点最近且未访问过的节点 + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2、标记该节点已被访问 + visited[cur.first] = true; + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + // 遍历 cur 可以链接的节点,更新 minDist[j] + for (int j = 1; j <= n; j++) { + if (!visited[j] && grid[cur.first][j] != INT_MAX && (minDist[cur.first] + grid[cur.first][j] < minDist[j])) { + minDist[j] = minDist[cur.first] + grid[cur.first][j]; + pq.push(pair(j, minDist[j])); + } + } + } + + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + + + return result; + + } +}; + +``` + +* 时间复杂度:O(E * (N + logE)) E为边的数量,N为节点数量 +* 空间复杂度:O(log(N^2)) + +`while (!pq.empty())` 时间复杂度为 E ,while 里面 每次取元素 时间复杂度 为 logE,和 一个for循环 时间复杂度 为 N 。 + +所以整体是 E * (N + logE) + + +## 总结 + +在学习一种优化思路的时候,首先就要知道为什么要优化,遇到了什么问题。 + +正如我在开篇就给大家交代清楚 堆优化方式的背景。 + +堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度触发,且利用堆来排序。 + +很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。 + +主要是因为两点: + +* 不熟悉邻接表的表达方式 +* 对dijkstra的实现思路还是不熟 + +这是我为什么 本篇花了大力气来讲解 图的存储,就是为了让大家彻底理解邻接表以及邻接表的代码写法。 + +至于 dijkstra的实现思路 ,朴素版 和 堆优化版本 都是 按照 dijkstra 三部曲来的。 + +理解了三部曲,dijkstra 的思路就是清晰的。 + +针对邻接表版本代码 我做了详细的 时间复杂度分析,也让录友们清楚,相对于 朴素版,时间都优化到哪了。 + +最后 我也给出了 邻接矩阵的版本代码,分析了这一版本的必要性以及时间复杂度。 + +至此通过 两篇dijkstra的文章,终于把 dijkstra 讲完了,如果大家对我讲解里所涉及的内容都吃透的话,详细对 dijkstra 算法也就理解到位了。 + +这里在给出本题的Bellman_ford解法,关于 Bellman_ford ,后面我会专门来讲解的,Bellman_ford 有其独特的应用场景 + +```CPP +class Solution { +public: + + int networkDelayTime(vector>& times, int n, int k) { + vector minDist(n + 1 , INT_MAX/2); + minDist[k] = 0; + //vector minDist_copy(n); // 用来记录每一次遍历的结果 + for (int i = 1; i <= n + 1; i++) { + //minDist_copy = minDist; // 获取上一次计算的结果 + for (auto &f : times) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + if (minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + + } + int result = 0; + for (int i = 1;i <= n; i++) { + if (minDist[i] == INT_MAX/2) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; +``` + diff --git a/problems/0746.使用最小花费爬楼梯.md b/problems/0746.使用最小花费爬楼梯.md index d13ff19f..753a104d 100644 --- a/problems/0746.使用最小花费爬楼梯.md +++ b/problems/0746.使用最小花费爬楼梯.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -244,6 +244,24 @@ class Solution { } ``` +```Java +// 状态压缩,使用三个变量来代替数组 +class Solution { + public int minCostClimbingStairs(int[] cost) { + // 以下三个变量分别表示前两个台阶的最少费用、前一个的、当前的。 + int beforeTwoCost = 0, beforeOneCost = 0, currentCost = 0; + // 前两个台阶不需要费用就能上到,因此从下标2开始;因为最后一个台阶需要跨越,所以需要遍历到cost.length + for (int i = 2; i <= cost.length; i ++) { + // 此处遍历的是cost[i - 1],不会越界 + currentCost = Math.min(beforeOneCost + cost[i - 1], beforeTwoCost + cost[i - 2]); + beforeTwoCost = beforeOneCost; + beforeOneCost = currentCost; + } + return currentCost; + } +} +``` + ### Python 动态规划(版本一) @@ -519,8 +537,8 @@ public class Solution +

- diff --git a/problems/0763.划分字母区间.md b/problems/0763.划分字母区间.md index 8b0ca7b8..52186927 100644 --- a/problems/0763.划分字母区间.md +++ b/problems/0763.划分字母区间.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 763.划分字母区间 diff --git a/problems/0787.K站中转内最便宜的航班.md b/problems/0787.K站中转内最便宜的航班.md new file mode 100644 index 00000000..9c0a8e7f --- /dev/null +++ b/problems/0787.K站中转内最便宜的航班.md @@ -0,0 +1,186 @@ +

+ + + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 787. K 站中转内最便宜的航班 + +有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。 + +现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。 + + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240319103900.png) + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240319103919.png) + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240319104026.png) + + +## 思路 + + + +```CPP +class Solution { +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + for (int i = 1; i <= k + 1; i++) { + minDist_copy = minDist; // 获取上一次计算的结果 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + minDist[to] = min(minDist_copy[from] + price, minDist[to]); + // if (minDist[to] > minDist_copy[from] + price) minDist[to] = minDist_copy[from] + price; + } + + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; +``` + +下面是典型的错误写法 + +```CPP +class Solution { +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + if (minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; +``` + + +----------- + +SPFA + + +class Solution { +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + vector> grid(n + 1); // 邻接表 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + grid[from].push_back(Edge(to, price)); + + } + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + k++; + queue que; + que.push(src); + std::vector visited(n + 1, false); // 可加,可不加,加了效率高一些,防止重复访问 + int que_size; + while (k-- && !que.empty()) { + + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); + while (que_size--) { // 这个while循环的设计实在是妙啊 + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + que.push(to); + } + } + + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; + + + +队列加上 visited 不能重复访问 + +class Solution { +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + vector> grid(n + 1); // 邻接表 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + grid[from].push_back(Edge(to, price)); + + } + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + k++; + queue que; + que.push(src); + int que_size; + while (k-- && !que.empty()) { + // 注意这个数组放的位置 + vector visited(n + 1, false); // 可加,可不加,加了效率高一些,防止队列里重复访问,其数值已经算过了 + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); + while (que_size--) { + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + if(visited[to]) continue; // 不用重复放入队列,但需要重复计算,所以放在这里位置 + visited[to] = true; + que.push(to); + } + } + + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; + + + +

+ + + diff --git a/problems/0797.所有可能的路径.md b/problems/0797.所有可能的路径.md index 05b55b5b..40e1bbe7 100644 --- a/problems/0797.所有可能的路径.md +++ b/problems/0797.所有可能的路径.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 797.所有可能的路径 @@ -52,7 +52,7 @@ graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节 至于 单一路径,和路径集合可以放在全局变量,那么代码是这样的: -```c++ +```CPP vector> result; // 收集符合条件的路径 vector path; // 0节点到终点的路径 // x:目前遍历的节点 @@ -71,7 +71,7 @@ void dfs (vector>& graph, int x) 所以 但 x 等于 graph.size() - 1 的时候就找到一条有效路径。 代码如下: -```c++ +```CPP // 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1 if (x == graph.size() - 1) { // 找到符合条件的一条路径 result.push_back(path); // 收集有效路径 @@ -104,13 +104,13 @@ path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 进入下一层递归 -```C++ +```CPP dfs(graph, graph[x][i]); // 进入下一层递归 ``` 最后就是回溯的过程,撤销本次添加节点的操作。 该过程整体代码: -```C++ +```CPP for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点 path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 dfs(graph, graph[x][i]); // 进入下一层递归 @@ -120,7 +120,7 @@ for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节 本题整体代码如下: -```c++ +```CPP class Solution { private: vector> result; // 收集符合条件的路径 diff --git a/problems/0827.最大人工岛.md b/problems/0827.最大人工岛.md index 7930c7a1..e452e4e3 100644 --- a/problems/0827.最大人工岛.md +++ b/problems/0827.最大人工岛.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 827.最大人工岛 diff --git a/problems/0841.钥匙和房间.md b/problems/0841.钥匙和房间.md index b4785d1b..b78693b4 100644 --- a/problems/0841.钥匙和房间.md +++ b/problems/0841.钥匙和房间.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/0844.比较含退格的字符串.md b/problems/0844.比较含退格的字符串.md index c7f52202..c32cdd33 100644 --- a/problems/0844.比较含退格的字符串.md +++ b/problems/0844.比较含退格的字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 844.比较含退格的字符串 @@ -587,8 +587,8 @@ impl Solution { } ``` +

- diff --git a/problems/0860.柠檬水找零.md b/problems/0860.柠檬水找零.md index db70112d..804ff13c 100644 --- a/problems/0860.柠檬水找零.md +++ b/problems/0860.柠檬水找零.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 860.柠檬水找零 @@ -439,8 +439,8 @@ public class Solution ``` +

- diff --git a/problems/0922.按奇偶排序数组II.md b/problems/0922.按奇偶排序数组II.md index 72be8fa7..1ac6800c 100644 --- a/problems/0922.按奇偶排序数组II.md +++ b/problems/0922.按奇偶排序数组II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -378,8 +378,8 @@ function sortArrayByParityII(nums: number[]): number[] { }; ``` +

- diff --git a/problems/0925.长按键入.md b/problems/0925.长按键入.md index 11edecd0..f4a8fa8e 100644 --- a/problems/0925.长按键入.md +++ b/problems/0925.长按键入.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 925.长按键入 diff --git a/problems/0941.有效的山脉数组.md b/problems/0941.有效的山脉数组.md index 48c29eb4..9f63f441 100644 --- a/problems/0941.有效的山脉数组.md +++ b/problems/0941.有效的山脉数组.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 941.有效的山脉数组 @@ -197,8 +197,8 @@ public class Solution { ``` +

- diff --git a/problems/0968.监控二叉树.md b/problems/0968.监控二叉树.md index 9743ca2b..d59496c8 100644 --- a/problems/0968.监控二叉树.md +++ b/problems/0968.监控二叉树.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ diff --git a/problems/0977.有序数组的平方.md b/problems/0977.有序数组的平方.md index bb311ca2..b10620d0 100644 --- a/problems/0977.有序数组的平方.md +++ b/problems/0977.有序数组的平方.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 双指针风骚起来,也是无敌 @@ -199,6 +199,17 @@ class Solution: ### Go: ```Go +// 排序法 +func sortedSquares(nums []int) []int { + for i, val := range nums { + nums[i] *= val + } + sort.Ints(nums) + return nums +} +``` +```Go +// 双指针法 func sortedSquares(nums []int) []int { n := len(nums) i, j, k := 0, n-1, n-1 @@ -525,8 +536,8 @@ public class Solution { } } ``` +

- diff --git a/problems/1002.查找常用字符.md b/problems/1002.查找常用字符.md index 8d81e3f8..f938c2b7 100644 --- a/problems/1002.查找常用字符.md +++ b/problems/1002.查找常用字符.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -581,8 +581,8 @@ def common_chars(words) end ``` +

- diff --git a/problems/1005.K次取反后最大化的数组和.md b/problems/1005.K次取反后最大化的数组和.md index 498015d0..fa27d3b7 100644 --- a/problems/1005.K次取反后最大化的数组和.md +++ b/problems/1005.K次取反后最大化的数组和.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1005.K次取反后最大化的数组和 diff --git a/problems/1020.飞地的数量.md b/problems/1020.飞地的数量.md index 3488777a..59610c68 100644 --- a/problems/1020.飞地的数量.md +++ b/problems/1020.飞地的数量.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1020. 飞地的数量 @@ -695,8 +695,8 @@ impl Solution { * 1254. 统计封闭岛屿的数目 +

- diff --git a/problems/1035.不相交的线.md b/problems/1035.不相交的线.md index e0625a2b..8ee52c5d 100644 --- a/problems/1035.不相交的线.md +++ b/problems/1035.不相交的线.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1035.不相交的线 @@ -273,8 +273,8 @@ function maxUncrossedLines(nums1: number[], nums2: number[]): number { ``` +

- diff --git a/problems/1047.删除字符串中的所有相邻重复项.md b/problems/1047.删除字符串中的所有相邻重复项.md index ffe13530..4aa0e954 100644 --- a/problems/1047.删除字符串中的所有相邻重复项.md +++ b/problems/1047.删除字符串中的所有相邻重复项.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 匹配问题都是栈的强项 @@ -495,8 +495,8 @@ def remove_duplicates(s) end ``` +

- diff --git a/problems/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md index 4c3c01a0..4f2cc9e3 100644 --- a/problems/1049.最后一块石头的重量II.md +++ b/problems/1049.最后一块石头的重量II.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1049.最后一块石头的重量II @@ -497,8 +497,8 @@ public class Solution } ``` +

- diff --git a/problems/1143.最长公共子序列.md b/problems/1143.最长公共子序列.md index 12bd90f8..7fa7bb68 100644 --- a/problems/1143.最长公共子序列.md +++ b/problems/1143.最长公共子序列.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1143.最长公共子序列 diff --git a/problems/1207.独一无二的出现次数.md b/problems/1207.独一无二的出现次数.md index 2ccd30c3..5c5f92c3 100644 --- a/problems/1207.独一无二的出现次数.md +++ b/problems/1207.独一无二的出现次数.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1207.独一无二的出现次数 @@ -224,8 +224,8 @@ func uniqueOccurrences(arr []int) bool { +

- diff --git a/problems/1221.分割平衡字符串.md b/problems/1221.分割平衡字符串.md index 2a7b0922..a32ca98f 100644 --- a/problems/1221.分割平衡字符串.md +++ b/problems/1221.分割平衡字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1221. 分割平衡字符串 diff --git a/problems/1254.统计封闭岛屿的数目.md b/problems/1254.统计封闭岛屿的数目.md index dc8fda41..3d7b9fe9 100644 --- a/problems/1254.统计封闭岛屿的数目.md +++ b/problems/1254.统计封闭岛屿的数目.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1254. 统计封闭岛屿的数目 @@ -134,9 +134,9 @@ var closedIsland = function(grid) { ``` + +

- - diff --git a/problems/1334.阈值距离内邻居最少的城市.md b/problems/1334.阈值距离内邻居最少的城市.md new file mode 100644 index 00000000..d8d8861f --- /dev/null +++ b/problems/1334.阈值距离内邻居最少的城市.md @@ -0,0 +1,55 @@ +

+ + + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +floyd + + +class Solution { +public: + int findTheCity(int n, vector>& edges, int distanceThreshold) { + vector> grid(n, vector(n, 10005)); // 因为边的最大距离是10^4 + + // 节点到自己的距离为0 + for (int i = 0; i < n; i++) grid[i][i] = 0; + // 构造邻接矩阵 + for (const vector& e : edges) { + int from = e[0]; + int to = e[1]; + int val = e[2]; + grid[from][to] = val; + grid[to][from] = val; // 注意这里是双向图 + } + + // 开始 floyd + // 思考 为什么 p 要放在最外面一层 + for (int p = 0; p < n; p++) { + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = min(grid[i][j], grid[i][p] + grid[p][j]); + } + } + } + + int result = 0; + int count = n + 10; // 记录所有城市在范围内连接的最小城市数量 + for (int i = 0; i < n; i++) { + int curCount = 0; // 统计一个城市在范围内可以连接几个城市 + for (int j = 0; j < n; j++) { + if (i != j && grid[i][j] <= distanceThreshold) curCount++; + // cout << "i:" << i << ", j:" << j << ", val: " << grid[i][j] << endl; + } + if (curCount <= count) { // 注意这里是 <= + count = curCount; + result = i; + } + } + return result; + } +}; +

+ + + diff --git a/problems/1356.根据数字二进制下1的数目排序.md b/problems/1356.根据数字二进制下1的数目排序.md index c2455cf0..9cfb6743 100644 --- a/problems/1356.根据数字二进制下1的数目排序.md +++ b/problems/1356.根据数字二进制下1的数目排序.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -216,8 +216,8 @@ var sortByBits = function(arr) { ``` +

- diff --git a/problems/1365.有多少小于当前数字的数字.md b/problems/1365.有多少小于当前数字的数字.md index c706ba21..94c1eb77 100644 --- a/problems/1365.有多少小于当前数字的数字.md +++ b/problems/1365.有多少小于当前数字的数字.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

diff --git a/problems/1382.将二叉搜索树变平衡.md b/problems/1382.将二叉搜索树变平衡.md index 57e56b8f..120cafff 100644 --- a/problems/1382.将二叉搜索树变平衡.md +++ b/problems/1382.将二叉搜索树变平衡.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1382.将二叉搜索树变平衡 @@ -217,8 +217,8 @@ function buildTree(arr: number[], left: number, right: number): TreeNode | null +

- diff --git a/problems/1791.找出星型图的中心节点.md b/problems/1791.找出星型图的中心节点.md index 9bcc7ef9..e3db7947 100644 --- a/problems/1791.找出星型图的中心节点.md +++ b/problems/1791.找出星型图的中心节点.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1791.找出星型图的中心节点 diff --git a/problems/1971.寻找图中是否存在路径.md b/problems/1971.寻找图中是否存在路径.md index 132b0181..24e81992 100644 --- a/problems/1971.寻找图中是否存在路径.md +++ b/problems/1971.寻找图中是否存在路径.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 1971. 寻找图中是否存在路径 @@ -16,16 +16,7 @@ ![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220705101442.png) -提示: -- 1 <= n <= 2 \* 10^5 -- 0 <= edges.length <= 2 \* 10^5 -- edges[i].length == 2 -- 0 <= ui, vi <= n - 1 -- ui != vi -- 0 <= start, end <= n - 1 -- 不存在双向边 -- 不存在指向顶点自身的边 ## 思路 @@ -284,6 +275,48 @@ var validPath = function(n, edges, source, destination) { }; ``` +### Go + +```go +func validPath(n int, edges [][]int, source int, destination int) bool { + n = 200005 + father := make([]int, n) + // 并查集初始化 + for i := 0; i < n; i++ { + father[i] = i + } + + var find func(u int) int // 并查集里寻根的过程 + find = func(u int) int { + // 如果根就是自己,直接返回 + // 如果根不是自己,就根据数组下标一层一层向下找 + if u == father[u] { + return u + } + return find(father[u]) + } + + var join func(u, v int) // 将 v->u 这条边加入并查集 + join = func(u, v int) { + u = find(u) + v = find(v) + if u == v { + return + } + father[v] = u + } + + for i := 0; i < len(edges); i++ { + join(edges[i][0], edges[i][1]) + } + + source = find(source) + destination = find(destination) + return source == destination +} + +``` +

diff --git a/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md index 8be48f38..a5dab942 100644 --- a/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md +++ b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 程序提交之后为什么会超时?O(n)的算法会超时,n究竟是多大? @@ -223,8 +223,8 @@ int main() { +

- diff --git a/problems/kamacoder/00.软件构建.md b/problems/kamacoder/00.软件构建.md new file mode 100644 index 00000000..7229489b --- /dev/null +++ b/problems/kamacoder/00.软件构建.md @@ -0,0 +1,337 @@ + +# 拓扑排序精讲 + +[卡码网:软件构建](https://kamacoder.com/problempage.php?pid=1191) + +题目描述: + +某个大型软件项目的构建系统拥有 N 个文件,文件编号从 0 到 N - 1,在这些文件中,某些文件依赖于其他文件的内容,这意味着如果文件 A 依赖于文件 B,则必须在处理文件 A 之前处理文件 B (0 <= A, B <= N - 1)。请编写一个算法,用于确定文件处理的顺序。 + +输入描述: + +第一行输入两个正整数 M, N。表示 N 个文件之间拥有 M 条依赖关系。 + +后续 M 行,每行两个正整数 S 和 T,表示 T 文件依赖于 S 文件。 + +输出描述: + +输出共一行,如果能处理成功,则输出文件顺序,用空格隔开。 + +如果不能成功处理(相互依赖),则输出 -1。 + +输入示例: + +``` +5 4 +0 1 +0 2 +1 3 +2 4 +``` + +输出示例: + +0 1 2 3 4 + +提示信息: + +文件依赖关系如下: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510192157.png) + +所以,文件处理的顺序除了示例中的顺序,还存在 + +0 2 4 1 3 + +0 2 1 3 4 + +等等合法的顺序。 + +数据范围: + +* 0 <= N <= 10 ^ 5 +* 1 <= M <= 10 ^ 9 + + +## 拓扑排序的背景 + +本题是拓扑排序的经典题目。 + +一聊到 拓扑排序,一些录友可能会想这是排序,不会想到这是图论算法。 + +其实拓扑排序是经典的图论问题。 + +先说说 拓扑排序的应用场景。 + +大学排课,例如 先上A课,才能上B课,上了B课才能上C课,上了A课才能上D课,等等一系列这样的依赖顺序。 问给规划出一条 完整的上课顺序。 + +拓扑排序在文件处理上也有应用,我们在做项目安装文件包的时候,经常发现 复杂的文件依赖关系, A依赖B,B依赖C,B依赖D,C依赖E 等等。 + +如果给出一条线性的依赖顺序来下载这些文件呢? + +有录友想上面的例子都很简单啊,我一眼能给排序出来。 + +那如果上面的依赖关系是一百对呢,一千对甚至上万个依赖关系,这些依赖关系中可能还有循环依赖,你如何发现循环依赖呢,又如果排出线性顺序呢。 + +所以 拓扑排序就是专门解决这类问题的。 + +概括来说,**给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序**。 + +当然拓扑排序也要检测这个有向图 是否有环,即存在循环依赖的情况,因为这种情况是不能做线性排序的。 + +所以**拓扑排序也是图论中判断有向无环图的常用方法**。 + +------------ + + +## 拓扑排序的思路 + +拓扑排序指的是一种 解决问题的大体思路, 而具体算法,可能是广搜也可能是深搜。 + +大家可能发现 各式各样的解法,纠结哪个是拓扑排序? + +其实只要能在把 有向无环图 进行线性排序 的算法 都可以叫做 拓扑排序。 + +实现拓扑排序的算法有两种:卡恩算法(BFS)和DFS + +> 卡恩1962年提出这种解决拓扑排序的思路 + +一般来说我们只需要掌握 BFS (广度优先搜索)就可以了,清晰易懂,如果还想多了解一些,可以再去学一下 DFS 的思路,但 DFS 不是本篇重点。 + +接下来我们来讲解BFS的实现思路。 + +以题目中示例为例如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510110836.png) + +做拓扑排序的话,如果肉眼去找开头的节点,一定能找到 节点0 吧,都知道要从节点0 开始。 + +但为什么我们能找到 节点0呢,因为我们肉眼看着 这个图就是从 节点0出发的。 + +作为出发节点,它有什么特征? + +你看节点0 的入度 为0 出度为2, 也就是 没有边指向它,而它有两条边是指出去的。 + +> 节点的入度表示 有多少条边指向它,节点的出度表示有多少条边 从该节点出发。 + +所以当我们做拓扑排序的时候,应该优先找 入度为 0 的节点,只有入度为0,它才是出发节点。 +**理解以上内容很重要**! + +接下来我给出 拓扑排序的过程,其实就两步: + +1. 找到入度为0 的节点,加入结果集 +2. 将该节点从图中移除 + +循环以上两步,直到 所有节点都在图中被移除了。 + +结果集的顺序,就是我们想要的拓扑排序顺序 (结果集里顺序可能不唯一) + +## 模拟过程 + +用本题的示例来模拟一下这一过程: + + +1、找到入度为0 的节点,加入结果集 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113110.png) + +2、将该节点从图中移除 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113142.png) + +---------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113345.png) + +这里大家会发现,节点1 和 节点2 入度都为0, 选哪个呢? + +选哪个都行,所以这也是为什么拓扑排序的结果是不唯一的。 + +2、将该节点从图中移除 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113640.png) + +--------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113853.png) + +节点2 和 节点3 入度都为0,选哪个都行,这里选节点2 + +2、将该节点从图中移除 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510114004.png) + +-------------- + +后面的过程一样的,节点3 和 节点4,入度都为0,选哪个都行。 + +最后结果集为: 0 1 2 3 4 。当然结果不唯一的。 + +## 判断有环 + +如果有 有向环怎么办呢?例如这个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510115115.png) + +这个图,我们只能将入度为0 的节点0 接入结果集。 + +之后,节点1、2、3、4 形成了环,找不到入度为0 的节点了,所以此时结果集里只有一个元素。 +那么如果我们发现结果集元素个数 不等于 图中节点个数,我们就可以认定图中一定有 有向环! +这也是拓扑排序判断有向环的方法。 + +通过以上过程的模拟大家会发现这个拓扑排序好像不难,还有点简单。 + +## 写代码 + +理解思想后,确实不难,但代码写起来也不容易。 + +为了每次可以找到所有节点的入度信息,我们要在初始话的时候,就把每个节点的入度 和 每个节点的依赖关系做统计。 + +代码如下: + +```CPP +cin >> n >> m; +vector inDegree(n, 0); // 记录每个文件的入度 +vector result; // 记录结果 +unordered_map> umap; // 记录文件依赖关系 + +while (m--) { + // s->t,先有s才能有t + cin >> s >> t; + inDegree[t]++; // t的入度加一 + umap[s].push_back(t); // 记录s指向哪些文件 +} + +``` + +找入度为0 的节点,我们需要用一个队列放存放。 + +因为每次寻找入度为0的节点,不一定只有一个节点,可能很多节点入度都为0,所以要将这些入度为0的节点放到队列里,依次去处理。 + +代码如下: + +```CPP + +queue que; +for (int i = 0; i < n; i++) { + // 入度为0的节点,可以作为开头,先加入队列 + if (inDegree[i] == 0) que.push(i); +} +``` + +开始从队列里遍历入度为0 的节点,将其放入结果集。 + +```CPP + +while (que.size()) { + int cur = que.front(); // 当前选中的节点 + que.pop(); + result.push_back(cur); + // 将该节点从图中移除 + +} +``` + +这里面还有一个很重要的过程,如何把这个入度为0的节点从图中移除呢? + +首先我们为什么要把节点从图中移除? + +为的是将 该节点作为出发点所连接的边删掉。 + +删掉的目的是什么呢? + +要把 该节点作为出发点所连接的节点的 入度 减一。 + +如果这里不理解,看上面的模拟过程第一步: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113110.png) + +这事节点1 和 节点2 的入度为 1。 + +将节点0删除后,图为这样: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510113142.png) + +那么 节点0 作为出发点 所连接的节点的入度 就都做了 减一 的操作。 + +此时 节点1 和 节点 2 的入度都为0, 这样才能作为下一轮选取的节点。 + +所以,我们在代码实现的过程中,本质是要将 该节点作为出发点所连接的节点的 入度 减一 就可以了,这样好能根据入度找下一个节点,不用真在图里把这个节点删掉。 + +该过程代码如下: + + +```CPP + +while (que.size()) { + int cur = que.front(); // 当前选中的节点 + que.pop(); + result.push_back(cur); + // 将该节点从图中移除 + vector files = umap[cur]; //获取cur指向的节点 + if (files.size()) { // 如果cur有指向的节点 + for (int i = 0; i < files.size(); i++) { // 遍历cur指向的节点 + inDegree[files[i]] --; // cur指向的节点入度都做减一操作 + // 如果指向的节点减一之后,入度为0,说明是我们要选取的下一个节点,放入队列。 + if(inDegree[files[i]] == 0) que.push(files[i]); + } + } + +} +``` + +最后代码如下: + + +```CPP +#include +#include +#include +#include +using namespace std; +int main() { + int m, n, s, t; + cin >> n >> m; + vector inDegree(n, 0); // 记录每个节点的入度 + + unordered_map> umap;// 记录节点依赖关系 + vector result; // 记录结果 + + while (m--) { + // s->t,先有s才能有t + cin >> s >> t; + inDegree[t]++; // t的入度加一 + umap[s].push_back(t); // 记录s指向哪些节点 + } + queue que; + for (int i = 0; i < n; i++) { + // 入度为0的节点,可以作为开头,先加入队列 + if (inDegree[i] == 0) que.push(i); + //cout << inDegree[i] << endl; + } + // int count = 0; + while (que.size()) { + int cur = que.front(); // 当前选中的节点 + que.pop(); + //count++; + result.push_back(cur); + vector files = umap[cur]; //获取该节点指向的节点 + if (files.size()) { // cur有后续节点 + for (int i = 0; i < files.size(); i++) { + inDegree[files[i]] --; // cur的指向的节点入度-1 + if(inDegree[files[i]] == 0) que.push(files[i]); + } + } + } + // 判断是否有有向环 + if (result.size() == n) { + // 注意输出格式,最后一个元素后面没有空格 + for (int i = 0; i < n - 2; i++) cout << result[i] << " "; + cout << result[n - 1]; + } else cout << -1 << endl; +} +``` diff --git a/problems/kamacoder/0047.参会dijkstra堆.md b/problems/kamacoder/0047.参会dijkstra堆.md new file mode 100644 index 00000000..b93138a7 --- /dev/null +++ b/problems/kamacoder/0047.参会dijkstra堆.md @@ -0,0 +1,651 @@ + +# dijkstra(堆优化版)精讲 + +[题目链接](https://kamacoder.com/problempage.php?pid=1047) + +【题目描述】 + +小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。 + +小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。 + +小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。 + +接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。 + +【输出描述】 + +输出一个整数,代表小明从起点到终点所花费的最小时间。 + +输入示例 + +``` +7 9 +1 2 1 +1 3 4 +2 3 2 +2 4 5 +3 4 2 +4 5 3 +2 6 4 +5 7 4 +6 7 9 +``` + +输出示例:12 + +【提示信息】 + +能够到达的情况: + +如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12,则输出 12。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + + +## 思路 + +> 本篇我们来讲解 堆优化版dijkstra,看本篇之前,一定要先看 我讲解的 朴素版dijkstra,否则本篇会有部分内容看不懂。 + +在上一篇中,我们讲解了朴素版的dijkstra,该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。 + +如果n很大的话,我们可以换一个角度来优先性能。 + +在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)(从点的角度来求最小生成树)、[Kruskal算法](https://mp.weixin.qq.com/s/rUVaBjCES_4eSjngceT5bw)(从边的角度来求最小生成树) + +这么在n 很大的时候,也有另一个思考维度,即:从边的数量出发。 + +当 n 很大,边 的数量 也很多的时候(稠密图),那么 上述解法没问题。 + +但 n 很大,边 的数量 很小的时候(稀疏图),是不是可以换成从边的角度来求最短路呢? + +毕竟边的数量少。 + +有的录友可能会想,n (节点数量)很大,边不就多吗? 怎么会边的数量少呢? + +别忘了,谁也没有规定 节点之间一定要有边连接着,例如有一万个节点,只有一条边,这也是一张图。 + +了解背景之后,再来看 解法思路。 + +### 图的存储 + +首先是 图的存储。 + +关于图的存储 主流有两种方式: 邻接矩阵和邻接表 + +#### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 链接 节点5 为有向图,节点2 指向 节点5,边的权值为6 (套在题意里,可能是距离为6 或者 消耗为6 等等) + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4,节点4指向节点1。 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的链接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点链接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点链接其他节点的数量。 +* 实现相对复杂,不易理解 + +#### 本题图的存储 + +接下来我们继续按照稀疏图的角度来分析本题。 + +在第一个版本的实现思路中,我们提到了三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +在第一个版本的代码中,这三部曲是套在一个 for 循环里,为什么? + +因为我们是从节点的角度来解决问题。 + +三部曲中第一步(选源点到哪个节点近且该节点未被访问过),这个操作本身需要for循环遍历 minDist 来寻找最近的节点。 + +同时我们需要 遍历所有 未访问过的节点,所以 我们从 节点角度出发,代码会有两层for循环,代码是这样的: (注意代码中的注释,标记两层for循环的用处) + +```CPP + +for (int i = 1; i <= n; i++) { // 遍历所有节点,第一层for循环 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 , 第二层for循环 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + +} +``` + +那么当从 边 的角度出发, 在处理 三部曲里的第一步(选源点到哪个节点近且该节点未被访问过)的时候 ,我们可以不用去遍历所有节点了。 + +而且 直接把 边(带权值)加入到 小顶堆(利用堆来自动排序),那么每次我们从 堆顶里 取出 边 自然就是 距离源点最近的节点所在的边。 + +这样我们就不需要两层for循环来寻找最近的节点了。 + +了解了大体思路,我们再来看代码实现。 + +首先是 如何使用 邻接表来表述图结构,这是摆在很多录友面前的第一个难题。 + +邻接表用 数组+链表 来表示,代码如下:(C++中 vector 为数组,list 为链表, 定义了 n+1 这么大的数组空间) + +```CPP +vector> grid(n + 1); +``` + +不少录友,不知道 如何定义的数据结构,怎么表示邻接表的,我来给大家画一个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +图中邻接表表示: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4 指向 节点1 + +大家发现图中的边没有权值,而本题中 我们的边是有权值的,权值怎么表示?在哪里表示? + +所以 在`vector> grid(n + 1);` 中 就不能使用int了,而是需要一个键值对 来存两个数字,一个数表示节点,一个数表示 指向该节点的这条边的权值。 + +那么 代码可以改成这样: (pair 为键值对,可以存放两个int) + +```CPP +vector>> grid(n + 1); +``` + +举例来给大家展示 该代码表达的数据 如下: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103904.png) + +* 节点1 指向 节点3 权值为 1 +* 节点1 指向 节点5 权值为 2 +* 节点2 指向 节点4 权值为 7 +* 节点2 指向 节点3 权值为 6 +* 节点2 指向 节点5 权值为 3 +* 节点3 指向 节点4 权值为 3 +* 节点5 指向 节点1 权值为 10 + +这样 我们就把图中权值表示出来了。 + +但是在代码中 使用 `pair` 很容易让我们搞混了,第一个int 表示什么,第二个int表示什么,导致代码可读性很差,或者说别人看你的代码看不懂。 + +那么 可以 定一个类 来取代 `pair` + +类(或者说是结构体)定义如下: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +这个类里有两个成员变量,有对应的命名,这样不容易搞混 两个int的含义。 + +所以 本题中邻接表的定义如下: + +```CPP +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +vector> grid(n + 1); // 邻接表 + +``` + +(我们在下面的讲解中会直接使用这个邻接表的代码表示方式) + +### 堆优化细节 + +其实思路依然是 dijkstra 三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +只不过之前是 通过遍历节点来遍历边,通过两层for循环来寻找距离源点最近节点。 这次我们直接遍历边,且通过堆来对边进行排序,达到直接选择距离源点最近节点。 + +先来看一下针对这三部曲,如果用 堆来优化。 + +那么三部曲中的第一步(选源点到哪个节点近且该节点未被访问过),我们如何选? + +我们要选择距离源点近的节点(即:该边的权值最小),所以 我们需要一个 小顶堆 来帮我们对边的权值排序,每次从小顶堆堆顶 取边就是权值最小的边。 + +C++定义小顶堆,可以用优先级队列实现,代码如下: + +```CPP +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 优先队列中存放 pair<节点编号,源点到该节点的权值> +priority_queue, vector>, mycomparison> pq; +``` + +(`pair`中 第二个int 为什么要存 源点到该节点的权值,因为 这个小顶堆需要按照权值来排序) + + +有了小顶堆自动对边的权值排序,那我们只需要直接从 堆里取堆顶元素(小顶堆中,最小的权值在上面),就可以取到离源点最近的节点了 (未访问过的节点,不会加到堆里进行排序) + +所以三部曲中的第一步,我们不用 for循环去遍历,直接取堆顶元素: + +```CPP +// pair<节点编号,源点到该节点的权值> +pair cur = pq.top(); pq.pop(); + +``` + +第二步(该最近节点被标记访问过) 这个就是将 节点做访问标记,和 朴素dijkstra 一样 ,代码如下: + +```CPP +// 2. 第二步,该最近节点被标记访问过 +visited[cur.first] = true; + +``` + +(`cur.first` 是指取 `pair` 里的第一个int,即节点编号 ) + +第三步(更新非访问节点到源点的距离),这里的思路 也是 和朴素dijkstra一样的。 + +但很多录友对这里是最懵的,主要是因为两点: + +* 没有理解透彻 dijkstra 的思路 +* 没有理解 邻接表的表达方式 + +我们来回顾一下 朴素dijkstra 在这一步的代码和思路(如果没看过我讲解的朴素版dijkstra,这里会看不懂) + +```CPP + +// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +其中 for循环是用来做什么的? 是为了 找到 节点cur 链接指向了哪些节点,因为使用邻接矩阵的表达方式 所以把所有节点遍历一遍。 + +而在邻接表中,我们可以以相对高效的方式知道一个节点链接指向哪些节点。 + +再回顾一下邻接表的构造(数组 + 链表): + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +假如 加入的cur 是节点 2, 那么 grid[2] 表示的就是图中第二行链表。 (grid数组的构造我们在 上面 「图的存储」中讲过) + +所以在邻接表中,我们要获取 节点cur 链接指向哪些节点,就是遍历 grid[cur节点编号] 这个链表。 + +这个遍历方式,C++代码如下: + +```CPP +for (Edge edge : grid[cur.first]) +``` + +(如果不知道 Edge 是什么,看上面「图的存储」中邻接表的讲解) + +`cur.first` 就是cur节点编号, 参考上面pair的定义: pair<节点编号,源点到该节点的权值> + +接下来就是更新 非访问节点到源点的距离,代码实现和 朴素dijkstra 是一样的,代码如下: + +```CPP +// 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } +} +``` + +但为什么思路一样,有的录友能写出朴素dijkstra,但堆优化这里的逻辑就是写不出来呢? + +**主要就是因为对邻接表的表达方式不熟悉**! + +以上代码中,cur 链接指向的节点编号 为 edge.to, 这条边的权值为 edge.val ,如果对这里模糊的就再回顾一下 Edge的定义: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +确定该节点没有被访问过,`!visited[edge.to]` , 目前 源点到cur.first的最短距离(minDist) + cur.first 到 edge.to 的距离 (edge.val) 是否 小于 minDist已经记录的 源点到 edge.to 的距离 (minDist[edge.to]) + +如果是的话,就开始更新操作。 + +即: + +```CPP +if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); // 由于cur节点的加入,而新链接的边,加入到优先级队里中 +} + +``` + +同时,由于cur节点的加入,源点又有可以新链接到的边,将这些边加入到优先级队里中。 + + +以上代码思路 和 朴素版dijkstra 是一样一样的,主要区别是两点: + +* 邻接表的表示方式不同 +* 使用优先级队列(小顶堆)来对新链接的边排序 + +### 代码实现 + +堆优化dijkstra完整代码如下: + +```CPP +#include +#include +#include +#include +#include +using namespace std; +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 定义一个结构体来表示带权重的边 +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的权值> + priority_queue, vector>, mycomparison> pq; + + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.push(pair(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.empty()) { + // 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现) + // <节点, 源点到该节点的距离> + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } + } + + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 +} + +``` + +* 时间复杂度:O(ElogE) E 为边的数量 +* 空间复杂度:O(N + E) N 为节点的数量 + +堆优化的时间复杂度 只和边的数量有关 和节点数无关,在 优先级队列中 放的也是边。 + +以上代码中,`while (!pq.empty())` 里套了 `for (Edge edge : grid[cur.first])` + +`for` 里 遍历的是 当前节点 cur 所连接边。 + +那 当前节点cur 所连接的边 也是不固定的, 这就让大家分不清,这时间复杂度究竟是多少? + +其实 `for (Edge edge : grid[cur.first])` 里最终的数据走向 是 给队列里添加边。 + +那么跳出局部代码,整个队列 一定是 所有边添加了一次,同时也弹出了一次。 + +所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。 + +(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总之是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出) + +所以 该算法整体时间复杂度为 O(ElogE) + +网上的不少分析 会把 n (节点的数量)算进来,这个分析是有问题的,举一个极端例子,在n 为 10000,且是有一条边的 图里,以上代码,大家感觉执行了多少次? + +`while (!pq.empty())` 中的 pq 存的是边,其实只执行了一次。 + +所以该算法时间复杂度 和 节点没有关系。 + +至于空间复杂度,邻接表是 数组 + 链表 数组的空间 是 N ,有E条边 就申请对应多少个链表节点,所以是 复杂度是 N + E + +## 拓展 + +当然也有录友可能想 堆优化dijkstra 中 我为什么一定要用邻接表呢,我就用邻接矩阵 行不行 ? + +也行的。 + +但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**。 + +如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。 + +这里我也给出 邻接矩阵版本的堆优化dijkstra代码: + +```CPP +#include +#include +#include +#include +using namespace std; +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1][p2] = val; + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的距离> + priority_queue, vector>, mycomparison> pq; + + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.push(pair(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1、选距离源点最近且未访问过的节点 + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + visited[cur.first] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int j = 1; j <= n; j++) { + if (!visited[j] && grid[cur.first][j] != INT_MAX && (minDist[cur.first] + grid[cur.first][j] < minDist[j])) { + minDist[j] = minDist[cur.first] + grid[cur.first][j]; + pq.push(pair(j, minDist[j])); + } + } + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} + +``` + +* 时间复杂度:O(E * (N + logE)) E为边的数量,N为节点数量 +* 空间复杂度:O(log(N^2)) + +`while (!pq.empty())` 时间复杂度为 E ,while 里面 每次取元素 时间复杂度 为 logE,和 一个for循环 时间复杂度 为 N 。 + +所以整体是 E * (N + logE) + + +## 总结 + +在学习一种优化思路的时候,首先就要知道为什么要优化,遇到了什么问题。 + +正如我在开篇就给大家交代清楚 堆优化方式的背景。 + +堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序。 + +很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。 + +主要是因为两点: + +* 不熟悉邻接表的表达方式 +* 对dijkstra的实现思路还是不熟 + +这是我为什么 本篇花了大力气来讲解 图的存储,就是为了让大家彻底理解邻接表以及邻接表的代码写法。 + +至于 dijkstra的实现思路 ,朴素版 和 堆优化版本 都是 按照 dijkstra 三部曲来的。 + +理解了三部曲,dijkstra 的思路就是清晰的。 + +针对邻接表版本代码 我做了详细的 时间复杂度分析,也让录友们清楚,相对于 朴素版,时间都优化到哪了。 + +最后 我也给出了 邻接矩阵的版本代码,分析了这一版本的必要性以及时间复杂度。 + +至此通过 两篇dijkstra的文章,终于把 dijkstra 讲完了,如果大家对我讲解里所涉及的内容都吃透的话,详细对 dijkstra 算法也就理解到位了。 + + + diff --git a/problems/kamacoder/0047.参会dijkstra朴素.md b/problems/kamacoder/0047.参会dijkstra朴素.md new file mode 100644 index 00000000..295d3aec --- /dev/null +++ b/problems/kamacoder/0047.参会dijkstra朴素.md @@ -0,0 +1,733 @@ + +# dijkstra(朴素版)精讲 + +[题目链接](https://kamacoder.com/problempage.php?pid=1047) + +【题目描述】 + +小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。 + +小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。 + +小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。 + +接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。 + +【输出描述】 + +输出一个整数,代表小明从起点到终点所花费的最小时间。 + +输入示例 + +``` +7 9 +1 2 1 +1 3 4 +2 3 2 +2 4 5 +3 4 2 +4 5 3 +2 6 4 +5 7 4 +6 7 9 +``` + +输出示例:12 + +【提示信息】 + +能够到达的情况: + +如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12,则输出 12。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + +## 思路 + +本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。 + +接下来,我们来详细讲解最短路算法中的 dijkstra 算法。 + +dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。 + +需要注意两点: + +* dijkstra 算法可以同时求 起点到所有节点的最短路径 +* 权值不能为负数 + +(这两点后面我们会讲到) + +如本题示例中的图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240125162647.png) + +起点(节点1)到终点(节点7) 的最短路径是 图中 标记绿线的部分。 + +最短路径的权值为12。 + +其实 dijkstra 算法 和 我们之前讲解的prim算法思路非常接近,如果大家认真学过[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w),那么理解 Dijkstra 算法会相对容易很多。(这也是我要先讲prim再讲dijkstra的原因) + +dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。 + +这里我也给出 **dijkstra三部曲**: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +大家此时已经会发现,这和prim算法 怎么这么像呢。 + +我在[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)讲解中也给出了三部曲。 prim 和 dijkstra 确实很像,思路也是类似的,这一点我在后面还会详细来讲。 + +在dijkstra算法中,同样有一个数组很重要,起名为:minDist。 + +**minDist数组 用来记录 每一个节点距离源点的最小距离**。 + +理解这一点很重要,也是理解 dijkstra 算法的核心所在。 + +大家现在看着可能有点懵,不知道什么意思。 + +没关系,先让大家有一个印象,对理解后面讲解有帮助。 + +我们先来画图看一下 dijkstra 的工作过程,以本题示例为例: (以下为朴素版dijkstra的思路) + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) + +## 朴素版dijkstra + +### 模拟过程 + +----------- + +0、初始化 + +minDist数组数值初始化为int最大值。 + +这里在强点一下 **minDist数组的含义:记录所有节点到源点的最短路径**,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115421.png) + + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1 +* 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[4] = 4 + +可能有录友问:为啥和 minDist[2] 比较? + +再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理 + + +------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点到节点2距离最近,选节点2 + +2、该最近节点被标记访问过 + +节点2被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130121240.png) + +更新 minDist数组,即:源点(节点1) 到 节点6 、 节点3 和 节点4的距离。 + +**为什么更新这些节点呢? 怎么不更新其他节点呢**? + +因为 源点(节点1)通过 已经计算过的节点(节点2) 可以链接到的节点 有 节点3,节点4和节点6. + + +更新 minDist数组: + +* 源点到节点6的最短距离为5,小于原minDist[6]的数值max,更新minDist[6] = 5 +* 源点到节点3的最短距离为3,小于原minDist[3]的数值4,更新minDist[3] = 3 +* 源点到节点4的最短距离为6,小于原minDist[4]的数值max,更新minDist[4] = 6 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点距离哪些节点最近,怎么算的? + +其实就是看 minDist数组里的数值,minDist 记录了 源点到所有节点的最近距离,结合visited数组筛选出未访问的节点就好。 + +从 上面的图,或者 从minDist数组中,我们都能看出 未访问过的节点中,源点(节点1)到节点3距离最近。 + + +2、该最近节点被标记访问过 + +节点3被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130120434.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +更新 minDist数组: + +* 源点到节点4的最短距离为5,小于原minDist[4]的数值6,更新minDist[4] = 5 + +------------------ + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,有节点4 和 节点6,距离源点距离都是 5 (minDist[4] = 5,minDist[6] = 5) ,选哪个节点都可以。 + +2、该最近节点被标记访问过 + +节点4被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201111352.png) + +在上面的讲解中,每一步 我都是按照 dijkstra 三部曲来讲解的,理解了这三部曲,代码也就好懂的。 + +### 代码实现 + +本题代码如下,里面的 三部曲 我都做了注释,大家按照我上面的讲解 来看如下代码: + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[start] = 0; // 起始点到自身的距离为0 + + for (int i = 1; i <= n; i++) { // 遍历所有节点 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### debug方法 + +写这种题目难免会有各种各样的问题,我们如何发现自己的代码是否有问题呢? + +最好的方式就是打日志,本题的话,就是将 minDist 数组打印出来,就可以很明显发现 哪里出问题了。 + +每次选择节点后,minDist数组的变化是否符合预期 ,是否和我上面讲的逻辑是对应的。 + +例如本题,如果想debug的话,打印日志可以这样写: + + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + std::vector minDist(n + 1, INT_MAX); + + std::vector visited(n + 1, false); + + minDist[start] = 0; + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + // 打印日志: + cout << "select:" << cur << endl; + for (int v = 1; v <= n; v++) cout << v << ":" << minDist[v] << " "; + cout << endl << endl;; + + } + if (minDist[end] == INT_MAX) cout << -1 << endl; + else cout << minDist[end] << endl; + +} + +``` + +打印后的结果: + +``` +select:1 +1:0 2:1 3:4 4:2147483647 5:2147483647 6:2147483647 7:2147483647 + +select:2 +1:0 2:1 3:3 4:6 5:2147483647 6:5 7:2147483647 + +select:3 +1:0 2:1 3:3 4:5 5:2147483647 6:5 7:2147483647 + +select:4 +1:0 2:1 3:3 4:5 5:8 6:5 7:2147483647 + +select:6 +1:0 2:1 3:3 4:5 5:8 6:5 7:14 + +select:5 +1:0 2:1 3:3 4:5 5:8 6:5 7:12 + +select:7 +1:0 2:1 3:3 4:5 5:8 6:5 7:12 +``` + +打印日志可以和上面我讲解的过程进行对比,每一步的结果是完全对应的。 + +所以如果大家如果代码有问题,打日志来debug是最好的方法 + +### 如何求路径 + +如果题目要求把最短路的路径打印出来,应该怎么办呢? + +这里还是有一些“坑”的,本题打印路径和 prim 打印路径是一样的,我在 [prim算法精讲](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w) 【拓展】中 已经详细讲解了。 + +在这里就不再赘述。 + +打印路径只需要添加 几行代码, 打印路径的代码我都加上的日志,如下: + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + std::vector minDist(n + 1, INT_MAX); + + std::vector visited(n + 1, false); + + minDist[start] = 0; + + //加上初始化 + vector parent(n + 1, -1); + + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + parent[v] = cur; // 记录边 + } + } + + } + + // 输出最短情况 + for (int i = 1; i <= n; i++) { + cout << parent[i] << "->" << i << endl; + } +} +``` + +打印结果: + +``` +-1->1 +1->2 +2->3 +3->4 +4->5 +2->6 +5->7 +``` + +对应如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201111352.png) + +### 出现负数 + +如果图中边的权值为负数,dijkstra 还合适吗? + +看一下这个图: (有负权值) + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110217.png) + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为100,小于原minDist[2]的数值max,更新minDist[2] = 100 +* 源点到节点3的最短距离为1,小于原minDist[3]的数值max,更新minDist[4] = 1 + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点3最近,距离为1,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点3访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227110711.png) + +-------------- + +至此dijkstra的模拟过程就结束了,根据最后的minDist数组,我们求 节点1 到 节点5 的最短路径的权值总和为 3,路径: 节点1 -> 节点3 -> 节点4 -> 节点5 + +通过以上的过程模拟,我们可以发现 之所以 没有走有负权值的最短路径 是因为 在 访问 节点 2 的时候,节点 3 已经访问过了,就不会再更新了。 + +那有录友可能会想: 我可以改代码逻辑啊,访问过的节点,也让它继续访问不就好了? + +那么访问过的节点还能继续访问会不会有死循环的出现呢?控制逻辑不让其死循环?那特殊情况自己能都想清楚吗?(可以试试,实践出真知) + +对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,**但最终会发现只是 拆了东墙补西墙**,对dijkstra的补充逻辑只能满足某特定场景最短路求解。 + +对于求解带有负权值的最短路问题,可以使用 Bellman-Ford 算法 ,我在后序会详细讲解。 + +## dijkstra与prim算法的区别 + +> 这里再次提示,需要先看我的 [prim算法精讲](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w) ,否则可能不知道我下面讲的是什么。 + +大家可以发现 dijkstra的代码看上去 怎么和 prim算法这么像呢。 + +其实代码大体不差,唯一区别在 三部曲中的 第三步: 更新minDist数组 + +因为**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**。 + +prim 更新 minDist数组的写法: + + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } +} +``` + +因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。 + +dijkstra 更新 minDist数组的写法: + +```CPP +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。 + +此时大家可能不禁要想 prim算法 可以有负权值吗? + +当然可以! + +录友们可以自己思考思考一下,这是为什么? + +这里我提示一下:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。 + + + +## 总结 + +本篇,我们深入讲解的dijkstra算法,详细模拟其工作的流程。 + +这里我给出了 **dijkstra 三部曲 来 帮助大家理解 该算法**,不至于 每次写 dijkstra 都是黑盒操作,没有框架没有章法。 + +在给出的代码中,我也按照三部曲的逻辑来给大家注释,只要理解这三部曲,即使 过段时间 对 dijkstra 算法有些遗忘,依然可以写出一个框架出来,然后再去调试细节。 + +对于图论算法,一般代码都比较长,很难写出代码直接可以提交通过,都需要一个debug的过程,所以 **学习如何debug 非常重要**! + +这也是我为什么 在本文中 单独用来讲解 debug方法。 + +本题求的是最短路径和是多少,**同时我们也要掌握 如何把最短路径打印出来**。 + +我还写了大篇幅来讲解 负权值的情况, 只有画图带大家一步一步去 看 出现负权值 dijkstra的求解过程,才能帮助大家理解,问题出在哪里。 + +如果我直接讲:是**因为访问过的节点 不能再访问,导致错过真正的最短路**,我相信大家都不知道我在说啥。 + +最后我还讲解了 dijkstra 和 prim 算法的 相同 与 不同之处, 我在图论的讲解安排中 先讲 prim算法 再讲 dijkstra 是有目的的, **理解这两个算法的相同与不同之处 有助于大家学习的更深入**。 + +而不是 学了 dijkstra 就只看 dijkstra, 算法之间 都是有联系的,多去思考 算法之间的相互联系,会帮助大家思考的更深入,掌握的更彻底。 + +本篇写了这么长,我也只讲解了 朴素版dijkstra,**关于 堆优化dijkstra,我会在下一篇再来给大家详细讲解**。 + +加油 + + + diff --git a/problems/kamacoder/0053.寻宝-Kruskal.md b/problems/kamacoder/0053.寻宝-Kruskal.md new file mode 100644 index 00000000..1d0c9c83 --- /dev/null +++ b/problems/kamacoder/0053.寻宝-Kruskal.md @@ -0,0 +1,400 @@ + +# kruskal算法精讲 + +[卡码网:53. 寻宝](https://kamacoder.com/problempage.php?pid=1053) + +题目描述: + +在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 + +不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来。 + +给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。 + +输入描述: + +第一行包含两个整数V 和 E,V代表顶点数,E代表边数 。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。 + +接下来共有 E 行,每行三个整数 v1,v2 和 val,v1 和 v2 为边的起点和终点,val代表边的权值。 + +输出描述: + +输出联通所有岛屿的最小路径总距离 + +输入示例: + +``` +7 11 +1 2 1 +1 3 1 +1 5 2 +2 6 1 +2 4 2 +2 3 2 +3 4 1 +4 5 1 +5 6 2 +5 7 1 +6 7 1 +``` + +输出示例: + +6 + +## 解题思路 + +在上一篇 我们讲解了 prim算法求解 最小生成树,本篇我们来讲解另一个算法:Kruskal,同样可以求最小生成树。 + +**prim 算法是维护节点的集合,而 Kruskal 是维护边的集合**。 + +上来就这么说,大家应该看不太懂,这里是先让大家有这么个印象,带着这个印象在看下文,理解的会更到位一些。 + +kruscal的思路: + +* 边的权值排序,因为要优先选最小的边加入到生成树里 +* 遍历排序后的边 + * 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环 + * 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合 + +下面我们画图举例说明kruscal的工作过程。 + +依然以示例中,如下这个图来举例。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240111113514.png) + +将图中的边按照权值有小到大排序,这样从贪心的角度来说,优先选 权值小的边加入到 最小生成树中。 + +排序后的边顺序为[(1,2) (4,5) (1,3) (2,6) (3,4) (6,7) (5,7) (1,5) (3,2) (2,4) (5,6)] + +> (1,2) 表示节点1 与 节点2 之间的边。权值相同的边,先后顺序无所谓。 + +**开始从头遍历排序后的边**。 + +-------- + +选边(1,2),节点1 和 节点2 不在同一个集合,所以生成树可以添加边(1,2),并将 节点1,节点2 放在同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240111114204.png) + +-------- + +选边(4,5),节点4 和 节点 5 不在同一个集合,生成树可以添加边(4,5) ,并将节点4,节点5 放到同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240111120458.png) + +**大家判断两个节点是否在同一个集合,就看图中两个节点是否有绿色的粗线连着就行** + +------ + +(这里在强调一下,以下选边是按照上面排序好的边的数组来选择的) + +选边(1,3),节点1 和 节点3 不在同一个集合,生成树添加边(1,3),并将节点1,节点3 放到同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240112105834.png) + +--------- + +选边(2,6),节点2 和 节点6 不在同一个集合,生成树添加边(2,6),并将节点2,节点6 放到同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240112110214.png) + +-------- + +选边(3,4),节点3 和 节点4 不在同一个集合,生成树添加边(3,4),并将节点3,节点4 放到同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240112110450.png) + +---------- + +选边(6,7),节点6 和 节点7 不在同一个集合,生成树添加边(6,7),并将 节点6,节点7 放到同一个集合。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240112110637.png) + +----------- + +选边(5,7),节点5 和 节点7 在同一个集合,不做计算。 + +选边(1,5),两个节点在同一个集合,不做计算。 + +后面遍历 边(3,2),(2,4),(5,6) 同理,都因两个节点已经在同一集合,不做计算。 + + +------- + +此时 我们就已经生成了一个最小生成树,即: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240112110637.png) + +在上面的讲解中,看图的话 大家知道如何判断 两个节点 是否在同一个集合(是否有绿色的线连在一起),以及如何把两个节点加入集合(就在图中把两个节点连上) + +**但在代码中,如果将两个节点加入同一个集合,又如何判断两个节点是否在同一个集合呢**? + +这里就涉及到我们之前讲解的[并查集](https://www.programmercarl.com/%E5%9B%BE%E8%AE%BA%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html)。 + +我们在并查集开篇的时候就讲了,并查集主要就两个功能: + +* 将两个元素添加到一个集合中 +* 判断两个元素在不在同一个集合 + +大家发现这正好符合 Kruskal算法的需求,这也是为什么 **我要先讲并查集,再讲 Kruskal**。 + +关于 并查集,我已经在[并查集精讲](https://www.programmercarl.com/%E5%9B%BE%E8%AE%BA%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) 详细讲解过了,所以这里不再赘述,我们直接用。 + +本题代码如下,已经详细注释: + +```CPP + +#include +#include +#include + +using namespace std; + +// l,r为 边两边的节点,val为边的数值 +struct Edge { + int l, r, val; +}; + +// 节点数量 +int n = 10001; +// 并查集标记节点关系的数组 +vector father(n, -1); // 节点编号是从1开始的,n要大一些 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} + +// 并查集的查找操作 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 并查集的加入集合 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} + +int main() { + + int v, e; + int v1, v2, val; + vector edges; + int result_val = 0; + cin >> v >> e; + while (e--) { + cin >> v1 >> v2 >> val; + edges.push_back({v1, v2, val}); + } + + // 执行Kruskal算法 + // 按边的权值对边进行从小到大排序 + sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) { + return a.val < b.val; + }); + + // 并查集初始化 + init(); + + // 从头开始遍历边 + for (Edge edge : edges) { + // 并查集,搜出两个节点的祖先 + int x = find(edge.l); + int y = find(edge.r); + + // 如果祖先不同,则不在同一个集合 + if (x != y) { + result_val += edge.val; // 这条边可以作为生成树的边 + join(x, y); // 两个节点加入到同一个集合 + } + } + cout << result_val << endl; + return 0; +} + +``` + +时间复杂度:nlogn (快排) + logn (并查集) ,所以最后依然是 nlogn 。n为边的数量。 + +关于并查集时间复杂度,可以看我在 [并查集理论基础](https://programmercarl.com/%E5%9B%BE%E8%AE%BA%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) 的讲解。 + +## 拓展一 + +如果题目要求将最小生成树的边输出的话,应该怎么办呢? + +Kruskal 算法 输出边的话,相对prim 要容易很多,因为 Kruskal 本来就是直接操作边,边的结构自然清晰,不用像 prim一样 需要再节点练成线输出边 (因为prim是对节点操作,而 Kruskal是对边操作,这是本质区别) + +本题中,边的结构为: + +```CPP +struct Edge { + int l, r, val; +}; +``` + +那么我们只需要找到 在哪里把生成树的边保存下来就可以了。 + +当判断两个节点不在同一个集合的时候,这两个节点的边就加入到最小生成树, 所以添加边的操作在这里: + +```CPP +vector result; // 存储最小生成树的边 +// 如果祖先不同,则不在同一个集合 +if (x != y) { + result.push_back(edge); // 记录最小生成树的边 + result_val += edge.val; // 这条边可以作为生成树的边 + join(x, y); // 两个节点加入到同一个集合 +} +``` + +整体代码如下,为了突出重点,我仅仅将 打印最小生成树的部分代码注释了,大家更容易看到哪些改动。 + +```CPP +#include +#include +#include + +using namespace std; + +struct Edge { + int l, r, val; +}; + + +int n = 10001; + +vector father(n, -1); + +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} + +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} + +void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; +} + +int main() { + + int v, e; + int v1, v2, val; + vector edges; + int result_val = 0; + cin >> v >> e; + while (e--) { + cin >> v1 >> v2 >> val; + edges.push_back({v1, v2, val}); + } + + sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) { + return a.val < b.val; + }); + + vector result; // 存储最小生成树的边 + + init(); + + for (Edge edge : edges) { + + int x = find(edge.l); + int y = find(edge.r); + + + if (x != y) { + result.push_back(edge); // 保存最小生成树的边 + result_val += edge.val; + join(x, y); + } + } + + // 打印最小生成树的边 + for (Edge edge : result) { + cout << edge.l << " - " << edge.r << " : " << edge.val << endl; + } + + return 0; +} + + +``` + +按照题目中的示例,打印边的输出为: + +``` +1 - 2 : 1 +1 - 3 : 1 +2 - 6 : 1 +3 - 4 : 1 +4 - 5 : 1 +5 - 7 : 1 +``` + +大家可能发现 怎么和我们 模拟画的图不一样,差别在于 代码生成的最小生成树中 节点5 和 节点7相连的。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240116163014.png) + + +其实造成这个差别 是对边排序的时候 权值相同的边先后顺序的问题导致的,无论相同权值边的顺序是什么样的,最后都能得出最小生成树。 + + +## 拓展二 + + +此时我们已经讲完了 Kruskal 和 prim 两个解法来求最小生成树。 + +什么情况用哪个算法更合适呢。 + +Kruskal 与 prim 的关键区别在于,prim维护的是节点的集合,而 Kruskal 维护的是边的集合。 +如果 一个图中,节点多,但边相对较少,那么使用Kruskal 更优。 + +有录友可能疑惑,一个图里怎么可能节点多,边却少呢? + +节点未必一定要连着边那, 例如 这个图,大家能明显感受到边没有那么多对吧,但节点数量 和 上述我们讲的例子是一样的。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240116152211.png) + +为什么边少的话,使用 Kruskal 更优呢? + +因为 Kruskal 是对边进行排序的后 进行操作是否加入到最小生成树。 + +边如果少,那么遍历操作的次数就少。 + +在节点数量固定的情况下,图中的边越少,Kruskal 需要遍历的边也就越少。 + +而 prim 算法是对节点进行操作的,节点数量越少,prim算法效率就越少。 + +所以在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。 + +> 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图 + + +Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。 + +Kruskal算法 时间复杂度 为 nlogn,其中n 为边的数量,适用稀疏图。 + +## 总结 + +如果学过了并查集,其实 kruskal 比 prim更好理解一些。 + +本篇,我们依然通过模拟 Kruskal 算法的过程,来带大家一步步了解其工作过程。 + +在 拓展一 中讲解了 如何输出最小生成树的边。 + +在拓展二 中讲解了 prim 和 Kruskal的区别。 + +录友们可以细细体会。 + + diff --git a/problems/kama53.寻宝.md b/problems/kamacoder/0053.寻宝-prim.md similarity index 93% rename from problems/kama53.寻宝.md rename to problems/kamacoder/0053.寻宝-prim.md index 3a4b8b27..4d3d9bd8 100644 --- a/problems/kama53.寻宝.md +++ b/problems/kamacoder/0053.寻宝-prim.md @@ -1,13 +1,15 @@ -# 寻宝 +# prim算法精讲 [卡码网:53. 寻宝](https://kamacoder.com/problempage.php?pid=1053) 题目描述: -在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。你是一名探险者,决定前往这些岛屿,但为了节省时间和资源,你希望规划一条最短的路径,以便在探索这些岛屿时尽量减少旅行的距离。 +在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 -给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。每个岛屿都需要被至少访问一次,你的目标是规划一条最短路径,以最小化探索路径的总距离,同时确保访问了所有岛屿。 +不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来。 + +给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。 输入描述: @@ -133,7 +135,7 @@ minDist 数组 里的数值初始化为 最大数,因为本题 节点距离不 1、prim三部曲,第一步:选距离生成树最近节点 -选取一个距离 最小生成树(节点1) 最近的非生成树里的节点,节点2,3,5 距离 最小生成树(节点1) 最近,选节点 2(其实选 节点3或者节点5都可以,距离一样的)加入最小生成树。 +选取一个距离 最小生成树(节点1) 最近的非生成树里的节点,节点2,3,5 距离 最小生成树(节点1) 最近,选节点 2(其实选 节点3或者节点2都可以,距离一样的)加入最小生成树。 2、prim三部曲,第二步:最近节点加入生成树 @@ -272,6 +274,8 @@ minDist数组已经更新了 所有非生成树的节点距离 最小生成树 ```CPP #include #include +#include + using namespace std; int main() { int v, e; @@ -296,28 +300,28 @@ int main() { for (int i = 1; i < v; i++) { // 1、prim三部曲,第一步:选距离生成树最近节点 - int cur = -1; // 选中哪个节点 加入最小生成树 + int cur = -1; // 选中哪个节点 加入最小生成树 + int minVal = INT_MAX; for (int j = 1; j <= v; j++) { // 1 - v,顶点编号,这里下标从1开始 - // 选取最小生成树节点的条件: + // 选取最小生成树节点的条件: // (1)不在最小生成树里 - // (2)距离最小生成树最近的节点 - // (3)只要不在最小生成树里,先默认选一个节点 ,在比较 哪一个是最小的 - // 理解条件3 很重要,才能理解这段代码:(cur == -1 || minDist[j] < minDist[cur]) - if (!isInTree[j] && (cur == -1 || minDist[j] < minDist[cur])) { + // (2)距离最小生成树最近的节点 + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; cur = j; } } - // 2、prim三部曲,第二步:最近节点(cur)加入生成树 + // 2、prim三部曲,第二步:最近节点(cur)加入生成树 isInTree[cur] = true; // 3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) // cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下 - // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢 - for (int j = 1; j <= v; j++) { - // 更新的条件: - // (1)节点是 非生成树里的节点 - // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小 - // 很多录友看到自己 就想不明白什么意思,其实就是 cur 是新加入 最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了 + // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢 + for (int j = 1; j <= v; j++) { + // 更新的条件: + // (1)节点是 非生成树里的节点 + // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小 + // 很多录友看到自己 就想不明白什么意思,其实就是 cur 是新加入 最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了 if (!isInTree[j] && grid[cur][j] < minDist[j]) { minDist[j] = grid[cur][j]; } @@ -325,14 +329,17 @@ int main() { } // 统计结果 int result = 0; - for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边 + for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边 result += minDist[i]; } cout << result << endl; } + ``` +时间复杂度为 O(n^2),其中 n 为节点数量。 + ## 拓展 上面讲解的是记录了最小生成树 所有边的权值,如果让打印出来 最小生成树的每条边呢? 或者说 要把这个最小生成树画出来呢? @@ -406,6 +413,8 @@ for (int j = 1; j <= v; j++) { ```CPP #include #include +#include + using namespace std; int main() { int v, e; @@ -426,11 +435,14 @@ int main() { for (int i = 1; i < v; i++) { int cur = -1; - for (int j = 1; j <= v; j++) { - if (!isInTree[j] && (cur == -1 || minDist[j] < minDist[cur])) { + int minVal = INT_MAX; + for (int j = 1; j <= v; j++) { + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; cur = j; } } + isInTree[cur] = true; for (int j = 1; j <= v; j++) { if (!isInTree[j] && grid[cur][j] < minDist[j]) { @@ -440,11 +452,12 @@ int main() { } } } - // 输出 最小生成树边的链接情况 + // 输出 最小生成树边的链接情况 for (int i = 1; i <= v; i++) { - cout << i "->" parent[i] << endl; + cout << i << "->" << parent[i] << endl; } } + ``` 按照本题示例,代码输入如下: diff --git a/problems/kama54.替换数字.md b/problems/kamacoder/0054.替换数字.md similarity index 79% rename from problems/kama54.替换数字.md rename to problems/kamacoder/0054.替换数字.md index f03e575d..45a0aa54 100644 --- a/problems/kama54.替换数字.md +++ b/problems/kamacoder/0054.替换数字.md @@ -142,21 +142,82 @@ for (int i = 0; i < a.size(); i++) { ### C: ### Java: - +解法一 ```java import java.util.Scanner; -class Main { - public static void main(String[] args) { - Scanner in = new Scanner(System.in); - String s = in.nextLine(); - StringBuilder sb = new StringBuilder(); +public class Main { + + public static String replaceNumber(String s) { + int count = 0; // 统计数字的个数 + int sOldSize = s.length(); for (int i = 0; i < s.length(); i++) { - if (Character.isDigit(s.charAt(i))) { - sb.append("number"); - }else sb.append(s.charAt(i)); + if(Character.isDigit(s.charAt(i))){ + count++; + } } - System.out.println(sb); + // 扩充字符串s的大小,也就是每个空格替换成"number"之后的大小 + char[] newS = new char[s.length() + count * 5]; + int sNewSize = newS.length; + // 将旧字符串的内容填入新数组 + System.arraycopy(s.toCharArray(), 0, newS, 0, sOldSize); + // 从后先前将空格替换为"number" + for (int i = sNewSize - 1, j = sOldSize - 1; j < i; j--, i--) { + if (!Character.isDigit(newS[j])) { + newS[i] = newS[j]; + } else { + newS[i] = 'r'; + newS[i - 1] = 'e'; + newS[i - 2] = 'b'; + newS[i - 3] = 'm'; + newS[i - 4] = 'u'; + newS[i - 5] = 'n'; + i -= 5; + } + } + return new String(newS); + }; + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + String s = scanner.next(); + System.out.println(replaceNumber(s)); + scanner.close(); + } +} +``` +解法二 +```java +// 为了还原题目本意,先把原数组复制到扩展长度后的新数组,然后不再使用原数组、原地对新数组进行操作。 +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.next(); + int len = s.length(); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) >= 0 && s.charAt(i) <= '9') { + len += 5; + } + } + + char[] ret = new char[len]; + for (int i = 0; i < s.length(); i++) { + ret[i] = s.charAt(i); + } + for (int i = s.length() - 1, j = len - 1; i >= 0; i--) { + if ('0' <= ret[i] && ret[i] <= '9') { + ret[j--] = 'r'; + ret[j--] = 'e'; + ret[j--] = 'b'; + ret[j--] = 'm'; + ret[j--] = 'u'; + ret[j--] = 'n'; + } else { + ret[j--] = ret[i]; + } + } + System.out.println(ret); } } ``` diff --git a/problems/kama55.右旋字符串.md b/problems/kamacoder/0055.右旋字符串.md similarity index 100% rename from problems/kama55.右旋字符串.md rename to problems/kamacoder/0055.右旋字符串.md diff --git a/problems/kamacoder/0094.城市间货物运输I-SPFA.md b/problems/kamacoder/0094.城市间货物运输I-SPFA.md new file mode 100644 index 00000000..f41d6fd1 --- /dev/null +++ b/problems/kamacoder/0094.城市间货物运输I-SPFA.md @@ -0,0 +1,348 @@ + +# Bellman_ford 队列优化算法(又名SPFA) + +[卡码网:94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152) + +题目描述 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。 + + +城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。 + +> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +输入描述 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v(单向图)。 + +输出描述 + +如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。 + +输入示例: + +``` +6 7 +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +## 思路 + +本题我们来系统讲解 Bellman_ford 队列优化算法 ,也叫SPFA算法(Shortest Path Faster Algorithm)。 + +> SPFA的称呼来自 1994年西南交通大学段凡丁的论文,其实Bellman_ford 提出后不久 (20世纪50年代末期) 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼 算法为 Bellman_ford 队列优化算法(Queue improved Bellman-Ford) + +大家知道以上来历,知道 SPFA 和 Bellman_ford 队列优化算法 指的都是一个算法就好。 + +如果大家还不够了解 Bellman_ford 算法,强烈建议按照《代码随想录》的顺序学习,否则可能看不懂下面的讲解。 + +大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。 + +但真正有效的松弛,是基于已经计算过的节点在做的松弛。 + +给大家举一个例子: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240328104119.png) + +本图中,对所有边进行松弛,真正有效的松弛,只有松弛 边(节点1->节点2) 和 边(节点1->节点5) 。 + +而松弛 边(节点4->节点6) ,边(节点5->节点3)等等 都是无效的操作,因为 节点4 和 节点 5 都是没有被计算过的节点。 + + +所以 Bellman_ford 算法 每次都是对所有边进行松弛,其实是多做了一些无用功。 + +**只需要对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了**。 + +基于以上思路,如何记录 上次松弛的时候更新过的节点呢? + +用队列来记录。(其实用栈也行,对元素顺序没有要求) + +接下来来举例这个队列是如何工作的。 + +以示例给出的所有边为例: + +``` +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +我们依然使用**minDist数组来表达 起点到各个节点的最短距离**,例如minDist[3] = 5 表示起点到达节点3 的最小距离为5 + +初始化,起点为节点1, 起点到起点的最短距离为0,所以minDist[1] 为 0。 将节点1 加入队列 (下次松弛送节点1开始) + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115555.png) + +------------ + +从队列里取出节点1,松弛节点1 作为出发点链接的边(节点1 -> 节点2)和边(节点1 -> 节点3) + +边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 。 + +边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5。 + +将节点2,节点3 加入队列,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115544.png) + + +----------------- + + +从队列里取出节点2,松弛节点2 作为出发点链接的边(节点2 -> 节点4)和边(节点2 -> 节点5) + +边:节点2 -> 节点4,权值为1 ,minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。 + +边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。 + + +将节点4,节点5 加入队列,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110348.png) + + +-------------------- + + +从队列里出去节点3,松弛节点3 作为出发点链接的边。 + +因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110420.png) + + +------------ + +从队列中取出节点4,松弛节点4作为出发点链接的边(节点4 -> 节点6) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 。 + +讲节点6加入队列 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110445.png) + + +--------------- + +从队列中取出节点5,松弛节点5作为出发点链接的边(节点5 -> 节点3),边(节点5 -> 节点6) + +边:节点5 -> 节点3,权值为1 ,minDist[3] > minDist[5] + 1 ,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4 + +边:节点5 -> 节点6,权值为-2 ,minDist[6] > minDist[5] + (-2) ,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412110509.png) + + + +因为节点3,和 节点6 都曾经加入过队列,不用重复加入,避免重复计算。 + +在代码中我们可以用一个数组 visited 来记录入过队列的元素,加入过队列的元素,不再重复入队列。 + + +-------------- + +从队列中取出节点6,松弛节点6 作为出发点链接的边。 + +节点6作为终点,没有可以出发的边。 + +所以直接从队列中取出,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240411115424.png) + + +---------- + +这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。 + +大家可以发现 基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。 + +了解了大体流程,我们再看代码应该怎么写。 + +在上面模拟过程中,我们每次都要知道 一个节点作为出发点 链接了哪些节点。 + +如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./kama0047.参会dijkstra堆.md) 中 图的存储 部分。 + + +整体代码如下: + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + while (!que.empty()) { + + int node = que.front(); que.pop(); + + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int value = edge.val; + if (minDist[to] > minDist[from] + value) { // 开始松弛 + minDist[to] = minDist[from] + value; + que.push(to); + } + } + + } + + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 +} + +``` + +## 效率分析 + +队列优化版Bellman_ford 的时间复杂度 并不稳定,效率高低依赖于图的结构。 + +例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E) N 为节点数量,E为边的数量。 + +在这种图中,每一个节点都会重复加入队列 n - 1次,因为 这种图中 每个节点 都有 n-1 条指向该节点的边,每条边指向该节点,就需要加入一次队列。(如果这里看不懂,可以在重温一下代码逻辑) + +至于为什么 双向图且每一个节点和所有其他节点都相连的话,每个节点 都有 n-1 条指向该节点的边, 我再来举个例子,如图: + +[](https://code-thinking-1253855093.file.myqcloud.com/pics/20240416104138.png) + +图中 每个节点都与其他所有节点相连,节点数n 为 4,每个节点都有3条指向该节点的边,即入度为3。 + +n为其他数值的时候,也是一样的。 + +当然这种图是比较极端的情况,也是最稠密的图。 + +所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。 + +反之,图越稀疏,SPFA的效率就越高。 + +一般来说,SPFA 的时间复杂度为 O(K * N) K 为不定值,因为 节点需要计入几次队列取决于 图的稠密度。 + +如果图是一条线形图且单向的话,每个节点的入度为1,那么只需要加入一次队列,这样时间复杂度就是 O(N)。 + +所以 SPFA 在最坏的情况下是 O(N * E),但 一般情况下 时间复杂度为 O(K * N)。 + +尽管如此,**以上分析都是 理论上的时间复杂度分析**。 + +并没有计算 出队列 和 入队列的时间消耗。 因为这个在不同语言上 时间消耗也是不一定的。 + +以C++为例,以下两端代码理论上,时间复杂度都是 O(n) : + +```CPP +for (long long i = 0; i < n; i++) { + k++; +} + +``` + +```CPP +for (long long i = 0; i < n; i++) { + que.push(i); + que.front(); + que.pop(); +} + +``` + +在 MacBook Pro (13-inch, M1, 2020) 机器上分别测试这两段代码的时间消耗情况: + +* n = 10^4,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 4 ms +* n = 10^5,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 13 ms +* n = 10^6,第一段代码的时间消耗:4ms,第二段代码的时间消耗: 59 ms +* n = 10^7,第一段代码的时间消耗: 24ms,第二段代码的时间消耗: 463 ms +* n = 10^8,第一段代码的时间消耗: 135ms,第二段代码的时间消耗: 4268 ms + +在这里就可以看出 出队列和入队列 其实也是十分耗时的。 + +SPFA(队列优化版Bellman_ford) 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford,但实际时间消耗 可能是 SPFA耗时更多。 + +针对这种情况,我在后面题目讲解中,会特别加入稠密图的测试用例来给大家讲解。 + + +## 拓展 + +这里可能有录友疑惑,`while (!que.empty())` 队里里 会不会造成死循环? 例如 图中有环,这样一直有元素加入到队列里? + +其实有环的情况,要看它是 正权回路 还是 负全回路。 + +题目描述中,已经说了,本题没有 负权回路 。 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240412111849.png) + +正权回路 就是有环,但环的总权值为正数。 + +在有环且只有正权回路的情况下,即使元素重复加入队列,最后,也会因为 所有边都松弛后,节点数值(minDist数组)不在发生变化了 而终止。 + +(而且有重复元素加入队列是正常的,多条路径到达同一个节点,节点必要要选择一个最短的路径,而这个节点就会重复加入队列进行判断,选一个最短的) + +在[0094.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲过对所有边 最多松弛 n -1 次,就一定可以求出所有起点到所有节点的最小距离即 minDist数组。 + +即使再松弛n次以上, 所有起点到所有节点的最小距离(minDist数组) 不会再变了。 (这里如果不理解,建议认真看[0094.城市间货物运输I](./0094.城市间货物运输I.md)讲解) + +所以本题我们使用队列优化,有元素重复加入队列,也会因为最后 minDist数组 不会在发生变化而终止。 + +节点再加入队列,需要有松弛的行为, 而 每个节点已经都计算出来 起点到该节点的最短路径,那么就不会有 执行这个判断条件`if (minDist[to] > minDist[from] + value)`,从而不会有新的节点加入到队列。 + +但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。 + + + + + + + diff --git a/problems/kamacoder/0094.城市间货物运输I.md b/problems/kamacoder/0094.城市间货物运输I.md new file mode 100644 index 00000000..dc9b46f3 --- /dev/null +++ b/problems/kamacoder/0094.城市间货物运输I.md @@ -0,0 +1,389 @@ + +# Bellman_ford 算法精讲 + +[卡码网:94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152) + +题目描述 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。 + +权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。 + +如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。 + +城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。 + +> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +输入描述 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v(单向图)。 + +输出描述 + +如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。 + +输入示例: + +``` +6 7 +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240509200224.png) + +## 思路 + +本题依然是单源最短路问题,求 从 节点1 到节点n 的最小费用。 **但本题不同之处在于 边的权值是有负数了**。 + +从 节点1 到节点n 的最小费用也可以是负数,费用如果是负数 则表示 运输的过程中 政府补贴大于运输成本。 + +在求单源最短路的方法中,使用dijkstra 的话,则要求图中边的权值都为正数。 + +我们在 [dijkstra朴素版](./0047.参会dijkstra朴素.md) 中专门有讲解:为什么有边为负数 使用dijkstra就不行了。 + +**本题是经典的带负权值的单源最短路问题,此时就轮到Bellman_ford登场了**,接下来我们来详细介绍Bellman_ford 算法 如何解决这类问题。 + +> 该算法是由 R.Bellman 和L.Ford 在20世纪50年代末期发明的算法,故称为Bellman_ford算法。 + +**Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路**。 + +## 什么叫做松弛 + +看到这里,估计大家都比较晕了,为什么是 n-1 次,那“松弛”这两个字究竟是个啥意思? + +我们先来说什么是 “松弛”。 + +《算法四》里面把这个操作叫做 “放松”, 英文版里叫做 “relax the edge” + +所以大家翻译过来,就是 “放松” 或者 “松弛” 。 + +但《算法四》没有具体去讲这个 “放松” 究竟是个啥? 网上很多题解也没有讲题解里的 “松弛这条边,松弛所有边”等等 里面的 “松弛” 究竟是什么意思? + +这里我给大家举一个例子,每条边有起点、终点和边的权值。例如一条边,节点A 到 节点B 权值为value,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240327102620.png) + +minDist[B] 表示 到达B节点 最小权值,minDist[B] 有哪些状态可以推出来? + +状态一: minDist[A] + value 可以推出 minDist[B] +状态二: minDist[B]本身就有权值 (可能是其他边链接的节点B 例如节点C,以至于 minDist[B]记录了其他边到minDist[B]的权值) + +minDist[B] 应为如何取舍。 + +本题我们要求最小权值,那么 这两个状态我们就取最小的 + +```CPP +if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value + +``` + +也就是说,如果 通过 A 到 B 这条边可以获得更短的到达B节点的路径,即如果 `minDist[B] > minDist[A] + value`,那么我们就更新 `minDist[B] = minDist[A] + value` ,**这个过程就叫做 “松弛**” 。 + +以上讲了这么多,其实都是围绕以下这句代码展开: + +``` +if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value + +``` + +**这句代码就是 Bellman_ford算法的核心操作**。 + +以上代码也可以这么写:`minDist[B] = min(minDist[A] + value, minDist[B]) ` + +如果大家看过代码随想录的动态规划章节,会发现 无论是背包问题还是子序列问题,这段代码(递推公式)出现频率非常高的。 + +其实 Bellman_ford算法 也是采用了动态规划的思想,即:将一个问题分解成多个决策阶段,通过状态之间的递归关系最后计算出全局最优解。 + +(如果理解不了动态规划的思想也无所谓,理解我上面讲的松弛操作就好) + +**那么为什么是 n - 1次 松弛呢**? + +这里要给大家模拟一遍 Bellman_ford 的算法才行,接下来我们来看看对所有边松弛 n - 1 次的操作是什么样的。 + +我们依然使用**minDist数组来表达 起点到各个节点的最短距离**,例如minDist[3] = 5 表示起点到达节点3 的最小距离为5 + +### 模拟过程 + +初始化过程。 + +起点为节点1, 起点到起点的距离为0,所以 minDist[1] 初始化为0 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240328104119.png) + +其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点 默认是一个最大数,这样才能更新最小距离。 + + +对所有边 进行第一次松弛: (什么是松弛,在上面我已经详细讲过) + +以示例给出的所有边为例: + +``` +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +接下来我们来松弛一遍所有的边。 + +边:节点5 -> 节点6,权值为-2 ,minDist[5] 还是默认数值max,所以不能基于 节点5 去更新节点6,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329113537.png) + +(在复习一下,minDist[5] 表示起点到节点5的最短距离) + + +边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329113703.png) + +边:节点5 -> 节点3,权值为1 ,minDist[5] 还是默认数值max,所以不能基于节点5去更新节点3 如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329113827.png) + + +边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 (经过上面的计算minDist[2]已经不是默认值,而是 1),更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329113927.png) + + +边:节点2 -> 节点4,权值为-3 ,minDist[4] > minDist[2] + (-3),更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329114036.png) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329114120.png) + +边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240329114324.png) + +-------- + +以上是对所有边进行一次松弛之后的结果。 + +那么需要对所有边松弛几次才能得到 起点(节点1) 到终点(节点6)的最短距离呢? + +**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +上面的距离中,我们得到里 起点达到 与起点一条边相邻的节点2 和 节点3 的最短距离,分别是 minDist[2] 和 minDist[3] + +这里有录友疑惑了 minDist[3] = 5,分明不是 起点到达 节点3 的最短距离,节点1 -> 节点2 -> 节点5 -> 节点3 这条路线 距离才是4。 + +注意我上面讲的是 **对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**,这里 说的是 一条边相连的节点。 + +与起点(节点1)一条边相邻的节点,到达节点2 最短距离是 1,到达节点3 最短距离是5。 + +而 节点1 -> 节点2 -> 节点5 -> 节点3 这条路线 是 与起点 三条边相连的路线了。 + +所以对所有边松弛一次 能得到 与起点 一条边相连的节点最短距离。 + +那对所有边松弛两次 可以得到与起点 两条边相连的节点的最短距离。 + +那对所有边松弛三次 可以得到与起点 三条边相连的节点的最短距离,这个时候,我们就能得到到达节点3真正的最短距离,也就是 节点1 -> 节点2 -> 节点5 -> 节点3 这条路线。 + +那么再回归刚刚的问题,**需要对所有边松弛几次才能得到 起点(节点1) 到终点(节点6)的最短距离呢**? + +节点数量为n,那么起点到终点,最多是 n-1 条边相连。 + +那么无论图是什么样的,边是什么样的顺序,我们对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。 + +其实也同时计算出了,起点 到达 所有节点的最短距离,因为所有节点与起点连接的边数最多也就是 n-1 条边。 + +截止到这里,Bellman_ford 的核心算法思路,大家就了解的差不多了。 + +共有两个关键点。 + +* “松弛”究竟是个啥? +* 为什么要对所有边松弛 n - 1 次 (n为节点个数) ? + +那么Bellman_ford的解题解题过程其实就是对所有边松弛 n-1 次,然后得出得到终点的最短路径。 + + +### 代码 + +理解上面讲解的内容,代码就更容易写了,本题代码如下:(详细注释) + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + for (int i = 1; i < n; i++) { // 对所有边 松弛 n-1 次 + for (vector &side : grid) { // 每一次松弛,都是对所有边进行松弛 + int from = side[0]; // 边的出发点 + int to = side[1]; // 边的到达点 + int price = side[2]; // 边的权值 + // 松弛操作 + // minDist[from] != INT_MAX 防止从未计算过的节点出发 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) { + minDist[to] = minDist[from] + price; + } + } + } + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} +``` + +* 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +关于空间复杂度,可能有录友疑惑,代码中数组grid不也开辟空间了吗? 为什么只算minDist数组的空间呢? + +grid数组是用来存图的,这是题目描述中必须要使用的空间,而不是我们算法所使用的空间。 + +我们在讲空间复杂度的时候,一般都是说,我们这个算法所用的空间复杂度。 + + +### 拓展 + +有录友可能会想,那我 松弛 n 次,松弛 n + 1次,松弛 2 * n 次会怎么样? + +其实没啥影响,结果不会变的,因为 题目中说了 “同时保证道路网络中不存在任何负权回路” 也就是图中没有 负权回路(在有向图中出现有向环 且环的总权值为负数)。 + +那么我们只要松弛 n - 1次 就一定能得到结果,没必要在松弛更多次了。 + +这里有疑惑的录友,可以加上打印 minDist数组 的日志,尝试一下,看看松弛 n 次会怎么样。 + +你会发现 松弛 大于 n - 1次,minDist数组 就不会变化了。 + +这里我给出打印日志的代码: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + for (int i = 1; i < n; i++) { // 对所有边 松弛 n-1 次 + for (vector &side : grid) { // 每一次松弛,都是对所有边进行松弛 + int from = side[0]; // 边的出发点 + int to = side[1]; // 边的到达点 + int price = side[2]; // 边的权值 + // 松弛操作 + // minDist[from] != INT_MAX 防止从未计算过的节点出发 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) { + minDist[to] = minDist[from] + price; + } + } + cout << "对所有边松弛 " << i << "次" << endl; + for (int k = 1; k <= n; k++) { + cout << minDist[k] << " "; + } + cout << endl; + } + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} + +``` + +通过打日志,大家发现,怎么对所有边进行第二次松弛以后结果就 不再变化了,那根本就不用松弛 n - 1 ? + +这是本题的样例的特殊性, 松弛 n-1 次 是保证对任何图 都能最后求得到终点的最小距离。 + +如果还想不明白 我再举一个例子,用以下测试用例再跑一下。 + + +``` +6 5 +5 6 1 +4 5 1 +3 4 1 +2 3 1 +1 2 1 +``` + +打印结果: + +``` +对所有边松弛 1次 +0 1 2147483647 2147483647 2147483647 2147483647 +对所有边松弛 2次 +0 1 2 2147483647 2147483647 2147483647 +对所有边松弛 3次 +0 1 2 3 2147483647 2147483647 +对所有边松弛 4次 +0 1 2 3 4 2147483647 +对所有边松弛 5次 +0 1 2 3 4 5 +``` + +你会发现到 n-1 次 才打印出最后的最短路结果。 + +关于上面的讲解,大家一定要多写代码去实验,验证自己的想法。 + +**至于 负权回路 ,我在下一篇会专门讲解这种情况,大家有个印象就好**。 + + +## 总结 + +Bellman_ford 是可以计算 负权值的单源最短路算法。 + +其算法核心思路是对 所有边进行 n-1 次 松弛。 + +弄清楚 什么是 松弛? 为什么要 n-1 次? 对理解Bellman_ford 非常重要。 + diff --git a/problems/kamacoder/0095.城市间货物运输II.md b/problems/kamacoder/0095.城市间货物运输II.md new file mode 100644 index 00000000..3200efb3 --- /dev/null +++ b/problems/kamacoder/0095.城市间货物运输II.md @@ -0,0 +1,240 @@ + +# bellman_ford之判断负权回路 + +[卡码网:95. 城市间货物运输 II](https://kamacoder.com/problempage.php?pid=1153) + +【题目描述】 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。权值为正表示扣除了政府补贴后运输货物仍需支付的费用; + +权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + +然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:**图中可能出现负权回路**。 + +负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +为了避免货物运输商采用负权回路这种情况无限的赚取政府补贴,算法还需检测这种特殊情况。 + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。同时能够检测并适当处理负权回路的存在。 + +城市 1 到城市 n 之间可能会出现没有路径的情况 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。 + +【输出描述】 + +如果没有发现负权回路,则输出一个整数,表示从城市 1 到城市 n 的最低运输成本(包括政府补贴)。 + +如果该整数是负数,则表示实现了盈利。如果发现了负权回路的存在,则输出 "circle"。如果从城市 1 无法到达城市 n,则输出 "unconnected"。 + + +输入示例 + +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +``` + +输出示例 + +``` +circle +``` + +## 思路 + +本题是 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 延伸题目。 + +本题是要我们判断 负权回路,也就是图中出现环且环上的边总权值为负数。 + +如果在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。 + +所以对于 在有负权值的图中求最短路,都需要先看看这个图里有没有负权回路。 + +接下来我们来看 如何使用 bellman_ford 算法来判断 负权回路。 + +在 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 中 我们讲了 bellman_ford 算法的核心就是一句话:对 所有边 进行 n-1 次松弛。 同时文中的 【拓展】部分, 我们也讲了 松弛n次以上 会怎么样? + +在没有负权回路的图中,松弛 n 次以上 ,结果不会有变化。 + +但本题有 负权回路,如果松弛 n 次,结果就会有变化了,因为 有负权回路 就是可以无限最短路径(一直绕圈,就可以一直得到无限小的最短距离)。 + +那么每松弛一次,都会更新最短路径,所以结果会一直有变化。 + +(如果对于 bellman_ford 不了解的录友,建议详细看这里:[kama94.城市间货物运输I](./kama94.城市间货物运输I.md)) + +以上为理论分析,接下来我们再画图举例。 + +我们拿题目中示例来画一个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240402103135.png) + +图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的) + +节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1 + +而图中有负权回路: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240402103712.png) + +那么我们在负权回路中多绕一圈,我们的最短路径 是不是就更小了 (也就是更低的运输成本) + +节点1 -> 节点2 -> 节点3 -> 节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本 (-1) + 1 + (-1) + (-1) + 1 + (-1) + 1 = -1 + +如果在负权回路多绕两圈,三圈,无穷圈,那么我们的总成本就会无限小, 如果要求最小成本的话,你会发现本题就无解了。 + +在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变 (如果对 bellman_ford 算法 不了解,也不知道 minDist 是什么,建议详看上篇讲解[kama94.城市间货物运输I](./kama94.城市间货物运输I.md)) + +而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。 + +那么解决本题的 核心思路,就是在 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 的基础上,再多松弛一次,看minDist数组 是否发生变化。 + +代码和 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 基本是一样的,如下:(关键地方已注释) + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + bool flag = false; + for (int i = 1; i <= n; i++) { // 这里我们松弛n次,最后一次判断负权回路 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (i < n) { + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } else { // 多加一次松弛判断负权回路 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) flag = true; + + } + } + + } + + if (flag) cout << "circle" << endl; + else if (minDist[end] == INT_MAX) { + cout << "unconnected" << endl; + } else { + cout << minDist[end] << endl; + } +} +``` + +* 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +## 拓展 + +本题可不可 使用 队列优化版的bellman_ford(SPFA)呢? + +上面的解法中,我们对所有边松弛了n-1次后,在松弛一次,如果出现minDist出现变化就判断有负权回路。 + +如果使用 SPFA 那么节点都是进队列的,那么节点进入队列几次后 足够判断该图是否有负权回路呢? + +在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA) 中,我们讲过 在极端情况下,即:所有节点都与其他节点相连,每个节点的入度为 n-1 (n为节点数量),所以每个节点最多加入 n-1 次队列。 + +那么如果节点加入队列的次数 超过了 n-1次 ,那么该图就一定有负权回路。 + +所以本题也是可以使用 SPFA 来做的。 代码如下: + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + vector count(n+1, 0); // 记录节点加入队列几次 + count[start]++; + + bool flag = false; + while (!que.empty()) { + + int node = que.front(); que.pop(); + + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int value = edge.val; + if (minDist[to] > minDist[from] + value) { // 开始松弛 + minDist[to] = minDist[from] + value; + que.push(to); + count[to]++; + if (count[to] == n) {// 如果加入队列次数超过 n-1次 就说明该图与负权回路 + flag = true; + while (!que.empty()) que.pop(); + break; + } + } + } + } + + if (flag) cout << "circle" << endl; + else if (minDist[end] == INT_MAX) { + cout << "unconnected" << endl; + } else { + cout << minDist[end] << endl; + } + +} + +``` diff --git a/problems/kamacoder/0096.城市间货物运输III.md b/problems/kamacoder/0096.城市间货物运输III.md new file mode 100644 index 00000000..f7533e02 --- /dev/null +++ b/problems/kamacoder/0096.城市间货物运输III.md @@ -0,0 +1,632 @@ + +# bellman_ford之单源有限最短路 + +[卡码网:96. 城市间货物运输 III](https://kamacoder.com/problempage.php?pid=1154) + +【题目描述】 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。 + +权值为正表示扣除了政府补贴后运输货物仍需支付的费用; + +权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + +请计算在最多经过 k 个城市的条件下,从城市 src 到城市 dst 的最低运输成本。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。 + +最后一行包含三个正整数,src、dst、和 k,src 和 dst 为城市编号,从 src 到 dst 经过的城市数量限制。 + +【输出描述】 + +输出一个整数,表示从城市 src 到城市 dst 的最低运输成本,如果无法在给定经过城市数量限制下找到从 src 到 dst 的路径,则输出 "unreachable",表示不存在符合条件的运输方案。 + +输入示例: + +``` +6 7 +1 2 1 +2 4 -3 +2 5 2 +1 3 5 +3 5 1 +4 6 4 +5 6 -2 +2 6 1 +``` + +输出示例: + +``` +0 +``` + +## 思路 + +本题为单源有限最短路问题,同样是 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 延伸题目。 + +注意题目中描述是 **最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径**。 + +在 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 中我们讲了:**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。 + +(如果对以上讲解看不懂,建议详看 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) ) + +本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 这里可能有录友想不懂为什么是k + 1,来看这个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240402115614.png) + +图中,节点2 最多已经经过2个节点 到达节点4,那么中间是有多少条边呢,是 3 条边对吧。 + +所以本题就是求:起点最多经过k + 1 条边到达终点的最短距离。 + +对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次,就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。 + +**注意**: 本题是 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 的拓展题,如果对 bellman_ford 没有深入了解,强烈建议先看 [kama94.城市间货物运输I](./kama94.城市间货物运输I.md) 再做本题。 + +理解以上内容,其实本题代码就很容易了,bellman_ford 标准写法是松弛 n-1 次,本题就松弛 k + 1次就好。 + +此时我们可以写出如下代码: + +```CPP +// 版本一 +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { // 对所有边松弛 k + 1次 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + + } + + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +以上代码 标准 bellman_ford 写法,松弛 k + 1次,看上去没什么问题。 + +但大家提交后,居然没通过! + +这是为什么呢? + +接下来我们拿这组数据来举例: + +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +1 4 3 +``` + +(**注意上面的示例是有负权回路的,只有带负权回路的图才能说明问题**) + +> 负权回路是指一条道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +正常来说,这组数据输出应该是 1,但以上代码输出的是 -2。 + + +在讲解原因的时候,强烈建议大家,先把 minDist数组打印出来,看看minDist数组是不是按照自己的想法变化的,这样更容易理解我接下来的讲解内容。 (**一定要动手,实践出真实,脑洞模拟不靠谱**) + +打印的代码可以是这样: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { // 对所有边松弛 k + 1次 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + // 打印 minDist 数组 + for (int j = 1; j <= n; j++) cout << minDist[j] << " "; + cout << endl; + + } + + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +接下来,我按照上面的示例带大家 画图举例 对所有边松弛一次 的效果图。 + +起点为节点1, 起点到起点的距离为0,所以 minDist[1] 初始化为0 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111940.png) + +其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点 默认是一个最大数,这样才能更新最小距离。 + +当我们开始对所有边开始第一次松弛: + +边:节点1 -> 节点2,权值为-1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = minDist[1] + (-1) = 0 - 1 = -1 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111914.png) + + +边:节点2 -> 节点3,权值为1 ,minDist[3] > minDist[2] + 1 ,更新 minDist[3] = minDist[2] + 1 = -1 + 1 = 0 ,如图: +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111903.png) + + +边:节点3 -> 节点1,权值为-1 ,minDist[1] > minDist[3] + (-1),更新 minDist[1] = 0 + (-1) = -1 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111849.png) + + +边:节点3 -> 节点4,权值为1 ,minDist[4] > minDist[3] + 1,更新 minDist[4] = 0 + (-1) = -1 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111837.png) + + +以上是对所有边进行的第一次松弛,最后 minDist数组为 :-1 -1 0 1 ,(从下标1算起) + +后面几次松弛我就不挨个画图了,过程大同小异,我直接给出minDist数组的变化: + +所有边进行的第二次松弛,minDist数组为 : -2 -2 -1 0 +所有边进行的第三次松弛,minDist数组为 : -3 -3 -2 -1 +所有边进行的第四次松弛,minDist数组为 : -4 -4 -3 -2 (本示例中k为3,所以松弛4次) + +最后计算的结果minDist[4] = -2,即 起点到 节点4,最多经过 3 个节点的最短距离是 -2,但 正确的结果应该是 1,即路径:节点1 -> 节点2 -> 节点3 -> 节点4。 + +理论上来说,**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +对所有边松弛两次,相当于计算 起点到达 与起点两条边相连的节点的最短距离。 + +对所有边松弛三次,以此类推。 + +但在对所有边松弛第一次的过程中,大家会发现,不仅仅 与起点一条边相连的节点更新了,所有节点都更新了。 + +而且对所有边的后面几次松弛,同样是更新了所有的节点,说明 至多经过k 个节点 这个限制 根本没有限制住,每个节点的数值都被更新了。 + +这是为什么? + +在上面画图距离中,对所有边进行第一次松弛,在计算 边(节点2 -> 节点3) 的时候,更新了 节点3。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240409111903.png) + +理论上来说节点3 应该在对所有边第二次松弛的时候才更新。 这因为当时是基于已经计算好的 节点2(minDist[2])来做计算了。 + +minDist[2]在计算边:(节点1 -> 节点2)的时候刚刚被赋值为 -1。 + +这样就造成了一个情况,即:计算minDist数组的时候,基于了本次松弛的 minDist数值,而不是上一次 松弛时候minDist的数值。 +所以在每次计算 minDist 时候,要基于 对所有边上一次松弛的 minDist 数值才行,所以我们要记录上一次松弛的minDist。 + +代码修改如下: (关键地方已经注释) + +```CPP +// 版本二 +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + vector minDist_copy(n + 1); // 用来记录上一次遍历的结果 + for (int i = 1; i <= k + 1; i++) { + minDist_copy = minDist; // 获取上一次计算的结果 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + // 注意使用 minDist_copy 来计算 minDist + if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + } + } + } + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +* 时间复杂度: O(K * E) , K为至多经过K个节点,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +## 拓展一(边的顺序的影响) + +其实边的顺序会影响我们每一次拓展的结果。 + +我来给大家举个例子。 + +我上面讲解中,给出的示例是这样的: +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +1 4 3 +``` + +我将示例中边的顺序改一下,给成: + +``` +4 4 +3 1 -1 +3 4 1 +2 3 1 +1 2 -1 +1 4 3 +``` + +所构成是图是一样的,都是如下的这个图,但给出的边的顺序是不一样的。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240410154340.png) + +再用版本一的代码是运行一下,发现结果输出是 1, 是对的。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240410154940.png) + +分明刚刚输出的结果是 -2,是错误的,怎么 一样的图,这次输出的结果就对了呢? + +其实这是和示例中给出的边的顺序是有关的, + +我们按照修改后的示例再来模拟 对所有边的第一次拓展情况。 + +初始化: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240410155545.png) + +边:节点3 -> 节点1,权值为-1 ,节点3还没有被计算过,节点1 不更新。 + +边:节点3 -> 节点4,权值为1 ,节点3还没有被计算过,节点4 不更新。 + +边:节点2 -> 节点3,权值为 1 ,节点2还没有被计算过,节点3 不更新。 + +边:节点1 -> 节点2,权值为 -1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = 0 + (-1) = -1 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240410160046.png) + +以上是对所有边 松弛一次的状态。 + +可以发现 同样的图,边的顺序不一样,使用版本一的代码 每次松弛更新的节点也是不一样的。 + +而边的顺序是随机的,是题目给我们的,所以本题我们才需要 记录上一次松弛的minDist,来保障 每一次对所有边松弛的结果。 + + +## 拓展二(本题本质) + +那么前面讲解过的 [94.城市间货物运输I](./kama94.城市间货物运输I.md) 和 [95.城市间货物运输II](./kama95.城市间货物运输II.md) 也是bellman_ford经典算法,也没使用 minDist_copy,怎么就没问题呢? + +> 如果没看过我上面这两篇讲解的话,建议详细学习上面两篇,再看我下面讲的区别,否则容易看不懂。 + +[94.城市间货物运输I](./kama94.城市间货物运输I.md), 是没有 负权回路的,那么 多松弛多少次,对结果都没有影响。 + +求 节点1 到 节点n 的最短路径,松弛n-1 次就够了,松弛 大于 n-1次,结果也不会变。 + +那么在对所有边进行第一次松弛的时候,如果基于 本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。 + +[95.城市间货物运输II](./kama95.城市间货物运输II.md) 是判断是否有 负权回路,一旦有负权回路, 对所有边松弛 n-1 次以后,在做松弛 minDist 数值一定会变,根据这一点来判断是否有负权回路。 + +所以,[95.城市间货物运输II](./kama95.城市间货物运输II.md) 只需要判断minDist数值变化了就行,而 minDist 的数值对不对,并不是我们关心的。 + +那么本题 为什么计算minDist 一定要基于上次 的 minDist 数值。 + +其关键在于本题的两个因素: + +* 本题可以有负权回路,说明只要多做松弛,结果是会变的。 +* 本题要求最多经过k个节点,对松弛次数是有限制的。 + +如果本题中 没有负权回路的测试用例, 那版本一的代码就可以过了,也就不用我费这么大口舌去讲解的这个坑了。 + +## 拓展三(SPFA) + +本题也可以用 SPFA来做,关于 SPFA ,已经在这里 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解。 + +使用SPFA算法解决本题的时候,关键在于 如何控制松弛k次。 + +其实实现不难,但有点技巧,可以用一个变量 que_size 记录每一轮松弛入队列的所有节点数量。 + +下一轮松弛的时候,就把队列里 que_size 个节点都弹出来,就是上一轮松弛入队列的节点。 + +代码如下(详细注释) + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start, end, k; + cin >> start >> end >> k; + + k++; + + vector minDist(n + 1 , INT_MAX); + vector minDist_copy(n + 1); // 用来记录每一次遍历的结果 + + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + int que_size; + while (k-- && !que.empty()) { + + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); // 记录上次入队列的节点个数 + while (que_size--) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛 + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + que.push(to); + } + } + + } + } + if (minDist[end] == INT_MAX) cout << "unreachable" << endl; + else cout << minDist[end] << endl; + +} + +``` + +时间复杂度: O(K * H) H 为不确定数,取决于 图的稠密度,但H 一定是小于等于 E 的 + +关于 SPFA的是时间复杂度分析,我在[0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解 + +但大家会发现,以上代码大家提交后,怎么耗时这么多? + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240418113308.png) + +理论上,SPFA的时间复杂度不是要比 bellman_ford 更优吗? + +怎么耗时多了这么多呢? + +以上代码有一个可以改进的点,每一轮松弛中,重复节点可以不用入队列。 + +因为重复节点入队列,下次从队列里取节点的时候,该节点要取很多次,而且都是重复计算。 + +所以代码可以优化成这样: + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start, end, k; + cin >> start >> end >> k; + + k++; + + vector minDist(n + 1 , INT_MAX); + vector minDist_copy(n + 1); // 用来记录每一次遍历的结果 + + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + int que_size; + while (k-- && !que.empty()) { + + vector visited(n + 1, false); // 每一轮松弛中,控制节点不用重复入队列 + minDist_copy = minDist; + que_size = que.size(); + while (que_size--) { + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + if(visited[to]) continue; // 不用重复放入队列,但需要重复松弛,所以放在这里位置 + visited[to] = true; + que.push(to); + } + } + + } + } + if (minDist[end] == INT_MAX) cout << "unreachable" << endl; + else cout << minDist[end] << endl; +} +``` + +以上代码提交后,耗时情况: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240418113952.png) + +大家发现 依然远比 bellman_ford 的代码版本 耗时高。 + +这又是为什么呢? + +对于后台数据,我特别制作的一个稠密大图,该图有250个节点和10000条边, 在这种情况下, SPFA 的时间复杂度 是接近与 bellman_ford的。 + +但因为 SPFA 节点的进出队列操作,耗时很大,所以相同的时间复杂度的情况下,SPFA 实际上更耗时了。 + +这一点我在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有分析,感兴趣的录友再回头去看看。 + +## 拓展四(能否用dijkstra) + +本题能否使用 dijkstra 算法呢? + +dijkstra 是贪心的思路 每一次搜索都只会找距离源点最近的非访问过的节点。 + +如果限制最多访问k个节点,那么 dijkstra 未必能在有限次就能到达终点,即使在经过k个节点确实可以到达终点的情况下。 + +这么说大家会感觉有点抽象,我用 [dijkstra朴素版精讲](./0047.参会dijkstra朴素.md) 里的示例在举例说明: (如果没看过我讲的[dijkstra朴素版精讲](./0047.参会dijkstra朴素.md),建议去仔细看一下,否则下面讲解容易看不懂) + + +在以下这个图中,求节点1 到 节点7 最多经过2个节点 的最短路是多少呢? + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240508112249.png) + +最短路显然是: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240508112416.png) + +最多经过2个节点,也就是3条边相连的路线:节点1 -> 节点2 -> 节点6-> 节点7 + +如果是 dijkstra 求解的话,求解过程是这样的: (下面是dijkstra的模拟过程,我精简了很多,如果看不懂,一定要先看[dijkstra朴素版精讲](./0047.参会dijkstra朴素.md)) + +初始化如图所示: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115306.png) + +找距离源点最近且没有被访问过的节点,先找节点1 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130115421.png) + + +距离源点最近且没有被访问过的节点,找节点2: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130121240.png) + +距离源点最近且没有被访问过的节点,找到节点3: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240130120434.png) + +距离源点最近且没有被访问过的节点,找到节点4: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240201105335.png) + +此时最多经过2个节点的搜索就完毕了,但结果中minDist[7] (即节点7的结果)并没有被更。 + +那么 dijkstra 会告诉我们 节点1 到 节点7 最多经过2个节点的情况下是不可到达的。 + +通过以上模拟过程,大家应该能感受到 dijkstra 贪心的过程,正是因为 贪心,所以 dijkstra 找不到 节点1 -> 节点2 -> 节点6-> 节点7 这条路径。 + +## 总结 + +本题是单源有限最短路问题,也是 bellman_ford的一个拓展问题,如果理解bellman_ford 其实思路比较容易理解,但有很多细节。 + +例如 为什么要用 minDist_copy 来记录上一轮 松弛的结果。 这也是本篇我为什么花了这么大篇幅讲解的关键所在。 + +接下来,还给大家做了四个拓展: + +* 边的顺序的影响 +* 本题的本质 +* SPFA的解法 +* 能否用dijkstra + +学透了以上四个拓展,相信大家会对bellman_ford有更深入的理解。 diff --git a/problems/kamacoder/0097.小明逛公园.md b/problems/kamacoder/0097.小明逛公园.md new file mode 100644 index 00000000..7e699949 --- /dev/null +++ b/problems/kamacoder/0097.小明逛公园.md @@ -0,0 +1,423 @@ + +# Floyd 算法精讲 + +[卡码网:97. 小明逛公园](https://kamacoder.com/problempage.php?pid=1155) + +【题目描述】 + +小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。 + + +给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。 + + +小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。 + +【输入描述】 + +第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。 + +接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。 + +接下里的一行包含一个整数 Q,表示观景计划的数量。 + +接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。 + +【输出描述】 + +对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。 + +【输入示例】 + +7 3 +1 2 4 +2 5 6 +3 6 8 +2 +1 2 +2 3 + +【输出示例】 + +4 +-1 + +【提示信息】 + +从 1 到 2 的路径长度为 4,2 到 3 之间并没有道路。 + +1 <= N, M, Q <= 1000. + +## 思路 + +本题是经典的多源最短路问题。 + +在这之前我们讲解过,dijkstra朴素版、dijkstra堆优化、Bellman算法、Bellman队列优化(SPFA) 都是单源最短路,即只能有一个起点。 + +而本题是多源最短路,即 求多个起点到多个终点的多条最短路径。 + +通过本题,我们来系统讲解一个新的最短路算法-Floyd 算法。 + +**Floyd 算法对边的权值正负没有要求,都可以处理**。 + +Floyd算法核心思想是动态规划。 + +例如我们再求节点1 到 节点9 的最短距离,用二维数组来表示即:grid[1][9],如果最短距离是10 ,那就是 grid[1][9] = 10。 + +那 节点1 到 节点9 的最短距离 是不是可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成呢? + +即 grid[1][9] = grid[1][5] + grid[5][9] + +节点1 到节点5的最短距离 是不是可以有 节点1 到 节点3的最短距离 + 节点3 到 节点5 的最短距离组成呢? + +即 grid[1][5] = grid[1][3] + grid[3][5] + +以此类推,节点1 到 节点3的最短距离 可以由更小的区间组成。 + +那么这样我们是不是就找到了,子问题推导求出整体最优方案的递归关系呢。 + +节点1 到 节点9 的最短距离 可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成, 也可以有 节点1 到节点7的最短距离 + 节点7 到节点9的最短距离的距离组成。 + +那么选哪个呢? + +是不是 要选一个最小的,毕竟是求最短路。 + +此时我们已经接近明确递归公式了。 + +之前在讲解动态规划的时候,给出过动规五部曲: + +* 确定dp数组(dp table)以及下标的含义 +* 确定递推公式 +* dp数组如何初始化 +* 确定遍历顺序 +* 举例推导dp数组 + +那么接下来我们还是用这五部来给大家讲解 Floyd。 + +1、确定dp数组(dp table)以及下标的含义 + +这里我们用 grid数组来存图,那就把dp数组命名为 grid。 + +grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。 + +可能有录友会想,凭什么就这么定义呢? + +节点i 到 节点j 的最短距离为m,这句话可以理解,但 以[1...k]集合为中间节点就理解不辽了。 + +节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。 + +你可以反过来想,节点i 到 节点j 中间一定经过很多节点,那么你能用什么方式来表述中间这么多节点呢? + +所以 这里的k不能单独指某个节点,k 一定要表示一个集合,即[1...k] ,表示节点1 到 节点k 一共k个节点的集合。 + + +2、确定递推公式 + +在上面的分析中我们已经初步感受到了递推的关系。 + +我们分两种情况: + +1. 节点i 到 节点j 的最短路径经过节点k +2. 节点i 到 节点j 的最短路径不经过节点k + +对于第一种情况,`grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]` + +节点i 到 节点k 的最短距离 是不经过节点k,中间节点集合为[1...k-1],所以 表示为`grid[i][k][k - 1]` + +节点k 到 节点j 的最短距离 也是不经过节点k,中间节点集合为[1...k-1],所以表示为 `grid[k][j][k - 1]` + +第二种情况,`grid[i][j][k] = grid[i][j][k - 1]` + +如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1...k-1],表示为 `grid[i][j][k - 1]` + +因为我们是求最短路,对于这两种情况自然是取最小值。 + +即: `grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])` + + +3、dp数组如何初始化 + +grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。 + +刚开始初始化k 是不确定的。 + +例如题目中只是输入边(节点2 -> 节点6,权值为3),那么grid[2][6][k] = 3,k需要填什么呢? + +把k 填成1,那如何上来就知道 节点2 经过节点1 到达节点6的最短距离是多少 呢。 + +所以 只能 把k 赋值为 0,本题 节点0 是无意义的,节点是从1 到 n。 + +这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。 + + +grid数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240425104247.png) + +红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要,下面我们在聊遍历顺序的时候还会再讲。 + +所以初始化代码: + +```CPP +vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); // C++定义了一个三位数组,10005是因为边的最大距离是10^4 + +for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2][0] = val; + grid[p2][p1][0] = val; // 注意这里是双向图 +} + +``` + +grid数组中其他元素数值应该初始化多少呢? + +本题求的是最小值,所以输入数据没有涉及到的节点的情况都应该初始为一个最大数。 + +这样才不会影响,每次计算去最小值的时候 初始值对计算结果的影响。 + +所以grid数组的定义可以是: + +```CPP +// C++写法,定义了一个三位数组,10005是因为边的最大距离是10^4 +vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); + +``` + +4、确定遍历顺序 + +从递推公式:`grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])` 可以看出,我们需要三个for循环,分别遍历i,j 和k + +而 k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。 + +那么这三个for的嵌套顺序应该是什么样的呢? + +我们来看初始化,我们是把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。 + +这就好比是一个三维坐标,i 和j 是平层,而k 是 垂直向上 的。 + +遍历的顺序是从底向上 一层一层去遍历。 + +所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历。如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240424120109.png) + +至于遍历 i 和 j 的话,for 循环的先后顺序无所谓。 + +代码如下: + +```CPP +for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } +} +``` + +有录友可能想,难道 遍历k 放在最里层就不行吗? + +k 放在最里层,代码是这样: + +```CPP +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + for (int k = 1; k <= n; k++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } +} +``` + +此时就遍历了 j 与 k 形成一个平面,i 则是纵面,那遍历 就是这样的: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240424115827.png) + + +而我们初始化的数据 是 k 为0, i 和 j 形成的平面做初始化,如果以 k 和 j 形成的平面去一层一层遍历,就造成了 递推公式 用不上上一轮计算的结果,从而导致结果不对(初始化的部分是 i 与j 形成的平面,在初始部分有讲过)。 + +我再给大家举一个测试用例 + +``` +5 4 +1 2 10 +1 3 1 +3 4 1 +4 2 1 +1 +1 2 +``` + +就是图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240424120942.png) + +求节点1 到 节点 2 的最短距离,运行结果是 10 ,但正确的结果很明显是3。 + +为什么呢? + +因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了,后面再也不会计算节点 1 和 节点 2的距离,同时也不会基于 初始化或者之前计算过的结果来计算,即:不会考虑 节点1 到 节点3, 节点3 到节点 4,节点4到节点2 的距离。 + + +造成这一原因,是 在三维立体坐标中, 我们初始化的是 i 和 i 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。 + + +而遍历k 的for循环如果放在中间呢,同样是 j 与k 行程一个平面,i 是纵面,遍历的也是这样: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240424115827.png) + +同样不能完全用上初始化 和 上一层计算的结果。 + +根据这个情况再举一个例子: + +``` +5 2 +1 2 1 +2 3 10 +1 +1 3 +``` + +图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240425112636.png) + +求 节点1 到节点3 的最短距离,如果k循环放中间,程序的运行结果是 -1,也就是不能到达节点3。 + +在计算 grid[i][j][k] 的时候,需要基于 grid[i][k][k-1] 和 grid[k][j][k-1]的数值。 + +也就是 计算 grid[1][3][2] (表示节点1 到 节点3,经过节点2) 的时候,需要基于 grid[1][2][1] 和 grid[2][3][1]的数值,而 我们初始化,只初始化了 k为0 的那一层。 + +造成这一原因 依然是 在三维立体坐标中, 我们初始化的是 i 和 j 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。 + + +很多录友对于 floyd算法的遍历顺序搞不懂,**其实 是没有从三维的角度去思考**,同时我把三维立体图给大家画出来,遍历顺序标出来,大家就很容易想明白,为什么 k 放在最外层 才能用上 初始化和上一轮计算的结果了。 + +5、举例推导dp数组 + +这里涉及到 三维矩阵,可以一层一层打印出来去分析,例如k=0 的这一层,k = 1的这一层,但一起把三维带数据的图画出来其实不太好画。 + +## 代码如下 + +以上分析完毕,最后代码如下: + + +```CPP +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); // 因为边的最大距离是10^4 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2][0] = val; + grid[p2][p1][0] = val; // 注意这里是双向图 + + } + // 开始 floyd + for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } + } + // 输出结果 + int z, start, end; + cin >> z; + while (z--) { + cin >> start >> end; + if (grid[start][end][n] == 10005) cout << -1 << endl; + else cout << grid[start][end][n] << endl; + } +} + +``` + +## 空间优化 + +这里 我们可以做一下 空间上的优化,从滚动数组的角度来看,我们定义一个 grid[n + 1][ n + 1][2] 这么大的数组就可以,因为k 只是依赖于 k-1的状态,并不需要记录k-2,k-3,k-4 等等这些状态。 + +那么我们只需要记录 grid[i][j][1] 和 grid[i][j][0] 就好,之后就是 grid[i][j][1] 和 grid[i][j][0] 交替滚动。 + +在进一步想,如果本层计算(本层计算即k相同,从三维角度来讲) gird[i][j] 用到了 本层中刚计算好的 grid[i][k] 会有什么问题吗? + +如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 小,说明确实有 i 到 k 的更短路径,那么基于 更小的 grid[i][k] 去计算 gird[i][j] 没有问题。 + +如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 大, 这不可能,因为这样也不会做更新 grid[i][k]的操作。 + +所以本层计算中,使用了本层计算过的 grid[i][k] 和 grid[k][j] 是没问题的。 + +那么就没必要区分,grid[i][k] 和 grid[k][j] 是 属于 k - 1 层的呢,还是 k 层的。 + +所以递归公式可以为: + +```CPP +grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]); +``` + +基于二维数组的本题代码为: + + +```CPP +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, 10005)); // 因为边的最大距离是10^4 + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + grid[p2][p1] = val; // 注意这里是双向图 + + } + // 开始 floyd + for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]); + } + } + } + // 输出结果 + int z, start, end; + cin >> z; + while (z--) { + cin >> start >> end; + if (grid[start][end] == 10005) cout << -1 << endl; + else cout << grid[start][end] << endl; + } +} + +``` + +* 时间复杂度: O(n^3) +* 空间复杂度:O(n^2) + +## 总结 + +本期如果上来只用二维数组来讲的话,其实更容易,但遍历顺序那里用二维数组其实是讲不清楚的,所以我直接用三维数组来讲,目的是将遍历顺序这里讲清楚。 + +理解了遍历顺序才是floyd算法最精髓的地方。 + + +floyd算法的时间复杂度相对较高,适合 稠密图且源点较多的情况。 + +如果是稀疏图,floyd是从节点的角度去计算了,例如 图中节点数量是 1000,就一条边,那 floyd的时间复杂度依然是 O(n^3) 。 + +如果 源点少,其实可以 多次dijsktra 求源点到终点。 + + + + + diff --git a/problems/kamacoder/0098.所有可达路径.md b/problems/kamacoder/0098.所有可达路径.md new file mode 100644 index 00000000..0573d081 --- /dev/null +++ b/problems/kamacoder/0098.所有可达路径.md @@ -0,0 +1,408 @@ + +# 98. 所有可达路径 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1170) + +[力扣题目讲解以及题目链接(核心代码模式)](https://programmercarl.com/0797.%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E8%B7%AF%E5%BE%84.html#%E6%80%9D%E8%B7%AF) + +【题目描述】 + +给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。 + +【输入描述】 + +第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边 + +后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径 + +【输出描述】 + +输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。 + +如果不存在任何一条路径,则输出 -1。 + +注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 `1 3 5`,而不是 `1 3 5 `, 5后面没有空格! + +【输入示例】 + +``` +5 5 +1 3 +3 5 +1 2 +2 4 +4 5 +``` + +【输出示例】 + +``` +1 3 5 +1 2 4 5 +``` + +提示信息 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240514103953.png) + +用例解释: + +有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。 + +因为拥有多条路径,所以输出结果为: + +``` +1 3 5 +1 2 4 5 +``` + +或 + +``` +1 2 4 5 +1 3 5 +``` + +都算正确。 + +数据范围: + +* 图中不存在自环 +* 图中不存在平行边 +* 1 <= N <= 100 +* 1 <= M <= 500 + + +## 插曲 + +这道题目是深度优先搜索,比较好的入门题。 + +如果对深度优先搜索还不够了解,可以先看这里:[深度优先搜索的理论基础](https://programmercarl.com/图论深搜理论基础.html) + +我依然总结了深搜三部曲,如果按照代码随想录刷题的录友,应该刷过 二叉树的递归三部曲,回溯三部曲。 + +**大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢**? 什么时候用深搜 什么时候用回溯? + +我在讲解[二叉树理论基础](https://programmercarl.com/二叉树理论基础.html)的时候,提到过,**二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用**。 + +那么回溯算法呢,**其实 回溯算法就是 深搜,只不过针对某一搜索场景 我们给他一个更细分的定义,叫做回溯算法**。 + +那有的录友可能说:那我以后称回溯算法为深搜,是不是没毛病? + +理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。 + +建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。 + +## 图的存储 + +在[图论理论基础篇](./图论理论基础.md) 中我们讲到了 两种 图的存储方式:邻接表 和 邻接矩阵。 + +本题我们将带大家分别实现这两个图的存储方式。 + +### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +本题我们会有n 个节点,因为节点标号是从1开始的,为了节点标号和下标对齐,我们申请 n + 1 * n + 1 这么大的二维数组。 + +```CPP +vector> graph(n + 1, vector(n + 1, 0)); +``` + +输入m个边,构造方式如下: + +```CPP +while (m--) { + cin >> s >> t; + // 使用邻接矩阵 ,1 表示 节点s 指向 节点t + graph[s][t] = 1; +} +``` + +### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造相对邻接矩阵难理解一些。 + +我在 [图论理论基础篇](./图论理论基础.md) 举了一个例子: + + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4指向节点1 + +我们需要构造一个数组,数组里的元素是一个链表。 + +C++写法: + +```CPP +// 节点编号从1到n,所以申请 n+1 这么大的数组 +vector> graph(n + 1); // 邻接表,list为C++里的链表 +``` + +输入m个边,构造方式如下: + +```CPP +while (m--) { + cin >> s >> t; + // 使用邻接表 ,表示 s -> t 是相连的 + graph[s].push_back(t); +} +``` + +本题我们使用邻接表 或者 邻接矩阵都可以,因为后台数据并没有对图的大小以及稠密度做很大的区分。 + +以下我们使用邻接矩阵的方式来讲解,文末我也会给出 使用邻接表的整体代码。 + +**注意邻接表 和 邻接矩阵的写法都要掌握**! + +## 深度优先搜索 + +本题是深度优先搜索的基础题目,关于深搜我在[图论深搜理论基础](./图论深搜理论基础.md) 已经有详细的讲解,图文并茂。 + +关于本题我会直接使用深搜三部曲来分析,如果对深搜不够了解,建议先看 [图论深搜理论基础](./图论深搜理论基础.md)。 + +深搜三部曲来分析题目: + +1. 确认递归函数,参数 + +首先我们dfs函数一定要存一个图,用来遍历的,需要存一个目前我们遍历的节点,定义为x。 + +还需要存一个n,表示终点,我们遍历的时候,用来判断当 x==n 时候 标明找到了终点。 + +(其实在递归函数的参数 不容易一开始就确定了,一般是在写函数体的时候发现缺什么,参加就补什么) + +至于 单一路径 和 路径集合 可以放在全局变量,那么代码是这样的: + +```CPP +vector> result; // 收集符合条件的路径 +vector path; // 0节点到终点的路径 +// x:目前遍历的节点 +// graph:存当前的图 +// n:终点 +void dfs (const vector>& graph, int x, int n) { +``` + +2. 确认终止条件 + +什么时候我们就找到一条路径了? + +当目前遍历的节点 为 最后一个节点 n 的时候 就找到了一条 从出发点到终止点的路径。 + +```CPP +// 当前遍历的节点x 到达节点n +if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; +} +``` + +3. 处理目前搜索节点出发的路径 + +接下来是走 当前遍历节点x的下一个节点。 + +首先是要找到 x节点指向了哪些节点呢? 遍历方式是这样的: + +```c++ +for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x指向的节点,就是节点i + } +} +``` + +接下来就是将 选中的x所指向的节点,加入到 单一路径来。 + +```C++ +path.push_back(i); // 遍历到的节点加入到路径中来 + +``` + + +进入下一层递归 + +```CPP +dfs(graph, i, n); // 进入下一层递归 +``` + +最后就是回溯的过程,撤销本次添加节点的操作。 + +为什么要有回溯,我在[图论深搜理论基础](./图论深搜理论基础.md) 也有详细的讲解。 + +该过程整体代码: + +```CPP +for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } +} +``` + +## 打印结果 + +ACM格式大家在输出结果的时候,要关注看看格式问题,特别是字符串,有的题目说的是每个元素后面都有空格,有的题目说的是 每个元素间有空格,最后一个元素没有空格。 + +有的题目呢,压根没说,那只能提交去试一试了。 + +很多录友在提交题目的时候发现结果一样,为什么提交就是不对呢。 + +例如示例输出是: + +`1 3 5` 而不是 `1 3 5 ` + +即 5 的后面没有空格! + +这是我们在输出的时候需要注意的点。 + +有录友可能会想,ACM格式就是麻烦,有空格没有空格有什么影响,结果对了不就行了? + +ACM模式相对于核心代码模式(力扣) 更考验大家对代码的掌控能力。 例如工程代码里,输出输出都是要自己控制的。这也是为什么大公司笔试,都是ACM模式。 + +以上代码中,结果都存在了 result数组里(二维数组,每一行是一个结果),最后将其打印出来。(重点看注释) + +```CPP +// 输出结果 +if (result.size() == 0) cout << -1 << endl; +for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { // 这里指打印到倒数第二个 + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; // 这里再打印倒数第一个,控制最后一个元素后面没有空格 +} +``` + +## 本题代码 + +### 邻接矩阵写法 + + +```CPP +#include +#include +using namespace std; +vector> result; // 收集符合条件的路径 +vector path; // 1节点到终点的路径 + +void dfs (const vector>& graph, int x, int n) { + // 当前遍历的节点x 到达节点n + if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; + } + for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + vector> graph(n + 1, vector(n + 1, 0)); + + while (m--) { + cin >> s >> t; + // 使用邻接矩阵 表示无线图,1 表示 s 与 t 是相连的 + graph[s][t] = 1; + } + + path.push_back(1); // 无论什么路径已经是从0节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.size() == 0) cout << -1 << endl; + for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; + } +} + +``` + +### 邻接表写法 + +```CPP +#include +#include +#include +using namespace std; + +vector> result; // 收集符合条件的路径 +vector path; // 1节点到终点的路径 + +void dfs (const vector>& graph, int x, int n) { + + if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; + } + for (int i : graph[x]) { // 找到 x指向的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + vector> graph(n + 1); // 邻接表 + while (m--) { + cin >> s >> t; + // 使用邻接表 ,表示 s -> t 是相连的 + graph[s].push_back(t); + + } + + path.push_back(1); // 无论什么路径已经是从0节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.size() == 0) cout << -1 << endl; + for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; + } +} + +``` + +## 总结 + +本题是一道简单的深搜题目,也可以说是模板题,和 [力扣797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) 思路是一样一样的。 + +很多录友做力扣的时候,轻松就把代码写出来了, 但做面试笔试的时候,遇到这样的题就写不出来了。 + +在力扣上刷题不用考虑图的存储方式,也不用考虑输出的格式。 + +而这些都是 ACM 模式题目的知识点(图的存储方式)和细节(输出的格式) + +所以我才会特别制作ACM题目,同样也重点去讲解图的存储和遍历方式,来帮大家去练习。 + +对于这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。 + +而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。 + +至于广搜理论基础,我们在下一篇在好好讲解,敬请期待! + + + diff --git a/problems/kamacoder/0099.岛屿的数量广搜.md b/problems/kamacoder/0099.岛屿的数量广搜.md new file mode 100644 index 00000000..dfcf062f --- /dev/null +++ b/problems/kamacoder/0099.岛屿的数量广搜.md @@ -0,0 +1,186 @@ + +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + +[力扣题目讲解以及题目链接(核心代码模式)](https://programmercarl.com/0200.%E5%B2%9B%E5%B1%BF%E6%95%B0%E9%87%8F.%E5%B9%BF%E6%90%9C%E7%89%88.html) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个数字,数字为 1 或者 0。 + +输出描述: + +输出一个整数,表示岛屿的数量。如果不存在岛屿,则输出 0。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +3 + +提示信息 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 广度优先搜索 + +不少同学用广搜做这道题目的时候,超时了。 这里有一个广搜中很重要的细节: + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +很多同学可能感觉这有区别吗? + +如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。 + +![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220727100846.png) + +超时写法 (从队列中取出节点再标记) + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + visited[curx][cury] = true; // 从队列中取出在标记走过 + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + } + } + } + +} +``` + + +加入队列 就代表走过,立刻标记,正确写法: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } + +} +``` + +以上两个版本其实,其实只有细微区别,就是 `visited[x][y] = true;` 放在的地方,这取决于我们对 代码中队列的定义,队列中的节点就表示已经走过的节点。 **所以只要加入队列,立即标记该节点走过**。 + +本题完整广搜代码: + +```CPP +#include +#include +#include +using namespace std; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(const vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } +} + + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + result++; // 遇到没访问过的陆地,+1 + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + + + cout << result << endl; +} + +``` + diff --git a/problems/kamacoder/0099.岛屿的数量深搜.md b/problems/kamacoder/0099.岛屿的数量深搜.md new file mode 100644 index 00000000..31067610 --- /dev/null +++ b/problems/kamacoder/0099.岛屿的数量深搜.md @@ -0,0 +1,179 @@ + +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + +[力扣题目讲解以及题目链接(核心代码模式)](https://programmercarl.com/0200.%E5%B2%9B%E5%B1%BF%E6%95%B0%E9%87%8F.%E6%B7%B1%E6%90%9C%E7%89%88.html) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个数字,数字为 1 或者 0。 + +输出描述: + +输出一个整数,表示岛屿的数量。如果不存在岛屿,则输出 0。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +3 + +提示信息 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如何把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 深度优先搜索 + +以下代码使用dfs实现,如果对dfs不太了解的话,**建议按照代码随想录的讲解顺序学习**。 + +C++代码如下: + +```CPP +// 版本一 +#include +#include +using namespace std; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(const vector>& grid, vector>& visited, int x, int y) { + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 没有访问过的 同时 是陆地的 + + visited[nextx][nexty] = true; + dfs(grid, visited, nextx, nexty); + } + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + visited[i][j] = true; + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + + cout << result << endl; +} +``` + +很多录友可能有疑惑,为什么 以上代码中的dfs函数,没有终止条件呢? 感觉递归没有终止很危险。 + +其实终止条件 就写在了 调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。 + +当然也可以这么写: + +```CPP +// 版本二 +#include +#include +using namespace std; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(const vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty); + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + cout << result << endl; +} +``` + +这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件判断 放在了 版本二 的 终止条件位置上。 + +**版本一的写法**是 :下一个节点是否能合法已经判断完了,传进dfs函数的就是合法节点。 + +**版本二的写法**是:不管节点是否合法,上来就dfs,然后在终止条件的地方进行判断,不合法再return。 + +**理论上来讲,版本一的效率更高一些**,因为避免了 没有意义的递归调用,在调用dfs之前,就做合法性判断。 但从写法来说,可能版本二 更利于理解一些。(不过其实都差不太多) + +很多同学看了同一道题目,都是dfs,写法却不一样,**有时候有终止条件,有时候连终止条件都没有,其实这就是根本原因,两种写法而已**。 + + +## 总结 + +其实本题是 dfs,bfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。 + +本篇我只给出的dfs的写法,大家发现我写的还是比较细的,那么后面我再单独给出本题的bfs写法,虽然是模板题,但依然有很多注意的点,敬请期待! + diff --git a/problems/图论并查集理论基础.md b/problems/kamacoder/图论并查集理论基础.md similarity index 99% rename from problems/图论并查集理论基础.md rename to problems/kamacoder/图论并查集理论基础.md index 6a1456e9..3bf45f77 100644 --- a/problems/图论并查集理论基础.md +++ b/problems/kamacoder/图论并查集理论基础.md @@ -1,4 +1,3 @@ - # 并查集理论基础 接下来我们来讲一下并查集,首先当然是并查集理论基础。 @@ -455,4 +454,3 @@ void join(int u, int v) { 敬请期待 并查集题目精讲系列。 - diff --git a/problems/图论广搜理论基础.md b/problems/kamacoder/图论广搜理论基础.md similarity index 92% rename from problems/图论广搜理论基础.md rename to problems/kamacoder/图论广搜理论基础.md index b631f4f5..c494e0d0 100644 --- a/problems/图论广搜理论基础.md +++ b/problems/kamacoder/图论广搜理论基础.md @@ -1,9 +1,3 @@ -

- - - -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

- # 广度优先搜索理论基础 @@ -156,8 +150,3 @@ def bfs(grid, visited, x, y): ``` - -

- - - diff --git a/problems/kamacoder/图论总结篇.md b/problems/kamacoder/图论总结篇.md new file mode 100644 index 00000000..92b3581d --- /dev/null +++ b/problems/kamacoder/图论总结篇.md @@ -0,0 +1,33 @@ + +# 图论总结篇 + +从深搜广搜 到并查集,从最小生成树到拓扑排序, 最后是最短路算法系列。 + +至此算上本篇,一共30篇文章,图论之旅就在此收官了。 + + +## 深搜与广搜 + +深搜与广搜是图论里基本的搜索方法,大家需要掌握三点: + +* 搜索方式:深搜是可一个方向搜,不到黄河不回头。 广搜是围绕这起点一圈一圈的去搜。 +* 代码模板:需要熟练掌握深搜和广搜的基本写法。 +* 应用场景:图论题目基本上可以即用深搜也可以广搜,无疑是用哪个方便而已 + +深搜注意事项 + +同样是深搜模板题,会有两种写法, + +广搜注意事项 + +## 并查集 + +## 最小生成树 + +## 拓扑排序 + +## 最短路算法 + + + +算法4,只讲解了 Dijkstra,SPFA (Bellman-Ford算法基于队列) 和 拓扑排序, diff --git a/problems/图论深搜理论基础.md b/problems/kamacoder/图论深搜理论基础.md similarity index 93% rename from problems/图论深搜理论基础.md rename to problems/kamacoder/图论深搜理论基础.md index 7f847f8f..2be97057 100644 --- a/problems/图论深搜理论基础.md +++ b/problems/kamacoder/图论深搜理论基础.md @@ -1,8 +1,3 @@ -

- - - -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

# 深度优先搜索理论基础 @@ -71,7 +66,7 @@ 有递归的地方就有回溯,那么回溯在哪里呢? -就地递归函数的下面,例如如下代码: +就递归函数的下面,例如如下代码: ```cpp void dfs(参数) { @@ -199,7 +194,3 @@ for (选择:本节点所连接的其他节点) { 后面我也会给大家安排具体练习的题目,依旧是代码随想录的风格,循序渐进由浅入深! -

- - - diff --git a/problems/kamacoder/图论理论基础.md b/problems/kamacoder/图论理论基础.md new file mode 100644 index 00000000..be42a5fc --- /dev/null +++ b/problems/kamacoder/图论理论基础.md @@ -0,0 +1,222 @@ + +# 图论理论基础 + +这一篇我们正式开始图论! + +## 图的基本概念 + +二维坐标中,两点可以连成线,多个点连成的线就构成了图。 + +当然图也可以就一个节点,甚至没有节点(空图) + +### 图的种类 + +整体上一般分为 有向图 和 无向图。 + +有向图是指 图中边是有方向的: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510195737.png) + +无向图是指 图中边没有方向: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510195451.png) + +加权有向图,就是图中边是有权值的,例如: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240510195821.png) + +加权无向图也是同理。 + +### 度 + +无向图中有几条边连接该节点,该节点就有几度。 + +例如,该无向图中,节点4的度为5,节点6的度为3。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511115029.png) + +在有向图中,每个节点有出度和入度。 + +出度:从该节点出发的边的个数。 + +入度:指向该节点边的个数。 + +例如,该有向图中,节点3的入度为2,出度为1,节点1的入度为0,出度为2。 + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511115235.png) + + +## 连通性 + +在图中表示节点的连通情况,我们称之为连通性。 + +### 连通图 + +在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511102351.png) + +如果有节点不能到达其他节点,则为非连通图,如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511102449.png) + +节点1 不能到达节点4。 + +### 强连通图 + +在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。 + +这里有录友可能想,这和无向图中的连通图有什么区别,不是一样的吗? + +我们来看这个有向图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511104531.png) + +这个图是强连通图吗? + +初步一看,好像这节点都连着呢,但这不是强连通图,节点1 可以到节点5,但节点5 不能到 节点1 。 + +强连通图是在有向图中**任何两个节点是可以相互到达** + +下面这个有向图才是强连通图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511113101.png) + + +### 连通分量 + +在无向图中的极大连通子图称之为该图的一个连通分量。 + +只看概念大家可能不理解,我来画个图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511111559.png) + +该无向图中 节点1、节点2、节点5 构成的子图就是 该无向图中的一个连通分量,该子图所有节点都是相互可达到的。 + +同理,节点3、节点4、节点6 构成的子图 也是该无向图中的一个连通分量。 + +那么无向图中 节点3 、节点4 构成的子图 是该无向图的联通分量吗? + +不是! + +因为必须是极大联通子图才能是连通分量,所以 必须是节点3、节点4、节点6 构成的子图才是连通分量。 + +在图论中,连通分量是一个很重要的概念,例如岛屿问题(后面章节会有专门讲解)其实就是求连通分量。 + +### 强连通分量 + +在有向图中极大强连通子图称之为该图的强连通分量。 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240511112951.png) + +节点1、节点2、节点3、节点4、节点5 构成的子图是强连通分量,因为这是强连通图,也是极大图。 + +节点6、节点7、节点8 构成的子图 不是强连通分量,因为这不是强连通图,节点8 不能达到节点6。 + +节点1、节点2、节点5 构成的子图 也不是 强连通分量,因为这不是极大图。 + + +## 图的构造 + +我们如何用代码来表示一个图呢? + +一般使用邻接表、邻接矩阵 或者用类来表示。 + +主流是 邻接表和邻接矩阵。 + +### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。 + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + +如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间。 + +图中有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4指向节点1 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的连接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点连接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点连接其他节点的数量。 +* 实现相对复杂,不易理解 + + +**以上大家可能理解比较模糊,没关系**,因为大家还没做过图论的题目,对于图的表达没有概念。 + +这里我先不给出具体的实现代码,大家先有个初步印象,在后面算法题实战中,我还会讲到具体代码实现,等带大家做算法题,写了代码之后,自然就理解了。 + +## 图的遍历方式 + +图的遍历方式基本是两大类: + +* 深度优先搜索(dfs) +* 广度优先搜索(bfs) + +在讲解二叉树章节的时候,其实就已经讲过这两种遍历方式。 + +二叉树的递归遍历,是dfs 在二叉树上的遍历方式。 + +二叉树的层序遍历,是bfs 在二叉树上的遍历方式。 + +dfs 和 bfs 一种搜索算法,可以在不同的数据结构上进行搜索,在二叉树章节里是在二叉树这样的数据结构上搜索。 + +而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。 + +## 总结 + +以上知识点 大家先有个印象,上面提到的每个知识点,其实都需要大篇幅才能讲明白的。 + +我这里先给大家做一个概括,后面章节会针对每个知识点都会有对应的算法题和针对性的讲解,大家再去深入学习。 + +图论是非常庞大的知识体系,上面的内容还不足以概括图论内容,仅仅是理论基础而已。 + +在图论章节我会带大家深入讲解 深度优先搜索(DFS)、广度优先搜索(BFS)、并查集、拓扑排序、最小生成树系列、最短路算法系列等等。 + +敬请期待! + + diff --git a/problems/kamacoder/最短路问题总结篇.md b/problems/kamacoder/最短路问题总结篇.md new file mode 100644 index 00000000..7f4ee6f8 --- /dev/null +++ b/problems/kamacoder/最短路问题总结篇.md @@ -0,0 +1,48 @@ + +# 最短路算法总结篇 + +至此已经讲解了四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd。 + +针对这四大最短路算法,我用了七篇长文才彻底讲清楚,分别是: + +* dijkstra朴素版 +* dijkstra堆优化版 +* Bellman_ford +* Bellman_ford 队列优化算法(又名SPFA) +* bellman_ford 算法判断负权回路 +* bellman_ford之单源有限最短路 +* Floyd 算法精讲 + + +最短路算法比较复杂,而且各自有各自的应用场景,我来用一张表把讲过的最短路算法的使用场景都展现出来: + +![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240508121355.png) + + +可能有同学感觉:这个表太复杂了,我记也记不住。 + +其实记不住的原因还是对 这几个最短路算法没有深刻的理解。 + +这里我给大家一个大体使用场景的分析: + +如果遇到单源且边为正数,直接Dijkstra。 + +至于 使用朴素版还是 堆优化版 还是取决于图的稠密度, 多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。 + +一般情况下,可以直接用堆优化版本。 + +如果遇到单源边可为负数,直接 Bellman-Ford,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。 + +一般情况下,直接用 SPFA。 + +如果有负权回路,优先 Bellman-Ford, 如果是有限节点最短路 也优先 Bellman-Ford,理由是写代码比较方便。 + +如果是遇到多源点求最短路,直接 Floyd。 + +除非 源点特别少,且边都是正数,那可以 多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。 + + + + + + diff --git a/problems/qita/shejimoshi.md b/problems/qita/shejimoshi.md index cf980e45..d5342c61 100644 --- a/problems/qita/shejimoshi.md +++ b/problems/qita/shejimoshi.md @@ -1,4 +1,6 @@ +# 23种设计模式精讲 | 配套练习题 | 卡码网 + 关于设计模式的学习,大家应该还是看书或者看博客,但却没有一个边学边练的学习环境。 学完了一种设计模式 是不是应该去练一练? diff --git a/problems/qita/xunlianying.md b/problems/qita/xunlianying.md index 0d8c1c83..f13290f3 100644 --- a/problems/qita/xunlianying.md +++ b/problems/qita/xunlianying.md @@ -1,7 +1,9 @@ # 代码随想录算法训练营 -> 训练营17期将在6月28日开营,目前可以报名,提前拉群,在群里等着开营就好! +::: tip 通知 +训练营35期,将于 4月3日开营,目前可以报名,报名后提前拉群,在群里等着开营就好。 +::: 大家可以百度搜索:代码随想录算法训练营, 看看往期录友们在训练营里打卡总结的博客。 @@ -9,14 +11,39 @@ 这是训练营里录友坚持到最后一天的打卡,大家可以看看他们的博客是每天都有记录的: -* [训练营结束,深感坚持是最难的(Java-犯困-东南研二)](https://blog.csdn.net/weixin_57956443/article/details/128995318) +* [这种方式,有效逼我坚持下来(C++-小飞-嘉院大三)(精华)](https://blog.csdn.net/weixin_60353640/article/details/133797799) +* [完成比完美重要(Java-小姜-已工作/南京)(精华)](https://xie.infoq.cn/article/3d07b4040ceab0f546d66e3e1) +* [已经刷了500题的基础,参加训练营依然收获满满(Java-怪懒懒-求职)(精华)](https://blog.csdn.net/2301_78266314/article/details/132144046) + +* [算法超级弱,最后坚持下来了(Java-信任呢-上大研二)(精华)](https://blog.csdn.net/xinrenne/article/details/133267089) +* [第一次比较完整的刷题训练经历,群里氛围超级好(JAVA-雷贯三十三重天-北航研二)(精华)](https://blog.csdn.net/qq_44120129/article/details/133230372) +* [我全程坚持下来,还是很有成就感的(python-wj-待业)(精华)](https://blog.csdn.net/u013441272/article/details/133229421) + +* [一点基础都没有,坚持下来了(C++ 润 大二)(精华)](https://blog.csdn.net/m0_74583479/article/details/132776719) +* [这个钱花的很值得(C++-GMZ-研一)(精华)](https://blog.csdn.net/weixin_43303286/article/details/132796571) +* [看着名单里录友都在坚持,自己也要坚持(C++-凯-湖工大研三)(精华)](https://blog.csdn.net/weixin_62453859/article/details/132788830) + +* [一刷心得(Java-小何同学-广财大二)(精华)](https://juejin.cn/post/7272250890597531684) +* [花钱买服务、买环境、买时间(Java-古今-大工研二)(精华)](https://blog.csdn.net/dannky_Z/article/details/132532049) +* [一刷心得(java-唔哩凹-大三)(精华)](https://blog.csdn.net/iwtup/article/details/132545456) + +* [训练营结束有点不舍,坚持最久的一件事(C++-徐一-中科院研二)(精华)](https://blog.csdn.net/weixin_46108098/article/details/132158352) +* [同学推荐,报名训练营,坚持下来了(c++-刘浩-沈自所-研二)(精华)](https://blog.csdn.net/qq1156148707/article/details/132155446) + +* [每日的刷题训练真的艰难,但坚持下来了(C++-五-已工作福建)(精华)](https://blog.csdn.net/weixin_44952586/article/details/131909720) +* [加入训练营,就是因为这个气氛,只靠自己很难坚持(cpp-Lord HouSton-cqu研二)(精华)](https://blog.csdn.net/HSL13594379250/article/details/131889934) +* [很幸运,我坚持下来了,感觉收货满满(java-李-UCAS研0)(精华)](https://blog.csdn.net/ResNet156/article/details/131920163) +* [谈谈自己的收获,养成了写博客的习惯(java-翌-研二)(精华)](https://blog.csdn.net/weixin_47460244/article/details/131912316) + +* [养成了刷题的习惯(C++-热心市民C先生-南理工研一)(精华)](https://blog.csdn.net/qqq1521902442/article/details/131614999) +* [工作也坚持下来(Python-Hongying-已工作杭州)(精华)](https://blog.csdn.net/weixin_42286468/article/details/131628069) +* [入营不亏(C++-小叶子-云财研二)(精华)](https://blog.csdn.net/dream_aleaf/article/details/131613667) + * [训练营一刷总结(Java-HQH-研二)](https://blog.csdn.net/weixin_43821876/article/details/128991822) * [训练营总结,一群人才能走的更远(Java-Lixy-已工作南京)](https://blog.csdn.net/weixin_45368277/article/details/128997823) * [训练营总结,中途🐑了,也坚持下来(C++-Jane-科大研二)](https://blog.csdn.net/Jane_10358/article/details/128977424) * [这两个月有很多不可控因素,但依然坚持下来(java-hha-南工大二)](https://blog.csdn.net/qerwtrt4t/article/details/128975401) * [训练营总结,最后坚持下来(C++ - 阿舟 - 已工作武汉)](https://blog.csdn.net/m0_74360161/article/details/129000723) -* [训练营总结,一刷知识点回顾(Java-魏-待就业)](https://blog.csdn.net/weixin_48111139/article/details/128973746) -* [在训练营中,零基础刷一遍的感受(C++-东风-东北大学研二)](https://blog.csdn.net/nightcood/article/details/128947111) 博客链接:[https://blog.csdn.net/m0_61724447/article/details/128443084](https://blog.csdn.net/m0_61724447/article/details/128443084) @@ -69,7 +96,7 @@ ----------- -### 训练营的目的是什么? +## 训练营的目的是什么? 对于刷题,学算法,[《代码随想录》](https://programmercarl.com/other/publish.html)(programmercarl.com)已经把刷题顺序给大家列好了,大家跟着刷就行。 @@ -97,7 +124,7 @@ 我亲自给大家规划节奏,大家一起按照我的节奏来,规定时间内,一刷一定能把代码随想录所有内容吃透,然后大家自己去二刷,三刷就好了,师傅领进门修行在个人。 -### 训练营提供一些什么呢? +## 训练营提供一些什么呢? 1.具体内容 @@ -180,7 +207,7 @@ 毕竟内容是开源的,质量如何 大家自己去看就好。 -### 训练营的学习方式 +## 训练营的学习方式 组织方式:一个学习微信群(180人左右),大家进群之后,等群公告就好,我会通知开始时间和每日刷题计划。 @@ -194,11 +221,11 @@ 所需语言:**所有语种都可以**,毕竟代码随想录几乎支持所有主流语言,**也会针对大家所用的语言做针对性答疑**。 -### 开营时间 +## 开营时间 -**训练营开始常态化报名,即一直可以报名,当人满180人的时候,就开始新的一期**。 最新的一期可以看文章评论区。 +**训练营开始常态化报名,即一直可以报名,当人满180人的时候,就开始新的一期**。 最新的一期可以看文章评论区,或者文章开头。 -### 训练营的价格 +## 训练营的价格 大家应该最关心的是价格了,**定价依然是268元**,注意这是两个月训练营的费用,而且是全程规划,指导,监督和答疑。 @@ -208,7 +235,7 @@ 后面一定会涨价的,**如果你确实需要有人带,有监督,给规划,有答疑,能花两个月时间更下来的话,还是早报早学习**。 -### 我适合报名吗? +## 我适合报名吗? 符合一下特点的录友可以报名: @@ -233,7 +260,7 @@ **训练营不限编程语言**,任何语言都可以报名,都会答疑。 -### 常见疑问 +## 常见疑问 **海外录友有时差可以报名吗**? @@ -265,22 +292,29 @@ 至于三个月的训练营,是可以考虑的,不过安排时间还要待定。 -### 报名方式 +## 报名方式 -扫码支付268元。 (如果是[代码随想录知识星球](https://programmercarl.com/other/kstar.html)成员录友,只需要支付238元,提交客服的时候需提供知识星球截图,**注意一定要是代码随想录知识星球**) +* 正常支付,价格:268 (支付成功后,支付记录发给客服 +* 知识星球录友支付,价格:238 (支付成功后,[代码随想录知识星球](https://programmercarl.com/other/kstar.html)截图 和 支付记录发给客服 +* 往期算法训练营录友再次报名,价格:130 (支付成功后,往期训练营群或者支付记录 和 本次支付记录发给客服 + +(**注意一定要是[代码随想录知识星球](https://programmercarl.com/other/kstar.html)成员才会有优惠**) + +支付宝支付如下:

+[微信支付点击这里](https://www.programmercarl.com/other/weixinzhifu.html) + 付款后,将付款截图发给客服,客服会在24h内统一回复,**所以大家发给客服信息不要急,当天一定会回复的**。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230603175235.png) - -客服的联系方式就在大家的微信聊天窗口,不用担心突出聊天窗口错过消息,客服回复之后 会有微信提示的。 +
关于训练营的任何问题,可以在客服这里咨询! -### 最后 + +## 最后 训练营其实算是代码随想录的一个补充,其内容都是免费开放的,有学习能力的录友自己学习就好。 @@ -294,5 +328,10 @@ 关于训练营的任何疑问都可以扫码联系客服 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230603175235.png) +
+ + + + + diff --git a/problems/toolgithub.sh b/problems/toolgithub.sh index ebe9c7df..9e005f0c 100644 --- a/problems/toolgithub.sh +++ b/problems/toolgithub.sh @@ -7,21 +7,35 @@ ######################################################################### #!/bin/bash +#

+# +# +# +#

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+ +# +#

+# +# +# + for i in *.md do if [[ $i != 'README.md' ]] then # 移除开头 - # sed -i '' '/align/d;/\"\"><\/a>/d;/<\/p>/d;/<\/a>/d;/20210924105952.png/d;/_blank/d' $i - # # 移除结尾 - # sed -i '' '/--------------/d;/训练营/d;' $i - # # 添加开头 - # # 记得从后向前添加 - # ex -sc '1i|

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

' -cx $i - # ex -sc '1i|' -cx $i - # ex -sc '1i| ' -cx $i - # ex -sc '1i|' -cx $i - # ex -sc '1i|

' -cx $i + sed -i '' '/align/d;/\"\"><\/a>/d;/<\/p>/d;/<\/a>/d;/20210924105952.png/d;/_blank/d' $i + # 移除结尾 + sed -i '' '/训练营/d;/网站星球宣传海报/d' $i + + + # 添加开头 + # 记得从后向前添加 + ex -sc '1i|

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

' -cx $i + ex -sc '1i|' -cx $i + ex -sc '1i| ' -cx $i + ex -sc '1i|' -cx $i + ex -sc '1i|

' -cx $i # echo '## 其他语言版本' >> $i # echo '\n' >> $i # echo 'Java:' >> $i diff --git a/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md b/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md index 442a5055..c2f5efd9 100644 --- a/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md +++ b/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划之编辑距离总结篇 diff --git a/problems/二叉树中递归带着回溯.md b/problems/二叉树中递归带着回溯.md index 67570bc8..42d78ae3 100644 --- a/problems/二叉树中递归带着回溯.md +++ b/problems/二叉树中递归带着回溯.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 二叉树:以为使用了递归,其实还隐藏着回溯 diff --git a/problems/二叉树总结篇.md b/problems/二叉树总结篇.md index 739184bb..8db40d65 100644 --- a/problems/二叉树总结篇.md +++ b/problems/二叉树总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 二叉树:总结篇!(需要掌握的二叉树技能都在这里了) diff --git a/problems/二叉树理论基础.md b/problems/二叉树理论基础.md index 50d592a2..c6658277 100644 --- a/problems/二叉树理论基础.md +++ b/problems/二叉树理论基础.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 二叉树理论基础篇 @@ -313,8 +313,8 @@ public class TreeNode public TreeNode(int x) { val = x; } } ``` +

- diff --git a/problems/二叉树的统一迭代法.md b/problems/二叉树的统一迭代法.md index ee4899b1..13c50737 100644 --- a/problems/二叉树的统一迭代法.md +++ b/problems/二叉树的统一迭代法.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 统一写法是一种什么感觉 @@ -835,8 +835,8 @@ public IList PostorderTraversal(TreeNode root) } ``` +

- diff --git a/problems/二叉树的迭代遍历.md b/problems/二叉树的迭代遍历.md index 35a01a7f..8549bac1 100644 --- a/problems/二叉树的迭代遍历.md +++ b/problems/二叉树的迭代遍历.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 听说还可以用非递归的方式 diff --git a/problems/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md index 4621d4a7..a1d49e98 100644 --- a/problems/二叉树的递归遍历.md +++ b/problems/二叉树的递归遍历.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 一看就会,一写就废! @@ -48,7 +48,7 @@ void traversal(TreeNode* cur, vector& vec) if (cur == NULL) return; ``` -3. **确定单层递归的逻辑**:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: +3. **确定单层递归的逻辑**:前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: ```cpp vec.push_back(cur->val); // 中 @@ -189,7 +189,7 @@ class Solution: res.append(node.val) dfs(node.left) dfs(node.right) - + dfs(root) return res ``` @@ -206,7 +206,7 @@ class Solution: dfs(node.left) res.append(node.val) dfs(node.right) - + dfs(root) return res ``` ```python @@ -224,7 +224,8 @@ class Solution: dfs(node.left) dfs(node.right) res.append(node.val) - + + dfs(root) return res ``` @@ -287,52 +288,91 @@ func postorderTraversal(root *TreeNode) (res []int) { 前序遍历: ```Javascript var preorderTraversal = function(root) { - let res=[]; - const dfs=function(root){ - if(root===null)return ; - //先序遍历所以从父节点开始 - res.push(root.val); - //递归左子树 - dfs(root.left); - //递归右子树 - dfs(root.right); - } - //只使用一个参数 使用闭包进行存储结果 - dfs(root); - return res; +// 第一种 +// let res=[]; +// const dfs=function(root){ +// if(root===null)return ; +// //先序遍历所以从父节点开始 +// res.push(root.val); +// //递归左子树 +// dfs(root.left); +// //递归右子树 +// dfs(root.right); +// } +// //只使用一个参数 使用闭包进行存储结果 +// dfs(root); +// return res; +// 第二种 + return root + ? [ + // 前序遍历:中左右 + root.val, + // 递归左子树 + ...preorderTraversal(root.left), + // 递归右子树 + ...preorderTraversal(root.right), + ] + : []; }; ``` 中序遍历 ```javascript var inorderTraversal = function(root) { - let res=[]; - const dfs=function(root){ - if(root===null){ - return ; - } - dfs(root.left); - res.push(root.val); - dfs(root.right); - } - dfs(root); - return res; +// 第一种 + + // let res=[]; + // const dfs=function(root){ + // if(root===null){ + // return ; + // } + // dfs(root.left); + // res.push(root.val); + // dfs(root.right); + // } + // dfs(root); + // return res; + +// 第二种 + return root + ? [ + // 中序遍历:左中右 + // 递归左子树 + ...inorderTraversal(root.left), + root.val, + // 递归右子树 + ...inorderTraversal(root.right), + ] + : []; }; ``` 后序遍历 ```javascript var postorderTraversal = function(root) { - let res=[]; - const dfs=function(root){ - if(root===null){ - return ; - } - dfs(root.left); - dfs(root.right); - res.push(root.val); - } - dfs(root); - return res; + // 第一种 + // let res=[]; + // const dfs=function(root){ + // if(root===null){ + // return ; + // } + // dfs(root.left); + // dfs(root.right); + // res.push(root.val); + // } + // dfs(root); + // return res; + + // 第二种 + // 后续遍历:左右中 + return root + ? [ + // 递归左子树 + ...postorderTraversal(root.left), + // 递归右子树 + ...postorderTraversal(root.right), + root.val, + ] + : []; }; ``` @@ -631,8 +671,8 @@ public void Traversal(TreeNode cur, IList res) } ``` +

- diff --git a/problems/关于时间复杂度,你不知道的都在这里!.md b/problems/关于时间复杂度,你不知道的都在这里!.md deleted file mode 100644 index a1e553d0..00000000 --- a/problems/关于时间复杂度,你不知道的都在这里!.md +++ /dev/null @@ -1,165 +0,0 @@ -

- - - -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

- -# 时间复杂度 - -## 究竟什么是时间复杂度 - -**时间复杂度是一个函数,它定性描述该算法的运行时间**。 - -我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的大体时间。 - -那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。 - -假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))。 - -## 什么是大O - -这里的大O是指什么呢,说到时间复杂度,**大家都知道O(n),O(n^2),却说不清什么是大O**。 - -算法导论给出的解释:**大O用来表示上界的**,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。 - -同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是O(n^2) 。 - -输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是O(n),但如果数据是逆序的话,插入排序的时间复杂度就是O(n^2),也就对于所有输入情况来说,最坏是O(n^2) 的时间复杂度,所以称插入排序的时间复杂度为O(n^2)。 - -同样的同理再看一下快速排序,都知道快速排序是O(nlog n),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)**。 - -**但是我们依然说快速排序是O(nlog n)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示: -![时间复杂度4,一般情况下的时间复杂度](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728185745611.png) - -我们主要关心的还是一般情况下的数据形式。 - -**面试中说道算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 - - -## 不同数据规模的差异 - -如下图中可以看出不同算法的时间复杂度在不同数据输入规模下的差异。 - -![时间复杂度,不同数据规模的差异](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728191447384.png) - -在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。 - -就像上图中 O(5n^2) 和 O(100n) 在n为20之前 很明显 O(5n^2)是更优的,所花费的时间也是最少的。 - -那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说O(100n) 就是O(n)的时间复杂度,O(5n^2) 就是O(n^2)的时间复杂度,而且要默认O(n) 优于O(n^2) 呢 ? - -这里就又涉及到大O的定义,**因为大O就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量**。 - -例如上图中20就是那个点,n只要大于20 常数项系数已经不起决定性作用了。 - -**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**: - -O(1) 常数阶 < O(\log n) 对数阶 < O(n) 线性阶 < O(n^2) 平方阶 < O(n^3) 立方阶 < O(2^n)指数阶 - -但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了。 - -## 复杂表达式的化简 - -有时候我们去计算时间复杂度的时候发现不是一个简单的O(n) 或者O(n^2), 而是一个复杂的表达式,例如: - -``` -O(2*n^2 + 10*n + 1000) -``` - -那这里如何描述这个算法的时间复杂度呢,一种方法就是简化法。 - -去掉运行时间中的加法常数项 (因为常数项并不会因为n的增大而增加计算机的操作次数)。 - -``` -O(2*n^2 + 10*n) -``` - -去掉常数系数(上文中已经详细讲过为什么可以去掉常数项的原因)。 - -``` -O(n^2 + n) -``` - -只保留最高项,去掉数量级小一级的n (因为n^2 的数据规模远大于n),最终简化为: - -``` -O(n^2) -``` - -如果这一步理解有困难,那也可以做提取n的操作,变成O(n(n+1)),省略加法常数项后也就变成了: - -``` -O(n^2) -``` - -所以最后我们说:这个算法的算法时间复杂度是O(n^2) 。 - - -也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于O(3 × n^2), -O(2 × n^2 + 10 × n + 1000) < O(3 × n^2),3 × n^2省略掉常数项系数,最终时间复杂度也是O(n^2)。 - -## O(log n)中的log是以什么为底? - -平时说这个算法的时间复杂度是logn的,那么一定是log 以2为底n的对数么? - -其实不然,也可以是以10为底n的对数,也可以是以20为底n的对数,**但我们统一说 logn,也就是忽略底数的描述**。 - -为什么可以这么做呢?如下图所示: - -![时间复杂度1.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728191447349.png) - - -假如有两个算法的时间复杂度,分别是log以2为底n的对数和log以10为底n的对数,那么这里如果还记得高中数学的话,应该不能理解`以2为底n的对数 = 以2为底10的对数 * 以10为底n的对数`。 - -而以2为底10的对数是一个常数,在上文已经讲述了我们计算时间复杂度是忽略常数项系数的。 - -抽象一下就是在时间复杂度的计算过程中,log以i为底n的对数等于log 以j为底n的对数,所以忽略了i,直接说是logn。 - -这样就应该不难理解为什么忽略底数了。 - -## 举一个例子 - -通过这道面试题目,来分析一下时间复杂度。题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。 - -如果是暴力枚举的话,时间复杂度是多少呢,是O(n^2)么? - -这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是O(m × n × n)。 - -接下来再想一下其他解题思路。 - -先对n个字符串按字典序来排序,排序后n个字符串就是有序的,意味着两个相同的字符串就是挨在一起,然后再遍历一遍n个字符串,这样就找到两个相同的字符串了。 - -那看看这种算法的时间复杂度,快速排序时间复杂度为O(nlog n),依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m × n × log n)。 - -之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m × n × log n + n × m)。 - -我们对O(m × n × log n + n × m)进行简化操作,把m × n提取出来变成O(m × n × (log n + 1)),再省略常数项最后的时间复杂度是O(m × n × log n)。 - -最后很明显O(m × n × log n) 要优于O(m × n × n)! - -所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。 - -这就是我们通过分析两种算法的时间复杂度得来的结论。 - -**当然这不是这道题目的最优解,我仅仅是用这道题目来讲解一下时间复杂度**。 - -## 总结 - -本篇讲解了什么是时间复杂度,复杂度是用来干什么的,以及数据规模对时间复杂度的影响。 - -还讲解了被大多数同学忽略的大O的定义以及log究竟是以谁为底的问题。 - -再分析了如何简化复杂的时间复杂度,最后举一个具体的例子,把本篇的内容串起来。 - -相信看完本篇,大家对时间复杂度的认识会深刻很多! - -如果感觉「代码随想录」很不错,赶快推荐给身边的朋友同学们吧,他们发现和「代码随想录」相见恨晚! - - - - -

- - - - diff --git a/problems/前序/什么是核心代码模式,什么又是ACM模式?.md b/problems/前序/ACM模式.md similarity index 100% rename from problems/前序/什么是核心代码模式,什么又是ACM模式?.md rename to problems/前序/ACM模式.md diff --git a/problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md b/problems/前序/内存消耗.md similarity index 100% rename from problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md rename to problems/前序/内存消耗.md diff --git a/problems/前序/力扣上的代码想在本地编译运行?.md b/problems/前序/力扣上的代码在本地编译运行.md similarity index 100% rename from problems/前序/力扣上的代码想在本地编译运行?.md rename to problems/前序/力扣上的代码在本地编译运行.md diff --git a/problems/前序/关于时间复杂度,你不知道的都在这里!.md b/problems/前序/时间复杂度.md similarity index 98% rename from problems/前序/关于时间复杂度,你不知道的都在这里!.md rename to problems/前序/时间复杂度.md index 9d00a0f2..61f0a7ef 100644 --- a/problems/前序/关于时间复杂度,你不知道的都在这里!.md +++ b/problems/前序/时间复杂度.md @@ -19,7 +19,7 @@ **时间复杂度是一个函数,它定性描述该算法的运行时间**。 -我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的答题时间。 +我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的大体时间。 那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。 @@ -42,7 +42,7 @@ 我们主要关心的还是一般情况下的数据形式。 -**面试中说道算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 +**面试中说的算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 ## 不同数据规模的差异 @@ -61,7 +61,7 @@ 例如上图中20就是那个点,n只要大于20 常数项系数已经不起决定性作用了。 -**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**: +**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂度的一个排行如下所示**: O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶 diff --git a/problems/前序/程序员简历.md b/problems/前序/程序员简历.md index fc06c713..7fbfa1fd 100644 --- a/problems/前序/程序员简历.md +++ b/problems/前序/程序员简历.md @@ -99,7 +99,7 @@ Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简 面试只有短短的30分钟或者一个小时,如何把自己掌握的技术更好的展现给面试官呢,博客、github都是很好的选择,如果把这些放在简历上,面试官一定会看的,这都是加分项。 -## 简历模板 +## 领取方式 最后福利,把我的简历模板贡献出来!如下图所示。 @@ -107,7 +107,11 @@ Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简 这里是简历模板中Markdown的代码:[https://github.com/youngyangyang04/Markdown-Resume-Template](https://github.com/youngyangyang04/Markdown-Resume-Template) ,可以fork到自己Github仓库上,按照这个模板来修改自己的简历。 -**Word版本的简历,大家可以在公众号「代码随想录」后台回复:简历模板,就可以获取!** +**Word版本的简历,添加如下企业微信,通过之后就会发你word版本**。 + +

+ +如果已经有我的企业微信,直接回复:简历模板,就可以了。 ## 总结 diff --git a/problems/前序/关于空间复杂度,可能有几个疑问?.md b/problems/前序/空间复杂度.md similarity index 97% rename from problems/前序/关于空间复杂度,可能有几个疑问?.md rename to problems/前序/空间复杂度.md index 19384fd9..b6690492 100644 --- a/problems/前序/关于空间复杂度,可能有几个疑问?.md +++ b/problems/前序/空间复杂度.md @@ -32,7 +32,7 @@ 同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。 -来看一下例子,什么时候的空间复杂度是$O(1)$呢,C++代码如下: +来看一下例子,什么时候的空间复杂度是 $O(1)$ 呢,C++代码如下: ```CPP int j = 0; diff --git a/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md b/problems/前序/算法超时.md similarity index 89% rename from problems/前序/On的算法居然超时了,此时的n究竟是多大?.md rename to problems/前序/算法超时.md index d5710d5a..34ebe6de 100644 --- a/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md +++ b/problems/前序/算法超时.md @@ -14,9 +14,9 @@ 也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。 -如果写出了一个$O(n)$的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。 +如果写出了一个 $O(n)$ 的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。 -如果n的规模已经足够让$O(n)$的算法运行时间超过了1s,就应该考虑log(n)的解法了。 +如果n的规模已经足够让 $O(n)$ 的算法运行时间超过了1s,就应该考虑log(n)的解法了。 ## 从硬件配置看计算机的性能 @@ -60,7 +60,7 @@ 测试硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5 -实现三个函数,时间复杂度分别是 $O(n)$ , $O(n^2)$, $O(n\log n)$,使用加法运算来统一测试。 +实现三个函数,时间复杂度分别是 $O(n)$ , $O(n^2)$ , $O(n\log n)$ ,使用加法运算来统一测试。 ```CPP // O(n) @@ -126,19 +126,19 @@ int main() { ![程序超时2](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729200018460-20230310124315093.png) -O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下$O(n^2)$ 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 +O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下 $O(n^2)$ 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 ![程序超时3](https://code-thinking-1253855093.file.myqcloud.com/pics/2020072919590970-20230310124318532.png) O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 -在推测一下$O(n\log n)$的话, 1s可以处理的数据规模是什么呢? +在推测一下 $O(n\log n)$ 的话, 1s可以处理的数据规模是什么呢? -理论上应该是比 $O(n)$少一个数量级,因为$\log n$的复杂度 其实是很快,看一下实验数据。 +理论上应该是比 $O(n)$ 少一个数量级,因为 $\log n$ 的复杂度 其实是很快,看一下实验数据。 ![程序超时4](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729195729407-20230310124322232.png) -$O(n\log n)$的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 +$O(n\log n)$ 的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 @@ -263,7 +263,7 @@ public class TimeComplexity { ## 总结 -本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看$O(n)$的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 +本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看 $O(n)$ 的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 diff --git a/problems/前序/递归算法的时间与空间复杂度分析.md b/problems/前序/递归算法的时间与空间复杂度分析.md index aacc4568..39513a91 100644 --- a/problems/前序/递归算法的时间与空间复杂度分析.md +++ b/problems/前序/递归算法的时间与空间复杂度分析.md @@ -191,7 +191,7 @@ int main() 因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。 -此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是$O(1)$。 +此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是 $O(1)$ 。 在看递归的深度是多少呢?如图所示: diff --git a/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md b/problems/前序/递归算法的时间复杂度.md similarity index 100% rename from problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md rename to problems/前序/递归算法的时间复杂度.md diff --git a/problems/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md index 040be299..f5803cb4 100644 --- a/problems/剑指Offer05.替换空格.md +++ b/problems/剑指Offer05.替换空格.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 替换数字 @@ -170,8 +170,8 @@ for (int i = 0; i < a.size(); i++) { +

- diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md index 138cf3a8..e32f4ce1 100644 --- a/problems/剑指Offer58-II.左旋转字符串.md +++ b/problems/剑指Offer58-II.左旋转字符串.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 右旋字符串 diff --git a/problems/动态规划-股票问题总结篇.md b/problems/动态规划-股票问题总结篇.md index 4df21fb7..7927deb7 100644 --- a/problems/动态规划-股票问题总结篇.md +++ b/problems/动态规划-股票问题总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# Leetcode股票问题总结篇! @@ -473,8 +473,8 @@ public: +

- diff --git a/problems/动态规划总结篇.md b/problems/动态规划总结篇.md index e28bfd04..3dda3768 100644 --- a/problems/动态规划总结篇.md +++ b/problems/动态规划总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划最强总结篇! diff --git a/problems/动态规划理论基础.md b/problems/动态规划理论基础.md index bff26d1d..c9420d24 100644 --- a/problems/动态规划理论基础.md +++ b/problems/动态规划理论基础.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划理论基础 @@ -131,8 +131,8 @@ 今天我们开始新的征程了,你准备好了么? +

- diff --git a/problems/双指针总结.md b/problems/双指针总结.md index 6621e039..02aec2e4 100644 --- a/problems/双指针总结.md +++ b/problems/双指针总结.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 又是一波总结 @@ -90,7 +90,7 @@ for (int i = 0; i < array.size(); i++) { ## 总结 -本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为$O(n)$。 +本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为 $O(n)$ 。 建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。 diff --git a/problems/周总结/20201003二叉树周末总结.md b/problems/周总结/20201003二叉树周末总结.md index 57416775..ea508224 100644 --- a/problems/周总结/20201003二叉树周末总结.md +++ b/problems/周总结/20201003二叉树周末总结.md @@ -13,9 +13,9 @@ 而本题的迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,认识到这一点之后就发现:用队列,用栈,甚至用数组,都是可以的。 -那么做完本题之后,在看如下两个题目。 -* 100.相同的树 -* 572.另一个树的子树 +那么做完本题之后,再看如下两个题目。 +* [100.相同的树](https://leetcode.cn/problems/same-tree/description/) +* [572.另一个树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) **[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)中的递归法和迭代法只需要稍作修改其中一个树的遍历顺序,便可刷了100.相同的树。** diff --git a/problems/周总结/20201210复杂度分析周末总结.md b/problems/周总结/20201210复杂度分析周末总结.md index cd3b2f13..461219f2 100644 --- a/problems/周总结/20201210复杂度分析周末总结.md +++ b/problems/周总结/20201210复杂度分析周末总结.md @@ -54,10 +54,10 @@ 文中涉及如下问题: * 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是$O(n^2)$的算法! -* $O(n^2)$的算法为什么有时候比$O(n)$的算法更优? +* $O(n^2)$ 的算法为什么有时候比 $O(n)$ 的算法更优? * 什么时间复杂度为什么可以忽略常数项? * 如何简化复杂的时间复杂度表达式,原理是什么? -* $O(\log n)$中的log究竟是以谁为底? +* $O(\log n)$ 中的log究竟是以谁为底? 这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。 @@ -96,7 +96,7 @@ 文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。 -此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了$O(\log n)$的代码。 +此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了 $O(\log n)$ 的代码。 其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题! diff --git a/problems/哈希表总结.md b/problems/哈希表总结.md index 465ef9d1..cde23ad1 100644 --- a/problems/哈希表总结.md +++ b/problems/哈希表总结.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 哈希表总结篇如约而至 @@ -125,8 +125,8 @@ std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层 +

- diff --git a/problems/哈希表理论基础.md b/problems/哈希表理论基础.md index e426c657..825c4657 100644 --- a/problems/哈希表理论基础.md +++ b/problems/哈希表理论基础.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ @@ -12,11 +12,11 @@ ## 哈希表 -首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 +首先什么是哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 > 哈希表是根据关键码的值而直接进行访问的数据结构。 -这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 +这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示: @@ -113,7 +113,7 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。 -虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。 +虽然std::set和std::multiset 的底层实现基于红黑树而非哈希表,它们通过红黑树来索引和存储数据。不过给我们的使用方式,还是哈希法的使用方式,即依靠键(key)来访问值(value)。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。std::map也是一样的道理。 这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢? diff --git a/problems/回溯总结.md b/problems/回溯总结.md index 5d4c9450..8d9b78c4 100644 --- a/problems/回溯总结.md +++ b/problems/回溯总结.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ > 20张树形结构图、14道精选回溯题目,21篇回溯法精讲文章,由浅入深,一气呵成,这是全网最强回溯算法总结! @@ -447,8 +447,8 @@ N皇后问题分析: **回溯算法系列正式结束,新的系列终将开始,录友们准备开启新的征程!** +

- diff --git a/problems/回溯算法去重问题的另一种写法.md b/problems/回溯算法去重问题的另一种写法.md index 2be79805..96dfeffe 100644 --- a/problems/回溯算法去重问题的另一种写法.md +++ b/problems/回溯算法去重问题的另一种写法.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 回溯算法去重问题的另一种写法 @@ -708,8 +708,8 @@ impl Solution { } ``` +

- diff --git a/problems/回溯算法理论基础.md b/problems/回溯算法理论基础.md index f22c67b1..862fb101 100644 --- a/problems/回溯算法理论基础.md +++ b/problems/回溯算法理论基础.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 回溯算法理论基础 @@ -175,8 +175,8 @@ void backtracking(参数) { +

- diff --git a/problems/字符串总结.md b/problems/字符串总结.md index df4db787..7da97914 100644 --- a/problems/字符串总结.md +++ b/problems/字符串总结.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 字符串:总结篇 diff --git a/problems/数组总结篇.md b/problems/数组总结篇.md index 7550ce02..f026e41b 100644 --- a/problems/数组总结篇.md +++ b/problems/数组总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 数组总结篇 @@ -16,7 +16,7 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: @@ -27,7 +27,7 @@ * **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: diff --git a/problems/数组理论基础.md b/problems/数组理论基础.md index d104c883..76e618c2 100644 --- a/problems/数组理论基础.md +++ b/problems/数组理论基础.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

@@ -16,7 +16,7 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: @@ -29,7 +29,7 @@ * **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: diff --git a/problems/栈与队列总结.md b/problems/栈与队列总结.md index 06a78270..113f4a06 100644 --- a/problems/栈与队列总结.md +++ b/problems/栈与队列总结.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 栈与队列总结篇 @@ -142,7 +142,7 @@ cd a/b/c/../../ 本题就要**使用优先级队列来对部分频率进行排序。** 注意这里是对部分数据进行排序而不需要对所有数据排序! -所以排序的过程的时间复杂度是$O(\log k)$,整个算法的时间复杂度是$O(n\log k)$。 +所以排序的过程的时间复杂度是 $O(\log k)$ ,整个算法的时间复杂度是 $O(n\log k)$ 。 ## 总结 @@ -159,8 +159,8 @@ cd a/b/c/../../ 好了,栈与队列我们就总结到这里了,接下来Carl就要带大家开启新的篇章了,大家加油! +

- diff --git a/problems/栈与队列理论基础.md b/problems/栈与队列理论基础.md index ad748e48..21c61a4c 100644 --- a/problems/栈与队列理论基础.md +++ b/problems/栈与队列理论基础.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

> 来看看栈和队列不为人知的一面 @@ -92,8 +92,8 @@ std::queue> third; // 定义以list为底层容器的队列 +

- diff --git a/problems/根据身高重建队列(vector原理讲解).md b/problems/根据身高重建队列(vector原理讲解).md index 5f9b46b3..70a9a97a 100644 --- a/problems/根据身高重建队列(vector原理讲解).md +++ b/problems/根据身高重建队列(vector原理讲解).md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ diff --git a/problems/算法模板.md b/problems/算法模板.md index 21b7f93b..0d32cebb 100644 --- a/problems/算法模板.md +++ b/problems/算法模板.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 算法模板 ## 算法模板 diff --git a/problems/背包总结篇.md b/problems/背包总结篇.md index 9be93096..651a92a8 100644 --- a/problems/背包总结篇.md +++ b/problems/背包总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 听说背包问题很难? 这篇总结篇来拯救你了 @@ -102,8 +102,8 @@ +

- diff --git a/problems/背包理论基础01背包-1.md b/problems/背包理论基础01背包-1.md index 413d984c..fa11fb94 100644 --- a/problems/背包理论基础01背包-1.md +++ b/problems/背包理论基础01背包-1.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ # 动态规划:01背包理论基础 diff --git a/problems/背包理论基础01背包-2.md b/problems/背包理论基础01背包-2.md index 5630bc99..b5838c5d 100644 --- a/problems/背包理论基础01背包-2.md +++ b/problems/背包理论基础01背包-2.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划:01背包理论基础(滚动数组) @@ -503,8 +503,8 @@ fn test_wei_bag_problem2() { } ``` +

- diff --git a/problems/背包问题理论基础多重背包.md b/problems/背包问题理论基础多重背包.md index 5d6440e3..878efc12 100644 --- a/problems/背包问题理论基础多重背包.md +++ b/problems/背包问题理论基础多重背包.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划:关于多重背包,你该了解这些! @@ -204,6 +204,29 @@ class multi_pack{ ``` ### Python: +```python + +C, N = input().split(" ") +C, N = int(C), int(N) + +# value数组需要判断一下非空不然过不了 +weights = [int(x) for x in input().split(" ")] +values = [int(x) for x in input().split(" ") if x] +nums = [int(x) for x in input().split(" ")] + +dp = [0] * (C + 1) +# 遍历背包容量 +for i in range(N): + for j in range(C, weights[i] - 1, -1): + for k in range(1, nums[i] + 1): + # 遍历 k,如果已经大于背包容量直接跳出循环 + if k * weights[i] > j: + break + dp[j] = max(dp[j], dp[j - weights[i] * k] + values[i] * k) +print(dp[-1]) + +``` + ### Go: @@ -213,8 +236,8 @@ class multi_pack{ +

- diff --git a/problems/背包问题理论基础完全背包.md b/problems/背包问题理论基础完全背包.md index efc56c50..3a50ee7b 100644 --- a/problems/背包问题理论基础完全背包.md +++ b/problems/背包问题理论基础完全背包.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 动态规划:完全背包理论基础 @@ -505,8 +505,8 @@ fn test_complete_pack() { } ``` +

- diff --git a/problems/贪心算法总结篇.md b/problems/贪心算法总结篇.md index d49cdc5f..14d82151 100644 --- a/problems/贪心算法总结篇.md +++ b/problems/贪心算法总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 贪心算法总结篇 @@ -150,8 +150,8 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 +

- diff --git a/problems/贪心算法理论基础.md b/problems/贪心算法理论基础.md index cac29292..f042c0ac 100644 --- a/problems/贪心算法理论基础.md +++ b/problems/贪心算法理论基础.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 关于贪心算法,你该了解这些! diff --git a/problems/链表总结篇.md b/problems/链表总结篇.md index b2b1b779..7da0d2de 100644 --- a/problems/链表总结篇.md +++ b/problems/链表总结篇.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 链表总结篇 @@ -97,8 +97,8 @@ +

- diff --git a/problems/链表理论基础.md b/problems/链表理论基础.md index 88e41d7d..d1313807 100644 --- a/problems/链表理论基础.md +++ b/problems/链表理论基础.md @@ -1,9 +1,9 @@ -

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ diff --git a/problems/面试题02.07.链表相交.md b/problems/面试题02.07.链表相交.md index e2905d49..b0f2e8ea 100644 --- a/problems/面试题02.07.链表相交.md +++ b/problems/面试题02.07.链表相交.md @@ -1,8 +1,8 @@

- + -

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

# 面试题 02.07. 链表相交