【问题标题】:Creating a power set of a Sequence创建序列的幂集
【发布时间】:2013-11-22 08:03:11
【问题描述】:

我正在尝试创建一个程序,该程序是创建序列、字符串或数字的可能组合的基础。这是某种加密/解密程序。我正在使用 Visual Studio 2013 和 C#。我想做的是从一个序列中生成一个功率集,但我有点困惑,无法继续前进。这是代码。

public static void randomSeq()
{
    int temp = 0;
    string seq = "1234";

    var sb = new StringBuilder();
    char[] bits = seq.Select((char c) => c).ToArray();

    Console.Write("Given Sequence: ");
    Console.Write(seq);
    Console.WriteLine();
    Console.WriteLine("Generated possiblities");

    foreach (char item in bits)
        Console.WriteLine(item);
    do
    {
        if (temp <= 2)
        {
            for (int i = temp + 1; i < bits.Length; i++)
            {
                 sb.Append(bits[temp]);
                 sb.Append(bits[i]);
                 Console.WriteLine(sb);
                
                 sb.Clear();
            }
        }
        else if (temp > 2)
        {
            for (int k = 0; k < temp; k++)
                sb.Append(bits[k]);

            for (int l = temp + 1; l < bits.Length; l++)
            {
                sb.Append(bits[temp]);
                sb.Append(bits[l]);
                Console.WriteLine(sb);

                sb.Clear();
            }
        }
        temp++;
    }
    while (temp != bits.Length);
}

我希望这段代码是通用的,即我传递任何序列,它会为我生成一个幂集。然后我想在我的程序中进一步重用它。我可以简单地完成剩下的工作,因为我被困在生成电源组中。有人可以帮我吗?

【问题讨论】:

标签: c# .net algorithm


【解决方案1】:

如果熟悉位,幂集很容易生成。对于N 元素的集合,将有2^N 子集进入幂集(包括空集和初始集)。所以每个元素要么是 IN 要么是 OUT(换句话说,10)。

考虑到这一点,很容易将集合的子集表示为位掩码。然后枚举所有可能的位掩码,就有可能构建整个幂集。为了做到这一点,我们需要检查位掩码中的每个位,如果该位置存在1,则获取输入集的元素。下面是string(字符集合)作为输入的示例。它可以很容易地重写以用于收集任何类型的值。

private static List<string> PowerSet(string input)
{
    int n = input.Length;
    // Power set contains 2^N subsets.
    int powerSetCount = 1 << n;
    var ans = new List<string>();

    for (int setMask = 0; setMask < powerSetCount; setMask++)
    {
        var s = new StringBuilder();
        for (int i = 0; i < n; i++)
        {
            // Checking whether i'th element of input collection should go to the current subset.
            if ((setMask & (1 << i)) > 0)
            {
                s.Append(input[i]);
            }
        }
        ans.Add(s.ToString());
    }

    return ans;
}

示例

假设您有字符串 "xyz" 作为输入,它包含 3 个元素,而不是在幂集中将有 2^3 == 8 元素。如果您将从0 迭代到7,您将获得下表。列:(10 基整数;位表示(2 基);初始集的子集)。

0   000    ...
1   001    ..z
2   010    .y.
3   011    .yz
4   100    x..
5   101    x.z
6   110    xy.
7   111    xyz

您可以注意到第三列包含初始字符串 "xyz" 的所有子集


另一种方法(快两倍)和通用实现

受 Eric 的想法启发,我实现了该算法的另一个变体(现在没有位)。我也让它通用。我相信这段代码几乎是可以为 Power Set 计算编写的代码中最快的。它的复杂性与位方法O(n * 2^n) 相同,但这种方法的常数减半。

public static T[][] FastPowerSet<T>(T[] seq)
{
    var powerSet = new T[1 << seq.Length][];
    powerSet[0] = new T[0]; // starting only with empty set

    for (int i = 0; i < seq.Length; i++)
    {
        var cur = seq[i];
        int count = 1 << i; // doubling list each time
        for (int j = 0; j < count; j++)
        {
            var source = powerSet[j];
            var destination = powerSet[count + j] = new T[source.Length + 1];
            for (int q = 0; q < source.Length; q++)
                destination[q] = source[q];
            destination[source.Length] = cur;
        }
    }
    return powerSet;
}

【讨论】:

  • 谢谢,这很有帮助。你是怎么用比特做的?
  • 我在答案中添加了示例,希望对您有所帮助。
  • 我明白了这个例子。你的解释很好!
  • 我知道它很晚,但我认为你的代码的两个版本都有一个问题。代码1 &lt;&lt; n1 &lt;&lt; seq.Length 仅在nseq.Length 小于或等于'30' 时才有效(换句话说,它可以计算包含30 个元素的集合的powerSet,不超过那个)因为文字 '1' 将被表示为 Int32 数据类型(意味着只有 32 位来表示 1)并且执行 1&lt;&lt;31 将导致 '-2147483648' 并且执行 1&lt;&lt;32 将导致 '1' 。我知道您这样做是为了避免计算可能很昂贵的 2^N。如果我错了,请纠正我。
  • @SyedAhmedJamil 它可以有 31 个元素,而不是 30 个(我们从 0 开始计数)。但是超过 31 个元素的幂集无论如何是不可行的——它将包含超过 2,147,483,648 元素!这么大的数组你能处理吗?
【解决方案2】:

游戏已经很晚了,但为什么不采用下面的方法呢?它似乎比这里发布的建议简单得多:

    /*
    Description for a sample set {1, 2, 2, 3}:
    Step 1 - Start with {}:
    {}
    Step 2 - "Expand" previous set by adding 1:
    {}
    ---
    {1}
    Step 3 - Expand previous set by adding the first 2:
    {}
    {1}
    ---
    {2}
    {1,2}
    Step 4 - Expand previous set by adding the second 2:
    {}
    {1}
    {2}
    {1,2}
    ---
    {2}
    {1,2}
    {2,2}
    {1,2,2}
    Step 5 - Expand previous set by adding 3:
    {}
    {1}
    {2}
    {1,2}
    {2}
    {1,2}
    {2,2}
    {1,2,2}
    ---
    {3}
    {1,3}
    {2,3}
    {1,2,3}
    {2,3}
    {1,2,3}
    {2,2,3}
    {1,2,2,3}
    Total elements = 16 (i.e. 2^4), as expected.
    */

    private static void PowerSet(IList<int> nums, ref IList<IList<int>> output)
    {
        // ToDo: validate args
        output.Add(new List<int>());
        ExpandSet(nums, 0, ref output);
    }

    private static void ExpandSet(IList<int> nums, int pos, ref IList<IList<int>> output)
    {
        if (pos == nums.Count)
        {
            return;
        }

        List<int> tmp;
        int item = nums[pos];

        for (int i = 0, n = output.Count; i < n; i++)
        {
            tmp = new List<int>();
            tmp.AddRange(output[i]);
            tmp.Add(item);
            output.Add(tmp);
        }

        ExpandSet(nums, pos + 1, ref output);
    }

【讨论】:

    【解决方案3】:

    同样的算法SergeyS 提到使用 Linq(其中 inputSet 是输入,outputPowerSet 是输出):

    int setLength = inputSet.Count;
    int powerSetLength = 1 << setLength;
    for (int bitMask = 0; bitMask < powerSetLength; bitMask++)
    {
        var subSet = from x in inputSet 
                     where ((1 << inputSet.IndexOf(x)) & bitMask) != 0 
                     select x;
        outputPowerSet.Add(subSet.ToList());
    }
    

    【讨论】:

    • 谢谢您的回答!
    • 我跑了一些不需要的结果,我稍微调整了一下,我认为现在会更好,将子集更改为var subSet = input.Where((u, i) =&gt; ((1 &lt;&lt; i) &amp; bitMask) != 0).Select(x =&gt; x);
    • 整个解决方案的单行版本(e 是任何可枚举的):Enumerable.Range(0, (int)Math.Pow(2, e.Count())).Select(b =&gt; e.Where((x, i) =&gt; ((1 &lt;&lt; i) &amp; b) != 0))
    【解决方案4】:

    SergeyS 的方法是完全合理的。这是另一种思考方式。

    出于本答案的目的,我将假设“集合”是有限序列。

    我们递归地定义函数P如下。

    • 一个集合要么是空的,要么是单个项目 H 后跟一个集合 T。
    • P(empty) --&gt; { empty }
    • P(H : T) --&gt; P(T) 的并集和 P(T) 的每个元素前面加上 H

    让我们试试看。 {Apple, Banana, Cherry}的幂集是什么?

    它不是一个空集,所以{Apple, Banana, Cherry}的幂集是{Banana, Cherry}的幂集,加上每个前面加上Apple形成的集。

    所以我们需要知道{Banana, Cherry} 的幂集。它是{Cherry} 的幂集加上通过在每个前面加上Banana 形成的集。

    所以我们需要知道{Cherry} 的幂集。它是空集的幂集,加上在每个集前面加上Cherry 形成的集。

    所以我们需要知道空集的幂集。它是包含空集的集合。 { {} }

    现在在每个元素前面加上Cherry 并采用联合。那是{ {Cherry}, {} }。这给了我们{ Cherry } 的力量集。请记住,我们需要它来找到{Banana, Cherry} 的幂集,所以我们将它与每个前面的Banana 联合并得到{ {Banana, Cherry}, {Banana}, {Cherry}, {}},这就是{Banana, Cherry} 的幂集。

    现在我们需要它来获得 {Apple, Banana, Cherry} 的幂集,因此将其与每个附加的 Apple 联合起来,我们就有了 { {Apple, Banana, Cherry}, {Apple, Banana}, {Apple, Cherry}, {Apple}, {Banana, Cherry}, {Banana}, {Cherry}, {}},我们就完成了。

    代码应该易于编写。首先我们需要一个辅助方法:

    static IEnumerable<T> Prepend<T>(this IEnumerable<T> tail, T head)
    {
        yield return head;
        foreach(T item in tail) yield return item;
    }
    

    现在代码是算法描述的直接翻译:

    static IEnumerable<IEnumerable<T>> PowerSet<T>(this IEnumerable<T> items)
    {
        if (!items.Any())
            yield return items; // { { } } 
        else
        {
            var head = items.First();
            var powerset = items.Skip(1).PowerSet().ToList();
            foreach(var set in powerset) yield return set.Prepend(head); 
            foreach(var set in powerset) yield return set;
         }
    }                
    

    有意义吗?

    ----------- 更新-----

    Sergey 正确地指出我的代码有一个 Schlemiel the Painter 算法,因此会消耗大量的时间和内存;很好地抓住谢尔盖。这是一个使用不可变堆栈的高效版本:

    class ImmutableList<T>
    {
        public static readonly ImmutableList<T> Empty = new ImmutableList<T>(null, default(T));
        private ImmutableList(ImmutableList<T> tail, T head)
        {
            this.Head = head;
            this.Tail = tail;
        }
        public T Head { get; private set; }
        public ImmutableList<T> Tail { get; private set; }
        public ImmutableList<T> Push(T head)
        {
            return new ImmutableList<T>(this, head);
        }
        public IEnumerable<ImmutableList<T>> PowerSet()
        {
            if (this == Empty)
                yield return this;
            else
            {
                var powerset = Tail.PowerSet();
                foreach (var set in powerset) yield return set.Push(Head);
                foreach (var set in powerset) yield return set;
            }
        }
    }
    

    【讨论】:

    • 是的,确实有道理.. 谢谢!
    • 想法不错,但从性能的角度来看,实现并不是那么好。对于我的计算机上的 20 个元素集,程序崩溃并出现 OutOfMemory 异常(崩溃时内存使用量超过 1.5 GB)。
    • Eric,我认为算法是多项式还是指数并不重要。如果它可以优化 10 倍,那么这样做肯定会让人感兴趣:) 关于指数算法,以非递归方式实现它们是非常有趣的。
    • @SergeyS:如果您有特定的目标大小,那么可以肯定,优化可能值得追求。我的观点是:假设您可以合理地以 X 速度处理大小为 40 的案例。然后您进行大量优化并使其速度达到 10X。现在突然之间,您可以合理地处理大小为 43 的案例,而不是大小 400。花在优化指数算法的常数因子上的努力不会给您带来很大的回报,就像优化多项式算法的常数因子一样。
    • @SergeyS:这些都不是说你的方法是错误的;这是一个很好的方法。这也是一个很好的例子,说明最简单的 LINQ 解决方案如何有时由于意外的重新计算和隐藏成本而出人意料地欠佳。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 2017-05-28
    • 2013-10-18
    • 1970-01-01
    • 2022-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多