Ethan's Blog


  • Home

  • Archives

  • Tags

  • Search

巧解数学问题

Posted on 2023-03-16

引言

对于 LeetCode 上数量不少的数学题,我们尽量将其按照类型划分讲解。然而很多数学题的解法并不通用,我们也很难在几道题里把所有的套路讲清楚,因此我们只选择了几道经典或是典型的题目,供大家参考。

公倍数与公因数

利用辗转相除法,我们可以很方便地求得两个数的最大公因数 (GCD);将两个数相乘再除以最大公因数即可得到最小公倍数 (LCM)。进一步地,我们也可以通过扩展欧几里得算法,在求得 a 和 b 最大公因数的同时,也得到它们的系数 x 和 y,从而使 ax + by = gcd(a, b):

1
2
3
4
5
6
7
8
9
def gcd_extended(a, b):
if a == 0:
return b, 0, 1

gcd, x1, y1 = gcd_extended(b % a, a)
x = y1 - (b // a) * x1
y = x1

return gcd, x, y
Read more »

化繁为简的分治法

Posted on 2022-09-19

算法解释

顾名思义,分治问题由“分 (Divide)”和“治 (Conquer)”两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。我们在排序章节展示的归并排序就是典型的分治问题,其中“分”即为把大数组平均分成两个小数组,通过递归实现,最终我们会得到多个长度为 1 的子数组;“治”即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为 1 的子数组开始,最终合成一个大数组。

我们也使用数学表达式来表示这个过程。定义 T(n) 表示处理一个长度为 n 的数组的时间复杂度,则归并排序的时间复杂度递推公式为 T(n) = 2T(n/2) + O(n)。其中 2T(n/2) 表示我们分成了两个长度减半的子问题,O(n) 则为合并两个长度为 n/2 数组的时间复杂度。

那么怎么利用这个递推公式得到最终的时间复杂度呢?这里我们可以利用著名的主定理 (Master Theorem) 求解:

通过主定理我们可以知道,归并排序属于第二种情况,且时间复杂度为 O(nlogn),其他的分治问题也可以通过主定理求得时间复杂度。另外,自上而下的分治可以和 Memoization 结合,避免重复遍历相同的子问题;如果方便推导,也可以换用自下而上的动态规划方法求解。

Read more »

深入浅出动态规划

Posted on 2022-07-27

算法解释

这里我们引用一下维基百科的描述:“动态规划 (DP) 在查找有很多重叠子问题的情况的最优解时有效,它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。动态规划只能应用于有最优子结构的问题,最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。”

通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是:动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。

同时,我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。在一些情况下,动态规划可以看成是带有状态记录 (Memoization) 的优先搜索。状态记录的意思为,如果一个子问题在优先搜索时已经计算过一次,我们可以把它的结果储存下来,之后遍历到该子问题的时候可以直接返回储存的结果:

  • 动态规划是自下而上的,即先解决子问题,再解决父问题。
  • 带有状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。

如果题目需求的是最终状态,那么使用动态规划比较方便;如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。

Read more »

一切皆可搜索

Posted on 2022-07-21

算法解释

深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。

深度优先搜索

深度优先搜索 (DFS) 在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。

深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。

有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化 (Memoization)。

695. Max Area of Island
一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。

547. Number of Provinces
本题拥有 n 个节点,每个节点最多有 n 条边,表示和所有城市在同一省份,最少可以有 1 条边,表示是孤立的城市。当清楚了图的表示方法后,这道题与上一道题本质相同:搜索城市圈(岛屿圈)的个数。

对于节点连接类问题,我们也可以利用并查集来进行快速的连接和搜索。

417. Pacific Atlantic Water Flow
虽然题目要求的是满足向下流能到达两个大洋的位置,如果我们对所有的位置进行搜索,那么在不剪枝的情况下复杂度会很高。因此我们可以反过来想,从两个大洋开始向上流,这样我们只需要对矩形四条边进行搜索。

Read more »

千奇百怪的排序算法

Posted on 2022-07-20

常用排序算法

虽然在 Python 里可以通过 sort() 快速排序,而且刷题时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解出由这些排序算法引申出来的题目。以下是一些最基本的排序算法:

  • 快速排序 (Quick Sort)
  • 归并排序 (Merge Sort)
  • 堆排序 (Heap Sort)

快速选择

215. Kth Largest Element in an Array
快速选择一般用于求解 Kth Element 问题,可以在 O(n) 时间复杂度,O(1) 空间复杂度完成求解工作。

桶排序

347. Top K Frequent Elements
顾名思义,桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数(或其它属性),然后对桶进行排序。

Read more »
1…567…55
necusjz

necusjz

271 posts
16 tags
© 2016 - 2026 necusjz
Powered by Hexo
Theme - NexT.Mist