只要思想不滑坡,算法总比困难多

674 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情

在复习算法设计的时候,好好回顾了一下几种常见的算法思想:分治发,动态规划,贪心法,回溯法。

分治法

基本思想将求出的小规模的问题的解合并为一个更大规模的问题的解,自底向上逐步求出原来问题的解。

image.png

通过小规模的问题求解,最终得到我们的最终答案,分而治之。这种题目就很常见了,比如上一期的手写快速排序就是分治的思想,以及爬楼梯问题,还有二分查找

手写一下二分:

let arr = [1, 2, 3, 5, 4, 6, 6];
function binarySearch(arr, left, right, target) {
  let mid = Math.trunc((left + right) / 2);
  if (arr[mid] === target) {
    return mid;
  } else if (arr[mid] > target) {
    return binarySearch(arr, mid + 1, right);
  } else {
    return binarySearch(arr, left, mid - 1);
  }
}
console.log(binarySearch(arr,0,arr.length-1,5));

动态规划

动态规划的基本思想如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而提高计算效率。也就是说从已经算出来的答案可以推出下一个答案,基本步骤如下:

  • 找出最优解的性质,并刻划其结构特征。
  • 递归地定义最优值。
  • 以自底向上的方式计算出最优值。
  • 根据计算最优值时得到的信息,构造最优解。

这个可以解决的问题就很多了,常见的背包问题,机器人选择路径问题以及我们起前面分治法可以解决的,在动态规划中有时候也是可以解决的,如爬楼梯问题就可以用动态规划来解决下:

//n为楼梯数
var climbStairs = function (n) {
  let dp = new Array(n + 1).fill(0);
  dp[0] = 1;
  dp[1] = 1;
  dp[2] = 2;
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[n];
};

贪心法

顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。希望贪心算法得到的最终结果是整体最优的。贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。基本思想就是每次都找局部最优推出全局最优

需要注意, 贪心算法和动态规划算法都要求问题具有最优子结构性质,这是2类算法的一个共同点。但是,对于具有最优子结构的问题应该选用贪心算法还是动态规划算法求解?是否能用动态规划算法求解的问题也能用贪心算法求解?

我们拿01背包来说,贪心选择不能得到最优解。在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题。这正是该问题可用动态规划算法求解的另一重要特征。

适合贪心做的题目也是很多,如经典的股票问题

var maxProfit = function(prices) {
  let max=0,min=prices[0];
  for(let i=1;i<prices.length;i++){
      if(prices[i]>min){
          if(max<prices[i]-min){
              max=prices[i]-min;
          }
      }else{
          min=prices[i];
      }
  }
  return max;
};

回溯法

有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。这个能解决的问题就多了,我们就写下最常见的排列组合问题:

求长度为2的组合

let arr=[1,2,3,4,5];
let tf=new Array(arr.length).fill(false);
let tem=[];
let res=[];
function dfs(k){
  if(tem.length===2){
    res.push([...tem])
  }
  for(let i=k;i<arr.length;i++){
    if(!tf[i]){
      tem.push(arr[i]);
      tf[i]=true;
      dfs(i+1);
      tem.pop();
      tf[i]=false;
    }
  }
}
dfs(0);
console.log(res)

求长度为2的排列数

let arr=[1,2,3,4,5];
let tf=new Array(arr.length).fill(false);
let tem=[];
let res=[];
function dfs(){
  if(tem.length===2){
    res.push([...tem])
    return;
  }
  for(let i=0;i<arr.length;i++){
    if(!tf[i]){
      tem.push(arr[i]);
      tf[i]=true;
      dfs(i+1);
      tem.pop();
      tf[i]=false;
    }
  }
}
dfs(0);
console.log(res)