动态规划

145 阅读6分钟

动态规划

斐波拉契数

leetCode

方法一: 递归

思路:

    • fn(x) = fn(x-1) + f(x-2)
  • 例如: 求fib(5)
    • fib(5) = fib(4) + fib(3)
    • fib(4) = fib(3) + fib(2)
    • fib(3) = fib(2) + fib(1)
/**
 * @param {number} n
 * @return {number}
 */
var fib = function(n) {
    if(n === 0) return 0
    if(n === 1) return 1
    return fib(n-1) + fib(n-2)
};

斐波拉契的性能优化

  • 求 fib(5)

    • fib(5) = fib(4) + fib(3)
    • fib(4) = fib(3) + fib(2)
    • fib(3) = fib(2) + fib(1)
  • 在上面运算中

    • fib(3)执行了两次, fib(2)执行了两次
    • 我们应该讲计算结果缓存起来, 避免重复计算
// 随着n增大,f(n)会超过Int32甚至Int64的取值范围,导致最终的返回值错误。
// 下面表示10的9次方+7 = 1000000007
// 这行代码定义了一个在算法和编程竞赛中常用的质数模数,用于处理大数运算和防止整数溢出
const mod = Math.pow(10,9)+7
const map = new Map()

function fib(n: number): number {
    let res = map.get(n)
    if(res) {
        return res
    }

    if(n === 0) res = 0
    if(n === 1) res = 1
    if(n > 1) res = (fib(n-1)+fib(n-2)) % mod
    map.set(n, res)
    return res
};

将业务分开写,如下

// 这行代码定义了一个在算法和编程竞赛中常用的质数模数,用于处理大数运算和防止整数溢出
const mod = Math.pow(10,9)+7 
const map = new Map()

function fibo(n: number): number {
    if(n === 0) return 0
    if(n === 1) return 1

    return (fib(n-1) + fib(n-2))%mod
}

function fib(n: number): number {
    if(map.has(n)) {
        return map.get(n)
    }
    
    const res = fibo(n)
    map.set(n, res)

    return res
};

方法二: 迭代

  • arr[5] = arr[4] + arr[3]
  • arr[x] = arr[x-1] + arr[x-2]
const mod = Math.pow(10,9)+7

// n:     [0, 1, 2, ...]
// value: [0, 1, 1, ...]
// index: [0, 1, 2, ...]
function fib(n: number): number {
    if(n <= 0) return 0
    if(n === 1) return 1

    const fibIterator = new FibIterator()

    for(let i=2; i<=n;i++) {
        fibIterator.next()
    }

    return fibIterator.v
};

class FibIterator {
    a = 0
    b = 1
    v = this.a + this.b

    next() {
        this.v = (this.a + this.b)%mod
        this.a = this.b
        this.b = this.v
    }
}

跳台阶

leetCode

今天的有氧运动训练内容是在一个长条形的平台上跳跃。平台有 num 个小格子,每次可以选择跳 一个格子 或者 两个格子。请返回在训练过程中,学员们共有多少种不同的跳跃方式。

分析

和斐波拉契数一摸一样的

f(x) = f(x-1) + f(x - 2)
  • 当台阶数为1时: f(1) = 1
  • 当台阶数量为2时: f(2) = 2
  • 当台阶数量为3时
    • 可以从 f(1) 跳两格到达 f(3)
    • 也可以从 f(2) 跳一格到达 f(3)
    • 所以跳到f(3)的方式: f(3) = f(2) + f(1)
    • 同理: f(n) = f(n-1) + f(n-2)

写法1: 递归写法

const mod = Math.pow(10, 9)+7
const map = new Map()

function trainWays(num: number): number {
    if(map.has(num)) {
        return map.get(num)
    }

    const res = trainWays2(num)
    map.set(num, res)
    return res
};

function trainWays2(num) {
    if(num <= 1) return 1
    if(num === 2) return 2
    return (trainWays(num-1) + trainWays(num-2))%mod
}

写法2: 迭代写法, 性能更好

const mod = Math.pow(10, 9) + 7;

// num:   [1个台阶, 2个台阶, 3个台阶, ...]
// value: [1种跳法, 2种跳法, 3种跳法, ...]
// index: [0,      1,      2, ...]
// num 和 index 的关系是 index = num -1
function trainWays(num: number): number {
  const iterator = new FibIterator();

  if (num <= 1) return 1;
  if (num === 2) return 2;

  for (let i = 2; i < num; i++) {
    iterator.next();
  }

  return iterator.v;
}

class FibIterator {
  a = 1;
  b = 2;
  v = this.a + this.b;

  next() {
    this.v = (this.a + this.b) % mod;
    this.a = this.b;
    this.b = this.v;
  }
}

快速排序

代码


function quickSort(arr: number[]): number[] {
  if (arr.length <= 1) return [...arr];
  const middle = arr.pop();
  const left = arr.filter(item => item <= middle);
  const right = arr.filter(item => item > middle);
  return [...quickSort(left), middle, ...quickSort(right)];
}

const res = quickSort([3, 5, 1, 7, 11, 9, 88, 2, 0, 1, 111, 22]);
console.log(res);

Edit 快速排序

分析

// 随便取一个数, 剩下的数小的方左边, 大的放右边, 排序完成
let arr = [8, 3, 5]

// 抽象公式
quickSort(arr) = [...quickSort(left), middle, ...quickSort(right)]

不同路径

leetcode.cn/problems/2A…

一个机器人位于一个 m x n **网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

分析

  • 如果第一步向右, 到达目的地有f(m-1, n)种方式
  • 如果第一步向下, 到达目的地有f(m, n-1)种方式
  • 所以到达目的地的方式: f(m, n) = f(m-1, n) + f(m, n-1)

方法一: 自上而下

const map = new Map()

function uniquePaths(m: number, n: number): number {
    const key = `${m}-${n}`
    if(map.has(key)) {
        return map.get(key)
    }

    let res = base(m, n)
    
    if(res === -1) {
        res = uniquePaths(m-1, n) + uniquePaths(m, n-1)
    }
    map.set(key, res)
    return res
};

function base(m, n) {
    if(m === 0) return 0
    if(n === 0) return 0
    if(m === 1) return 1
    if(n === 1) return 1
    return -1
}

方法二: 自下而上

/**
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var uniquePaths = function(m, n) {
    const res = []
    
    for(let i=0; i<m; i++) {
        res[i] = []
        for(let j=0; j<n; j++) {
            res[i][j] = 1
        }
    }

    for(let i=1; i<m; i++) {
        for(let j=1; j<n; j++) {
            res[i][j] = res[i][j-1] + res[i-1][j]
        }
    }

    return res[m-1][n-1]
};

背包问题

image.png

规律

  • 上图中 全为0的行全为0的列 忽略不看
  • 第0排是葡萄🍇
  • 第1排是矿泉水
  • 第2排是西瓜🍉
  • 求出第0排数据 -> 就能求出第1排数据
  • 求出第1排数据 -> 就能求出第2排数据
  • 求出第i排数据 -> 就能求出第i+1排数据
const x = 6; // 背包可承受重量
const w = [2, 3, 4]; // 物品重量
const v = [3, 5, 6]; // 物品价格
const i = w.length-1 // i表示第i行

// 如果不往背包里放西瓜, 那么背包的最大价值为
const res1 = f(6, i-1)
// 如果往背包里放西瓜, 
// 背包的最大价值 = 西瓜的价值 + 剩余空间的最大价值
const res2 = f(6-🍉重量, i-1) + 🍉价值

// 最终结果去两者最大值
f(x, i) = res1 > res2 ? res1 : res2

自上而下

function packages( bagWeight, index, values, weights ) {
  if (bagWeight <= 0) return 0;

  if (index === 0) {
    // 0 生万物
    // - 求出第`0`排数据 -> 就能求出第`1`排数据
    // - 求出第`1`排数据 -> 就能求出第`2`排数据
    // - 求出第`i`排数据 -> 就能求出第`i+1`排数据
    return bagWeight < weights[0] ? 0 : values[0];
  }

  // 不放入🍉的最大价值
  const res1 = packages(bagWeight, index - 1, values, weights);

  // 背包太小, 无法放入🍉
  if (bagWeight - weights[index] < 0) return res1;

  const res2 = // 放入🍉的最大价值
    packages(bagWeight - weights[index], index - 1, values, weights) +
    values[index];

  return res1 > res2 ? res1 : res2;
}

测试

const bagWeight = 6;
// 答案: 13
const weights1 = [2, 5, 1, 4, 3];
const values1 = [5, 10, 3, 6, 3];
// 答案: 9
const weights2 = [2, 3, 4];
const values2 = [3, 5, 6];

const res1 = packages(bagWeight, weights1.length - 1, values1, weights1);
const res2 = packages(bagWeight, weights2.length - 1, values2, weights2);
console.log(res1, res2); // 13, 9

自下而上

【动态规划中的01背包问题你清楚嘛?搞懂它再刷题!【渡一教育】】 www.bilibili.com/video/BV1qy…

【【自制】01背包问题算法动画讲解】 www.bilibili.com/video/BV1pY…

最长递增子序列

LeetCode

自下而上

抽象公式

res2 = cp(res1, 1)

res1 = [
    [2],
    [2, 3]
]

res2 = [
    [1],
    [2, 3],
]

遍历到2

arr[2, 3, 1, 5, 4]
res = [
    [2]
]

遍历到3

arr[2, 3, 1, 5, 4]
res = [
    [2],
    [2, 3]
]

遍历到1

arr[2, 3, 1, 5, 4]
res = [
    [1],
    [2, 3],
]

遍历到5

arr[2, 3, 1, 5, 4]
res = [
    [1],
    [2, 3],
    [2, 3, 5],
]

遍历到4

arr[2, 3, 1, 5, 4]
res = [
    [1],
    [2, 3],
    [2, 3, 4],
]

实现

const arrInput = [2,3,1,5,6,8,7,9,4]
const res = lengthOfLIS(arrInput)
console.log(res)

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    let res = [
        [nums[0]]
    ]

    for(let i=1; i<nums.length; i++) {
        res = cp(res, nums[i])
    }

    return res.pop().length
};


function cp(arr, num) {
    const index = find(arr, num)
    if(index === 0) {
      arr[0] = [num]
    }else {
        arr[index] = [...arr[index-1], num]
    }
    return arr
}

function find(arr, num) {
    for(let i = 0; i<arr.length; i++) {
        const nums = arr[i]
        const n = nums[nums.length-1]
        if(n >= num) return i
    }
    return arr.length
}