一、集合的排列
給定乙個集合s,含有n個不重複的元素,輸出該集合元素的所有排列,leetcode對應題目為:列印所有排列的複雜度為o(n*n!),因為共有n!個不同的排列,列印每個排列的複雜度為o(n)。列印所有的排列一般採用深搜策略,先給出乙個常規的方法:
[cpp]view plain
copy
print?
void
perm(
intstart,
intend,vector<
int> &num,vectorint
>> &result)
for(int
i=start;i<=end;i++)
} 該**可看成是標準深搜的乙個具體例項:
[cpp]view plain
copy
print?
void
dfs(
inti,
intn,…)
ifi=n
print;
return
; for
j=i to k
dodfs(j,n,…);
深搜的第一步是判斷是否已經達到遞迴結束的條件,之後針對不同的解空間進行遞迴。在某些情況下,在遞迴之前也伴隨著剪枝,以加速演算法的執行速度。上述儲存集合排列的**思想很簡單:將起始位置的元素置成集合的每乙個元素,然後遞迴下乙個位置。該問題的解空間是乙個排列樹。例如,針對集合的解空間為:
圖1排列樹形式的解空間
排列樹形式的解空間有個規律,就是隨著遞迴深度的增加,每個節點的子節點個數都逐次減一,這也是為什麼排列演算法的複雜度是o(n!)。
此外,還有一種生成排列的方法,思想不太好懂,下面只給出**:
[cpp]view plain
copy
print?
void
perm(
intn,vector<
int> &num,vectorint
>> &result)
for(int
i=0;i
else
} }
演算法正確性證明的基本思想是:陣列長度n為奇數時,操作完成後,陣列不變,n為偶數時,操作完成後,陣列迴圈右移一位。
二、集合的k元素子集
給定乙個集合s,含有n個不重複的元素,生成所有的含有k個元素的子集,也即求組合數,leetcode對應題目為:
。該問題也可以通過深搜完成,在寫**之前先分析一下其解空間的構造。針對每乙個元素,我們都有兩種選擇:選擇該元素或者不選該元素,由此問題的解空間是一棵二叉樹。例如,對集合,選擇2個數的子集的解空間如下圖:
圖2 子集樹形式的解空間
根據上面的分析,我們可以知道求k個元素的子集,我們只需要判斷當前已經選擇的元素個數,如果已經選擇了k個元素,則找到乙個符合的子集,無需再遍歷子節點。如果尚未選擇k個元素,但是已經達到葉節點(n個元素已經判斷一遍),我們可以直接返回,這說明此次的遍歷沒有找到符合的子集。按照這個思路,**如下:
[cpp]view plain
copy
print?
void
com(
intdepth,
intn,
intk,vector<
int>& r,vectorint
> >& result)
if(depth==n)
return
; r.push_back(depth+1);
com(depth+1,n,k,r,result);
r.pop_back();
com(depth+1,n,k,r,result);
}
其中引數depth表示當前深度,引數n表示最大深度,引數k表示當前儲存的元素個數。如上所述,**有兩個終止條件:找到符合的子集,或者達到葉節點。遍歷時,只需要考慮兩種情況:選擇該元素或者不選該元素,然後遍歷下一層節點。
上述**是最原始的**,因為提交已經ac,所以無需再做優化,但實際上**還可以有很大的優化餘地。事實上,在某些路徑中,我們無需遍歷到葉節點也可以知道後面的遍歷是不符合要求的,如果需要新增的元素個數大於剩餘路徑上所有元素的個數,則即使新增剩餘所有的元素也不符合要求,此時我們可以直接進行剪枝,避免不必要的搜尋。如果不剪枝複雜度最壞為o(2n),剪枝之後的複雜度為o(nk)。
在《挑戰程式設計競賽》一書的157頁,有介紹一種非遞迴列舉所包含的所有大小為k的子集的方法。有興趣的讀者可自行閱讀,下面只給出**:
[cpp]view plain
copy
print?
vectorint
> > combine(
intn,
intk)
} result.push_back(r);
intx=comb&-comb,y=comb+x;
comb=((comb&~y)/x>>1)|y;
} return
result;
}
三、集合的所有子集
問題二只求集合的k元素子集,現在要求集合的所有子集,leetcode對應的題目為:該問題更加簡單,只需要將圖2的子集樹完整遍歷一遍即可,從根節點到葉節點的每一條路徑都表示乙個可能的子集。**如下:
[cpp]view plain
copy
print?
void
sub(
intdepth,vector<
int> &s,vector<
int>& r,vectorint
>>& result)
r.push_back(s[depth]);
sub(depth+1,s,r,result);
r.pop_back();
sub(depth+1,s,r,result);
}
與問題二的區別是,不需要考慮當前已經儲存的元素個數,只需要判斷是否已經到達葉節點。當然,我們也可以通過遍歷乙個數的二進位制來獲得集合的所有子集。
[cpp]view plain
copy
print?
vectorint
> > subsets(vector<
int> &s)
} result.push_back(r);
} return
result;
}
上述三個問題都可以通過深搜完美解決,後兩個問題還可以通過位運算解決,不管是深搜還是位運算都有比較相似的模式,希望通過上面的分析,大家能深刻理解深搜和位運算。
集合的排列與組合
introductory combinatorics fifth edition 學習筆記 排列和組合的區別在於放置和選擇是否和順序有關。集合的排列 n元素集合的r排列a n,r n n 1 n 2 n r 1 n n r 集合的組合 n元素集合的r組合c n,r n n r r 組合不考慮順序 問...
經典演算法 排列組合 N元素集合的M元素子集
題目說明 假設有個集合擁有n個元素,任意的從集合中取出m個元素,則這m個元素所形成的可能子集有那些?題目解析 假設有5個元素的集合,取出3個元素的可能子集如下 這些子集已經使用字典順序排列,如此才可以觀察出一些規則 如果最右乙個元素小於m,則如上面一樣的不斷加1 如果右邊一位已至最大值,則加1的位置...
排列和組合
排列組合計算公式 排列a n,m n n 1 n m 1 n!n m n為下標,m為上標,以下同 組合c n,m a n,m a m,m n!m!n m 問題 從1到n 包含 中選出m n個數,在下列情況下,有多少種組合?限制條件 1 無限制 2 各位數字公升序排列 3 不能有重複數字 4 各位數字...