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

Most efficient way to calculate lexicographic index

本文关键字:索引 方法 字典 计算 有效      更新时间:2023-10-16

谁能找到更有效的算法来完成下面的任务?:

对于给定的整数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,10应该返回索引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;
}

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

我已经试过替换

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

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

和我也试过

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

两种替换实际上都导致了更差的性能。

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

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

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

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))

或者,您可以用以下方式替换内部循环,以牺牲内部循环中的另一个验证来消除一些最坏的情况:

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

:

(或者更确切地说"我是如何结束这段代码的",我认为它与你的代码没有什么不同,但它可能会让你有想法,也许)(为了减少混淆,我使用字符代替数字,只有四个字符)

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

第一个"反射":

熵的观点。Abcd的"熵"最小。如果一个字符出现在它"不应该"出现的地方,它就会产生熵,而且熵越早出现,它就会变得越大。

以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;

注意最后一个操作总是什么都不做,这就是为什么"i "

第一个问题 (pb1):

对于adcb,例如,第一个逻辑不起作用(它导致字典索引为((0* 3+ 2)* 2+ 0)* 1 = 4),因为c-d = 0,但它创建熵将c放在b之前。我添加了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"表示"1而不是3。我必须计算放置在x之前的字符,但是在x之后,所以我必须添加一个内循环。

把它们放在一起:

然后我意识到pb1只是pb2的一个特例,如果你去掉x,然后你简单地取diff = a [I],我们最终得到了你的解的非嵌套版本(阶乘一点一点地计算,我的diff对应于你的x)。

所以,基本上,我的"贡献"(我认为)是添加一个变量x,它可以避免在diff等于0或1时进行内循环,而代价是检查是否需要增加x并在需要时执行。

我也检查如果你要增加x的内循环(如果([j] = = x + 1)),因为如果你采取例如badce x将结束时,因为A之后,你将进入内循环一次,遇到c。如果你检查x在内部循环,当你遇到你别无选择做d内循环,但x将会更新到c, c,当你遇到你不会进入内循环。您可以删除此检查而不会破坏程序

有了备选版本和内循环中的检查,它产生了4个不同的版本。另一种带有check的方法是你输入的内循环越少,所以就"理论复杂性"而言,它是最好的,但就性能/操作次数而言,我不知道。

希望所有这些都有帮助(因为这个问题相当老,我没有详细阅读所有的答案)。如果没有,我也很享受。很抱歉写了这么长时间。此外,我是Stack Overflow的新手(作为会员),母语不是英语,所以请友好一点,如果我做错了什么,请不要犹豫告诉我。

线性遍历已经在缓存中的内存实际上根本不需要花费太多时间。别担心。在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;
}

比如说,对于一个m位的序列排列,从你的代码中,你可以得到一个字典顺序的SN公式,它类似于:Am-1*(m-1)!+ Am-2 * (m - 2) !+……+ A0 * (0) !,其中Aj的取值范围从0到j。你可以从A0*(0)计算SN !,则A1*(1)!,……,则Am-1 * (m-1)!,并将它们加在一起(假设您的整数类型没有溢出),因此您不需要递归地重复计算阶乘。序列号的取值范围是0 ~ M!-1(因为Sum(n*n!, 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;
}

这是整个分析代码,我只在Linux下运行测试,代码是用g++ 8.4编译的,带有'-std=c++11 -O3'编译器选项。为了公平起见,我稍微重写了您的代码,预先计算了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, 4gb台式机上运行的。

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

编辑:另一个可能影响性能的因素,我在代码中使用长整数,如果我将这两个'long'改为2 'int',运行时间将变成: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;
}