在西電開源社區逛論壇時候,發現下面的排列組合問題有一個高效的迭代方式實現。
如何從 ['ABC', '12'] 得到 A1 A2 B1 B2 C1 C2
然后推廣到 ['abcd', '98h40ui', 'f', 'AY', ...] 這種一般情況
就是一個不定長的列表中包含多個項,每個項中只拿出來一個元素,然后列出所有可能的組合
容易得到,所有可能的組合方案總數為 \(len_1 \cdot len_2 \cdot ... \cdot len_k\)(\(len_i\)為第i個字符串的長度)
如何不用dfs方式去枚舉每個列表的選擇呢?
進制轉換問題
我們回想K進制的計數原理:K進制數的每一位數字為 0~K-1,如10進制 4321,數值大小表示從 0001 到 4321 之間編碼的個數。
要分離 4321 每一位上的數字,則按如下操作不停取模獲得余數(從低位到高位):
BASE = 10;
while(N) {
bit = N % BASE;
printf("%d", bit);
N /= 10;
}
進制轉化的問題也是如此,如將BASE改為2,則上述算法得到十進制數N的二進制表示。
變進制思想
對於該問題,每個列表的長度是不同的,可以設想我們使用一個變化進制的計數方式,將方案總數轉化成該進制的數。依次從小到大遍歷所有編碼,分離出編碼的每一位,即表示每個列表實際選取的下標。
變進制在全排列中也有運用,可以計算得到一個排列的字典序。全排列用到的階乘數系的 BASE為 k!。
例:有排列 35241 ,我們從數字2開始看,2右側有1個比它小的數字,數字3右側有2個,數字4右側有1個,數字5右側有3個,我們將這些逆序數倒着寫下來是:3,1,2,1,則該序列在我們這種排序方法中的位置序號是:
3x4!+1x3!+2x2!+1x1! = 83 注意,排序是從0開始計數的。
算法實現
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
// 迭代方式直接打印結果
void printCombination(const char *words[], int num) {
int totCnt = 1;
for (int i=0;i<num;i++) totCnt *= strlen(words[i]);
for (int code=0;code<totCnt;code++) {
int codeNow = code;
for (int i=0;i<num;i++) {
int base = strlen(words[i]);
int bit = codeNow % base;
codeNow /= base;
printf("%c", words[i][bit]);
}
printf("\n");
}
}
// 遞歸方式
void dfs(const char *words[], int num, int k, char now[]) {
if (k>=num) {
now[k] = '\0';
printf("%s\n", now);
return;
}
for(int i=0;i<strlen(words[k]);i++) {
now[k] = words[k][i];
dfs(words, num, k+1, now);
}
}
int main() {
const char *words[3] = {"ABC", "1234", "XY"};
printCombination(words, 3);
// char now[4];
// dfs(strings, 3, 0, now);
return 0;
}
Python實現
作為一門簡潔、優雅的語言,對於這種繁雜的問題當然有更好的寫法
Python標准庫itertools為我們提供了非常方便的排列組合操作,itertools 模塊提供的迭代器函數主要有三種類型
- 無限迭代器:生成一個無限序列
- 有限迭代器:接收一個或多個序列作為參數,進行組合、分組和過濾等
- 組合生成器:序列的排列、組合,求序列的笛卡兒積等
- product:笛卡爾積
- permutations:排列
- combinations:組合
- combinations-with-replacement:生成的組合包含自身元素
代碼實現:
from itertools import product
words = ["HOW", "ARE", "YOU"]
for item in product(*words):
print("".join(item))
--End--
