问题
给出一个数组,比如 {1,2,3,4},请求出数组的所有子集(1)?给出一个存在重复元素的数组,比如 {1,2,2,3,4},请求出数组的所有子集(2)?请求出所有子集并且不允许出现重复子集(3)?
准备方法
/// <summary> /// 列表深拷贝 /// </summary> public static List<T> Clone<T>(this List<T> source) { List<T> newList = new List<T>(source.Count); foreach (var item in source) { newList.Add(item); } return newList; }
/// <summary> /// 比较两个列表是否等同,不考虑列表元素的顺序 /// 比如 {1,2,3,4}与{2,1,4,3}比较返回true. {2,3}与{1,2}比较返回false /// </summary> public static bool EqualsList<T>(this List<T> source, List<T> dest) where T : IEquatable<T> { if (source.Count != dest.Count) return false; for (int i = 0; i < source.Count; i++) { if (source.Count(item => item.Equals(source[i])) != dest.Count(item => item.Equals(source[i]))) return false; } return true; }
/// <summary> /// 目标列表是否包含于源列表集合中,不考虑列表元素顺序 /// </summary> public static bool ContainList<T>(this List<List<T>> source, List<T> destList) where T : IEquatable<T> { for (int i = 0; i < source.Count; i++) { List<T> sourceList = source[i]; if (sourceList.EqualsList(destList)) return true; } return false; }
递归解法
/// <summary> /// 获取集合的所有子集 /// </summary> /// <param name="source">源数组集合</param> /// <param name="allowRepeat">是否允许重复子集</param> /// <param name="rightSplitLength">可选参数(默认1),初始分割长度(数组右侧)</param> /// <returns></returns> public static List<List<T>> GetSubList<T>(T[] source, bool allowRepeat, int rightSplitLength = 1) where T : IEquatable<T> { // 返回子集集合 List<List<T>> rSet = new List<List<T>>(); // 数组长度为length int length = source.Length; // 递归基准情形,当数组长度为1时,子集为数组本身 if (length == 1) { rSet.Add(source.ToList<T>()); } else { // 左侧数组 T[] leftArray = source.Where((r, index) => index < length - rightSplitLength).ToArray(); // 右侧数组 T[] rightArray = source.Where((r, index) => index >= length - rightSplitLength).ToArray(); // 递归计算左侧数组子集集合 List<List<T>> leftSubSet = GetSubList(leftArray, allowRepeat); // 递归计算右侧数组子集集合 List<List<T>> rightSubSet = GetSubList(rightArray, allowRepeat); if (allowRepeat) { // A.左侧子集作为源数组子集 允许重复 rSet.AddRange(leftSubSet); // B.右侧子集作为源数组子集 允许重复 rSet.AddRange(rightSubSet); } else { // A.左侧子集作为源数组子集 不允许重复 foreach (var lefttemp in leftSubSet) { if (!rSet.ContainList(lefttemp)) { rSet.Add(lefttemp); } } // B.右侧子集作为源数组子集 不允许重复 foreach (var righttemp in rightSubSet) { if (!rSet.ContainList(righttemp)) { rSet.Add(righttemp); } } } // 左右侧子集合并集 List<List<T>> combineSubSet = new List<List<T>>(); foreach (var leftSubList in leftSubSet) { foreach (var rightSubList in rightSubSet) { // 左右侧集合项交叉合并 List<T> combineList = new List<T>(); combineList.AddRange(leftSubList.Clone<T>()); combineList.AddRange(rightSubList.Clone<T>()); combineSubSet.Add(combineList); } } if (allowRepeat) { // C.左右侧子集合并集,形成源数组子集 允许重复 rSet.AddRange(combineSubSet); } else { // C.左右侧子集合并集,形成源数组子集 不允许重复 foreach (var combinetemp in combineSubSet) { if (!rSet.ContainList(combinetemp)) { rSet.Add(combinetemp); } } } } return rSet; }
测试结果
输入1){1,2,3,4}结果:
输入2){1,2,2,3,4}允许重复,结果:
输入3){1,2,2,3,4} 不允许重复,结果:
非递归解法
{1,2,3,,,,N-1,N} 集合子集表示为f(N)
将数组进行拆分
f(N-1) = {1,2,3,,,,N-1} ,f(1) = {N}
很显然,问题已经拆分为具有相同情况的子问题
{1} => {1}
{1,2} => {1},{2} => {1}+{2}+{1,2}
很容易推出 f(N) 的子集为 f(N-1) + f(1) + COMBINE(f(N-1),f(1))(取笛卡尔并集)
代码如下(此处去掉了重复子集判断):
public static List<List<T>> GetSubList2<T>(T[] source) where T : IEquatable<T> { List<List<T>> rList = new List<List<T>>(); for (int i = 0; i < source.Length; i++) { List<List<T>> combineList = new List<List<T>>(); foreach (var list in rList) { List<T> tmpList = list.Clone(); tmpList.Add(source[i]); combineList.Add(tmpList); } rList.AddRange(combineList); rList.Add(new List<T>() { source[i] }); } return rList; }
另一种非递归解法
根据数学知识,很容易知道,N个元素的子集个数为2^N - 1,时间复杂度是指数级的,我们可以联想到位操作(比如位移就是2的指数级操作),我们用0和1为下标,标识每一个元素是否出现在子集中,因此可以如此标识子集 (此处比如N为5)
1(00001),2(00010),3(00011),,,,,31(11111)
我们可以发现,从1到2^N-1的十进制循环数据中,每一个数据的二进制位对应的下标的数据集合就是所有子集
代码如下:
public static void PrintSubList<T>(T[] source) { int length = source.Length; int loopCount = 1 << length; int subCount = 0; for (int i = 1; i < loopCount; i++) { int takeNumber = i; for (int bitIndex = 0; bitIndex < length; bitIndex++) { if ((takeNumber & 1) == 1) { Console.Write(" {0} ", source[bitIndex]); } takeNumber >>= 1; } Console.WriteLine(); subCount++; } Console.WriteLine("共有 {0} 个子集.", subCount); }
延伸题目
给定一个数t,以及n个整数,在这n个整数中找到相加之和为t的所有组合,例如t = 4,n = 6,这6个数为[4, 3, 2, 2, 1, 1],这样输出就有4个不同的组合,它们的相加之和为4:4, 3+1, 2+2, and 2+1+1。请设计一个高效算法实现这个需求
使用上述方法对此方法进行求解,代码如下:
/// <summary> /// 获取集合的所有子集,要求集合内元素相加之和为sum /// </summary> public static List<List<int>> GetSubList4Sum(List<int> source, int sum) { // 返回子集集合 List<List<int>> rSet = new List<List<int>>(); // 数组长度为length int length = source.Count; for (int i = length - 1; i >= 0; i--) { // 选取右侧数据 int rightNum = source[i]; // 获取左侧集合 List<int> leftList = source.Where((r, index) => index < i).ToList(); if (rightNum > sum) { continue; } else if (rightNum == sum) { List<int> rightList = new List<int>() { rightNum }; // 避免重复 2 if (!rSet.ContainList(rightList)) { rSet.Add(rightList); } } else { List<List<int>> leftSet = GetSubList4Sum(leftList, sum - rightNum); foreach (var leftAvail in leftSet) { List<int> combineList = new List<int>(); combineList.AddRange(leftAvail); combineList.Add(rightNum); if (!rSet.ContainList(combineList)) { rSet.Add(combineList); } } } } return rSet; }
结语
其实,这些题目是以前做过的题目,并且以前还发过博客,最近突然想把那些做过的题目找回来,重新做了一遍,温故而知新