From 8f72965be418bc3b5721f872be669e8ba12ae67d Mon Sep 17 00:00:00 2001 From: Ashish Bhanu Daulatabad Date: Thu, 25 Mar 2021 22:00:03 +0530 Subject: [PATCH] Feat: A Star Search, Type: AI/ML (#1468) * updating DIRECTORY.md * Feat: A* Search Algorithm, Type: AI/ML * Type changes * Code reformatting. * Code reformatting-2 * Code refactoring-3. * Code refactoring-3. * Code refactoring-4. * Code refactoring-4. * Code refactoring-5. * updating DIRECTORY.md * Minor changes * clang-format and clang-tidy fixes for edf593f7 * Minor changes-2 * Enhanced AyStarSearch class documentation. * Adding Closed List for faster A* search. * Type checks * Type checks-2 * Comments * Update machine_learning/a_star_search.cpp Co-authored-by: David Leal Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Co-authored-by: David Leal --- DIRECTORY.md | 1 + machine_learning/a_star_search.cpp | 701 +++++++++++++++++++++++++++++ 2 files changed, 702 insertions(+) create mode 100644 machine_learning/a_star_search.cpp diff --git a/DIRECTORY.md b/DIRECTORY.md index c046be5c7..eded4504e 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -129,6 +129,7 @@ * [Gram Schmidt](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/linear_algebra/gram_schmidt.cpp) ## Machine Learning + * [A Star Search](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/a_star_search.cpp) * [Adaline Learning](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/adaline_learning.cpp) * [Kohonen Som Topology](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/kohonen_som_topology.cpp) * [Kohonen Som Trace](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/kohonen_som_trace.cpp) diff --git a/machine_learning/a_star_search.cpp b/machine_learning/a_star_search.cpp new file mode 100644 index 000000000..cf3c7ab42 --- /dev/null +++ b/machine_learning/a_star_search.cpp @@ -0,0 +1,701 @@ +/** + * @brief + * [A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) + * @details + * A* is an informed search algorithm, or a best-first search, meaning that it + * is formulated in terms of weighted graphs: starting from a specific starting + * node of a graph (initial state), it aims to find a path to the given goal + * node having the smallest cost (least distance travelled, shortest time, + * etc.). It evaluates by maintaining a tree of paths originating at the start + * node and extending those paths one edge at a time until it reaches the final + * state. + * The weighted edges (or cost) is evaluated on two factors, G score + * (cost required from starting node or initial state to current state) and H + * score (cost required from current state to final state). The F(state), then + * is evaluated as: + * F(state) = G(state) + H(state). + * + * To solve the given search with shortest cost or path possible is to inspect + * values having minimum F(state). + * @author [Ashish Daulatabad](https://github.com/AshishYUO) + */ +#include /// for `std::reverse` function +#include /// for `std::array`, representing `EightPuzzle` board +#include /// for `assert` +#include /// for `std::function` STL +#include /// for IO operations +#include /// for `std::map` STL +#include /// for `std::set` STL +#include /// for `std::vector` STL +/** + * @namespace machine_learning + * @brief Machine learning algorithms + */ +namespace machine_learning { +/** + * @namespace aystar_search + * @brief Functions for [A* + * Search](https://en.wikipedia.org/wiki/A*_search_algorithm) implementation. + */ +namespace aystar_search { +/** + * @class EightPuzzle + * @brief A class defining [EightPuzzle/15-Puzzle + * game](https://en.wikipedia.org/wiki/15_puzzle). + * @details + * A well known 3 x 3 puzzle of the form + * ` + * 1 2 3 + * 4 5 6 + * 7 8 0 + * ` + * where `0` represents an empty space in the puzzle + * Given any random state, the goal is to achieve the above configuration + * (or any other configuration if possible) + * @tparam N size of the square Puzzle, default is set to 3 (since it is + * EightPuzzle) + */ +template +class EightPuzzle { + std::array, N> + board; /// N x N array to store the current state of the Puzzle. + + std::vector> moves = { + {0, 1}, + {1, 0}, + {0, -1}, + {-1, + 0}}; /// A helper array to evaluate the next state from current state; + /** + * @brief Finds an empty space in puzzle (in this case; a zero) + * @returns a pair indicating integer distances from top and right + * respectively, else returns -1, -1 + */ + std::pair find_zero() { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + if (!board[i][j]) { + return {i, j}; + } + } + } + return {-1, -1}; + } + /** + * @brief check whether the index value is bounded within the puzzle area + * @param value index for the current board + * @returns `true` if index is within the board, else `false` + */ + inline bool in_range(const uint32_t value) const { + return value >= 0 && value < N; + } + + public: + /** + * @brief get the value from i units from right and j units from left side + * of the board + * @param i integer denoting ith row + * @param j integer denoting column + * @returns non-negative integer denoting the value at ith row and jth + * column + * @returns -1 if invalid i or j position + */ + uint32_t get(size_t i, size_t j) const { + if (in_range(i) && in_range(j)) { + return board[i][j]; + } + return -1; + } + /** + * @brief Returns the current state of the board + */ + std::array, N> get_state() { return board; } + + /** + * @brief returns the size of the EightPuzzle (number of row / column) + * @return N, the size of the puzzle. + */ + inline size_t get_size() const { return N; } + /** + * @brief Default constructor for EightPuzzle + */ + EightPuzzle() { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + board[i][j] = ((i * 3 + j + 1) % (N * N)); + } + } + } + /** + * @brief Parameterized Constructor for EightPuzzle + * @param init a 2-dimensional array denoting a puzzle configuration + */ + explicit EightPuzzle(const std::array, N> &init) + : board(init) {} + + /** + * @brief Copy constructor + * @param A a reference of an EightPuzzle + */ + EightPuzzle(const EightPuzzle &A) : board(A.board) {} + + /** + * @brief Move constructor + * @param A a reference of an EightPuzzle + */ + EightPuzzle(const EightPuzzle &&A) noexcept + : board(std::move(A.board)) {} + /** + * @brief Destructor of EightPuzzle + */ + ~EightPuzzle() = default; + + /** + * @brief Copy assignment operator + * @param A a reference of an EightPuzzle + */ + EightPuzzle &operator=(const EightPuzzle &A) { + board = A.board; + return *this; + } + + /** + * @brief Move assignment operator + * @param A a reference of an EightPuzzle + */ + EightPuzzle &operator=(EightPuzzle &&A) noexcept { + board = std::move(A.board); + return *this; + } + + /** + * @brief Find all possible states after processing all possible + * moves, given the current state of the puzzle + * @returns list of vector containing all possible next moves + * @note the implementation is compulsory to create A* search + */ + std::vector> generate_possible_moves() { + auto zero_pos = find_zero(); + // vector which will contain all possible state from current state + std::vector> NewStates; + for (auto &move : moves) { + if (in_range(zero_pos.first + move.first) && + in_range(zero_pos.second + move.second)) { + // swap with the possible moves + std::array, N> new_config = board; + std::swap(new_config[zero_pos.first][zero_pos.second], + new_config[zero_pos.first + move.first] + [zero_pos.second + move.second]); + EightPuzzle new_state(new_config); + // Store new state and calculate heuristic value, and depth + NewStates.emplace_back(new_state); + } + } + return NewStates; + } + /** + * @brief check whether two boards are equal + * @returns `true` if check.state is equal to `this->state`, else + * `false` + */ + bool operator==(const EightPuzzle &check) const { + if (check.get_size() != N) { + return false; + } + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + if (board[i][j] != check.board[i][j]) { + return false; + } + } + } + return true; + } + /** + * @brief check whether one board is lexicographically smaller + * @returns `true` if this->state is lexicographically smaller than + * `check.state`, else `false` + */ + bool operator<(const EightPuzzle &check) const { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + if (board[i][j] != check.board[i][j]) { + return board[i][j] < check.board[i][j]; + } + } + } + return false; + } + /** + * @brief check whether one board is lexicographically smaller or equal + * @returns `true` if this->state is lexicographically smaller than + * `check.state` or same, else `false` + */ + bool operator<=(const EightPuzzle &check) const { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + if (board[i][j] != check.board[i][j]) { + return board[i][j] < check.board[i][j]; + } + } + } + return true; + } + + /** + * @brief friend operator to display EightPuzzle<> + * @param op ostream object + * @param SomeState a certain state. + * @returns ostream operator op + */ + friend std::ostream &operator<<(std::ostream &op, + const EightPuzzle &SomeState) { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + op << SomeState.board[i][j] << " "; + } + op << "\n"; + } + return op; + } +}; +/** + * @class AyStarSearch + * @brief A class defining [A* search + * algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). for some + * initial state and final state + * @details AyStarSearch class is defined as the informed search algorithm + * that is formulated in terms of weighted graphs: starting from a specific + * starting node of a graph (initial state), it aims to find a path to the given + * goal node having the smallest cost (least distance travelled, shortest time, + * etc.) + * The weighted edges (or cost) is evaluated on two factors, G score + * (cost required from starting node or initial state to current state) and H + * score (cost required from current state to final state). The `F(state)`, then + * is evaluated as: + * `F(state) = G(state) + H(state)`. + * The best search would be the final state having minimum `F(state)` value + * @tparam Puzzle denotes the puzzle or problem involving initial state and + * final state to be solved by A* search. + * @note 1. The algorithm is referred from pesudocode from + * [Wikipedia page](https://en.wikipedia.org/wiki/A*_search_algorithm) + * as is. + * 2. For `AyStarSearch` to work, the definitions for template Puzzle is + * compulsory. + * a. Comparison operator for template Puzzle (`<`, `==`, and `<=`) + * b. `generate_possible_moves()` + */ +template +class AyStarSearch { + /** + * @brief Struct that handles all the information related to the current + * state. + */ + typedef struct Info { + Puzzle state; /// Holds the current state. + size_t heuristic_value = 0; /// stores h score + size_t depth = 0; /// stores g score + + /** + * @brief Default constructor + */ + Info() = default; + + /** + * @brief constructor having Puzzle as parameter + * @param A a puzzle object + */ + explicit Info(Puzzle A) : state(std::move(A)) {} + + /** + * @brief constructor having three parameters + * @param A a puzzle object + * @param h_value heuristic value of this puzzle object + * @param depth the depth at which this node was found during traversal + */ + Info(Puzzle A, size_t h_value, size_t d) + : state(std::move(A)), heuristic_value(h_value), depth(d) {} + + /** + * @brief Copy constructor + * @param A Info object reference + */ + Info(const Info &A) + : state(A.state), + heuristic_value(A.heuristic_value), + depth(A.depth) {} + + /** + * @brief Move constructor + * @param A Info object reference + */ + Info(const Info &&A) noexcept + : state(std::move(A.state)), + heuristic_value(std::move(A.heuristic_value)), + depth(std::move(A.depth)) {} + + /** + * @brief copy assignment operator + * @param A Info object reference + */ + Info &operator=(const Info &A) { + state = A.state; + heuristic_value = A.heuristic_value; + depth = A.depth; + return *this; + } + + /** + * @brief move assignment operator + * @param A Info object reference + */ + Info &operator=(Info &&A) noexcept { + state = std::move(A.state); + heuristic_value = std::move(A.heuristic_value); + depth = std::move(A.depth); + return *this; + } + /** + * @brief Destructor for Info + */ + ~Info() = default; + } Info; + + Info Initial; // Initial state of the AyStarSearch + Info Final; // Final state of the AyStarSearch + /** + * @brief Custom comparator for open_list + */ + struct comparison_operator { + bool operator()(const Info &a, const Info &b) const { + return a.state < b.state; + } + }; + + public: + /** + * @brief Parameterized constructor for AyStarSearch + * @param initial denoting initial state of the puzzle + * @param final denoting final state of the puzzle + */ + AyStarSearch(const Puzzle &initial, const Puzzle &final) { + Initial = Info(initial); + Final = Info(final); + } + /** + * @brief A helper solution: launches when a solution for AyStarSearch + * is found + * @param FinalState the pointer to the obtained final state + * @param parent_of the list of all parents of nodes stored during A* + * search + * @returns the list of moves denoting moves from final state to initial + * state (in reverse) + */ + std::vector Solution( + Info *FinalState, + const std::map &parent_of) { + // Useful for traversing from final state to current state. + Info *current_state = FinalState; + /* + * For storing the solution tree starting from initial state to + * final state + */ + std::vector answer; + while (current_state != nullptr) { + answer.emplace_back(current_state->state); + current_state = parent_of.find(*current_state)->second; + } + return answer; + } + /** + * Main algorithm for finding `FinalState`, given the `InitialState` + * @param dist the heuristic finction, defined by the user + * @param permissible_depth the depth at which the A* search discards + * searching for solution + * @returns List of moves from Final state to initial state, if + * evaluated, else returns an empty array + */ + std::vector a_star_search( + const std::function &dist, + const uint32_t permissible_depth = 30) { + std::map + parent_of; /// Stores the parent of the states + std::map + g_score; /// Stores the g_score + std::set + open_list; /// Stores the list to explore + std::set + closed_list; /// Stores the list that are explored + + // Before starting the AyStartSearch, initialize the set and maps + open_list.emplace(Initial); + parent_of[Initial] = nullptr; + g_score[Initial] = 0; + + while (!open_list.empty()) { + // Iterator for state having having lowest f_score. + typename std::set::iterator + it_low_f_score; + uint32_t min_f_score = 1e9; + for (auto iter = open_list.begin(); iter != open_list.end(); + ++iter) { + // f score here is evaluated by g score (depth) and h score + // (distance between current state and final state) + uint32_t f_score = iter->heuristic_value + iter->depth; + if (f_score < min_f_score) { + min_f_score = f_score; + it_low_f_score = iter; + } + } + + // current_state, stores lowest f score so far for this state. + Info *current_state = new Info(*it_low_f_score); + + // if this current state is equal to final, return + if (current_state->state == Final.state) { + return Solution(current_state, parent_of); + } + // else remove from open list as visited. + open_list.erase(it_low_f_score); + // if current_state has exceeded the allowed depth, skip + // neighbor checking + if (current_state->depth >= permissible_depth) { + continue; + } + // Generate all possible moves (neighbors) given the current + // state + std::vector total_possible_moves = + current_state->state.generate_possible_moves(); + + for (Puzzle &neighbor : total_possible_moves) { + // calculate score of neighbors with respect to + // current_state + Info Neighbor = {neighbor, dist(neighbor, Final.state), + current_state->depth + 1}; + uint32_t temp_g_score = Neighbor.depth; + + // Check whether this state is explored. + // If this state is discovered at greater depth, then discard, + // else remove from closed list and explore the node + auto closed_list_iter = closed_list.find(Neighbor); + if (closed_list_iter != closed_list.end()) { + // 1. If state in closed list has higher depth, then remove + // from list since we have found better option, + // 2. Else don't explore this state. + if (Neighbor.depth < closed_list_iter->depth) { + closed_list.erase(closed_list_iter); + } else { + continue; + } + } + auto neighbor_g_score_iter = g_score.find(Neighbor); + // if the neighbor is already created and has minimum + // g_score, then update g_score and f_score else insert new + if (neighbor_g_score_iter != g_score.end()) { + if (neighbor_g_score_iter->second > temp_g_score) { + neighbor_g_score_iter->second = temp_g_score; + parent_of[Neighbor] = current_state; + } + } else { + g_score[Neighbor] = temp_g_score; + parent_of[Neighbor] = current_state; + } + // If this is a new state, insert into open_list + // else update if the this state has better g score than + // existing one. + auto iter = open_list.find(Neighbor); + if (iter == open_list.end()) { + open_list.emplace(Neighbor); + } else if (iter->depth > Neighbor.depth) { + open_list.erase(iter); + open_list.emplace(Neighbor); + } + } + closed_list.emplace(*current_state); + } + // Cannot find the solution, return empty vector + return std::vector(0); + } +}; +} // namespace aystar_search +} // namespace machine_learning + +/** + * @brief Self test-implementations + * @returns void + */ +static void test() { + // Renaming for simplicity + using matrix3 = std::array, 3>; + using row3 = std::array; + using matrix4 = std::array, 4>; + using row4 = std::array; + // 1st test: A* search for simple EightPuzzle problem + matrix3 puzzle; + puzzle[0] = row3({0, 2, 3}); + puzzle[1] = row3({1, 5, 6}); + puzzle[2] = row3({4, 7, 8}); + + matrix3 ideal; + ideal[0] = row3({1, 2, 3}); + ideal[1] = row3({4, 5, 6}); + ideal[2] = row3({7, 8, 0}); + + /* + * Heuristic function: Manhattan distance + */ + auto manhattan_distance = + [](const machine_learning::aystar_search::EightPuzzle<> &first, + const machine_learning::aystar_search::EightPuzzle<> &second) { + uint32_t ret = 0; + for (int i = 0; i < first.get_size(); ++i) { + for (int j = 0; j < first.get_size(); ++j) { + uint32_t find = first.get(i, j); + int m = -1, n = -1; + for (int k = 0; k < second.get_size(); ++k) { + for (int l = 0; l < second.get_size(); ++l) { + if (find == second.get(k, l)) { + std::tie(m, n) = std::make_pair(k, l); + break; + } + } + if (m != -1) { + break; + } + } + if (m != -1) { + ret += abs(m - i) + abs(n - j); + } + } + } + return ret; + }; + + machine_learning::aystar_search::EightPuzzle<> Puzzle(puzzle); + machine_learning::aystar_search::EightPuzzle<> Ideal(ideal); + machine_learning::aystar_search::AyStarSearch< + machine_learning::aystar_search::EightPuzzle<3>> + search(Puzzle, Ideal); /// Search object + + std::vector answer; /// Array that validates the answer + + answer.push_back( + matrix3({row3({0, 2, 3}), row3({1, 5, 6}), row3({4, 7, 8})})); + answer.push_back( + matrix3({row3({1, 2, 3}), row3({0, 5, 6}), row3({4, 7, 8})})); + answer.push_back( + matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({0, 7, 8})})); + answer.push_back( + matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({7, 0, 8})})); + answer.push_back( + matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({7, 8, 0})})); + + auto Solution = search.a_star_search(manhattan_distance); + std::cout << Solution.size() << std::endl; + + assert(Solution.size() == answer.size()); + + uint32_t i = 0; + for (auto it = Solution.rbegin(); it != Solution.rend(); ++it) { + assert(it->get_state() == answer[i]); + ++i; + } + + // 2nd test: A* search for complicated EightPuzzle problem + // Initial state + puzzle[0] = row3({5, 7, 3}); + puzzle[1] = row3({2, 0, 6}); + puzzle[2] = row3({1, 4, 8}); + // Final state + ideal[0] = row3({1, 2, 3}); + ideal[1] = row3({4, 5, 6}); + ideal[2] = row3({7, 8, 0}); + + Puzzle = machine_learning::aystar_search::EightPuzzle<>(puzzle); + Ideal = machine_learning::aystar_search::EightPuzzle<>(ideal); + + // Initialize the search object + search = machine_learning::aystar_search::AyStarSearch< + machine_learning::aystar_search::EightPuzzle<3>>(Puzzle, Ideal); + + Solution = search.a_star_search(manhattan_distance); + std::cout << Solution.size() << std::endl; + // Static assertion due to large solution + assert(13 == Solution.size()); + // Check whether the final state is equal to expected one + assert(Solution[0].get_state() == ideal); + for (auto it = Solution.rbegin(); it != Solution.rend(); ++it) { + std::cout << *it << std::endl; + } + + // 3rd test: A* search for 15-Puzzle + // Initial State of the puzzle + matrix4 puzzle2; + puzzle2[0] = row4({5, 1, 2, 3}); + puzzle2[1] = row4({9, 6, 8, 4}); + puzzle2[2] = row4({13, 10, 7, 11}); + puzzle2[3] = row4({14, 15, 12, 0}); + // Final state of the puzzle + matrix4 ideal2; + ideal2[0] = row4({1, 2, 3, 4}); + ideal2[1] = row4({5, 6, 7, 8}); + ideal2[2] = row4({9, 10, 11, 12}); + ideal2[3] = row4({13, 14, 15, 0}); + + // Instantiate states for a*, initial state and final states + machine_learning::aystar_search::EightPuzzle<4> Puzzle2(puzzle2), + Ideal2(ideal2); + // Initialize the search object + machine_learning::aystar_search::AyStarSearch< + machine_learning::aystar_search::EightPuzzle<4>> + search2(Puzzle2, Ideal2); + /** + * Heuristic function: Manhattan distance + */ + auto manhattan_distance2 = + [](const machine_learning::aystar_search::EightPuzzle<4> &first, + const machine_learning::aystar_search::EightPuzzle<4> &second) { + uint32_t ret = 0; + for (int i = 0; i < first.get_size(); ++i) { + for (int j = 0; j < first.get_size(); ++j) { + uint32_t find = first.get(i, j); + int m = -1, n = -1; + for (int k = 0; k < second.get_size(); ++k) { + for (int l = 0; l < second.get_size(); ++l) { + if (find == second.get(k, l)) { + std::tie(m, n) = std::make_pair(k, l); + break; + } + } + if (m != -1) { + break; + } + } + if (m != -1) { + ret += abs(m - i) + abs(n - j); + } + } + } + return ret; + }; + + auto sol2 = search2.a_star_search(manhattan_distance2); + std::cout << sol2.size() << std::endl; + + // Static assertion due to large solution + assert(15 == sol2.size()); + // Check whether the final state is equal to expected one + assert(sol2[0].get_state() == ideal2); + + for (auto it = sol2.rbegin(); it != sol2.rend(); ++it) { + std::cout << *it << std::endl; + } +} +/** + * @brief Main function + * @returns 0 on exit + */ +int main() { + test(); // run self-test implementations + return 0; +}