前言
本人是一名前端程序员,平时开发中能使用算法的地方其实不多,学习算法主要就通过每天的力扣打卡
本文主要介绍我在近期的笔试中的解题思路,并对动态规划这一算法做重点讲解
解题思路
近期也是参加了约10场的笔试,除了两道卡在80%没找到边界条件,其余的算法题都AC了
先概括一下拿到题目后的基本思路:数据量小,动态规划;数据量大,绝逼贪心;涉及树图表,广深遍历
详细说下:
- 动态规划能解决面试中的大部分问题,但数据量的极限大约在
10**5左右,在这个范围内的问题都可以尝试动态规划 - 一般遇到很离谱的数据范围(
10**9),不用想一定是贪心来解决的。贪心与动态规划的区别在于前者求的是局部最优,后者求的是全局最优,使用贪心解决问题的前置条件是局部最优能推导出全局最优 - 还有就是树、图、表这类特殊的数据结构,解题方案基本都是深度递归、广度遍历了
接下来详细讲解一下动态规划;贪心其实是一种直觉,主要在于对题目的理解;而广深遍历去专门练一些基本都能掌握。唯有动态规划的落差感最强烈,看题解是真简单,但自己做又想不出来
动态规划
动态规划和记忆化搜索一样,都是将过程中的状态保存下来,避免暴力递归中的重复计算,通过空间换取时间
先说一个误区,大部分人在学动态规划时可能只看重递推公式。
但其实动态规划主要分为以下四步:
- 明确
dp数组下标的含义,根据含义确定结果 dp数组的初始化- 确定递推公式
- 确定遍历顺序
好多人面对动态规划题,直接就去思考递推公式,在dp数组还没想清楚时,想要同时完成前三步怎会不难呢?
例题
接下来用道较难的例题来实操一下,例题选自力扣664. 奇怪的打印机
观察题目数据范围,只有100,符合动态规划的要求,甚至支持n**3的时间复杂度
-
第一步,思考如何建立
dp数组。题目中并没有明显的迭代环节,我们可以尝试每次迭代一个字母,先用dp数组存储从开头到当前字符,所需的最少打印数,而所求结果就是dp[n-1](n为字符串长度,下标从0开始) -
第二步,初始化
dp数组。没啥需要初始化的,打印第一个字符时需要1次dp[0]=1 -
第三步,确定递推公式。这里出现分支:
- 当前字符与前一字符相等,很简单,打印前一字符时可以同时打印当前字符(
dp[i] = dp[i-1]) - 当前字符与前一字符不相等,一种措施是单独打印当前字符(
dp[i] = dp[i-1] + 1);
当然我们想要最少的打印数,所以得思考该字符能否在之前一起打印,比如aba也只需要打印两次。
然而在是否能够一起打印时发现了问题。因为我们不能粗暴的认为前面有相同字符,就能一起打印,比如abab必须打印3次,其中 a 或 b 只能够单独打印;也不能简单的与第一个字符作比较,cbab也只需要3次
- 当前字符与前一字符相等,很简单,打印前一字符时可以同时打印当前字符(
到这里,我们发现dp数组保存的内容无法支持我们的递推了,说明dp数组的含义存在问题,这时要回过头去修改dp数组的定义
-
回到第一步,将 dp 数组升维,
dp[i][j]表示打印完成区间 [i, j] 的最少打印数 -
然后初始化,长度为 1 的区间只需要打印一次(
dp[i][i] = 1) -
递推公式,与前一字符相等时还是直接
dp[i] = dp[i-1];而与前一字符不同时,为了省略这次打印,可尝试与之前相同字符同时打印,也就是从那一位置做分割:dp[i][j] = Min(dp[i][k-1] + dp[k][j-1])(k 需满足s[k] = s[j]) -
确定遍历顺序,因为我们计算
dp[i][j]时,要求dp[i][k-1]与dp[k][j-1]已计算(i<k<j)。所以遍历顺序是从下至上,从前往后
上代码:
var strangePrinter = function(s) {
const n = s.length
// 定义dp数组 dp[i][j]表示打印完成区间 [i, j] 的最少打印数
const dp = new Array(n).fill(0).map(() => new Array(n))
// 初始化
for (let i = n-1; i >= 0; i--) {
dp[i][i] = 1
}
// 从下往上,从前往后遍历
for (let i = n-2; i >= 0; i--) {
for (let j = i + 1; j < n; j++) {
if (s[j] == s[j - 1]) {
// 与前一字符相同,直接一起打印
dp[i][j] = dp[i][j - 1]
} else {
let min = dp[i][j - 1] + 1 // 单独打印
for (let k = i; k < j; k++) {
// 遍历之前的所有相同字符,尝试省略这次打印
if (s[k] == s[j]) {
// 注意处理 k==i 时的情况
min = Math.min(min, dp[k][j-1] + (k==i ? 0 : dp[i][k-1]))
}
}
dp[i][j] = min
}
}
}
// 输出结果
return dp[0][n - 1]
};
针对这道题,如果直接思考递推公式,往往会默认成一维的dp数组,进而由于信息不足无法确定递推逻辑
我们提前明确了dp数组的定义,在递推逻辑出现问题时就能够及时去修改,一般都是升维dp数组,让其存储更多的信息
其实吧,这道例题也出自我昨天的笔试,当时也意识到是力扣的原题,但具体解法忘了,通过以上几步,重新写出了动态规划的代码
结语
前端考的算法题相对来说应该较为简单,本文的讲解也并不深入,欢迎掘友评论指正
如果觉得喜欢或有所帮助,欢迎点赞关注,鼓励一下作者