前言
在今天的麻将桌上,我收到了会长的喜讯,他在年前就已经成功斩获了字节跳动的offer,初八就要去北京报到入职了。
挺为他开心的,毕竟能去宇宙条实习,技术成长和单身问题都能漂亮解决。一边陪着会长打了会牌,一边问了些面试题,请教了些大厂面试经验。会长面的大厂还挺多的,其中有道小米的算法题,是leetcode困难级别的dp
题,麻将打了一大圈还没想明白...
会长都去字节了 ,麻将赢的一百多不香了,刷题战春招,开个春招备战
系列,我们一起冲。
小米面试
会长是拿下字节后,收到小米的面试邀请的,所以心态比较轻松。现在的行情,双非同学能够拿下字节的实习offer, 真个是"一日看尽长安花"。
我比较想去小米,特别喜欢小米SU7的帅气,家里用的一干物品,基本能用小米的。这不,初六写文章的我,用的是红米笔记本。
春招我已经做了心理建设了,多输(挂)几次就没关系。只要在输7次左右的时候,面上小米这样级别的公司,毕业年薪够买一辆SU7就行(20万+,哈哈)。目标有了, 开整。
算法题
会长说,他在面小米的时候,面试官出了一道leetcode 困难级别原题,动态规划的。题目是:2218. 从栈中取出 K 个硬币的最大面值和 - 力扣(LeetCode)。大家可以点开链接,先写一写,我们再讨论。
读题
一张桌子上总共有 n
个硬币 栈 。每个栈有 正整数 个带面值的硬币。
每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里。
给你一个列表 piles
,其中 piles[i]
是一个整数数组,分别表示第 i
个栈里 从顶到底 的硬币面值。同时给你一个正整数 k
,请你返回在 恰好 进行 k
次操作的前提下,你钱包里硬币面值之和 最大为多少 。
在牌桌上,听会长说出这道题的时候,就有点懵。这道leetcode没刷过,回来一看,果然是道困难
题。
-
这是一道最值问题,最佳问题要不就用贪心,要不就是动态规划。动态规划比较难,大厂考
dp
概率大些。只要拿到状态转移方程, 很快搞定。 -
硬币问题是道经典的动态规划题,不清楚的同学可以先去刷一下。 322. 零钱兑换 - 力扣(LeetCode)
-
栈,题目中的这个数据结构跟解的关系让我陷入了泥潭。
母题
栈
开始让我迷惑,只能在栈顶操作,当我把一坨硬币摆来摆去的时候,好像跟背包问题有点像。在几坨硬币中,怎么拿,面值和最大?
- 每一坨都是一个栈,要取的硬币是连续的栈顶元素。
- 在某坨取的硬币个数可以看成质量
- 在某坨取的硬币的总价值可以看成价值
- 这个问题就变成了怎么取K次(总重量),拿到的价值最大
确信,这就是一个背包问题。总了问度娘和通义千问后,解此题的第一个关键思路是将它转变成一个分组(栈)背包问题。
大厂面试算法题一般是一组题,动态规划组题出现的最多。既然此题的母题是背包问题,我们就来先复习下01背包问题。
0-1背包模型
0-1背包问题是一个基本问题,基于这个基本问题,可以衍生出千姿百态的变种问题,这种题目就非常适合拿来构造解题模型,今儿就用上了。
0-1背包问题说的是这么回事儿:
有 n 件物品,物品体积用一个名为 w 的数组存起来,物品的价值用一个名为 value 的数组存起来;每件物品的体积用 w[i] 来表示,每件物品的价值用 value[i] 来表示。现在有一个容量为 c 的背包,问你如何选取物品放入背包,才能使得背包内的物品总价值最大?
背包问题是动态规划的标准对口问题,我们来回忆下dp
的套路
-
用递归思想,自顶向下倒推,找到最优子结构 容量c是终点,倒推,f(i, c) 表示有i件物品在容量c的背包中,且价值最大。 作为0-1背包,这个物品拿不拿就是状态转变的关键
如果没拿,
f(i, c) = f(i-1, c)
如果拿了,
f(i, c) - value[i] = f(i-1, c-w[i])
w[i]是第i件物品的重量,value[i]是第i件物品的价值 -
从最优子结构提取的过程中,得到状态转移方程
css
dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]] + c[i])
- 自底向上迭代,得到最优结果。
ini
for(let i=1;i<=n;i++) {
for(let v=w[i]; v<=c;v++) {
dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]]+value[i])
}
}
- 优化一下,二维dp数组会浪费空间,转为一维。
0-1背包问题代码如下:
scss
function knapsack(n, c, w, value) {
// dp是动态规划的状态保存数组
const dp = new Array(c+1)).fill(0)
// res 用来记录所有组合方案中的最大值
let res = -Infinity
for(let i=1;i<=n;i++) {
for(let v=c;v>=w[i];v--) {
// 写出状态转移方程
dp[v] = Math.max(dp[v], dp[v-w[i]] + value[i])
// 即时更新最大值
if(dp[v] > res) { res = dp[v] }
}
}
return res
}
回到原题
ini
function maxValueOfCoins(piles, k) {
// 状态转移方程f[i], 0 dummy 初始都为0
let f = new Array(k + 1).fill(0);
let sumN = 0;
for (let pile of piles) {
// 当前pile的长度
let n = pile.length;
// 计算每个堆叠的前缀和
// pile 里的值是前面值的和
for (let i = 1; i < n; i++) {
pile[i] += pile[i - 1];
}
// 更新 sumN 为当前栈集合中前 i 个栈大小之和的最小值(不超过 k)
// 一个栈全拿 n , k < n k, k > n 拿多少个?
sumN = Math.min(sumN + n, k);
// 枚举所有可能的组合数 j,并根据物品体积计算最大价值
// 递减式的去遍历
for (let j = sumN; j > 0; j--) {
// 遍历当前堆叠中前 min(n, j) 个元素
for (let w = 0, v = pile[0]; w < Math.min(n, j); w++, v = pile[w]) {
// 注意:此处由于数组下标从 0 开始,因此物品体积对应的下标应为 w
f[j] = Math.max(f[j], f[j - w - 1] + v);
}
}
}
return f[k];
}
参考资料
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!