<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Data Structure and Algorithm &#8211; HU Xiaoxu</title>
	<atom:link href="https://blog.ihuxu.com/category/computer-science/data-structure-and-algorithm/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.ihuxu.com</link>
	<description>a software engineer&#039;s blog</description>
	<lastBuildDate>Sat, 21 Jun 2025 01:45:55 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6.2</generator>
	<item>
		<title>60 分钟搞定 Tarjan 算法求解无向图的割点与桥</title>
		<link>https://blog.ihuxu.com/tarjan-algorithm-and-cut-points-and-bridges-for-undirected-graphs/</link>
					<comments>https://blog.ihuxu.com/tarjan-algorithm-and-cut-points-and-bridges-for-undirected-graphs/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Mon, 09 Dec 2019 16:09:16 +0000</pubDate>
				<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Graph]]></category>
		<category><![CDATA[Original]]></category>
		<category><![CDATA[Tarjan]]></category>
		<guid isPermaLink="false">https://blog.ihuxu.com/?p=12052</guid>

					<description><![CDATA[本人在学习 Tarjan 算法求解无向图的割点与桥的问题时，很快发现了一篇简洁易懂的文章。很顺利地了解算法的思路，并写出了“高效”的代码，此时内心飘过 —— So Easy。然而，当我翻开《算法竞赛进阶指南》这本书的有关篇章时，我发现其中经过精简优化的代码有几条语句让我不得其所。以至于，花了较多心思和时间来思考🤔这段真正高效的 Tarjan 算法的工作原理以及代码的编写。 关于 Tarjan 算法，我将会写若干篇系列文章，来完整系统地介绍 Tarjan 算法的原理以及其主要解决的问题。而在本章我主要讲一个问题 —— 如何使用 Tarjan 算法求解无向图的割点与桥。 在讲述问题之前，我们先来简单地了解下什么是 Tarjan 算法？ Tarjan 算法 Tarjan 算法是基于深度优先搜索的算法，用于求解图的连通性问题。Tarjan 算法可以在线性时间内求出无向图的割点与桥，进一步地可以求解无向图的双连通分量；同时，也可以求解有向图的强连通分量、必经点与必经边。 如果，你对上面的一些术语不是很了解，那么也毫无关系。目前为止，我们只要知道 Tarjan 算法是基于深度优先搜索的，用于求解图的连通性问题的算法就好了。 提到 Tarjan，不得不提的就是算法的作者 —— Robert Tarjan。他可是一名著名的计算机科学家，我们耳熟能详的最近公共祖先（LCA）问题、强连通分量问题、双连通分量问题的高效算法都是由他发现并解决的，同时他还参与了开发斐波那契堆、伸展树的工作。 无向图的割点与桥 首先，什么是无向图？简单说，若一个图中每条边都是无方向的，则称为无向图。 割点 若从图中删除节点 以及所有与 关联的边之后，图将被分成两个或两个以上的不相连的子图，那么称 为图的割点。 桥 若从图中删除边 之后，图将分裂成两个不相连的子图，那么称 为图的桥或割边。 如何求解图的割点与桥？ 那么，在了解了 Tarjan 算法的背景以及图的割点与桥的基本概念之后，我们下面所面临的问题就是 —— 如何求解图的割点与桥？ 在这里，我们开门见山，直接引出 Tarjan 算法在求解无向图的割点与桥的工作原理。 时间戳 时间戳是用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序，当然，你可以理解成一个序号（这个序号由小到大），用 来表示。 搜索树 在无向图中，我们以某一个节点 出发进行深度优先搜索，每一个节点只访问一次，所有被访问过的节点与边构成一棵树，我们可以称之为“无向连通图的搜索树”。 追溯值 追溯值用来表示从当前节点 作为搜索树的根节点出发，能够访问到的所有节点中，时间戳最小的值 —— 。那么，我们要限定下什么是“能够访问到的所有节点”？，其需要满足下面的条件之一即可： 是的，你可能觉得这段话太绕了，那么我们通过动画的方式来摸你追溯值真实计算过程。 在上面的计算过程中，我们可以认为以序号 为根的搜索树的节点有 。上面所说的“通过一条非搜索树上的边”可以理解成动画中的 这条边，“能够到达搜索树的所有节点”即为节点 。<div class="read-more"><a class="btn read-more-btn" href="https://blog.ihuxu.com/tarjan-algorithm-and-cut-points-and-bridges-for-undirected-graphs/">Read More</a></div>]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/tarjan-algorithm-and-cut-points-and-bridges-for-undirected-graphs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://blog.ihuxu.com/wp-content/uploads/2024/04/v3_6cd03b2c-3292-11ea-8fc1-165bd6f00369_1.mp4" length="104155" type="video/mp4" />
<enclosure url="https://blog.ihuxu.com/wp-content/uploads/2024/04/v3_1d8dc54c-3293-11ea-adeb-8e79ebed40e0_1.mp4" length="26240" type="video/mp4" />

			</item>
		<item>
		<title>递归</title>
		<link>https://blog.ihuxu.com/recursive/</link>
					<comments>https://blog.ihuxu.com/recursive/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Wed, 04 Dec 2019 16:38:43 +0000</pubDate>
				<category><![CDATA[Basic Algorithm]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Original]]></category>
		<category><![CDATA[Recursive]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=11712</guid>

					<description><![CDATA[递归是什么？ 递归（英语：Recursion），又译为递回，在数学与计算机科学中，是指在函数的定义中使用函数自身的方法 。 维基百科 简单说，就是自身调用自身。 为什么使用递归？ 往往面对一类问题时，如果它的规模足够小或者说达到既定的边界条件时，我们可以直接获取答案。但是，当这类问题的规模比较大时，却往往无法直接获取答案。那么，这个时候就可以通过“自身调用自身”的方式，来不断地减小问题的规模，直到问题的规模被缩减到足够小时，直接将答案返回上层的调用者，最终获取到原问题的解。如果将求解的过程逆过来，那么就是所谓的递推。 通过这种方式，我们可以写出“优雅”的代码去解决规模比较大的问题。进而，避免了通过递推的方式，在每一次递推时产生的复杂的条件判断的问题。 上文中提到经过递归调用，会不断地减小问题的规模，有些作者认为这是一种减治法。 递归的特性 自身调用自身 在上文中，已经提到了这个特性，而且也非常好理解，不再赘述。 回溯时还原现场 在使用递归方法时，其中有一个不得不提的特性——回溯时还原现场。 通过递归调用，程序将执行到极限触达边界条件时，就需要将当前层的调用跳出“调用栈”，在跳出“调用栈”时，需要将一些状态信息还原到上一层场景所属的状态，即所谓的回溯时还原场景。 举个例子 有一颗二叉树，求解。代表以root为根的树的最大高度，即。 首先，我们要定义一个递归函数。在定义函数之前，我们要明确两个重要的事情： 函数的含义，代表了递归函数能为我们解决什么样的问题。在这里，我们定义函数的含义为求解某一个子树的高度。 函数的参数，代表了递归函数求解的问题的规模。在这里，我们定义函数的参数为当前需要遍历的节点 —— 以当前节点为根的子树（问题规模）。 在明确了这两件事情之后，面对一个规模较大的、复杂的问题就会变的简单得多。 下面，我们来看看在使用递归函数解决当前问题的整个过程。 递归求解树的最大高度 记忆化递归 通过上面简单的例子，我们了解了如何通过递归解决一个较大规模的问题。但是，我们会发现，使用递归函数解决的每个子问题的解仅仅被使用了一次。然而，在某些复杂的场景，子问题的解可能被使用若干次。那么这个时候，可以考虑加上备忘录法进行优化，即记忆化递归。 递归 —— 这种方法的思考方式是自顶向下的，也就是说符合我们常人在解决问题时的正向思考的过程。这与动态规划 —— 自底向上的方法恰恰相反（记忆化递归与动态规划的问题，我将在后续的篇章中讲解）。 三个经典问题 递归实现指数型枚举 从 ~ 这 个整数中随机选取任意多个，输出所有可能的选择方案。 在最开始学习算法时，面对这种类似的问题我思考了好久。当打开大牛的代码时，才恍然大悟，原来可以用这么简洁的代码解决 —— 递归。 那么，先来分析下这个“小”问题。我们需要在一个序列中随机选取任意多个数字，也就是说每一个数字都有两种可能——选择或不选择。那么，一共的可能的方案总数即 —— 那么，根据上文提到的方案，我们先来确认两件事情： 明确这两件事情之后，我们还需要一个额外的全局变量（），用来保留递归过程中产生的状态信息 —— 当前调用栈被选中的数字。 时间复杂度 每一个数字有两种可能 —— 选择或不选择 递归实现组合型枚举 从 ~ 这 个整数中随机选出 个，输出所有可能的选择方案。 这个问题与上面的问题的唯一区别是限定了选定元素的个数。 很自然地我们可以想到，在输出答案时我们只需要判断下结果集变量 —— 的大小是不是即可。当然，我们也可以在每次搜索时进行判断 —— 剪枝法，这样可以带来性能上的提升。 时间复杂度 我们可以通过判断当前调用栈中的中的数字个数如果大于，或者加上剩余的所有数字都达不到个的话，那么直接返回，跳出调用栈，进行其他情况的搜索。这样，避免了无谓的搜索，只关心满足条件的结果集。所以时间复杂度就从 降到了 。 递归实现排列型枚举 把 ~<div class="read-more"><a class="btn read-more-btn" href="https://blog.ihuxu.com/recursive/">Read More</a></div>]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/recursive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://qoogle.top/wp-content/uploads/2019/12/演示文稿-1.mp4" length="1596806" type="video/mp4" />

			</item>
		<item>
		<title>后缀数组（Suffix Array）</title>
		<link>https://blog.ihuxu.com/suffix-array/</link>
					<comments>https://blog.ihuxu.com/suffix-array/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Tue, 22 Oct 2019 09:14:45 +0000</pubDate>
				<category><![CDATA[Basic Data Structure]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Original]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[Suffix Array]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=11466</guid>

					<description><![CDATA[本文介绍后缀数组的定义与构建的过程。首先，文章介绍什么是后缀数组，随后讲解了最自然的朴素算法。为了引出更高效的算法，文章提及了倍增思想与基数排序的背景基础知识。接着，通过模拟演练的方式一步一步地演示如何创建后缀数组。将构建的抽象过程形象地展示出来，使读者更易理解。 定义 给定字符串，其所有后缀有。（6为字符串的长度）。如下所示： S = "banana" s1 = "banana" s2 = "anana" s3 = "nana" s4 = "ana" s5 = "na" s6 = "a" 后缀数组即为由构成的有序的（字典序升序排列的）字符串数组。 构建后缀数组的动态过程演示动画： https://visualgo.net/zh/suffixarray 思路 如何构建后缀数组？ 若字符串的长度为，那么有个后缀。先考虑朴素算法，将个字符串进行排序，需要，此外每一次比较两个字符串大小时需要枚举字符串的长度。故： 时间复杂度： 倍增思想 朴素算法是将所有后缀字符串进行排序，其中每一个都是完整的后缀。那么，这里能否从后缀字符串的长度入手，以后缀字符串的长度为规模将问题进行分解——先排序后缀字符串长度较小的情况，再排序长度较大的情况，依次递推。 那么，在递增后缀字符串的长度时，如果以线性方式进行递推，那么在时间上仍然不会有很好的改善。此时，可以通过成倍增长（倍增）的方式进行递推，只递推状态空间中在2的整数次幂位置上的值。在计算某一个值时，可以利用之前计算过的状态空间的值拼成所需的值即可。 在计算每一个后缀字符串长度时，两两比较字符串只需要的时间，将当前长度的后缀进行排序需要的时间为。长度递增次，所以一共需要的时间为。 基数排序 我们知道基数排序是一种稳定的（大小相同的元素在排序后，其相对位置不变）线性时间复杂度——的排序算法，可以将其融入到上述中的字符串排序中。可以将时间复杂度整体降到。 模拟演算 排序长度为的后缀数组 使用ASCII计算每一个后缀数组的第一个字符的权重（Rank）。 Index Suffix Rank 0 banana 2 1 anana 1 2 nana 14 3 ana 1 4 na 14 5 a 1 为了能够计算出长度为2的后缀数组的顺序，还需要保留右边相邻字符的权重（Next Rank）。如果，当前字符已为最后一个，那么相邻字符的权重可认为是最小值0。 Index Suffix Rank Next Rank 0 banana<div class="read-more"><a class="btn read-more-btn" href="https://blog.ihuxu.com/suffix-array/">Read More</a></div>]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/suffix-array/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>树状数组（Binary Indexed Tree）</title>
		<link>https://blog.ihuxu.com/binary-indexed-tree/</link>
					<comments>https://blog.ihuxu.com/binary-indexed-tree/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Thu, 10 Oct 2019 16:04:30 +0000</pubDate>
				<category><![CDATA[Advanced Data Structure]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Binary Indexed Tree]]></category>
		<category><![CDATA[Original]]></category>
		<guid isPermaLink="false">https://blog.ihuxu.com/?p=12051</guid>

					<description><![CDATA[树状数组或二叉索引树（英语：Binary Indexed Tree），又以其发明者命名为Fenwick树。其初衷是解决数据压缩里的累积频率（Cumulative Frequency）的计算问题，现多用于高效计算数列的前缀和， 区间和。它可以以的时间得到任意前缀和，并同时支持在时间内支持动态单点值的修改。空间复杂度。 摘自维基百科 文章先介绍低位运算（lowbit）的基本知识，再提及如何将一个整数划分为个区间的运算过程，进而延展到如何将线性序列以树行结构进行存取，接着介绍了高级数据结构——树状数组的两个基本操作——查询前缀和与单点增加，最后介绍了树状数组的一个应用——求解逆序对数。 lowbit（低位）运算 定义为非负整数在二进制表示下“最低位的1及其后边所有的0”构成的数值。 比如：，其二进制表示为 ，则其低位。 公式 如何计算一个整数中二进制表示下所有位是1的数值？ 比如，则其二进制表示下所有位是1的数值有：，。 朴素算法需要枚举整数中所有的位，时间复杂度为，为整数的二进制表示下的位数。 为了高效获取二进制表示下所有位是1的数值，可以利用运算，得到时间复杂度，为二进制表示下为1的位的个数。 比如，；接着另，则；接着另，停止。 为了得到的第几位为1，可以对2和8分别取对数，即, 。由于C++ math.h 库的函数是以为底的实数运算，并且复杂度常数较大，所以可以通过预处理，利用哈希表来代替运算。 代码 树状数组 假设整数，其二进制表示形式为： 代表二进制表示下位为1的索引下标值，且假设。 那么，可以将区间划分成个小区间 比如，，那么区间可以划分成, 和，其区间长度分别为, 和. 利用运算计算区间： 树状数组是基于以上思想的数据结构，基本用途是维护序列的前缀和。 那么，假设有序列，现在的问题就是如何将这个序列划分成个小区间。不妨，利用序列的索引值（以1为起点开始计数），根据上述计算区间的方式，将其以如下树形结构展开。 此时，以树形结构展开的序列A中的每一个节点都对应着树状数组中的一个值。那么这个值为以当前节点为根的子树中所有节点值的总和。 接着，我们看下以树形结构展开的树状数组是什么样的。 上图中最大的区别是某些节点中的值发生了变化。这是因为，在以树形结构展开的树状数组中的每一个值代表的是一个区间的总和。这个区间即为我们上述求解的区间，比如一个整数7，可以将其划分成, 和三个小区间。那么，这三个小区间的右端值作为索引对应的树状数组中的值即为当前区间元素的总和。 比如对应的树状数组的值为（BIT Value）10，它代表这个区间的和。 再比如对应的树状数组的值为11，它代表这个区间的和。 基本操作 树状数组支持两个基本操作——查询前缀和，单点增加。 查询前缀和 在寻求序列A的前n项的前缀和时，等于n代表的个区间的总和。 单点增加 观察父子节点的关系，可以推算出，父节点的索引parent(i)，为其子节点索引值 + 其低位——。 关于查询前缀和与单点增加的计算过程，可以观看下面视频展示的动画。 树状数组与逆序对 对于一个序列，如果，并且，那么则称与构成逆序对。利用树状数组数据结构可以求解序列中的逆序对个数。 在每一次更新（）树状数组时，以元素的值作为树状数组的索引，更新的值为+1，代表个数。 在每一次获取（）逆序对数时，存在于树状数组中的元素的索引值都比当前元素的大（逆序遍历），那么自然获取到的树状数组的值即为索引值比当前元素的大，且值比当前元素的小的个数。 注意，上述的求解过程时，如果序列A的值范围较大时，那么需要离散化处理。 参考]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/binary-indexed-tree/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>动态规划与分治法的思考</title>
		<link>https://blog.ihuxu.com/thoughts-on-dynamic-planning-and-dividing-and-conquer-method/</link>
					<comments>https://blog.ihuxu.com/thoughts-on-dynamic-planning-and-dividing-and-conquer-method/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Tue, 01 Jan 2019 10:55:27 +0000</pubDate>
				<category><![CDATA[Basic Algorithm]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Dynamic Programming]]></category>
		<category><![CDATA[Original]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=9939</guid>

					<description><![CDATA[如果一个问题具有最优子结构的性质，此外子问题具有重叠性质，那么可以采用自底向上的动态规划的思路进行求解。 同时，往往可以用递归的方式自顶向地进行求解，即分治法。 如果用分治法去求解这个问题时，能够利用备忘录法进行避免对于子问题的重复计算，那么其计算的效率可以和动态规划的计算效率相比。]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/thoughts-on-dynamic-planning-and-dividing-and-conquer-method/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>分治法与回溯法的思考</title>
		<link>https://blog.ihuxu.com/thoughts-on-divide-and-conquer-and-backtracking/</link>
					<comments>https://blog.ihuxu.com/thoughts-on-divide-and-conquer-and-backtracking/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Tue, 01 Jan 2019 10:39:37 +0000</pubDate>
				<category><![CDATA[Basic Algorithm]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Back Tracking]]></category>
		<category><![CDATA[DFS]]></category>
		<category><![CDATA[Divide and Conquer]]></category>
		<category><![CDATA[Original]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=9935</guid>

					<description><![CDATA[共同的递归性质 在广义上来说，所有递归的算法都属于分治法。无非是将问题分解成一个规模更小的问题，还是将问题分解成若干个，甚至和输入规模多项式级别的子问题。那么对于前者，有些作者称作是减治法，后者称作分治法。 那么对于回溯法（以深度优先搜搜方式进行）来说，目前为止我见过的都是通过递归的形式来实现的，那么从这个意义上来讲，回溯算法就是分治法的一种。 回溯状态的有无 再说，之所以称作是回溯法，是因为在搜索的过程中需要回溯到问题的某个状态，所以这往往需要保存回溯时的一些状态属性。然而，分治法通常并不需要考虑回溯状态的保存。 分解问题的规模 分治法往往是将问题分解成若干个子问题的形式，然而回溯法往往是将问题分解成规模更小的一个子问题。由于分治法将问题分解成若干个子问题，故当前问题的解需要依赖于若干个子问题，也就是若干个搜索路径的解，所以重点在于如何合并子问题的解；而然回溯法问题规模就为1个，当前的搜索路径的问题触碰到边界条件时，即可得到当前搜索路径的问题的解，并不需要合并，或者说合并很简单。 分治法可变成复杂的回溯法 如果分治法保留了每个问题的可回溯的状态，并利用了这些状态，尽管这非常的不容易实现，因为某个问题的回溯的状态可能需要依赖很多其子问题的状态。如果成功保存了这些回溯状态，那么这个时候，分治法就成了复杂的回溯法。]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/thoughts-on-divide-and-conquer-and-backtracking/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>一只青蛙跳出来的分治法、回溯法与动态规划</title>
		<link>https://blog.ihuxu.com/divide-and-conquer-backtracking-and-dynamic-programming-from-a-frog-jumping-out/</link>
					<comments>https://blog.ihuxu.com/divide-and-conquer-backtracking-and-dynamic-programming-from-a-frog-jumping-out/#comments</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Fri, 28 Dec 2018 07:40:53 +0000</pubDate>
				<category><![CDATA[Basic Algorithm]]></category>
		<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Dynamic Programming]]></category>
		<category><![CDATA[Back Tracking]]></category>
		<category><![CDATA[Divide and Conquer]]></category>
		<category><![CDATA[Original]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=9925</guid>

					<description><![CDATA[从2018年7月份开始，基础薄弱的我从0开始刷LeetCode题目。目的性很明确，也很简单——就是为了提高解决问题的思考实践能力，也为了提升自己的核心竞争力。也许，牛人会觉得这并不算什么竞争力。是的，我同意的。但，这是我目前能做的比较基础的事情罢了。 迄今（2018年12月28日）为止，已经刷了108道题目。顺序基本上是按照出现的频率（Frequency）来刷的，这个频率在LeetCode上需要订阅后才可以看得到。那么在刷了108道题目后，有那么一些题目会觉得“似曾相识”了，也会有一种触类旁通的感觉了。所以，我觉得应该适当放慢刷题的速度，同时做做总结了。 所以，计划了一项视频解说计划，在YouTubeh和B站都建立了《小旭解说算法之路》的频道，欢迎订阅，多多提建议。 那么，进入正题。经过了108道题的历练之后，我来说说对于分治法、回溯法和动态规划的理解。 我觉得他们三者是一个相互有交集的概念，并不是相互完全独立的。至于为什么不是完全独立的，在分别说说这三种方法的解决思路后，我们再终结一下。 分治法（Divide and Conquer） 分治法是解决规模庞大的问题的很好的思路，他通过降低问题的规模，形成若干个规模更小但形式相同的子问题，进行递归求解。在求解过后，将各个子问题的解合并起来，形成原问题的解。 那么它的大致流程主要分成三步： 所以，明确了三步之后，还要明确一件事件——实现方式：递归法。 分治法一般来说会采用递归法来进行实现，当然，利用迭代法（比如for、while）也是可以的。 所以，我们往往看到的递归算法从广义上来说都是分治法。无非就是有些递归算法将问题分解了若干个子问题，然而有些递归算法将问题分解成了一个子问题。那么有些作者会称作前者是分治法，后者是减治法。 其实，这个概念真的非常非常重要。在面对很多问题的时候，都可以用这种思路去思考。那么其中思考的一个非常重要的一点就是递归算法中的边界（跳出）条件的判定。 只要，我们想明白了求解子问题过程中的边界条件，那么问题就会很清晰，并且很容易写出程序来。否则，模糊的边界条件，会导致整个递归算法进入到“死循环”的尴尬地步。 举个例子 青蛙🐸跳台阶的问题：一只青蛙一次可以跳上1级台阶，也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法？ 那么如果我们用分治法的思路思考的话，这道题真是非常非常容易理解。 首先，当青蛙在面对第一个台阶时，他只有两种选择——跳一步还是跳两步。如果我们定义f(n)代表青蛙跳跃到n层台阶一共的方法数，那么我们可以将问题进行分解两个规模更小，但形式相同的问题： 其中f(n &#8211; 1)是青蛙选择跳一步后，剩下的子问题，同理f(n &#8211; 2)是青蛙选择跳两步后剩下的子问题。这样，我们就把问题进行了分解。 下面再谈谈如何解决，正如上面谈到的解决步骤，如果规模足够小那么直接返回，否则继续降低规模进行递归求解。这时，就是我们要确定边界条件——即当n = 1 和n = 2时的情况。 在明确了边角条件后，合并就非常的简单，也就是简单的相加即可了。 那么代码写出来是什么样子呢？ 回溯法（Backtracking） 回溯法，我理解应该也可以叫做深度优先搜索（Depth-First Search）。所以，他是一种搜索算法。 既然谈到搜索，往往这里面会面临选择的情景。以那个青蛙为例，当面对第一个台阶时，他有两个选择。当他选择一种选择后，将“义无反顾”的一条道走下去，每层都会进行一次选择，直到走到地n层位置时。这时，青蛙已经触碰到了边界，并得到了一种方案，之后青蛙会返回到最近的上一次选择时的情景，选择第二种情况继续走下去。以此往复，直到搜索全部的情景。 是的，这非常的抽象。我们来看看用二叉树来描述运动轨迹是怎么样的。我们假设n = 3。 颜色 左右子树 回溯 如下图中红框标记的位置就是回溯到某一个情况。 如果你理解了下图的运动轨迹，我想差不多对于回溯的搜索过程就基本了解了。所以，你可以找到其他回溯点么？ 理解了上图的运动轨迹后，那么，代码是什么样子呢？ 边界条件 再说更重要的选择的两个基本点 以此进行递归搜索（深度优先搜索DFS），在搜索到边界时进行回溯，以此往复直到搜索到所有情况为止。 动态规划（Dynamic Programming） 动态规划有两个重要的基本性质 最优子结构 如果一个问题的最优解包含了其中子问题的最优解，那么称其具有最优子结构的性质。 什么意思？青蛙在面对n个台阶时的解决方案数是f(n)，那么我们知道f(n) = f(n &#8211; 1) + f(n &#8211; 2)。其中的f(n &#8211; 1)与f(n &#8211; 2)就是两个子问题的最优解，此时我们可以理解成一个问题的最优解包含了其子问题的最优解，那么这个时候这种问题具有了最优子结构性质。 重叠子问题 这个性质，在我理解是对于上文提到的子问题的补充说明。当解决一个问题时，往往需要依赖于其更小规模的子问题的解，甚至是同时依赖于若干个规模更小的子问题的解，即子问题是被（重复）包含于比其更大的问题中的，所以他是具有重叠子问题的性质。 在这里，多提出一句，这个子问题是在解决当前问题时需要依赖的。即，只有计算了子问题，父问题才可能被求解。这是和贪心算法的重要区别所在。 状态转移方程<div class="read-more"><a class="btn read-more-btn" href="https://blog.ihuxu.com/divide-and-conquer-backtracking-and-dynamic-programming-from-a-frog-jumping-out/">Read More</a></div>]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/divide-and-conquer-backtracking-and-dynamic-programming-from-a-frog-jumping-out/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>最大流问题之 Ford-Fulkerson 算法</title>
		<link>https://blog.ihuxu.com/maximum_flow_problem_of_graph_ford_fulkerson_algorithm/</link>
					<comments>https://blog.ihuxu.com/maximum_flow_problem_of_graph_ford_fulkerson_algorithm/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Thu, 18 Oct 2018 01:17:25 +0000</pubDate>
				<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[Graph]]></category>
		<category><![CDATA[Maximum Flow]]></category>
		<category><![CDATA[Original]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=9844</guid>

					<description><![CDATA[Ford-Fulkerson算法（亦即标号法？）的输入与步骤如下： 输入 给定一个容量为c的图G=(V, E)，源点s与汇点（终点）t 步骤 对图G中每一个边(u, v)的流量f(u, v)进行初始化为0 查询过程：寻找（DFS、深度优先搜索方式）图G中的一条路径p，其中每一条边(u, v) ∈p，都有fc(u, v) = c(u, v) &#8211; f(u, v) &#62; 0（c(u, v) 代表当前边的容量，f(u, v) 代表当前边已有的流量，即c(u, v) &#8211; f(u, v)代表当前边可用的最大流量，即剩余流量） 调整过程：计算当前路径下每条边的最小剩余容量，cf(p) = min{fc(u, v) : (u, v) ∈p}，然后对于每条边进行如下操作： f(u, v) = f(u, v) + cf(p) （前向狐） f(v, u) = f(v, u) &#8211; cf(p) （后向狐） 往复上述2与3步骤，直至无法找到路径p为止]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/maximum_flow_problem_of_graph_ford_fulkerson_algorithm/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ZigZag Conversion(math)</title>
		<link>https://blog.ihuxu.com/zigzag-conversion-math/</link>
					<comments>https://blog.ihuxu.com/zigzag-conversion-math/#respond</comments>
		
		<dc:creator><![CDATA[HU Xiaoxu]]></dc:creator>
		<pubDate>Thu, 31 Aug 2017 15:52:40 +0000</pubDate>
				<category><![CDATA[Computer Science]]></category>
		<category><![CDATA[Data Structure and Algorithm]]></category>
		<category><![CDATA[LeetCode]]></category>
		<category><![CDATA[Math]]></category>
		<category><![CDATA[Original]]></category>
		<guid isPermaLink="false">http://blog.ihuxu.com/?p=9594</guid>

					<description><![CDATA[Question The string&#160;"PAYPALISHIRING"&#160;is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility) P A H N A P L S I I G Y I R And then read line by line:&#160;"PAHNAPLSIIGYIR"Write the code that will take a string and make this conversion given a number of rows: string convert(string text, int<div class="read-more"><a class="btn read-more-btn" href="https://blog.ihuxu.com/zigzag-conversion-math/">Read More</a></div>]]></description>
		
					<wfw:commentRss>https://blog.ihuxu.com/zigzag-conversion-math/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
