diff --git a/.gitignore b/.gitignore index 6a3e68da15..4bd29f751f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -**/.DS_Store \ No newline at end of file +.idea/ +.DS_Store +.vscode +.temp +.cache +*.iml +__pycache__ diff --git a/README.md b/README.md index b25b102dfe..92336a19fc 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 @@

- +

@@ -53,7 +53,7 @@ 按照先面的排列顺序,从数组开始刷起就可以了,顺序都安排好了,按顺序刷就好。 -在刷题攻略中,每个专题开始都有理论基础篇,并不像是教科书般的理论介绍,而是从实战中归纳需要的基础知识。每个专题结束都有总结篇,最这个专题的归纳总结。 +在刷题攻略中,每个专题开始都有理论基础篇,并不像是教科书般的理论介绍,而是从实战中归纳需要的基础知识。每个专题结束都有总结篇,是这个专题的归纳总结。 如果你是算法老手,这篇攻略也是复习的最佳资料,如果把每个系列对应的总结篇,快速过一遍,整个算法知识体系以及各种解法就重现脑海了。 @@ -69,26 +69,14 @@ ## 前序 -* [「代码随想录」学习社区](https://programmercarl.com/other/kstar.html) +* [做项目(多个C++、Java、Go、前端、测开项目)](https://programmercarl.com/other/kstar.html) * 编程语言 * [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) + * [大厂算法笔试题](https://kamacoder.com/company.php) * 工具 * [一站式vim配置](https://github.com/youngyangyang04/PowerVim) @@ -103,13 +91,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) ## 数组 @@ -119,8 +106,10 @@ 3. [数组:27.移除元素](./problems/0027.移除元素.md) 4. [数组:977.有序数组的平方](./problems/0977.有序数组的平方.md) 5. [数组:209.长度最小的子数组](./problems/0209.长度最小的子数组.md) -6. [数组:59.螺旋矩阵II](./problems/0059.螺旋矩阵II.md) -7. [数组:总结篇](./problems/数组总结篇.md) +6. [数组:区间和](./problems/kamacoder/0058.区间和.md) +7. [数组:开发商购买土地](./problems/kamacoder/0044.开发商购买土地.md) +8. [数组:59.螺旋矩阵II](./problems/0059.螺旋矩阵II.md) +9. [数组:总结篇](./problems/数组总结篇.md) ## 链表 @@ -153,9 +142,9 @@ 1. [字符串:344.反转字符串](./problems/0344.反转字符串.md) 2. [字符串:541.反转字符串II](./problems/0541.反转字符串II.md) -3. [字符串:替换数字](./problems/kama54.替换数字.md) +3. [字符串:替换数字](./problems/kamacoder/0054.替换数字.md) 4. [字符串:151.翻转字符串里的单词](./problems/0151.翻转字符串里的单词.md) -5. [字符串:右旋字符串](./problems/kama55.右旋字符串.md) +5. [字符串:右旋字符串](./problems/kamacoder/0055.右旋字符串.md) 6. [帮你把KMP算法学个通透](./problems/0028.实现strStr.md) 8. [字符串:459.重复的子字符串](./problems/0459.重复的子字符串.md) 9. [字符串:总结篇!](./problems/字符串总结.md) @@ -166,7 +155,7 @@ 1. [数组:27.移除元素](./problems/0027.移除元素.md) 2. [字符串:344.反转字符串](./problems/0344.反转字符串.md) -3. [字符串:替换数字](./problems/kama54.替换数字.md) +3. [字符串:替换数字](./problems/kamacoder/0054.替换数字.md) 4. [字符串:151.翻转字符串里的单词](./problems/0151.翻转字符串里的单词.md) 5. [链表:206.翻转链表](./problems/0206.翻转链表.md) 6. [链表:19.删除链表的倒数第 N 个结点](./problems/0019.删除链表的倒数第N个节点.md) @@ -190,8 +179,9 @@ ## 二叉树 + 题目分类大纲如下: -二叉树大纲 +二叉树大纲 1. [关于二叉树,你该了解这些!](./problems/二叉树理论基础.md) 2. [二叉树:二叉树的递归遍历](./problems/二叉树的递归遍历.md) @@ -207,7 +197,6 @@ 12. [二叉树:110.平衡二叉树](./problems/0110.平衡二叉树.md) 13. [二叉树:257.二叉树的所有路径](./problems/0257.二叉树的所有路径.md) 14. [本周总结!(二叉树)](./problems/周总结/20201003二叉树周末总结.md) -15. [二叉树:二叉树中递归带着回溯](./problems/二叉树中递归带着回溯.md) 16. [二叉树:404.左叶子之和](./problems/0404.左叶子之和.md) 17. [二叉树:513.找树左下角的值](./problems/0513.找树左下角的值.md) 18. [二叉树:112.路径总和](./problems/0112.路径总和.md) @@ -233,7 +222,7 @@ 题目分类大纲如下: -回溯算法大纲 +回溯算法大纲 1. [关于回溯算法,你该了解这些!](./problems/回溯算法理论基础.md) 2. [回溯算法:77.组合](./problems/0077.组合.md) @@ -263,7 +252,7 @@ 题目分类大纲如下: -贪心算法大纲 +贪心算法大纲 1. [关于贪心算法,你该了解这些!](./problems/贪心算法理论基础.md) 2. [贪心算法:455.分发饼干](./problems/0455.分发饼干.md) @@ -294,7 +283,7 @@ 动态规划专题已经开始啦,来不及解释了,小伙伴们上车别掉队! - + 1. [关于动态规划,你该了解这些!](./problems/动态规划理论基础.md) 2. [动态规划:509.斐波那契数](./problems/0509.斐波那契数.md) 3. [动态规划:70.爬楼梯](./problems/0070.爬楼梯.md) @@ -308,27 +297,28 @@ 背包问题系列: -背包问题大纲 +背包问题大纲 -11. [动态规划:01背包理论基础](./problems/背包理论基础01背包-1.md) -12. [动态规划:01背包理论基础(滚动数组)](./problems/背包理论基础01背包-2.md) +11. [动态规划:01背包理论基础(二维dp数组)](./problems/背包理论基础01背包-1.md) +12. [动态规划:01背包理论基础(一维dp数组)](./problems/背包理论基础01背包-2.md) 13. [动态规划:416.分割等和子集](./problems/0416.分割等和子集.md) 14. [动态规划:1049.最后一块石头的重量II](./problems/1049.最后一块石头的重量II.md) 15. [本周小结!(动态规划系列三)](./problems/周总结/20210121动规周末总结.md) 16. [动态规划:494.目标和](./problems/0494.目标和.md) 17. [动态规划:474.一和零](./problems/0474.一和零.md) -18. [动态规划:完全背包总结篇](./problems/背包问题理论基础完全背包.md) -19. [动态规划:518.零钱兑换II](./problems/0518.零钱兑换II.md) -20. [本周小结!(动态规划系列四)](./problems/周总结/20210128动规周末总结.md) -21. [动态规划:377.组合总和Ⅳ](./problems/0377.组合总和Ⅳ.md) -22. [动态规划:70.爬楼梯(完全背包版本)](./problems/0070.爬楼梯完全背包版本.md) -23. [动态规划:322.零钱兑换](./problems/0322.零钱兑换.md) -24. [动态规划:279.完全平方数](./problems/0279.完全平方数.md) -25. [本周小结!(动态规划系列五)](./problems/周总结/20210204动规周末总结.md) -26. [动态规划:139.单词拆分](./problems/0139.单词拆分.md) -27. [动态规划:多重背包理论基础](./problems/背包问题理论基础多重背包.md) -28. [背包问题总结篇](./problems/背包总结篇.md) +18. [动态规划:完全背包理论基础(二维dp数组)](./problems/背包问题理论基础完全背包.md) +19. [动态规划:完全背包理论基础(一维dp数组)](./problems/背包问题完全背包一维.md) +20. [动态规划:518.零钱兑换II](./problems/0518.零钱兑换II.md) +21. [本周小结!(动态规划系列四)](./problems/周总结/20210128动规周末总结.md) +22. [动态规划:377.组合总和Ⅳ](./problems/0377.组合总和Ⅳ.md) +23. [动态规划:70.爬楼梯(完全背包版本)](./problems/0070.爬楼梯完全背包版本.md) +24. [动态规划:322.零钱兑换](./problems/0322.零钱兑换.md) +25. [动态规划:279.完全平方数](./problems/0279.完全平方数.md) +26. [本周小结!(动态规划系列五)](./problems/周总结/20210204动规周末总结.md) +27. [动态规划:139.单词拆分](./problems/0139.单词拆分.md) +28. [动态规划:多重背包理论基础](./problems/背包问题理论基础多重背包.md) +29. [背包问题总结篇](./problems/背包总结篇.md) 打家劫舍系列: @@ -338,7 +328,7 @@ 股票系列: -股票问题总结 +股票问题总结 32. [动态规划:121.买卖股票的最佳时机](./problems/0121.买卖股票的最佳时机.md) @@ -353,7 +343,7 @@ 子序列系列: - + 41. [动态规划:300.最长递增子序列](./problems/0300.最长上升子序列.md) @@ -383,44 +373,42 @@ ## 图论 -通知:开始更新图论内容,图论部分还没有其他语言版本,欢迎录友们提交PR,成为contributor - -### 深搜广搜 - -* [图论:深度优先搜索理论基础](./problems/图论深搜理论基础.md) -* [图论:797.所有可能的路径](./problems/0797.所有可能的路径.md) -* [图论:广度优先搜索理论基础](./problems/图论广搜理论基础.md) -* [图论:200.岛屿数量.深搜版](./problems/0200.岛屿数量.深搜版.md) -* [图论:200.岛屿数量.广搜版](./problems/0200.岛屿数量.广搜版.md) -* [图论:695.岛屿的最大面积](./problems/0695.岛屿的最大面积.md) -* [图论:1020.飞地的数量](./problems/1020.飞地的数量.md) -* [图论:130.被围绕的区域](./problems/0130.被围绕的区域.md) -* [图论:417.太平洋大西洋水流问题](./problems/0417.太平洋大西洋水流问题.md) -* [图论:827.最大人工岛](./problems/0827.最大人工岛.md) -* [图论:127. 单词接龙](./problems/0127.单词接龙.md) -* [图论:841.钥匙和房间](./problems/0841.钥匙和房间.md) -* [图论:463. 岛屿的周长](./problems/0463.岛屿的周长.md) -* [图论:并查集理论基础](./problems/图论并查集理论基础.md) -* [图论:1971. 寻找图中是否存在路径](./problems/1971.寻找图中是否存在路径.md) -* [图论:684.冗余连接](./problems/0684.冗余连接.md) -* [图论:685.冗余连接II](./problems/0685.冗余连接II.md) - -(持续更新中....) - - -## 十大排序 - -## 数论 - -## 高级数据结构经典题目 +**[图论正式发布](./problems/qita/tulunfabu.md)** + +1. [图论:理论基础](./problems/kamacoder/图论理论基础.md) +2. [图论:深度优先搜索理论基础](./problems/kamacoder/图论深搜理论基础.md) +3. [图论:所有可达路径](./problems/kamacoder/0098.所有可达路径.md) +4. [图论:广度优先搜索理论基础](./problems/kamacoder/图论广搜理论基础.md) +5. [图论:岛屿数量.深搜版](./problems/kamacoder/0099.岛屿的数量深搜.md) +6. [图论:岛屿数量.广搜版](./problems/kamacoder/0099.岛屿的数量广搜.md) +7. [图论:岛屿的最大面积](./problems/kamacoder/0100.岛屿的最大面积.md) +8. [图论:孤岛的总面积](./problems/kamacoder/0101.孤岛的总面积.md) +9. [图论:沉没孤岛](./problems/kamacoder/0102.沉没孤岛.md) +10. [图论:水流问题](./problems/kamacoder/0103.水流问题.md) +11. [图论:建造最大岛屿](./problems/kamacoder/0104.建造最大岛屿.md) +12. [图论:岛屿的周长](./problems/kamacoder/0106.岛屿的周长.md) +13. [图论:字符串接龙](./problems/kamacoder/0110.字符串接龙.md) +14. [图论:有向图的完全可达性](./problems/kamacoder/0105.有向图的完全可达性.md) +15. [图论:并查集理论基础](./problems/kamacoder/图论并查集理论基础.md) +16. [图论:寻找存在的路径](./problems/kamacoder/0107.寻找存在的路径.md) +17. [图论:冗余连接](./problems/kamacoder/0108.冗余连接.md) +18. [图论:冗余连接II](./problems/kamacoder/0109.冗余连接II.md) +19. [图论:最小生成树之prim](./problems/kamacoder/0053.寻宝-prim.md) +20. [图论:最小生成树之kruskal](./problems/kamacoder/0053.寻宝-Kruskal.md) +21. [图论:拓扑排序](./problems/kamacoder/0117.软件构建.md) +22. [图论:dijkstra(朴素版)](./problems/kamacoder/0047.参会dijkstra朴素.md) +23. [图论:dijkstra(堆优化版)](./problems/kamacoder/0047.参会dijkstra堆.md) +24. [图论:Bellman_ford 算法](./problems/kamacoder/0094.城市间货物运输I.md) +25. [图论:Bellman_ford 队列优化算法(又名SPFA)](./problems/kamacoder/0094.城市间货物运输I-SPFA.md) +26. [图论:Bellman_ford之判断负权回路](./problems/kamacoder/0095.城市间货物运输II.md) +27. [图论:Bellman_ford之单源有限最短路](./problems/kamacoder/0096.城市间货物运输III.md) +28. [图论:Floyd 算法](./problems/kamacoder/0097.小明逛公园.md) +29. [图论:A * 算法](./problems/kamacoder/0126.骑士的攻击astar.md) +30. [图论:最短路算法总结篇](./problems/kamacoder/最短路问题总结篇.md) +31. [图论:图论总结篇](./problems/kamacoder/图论总结篇.md) -* 并查集 -* 最小生成树 -* 线段树 -* 树状数组 -* 字典树 -## 海量数据处理 +(持续更新中....) # 补充题目 @@ -499,7 +487,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 +495,13 @@ # 关于作者 -大家好,我是程序员Carl,哈工大师兄,《代码随想录》作者,先后在腾讯和百度从事后端技术研发。对算法和C++后端技术有一定的见解,利用工作之余重新刷leetcode。 - -加入「代码随想录」刷题小分队(微信群),可以扫下方二维码,加代码随想录客服微信。 - -如果是已工作,备注:姓名-城市-岗位-组队刷题。如果学生,备注:姓名-学校-年级-组队刷题。**备注没有自我介绍不通过哦** - - -
- - -# 公众号 - -更多精彩文章持续更新,微信搜索:「代码随想录」第一时间围观,关注后回复:666,可以获得我的所有算法专题原创PDF。 +大家好,我是程序员Carl,哈工大师兄,《代码随想录》作者,先后在腾讯和百度从事后端技术底层技术研发。 -**来看看就知道了,你会发现相见恨晚!** +# PDF下载 +添加如下企业微信,会自动发送给大家PDF版本,顺便可以选择是否加入刷题群。 - -
+添加微信记得备注,如果是已工作,备注:姓名-城市-岗位。如果学生,备注:姓名-学校-年级。**备注没有自我介绍不通过哦** +
diff --git "a/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" "b/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" index b0325d8733..547d570418 100644 Binary files "a/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" and "b/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" differ diff --git "a/pics/\351\230\277\351\207\214\344\272\221.png" "b/pics/\351\230\277\351\207\214\344\272\221.png" deleted file mode 100644 index 79f41d6beb..0000000000 Binary files "a/pics/\351\230\277\351\207\214\344\272\221.png" and /dev/null differ diff --git "a/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" old mode 100644 new mode 100755 index 80218cb535..be4455bda2 --- "a/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" +++ "b/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1. 两数之和 @@ -85,10 +83,10 @@ map目的用来存放我们访问过的元素,因为遍历数组的时候, 过程如下: -![过程一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220711202638.png) +![过程一](https://file1.kamacoder.com/i/algo/20220711202638.png) -![过程二](https://code-thinking-1253855093.file.myqcloud.com/pics/20230220223536.png) +![过程二](https://file1.kamacoder.com/i/algo/20230220223536.png) C++代码: @@ -152,6 +150,24 @@ public int[] twoSum(int[] nums, int target) { return res; } ``` + +```java +//使用哈希表方法2 +public int[] twoSum(int[] nums, int target) { + Map indexMap = new HashMap<>(); + + for(int i = 0; i < nums.length; i++){ + int balance = target - nums[i]; // 记录当前的目标值的余数 + if(indexMap.containsKey(balance)){ // 查找当前的map中是否有满足要求的值 + return new int []{i, indexMap.get(balance)}; // 如果有,返回目标值 + } else{ + indexMap.put(nums[i], i); // 如果没有,把访问过的元素和下标加入map中 + } + } + return null; +} +``` + ```java //使用双指针 public int[] twoSum(int[] nums, int target) { @@ -269,17 +285,16 @@ func twoSum(nums []int, target int) []int { ``` ```go -// 使用map方式解题,降低时间复杂度 func twoSum(nums []int, target int) []int { m := make(map[int]int) - for index, val := range nums { - if preIndex, ok := m[target-val]; ok { - return []int{preIndex, index} - } else { - m[val] = index + for i, num := range nums { + complement := target - num + if index, found := m[complement]; found { + return []int{index, i} } + m[num] = i } - return []int{} + return nil // 返回空数组 nil 代替空切片 } ``` @@ -323,7 +338,7 @@ impl Solution { } ``` -### Javascript: +### JavaScript: ```javascript var twoSum = function (nums, target) { @@ -349,6 +364,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 +553,4 @@ int* twoSum(int* nums, int numsSize, int target, int* returnSize){ return NULL; } ``` -

- - - diff --git "a/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" old mode 100644 new mode 100755 index b13f9ac356..05dd610a72 --- "a/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" +++ "b/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -108,7 +106,7 @@ dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹 dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图: -![647.回文子串](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171032473.jpg) +![647.回文子串](https://file1.kamacoder.com/i/algo/20210121171032473.jpg) 如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。 @@ -142,7 +140,7 @@ for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 举例,输入:"aaa",dp[i][j]状态如下: -![647.回文子串1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171059951.jpg) +![647.回文子串1](https://file1.kamacoder.com/i/algo/20210121171059951.jpg) **注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分**。 @@ -256,7 +254,60 @@ public: * 时间复杂度:O(n^2) * 空间复杂度:O(1) +### Manacher 算法 +Manacher 算法的关键在于高效利用回文的对称性,通过插入分隔符和维护中心、边界等信息,在线性时间内找到最长回文子串。这种方法避免了重复计算,是处理回文问题的最优解。 + +```c++ +//Manacher 算法 +class Solution { +public: + string longestPalindrome(string s) { + // 预处理字符串,在每个字符之间插入 '#' + string t = "#"; + for (char c : s) { + t += c; // 添加字符 + t += '#';// 添加分隔符 + } + int n = t.size();// 新字符串的长度 + vector p(n, 0);// p[i] 表示以 t[i] 为中心的回文半径 + int center = 0, right = 0;// 当前回文的中心和右边界 + + + // 遍历预处理后的字符串 + for (int i = 0; i < n; i++) { + // 如果当前索引在右边界内,利用对称性初始化 p[i] + if (i < right) { + p[i] = min(right - i, p[2 * center - i]); + } + // 尝试扩展回文 + while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n && t[i - p[i] - 1] == t[i + p[i] + 1]) { + p[i]++;// 增加回文半径 + } + // 如果当前回文扩展超出右边界,更新中心和右边界 + if (i + p[i] > right) { + center = i;// 更新中心 + right = i + p[i];// 更新右边界 + } + } + // 找到最大回文半径和对应的中心 + int maxLen = 0, centerIndex = 0; + for (int i = 0; i < n; i++) { + if (p[i] > maxLen) { + maxLen = p[i];// 更新最大回文长度 + centerIndex = i;// 更新中心索引 + } + } + // 计算原字符串中回文子串的起始位置并返回 + return s.substr((centerIndex - maxLen) / 2, maxLen); + } +}; +``` + + + +* 时间复杂度:O(n) +* 空间复杂度:O(n) ## 其他语言版本 @@ -677,8 +728,5 @@ public class Solution { } ``` -

- - - + diff --git "a/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" old mode 100644 new mode 100755 index bf165788f8..e2cb3f4612 --- "a/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" +++ "b/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -1,13 +1,9 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -> 用哈希表解决了[两数之和](https://programmercarl.com/0001.两数之和.html),那么三数之和呢? - # 第15题. 三数之和 [力扣题目链接](https://leetcode.cn/problems/3sum/) @@ -36,7 +32,7 @@ ### 哈希解法 -两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。 +两层for循环就可以确定 两个数值,可以使用哈希法来确定 第三个数 0-(a+b) 或者 0 - (a + c) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。 把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。 @@ -50,35 +46,41 @@ ```CPP class Solution { public: + // 在一个数组中找到3个数形成的三元组,它们的和为0,不能重复使用(三数下标互不相同),且三元组不能重复。 + // b(存储)== 0-(a+c)(检索) vector> threeSum(vector& nums) { vector> result; sort(nums.begin(), nums.end()); - // 找出a + b + c = 0 - // a = nums[i], b = nums[j], c = -(a + b) + for (int i = 0; i < nums.size(); i++) { - // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 - if (nums[i] > 0) { + // 如果a是正数,a 0) break; - } - if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重 + + // [a, a, ...] 如果本轮a和上轮a相同,那么找到的b,c也是相同的,所以去重a + if (i > 0 && nums[i] == nums[i - 1]) continue; - } + + // 这个set的作用是存储b unordered_set set; - for (int j = i + 1; j < nums.size(); j++) { - if (j > i + 2 - && nums[j] == nums[j-1] - && nums[j-1] == nums[j-2]) { // 三元组元素b去重 + + for (int k = i + 1; k < nums.size(); k++) { + // 去重b=c时的b和c + if (k > i + 2 && nums[k] == nums[k - 1] && nums[k - 1] == nums[k - 2]) continue; + + // a+b+c=0 <=> b=0-(a+c) + int target = 0 - (nums[i] + nums[k]); + if (set.find(target) != set.end()) { + result.push_back({nums[i], target, nums[k]}); // nums[k]成为c + set.erase(target); } - int c = 0 - (nums[i] + nums[j]); - if (set.find(c) != set.end()) { - result.push_back({nums[i], nums[j], c}); - set.erase(c);// 三元组元素c去重 - } else { - set.insert(nums[j]); + else { + set.insert(nums[k]); // nums[k]成为b } } } + return result; } }; @@ -98,7 +100,7 @@ public: 动画效果如下: -![15.三数之和](https://code-thinking.cdn.bcebos.com/gifs/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.gif) +![15.三数之和](https://file1.kamacoder.com/i/algo/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.gif) 拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。 @@ -405,6 +407,7 @@ class Solution: ``` ### Go: +(版本一) 双指针 ```Go func threeSum(nums []int) [][]int { @@ -444,6 +447,42 @@ func threeSum(nums []int) [][]int { return res } ``` +(版本二) 哈希解法 + +```Go +func threeSum(nums []int) [][]int { + res := make([][]int, 0) + sort.Ints(nums) + // 找出a + b + c = 0 + // a = nums[i], b = nums[j], c = -(a + b) + for i := 0; i < len(nums); i++ { + // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 + if nums[i] > 0 { + break + } + // 三元组元素a去重 + if i > 0 && nums[i] == nums[i-1] { + continue + } + set := make(map[int]struct{}) + for j := i + 1; j < len(nums); j++ { + // 三元组元素b去重 + if j > i + 2 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2] { + continue + } + c := -nums[i] - nums[j] + if _, ok := set[c]; ok { + res = append(res, []int{nums[i], nums[j], c}) + // 三元组元素c去重 + delete(set, c) + } else { + set[nums[j]] = struct{}{} + } + } + } + return res +} +``` ### JavaScript: @@ -938,8 +977,4 @@ object Solution { } } ``` -

- - - diff --git "a/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" "b/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" old mode 100644 new mode 100755 index cbd99f8d93..6dcf9ee690 --- "a/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" +++ "b/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 17.电话号码的字母组合 @@ -13,7 +11,7 @@ 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 -![17.电话号码的字母组合](https://code-thinking-1253855093.file.myqcloud.com/pics/2020102916424043.png) +![17.电话号码的字母组合](https://file1.kamacoder.com/i/algo/2020102916424043.png) 示例: * 输入:"23" @@ -66,7 +64,7 @@ const string letterMap[10] = { 例如:输入:"23",抽象为树形结构,如图所示: -![17. 电话号码的字母组合](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123200304469.png) +![17. 电话号码的字母组合](https://file1.kamacoder.com/i/algo/20201123200304469.png) 图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。 @@ -180,7 +178,7 @@ public: } }; ``` -* 时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数 +* 时间复杂度: O(3^m * 4^n),其中 m 是对应三个字母的数字个数,n 是对应四个字母的数字个数 * 空间复杂度: O(3^m * 4^n) 一些写法,是把回溯的过程放在递归函数里了,例如如下代码,我可以写成这样:(注意注释中不一样的地方) @@ -260,7 +258,7 @@ class Solution { } - //每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuilder + //每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder StringBuilder temp = new StringBuilder(); //比如digits如果为"23",num 为0,则str表示2对应的 abc @@ -274,7 +272,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 +763,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" "b/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" old mode 100644 new mode 100755 index 17715b2e6f..bf7d3bd4ee --- "a/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" +++ "b/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 一样的道理,能解决四数之和 @@ -35,7 +33,7 @@ 四数之和,和[15.三数之和](https://programmercarl.com/0015.三数之和.html)是一个思路,都是使用双指针法, 基本解法就是在[15.三数之和](https://programmercarl.com/0015.三数之和.html) 的基础上再套一层for循环。 -但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是`[-4, -3, -2, -1]`,`target`是`-10`,不能因为`-4 > -10`而跳过。但是我们依旧可以去做剪枝,逻辑变成`nums[i] > target && (nums[i] >=0 || target >= 0)`就可以了。 +但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是`[-4, -3, -2, -1]`,`target`是`-10`,不能因为`-4 > -10`而跳过。但是我们依旧可以去做剪枝,逻辑变成`nums[k] > target && (nums[k] >=0 || target >= 0)`就可以了。 [15.三数之和](https://programmercarl.com/0015.三数之和.html)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。 @@ -151,54 +149,154 @@ if (nums[k] + nums[i] > target && nums[i] >= 0) { ## 其他语言版本 +### C: + +```C +/* qsort */ +static int cmp(const void* arg1, const void* arg2) { + int a = *(int *)arg1; + int b = *(int *)arg2; + return (a > b); +} + +int** fourSum(int* nums, int numsSize, int target, int* returnSize, int** returnColumnSizes) { + + /* 对nums数组进行排序 */ + qsort(nums, numsSize, sizeof(int), cmp); + + int **res = (int **)malloc(sizeof(int *) * 40000); + int index = 0; + + /* k */ + for (int k = 0; k < numsSize - 3; k++) { /* 第一级 */ + + /* k剪枝 */ + if ((nums[k] > target) && (nums[k] >= 0)) { + break; + } + /* k去重 */ + if ((k > 0) && (nums[k] == nums[k - 1])) { + continue; + } + + /* i */ + for (int i = k + 1; i < numsSize - 2; i++) { /* 第二级 */ + + /* i剪枝 */ + if ((nums[k] + nums[i] > target) && (nums[i] >= 0)) { + break; + } + /* i去重 */ + if ((i > (k + 1)) && (nums[i] == nums[i - 1])) { + continue; + } + + /* left and right */ + int left = i + 1; + int right = numsSize - 1; + + while (left < right) { + + /* 防止大数溢出 */ + long long val = (long long)nums[k] + nums[i] + nums[left] + nums[right]; + if (val > target) { + right--; + } else if (val < target) { + left++; + } else { + int *res_tmp = (int *)malloc(sizeof(int) * 4); + res_tmp[0] = nums[k]; + res_tmp[1] = nums[i]; + res_tmp[2] = nums[left]; + res_tmp[3] = nums[right]; + res[index++] = res_tmp; + + /* right去重 */ + while ((right > left) && (nums[right] == nums[right - 1])) { + right--; + } + /* left去重 */ + while ((left < right) && (nums[left] == nums[left + 1])) { + left++; + } + + /* 更新right与left */ + left++, right--; + } + } + } + } + + /* 返回值处理 */ + *returnSize = index; + + int *column = (int *)malloc(sizeof(int) * index); + for (int i = 0; i < index; i++) { + column[i] = 4; + } + *returnColumnSizes = column; + return res; +} +``` + ### Java: ```Java -class Solution { +import java.util.*; + +public class Solution { public List> fourSum(int[] nums, int target) { - List> result = new ArrayList<>(); - Arrays.sort(nums); - - for (int i = 0; i < nums.length; i++) { - - // nums[i] > target 直接返回, 剪枝操作 - if (nums[i] > 0 && nums[i] > target) { - return result; + Arrays.sort(nums); // 排序数组 + List> result = new ArrayList<>(); // 结果集 + for (int k = 0; k < nums.length; k++) { + // 剪枝处理 + if (nums[k] > target && nums[k] >= 0) { + break; // 此处的break可以等价于return result; } - - if (i > 0 && nums[i - 1] == nums[i]) { // 对nums[i]去重 + // 对nums[k]去重 + if (k > 0 && nums[k] == nums[k - 1]) { continue; } - - for (int j = i + 1; j < nums.length; j++) { - - if (j > i + 1 && nums[j - 1] == nums[j]) { // 对nums[j]去重 + for (int i = k + 1; i < nums.length; i++) { + // 第二级剪枝 + if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) { + break; // 注意是break到上一级for循环,如果直接return result;会有遗漏 + } + // 对nums[i]去重 + if (i > k + 1 && nums[i] == nums[i - 1]) { continue; } - - int left = j + 1; + int left = i + 1; int right = nums.length - 1; while (right > left) { - // nums[k] + nums[i] + nums[left] + nums[right] > target int会溢出 - long sum = (long) nums[i] + nums[j] + nums[left] + nums[right]; + long sum = (long) nums[k] + nums[i] + nums[left] + nums[right]; if (sum > target) { right--; } else if (sum < target) { left++; } else { - result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); + result.add(Arrays.asList(nums[k], nums[i], nums[left], nums[right])); // 对nums[left]和nums[right]去重 while (right > left && nums[right] == nums[right - 1]) right--; while (right > left && nums[left] == nums[left + 1]) left++; - - left++; right--; + left++; } } } } return result; } + + public static void main(String[] args) { + Solution solution = new Solution(); + int[] nums = {1, 0, -1, 0, -2, 2}; + int target = 0; + List> results = solution.fourSum(nums, target); + for (List result : results) { + System.out.println(result); + } + } } ``` @@ -697,8 +795,5 @@ def four_sum(nums, target) return result end ``` -

- - - + diff --git "a/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" old mode 100644 new mode 100755 index f508b523e3..08f602c1c1 --- "a/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" +++ "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -18,14 +16,16 @@ 示例 1: -![19.删除链表的倒数第N个节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20210510085957392.png) +![19.删除链表的倒数第N个节点](https://file1.kamacoder.com/i/algo/20210510085957392.png) 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] + 示例 2: 输入:head = [1], n = 1 输出:[] + 示例 3: 输入:head = [1,2], n = 1 @@ -49,16 +49,16 @@ * 定义fast指针和slow指针,初始值为虚拟头结点,如图: - + * fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: - + * fast和slow同时移动,直到fast指向末尾,如题: - - + +//图片中有错别词:应该将“只到”改为“直到” * 删除slow指向的下一个节点,如图: - + 此时不难写出如下C++代码: @@ -82,7 +82,7 @@ public: // ListNode *tmp = slow->next; C++释放内存的逻辑 // slow->next = tmp->next; - // delete nth; + // delete tmp; return dummyHead->next; } @@ -98,30 +98,65 @@ public: ### Java: ```java -public ListNode removeNthFromEnd(ListNode head, int n){ - ListNode dummyNode = new ListNode(0); - dummyNode.next = head; - - ListNode fastIndex = dummyNode; - ListNode slowIndex = dummyNode; +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + //新建一个虚拟头节点指向head + ListNode dummyNode = new ListNode(0); + dummyNode.next = head; + //快慢指针指向虚拟头节点 + 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; +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + // 创建一个新的哑节点,指向原链表头 + ListNode s = new ListNode(-1, head); + // 递归调用remove方法,从哑节点开始进行删除操作 + remove(s, n); + // 返回新链表的头(去掉可能的哑节点) + return s.next; + } + + public int remove(ListNode p, int n) { + // 递归结束条件:如果当前节点为空,返回0 + if (p == null) { + return 0; + } + // 递归深入到下一个节点 + int net = remove(p.next, n); + // 如果当前节点是倒数第n个节点,进行删除操作 + if (net == n) { + p.next = p.next.next; + } + // 返回当前节点的总深度 + return net + 1; + } } ``` + ### Python: ```python @@ -165,20 +200,17 @@ class Solution: * } */ func removeNthFromEnd(head *ListNode, n int) *ListNode { - dummyHead := &ListNode{} - dummyHead.Next = head - cur := head - prev := dummyHead - i := 1 - for cur != nil { - cur = cur.Next - if i > n { - prev = prev.Next - } - i++ - } - prev.Next = prev.Next.Next - return dummyHead.Next + dummyNode := &ListNode{0, head} + fast, slow := dummyNode, dummyNode + for i := 0; i <= n; i++ { // 注意<=,否则快指针为空时,慢指针正好在倒数第n个上面 + fast = fast.Next + } + for fast != nil { + fast = fast.Next + slow = slow.Next + } + slow.Next = slow.Next.Next + return dummyNode.Next } ``` @@ -190,16 +222,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: @@ -443,7 +477,3 @@ public class Solution { } } ``` -

- - - diff --git "a/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" "b/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" old mode 100644 new mode 100755 index 17fbe2be2d..09cf997839 --- "a/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" +++ "b/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -83,13 +81,13 @@ cd a/b/c/../../ 1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。 -![括号匹配1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020080915505387.png) +![括号匹配1](https://file1.kamacoder.com/i/algo/2020080915505387.png) 2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 -![括号匹配2](https://code-thinking-1253855093.file.myqcloud.com/pics/20200809155107397.png) +![括号匹配2](https://file1.kamacoder.com/i/algo/20200809155107397.png) 3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。 -![括号匹配3](https://code-thinking-1253855093.file.myqcloud.com/pics/20200809155115779.png) +![括号匹配3](https://file1.kamacoder.com/i/algo/20200809155115779.png) @@ -97,7 +95,7 @@ cd a/b/c/../../ 动画如下: -![20.有效括号](https://code-thinking.cdn.bcebos.com/gifs/20.有效括号.gif) +![20.有效括号](https://file1.kamacoder.com/i/algo/20.有效括号.gif) 第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false @@ -166,12 +164,35 @@ class Solution { deque.pop(); } } - //最后判断栈中元素是否匹配 + //遍历结束,如果栈为空,则括号全部匹配 return deque.isEmpty(); } } ``` +```java +// 解法二 +// 对应的另一半一定在栈顶 +class Solution { + public boolean isValid(String s) { + Stack stack = new Stack<>(); + for(char c : s.toCharArray()){ + // 有对应的另一半就直接消消乐 + if(c == ')' && !stack.isEmpty() && stack.peek() == '(') + stack.pop(); + else if(c == '}' && !stack.isEmpty() && stack.peek() == '{') + stack.pop(); + else if(c == ']' && !stack.isEmpty() && stack.peek() == '[') + stack.pop(); + else + stack.push(c);// 没有匹配的就放进去 + } + + return stack.isEmpty(); + } +} +``` + ### Python: ```python @@ -275,7 +296,7 @@ def is_valid(strs) end ``` -### Javascript: +### JavaScript: ```javascript var isValid = function (s) { @@ -551,7 +572,4 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" old mode 100644 new mode 100755 index b2a830a746..14d2538f45 --- "a/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" +++ "b/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 24. 两两交换链表中的节点 @@ -14,7 +12,7 @@ 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 -24.两两交换链表中的节点-题意 +24.两两交换链表中的节点-题意 ## 算法公开课 @@ -33,16 +31,16 @@ 初始时,cur指向虚拟头结点,然后进行如下三步: -![24.两两交换链表中的节点1](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%B91.png) +![24.两两交换链表中的节点1](https://file1.kamacoder.com/i/algo/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%B91.png) 操作之后,链表如下: -![24.两两交换链表中的节点2](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%B92.png) +![24.两两交换链表中的节点2](https://file1.kamacoder.com/i/algo/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%B92.png) 看这个可能就更直观一些了: -![24.两两交换链表中的节点3](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%B93.png) +![24.两两交换链表中的节点3](https://file1.kamacoder.com/i/algo/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%B93.png) 对应的C++代码实现如下: (注释中详细和如上图中的三步做对应) @@ -81,9 +79,9 @@ 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) +![24.两两交换链表中的节点](https://file1.kamacoder.com/i/algo/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) 力扣上的统计如果两份代码是 100ms 和 300ms的耗时,其实是需要注意的。 @@ -181,6 +179,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 @@ -269,7 +284,7 @@ func swapPairs(head *ListNode) *ListNode { } ``` -### Javascript: +### JavaScript: ```javascript var swapPairs = function (head) { @@ -285,6 +300,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 @@ -495,7 +525,3 @@ public ListNode SwapPairs(ListNode head) } ``` -

- - - diff --git "a/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" "b/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" old mode 100644 new mode 100755 index dbde3d198c..47e05eec6a --- "a/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" +++ "b/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 27. 移除元素 @@ -45,7 +43,7 @@ 删除过程如下: -![27.移除元素-暴力解法](https://code-thinking.cdn.bcebos.com/gifs/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E6%9A%B4%E5%8A%9B%E8%A7%A3%E6%B3%95.gif) +![27.移除元素-暴力解法](https://file1.kamacoder.com/i/algo/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E6%9A%B4%E5%8A%9B%E8%A7%A3%E6%B3%95.gif) 很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。 @@ -89,7 +87,7 @@ public: 删除过程如下: -![27.移除元素-双指针法](https://code-thinking.cdn.bcebos.com/gifs/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95.gif) +![27.移除元素-双指针法](https://file1.kamacoder.com/i/algo/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95.gif) 很多同学不了解 @@ -119,35 +117,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一定指向了最终数组末尾的下一个元素 - } -}; -``` ## 相关题目推荐 @@ -160,7 +129,24 @@ public: ## 其他语言版本 ### Java: - +```java +class Solution { + public int removeElement(int[] nums, int val) { + // 暴力法 + int n = nums.length; + for (int i = 0; i < n; i++) { + if (nums[i] == val) { + for (int j = i + 1; j < n; j++) { + nums[j - 1] = nums[j]; + } + i--; + n--; + } + } + return n; + } +} +``` ```java class Solution { public int removeElement(int[] nums, int val) { @@ -253,8 +239,47 @@ class Solution: ``` +``` python 3 +# 相向双指针法 +# 时间复杂度 O(n) +# 空间复杂度 O(1) +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + n = len(nums) + left, right = 0, n - 1 + while left <= right: + while left <= right and nums[left] != val: + left += 1 + while left <= right and nums[right] == val: + right -= 1 + if left < right: + nums[left] = nums[right] + left += 1 + right -= 1 + return left + +``` + ### 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) @@ -297,7 +322,6 @@ func removeElement(nums []int, val int) int { right-- } } - fmt.Println(nums) return left } ``` @@ -467,7 +491,29 @@ 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.\345\256\236\347\216\260strStr.md" "b/problems/0028.\345\256\236\347\216\260strStr.md" old mode 100644 new mode 100755 index 8d0cc52559..ef8a6c58e6 --- "a/problems/0028.\345\256\236\347\216\260strStr.md" +++ "b/problems/0028.\345\256\236\347\216\260strStr.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 @@ -108,7 +106,7 @@ next数组就是一个前缀表(prefix table)。 如动画所示: -![KMP详解1](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B21.gif) +![KMP详解1](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B21.gif) 动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说到。 @@ -149,11 +147,11 @@ next数组就是一个前缀表(prefix table)。 这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢? 回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: -KMP精讲1 +KMP精讲1 然后就找到了下标2,指向b,继续匹配:如图: -KMP精讲2 +KMP精讲2 以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! @@ -169,15 +167,15 @@ next数组就是一个前缀表(prefix table)。 如图: -KMP精讲5 +KMP精讲5 长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) -KMP精讲6 +KMP精讲6 长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 -KMP精讲7 +KMP精讲7 长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 @@ -187,13 +185,13 @@ next数组就是一个前缀表(prefix table)。 长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: -KMP精讲8 +KMP精讲8 可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** 再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: -![KMP精讲2](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B22.gif) +![KMP精讲2](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B22.gif) 找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 @@ -227,7 +225,7 @@ next数组就可以是前缀表,但是很多实现都是把前缀表统一减 匹配过程动画如下: -![KMP精讲4](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B24.gif) +![KMP精讲4](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B24.gif) ### 时间复杂度分析 @@ -334,7 +332,7 @@ void getNext(int* next, const string& s){ 代码构造next数组的逻辑流程动画如下: -![KMP精讲3](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B23.gif) +![KMP精讲3](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B23.gif) 得到了next数组之后,就要用这个来做匹配了。 @@ -564,6 +562,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 { @@ -1424,7 +1454,67 @@ public int[] GetNext(string needle) } ``` -

- - - +### C: + +> 前缀表统一右移和减一 + +```c + +int *build_next(char* needle, int len) { + + int *next = (int *)malloc(len * sizeof(int)); + assert(next); // 确保分配成功 + + // 初始化next数组 + next[0] = -1; // next[0] 设置为 -1,表示没有有效前缀匹配 + if (len <= 1) { // 如果模式串长度小于等于 1,直接返回 + return next; + } + next[1] = 0; // next[1] 设置为 0,表示第一个字符没有公共前后缀 + + // 构建next数组, i 从模式串的第三个字符开始, j 指向当前匹配的最长前缀长度 + int i = 2, j = 0; + while (i < len) { + if (needle[i - 1] == needle[j]) { + j++; + next[i] = j; + i++; + } else if (j > 0) { + // 如果不匹配且 j > 0, 回退到次长匹配前缀的长度 + j = next[j]; + } else { + next[i] = 0; + i++; + } + } + return next; +} + +int strStr(char* haystack, char* needle) { + + int needle_len = strlen(needle); + int haystack_len = strlen(haystack); + + int *next = build_next(needle, needle_len); + + int i = 0, j = 0; // i 指向主串的当前起始位置, j 指向模式串的当前匹配位置 + while (i <= haystack_len - needle_len) { + if (haystack[i + j] == needle[j]) { + j++; + if (j == needle_len) { + free(next); + next = NULL + return i; + } + } else { + i += j - next[j]; // 调整主串的起始位置 + j = j > 0 ? next[j] : 0; + } + } + + free(next); + next = NULL; + return -1; +} +``` + diff --git "a/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" "b/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" old mode 100644 new mode 100755 index 3cfb673a29..4bbf20fbb8 --- "a/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" +++ "b/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -69,7 +67,7 @@ 以求1243为例,流程如图: - + 对应的C++代码如下: @@ -268,8 +266,4 @@ var nextPermutation = function(nums) { ``` -

- - - diff --git "a/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" "b/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" old mode 100644 new mode 100755 index 22936fef13..37248e4819 --- "a/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" +++ "b/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 34. 在排序数组中查找元素的第一个和最后一个位置 @@ -233,7 +231,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 +448,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 +852,4 @@ int* searchRange(int* nums, int numsSize, int target, int* returnSize){ } ``` -

- - - diff --git "a/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" "b/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" old mode 100644 new mode 100755 index 80b7e40e4a..b48910eef7 --- "a/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" +++ "b/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -43,7 +41,7 @@ 这道题目,要在数组中插入目标值,无非是这四种情况。 -![35_搜索插入位置3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201216232148471.png) +![35_搜索插入位置3](https://file1.kamacoder.com/i/algo/20201216232148471.png) * 目标值在数组所有元素之前 * 目标值等于数组中某一个元素 @@ -84,14 +82,14 @@ public: 效率如下: -![35_搜索插入位置](https://code-thinking-1253855093.file.myqcloud.com/pics/20201216232127268.png) +![35_搜索插入位置](https://file1.kamacoder.com/i/algo/20201216232127268.png) ### 二分法 既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 -![35_搜索插入位置4](https://code-thinking-1253855093.file.myqcloud.com/pics/202012162326354.png) +![35_搜索插入位置4](https://file1.kamacoder.com/i/algo/202012162326354.png) 大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。 @@ -101,7 +99,7 @@ public: 大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。 -![35_搜索插入位置5](https://code-thinking-1253855093.file.myqcloud.com/pics/20201216232659199.png) +![35_搜索插入位置5](https://file1.kamacoder.com/i/algo/20201216232659199.png) 二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。 @@ -152,7 +150,7 @@ public: * 空间复杂度:O(1) 效率如下: -![35_搜索插入位置2](https://code-thinking-1253855093.file.myqcloud.com/pics/2020121623272877.png) +![35_搜索插入位置2](https://file1.kamacoder.com/i/algo/2020121623272877.png) ### 二分法第二种写法 @@ -313,18 +311,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 +330,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 +347,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 +546,4 @@ int searchInsert(int* nums, int numsSize, int target){ } ``` -

- - - diff --git "a/problems/0037.\350\247\243\346\225\260\347\213\254.md" "b/problems/0037.\350\247\243\346\225\260\347\213\254.md" old mode 100644 new mode 100755 index d96e59dfeb..204f0cc092 --- "a/problems/0037.\350\247\243\346\225\260\347\213\254.md" +++ "b/problems/0037.\350\247\243\346\225\260\347\213\254.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

> 如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) @@ -20,11 +18,11 @@ 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。 -![解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/202011171912586.png) +![解数独](https://file1.kamacoder.com/i/algo/202011171912586.png) 一个数独。 -![解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/20201117191340669.png) +![解数独](https://file1.kamacoder.com/i/algo/20201117191340669.png) 答案被标成红色。 @@ -54,7 +52,7 @@ 因为这个树形结构太大了,我抽取一部分,如图所示: -![37.解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111720451790-20230310131816104.png) +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790-20230310131816104.png) ### 回溯三部曲 @@ -85,9 +83,9 @@ bool backtracking(vector>& board) * 递归单层搜索逻辑 -![37.解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111720451790-20230310131822254.png) +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790-20230310131822254.png) -在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归) +在树形图中可以看出我们需要的是一个二维的递归 (一行一列) **一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!** @@ -224,7 +222,7 @@ public: ### Java - +解法一: ```java class Solution { public void solveSudoku(char[][] board) { @@ -291,7 +289,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 @@ -300,40 +364,56 @@ class Solution: """ Do not return anything, modify board in-place instead. """ - self.backtracking(board) - - def backtracking(self, board: List[List[str]]) -> bool: - # 若有解,返回True;若无解,返回False - for i in range(len(board)): # 遍历行 - for j in range(len(board[0])): # 遍历列 - # 若空格内已有数字,跳过 - if board[i][j] != '.': continue - for k in range(1, 10): - if self.is_valid(i, j, k, board): - board[i][j] = str(k) - if self.backtracking(board): return True - board[i][j] = '.' - # 若数字1-9都不能成功填入空格,返回False无解 - return False - return True # 有解 - - def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool: - # 判断同一行是否冲突 - for i in range(9): - if board[row][i] == str(val): - return False - # 判断同一列是否冲突 - for j in range(9): - if board[j][col] == str(val): - return False - # 判断同一九宫格是否有冲突 - start_row = (row // 3) * 3 - start_col = (col // 3) * 3 - for i in range(start_row, start_row + 3): - for j in range(start_col, start_col + 3): - if board[i][j] == str(val): - return False - return True + row_used = [set() for _ in range(9)] + col_used = [set() for _ in range(9)] + box_used = [set() for _ in range(9)] + for row in range(9): + for col in range(9): + num = board[row][col] + if num == ".": + continue + row_used[row].add(num) + col_used[col].add(num) + box_used[(row // 3) * 3 + col // 3].add(num) + self.backtracking(0, 0, board, row_used, col_used, box_used) + + def backtracking( + self, + row: int, + col: int, + board: List[List[str]], + row_used: List[List[int]], + col_used: List[List[int]], + box_used: List[List[int]], + ) -> bool: + if row == 9: + return True + + next_row, next_col = (row, col + 1) if col < 8 else (row + 1, 0) + if board[row][col] != ".": + return self.backtracking( + next_row, next_col, board, row_used, col_used, box_used + ) + + for num in map(str, range(1, 10)): + if ( + num not in row_used[row] + and num not in col_used[col] + and num not in box_used[(row // 3) * 3 + col // 3] + ): + board[row][col] = num + row_used[row].add(num) + col_used[col].add(num) + box_used[(row // 3) * 3 + col // 3].add(num) + if self.backtracking( + next_row, next_col, board, row_used, col_used, box_used + ): + return True + board[row][col] = "." + row_used[row].remove(num) + col_used[col].remove(num) + box_used[(row // 3) * 3 + col // 3].remove(num) + return False ``` ### Go @@ -394,7 +474,7 @@ func isvalid(row, col int, k byte, board [][]byte) bool { -### Javascript +### JavaScript ```Javascript var solveSudoku = function(board) { @@ -810,8 +890,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" old mode 100644 new mode 100755 index 81558cc12c..d8dac0b45b --- "a/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" +++ "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -52,7 +50,7 @@ candidates 中的数字可以无限制重复被选取。 本题搜索的过程抽象成树形结构如下: -![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170730367.png) +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) 注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回! 而在[77.组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html) 中都可以知道要递归K层,因为要取k个元素的组合。 @@ -87,7 +85,7 @@ void backtracking(vector& candidates, int target, int sum, int startIndex) 在如下树形结构中: -![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170730367-20230310135337214.png) +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367-20230310135337214.png) 从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。 @@ -160,7 +158,7 @@ public: 在这个树形结构中: -![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170730367-20230310135342472.png) +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367-20230310135342472.png) 以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。 @@ -173,7 +171,7 @@ public: 如图: -![39.组合总和1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170809182.png) +![39.组合总和1](https://file1.kamacoder.com/i/algo/20201223170809182.png) for循环剪枝代码如下: @@ -311,7 +309,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 +658,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" "b/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" old mode 100644 new mode 100755 index 994b04b82f..0d3972662f --- "a/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" +++ "b/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" @@ -1,12 +1,8 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -> 这篇可以说是全网把组合问题如何去重,讲的最清晰的了! - # 40.组合总和II [力扣题目链接](https://leetcode.cn/problems/combination-sum-ii/) @@ -80,7 +76,7 @@ candidates 中的每个数字在每个组合中只能使用一次。 选择过程树形结构如图所示: -![40.组合总和II](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000918.png) +![40.组合总和II](https://file1.kamacoder.com/i/algo/20230310000918.png) 可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。 @@ -130,7 +126,7 @@ if (sum == target) { 这块比较抽象,如图: -![40.组合总和II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000954.png) +![40.组合总和II1](https://file1.kamacoder.com/i/algo/20230310000954.png) 我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: @@ -141,7 +137,7 @@ if (sum == target) { 而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221021163812.png) +![](https://file1.kamacoder.com/i/algo/20221021163812.png) **这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** @@ -806,8 +802,4 @@ public class Solution } } ``` -

- - - diff --git "a/problems/0042.\346\216\245\351\233\250\346\260\264.md" "b/problems/0042.\346\216\245\351\233\250\346\260\264.md" old mode 100644 new mode 100755 index 73d787b13c..c208637b2f --- "a/problems/0042.\346\216\245\351\233\250\346\260\264.md" +++ "b/problems/0042.\346\216\245\351\233\250\346\260\264.md" @@ -1,13 +1,10 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

-> 这个图就是大厂面试经典题目,接雨水! 最常青藤的一道题,面试官百出不厌! # 42. 接雨水 @@ -50,10 +47,10 @@ 首先要明确,要按照行来计算,还是按照列来计算。 按照行来计算如图: -![42.接雨水2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210402091118927.png) +![42.接雨水2](https://file1.kamacoder.com/i/algo/20210402091118927.png) 按照列来计算如图: -![42.接雨水1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210402091208445.png) +![42.接雨水1](https://file1.kamacoder.com/i/algo/20210402091208445.png) 一些同学在实现的时候,很容易一会按照行来计算一会按照列来计算,这样就会越写越乱。 @@ -65,7 +62,7 @@ 这句话可以有点绕,来举一个理解,例如求列4的雨水高度,如图: -![42.接雨水3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210223092732301.png) +![42.接雨水3](https://file1.kamacoder.com/i/algo/20210223092732301.png) 列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。 @@ -204,7 +201,7 @@ public: 1. 首先单调栈是按照行方向来计算雨水,如图: -![42.接雨水2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210223092629946.png) +![42.接雨水2](https://file1.kamacoder.com/i/algo/20210223092629946.png) 知道这一点,后面的就可以理解了。 @@ -218,7 +215,7 @@ public: 如图: -![42.接雨水4](https://code-thinking-1253855093.file.myqcloud.com/pics/2021022309321229.png) +![42.接雨水4](https://file1.kamacoder.com/i/algo/2021022309321229.png) 关于单调栈的顺序给大家一个总结: [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 中求一个元素右边第一个更大元素,单调栈就是递增的,[84.柱状图中最大的矩形](https://programmercarl.com/0084.柱状图中最大的矩形.html)求一个元素右边第一个更小元素,单调栈就是递减的。 @@ -232,7 +229,7 @@ public: 如图所示: -![42.接雨水5](https://code-thinking-1253855093.file.myqcloud.com/pics/20210223094619398.png) +![42.接雨水5](https://file1.kamacoder.com/i/algo/20210223094619398.png) 4. 栈里要保存什么数值 @@ -287,7 +284,7 @@ if (height[i] == height[st.top()]) { // 例如 5 5 1 7 这种情况 如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示: -![42.接雨水4](https://code-thinking-1253855093.file.myqcloud.com/pics/2021022309321229-20230310123027977.png) +![42.接雨水4](https://file1.kamacoder.com/i/algo/2021022309321229-20230310123027977.png) 取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下标记为mid,对应的高度为height[mid](就是图中的高度1)。 @@ -440,6 +437,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 @@ -1068,7 +1092,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" "b/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" old mode 100644 new mode 100755 index d290f55e80..c20cdc65e6 --- "a/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" +++ "b/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" @@ -1,10 +1,8 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心里准备! +> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心理准备! # 45.跳跃游戏 II @@ -49,7 +47,7 @@ 如图: -![45.跳跃游戏II](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232309103.png) +![45.跳跃游戏II](https://file1.kamacoder.com/i/algo/20201201232309103.png) **图中覆盖范围的意义在于,只要红色的区域,最多两步一定可以到!(不用管具体怎么跳,反正一定可以跳到)** @@ -101,11 +99,11 @@ public: 因为当移动下标指向 nums.size - 2 时: - 如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即 ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图: - ![45.跳跃游戏II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232445286.png) + ![45.跳跃游戏II2](https://file1.kamacoder.com/i/algo/20201201232445286.png) - 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图: -![45.跳跃游戏II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232338693.png) +![45.跳跃游戏II1](https://file1.kamacoder.com/i/algo/20201201232338693.png) 代码如下: @@ -374,7 +372,7 @@ func max(a, b int) int { } ``` -### Javascript +### JavaScript ```Javascript var jump = function(nums) { @@ -492,7 +490,34 @@ impl Solution { } } ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int jump(int* nums, int numsSize) { + if(numsSize == 1){ + return 0; + } + int count = 0; + // 记录当前能走的最远距离 + int curDistance = 0; + // 记录下一步能走的最远距离 + int nextDistance = 0; + for(int i = 0; i < numsSize; i++){ + nextDistance = max(i + nums[i], nextDistance); + // 下标到了当前的最大距离 + if(i == nextDistance){ + count++; + curDistance = nextDistance; + } + } + return count; +} +``` + ### C# + ```csharp // 版本二 public class Solution @@ -514,7 +539,4 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0046.\345\205\250\346\216\222\345\210\227.md" "b/problems/0046.\345\205\250\346\216\222\345\210\227.md" old mode 100644 new mode 100755 index 15e6ae162a..356f51b5a8 --- "a/problems/0046.\345\205\250\346\216\222\345\210\227.md" +++ "b/problems/0046.\345\205\250\346\216\222\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 46.全排列 @@ -42,7 +40,8 @@ 我以[1,2,3]为例,抽象成树形结构如下: -![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20211027181706.png) + +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) ### 回溯三部曲 @@ -54,7 +53,7 @@ 但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示: -![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20211027181706.png) +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) 代码如下: @@ -66,7 +65,7 @@ void backtracking (vector& nums, vector& used) * 递归终止条件 -![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209174225145.png) +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) 可以看出叶子节点,就是收割结果的地方。 @@ -200,6 +199,7 @@ class Solution { public void backtrack(int[] nums, LinkedList path) { if (path.size() == nums.length) { result.add(new ArrayList<>(path)); + return; } for (int i =0; i < nums.length; i++) { // 如果path中已有,则跳过 @@ -270,7 +270,7 @@ func dfs(nums []int, cur int) { } ``` -### Javascript +### JavaScript ```js @@ -518,8 +518,5 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0047.\345\205\250\346\216\222\345\210\227II.md" "b/problems/0047.\345\205\250\346\216\222\345\210\227II.md" old mode 100644 new mode 100755 index 7f2c363889..5330997a66 --- "a/problems/0047.\345\205\250\346\216\222\345\210\227II.md" +++ "b/problems/0047.\345\205\250\346\216\222\345\210\227II.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -50,7 +48,7 @@ 我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图: -![47.全排列II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124201331223.png) +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201124201331223.png) 图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。 @@ -132,11 +130,11 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { 树层上去重(used[i - 1] == false),的树形结构如下: -![47.全排列II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124201406192.png) +![47.全排列II2](https://file1.kamacoder.com/i/algo/20201124201406192.png) 树枝上去重(used[i - 1] == true)的树型结构如下: -![47.全排列II3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124201431571.png) +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201124201431571.png) 大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。 @@ -283,7 +281,7 @@ func dfs(nums []int, cur int) { } ``` -### Javascript +### JavaScript ```javascript var permuteUnique = function (nums) { @@ -554,8 +552,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0051.N\347\232\207\345\220\216.md" "b/problems/0051.N\347\232\207\345\220\216.md" old mode 100644 new mode 100755 index 1e1085401d..d06d7798e8 --- "a/problems/0051.N\347\232\207\345\220\216.md" +++ "b/problems/0051.N\347\232\207\345\220\216.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 51. N皇后 @@ -17,7 +15,7 @@ n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上, 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211020232201.png) +![](https://file1.kamacoder.com/i/algo/20211020232201.png) * 输入:n = 4 * 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] @@ -47,7 +45,7 @@ n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上, 下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图: -![51.N皇后](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130182532303.jpg) +![51.N皇后](https://file1.kamacoder.com/i/algo/20210130182532303.jpg) 从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 @@ -87,7 +85,7 @@ void backtracking(int n, int row, vector& chessboard) { * 递归终止条件 在如下树形结构中: -![51.N皇后](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130182532303-20230310122134167.jpg) +![51.N皇后](https://file1.kamacoder.com/i/algo/20210130182532303-20230310122134167.jpg) 可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。 @@ -451,7 +449,7 @@ func isValid(n, row, col int, chessboard [][]string) bool { ``` -### Javascript +### JavaScript ```Javascript /** * @param {number} n @@ -920,8 +918,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0052.N\347\232\207\345\220\216II.md" "b/problems/0052.N\347\232\207\345\220\216II.md" old mode 100644 new mode 100755 index 29c2b58818..6c6650ad00 --- "a/problems/0052.N\347\232\207\345\220\216II.md" +++ "b/problems/0052.N\347\232\207\345\220\216II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -15,7 +13,7 @@ n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并 上图为 8 皇后问题的一种解法。 -![51n皇后](https://code-thinking-1253855093.file.myqcloud.com/pics/20200821152118456.png) +![51n皇后](https://file1.kamacoder.com/i/algo/20200821152118456.png) 给定一个整数 n,返回 n 皇后不同的解决方案的数量。 @@ -306,8 +304,4 @@ class Solution { } } ``` -

- - - diff --git "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" old mode 100644 new mode 100755 index 74ff2ca40d..84bb5f6663 --- "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" +++ "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 53. 最大子序和 @@ -78,7 +76,7 @@ if (count > result) result = count; 如动画所示: -![53.最大子序和](https://code-thinking.cdn.bcebos.com/gifs/53.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C.gif) +![53.最大子序和](https://file1.kamacoder.com/i/algo/53.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C.gif) 红色的起始位置就是贪心每次取 count 为正数的时候,开始一个区间的统计。 @@ -214,6 +212,7 @@ class Solution: return result ``` +贪心法 ```python class Solution: def maxSubArray(self, nums): @@ -226,9 +225,55 @@ class Solution: if count <= 0: # 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 count = 0 return result +``` +动态规划 +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + dp = [0] * len(nums) + dp[0] = nums[0] + res = nums[0] + for i in range(1, len(nums)): + dp[i] = max(dp[i-1] + nums[i], nums[i]) + res = max(res, dp[i]) + return res +``` + +动态规划 + +```python +class Solution: + def maxSubArray(self, nums): + if not nums: + return 0 + dp = [0] * len(nums) # dp[i]表示包括i之前的最大连续子序列和 + dp[0] = nums[0] + result = dp[0] + for i in range(1, len(nums)): + dp[i] = max(dp[i-1]+nums[i], nums[i]) # 状态转移公式 + if dp[i] > result: + result = dp[i] # result 保存dp[i]的最大值 + return result +``` + +动态规划优化 +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + max_sum = float("-inf") # 初始化结果为负无穷大,方便比较取最大值 + current_sum = 0 # 初始化当前连续和 + + for num in nums: + # 更新当前连续和 + # 如果原本的连续和加上当前数字之后没有当前数字大,说明原本的连续和是负数,那么就直接从当前数字开始重新计算连续和 + current_sum = max(current_sum+num, num) + max_sum = max(max_sum, current_sum) # 更新结果 + + return max_sum ``` + ### Go 贪心法 ```go @@ -279,7 +324,7 @@ pub fn max_sub_array(nums: Vec) -> i32 { } ``` -### Javascript: +### JavaScript: ```Javascript var maxSubArray = function(nums) { @@ -445,7 +490,3 @@ public class Solution ``` -

- - - diff --git "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" old mode 100644 new mode 100755 index 70ad7a8482..ba44a36104 --- "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" +++ "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 53. 最大子序和 @@ -56,7 +54,7 @@ dp[0]应该是多少呢? 5. 举例推导dp数组 以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下: -![53.最大子序和(动态规划)](https://code-thinking-1253855093.file.myqcloud.com/pics/20210303104129101.png) +![53.最大子序和(动态规划)](https://file1.kamacoder.com/i/algo/20210303104129101.png) **注意最后的结果可不是dp[nums.size() - 1]!** ,而是dp[6]。 @@ -243,8 +241,4 @@ function maxSubArray(nums: number[]): number { ``` -

- - - diff --git "a/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" "b/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" old mode 100644 new mode 100755 index 85e6a9364b..8b700c1fe8 --- "a/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" +++ "b/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -38,7 +36,7 @@ 由外向内一圈一圈这么画下去,如下所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220922102236.png) +![](https://file1.kamacoder.com/i/algo/20220922102236.png) 这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 @@ -200,7 +198,67 @@ class Solution { } ``` -### Javascript +```java +class Solution { + public List spiralOrder(int[][] matrix) { + List res = new ArrayList<>(); // 存放结果 + if (matrix.length == 0 || matrix[0].length == 0) + return res; + int rows = matrix.length, columns = matrix[0].length; + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = 0; // 循环次数 + int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度 + while (loop < Math.min(rows, columns) / 2) { + int i = startx; + int j = starty; + // 模拟填充上行从左到右(左闭右开) + for (; j < columns - offset; j++) { + res.add(matrix[i][j]); + } + // 模拟填充右列从上到下(左闭右开) + for (; i < rows - offset; i++) { + res.add(matrix[i][j]); + } + // 模拟填充下行从右到左(左闭右开) + for (; j > starty; j--) { + res.add(matrix[i][j]); + } + // 模拟填充左列从下到上(左闭右开) + for (; i > startx; i--) { + res.add(matrix[i][j]); + } + + // 起始位置加1 循环次数加1 并控制每条边遍历的长度 + startx++; + starty++; + offset++; + loop++; + } + + // 如果列或行中的最小值为奇数 则一定有未遍历的部分 + // 可以自行画图理解 + if (Math.min(rows, columns) % 2 == 1) { + // 当行大于列时 未遍历的部分是列 + // (startx, starty)即下一个要遍历位置 从该位置出发 遍历完未遍历的列 + // 遍历次数为rows - columns + 1 + if (rows > columns) { + for (int i = 0; i < rows - columns + 1; i++) { + res.add(matrix[startx++][starty]); + } + } else { + // 此处与上面同理 遍历完未遍历的行 + for (int i = 0; i < columns - rows + 1; i++) { + res.add(matrix[startx][starty++]); + } + } + } + + return res; + } +} +``` + +### JavaScript ``` /** * @param {number[][]} matrix @@ -348,8 +406,80 @@ class Solution(object): return print_list ``` +### Go + +```go +func spiralOrder(matrix [][]int) []int { + rows := len(matrix) + if rows == 0 { + return []int{} + } + columns := len(matrix[0]) + if columns == 0 { + return []int{} + } + res := make([]int, rows * columns) + startx, starty := 0, 0 // 定义每循环一个圈的起始位置 + loop := min(rows, columns) / 2 + mid := min(rows, columns) / 2 + count := 0 // 用来给矩阵中每一个空格赋值 + offset := 1 // 每一圈循环,需要控制每一条边遍历的长度 + for loop > 0 { + i, j := startx, starty + + // 模拟填充上行从左到右(左闭右开) + for ; j < starty + columns - offset; j++ { + res[count] = matrix[startx][j] + count++ + } + // 模拟填充右列从上到下(左闭右开) + for ; i < startx + rows - offset; i++ { + res[count] = matrix[i][j] + count++ + } + // 模拟填充下行从右到左(左闭右开) + for ; j > starty; j-- { + res[count] = matrix[i][j] + count++ + } + // 模拟填充左列从下到上(左闭右开) + for ; i > startx; i-- { + res[count] = matrix[i][starty] + count++ + } + + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startx++ + starty++ + + // offset 控制每一圈里每一条边遍历的长度 + offset += 2 + loop-- + } + + // 如果min(rows, columns)为奇数的话,需要单独给矩阵最中间的位置赋值 + if min(rows, columns) % 2 == 1 { + if rows > columns { + for i := mid; i < mid + rows - columns + 1; i++ { + res[count] = matrix[i][mid] + count++ + } + } else { + for i := mid; i < mid + columns - rows + 1; i++ { + res[count] = matrix[mid][i] + count++ + } + } + } + return res +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + -

- - - diff --git "a/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" old mode 100644 new mode 100755 index 086fd64f5e..513fc2e340 --- "a/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" +++ "b/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 55. 跳跃游戏 @@ -50,7 +48,7 @@ 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230203105634.png) +![](https://file1.kamacoder.com/i/algo/20230203105634.png) i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。 @@ -143,6 +141,23 @@ class Solution: return False ``` +```python +## 基于当前最远可到达位置判断 +class Solution: + def canJump(self, nums: List[int]) -> bool: + far = nums[0] + for i in range(len(nums)): + # 要考虑两个情况 + # 1. i <= far - 表示 当前位置i 可以到达 + # 2. i > far - 表示 当前位置i 无法到达 + if i > far: + return False + far = max(far, nums[i]+i) + # 如果循环正常结束,表示最后一个位置也可以到达,否则会在中途直接退出 + # 关键点在于,要想明白其实列表中的每个位置都是需要验证能否到达的 + return True +``` + ### Go ```go @@ -166,7 +181,7 @@ func max(a, b int ) int { } ``` -### Javascript +### JavaScript ```Javascript var canJump = function(nums) { @@ -276,7 +291,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" "b/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" old mode 100644 new mode 100755 index 122e783a27..24a97f6c5a --- "a/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" +++ "b/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 56. 合并区间 @@ -40,7 +38,7 @@ 这么说有点抽象,看图:(**注意图中区间都是按照左边界排序之后了**) -![56.合并区间](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223200632791.png) +![56.合并区间](https://file1.kamacoder.com/i/algo/20201223200632791.png) 知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢? @@ -215,7 +213,7 @@ func max56(a, b int) int { ``` -### Javascript +### JavaScript ```javascript var merge = function (intervals) { intervals.sort((a, b) => a[0] - b[0]); @@ -336,7 +334,49 @@ impl Solution { } } ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// 根据左边界进行排序 +int cmp(const void * var1, const void * var2){ + int *v1 = *(int **) var1; + int *v2 = *(int **) var2; + return v1[0] - v2[0]; +} + +int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes) { + int ** result = malloc(sizeof (int *) * intervalsSize); + * returnColumnSizes = malloc(sizeof (int ) * intervalsSize); + for(int i = 0; i < intervalsSize; i++){ + result[i] = malloc(sizeof (int ) * 2); + } + qsort(intervals, intervalsSize, sizeof (int *), cmp); + int count = 0; + for(int i = 0; i < intervalsSize; i++){ + // 记录区间的左右边界 + int L = intervals[i][0], R = intervals[i][1]; + // 如果count为0或者前一区间的右区间小于此时的左边,加入结果中 + if (count == 0 || result[count - 1][1] < L) { + returnColumnSizes[0][count] = 2; + result[count][0] = L; + result[count][1] = R; + count++; + } + else{ // 更新右边界的值 + result[count - 1][1] = max(R, result[count - 1][1]); + } + } + *returnSize = count; + return result; +} +``` + + + ### C# + ```csharp public class Solution { @@ -363,8 +403,3 @@ public class Solution } ``` -

- - - - diff --git "a/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" old mode 100644 new mode 100755 index 7f73bc488d..927df1c6c1 --- "a/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" +++ "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -56,7 +54,7 @@ 那么我按照左闭右开的原则,来画一圈,大家看一下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220922102236.png) +![](https://file1.kamacoder.com/i/algo/20220922102236.png) 这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 @@ -715,26 +713,65 @@ object Solution { ### C#: ```csharp -public class Solution { - public int[][] GenerateMatrix(int n) { - int[][] answer = new int[n][]; - for(int i = 0; i < n; i++) - answer[i] = new int[n]; - int start = 0; - int end = n - 1; - int tmp = 1; - while(tmp < n * n) +public int[][] GenerateMatrix(int n) +{ + // 参考Carl的代码随想录里面C++的思路 + // https://www.programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html#%E6%80%9D%E8%B7%AF + int startX = 0, startY = 0; // 定义每循环一个圈的起始位置 + int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 + int count = 1; // 用来给矩阵每个空格赋值 + int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) + int offset = 1;// 需要控制每一条边遍历的长度,每次循环右边界收缩一位 + + // 构建result二维数组 + int[][] result = new int[n][]; + for (int k = 0; k < n; k++) + { + result[k] = new int[n]; + } + + int i = 0, j = 0; // [i,j] + while (loop > 0) + { + i = startX; + j = startY; + // 四个For循环模拟转一圈 + // 第一排,从左往右遍历,不取最右侧的值(左闭右开) + for (; j < n - offset; j++) + { + result[i][j] = count++; + } + // 右侧的第一列,从上往下遍历,不取最下面的值(左闭右开) + for (; i < n - offset; i++) + { + result[i][j] = count++; + } + + // 最下面的第一行,从右往左遍历,不取最左侧的值(左闭右开) + for (; j > startY; j--) + { + result[i][j] = count++; + } + + // 左侧第一列,从下往上遍历,不取最左侧的值(左闭右开) + for (; i > startX; i--) { - for(int i = start; i < end; i++) answer[start][i] = tmp++; - for(int i = start; i < end; i++) answer[i][end] = tmp++; - for(int i = end; i > start; i--) answer[end][i] = tmp++; - for(int i = end; i > start; i--) answer[i][start] = tmp++; - start++; - end--; - } - if(n % 2 == 1) answer[n / 2][n / 2] = tmp; - return answer; + result[i][j] = count++; + } + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startX++; + startY++; + + // offset 控制每一圈里每一条边遍历的长度 + offset++; + loop--; + } + if (n % 2 == 1) + { + // n 为奇数 + result[mid][mid] = count; } + return result; } ``` @@ -790,7 +827,3 @@ def generate_matrix(n) end ``` -

- - - diff --git "a/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" "b/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" old mode 100644 new mode 100755 index 207a66ee80..ac60767dce --- "a/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" +++ "b/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 62.不同路径 @@ -18,7 +16,7 @@ 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110174033215.png) +![](https://file1.kamacoder.com/i/algo/20210110174033215.png) * 输入:m = 3, n = 7 * 输出:28 @@ -64,7 +62,7 @@ 如图举例: -![62.不同路径](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209113602700.png) +![62.不同路径](https://file1.kamacoder.com/i/algo/20201209113602700.png) 此时问题就可以转化为求二叉树叶子节点的个数,代码如下: @@ -133,7 +131,7 @@ for (int j = 0; j < n; j++) dp[0][j] = 1; 如图所示: -![62.不同路径1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209113631392.png) +![62.不同路径1](https://file1.kamacoder.com/i/algo/20201209113631392.png) 以上动规五部曲分析完毕,C++代码如下: @@ -182,7 +180,7 @@ public: 在这个图中,可以看出一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。 -![62.不同路径](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209113602700-20230310120944078.png) +![62.不同路径](https://file1.kamacoder.com/i/algo/20201209113602700-20230310120944078.png) 在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。 @@ -192,7 +190,7 @@ public: 那么答案,如图所示: -![62.不同路径2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209113725324.png) +![62.不同路径2](https://file1.kamacoder.com/i/algo/20201209113725324.png) **求组合的时候,要防止两个int相乘溢出!** 所以不能把算式的分子都算出来,分母都算出来再做除法。 @@ -285,6 +283,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 递归 @@ -353,6 +369,7 @@ class Solution: ``` ### Go +动态规划 ```Go func uniquePaths(m int, n int) int { dp := make([][]int, m) @@ -372,7 +389,27 @@ func uniquePaths(m int, n int) int { } ``` -### Javascript +数论方法 +```Go +func uniquePaths(m int, n int) int { + numerator := 1 + denominator := m - 1 + count := m - 1 + t := m + n - 2 + for count > 0 { + numerator *= t + t-- + for denominator != 0 && numerator % denominator == 0 { + numerator /= denominator + denominator-- + } + count-- + } + return numerator +} +``` + +### JavaScript ```Javascript var uniquePaths = function(m, n) { @@ -576,8 +613,4 @@ public class Solution -

- - - diff --git "a/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" "b/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" old mode 100644 new mode 100755 index 8c208ea865..f39afe8455 --- "a/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" +++ "b/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 63. 不同路径 II @@ -16,13 +14,13 @@ 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210111204901338.png) +![](https://file1.kamacoder.com/i/algo/20210111204901338.png) 网格中的障碍物和空位置分别用 1 和 0 来表示。 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210111204939971.png) +![](https://file1.kamacoder.com/i/algo/20210111204939971.png) * 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] * 输出:2 @@ -34,7 +32,7 @@ 示例 2: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210111205857918.png) +![](https://file1.kamacoder.com/i/algo/20210111205857918.png) * 输入:obstacleGrid = [[0,1],[0,0]] * 输出:1 @@ -95,7 +93,7 @@ for (int j = 0; j < n; j++) dp[0][j] = 1; 如图: -![63.不同路径II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114513928.png) +![63.不同路径II](https://file1.kamacoder.com/i/algo/20210104114513928.png) 下标(0, j)的初始化情况同理。 @@ -129,11 +127,11 @@ for (int i = 1; i < m; i++) { 拿示例1来举例如题: -![63.不同路径II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114548983.png) +![63.不同路径II1](https://file1.kamacoder.com/i/algo/20210104114548983.png) 对应的dp table 如图: -![63.不同路径II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114610256.png) +![63.不同路径II2](https://file1.kamacoder.com/i/algo/20210104114610256.png) 如果这个图看不懂,建议再理解一下递归公式,然后照着文章中说的遍历顺序,自己推导一下! @@ -145,7 +143,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; @@ -465,7 +463,7 @@ func uniquePathsWithObstacles(obstacleGrid [][]int) int { } ``` -### Javascript +### JavaScript ```Javascript var uniquePathsWithObstacles = function(obstacleGrid) { @@ -550,6 +548,27 @@ function uniquePathsWithObstacles(obstacleGrid: number[][]): number { }; ``` +// 版本二: dp改為使用一維陣列,從終點開始遍歷 +```typescript +function uniquePathsWithObstacles(obstacleGrid: number[][]): number { + const m = obstacleGrid.length; + const n = obstacleGrid[0].length; + + const dp: number[] = new Array(n).fill(0); + dp[n - 1] = 1; + + // 由下而上,右而左進行遍歷 + for (let i = m - 1; i >= 0; i--) { + for (let j = n - 1; j >= 0; j--) { + if (obstacleGrid[i][j] === 1) dp[j] = 0; + else dp[j] = dp[j] + (dp[j + 1] || 0); + } + } + + return dp[0]; +}; +``` + ### Rust ```Rust @@ -759,8 +778,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0070.\347\210\254\346\245\274\346\242\257.md" "b/problems/0070.\347\210\254\346\245\274\346\242\257.md" old mode 100644 new mode 100755 index 67bbdd7b81..316fbd4f39 --- "a/problems/0070.\347\210\254\346\245\274\346\242\257.md" +++ "b/problems/0070.\347\210\254\346\245\274\346\242\257.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 70. 爬楼梯 @@ -103,7 +101,7 @@ dp[i]: 爬到第i层楼梯,有dp[i]种方法 举例当n为5的时候,dp table(dp数组)应该是这样的 -![70.爬楼梯](https://code-thinking-1253855093.file.myqcloud.com/pics/20210105202546299.png) +![70.爬楼梯](https://file1.kamacoder.com/i/algo/20210105202546299.png) 如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。 @@ -130,8 +128,8 @@ public: }; ``` -* 时间复杂度:$O(n)$ -* 空间复杂度:$O(n)$ +* 时间复杂度:O(n) +* 空间复杂度:O(n) 当然依然也可以,优化一下空间复杂度,代码如下: @@ -154,8 +152,8 @@ public: }; ``` -* 时间复杂度:$O(n)$ -* 空间复杂度:$O(1)$ +* 时间复杂度:O(n) +* 空间复杂度:O(1) 后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,**但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化**。 @@ -327,7 +325,7 @@ func climbStairs(n int) int { return dp[n] } ``` -### Javascript +### JavaScript ```Javascript var climbStairs = function(n) { // dp[i] 为第 i 阶楼梯有多少种方法爬到楼顶 @@ -519,8 +517,5 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" "b/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" old mode 100644 new mode 100755 index 4fa294cfe2..a5435ddd71 --- "a/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" +++ "b/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 70. 爬楼梯(进阶版) @@ -165,11 +163,35 @@ class climbStairs{ ``` ### Python3: +```python3 +def climbing_stairs(n,m): + dp = [0]*(n+1) # 背包总容量 + dp[0] = 1 + # 排列题,注意循环顺序,背包在外物品在内 + for j in range(1,n+1): + for i in range(1,m+1): + if j>=i: + dp[j] += dp[j-i] # 这里i就是重量而非index + return dp[n] +if __name__ == '__main__': + n,m = list(map(int,input().split(' '))) + print(climbing_stairs(n,m)) +``` ### Go: ```go +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + func climbStairs(n int, m int) int { dp := make([]int, n+1) dp[0] = 1 @@ -197,15 +219,34 @@ 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.\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" old mode 100644 new mode 100755 index 777b851cca..c4bcbb4338 --- "a/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" +++ "b/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 72. 编辑距离 @@ -172,7 +170,7 @@ for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; 可以看出dp[i][j]是依赖左方,上方和左上方元素的,如图: -![72.编辑距离](https://code-thinking-1253855093.file.myqcloud.com/pics/20210114162113131.jpg) +![72.编辑距离](https://file1.kamacoder.com/i/algo/20210114162113131.jpg) 所以在dp矩阵中一定是从左到右从上到下去遍历。 @@ -196,7 +194,7 @@ for (int i = 1; i <= word1.size(); i++) { 以示例1为例,输入:`word1 = "horse", word2 = "ros"`为例,dp矩阵状态图如下: -![72.编辑距离1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210114162132300.jpg) +![72.编辑距离1](https://file1.kamacoder.com/i/algo/20210114162132300.jpg) 以上动规五部分析完毕,C++代码如下: @@ -313,7 +311,7 @@ func Min(args ...int) int { } ``` -### Javascript: +### JavaScript: ```javascript const minDistance = (word1, word2) => { @@ -462,7 +460,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0077.\347\273\204\345\220\210.md" "b/problems/0077.\347\273\204\345\220\210.md" old mode 100644 new mode 100755 index 103fb627f5..4c9e97fd47 --- "a/problems/0077.\347\273\204\345\220\210.md" +++ "b/problems/0077.\347\273\204\345\220\210.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 第77题. 组合 @@ -84,7 +82,7 @@ for (int i = 1; i <= n; i++) { 那么我把组合问题抽象为如下树形结构: -![77.组合](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195223940.png) +![77.组合](https://file1.kamacoder.com/i/algo/20201123195223940.png) 可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。 @@ -128,7 +126,7 @@ vector path; // 用来存放符合条件结果 从下图中红线部分可以看出,在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。 -![77.组合2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195328976.png) +![77.组合2](https://file1.kamacoder.com/i/algo/20201123195328976.png) 所以需要startIndex来记录下一层递归,搜索的起始位置。 @@ -148,7 +146,7 @@ path这个数组的大小如果达到k,说明我们找到了一个子集大小 如图红色部分: -![77.组合3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195407907.png) +![77.组合3](https://file1.kamacoder.com/i/algo/20201123195407907.png) 此时用result二维数组,把path保存起来,并终止本层递归。 @@ -165,7 +163,7 @@ if (path.size() == k) { 回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。 -![77.组合1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195242899.png) +![77.组合1](https://file1.kamacoder.com/i/algo/20201123195242899.png) 如此我们才遍历完图中的这棵树。 @@ -269,7 +267,7 @@ for (int i = startIndex; i <= n; i++) { 这么说有点抽象,如图所示: -![77.组合4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130194335207-20230310134409532.png) +![77.组合4](https://file1.kamacoder.com/i/algo/20210130194335207-20230310134409532.png) 图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 @@ -468,29 +466,59 @@ func dfs(n int, k int, start int) { } ``` -### Javascript +### 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 +873,4 @@ public class Solution } } ``` -

- - - diff --git "a/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" "b/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" old mode 100644 new mode 100755 index 9577d65f3c..8ddc4058cc --- "a/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" +++ "b/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -69,7 +67,7 @@ for (int i = startIndex; i <= n; i++) { 这么说有点抽象,如图所示: -![77.组合4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130194335207.png) +![77.组合4](https://file1.kamacoder.com/i/algo/20210130194335207.png) 图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 @@ -411,8 +409,4 @@ object Solution { } ``` -

- - - diff --git "a/problems/0078.\345\255\220\351\233\206.md" "b/problems/0078.\345\255\220\351\233\206.md" old mode 100644 new mode 100755 index 06547e3df5..844b8dc2ca --- "a/problems/0078.\345\255\220\351\233\206.md" +++ "b/problems/0078.\345\255\220\351\233\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 78.子集 @@ -48,7 +46,7 @@ 以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下: -![78.子集](https://code-thinking.cdn.bcebos.com/pics/78.%E5%AD%90%E9%9B%86.png) +![78.子集](https://file1.kamacoder.com/i/algo/78.%E5%AD%90%E9%9B%86.png) 从图中红线部分,可以看出**遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合**。 @@ -72,7 +70,7 @@ void backtracking(vector& nums, int startIndex) { 从图中可以看出: -![78.子集](https://code-thinking.cdn.bcebos.com/pics/78.%E5%AD%90%E9%9B%86.png) +![78.子集](https://file1.kamacoder.com/i/algo/78.%E5%AD%90%E9%9B%86.png) 剩余集合为空的时候,就是叶子节点。 @@ -246,7 +244,7 @@ func dfs(nums []int, start int) { } ``` -### Javascript +### JavaScript ```Javascript var subsets = function(nums) { @@ -287,6 +285,7 @@ function subsets(nums: number[]): number[][] { ### Rust +思路一:使用本题的标准解法,递归回溯。 ```Rust impl Solution { fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, start_index: usize) { @@ -308,6 +307,30 @@ impl Solution { } } ``` +思路二:使用二进制枚举,n个元素的子集问题一共是$2^n$种情况。如果我们使用一个二进制数字,每一位根据0和1来决定是选取该元素与否,那么一共也是$2^n$的情况,正好可以一一对应,所以我们可以不使用递归,直接利用循环枚举完成子集问题。 +这种方法的优点在于效率高,不需要递归调用,并且代码容易编写。缺点则是过滤某些非法情况时会比递归方法难写一点,不过在子集问题中不存在这个问题。 +```Rust +impl Solution { + pub fn subsets(nums: Vec) -> Vec> { + let n = nums.len(); + // 预分配2^n空间 + let mut result = Vec::with_capacity(1 << n); + // 二进制枚举,2^n种情况 + for i in 0..(1 << n) { + let mut subset = Vec::new(); + for j in 0..n { + // 枚举该二进制数字的每一位 + // 如果该位是1,对应位置上的元素加入子集,否则跳过 + if i & (1 << j) != 0 { + subset.push(nums[j]); + } + } + result.push(subset); + } + result + } +} +``` ### C @@ -466,8 +489,4 @@ public class Solution { ``` -

- - - diff --git "a/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" "b/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" old mode 100644 new mode 100755 index b836705ab5..99fb1678e6 --- "a/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" +++ "b/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 84.柱状图中最大的矩形 @@ -13,9 +11,9 @@ 求在该柱状图中,能够勾勒出来的矩形的最大面积。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210803220437.png) +![](https://file1.kamacoder.com/i/algo/20210803220437.png) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210803220506.png) +![](https://file1.kamacoder.com/i/algo/20210803220506.png) * 1 <= heights.length <=10^5 * 0 <= heights[i] <= 10^4 @@ -116,7 +114,7 @@ public: 我来举一个例子,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230221165730.png) +![](https://file1.kamacoder.com/i/algo/20230221165730.png) 只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。 @@ -181,22 +179,22 @@ public: 如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230221163936.png) +![](https://file1.kamacoder.com/i/algo/20230221163936.png) 那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。 开头为什么要加元素0? -如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),rigt(6),但是得不到 left。 +如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),right(6),但是得不到 left。 (mid、left,right 都是对应版本一里的逻辑) 因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。 -之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 8 进行比较,周而复始,那么计算的最后结果resutl就是0。 如图所示: +之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 6 进行比较,周而复始,那么计算的最后结果result就是0。 如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230221164533.png) +![](https://file1.kamacoder.com/i/algo/20230221164533.png) 所以我们需要在 height数组前后各加一个元素0。 @@ -474,7 +472,128 @@ class Solution: ### Go: -> 单调栈 +暴力解法 + +```go +func largestRectangleArea(heights []int) int { + sum := 0 + for i := 0; i < len(heights); i++ { + left, right := i, i + for left >= 0 { + if heights[left] < heights[i] { + break + } + left-- + } + for right < len(heights) { + if heights[right] < heights[i] { + break + } + right++ + } + w := right - left - 1 + h := heights[i] + sum = max(sum, w * h) + } + return sum +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +双指针解法 + +```go +func largestRectangleArea(heights []int) int { + size := len(heights) + minLeftIndex := make([]int, size) + minRightIndex := make([]int, size) + + // 记录每个柱子 左边第一个小于该柱子的下标 + minLeftIndex[0] = -1 // 注意这里初始化,防止下面while死循环 + for i := 1; i < size; i++ { + t := i - 1 + // 这里不是用if,而是不断向左寻找的过程 + for t >= 0 && heights[t] >= heights[i] { + t = minLeftIndex[t] + } + minLeftIndex[i] = t + } + // 记录每个柱子 右边第一个小于该柱子的下标 + minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环 + for i := size - 2; i >= 0; i-- { + t := i + 1 + // 这里不是用if,而是不断向右寻找的过程 + for t < size && heights[t] >= heights[i] { + t = minRightIndex[t] + } + minRightIndex[i] = t + } + // 求和 + result := 0 + for i := 0; i < size; i++ { + sum := heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1) + result = max(sum, result) + } + return result +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +单调栈 + +```go +func largestRectangleArea(heights []int) int { + result := 0 + heights = append([]int{0}, heights...) // 数组头部加入元素0 + heights = append(heights, 0) // 数组尾部加入元素0 + st := []int{0} + + // 第一个元素已经入栈,从下标1开始 + for i := 1; i < len(heights); i++ { + if heights[i] > heights[st[len(st)-1]] { + st = append(st, i) + } else if heights[i] == heights[st[len(st)-1]] { + st = st[:len(st)-1] + st = append(st, i) + } else { + for len(st) > 0 && heights[i] < heights[st[len(st)-1]] { + mid := st[len(st)-1] + st = st[:len(st)-1] + if len(st) > 0 { + left := st[len(st)-1] + right := i + w := right - left - 1 + h := heights[mid] + result = max(result, w * h) + } + } + st = append(st, i) + } + } + return result +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +单调栈精简 ```go func largestRectangleArea(heights []int) int { @@ -741,8 +860,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0090.\345\255\220\351\233\206II.md" "b/problems/0090.\345\255\220\351\233\206II.md" old mode 100644 new mode 100755 index 6d618978a8..2e8945c90f --- "a/problems/0090.\345\255\220\351\233\206II.md" +++ "b/problems/0090.\345\255\220\351\233\206II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 90.子集II @@ -41,7 +39,7 @@ 用示例中的[1, 2, 2] 来举例,如图所示: (**注意去重需要先对集合排序**) -![90.子集II](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124195411977.png) +![90.子集II](https://file1.kamacoder.com/i/algo/20201124195411977.png) 从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集! @@ -310,6 +308,43 @@ class Solution: ``` ### Go +使用used数组 +```Go +var ( + result [][]int + path []int +) + +func subsetsWithDup(nums []int) [][]int { + result = make([][]int, 0) + path = make([]int, 0) + used := make([]bool, len(nums)) + sort.Ints(nums) // 去重需要排序 + backtracing(nums, 0, used) + return result +} + +func backtracing(nums []int, startIndex int, used []bool) { + tmp := make([]int, len(path)) + copy(tmp, path) + result = append(result, tmp) + for i := startIndex; i < len(nums); i++ { + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 而我们要对同一树层使用过的元素进行跳过 + if i > 0 && nums[i] == nums[i-1] && used[i-1] == false { + continue + } + path = append(path, nums[i]) + used[i] = true + backtracing(nums, i + 1, used) + path = path[:len(path)-1] + used[i] = false + } +} +``` + +不使用used数组 ```Go var ( path []int @@ -339,7 +374,7 @@ func dfs(nums []int, start int) { ``` -### Javascript +### JavaScript ```Javascript @@ -659,8 +694,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" "b/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" old mode 100644 new mode 100755 index c662957a10..6fa732d0c1 --- "a/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" +++ "b/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -56,7 +54,7 @@ 切割问题可以抽象为树型结构,如图: -![93.复原IP地址](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123203735933.png) +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933.png) ### 回溯三部曲 @@ -108,7 +106,7 @@ if (pointNum == 3) { // 逗点数量为3时,分隔结束 如果不合法就结束本层循环,如图中剪掉的分支: -![93.复原IP地址](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123203735933-20230310132314109.png) +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933-20230310132314109.png) 然后就是递归和回溯的过程: @@ -143,7 +141,7 @@ for (int i = startIndex; i < s.size(); i++) { 代码如下: ```CPP -// 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法 +// 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法 bool isValid(const string& s, int start, int end) { if (start > end) { return false; @@ -208,7 +206,7 @@ private: } else break; // 不合法,直接结束本层循环 } } - // 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法 + // 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法 bool isValid(const string& s, int start, int end) { if (start > end) { return false; @@ -376,9 +374,8 @@ class Solution { // 剪枝:ip段的长度最大是3,并且ip段处于[0,255] for (int i = start; i < s.length() && i - start < 3 && Integer.parseInt(s.substring(start, i + 1)) >= 0 && Integer.parseInt(s.substring(start, i + 1)) <= 255; i++) { - // 如果ip段的长度大于1,并且第一位为0的话,continue if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) { - continue; + break; } stringBuilder.append(s.substring(start, i + 1)); // 当stringBuilder里的网段数量小于3时,才会加点;如果等于3,说明已经有3段了,最后一段不需要再加点 @@ -467,9 +464,37 @@ class Solution: num = int(s[start:end+1]) return 0 <= num <= 255 +回溯(版本三) - - +```python +class Solution: + def restoreIpAddresses(self, s: str) -> List[str]: + result = [] + self.backtracking(s, 0, [], result) + return result + + def backtracking(self, s, startIndex, path, result): + if startIndex == len(s): + result.append('.'.join(path[:])) + return + + for i in range(startIndex, min(startIndex+3, len(s))): + # 如果 i 往后遍历了,并且当前地址的第一个元素是 0 ,就直接退出 + if i > startIndex and s[startIndex] == '0': + break + # 比如 s 长度为 5,当前遍历到 i = 3 这个元素 + # 因为还没有执行任何操作,所以此时剩下的元素数量就是 5 - 3 = 2 ,即包括当前的 i 本身 + # path 里面是当前包含的子串,所以有几个元素就表示储存了几个地址 + # 所以 (4 - len(path)) * 3 表示当前路径至多能存放的元素个数 + # 4 - len(path) 表示至少要存放的元素个数 + if (4 - len(path)) * 3 < len(s) - i or 4 - len(path) > len(s) - i: + break + if i - startIndex == 2: + if not int(s[startIndex:i+1]) <= 255: + break + path.append(s[startIndex:i+1]) + self.backtracking(s, i+1, path, result) + path.pop() ``` ### Go @@ -848,8 +873,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" old mode 100644 new mode 100755 index 15b99083e0..e5bc2b6b65 --- "a/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" +++ "b/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 96.不同的二叉搜索树 @@ -14,7 +12,7 @@ 示例: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210113161941835.png) +![](https://file1.kamacoder.com/i/algo/20210113161941835.png) ## 算法公开课 @@ -29,11 +27,11 @@ 了解了二叉搜索树之后,我们应该先举几个例子,画画图,看看有没有什么规律,如图: -![96.不同的二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210107093106367.png) +![96.不同的二叉搜索树](https://file1.kamacoder.com/i/algo/20210107093106367.png) n为1的时候有一棵树,n为2有两棵树,这个是很直观的。 -![96.不同的二叉搜索树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210107093129889.png) +![96.不同的二叉搜索树1](https://file1.kamacoder.com/i/algo/20210107093129889.png) 来看看n为3的时候,有哪几种情况。 @@ -67,7 +65,7 @@ dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索 如图所示: -![96.不同的二叉搜索树2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210107093226241.png) +![96.不同的二叉搜索树2](https://file1.kamacoder.com/i/algo/20210107093226241.png) 此时我们已经找到递推关系了,那么可以用动规五部曲再系统分析一遍。 @@ -120,7 +118,7 @@ for (int i = 1; i <= n; i++) { n为5时候的dp数组状态如图: -![96.不同的二叉搜索树3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210107093253987.png) +![96.不同的二叉搜索树3](https://file1.kamacoder.com/i/algo/20210107093253987.png) 当然如果自己画图举例的话,基本举例到n为3就可以了,n为4的时候,画图已经比较麻烦了。 @@ -221,7 +219,7 @@ func numTrees(n int)int{ } ``` -### Javascript +### JavaScript ```Javascript const numTrees =(n) => { @@ -348,7 +346,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" old mode 100644 new mode 100755 index 88e1628243..990d3c8413 --- "a/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" +++ "b/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 98.验证二叉搜索树 @@ -18,11 +16,11 @@ * 节点的右子树只包含大于当前节点的数。 * 所有左子树和右子树自身必须也是二叉搜索树。 -![98.验证二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000750.png) +![98.验证二叉搜索树](https://file1.kamacoder.com/i/algo/20230310000750.png) ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你对二叉搜索树了解的还不够! | LeetCode:98.验证二叉搜索树](https://www.bilibili.com/video/BV18P411n7Q4),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你对二叉搜索树了解的还不够! | LeetCode:98.验证二叉搜索树](https://www.bilibili.com/video/BV18P411n7Q4),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -104,7 +102,7 @@ if (root->val > root->left->val && root->val < root->right->val) { 例如: [10,5,15,null,null,6,20] 这个case: -![二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000824.png) +![二叉搜索树](https://file1.kamacoder.com/i/algo/20230310000824.png) 节点10大于左节点5,小于右节点15,但右子树里出现了一个6 这就不符合了! @@ -806,8 +804,4 @@ public bool IsValidBST(TreeNode root) } ``` -

- - - diff --git "a/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" "b/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" old mode 100644 new mode 100755 index 56a6c8840f..df1b55a462 --- "a/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" +++ "b/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -14,9 +12,9 @@ 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210726172932.png) +![](https://file1.kamacoder.com/i/algo/20210726172932.png) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210726173011.png) +![](https://file1.kamacoder.com/i/algo/20210726173011.png) ## 思路 @@ -339,8 +337,4 @@ function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { -

- - - diff --git "a/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" "b/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 8442f0ab9e..24e9e2684e --- "a/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 101. 对称二叉树 @@ -11,7 +9,7 @@ 给定一个二叉树,检查它是否是镜像对称的。 -![101. 对称二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203144607387.png) +![101. 对称二叉树](https://file1.kamacoder.com/i/algo/20210203144607387.png) ## 算法公开课 @@ -27,7 +25,7 @@ 比较的是两个子树的里侧和外侧的元素是否相等。如图所示: -![101. 对称二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203144624414.png) +![101. 对称二叉树1](https://file1.kamacoder.com/i/algo/20210203144624414.png) 那么遍历的顺序应该是什么样的呢? @@ -171,7 +169,7 @@ public: 通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示: -![101.对称二叉树](https://code-thinking.cdn.bcebos.com/gifs/101.%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91.gif) +![101.对称二叉树](https://file1.kamacoder.com/i/algo/101.%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91.gif) @@ -224,8 +222,8 @@ public: st.push(root->left); st.push(root->right); while (!st.empty()) { - TreeNode* leftNode = st.top(); st.pop(); TreeNode* rightNode = st.top(); st.pop(); + TreeNode* leftNode = st.top(); st.pop(); if (!leftNode && !rightNode) { continue; } @@ -945,8 +943,5 @@ public bool IsSymmetric(TreeNode root) } ``` -

- - - + diff --git "a/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" "b/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" old mode 100644 new mode 100755 index 4411b5609f..819153be97 --- "a/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" +++ "b/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 二叉树层序遍历登场! @@ -28,7 +26,7 @@ -![我要打十个](https://code-thinking.cdn.bcebos.com/gifs/%E6%88%91%E8%A6%81%E6%89%93%E5%8D%81%E4%B8%AA.gif) +![我要打十个](https://file1.kamacoder.com/i/algo/%E6%88%91%E8%A6%81%E6%89%93%E5%8D%81%E4%B8%AA.gif) @@ -39,7 +37,7 @@ 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 -![102.二叉树的层序遍历](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203144842988.png) +![102.二叉树的层序遍历](https://file1.kamacoder.com/i/algo/20210203144842988.png) ### 思路 @@ -59,7 +57,7 @@ 使用队列实现二叉树广度优先遍历,动画如下: -![102二叉树的层序遍历](https://code-thinking.cdn.bcebos.com/gifs/102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif) +![102二叉树的层序遍历](https://file1.kamacoder.com/i/algo/102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif) 这样就实现了层序从左到右遍历二叉树。 @@ -129,7 +127,7 @@ class Solution { return resList; } - //DFS--递归方式 + //BFS--递归方式 public void checkFun01(TreeNode node, Integer deep) { if (node == null) return; deep++; @@ -201,7 +199,7 @@ class Solution: return result ``` ```python -# 递归法 +#递归法 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): @@ -210,18 +208,24 @@ class Solution: # self.right = right class Solution: def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: + if not root: + return [] + levels = [] - self.helper(root, 0, levels) + + def traverse(node, level): + if not node: + return + + if len(levels) == level: + levels.append([]) + + levels[level].append(node.val) + traverse(node.left, level + 1) + traverse(node.right, level + 1) + + traverse(root, 0) return levels - - def helper(self, node, level, levels): - if not node: - return - if len(levels) == level: - levels.append([]) - levels[level].append(node.val) - self.helper(node.left, level + 1, levels) - self.helper(node.right, level + 1, levels) ``` @@ -259,7 +263,7 @@ func levelOrder(root *TreeNode) [][]int { ```go /** -102. 二叉树的层序遍历 +102. 二叉树的层序遍历 使用container包 */ func levelOrder(root *TreeNode) [][]int { res := [][]int{} @@ -290,6 +294,35 @@ func levelOrder(root *TreeNode) [][]int { return res } +/** + 102. 二叉树的层序遍历 使用切片 +*/ +func levelOrder(root *TreeNode) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + res = append(res, level) + } + return res +} + /** 102. 二叉树的层序遍历:使用切片模拟队列,易理解 */ @@ -321,7 +354,7 @@ func levelOrder(root *TreeNode) (res [][]int) { } ``` -#### Javascript: +#### JavaScript: ```javascript var levelOrder = function(root) { @@ -499,7 +532,7 @@ public IList> LevelOrder(TreeNode root) 给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) -![107.二叉树的层次遍历II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203151058308.png) +![107.二叉树的层次遍历II](https://file1.kamacoder.com/i/algo/20210203151058308.png) ### 思路 @@ -689,30 +722,67 @@ 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); +```GO +// 使用切片作为队列 +func levelOrderBottom(root *TreeNode) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } } - // 从数组前头插入值,避免最后反转数组,减少运算时间 - res.unshift(curLevel); + res = append(res, level) } - return res; + l, r := 0, len(res)-1 + for l < r { + res[l], res[r] = res[r], res[l] + l++ + r-- + } + return res +} +``` + +#### 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); + } + return res; }; + ``` #### TypeScript: @@ -856,7 +926,7 @@ public IList> LevelOrderBottom(TreeNode root) 给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 -![199.二叉树的右视图](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203151307377.png) +![199.二叉树的右视图](https://file1.kamacoder.com/i/algo/20210203151307377.png) ### 思路 @@ -1000,7 +1070,36 @@ func rightSideView(root *TreeNode) []int { } ``` -#### Javascript: +```GO +// 使用切片作为队列 +func rightSideView(root *TreeNode) []int { + res := make([]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + if i == size-1 { + res = append(res, node.Val) + } + } + } + return res +} +``` + +#### JavaScript: ```javascript var rightSideView = function(root) { @@ -1130,17 +1229,58 @@ impl Solution { } ``` +#### C#: + +```C# 199.二叉树的右视图 +public class Solution +{ + public IList RightSideView(TreeNode root) + { + var result = new List(); + Queue queue = new(); + + if (root != null) + { + queue.Enqueue(root); + } + while (queue.Count > 0) + { + int count = queue.Count; + int lastValue = count - 1; + for (int i = 0; i < count; i++) + { + var currentNode = queue.Dequeue(); + if (i == lastValue) + { + result.Add(currentNode.val); + } + + // lastValue == i == count -1 : left 先于 right 进入Queue + if (currentNode.left != null) queue.Enqueue(currentNode.left); + if (currentNode.right != null) queue.Enqueue(currentNode.right); + + //// lastValue == i == 0: right 先于 left 进入Queue + // if(currentNode.right !=null ) queue.Enqueue(currentNode.right); + // if(currentNode.left !=null ) queue.Enqueue(currentNode.left); + } + } + + return result; + } +} +``` + ## 637.二叉树的层平均值 [力扣题目链接](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) 给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 -![637.二叉树的层平均值](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203151350500.png) +![637.二叉树的层平均值](https://file1.kamacoder.com/i/algo/20210203151350500.png) ### 思路 -本题就是层序遍历的时候把一层求个总和在取一个均值。 +本题就是层序遍历的时候把一层求个总和再取一个均值。 C++代码: @@ -1291,30 +1431,59 @@ func averageOfLevels(root *TreeNode) []float64 { } ``` -#### Javascript: - -```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); +```GO +// 使用切片作为队列 +func averageOfLevels(root *TreeNode) []float64 { + res := make([]float64, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + sum := 0 + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + sum += node.Val + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } } - //每一层的平均值存入数组res - res.push(sum/length); + res = append(res, float64(sum)/float64(size)) } + return res +} +``` - return res; +#### JavaScript: + +```javascript +var averageOfLevels = function(root) { + 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); + } + // 求平均值 + res.push(sum / len); + } + return res; }; ``` @@ -1428,6 +1597,35 @@ impl Solution { } ``` +#### C#: + +```C# 二叉树的层平均值 +public class Solution { + public IList AverageOfLevels(TreeNode root) { + var result= new List(); + Queue queue = new(); + if(root !=null) queue.Enqueue(root); + + while (queue.Count > 0) + { + int count = queue.Count; + double value=0; + for (int i = 0; i < count; i++) + { + var curentNode=queue.Dequeue(); + value += curentNode.val; + if (curentNode.left!=null) queue.Enqueue(curentNode.left); + if (curentNode.right!=null) queue.Enqueue(curentNode.right); + } + result.Add(value/count); + } + + return result; + } +} + +``` + ## 429.N叉树的层序遍历 [力扣题目链接](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) @@ -1436,7 +1634,7 @@ impl Solution { 例如,给定一个 3叉树 : -![429. N叉树的层序遍历](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203151439168.png) +![429. N叉树的层序遍历](https://file1.kamacoder.com/i/algo/20210203151439168.png) 返回其层序遍历: @@ -1623,6 +1821,32 @@ func levelOrder(root *Node) [][]int { } ``` +```GO +// 使用切片作为队列 +func levelOrder(root *Node) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if len(node.Children) > 0 { + queue = append(queue, node.Children...) + } + } + res = append(res, level) + } + return res +} +``` + #### JavaScript: ```JavaScript @@ -1782,7 +2006,7 @@ impl Solution { 您需要在二叉树的每一行中找到最大的值。 -![515.在每个树行中找最大值](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203151532153.png) +![515.在每个树行中找最大值](https://file1.kamacoder.com/i/algo/20210203151532153.png) ### 思路 @@ -1922,29 +2146,62 @@ 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); +```GO +// 使用切片作为队列 +func largestValues(root *TreeNode) []int { + res := make([]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + maxValue := math.MinInt64 + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Val > maxValue { + maxValue = node.Val + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } } - //把每一层的最大值放到res数组 - res.push(max); + res = append(res, maxValue) } + return res +} +``` + +#### JavaScript: +```javascript +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; }; ``` @@ -2080,7 +2337,7 @@ struct Node { 初始状态下,所有 next 指针都被设置为 NULL。 -![116.填充每个节点的下一个右侧节点指针](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203152044855.jpg) +![116.填充每个节点的下一个右侧节点指针](https://file1.kamacoder.com/i/algo/20210203152044855.jpg) ### 思路 @@ -2233,6 +2490,34 @@ func connect(root *Node) *Node { ``` +```GO +// 使用切片作为队列 +func connect(root *Node) *Node { + if root == nil { + return root + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[i] + if i != size - 1 { + queue[i].Next = queue[i+1] + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + queue = queue[size:] + } + return root +} +``` + #### JavaScript: ```javascript @@ -2521,6 +2806,34 @@ func connect(root *Node) *Node { } ``` +```GO +// 使用切片作为队列 +func connect(root *Node) *Node { + if root == nil { + return root + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[i] + if i != size - 1 { + queue[i].Next = queue[i+1] + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + queue = queue[size:] + } + return root +} +``` + #### JavaScript: ```javascript @@ -2658,7 +2971,7 @@ object Solution { 给定二叉树 [3,9,20,null,null,15,7], -![104. 二叉树的最大深度](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203153031914-20230310134849764.png) +![104. 二叉树的最大深度](https://file1.kamacoder.com/i/algo/20210203153031914-20230310134849764.png) 返回它的最大深度 3 。 @@ -2668,7 +2981,7 @@ object Solution { 在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示: -![层序遍历](https://code-thinking-1253855093.file.myqcloud.com/pics/20200810193056585-20230310134854803.png) +![层序遍历](https://file1.kamacoder.com/i/algo/20200810193056585-20230310134854803.png) 所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。 @@ -2790,6 +3103,33 @@ func maxDepth(root *TreeNode) int { } ``` +```go +// 使用切片作为队列 +func maxDepth(root *TreeNode) int { + if root == nil { + return 0 + } + depth := 0 + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + depth++ + } + return depth +} +``` + #### JavaScript: ```javascript @@ -2805,21 +3145,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 0 { + size := len(queue) + depth++ + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + if node.Left == nil && node.Right == nil { + return depth + } + } + } + return depth } ``` @@ -3226,8 +3598,4 @@ impl Solution { **致敬叶师傅!** -

- - - diff --git "a/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" "b/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" old mode 100644 new mode 100755 index 1f55f197e5..52d6d0e5fd --- "a/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" +++ "b/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -20,7 +18,7 @@ 给定二叉树 [3,9,20,null,null,15,7], -![104. 二叉树的最大深度](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203153031914-20230310121809902.png) +![104. 二叉树的最大深度](https://file1.kamacoder.com/i/algo/20210203153031914-20230310121809902.png) 返回它的最大深度 3 。 @@ -77,7 +75,7 @@ return depth; 所以整体c++代码如下: ```CPP -class solution { +class Solution { public: int getdepth(TreeNode* node) { if (node == NULL) return 0; @@ -94,7 +92,7 @@ public: 代码精简之后c++代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(TreeNode* root) { if (root == null) return 0; @@ -110,7 +108,7 @@ public: 本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**) ```CPP -class solution { +class Solution { public: int result; void getdepth(TreeNode* node, int depth) { @@ -144,7 +142,7 @@ public: 注意以上代码是为了把细节体现出来,简化一下代码如下: ```CPP -class solution { +class Solution { public: int result; void getdepth(TreeNode* node, int depth) { @@ -174,7 +172,7 @@ public: 在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示: -![层序遍历](https://code-thinking-1253855093.file.myqcloud.com/pics/20200810193056585.png) +![层序遍历](https://file1.kamacoder.com/i/algo/20200810193056585.png) 所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。 @@ -183,7 +181,7 @@ public: c++代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(TreeNode* root) { if (root == NULL) return 0; @@ -219,7 +217,7 @@ public: 例如,给定一个 3叉树 : -![559.n叉树的最大深度](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020315313214.png) +![559.n叉树的最大深度](https://file1.kamacoder.com/i/algo/2021020315313214.png) 我们应返回其最大深度,3。 @@ -232,7 +230,7 @@ public: c++代码: ```CPP -class solution { +class Solution { public: int maxDepth(Node* root) { if (root == 0) return 0; @@ -249,7 +247,7 @@ public: 依然是层序遍历,代码如下: ```CPP -class solution { +class Solution { public: int maxDepth(Node* root) { queue que; @@ -278,7 +276,7 @@ public: 104.二叉树的最大深度 ```java -class solution { +class Solution { /** * 递归法 */ @@ -319,7 +317,7 @@ class Solution { ``` ```java -class solution { +class Solution { /** * 迭代法,使用层序遍历 */ @@ -369,7 +367,7 @@ class Solution { ``` ```java -class solution { +class Solution { /** * 迭代法,使用层序遍历 */ @@ -402,7 +400,7 @@ class solution { 递归法: ```python -class solution: +class Solution: def maxdepth(self, root: treenode) -> int: return self.getdepth(root) @@ -417,7 +415,7 @@ class solution: 递归法:精简代码 ```python -class solution: +class Solution: def maxdepth(self, root: treenode) -> int: if not root: return 0 @@ -604,7 +602,7 @@ func maxDepth(root *Node) int { } ``` -### Javascript : +### JavaScript : 104.二叉树的最大深度 @@ -829,7 +827,42 @@ int maxDepth(struct TreeNode* root){ return depth; } ``` - +二叉树最大深度迭代——后序遍历实现 +```c +int maxDepth(struct TreeNode *root) +{ + if(root == NULL) + return 0; + struct TreeNode *stack[10000] = {}; + int top = -1; + struct TreeNode *p = root, *r = NULL; // r指向上一个被访问的结点 + int depth = 0, maxDepth = -1; + while(p != NULL || top >= 0) + { + if(p != NULL) + { + stack[++top] = p; + depth++; + p = p->left; + } + else + { + p = stack[top]; + if(p->right != NULL && p->right != r) // 右子树未被访问 + p = p->right; + else + { + if(depth >= maxDepth) maxDepth = depth; + p = stack[top--]; + depth--; + r = p; + p = NULL; + } + } + } + return maxDepth; +} +``` ### Swift: 104.二叉树的最大深度 @@ -1159,8 +1192,4 @@ public int MaxDepth(TreeNode root) ``` -

- - - diff --git "a/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" "b/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 0e0ab1d74f..5253325835 --- "a/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -27,7 +25,7 @@ * 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树: -![106. 从中序与后序遍历序列构造二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203154316774.png) +![106. 从中序与后序遍历序列构造二叉树1](https://file1.kamacoder.com/i/algo/20210203154316774.png) ## 算法公开课 @@ -42,7 +40,7 @@ 流程如图: -![106.从中序与后序遍历序列构造二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203154249860.png) +![106.从中序与后序遍历序列构造二叉树](https://file1.kamacoder.com/i/algo/20210203154249860.png) 那么代码应该怎么写呢? @@ -413,7 +411,7 @@ public: 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: -![105. 从前序与中序遍历序列构造二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203154626672.png) +![105. 从前序与中序遍历序列构造二叉树](https://file1.kamacoder.com/i/algo/20210203154626672.png) ### 思路 @@ -556,7 +554,7 @@ public: 举一个例子: -![106.从中序与后序遍历序列构造二叉树2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203154720326.png) +![106.从中序与后序遍历序列构造二叉树2](https://file1.kamacoder.com/i/algo/20210203154720326.png) tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 @@ -794,6 +792,60 @@ func rebuild(inorder []int, postorder []int, rootIdx int, l, r int) *TreeNode { } ``` +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func buildTree(inorder []int, postorder []int) *TreeNode { + if len(postorder) == 0 { + return nil + } + + // 后序遍历数组最后一个元素,就是当前的中间节点 + rootValue := postorder[len(postorder)-1] + root := &TreeNode{Val:rootValue} + + // 叶子结点 + if len(postorder) == 1 { + return root + } + + // 找到中序遍历的切割点 + var delimiterIndex int + for delimiterIndex = 0; delimiterIndex < len(inorder); delimiterIndex++ { + if inorder[delimiterIndex] == rootValue { + break; + } + } + + // 切割中序数组 + // 左闭右开区间:[0, delimiterIndex) + leftInorder := inorder[:delimiterIndex] + // [delimiterIndex + 1, end) + rightInorder := inorder[delimiterIndex+1:] + + // postorder 舍弃末尾元素 + postorder = postorder[:len(postorder)-1] + + // 切割后序数组 + // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 + // [0, len(leftInorder)) + leftPostorder := postorder[:len(leftInorder)] + // [len(leftInorder), end) + rightPostorder := postorder[len(leftInorder):] + + root.Left = buildTree(leftInorder, leftPostorder) + root.Right = buildTree(rightInorder, rightPostorder) + + return root +} +``` + 105 从前序与中序遍历序列构造二叉树 ```go @@ -829,6 +881,60 @@ func build(pre []int, in []int, root int, l, r int) *TreeNode { } ``` +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func buildTree(preorder []int, inorder []int) *TreeNode { + if len(preorder) == 0 { + return nil + } + + // 前序遍历数组第一个元素,就是当前的中间节点 + rootValue := preorder[0] + root := &TreeNode{Val:rootValue} + + // 叶子结点 + if len(preorder) == 1 { + return root + } + + // 找到中序遍历的切割点 + var delimiterIndex int + for delimiterIndex = 0; delimiterIndex < len(inorder); delimiterIndex++ { + if inorder[delimiterIndex] == rootValue { + break + } + } + + // 切割中序数组 + // 左闭右开区间:[0, delimiterIndex) + leftInorder := inorder[:delimiterIndex] + // [delimiterIndex + 1, end) + rightInorder := inorder[delimiterIndex+1:] + + // preorder 舍弃首位元素 + preorder = preorder[1:] + + // 切割前序数组 + // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 + // [0, len(leftInorder)) + leftPreorder := preorder[:len(leftInorder)] + // [len(leftInorder), end) + rightPreorder := preorder[len(leftInorder):] + + root.Left = buildTree(leftPreorder, leftInorder) + root.Right = buildTree(rightPreorder, rightInorder) + + return root +} +``` + ### JavaScript @@ -1242,8 +1348,4 @@ public TreeNode BuildTree(int[] inorder, int[] postorder) } ``` -

- - - diff --git "a/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" old mode 100644 new mode 100755 index 9fa684cfdf..2df1c2615b --- "a/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" +++ "b/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 构造二叉搜索树,一不小心就平衡了 @@ -18,7 +16,7 @@ 示例: -![108.将有序数组转换为二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20201022164420763.png) +![108.将有序数组转换为二叉搜索树](https://file1.kamacoder.com/i/algo/20201022164420763.png) ## 算法公开课 @@ -42,7 +40,7 @@ 例如 有序数组[-10,-3,0,5,9] 就可以构造成这样的二叉搜索树,如图。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220930173553.png) +![](https://file1.kamacoder.com/i/algo/20220930173553.png) 上图中,是符合二叉搜索树的特性吧,如果要这么做的话,是不是本题意义就不大了,所以才强调是平衡二叉搜索树。 @@ -65,7 +63,7 @@ 如下两棵树,都是这个数组的平衡二叉搜索树: -![108.将有序数组转换为二叉搜索树](https://code-thinking.cdn.bcebos.com/pics/108.将有序数组转换为二叉搜索树.png) +![108.将有序数组转换为二叉搜索树](https://file1.kamacoder.com/i/algo/108.将有序数组转换为二叉搜索树.png) 如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。 @@ -334,6 +332,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 +559,4 @@ public TreeNode Traversal(int[] nums, int left, int right) ``` -

- - - diff --git "a/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" "b/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 40fdcd143d..d5b100ae80 --- "a/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -21,7 +19,7 @@ 给定二叉树 [3,9,20,null,null,15,7] -![110.平衡二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020315542230.png) +![110.平衡二叉树](https://file1.kamacoder.com/i/algo/2021020315542230.png) 返回 true 。 @@ -29,7 +27,7 @@ 给定二叉树 [1,2,2,3,3,null,null,4,4] -![110.平衡二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203155447919.png) +![110.平衡二叉树1](https://file1.kamacoder.com/i/algo/20210203155447919.png) 返回 false 。 @@ -48,7 +46,7 @@ 但leetcode中强调的深度和高度很明显是按照节点来计算的,如图: -![110.平衡二叉树2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203155515650.png) +![110.平衡二叉树2](https://file1.kamacoder.com/i/algo/20210203155515650.png) 关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 @@ -609,10 +607,13 @@ class Solution: while stack: node = stack.pop() if node: - stack.append(node) + stack.append(node) # 中 stack.append(None) - if node.left: stack.append(node.left) - if node.right: stack.append(node.right) + # 采用数组进行迭代,先将右节点加入,保证左节点能够先出栈 + if node.right: # 右 + stack.append(node.right) + if node.left: # 左 + stack.append(node.left) else: real_node = stack.pop() left, right = height_map.get(real_node.left, 0), height_map.get(real_node.right, 0) @@ -623,6 +624,8 @@ class Solution: ``` ### Go: +递归法 + ```Go func isBalanced(root *TreeNode) bool { h := getHeight(root) @@ -653,6 +656,64 @@ func max(a, b int) int { } ``` +迭代法 + +```Go +func isBalanced(root *TreeNode) bool { + st := make([]*TreeNode, 0) + if root == nil { + return true + } + st = append(st, root) + for len(st) > 0 { + node := st[len(st)-1] + st = st[:len(st)-1] + if math.Abs(float64(getDepth(node.Left)) - float64(getDepth(node.Right))) > 1 { + return false + } + if node.Right != nil { + st = append(st, node.Right) + } + if node.Left != nil { + st = append(st, node.Left) + } + } + return true +} + +func getDepth(cur *TreeNode) int { + st := make([]*TreeNode, 0) + if cur != nil { + st = append(st, cur) + } + depth := 0 + result := 0 + for len(st) > 0 { + node := st[len(st)-1] + if node != nil { + st = st[:len(st)-1] + st = append(st, node, nil) + depth++ + if node.Right != nil { + st = append(st, node.Right) + } + if node.Left != nil { + st = append(st, node.Left) + } + } else { + st = st[:len(st)-1] + node = st[len(st)-1] + st = st[:len(st)-1] + depth-- + } + if result < depth { + result = depth + } + } + return result +} +``` + ### JavaScript: 递归法: @@ -934,8 +995,4 @@ public int GetHeight(TreeNode root) } ``` -

- - - diff --git "a/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" "b/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" old mode 100644 new mode 100755 index 6d1632d593..e1ee42657c --- "a/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" +++ "b/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 和求最大深度一个套路? @@ -22,7 +20,7 @@ 给定二叉树 [3,9,20,null,null,15,7], -![111.二叉树的最小深度1](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020315582586.png) +![111.二叉树的最小深度1](https://file1.kamacoder.com/i/algo/2021020315582586.png) 返回它的最小深度 2. @@ -40,7 +38,7 @@ 本题依然是前序遍历和后序遍历都可以,前序求的是深度,后序求的是高度。 * 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始) -* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始) +* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从0开始还是从1开始) 那么使用后序遍历,其实求的是根节点到叶子节点的最小距离,就是求高度的过程,不过这个最小距离 也同样是最小深度。 @@ -48,7 +46,7 @@ 本题还有一个误区,在处理节点的过程中,最大深度很容易理解,最小深度就不那么好理解,如图: -![111.二叉树的最小深度](https://code-thinking.cdn.bcebos.com/pics/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) +![111.二叉树的最小深度](https://file1.kamacoder.com/i/algo/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) 这就重新审题了,题目中说的是:**最小深度是从根节点到最近叶子节点的最短路径上的节点数量。**注意是**叶子节点**。 @@ -64,7 +62,7 @@ 代码如下: -``` +```CPP int getDepth(TreeNode* node) ``` @@ -74,14 +72,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); @@ -90,7 +88,7 @@ return result; 这个代码就犯了此图中的误区: -![111.二叉树的最小深度](https://code-thinking.cdn.bcebos.com/pics/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) +![111.二叉树的最小深度](https://file1.kamacoder.com/i/algo/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) 如果这么求的话,没有左孩子的分支会算为最短深度。 @@ -752,7 +750,3 @@ public int MinDepth(TreeNode root) } ``` -

- - - diff --git "a/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" "b/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" old mode 100644 new mode 100755 index d45be3bd82..73795bcfc9 --- "a/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" +++ "b/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 112. 路径总和 @@ -17,7 +15,7 @@ 示例: 给定如下二叉树,以及目标和 sum = 22, -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230407210247.png) +![](https://file1.kamacoder.com/i/algo/20230407210247.png) 返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。 @@ -55,7 +53,7 @@ 如图所示: -![112.路径总和](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020316051216.png) +![112.路径总和](https://file1.kamacoder.com/i/algo/2021020316051216.png) 图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。 @@ -232,7 +230,7 @@ public: 给定如下二叉树,以及目标和 sum = 22, -![113.路径总和ii1.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203160854654.png) +![113.路径总和ii1.png](https://file1.kamacoder.com/i/algo/20210203160854654.png) ### 思路 @@ -241,7 +239,7 @@ public: 如图: -![113.路径总和ii](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203160922745.png) +![113.路径总和ii](https://file1.kamacoder.com/i/algo/20210203160922745.png) 为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**) @@ -309,25 +307,25 @@ public: 0112.路径总和 ```java -class solution { - public boolean haspathsum(treenode root, int targetsum) { +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) { return false; } - targetsum -= root.val; + targetSum -= root.val; // 叶子结点 if (root.left == null && root.right == null) { - return targetsum == 0; + return targetSum == 0; } if (root.left != null) { - boolean left = haspathsum(root.left, targetsum); - if (left) { // 已经找到 + boolean left = hasPathSum(root.left, targetSum); + if (left) { // 已经找到,提前返回 return true; } } if (root.right != null) { - boolean right = haspathsum(root.right, targetsum); - if (right) { // 已经找到 + boolean right = hasPathSum(root.right, targetSum); + if (right) { // 已经找到,提前返回 return true; } } @@ -336,16 +334,16 @@ class solution { } // lc112 简洁方法 -class solution { - public boolean haspathsum(treenode root, int targetsum) { +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) return false; // 为空退出 // 叶子节点判断是否符合 - if (root.left == null && root.right == null) return root.val == targetsum; + if (root.left == null && root.right == null) return root.val == targetSum; // 求两侧分支的路径和 - return haspathsum(root.left, targetsum - root.val) || haspathsum(root.right, targetsum - root.val); + return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); } } ``` @@ -353,22 +351,22 @@ class solution { 迭代 ```java -class solution { - public boolean haspathsum(treenode root, int targetsum) { +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { if(root == null) return false; - stack stack1 = new stack<>(); - stack stack2 = new stack<>(); + Stack stack1 = new Stack<>(); + Stack stack2 = new Stack<>(); stack1.push(root); stack2.push(root.val); - while(!stack1.isempty()) { + while(!stack1.isEmpty()) { int size = stack1.size(); for(int i = 0; i < size; i++) { - treenode node = stack1.pop(); + TreeNode node = stack1.pop(); int sum = stack2.pop(); // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true - if(node.left == null && node.right == null && sum == targetsum) { + if(node.left == null && node.right == null && sum == targetSum) { return true; } // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 @@ -387,8 +385,9 @@ class solution { } } ``` -```Java 統一迭代法 - public boolean hasPathSum(TreeNode root, int targetSum) { +```Java +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { Stack treeNodeStack = new Stack<>(); Stack sumStack = new Stack<>(); @@ -422,38 +421,39 @@ class solution { } return false; } +} ``` 0113.路径总和-ii ```java -class solution { - public List> pathsum(TreeNode root, int targetsum) { +class Solution { + public List> pathSum(TreeNode root, int targetSum) { List> res = new ArrayList<>(); if (root == null) return res; // 非空判断 List path = new LinkedList<>(); - preorderdfs(root, targetsum, res, path); + preOrderDfs(root, targetSum, res, path); return res; } - public void preorderdfs(TreeNode root, int targetsum, List> res, List path) { + public void preOrderDfs(TreeNode root, int targetSum, List> res, List path) { path.add(root.val); // 遇到了叶子节点 if (root.left == null && root.right == null) { // 找到了和为 targetsum 的路径 - if (targetsum - root.val == 0) { + if (targetSum - root.val == 0) { res.add(new ArrayList<>(path)); } return; // 如果和不为 targetsum,返回 } if (root.left != null) { - preorderdfs(root.left, targetsum - root.val, res, path); + preOrderDfs(root.left, targetSum - root.val, res, path); path.remove(path.size() - 1); // 回溯 } if (root.right != null) { - preorderdfs(root.right, targetsum - root.val, res, path); + preOrderDfs(root.right, targetSum - root.val, res, path); path.remove(path.size() - 1); // 回溯 } } @@ -564,10 +564,10 @@ class Solution: return False - def hasPathSum(self, root: TreeNode, sum: int) -> bool: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: if root is None: return False - return self.traversal(root, sum - root.val) + return self.traversal(root, targetSum - root.val) ``` (版本二) 递归 + 精简 @@ -579,12 +579,12 @@ class Solution: # self.left = left # self.right = right class Solution: - def hasPathSum(self, root: TreeNode, sum: int) -> bool: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: if not root: return False - if not root.left and not root.right and sum == root.val: + if not root.left and not root.right and targetSum == root.val: return True - return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val) + return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val) ``` (版本三) 迭代 @@ -596,7 +596,7 @@ class Solution: # self.left = left # self.right = right class Solution: - def hasPathSum(self, root: TreeNode, sum: int) -> bool: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: if not root: return False # 此时栈里要放的是pair<节点指针,路径数值> @@ -659,13 +659,13 @@ class Solution: return - def pathSum(self, root: TreeNode, sum: int) -> List[List[int]]: + def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: self.result.clear() self.path.clear() if not root: return self.result self.path.append(root.val) # 把根节点放进路径 - self.traversal(root, sum - root.val) + self.traversal(root, targetSum - root.val) return self.result ``` @@ -678,7 +678,7 @@ class Solution: # self.left = left # self.right = right class Solution: - def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]: + def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: result = [] self.traversal(root, targetSum, [], result) @@ -703,7 +703,7 @@ class Solution: # self.left = left # self.right = right class Solution: - def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]: + def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: if not root: return [] stack = [(root, [root.val])] @@ -727,6 +727,48 @@ class Solution: ```go //递归法 +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func hasPathSum(root *TreeNode, targetSum int) bool { + if root == nil { + return false + } + return traversal(root, targetSum - root.Val) +} + +func traversal(cur *TreeNode, count int) bool { + if cur.Left == nil && cur.Right == nil && count == 0 { + return true + } + if cur.Left == nil && cur.Right == nil { + return false + } + if cur.Left != nil { + count -= cur.Left.Val + if traversal(cur.Left, count) { + return true + } + count += cur.Left.Val + } + if cur.Right != nil { + count -= cur.Right.Val + if traversal(cur.Right, count) { + return true + } + count += cur.Right.Val + } + return false +} +``` + +```go +//递归法精简 /** * Definition for a binary tree node. * type TreeNode struct { @@ -788,7 +830,7 @@ func traverse(node *TreeNode, result *[][]int, currPath *[]int, targetSum int) { } ``` -### Javascript +### JavaScript 0112.路径总和 @@ -1579,8 +1621,5 @@ public class Solution { // @lc code=end ``` -

- - - + diff --git "a/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" "b/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 96ab2583f1..499bf100e2 --- "a/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 115.不同的子序列 @@ -14,7 +12,7 @@ 题目数据保证答案符合 32 位带符号整数范围。 -![115.不同的子序列示例](https://code-thinking.cdn.bcebos.com/pics/115.不同的子序列示例.jpg) +![115.不同的子序列示例](https://file1.kamacoder.com/i/algo/115.不同的子序列示例.jpg) 提示: @@ -72,7 +70,7 @@ dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为d 从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,如图:,那么 dp[i][0] 和dp[0][j]是一定要初始化的。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221222165412.png) +![](https://file1.kamacoder.com/i/algo/20221222165412.png) 每次当初始化的时候,都要回顾一下dp[i][j]的定义,不要凭感觉初始化。 @@ -103,7 +101,7 @@ for (int j = 1; j <= t.size(); j++) dp[0][j] = 0; // 其实这行代码可以和 从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221222165412.png) +![](https://file1.kamacoder.com/i/algo/20221222165412.png) 所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。 @@ -125,7 +123,7 @@ for (int i = 1; i <= s.size(); i++) { 以s:"baegg",t:"bag"为例,推导dp数组状态如下: -![115.不同的子序列](https://code-thinking.cdn.bcebos.com/pics/115.%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97.jpg) +![115.不同的子序列](https://file1.kamacoder.com/i/algo/115.%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97.jpg) 如果写出来的代码怎么改都通过不了,不妨把dp数组打印出来,看一看,是不是这样的。 @@ -265,7 +263,7 @@ func numDistinct(s string, t string) int { } ``` -### Javascript: +### JavaScript: ```javascript const numDistinct = (s, t) => { @@ -375,8 +373,4 @@ impl Solution { ``` -

- - - diff --git "a/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" "b/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" old mode 100644 new mode 100755 index 60ea9210a2..88d3abc93e --- "a/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" +++ "b/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 116. 填充每个节点的下一个右侧节点指针 @@ -28,7 +26,7 @@ struct Node { * 你只能使用常量级额外空间。 * 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727143202.png) +![](https://file1.kamacoder.com/i/algo/20210727143202.png) ## 思路 @@ -44,7 +42,7 @@ struct Node { 如图,假如当前操作的节点是cur: - + 最关键的点是可以通过上一层递归 搭出来的线,进行本次搭线。 @@ -169,6 +167,56 @@ class Solution { } ``` +```Java +// 迭代法 +class Solution { + public Node connect(Node root) { + if (root == null) { + return root; + } + + Queue queue = new LinkedList<>(); + + queue.add(root); + + while (!queue.isEmpty()) { + int size = queue.size(); + + // 每层的第一个节点 + Node cur = queue.poll(); + if (cur.left != null) { + queue.add(cur.left); + } + if (cur.right != null) { + queue.add(cur.right); + } + + // 因为已经移除了每层的第一个节点,所以将 0 改为 1 + while (size-- > 1) { + Node next = queue.poll(); + + if (next.left != null) { + queue.add(next.left); + } + if (next.right != null) { + queue.add(next.right); + } + + // 当前节点指向同层的下一个节点 + cur.next = next; + // 更新当前节点 + cur = next; + } + + // 每层的最后一个节点不指向 null 在力扣也能过 + cur.next = null; + } + + return root; + } +} +``` + ### Python ```python @@ -438,8 +486,5 @@ public class Solution ``` -

- - - + diff --git "a/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" "b/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" old mode 100644 new mode 100755 index cbdf40e85c..d12cbf2fe2 --- "a/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" +++ "b/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 121. 买卖股票的最佳时机 @@ -131,7 +129,7 @@ dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所 以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下: -![121.买卖股票的最佳时机](https://code-thinking-1253855093.file.myqcloud.com/pics/20210224225642465.png) +![121.买卖股票的最佳时机](https://file1.kamacoder.com/i/algo/20210224225642465.png) dp[5][1]就是最终结果。 @@ -287,9 +285,6 @@ class Solution { return dp[1]; } } -``` -```Java - ``` ### Python: @@ -311,7 +306,7 @@ class Solution: class Solution: def maxProfit(self, prices: List[int]) -> int: length = len(prices) - if len == 0: + if length == 0: return 0 dp = [[0] * 2 for _ in range(length)] dp[0][0] = -prices[0] @@ -469,7 +464,7 @@ function maxProfit(prices: number[]): number { }; ``` -> 动态规划 +> 动态规划:版本一 ```typescript function maxProfit(prices: number[]): number { @@ -490,6 +485,26 @@ function maxProfit(prices: number[]): number { }; ``` +> 动态规划:版本二 + +```typescript +// dp[i][0] 表示第i天持有股票所得最多现金 +// dp[i][1] 表示第i天不持有股票所得最多现金 +function maxProfit(prices: number[]): number { + const dp:number[][] = Array(2).fill(0).map(item => Array(2)); + dp[0][0] = -prices[0]; + dp[0][1] = 0; + + for (let i = 1; i < prices.length; i++) { + dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], -prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]); + } + + // 返回不持有股票的最大现金 + return dp[(prices.length-1) % 2][1]; +}; +``` + ### C#: > 贪心法 @@ -531,6 +546,52 @@ public class Solution } ``` +### C: + +> 贪心 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int maxProfit(int* prices, int pricesSize) { + int low = INT_MIN; + int result = 0; + for(int i = 0; i < pricesSize; i++){ + low = min(low, prices[i]); + result = max(result, prices[i] - low); + } + return result; +} +``` + +> 动态规划 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int* prices, int pricesSize){ + if(pricesSize == 0){ + return 0; + } + // dp初始化 + int ** dp = malloc(sizeof (int *) * pricesSize); + for(int i = 0; i < pricesSize; i++){ + dp[i] = malloc(sizeof (int ) * 2); + } + // 下标0表示持有股票的情况下的最大现金,下标1表示不持有股票的情况下获得的最大现金 + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for(int i = 1; i < pricesSize; i++){ + dp[i][0] = max(dp[i - 1][0], - prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return dp[pricesSize - 1][1]; +} +``` + + + ### Rust: > 贪心 @@ -564,8 +625,3 @@ impl Solution { ``` -

- - - - diff --git "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" old mode 100644 new mode 100755 index 69706e369a..0da4241931 --- "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" +++ "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 122.买卖股票的最佳时机 II @@ -68,7 +66,7 @@ 如图: -![122.买卖股票的最佳时机II](https://code-thinking-1253855093.file.myqcloud.com/pics/2020112917480858-20230310134659477.png) +![122.买卖股票的最佳时机II](https://file1.kamacoder.com/i/algo/2020112917480858-20230310134659477.png) 一些同学陷入:第一天怎么就没有利润呢,第一天到底算不算的困惑中。 @@ -249,7 +247,7 @@ func max(a, b int) int { } ``` -### Javascript: +### JavaScript: 贪心 @@ -422,8 +420,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" old mode 100644 new mode 100755 index 6e08b57c1d..d8cb308b7e --- "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" +++ "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 122.买卖股票的最佳时机II @@ -251,6 +249,27 @@ func max(a, b int) int { } ``` +```go +// 动态规划 版本二 滚动数组 +func maxProfit(prices []int) int { + dp := [2][2]int{} // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i := 1; i < len(prices); i++ { + dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i - 1) % 2][1] - prices[i]) + dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i]) + } + return dp[(len(prices)-1)%2][1] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + ### JavaScript: ```javascript @@ -365,6 +384,49 @@ public class Solution } ``` +### C: + +> 动态规划 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int* prices, int pricesSize){ + int **dp = malloc(sizeof (int *) * pricesSize); + for (int i = 0; i < pricesSize; ++i) { + dp[i] = malloc(sizeof (int ) * 2); + } + // 0表示持有该股票所得最大,1表示不持有所得最大 + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (int i = 1; i < pricesSize; ++i) { + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return dp[pricesSize - 1][1]; +} +``` + +> 贪心 + +```c +int maxProfit(int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + int result = 0; + for(int i = 1; i < pricesSize; i++){ + // 如果今天股票价格大于昨天,代表有利润 + if(prices[i] > prices[i - 1]){ + result += prices[i] - prices[i - 1]; + } + } + return result; +} +``` + + + ### Rust: > 贪心 @@ -412,7 +474,4 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" "b/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" old mode 100644 new mode 100755 index 72dd90426c..063477cb5a --- "a/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" +++ "b/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 123.买卖股票的最佳时机III @@ -122,7 +120,7 @@ dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); 以输入[1,2,3,4,5]为例 -![123.买卖股票的最佳时机III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201228181724295-20230310134201291.png) +![123.买卖股票的最佳时机III](https://file1.kamacoder.com/i/algo/20201228181724295-20230310134201291.png) 大家可以看到红色框为最后两次卖出的状态。 @@ -316,6 +314,8 @@ class Solution: ### Go: +> 版本一 + ```go func maxProfit(prices []int) int { dp := make([][]int, len(prices)) @@ -344,6 +344,80 @@ func max(a, b int) int { } ``` +> 版本二 + +```go +func maxProfit(prices []int) int { + if len(prices) == 0 { + return 0 + } + dp := make([]int, 5) + dp[1] = -prices[0] + dp[3] = -prices[0] + for i := 1; i < len(prices); i++ { + dp[1] = max(dp[1], dp[0] - prices[i]) + dp[2] = max(dp[2], dp[1] + prices[i]) + dp[3] = max(dp[3], dp[2] - prices[i]) + dp[4] = max(dp[4], dp[3] + prices[i]) + } + return dp[4] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +> 版本三 + +```go +func maxProfit(prices []int) int { + if len(prices) == 0 { + return 0 + } + dp := make([][5]int, len(prices)) + dp[0][1] = -prices[0] + dp[0][3] = -prices[0] + for i := 1; i < len(prices); i++ { + dp[i][1] = max(dp[i-1][1], 0 - prices[i]) + dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]) + dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]) + dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]) + } + return dp[len(prices)-1][4] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +> 版本四:一维 dp 易懂版本 + +```go +func maxProfit(prices []int) int { + dp := make([]int, 4) + dp[0] = -prices[0] + dp[2] = -prices[0] + + for _, price := range prices[1:] { + dc := slices.Clone(dp) // 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + dp[0] = max(dc[0], -price) + dp[1] = max(dc[1], dc[0] + price) + dp[2] = max(dc[2], dc[1] - price) + dp[3] = max(dc[3], dc[2] + price) + } + + return dp[3] +} +``` + ### JavaScript: > 版本一: @@ -413,6 +487,34 @@ function maxProfit(prices: number[]): number { }; ``` +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int maxProfit(int* prices, int pricesSize) { + int buy1 = prices[0], buy2 = prices[0]; + int profit1 = 0, profit2 = 0; + for (int i = 0; i < pricesSize; ++i) { + // 寻找最低点买入 + buy1 = min(buy1, prices[i]); + // 找到第一次交易的最大盈利,并不断维护这一最大值 + profit1 = max(profit1, prices[i] - buy1); + + // 寻找第二次交易的最低投资点,并且考虑前一次交易的成本 + // 当前价格 - 第一次操作的盈利=新的投入成本( + // 为了让盈利最大,要寻找最小的成本) + buy2 = min(buy2, prices[i] - profit1); + // 第二次卖出后的盈利:当前价格减去成本,不断维护这一最大的总利润 + profit2 = max(profit2, prices[i] - buy2); + } + return profit2; +} +``` + + + ### Rust: > 版本一 @@ -461,8 +563,3 @@ impl Solution { ``` -

- - - - diff --git "a/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" "b/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" old mode 100644 new mode 100755 index 6f8933101e..0204606056 --- "a/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" +++ "b/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 127. 单词接龙 @@ -33,7 +31,7 @@ 以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,一条是最短的长度为5,两条长度为6。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210827175432.png) +![](https://file1.kamacoder.com/i/algo/20210827175432.png) 本题只需要求出最短路径的长度就可以了,不用找出路径。 @@ -360,7 +358,3 @@ function diffonechar(word1: string, word2: string): boolean { ``` -

- - - diff --git "a/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" "b/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" old mode 100644 new mode 100755 index ebb36071cf..1568a49469 --- "a/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" +++ "b/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -83,7 +81,7 @@ int vectorToInt(const vector& vec) { 如图: - + 代码如下: @@ -382,8 +380,4 @@ int sumNumbers(struct TreeNode* root){ } ``` -

- - - diff --git "a/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" "b/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" old mode 100644 new mode 100755 index 1ddaaa7f83..10d6585c4c --- "a/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" +++ "b/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 130. 被围绕的区域 @@ -10,7 +8,7 @@ 给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220901104745.png) +![](https://file1.kamacoder.com/i/algo/20220901104745.png) * 输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]] * 输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]] @@ -30,11 +28,11 @@ 步骤一:深搜或者广搜将地图周边的'O'全部改成'A',如图所示: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220902102337.png) +![图一](https://file1.kamacoder.com/i/algo/20220902102337.png) 步骤二:在遍历地图,将'O'全部改成'X'(地图中间的'O'改成了'X'),将'A'改回'O'(保留的地图周边的'O'),如图所示: -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220902102831.png) +![图二](https://file1.kamacoder.com/i/algo/20220902102831.png) 整体C++代码如下,以下使用dfs实现,其实遍历方式dfs,bfs都是可以的。 @@ -792,8 +790,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" old mode 100644 new mode 100755 index ca342d4bc6..c76f1ce2f1 --- "a/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" +++ "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 切割问题其实是一种组合问题! @@ -52,7 +50,7 @@ 所以切割问题,也可以抽象为一棵树形结构,如图: -![131.分割回文串](https://code-thinking.cdn.bcebos.com/pics/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) +![131.分割回文串](https://file1.kamacoder.com/i/algo/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) 递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。 @@ -78,7 +76,7 @@ void backtracking (const string& s, int startIndex) { * 递归函数终止条件 -![131.分割回文串](https://code-thinking.cdn.bcebos.com/pics/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) +![131.分割回文串](https://file1.kamacoder.com/i/algo/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) 从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。 @@ -310,39 +308,35 @@ public: ### Java ```Java class Solution { - List> lists = new ArrayList<>(); - Deque deque = new LinkedList<>(); - + //保持前几题一贯的格式, initialization + List> res = new ArrayList<>(); + List cur = new ArrayList<>(); public List> partition(String s) { - backTracking(s, 0); - return lists; + backtracking(s, 0, new StringBuilder()); + return res; } - - private void backTracking(String s, int startIndex) { - //如果起始位置大于s的大小,说明找到了一组分割方案 - if (startIndex >= s.length()) { - lists.add(new ArrayList(deque)); + private void backtracking(String s, int start, StringBuilder sb){ + //因为是起始位置一个一个加的,所以结束时start一定等于s.length,因为进入backtracking时一定末尾也是回文,所以cur是满足条件的 + if (start == s.length()){ + //注意创建一个新的copy + res.add(new ArrayList<>(cur)); return; } - for (int i = startIndex; i < s.length(); i++) { - //如果是回文子串,则记录 - if (isPalindrome(s, startIndex, i)) { - String str = s.substring(startIndex, i + 1); - deque.addLast(str); - } else { - continue; + //像前两题一样从前往后搜索,如果发现回文,进入backtracking,起始位置后移一位,循环结束照例移除cur的末位 + for (int i = start; i < s.length(); i++){ + sb.append(s.charAt(i)); + if (check(sb)){ + cur.add(sb.toString()); + backtracking(s, i + 1, new StringBuilder()); + cur.remove(cur.size() -1 ); } - //起始位置后移,保证不重复 - backTracking(s, i + 1); - deque.removeLast(); } } - //判断是否是回文串 - private boolean isPalindrome(String s, int startIndex, int end) { - for (int i = startIndex, j = end; i < j; i++, j--) { - if (s.charAt(i) != s.charAt(j)) { - return false; - } + + //helper method, 检查是否是回文 + private boolean check(StringBuilder sb){ + for (int i = 0; i < sb.length()/ 2; i++){ + if (sb.charAt(i) != sb.charAt(sb.length() - 1 - i)){return false;} } return true; } @@ -531,6 +525,7 @@ class Solution: ``` ### Go +回溯 基本版 ```go var ( path []string // 放已经回文的子串 @@ -569,6 +564,63 @@ func isPalindrome(s string) bool { } ``` +回溯+动态规划优化回文串判断 +```go +var ( + result [][]string + path []string // 放已经回文的子串 + isPalindrome [][]bool // 放事先计算好的是否回文子串的结果 +) + +func partition(s string) [][]string { + result = make([][]string, 0) + path = make([]string, 0) + computePalindrome(s) + backtracing(s, 0) + return result +} + +func backtracing(s string, startIndex int) { + // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 + if startIndex >= len(s) { + tmp := make([]string, len(path)) + copy(tmp, path) + result = append(result, tmp) + return + } + for i := startIndex; i < len(s); i++ { + if isPalindrome[startIndex][i] { // 是回文子串 + // 获取[startIndex,i]在s中的子串 + path = append(path, s[startIndex:i+1]) + } else { // 不是回文,跳过 + continue + } + backtracing(s, i + 1) // 寻找i+1为起始位置的子串 + path = path[:len(path)-1] // 回溯过程,弹出本次已经添加的子串 + } +} + +func computePalindrome(s string) { + // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 + isPalindrome = make([][]bool, len(s)) + for i := 0; i < len(isPalindrome); i++ { + isPalindrome[i] = make([]bool, len(s)) + } + for i := len(s)-1; i >= 0; i-- { + // 需要倒序计算, 保证在i行时, i+1行已经计算好了 + for j := i; j < len(s); j++ { + if j == i { + isPalindrome[i][j] = true + } else if j - i == 1 { + isPalindrome[i][j] = s[i] == s[j] + } else { + isPalindrome[i][j] = s[i] == s[j] && isPalindrome[i+1][j-1] + } + } + } +} +``` + ### JavaScript ```js @@ -952,8 +1004,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" "b/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" old mode 100644 new mode 100755 index eb91a1899f..8bbfa4ee10 --- "a/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" +++ "b/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -163,7 +161,7 @@ for (int i = s.size() - 1; i >= 0; i--) { 以输入:"aabc" 为例: -![132.分割回文串II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210124182218844.jpg) +![132.分割回文串II](https://file1.kamacoder.com/i/algo/20210124182218844.jpg) 以上分析完毕,代码如下: @@ -372,8 +370,4 @@ var minCut = function(s) { ``` -

- - - diff --git "a/problems/0134.\345\212\240\346\262\271\347\253\231.md" "b/problems/0134.\345\212\240\346\262\271\347\253\231.md" old mode 100644 new mode 100755 index c093023d5f..5c8b0c3cc8 --- "a/problems/0134.\345\212\240\346\262\271\347\253\231.md" +++ "b/problems/0134.\345\212\240\346\262\271\347\253\231.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 134. 加油站 @@ -146,7 +144,7 @@ i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230117165628.png) +![](https://file1.kamacoder.com/i/algo/20230117165628.png) 那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数? @@ -154,11 +152,11 @@ i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i 那有没有可能 [0,i] 区间 选某一个作为起点,累加到 i这里 curSum是不会小于零呢? 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230117170703.png) +![](https://file1.kamacoder.com/i/algo/20230117170703.png) 如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。 -区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择其实位置了。 +区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择起始位置了。 **那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置**。 @@ -249,6 +247,29 @@ class Solution { } } ``` +``` +// 解法3 +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int tank = 0; // 当前油量 + int totalGas = 0; // 总加油量 + int totalCost = 0; // 总油耗 + int start = 0; // 起点 + for (int i = 0; i < gas.length; i++) { + totalGas += gas[i]; + totalCost += cost[i]; + + tank += gas[i] - cost[i]; + if (tank < 0) { // tank 变为负数 意味着 从0到i之间出发都不能顺利环路一周,因为在此i点必会没油 + tank = 0; // reset tank,类似于题目53.最大子树和reset sum + start = i + 1; // 起点变为i点往后一位 + } + } + if (totalCost > totalGas) return -1; + return start; + } +} +``` ### Python 暴力法 @@ -322,6 +343,37 @@ class Solution: ``` ### Go + +贪心算法(方法一) +```go +func canCompleteCircuit(gas []int, cost []int) int { + curSum := 0 + min := math.MaxInt64 + for i := 0; i < len(gas); i++ { + rest := gas[i] - cost[i] + curSum += rest + if curSum < min { + min = curSum + } + } + if curSum < 0 { + return -1 + } + if min >= 0 { + return 0 + } + for i := len(gas) - 1; i > 0; i-- { + rest := gas[i] - cost[i] + min += rest + if min >= 0 { + return i + } + } + return -1 +} +``` + +贪心算法(方法二) ```go func canCompleteCircuit(gas []int, cost []int) int { curSum := 0 @@ -342,7 +394,7 @@ func canCompleteCircuit(gas []int, cost []int) int { } ``` -### Javascript +### JavaScript 暴力: ```js var canCompleteCircuit = function(gas, cost) { @@ -654,8 +706,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" "b/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" old mode 100644 new mode 100755 index 210f4995dc..9701f0f0c1 --- "a/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" +++ "b/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 135. 分发糖果 @@ -58,7 +56,7 @@ for (int i = 1; i < ratings.size(); i++) { 如图: -![135.分发糖果](https://code-thinking-1253855093.file.myqcloud.com/pics/20201117114916878.png) +![135.分发糖果](https://file1.kamacoder.com/i/algo/20201117114916878.png) 再确定左孩子大于右孩子的情况(从后向前遍历) @@ -68,7 +66,7 @@ for (int i = 1; i < ratings.size(); i++) { 如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230202102044.png) +![](https://file1.kamacoder.com/i/algo/20230202102044.png) **所以确定左孩子大于右孩子的情况一定要从后向前遍历!** @@ -84,7 +82,7 @@ for (int i = 1; i < ratings.size(); i++) { 如图: -![135.分发糖果1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201117115658791.png) +![135.分发糖果1](https://file1.kamacoder.com/i/algo/20201117115658791.png) 所以该过程代码如下: @@ -177,21 +175,20 @@ class Solution { ```python class Solution: def candy(self, ratings: List[int]) -> int: - candyVec = [1] * len(ratings) + n = len(ratings) + candies = [1] * n - # 从前向后遍历,处理右侧比左侧评分高的情况 - for i in range(1, len(ratings)): + # Forward pass: handle cases where right rating is higher than left + for i in range(1, n): if ratings[i] > ratings[i - 1]: - candyVec[i] = candyVec[i - 1] + 1 + candies[i] = candies[i - 1] + 1 - # 从后向前遍历,处理左侧比右侧评分高的情况 - for i in range(len(ratings) - 2, -1, -1): + # Backward pass: handle cases where left rating is higher than right + for i in range(n - 2, -1, -1): if ratings[i] > ratings[i + 1]: - candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1) + candies[i] = max(candies[i], candies[i + 1] + 1) - # 统计结果 - result = sum(candyVec) - return result + return sum(candies) ``` @@ -234,7 +231,7 @@ func findMax(num1 int, num2 int) int { } ``` -### Javascript +### JavaScript ```Javascript var candy = function(ratings) { let candys = new Array(ratings.length).fill(1) @@ -401,8 +398,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" "b/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" old mode 100644 new mode 100755 index d93288ae5a..2015cb90c1 --- "a/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" +++ "b/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -182,7 +180,7 @@ dp[0]表示如果字符串为空的话,说明出现在字典里。 以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图: -![139.单词拆分](https://code-thinking-1253855093.file.myqcloud.com/pics/20210202162652727.jpg) +![139.单词拆分](https://file1.kamacoder.com/i/algo/20210202162652727.jpg) dp[s.size()]就是最终结果。 @@ -243,7 +241,7 @@ public: 使用用例:s = "applepenapple", wordDict = ["apple", "pen"],对应的dp数组状态如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221123205105.png) +![](https://file1.kamacoder.com/i/algo/20240809155103.png) 最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 "apple" 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1。 @@ -394,7 +392,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: @@ -498,6 +517,33 @@ function wordBreak(s: string, wordDict: string[]): boolean { }; ``` +### C + +```c +bool wordBreak(char* s, char** wordDict, int wordDictSize) { + int len = strlen(s); + // 初始化 + bool dp[len + 1]; + memset(dp, false, sizeof (dp)); + dp[0] = true; + for (int i = 1; i < len + 1; ++i) { + for(int j = 0; j < wordDictSize; j++){ + int wordLen = strlen(wordDict[j]); + // 分割点是由i和字典单词长度决定 + int k = i - wordLen; + if(k < 0){ + continue; + } + // 这里注意要限制长度,故用strncmp + dp[i] = (dp[k] && !strncmp(s + k, wordDict[j], wordLen)) || dp[i]; + } + } + return dp[len]; +} +``` + + + ### Rust: ```rust @@ -517,8 +563,3 @@ impl Solution { } ``` -

- - - - diff --git "a/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" "b/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index b1f42ba979..d3583ba866 --- "a/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" +++ "b/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 141. 环形链表 @@ -15,7 +13,7 @@ 如果链表中存在环,则返回 true 。 否则,返回 false 。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727173600.png) +![](https://file1.kamacoder.com/i/algo/20210727173600.png) ## 思路 @@ -31,7 +29,7 @@ 会发现最终都是这种情况, 如下图: - + fast和slow各自再走一步, fast和slow就相遇了 @@ -40,7 +38,7 @@ fast和slow各自再走一步, fast和slow就相遇了 动画如下: -![141.环形链表](https://code-thinking.cdn.bcebos.com/gifs/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) +![141.环形链表](https://file1.kamacoder.com/i/algo/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) C++代码如下 @@ -159,8 +157,4 @@ function hasCycle(head: ListNode | null): boolean { -

- - - diff --git "a/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" old mode 100644 new mode 100755 index a643fd7091..4fd81ef0f5 --- "a/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" +++ "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -22,11 +20,11 @@ **说明**:不允许修改给定的链表。 -![循环链表](https://code-thinking-1253855093.file.myqcloud.com/pics/20200816110112704.png) +![循环链表](https://file1.kamacoder.com/i/algo/20200816110112704.png) ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[把环形链表讲清楚!| LeetCode:142.环形链表II](https://www.bilibili.com/video/BV1if4y1d7ob),相信结合视频在看本篇题解,更有助于大家对链表的理解。** +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[把环形链表讲清楚!| LeetCode:142.环形链表II](https://www.bilibili.com/video/BV1if4y1d7ob),相信结合视频再看本篇题解,更有助于大家对链表的理解。** ## 思路 @@ -52,7 +50,7 @@ 会发现最终都是这种情况, 如下图: -![142环形链表1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210318162236720.png) +![142环形链表1](https://file1.kamacoder.com/i/algo/20210318162236720.png) fast和slow各自再走一步, fast和slow就相遇了 @@ -61,7 +59,7 @@ fast和slow各自再走一步, fast和slow就相遇了 动画如下: -![141.环形链表](https://code-thinking.cdn.bcebos.com/gifs/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) +![141.环形链表](https://file1.kamacoder.com/i/algo/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) ### 如果有环,如何找到这个环的入口 @@ -72,7 +70,7 @@ fast和slow各自再走一步, fast和slow就相遇了 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220925103433.png) +![](https://file1.kamacoder.com/i/algo/20220925103433.png) 那么相遇时: slow指针走过的节点数为: `x + y`, @@ -105,7 +103,7 @@ fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走 动画如下: -![142.环形链表II(求入口)](https://code-thinking.cdn.bcebos.com/gifs/142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II%EF%BC%88%E6%B1%82%E5%85%A5%E5%8F%A3%EF%BC%89.gif) +![142.环形链表II(求入口)](https://file1.kamacoder.com/i/algo/142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II%EF%BC%88%E6%B1%82%E5%85%A5%E5%8F%A3%EF%BC%89.gif) 那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 @@ -156,20 +154,20 @@ public: 即文章[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中如下的地方: -![142环形链表5](https://code-thinking-1253855093.file.myqcloud.com/pics/20210318165123581.png) +![142环形链表5](https://file1.kamacoder.com/i/algo/20210318165123581.png) 首先slow进环的时候,fast一定是先进环来了。 如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: -![142环形链表3](https://code-thinking-1253855093.file.myqcloud.com/pics/2021031816503266.png) +![142环形链表3](https://file1.kamacoder.com/i/algo/2021031816503266.png) 可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: -![142环形链表4](https://code-thinking-1253855093.file.myqcloud.com/pics/2021031816515727.png) +![142环形链表4](https://file1.kamacoder.com/i/algo/2021031816515727.png) 那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 @@ -465,7 +463,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" "b/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index 0b3be9a0f1..e7056913ee --- "a/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" +++ "b/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" @@ -1,14 +1,12 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 143.重排链表 [力扣题目链接](https://leetcode.cn/problems/reorder-list/submissions/) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210726160122.png) +![](https://file1.kamacoder.com/i/algo/20210726160122.png) ## 思路 @@ -38,7 +36,7 @@ public: cur = head; int i = 1; int j = vec.size() - 1; // i j为之前前后的双指针 - int count = 0; // 计数,偶数去后面,奇数取前面 + int count = 0; // 计数,偶数取后面,奇数取前面 while (i <= j) { if (count % 2 == 0) { cur->next = vec[j]; @@ -73,7 +71,7 @@ public: } cur = head; - int count = 0; // 计数,偶数去后面,奇数取前面 + int count = 0; // 计数,偶数取后面,奇数取前面 ListNode* node; while(que.size()) { if (count % 2 == 0) { @@ -98,7 +96,7 @@ public: 如图: - + 这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意! @@ -338,8 +336,85 @@ class Solution: return pre ``` ### Go + +```go +// 方法一 数组模拟 +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func reorderList(head *ListNode) { + vec := make([]*ListNode, 0) + cur := head + if cur == nil { + return + } + for cur != nil { + vec = append(vec, cur) + cur = cur.Next + } + cur = head + i := 1 + j := len(vec) - 1 // i j为前后的双指针 + count := 0 // 计数,偶数取后面,奇数取前面 + for i <= j { + if count % 2 == 0 { + cur.Next = vec[j] + j-- + } else { + cur.Next = vec[i] + i++ + } + cur = cur.Next + count++ + } + cur.Next = nil // 注意结尾 +} +``` + +```go +// 方法二 双向队列模拟 +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func reorderList(head *ListNode) { + que := make([]*ListNode, 0) + cur := head + if cur == nil { + return + } + + for cur.Next != nil { + que = append(que, cur.Next) + cur = cur.Next + } + + cur = head + count := 0 // 计数,偶数取后面,奇数取前面 + for len(que) > 0 { + if count % 2 == 0 { + cur.Next = que[len(que)-1] + que = que[:len(que)-1] + } else { + cur.Next = que[0] + que = que[1:] + } + count++ + cur = cur.Next + } + cur.Next = nil // 注意结尾 +} +``` + ```go -# 方法三 分割链表 +// 方法三 分割链表 func reorderList(head *ListNode) { var slow=head var fast=head @@ -612,7 +687,3 @@ void reorderList(struct ListNode* head){ ``` -

- - - diff --git "a/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" "b/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" old mode 100644 new mode 100755 index 663a68ea5c..de56c51ff7 --- "a/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" +++ "b/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 这不仅仅是一道好题,也展现出计算机的思考方式 @@ -80,9 +78,9 @@ 在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中的对对碰游戏是不是就非常像了。** 如动画所示: -![150.逆波兰表达式求值](https://code-thinking.cdn.bcebos.com/gifs/150.逆波兰表达式求值.gif) +![150.逆波兰表达式求值](https://file1.kamacoder.com/i/algo/150.逆波兰表达式求值.gif) -相信看完动画大家应该知道,这和[1047. 删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)是差不错的,只不过本题不要相邻元素做消除了,而是做运算! +相信看完动画大家应该知道,这和[1047. 删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)是差不多的,只不过本题不要相邻元素做消除了,而是做运算! C++代码如下: @@ -108,7 +106,7 @@ public: } } - int result = st.top(); + long long result = st.top(); st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事) return result; } @@ -169,8 +167,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 = [] @@ -184,21 +186,21 @@ class Solution: return stack.pop() ``` -另一种可行,但因为使用eval相对较慢的方法: +另一种可行,但因为使用eval()相对较慢的方法: ```python -class Solution: +class Solution(object): def evalRPN(self, tokens: List[str]) -> int: stack = [] - for item in tokens: - if item not in {"+", "-", "*", "/"}: - stack.append(item) + for token in tokens: + # 判断是否为数字,因为isdigit()不识别负数,故需要排除第一位的符号 + if token.isdigit() or (len(token)>1 and token[1].isdigit()): + stack.append(token) else: - first_num, second_num = stack.pop(), stack.pop() - stack.append( - int(eval(f'{second_num} {item} {first_num}')) # 第一个出来的在运算符后面 - ) - return int(stack.pop()) # 如果一开始只有一个数,那么会是字符串形式的 - + op2 = stack.pop() + op1 = stack.pop() + # 由题意"The division always truncates toward zero",所以使用int()可以天然取整 + stack.append(str(int(eval(op1 + token + op2)))) + return int(stack.pop()) ``` ### Go: @@ -485,7 +487,64 @@ impl Solution { } ``` -

- - - +### C: + +```c +int str_to_int(char *str) { + // string转integer + int num = 0, tens = 1; + for (int i = strlen(str) - 1; i >= 0; i--) { + if (str[i] == '-') { + num *= -1; + break; + } + num += (str[i] - '0') * tens; + tens *= 10; + } + return num; +} + +int evalRPN(char** tokens, int tokensSize) { + + int *stack = (int *)malloc(tokensSize * sizeof(int)); + assert(stack); + int stackTop = 0; + + for (int i = 0; i < tokensSize; i++) { + char symbol = (tokens[i])[0]; + if (symbol < '0' && (tokens[i])[1] == '\0') { + + // pop两个数字 + int num1 = stack[--stackTop]; + int num2 = stack[--stackTop]; + + // 计算结果 + int result; + if (symbol == '+') { + result = num1 + num2; + } else if (symbol == '-') { + result = num2 - num1; + } else if (symbol == '/') { + result = num2 / num1; + } else { + result = num1 * num2; + } + + // push回stack + stack[stackTop++] = result; + + } else { + + // push数字进stack + int num = str_to_int(tokens[i]); + stack[stackTop++] = num; + + } + } + + int result = stack[0]; + free(stack); + return result; +} +``` + diff --git "a/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" old mode 100644 new mode 100755 index 520f17a7ec..b5246a7dbb --- "a/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" +++ "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -440,11 +438,10 @@ class Solution { ```Python class Solution: def reverseWords(self, s: str) -> str: - # 删除前后空白 - s = s.strip() # 反转整个字符串 s = s[::-1] # 将字符串拆分为单词,并反转每个单词 + # split()函数能够自动忽略多余的空白字符 s = ' '.join(word[::-1] for word in s.split()) return s @@ -467,6 +464,75 @@ class Solution: # 将列表转换成字符串 return " ".join(words) ``` +(版本三) 拆分字符串 + 反转列表 +```python +class Solution: + def reverseWords(self, s): + words = s.split() #type(words) --- list + words = words[::-1] # 反转单词 + return ' '.join(words) #列表转换成字符串 +``` +(版本四) 将字符串转换为列表后,使用双指针去除空格 +```python +class Solution: + def single_reverse(self, s, start: int, end: int): + while start < end: + s[start], s[end] = s[end], s[start] + start += 1 + end -= 1 + + def reverseWords(self, s: str) -> str: + result = "" + fast = 0 + # 1. 首先将原字符串反转并且除掉空格, 并且加入到新的字符串当中 + # 由于Python字符串的不可变性,因此只能转换为列表进行处理 + s = list(s) + s.reverse() + while fast < len(s): + if s[fast] != " ": + if len(result) != 0: + result += " " + while s[fast] != " " and fast < len(s): + result += s[fast] + fast += 1 + else: + fast += 1 + # 2.其次将每个单词进行翻转操作 + slow = 0 + fast = 0 + result = list(result) + while fast <= len(result): + if fast == len(result) or result[fast] == " ": + self.single_reverse(result, slow, fast - 1) + slow = fast + 1 + fast += 1 + else: + fast += 1 + + return "".join(result) +``` + +(版本五) 遇到空格就说明前面的是一个单词,把它加入到一个数组中。 + +```python +class Solution: + def reverseWords(self, s: str) -> str: + words = [] + word = '' + s += ' ' # 帮助处理最后一个字词 + + for char in s: + if char == ' ': # 遇到空格就说明前面的可能是一个单词 + if word != '': # 确认是单词,把它加入到一个数组中 + words.append(word) + word = '' # 清空当前单词 + continue + + word += char # 收集单词的字母 + + words.reverse() + return ' '.join(words) +``` ### Go: @@ -571,7 +637,46 @@ func reverse(b []byte) { } } ``` - +```go +//双指针解法。指针逆序遍历,将遍历后得到的单词(间隔为空格,用以区分)顺序放置在额外空间 +//时间复杂度O(n),空间复杂度O(n) +func reverseWords(s string) string { + strBytes := []byte(s) + n := len(strBytes) + // 记录有效字符范围的起始和结束位置 + start, end := 0, n-1 + // 去除开头空格 + for start < n && strBytes[start] == 32 { + start++ + } + // 处理全是空格或空字符串情况 + if start == n { + return "" + } + // 去除结尾空格 + for end >= 0 && strBytes[end] == 32 { + end-- + } + // 结果切片,预分配容量 + res := make([]byte, 0, end-start+1)//这里挺重要的,本人之前没有预分配容量,每次循环都添加单词,导致内存超限(也可能就是我之前的思路有问题) + // 从后往前遍历有效字符范围 + for i := end; i >= start; { + // 找单词起始位置,直接通过循环条件判断定位 + for ; i >= start && strBytes[i] == 32; i-- { + } + j := i + for ; j >= start && strBytes[j]!= 32; j-- { + } + res = append(res, strBytes[j+1:i+1]...) + // 只在不是最后一个单词时添加空格 + if j > start { + res = append(res, 32) + } + i = j + } + return string(res) +} +``` ### JavaScript: @@ -979,7 +1084,4 @@ public string ReverseWords(string s) { } ``` -

- - - + diff --git "a/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" "b/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index bf62ab30b5..cdc58912fe --- "a/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" +++ "b/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" @@ -1,11 +1,5 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) 同:[链表:链表相交](https://programmercarl.com/面试题02.07.链表相交.html) -

- - - diff --git "a/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" "b/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" old mode 100644 new mode 100755 index e4c5c48400..350533d8c8 --- "a/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" +++ "b/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 188.买卖股票的最佳时机IV @@ -134,7 +132,7 @@ for (int j = 1; j < 2 * k; j += 2) { 以输入[1,2,3,4,5],k=2为例。 -![188.买卖股票的最佳时机IV](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229100358221.png) +![188.买卖股票的最佳时机IV](https://file1.kamacoder.com/i/algo/20201229100358221.png) 最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 @@ -297,8 +295,7 @@ class Solution { ### Python: -版本一 - +> 版本一 ```python class Solution: def maxProfit(self, k: int, prices: List[int]) -> int: @@ -313,7 +310,8 @@ class Solution: dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]) return dp[-1][2*k] ``` -版本二 + +> 版本二 ```python class Solution: def maxProfit(self, k: int, prices: List[int]) -> int: @@ -329,9 +327,31 @@ class Solution: dp[j] = max(dp[j],dp[j-1]+prices[i]) return dp[2*k] ``` + +> 版本三: 一维 dp 数组(易理解版本) +```python +class Solution: + def maxProfit(self, k: int, prices: List[int]) -> int: + dp = [0] * k * 2 + for i in range(k): + dp[i * 2] = -prices[0] + + for price in prices[1:]: + dc = dp.copy() # 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + + for i in range(2 * k): + if i % 2 == 1: + dp[i] = max(dc[i], dc[i - 1] + price) + else: + pre = 0 if i == 0 else dc[i - 1] + dp[i] = max(dc[i], pre - price) + + return dp[-1] +``` + ### Go: -版本一: +> 版本一: ```go // 买卖股票的最佳时机IV 动态规划 @@ -368,7 +388,7 @@ func max(a, b int) int { } ``` -版本二: 三维 dp数组 +> 版本二: 三维 dp数组 ```go func maxProfit(k int, prices []int) int { length := len(prices) @@ -404,6 +424,71 @@ func max188(a, b int) int { } ``` +版本三:空间优化版本 + +```go +func maxProfit(k int, prices []int) int { + n := len(prices) + // k次交易,2 * k种状态 + // 状态从1开始计算,避免判断 + // 奇数时持有(保持或买入) + // 偶数时不持有(保持或卖出) + dp := make([][]int, 2) + dp[0] = make([]int, k * 2 + 1) + dp[1] = make([]int, k * 2 + 1) + + // 奇数状态时持有,i += 2 + for i := 1; i <= k * 2; i += 2 { + dp[0][i] = -prices[0] + } + + for i := 1; i < len(prices); i++ { + for j := 1; j <= k * 2; j++ { + if j % 2 == 1 { + dp[i % 2][j] = max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - 1] - prices[i]) + } else { + dp[i % 2][j] = max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - 1] + prices[i]) + } + } + } + + return dp[(n - 1) % 2][k * 2] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +> 版本四:一维 dp 数组(易理解版本) + +```go +func maxProfit(k int, prices []int) int { + dp := make([]int, 2 * k) + for i := range k { + dp[i * 2] = -prices[0] + } + + for j := 1; j < len(prices); j++ { + dc := slices.Clone(dp) // 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + + for i := range k * 2 { + if i % 2 == 1 { + dp[i] = max(dc[i], dc[i - 1] + prices[j]) + } else { + pre := 0; if i >= 1 { pre = dc[i - 1] } + dp[i] = max(dc[i], pre - prices[j]) + } + } + } + + return dp[2 * k - 1] +} +``` + ### JavaScript: ```javascript @@ -474,6 +559,34 @@ function maxProfit(k: number, prices: number[]): number { }; ``` +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int k, int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + + int dp[pricesSize][2 * k + 1]; + memset(dp, 0, sizeof(int) * pricesSize * (2 * k + 1)); + for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; + } + + for (int i = 1;i < pricesSize; i++) {//枚举股票 + for (int j = 0; j < 2 * k - 1; j += 2) { //更新每一次买入卖出 + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); + } + } + return dp[pricesSize - 1][2 * k]; +} +``` + + + ### Rust: ```rust @@ -525,7 +638,5 @@ impl Solution { ``` -

- - - + + diff --git "a/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" "b/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" old mode 100644 new mode 100755 index d60612e92b..976cbed4d1 --- "a/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" +++ "b/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 189. 旋转数组 @@ -199,10 +197,16 @@ function reverseByRange(nums: number[], left: number, right: number): void { } ``` +### Rust +```rust +impl Solution { + pub fn rotate(nums: &mut Vec, k: i32) { + let k = k as usize % nums.len(); + nums.reverse(); + nums[..k].reverse(); + nums[k..].reverse(); + } +} +``` -

- - - - diff --git "a/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" "b/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" old mode 100644 new mode 100755 index a7bc4c998e..7c0aab8ec0 --- "a/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" +++ "b/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 198.打家劫舍 @@ -89,7 +87,7 @@ for (int i = 2; i < nums.size(); i++) { 以示例二,输入[2,7,9,3,1]为例。 -![198.打家劫舍](https://code-thinking-1253855093.file.myqcloud.com/pics/20210221170954115.jpg) +![198.打家劫舍](https://file1.kamacoder.com/i/algo/20210221170954115.jpg) 红框dp[nums.size() - 1]为结果。 @@ -315,6 +313,31 @@ function rob(nums: number[]): number { }; ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int rob(int* nums, int numsSize) { + if(numsSize == 0){ + return 0; + } + if(numsSize == 1){ + return nums[0]; + } + // dp初始化 + int dp[numsSize]; + dp[0] = nums[0]; + dp[1] = max(nums[0], nums[1]); + for(int i = 2; i < numsSize; i++){ + dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]); + } + return dp[numsSize - 1]; +} +``` + + + ### Rust: ```rust @@ -335,7 +358,4 @@ impl Solution { ``` -

- - - + diff --git "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" old mode 100644 new mode 100755 index 85471f73b5..7ae44b5222 --- "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" +++ "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 200. 岛屿数量 @@ -15,7 +13,7 @@ 此外,你可以假设该网格的四条边均被水包围。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726093256.png) +![](https://file1.kamacoder.com/i/algo/20220726093256.png) 提示: @@ -30,7 +28,7 @@ 也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726094200.png) +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) 这道题题目是 DFS,BFS,并查集,基础题目。 @@ -50,7 +48,7 @@ 如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。 -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220727100846.png) +![图二](https://file1.kamacoder.com/i/algo/20220727100846.png) 超时写法 (从队列中取出节点再标记) @@ -408,9 +406,5 @@ impl Solution { } } ``` -

- - - ``` diff --git "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" old mode 100644 new mode 100755 index 18442943ad..128007bb62 --- "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" +++ "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 200. 岛屿数量 @@ -14,7 +12,7 @@ 此外,你可以假设该网格的四条边均被水包围。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726093256.png) +![](https://file1.kamacoder.com/i/algo/20220726093256.png) 提示: @@ -29,7 +27,7 @@ 也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726094200.png) +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) 这道题题目是 DFS,BFS,并查集,基础题目。 @@ -37,7 +35,7 @@ 在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 -那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 +那么如何把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 ### 深度优先搜索 @@ -463,7 +461,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0202.\345\277\253\344\271\220\346\225\260.md" "b/problems/0202.\345\277\253\344\271\220\346\225\260.md" old mode 100644 new mode 100755 index 719672a281..fdcadee97c --- "a/problems/0202.\345\277\253\344\271\220\346\225\260.md" +++ "b/problems/0202.\345\277\253\344\271\220\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -533,8 +531,28 @@ public class Solution { } } ``` -

- - - + +### Ruby: + +```ruby +# @param {Integer} n +# @return {Boolean} +def is_happy(n) + @occurred_nums = Set.new + + while true + n = next_value(n) + + return true if n == 1 + + return false if @occurred_nums.include?(n) + + @occurred_nums << n + end +end + +def next_value(n) + n.to_s.chars.sum { |char| char.to_i ** 2 } +end +``` diff --git "a/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" old mode 100644 new mode 100755 index d6d7e6c2ef..ffe04a5b07 --- "a/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" +++ "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -36,11 +34,11 @@ 这里以链表 1 4 2 4 来举例,移除元素4。 -![203_链表删除元素1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210316095351161.png) +![203_链表删除元素1](https://file1.kamacoder.com/i/algo/20210316095351161.png) 如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图: -![203_链表删除元素2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210316095418280.png) +![203_链表删除元素2](https://file1.kamacoder.com/i/algo/20210316095418280.png) **当然如果使用java ,python的话就不用手动管理内存了。** @@ -58,16 +56,16 @@ 来看第一种操作:直接使用原来的链表来进行移除。 -![203_链表删除元素3](https://code-thinking-1253855093.file.myqcloud.com/pics/2021031609544922.png) +![203_链表删除元素3](https://file1.kamacoder.com/i/algo/2021031609544922.png) 移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。 所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。 -![203_链表删除元素4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210316095512470.png) +![203_链表删除元素4](https://file1.kamacoder.com/i/algo/20210316095512470.png) 依然别忘将原头结点从内存中删掉。 -![203_链表删除元素5](https://code-thinking-1253855093.file.myqcloud.com/pics/20210316095543775.png) +![203_链表删除元素5](https://file1.kamacoder.com/i/algo/20210316095543775.png) 这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。 @@ -78,7 +76,7 @@ 来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。 -![203_链表删除元素6](https://code-thinking-1253855093.file.myqcloud.com/pics/20210316095619221.png) +![203_链表删除元素6](https://file1.kamacoder.com/i/algo/20210316095619221.png) 这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。 @@ -149,7 +147,35 @@ public: * 时间复杂度: O(n) * 空间复杂度: O(1) +**也可以通过递归的思路解决本题:** +基础情况:对于空链表,不需要移除元素。 + +递归情况:首先检查头节点的值是否为 val,如果是则移除头节点,答案即为在头节点的后续节点上递归的结果;如果头节点的值不为 val,则答案为头节点与在头节点的后续节点上递归得到的新链表拼接的结果。 + +```CPP +class Solution { +public: + ListNode* removeElements(ListNode* head, int val) { + // 基础情况:空链表 + if (head == nullptr) { + return nullptr; + } + + // 递归处理 + if (head->val == val) { + ListNode* newHead = removeElements(head->next, val); + delete head; + return newHead; + } else { + head->next = removeElements(head->next, val); + return head; + } + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) ## 其他语言版本 @@ -224,9 +250,10 @@ struct ListNode* removeElements(struct ListNode* head, int val){ ### Java: +用原来的链表操作: ```java /** - * 添加虚节点方式 + * 方法1 * 时间复杂度 O(n) * 空间复杂度 O(1) * @param head @@ -234,25 +261,22 @@ struct ListNode* removeElements(struct ListNode* head, int val){ * @return */ public ListNode removeElements(ListNode head, int val) { - if (head == null) { - return head; + while(head!=null && head.val==val) { + head = head.next; } - // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作 - ListNode dummy = new ListNode(-1, head); - ListNode pre = dummy; - ListNode cur = head; - while (cur != null) { - if (cur.val == val) { - pre.next = cur.next; + ListNode curr = head; + while(curr!=null && curr.next !=null) { + if(curr.next.val == val){ + curr.next = curr.next.next; } else { - pre = cur; + curr = curr.next; } - cur = cur.next; } - return dummy.next; + return head; } + /** - * 不添加虚拟节点方式 + * 方法1 * 时间复杂度 O(n) * 空间复杂度 O(1) * @param head @@ -280,8 +304,13 @@ public ListNode removeElements(ListNode head, int val) { } return head; } + +``` + +设置一个虚拟头结点: + +```java /** - * 不添加虚拟节点and pre Node方式 * 时间复杂度 O(n) * 空间复杂度 O(1) * @param head @@ -289,17 +318,51 @@ public ListNode removeElements(ListNode head, int val) { * @return */ public ListNode removeElements(ListNode head, int val) { - while(head!=null && head.val==val){ - head = head.next; + // 设置一个虚拟的头结点 + ListNode dummy = new ListNode(); + dummy.next = head; + + ListNode cur = dummy; + while (cur.next != null) { + if (cur.next.val == val) { + cur.next = cur.next.next; + } else { + cur = cur.next; + } } - ListNode curr = head; - while(curr!=null){ - while(curr.next!=null && curr.next.val == val){ - curr.next = curr.next.next; + return dummy.next; +} + +``` + +递归 + +```java +/** + * 时间复杂度 O(n) + * 空间复杂度 O(n) + * @param head + * @param val + * @return + */ +class Solution { + public ListNode removeElements(ListNode head, int val) { + if (head == null) { + return head; } - curr = curr.next; + + // 假设 removeElements() 返回后面完整的已经去掉val节点的子链表 + // 在当前递归层用当前节点接住后面的子链表 + // 随后判断当前层的node是否需要被删除,如果是,就返回 + // 也可以先判断是否需要删除当前node,但是这样条件语句会比较不好想 + head.next = removeElements(head.next, val); + if (head.val == val) { + return head.next; + } + return head; + + // 实际上就是还原一个从尾部开始重新构建链表的过程 } - return head; } ``` @@ -497,27 +560,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 +648,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; @@ -663,9 +766,43 @@ public class Solution } } ``` +### Ruby# + +```ruby +# 定义链表节点 +class ListNode + attr_accessor :val, :next + def initialize(val = 0, _next = nil) + @val = val + @next = _next + end +end + +# 删除链表中值为 val 的节点 +def remove_elements(head, val) + # 创建一个虚拟头节点,这样可以简化删除头节点的处理 + # 虚拟头节点的值为 0,指向当前链表的头节点 + dummy = ListNode.new(0) + dummy.next = head + + # 初始化当前节点为虚拟头节点 + current = dummy + + # 遍历链表,直到当前节点的下一个节点为空 + while current.next + # 如果当前节点的下一个节点的值等于 val + if current.next.val == val + # 跳过该节点,即将当前节点的 next 指向下一个节点的 next + current.next = current.next.next + else + # 否则继续遍历,当前节点向前移动 + current = current.next + end + end + + # 返回删除 val 后的新链表的头节点,虚拟头节点的 next 就是新的头节点 + dummy.next +end -

- - - +``` diff --git "a/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" "b/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index e07ab746d9..ba255e0685 --- "a/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 205. 同构字符串 @@ -179,8 +177,4 @@ function isIsomorphic(s: string, t: string): boolean { -

- - - diff --git "a/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index 5a57939af2..e49037dd2b --- "a/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" +++ "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 @@ -29,7 +27,7 @@ 其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示: -![206_反转链表](https://code-thinking-1253855093.file.myqcloud.com/pics/20210218090901207.png) +![206_反转链表](https://file1.kamacoder.com/i/algo/20210218090901207.png) 之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。 @@ -37,7 +35,7 @@ 我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur) -![](https://code-thinking.cdn.bcebos.com/gifs/206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.gif) +![](https://file1.kamacoder.com/i/algo/206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.gif) 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。 @@ -739,7 +737,3 @@ public ListNode reverseList(ListNode head) { > 采用这种方法需要注意一点。就是当整个出栈循环结束以后,cur正好指向原来链表的第一个结点,而此时结点1中的next指向的是结点2,因此最后还需要`cur.next = null` ![image-20230117195418626](https://raw.githubusercontent.com/liyuxuan7762/MyImageOSS/master/md_images/image-20230117195418626.png) -

- - - diff --git "a/problems/0207.\350\257\276\347\250\213\350\241\250.md" "b/problems/0207.\350\257\276\347\250\213\350\241\250.md" new file mode 100644 index 0000000000..f992c72b89 --- /dev/null +++ "b/problems/0207.\350\257\276\347\250\213\350\241\250.md" @@ -0,0 +1,59 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +拓扑排序指的是一种 解决问题的大体思路, 而具体算法,可能是 广搜 可能是深搜。 + +大家可能发现 各式各样的解法,纠结哪个是拓扑排序? + +只要能在把 有向无环图 进行线性排序 的算法 都可以叫做 拓扑排序。 + +引用与任务调度,课程安排等等。 + + +「拓扑排序」是专门应用于有向图的算法; + +把一个 有向无环图 转成 线性的排序 就叫 拓扑排序。 + +拓扑排序(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.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" old mode 100644 new mode 100755 index 5934d5e364..c8440c4a67 --- "a/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" +++ "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 209.长度最小的子数组 @@ -68,7 +66,7 @@ public: 接下来就开始介绍数组操作中另一个重要的方法:**滑动窗口**。 -所谓滑动窗口,**就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果**。 +所谓滑动窗口,**就是不断的调节子序列的起始位置和终止位置,从而得出我们想要的结果**。 在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。 @@ -86,7 +84,7 @@ public: 这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程: -![209.长度最小的子数组](https://code-thinking.cdn.bcebos.com/gifs/209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.gif) +![209.长度最小的子数组](https://file1.kamacoder.com/i/algo/209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.gif) 最后找到 4,3 是最短距离。 @@ -106,7 +104,7 @@ public: 解题的关键在于 窗口的起始位置如何移动,如图所示: -![leetcode_209](https://code-thinking-1253855093.file.myqcloud.com/pics/20210312160441942.png) +![leetcode_209](https://file1.kamacoder.com/i/algo/20210312160441942.png) 可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。** @@ -266,26 +264,25 @@ var minSubArrayLen = function(target, nums) { }; ``` -### Typescript: +### TypeScript: ```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: @@ -559,7 +556,3 @@ public class Solution { } } ``` -

- - - diff --git "a/problems/0210.\350\257\276\347\250\213\350\241\250II.md" "b/problems/0210.\350\257\276\347\250\213\350\241\250II.md" new file mode 100644 index 0000000000..b0d9fe8a9e --- /dev/null +++ "b/problems/0210.\350\257\276\347\250\213\350\241\250II.md" @@ -0,0 +1,42 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +```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.\346\211\223\345\256\266\345\212\253\350\210\215II.md" "b/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" old mode 100644 new mode 100755 index 385c58675e..6f2fdd0610 --- "a/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" +++ "b/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 213.打家劫舍II @@ -44,15 +42,15 @@ * 情况一:考虑不包含首尾元素 -![213.打家劫舍II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160748643-20230310134000692.jpg) +![213.打家劫舍II](https://file1.kamacoder.com/i/algo/20210129160748643-20230310134000692.jpg) * 情况二:考虑包含首元素,不包含尾元素 -![213.打家劫舍II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160821374-20230310134003961.jpg) +![213.打家劫舍II1](https://file1.kamacoder.com/i/algo/20210129160821374-20230310134003961.jpg) * 情况三:考虑包含尾元素,不包含首元素 -![213.打家劫舍II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160842491-20230310134008133.jpg) +![213.打家劫舍II2](https://file1.kamacoder.com/i/algo/20210129160842491-20230310134008133.jpg) **注意我这里用的是"考虑"**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。 @@ -308,6 +306,34 @@ function robRange(nums: number[], start: number, end: number): number { } ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// 198.打家劫舍的逻辑 +int robRange(int* nums, int start, int end, int numsSize) { + if (end == start) return nums[start]; + int dp[numsSize]; + dp[start] = nums[start]; + dp[start + 1] = max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + } + return dp[end]; +} + +int rob(int* nums, int numsSize) { + if (numsSize == 0) return 0; + if (numsSize == 1) return nums[0]; + int result1 = robRange(nums, 0, numsSize - 2, numsSize); // 情况二 + int result2 = robRange(nums, 1, numsSize - 1, numsSize); // 情况三 + return max(result1, result2); +} +``` + + + ### Rust: ```rust @@ -339,8 +365,3 @@ impl Solution { ``` -

- - - - diff --git "a/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" old mode 100644 new mode 100755 index ac28f9fcfa..5ef5b5e67c --- "a/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" +++ "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -47,7 +45,7 @@ 选取过程如图: -![216.组合总和III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195717975.png) +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201123195717975.png) 图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。 @@ -110,7 +108,7 @@ if (path.size() == k) { 本题和[77. 组合](https://programmercarl.com/0077.组合.html)区别之一就是集合固定的就是9个数[1,...,9],所以for循环固定i<=9 如图: -![216.组合总和III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123195717975-20230310113546003.png) +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201123195717975-20230310113546003.png) 处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。 @@ -168,7 +166,7 @@ public: 这道题目,剪枝操作其实是很容易想到了,想必大家看上面的树形图的时候已经想到了。 如图: -![216.组合总和III1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020112319580476.png) +![216.组合总和III1](https://file1.kamacoder.com/i/algo/2020112319580476.png) 已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。 @@ -367,7 +365,7 @@ class Solution: def backtracking(self, targetSum, k, currentSum, startIndex, path, result): if currentSum > targetSum: # 剪枝操作 - return # 如果path的长度等于k但currentSum不等于targetSum,则直接返回 + return # 如果currentSum已经超过targetSum,则直接返回 if len(path) == k: if currentSum == targetSum: result.append(path[:]) @@ -417,6 +415,7 @@ func dfs(k, n int, start int, sum int) { ``` ### JavaScript +- 未剪枝: ```js /** @@ -424,32 +423,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; }; ``` @@ -696,7 +737,3 @@ public class Solution -

- - - diff --git "a/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" "b/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" old mode 100644 new mode 100755 index d93d2a3381..eaf4eab2c9 --- "a/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" +++ "b/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 222.完全二叉树的节点个数 @@ -54,7 +52,7 @@ 1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。 代码如下: -``` +```CPP int getNodesNum(TreeNode* cur) { ``` @@ -62,7 +60,7 @@ int getNodesNum(TreeNode* cur) { 代码如下: -``` +```CPP if (cur == NULL) return 0; ``` @@ -70,7 +68,7 @@ if (cur == NULL) return 0; 代码如下: -``` +```CPP int leftNum = getNodesNum(cur->left); // 左 int rightNum = getNodesNum(cur->right); // 右 int treeNum = leftNum + rightNum + 1; // 中 @@ -155,7 +153,7 @@ public: 我来举一个典型的例子如题: - + 完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。 @@ -164,10 +162,10 @@ public: 对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。 完全二叉树(一)如图: -![222.完全二叉树的节点个数](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124092543662.png) +![222.完全二叉树的节点个数](https://file1.kamacoder.com/i/algo/20201124092543662.png) 完全二叉树(二)如图: -![222.完全二叉树的节点个数1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124092634138.png) +![222.完全二叉树的节点个数1](https://file1.kamacoder.com/i/algo/20201124092634138.png) 可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。 @@ -175,15 +173,15 @@ public: 在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829163554.png) +![](https://file1.kamacoder.com/i/algo/20220829163554.png) 在完全二叉树中,如果递归向左遍历的深度不等于递归向右遍历的深度,则说明不是满二叉树,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829163709.png) +![](https://file1.kamacoder.com/i/algo/20220829163709.png) 那有录友说了,这种情况,递归向左遍历的深度等于递归向右遍历的深度,但也不是满二叉树,如题: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829163811.png) +![](https://file1.kamacoder.com/i/algo/20220829163811.png) 如果这么想,大家就是对 完全二叉树理解有误区了,**以上这棵二叉树,它根本就不是一个完全二叉树**! @@ -893,8 +891,4 @@ public int CountNodes(TreeNode root) } ``` -

- - - diff --git "a/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" "b/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" old mode 100644 new mode 100755 index 6900e66869..72dfd2aacf --- "a/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" +++ "b/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -62,7 +60,7 @@ queue.pop(); queue.empty(); ``` -![225.用队列实现栈](https://code-thinking.cdn.bcebos.com/gifs/225.用队列实现栈.gif) +![225.用队列实现栈](https://file1.kamacoder.com/i/algo/225.用队列实现栈.gif) 详细如代码注释所示: @@ -72,6 +70,7 @@ class MyStack { public: queue que1; queue que2; // 辅助队列,用来备份 + /** Initialize your data structure here. */ MyStack() { @@ -100,9 +99,28 @@ public: return result; } - /** Get the top element. */ - int top() { - return que1.back(); + /** Get the top element. + ** Can not use back() direactly. + */ + int top(){ + int size = que1.size(); + size--; + while (size--){ + // 将que1 导入que2,但要留下最后一个元素 + que2.push(que1.front()); + que1.pop(); + } + + int result = que1.front(); // 留下的最后一个元素就是要回返的值 + que2.push(que1.front()); // 获取值后将最后一个元素也加入que2中,保持原本的结构不变 + que1.pop(); + + que1 = que2; // 再将que2赋值给que1 + while (!que2.empty()){ + // 清空que2 + que2.pop(); + } + return result; } /** Returns whether the stack is empty. */ @@ -111,7 +129,7 @@ public: } }; ``` -* 时间复杂度: pop为O(n),其他为O(1) +* 时间复杂度: pop为O(n),top为O(n),其他为O(1) * 空间复杂度: O(n) ## 优化 @@ -126,15 +144,15 @@ C++优化代码 class MyStack { public: queue que; - /** Initialize your data structure here. */ + MyStack() { } - /** Push element x onto stack. */ + void push(int x) { que.push(x); } - /** Removes the element on top of the stack and returns that element. */ + int pop() { int size = que.size(); size--; @@ -147,18 +165,26 @@ public: return result; } - /** Get the top element. */ - int top() { - return que.back(); + int top(){ + int size = que.size(); + size--; + while (size--){ + // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部 + que.push(que.front()); + que.pop(); + } + int result = que.front(); // 此时获得的元素就是栈顶的元素了 + que.push(que.front()); // 将获取完的元素也重新添加到队列尾部,保证数据结构没有变化 + que.pop(); + return result; } - /** Returns whether the stack is empty. */ bool empty() { return que.empty(); } }; ``` -* 时间复杂度: pop为O(n),其他为O(1) +* 时间复杂度: pop为O(n),top为O(n),其他为O(1) * 空间复杂度: O(n) @@ -1249,8 +1275,93 @@ impl MyStack { } ``` -

- - - +### C: + +> C:单队列 + +```c +typedef struct Node { + int val; + struct Node *next; +} Node_t; + +// 用单向链表实现queue +typedef struct { + Node_t *head; + Node_t *foot; + int size; +} MyStack; + +MyStack* myStackCreate() { + MyStack *obj = (MyStack *)malloc(sizeof(MyStack)); + assert(obj); + obj->head = NULL; + obj->foot = NULL; + obj->size = 0; + return obj; +} + +void myStackPush(MyStack* obj, int x) { + + Node_t *temp = (Node_t *)malloc(sizeof(Node_t)); + assert(temp); + temp->val = x; + temp->next = NULL; + + // 添加至queue末尾 + if (obj->foot) { + obj->foot->next = temp; + } else { + obj->head = temp; + } + obj->foot = temp; + obj->size++; +} + +int myStackPop(MyStack* obj) { + + // 获取末尾元素 + int target = obj->foot->val; + + if (obj->head == obj->foot) { + free(obj->foot); + obj->head = NULL; + obj->foot = NULL; + } else { + + Node_t *prev = obj->head; + // 移动至queue尾部节点前一个节点 + while (prev->next != obj->foot) { + prev = prev->next; + } + + free(obj->foot); + obj->foot = prev; + obj->foot->next = NULL; + } + + obj->size--; + return target; +} + +int myStackTop(MyStack* obj) { + return obj->foot->val; +} + +bool myStackEmpty(MyStack* obj) { + return obj->size == 0; +} + +void myStackFree(MyStack* obj) { + Node_t *curr = obj->head; + while (curr != NULL) { + Node_t *temp = curr->next; + free(curr); + curr = temp; + } + free(obj); +} + +``` + diff --git "a/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" "b/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 8691953a3e..67a1a59338 --- "a/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 226.翻转二叉树 @@ -12,9 +10,9 @@ 翻转一棵二叉树。 -![226.翻转二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203192644329.png) +![226.翻转二叉树](https://file1.kamacoder.com/i/algo/20210203192644329.png) -这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,权当一个乐子哈) +这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,全当一个乐子哈) ## 算法公开课 @@ -37,7 +35,7 @@ 如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图: -![226.翻转二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210203192724351.png) +![226.翻转二叉树1](https://file1.kamacoder.com/i/algo/20210203192724351.png) 可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。 @@ -57,7 +55,7 @@ 我们下文以前序遍历为例,通过动画来看一下翻转的过程: -![翻转二叉树](https://code-thinking.cdn.bcebos.com/gifs/%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91.gif) +![翻转二叉树](https://file1.kamacoder.com/i/algo/%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91.gif) 我们来看一下递归三部曲: @@ -81,7 +79,7 @@ if (root == NULL) return root; 3. 确定单层递归的逻辑 -因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。 +因为是前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。 ```cpp swap(root->left, root->right); @@ -348,14 +346,13 @@ class Solution: while stack: node = stack.pop() node.left, node.right = node.right, node.left + if node.right: + stack.append(node.right) if node.left: stack.append(node.left) - if node.right: - stack.append(node.right) return root ``` - 递归法:中序遍历: ```python # Definition for a binary tree node. @@ -374,7 +371,7 @@ class Solution: return root ``` -迭代法:中序遍历: +迭代法,伪中序遍历(结果是对的,看起来像是中序遍历,实际上它是前序遍历,只不过把中间节点处理逻辑放到了中间。还是要用'统一写法'才是真正的中序遍历): ```python # Definition for a binary tree node. # class TreeNode: @@ -386,18 +383,17 @@ class Solution: def invertTree(self, root: TreeNode) -> TreeNode: if not root: return None - stack = [root] + stack = [root] while stack: - node = stack.pop() - if node.left: - stack.append(node.left) - node.left, node.right = node.right, node.left - if node.left: - stack.append(node.left) + node = stack.pop() + if node.right: + stack.append(node.right) + node.left, node.right = node.right, node.left # 放到中间,依然是前序遍历 + if node.right: + stack.append(node.right) return root ``` - 递归法:后序遍历: ```python # Definition for a binary tree node. @@ -416,7 +412,7 @@ class Solution: return root ``` -迭代法:后序遍历: +迭代法,伪后序遍历(结果是对的,看起来像是后序遍历,实际上它是前序遍历,只不过把中间节点处理逻辑放到了最后。还是要用'统一写法'才是真正的后序遍历): ```python # Definition for a binary tree node. # class TreeNode: @@ -427,23 +423,19 @@ class Solution: class Solution: def invertTree(self, root: TreeNode) -> TreeNode: if not root: - return None - stack = [root] + return None + stack = [root] while stack: - node = stack.pop() + node = stack.pop() + if node.right: + stack.append(node.right) if node.left: stack.append(node.left) - if node.right: - stack.append(node.right) node.left, node.right = node.right, node.left return root ``` - - - - 迭代法:广度优先遍历(层序遍历): ```python # Definition for a binary tree node. @@ -459,11 +451,10 @@ class Solution: queue = collections.deque([root]) while queue: - for i in range(len(queue)): - node = queue.popleft() - node.left, node.right = node.right, node.left - if node.left: queue.append(node.left) - if node.right: queue.append(node.right) + node = queue.popleft() + node.left, node.right = node.right, node.left + if node.left: queue.append(node.left) + if node.right: queue.append(node.right) return root ``` @@ -1028,8 +1019,4 @@ public TreeNode InvertTree(TreeNode root) { } ``` -

- - - diff --git "a/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" old mode 100644 new mode 100755 index 41933ca4ba..56698e023f --- "a/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" +++ "b/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 @@ -44,7 +42,7 @@ queue.empty(); // 返回 false 这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 -使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 +使用栈来模拟队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 下面动画模拟以下队列的执行过程: @@ -59,7 +57,7 @@ queue.pop();**注意此时的输出栈的操作** queue.pop(); queue.empty(); -![232.用栈实现队列版本2](https://code-thinking.cdn.bcebos.com/gifs/232.用栈实现队列版本2.gif) +![232.用栈实现队列版本2](https://file1.kamacoder.com/i/algo/232.用栈实现队列版本2.gif) 在push数据的时候,只要数据放进输入栈就好,**但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入)**,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。 @@ -113,7 +111,7 @@ public: ``` -* 时间复杂度: push和empty为O(1), pop和peek为O(n) +* 时间复杂度: 都为O(1)。pop和peek看起来像O(n),实际上一个循环n会被使用n次,最后还是O(1)。 * 空间复杂度: O(n) @@ -691,7 +689,3 @@ impl MyQueue { } ``` -

- - - diff --git "a/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index fef942fc49..6248861d94 --- "a/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" +++ "b/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 234.回文链表 @@ -89,7 +87,7 @@ public: 如图所示: - + 代码如下: @@ -428,8 +426,4 @@ function reverseList(head: ListNode | null): ListNode | null { -

- - - diff --git "a/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" "b/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" old mode 100644 new mode 100755 index 2a11f9f4b7..a1fe78d169 --- "a/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" +++ "b/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 235. 二叉搜索树的最近公共祖先 @@ -16,7 +14,7 @@ 例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5] -![235. 二叉搜索树的最近公共祖先](https://code-thinking-1253855093.file.myqcloud.com/pics/20201018172243602.png) +![235. 二叉搜索树的最近公共祖先](https://file1.kamacoder.com/i/algo/20201018172243602.png) 示例 1: @@ -38,7 +36,7 @@ ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先](https://www.bilibili.com/video/BV1Zt4y1F7ww?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先](https://www.bilibili.com/video/BV1Zt4y1F7ww?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -54,7 +52,7 @@ 如图,我们从根节点搜索,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。 -![235.二叉搜索树的最近公共祖先](https://code-thinking-1253855093.file.myqcloud.com/pics/20220926164214.png) +![235.二叉搜索树的最近公共祖先](https://file1.kamacoder.com/i/algo/20220926164214.png) 此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。 @@ -66,7 +64,7 @@ 如图所示:p为节点6,q为节点9 -![235.二叉搜索树的最近公共祖先2](https://code-thinking-1253855093.file.myqcloud.com/pics/20220926165141.png) +![235.二叉搜索树的最近公共祖先2](https://file1.kamacoder.com/i/algo/20220926165141.png) 可以看出直接按照指定的方向,就可以找到节点8,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回! @@ -99,7 +97,7 @@ if (cur == NULL) return cur; * 确定单层递归的逻辑 -在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭) +在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭右闭) 那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。 @@ -547,8 +545,4 @@ public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) ``` -

- - - diff --git "a/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" "b/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" old mode 100644 new mode 100755 index 049f70c7ae..5044e3ba00 --- "a/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" +++ "b/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 本来是打算将二叉树和二叉搜索树的公共祖先问题一起讲,后来发现篇幅过长了,只能先说一说二叉树的公共祖先问题。 @@ -18,7 +16,7 @@ 例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4] -![236. 二叉树的最近公共祖先](https://code-thinking-1253855093.file.myqcloud.com/pics/20201016173414722.png) +![236. 二叉树的最近公共祖先](https://file1.kamacoder.com/i/algo/20201016173414722.png) 示例 1: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 @@ -36,7 +34,7 @@ ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[自底向上查找,有点难度! | LeetCode:236. 二叉树的最近公共祖先](https://www.bilibili.com/video/BV1jd4y1B7E2),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[自底向上查找,有点难度! | LeetCode:236. 二叉树的最近公共祖先](https://www.bilibili.com/video/BV1jd4y1B7E2),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -45,7 +43,7 @@ 那么二叉树如何可以自底向上查找呢? -回溯啊,二叉树回溯的过程就是从低到上。 +回溯啊,二叉树回溯的过程就是从底到上。 后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。 @@ -53,7 +51,7 @@ **首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** 即情况一: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220922173502.png) +![](https://file1.kamacoder.com/i/algo/20220922173502.png) 判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。 @@ -63,7 +61,7 @@ **但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。** 情况二: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220922173530.png) +![](https://file1.kamacoder.com/i/algo/20220922173530.png) 其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。 @@ -131,7 +129,7 @@ left与right的逻辑处理; // 中 如图: -![236.二叉树的最近公共祖先](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020415105872.png) +![236.二叉树的最近公共祖先](https://file1.kamacoder.com/i/algo/2021020415105872.png) 就像图中一样直接返回7。 @@ -164,7 +162,7 @@ TreeNode* right = lowestCommonAncestor(root->right, p, q); 如图: -![236.二叉树的最近公共祖先1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204151125844.png) +![236.二叉树的最近公共祖先1](https://file1.kamacoder.com/i/algo/20210204151125844.png) 图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去! @@ -185,7 +183,7 @@ else { // (left == NULL && right == NULL) 那么寻找最小公共祖先,完整流程图如下: -![236.二叉树的最近公共祖先2](https://code-thinking-1253855093.file.myqcloud.com/pics/202102041512582.png) +![236.二叉树的最近公共祖先2](https://file1.kamacoder.com/i/algo/202102041512582.png) **从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!** @@ -247,7 +245,7 @@ public: ### Java - +递归 ```Java class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { @@ -271,6 +269,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 @@ -413,7 +452,11 @@ impl Solution { p: Option>>, q: Option>>, ) -> Option>> { - if root == p || root == q || root.is_none() { + if root.is_none() { + return root; + } + if Rc::ptr_eq(root.as_ref().unwrap(), p.as_ref().unwrap()) + || Rc::ptr_eq(root.as_ref().unwrap(), q.as_ref().unwrap()) { return root; } let left = Self::lowest_common_ancestor( @@ -445,8 +488,4 @@ public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) } ``` -

- - - diff --git "a/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" "b/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" old mode 100644 new mode 100755 index 19ac12619e..5ea810104d --- "a/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" +++ "b/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -20,7 +18,7 @@ 你能在线性时间复杂度内解决此题吗? - + 提示: @@ -84,7 +82,7 @@ public: 动画如下: -![239.滑动窗口最大值](https://code-thinking.cdn.bcebos.com/gifs/239.滑动窗口最大值.gif) +![239.滑动窗口最大值](https://file1.kamacoder.com/i/algo/239.滑动窗口最大值.gif) 对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。 @@ -100,7 +98,7 @@ public: 为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下: -![239.滑动窗口最大值-2](https://code-thinking.cdn.bcebos.com/gifs/239.滑动窗口最大值-2.gif) +![239.滑动窗口最大值-2](https://file1.kamacoder.com/i/algo/239.滑动窗口最大值-2.gif) 那么我们用什么数据结构来实现这个单调队列呢? @@ -267,7 +265,7 @@ class Solution { //利用双端队列手动实现单调队列 /** * 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可 - * 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标) + * 单调递减队列类似 (head -->) 3 --> 2 --> 1 --> 0 (--> tail) (左边为头结点,元素存的是下标) */ class Solution { public int[] maxSlidingWindow(int[] nums, int k) { @@ -281,7 +279,7 @@ class Solution { while(!deque.isEmpty() && deque.peek() < i - k + 1){ deque.poll(); } - // 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出 + // 2.维护单调递减队列:新元素若大于队尾元素,则弹出队尾元素,直到满足单调性 while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { deque.pollLast(); } @@ -299,7 +297,7 @@ class Solution { ``` ### Python: - +#### 解法一:使用自定义的单调队列类 ```python from collections import deque @@ -339,6 +337,35 @@ class Solution: return result ``` + +#### 解法二:直接用单调队列 +```python +from collections import deque +class Solution: + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + max_list = [] # 结果集合 + kept_nums = deque() # 单调队列 + + for i in range(len(nums)): + update_kept_nums(kept_nums, nums[i]) # 右侧新元素加入 + + if i >= k and nums[i - k] == kept_nums[0]: # 左侧旧元素如果等于单调队列头元素,需要移除头元素 + kept_nums.popleft() + + if i >= k - 1: + max_list.append(kept_nums[0]) + + return max_list + +def update_kept_nums(kept_nums, num): # num 是新加入的元素 + # 所有小于新元素的队列尾部元素,在新元素出现后,都是没有价值的,都需要被移除 + while kept_nums and num > kept_nums[-1]: + kept_nums.pop() + + kept_nums.append(num) + +``` + ### Go: ```go @@ -401,7 +428,7 @@ func maxSlidingWindow(nums []int, k int) []int { } ``` -### Javascript: +### JavaScript: ```javascript /** @@ -861,7 +888,36 @@ public: }; ``` -

- - - +### C + +```c +int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize) { + *returnSize = numsSize - k + 1; + int *res = (int*)malloc((*returnSize) * sizeof(int)); + assert(res); + int *deque = (int*)malloc(numsSize * sizeof(int)); + assert(deque); + int front = 0, rear = 0, idx = 0; + + for (int i = 0 ; i < numsSize ; i++) { + while (front < rear && deque[front] <= i - k) { + front++; + } + + while (front < rear && nums[deque[rear - 1]] <= nums[i]) { + rear--; + } + + deque[rear++] = i; + + if (i >= k - 1) { + res[idx++] = nums[deque[front]]; + } + } + + return res; +} + +``` + + diff --git "a/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" "b/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" old mode 100644 new mode 100755 index 6eed90a73a..0a37ea26cc --- "a/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" +++ "b/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 数组就是简单的哈希表,但是数组的大小可不是无限开辟的 @@ -44,7 +42,7 @@ 操作动画如下: -![242.有效的字母异位词](https://code-thinking.cdn.bcebos.com/gifs/242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.gif) +![242.有效的字母异位词](https://file1.kamacoder.com/i/algo/242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.gif) 定义一个数组叫做record用来上记录字符串s里字符出现的次数。 @@ -383,6 +381,31 @@ object Solution { } ``` +### C + +```c +bool isAnagram(char* s, char* t) { + int len1 = strlen(s), len2 = strlen(t); + if (len1 != len2) { + return false; + } + + int map1[26] = {0}, map2[26] = {0}; + for (int i = 0; i < len1; i++) { + map1[s[i] - 'a'] += 1; + map2[t[i] - 'a'] += 1; + } + + for (int i = 0; i < 26; i++) { + if (map1[i] != map2[i]) { + return false; + } + } + + return true; +} +``` + ## 相关题目 * [383.赎金信](https://programmercarl.com/0383.%E8%B5%8E%E9%87%91%E4%BF%A1.html) @@ -390,8 +413,4 @@ object Solution { * [438.找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) -

- - - diff --git "a/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" "b/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" old mode 100644 new mode 100755 index 4c6c92c5b4..4a66c816bc --- "a/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" +++ "b/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 以为只用了递归,其实还用了回溯 @@ -16,7 +14,7 @@ 说明: 叶子节点是指没有子节点的节点。 示例: -![257.二叉树的所有路径1](https://code-thinking-1253855093.file.myqcloud.com/pics/2021020415161576.png) +![257.二叉树的所有路径1](https://file1.kamacoder.com/i/algo/2021020415161576.png) ## 算法公开课 @@ -30,7 +28,7 @@ 前序遍历以及回溯的过程如图: -![257.二叉树的所有路径](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204151702443.png) +![257.二叉树的所有路径](https://file1.kamacoder.com/i/algo/20210204151702443.png) 我们先使用递归的方式,来做前序遍历。**要知道递归和回溯就是一家的,本题也需要回溯。** @@ -40,7 +38,7 @@ 要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下: -``` +```CPP void traversal(TreeNode* cur, vector& path, vector& result) ``` @@ -48,7 +46,7 @@ void traversal(TreeNode* cur, vector& path, vector& result) 在写递归的时候都习惯了这么写: -``` +```CPP if (cur == NULL) { 终止处理逻辑 } @@ -59,7 +57,7 @@ if (cur == NULL) { **那么什么时候算是找到了叶子节点?** 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。 所以本题的终止条件是: -``` +```CPP if (cur->left == NULL && cur->right == NULL) { 终止处理逻辑 } @@ -69,15 +67,15 @@ if (cur->left == NULL && cur->right == NULL) { 再来看一下终止处理的逻辑。 -这里使用vector 结构path来记录路径,所以要把vector 结构的path转为string格式,再把这个string 放进 result里。 +这里使用`vector` 结构path来记录路径,所以要把`vector` 结构的path转为string格式,再把这个string 放进 result里。 -**那么为什么使用了vector 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 +**那么为什么使用了`vector` 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 可能有的同学问了,我看有些人的代码也没有回溯啊。 **其实是有回溯的,只不过隐藏在函数调用时的参数赋值里**,下文我还会提到。 -这里我们先使用vector结构的path容器来记录路径,那么终止处理逻辑如下: +这里我们先使用`vector`结构的path容器来记录路径,那么终止处理逻辑如下: ```CPP if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 @@ -102,7 +100,7 @@ if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 所以递归前要加上判断语句,下面要递归的节点是否为空,如下 -``` +```CPP if (cur->left) { traversal(cur->left, path, result); } @@ -317,7 +315,7 @@ public: 其实关键还在于 参数,使用的是 `string path`,这里并没有加上引用`&` ,即本层递归中,path + 该节点数值,但该层递归结束,上一层path的数值并不会受到任何影响。 如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220831173322.png) +![](https://file1.kamacoder.com/i/algo/20220831173322.png) 节点4 的path,在遍历到节点3,path+3,遍历节点3的递归结束之后,返回节点4(回溯的过程),path并不会把3加上。 @@ -938,7 +936,3 @@ public void Traversal(TreeNode node, List path, List res) } ``` -

- - - diff --git "a/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" "b/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" old mode 100644 new mode 100755 index a0e62d4800..7c5d7c9c9f --- "a/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" +++ "b/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 279.完全平方数 @@ -95,7 +93,7 @@ for (int i = 0; i <= n; i++) { // 遍历背包 已输入n为5例,dp状态图如下: -![279.完全平方数](https://code-thinking-1253855093.file.myqcloud.com/pics/20210202112617341.jpg) +![279.完全平方数](https://file1.kamacoder.com/i/algo/20210202112617341.jpg) dp[0] = 0 dp[1] = min(dp[0] + 1) = 1 @@ -219,7 +217,7 @@ class Solution { ### Python: -先遍历物品, 再遍历背包 +先遍历背包, 再遍历物品 ```python class Solution: def numSquares(self, n: int) -> int: @@ -234,7 +232,7 @@ class Solution: return dp[n] ``` -先遍历背包, 再遍历物品 +先遍历物品, 再遍历背包 ```python class Solution: def numSquares(self, n: int) -> int: @@ -271,7 +269,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: @@ -326,7 +344,7 @@ func min(a, b int) int { } ``` -### Javascript: +### JavaScript: ```Javascript // 先遍历物品,再遍历背包 @@ -389,6 +407,30 @@ function numSquares(n: number): number { }; ``` +### C + +```c +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int numSquares(int n) { + int* dp = (int*)malloc(sizeof(int) * (n + 1)); + for (int j = 0; j < n + 1; j++) { + dp[j] = INT_MAX; + } + dp[0] = 0; + // 遍历背包 + for (int i = 0; i <= n; ++i) { + // 遍历物品 + for (int j = 1; j * j <= i; ++j) { + dp[i] = min(dp[i - j * j] + 1, dp[i]); + } + } + return dp[n]; +} +``` + + + ### Rust: ```rust @@ -435,8 +477,3 @@ impl Solution { ``` -

- - - - diff --git "a/problems/0283.\347\247\273\345\212\250\351\233\266.md" "b/problems/0283.\347\247\273\345\212\250\351\233\266.md" old mode 100644 new mode 100755 index fc708844bd..e25525684e --- "a/problems/0283.\347\247\273\345\212\250\351\233\266.md" +++ "b/problems/0283.\347\247\273\345\212\250\351\233\266.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 283. 移动零:动态规划:一样的套路,再求一次完全平方数 @@ -34,7 +32,7 @@ 如动画所示: -![移动零](https://code-thinking.cdn.bcebos.com/gifs/283.%E7%A7%BB%E5%8A%A8%E9%9B%B6.gif) +![移动零](https://file1.kamacoder.com/i/algo/283.%E7%A7%BB%E5%8A%A8%E9%9B%B6.gif) C++代码如下: @@ -169,11 +167,20 @@ void moveZeroes(int* nums, int numsSize){ } ``` +### Rust +```rust +impl Solution { + pub fn move_zeroes(nums: &mut Vec) { + let mut slow = 0; + for fast in 0..nums.len() { + if nums[fast] != 0 { + nums.swap(slow, fast); + slow += 1; + } + } + } +} +``` -

- - - - diff --git "a/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" "b/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 64f75291f5..06adfd950d --- "a/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 300.最长递增子序列 @@ -41,7 +39,7 @@ 首先通过本题大家要明确什么是子序列,“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。 本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。 -子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下表j的子序列长度有关系,那又是什么样的关系呢。 +子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下标j的子序列长度有关系,那又是什么样的关系呢。 接下来,我们依然用动规五部曲来详细分析一波: @@ -87,7 +85,7 @@ for (int i = 1; i < nums.size(); i++) { 输入:[0,1,0,3,2],dp数组的变化如下: -![300.最长上升子序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110170945618.jpg) +![300.最长上升子序列](https://file1.kamacoder.com/i/algo/20210110170945618.jpg) 如果代码写出来,但一直AC不了,那么就把dp数组打印出来,看看对不对! @@ -129,6 +127,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 +136,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; } @@ -247,7 +246,7 @@ func lengthOfLIS(nums []int ) int { } ``` -### Javascript: +### JavaScript: ```javascript const lengthOfLIS = (nums) => { @@ -288,6 +287,36 @@ function lengthOfLIS(nums: number[]): number { }; ``` +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int lengthOfLIS(int* nums, int numsSize) { + if(numsSize <= 1){ + return numsSize; + } + int dp[numsSize]; + for(int i = 0; i < numsSize; i++){ + dp[i]=1; + } + int result = 1; + for (int i = 1; i < numsSize; ++i) { + for (int j = 0; j < i; ++j) { + if(nums[i] > nums[j]){ + dp[i] = max(dp[i], dp[j] + 1); + } + if(dp[i] > result){ + result = dp[i]; + } + } + } + return result; +} +``` + + + ### Rust: ```rust @@ -306,9 +335,27 @@ pub fn length_of_lis(nums: Vec) -> i32 { } ``` +### Cangjie: + +```cangjie +func lengthOfLIS(nums: Array): Int64 { + let n = nums.size + if (n <= 1) { + return n + } + + let dp = Array(n, item: 1) + var res = 0 + for (i in 1..n) { + for (j in 0..i) { + if (nums[i] > nums[j]) { + dp[i] = max(dp[i], dp[j] + 1) + } + } + res = max(dp[i], res) + } + return res +} +``` -

- - - diff --git "a/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" "b/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" old mode 100644 new mode 100755 index 0eb66fb543..d396e521b3 --- "a/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" +++ "b/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 309.最佳买卖股票时机含冷冻期 @@ -49,7 +47,7 @@ dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。 * 状态三:今天卖出股票 * 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! -![](https://code-thinking-1253855093.file.myqcloud.com/pics/518d5baaf33f4b2698064f8efb42edbf.png) +![](https://file1.kamacoder.com/i/algo/518d5baaf33f4b2698064f8efb42edbf.png) j的状态为: @@ -138,7 +136,7 @@ dp[i][3] = dp[i - 1][2]; 以 [1,2,3,0,2] 为例,dp数组如下: -![309.最佳买卖股票时机含冷冻期](https://code-thinking-1253855093.file.myqcloud.com/pics/2021032317451040.png) +![309.最佳买卖股票时机含冷冻期](https://file1.kamacoder.com/i/algo/2021032317451040.png) 最后结果是取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。 @@ -274,7 +272,7 @@ class Solution { ``` ### Python: -版本一 +> 版本一 ```python from typing import List @@ -294,7 +292,8 @@ class Solution: return max(dp[n-1][3], dp[n-1][1], dp[n-1][2]) # 返回最后一天不持有股票的最大利润 ``` -版本二 + +> 版本二 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: @@ -320,6 +319,36 @@ class Solution: return max(dp[-1][1], dp[-1][2]) ``` + +> 版本三 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + # 0: holding stocks + # (1) keep holding stocks: dp[i][0] = dp[i - 1][0] + # (2) buy stocks: dp[i][0] = dp[i - 1][1] - price, or dp[i - 1][3] - price + # 1: keep no stocks: dp[i][1] = dp[i - 1][1] + # 2: sell stocks: dp[i][2] = dp[i - 1][0] + price + # 3: cooldown day: dp[i][3] = dp[i - 1][2] + dp = [-prices[0], 0, 0, 0] + + for price in prices[1:]: + dc = dp.copy() # 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + dp[0] = max( + dc[0], + dc[1] - price, + dc[3] - price + ) + dp[1] = max( + dc[1], + dc[3] + ) + dp[2] = dc[0] + price + dp[3] = dc[2] + + return max(dp) +``` + ### Go: ```go @@ -357,7 +386,63 @@ func max(a, b int) int { } ``` -### Javascript: +```go +// 一维优化版本 +// 时间复杂度O(n), 空间复杂度O(1) +func maxProfit(prices []int) int { + + // 0: 持有,一直持有和买入 + // 1: 不持有,一直不持有(不包含前一天卖出,因为这样的一天是冷静期,状态有区别) + // 2:不持有,今天卖出 + // 3:冷静期,前一天卖出(一直不持有) + dp0, dp1, dp2, dp3 := -prices[0], 0, 0, 0 + + n := len(prices) + + for i := 1; i < n; i++ { + t0 := max(dp0, max(dp1, dp3)-prices[i]) + t1 := max(dp1, dp3) + t2 := dp0 + prices[i] + t3 := dp2 + + // 更新 + dp0, dp1, dp2, dp3 = t0, t1, t2, t3 + } + + return max(dp1, max(dp2, dp3)) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + + + +### JavaScript: + +> 不同的状态定义 感觉更容易理解些 +```javascript +function maxProfit(prices) { + // 第i天状态 持股 卖出 非冷冻期(不持股) 处于冷冻期 + const dp = new Array(prices.length).fill(0).map(() => [0, 0, 0, 0]); + dp[0][0] = -prices[0]; + for (let i = 1; i < prices.length; i++) { + // 持股 + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]); + // 卖出 + dp[i][1] = dp[i - 1][0] + prices[i]; + // 非冷冻期(不持股) + dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1]); + // 冷冻期(上一天卖出) + dp[i][3] = dp[i - 1][1]; + } + return Math.max(...dp.pop()); +}; +``` ```javascript const maxProfit = (prices) => { @@ -457,6 +542,40 @@ function maxProfit(prices: number[]): number { }; ``` +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +/** + * 状态一:持有股票状态(今天买入股票, + * 或者是之前就买入了股票然后没有操作,一直持有) + * 不持有股票状态,这里就有两种卖出股票状态 + * 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。 + * 或者是前一天就是卖出股票状态,一直没操作) + * 状态三:今天卖出股票 + * 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! + + */ +int maxProfit(int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + int dp[pricesSize][4]; + memset(dp, 0, sizeof (int ) * pricesSize * 4); + dp[0][0] = -prices[0]; + for (int i = 1; i < pricesSize; ++i) { + dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1] - prices[i], dp[i - 1][3] - prices[i])); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + prices[i]; + dp[i][3] = dp[i - 1][2]; + } + return max(dp[pricesSize - 1][1], max(dp[pricesSize - 1][2], dp[pricesSize - 1][3])); +} +``` + + + ### Rust: ```rust @@ -482,8 +601,4 @@ impl Solution { ``` -

- - - diff --git "a/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" "b/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" old mode 100644 new mode 100755 index eae4ab3ac0..f3a0a07dd9 --- "a/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" +++ "b/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 322. 零钱兑换 @@ -106,7 +104,7 @@ dp[0] = 0; 以输入:coins = [1, 2, 5], amount = 5为例 -![322.零钱兑换](https://code-thinking-1253855093.file.myqcloud.com/pics/20210201111833906.jpg) +![322.零钱兑换](https://file1.kamacoder.com/i/algo/20210201111833906.jpg) dp[amount]为最终结果。 @@ -352,6 +350,35 @@ func min(a, b int) int { ``` +## C + +```c +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int coinChange(int* coins, int coinsSize, int amount) { + int* dp = (int*)malloc(sizeof(int) * (amount + 1)); + for (int j = 0; j < amount + 1; j++) { + dp[j] = INT_MAX; + } + dp[0] = 0; + // 遍历背包 + for(int i = 0; i <= amount; i++){ + // 遍历物品 + for(int j = 0; j < coinsSize; j++){ + if(i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX){ + dp[i] = min(dp[i], dp[i - coins[j]] + 1); + } + } + } + if(dp[amount] == INT_MAX){ + return -1; + } + return dp[amount]; +} +``` + + + ### Rust: ```rust @@ -398,7 +425,7 @@ impl Solution { } ``` -### Javascript: +### JavaScript: ```javascript // 遍历物品 @@ -470,8 +497,3 @@ function coinChange(coins: number[], amount: number): number { ``` -

- - - - diff --git "a/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" "b/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" old mode 100644 new mode 100755 index 036cfc51b5..1168277a8d --- "a/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" +++ "b/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。 @@ -59,7 +57,7 @@ 对于死循环,我来举一个有重复机场的例子: -![332.重新安排行程](https://code-thinking-1253855093.file.myqcloud.com/pics/20201115180537865.png) +![332.重新安排行程](https://file1.kamacoder.com/i/algo/20201115180537865.png) 为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,**如果在解题的过程中没有对集合元素处理好,就会死循环。** @@ -113,7 +111,7 @@ void backtracking(参数) { 本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下: -![332.重新安排行程1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111518065555-20230310121223600.png) +![332.重新安排行程1](https://file1.kamacoder.com/i/algo/2020111518065555-20230310121223600.png) 开始回溯三部曲讲解: @@ -139,7 +137,7 @@ bool backtracking(int ticketNum, vector& result) { 因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图: -![332.重新安排行程1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111518065555-20230310121240991.png) +![332.重新安排行程1](https://file1.kamacoder.com/i/algo/2020111518065555-20230310121240991.png) 所以找到了这个叶子节点了直接返回,这个递归函数的返回值问题我们在讲解二叉树的系列的时候,在这篇[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)详细介绍过。 @@ -172,7 +170,7 @@ if (result.size() == ticketNum + 1) { 回溯的过程中,如何遍历一个机场所对应的所有机场呢? -这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map> targets`, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。 +这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map> targets`, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不再讨论了。 **可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效**。 @@ -253,9 +251,6 @@ for (pair& target : targets[result[result.size() - 1]]) 如果最终代码,发现照着回溯法模板画的话好像也能画出来,但难就难如何知道可以使用回溯,以及如果套进去,所以我再写了这么长的一篇来详细讲解。 -就酱,很多录友表示和「代码随想录」相见恨晚,那么帮Carl宣传一波吧,让更多同学知道这里! - - ## 其他语言版本 @@ -379,6 +374,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); @@ -418,35 +415,7 @@ class Solution { } ``` -### Python -回溯 使用used数组 - -```python -class Solution: - def findItinerary(self, tickets: List[List[str]]) -> List[str]: - tickets.sort() # 先排序,这样一旦找到第一个可行路径,一定是字母排序最小的 - used = [0] * len(tickets) - path = ['JFK'] - results = [] - self.backtracking(tickets, used, path, 'JFK', results) - return results[0] - - def backtracking(self, tickets, used, path, cur, results): - if len(path) == len(tickets) + 1: # 终止条件:路径长度等于机票数量+1 - results.append(path[:]) # 将当前路径添加到结果列表 - return True - - for i, ticket in enumerate(tickets): # 遍历机票列表 - if ticket[0] == cur and used[i] == 0: # 找到起始机场为cur且未使用过的机票 - used[i] = 1 # 标记该机票为已使用 - path.append(ticket[1]) # 将到达机场添加到路径中 - state = self.backtracking(tickets, used, path, ticket[1], results) # 递归搜索 - path.pop() # 回溯,移除最后添加的到达机场 - used[i] = 0 # 标记该机票为未使用 - if state: - return True # 只要找到一个可行路径就返回,不继续搜索 - -``` +### Python 回溯 使用字典 ```python class Solution: @@ -564,7 +533,7 @@ func findItinerary(tickets [][]string) []string { } ``` -### Javascript +### JavaScript ```Javascript @@ -969,8 +938,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" "b/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" old mode 100644 new mode 100755 index f616ec7417..44af86bb4c --- "a/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" +++ "b/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 337.打家劫舍 III @@ -14,7 +12,7 @@ 计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。 -![337.打家劫舍III](https://code-thinking-1253855093.file.myqcloud.com/pics/20210223173849619.png) +![337.打家劫舍III](https://file1.kamacoder.com/i/algo/20210223173849619.png) ## 算法公开课 @@ -179,7 +177,7 @@ return {val2, val1}; 以示例1为例,dp数组状态如下:(**注意用后序遍历的方式推导**) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230203110031.png) +![](https://file1.kamacoder.com/i/algo/20230203110031.png) **最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 @@ -388,19 +386,96 @@ class Solution: ### Go -动态规划 +暴力递归 ```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ func rob(root *TreeNode) int { - res := robTree(root) - return max(res[0], res[1]) + if root == nil { + return 0 + } + if root.Left == nil && root.Right == nil { + return root.Val + } + // 偷父节点 + val1 := root.Val + if root.Left != nil { + val1 += rob(root.Left.Left) + rob(root.Left.Right) // 跳过root->left,相当于不考虑左孩子了 + } + if root.Right != nil { + val1 += rob(root.Right.Left) + rob(root.Right.Right) // 跳过root->right,相当于不考虑右孩子了 + } + // 不偷父节点 + val2 := rob(root.Left) + rob(root.Right) // 考虑root的左右孩子 + return max(val1, val2) } -func max(a, b int) int { - if a > b { - return a - } - return b +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +记忆化递推 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var umap = make(map[*TreeNode]int) + +func rob(root *TreeNode) int { + if root == nil { + return 0 + } + if root.Left == nil && root.Right == nil { + return root.Val + } + if val, ok := umap[root]; ok { + return val // 如果umap里已经有记录则直接返回 + } + // 偷父节点 + val1 := root.Val + if root.Left != nil { + val1 += rob(root.Left.Left) + rob(root.Left.Right) // 跳过root->left,相当于不考虑左孩子了 + } + if root.Right != nil { + val1 += rob(root.Right.Left) + rob(root.Right.Right) // 跳过root->right,相当于不考虑右孩子了 + } + // 不偷父节点 + val2 := rob(root.Left) + rob(root.Right) // 考虑root的左右孩子 + umap[root] = max(val1, val2) // umap记录一下结果 + return max(val1, val2) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +动态规划 + +```go +func rob(root *TreeNode) int { + res := robTree(root) + return slices.Max(res) } func robTree(cur *TreeNode) []int { @@ -414,7 +489,7 @@ func robTree(cur *TreeNode) []int { // 考虑去偷当前的屋子 robCur := cur.Val + left[0] + right[0] // 考虑不去偷当前的屋子 - notRobCur := max(left[0], left[1]) + max(right[0], right[1]) + notRobCur := slices.Max(left) + slices.Max(right) // 注意顺序:0:不偷,1:去偷 return []int{notRobCur, robCur} @@ -490,6 +565,33 @@ function robNode(node: TreeNode | null): MaxValueArr { } ``` +### C + +```c +int *robTree(struct TreeNode *node) { + int* amounts = (int*) malloc(sizeof(int) * 2); + memset(amounts, 0, sizeof(int) * 2); + if(node == NULL){ + return amounts; + } + int * left = robTree(node->left); + int * right = robTree(node->right); + // 偷当前节点 + amounts[1] = node->val + left[0] + right[0]; + // 不偷当前节点 + amounts[0] = max(left[0], left[1]) + max(right[0], right[1]); + return amounts; +} + +int rob(struct TreeNode* root) { + int * dp = robTree(root); + // 0代表不偷当前节点可以获得的最大值,1表示偷当前节点可以获取的最大值 + return max(dp[0], dp[1]); +} +``` + + + ### Rust 动态规划: @@ -519,8 +621,3 @@ impl Solution { ``` -

- - - - diff --git "a/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" "b/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" old mode 100644 new mode 100755 index bbbd5c6379..c9467e361f --- "a/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" +++ "b/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 343. 整数拆分 @@ -129,7 +127,7 @@ for (int i = 3; i <= n ; i++) { 举例当n为10 的时候,dp数组里的数值,如下: -![343.整数拆分](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104173021581.png) +![343.整数拆分](https://file1.kamacoder.com/i/algo/20210104173021581.png) 以上动规五部曲分析完毕,C++代码如下: @@ -243,6 +241,29 @@ class Solution { } } ``` +贪心 +```Java +class Solution { + public int integerBreak(int n) { + // with 贪心 + // 通过数学原理拆出更多的3乘积越大,则 + /** + @Param: an int, the integer we need to break. + @Return: an int, the maximum integer after breaking + @Method: Using math principle to solve this problem + @Time complexity: O(1) + **/ + if(n == 2) return 1; + if(n == 3) return 2; + int result = 1; + while(n > 4) { + n-=3; + result *=3; + } + return result*n; + } +} +``` ### Python 动态规划(版本一) @@ -296,7 +317,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 @@ -309,6 +330,8 @@ class Solution: ``` ### Go + +动态规划 ```go func integerBreak(n int) int { /** @@ -338,7 +361,29 @@ func max(a, b int) int{ } ``` -### Javascript +贪心 +```go +func integerBreak(n int) int { + if n == 2 { + return 1 + } + if n == 3 { + return 2 + } + if n == 4 { + return 4 + } + result := 1 + for n > 4 { + result *= 3 + n -= 3 + } + result *= n + return result +} +``` + +### JavaScript ```Javascript var integerBreak = function(n) { let dp = new Array(n + 1).fill(0) @@ -516,8 +561,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index 44184c53bc..cadb31c97b --- "a/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -71,7 +69,7 @@ 以字符串`hello`为例,过程如下: -![344.反转字符串](https://code-thinking.cdn.bcebos.com/gifs/344.%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2.gif) +![344.反转字符串](https://file1.kamacoder.com/i/algo/344.%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2.gif) 不难写出如下C++代码: @@ -429,7 +427,3 @@ object Solution { } } ``` -

- - - diff --git "a/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" "b/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" old mode 100644 new mode 100755 index 93d605f5fe..fa7d6155a5 --- "a/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" +++ "b/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 前K个大数问题,老生常谈,不得不谈 @@ -72,7 +70,7 @@ 寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描) -![347.前K个高频元素](https://code-thinking.cdn.bcebos.com/pics/347.前K个高频元素.jpg) +![347.前K个高频元素](https://file1.kamacoder.com/i/algo/347.前K个高频元素.jpg) 我们来看一下C++代码: @@ -218,7 +216,7 @@ class Solution { ``` ### Python: - +解法一: ```python #时间复杂度:O(nlogk) #空间复杂度:O(n) @@ -246,6 +244,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: @@ -380,6 +403,11 @@ class Heap { // 获取堆顶元素并移除 pop() { + // 边界情况,只有一个元素或没有元素应直接弹出 + if (this.size() <= 1) { + return this.queue.pop() + } + // 堆顶元素 const out = this.queue[0]; @@ -391,7 +419,7 @@ class Heap { let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标 let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left; - while (searchChild !== undefined && this.compare(index, searchChild) > 0) { // 注意compare参数顺序 + while (this.compare(index, searchChild) > 0) { // 注意compare参数顺序 [this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]]; // 更新下标 @@ -578,8 +606,5 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" "b/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" old mode 100644 new mode 100755 index 9902fff880..77e895da61 --- "a/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" +++ "b/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -16,7 +14,7 @@ 题意:给定两个数组,编写一个函数来计算它们的交集。 -![349. 两个数组的交集](https://code-thinking-1253855093.file.myqcloud.com/pics/20200818193523911.png) +![349. 两个数组的交集](https://file1.kamacoder.com/i/algo/20200818193523911.png) **说明:** 输出结果中的每个元素一定是唯一的。 @@ -53,7 +51,7 @@ std::set和std::multiset底层实现都是红黑树,std::unordered_set的底 思路如图所示: -![set哈希法](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707173513.png) +![set哈希法](https://file1.kamacoder.com/i/algo/20220707173513.png) C++代码如下: @@ -123,6 +121,9 @@ public: ### Java: 版本一:使用HashSet ```Java +// 时间复杂度O(n+m+k) 空间复杂度O(n+k) +// 其中n是数组nums1的长度,m是数组nums2的长度,k是交集元素的个数 + import java.util.HashSet; import java.util.Set; @@ -145,8 +146,15 @@ class Solution { } //方法1:将结果集合转为数组 - - return resSet.stream().mapToInt(x -> x).toArray(); + return res.stream().mapToInt(Integer::intValue).toArray(); + /** + * 将 Set 转换为 int[] 数组: + * 1. stream() : Collection 接口的方法,将集合转换为 Stream + * 2. mapToInt(Integer::intValue) : + * - 中间操作,将 Stream 转换为 IntStream + * - 使用方法引用 Integer::intValue,将 Integer 对象拆箱为 int 基本类型 + * 3. toArray() : 终端操作,将 IntStream 转换为 int[] 数组。 + */ //方法2:另外申请一个数组存放setRes中的元素,最后返回数组 int[] arr = new int[resSet.size()]; @@ -511,7 +519,7 @@ object Solution { ``` -###Ruby +### Ruby: ```ruby def intersection(nums1, nums2) hash = {} @@ -534,7 +542,4 @@ end * [350.两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) -

- - - + diff --git "a/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" "b/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 5c2241c805..1be9cb4178 --- "a/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" +++ "b/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 376. 摆动序列 @@ -48,7 +46,7 @@ 用示例二来举例,如图所示: -![376.摆动序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124174327597.png) +![376.摆动序列](https://file1.kamacoder.com/i/algo/20201124174327597.png) **局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值**。 @@ -72,15 +70,15 @@ #### 情况一:上下坡中有平坡 -例如 [1,2,2,2,1]这样的数组,如图: +例如 [1,2,2,2,2,1]这样的数组,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230106170449.png) +![](https://file1.kamacoder.com/i/algo/20230106170449.png) 它的摇摆序列长度是多少呢? **其实是长度是 3**,也就是我们在删除的时候 要不删除左面的三个 2,要不就删除右边的三个 2。 如图,可以统一规则,删除左边的三个 2: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230106172613.png) +![](https://file1.kamacoder.com/i/algo/20230106172613.png) 在图中,当 i 指向第一个 2 的时候,`prediff > 0 && curdiff = 0` ,当 i 指向最后一个 2 的时候 `prediff = 0 && curdiff < 0`。 @@ -108,7 +106,7 @@ 那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图: -![376.摆动序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124174357612.png) +![376.摆动序列1](https://file1.kamacoder.com/i/algo/20201124174357612.png) 针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2) @@ -147,7 +145,7 @@ public: 在版本一中,我们忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230108171505.png) +![](https://file1.kamacoder.com/i/algo/20230108171505.png) 图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。 @@ -186,7 +184,7 @@ public: **本题异常情况的本质,就是要考虑平坡**, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230108174452.png) +![](https://file1.kamacoder.com/i/algo/20230108174452.png) ### 思路 2(动态规划) @@ -466,7 +464,7 @@ func max(a, b int) int { } ``` -### Javascript +### JavaScript **贪心** @@ -714,7 +712,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" "b/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" old mode 100644 new mode 100755 index a840ec9bcc..ab92f24aef --- "a/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" +++ "b/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 377. 组合总和 Ⅳ @@ -105,7 +103,7 @@ dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导 我们再来用示例中的例子推导一下: -![377.组合总和Ⅳ](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000625.png) +![377.组合总和Ⅳ](https://file1.kamacoder.com/i/algo/20230310000625.png) 如果代码运行处的结果不是想要的结果,就把dp[i]都打出来,看看和我们推导的一不一样。 @@ -254,7 +252,7 @@ func combinationSum4(nums []int, target int) int { } ``` -### Javascript: +### JavaScript: ```javascript const combinationSum4 = (nums, target) => { @@ -312,7 +310,28 @@ impl Solution { } } ``` +### C + +```c +int combinationSum4(int* nums, int numsSize, int target) { + int dp[target + 1]; + memset(dp, 0, sizeof (dp )); + dp[0] = 1; + for(int i = 0; i <= target; i++){ + for(int j = 0; j < numsSize; j++){ + if(i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]){ + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; +} +``` + + + ### C# + ```csharp public class Solution { @@ -336,8 +355,3 @@ public class Solution ``` -

- - - - diff --git "a/problems/0383.\350\265\216\351\207\221\344\277\241.md" "b/problems/0383.\350\265\216\351\207\221\344\277\241.md" old mode 100644 new mode 100755 index ff5aafed62..8a2f52ae42 --- "a/problems/0383.\350\265\216\351\207\221\344\277\241.md" +++ "b/problems/0383.\350\265\216\351\207\221\344\277\241.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -104,7 +102,7 @@ public: }; ``` -* 时间复杂度: O(n) +* 时间复杂度: O(m+n),其中m表示ransomNote的长度,n表示magazine的长度 * 空间复杂度: O(1) @@ -133,7 +131,7 @@ class Solution { record[c - 'a'] -= 1; } - // 如果数组中存在负数,说明ransomNote字符串总存在magazine中没有的字符 + // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符 for(int i : record){ if(i < 0){ return false; @@ -176,7 +174,7 @@ class Solution: for x in ransomNote: value = hashmap.get(x) - if not value or not value: + if not value: return False else: hashmap[x] -= 1 @@ -465,8 +463,5 @@ bool canConstruct(char* ransomNote, char* magazine) { } ``` -

- - - + diff --git "a/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" "b/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index ebd567cbb7..bf2d959682 --- "a/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 392.判断子序列 @@ -82,7 +80,7 @@ if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前 因为这样的定义在dp二维矩阵中可以留出初始化的区间,如图: -![392.判断子序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20210303173115966.png) +![392.判断子序列](https://file1.kamacoder.com/i/algo/20210303173115966.png) 如果要是定义的dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t,初始化就比较麻烦了。 @@ -100,14 +98,14 @@ vector> dp(s.size() + 1, vector(t.size() + 1, 0)); 如图所示: -![392.判断子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210303172354155.jpg) +![392.判断子序列1](https://file1.kamacoder.com/i/algo/20210303172354155.jpg) 5. 举例推导dp数组 以示例一为例,输入:s = "abc", t = "ahbgdc",dp状态转移图如下: -![392.判断子序列2](https://code-thinking-1253855093.file.myqcloud.com/pics/2021030317364166.jpg) +![392.判断子序列2](https://file1.kamacoder.com/i/algo/2021030317364166.jpg) dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。 @@ -173,6 +171,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 +402,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" "b/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" old mode 100644 new mode 100755 index 3d0f5a8abf..10b159b181 --- "a/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" +++ "b/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 404.左叶子之和 @@ -14,7 +12,7 @@ 示例: -![404.左叶子之和1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204151927654.png) +![404.左叶子之和1](https://file1.kamacoder.com/i/algo/20210204151927654.png) ## 算法公开课 @@ -28,12 +26,12 @@ 大家思考一下如下图中二叉树,左叶子之和究竟是多少? -![404.左叶子之和](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204151949672.png) +![404.左叶子之和](https://file1.kamacoder.com/i/algo/20210204151949672.png) **其实是0,因为这棵树根本没有左叶子!** 但看这个图的左叶子之和是多少? -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220902165805.png) +![图二](https://file1.kamacoder.com/i/algo/20220902165805.png) 相信通过这两个图,大家对最左叶子的定义有明确理解了。 @@ -337,6 +335,21 @@ func sumOfLeftLeaves(root *TreeNode) int { } ``` +**递归精简版** + +```go +func sumOfLeftLeaves(root *TreeNode) int { + if root == nil { + return 0 + } + leftValue := 0 + if root.Left != nil && root.Left.Left == nil && root.Left.Right == nil { + leftValue = root.Left.Val + } + return leftValue + sumOfLeftLeaves(root.Left) + sumOfLeftLeaves(root.Right) +} +``` + **迭代法(前序遍历)** ```go @@ -669,8 +682,4 @@ public int SumOfLeftLeaves(TreeNode root) } ``` -

- - - diff --git "a/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" "b/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" old mode 100644 new mode 100755 index b0b02c1454..ce9d3bfb4e --- "a/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" +++ "b/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 406.根据身高重建队列 @@ -63,7 +61,7 @@ 以图中{5,2} 为例: -![406.根据身高重建队列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201216201851982.png) +![406.根据身高重建队列](https://file1.kamacoder.com/i/algo/20201216201851982.png) 按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。 @@ -87,7 +85,7 @@ 回归本题,整个插入过程如下: 排序完的people: -[[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]] +[[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]] 插入的过程: * 插入[7,0]:[[7,0]] @@ -270,7 +268,7 @@ func reconstructQueue(people [][]int) [][]int { } ``` -### Javascript +### JavaScript ```Javascript var reconstructQueue = function(people) { @@ -421,8 +419,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" "b/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" old mode 100644 new mode 100755 index 71e01ae392..75bc5d0e10 --- "a/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" +++ "b/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 416. 分割等和子集 @@ -47,16 +45,22 @@ 那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。 -本题是可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯,直接上01背包吧。 +本题是可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯。 + +是否有其他解法可以解决此题。 + +本题的本质是,能否把容量为 sum / 2的背包装满。 + +**这是 背包算法可以解决的经典类型题目**。 如果对01背包不够了解,建议仔细看完如下两篇: * [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) * [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) -### 01背包问题 +## 01背包问题 -背包问题,大家都知道,有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 +01背包问题,大家都知道,有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 **背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。** @@ -64,32 +68,33 @@ **即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。** -**要明确本题中我们要使用的是01背包,因为元素我们只能用一次。** +**元素我们只能用一次,如果使用背包,那么也是01背包** 回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。 -那么来一一对应一下本题,看看背包问题如何来解决。 +既有一个 只能装重量为 sum / 2 的背包,商品为数字,这些数字能不能把 这个背包装满。 -**只有确定了如下四点,才能把01背包问题套到本题上来。** +那每一件商品是数字的话,对应的重量 和 价值是多少呢? -* 背包的体积为sum / 2 -* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值 -* 背包如果正好装满,说明找到了总和为 sum / 2 的子集。 -* 背包中每一个元素是不可重复放入。 +一个数字只有一个维度,即 重量等于价值。 -以上分析完,我们就可以套用01背包,来解决这个问题了。 +当数字 可以装满 承载重量为 sum / 2 的背包的背包时,这个背包的价值也是 sum / 2。 -动规五部曲分析如下: +那么这道题就是 装满 承载重量为 sum / 2 的背包,价值最大是多少? -1. 确定dp数组以及下标的含义 +如果最大价值是 sum / 2,说明正好被商品装满了。 -01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。 +因为商品是数字,重量和对应的价值是相同的。 + +以上分析完,我们就可以直接用01背包 来解决这个问题了。 + +动规五部曲分析如下: -本题中每一个元素的数值既是重量,也是价值。 +### 1. 确定dp数组以及下标的含义 -**套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]**。 +01背包中,dp[j] 表示: 容量(所能装的重量)为j的背包,所背的物品价值最大可以为dp[j]。 -那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。 +如果背包所载重量为target, dp[target]就是装满 背包之后的总价值,因为 本题中每一个元素的数值既是重量,也是价值,所以,当 dp[target] == target 的时候,背包就装满了。 有录友可能想,那还有装不满的时候? @@ -97,7 +102,7 @@ 而dp[6] 就可以等于6了,放进1 和 5,那么dp[6] == 6,说明背包装满了。 -2. 确定递推公式 +### 2. 确定递推公式 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); @@ -106,7 +111,7 @@ 所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); -3. dp数组如何初始化 +### 3. dp数组如何初始化 在01背包,一维dp如何初始化,已经讲过, @@ -126,7 +131,7 @@ vector dp(10001, 0); ``` -4. 确定遍历顺序 +### 4. 确定遍历顺序 在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历! @@ -141,7 +146,7 @@ for(int i = 0; i < nums.size(); i++) { } ``` -5. 举例推导dp数组 +### 5. 举例推导dp数组 dp[j]的数值一定是小于等于j的。 @@ -150,7 +155,7 @@ dp[j]的数值一定是小于等于j的。 用例1,输入[1,5,11,5] 为例,如图: -![416.分割等和子集2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110104240545.png) +![416.分割等和子集2](https://file1.kamacoder.com/i/algo/20210110104240545.png) 最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。 @@ -192,12 +197,11 @@ public: ## 总结 -这道题目就是一道01背包应用类的题目,需要我们拆解题目,然后套入01背包的场景。 +这道题目就是一道01背包经典应用类的题目,需要我们拆解题目,然后才能发现可以使用01背包。 01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。 -看代码的话,就可以发现,基本就是按照01背包的写法来的。 - +做完本题后,需要大家清晰:背包问题,不仅可以求 背包能被的最大价值,还可以求这个背包是否可以装满。 ## 其他语言版本 @@ -453,6 +457,7 @@ class Solution: ``` ### Go: +一维dp ```go // 分割等和子集 动态规划 // 时间复杂度O(n^2) 空间复杂度O(n) @@ -480,6 +485,44 @@ func canPartition(nums []int) bool { } ``` +二维dp +```go +func canPartition(nums []int) bool { + sum := 0 + for _, val := range nums { + sum += val + } + if sum % 2 == 1 { + return false + } + target := sum / 2 + dp := make([][]int, len(nums)) + for i := range dp { + dp[i] = make([]int, target + 1) + } + for j := nums[0]; j <= target; j++ { + dp[0][j] = nums[0] + } + for i := 1; i < len(nums); i++ { + for j := 0; j <= target; j++ { + if j < nums[i] { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i]) + } + } + } + return dp[len(nums)-1][target] == target +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + ### JavaScript: ```js @@ -755,8 +798,4 @@ public class Solution } } ``` -

- - - diff --git "a/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" "b/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" old mode 100644 new mode 100755 index 8fe0f1b426..c9494313a1 --- "a/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" +++ "b/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -20,7 +18,7 @@ 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230129103212.png) +![](https://file1.kamacoder.com/i/algo/20230129103212.png) * 输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]] * 输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]] @@ -132,11 +130,11 @@ public: 从太平洋边上节点出发,如图: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220722103029.png) +![图一](https://file1.kamacoder.com/i/algo/20220722103029.png) 从大西洋边上节点出发,如图: -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220722103330.png) +![图二](https://file1.kamacoder.com/i/algo/20220722103330.png) 按照这样的逻辑,就可以写出如下遍历代码:(详细注释) @@ -177,14 +175,14 @@ public: // 记录从大西洋出发,可以遍历的节点 vector> atlantic = vector>(n, vector(m, false)); - - // 从最上最下行的节点出发,向高处遍历 + + // 从最左最右列的节点出发,向高处遍历 for (int i = 0; i < n; i++) { dfs (heights, pacific, i, 0); // 遍历最左列,接触太平洋 dfs (heights, atlantic, i, m - 1); // 遍历最右列,接触大西 } - // 从最左最右列的节点出发,向高处遍历 + // 从最上最下行的节点出发,向高处遍历 for (int j = 0; j < m; j++) { dfs (heights, pacific, 0, j); // 遍历最上行,接触太平洋 dfs (heights, atlantic, n - 1, j); // 遍历最下行,接触大西洋 @@ -297,6 +295,73 @@ class Solution { } ``` +```Java +class Solution { + + // 和Carl题解更加符合的Java DFS + private int[][] directions = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}}; + + /** + * @param heights 题目给定的二维数组 + * @param m 当前位置的行号 + * @param n 当前位置的列号 + * @param visited 记录这个位置可以到哪条河 + */ + + public void dfs(int[][] heights, boolean[][] visited, int m, int n){ + if(visited[m][n]) return; + visited[m][n] = true; + + for(int[] dir: directions){ + int nextm = m + dir[0]; + int nextn = n + dir[1]; + //出了2D array的边界,continue + if(nextm < 0||nextm == heights.length||nextn <0||nextn== heights[0].length) continue; + //下一个位置比当下位置还要低,跳过,继续找下一个更高的位置 + if(heights[m][n] > heights[nextm][nextn]) continue; + dfs(heights, visited, nextm, nextn); + } + } + + + public List> pacificAtlantic(int[][] heights) { + int m = heights.length; + int n = heights[0].length; + + // 记录从太平洋边出发,可以遍历的节点 + boolean[][] pacific = new boolean[m][n]; + // 记录从大西洋出发,可以遍历的节点 + boolean[][] atlantic = new boolean[m][n]; + + // 从最左最右列的节点出发,向高处遍历 + for(int i = 0; i> result = new ArrayList<>(); + for(int a = 0; a pair = new ArrayList<>(); + pair.add(a); + pair.add(b); + result.add(pair); + } + } + } + return result; + } +} +``` + 广度优先遍历: ```Java @@ -770,7 +835,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" "b/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" old mode 100644 new mode 100755 index cf9996ddef..4231a8ee90 --- "a/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" +++ "b/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 435. 无重叠区间 @@ -46,7 +44,7 @@ 这里记录非交叉区间的个数还是有技巧的,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230201164134.png) +![](https://file1.kamacoder.com/i/algo/20230201164134.png) 区间,1,2,3,4,5,6都按照右边界排好序。 @@ -311,7 +309,7 @@ func min(a, b int) int { } ``` -### Javascript +### JavaScript - 按右边界排序 ```Javascript var eraseOverlapIntervals = function(intervals) { @@ -441,7 +439,37 @@ impl Solution { } } ``` +### C + +```c +// 按照区间右边界排序 +int cmp(const void * var1, const void * var2){ + return (*(int **) var1)[1] - (*(int **) var2)[1]; +} + +int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize) { + if(intervalsSize == 0){ + return 0; + } + qsort(intervals, intervalsSize, sizeof (int *), cmp); + // 记录非重叠的区间数量 + int count = 1; + // 记录区间分割点 + int end = intervals[0][1]; + for(int i = 1; i < intervalsSize; i++){ + if(end <= intervals[i][0]){ + end = intervals[i][1]; + count++; + } + } + return intervalsSize - count; +} +``` + + + ### C# + ```csharp public class Solution { @@ -464,7 +492,4 @@ public class Solution ``` -

- - - + diff --git "a/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" old mode 100644 new mode 100755 index 8922a14e14..44575b8aff --- "a/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" +++ "b/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 二叉搜索树删除节点就涉及到结构调整了 @@ -22,11 +20,11 @@ 示例: -![450.删除二叉搜索树中的节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20201020171048265.png) +![450.删除二叉搜索树中的节点](https://file1.kamacoder.com/i/algo/20201020171048265.png) ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[调整二叉树的结构最难!| LeetCode:450.删除二叉搜索树中的节点](https://www.bilibili.com/video/BV1tP41177us?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[调整二叉树的结构最难!| LeetCode:450.删除二叉搜索树中的节点](https://www.bilibili.com/video/BV1tP41177us?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -42,7 +40,7 @@ 代码如下: -``` +```cpp TreeNode* deleteNode(TreeNode* root, int key) ``` @@ -50,7 +48,7 @@ TreeNode* deleteNode(TreeNode* root, int key) 遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了 -``` +```cpp if (root == nullptr) return root; ``` @@ -69,7 +67,7 @@ if (root == nullptr) return root; 第五种情况有点难以理解,看下面动画: -![450.删除二叉搜索树中的节点](https://code-thinking.cdn.bcebos.com/gifs/450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.gif) +![450.删除二叉搜索树中的节点](https://file1.kamacoder.com/i/algo/450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.gif) 动画中的二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。 @@ -106,7 +104,7 @@ if (root->val == key) { 这里相当于把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住,代码如下: -``` +```cpp if (root->val > key) root->left = deleteNode(root->left, key); if (root->val < key) root->right = deleteNode(root->right, key); return root; @@ -801,8 +799,38 @@ impl Solution { } ``` -

- - - +### Ruby +> 递归法: +```ruby +# @param {TreeNode} root +# @param {Integer} key +# @return {TreeNode} +def delete_node(root, key) + return nil if root.nil? + + right = root.right + left = root.left + + if root.val == key + return right if left.nil? + return left if right.nil? + + node = right + while node.left + node = node.left + end + node.left = left + + return right + end + + if root.val > key + root.left = delete_node(left, key) + else + root.right = delete_node(right, key) + end + + return root +end +``` diff --git "a/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" "b/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" old mode 100644 new mode 100755 index cd57f83b26..76de3f93a2 --- "a/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" +++ "b/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 452. 用最少数量的箭引爆气球 @@ -78,7 +76,7 @@ 以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序) -![452.用最少数量的箭引爆气球](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123101929791.png) +![452.用最少数量的箭引爆气球](https://file1.kamacoder.com/i/algo/20201123101929791.png) 可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。 @@ -110,7 +108,7 @@ public: ``` * 时间复杂度:O(nlog n),因为有一个快排 -* 空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间 +* 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间 可以看出代码并不复杂。 @@ -180,19 +178,25 @@ class Solution: ```python class Solution: # 不改变原数组 def findMinArrowShots(self, points: List[List[int]]) -> int: + if len(points) == 0: + return 0 + points.sort(key = lambda x: x[0]) - sl,sr = points[0][0],points[0][1] + + # points已经按照第一个坐标正序排列,因此只需要设置一个变量,记录右侧坐标(阈值) + # 考虑一个气球范围包含两个不相交气球的情况:气球1: [1, 10], 气球2: [2, 5], 气球3: [6, 10] + curr_min_right = points[0][1] count = 1 + for i in points: - if i[0]>sr: - count+=1 - sl,sr = i[0],i[1] + if i[0] > curr_min_right: + # 当气球左侧大于这个阈值,那么一定就需要在发射一只箭,并且将阈值更新为当前气球的右侧 + count += 1 + curr_min_right = i[1] else: - sl = max(sl,i[0]) - sr = min(sr,i[1]) + # 否则的话,我们只需要求阈值和当前气球的右侧的较小值来更新阈值 + curr_min_right = min(curr_min_right, i[1]) return count - - ``` ### Go ```go @@ -220,7 +224,7 @@ func min(a, b int) int { } ``` -### Javascript +### JavaScript ```Javascript var findMinArrowShots = function(points) { points.sort((a, b) => { @@ -351,7 +355,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" "b/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" old mode 100644 new mode 100755 index a0bf84da83..a26071a1fa --- "a/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" +++ "b/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 需要哈希的地方都能找到map的身影 @@ -54,7 +52,7 @@ 1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。 2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。 3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。 -4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。 +4. 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。 5. 最后返回统计值 count 就可以了 C++代码: @@ -71,7 +69,7 @@ public: } } int count = 0; // 统计a+b+c+d = 0 出现的次数 - // 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。 + // 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。 for (int c : C) { for (int d : D) { if (umap.find(0 - (c + d)) != umap.end()) { @@ -185,22 +183,28 @@ class Solution: ### Go: ```go -func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int { - m := make(map[int]int) //key:a+b的数值,value:a+b数值出现的次数 - count := 0 - // 遍历nums1和nums2数组,统计两个数组元素之和,和出现的次数,放到map中 +func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int { + m := make(map[int]int) + count := 0 + + // 构建nums1和nums2的和的map for _, v1 := range nums1 { - for _, v2 := range nums2 { - m[v1+v2]++ - } - } - // 遍历nums3和nums4数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来 - for _, v3 := range nums3 { - for _, v4 := range nums4 { - count += m[-v3-v4] - } - } - return count + for _, v2 := range nums2 { + m[v1+v2]++ + } + } + + // 遍历nums3和nums4,检查-c-d是否在map中 + for _, v3 := range nums3 { + for _, v4 := range nums4 { + sum := -v3 - v4 + if countVal, ok := m[sum]; ok { + count += countVal + } + } + } + + return count } ``` @@ -481,8 +485,42 @@ int fourSumCount(int* nums1, int nums1Size, int* nums2, int nums2Size, int* nums return count; } ``` -

- - - + +### Ruby: + +```ruby +# @param {Integer[]} nums1 +# @param {Integer[]} nums2 +# @param {Integer[]} nums3 +# @param {Integer[]} nums4 +# @return {Integer} +# 新思路:和版主的思路基本相同,只是对后面两个数组的二重循环,用一个方法调用外加一重循环替代,简化了一点。 +# 简单的说,就是把四数和变成了两个两数和的统计(结果放到两个 hash 中),然后再来一次两数和为0. +# 把四个数分成两组两个数,然后分别计算每组可能的和情况,分别存入 hash 中,key 是 和,value 是 数量; +# 最后,得到的两个 hash 只需要遍历一次,符合和为零的 value 相乘并加总。 +def four_sum_count(nums1, nums2, nums3, nums4) + num_to_count_1 = two_sum_mapping(nums1, nums2) + num_to_count_2 = two_sum_mapping(nums3, nums4) + + count_sum = 0 + + num_to_count_1.each do |num, count| + count_sum += num_to_count_2[-num] * count # 反查另一个 hash,看有没有匹配的,没有的话,hash 默认值为 0,不影响加总;有匹配的,乘积就是可能的情况 + end + + count_sum +end + +def two_sum_mapping(nums1, nums2) + num_to_count = Hash.new(0) + + nums1.each do |num1| + nums2.each do |nums2| + num_to_count[num1 + nums2] += 1 # 统计和为 num1 + nums2 的有几个 + end + end + + num_to_count +end +``` diff --git "a/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" "b/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" old mode 100644 new mode 100755 index 6ae206dba2..2c38ab9ec1 --- "a/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" +++ "b/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 455.分发饼干 @@ -48,7 +46,7 @@ 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230405225628.png) +![](https://file1.kamacoder.com/i/algo/20230405225628.png) 这个例子可以看出饼干 9 只有喂给胃口为 7 的小孩,这样才是整体最优解,并想不出反例,那么就可以撸代码了。 @@ -91,7 +89,7 @@ public: 如果 for 控制的是饼干, if 控制胃口,就是出现如下情况 : -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230112102848.png) +![](https://file1.kamacoder.com/i/algo/20230112102848.png) if 里的 index 指向 胃口 10, for 里的 i 指向饼干 9,因为 饼干 9 满足不了 胃口 10,所以 i 持续向前移动,而 index 走不到` s[index] >= g[i]` 的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最后所有的饼干都匹配不上。 @@ -206,23 +204,56 @@ class Solution: ``` +栈 大饼干优先 +```python +from collecion import deque +class Solution: + def findContentChildren(self, g: List[int], s: List[int]) -> int: + #思路,饼干和孩子按从大到小排序,依次从栈中取出,若满足条件result += 1 否则将饼干栈顶元素重新返回 + result = 0 + queue_g = deque(sorted(g, reverse = True)) + queue_s = deque(sorted(s, reverse = True)) + while queue_g and queue_s: + child = queue_g.popleft() + cookies = queue_s.popleft() + if child <= cookies: + result += 1 + else: + queue_s.appendleft(cookies) + return result +``` + ### Go -```golang -//排序后,局部最优 +版本一 大饼干优先 +```Go func findContentChildren(g []int, s []int) int { - sort.Ints(g) - sort.Ints(s) - - // 从小到大 - child := 0 - for sIdx := 0; child < len(g) && sIdx < len(s); sIdx++ { - if s[sIdx] >= g[child] {//如果饼干的大小大于或等于孩子的为空则给与,否则不给予,继续寻找选一个饼干是否符合 - child++ + sort.Ints(g) + sort.Ints(s) + index := len(s) - 1 + result := 0 + for i := len(g) - 1; i >= 0; i-- { + if index >= 0 && s[index] >= g[i] { + result++ + index-- + } } - } + return result +} +``` - return child +版本二 小饼干优先 +```Go +func findContentChildren(g []int, s []int) int { + sort.Ints(g) + sort.Ints(s) + index := 0 + for i := 0; i < len(s); i++ { + if index < len(g) && g[index] <= s[i] { + index++ + } + } + return index } ``` @@ -245,7 +276,7 @@ pub fn find_content_children(mut children: Vec, mut cookies: Vec) -> i } ``` -### Javascript +### JavaScript ```js var findContentChildren = function (g, s) { @@ -401,7 +432,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index 311e3a695e..d164277731 --- "a/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > KMP算法还能干这个 @@ -48,18 +46,74 @@ 当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104518.png) +![图一](https://file1.kamacoder.com/i/algo/20220728104518.png) 也就是由前后相同的子串组成。 那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,如图: -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728104931.png) +![图二](https://file1.kamacoder.com/i/algo/20220728104931.png) -所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。 当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,**要刨除 s + s 的首字符和尾字符**,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。 + +以上证明的充分性,接下来证明必要性: + +如果有一个字符串s,在 s + s 拼接后, 不算首尾字符,如果能凑成s字符串,说明s 一定是重复子串组成。 + +如图,字符串s,图中数字为数组下标,在 s + s 拼接后, 不算首尾字符,中间凑成s字符串。 (图中数字为数组下标) + +![](https://file1.kamacoder.com/i/algo/20240910115555.png) + +图中,因为中间拼接成了s,根据红色框 可以知道 s[4] = s[0], s[5] = s[1], s[0] = s[2], s[1] = s[3] s[2] = s[4] ,s[3] = s[5] + +![](https://file1.kamacoder.com/i/algo/20240910115819.png) + +以上相等关系我们串联一下: + +s[4] = s[0] = s[2] + +s[5] = s[1] = s[3] + + +即:s[4],s[5] = s[0],s[1] = s[2],s[3] + +**说明这个字符串,是由 两个字符 s[0] 和 s[1] 重复组成的**! + +这里可以有录友想,凭什么就是这样组成的s呢,我换一个方式组成s 行不行,如图: + +![](https://file1.kamacoder.com/i/algo/20240910120751.png) + +s[3] = s[0],s[4] = s[1] ,s[5] = s[2],s[0] = s[3],s[1] = s[4],s[2] = s[5] + +以上相等关系串联: + +s[3] = s[0] + +s[1] = s[4] + +s[2] = s[5] + +s[0] s[1] s[2] = s[3] s[4] s[5] + +和以上推导过程一样,最后可以推导出,这个字符串是由 s[0] ,s[1] ,s[2] 重复组成。 + +如果是这样的呢,如图: + +![](https://file1.kamacoder.com/i/algo/20240910121236.png) + +s[1] = s[0],s[2] = s[1] ,s[3] = s[2],s[4] = s[3],s[5] = s[4],s[0] = s[5] + +以上相等关系串联 + +s[0] = s[1] = s[2] = s[3] = s[4] = s[5] + +最后可以推导出,这个字符串是由 s[0] 重复组成。 + +以上 充分和必要性都证明了,所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。 + + 代码如下: ```CPP @@ -76,13 +130,14 @@ public: * 时间复杂度: O(n) * 空间复杂度: O(1) -不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。 +不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数, 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。 如果我们做过 [28.实现strStr](https://programmercarl.com/0028.实现strStr.html) 题目的话,其实就知道,**实现一个 高效的算法来判断 一个字符串中是否出现另一个字符串是很复杂的**,这里就涉及到了KMP算法。 ### KMP #### 为什么会使用KMP + 以下使用KMP方式讲解,强烈建议大家先把以下两个视频看了,理解KMP算法,再来看下面讲解,否则会很懵。 * [视频讲解版:帮你把KMP算法学个通透!(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) @@ -91,7 +146,9 @@ public: 在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢? -KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。 +KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 + +前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。 那么 最长相同前后缀和重复子串的关系又有什么关系呢。 @@ -100,16 +157,61 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一 * 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; * 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串 -在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示: +#### 充分性证明 + +如果一个字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串一定是字符串s的最小重复子串。 + +如果s 是由最小重复子串p组成,即 s = n * p + +那么相同前后缀可以是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110257.png) + +也可以是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110316.png) + +最长的相等前后缀,也就是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110841.png) -![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728205249.png) +这里有录友就想:如果字符串s 是由最小重复子串p组成,最长相等前后缀就不能更长一些? 例如这样: +![](https://file1.kamacoder.com/i/algo/20240913114348.png) -#### 如何找到最小重复子串 +如果这样的话,因为前后缀要相同,所以 p2 = p1,p3 = p2,如图: -这里有同学就问了,为啥一定是开头的ab呢。 其实最关键还是要理解 最长相等前后缀,如图: +![](https://file1.kamacoder.com/i/algo/20240913114818.png) -![图四](https://code-thinking-1253855093.file.myqcloud.com/pics/20220728212157.png) +p2 = p1,p3 = p2 即: p1 = p2 = p3 + +说明 p = p1 * 3。 + +这样p 就不是最小重复子串了,不符合我们定义的条件。 + +所以,**如果这个字符串s是由重复子串组成,那么最长相等前后缀不包含的子串是字符串s的最小重复子串**。 + +#### 必要性证明 + +以上是充分性证明,以下是必要性证明: + +**如果 最长相等前后缀不包含的子串是字符串s的最小重复子串, 那么字符串s一定由重复子串组成吗**? + +最长相等前后缀不包含的子串已经是字符串s的最小重复子串,那么字符串s一定由重复子串组成,这个不需要证明了。 + +关键是要证明:最长相等前后缀不包含的子串什么时候才是字符串s的最小重复子串呢。 + +情况一, 最长相等前后缀不包含的子串的长度 比 字符串s的一半的长度还大,那一定不是字符串s的重复子串,如图: + +![](https://file1.kamacoder.com/i/algo/20240911110236.png) + +图中:前后缀不包含的子串的长度 大于 字符串s的长度的 二分之一 + +-------------- + +情况二,最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,如图: + +![](https://file1.kamacoder.com/i/algo/20240910174249.png) 步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。 @@ -121,39 +223,94 @@ KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一 所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。 -正是因为 最长相等前后缀的规则,当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。 +可以推出,在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。 + +即 s[0]s[1] 是最小重复子串 + + +以上推导中,录友可能想,你怎么知道 s[0] 和 s[1] 就不相同呢? s[0] 为什么就不能是最小重复子串。 + +如果 s[0] 和 s[1] 也相同,同时 s[0]s[1]与s[2]s[3]相同,s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同,那么这个字符串就是有一个字符构成的字符串。 + +那么它的最长相同前后缀,就不是上图中的前后缀,而是这样的的前后缀: + +![](https://file1.kamacoder.com/i/algo/20240910175053.png) + +录友可能再问,由一个字符组成的字符串,最长相等前后缀凭什么就是这样的。 + +有这种疑惑的录友,就是还不知道 最长相等前后缀 是怎么算的。 + +可以看这里:[KMP讲解](https://programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html),再去回顾一下。 + +或者说,自己举个例子,`aaaaaa`,这个字符串,他的最长相等前后缀是什么? + +同上以上推导,最长相等前后缀不包含的子串的长度只要被 字符串s的长度整除,最长相等前后缀不包含的子串一定是最小重复子串。 + +---------------- + +**情况三,最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除得情况**,如图: + +![](https://file1.kamacoder.com/i/algo/20240913115854.png) + + +步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,t[2] 与 k[2]相同。 + +所以 s[0] 与 s[3]相同,s[1] 与 s[4]相同,s[2] 与s[5],即:,s[0]s[1]与s[2]s[3]相同 。 + +步骤二: 因为在同一个字符串位置,所以 t[3] 与 k[0]相同,t[4] 与 k[1]相同。 + + +步骤三: 因为 这是相等的前缀和后缀,t[3] 与 k[3]相同 ,t[4]与k[5] 相同,所以,s[3]一定和s[6]相同,s[4]一定和s[7]相同,即:s[3]s[4] 与 s[6]s[7]相同。 + + +以上推导,可以得出 s[0],s[1],s[2] 与 s[3],s[4],s[5] 相同,s[3]s[4] 与 s[6]s[7]相同。 + +那么 最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除 ,最长相等前后缀不包含的子串就不是s的重复子串 + +----------- + +充分条件:如果字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串 一定是 s的最小重复子串。 -#### 简单推理 +必要条件:如果字符串s的最长相等前后缀不包含的子串 是 s最小重复子串,那么 s是由重复子串组成。 -这里再给出一个数学推导,就容易理解很多。 +在必要条件,这个是 显而易见的,都已经假设 最长相等前后缀不包含的子串 是 s的最小重复子串了,那s必然是重复子串。 -假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。 +**关键是需要证明, 字符串s的最长相等前后缀不包含的子串 什么时候才是 s最小重复子串**。 -因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且 n - m = 1,(这里如果不懂,看上面的推理) +同上我们证明了,当 最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,那么不包含的子串 就是s的最小重复子串。 -所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。 -next 数组记录的就是最长相同前后缀 [字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 +------------- -最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:[字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)) + +### 代码分析 + +next 数组记录的就是最长相同前后缀( [字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)), 如果 `next[len - 1] != -1`,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 + +最长相等前后缀的长度为:`next[len - 1] + 1`。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:[字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)) 数组长度为:len。 -如果len % (len - (next[len - 1] + 1)) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。 +`len - (next[len - 1] + 1)` 是最长相等前后缀不包含的子串的长度。 -**数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。** +如果`len % (len - (next[len - 1] + 1)) == 0` ,则说明数组的长度正好可以被 最长相等前后缀不包含的子串的长度 整除 ,说明该字符串有重复的子字符串。 +### 打印数组 **强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法** 如图: -![459.重复的子字符串_1](https://code-thinking.cdn.bcebos.com/pics/459.重复的子字符串_1.png) +![459.重复的子字符串_1](https://file1.kamacoder.com/i/algo/459.重复的子字符串_1.png) + +`next[len - 1] = 7`,`next[len - 1] + 1 = 8`,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。 + +`(len - (next[len - 1] + 1))` 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 为最长相同前后缀不包含的子串长度 -next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。 -(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 +4可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 +### 代码实现 C++代码如下:(这里使用了前缀表统一减一的实现方式) @@ -231,6 +388,8 @@ public: ### Java: +(版本一) 前缀表 减一 + ```java class Solution { public boolean repeatedSubstringPattern(String s) { @@ -261,6 +420,45 @@ class Solution { } ``` +(版本二) 前缀表 不减一 + +```java +/* + * 充分条件:如果字符串s是由重复子串组成的,那么它的最长相等前后缀不包含的子串一定是s的最小重复子串。 + * 必要条件:如果字符串s的最长相等前后缀不包含的子串是s的最小重复子串,那么s必然是由重复子串组成的。 + * 推得:当字符串s的长度可以被其最长相等前后缀不包含的子串的长度整除时,不包含的子串就是s的最小重复子串。 + * + * 时间复杂度:O(n) + * 空间复杂度:O(n) + */ +class Solution { + public boolean repeatedSubstringPattern(String s) { + // if (s.equals("")) return false; + // 边界判断(可以去掉,因为题目给定范围是1 <= s.length <= 10^4) + int n = s.length(); + + // Step 1.构建KMP算法的前缀表 + int[] next = new int[n]; // 前缀表的值表示 以该位置结尾的字符串的最长相等前后缀的长度 + int j = 0; + next[0] = 0; + for (int i = 1; i < n; i++) { + while (j > 0 && s.charAt(i) != s.charAt(j)) // 只要前缀后缀还不一致,就根据前缀表回退j直到起点为止 + j = next[j - 1]; + if (s.charAt(i) == s.charAt(j)) + j++; + next[i] = j; + } + + // Step 2.判断重复子字符串 + if (next[n - 1] > 0 && n % (n - next[n - 1]) == 0) { // 当字符串s的长度可以被其最长相等前后缀不包含的子串的长度整除时 + return true; // 不包含的子串就是s的最小重复子串 + } else { + return false; + } + } +} +``` + ### Python: (版本一) 前缀表 减一 @@ -403,6 +601,18 @@ func repeatedSubstringPattern(s string) bool { } ``` +移动匹配 + +```go +func repeatedSubstringPattern(s string) bool { + if len(s) == 0 { + return false + } + t := s + s + return strings.Contains(t[1:len(t)-1], s) +} +``` + ### JavaScript: > 前缀表统一减一 @@ -477,6 +687,29 @@ var repeatedSubstringPattern = function (s) { }; ``` +> 正则匹配 +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function(s) { + let reg = /^(\w+)\1+$/ + return reg.test(s) +}; +``` +> 移动匹配 +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function (s) { + let ss = s + s; + return ss.substring(1, ss.length - 1).includes(s); +}; +``` + ### TypeScript: > 前缀表统一减一 @@ -682,8 +915,10 @@ impl Solution { } ``` ### C# + +> 前缀表不减一 + ```csharp -// 前缀表不减一 public bool RepeatedSubstringPattern(string s) { if (s.Length == 0) @@ -708,8 +943,57 @@ public int[] GetNext(string s) } ``` +> 移动匹配 +```csharp +public bool RepeatedSubstringPattern(string s) { + string ss = (s + s).Substring(1, (s + s).Length - 2); + return ss.Contains(s); +} +``` +### C + +```c +// 前缀表不减一 +int *build_next(char* s, int len) { + + int *next = (int *)malloc(len * sizeof(int)); + assert(next); + + // 初始化前缀表 + next[0] = 0; + + // 构建前缀表表 + int i = 1, j = 0; + while (i < len) { + if (s[i] == s[j]) { + j++; + next[i] = j; + i++; + } else if (j > 0) { + j = next[j - 1]; + } else { + next[i] = 0; + i++; + } + } + return next; +} + +bool repeatedSubstringPattern(char* s) { + + int len = strlen(s); + int *next = build_next(s, len); + bool result = false; + + // 检查最小重复片段能否被长度整除 + if (next[len - 1]) { + result = len % (len - next[len - 1]) == 0; + } + + free(next); + return result; +} + +``` + -

- - - diff --git "a/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" "b/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" old mode 100644 new mode 100755 index 2e954e30ad..ba60bc4564 --- "a/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" +++ "b/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -17,7 +15,7 @@ 岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230829180848.png) +![](https://file1.kamacoder.com/i/algo/20230829180848.png) * 输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]] * 输出:16 @@ -51,7 +49,7 @@ 如图: - + C++代码如下:(详细注释) @@ -91,7 +89,7 @@ result = 岛屿数量 * 4 - cover * 2; 如图: - + C++代码如下:(详细注释) @@ -432,8 +430,4 @@ function islandPerimeter(grid: number[][]): number { } ``` -

- - - diff --git "a/problems/0474.\344\270\200\345\222\214\351\233\266.md" "b/problems/0474.\344\270\200\345\222\214\351\233\266.md" old mode 100644 new mode 100755 index 904d941e9d..750917de3e --- "a/problems/0474.\344\270\200\345\222\214\351\233\266.md" +++ "b/problems/0474.\344\270\200\345\222\214\351\233\266.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 474.一和零 @@ -53,7 +51,7 @@ 其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系 -![416.分割等和子集1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210117171307407-20230310132423205.png) +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407-20230310132423205.png) 多重背包是每个物品,数量不同的情况。 @@ -129,7 +127,7 @@ for (string str : strs) { // 遍历物品 最后dp数组的状态如下所示: -![474.一和零](https://code-thinking-1253855093.file.myqcloud.com/pics/20210120111201512.jpg) +![474.一和零](https://file1.kamacoder.com/i/algo/20210120111201512.jpg) 以上动规五部曲分析完毕,C++代码如下: @@ -159,7 +157,89 @@ public: * 时间复杂度: O(kmn),k 为strs的长度 * 空间复杂度: O(mn) +C++: +使用三维数组的版本 +```CPP +class Solution { +public: + int findMaxForm(vector& strs, int m, int n) { + int num_of_str = strs.size(); + + vector>> dp(num_of_str, vector>(m + 1,vector(n + 1, 0))); + + /* dp[i][j][k] represents, if choosing items among strs[0] to strs[i] to form a subset, + what is the maximum size of this subset such that there are no more than m 0's and n 1's in this subset. + Each entry of dp[i][j][k] is initialized with 0 + + transition formula: + using x[i] to indicates the number of 0's in strs[i] + using y[i] to indicates the number of 1's in strs[i] + + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - x[i]][k - y[i]] + 1) + + */ + + + // num_of_zeros records the number of 0's for each str + // num_of_ones records the number of 1's for each str + // find the number of 0's and the number of 1's for each str in strs + vector num_of_zeros; + vector num_of_ones; + for (auto& str : strs){ + int count_of_zero = 0; + int count_of_one = 0; + for (char &c : str){ + if(c == '0') count_of_zero ++; + else count_of_one ++; + } + num_of_zeros.push_back(count_of_zero); + num_of_ones.push_back(count_of_one); + + } + + + // num_of_zeros[0] indicates the number of 0's for str[0] + // num_of_ones[0] indiates the number of 1's for str[1] + + // initialize the 1st plane of dp[i][j][k], i.e., dp[0][j][k] + // if num_of_zeros[0] > m or num_of_ones[0] > n, no need to further initialize dp[0][j][k], + // because they have been intialized to 0 previously + if(num_of_zeros[0] <= m && num_of_ones[0] <= n){ + // for j < num_of_zeros[0] or k < num_of_ones[0], dp[0][j][k] = 0 + for(int j = num_of_zeros[0]; j <= m; j++){ + for(int k = num_of_ones[0]; k <= n; k++){ + dp[0][j][k] = 1; + } + } + } + + /* if j - num_of_zeros[i] >= 0 and k - num_of_ones[i] >= 0: + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - num_of_zeros[i]][k - num_of_ones[i]] + 1) + else: + dp[i][j][k] = dp[i-1][j][k] + */ + + for (int i = 1; i < num_of_str; i++){ + int count_of_zeros = num_of_zeros[i]; + int count_of_ones = num_of_ones[i]; + for (int j = 0; j <= m; j++){ + for (int k = 0; k <= n; k++){ + if( j < count_of_zeros || k < count_of_ones){ + dp[i][j][k] = dp[i-1][j][k]; + }else{ + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - count_of_zeros][k - count_of_ones] + 1); + } + } + } + + } + + return dp[num_of_str-1][m][n]; + + } +}; +``` ## 总结 @@ -179,8 +259,70 @@ public: ## 其他语言版本 - ### Java + +三维DP数组实现 + +```java +class Solution { + public int findMaxForm(String[] strs, int m, int n) { + /// 数组有三个维度 + // 第一个维度:取前面的几个字符串 + // 第二个维度:0的数量限制(背包维度 1 容量) + // 第三个维度:1的数量限制(背包维度 2 容量) + int[][][] dpArr = new int[strs.length][m + 1][n + 1]; + + /// 初始化dpArr数组 + // 计算第一个字符串的零数量和1数量 + int zeroNum = 0; + int oneNum = 0; + for (char c : strs[0].toCharArray()) { + if (c == '0') { + zeroNum++; + } else { + oneNum++; + } + } + // 当0数量、1数量都容得下第一个字符串时,将DP数组的相应位置初始化为1,因为当前的子集数量为1 + for (int j = zeroNum; j <= m; j++) { + for (int k = oneNum; k <= n; k++) { + dpArr[0][j][k] = 1; + } + } + /// 依次填充加入第i个字符串之后的DP数组 + for (int i = 1; i < strs.length; i++) { + zeroNum = 0; + oneNum = 0; + for (char c : strs[i].toCharArray()) { + if (c == '0') { + zeroNum++; + } else { + oneNum++; + } + } + for (int j = 0; j <= m; j++) { + for (int k = 0; k <= n; k++) { + if (j >= zeroNum && k >= oneNum) { + // --if-- 当0数量维度和1数量维度的容量都大于等于当前字符串的0数量和1数量时,才考虑是否将当前字符串放入背包 + // 不放入第i个字符串,子集数量仍为 dpArr[i - 1][j][k] + // 放入第i个字符串,需要在0维度腾出 zeroNum 个容量,1维度腾出 oneNum 个容量,然后放入当前字符串,即 dpArr[i - 1][j - zeroNum][k - oneNum] + 1) + dpArr[i][j][k] = Math.max(dpArr[i - 1][j][k], dpArr[i - 1][j - zeroNum][k - oneNum] + 1); + } else { + // --if-- 无法放入第i个字符串,子集数量仍为 dpArr[i - 1][j][k] + dpArr[i][j][k] = dpArr[i - 1][j][k]; + } + } + } + } + return dpArr[dpArr.length - 1][m][n]; + } +} +``` + + + +二维DP数组实现 + ```Java class Solution { public int findMaxForm(String[] strs, int m, int n) { @@ -280,7 +422,7 @@ func max(a,b int) int { } ``` -### Javascript +### JavaScript ```javascript const findMaxForm = (strs, m, n) => { const dp = Array.from(Array(m+1), () => Array(n+1).fill(0)); @@ -533,7 +675,41 @@ impl Solution { } } ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int findMaxForm(char** strs, int strsSize, int m, int n) { + int dp[m + 1][n + 1]; + memset(dp, 0, sizeof (int ) * (m + 1) * (n + 1)); + for(int i = 0; i < strsSize; i++){ + // 统计0和1的数量 + int count0 = 0; + int count1 = 0; + char *str = strs[i]; + while (*str != '\0'){ + if(*str == '0'){ + count0++; + } else{ + count1++; + } + str++; + } + for(int j = m; j >= count0; j--){ + for(int k = n; k >= count1; k--){ + dp[j][k] = max(dp[j][k], dp[j - count0][k - count1] + 1); + } + } + } + return dp[m][n]; +} +``` + + + ### C# + ```csharp public class Solution { @@ -561,8 +737,5 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" "b/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 1aa69a3669..5d37737169 --- "a/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 和子集问题有点像,但又处处是陷阱 @@ -47,7 +45,7 @@ 为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图: -![491. 递增子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124200229824.png) +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201124200229824.png) @@ -81,7 +79,7 @@ if (path.size() > 1) { * 单层搜索逻辑 -![491. 递增子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124200229824-20230310131640070.png) +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201124200229824-20230310131640070.png) 在图中可以看出,**同一父节点下的同层上使用过的元素就不能再使用了** 那么单层搜索代码如下: @@ -206,7 +204,6 @@ public: ### Java ```Java -//using set, aligned with the unimproved method class Solution { List> result = new ArrayList<>(); List path = new ArrayList<>(); @@ -376,7 +373,7 @@ func dfs(nums []int, start int) { } ``` -### Javascript +### JavaScript ```Javascript @@ -641,7 +638,3 @@ public class Solution { } ``` -

- - - diff --git "a/problems/0494.\347\233\256\346\240\207\345\222\214.md" "b/problems/0494.\347\233\256\346\240\207\345\222\214.md" old mode 100644 new mode 100755 index e7a05d45c3..b161bc57a8 --- "a/problems/0494.\347\233\256\346\240\207\345\222\214.md" +++ "b/problems/0494.\347\233\256\346\240\207\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -49,7 +47,7 @@ * [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) * [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) -如果跟着「代码随想录」一起学过[回溯算法系列](https://programmercarl.com/回溯总结.html)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以爆搜出来。 +如果跟着「代码随想录」一起学过[回溯算法系列](https://programmercarl.com/回溯总结.html)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以暴搜出来。 事实确实如此,下面我也会给出相应的代码,只不过会超时。 @@ -61,7 +59,7 @@ left + right = sum,而sum是固定的。right = sum - left -公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。 +left - (sum - left) = target 推导出 left = (target + sum)/2 。 target是固定的,sum是固定的,left就可以求出来。 @@ -101,7 +99,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就是要求的和 // 以下为回溯法代码 @@ -118,9 +116,7 @@ public: 也可以使用记忆化回溯,但这里我就不在回溯上下功夫了,直接看动规吧 -### 动态规划 - -如何转化为01背包问题呢。 +### 动态规划 (二维dp数组) 假设加法的总和为x,那么减法对应的总和就是sum - x。 @@ -128,110 +124,342 @@ public: x = (target + sum) / 2 -**此时问题就转化为,装满容量为x的背包,有几种方法**。 +**此时问题就转化为,用nums装满容量为x的背包,有几种方法**。 这里的x,就是bagSize,也就是我们后面要求的背包容量。 -大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。 +大家看到`(target + sum) / 2` 应该担心计算的过程中向下取整有没有影响。 -这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以: +这么担心就对了,例如sum是5,target是2 的话其实就是无解的,所以: ```CPP (C++代码中,输入的S 就是题目描述的 target) -if ((S + sum) % 2 == 1) return 0; // 此时没有方案 +if ((target + sum) % 2 == 1) return 0; // 此时没有方案 ``` -同时如果 S的绝对值已经大于sum,那么也是没有方案的。 +同时如果target 的绝对值已经大于sum,那么也是没有方案的。 + ```CPP -(C++代码中,输入的S 就是题目描述的 target) -if (abs(S) > sum) return 0; // 此时没有方案 +if (abs(target) > sum) return 0; // 此时没有方案 ``` -再回归到01背包问题,为什么是01背包呢? - 因为每个物品(题目中的1)只用一次! 这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。 本题则是装满有几种方法。其实这就是一个组合问题了。 -1. 确定dp数组以及下标的含义 +#### 1. 确定dp数组以及下标的含义 -dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法 +先用 二维 dp数组求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。 -其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。 +01背包为什么这么定义dp数组,我在[0-1背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中 确定dp数组的含义里讲解过。 -下面我都是统一使用一维数组进行讲解, 二维降为一维(滚动数组),其实就是上一层拷贝下来,这个我在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)也有介绍。 +#### 2. 确定递推公式 -2. 确定递推公式 +我们先手动推导一下,这个二维数组里面的数值。 -有哪些来源可以推出dp[j]呢? +------------ -只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。 +先只考虑物品0,如图: -例如:dp[j],j 为5, +![](https://file1.kamacoder.com/i/algo/20240808161747.png) -* 已经有一个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的背包 +(这里的所有物品,都是题目中的数字1)。 -那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。 +装满背包容量为0 的方法个数是1,即 放0件物品。 -所以求组合类问题的公式,都是类似这种: +装满背包容量为1 的方法个数是1,即 放物品0。 + +装满背包容量为2 的方法个数是0,目前没有办法能装满容量为2的背包。 + +-------------- + +接下来 考虑 物品0 和 物品1,如图: + +![](https://file1.kamacoder.com/i/algo/20240808162052.png) + +装满背包容量为0 的方法个数是1,即 放0件物品。 + +装满背包容量为1 的方法个数是2,即 放物品0 或者 放物品1。 + +装满背包容量为2 的方法个数是1,即 放物品0 和 放物品1。 + +其他容量都不能装满,所以方法是0。 + +----------------- + +接下来 考虑 物品0 、物品1 和 物品2 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240808162533.png) + +装满背包容量为0 的方法个数是1,即 放0件物品。 + +装满背包容量为1 的方法个数是3,即 放物品0 或者 放物品1 或者 放物品2。 + +装满背包容量为2 的方法个数是3,即 放物品0 和 放物品1、放物品0 和 物品2、放物品1 和 物品2。 + +装满背包容量为3的方法个数是1,即 放物品0 和 物品1 和 物品2。 + +--------------- +通过以上举例,我们来看 dp[2][2] 可以有哪些方向推出来。 + +如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240808163312.png) + +dp[2][2] = 3,即 放物品0 和 放物品1、放物品0 和 物品 2、放物品1 和 物品2, 如图所示,三种方法: + +![](https://file1.kamacoder.com/i/algo/20240826111946.png) + +**容量为2 的背包,如果不放 物品2 有几种方法呢**? + +有 dp[1][2] 种方法,即 背包容量为2,只考虑物品0 和 物品1 ,有 dp[1][2] 种方法,如图: + +![](https://file1.kamacoder.com/i/algo/20240826112805.png) + +**容量为2 的背包, 如果放 物品2 有几种方法呢**? + +首先 要在背包里 先把物品2的容量空出来, 装满 刨除物品2容量 的背包 有几种方法呢? + +刨除物品2容量后的背包容量为 1。 + +此时装满背包容量为1 有 dp[1][1] 种方法,即: 不放物品2,背包容量为1,只考虑物品 0 和 物品 1,有 dp[1][1] 种方法。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240826113043.png) + +有录友可能疑惑,这里计算的是放满 容量为2的背包 有几种方法,那物品2去哪了? + +在上面图中,你把物品2补上就好,同样是两种方法。 + +dp[2][2] = 容量为2的背包不放物品2有几种方法 + 容量为2的背包放物品2有几种方法 + +所以 dp[2][2] = dp[1][2] + dp[1][1] ,如图: + +![](https://file1.kamacoder.com/i/algo/20240826113258.png) + +以上过程,抽象化如下: + +* **不放物品i**:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。 + +* **放物品i**: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。 + +本题中,物品i的容量是nums[i],价值也是nums[i]。 + +递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; + +考到这个递推公式,我们应该注意到,`j - nums[i]` 作为数组下标,如果 `j - nums[i]` 小于零呢? + +说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,即:dp[i][j] = dp[i - 1][j]; + +所以递推公式: + +```CPP +if (nums[i] > j) dp[i][j] = dp[i - 1][j]; +else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; ``` -dp[j] += dp[j - nums[i]] + +#### 3. dp数组如何初始化 + +先明确递推的方向,如图,求解 dp[2][2] 是由 上方和左上方推出。 + +![](https://file1.kamacoder.com/i/algo/20240826115800.png) + +那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240827103507.png) + +关于dp[0][0]的值,在上面的递推公式讲解中已经讲过,装满背包容量为0 的方法数量是1,即 放0件物品。 + +那么最上行dp[0][j] 如何初始化呢? + +dp[0][j]:只放物品0, 把容量为j的背包填满有几种方法。 + +只有背包容量为 物品0 的容量的时候,方法为1,正好装满。 + +其他情况下,要不是装不满,要不是装不下。 + +所以初始化:dp[0][nums[0]] = 1 ,其他均为0 。 + +表格最左列也要初始化,dp[i][0] : 背包容量为0, 放物品0 到 物品i,装满有几种方法。 + +都是有一种方法,就是放0件物品。 + +即 dp[i][0] = 1 + +但这里有例外,就是如果 物品数值就是0呢? + +如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。 + +* 放0件物品 +* 放物品0 +* 放物品1 +* 放物品0 和 物品1 + +此时是有4种方法。 + +其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。 + +初始化如下: + +```CPP +int numZero = 0; +for (int i = 0; i < nums.size(); i++) { + if (nums[i] == 0) numZero++; + dp[i][0] = (int) pow(2.0, numZero); +} ``` -**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!** +#### 4. 确定遍历顺序 -3. dp数组如何初始化 +在明确递推方向时,我们知道 当前值 是由上方和左上方推出。 -从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。 +那么我们的遍历顺序一定是 从上到下,从左到右。 -这里有录友可能认为从dp数组定义来说 dp[0] 应该是0,也有录友认为dp[0]应该是1。 +因为只有这样,我们才能基于之前的数值做推导。 -其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看应该等于多少。 +例如下图,如果上方没数值,左上方没数值,就无法推出 dp[2][2]。 -如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。 +![](https://file1.kamacoder.com/i/algo/20240827105427.png) -所以本题我们应该初始化 dp[0] 为 1。 +那么是先 从上到下 ,再从左到右遍历,例如这样: -可能有同学想了,那 如果是 数组[0,0,0,0,0] target = 0 呢。 +```CPP +for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + } +} +``` -其实 此时最终的dp[0] = 32,也就是这五个零 子集的所有组合情况,但此dp[0]非彼dp[0],dp[0]能算出32,其基础是因为dp[0] = 1 累加起来的。 +还是先 从左到右,再从上到下呢,例如这样: -dp[j]其他下标对应的数值也应该初始化为0,从递推公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。 +```CPP +for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + } +} +``` + +**其实以上两种遍历都可以**! (但仅针对二维DP数组是这样的) + +这一点我在 [01背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中的 遍历顺序部分讲过。 + +这里我再画图讲一下,以求dp[2][2]为例,当先从上到下,再从左到右遍历,矩阵是这样: + +![](https://file1.kamacoder.com/i/algo/20240827110933.png) +当先从左到右,再从上到下遍历,矩阵是这样: -4. 确定遍历顺序 +![](https://file1.kamacoder.com/i/algo/20240827111013.png) -在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。 +这里大家可以看出,无论是以上哪种遍历,都不影响 dp[2][2]的求值,用来 推导 dp[2][2] 的数值都在。 -5. 举例推导dp数组 +#### 5. 举例推导dp数组 -输入:nums: [1, 1, 1, 1, 1], S: 3 +输入:nums: [1, 1, 1, 1, 1], target: 3 -bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4 +bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4 dp数组状态变化如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210125120743274.jpg) +![](https://file1.kamacoder.com/i/algo/20240827111612.png) -C++代码如下: +这么大的矩阵,我们是可以自己手动模拟出来的。 + +在模拟的过程中,既可以帮我们寻找规律,也可以帮我们验证 递推公式加遍历顺序是不是按照我们想象的结果推进的。 + + +最后二维dp数组的C++代码如下: + +```CPP +class Solution { +public: + int findTargetSumWays(vector& nums, int target) { + int sum = 0; + for (int i = 0; i < nums.size(); i++) sum += nums[i]; + if (abs(target) > sum) return 0; // 此时没有方案 + if ((target + sum) % 2 == 1) return 0; // 此时没有方案 + int bagSize = (target + sum) / 2; + + vector> dp(nums.size(), vector(bagSize + 1, 0)); + + // 初始化最上行 + if (nums[0] <= bagSize) dp[0][nums[0]] = 1; + + // 初始化最左列,最左列其他数值在递推公式中就完成了赋值 + dp[0][0] = 1; + + int numZero = 0; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] == 0) numZero++; + dp[i][0] = (int) pow(2.0, numZero); + } + + // 以下遍历顺序行列可以颠倒 + for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + if (nums[i] > j) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; + } + } + return dp[nums.size() - 1][bagSize]; + } +}; +``` + +### 动态规划 (一维dp数组) + +将二维dp数组压缩成一维dp数组,我们在 [01背包理论基础(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) 讲过滚动数组,原理是一样的,即重复利用每一行的数值。 + +既然是重复利用每一行,就是将二维数组压缩成一行。 + +dp[i][j] 去掉 行的维度,即 dp[j],表示:填满j(包括j)这么大容积的包,有dp[j]种方法。 + +#### 2. 确定递推公式 + +二维DP数组递推公式: `dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];` + +去掉维度i 之后,递推公式:`dp[j] = dp[j] + dp[j - nums[i]]` ,即:`dp[j] += dp[j - nums[i]]` + +**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!** + +#### 3. dp数组如何初始化 + +在上面 二维dp数组中,我们讲解过 dp[0][0] 初始为1,这里dp[0] 同样初始为1 ,即装满背包为0的方法有一种,放0件物品。 + +#### 4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们系统讲过对于01背包问题一维dp的遍历。 + +遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。 + +#### 5. 举例推导dp数组 + +输入:nums: [1, 1, 1, 1, 1], target: 3 + +bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4 + +dp数组状态变化如下: + +![](https://file1.kamacoder.com/i/algo/20210125120743274.jpg) + +大家可以和 二维dp数组的打印结果做一下对比。 + +一维DP的C++代码如下: ```CPP class Solution { public: - int findTargetSumWays(vector& nums, int S) { + int findTargetSumWays(vector& nums, int target) { int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; - if (abs(S) > sum) return 0; // 此时没有方案 - if ((S + sum) % 2 == 1) return 0; // 此时没有方案 - int bagSize = (S + sum) / 2; + if (abs(target) > sum) return 0; // 此时没有方案 + if ((target + sum) % 2 == 1) return 0; // 此时没有方案 + int bagSize = (target + sum) / 2; vector dp(bagSize + 1, 0); dp[0] = 1; for (int i = 0; i < nums.size(); i++) { @@ -248,26 +476,53 @@ public: * 空间复杂度:O(m),m为背包容量 -## 总结 -此时 大家应该不禁想起,我们之前讲过的[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)是不是应该也可以用dp来做啊? +### 拓展 -是的,如果仅仅是求个数的话,就可以用dp,但[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)要求的是把所有组合列出来,还是要使用回溯法爆搜的。 +关于一维dp数组的递推公式解释,也可以从以下维度来理解。 (**但还是从二维DP数组到一维DP数组这样更容易理解一些**) -本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为: +2. 确定递推公式 -```CPP -dp[j] += dp[j - nums[i]]; +有哪些来源可以推出dp[j]呢? + +只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。 + +例如:dp[j],j 为5, + +* 已经有一个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的背包 + +那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。 + +所以求组合类问题的公式,都是类似这种: + +``` +dp[j] += dp[j - nums[i]] ``` -后面我们在讲解完全背包的时候,还会用到这个递推公式! +## 总结 +此时 大家应该不禁想起,我们之前讲过的[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)是不是应该也可以用dp来做啊? +是可以求的,如果仅仅是求个数的话,就可以用dp,但[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)要求的是把所有组合列出来,还是要使用回溯法暴搜的。 +本题还是有点难度,理解上从二维DP数组更容易理解,做题上直接用一维DP更简洁一些。 -## 其他语言版本 +大家可以选择哪种方式自己更容易理解。 + +在后面得题目中,在求装满背包有几种方法的情况下,递推公式一般为: + +```CPP +dp[j] += dp[j - nums[i]]; +``` + +我们在讲解完全背包的时候,还会用到这个递推公式! +## 其他语言版本 ### Java ```java @@ -359,13 +614,6 @@ class Solution { } } - // 打印dp数组 - // for(int i = 0; i < nums.length; i++) { - // for(int j = 0; j <= left; j++) { - // System.out.print(dp[i][j] + " "); - // } - // System.out.println(""); - // } return dp[nums.length - 1][left]; @@ -420,18 +668,26 @@ class Solution: # 创建二维动态规划数组,行表示选取的元素数量,列表示累加和 dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)] + dp = [[0] * (target_sum + 1) for _ in range(len(nums))] # 初始化状态 dp[0][0] = 1 + if nums[0] <= target_sum: + dp[0][nums[0]] = 1 + numZero = 0 + for i in range(len(nums)): + if nums[i] == 0: + numZero += 1 + dp[i][0] = int(math.pow(2, numZero)) # 动态规划过程 - for i in range(1, len(nums) + 1): + for i in range(1, len(nums)): for j in range(target_sum + 1): dp[i][j] = dp[i - 1][j] # 不选取当前元素 if j >= nums[i - 1]: - dp[i][j] += dp[i - 1][j - nums[i - 1]] # 选取当前元素 + dp[i][j] += dp[i - 1][j - nums[i]] # 选取当前元素 - return dp[len(nums)][target_sum] # 返回达到目标和的方案数 + return dp[len(nums)-1][target_sum] # 返回达到目标和的方案数 ``` @@ -455,6 +711,82 @@ class Solution: ``` ### Go +回溯法思路 +```go +func findTargetSumWays(nums []int, target int) int { + var result int + var backtracking func(nums []int, target int, index int, currentSum int) + + backtracking = func(nums []int, target int, index int, currentSum int) { + if index == len(nums) { + if currentSum == target { + result++ + } + return + } + + // 选择加上当前数字 + backtracking(nums, target, index+1, currentSum+nums[index]) + + // 选择减去当前数字 + backtracking(nums, target, index+1, currentSum-nums[index]) + } + + backtracking(nums, target, 0, 0) + return result +} +``` +二维dp +```go +func findTargetSumWays(nums []int, target int) int { + sum := 0 + for _, v := range nums { + sum += v + } + if math.Abs(float64(target)) > float64(sum) { + return 0 // 此时没有方案 + } + if (target + sum) % 2 == 1 { + return 0 // 此时没有方案 + } + bagSize := (target + sum) / 2 + + dp := make([][]int, len(nums)) + for i := range dp { + dp[i] = make([]int, bagSize + 1) + } + + // 初始化最上行 + if nums[0] <= bagSize { + dp[0][nums[0]] = 1 + } + + // 初始化最左列,最左列其他数值在递推公式中就完成了赋值 + dp[0][0] = 1 + + var numZero float64 + for i := range nums { + if nums[i] == 0 { + numZero++ + } + dp[i][0] = int(math.Pow(2, numZero)) + } + + // 以下遍历顺序行列可以颠倒 + for i := 1; i < len(nums); i++ { // 行,遍历物品 + for j := 0; j <= bagSize; j++ { // 列,遍历背包 + if nums[i] > j { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]] + } + } + } + return dp[len(nums)-1][bagSize] +} +``` + +一维dp ```go func findTargetSumWays(nums []int, target int) int { sum := 0 @@ -489,32 +821,71 @@ func abs(x int) int { } ``` -### Javascript +### JavaScript ```javascript +/** + * 题目来源: {@link https://leetcode.cn/problems/target-sum/} + * + * 题解来源: {@link https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE} + * + * 时间复杂度: O(n * C), C 为数组元素总和与目标值之和的一半 + * + * 空间复杂度: O(C) + * + * @param { number[] } nums + * @param { number } target + * @return { number } + */ const findTargetSumWays = (nums, target) => { - - const sum = nums.reduce((a, b) => a+b); + // 原题目可转化为: + // + // 将所有元素划分为 2 个集合, + // 一个集合中包含所有要添加 "+" 号的元素, 一个集合中包含所有要添加 "-" 号的元素 + // + // 设两个集合的元素和分别为 positive 和 negative, 所有元素总和为 sum, 那么有如下等式: + // positive + negative = sum (1) + // positive - negative = target (2) + // (1) 与 (2) 联立可得: positive = (sum + target) / 2, + // 所以如果能从原数组中取出若干个元素形成 1 个元素总和为 (sum + target) / 2 的集合, + // 就算得到了 1 种满足题意的组合方法 + // + // 因此, 所求变为: 有多少种取法, 可使得容量为 (sum + target) / 2 的背包被装满? + + const sum = nums.reduce((a, b) => a + b); - if(Math.abs(target) > sum) { + if (Math.abs(target) > sum) { return 0; } - if((target + sum) % 2) { + if ((target + sum) % 2) { return 0; } - const halfSum = (target + sum) / 2; - - let dp = new Array(halfSum+1).fill(0); + const bagWeight = (target + sum) / 2; + + // 1. dp 数组的含义 + // dp[j]: 装满容量为 j 的背包, 有 dp[j] 种方法 + let dp = new Array(bagWeight + 1).fill(0); + + // 2. 递推公式 + // dp[j] = Σ(dp[j - nums[j]]), (j ∈ [0, j] 且 j >= nums[j]) + // 因为 dp[j - nums[j]] 表示: 装满容量为 j - nums[j] 背包有 dp[j - nums[j]] 种方法 + // 而容量为 j - nums[j] 的背包只需要再将 nums[j] 放入背包就能使得背包容量达到 j + // 因此, 让背包容量达到 j 有 Σ(dp[j - nums[j]]) 种方法 + + // 3. dp 数组如何初始化 + // dp[0] = 1, dp[1 ~ bagWeight] = 0 dp[0] = 1; - - for(let i = 0; i < nums.length; i++) { - for(let j = halfSum; j >= nums[i]; j--) { + + // 4. 遍历顺序 + // 先物品后背包, 物品从前往后遍历, 背包容量从后往前遍历 + for (let i = 0; i < nums.length; i++) { + for (let j = bagWeight; j >= nums[i]; j--) { dp[j] += dp[j - nums[i]]; } } - return dp[halfSum]; + return dp[bagWeight]; }; ``` @@ -585,7 +956,44 @@ impl Solution { } } ``` +### C + +```c +int getSum(int * nums, int numsSize){ + int sum = 0; + for(int i = 0; i < numsSize; i++){ + sum += nums[i]; + } + return sum; +} + +int findTargetSumWays(int* nums, int numsSize, int target) { + int sum = getSum(nums, numsSize); + int diff = sum - target; + // 两种情况不满足 + if(diff < 0 || diff % 2 != 0){ + return 0; + } + int bagSize = diff / 2; + int dp[numsSize + 1][bagSize + 1]; + dp[0][0] = 1; + for(int i = 1; i <= numsSize; i++){ + int num = nums[i - 1]; + for(int j = 0; j <= bagSize; j++){ + dp[i][j] = dp[i - 1][j]; + if(j >= num){ + dp[i][j] += dp[i - 1][j - num]; + } + } + } + return dp[numsSize][bagSize]; +} +``` + + + ### C# + ```csharp public class Solution { @@ -613,8 +1021,5 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" "b/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" old mode 100644 new mode 100755 index d97a3e8482..628149b75d --- "a/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" +++ "b/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 496.下一个更大元素 I @@ -195,6 +193,62 @@ public: 建议大家把情况一二三想清楚了,先写出版本一的代码,然后在其基础上在做精简! ## 其他语言版本 + +### C + +``` C +/* 先用单调栈的方法计算出结果,再根据nums1中的元素去查找对应的结果 */ +/** + * Note: The returned array must be malloced, assume caller calls free(). + */ +int* nextGreaterElement(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) { + + /* stcak */ + int top = -1; + int stack_len = nums2Size; + int stack[stack_len]; + //memset(stack, 0x00, sizeof(stack)); + + /* nums2 result */ + int* result_nums2 = (int *)malloc(sizeof(int) * nums2Size); + //memset(result_nums2, 0x00, sizeof(int) * nums2Size); + + /* result */ + int* result = (int *)malloc(sizeof(int) * nums1Size); + //memset(result, 0x00, sizeof(int) * nums1Size); + *returnSize = nums1Size; + + /* init */ + stack[++top] = 0; /* stack loaded with array subscripts */ + + for (int i = 0; i < nums2Size; i++) { + result_nums2[i] = -1; + } + + /* get the result_nums2 */ + for (int i = 1; i < nums2Size; i++) { + if (nums2[i] <= nums2[stack[top]]) { + stack[++top] = i; /* push */ + } else { + while ((top >= 0) && (nums2[i] > nums2[stack[top]])) { + result_nums2[stack[top]] = nums2[i]; + top--; /* pop */ + } + stack[++top] = i; + } + } + + /* get the result */ + for (int i = 0; i < nums1Size; i++) { + for (int j = 0; j < nums2Size; j++) { + if (nums1[i] == nums2[j]) { + result[i] = result_nums2[j]; + } + } + } + return result; +} +``` ### Java ```java @@ -450,8 +504,4 @@ impl Solution { -

- - - diff --git "a/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" "b/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" old mode 100644 new mode 100755 index 20627d1ad6..457fd61d24 --- "a/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" +++ "b/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 二叉树上应该怎么求,二叉搜索树上又应该怎么求? @@ -25,7 +23,7 @@ 给定 BST [1,null,2,2], -![501. 二叉搜索树中的众数](https://code-thinking-1253855093.file.myqcloud.com/pics/20201014221532206.png) +![501. 二叉搜索树中的众数](https://file1.kamacoder.com/i/algo/20201014221532206.png) 返回[2]. @@ -35,7 +33,7 @@ ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[不仅双指针,还有代码技巧可以惊艳到你! | LeetCode:501.二叉搜索树中的众数](https://www.bilibili.com/video/BV1fD4y117gp),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[不仅双指针,还有代码技巧可以惊艳到你! | LeetCode:501.二叉搜索树中的众数](https://www.bilibili.com/video/BV1fD4y117gp),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -146,7 +144,7 @@ public: 如图: -![501.二叉搜索树中的众数1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204152758889.png) +![501.二叉搜索树中的众数1](https://file1.kamacoder.com/i/algo/20210204152758889.png) 中序遍历代码如下: @@ -1051,8 +1049,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" "b/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" old mode 100644 new mode 100755 index 6df83fb208..93924483f3 --- "a/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" +++ "b/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 503.下一个更大元素II @@ -168,6 +166,7 @@ class Solution { ``` ### Python: +> 版本一: ```python class Solution: @@ -181,9 +180,71 @@ class Solution: stack.append(i%len(nums)) return dp ``` + +> 版本二:针对版本一的优化 + +```python +class Solution: + def nextGreaterElements(self, nums: List[int]) -> List[int]: + res = [-1] * len(nums) + stack = [] + #第一次遍历nums + for i, num in enumerate(nums): + while stack and num > nums[stack[-1]]: + res[stack[-1]] = num + stack.pop() + stack.append(i) + #此时stack仍有剩余,有部分数‘无下一个更大元素’待修正 + #第二次遍历nums + for num in nums: + #一旦stack为空,就表明所有数都有下一个更大元素,可以返回结果 + if not stack: + return res + while stack and num > nums[stack[-1]]: + res[stack[-1]] = num + stack.pop() + #不要将已经有下一个更大元素的数加入栈,这样会重复赋值,只需对第一次遍历剩余的数再尝试寻找下一个更大元素即可 + #最后仍有部分最大数无法有下一个更大元素,返回结果 + return res +``` + ### Go: ```go +// 版本一 +func nextGreaterElements(nums []int) []int { + // 拼接一个新的nums + numsNew := make([]int, len(nums) * 2) + copy(numsNew, nums) + copy(numsNew[len(nums):], nums) + // 用新的nums大小来初始化result + result := make([]int, len(numsNew)) + for i := range result { + result[i] = -1 + } + + // 开始单调栈 + st := []int{0} + for i := 1; i < len(numsNew); i++ { + if numsNew[i] < numsNew[st[len(st)-1]] { + st = append(st, i) + } else if numsNew[i] == numsNew[st[len(st)-1]] { + st = append(st, i) + } else { + for len(st) > 0 && numsNew[i] > numsNew[st[len(st)-1]] { + result[st[len(st)-1]] = numsNew[i] + st = st[:len(st)-1] + } + st = append(st, i) + } + } + result = result[:len(result)/2] + return result +} +``` + +```go +// 版本二 func nextGreaterElements(nums []int) []int { length := len(nums) result := make([]int,length) @@ -203,7 +264,6 @@ func nextGreaterElements(nums []int) []int { return result } ``` - ### JavaScript: ```JS @@ -295,8 +355,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" "b/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" old mode 100644 new mode 100755 index 71c022bd34..b2e56a613c --- "a/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" +++ "b/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 509. 斐波那契数 @@ -151,7 +149,7 @@ public: * 时间复杂度:O(2^n) * 空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间 -这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html) +这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:[通过一道面试题目,讲一讲递归算法的时间复杂度!](./前序/递归算法的时间复杂度.md) ## 总结 @@ -292,7 +290,7 @@ func fib(n int) int { return c } ``` -### Javascript +### JavaScript 解法一 ```Javascript var fib = function(n) { @@ -475,8 +473,4 @@ public class Solution -

- - - diff --git "a/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" "b/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" old mode 100644 new mode 100755 index d897bba13a..03f076a2c9 --- "a/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" +++ "b/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 513.找树左下角的值 @@ -14,11 +12,11 @@ 示例 1: -![513.找树左下角的值](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204152956836.png) +![513.找树左下角的值](https://file1.kamacoder.com/i/algo/20210204152956836.png) 示例 2: -![513.找树左下角的值1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204153017586.png) +![513.找树左下角的值1](https://file1.kamacoder.com/i/algo/20210204153017586.png) ## 算法公开课 @@ -55,7 +53,7 @@ 参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。 -本题还需要类里的两个全局变量,maxLen用来记录最大深度,result记录最大深度最左节点的数值。 +本题还需要类里的两个全局变量,maxDepth用来记录最大深度,result记录最大深度最左节点的数值。 代码如下: @@ -764,7 +762,3 @@ public class Solution // @lc code=end ``` -

- - - diff --git "a/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" "b/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 44bdec1f19..882c36bb05 --- "a/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 516.最长回文子序列 @@ -58,7 +56,7 @@ 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2; 如图: -![516.最长回文子序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20210127151350563.jpg) +![516.最长回文子序列](https://file1.kamacoder.com/i/algo/20210127151350563.jpg) (如果这里看不懂,回忆一下dp[i][j]的定义) @@ -70,7 +68,7 @@ 那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); -![516.最长回文子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210127151420476.jpg) +![516.最长回文子序列1](https://file1.kamacoder.com/i/algo/20210127151420476.jpg) 代码如下: @@ -99,7 +97,7 @@ for (int i = 0; i < s.size(); i++) dp[i][i] = 1; 从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230102172155.png) +![](https://file1.kamacoder.com/i/algo/20230102172155.png) **所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的**。 @@ -123,7 +121,7 @@ for (int i = s.size() - 1; i >= 0; i--) { 输入s:"cbbd" 为例,dp数组状态如图: -![516.最长回文子序列3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210127151521432.jpg) +![516.最长回文子序列3](https://file1.kamacoder.com/i/algo/20210127151521432.jpg) 红色框即:dp[0][s.size() - 1]; 为最终结果。 @@ -224,7 +222,7 @@ func longestPalindromeSubseq(s string) int { } ``` -### Javascript: +### JavaScript: ```javascript const longestPalindromeSubseq = (s) => { @@ -298,8 +296,4 @@ impl Solution { ``` -

- - - diff --git "a/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" "b/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" old mode 100644 new mode 100755 index da1c475565..7e4bbb9a81 --- "a/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" +++ "b/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" @@ -1,10 +1,6 @@ -

- - - -

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

- - +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 518.零钱兑换II @@ -45,15 +41,19 @@ **[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II](https://www.bilibili.com/video/BV1KM411k75j/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 +## 二维dp讲解 +如果大家认真做完:[分割等和子集](https://www.programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html) , [最后一块石头的重量II](https://www.programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html) 和 [目标和](https://www.programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html) +应该会知道类似这种题目:给出一个总数,一些物品,问能否凑成这个总数。 -## 思路 +这是典型的背包问题! -这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。 +本题求的是装满这个背包的物品组合数是多少。 +因为每一种面额的硬币有无限个,所以这是完全背包。 -对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) +对完全背包还不了解的同学,可以看这篇:[完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 但本题和纯完全背包不一样,**纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!** @@ -69,44 +69,182 @@ 如果问的是排列数,那么上面就是两种排列了。 -**组合不强调元素之间的顺序,排列强调元素之间的顺序**。 其实这一点我们在讲解回溯算法专题的时候就讲过了哈。 +**组合不强调元素之间的顺序,排列强调元素之间的顺序**。 其实这一点我们在讲解回溯算法专题的时候就讲过。 那我为什么要介绍这些呢,因为这和下文讲解遍历顺序息息相关! -回归本题,动规五步曲来分析如下: +本题其实与我们讲过 [494. 目标和](https://programmercarl.com/0494.目标和.html) 十分类似。 -1. 确定dp数组以及下标的含义 +[494. 目标和](https://programmercarl.com/0494.目标和.html) 求的是装满背包有多少种方法,而本题是求装满背包有多少种组合。 -dp[j]:凑成总金额j的货币组合数为dp[j] +这有啥区别? -2. 确定递推公式 +**求装满背包有几种方法其实就是求组合数**。 不过 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是 01背包,即每一类物品只有一个。 -dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。 +以下动规五部曲: -所以递推公式:dp[j] += dp[j - coins[i]]; +### 1、确定dp数组以及下标的含义 -**这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇[494. 目标和](https://programmercarl.com/0494.目标和.html)中就讲解了,求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];** +定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。 -3. dp数组如何初始化 +很多录友也会疑惑,凭什么上来就定义 dp数组,思考过程是什么样的, 这个思考过程我在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 “确定dp数组以及下标的含义” 有详细讲解。 -首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。 +(**强烈建议按照代码随想录的顺序学习,否则可能看不懂我的讲解**) -那么 dp[0] = 1 有没有含义,其实既可以说 凑成总金额0的货币组合数为1,也可以说 凑成总金额0的货币组合数为0,好像都没有毛病。 -但题目描述中,也没明确说 amount = 0 的情况,结果应该是多少。 +### 2、确定递推公式 -这里我认为题目描述还是要说明一下,因为后台测试数据是默认,amount = 0 的情况,组合数为1的。 +> **注意**: 这里的公式推导,与之前讲解过的 [494. 目标和](https://programmercarl.com/0494.目标和.html) 、[完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 有极大重复,所以我不在重复讲解原理,而是只讲解区别。 -下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j] +我们再回顾一下,[01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html),中二维DP数组的递推公式为: -dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。 +`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])` -4. 确定遍历顺序 +在 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 详细讲解了完全背包二维DP数组的递推公式为: -本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢? +`dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])` + + +看去完全背包 和 01背包的差别在哪里? + +在于01背包是 `dp[i - 1][j - weight[i]] + value[i]` ,完全背包是 `dp[i][j - weight[i]] + value[i])` + +主要原因就是 完全背包单类物品有无限个。 + +具体原因我在 [完全背包理论基础(二维)](https://programmercarl.com/背包问题理论基础完全背包.html) 的 「确定递推公式」有详细讲解,如果大家忘了,再回顾一下。 + +我上面有说过,本题和 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是一样的,唯一区别就是 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是 01背包,本题是完全背包。 + + +在[494. 目标和](https://programmercarl.com/0494.目标和.html)中详解讲解了装满背包有几种方法,二维DP数组的递推公式: +`dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]` + +所以本题递推公式:`dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]` ,区别依然是 ` dp[i - 1][j - nums[i]]` 和 `dp[i][j - nums[i]]` + +这个 ‘所以’ 我省略了很多推导的内容,因为这些内容在 [494. 目标和](https://programmercarl.com/0494.目标和.html) 和 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 都详细讲过。 + +这里不再重复讲解。 + +大家主要疑惑点 + +1、 `dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]` 这个递归公式框架怎么来的,在 [494. 目标和](https://programmercarl.com/0494.目标和.html) 有详细讲解。 + +2、为什么是 ` dp[i][j - nums[i]]` 而不是 ` dp[i - 1][j - nums[i]]` ,在[完全背包理论基础(二维)](https://programmercarl.com/背包问题理论基础完全背包.html) 有详细讲解 + + +### 3. dp数组如何初始化 + +那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240827103507.png) + + +这里首先要关注的就是 dp[0][0] 应该是多少? + +背包空间为0,装满「物品0」 的组合数有多少呢? + +应该是 0 个, 但如果 「物品0」 的 数值就是0呢? 岂不是可以有无限个0 组合 和为0! + +题目描述中说了`1 <= coins.length <= 300` ,所以不用考虑 物品数值为0的情况。 + +那么最上行dp[0][j] 如何初始化呢? + +dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。 (如果看不懂dp数组的含义,建议先学习[494. 目标和](https://programmercarl.com/0494.目标和.html)) + +如果 j 可以整除 物品0,那么装满背包就有1种组合方法。 + +初始化代码: +```CPP +for (int j = 0; j <= bagSize; j++) { + if (j % coins[0] == 0) dp[0][j] = 1; +} +``` + +最左列如何初始化呢? + +dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。 + +都有一种方法,即不装。 + +所以 dp[i][0] 都初始化为1 + +### 4. 确定遍历顺序 + +二维DP数组的完全背包的两个for循环先后顺序是无所谓的。 -我在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中讲解了完全背包的两个for循环的先后顺序都是可以的。 +先遍历背包,还是先遍历物品都是可以的。 + +原理和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 「遍历顺序」是一样的,都是因为 两个for循环的先后顺序不影响 递推公式 所需要的数值。 + +具体分析过程看 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 「遍历顺序」 + +### 5. 打印DP数组 + +以amount为5,coins为:[2,3,5] 为例: + +dp数组应该是这样的: + +``` +1 0 1 0 1 0 +1 0 1 1 1 1 +1 0 1 1 1 2 +``` + +### 代码实现: + +```CPP +class Solution { +public: + int change(int amount, vector& coins) { + int bagSize = amount; + + vector> dp(coins.size(), vector(bagSize + 1, 0)); + + // 初始化最上行 + for (int j = 0; j <= bagSize; j++) { + if (j % coins[0] == 0) dp[0][j] = 1; + } + // 初始化最左列 + for (int i = 0; i < coins.size(); i++) { + dp[i][0] = 1; + } + // 以下遍历顺序行列可以颠倒 + for (int i = 1; i < coins.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + if (coins[i] > j) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]; + } + } + return dp[coins.size() - 1][bagSize]; + } +}; +``` + +## 一维dp讲解 + +### 1、确定dp数组以及下标的含义 + +dp[j]:凑成总金额j的货币组合数为dp[j] + +### 2、确定递推公式 + +本题 二维dp 递推公式: `dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]` + +压缩成一维:`dp[j] += dp[j - coins[i]]` + +这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇[494. 目标和](https://programmercarl.com/0494.目标和.html)中就讲解了,求装满背包有几种方法,公式都是:`dp[j] += dp[j - nums[i]]` + +### 3. dp数组如何初始化 + +装满背包容量为0 的方法是1,即不放任何物品,`dp[0] = 1` + +### 4. 确定遍历顺序 + + +本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢? + +我在[完全背包(一维DP)](./背包问题完全背包一维.md)中讲解了完全背包的两个for循环的先后顺序都是可以的。 **但本题就不行了!** @@ -116,7 +254,7 @@ dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coi 所以纯完全背包是能凑成总和就行,不用管怎么凑的。 -本题是求凑出来的方案个数,且每个方案个数是为组合数。 +本题是求凑出来的方案个数,且每个方案个数是组合数。 那么本题,两个for循环的先后顺序可就有说法了。 @@ -154,11 +292,11 @@ for (int j = 0; j <= amount; j++) { // 遍历背包容量 可能这里很多同学还不是很理解,**建议动手把这两种方案的dp数组数值变化打印出来,对比看一看!(实践出真知)** -5. 举例推导dp数组 +### 5. 举例推导dp数组 输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下: -![518.零钱兑换II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210120181331461.jpg) +![518.零钱兑换II](https://file1.kamacoder.com/i/algo/20210120181331461.jpg) 最后红色框dp[amount]为最终结果。 @@ -168,27 +306,57 @@ for (int j = 0; j <= amount; j++) { // 遍历背包容量 class Solution { public: int change(int amount, vector& coins) { - vector dp(amount + 1, 0); - dp[0] = 1; + vector dp(amount + 1, 0); // 防止相加数据超int + dp[0] = 1; // 只有一种方式达到0 for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int j = coins[i]; j <= amount; j++) { // 遍历背包 dp[j] += dp[j - coins[i]]; } } - return dp[amount]; + return dp[amount]; // 返回组合数 } }; ``` +C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 + * 时间复杂度: O(mn),其中 m 是amount,n 是 coins 的长度 * 空间复杂度: O(m) +为了防止相加的数据 超int 也可以这么写: + +```CPP +class Solution { +public: + int change(int amount, vector& coins) { + vector dp(amount + 1, 0); + dp[0] = 1; // 只有一种方式达到0 + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包 + if (dp[j] < INT_MAX - dp[j - coins[i]]) { //防止相加数据超int + dp[j] += dp[j - coins[i]]; + } + } + } + return dp[amount]; // 返回组合数 + } +}; +``` -是不是发现代码如此精简 ## 总结 -本题的递推公式,其实我们在[494. 目标和](https://programmercarl.com/0494.目标和.html)中就已经讲过了,**而难点在于遍历顺序!** +本题我们从 二维 分析到 一维。 + +大家在刚开始学习的时候,从二维开始学习 容易理解。 + +之后,推荐大家直接掌握一维的写法,熟练后更容易写出来。 + +本题中,二维dp主要是就要 想清楚和我们之前讲解的 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html)、[494. 目标和](https://programmercarl.com/0494.目标和.html)、 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 联系与区别。 + +这也是代码随想录安排刷题顺序的精髓所在。 + +本题的一维dp中,难点在于理解便利顺序。 在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。 @@ -196,8 +364,7 @@ public: **如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 -可能说到排列数录友们已经有点懵了,后面Carl还会安排求排列数的题目,到时候在对比一下,大家就会发现神奇所在! - +可能说到排列数录友们已经有点懵了,后面我还会安排求排列数的题目,到时候在对比一下,大家就会发现神奇所在! ## 其他语言版本 @@ -268,6 +435,7 @@ class Solution: ### Go: +一维dp ```go func change(amount int, coins []int) int { // 定义dp数组 @@ -286,6 +454,29 @@ func change(amount int, coins []int) int { return dp[amount] } ``` +二维dp +```go +func change(amount int, coins []int) int { + dp := make([][]int, len(coins)) + for i := range dp { + dp[i] = make([]int, amount + 1) + dp[i][0] = 1 + } + for j := coins[0]; j <= amount; j++ { + dp[0][j] += dp[0][j-coins[0]] + } + for i := 1; i < len(coins); i++ { + for j := 1; j <= amount; j++ { + if j < coins[i] { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j] + } + } + } + return dp[len(coins)-1][amount] +} +``` ### Rust: @@ -305,7 +496,7 @@ impl Solution { } ``` -### Javascript: +### JavaScript: ```javascript const change = (amount, coins) => { @@ -353,7 +544,28 @@ object Solution { } } ``` +### C + +```c +int change(int amount, int* coins, int coinsSize) { + int dp[amount + 1]; + memset(dp, 0, sizeof (dp)); + dp[0] = 1; + // 遍历物品 + for(int i = 0; i < coinsSize; i++){ + // 遍历背包 + for(int j = coins[i]; j <= amount; j++){ + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; +} +``` + + + ### C# + ```csharp public class Solution { @@ -374,7 +586,4 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" "b/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" old mode 100644 new mode 100755 index 82b3f5d4e9..a8eca862ef --- "a/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" +++ "b/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 利用二叉搜索树的特性搞起! @@ -15,13 +13,13 @@ 示例: -![530二叉搜索树的最小绝对差](https://code-thinking-1253855093.file.myqcloud.com/pics/20201014223400123.png) +![530二叉搜索树的最小绝对差](https://file1.kamacoder.com/i/algo/20201014223400123.png) 提示:树中至少有 2 个节点。 ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树中,需要掌握如何双指针遍历!| LeetCode:530.二叉搜索树的最小绝对差](https://www.bilibili.com/video/BV1DD4y11779),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树中,需要掌握如何双指针遍历!| LeetCode:530.二叉搜索树的最小绝对差](https://www.bilibili.com/video/BV1DD4y11779),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -72,7 +70,7 @@ public: 如图: -![530.二叉搜索树的最小绝对差](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204153247458.png) +![530.二叉搜索树的最小绝对差](https://file1.kamacoder.com/i/algo/20210204153247458.png) 一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。 @@ -153,23 +151,27 @@ public: 递归 ```java class Solution { - TreeNode pre;// 记录上一个遍历的结点 + TreeNode pre; // 记录上一个遍历的结点 int result = Integer.MAX_VALUE; + public int getMinimumDifference(TreeNode root) { - if(root==null)return 0; - traversal(root); - return result; + if (root == null) + return 0; + traversal(root); + return result; } - public void traversal(TreeNode root){ - if(root==null)return; - //左 + + public void traversal(TreeNode root) { + if (root == null) + return; + // 左 traversal(root.left); - //中 - if(pre!=null){ - result = Math.min(result,root.val-pre.val); + // 中 + if (pre != null) { + result = Math.min(result, root.val - pre.val); } pre = root; - //右 + // 右 traversal(root.right); } } @@ -182,22 +184,27 @@ class Solution { TreeNode pre = null; int result = Integer.MAX_VALUE; - if(root != null) + if (root != null) stack.add(root); - while(!stack.isEmpty()){ + + // 中序遍历(左中右),由于栈先入后出,反序(右中左) + while (!stack.isEmpty()) { TreeNode curr = stack.peek(); - if(curr != null){ + if (curr != null) { stack.pop(); - if(curr.right != null) + // 右 + if (curr.right != null) stack.add(curr.right); + // 中(先用null标记) stack.add(curr); stack.add(null); - if(curr.left != null) + // 左 + if (curr.left != null) stack.add(curr.left); - }else{ + } else { // 中(遇到null再处理) stack.pop(); TreeNode temp = stack.pop(); - if(pre != null) + if (pre != null) result = Math.min(result, temp.val - pre.val); pre = temp; } @@ -669,8 +676,5 @@ public class Solution } ``` -

- - - + diff --git "a/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" "b/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" old mode 100644 new mode 100755 index 7fcb5efd32..c4bfeae4b8 --- "a/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" +++ "b/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 538.把二叉搜索树转换为累加树 @@ -20,7 +18,7 @@ 示例 1: -![538.把二叉搜索树转换为累加树](https://code-thinking-1253855093.file.myqcloud.com/pics/20201023160751832.png) +![538.把二叉搜索树转换为累加树](https://file1.kamacoder.com/i/algo/20201023160751832.png) * 输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8] * 输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8] @@ -69,7 +67,7 @@ 遍历顺序如图所示: -![538.把二叉搜索树转换为累加树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204153440666.png) +![538.把二叉搜索树转换为累加树](https://file1.kamacoder.com/i/algo/20210204153440666.png) 本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。 @@ -548,8 +546,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" old mode 100644 new mode 100755 index 80e662f9dd..d5ad95c112 --- "a/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" +++ "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -37,10 +35,10 @@ 因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 -**所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。** +**所以当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。** 性能如下: - + 那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 @@ -282,7 +280,7 @@ class Solution: return ''.join(res) ``` -### Python3 (v2): +#### Python3 (v2): ```python class Solution: @@ -297,6 +295,21 @@ class Solution: return s ``` +#### Python3 (v3): + +```python +class Solution: + def reverseStr(self, s: str, k: int) -> str: + i = 0 + chars = list(s) + + while i < len(chars): + chars[i:i + k] = chars[i:i + k][::-1] # 反转后,更改原值为反转后值 + i += k * 2 + + return ''.join(chars) +``` + ### Go: ```go @@ -501,7 +514,4 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" "b/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" old mode 100644 new mode 100755 index 7dbb8ef542..8208d9a1eb --- "a/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" +++ "b/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 583. 两个字符串的删除操作 @@ -33,7 +31,7 @@ dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。 -这里dp数组的定义有点点绕,大家要撸清思路。 +这里dp数组的定义有点点绕,大家要理清思路。 2. 确定递推公式 @@ -83,7 +81,7 @@ for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; 以word1:"sea",word2:"eat"为例,推导dp数组状态图如下: -![583.两个字符串的删除操作1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210714101750205.png) +![583.两个字符串的删除操作1](https://file1.kamacoder.com/i/algo/20210714101750205.png) 以上分析完毕,代码如下: @@ -234,8 +232,29 @@ 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 func minDistance(word1 string, word2 string) int { dp := make([][]int, len(word1)+1) @@ -268,7 +287,39 @@ func min(a, b int) int { return b } ``` -### Javascript: + + +动态规划二 + +```go +func minDistance(word1 string, word2 string) int { + dp := make([][]int, len(word1) + 1) + for i := range dp { + dp[i] = make([]int, len(word2) + 1) + } + for i := 1; i <= len(word1); i++ { + for j := 1; j <= len(word2); j++ { + 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 len(word1) + len(word2) - dp[len(word1)][len(word2)] * 2 +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + + +### JavaScript: + ```javascript // 方法一 @@ -417,7 +468,3 @@ impl Solution { ``` -

- - - diff --git "a/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" "b/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 3478a2af5d..3ca5feb9da --- "a/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 617.合并二叉树 @@ -15,7 +13,7 @@ 示例 1: -![617.合并二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000854.png) +![617.合并二叉树](https://file1.kamacoder.com/i/algo/20230310000854.png) 注意: 合并必须从两个树的根节点开始。 @@ -40,7 +38,7 @@ 动画如下: -![617.合并二叉树](https://code-thinking.cdn.bcebos.com/gifs/617.%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91.gif) +![617.合并二叉树](https://file1.kamacoder.com/i/algo/617.%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91.gif) 那么我们来按照递归三部曲来解决: @@ -803,8 +801,4 @@ public TreeNode MergeTrees(TreeNode root1, TreeNode root2) } ``` -

- - - diff --git "a/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" old mode 100644 new mode 100755 index d94d18c5c0..fd2ae43886 --- "a/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" +++ "b/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 647. 回文子串 @@ -50,12 +48,12 @@ dp[i] 和 dp[i-1] ,dp[i + 1] 看上去都没啥关系。 所以我们要看回文串的性质。 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230102170752.png) +![](https://file1.kamacoder.com/i/algo/20230102170752.png) 我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。 -那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。 +那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。 所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。 @@ -102,13 +100,13 @@ dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹 4. 确定遍历顺序 -遍历顺序可有有点讲究了。 +遍历顺序可就有点讲究了。 首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。 dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图: -![647.回文子串](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171032473-20230310132134822.jpg) +![647.回文子串](https://file1.kamacoder.com/i/algo/20210121171032473-20230310132134822.jpg) 如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。 @@ -138,7 +136,7 @@ for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 举例,输入:"aaa",dp[i][j]状态如下: -![647.回文子串1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121171059951-20230310132153163.jpg) +![647.回文子串1](https://file1.kamacoder.com/i/algo/20210121171059951-20230310132153163.jpg) 图中有6个true,所以就是有6个回文子串。 @@ -397,6 +395,7 @@ class Solution: ``` ### Go: +> 动态规划: ```Go func countSubstrings(s string) int { @@ -422,8 +421,49 @@ func countSubstrings(s string) int { return res } ``` +> 动态规划:简洁版 +```Go +func countSubstrings(s string) int { + res := 0 + dp := make([][]bool, len(s)) + for i := 0; i < len(s); i++ { + dp[i] = make([]bool, len(s)) + } + + for i := len(s) - 1; i >= 0; i-- { + for j := i; j < len(s); j++ { + if s[i] == s[j] && (j-i <= 1 || dp[i+1][j-1]) { + res++ + dp[i][j] = true + } + } + } + return res +} +``` + +> 双指针法: +```Go +func countSubstrings(s string) int { + extend := func(i, j int) int { + res := 0 + for i >= 0 && j < len(s) && s[i] == s[j] { + i -- + j ++ + res ++ + } + return res + } + res := 0 + for i := 0; i < len(s); i++ { + res += extend(i, i) // 以i为中心 + res += extend(i, i+1) // 以i和i+1为中心 + } + return res +} +``` -### Javascript: +### JavaScript: > 动态规划 ```javascript @@ -571,7 +611,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" "b/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" old mode 100644 new mode 100755 index db6b43df8e..e77070fcf8 --- "a/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" +++ "b/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -283,8 +281,4 @@ function predictPartyVictory(senate: string): string { -

- - - diff --git "a/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" "b/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index f54558a68c..49ccc9cdea --- "a/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 654.最大二叉树 @@ -19,7 +17,7 @@ 示例 : -![654.最大二叉树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204154534796.png) +![654.最大二叉树](https://file1.kamacoder.com/i/algo/20210204154534796.png) 提示: @@ -34,7 +32,7 @@ 最大二叉树的构建过程如下: -![654.最大二叉树](https://code-thinking.cdn.bcebos.com/gifs/654.%E6%9C%80%E5%A4%A7%E4%BA%8C%E5%8F%89%E6%A0%91.gif) +![654.最大二叉树](https://file1.kamacoder.com/i/algo/654.%E6%9C%80%E5%A4%A7%E4%BA%8C%E5%8F%89%E6%A0%91.gif) 构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 @@ -598,8 +596,4 @@ public TreeNode ConstructMaximumBinaryTree(int[] nums) } ``` -

- - - diff --git "a/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" "b/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" old mode 100644 new mode 100755 index ef58739122..c1706df4fa --- "a/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" +++ "b/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 657. 机器人能否返回原点 @@ -42,7 +40,7 @@ 最后判断一下x,y是否回到了(0, 0)位置就可以了。 如图所示: - + C++代码如下: @@ -181,8 +179,4 @@ var judgeCircle = function (moves) { ``` -

- - - diff --git "a/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" old mode 100644 new mode 100755 index 916013c52c..dbcc6ed63d --- "a/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" +++ "b/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -16,13 +14,13 @@ 给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。 -![669.修剪二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20201014173115788.png) +![669.修剪二叉搜索树](https://file1.kamacoder.com/i/algo/20201014173115788.png) -![669.修剪二叉搜索树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201014173219142.png) +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20201014173219142.png) ## 算法公开课 -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你修剪的方式不对,我来给你纠正一下!| LeetCode:669. 修剪二叉搜索树](https://www.bilibili.com/video/BV17P41177ud?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你修剪的方式不对,我来给你纠正一下!| LeetCode:669. 修剪二叉搜索树](https://www.bilibili.com/video/BV17P41177ud?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 思路 @@ -52,7 +50,7 @@ public: 我们在重新关注一下第二个示例,如图: -![669.修剪二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204155302751.png) +![669.修剪二叉搜索树](https://file1.kamacoder.com/i/algo/20210204155302751.png) **所以以上的代码是不可行的!** @@ -62,7 +60,7 @@ public: 在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图: -![669.修剪二叉搜索树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204155327203.png) +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20210204155327203.png) 理解了最关键部分了我们再递归三部曲: @@ -79,7 +77,7 @@ public: 代码如下: -``` +```cpp TreeNode* trimBST(TreeNode* root, int low, int high) ``` @@ -87,7 +85,7 @@ TreeNode* trimBST(TreeNode* root, int low, int high) 修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 -``` +```cpp if (root == nullptr ) return nullptr; ``` @@ -97,7 +95,7 @@ if (root == nullptr ) return nullptr; 代码如下: -``` +```cpp if (root->val < low) { TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 return right; @@ -108,7 +106,7 @@ if (root->val < low) { 代码如下: -``` +```cpp if (root->val > high) { TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 return left; @@ -119,7 +117,7 @@ if (root->val > high) { 最后返回root节点,代码如下: -``` +```cpp root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 return root; @@ -129,11 +127,11 @@ return root; 在回顾一下上面的代码,针对下图中二叉树的情况: -![669.修剪二叉搜索树1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204155327203-20230310120126738.png) +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20210204155327203-20230310120126738.png) 如下代码相当于把节点0的右孩子(节点2)返回给上一层, -``` +```cpp if (root->val < low) { TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 return right; @@ -142,7 +140,7 @@ if (root->val < low) { 然后如下代码相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。 -``` +``` cpp root->left = trimBST(root->left, low, high); ``` @@ -586,8 +584,4 @@ public TreeNode TrimBST(TreeNode root, int low, int high) ``` -

- - - diff --git "a/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" "b/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" old mode 100644 new mode 100755 index 0277f24989..9e61229abb --- "a/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" +++ "b/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 673.最长递增子序列的个数 @@ -180,7 +178,7 @@ for (int i = 0; i < nums.size(); i++) { 输入:[1,3,5,4,7] -![673.最长递增子序列的个数](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000656.png) +![673.最长递增子序列的个数](https://file1.kamacoder.com/i/algo/20230310000656.png) **如果代码写出来了,怎么改都通过不了,那么把dp和count打印出来看看对不对!** @@ -360,8 +358,4 @@ var findNumberOfLIS = function(nums) { ``` -

- - - diff --git "a/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" "b/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index 485e321c99..dae64a11ac --- "a/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" +++ "b/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 674. 最长连续递增序列 @@ -87,7 +85,7 @@ for (int i = 1; i < nums.size(); i++) { 已输入nums = [1,3,5,4,7]为例,dp数组状态如下: -![674.最长连续递增序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204103529742.jpg) +![674.最长连续递增序列](https://file1.kamacoder.com/i/algo/20210204103529742.jpg) **注意这里要取dp[i]里的最大值,所以dp[2]才是结果!** @@ -186,7 +184,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 @@ -343,7 +357,7 @@ impl Solution { ``` -### Javascript: +### JavaScript: > 动态规划: ```javascript @@ -425,11 +439,76 @@ function findLengthOfLCIS(nums: number[]): number { }; ``` +### C: + +> 动态规划: + +```c +int findLengthOfLCIS(int* nums, int numsSize) { + if(numsSize == 0){ + return 0; + } + int dp[numsSize]; + for(int i = 0; i < numsSize; i++){ + dp[i] = 1; + } + int result = 1; + for (int i = 1; i < numsSize; ++i) { + if(nums[i] > nums[i - 1]){ + dp[i] = dp[i - 1] + 1; + } + if(dp[i] > result){ + result = dp[i]; + } + } + return result; +} +``` + + + +> 贪心: +```c +int findLengthOfLCIS(int* nums, int numsSize) { + int result = 1; + int count = 1; + if(numsSize == 0){ + return result; + } + for (int i = 1; i < numsSize; ++i) { + if(nums[i] > nums[i - 1]){ + count++; + } else{ + count = 1; + } + if(count > result){ + result = count; + } + } + return result; +} +``` + +### Cangjie + +```cangjie +func findLengthOfLCIS(nums: Array): Int64 { + let n = nums.size + if (n <= 1) { + return n + } + let dp = Array(n, repeat: 1) + var res = 0 + for (i in 1..n) { + if (nums[i] > nums[i - 1]) { + dp[i] = dp[i - 1] + 1 + } + res = max(res, dp[i]) + } + return res +} +``` -

- - - diff --git "a/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" "b/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" old mode 100644 new mode 100755 index f5e84223b1..2f939d0827 --- "a/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" +++ "b/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 684.冗余连接 @@ -14,7 +12,7 @@ 请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727150215.png) +![](https://file1.kamacoder.com/i/algo/20210727150215.png) 提示: * n == edges.length @@ -75,7 +73,7 @@ void join(int u, int v) { 2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 -如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/图论并查集理论基础.html) +如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) 我们再来看一下这道题目。 @@ -87,7 +85,7 @@ void join(int u, int v) { 如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230604104720.png) +![](https://file1.kamacoder.com/i/algo/20230604104720.png) 节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。 @@ -97,7 +95,7 @@ void join(int u, int v) { 如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230604104330.png) +![](https://file1.kamacoder.com/i/algo/20230604104330.png) 已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。 @@ -378,8 +376,4 @@ var findRedundantConnection = function(edges) { -

- - - diff --git "a/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" "b/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" old mode 100644 new mode 100755 index 0cad5ad581..27161d174c --- "a/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" +++ "b/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 685.冗余连接II @@ -18,9 +16,9 @@ 返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727151057.png) +![](https://file1.kamacoder.com/i/algo/20210727151057.png) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727151118.png) +![](https://file1.kamacoder.com/i/algo/20210727151118.png) 提示: @@ -40,7 +38,7 @@ 那么有如下三种情况,前两种情况是出现入度为2的点,如图: - + 且只有一个节点入度为2,为什么不看出度呢,出度没有意义,一棵树中随便一个父节点就有多个出度。 @@ -48,7 +46,7 @@ 如图: - + 首先先计算节点的入度,这里不少录友在计算入度的时候就搞蒙了,分不清 edges[i][j] 表示的都是什么。 @@ -69,7 +67,7 @@ edges[2][0] = 2,edges[2][1] = 3, 搞清楚之后,我们如何统计入度呢? -即 edges[i][1] 表示的节点都是 箭头指向的节点,即这个几点有一个入度! (如果想统计出度,那么就是 edges[i][0])。 +即 edges[i][1] 表示的节点都是 箭头指向的节点,即这个节点有一个入度! (如果想统计出度,那么就是 edges[i][0])。 所以,统计入度的代码如下: @@ -108,7 +106,7 @@ if (vec.size() > 0) { 可以定义一个函数,代码如下: -``` +```cpp // 在有向图里找到删除的那条边,使其变成树,返回值就是要删除的边 vector getRemoveEdge(const vector>& edges) ``` @@ -619,8 +617,4 @@ var findRedundantDirectedConnection = function(edges) { -

- - - diff --git "a/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" "b/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" old mode 100644 new mode 100755 index 87b1b5bbbb..a63d2b0e06 --- "a/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" +++ "b/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 695. 岛屿的最大面积 @@ -16,7 +14,7 @@ 计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220729111528.png) +![](https://file1.kamacoder.com/i/algo/20220729111528.png) * 输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] * 输出:6 @@ -29,7 +27,7 @@ 也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220726094200.png) +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) 这道题目也是 dfs bfs基础类题目,就是搜索每个岛屿上“1”的数量,然后取一个最大的。 @@ -44,7 +42,7 @@ 这里其实涉及到dfs的两种写法。 -写法一,dfs只处理下一个节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地 +写法一,dfs处理当前节点的相邻节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地 ```CPP // 版本一 @@ -87,7 +85,7 @@ public: }; ``` -写法二,dfs处理当前节点,即即在主函数遇到岛屿就计数为0,dfs处理接下来的全部陆地 +写法二,dfs处理当前节点,即在主函数遇到岛屿就计数为0,dfs处理接下来的全部陆地 dfs ```CPP @@ -708,8 +706,4 @@ impl Solution { } } ``` -

- - - diff --git "a/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" "b/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" old mode 100644 new mode 100755 index 9efb1e0519..40777a67a2 --- "a/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" +++ "b/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 700.二叉搜索树中的搜索 @@ -14,7 +12,7 @@ 例如, -![700.二叉搜索树中的搜索](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204155522476.png) +![700.二叉搜索树中的搜索](https://file1.kamacoder.com/i/algo/20210204155522476.png) 在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 @@ -126,7 +124,7 @@ public: 中间节点如果大于3就向左走,如果小于3就向右走,如图: -![二叉搜索树](https://code-thinking-1253855093.file.myqcloud.com/pics/20200812190213280.png) +![二叉搜索树](https://file1.kamacoder.com/i/algo/20200812190213280.png) 所以迭代法代码如下: @@ -262,6 +260,26 @@ class Solution: return None ``` +(方法三) 栈-遍历 +```python +class Solution: + def searchBST(self, root: TreeNode, val: int) -> TreeNode: + stack = [root] + while stack: + node = stack.pop() + # 根据TreeNode的定义 + # node携带有三类信息 node.left/node.right/node.val + # 找到val直接返回node 即是找到了该节点为根的子树 + # 此处node.left/node.right/val的前后顺序可打乱 + if node.val == val: + return node + if node.right: + stack.append(node.right) + if node.left: + stack.append(node.left) + return None +``` + ### Go @@ -487,8 +505,4 @@ public TreeNode SearchBST(TreeNode root, int val) } ``` -

- - - diff --git "a/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" "b/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" old mode 100644 new mode 100755 index 98e60d5f5f..fec287449c --- "a/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" +++ "b/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 701.二叉搜索树中的插入操作 @@ -14,7 +12,7 @@ 注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。 -![701.二叉搜索树中的插入操作](https://code-thinking-1253855093.file.myqcloud.com/pics/20201019173259554.png) +![701.二叉搜索树中的插入操作](https://file1.kamacoder.com/i/algo/20201019173259554.png) 提示: @@ -35,7 +33,7 @@ 如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。 -![701.二叉搜索树中的插入操作](https://code-thinking.cdn.bcebos.com/gifs/701.%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E6%8F%92%E5%85%A5%E6%93%8D%E4%BD%9C.gif) +![701.二叉搜索树中的插入操作](https://file1.kamacoder.com/i/algo/701.%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E6%8F%92%E5%85%A5%E6%93%8D%E4%BD%9C.gif) 例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,**需要调整二叉树的结构么? 并不需要。**。 @@ -59,7 +57,7 @@ 代码如下: -``` +```cpp TreeNode* insertIntoBST(TreeNode* root, int val) ``` @@ -69,7 +67,7 @@ TreeNode* insertIntoBST(TreeNode* root, int val) 代码如下: -``` +```cpp if (root == NULL) { TreeNode* node = new TreeNode(val); return node; @@ -88,7 +86,7 @@ if (root == NULL) { 代码如下: -``` +```cpp if (root->val > val) root->left = insertIntoBST(root->left, val); if (root->val < val) root->right = insertIntoBST(root->right, val); return root; @@ -120,7 +118,7 @@ public: 那么递归函数定义如下: -``` +```cpp TreeNode* parent; // 记录遍历节点的父节点 void traversal(TreeNode* cur, int val) ``` @@ -283,32 +281,10 @@ class Solution: return TreeNode(val) self.traversal(root, val) return root - ``` 递归法(版本二) ```python -class Solution: - def insertIntoBST(self, root, val): - if root is None: - return TreeNode(val) - parent = None - cur = root - while cur: - parent = cur - if val < cur.val: - cur = cur.left - else: - cur = cur.right - if val < parent.val: - parent.left = TreeNode(val) - else: - parent.right = TreeNode(val) - return root -``` - -递归法(版本三) -```python class Solution: def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: if root is None or root.val == val: @@ -326,7 +302,7 @@ class Solution: return root ``` -递归法(版本四) +递归法(版本三) ```python class Solution: def insertIntoBST(self, root, val): @@ -340,10 +316,9 @@ class Solution: root.right = self.insertIntoBST(root.right, val) return root - ``` -迭代法 +迭代法(版本一) ```python class Solution: def insertIntoBST(self, root, val): @@ -366,10 +341,53 @@ class Solution: else: parent.right = node # 将新节点连接到父节点的右子树 + return root +``` + +迭代法(版本二) +```python +class Solution: + def insertIntoBST(self, root, val): + if root is None: + return TreeNode(val) + parent = None + cur = root + while cur: + parent = cur + if val < cur.val: + cur = cur.left + else: + cur = cur.right + if val < parent.val: + parent.left = TreeNode(val) + else: + parent.right = TreeNode(val) return root +``` + +迭代法(精简) +```python +class Solution: + def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: + if not root: # 如果根节点为空,创建新节点作为根节点并返回 + return TreeNode(val) + cur = root + while cur: + if val < cur.val: + if not cur.left: # 如果此时父节点的左子树为空 + cur.left = TreeNode(val) # 将新节点连接到父节点的左子树 + return root + else: + cur = cur.left + elif val > cur.val: + if not cur.right: # 如果此时父节点的左子树为空 + cur.right = TreeNode(val) # 将新节点连接到父节点的右子树 + return root + else: + cur = cur.right - ``` + ----- ### Go @@ -704,7 +722,3 @@ public TreeNode InsertIntoBST(TreeNode root, int val) { ``` -

- - - diff --git "a/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" "b/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" old mode 100644 new mode 100755 index 31e89ae344..e529629492 --- "a/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" +++ "b/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 704. 二分查找 @@ -61,7 +59,7 @@ 例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示: -![704.二分查找](https://code-thinking-1253855093.file.myqcloud.com/pics/20210311153055723.jpg) +![704.二分查找](https://file1.kamacoder.com/i/algo/20210311153055723.jpg) 代码如下:(详细注释) @@ -104,7 +102,7 @@ public: 在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(**注意和方法一的区别**) -![704.二分查找1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210311153123632.jpg) +![704.二分查找1](https://file1.kamacoder.com/i/algo/20210311153123632.jpg) 代码如下:(详细注释) @@ -174,13 +172,17 @@ class Solution { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + ((right - left) >> 1); - if (nums[mid] == target) + if (nums[mid] == target) { return mid; - else if (nums[mid] < target) + } + else if (nums[mid] < target) { left = mid + 1; - else if (nums[mid] > target) + } + else { // nums[mid] > target right = mid - 1; + } } + // 未找到目标值 return -1; } } @@ -194,13 +196,17 @@ class Solution { int left = 0, right = nums.length; while (left < right) { int mid = left + ((right - left) >> 1); - if (nums[mid] == target) + if (nums[mid] == target) { return mid; - else if (nums[mid] < target) + } + else if (nums[mid] < target) { left = mid + 1; - else if (nums[mid] > target) + } + else { // nums[mid] > target right = mid; + } } + // 未找到目标值 return -1; } } @@ -501,19 +507,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 +527,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 +834,4 @@ class Solution { } ``` -

- - - diff --git "a/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" old mode 100644 new mode 100755 index fecdbc3cad..72e35f430f --- "a/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" +++ "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 听说这道题目把链表常见的五个操作都覆盖了? @@ -22,7 +20,7 @@ * deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 -![707示例](https://code-thinking-1253855093.file.myqcloud.com/pics/20200814200558953.png) +![707示例](https://file1.kamacoder.com/i/algo/20200814200558953.png) ## 算法公开课 @@ -37,10 +35,10 @@ 如果对链表的虚拟头结点不清楚,可以看这篇文章:[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html) 删除链表节点: -![链表-删除节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806195114541.png) +![链表-删除节点](https://file1.kamacoder.com/i/algo/20200806195114541.png) 添加链表节点: -![链表-添加节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806195134331.png) +![链表-添加节点](https://file1.kamacoder.com/i/algo/20200806195134331.png) 这道题目设计链表的五个接口: * 获取链表第index个节点的数值 @@ -165,112 +163,240 @@ private: ## 其他语言版本 +### C++双链表法: + +```CPP +//采用循环虚拟结点的双链表实现 +class MyLinkedList { +public: + // 定义双向链表节点结构体 + struct DList { + int elem; // 节点存储的元素 + DList *next; // 指向下一个节点的指针 + DList *prev; // 指向上一个节点的指针 + // 构造函数,创建一个值为elem的新节点 + DList(int elem) : elem(elem), next(nullptr), prev(nullptr) {}; + }; + + // 构造函数,初始化链表 + MyLinkedList() { + sentinelNode = new DList(0); // 创建哨兵节点,不存储有效数据 + sentinelNode->next = sentinelNode; // 哨兵节点的下一个节点指向自身,形成循环 + sentinelNode->prev = sentinelNode; // 哨兵节点的上一个节点指向自身,形成循环 + size = 0; // 初始化链表大小为0 + } + + // 获取链表中第index个节点的值 + int get(int index) { + if (index > (size - 1) || index < 0) { // 检查索引是否超出范围 + return -1; // 如果超出范围,返回-1 + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index + 1; i++) { + curNode = curNode->next; // 移动到目标节点 + } + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index; i++) { + curNode = curNode->prev; // 移动到目标节点 + } + } + num = curNode->elem; // 获取目标节点的值 + return num; // 返回节点的值 + } + + // 在链表头部添加节点 + void addAtHead(int val) { + DList *newNode = new DList(val); // 创建新节点 + DList *next = sentinelNode->next; // 获取当前头节点的下一个节点 + newNode->prev = sentinelNode; // 新节点的上一个节点指向哨兵节点 + newNode->next = next; // 新节点的下一个节点指向原来的头节点 + size++; // 链表大小加1 + sentinelNode->next = newNode; // 哨兵节点的下一个节点指向新节点 + next->prev = newNode; // 原来的头节点的上一个节点指向新节点 + } + + // 在链表尾部添加节点 + void addAtTail(int val) { + DList *newNode = new DList(val); // 创建新节点 + DList *prev = sentinelNode->prev; // 获取当前尾节点的上一个节点 + newNode->next = sentinelNode; // 新节点的下一个节点指向哨兵节点 + newNode->prev = prev; // 新节点的上一个节点指向原来的尾节点 + size++; // 链表大小加1 + sentinelNode->prev = newNode; // 哨兵节点的上一个节点指向新节点 + prev->next = newNode; // 原来的尾节点的下一个节点指向新节点 + } + + // 在链表中的第index个节点之前添加值为val的节点 + void addAtIndex(int index, int val) { + if (index > size) { // 检查索引是否超出范围 + return; // 如果超出范围,直接返回 + } + if (index <= 0) { // 如果索引为0或负数,在头部添加节点 + addAtHead(val); + return; + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index; i++) { + curNode = curNode->next; // 移动到目标位置的前一个节点 + } + DList *temp = curNode->next; // 获取目标位置的节点 + DList *newNode = new DList(val); // 创建新节点 + curNode->next = newNode; // 在目标位置前添加新节点 + temp->prev = newNode; // 目标位置的节点的前一个节点指向新节点 + newNode->next = temp; // 新节点的下一个节点指向目标位置的结点 + newNode->prev = curNode; // 新节点的上一个节点指向当前节点 + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index; i++) { + curNode = curNode->prev; // 移动到目标位置的后一个节点 + } + DList *temp = curNode->prev; // 获取目标位置的节点 + DList *newNode = new DList(val); // 创建新节点 + curNode->prev = newNode; // 在目标位置后添加新节点 + temp->next = newNode; // 目标位置的节点的下一个节点指向新节点 + newNode->prev = temp; // 新节点的上一个节点指向目标位置的节点 + newNode->next = curNode; // 新节点的下一个节点指向当前节点 + } + size++; // 链表大小加1 + } + + // 删除链表中的第index个节点 + void deleteAtIndex(int index) { + if (index > (size - 1) || index < 0) { // 检查索引是否超出范围 + return; // 如果超出范围,直接返回 + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index; i++) { + curNode = curNode->next; // 移动到目标位置的前一个节点 + } + DList *next = curNode->next->next; // 获取目标位置的下一个节点 + curNode->next = next; // 删除目标位置的节点 + next->prev = curNode; // 目标位置的下一个节点的前一个节点指向当前节点 + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index - 1; i++) { + curNode = curNode->prev; // 移动到目标位置的后一个节点 + } + DList *prev = curNode->prev->prev; // 获取目标位置的下一个节点 + curNode->prev = prev; // 删除目标位置的节点 + prev->next = curNode; // 目标位置的下一个节点的下一个节点指向当前节点 + } + size--; // 链表大小减1 + } + +private: + int size; // 链表的大小 + DList *sentinelNode; // 哨兵节点的指针 +}; +``` + ### C: ```C -typedef struct MyLinkedList { - int val; - struct MyLinkedList* next; -}MyLinkedList; +typedef struct Node { + int val; + struct Node* next; +} Node; + + +typedef struct { + int size; + Node* data; +} MyLinkedList; /** Initialize your data structure here. */ MyLinkedList* myLinkedListCreate() { - //这个题必须用虚拟头指针,参数都是一级指针,头节点确定后没法改指向了!!! - MyLinkedList* head = (MyLinkedList *)malloc(sizeof (MyLinkedList)); - head->next = NULL; - return head; + MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList)); + Node* head = (Node*)malloc(sizeof(Node)); + head->next = (void*)0; + obj->data = head; + obj->size = 0; + return obj; } /** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */ int myLinkedListGet(MyLinkedList* obj, int index) { - MyLinkedList *cur = obj->next; - for (int i = 0; cur != NULL; i++){ - if (i == index){ - return cur->val; - } - else{ - cur = cur->next; - } + if (index < 0 || index >= obj->size) return -1; + + Node* cur = obj->data; + while (index-- >= 0) { + cur = cur->next; } - return -1; + + return cur->val; } /** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */ void myLinkedListAddAtHead(MyLinkedList* obj, int val) { - MyLinkedList *nhead = (MyLinkedList *)malloc(sizeof (MyLinkedList)); - nhead->val = val; - nhead->next = obj->next; - obj->next = nhead; + Node* node = (Node*)malloc(sizeof(Node)); + node->val = val; + node->next = obj->data->next; + obj->data->next = node; + obj->size++; } /** Append a node of value val to the last element of the linked list. */ void myLinkedListAddAtTail(MyLinkedList* obj, int val) { - MyLinkedList *cur = obj; - while(cur->next != NULL){ + Node* cur = obj->data; + while (cur->next != ((void*)0)) { cur = cur->next; } - MyLinkedList *ntail = (MyLinkedList *)malloc(sizeof (MyLinkedList)); - ntail->val = val; - ntail->next = NULL; - cur->next = ntail; + + Node* tail = (Node*)malloc(sizeof(Node)); + tail->val = val; + tail->next = (void*)0; + cur->next = tail; + obj->size++; } /** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */ void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) { - if (index == 0){ - myLinkedListAddAtHead(obj, val); - return; - } - MyLinkedList *cur = obj->next; - for (int i = 1 ;cur != NULL; i++){ - if (i == index){ - MyLinkedList* newnode = (MyLinkedList *)malloc(sizeof (MyLinkedList)); - newnode->val = val; - newnode->next = cur->next; - cur->next = newnode; - return; - } - else{ - cur = cur->next; - } + if (index > obj->size) return; + + Node* cur = obj->data; + while (index-- > 0) { + cur = cur->next; } + + Node* node = (Node*)malloc(sizeof(Node)); + node->val = val; + node->next = cur->next; + cur->next = node; + obj->size++; } /** Delete the index-th node in the linked list, if the index is valid. */ void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) { - if (index == 0){ - MyLinkedList *tmp = obj->next; - if (tmp != NULL){ - obj->next = tmp->next; - free(tmp); - } - return; - } - MyLinkedList *cur = obj->next; - for (int i = 1 ;cur != NULL && cur->next != NULL; i++){ - if (i == index){ - MyLinkedList *tmp = cur->next; - if (tmp != NULL) { - cur->next = tmp->next; - free(tmp); - } - return; - } - else{ - cur = cur->next; - } + if (index < 0 || index >= obj->size) return; + + Node* cur = obj->data; + while (index-- > 0) { + cur = cur->next; } - + + Node* temp = cur->next; + cur->next = temp->next; + free(temp); + obj->size--; } void myLinkedListFree(MyLinkedList* obj) { - while(obj != NULL){ - MyLinkedList *tmp = obj; - obj = obj->next; - free(tmp); - } + Node* tmp = obj->data; + while (tmp != NULL) { + Node* n = tmp; + tmp = tmp->next; + free(n); + } + free(obj); } /** @@ -294,106 +420,115 @@ void myLinkedListFree(MyLinkedList* obj) { ```Java //单链表 -class ListNode { - int val; - ListNode next; - ListNode(){} - ListNode(int val) { - this.val=val; - } -} class MyLinkedList { + + class ListNode { + int val; + ListNode next; + ListNode(int val) { + this.val=val; + } + } //size存储链表元素的个数 - int size; - //虚拟头结点 - ListNode head; + private int size; + //注意这里记录的是虚拟头结点 + private ListNode head; //初始化链表 public MyLinkedList() { - size = 0; - head = new ListNode(0); + this.size = 0; + this.head = new ListNode(0); } - //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点 + //获取第index个节点的数值,注意index是从0开始的,第0个节点就是虚拟头结点 public int get(int index) { //如果index非法,返回-1 if (index < 0 || index >= size) { return -1; } - ListNode currentNode = head; - //包含一个虚拟头节点,所以查找第 index+1 个节点 + ListNode cur = head; + //第0个节点是虚拟头节点,所以查找第 index+1 个节点 for (int i = 0; i <= index; i++) { - currentNode = currentNode.next; + cur = cur.next; } - return currentNode.val; + return cur.val; } - //在链表最前面插入一个节点,等价于在第0个元素前添加 public void addAtHead(int val) { - addAtIndex(0, val); + ListNode newNode = new ListNode(val); + newNode.next = head.next; + head.next = newNode; + size++; + + // 在链表最前面插入一个节点,等价于在第0个元素前添加 + // addAtIndex(0, val); } - //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加 + public void addAtTail(int val) { - addAtIndex(size, val); + ListNode newNode = new ListNode(val); + ListNode cur = head; + while (cur.next != null) { + cur = cur.next; + } + cur.next = newNode; + size++; + + // 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加 + // addAtIndex(size, val); } // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点 // 如果 index 大于链表的长度,则返回空 public void addAtIndex(int index, int val) { - if (index > size) { + if (index < 0 || index > size) { return; } - if (index < 0) { - index = 0; - } - size++; + //找到要插入节点的前驱 - ListNode pred = head; + ListNode pre = head; for (int i = 0; i < index; i++) { - pred = pred.next; + pre = pre.next; } - ListNode toAdd = new ListNode(val); - toAdd.next = pred.next; - pred.next = toAdd; + ListNode newNode = new ListNode(val); + newNode.next = pre.next; + pre.next = newNode; + size++; } - //删除第index个节点 public void deleteAtIndex(int index) { if (index < 0 || index >= size) { return; } - size--; - if (index == 0) { - head = head.next; - return; - } - ListNode pred = head; + + //因为有虚拟头节点,所以不用对index=0的情况进行特殊处理 + ListNode pre = head; for (int i = 0; i < index ; i++) { - pred = pred.next; + pre = pre.next; } - pred.next = pred.next.next; + pre.next = pre.next.next; + size--; } } +``` +```Java //双链表 -class ListNode{ - int val; - ListNode next,prev; - ListNode() {}; - ListNode(int val){ - this.val = val; - } -} - - class MyLinkedList { + class ListNode{ + int val; + ListNode next, prev; + ListNode(int val){ + this.val = val; + } + } + //记录链表中元素的数量 - int size; + private int size; //记录链表的虚拟头结点和尾结点 - ListNode head,tail; + private ListNode head, tail; public MyLinkedList() { //初始化操作 @@ -401,25 +536,25 @@ class MyLinkedList { this.head = new ListNode(0); this.tail = new ListNode(0); //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!! - head.next=tail; - tail.prev=head; + this.head.next = tail; + this.tail.prev = head; } public int get(int index) { //判断index是否有效 - if(index<0 || index>=size){ + if(index < 0 || index >= size){ return -1; } - ListNode cur = this.head; + ListNode cur = head; //判断是哪一边遍历时间更短 if(index >= size / 2){ //tail开始 cur = tail; - for(int i=0; i< size-index; i++){ + for(int i = 0; i < size - index; i++){ cur = cur.prev; } }else{ - for(int i=0; i<= index; i++){ + for(int i = 0; i <= index; i++){ cur = cur.next; } } @@ -428,27 +563,23 @@ class MyLinkedList { public void addAtHead(int val) { //等价于在第0个元素前添加 - addAtIndex(0,val); + addAtIndex(0, val); } public void addAtTail(int val) { //等价于在最后一个元素(null)前添加 - addAtIndex(size,val); + addAtIndex(size, val); } public void addAtIndex(int index, int val) { - //index大于链表长度 - if(index>size){ + //判断index是否有效 + if(index < 0 || index > size){ return; } - //index小于0 - if(index<0){ - index = 0; - } - size++; + //找到前驱 - ListNode pre = this.head; - for(int i=0; i=size){ + //判断index是否有效 + if(index < 0 || index >= size){ return; } + //删除操作 - size--; - ListNode pre = this.head; - for(int i=0; i this.size) { + return -1 + } + + let cur = this.head + for (let i = 0; i <= index; i++) { + cur = cur.next + } + + return cur.val +}; + +/** + * 在链表头部添加一个新节点 + * + * @param {number} val + * @return {void} + * + * 时间复杂度: O(1) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtHead = function (val) { + /** + head <-> [newNode] <-> originNode + */ + this.size++ + const originNode = this.head.next + // 创建新节点,并建立连接 + const newNode = new Node(val, this.head, originNode) + + // 取消原前后结点的连接 + this.head.next = newNode + originNode.prev = newNode +}; + +/** + * 在链表尾部添加一个新节点 + * + * @param {number} val + * @return {void} + * + * 时间复杂度: O(1) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtTail = function (val) { + /** + originNode <-> [newNode] <-> tail + */ + this.size++ + const originNode = this.tail.prev + + // 创建新节点,并建立连接 + const newNode = new Node(val, originNode, this.tail) + + // 取消原前后结点的连接 + this.tail.prev = newNode + originNode.next = newNode +}; + +/** + * 在指定索引位置前添加一个新节点 + * + * @param {number} index + * @param {number} val + * @return {void} + * + * 时间复杂度: O(n) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtIndex = function (index, val) { + // 当索引超出范围时,直接返回 + if (index > this.size) { + return + } + this.size++ + + let cur = this.head + for (let i = 0; i < index; i++) { + cur = cur.next + } + + const new_next = cur.next + + // 创建新节点,并建立连接 + const node = new Node(val, cur, new_next) + + // 取消原前后结点的连接 + cur.next = node + new_next.prev = node +}; + +/** + * 删除指定索引位置的节点 + * + * @param {number} index + * @return {void} + * + * 时间复杂度: O(n) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.deleteAtIndex = function (index) { + // 当索引超出范围时,直接返回 + if (index >= this.size) { + return + } + + this.size-- + let cur = this.head + for (let i = 0; i < index; i++) { + cur = cur.next + } + + const new_next = cur.next.next + // 取消原前后结点的连接 + new_next.prev = cur + cur.next = new_next +}; +``` + ### TypeScript: ```TypeScript @@ -1556,7 +1843,4 @@ public class MyLinkedList } ``` -

- - - + diff --git "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" old mode 100644 new mode 100755 index db39864908..fb095d7518 --- "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" +++ "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 714. 买卖股票的最佳时机含手续费 @@ -43,7 +41,7 @@ 在[贪心算法:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html)中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。 -而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。 +而本题有了手续费,就要关心什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以扣减手续费的情况。 如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。 @@ -122,7 +120,7 @@ public: * 时间复杂度:O(n) * 空间复杂度:O(n) -当然可以对空间经行优化,因为当前状态只是依赖前一个状态。 +当然可以对空间进行优化,因为当前状态只是依赖前一个状态。 C++ 代码如下: @@ -243,7 +241,7 @@ func maxProfit(prices []int, fee int) int { return res } ``` -### Javascript +### JavaScript ```Javascript // 贪心思路 var maxProfit = function(prices, fee) { @@ -360,8 +358,4 @@ object Solution { } ``` -

- - - diff --git "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" old mode 100644 new mode 100755 index 7e8e3d7c61..ebed4a0b30 --- "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" +++ "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 714.买卖股票的最佳时机含手续费 @@ -46,7 +44,7 @@ * 时间复杂度:O(n) * 空间复杂度:O(1) -本题使用贪心算法并不好理解,也很容易出错,那么我们再来看看是使用动规的方法如何解题。 +本题使用贪心算法并不好理解,也很容易出错,那么我们再来看看使用动规的方法如何解题。 相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 @@ -54,7 +52,7 @@ 这里重申一下dp数组的含义: -dp[i][0] 表示第i天持有股票所省最多现金。 +dp[i][0] 表示第i天持有股票所得最多现金。 dp[i][1] 表示第i天不持有股票所得最多现金 @@ -188,6 +186,20 @@ class Solution: return max(dp[-1][0], dp[-1][1]) ``` +```python +class Solution: + def maxProfit(self, prices: List[int], fee: int) -> int: + # 持有股票手上的最大現金 + hold = -prices[0] - fee + # 不持有股票手上的最大現金 + not_hold = 0 + for price in prices[1:]: + new_hold = max(hold, not_hold - price - fee) + new_not_hold = max(not_hold, hold + price) + hold, not_hold = new_hold, new_not_hold + return not_hold +``` + ### Go: ```go @@ -212,7 +224,7 @@ func max(a, b int) int { } ``` -### Javascript: +### JavaScript: ```javascript const maxProfit = (prices,fee) => { @@ -247,7 +259,29 @@ function maxProfit(prices: number[], fee: number): number { }; ``` +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// dp[i][0] 表示第i天持有股票所省最多现金。 +// dp[i][1] 表示第i天不持有股票所得最多现金 +int maxProfit(int* prices, int pricesSize, int fee) { + int dp[pricesSize][2]; + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (int i = 1; i < pricesSize; ++i) { + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); + } + return dp[pricesSize - 1][1]; +} +``` + + + ### Rust: + **贪心** ```Rust @@ -300,7 +334,4 @@ impl Solution { } ``` -

- - - + diff --git "a/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" "b/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" old mode 100644 new mode 100755 index 272cf2b2a4..12384a57a7 --- "a/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" +++ "b/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 718. 最长重复子数组 @@ -97,7 +95,7 @@ for (int i = 1; i <= nums1.size(); i++) { 拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下: -![718.最长重复子数组](https://code-thinking-1253855093.file.myqcloud.com/pics/2021011215282060.jpg) +![718.最长重复子数组](https://file1.kamacoder.com/i/algo/2021011215282060.jpg) 以上五部曲分析完毕,C++代码如下: @@ -129,7 +127,7 @@ public: 在如下图中: -![718.最长重复子数组](https://code-thinking-1253855093.file.myqcloud.com/pics/2021011215282060-20230310134554486.jpg) +![718.最长重复子数组](https://file1.kamacoder.com/i/algo/2021011215282060-20230310134554486.jpg) 我们可以看出dp[i][j]都是由dp[i - 1][j - 1]推出。那么压缩为一维数组,也就是dp[j]都是由dp[j - 1]推出。 @@ -560,10 +558,45 @@ impl Solution { } ``` +### C: +```c +int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size) { + int dp[nums1Size + 1][nums2Size + 1]; + memset(dp, 0, sizeof(dp)); + int result = 0; + for (int i = 1; i <= nums1Size; ++i) { + for (int j = 1; j <= nums2Size; ++j) { + if(nums1[i - 1] == nums2[j - 1]){ + dp[i][j] = dp[i - 1][j - 1] + 1; + } + if(dp[i][j] > result){ + result = dp[i][j]; + } + } + } + return result; +} +``` + +### Cangjie + +```cangjie +func findLength(nums1: Array, nums2: Array): Int64 { + let n = nums1.size + let m = nums2.size + let dp = Array(n + 1, {_ => Array(m + 1, item: 0)}) + var res = 0 + for (i in 1..=n) { + for (j in 1..=m) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } + res = max(res, dp[i][j]) + } + } + return res +} +``` -

- - - diff --git "a/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" "b/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" old mode 100644 new mode 100755 index 9ed8535e0d..bccca4f2d4 --- "a/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" +++ "b/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 724.寻找数组的中心下标 @@ -159,7 +157,3 @@ function pivotIndex(nums: number[]): number { -

- - - diff --git "a/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" "b/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" old mode 100644 new mode 100755 index 400dc90daa..17182778ae --- "a/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" +++ "b/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 738.单调递增的数字 @@ -190,9 +188,9 @@ class Solution: 贪心(版本一) ```python class Solution: - def monotoneIncreasingDigits(self, N: int) -> int: + def monotoneIncreasingDigits(self, n: int) -> int: # 将整数转换为字符串 - strNum = str(N) + strNum = str(n) # flag用来标记赋值9从哪里开始 # 设置为字符串长度,为了防止第二个for循环在flag没有被赋值的情况下执行 flag = len(strNum) @@ -216,9 +214,9 @@ class Solution: 贪心(版本二) ```python class Solution: - def monotoneIncreasingDigits(self, N: int) -> int: + def monotoneIncreasingDigits(self, n: int) -> int: # 将整数转换为字符串 - strNum = list(str(N)) + strNum = list(str(n)) # 从右往左遍历字符串 for i in range(len(strNum) - 1, 0, -1): @@ -238,9 +236,9 @@ class Solution: ```python class Solution: - def monotoneIncreasingDigits(self, N: int) -> int: + def monotoneIncreasingDigits(self, n: int) -> int: # 将整数转换为字符串 - strNum = list(str(N)) + strNum = list(str(n)) # 从右往左遍历字符串 for i in range(len(strNum) - 1, 0, -1): @@ -258,8 +256,8 @@ class Solution: ```python class Solution: - def monotoneIncreasingDigits(self, N: int) -> int: - strNum = str(N) + def monotoneIncreasingDigits(self, n: int) -> int: + strNum = str(n) for i in range(len(strNum) - 1, 0, -1): # 如果当前字符比前一个字符小,说明需要修改前一个字符 if strNum[i - 1] > strNum[i]: @@ -272,27 +270,25 @@ class Solution: ``` ### Go ```go -func monotoneIncreasingDigits(N int) int { - s := strconv.Itoa(N)//将数字转为字符串,方便使用下标 - ss := []byte(s)//将字符串转为byte数组,方便更改。 - n := len(ss) - if n <= 1 { - return N - } - for i := n-1; i > 0; i-- { - if ss[i-1] > ss[i] { //前一个大于后一位,前一位减1,后面的全部置为9 - ss[i-1] -= 1 - for j := i; j < n; j++ { //后面的全部置为9 - ss[j] = '9' - } - } - } - res, _ := strconv.Atoi(string(ss)) - return res +func monotoneIncreasingDigits(n int) int { + s := strconv.Itoa(n) + // 从左到右遍历字符串,找到第一个不满足单调递增的位置 + for i := len(s) - 2; i >= 0; i-- { + if s[i] > s[i+1] { + // 将该位置的数字减1 + s = s[:i] + string(s[i]-1) + s[i+1:] + // 将该位置之后的所有数字置为9 + for j := i + 1; j < len(s); j++ { + s = s[:j] + "9" + s[j+1:] + } + } + } + result, _ := strconv.Atoi(s) + return result } ``` -### Javascript +### JavaScript ```Javascript var monotoneIncreasingDigits = function(n) { n = n.toString() @@ -392,7 +388,33 @@ impl Solution { } } ``` +### C + +```c +int monotoneIncreasingDigits(int n) { + char str[11]; + // 将数字转换为字符串 + sprintf(str, "%d", n); + int len = strlen(str); + int flag = strlen(str); + for(int i = len - 1; i > 0; i--){ + if(str[i] < str[i - 1]){ + str[i - 1]--; + flag = i; + } + } + for(int i = flag; i < len; i++){ + str[i] = '9'; + } + // 字符串转数字 + return atoi(str); +} +``` + + + ### C# + ```csharp public class Solution { @@ -417,8 +439,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" "b/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" old mode 100644 new mode 100755 index fdb11c63e3..2ad7e6b79b --- "a/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" +++ "b/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -71,7 +69,7 @@ 首先先将第一个遍历元素加入单调栈 -![739.每日温度1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124434172.jpg) +![739.每日温度1](https://file1.kamacoder.com/i/algo/20210219124434172.jpg) --------- @@ -79,65 +77,65 @@ 我们要保持一个递增单调栈(从栈头到栈底),所以将T[0]弹出,T[1]加入,此时result数组可以记录了,result[0] = 1,即T[0]右面第一个比T[0]大的元素是T[1]。 -![739.每日温度2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124504299.jpg) +![739.每日温度2](https://file1.kamacoder.com/i/algo/20210219124504299.jpg) ----------- 加入T[2],同理,T[1]弹出 -![739.每日温度3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124527361.jpg) +![739.每日温度3](https://file1.kamacoder.com/i/algo/20210219124527361.jpg) ------- 加入T[3],T[3] < T[2] (当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况),加T[3]加入单调栈。 -![739.每日温度4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124610761.jpg) +![739.每日温度4](https://file1.kamacoder.com/i/algo/20210219124610761.jpg) --------- 加入T[4],T[4] == T[3] (当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况),此时依然要加入栈,不用计算距离,因为我们要求的是右面第一个大于本元素的位置,而不是大于等于! -![739.每日温度5](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124633444.jpg) +![739.每日温度5](https://file1.kamacoder.com/i/algo/20210219124633444.jpg) --------- 加入T[5],T[5] > T[4] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[4]弹出,同时计算距离,更新result -![739.每日温度6](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124700567.jpg) +![739.每日温度6](https://file1.kamacoder.com/i/algo/20210219124700567.jpg) ---------- T[4]弹出之后, T[5] > T[3] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[3]继续弹出,同时计算距离,更新result -![739.每日温度7](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124726613.jpg) +![739.每日温度7](https://file1.kamacoder.com/i/algo/20210219124726613.jpg) ------- 直到发现T[5]小于T[st.top()],终止弹出,将T[5]加入单调栈 -![739.每日温度8](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124807715.jpg) +![739.每日温度8](https://file1.kamacoder.com/i/algo/20210219124807715.jpg) ------- 加入T[6],同理,需要将栈里的T[5],T[2]弹出 -![739.每日温度9](https://code-thinking-1253855093.file.myqcloud.com/pics/2021021912483374.jpg) +![739.每日温度9](https://file1.kamacoder.com/i/algo/2021021912483374.jpg) ------- 同理,继续弹出 -![739.每日温度10](https://code-thinking-1253855093.file.myqcloud.com/pics/2021021912490098.jpg) +![739.每日温度10](https://file1.kamacoder.com/i/algo/2021021912490098.jpg) ------ 此时栈里只剩下了T[6] -![739.每日温度11](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124930156.jpg) +![739.每日温度11](https://file1.kamacoder.com/i/algo/20210219124930156.jpg) ------------ 加入T[7], T[7] < T[6] 直接入栈,这就是最后的情况,result数组也更新完了。 -![739.每日温度12](https://code-thinking-1253855093.file.myqcloud.com/pics/20210219124957216.jpg) +![739.每日温度12](https://file1.kamacoder.com/i/algo/20210219124957216.jpg) 此时有同学可能就疑惑了,那result[6] , result[7]怎么没更新啊,元素也一直在栈里。 @@ -215,6 +213,38 @@ public: ## 其他语言版本 +### C: + +```C +/** + * Note: The returned array must be malloced, assume caller calls free(). + */ +int* dailyTemperatures(int* temperatures, int temperaturesSize, int* returnSize) { + int len = temperaturesSize; + *returnSize = len; + + int *result = (int *)malloc(sizeof(int) * len); + memset(result, 0x00, sizeof(int) * len); + + int stack[len]; + memset(stack, 0x00, sizeof(stack)); + int top = 0; + + for (int i = 1; i < len; i++) { + if (temperatures[i] <= temperatures[stack[top]]) { /* push */ + stack[++top] = i; + } else { + while (top >= 0 && temperatures[i] > temperatures[stack[top]]) { /* stack not empty */ + result[stack[top]] = i - stack[top]; + top--; /* pop */ + } + stack[++top] = i; /* push */ + } + } + return result; +} +``` + ### Java: ```java @@ -482,8 +512,4 @@ impl Solution { ``` -

- - - diff --git "a/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" "b/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" new file mode 100644 index 0000000000..40b699c18f --- /dev/null +++ "b/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" @@ -0,0 +1,1230 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 743.网络延迟时间 + +https://leetcode.cn/problems/network-delay-time/description/ + + +有 n 个网络节点,标记为 1 到 n。 + +给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。 + +现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。 + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://file1.kamacoder.com/i/algo/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" "b/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" old mode 100644 new mode 100755 index d13ff19f5a..952d4d2ab7 --- "a/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" +++ "b/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -54,7 +52,7 @@ 请你计算并返回达到楼梯顶部的最低花费。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221031170131.png) +![](https://file1.kamacoder.com/i/algo/20221031170131.png) ## 思路 @@ -114,7 +112,7 @@ dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。 拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221026175104.png) +![](https://file1.kamacoder.com/i/algo/20221026175104.png) 如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。 @@ -244,6 +242,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 +535,4 @@ public class Solution -

- - - diff --git "a/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" "b/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" old mode 100644 new mode 100755 index 4e9ec578bc..d17878381f --- "a/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" +++ "b/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 763.划分字母区间 @@ -46,7 +44,7 @@ 如图: -![763.划分字母区间](https://code-thinking-1253855093.file.myqcloud.com/pics/20201222191924417.png) +![763.划分字母区间](https://file1.kamacoder.com/i/algo/20201222191924417.png) 明白原理之后,代码并不复杂,如下: @@ -312,7 +310,7 @@ func max(a, b int) int { } ``` -### Javascript +### JavaScript ```Javascript var partitionLabels = function(s) { let hash = {} @@ -404,7 +402,38 @@ impl Solution { } } ``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int* partitionLabels(char* s, int* returnSize) { + // 记录每个字符最远出现的位置 + int last[26] = {0}; + int len = strlen(s); + for (int i = 0; i < len; ++i) { + last[s[i] - 'a'] = i; + } + int left = 0, right = 0; + int * partition = malloc(sizeof (int ) * len); + // 初始化值 + *returnSize = 0; + for(int i = 0; i < len; i++){ + right = max(right, last[s[i] - 'a']); + // 到达最远位置,加入答案,并且更新左边下标 + if(i == right){ + partition[(*returnSize)++] = right - left + 1; + left = i + 1; + } + } + return partition; +} +``` + + + ### C# + ```csharp public class Solution { @@ -431,8 +460,3 @@ public class Solution } ``` -

- - - - diff --git "a/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" "b/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" new file mode 100644 index 0000000000..fb58c14816 --- /dev/null +++ "b/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" @@ -0,0 +1,180 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 787. K 站中转内最便宜的航班 + +有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。 + +现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。 + + +![](https://file1.kamacoder.com/i/algo/20240319103900.png) + +![](https://file1.kamacoder.com/i/algo/20240319103919.png) + +![](https://file1.kamacoder.com/i/algo/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.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" "b/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" old mode 100644 new mode 100755 index 05b55b5b16..db4d249a15 --- "a/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" +++ "b/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 797.所有可能的路径 @@ -13,7 +11,7 @@ graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221203135439.png) +![](https://file1.kamacoder.com/i/algo/20221203135439.png) 提示: @@ -52,7 +50,7 @@ graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节 至于 单一路径,和路径集合可以放在全局变量,那么代码是这样的: -```c++ +```CPP vector> result; // 收集符合条件的路径 vector path; // 0节点到终点的路径 // x:目前遍历的节点 @@ -71,7 +69,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); // 收集有效路径 @@ -98,19 +96,19 @@ path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 一些录友可以疑惑这里如果找到x 链接的节点的,例如如果x目前是节点0,那么目前的过程就是这样的: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221204111937.png) +![](https://file1.kamacoder.com/i/algo/20221204111937.png) 二维数组中,graph[x][i] 都是x链接的节点,当前遍历的节点就是 `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 +118,7 @@ for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节 本题整体代码如下: -```c++ +```CPP class Solution { private: vector> result; // 收集符合条件的路径 @@ -294,7 +292,3 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" "b/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" old mode 100644 new mode 100755 index 3f32dd7aa1..118735e943 --- "a/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" +++ "b/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 827.最大人工岛 @@ -39,7 +37,7 @@ 每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为:n^4。 -如果对深度优先搜索不了解的录友,可以看这里:[深度优先搜索精讲](https://programmercarl.com/图论深搜理论基础.html) +如果对深度优先搜索不了解的录友,可以看这里:[深度优先搜索精讲](https://programmercarl.com/kamacoder/图论深搜理论基础.html) ## 优化思路 @@ -53,11 +51,11 @@ 拿如下地图的岛屿情况来举例: (1为陆地) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829104834.png) +![](https://file1.kamacoder.com/i/algo/20220829104834.png) 第一步,则遍历题目,并将岛屿到编号和面积上的统计,过程如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829105644.png) +![](https://file1.kamacoder.com/i/algo/20220829105644.png) 本过程代码如下: @@ -104,7 +102,7 @@ int largestIsland(vector>& grid) { 第二步过程如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220829105249.png) +![](https://file1.kamacoder.com/i/algo/20220829105249.png) 也就是遍历每一个0的方格,并统计其相邻岛屿面积,最后取一个最大值。 @@ -348,6 +346,99 @@ class Solution: ``` + +### Go + +```go +func largestIsland(grid [][]int) int { + dir := [][]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} + n := len(grid) + m := len(grid[0]) + area := 0 + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + gridNum := make(map[int]int, 0) // 记录每一个岛屿的面积 + mark := 2 // 记录每个岛屿的编号 + isAllGrid := true + res := 0 // 标记是否整个地图都是陆地 + + var dfs func(grid [][]int, visited [][]bool, x, y, mark int) + dfs = func(grid [][]int, visited [][]bool, x, y, mark int) { + // 终止条件:访问过的节点 或者 遇到海水 + if visited[x][y] || grid[x][y] == 0 { + return + } + visited[x][y] = true // 标记访问过 + grid[x][y] = mark // 给陆地标记新标签 + area++ + for i := 0; i < 4; i++ { + nextX := x + dir[i][0] + nextY := y + dir[i][1] + if nextX < 0 || nextX >= len(grid) || nextY < 0 || nextY >= len(grid[0]) { + continue + } + dfs(grid, visited, nextX, nextY, mark) + } + } + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 0 { + isAllGrid = false + } + if !visited[i][j] && grid[i][j] == 1 { + area = 0 + dfs(grid, visited, i, j, mark) // 将与其链接的陆地都标记上 true + gridNum[mark] = area // 记录每一个岛屿的面积 + mark++ // 更新下一个岛屿编号 + } + } + } + if isAllGrid { + return n * m + } + // 根据添加陆地的位置,计算周边岛屿面积之和 + visitedGrid := make(map[int]struct{}) // 标记访问过的岛屿 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + count := 1 // 记录连接之后的岛屿数量 + visitedGrid = make(map[int]struct{}) // 每次使用时,清空 + if grid[i][j] == 0 { + for k := 0; k < 4; k++ { + // 计算相邻坐标 + nearI := i + dir[k][0] + nearJ := j + dir[k][1] + if nearI < 0 || nearI >= len(grid) || nearJ < 0 || nearJ >= len(grid[0]) { + continue + } + // 添加过的岛屿不要重复添加 + if _, ok := visitedGrid[grid[nearI][nearJ]]; ok { + continue + } + // 把相邻四面的岛屿数量加起来 + count += gridNum[grid[nearI][nearJ]] + // 标记该岛屿已经添加过 + visitedGrid[grid[nearI][nearJ]] = struct{}{} + } + } + res = max827(res, count) + } + } + return res +} + +func max827(x, y int) int { + if x > y { + return x + } + return y +} + +``` + + ### JavaScript ```JavaScript @@ -411,9 +502,3 @@ return res; ``` - - -

- - - diff --git "a/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" "b/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" old mode 100644 new mode 100755 index b4785d1b02..ffcf2fb919 --- "a/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" +++ "b/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -37,7 +35,7 @@ 图中给我的两个示例: `[[1],[2],[3],[]]` `[[1,3],[3,0,1],[2],[0]]`,画成对应的图如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220714101414.png) +![](https://file1.kamacoder.com/i/algo/20220714101414.png) 我们可以看出图1的所有节点都是链接的,而图二中,节点2 是孤立的。 @@ -50,7 +48,7 @@ 图3:[[5], [], [1, 3], [5]] ,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220714102201.png) +![](https://file1.kamacoder.com/i/algo/20220714102201.png) 在图3中,大家可以发现,节点0只能到节点5,然后就哪也去不了了。 @@ -484,7 +482,3 @@ function canVisitAllRooms(rooms: number[][]): boolean { ``` -

- - - diff --git "a/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" "b/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index c7f5220288..6d0cd68578 --- "a/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 844.比较含退格的字符串 @@ -108,7 +106,7 @@ public: 动画如下: - + 如果S[i]和S[j]不相同返回false,如果有一个指针(i或者j)先走到的字符串头部位置,也返回false。 @@ -587,8 +585,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" "b/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" old mode 100644 new mode 100755 index db70112d8f..aeb470fe5a --- "a/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" +++ "b/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 860.柠檬水找零 @@ -226,7 +224,7 @@ func lemonadeChange(bills []int) bool { } ``` -### Javascript +### JavaScript ```Javascript var lemonadeChange = function(bills) { let fiveCount = 0 @@ -439,8 +437,4 @@ public class Solution ``` -

- - - diff --git "a/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" "b/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" old mode 100644 new mode 100755 index 72be8fa732..484099f89f --- "a/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" +++ "b/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -11,9 +9,9 @@ [力扣题目链接](https://leetcode.cn/problems/sort-array-by-parity-ii/) -给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。 +给定一个非负整数数组 nums, nums 中一半整数是奇数,一半整数是偶数。 -对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。 +对数组进行排序,以便当 nums[i] 为奇数时,i 也是奇数;当 nums[i] 为偶数时, i 也是偶数。 你可以返回任何满足上述条件的数组作为答案。 @@ -35,17 +33,17 @@ ```CPP class Solution { public: - vector sortArrayByParityII(vector& A) { - vector even(A.size() / 2); // 初始化就确定数组大小,节省开销 - vector odd(A.size() / 2); - vector result(A.size()); + vector sortArrayByParityII(vector& nums) { + vector even(nums.size() / 2); // 初始化就确定数组大小,节省开销 + vector odd(nums.size() / 2); + vector result(nums.size()); int evenIndex = 0; int oddIndex = 0; int resultIndex = 0; - // 把A数组放进偶数数组,和奇数数组 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) even[evenIndex++] = A[i]; - else odd[oddIndex++] = A[i]; + // 把nums数组放进偶数数组,和奇数数组 + for (int i = 0; i < nums.size(); i++) { + if (nums[i] % 2 == 0) even[evenIndex++] = nums[i]; + else odd[oddIndex++] = nums[i]; } // 把偶数数组,奇数数组分别放进result数组中 for (int i = 0; i < evenIndex; i++) { @@ -62,22 +60,22 @@ public: ### 方法二 -以上代码我是建了两个辅助数组,而且A数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助树,代码如下: +以上代码我是建了两个辅助数组,而且nums数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助数组,代码如下: ```CPP class Solution { public: - vector sortArrayByParityII(vector& A) { - vector result(A.size()); + vector sortArrayByParityII(vector& nums) { + vector result(nums.size()); int evenIndex = 0; // 偶数下标 int oddIndex = 1; // 奇数下标 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) { - result[evenIndex] = A[i]; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] % 2 == 0) { + result[evenIndex] = nums[i]; evenIndex += 2; } else { - result[oddIndex] = A[i]; + result[oddIndex] = nums[i]; oddIndex += 2; } } @@ -96,15 +94,15 @@ public: ```CPP class Solution { public: - vector sortArrayByParityII(vector& A) { + vector sortArrayByParityII(vector& nums) { int oddIndex = 1; - for (int i = 0; i < A.size(); i += 2) { - if (A[i] % 2 == 1) { // 在偶数位遇到了奇数 - while(A[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数 - swap(A[i], A[oddIndex]); // 替换 + for (int i = 0; i < nums.size(); i += 2) { + if (nums[i] % 2 == 1) { // 在偶数位遇到了奇数 + while(nums[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数 + swap(nums[i], nums[oddIndex]); // 替换 } } - return A; + return nums; } }; ``` @@ -253,6 +251,37 @@ func sortArrayByParityII(nums []int) []int { } return result; } + +// 方法二 +func sortArrayByParityII(nums []int) []int { + result := make([]int, len(nums)) + evenIndex := 0 // 偶数下标 + oddIndex := 1 // 奇数下标 + for _, v := range nums { + if v % 2 == 0 { + result[evenIndex] = v + evenIndex += 2 + } else { + result[oddIndex] = v + oddIndex += 2 + } + } + return result +} + +// 方法三 +func sortArrayByParityII(nums []int) []int { + oddIndex := 1 + for i := 0; i < len(nums); i += 2 { + if nums[i] % 2 == 1 { // 在偶数位遇到了奇数 + for nums[oddIndex] % 2 != 0 { + oddIndex += 2 // 在奇数位找一个偶数 + } + nums[i], nums[oddIndex] = nums[oddIndex], nums[i] + } + } + return nums +} ``` ### JavaScript @@ -378,8 +407,4 @@ function sortArrayByParityII(nums: number[]): number[] { }; ``` -

- - - diff --git "a/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" "b/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" old mode 100644 new mode 100755 index 11edecd0d1..f653caef52 --- "a/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" +++ "b/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 925.长按键入 @@ -54,7 +52,7 @@ 动画如下: - + 上面的逻辑想清楚了,不难写出如下C++代码: @@ -227,7 +225,3 @@ function isLongPressedName(name: string, typed: string): boolean { -

- - - diff --git "a/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" "b/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" old mode 100644 new mode 100755 index 48c29eb479..9f000e2bb5 --- "a/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" +++ "b/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 941.有效的山脉数组 @@ -18,7 +16,7 @@ * arr[0] < arr[1] < ... arr[i-1] < arr[i] * arr[i] > arr[i+1] > ... > arr[arr.length - 1] -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210729103604.png) +![](https://file1.kamacoder.com/i/algo/20210729103604.png) 示例 1: * 输入:arr = [2,1] @@ -39,7 +37,7 @@ 这样可以使用两个指针,left和right,让其按照如下规则移动,如图: - + **注意这里还是有一些细节,例如如下两点:** @@ -196,9 +194,20 @@ public class Solution { } ``` - -

- - - +### Rust +```rust +impl Solution { + pub fn valid_mountain_array(arr: Vec) -> bool { + let mut i = 0; + let mut j = arr.len() - 1; + while i < arr.len() - 1 && arr[i] < arr[i + 1] { + i += 1; + } + while j > 0 && arr[j] < arr[j - 1] { + j -= 1; + } + i > 0 && j < arr.len() - 1 && i == j + } +} +``` diff --git "a/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" "b/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" old mode 100644 new mode 100755 index 9743ca2b80..989993acf5 --- "a/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -19,7 +17,7 @@ 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229175736596.png) +![](https://file1.kamacoder.com/i/algo/20201229175736596.png) * 输入:[0,0,null,0,0] * 输出:1 @@ -27,7 +25,7 @@ 示例 2: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/2020122917584449.png) +![](https://file1.kamacoder.com/i/algo/2020122917584449.png) * 输入:[0,0,null,0,null,0,null,null,0] * 输出:2 @@ -145,7 +143,7 @@ if (cur == NULL) return 2; 如图: -![968.监控二叉树2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229203710729.png) +![968.监控二叉树2](https://file1.kamacoder.com/i/algo/20201229203710729.png) 代码如下: @@ -193,7 +191,7 @@ if (left == 1 || right == 1) return 2; **从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了**,如图: -![968.监控二叉树1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020122920362355.png) +![968.监控二叉树1](https://file1.kamacoder.com/i/algo/2020122920362355.png) 这种情况也是大多数同学容易迷惑的情况。 @@ -201,7 +199,7 @@ if (left == 1 || right == 1) return 2; 以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图: -![968.监控二叉树3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229203742446.png) +![968.监控二叉树3](https://file1.kamacoder.com/i/algo/20201229203742446.png) 所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下: @@ -536,7 +534,7 @@ func min(a, b int) int { ``` -### Javascript +### JavaScript ```Javascript var minCameraCover = function(root) { @@ -789,7 +787,3 @@ public class Solution } ``` -

- - - diff --git "a/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" "b/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" old mode 100644 new mode 100755 index 5bdbcbc7e0..1f58fd5198 --- "a/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" +++ "b/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 双指针风骚起来,也是无敌 @@ -63,7 +61,7 @@ public: 如动画所示: -![](https://code-thinking.cdn.bcebos.com/gifs/977.有序数组的平方.gif) +![](https://file1.kamacoder.com/i/algo/977.有序数组的平方.gif) 不难写出如下代码: @@ -101,6 +99,19 @@ public: ### Java: +排序法 +```Java +class Solution { + public int[] sortedSquares(int[] nums) { + for (int i = 0; i < nums.length; i++) { + nums[i] = nums[i] * nums[i]; + } + Arrays.sort(nums); + return nums; + } +} +``` + ```Java class Solution { public int[] sortedSquares(int[] nums) { @@ -178,9 +189,76 @@ class Solution: return sorted(x*x for x in nums) ``` +```Python +(版本四) 双指针+ 反转列表 +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + #根据list的先进排序在先原则 + #将nums的平方按从大到小的顺序添加进新的list + #最后反转list + new_list = [] + left, right = 0 , len(nums) -1 + while left <= right: + if abs(nums[left]) <= abs(nums[right]): + new_list.append(nums[right] ** 2) + right -= 1 + else: + new_list.append(nums[left] ** 2) + left += 1 + return new_list[::-1] +``` + +```python3 +(双指针优化版本) 三步优化 + class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + """ + 整体思想:有序数组的绝对值最大值永远在两头,比较两头,平方大的插到新数组的最后 + 优 化:1. 优化所有元素为非正或非负的情况 + 2. 头尾平方的大小比较直接将头尾相加与0进行比较即可 + 3. 新的平方排序数组的插入索引可以用倒序插入实现(针对for循环,while循环不适用) + """ + + # 特殊情况, 元素都非负(优化1) + if nums[0] >= 0: + return [num ** 2 for num in nums] # 按顺序平方即可 + # 最后一个非正,全负有序的 + if nums[-1] <= 0: + return [x ** 2 for x in nums[::-1]] # 倒序平方后的数组 + + # 一般情况, 有正有负 + i = 0 # 原数组头索引 + j = len(nums) - 1 # 原数组尾部索引 + new_nums = [0] * len(nums) # 新建一个等长数组用于保存排序后的结果 + # end_index = len(nums) - 1 # 新的排序数组(是新数组)尾插索引, 每次需要减一(优化3优化了) + + for end_index in range(len(nums)-1, -1, -1): # (优化3,倒序,不用单独创建变量) + # if nums[i] ** 2 >= nums[j] ** 2: + if nums[i] + nums[j] <= 0: # (优化2) + new_nums[end_index] = nums[i] ** 2 + i += 1 + # end_index -= 1 (优化3) + else: + new_nums[end_index] = nums[j] ** 2 + j -= 1 + # end_index -= 1 (优化3) + return new_nums +``` + ### 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 @@ -221,7 +299,7 @@ impl Solution { } } ``` -### Javascript: +### JavaScript: ```Javascript /** @@ -247,7 +325,7 @@ var sortedSquares = function(nums) { }; ``` -### Typescript: +### TypeScript: 双指针法: @@ -498,7 +576,7 @@ public class Solution { return result; } } - +``` C# LINQ: ```csharp public class Solution { @@ -507,8 +585,5 @@ public class Solution { } } ``` -

- - - + diff --git "a/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" "b/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" old mode 100644 new mode 100755 index 8d81e3f8a0..cbf5ecdb78 --- "a/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" +++ "b/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -55,7 +53,7 @@ words[i] 由小写英文字母组成 如图: -![1002.查找常用字符](https://code-thinking.cdn.bcebos.com/pics/1002.查找常用字符.png) +![1002.查找常用字符](https://file1.kamacoder.com/i/algo/1002.查找常用字符.png) 先统计第一个字符串所有字符出现的次数,代码如下: @@ -581,8 +579,4 @@ def common_chars(words) end ``` -

- - - diff --git "a/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" "b/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" old mode 100644 new mode 100755 index 498015d0cd..6e908d5af6 --- "a/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" +++ "b/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1005.K次取反后最大化的数组和 @@ -128,6 +126,36 @@ class Solution { } } + +// 版本二:排序数组并贪心地尽可能将负数翻转为正数,再根据剩余的k值调整最小元素的符号,从而最大化数组的总和。 +class Solution { + public int largestSumAfterKNegations(int[] nums, int k) { + if (nums.length == 1) return nums[0]; + + // 排序:先把负数处理了 + Arrays.sort(nums); + + for (int i = 0; i < nums.length && k > 0; i++) { // 贪心点, 通过负转正, 消耗尽可能多的k + if (nums[i] < 0) { + nums[i] = -nums[i]; + k--; + } + } + + // 退出循环, k > 0 || k < 0 (k消耗完了不用讨论) + if (k % 2 == 1) { // k > 0 && k is odd:对于负数:负-正-负-正 + Arrays.sort(nums); // 再次排序得到剩余的负数,或者最小的正数 + nums[0] = -nums[0]; + } + // k > 0 && k is even,flip数字不会产生影响: 对于负数: 负-正-负;对于正数:正-负-正 + + int sum = 0; + for (int num : nums) { // 计算最大和 + sum += num; + } + return sum; + } +} ``` ### Python @@ -177,7 +205,7 @@ func largestSumAfterKNegations(nums []int, K int) int { ``` -### Javascript +### JavaScript ```Javascript var largestSumAfterKNegations = function(nums, k) { @@ -347,7 +375,3 @@ public class Solution -

- - - diff --git "a/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" "b/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" old mode 100644 new mode 100755 index 3488777ad2..396e6c566b --- "a/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" +++ "b/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1020. 飞地的数量 @@ -14,13 +12,13 @@ 返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830100710.png) +![](https://file1.kamacoder.com/i/algo/20220830100710.png) * 输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]] * 输出:3 * 解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830100742.png) +![](https://file1.kamacoder.com/i/algo/20220830100742.png) * 输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]] * 输出:0 @@ -34,11 +32,11 @@ 如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色, -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830104632.png) +![](https://file1.kamacoder.com/i/algo/20220830104632.png) 在遇到地图周边陆地的时候,将1都变为0,此时地图为这样: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830104651.png) +![](https://file1.kamacoder.com/i/algo/20220830104651.png) 然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。 @@ -395,7 +393,7 @@ class Solution { 深度优先遍历 -```Python3 +```Python class Solution: def __init__(self): self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] # 四个方向 @@ -442,7 +440,7 @@ class Solution: 广度优先遍历 -```Python3 +```Python class Solution: def __init__(self): self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] # 四个方向 @@ -605,6 +603,63 @@ func bfs(grid [][]int, i, j int) { } ``` +### JavaScript + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var numEnclaves = function (grid) { + let row = grid.length; + let col = grid[0].length; + let count = 0; + + // Check the first and last row, if there is a 1, then change all the connected 1s to 0 and don't count them. + for (let j = 0; j < col; j++) { + if (grid[0][j] === 1) { + dfs(0, j, false); + } + if (grid[row - 1][j] === 1) { + dfs(row - 1, j, false); + } + } + + // Check the first and last column, if there is a 1, then change all the connected 1s to 0 and don't count them. + for (let i = 0; i < row; i++) { + if (grid[i][0] === 1) { + dfs(i, 0, false); + } + if (grid[i][col - 1] === 1) { + dfs(i, col - 1, false); + } + } + + // Check the rest of the grid, if there is a 1, then change all the connected 1s to 0 and count them. + for (let i = 1; i < row - 1; i++) { + for (let j = 1; j < col - 1; j++) { + dfs(i, j, true); + } + } + + function dfs(i, j, isCounting) { + let condition = i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] === 0; + + if (condition) return; + if (isCounting) count++; + + grid[i][j] = 0; + + dfs(i - 1, j, isCounting); + dfs(i + 1, j, isCounting); + dfs(i, j - 1, isCounting); + dfs(i, j + 1, isCounting); + } + + return count; +}; +``` + ### Rust dfs: @@ -695,8 +750,5 @@ impl Solution { * 1254. 统计封闭岛屿的数目 -

- - - + diff --git "a/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" "b/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" old mode 100644 new mode 100755 index e0625a2ba3..16bf869ea6 --- "a/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" +++ "b/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" @@ -1,21 +1,24 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1035.不相交的线 [力扣题目链接](https://leetcode.cn/problems/uncrossed-lines/) -我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。 +在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 -现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。 +现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: -以这种方法绘制线条,并返回我们可以绘制的最大连线数。 +* nums1[i] == nums2[j] +* 且绘制的直线不与任何其他连线(非水平线)相交。 +请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。 -![1035.不相交的线](https://code-thinking-1253855093.file.myqcloud.com/pics/2021032116363533.png) +以这种方法绘制线条,并返回可以绘制的最大连线数。 + + +![1035.不相交的线](https://file1.kamacoder.com/i/algo/2021032116363533.png) ## 算法公开课 @@ -26,16 +29,16 @@ 相信不少录友看到这道题目都没啥思路,我们来逐步分析一下。 -绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交! +绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,只要 nums1[i] == nums2[j],且直线不能相交! -直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。 +直线不能相交,这就是说明在字符串nums1中 找到一个与字符串nums2相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。 -拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图: +拿示例一nums1 = [1,4,2], nums2 = [1,2,4]为例,相交情况如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914145158.png) +![](https://file1.kamacoder.com/i/algo/20210914145158.png) -其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面) +其实也就是说nums1和nums2的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串nums1中数字1的后面,那么数字4也应该在字符串nums2数字1的后面) 这么分析完之后,大家可以发现:**本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!** @@ -52,18 +55,18 @@ ```CPP class Solution { public: - int maxUncrossedLines(vector& A, vector& B) { - vector> dp(A.size() + 1, vector(B.size() + 1, 0)); - for (int i = 1; i <= A.size(); i++) { - for (int j = 1; j <= B.size(); j++) { - if (A[i - 1] == B[j - 1]) { + int maxUncrossedLines(vector& nums1, vector& nums2) { + vector> dp(nums1.size() + 1, vector(nums2.size() + 1, 0)); + for (int i = 1; i <= nums1.size(); i++) { + for (int j = 1; j <= nums2.size(); j++) { + if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); } } } - return dp[A.size()][B.size()]; + return dp[nums1.size()][nums2.size()]; } }; ``` @@ -110,11 +113,11 @@ public: ```python class Solution: - def maxUncrossedLines(self, A: List[int], B: List[int]) -> int: - dp = [[0] * (len(B)+1) for _ in range(len(A)+1)] - for i in range(1, len(A)+1): - for j in range(1, len(B)+1): - if A[i-1] == B[j-1]: + def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int: + dp = [[0] * (len(nums2)+1) for _ in range(len(nums1)+1)] + for i in range(1, len(nums1)+1): + for j in range(1, len(nums2)+1): + if nums1[i-1] == nums2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) @@ -124,23 +127,22 @@ class Solution: ### Go: ```go -func maxUncrossedLines(A []int, B []int) int { - m, n := len(A), len(B) - dp := make([][]int, m+1) +func maxUncrossedLines(nums1 []int, nums2 []int) int { + dp := make([][]int, len(nums1) + 1) for i := range dp { - dp[i] = make([]int, n+1) + dp[i] = make([]int, len(nums2) + 1) } - for i := 1; i <= len(A); i++ { - for j := 1; j <= len(B); j++ { - if (A[i - 1] == B[j - 1]) { + for i := 1; i <= len(nums1); i++ { + for j := 1; j <= len(nums2); j++ { + if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1 } else { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) } } } - return dp[m][n] + return dp[len(nums1)][len(nums2)] } @@ -273,8 +275,4 @@ function maxUncrossedLines(nums1: number[], nums2: number[]): number { ``` -

- - - diff --git "a/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" "b/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" old mode 100644 new mode 100755 index ffe1353039..36702194be --- "a/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" +++ "b/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 匹配问题都是栈的强项 @@ -48,7 +46,7 @@ 然后再去做对应的消除操作。 如动画所示: -![1047.删除字符串中的所有相邻重复项](https://code-thinking.cdn.bcebos.com/gifs/1047.删除字符串中的所有相邻重复项.gif) +![1047.删除字符串中的所有相邻重复项](https://file1.kamacoder.com/i/algo/1047.删除字符串中的所有相邻重复项.gif) 从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。 @@ -164,7 +162,7 @@ class Solution { int top = -1; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); - // 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top-- + // 当 top >= 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top-- if (top >= 0 && res.charAt(top) == c) { res.deleteCharAt(top); top--; @@ -241,6 +239,33 @@ class Solution: ### Go: +使用栈 +```go +func removeDuplicates(s string) string { + stack := make([]rune, 0) + for _, val := range s { + if len(stack) == 0 || val != stack[len(stack)-1] { + stack = append(stack, val) + } else { + stack = stack[:len(stack)-1] + } + } + var res []rune + for len(stack) != 0 { // 将栈中元素放到result字符串汇总 + res = append(res, stack[len(stack)-1]) + stack = stack[:len(stack)-1] + } + // 此时字符串需要反转一下 + l, r := 0, len(res)-1 + for l < r { + res[l], res[r] = res[r], res[l] + l++ + r-- + } + return string(res) +} +``` +拿字符串直接作为栈,省去了栈还要转为字符串的操作 ```go func removeDuplicates(s string) string { var stack []byte @@ -495,8 +520,4 @@ def remove_duplicates(s) end ``` -

- - - diff --git "a/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" "b/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" old mode 100644 new mode 100755 index 4c3c01a037..ddc9f313db --- "a/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" +++ "b/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1049.最后一块石头的重量II @@ -42,40 +40,41 @@ ## 思路 -如果对背包问题不都熟悉先看这两篇: +如果对背包问题不熟悉的话先看这两篇: -* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) -* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) -本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,**这样就化解成01背包问题了**。 +本题其实是尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。 -是不是感觉和昨天讲解的[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)非常像了。 +一堆的石头重量是sum,那么我们就尽可能拼成 重量为 sum / 2 的石头堆。 这样剩下的石头堆也是 尽可能接近 sum/2 的重量。 +那么此时问题就是有一堆石头,每个石头都有自己的重量,是否可以 装满 最大重量为 sum / 2的背包。 -本题物品的重量为stones[i],物品的价值也为stones[i]。 +看到这里,大家是否感觉和昨天讲解的 [416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)非常像了,简直就是同一道题。 -对应着01背包里的物品重量weight[i]和 物品价值value[i]。 +本题**这样就化解成01背包问题了**。 + +**[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html) 是求背包是否正好装满,而本题是求背包最多能装多少**。 + +物品就是石头,物品的重量为stones[i],物品的价值也为stones[i]。 接下来进行动规五步曲: -1. 确定dp数组以及下标的含义 +### 1. 确定dp数组以及下标的含义 **dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]**。 -可以回忆一下01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。 +相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] 。 -相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]” +“最多可以装的价值为 dp[j]” 等同于 “最多可以背的重量为dp[j]” -2. 确定递推公式 +### 2. 确定递推公式 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 本题则是:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);** -一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 - -大家可以再去看 dp[j]的含义。 - -3. dp数组如何初始化 +### 3. dp数组如何初始化 既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。 @@ -95,7 +94,7 @@ vector dp(15001, 0); ``` -4. 确定遍历顺序 +### 4. 确定遍历顺序 在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历! @@ -111,11 +110,11 @@ for (int i = 0; i < stones.size(); i++) { // 遍历物品 ``` -5. 举例推导dp数组 +### 5. 举例推导dp数组 举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下: -![1049.最后一块石头的重量II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210121115805904.jpg) +![1049.最后一块石头的重量II](https://file1.kamacoder.com/i/algo/20210121115805904.jpg) 最后dp[target]里是容量为target的背包所能背的最大重量。 @@ -154,10 +153,7 @@ public: 本题其实和[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)几乎是一样的,只是最后对dp[target]的处理方式不同。 -[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)相当于是求背包是否正好装满,而本题是求背包最多能装多少。 - - - +**[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)相当于是求背包是否正好装满,而本题是求背包最多能装多少**。 ## 其他语言版本 @@ -313,6 +309,8 @@ class Solution: ``` ### Go: + +一维dp ```go func lastStoneWeightII(stones []int) int { // 15001 = 30 * 1000 /2 +1 @@ -341,6 +339,43 @@ func max(a, b int) int { } ``` +二维dp +```go +func lastStoneWeightII(stones []int) int { + sum := 0 + for _, val := range stones { + sum += val + } + target := sum / 2 + + dp := make([][]int, len(stones)) + for i := range dp { + dp[i] = make([]int, target + 1) + } + for j := stones[0]; j <= target; j++ { + dp[0][j] = stones[0] + } + + for i := 1; i < len(stones); i++ { + for j := 0; j <= target; j++ { + if stones[i] > j { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-stones[i]] + stones[i]) + } + } + } + return (sum - dp[len(stones)-1][target]) - dp[len(stones)-1][target] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + ### JavaScript: ```javascript @@ -497,8 +532,4 @@ public class Solution } ``` -

- - - diff --git "a/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" "b/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" old mode 100644 new mode 100755 index f33391c3e6..424f403938 --- "a/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" +++ "b/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1143.最长公共子序列 @@ -80,7 +78,7 @@ if (text1[i - 1] == text2[j - 1]) { 先看看dp[i][0]应该是多少呢? -test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0; +text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0; 同理dp[0][j]也是0。 @@ -96,7 +94,7 @@ vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); 从递推公式,可以看出,有三个方向可以推出dp[i][j],如图: -![1143.最长公共子序列](https://code-thinking-1253855093.file.myqcloud.com/pics/20210204115139616.jpg) +![1143.最长公共子序列](https://file1.kamacoder.com/i/algo/20210204115139616.jpg) 那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。 @@ -105,7 +103,7 @@ vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); 以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图: -![1143.最长公共子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210210150215918.jpg) +![1143.最长公共子序列1](https://file1.kamacoder.com/i/algo/20210210150215918.jpg) 最后红框dp[text1.size()][text2.size()]为最终结果 @@ -376,10 +374,47 @@ impl Solution { } ``` +### C: +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int longestCommonSubsequence(char* text1, char* text2) { + int text1Len = strlen(text1); + int text2Len = strlen(text2); + int dp[text1Len + 1][text2Len + 1]; + memset(dp, 0, sizeof (dp)); + for (int i = 1; i <= text1Len; ++i) { + for (int j = 1; j <= text2Len; ++j) { + if(text1[i - 1] == text2[j - 1]){ + dp[i][j] = dp[i - 1][j - 1] + 1; + } else{ + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[text1Len][text2Len]; +} +``` + +### Cangjie + +```cangjie +func longestCommonSubsequence(text1: String, text2: String): Int64 { + let n = text1.size + let m = text2.size + let dp = Array(n + 1, {_ => Array(m + 1, repeat: 0)}) + for (i in 1..=n) { + for (j in 1..=m) { + if (text1[i - 1] == text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + return dp[n][m] +} +``` -

- - - diff --git "a/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" "b/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" old mode 100644 new mode 100755 index 2ccd30c376..72462f1119 --- "a/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" +++ "b/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1207.独一无二的出现次数 @@ -40,14 +38,14 @@ 回归本题,**本题强调了-1000 <= arr[i] <= 1000**,那么就可以用数组来做哈希,arr[i]作为哈希表(数组)的下标,那么arr[i]可以是负数,怎么办?负数不能做数组下标。 -**此时可以定义一个2000大小的数组,例如int count[2002];**,统计的时候,将arr[i]统一加1000,这样就可以统计arr[i]的出现频率了。 +**此时可以定义一个2001大小的数组,例如int count[2001];**,统计的时候,将arr[i]统一加1000,这样就可以统计arr[i]的出现频率了。 -题目中要求的是是否有相同的频率出现,那么需要再定义一个哈希表(数组)用来记录频率是否重复出现过,bool fre[1002]; 定义布尔类型的就可以了,**因为题目中强调1 <= arr.length <= 1000,所以哈希表大小为1000就可以了**。 +题目中要求的是是否有相同的频率出现,那么需要再定义一个哈希表(数组)用来记录频率是否重复出现过,bool fre[1001]; 定义布尔类型的就可以了,**因为题目中强调1 <= arr.length <= 1000,所以哈希表大小为1000就可以了**。 如图所示: - + C++代码如下: @@ -55,11 +53,11 @@ C++代码如下: class Solution { public: bool uniqueOccurrences(vector& arr) { - int count[2002] = {0}; // 统计数字出现的频率 + int count[2001] = {0}; // 统计数字出现的频率 for (int i = 0; i < arr.size(); i++) { count[arr[i] + 1000]++; } - bool fre[1002] = {false}; // 看相同频率是否重复出现 + bool fre[1001] = {false}; // 看相同频率是否重复出现 for (int i = 0; i <= 2000; i++) { if (count[i]) { if (fre[count[i]] == false) fre[count[i]] = true; @@ -78,7 +76,7 @@ public: ```java class Solution { public boolean uniqueOccurrences(int[] arr) { - int[] count = new int[2002]; + int[] count = new int[2001]; for (int i = 0; i < arr.length; i++) { count[arr[i] + 1000]++; // 防止负数作为下标 } @@ -103,10 +101,10 @@ class Solution { # 方法 1: 数组在哈西法的应用 class Solution: def uniqueOccurrences(self, arr: List[int]) -> bool: - count = [0] * 2002 + count = [0] * 2001 for i in range(len(arr)): count[arr[i] + 1000] += 1 # 防止负数作为下标 - freq = [False] * 1002 # 标记相同频率是否重复出现 + freq = [False] * 1001 # 标记相同频率是否重复出现 for i in range(2001): if count[i] > 0: if freq[count[i]] == False: @@ -139,12 +137,12 @@ class Solution: ``` javascript // 方法一:使用数组记录元素出现次数 var uniqueOccurrences = function(arr) { - const count = new Array(2002).fill(0);// -1000 <= arr[i] <= 1000 + const count = new Array(2001).fill(0);// -1000 <= arr[i] <= 1000 for(let i = 0; i < arr.length; i++){ count[arr[i] + 1000]++;// 防止负数作为下标 } // 标记相同频率是否重复出现 - const fre = new Array(1002).fill(false);// 1 <= arr.length <= 1000 + const fre = new Array(1001).fill(false);// 1 <= arr.length <= 1000 for(let i = 0; i <= 2000; i++){ if(count[i] > 0){//有i出现过 if(fre[count[i]] === false) fre[count[i]] = true;//之前未出现过,标记为出现 @@ -221,11 +219,29 @@ func uniqueOccurrences(arr []int) bool { } ``` +### Rust + +```rust +use std::collections::{HashMap, HashSet}; +impl Solution { + pub fn unique_occurrences(arr: Vec) -> bool { + let mut hash = HashMap::::new(); + for x in arr { + *hash.entry(x).or_insert(0) += 1; + } + let mut set = HashSet::::new(); + for (_k, v) in hash { + if set.contains(&v) { + return false + } else { + set.insert(v); + } + } + true + } +} +``` -

- - - diff --git "a/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" "b/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index 2a7b092290..a9e275d92d --- "a/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1221. 分割平衡字符串 @@ -172,7 +170,3 @@ function balancedStringSplit(s: string): number { }; ``` -

- - - diff --git "a/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" "b/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" old mode 100644 new mode 100755 index dc8fda41f1..b440c32648 --- "a/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" +++ "b/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1254. 统计封闭岛屿的数目 @@ -12,13 +10,13 @@ 请返回 封闭岛屿 的数目。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830111533.png) +![](https://file1.kamacoder.com/i/algo/20220830111533.png) * 输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]] * 输出:2 * 解释:灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220830111601.png) +![](https://file1.kamacoder.com/i/algo/20220830111601.png) * 输入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]] * 输出:1 @@ -134,9 +132,5 @@ var closedIsland = function(grid) { ``` -

- - - diff --git "a/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" "b/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" new file mode 100644 index 0000000000..bea47a2e63 --- /dev/null +++ "b/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" @@ -0,0 +1,49 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +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.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" "b/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" old mode 100644 new mode 100755 index c2455cf043..11f947a184 --- "a/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" +++ "b/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -83,7 +81,7 @@ int bitCount(int n) { ``` 以计算12的二进制1的数量为例,如图所示: - + 下面我就使用方法二,来做这道题目: @@ -216,8 +214,4 @@ var sortByBits = function(arr) { ``` -

- - - diff --git "a/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" "b/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" old mode 100644 new mode 100755 index c706ba216e..61b548abf8 --- "a/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" +++ "b/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -87,7 +85,7 @@ for (int i = 0; i < nums.size(); i++) { 流程如图: - + 关键地方讲完了,整体C++代码如下: @@ -115,7 +113,7 @@ public: ## 其他语言版本 -### Java: +### Java: ```Java public int[] smallerNumbersThanCurrent(int[] nums) { @@ -138,18 +136,51 @@ public int[] smallerNumbersThanCurrent(int[] nums) { ### Python: -```python +> 暴力法: + +```python3 +class Solution: + def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: + res = [0 for _ in range(len(nums))] + for i in range(len(nums)): + cnt = 0 + for j in range(len(nums)): + if j == i: + continue + if nums[i] > nums[j]: + cnt += 1 + res[i] = cnt + return res +``` + +> 排序+hash: + +```python3 class Solution: + # 方法一:使用字典 def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: res = nums[:] - hash = dict() + hash_dict = dict() res.sort() # 从小到大排序之后,元素下标就是小于当前数字的数字 for i, num in enumerate(res): - if num not in hash.keys(): # 遇到了相同的数字,那么不需要更新该 number 的情况 - hash[num] = i + if num not in hash_dict.keys(): # 遇到了相同的数字,那么不需要更新该 number 的情况 + hash_dict[num] = i for i, num in enumerate(nums): - res[i] = hash[num] + res[i] = hash_dict[num] return res + + # 方法二:使用数组 + def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: + # 同步进行排序和创建新数组的操作,这样可以减少一次冗余的数组复制操作,以减少一次O(n) 的复制时间开销 + sort_nums = sorted(nums) + # 题意中 0 <= nums[i] <= 100,故range的参数设为101 + hash_lst = [0 for _ in range(101)] + # 从后向前遍历,这样hash里存放的就是相同元素最左面的数值和下标了 + for i in range(len(sort_nums)-1,-1,-1): + hash_lst[sort_nums[i]] = i + for i in range(len(nums)): + nums[i] = hash_lst[nums[i]] + return nums ``` ### Go: @@ -220,7 +251,7 @@ var smallerNumbersThanCurrent = function(nums) { }; ``` -### TypeScript: +### TypeScript: > 暴力法: @@ -241,7 +272,7 @@ function smallerNumbersThanCurrent(nums: number[]): number[] { }; ``` -> 排序+hash +> 排序+hash: ```typescript function smallerNumbersThanCurrent(nums: number[]): number[] { @@ -260,9 +291,21 @@ function smallerNumbersThanCurrent(nums: number[]): number[] { }; ``` +### Rust: +```rust +use std::collections::HashMap; +impl Solution { + pub fn smaller_numbers_than_current(nums: Vec) -> Vec { + let mut v = nums.clone(); + v.sort(); + let mut hash = HashMap::new(); + for i in 0..v.len() { + // rust中使用or_insert插入值, 如果存在就不插入,可以使用正序遍历 + hash.entry(v[i]).or_insert(i as i32); + } + nums.into_iter().map(|x| *hash.get(&x).unwrap()).collect() + } +} +``` -

- - - diff --git "a/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" "b/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" old mode 100644 new mode 100755 index 57e56b8fe3..551766ff0d --- "a/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" +++ "b/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1382.将二叉搜索树变平衡 @@ -17,7 +15,7 @@ 示例: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210726154512.png) +![](https://file1.kamacoder.com/i/algo/20210726154512.png) * 输入:root = [1,null,2,null,3,null,4,null,null] * 输出:[2,1,3,null,null,null,4] @@ -217,8 +215,4 @@ function buildTree(arr: number[], left: number, right: number): TreeNode | null -

- - - diff --git "a/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" "b/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" old mode 100644 new mode 100755 index 9bcc7ef9ca..9991249f5a --- "a/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" +++ "b/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1791.找出星型图的中心节点 @@ -12,7 +10,7 @@ 什么是度,可以理解为,链接节点的边的数量。 题目中度如图所示: -![1791.找出星型图的中心节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20220704113207.png) +![1791.找出星型图的中心节点](https://file1.kamacoder.com/i/algo/20220704113207.png) 至于出度和入度,那就是在有向图里的概念了,本题是无向图。 @@ -77,7 +75,3 @@ public: ``` -

- - - diff --git "a/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" "b/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" old mode 100644 new mode 100755 index 132b0181c0..9048b0f6af --- "a/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" +++ "b/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 1971. 寻找图中是否存在路径 @@ -14,18 +12,9 @@ 给你数组 edges 和整数 n、start 和 end,如果从 start 到 end 存在 有效路径 ,则返回 true,否则返回 false 。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20220705101442.png) +![](https://file1.kamacoder.com/i/algo/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 -- 不存在双向边 -- 不存在指向顶点自身的边 ## 思路 @@ -47,11 +36,22 @@ void init() { father[i] = i; } } -// 并查集里寻根的过程 +// 并查集里寻根的过程,这里递归调用当题目数据过多,递归调用可能会发生栈溢出 + int find(int u) { return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 } +// 使用迭代的方法可以避免栈溢出问题 +int find(int x) { + while (x != parent[x]) { + // 路径压缩,直接将x链接到其祖先节点,减少树的高度 + parent[x] = parent[parent[x]]; + x = parent[x]; + } +return x; +} + // 判断 u 和 v是否找到同一个根 bool isSame(int u, int v) { u = find(u); @@ -84,6 +84,8 @@ void join(int u, int v) { 此时我们就可以直接套用并查集模板。 +本题在join函数调用find函数时如果是递归调用会发生栈溢出提示,建议使用迭代方法 + 使用 join(int u, int v)将每条边加入到并查集。 最后 isSame(int u, int v) 判断是否是同一个根 就可以了。 @@ -102,8 +104,13 @@ private: } } // 并查集里寻根的过程 - int find(int u) { - return u == father[u] ? u : father[u] = find(father[u]); + int find(int x) { + while (x != parent[x]) { + // 路径压缩,直接将x链接到其祖先节点,减少树的高度 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; } // 判断 u 和 v是否找到同一个根 @@ -204,7 +211,7 @@ class Solution: return find(source) == find(destination) ``` -### Javascript: +### JavaScript: Javascript 并查集解法如下: @@ -284,7 +291,45 @@ 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)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" "b/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" index 8be48f38c2..d74f1a01aa 100644 --- "a/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" +++ "b/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 程序提交之后为什么会超时?O(n)的算法会超时,n究竟是多大? @@ -15,7 +13,7 @@ ## 超时是怎么回事 -![程序超时](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729112716117.png) +![程序超时](https://file1.kamacoder.com/i/algo/20200729112716117.png) 大家在leetcode上练习算法的时候应该都遇到过一种错误是“超时”。 @@ -131,11 +129,11 @@ int main() { 来看一下运行的效果,如下图: -![程序超时2](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729200018460.png) +![程序超时2](https://file1.kamacoder.com/i/algo/20200729200018460.png) O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 -![程序超时3](https://code-thinking-1253855093.file.myqcloud.com/pics/2020072919590970.png) +![程序超时3](https://file1.kamacoder.com/i/algo/2020072919590970.png) O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 @@ -143,7 +141,7 @@ O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚 理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。 -![程序超时4](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729195729407.png) +![程序超时4](https://file1.kamacoder.com/i/algo/20200729195729407.png) O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 @@ -151,7 +149,7 @@ O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符 **整体测试数据整理如下:** -![程序超时1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201208231559175.png) +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175.png) 至于O(log n)和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 @@ -223,8 +221,4 @@ int main() { -

- - - diff --git "a/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" "b/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" new file mode 100644 index 0000000000..7ad2ced690 Binary files /dev/null and "b/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" differ diff --git a/problems/images/test b/problems/images/test new file mode 100644 index 0000000000..e69de29bb2 diff --git "a/problems/kama53.\345\257\273\345\256\235.md" "b/problems/kama53.\345\257\273\345\256\235.md" deleted file mode 100644 index 3a4b8b274c..0000000000 --- "a/problems/kama53.\345\257\273\345\256\235.md" +++ /dev/null @@ -1,503 +0,0 @@ - -# 寻宝 - -[卡码网: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算法。 - -最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起。 - -图中有n个节点,那么一定可以用 n - 1 条边将所有节点连接到一起。 - -那么如何选择 这 n-1 条边 就是 最小生成树算法的任务所在。 - -例如本题示例中的无向有权图为: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231206164306.png) - -那么在这个图中,如何选取 n-1 条边 使得 图中所有节点连接到一起,并且边的权值和最小呢? - -(图中为n为7,即7个节点,那么只需要 n-1 即 6条边就可以讲所有顶点连接到一起) - -prim算法 是从节点的角度 采用贪心的策略 每次寻找距离 最小生成树最近的节点 并加入到最小生成树中。 - -prim算法核心就是三步,我称为**prim三部曲**,大家一定要熟悉这三步,代码相对会好些很多: - -1. 第一步,选距离生成树最近节点 -2. 第二步,最近节点加入生成树 -3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) - -现在录友们会对这三步很陌生,不知道这是干啥的,没关系,下面将会画图举例来带大家把这**prim三部曲**理解到位。 - -在prim算法中,有一个数组特别重要,这里我起名为:minDist。 - -刚刚我有讲过 “每次寻找距离 最小生成树最近的节点 并加入到最小生成树中”,那么如何寻找距离最小生成树最近的节点呢? - -这就用到了 minDist 数组, 它用来作什么呢? - -**minDist数组 用来记录 每一个节点距离最小生成树的最近距离**。 理解这一点非常重要,这也是 prim算法最核心要点所在,很多录友看不懂prim算法的代码,都是因为没有理解透 这个数组的含义。 - -接下来,我们来通过一步一步画图,来带大家巩固 **prim三部曲** 以及 minDist数组 的作用。 - -(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) - - -### 1 初始状态 - -minDist 数组 里的数值初始化为 最大数,因为本题 节点距离不会超过 10000,所以 初始化最大数为 10001就可以。 - -相信这里录友就要问了,为什么这么做? - -现在 还没有最小生成树,默认每个节点距离最小生成树是最大的,这样后面我们在比较的时候,发现更近的距离,才能更新到 minDist 数组上。 - -如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231215105603.png) - -开始构造最小生成树 - -### 2 - -1、prim三部曲,第一步:选距离生成树最近节点 - -选择距离最小生成树最近的节点,加入到最小生成树,刚开始还没有最小生成树,所以随便选一个节点加入就好(因为每一个节点一定会在最小生成树里,所以随便选一个就好),那我们选择节点1 (符合遍历数组的习惯,第一个遍历的也是节点1) - -2、prim三部曲,第二步:最近节点加入生成树 - -此时 节点1 已经算最小生成树的节点。 - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -接下来,我们要更新所有节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102048.png) - - -注意下标0,我们就不管它了,下标 1 与节点 1 对应,这样可以避免大家把节点搞混。 - -此时所有非生成树的节点距离 最小生成树(节点1)的距离都已经跟新了 。 - -* 节点2 与 节点1 的距离为1,比原先的 距离值10001小,所以更新minDist[2]。 -* 节点3 和 节点1 的距离为1,比原先的 距离值10001小,所以更新minDist[3]。 -* 节点5 和 节点1 的距离为2,比原先的 距离值10001小,所以更新minDist[5]。 - -**注意图中我标记了 minDist数组里更新的权值**,是哪两个节点之间的权值,例如 minDist[2] =1 ,这个 1 是 节点1 与 节点2 之间的连线,清楚这一点对最后我们记录 最小生成树的权值总和很重要。 - -(我在后面依然会不断重复 prim三部曲,可能基础好的录友会感觉有点啰嗦,但也是让大家感觉这三部曲求解的过程) - -### 3 - -1、prim三部曲,第一步:选距离生成树最近节点 - -选取一个距离 最小生成树(节点1) 最近的非生成树里的节点,节点2,3,5 距离 最小生成树(节点1) 最近,选节点 2(其实选 节点3或者节点5都可以,距离一样的)加入最小生成树。 - -2、prim三部曲,第二步:最近节点加入生成树 - -此时 节点1 和 节点2,已经算最小生成树的节点。 - - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -接下来,我们要更新节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102431.png) - -此时所有非生成树的节点距离 最小生成树(节点1、节点2)的距离都已经跟新了 。 - -* 节点3 和 节点2 的距离为2,和原先的距离值1 小,所以不用更新。 -* 节点4 和 节点2 的距离为2,比原先的距离值10001小,所以更新minDist[4]。 -* 节点5 和 节点2 的距离为10001(不连接),所以不用更新。 -* 节点6 和 节点2 的距离为1,比原先的距离值10001小,所以更新minDist[6]。 - -### 4 - -1、prim三部曲,第一步:选距离生成树最近节点 - -选择一个距离 最小生成树(节点1、节点2) 最近的非生成树里的节点,节点3,6 距离 最小生成树(节点1、节点2) 最近,选节点3 (选节点6也可以,距离一样)加入最小生成树。 - -2、prim三部曲,第二步:最近节点加入生成树 - -此时 节点1 、节点2 、节点3 算是最小生成树的节点。 - - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -接下来更新节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102457.png) - -所有非生成树的节点距离 最小生成树(节点1、节点2、节点3 )的距离都已经跟新了 。 - -* 节点 4 和 节点 3的距离为 1,和原先的距离值 2 小,所以更新minDist[3]为1。 - -上面为什么我们只比较 节点4 和 节点3 的距离呢? - -因为节点3加入 最小生成树后,非 生成树节点 只有 节点 4 和 节点3是链接的,所以需要重新更新一下 节点4距离最小生成树的距离,其他节点距离最小生成树的距离 都不变。 - -### 5 - -1、prim三部曲,第一步:选距离生成树最近节点 - -继续选择一个距离 最小生成树(节点1、节点2、节点3) 最近的非生成树里的节点,为了巩固大家对 minDist数组的理解,这里我再啰嗦一遍: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231217213516.png) - -**minDist数组 是记录了 所有非生成树节点距离生成树的最小距离**,所以 从数组里我们能看出来,非生成树节点 4 和 节点 6 距离 生成树最近。 - - -任选一个加入生成树,我们选 节点4(选节点6也行) 。 - -**注意**,我们根据 minDist数组,选取距离 生成树 最近的节点 加入生成树,那么 **minDist数组里记录的其实也是 最小生成树的边的权值**(我在图中把权值对应的是哪两个节点也标记出来了)。 - -如果大家不理解,可以跟着我们下面的讲解,看 minDist数组的变化, minDist数组 里记录的权值对应的哪条边。 - -理解这一点很重要,因为 最后我们要求 最小生成树里所有边的权值和。 - -2、prim三部曲,第二步:最近节点加入生成树 - -此时 节点1、节点2、节点3、节点4 算是 最小生成树的节点。 - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -接下来更新节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102618.png) - -minDist数组已经更新了 所有非生成树的节点距离 最小生成树(节点1、节点2、节点3、节点4 )的距离 。 - -* 节点 5 和 节点 4的距离为 1,和原先的距离值 2 小,所以更新minDist[4]为1。 - -### 6 - -1、prim三部曲,第一步:选距离生成树最近节点 - -继续选距离 最小生成树(节点1、节点2、节点3、节点4 )最近的非生成树里的节点,只有 节点 5 和 节点6。 - - -选节点5 (选节点6也可以)加入 生成树。 - -2、prim三部曲,第二步:最近节点加入生成树 - -节点1、节点2、节点3、节点4、节点5 算是 最小生成树的节点。 - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -接下来更新节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102646.png) - -minDist数组已经更新了 所有非生成树的节点距离 最小生成树(节点1、节点2、节点3、节点4 、节点5)的距离 。 - -* 节点 6 和 节点 5 距离为 2,比原先的距离值 1 大,所以不更新 -* 节点 7 和 节点 5 距离为 1,比原先的距离值 10001小,更新 minDist[7] - -### 7 - -1、prim三部曲,第一步:选距离生成树最近节点 - -继续选距离 最小生成树(节点1、节点2、节点3、节点4 、节点5)最近的非生成树里的节点,只有 节点 6 和 节点7。 - -2、prim三部曲,第二步:最近节点加入生成树 - -选节点6 (选节点7也行,距离一样的)加入生成树。 - -3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) - -节点1、节点2、节点3、节点4、节点5、节点6 算是 最小生成树的节点 ,接下来更新节点距离最小生成树的距离,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102732.png) - -这里就不在重复描述了,大家类推,最后,节点7加入生成树,如图: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231222102820.png) - -### 最后 - -最后我们就生成了一个 最小生成树, 绿色的边将所有节点链接到一起,并且 保证权值是最小的,因为我们在更新 minDist 数组的时候,都是选距离 最小生成树最近的点 加入到树中。 - -讲解上面的模拟过程的时候,我已经强调多次 minDist数组 是记录了 所有非生成树节点距离生成树的最小距离。 - -最后,minDist数组 也就是记录的是最小生成树所有边的权值。 - -我在图中,特别把 每条边的权值对应的是哪两个节点 标记出来(例如minDist[7] = 1,对应的是节点5 和 节点7之间的边,而不是 节点6 和 节点7),为了就是让大家清楚, minDist里的每一个值 对应的是哪条边。 - -那么我们要求最小生成树里边的权值总和 就是 把 最后的 minDist 数组 累加一起。 - -以下代码,我对 prim三部曲,做了重点注释,大家根据这三步,就可以 透彻理解prim。 - -```CPP -#include -#include -using namespace std; -int main() { - int v, e; - int x, y, k; - cin >> v >> e; - // 填一个默认最大值,题目描述val最大为10000 - vector> grid(v + 1, vector(v + 1, 10001)); - while (e--) { - cin >> x >> y >> k; - // 因为是双向图,所以两个方向都要填上 - grid[x][y] = k; - grid[y][x] = k; - - } - // 所有节点到最小生成树的最小距离 - vector minDist(v + 1, 10001); - - // 这个节点是否在树里 - vector isInTree(v + 1, false); - - // 我们只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起 - for (int i = 1; i < v; i++) { - - // 1、prim三部曲,第一步:选距离生成树最近节点 - int cur = -1; // 选中哪个节点 加入最小生成树 - 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])) { - cur = j; - } - } - // 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的新加入,需要更新一下数据了 - if (!isInTree[j] && grid[cur][j] < minDist[j]) { - minDist[j] = grid[cur][j]; - } - } - } - // 统计结果 - int result = 0; - for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边 - result += minDist[i]; - } - cout << result << endl; - -} -``` - -## 拓展 - -上面讲解的是记录了最小生成树 所有边的权值,如果让打印出来 最小生成树的每条边呢? 或者说 要把这个最小生成树画出来呢? - - -此时我们就需要把 最小生成树里每一条边记录下来。 - -此时有两个问题: - -* 1、用什么结构来记录 -* 2、如何记录 - -如果记录边,其实就是记录两个节点就可以,两个节点连成一条边。 - -如何记录两个节点呢? - -我们使用一维数组就可以记录。 parent[节点编号] = 节点编号, 这样就把一条边记录下来了。(当然如果节点编号非常大,可以考虑使用map) - -使用一维数组记录是有向边,不过我们这里不需要记录方向,所以只关注两条边是连接的就行。 - -parent数组初始化代码: - -```CPP -vector parent(v + 1, -1); -``` - -接下来就是第二个问题,如何记录? - -我们再来回顾一下 prim三部曲, - -1. 第一步,选距离生成树最近节点 -2. 第二步,最近节点加入生成树 -3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) - -大家先思考一下,我们是在第几步,可以记录 最小生成树的边呢? - -在本面上半篇 我们讲解过:“我们根据 minDist数组,选组距离 生成树 最近的节点 加入生成树,那么 **minDist数组里记录的其实也是 最小生成树的边的权值**。” - -既然 minDist数组 记录了 最小生成树的边,是不是就是在更新 minDist数组 的时候,去更新parent数组来记录一下对应的边呢。 - - -所以 在 prim三部曲中的第三步,更新 parent数组,代码如下: - -```CPP -for (int j = 1; j <= v; j++) { - if (!isInTree[j] && grid[cur][j] < minDist[j]) { - minDist[j] = grid[cur][j]; - parent[j] = cur; // 记录最小生成树的边 (注意数组指向的顺序很重要) - } -} -``` - -代码中注释中,我强调了 数组指向的顺序很重要。 因为不少录友在这里会写成这样: `parent[cur] = j` 。 - -这里估计大家会疑惑了,parent[节点编号A] = 节点编号B, 就表示A 和 B 相连,我们这里就不用在意方向,代码中 为什么 只能 `parent[j] = cur` 而不能 `parent[cur] = j` 这么写呢? - -如果写成 `parent[cur] = j`,在 for 循环中,有多个 j 满足要求, 那么 parent[cur] 就会被反复覆盖,因为 cur 是一个固定值。 - -举个例子,cur = 1, 在 for循环中,可能 就 j = 2, j = 3,j =4 都符合条件,那么本来应该记录 节点1 与 节点 2、节点3、节点4相连的。 - -如果 `parent[cur] = j` 这么写,最后更新的逻辑是 parent[1] = 2, parent[1] = 3, parent[1] = 4, 最后只能记录 节点1 与节点 4 相连,其他相连情况都被覆盖了。 - -如果这么写 `parent[j] = cur`, 那就是 parent[2] = 1, parent[3] = 1, parent[4] = 1 ,这样 才能完整表示出 节点1 与 其他节点都是链接的,才没有被覆盖。 - -主要问题也是我们使用了一维数组来记录。 - -如果是二维数组,来记录两个点链接,例如 parent[节点编号A][节点编号B] = 1 ,parent[节点编号B][节点编号A] = 1,来表示 节点A 与 节点B 相连,那就没有上面说的这个注意事项了,当然这么做的话,就是多开辟的内存空间。 - -以下是输出最小生成树边的代码,不算最后输出, 就额外添加了两行代码,我都注释标记了: - -```CPP -#include -#include -using namespace std; -int main() { - int v, e; - int x, y, k; - cin >> v >> e; - vector> grid(v + 1, vector(v + 1, 10001)); - while (e--) { - cin >> x >> y >> k; - grid[x][y] = k; - grid[y][x] = k; - } - - vector minDist(v + 1, 10001); - vector isInTree(v + 1, false); - - //加上初始化 - vector parent(v + 1, -1); - - 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])) { - cur = j; - } - } - isInTree[cur] = true; - for (int j = 1; j <= v; j++) { - if (!isInTree[j] && grid[cur][j] < minDist[j]) { - minDist[j] = grid[cur][j]; - - parent[j] = cur; // 记录边 - } - } - } - // 输出 最小生成树边的链接情况 - for (int i = 1; i <= v; i++) { - cout << i "->" parent[i] << endl; - } -} -``` - -按照本题示例,代码输入如下: - -``` -1->-1 -2->1 -3->1 -4->3 -5->4 -6->2 -7->5 -``` - -注意,这里是无向图,我在输出上添加了箭头仅仅是为了方便大家看出是边的意思。 - -大家可以和我们本题最后生成的最小生成树的图 去对比一下 边的链接情况: - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231229115714.png) - -绿色的边 是最小生成树,和我们的 输出完全一致。 - -## 总结 - -此时我就把prim算法讲解完毕了,我们再来回顾一下。 - -关于 prim算法,我自创了三部曲,来帮助大家理解: - -1. 第一步,选距离生成树最近节点 -2. 第二步,最近节点加入生成树 -3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) - -大家只要理解这三部曲, prim算法 至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于 自己在写prim的时候 两眼一抹黑 完全凭感觉去写。 -这也为什么很多录友感觉 prim算法比较难,而且每次学会来,隔一段时间 又不会写了,主要是 没有一个纲领。 - -理解这三部曲之后,更重要的 就是理解 minDist数组。 - -**minDist数组 是prim算法的灵魂,它帮助 prim算法完成最重要的一步,就是如何找到 距离最小生成树最近的点**。 - -再来帮大家回顾 minDist数组 的含义:记录 每一个节点距离最小生成树的最近距离。 - -理解 minDist数组 ,至少大家看prim算法的代码不会懵。 - -也正是 因为 minDist数组 的作用,我们根据 minDist数组,选取距离 生成树 最近的节点 加入生成树,那么 **minDist数组里记录的其实也是 最小生成树的边的权值**。 - -所以我们求 最小生成树的权值和 就是 计算后的 minDist数组 数值总和。 - -最后我们拓展了如何求职 最小生成树 的每一条边,其实 添加的代码很简单,主要是理解 为什么使用 parent数组 来记录边 以及 在哪里 更新parent数组。 - -同时,因为使用一维数组,数组的下标和数组 如何赋值很重要,不要搞反,导师结果被覆盖。 - -好了,以上为总结,录友们学习愉快。 - - - - diff --git a/problems/kamacoder/.DS_Store b/problems/kamacoder/.DS_Store new file mode 100644 index 0000000000..5008ddfcf5 Binary files /dev/null and b/problems/kamacoder/.DS_Store differ diff --git "a/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" "b/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" new file mode 100644 index 0000000000..cb5fbb7468 --- /dev/null +++ "b/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" @@ -0,0 +1,616 @@ + +# 44. 开发商购买土地 + +> 本题为代码随想录后续扩充题目,还没有视频讲解,顺便让大家练习一下ACM输入输出模式(笔试面试必备) + +[题目链接](https://kamacoder.com/problempage.php?pid=1044) + +【题目描述】 + +在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。 + +现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。 + +然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。 + +为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。 + +注意:区块不可再分。 + +【输入描述】 + +第一行输入两个正整数,代表 n 和 m。 + +接下来的 n 行,每行输出 m 个正整数。 + +输出描述 + +请输出一个整数,代表两个子区域内土地总价值之间的最小差距。 + +【输入示例】 + +3 3 +1 2 3 +2 1 3 +1 2 3 + +【输出示例】 + +0 + +【提示信息】 + +如果将区域按照如下方式划分: + +1 2 | 3 +2 1 | 3 +1 2 | 3 + +两个子区域内土地总价值之间的最小差距可以达到 0。 + +【数据范围】: + +* 1 <= n, m <= 100; +* n 和 m 不同时为 1。 + +## 思路 + +看到本题,大家如果想暴力求解,应该是 n^3 的时间复杂度, + +一个 for 枚举分割线, 嵌套 两个for 去累加区间里的和。 + +如果本题要求 任何两个行(或者列)之间的数值总和,大家在[0058.区间和](./0058.区间和.md) 的基础上 应该知道怎么求。 + +就是前缀和的思路,先统计好,前n行的和 q[n],如果要求矩阵 a行 到 b行 之间的总和,那么就 q[b] - q[a - 1]就好。 + +至于为什么是 a - 1,大家去看 [0058.区间和](./0058.区间和.md) 的分析,使用 前缀和 要注意 区间左右边的开闭情况。 + +本题也可以使用 前缀和的思路来求解,先将 行方向,和 列方向的和求出来,这样可以方便知道 划分的两个区间的和。 + +代码如下: + + +```CPP +#include +#include +#include + +using namespace std; +int main () { + int n, m; + cin >> n >> m; + int sum = 0; + vector> vec(n, vector(m, 0)) ; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> vec[i][j]; + sum += vec[i][j]; + } + } + // 统计横向 + vector horizontal(n, 0); + for (int i = 0; i < n; i++) { + for (int j = 0 ; j < m; j++) { + horizontal[i] += vec[i][j]; + } + } + // 统计纵向 + vector vertical(m , 0); + for (int j = 0; j < m; j++) { + for (int i = 0 ; i < n; i++) { + vertical[j] += vec[i][j]; + } + } + int result = INT_MAX; + int horizontalCut = 0; + for (int i = 0 ; i < n; i++) { + horizontalCut += horizontal[i]; + result = min(result, abs(sum - horizontalCut - horizontalCut)); + } + int verticalCut = 0; + for (int j = 0; j < m; j++) { + verticalCut += vertical[j]; + result = min(result, abs(sum - verticalCut - verticalCut)); + } + cout << result << endl; +} + +``` + +时间复杂度: O(n^2) + +其实本题可以在暴力求解的基础上,优化一下,就不用前缀和了,在行向遍历的时候,遇到行末尾就统一一下, 在列向遍历的时候,遇到列末尾就统计一下。 + +时间复杂度也是 O(n^2) + +代码如下: + +```CPP +#include +#include +#include + +using namespace std; +int main () { + int n, m; + cin >> n >> m; + int sum = 0; + vector> vec(n, vector(m, 0)) ; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> vec[i][j]; + sum += vec[i][j]; + } + } + + int result = INT_MAX; + int count = 0; // 统计遍历过的行 + for (int i = 0; i < n; i++) { + for (int j = 0 ; j < m; j++) { + count += vec[i][j]; + // 遍历到行末尾时候开始统计 + if (j == m - 1) result = min (result, abs(sum - count - count)); + + } + } + + count = 0; // 统计遍历过的列 + for (int j = 0; j < m; j++) { + for (int i = 0 ; i < n; i++) { + count += vec[i][j]; + // 遍历到列末尾的时候开始统计 + if (i == n - 1) result = min (result, abs(sum - count - count)); + } + } + cout << result << endl; +} + +``` + + + +## 其他语言版本 + +### Java + +前缀和 + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int sum = 0; + int[][] vec = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + vec[i][j] = scanner.nextInt(); + sum += vec[i][j]; + } + } + + // 统计横向 + int[] horizontal = new int[n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + horizontal[i] += vec[i][j]; + } + } + + // 统计纵向 + int[] vertical = new int[m]; + for (int j = 0; j < m; j++) { + for (int i = 0; i < n; i++) { + vertical[j] += vec[i][j]; + } + } + + int result = Integer.MAX_VALUE; + int horizontalCut = 0; + for (int i = 0; i < n; i++) { + horizontalCut += horizontal[i]; + result = Math.min(result, Math.abs((sum - horizontalCut) - horizontalCut)); + // 更新result。其中,horizontalCut表示前i行的和,sum - horizontalCut表示剩下的和,作差、取绝对值,得到题目需要的“A和B各自的子区域内的土地总价值之差”。下同。 + } + + int verticalCut = 0; + for (int j = 0; j < m; j++) { + verticalCut += vertical[j]; + result = Math.min(result, Math.abs((sum - verticalCut) - verticalCut)); + } + + System.out.println(result); + scanner.close(); + } +} + +``` + +优化暴力 + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int sum = 0; + int[][] vec = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + vec[i][j] = scanner.nextInt(); + sum += vec[i][j]; + } + } + + int result = Integer.MAX_VALUE; + int count = 0; // 统计遍历过的行 + + // 行切分 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + count += vec[i][j]; + // 遍历到行末尾时候开始统计 + if (j == m - 1) { + result = Math.min(result, Math.abs(sum - 2 * count)); + } + } + } + + count = 0; + // 列切分 + for (int j = 0; j < m; j++) { + for (int i = 0; i < n; i++) { + count += vec[i][j]; + // 遍历到列末尾时候开始统计 + if (i == n - 1) { + result = Math.min(result, Math.abs(sum - 2 * count)); + } + } + } + + System.out.println(result); + scanner.close(); + } +} + +``` + +### python + +前缀和 +```python +def main(): + import sys + input = sys.stdin.read + data = input().split() + + idx = 0 + n = int(data[idx]) + idx += 1 + m = int(data[idx]) + idx += 1 + sum = 0 + vec = [] + for i in range(n): + row = [] + for j in range(m): + num = int(data[idx]) + idx += 1 + row.append(num) + sum += num + vec.append(row) + + # 统计横向 + horizontal = [0] * n + for i in range(n): + for j in range(m): + horizontal[i] += vec[i][j] + + # 统计纵向 + vertical = [0] * m + for j in range(m): + for i in range(n): + vertical[j] += vec[i][j] + + result = float('inf') + horizontalCut = 0 + for i in range(n): + horizontalCut += horizontal[i] + result = min(result, abs(sum - 2 * horizontalCut)) + + verticalCut = 0 + for j in range(m): + verticalCut += vertical[j] + result = min(result, abs(sum - 2 * verticalCut)) + + print(result) + +if __name__ == "__main__": + main() + + +``` + +优化暴力 + +```python +def main(): + import sys + input = sys.stdin.read + data = input().split() + + idx = 0 + n = int(data[idx]) + idx += 1 + m = int(data[idx]) + idx += 1 + sum = 0 + vec = [] + for i in range(n): + row = [] + for j in range(m): + num = int(data[idx]) + idx += 1 + row.append(num) + sum += num + vec.append(row) + + result = float('inf') + + count = 0 + # 行切分 + for i in range(n): + + for j in range(m): + count += vec[i][j] + # 遍历到行末尾时候开始统计 + if j == m - 1: + result = min(result, abs(sum - 2 * count)) + + count = 0 + # 列切分 + for j in range(m): + + for i in range(n): + count += vec[i][j] + # 遍历到列末尾时候开始统计 + if i == n - 1: + result = min(result, abs(sum - 2 * count)) + + print(result) + +if __name__ == "__main__": + main() + +``` + +### JavaScript + +前缀和 +```js +function func() { + const readline = require('readline') + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + let inputLines = [] + rl.on('line', function (line) { + inputLines.push(line.trim()) + }) + + rl.on('close', function () { + let [n, m] = inputLines[0].split(" ").map(Number) + let c = new Array(n).fill(0) + let r = new Array(m).fill(0) + let arr = new Array(n) + let sum = 0//数组总和 + let min = Infinity//设置最小值的初始值为无限大 + //定义数组 + for (let s = 0; s < n; s++) { + arr[s] = inputLines[s + 1].split(" ").map(Number) + } + //每一行的和 + for (let i = 0; i < n; i++) { + for (let j = 0; j < m; j++) { + c[i] += arr[i][j] + sum += arr[i][j] + } + } + //每一列的和 + for (let i = 0; i < n; i++) { + for (let j = 0; j < m; j++) { + r[j] += arr[i][j] + } + } + let sum1 = 0, sum2 = 0 + //横向切割 + for (let i = 0; i < n; i++) { + sum1 += c[i] + min = min < Math.abs(sum - 2 * sum1) ? min : Math.abs(sum - 2 * sum1) + } + //纵向切割 + for (let j = 0; j < m; j++) { + sum2 += r[j] + min = min < Math.abs(sum - 2 * sum2) ? min : Math.abs(sum - 2 * sum2) + } + console.log(min); + }) +} +``` + +### C + +前缀和 +```c +#include +#include + +int main() +{ + int n = 0, m = 0, ret_ver = 0, ret_hor = 0; + + // 读取行和列的值 + scanf("%d%d", &n, &m); + // 动态分配数组a(横)和b(纵)的空间 + int *a = (int *)malloc(sizeof(int) * n); + int *b = (int *)malloc(sizeof(int) * m); + + // 初始化数组a和b + for (int i = 0; i < n; i++) + { + a[i] = 0; + } + for (int i = 0; i < m; i++) + { + b[i] = 0; + } + + // 读取区块权值并计算每行和每列的总权值 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < m; j++) + { + int tmp; + scanf("%d", &tmp); + a[i] += tmp; + b[j] += tmp; + } + } + + // 计算每列以及每行的前缀和 + for (int i = 1; i < n; i++) + { + a[i] += a[i - 1]; + } + for (int i = 1; i < m; i++) + { + b[i] += b[i - 1]; + } + + // 初始化ret_ver和ret_hor为最大可能值 + ret_hor = a[n - 1]; + ret_ver = b[m - 1]; + + // 计算按行划分的最小差异 + int ret2 = 0; + while (ret2 < n) + { + ret_hor = (ret_hor > abs(a[n - 1] - 2 * a[ret2])) ? abs(a[n - 1] - 2 * a[ret2]) : ret_hor; + // 原理同列,但更高级 + ret2++; + } + // 计算按列划分的最小差异 + int ret1 = 0; + while (ret1 < m) + { + if (ret_ver > abs(b[m - 1] - 2 * b[ret1])) + { + ret_ver = abs(b[m - 1] - 2 * b[ret1]); + } + ret1++; + } + + // 输出最小差异 + printf("%d\n", (ret_ver <= ret_hor) ? ret_ver : ret_hor); + + // 释放分配的内存 + free(a); + free(b); + return 0; +} + +``` + +### Go + +前缀和 + +```go +package main + +import ( + "fmt" + "os" + "bufio" + "strings" + "strconv" + "math" +) + +func main() { + var n, m int + + reader := bufio.NewReader(os.Stdin) + + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + params := strings.Split(line, " ") + + n, _ = strconv.Atoi(params[0]) + m, _ = strconv.Atoi(params[1])//n和m读取完成 + + land := make([][]int, n)//土地矩阵初始化 + + for i := 0; i < n; i++ { + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + values := strings.Split(line, " ") + land[i] = make([]int, m) + for j := 0; j < m; j++ { + value, _ := strconv.Atoi(values[j]) + land[i][j] = value + } + }//所有读取完成 + + //初始化前缀和矩阵 + preMatrix := make([][]int, n+1) + for i := 0; i <= n; i++ { + preMatrix[i] = make([]int, m+1) + } + + for a := 1; a < n+1; a++ { + for b := 1; b < m+1; b++ { + preMatrix[a][b] = land[a-1][b-1] + preMatrix[a-1][b] + preMatrix[a][b-1] - preMatrix[a-1][b-1] + } + } + + totalSum := preMatrix[n][m] + + minDiff := math.MaxInt32//初始化极大数,用于比较 + + //按行分割 + for i := 1; i < n; i++ { + topSum := preMatrix[i][m] + + bottomSum := totalSum - topSum + + diff := int(math.Abs(float64(topSum - bottomSum))) + if diff < minDiff { + minDiff = diff + } + } + + //按列分割 + for j := 1; j < m; j++ { + topSum := preMatrix[n][j] + + bottomSum := totalSum - topSum + + diff := int(math.Abs(float64(topSum - bottomSum))) + if diff < minDiff { + minDiff = diff + } + } + + fmt.Println(minDiff) +} +``` + +

diff --git "a/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" new file mode 100644 index 0000000000..80d7851eaf --- /dev/null +++ "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" @@ -0,0 +1,930 @@ + +

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

+ +# dijkstra(堆优化版)精讲 + +[卡码网:47. 参加科学大会](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://file1.kamacoder.com/i/algo/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://file1.kamacoder.com/i/algo/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + + +## 思路 + +> 本篇我们来讲解 堆优化版dijkstra,看本篇之前,一定要先看 我讲解的 朴素版dijkstra,否则本篇会有部分内容看不懂。 + +在上一篇中,我们讲解了朴素版的dijkstra,该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。 + +如果n很大的话,我们可以换一个角度来优先性能。 + +在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](./0053.寻宝-prim.md)(从点的角度来求最小生成树)、[Kruskal算法](./0053.寻宝-Kruskal.md)(从边的角度来求最小生成树) + +这么在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://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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 算法也就理解到位了。 + + + +## 其他语言版本 + +### Java + +```Java + +import java.util.*; + +class Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int to, int val) { + this.to = to; + this.val = val; + } +} + +class MyComparison implements Comparator> { + @Override + public int compare(Pair lhs, Pair rhs) { + return Integer.compare(lhs.second, rhs.second); + } +} + +class Pair { + public final U first; + public final V second; + + public Pair(U first, V second) { + this.first = first; + this.second = second; + } +} + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + List> grid = new ArrayList<>(n + 1); + for (int i = 0; i <= n; i++) { + grid.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int p1 = scanner.nextInt(); + int p2 = scanner.nextInt(); + int val = scanner.nextInt(); + grid.get(p1).add(new Edge(p2, val)); + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + + // 记录顶点是否被访问过 + boolean[] visited = new boolean[n + 1]; + + // 优先队列中存放 Pair<节点,源点到该节点的权值> + PriorityQueue> pq = new PriorityQueue<>(new MyComparison()); + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.add(new Pair<>(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.isEmpty()) { + // 1. 第一步,选源点到哪个节点近且该节点未被访问过(通过优先级队列来实现) + // <节点, 源点到该节点的距离> + Pair cur = pq.poll(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid.get(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.add(new Pair<>(edge.to, minDist[edge.to])); + } + } + } + + if (minDist[end] == Integer.MAX_VALUE) { + System.out.println(-1); // 不能到达终点 + } else { + System.out.println(minDist[end]); // 到达终点最短路径 + } + } +} + +``` + + +### Python + +```python +import heapq + +class Edge: + def __init__(self, to, val): + self.to = to + self.val = val + +def dijkstra(n, m, edges, start, end): + grid = [[] for _ in range(n + 1)] + + for p1, p2, val in edges: + grid[p1].append(Edge(p2, val)) + + minDist = [float('inf')] * (n + 1) + visited = [False] * (n + 1) + + pq = [] + heapq.heappush(pq, (0, start)) + minDist[start] = 0 + + while pq: + cur_dist, cur_node = heapq.heappop(pq) + + if visited[cur_node]: + continue + + visited[cur_node] = True + + for edge in grid[cur_node]: + if not visited[edge.to] and cur_dist + edge.val < minDist[edge.to]: + minDist[edge.to] = cur_dist + edge.val + heapq.heappush(pq, (minDist[edge.to], edge.to)) + + return -1 if minDist[end] == float('inf') else minDist[end] + +# 输入 +n, m = map(int, input().split()) +edges = [tuple(map(int, input().split())) for _ in range(m)] +start = 1 # 起点 +end = n # 终点 + +# 运行算法并输出结果 +result = dijkstra(n, m, edges, start, end) +print(result) + +``` + +### Go + +```go +package main + +import ( + "container/heap" + "fmt" + "math" +) + +// Edge 表示带权重的边 +type Edge struct { + to, val int +} + +// PriorityQueue 实现一个小顶堆 +type Item struct { + node, dist int +} + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { return len(pq) } + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].dist < pq[j].dist +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + *pq = append(*pq, x.(*Item)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} + +func dijkstra(n, m int, edges [][]int, start, end int) int { + grid := make([][]Edge, n+1) + for _, edge := range edges { + p1, p2, val := edge[0], edge[1], edge[2] + grid[p1] = append(grid[p1], Edge{to: p2, val: val}) + } + + minDist := make([]int, n+1) + for i := range minDist { + minDist[i] = math.MaxInt64 + } + visited := make([]bool, n+1) + + pq := &PriorityQueue{} + heap.Init(pq) + heap.Push(pq, &Item{node: start, dist: 0}) + minDist[start] = 0 + + for pq.Len() > 0 { + cur := heap.Pop(pq).(*Item) + + if visited[cur.node] { + continue + } + + visited[cur.node] = true + + for _, edge := range grid[cur.node] { + if !visited[edge.to] && minDist[cur.node]+edge.val < minDist[edge.to] { + minDist[edge.to] = minDist[cur.node] + edge.val + heap.Push(pq, &Item{node: edge.to, dist: minDist[edge.to]}) + } + } + } + + if minDist[end] == math.MaxInt64 { + return -1 + } + return minDist[end] +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + edges := make([][]int, m) + for i := 0; i < m; i++ { + var p1, p2, val int + fmt.Scan(&p1, &p2, &val) + edges[i] = []int{p1, p2, val} + } + + start := 1 // 起点 + end := n // 终点 + + result := dijkstra(n, m, edges, start, end) + fmt.Println(result) +} + +``` + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" new file mode 100644 index 0000000000..42099df92a --- /dev/null +++ "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" @@ -0,0 +1,945 @@ + +

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

+ +# dijkstra(朴素版)精讲 + +[卡码网:47. 参加科学大会](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://file1.kamacoder.com/i/algo/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://file1.kamacoder.com/i/algo/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + +## 思路 + +本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。 + +接下来,我们来详细讲解最短路算法中的 dijkstra 算法。 + +dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。 + +需要注意两点: + +* dijkstra 算法可以同时求 起点到所有节点的最短路径 +* 权值不能为负数 + +(这两点后面我们会讲到) + +如本题示例中的图: + +![](https://file1.kamacoder.com/i/algo/20240125162647.png) + +起点(节点1)到终点(节点7) 的最短路径是 图中 标记绿线的部分。 + +最短路径的权值为12。 + +其实 dijkstra 算法 和 我们之前讲解的prim算法思路非常接近,如果大家认真学过[prim算法](./0053.寻宝-prim.md),那么理解 Dijkstra 算法会相对容易很多。(这也是我要先讲prim再讲dijkstra的原因) + +dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。 + +这里我也给出 **dijkstra三部曲**: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +大家此时已经会发现,这和prim算法 怎么这么像呢。 + +我在[prim算法](./0053.寻宝-prim.md)讲解中也给出了三部曲。 prim 和 dijkstra 确实很像,思路也是类似的,这一点我在后面还会详细来讲。 + +在dijkstra算法中,同样有一个数组很重要,起名为:minDist。 + +**minDist数组 用来记录 每一个节点距离源点的最小距离**。 + +理解这一点很重要,也是理解 dijkstra 算法的核心所在。 + +大家现在看着可能有点懵,不知道什么意思。 + +没关系,先让大家有一个印象,对理解后面讲解有帮助。 + +我们先来画图看一下 dijkstra 的工作过程,以本题示例为例: (以下为朴素版dijkstra的思路) + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) + +## 朴素版dijkstra + +### 模拟过程 + +----------- + +0、初始化 + +minDist数组数值初始化为int最大值。 + +这里在强点一下 **minDist数组的含义:记录所有节点到源点的最短路径**,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。 + +![](https://file1.kamacoder.com/i/algo/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240130115421.png) + + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1 +* 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[3] = 4 + +可能有录友问:为啥和 minDist[2] 比较? + +再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理 + + +------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点到节点2距离最近,选节点2 + +2、该最近节点被标记访问过 + +节点2被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://file1.kamacoder.com/i/algo/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算法精讲](./0053.寻宝-prim.md) 【拓展】中 已经详细讲解了。 + +在这里就不再赘述。 + +打印路径只需要添加 几行代码, 打印路径的代码我都加上的日志,如下: + +```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://file1.kamacoder.com/i/algo/20240201111352.png) + +### 出现负数 + +如果图中边的权值为负数,dijkstra 还合适吗? + +看一下这个图: (有负权值) + +![](https://file1.kamacoder.com/i/algo/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://file1.kamacoder.com/i/algo/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110217.png) + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为100,小于原minDist[2]的数值max,更新minDist[2] = 100 +* 源点到节点3的最短距离为1,小于原minDist[3]的数值max,更新minDist[3] = 1 + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点3最近,距离为1,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点3访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110711.png) + +-------------- + +至此dijkstra的模拟过程就结束了,根据最后的minDist数组,我们求 节点1 到 节点5 的最短路径的权值总和为 3,路径: 节点1 -> 节点3 -> 节点4 -> 节点5 + +通过以上的过程模拟,我们可以发现 之所以 没有走有负权值的最短路径 是因为 在 访问 节点 2 的时候,节点 3 已经访问过了,就不会再更新了。 + +那有录友可能会想: 我可以改代码逻辑啊,访问过的节点,也让它继续访问不就好了? + +那么访问过的节点还能继续访问会不会有死循环的出现呢?控制逻辑不让其死循环?那特殊情况自己能都想清楚吗?(可以试试,实践出真知) + +对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,**但最终会发现只是 拆了东墙补西墙**,对dijkstra的补充逻辑只能满足某特定场景最短路求解。 + +对于求解带有负权值的最短路问题,可以使用 Bellman-Ford 算法 ,我在后序会详细讲解。 + +## dijkstra与prim算法的区别 + +> 这里再次提示,需要先看我的 [prim算法精讲](./0053.寻宝-prim.md) ,否则可能不知道我下面讲的是什么。 + +大家可以发现 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,我会在下一篇再来给大家详细讲解**。 + +加油 + + + +## 其他语言版本 + +### Java + +```Java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + int[][] grid = new int[n + 1][n + 1]; + for (int i = 0; i <= n; i++) { + Arrays.fill(grid[i], Integer.MAX_VALUE); + } + + for (int i = 0; i < m; i++) { + int p1 = scanner.nextInt(); + int p2 = scanner.nextInt(); + int val = scanner.nextInt(); + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + // 存储从源点到每个节点的最短距离 + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + + // 记录顶点是否被访问过 + boolean[] visited = new boolean[n + 1]; + + minDist[start] = 0; // 起始点到自身的距离为0 + + for (int i = 1; i <= n; i++) { // 遍历所有节点 + + int minVal = Integer.MAX_VALUE; + 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] != Integer.MAX_VALUE && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + } + + if (minDist[end] == Integer.MAX_VALUE) { + System.out.println(-1); // 不能到达终点 + } else { + System.out.println(minDist[end]); // 到达终点最短路径 + } + } +} + +``` + +### Python + +``` +import sys + +def dijkstra(n, m, edges, start, end): + # 初始化邻接矩阵 + grid = [[float('inf')] * (n + 1) for _ in range(n + 1)] + for p1, p2, val in edges: + grid[p1][p2] = val + + # 初始化距离数组和访问数组 + minDist = [float('inf')] * (n + 1) + visited = [False] * (n + 1) + + minDist[start] = 0 # 起始点到自身的距离为0 + + for _ in range(1, n + 1): # 遍历所有节点 + minVal = float('inf') + cur = -1 + + # 选择距离源点最近且未访问过的节点 + for v in range(1, n + 1): + if not visited[v] and minDist[v] < minVal: + minVal = minDist[v] + cur = v + + if cur == -1: # 如果找不到未访问过的节点,提前结束 + break + + visited[cur] = True # 标记该节点已被访问 + + # 更新未访问节点到源点的距离 + for v in range(1, n + 1): + if not visited[v] and grid[cur][v] != float('inf') and minDist[cur] + grid[cur][v] < minDist[v]: + minDist[v] = minDist[cur] + grid[cur][v] + + return -1 if minDist[end] == float('inf') else minDist[end] + +if __name__ == "__main__": + input = sys.stdin.read + data = input().split() + n, m = int(data[0]), int(data[1]) + edges = [] + index = 2 + for _ in range(m): + p1 = int(data[index]) + p2 = int(data[index + 1]) + val = int(data[index + 2]) + edges.append((p1, p2, val)) + index += 3 + start = 1 # 起点 + end = n # 终点 + + result = dijkstra(n, m, edges, start, end) + print(result) + +``` + +### Go + +### Rust + +### JavaScript + +```js +function dijkstra(grid, start, end) { + const visited = Array.from({length: end + 1}, () => false) + const minDist = Array.from({length: end + 1}, () => Number.MAX_VALUE) + minDist[start] = 0 + + for (let i = 1 ; i < end + 1 ; i++) { + let cur = -1 + let tempMinDist = Number.MAX_VALUE + // 1. 找尋與起始點距離最近且未被訪的節點 + for (let j = 1 ; j < end + 1 ; j++) { + if (!visited[j] && minDist[j] < tempMinDist) { + cur = j + tempMinDist = minDist[j] + } + } + if (cur === -1) break; + + // 2. 更新節點狀態為已拜訪 + visited[cur] = true + + // 3. 更新未拜訪節點與起始點的最短距離 + for (let j = 1 ; j < end + 1 ; j++) { + if(!visited[j] && grid[cur][j] != Number.MAX_VALUE + && grid[cur][j] + minDist[cur] < minDist[j] + ) { + minDist[j] = grid[cur][j] + minDist[cur] + } + } + } + + return minDist[end] === Number.MAX_VALUE ? -1 : minDist[end] +} + + +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const grid = Array.from({length: n + 1}, + () => Array.from({length:n + 1}, () => Number.MAX_VALUE)) + for (let i = 0 ; i < m ; i++) { + const [s, e, w] = (await readline()).split(" ").map(Number) + grid[s][e] = w + } + + // dijkstra + const result = dijkstra(grid, 1, n) + + // 輸出 + console.log(result) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" "b/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" new file mode 100644 index 0000000000..53da7af9ee --- /dev/null +++ "b/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" @@ -0,0 +1,763 @@ + +

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

+ +# 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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240111114204.png) + +-------- + +选边(4,5),节点4 和 节点 5 不在同一个集合,生成树可以添加边(4,5) ,并将节点4,节点5 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240111120458.png) + +**大家判断两个节点是否在同一个集合,就看图中两个节点是否有绿色的粗线连着就行** + +------ + +(这里在强调一下,以下选边是按照上面排序好的边的数组来选择的) + +选边(1,3),节点1 和 节点3 不在同一个集合,生成树添加边(1,3),并将节点1,节点3 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112105834.png) + +--------- + +选边(2,6),节点2 和 节点6 不在同一个集合,生成树添加边(2,6),并将节点2,节点6 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110214.png) + +-------- + +选边(3,4),节点3 和 节点4 不在同一个集合,生成树添加边(3,4),并将节点3,节点4 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110450.png) + +---------- + +选边(6,7),节点6 和 节点7 不在同一个集合,生成树添加边(6,7),并将 节点6,节点7 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110637.png) + +----------- + +选边(5,7),节点5 和 节点7 在同一个集合,不做计算。 + +选边(1,5),两个节点在同一个集合,不做计算。 + +后面遍历 边(3,2),(2,4),(5,6) 同理,都因两个节点已经在同一集合,不做计算。 + + +------- + +此时 我们就已经生成了一个最小生成树,即: + +![](https://file1.kamacoder.com/i/algo/20240112110637.png) + +在上面的讲解中,看图的话 大家知道如何判断 两个节点 是否在同一个集合(是否有绿色的线连在一起),以及如何把两个节点加入集合(就在图中把两个节点连上) + +**但在代码中,如果将两个节点加入同一个集合,又如何判断两个节点是否在同一个集合呢**? + +这里就涉及到我们之前讲解的[并查集](./图论并查集理论基础.md)。 + +我们在并查集开篇的时候就讲了,并查集主要就两个功能: + +* 将两个元素添加到一个集合中 +* 判断两个元素在不在同一个集合 + +大家发现这正好符合 Kruskal算法的需求,这也是为什么 **我要先讲并查集,再讲 Kruskal**。 + +关于 并查集,我已经在[并查集精讲](./图论并查集理论基础.md) 详细讲解过了,所以这里不再赘述,我们直接用。 + +本题代码如下,已经详细注释: + +```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://file1.kamacoder.com/i/algo/20240116163014.png) + + +其实造成这个差别 是对边排序的时候 权值相同的边先后顺序的问题导致的,无论相同权值边的顺序是什么样的,最后都能得出最小生成树。 + + +## 拓展二 + + +此时我们已经讲完了 Kruskal 和 prim 两个解法来求最小生成树。 + +什么情况用哪个算法更合适呢。 + +Kruskal 与 prim 的关键区别在于,prim维护的是节点的集合,而 Kruskal 维护的是边的集合。 +如果 一个图中,节点多,但边相对较少,那么使用Kruskal 更优。 + +有录友可能疑惑,一个图里怎么可能节点多,边却少呢? + +节点未必一定要连着边那, 例如 这个图,大家能明显感受到边没有那么多对吧,但节点数量 和 上述我们讲的例子是一样的。 + +![](https://file1.kamacoder.com/i/algo/20240116152211.png) + +为什么边少的话,使用 Kruskal 更优呢? + +因为 Kruskal 是对边进行排序的后 进行操作是否加入到最小生成树。 + +边如果少,那么遍历操作的次数就少。 + +在节点数量固定的情况下,图中的边越少,Kruskal 需要遍历的边也就越少。 + +而 prim 算法是对节点进行操作的,节点数量越少,prim算法效率就越优。 + +所以在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。 + +> 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图 + + +Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。 + +Kruskal算法 时间复杂度 为 nlogn,其中n 为边的数量,适用稀疏图。 + +## 总结 + +如果学过了并查集,其实 kruskal 比 prim更好理解一些。 + +本篇,我们依然通过模拟 Kruskal 算法的过程,来带大家一步步了解其工作过程。 + +在 拓展一 中讲解了 如何输出最小生成树的边。 + +在拓展二 中讲解了 prim 和 Kruskal的区别。 + +录友们可以细细体会。 + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +class Edge { + int l, r, val; + + Edge(int l, int r, int val) { + this.l = l; + this.r = r; + this.val = val; + } +} + +public class Main { + private static int n = 10001; + private static int[] father = new int[n]; + + // 并查集初始化 + public static void init() { + for (int i = 0; i < n; i++) { + father[i] = i; + } + } + + // 并查集的查找操作 + public static int find(int u) { + if (u == father[u]) return u; + return father[u] = find(father[u]); + } + + // 并查集的加入集合 + public static void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return; + father[v] = u; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int v = scanner.nextInt(); + int e = scanner.nextInt(); + List edges = new ArrayList<>(); + int result_val = 0; + + for (int i = 0; i < e; i++) { + int v1 = scanner.nextInt(); + int v2 = scanner.nextInt(); + int val = scanner.nextInt(); + edges.add(new Edge(v1, v2, val)); + } + + // 执行Kruskal算法 + edges.sort(Comparator.comparingInt(edge -> edge.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); + } + } + System.out.println(result_val); + scanner.close(); + } +} + +``` + +### Python + +```python +class Edge: + def __init__(self, l, r, val): + self.l = l + self.r = r + self.val = val + +n = 10001 +father = list(range(n)) + +def init(): + global father + father = list(range(n)) + +def find(u): + if u != father[u]: + father[u] = find(father[u]) + return father[u] + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[v] = u + +def kruskal(v, edges): + edges.sort(key=lambda edge: edge.val) + init() + result_val = 0 + + for edge in edges: + x = find(edge.l) + y = find(edge.r) + if x != y: + result_val += edge.val + join(x, y) + + return result_val + +if __name__ == "__main__": + import sys + input = sys.stdin.read + data = input().split() + + v = int(data[0]) + e = int(data[1]) + + edges = [] + index = 2 + for _ in range(e): + v1 = int(data[index]) + v2 = int(data[index + 1]) + val = int(data[index + 2]) + edges.append(Edge(v1, v2, val)) + index += 3 + + result_val = kruskal(v, edges) + print(result_val) + +``` + + +### Go + +### Rust + +### JavaScript + +```js +function kruskal(v, edges) { + const father = Array.from({ length: v + 1 }, (_, i) => i) + + function find(u){ + if (u === father[u]) { + return u + } else { + father[u] = find(father[u]) + return father[u] + } + + } + + function isSame(u, v) { + let s = find(u) + let t = find(v) + return s === t + } + + function join(u, v) { + let s = find(u) + let t = find(v) + if (s !== t) { + father[s] = t + } + } + + edges.sort((a, b) => a[2] - b[2]) + let result = 0 + for (const [v1, v2, w] of edges) { + if (!isSame(v1, v2)) { + result += w + join(v1 ,v2) + } + } + console.log(result) +} + + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [v, e] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < e ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + kruskal(v, edges) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C +并查集方法一 +```c +#include +#include + +// 定义边结构体,包含两个顶点vex1和vex2以及它们之间的权重val +struct Edge +{ + int vex1, vex2, val; +}; + +// 冒泡排序函数,用于按边的权重val不减序排序边数组 +void bubblesort(struct Edge *a, int numsize) +{ + for (int i = 0; i < numsize - 1; ++i) + { + + for (int j = 0; j < numsize - i - 1; ++j) + { + if (a[j].val > a[j + 1].val) + { + struct Edge temp = a[j]; + a[j] = a[j + 1]; + a[j + 1] = temp; + } + } + } +} + +int main() +{ + int v, e; + int v1, v2, val; + int ret = 0; + + scanf("%d%d", &v, &e); + struct Edge *edg = (struct Edge *)malloc(sizeof(struct Edge) * e); + int *conne_gra = (int *)malloc(sizeof(int) * (v + 1)); + + // 初始化连通图数组,每个顶点初始时只与自己相连通 + for (int i = 0; i <= v; ++i) + { + conne_gra[i] = i; + } + + // 读取所有边的信息并存储到edg(存储所有边的)数组中 + for (int i = 0; i < e; ++i) + { + scanf("%d%d%d", &v1, &v2, &val); + edg[i].vex1 = v1; + edg[i].vex2 = v2; + edg[i].val = val; + } + bubblesort(edg, e); // 调用冒泡排序函数对边进行排序 + + // 遍历所有边,执行Kruskal算法来找到最小生成树 + for (int i = 0; i < e; ++i) + { + if (conne_gra[edg[i].vex1] != conne_gra[edg[i].vex2]) + { // 如果当前边的两个顶点不在同一个连通分量中 + int tmp1 = conne_gra[edg[i].vex1], tmp2 = conne_gra[edg[i].vex2]; + for (int k = 1; k <= v; ++k) + { // 将所有属于tmp2的顶点合并到tmp1的连通分量中 + if (conne_gra[k] == tmp2) + conne_gra[k] = tmp1; + } + ret += edg[i].val; // 将当前边的权重加到最小生成树的权重中 + } + } + printf("%d", ret); + return 0; +} + +``` +并查集方法二 +```c +#include +#include + +// 定义边结构体,包含两个顶点vex1和vex2以及它们之间的权重val (略,同上) +// 冒泡排序函数,用于按边的权重val不减序排序边数组(略,同上) + +// 并查集的查找操作 +int find(int m, int *father) +{ // 如果当前节点是其自身的父节点,则直接返回该节点 + // 否则递归查找其父节点的根,并将当前节点直接连接到根节点 + return (m == father[m]) ? m : (father[m] = find(father[m], father)); // 路径压缩 +} + +// 并查集的加入集合 +void Union(int m, int n, int *father) +{ + int x = find(m, father); + int y = find(n, father); + if (x == y) + return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[y] = x; +} + +int main() +{ + int v, e; + int v1, v2, val; + int ret = 0; + + scanf("%d%d", &v, &e); + struct Edge *edg = (struct Edge *)malloc(sizeof(struct Edge) * e); + int *conne_gra = (int *)malloc(sizeof(int) * (v + 1)); + + // 初始化连通图数组,每个顶点初始时只与自己相连通 + for (int i = 0; i <= v; ++i) + { + conne_gra[i] = i; + } + // 读取所有边的信息并存储到edg(存储所有边的)数组中 + for (int i = 0; i < e; ++i) + { + scanf("%d%d%d", &v1, &v2, &val); + edg[i].vex1 = v1; + edg[i].vex2 = v2; + edg[i].val = val; + } + + bubblesort(edg, e); // 调用冒泡排序函数对边进行排序 + + // Kruskal算法的实现,通过边数组构建最小生成树 + int j = 0, count = 0; + while (v > 1) + { + if (find(edg[j].vex1, conne_gra) != find(edg[j].vex2, conne_gra)) + { + ret += edg[j].val; // 将当前边的权重加到最小生成树的权重中 + Union(edg[j].vex1, edg[j].vex2, conne_gra); + v--; + } + j++; + } + printf("%d", ret); + return 0; +} + +``` +
diff --git "a/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" "b/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" new file mode 100644 index 0000000000..df0129ee2a --- /dev/null +++ "b/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" @@ -0,0 +1,760 @@ + +

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

+ +# prim算法精讲 + +[卡码网: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算法。 + +最小生成树是所有节点的最小连通子图,即:以最小的成本(边的权值)将图中所有节点链接到一起。 + +图中有n个节点,那么一定可以用n-1条边将所有节点连接到一起。 + +那么如何选择这n-1条边就是最小生成树算法的任务所在。 + +例如本题示例中的无向有权图为: + +![](https://file1.kamacoder.com/i/algo/20231206164306.png) + +那么在这个图中,如何选取n-1条边使得图中所有节点连接到一起,并且边的权值和最小呢? + +(图中为n为7,即7个节点,那么只需要n-1即6条边就可以讲所有顶点连接到一起) + +prim算法是从节点的角度采用贪心的策略每次寻找距离最小生成树最近的节点并加入到最小生成树中。 + +prim算法核心就是三步,我称为**prim三部曲**,大家一定要熟悉这三步,代码相对会好些很多: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +现在录友们会对这三步很陌生,不知道这是干啥的,没关系,下面将会画图举例来带大家把这**prim三部曲**理解到位。 + +在prim算法中,有一个数组特别重要,这里我起名为:minDist。 + +刚刚我有讲过“每次寻找距离最小生成树最近的节点并加入到最小生成树中”,那么如何寻找距离最小生成树最近的节点呢? + +这就用到了minDist数组,它用来作什么呢? + +**minDist数组用来记录每一个节点距离最小生成树的最近距离**。理解这一点非常重要,这也是prim算法最核心要点所在,很多录友看不懂prim算法的代码,都是因为没有理解透这个数组的含义。 + +接下来,我们来通过一步一步画图,来带大家巩固**prim三部曲**以及minDist数组的作用。 + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从1开始计数,下标0就不使用了,这样下标和节点标号就可以对应上了,避免大家搞混**) + + +### 1 初始状态 + +minDist数组里的数值初始化为最大数,因为本题节点距离不会超过10000,所以初始化最大数为10001就可以。 + +相信这里录友就要问了,为什么这么做? + +现在还没有最小生成树,默认每个节点距离最小生成树是最大的,这样后面我们在比较的时候,发现更近的距离,才能更新到minDist数组上。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20231215105603.png) + +开始构造最小生成树 + +### 2 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选择距离最小生成树最近的节点,加入到最小生成树,刚开始还没有最小生成树,所以随便选一个节点加入就好(因为每一个节点一定会在最小生成树里,所以随便选一个就好),那我们选择节点1(符合遍历数组的习惯,第一个遍历的也是节点1) + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1已经算最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来,我们要更新所有节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102048.png) + + +注意下标0,我们就不管它了,下标1与节点1对应,这样可以避免大家把节点搞混。 + +此时所有非生成树的节点距离最小生成树(节点1)的距离都已经跟新了。 + +* 节点2与节点1的距离为1,比原先的距离值10001小,所以更新minDist[2]。 +* 节点3和节点1的距离为1,比原先的距离值10001小,所以更新minDist[3]。 +* 节点5和节点1的距离为2,比原先的距离值10001小,所以更新minDist[5]。 + +**注意图中我标记了minDist数组里更新的权值**,是哪两个节点之间的权值,例如minDist[2]=1,这个1是节点1与节点2之间的连线,清楚这一点对最后我们记录最小生成树的权值总和很重要。 + +(我在后面依然会不断重复prim三部曲,可能基础好的录友会感觉有点啰嗦,但也是让大家感觉这三部曲求解的过程) + +### 3 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选取一个距离最小生成树(节点1)最近的非生成树里的节点,节点2,3,5距离最小生成树(节点1)最近,选节点2(其实选节点3或者节点2都可以,距离一样的)加入最小生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1和节点2,已经算最小生成树的节点。 + + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来,我们要更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102431.png) + +此时所有非生成树的节点距离最小生成树(节点1、节点2)的距离都已经跟新了。 + +* 节点3和节点2的距离为2,和原先的距离值1小,所以不用更新。 +* 节点4和节点2的距离为2,比原先的距离值10001小,所以更新minDist[4]。 +* 节点5和节点2的距离为10001(不连接),所以不用更新。 +* 节点6和节点2的距离为1,比原先的距离值10001小,所以更新minDist[6]。 + +### 4 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选择一个距离最小生成树(节点1、节点2)最近的非生成树里的节点,节点3,6距离最小生成树(节点1、节点2)最近,选节点3(选节点6也可以,距离一样)加入最小生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1、节点2、节点3算是最小生成树的节点。 + + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102457.png) + +所有非生成树的节点距离最小生成树(节点1、节点2、节点3)的距离都已经跟新了。 + +* 节点4和节点3的距离为1,和原先的距离值2小,所以更新minDist[4]为1。 + +上面为什么我们只比较节点4和节点3的距离呢? + +因为节点3加入最小生成树后,非生成树节点只有节点4和节点3是链接的,所以需要重新更新一下节点4距离最小生成树的距离,其他节点距离最小生成树的距离都不变。 + +### 5 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选择一个距离最小生成树(节点1、节点2、节点3)最近的非生成树里的节点,为了巩固大家对minDist数组的理解,这里我再啰嗦一遍: + +![](https://file1.kamacoder.com/i/algo/20231217213516.png) + +**minDist数组是记录了所有非生成树节点距离生成树的最小距离**,所以从数组里我们能看出来,非生成树节点4和节点6距离生成树最近。 + + +任选一个加入生成树,我们选节点4(选节点6也行)。 + +**注意**,我们根据minDist数组,选取距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**(我在图中把权值对应的是哪两个节点也标记出来了)。 + +如果大家不理解,可以跟着我们下面的讲解,看minDist数组的变化,minDist数组里记录的权值对应的哪条边。 + +理解这一点很重要,因为最后我们要求最小生成树里所有边的权值和。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1、节点2、节点3、节点4算是最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102618.png) + +minDist数组已经更新了所有非生成树的节点距离最小生成树(节点1、节点2、节点3、节点4)的距离。 + +* 节点5和节点4的距离为1,和原先的距离值2小,所以更新minDist[5]为1。 + +### 6 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选距离最小生成树(节点1、节点2、节点3、节点4)最近的非生成树里的节点,只有节点5和节点6。 + + +选节点5(选节点6也可以)加入生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +节点1、节点2、节点3、节点4、节点5算是最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102646.png) + +minDist数组已经更新了所有非生成树的节点距离最小生成树(节点1、节点2、节点3、节点4、节点5)的距离。 + +* 节点6和节点5距离为2,比原先的距离值1大,所以不更新 +* 节点7和节点5距离为1,比原先的距离值10001小,更新minDist[7] + +### 7 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选距离最小生成树(节点1、节点2、节点3、节点4、节点5)最近的非生成树里的节点,只有节点6和节点7。 + +2、prim三部曲,第二步:最近节点加入生成树 + +选节点6(选节点7也行,距离一样的)加入生成树。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +节点1、节点2、节点3、节点4、节点5、节点6算是最小生成树的节点,接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102732.png) + +这里就不在重复描述了,大家类推,最后,节点7加入生成树,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102820.png) + +### 最后 + +最后我们就生成了一个最小生成树,绿色的边将所有节点链接到一起,并且保证权值是最小的,因为我们在更新minDist数组的时候,都是选距离最小生成树最近的点加入到树中。 + +讲解上面的模拟过程的时候,我已经强调多次minDist数组是记录了所有非生成树节点距离生成树的最小距离。 + +最后,minDist数组也就是记录的是最小生成树所有边的权值。 + +我在图中,特别把每条边的权值对应的是哪两个节点标记出来(例如minDist[7]=1,对应的是节点5和节点7之间的边,而不是节点6和节点7),为了就是让大家清楚,minDist里的每一个值对应的是哪条边。 + +那么我们要求最小生成树里边的权值总和就是把最后的minDist数组累加一起。 + +以下代码,我对prim三部曲,做了重点注释,大家根据这三步,就可以透彻理解prim。 + +```CPP +#include +#include +#include + +using namespace std; +int main() { + int v, e; + int x, y, k; + cin >> v >> e; + // 填一个默认最大值,题目描述val最大为10000 + vector> grid(v + 1, vector(v + 1, 10001)); + while (e--) { + cin >> x >> y >> k; + // 因为是双向图,所以两个方向都要填上 + grid[x][y] = k; + grid[y][x] = k; + + } + // 所有节点到最小生成树的最小距离 + vector minDist(v + 1, 10001); + + // 这个节点是否在树里 + vector isInTree(v + 1, false); + + // 我们只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起 + for (int i = 1; i < v; i++) { + + // 1、prim三部曲,第一步:选距离生成树最近节点 + int cur = -1; // 选中哪个节点 加入最小生成树 + int minVal = INT_MAX; + for (int j = 1; j <= v; j++) { // 1 - v,顶点编号,这里下标从1开始 + // 选取最小生成树节点的条件: + // (1)不在最小生成树里 + // (2)距离最小生成树最近的节点 + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; + cur = j; + } + } + // 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的新加入,需要更新一下数据了 + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } + } + } + // 统计结果 + int result = 0; + for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边 + result += minDist[i]; + } + cout << result << endl; + +} + +``` + +时间复杂度为O(n^2),其中n为节点数量。 + +## 拓展 + +上面讲解的是记录了最小生成树所有边的权值,如果让打印出来最小生成树的每条边呢?或者说要把这个最小生成树画出来呢? + + +此时我们就需要把最小生成树里每一条边记录下来。 + +此时有两个问题: + +* 1、用什么结构来记录 +* 2、如何记录 + +如果记录边,其实就是记录两个节点就可以,两个节点连成一条边。 + +如何记录两个节点呢? + +我们使用一维数组就可以记录。parent[节点编号] = 节点编号,这样就把一条边记录下来了。(当然如果节点编号非常大,可以考虑使用map) + +使用一维数组记录是有向边,不过我们这里不需要记录方向,所以只关注两条边是连接的就行。 + +parent数组初始化代码: + +```CPP +vector parent(v + 1, -1); +``` + +接下来就是第二个问题,如何记录? + +我们再来回顾一下prim三部曲, + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家先思考一下,我们是在第几步,可以记录最小生成树的边呢? + +在本面上半篇我们讲解过:“我们根据minDist数组,选组距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**。” + +既然minDist数组记录了最小生成树的边,是不是就是在更新minDist数组的时候,去更新parent数组来记录一下对应的边呢。 + + +所以在prim三部曲中的第三步,更新parent数组,代码如下: + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + parent[j] = cur; // 记录最小生成树的边 (注意数组指向的顺序很重要) + } +} +``` + +代码中注释中,我强调了数组指向的顺序很重要。因为不少录友在这里会写成这样: `parent[cur] = j` 。 + +这里估计大家会疑惑了,parent[节点编号A] = 节点编号B,就表示A和B相连,我们这里就不用在意方向,代码中为什么只能 `parent[j] = cur` 而不能 `parent[cur] = j` 这么写呢? + +如果写成 `parent[cur] = j`,在for循环中,有多个j满足要求,那么 parent[cur] 就会被反复覆盖,因为cur是一个固定值。 + +举个例子,cur=1,在for循环中,可能就j=2,j=3,j=4都符合条件,那么本来应该记录节点1与节点2、节点3、节点4相连的。 + +如果 `parent[cur] = j` 这么写,最后更新的逻辑是 parent[1] = 2, parent[1] = 3, parent[1] = 4,最后只能记录节点1与节点4相连,其他相连情况都被覆盖了。 + +如果这么写 `parent[j] = cur`,那就是 parent[2] = 1, parent[3] = 1, parent[4] = 1 ,这样才能完整表示出节点1与其他节点都是链接的,才没有被覆盖。 + +主要问题也是我们使用了一维数组来记录。 + +如果是二维数组,来记录两个点链接,例如 parent[节点编号A][节点编号B] = 1 ,parent[节点编号B][节点编号A] = 1,来表示节点A与节点B相连,那就没有上面说的这个注意事项了,当然这么做的话,就是多开辟的内存空间。 + +以下是输出最小生成树边的代码,不算最后输出,就额外添加了两行代码,我都注释标记了: + +```CPP +#include +#include +#include + +using namespace std; +int main() { + int v, e; + int x, y, k; + cin >> v >> e; + vector> grid(v + 1, vector(v + 1, 10001)); + while (e--) { + cin >> x >> y >> k; + grid[x][y] = k; + grid[y][x] = k; + } + + vector minDist(v + 1, 10001); + vector isInTree(v + 1, false); + + //加上初始化 + vector parent(v + 1, -1); + + for (int i = 1; i < v; i++) { + int cur = -1; + 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]) { + minDist[j] = grid[cur][j]; + + parent[j] = cur; // 记录边 + } + } + } + // 输出 最小生成树边的链接情况 + for (int i = 1; i <= v; i++) { + cout << i << "->" << parent[i] << endl; + } +} + +``` + +按照本题示例,代码输入如下: + +``` +1->-1 +2->1 +3->1 +4->3 +5->4 +6->2 +7->5 +``` + +注意,这里是无向图,我在输出上添加了箭头仅仅是为了方便大家看出是边的意思。 + +大家可以和我们本题最后生成的最小生成树的图去对比一下边的链接情况: + +![](https://file1.kamacoder.com/i/algo/20231229115714.png) + +绿色的边是最小生成树,和我们的输出完全一致。 + +## 总结 + +此时我就把prim算法讲解完毕了,我们再来回顾一下。 + +关于prim算法,我自创了三部曲,来帮助大家理解: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家只要理解这三部曲,prim算法至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于自己在写prim的时候两眼一抹黑完全凭感觉去写。 +这也为什么很多录友感觉prim算法比较难,而且每次学会来,隔一段时间又不会写了,主要是没有一个纲领。 + +理解这三部曲之后,更重要的就是理解minDist数组。 + +**minDist数组是prim算法的灵魂,它帮助prim算法完成最重要的一步,就是如何找到距离最小生成树最近的点**。 + +再来帮大家回顾minDist数组的含义:记录每一个节点距离最小生成树的最近距离。 + +理解minDist数组,至少大家看prim算法的代码不会懵。 + +也正是因为minDist数组的作用,我们根据minDist数组,选取距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**。 + +所以我们求最小生成树的权值和就是计算后的minDist数组数值总和。 + +最后我们拓展了如何获得最小生成树的每一条边,其实添加的代码很简单,主要是理解为什么使用parent数组来记录边以及在哪里更新parent数组。 + +同时,因为使用一维数组,数组的下标和数组如何赋值很重要,不要搞反,导致结果被覆盖。 + +好了,以上为总结,录友们学习愉快。 + + + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int v = scanner.nextInt(); + int e = scanner.nextInt(); + + // 初始化邻接矩阵,所有值初始化为一个大值,表示无穷大 + int[][] grid = new int[v + 1][v + 1]; + for (int i = 0; i <= v; i++) { + Arrays.fill(grid[i], 10001); + } + + // 读取边的信息并填充邻接矩阵 + for (int i = 0; i < e; i++) { + int x = scanner.nextInt(); + int y = scanner.nextInt(); + int k = scanner.nextInt(); + grid[x][y] = k; + grid[y][x] = k; + } + + // 所有节点到最小生成树的最小距离 + int[] minDist = new int[v + 1]; + Arrays.fill(minDist, 10001); + + // 记录节点是否在树里 + boolean[] isInTree = new boolean[v + 1]; + + // Prim算法主循环 + for (int i = 1; i < v; i++) { + int cur = -1; + int minVal = Integer.MAX_VALUE; + + // 选择距离生成树最近的节点 + 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]) { + minDist[j] = grid[cur][j]; + } + } + } + + // 统计结果 + int result = 0; + for (int i = 2; i <= v; i++) { + result += minDist[i]; + } + System.out.println(result); + scanner.close(); + } +} + +``` + +### Python +```python +# 接收输入 +v, e = list(map(int, input().strip().split())) +# 按照常规的邻接矩阵存储图信息,不可达的初始化为10001 +graph = [[10001] * (v+1) for _ in range(v+1)] +for _ in range(e): + x, y, w = list(map(int, input().strip().split())) + graph[x][y] = w + graph[y][x] = w + +# 定义加入生成树的标记数组和未加入生成树的最近距离 +visited = [False] * (v + 1) +minDist = [10001] * (v + 1) + +# 循环 n - 1 次,建立 n - 1 条边 +# 从节点视角来看:每次选中一个节点加入树,更新剩余的节点到树的最短距离, +# 这一步其实蕴含了确定下一条选取的边,计入总路程 ans 的计算 +for _ in range(1, v + 1): + min_val = 10002 + cur = -1 + for j in range(1, v + 1): + if visited[j] == False and minDist[j] < min_val: + cur = j + min_val = minDist[j] + visited[cur] = True + for j in range(1, v + 1): + if visited[j] == False and minDist[j] > graph[cur][j]: + minDist[j] = graph[cur][j] + +ans = 0 +for i in range(2, v + 1): + ans += minDist[i] +print(ans) +``` + +```python +def prim(v, e, edges): + import sys + import heapq + + # 初始化邻接矩阵,所有值初始化为一个大值,表示无穷大 + grid = [[10001] * (v + 1) for _ in range(v + 1)] + + # 读取边的信息并填充邻接矩阵 + for edge in edges: + x, y, k = edge + grid[x][y] = k + grid[y][x] = k + + # 所有节点到最小生成树的最小距离 + minDist = [10001] * (v + 1) + + # 记录节点是否在树里 + isInTree = [False] * (v + 1) + + # Prim算法主循环 + for i in range(1, v): + cur = -1 + minVal = sys.maxsize + + # 选择距离生成树最近的节点 + for j in range(1, v + 1): + if not isInTree[j] and minDist[j] < minVal: + minVal = minDist[j] + cur = j + + # 将最近的节点加入生成树 + isInTree[cur] = True + + # 更新非生成树节点到生成树的距离 + for j in range(1, v + 1): + if not isInTree[j] and grid[cur][j] < minDist[j]: + minDist[j] = grid[cur][j] + + # 统计结果 + result = sum(minDist[2:v+1]) + return result + +if __name__ == "__main__": + import sys + input = sys.stdin.read + data = input().split() + + v = int(data[0]) + e = int(data[1]) + + edges = [] + index = 2 + for _ in range(e): + x = int(data[index]) + y = int(data[index + 1]) + k = int(data[index + 2]) + edges.append((x, y, k)) + index += 3 + + result = prim(v, e, edges) + print(result) + +``` + +### Go + +### Rust + +### JavaScript +```js +function prim(v, edges) { + const grid = Array.from({ length: v + 1 }, () => new Array(v + 1).fill(10001)); // Fixed grid initialization + const minDist = new Array(v + 1).fill(10001) + const isInTree = new Array(v + 1).fill(false) + // 建構鄰接矩陣 + for(const [v1, v2, w] of edges) { + grid[v1][v2] = w + grid[v2][v1] = w + } + // prim 演算法 + for (let i = 1 ; i < v ; i++) { + let cur = -1 + let tempMinDist = Number.MAX_VALUE + // 1. 尋找距離生成樹最近的節點 + for (let j = 1 ; j < v + 1 ; j++) { + if (!isInTree[j] && minDist[j] < tempMinDist) { + tempMinDist = minDist[j] + cur = j + } + } + // 2. 將節點放入生成樹 + isInTree[cur] = true + // 3. 更新非生成樹節點與生成樹的最短距離 + for (let j = 1 ; j < v + 1 ; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j] + } + } + } + console.log(minDist.slice(2).reduce((acc, cur) => acc + cur, 0)) +} + + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [v, e] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < e ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + prim(v, edges) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kama54.\346\233\277\346\215\242\346\225\260\345\255\227.md" "b/problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" similarity index 62% rename from "problems/kama54.\346\233\277\346\215\242\346\225\260\345\255\227.md" rename to "problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" index f03e575d1d..67d31a5564 100644 --- "a/problems/kama54.\346\233\277\346\215\242\346\225\260\345\255\227.md" +++ "b/problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" @@ -1,11 +1,4 @@ -

- - - -

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

- - # 替换数字 [卡码网题目链接](https://kamacoder.com/problempage.php?pid=1064) @@ -28,7 +21,7 @@ ## 思路 -如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java刷题的录友,一定要使用辅助空间,因为Java里的string不能修改) +如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java和Python刷题的录友,一定要使用辅助空间,因为Java和Python里的string不能修改) 首先扩充数组到每个数字字符替换成 "number" 之后的大小。 @@ -36,11 +29,11 @@ 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231030165201.png) +![](https://file1.kamacoder.com/i/algo/20231030165201.png) 然后从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231030173058.png) +![](https://file1.kamacoder.com/i/algo/20231030173058.png) 有同学问了,为什么要从后向前填充,从前向后填充不行么? @@ -142,25 +135,126 @@ for (int i = 0; i < a.size(); i++) { ### C: ### Java: - +解法一 ```java import java.util.Scanner; -class Main { +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))){ + count++; + } + } + // 扩充字符串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 in = new Scanner(System.in); - String s = in.nextLine(); - StringBuilder sb = new StringBuilder(); + Scanner sc = new Scanner(System.in); + String s = sc.next(); + int len = 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 (s.charAt(i) >= 0 && s.charAt(i) <= '9') { + len += 5; + } } - System.out.println(sb); + + 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); } } ``` +### Python: +```python +class Solution(object): + def subsitute_numbers(self, s): + """ + :type s: str + :rtype: str + """ + + count = sum(1 for char in s if char.isdigit()) # 统计数字的个数 + expand_len = len(s) + (count * 5) # 计算扩充后字符串的大小, x->number, 每有一个数字就要增加五个长度 + res = [''] * expand_len + + new_index = expand_len - 1 # 指向扩充后字符串末尾 + old_index = len(s) - 1 # 指向原字符串末尾 + + while old_index >= 0: # 从后往前, 遇到数字替换成“number” + if s[old_index].isdigit(): + res[new_index-5:new_index+1] = "number" + new_index -= 6 + else: + res[new_index] = s[old_index] + new_index -= 1 + old_index -= 1 + + return "".join(res) + +if __name__ == "__main__": + solution = Solution() + + while True: + try: + s = input() + result = solution.subsitute_numbers(s) + print(result) + except EOFError: + break + +``` + ### Go: ````go package main @@ -234,16 +328,6 @@ func main(){ -### python: -```Python -class Solution: - def change(self, s): - lst = list(s) # Python里面的string也是不可改的,所以也是需要额外空间的。空间复杂度:O(n)。 - for i in range(len(lst)): - if lst[i].isdigit(): - lst[i] = "number" - return ''.join(lst) -``` ### JavaScript: ```js const readline = require("readline"); @@ -308,16 +392,44 @@ main(); ### Scala: - ### PHP: +```php += 0) { + if (is_numeric($s[$oldLen])) { + $s[$newLen--] = 'r'; + $s[$newLen--] = 'e'; + $s[$newLen--] = 'b'; + $s[$newLen--] = 'm'; + $s[$newLen--] = 'u'; + $s[$newLen--] = 'n'; + } else { + $s[$newLen--] = $s[$oldLen]; + } + $oldLen--; +} + +echo $s; +?> +``` -

- - - +### Rust: + +

diff --git "a/problems/kama55.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" "b/problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" similarity index 68% rename from "problems/kama55.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" rename to "problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" index 7137186042..be998390c5 100644 --- "a/problems/kama55.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" @@ -1,9 +1,4 @@ -

- - - -

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

# 右旋字符串 @@ -23,7 +18,7 @@ ``` 2 abcdefg -``` +``` 样例输出: @@ -45,16 +40,16 @@ fgabcde 本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106170143.png) +![](https://file1.kamacoder.com/i/algo/20231106170143.png) 右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106171557.png) +![](https://file1.kamacoder.com/i/algo/20231106171557.png) 此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106172058.png) +![](https://file1.kamacoder.com/i/algo/20231106172058.png) 其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,**负负得正**,这样就不影响子串里面字符的顺序了。 @@ -85,7 +80,7 @@ int main() { 可以的,不过,要记得 控制好 局部反转的长度,如果先局部反转,那么先反转的子串长度就是 len - n,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106172534.png) +![](https://file1.kamacoder.com/i/algo/20231106172534.png) 代码如下: @@ -222,6 +217,13 @@ s = s[len(s)-k:] + s[:len(s)-k] print(s) ``` +```Python 切片法 +k = int(input()) +s = input() + +print(s[-k:] + s[:-k]) +``` + ### Go: ```go package main @@ -253,28 +255,158 @@ func main(){ ``` +### C: +```C +#include +#include + +void reverse(char *s, int left, int right) +{ + while(left <= right) + { + char c = s[left]; + s[left] = s[right]; + s[right] = c; + left++; + right--; + } +} + +void rightRotate(char *s, int k) +{ + int len = strlen(s); + // 先局部反转再整体反转 + reverse(s, 0, len - k - 1); // 反转前部分 + reverse(s, len - k, len - 1); // 反转后部分:后k位 + reverse(s, 0, len - 1); // 整体反转 +} + +int main() +{ + + int k; + scanf("%d", &k); + char s[10000]; + scanf("%s", s); + + rightRotate(s, k); + printf("%s\n", s); + + return 0; +} +``` + ### JavaScript: +```javascript +// JS中字符串内不可单独修改 + +const readline = require('readline') + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +const inputs = []; // 存储输入 + +rl.on('line', function(data) { + inputs.push(data); + +}).on('close', function() { + const res = deal(inputs); + // 打印结果 + console.log(res); +}) +// 对传入的数据进行处理 +function deal(inputs) { + let [k, s] = inputs; + const len = s.length - 1; + k = parseInt(k); + str = s.split(''); + + str = reverseStr(str, 0, len - k) + str = reverseStr(str, len - k + 1, len) + str = reverseStr(str, 0, len) + + return str.join(''); +} + +// 根据提供的范围进行翻转 +function reverseStr(s, start, end) { + + while (start < end) { + [s[start], s[end]] = [s[end], s[start]] + + start++; + end--; + } + + return s; +} +``` ### TypeScript: ### Swift: +```swift +func rotateWords(_ s: String, _ k: Int) -> String { + var chars = Array(s) + // 先反转整体 + reverseWords(&chars, start: 0, end: s.count - 1) + // 反转前半段 + reverseWords(&chars, start: 0, end: k - 1) + // 反转后半段 + reverseWords(&chars, start: k, end: s.count - 1) + return String(chars) +} +// 反转start...end 的字符数组 +func reverseWords(_ chars: inout [Character], start: Int, end: Int) { + var left = start + var right = end + while left < right, right < chars.count { + (chars[left], chars[right]) = (chars[right], chars[left]) + left += 1 + right -= 1 + } +} +``` ### PHP: +```php + +``` + ### Scala: ### Rust: - - - -

- - - +

diff --git "a/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" "b/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" new file mode 100644 index 0000000000..894e0383d5 --- /dev/null +++ "b/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" @@ -0,0 +1,411 @@ + +# 58. 区间和 + +> 本题为代码随想录后续扩充题目,还没有视频讲解,顺便让大家练习一下ACM输入输出模式(笔试面试必备) + +[题目链接](https://kamacoder.com/problempage.php?pid=1070) + +题目描述 + +给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。 + +输入描述 + +第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间,直至文件结束。 + +输出描述 + +输出每个指定区间内元素的总和。 + +输入示例 + +``` +5 +1 +2 +3 +4 +5 +0 1 +1 3 +``` + +输出示例 + +``` +3 +9 +``` + +数据范围: + +0 < n <= 100000 + +## 思路 + +本题我们来讲解 数组 上常用的解题技巧:前缀和 + +首先来看本题,我们最直观的想法是什么? + +那就是给一个区间,然后 把这个区间的和都累加一遍不就得了,是一道简单不能再简单的题目。 + +代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + for (int i = 0; i < n; i++) cin >> vec[i]; + while (cin >> a >> b) { + int sum = 0; + // 累加区间 a 到 b 的和 + for (int i = a; i <= b; i++) sum += vec[i]; + cout << sum << endl; + } +} +``` + +代码一提交,发现超时了..... + +我在制作本题的时候,特别制作了大数据量查询,卡的就是这种暴力解法。 + +来举一个极端的例子,如果我查询m次,每次查询的范围都是从0 到 n - 1 + +那么该算法的时间复杂度是 O(n * m) m 是查询的次数 + +如果查询次数非常大的话,这个时间复杂度也是非常大的。 + +接下来我们来引入前缀和,看看前缀和如何解决这个问题。 + +前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。 + +**前缀和 在涉及计算区间和的问题时非常有用**! + +前缀和的思路其实很简单,我给大家举个例子很容易就懂了。 + +例如,我们要统计 vec[i] 这个数组上的区间和。 + +我们先做累加,即 p[i] 表示 下标 0 到 i 的 vec[i] 累加 之和。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240627110604.png) + +如果,我们想统计,在vec数组上 下标 2 到下标 5 之间的累加和,那是不是就用 p[5] - p[1] 就可以了。 + +为什么呢? + +`p[1] = vec[0] + vec[1];` + +`p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];` + +`p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];` + +这不就是我们要求的 下标 2 到下标 5 之间的累加和吗。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20240627111319.png) + +`p[5] - p[1]` 就是 红色部分的区间和。 + +而 p 数组是我们之前就计算好的累加和,所以后面每次求区间和的之后 我们只需要 O(1) 的操作。 + +**特别注意**: 在使用前缀和求解的时候,要特别注意 求解区间。 + +如上图,如果我们要求 区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1],而不是 p[5] - p[2]。 + +**很多录友在使用前缀和的时候,分不清前缀和的区间,建议画一画图,模拟一下 思路会更清晰**。 + +本题C++代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + vector p(n); + int presum = 0; + for (int i = 0; i < n; i++) { + cin >> vec[i]; + presum += vec[i]; + p[i] = presum; + } + + while (cin >> a >> b) { + int sum; + if (a == 0) sum = p[b]; + else sum = p[b] - p[a - 1]; + cout << sum << endl; + } +} + +``` + +C++ 代码 面对大量数据 读取 输出操作,最好用scanf 和 printf,耗时会小很多: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + vector p(n); + int presum = 0; + for (int i = 0; i < n; i++) { + scanf("%d", &vec[i]); + presum += vec[i]; + p[i] = presum; + } + + while (~scanf("%d%d", &a, &b)) { + int sum; + if (a == 0) sum = p[b]; + else sum = p[b] - p[a - 1]; + printf("%d\n", sum); + } +} + +``` + +## 其他语言版本 + +### Java + +```Java + +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + int n = scanner.nextInt(); + int[] vec = new int[n]; + int[] p = new int[n]; + + int presum = 0; + for (int i = 0; i < n; i++) { + vec[i] = scanner.nextInt(); + presum += vec[i]; + p[i] = presum; + } + + while (scanner.hasNextInt()) { + int a = scanner.nextInt(); + int b = scanner.nextInt(); + + int sum; + if (a == 0) { + sum = p[b]; + } else { + sum = p[b] - p[a - 1]; + } + System.out.println(sum); + } + + scanner.close(); + } +} + + +``` + +### Python + +```python + +import sys +input = sys.stdin.read + +def main(): + data = input().split() + index = 0 + n = int(data[index]) + index += 1 + vec = [] + for i in range(n): + vec.append(int(data[index + i])) + index += n + + p = [0] * n + presum = 0 + for i in range(n): + presum += vec[i] + p[i] = presum + + results = [] + while index < len(data): + a = int(data[index]) + b = int(data[index + 1]) + index += 2 + + if a == 0: + sum_value = p[b] + else: + sum_value = p[b] - p[a - 1] + + results.append(sum_value) + + for result in results: + print(result) + +if __name__ == "__main__": + main() + +``` + + +### JavaScript + +``` JavaScript + +function prefixSum() { + const readline = require('readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + let inputLines = []; + rl.on('line', (line) => { + inputLines.push(line.trim()); + }); + + rl.on('close', () => { + // 读取项数 n + const n = parseInt(inputLines[0]); + + // 使用前缀和,复杂度控制在 O(1) + let sum = new Array(n); + sum[0] = parseInt(inputLines[1]); + + // 计算前缀和数组 + for (let i = 1; i < n; i++) { + let value = parseInt(inputLines[i + 1]); + sum[i] = sum[i - 1] + value; + } + + // 处理区间和查询 + for (let i = n + 1; i < inputLines.length; i++) { + let [left, right] = inputLines[i].split(' ').map(Number); + + if (left === 0) { + console.log(sum[right]); + } else { + console.log(sum[right] - sum[left - 1]); + } + } + }); +} + + +``` + + + +### C + +```C +#include +#include + +int main(int argc, char *argv[]) +{ + int num; + // 读取数组长度 + scanf("%d", &num); + + // 使用动态内存分配而不是静态数组,以适应不同的输入大小 + int *a = (int *)malloc((num + 1) * sizeof(int)); + + // 初始化前缀和数组的第一个元素为0 + a[0] = 0; + + // 读取数组元素并计算前缀和 + for (int i = 1; i <= num; i++) + { + int mm; + scanf("%d", &mm); + // 累加前缀和 + a[i] = a[i - 1] + mm; + } + + int m, n; + // 循环读取区间并计算区间和,直到输入结束 + // scanf()返回成功匹配和赋值的个数,到达文件末尾则返回 EOF + while (scanf("%d%d", &m, &n) == 2) + { + // 输出区间和,注意区间是左闭右开,因此a[n+1]是包含n的元素的前缀和 + printf("%d\n", a[n+1] - a[m]); + } + + // 释放之前分配的内存 + free(a); + return 0; +} + +``` + +### Go + +```go +package main + +import ( + "fmt" + "bufio" + "strconv" + "os" +) + +func main() { + // bufio中读取数据的接口,因为数据卡的比较严,导致使用fmt.Scan会超时 + scanner := bufio.NewScanner(os.Stdin) + + // 获取数组大小 + scanner.Scan() + n, _ := strconv.Atoi(scanner.Text()) + + // 获取数组元素的同时计算前缀和,一般建议切片开大一点防止各种越界问题 + arr := make([]int, n + 1) + for i := 0; i < n; i++ { + scanner.Scan() + arr[i], _ = strconv.Atoi(scanner.Text()) + if i != 0 { + arr[i] += arr[i - 1] + } + } + + /* + 区间[l, r]的和可以使用区间[0, r]和[0, l - 1]相减得到, + 在代码中即为arr[r]-arr[l-1]。这里需要注意l-1是否越界 + */ + for { + var l, r int + scanner.Scan() + _, err := fmt.Sscanf(scanner.Text(), "%d %d", &l, &r) + if err != nil { + return + } + + if l > 0 { + fmt.Println(arr[r] - arr[l - 1]) + } else { + fmt.Println(arr[r]) + } + } +} +``` + +
diff --git "a/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" new file mode 100644 index 0000000000..9d3fbe839e --- /dev/null +++ "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" @@ -0,0 +1,539 @@ + +

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

+ +# 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://file1.kamacoder.com/i/algo/20240328104119.png) + +本图中,对所有边进行松弛,真正有效的松弛,只有松弛 边(节点1->节点2) 和 边(节点1->节点3) 。 + +而松弛 边(节点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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240412110348.png) + + +-------------------- + + +从队列里出去节点3,松弛节点3 作为出发点连接的边。 + +因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作,如图: + +![](https://file1.kamacoder.com/i/algo/20240412110420.png) + + +------------ + +从队列中取出节点4,松弛节点4作为出发点连接的边(节点4 -> 节点6) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 。 + +将节点6加入队列 + +如图: + +![](https://file1.kamacoder.com/i/algo/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 + +如图,将节点3加入队列,因为节点6已经在队列里,所以不用重复添加 + +![](https://file1.kamacoder.com/i/algo/20240729161116.png) + +所以我们在加入队列的过程可以有一个优化,**用visited数组记录已经在队列里的元素,已经在队列的元素不用重复加入** + +-------------- + +从队列中取出节点6,松弛节点6 作为出发点连接的边。 + +节点6作为终点,没有可以出发的边。 + +同理从队列中取出节点3,也没有可以出发的边 + +所以直接从队列中取出,如图: + +![](https://file1.kamacoder.com/i/algo/20240411115424.png) + +---------- + +这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。 + +大家可以发现 基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。 + +了解了大体流程,我们再看代码应该怎么写。 + +在上面模拟过程中,我们每次都要知道 一个节点作为出发点连接了哪些节点。 + +如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./0047.参会dijkstra堆.md) 中 图的存储 部分。 + + +整体代码如下: + +``` +#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); + + vector isInQueue(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(); + isInQueue[node] = false; // 从队列里取出的时候,要取消标记,我们只保证已经在队列里的元素不用重复加入 + 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; + if (isInQueue[to] == false) { // 已经在队列里的元素不用重复添加 + que.push(to); + isInQueue[to] = true; + } + } + } + + } + 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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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)`,从而不会有新的节点加入到队列。 + +但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。 + + + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List> graph = new ArrayList<>(); + + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.get(from).add(new Edge(from, to, val)); + } + + // Declare the minDist array to record the minimum distance form current node to the original node + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency + Queue queue = new LinkedList<>(); + queue.offer(1); + + // Declare a boolean array to record if the current node is in the queue to optimise the processing + boolean[] isInQueue = new boolean[n + 1]; + + while (!queue.isEmpty()) { + int curNode = queue.poll(); + isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled + for (Edge edge : graph.get(curNode)) { + if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge + minDist[edge.to] = minDist[edge.from] + edge.val; + if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue + queue.offer(edge.to); + isInQueue[edge.to] = true; + } + } + } + } + + // Outcome printing + if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python +```Python +import collections + +def main(): + n, m = map(int, input().strip().split()) + edges = [[] for _ in range(n + 1)] + for _ in range(m): + src, dest, weight = map(int, input().strip().split()) + edges[src].append([dest, weight]) + + minDist = [float("inf")] * (n + 1) + minDist[1] = 0 + que = collections.deque([1]) + visited = [False] * (n + 1) + visited[1] = True + + while que: + cur = que.popleft() + visited[cur] = False + for dest, weight in edges[cur]: + if minDist[cur] != float("inf") and minDist[cur] + weight < minDist[dest]: + minDist[dest] = minDist[cur] + weight + if visited[dest] == False: + que.append(dest) + visited[dest] = True + + if minDist[-1] == float("inf"): + return "unconnected" + return minDist[-1] + +if __name__ == "__main__": + print(main()) +``` +### Go + +### Rust + +### JavaScript + +```js +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const grid = {} + for (let i = 0 ; i < m ; i++) { + const [src, desc, w] = (await readline()).split(" ").map(Number) + if (grid.hasOwnProperty(src)) { + grid[src].push([desc, w]) + } else { + grid[src] = [[desc, w]] + } + } + const minDist = Array.from({length: n + 1}, () => Number.MAX_VALUE) + + // 起始點 + minDist[1] = 0 + + const q = [1] + const visited = Array.from({length: n + 1}, () => false) + + while (q.length) { + const src = q.shift() + const neighbors = grid[src] + visited[src] = false + if (neighbors) { + for (const [desc, w] of neighbors) { + if (minDist[src] !== Number.MAX_VALUE + && minDist[src] + w < minDist[desc]) { + minDist[desc] = minDist[src] + w + if (!visited[desc]) { + q.push(desc) + visited[desc] = true + } + + } + } + } + } + + // 輸出 + if (minDist[n] === Number.MAX_VALUE) { + console.log('unconnected') + } else { + console.log(minDist[n]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + + + +
diff --git "a/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" new file mode 100644 index 0000000000..63d1be2a30 --- /dev/null +++ "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" @@ -0,0 +1,541 @@ + +

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

+ +# 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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240329113537.png) + +(在复习一下,minDist[5] 表示起点到节点5的最短距离) + + +边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329113703.png) + +边:节点5 -> 节点3,权值为1 ,minDist[5] 还是默认数值max,所以不能基于节点5去更新节点3 如图: + +![](https://file1.kamacoder.com/i/algo/20240329113827.png) + + +边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 (经过上面的计算minDist[2]已经不是默认值,而是 1),更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329113927.png) + + +边:节点2 -> 节点4,权值为-3 ,minDist[4] > minDist[2] + (-3),更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329114036.png) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 + +![](https://file1.kamacoder.com/i/algo/20240329114120.png) + +边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5 ,如图: + +![](https://file1.kamacoder.com/i/algo/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 非常重要。 + +## 其他语言版本 + +### Java +```Java +public class Main { + + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List edges = new ArrayList<>(); + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + edges.add(new Edge(from, to, val)); + } + + // Represents the minimum distance from the current node to the original node + int[] minDist = new int[n + 1]; + + // Initialize the minDist array + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Starts the loop to relax all edges n - 1 times to update minDist array + for (int i = 1; i < n; i++) { + + for (Edge edge : edges) { + // Updates the minDist array + if (minDist[edge.from] != Integer.MAX_VALUE && (minDist[edge.from] + edge.val) < minDist[edge.to]) { + minDist[edge.to] = minDist[edge.from] + edge.val; + } + } + } + + // Outcome printing + if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python +```Python +def main(): + n, m = map(int, input().strip().split()) + edges = [] + for _ in range(m): + src, dest, weight = map(int, input().strip().split()) + edges.append([src, dest, weight]) + + minDist = [float("inf")] * (n + 1) + minDist[1] = 0 # 起点处距离为0 + + for i in range(1, n): + updated = False + for src, dest, weight in edges: + if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]: + minDist[dest] = minDist[src] + weight + updated = True + if not updated: # 若边不再更新,即停止回圈 + break + + if minDist[-1] == float("inf"): # 返还终点权重 + return "unconnected" + return minDist[-1] + +if __name__ == "__main__": + print(main()) +``` + +### Go + +### Rust + +### JavaScript + +```js +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < m ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + const minDist = Array.from({length: n + 1}, () => Number.MAX_VALUE) + // 起始點 + minDist[1] = 0 + + for (let i = 1 ; i < n ; i++) { + let update = false + for (const [src, desc, w] of edges) { + if (minDist[src] !== Number.MAX_VALUE && minDist[src] + w < minDist[desc]) { + minDist[desc] = minDist[src] + w + update = true + } + } + if (!update) { + break; + } + } + + // 輸出 + if (minDist[n] === Number.MAX_VALUE) { + console.log('unconnected') + } else { + console.log(minDist[n]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" "b/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" new file mode 100644 index 0000000000..957b8a80bc --- /dev/null +++ "b/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" @@ -0,0 +1,459 @@ + +

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

+ +# 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](./0094.城市间货物运输I.md) 延伸题目。 + +本题是要我们判断 负权回路,也就是图中出现环且环上的边总权值为负数。 + +如果在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。 + +所以对于 在有负权值的图中求最短路,都需要先看看这个图里有没有负权回路。 + +接下来我们来看 如何使用 bellman_ford 算法来判断 负权回路。 + +在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 中 我们讲了 bellman_ford 算法的核心就是一句话:对 所有边 进行 n-1 次松弛。 同时文中的 【拓展】部分, 我们也讲了 松弛n次以上 会怎么样? + +在没有负权回路的图中,松弛 n 次以上 ,结果不会有变化。 + +但本题有 负权回路,如果松弛 n 次,结果就会有变化了,因为 有负权回路 就是可以无限最短路径(一直绕圈,就可以一直得到无限小的最短距离)。 + +那么每松弛一次,都会更新最短路径,所以结果会一直有变化。 + +(如果对于 bellman_ford 不了解的录友,建议详细看这里:[kama94.城市间货物运输I](./0094.城市间货物运输I.md)) + +以上为理论分析,接下来我们再画图举例。 + +我们拿题目中示例来画一个图: + +![](https://file1.kamacoder.com/i/algo/20240705161426.png) + +图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的) + +节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1 + +而图中有负权回路: + +![](https://file1.kamacoder.com/i/algo/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](./0094.城市间货物运输I.md)) + +而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。 + +那么解决本题的 核心思路,就是在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 的基础上,再多松弛一次,看minDist数组 是否发生变化。 + +代码和 [kama94.城市间货物运输I](./0094.城市间货物运输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; + } + +} + +``` + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 基于Bellman_ford-SPFA方法 + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List> graph = new ArrayList<>(); + + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.get(from).add(new Edge(from, to, val)); + } + + // Declare the minDist array to record the minimum distance form current node to the original node + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency + Queue queue = new LinkedList<>(); + queue.offer(1); + + // Declare an array to record the times each node has been offered in the queue + int[] count = new int[n + 1]; + count[1]++; + + // Declare a boolean array to record if the current node is in the queue to optimise the processing + boolean[] isInQueue = new boolean[n + 1]; + + // Declare a boolean value to check if there is a negative weight loop inside the graph + boolean flag = false; + + while (!queue.isEmpty()) { + int curNode = queue.poll(); + isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled + for (Edge edge : graph.get(curNode)) { + if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge + minDist[edge.to] = minDist[edge.from] + edge.val; + if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue + queue.offer(edge.to); + count[edge.to]++; + isInQueue[edge.to] = true; + } + + if (count[edge.to] == n) { // If some node has been offered in the queue more than n-1 times + flag = true; + while (!queue.isEmpty()) queue.poll(); + break; + } + } + } + } + + if (flag) { + System.out.println("circle"); + } else if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python + +Bellman-Ford方法求解含有负回路的最短路问题 + +```python +import sys + +def main(): + input = sys.stdin.read + data = input().split() + index = 0 + + n = int(data[index]) + index += 1 + m = int(data[index]) + index += 1 + + grid = [] + for i in range(m): + p1 = int(data[index]) + index += 1 + p2 = int(data[index]) + index += 1 + val = int(data[index]) + index += 1 + # p1 指向 p2,权值为 val + grid.append([p1, p2, val]) + + start = 1 # 起点 + end = n # 终点 + + minDist = [float('inf')] * (n + 1) + minDist[start] = 0 + flag = False + + for i in range(1, n + 1): # 这里我们松弛n次,最后一次判断负权回路 + for side in grid: + from_node = side[0] + to = side[1] + price = side[2] + if i < n: + if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price: + minDist[to] = minDist[from_node] + price + else: # 多加一次松弛判断负权回路 + if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price: + flag = True + + if flag: + print("circle") + elif minDist[end] == float('inf'): + print("unconnected") + else: + print(minDist[end]) + +if __name__ == "__main__": + main() + +``` + +SPFA方法求解含有负回路的最短路问题 + +```python +from collections import deque +from math import inf + +def main(): + n, m = [int(i) for i in input().split()] + graph = [[] for _ in range(n+1)] + min_dist = [inf for _ in range(n+1)] + count = [0 for _ in range(n+1)] # 记录节点加入队列的次数 + for _ in range(m): + s, t, v = [int(i) for i in input().split()] + graph[s].append([t, v]) + + min_dist[1] = 0 # 初始化 + count[1] = 1 + d = deque([1]) + flag = False + + while d: # 主循环 + cur_node = d.popleft() + for next_node, val in graph[cur_node]: + if min_dist[next_node] > min_dist[cur_node] + val: + min_dist[next_node] = min_dist[cur_node] + val + count[next_node] += 1 + if next_node not in d: + d.append(next_node) + if count[next_node] == n: # 如果某个点松弛了n次,说明有负回路 + flag = True + if flag: + break + + if flag: + print("circle") + else: + if min_dist[-1] == inf: + print("unconnected") + else: + print(min_dist[-1]) + + +if __name__ == "__main__": + main() +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" "b/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" new file mode 100644 index 0000000000..0c00ccb668 --- /dev/null +++ "b/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" @@ -0,0 +1,925 @@ + +

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

+ +# 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](./0094.城市间货物运输I.md) 延伸题目。 + +注意题目中描述是 **最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径**。 + +在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲了:**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。 + +(如果对以上讲解看不懂,建议详看 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) ) + +本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 这里可能有录友想不懂为什么是k + 1,来看这个图: + +![](https://file1.kamacoder.com/i/algo/20240402115614.png) + +图中,节点1 最多已经经过2个节点 到达节点4,那么中间是有多少条边呢,是 3 条边对吧。 + +所以本题就是求:起点最多经过k + 1 条边到达终点的最短距离。 + +对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次,就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。 + +**注意**: 本题是 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 的拓展题,如果对 bellman_ford 没有深入了解,强烈建议先看 [kama94.城市间货物运输I](./0094.城市间货物运输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://file1.kamacoder.com/i/algo/20240409111940.png) + +其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点 默认是一个最大数,这样才能更新最小距离。 + +当我们开始对所有边开始第一次松弛: + +边:节点1 -> 节点2,权值为-1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = minDist[1] + (-1) = 0 - 1 = -1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240409111914.png) + + +边:节点2 -> 节点3,权值为1 ,minDist[3] > minDist[2] + 1 ,更新 minDist[3] = minDist[2] + 1 = -1 + 1 = 0 ,如图: +![](https://file1.kamacoder.com/i/algo/20240409111903.png) + + +边:节点3 -> 节点1,权值为-1 ,minDist[1] > minDist[3] + (-1),更新 minDist[1] = 0 + (-1) = -1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240409111849.png) + + +边:节点3 -> 节点4,权值为1 ,minDist[4] > minDist[3] + 1,更新 minDist[4] = 0 + 1 = 1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20241018192042.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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240410154340.png) + +再用版本一的代码是运行一下,发现结果输出是 1, 是对的。 + +![](https://file1.kamacoder.com/i/algo/20240410154940.png) + +分明刚刚输出的结果是 -2,是错误的,怎么 一样的图,这次输出的结果就对了呢? + +其实这是和示例中给出的边的顺序是有关的, + +我们按照修改后的示例再来模拟 对所有边的第一次拓展情况。 + +初始化: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240410160046.png) + +以上是对所有边 松弛一次的状态。 + +可以发现 同样的图,边的顺序不一样,使用版本一的代码 每次松弛更新的节点也是不一样的。 + +而边的顺序是随机的,是题目给我们的,所以本题我们才需要 记录上一次松弛的minDist,来保障 每一次对所有边松弛的结果。 + + +## 拓展二(本题本质) + +那么前面讲解过的 [94.城市间货物运输I](./0094.城市间货物运输I.md) 和 [95.城市间货物运输II](./0095.城市间货物运输II.md) 也是bellman_ford经典算法,也没使用 minDist_copy,怎么就没问题呢? + +> 如果没看过我上面这两篇讲解的话,建议详细学习上面两篇,再看我下面讲的区别,否则容易看不懂。 + +[94.城市间货物运输I](./0094.城市间货物运输I.md), 是没有 负权回路的,那么 多松弛多少次,对结果都没有影响。 + +求 节点1 到 节点n 的最短路径,松弛n-1 次就够了,松弛 大于 n-1次,结果也不会变。 + +那么在对所有边进行第一次松弛的时候,如果基于 本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。 + +[95.城市间货物运输II](./0095.城市间货物运输II.md) 是判断是否有 负权回路,一旦有负权回路, 对所有边松弛 n-1 次以后,在做松弛 minDist 数值一定会变,根据这一点来判断是否有负权回路。 + +所以,[95.城市间货物运输II](./0095.城市间货物运输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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240508112249.png) + +最短路显然是: + +![](https://file1.kamacoder.com/i/algo/20240508112416.png) + +最多经过2个节点,也就是3条边相连的路线:节点1 -> 节点2 -> 节点6-> 节点7 + +如果是 dijkstra 求解的话,求解过程是这样的: (下面是dijkstra的模拟过程,我精简了很多,如果看不懂,一定要先看[dijkstra朴素版精讲](./0047.参会dijkstra朴素.md)) + +初始化如图所示: + +![](https://file1.kamacoder.com/i/algo/20240130115306.png) + +找距离源点最近且没有被访问过的节点,先找节点1 + +![](https://file1.kamacoder.com/i/algo/20240130115421.png) + + +距离源点最近且没有被访问过的节点,找节点2: + +![](https://file1.kamacoder.com/i/algo/20240130121240.png) + +距离源点最近且没有被访问过的节点,找到节点3: + +![](https://file1.kamacoder.com/i/algo/20240130120434.png) + +距离源点最近且没有被访问过的节点,找到节点4: + +![](https://file1.kamacoder.com/i/algo/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有更深入的理解。 + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 基于Bellman_for一般解法解决单源最短路径问题 + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + + List graph = new ArrayList<>(); + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.add(new Edge(from, to, val)); + } + + int src = sc.nextInt(); + int dst = sc.nextInt(); + int k = sc.nextInt(); + + int[] minDist = new int[n + 1]; + int[] minDistCopy; + + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[src] = 0; + + for (int i = 0; i < k + 1; i++) { // Relax all edges k + 1 times + minDistCopy = Arrays.copyOf(minDist, n + 1); + for (Edge edge : graph) { + int from = edge.from; + int to = edge.to; + int val = edge.val; + // Use minDistCopy to calculate minDist + if (minDistCopy[from] != Integer.MAX_VALUE && minDist[to] > minDistCopy[from] + val) { + minDist[to] = minDistCopy[from] + val; + } + } + } + + // Output printing + if (minDist[dst] == Integer.MAX_VALUE) { + System.out.println("unreachable"); + } else { + System.out.println(minDist[dst]); + } + } +} + +``` + +```java +class Edge { + public int u; // 边的端点1 + public int v; // 边的端点2 + public int val; // 边的权值 + + public Edge() { + } + + public Edge(int u, int v) { + this.u = u; + this.v = v; + this.val = 0; + } + + public Edge(int u, int v, int val) { + this.u = u; + this.v = v; + this.val = val; + } +} + +/** + * SPFA算法(版本3):处理含【负权回路】的有向图的最短路径问题 + * bellman_ford(版本3) 的队列优化算法版本 + * 限定起点、终点、至多途径k个节点 + */ +public class SPFAForSSSP { + + /** + * SPFA算法 + * + * @param n 节点个数[1,n] + * @param graph 邻接表 + * @param startIdx 开始节点(源点) + */ + public static int[] spfa(int n, List> graph, int startIdx, int k) { + // 定义最大范围 + int maxVal = Integer.MAX_VALUE; + // minDist[i] 源点到节点i的最短距离 + int[] minDist = new int[n + 1]; // 有效节点编号范围:[1,n] + Arrays.fill(minDist, maxVal); // 初始化为maxVal + minDist[startIdx] = 0; // 设置源点到源点的最短路径为0 + + // 定义queue记录每一次松弛更新的节点 + Queue queue = new LinkedList<>(); + queue.offer(startIdx); // 初始化:源点开始(queue和minDist的更新是同步的) + + + // SPFA算法核心:只对上一次松弛的时候更新过的节点关联的边进行松弛操作 + while (k + 1 > 0 && !queue.isEmpty()) { // 限定松弛 k+1 次 + int curSize = queue.size(); // 记录当前队列节点个数(上一次松弛更新的节点个数,用作分层统计) + while (curSize-- > 0) { //分层控制,限定本次松弛只针对上一次松弛更新的节点,不对新增的节点做处理 + // 记录当前minDist状态,作为本次松弛的基础 + int[] minDist_copy = Arrays.copyOfRange(minDist, 0, minDist.length); + + // 取出节点 + int cur = queue.poll(); + // 获取cur节点关联的边,进行松弛操作 + List relateEdges = graph.get(cur); + for (Edge edge : relateEdges) { + int u = edge.u; // 与`cur`对照 + int v = edge.v; + int weight = edge.val; + if (minDist_copy[u] + weight < minDist[v]) { + minDist[v] = minDist_copy[u] + weight; // 更新 + // 队列同步更新(此处有一个针对队列的优化:就是如果已经存在于队列的元素不需要重复添加) + if (!queue.contains(v)) { + queue.offer(v); // 与minDist[i]同步更新,将本次更新的节点加入队列,用做下一个松弛的参考基础 + } + } + } + } + // 当次松弛结束,次数-1 + k--; + } + + // 返回minDist + return minDist; + } + + public static void main(String[] args) { + // 输入控制 + Scanner sc = new Scanner(System.in); + System.out.println("1.输入N个节点、M条边(u v weight)"); + int n = sc.nextInt(); + int m = sc.nextInt(); + + System.out.println("2.输入M条边"); + List> graph = new ArrayList<>(); // 构建邻接表 + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + while (m-- > 0) { + int u = sc.nextInt(); + int v = sc.nextInt(); + int weight = sc.nextInt(); + graph.get(u).add(new Edge(u, v, weight)); + } + + System.out.println("3.输入src dst k(起点、终点、至多途径k个点)"); + int src = sc.nextInt(); + int dst = sc.nextInt(); + int k = sc.nextInt(); + + // 调用算法 + int[] minDist = SPFAForSSSP.spfa(n, graph, src, k); + // 校验起点->终点 + if (minDist[dst] == Integer.MAX_VALUE) { + System.out.println("unreachable"); + } else { + System.out.println("最短路径:" + minDist[n]); + } + } +} +``` + + + +### Python + +Bellman-Ford方法求解单源有限最短路 + +```python +def main(): + # 輸入 + n, m = map(int, input().split()) + edges = list() + for _ in range(m): + edges.append(list(map(int, input().split() ))) + + start, end, k = map(int, input().split()) + min_dist = [float('inf') for _ in range(n + 1)] + min_dist[start] = 0 + + # 只能經過k個城市,所以從起始點到中間有(k + 1)個邊連接 + # 需要鬆弛(k + 1)次 + + for _ in range(k + 1): + update = False + min_dist_copy = min_dist.copy() + for src, desc, w in edges: + if (min_dist_copy[src] != float('inf') and + min_dist_copy[src] + w < min_dist[desc]): + min_dist[desc] = min_dist_copy[src] + w + update = True + if not update: + break + # 輸出 + if min_dist[end] == float('inf'): + print('unreachable') + else: + print(min_dist[end]) + + + +if __name__ == "__main__": + main() +``` + +SPFA方法求解单源有限最短路 + +```python +from collections import deque +from math import inf + + +def main(): + n, m = [int(i) for i in input().split()] + graph = [[] for _ in range(n+1)] + for _ in range(m): + v1, v2, val = [int(i) for i in input().split()] + graph[v1].append([v2, val]) + src, dst, k = [int(i) for i in input().split()] + min_dist = [inf for _ in range(n+1)] + min_dist[src] = 0 # 初始化起点的距离 + que = deque([src]) + + while k != -1 and que: + visited = [False for _ in range(n+1)] # 用于保证每次松弛时一个节点最多加入队列一次 + que_size = len(que) + temp_dist = min_dist.copy() # 用于记录上一次遍历的结果 + for _ in range(que_size): + cur_node = que.popleft() + for next_node, val in graph[cur_node]: + if min_dist[next_node] > temp_dist[cur_node] + val: + min_dist[next_node] = temp_dist[cur_node] + val + if not visited[next_node]: + que.append(next_node) + visited[next_node] = True + k -= 1 + + if min_dist[dst] == inf: + print("unreachable") + else: + print(min_dist[dst]) + + +if __name__ == "__main__": + main() +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" "b/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" new file mode 100644 index 0000000000..53e66ee46f --- /dev/null +++ "b/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" @@ -0,0 +1,577 @@ + +

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

+ +# 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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240424115827.png) + +同样不能完全用上初始化 和 上一层计算的结果。 + +根据这个情况再举一个例子: + +``` +5 2 +1 2 1 +2 3 10 +1 +1 3 +``` + +图: + +![](https://file1.kamacoder.com/i/algo/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 求源点到终点。 + + +## 其他语言版本 + +### Java + +- 基于三维数组的Floyd算法 + +```java +public class FloydBase { + + // public static int MAX_VAL = Integer.MAX_VALUE; + public static int MAX_VAL = 10005; // 边的最大距离是10^4(不选用Integer.MAX_VALUE是为了避免相加导致数值溢出) + + public static void main(String[] args) { + // 输入控制 + Scanner sc = new Scanner(System.in); + System.out.println("1.输入N M"); + int n = sc.nextInt(); + int m = sc.nextInt(); + + System.out.println("2.输入M条边"); + + // ① dp定义(grid[i][j][k] 节点i到节点j 可能经过节点K(k∈[1,n]))的最短路径 + int[][][] grid = new int[n + 1][n + 1][n + 1]; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + for (int k = 0; k <= n; k++) { + grid[i][j][k] = grid[j][i][k] = MAX_VAL; // 其余设置为最大值 + } + } + } + + // ② dp 推导:grid[i][j][k] = min{grid[i][k][k-1] + grid[k][j][k-1], grid[i][j][k-1]} + while (m-- > 0) { + int u = sc.nextInt(); + int v = sc.nextInt(); + int weight = sc.nextInt(); + grid[u][v][0] = grid[v][u][0] = weight; // 初始化(处理k=0的情况) ③ dp初始化 + } + + // ④ dp推导: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] = Math.min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]); + } + } + } + + System.out.println("3.输入[起点-终点]计划个数"); + int x = sc.nextInt(); + + System.out.println("4.输入每个起点src 终点dst"); + + while (x-- > 0) { + int src = sc.nextInt(); + int dst = sc.nextInt(); + // 根据floyd推导结果输出计划路径的最小距离 + if (grid[src][dst][n] == MAX_VAL) { + System.out.println("-1"); + } else { + System.out.println(grid[src][dst][n]); + } + } + } +} +``` + + + +### Python + +基于三维数组的Floyd + +```python +if __name__ == '__main__': + max_int = 10005 # 设置最大路径,因为边最大距离为10^4 + + n, m = map(int, input().split()) + + grid = [[[max_int] * (n+1) for _ in range(n+1)] for _ in range(n+1)] # 初始化三维dp数组 + + for _ in range(m): + p1, p2, w = map(int, input().split()) + grid[p1][p2][0] = w + grid[p2][p1][0] = w + + # 开始floyd + for k in range(1, n+1): + for i in range(1, n+1): + for j in range(1, n+1): + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]) + + # 输出结果 + z = int(input()) + for _ in range(z): + start, end = map(int, input().split()) + if grid[start][end][n] == max_int: + print(-1) + else: + print(grid[start][end][n]) +``` + +基于二维数组的Floyd + +```python +if __name__ == '__main__': + max_int = 10005 # 设置最大路径,因为边最大距离为10^4 + + n, m = map(int, input().split()) + + grid = [[max_int]*(n+1) for _ in range(n+1)] # 初始化二维dp数组 + + for _ in range(m): + p1, p2, val = map(int, input().split()) + grid[p1][p2] = val + grid[p2][p1] = val + + # 开始floyd + for k in range(1, n+1): + for i in range(1, n+1): + for j in range(1, n+1): + grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]) + + # 输出结果 + z = int(input()) + for _ in range(z): + start, end = map(int, input().split()) + if grid[start][end] == max_int: + print(-1) + else: + print(grid[start][end]) +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" "b/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" new file mode 100644 index 0000000000..c71981996b --- /dev/null +++ "b/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" @@ -0,0 +1,890 @@ + +

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

+ +# 98. 所有可达路径 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1170) + +【题目描述】 + +给定一个有 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://file1.kamacoder.com/i/algo/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 + + +## 插曲 + +------------- + +本题和力扣 [797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) 是一样的,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。 + +力扣是核心代码模式,把图的存储方式给大家定义好了,只需要写出深搜的核心代码就可以。 + +如果笔试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,因为不知道图应该怎么存,也不知道自己存的图如何去遍历。 + +所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。 + +-------- + + +这道题目是深度优先搜索,比较好的入门题。 + +如果对深度优先搜索还不够了解,可以先看这里:[深度优先搜索的理论基础](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://file1.kamacoder.com/i/algo/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题目,同样也重点去讲解图的存储和遍历方式,来帮大家去练习。 + +对于这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。 + +而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。 + +至于广搜理论基础,我们在下一篇在好好讲解,敬请期待! + + + +## 其他语言版本 + +### Java + +邻接矩阵写法 +```java +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class Main { + static List> result = new ArrayList<>(); // 收集符合条件的路径 + static List path = new ArrayList<>(); // 1节点到终点的路径 + + public static void dfs(int[][] graph, int x, int n) { + // 当前遍历的节点x 到达节点n + if (x == n) { // 找到符合条件的一条路径 + result.add(new ArrayList<>(path)); + return; + } + for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.add(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.remove(path.size() - 1); // 回溯,撤销本节点 + } + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + int[][] graph = new int[n + 1][n + 1]; + + for (int i = 0; i < m; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的 + graph[s][t] = 1; + } + + path.add(1); // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.isEmpty()) System.out.println(-1); + for (List pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + System.out.print(pa.get(i) + " "); + } + System.out.println(pa.get(pa.size() - 1)); + } + } +} +``` + +邻接表写法 +```java +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +public class Main { + static List> result = new ArrayList<>(); // 收集符合条件的路径 + static List path = new ArrayList<>(); // 1节点到终点的路径 + + public static void dfs(List> graph, int x, int n) { + if (x == n) { // 找到符合条件的一条路径 + result.add(new ArrayList<>(path)); + return; + } + for (int i : graph.get(x)) { // 找到 x指向的节点 + path.add(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.remove(path.size() - 1); // 回溯,撤销本节点 + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + List> graph = new ArrayList<>(n + 1); + for (int i = 0; i <= n; i++) { + graph.add(new LinkedList<>()); + } + + while (m-- > 0) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + // 使用邻接表表示 s -> t 是相连的 + graph.get(s).add(t); + } + + path.add(1); // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.isEmpty()) System.out.println(-1); + for (List pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + System.out.print(pa.get(i) + " "); + } + System.out.println(pa.get(pa.size() - 1)); + } + } +} +``` +### Python +邻接矩阵写法 +``` python +def dfs(graph, x, n, path, result): + if x == n: + result.append(path.copy()) + return + for i in range(1, n + 1): + if graph[x][i] == 1: + path.append(i) + dfs(graph, i, n, path, result) + path.pop() + +def main(): + n, m = map(int, input().split()) + graph = [[0] * (n + 1) for _ in range(n + 1)] + + for _ in range(m): + s, t = map(int, input().split()) + graph[s][t] = 1 + + result = [] + dfs(graph, 1, n, [1], result) + + if not result: + print(-1) + else: + for path in result: + print(' '.join(map(str, path))) + +if __name__ == "__main__": + main() +``` + +邻接表写法 +``` python +from collections import defaultdict + +result = [] # 收集符合条件的路径 +path = [] # 1节点到终点的路径 + +def dfs(graph, x, n): + if x == n: # 找到符合条件的一条路径 + result.append(path.copy()) + return + for i in graph[x]: # 找到 x指向的节点 + path.append(i) # 遍历到的节点加入到路径中来 + dfs(graph, i, n) # 进入下一层递归 + path.pop() # 回溯,撤销本节点 + +def main(): + n, m = map(int, input().split()) + + graph = defaultdict(list) # 邻接表 + for _ in range(m): + s, t = map(int, input().split()) + graph[s].append(t) + + path.append(1) # 无论什么路径已经是从1节点出发 + dfs(graph, 1, n) # 开始遍历 + + # 输出结果 + if not result: + print(-1) + for pa in result: + print(' '.join(map(str, pa))) + +if __name__ == "__main__": + main() +``` +### Go + +#### 邻接矩阵写法 +```go +package main + +import ( + "fmt" +) + +var result [][]int // 收集符合条件的路径 +var path []int // 1节点到终点的路径 + +func dfs(graph [][]int, x, n int) { + // 当前遍历的节点x 到达节点n + if x == n { // 找到符合条件的一条路径 + temp := make([]int, len(path)) + copy(temp, path) + result = append(result, temp) + return + } + for i := 1; i <= n; i++ { // 遍历节点x链接的所有节点 + if graph[x][i] == 1 { // 找到 x链接的节点 + path = append(path, i) // 遍历到的节点加入到路径中来 + dfs(graph, i, n) // 进入下一层递归 + path = path[:len(path)-1] // 回溯,撤销本节点 + } + } +} + +func main() { + var n, m int + fmt.Scanf("%d %d", &n, &m) + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + graph := make([][]int, n+1) + for i := range graph { + graph[i] = make([]int, n+1) + } + + for i := 0; i < m; i++ { + var s, t int + fmt.Scanf("%d %d", &s, &t) + // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的 + graph[s][t] = 1 + } + + path = append(path, 1) // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n) // 开始遍历 + + // 输出结果 + if len(result) == 0 { + fmt.Println(-1) + } else { + for _, pa := range result { + for i := 0; i < len(pa)-1; i++ { + fmt.Print(pa[i], " ") + } + fmt.Println(pa[len(pa)-1]) + } + } +} +``` + +#### 邻接表写法 +```go +package main + +import ( + "fmt" +) + +var result [][]int +var path []int + +func dfs(graph [][]int, x, n int) { + if x == n { + temp := make([]int, len(path)) + copy(temp, path) + result = append(result, temp) + return + } + for _, i := range graph[x] { + path = append(path, i) + dfs(graph, i, n) + path = path[:len(path)-1] + } +} + +func main() { + var n, m int + fmt.Scanf("%d %d", &n, &m) + + graph := make([][]int, n+1) + for i := 0; i <= n; i++ { + graph[i] = make([]int, 0) + } + + for m > 0 { + var s, t int + fmt.Scanf("%d %d", &s, &t) + graph[s] = append(graph[s], t) + m-- + } + + path = append(path, 1) + dfs(graph, 1, n) + + if len(result) == 0 { + fmt.Println(-1) + } else { + for _, pa := range result { + for i := 0; i < len(pa)-1; i++ { + fmt.Print(pa[i], " ") + } + fmt.Println(pa[len(pa)-1]) + } + } +} +``` + +### Rust + +### JavaScript + +#### 邻接矩阵写法 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async ()=>(await iter.next()).value; + + +let graph; +let N, M; +// 收集符合条件的路径 +let result = []; +// 1节点到终点的路径 +let path = []; + +// 创建邻接矩阵,初始化邻接矩阵 +async function initGraph(){ + let line; + + line = await readline(); + [N, M] = line.split(' ').map(i => parseInt(i)) + graph = new Array(N + 1).fill(0).map(() => new Array(N + 1).fill(0)) + + while(M--){ + line = await readline() + const strArr = line ? line.split(' ').map(i => parseInt(i)) : undefined + strArr ? graph[strArr[0]][strArr[1]] = 1 : null + } +}; + +// 深度搜索 +function dfs(graph, x, n){ + // 当前遍历节点为x, 到达节点为n + if(x == n){ + result.push([...path]) + return + } + for(let i = 1 ; i <= n ; i++){ + if(graph[x][i] == 1){ + path.push(i) + dfs(graph, i, n ) + path.pop(i) + } + } +}; + +(async function(){ + // 创建邻接矩阵,初始化邻接矩阵 + await initGraph(); + + // 从节点1开始深度搜索 + path.push(1); + + // 深度搜索 + dfs(graph, 1, N ); + + // 输出 + if(result.length > 0){ + result.forEach(i => { + console.log(i.join(' ')) + }) + }else{ + console.log(-1) + } + +})(); + +``` + + + +#### 邻接表写法 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph; +let N, M; + +// 收集符合条件的路径 +let result = []; +// 1节点到终点的路径 +let path = []; + +// 创建邻接表,初始化邻接表 +async function initGraph() { + let line; + line = await readline(); + [N, M] = line.split(' ').map(i => parseInt(i)) + graph = new Array(N + 1).fill(0).map(() => new Array()) + + while (line = await readline()) { + const strArr = line.split(' ').map(i => parseInt(i)) + strArr ? graph[strArr[0]].push(strArr[1]) : null + } +}; + +// 深度搜索 +async function dfs(graph, x, n) { + // 当前遍历节点为x, 到达节点为n + if (x == n) { + result.push([...path]) + return + } + + graph[x].forEach(i => { + path.push(i) + dfs(graph, i, n) + path.pop(i) + }) +}; + +(async function () { + // 创建邻接表,初始化邻接表 + await initGraph(); + + // 从节点1开始深度搜索 + path.push(1); + + // 深度搜索 + dfs(graph, 1, N); + + // 输出 + if (result.length > 0) { + result.forEach(i => { + console.log(i.join(' ')) + }) + } else { + console.log(-1) + } +})(); +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + + + + + +
diff --git "a/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" new file mode 100644 index 0000000000..93c1fe41fa --- /dev/null +++ "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" @@ -0,0 +1,560 @@ + +

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

+ +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + +题目描述: + +给定一个由 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://file1.kamacoder.com/i/algo/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路:遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +再遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 广度优先搜索 + +如果不熟悉广搜,建议先看 [广搜理论基础](./图论广搜理论基础.md)。 + +不少同学用广搜做这道题目的时候,超时了。 这里有一个广搜中很重要的细节: + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +很多同学可能感觉这有区别吗? + +如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。 + +![](https://file1.kamacoder.com/i/algo/20250124094043.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; +} + +``` + + +## 其他语言版本 + +### Java + +```java +import java.util.*; + +public class Main { + public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};//下右上左逆时针遍历 + + public static void bfs(int[][] grid, boolean[][] visited, int x, int y) { + Queue queue = new LinkedList();//定义坐标队列,没有现成的pair类,在下面自定义了 + queue.add(new pair(x, y)); + visited[x][y] = true;//遇到入队直接标记为优先, + // 否则出队时才标记的话会导致重复访问,比如下方节点会在右下顺序的时候被第二次访问入队 + while (!queue.isEmpty()) { + int curX = queue.peek().first; + int curY = queue.poll().second;//当前横纵坐标 + for (int i = 0; i < 4; i++) { + //顺时针遍历新节点next,下面记录坐标 + int nextX = curX + dir[i][0]; + int nextY = curY + dir[i][1]; + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) { + continue; + }//去除越界部分 + if (!visited[nextX][nextY] && grid[nextX][nextY] == 1) { + queue.add(new pair(nextX, nextY)); + visited[nextX][nextY] = true;//逻辑同上 + } + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + int[][] grid = new int[m][n]; + boolean[][] visited = new boolean[m][n]; + int ans = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = sc.nextInt(); + } + } + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + ans++; + bfs(grid, visited, i, j); + } + } + } + System.out.println(ans); + } +} +``` + + +### Python + +```python + +from collections import deque +directions = [[0, 1], [1, 0], [0, -1], [-1, 0]] +def bfs(grid, visited, x, y): + que = deque([]) + que.append([x,y]) + visited[x][y] = True + while que: + cur_x, cur_y = que.popleft() + for i, j in directions: + next_x = cur_x + i + next_y = cur_y + j + if next_y < 0 or next_x < 0 or next_x >= len(grid) or next_y >= len(grid[0]): + continue + if not visited[next_x][next_y] and grid[next_x][next_y] == 1: + visited[next_x][next_y] = True + que.append([next_x, next_y]) + + +def main(): + n, m = map(int, input().split()) + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + visited = [[False] * m for _ in range(n)] + res = 0 + for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + bfs(grid, visited, i, j) + print(res) + +if __name__ == "__main__": + main() + + + +``` + + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +var dir = [4][2]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func dfs(grid [][]int, visited [][]bool, x, y int) { + for i := 0; i < 4; i++ { + nextx := x + dir[i][0] + nexty := y + dir[i][1] + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + if !visited[nextx][nexty] && grid[nextx][nexty] == 1 { // 没有访问过的 同时 是陆地的 + visited[nextx][nexty] = true + dfs(grid, visited, nextx, nexty) + } + } +} + +func main() { + reader := bufio.NewReader(os.Stdin) + var n, m int + fmt.Scanf("%d %d", &n, &m) + + grid := make([][]int, n) + for i := 0; i < n; i++ { + grid[i] = make([]int, m) + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + elements := strings.Split(line, " ") + for j := 0; j < m; j++ { + grid[i][j], _ = strconv.Atoi(elements[j]) + } + } + + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + + result := 0 + for i := 0; i < n; i++ { + for 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 + } + } + } + + fmt.Println(result) +} + + +``` + + + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph +let N, M +let visited +let result = 0 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x, y)开始广度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + visited[x][y] = true //只要加入队列就立刻标记为访问过 + + while (queue.length) { + let [x, y] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计岛屿数 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { + // 遇到没访问过的陆地,+1 + result++ + + // 广度优先遍历,将相邻陆地标记为已访问 + bfs(graph, visited, i, j) + } + } + } + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +```PHP + += count($grid) || $nexty < 0 || $nexty >= count($grid[0])) { + continue; // 越界了,直接跳过 + } + if (!$visited[$nextx][$nexty] && $grid[$nextx][$nexty] == 1) { // 没有访问过的 同时 是陆地的 + $visited[$nextx][$nexty] = true; + dfs($grid, $visited, $nextx, $nexty); + } + } +} + +function main() { + fscanf(STDIN, "%d %d", $n, $m); + $grid = []; + for ($i = 0; $i < $n; $i++) { + $grid[$i] = array_map('intval', explode(' ', trim(fgets(STDIN)))); + } + + $visited = array_fill(0, $n, array_fill(0, $m, false)); + + $result = 0; + for ($i = 0; $i < $n; $i++) { + for ($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 + } + } + } + + echo $result . PHP_EOL; +} + +main(); +?> + + +``` + + +### Swift + +### Scala +```scala +import scala.collection.mutable.Queue +import util.control.Breaks._ + +// Dev on LeetCode: https://leetcode.cn/problems/number-of-islands/description/ +object Solution { + def numIslands(grid: Array[Array[Char]]): Int = { + val row = grid.length + val col = grid(0).length + val dir = List((-1,0), (0,-1), (1,0), (0,1)) // 四个方向 + var visited = Array.fill(row)(Array.fill(col)(false)) + var counter = 0 + var que = Queue.empty[Tuple2[Int, Int]] + + (0 until row).map{ r => + (0 until col).map{ c => + breakable { + if (!visited(r)(c) && grid(r)(c) == '1') { + que.enqueue((r, c)) + visited(r)(c) // 只要加入队列,立刻标记 + } else break // 不是岛屿不进入queue,也不记录 + + while (!que.isEmpty) { + val cur = que.head + que.dequeue() + val x = cur(0) + val y = cur(1) + dir.map{ d => + val nextX = x + d(0) + val nextY = y + d(1) + breakable { + // 越界就跳过 + if (nextX < 0 || nextX >= row || nextY < 0 || nextY >= col) break + if (!visited(nextX)(nextY) && grid(nextX)(nextY) == '1') { + visited(nextX)(nextY) = true // 只要加入队列,立刻标记 + que.enqueue((nextX, nextY)) + } + } + } + } + counter = counter + 1 // 找完一个岛屿后记录一下 + } + } + } + + counter + } +} +``` + +### C# + + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" new file mode 100644 index 0000000000..3c54278af0 --- /dev/null +++ "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" @@ -0,0 +1,462 @@ + +

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

+ +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + + +题目描述: + +给定一个由 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://file1.kamacoder.com/i/algo/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/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写法,虽然是模板题,但依然有很多注意的点,敬请期待! + + +## 其他语言版本 + +### Java +```java +import java.util.Scanner; + +public class Main { + public static int[][] dir ={{0,1},{1,0},{-1,0},{0,-1}}; + public static void dfs(boolean[][] visited,int x,int y ,int [][]grid) + { + for (int i = 0; i < 4; i++) { + int nextX=x+dir[i][0]; + int nextY=y+dir[i][1]; + if(nextY<0||nextX<0||nextX>= grid.length||nextY>=grid[0].length) + continue; + if(!visited[nextX][nextY]&&grid[nextX][nextY]==1) + { + visited[nextX][nextY]=true; + dfs(visited,nextX,nextY,grid); + } + } + } + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m= sc.nextInt(); + int n = sc.nextInt(); + int[][] grid = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j]=sc.nextInt(); + } + } + boolean[][]visited =new boolean[m][n]; + int ans = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if(!visited[i][j]&&grid[i][j]==1) + { + ans++; + visited[i][j]=true; + dfs(visited,i,j,grid); + } + } + } + System.out.println(ans); + } +} + +``` +### Python + +版本一 + +```python +direction = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向:上、右、下、左 + + +def dfs(grid, visited, x, y): + """ + 对一块陆地进行深度优先遍历并标记 + """ + for i, j in direction: + next_x = x + i + next_y = y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + # 未访问的陆地,标记并调用深度优先搜索 + if not visited[next_x][next_y] and grid[next_x][next_y] == 1: + visited[next_x][next_y] = True + dfs(grid, visited, next_x, next_y) + + +if __name__ == '__main__': + # 版本一 + n, m = map(int, input().split()) + + # 邻接矩阵 + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + + # 访问表 + visited = [[False] * m for _ in range(n)] + + res = 0 + for i in range(n): + for j in range(m): + # 判断:如果当前节点是陆地,res+1并标记访问该节点,使用深度搜索标记相邻陆地。 + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + visited[i][j] = True + dfs(grid, visited, i, j) + + print(res) +``` + +版本二 + +```python +direction = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向:上、右、下、左 + + +def dfs(grid, visited, x, y): + """ + 对一块陆地进行深度优先遍历并标记 + """ + # 与版本一的差别,在调用前增加判断终止条件 + if visited[x][y] or grid[x][y] == 0: + return + visited[x][y] = True + + for i, j in direction: + next_x = x + i + next_y = y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + # 由于判断条件放在了方法首部,此处直接调用dfs方法 + dfs(grid, visited, next_x, next_y) + + +if __name__ == '__main__': + # 版本二 + n, m = map(int, input().split()) + + # 邻接矩阵 + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + + # 访问表 + visited = [[False] * m for _ in range(n)] + + res = 0 + for i in range(n): + for j in range(m): + # 判断:如果当前节点是陆地,res+1并标记访问该节点,使用深度搜索标记相邻陆地。 + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + dfs(grid, visited, i, j) + + print(res) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph +let N, M +let visited +let result = 0 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从节点x,y开始深度优先遍历 + * @param {*} graph 是地图,也就是一个二维数组 + * @param {*} visited 标记访问过的节点,不要重复访问 + * @param {*} x 表示开始搜索节点的下标 + * @param {*} y 表示开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + for (let i = 0; i < 4; i++) { + const nextx = x + dir[i][0] + const nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (!visited[nextx][nexty] && graph[nextx][nexty] === 1) { + visited[nextx][nexty] = true + dfs(graph, visited, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计岛屿数 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { + // 标记已访问 + visited[i][j] = true + + // 遇到没访问过的陆地,+1 + result++ + + // 深度优先遍历,将相邻陆地标记为已访问 + dfs(graph, visited, i, j) + } + } + } + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala +```scala +import util.control.Breaks._ + +object Solution { + val dir = List((-1,0), (0,-1), (1,0), (0,1)) // 四个方向 + + def dfs(grid: Array[Array[Char]], visited: Array[Array[Boolean]], row: Int, col: Int): Unit = { + (0 until 4).map { x => + val nextR = row + dir(x)(0) + val nextC = col + dir(x)(1) + breakable { + if(nextR < 0 || nextR >= grid.length || nextC < 0 || nextC >= grid(0).length) break + if (!visited(nextR)(nextC) && grid(nextR)(nextC) == '1') { + visited(nextR)(nextC) = true // 经过就记录 + dfs(grid, visited, nextR, nextC) + } + } + } + } + + def numIslands(grid: Array[Array[Char]]): Int = { + val row = grid.length + val col = grid(0).length + var visited = Array.fill(row)(Array.fill(col)(false)) + var counter = 0 + + (0 until row).map{ r => + (0 until col).map{ c => + if (!visited(r)(c) && grid(r)(c) == '1') { + visited(r)(c) = true // 经过就记录 + dfs(grid, visited, r, c) + counter += 1 + } + } + } + + counter + } +} +``` + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" "b/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" new file mode 100644 index 0000000000..2ae1f452e0 --- /dev/null +++ "b/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" @@ -0,0 +1,893 @@ + +

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

+ +# 100. 岛屿的最大面积 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1172) + +题目描述 + +给定一个由 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 +``` + +输出示例 + +4 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240517103410.png) + +样例输入中,岛屿的最大面积为 4。 + +数据范围: + +* 1 <= M, N <= 50。 + + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题目也是 dfs bfs基础类题目,就是搜索每个岛屿上“1”的数量,然后取一个最大的。 + +本题思路上比较简单,难点其实都是 dfs 和 bfs的理论基础,关于理论基础我在这里都有详细讲解 : + +* [DFS理论基础](https://programmercarl.com/kamacoder/图论深搜理论基础.html) +* [BFS理论基础](https://programmercarl.com/kamacoder/图论广搜理论基础.html) + +### DFS + +很多同学写dfs其实也是凭感觉来的,有的时候dfs函数中写终止条件才能过,有的时候 dfs函数不写终止添加也能过! + +这里其实涉及到dfs的两种写法。 + +写法一,dfs处理当前节点的相邻节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地 + +```CPP +// 版本一 +#include +#include +using namespace std; +int count; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(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; + count++; + 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) { + count = 1; // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + visited[i][j] = true; + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + cout << result << endl; + +} +``` + +写法二,dfs处理当前节点,即在主函数遇到岛屿就计数为0,dfs处理接下来的全部陆地 + +dfs +```CPP +// 版本二 +#include +#include +using namespace std; + +int count; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + count++; + 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 = vector>(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) { + count = 0; // 因为dfs处理当前节点,所以遇到陆地计数为0,进dfs之后在开始从1计数 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + cout << result << endl; +} +``` + +大家通过注释可以发现,两种写法,版本一,在主函数遇到陆地就计数为1,接下来的相邻陆地都在dfs中计算。 + +版本二 在主函数遇到陆地 计数为0,也就是不计数,陆地数量都去dfs里做计算。 + +这也是为什么大家看了很多 dfs的写法 ,发现写法怎么都不一样呢? 其实这就是根本原因。 + + +### BFS + +关于广度优先搜索,如果大家还不了解的话,看这里:[广度优先搜索精讲](./图论广搜理论基础.md) + +本题BFS代码如下: + +```CPP +class Solution { +private: + int count; + 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); + que.push(y); + visited[x][y] = true; // 加入队列就意味节点是陆地可到达的点 + count++; + while(!que.empty()) { + int xx = que.front();que.pop(); + int yy = que.front();que.pop(); + for (int i = 0 ;i < 4; i++) { + int nextx = xx + dir[i][0]; + int nexty = yy + 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; + count++; + que.push(nextx); + que.push(nexty); + } + } + } + } + +public: + int maxAreaOfIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(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) { + count = 0; + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + return result; + } +}; + +``` + + +## 其他语言版本 + +### Java + +```java +import java.util.*; +import java.math.*; + +/** + * DFS版 + */ +public class Main{ + + static final int[][] dir={{0,1},{1,0},{0,-1},{-1,0}}; + static int result=0; + static int count=0; + + public static void main(String[] args){ + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] map = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + map[i][j]=scanner.nextInt(); + } + } + boolean[][] visited = new boolean[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if(!visited[i][j]&&map[i][j]==1){ + count=0; + dfs(map,visited,i,j); + result= Math.max(count, result); + } + } + } + System.out.println(result); + } + + static void dfs(int[][] map,boolean[][] visited,int x,int y){ + count++; + 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||nextY<0 + ||nextX>=map.length||nextY>=map[0].length + ||visited[nextX][nextY]||map[nextX][nextY]==0)continue; + + dfs(map,visited,nextX,nextY); + } + } +} +``` + +```java +import java.util.*; +import java.math.*; + +/** + * BFS版 + */ +public class Main { + static class Node { + int x; + int y; + + public Node(int x, int y) { + this.x = x; + this.y = y; + } + } + + static final int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + static int result = 0; + static int count = 0; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] map = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + map[i][j] = scanner.nextInt(); + } + } + boolean[][] visited = new boolean[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && map[i][j] == 1) { + count = 0; + bfs(map, visited, i, j); + result = Math.max(count, result); + + } + } + } + System.out.println(result); + } + + static void bfs(int[][] map, boolean[][] visited, int x, int y) { + Queue q = new LinkedList<>(); + q.add(new Node(x, y)); + visited[x][y] = true; + count++; + while (!q.isEmpty()) { + Node node = q.remove(); + for (int i = 0; i < 4; i++) { + int nextX = node.x + dir[i][0]; + int nextY = node.y + dir[i][1]; + if (nextX < 0 || nextY < 0 || nextX >= map.length || nextY >= map[0].length || visited[nextX][nextY] || map[nextX][nextY] == 0) + continue; + q.add(new Node(nextX, nextY)); + visited[nextX][nextY] = true; + count++; + } + } + } +} + +``` +### Python + +DFS + +```python +# 四个方向 +position = [[0, 1], [1, 0], [0, -1], [-1, 0]] +count = 0 + + +def dfs(grid, visited, x, y): + """ + 深度优先搜索,对一整块陆地进行标记 + """ + global count # 定义全局变量,便于传递count值 + for i, j in position: + cur_x = x + i + cur_y = y + j + # 下标越界,跳过 + if cur_x < 0 or cur_x >= len(grid) or cur_y < 0 or cur_y >= len(grid[0]): + continue + if not visited[cur_x][cur_y] and grid[cur_x][cur_y] == 1: + visited[cur_x][cur_y] = True + count += 1 + dfs(grid, visited, cur_x, cur_y) + + +n, m = map(int, input().split()) +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) +# 访问表 +visited = [[False] * m for _ in range(n)] + +result = 0 # 记录最终结果 +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + count = 1 + visited[i][j] = True + dfs(grid, visited, i, j) + result = max(count, result) + +print(result) +``` + +BFS + +```python +from collections import deque + +position = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向 +count = 0 + + +def bfs(grid, visited, x, y): + """ + 广度优先搜索对陆地进行标记 + """ + global count # 声明全局变量 + que = deque() + que.append([x, y]) + while que: + cur_x, cur_y = que.popleft() + for i, j in position: + next_x = cur_x + i + next_y = cur_y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + if grid[next_x][next_y] == 1 and not visited[next_x][next_y]: + visited[next_x][next_y] = True + count += 1 + que.append([next_x, next_y]) + + +n, m = map(int, input().split()) +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) +visited = [[False] * m for _ in range(n)] # 访问表 + +result = 0 # 记录最终结果 +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + count = 1 + visited[i][j] = True + bfs(grid, visited, i, j) + res = max(result, count) + +print(result) +``` + +### Go + +``` go + +package main + +import ( + "fmt" +) + +var count int +var dir = [][]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func dfs(grid [][]int, visited [][]bool, x, y int) { + for i := 0; i < 4; i++ { + nextx := x + dir[i][0] + nexty := y + dir[i][1] + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + if !visited[nextx][nexty] && grid[nextx][nexty] == 1 { // 没有访问过的 同时 是陆地的 + visited[nextx][nexty] = true + count++ + dfs(grid, visited, nextx, nexty) + } + } +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + grid := make([][]int, n) + for i := 0; i < n; i++ { + grid[i] = make([]int, m) + for j := 0; j < m; j++ { + fmt.Scan(&grid[i][j]) + } + } + + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + + result := 0 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if !visited[i][j] && grid[i][j] == 1 { + count = 1 // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + visited[i][j] = true + dfs(grid, visited, i, j) + if count > result { + result = count + } + } + } + } + + fmt.Println(result) +} + + + +``` + + + +### Rust +DFS + +``` rust +use std::io; +use std::cmp; + +// 定义四个方向 +const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + +fn dfs(grid: &Vec>, visited: &mut Vec>, x: usize, y: usize, count: &mut i32) { + if visited[x][y] || grid[x][y] == 0 { + return; // 终止条件:已访问或者遇到海水 + } + visited[x][y] = true; // 标记已访问 + *count += 1; + + for &(dx, dy) in DIRECTIONS.iter() { + let new_x = x as i32 + dx; + let new_y = y as i32 + dy; + + // 检查边界条件 + if new_x >= 0 && new_x < grid.len() as i32 && new_y >= 0 && new_y < grid[0].len() as i32 { + dfs(grid, visited, new_x as usize, new_y as usize, count); + } + } +} + +fn main() { + let mut input = String::new(); + + // 读取 n 和 m + io::stdin().read_line(&mut input); + let dims: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + let (n, m) = (dims[0], dims[1]); + + // 读取 grid + let mut grid = vec![]; + for _ in 0..n { + input.clear(); + io::stdin().read_line(&mut input); + let row: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + grid.push(row); + } + + // 初始化访问记录 + let mut visited = vec![vec![false; m]; n]; + let mut result = 0; + + // 遍历所有格子 + for i in 0..n { + for j in 0..m { + if !visited[i][j] && grid[i][j] == 1 { + let mut count = 0; + dfs(&grid, &mut visited, i, j, &mut count); + result = cmp::max(result, count); + } + } + } + + // 输出结果 + println!("{}", result); +} + +``` +BFS +```rust +use std::io; +use std::collections::VecDeque; + +// 定义四个方向 +const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + +fn bfs(grid: &Vec>, visited: &mut Vec>, x: usize, y: usize) -> i32 { + let mut count = 0; + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + visited[x][y] = true; // 标记已访问 + + while let Some((cur_x, cur_y)) = queue.pop_front() { + count += 1; // 增加计数 + + for &(dx, dy) in DIRECTIONS.iter() { + let new_x = cur_x as i32 + dx; + let new_y = cur_y as i32 + dy; + + // 检查边界条件 + if new_x >= 0 && new_x < grid.len() as i32 && new_y >= 0 && new_y < grid[0].len() as i32 { + let new_x_usize = new_x as usize; + let new_y_usize = new_y as usize; + + // 如果未访问且是陆地,加入队列 + if !visited[new_x_usize][new_y_usize] && grid[new_x_usize][new_y_usize] == 1 { + visited[new_x_usize][new_y_usize] = true; // 标记已访问 + queue.push_back((new_x_usize, new_y_usize)); + } + } + } + } + + count +} + +fn main() { + let mut input = String::new(); + + // 读取 n 和 m + io::stdin().read_line(&mut input).expect("Failed to read line"); + let dims: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + let (n, m) = (dims[0], dims[1]); + + // 读取 grid + let mut grid = vec![]; + for _ in 0..n { + input.clear(); + io::stdin().read_line(&mut input).expect("Failed to read line"); + let row: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + grid.push(row); + } + + // 初始化访问记录 + let mut visited = vec![vec![false; m]; n]; + let mut result = 0; + + // 遍历所有格子 + for i in 0..n { + for j in 0..m { + if !visited[i][j] && grid[i][j] == 1 { + let count = bfs(&grid, &mut visited, i, j); + result = result.max(count); + } + } + } + + // 输出结果 + println!("{}", result); +} + +``` + +### JavaScript + +```javascript +// 广搜版 + +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点 +let result = 0 // 最大岛屿面积 +let count = 0 // 岛屿内节点数 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x, y)开始广度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + count++ + visited[x][y] = true //只要加入队列就立刻标记为访问过 + + while (queue.length) { + let [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + queue.push([nextx, nexty]) + count++ + visited[nextx][nexty] = true + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计最大岛屿面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { //遇到没有访问过的陆地 + // 重新计算面积 + count = 0 + + // 广度优先遍历,统计岛屿内节点数,并将岛屿标记为已访问 + bfs(graph, visited, i, j) + + // 更新最大岛屿面积 + result = Math.max(result, count) + } + } + } + console.log(result); +})() +``` + +```javascript + +// 深搜版 + +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点 +let result = 0 // 最大岛屿面积 +let count = 0 // 岛屿内节点数 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从(x, y)开始深度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + count++ + visited[nextx][nexty] = true + dfs(graph, visited, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计最大岛屿面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { //遇到没有访问过的陆地 + // 重新计算面积 + count = 1 + visited[i][j] = true + + // 深度优先遍历,统计岛屿内节点数,并将岛屿标记为已访问 + dfs(graph, visited, i, j) + + // 更新最大岛屿面积 + result = Math.max(result, count) + } + } + } + console.log(result); +})() +``` + +### TypeScript + +### PhP + +``` php + += count($grid) || $nexty < 0 || $nexty >= count($grid[0])) continue; // 越界了,直接跳过 + if (!$visited[$nextx][$nexty] && $grid[$nextx][$nexty] == 1) { // 没有访问过的 同时 是陆地的 + $visited[$nextx][$nexty] = true; + $count++; + dfs($grid, $visited, $nextx, $nexty, $count, $dir); + } + } +} + +// Main function +function main() { + $input = trim(fgets(STDIN)); + list($n, $m) = explode(' ', $input); + + $grid = []; + for ($i = 0; $i < $n; $i++) { + $input = trim(fgets(STDIN)); + $grid[] = array_map('intval', explode(' ', $input)); + } + + $visited = []; + for ($i = 0; $i < $n; $i++) { + $visited[] = array_fill(0, $m, false); + } + + $result = 0; + $count = 0; + $dir = [[0, 1], [1, 0], [-1, 0], [0, -1]]; // 四个方向 + + for ($i = 0; $i < $n; $i++) { + for ($j = 0; $j < $m; $j++) { + if (!$visited[$i][$j] && $grid[$i][$j] == 1) { + $count = 1; // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + $visited[$i][$j] = true; + dfs($grid, $visited, $i, $j, $count, $dir); // 将与其链接的陆地都标记上 true + $result = max($result, $count); + } + } + } + + echo $result . "\n"; +} + +main(); + +?> + + +``` + + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" "b/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" new file mode 100644 index 0000000000..c883100724 --- /dev/null +++ "b/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" @@ -0,0 +1,703 @@ + +

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

+ +# 101. 孤岛的总面积 + +[卡码网:101. 孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173) + +题目描述 + +给定一个由 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 +``` + + +输出示例: + +1 + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517105557.png) + +在矩阵中心部分的岛屿,因为没有任何一个单元格接触到矩阵边缘,所以该岛屿属于孤岛,总面积为 1。 + + +数据范围: + +1 <= M, N <= 50。 + +## 思路 + +本题使用dfs,bfs,并查集都是可以的。 + +本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图 统计此时还剩下的陆地就可以了。 + +如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色, + +![](https://file1.kamacoder.com/i/algo/20220830104632.png) + +在遇到地图周边陆地的时候,将1都变为0,此时地图为这样: + +![](https://file1.kamacoder.com/i/algo/20220830104651.png) + +然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。 + +如果对深搜或者广搜不够了解,建议先看这里:[深度优先搜索精讲](./图论深搜理论基础.md),[广度优先搜索精讲](./图论广搜理论基础.md)。 + + +采用深度优先搜索的代码如下: + +```CPP +#include +#include +using namespace std; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 +void dfs(vector>& grid, int x, int y) { + grid[x][y] = 0; + 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 (grid[nextx][nexty] == 0) continue; + + dfs (grid, nextx, nexty); + } + return; +} + +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]; + } + } + + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + int count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) count++; + } + } + cout << count << endl; +} +``` + + +采用广度优先搜索的代码如下: + +```CPP +#include +#include +#include +using namespace std; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, int x, int y) { + queue> que; + que.push({x, y}); + grid[x][y] = 0; // 只要加入队列,立刻标记 + 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 (grid[nextx][nexty] == 1) { + que.push({nextx, nexty}); + grid[nextx][nexty] = 0; // 只要加入队列立刻标记 + } + } + } +} + +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]; + } + } + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) bfs(grid, i, 0); + if (grid[i][m - 1] == 1) bfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) bfs(grid, 0, j); + if (grid[n - 1][j] == 1) bfs(grid, n - 1, j); + } + int count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) count++; + } + } + + cout << count << endl; +} + +``` + + +## 其他语言版本 + +### Java + +``` java + +import java.util.*; + +public class Main { + private static int count = 0; + private static final int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; // 四个方向 + + private static void bfs(int[][] grid, int x, int y) { + Queue que = new LinkedList<>(); + que.add(new int[]{x, y}); + grid[x][y] = 0; // 只要加入队列,立刻标记 + count++; + while (!que.isEmpty()) { + int[] cur = que.poll(); + int curx = cur[0]; + int cury = cur[1]; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.length || nexty < 0 || nexty >= grid[0].length) continue; // 越界了,直接跳过 + if (grid[nextx][nexty] == 1) { + que.add(new int[]{nextx, nexty}); + count++; + grid[nextx][nexty] = 0; // 只要加入队列立刻标记 + } + } + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] grid = new int[n][m]; + + // 读取网格 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + // 从左侧边,和右侧边向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) bfs(grid, i, 0); + if (grid[i][m - 1] == 1) bfs(grid, i, m - 1); + } + + // 从上边和下边向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) bfs(grid, 0, j); + if (grid[n - 1][j] == 1) bfs(grid, n - 1, j); + } + + count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) bfs(grid, i, j); + } + } + + System.out.println(count); + } +} + + + +``` + + +### Python +#### 深搜版 +```python +position = [[1, 0], [0, 1], [-1, 0], [0, -1]] +count = 0 + +def dfs(grid, x, y): + global count + grid[x][y] = 0 + count += 1 + for i, j in position: + next_x = x + i + next_y = y + j + if next_x < 0 or next_y < 0 or next_x >= len(grid) or next_y >= len(grid[0]): + continue + if grid[next_x][next_y] == 1: + dfs(grid, next_x, next_y) + +n, m = map(int, input().split()) + +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) + +# 清除边界上的连通分量 +for i in range(n): + if grid[i][0] == 1: + dfs(grid, i, 0) + if grid[i][m - 1] == 1: + dfs(grid, i, m - 1) + +for j in range(m): + if grid[0][j] == 1: + dfs(grid, 0, j) + if grid[n - 1][j] == 1: + dfs(grid, n - 1, j) + +count = 0 # 将count重置为0 +# 统计内部所有剩余的连通分量 +for i in range(n): + for j in range(m): + if grid[i][j] == 1: + dfs(grid, i, j) + +print(count) +``` + +#### 广搜版 +```python +from collections import deque + +# 处理输入 +n, m = list(map(int, input().split())) +g = [] +for _ in range(n): + row = list(map(int, input().split())) + g.append(row) + +# 定义四个方向、孤岛面积(遍历完边缘后会被重置) +directions = [[0,1], [1,0], [-1,0], [0,-1]] +count = 0 + +# 广搜 +def bfs(r, c): + global count + q = deque() + q.append((r, c)) + g[r][c] = 0 + count += 1 + + while q: + r, c = q.popleft() + for di in directions: + next_r = r + di[0] + next_c = c + di[1] + if next_c < 0 or next_c >= m or next_r < 0 or next_r >= n: + continue + if g[next_r][next_c] == 1: + q.append((next_r, next_c)) + g[next_r][next_c] = 0 + count += 1 + + +for i in range(n): + if g[i][0] == 1: + bfs(i, 0) + if g[i][m-1] == 1: + bfs(i, m-1) + +for i in range(m): + if g[0][i] == 1: + bfs(0, i) + if g[n-1][i] == 1: + bfs(n-1, i) + +count = 0 +for i in range(n): + for j in range(m): + if g[i][j] == 1: + bfs(i, j) + +print(count) +``` + +```python +direction = [[1, 0], [-1, 0], [0, 1], [0, -1]] +result = 0 + +# 深度搜尋 +def dfs(grid, y, x): + grid[y][x] = 0 + global result + result += 1 + + for i, j in direction: + next_x = x + j + next_y = y + i + if (next_x < 0 or next_y < 0 or + next_x >= len(grid[0]) or next_y >= len(grid) + ): + continue + if grid[next_y][next_x] == 1 and not visited[next_y][next_x]: + visited[next_y][next_x] = True + dfs(grid, next_y, next_x) + + +# 讀取輸入值 +n, m = map(int, input().split()) +grid = [] +visited = [[False] * m for _ in range(n)] + +for i in range(n): + grid.append(list(map(int, input().split()))) + +# 處理邊界 +for j in range(m): + # 上邊界 + if grid[0][j] == 1 and not visited[0][j]: + visited[0][j] = True + dfs(grid, 0, j) + # 下邊界 + if grid[n - 1][j] == 1 and not visited[n - 1][j]: + visited[n - 1][j] = True + dfs(grid, n - 1, j) + +for i in range(n): + # 左邊界 + if grid[i][0] == 1 and not visited[i][0]: + visited[i][0] = True + dfs(grid, i, 0) + # 右邊界 + if grid[i][m - 1] == 1 and not visited[i][m - 1]: + visited[i][m - 1] = True + dfs(grid, i, m - 1) + +# 計算孤島總面積 +result = 0 # 初始化,避免使用到處理邊界時所產生的累加值 + +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + visited[i][j] = True + dfs(grid, i, j) + +# 輸出孤島的總面積 +print(result) +``` + +### Go + +``` go + +package main + +import ( + "fmt" +) + +var count int +var dir = [4][2]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func bfs(grid [][]int, x, y int) { + queue := [][2]int{{x, y}} + grid[x][y] = 0 // 只要加入队列,立刻标记 + count++ + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + curx, cury := cur[0], cur[1] + + for i := 0; i < 4; i++ { + nextx := curx + dir[i][0] + nexty := cury + dir[i][1] + + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + + if grid[nextx][nexty] == 1 { + queue = append(queue, [2]int{nextx, nexty}) + count++ + grid[nextx][nexty] = 0 // 只要加入队列立刻标记 + } + } + } +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + grid := make([][]int, n) + for i := range grid { + grid[i] = make([]int, m) + } + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + fmt.Scan(&grid[i][j]) + } + } + + // 从左侧边,和右侧边向中间遍历 + for i := 0; i < n; i++ { + if grid[i][0] == 1 { + bfs(grid, i, 0) + } + if grid[i][m-1] == 1 { + bfs(grid, i, m-1) + } + } + + // 从上边和下边向中间遍历 + for j := 0; j < m; j++ { + if grid[0][j] == 1 { + bfs(grid, 0, j) + } + if grid[n-1][j] == 1 { + bfs(grid, n-1, j) + } + } + + // 清空之前的计数 + count = 0 + + // 遍历所有位置 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 1 { + bfs(grid, i, j) + } + } + } + + fmt.Println(count) +} + + +``` + +### Rust + +### JavaScript + +#### 深搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let count = 0 // 孤岛的总面积 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, x, y) => { + if(graph[x][y] === 0) return + graph[x][y] = 0 // 标记为海洋 + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + dfs(graph, nextx, nexty) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) dfs(graph, i, 0) + if (graph[i][M - 1] === 1) dfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) dfs(graph, 0, j) + if (graph[N - 1][j] === 1) dfs(graph, N - 1, j) + } + + count = 0 + // 统计孤岛的总面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) count++ + } + } + console.log(count); +})() +``` + + + +#### 广搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let count = 0 // 孤岛的总面积 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, x, y) => { + let queue = [] + queue.push([x, y]) + graph[x][y] = 0 // 只要加入队列,立刻标记 + + while (queue.length) { + let [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (graph[nextx][nexty] === 1) { + queue.push([nextx, nexty]) + graph[nextx][nexty] = 0 // 只要加入队列,立刻标记 + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) bfs(graph, i, 0) + if (graph[i][M - 1] === 1) bfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) bfs(graph, 0, j) + if (graph[N - 1][j] === 1) bfs(graph, N - 1, j) + } + + count = 0 + // 统计孤岛的总面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) count++ + } + } + console.log(count); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" "b/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" new file mode 100644 index 0000000000..1b31676277 --- /dev/null +++ "b/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" @@ -0,0 +1,506 @@ + +

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

+ +# 102. 沉没孤岛 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1174) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。 + + +现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述 + +输出将孤岛“沉没”之后的岛屿矩阵。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +``` +1 1 0 0 0 +1 1 0 0 0 +0 0 0 0 0 +0 0 0 1 1 +``` + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517110932.png) + +将孤岛沉没: + +![](https://file1.kamacoder.com/i/algo/20240517110953.png) + +数据范围: + +1 <= M, N <= 50 + +## 思路 + +这道题目和[0101.孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173)正好反过来了,[0101.孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173)是求 地图中间的空格数,而本题是要把地图中间的 1 都改成 0 。 + +那么两题在思路上也是差不多的。 + +思路依然是从地图周边出发,将周边空格相邻的陆地都做上标记,然后在遍历一遍地图,遇到 陆地 且没做过标记的,那么都是地图中间的 陆地 ,全部改成水域就行。 + +有的录友可能想,我再定义一个 visited 二维数组,单独标记周边的陆地,然后遍历地图的时候同时对 地图数组 和 数组visited 进行判断,决定 陆地是否变成水域。 + +这样做其实就有点麻烦了,不用额外定义空间了,标记周边的陆地,可以直接改陆地为其他特殊值作为标记。 + +步骤一:深搜或者广搜将地图周边的 1 (陆地)全部改成 2 (特殊标记) + +步骤二:将水域中间 1 (陆地)全部改成 水域(0) + +步骤三:将之前标记的 2 改为 1 (陆地) + +如图: + +![](https://file1.kamacoder.com/i/algo/20240517113813.png) + +整体C++代码如下,以下使用dfs实现,其实遍历方式dfs,bfs都是可以的。 + +```CPP +#include +#include +using namespace std; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 +void dfs(vector>& grid, int x, int y) { + grid[x][y] = 2; + 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 (grid[nextx][nexty] == 0 || grid[nextx][nexty] == 2) continue; + dfs (grid, nextx, nexty); + } + return; +} + +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]; + } + } + + // 步骤一: + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + // 步骤二、步骤三 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) grid[i][j] = 0; + if (grid[i][j] == 2) grid[i][j] = 1; + } + } + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cout << grid[i][j] << " "; + } + cout << endl; + } +} +``` + +## 其他语言版本 + +### Java + +```JAVA + +import java.util.Scanner; + +public class Main { + static int[][] dir = { {-1, 0}, {0, -1}, {1, 0}, {0, 1} }; // 保存四个方向 + + public static void dfs(int[][] grid, int x, int y) { + grid[x][y] = 2; + for (int[] d : dir) { + int nextX = x + d[0]; + int nextY = y + d[1]; + // 超过边界 + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) continue; + // 不符合条件,不继续遍历 + if (grid[nextX][nextY] == 0 || grid[nextX][nextY] == 2) continue; + dfs(grid, nextX, nextY); + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] grid = new int[n][m]; + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + // 步骤一: + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + + // 步骤二、步骤三 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) grid[i][j] = 0; + if (grid[i][j] == 2) grid[i][j] = 1; + } + } + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + System.out.print(grid[i][j] + " "); + } + System.out.println(); + } + + scanner.close(); + } +} + + +``` + + +### Python + + +```python + +def dfs(grid, x, y): + grid[x][y] = 2 + directions = [(-1, 0), (0, -1), (1, 0), (0, 1)] # 四个方向 + for dx, dy in directions: + nextx, nexty = x + dx, y + dy + # 超过边界 + if nextx < 0 or nextx >= len(grid) or nexty < 0 or nexty >= len(grid[0]): + continue + # 不符合条件,不继续遍历 + if grid[nextx][nexty] == 0 or grid[nextx][nexty] == 2: + continue + dfs(grid, nextx, nexty) + +def main(): + n, m = map(int, input().split()) + grid = [[int(x) for x in input().split()] for _ in range(n)] + + # 步骤一: + # 从左侧边,和右侧边 向中间遍历 + for i in range(n): + if grid[i][0] == 1: + dfs(grid, i, 0) + if grid[i][m - 1] == 1: + dfs(grid, i, m - 1) + + # 从上边和下边 向中间遍历 + for j in range(m): + if grid[0][j] == 1: + dfs(grid, 0, j) + if grid[n - 1][j] == 1: + dfs(grid, n - 1, j) + + # 步骤二、步骤三 + for i in range(n): + for j in range(m): + if grid[i][j] == 1: + grid[i][j] = 0 + if grid[i][j] == 2: + grid[i][j] = 1 + + # 打印结果 + for row in grid: + print(' '.join(map(str, row))) + +if __name__ == "__main__": + main() +``` + + + 广搜版 +```Python +from collections import deque + +n, m = list(map(int, input().split())) +g = [] +for _ in range(n): + row = list(map(int,input().split())) + g.append(row) + +directions = [(1,0),(-1,0),(0,1),(0,-1)] +count = 0 + +def bfs(r,c,mode): + global count + q = deque() + q.append((r,c)) + count += 1 + + while q: + r, c = q.popleft() + if mode: + g[r][c] = 2 + + for di in directions: + next_r = r + di[0] + next_c = c + di[1] + if next_c < 0 or next_c >= m or next_r < 0 or next_r >= n: + continue + if g[next_r][next_c] == 1: + q.append((next_r,next_c)) + if mode: + g[r][c] = 2 + + count += 1 + + +for i in range(n): + if g[i][0] == 1: bfs(i,0,True) + if g[i][m-1] == 1: bfs(i, m-1,True) + +for j in range(m): + if g[0][j] == 1: bfs(0,j,1) + if g[n-1][j] == 1: bfs(n-1,j,1) + +for i in range(n): + for j in range(m): + if g[i][j] == 2: + g[i][j] = 1 + else: + g[i][j] = 0 + +for row in g: + print(" ".join(map(str, row))) + +``` + + +### Go + +### Rust + +### JavaScript + +#### 深搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, x, y) => { + if (graph[x][y] !== 1) return + graph[x][y] = 2 // 标记为非孤岛陆地 + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + dfs(graph, nextx, nexty) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) dfs(graph, i, 0) + if (graph[i][M - 1] === 1) dfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) dfs(graph, 0, j) + if (graph[N - 1][j] === 1) dfs(graph, N - 1, j) + } + + + // 遍历地图,将孤岛沉没,即将陆地1标记为0;将非孤岛陆地2标记为1 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) graph[i][j] = 0 + else if (graph[i][j] === 2) graph[i][j] = 1 + } + console.log(graph[i].join(' ')); + } +})() +``` + + + +#### 广搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, x, y) => { + let queue = [] + queue.push([x, y]) + graph[x][y] = 2 // 标记为非孤岛陆地 + + while (queue.length) { + let [xx, yy] = queue.shift() + + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (graph[nextx][nexty] === 1) bfs(graph, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) bfs(graph, i, 0) + if (graph[i][M - 1] === 1) bfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) bfs(graph, 0, j) + if (graph[N - 1][j] === 1) bfs(graph, N - 1, j) + } + + + // 遍历地图,将孤岛沉没,即将陆地1标记为0;将非孤岛陆地2标记为1 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) graph[i][j] = 0 + else if (graph[i][j] === 2) graph[i][j] = 1 + } + console.log(graph[i].join(' ')); + } +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" "b/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..bf6cd40f43 --- /dev/null +++ "b/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" @@ -0,0 +1,857 @@ + +

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

+ +# 103. 水流问题 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1175) + +题目描述: + +现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。 + + +矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。 + +输入描述: + +第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。 + +输出描述: + +输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。 + +输入示例: + +``` +5 5 +1 3 1 2 4 +1 2 1 3 2 +2 4 7 2 1 +4 5 6 1 1 +1 4 1 2 1 +``` + +输出示例: + +``` +0 4 +1 3 +2 2 +3 0 +3 1 +3 2 +4 0 +4 1 +``` + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517115816.png) + +图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。 + + +数据范围: + +1 <= M, N <= 50 + +## 思路 + +一个比较直白的想法,其实就是 遍历每个点,然后看这个点 能不能同时到达第一组边界和第二组边界。 + +至于遍历方式,可以用dfs,也可以用bfs,以下用dfs来举例。 + +那么这种思路的实现代码如下: + +```CPP +#include +#include +using namespace std; +int n, m; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; + +// 从 x,y 出发 把可以走的地方都标记上 +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y]) 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 >= n || nexty < 0 || nexty >= m) continue; + if (grid[x][y] < grid[nextx][nexty]) continue; // 高度不合适 + + dfs (grid, visited, nextx, nexty); + } + return; +} +bool isResult(vector>& grid, int x, int y) { + vector> visited(n, vector(m, false)); + + // 深搜,将x,y出发 能到的节点都标记上。 + dfs(grid, visited, x, y); + bool isFirst = false; + bool isSecond = false; + + // 以下就是判断x,y出发,是否到达第一组边界和第二组边界 + // 第一边界的上边 + for (int j = 0; j < m; j++) { + if (visited[0][j]) { + isFirst = true; + break; + } + } + // 第一边界的左边 + for (int i = 0; i < n; i++) { + if (visited[i][0]) { + isFirst = true; + break; + } + } + // 第二边界下边 + for (int j = 0; j < m; j++) { + if (visited[n - 1][j]) { + isSecond = true; + break; + } + } + // 第二边界右边 + for (int i = 0; i < n; i++) { + if (visited[i][m - 1]) { + isSecond = true; + break; + } + } + if (isFirst && isSecond) return true; + return false; +} + + +int main() { + 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]; + } + } + // 遍历每一个点,看是否能同时到达第一组边界和第二组边界 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (isResult(grid, i, j)) { + cout << i << " " << j << endl; + } + } + } +} + +``` + +这种思路很直白,但很明显,以上代码超时了。 来看看时间复杂度。 + +遍历每一个节点,是 m * n,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是: m * n + +那么整体时间复杂度 就是 O(m^2 * n^2) ,这是一个四次方的时间复杂度。 + +## 优化 + +那么我们可以 反过来想,从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。 + +同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。 + +然后**两方都标记过的节点就是既可以流向第一组边界也可以流向第二组边界的节点**。 + +从第一组边界边上节点出发,如图: (图中并没有把所有遍历的方向都画出来,只画关键部分) + +![](https://file1.kamacoder.com/i/algo/20250304174747.png) + +从第二组边界上节点出发,如图: (图中并没有把所有遍历的方向都画出来,只画关键部分) + +![](https://file1.kamacoder.com/i/algo/20250304174801.png) + +最后,我们得到两个方向交界的这些节点,就是我们最后要求的节点。 + +按照这样的逻辑,就可以写出如下遍历代码:(详细注释) + + +```CPP +#include +#include +using namespace std; +int n, m; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y]) 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 >= n || nexty < 0 || nexty >= m) continue; + if (grid[x][y] > grid[nextx][nexty]) continue; // 注意:这里是从低向高遍历 + + dfs (grid, visited, nextx, nexty); + } + return; +} + + + +int main() { + + 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> firstBorder(n, vector(m, false)); + + // 标记从第一组边界上的节点出发,可以遍历的节点 + vector> secondBorder(n, vector(m, false)); + + // 从最上和最下行的节点出发,向高处遍历 + for (int i = 0; i < n; i++) { + dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 + dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 + } + + // 从最左和最右列的节点出发,向高处遍历 + for (int j = 0; j < m; j++) { + dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 + dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 + } + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + // 如果这个节点,从第一组边界和第二组边界出发都遍历过,就是结果 + if (firstBorder[i][j] && secondBorder[i][j]) cout << i << " " << j << endl;; + } + } + + +} + +``` + + +时间复杂度分析, 关于dfs函数搜索的过程 时间复杂度是 O(n * m),这个大家比较容易想。 + +关键看主函数,那么每次dfs的时候,上面还是有for循环的。 + +第一个for循环,时间复杂度是:n * (n * m) 。 + +第二个for循环,时间复杂度是:m * (n * m)。 + +所以本题看起来 时间复杂度好像是 : n * (n * m) + m * (n * m) = (m * n) * (m + n) 。 + +其实这是一个误区,大家再自己看 dfs函数的实现,其实 有visited函数记录 走过的节点,而走过的节点是不会再走第二次的。 + +所以 调用dfs函数,**只要参数传入的是 数组 firstBorder,那么地图中 每一个节点其实就遍历一次,无论你调用多少次**。 + +同理,调用dfs函数,只要 参数传入的是 数组 secondBorder,地图中每个节点也只会遍历一次。 + +所以,以下这段代码的时间复杂度是 2 * n * m。 地图用每个节点就遍历了两次,参数传入 firstBorder 的时候遍历一次,参数传入 secondBorder 的时候遍历一次。 + +```CPP +// 从最上和最下行的节点出发,向高处遍历 +for (int i = 0; i < n; i++) { + dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 + dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 +} + +// 从最左和最右列的节点出发,向高处遍历 +for (int j = 0; j < m; j++) { + dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 + dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 +} +``` + +那么本题整体的时间复杂度其实是: 2 * n * m + n * m ,所以最终时间复杂度为 O(n * m) 。 + +空间复杂度为:O(n * m) 这个就不难理解了。开了几个 n * m 的数组。 + + + +## 其他语言版本 + +### Java +```Java +public class Main { + + // 采用 DFS 进行搜索 + public static void dfs(int[][] heights, int x, int y, boolean[][] visited, int preH) { + // 遇到边界或者访问过的点,直接返回 + if (x < 0 || x >= heights.length || y < 0 || y >= heights[0].length || visited[x][y]) return; + // 不满足水流入条件的直接返回 + if (heights[x][y] < preH) return; + // 满足条件,设置为true,表示可以从边界到达此位置 + visited[x][y] = true; + + // 向下一层继续搜索 + dfs(heights, x + 1, y, visited, heights[x][y]); + dfs(heights, x - 1, y, visited, heights[x][y]); + dfs(heights, x, y + 1, visited, heights[x][y]); + dfs(heights, x, y - 1, visited, heights[x][y]); + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + + int[][] heights = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + heights[i][j] = sc.nextInt(); + } + } + + // 初始化两个二位boolean数组,代表两个边界 + boolean[][] pacific = new boolean[m][n]; + boolean[][] atlantic = new boolean[m][n]; + + // 从左右边界出发进行DFS + for (int i = 0; i < m; i++) { + dfs(heights, i, 0, pacific, Integer.MIN_VALUE); + dfs(heights, i, n - 1, atlantic, Integer.MIN_VALUE); + } + + // 从上下边界出发进行DFS + for (int j = 0; j < n; j++) { + dfs(heights, 0, j, pacific, Integer.MIN_VALUE); + dfs(heights, m - 1, j, atlantic, Integer.MIN_VALUE); + } + + // 当两个边界二维数组在某个位置都为true时,符合题目要求 + List> res = new ArrayList<>(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (pacific[i][j] && atlantic[i][j]) { + res.add(Arrays.asList(i, j)); + } + } + } + + // 打印结果 + for (List list : res) { + for (int k = 0; k < list.size(); k++) { + if (k == 0) { + System.out.print(list.get(k) + " "); + } else { + System.out.print(list.get(k)); + } + } + System.out.println(); + } + } +} + +``` + +### Python +```Python +first = set() +second = set() +directions = [[-1, 0], [0, 1], [1, 0], [0, -1]] + +def dfs(i, j, graph, visited, side): + if visited[i][j]: + return + + visited[i][j] = True + side.add((i, j)) + + for x, y in directions: + new_x = i + x + new_y = j + y + if ( + 0 <= new_x < len(graph) + and 0 <= new_y < len(graph[0]) + and int(graph[new_x][new_y]) >= int(graph[i][j]) + ): + dfs(new_x, new_y, graph, visited, side) + +def main(): + global first + global second + + N, M = map(int, input().strip().split()) + graph = [] + for _ in range(N): + row = input().strip().split() + graph.append(row) + + # 是否可到达第一边界 + visited = [[False] * M for _ in range(N)] + for i in range(M): + dfs(0, i, graph, visited, first) + for i in range(N): + dfs(i, 0, graph, visited, first) + + # 是否可到达第二边界 + visited = [[False] * M for _ in range(N)] + for i in range(M): + dfs(N - 1, i, graph, visited, second) + for i in range(N): + dfs(i, M - 1, graph, visited, second) + + # 可到达第一边界和第二边界 + res = first & second + + for x, y in res: + print(f"{x} {y}") + + +if __name__ == "__main__": + main() +``` + +### Go +```go +package main + +import ( + "os" + "fmt" + "strings" + "strconv" + "bufio" +) + +var directions = [][]int{{0, -1}, {0, 1}, {-1, 0}, {1, 0}} // 四个方向的偏移量 + +func main() { + scanner := bufio.NewScanner(os.Stdin) + + scanner.Scan() + lineList := strings.Fields(scanner.Text()) + N, _ := strconv.Atoi(lineList[0]) + M, _ := strconv.Atoi(lineList[1]) + + grid := make([][]int, N) + visited := make([][]bool, N) // 用于标记是否访问过 + for i := 0; i < N; i++ { + grid[i] = make([]int, M) + visited[i] = make([]bool, M) + scanner.Scan() + lineList = strings.Fields(scanner.Text()) + + for j := 0; j < M; j++ { + grid[i][j], _ = strconv.Atoi(lineList[j]) + } + } + + // 遍历每个单元格,使用DFS检查是否可达两组边界 + for i := 0; i < N; i++ { + for j := 0; j < M; j++ { + canReachFirst, canReachSecond := dfs(grid, visited, i, j) + if canReachFirst && canReachSecond { + fmt.Println(strconv.Itoa(i) + " " + strconv.Itoa(j)) + } + } + } +} + +func dfs(grid [][]int, visited [][]bool, startx int, starty int) (bool, bool) { + visited[startx][starty] = true + canReachFirst := startx == 0 || starty == 0 || startx == len(grid)-1 || starty == len(grid[0])-1 + canReachSecond := startx == len(grid)-1 || starty == len(grid[0])-1 || startx == 0 || starty == 0 + + if canReachFirst && canReachSecond { + return true, true + } + + for _, direction := range directions { + nextx := startx + direction[0] + nexty := starty + direction[1] + + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue + } + + if grid[nextx][nexty] <= grid[startx][starty] && !visited[nextx][nexty] { + hasReachFirst, hasReachSecond := dfs(grid, visited, nextx, nexty) + if !canReachFirst { + canReachFirst = hasReachFirst + } + if !canReachSecond { + canReachSecond = hasReachSecond + } + } + } + return canReachFirst, canReachSecond +} +``` + +### Rust + +### JavaScript + +#### 深搜 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + if (visited[x][y]) return + visited[x][y] = true // 标记为可访问 + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界,跳过 + if (graph[x][y] < graph[nextx][nexty]) continue //不能流过.跳过 + dfs(graph, visited, nextx, nexty) + } +} + + +/** + * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界 + * @param {*} x 坐标 + * @param {*} y 坐标 + * @return {*} true可以到达,false不可以到达 + */ +const isResult = (x, y) => { + let visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + let isFirst = false //是否可到达第一边界 + let isSecond = false //是否可到达第二边界 + + // 深搜,将(x, y)可到达的所有节点做标记 + dfs(graph, visited, x, y) + + // 判断能否到第一边界左边 + for (let i = 0; i < N; i++) { + if (visited[i][0]) { + isFirst = true + break + } + } + + // 判断能否到第一边界上边 + for (let j = 0; j < M; j++) { + if (visited[0][j]) { + isFirst = true + break + } + } + + // 判断能否到第二边界右边 + for (let i = 0; i < N; i++) { + if (visited[i][M - 1]) { + isSecond = true + break + } + } + + // 判断能否到第二边界下边 + for (let j = 0; j < M; j++) { + if (visited[N - 1][j]) { + isSecond = true + break + } + } + + return isFirst && isSecond +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (isResult(i, j)) console.log(i + ' ' + j); + } + } +})() +``` + + + +#### 广搜-解法一 + +```java +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + visited[x][y] = true + + while (queue.length) { + const [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 可访问或者不能流过, 跳过 (注意这里是graph[xx][yy] < graph[nextx][nexty], 不是graph[x][y] < graph[nextx][nexty]) + if (visited[nextx][nexty] || graph[xx][yy] < graph[nextx][nexty]) continue + + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + + } + } +} + + +/** + * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界 + * @param {*} x 坐标 + * @param {*} y 坐标 + * @return {*} true可以到达,false不可以到达 + */ +const isResult = (x, y) => { + let visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + let isFirst = false //是否可到达第一边界 + let isSecond = false //是否可到达第二边界 + + // 深搜,将(x, y)可到达的所有节点做标记 + bfs(graph, visited, x, y) + + // console.log(visited); + + // 判断能否到第一边界左边 + for (let i = 0; i < N; i++) { + if (visited[i][0]) { + isFirst = true + break + } + } + + // 判断能否到第一边界上边 + for (let j = 0; j < M; j++) { + if (visited[0][j]) { + isFirst = true + break + } + } + + // 判断能否到第二边界右边 + for (let i = 0; i < N; i++) { + if (visited[i][M - 1]) { + isSecond = true + break + } + } + + // 判断能否到第二边界下边 + for (let j = 0; j < M; j++) { + if (visited[N - 1][j]) { + isSecond = true + break + } + } + + return isFirst && isSecond +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (isResult(i, j)) console.log(i + ' ' + j); + } + } +})() +``` + + + +#### 广搜-解法二 + +从第一边界和第二边界开始向高处流, 标记可以流到的位置, 两个边界都能到达的位置就是所求结果 + +```javascript + const r1 = require('readline').createInterface({ input: process.stdin }); + // 创建readline接口 + let iter = r1[Symbol.asyncIterator](); + // 创建异步迭代器 + const readline = async () => (await iter.next()).value; + + let graph // 地图 + let N, M // 地图大小 + const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + + // 读取输入,初始化地图 + const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } + } + + + /** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ + const bfs = (graph, visited, x, y) => { + if(visited[x][y]) return + + let queue = [] + queue.push([x, y]) + visited[x][y] = true + + while (queue.length) { + const [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 可访问或者不能流过, 跳过 (注意因为是从边界往高处流, 所以这里是graph[xx][yy] >= graph[nextx][nexty], 还要注意不是graph[xx][yy] >= graph[nextx][nexty]) + if (visited[nextx][nexty] || graph[xx][yy] >= graph[nextx][nexty]) continue + + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + } + } + } + + (async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 记录第一边界可到达的节点 + let firstBorder = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + // 记录第二边界可到达的节点 + let secondBorder = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + // 第一边界左边和第二边界右边 + for (let i = 0; i < N; i++) { + bfs(graph, firstBorder, i, 0) + bfs(graph, secondBorder, i, M - 1) + } + + // 第一边界上边和第二边界下边 + for (let j = 0; j < M; j++) { + bfs(graph, firstBorder, 0, j) + bfs(graph, secondBorder, N - 1, j) + } + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (firstBorder[i][j] && secondBorder[i][j]) console.log(i + ' ' + j); + } + } + })() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + +
diff --git "a/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" "b/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" new file mode 100644 index 0000000000..8c4964a9e4 --- /dev/null +++ "b/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" @@ -0,0 +1,666 @@ + +

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

+ +# 104.建造最大岛屿 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1176) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。 + +岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述: + +输出一个整数,表示最大的岛屿面积。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例 + +6 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240522154055.png) + + +对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。 + +![](https://file1.kamacoder.com/i/algo/20240522154110.png) + + +数据范围: + +1 <= M, N <= 50。 + + +## 思路 + +本题的一个暴力想法,应该是遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积。 + +计算地图的最大面积:遍历地图 + 深搜岛屿,时间复杂度为 n * n。 + +(其实使用深搜还是广搜都是可以的,其目的就是遍历岛屿做一个标记,相当于染色,那么使用哪个遍历方式都行,以下我用深搜来讲解) + +每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为:n^4。 + +## 优化思路 + +其实每次深搜遍历计算最大岛屿面积,我们都做了很多重复的工作。 + +只要用一次深搜把每个岛屿的面积记录下来就好。 + +第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积 + +第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。 + +拿如下地图的岛屿情况来举例: (1为陆地) + +![](https://file1.kamacoder.com/i/algo/20220829104834.png) + +第一步,则遍历地图,并将岛屿的编号和面积都统计好,过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105644.png) + + +本过程代码如下: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + 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, mark); + } +} + +int largestIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } +} +``` + + +这个过程时间复杂度 n * n 。可能有录友想:分明是两个for循环下面套这一个dfs,时间复杂度怎么回事 n * n呢? + +其实大家可以仔细看一下代码,**n * n这个方格地图中,每个节点我们就遍历一次,并不会重复遍历**。 + +第二步过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105249.png) + +也就是遍历每一个0的方格,并统计其相邻岛屿面积,最后取一个最大值。 + +这个过程的时间复杂度也为 n * n。 + +所以整个解法的时间复杂度,为 n * n + n * n 也就是 n^2。 + +当然这里还有一个优化的点,就是 可以不用 visited数组,因为有mark来标记,所以遍历过的grid[i][j]是不等于1的。 + +代码如下: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, int x, int y, int mark) { + if (grid[x][y] != 1 || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; // 越界了,直接跳过 + dfs(grid, nextx, nexty, mark); + } +} + +int main() { + 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]; + } + } + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (grid[i][j] == 1) { + count = 0; + dfs(grid, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } +``` + +不过为了让各个变量各司其事,代码清晰一些,完整代码还是使用visited数组来标记。 + +最后,整体代码如下: + +```CPP +#include +#include +#include +#include +using namespace std; +int n, m; +int count; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty, mark); + } +} + +int main() { + 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)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } + if (isAllGrid) { + cout << n * m << endl; // 如果都是陆地,返回全面积 + return 0; // 结束程序 + } + + // 以下逻辑是根据添加陆地的位置,计算周边岛屿面积之和 + int result = 0; // 记录最后结果 + unordered_set visitedGrid; // 标记访问过的岛屿 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + count = 1; // 记录连接之后的岛屿数量 + visitedGrid.clear(); // 每次使用时,清空 + if (grid[i][j] == 0) { + for (int k = 0; k < 4; k++) { + int neari = i + dir[k][1]; // 计算相邻坐标 + int nearj = j + dir[k][0]; + if (neari < 0 || neari >= n || nearj < 0 || nearj >= m) continue; + if (visitedGrid.count(grid[neari][nearj])) continue; // 添加过的岛屿不要重复添加 + // 把相邻四面的岛屿数量加起来 + count += gridNum[grid[neari][nearj]]; + visitedGrid.insert(grid[neari][nearj]); // 标记该岛屿已经添加过 + } + } + result = max(result, count); + } + } + cout << result << endl; + +} +``` + + +## 其他语言版本 + +### Java +```Java +public class Main { + // 该方法采用 DFS + // 定义全局变量 + // 记录每次每个岛屿的面积 + static int count; + // 对每个岛屿进行标记 + static int mark; + // 定义二维数组表示四个方位 + static int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + // DFS 进行搜索,将每个岛屿标记为不同的数字 + public static void dfs(int[][] grid, int x, int y, boolean[][] visited) { + // 当遇到边界,直接return + if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length) return; + // 遇到已经访问过的或者遇到海水,直接返回 + if (visited[x][y] || grid[x][y] == 0) return; + + visited[x][y] = true; + count++; + grid[x][y] = mark; + + // 继续向下层搜索 + dfs(grid, x, y + 1, visited); + dfs(grid, x, y - 1, visited); + dfs(grid, x + 1, y, visited); + dfs(grid, x - 1, y, visited); + } + + public static void main (String[] args) { + // 接收输入 + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + + int[][] grid = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = sc.nextInt(); + } + } + + // 初始化mark变量,从2开始(区别于0水,1岛屿) + mark = 2; + + // 定义二位boolean数组记录该位置是否被访问 + boolean[][] visited = new boolean[m][n]; + + // 定义一个HashMap,记录某片岛屿的标记号和面积 + HashMap getSize = new HashMap<>(); + + // 定义一个HashSet,用来判断某一位置水四周是否存在不同标记编号的岛屿 + HashSet set = new HashSet<>(); + + // 定义一个boolean变量,看看DFS之后,是否全是岛屿 + boolean isAllIsland = true; + + // 遍历二维数组进行DFS搜索,标记每片岛屿的编号,记录对应的面积 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) isAllIsland = false; + if (grid[i][j] == 1) { + count = 0; + dfs(grid, i, j, visited); + getSize.put(mark, count); + mark++; + } + } + } + + int result = 0; + if (isAllIsland) result = m * n; + + // 对标记完的grid继续遍历,判断每个水位置四周是否有岛屿,并记录下四周不同相邻岛屿面积之和 + // 每次计算完一个水位置周围可能存在的岛屿面积之和,更新下result变量 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + set.clear(); + // 当前水位置变更为岛屿,所以初始化为1 + int curSize = 1; + + for (int[] dir : dirs) { + int curRow = i + dir[0]; + int curCol = j + dir[1]; + + if (curRow < 0 || curRow >= m || curCol < 0 || curCol >= n) continue; + int curMark = grid[curRow][curCol]; + // 如果当前相邻的岛屿已经遍历过或者HashMap中不存在这个编号,继续搜索 + if (set.contains(curMark) || !getSize.containsKey(curMark)) continue; + set.add(curMark); + curSize += getSize.get(curMark); + } + + result = Math.max(result, curSize); + } + } + } + + // 打印结果 + System.out.println(result); + } +} + +``` + +### Python + + +#### BFS +```Python +from typing import List +from collections import defaultdict + +class Solution: + def __init__(self): + self.direction = [(1,0),(-1,0),(0,1),(0,-1)] + self.res = 0 + self.count = 0 + self.idx = 1 + self.count_area = defaultdict(int) + + def max_area_island(self, grid: List[List[int]]) -> int: + if not grid or len(grid) == 0 or len(grid[0]) == 0: + return 0 + + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] == 1: + self.count = 0 + self.idx += 1 + self.dfs(grid,i,j) + # print(grid) + self.check_area(grid) + # print(self.count_area) + + if self.check_largest_connect_island(grid=grid): + return self.res + 1 + return max(self.count_area.values()) + + def dfs(self,grid,row,col): + grid[row][col] = self.idx + self.count += 1 + for dr,dc in self.direction: + _row = dr + row + _col = dc + col + if 0<=_row (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点, 标记岛屿 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + +let count = 0 // 统计岛屿面积 +let areaMap = new Map() // 存储岛屿面积 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @param {*} mark 当前岛屿的标记 + * @return {*} + */ +const dfs = (graph, visited, x, y, mark) => { + if (visited[x][y] != 0) return + visited[x][y] = mark + count++ + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 已访问过, 或者是海洋, 跳过 + if (visited[nextx][nexty] != 0 || graph[nextx][nexty] == 0) continue + + dfs(graph, visited, nextx, nexty, mark) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + let isAllLand = true //标记整个地图都是陆地 + + let mark = 2 // 标记岛屿 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] == 0 && isAllLand) isAllLand = false + if (graph[i][j] === 1 && visited[i][j] === 0) { + count = 0 + dfs(graph, visited, i, j, mark) + areaMap.set(mark, count) + mark++ + } + } + } + + // 如果全是陆地, 直接返回面积 + if (isAllLand) { + console.log(N * M); + return + } + + let result = 0 // 记录最后结果 + let visitedIsland = new Map() //标记访问过的岛屿, 因为海洋四周可能是同一个岛屿, 需要标记避免重复统计面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (visited[i][j] === 0) { + count = 1 // 记录连接之后的岛屿数量 + visitedIsland.clear() // 每次使用时,清空 + + // 计算海洋周围岛屿面积 + for (let m = 0; m < 4; m++) { + const nextx = i + dir[m][0] + const nexty = j + dir[m][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + const island = visited[nextx][nexty] + if (island == 0 || visitedIsland.get(island)) continue// 四周是海洋或者访问过的陆地 跳过 + + // 标记为访问, 计算面积 + visitedIsland.set(island, true) + count += areaMap.get(visited[nextx][nexty]) + } + + result = Math.max(result, count) + } + } + } + + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" "b/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" new file mode 100644 index 0000000000..1358f673d5 --- /dev/null +++ "b/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" @@ -0,0 +1,556 @@ + +

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

+ +# 105.有向图的完全可达性 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1177) + +【题目描述】 + +给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。 + +【输入描述】 + +第一行包含两个正整数,表示节点数量 N 和边的数量 K。 后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。 + +【输出描述】 + +如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。 + +【输入示例】 + +``` +4 4 +1 2 +2 1 +1 3 +2 4 +``` + +【输出示例】 + +1 + +【提示信息】 + +![](https://file1.kamacoder.com/i/algo/20240522174707.png) + +从 1 号节点可以到达任意节点,输出 1。 + +数据范围: + +* 1 <= N <= 100; +* 1 <= K <= 2000。 + +## 思路 + +本题给我们是一个有向图, 意识到这是有向图很重要! + +接下来我们再画一个图,从图里可以直观看出来,节点6 是 不能到达节点1 的 + +![](https://file1.kamacoder.com/i/algo/20240522175451.png) + +这就很容易让我们想起岛屿问题,只要发现独立的岛,就是不可到达的。 + +**但本题是有向图**,在有向图中,即使所有节点都是链接的,但依然不可能从0出发遍历所有边。 + +例如上图中,节点1 可以到达节点2,但节点2是不能到达节点1的。 + +所以本题是一个**有向图搜索全路径的问题**。 只能用深搜(DFS)或者广搜(BFS)来搜。 + +**以下dfs分析 大家一定要仔细看,本题有两种dfs的解法,很多题解没有讲清楚**。 看完之后 相信你对dfs会有更深的理解。 + +深搜三部曲: + +1. 确认递归函数,参数 + +需要传入地图,需要知道当前我们拿到的key,以至于去下一个房间。 + +同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。 + +所以 递归函数参数如下: + +```C++ +// key 当前得到的可以 +// visited 记录访问过的房间 +void dfs(const vector>& graph, int key, vector& visited) { +``` + +2. 确认终止条件 + +遍历的时候,什么时候终止呢? + +这里有一个很重要的逻辑,就是在递归中,**我们是处理当前访问的节点,还是处理下一个要访问的节点**。 + +这决定 终止条件怎么写。 + +首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。 + +如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。 + +代码就是这样: + +```C++ +// 写法一:处理当前访问的节点 +void dfs(const vector>& graph, int key, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = true; + list keys = graph[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(graph, key, visited); + } +} +``` + +如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。 + +这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。 + +代码就是这样的: + +```C++ +// 写法二:处理下一个要访问的节点 +void dfs(const vector>& graph, int key, vector& visited) { + list keys = graph[key]; + for (int key : keys) { + if (visited[key] == false) { // 确认下一个是没访问过的节点 + visited[key] = true; + dfs(graph, key, visited); + } + } +} +``` + +可以看出,**如何看待 我们要访问的节点,直接决定了两种不一样的写法**,很多录友对这一块很模糊,可能做过这道题,但没有思考到这个维度上。 + + +3. 处理目前搜索节点出发的路径 + +其实在上面,深搜三部曲 第二部,就已经讲了,因为终止条件的两种写法, 直接决定了两种不一样的递归写法。 + +这里还有细节: + +看上面两个版本的写法中, 好像没有发现回溯的逻辑。 + +我们都知道,有递归就有回溯,回溯就在递归函数的下面, 那么之前我们做的dfs题目,都需要回溯操作,例如:[0098.所有可达路径](./0098.所有可达路径), **为什么本题就没有回溯呢?** + +代码中可以看到dfs函数下面并没有回溯的操作。 + +此时就要在思考本题的要求了,本题是需要判断 1节点 是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。 + +**那什么时候需要回溯操作呢?** + +当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”, 如果不理解的话,去看我写的 [0098.所有可达路径](./0098.所有可达路径.md) 的题解。 + + +以上分析完毕,DFS整体实现C++代码如下: + +```CPP +// 写法一:dfs 处理当前访问的节点 +#include +#include +#include +using namespace std; + +void dfs(const vector>& graph, int key, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = true; + list keys = graph[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(graph, key, visited); + } +} + +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); + } + vector visited(n + 1, false); + dfs(graph, 1, visited); + //检查是否都访问到了 + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + +**第二种写法注意有注释的地方是和写法一的区别** + +```CPP +写法二:dfs处理下一个要访问的节点 +#include +#include +#include +using namespace std; + +void dfs(const vector>& graph, int key, vector& visited) { + list keys = graph[key]; + for (int key : keys) { + if (visited[key] == false) { // 确认下一个是没访问过的节点 + visited[key] = true; + dfs(graph, key, visited); + } + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + vector> graph(n + 1); + while (m--) { + cin >> s >> t; + graph[s].push_back(t); + + } + vector visited(n + 1, false); + + visited[1] = true; // 节点1 预先处理 + dfs(graph, 1, visited); + + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + +本题我也给出 BFS C++代码,[BFS理论基础](https://programmercarl.com/kamacoder/%E5%9B%BE%E8%AE%BA%E6%B7%B1%E6%90%9C%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html),代码如下: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, s, t; + cin >> n >> m; + + vector> graph(n + 1); + while (m--) { + cin >> s >> t; + graph[s].push_back(t); + + } + vector visited(n + 1, false); + visited[1] = true; // 1 号房间开始 + queue que; + que.push(1); // 1 号房间开始 + + // 广度优先搜索的过程 + while (!que.empty()) { + int key = que.front(); que.pop(); + list keys = graph[key]; + for (int key : keys) { + if (!visited[key]) { + que.push(key); + visited[key] = true; + } + } + } + + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + + +## 其他语言版本 + +### Java + +```java + +import java.util.*; + +public class Main { + public static List> adjList = new ArrayList<>(); + + public static void dfs(boolean[] visited, int key) { + if (visited[key]) { + return; + } + visited[key] = true; + List nextKeys = adjList.get(key); + for (int nextKey : nextKeys) { + dfs(visited, nextKey); + } + } + + public static void bfs(boolean[] visited, int key) { + Queue queue = new LinkedList(); + queue.add(key); + visited[key] = true; + while (!queue.isEmpty()) { + int curKey = queue.poll(); + List list = adjList.get(curKey); + for (int nextKey : list) { + if (!visited[nextKey]) { + queue.add(nextKey); + visited[nextKey] = true; + } + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int vertices_num = sc.nextInt(); + int line_num = sc.nextInt(); + for (int i = 0; i < vertices_num; i++) { + adjList.add(new LinkedList<>()); + }//Initialization + for (int i = 0; i < line_num; i++) { + int s = sc.nextInt(); + int t = sc.nextInt(); + adjList.get(s - 1).add(t - 1); + }//构造邻接表 + boolean[] visited = new boolean[vertices_num]; + dfs(visited, 0); +// bfs(visited, 0); + + for (int i = 0; i < vertices_num; i++) { + if (!visited[i]) { + System.out.println(-1); + return; + } + } + System.out.println(1); + } +} + +``` + + +### Python +BFS算法 +```Python +import collections + +path = set() # 纪录 BFS 所经过之节点 + +def bfs(root, graph): + global path + + que = collections.deque([root]) + while que: + cur = que.popleft() + path.add(cur) + + for nei in graph[cur]: + que.append(nei) + graph[cur] = [] + return + +def main(): + N, K = map(int, input().strip().split()) + graph = collections.defaultdict(list) + for _ in range(K): + src, dest = map(int, input().strip().split()) + graph[src].append(dest) + + bfs(1, graph) + if path == {i for i in range(1, N + 1)}: + return 1 + return -1 + + +if __name__ == "__main__": + print(main()) + +``` + +``` python + +def dfs(graph, key, visited): + for neighbor in graph[key]: + if not visited[neighbor]: # Check if the next node is not visited + visited[neighbor] = True + dfs(graph, neighbor, visited) + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + n = int(data[0]) + m = int(data[1]) + + graph = [[] for _ in range(n + 1)] + index = 2 + for _ in range(m): + s = int(data[index]) + t = int(data[index + 1]) + graph[s].append(t) + index += 2 + + visited = [False] * (n + 1) + visited[1] = True # Process node 1 beforehand + dfs(graph, 1, visited) + + for i in range(1, n + 1): + if not visited[i]: + print(-1) + return + + print(1) + +if __name__ == "__main__": + main() + + +``` + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" +) + +func dfs(graph [][]int, key int, visited []bool) { + visited[key] = true + for _, neighbor := range graph[key] { + if !visited[neighbor] { + dfs(graph, neighbor, visited) + } + } +} + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + var n, m int + fmt.Sscanf(scanner.Text(), "%d %d", &n, &m) + + graph := make([][]int, n+1) + for i := 0; i <= n; i++ { + graph[i] = make([]int, 0) + } + + for i := 0; i < m; i++ { + scanner.Scan() + var s, t int + fmt.Sscanf(scanner.Text(), "%d %d", &s, &t) + graph[s] = append(graph[s], t) + } + + visited := make([]bool, n+1) + + dfs(graph, 1, visited) + + for i := 1; i <= n; i++ { + if !visited[i] { + fmt.Println(-1) + return + } + } + fmt.Println(1) +} + + +``` + + +### Rust + +### JavaScript + +```javascript +const rl = require('readline').createInterface({ + input:process.stdin, + output:process.stdout +}) + +let inputLines = [] + +rl.on('line' , (line)=>{ + inputLines.push(line) +}) + +rl.on('close',()=>{ + let [n , edgesCount]= inputLines[0].trim().split(' ').map(Number) + + let graph = Array.from({length:n+1} , ()=>{return[]}) + + for(let i = 1 ; i < inputLines.length ; i++ ){ + let [from , to] = inputLines[i].trim().split(' ').map(Number) + graph[from].push(to) + } + + let visited = new Array(n + 1).fill(false) + + let dfs = (graph , key , visited)=>{ + if(visited[key]){ + return + } + + visited[key] = true + for(let nextKey of graph[key]){ + dfs(graph,nextKey , visited) + } + } + + dfs(graph , 1 , visited) + + for(let i = 1 ; i <= n;i++){ + if(visited[i] === false){ + console.log(-1) + return + } + } + console.log(1) + +}) +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" "b/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" new file mode 100644 index 0000000000..4492d5cd6d --- /dev/null +++ "b/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" @@ -0,0 +1,362 @@ + +

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

+ +# 106. 岛屿的周长 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1178) + +题目描述 + +给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。 + + +你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。 + +输入描述 + +第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述 + +输出一个整数,表示岛屿的周长。 + +输入示例 + +``` +5 5 +0 0 0 0 0 +0 1 0 1 0 +0 1 1 1 0 +0 1 1 1 0 +0 0 0 0 0 +``` + +输出示例 + +14 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240524115244.png) + +岛屿的周长为 14。 + +数据范围: + +1 <= M, N <= 50。 + +## 思路 + +岛屿问题最容易让人想到BFS或者DFS,但本题确实还用不上。 + +为了避免大家惯性思维,所以给大家安排了这道题目。 + +### 解法一: + +遍历每一个空格,遇到岛屿则计算其上下左右的空格情况。 + +如果该陆地上下左右的空格是有水域,则说明是一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240524115933.png) + +陆地的右边空格是水域,则说明找到一条边。 + + +如果该陆地上下左右的空格出界了,则说明是一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240524120105.png) + +该陆地的下边空格出界了,则说明找到一条边。 + + +C++代码如下:(详细注释) + +```CPP +#include +#include +using namespace std; +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]; + } + } + int direction[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) { + for (int k = 0; k < 4; k++) { // 上下左右四个方向 + int x = i + direction[k][0]; + int y = j + direction[k][1]; // 计算周边坐标x,y + if (x < 0 // x在边界上 + || x >= grid.size() // x在边界上 + || y < 0 // y在边界上 + || y >= grid[0].size() // y在边界上 + || grid[x][y] == 0) { // x,y位置是水域 + result++; + } + } + } + } + } + cout << result << endl; + +} +``` + +### 解法二: + +计算出总的岛屿数量,总的变数为:岛屿数量 * 4 + +因为有一对相邻两个陆地,边的总数就要减2,如图红线部分,有两个陆地相邻,总边数就要减2 + +![](https://file1.kamacoder.com/i/algo/20240524120855.png) + +那么只需要在计算出相邻岛屿的数量就可以了,相邻岛屿数量为cover。 + +结果 result = 岛屿数量 * 4 - cover * 2; + +C++代码如下:(详细注释) + +```CPP +#include +#include +using namespace std; +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]; + } + } + int sum = 0; // 陆地数量 + int cover = 0; // 相邻数量 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) { + sum++; // 统计总的陆地数量 + // 统计上边相邻陆地 + if(i - 1 >= 0 && grid[i - 1][j] == 1) cover++; + // 统计左边相邻陆地 + if(j - 1 >= 0 && grid[i][j - 1] == 1) cover++; + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + + cout << sum * 4 - cover * 2 << endl; + +} +``` + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 每次遍历到1,探索其周围4个方向,并记录周长,最终合计 + // 声明全局变量,dirs表示4个方向 + static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + // 统计每单个1的周长 + static int count; + + // 探索其周围4个方向,并记录周长 + public static void helper(int[][] grid, int x, int y) { + for (int[] dir : dirs) { + int nx = x + dir[0]; + int ny = y + dir[1]; + + // 遇到边界或者水,周长加一 + if (nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length + || grid[nx][ny] == 0) { + count++; + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + + // 接收输入 + int M = sc.nextInt(); + int N = sc.nextInt(); + + int[][] grid = new int[M][N]; + for (int i = 0; i < M; i++) { + for (int j = 0; j < N; j++) { + grid[i][j] = sc.nextInt(); + } + } + + int result = 0; // 总周长 + for (int i = 0; i < M; i++) { + for (int j = 0; j < N; j++) { + if (grid[i][j] == 1) { + count = 0; + helper(grid, i, j); + // 更新总周长 + result += count; + } + } + } + + // 打印结果 + System.out.println(result); + } +} +``` + +### Python + +```python + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + # 读取 n 和 m + n = int(data[0]) + m = int(data[1]) + + # 初始化 grid + grid = [] + index = 2 + for i in range(n): + grid.append([int(data[index + j]) for j in range(m)]) + index += m + + sum_land = 0 # 陆地数量 + cover = 0 # 相邻数量 + + for i in range(n): + for j in range(m): + if grid[i][j] == 1: + sum_land += 1 + # 统计上边相邻陆地 + if i - 1 >= 0 and grid[i - 1][j] == 1: + cover += 1 + # 统计左边相邻陆地 + if j - 1 >= 0 and grid[i][j - 1] == 1: + cover += 1 + # 不统计下边和右边,避免重复计算 + + result = sum_land * 4 - cover * 2 + print(result) + +if __name__ == "__main__": + main() + + +``` + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + line := scanner.Text() + + n, m := parseInput(line) + + // 初始化 grid + grid := make([][]int, n) + for i := range grid { + grid[i] = make([]int, m) + } + + // 读入 grid 数据 + for i := 0; i < n; i++ { + scanner.Scan() + line := scanner.Text() + values := parseLine(line, m) + for j := 0; j < m; j++ { + grid[i][j] = values[j] + } + } + + sum := 0 // 陆地数量 + cover := 0 // 相邻数量 + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 1 { + sum++ // 统计总的陆地数量 + + // 统计上边相邻陆地 + if i-1 >= 0 && grid[i-1][j] == 1 { + cover++ + } + // 统计左边相邻陆地 + if j-1 >= 0 && grid[i][j-1] == 1 { + cover++ + } + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + + fmt.Println(sum*4 - cover*2) +} + +// parseInput 解析 n 和 m +func parseInput(line string) (int, int) { + parts := strings.Split(line, " ") + n, _ := strconv.Atoi(parts[0]) + m, _ := strconv.Atoi(parts[1]) + return n, m +} + +// parseLine 解析一行中的多个值 +func parseLine(line string, count int) []int { + parts := strings.Split(line, " ") + values := make([]int, count) + for i := 0; i < count; i++ { + values[i], _ = strconv.Atoi(parts[i]) + } + return values +} + + +``` + + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" "b/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" new file mode 100644 index 0000000000..9ab3388f17 --- /dev/null +++ "b/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" @@ -0,0 +1,425 @@ + +

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

+ +# 107. 寻找存在的路径 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1179) + +题目描述 + +给定一个包含 n 个节点的无向图中,节点编号从 1 到 n (含 1 和 n )。 + +你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。 + +输入描述 + +第一行包含两个正整数 N 和 M,N 代表节点的个数,M 代表边的个数。  + +后续 M 行,每行两个正整数 s 和 t,代表从节点 s 与节点 t 之间有一条边。 + +最后一行包含两个正整数,代表起始节点 source 和目标节点 destination。 + +输出描述 + +输出一个整数,代表是否存在从节点 source 到节点 destination 的路径。如果存在,输出 1;否则,输出 0。 + +输入示例 + +``` +5 4 +1 2 +1 3 +2 4 +3 4 +1 4 +``` + +输出示例 + +1 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240527104432.png) + +数据范围: + +1 <= M, N <= 100。 + +## 思路 + +本题是并查集基础题目。 如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) + +并查集可以解决什么问题呢? + +主要就是集合问题,**两个节点在不在一个集合,也可以将两个节点添加到一个集合中**。 + +这里整理出我的并查集模板如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 + +// 并查集初始化 +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]); // 路径压缩 +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +以上模板中,只要修改 n 大小就可以。 + +并查集主要有三个功能: + +1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个 +2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 +3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 + +简单介绍并查集之后,我们再来看一下这道题目。 + +为什么说这道题目是并查集基础题目,题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。 + +如何算是同一个集合呢,有边连在一起,就算是一个集合。 + +此时我们就可以直接套用并查集模板。 + +使用 join(int u, int v)将每条边加入到并查集。 + +最后 isSame(int u, int v) 判断是否是同一个根 就可以了。 + +C++代码如下: + +```CPP +#include +#include +using namespace std; + +int n; // 节点数量 +vector father = vector (101, 0); // 按照节点大小定义数组大小 + +// 并查集初始化 +void init() { + for (int i = 1; i <= n; i++) father[i] = i; +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->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 m, s, t, source, destination; + cin >> n >> m; + init(); + while (m--) { + cin >> s >> t; + join(s, t); + } + cin >> source >> destination; + if (isSame(source, destination)) cout << 1 << endl; + else cout << 0 << endl; +} +``` + + +## 其他语言版本 + +### Java + +```Java + +import java.util.*; + +public class Main{ + public static void main(String[] args) { + int N, M; + Scanner scanner = new Scanner(System.in); + N = scanner.nextInt(); + M = scanner.nextInt(); + DisJoint disJoint = new DisJoint(N + 1); + for (int i = 0; i < M; ++i) { + disJoint.join(scanner.nextInt(), scanner.nextInt()); + } + if(disJoint.isSame(scanner.nextInt(), scanner.nextInt())) { + System.out.println("1"); + } else { + System.out.println("0"); + } + } + +} + +//并查集模板 +class DisJoint{ + private int[] father; + + public DisJoint(int N) { + father = new int[N]; + for (int i = 0; i < N; ++i){ + father[i] = i; + } + } + + public int find(int n) { + return n == father[n] ? n : (father[n] = find(father[n])); + } + + public void join (int n, int m) { + n = find(n); + m = find(m); + if (n == m) return; + father[m] = n; + } + + public boolean isSame(int n, int m){ + n = find(n); + m = find(m); + return n == m; + } + +} + +``` + +### Python + +```python + +class UnionFind: + def __init__(self, size): + self.parent = list(range(size + 1)) # 初始化并查集 + + def find(self, u): + if self.parent[u] != u: + self.parent[u] = self.find(self.parent[u]) # 路径压缩 + return self.parent[u] + + def union(self, u, v): + root_u = self.find(u) + root_v = self.find(v) + if root_u != root_v: + self.parent[root_v] = root_u + + def is_same(self, u, v): + return self.find(u) == self.find(v) + + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + index = 0 + n = int(data[index]) + index += 1 + m = int(data[index]) + index += 1 + + uf = UnionFind(n) + + for _ in range(m): + s = int(data[index]) + index += 1 + t = int(data[index]) + index += 1 + uf.union(s, t) + + source = int(data[index]) + index += 1 + destination = int(data[index]) + + if uf.is_same(source, destination): + print(1) + else: + print(0) + +if __name__ == "__main__": + main() + + +``` + + +### Go + +```go + +package main + +import ( + "fmt" +) + +const MaxNodes = 101 + +var n int +var father [MaxNodes]int + +// 初始化并查集 +func initialize() { + for i := 1; i <= n; i++ { + father[i] = i + } +} + +// 并查集里寻根的过程 +func find(u int) int { + if u == father[u] { + return u + } + father[u] = find(father[u]) + return father[u] +} + +// 判断 u 和 v 是否找到同一个根 +func isSame(u, v int) bool { + return find(u) == find(v) +} + +// 将 v->u 这条边加入并查集 +func join(u, v int) { + rootU := find(u) + rootV := find(v) + if rootU != rootV { + father[rootV] = rootU + } +} + +func main() { + var m, s, t, source, destination int + fmt.Scan(&n, &m) + initialize() + for i := 0; i < m; i++ { + fmt.Scan(&s, &t) + join(s, t) + } + fmt.Scan(&source, &destination) + if isSame(source, destination) { + fmt.Println(1) + } else { + fmt.Println(0) + } +} + + +``` + +### Rust + +### JavaScript + +```java +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N, M // 节点数和边数 +let source, destination // 起点 终点 +let father = [] // 并查集 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + [N, M] = line.split(' ').map(Number); + + // 初始化并查集 + father = new Array(N) + init() + + // 读取边信息, 加入并查集 + for (let i = 0; i < M; i++) { + line = await readline() + line = line.split(' ').map(Number) + join(line[0], line[1]) + } + + // 读取起点和终点 + line = await readline(); //JS注意这里的冒号 + [source, destination] = line.split(' ').map(Number) + + if (isSame(source, destination)) return console.log(1); + console.log(0); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" "b/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" new file mode 100644 index 0000000000..de2435073c --- /dev/null +++ "b/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" @@ -0,0 +1,379 @@ + +

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

+ +# 108. 冗余连接 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1181) + +题目描述 + +有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图(其实就是一个线形图),如图: + +![](https://file1.kamacoder.com/i/algo/20240905163122.png) + +现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图 + +![](https://file1.kamacoder.com/i/algo/20240905164721.png) + +先请你找出冗余边,删除后,使该图可以重新变成一棵树。 + +输入描述 + +第一行包含一个整数 N,表示图的节点个数和边的个数。 + +后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。 + +输出描述 + +输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。 + +输入示例 + +``` +3 +1 2 +2 3 +1 3 +``` + +输出示例 + +1 3 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240527110320.png) + +图中的 1 2,2 3,1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输入里最后出现的那条边,所以输出结果为 1 3 + +数据范围: + +1 <= N <= 1000. + +## 思路 + +这道题目也是并查集基础题目。 + +这里我依然降调一下,并查集可以解决什么问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。 + +如果还不了解并查集,可以看这里:[并查集理论基础](./图论并查集理论基础.md) + +我们再来看一下这道题目。 + +题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。 + +如果有多个答案,则返回二维数组中最后出现的边。 + +那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。 + + +如图所示,节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。 + +![](https://file1.kamacoder.com/i/algo/20230604104720.png) + +如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20230604104330.png) + +已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。 + +这个思路清晰之后,代码就很好写了。 + +并查集C++代码如下: + +```CPP +#include +#include +using namespace std; +int n; // 节点数量 +vector father(1001, 0); // 按照节点大小范围定义数组 + +// 并查集初始化 +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]); +} +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} +// 将v->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 s, t; + cin >> n; + init(); + for (int i = 0; i < n; i++) { + cin >> s >> t; + if (isSame(s, t)) { + cout << s << " " << t << endl; + return 0; + } else { + join(s, t); + } + } +} +``` + +可以看出,主函数的代码很少,就判断一下边的两个节点在不在同一个集合就可以了。 + +## 拓展 + +题目要求 “请删除标准输入中最后出现的那条边” ,不少录友疑惑,这代码分明是遇到在同一个根的两个节点立刻就返回了,怎么就求出 最后出现的那条边 了呢。 + +有这种疑惑的录友是 认为发现一条冗余边后,后面还可能会有一条冗余边。 + +其实并不会。 + +题目是在 树的基础上 添加一条边,所以冗余边仅仅是一条。 + +到这一条可能靠前出现,可能靠后出现。 + +例如,题目输入示例: + +输入示例 + +``` +3 +1 2 +2 3 +1 3 +``` + +图: + +![](https://file1.kamacoder.com/i/algo/20240527110320.png) + +输出示例 + +1 3 + +当我们从前向后遍历,优先让前面的边连上,最后判断冗余边就是 1 3。 + +如果我们从后向前便利,优先让后面的边连上,最后判断的冗余边就是 1 2。 + +题目要求“请删除标准输入中最后出现的那条边”,所以 1 3 这条边才是我们要求的。 + + + + +## 其他语言版本 + +### Java + +```java +import java.util.Scanner; + +public class Main { + private static int[] father; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int pointNum = scanner.nextInt(); + father = new int[pointNum + 1]; + init(); + for (int i = 0; i < pointNum; i++) { + join(scanner.nextInt(), scanner.nextInt()); + } + } + + /** + * 并查集初始化 + */ + private static void init() { + for (int i = 1; i < father.length; i++) { + // 让每个元素指向自己 + father[i] = i; + } + } + + /** + * 并查集寻根 + * + * @param u + * @return + */ + private static int find(int u) { + // 判断 u 是否等于自己,如果是的话,直接返回自己 + // 如果不等于自己,就寻找根,寻找的时候,反复进行路径压缩 + return u == father[u] ? u : (father[u] = find(father[u])); + } + + /** + * 判断 u 和 v 是否同根 + * + * @param u + * @param v + * @return + */ + private static boolean isSame(int u, int v) { + return find(u) == find(v); + } + + /** + * 添加 边 到并查集,v 指向 u + * + * @param u + * @param v + */ + private static void join(int u, int v) { + // --if-- 如果两个点已经同根,说明他们的信息已经存储到并查集中了,直接返回即可 + // 寻找u的根 + int uRoot = find(u); + // 寻找v的根 + int vRoot = find(v); + if (uRoot == vRoot) { + // --if-- 如果u,v的根相同,说明两者已经连接了,直接输出 + System.out.println(u + " " + v); + return; + } + // --if-- 将信息添加到并查集 + father[vRoot] = uRoot; + } + +} +``` + + + +### Python + +```python +father = list() + +def find(u): + if u == father[u]: + return u + else: + father[u] = find(father[u]) + return father[u] + +def is_same(u, v): + u = find(u) + v = find(v) + return u == v + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[u] = v + +if __name__ == "__main__": + # 輸入 + n = int(input()) + for i in range(n + 1): + father.append(i) + # 尋找冗余邊 + result = None + for i in range(n): + s, t = map(int, input().split()) + if is_same(s, t): + result = str(s) + ' ' + str(t) + else: + join(s, t) + + # 輸出 + print(result) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N // 节点数和边数 +let father = [] // 并查集 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + N = Number(line); + + // 初始化并查集 + father = new Array(N) + init() + + // 读取边信息, 加入并查集 + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + + if (!isSame(line[0], line[1])) { + join(line[0], line[1]) + }else{ + console.log(line[0], line[1]); + break + } + } +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" "b/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" new file mode 100644 index 0000000000..6ad59c4188 --- /dev/null +++ "b/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" @@ -0,0 +1,599 @@ + +

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

+ +# 109. 冗余连接II + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1182) + +题目描述 + +有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。如图:  + + + +现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。如图: + + + +输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。 + +输入描述 + +第一行输入一个整数 N,表示有向图中节点和边的个数。 + +后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边 + +输出描述 + +输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。 + +输入示例 + +``` +3 +1 2 +1 3 +2 3 +``` + +输出示例 + +2 3 + +提示信息 + + + +在删除 2 3 后有向图可以变为一棵合法的有向树,所以输出 2 3 + +数据范围: + +1 <= N <= 1000. + +## 思路 + +本题与 [108.冗余连接](./0108.冗余连接.md) 类似,但本题是一个有向图,有向图相对要复杂一些。 + +本题的本质是 :有一个有向图,是由一颗有向树 + 一条有向边组成的 (所以此时这个图就不能称之为有向树),现在让我们找到那条边 把这条边删了,让这个图恢复为有向树。 + +还有“**若有多条边可以删除,请输出标准输入中最后出现的一条边**”,这说明在两条边都可以删除的情况下,要删顺序靠后的边! + +我们来想一下 有向树的性质,如果是有向树的话,只有根节点入度为0,其他节点入度都为1(因为该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点)。 + +所以情况一:如果我们找到入度为2的点,那么删一条指向该节点的边就行了。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240527115807.png) + +找到了节点3 的入度为2,删 1 -> 3 或者 2 -> 3 。选择删顺序靠后便可。 + +但 入度为2 还有一种情况,情况二,只能删特定的一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240527151456.png) + +节点3 的入度为 2,但在删除边的时候,只能删 这条边(节点1 -> 节点3),如果删这条边(节点4 -> 节点3),那么删后本图也不是有向树了(因为找不到根节点)。 + +综上,如果发现入度为2的节点,我们需要判断 删除哪一条边,删除后本图能成为有向树。如果是删哪个都可以,优先删顺序靠后的边。 + + +情况三: 如果没有入度为2的点,说明 图中有环了(注意是有向环)。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240527120531.png) + +对于情况三,删掉构成环的边就可以了。 + + +## 写代码 + +把每条边记录下来,并统计节点入度: + +```cpp + int s, t; + vector> edges; + cin >> n; + vector inDegree(n + 1, 0); // 记录节点入度 + for (int i = 0; i < n; i++) { + cin >> s >> t; + inDegree[t]++; + edges.push_back({s, t}); + } + + +``` + +前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案。 + +同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为树,就删最后那一条。 + +代码如下: + +```cpp +vector vec; // 记录入度为2的边(如果有的话就两条边) +// 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 +for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } +} +if (vec.size() > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + cout << edges[vec[0]][0] << " " << edges[vec[0]][1]; + } else { + cout << edges[vec[1]][0] << " " << edges[vec[1]][1]; + } + return 0; +} +``` + +再来看情况三,明确没有入度为2的情况,那么一定有向环,找到构成环的边就是要删除的边。 + +可以定义一个函数,代码如下: + +```cpp +// 在有向图里找到删除的那条边,使其变成树 +void getRemoveEdge(const vector>& edges) +``` + +大家应该知道了,我们要解决本题要实现两个最为关键的函数: + +* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树 +* `getRemoveEdge()` 确定图中一定有了有向环,那么要找到需要删除的那条边 + +此时就用到**并查集**了。 + +如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) + +`isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树: 将所有边的两端节点分别加入并查集,遇到要 要删除的边则跳过,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。 + +如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除该条边 还是一个有向树 + +`getRemoveEdge()`确定图中一定有了有向环,那么要找到需要删除的那条边: 将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。 + +本题C++代码如下:(详细注释了) + + +```cpp +#include +#include +using namespace std; +int n; +vector father (1001, 0); +// 并查集初始化 +void init() { + for (int i = 1; i <= n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; +} +// 判断 u 和 v是否找到同一个根 +bool same(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 在有向图里找到删除的那条边,使其变成树 +void getRemoveEdge(const vector>& edges) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { // 遍历所有的边 + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + cout << edges[i][0] << " " << edges[i][1]; + return; + } else { + join(edges[i][0], edges[i][1]); + } + } +} + +// 删一条边之后判断是不是树 +bool isTreeAfterRemoveEdge(const vector>& edges, int deleteEdge) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { + if (i == deleteEdge) continue; + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false; + } + join(edges[i][0], edges[i][1]); + } + return true; +} + +int main() { + int s, t; + vector> edges; + cin >> n; + vector inDegree(n + 1, 0); // 记录节点入度 + for (int i = 0; i < n; i++) { + cin >> s >> t; + inDegree[t]++; + edges.push_back({s, t}); + } + + vector vec; // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 + for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } + } + // 情况一、情况二 + if (vec.size() > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + cout << edges[vec[0]][0] << " " << edges[vec[0]][1]; + } else { + cout << edges[vec[1]][0] << " " << edges[vec[1]][1]; + } + return 0; + } + + // 处理情况三 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + getRemoveEdge(edges); +} +``` + +## 其他语言版本 + +### Java + +```java +import java.util.*; + +/* + * 冗余连接II。主要问题是存在入度为2或者成环,也可能两个问题同时存在。 + * 1.判断入度为2的边 + * 2.判断是否成环(并查集) + */ + +public class Main { + /** + * 并查集模板 + */ + static class Disjoint { + + private final int[] father; + + public Disjoint(int n) { + father = new int[n]; + for (int i = 0; i < n; i++) { + father[i] = i; + } + } + + public void join(int n, int m) { + n = find(n); + m = find(m); + if (n == m) return; + father[n] = m; + } + + public int find(int n) { + return father[n] == n ? n : (father[n] = find(father[n])); + } + + public boolean isSame(int n, int m) { + return find(n) == find(m); + } + } + + static class Edge { + int s; + int t; + + public Edge(int s, int t) { + this.s = s; + this.t = t; + } + } + + static class Node { + int id; + int in; + int out; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + List edges = new ArrayList<>(); + Node[] nodeMap = new Node[n + 1]; + for (int i = 1; i <= n; i++) { + nodeMap[i] = new Node(); + } + Integer doubleIn = null; + for (int i = 0; i < n; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + //记录入度 + nodeMap[t].in++; + if (!(nodeMap[t].in < 2)) doubleIn = t; + Edge edge = new Edge(s, t); + edges.add(edge); + } + Edge result = null; + //存在入度为2的节点,既要消除入度为2的问题同时解除可能存在的环 + if (doubleIn != null) { + List doubleInEdges = new ArrayList<>(); + for (Edge edge : edges) { + if (edge.t == doubleIn) doubleInEdges.add(edge); + if (doubleInEdges.size() == 2) break; + } + Edge edge = doubleInEdges.get(1); + if (isTreeWithExclude(edges, edge, nodeMap)) { + result = edge; + } else { + result = doubleInEdges.get(0); + } + } else { + //不存在入度为2的节点,则只需要解除环即可 + result = getRemoveEdge(edges, nodeMap); + } + + System.out.println(result.s + " " + result.t); + } + + public static boolean isTreeWithExclude(List edges, Edge exculdEdge, Node[] nodeMap) { + Disjoint disjoint = new Disjoint(nodeMap.length + 1); + for (Edge edge : edges) { + if (edge == exculdEdge) continue; + //成环则不是树 + if (disjoint.isSame(edge.s, edge.t)) { + return false; + } + disjoint.join(edge.s, edge.t); + } + return true; + } + + public static Edge getRemoveEdge(List edges, Node[] nodeMap) { + int length = nodeMap.length; + Disjoint disjoint = new Disjoint(length); + + for (Edge edge : edges) { + if (disjoint.isSame(edge.s, edge.t)) return edge; + disjoint.join(edge.s, edge.t); + } + return null; + } + +} + +``` + +### Python + +```python +from collections import defaultdict + +father = list() + + +def find(u): + if u == father[u]: + return u + else: + father[u] = find(father[u]) + return father[u] + + +def is_same(u, v): + u = find(u) + v = find(v) + return u == v + + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[u] = v + + +def is_tree_after_remove_edge(edges, edge, n): + # 初始化并查集 + global father + father = [i for i in range(n + 1)] + + for i in range(len(edges)): + if i == edge: + continue + s, t = edges[i] + if is_same(s, t): # 成環,即不是有向樹 + return False + else: # 將s,t放入集合中 + join(s, t) + return True + + +def get_remove_edge(edges): + # 初始化并查集 + global father + father = [i for i in range(n + 1)] + + for s, t in edges: + if is_same(s, t): + print(s, t) + return + else: + join(s, t) + + +if __name__ == "__main__": + # 輸入 + n = int(input()) + edges = list() + in_degree = defaultdict(int) + + for i in range(n): + s, t = map(int, input().split()) + in_degree[t] += 1 + edges.append([s, t]) + + # 尋找入度為2的邊,並紀錄其下標(index) + vec = list() + for i in range(n - 1, -1, -1): + if in_degree[edges[i][1]] == 2: + vec.append(i) + + # 輸出 + if len(vec) > 0: + # 情況一:刪除輸出順序靠後的邊 + if is_tree_after_remove_edge(edges, vec[0], n): + print(edges[vec[0]][0], edges[vec[0]][1]) + # 情況二:只能刪除特定的邊 + else: + print(edges[vec[1]][0], edges[vec[1]][1]) + else: + # 情況三: 原圖有環 + get_remove_edge(edges) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N // 节点数和边数 +let father = [] // 并查集 +let edges = [] // 边集 +let inDegree = [] // 入度 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + +// 判断删除一条边后是不是树 +const isTreeAfterRemoveEdge = (edges, edge) => { + // 初始化并查集 + init() + + for (let i = 0; i < N; i++) { + if (i == edge) continue + if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false + } + join(edges[i][0], edges[i][1]) + } + return true +} + +// 在有向图里找到删除的那条边, 使其成为树 +const getRemoveEdge = (edges) => { + // 初始化并查集 + init() + + for (let i = 0; i < N; i++) { + if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + console.log(edges[i][0], edges[i][1]); + return + } else { + join(edges[i][0], edges[i][1]) + } + } +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + N = Number(line); + + // 读取边信息, 统计入度 + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + + edges.push(line) + + inDegree[line[1]] = (inDegree[line[1]] || 0) + 1 + } + + // 找到入度为2的节点 + let vec = [] // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 + for (let i = N - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push(i) + } + } + + // 情况一、情况二 + if (vec.length > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + console.log(edges[vec[0]][0], edges[vec[0]][1]); + } else { + console.log(edges[vec[1]][0], edges[vec[1]][1]); + } + return 0 + } + + // 情况三 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + getRemoveEdge(edges) +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" "b/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" new file mode 100644 index 0000000000..6cb5886d20 --- /dev/null +++ "b/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" @@ -0,0 +1,363 @@ + +

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

+ +# 110. 字符串接龙 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1183) + +题目描述 + +字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列: + +1. 序列中第一个字符串是 beginStr。 + +2. 序列中最后一个字符串是 endStr。 + +3. **每次转换只能改变一个位置的字符**(例如 ftr 可以转化 fty ,但 ftr 不能转化 frx)。 + +4. 转换过程中的中间字符串必须是字典 strList 中的字符串。 + +5. beginStr 和 endStr 不在 字典 strList 中 + +6. 字符串中只有小写的26个字母 + +给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。 + +输入描述 + +第一行包含一个整数 N,表示字典 strList 中的字符串数量。 第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。 后续 N 行,每行一个字符串,代表 strList 中的字符串。 + +输出描述 + +输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。 + +输入示例 + +``` +6 +abc def +efc +dbc +ebc +dec +dfc +yhn +``` + +输出示例 + +4 + +提示信息 + +从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4 + +数据范围: + +2 <= N <= 500 + +

+ +

+ + +## 思路 + +以示例1为例,从这个图中可以看出 abc 到 def的路线 不止一条,但最短的一条路径上是4个节点。 + +![](https://file1.kamacoder.com/i/algo/20250317105155.png) + + +本题只需要求出最短路径的长度就可以了,不用找出具体路径。 + +所以这道题要解决两个问题: + +1、图中的线是如何连在一起的 + +在搜索的过程中,我们可以枚举,用26个字母替换当前字符串的每一个字符,在看替换后 是否在 strList里出现过,就可以判断 两个字符串 是否是链接的。 + +2、起点和终点的最短路径长度 + +首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个。 + +所以判断点与点之间的关系,需要判断是不是差一个字符,**如果差一个字符,那就是有链接**。 + +然后就是求起点和终点的最短路径长度,在无权图中,求最短路,用深搜或者广搜就行,没必要用最短路算法。 + +**在无权图中,用广搜求最短路最为合适,广搜只要搜到了终点,那么一定是最短的路径**。因为广搜就是以起点中心向四周扩散的搜索。 + +**本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路**。 + +而广搜只要达到终点,一定是最短路。 + +另外需要有一个注意点: + +* 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环! +* 使用set来检查字符串是否出现在字符串集合里更快一些 + +C++代码如下:(详细注释) + +```CPP +#include +#include +#include +#include +#include +#include +using namespace std; +int main() { + string beginStr, endStr, str; + int n; + cin >> n; + unordered_set strSet; + cin >> beginStr >> endStr; + for (int i = 0; i < n; i++) { + cin >> str; + strSet.insert(str); + } + + // 记录strSet里的字符串是否被访问过,同时记录路径长度 + unordered_map visitMap; // <记录的字符串,路径长度> + + // 初始化队列 + queue que; + que.push(beginStr); + + // 初始化visitMap + visitMap.insert(pair(beginStr, 1)); + + while(!que.empty()) { + string word = que.front(); + que.pop(); + int path = visitMap[word]; // 这个字符串在路径中的长度 + + // 开始在这个str中,挨个字符去替换 + for (int i = 0; i < word.size(); i++) { + string newWord = word; // 用一个新字符串替换str,因为每次要置换一个字符 + + // 遍历26的字母 + for (int j = 0 ; j < 26; j++) { + newWord[i] = j + 'a'; + if (newWord == endStr) { // 发现替换字母后,字符串与终点字符串相同 + cout << path + 1 << endl; // 找到了路径 + return 0; + } + // 字符串集合里出现了newWord,并且newWord没有被访问过 + if (strSet.find(newWord) != strSet.end() + && visitMap.find(newWord) == visitMap.end()) { + // 添加访问信息,并将新字符串放到队列中 + visitMap.insert(pair(newWord, path + 1)); + que.push(newWord); + } + } + } + } + + // 没找到输出0 + cout << 0 << endl; + +} +``` + +当然本题也可以用双向BFS,就是从头尾两端进行搜索,大家感兴趣,可以自己去实现,这里就不再做详细讲解了。 + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + scanner.nextLine(); + String beginStr = scanner.next(); + String endStr = scanner.next(); + scanner.nextLine(); + List wordList = new ArrayList<>(); + wordList.add(beginStr); + wordList.add(endStr); + for (int i = 0; i < n; i++) { + wordList.add(scanner.nextLine()); + } + int count = bfs(beginStr, endStr, wordList); + System.out.println(count); + } + + /** + * 广度优先搜索-寻找最短路径 + */ + public static int bfs(String beginStr, String endStr, List wordList) { + int len = 1; + Set set = new HashSet<>(wordList); + Set visited = new HashSet<>(); + Queue q = new LinkedList<>(); + visited.add(beginStr); + q.add(beginStr); + q.add(null); + while (!q.isEmpty()) { + String node = q.remove(); + //上一层结束,若下一层还有节点进入下一层 + if (node == null) { + if (!q.isEmpty()) { + len++; + q.add(null); + } + continue; + } + char[] charArray = node.toCharArray(); + //寻找邻接节点 + for (int i = 0; i < charArray.length; i++) { + //记录旧值,用于回滚修改 + char old = charArray[i]; + for (char j = 'a'; j <= 'z'; j++) { + charArray[i] = j; + String newWord = new String(charArray); + if (set.contains(newWord) && !visited.contains(newWord)) { + q.add(newWord); + visited.add(newWord); + //找到结尾 + if (newWord.equals(endStr)) return len + 1; + } + } + charArray[i] = old; + } + } + return 0; + } +} + +``` + +### Python +```Python +def judge(s1,s2): + count=0 + for i in range(len(s1)): + if s1[i]!=s2[i]: + count+=1 + return count==1 + +if __name__=='__main__': + n=int(input()) + beginstr,endstr=map(str,input().split()) + if beginstr==endstr: + print(0) + exit() + strlist=[] + for i in range(n): + strlist.append(input()) + + # use bfs + visit=[False for i in range(n)] + queue=[[beginstr,1]] + while queue: + str,step=queue.pop(0) + if judge(str,endstr): + print(step+1) + exit() + for i in range(n): + if visit[i]==False and judge(strlist[i],str): + visit[i]=True + queue.append([strlist[i],step+1]) + print(0) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let N //输入字符串个数 +let beginStr //开始字符串 +let endStr //结束字符串 +let strSet = new Set() //字符串集合 +let visitedMap = new Map() //访问过的字符串 + +// 读取输入,初始化地图 +const init = async () => { + let line = await readline(); + line = line.trim() + N = Number(line); + + line = await readline(); + line = line.trim().split(' ') + beginStr = line[0] + endStr = line[1] + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.trim() + strSet.add(line.trim()) + } +} + +(async function () { + + // 读取输入,初始化数据 + await init() + + // 初始化队列 + let queue = [] + queue.push(beginStr) + + // 初始化visitMap + visitedMap.set(beginStr, 1) + + while (queue.length) { + let word = queue.shift() + let path = visitedMap.get(word) + + // 遍历26个字母 + for (let i = 0; i < word.length; i++) { + let newWord = word.split('') // 用一个新字符串替换str,因为每次要置换一个字符 + for (let j = 0; j < 26; j++) { + newWord[i] = String.fromCharCode('a'.charCodeAt() + j) + // 发现替换字母后,字符串与终点字符串相同 + if (newWord.join('') === endStr) { + console.log(path + 1); + return 0; // 找到了路径 + } + + // 字符串集合里出现了newWord,并且newWord没有被访问过 + if (strSet.has(newWord.join('')) && !visitedMap.has(newWord.join(''))) { + // 添加访问信息,并将新字符串放到队列中 + queue.push(newWord.join('')) + visitedMap.set(newWord.join(''), path + 1) + } + } + } + } + + console.log(0); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" "b/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" new file mode 100644 index 0000000000..c5650d9708 --- /dev/null +++ "b/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" @@ -0,0 +1,542 @@ + +

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

+ +# 拓扑排序精讲 + +[卡码网:117. 软件构建](https://kamacoder.com/problempage.php?pid=1191) + +题目描述: + +某个大型软件项目的构建系统拥有 N 个文件,文件编号从 0 到 N - 1,在这些文件中,某些文件依赖于其他文件的内容,这意味着如果文件 A 依赖于文件 B,则必须在处理文件 A 之前处理文件 B (0 <= A, B <= N - 1)。请编写一个算法,用于确定文件处理的顺序。 + +输入描述: + +第一行输入两个正整数 N, M。表示 N 个文件之间拥有 M 条依赖关系。 + +后续 M 行,每行两个正整数 S 和 T,表示 T 文件依赖于 S 文件。 + +输出描述: + +输出共一行,如果能处理成功,则输出文件顺序,用空格隔开。 + +如果不能成功处理(相互依赖),则输出 -1。 + +输入示例: + +``` +5 4 +0 1 +0 2 +1 3 +2 4 +``` + +输出示例: + +0 1 2 3 4 + +提示信息: + +文件依赖关系如下: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240510110836.png) + +做拓扑排序的话,如果肉眼去找开头的节点,一定能找到 节点0 吧,都知道要从节点0 开始。 + +但为什么我们能找到 节点0呢,因为我们肉眼看着 这个图就是从 节点0出发的。 + +作为出发节点,它有什么特征? + +你看节点0 的入度 为0 出度为2, 也就是 没有边指向它,而它有两条边是指出去的。 + +> 节点的入度表示 有多少条边指向它,节点的出度表示有多少条边 从该节点出发。 + +所以当我们做拓扑排序的时候,应该优先找 入度为 0 的节点,只有入度为0,它才是出发节点。 +**理解以上内容很重要**! + +接下来我给出 拓扑排序的过程,其实就两步: + +1. 找到入度为0 的节点,加入结果集 +2. 将该节点从图中移除 + +循环以上两步,直到 所有节点都在图中被移除了。 + +结果集的顺序,就是我们想要的拓扑排序顺序 (结果集里顺序可能不唯一) + +## 模拟过程 + +用本题的示例来模拟这一过程: + + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113110.png) + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510113142.png) + +---------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113345.png) + +这里大家会发现,节点1 和 节点2 入度都为0, 选哪个呢? + +选哪个都行,所以这也是为什么拓扑排序的结果是不唯一的。 + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510113640.png) + +--------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113853.png) + +节点2 和 节点3 入度都为0,选哪个都行,这里选节点2 + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510114004.png) + +-------------- + +后面的过程一样的,节点3 和 节点4,入度都为0,选哪个都行。 + +最后结果集为: 0 1 2 3 4 。当然结果不唯一的。 + +## 判断有环 + +如果有 有向环怎么办呢?例如这个图: + +![](https://file1.kamacoder.com/i/algo/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://file1.kamacoder.com/i/algo/20240510113110.png) + +这事节点1 和 节点2 的入度为 1。 + +将节点0删除后,图为这样: + +![](https://file1.kamacoder.com/i/algo/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 - 1; i++) cout << result[i] << " "; + cout << result[n - 1]; + } else cout << -1 << endl; + + +} +``` + +## 其他语言版本 + +### Java + +```java +import java.util.*; + + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + List> umap = new ArrayList<>(); // 记录文件依赖关系 + int[] inDegree = new int[n]; // 记录每个文件的入度 + + for (int i = 0; i < n; i++) + umap.add(new ArrayList<>()); + + for (int i = 0; i < m; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + umap.get(s).add(t); // 记录s指向哪些文件 + inDegree[t]++; // t的入度加一 + } + + Queue queue = new LinkedList<>(); + for (int i = 0; i < n; i++) { + if (inDegree[i] == 0) { + // 入度为0的文件,可以作为开头,先加入队列 + queue.add(i); + } + } + + List result = new ArrayList<>(); + + // 拓扑排序 + while (!queue.isEmpty()) { + int cur = queue.poll(); // 当前选中的文件 + result.add(cur); + for (int file : umap.get(cur)) { + inDegree[file]--; // cur的指向的文件入度-1 + if (inDegree[file] == 0) { + queue.add(file); + } + } + } + + if (result.size() == n) { + for (int i = 0; i < result.size(); i++) { + System.out.print(result.get(i)); + if (i < result.size() - 1) { + System.out.print(" "); + } + } + } else { + System.out.println(-1); + } + } +} +``` + + + +### Python + +```python +from collections import deque, defaultdict + +def topological_sort(n, edges): + inDegree = [0] * n # inDegree 记录每个文件的入度 + umap = defaultdict(list) # 记录文件依赖关系 + + # 构建图和入度表 + for s, t in edges: + inDegree[t] += 1 + umap[s].append(t) + + # 初始化队列,加入所有入度为0的节点 + queue = deque([i for i in range(n) if inDegree[i] == 0]) + result = [] + + while queue: + cur = queue.popleft() # 当前选中的文件 + result.append(cur) + for file in umap[cur]: # 获取该文件指向的文件 + inDegree[file] -= 1 # cur的指向的文件入度-1 + if inDegree[file] == 0: + queue.append(file) + + if len(result) == n: + print(" ".join(map(str, result))) + else: + print(-1) + + +if __name__ == "__main__": + n, m = map(int, input().split()) + edges = [tuple(map(int, input().split())) for _ in range(m)] + topological_sort(n, edges) +``` + + + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N, M // 节点数和边数 +let inDegrees = [] // 入度 +let umap = new Map() // 记录文件依赖关系 +let result = [] // 结果 + + +// 根据输入, 初始化数据 +const init = async () => { + // 读取第一行输入 + let line = await readline(); + [N, M] = line.split(' ').map(Number) + + inDegrees = new Array(N).fill(0) + + // 读取边集 + while (M--) { + line = await readline(); + let [x, y] = line.split(' ').map(Number) + + // 记录入度 + inDegrees[y]++ + + // 记录x指向哪些文件 + if (!umap.has(x)) { + umap.set(x, [y]) + } else { + umap.get(x).push(y) + } + } +} + +(async function () { + // 根据输入, 初始化数据 + await init() + + let queue = [] // 入度为0的节点 + for (let i = 0; i < N; i++) { + if (inDegrees[i] == 0) { + queue.push(i) + } + } + + while (queue.length) { + let cur = queue.shift() //当前文件 + + result.push(cur) + + let files = umap.get(cur) // 当前文件指向的文件 + + // 当前文件指向的文件入度减1 + if (files && files.length) { + for (let i = 0; i < files.length; i++) { + inDegrees[files[i]]-- + if (inDegrees[files[i]] == 0) queue.push(files[i]) + } + } + } + + // 这里result.length == N 一定要判断, 因为可能存在环 + if (result.length == N) return console.log(result.join(' ')) + console.log(-1) +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" "b/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" new file mode 100644 index 0000000000..6669ce7ba1 --- /dev/null +++ "b/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" @@ -0,0 +1,687 @@ + +# A * 算法精讲 (A star算法) + +[卡码网:126. 骑士的攻击](https://kamacoder.com/problempage.php?pid=1203) + +题目描述 + +在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。 + +骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。 + +![](https://file1.kamacoder.com/i/algo/20240626104833.png) + +棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界) + +输入描述 + +第一行包含一个整数 n,表示测试用例的数量。 + +接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。 + +输出描述 + +输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。 + +输入示例 + +``` +6 +5 2 5 4 +1 1 2 2 +1 1 8 8 +1 1 8 7 +2 1 3 3 +4 6 4 6 +``` + +输出示例 + +``` +2 +4 +6 +5 +1 +0 +``` + + +## 思路 + +我们看到这道题目的第一个想法就是广搜,这也是最经典的广搜类型题目。 + +这里我直接给出广搜的C++代码: + +```CPP +#include +#include +#include +using namespace std; +int moves[1001][1001]; +int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2}; +void bfs(int a1,int a2, int b1, int b2) +{ + queue q; + q.push(a1); + q.push(a2); + while(!q.empty()) + { + int m=q.front(); q.pop(); + int n=q.front(); q.pop(); + if(m == b1 && n == b2) + break; + for(int i=0;i<8;i++) + { + int mm=m + dir[i][0]; + int nn=n + dir[i][1]; + if(mm < 1 || mm > 1000 || nn < 1 || nn > 1000) + continue; + if(!moves[mm][nn]) + { + moves[mm][nn]=moves[m][n]+1; + q.push(mm); + q.push(nn); + } + } + } +} + +int main() +{ + int n, a1, a2, b1, b2; + cin >> n; + while (n--) { + cin >> a1 >> a2 >> b1 >> b2; + memset(moves,0,sizeof(moves)); + bfs(a1, a2, b1, b2); + cout << moves[b1][b2] << endl; + } + return 0; +} + +``` + +提交后,大家会发现,超时了。 + +因为本题地图足够大,且 n 也有可能很大,导致有非常多的查询。 + +我们来看一下广搜的搜索过程,如图,红色是起点,绿色是终点,黄色是要遍历的点,最后从 起点 找到 达到终点的最短路径是棕色。 + +![](https://file1.kamacoder.com/i/algo/20240611143712.png) + +可以看出 广搜中,做了很多无用的遍历, 黄色的格子是广搜遍历到的点。 + +这里我们能不能让便利方向,向这终点的方向去遍历呢? + +这样我们就可以避免很多无用遍历。 + + +## Astar + +Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。 + +其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密) + +如果是有权图(边有不同的权值),优先考虑 dijkstra。 + +而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。 + +以下,我用BFS版本的A * 来进行讲解。 + +在BFS中,我们想搜索,从起点到终点的最短路径,要一层一层去遍历。 + +![](https://file1.kamacoder.com/i/algo/20240611143712.png) + +如果 使用A * 的话,其搜索过程是这样的,如图,图中着色的都是我们要遍历的点。 + +![](https://file1.kamacoder.com/i/algo/20240611195223.png) + + +(上面两图中 最短路长度都是8,只是走的方式不同而已) + +大家可以发现 **BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索**。 + +看出 A * 可以节省很多没有必要的遍历步骤。 + +为了让大家可以明显看到区别,我将 BFS 和 A * 制作成可视化动图,大家可以自己看看动图,效果更好。 + +地址:https://kamacoder.com/tools/knight.html + +那么 A * 为什么可以有方向性的去搜索,它的如何知道方向呢? + +**其关键在于 启发式函数**。 + +那么启发式函数落实到代码处,如果指引搜索的方向? + +在本篇开篇中给出了BFS代码,指引 搜索的方向的关键代码在这里: + +```CPP +int m=q.front();q.pop(); +int n=q.front();q.pop(); +``` + +从队列里取出什么元素,接下来就是从哪里开始搜索。 + +**所以 启发式函数 要影响的就是队列里元素的排序**! + +这是影响BFS搜索方向的关键。 + +对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢? + +每个节点的权值为F,给出公式为:F = G + H + +G:起点达到目前遍历节点的距离 + +H:目前遍历的节点到达终点的距离 + +起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。 + +本题的图是无权网格状,在计算两点距离通常有如下三种计算方式: + +1. 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2) +2. 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 ) +3. 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2)) + +x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号, + +选择哪一种距离计算方式 也会导致 A * 算法的结果不同。 + +本题,采用欧拉距离才能最大程度体现 点与点之间的距离。 + +所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的) + +我在制作动画演示的过程中,分别给出了曼哈顿、欧拉以及契比雪夫 三种计算方式下,A * 算法的寻路过程,大家可以自己看看看其区别。 + +动画地址:https://kamacoder.com/tools/knight.html + +计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。 + +可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。 + +实现代码如下:(启发式函数 采用 欧拉距离计算方式) + +```CPP +#include +#include +#include +using namespace std; +int moves[1001][1001]; +int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2}; +int b1, b2; +// F = G + H +// G = 从起点到该节点路径消耗 +// H = 该节点到终点的预估消耗 + +struct Knight{ + int x,y; + int g,h,f; + bool operator < (const Knight & k) const{ // 重载运算符, 从小到大排序 + return k.f < f; + } +}; + +priority_queue que; + +int Heuristic(const Knight& k) { // 欧拉距离 + return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2); // 统一不开根号,这样可以提高精度 +} +void astar(const Knight& k) +{ + Knight cur, next; + que.push(k); + while(!que.empty()) + { + cur=que.top(); que.pop(); + if(cur.x == b1 && cur.y == b2) + break; + for(int i = 0; i < 8; i++) + { + next.x = cur.x + dir[i][0]; + next.y = cur.y + dir[i][1]; + if(next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) + continue; + if(!moves[next.x][next.y]) + { + moves[next.x][next.y] = moves[cur.x][cur.y] + 1; + + // 开始计算F + next.g = cur.g + 5; // 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5 + next.h = Heuristic(next); + next.f = next.g + next.h; + que.push(next); + } + } + } +} + +int main() +{ + int n, a1, a2; + cin >> n; + while (n--) { + cin >> a1 >> a2 >> b1 >> b2; + memset(moves,0,sizeof(moves)); + Knight start; + start.x = a1; + start.y = a2; + start.g = 0; + start.h = Heuristic(start); + start.f = start.g + start.h; + astar(start); + while(!que.empty()) que.pop(); // 队列清空 + cout << moves[b1][b2] << endl; + } + return 0; +} + +``` + +## 复杂度分析 + +A * 算法的时间复杂度 其实是不好去量化的,因为他取决于 启发式函数怎么写。 + +最坏情况下,A * 退化成广搜,算法的时间复杂度 是 O(n * 2),n 为节点数量。 + +最佳情况,是从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度。 + +因为在搜索的过程中也需要堆排序,所以是 O(dlogd)。 + +实际上 A * 的时间复杂度是介于 最优 和最坏 情况之间, 可以 非常粗略的认为 A * 算法的时间复杂度是 O(nlogn) ,n 为节点数量。 + +A * 算法的空间复杂度 O(b ^ d) ,d 为起点到终点的深度,b 是 图中节点间的连接数量,本题因为是无权网格图,所以 节点间连接数量为 4。 + + +## 拓展 + +如果本题大家使用 曼哈顿距离 或者 切比雪夫距离 计算的话,可以提交试一试,有的最短路结果是并不是最短的。 + +原因也是 曼哈顿 和 切比雪夫这两种计算方式在 本题的网格地图中,都没有体现出点到点的真正距离! + +可能有些录友找到类似的题目,例如 [poj 2243](http://poj.org/problem?id=2243),使用 曼哈顿距离 提交也过了, 那是因为题目中的地图太小了,仅仅是一张 8 * 8的地图,根本看不出来 不同启发式函数写法的区别。 + +A * 算法 并不是一个明确的最短路算法,**A * 算法搜的路径如何,完全取决于 启发式函数怎么写**。 + +**A * 算法并不能保证一定是最短路**,因为在设计 启发式函数的时候,要考虑 时间效率与准确度之间的一个权衡。 + +虽然本题中,A * 算法得到是最短路,也是因为本题 启发式函数 和 地图结构都是最简单的。 + +例如在游戏中,在地图很大、不同路径权值不同、有障碍 且多个游戏单位在地图中寻路的情况,如果要计算准确最短路,耗时很大,会给玩家一种卡顿的感觉。 + +而真实玩家在玩游戏的时候,并不要求一定是最短路,次短路也是可以的 (玩家不一定能感受出来,及时感受出来也不是很在意),只要奔着目标走过去 大体就可以接受。 + +所以 在游戏开发设计中,**保证运行效率的情况下,A * 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计**。 + +大家如果玩 LOL,或者 王者荣耀 可以回忆一下:如果 从很远的地方点击 让英雄直接跑过去 是 跑的路径是不靠谱的,所以玩家们才会在 距离英雄尽可能近的位置去点击 让英雄跑过去。 + +## A * 的缺点 + +大家看上述 A * 代码的时候,可以看到 我们想 队列里添加了很多节点,但真正从队列里取出来的 仅仅是 靠启发式函数判断 距离终点最近的节点。 + +相对了 普通BFS,A * 算法只从 队列里取出 距离终点最近的节点。 + +那么问题来了,A * 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。 + +IDA * 算法 对这一空间增长问题进行了优化,关于 IDA * 算法,本篇不再做讲解,感兴趣的录友可以自行找资料学习。 + +另外还有一种场景 是 A * 解决不了的。 + +如果题目中,给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标 然后找到最短路径。 + +如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd。 + + +## 其他语言版本 + +### Java + +### Python + +```Python +import heapq + +n = int(input()) + +moves = [(1, 2), (2, 1), (-1, 2), (2, -1), (1, -2), (-2, 1), (-1, -2), (-2, -1)] + +def distance(a, b): + return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 + +def bfs(start, end): + q = [(distance(start, end), start)] + step = {start: 0} + + while q: + d, cur = heapq.heappop(q) + if cur == end: + return step[cur] + for move in moves: + new = (move[0] + cur[0], move[1] + cur[1]) + if 1 <= new[0] <= 1000 and 1 <= new[1] <= 1000: + step_new = step[cur] + 1 + if step_new < step.get(new, float('inf')): + step[new] = step_new + heapq.heappush(q, (distance(new, end) + step_new, new)) + return False + +for _ in range(n): + a1, a2, b1, b2 = map(int, input().split()) + print(bfs((a1, a2), (b1, b2))) +``` + +### Go + +### Rust + +### JavaScript + +```js +class MinHeap { + constructor() { + this.val = [] + } + push(val) { + this.val.push(val) + if (this.val.length > 1) { + this.bubbleUp() + } + } + bubbleUp() { + let pi = this.val.length - 1 + let pp = Math.floor((pi - 1) / 2) + while (pi > 0 && this.val[pp][0] > this.val[pi][0]) { + ;[this.val[pi], this.val[pp]] = [this.val[pp], this.val[pi]] + pi = pp + pp = Math.floor((pi - 1) / 2) + } + } + pop() { + if (this.val.length > 1) { + let pp = 0 + let pi = this.val.length - 1 + ;[this.val[pi], this.val[pp]] = [this.val[pp], this.val[pi]] + const min = this.val.pop() + if (this.val.length > 1) { + this.sinkDown(0) + } + return min + } else if (this.val.length == 1) { + return this.val.pop() + } + + } + sinkDown(parentIdx) { + let pp = parentIdx + let plc = pp * 2 + 1 + let prc = pp * 2 + 2 + let pt = pp // temp pointer + if (plc < this.val.length && this.val[pp][0] > this.val[plc][0]) { + pt = plc + } + if (prc < this.val.length && this.val[pt][0] > this.val[prc][0]) { + pt = prc + } + if (pt != pp) { + ;[this.val[pp], this.val[pt]] = [this.val[pt], this.val[pp]] + this.sinkDown(pt) + } + } +} + +const moves = [ + [1, 2], + [2, 1], + [-1, -2], + [-2, -1], + [-1, 2], + [-2, 1], + [1, -2], + [2, -1] +] + +function dist(a, b) { + return ((a[0] - b[0])**2 + (a[1] - b[1])**2)**0.5 +} + +function isValid(x, y) { + return x >= 1 && y >= 1 && x < 1001 && y < 1001 +} + +function bfs(start, end) { + const step = new Map() + step.set(start.join(" "), 0) + const q = new MinHeap() + q.push([dist(start, end), start[0], start[1]]) + + while(q.val.length) { + const [d, x, y] = q.pop() + // if x and y correspond to end position output result + if (x == end[0] && y == end[1]) { + console.log(step.get(end.join(" "))) + break; + } + for (const [dx, dy] of moves) { + const nx = dx + x + const ny = dy + y + if (isValid(nx, ny)) { + const newStep = step.get([x, y].join(" ")) + 1 + const newDist = dist([nx, ny], [...end]) + const s = step.get([nx, ny].join(" ")) ? + step.get([nx, ny]) : + Number.MAX_VALUE + if (newStep < s) { + q.push( + [ + newStep + newDist, + nx, + ny + ] + ) + step.set([nx, ny].join(" "), newStep) + } + } + } + } +} + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const n = Number((await readline())) + + // find min step + for (let i = 0 ; i < n ; i++) { + const [s1, s2, t1, t2] = (await readline()).split(" ").map(Number) + bfs([s1, s2], [t1, t2]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +```C +#include +#include +#include + +// 定义一个结构体,表示棋盘上骑士的位置和相关的 A* 算法参数 +typedef struct { + int x, y; // 骑士在棋盘上的坐标 + int g; // 从起点到当前节点的实际消耗 + int h; // 从当前节点到目标节点的估计消耗(启发式函数值) + int f; // 总的估计消耗(f = g + h) +} Knight; + +#define MAX_HEAP_SIZE 2000000 // 假设优先队列的最大容量 + +// 定义一个优先队列,使用最小堆来实现 A* 算法中的 Open 列表 +typedef struct { + Knight data[MAX_HEAP_SIZE]; + int size; +} PriorityQueue; + +// 初始化优先队列 +void initQueue(PriorityQueue *pq) { + pq->size = 0; +} + +// 将骑士节点插入优先队列 +void push(PriorityQueue *pq, Knight k) { + if (pq->size >= MAX_HEAP_SIZE) { + // 堆已满,无法插入新节点 + return; + } + int i = pq->size++; + pq->data[i] = k; + // 上滤操作,维护最小堆的性质,使得 f 值最小的节点在堆顶 + while (i > 0) { + int parent = (i - 1) / 2; + if (pq->data[parent].f <= pq->data[i].f) { + break; + } + // 交换父节点和当前节点 + Knight temp = pq->data[parent]; + pq->data[parent] = pq->data[i]; + pq->data[i] = temp; + i = parent; + } +} + +// 从优先队列中弹出 f 值最小的骑士节点 +Knight pop(PriorityQueue *pq) { + Knight min = pq->data[0]; + pq->size--; + pq->data[0] = pq->data[pq->size]; + // 下滤操作,维护最小堆的性质 + int i = 0; + while (1) { + int left = 2 * i + 1; + int right = 2 * i + 2; + int smallest = i; + if (left < pq->size && pq->data[left].f < pq->data[smallest].f) { + smallest = left; + } + if (right < pq->size && pq->data[right].f < pq->data[smallest].f) { + smallest = right; + } + if (smallest == i) { + break; + } + // 交换当前节点与最小子节点 + Knight temp = pq->data[smallest]; + pq->data[smallest] = pq->data[i]; + pq->data[i] = temp; + i = smallest; + } + return min; +} + +// 判断优先队列是否为空 +int isEmpty(PriorityQueue *pq) { + return pq->size == 0; +} + +// 启发式函数:计算从当前位置到目标位置的欧几里得距离的平方(避免开方,提高效率) +int heuristic(int x, int y, int goal_x, int goal_y) { + int dx = x - goal_x; + int dy = y - goal_y; + return dx * dx + dy * dy; // 欧几里得距离的平方 +} + +// 用于记录从起点到棋盘上每个位置的最小移动次数 +int moves[1001][1001]; + +// 骑士在棋盘上的8个可能移动方向 +int dir[8][2] = { + {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, + {2, 1}, {2, -1}, {1, -2}, {-1, -2} +}; + +// 使用 A* 算法寻找从起点到目标点的最短路径 +int astar(int start_x, int start_y, int goal_x, int goal_y) { + PriorityQueue pq; + initQueue(&pq); + + // 初始化 moves 数组,-1 表示未访问过的位置 + memset(moves, -1, sizeof(moves)); + moves[start_x][start_y] = 0; // 起点位置的移动次数为 0 + + // 初始化起始节点 + Knight start; + start.x = start_x; + start.y = start_y; + start.g = 0; + start.h = heuristic(start_x, start_y, goal_x, goal_y); + start.f = start.g + start.h; // 总的估计消耗 + + push(&pq, start); // 将起始节点加入优先队列 + + while (!isEmpty(&pq)) { + Knight current = pop(&pq); // 取出 f 值最小的节点 + + // 如果已经到达目标位置,返回所需的最小移动次数 + if (current.x == goal_x && current.y == goal_y) { + return moves[current.x][current.y]; + } + + // 遍历当前节点的所有可能移动方向 + for (int i = 0; i < 8; i++) { + int nx = current.x + dir[i][0]; + int ny = current.y + dir[i][1]; + + // 检查新位置是否在棋盘范围内且未被访问过 + if (nx >= 1 && nx <= 1000 && ny >= 1 && ny <= 1000 && moves[nx][ny] == -1) { + moves[nx][ny] = moves[current.x][current.y] + 1; // 更新移动次数 + + // 创建新节点,表示骑士移动到的新位置 + Knight neighbor; + neighbor.x = nx; + neighbor.y = ny; + neighbor.g = current.g + 5; // 每次移动的消耗为 5(骑士移动的距离平方) + neighbor.h = heuristic(nx, ny, goal_x, goal_y); + neighbor.f = neighbor.g + neighbor.h; + + push(&pq, neighbor); // 将新节点加入优先队列 + } + } + } + + return -1; // 如果无法到达目标位置,返回 -1 +} + +int main() { + int n; + scanf("%d", &n); + while (n--) { + int a1, a2, b1, b2; // 起点和目标点的坐标 + scanf("%d %d %d %d", &a1, &a2, &b1, &b2); + + int result = astar(a1, a2, b1, b2); // 使用 A* 算法计算最短路径 + printf("%d\n", result); // 输出最小移动次数 + } + return 0; +} + +``` + + + + + + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" "b/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..c5122b1760 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" @@ -0,0 +1,94 @@ + + + +# 图论为什么统一使用ACM模式 + +代码随想录图论章节给大家统一换成ACM输入输出模式。 + +图论是在笔试还有面试中,通常都是以ACM模式来考察大家,而大家习惯在力扣刷题(核心代码模式),核心代码模式对图的存储和输出都隐藏了。 + +而图论题目的输出输出 相对其他类型题目来说是最难处理的。 + +ACM模式是最考察候选人对代码细节把控程度的, 图的构成,图的输出,这些只有ACM输入输出模式才能体现出来。 + +### 输入的细节 + +图论的输入难在 图的存储结构,**如果没有练习过 邻接表和邻接矩阵 ,很多录友是写不出来的**。 + +而力扣上是直接给好现成的 数据结构,可以直接用,所以练习不到图的输入,也练习不到邻接表和邻接矩阵。 + +**如果连邻接表 和 邻接矩阵都不知道或者写不出来的话,可以说 图论没有入门**。 + +举个例子,对于力扣 [797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) ,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。 + +如果面试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,**因为不知道图应该怎么存,也不知道自己存的图如何去遍历**。 + +即使面试的时候,有的面试官,让你用核心代码模式做题,当你写出代码后,**面试官补充一句:这个图 你是怎么存的**? + +难道和面试官说:我只知道图的算法,但我不知道图怎么存。 + +后面大家在刷 代码随想录图论第一题[98. 所有可达路径](./0098.所有可达路径.md) 的时候,就可以感受到图存储的难点所在。 + +所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。 + +### 输出的细节 + +同样,图论的输出也有细节,例如 求节点1 到节点5的所有路径, 输出可能是: + +``` +1 2 4 5 +1 3 5 +``` + +表示有两条路可以到节点5, 那储存这个结果需要二维数组,最后在一起输出,力扣是直接return数组就好了,但 ACM模式要求我们自己输出,这里有就细节了。 + +就拿 只输出一行数据,输出 `1 2 4 5` 来说, + +很多录友代码可能直接就这么写了: + +```CPP +for (int i = 0 ; i < result.size(); i++) { + cout << result[i] << " "; +} +``` + +这么写输出的结果是 `1 2 4 5 `, 发现结果是对的,一提交,发现OJ返回 格式错误 或者 结果错误。 + +如果没练习过这种输出方式的录友,就开始怀疑了,这结果一样一样的,怎么就不对,我在力扣上提交都是对的! + +**大家要注意,5 后面要不要有空格**! + +上面这段代码输出,5后面是加上了空格了,如果判题机判断 结果的长度,标准答案`1 2 4 5`长度是7,而上面代码输出的长度是 8,很明显就是不对的。 + +所以正确的写法应该是: + +```CPP +for (int i = 0 ; i < result.size() - 1; i++) { + cout << result[i] << " "; +} +cout << result[result.size() - 1]; +``` + +这么写,最后一个元素后面就没有空格了。 + +这是很多录友经常疏忽的,也是大家刷习惯了 力扣(核心代码模式)根本不会注意到的细节。 + +**同样在工程开发中,这些细节都是影响系统稳定运行的因素之一**。 + +**ACM模式 除了考验算法思路,也考验 大家对 代码的把控力度**, 而 核心代码模式 只注重算法的解题思路,所以输入输出这些就省略掉了。 + + +### 其他 + +**大家如果熟练ACM模式,那么核心代码模式没问题,但反过来就不一定了**。 + +而且我在讲解图论的时候,最头疼的就是找题,在力扣上 找题总是找不到符合思路且来完整表达算法精髓的题目。 + +特别是最短路算法相关的题目,例如 Bellman_ford系列 ,Floyd ,A * 等等总是找不到符合思路的题目。 + +索性统一我自己来出题,这其中也是巨大的工作量。为了给大家带来极致的学习体验,我在很多细节上都下了功夫。 + +等大家将图论刷完,就会感受到我的良苦用心。加油 + + +
diff --git "a/problems/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" similarity index 91% rename from "problems/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" rename to "problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" index 347bf58f13..61d49b5297 100644 --- "a/problems/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,4 +1,3 @@ - # 并查集理论基础 接下来我们来讲一下并查集,首先当然是并查集理论基础。 @@ -30,7 +29,7 @@ 但如果我们要判断两个元素是否在同一个集合里的时候 我们又能怎么办? 只能把而二维数组都遍历一遍。 -而且每当想添加一个元素到某集合的时候,依然需要把把二维数组组都遍历一遍,才知道要放在哪个集合里。 +而且每当想添加一个元素到某集合的时候,依然需要把把二维数组都遍历一遍,才知道要放在哪个集合里。 这仅仅是一个粗略的思路,如果沿着这个思路去实现代码,非常复杂,因为管理集合还需要很多逻辑。 @@ -106,13 +105,13 @@ bool isSame(int u, int v) { 搜索过程像是一个多叉树中从叶子到根节点的过程,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230602102619.png) +![](https://file1.kamacoder.com/i/algo/20230602102619.png) -如果这棵多叉树高度很深的话,每次find函数 去寻找跟的过程就要递归很多次。 +如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。 我们的目的只需要知道这些节点在同一个根下就可以,所以对这棵多叉树的构造只需要这样就可以了,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230602103040.png) +![](https://file1.kamacoder.com/i/algo/20230602103040.png) 除了根节点其他所有节点都挂载根节点下,这样我们在寻根的时候就很快,只需要一步, @@ -209,7 +208,7 @@ bool isSame(int u, int v) { // 将v->u 这条边加入并查集 void join(int u, int v) { - if (isSame) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + if (isSame(u, v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 father[v] = u; } ``` @@ -220,14 +219,14 @@ void join(int u, int v) { 举一个例子: -``` +```CPP join(1, 2); join(3, 2); ``` 此时构成的图是这样的: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230525111307.png) +![](https://file1.kamacoder.com/i/algo/20230525111307.png) 此时问 1,3是否在同一个集合,我们调用 `join(1, 2); join(3, 2);` 很明显本意要表示 1,3是在同一个集合。 @@ -257,7 +256,7 @@ join(3, 2); 构成的图是这样的: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230525112101.png) +![](https://file1.kamacoder.com/i/algo/20230525112101.png) 因为在join函数里,我们有find函数进行寻根的过程,这样就保证元素 1,2,3在这个有向图里是强连通的。 @@ -272,16 +271,16 @@ join(3, 2); 不少录友在接触并查集模板之后,用起来很娴熟,因为模板确实相对固定,但是对并查集内部数据组织方式以及如何判断是否是同一个集合的原理很模糊。 -通过以上讲解之后,我在带大家一步一步去画一下,并查集内部数据连接方式。 +通过以上讲解之后,我再带大家一步一步去画一下,并查集内部数据连接方式。 1、`join(1, 8);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122112727.png) +![](https://file1.kamacoder.com/i/algo/20231122112727.png) 2、`join(3, 8);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122113857.png) +![](https://file1.kamacoder.com/i/algo/20231122113857.png) 有录友可能想,`join(3, 8)` 在图中为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3 呢? @@ -289,20 +288,20 @@ join(3, 2); 3、`join(1, 7);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122114108.png) +![](https://file1.kamacoder.com/i/algo/20231122114108.png) 4、`join(8, 5);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122114847.png) +![](https://file1.kamacoder.com/i/algo/20231122114847.png) 这里8的根是3,那么 5 应该指向 8 的根 3,这里的原因,我们在上面「常见误区」已经讲过了。 但 为什么 图中 8 又直接指向了 3 了呢? **因为路经压缩了** -即如下代码在寻找跟的过程中,会有路径压缩,减少 下次查询的路径长度。 +即如下代码在寻找根的过程中,会有路径压缩,减少 下次查询的路径长度。 -``` +```CPP // 并查集里寻根的过程 int find(int u) { return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 @@ -311,11 +310,11 @@ int find(int u) { 5、`join(2, 9);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122115000.png) +![](https://file1.kamacoder.com/i/algo/20231122115000.png) 6、`join(6, 9);` -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231122115404.png) +![](https://file1.kamacoder.com/i/algo/20231122115404.png) 这里为什么是 2 指向了 6,因为 9的根为 2,所以用2指向6。 @@ -348,13 +347,13 @@ rank表示树的高度,即树中结点层次的最大值。 例如两个集合(多叉树)需要合并,如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230602172250.png) +![](https://file1.kamacoder.com/i/algo/20230602172250.png) 树1 rank 为2,树2 rank 为 3。那么合并两个集合,是 树1 合入 树2,还是 树2 合入 树1呢? 我们来看两个不同方式合入的效果。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230602172933.png) +![](https://file1.kamacoder.com/i/algo/20230602172933.png) 这里可以看出,树2 合入 树1 会导致整棵树的高度变的更高,而 树1 合入 树2 整棵树的高度 和 树2 保持一致。 @@ -396,7 +395,7 @@ void join(int u, int v) { if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树 else father[v] = u; - if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1因为,方面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <= + if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <= } ``` @@ -423,7 +422,7 @@ void join(int u, int v) { 空间复杂度: O(n) ,申请一个father数组。 -关于时间复杂度,如果想精确表达出来需要繁琐的数学证明,就不在本篇讲解范围内了,大家感兴趣可以自己去深入去研究。 +关于时间复杂度,如果想精确表达出来需要繁琐的数学证明,就不在本篇讲解范围内了,大家感兴趣可以自己去深入研究。 这里做一个简单的分析思路。 @@ -455,4 +454,4 @@ void join(int u, int v) { 敬请期待 并查集题目精讲系列。 - +
diff --git "a/problems/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" similarity index 65% rename from "problems/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" rename to "problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" index b631f4f5c5..718f5484a1 100644 --- "a/problems/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,13 +1,6 @@ -

- - - -

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

- # 广度优先搜索理论基础 - -在[深度优先搜索](https://programmercarl.com/图论深搜理论基础.html)的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。 +在[深度优先搜索](./图论深搜理论基础.md)的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。 广搜(bfs)是一圈一圈的搜索过程,和深搜(dfs)是一条路跑到黑然后再回溯。 @@ -25,11 +18,11 @@ 我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步。 -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825104505.png) +![图一](https://file1.kamacoder.com/i/algo/20220825104505.png) 如果加上一个end终止位置,那么使用BFS的搜索过程如图所示: -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825102653.png) +![图二](https://file1.kamacoder.com/i/algo/20220825102653.png) 我们从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。 @@ -37,7 +30,7 @@ 而且地图还可以有障碍,如图所示: -![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220825103900.png) +![图三](https://file1.kamacoder.com/i/algo/20220825103900.png) 在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。 @@ -94,28 +87,12 @@ void bfs(vector>& grid, vector>& visited, int x, int y } ``` -以上模板代码,就是可以直接拿来做 [200.岛屿数量](https://leetcode.cn/problems/number-of-islands/solution/by-carlsun-2-n72a/) 这道题目,唯一区别是 针对地图 grid 中有数字1的地方去做一个遍历。 - -即: - -``` -if (!visited[nextx][nexty]) { // 如果节点没被访问过 -``` - -改为 - -``` -if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { // 如果节点没被访问过且节点是可遍历的 - -``` -就可以通过 [200.岛屿数量](https://leetcode.cn/problems/number-of-islands/solution/by-carlsun-2-n72a/) 这道题目,大家可以去体验一下。 - - - ## 总结 -当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解,因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵,所以细节方面针对具体题目在做讲解。 +当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解。 + +**因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵**,所以细节方面针对具体题目在做讲解。 本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。 @@ -125,39 +102,4 @@ if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { // 如果节点没被 相信看完本篇,大家会对广搜有一个基础性的认识,后面再来做对应的题目就会得心应手一些。 -## 其他语言版本 - -### Python -```python -from collections import deque - -dir = [(0, 1), (1, 0), (-1, 0), (0, -1)] # 创建方向元素 - -def bfs(grid, visited, x, y): - - queue = deque() # 初始化队列 - queue.append((x, y)) # 放入第一个元素/起点 - visited[x][y] = True # 标记为访问过的节点 - - while queue: # 遍历队列里的元素 - - curx, cury = queue.popleft() # 取出第一个元素 - - for dx, dy in dir: # 遍历四个方向 - - nextx, nexty = curx + dx, cury + dy - - if nextx < 0 or nextx >= len(grid) or nexty < 0 or nexty >= len(grid[0]): # 越界了,直接跳过 - continue - - if not visited[nextx][nexty]: # 如果节点没被访问过 - queue.append((nextx, nexty)) # 加入队列 - visited[nextx][nexty] = True # 标记为访问过的节点 - -``` - - -

- - - +

diff --git "a/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" "b/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" new file mode 100644 index 0000000000..d7b8da94cb --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,146 @@ + +# 图论总结篇 + +从深搜广搜 到并查集,从最小生成树到拓扑排序, 最后是最短路算法系列。 + +至此算上本篇,一共30篇文章,图论之旅就在此收官了。 + +在[0098.所有可达路径](./0098.所有可达路径.md) ,我们接触了两种图的存储方式,邻接表和邻接矩阵,掌握两种图的存储方式很重要。 + +图的存储方式也是大家习惯在核心代码模式下刷题 经常忽略的 知识点。因为在力扣上刷题不需要掌握图的存储方式。 + +## 深搜与广搜 + +在二叉树章节中,其实我们讲过了 深搜和广搜在二叉树上的搜索过程。 + +在图论章节中,深搜与广搜就是在图这个数据结构上的搜索过程。 + +深搜与广搜是图论里基本的搜索方法,大家需要掌握三点: + +* 搜索方式:深搜是可一个方向搜,不到黄河不回头。 广搜是围绕这起点一圈一圈的去搜。 +* 代码模板:需要熟练掌握深搜和广搜的基本写法。 +* 应用场景:图论题目基本上可以即用深搜也可用广搜,无疑是用哪个方便而已 + +### 注意事项 + +需要注意的是,同样是深搜模板题,会有两种写法。 + +在[0099.岛屿的数量深搜.md](./0099.岛屿的数量深搜.md) 和 [0105.有向图的完全可达性](./0105.有向图的完全可达性.md),涉及到dfs的两种写法。 + +**我们对dfs函数的定义是 是处理当前节点 还是处理下一个节点 很重要**,决定了两种dfs的写法。 + +这也是为什么很多录友看到不同的dfs写法,结果发现提交都能过的原因。 + +而深搜还有细节,有的深搜题目需要用到回溯的过程,有的就不用回溯的过程, + +一般是需要计算路径的问题 需要回溯,如果只是染色问题(岛屿问题系列) 就不需要回溯。 + +例如: [0105.有向图的完全可达性](./0105.有向图的完全可达性.md) 深搜就不需要回溯,而 [0098.所有可达路径](./0098.所有可达路径.md) 中的递归就需要回溯,文章中都有详细讲解 + +注意:以上说的是不需要回溯,不是没有回溯,只要有递归就会有回溯,只是我们是否需要用到回溯这个过程,这是需要考虑的。 + +很多录友写出来的广搜可能超时了, 例如题目:[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md) + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +具体原因,我在[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md) 中详细讲了。 + +在深搜与广搜的讲解中,为了防止惯性思维,我特别加入了题目 [0106.岛屿的周长](./0106.岛屿的周长.md),提醒大家,看到类似的题目,也不要上来就想着深搜和广搜。 + +还有一些图的问题,在题目描述中,是没有图的,需要我们自己构建一个图,例如 [0110.字符串接龙](./0110.字符串接龙.md),题目中连线都没有,需要我们自己去思考 什么样的两个字符串可以连成线。 + +## 并查集 + +并查集相对来说是比较复杂的数据结构,其实他的代码不长,但想彻底学透并查集,需要从多个维度入手, + +我在理论基础篇的时候 讲解如下重点: + +* 为什么要用并查集,怎么不用个二维数据,或者set、map之类的。 +* 并查集能解决那些问题,哪些场景会用到并查集 +* 并查集原理以及代码实现 +* 并查集写法的常见误区 +* 带大家去模拟一遍并查集的过程 +* 路径压缩的过程 +* 时间复杂度分析 + +上面这几个维度 大家都去思考了,并查集基本就学明白了。 + +其实理论基础篇就算是给大家出了一道裸的并查集题目了,所以在后面的题目安排中,会稍稍的拔高一些,重点在于并查集的应用上。 + +例如 并查集可以判断这个图是否是树,因为树的话,只有一个根,符合并查集判断集合的逻辑,题目:[0108.冗余连接](./0108.冗余连接.md)。 + +在[0109.冗余连接II](./0109.冗余连接II.md) 中 对有向树的判断难度更大一些,需要考虑的情况比较多。 + + +## 最小生成树 + +最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起。 + +最小生成树算法,有prim 和 kruskal。 + +**prim 算法是维护节点的集合,而 Kruskal 是维护边的集合**。 + +在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。 + +> 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图 + +Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。 + +Kruskal算法 时间复杂度 为 O(nlogn),其中n 为边的数量,适用稀疏图。 + +关于 prim算法,我自创了三部曲,来帮助大家理解: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家只要理解这三部曲, prim算法 至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于 自己在写prim的时候 两眼一抹黑 完全凭感觉去写。 + +**minDist数组 是prim算法的灵魂,它帮助 prim算法完成最重要的一步,就是如何找到 距离最小生成树最近的点**。 + +kruscal的主要思路: + +* 边的权值排序,因为要优先选最小的边加入到生成树里 +* 遍历排序后的边 + * 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环 + * 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合 + +而判断节点是否在一个集合 以及将两个节点放入同一个集合,正是并查集的擅长所在。 + +所以 Kruskal 是需要用到并查集的。 + +这也是我在代码随想录图论编排上 为什么要先 讲解 并查集 在讲解 最小生成树。 + + +## 拓扑排序 + +拓扑排序 是在图上的一种排序。 + +概括来说,**给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序**。 + +同样,拓扑排序也可以检测这个有向图 是否有环,即存在循环依赖的情况。 + +拓扑排序的一些应用场景,例如:大学排课,文件下载依赖 等等。 + +只要记住如下两步拓扑排序的过程,代码就容易写了: + +1. 找到入度为0 的节点,加入结果集 +2. 将该节点从图中移除 + +## 最短路算法 + +最短路算法是图论中,比较复杂的算法,而且不同的最短路算法都有不同的应用场景。 + +我在 [最短路算法总结篇](./最短路问题总结篇.md) 里已经做了一个高度的概括。 + +大家要时常温故而知新,才能透彻理解各个最短路算法。 + + +## 总结 + +到最后,图论终于剧终了,相信这是市面上大家能看到最全最细致的图论讲解教程。 + +图论也是我 《代码随想录》所有章节里 所费精力最大的一个章节。 + +只为了不负录友们的期待。 大家加油💪🏻 +
diff --git "a/problems/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" similarity index 76% rename from "problems/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" rename to "problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" index 7f847f8fae..50df8aa6df 100644 --- "a/problems/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,8 +1,3 @@ -

- - - -

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

# 深度优先搜索理论基础 @@ -33,29 +28,29 @@ 如图一,是一个无向图,我们要搜索从节点1到节点6的所有路径。 -![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707093643.png) +![图一](https://file1.kamacoder.com/i/algo/20220707093643.png) 那么dfs搜索的第一条路径是这样的: (假设第一次延默认方向,就找到了节点6),图二 -![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707093807.png) +![图二](https://file1.kamacoder.com/i/algo/20220707093807.png) 此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。 如图三: -![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094011.png) +![图三](https://file1.kamacoder.com/i/algo/20220707094011.png) 路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程(这一点很重要,很多录友不理解dfs代码中回溯是用来干什么的) 又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。 -![图四](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094322.png) +![图四](https://file1.kamacoder.com/i/algo/20220707094322.png) 又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点。 -![图五](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094813.png) +![图五](https://file1.kamacoder.com/i/algo/20220707094813.png) 那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如图图六: -![图六](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707095232.png) +![图六](https://file1.kamacoder.com/i/algo/20220707095232.png) 上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点: @@ -67,11 +62,11 @@ 正是因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。 -很多录友对回溯很陌生,建议先看看代码随想录,[回溯算法章节](https://programmercarl.com/回溯算法理论基础.html)。 +很多录友对回溯很陌生,建议先看看代码随想录,[回溯算法章节](../回溯算法理论基础.md)。 有递归的地方就有回溯,那么回溯在哪里呢? -就地递归函数的下面,例如如下代码: +就递归函数的下面,例如如下代码: ```cpp void dfs(参数) { @@ -83,11 +78,11 @@ void dfs(参数) { 可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。 -在讲解[二叉树章节](https://programmercarl.com/二叉树理论基础.html)的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索) +在讲解[二叉树章节](../二叉树理论基础.md)的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索) 所以**dfs,bfs其实是基础搜索算法,也广泛应用与其他数据结构与算法中**。 -我们在回顾一下[回溯法](https://programmercarl.com/回溯算法理论基础.html)的代码框架: +我们在回顾一下[回溯法](../回溯算法理论基础.md)的代码框架: ```cpp void backtracking(参数) { @@ -128,9 +123,9 @@ void dfs(参数) { ## 深搜三部曲 -在 [二叉树递归讲解](https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86.html)中,给出了递归三部曲。 +在 [二叉树递归讲解](../二叉树的递归遍历.md)中,给出了递归三部曲。 -[回溯算法](https://programmercarl.com/回溯算法理论基础.html)讲解中,给出了 回溯三部曲。 +[回溯算法](../回溯算法理论基础.md)讲解中,给出了 回溯三部曲。 其实深搜也是一样的,深搜三部曲如下: @@ -167,7 +162,7 @@ if (终止条件) { 终止添加不仅是结束本层递归,同时也是我们收获结果的时候。 -另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。 +另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 隐藏在下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。 3. 处理目前搜索节点出发的路径 @@ -185,7 +180,7 @@ for (选择:本节点所连接的其他节点) { 如图七所示, 路径2 已经走到了 目的地节点6,那么 路径2 是如何撤销,然后改为 路径3呢? 其实这就是 回溯的过程,撤销路径2,走换下一个方向。 -![图七](https://code-thinking-1253855093.file.myqcloud.com/pics/20220708093544.png) +![图七](https://file1.kamacoder.com/i/algo/20220708093544.png) ## 总结 @@ -199,7 +194,4 @@ for (选择:本节点所连接的其他节点) { 后面我也会给大家安排具体练习的题目,依旧是代码随想录的风格,循序渐进由浅入深! -

- - - +

diff --git "a/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100644 index 0000000000..fb52c83921 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,249 @@ + +# 图论理论基础 + +这一篇我们正式开始图论! + +代码随想录图论中的算法题目将统一使用ACM模式,[为什么要使用ACM模式](./图论为什么用ACM模式.md) + +## 图的基本概念 + +二维坐标中,两点可以连成线,多个点连成的线就构成了图。 + +当然图也可以就一个节点,甚至没有节点(空图) + +### 图的种类 + +整体上一般分为 有向图 和 无向图。 + +有向图是指 图中边是有方向的: + +![](https://file1.kamacoder.com/i/algo/20240510195737.png) + +无向图是指 图中边没有方向: + +![](https://file1.kamacoder.com/i/algo/20240510195451.png) + +加权有向图,就是图中边是有权值的,例如: + +![](https://file1.kamacoder.com/i/algo/20240510195821.png) + +加权无向图也是同理。 + +### 度 + +无向图中有几条边连接该节点,该节点就有几度。 + +例如,该无向图中,节点4的度为5,节点6的度为3。 + +![](https://file1.kamacoder.com/i/algo/20240511115029.png) + +在有向图中,每个节点有出度和入度。 + +出度:从该节点出发的边的个数。 + +入度:指向该节点边的个数。 + +例如,该有向图中,节点3的入度为2,出度为1,节点1的入度为0,出度为2。 + +![](https://file1.kamacoder.com/i/algo/20240511115235.png) + + +## 连通性 + +在图中表示节点的连通情况,我们称之为连通性。 + +### 连通图 + +在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240511102351.png) + +如果有节点不能到达其他节点,则为非连通图,如图: + +![](https://file1.kamacoder.com/i/algo/20240511102449.png) + +节点1 不能到达节点4。 + +### 强连通图 + +在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。 + +这里有录友可能想,这和无向图中的连通图有什么区别,不是一样的吗? + +我们来看这个有向图: + +![](https://file1.kamacoder.com/i/algo/20240511104531.png) + +这个图是强连通图吗? + +初步一看,好像这节点都连着呢,但这不是强连通图,节点1 可以到节点5,但节点5 不能到 节点1 。 + +强连通图是在有向图中**任何两个节点是可以相互到达** + +下面这个有向图才是强连通图: + +![](https://file1.kamacoder.com/i/algo/20240511113101.png) + + +### 连通分量 + +在无向图中的极大连通子图称之为该图的一个连通分量。 + +只看概念大家可能不理解,我来画个图: + +![](https://file1.kamacoder.com/i/algo/20240511111559.png) + +该无向图中 节点1、节点2、节点5 构成的子图就是 该无向图中的一个连通分量,该子图所有节点都是相互可达到的。 + +同理,节点3、节点4、节点6 构成的子图 也是该无向图中的一个连通分量。 + +那么无向图中 节点3 、节点4 构成的子图 是该无向图的联通分量吗? + +不是! + +因为必须是极大联通子图才能是连通分量,所以 必须是节点3、节点4、节点6 构成的子图才是连通分量。 + +在图论中,连通分量是一个很重要的概念,例如岛屿问题(后面章节会有专门讲解)其实就是求连通分量。 + +### 强连通分量 + +在有向图中极大强连通子图称之为该图的强连通分量。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240511112951.png) + +节点1、节点2、节点3、节点4、节点5 构成的子图是强连通分量,因为这是强连通图,也是极大图。 + +节点6、节点7、节点8 构成的子图 不是强连通分量,因为这不是强连通图,节点8 不能达到节点6。 + +节点1、节点2、节点5 构成的子图 也不是 强连通分量,因为这不是极大图。 + + +## 图的构造 + +我们如何用代码来表示一个图呢? + +一般使用邻接表、邻接矩阵 或者用类来表示。 + +主要是 朴素存储、邻接表和邻接矩阵。 + +关于朴素存储,这是我自创的名字,因为这种存储方式,就是将所有边存下来。 + +例如图: + +![](https://file1.kamacoder.com/i/algo/20240511112951.png) + +图中有8条边,我们就定义 8 * 2的数组,即有n条边就申请n * 2,这么大的数组: + +![](https://file1.kamacoder.com/i/algo/20250110114348.png) + +数组第一行:6 7,就表示节点6 指向 节点7,以此类推。 + +当然可以不用数组,用map,或者用 类 到可以表示出 这种边的关系。 + +这种表示方式的好处就是直观,把节点与节点之间关系很容易展现出来。 + +但如果我们想知道 节点1 和 节点6 是否相连,我们就需要把存储空间都枚举一遍才行。 + +这是明显的缺点,同时,我们在深搜和广搜的时候,都不会使用这种存储方式。 + +因为 搜索中,需要知道 节点与其他节点的链接情况,而这种朴素存储,都需要全部枚举才知道链接情况。 + +在图论章节的后面文章讲解中,我会举例说明的。大家先有个印象。 + +### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。 + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间。 + +图中有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/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/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" "b/problems/kamacoder/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" new file mode 100644 index 0000000000..194f1f5ee2 --- /dev/null +++ "b/problems/kamacoder/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,54 @@ + +# 最短路算法总结篇 + +至此已经讲解了四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd。 + +针对这四大最短路算法,我用了七篇长文才彻底讲清楚,分别是: + +* dijkstra朴素版 +* dijkstra堆优化版 +* Bellman_ford +* Bellman_ford 队列优化算法(又名SPFA) +* bellman_ford 算法判断负权回路 +* bellman_ford之单源有限最短路 +* Floyd 算法精讲 +* 启发式搜索:A * 算法 + + +最短路算法比较复杂,而且各自有各自的应用场景,我来用一张表把讲过的最短路算法的使用场景都展现出来: + +![](https://file1.kamacoder.com/i/algo/20240508121355.png) + +(因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起) + + +可能有同学感觉:这个表太复杂了,我记也记不住。 + +其实记不住的原因还是对 这几个最短路算法没有深刻的理解。 + +这里我给大家一个大体使用场景的分析: + +**如果遇到单源且边为正数,直接Dijkstra**。 + +至于 **使用朴素版还是 堆优化版 还是取决于图的稠密度**, 多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。 + +一般情况下,可以直接用堆优化版本。 + +**如果遇到单源边可为负数,直接 Bellman-Ford**,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。 + +一般情况下,直接用 SPFA。 + +**如果有负权回路,优先 Bellman-Ford**, 如果是有限节点最短路 也优先 Bellman-Ford,理由是写代码比较方便。 + +**如果是遇到多源点求最短路,直接 Floyd**。 + +除非 源点特别少,且边都是正数,那可以 多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。 + +对于A * ,由于其高效性,所以在实际工程应用中使用最为广泛 ,由于其 结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题。 + +游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。 + + + + +
diff --git a/problems/qita/acm.md b/problems/qita/acm.md index 999283564a..d4d942daca 100644 --- a/problems/qita/acm.md +++ b/problems/qita/acm.md @@ -11,15 +11,15 @@ [知识星球](https://programmercarl.com/other/kstar.html)里很多录友的日常打卡中,都表示被 ACM模式折磨过: -
+
-
+
-
+
-
+
-
+
所以我正式推出:**卡码网**([https://kamacoder.com](https://kamacoder.com)),**专门帮助大家练习ACM模式**。 @@ -43,17 +43,17 @@ 来看看这极简的界面,没有烂七八糟的功能,只有刷题! -
+
在「状态」这里可以看到 大家提交的代码和判题记录,目前卡码网([https://kamacoder.com](https://kamacoder.com))几乎无时无刻都有卡友在提交代码。 看看大家周六晚上都在做什么,刷哪些题目。 -
+
提交代码的界面是这样的,**目前支持所有主流刷题语言**。 -
+
## 题解 @@ -63,7 +63,7 @@ [https://github.com/youngyangyang04/kamacoder-solutions](https://github.com/youngyangyang04/kamacoder-solutions) -
+
**欢迎去Github上star,欢迎fork,也欢迎来Github仓库贡献其他语言版本,成为contributor**。 @@ -71,7 +71,7 @@ 目前已经有两位录友贡献C和Java版本了。 -
+
期待在Github(https://github.com/youngyangyang04/kamacoder-solutions) 的contributors上也出现你的头像。 diff --git a/problems/qita/acm_backup.md b/problems/qita/acm_backup.md new file mode 100755 index 0000000000..d4d942daca --- /dev/null +++ b/problems/qita/acm_backup.md @@ -0,0 +1,89 @@ + +# 如何练习ACM模式输入输出模式 | 如何准备笔试 | 卡码网 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +## 为什么卡码网 + +录友们在求职的时候会发现,很多公司的笔试题和面试题都是ACM模式, 而大家习惯去力扣刷题,力扣是核心代码模式。 + +当大家在做ACM模式的算法题的时候,需要自己处理数据的输入输出,**如果没有接触过的话,还是挺难的**。 + +[知识星球](https://programmercarl.com/other/kstar.html)里很多录友的日常打卡中,都表示被 ACM模式折磨过: + +
+ +
+ +
+ +
+ +
+ +所以我正式推出:**卡码网**([https://kamacoder.com](https://kamacoder.com)),**专门帮助大家练习ACM模式**。 + +那么之前大家去哪里练习ACM模式呢? + +去牛客做笔试真题,结果发现 ACM模式没练出来,题目倒是巨难,一点思路都没有,代码更没有写,ACM模式无从练起。 + +去洛谷,POJ上练习? 结果发现 题目超多,不知道从哪里开始刷,也没有一个循序渐进的刷题顺序。 + +**而卡码网上有我精选+制作的25道题目**!我还把25题的后台测试数据制作了一遍,保证大家练习的效果。 + +为什么题目不多,只有25道? + +因为大家练习ACM模式不需要那么多题目,有一个循序渐进的练习过程就好了。 + +这25道题目包含了数组、链表、哈希表、字符串、二叉树、动态规划以及图的的题目,常见的输入输出方式都覆盖了。 + +**这是最精华的25道题目**!。 + +## 卡码网长什么样 + +来看看这极简的界面,没有烂七八糟的功能,只有刷题! + +
+ +在「状态」这里可以看到 大家提交的代码和判题记录,目前卡码网([https://kamacoder.com](https://kamacoder.com))几乎无时无刻都有卡友在提交代码。 +看看大家周六晚上都在做什么,刷哪些题目。 + +
+ + +提交代码的界面是这样的,**目前支持所有主流刷题语言**。 + +
+ +## 题解 + +基本大家来卡码网([https://kamacoder.com](https://kamacoder.com))练习ACM模式,都是对输入输出不够了解的,所以想看现成的题解,看看究竟是怎么处理的。 + +所以我用C++把卡码网上25道题目的题解都写了,并发布到Github上: + +[https://github.com/youngyangyang04/kamacoder-solutions](https://github.com/youngyangyang04/kamacoder-solutions) + +
+ +**欢迎去Github上star,欢迎fork,也欢迎来Github仓库贡献其他语言版本,成为contributor**。 + +如果不懂如何和开源项目提交代码,[可以看这里](https://www.programmercarl.com/qita/join.html) + +目前已经有两位录友贡献C和Java版本了。 + +
+ +期待在Github(https://github.com/youngyangyang04/kamacoder-solutions) 的contributors上也出现你的头像。 + +目前题解只有C++代码吗? + +当然不是,大多数题目已经有了 Java、python、C版本。 **其他语言版本,就给录友们成为contributor的机会了**。 + +## 最后 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +快去体验吧,笔试之前最好 把卡码网25道题目都刷完。 + +期待录友们成为最早一批把卡码网刷爆的coder! + diff --git a/problems/qita/algo_pdf.md b/problems/qita/algo_pdf.md new file mode 100755 index 0000000000..76e2d16af1 --- /dev/null +++ b/problems/qita/algo_pdf.md @@ -0,0 +1,70 @@ +# 代码随想录完整版PDF下载 | 合集下载 | 百度云 | + +代码随想录已经是很多学习算法的小伙伴刷题必备的资料,也得到非常多的好评,这是Carl继续创作的动力。 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +估计绝大多数录友之前也都下载过代码随想录PDF,但是那是我两年前整理的了。 + +![](https://file1.kamacoder.com/i/algo/20230815200530.png) + +如今,很多题目的讲解都改了上十遍,很多图都重画过。 + +之前的PDF一直都没有全集,而且章节也不全,主要是重点章节:二叉树、回溯算法、贪心、动态规划的整理。 + +也有太多录友和我反馈过:由于XXX原因,自己不能上网,看不了网站,pdf有完整版吗? + +其实录友们的需求我都记下来了,就是工作太多,我只能慢慢一项一项去处理。 + +**虽然慢,但我一直在做**! + +现在代码随想录网站最新的内容以及全集整理完毕。 + +
+ +这份PDF整理的非常精细,并把我的[算法公开课](./gongkaike.md)视频,对应题目的链接都放上去了: + +
+ +这份《代码随想录》PDF 和 《代码随想录》纸质版 和 代码随想录网站基本一致,大家选一个合适自己的阅读方式就好。 + +不过这里我依然建议大家尽量看代码随想录网站(programmercarl.com),因为网站一直都是最新的,也是经常更新的。 + +PDF可以作为辅助,例如不能上网的时候。 + +昨天我在[知识星球](./kstar.md)里第一时间公布这份全版代码随想录PDF下载的消息 + +
+ +同时有我企业微信的录友,都接到了这份PDF的推送: + +
+ +
+ +
+ +
+ +
+ +现在我把它免费分享给录友们,大家可以加我的企业微信,备注:简单自我介绍+pdf ,例如:XX大学研二-pdf 或者 XX城市后端开发-pdf ,通过之后,会直接发给大家的。 + +
+ + + diff --git a/problems/qita/ewaishuoming.md b/problems/qita/ewaishuoming.md new file mode 100755 index 0000000000..7c3f690368 --- /dev/null +++ b/problems/qita/ewaishuoming.md @@ -0,0 +1,16 @@ + + +# 本模块说明 + +本模块题目,暂时没有纳入「代码随想录」算法教程体系之中。 + +* 本模块部分题解还不够完善。 +* 本模块部分题目和「代码随想录」中是相似的。 +* 本模块题解并没有体系化 + +很多录友反馈,除了「代码随想录」还有没有其他题目可以练手,最好也有题解,所以我才把这些题解放出来。本模块题目可以作为大家刷题的一个补充。 + +加油💪 + + + diff --git a/problems/qita/gitserver.md b/problems/qita/gitserver.md index 9ee06ae4d9..caf93ec6ec 100644 --- a/problems/qita/gitserver.md +++ b/problems/qita/gitserver.md @@ -138,7 +138,7 @@ passwd gitpassword 创建`.ssh` 目录,如果`.ssh` 已经存在了,可以忽略这一项 -为啥用配置ssh公钥呢,同学们记不记得我急使用github上传上传代码的时候也要把自己的公钥配置上github上 +为啥用配置ssh公钥呢,同学们记不记得我使用github上传代码的时候也要把自己的公钥配置上传到github上 这也是方面每次操作git仓库的时候不用再去输入密码 @@ -186,7 +186,7 @@ cd /home/git/.ssh/ cat id_rsa.pub >> authorized_keys ``` -如何看我们配置的密钥是否成功呢, 在客户点直接登录git服务器,看看是否是免密登陆 +如何看我们配置的密钥是否成功呢, 在客户端直接登录git服务器,看看是否是免密登陆 ``` ssh git@git服务器ip ``` diff --git a/problems/qita/gongkaike.md b/problems/qita/gongkaike.md new file mode 100755 index 0000000000..26c874b0c8 --- /dev/null +++ b/problems/qita/gongkaike.md @@ -0,0 +1,162 @@ + +# 代码随想录算法公开课 | 最强算法公开课 + +和录友们汇报一下,**代码随想录算法公开课**已经更新完毕了。 + +由我亲自录制了140期算法视频,覆盖了 [《代码随想录》纸质版](./publish.md)上全部题目的讲解。 + +视频全部免费开放在[B站:代码随想录](https://www.bilibili.com/video/BV1fA4y1o715) + +目录就在视频播放的右边,完全按照代码随想录的顺序讲解,配合 《代码随想录》或者代码随想录网站一起学习,味道更佳! + +
+ +从在22年的5月份开始决定把《代码随想录》上的算法题都由我亲自讲解一波。 + +当时录了第一期算法视频 「二分查找」: + +
+ +别看现在这期视频有10w的播放量,因为都是后序录友们自己找到我的视频来看的,一年后才到10w播放。 + +当时这个视频发出去,播放量就几百。 + +毕竟这种算法视频和普通娱乐或者范技术类视频没法比,平台也不会推荐的。 + +我的视频播放量虽然低,但只要看过视频的录友,评论都很高。随便找了几个最新的评论: + +![](https://file1.kamacoder.com/i/algo/20221121094718.png) + +![](https://file1.kamacoder.com/i/algo/20230222100337.png) + +![](https://file1.kamacoder.com/i/algo/20230222113111.png) + +![](https://file1.kamacoder.com/i/algo/20230222163023.png) + +![](https://file1.kamacoder.com/i/algo/20230223235500.png) + +![](https://file1.kamacoder.com/i/algo/20230323211739.png) + +![](https://file1.kamacoder.com/i/algo/20230324150536.png) + +![](https://file1.kamacoder.com/i/algo/20230325215454.png) + +![](https://file1.kamacoder.com/i/algo/20230327100147.png) + +![](https://file1.kamacoder.com/i/algo/20230329101702.png) + +![](https://file1.kamacoder.com/i/algo/20230404202808.png) + +当初也是看到大家的评论,我才下决心继续更下去,从 去年5月份,每周坚持更新四期算法视频,雷打不动,一直坚持到现在。 + +![](https://file1.kamacoder.com/i/algo/20230303170120.png) + +一晃 ,大半年过去了,足足更新了 140期算法视频,已经覆盖了 [《代码随想录》纸质版](./publish.md)上全部题目的讲解。 + +**我应该为数不多(至少目前我还没看到)的技术书籍作者能亲自把书中全部内容以视频的方式讲解出来并免费分享给大家的** + +大家可以想一想这些年买过哪些技术书籍,有作者亲自给大家把每章每一节都做视频讲解并免费开放的。 + +**那么看 [《代码随想录》](./publish.md)的录友们就有这个待遇**! + +讲课录视频,其实是很费精力的,大家看视频,可能看我讲的行云流水,其实我都是做了十足的功课。 + +**我平时养成了只要有空的时候就模拟一遍某算法运行过程的习惯,板书更是擦了写写了再擦**,反复尝试那种方式能给大家讲清楚的,然后再开拍视频。 + +可能大家会想,都出书了照着书讲不就好了吗? 应该不难吧? + +如果这么简单的话,可能市面上 很多技术书籍作者们就都亲自讲解一波了。 + +写出来和讲出来还不是一个维度。 + +讲出来需要很综合的能力包括表达力,而且大家看我的算法视频会发现:我是脱稿的,我没有提示词,摄像头开了就开讲 一镜到底。 + +**我是做了非常多练习才达到这个程度**。 + +很多人看到摄像头,就会紧张,没有提示词就不知道自己下句该说什么了,瞬间就会:我是谁,我在哪,我在干什么? + +## 算法公开课质量如何 + +目前国内算法视频的讲解风格,一般是 录屏力扣写代码 或者 ppt演示。 这样其实录制视频难度低了很多。 + +但大家上油管的话,会发现 海外经典算法视频的up ,都是一个小白板直接开讲。 + +
+
+
+ +这种讲课方式 容易走两个极端,**一种就是非常好,成为经典系列,一种就是被喷讲的像垃圾一样**。 + +如果是 录屏或者ppt演示,这样至少有稿件照着读,或者提示词,就算差也不会差到哪去。 + +所以呢,我选择这种白板模拟思路直接手撕代码的讲课方式,也是给自己一个挑战,目前的口碑来看,还有走向了好的那个极端。 + + +关于质量究竟如何,学习效果如何,大家可以去B站上去看(B站同名:[代码随想录](https://www.bilibili.com/video/BV1fA4y1o715)),有口皆碑! + + +## 辛苦录视频为了啥 + +再说说这个辛苦录视频,一忙就忙大半年,投入这么多时间和精力,最后为什么还免费开放。 + +书 + 付费视频讲解 或者 免费网站内容 + 付费视频讲解 或者 免费网站内容 + 部分视频免费 + 部分视频付费,**这些都是非常好的盈利模式**,而且还可持续变现的。 + +以代码随想录目前的影响力来说,我这套140集视频教程,不用很贵,定价99元,每年卖出上万份问题不大。 + +这可是一笔非常可观的收入!(真的很香!)而且还是持续收入,后期还不用什么去维护,不像 [知识星球](./kstar.md) 或者 [算法训练营](./xunlianying.md) 我需要花大量时间给录友们答疑。 + +那我为什么不这么做呢? + +**“代码随想录” 这五个字,我是会用一生去经营的**,凡事要看的长远,不是什么赚钱就立刻要去干什么。 + +这套算法公开课视频,不仅造福录友们,也放大了代码随想录的品牌影响,这是双赢的。 + +**免费硬核的算法内容是 代码随想录的立身之本**,也是 大家为什么学算法学编程首选代码随想录的根本所在。 + +如果有点影响力了,就暗插各种付费,这样不持久! **也会伤了很多录友的心**。 + +所以目前 代码随想录网站(programmercarl.com),代码随想录Github(https://github.com/youngyangyang04/leetcode-master),代码随想录算法公开课(B站:代码随想录),**都是完全免费**,也足够大家学习算法所用。 + +**我的免费算法视频内容,要比绝大多数视频上大家付费的 算法视频课、算法训练营质量要高得多**,视频课程基于《代码随想录》的刷题顺序来录制,会让视频内容非常系统,而不是东一块,西一块的。 + +至于《代码随想录》纸质版的内容其实和代码随想录网站是一样的,很的录友买纸质版是因为习惯看纸质版,有的是仅仅是为了留念和收藏。 + +而且以上开源内容,我还会持续优化迭代的,不会做完了就放着不管,如果一年前刷完代码随想录的录友现在在重看一遍 代码随想录网站,**你会发现很多题解了又多了很多图,又优化了很多讲解内容**。 + +这是我的github提交记录:(https://github.com/youngyangyang04) + +
+ +每天或多或少多要优化一点点。 **每天的量可能不多,但每天都在优化**! + +如果感觉代码随想录网站 和 代码随想录算法公开课对大家确实有帮助,不用买书,欢迎去[豆瓣](https://book.douban.com/subject/35680544/)给一个好评就好,感谢录友们的支持。 + + +## 公开课 + +再回头说说目前算法公开课, 其实直到现在 我新发的视频播放也就两三千的播放量。 + +
+ +(大家现在去B站上去看,可能已经上万播放量,但新发的时候 播放量一直都很惨淡) + +但为什么发视频就两三千播放量,就可以在B站聚集10w录友。 + +
+ +对于B站十万粉的号主来说,好像都得有几个百万播放的爆款视频。 + +**但我的视频从来没有爆款过,也没有被平台推荐过,都是非常稳定的几千播放**。 + +很多录友都是主动搜索找过来的就关注了,或者身边的人推荐来的。 + +只要是真正能给大家带来价值的,真正能让大家学明白算法,就会得到认可。 + +所以,**酒香不怕巷子深,真正有价值的内容,不需要蹭热点,想学习的人一定会找到你**。 + +目前《代码随想录》上的算法讲解视频终于更新完了,后面有就有足够的精力去更图论、排序、高级数据结构了。 + +算法公开课全部发布在B站上,链接直达:[《代码随想录》算法公开课](https://www.bilibili.com/video/BV1fA4y1o715) + +最后,**认准代码随想录,学习算法不迷路**。加油💪🏻 + diff --git a/problems/qita/join.md b/problems/qita/join.md index 62dec674d5..232d84ca2f 100644 --- a/problems/qita/join.md +++ b/problems/qita/join.md @@ -28,10 +28,10 @@ 点击这里Fetch upstream。 -
+
点击之后,这里就会显示最新的信息了 -
+
注意这时是你的远端仓库为最新版本,本地还不是最新的,本地要git pull一下。 @@ -39,18 +39,18 @@ 如何提交代码呢,首先把自己的代码提交到自己的fork的远端仓库中,然后open pull request,如图: -
+
点击 open pull request之后,就是如下画面,一个pull request有多个commit。 -
+
然后就是给pull request 添加备注,pull request是对本次commit的一个总结。如果一个pull request就一个commit,那么就和commit的备注保持一次。 然后点击 create pull request 就可以了 -
+
此时你就提交成功了,我会在项目中的pull requests 处理列表里看到你的请求。 -
+
然后如果你发现自己的代码没有合入多半是有问题,如果有问题都有会在pull request里给出留言的, @@ -78,27 +78,27 @@ C++代码 \`\`\` 例如这个commit,在添加java代码的时候,就直接添加代码 -
+
正确的格式应该是这样: -
+
一般发现问题,我也会在代码中给出评论: -
+
这样大家也可以学习一些 提交代码的规范方面的知识 有的录友 是添加的代码块语法,但没有标记是哪种语言,这样的话 代码就不会针对某种语言高亮显示了,也比较影响阅读,例如: -
+
提交python代码的话,要注释好,是python2还是python3 例如这样: -
+
当然python2的话,只这么写就行 @@ -113,7 +113,7 @@ python代码 有的录友是一个pull request 里只有一个commit。 -
+
其实如果大家是平时一天写了两三道题目的话,那么分三个commit,一个pull request提交上来就行。 @@ -127,13 +127,13 @@ python代码 例如这位录友,在提交Java代码的时候,按照题解的意思对Java版本的代码进行的注释,这就很棒👍 -
+
-
+
当然如果大家感觉 已有的代码 不符合以上要求的话,例如 代码思路不够清晰不够规范,注释不够友好,依然欢迎提交优化代码,要记得详细注释哦。 -
+
### 说明具体是哪种方法 @@ -141,10 +141,10 @@ python代码 下面这位录友做的就很好 -
+
-
+
有的题解,是一起给出了多道题目的讲解,例如项目中0102.二叉树的层序遍历.md 中有八道题目,那么大家添加代码的时候 应该在代码注释上,或者 直接写上 是哪个题目的代码。 @@ -162,7 +162,7 @@ python代码 有一位录友在提交代码的时候会把之前的代码 做一下规范性的调整,这就很棒。 -
+
**代码规范从你我做起!** @@ -183,12 +183,12 @@ python代码 在合入的过程中还要处理冲突的代码, 理解大家代码的思路,解决冲突,然后在力扣提交一下,确保是没问题。 例如同一道题目, 一位录友提交了, 我还没处理如何,另一位录友也对这道题也提交了代码,这样就会发生冲突 -
+
大家提交代码的热情太高了,我有时候根本处理不过来,但我必须当天处理完,否则第二天代码冲突会越来越多。 -
+
-一天晚分别有两位录友提交了 30多道 java代码,全部冲突,解决冲突处理的我脖子疼[哭] +一天晚上分别有两位录友提交了 30多道 java代码,全部冲突,解决冲突处理的我脖子疼[哭] 那么在处理冲突的时候 保留谁的代码,删点谁的代码呢? @@ -201,11 +201,11 @@ python代码 确保这种额外文件不要提交。 -
+
还有添加不同方法的时候,直接用正文格式写,哪种方法就可以了,不要添加目录 ,例如这样,这样整篇文章目录结构就有影响了。 -
+
前面不要加 `## 前序遍历(迭代法)`,直接写`前序遍历(迭代法)`就可以了。 @@ -233,11 +233,11 @@ Go语言代码 甚至发现哪里有语病,也欢迎提交PR来修改,例如下面:就是把【下表】 纠正为【下标】 -
+
不用非要写出牛逼的代码才能提交PR,只要发现 文章中有任何问题,或者错别字,都欢迎提交PR,成为contributor。 -
+
## 特别注意 diff --git a/problems/qita/kstar.md b/problems/qita/kstar.md deleted file mode 100644 index 7d1b045c8e..0000000000 --- a/problems/qita/kstar.md +++ /dev/null @@ -1,131 +0,0 @@ -# 代码随想录知识星球 - -前一阵知识星球刚刚发布了[星球精华-大厂八股文(第三版)](https://programmercarl.com/other/kstar_baguwen.html) - -这份八股文,就有30w字,将近400张思维导图,表格,分析图,整个PDF将近900页的篇幅。 - -这些其实都是星球录友们,每日打卡的内容,但这我也仅仅是整理了一部分,因为信息量确实巨大。 - -目前星球里已经有将近1000 个精华帖: - -
- -同时还有[计算机2023届求职薪资PDF](https://programmercarl.com/other/2022salary.html)等一些列独家资料,都在星期置顶帖里: - -
- -星球里的录友都可以得到我1V1的指导,**我已经详细回答了7000+个问题**: (这个回答问题数量,可以看出我有劳模的潜质 ) - -
- -有的时候,大家还是需要过来人,给指点一点,甚至是“踹一脚” 就会想清楚很多。 - -
- -不仅我回答问题,我还会邀请星球里各个方向的录友来和大家一起交流具体技术问题,这个就是星球导师计划: (如果想提问的话,也在星球置顶1可以找到链接) - -
- -
- -可以看看星球导师计划里具体的问答: - -
- -同时我还给录友们至少修改了上千份的简历,我也总结了很多大家写简历上问题。在 「写简历」这个tab上,可以找到我总结的所有问题和简历模板 - -
- -【专业技能】【项目经验】【自我评价】都应该怎么写,面试时候 自我介绍,应该怎么说,我都给出了我的建议: - -
- - -如果你还在犹豫要不要加入的话,**可以进来体验三天,三天内点击知识星球APP右上角,可以自助全额退款**。 绝对不会坑大家! -
- - -一些录友当初也是进来 白嫖一波资料,就退款跑了,不过后面又加回来,例如这位录友: - -
- -**星球里的资料仅仅是辅助,更重要的是星球里的这一圈人,你会发现 这个圈子的质量非常高!** - -不仅仅是 **211、985录友非常之多**,关键是大家都非常努力上进! - -这是知识星球APP里可以看到,录友们的日常打卡: - -
- -刷星球上的内容,要刷朋友圈,刷抖音,有意义的多。 - -
- -
- -星球网页版是这样的: - -
- -加入星球,是很多录友当年做的最有意义的一件事情 - -
-
-
-
- -可以看看星球里的交流氛围: - -
- -
- -
- -
- -
- -大家的很多疑问在星球置顶3,我都做了详细的整理,录友都说我是“整理狂魔”,不过大家懒,我就得勤劳一些。 - -
- -星球置顶3的信息量非常大,不仅仅是整理各个求职方向的学习路线,还有大家的常见疑惑,我之前回答过的内容,都做了整理。 - -大家看完之后,其实对自己就会有明确的规划了。 - -
- -给大家看看星球置顶帖3的部分内容,以下仅仅是部分截图: - -
- -
- -
- -
- - -大家加入星球后,一定要看星球置顶帖和精华帖的内容,你会发现这里很有优秀录友的帖子,包括:各种资料,学习路线,学习心得,规划,职场发展等等。 - -
- -很多录友看完之后都更加明确了自己的方向。 - -
- - -相对于其他星球,「代码随想录」知识星球到底怎么样,可以看看录友们是怎么说的。 - -
- -最后也欢迎大家加入代码随想录[知识星球](https://mp.weixin.qq.com/s/wPaJumc8afuzWLo72yRlIw),**这里有很多优秀的人,有很多精彩的事!** - -
- -这里依然给出10元代金券,微信扫领代金券加入,如果感觉不值得,**三天内知识星球APP右上角直接全额退款!** 无任何套路。 - -
- - diff --git a/problems/qita/language.md b/problems/qita/language.md new file mode 100755 index 0000000000..625154e0a5 --- /dev/null +++ b/problems/qita/language.md @@ -0,0 +1,22 @@ + +# 编程语言基础课 + +「代码随想录」的内容是完全免费的。 + +**不过不少录友是编程零基础**,而刷「代码随想录」至少默认你是会一定的编程语言知识的。 + +如果你是编程零基础,又想快速达到刷算法题(或者说刷代码随想录)所需编程语言的水平,推荐 + +* [C++基础课](../ke/cplus.md) +* [Java基础课](../ke/java.md) +* [Python基础课](../ke/python.md) +* [Go基础课](../ke/go.md) +* [Javascript基础课](../ke/js.md) + +如果你有一定数据结构算法知识,想用数据结构做一个小项目的话,推荐: + +* [手写STL(C++)](../ke/stl.md) +* [kv存储-CPP](../ke/kvcplus.md) +* [kv存储-JAVA](../ke/java.md) + + diff --git a/problems/qita/publish.md b/problems/qita/publish.md new file mode 100755 index 0000000000..5a57ec4e4b --- /dev/null +++ b/problems/qita/publish.md @@ -0,0 +1,200 @@ +# 十年所学,终成《代码随想录》 + +**《代码随想录》终于终于正式出版上市了!** (文末附购买链接,直接五折!) + +[B站介绍](https://www.bilibili.com/video/BV13L4y1E7s4/) + +最近这一年不少录友都问我,代码随想录什么时候出版啊? + +**其实我比大家还期待这一刻的到来!** + +先奉上几张书照片:(封面最终选定为梵高的画作,阿姆斯特丹,圣马迪拉莫,1888,海景) + +
+ +
+ +其实在去年,也就是2020年我就已经将这本书的内容写好了,本以为可以很快出版,但我还是严重低估了写书的工作量。 + +因为自己对质量的追求,一直在不断打磨,所以又是一年快过去了。 + +**《代码随想录》总共将近500页,70w字,200多个插图,真的处处都是心血**。 + +出书是一件浩大工程,比写文章难太多了,**真的字字斟酌,大家看书里可能平平淡淡的一句话、一个词语、一个概念,我可能就查阅很多资料,反复推敲:表达是否准确,用词是否到位,生怕辜负了大家的期待**。 + +这是我自己平时书桌的场景: + +
+ +这两年可以说我没有什么娱乐活动,业余生活极其枯燥,都花费在这本书上了,其中艰辛只有自己知道。 + +而此时当大家都能看到《代码随想录》这部作品的时候,其满足感对我来说已经足以。 + +写这本书用了两年,**而真正消化、理解、研究这些算法知识,我用了整整十年**,十年前我就开始写算法文章,妄图闯进算法的大门,这一写就是十年。 + +
+ +**真的是十年所学,两年打磨,终成《代码随想录》!** + +所以当坚持一件事情的时候,一年、两年,甚至三年、五年,不足以看出其效果,但也许坚持十年的时候,才等到真正收获的时刻。 + +## 代码随想录的故事 + +《代码随想录》不是两年憋大招来个横空出世。 + +而是一点一点打磨出来的,其刷题顺序、题解内容、思考深度 都是经过了上10w录友的共同见证。 + +也正是这些内容,把大家汇聚在一起,一起攻克算法的一座又一座高山。 + +与此同时,也几乎每天都会有录友来专门私信我来表达自己的感激: + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +这些都是大家与“代码随想录”之间的故事,也欢迎大家在文章留言,说一说自己和 “代码随想录”之间的故事。 + +## 《代码随想录》有何不同? + +大家在学习编程、算法,刷题的时候,**真正的苦恼在于没有一套行之有效的刷题顺序**。 + +从何学起,先学什么,再学什么。力扣(Leetcode)上两千道题目,怎么刷,很多人刷题的效率低,主要体现在如下三点: + +* 找题 +* 找到了不合适现阶段做的题 +* 没有全套的优质题解可以参考 + +而市面上基本找不到真正能解决以上痛点的算法书籍。 + +一些书籍是每个知识点蜻蜓点水,然后就叫大家举一反三。 + +一些书籍是一堆题解堆在一起,让大家学起来感受不到知识的连贯性和系统性。 + +断片式的学习,效率怎么能高呢? + +当初我在学习算法的时候,就深感其中的艰难,当我的题量达到一定数量时候,随着反复的琢磨和深入的思考,我再去回顾这些算法题目,**发现如果要是按照合理的顺序来刷题,那效果一定是 事半功倍!** + +所以我将每一个专题中的**题目按照由易到难的顺序进行编排,每一道题目所涉及的知识,前面都会有相应的题目做知识铺垫,做到环环相扣**。 + +**建议大家按照章节顺序阅读本书**,在阅读的过程中会发现我在题目编排上的良苦用心! + +本书不仅在题目编排上精心设计,而且在针对读者最头痛的算法问题上做了详细且深入的讲解。 + +* 关于动态规划,都知道递推公式的重要性,但dp数组的含义、dp数组的初始化、遍历顺序以及如何打印dp数组来排查Bug,这些都很重要。例如,解决背包问题的时候,遍历顺序才是最关键的,也是最难理解的。 + +* 关于回溯算法,题目要求集合之间不可重复,那么就需要去重,各种资料都说要去重,但没有说清楚是“树层去重”还是“树枝去重”——这是我为了说明去重的过程而创造的两个词汇。 + +* 关于KMP算法,都知道使用前缀表进行回退,可什么是前缀表,为什么一定要用前缀表,根据前缀表回退有几种方式,这些却没有说清楚,导致最后大家看的一头雾水。 + +* 关于二叉树,不同的遍历顺序其递归函数究竟如何安排,递归函数什么时候需要返回值,什么时候不用返回值,什么情况下分别使用前、中、后序遍历,迭代法又要如何实现,这些都决定了对二叉树的理解是否到位。 + +同时我针对每一个专题的特点,整理出其通用的解法套路。 + +例如: + +* 在二叉树专题中,总结了递归“三部曲”来帮助读者掌握二叉树中各种遍历方式的写法。 +* 回溯算法中的回溯“三部曲”可以帮助读者理解回溯算法晦涩难懂的过程。 +* 动态规划中的动规“五部曲”可以帮助读者在一套思考框架下去解决动态规划题目。 + +再来说一说动态规划,在程序员面试中,动态规划是公认的最难掌握的算法,也是出现频率最高的算法。 + +**如果仅仅讲解几道题目,即使再举一反三也远远达不到真正理解动态规划的程度**。 + +**如果把动态规划的题目单纯地堆砌在一起,也只会让人越学越懵,陷入“一看就会,一写就废”的怪圈**。 + +讲清楚一道题容易,讲清楚两道题也容易,但把整个动态规划的各个分支讲清楚,把每道题目讲透彻,并用一套方法论把整个动态规划的题目贯彻始终就有难度了。 + +**而帮助大家解决这个问题,这也是这本书的使命所在**。 + +购买方式,可以扫下方二维码,也可以直接[点击这里,京东直达](https://union-click.jd.com/jdc?e=&p=JF8BAMQJK1olXg8EUVhVCkkWAV8IGV8WVAICU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAUF1UDEsQHDZNRwYlGFh6NVkPcRdyHWwMZRlLHlQDUj02eEcbM244GFIXWQYAUV5VOHsXBF9edVsUXAcDVVtdDUgnAl8IHFkdXw4BU1lfCkoRM2gIEmtIFVpKAxVtOHsUM184G2sWbURsUVpcCEMVAjgIHAxBWFYAAVdfXE8QBGkBGQsdCQEFVgttCkoWB2Y4) + +
+ + +## 目录 + +
+ +
+ +这里不少录友会问,书的内容和Github:https://github.com/youngyangyang04/leetcode-master,和网站:programmercarl.com 有什么区别呢? + +其实写文章相对来说是随意一些的,但书一定要非常严谨。 + +正如我本篇开头所说,书的内容其实一年前就写好的,但排版、纠错、打磨、重新画图,又花费了一年,所以书一定是更精细的,更严谨的。 + +**《代码随想录》的排版看起来非常舒服,会让你发现 原来学算法 会上瘾!** + +
+ +《代码随想录》的推荐语,我都是颇为用心,不是随随便便找个人写一写推荐语来凑数的。 + +
+ +哈工大计算机王院长,百度杰出架构师猛哥,腾讯专家工程师强哥,王道论坛创始人风华哥,**他们是在我学习工作的不同阶段里对我影响非常大的顶级巨佬**。 + +他们的学习方法,做事风格,都是值得每一位技术人学习。同时他们也是每一位技术人的榜样。 + +特别感谢巨佬们能在百忙之中阅读了本书的书稿,并给本书写了评语。 + +## 最后 + +我希望这本书,不仅仅是可以帮助大家学习编程,循序渐进的去学习算法,高效刷题,进大公司。 + +**同时 当你把这本书放在自己的书桌前,床头前的时候,它也会给你一种乘风破浪的勇气!** + +正如封面(梵高,阿姆斯特丹,圣马迪拉莫,1888,海景),一只帆船在波涛汹涌的大海里扬帆远航! + +《代码随想录》这就要和大家见面了,其实很多录友已经迫不及待: + +
+ +
+ +
+ +
+ +这本书原价还挺贵的(毕竟比较厚),但这里申请到了京东五折优惠,大家可以速度下手了。 + +点击下方链接直接五折购买,全网最低价格了。海外的录友们可以在等几天,广州有货之后,就可以配送的海外了。 + +《代码随想录》使用的语言是C++,使用其他语言的录友可以看本书的讲解思路,刷题顺序,然后配合看网站:programmercarl.com,网站上都对应的Java,Python,Go,Js,C,Swift版本 基本可以满足大家的学习需求。 + +购买方式,可以扫下方二维码,也可以直接[点击这里,京东直达](https://union-click.jd.com/jdc?e=&p=JF8BAMQJK1olXg8EUVhVCkkWAV8IGV8WVAICU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAUF1UDEsQHDZNRwYlGFh6NVkPcRdyHWwMZRlLHlQDUj02eEcbM244GFIXWQYAUV5VOHsXBF9edVsUXAcDVVtdDUgnAl8IHFkdXw4BU1lfCkoRM2gIEmtIFVpKAxVtOHsUM184G2sWbURsUVpcCEMVAjgIHAxBWFYAAVdfXE8QBGkBGQsdCQEFVgttCkoWB2Y4) + +
+ +最后也感谢录友们的陪伴,真心希望大家都有一个好的前程! + +正如《代码随想录》正式出版一样,**你所期盼,终将到来! 加油💪** + + diff --git a/problems/qita/say_feel.md b/problems/qita/say_feel.md new file mode 100755 index 0000000000..00df8c0bf2 --- /dev/null +++ b/problems/qita/say_feel.md @@ -0,0 +1,14 @@ +恭喜你,已经把代码随想录通关了,欢迎在[卡码笔记](https://notes.kamacoder.com/question/102144)记录一下自己的收获,写一篇小作文。 + +不过一刷代码随想录,理解的一定是不到位的,建议二刷之后,对各个经典类型的题目就有自己的想法了。 + +大家可以在自己的博客写一篇 代码随想录一刷总结,记录这阶段性进步的一刻。 + +如果感觉代码随想录对你确实有帮助,不用买书,欢迎去[豆瓣](https://book.douban.com/subject/35680544/)给一个好评就好,代码随想录在豆瓣上被人恶意抹黑,希望录友们可以去说一说自己刷代码随想录的真实感受,感谢录友们的支持。 + +希望大家都能梦想成真,有好的前程,加油💪 + + + + + diff --git a/problems/qita/server.md b/problems/qita/server.md index 7e214d7993..890cf8bcd5 100644 --- a/problems/qita/server.md +++ b/problems/qita/server.md @@ -51,7 +51,7 @@ 操作方式这样,我把命令包 包装成一个shell命令,想传那个文件,直接 uploadtomyserver,然后就返回可以下载的链接,这个文件也同时传到了我的服务器上。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211126165643.png) +![](https://file1.kamacoder.com/i/algo/20211126165643.png) 我也把我的项目代码放在了github上: @@ -93,11 +93,11 @@ https://github.com/youngyangyang04/fileHttpServer 就是这样一个非常普通的查询页面。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211126160200.png) +![](https://file1.kamacoder.com/i/algo/20211126160200.png) 查询通过之后,就会展现返现群二维码。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211127160558.png) +![](https://file1.kamacoder.com/i/algo/20211127160558.png) 但要部署在服务器上,因为没有公网IP,别人用不了你的服务。 diff --git a/problems/qita/shejimoshi.md b/problems/qita/shejimoshi.md index cf980e45f6..959a3fa90a 100644 --- a/problems/qita/shejimoshi.md +++ b/problems/qita/shejimoshi.md @@ -1,15 +1,17 @@ +# 23种设计模式精讲 | 配套练习题 | 卡码网 + 关于设计模式的学习,大家应该还是看书或者看博客,但却没有一个边学边练的学习环境。 学完了一种设计模式 是不是应该去练一练? 所以卡码网 针对 23种设计,**推出了 23道编程题目,来帮助大家练习设计模式**。 -
+
这里的23到编程题目对应了 23种这几模式。 例如第一题,小明的购物车,就是单例模式: -
+
区别于网上其他教程,本教程的特点是: @@ -38,18 +40,18 @@ 同时还给全部整理到PDF上,这份PDF,我们写的很用心了,来个大家截个图: -
+
-
+
-
+
-
+
关于设计模式的题目,大家现在就可以去 卡码网(kamacoder)去做了。 关于这23道题目对应 设计模式精讲 PDF,也免费分享给录友们,大家可以加我的企业微信获取: -
+
已经有我企业微信的录友,直接发:设计模式,这四个字就好,我会直接发你。 diff --git a/problems/qita/tulunfabu.md b/problems/qita/tulunfabu.md new file mode 100644 index 0000000000..8987709564 --- /dev/null +++ b/problems/qita/tulunfabu.md @@ -0,0 +1,239 @@ + + +# 图论正式发布! + +录友们! 今天和大家正式宣布:大家期盼已久的「代码随想录图论」正式发布了。 + +**一年多来,日日夜夜,伏案编码、思考、写作,就为了今天给录友们一个交代**! + +我知道录友们在等图论等太久了,其实我比大家都着急。 + +![大家一直都在催](https://file1.kamacoder.com/i/algo/20240613105618.png) + +图论完整版目前已经开放在代码随想录网站:programmercarl.com + +**「代码随想录图论」共分为 五大模块**,共有三十一篇长文讲解: + +* 深搜与广搜 +* 并查集 +* 最小生成树 +* 拓扑排序 +* 最短路算法 + +![](https://file1.kamacoder.com/i/algo/20240613104436.png) + +**耗时一年之久,代码随想录图论 终于面世了**! + +一年前 23年的3月份 我刚刚更新完了 [代码随想录算法公开课](https://mp.weixin.qq.com/s/xsKjrnB4GyWApm4BYxshvg) ,这是我亲自录制的 140期 算法视频讲解,目前口碑极佳。 + +录完公开课之后,我就开始筹划更新图论内容了,无奈图论内容真的很庞大。 + +关于图论章节,**可以说 是代码随想录所有章节里画图数量最多,工程量最大的一个章节**,整个图论 画图就400百多幅。 + +随便截一些图,大家感受一下: + +![](https://file1.kamacoder.com/i/algo/20240613104703.png) + +![](https://file1.kamacoder.com/i/algo/20240613104824.png) + +![](https://file1.kamacoder.com/i/algo/20240613104852.png) + +![](https://file1.kamacoder.com/i/algo/20240613104926.png) + +![](https://file1.kamacoder.com/i/algo/20240613105007.png) + +![](https://file1.kamacoder.com/i/algo/20240613105030.png) + +![](https://file1.kamacoder.com/i/algo/20240613105106.png) + +![](https://file1.kamacoder.com/i/algo/20240613105143.png) + +具体内容,大家可以去代码随想录网站(programmercarl.com)去看看,非常精彩! + +期间有很多录友再催:**卡哥怎么不更新图论呢? 卡哥是不是不打算更新图论了**? 等等 + +每次我都要去解释一波。 + +如果我想 快速出图论章节,可以很快! + +**但我不想做 降低 代码随想录整体质量和口碑的事情**。 + +所以关于现在发布的「代码随想录图论」,我可以很自信的说,**这是市面上大家能看到的,最全、最细致图论算法教程**。 + +**我在写作的时候没有避开任何雷区,完全遇雷排雷,然后让大家舒舒服服的走过去**。 + +什么是雷区? + +很多知识点 ,大家去看资料的时候会发现是 没有讲解的或者一笔带过, 因为这种雷区知识点 很难讲清楚或者需要花费大量的时间去讲明白。 + +一些知识点是这样的:**自己一旦懂了就知道是那么回事,但要写出来,要给别人讲清楚,是很难的一件事**。 + +这些知识点同样是雷区,就是大家在看 教程或者算法讲解的时候,作者避而不谈的部分。 + +例如: 深搜为什么有两种写法,同样的广搜为什么有一种写法超时了,bellman_ford 为什么 要松弛 n - 1次,负权回路对最短路求解的影响 等等。 + +这一点大家在阅读代码随想录图论的时候,**可以感受到 我对细节讲解的把控程度**。 + +## 为什么要出图论 + +图论是很重要的章节,也是 大家求职笔试面试,无论是社招还是校招,都会考察的知识内容。 + +而且图论应用广泛,在大家做项目开发的时候,或多或少都会用到图论相关知识。 + +例如:通信网络(拓扑排序、最短路算法),社交网络(深搜、广搜),路径优化(最短路算法),任务调度(拓扑排序),生物信息学(基因为节点,基因关系为边),游戏开发(A * 算法等)等等 + +为了保质保量更新图论,**市面上所有的算法书籍,我都看过**! + +**反复确认 思路正确性的同时,不仅感叹 市面上的算法书籍 在图论方面的 “缺斤少两**” 。 + +大名鼎鼎的《算法4》 以图论内容详细且图解多 而被大家好评, + +以最短路算法为例,《算法4》,只讲解了 Dijkstra(堆优化)、SPFA (Bellman-Ford算法基于队列) 和 拓扑排序, + +而 dijkstra朴素版、Bellman_ford 朴素版、bellman_ford之单源有限最短路、Floyd 、A * 算法 以及各个最短路算法的优劣,并没有讲解。 + +其他算法书籍 更是对图论的很多知识点一笔带过。 + +而在 代码随想录图论章节,**仅仅是 最短路算法方面,我就详细讲解了如下内容**: + +* dijkstra朴素版 +* dijkstra堆优化版 +* Bellman_ford +* Bellman_ford 队列优化算法(又名SPFA) +* bellman_ford 算法判断负权回路 +* bellman_ford之单源有限最短路 +* Floyd 算法精讲 +* 启发式搜索:A * 算法 + +**常见最短路算法,我都没有落下**。 + +而且大家在看很多算法书籍是没有编程题目配合练习,这样学习效果大打折扣, 一些书籍有编程题目配合练习但知识点严重不全。 + +## 出题 + +我在讲解图论的时候,最头疼的就是找题,在力扣上 找题总是找不到符合思路且来完整表达算法精髓的题目。 + +特别是最短路算法相关的题目,例如 Bellman_ford系列 ,Floyd ,A * 等等总是找不到符合思路的题目。 + +所以我索性就自己出题吧,**这也是 卡码网(kamacoder.com)诞生的一个原因之一**。 + +**为了给大家带来极致的学习体验,我在很多细节上都下了功**夫。 + +卡码网专门给大家准备的ACM输入输出模式,**图论是在笔试还有面试中,通常都是以ACM模式来考察大家**,而大家习惯在力扣刷题(核心代码模式),核心代码模式对图的存储和输出都隐藏了。 + +**图论题目的输出输出相对其他章节的题目来说是最难处理的**。 + +### 输入的细节 + +图论的输入难在 图的存储结构,**如果没有练习过 邻接表和邻接矩阵 ,很多录友是写不出来的**。 + +而力扣上是直接给好现成的 数据结构,可以直接用,所以练习不到图的输入,也练习不到邻接表和邻接矩阵。 + +ACM输入输出模式是最考察候选人对代码细节把控程度。 + +如果熟练ACM模式,那么核心代码模式基本没问题,但反过来就不一定了。 + +### 输出的细节 + +同样,图论的输出也有细节,例如 求节点1 到节点5的所有路径, 输出可能是: + +``` +1 2 4 5 +1 3 5 +``` + +表示有两条路可以到节点5, 那储存这个结果需要二维数组,最后在一起输出,力扣是直接return数组就好了,但 ACM模式要求我们自己输出,这里有就细节了。 + +就拿 只输出一行数据,输出 `1 2 4 5` 来说, + +很多录友代码可能直接就这么写了: + +```CPP +for (int i = 0 ; i < result.size(); i++) { + cout << result[i] << " "; +} +``` + +这么写输出的结果是 `1 2 4 5 `, 发现结果是对的,一提交,发现OJ返回 格式错误 或者 结果错误。 + +如果没练习过这种输出方式的录友,就开始怀疑了,这结果一样一样的,怎么就不对,我在力扣上提交都是对的! + +**大家要注意,5 后面要不要有空格**! + +上面这段代码输出,5后面是加上了空格了,如果判题机判断 结果的长度,标准答案`1 2 4 5`长度是7,而上面代码输出的长度是 8,很明显就是不对的。 + +所以正确的写法应该是: + +```CPP +for (int i = 0 ; i < result.size() - 1; i++) { + cout << result[i] << " "; +} +cout << result[result.size() - 1]; +``` + +这么写,最后一个元素后面就没有空格了。 + +这是很多录友经常疏忽的,也是大家刷习惯了 力扣(核心代码模式)根本不会注意到的细节。 + +**同样在工程开发中,这些细节都是影响系统稳定运行的因素之一**。 + +**ACM模式 除了考验算法思路,也考验 大家对 代码的把控力度**, 而 核心代码模式 只注重算法的解题思路,所以输入输出这些就省略掉了。 + +## 情怀 + +大家可以发现,**现在 用心给大家更新硬核且免费资料的博主 已经不多了**。 + +这一年的空闲时间,如果我用来打磨付费课程或者付费项目,或者干脆把图论做成付费专栏 加上现在的影响力,一定可以 “狠狠赚一笔”。 + +对我来说,有些钱可以赚,有些钱不赚。 + +如果持续关注代码随想录的录友可以发现:代码随想录不仅仅优质题解和视频免费,还有 [ACM模版配套25题](https://mp.weixin.qq.com/s/ai_Br_jSayeV2ELIYvnMYQ)、 [设计模式精讲配套23题](https://mp.weixin.qq.com/s/Wmu8jW4ezCi4CQ0uT9v9iA)、[每周举办大厂笔试真题(制作真题是十分费时的)](https://mp.weixin.qq.com/s/ULTehoK4GbdbQIdauKYt1Q), 这些都是免费优质资源。 + +**在付费与免费之间,我一直都在努力寻找平衡**。 + +很多录友之所以付费加入 [知识星球](https://mp.weixin.qq.com/s/65Vrq6avJkuTqofnz361Rw) 或者 [算法训练营](https://mp.weixin.qq.com/s/vkbcihvdNvBu1W4-bExoXA) ,也是因为看了这些免费资源想支持我一下。 + +“不忘初心”,说出来很容易,**但真正能随着岁月的流淌 坚持初心,是非常非常难的事情**。 + +**诱惑太多!有惰性的诱惑,有利益的诱惑**。 + +正如我之前说的:“代码随想录” 这五个字,我是会用一生去经营。 + +**免费硬核的算法内容是 代码随想录的立身之本**,也是 大家为什么学算法学编程首选代码随想录的根本所在。 + +当大家通过 代码随想录 提升了编程与算法能力,考上研或者找到好工作的时候,于我来说已经是很幸福的事情: + +![对笔试帮助大](https://file1.kamacoder.com/i/algo/20230914172536.png) + +![华为od将近满分](https://file1.kamacoder.com/i/algo/20230914172607.png) + +![研究生复试](https://file1.kamacoder.com/i/algo/20240621103130.png) + +![红包感谢代码随想录366](https://file1.kamacoder.com/i/algo/20231123151310.png) + +![上岸亚马逊](https://file1.kamacoder.com/i/algo/20240206174151.png) + +![](https://file1.kamacoder.com/i/algo/20220718094112.png) + +![](https://file1.kamacoder.com/i/algo/20220718094332.png) + +至此**图论内容 已完全免费开放在代码随想录网站(programmercarl.com),造福广大学习编程的录友们**! + +Github 也已经同步 :https://github.com/youngyangyang04/leetcode-master ,关于其他语言版本,欢迎录友们去仓库提交PR + +## 后序 + +关于图论PDF版本,我后面会整理出来,免费发放给大家。 + +关于图论视频版本,不出意外,应该在年底开始在B站更新,同样免费开放。 + +总之,代码随想录会持续更新下去,无论是文字版还是视频版。 + +希望大家 不用 非要到找工作的时候 或者要考研的时候 才想到代码随想录。 + +**代码是作品,算法更是艺术**,时不时来欣赏一段解决关键问题的优雅代码,也是一种享受。 + +最后,**愿录友们学有所成,归来仍看代码随想录**! + + + diff --git a/problems/qita/tulunshuoming.md b/problems/qita/tulunshuoming.md new file mode 100755 index 0000000000..9d6e761ca8 --- /dev/null +++ b/problems/qita/tulunshuoming.md @@ -0,0 +1,44 @@ + +# 图论模块说明 + + +非常多录友在催更图论,同时大家也反馈面试中深搜广搜也最近常考的类型。 + +其实在代码随想录中的二叉树和回溯算法章节中已经讲过深搜和广搜,二叉树的遍历就是深搜和广搜在二叉树结构上的应用, 而回溯算法本身就是深搜,只不过利用其回溯的过程。 + +那么在图论中,深搜和广搜就是在图上的遍历,图的存储方式一般是 邻接表和邻接矩阵。 + +我已经在更新图论ing,不过还没有跟更新完,**之前计划是把更新完的部分先分享给[训练营](./xunlianying.html)和 [知识星球](./kstar.md) 录友,等全部更新完之后在完整的分享到网站上**。 + +不过其他录友们也很着急,我也算更新了不少了,就先分享出来给大家吧。 + +**我一直坚持给大家打造最硬核的算法教程而且是免费的!这一点一直都不会变!**。 + +(**注意图论章节还没有更新完,还有更精彩的内容在路上**) + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/problems/qita/update.md b/problems/qita/update.md new file mode 100755 index 0000000000..c562fd4b3c --- /dev/null +++ b/problems/qita/update.md @@ -0,0 +1,56 @@ + +## 2021年 8月11日 + +[代码随想录网站正式上线](https://mp.weixin.qq.com/s/-6rd_g7LrVD1fuKBYk2tXQ) + +## 2021年 10月19日 + +更新Java,Python,JS,Go版本题解 + +## 2022年 1月17日 + +添加评论功能和阅读量统计。 + +为了方便大家阅读,使用无需登录的评论插件 valine。 + +但由于本站访问量太大,leancloud的api调用超过3w次,就只能用付费版本了,本站使用2个小时之后就超过了3w次条用,而付费版本一年要上万块。 + +免费的网站实在承担不起,所以仅在部分页面添加的评论区。 + +例如各个专题中的理论基础和本章总结,都添加的评论区。 + +## 2022年 2月22日 + +升级内存和带宽以应对更大的访问量 + +## 2022年 5月12日 + +更新[星球生活](https://programmercarl.com/other/)专栏,题解支持C、TypeScript 语言版本 + +## 2022年 5月19日 + +补充[额外题目](https://programmercarl.com/other/ewaishuoming.html) + +## 2022年 6月10日 + +添加边框,可以方便调节黑暗模式,开始加入Scala 和 C# 语言版本。 + +## 2023年 5月8日 + +题解都配上了《代码随想录》算法公开课视频讲解 + +## 2023年 9月11日 + +更新部分图论内容,深搜广搜和并查集 + +## 2024年 4月7日 + +由于访问量过大,网站访问速度慢一直被很多录友诟病,特别是海外录友访问更卡。 + +这次网站全部上CDN,全球加速,方便全球录友学习。 + +同时添加github评论区,录友可以在每篇文章下打卡了! + +## 更多精彩,敬请期待 + + diff --git a/problems/qita/xunlianying.md b/problems/qita/xunlianying.md deleted file mode 100644 index 0d8c1c83a6..0000000000 --- a/problems/qita/xunlianying.md +++ /dev/null @@ -1,298 +0,0 @@ - -# 代码随想录算法训练营 - -> 训练营17期将在6月28日开营,目前可以报名,提前拉群,在群里等着开营就好! - -大家可以百度搜索:代码随想录算法训练营, 看看往期录友们在训练营里打卡总结的博客。 - -
- -这是训练营里录友坚持到最后一天的打卡,大家可以看看他们的博客是每天都有记录的: - -* [训练营结束,深感坚持是最难的(Java-犯困-东南研二)](https://blog.csdn.net/weixin_57956443/article/details/128995318) -* [训练营一刷总结(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) -
- -博客链接:[https://juejin.cn/post/7170304080504586254](https://juejin.cn/post/7170304080504586254) -
- -博客链接:[https://blog.csdn.net/weixin_44047621/article/details/128430623](https://blog.csdn.net/weixin_44047621/article/details/128430623) -
- -博客链接:[https://blog.csdn.net/weixin_47467016/article/details/128460565](https://blog.csdn.net/weixin_47467016/article/details/128460565v) -
- -也有一些录友,把总结发在训练营内部打卡表里,例如: - -昵称:java-低调-已工作 - -通过两个月的时间系统性的学习了算法,然后按照不同的题目去做分类,设计的刷题进度也很好,让自己有了一个质的提升,贵在坚持,好在自己也是坚持了下来,**通过自己的坚持,让自己养成了一个刷题的好习惯,这才是最难能可贵的**。 - -但是时间跨度有点大,还是要继续坚持之后自己去二刷,这样才能更好的巩固,把算法知识学习的更好。 - ---------- - -昵称:java-岂几岂几-毕业 - -收获真的很大,这是第一次刷算法题,清楚了面试高频题的题型,**巩固了之前摇摇欲坠的自学算法基础**。接下来计划是重刷随想录,并且补充上一亩三分地刷题区置顶贴里列出的题型,在巩固一刷的基础上增加做题量。 - ------------- - -昵称:python/go-ds-研三 - -跟着卡哥的训练营最大的收获就是把代码随想录都通读了一遍,因为进营之前就已经刷过不少力扣题了,但很多都是当时自己捣鼓出来或者看官方题解的。 - -而这一次的60天刷题,不管题目做没做过,都看过卡哥的代码随想录了,**这其中的区别也是最大的收获就是知识体系建立起来了**,越往后做题,条理越清晰。 - -即使有些题一刷还是做不太出来,但不再像之前自己做那样做题前后都是懵逼状态了,而是有一个清晰明了的判断了。 - -但coding能力还是有待改进,接下来要进行二刷,同时也祝卡哥的事业蒸蒸日上,代码随想录越办越好! - ------------- - -昵称:Python-ukn-研二 - -完美收官,有点小遗憾的是后面dp做得有点赶,没有沉下心来消化,接下来重点把自己不擅长的专题和重点专题二刷甚至三刷。 - -**跟着训练营练下来最大的感受是很有信心,有节奏有计划**,每过完一个专题,就多一分成就感,题感也越来越好,期待自己的规律二刷,谢谢一路坚持的小伙伴们!谢谢大佬助手和卡哥! - - ------------ - - -### 训练营的目的是什么? - -对于刷题,学算法,[《代码随想录》](https://programmercarl.com/other/publish.html)(programmercarl.com)已经把刷题顺序给大家列好了,大家跟着刷就行。 - -但即使这样,其实不少录友还会有很多疑问,不知道怎么用代码随想录,例如: - -* 卡哥,**有没有一起从0开始刷代码随想录的录友,想一起组个队** -* 卡哥您好,我是985准研一非科班,自学java, 然后现在在刷代码随想录,**请问需要每个题目的所有解法都掌握吗**?请教下卡哥正确的刷题姿势🙏 -* **我大概多久才能刷完代码随想录**? -* 二叉树,我只掌握 递归够用么? -* 很多解法,我是不是只用暴力就可以,**时间比较紧,我还要去掌握优化方法吗**? -* 卡哥,**请问跟着代码随想录刷题有答疑的服务吗**? 因为有的题目 自己写的怎么都不对,浪费很多时间,可能过来人指点一下立刻就知道。 -* 卡哥,我KMP太难了,我跳过可以吗? -* 卡哥,我进了刷题群,可是**大家刷题进度不尽相同,所以讨论起来经常不在一个频道上**。 -* 刚开始还看了一周代码随想录,后来又..摆烂了... **最近又重新再看代码随想录,然后卡住了又摆烂了好几天了**...... -* 卡哥,**我刷题很容易囫囵吞枣,虽然说代码随想录一刷,但很多内容根本没消化,在进度上欺骗自己**,好像一刷完了,但感觉自己理解的,不到30%。 -* 卡哥,**感觉之前刷的都忘了,能力没有什么提升,现在还是一道都不会做**。我一刷每道题都得先看看题解然后忘了再去看边看边写。 - -**以上这些是不是有戳中某些录友们的痛处**。 - -其实对于很多算法基础不太好的录友,即使资料已经很齐全,但还是需要一些规划和答疑。 - -而且在时间规划上,因为刚开始刷的录友,不知道 前方题目 是多大难度,所以 一开始计划 一天刷三道,往往因为遇到了一道难题,一天也解决不了,耽误了整体进度,甚至直接开始摆烂,下次再开始刷题可能就很久以后了...... - -所以 **代码随想录算法训练营** 帮助大家在规划时间内,有质量的完成代码随想录一刷。 - -我亲自给大家规划节奏,大家一起按照我的节奏来,规定时间内,一刷一定能把代码随想录所有内容吃透,然后大家自己去二刷,三刷就好了,师傅领进门修行在个人。 - -### 训练营提供一些什么呢? - -1.具体内容 - -针对代码随想录上,**195篇算法文章,主要题目150道**,手把手带大家刷完,帮大家做好详细刷题规划,每天布置刷题任务,监督博客记录总结。 - -任务布置 -
- -每日规划: -
- -训练营周期内,每天应该做哪些题目,同时我根据题目的难度,适当调整每天学习安排,不会是 每天固定3题的这种,而是根据难度而定。 - -我会告诉大家,哪些解法是一刷的时候必须掌握的,哪些解法可以二刷再去学习,哪些总结是必看的。 - -每日打卡: -
- -关于如果debug自己的代码,训练营会给具体建议: -
- -训练营群中每日讨论的重点内容都会做整理,在分享给大家训练营成员: -
- -**同时每天做针对大家的疑问做详细答疑,保证大家消化当天的学习内容**。 - -2.**气氛气氛还是气氛** - -训练营中,**大家都是同一个基础,同一个进度刷题,每天刷题题目都是一样的**,这样的一个学习群,大家讨论起来更有意义。 -**还有会监督机制**,训练营的成员要注册一个自己的博客(自己搭建或者使用博客网站都可以),每天要去写今日刷题心得和总结,来进行打卡。 - -
- -3.带大家写博客 - -很多录友平时刷题,或者学习技术,没有写博客的习惯,或者因为懒,就不写了。 - -但大家学了很多技术之后,发现 好像都忘了。。。 - -所以训练营会带着大家写博客,每天都要写博客,博客的标题,格式,我都帮大家规划好,倒逼自己养生记录的习惯。 - -因为训练营很多录友开始有了写博客的习惯,以下是一些录友博客的结尾部分: - -
- -
- -每天训练营群里会每天统计大家的博客情况。这样不仅可以监督自己总结,针对大家写的比较好的博客,会给予曝光,增加自己写博客的动力。 - -训练营里的录友们可以相互参考对方的博客,看谁总结的更好。 - -
- - -4.关于答疑 - -很多录友可能担心自己的问题,得不到解决,或者在群里和大家讨论,也没人回复 导致自己因为小问题卡了很久,甚至直接摆烂好一阵子。 - -所以训练营里大家的问题,我都会做答疑。 - -估计训练营里的问题会比较多,我也可能回答不过来。所以我会找了算法能力很强的助手协助我给大家答疑,也就是说,**大家刷题遇到问题,不会有后顾之忧,当天的问题,当天一定会得到解决**。 - -
- -当然训练营题目答疑,**仅限于 每天规划的题目**,并不会大家刷的其他算法题都做答疑,那样的话工作量很容易不可控(这里我也不会夸大承诺,欺骗大家报名之类的),如果是其他算法题可以在群里和大家交流。 - -### 训练营的资料是什么呢? - -**强调一下:训练营里所有的资料,都是我独立制作而且是开源免费的:即代码随想录网站(programmercarl.com),Github:https://github.com/youngyangyang04/leetcode-master和[代码随想录算法公开课](https://mp.weixin.qq.com/s/xncn6IHJGs45sJOChN6V_g)** - -训练营提供给大家的服务是**规划,监督,指导和答疑**。 - -至于代码随想录算法内容的质量如何,这个已经是有口皆碑了,基本是面试求职必刷的资料。 - -
- -
- -而且代码随想录开源的内容要比市面上 大家付费几百,上千元报的算法训练营的资料都要好的多。 - -毕竟内容是开源的,质量如何 大家自己去看就好。 - -### 训练营的学习方式 - -组织方式:一个学习微信群(180人左右),大家进群之后,等群公告就好,我会通知开始时间和每日刷题计划。 - -所需时间:训练营为期60天(两个月),群里每天会布置学习任务,只要大家跟上节奏,60天一定可以刷完代码随想录。 - -每日任务:需要花费3-4个小时左右的时间来完成。这是针对一般算法水平的学习速度来规划的时间,不同水平会有差异。 - -每周周日会休息一天,没跟上进度的录友,可以跟进度,跟上进度的录友可以复习或者适当放松一下。 - -监督机制:训练营里,每天会针对大家每天所刷的题目做答疑,同时也会有监督打卡机制,在群公告里会详细描述。 - -所需语言:**所有语种都可以**,毕竟代码随想录几乎支持所有主流语言,**也会针对大家所用的语言做针对性答疑**。 - -### 开营时间 - -**训练营开始常态化报名,即一直可以报名,当人满180人的时候,就开始新的一期**。 最新的一期可以看文章评论区。 - -### 训练营的价格 - -大家应该最关心的是价格了,**定价依然是268元**,注意这是两个月训练营的费用,而且是全程规划,指导,监督和答疑。 - -(对于[知识星球](https://programmercarl.com/other/kstar.html)里的录友的话,训练营会立减30元,也就是238元,后面如果推出其他服务,星球录友都相当于VIP,都会有优惠。当然如果你已经报了训练营,再去报知识星球,并不给再给大家优惠了,一定要先是星球成员,再报训练营才有优惠) - -大家能在市面上找到算法训练营都价格不菲,基本都是上千的单价,**而且内容和质量并没有 代码随想录 优质**。 - -后面一定会涨价的,**如果你确实需要有人带,有监督,给规划,有答疑,能花两个月时间更下来的话,还是早报早学习**。 - -### 我适合报名吗? - -符合一下特点的录友可以报名: - -* 基础比较差,没刷过代码随想录或者刚开始刷 -* 刷过一些代码随想录的题目了,感觉掌握不扎实,想用2个月时间系统重刷一遍 -* 自己刷题,**很容易遇到各种代码问题,需要有人答疑** -* 以前一刷过,但基本都忘了,想高质量二刷 -* **想找队友,一起从0刷代码随想录** -* 自控能力差,遇到点问题就容易躺好一阵子,需要别人监督学习 -* **想有一个规划时间,来刷完代码随想录** -* 不知道代码随想录中哪些解法是必备的,哪些解法是可以简单了解的 -* 刷题总会忘,感觉刷了和没刷差不多,**不擅长做总结,不擅长写博客记录心得**,自己也懒得写博客写总结 - -以下录友不合适报名: - -* 自学能力强,代码随想录资料都是开源的,刷题顺序也列好了,自学能力强的录友自己学就行 -* 有算法和代码基础,基本算法题遇到的问题,都能通过自己debug解决 -* 没有两个月时间,每天也不能抽出那么多时间学习算法 -* 算法0基础,基本的数据结构都没听说过,例如数组,链表。 -* 编程0基础,基本的编程语言还不会,因为训练营还是默认大家会熟悉所用编程语言里的各种容器的使用 - -**训练营不限编程语言**,任何语言都可以报名,都会答疑。 - - -### 常见疑问 - -**海外录友有时差可以报名吗**? - -可以的,一期就有很多海外的录友,有疑问在群里也会回复,而且群里讨论的重点内容,都会有总结,不用担心错过了精彩内容。 - -**已经工作的录友适合报名吗**? - -适合报名。对于工作的录友,每天未必说一定挤出3-4个小时来刷题。 - -对于时间充足的录友,要刷拓展题,要写博客作总结。 如果时间紧张,任务上是可以适当精简。 - -所以每日任务弹性还是比较大的,至少跟上进度保证每天的题目代码提交通过了,看看群里的讨论,自己理解加深了就可以。 - -工作的录友要学会挤时间,训练营一期录友有不少是工作的,他们是这么搞的: - -训练营每日晚上提前发布明天的任务, 他们第二天通勤 时候 可以先看题想思路,白天抽空看文章解析 看看思路是否一致,看看群里讨论内容,晚上下班可以一口气把当天的题目刷完。 - -加入训练营,每日对自己有一个压迫感,挤一挤 时间就有了。 - -对于工作的录友,我之前本来计划是安排一期 工作日题量小 休息日题量多一些的训练营,但通过一期发现,包括已经工作的录友,**大家休息日真的没有心思学习,甚至“比工作日更忙”**,所以理想很丰满,现实很骨感。 - -**要不要搞三个月四个月半年的训练营**? - -目前来看四个月以上的时间有点太长了,时间长价格也会高,毕竟要服务的时间长了。 - -而且刷题在于一鼓作气,把时间拉的太长,很多录友都是前期 动力十足,后面无论是 如何@ 如何公告 如果催大家 赶进度,大家都会无动于衷,从最终效果来看 战线不能太长。 - -所以没有逼自己一把 跟上进度的决心,就算搞一年时间的训练营,该放弃的还是会放弃。 - -至于三个月的训练营,是可以考虑的,不过安排时间还要待定。 - -### 报名方式 - -扫码支付268元。 (如果是[代码随想录知识星球](https://programmercarl.com/other/kstar.html)成员录友,只需要支付238元,提交客服的时候需提供知识星球截图,**注意一定要是代码随想录知识星球**) - -
- -付款后,将付款截图发给客服,客服会在24h内统一回复,**所以大家发给客服信息不要急,当天一定会回复的**。 - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230603175235.png) - -客服的联系方式就在大家的微信聊天窗口,不用担心突出聊天窗口错过消息,客服回复之后 会有微信提示的。 - -关于训练营的任何问题,可以在客服这里咨询! - - -### 最后 - -训练营其实算是代码随想录的一个补充,其内容都是免费开放的,有学习能力的录友自己学习就好。 - -单就从我的 [代码随想录算法公开课](https://mp.weixin.qq.com/s/xncn6IHJGs45sJOChN6V_g) 来说,质量如何,大家可以去看评论区,我完全可以把它做成付费的视频课,但我还是选择免费开放给大家,目前一周会更新四个算法视频,已经快把二叉树系列更完了。 - -之所以做训练营,是因为大家太多的问题,不是视频或者文章教程可以解决的,需要的是规划,组织,监督和答疑。 - -所以我才组织训练营,搞成付费的也是为了质量更高一些,同时也是因为需要一些门槛,要不然就和普通刷题群没什么区别了。 - -等大家跟着代码随想录训练营一路走下来之后,大家再回顾自己两个月学习的内容和总结的博客,**一定会发现 这个价格 物超所值**! - -关于训练营的任何疑问都可以扫码联系客服 - -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20230603175235.png) - diff --git a/problems/toolgithub.sh b/problems/toolgithub.sh index ebe9c7df7d..4a0b0a75f0 100644 --- a/problems/toolgithub.sh +++ b/problems/toolgithub.sh @@ -7,21 +7,41 @@ ######################################################################### #!/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 + 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 # ex -sc '1i|

' -cx $i + + ex -sc '1i|* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html)' -cx $i + ex -sc '1i|* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)' -cx $i + ex -sc '1i|* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html)' -cx $i + + # echo '## 其他语言版本' >> $i # echo '\n' >> $i # echo 'Java:' >> $i @@ -34,10 +54,10 @@ do # 添加结尾 - echo '

' >> $i - echo '' >> $i - echo ' ' >> $i - echo '' >> $i + # echo '

' >> $i + # echo '' >> $i + # echo ' ' >> $i + # echo '' >> $i # echo '-----------------------' >> $i diff --git "a/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" "b/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" old mode 100644 new mode 100755 index 442a505573..69d6aa9c45 --- "a/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" +++ "b/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 动态规划之编辑距离总结篇 @@ -196,7 +194,3 @@ class Solution { -

- - - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" "b/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" old mode 100644 new mode 100755 index 67570bc8d6..7fa2b6ec94 --- "a/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 二叉树:以为使用了递归,其实还隐藏着回溯 @@ -687,7 +685,3 @@ impl Solution { ``` -

- - - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" "b/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 739184bbf7..7d25d81876 --- "a/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 二叉树:总结篇!(需要掌握的二叉树技能都在这里了) @@ -92,10 +90,9 @@ * 递归:中序,双指针操作 * 迭代:模拟中序,逻辑相同 * [求二叉搜索树的众数](https://programmercarl.com/0501.二叉搜索树中的众数.html) - + * 递归:中序,清空结果集的技巧,遍历一遍便可求众数集合 - * [二叉搜索树转成累加树](https://programmercarl.com/0538.把二叉搜索树转换为累加树.html) - +* [二叉搜索树转成累加树](https://programmercarl.com/0538.把二叉搜索树转换为累加树.html) * 递归:中序,双指针操作累加 * 迭代:模拟中序,逻辑相同 @@ -150,7 +147,7 @@ 二叉树专题汇聚为一张图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211030125421.png) +![](https://file1.kamacoder.com/i/algo/20211030125421.png) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[青](https://wx.zsxq.com/dweb2/index/footprint/185251215558842),所画,总结的非常好,分享给大家。 @@ -159,7 +156,4 @@ -

- - - + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 50d592a24c..a68f93a901 --- "a/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 二叉树理论基础篇 @@ -17,7 +15,7 @@ 题目分类大纲如下: -二叉树大纲 +二叉树大纲 说到二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容再啰嗦一遍,所以以下我讲的都是一些比较重点的内容。 @@ -33,7 +31,7 @@ 如图所示: - + 这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。 @@ -48,7 +46,7 @@ 我来举一个典型的例子如题: - + 相信不少同学最后一个二叉树是不是完全二叉树都中招了。 @@ -65,7 +63,7 @@ 下面这两棵树都是搜索树 - + ### 平衡二叉搜索树 @@ -74,7 +72,7 @@ 如图: - + 最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。 @@ -93,13 +91,13 @@ 链式存储如图: - + 链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢? 其实就是用数组来存储二叉树,顺序存储的方式如图: - + 用数组来存储二叉树如何遍历的呢? @@ -146,7 +144,7 @@ 大家可以对着如下图,看看自己理解的前后中序有没有问题。 - + 最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。 @@ -313,8 +311,4 @@ public class TreeNode public TreeNode(int x) { val = x; } } ``` -

- - - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" old mode 100644 new mode 100755 index ee4899b1b1..803b25ae80 --- "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 统一写法是一种什么感觉 @@ -27,11 +25,16 @@ **那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。** -如何标记呢,**就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。** 这种方法也可以叫做标记法。 +如何标记呢? + +* 方法一:**就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。** 这种方法可以叫做`空指针标记法`。 + +* 方法二:**加一个 `boolean` 值跟随每个节点,`false` (默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,`true` 表示该节点的位次之前已经安排过了,可以收割节点了。** +这种方法可以叫做`boolean 标记法`,样例代码见下文`C++ 和 Python 的 boolean 标记法`。 这种方法更容易理解,在面试中更容易写出来。 ### 迭代法中序遍历 -中序遍历代码如下:(详细注释) +> 中序遍历(空指针标记法)代码如下:(详细注释) ```CPP class Solution { @@ -64,12 +67,51 @@ public: 看代码有点抽象我们来看一下动画(中序遍历): -![中序遍历迭代(统一写法)](https://code-thinking.cdn.bcebos.com/gifs/%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E8%BF%AD%E4%BB%A3%EF%BC%88%E7%BB%9F%E4%B8%80%E5%86%99%E6%B3%95%EF%BC%89.gif) +![中序遍历迭代(统一写法)](https://file1.kamacoder.com/i/algo/%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E8%BF%AD%E4%BB%A3%EF%BC%88%E7%BB%9F%E4%B8%80%E5%86%99%E6%B3%95%EF%BC%89.gif) 动画中,result数组就是最终结果集。 可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。 +> 中序遍历(boolean 标记法): +```c++ +class Solution { +public: + vector inorderTraversal(TreeNode* root) { + vector result; + stack> st; + if (root != nullptr) + st.push(make_pair(root, false)); // 多加一个参数,false 为默认值,含义见下文注释 + + while (!st.empty()) { + auto node = st.top().first; + auto visited = st.top().second; //多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + st.pop(); + + if (visited) { // visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + result.push_back(node->val); + continue; + } + + // visited 当前为 false, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + + // 中序遍历是'左中右',右儿子最先入栈,最后出栈。 + if (node->right) + st.push(make_pair(node->right, false)); + + // 把自己加回到栈中,位置居中。 + // 同时,设置 visited 为 true,表示下次再访问本节点时,允许收割。 + st.push(make_pair(node, true)); + + if (node->left) + st.push(make_pair(node->left, false)); // 左儿子最后入栈,最先出栈 + } + + return result; + } +}; +``` + 此时我们再来看前序遍历代码。 ### 迭代法前序遍历 @@ -105,7 +147,7 @@ public: ### 迭代法后序遍历 -后续遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) +> 后续遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) ```CPP class Solution { @@ -136,6 +178,42 @@ public: }; ``` +> 迭代法后序遍历(boolean 标记法): +```c++ +class Solution { +public: + vector postorderTraversal(TreeNode* root) { + vector result; + stack> st; + if (root != nullptr) + st.push(make_pair(root, false)); // 多加一个参数,false 为默认值,含义见下文 + + while (!st.empty()) { + auto node = st.top().first; + auto visited = st.top().second; //多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + st.pop(); + + if (visited) { // visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + result.push_back(node->val); + continue; + } + + // visited 当前为 false, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + // 后序遍历是'左右中',节点自己最先入栈,最后出栈。 + // 同时,设置 visited 为 true,表示下次再访问本节点时,允许收割。 + st.push(make_pair(node, true)); + + if (node->right) + st.push(make_pair(node->right, false)); // 右儿子位置居中 + + if (node->left) + st.push(make_pair(node->left, false)); // 左儿子最后入栈,最先出栈 + } + + return result; + } +}; +``` ## 总结 此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。 @@ -158,7 +236,7 @@ class Solution { while (!st.empty()) { TreeNode node = st.peek(); if (node != null) { - st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中 + st.pop(); // 将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中(前序遍历-中左右,入栈顺序右左中) if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈) st.push(node); // 添加中节点 @@ -186,11 +264,10 @@ public List inorderTraversal(TreeNode root) { while (!st.empty()) { TreeNode node = st.peek(); if (node != null) { - st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中 + st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中(中序遍历-左中右,入栈顺序右中左) if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) st.push(node); // 添加中节点 st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。 - if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈) } else { // 只有遇到空节点的时候,才将下一个节点放进结果集 st.pop(); // 将空节点弹出 @@ -214,7 +291,7 @@ class Solution { while (!st.empty()) { TreeNode node = st.peek(); if (node != null) { - st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中 + st.pop(); // 将该节点弹出,避免重复操作,下面再将中右左节点添加到栈中(后序遍历-左右中,入栈顺序中右左) st.push(node); // 添加中节点 st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。 if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) @@ -234,7 +311,7 @@ class Solution { ### Python: -迭代法前序遍历: +> 迭代法前序遍历(空指针标记法): ```python class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: @@ -257,7 +334,7 @@ class Solution: return result ``` -迭代法中序遍历: +> 迭代法中序遍历(空指针标记法): ```python class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: @@ -282,7 +359,7 @@ class Solution: return result ``` -迭代法后序遍历: +> 迭代法后序遍历(空指针标记法): ```python class Solution: def postorderTraversal(self, root: TreeNode) -> List[int]: @@ -306,6 +383,61 @@ class Solution: return result ``` +> 中序遍历,统一迭代(boolean 标记法): +```python +class Solution: + def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [(root, False)] if root else [] # 多加一个参数,False 为默认值,含义见下文 + + while stack: + node, visited = stack.pop() # 多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + + if visited: # visited 为 True,表示该节点和两个儿子的位次之前已经安排过了,现在可以收割节点了 + values.append(node.val) + continue + + # visited 当前为 False, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + # 中序遍历是'左中右',右儿子最先入栈,最后出栈。 + if node.right: + stack.append((node.right, False)) + + stack.append((node, True)) # 把自己加回到栈中,位置居中。同时,设置 visited 为 True,表示下次再访问本节点时,允许收割 + + if node.left: + stack.append((node.left, False)) # 左儿子最后入栈,最先出栈 + + return values +``` + +> 后序遍历,统一迭代(boolean 标记法): +```python +class Solution: + def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [(root, False)] if root else [] # 多加一个参数,False 为默认值,含义见下文 + + while stack: + node, visited = stack.pop() # 多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + + if visited: # visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + values.append(node.val) + continue + + # visited 当前为 False, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次” + # 后序遍历是'左右中',节点自己最先入栈,最后出栈。 + # 同时,设置 visited 为 True,表示下次再访问本节点时,允许收割。 + stack.append((node, True)) + + if node.right: + stack.append((node.right, False)) # 右儿子位置居中 + + if node.left: + stack.append((node.left, False)) # 左儿子最后入栈,最先出栈 + + return values +``` + ### Go: > 前序遍历统一迭代法 @@ -835,8 +967,5 @@ public IList PostorderTraversal(TreeNode root) } ``` -

- - - + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" old mode 100644 new mode 100755 index 35a01a7fbe..efa07d97d9 --- "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 听说还可以用非递归的方式 @@ -40,7 +38,7 @@ 动画如下: -![二叉树前序遍历(迭代法)](https://code-thinking.cdn.bcebos.com/gifs/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) +![二叉树前序遍历(迭代法)](https://file1.kamacoder.com/i/algo/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) 不难写出如下代码: (**注意代码中空节点不入栈**) @@ -87,7 +85,7 @@ public: 动画如下: -![二叉树中序遍历(迭代法)](https://code-thinking.cdn.bcebos.com/gifs/%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) +![二叉树中序遍历(迭代法)](https://file1.kamacoder.com/i/algo/%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) **中序遍历,可以写出如下代码:** @@ -117,9 +115,9 @@ public: ### 后序遍历(迭代法) -再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图: +再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图: -![前序到后序](https://code-thinking-1253855093.file.myqcloud.com/pics/20200808200338924.png) +![前序到后序](https://file1.kamacoder.com/i/algo/20200808200338924.png) **所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:** @@ -153,7 +151,7 @@ public: 上面这句话,可能一些同学不太理解,建议自己亲手用迭代法,先写出来前序,再试试能不能写出中序,就能理解了。 -**那么问题又来了,难道 二叉树前后中序遍历的迭代法实现,就不能风格统一么(即前序遍历 改变代码顺序就可以实现中序 和 后序)?** +**那么问题又来了,难道二叉树前后中序遍历的迭代法实现,就不能风格统一么(即前序遍历改变代码顺序就可以实现中序 和 后序)?** 当然可以,这种写法,还不是很好理解,我们将在下一篇文章里重点讲解,敬请期待! @@ -240,14 +238,14 @@ class Solution { # 前序遍历-迭代-LC144_二叉树的前序遍历 class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: - # 根结点为空则返回空列表 + # 根节点为空则返回空列表 if not root: return [] stack = [root] result = [] while stack: node = stack.pop() - # 中结点先处理 + # 中节点先处理 result.append(node.val) # 右孩子先入栈 if node.right: @@ -262,25 +260,27 @@ class Solution: # 中序遍历-迭代-LC94_二叉树的中序遍历 class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: + if not root: return [] - stack = [] # 不能提前将root结点加入stack中 + stack = [] # 不能提前将root节点加入stack中 + result = [] cur = root while cur or stack: - # 先迭代访问最底层的左子树结点 + # 先迭代访问最底层的左子树节点 if cur: stack.append(cur) cur = cur.left - # 到达最左结点后处理栈顶结点 + # 到达最左节点后处理栈顶节点 else: cur = stack.pop() result.append(cur.val) - # 取栈顶元素右结点 + # 取栈顶元素右节点 cur = cur.right return result ``` - ```python +```python # 后序遍历-迭代-LC145_二叉树的后序遍历 class Solution: @@ -291,7 +291,7 @@ class Solution: result = [] while stack: node = stack.pop() - # 中结点先处理 + # 中节点先处理 result.append(node.val) # 左孩子先入栈 if node.left: @@ -303,6 +303,44 @@ class Solution: return result[::-1] ``` +#### Python 后序遍历的迭代新解法: +* 本解法不同于前文介绍的`逆转前序遍历调整后的结果`,而是采用了对每个节点直接处理。这个实现方法在面试中不容易写出来,在下一节,我将改造本代码,奉上代码更简洁、更套路化、更容易实现的统一方法。 + +```python +class Solution: + def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [] + popped_nodes = set() # 记录值已经被收割了的 nodes,这是关键,已经被收割的节点还在树中,还会被访问到,但逻辑上已经等同于 null 节点。 + current = root + + while current or stack: + if current: # 一次处理完一个节点和他的左右儿子节点,不处理孙子节点,孙子节点由左右儿子等会分别处理。 + stack.append(current) # 入栈自己 + + if current.right: + stack.append(current.right) # 入栈右儿子 + + if current.left: # 因为栈是后进先出,后序是‘左右中’,所以后加左儿子 + stack.append(current.left) # 入栈左儿子 + + current = None # 会导致后面A处出栈 + continue + + node = stack.pop() # A处,出的是左儿子,如果无左儿子,出的就是右儿子,如果连右儿子也没有,出的就是自己了。 + + # 如果 node 是叶子节点,就可以收割了;如果左右儿子都已经被收割了,也可以收割 + if (node.left is None or node.left in popped_nodes) and \ + (node.right is None or node.right in popped_nodes): + popped_nodes.add(node) + values.append(node.val) + continue + + current = node # 不符合收割条件,说明 node 下还有未入栈的儿子,就去入栈 + + return values +``` + ### Go: > 迭代法前序遍历 @@ -757,7 +795,3 @@ public IList PostorderTraversal(TreeNode root) } ``` -

- - - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" old mode 100644 new mode 100755 index edd55aad3c..ffa3ff6cf8 --- "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 一看就会,一写就废! @@ -48,7 +46,7 @@ void traversal(TreeNode* cur, vector& vec) if (cur == NULL) return; ``` -3. **确定单层递归的逻辑**:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: +3. **确定单层递归的逻辑**:前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: ```cpp vec.push_back(cur->val); // 中 @@ -180,26 +178,34 @@ class Solution { class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: - if not root: - return [] - - left = self.preorderTraversal(root.left) - right = self.preorderTraversal(root.right) - - return [root.val] + left + right + res = [] + + def dfs(node): + if node is None: + return + + res.append(node.val) + dfs(node.left) + dfs(node.right) + dfs(root) + return res ``` ```python # 中序遍历-递归-LC94_二叉树的中序遍历 class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: - if root is None: - return [] - - left = self.inorderTraversal(root.left) - right = self.inorderTraversal(root.right) - - return left + [root.val] + right + res = [] + + def dfs(node): + if node is None: + return + + dfs(node.left) + res.append(node.val) + dfs(node.right) + dfs(root) + return res ``` ```python @@ -207,13 +213,18 @@ class Solution: # 后序遍历-递归-LC145_二叉树的后序遍历 class Solution: def postorderTraversal(self, root: TreeNode) -> List[int]: - if not root: - return [] - - left = self.postorderTraversal(root.left) - right = self.postorderTraversal(root.right) - - return left + right + [root.val] + res = [] + + def dfs(node): + if node is None: + return + + dfs(node.left) + dfs(node.right) + res.append(node.val) + + dfs(root) + return res ``` ### Go: @@ -270,57 +281,96 @@ func postorderTraversal(root *TreeNode) (res []int) { } ``` -### Javascript: +### JavaScript: 前序遍历: ```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, + ] + : []; }; ``` @@ -619,8 +669,60 @@ public void Traversal(TreeNode cur, IList res) } ``` -

- - - +### PHP +```php +// 144.前序遍历 +function preorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $output[] = $root->val; + $this->traversal($root->left, $output); + $this->traversal($root->right, $output); +} +``` +```php +// 94.中序遍历 +function inorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $this->traversal($root->left, $output); + $output[] = $root->val; + $this->traversal($root->right, $output); +} +``` +```php +// 145.后序遍历 +function postorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $this->traversal($root->left, $output); + $this->traversal($root->right, $output); + $output[] = $root->val; +} +``` + diff --git "a/problems/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.md" "b/problems/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.md" deleted file mode 100644 index a1e553d0bc..0000000000 --- "a/problems/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.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/\345\211\215\345\272\217/\344\273\200\344\271\210\346\230\257\346\240\270\345\277\203\344\273\243\347\240\201\346\250\241\345\274\217\357\274\214\344\273\200\344\271\210\345\217\210\346\230\257ACM\346\250\241\345\274\217\357\274\237.md" "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" similarity index 68% rename from "problems/\345\211\215\345\272\217/\344\273\200\344\271\210\346\230\257\346\240\270\345\277\203\344\273\243\347\240\201\346\250\241\345\274\217\357\274\214\344\273\200\344\271\210\345\217\210\346\230\257ACM\346\250\241\345\274\217\357\274\237.md" rename to "problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" index 313264fba2..70643b7e49 100644 --- "a/problems/\345\211\215\345\272\217/\344\273\200\344\271\210\346\230\257\346\240\270\345\277\203\344\273\243\347\240\201\346\250\241\345\274\217\357\274\214\344\273\200\344\271\210\345\217\210\346\230\257ACM\346\250\241\345\274\217\357\274\237.md" +++ "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" @@ -5,15 +5,15 @@ 平时大家在力扣上刷题,就是 核心代码模式,即给你一个函数,直接写函数实现,例如这样: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231109193631.png) +![](https://file1.kamacoder.com/i/algo/20231109193631.png) 而ACM模式,是程序头文件,main函数,数据的输入输出都要自己处理,例如这样: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231109193743.png) +![](https://file1.kamacoder.com/i/algo/20231109193743.png) 大家可以发现 右边代码框什么都没有,程序从头到尾都需要自己实现,本题如果写完代码是这样的: (细心的录友可以发现和力扣上刷题是不一样的) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231109193931.png) +![](https://file1.kamacoder.com/i/algo/20231109193931.png) **如果大家从一开始学习算法就一直在力扣上的话,突然切到ACM模式会非常不适应**。 @@ -21,15 +21,15 @@ 知识星球里也有很多录友,因为不熟悉ACM模式在面试的过程中吃了不少亏。 -

+
-
+
-
+
-
+
-
+
## 面试究竟怎么考? @@ -53,7 +53,7 @@ 你只要能把卡码网首页的25道题目 都刷了 ,就把所有的ACM输入输出方式都练习到位了,不会有任何盲区。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231109195056.png) +![](https://file1.kamacoder.com/i/algo/20231109195056.png) 而且你不用担心,题目难度太大,直接给自己劝退,**卡码网的前25道题目都是我精心制作的,难度也是循序渐进的**,大家去刷一下就知道了。 diff --git "a/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" index 48781eda88..2eeb7431bf 100644 --- "a/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" +++ "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" @@ -15,7 +15,7 @@ 其输入用例,就是用一个数组来表述 二叉树,如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914222335.png) +![](https://file1.kamacoder.com/i/algo/20210914222335.png) 一直跟着公众号学算法的录友 应该知道,我在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/Dza-fqjTyGrsRw4PWNKdxA),已经讲过,**只有 中序与后序 和 中序和前序 可以确定一棵唯一的二叉树。 前序和后序是不能确定唯一的二叉树的**。 @@ -24,7 +24,7 @@ 很明显,是后台直接明确了构造规则。 再看一下 这个 输入序列 和 对应的二叉树。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914222335.png) +![](https://file1.kamacoder.com/i/algo/20210914222335.png) 从二叉树 推导到 序列,大家可以发现这就是层序遍历。 @@ -36,7 +36,7 @@ 顺序存储,就是用一个数组来存二叉树,其方式如图所示: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914223147.png) +![](https://file1.kamacoder.com/i/algo/20210914223147.png) 那么此时大家是不是应该知道了,数组如何转化成 二叉树了。**如果父节点的数组下标是i,那么它的左孩子下标就是i * 2 + 1,右孩子下标就是 i * 2 + 2**。 @@ -80,7 +80,7 @@ TreeNode* construct_binary_tree(const vector& vec) { 这个函数最后返回的 指针就是 根节点的指针, 这就是 传入二叉树的格式了,也就是 力扣上的用例输入格式,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914224422.png) +![](https://file1.kamacoder.com/i/algo/20210914224422.png) 也有不少同学在做ACM模式的题目,就经常疑惑: @@ -176,7 +176,7 @@ int main() { 和 [538.把二叉搜索树转换为累加树](https://mp.weixin.qq.com/s/rlJUFGCnXsIMX0Lg-fRpIw) 中的输入是一样的 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914222335.png) +![](https://file1.kamacoder.com/i/algo/20210914222335.png) 这里可能又有同学疑惑,你这不一样啊,题目是null,你为啥用-1。 @@ -184,11 +184,11 @@ int main() { 在来看,测试代码输出的效果: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914230045.png) +![](https://file1.kamacoder.com/i/algo/20210914230045.png) 可以看出和 题目中输入用例 这个图 是一样一样的。 只不过题目中图没有把 空节点 画出来而已。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914230118.png) +![](https://file1.kamacoder.com/i/algo/20210914230118.png) 大家可以拿我的代码去测试一下,跑一跑。 @@ -205,7 +205,7 @@ int main() { **[知识星球](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)**里有的录友已经开始三刷: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210727234031.png) +![](https://file1.kamacoder.com/i/algo/20210727234031.png) 只做过一遍,真的就是懂了一点皮毛, 第二遍刷才有真的对各个题目有较为深入的理解,也会明白 我为什么要这样安排刷题的顺序了。 @@ -419,4 +419,4 @@ func main() { ``` ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" "b/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" index 27940f1bd5..7d112a19c8 100644 --- "a/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" +++ "b/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" @@ -211,4 +211,4 @@ leetcode是专门针对算法练习的题库,leetcode现在也推出了中文 ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/gitserver.md" "b/problems/\345\211\215\345\272\217/gitserver.md" new file mode 100755 index 0000000000..caf93ec6ec --- /dev/null +++ "b/problems/\345\211\215\345\272\217/gitserver.md" @@ -0,0 +1,312 @@ + +# 一文手把手教你搭建Git私服 + +## 为什么要搭建Git私服 + +很多同学都问文章,文档,资料怎么备份啊,自己电脑和公司电脑怎么随时同步资料啊等等,这里呢我写一个搭建自己的git私服的详细教程 + +为什么要搭建一个Git私服呢,而不是用Github免费的私有仓库,有以下几点: +* Github 私有仓库真的慢,文件一旦多了,或者有图片文件,git pull 的时候半天拉不下来 +* 自己的文档难免有自己个人信息,放在github心里也是担心的 +* 想建几个库就建几个,想几个人合作开发都可以,不香么? + +**网上可以搜到很多git搭建,但是说的模棱两可**,而且有的直接是在本地搭建git服务,既然是备份,搭建在本地哪有备份的意义,一定要有一个远端服务器, 而且自己的电脑和公司的电脑还是同步自己的文章,文档和资料等等。 + + +适合人群: 想通过git私服来备份自己的文章,Markdown,并做版本管理的同学 +最后,写好每篇 Chat 是对我的责任,也是对你的尊重。谢谢大家~ + +正文如下: + +----------------------------- + +## 如何找到可以外网访问服务器 + +有的同学问了,自己的电脑就不能作为服务器么? + +这里要说一下,安装家庭带宽,运营商默认是不会给我们独立分配公网IP的 + +一般情况下是一片区域公用一个公网IP池,所以外网是不能访问到在家里我们使用的电脑的 + +除非我们自己去做映射,这其实非常麻烦而且公网IP池 是不断变化的 + +辛辛苦苦做了映射,运营商给IP一换,我们的努力就白扯了 + +那我们如何才能找到一个外网可以访问的服务器呢,此时云计算拯救了我们。 + +推荐大家选一家云厂商(阿里云,腾讯云,百度云都可以)在上面上买一台云服务器 + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +云厂商经常做活动,如果从来没有买过云服务器的账号更便宜,低配一年一百块左右的样子,强烈推荐一起买个三年。 + +买云服务器的时候推荐直接安装centos系统。 + +这里要说一下,有了自己的云服务器之后 不仅仅可以用来做git私服 + +**同时还可以做网站,做程序后台,跑程序,做测试**(这样我们自己的电脑就不会因为自己各种搭建环境下载各种包而搞的的烂糟糟),等等等。 + +有自己云服务器和一个公网IP真的是一件非常非常幸福的事情,能体验到自己的服务随时可以部署上去提供给所有人使用的喜悦。 + +外网可以访问的服务器解决了,接下来就要部署git服务了 + +本文将采用centos系统来部署git私服 + +## 服务器端安装Git + +切换至root账户 + +``` +su root +``` + +看一下服务器有没有安装git,如果出现下面信息就说明是有git的 +``` +[root@instance-5fcyjde7 ~]# git +usage: git [--version] [--help] [-c name=value] + [--exec-path[=]] [--html-path] [--man-path] [--info-path] + [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] + [--git-dir=] [--work-tree=] [--namespace=] + [] + +The most commonly used git commands are: + add Add file contents to the index + bisect Find by binary search the change that introduced a bug + branch List, create, or delete branches + checkout Checkout a branch or paths to the working tree + clone Clone a repository into a new directory + commit Record changes to the repository + diff Show changes between commits, commit and working tree, etc + fetch Download objects and refs from another repository + grep Print lines matching a pattern + init Create an empty Git repository or reinitialize an existing one + log Show commit logs + merge Join two or more development histories together + mv Move or rename a file, a directory, or a symlink + pull Fetch from and merge with another repository or a local branch + push Update remote refs along with associated objects + rebase Forward-port local commits to the updated upstream head + reset Reset current HEAD to the specified state + rm Remove files from the working tree and from the index + show Show various types of objects + status Show the working tree status + tag Create, list, delete or verify a tag object signed with GPG + +'git help -a' and 'git help -g' lists available subcommands and some +concept guides. See 'git help ' or 'git help ' +to read about a specific subcommand or concept. +``` + +如果没有git,就安装一下,yum安装的版本默认是 `1.8.3.1` + +``` +yum install git +``` + +安装成功之后,看一下自己安装的版本 + +``` +git --version +``` + +## 服务器端设置Git账户 + +创建一个git的linux账户,这个账户只做git私服的操作,也是为了安全起见 + +如果不新创建一个linux账户,在自己的常用的linux账户下创建的话,哪天手抖 来一个`rm -rf *` 操作 数据可全没了 + +**这里linux git账户的密码设置的尽量复杂一些,我这里为了演示,就设置成为'gitpassword'** +``` +adduser git +passwd gitpassword +``` + +然后就要切换成git账户,进行后面的操作了 +``` +[root@instance-5fcyjde7 ~]# su - git +``` + +看一下自己所在的目录,是不是在git目录下面 + +``` +[git@instance-5fcyjde7 ~]$ pwd +/home/git +``` + +## 服务器端密钥管理 + +创建`.ssh` 目录,如果`.ssh` 已经存在了,可以忽略这一项 + +为啥用配置ssh公钥呢,同学们记不记得我使用github上传代码的时候也要把自己的公钥配置上传到github上 + +这也是方面每次操作git仓库的时候不用再去输入密码 + +``` +cd ~/ +mkdir .ssh +``` + +进入.ssh 文件下,创建一个 `authorized_keys` 文件,这个文件就是后面就是要放我们客户端的公钥 + +``` +cd ~/.ssh +touch authorized_keys +``` + +别忘了`authorized_keys`给设置权限,很多同学发现自己不能免密登陆,都是因为忘记了给`authorized_keys` 设置权限 + +``` +chmod 700 /home/git/.ssh +chmod 600 /home/git/.ssh/authorized_keys +``` + +接下来我们要把客户端的公钥放在git服务器上,我们在回到客户端,创建一个公钥 + +在我们自己的电脑上,有公钥和私钥 两个文件分别是:`id_rsa` 和 `id_rsa.pub` + +如果是`windows`系统公钥私钥的目录在`C:\Users\用户名\.ssh` 下 + +如果是mac 或者 linux, 公钥和私钥的目录这里 `cd ~/.ssh/`, 如果发现自己的电脑上没有公钥私钥,那就自己创建一个 + +创建密钥的命令 + +``` +ssh-keygen -t rsa +``` + +创建密钥的过程中,一路点击回车就可以了。不需要填任何东西 + +把公钥拷贝到git服务器上,将我们刚刚生成的`id_rsa.pub`,拷贝到git服务器的`/home/git/.ssh/`目录 + +在git服务器上,将公钥添加到`authorized_keys` 文件中 + +``` +cd /home/git/.ssh/ +cat id_rsa.pub >> authorized_keys +``` + +如何看我们配置的密钥是否成功呢, 在客户端直接登录git服务器,看看是否是免密登陆 +``` +ssh git@git服务器ip +``` + +例如: + +``` +ssh git@127.0.0.1 +``` + +如果可以免密登录,那就说明服务器端密钥配置成功了 + +## 服务器端部署Git 仓库 + +我们在登陆到git 服务器端,切换为成 git账户 + +如果是root账户切换成git账户 +``` +su - git +``` + +如果是其他账户切换为git账户 +``` +sudo su - git +``` + +进入git目录下 +``` +cd ~/git +``` + +创建我们的第一个Git私服的仓库,我们叫它为world仓库 + +那么首先创建一个文件夹名为: world.git ,然后进入这个目录 + +有同学问,为什么文件夹名字后面要放`.git`, 其实不这样命名也是可以的 + +但是细心的同学可能注意到,我们平时在github上 `git clone` 其他人的仓库的时候,仓库名字后面,都是加上`.git`的 + +例如下面这个例子,其实就是github对仓库名称的一个命名规则,所以我们也遵守github的命名规则。 + +``` +git clone https://github.com/youngyangyang04/NoSQLAttack.git +``` + +所以我们的操作是 +``` +[git@localhost git]# mkdir world.git +[git@localhost git]# cd world.git +``` + +初始化我们的`world`仓库 + +``` +git init --bare + +``` + +**如果我们想创建多个仓库,就在这里创建多个文件夹并初始化就可以了,和world仓库的操作过程是一样一样的** + +现在我们服务端的git仓库就部署完了,接下来就看看客户端,如何使用这个仓库呢 + +## 客户端连接远程仓库 + +我们在自己的电脑上创建一个文件夹 也叫做`world`吧 + +其实这里命名是随意的,但是我们为了和git服务端的仓库名称保持同步。 这样更直观我们操作的是哪一个仓库。 + +``` +mkdir world +cd world +``` + +进入world文件,并初始化操作 + +``` +cd world +git init +``` + +在world目录上创建一个测试文件,并且将其添加到git版本管理中 + +``` +touch test +git add test +git commit -m "add test file" +``` + +将次仓库和远端仓库同步 + +``` +git remote add origin git@git服务器端的ip:world.git +git push -u origin master +``` + +此时这个test测试文件就已经提交到我们的git远端私服上了 + +## Git私服安全问题 + +这里有两点安全问题 + +### linux git的密码不要泄露出去 + +否则,别人可以通过 ssh git@git服务器IP 来登陆到你的git私服服务器上 + +当然了,这里同学们如果买的是云厂商的云服务器的话 + +如果有人恶意想通过 尝试不同密码链接的方式来链接你的服务器,重试三次以上 + +这个客户端的IP就会被封掉,同时邮件通知我们可以IP来自哪里 + +所以大可放心 密码只要我们不泄露出去,基本上不会有人同时不断尝试密码的方式来登上我们的git私服服务器 + +### 私钥文件`id_rsa` 不要给别人 + +如果有人得到了这个私钥,就可以免密码登陆我们的git私服上了,我相信大家也不至于把自己的私钥主动给别人吧 + +## 总结 + +这里就是整个git私服搭建的全过程,安全问题我也给大家列举了出来,接下来好好享受自己的Git私服吧 + +**enjoy!** + diff --git "a/problems/\345\211\215\345\272\217/kvstore.md" "b/problems/\345\211\215\345\272\217/kvstore.md" new file mode 100755 index 0000000000..268fc018c7 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/kvstore.md" @@ -0,0 +1,124 @@ + +# 手把手带你实现存储引擎 + + +之前在 [刷题攻略登上榜首](https://mp.weixin.qq.com/s/wZRTrA9Rbvgq1yEkSw4vfQ)这篇文章中说过,Carl不仅写了刷题攻略,还写了很多优秀的开源项目。 + +在星球里也有很多小伙伴问我关于一些,项目的选择,**相信如果是C++后台开发路线的话,基本都会去做WebServer 服务器**。 + +我在[知识星球](https://programmercarl.com/other/kstar.html)给小伙伴答疑,包括看了这么多简历,**发现WebServer这个项目是真的多,有点烂大街了**。 + +所以今天我把自己曾经开发的 KV存储引擎 给大家介绍一波,大家可以拿去当做自己的项目经验。 + +**相信只要是搞后端的同学应该都要熟悉非关系型数据库redis吧,那么应该知道redis的存储引擎是跳表实现的**。 + +现在很多云厂商提供的云数据库,其底层都是用了Facebook开源的rocksdb,而rocksdb的底层是Google开源的Levedb,**而Levedb的核心实现也是跳表**。 + +所以大家应该知道跳表的应用有多么的广泛了。 + +那么为什么这个项目非常合适大家用来做自己的项目经验呢? + +如果你是后端开发的话,你在简历上一定会写熟悉或者了解redis吧,那么可以进一步介绍一下自己的项目用跳表实现了redis核心引擎。 + +面试官一定会非常感兴趣的,然后你就可以和面试官侃侃而谈你是如何用跳表实现的这个KV存储引擎的。 + +**瞬间逼格就高了,有木有!** + +我在18年的时候,用跳表实现了一个轻量级KV存储引擎,代码也写的非常规范,熟悉我的录友应该知道,我的代码严格按照Google C++ style来的。 + +因为当时我是想把这个项目国际化的,注释和readme都是英文的,但最近我把这个项目又汉化回来了,方便大家理解。 + +给大家先随意看一段代码,我在注释中其实就已经在讲解跳表的运行原理了。代码使用了C++模板编程,这样接口支持任意类型的数据(包括自己自定义的类) + +![](https://file1.kamacoder.com/i/algo/20221104121454.png) + +项目地址:**https://github.com/youngyangyang04/Skiplist-CPP** + +这个项目中的代码质量是非常高的,如果无论是C++特性的运用,还是代码风格都是绝对拿得出手的! + +好了,牛逼吹完,然后给大家正式介绍一下这个项目 + +## KV存储引擎 + +本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。 + +在随机写读情况下,该项目每秒可处理啊请求数(QPS): 24.39w,每秒可处理读请求数(QPS): 18.41w + +## 项目展示 + +![](https://file1.kamacoder.com/i/algo/20221104121509.png) + +文件功能: + +* main.cpp 包含skiplist.h使用跳表进行数据操作 +* skiplist.h 跳表核心实现 +* README.md 中文介绍 +* README-en.md 英文介绍 +* bin 生成可执行文件目录 +* makefile 编译脚本 +* store 数据落盘的文件存放在这个文件夹 +* stress_test_start.sh 压力测试脚本 +* LICENSE 使用协议 + + +## 提供接口 + +* insertElement(插入数据) +* deleteElement(删除数据) +* searchElement(查询数据) +* displayList(展示已存数据) +* dumpFile(数据落盘) +* loadFile(文件加载数据) +* size(返回数据规模) + +## 存储引擎数据表现 + +### 插入操作 + +跳表树高:18 + +采用随机插入数据测试: + + +|插入数据规模(万条) |耗时(秒) | +|---|---| +|10 |0.316763 | +|50 |1.86778 | +|100 |4.10648 | + + +每秒可处理写请求数(QPS): 24.39w + +### 取数据操作 + +|取数据规模(万条) |耗时(秒) | +|---|---| +|10|0.47148 |10| +|50|2.56373 |50| +|100|5.43204 |100| + +每秒可处理读请求数(QPS): 18.41w + +## 项目运行方式 + +``` +make // complie demo main.cpp +./bin/main // run +``` + +运行截图:(其中展示了插入数据,删除数据,展示数据等等功能) + +![](https://file1.kamacoder.com/i/algo/20221104121525.png) + +如果想自己写程序使用这个kv存储引擎,只需要在你的CPP文件中include skiplist.h 就可以了。 + +可以运行如下脚本测试kv存储引擎的性能(当然你可以根据自己的需求进行修改) + +``` +sh stress_test_start.sh +``` + +项目地址:**https://github.com/youngyangyang04/Skiplist-CPP** + +**大家白嫖的同时,别忘了给个star,fork,支持一波!** 录友如果最后拿到offer了,也别忘了和我道个喜哦。 + diff --git "a/problems/\345\211\215\345\272\217/server.md" "b/problems/\345\211\215\345\272\217/server.md" new file mode 100755 index 0000000000..890cf8bcd5 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/server.md" @@ -0,0 +1,129 @@ + +# 一台服务器有什么用! + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +但在组织这场活动的时候,了解到大家都有一个共同的问题: **这个服务器究竟有啥用??** + +这真是一个好问题,而且我一句两句还说不清楚,所以就专门发文来讲一讲。 + +同时我还录制的一期视频,我的视频号,大家可以关注一波。 + + +一说到服务器,可能很多人都说搞分布式,做计算,搞爬虫,做程序后台服务,多人合作等等。 + +其实这些普通人都用不上,我来说一说大家能用上的吧。 + +## 搭建git私服 + +大家平时工作的时候一定有一个自己的工作文件夹,学生的话就是自己的课件,考试,准备面试的资料等等。 + +已经工作的录友,会有一个文件夹放着自己重要的文档,Markdown,图片,简历等等。 + +这么重要的文件夹,而且我们每天都要更新,也担心哪天电脑丢了,或者坏了,突然这些都不见了。 + +所以我们想备份嘛。 + +还有就是我们经常个人电脑和工作电脑要同步一些私人资料,而不是用微信传来传去。 + +这些都是git私服的使用场景,而且很好用。 + +大家也知道 github,gitee也可以搞私人仓库 用来备份,同步文件,但自己的文档可能放着很多重要的信息,包括自己的各种密码,密钥之类的,放到上面未必安全。你就不怕哪些重大bug把你的信息都泄漏了么[机智] + +更关键的是,github 和 gitee都限速的。毕竟人家的功能定位并不是网盘。 + +项目里有大文件(几百M以上),例如pdf,ppt等等 其上传和下载速度会让你窒息。 + +**后面我会发文专门来讲一讲,如何大家git私服!** + +## 搞一个文件存储 + +这个可以用来生成文件的下载链接,也可以把本地文件传到服务器上。 + +相当于自己做一个对象存储,其实云厂商也有对象存储的产品。 + +不过我们自己也可以做一个,不够很多很同学应该都不知道对象存储怎么用吧,其实我们用服务器可以自己做一个类似的公司。 + +我现在就用自己用go写的一个工具,部署在服务器上。 用来和服务器传文件,或者生成一些文件的临时下载链接。 + +这些都是直接命令行操作的, + +操作方式这样,我把命令包 包装成一个shell命令,想传那个文件,直接 uploadtomyserver,然后就返回可以下载的链接,这个文件也同时传到了我的服务器上。 + +![](https://file1.kamacoder.com/i/algo/20211126165643.png) + +我也把我的项目代码放在了github上: + +https://github.com/youngyangyang04/fileHttpServer + +感兴趣的录友可以去学习一波,顺便给个star。 + + +## 网站 + +做网站,例如 大家知道用html 写几行代码,就可以生成一个网页,但怎么给别人展示呢? + +大家如果用自己的电脑做服务器,只能同一个路由器下的设备可以访问你的网站,可能这个设备出了这个屋子 都访问不了你的网站了。 + +因为你的IP不是公网IP。 + +如果有了一台云服务器,都是配公网IP,你的网站就可以让任何人访问了。 + +或者说 你提供的一个服务就可以让任何人使用。 + +例如第二个例子中,我们可以自己开发一个文件存储,这个服务,我只把把命令行给其他人,其他人都可以使用我的服务来生成链接,当然他们的文件也都传到了我的服务器上。 + +再说一个使用场景。 + +我之前在组织免费里服务器的活动的时候,阿里云给我一个excel,让面就是从我这里买服务器录友的名单,我直接把这个名单甩到群里,让大家自己检查,出现在名单里就可以找我返现,这样做是不是也可以。 + +这么做有几个很大的问题: +* 大家都要去下载excel,做对比,会有人改excel的内容然后就说是从你这里买的,我不可能挨个去比较excel有没有改动 +* excel有其他人的个人信息,这是不能暴漏的。 +* 如果每个人自己用excel查询,私信我返现,一个将近两千人找我返现,我微信根本处理不过来,这就变成体力活了。 + +那应该怎么做呢, + +我就简单写一个查询的页面,后端逻辑就是读一个execel表格,大家在查询页面输入自己的阿里云ID,如果在excel里,页面就会返回返现群的二维码,大家就可以自主扫码加群了。 + +这样,我最后就直接在返现群里 发等额红包就好了,是不是极大降低人力成本了 + +当然我是把 17个返现群的二维码都生成好了,按照一定的规则,展现给查询通过的录友。 + +就是这样一个非常普通的查询页面。 + +![](https://file1.kamacoder.com/i/algo/20211126160200.png) + +查询通过之后,就会展现返现群二维码。 + +![](https://file1.kamacoder.com/i/algo/20211127160558.png) + +但要部署在服务器上,因为没有公网IP,别人用不了你的服务。 + + +## 学习linux + +学习linux其实在自己的电脑上搞一台虚拟机,或者安装双系统也可以学习,不过这很考验你的电脑性能如何了。 + +如果你有一个服务器,那就是独立的一台电脑,你怎么霍霍就怎么霍霍,而且一年都不用关机的,可以一直跑你的任务,和你本地电脑也完全隔离。 + +更方便的是,你目前系统假如是CentOS,想做一个实验需要在Ubuntu上,如果是云服务器,更换系统就是在 后台点一下,一键重装,云厂商基本都是支持所有系统一件安装的。 + +我们平时自己玩linux经常是配各种环境,然后这个linux就被自己玩坏了(一般都是毫无节制使用root权限导致的),总之就是环境配不起来了,基本就要重装了。 + +那云服务器重装系统可太方便了。 + +还有就是加入你好不容易配好的环境,如果以后把这个环境玩坏了,你先回退这之前配好的环境而不是重装系统在重新配一遍吧。 + +那么可以用云服务器的镜像保存功能,就是你配好环境的那一刻就可以打一个镜像包,以后如果环境坏了,直接回退到上次镜像包的状态,这是不是就很香了。 + + +## 总结 + +其实云服务器还有很多其他用处,不过我就说一说大家普遍能用的上的。 + + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + diff --git "a/problems/\345\211\215\345\272\217/vim.md" "b/problems/\345\211\215\345\272\217/vim.md" index 581019995a..3f1daa53aa 100644 --- "a/problems/\345\211\215\345\272\217/vim.md" +++ "b/problems/\345\211\215\345\272\217/vim.md" @@ -62,7 +62,7 @@ IDE那么很吃内存,打开个IDE卡半天,用VIM就很轻便了,秒开 来感受一下PowerVim的使用体验,看起来很酷吧!注意这些操作都不用鼠标的,一波键盘控制流!所以我平时写代码是不碰鼠标的! -![](https://code-thinking.cdn.bcebos.com/gifs/vim_overview.gif) +![](https://file1.kamacoder.com/i/algo/vim_overview.gif) ## 安装 @@ -93,7 +93,7 @@ sh install.sh 当然 还有很多,我还详细写了PowerVim的快捷键,使用方法,插件,配置,等等,都在Github主页的README上。当时我的Github上写的都是英文README,这次为了方便大家阅读,我又翻译成中文README。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211013102249.png) +![](https://file1.kamacoder.com/i/algo/20211013102249.png) Github地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim) diff --git "a/problems/\345\211\215\345\272\217/\344\270\212\346\265\267\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\344\270\212\346\265\267\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index 6309ef583e..0000000000 --- "a/problems/\345\211\215\345\272\217/\344\270\212\346\265\267\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,125 +0,0 @@ - -# 上海互联网公司总结 - -**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** - -## 一线互联网 - -* 百度(上海) -* 阿里(上海) -* 腾讯(上海) -* 字节跳动(上海) -* 蚂蚁金服(上海) - -## 外企IT/互联网/硬件 - -* 互联网 - * Google(上海) - * 微软(上海) - * LeetCode/力扣(上海) - * unity(上海)游戏引擎 - * SAP(上海)主要产品是ERP - * PayPal(上海)在线支付鼻祖 - * eBay(上海)电子商务公司 -* 偏硬件 - * IBM(上海) - * Tesla(上海)特斯拉 - * Cisco(上海)思科 - * Intel(上海) - * AMD(上海)半导体产品领域 - * EMC(上海)易安信是美国信息存储资讯科技公司 - * NVIDIA(上海)英伟达是GPU(图形处理器)的发明者,人工智能计算的引领者 - -## 二线互联网 - -* 拼多多(总部) -* 饿了么(总部)阿里旗下。 -* 哈啰出行(总部)阿里旗下 -* 盒马(总部)阿里旗下 -* 哔哩哔哩(总部) -* 阅文集团(总部)腾讯旗下 -* 爱奇艺(上海)百度旗下 -* 携程(总部) -* 京东(上海) -* 网易(上海) -* 美团点评(上海) -* 唯品会(上海) - -## 硬件巨头 (有软件/互联网业务) - -华为(上海) - -## 三线互联网 - -* PPTV(总部) -* 微盟(总部)企业云端商业及营销解决方案提供商 -* 喜马拉雅(总部) -* 陆金所(总部)全球领先的线上财富管理平台 -* 口碑(上海)阿里旗下。 -* 三七互娱(上海) -* 趣头条(总部) -* 巨人网络(总部)游戏公司 -* 盛大网络(总部)游戏公司 -* UCloud(总部)云服务提供商 -* 达达集团(总部)本地即时零售与配送平台 -* 众安保险(总部)在线财产保险 -* 触宝(总部)触宝输入法等多款APP -* 平安系列 - -## 明星创业公司 - -* 小红书(总部) -* 叮咚买菜(总部) -* 蔚来汽车(总部) -* 七牛云(总部) -* 得物App(总部)品潮流尖货装备交易、球鞋潮品鉴别查验、互动潮流社区 -* 收钱吧(总部)开创了中国移动支付市场“一站式收款” -* 蜻蜓FM(总部)音频内容聚合平台 -* 流利说(总部)在线教育 -* Soul(总部)社交软件 -* 美味不用等(总部)智慧餐饮服务商 -* 微鲸科技(总部)专注于智能家居领域 -* 途虎养车(总部) -* 米哈游(总部)游戏公司 -* 莉莉丝游戏(总部)游戏公司 -* 樊登读书(总部)在线教育 - -## AI独角兽公司 - -* 依图科技(总部)和旷视,商汤对标,都是做安防视觉 -* 深兰科技(总部)致力于人工智能基础研究和应用开发 - -## 其他行业,涉及互联网 -* 花旗、摩根大通等一些列金融巨头 -* 百姓网 -* 找钢网 -* 安居客 -* 前程无忧 -* 东方财富 -* 三大电信运营商:中国移动、中国电信、中国联通 -* 沪江英语 -* 各大银行 - -通知:很多同学感觉自己基础还比较薄弱,想循序渐进的从头学一遍数据结构与算法,那你来对地方了。在公众号左下角「算法汇总」里已经按照各个系列难易程度排好顺序了,大家跟着文章顺序打卡学习就可以了,留言区有很多录友都在从头打卡!「算法汇总」会持续更新,大家快去看看吧! - -## 总结 - -大家如果看了[北京有这些互联网公司,你都知道么?](https://programmercarl.com/前序/北京互联网公司总结.html)和[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)就可以看出中国互联网氛围最浓的当然是北京,其次就是上海! - -很多人说深圳才是第二,上海没有产生BAT之类的企业。 - -**那么来看看上海在垂直领域上是如何独领风骚的,视频领域B站,电商领域拼多多小红书,生活周边有饿了么,大众点评(现与美团合并),互联网金融有蚂蚁金服和陆金所,出行领域有行业老大携程,而且BAT在上海都有部门还是很大的团队,再加上上海众多的外企,以及金融公司(有互联网业务)**。 - -此时就能感受出来,上海的互联网氛围要比深圳强很多! - -好了,希望这份list可以帮助到想在上海发展的录友们。 - -相对于北京和上海,深圳互联网公司断层很明显,腾讯一家独大,二线三线垂直行业的公司很少,所以说深圳腾讯的员工流动性相对是较低的,因为基本没得选。 - - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\344\272\222\350\201\224\347\275\221\345\244\247\345\216\202\347\240\224\345\217\221\346\265\201\347\250\213.md" "b/problems/\345\211\215\345\272\217/\344\272\222\350\201\224\347\275\221\345\244\247\345\216\202\347\240\224\345\217\221\346\265\201\347\250\213.md" deleted file mode 100644 index ed526897e5..0000000000 --- "a/problems/\345\211\215\345\272\217/\344\272\222\350\201\224\347\275\221\345\244\247\345\216\202\347\240\224\345\217\221\346\265\201\347\250\213.md" +++ /dev/null @@ -1,240 +0,0 @@ -# 揭秘互联网大厂研发流程 - -[B站:揭秘互联网大厂研发流程](https://www.bilibili.com/video/BV1KR4y1H7ST) - -很多录友会很好奇这么个事: - -* 大厂的研发流程应该是什么样的呢 -* 为什么会有那么多繁琐的流程呢 -* 每一步都有什么作用呢 - -这次给大家介绍一波大厂的研发流程,让大家明明白白。 - -其实对于几十人或者上百人一起开发一个项目的话,一个规范的研发流程是很重要的。 - -有的同学可能想,哪有这么多流程啊,就是写完代码,跑一下,没问题,然后就上线了。 - -其实在大厂里研发流程是很重要的。 - -**一个项目从开发到上线到后面的维护,从流程上就保证大家少出错,也方便后面人继续维护**。 - -那么接下来给大家介绍一下详细的流程。 - -## 1.需求文档 - -看需求文档,我们要根据需求文档来确定我们究竟要做什么。 - -一些同学可能感觉 为什么还要用一个需求文档呢,你就告诉我做啥我就做啥不就完事了? - -需求文档一方面是**倒逼产品经理去系统性的思考这个需求究竟有哪些功能**,用来满足哪些用户的需求。 - -另一方面是**保证我们在研发的时候,研发出来的功能是满足需求文档里所描述的**。 - -如果是口头对接的话,很有可能就是你做出来的东西,产品经理看完感觉:这和我说的需求不一样啊!!这和我想的不一样啊!! - -这样就是两个人相互“甩锅”,那这究竟是谁的锅呢。都没有一个证据,对吧。 - -所以说,有一个需求文档很重要。 - -而且每个阶段的需求文档相当于是把这个项目的整个迭代过程都记录下来了。 - -这样也方便后面的人,了解这个项目是如何迭代的。 - -## 2.这个需求包含了哪些功能 - -产品经理在需求文档里描述一个功能,那么我们在实现的时候,可能要改很多模块,或者说我们要增加一些模块。 - -就是我们从代码的角度上来讲,可能要增添很多功能才能满足 用户看起来好像微不足道的小功能。 - -例如点击登录,点击下单,后台都有很多模块协同运行的。 - -我们要把产品经理角度上的这个功能,拆解为我们代码角度上的具体应该开发的那些功能。 - -## 3.确定有哪些难点 - -这里可能有同学疑惑了,我确定这东西干嘛呢。 - -给大家举一个例子,给你一个需求文档。 - -你说你一周的时间就能把它开发完,那为什么是一周呢,为什么不是两天,为什么不是两周呢。 - -其实 和上面的领导汇报你的工作的时候 **都要把自己的工作进行量化**。 - -那么这个功能有哪些难点,我们要克服这个难点,所需要花费的时间,都要有一个大体的量化。 - -这样才能量化我们自己的工作,**领导其实不知道你的工作是简单 还是困难, 领导只在意最终结果**,所以你需要展现给领导你的工作是有难度的是有挑战的。 - -而且**这些也是我们年底用来晋升或者评职称的素材**。 - -如果这些东西你自己都不在乎的话,谁还会帮你在乎呢。 - -而且**确定了自己的工作难点,把这些难点都记录下来,对以后跳槽也很有帮助**。 - -面试官最喜欢问的问题,就是:**你做的项目中有哪些难点?以及你是如何克服的**。 - -所以这一步对自己个人成长来说也是很有重要的。 对于项目组来说也是记录下来,每一个迭代版本有哪些难点,这些难点团队是如何克服的。 - -这也是项目组往上一级去邀功的资料。 - - -## 4.画架构图 - -一般来说,大厂项目的架构都是比较复杂的,特别是后端架构。 - -如果添加一个模块连个文档都没有,连个图也没有,上来就添加的话,后面的人是很难维护的。 - -而且每个模块和每一个模块之间的依赖关系,如果没有画出一个架构图的话,直接看代码很容易直接就看懵了。 - -为什么你可以快速开发一个新的模块,也是因为之前团队有人把这个架构图画清楚了,你只需要看一眼这个架构图,就知道你的模块应该添加在哪里。 - -那么你去添加模块的时候,也应该把这个架构图相应的位置 完善一下。 - -同时呢,在画架构图的过程中,也增添了自己对整个系统架构的掌握程度。 - -这个图也会让你确定,你的模块在整个项目中扮演一个什么样的角色。 - -## 5.定协议 - -后台模块之间进行通讯需要协议,后台和前端通讯也需要协议。 - -所以只要有交互,就要确定协议的数据格式。 - -**定协议要考虑到兼容,要考虑易于维护**。 - -## 6.设计数据结构和算法 - -其实设计数据结构更多一些,因为我们要选择使用什么容器,什么格式来处理我们的数据。 - -至于算法的话,就很少我们亲自设计了。 - -什么快排,二叉树,动态规划,最短路啥的,在实际开发中,都不需要我们自己去写,**直接调包!** - -面试造火箭,工作拧螺丝 就体现在这里。 - -为什么会这样呢? 一个很简单的例子,互联网研发讲究其实就是要快,例如一个功能2天就要开发完,如果算法都要自己去写的话,等都写完了,花都谢了。 - -最关键的是,**你实现的算法 极大概率没有现成的算法接口安全性高**。 - -**开发中要学会才在巨人的肩膀上**。 - -## 7.预估一下容量 - -特别是后端开发,要估计出 我们自己模块大体需要多大磁盘,多大内存,多大带宽,多少核CPU。 - -这也是没有做过研发工作的同学经常忽略的,**因为大家好像默认 磁盘、内存、带宽、cpu是无穷的**。 - -其实我们在设计一个模块的时候,这些都要评估的,不能模块一上线,把机器直接打爆了。 - -例如 直接把带宽打满了,不仅影响自己的模块功能,还影响了机器上其他模块的运行。 - - -## 8.考虑部署 - -要考虑如果一台机器挂了(可能是硬件原因),那么我们的模块还能不能正常提供服务。 - -这就是考虑模块的容灾性,一般都是采用分布式,服务部署在三台机器上,一台如果挂了,还有其他两台提供服务。 - -还有就是要弹性可伸缩,即我们的模块可不可以直接 部署多台机器来提高承载能力。 - -如果用户量突然上来了,或者流量突然上来了,可以通过快速部署多台机器来抗住流量。 - -而不是模块只能在单机上跑,多部署几台就发生各种问题。 - -**这才能说明是有足够强的风险意识的**。 - -## 9.设计评审 - -前八的阶段其实都是设计阶段,那么你的设计需要让组里的同学一起来评审一下,看看有没有什么问题。 - -大家主要也是看看你的模块 会不会给其他模块或者整个系统带来什么问题 以及 设计的是否合理。 - - -## 10.写代码 - -终于到写代码的阶段了,其实到这时候,是最容易的。 - -**写代码就是体力活了,不是脑力活了**。 - -## 11.自测 - -写完代码,我们需要自测,自己的功能会不会有什么问题。 - -这里可能需要自己造一造数据,跑一跑 看看和预想的是不是一样的。 - -## 12.联调 - -自己的模块可能会涉及到其他模块之间的一个交互,或者和前端的一个交互。 - -所以需要其他同学配合一起来测试。 - -这里就有很多沟通工作了,因为其他同学可能手头有自己的活,那么就要协调一个时间来一起测试。 - -这一步也是很费时间的,**其费时关键是要等,要等其他同学有空和你联调或者是别人等你**,而且往往不是联调一次就能解决问题的。 - -所以 在评估开发时间的时候 也要考虑到联调的时间。 - -这也是大厂研发效率低的地方,但上百人开发的项目,**这种沟通上消耗也是在所难免的**。 - -## 13.交给测试 - -自己的代码,自己测 一般都测不出什么问题,需要交给测试同学来给你测一测。 - -这里如果测试同学测出问题,你就要判断确实有问题还是 测试方式不对,如果有问题就要修改,再提给测试,反反复复这么几轮,直到测试同学测试没问题了。 - -**这个过程也是累心的**。 - -## 14.code review - -代码合入主干之前,需要 项目组的同学一起来评审一下你的代码。 - -之前是评审设计,看设计上有没有什么缺失,这次是大家来看看你代码写的怎么样。 - -例如合入主干会不会有什么问题,代码兼容性做的好不好,接口设计的好不好,甚至字段,函数,变量名,命名合不合理。 - -都要经过大家的评审,如果有问题就还是要改。 - -如果没有问题一般 大家会给+2(就是通过的意思),这样就可以合入主干了。 - -## 15.合入主干 - -合入主干为什么会单独列出来呢。 - -其实合入主干是很重要的,经常是自己的代码没问题,但合入主干之后就有问题了。 - -一般就是合入主干的时候有冲突,例如你从主干拉出一个分支,另一个同学从主干拉出一个分支,而且两个分支修改了同一个模块,如果另一个同学提前合入主干,你再合入主干的时候就会有代码冲突。 - -在解决代码冲突的时候 就会修改别人的代码,这个过程很容易产生新的bug。 - - -**一般合入主干之后,测试同学还要重新跑一个全量测试,才能放心发布**。 - -如果跑全量测试没有问题的话,才会松一口气(懂的人都懂)。 - -## 16.发布 - -最后一步就是发布了。 - -发布其实就是把主干的代码更新到线上的服务器上。 - -一些还没有工作的同学,可能不太理解究竟什么是发布。 - -用大白话来讲,就是把 本地的代码或者某台机器的代码,编译成可执行文件,然后更新到 线上的服务器(一个独立的集群,专门处理线上的流量)并运行起来。 - -上线是最重要的一步了,也很容易出问题,因为一个大型项目,其上线的过程都非常复杂(要更新上百台机器的集群),而且**要考虑线上新版和旧版本的兼容问题**。 - -这也是为什么大厂项目都选择深夜上线,**因为深夜在线用户最少,如果出问题,影响的用户会比较少,可以快速修复**。 - -所以大家经常看到 某大厂程序员深夜上线发布版本之类的。 - -## 总结 - -好了,整整讲了十六个步骤!把大厂研发流程中 具体都有哪一步,为什么要这么做,都分析的很清楚了。 - -不过在大厂也不是所有部门都按照这个流程来的,每个部门都有自己玩法,各个部门也不太统一。 - -我这里是介绍的是已经比较规范的流程,**但流程越正规,研发效率就越低,想要提高效率,就是简化流程,简化流程,就会提高项目出错的概率**。 - -**所以这也是一个相互权衡的过程,每一个部门可能根据自己的业务特点,适当简化流程**。 - -好了,讲了这么多,希望对录友们有所启发。 - diff --git "a/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" "b/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" index 95f4f129da..db54fcb3e7 100644 --- "a/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" +++ "b/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" @@ -57,7 +57,7 @@ 我做了一下总结如图: -![编程风格](https://code-thinking-1253855093.file.myqcloud.com/pics/20201119173039835.png) +![编程风格](https://file1.kamacoder.com/i/algo/20201119173039835.png) ### 水平留白(代码空格) @@ -134,4 +134,4 @@ Google规范是 大括号和 控制语句保持同一行的,我个人也很认 ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\345\210\267\344\272\206\350\277\231\344\271\210\345\244\232\351\242\230\357\274\214\344\275\240\344\272\206\350\247\243\350\207\252\345\267\261\344\273\243\347\240\201\347\232\204\345\206\205\345\255\230\346\266\210\350\200\227\344\271\210\357\274\237.md" "b/problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" similarity index 92% rename from "problems/\345\211\215\345\272\217/\345\210\267\344\272\206\350\277\231\344\271\210\345\244\232\351\242\230\357\274\214\344\275\240\344\272\206\350\247\243\350\207\252\345\267\261\344\273\243\347\240\201\347\232\204\345\206\205\345\255\230\346\266\210\350\200\227\344\271\210\357\274\237.md" rename to "problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" index f70564e4a2..2be0bea5c1 100644 --- "a/problems/\345\211\215\345\272\217/\345\210\267\344\272\206\350\277\231\344\271\210\345\244\232\351\242\230\357\274\214\344\275\240\344\272\206\350\247\243\350\207\252\345\267\261\344\273\243\347\240\201\347\232\204\345\206\205\345\255\230\346\266\210\350\200\227\344\271\210\357\274\237.md" +++ "b/problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" @@ -19,7 +19,7 @@ 如果我们写C++的程序,就要知道栈和堆的概念,程序运行时所需的内存空间分为 固定部分,和可变部分,如下: -![C++内存空间](https://code-thinking-1253855093.file.myqcloud.com/pics/20210309165950660.png) +![C++内存空间](https://file1.kamacoder.com/i/algo/20210309165950660.png) 固定部分的内存消耗 是不会随着代码运行产生变化的, 可变部分则是会产生变化的 @@ -41,7 +41,7 @@ 想要算出自己程序会占用多少内存就一定要了解自己定义的数据类型的大小,如下: -![C++数据类型的大小](https://code-thinking-1253855093.file.myqcloud.com/pics/20200804193045440.png) +![C++数据类型的大小](https://file1.kamacoder.com/i/algo/20200804193045440.png) 注意图中有两个不一样的地方,为什么64位的指针就占用了8个字节,而32位的指针占用4个字节呢? @@ -109,7 +109,7 @@ CPU读取内存不是一次读取单个字节,而是一块一块的来读取 第一种就是内存对齐的情况,如图: -![内存对齐](https://code-thinking-1253855093.file.myqcloud.com/pics/20200804193307347.png) +![内存对齐](https://file1.kamacoder.com/i/algo/20200804193307347.png) 一字节的char占用了四个字节,空了三个字节的内存地址,int数据从地址4开始。 @@ -117,7 +117,7 @@ CPU读取内存不是一次读取单个字节,而是一块一块的来读取 第二种是没有内存对齐的情况如图: -![非内存对齐](https://code-thinking-1253855093.file.myqcloud.com/pics/20200804193353926.png) +![非内存对齐](https://file1.kamacoder.com/i/algo/20200804193353926.png) char型的数据和int型的数据挨在一起,该int数据从地址1开始,那么CPU想要读这个数据的话来看看需要几步操作: @@ -145,4 +145,4 @@ char型的数据和int型的数据挨在一起,该int数据从地址1开始, ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\346\203\263\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214\357\274\237.md" "b/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" similarity index 95% rename from "problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\346\203\263\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214\357\274\237.md" rename to "problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" index 5b57d214db..5e91198948 100644 --- "a/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\346\203\263\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214\357\274\237.md" +++ "b/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" @@ -61,4 +61,4 @@ int main() { ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\345\214\227\344\272\254\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\345\214\227\344\272\254\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index 7b42868e8f..0000000000 --- "a/problems/\345\211\215\345\272\217/\345\214\227\344\272\254\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,109 +0,0 @@ -# 北京互联网公司总结 - -

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

- - -**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** - -如果要在北京找工作,这份list可以作为一个大纲,寻找自己合适的公司。 - -## 一线互联网 - -* 百度(总部) -* 阿里(北京) -* 腾讯(北京) -* 字节跳动(总部) - -## 外企 - -* 微软(北京)微软中国主要就是北京和苏州 -* Hulu(北京)美国的视频网站,听说福利待遇超级棒 -* Airbnb(北京)房屋租赁平台 -* Grab(北京)东南亚第一大出行 App -* 印象笔记(北京)evernote在中国的独立品牌 -* FreeWheel(北京)美国最大的视频广告管理和投放平台 -* amazon(北京)全球最大的电商平台 - -## 二线互联网 - -* 美团点评(总部) -* 京东(总部) -* 网易(北京) -* 滴滴出行(总部) -* 新浪(总部) -* 快手(总部) -* 搜狐(总部) -* 搜狗(总部) -* 360(总部) - -## 硬件巨头 (有软件/互联网业务) - -* 华为(北京) -* 联想(总部) -* 小米(总部)后序要搬到武汉,互联网业务也是小米重头 - -## 三线互联网 - -* 爱奇艺(总部) -* 去哪儿网(总部) -* 知乎(总部) -* 豆瓣(总部) -* 当当网(总部) -* 完美世界(总部)游戏公司 -* 昆仑万维(总部)游戏公司 -* 58同城(总部) -* 陌陌(总部) -* 金山软件(北京)包括金山办公软件 -* 用友网络科技(总部)企业服务ERP提供商 -* 映客直播(总部) -* 猎豹移动(总部) -* 一点资讯(总部) -* 国双(总部)企业级大数据和人工智能解决方案提供商 - -## 明星创业公司 - -可以发现北京一堆在线教育的公司,可能教育要紧盯了政策变化,所以都要在北京吧 - -* 好未来(总部)在线教育 -* 猿辅导(总部)在线教育 -* 跟谁学(总部)在线教育 -* 作业帮(总部)在线教育 -* VIPKID(总部)在线教育 -* 雪球(总部)股市资讯 -* 唱吧(总部) -* 每日优鲜(总部)让每个人随时随地享受食物的美好 -* 微店(总部) -* 罗辑思维(总部)得到APP -* 值得买科技(总部)让每一次消费产生幸福感 -* 拉勾网(总部)互联网招聘 - -## AI独角兽公司 - -* 商汤科技(总部)专注于计算机视觉和深度学习 -* 旷视科技(总部)人工智能产品和解决方案公司 -* 第四范式(总部)人工智能技术与服务提供商 -* 地平线机器人(总部)边缘人工智能芯片的全球领导者 -* 寒武纪(总部)全球智能芯片领域的先行者 - -## 互联网媒体 - -* 央视网 -* 搜房网 -* 易车网 -* 链家网 -* 自如网 -* 汽车之家 - - -北京的互联网氛围绝对是最好的(暂不讨论户口和房价问题),大家如果看了[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)这篇之后,**会发现北京互联网外企和二线互联网公司数量多的优势,在深圳的互联网公司断档比较严重,如果去不了为数不多的一线公司,可选择的余地就非常少了,而北京选择的余地就很多!** - -相对来说,深圳的硬件企业更多一些,因为珠三角制造业配套比较完善。而大多数互联网公司其实就是媒体公司,当然要靠近政治文化中心,这也是有原因的。 - -就酱,我也会陆续整理其他城市的互联网公司,希望对大家有所帮助。 - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\345\271\277\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\345\271\277\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index b8b1641b63..0000000000 --- "a/problems/\345\211\215\345\272\217/\345\271\277\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,77 +0,0 @@ -# 广州互联网公司总结 - -

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

- - -**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** - -## 一线互联网 - -* 微信(总部) 有点难进! -* 字节跳动(广州) - -## 二线 -* 网易(总部)主要是游戏 - -## 三线 - -* 唯品会(总部) -* 欢聚时代(总部)旗下YY,虎牙,YY最近被浑水做空,不知百度还要不要收购了 -* 酷狗音乐(总部) -* UC浏览器(总部)现在隶属阿里创始人何小鹏现在搞小鹏汽车 -* 荔枝FM(总部)用户可以在手机上开设自己的电台和录制节目 -* 映客直播(总部)股票已经跌成渣了 -* 爱范儿(总部) -* 三七互娱(总部)游戏公司 -* 君海游戏(总部)游戏公司 -* 4399游戏(总部)游戏公司 -* 多益网络(总部)游戏公司 - -## 硬件巨头 (有软件/互联网业务) -* 小鹏汽车(总部)新能源汽车小霸王 - -## 创业公司 - -* 妈妈网(总部)母婴行业互联网公司 -* 云徙科技(总部)数字商业云服务提供商 -* Fordeal(总部)中东领先跨境电商平台 -* Mobvista(总部)移动数字营销 -* 久邦GOMO(总部)游戏 -* 深海游戏(总部)游戏 - -## 国企 - -* 中国电信广州研发(听说没有996) - - -## 总结 - -同在广东省,难免不了要和深圳对比,大家如果看了这篇:[深圳原来有这么多互联网公司,你都知道么?](https://programmercarl.com/前序/深圳互联网公司总结.html)就能感受到鲜明的对比了。 - -广州大厂高端岗位其实比较少,本土只有微信和网易,微信呢毕竟还是腾讯的分部,而网易被很多人认为是杭州企业,其实网易总部在广州。 - -广州是唯一一个一线城市没有自己本土互联网巨头的城市,所以网易选择在广州扎根还是很正确的,毕竟杭州是阿里的天下,广州也应该扶持一把本土的互联网公司。 - -虽然对于互联网从业人员来说,广州的岗位要比深圳少很多,**但是!!广州的房价整体要比深圳低30%左右,而且广州的教育,医疗,公共资源完全碾压深圳**。 - -教育方面:大学广州有两个985,四个211,深圳这方面就不用说了,大家懂得。 - -基础教育方面深圳的小学初中高中学校数量远远不够用,小孩上学竞争很激烈,我也是经常听同事们说,耳濡目染了。 - -而医疗上基本深圳看不了的病都要往广州跑,深圳的医院数量也不够用。 - -在生活节奏上,广州更慢一些,更有生活的气息,而深圳生存下去的气息更浓烈一些。 - -所以很多在深圳打拼多年的IT从业者选择去广州安家也是有原因的。 - -但也有很多从广州跑到深圳的,深圳发展的机会更多,而广州教育医疗更丰富,房价不高(相对深圳)。 - - - - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\346\210\220\351\203\275\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\346\210\220\351\203\275\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index f6a575f64d..0000000000 --- "a/problems/\345\211\215\345\272\217/\346\210\220\351\203\275\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,72 +0,0 @@ - -# 成都互联网公司总结 -

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

- - -**排名不分先后,个人总结难免有所疏漏,欢迎补充!** - -## 一线互联网 -* 腾讯(成都) 游戏,王者荣耀就在成都! -* 阿里(成都) -* 蚂蚁金服(成都) -* 字节跳动(成都) - -## 硬件巨头 (有软件/互联网业务) - -* 华为(成都) -* OPPO(成都) - -## 二线互联网 - -* 京东(成都) -* 美团(成都) -* 滴滴(成都) - -## 三线互联网 - -* 完美世界 (成都)游戏 -* 聚美优品 (成都) -* 陌陌 (成都) -* 爱奇艺(成都) - -## 外企互联网 - -* NAVER China (成都)搜索引擎公司,主要针对韩国市场 - -## 创业公司 - -* tap4fun(总部)游戏 -* 趣乐多(总部)游戏 -* 天上友嘉(总部)游戏 -* 三七互娱(成都)游戏 -* 咕咚(总部)智能运动 -* 百词斩(总部)在线教育 -* 晓多科技(总部)AI方向 -* 萌想科技(总部)实习僧 -* Camera360(总部)移动影像社区 -* 医联 (总部)医疗解决方案提供商 -* 小明太极 (总部)原创漫画文娱内容网站以及相关APP -* 小鸡叫叫(总部)致力于儿童教育的智慧解决方案 - - -## AI独角兽公司 - -* 科大讯飞(成都) -* 商汤(成都) - -## 总结 - -可以看出成都相对一线城市的互联网氛围确实差了很多。**但是!成都已经是在内陆城市中甚至二线城市中的佼佼者了!** - -从公司的情况上也可以看出:**成都互联网行业目前的名片是“游戏”**,腾讯、完美世界等大厂,还有无数小厂都在成都搞游戏,可能成都的天然属性就是娱乐,这里是游戏的沃土吧。 - -相信大家如果在一些招聘平台上去搜,其实很多公司都在成都,但都是把客服之类的工作安排在成都,而我在列举的时候尽量把研发相关在成都的公司列出来,这样对大家更有帮助。 - - - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.md" "b/problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" similarity index 92% rename from "problems/\345\211\215\345\272\217/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.md" rename to "problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" index 9d00a0f2a0..4252fc8779 100644 --- "a/problems/\345\211\215\345\272\217/\345\205\263\344\272\216\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\344\275\240\344\270\215\347\237\245\351\201\223\347\232\204\351\203\275\345\234\250\350\277\231\351\207\214\357\274\201.md" +++ "b/problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -19,7 +19,7 @@ **时间复杂度是一个函数,它定性描述该算法的运行时间**。 -我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的答题时间。 +我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的大体时间。 那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。 @@ -38,18 +38,18 @@ 同样的同理再看一下快速排序,都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)**。 **但是我们依然说快速排序是O(nlogn)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示: -![时间复杂度4,一般情况下的时间复杂度](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728185745611-20230310123844306.png) +![时间复杂度4,一般情况下的时间复杂度](https://file1.kamacoder.com/i/algo/20200728185745611-20230310123844306.png) 我们主要关心的还是一般情况下的数据形式。 -**面试中说道算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 +**面试中说的算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 ## 不同数据规模的差异 如下图中可以看出不同算法的时间复杂度在不同数据输入规模下的差异。 -![时间复杂度,不同数据规模的差异](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728191447384-20230310124015324.png) +![时间复杂度,不同数据规模的差异](https://file1.kamacoder.com/i/algo/20200728191447384-20230310124015324.png) 在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。 @@ -61,7 +61,7 @@ 例如上图中20就是那个点,n只要大于20 常数项系数已经不起决定性作用了。 -**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**: +**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂度的一个排行如下所示**: O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶 @@ -115,7 +115,7 @@ O(2 × n^2 + 10 × n + 1000) < O(3 × n^2),所以说最后省略掉常数项 为什么可以这么做呢?如下图所示: -![时间复杂度1.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728191447349-20230310124032001.png) +![时间复杂度1.png](https://file1.kamacoder.com/i/algo/20200728191447349-20230310124032001.png) 假如有两个算法的时间复杂度,分别是log以2为底n的对数和log以10为底n的对数,那么这里如果还记得高中数学的话,应该不难理解`以2为底n的对数 = 以2为底10的对数 * 以10为底n的对数`。 @@ -164,4 +164,4 @@ O(2 × n^2 + 10 × n + 1000) < O(3 × n^2),所以说最后省略掉常数项 ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\346\235\255\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\346\235\255\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index 6154cfe50f..0000000000 --- "a/problems/\345\211\215\345\272\217/\346\235\255\345\267\236\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,83 +0,0 @@ -# 杭州互联网公司总结 - -

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

- - -**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** - -## 一线互联网 - -* 阿里巴巴(总部) -* 蚂蚁金服(总部)阿里旗下 -* 阿里云(总部)阿里旗下 -* 网易(杭州) 网易云音乐 -* 字节跳动(杭州)抖音分部 - -## 外企 - -* ZOOM (杭州研发中心)全球知名云视频会议服务提供商 -* infosys(杭州)印度公司,据说工资相对不高 -* 思科(杭州) - -## 二线互联网 - -* 滴滴(杭州) -* 快手(杭州) - -## 硬件巨头 (有软件/互联网业务) - -* 海康威视(总部)安防三巨头 -* 浙江大华(总部)安防三巨头 -* 杭州宇视(总部) 安防三巨头 -* 萤石 -* 华为(杭州) -* vivo(杭州) -* oppo(杭州) -* 魅族(杭州) - -## 三线互联网 - -* 蘑菇街(总部)女性消费者的电子商务网站 -* 有赞(总部)帮助商家进行网上开店、社交营销 -* 菜鸟网络(杭州) -* 花瓣网(总部)图片素材领导者 -* 兑吧(总部)用户运营服务平台 -* 同花顺(总部)网上股票证券交易分析软件 -* 51信用卡(总部)信用卡管理 -* 虾米(总部)已被阿里收购 -* 曹操出行(总部) -* 口碑网 (总部) - -## AI独角兽公司 - -* 旷视科技(杭州) -* 商汤(杭州) - -## 创业公司 - -* e签宝(总部)做电子签名 -* 婚礼纪(总部)好多结婚的朋友都用 -* 大搜车(总部)中国领先的汽车交易服务供应商 -* 二更(总部)自媒体 -* 丁香园(总部) - -## 总结 - -杭州距离上海非常近,难免不了和上海做对比,上海是金融之都,如果看了[上海有这些互联网公司,你都知道么?](https://programmercarl.com/前序/上海互联网公司总结.html)就会发现上海互联网也是仅次于北京的。 - -而杭州是阿里的大本营,到处都有阿里的影子,虽然有网易在,但是也基本是盖过去了,很多中小公司也都是阿里某某高管出来创业的。 - -杭州的阿里带动了杭州的电子商务领域热度非常高,如果你想做电商想做直播带货想做互联网营销,杭州都是圣地! - -如果要是写代码的话,每年各种节日促销,加班996应该是常态,电商公司基本都是这样,当然如果赶上一个好领导的话,回报也是很丰厚的。 - -「代码随想录」一直都是干活满满,值得介绍给每一位学习算法的同学! - - - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\346\267\261\345\234\263\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" "b/problems/\345\211\215\345\272\217/\346\267\261\345\234\263\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" deleted file mode 100644 index 52a8448b51..0000000000 --- "a/problems/\345\211\215\345\272\217/\346\267\261\345\234\263\344\272\222\350\201\224\347\275\221\345\205\254\345\217\270\346\200\273\347\273\223.md" +++ /dev/null @@ -1,79 +0,0 @@ - -# 深圳互联网公司总结 - -

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

- -**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** - -## 一线互联网 - -* 腾讯(总部深圳) -* 百度(深圳) -* 阿里(深圳) -* 字节跳动(深圳) - -## 硬件巨头 (有软件/互联网业务) - -* 华为(总部深圳) -* 中兴(总部深圳) -* 海能达(总部深圳) -* oppo(总部深圳) -* vivo(总部深圳) -* 深信服(总部深圳) -* 大疆(总部深圳,无人机巨头) -* 一加手机(总部深圳) -* 柔宇科技(最近口碑急转直下) - -## 二线大厂 - -* 快手(深圳) -* 京东(深圳) -* 顺丰(总部深圳) - -## 三线大厂 - -* 富途证券(2020年成功赴美上市,主要经营港股美股) -* 微众银行(总部深圳) -* 招银科技(总部深圳) -* 平安系列(平安科技、平安寿险、平安产险、平安金融、平安好医生等) -* Shopee(21年有裁员风波) -* 有赞(深圳) -* 迅雷(总部深圳) -* 金蝶(总部深圳) -* 随手记(总部深圳) - -## AI独角兽公司 - -* 商汤科技(人工智能领域的独角兽) -* 追一科技(一家企业级智能服务AI公司) -* 超多维科技 (计算机视觉、裸眼3D) -* 优必选科技 (智能机器人、人脸识别) - -## 明星创业公司 - -* 丰巢科技(让生活更简单) -* 人人都是产品经理(全球领先的产品经理和运营人 学习、交流、分享平台) -* 大丰收(综合农业互联网服务平台) -* 小鹅通(专注新教育的技术服务商) -* 货拉拉(拉货就找货拉拉) -* 编程猫(少儿编程教育头部企业) -* HelloTalk(全球最大的语言学习社交社区) -* 大宇无限( 拥有SnapTube, Lark Player 等多款广受海外新兴市场用户欢迎的产品) -* 知识星球(深圳大成天下公司出品) -* XMind(隶属深圳市爱思软件技术有限公司,思维导图软件) -* 小赢科技(以技术重塑人类的金融体验) - -## 其他行业(有软件/互联网业务) - -* 三大电信运营商:中国移动、中国电信、中国联通 -* 房产企业:恒大(暴雷)、万科 -* 中信深圳 -* 广发证券,深交所 -* 珍爱网(珍爱网是国内知名的婚恋服务网站之一) - - - - - ------------------------ -
diff --git "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" index a2f6ee3b5f..c991be1542 100644 --- "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" +++ "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" @@ -130,4 +130,4 @@ Markdown支持部分html,例如这样 ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" index fc06c713a0..762b55f400 100644 --- "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" +++ "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" @@ -99,15 +99,19 @@ Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简 面试只有短短的30分钟或者一个小时,如何把自己掌握的技术更好的展现给面试官呢,博客、github都是很好的选择,如果把这些放在简历上,面试官一定会看的,这都是加分项。 -## 简历模板 +## 领取方式 最后福利,把我的简历模板贡献出来!如下图所示。 -![简历模板](https://code-thinking-1253855093.file.myqcloud.com/pics/20200803175538158.png) +![简历模板](https://file1.kamacoder.com/i/algo/20200803175538158.png) 这里是简历模板中Markdown的代码:[https://github.com/youngyangyang04/Markdown-Resume-Template](https://github.com/youngyangyang04/Markdown-Resume-Template) ,可以fork到自己Github仓库上,按照这个模板来修改自己的简历。 -**Word版本的简历,大家可以在公众号「代码随想录」后台回复:简历模板,就可以获取!** +**Word版本的简历,添加如下企业微信,通过之后就会发你word版本**。 + +
+ +如果已经有我的企业微信,直接回复:简历模板,就可以了。 ## 总结 @@ -115,4 +119,4 @@ Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简 ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\345\205\263\344\272\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\345\217\257\350\203\275\346\234\211\345\207\240\344\270\252\347\226\221\351\227\256\357\274\237.md" "b/problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" similarity index 94% rename from "problems/\345\211\215\345\272\217/\345\205\263\344\272\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\345\217\257\350\203\275\346\234\211\345\207\240\344\270\252\347\226\221\351\227\256\357\274\237.md" rename to "problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" index 19384fd980..da2caa24b6 100644 --- "a/problems/\345\211\215\345\272\217/\345\205\263\344\272\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\357\274\214\345\217\257\350\203\275\346\234\211\345\207\240\344\270\252\347\226\221\351\227\256\357\274\237.md" +++ "b/problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -32,7 +32,7 @@ 同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。 -来看一下例子,什么时候的空间复杂度是$O(1)$呢,C++代码如下: +来看一下例子,什么时候的空间复杂度是 $O(1)$ 呢,C++代码如下: ```CPP int j = 0; @@ -65,4 +65,4 @@ for (int i = 0; i < n; i++) { ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/On\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" "b/problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" similarity index 80% rename from "problems/\345\211\215\345\272\217/On\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" rename to "problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" index 8b0934c582..5603412717 100644 --- "a/problems/\345\211\215\345\272\217/On\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" +++ "b/problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" @@ -8,15 +8,15 @@ ## 超时是怎么回事 -![程序超时](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729112716117-20230310124308704.png) +![程序超时](https://file1.kamacoder.com/i/algo/20200729112716117-20230310124308704.png) 大家在leetcode上练习算法的时候应该都遇到过一种错误是“超时”。 也就是说程序运行的时间超过了规定的时间,一般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)的解法了。 ## 从硬件配置看计算机的性能 @@ -29,7 +29,7 @@ 所以 1GHz = 10亿Hz,表示CPU可以一秒脉冲10亿次(有10亿个时钟周期),这里不要简单理解一个时钟周期就是一次CPU运算。 -例如1 + 2 = 3,cpu要执行四次才能完整这个操作,步骤一:把1放入寄存机,步骤二:把2放入寄存器,步骤三:做加法,步骤四:保存3。 +例如1 + 2 = 3,cpu要执行四次才能完整这个操作,步骤一:把1放入寄存器,步骤二:把2放入寄存器,步骤三:做加法,步骤四:保存3。 而且计算机的cpu也不会只运行我们自己写的程序上,同时cpu也要执行计算机的各种进程任务等等,我们的程序仅仅是其中的一个进程而已。 @@ -52,7 +52,7 @@ * 火箭科学家需要大致知道一枚试射火箭的着陆点是在大海里还是在城市中; * 医学研究者需要知道一次药物测试是会杀死还是会治愈实验对象; -所以**任何开发计算机程序员的软件工程师都应该能够估计这个程序的运行时间是一秒钟还是一年**。 +所以**任何开发计算机程序的软件工程师都应该能够估计这个程序的运行时间是一秒钟还是一年**。 这个是最基本的,所以以上误差就不算事了。 @@ -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) @@ -124,27 +124,27 @@ int main() { 来看一下运行的效果,如下图: -![程序超时2](https://code-thinking-1253855093.file.myqcloud.com/pics/20200729200018460-20230310124315093.png) +![程序超时2](https://file1.kamacoder.com/i/algo/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) +![程序超时3](https://file1.kamacoder.com/i/algo/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) +![程序超时4](https://file1.kamacoder.com/i/algo/20200729195729407-20230310124322232.png) -$O(n\log n)$的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 +$O(n\log n)$ 的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 **整体测试数据整理如下:** -![程序超时1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201208231559175-20230310124325152.png) +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175-20230310124325152.png) 至于 $O(\log n)$ 和 $O(n^3)$ 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 @@ -263,7 +263,7 @@ public class TimeComplexity { ## 总结 -本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看$O(n)$的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 +本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看 $O(n)$ 的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 @@ -278,4 +278,4 @@ public class TimeComplexity { ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" "b/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" index 6099747a92..edb62bc8b5 100644 --- "a/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" +++ "b/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" @@ -25,7 +25,7 @@ index_{left}=(\sum_{i=0}^{i=k}2^i)+2*m-1-1=2^{k+1}+2m-3 $$ - - 故左孩子的下表为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。 + - 故左孩子的下标为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。 diff --git "a/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" index aacc456857..01c07a5c00 100644 --- "a/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" +++ "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" @@ -29,7 +29,7 @@ int fibonacci(int i) { 可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一棵递归树,如图: -![递归空间复杂度分析](https://code-thinking-1253855093.file.myqcloud.com/pics/20210305093200104.png) +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305093200104.png) 从图中,可以看出f(5)是由f(4)和f(3)相加而来,那么f(4)是由f(3)和f(2)相加而来 以此类推。 @@ -191,12 +191,12 @@ int main() 因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。 -此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是$O(1)$。 +此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是 $O(1)$ 。 在看递归的深度是多少呢?如图所示: -![递归空间复杂度分析](https://code-thinking-1253855093.file.myqcloud.com/pics/20210305094749554.png) +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305094749554.png) 递归第n个斐波那契数的话,递归调用栈的深度就是n。 @@ -214,7 +214,7 @@ int fibonacci(int i) { 最后对各种求斐波那契数列方法的性能做一下分析,如题: -![递归的空间复杂度分析](https://code-thinking-1253855093.file.myqcloud.com/pics/20210305095227356.png) +![递归的空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305095227356.png) 可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。 @@ -264,4 +264,4 @@ int binary_search( int arr[], int l, int r, int x) { ----------------------- -
+
diff --git "a/problems/\345\211\215\345\272\217/\351\200\232\350\277\207\344\270\200\351\201\223\351\235\242\350\257\225\351\242\230\347\233\256\357\274\214\350\256\262\344\270\200\350\256\262\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\201.md" "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" similarity index 94% rename from "problems/\345\211\215\345\272\217/\351\200\232\350\277\207\344\270\200\351\201\223\351\235\242\350\257\225\351\242\230\347\233\256\357\274\214\350\256\262\344\270\200\350\256\262\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\201.md" rename to "problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" index 1ea1e65be8..a02c37f2c9 100644 --- "a/problems/\345\211\215\345\272\217/\351\200\232\350\277\207\344\270\200\351\201\223\351\235\242\350\257\225\351\242\230\347\233\256\357\274\214\350\256\262\344\270\200\350\256\262\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246\357\274\201.md" +++ "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -69,7 +69,7 @@ int function3(int x, int n) { 我们来分析一下,首先看递归了多少次呢,可以把递归抽象出一棵满二叉树。刚刚同学写的这个算法,可以用一棵满二叉树来表示(为了方便表示,选择n为偶数16),如图: -![递归算法的时间复杂度](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209193909426.png) +![递归算法的时间复杂度](https://file1.kamacoder.com/i/algo/20201209193909426.png) 当前这棵二叉树就是求x的n次方,n为16的情况,n为16的时候,进行了多少次乘法运算呢? @@ -79,7 +79,7 @@ int function3(int x, int n) { 这么如果是求x的n次方,这个递归树有多少个节点呢,如下图所示:(m为深度,从0开始) -![递归求时间复杂度](https://code-thinking-1253855093.file.myqcloud.com/pics/20200728195531892.png) +![递归求时间复杂度](https://file1.kamacoder.com/i/algo/20200728195531892.png) **时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是O(n)**。对,你没看错,依然是O(n)的时间复杂度! @@ -140,4 +140,4 @@ int function3(int x, int n) { ----------------------- -
+
diff --git "a/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" old mode 100644 new mode 100755 index 040be29983..547cae8a30 --- "a/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" +++ "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 替换数字 @@ -37,11 +35,11 @@ 如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231030165201.png) +![](https://file1.kamacoder.com/i/algo/20231030165201.png) 然后从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231030173058.png) +![](https://file1.kamacoder.com/i/algo/20231030173058.png) 有同学问了,为什么要从后向前填充,从前向后填充不行么? @@ -170,8 +168,4 @@ for (int i = 0; i < a.size(); i++) { -

- - - diff --git "a/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" old mode 100644 new mode 100755 index 138cf3a88d..025073220a --- "a/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" +++ "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 右旋字符串 @@ -46,16 +44,16 @@ fgabcde 本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106170143.png) +![](https://file1.kamacoder.com/i/algo/20231106170143.png) 右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106171557.png) +![](https://file1.kamacoder.com/i/algo/20231106171557.png) 此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106172058.png) +![](https://file1.kamacoder.com/i/algo/20231106172058.png) 其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,**负负得正**,这样就不影响子串里面字符的顺序了。 @@ -86,7 +84,7 @@ int main() { 可以的,不过,要记得 控制好 局部反转的长度,如果先局部反转,那么先反转的子串长度就是 len - n,如图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20231106172534.png) +![](https://file1.kamacoder.com/i/algo/20231106172534.png) 代码如下: @@ -176,7 +174,3 @@ int main() { -

- - - diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 4df21fb761..ff73cd9606 --- "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" @@ -1,14 +1,12 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # Leetcode股票问题总结篇! 之前我们已经把力扣上股票系列的题目都讲过的,但没有来一篇股票总结,来帮大家高屋建瓴,所以总结篇这就来了! -![股票问题总结](https://code-thinking.cdn.bcebos.com/pics/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.jpg) +![股票问题总结](https://file1.kamacoder.com/i/algo/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.jpg) * [动态规划:121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) * [动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) @@ -473,8 +471,4 @@ public: -

- - - diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index e28bfd0472..32df8af41c --- "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 动态规划最强总结篇! @@ -11,7 +9,7 @@ 关于动态规划,在专题第一篇[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)就说了动规五部曲,**而且强调了五部对解动规题目至关重要!** -这是Carl做过一百多道动规题目总结出来的经验结晶啊,如果大家跟着「代码随想哦」刷过动规专题,一定会对这动规五部曲的作用感受极其深刻。 +这是Carl做过一百多道动规题目总结出来的经验结晶啊,如果大家跟着「代码随想录」刷过动规专题,一定会对这动规五部曲的作用感受极其深刻。 动规五部曲分别为: @@ -53,7 +51,7 @@ ## 背包问题系列 -背包问题大纲 +背包问题大纲 * [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) * [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) @@ -79,7 +77,7 @@ ## 股票系列 -股票问题总结 +股票问题总结 * [动态规划:买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) * [动态规划:本周我们都讲了这些(系列六)](https://programmercarl.com/周总结/20210225动规周末总结.html) @@ -93,7 +91,7 @@ ## 子序列系列 - + * [动态规划:最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html) * [动态规划:最长连续递增序列](https://programmercarl.com/0674.最长连续递增序列.html) @@ -132,7 +130,3 @@ 所以Carl花费的这么大精力,把自己对动规算法理解 一五一十的全部分享给了录友们,帮助大家少走弯路,加油! -

- - - diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index bff26d1d34..63f059798e --- "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,14 +1,12 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 动态规划理论基础 动态规划刷题大纲 - + ## 算法公开课 @@ -106,7 +104,7 @@ **如果这灵魂三问自己都做到了,基本上这道题目也就解决了**,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。 -然后在问问题,目的性就很强了,群里的小伙伴也可以快速知道提问者的疑惑了。 +然后再问问题,目的性就很强了,群里的小伙伴也可以快速知道提问者的疑惑了。 **注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!** @@ -131,8 +129,4 @@ 今天我们开始新的征程了,你准备好了么? -

- - - diff --git "a/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" "b/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 6621e0396b..9c92e3d6c3 --- "a/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" +++ "b/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 又是一波总结 @@ -69,7 +67,7 @@ for (int i = 0; i < array.size(); i++) { 其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,大家可以尝试用双指针做一个leetcode上两数之和的题目,就可以体会到我说的意思了。 -使用了哈希法解决了两数之和,但是哈希法并不使用于三数之和! +使用了哈希法解决了两数之和,但是哈希法并不适用于三数之和! 使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。 @@ -77,7 +75,7 @@ for (int i = 0; i < array.size(); i++) { 时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。 -所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。** +所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不断向中间逼近,在一个for循环下完成两个for循环的工作。** 只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。 @@ -90,12 +88,8 @@ for (int i = 0; i < array.size(); i++) { ## 总结 -本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为$O(n)$。 +本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为 $O(n)$ 。 建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。 -

- - - diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" index 594656ca23..11dd298292 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -203,4 +203,4 @@ public: **本周我们都是讲解了二叉树,从理论基础到遍历方式,从递归到迭代,从深度遍历到广度遍历,最后再用了一个翻转二叉树的题目把我们之前讲过的遍历方式都串了起来。** -

+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" index 57416775fb..5b25e9c076 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.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.相同的树。** @@ -254,5 +254,5 @@ traversal(cur->left, tmp, result); * Github:[leetcode-master](https://github.com/youngyangyang04/leetcode-master) * 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64) -![](https://code-thinking-1253855093.file.myqcloud.com/pics/2021013018121150.png) -
+![](https://file1.kamacoder.com/i/algo/2021013018121150.png) +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" index 391a463102..94d95efd38 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -55,7 +55,7 @@ **构造二叉树有三个注意的点:** -* 分割时候,坚持区间不变量原则,左闭右开,或者左闭又闭。 +* 分割时候,坚持区间不变量原则,左闭右开,或者左闭右闭。 * 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。 * 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。 @@ -87,4 +87,4 @@ **如果大家一路跟下来,一定收获满满,如果周末不做这个总结,大家可能都不知道自己收获满满,啊哈!** -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" index bbb14502c0..5276cdde6b 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -40,7 +40,7 @@ * 陷阱二 -在一个有序序列求最值的时候,不要定义一个全局遍历,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。 +在一个有序序列求最值的时候,不要定义一个全局变量,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。 推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。 @@ -116,4 +116,4 @@ 大家如果每天坚持跟下来,会发现又是充实的一周![机智] -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" index 77db9708c0..e8e2948791 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -114,4 +114,4 @@ * B站:[代码随想录](https://space.bilibili.com/525438321) * 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ) -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" index 2d20a19702..9da360c2c6 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -31,7 +31,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 在[回溯算法:求组合总和(二)](https://programmercarl.com/0039.组合总和.html)第一个树形结构没有画出startIndex的作用,**这里这里纠正一下,准确的树形结构如图所示:** -![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170730367.png) +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) ## 周二 @@ -45,7 +45,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因**。 -![40.组合总和II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123202817973.png) +![40.组合总和II1](https://file1.kamacoder.com/i/algo/20201123202817973.png) 我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: @@ -75,11 +75,11 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 -所以本题应该是一个道hard题目了。 +所以本题应该是一道hard题目了。 **本题的树形结构中,和代码的逻辑有一个小出入,已经判断不是回文的子串就不会进入递归了,纠正如下:** -![131.分割回文串](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123203228309.png) +![131.分割回文串](https://file1.kamacoder.com/i/algo/20201123203228309.png) ## 周四 @@ -90,7 +90,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 树形图如下: -![93.复原IP地址](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123203735933-20230310133532452.png) +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933-20230310133532452.png) 在本文的树形结构图中,我已经把详细的分析思路都画了出来,相信大家看了之后一定会思路清晰不少! @@ -112,7 +112,7 @@ if (s.size() > 12) return result; // 剪枝 如图: -![78.子集](https://code-thinking-1253855093.file.myqcloud.com/pics/202011232041348.png) +![78.子集](https://file1.kamacoder.com/i/algo/202011232041348.png) 认清这个本质之后,今天的题目就是一道模板题了。 @@ -166,4 +166,4 @@ leetcode上的计时应该是以4ms为单位,有的多提交几次,多个4ms -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" index ec36d1213d..031ddc0250 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -11,14 +11,14 @@ 树形结构如下: -![90.子集II](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111217110449-20230310133150714.png) +![90.子集II](https://file1.kamacoder.com/i/algo/2020111217110449-20230310133150714.png) ## 周二 在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨! 树形结构如下: -![491. 递增子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170832333-20230310133155209.png) +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201112170832333-20230310133155209.png) [回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)留言区大家有很多疑问,主要还是和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混合在了一起。 @@ -33,7 +33,7 @@ 可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。 如图: -![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170304979-20230310133201250.png) +![46.全排列](https://file1.kamacoder.com/i/algo/20201112170304979-20230310133201250.png) **大家此时可以感受出排列问题的不同:** @@ -46,7 +46,7 @@ 树形结构如下: -![47.全排列II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112171930470-20230310133206398.png) +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201112171930470-20230310133206398.png) **这道题目神奇的地方就是used[i - 1] == false也可以,used[i - 1] == true也可以!** @@ -54,11 +54,11 @@ 树层上去重(used[i - 1] == false),的树形结构如下: -![47.全排列II2.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172230434-20230310133211392.png) +![47.全排列II2.png](https://file1.kamacoder.com/i/algo/20201112172230434-20230310133211392.png) 树枝上去重(used[i - 1] == true)的树型结构如下: -![47.全排列II3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172327967-20230310133216389.png) +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201112172327967-20230310133216389.png) **可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!** @@ -97,4 +97,4 @@ -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" index 3494a32074..d934270a27 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -112,4 +112,4 @@ public: -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" index 9209efda71..70081e82e2 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -15,7 +15,7 @@ 如图: -![122.买卖股票的最佳时机II](https://code-thinking-1253855093.file.myqcloud.com/pics/2020112917480858.png) +![122.买卖股票的最佳时机II](https://file1.kamacoder.com/i/algo/2020112917480858.png) ## 周二 @@ -31,7 +31,7 @@ 如图: -![55.跳跃游戏](https://code-thinking-1253855093.file.myqcloud.com/pics/20201124154758229.png) +![55.跳跃游戏](https://file1.kamacoder.com/i/algo/20201124154758229.png) ## 周三 @@ -44,7 +44,7 @@ 如图: -![45.跳跃游戏II](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232309103-20230310133110942.png) +![45.跳跃游戏II](https://file1.kamacoder.com/i/algo/20201201232309103-20230310133110942.png) 注意:**图中的移动下标是到当前这步覆盖的最远距离(下标2的位置),此时没有到终点,只能增加第二步来扩大覆盖范围**。 @@ -55,10 +55,10 @@ 而版本二就比较统一的,超过范围,步数就加一,但在移动下标的范围了做了文章。 即如果覆盖最远距离下标是倒数第二点:直接加一就行,默认一定可以到终点。如图: -![45.跳跃游戏II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232445286-20230310133115650.png) +![45.跳跃游戏II2](https://file1.kamacoder.com/i/algo/20201201232445286-20230310133115650.png) 如果覆盖最远距离下标不是倒数第二点,说明本次覆盖已经到终点了。如图: -![45.跳跃游戏II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201201232338693-20230310133120115.png) +![45.跳跃游戏II1](https://file1.kamacoder.com/i/algo/20201201232338693-20230310133120115.png) 有的录友认为版本一好理解,有的录友认为版本二好理解,其实掌握一种就可以了,也不用非要比拼一下代码的简洁性,简洁程度都差不多了。 @@ -92,4 +92,4 @@ -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" index cd3b2f1346..dec7511c21 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.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究竟是以谁为底? 这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。 @@ -77,7 +77,7 @@ 文中从计算机硬件出发,分析计算机的计算性能,然后亲自做实验,整理出数据如下: -![程序超时1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201208231559175-20230310133304038.png) +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175-20230310133304038.png) **大家有一个数量级上的概念就可以了!** @@ -96,7 +96,7 @@ 文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。 -此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了$O(\log n)$的代码。 +此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了 $O(\log n)$ 的代码。 其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题! @@ -120,4 +120,4 @@ 就酱,「代码随想录」是技术公众号里的一抹清流,值得推荐给身边的朋友同学们! -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" index 91f9656f48..cface1c9d3 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -38,7 +38,7 @@ 如图: -![135.分发糖果](https://code-thinking-1253855093.file.myqcloud.com/pics/20201117114916878-20230310133332759.png) +![135.分发糖果](https://file1.kamacoder.com/i/algo/20201117114916878-20230310133332759.png) 接着在贪心另一边,左孩子大于右孩子,左孩子的糖果就要比右孩子多。 @@ -50,7 +50,7 @@ 局部最优可以推出全局最优。 如图: -![135.分发糖果1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201117115658791-20230310133346127.png) +![135.分发糖果1](https://file1.kamacoder.com/i/algo/20201117115658791-20230310133346127.png) ## 周三 @@ -98,4 +98,4 @@ 而且大家也会发现,贪心并没有想象中的那么简单,贪心往往妙的出其不意,触不及防! -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" index b37bba80dc..71abb155c6 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -9,7 +9,7 @@ 如图: -![452.用最少数量的箭引爆气球](https://code-thinking-1253855093.file.myqcloud.com/pics/20201123101929791-20230310133845522.png) +![452.用最少数量的箭引爆气球](https://file1.kamacoder.com/i/algo/20201123101929791-20230310133845522.png) 模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了,从前向后遍历重复的只要跳过就可以的。 @@ -21,7 +21,7 @@ 如图: -![435.无重叠区间](https://code-thinking-1253855093.file.myqcloud.com/pics/20201221201553618.png) +![435.无重叠区间](https://file1.kamacoder.com/i/algo/20201221201553618.png) 细心的同学就发现了,此题和 [贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)非常像。 @@ -71,7 +71,7 @@ public: 如图: -![763.划分字母区间](https://code-thinking-1253855093.file.myqcloud.com/pics/20201222191924417-20230310133855435.png) +![763.划分字母区间](https://file1.kamacoder.com/i/algo/20201222191924417-20230310133855435.png) ## 周四 @@ -86,7 +86,7 @@ public: 如图: -![56.合并区间](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223200632791-20230310133859587.png) +![56.合并区间](https://file1.kamacoder.com/i/algo/20201223200632791-20230310133859587.png) ## 总结 @@ -102,4 +102,4 @@ public: **「代码随想录」里总结的都是经典题目,大家跟着练就节省了不少选择题目的时间了**。 -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index 4cab00ccbd..e74907013c 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -99,7 +99,7 @@ public: 这道绝佳的面试题我没有用过,如果录友们有面试别人的需求,就把这个套路拿去吧。 -我在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中,以我自己面试别人的真实经历,通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获! +我在[通过一道面试题目,讲一讲递归算法的时间复杂度!](../前序/递归算法的时间复杂度.md)中,以我自己面试别人的真实经历,通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获! ## 周四 @@ -150,4 +150,4 @@ public: -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index ebc720caa0..7cedf63919 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -31,7 +31,7 @@ for (int i = 1; i < m; i++) { } ``` -![62.不同路径1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201209113631392-20230310133703294.png) +![62.不同路径1](https://file1.kamacoder.com/i/algo/20201209113631392-20230310133703294.png) ## 周二 @@ -45,7 +45,7 @@ dp[i][j]定义依然是:表示从(0 ,0)出发,到(i, j) 有dp[i][j]条 如图: -![63.不同路径II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114513928-20230310133707783.png) +![63.不同路径II](https://file1.kamacoder.com/i/algo/20210104114513928-20230310133707783.png) 这里难住了不少同学,代码如下: @@ -70,11 +70,11 @@ for (int i = 1; i < m; i++) { 拿示例1来举例如题: -![63.不同路径II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114548983-20230310133711888.png) +![63.不同路径II1](https://file1.kamacoder.com/i/algo/20210104114548983-20230310133711888.png) 对应的dp table 如图: -![63.不同路径II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104114610256-20230310133715981.png) +![63.不同路径II2](https://file1.kamacoder.com/i/algo/20210104114610256-20230310133715981.png) ## 周三 @@ -111,7 +111,7 @@ for (int i = 3; i <= n ; i++) { 举例当n为10 的时候,dp数组里的数值,如下: -![343.整数拆分](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104173021581-20230310133720552.png) +![343.整数拆分](https://file1.kamacoder.com/i/algo/20210104173021581-20230310133720552.png) @@ -143,7 +143,7 @@ dp数组如何初始化:只需要初始化dp[0]就可以了,推导的基础 n为5时候的dp数组状态如图: -![96.不同的二叉搜索树3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210107093253987-20230310133724531.png) +![96.不同的二叉搜索树3](https://file1.kamacoder.com/i/algo/20210107093253987-20230310133724531.png) ## 总结 @@ -153,4 +153,4 @@ n为5时候的dp数组状态如图: **但我还会坚持规划好的路线,难度循序渐进,并以面试经典题目为准,该简单的时候就是简单,同时也不会因为阅读量低就放弃有难度的题目!**。 -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index 7bae5ca9ee..8ae7882e61 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -17,7 +17,7 @@ 关于其他几种常用的背包,大家看这张图就了然于胸了: -![416.分割等和子集1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210117171307407-20230310133624872.png) +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407-20230310133624872.png) 本文用动规五部曲详细讲解了01背包的二维dp数组的实现方法,大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在确定初始化和遍历顺序上。 @@ -70,7 +70,7 @@ for(int i = 1; i < weight.size(); i++) { // 遍历物品 来看一下对应的dp数组的数值,如图: -![动态规划-背包问题4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210118163425129-20230310133630224.jpg) +![动态规划-背包问题4](https://file1.kamacoder.com/i/algo/20210118163425129-20230310133630224.jpg) 最终结果就是dp[2][4]。 @@ -122,7 +122,7 @@ for(int i = 0; i < weight.size(); i++) { // 遍历物品 一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: -![动态规划-背包问题9](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110103614769-20230310133634873.png) +![动态规划-背包问题9](https://file1.kamacoder.com/i/algo/20210110103614769-20230310133634873.png) ## 周三 @@ -160,4 +160,4 @@ for(int i = 0; i < weight.size(); i++) { // 遍历物品 就像是我们讲解01背包的时候,花了那么大力气才把每一个细节都讲清楚,这里其实是基础,后面的背包问题怎么变,基础比较牢固自然会有自己的一套思考过程。 -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index e785af1228..ff3b771aa8 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -35,7 +35,7 @@ bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4 dp数组状态变化如下: -![494.目标和](https://code-thinking-1253855093.file.myqcloud.com/pics/20210125120743274-20230310132918821.jpg) +![494.目标和](https://file1.kamacoder.com/i/algo/20210125120743274-20230310132918821.jpg) ## 周二 @@ -73,7 +73,7 @@ dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); 最后dp数组的状态如下所示: -![474.一和零](https://code-thinking-1253855093.file.myqcloud.com/pics/20210120111201512-20230310132936011.jpg) +![474.一和零](https://file1.kamacoder.com/i/algo/20210120111201512-20230310132936011.jpg) ## 周三 @@ -140,4 +140,4 @@ for(int i = 0; i < weight.size(); i++) { // 遍历物品 此时相信大家对动规五部曲也有更深的理解了,同样也验证了Carl之前讲过的:**简单题是用来学习方法论的,而遇到难题才体现出方法论的重要性!** -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index fb570bd402..e1e6baf503 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -200,4 +200,4 @@ public: 此时我们就已经把完全背包的遍历顺序研究的透透的了! -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index 26d805bb3a..b17a682972 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -30,7 +30,7 @@ dp[1] = max(nums[0], nums[1]); 以示例二,输入[2,7,9,3,1]为例。 -![198.打家劫舍](https://code-thinking-1253855093.file.myqcloud.com/pics/20210221170954115-20230310133425353.jpg) +![198.打家劫舍](https://file1.kamacoder.com/i/algo/20210221170954115-20230310133425353.jpg) 红框dp[nums.size() - 1]为结果。 @@ -42,15 +42,15 @@ dp[1] = max(nums[0], nums[1]); * 情况一:考虑不包含首尾元素 -![213.打家劫舍II](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160748643.jpg) +![213.打家劫舍II](https://file1.kamacoder.com/i/algo/20210129160748643.jpg) * 情况二:考虑包含首元素,不包含尾元素 -![213.打家劫舍II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160821374.jpg) +![213.打家劫舍II1](https://file1.kamacoder.com/i/algo/20210129160821374.jpg) * 情况三:考虑包含尾元素,不包含首元素 -![213.打家劫舍II2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129160842491.jpg) +![213.打家劫舍II2](https://file1.kamacoder.com/i/algo/20210129160842491.jpg) 需要注意的是,**“考虑” 不等于 “偷”**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素!对于情况三,取nums[1] 和 nums[3]就是最大的。 @@ -178,7 +178,7 @@ return {val2, val1}; 以示例1为例,dp数组状态如下:(**注意用后序遍历的方式推导**) -![337.打家劫舍III](https://code-thinking-1253855093.file.myqcloud.com/pics/20210129181331613.jpg) +![337.打家劫舍III](https://file1.kamacoder.com/i/algo/20210129181331613.jpg) **最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 @@ -306,4 +306,4 @@ public: **代码随想录温馨提醒:投资有风险,入市需谨慎!** -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" index ec442a3979..0749becbba 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -77,7 +77,7 @@ dp[0][4] = 0; 以输入[1,2,3,4,5]为例 -![123.买卖股票的最佳时机III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201228181724295.png) +![123.买卖股票的最佳时机III](https://file1.kamacoder.com/i/algo/20201228181724295.png) 可以看到红色框为最后两次卖出的状态。 @@ -144,7 +144,7 @@ for (int j = 1; j < 2 * k; j += 2) { 以输入[1,2,3,4,5],k=2为例。 -![188.买卖股票的最佳时机IV](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229100358221-20230310133805763.png) +![188.买卖股票的最佳时机IV](https://file1.kamacoder.com/i/algo/20201229100358221-20230310133805763.png) 最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 @@ -197,7 +197,7 @@ vector> dp(n, vector(3, 0)); 以 [1,2,3,0,2] 为例,dp数组如下: -![309.最佳买卖股票时机含冷冻期](https://code-thinking-1253855093.file.myqcloud.com/pics/20201229163725348.png) +![309.最佳买卖股票时机含冷冻期](https://file1.kamacoder.com/i/algo/20201229163725348.png) 最后两个状态 不持有股票(能购买) 和 不持有股票(冷冻期)都有可能最后结果,取最大的。 @@ -206,4 +206,4 @@ vector> dp(n, vector(3, 0)); 下周还会有一篇股票系列的文章,**股票系列后面我也会单独写一篇总结,来高度概括一下,这样大家会对股票问题就有一个整体性的理解了**。 -
+
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" "b/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" index 52e2fec11f..6cbf6e2a77 100644 --- "a/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" +++ "b/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" @@ -206,4 +206,4 @@ public: **本周我们都是讲解了二叉树,从理论基础到遍历方式,从递归到迭代,从深度遍历到广度遍历,最后再用了一个翻转二叉树的题目把我们之前讲过的遍历方式都串了起来。** -
+
diff --git "a/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" "b/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 465ef9d1da..55caffe4f0 --- "a/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" +++ "b/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 哈希表总结篇如约而至 @@ -125,8 +123,4 @@ std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层 -

- - - diff --git "a/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index e426c65791..e3400ad7f0 --- "a/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -12,15 +10,15 @@ ## 哈希表 -首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 +首先什么是哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 > 哈希表是根据关键码的值而直接进行访问的数据结构。 -这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 +这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示: -![哈希表1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104234805168.png) +![哈希表1](https://file1.kamacoder.com/i/algo/20210104234805168.png) 那么哈希表能解决什么问题呢,**一般哈希表都是用来快速判断一个元素是否出现集合里。** @@ -38,7 +36,7 @@ 哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。 -![哈希表2](https://code-thinking-1253855093.file.myqcloud.com/pics/2021010423484818.png) +![哈希表2](https://file1.kamacoder.com/i/algo/2021010423484818.png) 如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢? @@ -54,7 +52,7 @@ 如图所示,小李和小王都映射到了索引下标 1 的位置,**这一现象叫做哈希碰撞**。 -![哈希表3](https://code-thinking-1253855093.file.myqcloud.com/pics/2021010423494884.png) +![哈希表3](https://file1.kamacoder.com/i/algo/2021010423494884.png) 一般哈希碰撞有两种解决方法, 拉链法和线性探测法。 @@ -62,7 +60,7 @@ 刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了 -![哈希表4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235015226.png) +![哈希表4](https://file1.kamacoder.com/i/algo/20210104235015226.png) (数据规模是dataSize, 哈希表的大小为tableSize) @@ -74,7 +72,7 @@ 例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示: -![哈希表5](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235109950.png) +![哈希表5](https://file1.kamacoder.com/i/algo/20210104235109950.png) 其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。 @@ -113,13 +111,13 @@ 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又有什么关系呢? 实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。 -![哈希表6](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235134572.png) +![哈希表6](https://file1.kamacoder.com/i/algo/20210104235134572.png) ## 总结 @@ -132,7 +130,3 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 -

- - - diff --git "a/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" "b/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 5d4c945075..7a7929f41a --- "a/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" +++ "b/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

> 20张树形结构图、14道精选回溯题目,21篇回溯法精讲文章,由浅入深,一气呵成,这是全网最强回溯算法总结! @@ -65,7 +63,7 @@ void backtracking(参数) { 本题我把回溯问题抽象为树形结构,如题: -![77.组合1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118152928844.png) +![77.组合1](https://file1.kamacoder.com/i/algo/20201118152928844.png) 可以直观的看出其搜索的过程:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**,这个理念贯穿整个回溯法系列,也是我做了很多回溯的题目,不断摸索其规律才总结出来的。 @@ -75,7 +73,7 @@ void backtracking(参数) { 优化回溯算法只有剪枝一种方法,在[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中把回溯法代码做了剪枝优化,树形结构如图: -![77.组合4](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118153133458.png) +![77.组合4](https://file1.kamacoder.com/i/algo/20201118153133458.png) 大家可以一目了然剪的究竟是哪里。 @@ -91,11 +89,11 @@ void backtracking(参数) { 在[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中,相当于 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)加了一个元素总和的限制。 树形结构如图: -![216.组合总和III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118201921245.png) +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201118201921245.png) 整体思路还是一样的,本题的剪枝会好想一些,即:**已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉**,如图: -![216.组合总和III1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202038240.png) +![216.组合总和III1](https://file1.kamacoder.com/i/algo/20201118202038240.png) 在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中提到的,对for循环选择的起始范围的剪枝。 @@ -116,7 +114,7 @@ void backtracking(参数) { **注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路**。 树形结构如下: -![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201223170730367.png) +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) 最后还给出了本题的剪枝优化,如下: @@ -127,7 +125,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 优化后树形结构如下: -![39.组合总和1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202115929.png) +![39.组合总和1](https://file1.kamacoder.com/i/algo/20201118202115929.png) #### 组合总和(三) @@ -142,7 +140,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因**。 -![40.组合总和II1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111820220675.png) +![40.组合总和II1](https://file1.kamacoder.com/i/algo/2020111820220675.png) 我在图中将used的变化用橘黄色标注上,**可以看出在candidates[i] == candidates[i - 1]相同的情况下:** @@ -163,7 +161,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 树形结构如下: -![17. 电话号码的字母组合](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202335724.png) +![17. 电话号码的字母组合](https://file1.kamacoder.com/i/algo/20201118202335724.png) 如果大家在现场面试的时候,一定要注意各种输入异常的情况,例如本题输入1 * #按键。 @@ -191,7 +189,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 树形结构如下: -![131.分割回文串](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202448642.png) +![131.分割回文串](https://file1.kamacoder.com/i/algo/20201118202448642.png) ## 子集问题 @@ -202,7 +200,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 如图: -![78.子集](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202544339.png) +![78.子集](https://file1.kamacoder.com/i/algo/20201118202544339.png) 认清这个本质之后,今天的题目就是一道模板题了。 @@ -229,14 +227,14 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 树形结构如下: -![90.子集II](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111217110449.png) +![90.子集II](https://file1.kamacoder.com/i/algo/2020111217110449.png) ### 递增子序列 在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨! 树形结构如下: -![491. 递增子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170832333.png) +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201112170832333.png) 很多同学都会把这道题目和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混在一起。 @@ -245,7 +243,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 我用没有排序的集合{2,1,2,2}来举个例子画一个图,如下: -![90.子集II2](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111316440479.png) +![90.子集II2](https://file1.kamacoder.com/i/algo/2020111316440479.png) **相信这个图胜过千言万语的解释了**。 @@ -261,7 +259,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 如图: -![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170304979.png) +![46.全排列](https://file1.kamacoder.com/i/algo/20201112170304979.png) **大家此时可以感受出排列问题的不同:** @@ -274,7 +272,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 树形结构如下: -![47.全排列II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112171930470.png) +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201112171930470.png) **这道题目神奇的地方就是used[i - 1] == false也可以,used[i - 1] == true也可以!** @@ -282,11 +280,11 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 树层上去重(used[i - 1] == false),的树形结构如下: -![47.全排列II2.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172230434.png) +![47.全排列II2.png](https://file1.kamacoder.com/i/algo/20201112172230434.png) 树枝上去重(used[i - 1] == true)的树型结构如下: -![47.全排列II3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172327967.png) +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201112172327967.png) **可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!** @@ -320,7 +318,7 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111518065555.png) +![](https://file1.kamacoder.com/i/algo/2020111518065555.png) 本题可以算是一道hard的题目了,关于本题的难点我在文中已经详细列出。 @@ -337,7 +335,7 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 下面我用一个3 * 3 的棋盘,将搜索过程抽象为一棵树,如图: -![51.N皇后](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118225433127.png) +![51.N皇后](https://file1.kamacoder.com/i/algo/20201118225433127.png) 从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 @@ -365,7 +363,7 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 因为这个树形结构太大了,我抽取一部分,如图所示: -![37.解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111720451790.png) +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790.png) 解数独可以说是非常难的题目了,如果还一直停留在一维递归的逻辑中,这道题目可以让大家瞬间崩溃。 @@ -440,15 +438,11 @@ N皇后问题分析: 回溯专题汇聚为一张图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211030124742.png) +![](https://file1.kamacoder.com/i/algo/20211030124742.png) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[莫非毛](https://wx.zsxq.com/dweb2/index/footprint/828844212542),所画,总结的非常好,分享给大家。 **回溯算法系列正式结束,新的系列终将开始,录友们准备开启新的征程!** -

- - - diff --git "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" old mode 100644 new mode 100755 index 2be79805af..5e2c9345c4 --- "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" +++ "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 回溯算法去重问题的另一种写法 @@ -17,7 +15,7 @@ 我用没有排序的集合{2,1,2,2}来举例子画一个图,如图: -![90.子集II2](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111316440479-20230310121930316.png) +![90.子集II2](https://file1.kamacoder.com/i/algo/2020111316440479-20230310121930316.png) 图中,大家就很明显的看到,子集重复了。 @@ -97,7 +95,7 @@ private: 如图: -![90.子集II1](https://code-thinking-1253855093.file.myqcloud.com/pics/202011131625054.png) +![90.子集II1](https://file1.kamacoder.com/i/algo/202011131625054.png) 可以看出一旦把unordered_set uset放在类成员位置,它控制的就是整棵树,包括树枝。 @@ -708,8 +706,4 @@ impl Solution { } ``` -

- - - diff --git "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index f22c67b137..c17e0be3f6 --- "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,14 +1,12 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 回溯算法理论基础 ## 题目分类 -回溯算法大纲 +回溯算法大纲 ## 算法公开课 @@ -116,7 +114,7 @@ if (终止条件) { 如图: -![回溯算法理论基础](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130173631174.png) +![回溯算法理论基础](https://file1.kamacoder.com/i/algo/20210130173631174.png) 注意图中,我特意举例集合大小和孩子的数量是相等的! @@ -175,8 +173,4 @@ void backtracking(参数) { -

- - - diff --git "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index df4db78729..460944c5d2 --- "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" +++ "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 字符串:总结篇 @@ -124,7 +122,3 @@ KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易 -

- - - diff --git "a/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 7550ce0254..98ba371fdc --- "a/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 数组总结篇 @@ -16,22 +14,22 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: - + 需要两点注意的是 * **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: - + 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。 @@ -39,7 +37,7 @@ 那么二维数组直接上图,大家应该就知道怎么回事了 - + **那么二维数组在内存的空间地址是连续的么?** @@ -47,7 +45,7 @@ 看了下图,就应该明白了: - + 所以**Java的二维数组在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** @@ -117,10 +115,17 @@ 相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。 +### 前缀和 + +> 代码随想录后续补充题目 + +* [数组:求取区间和](https://programmercarl.com/kamacoder/0058.区间和.html) + +前缀和的思路其实很简单,但非常实用,如果没接触过的录友,也很难想到这个解法维度,所以 这是开阔思路 而难度又不高的好题。 ## 总结 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/数组总结.png) +![](https://file1.kamacoder.com/i/algo/数组总结.png) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。 @@ -128,7 +133,3 @@ 推荐的题目即使大家之前做过了,再读一遍文章,也会帮助你提炼出解题的精髓所在。 -

- - - diff --git "a/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index d104c883f7..49c41f5abb --- "a/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) @@ -16,11 +14,11 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: -![算法通关数组](https://code-thinking.cdn.bcebos.com/pics/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%84.png) +![算法通关数组](https://file1.kamacoder.com/i/algo/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%84.png) @@ -29,11 +27,11 @@ * **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: -![算法通关数组1](https://code-thinking.cdn.bcebos.com/pics/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%841.png) +![算法通关数组1](https://file1.kamacoder.com/i/algo/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%841.png) 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。 @@ -42,8 +40,7 @@ 那么二维数组直接上图,大家应该就知道怎么回事了 -![算法通关数组2](https://code-thinking.cdn.bcebos.com/pics/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%842.png) - +![](https://file1.kamacoder.com/i/algo/20240606105522.png) **那么二维数组在内存的空间地址是连续的么?** @@ -83,7 +80,7 @@ int main() { 如图: -![数组内存](https://code-thinking-1253855093.file.myqcloud.com/pics/20210310150641186.png) +![数组内存](https://file1.kamacoder.com/i/algo/20210310150641186.png) **所以可以看出在C++中二维数组在地址空间上是连续的**。 @@ -114,13 +111,9 @@ public static void test_arr() { 所以Java的二维数组可能是如下排列的方式: -![算法通关数组3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201214111631844.png) +![算法通关数组3](https://file1.kamacoder.com/i/algo/20201214111631844.png) 这里面试中数组相关的理论知识就介绍完了。 -

- - - diff --git "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 06a7827050..2d09daeb94 --- "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" +++ "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 栈与队列总结篇 @@ -107,7 +105,7 @@ cd a/b/c/../../ 设计单调队列的时候,pop,和push操作要保持如下规则: 1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作 -2. push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 +2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。 @@ -142,7 +140,7 @@ cd a/b/c/../../ 本题就要**使用优先级队列来对部分频率进行排序。** 注意这里是对部分数据进行排序而不需要对所有数据排序! -所以排序的过程的时间复杂度是$O(\log k)$,整个算法的时间复杂度是$O(n\log k)$。 +所以排序的过程的时间复杂度是 $O(\log k)$ ,整个算法的时间复杂度是 $O(n\log k)$ 。 ## 总结 @@ -159,8 +157,4 @@ cd a/b/c/../../ 好了,栈与队列我们就总结到这里了,接下来Carl就要带大家开启新的篇章了,大家加油! -

- - - diff --git "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index ad748e489e..a33c2a880f --- "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 来看看栈和队列不为人知的一面 @@ -13,7 +11,7 @@ 如图所示: -![栈与队列理论1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235346563.png) +![栈与队列理论1](https://file1.kamacoder.com/i/algo/20210104235346563.png) 那么我这里再列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 @@ -48,7 +46,7 @@ C++标准库是有多个版本的,要知道我们使用的STL是哪个版本 来说一说栈,栈先进后出,如图所示: -![栈与队列理论2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235434905.png) +![栈与队列理论2](https://file1.kamacoder.com/i/algo/20210104235434905.png) 栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。 @@ -61,11 +59,11 @@ C++标准库是有多个版本的,要知道我们使用的STL是哪个版本 从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。 -![栈与队列理论3](https://code-thinking-1253855093.file.myqcloud.com/pics/20210104235459376.png) +![栈与队列理论3](https://file1.kamacoder.com/i/algo/20210104235459376.png) **我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。** -deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。 +deque是一个双向队列,只要封住一端,只开通另一端就可以实现栈的逻辑了。 **SGI STL中 队列底层实现缺省情况下一样使用deque实现的。** @@ -92,8 +90,4 @@ std::queue> third; // 定义以list为底层容器的队列 -

- - - diff --git "a/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" "b/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" old mode 100644 new mode 100755 index 5f9b46b373..a3566268a1 --- "a/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" +++ "b/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -37,7 +35,7 @@ public: ``` 耗时如下: -![vectorinsert](https://code-thinking-1253855093.file.myqcloud.com/pics/20201218203611181.png) +![vectorinsert](https://file1.kamacoder.com/i/algo/20201218203611181.png) 其直观上来看数组的insert操作是O(n)的,整体代码的时间复杂度是O(n^2)。 @@ -70,7 +68,7 @@ public: 耗时如下: -![使用链表](https://code-thinking-1253855093.file.myqcloud.com/pics/20201218200756257.png) +![使用链表](https://file1.kamacoder.com/i/algo/20201218200756257.png) 大家都知道对于普通数组,一旦定义了大小就不能改变,例如int a[10];,这个数组a至多只能放10个元素,改不了的。 @@ -97,7 +95,7 @@ for (int i = 0; i < vec.size(); i++) { 就是重新申请一个二倍于原数组大小的数组,然后把数据都拷贝过去,并释放原数组内存。(对,就是这么原始粗暴的方法!) 举一个例子,如图: -![vector原理](https://code-thinking-1253855093.file.myqcloud.com/pics/20201218185902217.png) +![vector原理](https://file1.kamacoder.com/i/algo/20201218185902217.png) 原vector中的size和capicity相同都是3,初始化为1 2 3,此时要push_back一个元素4。 @@ -140,7 +138,7 @@ public: 耗时如下: -![vector手动模拟insert](https://code-thinking-1253855093.file.myqcloud.com/pics/20201218200626718.png) +![vector手动模拟insert](https://file1.kamacoder.com/i/algo/20201218200626718.png) 这份代码就是不让vector动态扩容,全程我们自己模拟insert的操作,大家也可以直观的看出是一个O(n^2)的方法了。 @@ -209,7 +207,3 @@ Go中slice的`append`操作和C++中vector的扩容机制基本相同。 -

- - - diff --git "a/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" "b/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" old mode 100644 new mode 100755 index 21b7f93b1c..068806d622 --- "a/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" +++ "b/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 算法模板 ## 算法模板 @@ -853,7 +851,3 @@ Go: -

- - - diff --git "a/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" "b/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 9be93096fd..3f3841e1a2 --- "a/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 听说背包问题很难? 这篇总结篇来拯救你了 @@ -13,7 +11,7 @@ 关于这几种常见的背包,其关系如下: -![416.分割等和子集1](https://code-thinking-1253855093.file.myqcloud.com/pics/20230310000726.png) +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20230310000726.png) 通过这个图,可以很清晰分清这几种常见背包之间的关系。 @@ -95,15 +93,11 @@ 背包问题总结: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/背包问题1.jpeg) +![](https://file1.kamacoder.com/i/algo/背包问题1.jpeg) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画结的非常好,分享给大家。 -

- - - diff --git "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" old mode 100644 new mode 100755 index 413d984c08..d3258c425e --- "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" +++ "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

# 动态规划:01背包理论基础 @@ -17,25 +15,19 @@ ## 思路 +正式开始讲解背包问题! -这周我们正式开始讲解背包问题! - -背包问题的经典资料当然是:背包九讲。在公众号「代码随想录」后台回复:背包九讲,就可以获得背包九讲的pdf。 - -但说实话,背包九讲对于小白来说确实不太友好,看起来还是有点费劲的,而且都是伪代码理解起来也吃力。 - -对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。 +对于面试的话,其实掌握01背包和完全背包,就够用了,最多可以再来一个多重背包。 如果这几种背包,分不清,我这里画了一个图,如下: -![416.分割等和子集1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210117171307407.png) +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407.png) - -至于背包九讲其他背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 +除此以外其他类型的背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 -**所以背包问题的理论基础重中之重是01背包,一定要理解透!** +**所以背包问题的理论基础重中之重是01背包,一定要理解透**! leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。 @@ -47,13 +39,11 @@ leetcode上没有纯01背包的问题,都是01背包应用方面的题目, 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 -![动态规划-背包问题](https://code-thinking-1253855093.file.myqcloud.com/pics/20210117175428387.jpg) - 这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。 这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢? -每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是$o(2^n)$,这里的n表示物品数量。 +每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。 **所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!** @@ -73,54 +63,138 @@ leetcode上没有纯01背包的问题,都是01背包应用方面的题目, 以下讲解和图示中出现的数字都是以这个例子为例。 +(为了方便表述,下面描述 统一用 容量为XX的背包,放下容量(重量)为XX的物品,物品的价值是XX) + ### 二维dp数组01背包 依然动规五部曲分析一波。 -1. 确定dp数组以及下标的含义 +#### 1. 确定dp数组以及下标的含义 + +我们需要使用二维数组,为什么呢? + +因为有两个维度需要分别表示:物品 和 背包容量 + +如图,二维数组为 dp[i][j]。 + +![动态规划-背包问题1](https://file1.kamacoder.com/i/algo/20210110103003361.png) + +那么这里 i 、j、dp[i][j] 分别表示什么呢? + +i 来表示物品、j表示背包容量。 + +(如果想用j 表示物品,i 表示背包容量 行不行? 都可以的,个人习惯而已) + +我们来尝试把上面的 二维表格填写一下。 + +动态规划的思路是根据子问题的求解推导出整体的最优解。 + +我们先看把物品0 放入背包的情况: + +![](https://file1.kamacoder.com/i/algo/20240730113455.png) + +背包容量为0,放不下物品0,此时背包里的价值为0。 + +背包容量为1,可以放下物品0,此时背包里的价值为15. + +背包容量为2,依然可以放下物品0 (注意 01背包里物品只有一个),此时背包里的价值为15。 + +以此类推。 + +再看把物品1 放入背包: -对于背包问题,有一种写法, 是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 +![](https://file1.kamacoder.com/i/algo/20240730114228.png) -只看这个二维数组的定义,大家一定会有点懵,看下面这个图: +背包容量为 0,放不下物品0 或者物品1,此时背包里的价值为0。 -![动态规划-背包问题1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110103003361.png) +背包容量为 1,只能放下物品0,背包里的价值为15。 + +背包容量为 2,只能放下物品0,背包里的价值为15。 + +背包容量为 3,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放物品1 或者 物品0,物品1价值更大,背包里的价值为20。 + +背包容量为 4,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放下物品0 和 物品1,背包价值为35。 + +以上举例,是比较容易看懂,我主要是通过这个例子,来帮助大家明确dp数组的含义。 + +上图中,我们看 dp[1][4] 表示什么意思呢。 + +任取 物品0,物品1 放进容量为4的背包里,最大价值是 dp[1][4]。 + +通过这个举例,我们来进一步明确dp数组的含义。 + +即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 **要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的**,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。 -2. 确定递推公式 +#### 2. 确定递推公式 + +这里在把基本信息给出来: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。 + +这里我们dp[1][4]的状态来举例: + +求取 dp[1][4] 有两种情况: + +1. 放物品1 +2. 还是不放物品1 + +如果不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。 + +推导方向如图: + +![](https://file1.kamacoder.com/i/algo/20240730174246.png) -再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 +如果放物品1, **那么背包要先留出物品1的容量**,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。 -那么可以有两个方向推出来dp[i][j], +容量为1,只考虑放物品0 的最大价值是 dp[0][1],这个值我们之前就计算过。 -* **不放物品i**:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。) -* **放物品i**:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 +所以 放物品1 的情况 = dp[0][1] + 物品1 的价值,推导方向如图: -所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); +![](https://file1.kamacoder.com/i/algo/20240730174436.png) -3. dp数组如何初始化 +两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值) + +`dp[1][4] = max(dp[0][4], dp[0][1] + 物品1 的价值) ` + +以上过程,抽象化如下: + +* **不放物品i**:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。 + +* **放物品i**:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 + +递归公式: `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` + +#### 3. dp数组如何初始化 **关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图: -![动态规划-背包问题2](https://code-thinking-1253855093.file.myqcloud.com/pics/2021011010304192.png) +![动态规划-背包问题2](https://file1.kamacoder.com/i/algo/2021011010304192.png) 在看其他情况。 -状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。 +状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。 dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 -那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 +那么很明显当 `j < weight[0]`的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 -当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。 +当`j >= weight[0]`时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。 代码初始化如下: -``` -for (int j = 0 ; j < weight[0]; j++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 - dp[0][j] = 0; +```CPP +for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 + dp[i][0] = 0; } // 正序遍历 for (int j = weight[0]; j <= bagweight; j++) { @@ -131,7 +205,7 @@ for (int j = weight[0]; j <= bagweight; j++) { 此时dp数组初始化情况如图所示: -![动态规划-背包问题7](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110103109140.png) +![动态规划-背包问题7](https://file1.kamacoder.com/i/algo/20210110103109140.png) dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? @@ -143,11 +217,11 @@ dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化 如图: -![动态规划-背包问题10](https://code-thinking-1253855093.file.myqcloud.com/pics/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%9810.jpg) +![动态规划-背包问题10](https://file1.kamacoder.com/i/algo/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%9810.jpg) 最后初始化代码如下: -``` +```CPP // 初始化 dp vector> dp(weight.size(), vector(bagweight + 1, 0)); for (int j = weight[0]; j <= bagweight; j++) { @@ -158,12 +232,11 @@ for (int j = weight[0]; j <= bagweight; j++) { **费了这么大的功夫,才把如何初始化讲清楚,相信不少同学平时初始化dp数组是凭感觉来的,但有时候感觉是不靠谱的**。 -4. 确定遍历顺序 - +#### 4. 确定遍历顺序 在如下图中,可以看出,有两个遍历的维度:物品与背包重量 -![动态规划-背包问题3](https://code-thinking-1253855093.file.myqcloud.com/pics/2021011010314055.png) +![动态规划-背包问题3](https://file1.kamacoder.com/i/algo/2021011010314055.png) 那么问题来了,**先遍历 物品还是先遍历背包重量呢?** @@ -171,7 +244,7 @@ for (int j = weight[0]; j <= bagweight; j++) { 那么我先给出先遍历物品,然后遍历背包重量的代码。 -``` +```CPP // weight数组的大小 就是物品个数 for(int i = 1; i < weight.size(); i++) { // 遍历物品 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 @@ -186,7 +259,7 @@ for(int i = 1; i < weight.size(); i++) { // 遍历物品 例如这样: -``` +```CPP // weight数组的大小 就是物品个数 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 for(int i = 1; i < weight.size(); i++) { // 遍历物品 @@ -200,15 +273,15 @@ for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 **要理解递归的本质和递推的方向**。 -dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。 +`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。 dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示: -![动态规划-背包问题5](https://code-thinking-1253855093.file.myqcloud.com/pics/202101101032124.png) +![动态规划-背包问题5](https://file1.kamacoder.com/i/algo/202101101032124.png) 再来看看先遍历背包,再遍历物品呢,如图: -![动态规划-背包问题6](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110103244701.png) +![动态规划-背包问题6](https://file1.kamacoder.com/i/algo/20210110103244701.png) **大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!** @@ -216,11 +289,11 @@ dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括 **其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了**。 -5. 举例推导dp数组 +#### 5. 举例推导dp数组 来看一下对应的dp数组的数值,如图: -![动态规划-背包问题4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210118163425129.jpg) +![动态规划-背包问题4](https://file1.kamacoder.com/i/algo/20210118163425129.jpg) 最终结果就是dp[2][4]。 @@ -232,50 +305,20 @@ dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括 主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。 -```cpp -void test_2_wei_bag_problem1() { - vector weight = {1, 3, 4}; - vector value = {15, 20, 30}; - int bagweight = 4; - - // 二维数组 - vector> dp(weight.size(), vector(bagweight + 1, 0)); - - // 初始化 - for (int j = weight[0]; j <= bagweight; j++) { - dp[0][j] = value[0]; - } - - // weight数组的大小 就是物品个数 - for(int i = 1; i < weight.size(); i++) { // 遍历物品 - for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 - if (j < weight[i]) dp[i][j] = dp[i - 1][j]; - else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); - - } - } - - cout << dp[weight.size() - 1][bagweight] << endl; -} - -int main() { - test_2_wei_bag_problem1(); -} - -``` - 本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的,代码如下: ```CPP - -//二维dp数组实现 #include using namespace std; -int n, bagweight;// bagweight代表行李箱空间 -void solve() { +int main() { + int n, bagweight;// bagweight代表行李箱空间 + + cin >> n >> bagweight; + vector weight(n, 0); // 存储每件物品所占空间 vector value(n, 0); // 存储每件物品价值 + for(int i = 0; i < n; ++i) { cin >> weight[i]; } @@ -294,34 +337,28 @@ void solve() { for(int i = 1; i < weight.size(); i++) { // 遍历科研物品 for(int j = 0; j <= bagweight; j++) { // 遍历行李箱容量 - // 如果装不下这个物品,那么就继承dp[i - 1][j]的值 - if (j < weight[i]) dp[i][j] = dp[i - 1][j]; - // 如果能装下,就将值更新为 不装这个物品的最大值 和 装这个物品的最大值 中的 最大值 - // 装这个物品的最大值由容量为j - weight[i]的包任意放入序号为[0, i - 1]的最大值 + 该物品的价值构成 - else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 如果装不下这个物品,那么就继承dp[i - 1][j]的值 + else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } } } - cout << dp[weight.size() - 1][bagweight] << endl; -} + cout << dp[n - 1][bagweight] << endl; -int main() { - while(cin >> n >> bagweight) { - solve(); - } return 0; } + ``` ## 总结 -讲了这么多才刚刚把二维dp的01背包讲完,**这里大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在如何初始化和遍历顺序上**。 +背包问题 是动态规划里的经典类型题目,大家要细细品味。 可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。 -下一篇 还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异,敬请期待! - +下一篇 还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异。 @@ -329,120 +366,42 @@ int main() { ### Java -```java -public class BagProblem { +```Java +import java.util.Scanner; + +public class Main { public static void main(String[] args) { - int[] weight = {1,3,4}; - int[] value = {15,20,30}; - int bagSize = 4; - testWeightBagProblem(weight,value,bagSize); - } + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int bagweight = scanner.nextInt(); - /** - * 动态规划获得结果 - * @param weight 物品的重量 - * @param value 物品的价值 - * @param bagSize 背包的容量 - */ - public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){ - - // 创建dp数组 - int goods = weight.length; // 获取物品的数量 - int[][] dp = new int[goods][bagSize + 1]; - - // 初始化dp数组 - // 创建数组后,其中默认的值就是0 - for (int j = weight[0]; j <= bagSize; j++) { - dp[0][j] = value[0]; - } + int[] weight = new int[n]; + int[] value = new int[n]; - // 填充dp数组 - for (int i = 1; i < weight.length; i++) { - for (int j = 1; j <= bagSize; j++) { - if (j < weight[i]) { - /** - * 当前背包的容量都没有当前物品i大的时候,是不放物品i的 - * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值 - */ - dp[i][j] = dp[i-1][j]; - } else { - /** - * 当前背包的容量可以放下物品i - * 那么此时分两种情况: - * 1、不放物品i - * 2、放物品i - * 比较这两种情况下,哪种背包中物品的最大价值最大 - */ - dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]); - } - } + for (int i = 0; i < n; ++i) { + weight[i] = scanner.nextInt(); } - - // 打印dp数组 - for (int i = 0; i < goods; i++) { - for (int j = 0; j <= bagSize; j++) { - System.out.print(dp[i][j] + "\t"); - } - System.out.println("\n"); + for (int j = 0; j < n; ++j) { + value[j] = scanner.nextInt(); } - } -} - -``` -```java -import java.util.Arrays; + int[][] dp = new int[n][bagweight + 1]; -public class BagProblem { - public static void main(String[] args) { - int[] weight = {1,3,4}; - int[] value = {15,20,30}; - int bagSize = 4; - testWeightBagProblem(weight,value,bagSize); - } + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } - /** - * 初始化 dp 数组做了简化(给物品增加冗余维)。这样初始化dp数组,默认全为0即可。 - * dp[i][j] 表示从下标为[0 - i-1]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 - * 其实是模仿背包重量从 0 开始,背包容量 j 为 0 的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为 0。 - * 可选物品也可以从无开始,也就是没有物品可选,即dp[0][j],这样无论背包容量为多少,背包价值总和一定为 0。 - * @param weight 物品的重量 - * @param value 物品的价值 - * @param bagSize 背包的容量 - */ - public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){ - - // 创建dp数组 - int goods = weight.length; // 获取物品的数量 - int[][] dp = new int[goods + 1][bagSize + 1]; // 给物品增加冗余维,i = 0 表示没有物品可选 - - // 初始化dp数组,默认全为0即可 - // 填充dp数组 - for (int i = 1; i <= goods; i++) { - for (int j = 1; j <= bagSize; j++) { - if (j < weight[i - 1]) { // i - 1 对应物品 i - /** - * 当前背包的容量都没有当前物品i大的时候,是不放物品i的 - * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值 - */ + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { dp[i][j] = dp[i - 1][j]; } else { - /** - * 当前背包的容量可以放下物品i - * 那么此时分两种情况: - * 1、不放物品i - * 2、放物品i - * 比较这两种情况下,哪种背包中物品的最大价值最大 - */ - dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]); // i - 1 对应物品 i + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } } - // 打印dp数组 - for(int[] arr : dp){ - System.out.println(Arrays.toString(arr)); - } + System.out.println(dp[n - 1][bagweight]); } } @@ -450,137 +409,123 @@ public class BagProblem { ### Python -无参数版 ```python -def test_2_wei_bag_problem1(): - weight = [1, 3, 4] - value = [15, 20, 30] - bagweight = 4 +n, bagweight = map(int, input().split()) - # 二维数组 - dp = [[0] * (bagweight + 1) for _ in range(len(weight))] +weight = list(map(int, input().split())) +value = list(map(int, input().split())) - # 初始化 - for j in range(weight[0], bagweight + 1): - dp[0][j] = value[0] +dp = [[0] * (bagweight + 1) for _ in range(n)] - # weight数组的大小就是物品个数 - for i in range(1, len(weight)): # 遍历物品 - for j in range(bagweight + 1): # 遍历背包容量 - if j < weight[i]: - dp[i][j] = dp[i - 1][j] - else: - dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) +for j in range(weight[0], bagweight + 1): + dp[0][j] = value[0] - print(dp[len(weight) - 1][bagweight]) +for i in range(1, n): + for j in range(bagweight + 1): + if j < weight[i]: + dp[i][j] = dp[i - 1][j] + else: + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) -test_2_wei_bag_problem1() +print(dp[n - 1][bagweight]) ``` -有参数版 -```python -def test_2_wei_bag_problem1(weight, value, bagweight): - # 二维数组 - dp = [[0] * (bagweight + 1) for _ in range(len(weight))] - # 初始化 - for j in range(weight[0], bagweight + 1): - dp[0][j] = value[0] - - # weight数组的大小就是物品个数 - for i in range(1, len(weight)): # 遍历物品 - for j in range(bagweight + 1): # 遍历背包容量 - if j < weight[i]: - dp[i][j] = dp[i - 1][j] - else: - dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) +### Go - return dp[len(weight) - 1][bagweight] +```go +package main -if __name__ == "__main__": +import ( + "fmt" +) - weight = [1, 3, 4] - value = [15, 20, 30] - bagweight = 4 +func main() { + var n, bagweight int // bagweight代表行李箱空间 + fmt.Scan(&n, &bagweight) - result = test_2_wei_bag_problem1(weight, value, bagweight) - print(result) + weight := make([]int, n) // 存储每件物品所占空间 + value := make([]int, n) // 存储每件物品价值 + for i := 0; i < n; i++ { + fmt.Scan(&weight[i]) + } + for j := 0; j < n; j++ { + fmt.Scan(&value[j]) + } + // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值 + dp := make([][]int, n) + for i := range dp { + dp[i] = make([]int, bagweight + 1) + } -``` + // 初始化, 因为需要用到dp[i - 1]的值 + // j < weight[0]已在上方被初始化为0 + // j >= weight[0]的值就初始化为value[0] + for j := weight[0]; j <= bagweight; j++ { + dp[0][j] = value[0] + } -### Go + for i := 1; i < n; i++ { // 遍历科研物品 + for j := 0; j <= bagweight; j++ { // 遍历行李箱容量 + if j < weight[i] { + dp[i][j] = dp[i-1][j] // 如果装不下这个物品,那么就继承dp[i - 1][j]的值 + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]) + } + } + } -```go -func test_2_wei_bag_problem1(weight, value []int, bagweight int) int { - // 定义dp数组 - dp := make([][]int, len(weight)) - for i, _ := range dp { - dp[i] = make([]int, bagweight+1) - } - // 初始化 - for j := bagweight; j >= weight[0]; j-- { - dp[0][j] = dp[0][j-weight[0]] + value[0] - } - // 递推公式 - for i := 1; i < len(weight); i++ { - //正序,也可以倒序 - for j := 0; j <= bagweight; j++ { - if j < weight[i] { - dp[i][j] = dp[i-1][j] - } else { - dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]) - } - } - } - return dp[len(weight)-1][bagweight] + fmt.Println(dp[n-1][bagweight]) } -func max(a,b int) int { - if a > b { - return a - } - return b +func max(x, y int) int { + if x > y { + return x + } + return y } -func main() { - weight := []int{1,3,4} - value := []int{15,20,30} - test_2_wei_bag_problem1(weight,value,4) -} ``` -### Javascript +### JavaScript ```js -function testWeightBagProblem (weight, value, size) { - // 定义 dp 数组 - const len = weight.length, - dp = Array(len).fill().map(() => Array(size + 1).fill(0)); +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +let input = []; + +readline.on('line', (line) => { + input.push(line); +}); - // 初始化 - for(let j = weight[0]; j <= size; j++) { +readline.on('close', () => { + let [n, bagweight] = input[0].split(' ').map(Number); + let weight = input[1].split(' ').map(Number); + let value = input[2].split(' ').map(Number); + + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); + + for (let j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; } - // weight 数组的长度len 就是物品个数 - for(let i = 1; i < len; i++) { // 遍历物品 - for(let j = 0; j <= size; j++) { // 遍历背包容量 - if(j < weight[i]) dp[i][j] = dp[i - 1][j]; - else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } } } - console.table(dp) + console.log(dp[n - 1][bagweight]); +}); - return dp[len - 1][size]; -} - -function test () { - console.log(testWeightBagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6)); -} - -test(); ``` @@ -589,159 +534,60 @@ test(); ```c #include #include -#include - -#define MAX(a, b) (((a) > (b)) ? (a) : (b)) -#define ARR_SIZE(a) (sizeof((a)) / sizeof((a)[0])) -#define BAG_WEIGHT 4 - -void backPack(int* weights, int weightSize, int* costs, int costSize, int bagWeight) { - // 开辟dp数组 - int dp[weightSize][bagWeight + 1]; - memset(dp, 0, sizeof(int) * weightSize * (bagWeight + 1)); - - int i, j; - // 当背包容量大于物品0的重量时,将物品0放入到背包中 - for(j = weights[0]; j <= bagWeight; ++j) { - dp[0][j] = costs[0]; - } - // 先遍历物品,再遍历重量 - for(j = 1; j <= bagWeight; ++j) { - for(i = 1; i < weightSize; ++i) { - // 如果当前背包容量小于物品重量 - if(j < weights[i]) - // 背包物品的价值等于背包不放置当前物品时的价值 - dp[i][j] = dp[i-1][j]; - // 若背包当前重量可以放置物品 - else - // 背包的价值等于放置该物品或不放置该物品的最大值 - dp[i][j] = MAX(dp[i - 1][j], dp[i - 1][j - weights[i]] + costs[i]); - } - } - - printf("%d\n", dp[weightSize - 1][bagWeight]); +int max(int a, int b) { + return a > b ? a : b; } -int main(int argc, char* argv[]) { - int weights[] = {1, 3, 4}; - int costs[] = {15, 20, 30}; - backPack(weights, ARR_SIZE(weights), costs, ARR_SIZE(costs), BAG_WEIGHT); - return 0; -} -``` +int main() { + int n, bagweight; + scanf("%d %d", &n, &bagweight); + int *weight = (int *)malloc(n * sizeof(int)); + int *value = (int *)malloc(n * sizeof(int)); -### TypeScript - -```typescript -function testWeightBagProblem( - weight: number[], - value: number[], - size: number -): number { - /** - * dp[i][j]: 前i个物品,背包容量为j,能获得的最大价值 - * dp[0][*]: u=weight[0],u之前为0,u之后(含u)为value[0] - * dp[*][0]: 0 - * ... - * dp[i][j]: max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]); - */ - const goodsNum: number = weight.length; - const dp: number[][] = new Array(goodsNum) - .fill(0) - .map((_) => new Array(size + 1).fill(0)); - for (let i = weight[0]; i <= size; i++) { - dp[0][i] = value[0]; - } - for (let i = 1; i < goodsNum; i++) { - for (let j = 1; j <= size; j++) { - if (j < weight[i]) { - dp[i][j] = dp[i - 1][j]; - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); - } + for (int i = 0; i < n; ++i) { + scanf("%d", &weight[i]); + } + for (int j = 0; j < n; ++j) { + scanf("%d", &value[j]); } - } - return dp[goodsNum - 1][size]; -} -// test -const weight = [1, 3, 4]; -const value = [15, 20, 30]; -const size = 4; -console.log(testWeightBagProblem(weight, value, size)); -``` - -### Scala - -```scala -object Solution { - // 01背包 - def test_2_wei_bag_problem1(): Unit = { - var weight = Array[Int](1, 3, 4) - var value = Array[Int](15, 20, 30) - var baseweight = 4 - // 二维数组 - var dp = Array.ofDim[Int](weight.length, baseweight + 1) + int **dp = (int **)malloc(n * sizeof(int *)); + for (int i = 0; i < n; ++i) { + dp[i] = (int *)malloc((bagweight + 1) * sizeof(int)); + for (int j = 0; j <= bagweight; ++j) { + dp[i][j] = 0; + } + } - // 初始化 - for (j <- weight(0) to baseweight) { - dp(0)(j) = value(0) + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; } - // 遍历 - for (i <- 1 until weight.length; j <- 1 to baseweight) { - if (j - weight(i) >= 0) dp(i)(j) = dp(i - 1)(j - weight(i)) + value(i) - dp(i)(j) = math.max(dp(i)(j), dp(i - 1)(j)) + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } } - // 打印数组 - dp.foreach(x => println("[" + x.mkString(",") + "]")) + printf("%d\n", dp[n - 1][bagweight]); - dp(weight.length - 1)(baseweight) // 最终返回 - } + for (int i = 0; i < n; ++i) { + free(dp[i]); + } + free(dp); + free(weight); + free(value); - def main(args: Array[String]): Unit = { - test_2_wei_bag_problem1() - } + return 0; } -``` - -### Rust -```rust -pub struct Solution; +``` -impl Solution { - pub fn wei_bag_problem1(weight: Vec, value: Vec, bag_size: usize) -> usize { - let mut dp = vec![vec![0; bag_size + 1]; weight.len()]; - for j in weight[0]..=weight.len() { - dp[0][j] = value[0]; - } - for i in 1..weight.len() { - for j in 0..=bag_size { - match j < weight[i] { - true => dp[i][j] = dp[i - 1][j], - false => dp[i][j] = dp[i - 1][j].max(dp[i - 1][j - weight[i]] + value[i]), - } - } - } - dp[weight.len() - 1][bag_size] - } -} - -#[test] -fn test_wei_bag_problem1() { - println!( - "{}", - Solution::wei_bag_problem1(vec![1, 3, 4], vec![15, 20, 30], 4) - ); -} -``` -

- - - diff --git "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" old mode 100644 new mode 100755 index 5630bc99d1..00dc593417 --- "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" +++ "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 动态规划:01背包理论基础(滚动数组) @@ -56,17 +54,31 @@ 1. 确定dp数组的定义 +关于dp数组的定义,我在 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html) 有详细讲解 + 在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 2. 一维dp数组的递推公式 -dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢? +二维dp数组的递推公式为: `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` + +公式是怎么来的 在这里 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html) 有详细讲解。 + +一维dp数组,其实就上上一层 dp[i-1] 这一层 拷贝的 dp[i]来。 + +所以在 上面递推公式的基础上,去掉i这个维度就好。 + +递推公式为:`dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);` + +以下为分析: + +dp[j]为 容量为j的背包所背的最大价值。 dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。 -dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j]) +`dp[j - weight[i]] + value[i]` 表示 容量为 [j - 物品i重量] 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j]) -此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值, +此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取`dp[j - weight[i]] + value[i]`,即放物品i,指定是取最大的,毕竟是求最大价值, 所以递归公式为: @@ -145,44 +157,15 @@ dp[1] = dp[1 - weight[0]] + value[0] = 15 因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。 -倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。 - -(这里如果读不懂,就再回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!) - **所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!**,这一点大家一定要注意。 5. 举例推导dp数组 一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: -![动态规划-背包问题9](https://code-thinking-1253855093.file.myqcloud.com/pics/20210110103614769.png) - +![动态规划-背包问题9](https://file1.kamacoder.com/i/algo/20210110103614769.png) -C++代码如下: - -```CPP -void test_1_wei_bag_problem() { - vector weight = {1, 3, 4}; - vector value = {15, 20, 30}; - int bagWeight = 4; - - // 初始化 - vector dp(bagWeight + 1, 0); - for(int i = 0; i < weight.size(); i++) { // 遍历物品 - for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } - } - cout << dp[bagWeight] << endl; -} - -int main() { - test_1_wei_bag_problem(); -} - -``` - 本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的,代码如下: ```CPP @@ -256,255 +239,223 @@ int main() { 即使代码没有通过,也会有自己的逻辑去debug,这样就思维清晰了。 -接下来就要开始用这两天的理论基础去做力扣上的背包面试题目了,录友们握紧扶手,我们要上高速啦! - - - ## 其他语言版本 ### Java ```java +import java.util.Scanner; + +public class Main { public static void main(String[] args) { - int[] weight = {1, 3, 4}; - int[] value = {15, 20, 30}; - int bagWight = 4; - testWeightBagProblem(weight, value, bagWight); - } + Scanner scanner = new Scanner(System.in); - public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){ - int wLen = weight.length; - //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值 - int[] dp = new int[bagWeight + 1]; - //遍历顺序:先遍历物品,再遍历背包容量 - for (int i = 0; i < wLen; i++){ - for (int j = bagWeight; j >= weight[i]; j--){ - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); - } + // 读取 M 和 N + int M = scanner.nextInt(); // 研究材料的数量 + int N = scanner.nextInt(); // 行李空间的大小 + + int[] costs = new int[M]; // 每种材料的空间占用 + int[] values = new int[M]; // 每种材料的价值 + + // 输入每种材料的空间占用 + for (int i = 0; i < M; i++) { + costs[i] = scanner.nextInt(); + } + + // 输入每种材料的价值 + for (int j = 0; j < M; j++) { + values[j] = scanner.nextInt(); } - //打印dp数组 - for (int j = 0; j <= bagWeight; j++){ - System.out.print(dp[j] + " "); + + // 创建一个动态规划数组 dp,初始值为 0 + int[] dp = new int[N + 1]; + + // 外层循环遍历每个类型的研究材料 + for (int i = 0; i < M; i++) { + // 内层循环从 N 空间逐渐减少到当前研究材料所占空间 + for (int j = N; j >= costs[i]; j--) { + // 考虑当前研究材料选择和不选择的情况,选择最大值 + dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]); + } } + + // 输出 dp[N],即在给定 N 行李空间可以携带的研究材料的最大价值 + System.out.println(dp[N]); + + scanner.close(); } +} + ``` ### Python -无参版 + ```python -def test_1_wei_bag_problem(): - weight = [1, 3, 4] - value = [15, 20, 30] - bagWeight = 4 +n, bagweight = map(int, input().split()) +weight = list(map(int, input().split())) +value = list(map(int, input().split())) + +dp = [0] * (bagweight + 1) # 创建一个动态规划数组dp,初始值为0 - # 初始化 - dp = [0] * (bagWeight + 1) - for i in range(len(weight)): # 遍历物品 - for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) +dp[0] = 0 # 初始化dp[0] = 0,背包容量为0,价值最大为0 - print(dp[bagWeight]) +for i in range(n): # 应该先遍历物品,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品 + for j in range(bagweight, weight[i]-1, -1): # 倒序遍历背包容量是为了保证物品i只被放入一次 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) +print(dp[bagweight]) -test_1_wei_bag_problem() ``` -有参版 -```python -def test_1_wei_bag_problem(weight, value, bagWeight): - # 初始化 - dp = [0] * (bagWeight + 1) - for i in range(len(weight)): # 遍历物品 - for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) +### Go +```go +package main - return dp[bagWeight] +import ( + "fmt" +) +func main() { + // 读取 M 和 N + var M, N int + fmt.Scan(&M, &N) -if __name__ == "__main__": + costs := make([]int, M) + values := make([]int, M) - weight = [1, 3, 4] - value = [15, 20, 30] - bagweight = 4 + for i := 0; i < M; i++ { + fmt.Scan(&costs[i]) + } + for j := 0; j < M; j++ { + fmt.Scan(&values[j]) + } - result = test_1_wei_bag_problem(weight, value, bagweight) - print(result) + // 创建一个动态规划数组dp,初始值为0 + dp := make([]int, N + 1) -``` -### Go -```go -func test_1_wei_bag_problem(weight, value []int, bagWeight int) int { - // 定义 and 初始化 - dp := make([]int,bagWeight+1) - // 递推顺序 - for i := 0 ;i < len(weight) ; i++ { - // 这里必须倒序,区别二维,因为二维dp保存了i的状态 - for j:= bagWeight; j >= weight[i] ; j-- { - // 递推公式 - dp[j] = max(dp[j], dp[j-weight[i]]+value[i]) - } - } - //fmt.Println(dp) - return dp[bagWeight] -} + // 外层循环遍历每个类型的研究材料 + for i := 0; i < M; i++ { + // 内层循环从 N 空间逐渐减少到当前研究材料所占空间 + for j := N; j >= costs[i]; j-- { + // 考虑当前研究材料选择和不选择的情况,选择最大值 + dp[j] = max(dp[j], dp[j-costs[i]] + values[i]) + } + } -func max(a,b int) int { - if a > b { - return a - } - return b + // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值 + fmt.Println(dp[N]) } - -func main() { - weight := []int{1,3,4} - value := []int{15,20,30} - test_1_wei_bag_problem(weight,value,4) +func max(x, y int) int { + if x > y { + return x + } + return y } + ``` ### JavaScript ```js +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); -function testWeightBagProblem(wight, value, size) { - const len = wight.length, - dp = Array(size + 1).fill(0); - for(let i = 1; i <= len; i++) { - for(let j = size; j >= wight[i - 1]; j--) { - dp[j] = Math.max(dp[j], value[i - 1] + dp[j - wight[i - 1]]); - } - } - return dp[size]; -} +let input = []; +readline.on('line', (line) => { + input.push(line); +}); -function test () { - console.log(testWeightBagProblem([1, 3, 4, 5], [15, 20, 30, 55], 6)); -} +readline.on('close', () => { + let [n, bagweight] = input[0].split(' ').map(Number); + let weight = input[1].split(' ').map(Number); + let value = input[2].split(' ').map(Number); -test(); -``` + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); -### C -```c -#include -#include - -#define MAX(a, b) (((a) > (b)) ? (a) : (b)) -#define ARR_SIZE(arr) ((sizeof((arr))) / sizeof((arr)[0])) -#define BAG_WEIGHT 4 - -void test_back_pack(int* weights, int weightSize, int* values, int valueSize, int bagWeight) { - int dp[bagWeight + 1]; - memset(dp, 0, sizeof(int) * (bagWeight + 1)); - - int i, j; - // 先遍历物品 - for(i = 0; i < weightSize; ++i) { - // 后遍历重量。从后向前遍历 - for(j = bagWeight; j >= weights[i]; --j) { - dp[j] = MAX(dp[j], dp[j - weights[i]] + values[i]); - } + for (let j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; } - // 打印最优结果 - printf("%d\n", dp[bagWeight]); -} + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } -int main(int argc, char** argv) { - int weights[] = {1, 3, 4}; - int values[] = {15, 20, 30}; - test_back_pack(weights, ARR_SIZE(weights), values, ARR_SIZE(values), BAG_WEIGHT); - return 0; -} -``` + console.log(dp[n - 1][bagweight]); +}); -### TypeScript - -```typescript -function testWeightBagProblem( - weight: number[], - value: number[], - size: number -): number { - const goodsNum: number = weight.length; - const dp: number[] = new Array(size + 1).fill(0); - for (let i = 0; i < goodsNum; i++) { - for (let j = size; j >= weight[i]; j--) { - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); - } - } - return dp[size]; -} -const weight = [1, 3, 4]; -const value = [15, 20, 30]; -const size = 4; -console.log(testWeightBagProblem(weight, value, size)); ``` -### Scala - -```scala -object Solution { - // 滚动数组 - def test_1_wei_bag_problem(): Unit = { - var weight = Array[Int](1, 3, 4) - var value = Array[Int](15, 20, 30) - var baseweight = 4 +### C +```c +#include +#include - // dp数组 - var dp = new Array[Int](baseweight + 1) +int max(int a, int b) { + return a > b ? a : b; +} - // 遍历 - for (i <- 0 until weight.length; j <- baseweight to weight(i) by -1) { - dp(j) = math.max(dp(j), dp(j - weight(i)) + value(i)) - } +int main() { + int n, bagweight; + scanf("%d %d", &n, &bagweight); - // 打印数组 - println("[" + dp.mkString(",") + "]") - } + int *weight = (int *)malloc(n * sizeof(int)); + int *value = (int *)malloc(n * sizeof(int)); - def main(args: Array[String]): Unit = { - test_1_wei_bag_problem() - } -} -``` + for (int i = 0; i < n; ++i) { + scanf("%d", &weight[i]); + } + for (int j = 0; j < n; ++j) { + scanf("%d", &value[j]); + } -### Rust + int **dp = (int **)malloc(n * sizeof(int *)); + for (int i = 0; i < n; ++i) { + dp[i] = (int *)malloc((bagweight + 1) * sizeof(int)); + for (int j = 0; j <= bagweight; ++j) { + dp[i][j] = 0; + } + } -```rust -pub struct Solution; + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } -impl Solution { - pub fn wei_bag_problem2(weight: Vec, value: Vec, bag_size: usize) -> usize { - let mut dp = vec![0; bag_size + 1]; - for i in 0..weight.len() { - for j in (weight[i]..=bag_size).rev() { - if j >= weight[i] { - dp[j] = dp[j].max(dp[j - weight[i]] + value[i]); - } + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } - dp[dp.len() - 1] } -} -#[test] -fn test_wei_bag_problem2() { - println!( - "{}", - Solution::wei_bag_problem2(vec![1, 3, 4], vec![15, 20, 30], 4) - ); + printf("%d\n", dp[n - 1][bagweight]); + + for (int i = 0; i < n; ++i) { + free(dp[i]); + } + free(dp); + free(weight); + free(value); + + return 0; } + ``` -

- - - diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" new file mode 100644 index 0000000000..7dd78302ee --- /dev/null +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" @@ -0,0 +1,214 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 完全背包-一维数组 + +本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透完全背包问题! ](https://www.bilibili.com/video/BV1uK411o7c9/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本篇我们不再做五部曲分析,核心内容 在 01背包二维 、01背包一维 和 完全背包二维 的讲解中都讲过了。 + +上一篇我们刚刚讲了完全背包二维DP数组的写法: + +```CPP +for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } +} +``` + +压缩成一维DP数组,也就是将上一层拷贝到当前层。 + +将上一层dp[i-1] 的那一层拷贝到 当前层 dp[i] ,那么 递推公式由:`dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])` 变成: `dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])` + +这里有录友想,这样拷贝的话, dp[i - 1][j] 的数值会不会 覆盖了 dp[i][j] 的数值呢? + +并不会,因为 当前层 dp[i][j] 是空的,是没有计算过的。 + +变成 `dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])` 我们压缩成一维dp数组,去掉 i 层数维度。 + +即:`dp[j] = max(dp[j], dp[j - weight[i]] + value[i])` + + +接下来我们重点讲一下遍历顺序。 + +看过这两篇的话: + +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +就知道了,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。 + +**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的**! + +因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 + +遍历物品在外层循环,遍历背包容量在内层循环,状态如图: + +![动态规划-完全背包1](https://file1.kamacoder.com/i/algo/20210126104529605.jpg) + +遍历背包容量在外层循环,遍历物品在内层循环,状态如图: + +![动态规划-完全背包2](https://file1.kamacoder.com/i/algo/20210729234011.png) + +看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。 + +先遍历背包再遍历物品,代码如下: + +```CPP +for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for(int i = 0; i < weight.size(); i++) { // 遍历物品 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + cout << endl; +} +``` + +先遍历物品再遍历背包: + +```CPP +for(int i = 0; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } +} +``` + +整体代码如下: + +```cpp +#include +#include +using namespace std; + +int main() { + int N, bagWeight; + cin >> N >> bagWeight; + vector weight(N, 0); + vector value(N, 0); + for (int i = 0; i < N; i++) { + int w; + int v; + cin >> w >> v; + weight[i] = w; + value[i] = v; + } + + vector dp(bagWeight + 1, 0); + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for(int i = 0; i < weight.size(); i++) { // 遍历物品 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + } + cout << dp[bagWeight] << endl; + + return 0; +} +``` + + + +## 总结 + +细心的同学可能发现,**全文我说的都是对于纯完全背包问题,其for循环的先后循环是可以颠倒的!** + +但如果题目稍稍有点变化,就会体现在遍历顺序上。 + +如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了,而leetcode上的题目都是这种稍有变化的类型。 + +这个区别,我将在后面讲解具体leetcode题目中给大家介绍,因为这块如果不结合具题目,单纯的介绍原理估计很多同学会越看越懵! + +别急,下一篇就是了! + +最后,**又可以出一道面试题了,就是纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后再问,两个for循环的先后是否可以颠倒?为什么?** + +这个简单的完全背包问题,估计就可以难住不少候选人了。 + + +## 其他语言版本 + +### Java: + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int N = scanner.nextInt(); + int bagWeight = scanner.nextInt(); + + int[] weight = new int[N]; + int[] value = new int[N]; + for (int i = 0; i < N; i++) { + weight[i] = scanner.nextInt(); + value[i] = scanner.nextInt(); + } + + int[] dp = new int[bagWeight + 1]; + + for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for (int i = 0; i < weight.length; i++) { // 遍历物品 + if (j >= weight[i]) { + dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); + } + } + } + + System.out.println(dp[bagWeight]); + scanner.close(); + } +} + +``` + + + +### Python: + +```python +def complete_knapsack(N, bag_weight, weight, value): + dp = [0] * (bag_weight + 1) + + for j in range(bag_weight + 1): # 遍历背包容量 + for i in range(len(weight)): # 遍历物品 + if j >= weight[i]: + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) + + return dp[bag_weight] + +# 输入 +N, bag_weight = map(int, input().split()) +weight = [] +value = [] +for _ in range(N): + w, v = map(int, input().split()) + weight.append(w) + value.append(v) + +# 输出结果 +print(complete_knapsack(N, bag_weight, weight, value)) + + +``` + + +### Go: + +```go + +``` +### Javascript: + +```Javascript +``` + diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" old mode 100644 new mode 100755 index 5d6440e3bf..39e7ebe378 --- "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 动态规划:关于多重背包,你该了解这些! @@ -204,6 +202,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 +234,4 @@ class multi_pack{ -

- - - diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" old mode 100644 new mode 100755 index efc56c50ac..02b3cdc32d --- "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" @@ -1,22 +1,13 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -# 动态规划:完全背包理论基础 +# 完全背包理论基础-二维DP数组 本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习,题意是一样的。 -## 算法公开课 - -**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透完全背包问题! ](https://www.bilibili.com/video/BV1uK411o7c9/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 - -## 思路 - -### 完全背包 - +## 完全背包 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。 @@ -24,14 +15,12 @@ 同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。 -在下面的讲解中,我依然举这个例子: +在下面的讲解中,我拿下面数据举例子: -背包最大重量为4。 - -物品为: +背包最大重量为4,物品为: | | 重量 | 价值 | -| --- | --- | --- | +| ----- | ---- | ---- | | 物品0 | 1 | 15 | | 物品1 | 3 | 20 | | 物品2 | 4 | 30 | @@ -40,473 +29,334 @@ 问背包能背的物品最大价值是多少? -01背包和完全背包唯一不同就是体现在遍历顺序上,所以本文就不去做动规五部曲了,我们直接针对遍历顺序经行分析! +**如果没看到之前的01背包讲解,已经要先仔细看如下两篇,01背包是基础,本篇在讲解完全背包,之前的背包基础我将不会重复讲解**。 -关于01背包我如下两篇已经进行深入分析了: +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) -* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) -* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) +动规五部曲分析完全背包,为了从原理上讲清楚,我们先从二维dp数组分析: -首先再回顾一下01背包的核心代码 -```cpp -for(int i = 0; i < weight.size(); i++) { // 遍历物品 - for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } -} -``` +### 1. 确定dp数组以及下标的含义 -我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。 +**dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少**。 -而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即: +很多录友也会疑惑,凭什么上来就定义 dp数组,思考过程是什么样的, 这个思考过程我在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 “确定dp数组以及下标的含义” 有详细讲解。 -```CPP -// 先遍历物品,再遍历背包 -for(int i = 0; i < weight.size(); i++) { // 遍历物品 - for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } -} -``` +### 2. 确定递推公式 -至于为什么,我在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中也做了讲解。 +这里在把基本信息给出来: -dp状态图如下: +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | +对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。 -![动态规划-完全背包](https://code-thinking-1253855093.file.myqcloud.com/pics/20210126104510106.jpg) +这里依然拿dp[1][4]的状态来举例: ([01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中也是这个例子,要注意下面的不同之处) -相信很多同学看网上的文章,关于完全背包介绍基本就到为止了。 +求取 dp[1][4] 有两种情况: -**其实还有一个很重要的问题,为什么遍历物品在外层循环,遍历背包容量在内层循环?** +1. 放物品1 +2. 还是不放物品1 -这个问题很多题解关于这里都是轻描淡写就略过了,大家都默认 遍历物品在外层,遍历背包容量在内层,好像本应该如此一样,那么为什么呢? +如果不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。 -难道就不能遍历背包容量在外层,遍历物品在内层? +推导方向如图: +![](https://file1.kamacoder.com/i/algo/20241126112952.png) -看过这两篇的话: -* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) -* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) +如果放物品1, **那么背包要先留出物品1的容量**,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。 -就知道了,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。 +容量为1,只考虑放物品0 和物品1 的最大价值是 dp[1][1], **注意 这里和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 有所不同了**! -**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!** +在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中,背包先空留出物品1的容量,此时容量为1,只考虑放物品0的最大价值是 dp[0][1],**因为01背包每个物品只有一个,既然空出物品1,那背包中也不会再有物品1**! -因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 +而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量,那背包中也可能还有物品1,所以此时我们依然考虑放 物品0 和 物品1 的最大价值即: **dp[1][1], 而不是 dp[0][1]** -遍历物品在外层循环,遍历背包容量在内层循环,状态如图: +所以 放物品1 的情况 = dp[1][1] + 物品1 的价值,推导方向如图: +![](https://file1.kamacoder.com/i/algo/20241126113104.png) -![动态规划-完全背包1](https://code-thinking-1253855093.file.myqcloud.com/pics/20210126104529605.jpg) -遍历背包容量在外层循环,遍历物品在内层循环,状态如图: +(**注意上图和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的区别**,对于理解完全背包很重要) -![动态规划-完全背包2](https://code-thinking-1253855093.file.myqcloud.com/pics/20210729234011.png) +两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值) -看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。 +`dp[1][4] = max(dp[0][4], dp[1][1] + 物品1 的价值) ` -先遍历背包在遍历物品,代码如下: +以上过程,抽象化如下: -```CPP -// 先遍历背包,再遍历物品 -for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 - for(int i = 0; i < weight.size(); i++) { // 遍历物品 - if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } - cout << endl; -} -``` +* **不放物品i**:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。 -完整的C++测试代码如下: +* **放物品i**:背包空出物品i的容量后,背包容量为j - weight[i],dp[i][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 -```CPP -// 先遍历物品,在遍历背包 -void test_CompletePack() { - vector weight = {1, 3, 4}; - vector value = {15, 20, 30}; - int bagWeight = 4; - vector dp(bagWeight + 1, 0); - for(int i = 0; i < weight.size(); i++) { // 遍历物品 - for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } - } - cout << dp[bagWeight] << endl; -} -int main() { - test_CompletePack(); -} +递推公式: `dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);` -``` +(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是 `dp[i - 1][j - weight[i]] + value[i])`) -```CPP +### 3. dp数组如何初始化 -// 先遍历背包,再遍历物品 -void test_CompletePack() { - vector weight = {1, 3, 4}; - vector value = {15, 20, 30}; - int bagWeight = 4; +**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 - vector dp(bagWeight + 1, 0); +首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图: - for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 - for(int i = 0; i < weight.size(); i++) { // 遍历物品 - if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } - } - cout << dp[bagWeight] << endl; -} -int main() { - test_CompletePack(); -} +![动态规划-背包问题2](https://file1.kamacoder.com/i/algo/2021011010304192.png) -``` +在看其他情况。 -本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习,题意是一样的,C++代码如下: +状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);` 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。 -```cpp -#include -#include -using namespace std; +dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 -// 先遍历背包,再遍历物品 -void test_CompletePack(vector weight, vector value, int bagWeight) { +那么很明显当 `j < weight[0]`的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 - vector dp(bagWeight + 1, 0); +当`j >= weight[0]`时,**dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个**。 - for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 - for(int i = 0; i < weight.size(); i++) { // 遍历物品 - if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - } - } - cout << dp[bagWeight] << endl; -} -int main() { - int N, V; - cin >> N >> V; - vector weight; - vector value; - for (int i = 0; i < N; i++) { - int w; - int v; - cin >> w >> v; - weight.push_back(w); - value.push_back(v); - } - test_CompletePack(weight, value, V); - return 0; +代码初始化如下: + +```CPP +for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 + dp[i][0] = 0; } + +// 正序遍历,如果能放下就一直装物品0 +for (int j = weight[0]; j <= bagWeight; j++) + dp[0][j] = dp[0][j - weight[0]] + value[0]; ``` +(注意上面初始化和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html)的区别在于物品有无限个) -## 总结 +此时dp数组初始化情况如图所示: -细心的同学可能发现,**全文我说的都是对于纯完全背包问题,其for循环的先后循环是可以颠倒的!** +![](https://file1.kamacoder.com/i/algo/20241114161608.png) -但如果题目稍稍有点变化,就会体现在遍历顺序上。 +dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? -如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了,而leetcode上的题目都是这种稍有变化的类型。 +其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。 -这个区别,我将在后面讲解具体leetcode题目中给大家介绍,因为这块如果不结合具题目,单纯的介绍原理估计很多同学会越看越懵! +但只不过一开始就统一把dp数组统一初始为0,更方便一些。 -别急,下一篇就是了! +最后初始化代码如下: -最后,**又可以出一道面试题了,就是纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后再问,两个for循环的先后是否可以颠倒?为什么?** -这个简单的完全背包问题,估计就可以难住不少候选人了。 +```CPP +// 初始化 dp +vector> dp(weight.size(), vector(bagweight + 1, 0)); +for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; +} +``` +### 4. 确定遍历顺序 +[01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中我们讲过,01背包二维DP数组,先遍历物品还是先遍历背包都是可以的。 -## 其他语言版本 +因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。 -### Java: - -```java -//先遍历物品,再遍历背包 -private static void testCompletePack(){ - int[] weight = {1, 3, 4}; - int[] value = {15, 20, 30}; - int bagWeight = 4; - int[] dp = new int[bagWeight + 1]; - for (int i = 0; i < weight.length; i++){ // 遍历物品 - for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量 - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); - } - } - for (int maxValue : dp){ - System.out.println(maxValue + " "); +详细可以看 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 【遍历顺序】的讲解 + +所以既可以 先遍历物品再遍历背包: + +```CPP +for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } +``` -//先遍历背包,再遍历物品 -private static void testCompletePackAnotherWay(){ - int[] weight = {1, 3, 4}; - int[] value = {15, 20, 30}; - int bagWeight = 4; - int[] dp = new int[bagWeight + 1]; - for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量 - for (int j = 0; j < weight.length; j++){ // 遍历物品 - if (i - weight[j] >= 0){ - dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]); - } - } - } - for (int maxValue : dp){ - System.out.println(maxValue + " "); +也可以 先遍历背包再遍历物品: + +```CPP +for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for (int i = 1; i < n; i++) { // 遍历物品 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } ``` +### 5. 举例推导dp数组 +以本篇举例数据为例,填满了dp二维数组如图: -### Python: +![](https://file1.kamacoder.com/i/algo/20241126113752.png) -先遍历物品,再遍历背包(无参版) -```python -def test_CompletePack(): - weight = [1, 3, 4] - value = [15, 20, 30] - bagWeight = 4 - dp = [0] * (bagWeight + 1) - for i in range(len(weight)): # 遍历物品 - for j in range(weight[i], bagWeight + 1): # 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) - print(dp[bagWeight]) +因为 物品0 的性价比是最高的,而且 在完全背包中,每一类物品都有无限个,所以有无限个物品0,既然物品0 性价比最高,当然是优先放物品0。 -test_CompletePack() -``` +### 本题代码: -先遍历物品,再遍历背包(有参版) -```python -def test_CompletePack(weight, value, bagWeight): - dp = [0] * (bagWeight + 1) - for i in range(len(weight)): # 遍历物品 - for j in range(weight[i], bagWeight + 1): # 遍历背包容量 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) - return dp[bagWeight] - -if __name__ == "__main__": - weight = [1, 3, 4] - value = [15, 20, 30] - bagWeight = 4 - result = test_CompletePack(weight, value, bagWeight) - print(result) -``` -先遍历背包,再遍历物品(无参版) -```python -def test_CompletePack(): - weight = [1, 3, 4] - value = [15, 20, 30] - bagWeight = 4 +```CPP +#include +#include +using namespace std; - dp = [0] * (bagWeight + 1) +int main() { + int n, bagWeight; + int w, v; + cin >> n >> bagWeight; + vector weight(n); + vector value(n); + for (int i = 0; i < n; i++) { + cin >> weight[i] >> value[i]; + } - for j in range(bagWeight + 1): # 遍历背包容量 - for i in range(len(weight)): # 遍历物品 - if j - weight[i] >= 0: - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) + vector> dp(n, vector(bagWeight + 1, 0)); - print(dp[bagWeight]) + // 初始化 + for (int j = weight[0]; j <= bagWeight; j++) + dp[0][j] = dp[0][j - weight[0]] + value[0]; -test_CompletePack() + for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } + } + cout << dp[n - 1][bagWeight] << endl; + + return 0; +} ``` -先遍历背包,再遍历物品(有参版) -```python -def test_CompletePack(weight, value, bagWeight): - dp = [0] * (bagWeight + 1) - for j in range(bagWeight + 1): # 遍历背包容量 - for i in range(len(weight)): # 遍历物品 - if j - weight[i] >= 0: - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) - return dp[bagWeight] +关于一维dp数组,大家看这里:[完全背包一维dp数组讲解](./背包问题完全背包一维.md) +## 其他语言版本 -if __name__ == "__main__": - weight = [1, 3, 4] - value = [15, 20, 30] - bagWeight = 4 - result = test_CompletePack(weight, value, bagWeight) - print(result) +### Java -``` +```Java +import java.util.Scanner; -### Go: - -```go - -// test_CompletePack1 先遍历物品, 在遍历背包 -func test_CompletePack1(weight, value []int, bagWeight int) int { - // 定义dp数组 和初始化 - dp := make([]int, bagWeight+1) - // 遍历顺序 - for i := 0; i < len(weight); i++ { - // 正序会多次添加 value[i] - for j := weight[i]; j <= bagWeight; j++ { - // 推导公式 - dp[j] = max(dp[j], dp[j-weight[i]]+value[i]) - // debug - //fmt.Println(dp) - } - } - return dp[bagWeight] -} +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int bagWeight = scanner.nextInt(); -// test_CompletePack2 先遍历背包, 在遍历物品 -func test_CompletePack2(weight, value []int, bagWeight int) int { - // 定义dp数组 和初始化 - dp := make([]int, bagWeight+1) - // 遍历顺序 - // j从0 开始 - for j := 0; j <= bagWeight; j++ { - for i := 0; i < len(weight); i++ { - if j >= weight[i] { - // 推导公式 - dp[j] = max(dp[j], dp[j-weight[i]]+value[i]) - } - // debug - //fmt.Println(dp) - } - } - return dp[bagWeight] -} + int[] weight = new int[n]; + int[] value = new int[n]; -func max(a, b int) int { - if a > b { - return a - } - return b -} + for (int i = 0; i < n; i++) { + weight[i] = scanner.nextInt(); + value[i] = scanner.nextInt(); + } -func main() { - weight := []int{1, 3, 4} - price := []int{15, 20, 30} - fmt.Println(test_CompletePack1(weight, price, 4)) - fmt.Println(test_CompletePack2(weight, price, 4)) -} -``` -### Javascript: - -```Javascript -// 先遍历物品,再遍历背包容量 -function test_completePack1() { - let weight = [1, 3, 5] - let value = [15, 20, 30] - let bagWeight = 4 - let dp = new Array(bagWeight + 1).fill(0) - for(let i = 0; i <= weight.length; i++) { - for(let j = weight[i]; j <= bagWeight; j++) { - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]) + int[][] dp = new int[n][bagWeight + 1]; + + // 初始化 + for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; } - } - console.log(dp) -} -// 先遍历背包容量,再遍历物品 -function test_completePack2() { - let weight = [1, 3, 5] - let value = [15, 20, 30] - let bagWeight = 4 - let dp = new Array(bagWeight + 1).fill(0) - for(let j = 0; j <= bagWeight; j++) { - for(let i = 0; i < weight.length; i++) { - if (j >= weight[i]) { - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]) + // 动态规划 + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagWeight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } } } - } - console.log(2, dp); -} -``` -### TypeScript: - -```typescript -// 先遍历物品,再遍历背包容量 -function test_CompletePack(): void { - const weight: number[] = [1, 3, 4]; - const value: number[] = [15, 20, 30]; - const bagSize: number = 4; - const dp: number[] = new Array(bagSize + 1).fill(0); - for (let i = 0; i < weight.length; i++) { - for (let j = weight[i]; j <= bagSize; j++) { - dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); + System.out.println(dp[n - 1][bagWeight]); + scanner.close(); } - } - console.log(dp); } -test_CompletePack(); + ``` -### Scala: +### Go -```scala -// 先遍历物品,再遍历背包容量 -object Solution { - def test_CompletePack() { - var weight = Array[Int](1, 3, 4) - var value = Array[Int](15, 20, 30) - var baseweight = 4 +### Python - var dp = new Array[Int](baseweight + 1) +```python +def knapsack(n, bag_weight, weight, value): + dp = [[0] * (bag_weight + 1) for _ in range(n)] + + # 初始化 + for j in range(weight[0], bag_weight + 1): + dp[0][j] = dp[0][j - weight[0]] + value[0] + + # 动态规划 + for i in range(1, n): + for j in range(bag_weight + 1): + if j < weight[i]: + dp[i][j] = dp[i - 1][j] + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) + + return dp[n - 1][bag_weight] + +# 输入 +n, bag_weight = map(int, input().split()) +weight = [] +value = [] +for _ in range(n): + w, v = map(int, input().split()) + weight.append(w) + value.append(v) + +# 输出结果 +print(knapsack(n, bag_weight, weight, value)) - for (i <- 0 until weight.length) { - for (j <- weight(i) to baseweight) { - dp(j) = math.max(dp(j), dp(j - weight(i)) + value(i)) - } - } - dp(baseweight) - } -} ``` -### Rust: - -```rust -impl Solution { - // 先遍历物品 - fn complete_pack() { - let (goods, bag_size) = (vec![(1, 15), (3, 20), (4, 30)], 4); - let mut dp = vec![0; bag_size + 1]; - for (weight, value) in goods { - for j in weight..=bag_size { - dp[j] = dp[j].max(dp[j - weight] + value); - } - } - println!("先遍历物品:{}", dp[bag_size]); +### JavaScript + +```js +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +let input = []; +readline.on('line', (line) => { + input.push(line.trim()); +}); + +readline.on('close', () => { + // 第一行解析 n 和 v + const [n, bagweight] = input[0].split(' ').map(Number); + + /// 剩余 n 行解析重量和价值 + const weight = []; + const value = []; + for (let i = 1; i <= n; i++) { + const [wi, vi] = input[i].split(' ').map(Number); + weight.push(wi); + value.push(vi); } - // 先遍历背包 - fn complete_pack_after() { - let (goods, bag_size) = (vec![(1, 15), (3, 20), (4, 30)], 4); - let mut dp = vec![0; bag_size + 1]; - for i in 0..=bag_size { - for (weight, value) in &goods { - if i >= *weight { - dp[i] = dp[i].max(dp[i - weight] + value); - } + + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); + + for (let j = weight[0]; j <= bagweight; j++) { + dp[0][j] = dp[0][j-weight[0]] + value[0]; + } + + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } - println!("先遍历背包:{}", dp[bag_size]); } -} -#[test] -fn test_complete_pack() { - Solution::complete_pack(); - Solution::complete_pack_after(); -} -``` + console.log(dp[n - 1][bagweight]); +}); -

- - - +``` diff --git "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index d49cdc5fb1..7aff85764e --- "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 贪心算法总结篇 @@ -130,7 +128,7 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 贪心专题汇聚为一张图: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/贪心总结water.png) +![](https://file1.kamacoder.com/i/algo/贪心总结water.png) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412)所画,总结的非常好,分享给大家。 @@ -150,8 +148,4 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 -

- - - diff --git "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index cac2929251..3bcf307525 --- "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,15 +1,13 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 关于贪心算法,你该了解这些! 题目分类大纲如下: -贪心算法大纲 +贪心算法大纲 ## 算法公开课 @@ -78,7 +76,7 @@ * 求解每一个子问题的最优解 * 将局部最优解堆叠成全局最优解 -这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。 +这个四步其实过于理论化了,我们平时在做贪心类的题目时,如果按照这四步去思考,真是有点“鸡肋”。 做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。 @@ -92,7 +90,3 @@ 最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。 -

- - - diff --git "a/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" "b/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" new file mode 100755 index 0000000000..98aa47a865 --- /dev/null +++ "b/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" @@ -0,0 +1,281 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](./other/kstar.md) +* [刷算法(两个月高强度学算法)](./xunlian/xunlianying.md) +* [背八股(40天挑战高频面试题)](./xunlian/bagu.md) + + + + + + + + + + + + + + +# 递归算法的时间与空间复杂度分析! + +之前在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中详细讲解了递归算法的时间复杂度,但没有讲空间复杂度。 + +本篇讲通过求斐波那契数列和二分法再来深入分析一波递归算法的时间和空间复杂度,细心看完,会刷新对递归的认知! + + +## 递归求斐波那契数列的性能分析 + +先来看一下求斐波那契数的递归写法。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + +对于递归算法来说,代码一般都比较简短,从算法逻辑上看,所用的存储空间也非常少,但运行时需要内存可不见得会少。 + +### 时间复杂度分析 + +来看看这个求斐波那契的递归算法的时间复杂度是多少呢? + +在讲解递归时间复杂度的时候,我们提到了递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归的时间复杂度**。 + +可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一棵递归树,如图: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305093200104.png) + +从图中,可以看出f(5)是由f(4)和f(3)相加而来,那么f(4)是由f(3)和f(2)相加而来 以此类推。 + +在这棵二叉树中每一个节点都是一次递归,那么这棵树有多少个节点呢? + +我们之前也有说到,一棵深度(按根节点深度为1)为k的二叉树最多可以有 2^k - 1 个节点。 + +所以该递归算法的时间复杂度为O(2^n),这个复杂度是非常大的,随着n的增大,耗时是指数上升的。 + +来做一个实验,大家可以有一个直观的感受。 + +以下为C++代码,来测一下,让我们输入n的时候,这段递归求斐波那契代码的耗时。 + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i - 1) + fibonacci(i - 2); +} +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci(n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} +``` + +根据以上代码,给出几组实验数据: + +测试电脑以2015版MacPro为例,CPU配置:`2.7 GHz Dual-Core Intel Core i5` + +测试数据如下: + +* n = 40,耗时:837 ms +* n = 50,耗时:110306 ms + +可以看出,O(2^n)这种指数级别的复杂度是非常大的。 + +所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。 + +其实罪魁祸首就是这里的两次递归,导致了时间复杂度以指数上升。 + +```CPP +return fibonacci(i-1) + fibonacci(i-2); +``` + +可不可以优化一下这个递归算法呢。 主要是减少递归的调用次数。 + +来看一下如下代码: + +```CPP +// 版本二 +int fibonacci(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci(second, first + second, n - 1); + } +} +``` + +这里相当于用first和second来记录当前相加的两个数值,此时就不用两次递归了。 + +因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。 + +同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是O(n)。 + +代码(版本二)的复杂度如下: + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +此时再来测一下耗时情况验证一下: + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci_3(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci_3(second, first + second, n - 1); + } +} + +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci_3(1, 1, n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} + +``` + +测试数据如下: + +* n = 40,耗时:0 ms +* n = 50,耗时:0 ms + +大家此时应该可以看出差距了!! + +### 空间复杂度分析 + +说完了这段递归代码的时间复杂度,再看看如何求其空间复杂度呢,这里给大家提供一个公式:**递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度** + +为什么要求递归的深度呢? + +因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。 + +此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是$O(1)$。 + +在看递归的深度是多少呢?如图所示: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305094749554.png) + +递归第n个斐波那契数的话,递归调用栈的深度就是n。 + +那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + + +最后对各种求斐波那契数列方法的性能做一下分析,如题: + + +![递归的空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305095227356.png) + +可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。 + +### 二分法(递归实现)的性能分析 + +带大家再分析一段二分查找的递归实现。 + +```CPP +int binary_search( int arr[], int l, int r, int x) { + if (r >= l) { + int mid = l + (r - l) / 2; + if (arr[mid] == x) + return mid; + if (arr[mid] > x) + return binary_search(arr, l, mid - 1, x); + return binary_search(arr, mid + 1, r, x); + } + return -1; +} +``` + +都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢? + +我们依然看 **每次递归的空间复杂度和递归的深度** + +每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。 + +**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:O(1)。 + +再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。 + +大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。 + + +## 总结 + +本章我们详细分析了递归实现的求斐波那契和二分法的空间复杂度,同时也对时间复杂度做了分析。 + +特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为O(2^n)是非常耗时的。 + +通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。 + + + + + + + diff --git "a/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" "b/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index b2b1b7795f..df1747e26a --- "a/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 链表总结篇 @@ -77,7 +75,7 @@ ## 总结 -![](https://code-thinking-1253855093.file.myqcloud.com/pics/链表总结.png) +![](https://file1.kamacoder.com/i/algo/链表总结.png) 这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。 @@ -97,8 +95,4 @@ -

- - - diff --git "a/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 88e41d7d3b..c465818739 --- "a/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,9 +1,7 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- - - -

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

@@ -14,7 +12,7 @@ 链表的入口节点称为链表的头结点也就是head。 如图所示: -![链表1](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806194529815.png) +![链表1](https://file1.kamacoder.com/i/algo/20200806194529815.png) ## 链表的类型 @@ -33,7 +31,7 @@ 双链表 既可以向前查询也可以向后查询。 如图所示: -![链表2](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806194559317.png) +![链表2](https://file1.kamacoder.com/i/algo/20200806194559317.png) ### 循环链表 @@ -41,7 +39,7 @@ 循环链表可以用来解决约瑟夫环问题。 -![链表4](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806194629603.png) +![链表4](https://file1.kamacoder.com/i/algo/20200806194629603.png) ## 链表的存储方式 @@ -56,7 +54,7 @@ 如图所示: -![链表3](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806194613920.png) +![链表3](https://file1.kamacoder.com/i/algo/20200806194613920.png) 这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。 @@ -106,7 +104,7 @@ head->val = 5; 删除D节点,如图所示: -![链表-删除节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806195114541-20230310121459257.png) +![链表-删除节点](https://file1.kamacoder.com/i/algo/20200806195114541-20230310121459257.png) 只要将C节点的next指针 指向E节点就可以了。 @@ -120,7 +118,7 @@ head->val = 5; 如图所示: -![链表-添加节点](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806195134331-20230310121503147.png) +![链表-添加节点](https://file1.kamacoder.com/i/algo/20200806195134331-20230310121503147.png) 可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。 @@ -130,7 +128,7 @@ head->val = 5; 再把链表的特性和数组的特性进行一个对比,如图所示: -![链表-链表与数据性能对比](https://code-thinking-1253855093.file.myqcloud.com/pics/20200806195200276.png) +![链表-链表与数据性能对比](https://file1.kamacoder.com/i/algo/20200806195200276.png) 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。 @@ -275,7 +273,3 @@ public class Node -

- - - diff --git "a/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" "b/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" old mode 100644 new mode 100755 index e2905d49be..7e23172093 --- "a/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" +++ "b/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" @@ -1,8 +1,6 @@ -

- - - -

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

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 面试题 02.07. 链表相交 @@ -15,7 +13,7 @@ 图示两个链表在节点 c1 开始相交: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211219221657.png) +![](https://file1.kamacoder.com/i/algo/20211219221657.png) 题目数据 保证 整个链式结构中不存在环。 @@ -23,15 +21,15 @@ 示例 1: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211219221723.png) +![](https://file1.kamacoder.com/i/algo/20211219221723.png) 示例 2: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211219221749.png) +![](https://file1.kamacoder.com/i/algo/20211219221749.png) 示例 3: -![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211219221812.png)![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211219221812.png) +![](https://file1.kamacoder.com/i/algo/20211219221812.png) @@ -44,11 +42,11 @@ 看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点: -![面试题02.07.链表相交_1](https://code-thinking.cdn.bcebos.com/pics/面试题02.07.链表相交_1.png) +![面试题02.07.链表相交_1](https://file1.kamacoder.com/i/algo/面试题02.07.链表相交_1.png) 我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图: -![面试题02.07.链表相交_2](https://code-thinking.cdn.bcebos.com/pics/面试题02.07.链表相交_2.png) +![面试题02.07.链表相交_2](https://file1.kamacoder.com/i/algo/面试题02.07.链表相交_2.png) 此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。 @@ -535,8 +533,43 @@ public ListNode GetIntersectionNode(ListNode headA, ListNode headB) } ``` +### Swift: +```swift +func getIntersectionNode(_ headA: ListNode?, _ headB: ListNode?) -> ListNode? { + var lenA = 0 + var lenB = 0 + var nodeA = headA + var nodeB = headB + // 计算链表A和链表B的长度 + while nodeA != nil { + lenA += 1 + nodeA = nodeA?.next + } + while nodeB != nil { + lenB += 1 + nodeB = nodeB?.next + } + // 重置指针 + nodeA = headA + nodeB = headB + // 如果链表A更长,让它先走lenA-lenB步 + if lenA > lenB { + for _ in 0..<(lenA - lenB) { + nodeA = nodeA?.next + } + } else if lenB > lenA { + // 如果链表B更长,让它先走lenB-lenA步 + for _ in 0..<(lenB - lenA) { + nodeB = nodeB?.next + } + } + // 同时遍历两个链表,寻找交点 + while nodeA !== nodeB { + nodeA = nodeA?.next + nodeB = nodeB?.next + } + return nodeA +} +``` + -

- - -