需求
需求描述起來很簡單,有這樣三個陣列:
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','黑色']
進入遞迴函式:
- 處理屬性陣列的下標0:假設我們在第一次迴圈中選擇了iphone XS 那此時我們有一個未完成的結果狀態,假設我們叫它prev,此時prev = ['iphone Xs']。
- 處理屬性陣列的下標1: 那麼就處理到colors陣列的了,並且我們擁有prev,在遍歷colors的時候繼續遞迴的去把prev 拼接成prev.concat(color),也就是['iphoneXs','黑色'] 這樣繼續把這個prev交給下一次遞迴
- 處理屬性陣列的下標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('~')