排列组合-组合

给出数据集合,从集合中选出 n 个元素,问可能出现的元素组合情况?

组合特点:内部元素是无序的,即[1,2,3] 和[3,2,1]是同一种组合情况

常用的求组合的方法有:递归法、转换法

递归法

存在 n 元素(无所谓重复与否),每次取出 m 个,问有多少种取法?

举个例子来说,存在[1,2,3,4,5]五个小球,每次可以取出 3 个球来放入 A 中,存在多少种取法?

可以按照如下步骤来取放

  1. 第一步:可以将 1 放入 A 中,那么还剩下 2、3、4、5

    • 第二步:可以将 2 放入 A 中,那么还剩下 3、4、5

      • 第三步:将 3 放入 A 中,此时完成一次组合
      • 第三步:将 4 放入 A 中,此时完成一次组合
      • 第三步:将 5 放入 A 中,此时完成一次组合
    • 第二步:可以将 3 放入 A 中,那么还剩下 4、5

      • 第三步:将 4 放入 A 中,此时完成一次组合
      • 第三步:将 5 放入 A 中,此时完成一次组合
    • 第二步:可以将 4 放入 A 中,那么还剩下 5

      • 第三步:将 5 放入 A 中,此时完成一次组合
  2. 第一步:可以将 2 放入 A 中,那么还剩下 3、4、5

    • 第二步:可以将 3 放入 A 中,那么还剩下 4、5

      • 第三步:可以将 4 放入 A 中,此时完成一次组合
      • 第三步:可以将 5 放入 A 中,此时完成一次组合

根据上述继续分析,A 是一个容器,我们每次可以从剩余的一堆小球中,取出一个来放入 A 容器中,例如:

  • 可以将 1、2、3、4、5 中 1 号取出放入 A 容器,此时小球剩余为 2、3、4、5 且容器 A 未满;
  • 可以从剩余小球中,取出 2 号放入容器,此处小球剩余为 3、4、5 且容器未满;
  • 最后我们从剩余小球中取出 3 号放入容器,此时小球剩余 4、5 且容器满了,即满足一次组合

由上面分析可以得出如下思路:

  1. 对于一堆小球,每个小球都有可能被取出,那么可以 循环 剩余的小球,让小球都有可能放入容器中
  2. 由于组合是无序的,那么先后放入小球不影响结果,例如先放入 1,再放入 2 和先放入 2 再放入 1 是一致,为了不产生重复计算,放小球时,不交换小球位置,从前往后依次放置
  3. 当把容器占满后,则完成一次组合

根据思路编码

function fullCombination(nums: number[], pick: number): number[][] {
  const ans: number[][] = []; // 结果
  recursion([], 0);
  return ans;

  function recursion(container: number[], startInd: number): void {
    // container 表示当前容器所包含的元素,当容器填满时,即完成一次组合
    if (container.length === pick) {
      ans.push(container);
      return;
    }

    // 已不可能达到pick的个数了,直接放弃
    if(startInd + pick > nums.length)return;

    for (let i = startInd; i < nums.length; i++) {
      const c = container.slice();
      c.push(nums[ i ]);
      recursion(c, i + 1);
    }
  }
}
复制代码

转换法

继续举上面的例子,将[1,2,3,4,5]放入容器内,我们使用 0,1 来来描述某个小球是否被选中,0 表示未选中,1 表示选中。例如五个元素全都不选中和全部选中为 [0,0,0,0,0]、[1,1,1,1,1],如果只选中三个元素的话,选中最右边的三个为[0,0,1,1,1],最左边的三个元素选中为[1,1,1,0,0],那么所有的组合可以描述为从 [1,1,1,0,0] ==> [0,0,1,1,1] 的转换过程

转换方法

假设起始位置是 [1,1,1,0,0],那么下一次选择应该是 [1,1,0,1,0],继续下次选择应该是 [1,1,0,0,1] … 直到[0,0,1,1,1]

根据上述找规律:

  1. 从数组尾部(index=nums.length-1)向头部(index=0)寻找第一个 1,0,记录此时 10 的索引为 [i,j];例如 [1,0,0,1,1] 第一个 1,0 索引表示为[0,1]
  2. 反转 1,0,即交换 i,j 两个位置的数;交换完后为 [0,1,0,1,1]
  3. 对于 j< index <= nums.length-1 的 1,全部移动到数组 j 的右侧,0 移动到最左侧;此时 j 为 1,在 1< index <=4 时,即在 index>j 的情况下,存在两个 1,则将这两个 1 移动到 j 的右侧 [0,1,1,1,0]
  4. 直到无法寻找到 1,0 为止
// 移动一步
function stepNext(picks: number[]): boolean {
  const first10Index = findFirst10(picks); // 步骤1 寻找从左至右的第一个1,0

  if (first10Index === -1) return false; // 步骤4 如果无法寻找到,则组合枚举结束

  swap(picks, first10Index, first10Index - 1); // 步骤2 交换i,j
  moveToLeft(picks, first10Index + 1, picks.length - 1); // 步骤3 移动1
  return true;
}

function findFirst10(nums: number[]): number {
  let prev = nums[ nums.length - 1 ];
  let first10Index = nums.length - 1;

  for (let i = nums.length - 2; i >= 0; i--) {
    if (prev === 0 && nums[ i ] === 1) break;
    first10Index = i;
    prev = nums[ i ];
  }
  return first10Index === 0 ? -1 : first10Index;
}

function moveToLeft(nums: number[], left: number, right: number) {
  let i = left, j = left;
  while (j <= right) {
    if (nums[ j ] === 1) {
      swap(nums, i, j);
      i++;
    }
    j++;
  }
}

function swap(nums: number[], i: number, j: number): void {
  const k = nums[ i ];
  nums[ i ] = nums[ j ];
  nums[ j ] = k;
}
复制代码

索引转换

上述只是描述了数对应的下标的组合,例如 [1,1,1,0,0],它是无法表示最后的数值的,需要将索引转换为数值

function transform(originNums: number[], picks: number[]): number[] {
  const v = [];
  for (let i = 0; i < picks.length; i++) {
    if (picks[ i ] === 1) {
      v.push(originNums[ i ]);
    }
  }
  return v;
}
复制代码

具体实现

function fullCombination(nums: number[], pick: number): number[][] {
  const ans: number[][] = [];

  const start = new Array(nums.length).fill(0);
  for (let i = 0; i < pick; i++){
    start[ i ] = 1;
  }

  do {
    ans.push(transform(nums, start));
  } while (stepNext(start))

  return ans;
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享