Ethan's Blog


  • Home

  • Archives

  • Tags

  • Search

深入浅出动态规划

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 »

居合斩!二分查找

Posted on 2022-07-19

算法解释

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(logn)。

具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍:

  • 尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法。
  • 在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

求开方

69. Sqrt(x)

mid = (lo + hi) // 2 可能会因为 lo + hi 溢出而错误,因而采用 mid = lo + ((hi - lo) >> 1) 的写法。

另外,这道题还有一种更快的算法——牛顿迭代法:

1
2
3
4
5
6
def sqrt(x):
num = x
while num * num > x:
num = (num + x // num) // 2

return num
Read more »

玩转双指针

Posted on 2022-07-17

算法解释

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针:

  • 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
  • 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

在 C++ 中,要注意 const 的位置对指针效果的影响:

1
2
3
4
5
int x;
int * p1 = &x; // 指针可以被修改,值也可以被修改
const int * p2 = &x; // 指针可以被修改,值不可以被修改 (const int)
int * const p3 = &x; // 指针不可以被修改 (* const),值可以被修改
const int * const p4 = &x; // 指针不可以被修改,值也不可以被修改

Two Sum

167. Two Sum II - Input Array Is Sorted
因为数组已经排好序,我们可以采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。

归并两个有序数组

88. Merge Sorted Array
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的 m−1 位和 nums2 的 n−1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。

a++ 和 ++a 都是将 a 加 1,但是 a++ 返回值为 a,而 ++a 返回值为 a+1。如果只是希望增加 a 的值,而不需要返回值,则推荐使用 ++a,其运行速度会略快一些。

Read more »
1…678…55
necusjz

necusjz

274 posts
16 tags
© 2016 - 2025 necusjz
Powered by Hexo
Theme - NexT.Mist