生成集合的排列(最有效方法)

79

我想要生成一个集合(一组)的所有排列,例如:

Collection: 1, 2, 3
Permutations: {1, 2, 3}
              {1, 3, 2}
              {2, 1, 3}
              {2, 3, 1}
              {3, 1, 2}
              {3, 2, 1}

这不是一个普遍的“如何”问题,而是更多关于如何最有效地解决问题。此外,我不想生成所有排列并返回它们,而只是一次生成一个排列,并在必要时继续(就像迭代器一样 - 我也尝试过,但效率不高)。

我测试了许多算法和方法,并得出了这个代码,它是我尝试过的最有效的代码:

public static bool NextPermutation<T>(T[] elements) where T : IComparable<T>
{
    // More efficient to have a variable instead of accessing a property
    var count = elements.Length;

    // Indicates whether this is the last lexicographic permutation
    var done = true;

    // Go through the array from last to first
    for (var i = count - 1; i > 0; i--)
    {
        var curr = elements[i];

        // Check if the current element is less than the one before it
        if (curr.CompareTo(elements[i - 1]) < 0)
        {
            continue;
        }

        // An element bigger than the one before it has been found,
        // so this isn't the last lexicographic permutation.
        done = false;

        // Save the previous (bigger) element in a variable for more efficiency.
        var prev = elements[i - 1];

        // Have a variable to hold the index of the element to swap
        // with the previous element (the to-swap element would be
        // the smallest element that comes after the previous element
        // and is bigger than the previous element), initializing it
        // as the current index of the current item (curr).
        var currIndex = i;

        // Go through the array from the element after the current one to last
        for (var j = i + 1; j < count; j++)
        {
            // Save into variable for more efficiency
            var tmp = elements[j];

            // Check if tmp suits the "next swap" conditions:
            // Smallest, but bigger than the "prev" element
            if (tmp.CompareTo(curr) < 0 && tmp.CompareTo(prev) > 0)
            {
                curr = tmp;
                currIndex = j;
            }
        }

        // Swap the "prev" with the new "curr" (the swap-with element)
        elements[currIndex] = prev;
        elements[i - 1] = curr;

        // Reverse the order of the tail, in order to reset it's lexicographic order
        for (var j = count - 1; j > i; j--, i++)
        {
            var tmp = elements[j];
            elements[j] = elements[i];
            elements[i] = tmp;
        }

        // Break since we have got the next permutation
        // The reason to have all the logic inside the loop is
        // to prevent the need of an extra variable indicating "i" when
        // the next needed swap is found (moving "i" outside the loop is a
        // bad practice, and isn't very readable, so I preferred not doing
        // that as well).
        break;
    }

    // Return whether this has been the last lexicographic permutation.
    return done;
}

它的用法是发送一个元素数组,返回一个布尔值,指示这是否是最后一个字典排序排列,同时将数组改变为下一个排列。

使用示例:

var arr = new[] {1, 2, 3};

PrintArray(arr);

while (!NextPermutation(arr))
{
    PrintArray(arr);
}

事实上,我对代码的速度不满意。
遍历大小为11的数组的所有排列需要大约4秒钟。虽然这可以被认为是令人印象深刻的成就,因为大小为11的集合的可能排列数是近4000万个。
从逻辑上讲,对于大小为12的数组,它将需要大约12倍的时间,因为12!等于11!* 12,对于大小为13的数组,它将需要比大小为12的数组多大约13倍的时间,以此类推。
因此,您可以轻松理解,在大小为12及以上的数组中,浏览所有排列确实需要很长时间。
我有一种强烈的预感,我可以通过某种方式大大缩短时间(而不是切换到除C#之外的其他语言-因为编译器优化确实可以很好地进行优化,而且我怀疑我手动使用汇编语言无法进行如此良好的优化)。
有人知道任何其他更快的方法吗?您有任何想法可以使当前算法更快吗?
请注意,我不想使用外部库或服务来完成这项工作-我希望拥有代码本身,并且希望尽可能高效。

13
生成 所有 排列组合的速度不可能快于排列组合的数量。 - nhahtdh
1
集合是否只包含唯一元素? - Lieven Keersmaekers
7
顺便说一下,由于你正在做的事情本质上是“O(n!)”级别的,所以总会有一个非常小的数量,你会说:“处理M只需要几秒钟,但是处理M+1将需要M+1倍的时间。”即使你能将代码加速一百万倍,你只能从12变成17。这会让你快乐一百万倍吗? - Steve Jessop
1
@DaveBish 这对我有什么帮助?这会生成组合,而不是排列。 - SimpleVar
我想指出这个算法在 k < n 时无法处理排列(n,k)。 - Mauro Sampietro
显示剩余26条评论
18个回答

39

这可能是你在寻找的东西。

    private static bool NextPermutation(int[] numList)
    {
        /*
         Knuths
         1. Find the largest index j such that a[j] < a[j + 1]. If no such index exists, the permutation is the last permutation.
         2. Find the largest index l such that a[j] < a[l]. Since j + 1 is such an index, l is well defined and satisfies j < l.
         3. Swap a[j] with a[l].
         4. Reverse the sequence from a[j + 1] up to and including the final element a[n].

         */
        var largestIndex = -1;
        for (var i = numList.Length - 2; i >= 0; i--)
        {
            if (numList[i] < numList[i + 1]) {
                largestIndex = i;
                break;
            }
        }

        if (largestIndex < 0) return false;

        var largestIndex2 = -1;
        for (var i = numList.Length - 1 ; i >= 0; i--) {
            if (numList[largestIndex] < numList[i]) {
                largestIndex2 = i;
                break;
            }
        }

        var tmp = numList[largestIndex];
        numList[largestIndex] = numList[largestIndex2];
        numList[largestIndex2] = tmp;

        for (int i = largestIndex + 1, j = numList.Length - 1; i < j; i++, j--) {
            tmp = numList[i];
            numList[i] = numList[j];
            numList[j] = tmp;
        }

        return true;
    }

3
在SO,3秒钟可以是永恒的... ;) 一个显著改进的方法是将算法并行化。但这并不总是适用的。你可以在这里看一下:http://scidok.sulb.uni-saarland.de/volltexte/2005/397/pdf/sfb124-94-02.pdf - Sani Huttunen
2
@YoryeNathan,你欠读者们一篇文章:“我想我会在某个地方发布一篇关于我的工作的文章。” - colinfang
1
@YoryeNathan:仍然想看看它。也许获得一些外部的见解会很好... ;) - Sani Huttunen
5
@YoryeNathan,写代码,否则就没发生过。 - Christoffer Lette
2
@SaniSinghHuttunen,嘿!只是想告诉你,我发布了一个新的答案,其中使用了你的代码(以及更多),我将其多线程化。在我的机器上,结果快了4倍。为了更快地进行,我必须找到一种方法,在排列的任何位置调用算法。我做了一个相当慢的,但我只调用每个线程的第一个调用,然后我调用你的算法。我们应该能够共同得出最佳答案;-) !!! - Eric Ouellet
显示剩余17条评论

33
更新于2018年5月28日: 有点晚了...
根据最近的测试(更新于2018年5月22日)
  • 我的是最快的,但不是按字典顺序排列的
  • 按字典顺序排列最快的解决方案是Sani Singh Huttunen的
在我的机器上,对10个项目(10!)进行性能测试(毫秒):
  • Ouellet:29
  • SimpleVar:95
  • Erez Robinson:156
  • Sani Singh Huttunen:37
  • Pengyang:45047
在我的机器上,对13个项目(13!)进行性能测试(秒):
  • Ouellet:48.437
  • SimpleVar:159.869
  • Erez Robinson:327.781
  • Sani Singh Huttunen:64.839

我的解决方案的优点:
  • Heap算法(每个排列只需要交换一次)
  • 没有乘法(与网上一些实现不同)
  • 内联交换
  • 通用
  • 没有不安全的代码
  • 原地操作(内存使用很低)
  • 没有模数(仅比较第一个位)
我的Heap算法实现:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;

namespace WpfPermutations
{
    /// <summary>
    /// EO: 2016-04-14
    /// Generator of all permutations of an array of anything.
    /// Base on Heap's Algorithm. See: https://en.wikipedia.org/wiki/Heap%27s_algorithm#cite_note-3
    /// </summary>
    public static class Permutations
    {
        /// <summary>
        /// Heap's algorithm to find all pmermutations. Non recursive, more efficient.
        /// </summary>
        /// <param name="items">Items to permute in each possible ways</param>
        /// <param name="funcExecuteAndTellIfShouldStop"></param>
        /// <returns>Return true if cancelled</returns> 
        public static bool ForAllPermutation<T>(T[] items, Func<T[], bool> funcExecuteAndTellIfShouldStop)
        {
            int countOfItem = items.Length;

            if (countOfItem <= 1)
            {
                return funcExecuteAndTellIfShouldStop(items);
            }

            var indexes = new int[countOfItem];
            
            // Unecessary. Thanks to NetManage for the advise
            // for (int i = 0; i < countOfItem; i++)
            // {
            //     indexes[i] = 0;
            // }

            if (funcExecuteAndTellIfShouldStop(items))
            {
                return true;
            }

            for (int i = 1; i < countOfItem;)
            {
                if (indexes[i] < i)
                { // On the web there is an implementation with a multiplication which should be less efficient.
                    if ((i & 1) == 1) // if (i % 2 == 1)  ... more efficient ??? At least the same.
                    {
                        Swap(ref items[i], ref items[indexes[i]]);
                    }
                    else
                    {
                        Swap(ref items[i], ref items[0]);
                    }

                    if (funcExecuteAndTellIfShouldStop(items))
                    {
                        return true;
                    }

                    indexes[i]++;
                    i = 1;
                }
                else
                {
                    indexes[i++] = 0;
                }
            }

            return false;
        }

        /// <summary>
        /// This function is to show a linq way but is far less efficient
        /// From: StackOverflow user: Pengyang : https://dev59.com/SnRA5IYBdhLWcg3w_DHF
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="list"></param>
        /// <param name="length"></param>
        /// <returns></returns>
        static IEnumerable<IEnumerable<T>> GetPermutations<T>(IEnumerable<T> list, int length)
        {
            if (length == 1) return list.Select(t => new T[] { t });

            return GetPermutations(list, length - 1)
                .SelectMany(t => list.Where(e => !t.Contains(e)),
                    (t1, t2) => t1.Concat(new T[] { t2 }));
        }

        /// <summary>
        /// Swap 2 elements of same type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="a"></param>
        /// <param name="b"></param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static void Swap<T>(ref T a, ref T b)
        {
            T temp = a;
            a = b;
            b = temp;
        }

        /// <summary>
        /// Func to show how to call. It does a little test for an array of 4 items.
        /// </summary>
        public static void Test()
        {
            ForAllPermutation("123".ToCharArray(), (vals) =>
            {
                Console.WriteLine(String.Join("", vals));
                return false;
            });

            int[] values = new int[] { 0, 1, 2, 4 };

            Console.WriteLine("Ouellet heap's algorithm implementation");
            ForAllPermutation(values, (vals) =>
            {
                Console.WriteLine(String.Join("", vals));
                return false;
            });

            Console.WriteLine("Linq algorithm");
            foreach (var v in GetPermutations(values, values.Length))
            {
                Console.WriteLine(String.Join("", v));
            }

            // Performance Heap's against Linq version : huge differences
            int count = 0;

            values = new int[10];
            for (int n = 0; n < values.Length; n++)
            {
                values[n] = n;
            }

            Stopwatch stopWatch = new Stopwatch();

            ForAllPermutation(values, (vals) =>
            {
                foreach (var v in vals)
                {
                    count++;
                }
                return false;
            });

            stopWatch.Stop();
            Console.WriteLine($"Ouellet heap's algorithm implementation {count} items in {stopWatch.ElapsedMilliseconds} millisecs");

            count = 0;
            stopWatch.Reset();
            stopWatch.Start();

            foreach (var vals in GetPermutations(values, values.Length))
            {
                foreach (var v in vals)
                {
                    count++;
                }
            }

            stopWatch.Stop();
            Console.WriteLine($"Linq {count} items in {stopWatch.ElapsedMilliseconds} millisecs");
        }
    }
}

这是我的测试代码:

这是我的测试代码:

Task.Run(() =>
            {

                int[] values = new int[12];
                for (int n = 0; n < values.Length; n++)
                {
                    values[n] = n;
                }

                // Eric Ouellet Algorithm
                int count = 0;
                var stopwatch = new Stopwatch();
                stopwatch.Reset();
                stopwatch.Start();
                Permutations.ForAllPermutation(values, (vals) =>
                {
                    foreach (var v in vals)
                    {
                        count++;
                    }
                    return false;
                });
                stopwatch.Stop();
                Console.WriteLine($"This {count} items in {stopwatch.ElapsedMilliseconds} millisecs");

                // Simple Plan Algorithm
                count = 0;
                stopwatch.Reset();
                stopwatch.Start();
                PermutationsSimpleVar permutations2 = new PermutationsSimpleVar();
                permutations2.Permutate(1, values.Length, (int[] vals) =>
                {
                    foreach (var v in vals)
                    {
                        count++;
                    }
                });
                stopwatch.Stop();
                Console.WriteLine($"Simple Plan {count} items in {stopwatch.ElapsedMilliseconds} millisecs");

                // ErezRobinson Algorithm
                count = 0;
                stopwatch.Reset();
                stopwatch.Start();
                foreach(var vals in PermutationsErezRobinson.QuickPerm(values))
                {
                    foreach (var v in vals)
                    {
                        count++;
                    }
                };
                stopwatch.Stop();
                Console.WriteLine($"Erez Robinson {count} items in {stopwatch.ElapsedMilliseconds} millisecs");
            });

使用示例:

ForAllPermutation("123".ToCharArray(), (vals) =>
    {
        Console.WriteLine(String.Join("", vals));
        return false;
    });

int[] values = new int[] { 0, 1, 2, 4 };
ForAllPermutation(values, (vals) =>
        {
            Console.WriteLine(String.Join("", vals));
            return false;
        });

相信你的基准测试,我已将其标记为答案。看起来非常不错! - SimpleVar
1
谢谢!我刚刚实现了在维基百科上找到的内容。 - Eric Ouellet
我不明白你的问题?排列总是会给您一个独特的集合(排列)所有值。您能详细解释一下您正在寻找什么吗? - Eric Ouellet
1
请注意,在 C# 中,新数组保证被初始化为其类型的默认值,因此 var indexes = new int[countOfItem]; 创建了所有元素均为0 (default(int)) 的 indexes 数组,并且您不需要对它们进行设置。PS:摘要中有错别字:pmer。 - NetMage
1
在今天的C#中,你可以用if ((funcExecuteAndTellIfShouldStop(items) is var firstStopFlag) || countOfItem <= 1) return firstStopFlag;来替换你的前两个if - NetMage
显示剩余9条评论

12

如果你可以用C语言处理它,然后将其翻译成你选择的语言,那么你无法比这个更快,因为时间会被print所主导:

void perm(char* s, int n, int i){
  if (i >= n-1) print(s);
  else {
    perm(s, n, i+1);
    for (int j = i+1; j<n; j++){
      swap(s[i], s[j]);
      perm(s, n, i+1);
      swap(s[i], s[j]);
    }
  }
}

perm("ABC", 3, 0);

那是我最早想出并尝试的算法之一,但它不是最快的。我的当前实现更快。 - SimpleVar
@Yorye:嗯,正如我所说的,几乎所有时间都花在了打印上。如果你只是注释掉打印语句,你就会看到算法本身需要多少时间。在C#中,你被鼓励制作集合类、迭代器,并进行各种内存分配,任何好的算法都可能变得像糖浆一样慢。 - Mike Dunlavey
1
@Yorye:好的,两次交换可能需要大约8条指令。函数调用、进入和返回最多可能需要10条指令。内部的循环是主要的,所以每个排列可能需要大约20条指令。如果你能做得更快,那真是相当聪明了。 - Mike Dunlavey
2
很棒的答案。我已经成功将其翻译成C#(正在处理ref int[])。 - AlainD
2
这是最好的算法,小巧、干净,没有互斥锁,非常棒,谢谢! - Lu4
显示剩余4条评论

9
更新于2018年5月28日,推出了最新版本,速度最快的多线程版。 enter image description here
                            Time taken for fastest algorithms

需要:Sani Singh Huttunen(最快的lexico)解决方案和支持索引的新OuelletLexico3。
索引有两个主要优点:
允许直接获取任何排列。
允许多线程(源自第一个优点)。
文章:排列组合:快速实现和一种新的索引算法,允许多线程
在我的机器上(6个超线程核心:12个线程)Xeon E5-1660 0 @ 3.30Ghz,测试算法以13!项为空项目运行的时间(以毫秒为单位):
53071:Ouellet(Heap的实现)
65366:Sani Singh Huttunen(最快的lexico)
11377:Mix OuelletLexico3 - Sani Singh Huttunen
附注:在置换操作中使用线程之间共享的属性/变量,如果它们的使用是修改(读/写),将严重影响性能。这样做会在线程之间生成“false sharing”。您将无法获得预期的性能。我在测试时遇到了这种情况。我的经验表明,当我尝试增加用于置换总计数的全局变量时会出现问题。
用法:
PermutationMixOuelletSaniSinghHuttunen.ExecuteForEachPermutationMT(
  new int[] {1, 2, 3, 4}, 
  p => 
    { 
      Console.WriteLine($"Values: {p[0]}, {p[1]}, p[2]}, {p[3]}"); 
    });

代码:

using System;
using System.Runtime.CompilerServices;

namespace WpfPermutations
{
    public class Factorial
    {
        // ************************************************************************
        protected static long[] FactorialTable = new long[21];

        // ************************************************************************
        static Factorial()
        {
            FactorialTable[0] = 1; // To prevent divide by 0
            long f = 1;
            for (int i = 1; i <= 20; i++)
            {
                f = f * i;
                FactorialTable[i] = f;
            }
        }

        // ************************************************************************
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static long GetFactorial(int val) // a long can only support up to 20!
        {
            if (val > 20)
            {
                throw new OverflowException($"{nameof(Factorial)} only support a factorial value <= 20");
            }

            return FactorialTable[val];
        }

        // ************************************************************************

    }
}


namespace WpfPermutations
{
    public class PermutationSaniSinghHuttunen
    {
        public static bool NextPermutation(int[] numList)
        {
            /*
             Knuths
             1. Find the largest index j such that a[j] < a[j + 1]. If no such index exists, the permutation is the last permutation.
             2. Find the largest index l such that a[j] < a[l]. Since j + 1 is such an index, l is well defined and satisfies j < l.
             3. Swap a[j] with a[l].
             4. Reverse the sequence from a[j + 1] up to and including the final element a[n].

             */
            var largestIndex = -1;
            for (var i = numList.Length - 2; i >= 0; i--)
            {
                if (numList[i] < numList[i + 1])
                {
                    largestIndex = i;
                    break;
                }
            }

            if (largestIndex < 0) return false;

            var largestIndex2 = -1;
            for (var i = numList.Length - 1; i >= 0; i--)
            {
                if (numList[largestIndex] < numList[i])
                {
                    largestIndex2 = i;
                    break;
                }
            }

            var tmp = numList[largestIndex];
            numList[largestIndex] = numList[largestIndex2];
            numList[largestIndex2] = tmp;

            for (int i = largestIndex + 1, j = numList.Length - 1; i < j; i++, j--)
            {
                tmp = numList[i];
                numList[i] = numList[j];
                numList[j] = tmp;
            }

            return true;
        }
    }
}


using System;

namespace WpfPermutations
{
    public class PermutationOuelletLexico3<T> // Enable indexing 
    {
        // ************************************************************************
        private T[] _sortedValues;

        private bool[] _valueUsed;

        public readonly long MaxIndex; // long to support 20! or less 

        // ************************************************************************
        public PermutationOuelletLexico3(T[] sortedValues)
        {
            _sortedValues = sortedValues;
            Result = new T[_sortedValues.Length];
            _valueUsed = new bool[_sortedValues.Length];

            MaxIndex = Factorial.GetFactorial(_sortedValues.Length);
        }

        // ************************************************************************
        public T[] Result { get; private set; }

        // ************************************************************************
        /// <summary>
        /// Sort Index is 0 based and should be less than MaxIndex. Otherwise you get an exception.
        /// </summary>
        /// <param name="sortIndex"></param>
        /// <param name="result">Value is not used as inpu, only as output. Re-use buffer in order to save memory</param>
        /// <returns></returns>
        public void GetSortedValuesFor(long sortIndex)
        {
            int size = _sortedValues.Length;

            if (sortIndex < 0)
            {
                throw new ArgumentException("sortIndex should greater or equal to 0.");
            }

            if (sortIndex >= MaxIndex)
            {
                throw new ArgumentException("sortIndex should less than factorial(the lenght of items)");
            }

            for (int n = 0; n < _valueUsed.Length; n++)
            {
                _valueUsed[n] = false;
            }

            long factorielLower = MaxIndex;

            for (int index = 0; index < size; index++)
            {
                long factorielBigger = factorielLower;
                factorielLower = Factorial.GetFactorial(size - index - 1);  //  factorielBigger / inverseIndex;

                int resultItemIndex = (int)(sortIndex % factorielBigger / factorielLower);

                int correctedResultItemIndex = 0;
                for(;;)
                {
                    if (! _valueUsed[correctedResultItemIndex])
                    {
                        resultItemIndex--;
                        if (resultItemIndex < 0)
                        {
                            break;
                        }
                    }
                    correctedResultItemIndex++;
                }

                Result[index] = _sortedValues[correctedResultItemIndex];
                _valueUsed[correctedResultItemIndex] = true;
            }
        }

        // ************************************************************************
    }
}


using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace WpfPermutations
{
    public class PermutationMixOuelletSaniSinghHuttunen
    {
        // ************************************************************************
        private long _indexFirst;
        private long _indexLastExclusive;
        private int[] _sortedValues;

        // ************************************************************************
        public PermutationMixOuelletSaniSinghHuttunen(int[] sortedValues, long indexFirst = -1, long indexLastExclusive = -1)
        {
            if (indexFirst == -1)
            {
                indexFirst = 0;
            }

            if (indexLastExclusive == -1)
            {
                indexLastExclusive = Factorial.GetFactorial(sortedValues.Length);
            }

            if (indexFirst >= indexLastExclusive)
            {
                throw new ArgumentException($"{nameof(indexFirst)} should be less than {nameof(indexLastExclusive)}");
            }

            _indexFirst = indexFirst;
            _indexLastExclusive = indexLastExclusive;
            _sortedValues = sortedValues;
        }

        // ************************************************************************
        public void ExecuteForEachPermutation(Action<int[]> action)
        {
            //          Console.WriteLine($"Thread {System.Threading.Thread.CurrentThread.ManagedThreadId} started: {_indexFirst} {_indexLastExclusive}");

            long index = _indexFirst;

            PermutationOuelletLexico3<int> permutationOuellet = new PermutationOuelletLexico3<int>(_sortedValues);

            permutationOuellet.GetSortedValuesFor(index);
            action(permutationOuellet.Result);
            index++;

            int[] values = permutationOuellet.Result;
            while (index < _indexLastExclusive)
            {
                PermutationSaniSinghHuttunen.NextPermutation(values);
                action(values);
                index++;
            }

            //          Console.WriteLine($"Thread {System.Threading.Thread.CurrentThread.ManagedThreadId} ended: {DateTime.Now.ToString("yyyyMMdd_HHmmss_ffffff")}");
        }

        // ************************************************************************
        public static void ExecuteForEachPermutationMT(int[] sortedValues, Action<int[]> action)
        {
            int coreCount = Environment.ProcessorCount; // Hyper treading are taken into account (ex: on a 4 cores hyperthreaded = 8)
            long itemsFactorial = Factorial.GetFactorial(sortedValues.Length);
            long partCount = (long)Math.Ceiling((double)itemsFactorial / (double)coreCount);
            long startIndex = 0;

            var tasks = new List<Task>();

            for (int coreIndex = 0; coreIndex < coreCount; coreIndex++)
            {
                long stopIndex = Math.Min(startIndex + partCount, itemsFactorial);

                PermutationMixOuelletSaniSinghHuttunen mix = new PermutationMixOuelletSaniSinghHuttunen(sortedValues, startIndex, stopIndex);
                Task task = Task.Run(() => mix.ExecuteForEachPermutation(action));
                tasks.Add(task);

                if (stopIndex == itemsFactorial)
                {
                    break;
                }

                startIndex = startIndex + partCount;
            }

            Task.WaitAll(tasks.ToArray());
        }

        // ************************************************************************


    }
}

1
翻译:嘭,宝贝。嘭!有些人会说多线程是作弊,但我不这么认为 :P 生成排列组合是一个很好的并行处理事情,而且在没有线程的情况下,你只能走到这里。 - SimpleVar
完全同意你的观点!:-)......在许多情况下,更快的机器翻译解决方案比较慢的单语翻译更受欢迎。只是提醒你,我一两年前就需要那段代码了。 - Eric Ouellet
1
真是令人印象深刻的实现!真希望我能给它加100分! - Sani Huttunen
@SaniSinghHuttunen,哇!非常感谢你!如果没有你的代码,我不可能达到这样的性能。这真的是两者的结合... +100 也给你 :-) ! - Eric Ouellet

8
我所知道的最快排列算法是快速排列(QuickPerm)算法。
下面是实现代码,它使用yield return,所以您可以按需要一次迭代一个。
public static IEnumerable<IEnumerable<T>> QuickPerm<T>(this IEnumerable<T> set)
    {
        int N = set.Count();
        int[] a = new int[N];
        int[] p = new int[N];

        var yieldRet = new T[N];

        List<T> list = new List<T>(set);

        int i, j, tmp; // Upper Index i; Lower Index j

        for (i = 0; i < N; i++)
        {
            // initialize arrays; a[N] can be any type
            a[i] = i + 1; // a[i] value is not revealed and can be arbitrary
            p[i] = 0; // p[i] == i controls iteration and index boundaries for i
        }
        yield return list;
        //display(a, 0, 0);   // remove comment to display array a[]
        i = 1; // setup first swap points to be 1 and 0 respectively (i & j)
        while (i < N)
        {
            if (p[i] < i)
            {
                j = i%2*p[i]; // IF i is odd then j = p[i] otherwise j = 0
                tmp = a[j]; // swap(a[j], a[i])
                a[j] = a[i];
                a[i] = tmp;

                //MAIN!

                for (int x = 0; x < N; x++)
                {
                    yieldRet[x] = list[a[x]-1];
                }
                yield return yieldRet;
                //display(a, j, i); // remove comment to display target array a[]

                // MAIN!

                p[i]++; // increase index "weight" for i by one
                i = 1; // reset index i to 1 (assumed)
            }
            else
            {
                // otherwise p[i] == i
                p[i] = 0; // reset p[i] to zero
                i++; // set new index value for i (increase by one)
            } // if (p[i] < i)
        } // while(i < N)
    }

2
这比我的当前实现慢了大约3倍,而且迭代顺序也不是按字典顺序进行的。 - SimpleVar
1
我没有检查词典顺序,但在我的电脑上,QuickPerm对于11个项目花费了11秒,而你的算法花费了15秒。无论如何,祝你好运。 - Erez Robinson
1
@ErezRobinson:这需要大约7秒,而我的Knuth算法实现在我的电脑上只需要1.7秒,所以你的算法慢了4倍以上。 - Sani Huttunen
1
@ErezRobinson 我的实现在我的电脑上是3.83.9秒(这并不是一台很好的电脑),而你的是13秒。Sani的实现对我来说是3.73.8秒。 - SimpleVar
1
顺便说一下,结果发现我的实现实际上是Knuth风格的。 - SimpleVar
显示剩余5条评论

4

这是我最终使用的最快实现:

public class Permutations
{
    private readonly Mutex _mutex = new Mutex();

    private Action<int[]> _action;
    private Action<IntPtr> _actionUnsafe;
    private unsafe int* _arr;
    private IntPtr _arrIntPtr;
    private unsafe int* _last;
    private unsafe int* _lastPrev;
    private unsafe int* _lastPrevPrev;

    public int Size { get; private set; }

    public bool IsRunning()
    {
        return this._mutex.SafeWaitHandle.IsClosed;
    }

    public bool Permutate(int start, int count, Action<int[]> action, bool async = false)
    {
        return this.Permutate(start, count, action, null, async);
    }

    public bool Permutate(int start, int count, Action<IntPtr> actionUnsafe, bool async = false)
    {
        return this.Permutate(start, count, null, actionUnsafe, async);
    }

    private unsafe bool Permutate(int start, int count, Action<int[]> action, Action<IntPtr> actionUnsafe, bool async = false)
    {
        if (!this._mutex.WaitOne(0))
        {
            return false;
        }

        var x = (Action)(() =>
                             {
                                 this._actionUnsafe = actionUnsafe;
                                 this._action = action;

                                 this.Size = count;

                                 this._arr = (int*)Marshal.AllocHGlobal(count * sizeof(int));
                                 this._arrIntPtr = new IntPtr(this._arr);

                                 for (var i = 0; i < count - 3; i++)
                                 {
                                     this._arr[i] = start + i;
                                 }

                                 this._last = this._arr + count - 1;
                                 this._lastPrev = this._last - 1;
                                 this._lastPrevPrev = this._lastPrev - 1;

                                 *this._last = count - 1;
                                 *this._lastPrev = count - 2;
                                 *this._lastPrevPrev = count - 3;

                                 this.Permutate(count, this._arr);
                             });

        if (!async)
        {
            x();
        }
        else
        {
            new Thread(() => x()).Start();
        }

        return true;
    }

    private unsafe void Permutate(int size, int* start)
    {
        if (size == 3)
        {
            this.DoAction();
            Swap(this._last, this._lastPrev);
            this.DoAction();
            Swap(this._last, this._lastPrevPrev);
            this.DoAction();
            Swap(this._last, this._lastPrev);
            this.DoAction();
            Swap(this._last, this._lastPrevPrev);
            this.DoAction();
            Swap(this._last, this._lastPrev);
            this.DoAction();

            return;
        }

        var sizeDec = size - 1;
        var startNext = start + 1;
        var usedStarters = 0;

        for (var i = 0; i < sizeDec; i++)
        {
            this.Permutate(sizeDec, startNext);

            usedStarters |= 1 << *start;

            for (var j = startNext; j <= this._last; j++)
            {
                var mask = 1 << *j;

                if ((usedStarters & mask) != mask)
                {
                    Swap(start, j);
                    break;
                }
            }
        }

        this.Permutate(sizeDec, startNext);

        if (size == this.Size)
        {
            this._mutex.ReleaseMutex();
        }
    }

    private unsafe void DoAction()
    {
        if (this._action == null)
        {
            if (this._actionUnsafe != null)
            {
                this._actionUnsafe(this._arrIntPtr);
            }

            return;
        }

        var result = new int[this.Size];

        fixed (int* pt = result)
        {
            var limit = pt + this.Size;
            var resultPtr = pt;
            var arrayPtr = this._arr;

            while (resultPtr < limit)
            {
                *resultPtr = *arrayPtr;
                resultPtr++;
                arrayPtr++;
            }
        }

        this._action(result);
    }

    private static unsafe void Swap(int* a, int* b)
    {
        var tmp = *a;
        *a = *b;
        *b = tmp;
    }
}

用法和测试性能:

var perms = new Permutations();

var sw1 = Stopwatch.StartNew();

perms.Permutate(0,
                11,
                (Action<int[]>)null); // Comment this line and...
                //PrintArr); // Uncomment this line, to print permutations

sw1.Stop();
Console.WriteLine(sw1.Elapsed);

打印方法:
private static void PrintArr(int[] arr)
{
    Console.WriteLine(string.Join(",", arr));
}

深入理解:

我很久以前就没有考虑过这个问题了,所以我只能解释我的代码,但是这是一个大致的想法:

  1. 排列不是按字典顺序排序的 - 这使得我在排列之间实际上执行更少的操作。
  2. 实现是递归的,当“视图”大小为3时,它跳过复杂的逻辑,只执行6次交换以获得6个排列(或子排列)。
  3. 因为排列不是按字典顺序排列的,所以我如何决定将哪个元素带到当前“视图”(子排列)的开头?我记录了在当前子排列递归调用中已经使用过的元素作为“起始器”,并且在数组的尾部简单地线性搜索未使用的元素。
  4. 该实现仅适用于整数,因此要对通用元素集合进行排列,您只需使用Permutations类对索引进行排列,而不是对实际集合进行排列。
  5. Mutex只是为了确保在执行异步操作时不会出错(请注意,您可以传递一个UnsafeAction参数,该参数将依次获取指向排列数组的指针。您不能更改该数组(指针)中的元素顺序!如果您想要,应将数组复制到tmp数组中或者只是使用安全操作参数,该参数已经为您处理了 - 传递的数组已经是一个副本)。

注意:

我不知道这个实现有多好 - 我已经很久没有碰它了。 请自行测试和比较其他实现,并让我知道您是否有任何反馈!

享受吧。


1
@Lu4,这有什么可糟的呢?优化越多,代码就越不美观 - 但它运行起来非常快。 - SimpleVar
你在问题中提供的原始实现是最佳解决方案。它是干净且快速的代码,并生成排序排列。实际上,我从未将其标记为答案... - Mauro Sampietro
我正在研究你的原始解决方案,我有与你相同的直觉,但我没有成功编写一个通用的解决方案。做得好。 - Mauro Sampietro
@sam 问题中的代码非常稳定且运行良好。但是,该主题确实要尽可能地提高效率(即使牺牲可读性),而这个解决方案对我来说是最好的选择。 - SimpleVar
@SimpleVar,它可以工作,但速度比我的慢了约2倍。此外,您的代码是不安全的。当您进行测试时,如果将null作为Action放入,您如何确定编译器优化不会丢弃yield的读取(排列的实际计算)? - Eric Ouellet

3
这里有一个通用的排列查找器,它将遍历集合中的每个排列并调用评估函数。如果评估函数返回 true(找到了它要寻找的答案),排列查找器就会停止处理。
public class PermutationFinder<T>
{
    private T[] items;
    private Predicate<T[]> SuccessFunc;
    private bool success = false;
    private int itemsCount;

    public void Evaluate(T[] items, Predicate<T[]> SuccessFunc)
    {
        this.items = items;
        this.SuccessFunc = SuccessFunc;
        this.itemsCount = items.Count();

        Recurse(0);
    }

    private void Recurse(int index)
    {
        T tmp;

        if (index == itemsCount)
            success = SuccessFunc(items);
        else
        {
            for (int i = index; i < itemsCount; i++)
            {
                tmp = items[index];
                items[index] = items[i];
                items[i] = tmp;

                Recurse(index + 1);

                if (success)
                    break;

                tmp = items[index];
                items[index] = items[i];
                items[i] = tmp;
            }
        }
    }
}

这里是一个简单的实现:
class Program
{
    static void Main(string[] args)
    {
        new Program().Start();
    }

    void Start()
    {
        string[] items = new string[5];
        items[0] = "A";
        items[1] = "B";
        items[2] = "C";
        items[3] = "D";
        items[4] = "E";
        new PermutationFinder<string>().Evaluate(items, Evaluate);
        Console.ReadLine();
    }

    public bool Evaluate(string[] items)
    {
        Console.WriteLine(string.Format("{0},{1},{2},{3},{4}", items[0], items[1], items[2], items[3], items[4]));
        bool someCondition = false;

        if (someCondition)
            return true;  // Tell the permutation finder to stop.

        return false;
    }
}

1
我将items.Count保存到一个变量中。现在发布的代码需要大约0.55秒来迭代10个项目的列表。原始帖子中的代码对于相同的列表需要大约2.22秒。 - Sam
1
对于一个包含12个项目的列表(39,916,800种排列),这段代码只需要大约1分13秒,而原始帖子中的代码需要大约2分40秒。 - Sam
1
我的当前代码对于11个元素约为1.3-1.5秒。事实上,当最小所需交换次数为N!时,你正在进行2N!次交换。 - SimpleVar
我期望我们的版本之间至少有1.5倍的差异,因为我几乎进行了N!次交换(其中k非常接近于1),所以我认为我的计算机只是稍微慢了一点。 - SimpleVar
1
这个算法比我的Knuth实现慢大约3倍。在12个元素上,它需要33169毫秒,而Knuth实现只需要11941毫秒。排序也不是严格词典顺序。 - Sani Huttunen
显示剩余9条评论

2

这是一种基于交换数组元素的递归实现,时间复杂度为O(n * n!)1。数组的初始值为1, 2, ..., n

using System;

namespace Exercise
{
class Permutations
{
    static void Main(string[] args)
    {
        int setSize = 3;
        FindPermutations(setSize);
    }
    //-----------------------------------------------------------------------------
    /* Method: FindPermutations(n) */
    private static void FindPermutations(int n)
    {
        int[] arr = new int[n];
        for (int i = 0; i < n; i++)
        {
            arr[i] = i + 1;
        }
        int iEnd = arr.Length - 1;
        Permute(arr, iEnd);
    }
    //-----------------------------------------------------------------------------  
    /* Method: Permute(arr) */
    private static void Permute(int[] arr, int iEnd)
    {
        if (iEnd == 0)
        {
            PrintArray(arr);
            return;
        }

        Permute(arr, iEnd - 1);
        for (int i = 0; i < iEnd; i++)
        {
            swap(ref arr[i], ref arr[iEnd]);
            Permute(arr, iEnd - 1);
            swap(ref arr[i], ref arr[iEnd]);
        }
    }
}
}

每次递归步骤中,我们将最后一个元素与在 for 循环中当前指向的元素交换,然后通过增加 for 循环的本地变量并减少终止条件来指示交换的独特性,当终止条件被减少至零时,递归终止。初始终止条件已经设置为数组中元素的数量。
以下是辅助函数:
    //-----------------------------------------------------------------------------
    /*
        Method: PrintArray()

    */
    private static void PrintArray(int[] arr, string label = "")
    {
        Console.WriteLine(label);
        Console.Write("{");
        for (int i = 0; i < arr.Length; i++)
        {
            Console.Write(arr[i]);
            if (i < arr.Length - 1)
            {
                Console.Write(", ");
            }
        }
        Console.WriteLine("}");
    }
    //-----------------------------------------------------------------------------

    /*
        Method: swap(ref int a, ref int b)

    */
    private static void swap(ref int a, ref int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }

1. 有 n! 种由 n 个元素组成的排列需要打印。


1
通用目的的解决方案非常好和整洁。然而在速度方面稍显不足。但是因为大多数程序员可能更喜欢可读性,所以对于良好的代码给予加分。 - SimpleVar
有没有办法让它返回组合?如果可以,它是否必须先创建排列? - Terry

1

我创建了一个比Knuth的算法稍微快一点的算法:

11个元素:

我的:0.39秒

Knuth的:0.624秒

13个元素:

我的:56.615秒

Knuth的:98.681秒

这是我的Java代码:

public static void main(String[] args)
{
    int n=11;
    int a,b,c,i,tmp;
    int end=(int)Math.floor(n/2);
    int[][] pos = new int[end+1][2];
    int[] perm = new int[n];

    for(i=0;i<n;i++) perm[i]=i;

    while(true)
    {
        //this is where you can use the permutations (perm)
        i=0;
        c=n;

        while(pos[i][1]==c-2 && pos[i][0]==c-1)
        {
            pos[i][0]=0;
            pos[i][1]=0;
            i++;
            c-=2;
        }

        if(i==end) System.exit(0);

        a=(pos[i][0]+1)%c+i;
        b=pos[i][0]+i;

        tmp=perm[b];
        perm[b]=perm[a];
        perm[a]=tmp;

        if(pos[i][0]==c-1)
        {
            pos[i][0]=0;
            pos[i][1]++;
        }
        else
        {
            pos[i][0]++;
        }
    }
}

我的算法只适用于元素数量为奇数的情况。我匆忙编写了这段代码,所以我相信有更好的方法来实现我的想法以获得更好的性能,但我现在没有时间优化它并解决元素数量为偶数时的问题。
每个排列只需要进行一次交换,并且使用一种非常简单的方法来知道要交换的元素。
我在博客上写了一篇关于代码背后方法的说明:http://antoinecomeau.blogspot.ca/2015/01/fast-generation-of-all-permutations.html

似乎很有趣,但是它似乎比我的当前实现慢一些(标记为答案)。我很想了解它。同时也想知道你是如何在一个有问题的代码中计时性能的(new int[end + 1][2] 应该改为 new int[end + 1][] 并跟随适当的循环初始化)。 - SimpleVar
1
由于我们要谈论性能,所以摆脱不规则数组,改用步幅。 - CSharpie
使用这种算法生成的排列不按顺序进行。 - Mauro Sampietro

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接