利用动态规划优化算法复杂度

208 阅读4分钟

这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

前言

这是阿源「Hecate」刷法成长之路的第四天。 想起之前部门内分享过的动态规划优化算法复杂度,重启算法学习之旅后翻找出来重新整理分析记录。


由来

  • 约瑟夫问题(猴子选王)
  • 链表模拟过程 分析复杂度O(n^2)
  • leetcode 模板抛弃链表
  • 类推法获取数学公式
  • 动态规划

认识复杂度

  • 空间复杂度
  • 时间复杂度
  • 最好情况复杂度
  • 最坏情况复杂度
  • 平均情况复杂度
  • 均摊情况复杂度

形式O()表示法

O(1) O(logn) O(n) O(nlogn) O(n^a) O(a^n) 复杂度图像对比

12.png

斐波那契数列算法的时间复杂度分析示例

// 递归
function fib0(n) {
  if(n <= 2) {
      return 1;
  }
  return fib0(n - 2) + fib0(n - 1);
}
// 循环
function fib1(n) {
  if( n <= 2 ) {
    return 1;
  }
  var first = 1;
  var second = 1;
  while(n-- > 2) {
    second += first;
    first = second - first;
  }
  return second;
}
// 尾递归优化
function fib2(n , r1 = 1 , r2 = 1) {
  if( n <= 2 ) {
    return r2;
  }
  return fib2(n - 1, r2, r1 + r2);
}

约瑟夫环

数据结构解法

约瑟夫问题分解

13.png

function Node(element) {
  this.element = element;
  this.next = null; // 单向
}
function LinkList(num) {
  var head = new Node(1);
  var p = head;
  for (var i = 2; i <= num; i++) {
    var temp = new Node(i);
    p.next = temp;
    p = temp;
  }
  p.next = head; // 环形
  return head;
}

function lastRemaining(n, m) {
  var current =  new LinkList(n);
  while (current.next.element != current.element) {
    // 数 m, 找 current
    for (var i = 1; i < m; i++) {
      var temp = current;
      current = current.next;
    }
    console.log(current.element);
    // 杀死 current
    temp.next = current.next;
    current = temp.next;
  }
  return current;
}

动态规划

动态规划(Dynamic Programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

适用情况
  • 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
  • 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  • 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
解法:

定义状态:设dp[i]为剩余i个玩家时剩余玩家的位置; 确定初态:那么dp[1]表示剩余1个玩家时,剩余玩家的位置,显然dp[1] = 0; 确定状态转移方程:那么dp[i - 1]为剩余i - 1个玩家时剩余玩家的位置;因为dp[i]过渡到dp[i - 1]向后数了m个数,所以dp[i]dp[i - 1]的转移方程初步为:dp[i] = dp[i - 1] + m;因为数m这个操作是环形结构,随着i的变化dp[i - 1] + m一定会大于i,所以最终的转移方程为:dp[i] = (dp[i - 1] + m) % i

var josephus = [1, 2, 3, 4, 5, 6]
function lastRemaining(n, m) {
  var dp = []; // 状态:dp[i]
  dp[0] = 0; dp[1] = 0; 
  for (var i = 1; i <= n; i++) {
    dp[i] = (dp[i - 1] + m) % i; // 状态dp[i] 转移到 dp[i - 1]
  }
  return josephus[dp.pop()];
}
lastRemaining(josephus.length, 3);
var josephus = [1, 2, 3, 4, 5, 6]
function lastRemaining(n, m) {
  var lastPos = 0;  // 记录上一次最后被删除数的位置
  for (var i = 1; i <= n; i++) {
    lastPos = (lastPos + m) % i;
  }
  return josephus[lastPos];
}
lastRemaining(josephus.length, 3);

动态规划解斐波那契数列

function fib(n) {
  var dp = []; // 设状态:dp[i]表示第i个斐波那契数
  dp[0] = 1; // 初态
  dp[1] = 1; // 初态
  for (var i = 3; i <= n; i++) {
    dp[i - 1] = dp[i - 2] + dp[i - 3]; // 状态转移方程 dp[i] 转移到 dp[i - 1]
  }
  return dp[n - 1];
}

线性代数特征方程、矩阵幂运算再优化斐波那契数列

复杂度:O(logn)