日常生活的交流与学习

首页 新随笔 联系 管理

最常公共子序列

算法原理

代码 最常公共子序列.js

var f = longCommonSubsequence("abcde", "ace");
console.log(f);

function longCommonSubsequence(text1, text2) {
  let m = text1.length;
  let n = text2.length;
  let f = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
  console.log(f);
  for (i = 1; i <= m; i++) {
    let a1 = text1[i - 1];
    for (j = 1; j <= n; j++) {
      let a2 = text2[j - 1];
      if (a1 === a2) {
        f[i][j] = f[i - 1][j - 1] + 1;
      } else {
        f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
      }
    }
  }
  return f;
}

01背包问题

算法原理

原理图

image-20211123233552620

image-20211123233625531

视频讲解


代码

代码目录

01背包问题.html

  1. 前i -1个物品的组合价值

    image-20211123234203635

  2. 仍然是前i-1个物品的组合价值,准确的说去掉当前物品容量的前i-1个物品的组合价值

    image-20211123234347845

  3. 加上当前物品的价值

var dp = backPack(
  [
    [2, 3],
    [3, 4],
    [4, 5],
    [5, 6],
  ],
  8
);

console.log(dp);

function backPack(arr, capacity) {
  let [weight, value] = arr[0];
  // m表示背包中物品的数量
  let m = arr.length;
  let dp = new Array(m + 1).fill(0).map(() => new Array(capacity + 1).fill(0));
  for (i = 1; i <= m; i++) {
    let [weight, value] = arr[i - 1];
    for (j = 1; j <= capacity; j++) {
      if (weight > j) {
        dp[i][j] = dp[i - 1][j];
      } else {
        // 不放当前物品,就是前n个物品的最佳组合
        // 放当前物品,就是背包容量减去当前物品重量,即 j - weight
        //                然后再加上物品的价值,即 + value
        dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight] + value);
      }
    }
  }
  return dp;
}

输出效果

image-20211123233825713

贪心算法

分配饼干问题

题目描述

有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃
一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子
可以吃饱。

代码

贪心算法.js

  1. 当饼干的饱腹度大于小孩的饥饿度时,此时这个孩子吃饱了,于是孩子指针就得下移一位 child++;

    image-20211124085219691

  2. 当饼干的饱腹度不能满足孩子时,此时饼干不能满足,于是饼干指针就得往下移一位

    image-20211124085307868

let filledChildren = greedy([1, 3], [1, 2, 3]);
console.log(filledChildren);

/**
 * 贪心的策略是:尽量使用 饱腹度最小 的饼干满足 饥饿度最小 的孩子
 * 所以这里很显然的就会用到排序,然后从小到大的往上试
 */
function greedy(children, cookies) {
  // 对数组进行从小到大的排序
  children.sort((m, n) => m - n);
  cookies.sort((m, n) => m - n);
  let child = 0;
  let cookie = 0;

  while (child < children.length && cookie < cookies.length) {
    // 当饼干的饱腹度大于小孩的饥饿度时,cookies[cookie] >= children[child]
    //    此时这个孩子吃饱了
    //    于是孩子指针就得下移一位 child++;
    // 当饼干的饱腹度不能满足孩子时,此时饼干不能满足,
    //    于是饼干指针就得往下移一位,cookies[cookie++]
    if (cookies[cookie++] >= children[child]) {
      child++;
    }
  }
  return child;
}

无重叠区间

题目

  给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
  可以认为区间的终点总是大于它的起点。
  区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
  输入: [ [1,2], [2,3], [3,4], [1,3] ]
  输出: 1
解释:
  移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
  输入: [ [1,2], [1,2], [1,2] ]
  输出: 2
解释:
  你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
  输入: [ [1,2], [2,3] ]
  输出: 0
解释:
  你不需要移除任何区间,因为它们已经是无重叠的了。

解题思路

  本题解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题)。给你很多形如 [start, end] 的闭区间,请你设计一个算法,算出这些区间中最多有几个互不相交的区间。这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 [start, end] 表示开始和结束的时间,请问你今天最多能参加几个活动呢?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。
  解题思路分三步:  
 (1)从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中结束最早的(end 最小)。
 (2)把所有与 x 区间相交的区间从区间集合 intvs 中删除。
 (3)重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。
可以按每个区间的 end 数值升序排序:

算法原理

在这里插入图片描述

[注]图片来自公众号:labuladong

代码

贪心算法_无重叠区间.js

  1. 对这些区间[start,end],按照end进行升序排序

    image-20211124102453834

  2. 比较前后区间的end值,如果后面区间的start 小于前面区间的start值,说明区间重叠

    image-20211124102507015

let count = eraseOverlapIntervals([
  [3, 4],
  [2, 3],
  [1, 2],
  [1, 3],
]);
console.log(count);

function eraseOverlapIntervals(arr) {
  // 对区间[start,end],按照end进行升序排序
  arr.sort(([, n], [, y]) => {
    return n - y;
  });
  let count = 0;
  let [, base_end] = arr[0];
  for (let i = 1; i < arr.length; i++) {
    let [start, end] = arr[i];
    // 后面区间的开始小于前面区间的结束,start < base_end
    // 则区间重叠
    if (start < base_end) {
      count++;
    } else {
      base_end = end;
    }
  }
  return count;
}

递归和回溯

全排列问题

原理图一

46.全排列

原理图二

image.png

算法框架

回溯算法框架。解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」和「N 皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。

代码方面,回溯算法的框架:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。

代码

全排列.js

console.log(permute([1, 2, 3]));



function permute(nums) {
  // 结果集
  let res = [];
  // 路径
  let path = [];
  // 已使用的数字
  let used = [];

  backtracking(nums, used);

  function backtracking(nums, used) {
    // 路径的长度等于数组的长度,遍历完成,退出
    if (path.length === nums.length) {
      res.push(Array.from(path));
      return;
    }

    for (let i = 0; i < nums.length; i++) {
      // 如果该数字已经被使用,则跳过
      if (used[i]) continue;
      path.push(nums[i]); // 当前元素推入path
      used[i] = true; // 标志位设置为true,表示该元素以遍历
      backtracking(nums, used);
      path.pop();
      used[i] = false;
    }
  }
  return res;
}

合并排序

merge()原理图

  1. A1和A2比较,谁小,谁就写入A.

  2. 与此同时, 写入和被写入的下标加一移动

  3. 哨兵,最后用一个很大的数,用作哨兵,这样就是不用在去判断这两个数组的长度了.

    image-20211124152100731

Honeycam 2021-11-24 15-18-00

merge_sort()原理图

image-20211124152247684

代码

合并排序.js

function merge(Arr, start, mid, end) {
  let Arr1 = Arr.slice(start, mid);
  let Arr2 = Arr.slice(mid, end);
  // 数组的会后一位,添加一个哨兵
  Arr1.push(Number.MAX_SAFE_INTEGER);
  Arr2.push(Number.MAX_SAFE_INTEGER);
  // x 表示原数组的写入下标,所以起始值为 x =start
  // 循环结束的条件,就是 写入下标小于这个end,即 x<end
  // y 和 z 分别表示Arr1 和 Arr2 的下标
  for (let x = start, y = 0, z = 0; x < end; x++) {
    Arr[x] = Arr1[y] < Arr2[z] ? Arr1[y++] : Arr2[z++];
  }
  return Arr;
}

function merge_sort(Arr, start, end) {
  // 递归终止条件
  if (end - start < 2) return;
  // 取中间值,向下取整
  let mid = Math.ceil((start + end) / 2);
  merge_sort(Arr, start, mid);
  merge_sort(Arr, mid, end);
  merge(Arr, start, mid, end);
}

let Arr = [2, 3, 52, 23, 2, 456, 74];
merge_sort(Arr, 0, Arr.length);
console.log(Arr);

自然合并排序_待填写

待完成代码1


/**
 * 若数组a中元素为{4,8,3,7,1,5,6,2},则自然排好序的子数组段有{4,8},{3,7},{1,5,6},{2},
 * 经一次合并后得到2个合并后的子数组段{3,4,7,8}和{1,2,5,6},
 * 继续合并相邻排好序的子数组段,直至整个数组已排好序,最终结果{1,2,3,4,5,6,7,8}
 */
let Arr = [4, 8, 3, 7, 1, 5, 6, 2];


/

function merge_pass(Arr) {
  let sortedArrOfStart = [0];
  let sortedCount = 1;
  let begin = Arr[0];
  for (let i = 1; i < Arr.length; i++) {
    if (begin > Arr[i]) {
      sortedArrOfStart[sortedCount++] = i;
    }
    begin = Arr[i];
  }
  return sortedArrOfStart;
}

待完成代码2

let Arr = [4, 8, 3, 7, 1, 5, 6, 2];
let splitArr = [0, 2, 4, 7];
/**
 * 若数组a中元素为{4,8,3,7,1,5,6,2},则自然排好序的子数组段有{4,8},{3,7},{1,5,6},{2},
 * 经一次合并后得到2个合并后的子数组段{3,4,7,8}和{1,2,5,6},
 * 继续合并相邻排好序的子数组段,直至整个数组已排好序,最终结果{1,2,3,4,5,6,7,8}
 */
merge_nature(Arr, splitArr);

function merge_nature(Arr, splitArr) {
  let start = splitArr[0];
  let end = splitArr[1];
  let mid = splitArr[2];
  merge(Arr, start, mid, end);
  merge_nature(Arr);
  return Arr;
}

function merge(Arr, start, mid, end) {
  let Arr1 = Arr.slice(start, mid);
  let Arr2 = Arr.slice(mid, end);
  // 数组的会后一位,添加一个哨兵
  Arr1.push(Number.MAX_SAFE_INTEGER);
  Arr2.push(Number.MAX_SAFE_INTEGER);
  // x 表示原数组的写入下标,所以起始值为 x =start
  // 循环结束的条件,就是 写入下标小于这个end,即 x<end
  // y 和 z 分别表示Arr1 和 Arr2 的下标
  for (let x = start, y = 0, z = 0; x < end; x++) {
    Arr[x] = Arr1[y] < Arr2[z] ? Arr1[y++] : Arr2[z++];
  }
  console.log(Arr);
  return Arr;
}

posted on 2021-11-24 18:12  lazycookie  阅读(50)  评论(0编辑  收藏  举报