练习
练习一: Fibancci数列
查找斐波纳契数列中第 N 个数。(N 从 0 开始),所谓的斐波纳契数列是必须满足下面两个条件:
- 前2个数是 0 和 1 。
- 第 i 个数是第 i-1 个数和第i-2 个数的和。
【示例】斐波纳契数列的前10个数字是: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
根据上面的描述,我们先写一个未优化的最简单版本,代码如下:
function Fibancci(n: number): number {
if (n < 2) {
return n
}
return Fibancci(n - 1) + Fibancci(n - 2)
}
console.log(Fibancci(20));
//output:
//6765
把这种暴力递归的解法画一张图出来:
是不是有重复的计算?像这样的二叉树的节点总数是指数级的,所以所有子问题的个数为 (2^n) 噢,这么做会导致时间复杂度爆炸,因为时间复杂度也变成了 O(2^n)
缓存剪枝策略
function Fibancci(n: number,cache:Map<number,number>): number {
if(cache.has(n)){
return cache.get(n)
}
let ans = 0
if (n < 2) {
ans = n
}else{
ans = Fibancci(n - 1,cache) + Fibancci(n - 2,cache)
}
cache.set(n,ans)
return ans
}
function main(num:number){
const cache = new Map();
return Fibancci(num,cache)
}
console.log(main(20));
//output:
//6765
这种带缓存的递归解法是针对暴力递归的解法做的优化,优化的操作其实就是对那颗指数级的二叉树进行剪枝,剪枝会减少重复的树节点(减少了重复的计算)。
红色实线部分圈着的是被剪枝的节点,是不是剪掉了所有的重复树节点?没错是的,所有重复的子问题都被干掉了,时间复杂度骤减。 它时间复杂度也从O(n^2)降低到了O(n)
练习二:机器人必须走 K 步,最终能来到P位置的方法有多少种
假设有排成一行的N个位置,记为1N,N一定大于或等于2开始时机器人在其中的start位置上(M 一定是 1N 中的一个)
如果机器人来到1位置,那么下一步只能往右来到2位置;
如果机器人来到N位置,那么下一步只能往左来到 N-1 位置;
如果机器人来到中间位置,那么下一步可以往左走或者往右走;
规定机器人必须走 K 步,最终能来到aim位置(aim也是1~N中的一个)的方法有多少种 ?
给定四个参数 N、start、K、aim,返回方法数。
/**
* @description: 从curPos位置出发,还剩restStep要走,返回可以走到aim的方法数
* @param {number} range 机器人可以走的范围 1——range(固定参数)
* @param {number} curPos 当前所在的位置
* @param {number} restStep 还剩restStep步可以走
* @param {number} aim 终点位置 固定参数
* @return {*} 方法数
*/
function robotWalk(range:number,curPos:number,restStep:number,aim:number): number {
if(restStep==0){
//剩余步数为0的时候如果当前位置是重点位置返回一种方法数
return curPos==aim ?1:0;
}
let ways = 0;
if(curPos==1){
//在1位置只能向右走
ways = robotWalk(range,curPos+1,restStep-1,aim)
}else if(curPos==range){
//在最后位置只能往左走
ways = robotWalk(range,curPos-1,restStep-1,aim)
}else{
//其余位置可以左右两个方向走
//方法1 向右走
let p1 = robotWalk(range,curPos+1,restStep-1,aim)
//方法2 向左走
let p2 = robotWalk(range,curPos-1,restStep-1,aim)
ways = p1+p2
}
return ways
}
function main(N:number,start:number,K:number,aim:number){
return robotWalk(N,start,K,aim)
}
console.log(main(7,2,4,4));
//output:
//4
以上方式是纯暴力,上面方式的可变参数就两个,当前所在的位置(curPos)和剩余步数(restStep),假设当前位置在7,剩余步数为10,我们根据下图会发现后续有重复的过程,而且会有很多,所以我们可以用缓存剪枝策略来进行优化,后续也称记忆化搜索
记忆化搜索方式
/**
* @description: 从curPos位置出发,还剩restStep要走,返回可以走到aim的方法数
* @param {number} range 机器人可以走的范围 1——range(固定参数)
* @param {number} curPos 当前所在的位置
* @param {number} restStep 还剩restStep步可以走
* @param {number} aim 终点位置 固定参数
* @param Map<string,number> cache 缓存已经算过方法数的表格
* @return {*} 方法数
*/
function robotWalk(range:number,curPos:number,restStep:number,aim:number,cache:Map<string,number>): number {
const cacheKey = `${curPos}-${restStep}`
//如果缓存中存在,直接获取该值
if(cache.has(cacheKey)){
return cache.get(cacheKey)
}
if(restStep==0){
//剩余步数为0的时候如果当前位置是重点位置返回一种方法数
return curPos==aim ?1:0;
}
let ways = 0;
if(curPos==1){
//在1位置只能向右走
ways = robotWalk(range,curPos+1,restStep-1,aim,cache)
}else if(curPos==range){
//在最后位置只能往左走
ways = robotWalk(range,curPos-1,restStep-1,aim,cache)
}else{
//其余位置可以左右两个方向走
//方法1 向右走
let p1 = robotWalk(range,curPos+1,restStep-1,aim,cache)
//方法2 向左走
let p2 = robotWalk(range,curPos-1,restStep-1,aim,cache)
ways = p1+p2
}
//设置改制到缓存表中
cache.set(cacheKey,ways)
return ways
}
function main(N:number,start:number,K:number,aim:number){
let cache = new Map()
return robotWalk(N,start,K,aim,cache)
}
console.log(main(7,2,4,4));
//output:
//4
动态规划方式
通过纯暴力的方式后,我们可知可变参数就两个,当前所在的位置(curPos)和剩余步数(restStep),
curPos的变化范围是1-N,restStep的变化范围是0-K,根据两个可变参数我们绘制一个表格
根据baseCase我们可以知道当restStep为0的时候,只有curPos==aim时方法数才是1
然后让我们分析普遍位置依赖,当curPos==1的时候,他的依赖如下图
当curPos==N的时候,他的依赖如下图
其余位置的依赖如下
而我们最终要获取的位置是(4,2)这个位置
根据依赖关系我们可以把第二行的值求出来
从而我们可以求出所有的值
最终得到的结果是4,详细代码如下:
function main(N: number, start: number, K: number, aim: number) {
//创建一个(K+1)*(N+1)的二位数组,并且每个位置的值都是0
let dp = Array.from(new Array(K + 1), () => new Array(N + 1).fill(0))
//根据baseCase可得
dp[0][aim] = 1
for (let i = 1; i <= K; i++) {
for (let j = 1; j <= N; j++) {
if (j == 1) {
dp[i][j] = dp[i - 1][j + 1]//依赖右上角位置
} else if (j == N) {
dp[i][j] = dp[i - 1][j - 1] //依赖左上角位置
} else {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1]//依赖右上角+左上角
}
}
}
return dp[K][start]
}
console.log(main(7, 2, 4, 4));
//output:
//4
练习三: A,B玩家从左右两边拿纸牌,返回最后获胜者的分数
给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数
【示例1】 arr=[1,2,100,4]
开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A...
如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A...
玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。
【示例2】 arr=[1,100,2]
开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。
暴力递归分析过程:假设我先拿牌,则我可以拿左侧的牌或者拿右侧的牌,这时我会有两种方案,因为我绝顶聪明,我肯定获取两种方案中最大的那个,然后轮到对手拿牌,对手也可以选择拿左侧或者右侧的牌,不管对手拿哪个牌,只要牌数大于1,则后续都会有我拿牌得到的分数,对手也决定聪明,肯定给我最小的值,分析代码如下:
/**
* @description: 我的拿牌时刻,从[L...R]范围上获取的最好成绩
* @param {number} pokers 所有的牌 (固定参数)
* @param {number} L 左侧可以抽的位置
* @param {number} R 右侧可以抽的位置
* @return {*} 我获取的最好成绩
*/
function meMoment(pokers: number[], L: number, R: number) {
//如果只剩一张牌,由于是我拿牌时刻,所以直接拿走
if (L == R) {
return pokers[L]
}
/**如果剩余牌数大于1张*/
//我拿左侧的牌
let p1 = pokers[L] + opponentMoment(pokers, L + 1, R)
//我拿右侧的牌
let p2 = pokers[R] + opponentMoment(pokers, L, R - 1)
return Math.max(p1, p2)
}
/**
* @description: 对手拿牌时刻,在[L...R]对手让我获取的成绩,对手绝顶聪明,让我获取对他有利的
* @param {number} pokers 所有的牌 (固定参数)
* @param {number} L 左侧可以抽的位置
* @param {number} R 右侧可以抽的位置
* @return {*} 对手拿牌后,我获取的成绩
*/
function opponentMoment(pokers: number[], L: number, R: number): number {
//如果只剩一张牌,由于是对手拿牌时刻,我得到的成绩是0
if (L == R) return 0;
//对手拿左侧的牌,后续我获取的最好成绩
let p1 = meMoment(pokers, L + 1, R)
//对手拿右侧的牌,后续我获取的最好成绩
let p2 = meMoment(pokers, L, R - 1)
return Math.min(p1, p2)
}
function main(pokers: number[]) {
//我先拿牌
let p1 = meMoment(pokers, 0, pokers.length - 1)
//对手先拿牌
let p2 = opponentMoment(pokers, 0, pokers.length - 1)
return Math.max(p1, p2)
}
console.log(main([1,5,233,7]));
//output:
//234
记忆化搜索方式
我们画图分析下具体过程
会发现5,233会有重复的部分,这时候我们就可以用记忆化搜索方式进行剪枝来提升速度了,代码如下
/**
* @description: 我的拿牌时刻,从[L...R]范围上获取的最好成绩
* @param {number} pokers 所有的牌 (固定参数)
* @param {number} L 左侧可以抽的位置
* @param {number} R 右侧可以抽的位置
* @param {number} meCache 针对于我的数据的缓存
* @param {number} opponentCache 针对于对手的数据的缓存
* @return {*} 我获取的最好成绩
*/
function meMoment(pokers: number[], L: number, R: number,meCache:Map<string,number>,opponentCache:Map<string,number>) {
const cacheKey = `${L}-${R}`
if(meCache.has(cacheKey)){
return meCache.get(cacheKey)
}
//如果只剩一张牌,由于是我拿牌时刻,所以直接拿走
if (L == R) {
return pokers[L]
}
/**如果剩余牌数大于1张*/
//我拿左侧的牌
let p1 = pokers[L] + opponentMoment(pokers, L + 1, R,meCache,opponentCache)
//我拿右侧的牌
let p2 = pokers[R] + opponentMoment(pokers, L, R - 1,meCache,opponentCache)
let res = Math.max(p1, p2)
meCache.set(cacheKey,res)
return res
}
/**
* @description: 对手拿牌时刻,在[L...R]对手让我获取的成绩,对手绝顶聪明,让我获取对他有利的
* @param {number} pokers 所有的牌 (固定参数)
* @param {number} L 左侧可以抽的位置
* @param {number} R 右侧可以抽的位置
* @param {number} meCache 针对于我的数据的缓存
* @param {number} opponentCache 针对于对手的数据的缓存
* @return {*} 对手拿牌后,我获取的成绩
*/
function opponentMoment(pokers: number[], L: number, R: number,meCache:Map<string,number>,opponentCache:Map<string,number>): number {
const cacheKey = `${L}-${R}`
if(opponentCache.has(cacheKey)){
return opponentCache.get(cacheKey)
}
//如果只剩一张牌,由于是对手拿牌时刻,我得到的成绩是0
if (L == R) return 0;
//对手拿左侧的牌,后续我获取的最好成绩
let p1 = meMoment(pokers, L + 1, R,meCache,opponentCache)
//对手拿右侧的牌,后续我获取的最好成绩
let p2 = meMoment(pokers, L, R - 1,meCache,opponentCache)
let res = Math.min(p1, p2)
opponentCache.set(cacheKey,res)
return res
}
function main(pokers: number[]) {
let meCache = new Map()
let meCache1 = new Map()
let opponentCache = new Map()
let opponentCache1= new Map()
//我先拿牌
let p1 = meMoment(pokers, 0, pokers.length - 1,meCache,opponentCache)
//对手先拿牌
let p2 = opponentMoment(pokers, 0, pokers.length - 1,meCache1,opponentCache1)
return Math.max(p1, p2)
}
console.log(main([1,5,233,7]));
//output:
//234
动态规划方式
通过暴力递归的方式,我们可知一共就两个可变参数L,R
L的变化范围是0-(len-1),R的变化范围也是0-(len-1),所以我们构建动态规划表如下,由于R>L,所以画x的位置都是无效位置
根据baseCase我们可以得到L==R上的两个动态规划表的值如下:
两张表的0,3位置的最大值即是我们要求得的位置
接下来我们分析普遍位置依赖,依赖关系如下所示
根据依赖关系,我们求得第一个斜角的值
根据依赖关系,我们求得所有的位置关系,最终结果为12
分析代码如下:
function main(pokers: number[]) {
let len = pokers.length;
//构建len*len的动态规划表格
let meDp = Array.from(new Array(len),()=>new Array(len))
let opponentDp = Array.from(new Array(len),()=>new Array(len))
//根据baseCase 设定基础条件
for (let i = 0; i < len; i++) {
meDp[i][i] = pokers[i]
opponentDp[i][i] = 0
}
//根据普遍依赖关系斜角求每个位置的值
for (let start = 1; start < len; start++) {
let L = 0;
let R = start;
while(R<len){
meDp[L][R] =Math.max(pokers[L]+opponentDp[L+1][R],pokers[R]+opponentDp[L][R-1])
opponentDp[L][R] =Math.min(meDp[L+1][R],meDp[L][R-1])
L++;
R++
}
}
return Math.max(meDp[0][len-1],opponentDp[0][len-1])
}
console.log(main([1,5,233,7]));
//output:
//234