这也意味着我不专注于算法,而是更专注于C ++习惯用法。
描述:
如果列出所有低于10的自然数,它们是3或5的倍数,则得到3、5、6和9。这些倍数的总和为23。
找到1000以下的3或5的所有倍数之和。
main.cpp:
#include <iostream>
#define UPPER_BOUND 1000
int main() {
int sum = 0;
for (int i = 1; i < UPPER_BOUND; ++i) {
if (i % 3 == 0 || i % 5 == 0) {
sum += i;
}
}
std::cout << "The sum of the natural numbers below " << UPPER_BOUND << " that are multiples of 3 and 5 is " << sum;
return 0;
}
我将添加将文件提交到GitLab,并在收到反馈后对其进行更改。
随时更改现有解决方案。
#1 楼
constexpr
一切除了
#define
之外,您的代码还不错。但是您没有使用constexpr
错过了一个真正简单的优化,constexpr variables
在不同的上下文中具有不同的含义:constexpr functions
是编译时常量。如果常量的参数在编译时已知,则它们可以在编译时执行;如果它们的参数在编译时已知,则它们可以很好地替代常量定义的宏。然后编译器将常量替换为代码中的函数调用。所以让我们从
main()
中提取一个函数并声明为constexpr
:#define UPPER_BOUND 1000 // bad, because preprocessing leads to a lot of bugs
constexpr int upper_bound = 1000; // good, safe
现在,每当在编译时使用已知的
upper_bound
调用它时,它将在编译时运行。将代码通用化
在以下位置运行函数编译时意义重大,因为如果不能,并且需要代码在运行时快速运行,则必须实现更好的算法。正如@Dannnno指出的那样,您可以使用该算法来计算算术级数,对于您的代码,它是
O(1)
而不是O(n)
:constexpr int sum_of_multiples_of_3_and_5_under(int upper_bound) {
int sum = 0;
for (int i = 1; i < upper_bound; ++i) {
if ( i % 3 == 0 || i % 5 == 0 ) {
sum += i;
}
}
return sum;
}
然后您可以计算3和5的倍数之和与:
constexpr int sum_multiples_under(int ratio, int upper_bound) {
int nb_val = (upper_bound - 1) / ratio;
int high = nb_val * ratio;
int extremes = ratio + high;
return nb_val * extremes / 2;
}
即使它比
for
-loop消耗了更多的脑力,它也很容易实现。但是,假设我们要泛化我们的代码,并使它适用于任意数量的除数,例如3、5和7的倍数之和。这将变得更加困难:您必须添加一个的所有成员集合,然后减去两个集合的所有成员,然后相加三个集合的所有成员...等等。这意味着计算组合,将函数应用于排列等。而我们的for
-loop可以轻松缩放。使用折叠表达式
通过折叠表达式(需要C ++ 17),您可以相当轻松地泛化代码:
auto sum = sum_multiples_under(3, 1000)
+ sum_multiples_under(5, 1000)
- sum_multiples_under(15, 1000);
Multiples
是可变参数模板参数。它可以代表任意数量的参数。然后,您可以在函数内部扩展参数的包。折叠表达式使您可以在运算符周围展开它。在这种情况下,运算符为||
。 ( ... || (i % multiples == 0))
将扩展为:template <typename... Multiples>
constexpr int sum_of_multiples_under(int upper_bound, Multiples... multiples) {
int sum = 0;
for (int i = 1; i < upper_bound; ++i) {
if ( ( ... || (i % multiples == 0)) ) {
sum += i;
}
}
return sum;
}
结论
constexpr
是一个非常非常有用的关键字,并且是现代,优化的C ++的标志。码。无节制地使用它。如果您想了解所有相关信息,YouTube上有一个主题演讲,名为“ constexpr everyting”。评论
\ $ \ begingroup \ $
您是指“ constexpr所有事物”吗?
\ $ \ endgroup \ $
–Rakete1111
18年6月21日在14:29
\ $ \ begingroup \ $
很可能,我不记得确切的标题了
\ $ \ endgroup \ $
– papagaga
18年6月21日在14:38
\ $ \ begingroup \ $
折叠表情对我来说绝对是新的!我很快将仔细研究它们。
\ $ \ endgroup \ $
– FromTheStackAndBack
18年6月22日在5:08
\ $ \ begingroup \ $
在发布此答案之前,我将通过将所有模值存储在向量中并在if语句中对其进行迭代来概括代码。我认为您的解决方案比我的解决方案干净得多。
\ $ \ endgroup \ $
– FromTheStackAndBack
18年6月22日在5:09
\ $ \ begingroup \ $
“不好,因为预处理会导致很多错误”您可能不应该进行那种预处理,而只能使用无错误的预处理。
\ $ \ endgroup \ $
– bibll
18年6月22日在5:50
#2 楼
我不是在专注于算法,而是在C ++惯用语上。
非常好的陈述了您想要的东西。太好了!
我建议您熟悉Bjarne Stroustrup和Herb Sutter分类的核心准则。
#define UPPER_BOUND 1000
不要使用
#define
用于常量或“函数”(⧺ES.31)。这是古代C ++首次出现在表中的内容之一!我们得到了
const
和inline
而不是#define所遇到的所有问题。即使使用前缀增量,循环本身也足够响亮。您将需要其他(非std)库来使用range-for编写整洁的代码。
但是,您根本不应该编写循环! Stroustrup的视频会说:“不要“编写代码”,请使用算法。”您需要count_if,因为您要问“有多少项满足该谓词”。 (更新:仔细阅读后,我看到您不是在计数而是在求和。这正是从低级循环中提取高级含义的危险!)但是,您只是在对数字进行计数而不是要通过的集合,因此这不是正常情况。
std::generate_n
将产生数字列表,但不能以可用于算法的形式显示!它会推动,而count_if
会被拉。其他库:Boost.Range v2或即将发布的Range.v3,将非常简单。许多程序员使用Boost或有自己的计数范围(请参阅我自己的教程和附带的卡通插图☺)。因此,有了手头的库,我可能会写类似以下内容:
auto r = int_range (1, upper_bound);
auto wanted = [](int i){return (i % 3 == 0 || i % 5 == 0); }
auto sum= accumulate (r|filtered(wanted));
由于这里是实际工作,因此
for
是如此琐碎,以至于对其进行任何详细说明都会使其变得更多代码。但是,想象一下业务逻辑中一个更现实的复杂情况。该算法(累加)以您正在执行的操作命名。选择标准在其自己的语句中被隔离,并被命名为Wanted,因此以后更容易编辑逻辑而又不会破坏任何内容。这里的两个问题是所需的惯用语领先于适当的库/编译器支持;并使用一个琐碎的程序,似乎需要人工和繁琐的工作,以在其周围放置任何正式结构或“高级工程”。
可能有用的是验证您的总和没有溢出。这是我在审阅时注意到的内容,因此请评论一下您认为这是个好主意。
// sum will be a subset of the sum of all numbers in the range,
// so must be < (n+1)*(n/2) ≈ 500'000, which is well within the range
// of a 32-bit int.
评论
\ $ \ begingroup \ $
我喜欢这种编码风格。但是它可以解释吗?仅在OP代码之前添加constexpr就足以使其可在编译时进行计算,这是一个优点,您不应该只为编码风格而放弃。
\ $ \ endgroup \ $
– papagaga
18年6月21日在10:56
\ $ \ begingroup \ $
好答案!但我认为,对于过滤器,multipleOf3or5比需要的名称更好。
\ $ \ endgroup \ $
– Alex Reinking
18年6月21日在18:29
\ $ \ begingroup \ $
范围V3不是“即将推出”。已经出来了s已经是一个库。s您可能是指Ranges TS
\ $ \ endgroup \ $
–贾斯汀
18年6月21日在19:30
\ $ \ begingroup \ $
@Justin我指的是可用性,可用性和广泛采用。它仍然不能在Microsoft的编译器上运行,并且在出现错误消息时遇到困难,因此,非专家将无法使用它,并且各种编译器也会出现故障。
\ $ \ endgroup \ $
–JDługosz
18年6月22日在6:42
#3 楼
您的编码样式可能更简洁;最好将其分解为一个函数,如下所示:#include <iostream>
#define UPPER_BOUND 1000
int sum_of_sequence(int first_multiple, int second_multiple, int max_value)
{
int sum = 0;
for (int i = 1; i < max_value; ++i) {
if (i % first_multiple == 0 || i % second_multiple == 0 ) {
sum += i;
}
}
}
int main() {
int sum = sum_of_sequence(3, 5, UPPER_BOUND);
std::cout << "The sum of the natural numbers below " << UPPER_BOUND << " that are multiples of 3 and 5 is " << sum;
return 0;
}
如果您决定要使用某个功能,则它具有更多的可配置性。不同的数字集。
我不喜欢你
#define
是一个常数,因为你真的可以做些const
或constexpr
-两者之间是有区别的,但这没关系。根据要编译的C ++标准的版本,如果适当地使用constexpr
,可能可以在编译时完成工作。您可以开始循环以最低的倍数,然后从那里开始递增;这样可以节省您的迭代次数。
您可以拥有更聪明的算法。对于算术序列,第一个\ $ n \ $项的总和为$$ S_n = \ frac {n(a_1 + a_n)} {2} $$
如果我们用它来计算3的前333倍和5的前199倍的总和将接近我们的答案(333,因为第334的倍数大于1000,并且对199的逻辑相同)。问题在于,我们将重复计算3和5的倍数,即15的倍数(3和5之间的最小公倍数)。因此,如果我们要计算这个值:
$$ \ frac {333(3 + 999)} {2} + \ frac {199(5 + 995)} {2}-\ frac {66 (15 + 990)} {2} $$
我们会得到正确的答案。您可以使用一些简单的函数(可能比它们所需的简单一些)来表达这一点。
int num_multiples_below_max(int value, int max)
{
return (max - 1) / value;
}
int largest_value(int value, count)
{
return value * count;
}
int sum_arithmetic_sequence_below_max(int value, int max_value)
{
return sum_arithmetic_sequence(value, num_multiples_below_max(value, max_value));
}
int sum_arithmetic_sequence(int value, int count)
{
return count * (value + largest_value(value, count)) / 2;
}
int least_common_multiple(int a, int b)
{
return a * b;
}
int sum_of_multiples_below_max(int first_multiple, int second_multiple, int max_value)
{
int sum_first_multiple = sum_arithmetic_sequence_below_max(first_multiple, max_value);
int sum_second_multiple = sum_arithmetic_sequence_below_max(second_multiple, max_value);
int lcm = least_common_multiple(first_multiple, second_multiple);
int sum_least_common_multiple = sum_arithmetic_sequence_below_max(lcm, max_value);
return sum_first_multiple + sum_second_multiple - sum_least_common_multiple;
}
您可以将其扩展到更多而不是2的倍数,但是我忘记了确切的算法,我的Google-fu目前似乎让我失望了。我将其留给用户练习。
评论
\ $ \ begingroup \ $
我认为这有点过头了。我同意必须对代码进行泛化和扩展,但是如果最终编写的代码行多于两倍(因为您还没有主代码行),那么您最好真正需要它们。此外,OP还指定了对C ++惯用语而不是算法的更多关注。您的第一段代码看起来更好,并且对我而言更具可读性。
\ $ \ endgroup \ $
–ChatterOne
18年6月21日在7:27
\ $ \ begingroup \ $
我同意这太过分了;正如我提到的那样,此功能被拆分为多余的功能。您可以很容易地清理它以减少空间,但是为了解释算法,我将其拆分了。
\ $ \ endgroup \ $
–丹农
18年6月21日在16:00
\ $ \ begingroup \ $
将其扩展为两个以上的数字的方法将使用
\ $ \ endgroup \ $
–戴维斯洛
18年6月21日在22:25
\ $ \ begingroup \ $
@Davislor是正确的,但请注意,结果花费的时间大约为Θ(2 ^ k),其中k是“倍数”的数量,而对于朴素的版本,大约为Θ(nk),其中n为总和的上限。在这个答案中,我们有k = 2,这在实践中显然比线性时间算法要好得多-但是,如果您有50的倍数并且总和达到一百万,则应该采用朴素算法。
\ $ \ endgroup \ $
– wchargin
18年6月21日在22:32
\ $ \ begingroup \ $
@wchargin另外请注意,您仍然可以对天真的算法进行一些优化。例如,您可以将范围划分为因子的最小公倍数的残基。
\ $ \ endgroup \ $
–戴维斯洛
18年6月21日在22:42
#4 楼
这是一个错误。该任务询问:
找到1000以下的3或5的所有倍数之和。
,但是您的程序报告:
“低于1000的自然数之和是3和5的倍数……”
您的代码计算得出正确的总和,但该总和与您所表示的含义不符!
这是一个小的优化:
鉴于两个倍数中的最小值为3,您可以从3开始而不是从1开始
for
循环。显然,这里并没有节省太多,但是想象一个类似的任务可以处理117或209的倍数。
for (int i = 3; i < UPPER_BOUND; ++i) {
if (i % 3 == 0 || i % 5 == 0) {
sum += i;
}
评论
\ $ \ begingroup \ $
它要求或,而不是xor。 “ 3和5的倍数”分别是0、15、30,...
\ $ \ endgroup \ $
– FromTheStackAndBack
18年7月4日在3:51
评论
值得一提的是,欧拉计划非常重视数学算法。其他编程挑战站点之一可能更适合您。 codekata.com或hackerrank.com仅举几例。@bruglesco在完成第一个挑战并展望未来之后,我意识到欧拉计画专注于算法。我将在codekata.com上进行代码挑战,以进行下一次审核。
为了补充@bruglesco的评论,adventofcode.com拥有与语言无关的谜题,我发现这是认识语言的好方法,因为它会迫使您使用多种技术和工具。