diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9ae4f93..02daa3c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,15 +2,31 @@ PLEASE FILL IN THE FOLLOWING! +> ok + ## Full Name -John Johnson + +Cole Fuerth ## UWindsor Email -john.johnson@uwindsor.ca + +[fuert11g@uwindsor.ca](mailto:fuert11g@uwindsor.ca) ## Application Form -- [ ] I certify I have submitted the [application form](https://forms.office.com/r/R4A1JyB3Xf) + +- [x] I certify I have submitted the [application form](https://forms.office.com/r/R4A1JyB3Xf) ## Briefly explain how your solution works and how to run it -My solution... +### Brief + +I first developed my solution in Python, then remade it in C++ for speed. **I would prefer the C++ solution to be considered, since it is considerably faster**, but it should still be noted that the Python solution came first, and runs almost as fast. The Python is also (understandably) much easier to read and understand, since the solutions are very much the same algorithmically. +### How it works + +My solution first tries to find exceptional cases that clearly will not fit first, then picks the better order of characters of the word (reversing the word will not change whether it fits the puzzle, and the order with the least starting points runs exponentially faster). Once it is known all the characters in the string appear on the board frequent enough to potentially fit, and that we have the optimal order of characters, a **depth first** approach is employed. Using sets to keep track of points, the board, and history, we can keep track of everything using set operations. `dfs` exhaustively tries all possible paths for the string, until eventually either fitting or exhausting all possibilities. + +### Running the code + +On **leetcode** (how I assume you are testing), you can copy and paste either of the solutions that are found under the **leetcode** folder directly onto the site. The C++ solution is in the top 1.5% of submissions; the Python solution is in the top 10% for runtime. + +**Locally**, I am testing using `make` with the provided Makefile, or `python3 solution.py`. The test case is hard coded into `main` on each of the source files in the root directory. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f51e39a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ +solution +submissionCode* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4c534a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ + +all: build run + +build: + g++ -o solution solution.cpp -Wall -Wextra + +run: build + ./solution + +clean: + rm -f solution \ No newline at end of file diff --git a/leetcode/solution.cpp b/leetcode/solution.cpp new file mode 100644 index 0000000..11971c8 --- /dev/null +++ b/leetcode/solution.cpp @@ -0,0 +1,103 @@ + +// include sets +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +typedef pair pairs; +typedef unsigned int uint; + +/** + * @brief depth first search to find the word. For each potential point, we recursively scheck valid next points that are not visited yet. + * @param board: A list of pair coordinates to check for the next move in s + * @param s: The string to check for + * @param board: A map of the board to check for the next move in s + * @param path: The path taken so far, do not repeat + * @param boardKeys: The keys of the board, used to find adjacent points + * @return: A boolean indicating if the string can be found on the board + */ +bool dfs(set &points, string s, map &board, set &path, set &boardKeys) +{ + if (s.length() == 0) + return true; + for (auto it : points) + { + if (board[it] == s[0]) + { + path.insert(it); + set adj; + adj.insert(pairs(it.first - 1, it.second)); + adj.insert(pairs(it.first + 1, it.second)); + adj.insert(pairs(it.first, it.second - 1)); + adj.insert(pairs(it.first, it.second + 1)); + set intersection; + set_intersection(adj.begin(), adj.end(), boardKeys.begin(), boardKeys.end(), inserter(intersection, intersection.begin())); + set new_points; + set_difference(intersection.begin(), intersection.end(), path.begin(), path.end(), inserter(new_points, new_points.begin())); + if (dfs(new_points, s.substr(1), board, path, boardKeys)) + return true; + path.erase(it); + } + } + return false; +} + +class Solution +{ +public: + bool exist(vector> &board, string word) + { + // reverse the word + string rword = word; + reverse(rword.begin(), rword.end()); + + set points; // points to check for the next move + set boardKeys; // keys of the board, for checking for valid adjacent points + map board_map; // coordinate -> char map of the board + map board_char_count; // char -> count map of the board + for (uint i = 0; i < board.size(); i++) + { + for (uint j = 0; j < board[i].size(); j++) + { + points.insert(make_pair(i, j)); + board_map[make_pair(i, j)] = board[i][j]; + boardKeys.insert(make_pair(i, j)); + if (board_char_count.find(board[i][j]) == board_char_count.end()) + board_char_count[board[i][j]] = 1; + else + board_char_count[board[i][j]]++; + } + } + set path; + + map word_char_count; + for (uint i = 0; i < word.length(); i++) + { + if (word_char_count.find(word[i]) == word_char_count.end()) + word_char_count[word[i]] = 1; + else + word_char_count[word[i]]++; + } + + // check the case where any character in the word does not appear on the board + for (uint i = 0; i < word.size(); i++) + { + if (board_char_count.find(word[i]) == board_char_count.end()) + return false; + else if (board_char_count[word[i]] < word_char_count[word[i]]) + return false; + } + + // use word if the first character has less occurences than the reverse of the word + if (board_char_count[word[0]] > board_char_count[rword[0]]) + word = rword; + + return dfs(points, word, board_map, path, boardKeys); + } +}; \ No newline at end of file diff --git a/leetcode/solution.py b/leetcode/solution.py new file mode 100644 index 0000000..44b6ab7 --- /dev/null +++ b/leetcode/solution.py @@ -0,0 +1,45 @@ + +from itertools import product + +def search(points: set, s: str, board: dict, path: set = None) -> bool: + # points is a list of tuples (x, y) for points to try to connect to the first character of s + # s is the string to connect, check the list of points to see if they can be connected to form s + # board is the board of characters + # path is a list of points that have been used to connect to s so far + # return True if s can be connected, False otherwise + if path is None: + path = set() + if len(s) == 0: + return True + for p in points: + if s[0] == board[p]: + path.add(p) + # list of points left, right, up, down from p that are in the board keyset + adj = set([(p[0] + 1, p[1]), (p[0] - 1, p[1]), (p[0], p[1] + 1), + (p[0], p[1] - 1)]).intersection(board.keys()).difference(path) + if search(adj, s[1:], board, path): + return True + path.remove(p) + return False + +class Solution: + def exist(self, board: List[List[str]], word: str) -> bool: + # convert board to a dictionary of (x, y) -> character + board = {(x, y): board[x][y] for x, y in product( + range(len(board)), range(len(board[0])))} + + # if any char in the word is not in the board, return False + if not set(word).issubset(set(board.values())): + return False + + # if there are less occurences of the last character than the first character, reverse the word + boardstr = ''.join(board.values()) + if boardstr.count(word[0]) > boardstr.count(word[-1]): + word = word[::-1] + + # you take the input, and see if for each character, you can follow a path from character to character. The same character cannot be used more than once + return search( + set(board.keys()), + word, + board + ) \ No newline at end of file diff --git a/solution.cpp b/solution.cpp new file mode 100644 index 0000000..51961ac --- /dev/null +++ b/solution.cpp @@ -0,0 +1,138 @@ + +// include sets +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +typedef pair pairs; +typedef unsigned int uint; + +/** + * @brief depth first search to find the word. For each potential point, we recursively scheck valid next points that are not visited yet. + * @param board: A list of pair coordinates to check for the next move in s + * @param s: The string to check for + * @param board: A map of the board to check for the next move in s + * @param path: The path taken so far, do not repeat + * @param boardKeys: The keys of the board, used to find adjacent points + * @return: A boolean indicating if the string can be found on the board + */ +bool dfs(set &points, string s, map &board, set &path, set &boardKeys) +{ + if (s.length() == 0) + return true; + for (auto it : points) + { + if (board[it] == s[0]) + { + path.insert(it); + set adj; + adj.insert(pairs(it.first - 1, it.second)); + adj.insert(pairs(it.first + 1, it.second)); + adj.insert(pairs(it.first, it.second - 1)); + adj.insert(pairs(it.first, it.second + 1)); + set intersection; + set_intersection(adj.begin(), adj.end(), boardKeys.begin(), boardKeys.end(), inserter(intersection, intersection.begin())); + set new_points; + set_difference(intersection.begin(), intersection.end(), path.begin(), path.end(), inserter(new_points, new_points.begin())); + if (dfs(new_points, s.substr(1), board, path, boardKeys)) + return true; + path.erase(it); + } + } + return false; +} + +bool exist(vector> &board, string word) +{ + // reverse the word + string rword = word; + reverse(rword.begin(), rword.end()); + + set points; // points to check for the next move + set boardKeys; // keys of the board, for checking for valid adjacent points + map board_map; // coordinate -> char map of the board + map board_char_count; // char -> count map of the board + for (uint i = 0; i < board.size(); i++) + { + for (uint j = 0; j < board[i].size(); j++) + { + points.insert(make_pair(i, j)); + board_map[make_pair(i, j)] = board[i][j]; + boardKeys.insert(make_pair(i, j)); + if (board_char_count.find(board[i][j]) == board_char_count.end()) + board_char_count[board[i][j]] = 1; + else + board_char_count[board[i][j]]++; + } + } + set path; + + map word_char_count; + for (uint i = 0; i < word.length(); i++) + { + if (word_char_count.find(word[i]) == word_char_count.end()) + word_char_count[word[i]] = 1; + else + word_char_count[word[i]]++; + } + + // check the case where any character in the word does not appear on the board + for (uint i = 0; i < word.size(); i++) + { + if (board_char_count.find(word[i]) == board_char_count.end()) + return false; + else if (board_char_count[word[i]] < word_char_count[word[i]]) + return false; + } + + // use word if the first character has less occurences than the reverse of the word + if (board_char_count[word[0]] > board_char_count[rword[0]]) + word = rword; + + return dfs(points, word, board_map, path, boardKeys); +} + +int main(void) +{ + // assume input comes from stdin + // board input is single characters, space separated for columns and newlines for rows + // word is on its own line + // example input: + // ``` + // A B C E + // S F C S + // A D E E + // word + // ``` + + // read board and word + vector> board = {{'A', 'A', 'A', 'A', 'A', 'A'}, + {'A', 'A', 'A', 'A', 'A', 'A'}, + {'A', 'A', 'A', 'A', 'A', 'A'}, + {'A', 'A', 'A', 'A', 'A', 'A'}, + {'A', 'A', 'A', 'A', 'A', 'B'}, + {'A', 'A', 'A', 'A', 'B', 'A'}}; + + string word = "AAAAAAAAAAAAABB"; + + // print the board and word + cout << "Board:" << endl; + for (uint i = 0; i < board.size(); i++) + { + for (uint j = 0; j < board[i].size(); j++) + { + cout << board[i][j]; + } + cout << endl; + } + cout << "Word: " << word << endl; + + // print result + cout << exist(board, word) << endl; +} diff --git a/solution.py b/solution.py new file mode 100644 index 0000000..ef82966 --- /dev/null +++ b/solution.py @@ -0,0 +1,66 @@ +# /usr/bin/python3 + +from itertools import product + + +def search(points: set, s: str, board: dict, path: set = None) -> bool: + # points is a list of tuples (x, y) for points to try to connect to the first character of s + # s is the string to connect, check the list of points to see if they can be connected to form s + # board is the board of characters + # path is a list of points that have been used to connect to s so far + # return True if s can be connected, False otherwise + if path is None: + path = set() + if len(s) == 0: + return True + for p in points: + if s[0] == board[p]: + path.add(p) + # list of points left, right, up, down from p that are in the board keyset + adj = set([(p[0] + 1, p[1]), (p[0] - 1, p[1]), (p[0], p[1] + 1), + (p[0], p[1] - 1)]).intersection(board.keys()).difference(path) + if search(adj, s[1:], board, path): + return True + path.remove(p) + return False + + +def exist(board: list[list[str]], word: str) -> bool: + # convert board to a dictionary of (x, y) -> character + board = {(x, y): board[x][y] for x, y in product( + range(len(board)), range(len(board[0])))} + + # if any char in the word is not in the board, return False + if not set(word).issubset(set(board.values())): + return False + + # if there are less occurences of the last character than the first character, reverse the word + boardstr = ''.join(board.values()) + if boardstr.count(word[0]) > boardstr.count(word[-1]): + word = word[::-1] + + return search( + set(board.keys()), + word, + board + ) + + +if __name__ == '__main__': + + # assume input comes from stdin + # board input is single characters, space separated for columns and newlines for rows + # word is on its own line + # example input: + # ``` + # A B C E + # S F C S + # A D E E + # word + # ``` + + board = [["A", "A", "A", "A", "A", "A"], ["A", "A", "A", "A", "A", "A"], ["A", "A", "A", "A", "A", "A"], [ + "A", "A", "A", "A", "A", "A"], ["A", "A", "A", "A", "A", "B"], ["A", "A", "A", "A", "B", "A"]] + word = "AAAAAAAAAAAAABB" + + print(exist(board, word))