Here are a number of ideas that may help you improve your program.
Don't abuse using namespace std
Putting using namespace std
at the top of every program is a bad habit that you'd do well to avoid. Know when to use it and when not to (as when writing include headers). In this particular case, I happen to think it's perfectly appropriate because it's a single short program that and not a header. Some people seem to think it should never be used under any circumstance, but my view is that it can be used as long as it is done responsibly and with full knowledge of the consequences.
Don't #include <bits/stdc++.h>
This is not a standard header and it is poor practice to include a gcc
specific header in your program. See this question for elaboration on why you should not do that.
Avoid the use of global variables
My rewrite of this code uses no global variables, so clearly they are neither faster nor necessary. Eliminating them allows your code to be more readable and maintainable, both of which are important characteristics of well-written code. Global variables introduce messy linkages that are difficult to spot and error prone.
Use iostreams
Using both iostreams
and cstdio
functions is not really helpful. It's easy enough and usually cleaner to use only iostreams
in modern C++ programs.
Use constexpr
where appropriate
In this program, the values of first
, last
, characters
and hashval
are never altered, so we can easily enable some compiler optimizations by declaring them all constexpr
.
Use a class
C++ is an object oriented language, and it seems natural for this problem to use an object to represent an array with the given number of characters. I implemented such a thing and the class header looks like this:
#ifndef REVERSE_HASHER2_H
#define REVERSE_HASHER2_H
#include <iostream>
#include <string>
class ReverseHasher {
public:
explicit ReverseHasher(char first, char last, char len, int hashval);
ReverseHasher& operator++();
bool isMax() const { return !more; }
friend std::ostream& operator<<(std::ostream& out, const ReverseHasher& fh);
private:
bool more{true};
const char first;
const char last;
const char characters;
std::string values;
};
#endif // REVERSE_HASHER2_H
As you can see, the constructor is given all of the relevant numbers and contains a single string of values
(as per the original code) which contains the hashed string. I'll show a bit more of how this works a little later.
Use a better algorithm
The algorithm used here is much slower than it needs to be and there are several things we could do to speed things up. First, we can check that the hash is even possible by quickly calculating the minimum and maximum values for the given character range. If the desired hash is not within that range, there are zero combinations and we're done in microseconds. Second, we can easily calculate a value that satisfies the hash value.
For instance, let's say there are \$n = 10\$ characters and the values range from \$a = 0\$ to \$b = 9\$ inclusive. It's easy to understand that the minimum hash value is \$x_{\text{min}} = n * a = 10 * 0 = 0\$ and the maximum value would be \$x_\text{max} = n * b = 10 * 9 = 90\$. For any hash value \$x\$ in that range, we could simply fill \$\lfloor \frac{x}{b-a} \rfloor\$ characters with \$b\$ and fill one character with the remainder of that division (if any) and the rest with the minimum value \$a\$. We can embody all of that in a constructor for the class shown above:
ReverseHasher::ReverseHasher(char first, char last, char len, int hashval) :
first{first}, last{last}, characters{len}, values(characters, first)
{
const int minval{characters * first};
const int maxval{characters * last};
const int range{last - first};
if (hashval < minval) {
throw std::domain_error("hashvalue too low; no combinations possible\n");
}
if (hashval > maxval) {
throw std::domain_error("hashvalue too high; no combinations possible\n");
}
hashval -= minval;
std::generate(values.rbegin(), values.rend(), [&hashval,first,range](){
auto digitval = std::min(hashval, range);
hashval -= digitval;
return first + digitval;
});
}
The effect is that this code either creates a sequence with the given hashval
or if that's not possible, it throw
s an exception to indicate what's wrong.
Increment intelligently
Rather than simply incrementing values and hoping to eventually encounter a string with the same sum, we can be a lot smarter about incrementing so that we only have valid values. Conceptually this is not hard. If we have, for instance a string of three digits, 0-9 that must sum to 11, by the previous point, we can easily come up with a valid value 029
that sums to that value. It would be nice if we increment so that the values are in numeric sequence; that is, if the values come out already sorted. So in this case, the next value would be 038
. If you work through this particular example, you'll see that the next values are 047
, 056
, 065
, 074
, 083
and 092
. The pattern is that we subtract one from the lowest digit and add one to the next digit. If we always subtract and add the same amounts, it's obvious we will always maintain a valid value. The algorithm is a bit tricky, but here's how it can be implemented in C++:
ReverseHasher& ReverseHasher::operator++() {
// from the right, try +1, -1 until it works
auto it{values.rbegin()};
auto end{values.rend()};
auto movable{0};
for (auto prev{it++}; it != end; ++it) {
if (*it != last && *prev != first) {
++(*it);
--(*prev);
if (movable != 0) { //propagate to the right
auto debt{movable};
while (movable && prev != values.rbegin()) {
auto delta{std::min(*prev - first, movable)};
*prev -= delta;
movable -= delta;
--prev;
}
debt -= movable;
for (auto ptr{values.rbegin()}; debt; ++ptr) {
auto delta{std::min(last - *ptr, debt)};
*ptr += delta;
debt -= delta;
}
}
return *this;
}
movable += last - *prev;
prev = it;
}
more = false;
return *this;
}
Essentially what this does it to keep trying to do the \$+1, -1\$ on consecutive digits, sliding to the left until it's possible to do that. But there's another step needed to make sure everything comes out in order. If we consider that the next step after 092
in the previous example would be 182
if we just did the increment in that way. That's a valid value, but in fact, the next one in sorted order would instead be 128
so we need to do a little adjustment. Essentially, as we're looking for a value to increment, each value to the right of that which isn't at the maximum value (last
in the code) represents a lower value (numerically sorted). The code above keeps track of the sum of those residual values in the variable movable
and then essentially redistributes that value over the next digits after the incremented one such that the largest digits are to the right and the lowest numbers are to the left, thus insuring that the terms are created in ascending order.
Putting it all together
Here's a main
function that uses this:
main.cpp
#include <iostream>
#include <chrono>
#include "ReverseHasher2.h"
int main() {
using namespace std;
using namespace std::chrono;
auto start = high_resolution_clock::now();
ios_base::sync_with_stdio(false);
cerr.imbue(locale("")); // get pretty local formatting for numeric values
int count{0};
for (ReverseHasher fh(32, 32+5, 13, 450); !fh.isMax(); ++fh) {
++count;
std::cout << fh << '\n';
}
auto stop = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(stop - start);
cerr << "Found " << count << " solutions in " << double(duration.count()/1000.0) << " seconds.\r\n";
}
Here's the full header file:
ReverseHasher2.h
#ifndef REVERSE_HASHER2_H
#define REVERSE_HASHER2_H
#include <iostream>
#include <string>
class ReverseHasher {
public:
explicit ReverseHasher(char first, char last, char len, int hashval);
ReverseHasher& operator++();
bool isMax() const { return !more; }
friend std::ostream& operator<<(std::ostream& out, const ReverseHasher& fh);
private:
bool more{true};
const char first;
const char last;
const char characters;
std::string values;
};
#endif // REVERSE_HASHER2_H
And finally the full implementation of this, as mostly already listed above:
ReverseHasher2.cpp
#include "ReverseHasher2.h"
#include <iostream>
#include <iterator>
#include <algorithm>
#include <string>
ReverseHasher::ReverseHasher(char first, char last, char len, int hashval) :
first{first}, last{last}, characters{len}, values(characters, first)
{
const int minval{characters * first};
const int maxval{characters * last};
const int range{last - first};
if (hashval < minval) {
throw std::domain_error("hashvalue too low; no combinations possible\n");
}
if (hashval > maxval) {
throw std::domain_error("hashvalue too high; no combinations possible\n");
}
hashval -= minval;
std::generate(values.rbegin(), values.rend(), [&hashval,first,range](){
auto digitval = std::min(hashval, range);
hashval -= digitval;
return first + digitval;
});
}
ReverseHasher& ReverseHasher::operator++() {
// from the right, try +1, -1 until it works
auto it{values.rbegin()};
auto end{values.rend()};
auto movable{0};
for (auto prev{it++}; it != end; ++it) {
if (*it != last && *prev != first) {
++(*it);
--(*prev);
if (movable != 0) { //propagate to the right
auto debt{movable};
while (movable && prev != values.rbegin()) {
auto delta{std::min(*prev - first, movable)};
*prev -= delta;
movable -= delta;
--prev;
}
debt -= movable;
for (auto ptr{values.rbegin()}; debt; ++ptr) {
auto delta{std::min(last - *ptr, debt)};
*ptr += delta;
debt -= delta;
}
}
return *this;
}
movable += last - *prev;
prev = it;
}
more = false;
return *this;
}
std::ostream& operator<<(std::ostream& out, const ReverseHasher& fh) {
return out << fh.values;
}
Results
I noticed this in one of the comments:
the outcome for parameters characters = 13, last = first+5, hashval = 450 was: 9 minutes runtime
I wrote a streamlined and simplified version using the same algorithm as your original and got this result:
Found 812,525,155 solutions in 257.426 seconds.
I piped the result to /dev/null
(I'm running this on a 64-bit Linux machine) because I didn't really want to waste drive space on the resulting >10GiB file. That's over 4 minutes.
With the code posted above, I got this result:
Found 812,525,155 solutions in 31.458 seconds.
In other words, it's over eight times faster.
Going further
There are still enhancements that could be done, such as doing parallel processing, but they are not likely to buy much overall performance because the I/O is likely to be the bottleneck at this point. It's an interesting intellectual puzzle, but you probably don't really need a 10GiB file on your computer either, so it may be worth pondering in more detail what you're really trying to do, especially since this represents only a very tiny subset of the range your code was originally using. Some benchmarking and estimation of how many solutions there might be and how much space they'll require is likely to be useful in determining whether or not it's even worthwhile writing a program to do this.
uint64_t
and a bitmask to clear out the uppermost bit of every byte (since ASCII is desired). That'll be a 56-bit counter to cycle through, which takes way, way long enough either way. Length is implicit (can be told from the number). \$\endgroup\$hashval
? I mean ifhashval
is 65, then you would have both 32,33 and 33,32 as possible results. If you only need one of those, you can speed up your algorithm quite a lot. \$\endgroup\$