代码改变世界

编程拾趣--集合子集问题

2014-11-02 20:41  周信达  阅读(560)  评论(0编辑  收藏  举报

问题

给出一个数组,比如 {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;
}

结语

其实,这些题目是以前做过的题目,并且以前还发过博客,最近突然想把那些做过的题目找回来,重新做了一遍,温故而知新