【转自:http://yishan.cc/blogs/gpww/archive/2009/11/01/1271.aspx】
一些常用集合算法——之组合生成
问题
在开发过程中常常需要处理集合,因此我写了一些常用算法,贴出大家提提意见。
本帖介绍组合生成算法。
分析
开一个数组,数组元素的值为1表示其下标代表的数被选中,为0则没选中。
首先初始化,将数组前m个元素置1,表示第一个组合选中前m个元素。
然后找到从左到右的第一个“10”组合,将其变为“01”组合,
同时将其左边的所有“1”全部移动到数组的最左端,例如:
1 1 1 0 0 //1,2,3
1 1 0 1 0 //1,2,4
1 0 1 1 0 //1,3,4
0 1 1 1 0 //2,3,4
1 1 0 0 1 //1,2,5
1 0 1 0 1 //1,3,5
0 1 1 0 1 //2,3,5
1 0 0 1 1 //1,4,5
0 1 0 1 1 //2,4,5
0 0 1 1 1 //3,4,5
解法
具体实现的时候,不需要每次“从左往右扫描”——我们可以这样考虑:每次将“10”变为“01”,可能产生新的翻转位置,那么就只需要记录新的翻转位置。
public static List<T[]> Combination<T>(IList<T> list, int n)
{
n = Math.Min(list.Count, n);
var combs = new List<T[ ]>();
var selection = new Dictionary<int>(n);//用于记录被选中元素的位置
Stack<int> pins = new Stack<int>();//记录翻转位置
for (int i = 0; i < n; i++) selection.TryAdd(i);//初始的时候选中前n个元素
if (n < list.Count) pins.Push(n - 1);//初始翻转位置
while (true)
{
<添加生成的组合>
if (pins.Count == 0) break;
var i = pins.Pop();
selection.Remove(i);//去掉i
var j = i + 1;
selection.TryAdd(j);//选中j
if (j + 1 < list.Count && !selection.ContainsKey(j + 1))
pins.Push(j);//增加翻转点
bool adjusted = false;
<将i左边这些连续的1全部移动到最左>
if (!adjusted && i - 1 >= 0 && selection.ContainsKey(i - 1))
pins.Push(i - 1);//增加翻转点
}
return combs;
}
其中:
<添加生成的组合>代码为:
int cnt = 0; var newComb = new T[ n ]; foreach (var index in selection) newComb[cnt++] = list[index]; combs.Add(newComb);
<将i左边这些连续的1全部移动到最左>代码为:
if (i >= 2) { int z = 0;//从左数0的数量 while (!selection.ContainsKey(z)) z++; if (z > 0 && z < i) { var m = Math.Min(z, i - z); for (int k = 0; k < m; k++) selection.TryAdd(k); //选中k for (int k = i - 1; k >= i - m; k--) selection.Remove(k); //去掉k pins.Push(i - z - 1); adjusted = true; } }
运行结果
输入abcdef,选3个:
1:abc 2:dab 3:dac 4:dbc 5:abe 6:ace 7:bce 8:ade 9:bde 10:cde 11:abf 12:acf 13:bcf 14:adf 15:bdf 16:cdf 17:aef 18:bef 19:cef 20:def
讨论
网上大多是“扫描算法”,这里用“翻转点”做了改进——不断构造新的翻转点,直到没办法再翻转。