我来自C语言,现在才刚开始使用C ++。
这也意味着我不专注于算法,而是更专注于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,并在收到反馈后对其进行更改。
随时更改现有解决方案。

评论

值得一提的是,欧拉计划非常重视数学算法。其他编程挑战站点之一可能更适合您。 codekata.com或hackerrank.com仅举几例。

@bruglesco在完成第一个挑战并展望未来之后,我意识到欧拉计画专注于算法。我将在codekata.com上进行代码挑战,以进行下一次审核。

为了补充@bruglesco的评论,adventofcode.com拥有与语言无关的谜题,我发现这是认识语言的好方法,因为它会迫使您使用多种技术和工具。

#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 ++首次出现在表中的内容之一!我们得到了constinline而不是#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是一个常数,因为你真的可以做些constconstexpr-两者之间是有区别的,但这没关系。根据要编译的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 \ $
将其扩展为两个以上的数字的方法将使用中的std :: lcm()计算数字对的最小公倍数,然后使用包含-排除原理将每个数字的倍数相加数字,减去每个数字对的LCM的倍数,加回每个数字三对的LCM的倍数,依此类推。
\ $ \ 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