最有效的计算词典索引的方法

时间:2014-05-31 01:08:33

标签: c++ c permutation mathematical-optimization lexicographic

任何人都可以找到任何可能更有效的算法来完成以下任务吗?:

对于整数0到7的任何给定排列,返回按字典顺序描述排列的索引(从0开始索引,而不是1)。

例如,

  • 数组0 1 2 3 4 5 6 7应返回索引0。
  • 数组0 1 2 3 4 5 7 6应返回索引1.
  • 数组0 1 2 3 4 6 5 7应返回索引2.
  • 数组1 0 2 3 4 5 6 7应返回索引5039(即7!-1或factorial(7)-1)。
  • 数组7 6 5 4 3 2 1 0应返回40319的索引(即8!-1)。这是最大可能的返回值。

我目前的代码如下:

int lexic_ix(int* A){
    int value = 0;
    for(int i=0 ; i<7 ; i++){
        int x = A[i];
        for(int j=0 ; j<i ; j++)
            if(A[j]<A[i]) x--;
        value += x*factorial(7-i);  // actual unrolled version doesn't have a function call
    }
    return value;
}

我想知道是否有任何方法可以通过删除内部循环来减少操作次数,或者我是否可以以任何方式减少条件分支(除了展开 - 我的当前代码实际上是上面的展开版本),或者是否有任何聪明的按位黑客或肮脏的C技巧来帮助。

我已经尝试过更换

if(A[j]<A[i]) x--;

x -= (A[j]<A[i]);

我也试过

x = A[j]<A[i] ? x-1 : x;

这两种替换实际上都会导致性能下降。

在有人说之前 - 是的,这是一个巨大的性能瓶颈:目前大约61%的程序运行时花在了这个函数上,不,我不想有一个预先计算好的值表。

除此之外,欢迎任何建议。

4 个答案:

答案 0 :(得分:2)

不知道这是否有帮助,但这是另一种解决方案:

int lexic_ix(int* A, int n){ //n = last index = number of digits - 1
    int value = 0;
    int x = 0;
    for(int i=0 ; i<n ; i++){
        int diff = (A[i] - x); //pb1
        if(diff > 0)
        {
            for(int j=0 ; j<i ; j++)//pb2
            {
                if(A[j]<A[i] && A[j] > x)
                {
                    if(A[j]==x+1)
                    {
                      x++;
                    }
                    diff--;
                }
            }
            value += diff;
        }
        else
        {
          x++;
        }
        value *= n - i;
    }
    return value;
}

我无法摆脱内循环,因此在最坏的情况下复杂度为o(n log(n)),但在最佳情况下为o(n),而对于o(n log(n) n))在所有情况下。

或者,您可以通过以下内容替换内部循环,以消除一些最坏的情况,代价是内循环中的另一个验证:

int j=0;
while(diff>1 && j<i)
{
  if(A[j]<A[i])
  {
    if(A[j]==x+1)
    {
      x++;
    }
    diff--;
  }
  j++;
}

解释

(或者更确切地说,&#34;我是如何结束该代码&#34;,我认为它与你的不同,但它可以让你有想法,也许) (为了减少混淆,我使用的是字符而不是数字,只有四个字符)

abcd 0  = ((0 * 3 + 0) * 2 + 0) * 1 + 0
abdc 1  = ((0 * 3 + 0) * 2 + 1) * 1 + 0
acbd 2  = ((0 * 3 + 1) * 2 + 0) * 1 + 0
acdb 3  = ((0 * 3 + 1) * 2 + 1) * 1 + 0
adbc 4  = ((0 * 3 + 2) * 2 + 0) * 1 + 0
adcb 5  = ((0 * 3 + 2) * 2 + 1) * 1 + 0 //pb1
bacd 6  = ((1 * 3 + 0) * 2 + 0) * 1 + 0
badc 7  = ((1 * 3 + 0) * 2 + 1) * 1 + 0
bcad 8  = ((1 * 3 + 1) * 2 + 0) * 1 + 0 //First reflexion
bcda 9  = ((1 * 3 + 1) * 2 + 1) * 1 + 0
bdac 10 = ((1 * 3 + 2) * 2 + 0) * 1 + 0
bdca 11 = ((1 * 3 + 2) * 2 + 1) * 1 + 0
cabd 12 = ((2 * 3 + 0) * 2 + 0) * 1 + 0
cadb 13 = ((2 * 3 + 0) * 2 + 1) * 1 + 0
cbad 14 = ((2 * 3 + 1) * 2 + 0) * 1 + 0
cbda 15 = ((2 * 3 + 1) * 2 + 1) * 1 + 0 //pb2
cdab 16 = ((2 * 3 + 2) * 2 + 0) * 1 + 0
cdba 17 = ((2 * 3 + 2) * 2 + 1) * 1 + 0
[...]
dcba 23 = ((3 * 3 + 2) * 2 + 1) * 1 + 0

首先&#34;反思&#34;

熵的观点。 abcd具有最少的熵&#34;。如果一个角色在一个地方,那么它就不应该是#34;是的,它创造了熵,熵越早越好。

例如,对于bcad,词典索引为8 =(( 1 * 3 + 1 )* 2 + 0 )* 1 + 0 ,可以这样计算:

value = 0;
value += max(b - a, 0); // = 1; (a "should be" in the first place [to create the less possible entropy] but instead it is b)
value *= 3 - 0; //last index - current index
value += max(c - b, 0); // = 1; (b "should be" in the second place but instead it is c)
value *= 3 - 1;
value += max(a - c, 0); // = 0; (a "should have been" put earlier, so it does not create entropy to put it there)
value *= 3 - 2;
value += max(d - d, 0); // = 0;

请注意,上一次操作将始终无效,这就是为什么&#34; i

第一个问题(pb1):

对于adcb,例如,第一个逻辑不起作用(它导致((0 * 3 + 2)* 2+ 0)* 1 = 4的词典索引,因为cd = 0但它创建熵以在b之前放置c。我添加了x因为它,它代表了尚未放置的第一个数字/字符。使用x,diff不能为负数。 对于adcb,词典索引是5 =(( 0 * 3 + 2 )* 2 + 1 )* 1 + 0并且可以计算那样:

value = 0; x=0;
diff = a - a; // = 0; (a is in the right place)
diff == 0 => x++; //x=b now and we don't modify value
value *= 3 - 0; //last index - current index
diff = d - b; // = 2; (b "should be" there (it's x) but instead it is d)
diff > 0 => value += diff; //we add diff to value and we don't modify x
diff = c - b; // = 1; (b "should be" there but instead it is c) This is where it differs from the first reflexion
diff > 0 => value += diff;
value *= 3 - 2;

第二个问题(pb2):

对于cbda,例如,词典索引是15 =((2 * 3 + 1)* 2 + 1)* 1 + 0,但第一个反射给出:((2 * 3 + 0)* 2 + 1 )* 1 + 0 = 13并且pb1的解给出((2 * 3 + 1)* 2 + 3)* 1 + 0 = 17.对pb1的解决方案不起作用,因为要放置的两个最后一个字符是d和a,所以d - a&#34;表示&#34;我不得不计算在角色到位之前放置的角色,但在x之后,我必须添加一个内循环。

全部放在一起

然后我意识到pb1只是pb2的一个特例,如果你删除x,你只需要使用diff = A [i],我们最终得到你的解决方案的unnested版本(使用因子计算得很少)很少,我的差异对应你的x)。

所以,基本上,我的贡献&#34; (我认为)是添加一个变量x,它可以避免在diff等于0或1时执行内部循环,代价是检查是否必须增加x并执行它(如果是这样的话。)

我还检查了你是否必须在内循环中递增x(如果(A [j] == x + 1)),因为如果你采用例如badce,x将在结尾处为b,因为a来自b之后,你将再次进入内循环,遇到c。如果在内循环中检查x,当遇到d时你除了做内循环之外别无选择,但x会更新为c,当你遇到c时你不会进入内循环。您可以在不破坏程序的情况下删除此检查

使用替代版本并在内部循环中检查它会产生4个不同的版本。检查的另一种选择是你输入内循环越少的那个,所以在理论上的复杂性&#34;它是最好的,但就性能/操作次数而言,我不知道。

希望所有这些都有所帮助(因为问题相当陈旧,而且我没有详细阅读所有答案)。如果没有,我仍然很开心。对不起,很长的帖子。我也是Stack Overflow的新成员(作为会员),而不是母语人士,所以请保持愉快,如果我做错了,请不要犹豫,告诉我。

答案 1 :(得分:0)

已经在缓存中的内存的线性遍历实际上并不需要花费很多时间。别担心。在factorial()溢出之前,你不会穿越足够的距离。

8作为参数移出。

int factorial ( int input )
{
    return input ? input * factorial (input - 1) : 1;
}

int lexic_ix ( int* arr, int N )
{
    int output = 0;
    int fact = factorial (N);
    for ( int i = 0; i < N - 1; i++ )
    {
        int order = arr [ i ];
        for ( int j = 0; j < i; j++ )
            order -= arr [ j ] < arr [ i ];
        output += order * (fact /= N - i);
    }
    return output;
}

int main()
{
    int arr [ ] = { 11, 10, 9, 8, 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 };

    const int length = 12;
    for ( int i = 0; i < length; ++i )
        std::cout << lexic_ix ( arr + i, length - i  ) << std::endl;
}

答案 2 :(得分:0)

比如说,对于M位序列排列,您可以从代码中获得字典SN公式,如:Am-1 *(m-1)! + Am-2 *(m-2)! + ... + A0 *(0)! ,其中Aj的范围从0到j。你可以从A0 *(0)!,然后A1 *(1)!,...,然后是Am-1 *(m-1)!来计算SN,并将它们加在一起(假设你的整数类型没有溢出),所以你不需要递归地重复计算阶乘。 SN编号的范围是0到M!-1(因为Sum(n * n!,n在0,1,... n中)=(n + 1)! - 1)

如果你不是递归地计算阶乘,我想不出任何可以带来任何重大改进的东西。

很抱歉发布的代码有点晚了,我刚做了一些研究,发现这个: http://swortham.blogspot.com.au/2011/10/how-much-faster-is-multiplication-than.html 根据这篇文章,整数乘法可以比整数除法快40倍。浮动数字虽然不是那么引人注目,但这里是纯整数。

int lexic_ix ( int arr[], int N )
{
    // if this function will be called repeatedly, consider pass in this pointer as parameter
    std::unique_ptr<int[]> coeff_arr = std::make_unique<int[]>(N);
    for ( int i = 0; i < N - 1; i++ )
    {
        int order = arr [ i ];
        for ( int j = 0; j < i; j++ )
            order -= arr [ j ] < arr [ i ];
        coeff_arr[i] = order; // save this into coeff_arr for later multiplication
    }
    // 
    // There are 2 points about the following code:
    // 1). most modern processors have built-in multiplier, \
    //    and multiplication is much faster than division
    // 2). In your code, you are only the maximum permutation serial number,
    //     if you put in a random sequence, say, when length is 10, you put in
    //     a random sequence, say, {3, 7, 2, 9, 0, 1, 5, 8, 4, 6}; if you look into
    //     the coeff_arr[] in debugger, you can see that coeff_arr[] is:
    //     {3, 6, 2, 6, 0, 0, 1, 2, 0, 0}, the last number will always be zero anyway.
    //     so, you will have good chance to reduce many multiplications.
    //     I did not do any performance profiling, you could have a go, and it will be
    //     much appreciated if you could give some feedback about the result.
    //
    long fac = 1;
    long sn = 0;
    for (int i = 1; i < N; ++i) // start from 1, because coeff_arr[N-1] is always 0 
    {
        fac *= i;
        if (coeff_arr[N - 1 - i])
            sn += coeff_arr[N - 1 - i] * fac;
    }
    return sn;
}

int main()
{
    int arr [ ] = { 3, 7, 2, 9, 0, 1, 5, 8, 4, 6 }; // try this and check coeff_arr

    const int length = 10;
    std::cout << lexic_ix(arr, length ) << std::endl;
    return 0;
}

答案 3 :(得分:0)

这是整个分析代码,我只在Linux中运行测试,代码是使用G ++ 8.4编译的,其中&#39; -std = c ++ 11 -O3&#39;编译器选项。公平地说,我稍微重写了你的代码,预先计算了N!并将其传递给函数,但似乎这没有多大帮助。

N = 9(362,880个排列)的性能分析是:

  • 持续时间为:34,30,25毫秒
  • 持续时间为:34,30,25毫秒
  • 持续时间为:33,30,25毫秒

N = 10(3,628,800个排列)的性能分析是:

  • 持续时间为:345,335,275毫秒
  • 持续时间为:348,334,275毫秒
  • 持续时间为:345,335,275毫秒

第一个数字是你的原始函数,第二个是重写的函数得到N!传入,最后一个数字是我的结果。置换生成函数非常原始并且运行缓慢,但只要它生成所有排列作为测试数据集,那就没问题。顺便说一下,这些测试是在运行Ubuntu 14.04的四核3.1Ghz,4GBytes桌面上运行。

编辑:我忘记了第一个函数可能需要扩展lexi_numbers向量的因素,所以我在计时之前调用了一个空调。在此之后,时间是333,334,275。

编辑:另一个可能影响性能的因素,我在我的代码中使用长整数,如果我改变那些2&#39; long&#39;到2&#39; int&#39;,运行时间将变为:334,333,264。

#include <iostream>
#include <vector>
#include <chrono>
using namespace std::chrono;

int factorial(int input)
{
    return input ? input * factorial(input - 1) : 1;
}

int lexic_ix(int* arr, int N)
{
    int output = 0;
    int fact = factorial(N);
    for (int i = 0; i < N - 1; i++)
    {
        int order = arr[i];
        for (int j = 0; j < i; j++)
            order -= arr[j] < arr[i];
        output += order * (fact /= N - i);
    }
    return output;
}

int lexic_ix1(int* arr, int N, int N_fac)
{
    int output = 0;
    int fact = N_fac;
    for (int i = 0; i < N - 1; i++)
    {
        int order = arr[i];
        for (int j = 0; j < i; j++)
            order -= arr[j] < arr[i];
        output += order * (fact /= N - i);
    }
    return output;
}

int lexic_ix2( int arr[], int N , int coeff_arr[])
{
    for ( int i = 0; i < N - 1; i++ )
    {
        int order = arr [ i ];
        for ( int j = 0; j < i; j++ )
            order -= arr [ j ] < arr [ i ];
        coeff_arr[i] = order;
    }
    long fac = 1;
    long sn = 0;
    for (int i = 1; i < N; ++i)
    {
        fac *= i;
        if (coeff_arr[N - 1 - i])
            sn += coeff_arr[N - 1 - i] * fac;
    }
    return sn;
}

std::vector<std::vector<int>> gen_permutation(const std::vector<int>& permu_base)
{
    if (permu_base.size() == 1)
        return std::vector<std::vector<int>>(1, std::vector<int>(1, permu_base[0]));

    std::vector<std::vector<int>> results;
    for (int i = 0; i < permu_base.size(); ++i)
    {
        int cur_int = permu_base[i];
        std::vector<int> cur_subseq = permu_base;
        cur_subseq.erase(cur_subseq.begin() + i);
        std::vector<std::vector<int>> temp = gen_permutation(cur_subseq);
        for (auto x : temp)
        {
            x.insert(x.begin(), cur_int);
            results.push_back(x);
        }
    }
    return results;
}

int main()
{
    #define N 10
    std::vector<int> arr;
    int buff_arr[N];
    const int length = N;
    int N_fac = factorial(N);
    for(int i=0; i<N; ++i)
        arr.push_back(N-i-1); // for N=10, arr is {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
    std::vector<std::vector<int>> all_permus = gen_permutation(arr);

    std::vector<int> lexi_numbers;
    // This call is not timed, only to expand the lexi_numbers vector 
    for (auto x : all_permus)
        lexi_numbers.push_back(lexic_ix2(&x[0], length, buff_arr));

    lexi_numbers.clear();
    auto t0 = high_resolution_clock::now();
    for (auto x : all_permus)
        lexi_numbers.push_back(lexic_ix(&x[0], length));
    auto t1 = high_resolution_clock::now();
    lexi_numbers.clear();
    auto t2 = high_resolution_clock::now();
    for (auto x : all_permus)
        lexi_numbers.push_back(lexic_ix1(&x[0], length, N_fac));
    auto t3 = high_resolution_clock::now();
    lexi_numbers.clear();
    auto t4 = high_resolution_clock::now();
    for (auto x : all_permus)
        lexi_numbers.push_back(lexic_ix2(&x[0], length, buff_arr));
    auto t5 = high_resolution_clock::now();

std::cout << std::endl << "Time durations are: " << duration_cast<milliseconds> \
    (t1 -t0).count() << ", " << duration_cast<milliseconds>(t3 - t2).count() << ", " \
        << duration_cast<milliseconds>(t5 - t4).count() <<" milliseconds" << std::endl;
    return 0;
}