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


    问题

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

    结语

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

  • 相关阅读:
    掩膜操作手写+API(第二天)
    图像的加载+修改+显示+保存(第一天)
    VS2015+Opencv半永久配置
    关于QT中“崩溃”问题
    关于QT内部16进制、十进制、QByteArray,QString
    QT按键(Qbutton)改变颜色
    QT中使用自己定的类和Vector出现错误
    Spring Boot 集成Jsp与生产环境部署
    SpringBoot入门篇--关于properties和yml两种配置文件的一些事情
    简单的linux使用命令
  • 原文地址:https://www.cnblogs.com/fecktty2013/p/4069875.html
Copyright © 2020-2023  润新知