本文仅供自己复习使用
1、五大基本算法
写在前面:
a、凡是用到递归,就画递归树
1)分治算法
具体来讲就是分而治之
,把一个大问题分成很多相同或相似的小问题,小问题再分,直到无法再分,所有小问题的结果合起来就是最终结果
常见的有排序中的快速排序和归并排序,分治经常和递归一起提及
①分治适合的情况
a、该问题缩小到一定规模就可以解决
b、该问题可以分成若干小问题
c、该问题若干小问题的解可以合并成这个问题的答案
d、该问题分出来的若干小问题都是独立的
若满足a、b,却不满足c,可以考虑贪心算法或者动态规划算法
若不满足d方法,可以考虑一下动态规划算法(这样效率更高)
②关于分治的经典问题
a、二分搜索
前提是有序数组
递归情况:
function binarySearch(arr, num, left = 0, right = arr.length - 1) {
if(left > right) {
return -1
}
let middle = Math.floor((left + right) / 2)
if(arr[middle] > num) {
right = middle - 1
} else if(arr[middle] < num) {
left = middle + 1
} else {
return middle
}
return binarySearch(arr, num, left, right)
}
非递归情况:
function binarySearch(arr, num) {
let left = 0;
let right = arr.length - 1;
while(left <= right) {
let middle = Math.floor((left + right) / 2)
if(arr[middle] > num) {
right = middle - 1
} else if(arr[middle] < num) {
left = middle + 1
} else {
return middle
}
}
return -1
}
深入讲解一下!!!
模板
function binarySearch(arr, value) {
let left = 0,right = arr.length - 1
while(...) {
// 这里使用 left + (right - left) / 2
// 而不是使用 (left + right) / 2
// 是因为 left + right 有可能超过 Number.MAX_VALUE
let mid = left + Math.ceil((right - left) / 2)
if(arr[mid] > value) {
right = ...
} else if(arr[mid] < value) {
left = ...
} else if(arr[mid] === value) {
...
}
}
return ...
}
这个模板的重点就是用 else-if 而不是 else,这样就能很简单看出逻辑
而 … 就是填充一些具体代码的地方
查找一个数(基本的二分搜索)
function binarySearch(arr, value) {
let left = 0; right = arr.length - 1
// ①
while(left <= right) {
let mid = left + Math.ceil((right - left) / 2)
if(arr[mid] === value) {
return mid
} else if(arr[mid] > value) {
// ②
right = mid - 1
} else if(arr[mid] < value) {
// ②
left = mid + 1
}
}
return -1
}
问题①:为啥用 left <= right,left < right 不行吗
.
left <= right 是对闭区间([left, right])也就是初始的时候 right = arr.length - 1 来说的,再具体一点,他的终止条件为[right + 1, right],举个例子就是[3, 2]
而 left < right 是对左闭右开([left, right))也就是初始的时候是 right = arr.length 来说的,终止条件为[right, right],即【2, 2】
但是正经的终止条件应当是区间为空,而第二种很明显,还有一个数 arr[right]
所以如果是第二种写法,就应该改一下
function binarySearch2(arr, value) {
let left = 0, right = arr.length;
while(left < right) {
let mid = left + Math.ceil((right - left) / 2)
if(arr[mid] > value) {
right = mid - 1
} else if(arr[mid] < value) {
left = mid + 1
} else if (arr[mid] === value) {
return mid
}
}
// 这个时候出来的left = right,而left这个值没有和value比较过
return arr[left] === value ? left : -1
}
问题②:为什么用 left = mid + 1, right = mid - 1 而不是用 left = mid, right = mid
因为我们之前搜索都是闭区间嘛,[left, right],如果mid不是,那么就不需要在mid上浪费,所以是 mid + 1 或者 mid - 1
问题③:该算法的缺点
这个算法是无法对一个数组中有两个相同的数进行搜索的,如1,2,2,3,4,5
,就不一定能找到第一个2
b、大整数乘法
首先,有些语言不支持存入很多位的整数,且乘法远比加法的速度慢,而这里的数的前提是位数很多,所以我们可以采取分治来做大整数相乘
假设做 8132579076 * 921352184
我们可以思考把这两个整数各自拆成两部分
从高到低,把两个整数都平分(这里往下取,所以是Math.floor,Math.ceil是向上取整),得到ABCD四块
然后我们可以得到第一种,也就是当两个整数位数都相等的时候
这样一来,两个整数的乘积可以转为
这样的话其实计算AC、AD、BC、BD即可,同理,继续往下分,直到我们拿的整数只有一位就很好计算了,但这个时候有四次乘法,三次加法,我们仍然可以找到简便方法,下面会说
当然,我们也可以运用数学方法,得到一个更简单的方法:
这样,我们就只需要求得三次乘法,四次加法即可
而如果两个整数位数不相等,就不能把位数都笼统地归为 n / 2,而且还有个问题就是这里的 n / 2 因为使用了 Math.floor() 会向下取整,所以真正的 n / 2 = nums.length - Math.floor(nums.length / 2),m / 2 也同理
然后看A大不大,A不大就直接乘以C,否则A和C继续,AD,BC,BD也是
c、棋盘覆盖
在一个2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。
在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,
且任何2个L型骨牌不得重叠覆盖。
分析:
我们可以使用分治的方法把2k棋盘往下分,分成4个子棋盘,然后一直分,直到找到某个 2*2 的棋盘中有这个特殊的方格,然后把这个 2*2 的棋盘的其他三格用L型特殊棋子覆盖即可,这样剩下的棋盘就一定可以被填满
d、归并排序
假设有一个数列,10, 4, 6, 3, 8, 2, 5, 7,做归并排序
如图,首先要这样慢慢分开,然后要再把他们都合并在一起
所以总结就两步,一分成小问题(分),二合并成最终问题(治)
function mergeSort(arr) {
// 如果这个数组只有一个或没有就停止迭代,把自己返回出去
if(arr.length <= 1) {
return arr;
}
// 拿到中间
let mid = Math.floor(arr.length / 2)
// 得到左边(slice包含begin,不包含end),不改变原数组
let left = mergeSort(arr.slice(0, mid));
// 右边
let right = mergeSort(arr.slice(mid))
let res = sort(left, right)
return res;
}
function sort(left, right) {
let res = []
// 如果两边都有数据就比大小
while(left.length && right.length) {
if(left[0] < right[0]) {
res.push(left.shift())
} else {
res.push(right.shift())
}
}
// 如果左边没了
while(right.length) {
res.push(right.shift())
}
// 如果右边没了
while(left.length) {
res.push(left.shift())
}
return res;
}
归并的时间复杂度为O(nlogN)
e、快速排序
归并排序是一分二,而快排不是,快排是找一个标杆,让小的在左边,大的在右边,先局部有序,再最终有序
function quickSort(arr, left=0, right=arr.length - 1) {
// 递归终止条件为只要左边大于等于右边就行
if(left >= right) return;
// 获取中间值
let index = partition(arr, left, right);
// 左边的继续
quickSort(arr, left, index - 1)
// 右边的继续
quickSort(arr, index + 1, right)
return arr;
}
function partition(arr, left, right) {
// 基准这里使用第一个
let pivot = left;
while(left < right) {
// 循环,直到找到小于基准的
while(arr[right] >= arr[pivot] && left < right) {
right--;
}
// 通过循环找到左边大于基准的
while(arr[left] <= arr[pivot] && left < right) {
left++;
}
// 将两个交换
swap(arr, left, right)
}
// 此时left=right,让这个值和基准交换
swap(arr, pivot, left)
return left;
}
function swap(arr, left, right) {
let temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
快速排序是不稳定的算法,时间复杂度为O(nlogn)
f、最接近点对问题
给定平面上n个点,找出其中的一对点的距离,使得在这n个点的所有点对中,该距离为所有点对中最小的
思路:
1、先根据x排序,找到中间的那个点
2、把区域分成左边和右边,递归地找到左边最小的距离d1,以及右边最下的距离d2,在d1和d2中找到最小的距离作为d,然后以中线为轴向两边各取d距离,被框住的点,挨个求最小的距离d3,然后与d比较
function getPoint(arr) {
// 先处理点
arr = arr.map(res => {
let xy = res.split(' ');
return {x: parseFloat(xy[0]), y: parseFloat(xy[1])}
})
// 以x升序
// 小于0,a在b前面
// 大于0,b在a前面
// 等于0,不变
arr.sort((a, b) => {
return a.x - b.x
})
return divide(arr)
}
function divide(arr, left=0, right=arr.length-1) {
// 如果left=right就返回一个最大值
if(right - left < 1) return Number.MAX_VALUE
if(right - left === 1) {
return getDistance(arr[left], arr[right])
}
// 这里应该向上取整,因为left和right是下标,当left+right是奇数时,其实真正的个数是偶数个
let middle = Math.ceil((left + right) / 2)
let leftMin = divide(arr, left, middle)
let rightMin = divide(arr, middle + 1, right)
let min = Math.min(leftMin, rightMin)
// 中间应该是以middle往两边各占min的距离,在这块区域内通过这些点拿到最短的距离
// i左j右
for(let i = middle; i >= 0; i--) {
for(let j = middle; j <= right; j++) {
if(arr[j].x - arr[i].x <= min * 2) break
let middleMin = getDistance(arr[j], arr[i])
min = Math.min(min, middleMin)
}
}
return min
}
function getDistance(num1, num2) {
return Math.sqrt((num1.x - num2.x) ** 2 + (num1.y - num2.y) ** 2)
}
(9)循环赛日程表
(10)汉诺塔
③分治算法的思维过程:通过数学归纳法,求得方程式,然后设计递归
a、找到最小规模的方程
b、考虑规模增大时的方程
c、找到递归方法
2)动态规划算法
跟分治算法有点相同,也是把一个问题划分为多个小问题,但是会丢弃某些非最优解,直到无法转换,最后的那个最优解就是结果
状态方程就是把所有状态弄出来,然后择优
①动态规划适合的情况
a、最优解的子问题的解也是最优的
b、某阶段状态只与当前状态有关,与后面分出的子问题无关
c、有重叠子问题,该阶段的状态在下一阶段也会用到(这是动态规划比其他算法高效的地方)
②关于动态规划的经典问题
a、斐波拉契数列
步骤一:暴力的递归算法
function fib(n) {
if(n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
设N = 20
这个递归树的理解就是,要得到 f(20),就要得到 f(19) 和 f(18),直到 f(1) 和 f(2) 是已知的,就不再递归了
递归算法的时间复杂度是子问题的个数,即O(2^N)
根据时间复杂度各级别排序:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
从中可以看出递归的速度其实没那么好,除此之外,可以从图中发现有很多重复的子问题,而分治算法中有说到要尽量避免重复子问题,所以这里不用分治,而动态规划就是要有重复子问题的算法
步骤二:带备忘录的递归算法
从步骤一可以看出,其实最耗时的就是重复的子问题,所以可以通过备忘录来解决,分解成子问题后马上去查找备忘录,如果里面有就直接取,没有再计算,备忘录一般都是一维数组,当然用字典等都可以
var arr = []
function fib(n) {
if(n <= 2) return 1;
if(arr[n - 1]) {
return arr[n - 1]
} else {
arr[n - 1] = fib(n - 1) + fib(n - 2)
return arr[n - 1]
}
}
这样就极大的减少了子问题的个数,时间复杂度降为O(N)
从时间复杂度来看,带备忘录的递归算法和动态规划的时间复杂度是一样的,都是O(N)
但是,动态规划是自底向上,而这里是自顶向下
步骤三:自底向上的非递归算法
这个就相当于使用的斐波拉契数列的数学意义,前两个相加等于后面的哪一个,而 fib(n) = fib(n - 1) + fib(n - 2) 以及 fib(1) = fib(2) = 1 就是动态规划中的状态转移方程
function fib(n) {
let arr = [0, 1, 1]; // 把第一个数空出来
for(let i = 3; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2];
}
return arr[n]
}
为什么叫做动态转移方程?
fib(n) = fib(n - 1) + fib(n - 2),从状态 n 变为了状态 n - 1 和状态 n - 2 而已
动态转移方程都是暴力解开,找规律得到的,而这个动态转移方程是动态规划里最重要的东西
另外,这个斐波拉契数列的程序其实还可以简便,因为后一个数只是与前两个数有关,所以可以把空间复杂度降到O(1)
function fib(n) {
let pre = 1;
let after = 1;
for(let i = 3; i <= n; i++) {
let temp = after;
after = pre + after;
pre = temp;
}
return after;
}
斐波拉契函数其实严格意义上来讲不是动态规划算法,因为没有涉及最优解,一旦使用max、min、循环、求最优解等,十有八九都是动态规划
b、凑零钱问题 leetcode322
题目:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,再给一个总金额 n,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,则回答 -1
比如说,k = 3,面值分别为 1,2,5,总金额 n = 11,那么最少需要 3 枚硬币,即 11 = 5 + 5 + 1 。下面走流程。
首先说一下,这里为什么动态规划好:
不仅是因为求最优解,而且每个子问题不会影响父问题,毕竟硬币的个数是无限的,不是说子问题中用了一个,父问题就要少一个,举个例子,就是说你考语文100,和考数学100是没有关系的,但如果你的两个子问题此消彼长,无法同时达到最优解,就没办法用动态规划算法
步骤一:递归暴力解法
根据那个例子,凑11的零钱,其实就可以想象成,我们把每次的钱都分为各个面值1、2、5的,然后再在剩下的零钱中再分,直到最后结果为0,然后我们比较哪个分的次数少(其实就是层数越少越好)就可以了,如图所示
所以写出状态转移方程
这个状态方程的解答是:用一个硬币(价格未知)+价值为XX的硬币集
以上面那个例子来说就是 一个1(价格为 ci)元硬币 + 最小个数的10元(价格为 n - ci)硬币,如果是面值为2,3,则是一个2元硬币 + 最小个数的9元硬币,下面公式的1代表的是一个硬币,而非硬币面值
// k为一个数组,里面放的是各种面值
function coin(account, k) {
// 如果金额小于0,返回-1
if(account < 0) {
return -1
}
// 如果金额为0,则不需要硬币(但是要返回0,因为万一是之前算的,就要+1)
if(account == 0) return 0
// 因为是求最小,先把结果定为最大
let res = Number.MAX_VALUE;
// 其实就是一枚硬币+剩下的面额,因为不知道这一枚硬币面值多少,所以用循环
for(let value of k) {
// 通过递归获得剩余钱的次数
temp = coin(account - value, k);
// 如果发现次数为-1,则去下个循环
if(temp == -1) continue;
// 求最小
res = Math.min(res, temp + 1)
}
return res = res !== Number.MAX_VALUE ? res : -1
}
这个的时间复杂度是O(k * n^k),其中总共有 n^k 个节点,且内部要有循环,所以是 k * n^k
步骤二:带备忘录的递归算法
子问题同样有很多重复的,所以可以简便写成:
let money = {}
// k为一个数组,里面放的是各种面值
function coin(account, k) {
// 结束条件
if(account == 0) return 0;
if(account < 0) return -1;
let res = Number.MAX_VALUE;
for(let value of k) {
let temp = 0;
let other = account - value;
if(money[other.toString()]) {
temp = money[other.toString()]
} else {
// 递归
temp = coin(other,k)
money[other.toString()] = temp;
}
// 循环条件
if(temp == -1) continue;
res = Math.min(res, temp + 1)
}
return res !== Number.MAX_VALUE ? res : -1;
}
所以就少了很多重复的,时间复杂度变为O(k*n),毕竟循环去不掉
c、自底向上的非递归算法
因为最低的面额都是1元,所以可以得到的 dp 数组的个数应当有 account 个(这是在最多硬币次数的情况下),但是因为下标为0需要占位(这样也方便加),所以得到下表
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
let dp = new Array(amount + 1).fill(Infinity)
dp[0] = 0
for(let i = 1; i < dp.length; i++) {
for(let coin of coins) {
if(coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1)
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount]
};
凑零钱的升级——最低票价
/**
* @param {number[]} days
* @param {number[]} costs
* @return {number}
*/
var mincostTickets = function(days, costs) {
if(days.length === 0) {
return 0
}
let last = days[days.length - 1]
let dp = new Array(last + 1).fill(Infinity)
dp[0] = 0
// 其实在开始时买票和结束时买票最后都要买票,为了方便这里想成结束时买票
let count = 0
for(let i = 1; i < dp.length; i++) {
// 假设只有到了那一天才能买票
if(days[count] === i) {
// 可以是7天前买的票,也可以是今天
let sevenDay = i - 7 >= 0 ? i - 7 : 0
// 可以是30天前买的票,也可以是今天
let thrityDay = i - 30 >= 0 ? i - 30 : 0
dp[i] = Math.min(dp[i - 1] + costs[0], dp[i])
dp[i] = Math.min(dp[sevenDay] + costs[1], dp[i])
dp[i] = Math.min(dp[thrityDay] + costs[2], dp[i])
count++
} else {
dp[i] = dp[i - 1]
}
}
return dp[last]
};
c、动态规划解决博弈问题(以双人拿石子为例)
假设有很多堆石子,两人去取这些石子,每人一次只能取一堆,求最后两人最大的石子差
假设石子为 piles = [3, 9, 1, 2]
首先这里的dp应该用二维数组,dp[i][j]就表示从第i堆的石子到第j堆的石子,除此之外dp[i][j]里面可以存个对象放先手
(pre)和后手
(after)总共得到的石子数
dp[i][j].pre = max(拿左边 + dp[i + 1][j].after,拿右边 + dp[i][j - 1].after)
为啥是这样的呢,因为如果这一步是先手,那么下一步必定是后手,只不过是拿哪一个地方的后手而已,还有就是为什么先手要考虑下一步呢?因为如果先手有主动权,那肯定是要拿能让下一步和这一步都最大化的那一手,如果只拿这一步最大的,可能下一步被后手追上
而如果先手拿了左边,后手只能拿i + 1 到 j 的先手,如果先手拿了右边的先手,后手只能拿 i 到 j - 1 的先手
相当于先手左边:dp[i][j].after = dp[i + 1][j].pre
先手右边:dp[i][j].after = dp[i][j - 1].pre
如果i = j,那么 dp[i][j].pre = piles[i],dp[i][j].after = 0,毕竟就一堆,先手拿了,后手就没了
得到图
因为 dp[i][j] 的值与 dp[i + 1][j] 和 dp[i][j - 1] 有关,所以就要用备忘录存起来
最后结果是:通过 dp[0][3].pre - dp[0][3] 能够得到最大差值
function pile(piles) {
let length = piles.length;
// 先拿到二维数组,让他们初始化先手后手都为0,不要用fill创建带引用对象的东西,因为是浅克隆
let res = new Array(length)
// 把斜对角上的先赋值
for(let i = 0; i < length; i++) {
res[i] = new Array(length)
for(let j = 0; j < length; j++) {
if(i === j) {
res[i][i] = {pre: piles[i], after: 0};
} else {
res[i][j] = {pre: 0, after: 0};
}
}
}
// 这里有个特点,就是要斜着遍历,从(0,1)->(1,2)->(2,3)->(0,2)等
// 从这里可以看出每次第一个都是0开始,所以内层循环i从0开始,终止条件用k来控制一下
for(let k = 1; k <= length - 1; k++) {
for(let i = 0; i < length - k; i++) {
// 找的j纯粹靠暴力看
let j = i + k;
if(i === j) continue;
let left = piles[i] + res[i + 1][j].after
let right = piles[j] + res[i][j - 1].after
// 如果左边大
if(left > right) {
res[i][j].pre = left;
res[i][j].after = res[i + 1][j].pre
} else {
res[i][j].pre = right;
res[i][j].after = res[i][j - 1].pre
}
}
}
return res[0][length - 1].pre - res[0][length - 1].after;
}
d、0-1背包问题
有N件物品和一个容量为V的背包,每件物品都有自己的重量,且只有一件,只能选择放或不放,求能放进背包的物品最大价值
思路:这道题其实类似于之前说的凑硬币,不过因为这道题有两个状态会变化(价值和重量,有点像博弈里的先手和后手各自的堆数不同),所以是二维数组(代表在这几件物品的最高价值,如果创成一维数组,就不知道之前是否使用了某个背包),长度为N*V
// value是背包价值,weight是背包重量
function bug(value, weight, bugWeight) {
// dp[i][j] = x 表示前i个物品共重量j,最大价值为x
// 因为是两个状态(价值和重量),所以二维数组
// dp[0][...] 和 dp[...][0] 没啥用,主要是为了方便
let dp = new Array(weight.length + 1)
for(let i = 0; i < dp.length; i++) {
dp[i] = new Array(bugWeight + 1).fill(0)
}
for(let i = 1; i < dp.length; i++) {
for(let j = 1; j < bugWeight + 1; j++) {
// 如果当前物品重量超过设置的状态二的总重量,那么就不放
if(j < weight[i - 1]) {
dp[i][j] = dp[i-1][j]
continue
}
// 选择为当前物品是否要放,放就是dp[i-1][j-weight[i-1]]+weight[i-1]
// 不放就是dp[i-1][j]
dp[i][j] = Math.max(dp[i-1][j-weight[i-1]] + value[i-1], dp[i-1][j])
}
}
return dp[weight.length][bugWeight]
}
e、子集背包问题
子集背包问题其实就是分割俩子集,求和把它转换为背包问题
可以先把这个非空数组求和,然后看分割成两个子集的N个值,是否能装进容量为 SUM/2 的背包
所以这道题的状态是第几个数,这个数的值,对应背包问题的第几个物品,这个物品的重量
然后选择就是是否要选进去
function isMiddle(arr) {
let sum = arr.reduce((a, b) => a + b)
// 如何和为奇数,就不可能返回两个相等和的数组
if(sum % 2 !== 0) return false
sum /= 2
// dp[i][j] = false 表示前i个总和不为j,true就是为j
let dp = new Array(arr.length + 1)
for(let i = 0; i < dp.length; i++) {
dp[i] = new Array(sum + 1).fill(false)
// 如何和为0,那肯定是满了的,就是true(这一步很重要,否则之后就无法给dp赋值true)
dp[i][0] = true
}
for(let j = 1; j < sum + 1; j++) {
for(let i = 1; i < arr.length + 1; i++) {
if(arr[i - 1] > j) {
dp[i][j] = dp[i - 1][j]
continue
}
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - arr[i]]
}
}
return dp[arr.length][sum]
}
然后又因为其实这个里面没必要划分物品(毕竟物品自身没有属性),所以可以用一维数组表示dp
function isMiddle(arr) {
let sum = arr.reduce((a, b) => a + b)
// 如何和为奇数,就不可能返回两个相等和的数组
if(sum % 2 !== 0) return false
sum /= 2
// dp[i][j] = false 表示前i个总和不为j,true就是为j
let dp = new Array(arr.length + 1)
for(let i = 0; i < dp.length; i++) {
dp[i] = new Array(sum + 1).fill(false)
// 如何和为0,那肯定是满了的,就是true(这一步很重要,否则之后就无法给dp赋值true)
dp[i][0] = true
}
for(let i = 1; i < arr.length + 1; i++) {
for(let j = 1; j < sum + 1; j++) {
if(arr[i - 1] > j) {
dp[i][j] = dp[i - 1][j]
continue
}
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - arr[i]]
}
}
return dp[arr.length][sum]
}
f、完全背包问题
将N个物品各自有重量,放进一个重量为M的背包中,物品数量无限可重复,问有多少种放法就是完全背包问题
这里for循环的位置特别重要,因为是要将所有选择都加在一起才算到总共耗费方法的
function getCount(arr, amount) {
// dp[i] = x 表示当总共有i元有x种凑齐方法
let dp = new Array(amount + 1).fill(0)
// 当0元肯定有1种凑齐方法
dp[0] = 1
// 必须先循环钱的面额,再循环每一笔钱
// 因为我们是将选择这个硬币和不选这个硬币加在一起才得到该费用的几种方法
for(let value of arr) {
for(let i = 1; i < amount + 1; i++) {
if(i < value) {
dp[i] = dp[i - 1]
continue
}
dp[i] = dp[i] + dp[i - value]
}
}
return dp[amount]
}
g、编辑距离问题(其实就类似diff,但是vue2没有使用动态规划)
具体步骤见 经典动态规划指南
// 认为应该让str1变成str2
function getCounts(str1, str2) {
// dp[i][j] = x 表示0-i下标的arr1和0-j下标的arr2,需要操作x次
let dp = new Array(str1.length + 1)
for(let i = 0; i < dp.length; i++) {
dp[i] = new Array(str2.length + 1).fill(Number.MAX_VALUE)
}
// base case,把dp[0][...] 和 dp[...][0] 设置一下
for(let i = 0; i < dp.length; i++) {
dp[i][0] = i
}
for(let i = 1; i <= str2.length; i++) {
dp[0][i] = i
}
for(let i = 1; i <= str1.length; i++) {
for(let j = 1; j <= str2.length; j++) {
// 如果相等,操作数就用之前的
if(str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]
continue
}
// 这里有三种选择,增、删、替换,找到操作数使用最小的一种即可
// 增
// (在str1的i指针指向的后面插入一个字母,所以下次要把j指针往前移动,与i比较)
// dp[i][j] = dp[i][j - 1] + 1
// 删
// (把str1的i指针所指的删掉,这个时候i指向之前那个i指向的后一个的数,所以要把i往前移动)
// dp[i][j] = dp[i - 1][j] + 1
// 替换
// (换完之后两边指针都要往前移动)
// dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = min(dp[i][j - 1] + 1, dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1)
}
}
return dp[str1.length][str2.length]
}
function min(a, b, c) {
return Math.min(a, Math.min(b, c))
}
h、高楼扔鸡蛋
总共有1-N的N层楼,有k个鸡蛋,问最坏情况下,至少扔多少次知道鸡蛋恰好不碎
读题干,有两个词很重要,最坏和至少
先考虑最少那就可以使用二分查找法,而非线性查找法,毕竟线性查找是一层一层找的
最坏我们可以理解为是第i层楼碎不碎是看二分查找中 [0, i -1] 和 [i + 1, n] 哪儿个找的多
所以根据题干就可一直到我们 dp[i][j] = min(max())
// n是楼层数,k是鸡蛋数
function egg(n, k) {
// 这里有两个状态,鸡蛋数和楼层数,所以dp是二维数组
// dp[i][j] = x 表示如果搜索共i层恰好不碎,在最坏情况下,花j个鸡蛋最少扔x次确定
// 总共0层和没有鸡蛋时肯定不用扔都能确定
let dp = new Array(n + 1)
for(let i = 0; i < n + 1; i++) {
dp[i] = new Array(k + 1).fill(0)
}
// basecase:
// ①如果是0层或者0个鸡蛋一定只要0次(这个在上一步做了)
// ②如果是n层,m个鸡蛋,n>=m时最大次数为m,n<m时最大次数n
for(let i = 1; i < n + 1; i++) {
for(let j = 1;j < k + 1; j++) {
if(i >= j) {