Learn C++ STL

Lesson 1 of 9 · The Algorithms You Will Use Weekly: accumulate to nth_element

Module 14 · The Algorithms You Will Use Weekly: accumulate to nth_element

Algorithms: One Call Instead of a Loop, and Every Order of a Set

FreeReading

In this lesson

  • Replace a hand-written loop for a total, a largest value, a count and a reversal with accumulate, max_element, count and reverse, each over a range [first, last).
  • Visit every order of a set with sort and a do-while around next_permutation, and explain why the sort comes first.
  • Choose accumulate's start value, 0, 0LL or 0.0, for the type the answer needs, and state what each call costs.

Bob is seating friends in a row of chairs for a photo, and he wants to try every seating. For three friends he writes three nested loops, and for six friends six. Then Maria tells him that nobody knows how many friends will come, and no fixed nest of loops fits that. At the end of the day, Amara asks him for the café's total, its biggest hour and how many hours sold 30 cups. He writes three more loops, and one of them starts at index 1 instead of 0.

Each of those jobs is one call in the standard library. This lesson shows the calls, how they see a container, and what each one costs.

An algorithm is a function that takes a range

In the STL, an algorithm is a function that works on a range of elements, not on a container. You hand it two iterators from Module 11, first and last. It uses first and every element after it, up to last but not including it. That is the half-open range [first, last).

The algorithms that do arithmetic, such as accumulate, live in the header <numeric>. The rest, such as count, max_element, reverse and next_permutation, live in <algorithm>. Here are two calls, each on the whole vector and then on a part of it.

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

int main() {
    vector<int> sold{4, 7, 2, 9, 5, 7};
    cout << accumulate(sold.begin(), sold.end(), 0) << '\n';
    cout << accumulate(sold.begin() + 1, sold.begin() + 4, 0) << '\n';
    cout << count(sold.begin(), sold.end(), 7) << '\n';
    cout << count(sold.begin(), sold.begin() + 3, 7) << '\n';
    return 0;
}
34
18
2
1

The whole vector adds up to 34. sold.begin() + 1 to sold.begin() + 4 covers positions 1, 2 and 3, so the second line is 7 + 2 + 9 = 18. count found two 7s in the whole vector, and one in its first three positions.

Two half-open ranges over one vector One vector, two ranges: the whole of it, and positions 1 to 3 0 1 2 3 4 5 end() 4 7 2 9 5 7 sold.begin() to sold.end(): 4 + 7 + 2 + 9 + 5 + 7 = 34 sold.begin() + 1 to sold.begin() + 4: 7 + 2 + 9 = 18 last is never used: position 4 is outside the second range, and end() holds no element.
Figure 1. A range is [first, last): first is used and last is not. The whole vector and its positions 1 to 3 are both ranges.

So the same call works on a whole vector or on any stretch of it, and only the two iterators change.

Four loops you no longer write

Here are Amara's end-of-day numbers: cups sold in each of five hours. The program answers each question twice, first with Bob's hand loops, then with one call each. Then it turns the vector around with reverse.

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

int main() {
    vector<int> cups{12, 30, 18, 30, 9};

    int total = 0;
    for (int x : cups) {
        total += x;
    }
    int biggest = cups[0];
    for (int x : cups) {
        if (x > biggest) {
            biggest = x;
        }
    }
    int thirties = 0;
    for (int x : cups) {
        if (x == 30) {
            thirties++;
        }
    }
    cout << "loops: " << total << ' ' << biggest << ' ' << thirties << '\n';

    cout << "calls: " << accumulate(cups.begin(), cups.end(), 0) << ' '
         << *max_element(cups.begin(), cups.end()) << ' '
         << count(cups.begin(), cups.end(), 30) << '\n';

    reverse(cups.begin(), cups.end());
    cout << "reversed:";
    for (int x : cups) {
        cout << ' ' << x;
    }
    cout << '\n';
    return 0;
}
loops: 99 30 2
calls: 99 30 2
reversed: 9 30 18 30 12
JobThe hand loopOne call
the totala running sum, started at 0accumulate(v.begin(), v.end(), 0)
the largesta best-so-far, started at the first element*max_element(v.begin(), v.end())
how many equal xa counter and an ifcount(v.begin(), v.end(), x)
back to fronttwo indexes swapping toward the middlereverse(v.begin(), v.end())

Both ways print the same numbers. The calls have no index to start at 1 by mistake, and no best-so-far to start at the wrong value. There is no <= to type in place of < either. max_element returns an iterator, not a value, so the program reads the value with *. So each call is one line that names its job, and the loop is gone.

Every order of a set: next_permutation

Bob's seatings are permutations: every order of the same items. Three friends can sit in 3 × 2 × 1 = 6 orders, written 3! and read "3 factorial". To list them without nested loops, put the orders in dictionary order. Compare two orders by their first item, then by the second if the first ones tie, as a dictionary compares words.

next_permutation(first, last) rearranges the range into the next order in dictionary order and returns true. When the range already holds the last order, the largest, it returns false. It also rearranges the range back into the first order, the smallest. So a loop that calls it until it says false visits every order once.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> seat{1, 2, 3};
    int orders = 0;
    do {
        cout << seat[0] << ' ' << seat[1] << ' ' << seat[2] << '\n';
        orders++;
    } while (next_permutation(seat.begin(), seat.end()));
    cout << orders << " orders, and seat is back to " << seat[0] << ' ' << seat[1] << ' ' << seat[2] << '\n';
    return 0;
}
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
6 orders, and seat is back to 1 2 3

The loop is a do-while because the first order, 1 2 3, must be used before the first call moves it. After 3 2 1 the call returned false and left 1 2 3 behind, so the loop stopped with the vector back where it began. The same five lines work for 3 friends, 6 friends or n friends. So next_permutation replaces Bob's nest of loops, whatever the number of chairs.

Watch next_permutation step through 1 2 3

Each call makes the smallest change that gives a bigger order, in four moves. It finds the last rise: the rightmost position i where v[i] < v[i + 1]. It finds the rightmost element after it that is bigger than v[i], and swaps the two. Then it reverses everything after position i. If there is no rise at all, the range is the largest order, so the call reverses the whole range and returns false.

The widget plays the six calls on vector<int> v{1, 2, 3}, one row per call. Every row was printed by a program on Compiler Explorer that made these moves by hand, and every row was checked against std::next_permutation.

Six calls of next_permutation on vector<int> v{1, 2, 3}, one row per call.

CallBeforeLast rise atSwap withAfter the swapAfter the reverseReturns
11 2 3v[1] = 2v[2] = 31 3 21 3 2true
21 3 2v[0] = 1v[2] = 22 3 12 1 3true
32 1 3v[1] = 1v[2] = 32 3 12 3 1true
42 3 1v[0] = 2v[1] = 33 2 13 1 2true
53 1 2v[1] = 1v[2] = 23 2 13 2 1true
63 2 1none--1 2 3false

Call 2 shows all four moves. The rise is at the 1, the 1 swaps with the 2, and the tail 3 1 turns into 1 3. Call 6 finds no rise in 3 2 1, so it reverses everything into 1 2 3 and returns false. Lesson 05 reads GCC 12's own code for these moves. So each call changes as little as it can, and the orders come out smallest first.

The syntax you need on day one

The first algorithms, and the loop that visits every order

#include <numeric>      accumulate, and the other arithmetic algorithms
#include <algorithm>    count, max_element, min_element, reverse, sort, next_permutation

accumulate(first, last, init)     init plus every element; the result has init's type
count(first, last, x)             how many elements equal x
max_element(first, last)          an iterator to the first largest element
min_element(first, last)          an iterator to the first smallest element
*it                               the element an iterator points at
it - v.begin()                    that element's index in a vector
reverse(first, last)              turn the range around, in place

sort(v.begin(), v.end());                          start from the smallest order
do {
    ... use v ...
} while (next_permutation(v.begin(), v.end()));   every order once, smallest first
  • first and last are iterators, usually v.begin() and v.end(). Any stretch works, such as v.begin() + 1 to v.begin() + 4.
  • accumulate's third argument is the start value, and it decides the type of the answer: 0 gives an int, 0LL a long long, 0.0 a double.
  • max_element and min_element return an iterator. Read the value with *it, and the index with it - v.begin().
  • reverse and next_permutation change the range itself. next_permutation returns true while there is a next order, and false after the last one.

So two headers and seven calls cover this lesson, and every call takes its range the same way.

Sort first, or you miss orders

next_permutation only moves forward in dictionary order. Start it in the middle, and it never visits the orders that come before the start. This program counts the orders from 2 1 3, then sorts and counts again.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int countOrders(vector<int> v) {
    int orders = 0;
    do {
        orders++;
    } while (next_permutation(v.begin(), v.end()));
    return orders;
}

int main() {
    vector<int> friends{2, 1, 3};
    cout << "from 2 1 3: " << countOrders(friends) << " orders\n";
    sort(friends.begin(), friends.end());
    cout << "from 1 2 3: " << countOrders(friends) << " orders\n";
    return 0;
}
from 2 1 3: 4 orders
from 1 2 3: 6 orders

From 2 1 3 the loop saw 2 1 3, 2 3 1, 3 1 2 and 3 2 1: four of the six. The two orders before the start, 1 2 3 and 1 3 2, were never visited. After sort, all six came out. With a repeated value, each different order comes out once: 1 1 2 gives three orders, not six, and lesson 02 steps through them. So sort first, every time.

The start value sets the type

accumulate keeps its running total in the type of its start value, not in the type of the elements. That matters at both ends: for big sums and for fractions.

#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

int main() {
    vector<int> steps(100000, 100000);
    cout << accumulate(steps.begin(), steps.end(), 0) << '\n';
    cout << accumulate(steps.begin(), steps.end(), 0LL) << '\n';

    vector<double> prices{1.5, 2.5, 3.5};
    cout << accumulate(prices.begin(), prices.end(), 0) << '\n';
    cout << accumulate(prices.begin(), prices.end(), 0.0) << '\n';
    return 0;
}
1410065408
10000000000
6
7.5

100000 days of 100000 steps make 10000000000, far past the largest int, 2147483647. With 0 the total is an int, so it wrapped, and GCC 12 printed 1410065408. Signed overflow is undefined behaviour, so that number is what this compiler did, not a promise; lesson 05 explains where it comes from. With 0LL the total is a long long, and the answer is right.

The prices add up to 7.5, but 0 made the total an int again. Each step was cut back to a whole number: 0 + 1.5 became 1, then 3.5 became 3, then 6.5 became 6. With 0.0 the total is a double. So write the start value in the type of the answer you want.

Start valueTotal kept asUse it for
0intsmall sums of whole numbers
0LLlong longsums that may pass 2147483647
0.0doubleprices, averages, anything with a fraction

What each call costs

accumulate, count, max_element and min_element look at each element once: n steps for n elements, O(n). reverse makes n / 2 swaps, also O(n). A hand loop doing the same job costs the same, so the call is never the slow choice.

One call of next_permutation costs O(n) at worst, when it reverses the whole range, as call 6 did. Over all the orders, though, it averages about 3 comparisons a call: 2.5 at n = 3 and 3.08 at n = 10, counted on Compiler Explorer. The real cost is the number of orders. n items have n! orders, and each extra item multiplies them. This program times the full do-while for 8 to 11 items.

#include <algorithm>
#include <chrono>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

int main() {
    for (int n = 8; n <= 11; n++) {
        vector<int> v(n);
        iota(v.begin(), v.end(), 1);
        auto t0 = chrono::steady_clock::now();
        long long count = 0, check = 0;
        do {
            count++;
            check += v[0];
        } while (next_permutation(v.begin(), v.end()));
        auto t1 = chrono::steady_clock::now();
        cout << n << ": " << count << " orders, " << chrono::duration<double, milli>(t1 - t0).count() << " ms (" << check << ")\n";
    }
    return 0;
}
norders, n!time for all of them
8403200.082 to 0.21 ms
93628800.75 to 2.88 ms
1036288007.4 to 16.8 ms
113991680081 to 189 ms

Eleven runs on Compiler Explorer's GCC 12.2 at -O2 -std=c++17, with a different comment each run so nothing came from a cache. The same input ran 2 to 3 times slower in some runs than in others, so read each row as a range. Each step up multiplies the time by about n, as n! says. 12 items would mean 479001600 orders, about 12 times the 11-item time. So visiting every order is fine up to about 10 or 11 items, and real work inside the loop only adds to it.

Example 1: three answers, three calls

The smallest program: a fixed list of sales, its total, its largest sale, and how many sales were exactly 45.

#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;

int main() {
    vector<int> sales{120, 45, 300, 45, 80};
    cout << "total: " << accumulate(sales.begin(), sales.end(), 0) << '\n';
    cout << "largest: " << *max_element(sales.begin(), sales.end()) << '\n';
    cout << "sales of 45: " << count(sales.begin(), sales.end(), 45) << '\n';
    return 0;
}
total: 590
largest: 300
sales of 45: 2

Three questions, three calls, and not one index in sight. The two sales of 45 were counted, wherever they sat.

Run in Compiler
Example 2: Kenji's lock

Kenji's bike lock has three dials, and he remembers the digits 3, 5 and 8, but not their order. He tries the codes in dictionary order until the lock opens. The program plays the lock too, so it knows the secret code; Kenji does not. A string is a range of characters, so sort and next_permutation work on it directly.

#include <algorithm>
#include <iostream>
#include <string>
using namespace std;

int main() {
    string secret = "853";   // the lock knows it; Kenji does not
    string code = "583";     // the three digits, in the order Kenji remembers
    sort(code.begin(), code.end());
    int tries = 0;
    do {
        tries++;
        cout << "try " << tries << ": " << code << '\n';
        if (code == secret) {
            break;
        }
    } while (next_permutation(code.begin(), code.end()));
    cout << "open after " << tries << " tries\n";
    return 0;
}
try 1: 358
try 2: 385
try 3: 538
try 4: 583
try 5: 835
try 6: 853
open after 6 tries

The sort turned 583 into 358, the smallest code, so no code was skipped. The break leaves the do-while the moment the lock opens, here on the sixth and last try. At worst three digits take 3! = 6 tries.

Run in Compiler
Example 3: the top score and who got it

Amara keeps the names in one vector and the scores in another, in the same order. max_element gives an iterator to the top score, and subtracting score.begin() turns it into an index. The same index then reads the name.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    vector<string> names{"alice", "bob", "david", "zara"};
    vector<int> score{72, 91, 85, 91};
    auto best = max_element(score.begin(), score.end());
    int at = best - score.begin();
    cout << "top score " << *best << " at index " << at << ": " << names[at] << '\n';
    return 0;
}
top score 91 at index 1: bob

Bob and Zara both scored 91, and max_element returned the first of them, Bob, at index 1. So when two elements tie for the largest, max_element points at the one nearer the front. This is the program Amara would actually write.

Run in Compiler

Where this is used

  • Python's itertools.permutations. Its documentation says the orders come out in lexicographic order, judged by the order of the input. So a sorted input gives sorted output: the same sort-first rule. Unlike next_permutation, it treats elements as different by position, so a repeated value gives repeated orders.
  • Java's java.util.Collections. Its max, min, reverse and frequency are the jobs of max_element, min_element, reverse and count. One difference: Collections.max throws NoSuchElementException on an empty collection, where max_element quietly returns end().
  • NumPy's argmax. np.argmax returns the index of the largest value in an array, as max_element minus begin() does here. Its documentation says that when the largest value appears more than once, the first index is returned, the same tie rule.

Common mistakes

1. Forgetting <numeric>.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 2, 3};
    cout << accumulate(v.begin(), v.end(), 0) << '\n';
    return 0;
}

An error at the runner's flags: error: 'accumulate' was not declared in this scope, and nothing more. GCC 12 adds no "did you forget to #include" note for accumulate, as it does for map, so the message does not name the header. Add #include <numeric>. You will forget it because accumulate feels like it belongs with sort, in <algorithm>.

2. Forgetting <algorithm>.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{3, 1, 2};
    cout << *max_element(v.begin(), v.end()) << '\n';
    reverse(v.begin(), v.end());
    return 0;
}

Two errors at the runner's flags: error: 'max_element' was not declared in this scope and error: 'reverse' was not declared in this scope, again with no note. One missing header, one error per call. You will forget it in a short program, where <iostream> and <vector> feel like enough.

3. Starting next_permutation from an unsorted order.

vector<int> friends{2, 1, 3};
int orders = 0;
do {
    orders++;
} while (next_permutation(friends.begin(), friends.end()));
cout << orders << '\n';

No message, and it prints 4, not 6: the orders 1 2 3 and 1 3 2 come before the start and are never visited. Call sort(friends.begin(), friends.end()) before the loop. You will skip the sort because the vector you typed looks like "all the friends", and the order seems not to matter.

4. Adding doubles with a start of 0.

vector<double> prices{1.5, 2.5, 3.5};
cout << accumulate(prices.begin(), prices.end(), 0) << '\n';

No message at the runner's flags, and it prints 6, not 7.5. The start value 0 is an int, so every step is cut back to a whole number. Write 0.0. You will write 0 because it is how every sum you have ever written began.

Brain teaser

Bob forgets the sort and runs the counting loop on three numbers.

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{3, 1, 2};
    int count = 0;
    do {
        count++;
    } while (next_permutation(v.begin(), v.end()));
    cout << count << '\n';
    for (int x : v) {
        cout << x << ' ';
    }
    cout << '\n';
    return 0;
}

What does count print, and what is v after the loop? Answer both before you run it.

Write the six orders of 1, 2 and 3 in dictionary order and find 3 1 2 among them. Then ask what the last call does to the range when it returns false.

Exercise 1Easy

Zara makes word cards for her little brother from a box of letter tiles. She lays the tiles of a word in every possible order and writes down each word she can make. Two orders that spell the same word count once. She wants the list in dictionary order, with its length first.

Input. One line: a word s of 1 to 8 lowercase English letters.

Output. The number k of different words, then the k words, one per line, in dictionary order.

Constraints. s has 1 to 8 letters, each from a to z.

Sample. Input bba gives 3, then abb, bab and bba. The two b tiles swapped spell the same word, so it counts once.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string s;
    cin >> s;
    vector<string> words;   // every different word, in dictionary order

    // Visit every arrangement of the letters of s with next_permutation,
    // and add each one to words.

    cout << words.size() << '\n';
    for (const string& w : words) {
        cout << w << '\n';
    }
    return 0;
}

Graded as letter-orders, a free problem in this module's problem set. The hidden tests include a single letter, eight equal letters, and eight different letters (40320 words). They also include letters typed in reverse order, and words with repeated letters.

Run in Compiler
Exercise 2Medium

Zara collects mirror words, words that read the same backwards, like level and noon. She has a list of words and wants to know how many are mirror words, and which mirror word is the longest.

Input. A line with n, then n words separated by spaces or new lines.

Output. Line 1: how many of the words are mirror words. Line 2: the longest mirror word, the first one in the list if several are equally long, or none if there is no mirror word.

Constraints. 1 <= n <= 100000. Every word is 1 to 20 lowercase English letters.

Sample. Input 6 and level noon map racecar wow tea gives 4, then racecar. The mirror words are level, noon, racecar and wow.

#include <algorithm>
#include <iostream>
#include <string>
using namespace std;

int main() {
    int n;
    cin >> n;
    int mirrors = 0;       // how many words read the same backwards
    string longest = "";   // the longest of them so far

    for (int i = 0; i < n; i++) {
        string word;
        cin >> word;
        // Make a reversed copy of word with reverse. If the two are equal,
        // count the word, and keep it if it is longer than longest.
    }

    // Line 1: mirrors. Line 2: longest, or none if no word was a mirror.

    return 0;
}

Not graded on its own. Copy the word, reverse the copy, and compare the two strings with ==. Keep a word only when it is strictly longer than the one you have, and the first of equal lengths stays.

Run in Compiler
Exercise 3Hard

Kenji has a handful of number cards. He lays all of them in a row to make one long number, and he wants the biggest number he can make. Putting the biggest card first is not enough: 3 and 30 make 330 one way and 303 the other.

Input. A line with n, then the n numbers on the cards.

Output. One line: the biggest number the cards can make in a row.

Constraints. 1 <= n <= 8. Every card holds a whole number from 1 to 999.

Sample. Input 5 and 3 30 34 5 9 gives 9534330, from the order 9, 5, 34, 3, 30.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<string> cards(n);   // each card's number, kept as text
    for (string& c : cards) {
        cin >> c;
    }
    string best = "";   // the biggest number found so far

    // Try every order of the cards. For each order, join the cards into one
    // string; keep it if it is a bigger number than best. Every joined string
    // has the same length, so comparing two of them as strings works.
    // Print best.

    return 0;
}

Not graded on its own. The idea the lesson only pointed at: next_permutation works on a vector of strings too, in their dictionary order. Eight cards give at most 40320 orders, few enough to try them all.

Run in Compiler

Common doubts

  • Why a do-while, and not a plain while?

    A plain while (next_permutation(...)) calls the function before the body runs even once. The starting order would be moved before you used it, so the smallest order would be lost. The do-while uses the order first and moves second.

  • Do these calls work on arrays and strings too?

    Yes. An algorithm only needs two iterators, and a pointer works as one, so accumulate(a, a + n, 0) adds a plain array of n ints. A string's begin() and end() work too, as Kenji's lock showed.

  • Is accumulate faster than my own loop?

    No, and not slower either. Both look at each element once, O(n), and lesson 04 times them side by side. The call wins on clarity and on the bugs it cannot have, not on speed.

  • What does max_element return for an empty vector?

    end(), because there is no element to point at. Reading *end() is undefined behaviour, so check v.empty() before you read the answer.

Key takeaways

  • An algorithm takes a range [first, last): first is used, last is not. The arithmetic ones live in <numeric>, the rest in <algorithm>.
  • accumulate, count, max_element and reverse each replace a hand loop at the same O(n), with no index to get wrong.
  • sort, then do { } while (next_permutation(...)), visits every order once, smallest first; a repeated value gives each different order once.
  • accumulate keeps its total in the type of its start value: 0LL for big sums, 0.0 for fractions.
  • n items have n! orders: all 3628800 orders of 10 items took 7.4 to 16.8 ms in eleven runs, and each extra item multiplies the time.
  • Go deeper: Under the Hood, introselect, next_permutation's four moves and the sum that wraps (Pro).

Next, lesson 02 goes through every algorithm of the weekly set, one by one, each with its cost.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Algorithm in the Weekly Set, One by One, With Its Cost