前端電商 sku 的全排列演算法

HappyCodingTop發表於2021-10-19

需求

需求描述起來很簡單,有這樣三個陣列:

let names = ["iPhone",'iPhone xs']

let colors = ['黑色','白色']

let storages = ['64g','256g']

需要把他們的所有組合窮舉出來,最終得到這樣一個陣列:

[

  ["iPhone X", "黑色", "64g"],

  ["iPhone X", "黑色", "256g"],

  ["iPhone X", "白色", "64g"],

  ["iPhone X", "白色", "256g"],

  ["iPhone XS", "黑色", "64g"],

  ["iPhone XS", "黑色", "256g"],

  ["iPhone XS", "白色", "64g"],

  ["iPhone XS", "白色", "256g"],

]

由於這些屬性陣列是不定項的,所以不能簡單的用三重的暴力迴圈來求解了

思路

如果我們選用遞迴溯法來解決這個問題,那麼最重要的問題就是設計我們的遞迴函式

思路分解

以上文所舉的例子來說,比如我們目前的屬性陣列就是 names,colors,storages,首先我們會處理names陣列
很顯然對於每個屬性陣列 都需要去遍歷它 然後一個一個選擇後再去和下一個陣列的每一項進行組合

我們設計的遞迴函式接收兩個引數

  • index 對應當前正在處理的下標,是names還是colors 或者storage。
  • prev 上一次遞迴已經拼接成的結果 比如['iphoneX','黑色']

進入遞迴函式:

  1. 處理屬性陣列的下標0:假設我們在第一次迴圈中選擇了iphone XS 那此時我們有一個未完成的結果狀態,假設我們叫它prev,此時prev = ['iphone Xs']。
  2. 處理屬性陣列的下標1: 那麼就處理到colors陣列的了,並且我們擁有prev,在遍歷colors的時候繼續遞迴的去把prev 拼接成prev.concat(color),也就是['iphoneXs','黑色'] 這樣繼續把這個prev交給下一次遞迴
  3. 處理屬性陣列的下標2: 那麼就處理到storages陣列的了 並且我們擁有了 name+ color 的prev,在遍歷storages的時候繼續遞迴的去把prev拼接成prev.concat(storage) 也就是['iPhoneXS','黑色','64g'],並且此時我們發現處理的屬性陣列下標已經達到了末尾,那麼就放入全域性的結果變數res中,作為一個結果

編碼實現

let names = ['iphoneX',"iPhone XS"]

let colors = ['黑色','白色']

let storages = ['64g','256g']

let combine = function(...chunks){

    let res = []

    let helper = function(chunkIndex,prev){

        let chunk = chunks[chunkIndex]

        let isLast = chunkIndex === chunks.length -1

        for(let val of chunk){

            let cur = prev.concat(val)

            // ['iphoneX','黑色','64g'],['iphoneX','黑色','256g'],['iphoneX','白色','64g']

            if(isLast){

                // 如果已經處理到陣列的最後一項 則把拼接的結果放入返回值中

                res.push(cur)

            }else{

                helper(chunkIndex+1,cur)

            }

        }

    }

    //從屬性陣列下標為0開始處理

    // 並且此時的prev是一個空陣列

    helper(0,[])

    return res

}

console.log(combine(names,colors,storages));

["iphoneX", "黑色", "64g"]

["iphoneX", "黑色", "256g"]

["iphoneX", "白色", "64g"]

["iphoneX", "白色", "256g"]

["iPhone XS", "黑色", "64g"]

["iPhone XS", "黑色", "256g"]

["iPhone XS", "白色", "64g"]

["iPhone XS", "白色", "256g"]

萬能模板

給定兩個整數n和k 返回1...n中所有可能的k個數的組合
輸入: n = 4, k = 2
輸出:

[

  [2,4],

  [3,4],

  [2,3],

  [1,2],

  [1,3],

  [1,4],

]

解答

let combine = function (n,k){

    let ret = []

    let helper = (start,prev)=>{

        let len = prev.length

        if(len === k){

            ret.push(prev)

            return //[[1,2]]

        }

        for(let i = start;i<=n;i++){

            helper(i+1,prev.concat(i))

            //helper(2,[1]) [1,2]

            //helper(3,[1]), [1,3]

            //helper(4,[1]) [1,4]

            //helper(3,[2])  [2,3]

            //helper(4,[2])[2,4]

            // helper(4,[3])[3,4]

        }

    }

    helper(1,[])

    return ret

}
  • 可以看出這題和我們求解電商排列組合的程式碼竟然如此相似 只需要設計一個接受start排列起始位置,prev上一次拼接結果為引數的遞迴helper函式
  • 然後對於每一個起點下標start,先拼接上start位置對應的值,再不斷的再以其他剩餘的下標作為起點去做下一次拼接。
  • 當prev這個中間狀態的拼接陣列到達題目的要求長度k後 就放入結果陣列中

優化

  • 在這個解法中 有一些遞迴分支是明顯不可能獲取到結果的
  • 我們每次遞迴都會迴圈嘗試 <= n 的所有項去作為start 假設我們要求的陣列長度k=3,
  • 最大值n=4而我們以prev = [1], 再去以 n=4為start 作為遞迴的起點
  • 那麼顯然是不可能得到結果的,因為n=4的話只剩下4這一項可以拼接, 最多
  • 就拼成[1,4], 不可能滿足k=3的條件所以在進入遞迴之前
  • 就果斷的把這些廢枝給減掉 這就叫做減枝

let combine = function (n,k){

    let ret = []

    let helper = (start,prev)=>{

        let len = prev.length

        if(len === k){

            ret.push(prev)

            return

        }

        // 還有rest個位置待填補

        let rest = k - prev.length

        for(let i = start;i<=n;i++){

            if(n-i+1<rest){

                continue

            }

            helper(i+1,prev.concat(i))

        }

    }

    helper(1,[])

    return ret

}

相似題型

給定一個可能包含重複元素的整數陣列nums,返回該陣列所有可能的子集(冪集)
說明: 解題不能包含重複的子集

輸入: [1,2,2]

輸出:

[

  [2],

  [1],

  [1,2,2],

  [2,2],

  [1,2],

  []

]

剪枝的思路也是和之前相似的 如果迴圈的時候發現剩餘的數字不足以湊成目標長度 就直接剪掉

var subsetsWithDup = function(nums){

    let n = nums.length

    let res = []

    if(!n){

        return res

    }

    nums.sort()

    let used = {}

    let helper = (start,prev,target)=>{ //0,[],2

        if(prev.length === target){

            let key = genKey(prev)

            if(!used[key]){

                res.push(prev)

                used[key] = true

            }

            return

        }

        for(let i = start; i<= n;i++){

            let rest = n - i

            let need = target - prev.length

            if(rest<need){

                continue

            }

            helper(i + 1,prev.concat(nums[i]),target)//1,[1],2     2,[2],2  3,[2],2

                                                    // 2,[1,2],2,  2,[2,2],2

                                                    //1,[1,]3    2,[2],3    3,[3],3

                                                    //2,[1,2],3   3,[2,2],3  

                                                    //3,[1,2,3],3  



        }

    }

  for(let i = 1;i<=n;i++){

      helper(0,[],i) //0,[],3

  }

  return [[],...res]

}

function genKey(arr){

    return arr.join('~')

}

陣列總和

給定一個陣列candidates和一個目標數target,找出candidates中所有可以使數字和為target的組合candidates中的每個數字在每個組合中只能使用一次

說明:
所有數字(包含目標數) 都是正整數
解集不能包含重複的組合


示例 1:

輸入: candidates = [10,1,2,7,6,1,5], target = 8,

所求解集為:

[

  [1, 7],

  [1, 2, 5],

  [2, 6],

  [1, 1, 6]

]

示例 2:

輸入: candidates = [2,5,2,1,2], target = 5,

所求解集為:

[

  [1,2,2],

  [5]

]
思路
  • 與上面思路類似 只不過由於不需要考慮同一個元素重複使用的情況 每次的遞迴start起點應該是prevStart + 1。
  • 由於陣列中可能出現多個相同的元素 他們可能會生成相同的解 比如 [1,1,7]去湊8的時候,可能會用下標為0的1和7去湊8,也可能用下標為1的1和7去湊8
  • 所以在把解放入到陣列之前 需要先通過唯一的key去判斷這個解是否生成過,但是考慮到[1,2,1,2,7]這種情況去湊10,可能會生成[1,2,7]和[2,1,7]
  • 這樣順序不同但是結果相同的解,這是不符合題目要求的 所以一個簡單的方法就是 先把陣列排序後再求解 這樣就不會出現順序不同相同的解了
  • 此時只需要做簡單的陣列拼接即可生成key[1,2,7]->1~2~7
/**

 * @param {number[]}candidates

 * @param {number} target

 * @return {number[][]}

 */

let combinationSum2 = function(candidates,target){

    let res = []

    if(!candidates.length){

        return res

    }

    candidates.sort()

    let used = {}

    let helper = (start,prevSum,prevArr) =>{

        // 由於全是正整數  所以一旦和大於目標值了  直接結束本次遞迴即可

        if(prevSUm >target){

            return

        }

        // 目標值達成

        if(prevSum === target){

            let key = genkey(prevArr)

            if(!used[key]){

                res.push(prevArr)

                used[key] = true

            }

            return

        }

        for(let i = start;i<candidates.length; i++){

            // 這裡還是繼續從start本身開始  因為多個重複值是允許的

            let cur = candidates[i]

            let sum = prevSum + cur

            let arr = prevArr.concat(cur)

            helper(i + 1,sum,arr)

        }

    }

    helper(0,0,[])

    return res

}

let genKey = (arr)=> arr.join('~')

相關文章