The Angry Professor¶
Title: Angry Professor
Link: https://www.hackerrank.com/challenges/angry-professor
In this challenge we can have several test cases with a single input. Each test case has the total number n
of students that could attend a class and the minimum number of attendants, the threshold k
, when the class is bound to start, for the professor not to cancel the class. The input has n
integer values, with negative and zero values indicating early or on-time arrival. Positive values are used for a late arrival.
And the output uses plain English, “YES” and “NO”* to let us know if the class was canceled.
Having multiple test cases has one implication: we have to consume the input of a test case before we can proceed to the next. That implication leads directly to the use an intermediate storage like std::vector<int>
and that pushes us in the direction of going once again for an iterator-based solution rather than showing a plain old approach.
#include <algorithm> // std::copy_n
#include <iostream> // std::cout/cin
#include <iterator> // std::istream/ostream_iterator, std::back_inserter
#include <vector> // std::vector
int
main(int, char *[]) {
auto in = std::istream_iterator<int>{std::cin}; // input iterator
auto out = std::ostream_iterator<std::string>{std::cout, "\n"}; // out iter
const auto fearly = [](const auto &x) { return x <= 0; };
for(auto t = *in++; t--; ++in) { // resync "in" after copy_n operation
const auto n = *in++, k = *in++; // students and threshold
auto c = std::vector<int>{}; // storage
std::copy_n(in, n, std::back_inserter(c)); // copy input
const auto students = std::count_if(c.begin(), c.end(), fearly);
*out++ = students < k ? "YES" : "NO"; // canceled if less than k
}
}
The code is easy to summarize
-
Create the input and output iterators.
-
Prepare a lambda expression to determine an early arrival, i.e..
x <= 0
. -
Read the number of test cases, to perform a loop until it is exhausted.
-
Read
n
,k
(arrival times, threshold) and then
elements into a vector. -
Use
std::count_if
with our lambda to count early arrivals -
Output if the class is canceled by comparing to the threshold
k
.
We should be happy after solving the problem in 18
lines. The code provided by HackerRank simply to manage the input is 101
lines long. Granted, 3
of those lines are the skeleton for the function the coder has to fill in. Had we used the header #include <bits/stdc++.h>
instead of the standard headers, we would have also saved 3
lines.
Iterator Attack¶
The previous solution does not take into account that the threshold, k
, could be met without having to check the arrival times of all students. std::count_if
counts from the beginning until the end, regardless of what the actual count
is.
The algorithm will of course check on every iteration if the first iterator, c.begin()
is already equal to the last, c.end()
, and proceed to increase, ++
the first if not. That is what we are going to take advantage of with our brilliant idea: an iterator, CountUntilIter
that will be and end
iterator as soon as the expected count (our threshold k
) has been reached. And we will avoid going over the entire range if it is not needed.
Our iterator takes the container for its construction and the threshold k
. To make it flexible it does also take a function that accepts and int
, our type in the problem, and returns a bool
. The container is used to get the begin
and end
iterators.
auto operator *() {
auto ret = m_fearly(*m_first);
if (not m_end and ret)
m_end = m_until == (m_count += ret);
return ret;
}
auto &operator ++() { // Prefix increment
if (not m_end)
m_end = ++m_first == m_last;
return *this;
}
auto operator ==(const CountUntilIter& o) const {
return o.m_end ? m_end : m_first == o.m_first;
}
The lines above contain the secret sauce. During dereferencing with *
, the current value is passed to the early detection function we got in the constructor. The result (true
or false
) is added to a running count and compared to the until
(the threshold) value we also got during construction, to determine if the end has been reached and pose as an iterator having met its end.
During ++
pre-increment, the iterator being moved forward is also checked against the real end of the range, to also mark itself as ended. This time for real!
And during comparison, to be done with an end
iterator, the mark we have set it is used to indicate equality to the end.
const auto students = std::count(
CountUntilIter{c, k, fearly},
CountUntilIter<decltype(c)>{},
true
);
The iterator is applied with std::count
the “if-less” variant that looks only for a specific value, true
in our case. The algorithm does not know that CountUntilIter
will perform a sudden jump to its end, but will break out early on that event.
We have also taken the chance to use an std::array
to hold the cancellation answer, simply for the sake of it. We did the same with some of the initial challenges, such as Hello, World!
.
And because “we can”, we have also done, as usual, away with using t
, the number of test cases, and keep on reading until we hit the end-of-input, being therefore capable of solving any number of cases. If needed be, of course.
Here is the entire code of this second 71
lines solution, still well under the 101
yet-to-be-solved proposal from HackerRank.
#include <array> // std::array
#include <algorithm> // std::copy_n
#include <functional> // std::function
#include <iostream> // std::cout/cin
#include <iterator> // std::istream/ostream_iterator, std::back_inserter
#include <vector> // std::vector
template <typename C>
struct CountUntilIter {
// Iterator tags
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = typename C::value_type;
using reference = value_type &;
using pointer = value_type *;
typename C::const_iterator m_first;
typename C::const_iterator m_last;
int m_until;
int m_count = 0;
using FuncEarly = std::function<bool(int)>;
FuncEarly m_fearly;
bool m_end = true;
CountUntilIter(const C &c, int until, FuncEarly fearly)
: m_first{c.cbegin()}, m_last{c.cend()}, m_until{until},
m_fearly{fearly}, m_end{m_count >= m_until or m_first == m_last} {};
CountUntilIter() {}
auto operator *() {
auto ret = m_fearly(*m_first);
if (not m_end and ret)
m_end = m_until == (m_count += ret);
return ret;
}
auto &operator ++() { // Prefix increment
if (not m_end)
m_end = ++m_first == m_last;
return *this;
}
auto operator ==(const CountUntilIter& o) const {
return o.m_end ? m_end : m_first == o.m_first;
}
auto operator !=(const CountUntilIter& o) const { return not (*this == o); }
};
// Main
int
main(int, char *[]) {
auto in = std::istream_iterator<int>{std::cin}; // input iterator
auto in_last = std::istream_iterator<int>{}; // input iterator end
auto out = std::ostream_iterator<std::string>{std::cout, "\n"}; // out iter
constexpr auto canceled = std::array{"NO", "YES"};
const auto fearly = [](const auto &x) { return x <= 0; };
[[maybe_unused]] auto t = *in++; // number of testcases (unused)
for(; in != in_last; ++in) { // resync "in" after copy_n operation
const auto n = *in++, k = *in++; // students and threshold
auto c = std::vector<int>{}; // storage
std::copy_n(in, n, std::back_inserter(c)); // copy input
const auto students = std::count(
CountUntilIter{c, k, fearly},
CountUntilIter<decltype(c)>{},
true
);
*out++ = canceled[students < k];
}
}
Rethinking The Iterator Approach¶
On second thought, we have probably overdone ourselves just a little bit with our CountUntilIter
design. The question is we, after all, do really need an iterator or we need to overcome std::count
(with and without if
) algorithm design, to be able to break out of the counting sequence if a threshold is reached.
Let us therefore design a count_n_until_k_if
algorithm to rule where all other counting algorithms fail to deliver.
template <typename I, typename F>
auto
count_n_until_k_if(I first, int n, int k, F fnk) {
auto count = 0;
for (; n-- and count != k; ++first)
count += fnk(*first);
return count;
}
Et voilá! Our algorithm takes a first
iterator, expected to be an InputIterator, takes also the maximum number of elements to check, n
and what threshold, k
to check for after the current value of the range has gone through the function fnk
. The for
look will interrupted if the running count
reaches k
.
The full code is here.
#include <array> // std::array
#include <algorithm> // std::copy_n
#include <iostream> // std::cout/cin
#include <iterator> // std::istream/ostream_iterator, std::back_inserter
#include <vector> // std::vector
template <typename I, typename F>
auto
count_n_until_k_if(I first, int n, int k, F fnk) {
auto count = 0;
for (; n-- and count != k; ++first)
count += fnk(*first);
return count;
}
// Main
int
main(int, char *[]) {
auto in = std::istream_iterator<int>{std::cin}; // input iterator
auto in_last = std::istream_iterator<int>{}; // input iterator end
auto out = std::ostream_iterator<std::string>{std::cout, "\n"}; // out iter
constexpr auto canceled = std::array{"NO", "YES"};
auto fearly = [](const auto &x) { return x <= 0; };
[[maybe_unused]] auto t = *in++; // number of testcases (unused)
for(; in != in_last; ++in) { // resync "in" after copy_n operation
const auto n = *in++, k = *in++; // students and threshold
auto c = std::vector<int>{}; // storage
std::copy_n(in, n, std::back_inserter(c)); // copy input
const auto students = count_n_until_k_if(c.begin(), n, k, fearly);
*out++ = canceled[students < k];
}
}
Storage-less Counting¶
At the beginning of this chapter we stated that we had to store the input, because we had to consume it. But again on second thought, the key is not to store, but to consume if it is needed.
And here is were C++17 comes to the rescue again with if constexpr
.
template <typename I, typename F, typename = enable_if_iter_fnk<I, F>>
auto
count_n_until_k_if(I first, int n, int k, F fnk) {
auto count = 0;
for (; n-- and count != k; ++first)
count += fnk(*first);
if constexpr (is_istream_iter_v<I>)
for(; n >= 0; --n, ++first); // consume "n" elements from istream_iter
return count;
}
You probably noticed that we were not taking an iterator pair, first
and last
and that was a forward-thinking decision to accommodate an std::istream_iterator
. With it our algorithm must no go until the end-of-range, because we would be reading past the next n
elements and consuming the input and elements of subsequent challenges.
We only have to consume n
elements. If the for
loop was interrupted early, the if constexpr
checks if the input iterator first
is of type std::istream_iterator
, adding the currently calculated iterator value_type
. If the check succeeds, as many elements as needed will be consumed, until the boundary of the next challenge, or the end-of-input, is reached.
In this case we added a complete set of SFINAE checks for the iterator and the value-check function. And we also added a CASE1
with full storage in an std::vector
to check the algorithm under both if constexpr
results.
(And yet, the code is at 70
lines, well below the HackerRank proposal).
#include <array> // std::array
#include <algorithm> // std::copy_n
#include <iostream> // std::cout/cin
#include <iterator> // std::istream/ostream_iterator, std::back_inserter
#include <vector> // std::vector
#include <type_traits> // std::void_t, std::enable_if ...
// get iterator type
template <typename I>
using it_type = typename std::iterator_traits<I>::value_type;
// check if iterator is input iterator
template <typename T, typename Tag>
constexpr bool is_it_tag_v =
std::is_base_of_v<Tag, typename std::iterator_traits<T>::iterator_category>;
template <typename I>
constexpr bool is_input_v = is_it_tag_v<I, std::input_iterator_tag>;
// check if iterator is std::istream_iterator
template <template <typename> class B, typename I>
constexpr bool is_base_of_iter_v = std::is_base_of_v<B<it_type<I>>, I>;
template <typename I>
constexpr bool is_istream_iter_v = is_base_of_iter_v<std::istream_iterator, I>;
// check function invocation and return type
template<typename I, typename F>
constexpr bool is_fnk =
std::is_same_v<bool, std::invoke_result_t<F, it_type<I>>>;
// enabling check for custom count algorithm
template<typename I, typename F>
using enable_if_iter_fnk = std::enable_if_t<is_input_v<I> and is_fnk<I, F>>;
// custom count algorithm
template <typename I, typename F, typename = enable_if_iter_fnk<I, F>>
auto
count_n_until_k_if(I first, int n, int k, F fnk) {
auto count = 0;
for (; n-- and count != k; ++first)
count += fnk(*first);
if constexpr (is_istream_iter_v<I>)
for(; n >= 0; --n, ++first); // consume "n" elements from istream_iter
return count;
}
// Main
int
main(int, char *[]) {
auto in = std::istream_iterator<int>{std::cin}; // input iterator
auto in_last = std::istream_iterator<int>{}; // input iterator end
auto out = std::ostream_iterator<std::string>{std::cout, "\n"}; // out iter
constexpr auto canceled = std::array{"NO", "YES"};
auto fearly = [](const auto &x) { return x <= 0; };
[[maybe_unused]] auto t = *in++; // number of testcases (unused)
for(; in != in_last; ++in) { // resync "in" after copy_n operation
const auto n = *in++, k = *in++; // students and threshold
#ifdef CASE1
auto c = std::vector<int>{}; // storage
std::copy_n(in, n, std::back_inserter(c)); // copy input
auto &&first = c.begin();
#else
auto first = in;
#endif
const auto students = count_n_until_k_if(first, n, k, fearly);
*out++ = canceled[students < k];
}
}