任何人都可以找到任何可能更有效的算法来完成以下任务吗?:
对于整数0到7的任何给定排列,返回按字典顺序描述排列的索引(从0开始索引,而不是1)。
例如,
factorial(7)-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%的程序运行时花在了这个函数上,不,我不想有一个预先计算好的值表。
除此之外,欢迎任何建议。
答案 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个排列)的性能分析是:
N = 10(3,628,800个排列)的性能分析是:
第一个数字是你的原始函数,第二个是重写的函数得到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;
}