組合演算法題往往有多個變種,如求一個集合的全部子集以及部分組合問題,這篇文章對這些常見的面試題做個匯總,權當做個記錄,以免自己哪天忘了,難得到網路上去找。
一、求一個集合的全部子集
題目:給定一個集合s={a, b, c, d},試給出一個演算法輸出該集合的除了空集之外的全部子集。
分析:我們知道,一個集合的子集數目跟它的元素數目有關,集合元素數目為n,則子集數目為2^n。如包含兩個元素的集合s1 = {a, b},則它的子集有:空集、{a}、{b}、{a, b}一共四個。求子集問題即從包含n個元素的集合中選取m個元素的問題,m可以是1...n。我們先從頭掃描字串的第一個字元。針對第一個字元,我們有兩種選擇:一是把這個字元放到組合中去,接下來我們需要在剩下的n-1個字元中選取m-1個字元;二是不把這個字元放到組合中去,接下來我們需要在剩下的n-1個字元中選擇m個字元。這兩種選擇都很容易用遞迴實現。實現代碼如下:
void Combination(char* string){ if(string == NULL) return; int length = strlen(string); vector<char> result; for(int i = 1; i <= length; ++ i) { Combination(string, i, result); //從strng中選擇i個字元,結果儲存在result中 }} void Combination(char* string, int number, vector<char>& result){ if(number == 0) { vector<char>::iterator iter = result.begin(); for(; iter < result.end(); ++ iter) printf("%c", *iter); printf("\n"); return; } if(*string == '\0') return; result.push_back(*string); //選擇當前字元 Combination(string + 1, number - 1, result); result.pop_back(); //不選當前字元 Combination(string + 1, number, result);}
求子集還有一個更簡單的演算法,就是使用二進位。將數字從1——2^n-1迴圈,哪個位置位就選擇。假定集合字元為{a, b ,c },則
001 a
010 b
011 a b
...
依次類推就可。代碼如下:
void comb(char *str) { int len = strlen(str); //字串長度,如str = “abc”長度為3 int max = 1 << len; //字串自己的數目,如"abc"子集數目為8 for (int i=1; i<max; i++) { //輸出所有子集,這裡除去空集,所以從i=1開始輸出 int k = i; int index = 0; while (k > 0) { if (k & 1) { cout << str[index] << " "; } k >>= 1; index++; } cout << endl; //每次輸出一個子集換行 }}
二、人民幣問題
題目:人民幣有1元、2元、5元、10元、20元、50元、100元面值,試給出一個演算法找出和為100的人民幣組合(不包括100本身)。比如2張50的,或者2張50+2張20+1張10等。
分析:該題目與上面問題類似,可以採用組合的思路來解決。代碼如下:
/* * rmb.cpp * * Created on: 2012-8-28 * Author: shusheng */#include <iostream>using namespace std;#define N 6int w[N];int number_used[N];bool is_used[N];int countnum = 0;void init() {w[0] = 1;w[1] = 2;w[2] = 5;w[3] = 10;w[4] = 20;w[5] = 50;for (int i = 0; i < N; i++) {number_used[i] = 0;}}void rmb(int start_index, int left_weight) {if (left_weight == 0) {for (int i = 0; i < N; i++) {if (number_used[i] > 0)cout << w[i] << "元: " << number_used[i] << "張 ";}cout << endl;countnum++;return;}for (int i = start_index; i < N; i++) {if (left_weight >= w[i]) {number_used[i]++;rmb(i, left_weight - w[i]);number_used[i]--;}}}int main() {init();rmb(0, 100);cout << countnum << endl;return 0;}
函數rmb(start_index, left_weight)的功能定義是從start_index開始選擇,輸出最終和為left_weight的所有錢幣組合。這裡類似於完全背包問題,即每一樣錢幣都可以選擇多次,而背包容量大小為100,每樣物品的價值就是錢幣面值,只是這裡不是求最大值,而是總的組合數目。
當然這裡的代碼可以修改成另外一種形式,也許更好理解,如下所示:
void rmb(int start_index, int left_weight){ if (left_weight == 0) { for (int i = 0; i < N; i++) { if (number_used[i] > 0) cout << w[i] << "元: "<< number_used[i] <<"張 "; } cout << endl; return; } for (int i = start_index; i < N; i++) { int y = left_weight; while (y >= w[i]) { number_used[i]++; y -= w[i]; rmb(i+1, y); } number_used[i] = 0; }}
這裡的代碼for迴圈中就是先從1元開始選,這種情況完成後,再從5元開始選(即最小錢幣值為5),再是10、20等。
三、整數分解問題
給定一個正整數,試輸出所有的分解。如5=1+1+1+1+1 = 1+1+1+2=1+1+3=1+2+2=1+4
其實這個問題也可以參照上面的人民幣的例子,只是這裡的數組取值改成了1,、2、3、4...n-1。當然此題應該還有更好的解法,暫時以這個思路寫一下:
#include <iostream>#include <vector>using namespace std;#define NUMBER 10 //要分解的數為10static int cnt = 0; //分解數目vector<int> part; //用於儲存分解結果void generate_partition(int x, int i, int v[]){ if (x == 0) { //輸出 cout << ++cnt << ": "; for (int j=0; j<part.size(); j++) { cout << part[j] << " "; } cout << endl; return; } for (int j=i; j<NUMBER-1; ++j) { //輸出邏輯是先輸出包含1個v[j]的,然後是2個v[j]的... int select = v[j]; int c = 0, y=x; while (y >= select) { part.push_back(select); y -= select; c++; generate_partition(y, j+1, v); } while (c--) part.pop_back(); }}int main(){ int x = NUMBER; int v[NUMBER-1]; for (int i=0; i<NUMBER-1; i++) v[i] = i+1; generate_partition(x, 0, v); return 1;}
參考資料
http://zhedahht.blog.163.com/blog/static/2541117420114172812217/
http://blog.csdn.net/yysdsyl/article/details/4215232