更新:我用这个问题作为从here开始的系列文章的主题;我将在该系列中介绍两种略有不同的算法。谢谢你的好问题!
到目前为止发布的两个解决方案是正确的,但对于数字变大的情况效率低下。到目前为止发布的解决方案使用算法:首先枚举所有可能性:
{1, 1, 1 }
{1, 1, 2 },
{1, 1, 3 },
...
{7, 7, 7}
在这样做的同时,过滤掉第二个不大于第一个,第三个不大于第二个的任何地方。这执行 7 x 7 x 7 过滤操作,数量不多,但如果您试图从 30 中获得 10 个元素的排列,那就是 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30,这是相当多的。你可以做得更好。
我会按如下方式解决这个问题。首先,生成一个有效的不可变集的数据结构。让我非常清楚不可变集是什么,因为您可能不熟悉它们。您通常将集合视为添加项目和从中删除项目的东西。不可变集合有一个Add 操作,但它不会改变集合;它会返回一个 new 集合,其中包含添加的项目。删除也一样。
这是一个不可变集合的实现,其中元素是从 0 到 31 的整数:
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System;
// A super-cheap immutable set of integers from 0 to 31 ;
// just a convenient wrapper around bit operations on an int.
internal struct BitSet : IEnumerable<int>
{
public static BitSet Empty { get { return default(BitSet); } }
private readonly int bits;
private BitSet(int bits) { this.bits = bits; }
public bool Contains(int item)
{
Debug.Assert(0 <= item && item <= 31);
return (bits & (1 << item)) != 0;
}
public BitSet Add(int item)
{
Debug.Assert(0 <= item && item <= 31);
return new BitSet(this.bits | (1 << item));
}
public BitSet Remove(int item)
{
Debug.Assert(0 <= item && item <= 31);
return new BitSet(this.bits & ~(1 << item));
}
IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
public IEnumerator<int> GetEnumerator()
{
for(int item = 0; item < 32; ++item)
if (this.Contains(item))
yield return item;
}
public override string ToString()
{
return string.Join(",", this);
}
}
仔细阅读此代码以了解其工作原理。同样,请始终记住向该集合添加元素不会更改该集合。它会生成一个新集合,其中包含添加的项目。
好的,既然我们已经知道了,让我们考虑一种更有效的算法来生成您的排列。
我们将递归地解决问题。递归解决方案始终具有相同的结构:
- 我们能解决一个小问题吗?如果是,请解决。
- 如果不是,请将问题分解为多个较小的问题并逐一解决。
让我们从琐碎的问题开始。
假设您有一个集合,并且您希望从中选择 零 个项目。答案很明确:只有一个可能的排列为零元素,那就是空集。
假设你有一个包含 n 个元素的集合,并且你想选择多于 n 个元素。显然没有解,甚至空集也没有。
我们现在已经处理了集合为空或选择的元素数量大于元素总数的情况,因此我们必须从至少有一个东西的集合中选择至少一个东西。
在可能的排列中,其中一些包含第一个元素,而另一些则没有。找到所有包含第一个元素的元素并生成它们。为此,我们通过递归选择 少一个 元素来缺少第一个元素。
那些没有的有第一个元素,我们通过枚举集合的排列没有第一个元素来找到。
static class Extensions
{
public static IEnumerable<BitSet> Choose(this BitSet b, int choose)
{
if (choose < 0) throw new InvalidOperationException();
if (choose == 0)
{
// Choosing zero elements from any set gives the empty set.
yield return BitSet.Empty;
}
else if (b.Count() >= choose)
{
// We are choosing at least one element from a set that has
// a first element. Get the first element, and the set
// lacking the first element.
int first = b.First();
BitSet rest = b.Remove(first);
// These are the permutations that contain the first element:
foreach(BitSet r in rest.Choose(choose-1))
yield return r.Add(first);
// These are the permutations that do not contain the first element:
foreach(BitSet r in rest.Choose(choose))
yield return r;
}
}
}
现在我们可以提出您需要回答的问题:
class Program
{
static void Main()
{
BitSet b = BitSet.Empty.Add(1).Add(2).Add(3).Add(4).Add(5).Add(6).Add(7);
foreach(BitSet result in b.Choose(3))
Console.WriteLine(result);
}
}
我们完成了。我们只生成了我们实际需要的序列。 (虽然我们已经做了很多集合操作来达到这个目的,但是集合操作很便宜。)这里的重点是理解这个算法是如何工作的非常有指导意义。对不可变结构进行递归编程是许多专业程序员工具箱中没有的强大工具。