Skip to content

Variable Sized Arrays

Title: Variable Sized Arrays
Link: https://www.hackerrank.com/challenges/variable-sized-arrays/problem

Direct Solution with Vectors

We are being challenged to take a number n of arrays and then take q pairs of integers, i and j, that will be used to search first over the list of n arrays with i and then the element j in the referenced array.

Let us spare us using dynamically allocated arrays with new[] and then deallocating them with delete[] before ending the program (or having used an std::unique_ptr) and go directly for a manual solution featuring std::vector.

#include <iostream> // std::cin, std::cout
#include <vector> // std::vector

int
main(int, char *[]) {
    // problem parameters (const and from input)
    int n, q; // number of arrays, number of queries
    std::cin >> n >> q;
    // define matrix, create and gather array elements
    auto vmatrix = std::vector<std::vector<int>>{};
    for(int k; n--;) {
        std::cin >> k;
        auto &vec = *vmatrix.emplace(vmatrix.end());
        for(int ki; k--; vec.push_back(ki))
            std::cin >> ki;
    }
    // run the queries
    for(int i, j; q--;std::cout << vmatrix[i][j] << std::endl)
        std::cin >> i >> j;
    return 0;
}

As we already know we need a lot of std::cin >> varname boilerplate after having defined the variables. The only novelty here is that we use std::vector::emplace, to let the inner vectors be constructed for us.

        for(int ki; k--; vec.push_back(ki))

Since we get an iterator to where the new std::vector<int> has been positioned, we keep a reference and using std::vector::push_back is straightforward.

To get the code in as few lines as possible, we use the “dirty” trick of defining the input variables in the for loop and then performing the storage or output operations in the final statement of the loop.

Iterator-Based Solution

Converting to an iterator based solution is easy. Even better, the “real” solution takes less lines if we take into account the two extra #include files we need. We do, of course, apply the same “dirty” for trick to save some lines.

    for(; n--; in++) { // resync "in" iterator after copy action

Because the vector is being created for us, we can use the reference we keep to std::copy_n the n elements for each instance with std::back_inserter. We already used in a previous challenge in which we also worked with arrays. Let us remark the fact, that by using it we are in fact also using an iterator. It is created in the background for us and takes advantage of the std::vector::push_back method.

Prototyping A Container

Given that we have a container, std::vector, holding another container, also an std::vector, it seems like natural that we try to prototype a container to do the work.

To save us some work later during the refinement phase, let us already add some SFINAE checks. To do so we are going to resort to one of the oldest technologies in C++ (coming from the C times), the preprocessor. Yes, that macro machinery that evoke the 80s.

// Macro for trait definitions
#define DEFINE_HAS_METHOD(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T,\
    std::void_t<decltype(std::declval<T>().method())>> = true;

// Macro for trait definitions where a value_type arg is expected
#define DEFINE_HAS_METHOD_ARG(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T, \
    std::void_t<decltype(std::declval<T>().method(\
        std::declval<typename T::value_type>()))>> = true;

DEFINE_HAS_METHOD(begin)
DEFINE_HAS_METHOD(end)
DEFINE_HAS_METHOD(rbegin)
DEFINE_HAS_METHOD(rend)
DEFINE_HAS_METHOD_ARG(push_back)
DEFINE_HAS_METHOD_ARG(push_front)

Looking at that code the reason is obvious: “DRY” (Don't Repeat Yourself). With the DEFINE_HAS_METHOD and DEFINE_HAS_METHOD_ARG we avoid almost endlessly repeating the same boilerplate to look for methods in classes. Although not shown above, we also have a has_insert_v checker. This one looks for a method with two arguments and the macro would have taken exactly the same place as the only use we make of it.

All those will be combined in a two extra checks to see if the template parameters for our container prototype are supported.

// is_container_v
template<typename C>
constexpr bool is_container_v =
    (has_push_back_v<C> and has_begin_v<C> and has_end_v<C>) or
    (has_push_front_v<C> and has_rbegin_v<C> and has_rend_v<C>) or
    (has_insert_v<C> and has_begin_v<C> and has_end_v<C>);

// enable_if
template <typename ContOut, typename ContIn>
using enable_if_containers =
    std::enable_if_t<is_container_v<ContOut> and is_container_v<ContIn>>;

// solution class
template <
    template <typename> class ContOut = std::vector,
    template <typename> class ContIn = std::vector,
    typename T = int,
    typename = enable_if_containers<ContOut<ContIn<T>>, ContIn<T>>
    >
class VariableSizedArrays {
    using Inner = ContIn<T>; // inner array
    using Outer = ContOut<Inner>; // outer array

VariableSizedArrays will also use the checks in combination with if constexpr to select the appropriate methods at compile time, to support our solution. For example, a get_inserter method will choose if std::back_inserter is the one to use or std::front_inserter is the one to go for.

    template <typename C>
    auto get_inserter(C &c) const {
        // constexpr chooses the right iterator inserter for C
        if constexpr (has_push_front_v<C>)
            return std::front_inserter(c);
        else if constexpr (has_push_back_v<C>)
            return std::back_inserter(c);
        else if constexpr (has_insert_v<C>)
            return std::inserter(c, c.end());
        // Unreachable SFINAE has checked before
    }

This method works for the selection of the outer array and the inner arrays, the ones holding the values. And because we have used different methods for the insertion and that means a different positioning, we also need to use if constexpr to select the method taking us to a given position, the query, in the outer and inner arrays.

    template<typename C>
    auto get_iterator(const C &c, size_t s) const {
        // constexpr chooses the right iterator to start with
        if constexpr (has_push_front_v<C>)
            return std::next(c.rbegin(), s);
        else if constexpr (has_push_back_v<C> or has_insert_v<C>)
            return std::next(c.begin(), s);
        // Unreachable SFINAE has checked before
    }

Both methods are used to implement the read_array and query methods, the ones doing the work for the outside world, i.e., for main.

    template <typename I>
    auto read_array(I &in, size_t n) {
        // read n values of type "S" from "in" to a container Inner
        // add the container to our outer container
        auto inner = Inner{};
        std::copy_n(in, n, get_inserter(inner));
        *get_inserter(m_c)++ = inner;
    }

    auto query(size_t j, size_t i) const {
        return *get_iterator(*get_iterator(m_c, i), j);
    }

The final solution offers a CASE1 option (build and test with make 03 case1) that uses std::deque for both the inner and outer arrays, selecting std::deque::push_front and std::front_inserter for the implementation of the private methods supporting the external interface.

#include <algorithm> // std::cin, std::cout
#include <deque> // std::deque
#include <iostream> // std::cin, std::cout
#include <iterator> // std::istream, std::ostream_iterator, std::inserter
#include <vector> // std::vector
#include <type_traits> // std::void_t, std::enable_if ...

// Macro for trait definitions
#define DEFINE_HAS_METHOD(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T,\
    std::void_t<decltype(std::declval<T>().method())>> = true;

// Macro for trait definitions where a value_type arg is expected
#define DEFINE_HAS_METHOD_ARG(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T, \
    std::void_t<decltype(std::declval<T>().method(\
        std::declval<typename T::value_type>()))>> = true;

DEFINE_HAS_METHOD(begin)
DEFINE_HAS_METHOD(end)
DEFINE_HAS_METHOD(rbegin)
DEFINE_HAS_METHOD(rend)
DEFINE_HAS_METHOD_ARG(push_back)
DEFINE_HAS_METHOD_ARG(push_front)

// has_insert
template <typename, typename = void>
constexpr bool has_insert_v = false;

template <typename C>
constexpr bool has_insert_v<
    C,
    std::void_t<
        decltype(
            std::declval<C>().insert(
                std::declval<typename C::const_iterator>(),
                std::declval<typename C::value_type>()))>> = true;

// is_container_v
template<typename C>
constexpr bool is_container_v =
    (has_push_back_v<C> and has_begin_v<C> and has_end_v<C>) or
    (has_push_front_v<C> and has_rbegin_v<C> and has_rend_v<C>) or
    (has_insert_v<C> and has_begin_v<C> and has_end_v<C>);

// enable_if
template <typename ContOut, typename ContIn>
using enable_if_containers =
    std::enable_if_t<is_container_v<ContOut> and is_container_v<ContIn>>;

// solution class
template <
    template <typename> class ContOut = std::vector,
    template <typename> class ContIn = std::vector,
    typename T = int,
    typename = enable_if_containers<ContOut<ContIn<T>>, ContIn<T>>
    >
class VariableSizedArrays {
    using Inner = ContIn<T>; // inner array
    using Outer = ContOut<Inner>; // outer array

    Outer m_c; // outer container, keeps the other arrays in pace

    template<typename C>
    auto get_iterator(const C &c, size_t s) const {
        // constexpr chooses the right iterator to start with
        if constexpr (has_push_front_v<C>)
            return std::next(c.rbegin(), s);
        else if constexpr (has_push_back_v<C> or has_insert_v<C>)
            return std::next(c.begin(), s);
        // Unreachable SFINAE has checked before
    }

    template <typename C>
    auto get_inserter(C &c) const {
        // constexpr chooses the right iterator inserter for C
        if constexpr (has_push_front_v<C>)
            return std::front_inserter(c);
        else if constexpr (has_push_back_v<C>)
            return std::back_inserter(c);
        else if constexpr (has_insert_v<C>)
            return std::inserter(c, c.end());
        // Unreachable SFINAE has checked before
    }

public:
    template <typename I>
    auto read_array(I &in, size_t n) {
        // read n values of type "S" from "in" to a container Inner
        // add the container to our outer container
        auto inner = Inner{};
        std::copy_n(in, n, get_inserter(inner));
        *get_inserter(m_c)++ = inner;
    }

    auto query(size_t j, size_t i) const {
        return *get_iterator(*get_iterator(m_c, i), j);
    }
};

// main solution
int
main(int, char *[]) {
    // prepare iterators for input/output
    auto in = std::istream_iterator<int>{std::cin};
    auto out = std::ostream_iterator<int>{std::cout, "\n"};
    // problem parameters (const and from input)
    auto n = *in++, q = *in++; // number of arrays, number of queries
    // define matrix, create and gather array elements
#ifdef CASE1
    auto vsa = VariableSizedArrays<std::deque, std::deque>{};
#else
    auto vsa = VariableSizedArrays{};
#endif
    for(; n--; in++) // resync "in" iterator after copy action
        vsa.read_array(in, *in++); // *in++ = k, number of elements
    // run the queries
    while(q--)
        *out++ = vsa.query(*in++, *in++);

    return 0;
}

An (almost) real container

The container prototype we crafted above does the job, but it feels awkward. It has no begin, no end and no push_xxx methods and that means we have to resort to handmade read_array and query methods.

Let us therefore implement those container methods to have an (almost) real container. begin and end will have to return an iterator and that means we have to also code one inside the container class.

And Houston ... we have a problem. Iterators are supposed to work linearly and the distance from one point to another must be linear because it has to be integer-like. It is in the standard. Nevertheless, we can work around the “limitations” by tagging our iterator as n RandomAccessIterator.

In the case of other iterator types, moving the iterator forward or backwards is done in this fashion. Going forward, for example:

   --distance;
   ++iterator;

When distance reaches 0, the iterator is no longer ++ed. The iterator can only operated inside the increment operator without actually knowing what the value of distance is.

But for a RandomAccessIterator the movement works like this:

   random_it += distance

The iterator itself has to move using the distance. That is exactly what we are going to “abuse” by creating a 2-D distance, that will always take us from the beginning of the container to the position the query wants resolved.

Here is our Distance/Position implementation

    struct ReIterator {
        using pos_t = std::pair<int, int>;

        template <typename Pair>
        struct PosT {
            typename Pair::first_type first;
            typename Pair::second_type second;

            PosT(const Pair& pos) : first{pos.first}, second{pos.second} {};

            template <typename P>
            auto operator ==(const P &) const { return false; }
            auto operator ==(const Pair &p) const {
                return p.first == first and p.second == second;
            }
        };
        using Pos = PosT<pos_t>;

Ideally we would have used an std::pair<int, int> directly, but reality killed the cat. The library implementation used by GCC relies on the fact that the distance for iterators has to be integer-like and makes a comparison with 1 and -1. Hence the need to wrap our std:pair<int, int> to provide the proper comparison operator and avoid chaos during compilation.

That distance definition will be used in the operator += to move to the desired position. The implementation does not consider the current position: it does simply take the given position as the new point the iterator has moved to. That position will later be used when the iterator is dereferenced with the operator *.

        auto &operator +=(const Pos &pos) {
            m_pos = pos;
            return *this;
        }

        constexpr auto operator *() const {
            using InnerT = typename std::iterator_traits<I>::value_type;
            auto inner = *std::next(m_it, m_pos.first);
            // constexpr chooses the right iterator to start with
            if constexpr (has_push_front_v<InnerT>)
                return *std::next(inner.rbegin(), m_pos.second);
            else if constexpr (has_push_back_v<InnerT> or has_insert_v<InnerT>)
                return *std::next(inner.begin(), m_pos.second);
            // Unreachable SFINAE has checked before
        }

As it can be seen, the dereferencing uses a real iterator passed during construction, m_it to find first the inner container and then move into the final position within it.

Our container chooses if the iterator from which the position is calculated, will be begin or rbegin from the outer array container.

    constexpr auto get_begin_end(bool begin) const {
        // constexpr chooses the right iterator to start with
        if constexpr (has_push_front_v<Outer>)
            return begin ? ReIterator(m_c.rbegin()) : ReIterator(m_c.rend());
        else if constexpr (has_push_back_v<Outer> or has_insert_v<Outer>)
            return begin ? ReIterator(m_c.begin()) : ReIterator(m_c.end());
        // Unreachable SFINAE has checked before
    }

public:
    using value_type = T; // needed to support std::back_inserter

    auto begin() const { return get_begin_end(true); }
    auto end() const { return get_begin_end(false); }

To complete the container, we reuse the previous get_inserter to implement the push_back method.

    constexpr auto push_back(const T &t) {
        if constexpr (has_push_front_v<Outer>)
            return *get_inserter(*(--m_c.rend())) = t;
        else if constexpr (has_push_back_v<Outer> or has_insert_v<Outer>)
            return *get_inserter(*(--m_c.end())) = t;
        // Unreachable SFINAE has checked before
    }

With the container in place, we can now use the standard library tools to store the input arrays and to execute the queries. Creating an array with the standard template parameters is now a matter of directly applying std::copy_n.

#else
    auto vsa = VariableSizedArrays{};
#endif
    for(; n--; in++) // resync "in" iterator after copy action
        std::copy_n(in, *in++, std::back_inserter(++vsa)); // *in++ = k

And executing the query a matter of moving the begin iterator with a 2-D distance, i.e., a pair of integers to the required destination and dereferencing the resulting iterator.

    auto &&vsa = variable_sized_arrays(in, n); // get matrix
    while(in != in_last) // run queries until input is exhausted
        *out++ = *std::next(vsa.begin(), {{*in++, *in++}}); // *in++ => i, j

Summary

As in previous examples we have iterated from a straightforward solution to an iterator based solution and we have moved a step further by implementing a container that does the iteration for us. The container uses a 2-D distance to reach the destination desired by the query. We have even removed the limitation on the number of queries by testing for the end of input before we stop reading values.

Here is the complete code.

#include <algorithm> // std::cin, std::cout
#include <deque> // std::deque
#include <iostream> // std::cin, std::cout
#include <iterator> // std::istream, std::ostream_iterator, std::inserter
#include <vector> // std::vector
#include <type_traits> // std::void_t, std::enable_if ...

// Macro for trait definitions
#define DEFINE_HAS_METHOD(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T,\
    std::void_t<decltype(std::declval<T>().method())>> = true;

// Macro for trait definitions where a value_type arg is expected
#define DEFINE_HAS_METHOD_ARG(method) \
template<typename, typename = void> \
constexpr bool has_##method##_v = false; \
template<typename T> \
constexpr bool has_##method##_v<T, \
    std::void_t<decltype(std::declval<T>().method(\
        std::declval<typename T::value_type>()))>> = true;

DEFINE_HAS_METHOD(begin)
DEFINE_HAS_METHOD(end)
DEFINE_HAS_METHOD(rbegin)
DEFINE_HAS_METHOD(rend)
DEFINE_HAS_METHOD_ARG(push_back)
DEFINE_HAS_METHOD_ARG(push_front)

// has_insert
template <typename, typename = void>
constexpr bool has_insert_v = false;

template <typename C>
constexpr bool has_insert_v<
    C,
    std::void_t<
        decltype(
            std::declval<C>().insert(
                std::declval<typename C::const_iterator>(),
                std::declval<typename C::value_type>()))>> = true;

// is_container_v
template<typename C>
constexpr bool is_container_v =
    (has_push_back_v<C> and has_begin_v<C> and has_end_v<C>) or
    (has_push_front_v<C> and has_rbegin_v<C> and has_rend_v<C>) or
    (has_insert_v<C> and has_begin_v<C> and has_end_v<C>);

// enable_if
template <typename ContOut, typename ContIn>
using enable_if_containers =
    std::enable_if_t<is_container_v<ContOut> and is_container_v<ContIn>>;

// solution class
template <
    template <typename> class ContOut = std::vector,
    template <typename> class ContIn = std::vector,
    typename T = int,
    typename = enable_if_containers<ContOut<ContIn<T>>, ContIn<T>>
    >
class VariableSizedArrays {
    template <typename I>
    struct ReIterator {
        using pos_t = std::pair<int, int>;

        template <typename Pair>
        struct PosT {
            typename Pair::first_type first;
            typename Pair::second_type second;

            PosT(const Pair& pos) : first{pos.first}, second{pos.second} {};

            template <typename P>
            auto operator ==(const P &) const { return false; }
            auto operator ==(const Pair &p) const {
                return p.first == first and p.second == second;
            }
        };
        using Pos = PosT<pos_t>;

        // Iterator tags
        using iterator_category = std::random_access_iterator_tag;
        using difference_type   = Pos;
        using value_type        = T;
        using reference         = value_type &; // usually value_type &
        using pointer           = value_type *; // usually value_type *

        I m_it;
        Pos m_pos = pos_t{0, 0};

        ReIterator(I it) : m_it{it} {}

        auto &operator ++() { return *this; } // nop - needed by std::advance
        auto &operator --() { return *this; } // nop - needed by std::advance

        auto &operator +=(const Pos &pos) {
            m_pos = pos;
            return *this;
        }

        constexpr auto operator *() const {
            using InnerT = typename std::iterator_traits<I>::value_type;
            auto inner = *std::next(m_it, m_pos.first);
            // constexpr chooses the right iterator to start with
            if constexpr (has_push_front_v<InnerT>)
                return *std::next(inner.rbegin(), m_pos.second);
            else if constexpr (has_push_back_v<InnerT> or has_insert_v<InnerT>)
                return *std::next(inner.begin(), m_pos.second);
            // Unreachable SFINAE has checked before
        }

        auto operator ==(const ReIterator& o) const { return m_it == o.m_it; }
        auto operator !=(const ReIterator& o) const { return m_it != o.m_it; }
    };

    using Inner = ContIn<T>; // inner array
    using Outer = ContOut<Inner>; // outer array
    Outer m_c; // outer container, keeps the other arrays in pace

    template <typename C>
    auto get_inserter(C &c) const {
        // constexpr chooses the right iterator inserter for C
        if constexpr (has_push_front_v<C>)
            return std::front_inserter(c);
        else if constexpr (has_push_back_v<C>)
            return std::back_inserter(c);
        else if constexpr (has_insert_v<C>)
            return std::inserter(c, c.end());
        // Unreachable SFINAE has checked before
    }

    constexpr auto get_begin_end(bool begin) const {
        // constexpr chooses the right iterator to start with
        if constexpr (has_push_front_v<Outer>)
            return begin ? ReIterator(m_c.rbegin()) : ReIterator(m_c.rend());
        else if constexpr (has_push_back_v<Outer> or has_insert_v<Outer>)
            return begin ? ReIterator(m_c.begin()) : ReIterator(m_c.end());
        // Unreachable SFINAE has checked before
    }

public:
    using value_type = T; // needed to support std::back_inserter

    auto begin() const { return get_begin_end(true); }
    auto end() const { return get_begin_end(false); }

    auto &operator ++() {
        *get_inserter(m_c)++ = Inner{};
        return *this;
    }

    constexpr auto push_back(const T &t) {
        if constexpr (has_push_front_v<Outer>)
            return *get_inserter(*(--m_c.rend())) = t;
        else if constexpr (has_push_back_v<Outer> or has_insert_v<Outer>)
            return *get_inserter(*(--m_c.end())) = t;
        // Unreachable SFINAE has checked before
    }
};

template <typename I>
auto
variable_sized_arrays(I &in, size_t n) {
#ifdef CASE1
    auto vsa = VariableSizedArrays<std::deque, std::deque>{};
#elif defined CASE2
    auto vsa = VariableSizedArrays<std::deque, std::vector>{};
#elif defined CASE3
    auto vsa = VariableSizedArrays<std::vector, std::deque>{};
#else
    auto vsa = VariableSizedArrays{};
#endif
    for(; n--; in++) // resync "in" iterator after copy action
        std::copy_n(in, *in++, std::back_inserter(++vsa)); // *in++ = k

    return vsa;
}

int
main(int, char *[]) {
    // prepare iterators for input/output
    auto in = std::istream_iterator<int>{std::cin};
    auto in_last = std::istream_iterator<int>{};
    auto out = std::ostream_iterator<int>{std::cout, "\n"};
    // problem parameters (const and from input)
    auto n = *in++;
    [[maybe_unused]] auto q = *in++; // num arrays, num queries
    auto &&vsa = variable_sized_arrays(in, n); // get matrix
    while(in != in_last) // run queries until input is exhausted
        *out++ = *std::next(vsa.begin(), {{*in++, *in++}}); // *in++ => i, j
    return 0;
}