计算两个不同数字的倍数之差
Posted
技术标签:
【中文标题】计算两个不同数字的倍数之差【英文标题】:Calculate difference between multiples of two different numbers 【发布时间】:2014-01-27 01:20:50 【问题描述】:这是一个算法问题。为简单起见,假设我有两个双精度数,A 和 B。如果有意义的话,我想构造一个函数,在 A 的下一个倍数或 B 的下一个倍数之前给出差异。
例如,假设 A 是 3,B 是 5。
考虑倍数:(3,6,9,12,15) 和 (5,10,15)。
我希望函数输出: (3, 2, 1, 3, 1, 2, 3),因为到 3 需要 3 个单位,再到 5 需要 2 个单位,然后是 1 到 6,然后是 3 到 9,等等......
我希望这是有道理的。理想情况下,它是一个 Python 风格的生成器(虽然我是用 Arduino ~ C++ 编写的)。我需要它快 - 非常快。
任何帮助将不胜感激。我的伪代码在下面,但不是那么好。
a = 3
b = 5
current = 0
distToA = a
distToB = b
for i in xrange(100):
if distToA > distToB: #B comes first
print "Adding 0".format(distToB)
current += distToB
distToA -= distToBb
distToB = b
elif distToB > distToA: #A comes first
print "Adding 0".format(distToA)
current += distToA
distToB -= distToA
distToA = a
else: #Equal
print "Adding 0".format(distToA)
current += distToA #Arbitrarily, could be distToB
distToA = a
distToB = b
编辑:如果有多个值,这会如何?不只是a和b,还有c、d、e等。 我想我会多做一些 if 语句,但成本更高(每个分支的操作更多)。
【问题讨论】:
为什么你认为你的代码不是那么好?它已经给了你最好的表现,对吧?每次迭代你只做三个操作,这已经很简单了。 这需要运行多长时间?如果您需要远远超过 lcm(a, b),则可以利用序列循环这一事实获得很好的提升。 哦,等等,这些应该是整数还是浮点数?你说“双打”,但该示例使用整数。 为什么需要这样做? 我正在使用它来近似相交波形。我正在使用 MIDI 来控制特斯拉线圈,所以如果我想演奏两个音符(例如,在 440 Hz 和 327 Hz 时,我会以 327、440、654、880 等频率发出脉冲。我可以使用 delayMicroseconds (),但 Arduino 不是多线程的,所以我必须使用这种技术计算直到脉冲的时间。 【参考方案1】:不清楚您对自己的代码不满意的原因。如果是因为有“太多”if
测试,那么没有任何测试就很容易:
def diffgen(a, b):
from itertools import cycle
diffs = []
current = 0
ab = a*b
while current < ab:
nextone = min((current // a + 1) * a,
(current // b + 1) * b)
diffs.append(nextone - current)
yield nextone - current
current = nextone
for d in cycle(diffs):
yield d
请注意,一旦您到达a*b
,差异序列就会重复,因此不再需要计算。
【讨论】:
我喜欢这个! C++ 中是否有类似物(或 Arduino,最好是?) 我努力忘记了所有曾经阻塞我大脑的 C++ ;-) 请注意,您开始使用的代码几乎肯定会明显更快!除法很昂贵,但您使用的所有操作都非常便宜(除了不可预测的分支 - 但我也有其中之一,隐藏在min()
的实现中)。
正如您所说,除法通常很昂贵,并且分支通常在周期数方面更好(比较比除法更容易)。就像在另一个答案中提到的那样,更短的代码并不意味着它在执行时间方面更快。请注意,这是 Python,你可以用短代码做大事 ;-)【参考方案2】:
让我们从一些一般性的观点开始。从你和你的同事都能理解的直观代码开始总是更好。然后测量性能并找到瓶颈。如果您从一开始就尝试超优化,您将:-
编写复杂、容易出错且不易理解的代码。 很可能会优化几乎不会对整体性能产生影响的代码,同时忽略主要瓶颈。除非您从头到尾了解处理器、编译器、编程语言和环境细微差别,否则如果您尝试猜测优化,则很有可能会使性能变得更糟。最好进行测量,找到瓶颈,然后针对这些瓶颈提高性能。如果您怀疑算法/实现速度很慢,请对其进行分析。如果您想知道哪种算法/实现会表现得最好,那就对它们进行竞赛。使用不同的数据集进行测试,因为对于一组输入 (3,5) 表现良好的数据可能不适用于另一组 (3, 500000000)。
话虽如此,让我们从您拥有的东西开始并探索一些选项,然后最终为您在编辑中描述的情况提供一个初始实现,即多个值。请注意,其中一些实现可能不适合您的案例或适用于您的环境,但它们涉及一种通用方法。
现状(您的代码原样)
此代码执行一些条件和算术运算。这些是处理器在早餐时吃的那种操作......在他们醒来之前......在纳米粒子的眼睑眨眼之间,即非常快。现在,我知道您使用的是 Arduino,因此不会有世界上最强大的处理器可供使用,但这些操作仍然是处理器非常快速地完成的。我想创建一些自己的基准,所以我在 C++ 中实现了一个与你非常相似的函数(你提到 C++ 在你的问题中是可以的)。我称测试为ConditionalTest
,因为它遵循if...else
流程并且因为我不擅长命名。
注意:虽然我对结果进行了一些基本测试,但这些答案中提供的代码绝不是生产就绪的。它缺少基本的参数检查(例如空值或未初始化的变量),并且有一些性能优化,我通常会为了安全而忽略这些优化。反正代码是:
static void ConditionalTest( int startA, int startB, unsigned long long testIterations )
gl_result = 0;
gl_current=0;
int distToA = startA;
int distToB = startB;
for( unsigned long long i = 0; i < testIterations; i++ )
if( distToA > distToB ) //B comes first
gl_result = distToB;
gl_current += distToB;
distToA -= distToB;
distToB = startB;
else if( distToB > distToA ) //A comes first
gl_result = distToA;
gl_current += distToA;
distToB -= distToA;
distToA = startA;
else
gl_result = distToA;
gl_current += distToA; //Arbitrarily, could be distToB
distToA = startA;
distToB = startB;
注意:-
我将值分配给全局 gl_result 而不是打印它以节省在我的控制台中填充消息的时间,而且因为打印到屏幕的操作与其他操作相比需要很长时间,因此它会使结果膨胀。 我必须将unsigned long long
用于testIterations
和其他一些变量,否则int
会环绕。
gl_
是测试中的全局变量。
这个算法的好处是它使用了非常基本的结构,所以
即使对编程或其他编程语言有非常基本了解的其他程序员也会很快理解它在做什么。 它非常便携 - 很容易翻译成其他语言和操作系统。 在性能方面,所见即所得 - 所见即所得,因此不太可能隐藏在 3rd 方库调用中的大性能瓶颈。现在,我正在运行一台相当笨拙的机器 (i7 2600),所以它需要 1000000000(10 亿)次迭代才能开始获得超过一秒的结果。在这种情况下,执行 10 亿次迭代平均需要 2400 毫秒。我认为这很快,但让我们看看如何改进。首先让我们看看我们可以调整什么。
对您的实施进行调整
参数是(3,5)
,所以最初distA 是3,distB 是5。注意3 小于5。第一个if
将检查distToA > distToB:
,然后是elif distToB > distToA:
。但是,distToB(最初为 5)比 distToA(最初为 3)的可能性大两倍。出于性能考虑,您希望尽可能频繁地满足第一个if
条件,以尽量减少每次迭代中检查的条件数量。在说这个时,我对编译器做了一些假设,但稍后会更多。
所以,非常简单,我们可以交换ifs
。然而,事情并没有那么简单。我发现的问题是编译器对第二个 if 和 last else 做了一些很好的优化。你看到你在哪里有评论Arbitrarily, could be distToB
?好吧,事实上你在else if
中有gl_current += distToA;
,在else
中有gl_current += distToA
,这一事实允许编译器将其优化为一个语句。因此,在我的情况下,它不是任意的(对您而言,这将取决于您的编译器)。因此,我们需要更改 else 以允许这些优化发生。最终代码为:
static void ConditionalTestV2( int startA, int startB, unsigned long long testIterations )
gl_result = 0;
gl_current=0;
int distToA = startA;
int distToB = startB;
for( unsigned long long i = 0; i < testIterations; i++ )
if( distToB > distToA ) //A comes first (where a is more likely to be first than b)
gl_result = distToA;
gl_current += distToA;
distToB -= distToA;
distToA = startA;
else if( distToA > distToB ) //B comes first
gl_result = distToB;
gl_current += distToB;
distToA -= distToB;
distToB = startB;
else
gl_result = distToB; //Should be distToB for optimisations
gl_current += distToB; //Should be distToB for optimisations
distToA = startA;
distToB = startB;
注意:if( distToB > distToA )
在 else if( distToA > distToB )
之前,而 else 现在有 gl_result = distToB
和 gl_current += distToB
。有了这些更改,运行测试所需的时间为:2108 毫秒。很高兴这些简单的调整减少了 12% 的执行时间。
从中获得的最大教训是衡量您为意外后果所做的任何更改。
您的编译器和执行环境可能与我的不同,因此您的结果可能会有所不同。如果您要开始在这个级别上进行调整,我建议您熟悉汇编程序并在关键点逐步完成汇编,以确定条件实际上是如何实现的。我确信还可以进行其他优化,例如这些。如果你真的进入它并且使用的是 GNU C++,那么有一个叫做 __builtin_expect
的东西,你可以在其中指导编译器选择哪个分支。
您可能并不总是按顺序获得起始值,在这种情况下,您需要根据执行算法的总时间来计算一次性排序的成本。
需要指出的其他一些事情是:-
您维护一个变量current
,但您不使用它。如果您不使用current
,则可以将其删除。如果编译器已经对其进行了优化,您可能看不到性能提升。
您的范围为 100,但循环将重复 3 * 5 = 15 次。因此,您可以在 current 为 15 时停止(如果这是您所需要的),或者您可以存储结果然后将它们写出来(请参阅模式部分)。
模数
查看算法,我们总是得到一个值的距离,所以想到的一种方法是取模(已经有一个答案可以涵盖这个问题)。我对性能有点怀疑,因为模倾向于使用比减法运算慢的除法。无论如何,这就是我想出的:
static void ModuloTest( int startA, int startB, unsigned long long testIterations )
unsigned long long current = 0;
unsigned long long prev = 0;
int distToA = startA;
int distToB = startB;
for( long long i = 0; i < testIterations; i++ )
current += (gl_result = FastMin(distToA - (current%distToA), distToB - (current%distToB)));
结果是 23349 毫秒。几乎比原来的慢 10 倍。
现在,我通常不会写像 current += (gl...
这样的行,但我试图减少分配的数量。这通常是一件愚蠢的事情,因为编译器会比我优化得更好,而且更容易出错。尽管如此,这个测试还是有点慢,我想确保我给了它一个很好的机会。由于流程有点不同,因此立即开始将手指指向模有点不公平,所以也许是其他原因。所以,我做了一个更简单的模数测试:
static void PureModuloTest( unsigned long long testIterations, unsigned long long mod )
for(long long i = 1; i <= testIterations; i++)
gl_result = mod % i;
其中 mod 是 50000,即使在这种情况下,测试所用的时间也比您的测试长 5 倍,所以我认为如果我们正在寻找纯粹的性能提升,那么 modulo 已经不存在了。我还发现 stl min() 有一些令人惊讶的低效率,但详细介绍会使这篇长篇文章变得更长。
接下来我做的是查看数据。有时,如果您可以在数据中找到特征/模式,您可以相应地优化您的实施。
模式
再次查看您的数据,会发现差异会在每a * b
个周期重复一次。所以,在你的测试中,一旦你达到 15,距离就会重复。您可能已经意识到这一点,但是在您的代码 sn-p 中,您运行了 100 个周期 (for i in xrange(100)
) 的测试,所以我不确定。
使用这个事实的一种方法是存储值直到我们到达a * b
,然后重复使用这些值直到我们完成。请注意,这本质上是使用您的算法开始,然后从那时起遍历列表的问题。
static void PatternTest( int startA, int startB, unsigned long long testIterations )
int stop = startA * startB;
list<int> resultList;
int distToA = startA;
int distToB = startB;
int val = 0;
long long count = 0;
while( val < stop )
if( distToB > distToA ) //A comes first (where a is more likely to be first than b)
gl_result = distToA;
distToB -= distToA;
distToA = startA;
else if( distToA > distToB ) //B comes first
gl_result = distToB;
distToA -= distToB;
distToB = startB;
else
gl_result = distToB;
distToA = startA;
distToB = startB;
val += gl_result;
resultList.push_back(gl_result);
count++;
std::list<int>::const_iterator iter;
while( count < testIterations )
for( iter = resultList.begin(); iter != resultList.end() && count < testIterations; iter++ )
gl_result = *iter;
count++;
此测试耗时 1711 毫秒,比原始测试快 29%,比当前最佳测试快 18%。我不确定这在您的情况下有多适用,但它是分析预期数据如何提供一些良好性能提升的一个示例。
线程富矿!
现在,这可能不适用于您的情况,因为您正在使用 Arduino。但也许将来会支持线程,或者您可以将问题转移到不同的处理器。无论哪种方式,不包括线程基准都是不友好的,因为这是他们的生活。另外,我的电脑有 8 个核心,其中 7 个核心都在闲逛,所以很高兴给他们一个狂奔的机会。
如果您的数据或算法可以分解为独立的离散部分,那么您可以设计您的程序,使其在不同的线程上运行独立的操作。现在我们从之前知道该序列每a * b
重复一次。所以,我们可以从不同的点 n
开始,其中 '(n modulo (a * b)) == 0'。
但是,我们可以做得更好,首先获取第一个 a * b
的值,然后循环遍历各个线程上的值。这就是我在这里所做的。我选择运行 4 个线程。
struct BonanzaThreadInfo
long long iterations;
list<int> resultList;
int result;
;
static void BonanzaTestThread( void* param )
BonanzaThreadInfo* info = (BonanzaThreadInfo*)param;
std::list<int>::const_iterator iter;
for( long long count = 0; count < info->iterations; )
for( iter = info->resultList.begin(); iter != info->resultList.end() && count < info->iterations; iter++ )
info->result = *iter;
count++;
delete param;
static void ThreadBonanzaTest( int startA, int startB, unsigned long long testIterations )
int stop = startA * startB;
list<int> resultList;
int distToA = startA;
int distToB = startB;
int val = 0;
long long count = 0;
while( val < stop )
if( distToB > distToA ) //A comes first (where a is more likely to be first than b)
gl_result = distToA;
distToB -= distToA;
distToA = startA;
else if( distToA > distToB ) //B comes first
gl_result = distToB;
distToA -= distToB;
distToB = startB;
else
gl_result = distToB;
distToA = startA;
distToB = startB;
val += gl_result;
resultList.push_back(gl_result);
count++;
long long threadIterations = (testIterations - count) / NUMTHREADS;
long long iterationsLeft = testIterations-count;
thread* bonanzaThreads = new thread[NUMTHREADS];
for( int i = 0; i < NUMTHREADS; i++ )
BonanzaThreadInfo* bonanzaThreadInfo = new BonanzaThreadInfo;
if( i == (NUMTHREADS - 1) )
bonanzaThreadInfo->iterations = iterationsLeft;
else
iterationsLeft -= threadIterations;
bonanzaThreadInfo->iterations = (threadIterations);
bonanzaThreadInfo->resultList = resultList;
bonanzaThreads[i] = thread(BonanzaTestThread,bonanzaThreadInfo);//http://***.com/a/10662506/746754
for( int i = 0; i < NUMTHREADS; i++ )
bonanzaThreads[i].join();
delete [] bonanzaThreads;
结果是这花费了 574 毫秒。高达 76% 的节省!关于线程的一些基本注意事项:-
复杂性和出错空间急剧增加。 如果线程之间有任何共享资源,则该资源将需要由互斥锁保护。如果线程经常同时需要相同的受保护资源,则需要该资源的所有线程都需要等待,直到它可用,如果性能非常差,可能会导致。这是我们目前为止的图表:
现在,关于多个值的编辑。
多个值
好吧,据我所知,如果您有多个输入值(a、b、c、d...),您的if
语句将很快变得非常嵌套和长度。
if a < b && a < c && a < d...
我们通常会尝试订购下一个值,所以这就是我要开始的地方。我的第一个想法是将值存储在一些有序的数据结构中。我选择使用集合是因为集合自然是按键排序的(实际上它是一个多重集合,因为我们需要允许欺骗)。在集合中,我放置了一个结构(称为 ValuesStruct,因为我在名称方面非常很糟糕),其中包含要递增 (a,b,c) 的值以及该值将在其中递增的下一个整数成为最接近的。 <
运算符使 stl 知道将该值放在集合中的哪个位置。
struct ValuesStruct
public:
int Value;
long long Next;
ValuesStruct( int start )
Value = start;
Next = start;
bool operator < (const ValuesStruct& rOther) const
return (Next < rOther.Next);
private:
ValuesStruct()
;
然后,我需要做的就是遍历集合。在每次迭代中,集合的前面将具有最小的下一个值。所以我可以通过从中减去前一个来计算当前间隔。然后,我只需要一个do..while()
循环来从列表中删除这个值,并将它与更新的 Next 值一起添加回来,这样它就会在集合中占据适当的位置。我需要对所有具有 Next
的值执行此操作(例如,对于您的简单 3,5 示例,15 就是这种情况)。我将测试称为MultiConditionalTest
,因为这里我们需要检查多个比较条件,而且我的名字很糟糕。
static void MultiConditionalTest( multiset<ValuesStruct>& values, unsigned long long testIterations )
unsigned long long prev = 0;
for( unsigned long long i = 0; i < testIterations; i++ )
multiset<ValuesStruct>::iterator iter = values.begin();
gl_result = (*(iter)).Next - prev;
prev = (*(iter)).Next;
do //handle case where equal
ValuesStruct valuesStruct = *iter;
values.erase(iter);
valuesStruct.Next += valuesStruct.Value;
values.insert( valuesStruct );
iter = values.begin();
while( (*iter).Next == prev );
函数使用如下:
multiset<ValuesStruct> values;
values.insert(ValuesStruct(3));
values.insert(ValuesStruct(5));
values.insert(ValuesStruct(7));
values.insert(ValuesStruct(11));
MultiConditionalTest( values, testIterations );
正如您所看到的,这里发生了很多事情,所以我预计会出现一些性能井喷并得到了它:105156 毫秒 - 大约慢了 50 倍。每次迭代仍然不到一微秒,所以这再次取决于您的目标。由于我今天晚上只是在没有对其进行太多分析的情况下就解决了这个问题,因此我很确定可以进行性能优化。首先,该集合通常实现为二叉搜索树。我会做一些研究并确定这是否是解决这个问题的最佳数据结构。此外,当向列表中插入一个新值时,可以给出关于它的放置位置的提示。如果我们在选择位置方面很聪明,那么我们也许可以加快这个操作。此外,和以前一样,当我们到达 (a * b * c * d...) 时,序列将重复,所以我们可以存储这些值,然后从那时起将它们写出来。我还会查看问题空间,看看是否有优化算法的方法,可能会在 math.stackexchange.com 上询问数学序列——这些人非常敏锐。
无论如何,这只是一个选项,它可能适合您,也可能不适合您,具体取决于您的实际性能要求。
其他一些想法:
-
您获得同一组值(a、b、c、d...)的可能性有多大?如果这很可能,您可能希望缓存以前的结果。然后只需从缓存数组中读取它们,这将非常快。
另一种提高性能的方法是打开编译器优化。您如何执行此操作以及它的有效性取决于您的编译器。
祝你好运。
【讨论】:
简直太棒了。关于您的最后一个问题,我从音符的频率(以赫兹为单位)中获得这些值,因此重复的和弦将具有相同的循环特性;但是,我循环的值可能不是整数。再次,谢谢你。像这样的答案让 *** 变得很棒。【参考方案3】:这是一种使用模运算的方法:
a = 3
b = 5
current = 0
def nearest_multiple_of_a_or_b_to_current(current, a, b):
distance_to_a = (a - current%a)
distance_to_b = (b - current%b)
return current + min(distance_to_a, distance_to_b)
for i in range(100):
next = nearest_multiple_of_a_or_b_to_current(current, a, b)
print(next - current)
current = next
输出:
3
2
1
3
1
2
3
3
2
1
【讨论】:
模缩短代码。不确定它是否比每个循环中的单减法和加法(当两者相等时可能是双加法)作为替代方案在性能上更好?不过,我认为任何一个都足够快,性能不太可能成为问题。 你知道模数是如何在内部实现的吗?它是减法直到你正在修改的东西还是更聪明? 这个链接是关于在 python 中实现模数的讨论,尽管原始帖子说它实际上不会在 python 中运行。 ***.com/a/18200092/947305 在几乎所有编程语言中,模数都是通过除法、乘法和减法实现的——除非模数是一个常数,编译器可以将其识别为一个简单的特殊情况(如% 7
应用于C 中的无符号整数 - 然后可以屏蔽最后 3 位)。
我认为模数通常被实现为除法代码/硬件的辅助输出。我很确定 Python 至少是这样做的。以上是关于计算两个不同数字的倍数之差的主要内容,如果未能解决你的问题,请参考以下文章