Example of Dynamic Programming to achieve O(n).
Interview question: N houses, each with weighting on value of goods. How can a burglar maximise profit if they are not allowed to visit neighbouring houses?
Solution: optimum(i) = max( optimum(i-1)-w(i-1)+w(i) , optimum(i-2)+w(i) )
1. /**
* @mainpage
* @anchor mainpage
* @brief
* @details
* @copyright Russell John Childs, PhD, 2016
* @author Russell John Childs, PhD
* @date 2016-03-06
*
* This file contains classes: BurglarProblem
* Problem statement: A burglar wishses to maximise the value of goods stolen
* from N houses, subject to the constraint that they can only visit houses
* that are not neighbours.
*
* Solution: Dyanmic programming with complexity O(n) through the fololowing
* recurrence relation:
*
* optimum(i) = max(optimum(i-1)-w(i-1)+w(i), optimum(i-2)+w(i)), where w(i) is
* the weighting for house i and optimum(i) is the optimum solution for the 1st
* i houses.
*
* Algorithm tested against exhaustive search using binary number 1 to 2^(N-1)
* where 1-bts are houses visited, 0-bits are houses skipped and neighbours are
* illegal: E.g. 10010, but not 100110
*
* Compiled and verified under Visual Studio 13
*
* Documentation: Doxygen comments for interfaces, normal for impl.
*
* Usage: After compiling, run. The binary will send:
* n
* (1) test results to stdout
*
* The file unit_test.hpp is required for the tests and must be requested from
* author.
*
* @file dynamic_programming_burglar_problem.cpp
* @see
* @ref mainpage
*/
#include <vector>
#include <utility>
#include <random>
#include <bitset>
#include<algorithm>
#include "unit_test.hpp"
//Unit test framework is written for Linux. This #define ports it to Visual Studio 2013
#define __PRETTY_FUNCTION__ __FUNCSIG__
/**
* addtogroup BurglarProblem
* @{
*/
namespace BurglarProblem
{
class Burglar
{
/**
* This class solves the following problem:
* A burglar wishses to maximise the value of goods stolen
* from N houses, subject to the constraint that they can only visit houses
* that are not neighbours.
*
* It uses Dynamic Programming to achieve O(n) complexity using the recurrence
* relation:
*
* optimum(i) = max(optimum(i-1)-w(i-1)+w(i), optimum(i-2)+w(i)), where w(i) is
* the weighting for house i and optimum(i) is the optimum solution for the 1st
* i houses.
*
*/
private:
2. /**
* Initialiser function that makes all weights positive, sets optimum(0) and
* optimum(1), house list for optimum(0) and house list for optimum(1)
*/
void init(void)
{
//Make all weights positive
unsigned i = 0;
for (auto weight : m_weights)
{
m_weights[i] = std::abs(m_weights[i]);
++i;
}
//Store current and previous max for recurrence relation
auto min_max = std::minmax(m_weights[0], m_weights[1]);
m_max_found[0] = min_max.first;
m_max_found[1] = min_max.second;
//Store current and previous best list of houses for recurrence relation
m_houses[0].clear();
m_houses[0].push_back(m_weights[0] > m_weights[1]);
m_houses[1].clear();
m_houses[1].push_back(m_weights[1] > m_weights[0]);
}
public:
/**
* @param weights {const std::vector<double>&} - new list of weights
*/
Burglar(const std::vector<double>& weights) :
m_weights(weights),
m_which_list(false)
{
init();
}
/**
* dtor
*/
~Burglar(void)
{
}
/**
* This function resets the list of weights used.
* @param weights {const std::vector<double>&} - new list of weights
*
* @return {Burglar& } - *this
*
* Usage my_burgalr_object(new_list).solve();
*/
Burglar& reset(const std::vector<double>& weights)
{
m_weights = weights;
init();
return *this;
}
/**
* This function finds the optimum set of houses to visit, using:
*
* optimum(i) = max(optimum(i-1)-w(i-1)+w(i), optimum(i-2)+w(i)), where w(i) is
* the weighting for house i and optimum(i) is the optimum solution for the 1st
* i houses.
*/
void solve(void)
{
auto size = m_weights.size();
//Loop over houses
for (unsigned i = 2; i < size; ++i)
{
//Get last house in current optimum list
unsigned last = m_houses[1].back();
3. //Check house has value
if (m_weights[i] > 0)
{
//If is not a neighbour of current house
if (i > (last + 1))
{
//Add house value to current value
m_max_found[1] += m_weights[i];
//set prev optimum to current
m_houses[0] = m_houses[1];
//Add house to current optimum
m_houses[1].push_back(i);
}
else
{
//house is a neighbour, find best way to add it:
//(Skip, replace current house, add to prev optimum)
enum Choose{ do_nothing = 0, replace = 1, add = 2 } choose
= do_nothing;
double tmp_max = m_max_found[1];
//Get value if we replace current house in optimum(i-1)
double tmp = m_max_found[1] + (m_weights[i] - m_weights[i - 1]);
if (tmp > tmp_max)
{
tmp_max = tmp;
choose = replace;
}
//Get value if we add house to previous optimum(i-2)
tmp = m_max_found[0] + m_weights[i];
if ((tmp > tmp_max) && (i != (m_houses[0].back()+1)))
{
tmp_max = tmp;
choose = add;
}
//Set new vals for optimum(i-1), optimum(i)
m_max_found[0] = m_max_found[1];
m_max_found[1] = tmp_max;
//Replace optimum(i) house with new house
if (choose == replace)
{
m_houses[0] = m_houses[1];
m_houses[1].back() = i;
}
//Add new house to optimum(i-1)
else if (choose == add)
{
std::vector<unsigned> tmp_list = m_houses[0];
m_houses[0] = m_houses[1];
m_houses[1] = tmp_list;
m_houses[1].push_back(i);
}
}
}
}
}
/**
* This function return optimum value of goods stolen and houses to visit
*
* @return {std::pair<double,std::reference_wrapper<std::vector<unsigned>>>}
* - {optimum value, list of houses}
*/
std::pair<double, std::reference_wrapper<std::vector<unsigned>>>
get_result(void)
{
//Return optimum value and corresponding house list
return std::pair<double, std::reference_wrapper<std::vector<unsigned>>>
(m_max_found[1], m_houses[1]);
}
4. //private:
std::vector<double> m_weights;
bool m_which_list;
double m_max_found[2];
std::vector<unsigned> m_houses[2];
};
}
/**
* @}
*/
/**
* addtogroup Tests
* @{
*/
namespace Tests
{
/**
* Wrapper class for std::vector converting {a, b, c, ...} to "a b c ..."
*/
struct PrintVector
{
PrintVector(const std::vector<unsigned>& vec) :
m_vec(vec)
{
}
std::string str()
{
std::stringstream ss;
for (auto elem : m_vec)
{
ss << elem << " ";
}
return ss.str();
}
std::vector<unsigned> m_vec;
};
/**
* This function compares algorithm against exhaustive search for best
* solution using: binary number 1 to 2^(N-1)
* where 1-bts are houses visited, 0-bits are houses skipped and neighbours are
* illegal: E.g. 10010, but not 100110
*/
void tests(void)
{
using namespace UnitTest;
using namespace BurglarProblem;
//Print test banner
Verify<> banner("Burglar solver");
typedef std::vector<unsigned> uv;
//Two houses with 1st being the optimal choice
Burglar test(std::vector<double>{3, 1});
test.solve();
std::string msg_arg("std::vector<double>{3, 1}");
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 3;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 0 }).str();
//Two houses with 2nd being the optimal choice
test.reset(std::vector<double>{2, 4}).solve();
msg_arg = "std::vector<double>{2, 4})";
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 4;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 1 }).str();
//Three houses with 1st and 3rd being the optimal choice
5. test.reset(std::vector<double>{4, 9, 6}).solve();
msg_arg = "std::vector<double>{4, 9, 6}";
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 10;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 0,2 }).str();
//Three houses with 2nd being the optimal choice
test.reset(std::vector<double>{5, 13, 7});
msg_arg = "std::vector<double>{5, 13, 7}";
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 13;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 1 }).str();
//Test proof of recurrence relation. Fourth house not optimal
test.reset(std::vector<double>{5, 10, 7, 1}).solve();
msg_arg = "std::vector<double>{5, 10, 7, 1}";
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 12;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 0, 2 }).str();
//Test proof of recurrence relation. Fourth house optimal
test.reset(std::vector<double>{5, 10, 7, 3}).solve();
msg_arg = "std::vector<double>{5, 10, 7, 3}";
VERIFY(std::string("Input weights=") + msg_arg,
test.get_result().first) == 13;
VERIFY(std::string("Input weights=") +
msg_arg, PrintVector(test.get_result().second.get()).str()) ==
PrintVector(uv{ 1, 3 }).str();
unsigned run = 0;
auto exhaustive = [&]()
{
//Test by exhaustive enumeration. Enumeration subject to constraints
//took me a while to deduce. Idea is to use a binary number from
//1 to 2^N, where "1" means visit house and "0" means skip. Discard
//any binary number that violates the constraints (visits 2 neighbours)
//I am not 100% confident this is right and welcome a code review.
//(1) 20 houses with random weights (if you change this, ensure n<64)
const unsigned n = 20;
std::vector<double> random_weights;
std::random_device dev;
//std::mt19937 generator(dev());
std::mt19937 generator(run);
std::uniform_int_distribution<> distribution(1, 1000000);
for (int i = 0; i < n; ++i)
{
random_weights.push_back(distribution(generator));
}
//(2) Generate a binary number from 1 to 2^n
//and treat as house1=visit/skip, house2=visit/skip ...
double exhaustive_search_max = 0;
std::vector<unsigned> exahustive_search_house_list;
for (long long unsigned bin = 0; bin < ((1 << n) - 1); ++bin)
{
//Reverse to represent house1=0/1, house2=0/1, ...
//Happily bitset does this anyway.
std::bitset<n> houses(bin);
//Loop over bits until we find two neighbours
unsigned prev_pos = 0;
bool is_neighbour = false;
unsigned j = 0;
while ((is_neighbour == false) && j < n)
{
is_neighbour = (j != 0) && (houses[prev_pos] && houses[j]);
prev_pos = j;
++j;
}
//if we haven't found any neighbours, we have a legal permutation
if (is_neighbour == false)
6. {
//Get global maximum and houses corresponding to global max
double tmp = 0;
unsigned house_number = 0;
for (auto weight : random_weights)
{
tmp += (weight * houses[house_number]);
++house_number;
}
//If new global max found
if (tmp > exhaustive_search_max)
{
//Update global max
exhaustive_search_max = tmp;
//update house list
exahustive_search_house_list.clear();
for (unsigned index = 0; index < n; ++index)
{
if (houses[index])
{
exahustive_search_house_list.push_back(index);
}
}
}
}
}
//(3) Validate algorithm agaisnt exhaustive search
test.reset(random_weights).solve();
msg_arg = "Exhaustive search with randomw weights: ";
VERIFY(msg_arg, test.get_result().first) == exhaustive_search_max;
VERIFY(msg_arg, PrintVector(uv(test.get_result().second.get())).str()) ==
PrintVector(exahustive_search_house_list).str();
};
//Run 20 exhasutive checks
for (unsigned i = 0; i < 20; ++i)
{
run = i;
std::cout << std::endl << "Run=" << run << std::endl;
exhaustive();
}
}
}
/**
* @}
*/
int main(void)
{
using namespace Tests;
using namespace UnitTest;
//This struct pauses at the end of tests to print out results
struct BreakPointAfterMainExits
{
BreakPointAfterMainExits(void)
{
static BreakPointAfterMainExits tmp;
}
~BreakPointAfterMainExits(void)
{
unsigned set_bp_here_for_test_results = 0;
}
} dummy;
//Run tests
tests();
Verify<Results> results;
}