diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000000..f7e6eb212bc8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +sort = Cover +omit = + .env/* diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 514c9327e231..000000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: TheAlgorithms -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: ['http://paypal.me/TheAlgorithms/1000', 'https://donorbox.org/thealgorithms'] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000000..103ecf7c288a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +### **Describe your change:** + + + +* [ ] Add an algorithm? +* [ ] Fix a bug or typo in an existing algorithm? +* [ ] Documentation change? + +### **Checklist:** +* [ ] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md). +* [ ] This pull request is all my own work -- I have not plagiarized. +* [ ] I know that pull requests will not be merged if they fail the automated tests. +* [ ] This PR only changes one algorithm file. To ease review, please open separate PRs for separate algorithms. +* [ ] All new Python files are placed inside an existing directory. +* [ ] All filenames are in all lowercase characters with no spaces or dashes. +* [ ] All functions and variable names follow Python naming conventions. +* [ ] All function parameters and return values are annotated with Python [type hints](https://docs.python.org/3/library/typing.html). +* [ ] All functions have [doctests](https://docs.python.org/3/library/doctest.html) that pass the automated testing. +* [ ] All new algorithms have a URL in its comments that points to Wikipedia or other similar explanation. +* [ ] If this pull request resolves one or more open issues then the commit message contains `Fixes: #{$ISSUE_NO}`. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000000..fe51e49e4707 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - bug + - help wanted + - OK to merge +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + Please reopen this issue once you commit the changes requested or + make improvements on the code. Thank you for your contributions. diff --git a/.github/workflows/autoblack.yml b/.github/workflows/autoblack.yml new file mode 100644 index 000000000000..99243f3ec7fc --- /dev/null +++ b/.github/workflows/autoblack.yml @@ -0,0 +1,24 @@ +# GitHub Action that uses Black to reformat Python code (if needed) when doing a git push. +# If all Python code in the repo is compliant with Black then this Action does nothing. +# Otherwise, Black is run and its changes are committed to the repo. +# https://github.com/cclauss/autoblack + +name: autoblack_push +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 # Use v1, NOT v2 + - uses: actions/setup-python@v1 + - run: pip install black + - run: black --check . + - name: If needed, commit black changes to a new pull request + if: failure() + run: | + black . + git config --global user.name github-actions + git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + git commit -am "fixup! Format Python code with psf/black push" + git push --force origin HEAD:$GITHUB_REF diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 000000000000..188538775a2f --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,14 @@ +# GitHub Action to automate the identification of common misspellings in text files +# https://github.com/codespell-project/codespell +name: codespell +on: [push, pull_request] +jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + - run: pip install codespell flake8 + - run: | + SKIP="./.*,./other/dictionary.txt,./other/words,./project_euler/problem_22/p022_names.txt" + codespell -L ans,fo,hist,iff,secant,tim --skip=$SKIP --quiet-level=2 diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml new file mode 100644 index 000000000000..77b0e1a262e1 --- /dev/null +++ b/.github/workflows/directory_writer.yml @@ -0,0 +1,23 @@ +# The objective of this GitHub Action is to update the DIRECTORY.md file (if needed) +# when doing a git push +name: directory_writer +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 # v1, NOT v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - name: Write DIRECTORY.md + run: | + scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md + git config --global user.name github-actions + git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + - name: Update DIRECTORY.md + run: | + git add DIRECTORY.md + git commit -am "updating DIRECTORY.md" || true + git push --force origin HEAD:$GITHUB_REF || true \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000000..4793f54f7af8 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,23 @@ +name: Mark stale issues and pull requests +on: + schedule: + - cron: "0 0 * * *" +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: > + Please reopen this issue once you add more information and updates here. + If this is not the case and you need some help, feel free to seek help + from our [Gitter](https://gitter.im/TheAlgorithms) or ping one of the + reviewers. Thank you for your contributions! + stale-pr-message: > + Please reopen this pull request once you commit the changes requested + or make improvements on the code. If this is not the case and you need + some help, feel free to seek help from our [Gitter](https://gitter.im/TheAlgorithms) + or ping one of the reviewers. Thank you for your contributions! + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.gitignore b/.gitignore index 0c3f33058614..574cdf312836 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,7 @@ __pycache__/ *.so # Distribution / packaging -.vscode/ .Python -env/ build/ develop-eggs/ dist/ @@ -21,12 +19,14 @@ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller -# Usually these files are written by a python script from a template +# Usually these files are written by a Python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -43,8 +43,9 @@ htmlcov/ .cache nosetests.xml coverage.xml -*,cover +*.cover .hypothesis/ +.pytest_cache/ # Translations *.mo @@ -53,6 +54,7 @@ coverage.xml # Django stuff: *.log local_settings.py +db.sqlite3 # Flask stuff: instance/ @@ -67,7 +69,7 @@ docs/_build/ # PyBuilder target/ -# IPython Notebook +# Jupyter Notebook .ipynb_checkpoints # pyenv @@ -76,18 +78,32 @@ target/ # celery beat schedule file celerybeat-schedule -# dotenv -.env +# SageMath parsed files +*.sage.py -# virtualenv +# Environments +.env +.venv +env/ venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject +.spyproject # Rope project settings .ropeproject -.idea + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + .DS_Store -.try \ No newline at end of file +.idea +.try +.vscode/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000000..5975b8b8e983 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,2 @@ +tasks: + - init: pip3 install -r ./requirements.txt diff --git a/.travis.yml b/.travis.yml index bea512264c19..10b91b2561ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,17 @@ +os: linux +dist: focal language: python -dist: xenial # required for Python >= 3.7 -python: 3.7 +python: 3.8 cache: pip -before_install: pip install --upgrade pip setuptools +before_install: pip install --upgrade pip setuptools six install: pip install -r requirements.txt before_script: - black --check . || true - - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - flake8 . --count --select=E101,E501,E722,E9,F4,F63,F7,F82,W191 --max-line-length=127 --show-source --statistics + - flake8 . --count --exit-zero --max-line-length=127 --statistics script: + - scripts/validate_filenames.py # no uppercase, no spaces, in a directory - mypy --ignore-missing-imports . - - pytest . --doctest-modules - --ignore=data_structures/stacks/balanced_parentheses.py - --ignore=data_structures/stacks/infix_to_postfix_conversion.py - --ignore=file_transfer_protocol/ftp_send_receive.py - --ignore=file_transfer_protocol/ftp_client_server.py - --ignore=machine_learning/linear_regression.py - --ignore=machine_learning/perceptron.py - --ignore=machine_learning/random_forest_classification/random_forest_classification.py - --ignore=machine_learning/random_forest_regression/random_forest_regression.py + - pytest --doctest-modules --cov-report=term-missing:skip-covered --cov=. . after_success: - - python scripts/build_directory_md.py - - cat DIRECTORY.md + - scripts/build_directory_md.py 2>&1 | tee DIRECTORY.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3202b817f1c5..dd4295404f78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,26 +23,63 @@ We are very happy that you consider implementing algorithms and data structure f We appreciate any contribution, from fixing a grammar mistake in a comment to implementing complex algorithms. Please read this section if you are contributing your work. +Your contribution will be tested by our [automated testing on Travis CI](https://travis-ci.org/TheAlgorithms/Python/pull_requests) to save time and mental energy. After you have submitted your pull request, you should see the Travis tests start to run at the bottom of your submission page. If those tests fail, then click on the ___details___ button try to read through the Travis output to understand the failure. If you do not understand, please leave a comment on your submission page and a community member will try to help. + +Please help us keep our issue list small by adding fixes: #{$ISSUE_NO} to the commit message of pull requests that resolve open issues. GitHub will use this tag to auto close the issue when the PR is merged. + +#### What is an Algorithm? + +An Algorithm is one or more functions (or classes) that: +* take one or more inputs, +* perform some internal calculations or data manipulations, +* return one or more outputs, +* have minimal side effects (Ex. print(), plot(), read(), write()). + +Algorithms should be packaged in a way that would make it easy for readers to put them into larger programs. + +Algorithms should: +* have intuitive class and function names that make their purpose clear to readers +* use Python naming conventions and intuitive variable names to ease comprehension +* be flexible to take different input values +* have Python type hints for their input parameters and return values +* raise Python exceptions (ValueError, etc.) on erroneous input values +* have docstrings with clear explanations and/or URLs to source materials +* contain doctests that test both valid and erroneous input values +* return all calculation results instead of printing or plotting them + +Algorithms in this repo should not be how-to examples for existing Python packages. Instead, they should perform internal calculations or manipulations to convert input values into different output values. Those calculations or manipulations can use data types, classes, or functions of existing Python packages but each algorithm in this repo should add unique value. + #### Coding Style We want your work to be readable by others; therefore, we encourage you to note the following: -- Please write in Python 3.x. -- Please consider running [__python/black__](https://github.com/python/black) on your Python file(s) before submitting your pull request. This is not a requirement but it does make your code more readable. There are other code formatters (autopep8, yapf) but the __black__ style is now the recommendation of the Python core team. To use it, - ```bash - pip3 install black # only required the first time - black my-submission.py - ``` +- Please write in Python 3.7+. __print()__ is a function in Python 3 so __print "Hello"__ will _not_ work but __print("Hello")__ will. +- Please focus hard on naming of functions, classes, and variables. Help your reader by using __descriptive names__ that can help you to remove redundant comments. + - Single letter variable names are _old school_ so please avoid them unless their life only spans a few lines. + - Expand acronyms because __gcd()__ is hard to understand but __greatest_common_divisor()__ is not. + - Please follow the [Python Naming Conventions](https://pep8.org/#prescriptive-naming-conventions) so variable_names and function_names should be lower_case, CONSTANTS in UPPERCASE, ClassNames should be CamelCase, etc. + +- We encourage the use of Python [f-strings](https://realpython.com/python-f-strings/#f-strings-a-new-and-improved-way-to-format-strings-in-python) where the make the code easier to read. + +- Please consider running [__psf/black__](https://github.com/python/black) on your Python file(s) before submitting your pull request. This is not yet a requirement but it does make your code more readable and automatically aligns it with much of [PEP 8](https://www.python.org/dev/peps/pep-0008/). There are other code formatters (autopep8, yapf) but the __black__ formatter is now hosted by the Python Software Foundation. To use it, + + ```bash + pip3 install black # only required the first time + black . + ``` - All submissions will need to pass the test __flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics__ before they will be accepted so if possible, try this test locally on your Python file(s) before submitting your pull request. -- If you know [PEP 8](https://www.python.org/dev/peps/pep-0008/) already, you will have no problem in coding style, though we do not follow it strictly. Read the remaining section and have fun coding! + ```bash + pip3 install flake8 # only required the first time + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + ``` -- Always use 4 spaces to indent. +- Original code submission require docstrings or comments to describe your work. -- Original code submission requires comments to describe your work. +- More on docstrings and comments: -- More on comments and docstrings: + If you used a Wikipedia article or some other source material to create your algorithm, please add the URL in a docstring or comment to help your reader. The following are considered to be bad and may be requested to be improved: @@ -52,88 +89,79 @@ We want your work to be readable by others; therefore, we encourage you to note This is too trivial. Comments are expected to be explanatory. For comments, you can write them above, on or below a line of code, as long as you are consistent within the same piece of code. - *Sometimes, docstrings are avoided.* This will happen if you are using some editors and not careful with indentation: + We encourage you to put docstrings inside your functions but please pay attention to indentation of docstrings. The following is a good example: ```python + def sum_ab(a, b): + """ + Return the sum of two integers a and b. """ - This function sums a and b - """ - def sum(a, b): return a + b ``` - However, if you insist to use docstrings, we encourage you to put docstrings inside functions. Also, please pay attention to indentation to docstrings. The following is acceptable in this case: +- Write tests (especially [__doctests__](https://docs.python.org/3/library/doctest.html)) to illustrate and verify your work. We highly encourage the use of _doctests on all functions_. ```python - def sumab(a, b): + def sum_ab(a, b): """ - This function sums two integers a and b - Return: a + b + Return the sum of two integers a and b + >>> sum_ab(2, 2) + 4 + >>> sum_ab(-2, 3) + 1 + >>> sum_ab(4.9, 5.1) + 10.0 """ return a + b ``` -- `lambda`, `map`, `filter`, `reduce` and complicated list comprehension are welcome and acceptable to demonstrate the power of Python, as long as they are simple enough to read. - - - This is arguable: **write comments** and assign appropriate variable names, so that the code is easy to read! + These doctests will be run by pytest as part of our automated testing so please try to run your doctests locally and make sure that they are found and pass: -- Write tests to illustrate your work. + ```bash + python3 -m doctest -v my_submission.py + ``` - The following "testing" approaches are **not** encouraged: + The use of the Python builtin __input()__ function is **not** encouraged: ```python - input('Enter your input:') + input('Enter your input:') # Or even worse... - input = eval(raw_input("Enter your input: ")) + input = eval(input("Enter your input: ")) ``` - - However, if your code uses __input()__ then we encourage you to gracefully deal with leading and trailing whitespace in user input by adding __.strip()__ to the end as in: - + + However, if your code uses __input()__ then we encourage you to gracefully deal with leading and trailing whitespace in user input by adding __.strip()__ as in: + ```python starting_value = int(input("Please enter a starting value: ").strip()) ``` - Please write down your test case, like the following: + The use of [Python type hints](https://docs.python.org/3/library/typing.html) is encouraged for function parameters and return values. Our automated testing will run [mypy](http://mypy-lang.org) so run that locally before making your submission. ```python - def sumab(a, b): + def sum_ab(a: int, b: int) -> int: return a + b - # Write tests this way: - print(sumab(1,2)) # 1+2 = 3 - print(sumab(6,4)) # 6+4 = 10 - # Or this way: - print("1 + 2 = ", sumab(1,2)) # 1+2 = 3 - print("6 + 4 = ", sumab(6,4)) # 6+4 = 10 ``` - - Better yet, if you know how to write [__doctests__](https://docs.python.org/3/library/doctest.html), please consider adding them. + +- [__List comprehensions and generators__](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) are preferred over the use of `lambda`, `map`, `filter`, `reduce` but the important thing is to demonstrate the power of Python in code that is easy to read and maintain. - Avoid importing external libraries for basic algorithms. Only use those libraries for complicated algorithms. +- If you need a third party module that is not in the file __requirements.txt__, please add it to that file as part of your submission. #### Other Standard While Submitting Your Work -- File extension for code should be `.py`. Jupiter notebook files are acceptable in machine learning algorithms. - -- Strictly use snake case (underscore separated) in your file name, as it will be easy to parse in future using scripts. - - If possible, follow the standard *within* the folder you are submitting to. - +- File extension for code should be `.py`. Jupyter notebook files are acceptable in machine learning algorithms. +- Strictly use snake_case (underscore_separated) in your file_name, as it will be easy to parse in future using scripts. +- Please avoid creating new directories if at all possible. Try to fit your work into the existing directory structure. +- If possible, follow the standard *within* the folder you are submitting to. - If you have modified/added code work, make sure the code compiles before submitting. - - If you have modified/added documentation work, ensure your language is concise and contains no grammar errors. - - Do not update the README.md or DIRECTORY.md file which will be periodically autogenerated by our Travis CI processes. - - Add a corresponding explanation to [Algorithms-Explanation](https://github.com/TheAlgorithms/Algorithms-Explanation) (Optional but recommended). - - All submissions will be tested with [__mypy__](http://www.mypy-lang.org) so we encourage to add [__Python type hints__](https://docs.python.org/3/library/typing.html) where it makes sense to do so. - Most importantly, - - **Be consistent in the use of these guidelines when submitting.** - **Join** [Gitter](https://gitter.im/TheAlgorithms) **now!** - Happy coding! - - Writer [@poyea](https://github.com/poyea), Jun 2019. diff --git a/DIRECTORY.md b/DIRECTORY.md index fc06a8cd1548..61783b0e5bcf 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1,392 +1,628 @@ + ## Arithmetic Analysis - * [bisection](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/bisection.py) - * [intersection](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/intersection.py) - * [lu decomposition](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/lu_decomposition.py) - * [newton method](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/newton_method.py) - * [newton raphson method](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/newton_raphson_method.py) + * [Bisection](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/bisection.py) + * [Gaussian Elimination](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/gaussian_elimination.py) + * [In Static Equilibrium](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/in_static_equilibrium.py) + * [Intersection](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/intersection.py) + * [Lu Decomposition](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/lu_decomposition.py) + * [Newton Forward Interpolation](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/newton_forward_interpolation.py) + * [Newton Method](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/newton_method.py) + * [Newton Raphson](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/newton_raphson.py) + * [Secant Method](https://github.com/TheAlgorithms/Python/blob/master/arithmetic_analysis/secant_method.py) + ## Backtracking - * [all combinations](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_combinations.py) - * [all permutations](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_permutations.py) - * [all subsequences](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_subsequences.py) - * [minimax](https://github.com/TheAlgorithms/Python/blob/master/backtracking/minimax.py) - * [n queens](https://github.com/TheAlgorithms/Python/blob/master/backtracking/n_queens.py) - * [sum of subsets](https://github.com/TheAlgorithms/Python/blob/master/backtracking/sum_of_subsets.py) + * [All Combinations](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_combinations.py) + * [All Permutations](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_permutations.py) + * [All Subsequences](https://github.com/TheAlgorithms/Python/blob/master/backtracking/all_subsequences.py) + * [Coloring](https://github.com/TheAlgorithms/Python/blob/master/backtracking/coloring.py) + * [Hamiltonian Cycle](https://github.com/TheAlgorithms/Python/blob/master/backtracking/hamiltonian_cycle.py) + * [Minimax](https://github.com/TheAlgorithms/Python/blob/master/backtracking/minimax.py) + * [N Queens](https://github.com/TheAlgorithms/Python/blob/master/backtracking/n_queens.py) + * [Sudoku](https://github.com/TheAlgorithms/Python/blob/master/backtracking/sudoku.py) + * [Sum Of Subsets](https://github.com/TheAlgorithms/Python/blob/master/backtracking/sum_of_subsets.py) + +## Blockchain + * [Chinese Remainder Theorem](https://github.com/TheAlgorithms/Python/blob/master/blockchain/chinese_remainder_theorem.py) + * [Diophantine Equation](https://github.com/TheAlgorithms/Python/blob/master/blockchain/diophantine_equation.py) + * [Modular Division](https://github.com/TheAlgorithms/Python/blob/master/blockchain/modular_division.py) + ## Boolean Algebra - * [quine mc cluskey](https://github.com/TheAlgorithms/Python/blob/master/boolean_algebra/quine_mc_cluskey.py) + * [Quine Mc Cluskey](https://github.com/TheAlgorithms/Python/blob/master/boolean_algebra/quine_mc_cluskey.py) + +## Cellular Automata + * [One Dimensional](https://github.com/TheAlgorithms/Python/blob/master/cellular_automata/one_dimensional.py) + ## Ciphers - * [affine cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/affine_cipher.py) - * [atbash](https://github.com/TheAlgorithms/Python/blob/master/ciphers/atbash.py) - * [base16](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base16.py) - * [base32](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base32.py) - * [base64 cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base64_cipher.py) - * [base85](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base85.py) - * [brute force caesar cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/brute_force_caesar_cipher.py) - * [caesar cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/caesar_cipher.py) - * [cryptomath module](https://github.com/TheAlgorithms/Python/blob/master/ciphers/cryptomath_module.py) - * [elgamal key generator](https://github.com/TheAlgorithms/Python/blob/master/ciphers/elgamal_key_generator.py) - * [hill cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/hill_cipher.py) - * [morse code implementation](https://github.com/TheAlgorithms/Python/blob/master/ciphers/morse_code_implementation.py) - * [onepad cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/onepad_cipher.py) - * [playfair cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/playfair_cipher.py) - * [rabin miller](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rabin_miller.py) - * [rot13](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rot13.py) - * [rsa cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rsa_cipher.py) - * [rsa key generator](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rsa_key_generator.py) - * [simple substitution cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/simple_substitution_cipher.py) - * [trafid cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/trafid_cipher.py) - * [transposition cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/transposition_cipher.py) - * [transposition cipher encrypt decrypt file](https://github.com/TheAlgorithms/Python/blob/master/ciphers/transposition_cipher_encrypt_decrypt_file.py) - * [vigenere cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/vigenere_cipher.py) - * [xor cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/xor_cipher.py) + * [A1Z26](https://github.com/TheAlgorithms/Python/blob/master/ciphers/a1z26.py) + * [Affine Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/affine_cipher.py) + * [Atbash](https://github.com/TheAlgorithms/Python/blob/master/ciphers/atbash.py) + * [Base16](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base16.py) + * [Base32](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base32.py) + * [Base64 Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base64_cipher.py) + * [Base85](https://github.com/TheAlgorithms/Python/blob/master/ciphers/base85.py) + * [Brute Force Caesar Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/brute_force_caesar_cipher.py) + * [Caesar Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/caesar_cipher.py) + * [Cryptomath Module](https://github.com/TheAlgorithms/Python/blob/master/ciphers/cryptomath_module.py) + * [Deterministic Miller Rabin](https://github.com/TheAlgorithms/Python/blob/master/ciphers/deterministic_miller_rabin.py) + * [Diffie](https://github.com/TheAlgorithms/Python/blob/master/ciphers/diffie.py) + * [Elgamal Key Generator](https://github.com/TheAlgorithms/Python/blob/master/ciphers/elgamal_key_generator.py) + * [Hill Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/hill_cipher.py) + * [Mixed Keyword Cypher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/mixed_keyword_cypher.py) + * [Morse Code Implementation](https://github.com/TheAlgorithms/Python/blob/master/ciphers/morse_code_implementation.py) + * [Onepad Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/onepad_cipher.py) + * [Playfair Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/playfair_cipher.py) + * [Porta Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/porta_cipher.py) + * [Rabin Miller](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rabin_miller.py) + * [Rot13](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rot13.py) + * [Rsa Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rsa_cipher.py) + * [Rsa Factorization](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rsa_factorization.py) + * [Rsa Key Generator](https://github.com/TheAlgorithms/Python/blob/master/ciphers/rsa_key_generator.py) + * [Shuffled Shift Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/shuffled_shift_cipher.py) + * [Simple Keyword Cypher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/simple_keyword_cypher.py) + * [Simple Substitution Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/simple_substitution_cipher.py) + * [Trafid Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/trafid_cipher.py) + * [Transposition Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/transposition_cipher.py) + * [Transposition Cipher Encrypt Decrypt File](https://github.com/TheAlgorithms/Python/blob/master/ciphers/transposition_cipher_encrypt_decrypt_file.py) + * [Vigenere Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/vigenere_cipher.py) + * [Xor Cipher](https://github.com/TheAlgorithms/Python/blob/master/ciphers/xor_cipher.py) + ## Compression - * [burrows wheeler](https://github.com/TheAlgorithms/Python/blob/master/compression/burrows_wheeler.py) - * [huffman](https://github.com/TheAlgorithms/Python/blob/master/compression/huffman.py) - * [peak signal to noise ratio](https://github.com/TheAlgorithms/Python/blob/master/compression/peak_signal_to_noise_ratio.py) - * Image Data + * [Burrows Wheeler](https://github.com/TheAlgorithms/Python/blob/master/compression/burrows_wheeler.py) + * [Huffman](https://github.com/TheAlgorithms/Python/blob/master/compression/huffman.py) + * [Peak Signal To Noise Ratio](https://github.com/TheAlgorithms/Python/blob/master/compression/peak_signal_to_noise_ratio.py) + ## Conversions - * [decimal to binary](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_binary.py) - * [decimal to hexadecimal](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_hexadecimal.py) - * [decimal to octal](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_octal.py) + * [Decimal To Binary](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_binary.py) + * [Decimal To Hexadecimal](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_hexadecimal.py) + * [Decimal To Octal](https://github.com/TheAlgorithms/Python/blob/master/conversions/decimal_to_octal.py) + * [Roman To Integer](https://github.com/TheAlgorithms/Python/blob/master/conversions/roman_to_integer.py) + ## Data Structures * Binary Tree - * [avl tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/avl_tree.py) - * [basic binary tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/basic_binary_tree.py) - * [binary search tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/binary_search_tree.py) - * [fenwick tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/fenwick_tree.py) - * [lazy segment tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lazy_segment_tree.py) - * [lca](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lca.py) - * [red black tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/red_black_tree.py) - * [segment tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/segment_tree.py) - * [treap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/treap.py) + * [Avl Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/avl_tree.py) + * [Basic Binary Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/basic_binary_tree.py) + * [Binary Search Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/binary_search_tree.py) + * [Binary Search Tree Recursive](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/binary_search_tree_recursive.py) + * [Fenwick Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/fenwick_tree.py) + * [Lazy Segment Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lazy_segment_tree.py) + * [Lowest Common Ancestor](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/lowest_common_ancestor.py) + * [Non Recursive Segment Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/non_recursive_segment_tree.py) + * [Number Of Possible Binary Trees](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/number_of_possible_binary_trees.py) + * [Red Black Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/red_black_tree.py) + * [Segment Tree](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/segment_tree.py) + * [Segment Tree Other](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/segment_tree_other.py) + * [Treap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/binary_tree/treap.py) + * Data Structures + * Heap + * [Heap Generic](https://github.com/TheAlgorithms/Python/blob/master/data_structures/data_structures/heap/heap_generic.py) + * Disjoint Set + * [Disjoint Set](https://github.com/TheAlgorithms/Python/blob/master/data_structures/disjoint_set/disjoint_set.py) * Hashing - * [double hash](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/double_hash.py) - * [hash table](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/hash_table.py) - * [hash table with linked list](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/hash_table_with_linked_list.py) - * [quadratic probing](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/quadratic_probing.py) + * [Double Hash](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/double_hash.py) + * [Hash Table](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/hash_table.py) + * [Hash Table With Linked List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/hash_table_with_linked_list.py) * Number Theory - * [prime numbers](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/number_theory/prime_numbers.py) + * [Prime Numbers](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/number_theory/prime_numbers.py) + * [Quadratic Probing](https://github.com/TheAlgorithms/Python/blob/master/data_structures/hashing/quadratic_probing.py) * Heap - * [heap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/heap/heap.py) + * [Binomial Heap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/heap/binomial_heap.py) + * [Heap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/heap/heap.py) + * [Min Heap](https://github.com/TheAlgorithms/Python/blob/master/data_structures/heap/min_heap.py) * Linked List - * [doubly linked list](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/doubly_linked_list.py) - * [is palindrome](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/is_palindrome.py) - * [singly linked list](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/singly_linked_list.py) - * [swap nodes](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/swap_nodes.py) + * [Circular Linked List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/circular_linked_list.py) + * [Deque Doubly](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/deque_doubly.py) + * [Doubly Linked List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/doubly_linked_list.py) + * [From Sequence](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/from_sequence.py) + * [Is Palindrome](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/is_palindrome.py) + * [Middle Element Of Linked List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/middle_element_of_linked_list.py) + * [Print Reverse](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/print_reverse.py) + * [Singly Linked List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/singly_linked_list.py) + * [Skip List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/skip_list.py) + * [Swap Nodes](https://github.com/TheAlgorithms/Python/blob/master/data_structures/linked_list/swap_nodes.py) * Queue - * [double ended queue](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/double_ended_queue.py) - * [queue on list](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/queue_on_list.py) - * [queue on pseudo stack](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/queue_on_pseudo_stack.py) + * [Circular Queue](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/circular_queue.py) + * [Double Ended Queue](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/double_ended_queue.py) + * [Linked Queue](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/linked_queue.py) + * [Queue On List](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/queue_on_list.py) + * [Queue On Pseudo Stack](https://github.com/TheAlgorithms/Python/blob/master/data_structures/queue/queue_on_pseudo_stack.py) * Stacks - * [balanced parentheses](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/balanced_parentheses.py) - * [infix to postfix conversion](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/infix_to_postfix_conversion.py) - * [infix to prefix conversion](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/infix_to_prefix_conversion.py) - * [next greater element](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/next_greater_element.py) - * [postfix evaluation](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/postfix_evaluation.py) - * [stack](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/stack.py) - * [stock span problem](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/stock_span_problem.py) + * [Balanced Parentheses](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/balanced_parentheses.py) + * [Infix To Postfix Conversion](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/infix_to_postfix_conversion.py) + * [Infix To Prefix Conversion](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/infix_to_prefix_conversion.py) + * [Linked Stack](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/linked_stack.py) + * [Next Greater Element](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/next_greater_element.py) + * [Postfix Evaluation](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/postfix_evaluation.py) + * [Prefix Evaluation](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/prefix_evaluation.py) + * [Stack](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/stack.py) + * [Stack Using Dll](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/stack_using_dll.py) + * [Stock Span Problem](https://github.com/TheAlgorithms/Python/blob/master/data_structures/stacks/stock_span_problem.py) * Trie - * [trie](https://github.com/TheAlgorithms/Python/blob/master/data_structures/trie/trie.py) + * [Trie](https://github.com/TheAlgorithms/Python/blob/master/data_structures/trie/trie.py) + ## Digital Image Processing + * [Change Contrast](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/change_contrast.py) + * [Convert To Negative](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/convert_to_negative.py) + * Dithering + * [Burkes](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/dithering/burkes.py) * Edge Detection - * [canny](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/edge_detection/canny.py) + * [Canny](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/edge_detection/canny.py) * Filters - * [convolve](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/convolve.py) - * [gaussian filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/gaussian_filter.py) - * [median filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/median_filter.py) - * [sobel filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/sobel_filter.py) - * Image Data + * [Bilateral Filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/bilateral_filter.py) + * [Convolve](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/convolve.py) + * [Gaussian Filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/gaussian_filter.py) + * [Median Filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/median_filter.py) + * [Sobel Filter](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/filters/sobel_filter.py) + * Histogram Equalization + * [Histogram Stretch](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/histogram_equalization/histogram_stretch.py) + * [Index Calculation](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/index_calculation.py) + * Rotation + * [Rotation](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/rotation/rotation.py) + * [Sepia](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/sepia.py) + * [Test Digital Image Processing](https://github.com/TheAlgorithms/Python/blob/master/digital_image_processing/test_digital_image_processing.py) + ## Divide And Conquer - * [closest pair of points](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/closest_pair_of_points.py) - * [max subarray sum](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/max_subarray_sum.py) + * [Closest Pair Of Points](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/closest_pair_of_points.py) + * [Convex Hull](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/convex_hull.py) + * [Inversions](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/inversions.py) + * [Max Subarray Sum](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/max_subarray_sum.py) + * [Mergesort](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/mergesort.py) + * [Power](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/power.py) + * [Strassen Matrix Multiplication](https://github.com/TheAlgorithms/Python/blob/master/divide_and_conquer/strassen_matrix_multiplication.py) + ## Dynamic Programming - * [abbreviation](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/abbreviation.py) - * [bitmask](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/bitmask.py) - * [climbing stairs](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/climbing_stairs.py) - * [coin change](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/coin_change.py) - * [edit distance](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/edit_distance.py) - * [factorial](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/factorial.py) - * [fast fibonacci](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fast_fibonacci.py) - * [fibonacci](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fibonacci.py) - * [floyd warshall](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/floyd_warshall.py) - * [fractional knapsack](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fractional_knapsack.py) - * [integer partition](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/integer_partition.py) - * [k means clustering tensorflow](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/k_means_clustering_tensorflow.py) - * [knapsack](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/knapsack.py) - * [longest common subsequence](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_common_subsequence.py) - * [longest increasing subsequence](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_increasing_subsequence.py) - * [longest increasing subsequence o(nlogn)](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_increasing_subsequence_o(nlogn).py) - * [longest sub array](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_sub_array.py) - * [matrix chain order](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/matrix_chain_order.py) - * [max sub array](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/max_sub_array.py) - * [minimum partition](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/minimum_partition.py) - * [rod cutting](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/rod_cutting.py) - * [subset generation](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/subset_generation.py) - * [sum of subset](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/sum_of_subset.py) -## File Transfer Protocol - * [ftp client server](https://github.com/TheAlgorithms/Python/blob/master/file_transfer_protocol/ftp_client_server.py) - * [ftp send receive](https://github.com/TheAlgorithms/Python/blob/master/file_transfer_protocol/ftp_send_receive.py) + * [Abbreviation](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/abbreviation.py) + * [Bitmask](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/bitmask.py) + * [Climbing Stairs](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/climbing_stairs.py) + * [Coin Change](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/coin_change.py) + * [Edit Distance](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/edit_distance.py) + * [Factorial](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/factorial.py) + * [Fast Fibonacci](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fast_fibonacci.py) + * [Fibonacci](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fibonacci.py) + * [Floyd Warshall](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/floyd_warshall.py) + * [Fractional Knapsack](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fractional_knapsack.py) + * [Fractional Knapsack 2](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/fractional_knapsack_2.py) + * [Integer Partition](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/integer_partition.py) + * [Iterating Through Submasks](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/iterating_through_submasks.py) + * [Knapsack](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/knapsack.py) + * [Longest Common Subsequence](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_common_subsequence.py) + * [Longest Increasing Subsequence](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_increasing_subsequence.py) + * [Longest Increasing Subsequence O(Nlogn)](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_increasing_subsequence_o(nlogn).py) + * [Longest Sub Array](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/longest_sub_array.py) + * [Matrix Chain Order](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/matrix_chain_order.py) + * [Max Sub Array](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/max_sub_array.py) + * [Max Sum Contiguous Subsequence](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/max_sum_contiguous_subsequence.py) + * [Minimum Partition](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/minimum_partition.py) + * [Optimal Binary Search Tree](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/optimal_binary_search_tree.py) + * [Rod Cutting](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/rod_cutting.py) + * [Subset Generation](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/subset_generation.py) + * [Sum Of Subset](https://github.com/TheAlgorithms/Python/blob/master/dynamic_programming/sum_of_subset.py) + +## File Transfer + * [Receive File](https://github.com/TheAlgorithms/Python/blob/master/file_transfer/receive_file.py) + * [Send File](https://github.com/TheAlgorithms/Python/blob/master/file_transfer/send_file.py) + +## Fuzzy Logic + * [Fuzzy Operations](https://github.com/TheAlgorithms/Python/blob/master/fuzzy_logic/fuzzy_operations.py) + +## Geodesy + * [Haversine Distance](https://github.com/TheAlgorithms/Python/blob/master/geodesy/haversine_distance.py) + * [Lamberts Ellipsoidal Distance](https://github.com/TheAlgorithms/Python/blob/master/geodesy/lamberts_ellipsoidal_distance.py) + +## Graphics + * [Bezier Curve](https://github.com/TheAlgorithms/Python/blob/master/graphics/bezier_curve.py) + ## Graphs - * [a star](https://github.com/TheAlgorithms/Python/blob/master/graphs/a_star.py) - * [articulation points](https://github.com/TheAlgorithms/Python/blob/master/graphs/articulation_points.py) - * [basic graphs](https://github.com/TheAlgorithms/Python/blob/master/graphs/basic_graphs.py) - * [bellman ford](https://github.com/TheAlgorithms/Python/blob/master/graphs/bellman_ford.py) - * [bfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/bfs.py) - * [bfs shortest path](https://github.com/TheAlgorithms/Python/blob/master/graphs/bfs_shortest_path.py) - * [breadth first search](https://github.com/TheAlgorithms/Python/blob/master/graphs/breadth_first_search.py) - * [check bipartite graph bfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/check_bipartite_graph_bfs.py) - * [check bipartite graph dfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/check_bipartite_graph_dfs.py) - * [depth first search](https://github.com/TheAlgorithms/Python/blob/master/graphs/depth_first_search.py) - * [dfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/dfs.py) - * [dijkstra](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra.py) - * [dijkstra 2](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra_2.py) - * [dijkstra algorithm](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra_algorithm.py) - * [directed and undirected (weighted) graph](https://github.com/TheAlgorithms/Python/blob/master/graphs/directed_and_undirected_(weighted)_graph.py) - * [edmonds karp multiple source and sink](https://github.com/TheAlgorithms/Python/blob/master/graphs/edmonds_karp_multiple_source_and_sink.py) - * [eulerian path and circuit for undirected graph](https://github.com/TheAlgorithms/Python/blob/master/graphs/eulerian_path_and_circuit_for_undirected_graph.py) - * [even tree](https://github.com/TheAlgorithms/Python/blob/master/graphs/even_tree.py) - * [finding bridges](https://github.com/TheAlgorithms/Python/blob/master/graphs/finding_bridges.py) - * [graph list](https://github.com/TheAlgorithms/Python/blob/master/graphs/graph_list.py) - * [graph matrix](https://github.com/TheAlgorithms/Python/blob/master/graphs/graph_matrix.py) - * [graphs floyd warshall](https://github.com/TheAlgorithms/Python/blob/master/graphs/graphs_floyd_warshall.py) - * [kahns algorithm long](https://github.com/TheAlgorithms/Python/blob/master/graphs/kahns_algorithm_long.py) - * [kahns algorithm topo](https://github.com/TheAlgorithms/Python/blob/master/graphs/kahns_algorithm_topo.py) - * [minimum spanning tree kruskal](https://github.com/TheAlgorithms/Python/blob/master/graphs/minimum_spanning_tree_kruskal.py) - * [minimum spanning tree prims](https://github.com/TheAlgorithms/Python/blob/master/graphs/minimum_spanning_tree_prims.py) - * [multi hueristic astar](https://github.com/TheAlgorithms/Python/blob/master/graphs/multi_hueristic_astar.py) - * [page rank](https://github.com/TheAlgorithms/Python/blob/master/graphs/page_rank.py) - * [prim](https://github.com/TheAlgorithms/Python/blob/master/graphs/prim.py) - * [scc kosaraju](https://github.com/TheAlgorithms/Python/blob/master/graphs/scc_kosaraju.py) - * [tarjans scc](https://github.com/TheAlgorithms/Python/blob/master/graphs/tarjans_scc.py) + * [A Star](https://github.com/TheAlgorithms/Python/blob/master/graphs/a_star.py) + * [Articulation Points](https://github.com/TheAlgorithms/Python/blob/master/graphs/articulation_points.py) + * [Basic Graphs](https://github.com/TheAlgorithms/Python/blob/master/graphs/basic_graphs.py) + * [Bellman Ford](https://github.com/TheAlgorithms/Python/blob/master/graphs/bellman_ford.py) + * [Bfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/bfs.py) + * [Bfs Shortest Path](https://github.com/TheAlgorithms/Python/blob/master/graphs/bfs_shortest_path.py) + * [Breadth First Search](https://github.com/TheAlgorithms/Python/blob/master/graphs/breadth_first_search.py) + * [Breadth First Search Shortest Path](https://github.com/TheAlgorithms/Python/blob/master/graphs/breadth_first_search_shortest_path.py) + * [Check Bipartite Graph Bfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/check_bipartite_graph_bfs.py) + * [Check Bipartite Graph Dfs](https://github.com/TheAlgorithms/Python/blob/master/graphs/check_bipartite_graph_dfs.py) + * [Depth First Search](https://github.com/TheAlgorithms/Python/blob/master/graphs/depth_first_search.py) + * [Depth First Search 2](https://github.com/TheAlgorithms/Python/blob/master/graphs/depth_first_search_2.py) + * [Dijkstra](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra.py) + * [Dijkstra 2](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra_2.py) + * [Dijkstra Algorithm](https://github.com/TheAlgorithms/Python/blob/master/graphs/dijkstra_algorithm.py) + * [Dinic](https://github.com/TheAlgorithms/Python/blob/master/graphs/dinic.py) + * [Directed And Undirected (Weighted) Graph](https://github.com/TheAlgorithms/Python/blob/master/graphs/directed_and_undirected_(weighted)_graph.py) + * [Edmonds Karp Multiple Source And Sink](https://github.com/TheAlgorithms/Python/blob/master/graphs/edmonds_karp_multiple_source_and_sink.py) + * [Eulerian Path And Circuit For Undirected Graph](https://github.com/TheAlgorithms/Python/blob/master/graphs/eulerian_path_and_circuit_for_undirected_graph.py) + * [Even Tree](https://github.com/TheAlgorithms/Python/blob/master/graphs/even_tree.py) + * [Finding Bridges](https://github.com/TheAlgorithms/Python/blob/master/graphs/finding_bridges.py) + * [G Topological Sort](https://github.com/TheAlgorithms/Python/blob/master/graphs/g_topological_sort.py) + * [Graph List](https://github.com/TheAlgorithms/Python/blob/master/graphs/graph_list.py) + * [Graph Matrix](https://github.com/TheAlgorithms/Python/blob/master/graphs/graph_matrix.py) + * [Graphs Floyd Warshall](https://github.com/TheAlgorithms/Python/blob/master/graphs/graphs_floyd_warshall.py) + * [Kahns Algorithm Long](https://github.com/TheAlgorithms/Python/blob/master/graphs/kahns_algorithm_long.py) + * [Kahns Algorithm Topo](https://github.com/TheAlgorithms/Python/blob/master/graphs/kahns_algorithm_topo.py) + * [Minimum Spanning Tree Kruskal](https://github.com/TheAlgorithms/Python/blob/master/graphs/minimum_spanning_tree_kruskal.py) + * [Minimum Spanning Tree Prims](https://github.com/TheAlgorithms/Python/blob/master/graphs/minimum_spanning_tree_prims.py) + * [Multi Heuristic Astar](https://github.com/TheAlgorithms/Python/blob/master/graphs/multi_heuristic_astar.py) + * [Page Rank](https://github.com/TheAlgorithms/Python/blob/master/graphs/page_rank.py) + * [Prim](https://github.com/TheAlgorithms/Python/blob/master/graphs/prim.py) + * [Scc Kosaraju](https://github.com/TheAlgorithms/Python/blob/master/graphs/scc_kosaraju.py) + * [Tarjans Scc](https://github.com/TheAlgorithms/Python/blob/master/graphs/tarjans_scc.py) + ## Hashes - * [chaos machine](https://github.com/TheAlgorithms/Python/blob/master/hashes/chaos_machine.py) - * [enigma machine](https://github.com/TheAlgorithms/Python/blob/master/hashes/enigma_machine.py) - * [md5](https://github.com/TheAlgorithms/Python/blob/master/hashes/md5.py) - * [sha1](https://github.com/TheAlgorithms/Python/blob/master/hashes/sha1.py) -## Linear Algebra Python + * [Chaos Machine](https://github.com/TheAlgorithms/Python/blob/master/hashes/chaos_machine.py) + * [Enigma Machine](https://github.com/TheAlgorithms/Python/blob/master/hashes/enigma_machine.py) + * [Hamming Code](https://github.com/TheAlgorithms/Python/blob/master/hashes/hamming_code.py) + * [Md5](https://github.com/TheAlgorithms/Python/blob/master/hashes/md5.py) + * [Sha1](https://github.com/TheAlgorithms/Python/blob/master/hashes/sha1.py) + +## Linear Algebra * Src - * [lib](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra_python/src/lib.py) - * [tests](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra_python/src/tests.py) + * [Lib](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/lib.py) + * [Polynom-For-Points](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/polynom-for-points.py) + * [Rayleigh Quotient](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/rayleigh_quotient.py) + * [Test Linear Algebra](https://github.com/TheAlgorithms/Python/blob/master/linear_algebra/src/test_linear_algebra.py) + ## Machine Learning - * [decision tree](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/decision_tree.py) - * [gradient descent](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/gradient_descent.py) - * [k means clust](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/k_means_clust.py) - * [knn sklearn](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/knn_sklearn.py) - * [linear regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/linear_regression.py) - * [logistic regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/logistic_regression.py) - * [NaiveBayes](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/NaiveBayes.ipynb) - * [perceptron](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/perceptron.py) - * [reuters one vs rest classifier](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/reuters_one_vs_rest_classifier.ipynb) - * [scoring functions](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/scoring_functions.py) - * Random Forest Classification - * [random forest classification](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_classification/random_forest_classification.py) - * [random forest classifier](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_classification/random_forest_classifier.ipynb) - * [Social Network Ads](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_classification/Social_Network_Ads.csv) - * Random Forest Regression - * [Position Salaries](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_regression/Position_Salaries.csv) - * [random forest regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_regression/random_forest_regression.ipynb) - * [random forest regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_regression/random_forest_regression.py) + * [Decision Tree](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/decision_tree.py) + * [Gaussian Naive Bayes](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/gaussian_naive_bayes.py) + * [Gradient Descent](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/gradient_descent.py) + * [K Means Clust](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/k_means_clust.py) + * [K Nearest Neighbours](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/k_nearest_neighbours.py) + * [Knn Sklearn](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/knn_sklearn.py) + * [Linear Discriminant Analysis](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/linear_discriminant_analysis.py) + * [Linear Regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/linear_regression.py) + * [Logistic Regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/logistic_regression.py) + * [Multilayer Perceptron Classifier](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/multilayer_perceptron_classifier.py) + * [Polymonial Regression](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/polymonial_regression.py) + * [Random Forest Classifier](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_classifier.py) + * [Random Forest Regressor](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/random_forest_regressor.py) + * [Scoring Functions](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/scoring_functions.py) + * [Sequential Minimum Optimization](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/sequential_minimum_optimization.py) + * [Support Vector Machines](https://github.com/TheAlgorithms/Python/blob/master/machine_learning/support_vector_machines.py) + ## Maths - * [3n+1](https://github.com/TheAlgorithms/Python/blob/master/maths/3n+1.py) - * [abs](https://github.com/TheAlgorithms/Python/blob/master/maths/abs.py) - * [abs max](https://github.com/TheAlgorithms/Python/blob/master/maths/abs_max.py) - * [abs min](https://github.com/TheAlgorithms/Python/blob/master/maths/abs_min.py) - * [average mean](https://github.com/TheAlgorithms/Python/blob/master/maths/average_mean.py) - * [average median](https://github.com/TheAlgorithms/Python/blob/master/maths/average_median.py) - * [basic maths](https://github.com/TheAlgorithms/Python/blob/master/maths/basic_maths.py) - * [binary exponentiation](https://github.com/TheAlgorithms/Python/blob/master/maths/binary_exponentiation.py) - * [extended euclidean algorithm](https://github.com/TheAlgorithms/Python/blob/master/maths/extended_euclidean_algorithm.py) - * [factorial python](https://github.com/TheAlgorithms/Python/blob/master/maths/factorial_python.py) - * [factorial recursive](https://github.com/TheAlgorithms/Python/blob/master/maths/factorial_recursive.py) - * [fermat little theorem](https://github.com/TheAlgorithms/Python/blob/master/maths/fermat_little_theorem.py) - * [fibonacci](https://github.com/TheAlgorithms/Python/blob/master/maths/fibonacci.py) - * [fibonacci sequence recursion](https://github.com/TheAlgorithms/Python/blob/master/maths/fibonacci_sequence_recursion.py) - * [find lcm](https://github.com/TheAlgorithms/Python/blob/master/maths/find_lcm.py) - * [find max](https://github.com/TheAlgorithms/Python/blob/master/maths/find_max.py) - * [find min](https://github.com/TheAlgorithms/Python/blob/master/maths/find_min.py) - * [greater common divisor](https://github.com/TheAlgorithms/Python/blob/master/maths/greater_common_divisor.py) - * [lucas series](https://github.com/TheAlgorithms/Python/blob/master/maths/lucas_series.py) - * [modular exponential](https://github.com/TheAlgorithms/Python/blob/master/maths/modular_exponential.py) - * [newton raphson](https://github.com/TheAlgorithms/Python/blob/master/maths/newton_raphson.py) - * [prime check](https://github.com/TheAlgorithms/Python/blob/master/maths/prime_check.py) - * [segmented sieve](https://github.com/TheAlgorithms/Python/blob/master/maths/segmented_sieve.py) - * [sieve of eratosthenes](https://github.com/TheAlgorithms/Python/blob/master/maths/sieve_of_eratosthenes.py) - * [simpson rule](https://github.com/TheAlgorithms/Python/blob/master/maths/simpson_rule.py) - * [trapezoidal rule](https://github.com/TheAlgorithms/Python/blob/master/maths/trapezoidal_rule.py) - * [volume](https://github.com/TheAlgorithms/Python/blob/master/maths/volume.py) + * [3N Plus 1](https://github.com/TheAlgorithms/Python/blob/master/maths/3n_plus_1.py) + * [Abs](https://github.com/TheAlgorithms/Python/blob/master/maths/abs.py) + * [Abs Max](https://github.com/TheAlgorithms/Python/blob/master/maths/abs_max.py) + * [Abs Min](https://github.com/TheAlgorithms/Python/blob/master/maths/abs_min.py) + * [Add](https://github.com/TheAlgorithms/Python/blob/master/maths/add.py) + * [Allocation Number](https://github.com/TheAlgorithms/Python/blob/master/maths/allocation_number.py) + * [Area Under Curve](https://github.com/TheAlgorithms/Python/blob/master/maths/area_under_curve.py) + * [Armstrong Numbers](https://github.com/TheAlgorithms/Python/blob/master/maths/armstrong_numbers.py) + * [Average Mean](https://github.com/TheAlgorithms/Python/blob/master/maths/average_mean.py) + * [Average Median](https://github.com/TheAlgorithms/Python/blob/master/maths/average_median.py) + * [Average Mode](https://github.com/TheAlgorithms/Python/blob/master/maths/average_mode.py) + * [Basic Maths](https://github.com/TheAlgorithms/Python/blob/master/maths/basic_maths.py) + * [Binary Exp Mod](https://github.com/TheAlgorithms/Python/blob/master/maths/binary_exp_mod.py) + * [Binary Exponentiation](https://github.com/TheAlgorithms/Python/blob/master/maths/binary_exponentiation.py) + * [Binomial Coefficient](https://github.com/TheAlgorithms/Python/blob/master/maths/binomial_coefficient.py) + * [Bisection](https://github.com/TheAlgorithms/Python/blob/master/maths/bisection.py) + * [Ceil](https://github.com/TheAlgorithms/Python/blob/master/maths/ceil.py) + * [Chudnovsky Algorithm](https://github.com/TheAlgorithms/Python/blob/master/maths/chudnovsky_algorithm.py) + * [Collatz Sequence](https://github.com/TheAlgorithms/Python/blob/master/maths/collatz_sequence.py) + * [Combinations](https://github.com/TheAlgorithms/Python/blob/master/maths/combinations.py) + * [Eulers Totient](https://github.com/TheAlgorithms/Python/blob/master/maths/eulers_totient.py) + * [Explicit Euler](https://github.com/TheAlgorithms/Python/blob/master/maths/explicit_euler.py) + * [Extended Euclidean Algorithm](https://github.com/TheAlgorithms/Python/blob/master/maths/extended_euclidean_algorithm.py) + * [Factorial Iterative](https://github.com/TheAlgorithms/Python/blob/master/maths/factorial_iterative.py) + * [Factorial Python](https://github.com/TheAlgorithms/Python/blob/master/maths/factorial_python.py) + * [Factorial Recursive](https://github.com/TheAlgorithms/Python/blob/master/maths/factorial_recursive.py) + * [Factors](https://github.com/TheAlgorithms/Python/blob/master/maths/factors.py) + * [Fermat Little Theorem](https://github.com/TheAlgorithms/Python/blob/master/maths/fermat_little_theorem.py) + * [Fibonacci](https://github.com/TheAlgorithms/Python/blob/master/maths/fibonacci.py) + * [Fibonacci Sequence Recursion](https://github.com/TheAlgorithms/Python/blob/master/maths/fibonacci_sequence_recursion.py) + * [Find Max](https://github.com/TheAlgorithms/Python/blob/master/maths/find_max.py) + * [Find Max Recursion](https://github.com/TheAlgorithms/Python/blob/master/maths/find_max_recursion.py) + * [Find Min](https://github.com/TheAlgorithms/Python/blob/master/maths/find_min.py) + * [Find Min Recursion](https://github.com/TheAlgorithms/Python/blob/master/maths/find_min_recursion.py) + * [Floor](https://github.com/TheAlgorithms/Python/blob/master/maths/floor.py) + * [Gamma](https://github.com/TheAlgorithms/Python/blob/master/maths/gamma.py) + * [Gaussian](https://github.com/TheAlgorithms/Python/blob/master/maths/gaussian.py) + * [Greatest Common Divisor](https://github.com/TheAlgorithms/Python/blob/master/maths/greatest_common_divisor.py) + * [Hardy Ramanujanalgo](https://github.com/TheAlgorithms/Python/blob/master/maths/hardy_ramanujanalgo.py) + * [Is Square Free](https://github.com/TheAlgorithms/Python/blob/master/maths/is_square_free.py) + * [Jaccard Similarity](https://github.com/TheAlgorithms/Python/blob/master/maths/jaccard_similarity.py) + * [Karatsuba](https://github.com/TheAlgorithms/Python/blob/master/maths/karatsuba.py) + * [Kth Lexicographic Permutation](https://github.com/TheAlgorithms/Python/blob/master/maths/kth_lexicographic_permutation.py) + * [Largest Of Very Large Numbers](https://github.com/TheAlgorithms/Python/blob/master/maths/largest_of_very_large_numbers.py) + * [Least Common Multiple](https://github.com/TheAlgorithms/Python/blob/master/maths/least_common_multiple.py) + * [Line Length](https://github.com/TheAlgorithms/Python/blob/master/maths/line_length.py) + * [Lucas Lehmer Primality Test](https://github.com/TheAlgorithms/Python/blob/master/maths/lucas_lehmer_primality_test.py) + * [Lucas Series](https://github.com/TheAlgorithms/Python/blob/master/maths/lucas_series.py) + * [Matrix Exponentiation](https://github.com/TheAlgorithms/Python/blob/master/maths/matrix_exponentiation.py) + * [Miller Rabin](https://github.com/TheAlgorithms/Python/blob/master/maths/miller_rabin.py) + * [Mobius Function](https://github.com/TheAlgorithms/Python/blob/master/maths/mobius_function.py) + * [Modular Exponential](https://github.com/TheAlgorithms/Python/blob/master/maths/modular_exponential.py) + * [Monte Carlo](https://github.com/TheAlgorithms/Python/blob/master/maths/monte_carlo.py) + * [Monte Carlo Dice](https://github.com/TheAlgorithms/Python/blob/master/maths/monte_carlo_dice.py) + * [Newton Raphson](https://github.com/TheAlgorithms/Python/blob/master/maths/newton_raphson.py) + * [Numerical Integration](https://github.com/TheAlgorithms/Python/blob/master/maths/numerical_integration.py) + * [Perfect Square](https://github.com/TheAlgorithms/Python/blob/master/maths/perfect_square.py) + * [Pi Monte Carlo Estimation](https://github.com/TheAlgorithms/Python/blob/master/maths/pi_monte_carlo_estimation.py) + * [Polynomial Evaluation](https://github.com/TheAlgorithms/Python/blob/master/maths/polynomial_evaluation.py) + * [Prime Check](https://github.com/TheAlgorithms/Python/blob/master/maths/prime_check.py) + * [Prime Factors](https://github.com/TheAlgorithms/Python/blob/master/maths/prime_factors.py) + * [Prime Numbers](https://github.com/TheAlgorithms/Python/blob/master/maths/prime_numbers.py) + * [Prime Sieve Eratosthenes](https://github.com/TheAlgorithms/Python/blob/master/maths/prime_sieve_eratosthenes.py) + * [Pythagoras](https://github.com/TheAlgorithms/Python/blob/master/maths/pythagoras.py) + * [Qr Decomposition](https://github.com/TheAlgorithms/Python/blob/master/maths/qr_decomposition.py) + * [Quadratic Equations Complex Numbers](https://github.com/TheAlgorithms/Python/blob/master/maths/quadratic_equations_complex_numbers.py) + * [Radians](https://github.com/TheAlgorithms/Python/blob/master/maths/radians.py) + * [Radix2 Fft](https://github.com/TheAlgorithms/Python/blob/master/maths/radix2_fft.py) + * [Relu](https://github.com/TheAlgorithms/Python/blob/master/maths/relu.py) + * [Runge Kutta](https://github.com/TheAlgorithms/Python/blob/master/maths/runge_kutta.py) + * [Segmented Sieve](https://github.com/TheAlgorithms/Python/blob/master/maths/segmented_sieve.py) + * Series + * [Geometric Series](https://github.com/TheAlgorithms/Python/blob/master/maths/series/geometric_series.py) + * [Harmonic Series](https://github.com/TheAlgorithms/Python/blob/master/maths/series/harmonic_series.py) + * [P Series](https://github.com/TheAlgorithms/Python/blob/master/maths/series/p_series.py) + * [Sieve Of Eratosthenes](https://github.com/TheAlgorithms/Python/blob/master/maths/sieve_of_eratosthenes.py) + * [Simpson Rule](https://github.com/TheAlgorithms/Python/blob/master/maths/simpson_rule.py) + * [Softmax](https://github.com/TheAlgorithms/Python/blob/master/maths/softmax.py) + * [Square Root](https://github.com/TheAlgorithms/Python/blob/master/maths/square_root.py) + * [Sum Of Arithmetic Series](https://github.com/TheAlgorithms/Python/blob/master/maths/sum_of_arithmetic_series.py) + * [Test Prime Check](https://github.com/TheAlgorithms/Python/blob/master/maths/test_prime_check.py) + * [Trapezoidal Rule](https://github.com/TheAlgorithms/Python/blob/master/maths/trapezoidal_rule.py) + * [Volume](https://github.com/TheAlgorithms/Python/blob/master/maths/volume.py) + * [Zellers Congruence](https://github.com/TheAlgorithms/Python/blob/master/maths/zellers_congruence.py) + ## Matrix - * [matrix operation](https://github.com/TheAlgorithms/Python/blob/master/matrix/matrix_operation.py) - * [nth fibonacci using matrix exponentiation](https://github.com/TheAlgorithms/Python/blob/master/matrix/nth_fibonacci_using_matrix_exponentiation.py) - * [rotate matrix](https://github.com/TheAlgorithms/Python/blob/master/matrix/rotate_matrix.py) - * [searching in sorted matrix](https://github.com/TheAlgorithms/Python/blob/master/matrix/searching_in_sorted_matrix.py) - * [spiral print](https://github.com/TheAlgorithms/Python/blob/master/matrix/spiral_print.py) + * [Matrix Class](https://github.com/TheAlgorithms/Python/blob/master/matrix/matrix_class.py) + * [Matrix Operation](https://github.com/TheAlgorithms/Python/blob/master/matrix/matrix_operation.py) + * [Nth Fibonacci Using Matrix Exponentiation](https://github.com/TheAlgorithms/Python/blob/master/matrix/nth_fibonacci_using_matrix_exponentiation.py) + * [Rotate Matrix](https://github.com/TheAlgorithms/Python/blob/master/matrix/rotate_matrix.py) + * [Searching In Sorted Matrix](https://github.com/TheAlgorithms/Python/blob/master/matrix/searching_in_sorted_matrix.py) + * [Sherman Morrison](https://github.com/TheAlgorithms/Python/blob/master/matrix/sherman_morrison.py) + * [Spiral Print](https://github.com/TheAlgorithms/Python/blob/master/matrix/spiral_print.py) + * Tests + * [Test Matrix Operation](https://github.com/TheAlgorithms/Python/blob/master/matrix/tests/test_matrix_operation.py) + ## Networking Flow - * [ford fulkerson](https://github.com/TheAlgorithms/Python/blob/master/networking_flow/ford_fulkerson.py) - * [minimum cut](https://github.com/TheAlgorithms/Python/blob/master/networking_flow/minimum_cut.py) + * [Ford Fulkerson](https://github.com/TheAlgorithms/Python/blob/master/networking_flow/ford_fulkerson.py) + * [Minimum Cut](https://github.com/TheAlgorithms/Python/blob/master/networking_flow/minimum_cut.py) + ## Neural Network - * [back propagation neural network](https://github.com/TheAlgorithms/Python/blob/master/neural_network/back_propagation_neural_network.py) - * [convolution neural network](https://github.com/TheAlgorithms/Python/blob/master/neural_network/convolution_neural_network.py) - * [fully connected neural network](https://github.com/TheAlgorithms/Python/blob/master/neural_network/fully_connected_neural_network.ipynb) - * [perceptron](https://github.com/TheAlgorithms/Python/blob/master/neural_network/perceptron.py) + * [Back Propagation Neural Network](https://github.com/TheAlgorithms/Python/blob/master/neural_network/back_propagation_neural_network.py) + * [Convolution Neural Network](https://github.com/TheAlgorithms/Python/blob/master/neural_network/convolution_neural_network.py) + * [Perceptron](https://github.com/TheAlgorithms/Python/blob/master/neural_network/perceptron.py) + ## Other - * [anagrams](https://github.com/TheAlgorithms/Python/blob/master/other/anagrams.py) - * [binary exponentiation](https://github.com/TheAlgorithms/Python/blob/master/other/binary_exponentiation.py) - * [binary exponentiation 2](https://github.com/TheAlgorithms/Python/blob/master/other/binary_exponentiation_2.py) - * [detecting english programmatically](https://github.com/TheAlgorithms/Python/blob/master/other/detecting_english_programmatically.py) - * [euclidean gcd](https://github.com/TheAlgorithms/Python/blob/master/other/euclidean_gcd.py) - * [finding primes](https://github.com/TheAlgorithms/Python/blob/master/other/finding_primes.py) - * [fischer yates shuffle](https://github.com/TheAlgorithms/Python/blob/master/other/fischer_yates_shuffle.py) - * [Food wastage analysis from 1961-2013 (FAO)](https://github.com/TheAlgorithms/Python/blob/master/other/Food%20wastage%20analysis%20from%201961-2013%20(FAO).ipynb) - * [frequency finder](https://github.com/TheAlgorithms/Python/blob/master/other/frequency_finder.py) - * [game of life](https://github.com/TheAlgorithms/Python/blob/master/other/game_of_life.py) - * [linear congruential generator](https://github.com/TheAlgorithms/Python/blob/master/other/linear_congruential_generator.py) - * [nested brackets](https://github.com/TheAlgorithms/Python/blob/master/other/nested_brackets.py) - * [palindrome](https://github.com/TheAlgorithms/Python/blob/master/other/palindrome.py) - * [password generator](https://github.com/TheAlgorithms/Python/blob/master/other/password_generator.py) - * [primelib](https://github.com/TheAlgorithms/Python/blob/master/other/primelib.py) - * [sierpinski triangle](https://github.com/TheAlgorithms/Python/blob/master/other/sierpinski_triangle.py) - * [tower of hanoi](https://github.com/TheAlgorithms/Python/blob/master/other/tower_of_hanoi.py) - * [two sum](https://github.com/TheAlgorithms/Python/blob/master/other/two_sum.py) - * [word patterns](https://github.com/TheAlgorithms/Python/blob/master/other/word_patterns.py) - * [words](https://github.com/TheAlgorithms/Python/blob/master/other/words) + * [Activity Selection](https://github.com/TheAlgorithms/Python/blob/master/other/activity_selection.py) + * [Anagrams](https://github.com/TheAlgorithms/Python/blob/master/other/anagrams.py) + * [Autocomplete Using Trie](https://github.com/TheAlgorithms/Python/blob/master/other/autocomplete_using_trie.py) + * [Binary Exponentiation](https://github.com/TheAlgorithms/Python/blob/master/other/binary_exponentiation.py) + * [Binary Exponentiation 2](https://github.com/TheAlgorithms/Python/blob/master/other/binary_exponentiation_2.py) + * [Detecting English Programmatically](https://github.com/TheAlgorithms/Python/blob/master/other/detecting_english_programmatically.py) + * [Dijkstra Bankers Algorithm](https://github.com/TheAlgorithms/Python/blob/master/other/dijkstra_bankers_algorithm.py) + * [Euclidean Gcd](https://github.com/TheAlgorithms/Python/blob/master/other/euclidean_gcd.py) + * [Fischer Yates Shuffle](https://github.com/TheAlgorithms/Python/blob/master/other/fischer_yates_shuffle.py) + * [Frequency Finder](https://github.com/TheAlgorithms/Python/blob/master/other/frequency_finder.py) + * [Game Of Life](https://github.com/TheAlgorithms/Python/blob/master/other/game_of_life.py) + * [Greedy](https://github.com/TheAlgorithms/Python/blob/master/other/greedy.py) + * [Integeration By Simpson Approx](https://github.com/TheAlgorithms/Python/blob/master/other/integeration_by_simpson_approx.py) + * [Largest Subarray Sum](https://github.com/TheAlgorithms/Python/blob/master/other/largest_subarray_sum.py) + * [Least Recently Used](https://github.com/TheAlgorithms/Python/blob/master/other/least_recently_used.py) + * [Linear Congruential Generator](https://github.com/TheAlgorithms/Python/blob/master/other/linear_congruential_generator.py) + * [Magicdiamondpattern](https://github.com/TheAlgorithms/Python/blob/master/other/magicdiamondpattern.py) + * [Nested Brackets](https://github.com/TheAlgorithms/Python/blob/master/other/nested_brackets.py) + * [Palindrome](https://github.com/TheAlgorithms/Python/blob/master/other/palindrome.py) + * [Password Generator](https://github.com/TheAlgorithms/Python/blob/master/other/password_generator.py) + * [Primelib](https://github.com/TheAlgorithms/Python/blob/master/other/primelib.py) + * [Sdes](https://github.com/TheAlgorithms/Python/blob/master/other/sdes.py) + * [Sierpinski Triangle](https://github.com/TheAlgorithms/Python/blob/master/other/sierpinski_triangle.py) + * [Tower Of Hanoi](https://github.com/TheAlgorithms/Python/blob/master/other/tower_of_hanoi.py) + * [Two Sum](https://github.com/TheAlgorithms/Python/blob/master/other/two_sum.py) + * [Word Patterns](https://github.com/TheAlgorithms/Python/blob/master/other/word_patterns.py) + ## Project Euler * Problem 01 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol2.py) - * [sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol3.py) - * [sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol4.py) - * [sol5](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol5.py) - * [sol6](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol6.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol3.py) + * [Sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol4.py) + * [Sol5](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol5.py) + * [Sol6](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol6.py) + * [Sol7](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_01/sol7.py) * Problem 02 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol2.py) - * [sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol3.py) - * [sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol4.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol3.py) + * [Sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol4.py) + * [Sol5](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_02/sol5.py) * Problem 03 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_03/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_03/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_03/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_03/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_03/sol3.py) * Problem 04 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_04/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_04/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_04/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_04/sol2.py) * Problem 05 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_05/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_05/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_05/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_05/sol2.py) * Problem 06 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol2.py) - * [sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol3.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol3.py) + * [Sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_06/sol4.py) * Problem 07 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol2.py) - * [sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol3.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_07/sol3.py) * Problem 08 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_08/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_08/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_08/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_08/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_08/sol3.py) * Problem 09 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol2.py) - * [sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol3.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_09/sol3.py) * Problem 10 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_10/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_10/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_10/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_10/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_10/sol3.py) * Problem 11 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_11/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_11/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_11/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_11/sol2.py) * Problem 12 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_12/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_12/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_12/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_12/sol2.py) * Problem 13 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_13/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_13/sol1.py) * Problem 14 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_14/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_14/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_14/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_14/sol2.py) * Problem 15 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_15/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_15/sol1.py) * Problem 16 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_16/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_16/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_16/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_16/sol2.py) * Problem 17 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_17/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_17/sol1.py) + * Problem 18 + * [Solution](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_18/solution.py) * Problem 19 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_19/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_19/sol1.py) * Problem 20 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol3.py) + * [Sol4](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_20/sol4.py) * Problem 21 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_21/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_21/sol1.py) * Problem 22 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_22/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_22/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_22/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_22/sol2.py) + * Problem 23 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_23/sol1.py) * Problem 234 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_234/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_234/sol1.py) * Problem 24 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_24/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_24/sol1.py) * Problem 25 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_25/sol1.py) - * [sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_25/sol2.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_25/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_25/sol2.py) + * [Sol3](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_25/sol3.py) + * Problem 26 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_26/sol1.py) + * Problem 27 + * [Problem 27 Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_27/problem_27_sol1.py) * Problem 28 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_28/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_28/sol1.py) * Problem 29 - * [solution](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_29/solution.py) + * [Solution](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_29/solution.py) + * Problem 30 + * [Soln](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_30/soln.py) * Problem 31 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_31/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_31/sol1.py) + * [Sol2](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_31/sol2.py) + * Problem 32 + * [Sol32](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_32/sol32.py) + * Problem 33 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_33/sol1.py) * Problem 36 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_36/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_36/sol1.py) * Problem 40 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_40/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_40/sol1.py) + * Problem 42 + * [Solution42](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_42/solution42.py) * Problem 48 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_48/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_48/sol1.py) * Problem 52 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_52/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_52/sol1.py) * Problem 53 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_53/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_53/sol1.py) + * Problem 551 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_551/sol1.py) + * Problem 56 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_56/sol1.py) + * Problem 67 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_67/sol1.py) * Problem 76 - * [sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_76/sol1.py) + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_76/sol1.py) + * Problem 99 + * [Sol1](https://github.com/TheAlgorithms/Python/blob/master/project_euler/problem_99/sol1.py) + +## Scheduling + * [First Come First Served](https://github.com/TheAlgorithms/Python/blob/master/scheduling/first_come_first_served.py) + ## Searches - * [binary search](https://github.com/TheAlgorithms/Python/blob/master/searches/binary_search.py) - * [interpolation search](https://github.com/TheAlgorithms/Python/blob/master/searches/interpolation_search.py) - * [jump search](https://github.com/TheAlgorithms/Python/blob/master/searches/jump_search.py) - * [linear search](https://github.com/TheAlgorithms/Python/blob/master/searches/linear_search.py) - * [quick select](https://github.com/TheAlgorithms/Python/blob/master/searches/quick_select.py) - * [sentinel linear search](https://github.com/TheAlgorithms/Python/blob/master/searches/sentinel_linear_search.py) - * [tabu search](https://github.com/TheAlgorithms/Python/blob/master/searches/tabu_search.py) - * [ternary search](https://github.com/TheAlgorithms/Python/blob/master/searches/ternary_search.py) + * [Binary Search](https://github.com/TheAlgorithms/Python/blob/master/searches/binary_search.py) + * [Fibonacci Search](https://github.com/TheAlgorithms/Python/blob/master/searches/fibonacci_search.py) + * [Hill Climbing](https://github.com/TheAlgorithms/Python/blob/master/searches/hill_climbing.py) + * [Interpolation Search](https://github.com/TheAlgorithms/Python/blob/master/searches/interpolation_search.py) + * [Jump Search](https://github.com/TheAlgorithms/Python/blob/master/searches/jump_search.py) + * [Linear Search](https://github.com/TheAlgorithms/Python/blob/master/searches/linear_search.py) + * [Quick Select](https://github.com/TheAlgorithms/Python/blob/master/searches/quick_select.py) + * [Sentinel Linear Search](https://github.com/TheAlgorithms/Python/blob/master/searches/sentinel_linear_search.py) + * [Simple-Binary-Search](https://github.com/TheAlgorithms/Python/blob/master/searches/simple-binary-search.py) + * [Simulated Annealing](https://github.com/TheAlgorithms/Python/blob/master/searches/simulated_annealing.py) + * [Tabu Search](https://github.com/TheAlgorithms/Python/blob/master/searches/tabu_search.py) + * [Ternary Search](https://github.com/TheAlgorithms/Python/blob/master/searches/ternary_search.py) + ## Sorts - * [bitonic sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bitonic_sort.py) - * [bogo sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bogo_sort.py) - * [bubble sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bubble_sort.py) - * [bucket sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bucket_sort.py) - * [cocktail shaker sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/cocktail_shaker_sort.py) - * [comb sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/comb_sort.py) - * [counting sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/counting_sort.py) - * [cycle sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/cycle_sort.py) - * [external sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/external_sort.py) - * [gnome sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/gnome_sort.py) - * [heap sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/heap_sort.py) - * [insertion sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/insertion_sort.py) - * [merge sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/merge_sort.py) - * [merge sort fastest](https://github.com/TheAlgorithms/Python/blob/master/sorts/merge_sort_fastest.py) - * [odd even transposition parallel](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_parallel.py) - * [odd even transposition single threaded](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_single_threaded.py) - * [pancake sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pancake_sort.py) - * [pigeon sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pigeon_sort.py) - * [quick sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/quick_sort.py) - * [quick sort 3 partition](https://github.com/TheAlgorithms/Python/blob/master/sorts/quick_sort_3_partition.py) - * [radix sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/radix_sort.py) - * [random normal distribution quicksort](https://github.com/TheAlgorithms/Python/blob/master/sorts/random_normal_distribution_quicksort.py) - * [random pivot quick sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/random_pivot_quick_sort.py) - * [selection sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/selection_sort.py) - * [shell sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/shell_sort.py) - * [tim sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/tim_sort.py) - * [topological sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/topological_sort.py) - * [tree sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/tree_sort.py) - * [wiggle sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/wiggle_sort.py) + * [Bitonic Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bitonic_sort.py) + * [Bogo Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bogo_sort.py) + * [Bubble Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bubble_sort.py) + * [Bucket Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/bucket_sort.py) + * [Cocktail Shaker Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/cocktail_shaker_sort.py) + * [Comb Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/comb_sort.py) + * [Counting Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/counting_sort.py) + * [Cycle Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/cycle_sort.py) + * [Double Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/double_sort.py) + * [External Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/external_sort.py) + * [Gnome Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/gnome_sort.py) + * [Heap Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/heap_sort.py) + * [I Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/i_sort.py) + * [Insertion Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/insertion_sort.py) + * [Merge Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/merge_sort.py) + * [Odd Even Transposition Parallel](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_parallel.py) + * [Odd Even Transposition Single Threaded](https://github.com/TheAlgorithms/Python/blob/master/sorts/odd_even_transposition_single_threaded.py) + * [Pancake Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pancake_sort.py) + * [Pigeon Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pigeon_sort.py) + * [Pigeonhole Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/pigeonhole_sort.py) + * [Quick Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/quick_sort.py) + * [Quick Sort 3 Partition](https://github.com/TheAlgorithms/Python/blob/master/sorts/quick_sort_3_partition.py) + * [Radix Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/radix_sort.py) + * [Random Normal Distribution Quicksort](https://github.com/TheAlgorithms/Python/blob/master/sorts/random_normal_distribution_quicksort.py) + * [Random Pivot Quick Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/random_pivot_quick_sort.py) + * [Recursive-Quick-Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/recursive-quick-sort.py) + * [Recursive Bubble Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/recursive_bubble_sort.py) + * [Recursive Insertion Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/recursive_insertion_sort.py) + * [Selection Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/selection_sort.py) + * [Shell Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/shell_sort.py) + * [Stooge Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/stooge_sort.py) + * [Tim Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/tim_sort.py) + * [Topological Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/topological_sort.py) + * [Tree Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/tree_sort.py) + * [Unknown Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/unknown_sort.py) + * [Wiggle Sort](https://github.com/TheAlgorithms/Python/blob/master/sorts/wiggle_sort.py) + ## Strings - * [boyer moore search](https://github.com/TheAlgorithms/Python/blob/master/strings/boyer_moore_search.py) - * [knuth morris pratt](https://github.com/TheAlgorithms/Python/blob/master/strings/knuth_morris_pratt.py) - * [levenshtein distance](https://github.com/TheAlgorithms/Python/blob/master/strings/levenshtein_distance.py) - * [manacher](https://github.com/TheAlgorithms/Python/blob/master/strings/manacher.py) - * [min cost string conversion](https://github.com/TheAlgorithms/Python/blob/master/strings/min_cost_string_conversion.py) - * [naive string search](https://github.com/TheAlgorithms/Python/blob/master/strings/naive_string_search.py) - * [rabin karp](https://github.com/TheAlgorithms/Python/blob/master/strings/rabin_karp.py) + * [Aho-Corasick](https://github.com/TheAlgorithms/Python/blob/master/strings/aho-corasick.py) + * [Boyer Moore Search](https://github.com/TheAlgorithms/Python/blob/master/strings/boyer_moore_search.py) + * [Check Pangram](https://github.com/TheAlgorithms/Python/blob/master/strings/check_pangram.py) + * [Is Palindrome](https://github.com/TheAlgorithms/Python/blob/master/strings/is_palindrome.py) + * [Knuth Morris Pratt](https://github.com/TheAlgorithms/Python/blob/master/strings/knuth_morris_pratt.py) + * [Levenshtein Distance](https://github.com/TheAlgorithms/Python/blob/master/strings/levenshtein_distance.py) + * [Lower](https://github.com/TheAlgorithms/Python/blob/master/strings/lower.py) + * [Manacher](https://github.com/TheAlgorithms/Python/blob/master/strings/manacher.py) + * [Min Cost String Conversion](https://github.com/TheAlgorithms/Python/blob/master/strings/min_cost_string_conversion.py) + * [Naive String Search](https://github.com/TheAlgorithms/Python/blob/master/strings/naive_string_search.py) + * [Rabin Karp](https://github.com/TheAlgorithms/Python/blob/master/strings/rabin_karp.py) + * [Remove Duplicate](https://github.com/TheAlgorithms/Python/blob/master/strings/remove_duplicate.py) + * [Reverse Words](https://github.com/TheAlgorithms/Python/blob/master/strings/reverse_words.py) + * [Split](https://github.com/TheAlgorithms/Python/blob/master/strings/split.py) + * [Upper](https://github.com/TheAlgorithms/Python/blob/master/strings/upper.py) + * [Word Occurrence](https://github.com/TheAlgorithms/Python/blob/master/strings/word_occurrence.py) + ## Traversals - * [binary tree traversals](https://github.com/TheAlgorithms/Python/blob/master/traversals/binary_tree_traversals.py) + * [Binary Tree Traversals](https://github.com/TheAlgorithms/Python/blob/master/traversals/binary_tree_traversals.py) + +## Web Programming + * [Crawl Google Results](https://github.com/TheAlgorithms/Python/blob/master/web_programming/crawl_google_results.py) + * [Current Stock Price](https://github.com/TheAlgorithms/Python/blob/master/web_programming/current_stock_price.py) + * [Emails From Url](https://github.com/TheAlgorithms/Python/blob/master/web_programming/emails_from_url.py) + * [Fetch Bbc News](https://github.com/TheAlgorithms/Python/blob/master/web_programming/fetch_bbc_news.py) + * [Fetch Github Info](https://github.com/TheAlgorithms/Python/blob/master/web_programming/fetch_github_info.py) + * [Get Imdb Top 250 Movies Csv](https://github.com/TheAlgorithms/Python/blob/master/web_programming/get_imdb_top_250_movies_csv.py) + * [Get Imdbtop](https://github.com/TheAlgorithms/Python/blob/master/web_programming/get_imdbtop.py) + * [Slack Message](https://github.com/TheAlgorithms/Python/blob/master/web_programming/slack_message.py) diff --git a/LICENSE.md b/LICENSE.md index a20869d96300..3b7951527ab3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 The Algorithms +Copyright (c) 2020 The Algorithms Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 30eccd361673..106f5907eebf 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,25 @@ -# The Algorithms - Python - -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/TheAlgorithms/100)   -[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/TheAlgorithms)   -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TheAlgorithms/Python) +# The Algorithms - Python +[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/TheAlgorithms/Python) +[![Gitter chat](https://img.shields.io/badge/Chat-Gitter-ff69b4.svg?label=Chat&logo=gitter&style=flat-square)](https://gitter.im/TheAlgorithms)  +[![Build Status](https://img.shields.io/travis/TheAlgorithms/Python.svg?label=Travis%20CI&logo=travis&style=flat-square)](https://travis-ci.com/TheAlgorithms/Python)  +[![LGTM](https://img.shields.io/lgtm/alerts/github/TheAlgorithms/Python.svg?label=LGTM&logo=LGTM&style=flat-square)](https://lgtm.com/projects/g/TheAlgorithms/Python/alerts)  +[![contributions welcome](https://img.shields.io/static/v1.svg?label=Contributions&message=Welcome&color=0059b3&style=flat-square)](https://github.com/TheAlgorithms/Python/blob/master/CONTRIBUTING.md)  +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal&style=flat-square)](https://www.paypal.me/TheAlgorithms/100)  +![](https://img.shields.io/github/repo-size/TheAlgorithms/Python.svg?label=Repo%20size&style=flat-square)  + ### All algorithms implemented in Python (for education) These implementations are for learning purposes. They may be less efficient than the implementations in the Python standard library. -## Owners - -Anup Kumar Panwar -  [[Gmail](mailto:1anuppanwar@gmail.com?Subject=The%20Algorithms%20-%20Python) -  [GitHub](https://github.com/anupkumarpanwar) -  [LinkedIn](https://www.linkedin.com/in/anupkumarpanwar/)] - -Chetan Kaushik -  [[Gmail](mailto:dynamitechetan@gmail.com?Subject=The%20Algorithms%20-%20Python) -  [GitHub](https://github.com/dynamitechetan) -  [LinkedIn](https://www.linkedin.com/in/chetankaushik/)] - ## Contribution Guidelines Read our [Contribution Guidelines](CONTRIBUTING.md) before you contribute. ## Community Channel -We're on [Gitter](https://gitter.im/TheAlgorithms)! Please join us. +We're on [Gitter](https://gitter.im/TheAlgorithms)! Please join us. -## Algorithms +## List of Algorithms See our [directory](DIRECTORY.md). diff --git a/Travis_CI_tests_are_failing.md b/Travis_CI_tests_are_failing.md new file mode 100644 index 000000000000..10bf5a6655d2 --- /dev/null +++ b/Travis_CI_tests_are_failing.md @@ -0,0 +1,9 @@ +# Travis CI test are failing +### How do I find out what is wrong with my pull request? +1. In your PR look for the failing test and click the `Details` link: ![Travis_CI_fail_1.png](images/Travis_CI_fail_1.png) +2. On the next page, click `The build failed` link: ![Travis_CI_fail_2.png](images/Travis_CI_fail_2.png) +3. Now scroll down and look for `red` text describing the error(s) in the test log. + +Pull requests will __not__ be merged if the Travis CI tests are failing. + +If anything is unclear, please read through [CONTRIBUTING.md](CONTRIBUTING.md) and attempt to run the failing tests on your computer before asking for assistance. diff --git a/arithmetic_analysis/bisection.py b/arithmetic_analysis/bisection.py index 8bf3f09782a3..78582b025880 100644 --- a/arithmetic_analysis/bisection.py +++ b/arithmetic_analysis/bisection.py @@ -1,7 +1,9 @@ import math -def bisection(function, a, b): # finds where the function becomes 0 in [a,b] using bolzano +def bisection( + function, a, b +): # finds where the function becomes 0 in [a,b] using bolzano start = a end = b @@ -9,13 +11,15 @@ def bisection(function, a, b): # finds where the function becomes 0 in [a,b] us return a elif function(b) == 0: return b - elif function(a) * function(b) > 0: # if none of these are root and they are both positive or negative, + elif ( + function(a) * function(b) > 0 + ): # if none of these are root and they are both positive or negative, # then his algorithm can't find the root print("couldn't find root in [a,b]") return else: mid = start + (end - start) / 2.0 - while abs(start - mid) > 10**-7: # until we achieve precise equals to 10^-7 + while abs(start - mid) > 10 ** -7: # until we achieve precise equals to 10^-7 if function(mid) == 0: return mid elif function(mid) * function(start) < 0: @@ -27,7 +31,8 @@ def bisection(function, a, b): # finds where the function becomes 0 in [a,b] us def f(x): - return math.pow(x, 3) - 2*x - 5 + return math.pow(x, 3) - 2 * x - 5 + if __name__ == "__main__": print(bisection(f, 1, 1000)) diff --git a/arithmetic_analysis/gaussian_elimination.py b/arithmetic_analysis/gaussian_elimination.py new file mode 100644 index 000000000000..51207686c12a --- /dev/null +++ b/arithmetic_analysis/gaussian_elimination.py @@ -0,0 +1,83 @@ +""" +Gaussian elimination method for solving a system of linear equations. +Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination +""" + + +import numpy as np + + +def retroactive_resolution(coefficients: np.matrix, vector: np.array) -> np.array: + """ + This function performs a retroactive linear system resolution + for triangular matrix + + Examples: + 2x1 + 2x2 - 1x3 = 5 2x1 + 2x2 = -1 + 0x1 - 2x2 - 1x3 = -7 0x1 - 2x2 = -1 + 0x1 + 0x2 + 5x3 = 15 + >>> gaussian_elimination([[2, 2, -1], [0, -2, -1], [0, 0, 5]], [[5], [-7], [15]]) + array([[2.], + [2.], + [3.]]) + >>> gaussian_elimination([[2, 2], [0, -2]], [[-1], [-1]]) + array([[-1. ], + [ 0.5]]) + """ + + rows, columns = np.shape(coefficients) + + x = np.zeros((rows, 1), dtype=float) + for row in reversed(range(rows)): + sum = 0 + for col in range(row + 1, columns): + sum += coefficients[row, col] * x[col] + + x[row, 0] = (vector[row] - sum) / coefficients[row, row] + + return x + + +def gaussian_elimination(coefficients: np.matrix, vector: np.array) -> np.array: + """ + This function performs Gaussian elimination method + + Examples: + 1x1 - 4x2 - 2x3 = -2 1x1 + 2x2 = 5 + 5x1 + 2x2 - 2x3 = -3 5x1 + 2x2 = 5 + 1x1 - 1x2 + 0x3 = 4 + >>> gaussian_elimination([[1, -4, -2], [5, 2, -2], [1, -1, 0]], [[-2], [-3], [4]]) + array([[ 2.3 ], + [-1.7 ], + [ 5.55]]) + >>> gaussian_elimination([[1, 2], [5, 2]], [[5], [5]]) + array([[0. ], + [2.5]]) + """ + # coefficients must to be a square matrix so we need to check first + rows, columns = np.shape(coefficients) + if rows != columns: + return [] + + # augmented matrix + augmented_mat = np.concatenate((coefficients, vector), axis=1) + augmented_mat = augmented_mat.astype("float64") + + # scale the matrix leaving it triangular + for row in range(rows - 1): + pivot = augmented_mat[row, row] + for col in range(row + 1, columns): + factor = augmented_mat[col, row] / pivot + augmented_mat[col, :] -= factor * augmented_mat[row, :] + + x = retroactive_resolution( + augmented_mat[:, 0:columns], augmented_mat[:, columns : columns + 1] + ) + + return x + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/arithmetic_analysis/image_data/2D_problems.jpg b/arithmetic_analysis/image_data/2D_problems.jpg new file mode 100644 index 000000000000..8887cf641685 Binary files /dev/null and b/arithmetic_analysis/image_data/2D_problems.jpg differ diff --git a/arithmetic_analysis/image_data/2D_problems_1.jpg b/arithmetic_analysis/image_data/2D_problems_1.jpg new file mode 100644 index 000000000000..aa9f45362014 Binary files /dev/null and b/arithmetic_analysis/image_data/2D_problems_1.jpg differ diff --git a/arithmetic_analysis/in_static_equilibrium.py b/arithmetic_analysis/in_static_equilibrium.py new file mode 100644 index 000000000000..addaff888f7f --- /dev/null +++ b/arithmetic_analysis/in_static_equilibrium.py @@ -0,0 +1,86 @@ +""" +Checks if a system of forces is in static equilibrium. + +python/black : true +flake8 : passed +mypy : passed +""" + +from numpy import array, cos, sin, radians, cross # type: ignore +from typing import List + + +def polar_force( + magnitude: float, angle: float, radian_mode: bool = False +) -> List[float]: + """ + Resolves force along rectangular components. + (force, angle) => (force_x, force_y) + >>> polar_force(10, 45) + [7.0710678118654755, 7.071067811865475] + >>> polar_force(10, 3.14, radian_mode=True) + [-9.999987317275394, 0.01592652916486828] + """ + if radian_mode: + return [magnitude * cos(angle), magnitude * sin(angle)] + return [magnitude * cos(radians(angle)), magnitude * sin(radians(angle))] + + +def in_static_equilibrium( + forces: array, location: array, eps: float = 10 ** -1 +) -> bool: + """ + Check if a system is in equilibrium. + It takes two numpy.array objects. + forces ==> [ + [force1_x, force1_y], + [force2_x, force2_y], + ....] + location ==> [ + [x1, y1], + [x2, y2], + ....] + >>> force = array([[1, 1], [-1, 2]]) + >>> location = array([[1, 0], [10, 0]]) + >>> in_static_equilibrium(force, location) + False + """ + # summation of moments is zero + moments: array = cross(location, forces) + sum_moments: float = sum(moments) + return abs(sum_moments) < eps + + +if __name__ == "__main__": + # Test to check if it works + forces = array( + [polar_force(718.4, 180 - 30), polar_force(879.54, 45), polar_force(100, -90)] + ) + + location = array([[0, 0], [0, 0], [0, 0]]) + + assert in_static_equilibrium(forces, location) + + # Problem 1 in image_data/2D_problems.jpg + forces = array( + [ + polar_force(30 * 9.81, 15), + polar_force(215, 180 - 45), + polar_force(264, 90 - 30), + ] + ) + + location = array([[0, 0], [0, 0], [0, 0]]) + + assert in_static_equilibrium(forces, location) + + # Problem in image_data/2D_problems_1.jpg + forces = array([[0, -2000], [0, -1200], [0, 15600], [0, -12400]]) + + location = array([[0, 0], [6, 0], [10, 0], [12, 0]]) + + assert in_static_equilibrium(forces, location) + + import doctest + + doctest.testmod() diff --git a/arithmetic_analysis/intersection.py b/arithmetic_analysis/intersection.py index 2f25f76ebd96..0fdcfbf1943e 100644 --- a/arithmetic_analysis/intersection.py +++ b/arithmetic_analysis/intersection.py @@ -1,17 +1,24 @@ import math -def intersection(function,x0,x1): #function is the f we want to find its root and x0 and x1 are two random starting points + +def intersection( + function, x0, x1 +): # function is the f we want to find its root and x0 and x1 are two random starting points x_n = x0 x_n1 = x1 while True: - x_n2 = x_n1-(function(x_n1)/((function(x_n1)-function(x_n))/(x_n1-x_n))) - if abs(x_n2 - x_n1) < 10**-5: + x_n2 = x_n1 - ( + function(x_n1) / ((function(x_n1) - function(x_n)) / (x_n1 - x_n)) + ) + if abs(x_n2 - x_n1) < 10 ** -5: return x_n2 - x_n=x_n1 - x_n1=x_n2 + x_n = x_n1 + x_n1 = x_n2 + def f(x): - return math.pow(x , 3) - (2 * x) -5 + return math.pow(x, 3) - (2 * x) - 5 + if __name__ == "__main__": - print(intersection(f,3,3.5)) + print(intersection(f, 3, 3.5)) diff --git a/arithmetic_analysis/lu_decomposition.py b/arithmetic_analysis/lu_decomposition.py index 19e259afb826..4372621d74cb 100644 --- a/arithmetic_analysis/lu_decomposition.py +++ b/arithmetic_analysis/lu_decomposition.py @@ -28,9 +28,7 @@ def LUDecompose(table): if __name__ == "__main__": - matrix = numpy.array([[2, -2, 1], - [0, 1, 2], - [5, 3, 1]]) + matrix = numpy.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]]) L, U = LUDecompose(matrix) print(L) print(U) diff --git a/arithmetic_analysis/newton_forward_interpolation.py b/arithmetic_analysis/newton_forward_interpolation.py new file mode 100644 index 000000000000..d91b9709f3d6 --- /dev/null +++ b/arithmetic_analysis/newton_forward_interpolation.py @@ -0,0 +1,55 @@ +# https://www.geeksforgeeks.org/newton-forward-backward-interpolation/ + +import math + +# for calculating u value +def ucal(u, p): + """ + >>> ucal(1, 2) + 0 + >>> ucal(1.1, 2) + 0.11000000000000011 + >>> ucal(1.2, 2) + 0.23999999999999994 + """ + temp = u + for i in range(1, p): + temp = temp * (u - i) + return temp + + +def main(): + n = int(input("enter the numbers of values: ")) + y = [] + for i in range(n): + y.append([]) + for i in range(n): + for j in range(n): + y[i].append(j) + y[i][j] = 0 + + print("enter the values of parameters in a list: ") + x = list(map(int, input().split())) + + print("enter the values of corresponding parameters: ") + for i in range(n): + y[i][0] = float(input()) + + value = int(input("enter the value to interpolate: ")) + u = (value - x[0]) / (x[1] - x[0]) + + # for calculating forward difference table + + for i in range(1, n): + for j in range(n - i): + y[j][i] = y[j + 1][i - 1] - y[j][i - 1] + + summ = y[0][0] + for i in range(1, n): + summ += (ucal(u, i) * y[0][i]) / math.factorial(i) + + print(f"the value at {value} is {summ}") + + +if __name__ == "__main__": + main() diff --git a/arithmetic_analysis/newton_method.py b/arithmetic_analysis/newton_method.py index cf5649ee3f3b..1408a983041d 100644 --- a/arithmetic_analysis/newton_method.py +++ b/arithmetic_analysis/newton_method.py @@ -8,17 +8,17 @@ def newton(function, function1, startingInt): x_n = startingInt while True: x_n1 = x_n - function(x_n) / function1(x_n) - if abs(x_n - x_n1) < 10**-5: + if abs(x_n - x_n1) < 10 ** -5: return x_n1 x_n = x_n1 def f(x): - return (x**3) - (2 * x) - 5 + return (x ** 3) - (2 * x) - 5 def f1(x): - return 3 * (x**2) - 2 + return 3 * (x ** 2) - 2 if __name__ == "__main__": diff --git a/arithmetic_analysis/newton_raphson.py b/arithmetic_analysis/newton_raphson.py new file mode 100644 index 000000000000..a3e8bbf0fc21 --- /dev/null +++ b/arithmetic_analysis/newton_raphson.py @@ -0,0 +1,40 @@ +# Implementing Newton Raphson method in Python +# Author: Syed Haseeb Shah (github.com/QuantumNovice) +# The Newton-Raphson method (also known as Newton's method) is a way to +# quickly find a good approximation for the root of a real-valued function + +from decimal import Decimal +from math import * # noqa: F401, F403 +from sympy import diff + + +def newton_raphson(func: str, a: int, precision: int = 10 ** -10) -> float: + """ Finds root from the point 'a' onwards by Newton-Raphson method + >>> newton_raphson("sin(x)", 2) + 3.1415926536808043 + >>> newton_raphson("x**2 - 5*x +2", 0.4) + 0.4384471871911695 + >>> newton_raphson("x**2 - 5", 0.1) + 2.23606797749979 + >>> newton_raphson("log(x)- 1", 2) + 2.718281828458938 + """ + x = a + while True: + x = Decimal(x) - (Decimal(eval(func)) / Decimal(eval(str(diff(func))))) + # This number dictates the accuracy of the answer + if abs(eval(func)) < precision: + return float(x) + + +# Let's Execute +if __name__ == "__main__": + # Find root of trigonometric function + # Find value of pi + print(f"The root of sin(x) = 0 is {newton_raphson('sin(x)', 2)}") + # Find root of polynomial + print(f"The root of x**2 - 5*x + 2 = 0 is {newton_raphson('x**2 - 5*x + 2', 0.4)}") + # Find Square Root of 5 + print(f"The root of log(x) - 1 = 0 is {newton_raphson('log(x) - 1', 2)}") + # Exponential Roots + print(f"The root of exp(x) - 1 = 0 is {newton_raphson('exp(x) - 1', 0)}") diff --git a/arithmetic_analysis/newton_raphson_method.py b/arithmetic_analysis/newton_raphson_method.py deleted file mode 100644 index 569f96476afc..000000000000 --- a/arithmetic_analysis/newton_raphson_method.py +++ /dev/null @@ -1,32 +0,0 @@ -# Implementing Newton Raphson method in Python -# Author: Syed Haseeb Shah (github.com/QuantumNovice) - -from sympy import diff -from decimal import Decimal - -def NewtonRaphson(func, a): - ''' Finds root from the point 'a' onwards by Newton-Raphson method ''' - while True: - c = Decimal(a) - ( Decimal(eval(func)) / Decimal(eval(str(diff(func)))) ) - - a = c - - # This number dictates the accuracy of the answer - if abs(eval(func)) < 10**-15: - return c - - -# Let's Execute -if __name__ == '__main__': - # Find root of trigonometric function - # Find value of pi - print ('sin(x) = 0', NewtonRaphson('sin(x)', 2)) - - # Find root of polynomial - print ('x**2 - 5*x +2 = 0', NewtonRaphson('x**2 - 5*x +2', 0.4)) - - # Find Square Root of 5 - print ('x**2 - 5 = 0', NewtonRaphson('x**2 - 5', 0.1)) - - # Exponential Roots - print ('exp(x) - 1 = 0', NewtonRaphson('exp(x) - 1', 0)) diff --git a/arithmetic_analysis/secant_method.py b/arithmetic_analysis/secant_method.py new file mode 100644 index 000000000000..b05d44c627d8 --- /dev/null +++ b/arithmetic_analysis/secant_method.py @@ -0,0 +1,28 @@ +# Implementing Secant method in Python +# Author: dimgrichr + + +from math import exp + + +def f(x): + """ + >>> f(5) + 39.98652410600183 + """ + return 8 * x - 2 * exp(-x) + + +def SecantMethod(lower_bound, upper_bound, repeats): + """ + >>> SecantMethod(1, 3, 2) + 0.2139409276214589 + """ + x0 = lower_bound + x1 = upper_bound + for i in range(0, repeats): + x0, x1 = x1, x1 - (f(x1) * (x1 - x0)) / (f(x1) - f(x0)) + return x1 + + +print(f"The solution is: {SecantMethod(1, 3, 2)}") diff --git a/backtracking/all_combinations.py b/backtracking/all_combinations.py index 63425aeabbd1..854dc5198422 100644 --- a/backtracking/all_combinations.py +++ b/backtracking/all_combinations.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- - """ - In this problem, we want to determine all possible combinations of k - numbers out of 1 ... n. We use backtracking to solve this problem. - Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))) + In this problem, we want to determine all possible combinations of k + numbers out of 1 ... n. We use backtracking to solve this problem. + Time complexity: O(C(n,k)) which is O(n choose k) = O((n!/(k! * (n - k)!))) """ @@ -12,7 +10,7 @@ def generate_all_combinations(n: int, k: int) -> [[int]]: >>> generate_all_combinations(n=4, k=2) [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] """ - + result = [] create_all_state(1, n, k, [], result) return result @@ -34,7 +32,7 @@ def print_all_state(total_list): print(*i) -if __name__ == '__main__': +if __name__ == "__main__": n = 4 k = 2 total_list = generate_all_combinations(n, k) diff --git a/backtracking/all_permutations.py b/backtracking/all_permutations.py index 299b708fef4e..d144436033de 100644 --- a/backtracking/all_permutations.py +++ b/backtracking/all_permutations.py @@ -1,42 +1,42 @@ -''' - In this problem, we want to determine all possible permutations - of the given sequence. We use backtracking to solve this problem. +""" + In this problem, we want to determine all possible permutations + of the given sequence. We use backtracking to solve this problem. - Time complexity: O(n! * n), - where n denotes the length of the given sequence. -''' + Time complexity: O(n! * n), + where n denotes the length of the given sequence. +""" def generate_all_permutations(sequence): - create_state_space_tree(sequence, [], 0, [0 for i in range(len(sequence))]) + create_state_space_tree(sequence, [], 0, [0 for i in range(len(sequence))]) def create_state_space_tree(sequence, current_sequence, index, index_used): - ''' - Creates a state space tree to iterate through each branch using DFS. - We know that each state has exactly len(sequence) - index children. - It terminates when it reaches the end of the given sequence. - ''' + """ + Creates a state space tree to iterate through each branch using DFS. + We know that each state has exactly len(sequence) - index children. + It terminates when it reaches the end of the given sequence. + """ - if index == len(sequence): - print(current_sequence) - return + if index == len(sequence): + print(current_sequence) + return - for i in range(len(sequence)): - if not index_used[i]: - current_sequence.append(sequence[i]) - index_used[i] = True - create_state_space_tree(sequence, current_sequence, index + 1, index_used) - current_sequence.pop() - index_used[i] = False + for i in range(len(sequence)): + if not index_used[i]: + current_sequence.append(sequence[i]) + index_used[i] = True + create_state_space_tree(sequence, current_sequence, index + 1, index_used) + current_sequence.pop() + index_used[i] = False -''' -remove the comment to take an input from the user +""" +remove the comment to take an input from the user print("Enter the elements") sequence = list(map(int, input().split())) -''' +""" sequence = [3, 1, 2, 4] generate_all_permutations(sequence) diff --git a/backtracking/all_subsequences.py b/backtracking/all_subsequences.py index d868377234a8..8283386991d9 100644 --- a/backtracking/all_subsequences.py +++ b/backtracking/all_subsequences.py @@ -1,39 +1,39 @@ -''' - In this problem, we want to determine all possible subsequences - of the given sequence. We use backtracking to solve this problem. +""" + In this problem, we want to determine all possible subsequences + of the given sequence. We use backtracking to solve this problem. - Time complexity: O(2^n), - where n denotes the length of the given sequence. -''' + Time complexity: O(2^n), + where n denotes the length of the given sequence. +""" def generate_all_subsequences(sequence): - create_state_space_tree(sequence, [], 0) + create_state_space_tree(sequence, [], 0) def create_state_space_tree(sequence, current_subsequence, index): - ''' - Creates a state space tree to iterate through each branch using DFS. - We know that each state has exactly two children. - It terminates when it reaches the end of the given sequence. - ''' + """ + Creates a state space tree to iterate through each branch using DFS. + We know that each state has exactly two children. + It terminates when it reaches the end of the given sequence. + """ - if index == len(sequence): - print(current_subsequence) - return + if index == len(sequence): + print(current_subsequence) + return - create_state_space_tree(sequence, current_subsequence, index + 1) - current_subsequence.append(sequence[index]) - create_state_space_tree(sequence, current_subsequence, index + 1) - current_subsequence.pop() + create_state_space_tree(sequence, current_subsequence, index + 1) + current_subsequence.append(sequence[index]) + create_state_space_tree(sequence, current_subsequence, index + 1) + current_subsequence.pop() -''' -remove the comment to take an input from the user +""" +remove the comment to take an input from the user print("Enter the elements") sequence = list(map(int, input().split())) -''' +""" sequence = [3, 1, 2, 4] generate_all_subsequences(sequence) diff --git a/backtracking/coloring.py b/backtracking/coloring.py new file mode 100644 index 000000000000..77beb5fc1956 --- /dev/null +++ b/backtracking/coloring.py @@ -0,0 +1,114 @@ +""" + Graph Coloring also called "m coloring problem" + consists of coloring given graph with at most m colors + such that no adjacent vertices are assigned same color + + Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring +""" +from typing import List + + +def valid_coloring( + neighbours: List[int], colored_vertices: List[int], color: int +) -> bool: + """ + For each neighbour check if coloring constraint is satisfied + If any of the neighbours fail the constraint return False + If all neighbours validate constraint return True + + >>> neighbours = [0,1,0,1,0] + >>> colored_vertices = [0, 2, 1, 2, 0] + + >>> color = 1 + >>> valid_coloring(neighbours, colored_vertices, color) + True + + >>> color = 2 + >>> valid_coloring(neighbours, colored_vertices, color) + False + """ + # Does any neighbour not satisfy the constraints + return not any( + neighbour == 1 and colored_vertices[i] == color + for i, neighbour in enumerate(neighbours) + ) + + +def util_color( + graph: List[List[int]], max_colors: int, colored_vertices: List[int], index: int +) -> bool: + """ + Pseudo-Code + + Base Case: + 1. Check if coloring is complete + 1.1 If complete return True (meaning that we successfully colored graph) + + Recursive Step: + 2. Itterates over each color: + Check if current coloring is valid: + 2.1. Color given vertex + 2.2. Do recursive call check if this coloring leads to solving problem + 2.4. if current coloring leads to solution return + 2.5. Uncolor given vertex + + >>> graph = [[0, 1, 0, 0, 0], + ... [1, 0, 1, 0, 1], + ... [0, 1, 0, 1, 0], + ... [0, 1, 1, 0, 0], + ... [0, 1, 0, 0, 0]] + >>> max_colors = 3 + >>> colored_vertices = [0, 1, 0, 0, 0] + >>> index = 3 + + >>> util_color(graph, max_colors, colored_vertices, index) + True + + >>> max_colors = 2 + >>> util_color(graph, max_colors, colored_vertices, index) + False + """ + + # Base Case + if index == len(graph): + return True + + # Recursive Step + for i in range(max_colors): + if valid_coloring(graph[index], colored_vertices, i): + # Color current vertex + colored_vertices[index] = i + # Validate coloring + if util_color(graph, max_colors, colored_vertices, index + 1): + return True + # Backtrack + colored_vertices[index] = -1 + return False + + +def color(graph: List[List[int]], max_colors: int) -> List[int]: + """ + Wrapper function to call subroutine called util_color + which will either return True or False. + If True is returned colored_vertices list is filled with correct colorings + + >>> graph = [[0, 1, 0, 0, 0], + ... [1, 0, 1, 0, 1], + ... [0, 1, 0, 1, 0], + ... [0, 1, 1, 0, 0], + ... [0, 1, 0, 0, 0]] + + >>> max_colors = 3 + >>> color(graph, max_colors) + [0, 1, 0, 2, 0] + + >>> max_colors = 2 + >>> color(graph, max_colors) + [] + """ + colored_vertices = [-1] * len(graph) + + if util_color(graph, max_colors, colored_vertices, 0): + return colored_vertices + + return [] diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py new file mode 100644 index 000000000000..e4f2c62d2341 --- /dev/null +++ b/backtracking/hamiltonian_cycle.py @@ -0,0 +1,175 @@ +""" + A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle + through a graph that visits each node exactly once. + Determining whether such paths and cycles exist in graphs + is the 'Hamiltonian path problem', which is NP-complete. + + Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path +""" +from typing import List + + +def valid_connection( + graph: List[List[int]], next_ver: int, curr_ind: int, path: List[int] +) -> bool: + """ + Checks whether it is possible to add next into path by validating 2 statements + 1. There should be path between current and next vertex + 2. Next vertex should not be in path + If both validations succeeds we return true saying that it is possible to connect this vertices + either we return false + + Case 1:Use exact graph as in main function, with initialized values + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, -1, -1, -1, -1, 0] + >>> curr_ind = 1 + >>> next_ver = 1 + >>> valid_connection(graph, next_ver, curr_ind, path) + True + + Case 2: Same graph, but trying to connect to node that is already in path + >>> path = [0, 1, 2, 4, -1, 0] + >>> curr_ind = 4 + >>> next_ver = 1 + >>> valid_connection(graph, next_ver, curr_ind, path) + False + """ + + # 1. Validate that path exists between current and next vertices + if graph[path[curr_ind - 1]][next_ver] == 0: + return False + + # 2. Validate that next vertex is not already in path + return not any(vertex == next_ver for vertex in path) + + +def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) -> bool: + """ + Pseudo-Code + Base Case: + 1. Chceck if we visited all of vertices + 1.1 If last visited vertex has path to starting vertex return True either return False + Recursive Step: + 2. Iterate over each vertex + Check if next vertex is valid for transiting from current vertex + 2.1 Remember next vertex as next transition + 2.2 Do recursive call and check if going to this vertex solves problem + 2.3 if next vertex leads to solution return True + 2.4 else backtrack, delete remembered vertex + + Case 1: Use exact graph as in main function, with initialized values + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, -1, -1, -1, -1, 0] + >>> curr_ind = 1 + >>> util_hamilton_cycle(graph, path, curr_ind) + True + >>> print(path) + [0, 1, 2, 4, 3, 0] + + Case 2: Use exact graph as in previous case, but in the properties taken from middle of calculation + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> path = [0, 1, 2, -1, -1, 0] + >>> curr_ind = 3 + >>> util_hamilton_cycle(graph, path, curr_ind) + True + >>> print(path) + [0, 1, 2, 4, 3, 0] + """ + + # Base Case + if curr_ind == len(graph): + # return whether path exists between current and starting vertices + return graph[path[curr_ind - 1]][path[0]] == 1 + + # Recursive Step + for next in range(0, len(graph)): + if valid_connection(graph, next, curr_ind, path): + # Insert current vertex into path as next transition + path[curr_ind] = next + # Validate created path + if util_hamilton_cycle(graph, path, curr_ind + 1): + return True + # Backtrack + path[curr_ind] = -1 + return False + + +def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: + r""" + Wrapper function to call subroutine called util_hamilton_cycle, + which will either return array of vertices indicating hamiltonian cycle + or an empty list indicating that hamiltonian cycle was not found. + Case 1: + Following graph consists of 5 edges. + If we look closely, we can see that there are multiple Hamiltonian cycles. + For example one result is when we iterate like: + (0)->(1)->(2)->(4)->(3)->(0) + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3)---------(4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> hamilton_cycle(graph) + [0, 1, 2, 4, 3, 0] + + Case 2: + Same Graph as it was in Case 1, changed starting index from default to 3 + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3)---------(4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 1], + ... [0, 1, 1, 1, 0]] + >>> hamilton_cycle(graph, 3) + [3, 0, 1, 2, 4, 3] + + Case 3: + Following Graph is exactly what it was before, but edge 3-4 is removed. + Result is that there is no Hamiltonian Cycle anymore. + + (0)---(1)---(2) + | / \ | + | / \ | + | / \ | + |/ \| + (3) (4) + >>> graph = [[0, 1, 0, 1, 0], + ... [1, 0, 1, 1, 1], + ... [0, 1, 0, 0, 1], + ... [1, 1, 0, 0, 0], + ... [0, 1, 1, 0, 0]] + >>> hamilton_cycle(graph,4) + [] + """ + + # Initialize path with -1, indicating that we have not visited them yet + path = [-1] * (len(graph) + 1) + # initialize start and end of path with starting index + path[0] = path[-1] = start_index + # evaluate and if we find answer return path either return empty array + return path if util_hamilton_cycle(graph, path, 1) else [] diff --git a/backtracking/minimax.py b/backtracking/minimax.py index 5168306e71fc..af07b8d8171a 100644 --- a/backtracking/minimax.py +++ b/backtracking/minimax.py @@ -1,28 +1,34 @@ -import math +import math -''' Minimax helps to achieve maximum score in a game by checking all possible moves +""" Minimax helps to achieve maximum score in a game by checking all possible moves depth is current depth in game tree. nodeIndex is index of current node in scores[]. if move is of maximizer return true else false leaves of game tree is stored in scores[] height is maximum height of Game tree -''' +""" -def minimax (Depth, nodeIndex, isMax, scores, height): - if Depth == height: - return scores[nodeIndex] +def minimax(Depth, nodeIndex, isMax, scores, height): - if isMax: - return (max(minimax(Depth + 1, nodeIndex * 2, False, scores, height), - minimax(Depth + 1, nodeIndex * 2 + 1, False, scores, height))) - return (min(minimax(Depth + 1, nodeIndex * 2, True, scores, height), - minimax(Depth + 1, nodeIndex * 2 + 1, True, scores, height))) + if Depth == height: + return scores[nodeIndex] -if __name__ == "__main__": - - scores = [90, 23, 6, 33, 21, 65, 123, 34423] - height = math.log(len(scores), 2) + if isMax: + return max( + minimax(Depth + 1, nodeIndex * 2, False, scores, height), + minimax(Depth + 1, nodeIndex * 2 + 1, False, scores, height), + ) + return min( + minimax(Depth + 1, nodeIndex * 2, True, scores, height), + minimax(Depth + 1, nodeIndex * 2 + 1, True, scores, height), + ) - print("Optimal value : ", end = "") - print(minimax(0, 0, True, scores, height)) + +if __name__ == "__main__": + + scores = [90, 23, 6, 33, 21, 65, 123, 34423] + height = math.log(len(scores), 2) + + print("Optimal value : ", end="") + print(minimax(0, 0, True, scores, height)) diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py index dfd4498b166b..58d9c4279a35 100644 --- a/backtracking/n_queens.py +++ b/backtracking/n_queens.py @@ -1,4 +1,4 @@ -''' +""" The nqueens problem is of placing N queens on a N * N chess board such that no queen can attack any other queens placed @@ -6,11 +6,12 @@ This means that one queen cannot have any other queen on its horizontal, vertical and diagonal lines. -''' +""" solution = [] + def isSafe(board, row, column): - ''' + """ This function returns a boolean value True if it is safe to place a queen there considering the current state of the board. @@ -21,64 +22,67 @@ def isSafe(board, row, column): Returns : Boolean Value - ''' + """ for i in range(len(board)): if board[row][i] == 1: return False for i in range(len(board)): if board[i][column] == 1: return False - for i,j in zip(range(row,-1,-1),range(column,-1,-1)): + for i, j in zip(range(row, -1, -1), range(column, -1, -1)): if board[i][j] == 1: return False - for i,j in zip(range(row,-1,-1),range(column,len(board))): + for i, j in zip(range(row, -1, -1), range(column, len(board))): if board[i][j] == 1: return False return True + def solve(board, row): - ''' - It creates a state space tree and calls the safe function untill it receives a - False Boolean and terminates that brach and backtracks to the next - poosible solution branch. - ''' + """ + It creates a state space tree and calls the safe function until it receives a + False Boolean and terminates that branch and backtracks to the next + possible solution branch. + """ if row >= len(board): - ''' + """ If the row number exceeds N we have board with a successful combination and that combination is appended to the solution list and the board is printed. - ''' + """ solution.append(board) printboard(board) print() - return + return for i in range(len(board)): - ''' - For every row it iterates through each column to check if it is feesible to place a + """ + For every row it iterates through each column to check if it is feasible to place a queen there. - If all the combinations for that particaular branch are successfull the board is + If all the combinations for that particular branch are successful the board is reinitialized for the next possible combination. - ''' - if isSafe(board,row,i): + """ + if isSafe(board, row, i): board[row][i] = 1 - solve(board,row+1) + solve(board, row + 1) board[row][i] = 0 return False + def printboard(board): - ''' - Prints the boards that have a successfull combination. - ''' + """ + Prints the boards that have a successful combination. + """ for i in range(len(board)): for j in range(len(board)): if board[i][j] == 1: - print("Q", end = " ") - else : - print(".", end = " ") + print("Q", end=" ") + else: + print(".", end=" ") print() -#n=int(input("The no. of queens")) + +# n=int(input("The no. of queens")) n = 8 -board = [[0 for i in range(n)]for j in range(n)] +board = [[0 for i in range(n)] for j in range(n)] solve(board, 0) print("The total no. of solutions are :", len(solution)) diff --git a/backtracking/sudoku.py b/backtracking/sudoku.py new file mode 100644 index 000000000000..d864e2823a9b --- /dev/null +++ b/backtracking/sudoku.py @@ -0,0 +1,147 @@ +""" + Given a partially filled 9×9 2D array, the objective is to fill a 9×9 + square grid with digits numbered 1 to 9, so that every row, column, and + and each of the nine 3×3 sub-grids contains all of the digits. + + This can be solved using Backtracking and is similar to n-queens. + We check to see if a cell is safe or not and recursively call the + function on the next column to see if it returns True. if yes, we + have solved the puzzle. else, we backtrack and place another number + in that cell and repeat this process. +""" +# assigning initial values to the grid +initial_grid = [ + [3, 0, 6, 5, 0, 8, 4, 0, 0], + [5, 2, 0, 0, 0, 0, 0, 0, 0], + [0, 8, 7, 0, 0, 0, 0, 3, 1], + [0, 0, 3, 0, 1, 0, 0, 8, 0], + [9, 0, 0, 8, 6, 3, 0, 0, 5], + [0, 5, 0, 0, 9, 0, 6, 0, 0], + [1, 3, 0, 0, 0, 0, 2, 5, 0], + [0, 0, 0, 0, 0, 0, 0, 7, 4], + [0, 0, 5, 2, 0, 6, 3, 0, 0], +] + +# a grid with no solution +no_solution = [ + [5, 0, 6, 5, 0, 8, 4, 0, 3], + [5, 2, 0, 0, 0, 0, 0, 0, 2], + [1, 8, 7, 0, 0, 0, 0, 3, 1], + [0, 0, 3, 0, 1, 0, 0, 8, 0], + [9, 0, 0, 8, 6, 3, 0, 0, 5], + [0, 5, 0, 0, 9, 0, 6, 0, 0], + [1, 3, 0, 0, 0, 0, 2, 5, 0], + [0, 0, 0, 0, 0, 0, 0, 7, 4], + [0, 0, 5, 2, 0, 6, 3, 0, 0], +] + + +def is_safe(grid, row, column, n): + """ + This function checks the grid to see if each row, + column, and the 3x3 subgrids contain the digit 'n'. + It returns False if it is not 'safe' (a duplicate digit + is found) else returns True if it is 'safe' + """ + for i in range(9): + if grid[row][i] == n or grid[i][column] == n: + return False + + for i in range(3): + for j in range(3): + if grid[(row - row % 3) + i][(column - column % 3) + j] == n: + return False + + return True + + +def is_completed(grid): + """ + This function checks if the puzzle is completed or not. + it is completed when all the cells are assigned with a non-zero number. + + >>> is_completed([[0]]) + False + >>> is_completed([[1]]) + True + >>> is_completed([[1, 2], [0, 4]]) + False + >>> is_completed([[1, 2], [3, 4]]) + True + >>> is_completed(initial_grid) + False + >>> is_completed(no_solution) + False + """ + return all(all(cell != 0 for cell in row) for row in grid) + + +def find_empty_location(grid): + """ + This function finds an empty location so that we can assign a number + for that particular row and column. + """ + for i in range(9): + for j in range(9): + if grid[i][j] == 0: + return i, j + + +def sudoku(grid): + """ + Takes a partially filled-in grid and attempts to assign values to + all unassigned locations in such a way to meet the requirements + for Sudoku solution (non-duplication across rows, columns, and boxes) + + >>> sudoku(initial_grid) # doctest: +NORMALIZE_WHITESPACE + [[3, 1, 6, 5, 7, 8, 4, 9, 2], + [5, 2, 9, 1, 3, 4, 7, 6, 8], + [4, 8, 7, 6, 2, 9, 5, 3, 1], + [2, 6, 3, 4, 1, 5, 9, 8, 7], + [9, 7, 4, 8, 6, 3, 1, 2, 5], + [8, 5, 1, 7, 9, 2, 6, 4, 3], + [1, 3, 8, 9, 4, 7, 2, 5, 6], + [6, 9, 2, 3, 5, 1, 8, 7, 4], + [7, 4, 5, 2, 8, 6, 3, 1, 9]] + >>> sudoku(no_solution) + False + """ + + if is_completed(grid): + return grid + + row, column = find_empty_location(grid) + + for digit in range(1, 10): + if is_safe(grid, row, column, digit): + grid[row][column] = digit + + if sudoku(grid): + return grid + + grid[row][column] = 0 + + return False + + +def print_solution(grid): + """ + A function to print the solution in the form + of a 9x9 grid + """ + for row in grid: + for cell in row: + print(cell, end=" ") + print() + + +if __name__ == "__main__": + # make a copy of grid so that you can compare with the unmodified grid + for grid in (initial_grid, no_solution): + grid = list(map(list, grid)) + solution = sudoku(grid) + if solution: + print("grid after solving:") + print_solution(solution) + else: + print("Cannot find a solution.") diff --git a/backtracking/sum_of_subsets.py b/backtracking/sum_of_subsets.py index b01bffbb651d..e765a1b69714 100644 --- a/backtracking/sum_of_subsets.py +++ b/backtracking/sum_of_subsets.py @@ -1,45 +1,55 @@ -''' - The sum-of-subsetsproblem states that a set of non-negative integers, and a value M, - determine all possible subsets of the given set whose summation sum equal to given M. +""" + The sum-of-subsetsproblem states that a set of non-negative integers, and a value M, + determine all possible subsets of the given set whose summation sum equal to given M. + + Summation of the chosen numbers must be equal to given number M and one number can + be used only once. +""" - Summation of the chosen numbers must be equal to given number M and one number can - be used only once. -''' def generate_sum_of_subsets_soln(nums, max_sum): - result = [] - path = [] - num_index = 0 - remaining_nums_sum = sum(nums) - create_state_space_tree(nums, max_sum, num_index, path,result, remaining_nums_sum) - return result - -def create_state_space_tree(nums,max_sum,num_index,path,result, remaining_nums_sum): - ''' - Creates a state space tree to iterate through each branch using DFS. - It terminates the branching of a node when any of the two conditions - given below satisfy. - This algorithm follows depth-fist-search and backtracks when the node is not branchable. - - ''' - if sum(path) > max_sum or (remaining_nums_sum + sum(path)) < max_sum: - return - if sum(path) == max_sum: - result.append(path) - return - for num_index in range(num_index,len(nums)): - create_state_space_tree(nums, max_sum, num_index + 1, path + [nums[num_index]], result, remaining_nums_sum - nums[num_index]) - -''' -remove the comment to take an input from the user + result = [] + path = [] + num_index = 0 + remaining_nums_sum = sum(nums) + create_state_space_tree(nums, max_sum, num_index, path, result, remaining_nums_sum) + return result + + +def create_state_space_tree(nums, max_sum, num_index, path, result, remaining_nums_sum): + """ + Creates a state space tree to iterate through each branch using DFS. + It terminates the branching of a node when any of the two conditions + given below satisfy. + This algorithm follows depth-fist-search and backtracks when the node is not branchable. + + """ + if sum(path) > max_sum or (remaining_nums_sum + sum(path)) < max_sum: + return + if sum(path) == max_sum: + result.append(path) + return + for num_index in range(num_index, len(nums)): + create_state_space_tree( + nums, + max_sum, + num_index + 1, + path + [nums[num_index]], + result, + remaining_nums_sum - nums[num_index], + ) + + +""" +remove the comment to take an input from the user print("Enter the elements") nums = list(map(int, input().split())) print("Enter max_sum sum") max_sum = int(input()) -''' +""" nums = [3, 34, 4, 12, 5, 2] max_sum = 9 -result = generate_sum_of_subsets_soln(nums,max_sum) -print(*result) \ No newline at end of file +result = generate_sum_of_subsets_soln(nums, max_sum) +print(*result) diff --git a/bit_manipulation/README.md b/bit_manipulation/README.md new file mode 100644 index 000000000000..2ef1661524f2 --- /dev/null +++ b/bit_manipulation/README.md @@ -0,0 +1,7 @@ +https://docs.python.org/3/reference/expressions.html#binary-bitwise-operations +https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations +https://docs.python.org/3/library/stdtypes.html#bitwise-operations-on-integer-types + +https://wiki.python.org/moin/BitManipulation +https://wiki.python.org/moin/BitwiseOperators +https://www.tutorialspoint.com/python3/bitwise_operators_example.htm diff --git a/blockchain/chinese_remainder_theorem.py b/blockchain/chinese_remainder_theorem.py new file mode 100644 index 000000000000..8c3eb9b4b01e --- /dev/null +++ b/blockchain/chinese_remainder_theorem.py @@ -0,0 +1,91 @@ +# Chinese Remainder Theorem: +# GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) + +# If GCD(a,b) = 1, then for any remainder ra modulo a and any remainder rb modulo b there exists integer n, +# such that n = ra (mod a) and n = ra(mod b). If n1 and n2 are two such integers, then n1=n2(mod ab) + +# Algorithm : + +# 1. Use extended euclid algorithm to find x,y such that a*x + b*y = 1 +# 2. Take n = ra*by + rb*ax + + +# Extended Euclid +def extended_euclid(a, b): + """ + >>> extended_euclid(10, 6) + (-1, 2) + + >>> extended_euclid(7, 5) + (-2, 3) + + """ + if b == 0: + return (1, 0) + (x, y) = extended_euclid(b, a % b) + k = a // b + return (y, x - k * y) + + +# Uses ExtendedEuclid to find inverses +def chinese_remainder_theorem(n1, r1, n2, r2): + """ + >>> chinese_remainder_theorem(5,1,7,3) + 31 + + Explanation : 31 is the smallest number such that + (i) When we divide it by 5, we get remainder 1 + (ii) When we divide it by 7, we get remainder 3 + + >>> chinese_remainder_theorem(6,1,4,3) + 14 + + """ + (x, y) = extended_euclid(n1, n2) + m = n1 * n2 + n = r2 * x * n1 + r1 * y * n2 + return (n % m + m) % m + + +# ----------SAME SOLUTION USING InvertModulo instead ExtendedEuclid---------------- + +# This function find the inverses of a i.e., a^(-1) +def invert_modulo(a, n): + """ + >>> invert_modulo(2, 5) + 3 + + >>> invert_modulo(8,7) + 1 + + """ + (b, x) = extended_euclid(a, n) + if b < 0: + b = (b % n + n) % n + return b + + +# Same a above using InvertingModulo +def chinese_remainder_theorem2(n1, r1, n2, r2): + """ + >>> chinese_remainder_theorem2(5,1,7,3) + 31 + + >>> chinese_remainder_theorem2(6,1,4,3) + 14 + + """ + x, y = invert_modulo(n1, n2), invert_modulo(n2, n1) + m = n1 * n2 + n = r2 * x * n1 + r1 * y * n2 + return (n % m + m) % m + + +# import testmod for testing our function +from doctest import testmod + +if __name__ == "__main__": + testmod(name="chinese_remainder_theorem", verbose=True) + testmod(name="chinese_remainder_theorem2", verbose=True) + testmod(name="invert_modulo", verbose=True) + testmod(name="extended_euclid", verbose=True) diff --git a/blockchain/diophantine_equation.py b/blockchain/diophantine_equation.py new file mode 100644 index 000000000000..ec2ed26e40ec --- /dev/null +++ b/blockchain/diophantine_equation.py @@ -0,0 +1,128 @@ +# Diophantine Equation : Given integers a,b,c ( at least one of a and b != 0), the diophantine equation +# a*x + b*y = c has a solution (where x and y are integers) iff gcd(a,b) divides c. + +# GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) + + +def diophantine(a, b, c): + """ + >>> diophantine(10,6,14) + (-7.0, 14.0) + + >>> diophantine(391,299,-69) + (9.0, -12.0) + + But above equation has one more solution i.e., x = -4, y = 5. + That's why we need diophantine all solution function. + + """ + + assert ( + c % greatest_common_divisor(a, b) == 0 + ) # greatest_common_divisor(a,b) function implemented below + (d, x, y) = extended_gcd(a, b) # extended_gcd(a,b) function implemented below + r = c / d + return (r * x, r * y) + + +# Lemma : if n|ab and gcd(a,n) = 1, then n|b. + +# Finding All solutions of Diophantine Equations: + +# Theorem : Let gcd(a,b) = d, a = d*p, b = d*q. If (x0,y0) is a solution of Diophantine Equation a*x + b*y = c. +# a*x0 + b*y0 = c, then all the solutions have the form a(x0 + t*q) + b(y0 - t*p) = c, where t is an arbitrary integer. + +# n is the number of solution you want, n = 2 by default + + +def diophantine_all_soln(a, b, c, n=2): + """ + >>> diophantine_all_soln(10, 6, 14) + -7.0 14.0 + -4.0 9.0 + + >>> diophantine_all_soln(10, 6, 14, 4) + -7.0 14.0 + -4.0 9.0 + -1.0 4.0 + 2.0 -1.0 + + >>> diophantine_all_soln(391, 299, -69, n = 4) + 9.0 -12.0 + 22.0 -29.0 + 35.0 -46.0 + 48.0 -63.0 + + """ + (x0, y0) = diophantine(a, b, c) # Initial value + d = greatest_common_divisor(a, b) + p = a // d + q = b // d + + for i in range(n): + x = x0 + i * q + y = y0 - i * p + print(x, y) + + +# Euclid's Lemma : d divides a and b, if and only if d divides a-b and b + +# Euclid's Algorithm + + +def greatest_common_divisor(a, b): + """ + >>> greatest_common_divisor(7,5) + 1 + + Note : In number theory, two integers a and b are said to be relatively prime, mutually prime, or co-prime + if the only positive integer (factor) that divides both of them is 1 i.e., gcd(a,b) = 1. + + >>> greatest_common_divisor(121, 11) + 11 + + """ + if a < b: + a, b = b, a + + while a % b != 0: + a, b = b, a % b + + return b + + +# Extended Euclid's Algorithm : If d divides a and b and d = a*x + b*y for integers x and y, then d = gcd(a,b) + + +def extended_gcd(a, b): + """ + >>> extended_gcd(10, 6) + (2, -1, 2) + + >>> extended_gcd(7, 5) + (1, -2, 3) + + """ + assert a >= 0 and b >= 0 + + if b == 0: + d, x, y = a, 1, 0 + else: + (d, p, q) = extended_gcd(b, a % b) + x = q + y = p - q * (a // b) + + assert a % d == 0 and b % d == 0 + assert d == a * x + b * y + + return (d, x, y) + + +# import testmod for testing our function +from doctest import testmod + +if __name__ == "__main__": + testmod(name="diophantine", verbose=True) + testmod(name="diophantine_all_soln", verbose=True) + testmod(name="extended_gcd", verbose=True) + testmod(name="greatest_common_divisor", verbose=True) diff --git a/blockchain/modular_division.py b/blockchain/modular_division.py new file mode 100644 index 000000000000..1255f04328d5 --- /dev/null +++ b/blockchain/modular_division.py @@ -0,0 +1,151 @@ +# Modular Division : +# An efficient algorithm for dividing b by a modulo n. + +# GCD ( Greatest Common Divisor ) or HCF ( Highest Common Factor ) + +# Given three integers a, b, and n, such that gcd(a,n)=1 and n>1, the algorithm should return an integer x such that +# 0≤x≤n−1, and b/a=x(modn) (that is, b=ax(modn)). + +# Theorem: +# a has a multiplicative inverse modulo n iff gcd(a,n) = 1 + + +# This find x = b*a^(-1) mod n +# Uses ExtendedEuclid to find the inverse of a + + +def modular_division(a, b, n): + """ + >>> modular_division(4,8,5) + 2 + + >>> modular_division(3,8,5) + 1 + + >>> modular_division(4, 11, 5) + 4 + + """ + assert n > 1 and a > 0 and greatest_common_divisor(a, n) == 1 + (d, t, s) = extended_gcd(n, a) # Implemented below + x = (b * s) % n + return x + + +# This function find the inverses of a i.e., a^(-1) +def invert_modulo(a, n): + """ + >>> invert_modulo(2, 5) + 3 + + >>> invert_modulo(8,7) + 1 + + """ + (b, x) = extended_euclid(a, n) # Implemented below + if b < 0: + b = (b % n + n) % n + return b + + +# ------------------ Finding Modular division using invert_modulo ------------------- + +# This function used the above inversion of a to find x = (b*a^(-1))mod n +def modular_division2(a, b, n): + """ + >>> modular_division2(4,8,5) + 2 + + >>> modular_division2(3,8,5) + 1 + + >>> modular_division2(4, 11, 5) + 4 + + """ + s = invert_modulo(a, n) + x = (b * s) % n + return x + + +# Extended Euclid's Algorithm : If d divides a and b and d = a*x + b*y for integers x and y, then d = gcd(a,b) + + +def extended_gcd(a, b): + """ + >>> extended_gcd(10, 6) + (2, -1, 2) + + >>> extended_gcd(7, 5) + (1, -2, 3) + + ** extended_gcd function is used when d = gcd(a,b) is required in output + + """ + assert a >= 0 and b >= 0 + + if b == 0: + d, x, y = a, 1, 0 + else: + (d, p, q) = extended_gcd(b, a % b) + x = q + y = p - q * (a // b) + + assert a % d == 0 and b % d == 0 + assert d == a * x + b * y + + return (d, x, y) + + +# Extended Euclid +def extended_euclid(a, b): + """ + >>> extended_euclid(10, 6) + (-1, 2) + + >>> extended_euclid(7, 5) + (-2, 3) + + """ + if b == 0: + return (1, 0) + (x, y) = extended_euclid(b, a % b) + k = a // b + return (y, x - k * y) + + +# Euclid's Lemma : d divides a and b, if and only if d divides a-b and b +# Euclid's Algorithm + + +def greatest_common_divisor(a, b): + """ + >>> greatest_common_divisor(7,5) + 1 + + Note : In number theory, two integers a and b are said to be relatively prime, mutually prime, or co-prime + if the only positive integer (factor) that divides both of them is 1 i.e., gcd(a,b) = 1. + + >>> greatest_common_divisor(121, 11) + 11 + + """ + if a < b: + a, b = b, a + + while a % b != 0: + a, b = b, a % b + + return b + + +# Import testmod for testing our function +from doctest import testmod + +if __name__ == "__main__": + testmod(name="modular_division", verbose=True) + testmod(name="modular_division2", verbose=True) + testmod(name="invert_modulo", verbose=True) + testmod(name="extended_gcd", verbose=True) + testmod(name="extended_euclid", verbose=True) + testmod(name="greatest_common_divisor", verbose=True) diff --git a/boolean_algebra/quine_mc_cluskey.py b/boolean_algebra/quine_mc_cluskey.py index db4d153cbfd7..036cfbe63e79 100644 --- a/boolean_algebra/quine_mc_cluskey.py +++ b/boolean_algebra/quine_mc_cluskey.py @@ -1,116 +1,168 @@ def compare_string(string1, string2): - l1 = list(string1); l2 = list(string2) - count = 0 - for i in range(len(l1)): - if l1[i] != l2[i]: - count += 1 - l1[i] = '_' - if count > 1: - return -1 - else: - return("".join(l1)) + """ + >>> compare_string('0010','0110') + '0_10' + + >>> compare_string('0110','1101') + -1 + """ + l1 = list(string1) + l2 = list(string2) + count = 0 + for i in range(len(l1)): + if l1[i] != l2[i]: + count += 1 + l1[i] = "_" + if count > 1: + return -1 + else: + return "".join(l1) + def check(binary): - pi = [] - while 1: - check1 = ['$']*len(binary) - temp = [] - for i in range(len(binary)): - for j in range(i+1, len(binary)): - k=compare_string(binary[i], binary[j]) - if k != -1: - check1[i] = '*' - check1[j] = '*' - temp.append(k) - for i in range(len(binary)): - if check1[i] == '$': - pi.append(binary[i]) - if len(temp) == 0: - return pi - binary = list(set(temp)) + """ + >>> check(['0.00.01.5']) + ['0.00.01.5'] + """ + pi = [] + while 1: + check1 = ["$"] * len(binary) + temp = [] + for i in range(len(binary)): + for j in range(i + 1, len(binary)): + k = compare_string(binary[i], binary[j]) + if k != -1: + check1[i] = "*" + check1[j] = "*" + temp.append(k) + for i in range(len(binary)): + if check1[i] == "$": + pi.append(binary[i]) + if len(temp) == 0: + return pi + binary = list(set(temp)) + def decimal_to_binary(no_of_variable, minterms): - temp = [] - s = '' - for m in minterms: - for i in range(no_of_variable): - s = str(m%2) + s - m //= 2 - temp.append(s) - s = '' - return temp + """ + >>> decimal_to_binary(3,[1.5]) + ['0.00.01.5'] + """ + temp = [] + s = "" + for m in minterms: + for i in range(no_of_variable): + s = str(m % 2) + s + m //= 2 + temp.append(s) + s = "" + return temp + def is_for_table(string1, string2, count): - l1 = list(string1);l2=list(string2) - count_n = 0 - for i in range(len(l1)): - if l1[i] != l2[i]: - count_n += 1 - if count_n == count: - return True - else: - return False + """ + >>> is_for_table('__1','011',2) + True + + >>> is_for_table('01_','001',1) + False + """ + l1 = list(string1) + l2 = list(string2) + count_n = 0 + for i in range(len(l1)): + if l1[i] != l2[i]: + count_n += 1 + if count_n == count: + return True + else: + return False + def selection(chart, prime_implicants): - temp = [] - select = [0]*len(chart) - for i in range(len(chart[0])): - count = 0 - rem = -1 - for j in range(len(chart)): - if chart[j][i] == 1: - count += 1 - rem = j - if count == 1: - select[rem] = 1 - for i in range(len(select)): - if select[i] == 1: - for j in range(len(chart[0])): - if chart[i][j] == 1: - for k in range(len(chart)): - chart[k][j] = 0 - temp.append(prime_implicants[i]) - while 1: - max_n = 0; rem = -1; count_n = 0 - for i in range(len(chart)): - count_n = chart[i].count(1) - if count_n > max_n: - max_n = count_n - rem = i - - if max_n == 0: - return temp - - temp.append(prime_implicants[rem]) - - for i in range(len(chart[0])): - if chart[rem][i] == 1: - for j in range(len(chart)): - chart[j][i] = 0 - + """ + >>> selection([[1]],['0.00.01.5']) + ['0.00.01.5'] + + >>> selection([[1]],['0.00.01.5']) + ['0.00.01.5'] + """ + temp = [] + select = [0] * len(chart) + for i in range(len(chart[0])): + count = 0 + rem = -1 + for j in range(len(chart)): + if chart[j][i] == 1: + count += 1 + rem = j + if count == 1: + select[rem] = 1 + for i in range(len(select)): + if select[i] == 1: + for j in range(len(chart[0])): + if chart[i][j] == 1: + for k in range(len(chart)): + chart[k][j] = 0 + temp.append(prime_implicants[i]) + while 1: + max_n = 0 + rem = -1 + count_n = 0 + for i in range(len(chart)): + count_n = chart[i].count(1) + if count_n > max_n: + max_n = count_n + rem = i + + if max_n == 0: + return temp + + temp.append(prime_implicants[rem]) + + for i in range(len(chart[0])): + if chart[rem][i] == 1: + for j in range(len(chart)): + chart[j][i] = 0 + + def prime_implicant_chart(prime_implicants, binary): - chart = [[0 for x in range(len(binary))] for x in range(len(prime_implicants))] - for i in range(len(prime_implicants)): - count = prime_implicants[i].count('_') - for j in range(len(binary)): - if(is_for_table(prime_implicants[i], binary[j], count)): - chart[i][j] = 1 - - return chart + """ + >>> prime_implicant_chart(['0.00.01.5'],['0.00.01.5']) + [[1]] + """ + chart = [[0 for x in range(len(binary))] for x in range(len(prime_implicants))] + for i in range(len(prime_implicants)): + count = prime_implicants[i].count("_") + for j in range(len(binary)): + if is_for_table(prime_implicants[i], binary[j], count): + chart[i][j] = 1 + + return chart + def main(): - no_of_variable = int(input("Enter the no. of variables\n")) - minterms = [int(x) for x in input("Enter the decimal representation of Minterms 'Spaces Seprated'\n").split()] - binary = decimal_to_binary(no_of_variable, minterms) - - prime_implicants = check(binary) - print("Prime Implicants are:") - print(prime_implicants) - chart = prime_implicant_chart(prime_implicants, binary) - - essential_prime_implicants = selection(chart,prime_implicants) - print("Essential Prime Implicants are:") - print(essential_prime_implicants) - -if __name__ == '__main__': - main() + no_of_variable = int(input("Enter the no. of variables\n")) + minterms = [ + int(x) + for x in input( + "Enter the decimal representation of Minterms 'Spaces Seprated'\n" + ).split() + ] + binary = decimal_to_binary(no_of_variable, minterms) + + prime_implicants = check(binary) + print("Prime Implicants are:") + print(prime_implicants) + chart = prime_implicant_chart(prime_implicants, binary) + + essential_prime_implicants = selection(chart, prime_implicants) + print("Essential Prime Implicants are:") + print(essential_prime_implicants) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/cellular_automata/README.md b/cellular_automata/README.md new file mode 100644 index 000000000000..c3fa0516f5dd --- /dev/null +++ b/cellular_automata/README.md @@ -0,0 +1,4 @@ +# Cellular Automata + +* https://en.wikipedia.org/wiki/Cellular_automaton +* https://mathworld.wolfram.com/ElementaryCellularAutomaton.html diff --git a/cellular_automata/one_dimensional.py b/cellular_automata/one_dimensional.py new file mode 100644 index 000000000000..7819088c8cff --- /dev/null +++ b/cellular_automata/one_dimensional.py @@ -0,0 +1,73 @@ +""" +Return an image of 16 generations of one-dimensional cellular automata based on a given +ruleset number +https://mathworld.wolfram.com/ElementaryCellularAutomaton.html +""" + +from typing import List + +from PIL import Image + +# Define the first generation of cells +# fmt: off +CELLS = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] +# fmt: on + + +def format_ruleset(ruleset: int) -> List[int]: + """ + >>> format_ruleset(11100) + [0, 0, 0, 1, 1, 1, 0, 0] + >>> format_ruleset(0) + [0, 0, 0, 0, 0, 0, 0, 0] + >>> format_ruleset(11111111) + [1, 1, 1, 1, 1, 1, 1, 1] + """ + return [int(c) for c in f"{ruleset:08}"[:8]] + + +def new_generation(cells: List[List[int]], rule: List[int], time: int) -> List[int]: + population = len(cells[0]) # 31 + next_generation = [] + for i in range(population): + # Get the neighbors of each cell + left_neighbor = 0 if i == 0 else cells[time][i - 1] # special: leftmost cell + right_neighbor = 0 if i == population - 1 else cells[time][i + 1] # rightmost + # Define a new cell and add it to the new generation + situation = 7 - int(f"{left_neighbor}{cells[time][i]}{right_neighbor}", 2) + next_generation.append(rule[situation]) + return next_generation + + +def generate_image(cells: List[List[int]]) -> Image.Image: + """ + Convert the cells into a greyscale PIL.Image.Image and return it to the caller. + >>> from random import random + >>> cells = [[random() for w in range(31)] for h in range(16)] + >>> img = generate_image(cells) + >>> isinstance(img, Image.Image) + True + >>> img.width, img.height + (31, 16) + """ + # Create the output image + img = Image.new("RGB", (len(cells[0]), len(cells))) + pixels = img.load() + # Generates image + for w in range(img.width): + for h in range(img.height): + color = 255 - int(255 * cells[h][w]) + pixels[w, h] = (color, color, color) + return img + + +if __name__ == "__main__": + rule_num = bin(int(input("Rule:\n").strip()))[2:] + rule = format_ruleset(int(rule_num)) + for time in range(16): + CELLS.append(new_generation(CELLS, rule, time)) + img = generate_image(CELLS) + # Uncomment to save the image + # img.save(f"rule_{rule_num}.png") + img.show() diff --git a/ciphers/a1z26.py b/ciphers/a1z26.py new file mode 100644 index 000000000000..92710ec44b0e --- /dev/null +++ b/ciphers/a1z26.py @@ -0,0 +1,33 @@ +""" +Convert a string of characters to a sequence of numbers +corresponding to the character's position in the alphabet. + +https://www.dcode.fr/letter-number-cipher +http://bestcodes.weebly.com/a1z26.html +""" + + +def encode(plain: str) -> list: + """ + >>> encode("myname") + [13, 25, 14, 1, 13, 5] + """ + return [ord(elem) - 96 for elem in plain] + + +def decode(encoded: list) -> str: + """ + >>> decode([13, 25, 14, 1, 13, 5]) + 'myname' + """ + return "".join(chr(elem + 96) for elem in encoded) + + +def main(): + encoded = encode(input("->").strip().lower()) + print("Encoded: ", encoded) + print("Decoded:", decode(encoded)) + + +if __name__ == "__main__": + main() diff --git a/ciphers/affine_cipher.py b/ciphers/affine_cipher.py index af5f4e0ff4c6..21c92c6437e7 100644 --- a/ciphers/affine_cipher.py +++ b/ciphers/affine_cipher.py @@ -1,44 +1,66 @@ -from __future__ import print_function -import sys, random, cryptomath_module as cryptoMath +import random +import sys + +import cryptomath_module as cryptomath + +SYMBOLS = ( + r""" !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`""" + r"""abcdefghijklmnopqrstuvwxyz{|}~""" +) -SYMBOLS = r""" !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~""" def main(): - message = input('Enter message: ') - key = int(input('Enter key [2000 - 9000]: ')) - mode = input('Encrypt/Decrypt [E/D]: ') - - if mode.lower().startswith('e'): - mode = 'encrypt' - translated = encryptMessage(key, message) - elif mode.lower().startswith('d'): - mode = 'decrypt' - translated = decryptMessage(key, message) - print('\n%sed text: \n%s' % (mode.title(), translated)) - -def getKeyParts(key): - keyA = key // len(SYMBOLS) - keyB = key % len(SYMBOLS) - return (keyA, keyB) - -def checkKeys(keyA, keyB, mode): - if keyA == 1 and mode == 'encrypt': - sys.exit('The affine cipher becomes weak when key A is set to 1. Choose different key') - if keyB == 0 and mode == 'encrypt': - sys.exit('The affine cipher becomes weak when key A is set to 1. Choose different key') + """ + >>> key = get_random_key() + >>> msg = "This is a test!" + >>> decrypt_message(key, encrypt_message(key, msg)) == msg + True + """ + message = input("Enter message: ").strip() + key = int(input("Enter key [2000 - 9000]: ").strip()) + mode = input("Encrypt/Decrypt [E/D]: ").strip().lower() + + if mode.startswith("e"): + mode = "encrypt" + translated = encrypt_message(key, message) + elif mode.startswith("d"): + mode = "decrypt" + translated = decrypt_message(key, message) + print(f"\n{mode.title()}ed text: \n{translated}") + + +def check_keys(keyA, keyB, mode): + if mode == "encrypt": + if keyA == 1: + sys.exit( + "The affine cipher becomes weak when key " + "A is set to 1. Choose different key" + ) + if keyB == 0: + sys.exit( + "The affine cipher becomes weak when key " + "B is set to 0. Choose different key" + ) if keyA < 0 or keyB < 0 or keyB > len(SYMBOLS) - 1: - sys.exit('Key A must be greater than 0 and key B must be between 0 and %s.' % (len(SYMBOLS) - 1)) - if cryptoMath.gcd(keyA, len(SYMBOLS)) != 1: - sys.exit('Key A %s and the symbol set size %s are not relatively prime. Choose a different key.' % (keyA, len(SYMBOLS))) + sys.exit( + "Key A must be greater than 0 and key B must " + f"be between 0 and {len(SYMBOLS) - 1}." + ) + if cryptomath.gcd(keyA, len(SYMBOLS)) != 1: + sys.exit( + f"Key A {keyA} and the symbol set size {len(SYMBOLS)} " + "are not relatively prime. Choose a different key." + ) -def encryptMessage(key, message): - ''' - >>> encryptMessage(4545, 'The affine cipher is a type of monoalphabetic substitution cipher.') + +def encrypt_message(key: int, message: str) -> str: + """ + >>> encrypt_message(4545, 'The affine cipher is a type of monoalphabetic substitution cipher.') 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF{xIp~{HL}Gi' - ''' - keyA, keyB = getKeyParts(key) - checkKeys(keyA, keyB, 'encrypt') - cipherText = '' + """ + keyA, keyB = divmod(key, len(SYMBOLS)) + check_keys(keyA, keyB, "encrypt") + cipherText = "" for symbol in message: if symbol in SYMBOLS: symIndex = SYMBOLS.find(symbol) @@ -47,15 +69,16 @@ def encryptMessage(key, message): cipherText += symbol return cipherText -def decryptMessage(key, message): - ''' - >>> decryptMessage(4545, 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF{xIp~{HL}Gi') + +def decrypt_message(key: int, message: str) -> str: + """ + >>> decrypt_message(4545, 'VL}p MM{I}p~{HL}Gp{vp pFsH}pxMpyxIx JHL O}F{~pvuOvF{FuF{xIp~{HL}Gi') 'The affine cipher is a type of monoalphabetic substitution cipher.' - ''' - keyA, keyB = getKeyParts(key) - checkKeys(keyA, keyB, 'decrypt') - plainText = '' - modInverseOfkeyA = cryptoMath.findModInverse(keyA, len(SYMBOLS)) + """ + keyA, keyB = divmod(key, len(SYMBOLS)) + check_keys(keyA, keyB, "decrypt") + plainText = "" + modInverseOfkeyA = cryptomath.findModInverse(keyA, len(SYMBOLS)) for symbol in message: if symbol in SYMBOLS: symIndex = SYMBOLS.find(symbol) @@ -64,14 +87,17 @@ def decryptMessage(key, message): plainText += symbol return plainText -def getRandomKey(): + +def get_random_key(): while True: keyA = random.randint(2, len(SYMBOLS)) keyB = random.randint(2, len(SYMBOLS)) - if cryptoMath.gcd(keyA, len(SYMBOLS)) == 1: + if cryptomath.gcd(keyA, len(SYMBOLS)) == 1: return keyA * len(SYMBOLS) + keyB -if __name__ == '__main__': + +if __name__ == "__main__": import doctest + doctest.testmod() main() diff --git a/ciphers/atbash.py b/ciphers/atbash.py index 5653f0213745..4cf003859856 100644 --- a/ciphers/atbash.py +++ b/ciphers/atbash.py @@ -1,23 +1,15 @@ -try: # Python 2 - raw_input - unichr -except NameError: # Python 3 - raw_input = input - unichr = chr - - -def Atbash(): - output="" - for i in raw_input("Enter the sentence to be encrypted ").strip(): +def atbash(): + output = "" + for i in input("Enter the sentence to be encrypted ").strip(): extract = ord(i) if 65 <= extract <= 90: - output += unichr(155-extract) + output += chr(155 - extract) elif 97 <= extract <= 122: - output += unichr(219-extract) + output += chr(219 - extract) else: - output+=i + output += i print(output) -if __name__ == '__main__': - Atbash() +if __name__ == "__main__": + atbash() diff --git a/ciphers/base16.py b/ciphers/base16.py index 9bc0e5d8337a..0210315d54e6 100644 --- a/ciphers/base16.py +++ b/ciphers/base16.py @@ -1,11 +1,13 @@ import base64 + def main(): - inp = input('->') - encoded = inp.encode('utf-8') #encoded the input (we need a bytes like object) - b16encoded = base64.b16encode(encoded) #b16encoded the encoded string + inp = input("->") + encoded = inp.encode("utf-8") # encoded the input (we need a bytes like object) + b16encoded = base64.b16encode(encoded) # b16encoded the encoded string print(b16encoded) - print(base64.b16decode(b16encoded).decode('utf-8'))#decoded it + print(base64.b16decode(b16encoded).decode("utf-8")) # decoded it + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/base32.py b/ciphers/base32.py index 2ac29f441e94..5bba8c4dd685 100644 --- a/ciphers/base32.py +++ b/ciphers/base32.py @@ -1,11 +1,13 @@ import base64 + def main(): - inp = input('->') - encoded = inp.encode('utf-8') #encoded the input (we need a bytes like object) - b32encoded = base64.b32encode(encoded) #b32encoded the encoded string + inp = input("->") + encoded = inp.encode("utf-8") # encoded the input (we need a bytes like object) + b32encoded = base64.b32encode(encoded) # b32encoded the encoded string print(b32encoded) - print(base64.b32decode(b32encoded).decode('utf-8'))#decoded it + print(base64.b32decode(b32encoded).decode("utf-8")) # decoded it + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/base64_cipher.py b/ciphers/base64_cipher.py index fa3451c0cbae..f95403c7b426 100644 --- a/ciphers/base64_cipher.py +++ b/ciphers/base64_cipher.py @@ -1,64 +1,88 @@ -def encodeBase64(text): - base64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - - r = "" #the result - c = 3 - len(text) % 3 #the length of padding - p = "=" * c #the padding - s = text + "\0" * c #the text to encode - - i = 0 +def encode_base64(text): + r""" + >>> encode_base64('WELCOME to base64 encoding 😁') + 'V0VMQ09NRSB0byBiYXNlNjQgZW5jb2Rpbmcg8J+YgQ==' + >>> encode_base64('AÅᐃ𐀏🤓') + 'QcOF4ZCD8JCAj/CfpJM=' + >>> encode_base64('A'*60) + 'QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\nQUFB' + """ + base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + byte_text = bytes(text, "utf-8") # put text in bytes for unicode support + r = "" # the result + c = -len(byte_text) % 3 # the length of padding + p = "=" * c # the padding + s = byte_text + b"\x00" * c # the text to encode + + i = 0 while i < len(s): if i > 0 and ((i / 3 * 4) % 76) == 0: - r = r + "\r\n" - - n = (ord(s[i]) << 16) + (ord(s[i+1]) << 8 ) + ord(s[i+2]) - + r = r + "\r\n" # for unix newline, put "\n" + + n = (s[i] << 16) + (s[i + 1] << 8) + s[i + 2] + n1 = (n >> 18) & 63 n2 = (n >> 12) & 63 - n3 = (n >> 6) & 63 + n3 = (n >> 6) & 63 n4 = n & 63 - - r += base64chars[n1] + base64chars[n2] + base64chars[n3] + base64chars[n4] + + r += base64_chars[n1] + base64_chars[n2] + base64_chars[n3] + base64_chars[n4] i += 3 - return r[0: len(r)-len(p)] + p - -def decodeBase64(text): - base64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + return r[0 : len(r) - len(p)] + p + + +def decode_base64(text): + r""" + >>> decode_base64('V0VMQ09NRSB0byBiYXNlNjQgZW5jb2Rpbmcg8J+YgQ==') + 'WELCOME to base64 encoding 😁' + >>> decode_base64('QcOF4ZCD8JCAj/CfpJM=') + 'AÅᐃ𐀏🤓' + >>> decode_base64("QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\nQUFB") + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + """ + base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" s = "" - + for i in text: - if i in base64chars: + if i in base64_chars: s += i c = "" else: - if i == '=': - c += '=' - + if i == "=": + c += "=" + p = "" if c == "=": - p = 'A' + p = "A" else: if c == "==": p = "AA" - - r = "" + + r = b"" s = s + p - + i = 0 while i < len(s): - n = (base64chars.index(s[i]) << 18) + (base64chars.index(s[i+1]) << 12) + (base64chars.index(s[i+2]) << 6) +base64chars.index(s[i+3]) - - r += chr((n >> 16) & 255) + chr((n >> 8) & 255) + chr(n & 255) - + n = ( + (base64_chars.index(s[i]) << 18) + + (base64_chars.index(s[i + 1]) << 12) + + (base64_chars.index(s[i + 2]) << 6) + + base64_chars.index(s[i + 3]) + ) + + r += bytes([(n >> 16) & 255]) + bytes([(n >> 8) & 255]) + bytes([n & 255]) + i += 4 - - return r[0: len(r) - len(p)] + + return str(r[0 : len(r) - len(p)], "utf-8") + def main(): - print(encodeBase64("WELCOME to base64 encoding")) - print(decodeBase64(encodeBase64("WELCOME to base64 encoding"))) - + print(encode_base64("WELCOME to base64 encoding 😁")) + print(decode_base64(encode_base64("WELCOME to base64 encoding 😁"))) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/base85.py b/ciphers/base85.py index 5fd13837f662..ebfd0480f794 100644 --- a/ciphers/base85.py +++ b/ciphers/base85.py @@ -1,11 +1,13 @@ import base64 + def main(): - inp = input('->') - encoded = inp.encode('utf-8') #encoded the input (we need a bytes like object) - a85encoded = base64.a85encode(encoded) #a85encoded the encoded string + inp = input("->") + encoded = inp.encode("utf-8") # encoded the input (we need a bytes like object) + a85encoded = base64.a85encode(encoded) # a85encoded the encoded string print(a85encoded) - print(base64.a85decode(a85encoded).decode('utf-8'))#decoded it + print(base64.a85decode(a85encoded).decode("utf-8")) # decoded it + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/brute_force_caesar_cipher.py b/ciphers/brute_force_caesar_cipher.py index 3b0716442fc5..5f11cb848c41 100644 --- a/ciphers/brute_force_caesar_cipher.py +++ b/ciphers/brute_force_caesar_cipher.py @@ -1,4 +1,3 @@ -from __future__ import print_function def decrypt(message): """ >>> decrypt('TMDETUX PMDVU') @@ -41,14 +40,17 @@ def decrypt(message): translated = translated + LETTERS[num] else: translated = translated + symbol - print("Decryption using Key #%s: %s" % (key, translated)) + print(f"Decryption using Key #{key}: {translated}") + def main(): message = input("Encrypted message: ") message = message.upper() decrypt(message) -if __name__ == '__main__': + +if __name__ == "__main__": import doctest + doctest.testmod() main() diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index 872b5d8195c1..4427a5234d70 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -1,65 +1,67 @@ -def encrypt(strng, key): - encrypted = '' - for x in strng: - indx = (ord(x) + key) % 256 - if indx > 126: - indx = indx - 95 - encrypted = encrypted + chr(indx) - return encrypted +def encrypt(input_string: str, key: int) -> str: + result = "" + for x in input_string: + if not x.isalpha(): + result += x + elif x.isupper(): + result += chr((ord(x) + key - 65) % 26 + 65) + elif x.islower(): + result += chr((ord(x) + key - 97) % 26 + 97) + return result -def decrypt(strng, key): - decrypted = '' - for x in strng: - indx = (ord(x) - key) % 256 - if indx < 32: - indx = indx + 95 - decrypted = decrypted + chr(indx) - return decrypted +def decrypt(input_string: str, key: int) -> str: + result = "" + for x in input_string: + if not x.isalpha(): + result += x + elif x.isupper(): + result += chr((ord(x) - key - 65) % 26 + 65) + elif x.islower(): + result += chr((ord(x) - key - 97) % 26 + 97) + return result -def brute_force(strng): + +def brute_force(input_string: str) -> None: key = 1 - decrypted = '' + result = "" while key <= 94: - for x in strng: + for x in input_string: indx = (ord(x) - key) % 256 if indx < 32: indx = indx + 95 - decrypted = decrypted + chr(indx) - print("Key: {}\t| Message: {}".format(key, decrypted)) - decrypted = '' + result = result + chr(indx) + print(f"Key: {key}\t| Message: {result}") + result = "" key += 1 return None def main(): while True: - print('-' * 10 + "\n**Menu**\n" + '-' * 10) - print("1.Encrpyt") - print("2.Decrypt") - print("3.BruteForce") - print("4.Quit") + print(f'{"-" * 10}\n Menu\n{"-" * 10}') + print(*["1.Encrpyt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n") choice = input("What would you like to do?: ") - if choice not in ['1', '2', '3', '4']: - print ("Invalid choice, please enter a valid choice") - elif choice == '1': - strng = input("Please enter the string to be encrypted: ") - key = int(input("Please enter off-set between 1-94: ")) + if choice not in ["1", "2", "3", "4"]: + print("Invalid choice, please enter a valid choice") + elif choice == "1": + input_string = input("Please enter the string to be encrypted: ") + key = int(input("Please enter off-set between 0-25: ")) if key in range(1, 95): - print (encrypt(strng.lower(), key)) - elif choice == '2': - strng = input("Please enter the string to be decrypted: ") + print(encrypt(input_string.lower(), key)) + elif choice == "2": + input_string = input("Please enter the string to be decrypted: ") key = int(input("Please enter off-set between 1-94: ")) - if key in range(1,95): - print(decrypt(strng, key)) - elif choice == '3': - strng = input("Please enter the string to be decrypted: ") - brute_force(strng) + if key in range(1, 95): + print(decrypt(input_string, key)) + elif choice == "3": + input_string = input("Please enter the string to be decrypted: ") + brute_force(input_string) main() - elif choice == '4': - print ("Goodbye.") + elif choice == "4": + print("Goodbye.") break -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/cryptomath_module.py b/ciphers/cryptomath_module.py index 3e8e71b117ed..fc38e4bd2a22 100644 --- a/ciphers/cryptomath_module.py +++ b/ciphers/cryptomath_module.py @@ -3,6 +3,7 @@ def gcd(a, b): a, b = b % a, a return b + def findModInverse(a, m): if gcd(a, m) != 1: return None @@ -10,5 +11,5 @@ def findModInverse(a, m): v1, v2, v3 = 0, 1, m while v3 != 0: q = u3 // v3 - v1, v2, v3, u1, u2, u3 = (u1 - q * v1), (u2 - q * v2), (u3 - q *v3), v1, v2, v3 - return u1 % m + v1, v2, v3, u1, u2, u3 = (u1 - q * v1), (u2 - q * v2), (u3 - q * v3), v1, v2, v3 + return u1 % m diff --git a/ciphers/deterministic_miller_rabin.py b/ciphers/deterministic_miller_rabin.py new file mode 100644 index 000000000000..e604a7b84166 --- /dev/null +++ b/ciphers/deterministic_miller_rabin.py @@ -0,0 +1,137 @@ +"""Created by Nathan Damon, @bizzfitch on github +>>> test_miller_rabin() +""" + + +def miller_rabin(n, allow_probable=False): + """Deterministic Miller-Rabin algorithm for primes ~< 3.32e24. + + Uses numerical analysis results to return whether or not the passed number + is prime. If the passed number is above the upper limit, and + allow_probable is True, then a return value of True indicates that n is + probably prime. This test does not allow False negatives- a return value + of False is ALWAYS composite. + + Parameters + ---------- + n : int + The integer to be tested. Since we usually care if a number is prime, + n < 2 returns False instead of raising a ValueError. + allow_probable: bool, default False + Whether or not to test n above the upper bound of the deterministic test. + + Raises + ------ + ValueError + + Reference + --------- + https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test + """ + if n == 2: + return True + if not n % 2 or n < 2: + return False + if n > 5 and n % 10 not in (1, 3, 7, 9): # can quickly check last digit + return False + if n > 3_317_044_064_679_887_385_961_981 and not allow_probable: + raise ValueError( + "Warning: upper bound of deterministic test is exceeded. " + "Pass allow_probable=True to allow probabilistic test. " + "A return value of True indicates a probable prime." + ) + # array bounds provided by analysis + bounds = [ + 2_047, + 1_373_653, + 25_326_001, + 3_215_031_751, + 2_152_302_898_747, + 3_474_749_660_383, + 341_550_071_728_321, + 1, + 3_825_123_056_546_413_051, + 1, + 1, + 318_665_857_834_031_151_167_461, + 3_317_044_064_679_887_385_961_981, + ] + + primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41] + for idx, _p in enumerate(bounds, 1): + if n < _p: + # then we have our last prime to check + plist = primes[:idx] + break + d, s = n - 1, 0 + # break up n -1 into a power of 2 (s) and + # remaining odd component + # essentially, solve for d * 2 ** s == n - 1 + while d % 2 == 0: + d //= 2 + s += 1 + for prime in plist: + pr = False + for r in range(s): + m = pow(prime, d * 2 ** r, n) + # see article for analysis explanation for m + if (r == 0 and m == 1) or ((m + 1) % n == 0): + pr = True + # this loop will not determine compositeness + break + if pr: + continue + # if pr is False, then the above loop never evaluated to true, + # and the n MUST be composite + return False + return True + + +def test_miller_rabin(): + """Testing a nontrivial (ends in 1, 3, 7, 9) composite + and a prime in each range. + """ + assert not miller_rabin(561) + assert miller_rabin(563) + # 2047 + + assert not miller_rabin(838_201) + assert miller_rabin(838_207) + # 1_373_653 + + assert not miller_rabin(17_316_001) + assert miller_rabin(17_316_017) + # 25_326_001 + + assert not miller_rabin(3_078_386_641) + assert miller_rabin(3_078_386_653) + # 3_215_031_751 + + assert not miller_rabin(1_713_045_574_801) + assert miller_rabin(1_713_045_574_819) + # 2_152_302_898_747 + + assert not miller_rabin(2_779_799_728_307) + assert miller_rabin(2_779_799_728_327) + # 3_474_749_660_383 + + assert not miller_rabin(113_850_023_909_441) + assert miller_rabin(113_850_023_909_527) + # 341_550_071_728_321 + + assert not miller_rabin(1_275_041_018_848_804_351) + assert miller_rabin(1_275_041_018_848_804_391) + # 3_825_123_056_546_413_051 + + assert not miller_rabin(79_666_464_458_507_787_791_867) + assert miller_rabin(79_666_464_458_507_787_791_951) + # 318_665_857_834_031_151_167_461 + + assert not miller_rabin(552_840_677_446_647_897_660_333) + assert miller_rabin(552_840_677_446_647_897_660_359) + # 3_317_044_064_679_887_385_961_981 + # upper limit for probabilistic test + + +if __name__ == "__main__": + test_miller_rabin() diff --git a/ciphers/diffie.py b/ciphers/diffie.py new file mode 100644 index 000000000000..c349aaa2f3b8 --- /dev/null +++ b/ciphers/diffie.py @@ -0,0 +1,25 @@ +def find_primitive(n): + for r in range(1, n): + li = [] + for x in range(n - 1): + val = pow(r, x, n) + if val in li: + break + li.append(val) + else: + return r + + +if __name__ == "__main__": + q = int(input("Enter a prime number q: ")) + a = find_primitive(q) + a_private = int(input("Enter private key of A: ")) + a_public = pow(a, a_private, q) + b_private = int(input("Enter private key of B: ")) + b_public = pow(a, b_private, q) + + a_secret = pow(b_public, a_private, q) + b_secret = pow(a_public, b_private, q) + + print("The key value generated by A is: ", a_secret) + print("The key value generated by B is: ", b_secret) diff --git a/ciphers/elgamal_key_generator.py b/ciphers/elgamal_key_generator.py index 6a8751f69524..cc6b297f2daf 100644 --- a/ciphers/elgamal_key_generator.py +++ b/ciphers/elgamal_key_generator.py @@ -7,9 +7,9 @@ def main(): - print('Making key files...') - makeKeyFiles('elgamal', 2048) - print('Key files generation successful') + print("Making key files...") + makeKeyFiles("elgamal", 2048) + print("Key files generation successful") # I have written my code naively same as definition of primitive root @@ -19,7 +19,7 @@ def main(): def primitiveRoot(p_val): print("Generating primitive root of p") while True: - g = random.randrange(3,p_val) + g = random.randrange(3, p_val) if pow(g, 2, p_val) == 1: continue if pow(g, p_val, p_val) == 1: @@ -28,7 +28,7 @@ def primitiveRoot(p_val): def generateKey(keySize): - print('Generating prime p...') + print("Generating prime p...") p = rabinMiller.generateLargePrime(keySize) # select large prime number. e_1 = primitiveRoot(p) # one primitive root on modulo p. d = random.randrange(3, p) # private_key -> have to be greater than 2 for safety. @@ -41,23 +41,28 @@ def generateKey(keySize): def makeKeyFiles(name, keySize): - if os.path.exists('%s_pubkey.txt' % name) or os.path.exists('%s_privkey.txt' % name): - print('\nWARNING:') - print('"%s_pubkey.txt" or "%s_privkey.txt" already exists. \n' - 'Use a different name or delete these files and re-run this program.' % - (name, name)) + if os.path.exists("%s_pubkey.txt" % name) or os.path.exists( + "%s_privkey.txt" % name + ): + print("\nWARNING:") + print( + '"%s_pubkey.txt" or "%s_privkey.txt" already exists. \n' + "Use a different name or delete these files and re-run this program." + % (name, name) + ) sys.exit() publicKey, privateKey = generateKey(keySize) - print('\nWriting public key to file %s_pubkey.txt...' % name) - with open('%s_pubkey.txt' % name, 'w') as fo: - fo.write('%d,%d,%d,%d' % (publicKey[0], publicKey[1], publicKey[2], publicKey[3])) + print("\nWriting public key to file %s_pubkey.txt..." % name) + with open("%s_pubkey.txt" % name, "w") as fo: + fo.write( + "%d,%d,%d,%d" % (publicKey[0], publicKey[1], publicKey[2], publicKey[3]) + ) - print('Writing private key to file %s_privkey.txt...' % name) - with open('%s_privkey.txt' % name, 'w') as fo: - fo.write('%d,%d' % (privateKey[0], privateKey[1])) + print("Writing private key to file %s_privkey.txt..." % name) + with open("%s_privkey.txt" % name, "w") as fo: + fo.write("%d,%d" % (privateKey[0], privateKey[1])) -if __name__ == '__main__': +if __name__ == "__main__": main() - \ No newline at end of file diff --git a/ciphers/hill_cipher.py b/ciphers/hill_cipher.py index 89b88beed17e..47910e4ebdaa 100644 --- a/ciphers/hill_cipher.py +++ b/ciphers/hill_cipher.py @@ -3,15 +3,15 @@ Hill Cipher: The below defined class 'HillCipher' implements the Hill Cipher algorithm. The Hill Cipher is an algorithm that implements modern linear algebra techniques -In this algortihm, you have an encryption key matrix. This is what will be used +In this algorithm, you have an encryption key matrix. This is what will be used in encoding and decoding your text. -Algortihm: +Algorithm: Let the order of the encryption key be N (as it is a square matrix). Your text is divided into batches of length N and converted to numerical vectors by a simple mapping starting with A=0 and so on. -The key is then mulitplied with the newly created batch vector to obtain the +The key is then multiplied with the newly created batch vector to obtain the encoded vector. After each multiplication modular 36 calculations are performed on the vectors so as to bring the numbers between 0 and 36 and then mapped with their corresponding alphanumerics. @@ -44,7 +44,7 @@ def gcd(a, b): if a == 0: return b - return gcd(b%a, a) + return gcd(b % a, a) class HillCipher: @@ -59,27 +59,31 @@ class HillCipher: modulus = numpy.vectorize(lambda x: x % 36) toInt = numpy.vectorize(lambda x: round(x)) - + def __init__(self, encrypt_key): """ encrypt_key is an NxN numpy matrix """ - self.encrypt_key = self.modulus(encrypt_key) # mod36 calc's on the encrypt key - self.checkDeterminant() # validate the determinant of the encryption key + self.encrypt_key = self.modulus(encrypt_key) # mod36 calc's on the encrypt key + self.check_determinant() # validate the determinant of the encryption key self.decrypt_key = None self.break_key = encrypt_key.shape[0] - def checkDeterminant(self): + def check_determinant(self): det = round(numpy.linalg.det(self.encrypt_key)) - + if det < 0: det = det % len(self.key_string) req_l = len(self.key_string) if gcd(det, len(self.key_string)) != 1: - raise ValueError("discriminant modular {0} of encryption key({1}) is not co prime w.r.t {2}.\nTry another key.".format(req_l, det, req_l)) + raise ValueError( + "discriminant modular {} of encryption key({}) is not co prime w.r.t {}.\nTry another key.".format( + req_l, det, req_l + ) + ) - def processText(self, text): + def process_text(self, text): text = list(text.upper()) text = [char for char in text if char in self.key_string] @@ -87,25 +91,27 @@ def processText(self, text): while len(text) % self.break_key != 0: text.append(last) - return ''.join(text) - + return "".join(text) + def encrypt(self, text): - text = self.processText(text.upper()) - encrypted = '' + text = self.process_text(text.upper()) + encrypted = "" for i in range(0, len(text) - self.break_key + 1, self.break_key): - batch = text[i:i+self.break_key] + batch = text[i : i + self.break_key] batch_vec = list(map(self.replaceLetters, batch)) batch_vec = numpy.matrix([batch_vec]).T - batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[0] - encrypted_batch = ''.join(list(map(self.replaceNumbers, batch_encrypted))) + batch_encrypted = self.modulus(self.encrypt_key.dot(batch_vec)).T.tolist()[ + 0 + ] + encrypted_batch = "".join(list(map(self.replaceNumbers, batch_encrypted))) encrypted += encrypted_batch return encrypted - def makeDecryptKey(self): + def make_decrypt_key(self): det = round(numpy.linalg.det(self.encrypt_key)) - + if det < 0: det = det % len(self.key_string) det_inv = None @@ -114,22 +120,27 @@ def makeDecryptKey(self): det_inv = i break - inv_key = det_inv * numpy.linalg.det(self.encrypt_key) *\ - numpy.linalg.inv(self.encrypt_key) + inv_key = ( + det_inv + * numpy.linalg.det(self.encrypt_key) + * numpy.linalg.inv(self.encrypt_key) + ) return self.toInt(self.modulus(inv_key)) - + def decrypt(self, text): - self.decrypt_key = self.makeDecryptKey() - text = self.processText(text.upper()) - decrypted = '' + self.decrypt_key = self.make_decrypt_key() + text = self.process_text(text.upper()) + decrypted = "" for i in range(0, len(text) - self.break_key + 1, self.break_key): - batch = text[i:i+self.break_key] + batch = text[i : i + self.break_key] batch_vec = list(map(self.replaceLetters, batch)) batch_vec = numpy.matrix([batch_vec]).T - batch_decrypted = self.modulus(self.decrypt_key.dot(batch_vec)).T.tolist()[0] - decrypted_batch = ''.join(list(map(self.replaceNumbers, batch_decrypted))) + batch_decrypted = self.modulus(self.decrypt_key.dot(batch_vec)).T.tolist()[ + 0 + ] + decrypted_batch = "".join(list(map(self.replaceNumbers, batch_decrypted))) decrypted += decrypted_batch return decrypted @@ -147,21 +158,22 @@ def main(): hc = HillCipher(numpy.matrix(hill_matrix)) print("Would you like to encrypt or decrypt some text? (1 or 2)") - option = input(""" + option = input( + """ 1. Encrypt 2. Decrypt """ - ) + ) - if option == '1': + if option == "1": text_e = input("What text would you like to encrypt?: ") print("Your encrypted text is:") print(hc.encrypt(text_e)) - elif option == '2': + elif option == "2": text_d = input("What text would you like to decrypt?: ") print("Your decrypted text is:") print(hc.decrypt(text_d)) - + if __name__ == "__main__": main() diff --git a/ciphers/mixed_keyword_cypher.py b/ciphers/mixed_keyword_cypher.py new file mode 100644 index 000000000000..a546e4c781e6 --- /dev/null +++ b/ciphers/mixed_keyword_cypher.py @@ -0,0 +1,68 @@ +def mixed_keyword(key="college", pt="UNIVERSITY"): + """ + + For key:hello + + H E L O + A B C D + F G I J + K M N P + Q R S T + U V W X + Y Z + and map vertically + + >>> mixed_keyword("college", "UNIVERSITY") # doctest: +NORMALIZE_WHITESPACE + {'A': 'C', 'B': 'A', 'C': 'I', 'D': 'P', 'E': 'U', 'F': 'Z', 'G': 'O', 'H': 'B', + 'I': 'J', 'J': 'Q', 'K': 'V', 'L': 'L', 'M': 'D', 'N': 'K', 'O': 'R', 'P': 'W', + 'Q': 'E', 'R': 'F', 'S': 'M', 'T': 'S', 'U': 'X', 'V': 'G', 'W': 'H', 'X': 'N', + 'Y': 'T', 'Z': 'Y'} + 'XKJGUFMJST' + """ + key = key.upper() + pt = pt.upper() + temp = [] + for i in key: + if i not in temp: + temp.append(i) + l = len(temp) + # print(temp) + alpha = [] + modalpha = [] + for i in range(65, 91): + t = chr(i) + alpha.append(t) + if t not in temp: + temp.append(t) + # print(temp) + r = int(26 / 4) + # print(r) + k = 0 + for i in range(r): + t = [] + for j in range(l): + t.append(temp[k]) + if not (k < 25): + break + k += 1 + modalpha.append(t) + # print(modalpha) + d = dict() + j = 0 + k = 0 + for j in range(l): + for i in modalpha: + if not (len(i) - 1 >= j): + break + d[alpha[k]] = i[j] + if not k < 25: + break + k += 1 + print(d) + cypher = "" + for i in pt: + cypher += d[i] + return cypher + + +print(mixed_keyword("college", "UNIVERSITY")) diff --git a/ciphers/morse_code_implementation.py b/ciphers/morse_code_implementation.py index 7b2d0a94b24b..6df4632af4cb 100644 --- a/ciphers/morse_code_implementation.py +++ b/ciphers/morse_code_implementation.py @@ -2,68 +2,93 @@ # Dictionary representing the morse code chart -MORSE_CODE_DICT = { 'A':'.-', 'B':'-...', - 'C':'-.-.', 'D':'-..', 'E':'.', - 'F':'..-.', 'G':'--.', 'H':'....', - 'I':'..', 'J':'.---', 'K':'-.-', - 'L':'.-..', 'M':'--', 'N':'-.', - 'O':'---', 'P':'.--.', 'Q':'--.-', - 'R':'.-.', 'S':'...', 'T':'-', - 'U':'..-', 'V':'...-', 'W':'.--', - 'X':'-..-', 'Y':'-.--', 'Z':'--..', - '1':'.----', '2':'..---', '3':'...--', - '4':'....-', '5':'.....', '6':'-....', - '7':'--...', '8':'---..', '9':'----.', - '0':'-----', ', ':'--..--', '.':'.-.-.-', - '?':'..--..', '/':'-..-.', '-':'-....-', - '(':'-.--.', ')':'-.--.-'} +MORSE_CODE_DICT = { + "A": ".-", + "B": "-...", + "C": "-.-.", + "D": "-..", + "E": ".", + "F": "..-.", + "G": "--.", + "H": "....", + "I": "..", + "J": ".---", + "K": "-.-", + "L": ".-..", + "M": "--", + "N": "-.", + "O": "---", + "P": ".--.", + "Q": "--.-", + "R": ".-.", + "S": "...", + "T": "-", + "U": "..-", + "V": "...-", + "W": ".--", + "X": "-..-", + "Y": "-.--", + "Z": "--..", + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + "0": "-----", + ", ": "--..--", + ".": ".-.-.-", + "?": "..--..", + "/": "-..-.", + "-": "-....-", + "(": "-.--.", + ")": "-.--.-", +} def encrypt(message): - cipher = '' + cipher = "" for letter in message: - if letter != ' ': + if letter != " ": - - cipher += MORSE_CODE_DICT[letter] + ' ' + cipher += MORSE_CODE_DICT[letter] + " " else: - cipher += ' ' + cipher += " " return cipher def decrypt(message): - message += ' ' + message += " " - decipher = '' - citext = '' + decipher = "" + citext = "" for letter in message: - if (letter != ' '): - + if letter != " ": i = 0 - citext += letter else: i += 1 + if i == 2: - if i == 2 : - - - decipher += ' ' + decipher += " " else: - - decipher += list(MORSE_CODE_DICT.keys())[list(MORSE_CODE_DICT - .values()).index(citext)] - citext = '' + decipher += list(MORSE_CODE_DICT.keys())[ + list(MORSE_CODE_DICT.values()).index(citext) + ] + citext = "" return decipher @@ -71,12 +96,12 @@ def decrypt(message): def main(): message = "Morse code here" result = encrypt(message.upper()) - print (result) + print(result) message = result result = decrypt(message) - print (result) + print(result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/onepad_cipher.py b/ciphers/onepad_cipher.py index 6afbd45249ec..fe07908afff5 100644 --- a/ciphers/onepad_cipher.py +++ b/ciphers/onepad_cipher.py @@ -1,32 +1,30 @@ -from __future__ import print_function - import random class Onepad: def encrypt(self, text): - '''Function to encrypt text using psedo-random numbers''' + """Function to encrypt text using pseudo-random numbers""" plain = [ord(i) for i in text] key = [] cipher = [] for i in plain: k = random.randint(1, 300) - c = (i+k)*k + c = (i + k) * k cipher.append(c) key.append(k) return cipher, key - + def decrypt(self, cipher, key): - '''Function to decrypt text using psedo-random numbers.''' + """Function to decrypt text using pseudo-random numbers.""" plain = [] for i in range(len(key)): - p = int((cipher[i]-(key[i])**2)/key[i]) + p = int((cipher[i] - (key[i]) ** 2) / key[i]) plain.append(chr(p)) - plain = ''.join([i for i in plain]) + plain = "".join([i for i in plain]) return plain -if __name__ == '__main__': - c, k = Onepad().encrypt('Hello') +if __name__ == "__main__": + c, k = Onepad().encrypt("Hello") print(c, k) print(Onepad().decrypt(c, k)) diff --git a/ciphers/playfair_cipher.py b/ciphers/playfair_cipher.py index 20449b161963..030fe8155a69 100644 --- a/ciphers/playfair_cipher.py +++ b/ciphers/playfair_cipher.py @@ -1,14 +1,14 @@ import string import itertools + def chunker(seq, size): it = iter(seq) while True: - chunk = tuple(itertools.islice(it, size)) - if not chunk: - return - yield chunk - + chunk = tuple(itertools.islice(it, size)) + if not chunk: + return + yield chunk def prepare_input(dirty): @@ -16,32 +16,33 @@ def prepare_input(dirty): Prepare the plaintext by up-casing it and separating repeated letters with X's """ - - dirty = ''.join([c.upper() for c in dirty if c in string.ascii_letters]) + + dirty = "".join([c.upper() for c in dirty if c in string.ascii_letters]) clean = "" - + if len(dirty) < 2: return dirty - for i in range(len(dirty)-1): + for i in range(len(dirty) - 1): clean += dirty[i] - - if dirty[i] == dirty[i+1]: - clean += 'X' - + + if dirty[i] == dirty[i + 1]: + clean += "X" + clean += dirty[-1] if len(clean) & 1: - clean += 'X' + clean += "X" return clean + def generate_table(key): # I and J are used interchangeably to allow # us to use a 5x5 table (25 letters) alphabet = "ABCDEFGHIKLMNOPQRSTUVWXYZ" - # we're using a list instead of a '2d' array because it makes the math + # we're using a list instead of a '2d' array because it makes the math # for setting up the table and doing the actual encoding/decoding simpler table = [] @@ -57,6 +58,7 @@ def generate_table(key): return table + def encode(plaintext, key): table = generate_table(key) plaintext = prepare_input(plaintext) @@ -68,14 +70,14 @@ def encode(plaintext, key): row2, col2 = divmod(table.index(char2), 5) if row1 == row2: - ciphertext += table[row1*5+(col1+1)%5] - ciphertext += table[row2*5+(col2+1)%5] + ciphertext += table[row1 * 5 + (col1 + 1) % 5] + ciphertext += table[row2 * 5 + (col2 + 1) % 5] elif col1 == col2: - ciphertext += table[((row1+1)%5)*5+col1] - ciphertext += table[((row2+1)%5)*5+col2] - else: # rectangle - ciphertext += table[row1*5+col2] - ciphertext += table[row2*5+col1] + ciphertext += table[((row1 + 1) % 5) * 5 + col1] + ciphertext += table[((row2 + 1) % 5) * 5 + col2] + else: # rectangle + ciphertext += table[row1 * 5 + col2] + ciphertext += table[row2 * 5 + col1] return ciphertext @@ -90,13 +92,13 @@ def decode(ciphertext, key): row2, col2 = divmod(table.index(char2), 5) if row1 == row2: - plaintext += table[row1*5+(col1-1)%5] - plaintext += table[row2*5+(col2-1)%5] + plaintext += table[row1 * 5 + (col1 - 1) % 5] + plaintext += table[row2 * 5 + (col2 - 1) % 5] elif col1 == col2: - plaintext += table[((row1-1)%5)*5+col1] - plaintext += table[((row2-1)%5)*5+col2] - else: # rectangle - plaintext += table[row1*5+col2] - plaintext += table[row2*5+col1] + plaintext += table[((row1 - 1) % 5) * 5 + col1] + plaintext += table[((row2 - 1) % 5) * 5 + col2] + else: # rectangle + plaintext += table[row1 * 5 + col2] + plaintext += table[row2 * 5 + col1] return plaintext diff --git a/ciphers/porta_cipher.py b/ciphers/porta_cipher.py new file mode 100644 index 000000000000..a8e79415958d --- /dev/null +++ b/ciphers/porta_cipher.py @@ -0,0 +1,110 @@ +alphabet = { + "A": ("ABCDEFGHIJKLM", "NOPQRSTUVWXYZ"), + "B": ("ABCDEFGHIJKLM", "NOPQRSTUVWXYZ"), + "C": ("ABCDEFGHIJKLM", "ZNOPQRSTUVWXY"), + "D": ("ABCDEFGHIJKLM", "ZNOPQRSTUVWXY"), + "E": ("ABCDEFGHIJKLM", "YZNOPQRSTUVWX"), + "F": ("ABCDEFGHIJKLM", "YZNOPQRSTUVWX"), + "G": ("ABCDEFGHIJKLM", "XYZNOPQRSTUVW"), + "H": ("ABCDEFGHIJKLM", "XYZNOPQRSTUVW"), + "I": ("ABCDEFGHIJKLM", "WXYZNOPQRSTUV"), + "J": ("ABCDEFGHIJKLM", "WXYZNOPQRSTUV"), + "K": ("ABCDEFGHIJKLM", "VWXYZNOPQRSTU"), + "L": ("ABCDEFGHIJKLM", "VWXYZNOPQRSTU"), + "M": ("ABCDEFGHIJKLM", "UVWXYZNOPQRST"), + "N": ("ABCDEFGHIJKLM", "UVWXYZNOPQRST"), + "O": ("ABCDEFGHIJKLM", "TUVWXYZNOPQRS"), + "P": ("ABCDEFGHIJKLM", "TUVWXYZNOPQRS"), + "Q": ("ABCDEFGHIJKLM", "STUVWXYZNOPQR"), + "R": ("ABCDEFGHIJKLM", "STUVWXYZNOPQR"), + "S": ("ABCDEFGHIJKLM", "RSTUVWXYZNOPQ"), + "T": ("ABCDEFGHIJKLM", "RSTUVWXYZNOPQ"), + "U": ("ABCDEFGHIJKLM", "QRSTUVWXYZNOP"), + "V": ("ABCDEFGHIJKLM", "QRSTUVWXYZNOP"), + "W": ("ABCDEFGHIJKLM", "PQRSTUVWXYZNO"), + "X": ("ABCDEFGHIJKLM", "PQRSTUVWXYZNO"), + "Y": ("ABCDEFGHIJKLM", "OPQRSTUVWXYZN"), + "Z": ("ABCDEFGHIJKLM", "OPQRSTUVWXYZN"), +} + + +def generate_table(key): + """ + >>> generate_table('marvin') # doctest: +NORMALIZE_WHITESPACE + [('ABCDEFGHIJKLM', 'UVWXYZNOPQRST'), ('ABCDEFGHIJKLM', 'NOPQRSTUVWXYZ'), + ('ABCDEFGHIJKLM', 'STUVWXYZNOPQR'), ('ABCDEFGHIJKLM', 'QRSTUVWXYZNOP'), + ('ABCDEFGHIJKLM', 'WXYZNOPQRSTUV'), ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST')] + """ + return [alphabet[char] for char in key.upper()] + + +def encrypt(key, words): + """ + >>> encrypt('marvin', 'jessica') + 'QRACRWU' + """ + cipher = "" + count = 0 + table = generate_table(key) + for char in words.upper(): + cipher += get_opponent(table[count], char) + count = (count + 1) % len(table) + return cipher + + +def decrypt(key, words): + """ + >>> decrypt('marvin', 'QRACRWU') + 'JESSICA' + """ + return encrypt(key, words) + + +def get_position(table, char): + """ + >>> table = [ + ... ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST'), ('ABCDEFGHIJKLM', 'NOPQRSTUVWXYZ'), + ... ('ABCDEFGHIJKLM', 'STUVWXYZNOPQR'), ('ABCDEFGHIJKLM', 'QRSTUVWXYZNOP'), + ... ('ABCDEFGHIJKLM', 'WXYZNOPQRSTUV'), ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST')] + >>> get_position(table, 'A') + (None, None) + """ + if char in table[0]: + row = 0 + else: + row = 1 if char in table[1] else -1 + return (None, None) if row == -1 else (row, table[row].index(char)) + + +def get_opponent(table, char): + """ + >>> table = [ + ... ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST'), ('ABCDEFGHIJKLM', 'NOPQRSTUVWXYZ'), + ... ('ABCDEFGHIJKLM', 'STUVWXYZNOPQR'), ('ABCDEFGHIJKLM', 'QRSTUVWXYZNOP'), + ... ('ABCDEFGHIJKLM', 'WXYZNOPQRSTUV'), ('ABCDEFGHIJKLM', 'UVWXYZNOPQRST')] + >>> get_opponent(table, 'A') + 'A' + """ + row, col = get_position(table, char.upper()) + if row == 1: + return table[0][col] + else: + return table[1][col] if row == 0 else char + + +if __name__ == "__main__": + import doctest + + doctest.testmod() # Fist ensure that all our tests are passing... + """ + ENTER KEY: marvin + ENTER TEXT TO ENCRYPT: jessica + ENCRYPTED: QRACRWU + DECRYPTED WITH KEY: JESSICA + """ + key = input("ENTER KEY: ").strip() + text = input("ENTER TEXT TO ENCRYPT: ").strip() + cipher_text = encrypt(key, text) + + print(f"ENCRYPTED: {cipher_text}") + print(f"DECRYPTED WITH KEY: {decrypt(key, cipher_text)}") diff --git a/ciphers/rabin_miller.py b/ciphers/rabin_miller.py index f71fb03c0051..c544abdf9acc 100644 --- a/ciphers/rabin_miller.py +++ b/ciphers/rabin_miller.py @@ -1,8 +1,8 @@ -from __future__ import print_function # Primality Testing with the Rabin-Miller Algorithm import random + def rabinMiller(num): s = num - 1 t = 0 @@ -24,24 +24,181 @@ def rabinMiller(num): v = (v ** 2) % num return True + def isPrime(num): - if (num < 2): + if num < 2: return False - lowPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, - 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, - 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, - 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, - 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, - 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, - 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, - 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, - 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, - 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, - 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, - 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, - 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, - 971, 977, 983, 991, 997] + lowPrimes = [ + 2, + 3, + 5, + 7, + 11, + 13, + 17, + 19, + 23, + 29, + 31, + 37, + 41, + 43, + 47, + 53, + 59, + 61, + 67, + 71, + 73, + 79, + 83, + 89, + 97, + 101, + 103, + 107, + 109, + 113, + 127, + 131, + 137, + 139, + 149, + 151, + 157, + 163, + 167, + 173, + 179, + 181, + 191, + 193, + 197, + 199, + 211, + 223, + 227, + 229, + 233, + 239, + 241, + 251, + 257, + 263, + 269, + 271, + 277, + 281, + 283, + 293, + 307, + 311, + 313, + 317, + 331, + 337, + 347, + 349, + 353, + 359, + 367, + 373, + 379, + 383, + 389, + 397, + 401, + 409, + 419, + 421, + 431, + 433, + 439, + 443, + 449, + 457, + 461, + 463, + 467, + 479, + 487, + 491, + 499, + 503, + 509, + 521, + 523, + 541, + 547, + 557, + 563, + 569, + 571, + 577, + 587, + 593, + 599, + 601, + 607, + 613, + 617, + 619, + 631, + 641, + 643, + 647, + 653, + 659, + 661, + 673, + 677, + 683, + 691, + 701, + 709, + 719, + 727, + 733, + 739, + 743, + 751, + 757, + 761, + 769, + 773, + 787, + 797, + 809, + 811, + 821, + 823, + 827, + 829, + 839, + 853, + 857, + 859, + 863, + 877, + 881, + 883, + 887, + 907, + 911, + 919, + 929, + 937, + 941, + 947, + 953, + 967, + 971, + 977, + 983, + 991, + 997, + ] if num in lowPrimes: return True @@ -52,13 +209,15 @@ def isPrime(num): return rabinMiller(num) -def generateLargePrime(keysize = 1024): + +def generateLargePrime(keysize=1024): while True: num = random.randrange(2 ** (keysize - 1), 2 ** (keysize)) if isPrime(num): return num -if __name__ == '__main__': + +if __name__ == "__main__": num = generateLargePrime() - print(('Prime number:', num)) - print(('isPrime:', isPrime(num))) + print(("Prime number:", num)) + print(("isPrime:", isPrime(num))) diff --git a/ciphers/rot13.py b/ciphers/rot13.py index 2abf981e9d7d..6bcb471d6e05 100644 --- a/ciphers/rot13.py +++ b/ciphers/rot13.py @@ -1,25 +1,37 @@ -from __future__ import print_function -def dencrypt(s, n): - out = '' +def dencrypt(s: str, n: int = 13): + """ + https://en.wikipedia.org/wiki/ROT13 + + >>> msg = "My secret bank account number is 173-52946 so don't tell anyone!!" + >>> s = dencrypt(msg) + >>> s + "Zl frperg onax nppbhag ahzore vf 173-52946 fb qba'g gryy nalbar!!" + >>> dencrypt(s) == msg + True + """ + out = "" for c in s: - if c >= 'A' and c <= 'Z': - out += chr(ord('A') + (ord(c) - ord('A') + n) % 26) - elif c >= 'a' and c <= 'z': - out += chr(ord('a') + (ord(c) - ord('a') + n) % 26) + if "A" <= c <= "Z": + out += chr(ord("A") + (ord(c) - ord("A") + n) % 26) + elif "a" <= c <= "z": + out += chr(ord("a") + (ord(c) - ord("a") + n) % 26) else: out += c return out def main(): - s0 = 'HELLO' + s0 = input("Enter message: ") s1 = dencrypt(s0, 13) - print(s1) # URYYB + print("Encryption:", s1) s2 = dencrypt(s1, 13) - print(s2) # HELLO + print("Decryption: ", s2) + +if __name__ == "__main__": + import doctest -if __name__ == '__main__': + doctest.testmod() main() diff --git a/ciphers/rsa_cipher.py b/ciphers/rsa_cipher.py index d81f1ffc1a1e..371966b8379b 100644 --- a/ciphers/rsa_cipher.py +++ b/ciphers/rsa_cipher.py @@ -1,44 +1,47 @@ -from __future__ import print_function -import sys, rsa_key_generator as rkg, os +import os +import sys + +import rsa_key_generator as rkg DEFAULT_BLOCK_SIZE = 128 BYTE_SIZE = 256 + def main(): - filename = 'encrypted_file.txt' - response = input(r'Encrypte\Decrypt [e\d]: ') - - if response.lower().startswith('e'): - mode = 'encrypt' - elif response.lower().startswith('d'): - mode = 'decrypt' - - if mode == 'encrypt': - if not os.path.exists('rsa_pubkey.txt'): - rkg.makeKeyFiles('rsa', 1024) - - message = input('\nEnter message: ') - pubKeyFilename = 'rsa_pubkey.txt' - print('Encrypting and writing to %s...' % (filename)) + filename = "encrypted_file.txt" + response = input(r"Encrypt\Decrypt [e\d]: ") + + if response.lower().startswith("e"): + mode = "encrypt" + elif response.lower().startswith("d"): + mode = "decrypt" + + if mode == "encrypt": + if not os.path.exists("rsa_pubkey.txt"): + rkg.makeKeyFiles("rsa", 1024) + + message = input("\nEnter message: ") + pubKeyFilename = "rsa_pubkey.txt" + print("Encrypting and writing to %s..." % (filename)) encryptedText = encryptAndWriteToFile(filename, pubKeyFilename, message) - print('\nEncrypted text:') + print("\nEncrypted text:") print(encryptedText) - elif mode == 'decrypt': - privKeyFilename = 'rsa_privkey.txt' - print('Reading from %s and decrypting...' % (filename)) + elif mode == "decrypt": + privKeyFilename = "rsa_privkey.txt" + print("Reading from %s and decrypting..." % (filename)) decryptedText = readFromFileAndDecrypt(filename, privKeyFilename) - print('writing decryption to rsa_decryption.txt...') - with open('rsa_decryption.txt', 'w') as dec: + print("writing decryption to rsa_decryption.txt...") + with open("rsa_decryption.txt", "w") as dec: dec.write(decryptedText) - print('\nDecryption:') + print("\nDecryption:") print(decryptedText) def getBlocksFromText(message, blockSize=DEFAULT_BLOCK_SIZE): - messageBytes = message.encode('ascii') + messageBytes = message.encode("ascii") blockInts = [] for blockStart in range(0, len(messageBytes), blockSize): @@ -59,7 +62,7 @@ def getTextFromBlocks(blockInts, messageLength, blockSize=DEFAULT_BLOCK_SIZE): blockInt = blockInt % (BYTE_SIZE ** i) blockMessage.insert(0, chr(asciiNumber)) message.extend(blockMessage) - return ''.join(message) + return "".join(message) def encryptMessage(message, key, blockSize=DEFAULT_BLOCK_SIZE): @@ -82,22 +85,29 @@ def decryptMessage(encryptedBlocks, messageLength, key, blockSize=DEFAULT_BLOCK_ def readKeyFile(keyFilename): with open(keyFilename) as fo: content = fo.read() - keySize, n, EorD = content.split(',') + keySize, n, EorD = content.split(",") return (int(keySize), int(n), int(EorD)) -def encryptAndWriteToFile(messageFilename, keyFilename, message, blockSize=DEFAULT_BLOCK_SIZE): +def encryptAndWriteToFile( + messageFilename, keyFilename, message, blockSize=DEFAULT_BLOCK_SIZE +): keySize, n, e = readKeyFile(keyFilename) if keySize < blockSize * 8: - sys.exit('ERROR: Block size is %s bits and key size is %s bits. The RSA cipher requires the block size to be equal to or greater than the key size. Either decrease the block size or use different keys.' % (blockSize * 8, keySize)) + sys.exit( + "ERROR: Block size is %s bits and key size is %s bits. The RSA cipher " + "requires the block size to be equal to or greater than the key size. " + "Either decrease the block size or use different keys." + % (blockSize * 8, keySize) + ) encryptedBlocks = encryptMessage(message, (n, e), blockSize) for i in range(len(encryptedBlocks)): encryptedBlocks[i] = str(encryptedBlocks[i]) - encryptedContent = ','.join(encryptedBlocks) - encryptedContent = '%s_%s_%s' % (len(message), blockSize, encryptedContent) - with open(messageFilename, 'w') as fo: + encryptedContent = ",".join(encryptedBlocks) + encryptedContent = "{}_{}_{}".format(len(message), blockSize, encryptedContent) + with open(messageFilename, "w") as fo: fo.write(encryptedContent) return encryptedContent @@ -106,18 +116,24 @@ def readFromFileAndDecrypt(messageFilename, keyFilename): keySize, n, d = readKeyFile(keyFilename) with open(messageFilename) as fo: content = fo.read() - messageLength, blockSize, encryptedMessage = content.split('_') + messageLength, blockSize, encryptedMessage = content.split("_") messageLength = int(messageLength) blockSize = int(blockSize) if keySize < blockSize * 8: - sys.exit('ERROR: Block size is %s bits and key size is %s bits. The RSA cipher requires the block size to be equal to or greater than the key size. Did you specify the correct key file and encrypted file?' % (blockSize * 8, keySize)) + sys.exit( + "ERROR: Block size is %s bits and key size is %s bits. The RSA cipher " + "requires the block size to be equal to or greater than the key size. " + "Did you specify the correct key file and encrypted file?" + % (blockSize * 8, keySize) + ) encryptedBlocks = [] - for block in encryptedMessage.split(','): + for block in encryptedMessage.split(","): encryptedBlocks.append(int(block)) return decryptMessage(encryptedBlocks, messageLength, (n, d), blockSize) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/ciphers/rsa_factorization.py b/ciphers/rsa_factorization.py new file mode 100644 index 000000000000..9ec34e6c5a17 --- /dev/null +++ b/ciphers/rsa_factorization.py @@ -0,0 +1,56 @@ +""" +An RSA prime factor algorithm. + +The program can efficiently factor RSA prime number given the private key d and +public key e. +Source: on page 3 of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf +More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html +large number can take minutes to factor, therefore are not included in doctest. +""" +import math +import random +from typing import List + + +def rsafactor(d: int, e: int, N: int) -> List[int]: + """ + This function returns the factors of N, where p*q=N + Return: [p, q] + + We call N the RSA modulus, e the encryption exponent, and d the decryption exponent. + The pair (N, e) is the public key. As its name suggests, it is public and is used to + encrypt messages. + The pair (N, d) is the secret key or private key and is known only to the recipient + of encrypted messages. + + >>> rsafactor(3, 16971, 25777) + [149, 173] + >>> rsafactor(7331, 11, 27233) + [113, 241] + >>> rsafactor(4021, 13, 17711) + [89, 199] + """ + k = d * e - 1 + p = 0 + q = 0 + while p == 0: + g = random.randint(2, N - 1) + t = k + while True: + if t % 2 == 0: + t = t // 2 + x = (g ** t) % N + y = math.gcd(x - 1, N) + if x > 1 and y > 1: + p = y + q = N // y + break # find the correct factors + else: + break # t is not divisible by 2, break and choose another g + return sorted([p, q]) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/rsa_key_generator.py b/ciphers/rsa_key_generator.py index 541e90d6e884..5514c69917bf 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -1,46 +1,59 @@ -from __future__ import print_function -import random, sys, os -import rabin_miller as rabinMiller, cryptomath_module as cryptoMath +import os +import random +import sys + +import cryptomath_module as cryptoMath +import rabin_miller as rabinMiller + def main(): - print('Making key files...') - makeKeyFiles('rsa', 1024) - print('Key files generation successful.') + print("Making key files...") + makeKeyFiles("rsa", 1024) + print("Key files generation successful.") + def generateKey(keySize): - print('Generating prime p...') + print("Generating prime p...") p = rabinMiller.generateLargePrime(keySize) - print('Generating prime q...') + print("Generating prime q...") q = rabinMiller.generateLargePrime(keySize) n = p * q - print('Generating e that is relatively prime to (p - 1) * (q - 1)...') + print("Generating e that is relatively prime to (p - 1) * (q - 1)...") while True: e = random.randrange(2 ** (keySize - 1), 2 ** (keySize)) if cryptoMath.gcd(e, (p - 1) * (q - 1)) == 1: break - print('Calculating d that is mod inverse of e...') + print("Calculating d that is mod inverse of e...") d = cryptoMath.findModInverse(e, (p - 1) * (q - 1)) publicKey = (n, e) privateKey = (n, d) return (publicKey, privateKey) + def makeKeyFiles(name, keySize): - if os.path.exists('%s_pubkey.txt' % (name)) or os.path.exists('%s_privkey.txt' % (name)): - print('\nWARNING:') - print('"%s_pubkey.txt" or "%s_privkey.txt" already exists. \nUse a different name or delete these files and re-run this program.' % (name, name)) + if os.path.exists("%s_pubkey.txt" % (name)) or os.path.exists( + "%s_privkey.txt" % (name) + ): + print("\nWARNING:") + print( + '"%s_pubkey.txt" or "%s_privkey.txt" already exists. \n' + "Use a different name or delete these files and re-run this program." + % (name, name) + ) sys.exit() publicKey, privateKey = generateKey(keySize) - print('\nWriting public key to file %s_pubkey.txt...' % name) - with open('%s_pubkey.txt' % name, 'w') as fo: - fo.write('%s,%s,%s' % (keySize, publicKey[0], publicKey[1])) + print("\nWriting public key to file %s_pubkey.txt..." % name) + with open("%s_pubkey.txt" % name, "w") as out_file: + out_file.write("{},{},{}".format(keySize, publicKey[0], publicKey[1])) + + print("Writing private key to file %s_privkey.txt..." % name) + with open("%s_privkey.txt" % name, "w") as out_file: + out_file.write("{},{},{}".format(keySize, privateKey[0], privateKey[1])) - print('Writing private key to file %s_privkey.txt...' % name) - with open('%s_privkey.txt' % name, 'w') as fo: - fo.write('%s,%s,%s' % (keySize, privateKey[0], privateKey[1])) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/shuffled_shift_cipher.py b/ciphers/shuffled_shift_cipher.py new file mode 100644 index 000000000000..be5c6caf845b --- /dev/null +++ b/ciphers/shuffled_shift_cipher.py @@ -0,0 +1,177 @@ +import random +import string + + +class ShuffledShiftCipher: + """ + This algorithm uses the Caesar Cipher algorithm but removes the option to + use brute force to decrypt the message. + + The passcode is a a random password from the selection buffer of + 1. uppercase letters of the English alphabet + 2. lowercase letters of the English alphabet + 3. digits from 0 to 9 + + Using unique characters from the passcode, the normal list of characters, + that can be allowed in the plaintext, is pivoted and shuffled. Refer to docstring + of __make_key_list() to learn more about the shuffling. + + Then, using the passcode, a number is calculated which is used to encrypt the + plaintext message with the normal shift cipher method, only in this case, the + reference, to look back at while decrypting, is shuffled. + + Each cipher object can possess an optional argument as passcode, without which a + new passcode is generated for that object automatically. + cip1 = ShuffledShiftCipher('d4usr9TWxw9wMD') + cip2 = ShuffledShiftCipher() + """ + + def __init__(self, passcode: str = None): + """ + Initializes a cipher object with a passcode as it's entity + Note: No new passcode is generated if user provides a passcode + while creating the object + """ + self.__passcode = passcode or self.__passcode_creator() + self.__key_list = self.__make_key_list() + self.__shift_key = self.__make_shift_key() + + def __str__(self): + """ + :return: passcode of the cipher object + """ + return "Passcode is: " + "".join(self.__passcode) + + def __neg_pos(self, iterlist: list) -> list: + """ + Mutates the list by changing the sign of each alternate element + + :param iterlist: takes a list iterable + :return: the mutated list + + """ + for i in range(1, len(iterlist), 2): + iterlist[i] *= -1 + return iterlist + + def __passcode_creator(self) -> list: + """ + Creates a random password from the selection buffer of + 1. uppercase letters of the English alphabet + 2. lowercase letters of the English alphabet + 3. digits from 0 to 9 + + :rtype: list + :return: a password of a random length between 10 to 20 + """ + choices = string.ascii_letters + string.digits + password = [random.choice(choices) for i in range(random.randint(10, 20))] + return password + + def __make_key_list(self) -> list: + """ + Shuffles the ordered character choices by pivoting at breakpoints + Breakpoints are the set of characters in the passcode + + eg: + if, ABCDEFGHIJKLMNOPQRSTUVWXYZ are the possible characters + and CAMERA is the passcode + then, breakpoints = [A,C,E,M,R] # sorted set of characters from passcode + shuffled parts: [A,CB,ED,MLKJIHGF,RQPON,ZYXWVUTS] + shuffled __key_list : ACBEDMLKJIHGFRQPONZYXWVUTS + + Shuffling only 26 letters of the english alphabet can generate 26! + combinations for the shuffled list. In the program we consider, a set of + 97 characters (including letters, digits, punctuation and whitespaces), + thereby creating a possibility of 97! combinations (which is a 152 digit number in itself), + thus diminishing the possibility of a brute force approach. Moreover, + shift keys even introduce a multiple of 26 for a brute force approach + for each of the already 97! combinations. + """ + # key_list_options contain nearly all printable except few elements from string.whitespace + key_list_options = ( + string.ascii_letters + string.digits + string.punctuation + " \t\n" + ) + + keys_l = [] + + # creates points known as breakpoints to break the key_list_options at those points and pivot each substring + breakpoints = sorted(set(self.__passcode)) + temp_list = [] + + # algorithm for creating a new shuffled list, keys_l, out of key_list_options + for i in key_list_options: + temp_list.extend(i) + + # checking breakpoints at which to pivot temporary sublist and add it into keys_l + if i in breakpoints or i == key_list_options[-1]: + keys_l.extend(temp_list[::-1]) + temp_list = [] + + # returning a shuffled keys_l to prevent brute force guessing of shift key + return keys_l + + def __make_shift_key(self) -> int: + """ + sum() of the mutated list of ascii values of all characters where the + mutated list is the one returned by __neg_pos() + """ + num = sum(self.__neg_pos([ord(x) for x in self.__passcode])) + return num if num > 0 else len(self.__passcode) + + def decrypt(self, encoded_message: str) -> str: + """ + Performs shifting of the encoded_message w.r.t. the shuffled __key_list + to create the decoded_message + + >>> ssc = ShuffledShiftCipher('4PYIXyqeQZr44') + >>> ssc.decrypt("d>**-1z6&'5z'5z:z+-='$'>=zp:>5:#z<'.&>#") + 'Hello, this is a modified Caesar cipher' + + """ + decoded_message = "" + + # decoding shift like Caesar cipher algorithm implementing negative shift or reverse shift or left shift + for i in encoded_message: + position = self.__key_list.index(i) + decoded_message += self.__key_list[ + (position - self.__shift_key) % -len(self.__key_list) + ] + + return decoded_message + + def encrypt(self, plaintext: str) -> str: + """ + Performs shifting of the plaintext w.r.t. the shuffled __key_list + to create the encoded_message + + >>> ssc = ShuffledShiftCipher('4PYIXyqeQZr44') + >>> ssc.encrypt('Hello, this is a modified Caesar cipher') + "d>**-1z6&'5z'5z:z+-='$'>=zp:>5:#z<'.&>#" + + """ + encoded_message = "" + + # encoding shift like Caesar cipher algorithm implementing positive shift or forward shift or right shift + for i in plaintext: + position = self.__key_list.index(i) + encoded_message += self.__key_list[ + (position + self.__shift_key) % len(self.__key_list) + ] + + return encoded_message + + +def test_end_to_end(msg: str = "Hello, this is a modified Caesar cipher"): + """ + >>> test_end_to_end() + 'Hello, this is a modified Caesar cipher' + """ + cip1 = ShuffledShiftCipher() + return cip1.decrypt(cip1.encrypt(msg)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/simple_keyword_cypher.py b/ciphers/simple_keyword_cypher.py new file mode 100644 index 000000000000..71c3083e9dfc --- /dev/null +++ b/ciphers/simple_keyword_cypher.py @@ -0,0 +1,90 @@ +def remove_duplicates(key: str) -> str: + """ + Removes duplicate alphabetic characters in a keyword (letter is ignored after its + first appearance). + :param key: Keyword to use + :return: String with duplicates removed + >>> remove_duplicates('Hello World!!') + 'Helo Wrd' + """ + + key_no_dups = "" + for ch in key: + if ch == " " or ch not in key_no_dups and ch.isalpha(): + key_no_dups += ch + return key_no_dups + + +def create_cipher_map(key: str) -> dict: + """ + Returns a cipher map given a keyword. + :param key: keyword to use + :return: dictionary cipher map + """ + # Create alphabet list + alphabet = [chr(i + 65) for i in range(26)] + # Remove duplicate characters from key + key = remove_duplicates(key.upper()) + offset = len(key) + # First fill cipher with key characters + cipher_alphabet = {alphabet[i]: char for i, char in enumerate(key)} + # Then map remaining characters in alphabet to + # the alphabet from the beginning + for i in range(len(cipher_alphabet), 26): + char = alphabet[i - offset] + # Ensure we are not mapping letters to letters previously mapped + while char in key: + offset -= 1 + char = alphabet[i - offset] + cipher_alphabet[alphabet[i]] = char + return cipher_alphabet + + +def encipher(message: str, cipher_map: dict) -> str: + """ + Enciphers a message given a cipher map. + :param message: Message to encipher + :param cipher_map: Cipher map + :return: enciphered string + >>> encipher('Hello World!!', create_cipher_map('Goodbye!!')) + 'CYJJM VMQJB!!' + """ + return "".join(cipher_map.get(ch, ch) for ch in message.upper()) + + +def decipher(message: str, cipher_map: dict) -> str: + """ + Deciphers a message given a cipher map + :param message: Message to decipher + :param cipher_map: Dictionary mapping to use + :return: Deciphered string + >>> cipher_map = create_cipher_map('Goodbye!!') + >>> decipher(encipher('Hello World!!', cipher_map), cipher_map) + 'HELLO WORLD!!' + """ + # Reverse our cipher mappings + rev_cipher_map = {v: k for k, v in cipher_map.items()} + return "".join(rev_cipher_map.get(ch, ch) for ch in message.upper()) + + +def main(): + """ + Handles I/O + :return: void + """ + message = input("Enter message to encode or decode: ").strip() + key = input("Enter keyword: ").strip() + option = input("Encipher or decipher? E/D:").strip()[0].lower() + try: + func = {"e": encipher, "d": decipher}[option] + except KeyError: + raise KeyError("invalid input option") + cipher_map = create_cipher_map(key) + print(func(message, cipher_map)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/ciphers/simple_substitution_cipher.py b/ciphers/simple_substitution_cipher.py index 1bdd7dc04a57..7da18482db8c 100644 --- a/ciphers/simple_substitution_cipher.py +++ b/ciphers/simple_substitution_cipher.py @@ -1,24 +1,25 @@ -from __future__ import print_function import sys, random -LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + def main(): - message = input('Enter message: ') - key = 'LFWOAYUISVKMNXPBDCRJTQEGHZ' - resp = input('Encrypt/Decrypt [e/d]: ') + message = input("Enter message: ") + key = "LFWOAYUISVKMNXPBDCRJTQEGHZ" + resp = input("Encrypt/Decrypt [e/d]: ") checkValidKey(key) - if resp.lower().startswith('e'): - mode = 'encrypt' + if resp.lower().startswith("e"): + mode = "encrypt" translated = encryptMessage(key, message) - elif resp.lower().startswith('d'): - mode = 'decrypt' + elif resp.lower().startswith("d"): + mode = "decrypt" translated = decryptMessage(key, message) - print('\n%sion: \n%s' % (mode.title(), translated)) - + print("\n{}ion: \n{}".format(mode.title(), translated)) + + def checkValidKey(key): keyList = list(key) lettersList = list(LETTERS) @@ -26,30 +27,33 @@ def checkValidKey(key): lettersList.sort() if keyList != lettersList: - sys.exit('Error in the key or symbol set.') + sys.exit("Error in the key or symbol set.") + def encryptMessage(key, message): """ >>> encryptMessage('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Harshil Darji') 'Ilcrism Olcvs' """ - return translateMessage(key, message, 'encrypt') + return translateMessage(key, message, "encrypt") + def decryptMessage(key, message): """ >>> decryptMessage('LFWOAYUISVKMNXPBDCRJTQEGHZ', 'Ilcrism Olcvs') 'Harshil Darji' """ - return translateMessage(key, message, 'decrypt') + return translateMessage(key, message, "decrypt") + def translateMessage(key, message, mode): - translated = '' + translated = "" charsA = LETTERS charsB = key - if mode == 'decrypt': + if mode == "decrypt": charsA, charsB = charsB, charsA - + for symbol in message: if symbol.upper() in charsA: symIndex = charsA.find(symbol.upper()) @@ -62,10 +66,12 @@ def translateMessage(key, message, mode): return translated + def getRandomKey(): key = list(LETTERS) random.shuffle(key) - return ''.join(key) + return "".join(key) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/trafid_cipher.py b/ciphers/trafid_cipher.py index 0453272f26a0..f1c954b5c34f 100644 --- a/ciphers/trafid_cipher.py +++ b/ciphers/trafid_cipher.py @@ -1,9 +1,10 @@ -#https://en.wikipedia.org/wiki/Trifid_cipher +# https://en.wikipedia.org/wiki/Trifid_cipher + def __encryptPart(messagePart, character2Number): one, two, three = "", "", "" tmp = [] - + for character in messagePart: tmp.append(character2Number[character]) @@ -11,8 +12,9 @@ def __encryptPart(messagePart, character2Number): one += each[0] two += each[1] three += each[2] - - return one+two+three + + return one + two + three + def __decryptPart(messagePart, character2Number): tmp, thisPart = "", "" @@ -25,62 +27,94 @@ def __decryptPart(messagePart, character2Number): tmp += digit if len(tmp) == len(messagePart): result.append(tmp) - tmp = "" + tmp = "" return result[0], result[1], result[2] + def __prepare(message, alphabet): - #Validate message and alphabet, set to upper and remove spaces + # Validate message and alphabet, set to upper and remove spaces alphabet = alphabet.replace(" ", "").upper() message = message.replace(" ", "").upper() - #Check length and characters + # Check length and characters if len(alphabet) != 27: raise KeyError("Length of alphabet has to be 27.") for each in message: if each not in alphabet: raise ValueError("Each message character has to be included in alphabet!") - #Generate dictionares - numbers = ("111","112","113","121","122","123","131","132","133","211","212","213","221","222","223","231","232","233","311","312","313","321","322","323","331","332","333") + # Generate dictionares + numbers = ( + "111", + "112", + "113", + "121", + "122", + "123", + "131", + "132", + "133", + "211", + "212", + "213", + "221", + "222", + "223", + "231", + "232", + "233", + "311", + "312", + "313", + "321", + "322", + "323", + "331", + "332", + "333", + ) character2Number = {} number2Character = {} for letter, number in zip(alphabet, numbers): character2Number[letter] = number number2Character[number] = letter - + return message, alphabet, character2Number, number2Character -def encryptMessage(message, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period=5): + +def encryptMessage(message, alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period=5): message, alphabet, character2Number, number2Character = __prepare(message, alphabet) encrypted, encrypted_numeric = "", "" - for i in range(0, len(message)+1, period): - encrypted_numeric += __encryptPart(message[i:i+period], character2Number) - + for i in range(0, len(message) + 1, period): + encrypted_numeric += __encryptPart(message[i : i + period], character2Number) + for i in range(0, len(encrypted_numeric), 3): - encrypted += number2Character[encrypted_numeric[i:i+3]] + encrypted += number2Character[encrypted_numeric[i : i + 3]] return encrypted -def decryptMessage(message, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period=5): + +def decryptMessage(message, alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period=5): message, alphabet, character2Number, number2Character = __prepare(message, alphabet) decrypted_numeric = [] decrypted = "" - for i in range(0, len(message)+1, period): - a,b,c = __decryptPart(message[i:i+period], character2Number) - + for i in range(0, len(message) + 1, period): + a, b, c = __decryptPart(message[i : i + period], character2Number) + for j in range(0, len(a)): - decrypted_numeric.append(a[j]+b[j]+c[j]) + decrypted_numeric.append(a[j] + b[j] + c[j]) for each in decrypted_numeric: decrypted += number2Character[each] return decrypted -if __name__ == '__main__': + +if __name__ == "__main__": msg = "DEFEND THE EAST WALL OF THE CASTLE." - encrypted = encryptMessage(msg,"EPSDUCVWYM.ZLKXNBTFGORIJHAQ") + encrypted = encryptMessage(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") decrypted = decryptMessage(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ") - print ("Encrypted: {}\nDecrypted: {}".format(encrypted, decrypted)) \ No newline at end of file + print(f"Encrypted: {encrypted}\nDecrypted: {decrypted}") diff --git a/ciphers/transposition_cipher.py b/ciphers/transposition_cipher.py index dbb358315d22..4bba88955433 100644 --- a/ciphers/transposition_cipher.py +++ b/ciphers/transposition_cipher.py @@ -1,31 +1,40 @@ -from __future__ import print_function import math +""" +In cryptography, the TRANSPOSITION cipher is a method of encryption where the +positions of plaintext are shifted a certain number(determined by the key) that +follows a regular system that results in the permuted text, known as the encrypted +text. The type of transposition cipher demonstrated under is the ROUTE cipher. +""" + + def main(): - message = input('Enter message: ') - key = int(input('Enter key [2-%s]: ' % (len(message) - 1))) - mode = input('Encryption/Decryption [e/d]: ') + message = input("Enter message: ") + key = int(input("Enter key [2-%s]: " % (len(message) - 1))) + mode = input("Encryption/Decryption [e/d]: ") - if mode.lower().startswith('e'): + if mode.lower().startswith("e"): text = encryptMessage(key, message) - elif mode.lower().startswith('d'): + elif mode.lower().startswith("d"): text = decryptMessage(key, message) # Append pipe symbol (vertical bar) to identify spaces at the end. - print('Output:\n%s' %(text + '|')) + print("Output:\n%s" % (text + "|")) + def encryptMessage(key, message): """ >>> encryptMessage(6, 'Harshil Darji') 'Hlia rDsahrij' """ - cipherText = [''] * key + cipherText = [""] * key for col in range(key): pointer = col while pointer < len(message): cipherText[col] += message[pointer] pointer += key - return ''.join(cipherText) + return "".join(cipherText) + def decryptMessage(key, message): """ @@ -36,19 +45,26 @@ def decryptMessage(key, message): numRows = key numShadedBoxes = (numCols * numRows) - len(message) plainText = [""] * numCols - col = 0; row = 0; + col = 0 + row = 0 for symbol in message: plainText[col] += symbol col += 1 - if (col == numCols) or (col == numCols - 1) and (row >= numRows - numShadedBoxes): + if ( + (col == numCols) + or (col == numCols - 1) + and (row >= numRows - numShadedBoxes) + ): col = 0 row += 1 return "".join(plainText) -if __name__ == '__main__': + +if __name__ == "__main__": import doctest + doctest.testmod() main() diff --git a/ciphers/transposition_cipher_encrypt_decrypt_file.py b/ciphers/transposition_cipher_encrypt_decrypt_file.py index a186cf81cde7..775df354e117 100644 --- a/ciphers/transposition_cipher_encrypt_decrypt_file.py +++ b/ciphers/transposition_cipher_encrypt_decrypt_file.py @@ -1,37 +1,38 @@ -from __future__ import print_function import time, os, sys import transposition_cipher as transCipher + def main(): - inputFile = 'Prehistoric Men.txt' - outputFile = 'Output.txt' - key = int(input('Enter key: ')) - mode = input('Encrypt/Decrypt [e/d]: ') + inputFile = "Prehistoric Men.txt" + outputFile = "Output.txt" + key = int(input("Enter key: ")) + mode = input("Encrypt/Decrypt [e/d]: ") if not os.path.exists(inputFile): - print('File %s does not exist. Quitting...' % inputFile) + print("File %s does not exist. Quitting..." % inputFile) sys.exit() if os.path.exists(outputFile): - print('Overwrite %s? [y/n]' % outputFile) - response = input('> ') - if not response.lower().startswith('y'): + print("Overwrite %s? [y/n]" % outputFile) + response = input("> ") + if not response.lower().startswith("y"): sys.exit() - + startTime = time.time() - if mode.lower().startswith('e'): + if mode.lower().startswith("e"): with open(inputFile) as f: content = f.read() translated = transCipher.encryptMessage(key, content) - elif mode.lower().startswith('d'): + elif mode.lower().startswith("d"): with open(outputFile) as f: content = f.read() - translated =transCipher .decryptMessage(key, content) + translated = transCipher.decryptMessage(key, content) - with open(outputFile, 'w') as outputObj: + with open(outputFile, "w") as outputObj: outputObj.write(translated) - + totalTime = round(time.time() - startTime, 2) - print(('Done (', totalTime, 'seconds )')) - -if __name__ == '__main__': + print(("Done (", totalTime, "seconds )")) + + +if __name__ == "__main__": main() diff --git a/ciphers/vigenere_cipher.py b/ciphers/vigenere_cipher.py index 5d5be0792835..6c10e7d773f2 100644 --- a/ciphers/vigenere_cipher.py +++ b/ciphers/vigenere_cipher.py @@ -1,34 +1,37 @@ -from __future__ import print_function -LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + def main(): - message = input('Enter message: ') - key = input('Enter key [alphanumeric]: ') - mode = input('Encrypt/Decrypt [e/d]: ') + message = input("Enter message: ") + key = input("Enter key [alphanumeric]: ") + mode = input("Encrypt/Decrypt [e/d]: ") - if mode.lower().startswith('e'): - mode = 'encrypt' + if mode.lower().startswith("e"): + mode = "encrypt" translated = encryptMessage(key, message) - elif mode.lower().startswith('d'): - mode = 'decrypt' + elif mode.lower().startswith("d"): + mode = "decrypt" translated = decryptMessage(key, message) - print('\n%sed message:' % mode.title()) + print("\n%sed message:" % mode.title()) print(translated) + def encryptMessage(key, message): - ''' + """ >>> encryptMessage('HDarji', 'This is Harshil Darji from Dharmaj.') 'Akij ra Odrjqqs Gaisq muod Mphumrs.' - ''' - return translateMessage(key, message, 'encrypt') + """ + return translateMessage(key, message, "encrypt") + def decryptMessage(key, message): - ''' + """ >>> decryptMessage('HDarji', 'Akij ra Odrjqqs Gaisq muod Mphumrs.') 'This is Harshil Darji from Dharmaj.' - ''' - return translateMessage(key, message, 'decrypt') + """ + return translateMessage(key, message, "decrypt") + def translateMessage(key, message, mode): translated = [] @@ -38,9 +41,9 @@ def translateMessage(key, message, mode): for symbol in message: num = LETTERS.find(symbol.upper()) if num != -1: - if mode == 'encrypt': + if mode == "encrypt": num += LETTERS.find(key[keyIndex]) - elif mode == 'decrypt': + elif mode == "decrypt": num -= LETTERS.find(key[keyIndex]) num %= len(LETTERS) @@ -55,7 +58,8 @@ def translateMessage(key, message, mode): keyIndex = 0 else: translated.append(symbol) - return ''.join(translated) + return "".join(translated) + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ciphers/xor_cipher.py b/ciphers/xor_cipher.py index 727fac3b0703..0fcfbb0b9ae2 100644 --- a/ciphers/xor_cipher.py +++ b/ciphers/xor_cipher.py @@ -1,186 +1,182 @@ """ - author: Christian Bender - date: 21.12.2017 - class: XORCipher - - This class implements the XOR-cipher algorithm and provides - some useful methods for encrypting and decrypting strings and - files. - - Overview about methods - - - encrypt : list of char - - decrypt : list of char - - encrypt_string : str - - decrypt_string : str - - encrypt_file : boolean - - decrypt_file : boolean + author: Christian Bender + date: 21.12.2017 + class: XORCipher + + This class implements the XOR-cipher algorithm and provides + some useful methods for encrypting and decrypting strings and + files. + + Overview about methods + + - encrypt : list of char + - decrypt : list of char + - encrypt_string : str + - decrypt_string : str + - encrypt_file : boolean + - decrypt_file : boolean """ -class XORCipher(object): - def __init__(self, key = 0): - """ - simple constructor that receives a key or uses - default key = 0 - """ - #private field - self.__key = key +class XORCipher: + def __init__(self, key=0): + """ + simple constructor that receives a key or uses + default key = 0 + """ - def encrypt(self, content, key): - """ - input: 'content' of type string and 'key' of type int - output: encrypted string 'content' as a list of chars - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + # private field + self.__key = key - # precondition - assert (isinstance(key,int) and isinstance(content,str)) + def encrypt(self, content, key): + """ + input: 'content' of type string and 'key' of type int + output: encrypted string 'content' as a list of chars + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - key = key or self.__key or 1 + # precondition + assert isinstance(key, int) and isinstance(content, str) - # make sure key can be any size - while (key > 255): - key -= 255 + key = key or self.__key or 1 - # This will be returned - ans = [] + # make sure key can be any size + while key > 255: + key -= 255 - for ch in content: - ans.append(chr(ord(ch) ^ key)) + # This will be returned + ans = [] - return ans + for ch in content: + ans.append(chr(ord(ch) ^ key)) - def decrypt(self,content,key): - """ - input: 'content' of type list and 'key' of type int - output: decrypted string 'content' as a list of chars - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + return ans - # precondition - assert (isinstance(key,int) and isinstance(content,list)) + def decrypt(self, content, key): + """ + input: 'content' of type list and 'key' of type int + output: decrypted string 'content' as a list of chars + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - key = key or self.__key or 1 + # precondition + assert isinstance(key, int) and isinstance(content, list) - # make sure key can be any size - while (key > 255): - key -= 255 + key = key or self.__key or 1 - # This will be returned - ans = [] + # make sure key can be any size + while key > 255: + key -= 255 - for ch in content: - ans.append(chr(ord(ch) ^ key)) + # This will be returned + ans = [] - return ans + for ch in content: + ans.append(chr(ord(ch) ^ key)) + return ans - def encrypt_string(self,content, key = 0): - """ - input: 'content' of type string and 'key' of type int - output: encrypted string 'content' - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + def encrypt_string(self, content, key=0): + """ + input: 'content' of type string and 'key' of type int + output: encrypted string 'content' + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - # precondition - assert (isinstance(key,int) and isinstance(content,str)) + # precondition + assert isinstance(key, int) and isinstance(content, str) - key = key or self.__key or 1 + key = key or self.__key or 1 - # make sure key can be any size - while (key > 255): - key -= 255 + # make sure key can be any size + while key > 255: + key -= 255 - # This will be returned - ans = "" + # This will be returned + ans = "" - for ch in content: - ans += chr(ord(ch) ^ key) + for ch in content: + ans += chr(ord(ch) ^ key) - return ans + return ans - def decrypt_string(self,content,key = 0): - """ - input: 'content' of type string and 'key' of type int - output: decrypted string 'content' - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + def decrypt_string(self, content, key=0): + """ + input: 'content' of type string and 'key' of type int + output: decrypted string 'content' + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - # precondition - assert (isinstance(key,int) and isinstance(content,str)) + # precondition + assert isinstance(key, int) and isinstance(content, str) - key = key or self.__key or 1 + key = key or self.__key or 1 - # make sure key can be any size - while (key > 255): - key -= 255 + # make sure key can be any size + while key > 255: + key -= 255 - # This will be returned - ans = "" - - for ch in content: - ans += chr(ord(ch) ^ key) + # This will be returned + ans = "" - return ans + for ch in content: + ans += chr(ord(ch) ^ key) + return ans - def encrypt_file(self, file, key = 0): - """ - input: filename (str) and a key (int) - output: returns true if encrypt process was - successful otherwise false - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + def encrypt_file(self, file, key=0): + """ + input: filename (str) and a key (int) + output: returns true if encrypt process was + successful otherwise false + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - #precondition - assert (isinstance(file,str) and isinstance(key,int)) + # precondition + assert isinstance(file, str) and isinstance(key, int) - try: - with open(file,"r") as fin: - with open("encrypt.out","w+") as fout: + try: + with open(file, "r") as fin: + with open("encrypt.out", "w+") as fout: - # actual encrypt-process - for line in fin: - fout.write(self.encrypt_string(line,key)) + # actual encrypt-process + for line in fin: + fout.write(self.encrypt_string(line, key)) - except: - return False + except IOError: + return False - return True + return True + def decrypt_file(self, file, key): + """ + input: filename (str) and a key (int) + output: returns true if decrypt process was + successful otherwise false + if key not passed the method uses the key by the constructor. + otherwise key = 1 + """ - def decrypt_file(self,file, key): - """ - input: filename (str) and a key (int) - output: returns true if decrypt process was - successful otherwise false - if key not passed the method uses the key by the constructor. - otherwise key = 1 - """ + # precondition + assert isinstance(file, str) and isinstance(key, int) - #precondition - assert (isinstance(file,str) and isinstance(key,int)) + try: + with open(file, "r") as fin: + with open("decrypt.out", "w+") as fout: - try: - with open(file,"r") as fin: - with open("decrypt.out","w+") as fout: - - # actual encrypt-process - for line in fin: - fout.write(self.decrypt_string(line,key)) - - except: - return False - - return True + # actual encrypt-process + for line in fin: + fout.write(self.decrypt_string(line, key)) + except IOError: + return False + return True # Tests @@ -188,22 +184,22 @@ def decrypt_file(self,file, key): # key = 67 # # test enrcypt -# print crypt.encrypt("hallo welt",key) +# print(crypt.encrypt("hallo welt",key)) # # test decrypt -# print crypt.decrypt(crypt.encrypt("hallo welt",key), key) +# print(crypt.decrypt(crypt.encrypt("hallo welt",key), key)) # # test encrypt_string -# print crypt.encrypt_string("hallo welt",key) +# print(crypt.encrypt_string("hallo welt",key)) # # test decrypt_string -# print crypt.decrypt_string(crypt.encrypt_string("hallo welt",key),key) +# print(crypt.decrypt_string(crypt.encrypt_string("hallo welt",key),key)) # if (crypt.encrypt_file("test.txt",key)): -# print "encrypt successful" +# print("encrypt successful") # else: -# print "encrypt unsuccessful" +# print("encrypt unsuccessful") # if (crypt.decrypt_file("encrypt.out",key)): -# print "decrypt successful" +# print("decrypt successful") # else: -# print "decrypt unsuccessful" \ No newline at end of file +# print("decrypt unsuccessful") diff --git a/compression/burrows_wheeler.py b/compression/burrows_wheeler.py index fabeab39adf8..2a08c6092cda 100644 --- a/compression/burrows_wheeler.py +++ b/compression/burrows_wheeler.py @@ -135,21 +135,14 @@ def reverse_bwt(bwt_string: str, idx_original_string: int) -> str: idx_original_string = int(idx_original_string) except ValueError: raise TypeError( - ( - "The parameter idx_original_string type must be int or passive" - " of cast to int." - ) + "The parameter idx_original_string type must be int or passive" + " of cast to int." ) if idx_original_string < 0: - raise ValueError( - "The parameter idx_original_string must not be lower than 0." - ) + raise ValueError("The parameter idx_original_string must not be lower than 0.") if idx_original_string >= len(bwt_string): raise ValueError( - ( - "The parameter idx_original_string must be lower than" - " len(bwt_string)." - ) + "The parameter idx_original_string must be lower than" " len(bwt_string)." ) ordered_rotations = [""] * len(bwt_string) @@ -164,13 +157,11 @@ def reverse_bwt(bwt_string: str, idx_original_string: int) -> str: entry_msg = "Provide a string that I will generate its BWT transform: " s = input(entry_msg).strip() result = bwt_transform(s) - bwt_output_msg = "Burrows Wheeler tranform for string '{}' results in '{}'" + bwt_output_msg = "Burrows Wheeler transform for string '{}' results in '{}'" print(bwt_output_msg.format(s, result["bwt_string"])) - original_string = reverse_bwt( - result["bwt_string"], result["idx_original_string"] - ) + original_string = reverse_bwt(result["bwt_string"], result["idx_original_string"]) fmt = ( - "Reversing Burrows Wheeler tranform for entry '{}' we get original" + "Reversing Burrows Wheeler transform for entry '{}' we get original" " string '{}'" ) print(fmt.format(result["bwt_string"], original_string)) diff --git a/compression/huffman.py b/compression/huffman.py index 7417551ba209..3a3cbfa4b0c6 100644 --- a/compression/huffman.py +++ b/compression/huffman.py @@ -1,5 +1,6 @@ import sys + class Letter: def __init__(self, letter, freq): self.letter = letter @@ -7,7 +8,7 @@ def __init__(self, letter, freq): self.bitstring = "" def __repr__(self): - return f'{self.letter}:{self.freq}' + return f"{self.letter}:{self.freq}" class TreeNode: @@ -20,7 +21,7 @@ def __init__(self, freq, left, right): def parse_file(file_path): """ Read the file and build a dict of all letters and their - frequences, then convert the dict into a list of Letters. + frequencies, then convert the dict into a list of Letters. """ chars = {} with open(file_path) as f: @@ -31,6 +32,7 @@ def parse_file(file_path): chars[c] = chars[c] + 1 if c in chars.keys() else 1 return sorted([Letter(c, f) for c, f in chars.items()], key=lambda l: l.freq) + def build_tree(letters): """ Run through the list of Letters and build the min heap @@ -45,6 +47,7 @@ def build_tree(letters): letters.sort(key=lambda l: l.freq) return letters[0] + def traverse_tree(root, bitstring): """ Recursively traverse the Huffman Tree to set each @@ -58,6 +61,7 @@ def traverse_tree(root, bitstring): letters += traverse_tree(root.right, bitstring + "1") return letters + def huffman(file_path): """ Parse the file, build the tree, then run through the file @@ -67,7 +71,7 @@ def huffman(file_path): letters_list = parse_file(file_path) root = build_tree(letters_list) letters = traverse_tree(root, "") - print(f'Huffman Coding of {file_path}: ') + print(f"Huffman Coding of {file_path}: ") with open(file_path) as f: while True: c = f.read(1) @@ -77,6 +81,7 @@ def huffman(file_path): print(le.bitstring, end=" ") print() + if __name__ == "__main__": # pass the file path to the huffman function huffman(sys.argv[1]) diff --git a/compression/peak_signal_to_noise_ratio.py b/compression/peak_signal_to_noise_ratio.py index b0efb1462dcc..f4a1ca41e14d 100644 --- a/compression/peak_signal_to_noise_ratio.py +++ b/compression/peak_signal_to_noise_ratio.py @@ -1,6 +1,6 @@ """ - Peak signal-to-noise ratio - PSNR - https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio - Soruce: https://tutorials.techonical.com/how-to-calculate-psnr-value-of-two-images-using-python/ + Peak signal-to-noise ratio - PSNR - https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio + Source: https://tutorials.techonical.com/how-to-calculate-psnr-value-of-two-images-using-python/ """ import math @@ -9,6 +9,7 @@ import cv2 import numpy as np + def psnr(original, contrast): mse = np.mean((original - contrast) ** 2) if mse == 0: @@ -21,11 +22,13 @@ def psnr(original, contrast): def main(): dir_path = os.path.dirname(os.path.realpath(__file__)) # Loading images (original image and compressed image) - original = cv2.imread(os.path.join(dir_path, 'image_data/original_image.png')) - contrast = cv2.imread(os.path.join(dir_path, 'image_data/compressed_image.png'), 1) + original = cv2.imread(os.path.join(dir_path, "image_data/original_image.png")) + contrast = cv2.imread(os.path.join(dir_path, "image_data/compressed_image.png"), 1) - original2 = cv2.imread(os.path.join(dir_path, 'image_data/PSNR-example-base.png')) - contrast2 = cv2.imread(os.path.join(dir_path, 'image_data/PSNR-example-comp-10.jpg'), 1) + original2 = cv2.imread(os.path.join(dir_path, "image_data/PSNR-example-base.png")) + contrast2 = cv2.imread( + os.path.join(dir_path, "image_data/PSNR-example-comp-10.jpg"), 1 + ) # Value expected: 29.73dB print("-- First Test --") @@ -36,5 +39,5 @@ def main(): print(f"PSNR value is {psnr(original2, contrast2)} dB") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/conversions/decimal_to_binary.py b/conversions/decimal_to_binary.py index 43ceee61a388..ad4ba166745d 100644 --- a/conversions/decimal_to_binary.py +++ b/conversions/decimal_to_binary.py @@ -2,24 +2,58 @@ def decimal_to_binary(num): - """Convert a Decimal Number to a Binary Number.""" + + """ + Convert a Integer Decimal Number to a Binary Number as str. + >>> decimal_to_binary(0) + '0b0' + >>> decimal_to_binary(2) + '0b10' + >>> decimal_to_binary(7) + '0b111' + >>> decimal_to_binary(35) + '0b100011' + >>> # negatives work too + >>> decimal_to_binary(-2) + '-0b10' + >>> # other floats will error + >>> decimal_to_binary(16.16) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + >>> # strings will error as well + >>> decimal_to_binary('0xfffff') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'str' object cannot be interpreted as an integer + """ + + if type(num) == float: + raise TypeError("'float' object cannot be interpreted as an integer") + if type(num) == str: + raise TypeError("'str' object cannot be interpreted as an integer") + + if num == 0: + return "0b0" + + negative = False + + if num < 0: + negative = True + num = -num + binary = [] while num > 0: binary.insert(0, num % 2) num >>= 1 - return "".join(str(e) for e in binary) + if negative: + return "-0b" + "".join(str(e) for e in binary) + + return "0b" + "".join(str(e) for e in binary) -def main(): - """Print binary equivelents of decimal numbers.""" - print("\n2 in binary is:") - print(decimal_to_binary(2)) # = 10 - print("\n7 in binary is:") - print(decimal_to_binary(7)) # = 111 - print("\n35 in binary is:") - print(decimal_to_binary(35)) # = 100011 - print("\n") +if __name__ == "__main__": + import doctest -if __name__ == '__main__': - main() + doctest.testmod() diff --git a/conversions/decimal_to_hexadecimal.py b/conversions/decimal_to_hexadecimal.py index f91fac063adc..a70e3c7b97bf 100644 --- a/conversions/decimal_to_hexadecimal.py +++ b/conversions/decimal_to_hexadecimal.py @@ -2,42 +2,76 @@ # set decimal value for each hexadecimal digit values = { - 0:'0', - 1:'1', - 2:'2', - 3:'3', - 4:'4', - 5:'5', - 6:'6', - 7:'7', - 8:'8', - 9:'9', - 10:'a', - 11:'b', - 12:'c', - 13:'d', - 14:'e', - 15:'f' + 0: "0", + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "a", + 11: "b", + 12: "c", + 13: "d", + 14: "e", + 15: "f", } + def decimal_to_hexadecimal(decimal): - """ take decimal value, return hexadecimal representation as str """ - hexadecimal = '' + """ + take integer decimal value, return hexadecimal representation as str beginning with 0x + >>> decimal_to_hexadecimal(5) + '0x5' + >>> decimal_to_hexadecimal(15) + '0xf' + >>> decimal_to_hexadecimal(37) + '0x25' + >>> decimal_to_hexadecimal(255) + '0xff' + >>> decimal_to_hexadecimal(4096) + '0x1000' + >>> decimal_to_hexadecimal(999098) + '0xf3eba' + >>> # negatives work too + >>> decimal_to_hexadecimal(-256) + '-0x100' + >>> # floats are acceptable if equivalent to an int + >>> decimal_to_hexadecimal(17.0) + '0x11' + >>> # other floats will error + >>> decimal_to_hexadecimal(16.16) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError + >>> # strings will error as well + >>> decimal_to_hexadecimal('0xfffff') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError + >>> # results are the same when compared to Python's default hex function + >>> decimal_to_hexadecimal(-256) == hex(-256) + True + """ + assert type(decimal) in (int, float) and decimal == int(decimal) + hexadecimal = "" + negative = False + if decimal < 0: + negative = True + decimal *= -1 while decimal > 0: - remainder = decimal % 16 - decimal -= remainder + decimal, remainder = divmod(decimal, 16) hexadecimal = values[remainder] + hexadecimal - decimal /= 16 + hexadecimal = "0x" + hexadecimal + if negative: + hexadecimal = "-" + hexadecimal return hexadecimal -def main(): - """ print test cases """ - print("5 in hexadecimal is", decimal_to_hexadecimal(5)) - print("15 in hexadecimal is", decimal_to_hexadecimal(15)) - print("37 in hexadecimal is", decimal_to_hexadecimal(37)) - print("255 in hexadecimal is", decimal_to_hexadecimal(255)) - print("4096 in hexadecimal is", decimal_to_hexadecimal(4096)) - print("999098 in hexadecimal is", decimal_to_hexadecimal(999098)) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/conversions/decimal_to_octal.py b/conversions/decimal_to_octal.py index 187a0300e33a..a89a2be982b8 100644 --- a/conversions/decimal_to_octal.py +++ b/conversions/decimal_to_octal.py @@ -6,8 +6,12 @@ # https://github.com/TheAlgorithms/Javascript/blob/master/Conversions/DecimalToOctal.js -def decimal_to_octal(num): - """Convert a Decimal Number to an Octal Number.""" +def decimal_to_octal(num: int) -> str: + """Convert a Decimal Number to an Octal Number. + + >>> all(decimal_to_octal(i) == oct(i) for i in (0, 2, 8, 64, 65, 216, 255, 256, 512)) + True + """ octal = 0 counter = 0 while num > 0: @@ -16,11 +20,11 @@ def decimal_to_octal(num): counter += 1 num = math.floor(num / 8) # basically /= 8 without remainder if any # This formatting removes trailing '.0' from `octal`. - return'{0:g}'.format(float(octal)) + return f"0o{int(octal)}" def main(): - """Print octal equivelents of decimal numbers.""" + """Print octal equivalents of decimal numbers.""" print("\n2 in octal is:") print(decimal_to_octal(2)) # = 2 print("\n8 in octal is:") @@ -34,5 +38,5 @@ def main(): print("\n") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/conversions/roman_to_integer.py b/conversions/roman_to_integer.py new file mode 100644 index 000000000000..ce52b6fb7cbb --- /dev/null +++ b/conversions/roman_to_integer.py @@ -0,0 +1,27 @@ +def roman_to_int(roman: str) -> int: + """ + LeetCode No. 13 Roman to Integer + Given a roman numeral, convert it to an integer. + Input is guaranteed to be within the range from 1 to 3999. + https://en.wikipedia.org/wiki/Roman_numerals + >>> tests = {"III": 3, "CLIV": 154, "MIX": 1009, "MMD": 2500, "MMMCMXCIX": 3999} + >>> all(roman_to_int(key) == value for key, value in tests.items()) + True + """ + vals = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} + total = 0 + place = 0 + while place < len(roman): + if (place + 1 < len(roman)) and (vals[roman[place]] < vals[roman[place + 1]]): + total += vals[roman[place + 1]] - vals[roman[place]] + place += 2 + else: + total += vals[roman[place]] + place += 1 + return total + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py index ff44963d1690..2df747c105ad 100644 --- a/data_structures/binary_tree/avl_tree.py +++ b/data_structures/binary_tree/avl_tree.py @@ -1,71 +1,87 @@ -# -*- coding: utf-8 -*- -''' +""" An auto-balanced binary tree! -''' +""" import math import random + + class my_queue: def __init__(self): self.data = [] self.head = 0 self.tail = 0 + def isEmpty(self): return self.head == self.tail - def push(self,data): + + def push(self, data): self.data.append(data) self.tail = self.tail + 1 + def pop(self): ret = self.data[self.head] self.head = self.head + 1 return ret + def count(self): return self.tail - self.head + def print(self): print(self.data) print("**************") - print(self.data[self.head:self.tail]) - + print(self.data[self.head : self.tail]) + + class my_node: - def __init__(self,data): + def __init__(self, data): self.data = data self.left = None self.right = None self.height = 1 + def getdata(self): return self.data + def getleft(self): return self.left + def getright(self): return self.right + def getheight(self): return self.height - def setdata(self,data): + + def setdata(self, data): self.data = data return - def setleft(self,node): + + def setleft(self, node): self.left = node return - def setright(self,node): + + def setright(self, node): self.right = node return - def setheight(self,height): + + def setheight(self, height): self.height = height return + def getheight(node): if node is None: return 0 return node.getheight() -def my_max(a,b): + +def my_max(a, b): if a > b: return a return b - def leftrotation(node): - r''' + r""" A B / \ / \ B C Bl A @@ -75,33 +91,35 @@ def leftrotation(node): UB UB = unbalanced node - ''' - print("left rotation node:",node.getdata()) + """ + print("left rotation node:", node.getdata()) ret = node.getleft() node.setleft(ret.getright()) ret.setright(node) - h1 = my_max(getheight(node.getright()),getheight(node.getleft())) + 1 + h1 = my_max(getheight(node.getright()), getheight(node.getleft())) + 1 node.setheight(h1) - h2 = my_max(getheight(ret.getright()),getheight(ret.getleft())) + 1 + h2 = my_max(getheight(ret.getright()), getheight(ret.getleft())) + 1 ret.setheight(h2) return ret + def rightrotation(node): - ''' + """ a mirror symmetry rotation of the leftrotation - ''' - print("right rotation node:",node.getdata()) + """ + print("right rotation node:", node.getdata()) ret = node.getright() node.setright(ret.getleft()) ret.setleft(node) - h1 = my_max(getheight(node.getright()),getheight(node.getleft())) + 1 + h1 = my_max(getheight(node.getright()), getheight(node.getleft())) + 1 node.setheight(h1) - h2 = my_max(getheight(ret.getright()),getheight(ret.getleft())) + 1 + h2 = my_max(getheight(ret.getright()), getheight(ret.getleft())) + 1 ret.setheight(h2) return ret + def rlrotation(node): - r''' + r""" A A Br / \ / \ / \ B C RR Br C LR B A @@ -110,51 +128,60 @@ def rlrotation(node): \ / UB Bl RR = rightrotation LR = leftrotation - ''' + """ node.setleft(rightrotation(node.getleft())) return leftrotation(node) + def lrrotation(node): node.setright(leftrotation(node.getright())) return rightrotation(node) -def insert_node(node,data): +def insert_node(node, data): if node is None: return my_node(data) if data < node.getdata(): - node.setleft(insert_node(node.getleft(),data)) - if getheight(node.getleft()) - getheight(node.getright()) == 2: #an unbalance detected - if data < node.getleft().getdata(): #new node is the left child of the left child + node.setleft(insert_node(node.getleft(), data)) + if ( + getheight(node.getleft()) - getheight(node.getright()) == 2 + ): # an unbalance detected + if ( + data < node.getleft().getdata() + ): # new node is the left child of the left child node = leftrotation(node) else: - node = rlrotation(node) #new node is the right child of the left child + node = rlrotation(node) # new node is the right child of the left child else: - node.setright(insert_node(node.getright(),data)) + node.setright(insert_node(node.getright(), data)) if getheight(node.getright()) - getheight(node.getleft()) == 2: if data < node.getright().getdata(): node = lrrotation(node) else: node = rightrotation(node) - h1 = my_max(getheight(node.getright()),getheight(node.getleft())) + 1 + h1 = my_max(getheight(node.getright()), getheight(node.getleft())) + 1 node.setheight(h1) return node + def getRightMost(root): while root.getright() is not None: root = root.getright() return root.getdata() + + def getLeftMost(root): while root.getleft() is not None: root = root.getleft() return root.getdata() -def del_node(root,data): + +def del_node(root, data): if root.getdata() == data: if root.getleft() is not None and root.getright() is not None: temp_data = getLeftMost(root.getright()) root.setdata(temp_data) - root.setright(del_node(root.getright(),temp_data)) + root.setright(del_node(root.getright(), temp_data)) elif root.getleft() is not None: root = root.getleft() else: @@ -164,12 +191,12 @@ def del_node(root,data): print("No such data") return root else: - root.setleft(del_node(root.getleft(),data)) + root.setleft(del_node(root.getleft(), data)) elif root.getdata() < data: if root.getright() is None: return root else: - root.setright(del_node(root.getright(),data)) + root.setright(del_node(root.getright(), data)) if root is None: return root if getheight(root.getright()) - getheight(root.getleft()) == 2: @@ -182,27 +209,31 @@ def del_node(root,data): root = leftrotation(root) else: root = rlrotation(root) - height = my_max(getheight(root.getright()),getheight(root.getleft())) + 1 + height = my_max(getheight(root.getright()), getheight(root.getleft())) + 1 root.setheight(height) return root + class AVLtree: def __init__(self): self.root = None + def getheight(self): -# print("yyy") + # print("yyy") return getheight(self.root) - def insert(self,data): - print("insert:"+str(data)) - self.root = insert_node(self.root,data) - - def del_node(self,data): - print("delete:"+str(data)) + + def insert(self, data): + print("insert:" + str(data)) + self.root = insert_node(self.root, data) + + def del_node(self, data): + print("delete:" + str(data)) if self.root is None: print("Tree is empty!") return - self.root = del_node(self.root,data) - def traversale(self): #a level traversale, gives a more intuitive look on the tree + self.root = del_node(self.root, data) + + def traversale(self): # a level traversale, gives a more intuitive look on the tree q = my_queue() q.push(self.root) layer = self.getheight() @@ -211,21 +242,21 @@ def traversale(self): #a level traversale, gives a more intuitive look on the tr cnt = 0 while not q.isEmpty(): node = q.pop() - space = " "*int(math.pow(2,layer-1)) - print(space,end = "") + space = " " * int(math.pow(2, layer - 1)) + print(space, end="") if node is None: - print("*",end = "") + print("*", end="") q.push(None) q.push(None) else: - print(node.getdata(),end = "") + print(node.getdata(), end="") q.push(node.getleft()) q.push(node.getright()) - print(space,end = "") + print(space, end="") cnt = cnt + 1 for i in range(100): - if cnt == math.pow(2,i) - 1: - layer = layer -1 + if cnt == math.pow(2, i) - 1: + layer = layer - 1 if layer == 0: print() print("*************************************") @@ -235,11 +266,13 @@ def traversale(self): #a level traversale, gives a more intuitive look on the tr print() print("*************************************") return - + def test(self): getheight(None) print("****") self.getheight() + + if __name__ == "__main__": t = AVLtree() t.traversale() @@ -248,7 +281,7 @@ def test(self): for i in l: t.insert(i) t.traversale() - + random.shuffle(l) for i in l: t.del_node(i) diff --git a/data_structures/binary_tree/basic_binary_tree.py b/data_structures/binary_tree/basic_binary_tree.py index 7c6240fb4dd4..3ed34fc6c68e 100644 --- a/data_structures/binary_tree/basic_binary_tree.py +++ b/data_structures/binary_tree/basic_binary_tree.py @@ -1,12 +1,13 @@ -class Node: # This is the Class Node with constructor that contains data variable to type data and left,right pointers. +class Node: # This is the Class Node with a constructor that contains data variable to type data and left, right pointers. def __init__(self, data): self.data = data self.left = None self.right = None -def display(tree): #In Order traversal of the tree - if tree is None: +def display(tree): # In Order traversal of the tree + + if tree is None: return if tree.left is not None: @@ -19,7 +20,10 @@ def display(tree): #In Order traversal of the tree return -def depth_of_tree(tree): #This is the recursive function to find the depth of binary tree. + +def depth_of_tree( + tree, +): # This is the recursive function to find the depth of binary tree. if tree is None: return 0 else: @@ -31,18 +35,20 @@ def depth_of_tree(tree): #This is the recursive function to find the depth of bi return 1 + depth_r_tree -def is_full_binary_tree(tree): # This functions returns that is it full binary tree or not? +def is_full_binary_tree( + tree, +): # This function returns that is it full binary tree or not? if tree is None: return True if (tree.left is None) and (tree.right is None): return True if (tree.left is not None) and (tree.right is not None): - return (is_full_binary_tree(tree.left) and is_full_binary_tree(tree.right)) + return is_full_binary_tree(tree.left) and is_full_binary_tree(tree.right) else: return False -def main(): # Main func for testing. +def main(): # Main function for testing. tree = Node(1) tree.left = Node(2) tree.right = Node(3) @@ -59,5 +65,5 @@ def main(): # Main func for testing. display(tree) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py index cef5b55f245d..3868130357fb 100644 --- a/data_structures/binary_tree/binary_search_tree.py +++ b/data_structures/binary_tree/binary_search_tree.py @@ -1,201 +1,159 @@ -''' +""" A binary search Tree -''' -from __future__ import print_function -class Node: +""" + - def __init__(self, label, parent): - self.label = label +class Node: + def __init__(self, value, parent): + self.value = value + self.parent = parent # Added in order to delete a node easier self.left = None self.right = None - #Added in order to delete a node easier - self.parent = parent - - def getLabel(self): - return self.label - - def setLabel(self, label): - self.label = label - def getLeft(self): - return self.left + def __repr__(self): + from pprint import pformat - def setLeft(self, left): - self.left = left + if self.left is None and self.right is None: + return str(self.value) + return pformat({"%s" % (self.value): (self.left, self.right)}, indent=1) - def getRight(self): - return self.right - - def setRight(self, right): - self.right = right - - def getParent(self): - return self.parent - - def setParent(self, parent): - self.parent = parent class BinarySearchTree: + def __init__(self, root=None): + self.root = root - def __init__(self): - self.root = None - - def insert(self, label): - # Create a new Node - new_node = Node(label, None) - # If Tree is empty - if self.empty(): - self.root = new_node - else: - #If Tree is not empty - curr_node = self.root - #While we don't get to a leaf - while curr_node is not None: - #We keep reference of the parent node - parent_node = curr_node - #If node label is less than current node - if new_node.getLabel() < curr_node.getLabel(): - #We go left - curr_node = curr_node.getLeft() - else: - #Else we go right - curr_node = curr_node.getRight() - #We insert the new node in a leaf - if new_node.getLabel() < parent_node.getLabel(): - parent_node.setLeft(new_node) + def __str__(self): + """ + Return a string of all the Nodes using in order traversal + """ + return str(self.root) + + def __reassign_nodes(self, node, new_children): + if new_children is not None: # reset its kids + new_children.parent = node.parent + if node.parent is not None: # reset its parent + if self.is_right(node): # If it is the right children + node.parent.right = new_children else: - parent_node.setRight(new_node) - #Set parent to the new node - new_node.setParent(parent_node) - - def delete(self, label): - if (not self.empty()): - #Look for the node with that label - node = self.getNode(label) - #If the node exists - if(node is not None): - #If it has no children - if(node.getLeft() is None and node.getRight() is None): - self.__reassignNodes(node, None) - node = None - #Has only right children - elif(node.getLeft() is None and node.getRight() is not None): - self.__reassignNodes(node, node.getRight()) - #Has only left children - elif(node.getLeft() is not None and node.getRight() is None): - self.__reassignNodes(node, node.getLeft()) - #Has two children - else: - #Gets the max value of the left branch - tmpNode = self.getMax(node.getLeft()) - #Deletes the tmpNode - self.delete(tmpNode.getLabel()) - #Assigns the value to the node to delete and keesp tree structure - node.setLabel(tmpNode.getLabel()) - - def getNode(self, label): - curr_node = None - #If the tree is not empty - if(not self.empty()): - #Get tree root - curr_node = self.getRoot() - #While we don't find the node we look for - #I am using lazy evaluation here to avoid NoneType Attribute error - while curr_node is not None and curr_node.getLabel() is not label: - #If node label is less than current node - if label < curr_node.getLabel(): - #We go left - curr_node = curr_node.getLeft() - else: - #Else we go right - curr_node = curr_node.getRight() - return curr_node - - def getMax(self, root = None): - if(root is not None): - curr_node = root + node.parent.left = new_children else: - #We go deep on the right branch - curr_node = self.getRoot() - if(not self.empty()): - while(curr_node.getRight() is not None): - curr_node = curr_node.getRight() - return curr_node + self.root = new_children - def getMin(self, root = None): - if(root is not None): - curr_node = root - else: - #We go deep on the left branch - curr_node = self.getRoot() - if(not self.empty()): - curr_node = self.getRoot() - while(curr_node.getLeft() is not None): - curr_node = curr_node.getLeft() - return curr_node + def is_right(self, node): + return node == node.parent.right def empty(self): - if self.root is None: - return True - return False - - def __InOrderTraversal(self, curr_node): - nodeList = [] - if curr_node is not None: - nodeList.insert(0, curr_node) - nodeList = nodeList + self.__InOrderTraversal(curr_node.getLeft()) - nodeList = nodeList + self.__InOrderTraversal(curr_node.getRight()) - return nodeList - - def getRoot(self): - return self.root - - def __isRightChildren(self, node): - if(node == node.getParent().getRight()): - return True - return False - - def __reassignNodes(self, node, newChildren): - if(newChildren is not None): - newChildren.setParent(node.getParent()) - if(node.getParent() is not None): - #If it is the Right Children - if(self.__isRightChildren(node)): - node.getParent().setRight(newChildren) + return self.root is None + + def __insert(self, value): + """ + Insert a new node in Binary Search Tree with value label + """ + new_node = Node(value, None) # create a new Node + if self.empty(): # if Tree is empty + self.root = new_node # set its root + else: # Tree is not empty + parent_node = self.root # from root + while True: # While we don't get to a leaf + if value < parent_node.value: # We go left + if parent_node.left is None: + parent_node.left = new_node # We insert the new node in a leaf + break + else: + parent_node = parent_node.left + else: + if parent_node.right is None: + parent_node.right = new_node + break + else: + parent_node = parent_node.right + new_node.parent = parent_node + + def insert(self, *values): + for value in values: + self.__insert(value) + return self + + def search(self, value): + if self.empty(): + raise IndexError("Warning: Tree is empty! please use another.") + else: + node = self.root + # use lazy evaluation here to avoid NoneType Attribute error + while node is not None and node.value is not value: + node = node.left if value < node.value else node.right + return node + + def get_max(self, node=None): + """ + We go deep on the right branch + """ + if node is None: + node = self.root + if not self.empty(): + while node.right is not None: + node = node.right + return node + + def get_min(self, node=None): + """ + We go deep on the left branch + """ + if node is None: + node = self.root + if not self.empty(): + node = self.root + while node.left is not None: + node = node.left + return node + + def remove(self, value): + node = self.search(value) # Look for the node with that label + if node is not None: + if node.left is None and node.right is None: # If it has no children + self.__reassign_nodes(node, None) + elif node.left is None: # Has only right children + self.__reassign_nodes(node, node.right) + elif node.right is None: # Has only left children + self.__reassign_nodes(node, node.left) else: - #Else it is the left children - node.getParent().setLeft(newChildren) - - #This function traversal the tree. By default it returns an - #In order traversal list. You can pass a function to traversal - #The tree as needed by client code - def traversalTree(self, traversalFunction = None, root = None): - if(traversalFunction is None): - #Returns a list of nodes in preOrder by default - return self.__InOrderTraversal(self.root) + tmp_node = self.get_max( + node.left + ) # Gets the max value of the left branch + self.remove(tmp_node.value) + node.value = ( + tmp_node.value + ) # Assigns the value to the node to delete and keep tree structure + + def preorder_traverse(self, node): + if node is not None: + yield node # Preorder Traversal + yield from self.preorder_traverse(node.left) + yield from self.preorder_traverse(node.right) + + def traversal_tree(self, traversal_function=None): + """ + This function traversal the tree. + You can pass a function to traversal the tree as needed by client code + """ + if traversal_function is None: + return self.preorder_traverse(self.root) else: - #Returns a list of nodes in the order that the users wants to - return traversalFunction(self.root) + return traversal_function(self.root) - #Returns an string of all the nodes labels in the list - #In Order Traversal - def __str__(self): - list = self.__InOrderTraversal(self.root) - str = "" - for x in list: - str = str + " " + x.getLabel().__str__() - return str -def InPreOrder(curr_node): - nodeList = [] +def postorder(curr_node): + """ + postOrder (left, right, self) + """ + node_list = list() if curr_node is not None: - nodeList = nodeList + InPreOrder(curr_node.getLeft()) - nodeList.insert(0, curr_node.getLabel()) - nodeList = nodeList + InPreOrder(curr_node.getRight()) - return nodeList + node_list = postorder(curr_node.left) + postorder(curr_node.right) + [curr_node] + return node_list + -def testBinarySearchTree(): - r''' +def binary_search_tree(): + r""" Example 8 / \ @@ -203,56 +161,47 @@ def testBinarySearchTree(): / \ \ 1 6 14 / \ / - 4 7 13 - ''' - - r''' - Example After Deletion - 7 - / \ - 1 4 - - ''' + 4 7 13 + + >>> t = BinarySearchTree().insert(8, 3, 6, 1, 10, 14, 13, 4, 7) + >>> print(" ".join(repr(i.value) for i in t.traversal_tree())) + 8 3 1 6 4 7 10 14 13 + >>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) + 1 4 7 6 3 13 14 10 8 + >>> BinarySearchTree().search(6) + Traceback (most recent call last): + ... + IndexError: Warning: Tree is empty! please use another. + """ + testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) t = BinarySearchTree() - t.insert(8) - t.insert(3) - t.insert(6) - t.insert(1) - t.insert(10) - t.insert(14) - t.insert(13) - t.insert(4) - t.insert(7) + for i in testlist: + t.insert(i) - #Prints all the elements of the list in order traversal - print(t.__str__()) + # Prints all the elements of the list in order traversal + print(t) - if(t.getNode(6) is not None): - print("The label 6 exists") + if t.search(6) is not None: + print("The value 6 exists") else: - print("The label 6 doesn't exist") + print("The value 6 doesn't exist") - if(t.getNode(-1) is not None): - print("The label -1 exists") + if t.search(-1) is not None: + print("The value -1 exists") else: - print("The label -1 doesn't exist") - - if(not t.empty()): - print(("Max Value: ", t.getMax().getLabel())) - print(("Min Value: ", t.getMin().getLabel())) - - t.delete(13) - t.delete(10) - t.delete(8) - t.delete(3) - t.delete(6) - t.delete(14) + print("The value -1 doesn't exist") + + if not t.empty(): + print("Max Value: ", t.get_max().value) + print("Min Value: ", t.get_min().value) + + for i in testlist: + t.remove(i) + print(t) - #Gets all the elements of the tree In pre order - #And it prints them - list = t.traversalTree(InPreOrder, t.root) - for x in list: - print(x) if __name__ == "__main__": - testBinarySearchTree() + import doctest + + doctest.testmod() + # binary_search_tree() diff --git a/data_structures/binary_tree/binary_search_tree_recursive.py b/data_structures/binary_tree/binary_search_tree_recursive.py new file mode 100644 index 000000000000..f1e46e33cd24 --- /dev/null +++ b/data_structures/binary_tree/binary_search_tree_recursive.py @@ -0,0 +1,613 @@ +""" +This is a python3 implementation of binary search tree using recursion + +To run tests: +python -m unittest binary_search_tree_recursive.py + +To run an example: +python binary_search_tree_recursive.py +""" +import unittest + + +class Node: + def __init__(self, label: int, parent): + self.label = label + self.parent = parent + self.left = None + self.right = None + + +class BinarySearchTree: + def __init__(self): + self.root = None + + def empty(self): + """ + Empties the tree + + >>> t = BinarySearchTree() + >>> assert t.root is None + >>> t.put(8) + >>> assert t.root is not None + """ + self.root = None + + def is_empty(self) -> bool: + """ + Checks if the tree is empty + + >>> t = BinarySearchTree() + >>> t.is_empty() + True + >>> t.put(8) + >>> t.is_empty() + False + """ + return self.root is None + + def put(self, label: int): + """ + Put a new node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> assert t.root.parent is None + >>> assert t.root.label == 8 + + >>> t.put(10) + >>> assert t.root.right.parent == t.root + >>> assert t.root.right.label == 10 + + >>> t.put(3) + >>> assert t.root.left.parent == t.root + >>> assert t.root.left.label == 3 + """ + self.root = self._put(self.root, label) + + def _put(self, node: Node, label: int, parent: Node = None) -> Node: + if node is None: + node = Node(label, parent) + else: + if label < node.label: + node.left = self._put(node.left, label, node) + elif label > node.label: + node.right = self._put(node.right, label, node) + else: + raise Exception(f"Node with label {label} already exists") + + return node + + def search(self, label: int) -> Node: + """ + Searches a node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> node = t.search(8) + >>> assert node.label == 8 + + >>> node = t.search(3) + Traceback (most recent call last): + ... + Exception: Node with label 3 does not exist + """ + return self._search(self.root, label) + + def _search(self, node: Node, label: int) -> Node: + if node is None: + raise Exception(f"Node with label {label} does not exist") + else: + if label < node.label: + node = self._search(node.left, label) + elif label > node.label: + node = self._search(node.right, label) + + return node + + def remove(self, label: int): + """ + Removes a node in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> t.remove(8) + >>> assert t.root.label == 10 + + >>> t.remove(3) + Traceback (most recent call last): + ... + Exception: Node with label 3 does not exist + """ + node = self.search(label) + if not node.right and not node.left: + self._reassign_nodes(node, None) + elif not node.right and node.left: + self._reassign_nodes(node, node.left) + elif node.right and not node.left: + self._reassign_nodes(node, node.right) + else: + lowest_node = self._get_lowest_node(node.right) + lowest_node.left = node.left + lowest_node.right = node.right + node.left.parent = lowest_node + if node.right: + node.right.parent = lowest_node + self._reassign_nodes(node, lowest_node) + + def _reassign_nodes(self, node: Node, new_children: Node): + if new_children: + new_children.parent = node.parent + + if node.parent: + if node.parent.right == node: + node.parent.right = new_children + else: + node.parent.left = new_children + else: + self.root = new_children + + def _get_lowest_node(self, node: Node) -> Node: + if node.left: + lowest_node = self._get_lowest_node(node.left) + else: + lowest_node = node + self._reassign_nodes(node, node.right) + + return lowest_node + + def exists(self, label: int) -> bool: + """ + Checks if a node exists in the tree + + >>> t = BinarySearchTree() + >>> t.put(8) + >>> t.put(10) + >>> t.exists(8) + True + + >>> t.exists(3) + False + """ + try: + self.search(label) + return True + except Exception: + return False + + def get_max_label(self) -> int: + """ + Gets the max label inserted in the tree + + >>> t = BinarySearchTree() + >>> t.get_max_label() + Traceback (most recent call last): + ... + Exception: Binary search tree is empty + + >>> t.put(8) + >>> t.put(10) + >>> t.get_max_label() + 10 + """ + if self.is_empty(): + raise Exception("Binary search tree is empty") + + node = self.root + while node.right is not None: + node = node.right + + return node.label + + def get_min_label(self) -> int: + """ + Gets the min label inserted in the tree + + >>> t = BinarySearchTree() + >>> t.get_min_label() + Traceback (most recent call last): + ... + Exception: Binary search tree is empty + + >>> t.put(8) + >>> t.put(10) + >>> t.get_min_label() + 8 + """ + if self.is_empty(): + raise Exception("Binary search tree is empty") + + node = self.root + while node.left is not None: + node = node.left + + return node.label + + def inorder_traversal(self) -> list: + """ + Return the inorder traversal of the tree + + >>> t = BinarySearchTree() + >>> [i.label for i in t.inorder_traversal()] + [] + + >>> t.put(8) + >>> t.put(10) + >>> t.put(9) + >>> [i.label for i in t.inorder_traversal()] + [8, 9, 10] + """ + return self._inorder_traversal(self.root) + + def _inorder_traversal(self, node: Node) -> list: + if node is not None: + yield from self._inorder_traversal(node.left) + yield node + yield from self._inorder_traversal(node.right) + + def preorder_traversal(self) -> list: + """ + Return the preorder traversal of the tree + + >>> t = BinarySearchTree() + >>> [i.label for i in t.preorder_traversal()] + [] + + >>> t.put(8) + >>> t.put(10) + >>> t.put(9) + >>> [i.label for i in t.preorder_traversal()] + [8, 10, 9] + """ + return self._preorder_traversal(self.root) + + def _preorder_traversal(self, node: Node) -> list: + if node is not None: + yield node + yield from self._preorder_traversal(node.left) + yield from self._preorder_traversal(node.right) + + +class BinarySearchTreeTest(unittest.TestCase): + @staticmethod + def _get_binary_search_tree(): + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + \ + 5 + """ + t = BinarySearchTree() + t.put(8) + t.put(3) + t.put(6) + t.put(1) + t.put(10) + t.put(14) + t.put(13) + t.put(4) + t.put(7) + t.put(5) + + return t + + def test_put(self): + t = BinarySearchTree() + assert t.is_empty() + + t.put(8) + r""" + 8 + """ + assert t.root.parent is None + assert t.root.label == 8 + + t.put(10) + r""" + 8 + \ + 10 + """ + assert t.root.right.parent == t.root + assert t.root.right.label == 10 + + t.put(3) + r""" + 8 + / \ + 3 10 + """ + assert t.root.left.parent == t.root + assert t.root.left.label == 3 + + t.put(6) + r""" + 8 + / \ + 3 10 + \ + 6 + """ + assert t.root.left.right.parent == t.root.left + assert t.root.left.right.label == 6 + + t.put(1) + r""" + 8 + / \ + 3 10 + / \ + 1 6 + """ + assert t.root.left.left.parent == t.root.left + assert t.root.left.left.label == 1 + + with self.assertRaises(Exception): + t.put(1) + + def test_search(self): + t = self._get_binary_search_tree() + + node = t.search(6) + assert node.label == 6 + + node = t.search(13) + assert node.label == 13 + + with self.assertRaises(Exception): + t.search(2) + + def test_remove(self): + t = self._get_binary_search_tree() + + t.remove(13) + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ + 4 7 + \ + 5 + """ + assert t.root.right.right.right is None + assert t.root.right.right.left is None + + t.remove(7) + r""" + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / + 4 + \ + 5 + """ + assert t.root.left.right.right is None + assert t.root.left.right.left.label == 4 + + t.remove(6) + r""" + 8 + / \ + 3 10 + / \ \ + 1 4 14 + \ + 5 + """ + assert t.root.left.left.label == 1 + assert t.root.left.right.label == 4 + assert t.root.left.right.right.label == 5 + assert t.root.left.right.left is None + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.parent == t.root.left + + t.remove(3) + r""" + 8 + / \ + 4 10 + / \ \ + 1 5 14 + """ + assert t.root.left.label == 4 + assert t.root.left.right.label == 5 + assert t.root.left.left.label == 1 + assert t.root.left.parent == t.root + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.parent == t.root.left + + t.remove(4) + r""" + 8 + / \ + 5 10 + / \ + 1 14 + """ + assert t.root.left.label == 5 + assert t.root.left.right is None + assert t.root.left.left.label == 1 + assert t.root.left.parent == t.root + assert t.root.left.left.parent == t.root.left + + def test_remove_2(self): + t = self._get_binary_search_tree() + + t.remove(3) + r""" + 8 + / \ + 4 10 + / \ \ + 1 6 14 + / \ / + 5 7 13 + """ + assert t.root.left.label == 4 + assert t.root.left.right.label == 6 + assert t.root.left.left.label == 1 + assert t.root.left.right.right.label == 7 + assert t.root.left.right.left.label == 5 + assert t.root.left.parent == t.root + assert t.root.left.right.parent == t.root.left + assert t.root.left.left.parent == t.root.left + assert t.root.left.right.left.parent == t.root.left.right + + def test_empty(self): + t = self._get_binary_search_tree() + t.empty() + assert t.root is None + + def test_is_empty(self): + t = self._get_binary_search_tree() + assert not t.is_empty() + + t.empty() + assert t.is_empty() + + def test_exists(self): + t = self._get_binary_search_tree() + + assert t.exists(6) + assert not t.exists(-1) + + def test_get_max_label(self): + t = self._get_binary_search_tree() + + assert t.get_max_label() == 14 + + t.empty() + with self.assertRaises(Exception): + t.get_max_label() + + def test_get_min_label(self): + t = self._get_binary_search_tree() + + assert t.get_min_label() == 1 + + t.empty() + with self.assertRaises(Exception): + t.get_min_label() + + def test_inorder_traversal(self): + t = self._get_binary_search_tree() + + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + assert inorder_traversal_nodes == [1, 3, 4, 5, 6, 7, 8, 10, 13, 14] + + def test_preorder_traversal(self): + t = self._get_binary_search_tree() + + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + assert preorder_traversal_nodes == [8, 3, 1, 6, 4, 5, 7, 10, 14, 13] + + +def binary_search_tree_example(): + r""" + Example + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + \ + 5 + + Example After Deletion + 4 + / \ + 1 7 + \ + 5 + + """ + + t = BinarySearchTree() + t.put(8) + t.put(3) + t.put(6) + t.put(1) + t.put(10) + t.put(14) + t.put(13) + t.put(4) + t.put(7) + t.put(5) + + print( + """ + 8 + / \\ + 3 10 + / \\ \\ + 1 6 14 + / \\ / + 4 7 13 + \\ + 5 + """ + ) + + print("Label 6 exists:", t.exists(6)) + print("Label 13 exists:", t.exists(13)) + print("Label -1 exists:", t.exists(-1)) + print("Label 12 exists:", t.exists(12)) + + # Prints all the elements of the list in inorder traversal + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + print("Inorder traversal:", inorder_traversal_nodes) + + # Prints all the elements of the list in preorder traversal + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + print("Preorder traversal:", preorder_traversal_nodes) + + print("Max. label:", t.get_max_label()) + print("Min. label:", t.get_min_label()) + + # Delete elements + print("\nDeleting elements 13, 10, 8, 3, 6, 14") + print( + """ + 4 + / \\ + 1 7 + \\ + 5 + """ + ) + t.remove(13) + t.remove(10) + t.remove(8) + t.remove(3) + t.remove(6) + t.remove(14) + + # Prints all the elements of the list in inorder traversal after delete + inorder_traversal_nodes = [i.label for i in t.inorder_traversal()] + print("Inorder traversal after delete:", inorder_traversal_nodes) + + # Prints all the elements of the list in preorder traversal after delete + preorder_traversal_nodes = [i.label for i in t.preorder_traversal()] + print("Preorder traversal after delete:", preorder_traversal_nodes) + + print("Max. label:", t.get_max_label()) + print("Min. label:", t.get_min_label()) + + +if __name__ == "__main__": + binary_search_tree_example() diff --git a/data_structures/binary_tree/fenwick_tree.py b/data_structures/binary_tree/fenwick_tree.py index f429161c8c36..54f0f07ac68d 100644 --- a/data_structures/binary_tree/fenwick_tree.py +++ b/data_structures/binary_tree/fenwick_tree.py @@ -1,29 +1,28 @@ -from __future__ import print_function class FenwickTree: - - def __init__(self, SIZE): # create fenwick tree with size SIZE + def __init__(self, SIZE): # create fenwick tree with size SIZE self.Size = SIZE - self.ft = [0 for i in range (0,SIZE)] + self.ft = [0 for i in range(0, SIZE)] - def update(self, i, val): # update data (adding) in index i in O(lg N) - while (i < self.Size): + def update(self, i, val): # update data (adding) in index i in O(lg N) + while i < self.Size: self.ft[i] += val i += i & (-i) - def query(self, i): # query cumulative data from index 0 to i in O(lg N) + def query(self, i): # query cumulative data from index 0 to i in O(lg N) ret = 0 - while (i > 0): + while i > 0: ret += self.ft[i] i -= i & (-i) return ret - -if __name__ == '__main__': + + +if __name__ == "__main__": f = FenwickTree(100) - f.update(1,20) - f.update(4,4) - print (f.query(1)) - print (f.query(3)) - print (f.query(4)) - f.update(2,-5) - print (f.query(1)) - print (f.query(3)) + f.update(1, 20) + f.update(4, 4) + print(f.query(1)) + print(f.query(3)) + print(f.query(4)) + f.update(2, -5) + print(f.query(1)) + print(f.query(3)) diff --git a/data_structures/binary_tree/lazy_segment_tree.py b/data_structures/binary_tree/lazy_segment_tree.py index 9b14b24e81fa..acd551b41b96 100644 --- a/data_structures/binary_tree/lazy_segment_tree.py +++ b/data_structures/binary_tree/lazy_segment_tree.py @@ -1,58 +1,61 @@ -from __future__ import print_function import math + class SegmentTree: - def __init__(self, N): self.N = N - self.st = [0 for i in range(0,4*N)] # approximate the overall size of segment tree with array N - self.lazy = [0 for i in range(0,4*N)] # create array to store lazy update - self.flag = [0 for i in range(0,4*N)] # flag for lazy update - + self.st = [ + 0 for i in range(0, 4 * N) + ] # approximate the overall size of segment tree with array N + self.lazy = [0 for i in range(0, 4 * N)] # create array to store lazy update + self.flag = [0 for i in range(0, 4 * N)] # flag for lazy update + def left(self, idx): - return idx*2 + return idx * 2 def right(self, idx): - return idx*2 + 1 + return idx * 2 + 1 def build(self, idx, l, r, A): - if l==r: - self.st[idx] = A[l-1] - else : - mid = (l+r)//2 - self.build(self.left(idx),l,mid, A) - self.build(self.right(idx),mid+1,r, A) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) + if l == r: + self.st[idx] = A[l - 1] + else: + mid = (l + r) // 2 + self.build(self.left(idx), l, mid, A) + self.build(self.right(idx), mid + 1, r, A) + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) # update with O(lg N) (Normal segment tree without lazy update will take O(Nlg N) for each update) - def update(self, idx, l, r, a, b, val): # update(1, 1, N, a, b, v) for update val v to [a,b] + def update( + self, idx, l, r, a, b, val + ): # update(1, 1, N, a, b, v) for update val v to [a,b] if self.flag[idx] == True: self.st[idx] = self.lazy[idx] self.flag[idx] = False - if l!=r: + if l != r: self.lazy[self.left(idx)] = self.lazy[idx] self.lazy[self.right(idx)] = self.lazy[idx] self.flag[self.left(idx)] = True self.flag[self.right(idx)] = True - + if r < a or l > b: return True - if l >= a and r <= b : + if l >= a and r <= b: self.st[idx] = val - if l!=r: + if l != r: self.lazy[self.left(idx)] = val self.lazy[self.right(idx)] = val self.flag[self.left(idx)] = True self.flag[self.right(idx)] = True return True - mid = (l+r)//2 - self.update(self.left(idx),l,mid,a,b,val) - self.update(self.right(idx),mid+1,r,a,b,val) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) + mid = (l + r) // 2 + self.update(self.left(idx), l, mid, a, b, val) + self.update(self.right(idx), mid + 1, r, a, b, val) + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) return True # query with O(lg N) - def query(self, idx, l, r, a, b): #query(1, 1, N, a, b) for query max of [a,b] + def query(self, idx, l, r, a, b): # query(1, 1, N, a, b) for query max of [a,b] if self.flag[idx] == True: self.st[idx] = self.lazy[idx] self.flag[idx] = False @@ -65,27 +68,27 @@ def query(self, idx, l, r, a, b): #query(1, 1, N, a, b) for query max of [a,b] return -math.inf if l >= a and r <= b: return self.st[idx] - mid = (l+r)//2 - q1 = self.query(self.left(idx),l,mid,a,b) - q2 = self.query(self.right(idx),mid+1,r,a,b) - return max(q1,q2) + mid = (l + r) // 2 + q1 = self.query(self.left(idx), l, mid, a, b) + q2 = self.query(self.right(idx), mid + 1, r, a, b) + return max(q1, q2) def showData(self): showList = [] - for i in range(1,N+1): + for i in range(1, N + 1): showList += [self.query(1, 1, self.N, i, i)] - print (showList) - + print(showList) + -if __name__ == '__main__': - A = [1,2,-4,7,3,-5,6,11,-20,9,14,15,5,2,-8] +if __name__ == "__main__": + A = [1, 2, -4, 7, 3, -5, 6, 11, -20, 9, 14, 15, 5, 2, -8] N = 15 segt = SegmentTree(N) - segt.build(1,1,N,A) - print (segt.query(1,1,N,4,6)) - print (segt.query(1,1,N,7,11)) - print (segt.query(1,1,N,7,12)) - segt.update(1,1,N,1,3,111) - print (segt.query(1,1,N,1,15)) - segt.update(1,1,N,7,8,235) + segt.build(1, 1, N, A) + print(segt.query(1, 1, N, 4, 6)) + print(segt.query(1, 1, N, 7, 11)) + print(segt.query(1, 1, N, 7, 12)) + segt.update(1, 1, N, 1, 3, 111) + print(segt.query(1, 1, N, 1, 15)) + segt.update(1, 1, N, 7, 8, 235) segt.showData() diff --git a/data_structures/binary_tree/lca.py b/data_structures/binary_tree/lowest_common_ancestor.py similarity index 92% rename from data_structures/binary_tree/lca.py rename to data_structures/binary_tree/lowest_common_ancestor.py index 9c9d8ca629c7..f560eaa5ef29 100644 --- a/data_structures/binary_tree/lca.py +++ b/data_structures/binary_tree/lowest_common_ancestor.py @@ -1,3 +1,6 @@ +# https://en.wikipedia.org/wiki/Lowest_common_ancestor +# https://en.wikipedia.org/wiki/Breadth-first_search + import queue @@ -8,7 +11,7 @@ def swap(a, b): return a, b -# creating sparse table which saves each nodes 2^ith parent +# creating sparse table which saves each nodes 2^i-th parent def creatSparse(max_node, parent): j = 1 while (1 << j) < max_node: @@ -75,7 +78,7 @@ def main(): 10: [], 11: [], 12: [], - 13: [] + 13: [], } level, parent = bfs(level, parent, max_node, graph, 1) parent = creatSparse(max_node, parent) diff --git a/data_structures/binary_tree/non_recursive_segment_tree.py b/data_structures/binary_tree/non_recursive_segment_tree.py new file mode 100644 index 000000000000..877ee45b5baa --- /dev/null +++ b/data_structures/binary_tree/non_recursive_segment_tree.py @@ -0,0 +1,153 @@ +""" +A non-recursive Segment Tree implementation with range query and single element update, +works virtually with any list of the same type of elements with a "commutative" combiner. + +Explanation: +https://www.geeksforgeeks.org/iterative-segment-tree-range-minimum-query/ +https://www.geeksforgeeks.org/segment-tree-efficient-implementation/ + +>>> SegmentTree([1, 2, 3], lambda a, b: a + b).query(0, 2) +6 +>>> SegmentTree([3, 1, 2], min).query(0, 2) +1 +>>> SegmentTree([2, 3, 1], max).query(0, 2) +3 +>>> st = SegmentTree([1, 5, 7, -1, 6], lambda a, b: a + b) +>>> st.update(1, -1) +>>> st.update(2, 3) +>>> st.query(1, 2) +2 +>>> st.query(1, 1) +-1 +>>> st.update(4, 1) +>>> st.query(3, 4) +0 +>>> st = SegmentTree([[1, 2, 3], [3, 2, 1], [1, 1, 1]], lambda a, b: [a[i] + b[i] for i in range(len(a))]) +>>> st.query(0, 1) +[4, 4, 4] +>>> st.query(1, 2) +[4, 3, 2] +>>> st.update(1, [-1, -1, -1]) +>>> st.query(1, 2) +[0, 0, 0] +>>> st.query(0, 2) +[1, 2, 3] +""" +from typing import List, Callable, TypeVar + +T = TypeVar("T") + + +class SegmentTree: + def __init__(self, arr: List[T], fnc: Callable[[T, T], T]) -> None: + """ + Segment Tree constructor, it works just with commutative combiner. + :param arr: list of elements for the segment tree + :param fnc: commutative function for combine two elements + + >>> SegmentTree(['a', 'b', 'c'], lambda a, b: '{}{}'.format(a, b)).query(0, 2) + 'abc' + >>> SegmentTree([(1, 2), (2, 3), (3, 4)], lambda a, b: (a[0] + b[0], a[1] + b[1])).query(0, 2) + (6, 9) + """ + self.N = len(arr) + self.st = [None for _ in range(len(arr))] + arr + self.fn = fnc + self.build() + + def build(self) -> None: + for p in range(self.N - 1, 0, -1): + self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1]) + + def update(self, p: int, v: T) -> None: + """ + Update an element in log(N) time + :param p: position to be update + :param v: new value + + >>> st = SegmentTree([3, 1, 2, 4], min) + >>> st.query(0, 3) + 1 + >>> st.update(2, -1) + >>> st.query(0, 3) + -1 + """ + p += self.N + self.st[p] = v + while p > 1: + p = p // 2 + self.st[p] = self.fn(self.st[p * 2], self.st[p * 2 + 1]) + + def query(self, l: int, r: int) -> T: + """ + Get range query value in log(N) time + :param l: left element index + :param r: right element index + :return: element combined in the range [l, r] + + >>> st = SegmentTree([1, 2, 3, 4], lambda a, b: a + b) + >>> st.query(0, 2) + 6 + >>> st.query(1, 2) + 5 + >>> st.query(0, 3) + 10 + >>> st.query(2, 3) + 7 + """ + l, r = l + self.N, r + self.N + res = None + while l <= r: + if l % 2 == 1: + res = self.st[l] if res is None else self.fn(res, self.st[l]) + if r % 2 == 0: + res = self.st[r] if res is None else self.fn(res, self.st[r]) + l, r = (l + 1) // 2, (r - 1) // 2 + return res + + +if __name__ == "__main__": + from functools import reduce + + test_array = [1, 10, -2, 9, -3, 8, 4, -7, 5, 6, 11, -12] + + test_updates = { + 0: 7, + 1: 2, + 2: 6, + 3: -14, + 4: 5, + 5: 4, + 6: 7, + 7: -10, + 8: 9, + 9: 10, + 10: 12, + 11: 1, + } + + min_segment_tree = SegmentTree(test_array, min) + max_segment_tree = SegmentTree(test_array, max) + sum_segment_tree = SegmentTree(test_array, lambda a, b: a + b) + + def test_all_segments(): + """ + Test all possible segments + """ + for i in range(len(test_array)): + for j in range(i, len(test_array)): + min_range = reduce(min, test_array[i : j + 1]) + max_range = reduce(max, test_array[i : j + 1]) + sum_range = reduce(lambda a, b: a + b, test_array[i : j + 1]) + assert min_range == min_segment_tree.query(i, j) + assert max_range == max_segment_tree.query(i, j) + assert sum_range == sum_segment_tree.query(i, j) + + test_all_segments() + + for index, value in test_updates.items(): + test_array[index] = value + min_segment_tree.update(index, value) + max_segment_tree.update(index, value) + sum_segment_tree.update(index, value) + test_all_segments() diff --git a/data_structures/binary_tree/number_of_possible_binary_trees.py b/data_structures/binary_tree/number_of_possible_binary_trees.py new file mode 100644 index 000000000000..1ad8f2ed4287 --- /dev/null +++ b/data_structures/binary_tree/number_of_possible_binary_trees.py @@ -0,0 +1,102 @@ +""" +Hey, we are going to find an exciting number called Catalan number which is use to find +the number of possible binary search trees from tree of a given number of nodes. + +We will use the formula: t(n) = SUMMATION(i = 1 to n)t(i-1)t(n-i) + +Further details at Wikipedia: https://en.wikipedia.org/wiki/Catalan_number +""" +""" +Our Contribution: +Basically we Create the 2 function: + 1. catalan_number(node_count: int) -> int + Returns the number of possible binary search trees for n nodes. + 2. binary_tree_count(node_count: int) -> int + Returns the number of possible binary trees for n nodes. +""" + + +def binomial_coefficient(n: int, k: int) -> int: + """ + Since Here we Find the Binomial Coefficient: + https://en.wikipedia.org/wiki/Binomial_coefficient + C(n,k) = n! / k!(n-k)! + :param n: 2 times of Number of nodes + :param k: Number of nodes + :return: Integer Value + + >>> binomial_coefficient(4, 2) + 6 + """ + result = 1 # To kept the Calculated Value + # Since C(n, k) = C(n, n-k) + if k > (n - k): + k = n - k + # Calculate C(n,k) + for i in range(k): + result *= n - i + result //= i + 1 + return result + + +def catalan_number(node_count: int) -> int: + """ + We can find Catalan number many ways but here we use Binomial Coefficient because it + does the job in O(n) + + return the Catalan number of n using 2nCn/(n+1). + :param n: number of nodes + :return: Catalan number of n nodes + + >>> catalan_number(5) + 42 + >>> catalan_number(6) + 132 + """ + return binomial_coefficient(2 * node_count, node_count) // (node_count + 1) + + +def factorial(n: int) -> int: + """ + Return the factorial of a number. + :param n: Number to find the Factorial of. + :return: Factorial of n. + + >>> import math + >>> all(factorial(i) == math.factorial(i) for i in range(10)) + True + >>> factorial(-5) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: factorial() not defined for negative values + """ + if n < 0: + raise ValueError("factorial() not defined for negative values") + result = 1 + for i in range(1, n + 1): + result *= i + return result + + +def binary_tree_count(node_count: int) -> int: + """ + Return the number of possible of binary trees. + :param n: number of nodes + :return: Number of possible binary trees + + >>> binary_tree_count(5) + 5040 + >>> binary_tree_count(6) + 95040 + """ + return catalan_number(node_count) * factorial(node_count) + + +if __name__ == "__main__": + node_count = int(input("Enter the number of nodes: ").strip() or 0) + if node_count <= 0: + raise ValueError("We need some nodes to work with.") + print( + f"Given {node_count} nodes, there are {binary_tree_count(node_count)} " + f"binary trees and {catalan_number(node_count)} binary search trees." + ) diff --git a/data_structures/binary_tree/red_black_tree.py b/data_structures/binary_tree/red_black_tree.py index 4ca1301dd8fe..f038b587616d 100644 --- a/data_structures/binary_tree/red_black_tree.py +++ b/data_structures/binary_tree/red_black_tree.py @@ -1,665 +1,710 @@ -class RedBlackTree: - """ - A Red-Black tree, which is a self-balancing BST (binary search - tree). - - This tree has similar performance to AVL trees, but the balancing is - less strict, so it will perform faster for writing/deleting nodes - and slower for reading in the average case, though, because they're - both balanced binary search trees, both will get the same asymptotic - perfomance. - - To read more about them, https://en.wikipedia.org/wiki/Red–black_tree - - Unless otherwise specified, all asymptotic runtimes are specified in - terms of the size of the tree. - """ - def __init__(self, label=None, color=0, parent=None, left=None, right=None): - """Initialize a new Red-Black Tree node with the given values: - label: The value associated with this node - color: 0 if black, 1 if red - parent: The parent to this node - left: This node's left child - right: This node's right child - """ - self.label = label - self.parent = parent - self.left = left - self.right = right - self.color = color - - # Here are functions which are specific to red-black trees - - def rotate_left(self): - """Rotate the subtree rooted at this node to the left and - returns the new root to this subtree. - - Perfoming one rotation can be done in O(1). - """ - parent = self.parent - right = self.right - self.right = right.left - if self.right: - self.right.parent = self - self.parent = right - right.left = self - if parent is not None: - if parent.left is self: - parent.left = right - else: - parent.right = right - right.parent = parent - return right - - def rotate_right(self): - """Rotate the subtree rooted at this node to the right and - returns the new root to this subtree. - - Performing one rotation can be done in O(1). - """ - parent = self.parent - left = self.left - self.left = left.right - if self.left: - self.left.parent = self - self.parent = left - left.right = self - if parent is not None: - if parent.right is self: - parent.right = left - else: - parent.left = left - left.parent = parent - return left - - def insert(self, label): - """Inserts label into the subtree rooted at self, performs any - rotations necessary to maintain balance, and then returns the - new root to this subtree (likely self). - - This is guaranteed to run in O(log(n)) time. - """ - if self.label is None: - # Only possible with an empty tree - self.label = label - return self - if self.label == label: - return self - elif self.label > label: - if self.left: - self.left.insert(label) - else: - self.left = RedBlackTree(label, 1, self) - self.left._insert_repair() - else: - if self.right: - self.right.insert(label) - else: - self.right = RedBlackTree(label, 1, self) - self.right._insert_repair() - return self.parent or self - - def _insert_repair(self): - """Repair the coloring from inserting into a tree.""" - if self.parent is None: - # This node is the root, so it just needs to be black - self.color = 0 - elif color(self.parent) == 0: - # If the parent is black, then it just needs to be red - self.color = 1 - else: - uncle = self.parent.sibling - if color(uncle) == 0: - if self.is_left() and self.parent.is_right(): - self.parent.rotate_right() - self.right._insert_repair() - elif self.is_right() and self.parent.is_left(): - self.parent.rotate_left() - self.left._insert_repair() - elif self.is_left(): - self.grandparent.rotate_right() - self.parent.color = 0 - self.parent.right.color = 1 - else: - self.grandparent.rotate_left() - self.parent.color = 0 - self.parent.left.color = 1 - else: - self.parent.color = 0 - uncle.color = 0 - self.grandparent.color = 1 - self.grandparent._insert_repair() - - def remove(self, label): - """Remove label from this tree.""" - if self.label == label: - if self.left and self.right: - # It's easier to balance a node with at most one child, - # so we replace this node with the greatest one less than - # it and remove that. - value = self.left.get_max() - self.label = value - self.left.remove(value) - else: - # This node has at most one non-None child, so we don't - # need to replace - child = self.left or self.right - if self.color == 1: - # This node is red, and its child is black - # The only way this happens to a node with one child - # is if both children are None leaves. - # We can just remove this node and call it a day. - if self.is_left(): - self.parent.left = None - else: - self.parent.right = None - else: - # The node is black - if child is None: - # This node and its child are black - if self.parent is None: - # The tree is now empty - return RedBlackTree(None) - else: - self._remove_repair() - if self.is_left(): - self.parent.left = None - else: - self.parent.right = None - self.parent = None - else: - # This node is black and its child is red - # Move the child node here and make it black - self.label = child.label - self.left = child.left - self.right = child.right - if self.left: - self.left.parent = self - if self.right: - self.right.parent = self - elif self.label > label: - if self.left: - self.left.remove(label) - else: - if self.right: - self.right.remove(label) - return self.parent or self - - def _remove_repair(self): - """Repair the coloring of the tree that may have been messed up.""" - if color(self.sibling) == 1: - self.sibling.color = 0 - self.parent.color = 1 - if self.is_left(): - self.parent.rotate_left() - else: - self.parent.rotate_right() - if color(self.parent) == 0 and color(self.sibling) == 0 \ - and color(self.sibling.left) == 0 \ - and color(self.sibling.right) == 0: - self.sibling.color = 1 - self.parent._remove_repair() - return - if color(self.parent) == 1 and color(self.sibling) == 0 \ - and color(self.sibling.left) == 0 \ - and color(self.sibling.right) == 0: - self.sibling.color = 1 - self.parent.color = 0 - return - if (self.is_left() - and color(self.sibling) == 0 - and color(self.sibling.right) == 0 - and color(self.sibling.left) == 1): - self.sibling.rotate_right() - self.sibling.color = 0 - self.sibling.right.color = 1 - if (self.is_right() - and color(self.sibling) == 0 - and color(self.sibling.right) == 1 - and color(self.sibling.left) == 0): - self.sibling.rotate_left() - self.sibling.color = 0 - self.sibling.left.color = 1 - if (self.is_left() - and color(self.sibling) == 0 - and color(self.sibling.right) == 1): - self.parent.rotate_left() - self.grandparent.color = self.parent.color - self.parent.color = 0 - self.parent.sibling.color = 0 - if (self.is_right() - and color(self.sibling) == 0 - and color(self.sibling.left) == 1): - self.parent.rotate_right() - self.grandparent.color = self.parent.color - self.parent.color = 0 - self.parent.sibling.color = 0 - - def check_color_properties(self): - """Check the coloring of the tree, and return True iff the tree - is colored in a way which matches these five properties: - (wording stolen from wikipedia article) - 1. Each node is either red or black. - 2. The root node is black. - 3. All leaves are black. - 4. If a node is red, then both its children are black. - 5. Every path from any node to all of its descendent NIL nodes - has the same number of black nodes. - - This function runs in O(n) time, because properties 4 and 5 take - that long to check. - """ - # I assume property 1 to hold because there is nothing that can - # make the color be anything other than 0 or 1. - - # Property 2 - if self.color: - # The root was red - print('Property 2') - return False; - - # Property 3 does not need to be checked, because None is assumed - # to be black and is all the leaves. - - # Property 4 - if not self.check_coloring(): - print('Property 4') - return False - - # Property 5 - if self.black_height() is None: - print('Property 5') - return False - # All properties were met - return True - - def check_coloring(self): - """A helper function to recursively check Property 4 of a - Red-Black Tree. See check_color_properties for more info. - """ - if self.color == 1: - if color(self.left) == 1 or color(self.right) == 1: - return False - if self.left and not self.left.check_coloring(): - return False - if self.right and not self.right.check_coloring(): - return False - return True - - def black_height(self): - """Returns the number of black nodes from this node to the - leaves of the tree, or None if there isn't one such value (the - tree is color incorrectly). - """ - if self is None: - # If we're already at a leaf, there is no path - return 1 - left = RedBlackTree.black_height(self.left) - right = RedBlackTree.black_height(self.right) - if left is None or right is None: - # There are issues with coloring below children nodes - return None - if left != right: - # The two children have unequal depths - return None - # Return the black depth of children, plus one if this node is - # black - return left + (1-self.color) - - # Here are functions which are general to all binary search trees - - def __contains__(self, label): - """Search through the tree for label, returning True iff it is - found somewhere in the tree. - - Guaranteed to run in O(log(n)) time. - """ - return self.search(label) is not None - - def search(self, label): - """Search through the tree for label, returning its node if - it's found, and None otherwise. - - This method is guaranteed to run in O(log(n)) time. - """ - if self.label == label: - return self - elif label > self.label: - if self.right is None: - return None - else: - return self.right.search(label) - else: - if self.left is None: - return None - else: - return self.left.search(label) - - def floor(self, label): - """Returns the largest element in this tree which is at most label. - - This method is guaranteed to run in O(log(n)) time.""" - if self.label == label: - return self.label - elif self.label > label: - if self.left: - return self.left.floor(label) - else: - return None - else: - if self.right: - attempt = self.right.floor(label) - if attempt is not None: - return attempt - return self.label - - def ceil(self, label): - """Returns the smallest element in this tree which is at least label. - - This method is guaranteed to run in O(log(n)) time. - """ - if self.label == label: - return self.label - elif self.label < label: - if self.right: - return self.right.ceil(label) - else: - return None - else: - if self.left: - attempt = self.left.ceil(label) - if attempt is not None: - return attempt - return self.label - - def get_max(self): - """Returns the largest element in this tree. - - This method is guaranteed to run in O(log(n)) time. - """ - if self.right: - # Go as far right as possible - return self.right.get_max() - else: - return self.label - - def get_min(self): - """Returns the smallest element in this tree. - - This method is guaranteed to run in O(log(n)) time. - """ - if self.left: - # Go as far left as possible - return self.left.get_min() - else: - return self.label - - @property - def grandparent(self): - """Get the current node's grandparent, or None if it doesn't exist.""" - if self.parent is None: - return None - else: - return self.parent.parent - - @property - def sibling(self): - """Get the current node's sibling, or None if it doesn't exist.""" - if self.parent is None: - return None - elif self.parent.left is self: - return self.parent.right - else: - return self.parent.left - - def is_left(self): - """Returns true iff this node is the left child of its parent.""" - return self.parent and self.parent.left is self - - def is_right(self): - """Returns true iff this node is the right child of its parent.""" - return self.parent and self.parent.right is self - - def __bool__(self): - return True - - def __len__(self): - """ - Return the number of nodes in this tree. - """ - ln = 1 - if self.left: - ln += len(self.left) - if self.right: - ln += len(self.right) - return ln - - def preorder_traverse(self): - yield self.label - if self.left: - yield from self.left.preorder_traverse() - if self.right: - yield from self.right.preorder_traverse() - - def inorder_traverse(self): - if self.left: - yield from self.left.inorder_traverse() - yield self.label - if self.right: - yield from self.right.inorder_traverse() - - - def postorder_traverse(self): - if self.left: - yield from self.left.postorder_traverse() - if self.right: - yield from self.right.postorder_traverse() - yield self.label - - def __repr__(self): - from pprint import pformat - if self.left is None and self.right is None: - return "'%s %s'" % (self.label, (self.color and 'red') or 'blk') - return pformat({'%s %s' % (self.label, (self.color and 'red') or 'blk'): - (self.left, self.right)}, - indent=1) - - def __eq__(self, other): - """Test if two trees are equal.""" - if self.label == other.label: - return self.left == other.left and self.right == other.right - else: - return False - -def color(node): - """Returns the color of a node, allowing for None leaves.""" - if node is None: - return 0 - else: - return node.color - -""" -Code for testing the various functions of the red-black tree. -""" - -def test_rotations(): - """Test that the rotate_left and rotate_right functions work.""" - # Make a tree to test on - tree = RedBlackTree(0) - tree.left = RedBlackTree(-10, parent=tree) - tree.right = RedBlackTree(10, parent=tree) - tree.left.left = RedBlackTree(-20, parent=tree.left) - tree.left.right = RedBlackTree(-5, parent=tree.left) - tree.right.left = RedBlackTree(5, parent=tree.right) - tree.right.right = RedBlackTree(20, parent=tree.right) - # Make the right rotation - left_rot = RedBlackTree(10) - left_rot.left = RedBlackTree(0, parent=left_rot) - left_rot.left.left = RedBlackTree(-10, parent=left_rot.left) - left_rot.left.right = RedBlackTree(5, parent=left_rot.left) - left_rot.left.left.left = RedBlackTree(-20, parent=left_rot.left.left) - left_rot.left.left.right = RedBlackTree(-5, parent=left_rot.left.left) - left_rot.right = RedBlackTree(20, parent=left_rot) - tree = tree.rotate_left() - if tree != left_rot: - return False - tree = tree.rotate_right() - tree = tree.rotate_right() - # Make the left rotation - right_rot = RedBlackTree(-10) - right_rot.left = RedBlackTree(-20, parent=right_rot) - right_rot.right = RedBlackTree(0, parent=right_rot) - right_rot.right.left = RedBlackTree(-5, parent=right_rot.right) - right_rot.right.right = RedBlackTree(10, parent=right_rot.right) - right_rot.right.right.left = RedBlackTree(5, parent=right_rot.right.right) - right_rot.right.right.right = RedBlackTree(20, parent=right_rot.right.right) - if tree != right_rot: - return False - return True - -def test_insertion_speed(): - """Test that the tree balances inserts to O(log(n)) by doing a lot - of them. - """ - tree = RedBlackTree(-1) - for i in range(300000): - tree = tree.insert(i) - return True - -def test_insert(): - """Test the insert() method of the tree correctly balances, colors, - and inserts. - """ - tree = RedBlackTree(0) - tree.insert(8) - tree.insert(-8) - tree.insert(4) - tree.insert(12) - tree.insert(10) - tree.insert(11) - ans = RedBlackTree(0, 0) - ans.left = RedBlackTree(-8, 0, ans) - ans.right = RedBlackTree(8, 1, ans) - ans.right.left = RedBlackTree(4, 0, ans.right) - ans.right.right = RedBlackTree(11, 0, ans.right) - ans.right.right.left = RedBlackTree(10, 1, ans.right.right) - ans.right.right.right = RedBlackTree(12, 1, ans.right.right) - return tree == ans - -def test_insert_and_search(): - """Tests searching through the tree for values.""" - tree = RedBlackTree(0) - tree.insert(8) - tree.insert(-8) - tree.insert(4) - tree.insert(12) - tree.insert(10) - tree.insert(11) - if 5 in tree or -6 in tree or -10 in tree or 13 in tree: - # Found something not in there - return False - if not (11 in tree and 12 in tree and -8 in tree and 0 in tree): - # Didn't find something in there - return False - return True - -def test_insert_delete(): - """Test the insert() and delete() method of the tree, verifying the - insertion and removal of elements, and the balancing of the tree. - """ - tree = RedBlackTree(0) - tree = tree.insert(-12) - tree = tree.insert(8) - tree = tree.insert(-8) - tree = tree.insert(15) - tree = tree.insert(4) - tree = tree.insert(12) - tree = tree.insert(10) - tree = tree.insert(9) - tree = tree.insert(11) - tree = tree.remove(15) - tree = tree.remove(-12) - tree = tree.remove(9) - if not tree.check_color_properties(): - return False - if list(tree.inorder_traverse()) != [-8, 0, 4, 8, 10, 11, 12]: - return False - return True - -def test_floor_ceil(): - """Tests the floor and ceiling functions in the tree.""" - tree = RedBlackTree(0) - tree.insert(-16) - tree.insert(16) - tree.insert(8) - tree.insert(24) - tree.insert(20) - tree.insert(22) - tuples = [(-20, None, -16), (-10, -16, 0), (8, 8, 8), (50, 24, None)] - for val, floor, ceil in tuples: - if tree.floor(val) != floor or tree.ceil(val) != ceil: - return False - return True - -def test_min_max(): - """Tests the min and max functions in the tree.""" - tree = RedBlackTree(0) - tree.insert(-16) - tree.insert(16) - tree.insert(8) - tree.insert(24) - tree.insert(20) - tree.insert(22) - if tree.get_max() != 22 or tree.get_min() != -16: - return False - return True - -def test_tree_traversal(): - """Tests the three different tree traversal functions.""" - tree = RedBlackTree(0) - tree.insert(-16) - tree.insert(16) - tree.insert(8) - tree.insert(24) - tree.insert(20) - tree.insert(22) - if list(tree.inorder_traverse()) != [-16, 0, 8, 16, 20, 22, 24]: - return False - if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: - return False - if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]: - return False - return True - -def main(): - if test_rotations(): - print('Rotating right and left works!') - else: - print('Rotating right and left doesn\'t work. :(') - if test_insert(): - print('Inserting works!') - else: - print('Inserting doesn\'t work :(') - if test_insert_and_search(): - print('Searching works!') - else: - print('Searching doesn\'t work :(') - if test_insert_delete(): - print('Deleting works!') - else: - print('Deleting doesn\'t work :(') - if test_floor_ceil(): - print('Floor and ceil work!') - else: - print('Floor and ceil don\'t work :(') - if test_tree_traversal(): - print('Tree traversal works!') - else: - print('Tree traversal doesn\'t work :(') - print('Testing tree balancing...') - print('This should only be a few seconds.') - test_insertion_speed() - print('Done!') - -if __name__ == '__main__': - main() +""" +python/black : true +flake8 : passed +""" + + +class RedBlackTree: + """ + A Red-Black tree, which is a self-balancing BST (binary search + tree). + This tree has similar performance to AVL trees, but the balancing is + less strict, so it will perform faster for writing/deleting nodes + and slower for reading in the average case, though, because they're + both balanced binary search trees, both will get the same asymptotic + performance. + To read more about them, https://en.wikipedia.org/wiki/Red–black_tree + Unless otherwise specified, all asymptotic runtimes are specified in + terms of the size of the tree. + """ + + def __init__(self, label=None, color=0, parent=None, left=None, right=None): + """Initialize a new Red-Black Tree node with the given values: + label: The value associated with this node + color: 0 if black, 1 if red + parent: The parent to this node + left: This node's left child + right: This node's right child + """ + self.label = label + self.parent = parent + self.left = left + self.right = right + self.color = color + + # Here are functions which are specific to red-black trees + + def rotate_left(self): + """Rotate the subtree rooted at this node to the left and + returns the new root to this subtree. + Performing one rotation can be done in O(1). + """ + parent = self.parent + right = self.right + self.right = right.left + if self.right: + self.right.parent = self + self.parent = right + right.left = self + if parent is not None: + if parent.left == self: + parent.left = right + else: + parent.right = right + right.parent = parent + return right + + def rotate_right(self): + """Rotate the subtree rooted at this node to the right and + returns the new root to this subtree. + Performing one rotation can be done in O(1). + """ + parent = self.parent + left = self.left + self.left = left.right + if self.left: + self.left.parent = self + self.parent = left + left.right = self + if parent is not None: + if parent.right is self: + parent.right = left + else: + parent.left = left + left.parent = parent + return left + + def insert(self, label): + """Inserts label into the subtree rooted at self, performs any + rotations necessary to maintain balance, and then returns the + new root to this subtree (likely self). + This is guaranteed to run in O(log(n)) time. + """ + if self.label is None: + # Only possible with an empty tree + self.label = label + return self + if self.label == label: + return self + elif self.label > label: + if self.left: + self.left.insert(label) + else: + self.left = RedBlackTree(label, 1, self) + self.left._insert_repair() + else: + if self.right: + self.right.insert(label) + else: + self.right = RedBlackTree(label, 1, self) + self.right._insert_repair() + return self.parent or self + + def _insert_repair(self): + """Repair the coloring from inserting into a tree.""" + if self.parent is None: + # This node is the root, so it just needs to be black + self.color = 0 + elif color(self.parent) == 0: + # If the parent is black, then it just needs to be red + self.color = 1 + else: + uncle = self.parent.sibling + if color(uncle) == 0: + if self.is_left() and self.parent.is_right(): + self.parent.rotate_right() + self.right._insert_repair() + elif self.is_right() and self.parent.is_left(): + self.parent.rotate_left() + self.left._insert_repair() + elif self.is_left(): + self.grandparent.rotate_right() + self.parent.color = 0 + self.parent.right.color = 1 + else: + self.grandparent.rotate_left() + self.parent.color = 0 + self.parent.left.color = 1 + else: + self.parent.color = 0 + uncle.color = 0 + self.grandparent.color = 1 + self.grandparent._insert_repair() + + def remove(self, label): + """Remove label from this tree.""" + if self.label == label: + if self.left and self.right: + # It's easier to balance a node with at most one child, + # so we replace this node with the greatest one less than + # it and remove that. + value = self.left.get_max() + self.label = value + self.left.remove(value) + else: + # This node has at most one non-None child, so we don't + # need to replace + child = self.left or self.right + if self.color == 1: + # This node is red, and its child is black + # The only way this happens to a node with one child + # is if both children are None leaves. + # We can just remove this node and call it a day. + if self.is_left(): + self.parent.left = None + else: + self.parent.right = None + else: + # The node is black + if child is None: + # This node and its child are black + if self.parent is None: + # The tree is now empty + return RedBlackTree(None) + else: + self._remove_repair() + if self.is_left(): + self.parent.left = None + else: + self.parent.right = None + self.parent = None + else: + # This node is black and its child is red + # Move the child node here and make it black + self.label = child.label + self.left = child.left + self.right = child.right + if self.left: + self.left.parent = self + if self.right: + self.right.parent = self + elif self.label > label: + if self.left: + self.left.remove(label) + else: + if self.right: + self.right.remove(label) + return self.parent or self + + def _remove_repair(self): + """Repair the coloring of the tree that may have been messed up.""" + if color(self.sibling) == 1: + self.sibling.color = 0 + self.parent.color = 1 + if self.is_left(): + self.parent.rotate_left() + else: + self.parent.rotate_right() + if ( + color(self.parent) == 0 + and color(self.sibling) == 0 + and color(self.sibling.left) == 0 + and color(self.sibling.right) == 0 + ): + self.sibling.color = 1 + self.parent._remove_repair() + return + if ( + color(self.parent) == 1 + and color(self.sibling) == 0 + and color(self.sibling.left) == 0 + and color(self.sibling.right) == 0 + ): + self.sibling.color = 1 + self.parent.color = 0 + return + if ( + self.is_left() + and color(self.sibling) == 0 + and color(self.sibling.right) == 0 + and color(self.sibling.left) == 1 + ): + self.sibling.rotate_right() + self.sibling.color = 0 + self.sibling.right.color = 1 + if ( + self.is_right() + and color(self.sibling) == 0 + and color(self.sibling.right) == 1 + and color(self.sibling.left) == 0 + ): + self.sibling.rotate_left() + self.sibling.color = 0 + self.sibling.left.color = 1 + if ( + self.is_left() + and color(self.sibling) == 0 + and color(self.sibling.right) == 1 + ): + self.parent.rotate_left() + self.grandparent.color = self.parent.color + self.parent.color = 0 + self.parent.sibling.color = 0 + if ( + self.is_right() + and color(self.sibling) == 0 + and color(self.sibling.left) == 1 + ): + self.parent.rotate_right() + self.grandparent.color = self.parent.color + self.parent.color = 0 + self.parent.sibling.color = 0 + + def check_color_properties(self): + """Check the coloring of the tree, and return True iff the tree + is colored in a way which matches these five properties: + (wording stolen from wikipedia article) + 1. Each node is either red or black. + 2. The root node is black. + 3. All leaves are black. + 4. If a node is red, then both its children are black. + 5. Every path from any node to all of its descendent NIL nodes + has the same number of black nodes. + This function runs in O(n) time, because properties 4 and 5 take + that long to check. + """ + # I assume property 1 to hold because there is nothing that can + # make the color be anything other than 0 or 1. + + # Property 2 + if self.color: + # The root was red + print("Property 2") + return False + + # Property 3 does not need to be checked, because None is assumed + # to be black and is all the leaves. + + # Property 4 + if not self.check_coloring(): + print("Property 4") + return False + + # Property 5 + if self.black_height() is None: + print("Property 5") + return False + # All properties were met + return True + + def check_coloring(self): + """A helper function to recursively check Property 4 of a + Red-Black Tree. See check_color_properties for more info. + """ + if self.color == 1: + if color(self.left) == 1 or color(self.right) == 1: + return False + if self.left and not self.left.check_coloring(): + return False + if self.right and not self.right.check_coloring(): + return False + return True + + def black_height(self): + """Returns the number of black nodes from this node to the + leaves of the tree, or None if there isn't one such value (the + tree is color incorrectly). + """ + if self is None: + # If we're already at a leaf, there is no path + return 1 + left = RedBlackTree.black_height(self.left) + right = RedBlackTree.black_height(self.right) + if left is None or right is None: + # There are issues with coloring below children nodes + return None + if left != right: + # The two children have unequal depths + return None + # Return the black depth of children, plus one if this node is + # black + return left + (1 - self.color) + + # Here are functions which are general to all binary search trees + + def __contains__(self, label): + """Search through the tree for label, returning True iff it is + found somewhere in the tree. + Guaranteed to run in O(log(n)) time. + """ + return self.search(label) is not None + + def search(self, label): + """Search through the tree for label, returning its node if + it's found, and None otherwise. + This method is guaranteed to run in O(log(n)) time. + """ + if self.label == label: + return self + elif label > self.label: + if self.right is None: + return None + else: + return self.right.search(label) + else: + if self.left is None: + return None + else: + return self.left.search(label) + + def floor(self, label): + """Returns the largest element in this tree which is at most label. + This method is guaranteed to run in O(log(n)) time.""" + if self.label == label: + return self.label + elif self.label > label: + if self.left: + return self.left.floor(label) + else: + return None + else: + if self.right: + attempt = self.right.floor(label) + if attempt is not None: + return attempt + return self.label + + def ceil(self, label): + """Returns the smallest element in this tree which is at least label. + This method is guaranteed to run in O(log(n)) time. + """ + if self.label == label: + return self.label + elif self.label < label: + if self.right: + return self.right.ceil(label) + else: + return None + else: + if self.left: + attempt = self.left.ceil(label) + if attempt is not None: + return attempt + return self.label + + def get_max(self): + """Returns the largest element in this tree. + This method is guaranteed to run in O(log(n)) time. + """ + if self.right: + # Go as far right as possible + return self.right.get_max() + else: + return self.label + + def get_min(self): + """Returns the smallest element in this tree. + This method is guaranteed to run in O(log(n)) time. + """ + if self.left: + # Go as far left as possible + return self.left.get_min() + else: + return self.label + + @property + def grandparent(self): + """Get the current node's grandparent, or None if it doesn't exist.""" + if self.parent is None: + return None + else: + return self.parent.parent + + @property + def sibling(self): + """Get the current node's sibling, or None if it doesn't exist.""" + if self.parent is None: + return None + elif self.parent.left is self: + return self.parent.right + else: + return self.parent.left + + def is_left(self): + """Returns true iff this node is the left child of its parent.""" + return self.parent and self.parent.left is self + + def is_right(self): + """Returns true iff this node is the right child of its parent.""" + return self.parent and self.parent.right is self + + def __bool__(self): + return True + + def __len__(self): + """ + Return the number of nodes in this tree. + """ + ln = 1 + if self.left: + ln += len(self.left) + if self.right: + ln += len(self.right) + return ln + + def preorder_traverse(self): + yield self.label + if self.left: + yield from self.left.preorder_traverse() + if self.right: + yield from self.right.preorder_traverse() + + def inorder_traverse(self): + if self.left: + yield from self.left.inorder_traverse() + yield self.label + if self.right: + yield from self.right.inorder_traverse() + + def postorder_traverse(self): + if self.left: + yield from self.left.postorder_traverse() + if self.right: + yield from self.right.postorder_traverse() + yield self.label + + def __repr__(self): + from pprint import pformat + + if self.left is None and self.right is None: + return "'{} {}'".format(self.label, (self.color and "red") or "blk") + return pformat( + { + "%s %s" + % (self.label, (self.color and "red") or "blk"): (self.left, self.right) + }, + indent=1, + ) + + def __eq__(self, other): + """Test if two trees are equal.""" + if self.label == other.label: + return self.left == other.left and self.right == other.right + else: + return False + + +def color(node): + """Returns the color of a node, allowing for None leaves.""" + if node is None: + return 0 + else: + return node.color + + +""" +Code for testing the various +functions of the red-black tree. +""" + + +def test_rotations(): + """Test that the rotate_left and rotate_right functions work.""" + # Make a tree to test on + tree = RedBlackTree(0) + tree.left = RedBlackTree(-10, parent=tree) + tree.right = RedBlackTree(10, parent=tree) + tree.left.left = RedBlackTree(-20, parent=tree.left) + tree.left.right = RedBlackTree(-5, parent=tree.left) + tree.right.left = RedBlackTree(5, parent=tree.right) + tree.right.right = RedBlackTree(20, parent=tree.right) + # Make the right rotation + left_rot = RedBlackTree(10) + left_rot.left = RedBlackTree(0, parent=left_rot) + left_rot.left.left = RedBlackTree(-10, parent=left_rot.left) + left_rot.left.right = RedBlackTree(5, parent=left_rot.left) + left_rot.left.left.left = RedBlackTree(-20, parent=left_rot.left.left) + left_rot.left.left.right = RedBlackTree(-5, parent=left_rot.left.left) + left_rot.right = RedBlackTree(20, parent=left_rot) + tree = tree.rotate_left() + if tree != left_rot: + return False + tree = tree.rotate_right() + tree = tree.rotate_right() + # Make the left rotation + right_rot = RedBlackTree(-10) + right_rot.left = RedBlackTree(-20, parent=right_rot) + right_rot.right = RedBlackTree(0, parent=right_rot) + right_rot.right.left = RedBlackTree(-5, parent=right_rot.right) + right_rot.right.right = RedBlackTree(10, parent=right_rot.right) + right_rot.right.right.left = RedBlackTree(5, parent=right_rot.right.right) + right_rot.right.right.right = RedBlackTree(20, parent=right_rot.right.right) + if tree != right_rot: + return False + return True + + +def test_insertion_speed(): + """Test that the tree balances inserts to O(log(n)) by doing a lot + of them. + """ + tree = RedBlackTree(-1) + for i in range(300000): + tree = tree.insert(i) + return True + + +def test_insert(): + """Test the insert() method of the tree correctly balances, colors, + and inserts. + """ + tree = RedBlackTree(0) + tree.insert(8) + tree.insert(-8) + tree.insert(4) + tree.insert(12) + tree.insert(10) + tree.insert(11) + ans = RedBlackTree(0, 0) + ans.left = RedBlackTree(-8, 0, ans) + ans.right = RedBlackTree(8, 1, ans) + ans.right.left = RedBlackTree(4, 0, ans.right) + ans.right.right = RedBlackTree(11, 0, ans.right) + ans.right.right.left = RedBlackTree(10, 1, ans.right.right) + ans.right.right.right = RedBlackTree(12, 1, ans.right.right) + return tree == ans + + +def test_insert_and_search(): + """Tests searching through the tree for values.""" + tree = RedBlackTree(0) + tree.insert(8) + tree.insert(-8) + tree.insert(4) + tree.insert(12) + tree.insert(10) + tree.insert(11) + if 5 in tree or -6 in tree or -10 in tree or 13 in tree: + # Found something not in there + return False + if not (11 in tree and 12 in tree and -8 in tree and 0 in tree): + # Didn't find something in there + return False + return True + + +def test_insert_delete(): + """Test the insert() and delete() method of the tree, verifying the + insertion and removal of elements, and the balancing of the tree. + """ + tree = RedBlackTree(0) + tree = tree.insert(-12) + tree = tree.insert(8) + tree = tree.insert(-8) + tree = tree.insert(15) + tree = tree.insert(4) + tree = tree.insert(12) + tree = tree.insert(10) + tree = tree.insert(9) + tree = tree.insert(11) + tree = tree.remove(15) + tree = tree.remove(-12) + tree = tree.remove(9) + if not tree.check_color_properties(): + return False + if list(tree.inorder_traverse()) != [-8, 0, 4, 8, 10, 11, 12]: + return False + return True + + +def test_floor_ceil(): + """Tests the floor and ceiling functions in the tree.""" + tree = RedBlackTree(0) + tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + tuples = [(-20, None, -16), (-10, -16, 0), (8, 8, 8), (50, 24, None)] + for val, floor, ceil in tuples: + if tree.floor(val) != floor or tree.ceil(val) != ceil: + return False + return True + + +def test_min_max(): + """Tests the min and max functions in the tree.""" + tree = RedBlackTree(0) + tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + if tree.get_max() != 22 or tree.get_min() != -16: + return False + return True + + +def test_tree_traversal(): + """Tests the three different tree traversal functions.""" + tree = RedBlackTree(0) + tree = tree.insert(-16) + tree.insert(16) + tree.insert(8) + tree.insert(24) + tree.insert(20) + tree.insert(22) + if list(tree.inorder_traverse()) != [-16, 0, 8, 16, 20, 22, 24]: + return False + if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: + return False + if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]: + return False + return True + + +def test_tree_chaining(): + """Tests the three different tree chaining functions.""" + tree = RedBlackTree(0) + tree = tree.insert(-16).insert(16).insert(8).insert(24).insert(20).insert(22) + if list(tree.inorder_traverse()) != [-16, 0, 8, 16, 20, 22, 24]: + return False + if list(tree.preorder_traverse()) != [0, -16, 16, 8, 22, 20, 24]: + return False + if list(tree.postorder_traverse()) != [-16, 8, 20, 24, 22, 16, 0]: + return False + return True + + +def print_results(msg: str, passes: bool) -> None: + print(str(msg), "works!" if passes else "doesn't work :(") + + +def pytests(): + assert test_rotations() + assert test_insert() + assert test_insert_and_search() + assert test_insert_delete() + assert test_floor_ceil() + assert test_tree_traversal() + assert test_tree_chaining() + + +def main(): + """ + >>> pytests() + """ + print_results("Rotating right and left", test_rotations()) + + print_results("Inserting", test_insert()) + + print_results("Searching", test_insert_and_search()) + + print_results("Deleting", test_insert_delete()) + + print_results("Floor and ceil", test_floor_ceil()) + + print_results("Tree traversal", test_tree_traversal()) + + print_results("Tree traversal", test_tree_chaining()) + + print("Testing tree balancing...") + print("This should only be a few seconds.") + test_insertion_speed() + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/data_structures/binary_tree/segment_tree.py b/data_structures/binary_tree/segment_tree.py index 001bf999f391..ad9476b4514b 100644 --- a/data_structures/binary_tree/segment_tree.py +++ b/data_structures/binary_tree/segment_tree.py @@ -1,13 +1,14 @@ -from __future__ import print_function import math + class SegmentTree: - def __init__(self, A): self.N = len(A) - self.st = [0] * (4 * self.N) # approximate the overall size of segment tree with array N + self.st = [0] * ( + 4 * self.N + ) # approximate the overall size of segment tree with array N self.build(1, 0, self.N - 1) - + def left(self, idx): return idx * 2 @@ -21,51 +22,55 @@ def build(self, idx, l, r): mid = (l + r) // 2 self.build(self.left(idx), l, mid) self.build(self.right(idx), mid + 1, r) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) - + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) + def update(self, a, b, val): return self.update_recursive(1, 0, self.N - 1, a - 1, b - 1, val) - - def update_recursive(self, idx, l, r, a, b, val): # update(1, 1, N, a, b, v) for update val v to [a,b] + + def update_recursive( + self, idx, l, r, a, b, val + ): # update(1, 1, N, a, b, v) for update val v to [a,b] if r < a or l > b: return True - if l == r : + if l == r: self.st[idx] = val return True - mid = (l+r)//2 + mid = (l + r) // 2 self.update_recursive(self.left(idx), l, mid, a, b, val) - self.update_recursive(self.right(idx), mid+1, r, a, b, val) - self.st[idx] = max(self.st[self.left(idx)] , self.st[self.right(idx)]) + self.update_recursive(self.right(idx), mid + 1, r, a, b, val) + self.st[idx] = max(self.st[self.left(idx)], self.st[self.right(idx)]) return True def query(self, a, b): return self.query_recursive(1, 0, self.N - 1, a - 1, b - 1) - def query_recursive(self, idx, l, r, a, b): #query(1, 1, N, a, b) for query max of [a,b] + def query_recursive( + self, idx, l, r, a, b + ): # query(1, 1, N, a, b) for query max of [a,b] if r < a or l > b: return -math.inf if l >= a and r <= b: return self.st[idx] - mid = (l+r)//2 + mid = (l + r) // 2 q1 = self.query_recursive(self.left(idx), l, mid, a, b) q2 = self.query_recursive(self.right(idx), mid + 1, r, a, b) return max(q1, q2) def showData(self): showList = [] - for i in range(1,N+1): + for i in range(1, N + 1): showList += [self.query(i, i)] - print (showList) - + print(showList) + -if __name__ == '__main__': - A = [1,2,-4,7,3,-5,6,11,-20,9,14,15,5,2,-8] +if __name__ == "__main__": + A = [1, 2, -4, 7, 3, -5, 6, 11, -20, 9, 14, 15, 5, 2, -8] N = 15 segt = SegmentTree(A) - print (segt.query(4, 6)) - print (segt.query(7, 11)) - print (segt.query(7, 12)) - segt.update(1,3,111) - print (segt.query(1, 15)) - segt.update(7,8,235) + print(segt.query(4, 6)) + print(segt.query(7, 11)) + print(segt.query(7, 12)) + segt.update(1, 3, 111) + print(segt.query(1, 15)) + segt.update(7, 8, 235) segt.showData() diff --git a/data_structures/binary_tree/segment_tree_other.py b/data_structures/binary_tree/segment_tree_other.py new file mode 100644 index 000000000000..c3ab493d5f4f --- /dev/null +++ b/data_structures/binary_tree/segment_tree_other.py @@ -0,0 +1,242 @@ +""" +Segment_tree creates a segment tree with a given array and function, +allowing queries to be done later in log(N) time +function takes 2 values and returns a same type value +""" + +from queue import Queue +from collections.abc import Sequence + + +class SegmentTreeNode(object): + def __init__(self, start, end, val, left=None, right=None): + self.start = start + self.end = end + self.val = val + self.mid = (start + end) // 2 + self.left = left + self.right = right + + def __str__(self): + return "val: %s, start: %s, end: %s" % (self.val, self.start, self.end) + + +class SegmentTree(object): + """ + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> for node in num_arr.traverse(): + ... print(node) + ... + val: 15, start: 0, end: 4 + val: 8, start: 0, end: 2 + val: 7, start: 3, end: 4 + val: 3, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 1, start: 1, end: 1 + >>> + >>> num_arr.update(1, 5) + >>> for node in num_arr.traverse(): + ... print(node) + ... + val: 19, start: 0, end: 4 + val: 12, start: 0, end: 2 + val: 7, start: 3, end: 4 + val: 7, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 5, start: 1, end: 1 + >>> + >>> num_arr.query_range(3, 4) + 7 + >>> num_arr.query_range(2, 2) + 5 + >>> num_arr.query_range(1, 3) + 13 + >>> + >>> max_arr = SegmentTree([2, 1, 5, 3, 4], max) + >>> for node in max_arr.traverse(): + ... print(node) + ... + val: 5, start: 0, end: 4 + val: 5, start: 0, end: 2 + val: 4, start: 3, end: 4 + val: 2, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 1, start: 1, end: 1 + >>> + >>> max_arr.update(1, 5) + >>> for node in max_arr.traverse(): + ... print(node) + ... + val: 5, start: 0, end: 4 + val: 5, start: 0, end: 2 + val: 4, start: 3, end: 4 + val: 5, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 5, start: 1, end: 1 + >>> + >>> max_arr.query_range(3, 4) + 4 + >>> max_arr.query_range(2, 2) + 5 + >>> max_arr.query_range(1, 3) + 5 + >>> + >>> min_arr = SegmentTree([2, 1, 5, 3, 4], min) + >>> for node in min_arr.traverse(): + ... print(node) + ... + val: 1, start: 0, end: 4 + val: 1, start: 0, end: 2 + val: 3, start: 3, end: 4 + val: 1, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 1, start: 1, end: 1 + >>> + >>> min_arr.update(1, 5) + >>> for node in min_arr.traverse(): + ... print(node) + ... + val: 2, start: 0, end: 4 + val: 2, start: 0, end: 2 + val: 3, start: 3, end: 4 + val: 2, start: 0, end: 1 + val: 5, start: 2, end: 2 + val: 3, start: 3, end: 3 + val: 4, start: 4, end: 4 + val: 2, start: 0, end: 0 + val: 5, start: 1, end: 1 + >>> + >>> min_arr.query_range(3, 4) + 3 + >>> min_arr.query_range(2, 2) + 5 + >>> min_arr.query_range(1, 3) + 3 + >>> + + """ + + def __init__(self, collection: Sequence, function): + self.collection = collection + self.fn = function + if self.collection: + self.root = self._build_tree(0, len(collection) - 1) + + def update(self, i, val): + """ + Update an element in log(N) time + :param i: position to be update + :param val: new value + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> num_arr.update(1, 5) + >>> num_arr.query_range(1, 3) + 13 + """ + self._update_tree(self.root, i, val) + + def query_range(self, i, j): + """ + Get range query value in log(N) time + :param i: left element index + :param j: right element index + :return: element combined in the range [i, j] + >>> import operator + >>> num_arr = SegmentTree([2, 1, 5, 3, 4], operator.add) + >>> num_arr.update(1, 5) + >>> num_arr.query_range(3, 4) + 7 + >>> num_arr.query_range(2, 2) + 5 + >>> num_arr.query_range(1, 3) + 13 + >>> + """ + return self._query_range(self.root, i, j) + + def _build_tree(self, start, end): + if start == end: + return SegmentTreeNode(start, end, self.collection[start]) + mid = (start + end) // 2 + left = self._build_tree(start, mid) + right = self._build_tree(mid + 1, end) + return SegmentTreeNode(start, end, self.fn(left.val, right.val), left, right) + + def _update_tree(self, node, i, val): + if node.start == i and node.end == i: + node.val = val + return + if i <= node.mid: + self._update_tree(node.left, i, val) + else: + self._update_tree(node.right, i, val) + node.val = self.fn(node.left.val, node.right.val) + + def _query_range(self, node, i, j): + if node.start == i and node.end == j: + return node.val + + if i <= node.mid: + if j <= node.mid: + # range in left child tree + return self._query_range(node.left, i, j) + else: + # range in left child tree and right child tree + return self.fn( + self._query_range(node.left, i, node.mid), + self._query_range(node.right, node.mid + 1, j), + ) + else: + # range in right child tree + return self._query_range(node.right, i, j) + + def traverse(self): + if self.root is not None: + queue = Queue() + queue.put(self.root) + while not queue.empty(): + node = queue.get() + yield node + + if node.left is not None: + queue.put(node.left) + + if node.right is not None: + queue.put(node.right) + + +if __name__ == "__main__": + import operator + + for fn in [operator.add, max, min]: + print("*" * 50) + arr = SegmentTree([2, 1, 5, 3, 4], fn) + for node in arr.traverse(): + print(node) + print() + + arr.update(1, 5) + for node in arr.traverse(): + print(node) + print() + + print(arr.query_range(3, 4)) # 7 + print(arr.query_range(2, 2)) # 5 + print(arr.query_range(1, 3)) # 13 + print() diff --git a/data_structures/binary_tree/treap.py b/data_structures/binary_tree/treap.py index 0399ff67030a..26f021445ca4 100644 --- a/data_structures/binary_tree/treap.py +++ b/data_structures/binary_tree/treap.py @@ -5,125 +5,176 @@ class Node: """ Treap's node - Treap is a binary tree by key and heap by priority + Treap is a binary tree by value and heap by priority """ - def __init__(self, key: int): - self.key = key + + def __init__(self, value: int = None): + self.value = value self.prior = random() - self.l = None - self.r = None + self.left = None + self.right = None + + def __repr__(self): + from pprint import pformat + + if self.left is None and self.right is None: + return f"'{self.value}: {self.prior:.5}'" + else: + return pformat( + {f"{self.value}: {self.prior:.5}": (self.left, self.right)}, indent=1 + ) + def __str__(self): + value = str(self.value) + " " + left = str(self.left or "") + right = str(self.right or "") + return value + left + right -def split(root: Node, key: int) -> Tuple[Node, Node]: + +def split(root: Node, value: int) -> Tuple[Node, Node]: """ - We split current tree into 2 trees with key: + We split current tree into 2 trees with value: - Left tree contains all keys less than split key. - Right tree contains all keys greater or equal, than split key + Left tree contains all values less than split value. + Right tree contains all values greater or equal, than split value """ if root is None: # None tree is split into 2 Nones return (None, None) - if root.key >= key: - """ - Right tree's root will be current node. - Now we split(with the same key) current node's left son - Left tree: left part of that split - Right tree's left son: right part of that split - """ - l, root.l = split(root.l, key) - return (l, root) + elif root.value is None: + return (None, None) else: - """ - Just symmetric to previous case - """ - root.r, r = split(root.r, key) - return (root, r) + if value < root.value: + """ + Right tree's root will be current node. + Now we split(with the same value) current node's left son + Left tree: left part of that split + Right tree's left son: right part of that split + """ + left, root.left = split(root.left, value) + return (left, root) + else: + """ + Just symmetric to previous case + """ + root.right, right = split(root.right, value) + return (root, right) def merge(left: Node, right: Node) -> Node: """ We merge 2 trees into one. - Note: all left tree's keys must be less than all right tree's + Note: all left tree's values must be less than all right tree's """ - if (not left) or (not right): - """ - If one node is None, return the other - """ + if (not left) or (not right): # If one node is None, return the other return left or right - if left.key > right.key: + elif left.prior < right.prior: """ Left will be root because it has more priority Now we need to merge left's right son and right tree """ - left.r = merge(left.r, right) + left.right = merge(left.right, right) return left else: """ Symmetric as well """ - right.l = merge(left, right.l) + right.left = merge(left, right.left) return right -def insert(root: Node, key: int) -> Node: +def insert(root: Node, value: int) -> Node: """ Insert element - Split current tree with a key into l, r, + Split current tree with a value into left, right, Insert new node into the middle - Merge l, node, r into root + Merge left, node, right into root """ - node = Node(key) - l, r = split(root, key) - root = merge(l, node) - root = merge(root, r) - return root + node = Node(value) + left, right = split(root, value) + return merge(merge(left, node), right) -def erase(root: Node, key: int) -> Node: +def erase(root: Node, value: int) -> Node: """ Erase element - Split all nodes with keys less into l, - Split all nodes with keys greater into r. - Merge l, r + Split all nodes with values less into left, + Split all nodes with values greater into right. + Merge left, right """ - l, r = split(root, key) - _, r = split(r, key + 1) - return merge(l, r) + left, right = split(root, value - 1) + _, right = split(right, value) + return merge(left, right) -def node_print(root: Node): +def inorder(root: Node): """ Just recursive print of a tree """ - if not root: + if not root: # None return - node_print(root.l) - print(root.key, end=" ") - node_print(root.r) + else: + inorder(root.left) + print(root.value, end=" ") + inorder(root.right) -def interactTreap(): +def interactTreap(root, args): """ Commands: - + key to add key into treap - - key to erase all nodes with key - - After each command, program prints treap + + value to add value into treap + - value to erase all nodes with value + + >>> root = interactTreap(None, "+1") + >>> inorder(root) + 1 + >>> root = interactTreap(root, "+3 +5 +17 +19 +2 +16 +4 +0") + >>> inorder(root) + 0 1 2 3 4 5 16 17 19 + >>> root = interactTreap(root, "+4 +4 +4") + >>> inorder(root) + 0 1 2 3 4 4 4 4 5 16 17 19 + >>> root = interactTreap(root, "-0") + >>> inorder(root) + 1 2 3 4 4 4 4 5 16 17 19 + >>> root = interactTreap(root, "-4") + >>> inorder(root) + 1 2 3 5 16 17 19 + >>> root = interactTreap(root, "=0") + Unknown command """ - root = None - while True: - cmd = input().split() - cmd[1] = int(cmd[1]) - if cmd[0] == "+": - root = insert(root, cmd[1]) - elif cmd[0] == "-": - root = erase(root, cmd[1]) + for arg in args.split(): + if arg[0] == "+": + root = insert(root, int(arg[1:])) + + elif arg[0] == "-": + root = erase(root, int(arg[1:])) + else: print("Unknown command") - node_print(root) + + return root + + +def main(): + """After each command, program prints treap""" + root = None + print( + "enter numbers to create a tree, + value to add value into treap, - value to erase all nodes with value. 'q' to quit. " + ) + + args = input() + while args != "q": + root = interactTreap(root, args) + print(root) + args = input() + + print("good by!") if __name__ == "__main__": - interactTreap() + import doctest + + doctest.testmod() + main() diff --git a/data_structures/data_structures/heap/heap_generic.py b/data_structures/data_structures/heap/heap_generic.py new file mode 100644 index 000000000000..8993d501331b --- /dev/null +++ b/data_structures/data_structures/heap/heap_generic.py @@ -0,0 +1,163 @@ +class Heap: + """A generic Heap class, can be used as min or max by passing the key function accordingly. + """ + + def __init__(self, key=None): + # Stores actual heap items. + self.arr = list() + # Stores indexes of each item for supporting updates and deletion. + self.pos_map = {} + # Stores current size of heap. + self.size = 0 + # Stores function used to evaluate the score of an item on which basis ordering will be done. + self.key = key or (lambda x: x) + + def _parent(self, i): + """Returns parent index of given index if exists else None""" + return int((i - 1) / 2) if i > 0 else None + + def _left(self, i): + """Returns left-child-index of given index if exists else None""" + left = int(2 * i + 1) + return left if 0 < left < self.size else None + + def _right(self, i): + """Returns right-child-index of given index if exists else None""" + right = int(2 * i + 2) + return right if 0 < right < self.size else None + + def _swap(self, i, j): + """Performs changes required for swapping two elements in the heap""" + # First update the indexes of the items in index map. + self.pos_map[self.arr[i][0]], self.pos_map[self.arr[j][0]] = ( + self.pos_map[self.arr[j][0]], + self.pos_map[self.arr[i][0]], + ) + # Then swap the items in the list. + self.arr[i], self.arr[j] = self.arr[j], self.arr[i] + + def _cmp(self, i, j): + """Compares the two items using default comparison""" + return self.arr[i][1] < self.arr[j][1] + + def _get_valid_parent(self, i): + """Returns index of valid parent as per desired ordering among given index and both it's children""" + left = self._left(i) + right = self._right(i) + valid_parent = i + + if left is not None and not self._cmp(left, valid_parent): + valid_parent = left + if right is not None and not self._cmp(right, valid_parent): + valid_parent = right + + return valid_parent + + def _heapify_up(self, index): + """Fixes the heap in upward direction of given index""" + parent = self._parent(index) + while parent is not None and not self._cmp(index, parent): + self._swap(index, parent) + index, parent = parent, self._parent(parent) + + def _heapify_down(self, index): + """Fixes the heap in downward direction of given index""" + valid_parent = self._get_valid_parent(index) + while valid_parent != index: + self._swap(index, valid_parent) + index, valid_parent = valid_parent, self._get_valid_parent(valid_parent) + + def update_item(self, item, item_value): + """Updates given item value in heap if present""" + if item not in self.pos_map: + return + index = self.pos_map[item] + self.arr[index] = [item, self.key(item_value)] + # Make sure heap is right in both up and down direction. + # Ideally only one of them will make any change. + self._heapify_up(index) + self._heapify_down(index) + + def delete_item(self, item): + """Deletes given item from heap if present""" + if item not in self.pos_map: + return + index = self.pos_map[item] + del self.pos_map[item] + self.arr[index] = self.arr[self.size - 1] + self.pos_map[self.arr[self.size - 1][0]] = index + self.size -= 1 + # Make sure heap is right in both up and down direction. + # Ideally only one of them will make any change- so no performance loss in calling both. + if self.size > index: + self._heapify_up(index) + self._heapify_down(index) + + def insert_item(self, item, item_value): + """Inserts given item with given value in heap""" + arr_len = len(self.arr) + if arr_len == self.size: + self.arr.append([item, self.key(item_value)]) + else: + self.arr[self.size] = [item, self.key(item_value)] + self.pos_map[item] = self.size + self.size += 1 + self._heapify_up(self.size - 1) + + def get_top(self): + """Returns top item tuple (Calculated value, item) from heap if present""" + return self.arr[0] if self.size else None + + def extract_top(self): + """Returns top item tuple (Calculated value, item) from heap and removes it as well if present""" + top_item_tuple = self.get_top() + if top_item_tuple: + self.delete_item(top_item_tuple[0]) + return top_item_tuple + + +def test_heap() -> None: + """ + >>> h = Heap() # Max-heap + >>> h.insert_item(5, 34) + >>> h.insert_item(6, 31) + >>> h.insert_item(7, 37) + >>> h.get_top() + [7, 37] + >>> h.extract_top() + [7, 37] + >>> h.extract_top() + [5, 34] + >>> h.extract_top() + [6, 31] + >>> h = Heap(key=lambda x: -x) # Min heap + >>> h.insert_item(5, 34) + >>> h.insert_item(6, 31) + >>> h.insert_item(7, 37) + >>> h.get_top() + [6, -31] + >>> h.extract_top() + [6, -31] + >>> h.extract_top() + [5, -34] + >>> h.extract_top() + [7, -37] + >>> h.insert_item(8, 45) + >>> h.insert_item(9, 40) + >>> h.insert_item(10, 50) + >>> h.get_top() + [9, -40] + >>> h.update_item(10, 30) + >>> h.get_top() + [10, -30] + >>> h.delete_item(10) + >>> h.get_top() + [9, -40] + """ + pass + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/disjoint_set/disjoint_set.py b/data_structures/disjoint_set/disjoint_set.py new file mode 100644 index 000000000000..a93b89621c4a --- /dev/null +++ b/data_structures/disjoint_set/disjoint_set.py @@ -0,0 +1,79 @@ +""" + disjoint set + Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure +""" + + +class Node: + def __init__(self, data): + self.data = data + + +def make_set(x): + """ + make x as a set. + """ + # rank is the distance from x to its' parent + # root's rank is 0 + x.rank = 0 + x.parent = x + + +def union_set(x, y): + """ + union two sets. + set with bigger rank should be parent, so that the + disjoint set tree will be more flat. + """ + x, y = find_set(x), find_set(y) + if x.rank > y.rank: + y.parent = x + else: + x.parent = y + if x.rank == y.rank: + y.rank += 1 + + +def find_set(x): + """ + return the parent of x + """ + if x != x.parent: + x.parent = find_set(x.parent) + return x.parent + + +def find_python_set(node: Node) -> set: + """ + Return a Python Standard Library set that contains i. + """ + sets = ({0, 1, 2}, {3, 4, 5}) + for s in sets: + if node.data in s: + return s + raise ValueError(f"{node.data} is not in {sets}") + + +def test_disjoint_set(): + """ + >>> test_disjoint_set() + """ + vertex = [Node(i) for i in range(6)] + for v in vertex: + make_set(v) + + union_set(vertex[0], vertex[1]) + union_set(vertex[1], vertex[2]) + union_set(vertex[3], vertex[4]) + union_set(vertex[3], vertex[5]) + + for node0 in vertex: + for node1 in vertex: + if find_python_set(node0).isdisjoint(find_python_set(node1)): + assert find_set(node0) != find_set(node1) + else: + assert find_set(node0) == find_set(node1) + + +if __name__ == "__main__": + test_disjoint_set() diff --git a/data_structures/hashing/double_hash.py b/data_structures/hashing/double_hash.py index 7a0ce0b3a67b..ce4454db0bef 100644 --- a/data_structures/hashing/double_hash.py +++ b/data_structures/hashing/double_hash.py @@ -8,26 +8,35 @@ class DoubleHash(HashTable): """ Hash Table example with open addressing and Double Hash """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def __hash_function_2(self, value, data): - next_prime_gt = next_prime(value % self.size_table) \ - if not check_prime(value % self.size_table) else value % self.size_table #gt = bigger than + next_prime_gt = ( + next_prime(value % self.size_table) + if not check_prime(value % self.size_table) + else value % self.size_table + ) # gt = bigger than return next_prime_gt - (data % next_prime_gt) def __hash_double_function(self, key, data, increment): return (increment * self.__hash_function_2(key, data)) % self.size_table - def _colision_resolution(self, key, data=None): + def _collision_resolution(self, key, data=None): i = 1 new_key = self.hash_function(data) while self.values[new_key] is not None and self.values[new_key] != key: - new_key = self.__hash_double_function(key, data, i) if \ - self.balanced_factor() >= self.lim_charge else None - if new_key is None: break - else: i += 1 + new_key = ( + self.__hash_double_function(key, data, i) + if self.balanced_factor() >= self.lim_charge + else None + ) + if new_key is None: + break + else: + i += 1 return new_key diff --git a/data_structures/hashing/hash_table.py b/data_structures/hashing/hash_table.py index f0de128d1ad1..3b39742f9d09 100644 --- a/data_structures/hashing/hash_table.py +++ b/data_structures/hashing/hash_table.py @@ -19,15 +19,16 @@ def keys(self): return self._keys def balanced_factor(self): - return sum([1 for slot in self.values - if slot is not None]) / (self.size_table * self.charge_factor) + return sum([1 for slot in self.values if slot is not None]) / ( + self.size_table * self.charge_factor + ) def hash_function(self, key): return key % self.size_table def _step_by_step(self, step_ord): - print("step {0}".format(step_ord)) + print(f"step {step_ord}") print([i for i in range(len(self.values))]) print(self.values) @@ -43,11 +44,10 @@ def _set_value(self, key, data): self.values[key] = data self._keys[key] = data - def _colision_resolution(self, key, data=None): + def _collision_resolution(self, key, data=None): new_key = self.hash_function(key + 1) - while self.values[new_key] is not None \ - and self.values[new_key] != key: + while self.values[new_key] is not None and self.values[new_key] != key: if self.values.count(None) > 0: new_key = self.hash_function(new_key + 1) @@ -61,8 +61,9 @@ def rehashing(self): survivor_values = [value for value in self.values if value is not None] self.size_table = next_prime(self.size_table, factor=2) self._keys.clear() - self.values = [None] * self.size_table #hell's pointers D: don't DRY ;/ - map(self.insert_data, survivor_values) + self.values = [None] * self.size_table # hell's pointers D: don't DRY ;/ + for value in survivor_values: + self.insert_data(value) def insert_data(self, data): key = self.hash_function(data) @@ -74,11 +75,9 @@ def insert_data(self, data): pass else: - colision_resolution = self._colision_resolution(key, data) - if colision_resolution is not None: - self._set_value(colision_resolution, data) + collision_resolution = self._collision_resolution(key, data) + if collision_resolution is not None: + self._set_value(collision_resolution, data) else: self.rehashing() self.insert_data(data) - - diff --git a/data_structures/hashing/hash_table_with_linked_list.py b/data_structures/hashing/hash_table_with_linked_list.py index a45876df49bd..48d93bbc5cff 100644 --- a/data_structures/hashing/hash_table_with_linked_list.py +++ b/data_structures/hashing/hash_table_with_linked_list.py @@ -7,18 +7,20 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _set_value(self, key, data): - self.values[key] = deque([]) if self.values[key] is None else self.values[key] + self.values[key] = deque([]) if self.values[key] is None else self.values[key] self.values[key].appendleft(data) self._keys[key] = self.values[key] def balanced_factor(self): - return sum([self.charge_factor - len(slot) for slot in self.values])\ - / self.size_table * self.charge_factor - - def _colision_resolution(self, key, data=None): - if not (len(self.values[key]) == self.charge_factor - and self.values.count(None) == 0): - return key - return super()._colision_resolution(key, data) - + return ( + sum([self.charge_factor - len(slot) for slot in self.values]) + / self.size_table + * self.charge_factor + ) + def _collision_resolution(self, key, data=None): + if not ( + len(self.values[key]) == self.charge_factor and self.values.count(None) == 0 + ): + return key + return super()._collision_resolution(key, data) diff --git a/data_structures/hashing/number_theory/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py index 8a521bc45758..2a966e0da7f2 100644 --- a/data_structures/hashing/number_theory/prime_numbers.py +++ b/data_structures/hashing/number_theory/prime_numbers.py @@ -5,25 +5,25 @@ def check_prime(number): - """ + """ it's not the best solution """ - special_non_primes = [0,1,2] - if number in special_non_primes[:2]: - return 2 - elif number == special_non_primes[-1]: - return 3 - - return all([number % i for i in range(2, number)]) + special_non_primes = [0, 1, 2] + if number in special_non_primes[:2]: + return 2 + elif number == special_non_primes[-1]: + return 3 + + return all([number % i for i in range(2, number)]) def next_prime(value, factor=1, **kwargs): value = factor * value first_value_val = value - + while not check_prime(value): value += 1 if not ("desc" in kwargs.keys() and kwargs["desc"] is True) else -1 - + if value == first_value_val: return next_prime(value + 1, **kwargs) return value diff --git a/data_structures/hashing/quadratic_probing.py b/data_structures/hashing/quadratic_probing.py index 1e61100a81fa..0dd84a5d987c 100644 --- a/data_structures/hashing/quadratic_probing.py +++ b/data_structures/hashing/quadratic_probing.py @@ -7,18 +7,21 @@ class QuadraticProbing(HashTable): """ Basic Hash Table example with open addressing using Quadratic Probing """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def _colision_resolution(self, key, data=None): + def _collision_resolution(self, key, data=None): i = 1 - new_key = self.hash_function(key + i*i) + new_key = self.hash_function(key + i * i) - while self.values[new_key] is not None \ - and self.values[new_key] != key: + while self.values[new_key] is not None and self.values[new_key] != key: i += 1 - new_key = self.hash_function(key + i*i) if not \ - self.balanced_factor() >= self.lim_charge else None + new_key = ( + self.hash_function(key + i * i) + if not self.balanced_factor() >= self.lim_charge + else None + ) if new_key is None: break diff --git a/data_structures/heap/binomial_heap.py b/data_structures/heap/binomial_heap.py new file mode 100644 index 000000000000..ac244023082a --- /dev/null +++ b/data_structures/heap/binomial_heap.py @@ -0,0 +1,401 @@ +""" +Binomial Heap +Reference: Advanced Data Structures, Peter Brass +""" + + +class Node: + """ + Node in a doubly-linked binomial tree, containing: + - value + - size of left subtree + - link to left, right and parent nodes + """ + + def __init__(self, val): + self.val = val + # Number of nodes in left subtree + self.left_tree_size = 0 + self.left = None + self.right = None + self.parent = None + + def mergeTrees(self, other): + """ + In-place merge of two binomial trees of equal size. + Returns the root of the resulting tree + """ + assert self.left_tree_size == other.left_tree_size, "Unequal Sizes of Blocks" + + if self.val < other.val: + other.left = self.right + other.parent = None + if self.right: + self.right.parent = other + self.right = other + self.left_tree_size = self.left_tree_size * 2 + 1 + return self + else: + self.left = other.right + self.parent = None + if other.right: + other.right.parent = self + other.right = self + other.left_tree_size = other.left_tree_size * 2 + 1 + return other + + +class BinomialHeap: + r""" + Min-oriented priority queue implemented with the Binomial Heap data + structure implemented with the BinomialHeap class. It supports: + - Insert element in a heap with n elements: Guaranteed logn, amoratized 1 + - Merge (meld) heaps of size m and n: O(logn + logm) + - Delete Min: O(logn) + - Peek (return min without deleting it): O(1) + + Example: + + Create a random permutation of 30 integers to be inserted and 19 of them deleted + >>> import numpy as np + >>> permutation = np.random.permutation(list(range(30))) + + Create a Heap and insert the 30 integers + __init__() test + >>> first_heap = BinomialHeap() + + 30 inserts - insert() test + >>> for number in permutation: + ... first_heap.insert(number) + + Size test + >>> print(first_heap.size) + 30 + + Deleting - delete() test + >>> for i in range(25): + ... print(first_heap.deleteMin(), end=" ") + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + + Create a new Heap + >>> second_heap = BinomialHeap() + >>> vals = [17, 20, 31, 34] + >>> for value in vals: + ... second_heap.insert(value) + + + The heap should have the following structure: + + 17 + / \ + # 31 + / \ + 20 34 + / \ / \ + # # # # + + preOrder() test + >>> print(second_heap.preOrder()) + [(17, 0), ('#', 1), (31, 1), (20, 2), ('#', 3), ('#', 3), (34, 2), ('#', 3), ('#', 3)] + + printing Heap - __str__() test + >>> print(second_heap) + 17 + -# + -31 + --20 + ---# + ---# + --34 + ---# + ---# + + mergeHeaps() test + >>> merged = second_heap.mergeHeaps(first_heap) + >>> merged.peek() + 17 + + values in merged heap; (merge is inplace) + >>> while not first_heap.isEmpty(): + ... print(first_heap.deleteMin(), end=" ") + 17 20 25 26 27 28 29 31 34 + """ + + def __init__(self, bottom_root=None, min_node=None, heap_size=0): + self.size = heap_size + self.bottom_root = bottom_root + self.min_node = min_node + + def mergeHeaps(self, other): + """ + In-place merge of two binomial heaps. + Both of them become the resulting merged heap + """ + + # Empty heaps corner cases + if other.size == 0: + return + if self.size == 0: + self.size = other.size + self.bottom_root = other.bottom_root + self.min_node = other.min_node + return + # Update size + self.size = self.size + other.size + + # Update min.node + if self.min_node.val > other.min_node.val: + self.min_node = other.min_node + # Merge + + # Order roots by left_subtree_size + combined_roots_list = [] + i, j = self.bottom_root, other.bottom_root + while i or j: + if i and ((not j) or i.left_tree_size < j.left_tree_size): + combined_roots_list.append((i, True)) + i = i.parent + else: + combined_roots_list.append((j, False)) + j = j.parent + # Insert links between them + for i in range(len(combined_roots_list) - 1): + if combined_roots_list[i][1] != combined_roots_list[i + 1][1]: + combined_roots_list[i][0].parent = combined_roots_list[i + 1][0] + combined_roots_list[i + 1][0].left = combined_roots_list[i][0] + # Consecutively merge roots with same left_tree_size + i = combined_roots_list[0][0] + while i.parent: + if ( + (i.left_tree_size == i.parent.left_tree_size) and (not i.parent.parent) + ) or ( + i.left_tree_size == i.parent.left_tree_size + and i.left_tree_size != i.parent.parent.left_tree_size + ): + + # Neighbouring Nodes + previous_node = i.left + next_node = i.parent.parent + + # Merging trees + i = i.mergeTrees(i.parent) + + # Updating links + i.left = previous_node + i.parent = next_node + if previous_node: + previous_node.parent = i + if next_node: + next_node.left = i + else: + i = i.parent + # Updating self.bottom_root + while i.left: + i = i.left + self.bottom_root = i + + # Update other + other.size = self.size + other.bottom_root = self.bottom_root + other.min_node = self.min_node + + # Return the merged heap + return self + + def insert(self, val): + """ + insert a value in the heap + """ + if self.size == 0: + self.bottom_root = Node(val) + self.size = 1 + self.min_node = self.bottom_root + else: + # Create new node + new_node = Node(val) + + # Update size + self.size += 1 + + # update min_node + if val < self.min_node.val: + self.min_node = new_node + # Put new_node as a bottom_root in heap + self.bottom_root.left = new_node + new_node.parent = self.bottom_root + self.bottom_root = new_node + + # Consecutively merge roots with same left_tree_size + while ( + self.bottom_root.parent + and self.bottom_root.left_tree_size + == self.bottom_root.parent.left_tree_size + ): + + # Next node + next_node = self.bottom_root.parent.parent + + # Merge + self.bottom_root = self.bottom_root.mergeTrees(self.bottom_root.parent) + + # Update Links + self.bottom_root.parent = next_node + self.bottom_root.left = None + if next_node: + next_node.left = self.bottom_root + + def peek(self): + """ + return min element without deleting it + """ + return self.min_node.val + + def isEmpty(self): + return self.size == 0 + + def deleteMin(self): + """ + delete min element and return it + """ + # assert not self.isEmpty(), "Empty Heap" + + # Save minimal value + min_value = self.min_node.val + + # Last element in heap corner case + if self.size == 1: + # Update size + self.size = 0 + + # Update bottom root + self.bottom_root = None + + # Update min_node + self.min_node = None + + return min_value + # No right subtree corner case + # The structure of the tree implies that this should be the bottom root + # and there is at least one other root + if self.min_node.right is None: + # Update size + self.size -= 1 + + # Update bottom root + self.bottom_root = self.bottom_root.parent + self.bottom_root.left = None + + # Update min_node + self.min_node = self.bottom_root + i = self.bottom_root.parent + while i: + if i.val < self.min_node.val: + self.min_node = i + i = i.parent + return min_value + # General case + # Find the BinomialHeap of the right subtree of min_node + bottom_of_new = self.min_node.right + bottom_of_new.parent = None + min_of_new = bottom_of_new + size_of_new = 1 + + # Size, min_node and bottom_root + while bottom_of_new.left: + size_of_new = size_of_new * 2 + 1 + bottom_of_new = bottom_of_new.left + if bottom_of_new.val < min_of_new.val: + min_of_new = bottom_of_new + # Corner case of single root on top left path + if (not self.min_node.left) and (not self.min_node.parent): + self.size = size_of_new + self.bottom_root = bottom_of_new + self.min_node = min_of_new + # print("Single root, multiple nodes case") + return min_value + # Remaining cases + # Construct heap of right subtree + newHeap = BinomialHeap( + bottom_root=bottom_of_new, min_node=min_of_new, heap_size=size_of_new + ) + + # Update size + self.size = self.size - 1 - size_of_new + + # Neighbour nodes + previous_node = self.min_node.left + next_node = self.min_node.parent + + # Initialize new bottom_root and min_node + self.min_node = previous_node or next_node + self.bottom_root = next_node + + # Update links of previous_node and search below for new min_node and + # bottom_root + if previous_node: + previous_node.parent = next_node + + # Update bottom_root and search for min_node below + self.bottom_root = previous_node + self.min_node = previous_node + while self.bottom_root.left: + self.bottom_root = self.bottom_root.left + if self.bottom_root.val < self.min_node.val: + self.min_node = self.bottom_root + if next_node: + next_node.left = previous_node + + # Search for new min_node above min_node + i = next_node + while i: + if i.val < self.min_node.val: + self.min_node = i + i = i.parent + # Merge heaps + self.mergeHeaps(newHeap) + + return min_value + + def preOrder(self): + """ + Returns the Pre-order representation of the heap including + values of nodes plus their level distance from the root; + Empty nodes appear as # + """ + # Find top root + top_root = self.bottom_root + while top_root.parent: + top_root = top_root.parent + # preorder + heap_preOrder = [] + self.__traversal(top_root, heap_preOrder) + return heap_preOrder + + def __traversal(self, curr_node, preorder, level=0): + """ + Pre-order traversal of nodes + """ + if curr_node: + preorder.append((curr_node.val, level)) + self.__traversal(curr_node.left, preorder, level + 1) + self.__traversal(curr_node.right, preorder, level + 1) + else: + preorder.append(("#", level)) + + def __str__(self): + """ + Overwriting str for a pre-order print of nodes in heap; + Performance is poor, so use only for small examples + """ + if self.isEmpty(): + return "" + preorder_heap = self.preOrder() + + return "\n".join(("-" * level + str(value)) for value, level in preorder_heap) + + +# Unit Tests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/heap/heap.py b/data_structures/heap/heap.py index 39778f725c3a..b901c54a4284 100644 --- a/data_structures/heap/heap.py +++ b/data_structures/heap/heap.py @@ -1,91 +1,135 @@ -#!/usr/bin/python - -from __future__ import print_function, division - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -#This heap class start from here. -class Heap: - def __init__(self): #Default constructor of heap class. - self.h = [] - self.currsize = 0 - - def leftChild(self,i): - if 2*i+1 < self.currsize: - return 2*i+1 - return None - - def rightChild(self,i): - if 2*i+2 < self.currsize: - return 2*i+2 - return None - - def maxHeapify(self,node): - if node < self.currsize: - m = node - lc = self.leftChild(node) - rc = self.rightChild(node) - if lc is not None and self.h[lc] > self.h[m]: - m = lc - if rc is not None and self.h[rc] > self.h[m]: - m = rc - if m!=node: - temp = self.h[node] - self.h[node] = self.h[m] - self.h[m] = temp - self.maxHeapify(m) - - def buildHeap(self,a): #This function is used to build the heap from the data container 'a'. - self.currsize = len(a) - self.h = list(a) - for i in range(self.currsize//2,-1,-1): - self.maxHeapify(i) - - def getMax(self): #This function is used to get maximum value from the heap. - if self.currsize >= 1: - me = self.h[0] - temp = self.h[0] - self.h[0] = self.h[self.currsize-1] - self.h[self.currsize-1] = temp - self.currsize -= 1 - self.maxHeapify(0) - return me - return None - - def heapSort(self): #This function is used to sort the heap. - size = self.currsize - while self.currsize-1 >= 0: - temp = self.h[0] - self.h[0] = self.h[self.currsize-1] - self.h[self.currsize-1] = temp - self.currsize -= 1 - self.maxHeapify(0) - self.currsize = size - - def insert(self,data): #This function is used to insert data in the heap. - self.h.append(data) - curr = self.currsize - self.currsize+=1 - while self.h[curr] > self.h[curr/2]: - temp = self.h[curr/2] - self.h[curr/2] = self.h[curr] - self.h[curr] = temp - curr = curr/2 - - def display(self): #This function is used to print the heap. - print(self.h) +#!/usr/bin/python3 + + +class Heap(object): + """ + >>> unsorted = [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5] + >>> h = Heap() + >>> h.build_heap(unsorted) + >>> h.display() + [209, 201, 25, 103, 107, 15, 1, 9, 7, 11, 5] + >>> + >>> h.get_max() + 209 + >>> h.display() + [201, 107, 25, 103, 11, 15, 1, 9, 7, 5] + >>> + >>> h.insert(100) + >>> h.display() + [201, 107, 25, 103, 100, 15, 1, 9, 7, 5, 11] + >>> + >>> h.heap_sort() + >>> h.display() + [1, 5, 7, 9, 11, 15, 25, 100, 103, 107, 201] + >>> + """ + + def __init__(self): + self.h = [] + self.curr_size = 0 + + def get_left_child_index(self, i): + left_child_index = 2 * i + 1 + if left_child_index < self.curr_size: + return left_child_index + return None + + def get_right_child(self, i): + right_child_index = 2 * i + 2 + if right_child_index < self.curr_size: + return right_child_index + return None + + def max_heapify(self, index): + if index < self.curr_size: + largest = index + lc = self.get_left_child_index(index) + rc = self.get_right_child(index) + if lc is not None and self.h[lc] > self.h[largest]: + largest = lc + if rc is not None and self.h[rc] > self.h[largest]: + largest = rc + if largest != index: + self.h[largest], self.h[index] = self.h[index], self.h[largest] + self.max_heapify(largest) + + def build_heap(self, collection): + self.curr_size = len(collection) + self.h = list(collection) + if self.curr_size <= 1: + return + for i in range(self.curr_size // 2 - 1, -1, -1): + self.max_heapify(i) + + def get_max(self): + if self.curr_size >= 2: + me = self.h[0] + self.h[0] = self.h.pop(-1) + self.curr_size -= 1 + self.max_heapify(0) + return me + elif self.curr_size == 1: + self.curr_size -= 1 + return self.h.pop(-1) + return None + + def heap_sort(self): + size = self.curr_size + for j in range(size - 1, 0, -1): + self.h[0], self.h[j] = self.h[j], self.h[0] + self.curr_size -= 1 + self.max_heapify(0) + self.curr_size = size + + def insert(self, data): + self.h.append(data) + curr = (self.curr_size - 1) // 2 + self.curr_size += 1 + while curr >= 0: + self.max_heapify(curr) + curr = (curr - 1) // 2 + + def display(self): + print(self.h) + def main(): - l = list(map(int, raw_input().split())) - h = Heap() - h.buildHeap(l) - h.heapSort() - h.display() + for unsorted in [ + [], + [0], + [2], + [3, 5], + [5, 3], + [5, 5], + [0, 0, 0, 0], + [1, 1, 1, 1], + [2, 2, 3, 5], + [0, 2, 2, 3, 5], + [2, 5, 3, 0, 2, 3, 0, 3], + [6, 1, 2, 7, 9, 3, 4, 5, 10, 8], + [103, 9, 1, 7, 11, 15, 25, 201, 209, 107, 5], + [-45, -2, -5], + ]: + print("source unsorted list: %s" % unsorted) + + h = Heap() + h.build_heap(unsorted) + print("after build heap: ", end=" ") + h.display() + + print("max value: %s" % h.get_max()) + print("delete max value: ", end=" ") + h.display() + + h.insert(100) + print("after insert new value 100: ", end=" ") + h.display() -if __name__=='__main__': - main() + h.heap_sort() + print("heap sort: ", end=" ") + h.display() + print() +if __name__ == "__main__": + main() diff --git a/data_structures/heap/min_heap.py b/data_structures/heap/min_heap.py new file mode 100644 index 000000000000..e68853837faa --- /dev/null +++ b/data_structures/heap/min_heap.py @@ -0,0 +1,170 @@ +# Min head data structure +# with decrease key functionality - in O(log(n)) time + + +class Node: + def __init__(self, name, val): + self.name = name + self.val = val + + def __str__(self): + return f"{self.__class__.__name__}({self.name}, {self.val})" + + def __lt__(self, other): + return self.val < other.val + + +class MinHeap: + """ + >>> r = Node("R", -1) + >>> b = Node("B", 6) + >>> a = Node("A", 3) + >>> x = Node("X", 1) + >>> e = Node("E", 4) + >>> print(b) + Node(B, 6) + >>> myMinHeap = MinHeap([r, b, a, x, e]) + >>> myMinHeap.decrease_key(b, -17) + >>> print(b) + Node(B, -17) + >>> print(myMinHeap["B"]) + -17 + """ + + def __init__(self, array): + self.idx_of_element = {} + self.heap_dict = {} + self.heap = self.build_heap(array) + + def __getitem__(self, key): + return self.get_value(key) + + def get_parent_idx(self, idx): + return (idx - 1) // 2 + + def get_left_child_idx(self, idx): + return idx * 2 + 1 + + def get_right_child_idx(self, idx): + return idx * 2 + 2 + + def get_value(self, key): + return self.heap_dict[key] + + def build_heap(self, array): + lastIdx = len(array) - 1 + startFrom = self.get_parent_idx(lastIdx) + + for idx, i in enumerate(array): + self.idx_of_element[i] = idx + self.heap_dict[i.name] = i.val + + for i in range(startFrom, -1, -1): + self.sift_down(i, array) + return array + + # this is min-heapify method + def sift_down(self, idx, array): + while True: + l = self.get_left_child_idx(idx) + r = self.get_right_child_idx(idx) + + smallest = idx + if l < len(array) and array[l] < array[idx]: + smallest = l + if r < len(array) and array[r] < array[smallest]: + smallest = r + + if smallest != idx: + array[idx], array[smallest] = array[smallest], array[idx] + ( + self.idx_of_element[array[idx]], + self.idx_of_element[array[smallest]], + ) = ( + self.idx_of_element[array[smallest]], + self.idx_of_element[array[idx]], + ) + idx = smallest + else: + break + + def sift_up(self, idx): + p = self.get_parent_idx(idx) + while p >= 0 and self.heap[p] > self.heap[idx]: + self.heap[p], self.heap[idx] = self.heap[idx], self.heap[p] + self.idx_of_element[self.heap[p]], self.idx_of_element[self.heap[idx]] = ( + self.idx_of_element[self.heap[idx]], + self.idx_of_element[self.heap[p]], + ) + idx = p + p = self.get_parent_idx(idx) + + def peek(self): + return self.heap[0] + + def remove(self): + self.heap[0], self.heap[-1] = self.heap[-1], self.heap[0] + self.idx_of_element[self.heap[0]], self.idx_of_element[self.heap[-1]] = ( + self.idx_of_element[self.heap[-1]], + self.idx_of_element[self.heap[0]], + ) + + x = self.heap.pop() + del self.idx_of_element[x] + self.sift_down(0, self.heap) + return x + + def insert(self, node): + self.heap.append(node) + self.idx_of_element[node] = len(self.heap) - 1 + self.heap_dict[node.name] = node.val + self.sift_up(len(self.heap) - 1) + + def is_empty(self): + return True if len(self.heap) == 0 else False + + def decrease_key(self, node, newValue): + assert ( + self.heap[self.idx_of_element[node]].val > newValue + ), "newValue must be less that current value" + node.val = newValue + self.heap_dict[node.name] = newValue + self.sift_up(self.idx_of_element[node]) + + +## USAGE + +r = Node("R", -1) +b = Node("B", 6) +a = Node("A", 3) +x = Node("X", 1) +e = Node("E", 4) + +# Use one of these two ways to generate Min-Heap + +# Generating Min-Heap from array +myMinHeap = MinHeap([r, b, a, x, e]) + +# Generating Min-Heap by Insert method +# myMinHeap.insert(a) +# myMinHeap.insert(b) +# myMinHeap.insert(x) +# myMinHeap.insert(r) +# myMinHeap.insert(e) + +# Before +print("Min Heap - before decrease key") +for i in myMinHeap.heap: + print(i) + +print("Min Heap - After decrease key of node [B -> -17]") +myMinHeap.decrease_key(b, -17) + +# After +for i in myMinHeap.heap: + print(i) + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/__init__.py b/data_structures/linked_list/__init__.py index 6d50f23c1f1a..a050adba42b2 100644 --- a/data_structures/linked_list/__init__.py +++ b/data_structures/linked_list/__init__.py @@ -3,6 +3,7 @@ def __init__(self, item, next): self.item = item self.next = next + class LinkedList: def __init__(self): self.head = None diff --git a/data_structures/linked_list/circular_linked_list.py b/data_structures/linked_list/circular_linked_list.py new file mode 100644 index 000000000000..cf523f0a4380 --- /dev/null +++ b/data_structures/linked_list/circular_linked_list.py @@ -0,0 +1,186 @@ +from typing import Any + + +class Node: + """ + Class to represent a single node. + + Each node has following attributes + * data + * next_ptr + """ + + def __init__(self, data: Any): + self.data = data + self.next_ptr = None + + +class CircularLinkedList: + """ + Class to represent the CircularLinkedList. + + CircularLinkedList has following attributes. + * head + * length + """ + + def __init__(self): + self.head = None + self.length = 0 + + def __len__(self) -> int: + """ + Dunder method to return length of the CircularLinkedList + >>> cll = CircularLinkedList() + >>> len(cll) + 0 + >>> cll.append(1) + >>> len(cll) + 1 + """ + return self.length + + def __str__(self) -> str: + """ + Dunder method to represent the string representation of the CircularLinkedList + >>> cll = CircularLinkedList() + >>> print(cll) + Empty linked list + >>> cll.append(1) + >>> cll.append(2) + >>> print(cll) + => + """ + current_node = self.head + if not current_node: + return "Empty linked list" + + results = [current_node.data] + current_node = current_node.next_ptr + + while current_node != self.head: + results.append(current_node.data) + current_node = current_node.next_ptr + + return " => ".join(f"" for result in results) + + def append(self, data: Any) -> None: + """ + Adds a node with given data to the end of the CircularLinkedList + >>> cll = CircularLinkedList() + >>> cll.append(1) + >>> print(f"{len(cll)}: {cll}") + 1: + >>> cll.append(2) + >>> print(f"{len(cll)}: {cll}") + 2: => + """ + current_node = self.head + + new_node = Node(data) + new_node.next_ptr = new_node + + if current_node: + while current_node.next_ptr != self.head: + current_node = current_node.next_ptr + + current_node.next_ptr = new_node + new_node.next_ptr = self.head + else: + self.head = new_node + + self.length += 1 + + def prepend(self, data: Any) -> None: + """ + Adds a ndoe with given data to the front of the CircularLinkedList + >>> cll = CircularLinkedList() + >>> cll.prepend(1) + >>> cll.prepend(2) + >>> print(f"{len(cll)}: {cll}") + 2: => + """ + current_node = self.head + + new_node = Node(data) + new_node.next_ptr = new_node + + if current_node: + while current_node.next_ptr != self.head: + current_node = current_node.next_ptr + + current_node.next_ptr = new_node + new_node.next_ptr = self.head + + self.head = new_node + self.length += 1 + + def delete_front(self) -> None: + """ + Removes the 1st node from the CircularLinkedList + >>> cll = CircularLinkedList() + >>> cll.delete_front() + Traceback (most recent call last): + ... + IndexError: Deleting from an empty list + >>> cll.append(1) + >>> cll.append(2) + >>> print(f"{len(cll)}: {cll}") + 2: => + >>> cll.delete_front() + >>> print(f"{len(cll)}: {cll}") + 1: + """ + if not self.head: + raise IndexError("Deleting from an empty list") + + current_node = self.head + + if current_node.next_ptr == current_node: + self.head, self.length = None, 0 + else: + while current_node.next_ptr != self.head: + current_node = current_node.next_ptr + + current_node.next_ptr = self.head.next_ptr + self.head = self.head.next_ptr + + self.length -= 1 + + def delete_rear(self) -> None: + """ + Removes the last node from the CircularLinkedList + >>> cll = CircularLinkedList() + >>> cll.delete_rear() + Traceback (most recent call last): + ... + IndexError: Deleting from an empty list + >>> cll.append(1) + >>> cll.append(2) + >>> print(f"{len(cll)}: {cll}") + 2: => + >>> cll.delete_rear() + >>> print(f"{len(cll)}: {cll}") + 1: + """ + if not self.head: + raise IndexError("Deleting from an empty list") + + temp_node, current_node = self.head, self.head + + if current_node.next_ptr == current_node: + self.head, self.length = None, 0 + else: + while current_node.next_ptr != self.head: + temp_node = current_node + current_node = current_node.next_ptr + + temp_node.next_ptr = current_node.next_ptr + + self.length -= 1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/linked_list/deque_doubly.py b/data_structures/linked_list/deque_doubly.py new file mode 100644 index 000000000000..b2e73a8f789b --- /dev/null +++ b/data_structures/linked_list/deque_doubly.py @@ -0,0 +1,143 @@ +""" +Implementing Deque using DoublyLinkedList ... +Operations: + 1. insertion in the front -> O(1) + 2. insertion in the end -> O(1) + 3. remove from the front -> O(1) + 4. remove from the end -> O(1) +""" + + +class _DoublyLinkedBase: + """ A Private class (to be inherited) """ + + class _Node: + __slots__ = "_prev", "_data", "_next" + + def __init__(self, link_p, element, link_n): + self._prev = link_p + self._data = element + self._next = link_n + + def has_next_and_prev(self): + return " Prev -> {0}, Next -> {1}".format( + self._prev is not None, self._next is not None + ) + + def __init__(self): + self._header = self._Node(None, None, None) + self._trailer = self._Node(None, None, None) + self._header._next = self._trailer + self._trailer._prev = self._header + self._size = 0 + + def __len__(self): + return self._size + + def is_empty(self): + return self.__len__() == 0 + + def _insert(self, predecessor, e, successor): + # Create new_node by setting it's prev.link -> header + # setting it's next.link -> trailer + new_node = self._Node(predecessor, e, successor) + predecessor._next = new_node + successor._prev = new_node + self._size += 1 + return self + + def _delete(self, node): + predecessor = node._prev + successor = node._next + + predecessor._next = successor + successor._prev = predecessor + self._size -= 1 + temp = node._data + node._prev = node._next = node._data = None + del node + return temp + + +class LinkedDeque(_DoublyLinkedBase): + def first(self): + """ return first element + >>> d = LinkedDeque() + >>> d.add_first('A').first() + 'A' + >>> d.add_first('B').first() + 'B' + """ + if self.is_empty(): + raise Exception("List is empty") + return self._header._next._data + + def last(self): + """ return last element + >>> d = LinkedDeque() + >>> d.add_last('A').last() + 'A' + >>> d.add_last('B').last() + 'B' + """ + if self.is_empty(): + raise Exception("List is empty") + return self._trailer._prev._data + + ### DEque Insert Operations (At the front, At the end) ### + + def add_first(self, element): + """ insertion in the front + >>> LinkedDeque().add_first('AV').first() + 'AV' + """ + return self._insert(self._header, element, self._header._next) + + def add_last(self, element): + """ insertion in the end + >>> LinkedDeque().add_last('B').last() + 'B' + """ + return self._insert(self._trailer._prev, element, self._trailer) + + ### DEqueu Remove Operations (At the front, At the end) ### + + def remove_first(self): + """ removal from the front + >>> d = LinkedDeque() + >>> d.is_empty() + True + >>> d.remove_first() + Traceback (most recent call last): + ... + IndexError: remove_first from empty list + >>> d.add_first('A') # doctest: +ELLIPSIS + >> d.remove_first() + 'A' + >>> d.is_empty() + True + """ + if self.is_empty(): + raise IndexError("remove_first from empty list") + return self._delete(self._header._next) + + def remove_last(self): + """ removal in the end + >>> d = LinkedDeque() + >>> d.is_empty() + True + >>> d.remove_last() + Traceback (most recent call last): + ... + IndexError: remove_first from empty list + >>> d.add_first('A') # doctest: +ELLIPSIS + >> d.remove_last() + 'A' + >>> d.is_empty() + True + """ + if self.is_empty(): + raise IndexError("remove_first from empty list") + return self._delete(self._trailer._prev) diff --git a/data_structures/linked_list/doubly_linked_list.py b/data_structures/linked_list/doubly_linked_list.py index 75b1f889dfc2..e449ed6ec8ac 100644 --- a/data_structures/linked_list/doubly_linked_list.py +++ b/data_structures/linked_list/doubly_linked_list.py @@ -1,77 +1,84 @@ -''' -- A linked list is similar to an array, it holds values. However, links in a linked list do not have indexes. +""" +- A linked list is similar to an array, it holds values. However, links in a linked + list do not have indexes. - This is an example of a double ended, doubly linked list. - Each link references the next link and the previous one. -- A Doubly Linked List (DLL) contains an extra pointer, typically called previous pointer, together with next pointer and data which are there in singly linked list. - - Advantages over SLL - IT can be traversed in both forward and backward direction.,Delete operation is more efficent''' -from __future__ import print_function +- A Doubly Linked List (DLL) contains an extra pointer, typically called previous + pointer, together with next pointer and data which are there in singly linked list. + - Advantages over SLL - IT can be traversed in both forward and backward direction., + Delete operation is more efficient""" -class LinkedList: #making main class named linked list +class LinkedList: # making main class named linked list def __init__(self): self.head = None self.tail = None - + def insertHead(self, x): - newLink = Link(x) #Create a new link with a value attached to it - if(self.isEmpty() == True): #Set the first element added to be the tail + newLink = Link(x) # Create a new link with a value attached to it + if self.isEmpty(): # Set the first element added to be the tail self.tail = newLink else: - self.head.previous = newLink # newLink <-- currenthead(head) - newLink.next = self.head # newLink <--> currenthead(head) - self.head = newLink # newLink(head) <--> oldhead - + self.head.previous = newLink # newLink <-- currenthead(head) + newLink.next = self.head # newLink <--> currenthead(head) + self.head = newLink # newLink(head) <--> oldhead + def deleteHead(self): temp = self.head - self.head = self.head.next # oldHead <--> 2ndElement(head) - self.head.previous = None # oldHead --> 2ndElement(head) nothing pointing at it so the old head will be removed - if(self.head is None): - self.tail = None #if empty linked list + self.head = self.head.next # oldHead <--> 2ndElement(head) + # oldHead --> 2ndElement(head) nothing pointing at it so the old head will be + # removed + self.head.previous = None + if self.head is None: + self.tail = None # if empty linked list return temp - + def insertTail(self, x): newLink = Link(x) - newLink.next = None # currentTail(tail) newLink --> - self.tail.next = newLink # currentTail(tail) --> newLink --> - newLink.previous = self.tail #currentTail(tail) <--> newLink --> - self.tail = newLink # oldTail <--> newLink(tail) --> - + newLink.next = None # currentTail(tail) newLink --> + self.tail.next = newLink # currentTail(tail) --> newLink --> + newLink.previous = self.tail # currentTail(tail) <--> newLink --> + self.tail = newLink # oldTail <--> newLink(tail) --> + def deleteTail(self): temp = self.tail - self.tail = self.tail.previous # 2ndLast(tail) <--> oldTail --> None - self.tail.next = None # 2ndlast(tail) --> None + self.tail = self.tail.previous # 2ndLast(tail) <--> oldTail --> None + self.tail.next = None # 2ndlast(tail) --> None return temp - + def delete(self, x): current = self.head - - while(current.value != x): # Find the position to delete + + while current.value != x: # Find the position to delete current = current.next - - if(current == self.head): + + if current == self.head: self.deleteHead() - - elif(current == self.tail): + + elif current == self.tail: self.deleteTail() - - else: #Before: 1 <--> 2(current) <--> 3 - current.previous.next = current.next # 1 --> 3 - current.next.previous = current.previous # 1 <--> 3 - - def isEmpty(self): #Will return True if the list is empty - return(self.head is None) - - def display(self): #Prints contents of the list + + else: # Before: 1 <--> 2(current) <--> 3 + current.previous.next = current.next # 1 --> 3 + current.next.previous = current.previous # 1 <--> 3 + + def isEmpty(self): # Will return True if the list is empty + return self.head is None + + def display(self): # Prints contents of the list current = self.head - while(current != None): + while current is not None: current.displayLink() - current = current.next + current = current.next print() + class Link: - next = None #This points to the link in front of the new link - previous = None #This points to the link behind the new link + next = None # This points to the link in front of the new link + previous = None # This points to the link behind the new link + def __init__(self, x): self.value = x + def displayLink(self): - print("{}".format(self.value), end=" ") + print(f"{self.value}", end=" ") diff --git a/data_structures/linked_list/from_sequence.py b/data_structures/linked_list/from_sequence.py new file mode 100644 index 000000000000..94b44f15037f --- /dev/null +++ b/data_structures/linked_list/from_sequence.py @@ -0,0 +1,44 @@ +# Recursive Prorgam to create a Linked List from a sequence and +# print a string representation of it. + + +class Node: + def __init__(self, data=None): + self.data = data + self.next = None + + def __repr__(self): + """Returns a visual representation of the node and all its following nodes.""" + string_rep = "" + temp = self + while temp: + string_rep += f"<{temp.data}> ---> " + temp = temp.next + string_rep += "" + return string_rep + + +def make_linked_list(elements_list): + """Creates a Linked List from the elements of the given sequence + (list/tuple) and returns the head of the Linked List.""" + + # if elements_list is empty + if not elements_list: + raise Exception("The Elements List is empty") + + # Set first element as Head + head = Node(elements_list[0]) + current = head + # Loop through elements from position 1 + for data in elements_list[1:]: + current.next = Node(data) + current = current.next + return head + + +list_data = [1, 3, 5, 32, 44, 12, 43] +print(f"List: {list_data}") +print("Creating Linked List from List.") +linked_list = make_linked_list(list_data) +print("Linked List:") +print(linked_list) diff --git a/data_structures/linked_list/middle_element_of_linked_list.py b/data_structures/linked_list/middle_element_of_linked_list.py new file mode 100644 index 000000000000..b845d2f19c20 --- /dev/null +++ b/data_structures/linked_list/middle_element_of_linked_list.py @@ -0,0 +1,64 @@ +class Node: + def __init__(self, data: int) -> int: + self.data = data + self.next = None + + +class LinkedList: + def __init__(self): + self.head = None + + def push(self, new_data: int) -> int: + new_node = Node(new_data) + new_node.next = self.head + self.head = new_node + return self.head.data + + def middle_element(self) -> int: + """ + >>> link = LinkedList() + >>> link.middle_element() + No element found. + >>> link.push(5) + 5 + >>> link.push(6) + 6 + >>> link.push(8) + 8 + >>> link.push(8) + 8 + >>> link.push(10) + 10 + >>> link.push(12) + 12 + >>> link.push(17) + 17 + >>> link.push(7) + 7 + >>> link.push(3) + 3 + >>> link.push(20) + 20 + >>> link.push(-20) + -20 + >>> link.middle_element() + 12 + >>> + """ + slow_pointer = self.head + fast_pointer = self.head + if self.head: + while fast_pointer and fast_pointer.next: + fast_pointer = fast_pointer.next.next + slow_pointer = slow_pointer.next + return slow_pointer.data + else: + print("No element found.") + + +if __name__ == "__main__": + link = LinkedList() + for i in range(int(input().strip())): + data = int(input().strip()) + link.push(data) + print(link.middle_element()) diff --git a/data_structures/linked_list/print_reverse.py b/data_structures/linked_list/print_reverse.py new file mode 100644 index 000000000000..c3a72b6b7a23 --- /dev/null +++ b/data_structures/linked_list/print_reverse.py @@ -0,0 +1,55 @@ +# Program to print the elements of a linked list in reverse + + +class Node: + def __init__(self, data=None): + self.data = data + self.next = None + + def __repr__(self): + """Returns a visual representation of the node and all its following nodes.""" + string_rep = "" + temp = self + while temp: + string_rep += f"<{temp.data}> ---> " + temp = temp.next + string_rep += "" + return string_rep + + +def make_linked_list(elements_list): + """Creates a Linked List from the elements of the given sequence + (list/tuple) and returns the head of the Linked List.""" + + # if elements_list is empty + if not elements_list: + raise Exception("The Elements List is empty") + + # Set first element as Head + head = Node(elements_list[0]) + current = head + # Loop through elements from position 1 + for data in elements_list[1:]: + current.next = Node(data) + current = current.next + return head + + +def print_reverse(head_node): + """Prints the elements of the given Linked List in reverse order""" + + # If reached end of the List + if head_node is None: + return None + else: + # Recurse + print_reverse(head_node.next) + print(head_node.data) + + +list_data = [14, 52, 14, 12, 43] +linked_list = make_linked_list(list_data) +print("Linked List:") +print(linked_list) +print("Elements in Reverse:") +print_reverse(linked_list) diff --git a/data_structures/linked_list/singly_linked_list.py b/data_structures/linked_list/singly_linked_list.py index 5ae97523b9a1..c90cfb6e59a9 100644 --- a/data_structures/linked_list/singly_linked_list.py +++ b/data_structures/linked_list/singly_linked_list.py @@ -1,60 +1,62 @@ -from __future__ import print_function - - class Node: # create a Node def __init__(self, data): self.data = data # given data self.next = None # given next to None + def __repr__(self): # string representation of a Node + return f"Node({self.data})" -class Linked_List: + +class LinkedList: def __init__(self): - self.Head = None # Initialize Head to None - - def insert_tail(self, data): - if(self.Head is None): self.insert_head(data) #If this is first node, call insert_head + self.head = None # initialize head to None + + def insert_tail(self, data) -> None: + if self.head is None: + self.insert_head(data) # if this is first node, call insert_head else: - temp = self.Head - while(temp.next != None): #traverse to last node + temp = self.head + while temp.next: # traverse to last node temp = temp.next - temp.next = Node(data) #create node & link to tail + temp.next = Node(data) # create node & link to tail - def insert_head(self, data): - newNod = Node(data) # create a new node - if self.Head != None: - newNod.next = self.Head # link newNode to head - self.Head = newNod # make NewNode as Head + def insert_head(self, data) -> None: + new_node = Node(data) # create a new node + if self.head: + new_node.next = self.head # link new_node to head + self.head = new_node # make NewNode as head - def printList(self): # print every node data - tamp = self.Head - while tamp is not None: - print(tamp.data) - tamp = tamp.next + def print_list(self) -> None: # print every node data + temp = self.head + while temp: + print(temp.data) + temp = temp.next def delete_head(self): # delete from head - temp = self.Head - if self.Head != None: - self.Head = self.Head.next + temp = self.head + if self.head: + self.head = self.head.next temp.next = None return temp - + def delete_tail(self): # delete from tail - tamp = self.Head - if self.Head != None: - if(self.Head.next is None): # if Head is the only Node in the Linked List - self.Head = None + temp = self.head + if self.head: + if self.head.next is None: # if head is the only Node in the Linked List + self.head = None else: - while tamp.next.next is not None: # find the 2nd last element - tamp = tamp.next - tamp.next, tamp = None, tamp.next #(2nd last element).next = None and tamp = last element - return tamp + while temp.next.next: # find the 2nd last element + temp = temp.next + # (2nd last element).next = None and temp = last element + temp.next, temp = None, temp.next + return temp - def isEmpty(self): - return self.Head is None # Return if Head is none + def is_empty(self) -> bool: + return self.head is None # return True if head is none def reverse(self): prev = None - current = self.Head + current = self.head while current: # Store the current node's next node. @@ -66,36 +68,74 @@ def reverse(self): # Make the current node the next node (to progress iteration) current = next_node # Return prev in order to put the head at the end - self.Head = prev + self.head = prev + + def __repr__(self): # String representation/visualization of a Linked Lists + current = self.head + string_repr = "" + while current: + string_repr += f"{current} --> " + current = current.next + # END represents end of the LinkedList + return string_repr + "END" + + # Indexing Support. Used to get a node at particular position + def __getitem__(self, index): + current = self.head + + # If LinkedList is empty + if current is None: + raise IndexError("The Linked List is empty") + + # Move Forward 'index' times + for _ in range(index): + # If the LinkedList ends before reaching specified node + if current.next is None: + raise IndexError("Index out of range.") + current = current.next + return current + + # Used to change the data of a particular node + def __setitem__(self, index, data): + current = self.head + # If list is empty + if current is None: + raise IndexError("The Linked List is empty") + for i in range(index): + if current.next is None: + raise IndexError("Index out of range.") + current = current.next + current.data = data + def main(): - A = Linked_List() - print("Inserting 1st at Head") - a1=input() - A.insert_head(a1) - print("Inserting 2nd at Head") - a2=input() - A.insert_head(a2) - print("\nPrint List : ") - A.printList() - print("\nInserting 1st at Tail") - a3=input() - A.insert_tail(a3) - print("Inserting 2nd at Tail") - a4=input() - A.insert_tail(a4) - print("\nPrint List : ") - A.printList() - print("\nDelete Head") + A = LinkedList() + A.insert_head(input("Inserting 1st at head ").strip()) + A.insert_head(input("Inserting 2nd at head ").strip()) + print("\nPrint list:") + A.print_list() + A.insert_tail(input("\nInserting 1st at tail ").strip()) + A.insert_tail(input("Inserting 2nd at tail ").strip()) + print("\nPrint list:") + A.print_list() + print("\nDelete head") A.delete_head() - print("Delete Tail") + print("Delete tail") A.delete_tail() - print("\nPrint List : ") - A.printList() - print("\nReverse Linked List") + print("\nPrint list:") + A.print_list() + print("\nReverse linked list") A.reverse() - print("\nPrint List : ") - A.printList() - -if __name__ == '__main__': - main() + print("\nPrint list:") + A.print_list() + print("\nString representation of linked list:") + print(A) + print("\nReading/changing Node data using indexing:") + print(f"Element at Position 1: {A[1]}") + A[1] = input("Enter New Value: ").strip() + print("New list:") + print(A) + + +if __name__ == "__main__": + main() diff --git a/data_structures/linked_list/skip_list.py b/data_structures/linked_list/skip_list.py new file mode 100644 index 000000000000..195d7ffc6b91 --- /dev/null +++ b/data_structures/linked_list/skip_list.py @@ -0,0 +1,443 @@ +""" +Based on "Skip Lists: A Probabilistic Alternative to Balanced Trees" by William Pugh +https://epaperpress.com/sortsearch/download/skiplist.pdf +""" + +from random import random +from typing import Generic, List, Optional, Tuple, TypeVar + +KT = TypeVar("KT") +VT = TypeVar("VT") + + +class Node(Generic[KT, VT]): + def __init__(self, key: KT, value: VT): + self.key = key + self.value = value + self.forward: List[Node[KT, VT]] = [] + + def __repr__(self) -> str: + """ + :return: Visual representation of Node + + >>> node = Node("Key", 2) + >>> repr(node) + 'Node(Key: 2)' + """ + + return f"Node({self.key}: {self.value})" + + @property + def level(self) -> int: + """ + :return: Number of forward references + + >>> node = Node("Key", 2) + >>> node.level + 0 + >>> node.forward.append(Node("Key2", 4)) + >>> node.level + 1 + >>> node.forward.append(Node("Key3", 6)) + >>> node.level + 2 + """ + + return len(self.forward) + + +class SkipList(Generic[KT, VT]): + def __init__(self, p: float = 0.5, max_level: int = 16): + self.head = Node("root", None) + self.level = 0 + self.p = p + self.max_level = max_level + + def __str__(self) -> str: + """ + :return: Visual representation of SkipList + + >>> skip_list = SkipList() + >>> print(skip_list) + SkipList(level=0) + >>> skip_list.insert("Key1", "Value") + >>> print(skip_list) # doctest: +ELLIPSIS + SkipList(level=... + [root]--... + [Key1]--Key1... + None *... + >>> skip_list.insert("Key2", "OtherValue") + >>> print(skip_list) # doctest: +ELLIPSIS + SkipList(level=... + [root]--... + [Key1]--Key1... + [Key2]--Key2... + None *... + """ + + items = list(self) + + if len(items) == 0: + return f"SkipList(level={self.level})" + + label_size = max((len(str(item)) for item in items), default=4) + label_size = max(label_size, 4) + 4 + + node = self.head + lines = [] + + forwards = node.forward.copy() + lines.append(f"[{node.key}]".ljust(label_size, "-") + "* " * len(forwards)) + lines.append(" " * label_size + "| " * len(forwards)) + + while len(node.forward) != 0: + node = node.forward[0] + + lines.append( + f"[{node.key}]".ljust(label_size, "-") + + " ".join(str(n.key) if n.key == node.key else "|" for n in forwards) + ) + lines.append(" " * label_size + "| " * len(forwards)) + forwards[: node.level] = node.forward + + lines.append("None".ljust(label_size) + "* " * len(forwards)) + return f"SkipList(level={self.level})\n" + "\n".join(lines) + + def __iter__(self): + node = self.head + + while len(node.forward) != 0: + yield node.forward[0].key + node = node.forward[0] + + def random_level(self) -> int: + """ + :return: Random level from [1, self.max_level] interval. + Higher values are less likely. + """ + + level = 1 + while random() < self.p and level < self.max_level: + level += 1 + + return level + + def _locate_node(self, key) -> Tuple[Optional[Node[KT, VT]], List[Node[KT, VT]]]: + """ + :param key: Searched key, + :return: Tuple with searched node (or None if given key is not present) + and list of nodes that refer (if key is present) of should refer to given node. + """ + + # Nodes with refer or should refer to output node + update_vector = [] + + node = self.head + + for i in reversed(range(self.level)): + # i < node.level - When node level is lesser than `i` decrement `i`. + # node.forward[i].key < key - Jumping to node with key value higher + # or equal to searched key would result + # in skipping searched key. + while i < node.level and node.forward[i].key < key: + node = node.forward[i] + # Each leftmost node (relative to searched node) will potentially have to be updated. + update_vector.append(node) + + update_vector.reverse() # Note that we were inserting values in reverse order. + + # len(node.forward) != 0 - If current node doesn't contain any further + # references then searched key is not present. + # node.forward[0].key == key - Next node key should be equal to search key + # if key is present. + if len(node.forward) != 0 and node.forward[0].key == key: + return node.forward[0], update_vector + else: + return None, update_vector + + def delete(self, key: KT): + """ + :param key: Key to remove from list. + + >>> skip_list = SkipList() + >>> skip_list.insert(2, "Two") + >>> skip_list.insert(1, "One") + >>> skip_list.insert(3, "Three") + >>> list(skip_list) + [1, 2, 3] + >>> skip_list.delete(2) + >>> list(skip_list) + [1, 3] + """ + + node, update_vector = self._locate_node(key) + + if node is not None: + for i, update_node in enumerate(update_vector): + # Remove or replace all references to removed node. + if update_node.level > i and update_node.forward[i].key == key: + if node.level > i: + update_node.forward[i] = node.forward[i] + else: + update_node.forward = update_node.forward[:i] + + def insert(self, key: KT, value: VT): + """ + :param key: Key to insert. + :param value: Value associated with given key. + + >>> skip_list = SkipList() + >>> skip_list.insert(2, "Two") + >>> skip_list.find(2) + 'Two' + >>> list(skip_list) + [2] + """ + + node, update_vector = self._locate_node(key) + if node is not None: + node.value = value + else: + level = self.random_level() + + if level > self.level: + # After level increase we have to add additional nodes to head. + for i in range(self.level - 1, level): + update_vector.append(self.head) + self.level = level + + new_node = Node(key, value) + + for i, update_node in enumerate(update_vector[:level]): + # Change references to pass through new node. + if update_node.level > i: + new_node.forward.append(update_node.forward[i]) + + if update_node.level < i + 1: + update_node.forward.append(new_node) + else: + update_node.forward[i] = new_node + + def find(self, key: VT) -> Optional[VT]: + """ + :param key: Search key. + :return: Value associated with given key or None if given key is not present. + + >>> skip_list = SkipList() + >>> skip_list.find(2) + >>> skip_list.insert(2, "Two") + >>> skip_list.find(2) + 'Two' + >>> skip_list.insert(2, "Three") + >>> skip_list.find(2) + 'Three' + """ + + node, _ = self._locate_node(key) + + if node is not None: + return node.value + + return None + + +def test_insert(): + skip_list = SkipList() + skip_list.insert("Key1", 3) + skip_list.insert("Key2", 12) + skip_list.insert("Key3", 41) + skip_list.insert("Key4", -19) + + node = skip_list.head + all_values = {} + while node.level != 0: + node = node.forward[0] + all_values[node.key] = node.value + + assert len(all_values) == 4 + assert all_values["Key1"] == 3 + assert all_values["Key2"] == 12 + assert all_values["Key3"] == 41 + assert all_values["Key4"] == -19 + + +def test_insert_overrides_existing_value(): + skip_list = SkipList() + skip_list.insert("Key1", 10) + skip_list.insert("Key1", 12) + + skip_list.insert("Key5", 7) + skip_list.insert("Key7", 10) + skip_list.insert("Key10", 5) + + skip_list.insert("Key7", 7) + skip_list.insert("Key5", 5) + skip_list.insert("Key10", 10) + + node = skip_list.head + all_values = {} + while node.level != 0: + node = node.forward[0] + all_values[node.key] = node.value + + if len(all_values) != 4: + print() + assert len(all_values) == 4 + assert all_values["Key1"] == 12 + assert all_values["Key7"] == 7 + assert all_values["Key5"] == 5 + assert all_values["Key10"] == 10 + + +def test_searching_empty_list_returns_none(): + skip_list = SkipList() + assert skip_list.find("Some key") is None + + +def test_search(): + skip_list = SkipList() + + skip_list.insert("Key2", 20) + assert skip_list.find("Key2") == 20 + + skip_list.insert("Some Key", 10) + skip_list.insert("Key2", 8) + skip_list.insert("V", 13) + + assert skip_list.find("Y") is None + assert skip_list.find("Key2") == 8 + assert skip_list.find("Some Key") == 10 + assert skip_list.find("V") == 13 + + +def test_deleting_item_from_empty_list_do_nothing(): + skip_list = SkipList() + skip_list.delete("Some key") + + assert len(skip_list.head.forward) == 0 + + +def test_deleted_items_are_not_founded_by_find_method(): + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 14) + skip_list.insert("Key2", 15) + + skip_list.delete("V") + skip_list.delete("Key2") + + assert skip_list.find("V") is None + assert skip_list.find("Key2") is None + + +def test_delete_removes_only_given_key(): + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 14) + skip_list.insert("Key2", 15) + + skip_list.delete("V") + assert skip_list.find("V") is None + assert skip_list.find("X") == 14 + assert skip_list.find("Key1") == 12 + assert skip_list.find("Key2") == 15 + + skip_list.delete("X") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") == 12 + assert skip_list.find("Key2") == 15 + + skip_list.delete("Key1") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") is None + assert skip_list.find("Key2") == 15 + + skip_list.delete("Key2") + assert skip_list.find("V") is None + assert skip_list.find("X") is None + assert skip_list.find("Key1") is None + assert skip_list.find("Key2") is None + + +def test_delete_doesnt_leave_dead_nodes(): + skip_list = SkipList() + + skip_list.insert("Key1", 12) + skip_list.insert("V", 13) + skip_list.insert("X", 142) + skip_list.insert("Key2", 15) + + skip_list.delete("X") + + def traverse_keys(node): + yield node.key + for forward_node in node.forward: + yield from traverse_keys(forward_node) + + assert len(set(traverse_keys(skip_list.head))) == 4 + + +def test_iter_always_yields_sorted_values(): + def is_sorted(lst): + for item, next_item in zip(lst, lst[1:]): + if next_item < item: + return False + return True + + skip_list = SkipList() + for i in range(10): + skip_list.insert(i, i) + assert is_sorted(list(skip_list)) + skip_list.delete(5) + skip_list.delete(8) + skip_list.delete(2) + assert is_sorted(list(skip_list)) + skip_list.insert(-12, -12) + skip_list.insert(77, 77) + assert is_sorted(list(skip_list)) + + +def pytests(): + for i in range(100): + # Repeat test 100 times due to the probabilistic nature of skip list + # random values == random bugs + test_insert() + test_insert_overrides_existing_value() + + test_searching_empty_list_returns_none() + test_search() + + test_deleting_item_from_empty_list_do_nothing() + test_deleted_items_are_not_founded_by_find_method() + test_delete_removes_only_given_key() + test_delete_doesnt_leave_dead_nodes() + + test_iter_always_yields_sorted_values() + + +def main(): + """ + >>> pytests() + """ + + skip_list = SkipList() + skip_list.insert(2, "2") + skip_list.insert(4, "4") + skip_list.insert(6, "4") + skip_list.insert(4, "5") + skip_list.insert(8, "4") + skip_list.insert(9, "4") + + skip_list.delete(4) + + print(skip_list) + + +if __name__ == "__main__": + main() diff --git a/data_structures/linked_list/swap_nodes.py b/data_structures/linked_list/swap_nodes.py index ce2543bc46d8..3f825756b3d2 100644 --- a/data_structures/linked_list/swap_nodes.py +++ b/data_structures/linked_list/swap_nodes.py @@ -1,75 +1,55 @@ +from typing import Any + + class Node: - def __init__(self, data): - self.data = data; + def __init__(self, data: Any): + self.data = data self.next = None -class Linkedlist: +class LinkedList: def __init__(self): self.head = None def print_list(self): temp = self.head while temp is not None: - print(temp.data) + print(temp.data, end=" ") temp = temp.next + print() -# adding nodes - def push(self, new_data): + # adding nodes + def push(self, new_data: Any): new_node = Node(new_data) new_node.next = self.head self.head = new_node -# swapping nodes - def swapNodes(self, d1, d2): - prevD1 = None - prevD2 = None - if d1 == d2: + # swapping nodes + def swap_nodes(self, node_data_1, node_data_2): + if node_data_1 == node_data_2: return else: - # find d1 - D1 = self.head - while D1 is not None and D1.data != d1: - prevD1 = D1 - D1 = D1.next - # find d2 - D2 = self.head - while D2 is not None and D2.data != d2: - prevD2 = D2 - D2 = D2.next - if D1 is None and D2 is None: - return - # if D1 is head - if prevD1 is not None: - prevD1.next = D2 - else: - self.head = D2 - # if D2 is head - if prevD2 is not None: - prevD2.next = D1 - else: - self.head = D1 - temp = D1.next - D1.next = D2.next - D2.next = temp - -# swapping code ends here + node_1 = self.head + while node_1 is not None and node_1.data != node_data_1: + node_1 = node_1.next + node_2 = self.head + while node_2 is not None and node_2.data != node_data_2: + node_2 = node_2.next + if node_1 is None or node_2 is None: + return -if __name__ == '__main__': - list = Linkedlist() - list.push(5) - list.push(4) - list.push(3) - list.push(2) - list.push(1) - - list.print_list() + node_1.data, node_2.data = node_2.data, node_1.data - list.swapNodes(1, 4) - print("After swapping") - list.print_list() +if __name__ == "__main__": + ll = LinkedList() + for i in range(5, 0, -1): + ll.push(i) + ll.print_list() + ll.swap_nodes(1, 4) + print("After swapping") + ll.print_list() diff --git a/data_structures/queue/circular_queue.py b/data_structures/queue/circular_queue.py new file mode 100644 index 000000000000..229a67ebb8be --- /dev/null +++ b/data_structures/queue/circular_queue.py @@ -0,0 +1,94 @@ +# Implementation of Circular Queue (using Python lists) + + +class CircularQueue: + """Circular FIFO queue with a fixed capacity""" + + def __init__(self, n: int): + self.n = n + self.array = [None] * self.n + self.front = 0 # index of the first element + self.rear = 0 + self.size = 0 + + def __len__(self) -> int: + """ + >>> cq = CircularQueue(5) + >>> len(cq) + 0 + >>> cq.enqueue("A") # doctest: +ELLIPSIS + >> len(cq) + 1 + """ + return self.size + + def is_empty(self) -> bool: + """ + >>> cq = CircularQueue(5) + >>> cq.is_empty() + True + >>> cq.enqueue("A").is_empty() + False + """ + return self.size == 0 + + def first(self): + """ + >>> cq = CircularQueue(5) + >>> cq.first() + False + >>> cq.enqueue("A").first() + 'A' + """ + return False if self.is_empty() else self.array[self.front] + + def enqueue(self, data): + """ + This function insert an element in the queue using self.rear value as an index + >>> cq = CircularQueue(5) + >>> cq.enqueue("A") # doctest: +ELLIPSIS + >> (cq.size, cq.first()) + (1, 'A') + >>> cq.enqueue("B") # doctest: +ELLIPSIS + >> (cq.size, cq.first()) + (2, 'A') + """ + if self.size >= self.n: + raise Exception("QUEUE IS FULL") + + self.array[self.rear] = data + self.rear = (self.rear + 1) % self.n + self.size += 1 + return self + + def dequeue(self): + """ + This function removes an element from the queue using on self.front value as an + index + >>> cq = CircularQueue(5) + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: UNDERFLOW + >>> cq.enqueue("A").enqueue("B").dequeue() + 'A' + >>> (cq.size, cq.first()) + (1, 'B') + >>> cq.dequeue() + 'B' + >>> cq.dequeue() + Traceback (most recent call last): + ... + Exception: UNDERFLOW + """ + if self.size == 0: + raise Exception("UNDERFLOW") + + temp = self.array[self.front] + self.array[self.front] = None + self.front = (self.front + 1) % self.n + self.size -= 1 + return temp diff --git a/data_structures/queue/double_ended_queue.py b/data_structures/queue/double_ended_queue.py index fdee64eb6ae0..dd003b7c98ac 100644 --- a/data_structures/queue/double_ended_queue.py +++ b/data_structures/queue/double_ended_queue.py @@ -1,40 +1,57 @@ -from __future__ import print_function -# Python code to demonstrate working of +# Python code to demonstrate working of # extend(), extendleft(), rotate(), reverse() - + # importing "collections" for deque operations import collections - + # initializing deque -de = collections.deque([1, 2, 3,]) - -# using extend() to add numbers to right end +de = collections.deque([1, 2, 3]) + +# using extend() to add numbers to right end # adds 4,5,6 to right end -de.extend([4,5,6]) - +de.extend([4, 5, 6]) + # printing modified deque -print ("The deque after extending deque at end is : ") -print (de) - -# using extendleft() to add numbers to left end +print("The deque after extending deque at end is : ") +print(de) + +# using extendleft() to add numbers to left end # adds 7,8,9 to right end -de.extendleft([7,8,9]) - +de.extendleft([7, 8, 9]) + # printing modified deque -print ("The deque after extending deque at beginning is : ") -print (de) - +print("The deque after extending deque at beginning is : ") +print(de) + # using rotate() to rotate the deque # rotates by 3 to left de.rotate(-3) - + # printing modified deque -print ("The deque after rotating deque is : ") -print (de) - +print("The deque after rotating deque is : ") +print(de) + # using reverse() to reverse the deque de.reverse() - + # printing modified deque -print ("The deque after reversing deque is : ") -print (de) +print("The deque after reversing deque is : ") +print(de) + +# get right-end value and eliminate +startValue = de.pop() + +print("The deque after popping value at end is : ") +print(de) + +# get left-end value and eliminate +endValue = de.popleft() + +print("The deque after popping value at start is : ") +print(de) + +# eliminate element searched by value +de.remove(5) + +print("The deque after eliminating element searched by value : ") +print(de) diff --git a/data_structures/queue/linked_queue.py b/data_structures/queue/linked_queue.py new file mode 100644 index 000000000000..614c60cd1ae2 --- /dev/null +++ b/data_structures/queue/linked_queue.py @@ -0,0 +1,74 @@ +""" A Queue using a Linked List like structure """ +from typing import Any, Optional + + +class Node: + def __init__(self, data: Any, next: Optional["Node"] = None): + self.data: Any = data + self.next: Optional["Node"] = next + + +class LinkedQueue: + """ + Linked List Queue implementing put (to end of queue), + get (from front of queue) and is_empty + + >>> queue = LinkedQueue() + >>> queue.is_empty() + True + >>> queue.put(5) + >>> queue.put(9) + >>> queue.put('python') + >>> queue.is_empty(); + False + >>> queue.get() + 5 + >>> queue.put('algorithms') + >>> queue.get() + 9 + >>> queue.get() + 'python' + >>> queue.get() + 'algorithms' + >>> queue.is_empty() + True + >>> queue.get() + Traceback (most recent call last): + ... + IndexError: get from empty queue + """ + + def __init__(self) -> None: + self.front: Optional[Node] = None + self.rear: Optional[Node] = None + + def is_empty(self) -> bool: + """ returns boolean describing if queue is empty """ + return self.front is None + + def put(self, item: Any) -> None: + """ append item to rear of queue """ + node: Node = Node(item) + if self.is_empty(): + # the queue contains just the single element + self.front = node + self.rear = node + else: + # not empty, so we add it to the rear of the queue + assert isinstance(self.rear, Node) + self.rear.next = node + self.rear = node + + def get(self) -> Any: + """ returns and removes item at front of queue """ + if self.is_empty(): + raise IndexError("get from empty queue") + else: + # "remove" element by having front point to the next one + assert isinstance(self.front, Node) + node: Node = self.front + self.front = node.next + if self.front is None: + self.rear = None + + return node.data diff --git a/data_structures/queue/queue_on_list.py b/data_structures/queue/queue_on_list.py index 2ec9bac8398a..485cf0b6f7a3 100644 --- a/data_structures/queue/queue_on_list.py +++ b/data_structures/queue/queue_on_list.py @@ -1,46 +1,52 @@ -"""Queue represented by a python list""" -class Queue(): +"""Queue represented by a Python list""" + + +class Queue: def __init__(self): self.entries = [] self.length = 0 - self.front=0 + self.front = 0 def __str__(self): - printed = '<' + str(self.entries)[1:-1] + '>' + printed = "<" + str(self.entries)[1:-1] + ">" return printed """Enqueues {@code item} @param item item to enqueue""" + def put(self, item): self.entries.append(item) self.length = self.length + 1 - """Dequeues {@code item} @requirement: |self.length| > 0 @return dequeued item that was dequeued""" + def get(self): self.length = self.length - 1 dequeued = self.entries[self.front] - #self.front-=1 - #self.entries = self.entries[self.front:] + # self.front-=1 + # self.entries = self.entries[self.front:] self.entries = self.entries[1:] return dequeued """Rotates the queue {@code rotation} times @param rotation number of times to rotate queue""" + def rotate(self, rotation): for i in range(rotation): self.put(self.get()) """Enqueues {@code item} @return item at front of self.entries""" - def front(self): + + def get_front(self): return self.entries[0] """Returns the length of this.entries""" + def size(self): return self.length diff --git a/data_structures/queue/queue_on_pseudo_stack.py b/data_structures/queue/queue_on_pseudo_stack.py index b69fbcc988f7..7fa2fb2566af 100644 --- a/data_structures/queue/queue_on_pseudo_stack.py +++ b/data_structures/queue/queue_on_pseudo_stack.py @@ -1,16 +1,19 @@ """Queue represented by a pseudo stack (represented by a list with pop and append)""" -class Queue(): + + +class Queue: def __init__(self): self.stack = [] self.length = 0 def __str__(self): - printed = '<' + str(self.stack)[1:-1] + '>' + printed = "<" + str(self.stack)[1:-1] + ">" return printed """Enqueues {@code item} @param item item to enqueue""" + def put(self, item): self.stack.append(item) self.length = self.length + 1 @@ -19,17 +22,19 @@ def put(self, item): @requirement: |self.length| > 0 @return dequeued item that was dequeued""" + def get(self): self.rotate(1) - dequeued = self.stack[self.length-1] + dequeued = self.stack[self.length - 1] self.stack = self.stack[:-1] - self.rotate(self.length-1) - self.length = self.length -1 + self.rotate(self.length - 1) + self.length = self.length - 1 return dequeued """Rotates the queue {@code rotation} times @param rotation number of times to rotate queue""" + def rotate(self, rotation): for i in range(rotation): temp = self.stack[0] @@ -39,12 +44,14 @@ def rotate(self, rotation): """Reports item at the front of self @return item at front of self.stack""" + def front(self): front = self.get() self.put(front) - self.rotate(self.length-1) + self.rotate(self.length - 1) return front """Returns the length of this.stack""" + def size(self): return self.length diff --git a/data_structures/stacks/__init__.py b/data_structures/stacks/__init__.py index f7e92ae2d269..f6995cf98977 100644 --- a/data_structures/stacks/__init__.py +++ b/data_structures/stacks/__init__.py @@ -1,23 +1,22 @@ class Stack: + def __init__(self): + self.stack = [] + self.top = 0 - def __init__(self): - self.stack = [] - self.top = 0 + def is_empty(self): + return self.top == 0 - def is_empty(self): - return (self.top == 0) + def push(self, item): + if self.top < len(self.stack): + self.stack[self.top] = item + else: + self.stack.append(item) - def push(self, item): - if self.top < len(self.stack): - self.stack[self.top] = item - else: - self.stack.append(item) + self.top += 1 - self.top += 1 - - def pop(self): - if self.is_empty(): - return None - else: - self.top -= 1 - return self.stack[self.top] + def pop(self): + if self.is_empty(): + return None + else: + self.top -= 1 + return self.stack[self.top] diff --git a/data_structures/stacks/balanced_parentheses.py b/data_structures/stacks/balanced_parentheses.py index 3229d19c8621..7aacd5969277 100644 --- a/data_structures/stacks/balanced_parentheses.py +++ b/data_structures/stacks/balanced_parentheses.py @@ -1,25 +1,23 @@ -from __future__ import print_function -from __future__ import absolute_import -from stack import Stack +from .stack import Stack -__author__ = 'Omkar Pathak' +__author__ = "Omkar Pathak" def balanced_parentheses(parentheses): """ Use a stack to check if a string of parentheses is balanced.""" stack = Stack(len(parentheses)) for parenthesis in parentheses: - if parenthesis == '(': + if parenthesis == "(": stack.push(parenthesis) - elif parenthesis == ')': + elif parenthesis == ")": if stack.is_empty(): return False stack.pop() return stack.is_empty() -if __name__ == '__main__': - examples = ['((()))', '((())', '(()))'] - print('Balanced parentheses demonstration:\n') +if __name__ == "__main__": + examples = ["((()))", "((())", "(()))"] + print("Balanced parentheses demonstration:\n") for example in examples: - print(example + ': ' + str(balanced_parentheses(example))) + print(example + ": " + str(balanced_parentheses(example))) diff --git a/data_structures/stacks/infix_to_postfix_conversion.py b/data_structures/stacks/infix_to_postfix_conversion.py index e71dccf1f45c..61114402377a 100644 --- a/data_structures/stacks/infix_to_postfix_conversion.py +++ b/data_structures/stacks/infix_to_postfix_conversion.py @@ -1,10 +1,8 @@ -from __future__ import print_function -from __future__ import absolute_import import string -from stack import Stack +from .stack import Stack -__author__ = 'Omkar Pathak' +__author__ = "Omkar Pathak" def is_operand(char): @@ -17,9 +15,7 @@ def precedence(char): https://en.wikipedia.org/wiki/Order_of_operations """ - dictionary = {'+': 1, '-': 1, - '*': 2, '/': 2, - '^': 3} + dictionary = {"+": 1, "-": 1, "*": 2, "/": 2, "^": 3} return dictionary.get(char, -1) @@ -36,29 +32,28 @@ def infix_to_postfix(expression): for char in expression: if is_operand(char): postfix.append(char) - elif char not in {'(', ')'}: - while (not stack.is_empty() - and precedence(char) <= precedence(stack.peek())): + elif char not in {"(", ")"}: + while not stack.is_empty() and precedence(char) <= precedence(stack.peek()): postfix.append(stack.pop()) stack.push(char) - elif char == '(': + elif char == "(": stack.push(char) - elif char == ')': - while not stack.is_empty() and stack.peek() != '(': + elif char == ")": + while not stack.is_empty() and stack.peek() != "(": postfix.append(stack.pop()) # Pop '(' from stack. If there is no '(', there is a mismatched # parentheses. - if stack.peek() != '(': - raise ValueError('Mismatched parentheses') + if stack.peek() != "(": + raise ValueError("Mismatched parentheses") stack.pop() while not stack.is_empty(): postfix.append(stack.pop()) - return ' '.join(postfix) + return " ".join(postfix) -if __name__ == '__main__': - expression = 'a+b*(c^d-e)^(f+g*h)-i' +if __name__ == "__main__": + expression = "a+b*(c^d-e)^(f+g*h)-i" - print('Infix to Postfix Notation demonstration:\n') - print('Infix notation: ' + expression) - print('Postfix notation: ' + infix_to_postfix(expression)) + print("Infix to Postfix Notation demonstration:\n") + print("Infix notation: " + expression) + print("Postfix notation: " + infix_to_postfix(expression)) diff --git a/data_structures/stacks/infix_to_prefix_conversion.py b/data_structures/stacks/infix_to_prefix_conversion.py index da5fc261fb9f..5aa41a119528 100644 --- a/data_structures/stacks/infix_to_prefix_conversion.py +++ b/data_structures/stacks/infix_to_prefix_conversion.py @@ -11,51 +11,85 @@ a | + | cb^a | | cb^a+ - a+b^c (Infix) -> +a^bc (Prefix) + a+b^c (Infix) -> +a^bc (Prefix) """ + def infix_2_postfix(Infix): Stack = [] Postfix = [] - priority = {'^':3, '*':2, '/':2, '%':2, '+':1, '-':1} # Priority of each operator - print_width = len(Infix) if(len(Infix)>7) else 7 + priority = { + "^": 3, + "*": 2, + "/": 2, + "%": 2, + "+": 1, + "-": 1, + } # Priority of each operator + print_width = len(Infix) if (len(Infix) > 7) else 7 # Print table header for output - print('Symbol'.center(8), 'Stack'.center(print_width), 'Postfix'.center(print_width), sep = " | ") - print('-'*(print_width*3+7)) + print( + "Symbol".center(8), + "Stack".center(print_width), + "Postfix".center(print_width), + sep=" | ", + ) + print("-" * (print_width * 3 + 7)) for x in Infix: - if(x.isalpha() or x.isdigit()): Postfix.append(x) # if x is Alphabet / Digit, add it to Postfix - elif(x == '('): Stack.append(x) # if x is "(" push to Stack - elif(x == ')'): # if x is ")" pop stack until "(" is encountered - while(Stack[-1] != '('): - Postfix.append( Stack.pop() ) #Pop stack & add the content to Postfix + if x.isalpha() or x.isdigit(): + Postfix.append(x) # if x is Alphabet / Digit, add it to Postfix + elif x == "(": + Stack.append(x) # if x is "(" push to Stack + elif x == ")": # if x is ")" pop stack until "(" is encountered + while Stack[-1] != "(": + Postfix.append(Stack.pop()) # Pop stack & add the content to Postfix Stack.pop() else: - if(len(Stack)==0): Stack.append(x) #If stack is empty, push x to stack + if len(Stack) == 0: + Stack.append(x) # If stack is empty, push x to stack else: - while( len(Stack) > 0 and priority[x] <= priority[Stack[-1]]): # while priority of x is not greater than priority of element in the stack - Postfix.append( Stack.pop() ) # pop stack & add to Postfix - Stack.append(x) # push x to stack + while ( + len(Stack) > 0 and priority[x] <= priority[Stack[-1]] + ): # while priority of x is not greater than priority of element in the stack + Postfix.append(Stack.pop()) # pop stack & add to Postfix + Stack.append(x) # push x to stack + + print( + x.center(8), + ("".join(Stack)).ljust(print_width), + ("".join(Postfix)).ljust(print_width), + sep=" | ", + ) # Output in tabular format - print(x.center(8), (''.join(Stack)).ljust(print_width), (''.join(Postfix)).ljust(print_width), sep = " | ") # Output in tabular format + while len(Stack) > 0: # while stack is not empty + Postfix.append(Stack.pop()) # pop stack & add to Postfix + print( + " ".center(8), + ("".join(Stack)).ljust(print_width), + ("".join(Postfix)).ljust(print_width), + sep=" | ", + ) # Output in tabular format - while(len(Stack) > 0): # while stack is not empty - Postfix.append( Stack.pop() ) # pop stack & add to Postfix - print(' '.center(8), (''.join(Stack)).ljust(print_width), (''.join(Postfix)).ljust(print_width), sep = " | ") # Output in tabular format + return "".join(Postfix) # return Postfix as str - return "".join(Postfix) # return Postfix as str def infix_2_prefix(Infix): - Infix = list(Infix[::-1]) # reverse the infix equation - + Infix = list(Infix[::-1]) # reverse the infix equation + for i in range(len(Infix)): - if(Infix[i] == '('): Infix[i] = ')' # change "(" to ")" - elif(Infix[i] == ')'): Infix[i] = '(' # change ")" to "(" - - return (infix_2_postfix("".join(Infix)))[::-1] # call infix_2_postfix on Infix, return reverse of Postfix + if Infix[i] == "(": + Infix[i] = ")" # change "(" to ")" + elif Infix[i] == ")": + Infix[i] = "(" # change ")" to "(" + + return (infix_2_postfix("".join(Infix)))[ + ::-1 + ] # call infix_2_postfix on Infix, return reverse of Postfix + if __name__ == "__main__": - Infix = input("\nEnter an Infix Equation = ") #Input an Infix equation - Infix = "".join(Infix.split()) #Remove spaces from the input + Infix = input("\nEnter an Infix Equation = ") # Input an Infix equation + Infix = "".join(Infix.split()) # Remove spaces from the input print("\n\t", Infix, "(Infix) -> ", infix_2_prefix(Infix), "(Prefix)") diff --git a/data_structures/stacks/linked_stack.py b/data_structures/stacks/linked_stack.py new file mode 100644 index 000000000000..18ba87ddc221 --- /dev/null +++ b/data_structures/stacks/linked_stack.py @@ -0,0 +1,67 @@ +""" A Stack using a Linked List like structure """ +from typing import Any, Optional + + +class Node: + def __init__(self, data: Any, next: Optional["Node"] = None): + self.data: Any = data + self.next: Optional["Node"] = next + + +class LinkedStack: + """ + Linked List Stack implementing push (to top), + pop (from top) and is_empty + + >>> stack = LinkedStack() + >>> stack.is_empty() + True + >>> stack.push(5) + >>> stack.push(9) + >>> stack.push('python') + >>> stack.is_empty(); + False + >>> stack.pop() + 'python' + >>> stack.push('algorithms') + >>> stack.pop() + 'algorithms' + >>> stack.pop() + 9 + >>> stack.pop() + 5 + >>> stack.is_empty() + True + >>> stack.pop() + Traceback (most recent call last): + ... + IndexError: pop from empty stack + """ + + def __init__(self) -> None: + self.top: Optional[Node] = None + + def is_empty(self) -> bool: + """ returns boolean describing if stack is empty """ + return self.top is None + + def push(self, item: Any) -> None: + """ append item to top of stack """ + node: Node = Node(item) + if self.is_empty(): + self.top = node + else: + # each node points to the item "lower" in the stack + node.next = self.top + self.top = node + + def pop(self) -> Any: + """ returns and removes item at top of stack """ + if self.is_empty(): + raise IndexError("pop from empty stack") + else: + # "remove" element by having top point to the next one + assert isinstance(self.top, Node) + node: Node = self.top + self.top = node.next + return node.data diff --git a/data_structures/stacks/next_greater_element.py b/data_structures/stacks/next_greater_element.py index bca83339592c..d8c7ed17317b 100644 --- a/data_structures/stacks/next_greater_element.py +++ b/data_structures/stacks/next_greater_element.py @@ -1,17 +1,98 @@ -from __future__ import print_function -# Function to print element and NGE pair for all elements of list -def printNGE(arr): - +arr = [-10, -5, 0, 5, 5.1, 11, 13, 21, 3, 4, -21, -10, -5, -1, 0] +expect = [-5, 0, 5, 5.1, 11, 13, 21, -1, 4, -1, -10, -5, -1, 0, -1] + + +def next_greatest_element_slow(arr: list) -> list: + """ + Get the Next Greatest Element (NGE) for all elements in a list. + Maximum element present after the current one which is also greater than the + current one. + >>> next_greatest_element_slow(arr) == expect + True + """ + result = [] for i in range(0, len(arr), 1): - next = -1 - for j in range(i+1, len(arr), 1): + for j in range(i + 1, len(arr), 1): if arr[i] < arr[j]: next = arr[j] break - - print(str(arr[i]) + " -- " + str(next)) - -# Driver program to test above function -arr = [11,13,21,3] -printNGE(arr) + result.append(next) + return result + + +def next_greatest_element_fast(arr: list) -> list: + """ + Like next_greatest_element_slow() but changes the loops to use + enumerate() instead of range(len()) for the outer loop and + for in a slice of arr for the inner loop. + >>> next_greatest_element_fast(arr) == expect + True + """ + result = [] + for i, outer in enumerate(arr): + next = -1 + for inner in arr[i + 1 :]: + if outer < inner: + next = inner + break + result.append(next) + return result + + +def next_greatest_element(arr: list) -> list: + """ + Get the Next Greatest Element (NGE) for all elements in a list. + Maximum element present after the current one which is also greater than the + current one. + + A naive way to solve this is to take two loops and check for the next bigger + number but that will make the time complexity as O(n^2). The better way to solve + this would be to use a stack to keep track of maximum number giving a linear time + solution. + >>> next_greatest_element(arr) == expect + True + """ + stack = [] + result = [-1] * len(arr) + + for index in reversed(range(len(arr))): + if len(stack): + while stack[-1] <= arr[index]: + stack.pop() + if len(stack) == 0: + break + + if len(stack) != 0: + result[index] = stack[-1] + + stack.append(arr[index]) + + return result + + +if __name__ == "__main__": + from doctest import testmod + from timeit import timeit + + testmod() + print(next_greatest_element_slow(arr)) + print(next_greatest_element_fast(arr)) + print(next_greatest_element(arr)) + + setup = ( + "from __main__ import arr, next_greatest_element_slow, " + "next_greatest_element_fast, next_greatest_element" + ) + print( + "next_greatest_element_slow():", + timeit("next_greatest_element_slow(arr)", setup=setup), + ) + print( + "next_greatest_element_fast():", + timeit("next_greatest_element_fast(arr)", setup=setup), + ) + print( + " next_greatest_element():", + timeit("next_greatest_element(arr)", setup=setup), + ) diff --git a/data_structures/stacks/postfix_evaluation.py b/data_structures/stacks/postfix_evaluation.py index 1786e71dd383..4cee0ba380b0 100644 --- a/data_structures/stacks/postfix_evaluation.py +++ b/data_structures/stacks/postfix_evaluation.py @@ -14,37 +14,57 @@ | pop(5) | + | push(5+54) | 59 - Result = 59 + Result = 59 """ import operator as op + def Solve(Postfix): Stack = [] - Div = lambda x, y: int(x/y) # integer division operation - Opr = {'^':op.pow, '*':op.mul, '/':Div, '+':op.add, '-':op.sub} # operators & their respective operation + Div = lambda x, y: int(x / y) # integer division operation + Opr = { + "^": op.pow, + "*": op.mul, + "/": Div, + "+": op.add, + "-": op.sub, + } # operators & their respective operation # print table header - print('Symbol'.center(8), 'Action'.center(12), 'Stack', sep = " | ") - print('-'*(30+len(Postfix))) + print("Symbol".center(8), "Action".center(12), "Stack", sep=" | ") + print("-" * (30 + len(Postfix))) for x in Postfix: - if( x.isdigit() ): # if x in digit - Stack.append(x) # append x to stack - print(x.rjust(8), ('push('+x+')').ljust(12), ','.join(Stack), sep = " | ") # output in tabular format + if x.isdigit(): # if x in digit + Stack.append(x) # append x to stack + print( + x.rjust(8), ("push(" + x + ")").ljust(12), ",".join(Stack), sep=" | " + ) # output in tabular format else: - B = Stack.pop() # pop stack - print("".rjust(8), ('pop('+B+')').ljust(12), ','.join(Stack), sep = " | ") # output in tabular format + B = Stack.pop() # pop stack + print( + "".rjust(8), ("pop(" + B + ")").ljust(12), ",".join(Stack), sep=" | " + ) # output in tabular format - A = Stack.pop() # pop stack - print("".rjust(8), ('pop('+A+')').ljust(12), ','.join(Stack), sep = " | ") # output in tabular format + A = Stack.pop() # pop stack + print( + "".rjust(8), ("pop(" + A + ")").ljust(12), ",".join(Stack), sep=" | " + ) # output in tabular format - Stack.append( str(Opr[x](int(A), int(B))) ) # evaluate the 2 values poped from stack & push result to stack - print(x.rjust(8), ('push('+A+x+B+')').ljust(12), ','.join(Stack), sep = " | ") # output in tabular format + Stack.append( + str(Opr[x](int(A), int(B))) + ) # evaluate the 2 values popped from stack & push result to stack + print( + x.rjust(8), + ("push(" + A + x + B + ")").ljust(12), + ",".join(Stack), + sep=" | ", + ) # output in tabular format return int(Stack[0]) if __name__ == "__main__": - Postfix = input("\n\nEnter a Postfix Equation (space separated) = ").split(' ') + Postfix = input("\n\nEnter a Postfix Equation (space separated) = ").split(" ") print("\n\tResult = ", Solve(Postfix)) diff --git a/data_structures/stacks/prefix_evaluation.py b/data_structures/stacks/prefix_evaluation.py new file mode 100644 index 000000000000..00df2c1e63b0 --- /dev/null +++ b/data_structures/stacks/prefix_evaluation.py @@ -0,0 +1,60 @@ +""" +Python3 program to evaluate a prefix expression. +""" + +calc = { + "+": lambda x, y: x + y, + "-": lambda x, y: x - y, + "*": lambda x, y: x * y, + "/": lambda x, y: x / y, +} + + +def is_operand(c): + """ + Return True if the given char c is an operand, e.g. it is a number + + >>> is_operand("1") + True + >>> is_operand("+") + False + """ + return c.isdigit() + + +def evaluate(expression): + """ + Evaluate a given expression in prefix notation. + Asserts that the given expression is valid. + + >>> evaluate("+ 9 * 2 6") + 21 + >>> evaluate("/ * 10 2 + 4 1 ") + 4.0 + """ + stack = [] + + # iterate over the string in reverse order + for c in expression.split()[::-1]: + + # push operand to stack + if is_operand(c): + stack.append(int(c)) + + else: + # pop values from stack can calculate the result + # push the result onto the stack again + o1 = stack.pop() + o2 = stack.pop() + stack.append(calc[c](o1, o2)) + + return stack.pop() + + +# Driver code +if __name__ == "__main__": + test_expression = "+ 9 * 2 6" + print(evaluate(test_expression)) + + test_expression = "/ * 10 2 + 4 1 " + print(evaluate(test_expression)) diff --git a/data_structures/stacks/stack.py b/data_structures/stacks/stack.py index 7f979d927d08..baa0857eec0a 100644 --- a/data_structures/stacks/stack.py +++ b/data_structures/stacks/stack.py @@ -1,8 +1,7 @@ -from __future__ import print_function -__author__ = 'Omkar Pathak' +__author__ = "Omkar Pathak" -class Stack(object): +class Stack: """ A stack is an abstract data type that serves as a collection of elements with two principal operations: push() and pop(). push() adds an element to the top of the stack, and pop() removes an element from the top @@ -33,7 +32,7 @@ def pop(self): if self.stack: return self.stack.pop() else: - raise IndexError('pop from an empty stack') + raise IndexError("pop from an empty stack") def peek(self): """ Peek at the top-most element of the stack.""" @@ -48,22 +47,29 @@ def size(self): """ Return the size of the stack.""" return len(self.stack) + def __contains__(self, item) -> bool: + """Check if item is in stack""" + return item in self.stack + class StackOverflowError(BaseException): pass -if __name__ == '__main__': +if __name__ == "__main__": stack = Stack() for i in range(10): stack.push(i) - print('Stack demonstration:\n') - print('Initial stack: ' + str(stack)) - print('pop(): ' + str(stack.pop())) - print('After pop(), the stack is now: ' + str(stack)) - print('peek(): ' + str(stack.peek())) + print("Stack demonstration:\n") + print("Initial stack: " + str(stack)) + print("pop(): " + str(stack.pop())) + print("After pop(), the stack is now: " + str(stack)) + print("peek(): " + str(stack.peek())) stack.push(100) - print('After push(100), the stack is now: ' + str(stack)) - print('is_empty(): ' + str(stack.is_empty())) - print('size(): ' + str(stack.size())) + print("After push(100), the stack is now: " + str(stack)) + print("is_empty(): " + str(stack.is_empty())) + print("size(): " + str(stack.size())) + num = 5 + if num in stack: + print(f"{num} is in stack") diff --git a/data_structures/stacks/stack_using_dll.py b/data_structures/stacks/stack_using_dll.py new file mode 100644 index 000000000000..75e0cd20640d --- /dev/null +++ b/data_structures/stacks/stack_using_dll.py @@ -0,0 +1,123 @@ +# A complete working Python program to demonstrate all +# stack operations using a doubly linked list + + +class Node: + def __init__(self, data): + self.data = data # Assign data + self.next = None # Initialize next as null + self.prev = None # Initialize prev as null + + +class Stack: + """ + >>> stack = Stack() + >>> stack.is_empty() + True + >>> stack.print_stack() + stack elements are: + >>> for i in range(4): + ... stack.push(i) + ... + >>> stack.is_empty() + False + >>> stack.print_stack() + stack elements are: + 3->2->1->0-> + >>> stack.top() + 3 + >>> len(stack) + 4 + >>> stack.pop() + 3 + >>> stack.print_stack() + stack elements are: + 2->1->0-> + """ + + def __init__(self): + self.head = None + + def push(self, data): + """add a Node to the stack""" + if self.head is None: + self.head = Node(data) + else: + new_node = Node(data) + self.head.prev = new_node + new_node.next = self.head + new_node.prev = None + self.head = new_node + + def pop(self): + """pop the top element off the stack""" + if self.head is None: + return None + else: + temp = self.head.data + self.head = self.head.next + self.head.prev = None + return temp + + def top(self): + """return the top element of the stack""" + return self.head.data + + def __len__(self): + temp = self.head + count = 0 + while temp is not None: + count += 1 + temp = temp.next + return count + + def is_empty(self): + return self.head is None + + def print_stack(self): + print("stack elements are:") + temp = self.head + while temp is not None: + print(temp.data, end="->") + temp = temp.next + + +# Code execution starts here +if __name__ == "__main__": + + # Start with the empty stack + stack = Stack() + + # Insert 4 at the beginning. So stack becomes 4->None + print("Stack operations using Doubly LinkedList") + stack.push(4) + + # Insert 5 at the beginning. So stack becomes 4->5->None + stack.push(5) + + # Insert 6 at the beginning. So stack becomes 4->5->6->None + stack.push(6) + + # Insert 7 at the beginning. So stack becomes 4->5->6->7->None + stack.push(7) + + # Print the stack + stack.print_stack() + + # Print the top element + print("\nTop element is ", stack.top()) + + # Print the stack size + print("Size of the stack is ", len(stack)) + + # pop the top element + stack.pop() + + # pop the top element + stack.pop() + + # two elements have now been popped off + stack.print_stack() + + # Print True if the stack is empty else False + print("\nstack is empty:", stack.is_empty()) diff --git a/data_structures/stacks/stock_span_problem.py b/data_structures/stacks/stock_span_problem.py index 9628864edd10..cc2adfdd6c21 100644 --- a/data_structures/stacks/stock_span_problem.py +++ b/data_structures/stacks/stock_span_problem.py @@ -1,52 +1,53 @@ -''' -The stock span problem is a financial problem where we have a series of n daily +""" +The stock span problem is a financial problem where we have a series of n daily price quotes for a stock and we need to calculate span of stock's price for all n days. -The span Si of the stock's price on a given day i is defined as the maximum -number of consecutive days just before the given day, for which the price of the stock +The span Si of the stock's price on a given day i is defined as the maximum +number of consecutive days just before the given day, for which the price of the stock on the current day is less than or equal to its price on the given day. -''' -from __future__ import print_function -def calculateSpan(price, S): - - n = len(price) - # Create a stack and push index of fist element to it - st = [] - st.append(0) - - # Span value of first element is always 1 - S[0] = 1 - - # Calculate span values for rest of the elements - for i in range(1, n): - - # Pop elements from stack whlie stack is not - # empty and top of stack is smaller than price[i] - while( len(st) > 0 and price[st[0]] <= price[i]): - st.pop() - - # If stack becomes empty, then price[i] is greater - # than all elements on left of it, i.e. price[0], - # price[1], ..price[i-1]. Else the price[i] is - # greater than elements after top of stack - S[i] = i+1 if len(st) <= 0 else (i - st[0]) - - # Push this element to stack - st.append(i) - - -# A utility function to print elements of array -def printArray(arr, n): - for i in range(0,n): - print (arr[i],end =" ") - - -# Driver program to test above function -price = [10, 4, 5, 90, 120, 80] -S = [0 for i in range(len(price)+1)] - -# Fill the span values in array S[] -calculateSpan(price, S) - -# Print the calculated span values -printArray(S, len(price)) +""" + + +def calculateSpan(price, S): + + n = len(price) + # Create a stack and push index of fist element to it + st = [] + st.append(0) + + # Span value of first element is always 1 + S[0] = 1 + + # Calculate span values for rest of the elements + for i in range(1, n): + + # Pop elements from stack while stack is not + # empty and top of stack is smaller than price[i] + while len(st) > 0 and price[st[0]] <= price[i]: + st.pop() + + # If stack becomes empty, then price[i] is greater + # than all elements on left of it, i.e. price[0], + # price[1], ..price[i-1]. Else the price[i] is + # greater than elements after top of stack + S[i] = i + 1 if len(st) <= 0 else (i - st[0]) + + # Push this element to stack + st.append(i) + + +# A utility function to print elements of array +def printArray(arr, n): + for i in range(0, n): + print(arr[i], end=" ") + + +# Driver program to test above function +price = [10, 4, 5, 90, 120, 80] +S = [0 for i in range(len(price) + 1)] + +# Fill the span values in array S[] +calculateSpan(price, S) + +# Print the calculated span values +printArray(S, len(price)) diff --git a/data_structures/trie/trie.py b/data_structures/trie/trie.py index b6234c6704c6..5a560b97c293 100644 --- a/data_structures/trie/trie.py +++ b/data_structures/trie/trie.py @@ -1,9 +1,8 @@ """ A Trie/Prefix Tree is a kind of search tree used to provide quick lookup of words/patterns in a set of words. A basic Trie however has O(n^2) space complexity -making it impractical in practice. It however provides O(max(search_string, length of longest word)) lookup -time making it an optimal approach when space is not an issue. - +making it impractical in practice. It however provides O(max(search_string, length of longest word)) +lookup time making it an optimal approach when space is not an issue. """ @@ -12,7 +11,7 @@ def __init__(self): self.nodes = dict() # Mapping from char to TrieNode self.is_leaf = False - def insert_many(self, words: [str]): # noqa: E999 This syntax is Python 3 only + def insert_many(self, words: [str]): """ Inserts a list of words into the Trie :param words: list of string words @@ -21,7 +20,7 @@ def insert_many(self, words: [str]): # noqa: E999 This syntax is Python 3 only for word in words: self.insert(word) - def insert(self, word: str): # noqa: E999 This syntax is Python 3 only + def insert(self, word: str): """ Inserts a word into the Trie :param word: word to be inserted @@ -34,7 +33,7 @@ def insert(self, word: str): # noqa: E999 This syntax is Python 3 only curr = curr.nodes[char] curr.is_leaf = True - def find(self, word: str) -> bool: # noqa: E999 This syntax is Python 3 only + def find(self, word: str) -> bool: """ Tries to find word in a Trie :param word: word to look for @@ -47,8 +46,36 @@ def find(self, word: str) -> bool: # noqa: E999 This syntax is Python 3 only curr = curr.nodes[char] return curr.is_leaf + def delete(self, word: str): + """ + Deletes a word in a Trie + :param word: word to delete + :return: None + """ -def print_words(node: TrieNode, word: str): # noqa: E999 This syntax is Python 3 only + def _delete(curr: TrieNode, word: str, index: int): + if index == len(word): + # If word does not exist + if not curr.is_leaf: + return False + curr.is_leaf = False + return len(curr.nodes) == 0 + char = word[index] + char_node = curr.nodes.get(char) + # If char not in current trie node + if not char_node: + return False + # Flag to check if node can be deleted + delete_curr = _delete(char_node, word, index + 1) + if delete_curr: + del curr.nodes[char] + return len(curr.nodes) == 0 + return delete_curr + + _delete(self, word, 0) + + +def print_words(node: TrieNode, word: str): """ Prints all the words in a Trie :param node: root node of Trie @@ -56,20 +83,45 @@ def print_words(node: TrieNode, word: str): # noqa: E999 This syntax is Python :return: None """ if node.is_leaf: - print(word, end=' ') + print(word, end=" ") for key, value in node.nodes.items(): print_words(value, word + key) -def test(): - words = ['banana', 'bananas', 'bandana', 'band', 'apple', 'all', 'beast'] +def test_trie(): + words = "banana bananas bandana band apple all beast".split() root = TrieNode() root.insert_many(words) - # print_words(root, '') - assert root.find('banana') - assert not root.find('bandanas') - assert not root.find('apps') - assert root.find('apple') + # print_words(root, "") + assert all(root.find(word) for word in words) + assert root.find("banana") + assert not root.find("bandanas") + assert not root.find("apps") + assert root.find("apple") + assert root.find("all") + root.delete("all") + assert not root.find("all") + root.delete("banana") + assert not root.find("banana") + assert root.find("bananas") + return True + + +def print_results(msg: str, passes: bool) -> None: + print(str(msg), "works!" if passes else "doesn't work :(") + + +def pytests(): + assert test_trie() + + +def main(): + """ + >>> pytests() + """ + print_results("Testing trie functionality", test_trie()) + -test() +if __name__ == "__main__": + main() diff --git a/digital_image_processing/change_contrast.py b/digital_image_processing/change_contrast.py new file mode 100644 index 000000000000..c7da52298ae2 --- /dev/null +++ b/digital_image_processing/change_contrast.py @@ -0,0 +1,35 @@ +""" +Changing contrast with PIL + +This algorithm is used in +https://noivce.pythonanywhere.com/ Python web app. + +python/black: True +flake8 : True +""" + +from PIL import Image + + +def change_contrast(img: Image, level: float) -> Image: + """ + Function to change contrast + """ + factor = (259 * (level + 255)) / (255 * (259 - level)) + + def contrast(c: int) -> float: + """ + Fundamental Transformation/Operation that'll be performed on + every bit. + """ + return 128 + factor * (c - 128) + + return img.point(contrast) + + +if __name__ == "__main__": + # Load image + with Image.open("image_data/lena.jpg") as img: + # Change contrast to 170 + cont_img = change_contrast(img, 170) + cont_img.save("image_data/lena_high_contrast.png", format="png") diff --git a/digital_image_processing/convert_to_negative.py b/digital_image_processing/convert_to_negative.py new file mode 100644 index 000000000000..cba503938aec --- /dev/null +++ b/digital_image_processing/convert_to_negative.py @@ -0,0 +1,30 @@ +""" + Implemented an algorithm using opencv to convert a colored image into its negative +""" + +from cv2 import imread, imshow, waitKey, destroyAllWindows + + +def convert_to_negative(img): + # getting number of pixels in the image + pixel_h, pixel_v = img.shape[0], img.shape[1] + + # converting each pixel's color to its negative + for i in range(pixel_h): + for j in range(pixel_v): + img[i][j] = [255, 255, 255] - img[i][j] + + return img + + +if __name__ == "__main__": + # read original image + img = imread("image_data/lena.jpg", 1) + + # convert to its negative + neg = convert_to_negative(img) + + # show result image + imshow("negative of original image", img) + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/dithering/__init__.py b/digital_image_processing/dithering/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/dithering/burkes.py b/digital_image_processing/dithering/burkes.py new file mode 100644 index 000000000000..54a243bd255a --- /dev/null +++ b/digital_image_processing/dithering/burkes.py @@ -0,0 +1,87 @@ +""" +Implementation Burke's algorithm (dithering) +""" +from cv2 import destroyAllWindows, imread, imshow, waitKey +import numpy as np + + +class Burkes: + """ + Burke's algorithm is using for converting grayscale image to black and white version + Source: Source: https://en.wikipedia.org/wiki/Dither + + Note: + * Best results are given with threshold= ~1/2 * max greyscale value. + * This implementation get RGB image and converts it to greyscale in runtime. + """ + + def __init__(self, input_img, threshold: int): + self.min_threshold = 0 + # max greyscale value for #FFFFFF + self.max_threshold = int(self.get_greyscale(255, 255, 255)) + + if not self.min_threshold < threshold < self.max_threshold: + raise ValueError(f"Factor value should be from 0 to {self.max_threshold}") + + self.input_img = input_img + self.threshold = threshold + self.width, self.height = self.input_img.shape[1], self.input_img.shape[0] + + # error table size (+4 columns and +1 row) greater than input image because of + # lack of if statements + self.error_table = [ + [0 for _ in range(self.height + 4)] for __ in range(self.width + 1) + ] + self.output_img = np.ones((self.width, self.height, 3), np.uint8) * 255 + + @classmethod + def get_greyscale(cls, blue: int, green: int, red: int) -> float: + """ + >>> Burkes.get_greyscale(3, 4, 5) + 3.753 + """ + return 0.114 * blue + 0.587 * green + 0.2126 * red + + def process(self) -> None: + for y in range(self.height): + for x in range(self.width): + greyscale = int(self.get_greyscale(*self.input_img[y][x])) + if self.threshold > greyscale + self.error_table[y][x]: + self.output_img[y][x] = (0, 0, 0) + current_error = greyscale + self.error_table[x][y] + else: + self.output_img[y][x] = (255, 255, 255) + current_error = greyscale + self.error_table[x][y] - 255 + """ + Burkes error propagation (`*` is current pixel): + + * 8/32 4/32 + 2/32 4/32 8/32 4/32 2/32 + """ + self.error_table[y][x + 1] += int(8 / 32 * current_error) + self.error_table[y][x + 2] += int(4 / 32 * current_error) + self.error_table[y + 1][x] += int(8 / 32 * current_error) + self.error_table[y + 1][x + 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x + 2] += int(2 / 32 * current_error) + self.error_table[y + 1][x - 1] += int(4 / 32 * current_error) + self.error_table[y + 1][x - 2] += int(2 / 32 * current_error) + + +if __name__ == "__main__": + # create Burke's instances with original images in greyscale + burkes_instances = [ + Burkes(imread("image_data/lena.jpg", 1), threshold) + for threshold in (1, 126, 130, 140) + ] + + for burkes in burkes_instances: + burkes.process() + + for burkes in burkes_instances: + imshow( + f"Original image with dithering threshold: {burkes.threshold}", + burkes.output_img, + ) + + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/edge_detection/canny.py b/digital_image_processing/edge_detection/canny.py index 7fde75a90a48..6f98fee6308e 100644 --- a/digital_image_processing/edge_detection/canny.py +++ b/digital_image_processing/edge_detection/canny.py @@ -8,8 +8,12 @@ def gen_gaussian_kernel(k_size, sigma): center = k_size // 2 - x, y = np.mgrid[0 - center:k_size - center, 0 - center:k_size - center] - g = 1 / (2 * np.pi * sigma) * np.exp(-(np.square(x) + np.square(y)) / (2 * np.square(sigma))) + x, y = np.mgrid[0 - center : k_size - center, 0 - center : k_size - center] + g = ( + 1 + / (2 * np.pi * sigma) + * np.exp(-(np.square(x) + np.square(y)) / (2 * np.square(sigma))) + ) return g @@ -34,27 +38,33 @@ def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): if ( 0 <= direction < 22.5 - or 15 * PI / 8 <= direction <= 2 * PI - or 7 * PI / 8 <= direction <= 9 * PI / 8 + or 15 * PI / 8 <= direction <= 2 * PI + or 7 * PI / 8 <= direction <= 9 * PI / 8 ): W = sobel_grad[row, col - 1] E = sobel_grad[row, col + 1] if sobel_grad[row, col] >= W and sobel_grad[row, col] >= E: dst[row, col] = sobel_grad[row, col] - elif (PI / 8 <= direction < 3 * PI / 8) or (9 * PI / 8 <= direction < 11 * PI / 8): + elif (PI / 8 <= direction < 3 * PI / 8) or ( + 9 * PI / 8 <= direction < 11 * PI / 8 + ): SW = sobel_grad[row + 1, col - 1] NE = sobel_grad[row - 1, col + 1] if sobel_grad[row, col] >= SW and sobel_grad[row, col] >= NE: dst[row, col] = sobel_grad[row, col] - elif (3 * PI / 8 <= direction < 5 * PI / 8) or (11 * PI / 8 <= direction < 13 * PI / 8): + elif (3 * PI / 8 <= direction < 5 * PI / 8) or ( + 11 * PI / 8 <= direction < 13 * PI / 8 + ): N = sobel_grad[row - 1, col] S = sobel_grad[row + 1, col] if sobel_grad[row, col] >= N and sobel_grad[row, col] >= S: dst[row, col] = sobel_grad[row, col] - elif (5 * PI / 8 <= direction < 7 * PI / 8) or (13 * PI / 8 <= direction < 15 * PI / 8): + elif (5 * PI / 8 <= direction < 7 * PI / 8) or ( + 13 * PI / 8 <= direction < 15 * PI / 8 + ): NW = sobel_grad[row - 1, col - 1] SE = sobel_grad[row + 1, col + 1] if sobel_grad[row, col] >= NW and sobel_grad[row, col] >= SE: @@ -82,14 +92,14 @@ def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): for col in range(1, image_col): if dst[row, col] == weak: if 255 in ( - dst[row, col + 1], - dst[row, col - 1], - dst[row - 1, col], - dst[row + 1, col], - dst[row - 1, col - 1], - dst[row + 1, col - 1], - dst[row - 1, col + 1], - dst[row + 1, col + 1], + dst[row, col + 1], + dst[row, col - 1], + dst[row - 1, col], + dst[row + 1, col], + dst[row - 1, col - 1], + dst[row + 1, col - 1], + dst[row - 1, col + 1], + dst[row + 1, col + 1], ): dst[row, col] = strong else: @@ -98,10 +108,10 @@ def canny(image, threshold_low=15, threshold_high=30, weak=128, strong=255): return dst -if __name__ == '__main__': +if __name__ == "__main__": # read original image in gray mode - lena = cv2.imread(r'../image_data/lena.jpg', 0) + lena = cv2.imread(r"../image_data/lena.jpg", 0) # canny edge detection canny_dst = canny(lena) - cv2.imshow('canny', canny_dst) + cv2.imshow("canny", canny_dst) cv2.waitKey(0) diff --git a/digital_image_processing/filters/bilateral_filter.py b/digital_image_processing/filters/bilateral_filter.py new file mode 100644 index 000000000000..753d6ddb7a3f --- /dev/null +++ b/digital_image_processing/filters/bilateral_filter.py @@ -0,0 +1,88 @@ +""" +Implementation of Bilateral filter + +Inputs: + img: A 2d image with values in between 0 and 1 + varS: variance in space dimension. + varI: variance in Intensity. + N: Kernel size(Must be an odd number) +Output: + img:A 2d zero padded image with values in between 0 and 1 +""" + +import cv2 +import numpy as np +import math +import sys + + +def vec_gaussian(img: np.ndarray, variance: float) -> np.ndarray: + # For applying gaussian function for each element in matrix. + sigma = math.sqrt(variance) + cons = 1 / (sigma * math.sqrt(2 * math.pi)) + return cons * np.exp(-((img / sigma) ** 2) * 0.5) + + +def get_slice(img: np.ndarray, x: int, y: int, kernel_size: int) -> np.ndarray: + half = kernel_size // 2 + return img[x - half : x + half + 1, y - half : y + half + 1] + + +def get_gauss_kernel(kernel_size: int, spatial_variance: float) -> np.ndarray: + # Creates a gaussian kernel of given dimension. + arr = np.zeros((kernel_size, kernel_size)) + for i in range(0, kernel_size): + for j in range(0, kernel_size): + arr[i, j] = math.sqrt( + abs(i - kernel_size // 2) ** 2 + abs(j - kernel_size // 2) ** 2 + ) + return vec_gaussian(arr, spatial_variance) + + +def bilateral_filter( + img: np.ndarray, + spatial_variance: float, + intensity_variance: float, + kernel_size: int, +) -> np.ndarray: + img2 = np.zeros(img.shape) + gaussKer = get_gauss_kernel(kernel_size, spatial_variance) + sizeX, sizeY = img.shape + for i in range(kernel_size // 2, sizeX - kernel_size // 2): + for j in range(kernel_size // 2, sizeY - kernel_size // 2): + + imgS = get_slice(img, i, j, kernel_size) + imgI = imgS - imgS[kernel_size // 2, kernel_size // 2] + imgIG = vec_gaussian(imgI, intensity_variance) + weights = np.multiply(gaussKer, imgIG) + vals = np.multiply(imgS, weights) + val = np.sum(vals) / np.sum(weights) + img2[i, j] = val + return img2 + + +def parse_args(args: list) -> tuple: + filename = args[1] if args[1:] else "../image_data/lena.jpg" + spatial_variance = float(args[2]) if args[2:] else 1.0 + intensity_variance = float(args[3]) if args[3:] else 1.0 + if args[4:]: + kernel_size = int(args[4]) + kernel_size = kernel_size + abs(kernel_size % 2 - 1) + else: + kernel_size = 5 + return filename, spatial_variance, intensity_variance, kernel_size + + +if __name__ == "__main__": + filename, spatial_variance, intensity_variance, kernel_size = parse_args(sys.argv) + img = cv2.imread(filename, 0) + cv2.imshow("input image", img) + + out = img / 255 + out = out.astype("float32") + out = bilateral_filter(out, spatial_variance, intensity_variance, kernel_size) + out = out * 255 + out = np.uint8(out) + cv2.imshow("output image", out) + cv2.waitKey(0) + cv2.destroyAllWindows() diff --git a/digital_image_processing/filters/convolve.py b/digital_image_processing/filters/convolve.py index b7600d74c294..ec500d940366 100644 --- a/digital_image_processing/filters/convolve.py +++ b/digital_image_processing/filters/convolve.py @@ -13,7 +13,7 @@ def im2col(image, block_size): row = 0 for i in range(0, dst_height): for j in range(0, dst_width): - window = ravel(image[i:i + block_size[0], j:j + block_size[1]]) + window = ravel(image[i : i + block_size[0], j : j + block_size[1]]) image_array[row, :] = window row += 1 @@ -23,9 +23,9 @@ def im2col(image, block_size): def img_convolve(image, filter_kernel): height, width = image.shape[0], image.shape[1] k_size = filter_kernel.shape[0] - pad_size = k_size//2 + pad_size = k_size // 2 # Pads image with the edge values of array. - image_tmp = pad(image, pad_size, mode='edge') + image_tmp = pad(image, pad_size, mode="edge") # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows image_array = im2col(image_tmp, (k_size, k_size)) @@ -37,13 +37,13 @@ def img_convolve(image, filter_kernel): return dst -if __name__ == '__main__': +if __name__ == "__main__": # read original image - img = imread(r'../image_data/lena.jpg') + img = imread(r"../image_data/lena.jpg") # turn image in gray scale value gray = cvtColor(img, COLOR_BGR2GRAY) # Laplace operator Laplace_kernel = array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]) out = img_convolve(gray, Laplace_kernel).astype(uint8) - imshow('Laplacian', out) + imshow("Laplacian", out) waitKey(0) diff --git a/digital_image_processing/filters/gaussian_filter.py b/digital_image_processing/filters/gaussian_filter.py index ff85ce047220..79026ddcc57a 100644 --- a/digital_image_processing/filters/gaussian_filter.py +++ b/digital_image_processing/filters/gaussian_filter.py @@ -3,29 +3,29 @@ """ from cv2 import imread, cvtColor, COLOR_BGR2GRAY, imshow, waitKey from numpy import pi, mgrid, exp, square, zeros, ravel, dot, uint8 +from itertools import product def gen_gaussian_kernel(k_size, sigma): center = k_size // 2 - x, y = mgrid[0-center:k_size-center, 0-center:k_size-center] - g = 1/(2*pi*sigma) * exp(-(square(x) + square(y))/(2*square(sigma))) + x, y = mgrid[0 - center : k_size - center, 0 - center : k_size - center] + g = 1 / (2 * pi * sigma) * exp(-(square(x) + square(y)) / (2 * square(sigma))) return g def gaussian_filter(image, k_size, sigma): height, width = image.shape[0], image.shape[1] # dst image height and width - dst_height = height-k_size+1 - dst_width = width-k_size+1 + dst_height = height - k_size + 1 + dst_width = width - k_size + 1 # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows - image_array = zeros((dst_height*dst_width, k_size*k_size)) + image_array = zeros((dst_height * dst_width, k_size * k_size)) row = 0 - for i in range(0, dst_height): - for j in range(0, dst_width): - window = ravel(image[i:i + k_size, j:j + k_size]) - image_array[row, :] = window - row += 1 + for i, j in product(range(dst_height), range(dst_width)): + window = ravel(image[i : i + k_size, j : j + k_size]) + image_array[row, :] = window + row += 1 # turn the kernel into shape(k*k, 1) gaussian_kernel = gen_gaussian_kernel(k_size, sigma) @@ -37,9 +37,9 @@ def gaussian_filter(image, k_size, sigma): return dst -if __name__ == '__main__': +if __name__ == "__main__": # read original image - img = imread(r'../image_data/lena.jpg') + img = imread(r"../image_data/lena.jpg") # turn image in gray scale value gray = cvtColor(img, COLOR_BGR2GRAY) @@ -48,6 +48,6 @@ def gaussian_filter(image, k_size, sigma): gaussian5x5 = gaussian_filter(gray, 5, sigma=0.8) # show result images - imshow('gaussian filter with 3x3 mask', gaussian3x3) - imshow('gaussian filter with 5x5 mask', gaussian5x5) + imshow("gaussian filter with 3x3 mask", gaussian3x3) + imshow("gaussian filter with 5x5 mask", gaussian5x5) waitKey() diff --git a/digital_image_processing/filters/median_filter.py b/digital_image_processing/filters/median_filter.py index ed20b1ab7f78..151ef8a55df1 100644 --- a/digital_image_processing/filters/median_filter.py +++ b/digital_image_processing/filters/median_filter.py @@ -15,20 +15,20 @@ def median_filter(gray_img, mask=3): # set image borders bd = int(mask / 2) # copy image size - median_img = zeros_like(gray) + median_img = zeros_like(gray_img) for i in range(bd, gray_img.shape[0] - bd): for j in range(bd, gray_img.shape[1] - bd): # get mask according with mask - kernel = ravel(gray_img[i - bd:i + bd + 1, j - bd:j + bd + 1]) + kernel = ravel(gray_img[i - bd : i + bd + 1, j - bd : j + bd + 1]) # calculate mask median median = sort(kernel)[int8(divide((multiply(mask, mask)), 2) + 1)] median_img[i, j] = median return median_img -if __name__ == '__main__': +if __name__ == "__main__": # read original image - img = imread('../image_data/lena.jpg') + img = imread("../image_data/lena.jpg") # turn image in gray scale value gray = cvtColor(img, COLOR_BGR2GRAY) @@ -37,6 +37,6 @@ def median_filter(gray_img, mask=3): median5x5 = median_filter(gray, 5) # show result images - imshow('median filter with 3x3 mask', median3x3) - imshow('median filter with 5x5 mask', median5x5) + imshow("median filter with 3x3 mask", median3x3) + imshow("median filter with 5x5 mask", median5x5) waitKey(0) diff --git a/digital_image_processing/filters/sobel_filter.py b/digital_image_processing/filters/sobel_filter.py index f3ef407d49e5..822d49fe38a1 100644 --- a/digital_image_processing/filters/sobel_filter.py +++ b/digital_image_processing/filters/sobel_filter.py @@ -13,26 +13,26 @@ def sobel_filter(image): dst_x = np.abs(img_convolve(image, kernel_x)) dst_y = np.abs(img_convolve(image, kernel_y)) # modify the pix within [0, 255] - dst_x = dst_x * 255/np.max(dst_x) - dst_y = dst_y * 255/np.max(dst_y) + dst_x = dst_x * 255 / np.max(dst_x) + dst_y = dst_y * 255 / np.max(dst_y) dst_xy = np.sqrt((np.square(dst_x)) + (np.square(dst_y))) - dst_xy = dst_xy * 255/np.max(dst_xy) + dst_xy = dst_xy * 255 / np.max(dst_xy) dst = dst_xy.astype(np.uint8) theta = np.arctan2(dst_y, dst_x) return dst, theta -if __name__ == '__main__': +if __name__ == "__main__": # read original image - img = imread('../image_data/lena.jpg') + img = imread("../image_data/lena.jpg") # turn image in gray scale value gray = cvtColor(img, COLOR_BGR2GRAY) sobel_grad, sobel_theta = sobel_filter(gray) # show result images - imshow('sobel filter', sobel_grad) - imshow('sobel theta', sobel_theta) + imshow("sobel filter", sobel_grad) + imshow("sobel theta", sobel_theta) waitKey(0) diff --git a/digital_image_processing/histogram_equalization/histogram_stretch.py b/digital_image_processing/histogram_equalization/histogram_stretch.py new file mode 100644 index 000000000000..d4a6c08418ee --- /dev/null +++ b/digital_image_processing/histogram_equalization/histogram_stretch.py @@ -0,0 +1,64 @@ +""" +Created on Fri Sep 28 15:22:29 2018 + +@author: Binish125 +""" +import copy +import os + +import numpy as np + +import cv2 +import matplotlib.pyplot as plt + + +class contrastStretch: + def __init__(self): + self.img = "" + self.original_image = "" + self.last_list = [] + self.rem = 0 + self.L = 256 + self.sk = 0 + self.k = 0 + self.number_of_rows = 0 + self.number_of_cols = 0 + + def stretch(self, input_image): + self.img = cv2.imread(input_image, 0) + self.original_image = copy.deepcopy(self.img) + x, _, _ = plt.hist(self.img.ravel(), 256, [0, 256], label="x") + self.k = np.sum(x) + for i in range(len(x)): + prk = x[i] / self.k + self.sk += prk + last = (self.L - 1) * self.sk + if self.rem != 0: + self.rem = int(last % last) + last = int(last + 1 if self.rem >= 0.5 else last) + self.last_list.append(last) + self.number_of_rows = int(np.ma.count(self.img) / self.img[1].size) + self.number_of_cols = self.img[1].size + for i in range(self.number_of_cols): + for j in range(self.number_of_rows): + num = self.img[j][i] + if num != self.last_list[num]: + self.img[j][i] = self.last_list[num] + cv2.imwrite("output_data/output.jpg", self.img) + + def plotHistogram(self): + plt.hist(self.img.ravel(), 256, [0, 256]) + + def showImage(self): + cv2.imshow("Output-Image", self.img) + cv2.imshow("Input-Image", self.original_image) + cv2.waitKey(5000) + cv2.destroyAllWindows() + + +if __name__ == "__main__": + file_path = os.path.join(os.path.basename(__file__), "image_data/input.jpg") + stretcher = contrastStretch() + stretcher.stretch(file_path) + stretcher.plotHistogram() + stretcher.showImage() diff --git a/digital_image_processing/histogram_equalization/image_data/input.jpg b/digital_image_processing/histogram_equalization/image_data/input.jpg new file mode 100644 index 000000000000..483da6fd97f4 Binary files /dev/null and b/digital_image_processing/histogram_equalization/image_data/input.jpg differ diff --git a/digital_image_processing/histogram_equalization/output_data/output.jpg b/digital_image_processing/histogram_equalization/output_data/output.jpg new file mode 100644 index 000000000000..48c70d8ae17d Binary files /dev/null and b/digital_image_processing/histogram_equalization/output_data/output.jpg differ diff --git a/digital_image_processing/image_data/lena_small.jpg b/digital_image_processing/image_data/lena_small.jpg new file mode 100644 index 000000000000..b85144e9f65c Binary files /dev/null and b/digital_image_processing/image_data/lena_small.jpg differ diff --git a/digital_image_processing/index_calculation.py b/digital_image_processing/index_calculation.py new file mode 100644 index 000000000000..a34e51e56310 --- /dev/null +++ b/digital_image_processing/index_calculation.py @@ -0,0 +1,568 @@ +# Author: João Gustavo A. Amorim +# Author email: joaogustavoamorim@gmail.com +# Coding date: jan 2019 +# python/black: True + +# Imports +import numpy as np + +# Class implemented to calculus the index +class IndexCalculation: + """ + # Class Summary + This algorithm consists in calculating vegetation indices, these indices + can be used for precision agriculture for example (or remote sensing). There are + functions to define the data and to calculate the implemented indices. + + # Vegetation index + https://en.wikipedia.org/wiki/Vegetation_Index + A Vegetation Index (VI) is a spectral transformation of two or more bands designed + to enhance the contribution of vegetation properties and allow reliable spatial and + temporal inter-comparisons of terrestrial photosynthetic activity and canopy + structural variations + + # Information about channels (Wavelength range for each) + * nir - near-infrared + https://www.malvernpanalytical.com/br/products/technology/near-infrared-spectroscopy + Wavelength Range 700 nm to 2500 nm + * Red Edge + https://en.wikipedia.org/wiki/Red_edge + Wavelength Range 680 nm to 730 nm + * red + https://en.wikipedia.org/wiki/Color + Wavelength Range 635 nm to 700 nm + * blue + https://en.wikipedia.org/wiki/Color + Wavelength Range 450 nm to 490 nm + * green + https://en.wikipedia.org/wiki/Color + Wavelength Range 520 nm to 560 nm + + + # Implemented index list + #"abbreviationOfIndexName" -- list of channels used + + #"ARVI2" -- red, nir + #"CCCI" -- red, redEdge, nir + #"CVI" -- red, green, nir + #"GLI" -- red, green, blue + #"NDVI" -- red, nir + #"BNDVI" -- blue, nir + #"redEdgeNDVI" -- red, redEdge + #"GNDVI" -- green, nir + #"GBNDVI" -- green, blue, nir + #"GRNDVI" -- red, green, nir + #"RBNDVI" -- red, blue, nir + #"PNDVI" -- red, green, blue, nir + #"ATSAVI" -- red, nir + #"BWDRVI" -- blue, nir + #"CIgreen" -- green, nir + #"CIrededge" -- redEdge, nir + #"CI" -- red, blue + #"CTVI" -- red, nir + #"GDVI" -- green, nir + #"EVI" -- red, blue, nir + #"GEMI" -- red, nir + #"GOSAVI" -- green, nir + #"GSAVI" -- green, nir + #"Hue" -- red, green, blue + #"IVI" -- red, nir + #"IPVI" -- red, nir + #"I" -- red, green, blue + #"RVI" -- red, nir + #"MRVI" -- red, nir + #"MSAVI" -- red, nir + #"NormG" -- red, green, nir + #"NormNIR" -- red, green, nir + #"NormR" -- red, green, nir + #"NGRDI" -- red, green + #"RI" -- red, green + #"S" -- red, green, blue + #"IF" -- red, green, blue + #"DVI" -- red, nir + #"TVI" -- red, nir + #"NDRE" -- redEdge, nir + + #list of all index implemented + #allIndex = ["ARVI2", "CCCI", "CVI", "GLI", "NDVI", "BNDVI", "redEdgeNDVI", "GNDVI", + "GBNDVI", "GRNDVI", "RBNDVI", "PNDVI", "ATSAVI", "BWDRVI", "CIgreen", + "CIrededge", "CI", "CTVI", "GDVI", "EVI", "GEMI", "GOSAVI", "GSAVI", + "Hue", "IVI", "IPVI", "I", "RVI", "MRVI", "MSAVI", "NormG", "NormNIR", + "NormR", "NGRDI", "RI", "S", "IF", "DVI", "TVI", "NDRE"] + + #list of index with not blue channel + #notBlueIndex = ["ARVI2", "CCCI", "CVI", "NDVI", "redEdgeNDVI", "GNDVI", "GRNDVI", + "ATSAVI", "CIgreen", "CIrededge", "CTVI", "GDVI", "GEMI", "GOSAVI", + "GSAVI", "IVI", "IPVI", "RVI", "MRVI", "MSAVI", "NormG", "NormNIR", + "NormR", "NGRDI", "RI", "DVI", "TVI", "NDRE"] + + #list of index just with RGB channels + #RGBIndex = ["GLI", "CI", "Hue", "I", "NGRDI", "RI", "S", "IF"] + """ + + def __init__(self, red=None, green=None, blue=None, redEdge=None, nir=None): + # print("Numpy version: " + np.__version__) + self.setMatrices(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + + def setMatrices(self, red=None, green=None, blue=None, redEdge=None, nir=None): + if red is not None: + self.red = red + if green is not None: + self.green = green + if blue is not None: + self.blue = blue + if redEdge is not None: + self.redEdge = redEdge + if nir is not None: + self.nir = nir + return True + + def calculation( + self, index="", red=None, green=None, blue=None, redEdge=None, nir=None + ): + """ + performs the calculation of the index with the values instantiated in the class + :str index: abbreviation of index name to perform + """ + self.setMatrices(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + funcs = { + "ARVI2": self.ARVI2, + "CCCI": self.CCCI, + "CVI": self.CVI, + "GLI": self.GLI, + "NDVI": self.NDVI, + "BNDVI": self.BNDVI, + "redEdgeNDVI": self.redEdgeNDVI, + "GNDVI": self.GNDVI, + "GBNDVI": self.GBNDVI, + "GRNDVI": self.GRNDVI, + "RBNDVI": self.RBNDVI, + "PNDVI": self.PNDVI, + "ATSAVI": self.ATSAVI, + "BWDRVI": self.BWDRVI, + "CIgreen": self.CIgreen, + "CIrededge": self.CIrededge, + "CI": self.CI, + "CTVI": self.CTVI, + "GDVI": self.GDVI, + "EVI": self.EVI, + "GEMI": self.GEMI, + "GOSAVI": self.GOSAVI, + "GSAVI": self.GSAVI, + "Hue": self.Hue, + "IVI": self.IVI, + "IPVI": self.IPVI, + "I": self.I, + "RVI": self.RVI, + "MRVI": self.MRVI, + "MSAVI": self.MSAVI, + "NormG": self.NormG, + "NormNIR": self.NormNIR, + "NormR": self.NormR, + "NGRDI": self.NGRDI, + "RI": self.RI, + "S": self.S, + "IF": self.IF, + "DVI": self.DVI, + "TVI": self.TVI, + "NDRE": self.NDRE, + } + + try: + return funcs[index]() + except KeyError: + print("Index not in the list!") + return False + + def ARVI2(self): + """ + Atmospherically Resistant Vegetation Index 2 + https://www.indexdatabase.de/db/i-single.php?id=396 + :return: index + −0.18+1.17*(self.nir−self.red)/(self.nir+self.red) + """ + return -0.18 + (1.17 * ((self.nir - self.red) / (self.nir + self.red))) + + def CCCI(self): + """ + Canopy Chlorophyll Content Index + https://www.indexdatabase.de/db/i-single.php?id=224 + :return: index + """ + return ((self.nir - self.redEdge) / (self.nir + self.redEdge)) / ( + (self.nir - self.red) / (self.nir + self.red) + ) + + def CVI(self): + """ + Chlorophyll vegetation index + https://www.indexdatabase.de/db/i-single.php?id=391 + :return: index + """ + return self.nir * (self.red / (self.green ** 2)) + + def GLI(self): + """ + self.green leaf index + https://www.indexdatabase.de/db/i-single.php?id=375 + :return: index + """ + return (2 * self.green - self.red - self.blue) / ( + 2 * self.green + self.red + self.blue + ) + + def NDVI(self): + """ + Normalized Difference self.nir/self.red Normalized Difference Vegetation Index, + Calibrated NDVI - CDVI + https://www.indexdatabase.de/db/i-single.php?id=58 + :return: index + """ + return (self.nir - self.red) / (self.nir + self.red) + + def BNDVI(self): + """ + Normalized Difference self.nir/self.blue self.blue-normalized difference + vegetation index + https://www.indexdatabase.de/db/i-single.php?id=135 + :return: index + """ + return (self.nir - self.blue) / (self.nir + self.blue) + + def redEdgeNDVI(self): + """ + Normalized Difference self.rededge/self.red + https://www.indexdatabase.de/db/i-single.php?id=235 + :return: index + """ + return (self.redEdge - self.red) / (self.redEdge + self.red) + + def GNDVI(self): + """ + Normalized Difference self.nir/self.green self.green NDVI + https://www.indexdatabase.de/db/i-single.php?id=401 + :return: index + """ + return (self.nir - self.green) / (self.nir + self.green) + + def GBNDVI(self): + """ + self.green-self.blue NDVI + https://www.indexdatabase.de/db/i-single.php?id=186 + :return: index + """ + return (self.nir - (self.green + self.blue)) / ( + self.nir + (self.green + self.blue) + ) + + def GRNDVI(self): + """ + self.green-self.red NDVI + https://www.indexdatabase.de/db/i-single.php?id=185 + :return: index + """ + return (self.nir - (self.green + self.red)) / ( + self.nir + (self.green + self.red) + ) + + def RBNDVI(self): + """ + self.red-self.blue NDVI + https://www.indexdatabase.de/db/i-single.php?id=187 + :return: index + """ + return (self.nir - (self.blue + self.red)) / (self.nir + (self.blue + self.red)) + + def PNDVI(self): + """ + Pan NDVI + https://www.indexdatabase.de/db/i-single.php?id=188 + :return: index + """ + return (self.nir - (self.green + self.red + self.blue)) / ( + self.nir + (self.green + self.red + self.blue) + ) + + def ATSAVI(self, X=0.08, a=1.22, b=0.03): + """ + Adjusted transformed soil-adjusted VI + https://www.indexdatabase.de/db/i-single.php?id=209 + :return: index + """ + return a * ( + (self.nir - a * self.red - b) + / (a * self.nir + self.red - a * b + X * (1 + a ** 2)) + ) + + def BWDRVI(self): + """ + self.blue-wide dynamic range vegetation index + https://www.indexdatabase.de/db/i-single.php?id=136 + :return: index + """ + return (0.1 * self.nir - self.blue) / (0.1 * self.nir + self.blue) + + def CIgreen(self): + """ + Chlorophyll Index self.green + https://www.indexdatabase.de/db/i-single.php?id=128 + :return: index + """ + return (self.nir / self.green) - 1 + + def CIrededge(self): + """ + Chlorophyll Index self.redEdge + https://www.indexdatabase.de/db/i-single.php?id=131 + :return: index + """ + return (self.nir / self.redEdge) - 1 + + def CI(self): + """ + Coloration Index + https://www.indexdatabase.de/db/i-single.php?id=11 + :return: index + """ + return (self.red - self.blue) / self.red + + def CTVI(self): + """ + Corrected Transformed Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=244 + :return: index + """ + ndvi = self.NDVI() + return ((ndvi + 0.5) / (abs(ndvi + 0.5))) * (abs(ndvi + 0.5) ** (1 / 2)) + + def GDVI(self): + """ + Difference self.nir/self.green self.green Difference Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=27 + :return: index + """ + return self.nir - self.green + + def EVI(self): + """ + Enhanced Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=16 + :return: index + """ + return 2.5 * ( + (self.nir - self.red) / (self.nir + 6 * self.red - 7.5 * self.blue + 1) + ) + + def GEMI(self): + """ + Global Environment Monitoring Index + https://www.indexdatabase.de/db/i-single.php?id=25 + :return: index + """ + n = (2 * (self.nir ** 2 - self.red ** 2) + 1.5 * self.nir + 0.5 * self.red) / ( + self.nir + self.red + 0.5 + ) + return n * (1 - 0.25 * n) - (self.red - 0.125) / (1 - self.red) + + def GOSAVI(self, Y=0.16): + """ + self.green Optimized Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=29 + mit Y = 0,16 + :return: index + """ + return (self.nir - self.green) / (self.nir + self.green + Y) + + def GSAVI(self, L=0.5): + """ + self.green Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=31 + mit L = 0,5 + :return: index + """ + return ((self.nir - self.green) / (self.nir + self.green + L)) * (1 + L) + + def Hue(self): + """ + Hue + https://www.indexdatabase.de/db/i-single.php?id=34 + :return: index + """ + return np.arctan( + ((2 * self.red - self.green - self.blue) / 30.5) * (self.green - self.blue) + ) + + def IVI(self, a=None, b=None): + """ + Ideal vegetation index + https://www.indexdatabase.de/db/i-single.php?id=276 + b=intercept of vegetation line + a=soil line slope + :return: index + """ + return (self.nir - b) / (a * self.red) + + def IPVI(self): + """ + Infraself.red percentage vegetation index + https://www.indexdatabase.de/db/i-single.php?id=35 + :return: index + """ + return (self.nir / ((self.nir + self.red) / 2)) * (self.NDVI() + 1) + + def I(self): + """ + Intensity + https://www.indexdatabase.de/db/i-single.php?id=36 + :return: index + """ + return (self.red + self.green + self.blue) / 30.5 + + def RVI(self): + """ + Ratio-Vegetation-Index + http://www.seos-project.eu/modules/remotesensing/remotesensing-c03-s01-p01.html + :return: index + """ + return self.nir / self.red + + def MRVI(self): + """ + Modified Normalized Difference Vegetation Index RVI + https://www.indexdatabase.de/db/i-single.php?id=275 + :return: index + """ + return (self.RVI() - 1) / (self.RVI() + 1) + + def MSAVI(self): + """ + Modified Soil Adjusted Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=44 + :return: index + """ + return ( + (2 * self.nir + 1) + - ((2 * self.nir + 1) ** 2 - 8 * (self.nir - self.red)) ** (1 / 2) + ) / 2 + + def NormG(self): + """ + Norm G + https://www.indexdatabase.de/db/i-single.php?id=50 + :return: index + """ + return self.green / (self.nir + self.red + self.green) + + def NormNIR(self): + """ + Norm self.nir + https://www.indexdatabase.de/db/i-single.php?id=51 + :return: index + """ + return self.nir / (self.nir + self.red + self.green) + + def NormR(self): + """ + Norm R + https://www.indexdatabase.de/db/i-single.php?id=52 + :return: index + """ + return self.red / (self.nir + self.red + self.green) + + def NGRDI(self): + """ + Normalized Difference self.green/self.red Normalized self.green self.red + difference index, Visible Atmospherically Resistant Indices self.green (VIself.green) + https://www.indexdatabase.de/db/i-single.php?id=390 + :return: index + """ + return (self.green - self.red) / (self.green + self.red) + + def RI(self): + """ + Normalized Difference self.red/self.green self.redness Index + https://www.indexdatabase.de/db/i-single.php?id=74 + :return: index + """ + return (self.red - self.green) / (self.red + self.green) + + def S(self): + """ + Saturation + https://www.indexdatabase.de/db/i-single.php?id=77 + :return: index + """ + max = np.max([np.max(self.red), np.max(self.green), np.max(self.blue)]) + min = np.min([np.min(self.red), np.min(self.green), np.min(self.blue)]) + return (max - min) / max + + def IF(self): + """ + Shape Index + https://www.indexdatabase.de/db/i-single.php?id=79 + :return: index + """ + return (2 * self.red - self.green - self.blue) / (self.green - self.blue) + + def DVI(self): + """ + Simple Ratio self.nir/self.red Difference Vegetation Index, Vegetation Index + Number (VIN) + https://www.indexdatabase.de/db/i-single.php?id=12 + :return: index + """ + return self.nir / self.red + + def TVI(self): + """ + Transformed Vegetation Index + https://www.indexdatabase.de/db/i-single.php?id=98 + :return: index + """ + return (self.NDVI() + 0.5) ** (1 / 2) + + def NDRE(self): + return (self.nir - self.redEdge) / (self.nir + self.redEdge) + + +""" +# genering a random matrices to test this class +red = np.ones((1000,1000, 1),dtype="float64") * 46787 +green = np.ones((1000,1000, 1),dtype="float64") * 23487 +blue = np.ones((1000,1000, 1),dtype="float64") * 14578 +redEdge = np.ones((1000,1000, 1),dtype="float64") * 51045 +nir = np.ones((1000,1000, 1),dtype="float64") * 52200 + +# Examples of how to use the class + +# instantiating the class +cl = IndexCalculation() + +# instantiating the class with the values +#cl = indexCalculation(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + +# how set the values after instantiate the class cl, (for update the data or when don't +# instantiating the class with the values) +cl.setMatrices(red=red, green=green, blue=blue, redEdge=redEdge, nir=nir) + +# calculating the indices for the instantiated values in the class + # Note: the CCCI index can be changed to any index implemented in the class. +indexValue_form1 = cl.calculation("CCCI", red=red, green=green, blue=blue, + redEdge=redEdge, nir=nir).astype(np.float64) +indexValue_form2 = cl.CCCI() + +# calculating the index with the values directly -- you can set just the values +# preferred note: the *calculation* function performs the function *setMatrices* +indexValue_form3 = cl.calculation("CCCI", red=red, green=green, blue=blue, + redEdge=redEdge, nir=nir).astype(np.float64) + +print("Form 1: "+np.array2string(indexValue_form1, precision=20, separator=', ', floatmode='maxprec_equal')) +print("Form 2: "+np.array2string(indexValue_form2, precision=20, separator=', ', floatmode='maxprec_equal')) +print("Form 3: "+np.array2string(indexValue_form3, precision=20, separator=', ', floatmode='maxprec_equal')) + +# A list of examples results for different type of data at NDVI +# float16 -> 0.31567383 #NDVI (red = 50, nir = 100) +# float32 -> 0.31578946 #NDVI (red = 50, nir = 100) +# float64 -> 0.3157894736842105 #NDVI (red = 50, nir = 100) +# longdouble -> 0.3157894736842105 #NDVI (red = 50, nir = 100) +""" diff --git a/digital_image_processing/rotation/__init__.py b/digital_image_processing/rotation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/rotation/rotation.py b/digital_image_processing/rotation/rotation.py new file mode 100644 index 000000000000..37b45ca39897 --- /dev/null +++ b/digital_image_processing/rotation/rotation.py @@ -0,0 +1,52 @@ +from matplotlib import pyplot as plt +import numpy as np +import cv2 + + +def get_rotation( + img: np.array, pt1: np.float32, pt2: np.float32, rows: int, cols: int +) -> np.array: + """ + Get image rotation + :param img: np.array + :param pt1: 3x2 list + :param pt2: 3x2 list + :param rows: columns image shape + :param cols: rows image shape + :return: np.array + """ + matrix = cv2.getAffineTransform(pt1, pt2) + return cv2.warpAffine(img, matrix, (rows, cols)) + + +if __name__ == "__main__": + # read original image + image = cv2.imread("lena.jpg") + # turn image in gray scale value + gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + # get image shape + img_rows, img_cols = gray_img.shape + + # set different points to rotate image + pts1 = np.float32([[50, 50], [200, 50], [50, 200]]) + pts2 = np.float32([[10, 100], [200, 50], [100, 250]]) + pts3 = np.float32([[50, 50], [150, 50], [120, 200]]) + pts4 = np.float32([[10, 100], [80, 50], [180, 250]]) + + # add all rotated images in a list + images = [ + gray_img, + get_rotation(gray_img, pts1, pts2, img_rows, img_cols), + get_rotation(gray_img, pts2, pts3, img_rows, img_cols), + get_rotation(gray_img, pts2, pts4, img_rows, img_cols), + ] + + # plot different image rotations + fig = plt.figure(1) + titles = ["Original", "Rotation 1", "Rotation 2", "Rotation 3"] + for i, image in enumerate(images): + plt.subplot(2, 2, i + 1), plt.imshow(image, "gray") + plt.title(titles[i]) + plt.axis("off") + plt.subplots_adjust(left=0.0, bottom=0.05, right=1.0, top=0.95) + plt.show() diff --git a/digital_image_processing/sepia.py b/digital_image_processing/sepia.py new file mode 100644 index 000000000000..e91d57d0379c --- /dev/null +++ b/digital_image_processing/sepia.py @@ -0,0 +1,48 @@ +""" + Implemented an algorithm using opencv to tone an image with sepia technique +""" + +from cv2 import imread, imshow, waitKey, destroyAllWindows + + +def make_sepia(img, factor: int): + """ Function create sepia tone. Source: https://en.wikipedia.org/wiki/Sepia_(color) """ + pixel_h, pixel_v = img.shape[0], img.shape[1] + + def to_grayscale(blue, green, red): + """ + Helper function to create pixel's greyscale representation + Src: https://pl.wikipedia.org/wiki/YUV + """ + return 0.2126 * red + 0.587 * green + 0.114 * blue + + def normalize(value): + """ Helper function to normalize R/G/B value -> return 255 if value > 255""" + return min(value, 255) + + for i in range(pixel_h): + for j in range(pixel_v): + greyscale = int(to_grayscale(*img[i][j])) + img[i][j] = [ + normalize(greyscale), + normalize(greyscale + factor), + normalize(greyscale + 2 * factor), + ] + + return img + + +if __name__ == "__main__": + # read original image + images = { + percentage: imread("image_data/lena.jpg", 1) for percentage in (10, 20, 30, 40) + } + + for percentage, img in images.items(): + make_sepia(img, percentage) + + for percentage, img in images.items(): + imshow(f"Original image with sepia (factor: {percentage})", img) + + waitKey(0) + destroyAllWindows() diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py new file mode 100644 index 000000000000..89cf6007af60 --- /dev/null +++ b/digital_image_processing/test_digital_image_processing.py @@ -0,0 +1,84 @@ +""" +PyTest's for Digital Image Processing +""" + +import digital_image_processing.edge_detection.canny as canny +import digital_image_processing.filters.gaussian_filter as gg +import digital_image_processing.filters.median_filter as med +import digital_image_processing.filters.sobel_filter as sob +import digital_image_processing.filters.convolve as conv +import digital_image_processing.change_contrast as cc +import digital_image_processing.convert_to_negative as cn +import digital_image_processing.sepia as sp +import digital_image_processing.dithering.burkes as bs +from cv2 import imread, cvtColor, COLOR_BGR2GRAY +from numpy import array, uint8 +from PIL import Image + +img = imread(r"digital_image_processing/image_data/lena_small.jpg") +gray = cvtColor(img, COLOR_BGR2GRAY) + + +# Test: convert_to_negative() +def test_convert_to_negative(): + negative_img = cn.convert_to_negative(img) + # assert negative_img array for at least one True + assert negative_img.any() + + +# Test: change_contrast() +def test_change_contrast(): + with Image.open("digital_image_processing/image_data/lena_small.jpg") as img: + # Work around assertion for response + assert str(cc.change_contrast(img, 110)).startswith( + " Divide and conquer -The points are sorted based on Xco-ords and +Approach used -> Divide and conquer +The points are sorted based on Xco-ords and then based on Yco-ords separately. -And by applying divide and conquer approach, +And by applying divide and conquer approach, minimum distance is obtained recursively. >> Closest points can lie on different sides of partition. -This case handled by forming a strip of points +This case handled by forming a strip of points whose Xco-ords distance is less than closest_pair_dis -from mid-point's Xco-ords. Points sorted based on Yco-ords +from mid-point's Xco-ords. Points sorted based on Yco-ords are used in this step to reduce sorting time. Closest pair distance is found in the strip of points. (closest_in_strip) min(closest_pair_dis, closest_in_strip) would be the final answer. - + Time complexity: O(n * log n) """ def euclidean_distance_sqr(point1, point2): + """ + >>> euclidean_distance_sqr([1,2],[2,4]) + 5 + """ return (point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2 -def column_based_sort(array, column = 0): - return sorted(array, key = lambda x: x[column]) - +def column_based_sort(array, column=0): + """ + >>> column_based_sort([(5, 1), (4, 2), (3, 0)], 1) + [(3, 0), (5, 1), (4, 2)] + """ + return sorted(array, key=lambda x: x[column]) + + +def dis_between_closest_pair(points, points_counts, min_dis=float("inf")): + """ + brute force approach to find distance between closest pair points -def dis_between_closest_pair(points, points_counts, min_dis = float("inf")): - """ brute force approach to find distance between closest pair points + Parameters : + points, points_count, min_dis (list(tuple(int, int)), int, int) - Parameters : - points, points_count, min_dis (list(tuple(int, int)), int, int) - - Returns : + Returns : min_dis (float): distance between closest pair of points + >>> dis_between_closest_pair([[1,2],[2,4],[5,7],[8,9],[11,0]],5) + 5 + """ for i in range(points_counts - 1): - for j in range(i+1, points_counts): + for j in range(i + 1, points_counts): current_dis = euclidean_distance_sqr(points[i], points[j]) if current_dis < min_dis: min_dis = current_dis return min_dis -def dis_between_closest_in_strip(points, points_counts, min_dis = float("inf")): - """ closest pair of points in strip +def dis_between_closest_in_strip(points, points_counts, min_dis=float("inf")): + """ + closest pair of points in strip + + Parameters : + points, points_count, min_dis (list(tuple(int, int)), int, int) - Parameters : - points, points_count, min_dis (list(tuple(int, int)), int, int) - - Returns : + Returns : min_dis (float): distance btw closest pair of points in the strip (< min_dis) + >>> dis_between_closest_in_strip([[1,2],[2,4],[5,7],[8,9],[11,0]],5) + 85 """ for i in range(min(6, points_counts - 1), points_counts): - for j in range(max(0, i-6), i): + for j in range(max(0, i - 6), i): current_dis = euclidean_distance_sqr(points[i], points[j]) if current_dis < min_dis: min_dis = current_dis @@ -69,29 +84,32 @@ def dis_between_closest_in_strip(points, points_counts, min_dis = float("inf")): def closest_pair_of_points_sqr(points_sorted_on_x, points_sorted_on_y, points_counts): """ divide and conquer approach - Parameters : - points, points_count (list(tuple(int, int)), int) - - Returns : - (float): distance btw closest pair of points + Parameters : + points, points_count (list(tuple(int, int)), int) + + Returns : + (float): distance btw closest pair of points + >>> closest_pair_of_points_sqr([(1, 2), (3, 4)], [(5, 6), (7, 8)], 2) + 8 """ # base case if points_counts <= 3: return dis_between_closest_pair(points_sorted_on_x, points_counts) - + # recursion - mid = points_counts//2 - closest_in_left = closest_pair_of_points_sqr(points_sorted_on_x, - points_sorted_on_y[:mid], - mid) - closest_in_right = closest_pair_of_points_sqr(points_sorted_on_y, - points_sorted_on_y[mid:], - points_counts - mid) + mid = points_counts // 2 + closest_in_left = closest_pair_of_points_sqr( + points_sorted_on_x, points_sorted_on_y[:mid], mid + ) + closest_in_right = closest_pair_of_points_sqr( + points_sorted_on_y, points_sorted_on_y[mid:], points_counts - mid + ) closest_pair_dis = min(closest_in_left, closest_in_right) - - """ cross_strip contains the points, whose Xcoords are at a + + """ + cross_strip contains the points, whose Xcoords are at a distance(< closest_pair_dis) from mid's Xcoord """ @@ -100,21 +118,26 @@ def closest_pair_of_points_sqr(points_sorted_on_x, points_sorted_on_y, points_co if abs(point[0] - points_sorted_on_x[mid][0]) < closest_pair_dis: cross_strip.append(point) - closest_in_strip = dis_between_closest_in_strip(cross_strip, - len(cross_strip), closest_pair_dis) + closest_in_strip = dis_between_closest_in_strip( + cross_strip, len(cross_strip), closest_pair_dis + ) return min(closest_pair_dis, closest_in_strip) - + def closest_pair_of_points(points, points_counts): - points_sorted_on_x = column_based_sort(points, column = 0) - points_sorted_on_y = column_based_sort(points, column = 1) - return (closest_pair_of_points_sqr(points_sorted_on_x, - points_sorted_on_y, - points_counts)) ** 0.5 + """ + >>> closest_pair_of_points([(2, 3), (12, 30)], len([(2, 3), (12, 30)])) + 28.792360097775937 + """ + points_sorted_on_x = column_based_sort(points, column=0) + points_sorted_on_y = column_based_sort(points, column=1) + return ( + closest_pair_of_points_sqr( + points_sorted_on_x, points_sorted_on_y, points_counts + ) + ) ** 0.5 if __name__ == "__main__": - points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)] + points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)] print("Distance:", closest_pair_of_points(points, len(points))) - - diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py new file mode 100644 index 000000000000..11b16975c8b4 --- /dev/null +++ b/divide_and_conquer/convex_hull.py @@ -0,0 +1,427 @@ +""" +The convex hull problem is problem of finding all the vertices of convex polygon, P of +a set of points in a plane such that all the points are either on the vertices of P or +inside P. TH convex hull problem has several applications in geometrical problems, +computer graphics and game development. + +Two algorithms have been implemented for the convex hull problem here. +1. A brute-force algorithm which runs in O(n^3) +2. A divide-and-conquer algorithm which runs in O(n log(n)) + +There are other several other algorithms for the convex hull problem +which have not been implemented here, yet. + +""" + + +class Point: + """ + Defines a 2-d point for use by all convex-hull algorithms. + + Parameters + ---------- + x: an int or a float, the x-coordinate of the 2-d point + y: an int or a float, the y-coordinate of the 2-d point + + Examples + -------- + >>> Point(1, 2) + (1.0, 2.0) + >>> Point("1", "2") + (1.0, 2.0) + >>> Point(1, 2) > Point(0, 1) + True + >>> Point(1, 1) == Point(1, 1) + True + >>> Point(-0.5, 1) == Point(0.5, 1) + False + >>> Point("pi", "e") + Traceback (most recent call last): + ... + ValueError: could not convert string to float: 'pi' + """ + + def __init__(self, x, y): + self.x, self.y = float(x), float(y) + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + def __ne__(self, other): + return not self == other + + def __gt__(self, other): + if self.x > other.x: + return True + elif self.x == other.x: + return self.y > other.y + return False + + def __lt__(self, other): + return not self > other + + def __ge__(self, other): + if self.x > other.x: + return True + elif self.x == other.x: + return self.y >= other.y + return False + + def __le__(self, other): + if self.x < other.x: + return True + elif self.x == other.x: + return self.y <= other.y + return False + + def __repr__(self): + return f"({self.x}, {self.y})" + + def __hash__(self): + return hash(self.x) + + +def _construct_points(list_of_tuples): + """ + constructs a list of points from an array-like object of numbers + + Arguments + --------- + + list_of_tuples: array-like object of type numbers. Acceptable types so far + are lists, tuples and sets. + + Returns + -------- + points: a list where each item is of type Point. This contains only objects + which can be converted into a Point. + + Examples + ------- + >>> _construct_points([[1, 1], [2, -1], [0.3, 4]]) + [(1.0, 1.0), (2.0, -1.0), (0.3, 4.0)] + >>> _construct_points([1, 2]) + Ignoring deformed point 1. All points must have at least 2 coordinates. + Ignoring deformed point 2. All points must have at least 2 coordinates. + [] + >>> _construct_points([]) + [] + >>> _construct_points(None) + [] + """ + + points = [] + if list_of_tuples: + for p in list_of_tuples: + try: + points.append(Point(p[0], p[1])) + except (IndexError, TypeError): + print( + f"Ignoring deformed point {p}. All points" + " must have at least 2 coordinates." + ) + return points + + +def _validate_input(points): + """ + validates an input instance before a convex-hull algorithms uses it + + Parameters + --------- + points: array-like, the 2d points to validate before using with + a convex-hull algorithm. The elements of points must be either lists, tuples or + Points. + + Returns + ------- + points: array_like, an iterable of all well-defined Points constructed passed in. + + + Exception + --------- + ValueError: if points is empty or None, or if a wrong data structure like a scalar is passed + + TypeError: if an iterable but non-indexable object (eg. dictionary) is passed. + The exception to this a set which we'll convert to a list before using + + + Examples + ------- + >>> _validate_input([[1, 2]]) + [(1.0, 2.0)] + >>> _validate_input([(1, 2)]) + [(1.0, 2.0)] + >>> _validate_input([Point(2, 1), Point(-1, 2)]) + [(2.0, 1.0), (-1.0, 2.0)] + >>> _validate_input([]) + Traceback (most recent call last): + ... + ValueError: Expecting a list of points but got [] + >>> _validate_input(1) + Traceback (most recent call last): + ... + ValueError: Expecting an iterable object but got an non-iterable type 1 + """ + + if not points: + raise ValueError(f"Expecting a list of points but got {points}") + + if isinstance(points, set): + points = list(points) + + try: + if hasattr(points, "__iter__") and not isinstance(points[0], Point): + if isinstance(points[0], (list, tuple)): + points = _construct_points(points) + else: + raise ValueError( + "Expecting an iterable of type Point, list or tuple. " + f"Found objects of type {type(points[0])} instead" + ) + elif not hasattr(points, "__iter__"): + raise ValueError( + f"Expecting an iterable object but got an non-iterable type {points}" + ) + except TypeError: + print("Expecting an iterable of type Point, list or tuple.") + raise + + return points + + +def _det(a, b, c): + """ + Computes the sign perpendicular distance of a 2d point c from a line segment + ab. The sign indicates the direction of c relative to ab. + A Positive value means c is above ab (to the left), while a negative value + means c is below ab (to the right). 0 means all three points are on a straight line. + + As a side note, 0.5 * abs|det| is the area of triangle abc + + Parameters + ---------- + a: point, the point on the left end of line segment ab + b: point, the point on the right end of line segment ab + c: point, the point for which the direction and location is desired. + + Returns + -------- + det: float, abs(det) is the distance of c from ab. The sign + indicates which side of line segment ab c is. det is computed as + (a_xb_y + c_xa_y + b_xc_y) - (a_yb_x + c_ya_x + b_yc_x) + + Examples + ---------- + >>> _det(Point(1, 1), Point(1, 2), Point(1, 5)) + 0.0 + >>> _det(Point(0, 0), Point(10, 0), Point(0, 10)) + 100.0 + >>> _det(Point(0, 0), Point(10, 0), Point(0, -10)) + -100.0 + """ + + det = (a.x * b.y + b.x * c.y + c.x * a.y) - (a.y * b.x + b.y * c.x + c.y * a.x) + return det + + +def convex_hull_bf(points): + """ + Constructs the convex hull of a set of 2D points using a brute force algorithm. + The algorithm basically considers all combinations of points (i, j) and uses the + definition of convexity to determine whether (i, j) is part of the convex hull or not. + (i, j) is part of the convex hull if and only iff there are no points on both sides + of the line segment connecting the ij, and there is no point k such that k is on either end + of the ij. + + Runtime: O(n^3) - definitely horrible + + Parameters + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Returns + ------ + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + See Also + -------- + convex_hull_recursive, + + Examples + --------- + >>> convex_hull_bf([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_bf([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_bf([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_bf([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + """ + + points = sorted(_validate_input(points)) + n = len(points) + convex_set = set() + + for i in range(n - 1): + for j in range(i + 1, n): + points_left_of_ij = points_right_of_ij = False + ij_part_of_convex_hull = True + for k in range(n): + if k != i and k != j: + det_k = _det(points[i], points[j], points[k]) + + if det_k > 0: + points_left_of_ij = True + elif det_k < 0: + points_right_of_ij = True + else: + # point[i], point[j], point[k] all lie on a straight line + # if point[k] is to the left of point[i] or it's to the + # right of point[j], then point[i], point[j] cannot be + # part of the convex hull of A + if points[k] < points[i] or points[k] > points[j]: + ij_part_of_convex_hull = False + break + + if points_left_of_ij and points_right_of_ij: + ij_part_of_convex_hull = False + break + + if ij_part_of_convex_hull: + convex_set.update([points[i], points[j]]) + + return sorted(convex_set) + + +def convex_hull_recursive(points): + """ + Constructs the convex hull of a set of 2D points using a divide-and-conquer strategy + The algorithm exploits the geometric properties of the problem by repeatedly partitioning + the set of points into smaller hulls, and finding the convex hull of these smaller hulls. + The union of the convex hull from smaller hulls is the solution to the convex hull of the larger problem. + + Parameter + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Runtime: O(n log n) + + Returns + ------- + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + Examples + --------- + >>> convex_hull_recursive([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_recursive([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_recursive([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_recursive([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + + """ + points = sorted(_validate_input(points)) + n = len(points) + + # divide all the points into an upper hull and a lower hull + # the left most point and the right most point are definitely + # members of the convex hull by definition. + # use these two anchors to divide all the points into two hulls, + # an upper hull and a lower hull. + + # all points to the left (above) the line joining the extreme points belong to the upper hull + # all points to the right (below) the line joining the extreme points below to the lower hull + # ignore all points on the line joining the extreme points since they cannot be part of the + # convex hull + + left_most_point = points[0] + right_most_point = points[n - 1] + + convex_set = {left_most_point, right_most_point} + upper_hull = [] + lower_hull = [] + + for i in range(1, n - 1): + det = _det(left_most_point, right_most_point, points[i]) + + if det > 0: + upper_hull.append(points[i]) + elif det < 0: + lower_hull.append(points[i]) + + _construct_hull(upper_hull, left_most_point, right_most_point, convex_set) + _construct_hull(lower_hull, right_most_point, left_most_point, convex_set) + + return sorted(convex_set) + + +def _construct_hull(points, left, right, convex_set): + """ + + Parameters + --------- + points: list or None, the hull of points from which to choose the next convex-hull point + left: Point, the point to the left of line segment joining left and right + right: The point to the right of the line segment joining left and right + convex_set: set, the current convex-hull. The state of convex-set gets updated by this function + + Note + ---- + For the line segment 'ab', 'a' is on the left and 'b' on the right. + but the reverse is true for the line segment 'ba'. + + Returns + ------- + Nothing, only updates the state of convex-set + """ + if points: + extreme_point = None + extreme_point_distance = float("-inf") + candidate_points = [] + + for p in points: + det = _det(left, right, p) + + if det > 0: + candidate_points.append(p) + + if det > extreme_point_distance: + extreme_point_distance = det + extreme_point = p + + if extreme_point: + _construct_hull(candidate_points, left, extreme_point, convex_set) + convex_set.add(extreme_point) + _construct_hull(candidate_points, extreme_point, right, convex_set) + + +def main(): + points = [ + (0, 3), + (2, 2), + (1, 1), + (2, 1), + (3, 0), + (0, 0), + (3, 3), + (2, -1), + (2, -4), + (1, -3), + ] + # the convex set of points is + # [(0, 0), (0, 3), (1, -3), (2, -4), (3, 0), (3, 3)] + results_recursive = convex_hull_recursive(points) + results_bf = convex_hull_bf(points) + assert results_bf == results_recursive + + print(results_bf) + + +if __name__ == "__main__": + main() diff --git a/divide_and_conquer/inversions.py b/divide_and_conquer/inversions.py new file mode 100644 index 000000000000..9bb656229321 --- /dev/null +++ b/divide_and_conquer/inversions.py @@ -0,0 +1,169 @@ +""" +Given an array-like data structure A[1..n], how many pairs +(i, j) for all 1 <= i < j <= n such that A[i] > A[j]? These pairs are +called inversions. Counting the number of such inversions in an array-like +object is the important. Among other things, counting inversions can help +us determine how close a given array is to being sorted + +In this implementation, I provide two algorithms, a divide-and-conquer +algorithm which runs in nlogn and the brute-force n^2 algorithm. + +""" + + +def count_inversions_bf(arr): + """ + Counts the number of inversions using a a naive brute-force algorithm + + Parameters + ---------- + arr: arr: array-like, the list containing the items for which the number + of inversions is desired. The elements of `arr` must be comparable. + + Returns + ------- + num_inversions: The total number of inversions in `arr` + + Examples + --------- + + >>> count_inversions_bf([1, 4, 2, 4, 1]) + 4 + >>> count_inversions_bf([1, 1, 2, 4, 4]) + 0 + >>> count_inversions_bf([]) + 0 + """ + + num_inversions = 0 + n = len(arr) + + for i in range(n - 1): + for j in range(i + 1, n): + if arr[i] > arr[j]: + num_inversions += 1 + + return num_inversions + + +def count_inversions_recursive(arr): + """ + Counts the number of inversions using a divide-and-conquer algorithm + + Parameters + ----------- + arr: array-like, the list containing the items for which the number + of inversions is desired. The elements of `arr` must be comparable. + + Returns + ------- + C: a sorted copy of `arr`. + num_inversions: int, the total number of inversions in 'arr' + + Examples + -------- + + >>> count_inversions_recursive([1, 4, 2, 4, 1]) + ([1, 1, 2, 4, 4], 4) + >>> count_inversions_recursive([1, 1, 2, 4, 4]) + ([1, 1, 2, 4, 4], 0) + >>> count_inversions_recursive([]) + ([], 0) + """ + if len(arr) <= 1: + return arr, 0 + else: + mid = len(arr) // 2 + P = arr[0:mid] + Q = arr[mid:] + + A, inversion_p = count_inversions_recursive(P) + B, inversions_q = count_inversions_recursive(Q) + C, cross_inversions = _count_cross_inversions(A, B) + + num_inversions = inversion_p + inversions_q + cross_inversions + return C, num_inversions + + +def _count_cross_inversions(P, Q): + """ + Counts the inversions across two sorted arrays. + And combine the two arrays into one sorted array + + For all 1<= i<=len(P) and for all 1 <= j <= len(Q), + if P[i] > Q[j], then (i, j) is a cross inversion + + Parameters + ---------- + P: array-like, sorted in non-decreasing order + Q: array-like, sorted in non-decreasing order + + Returns + ------ + R: array-like, a sorted array of the elements of `P` and `Q` + num_inversion: int, the number of inversions across `P` and `Q` + + Examples + -------- + + >>> _count_cross_inversions([1, 2, 3], [0, 2, 5]) + ([0, 1, 2, 2, 3, 5], 4) + >>> _count_cross_inversions([1, 2, 3], [3, 4, 5]) + ([1, 2, 3, 3, 4, 5], 0) + """ + + R = [] + i = j = num_inversion = 0 + while i < len(P) and j < len(Q): + if P[i] > Q[j]: + # if P[1] > Q[j], then P[k] > Q[k] for all i < k <= len(P) + # These are all inversions. The claim emerges from the + # property that P is sorted. + num_inversion += len(P) - i + R.append(Q[j]) + j += 1 + else: + R.append(P[i]) + i += 1 + + if i < len(P): + R.extend(P[i:]) + else: + R.extend(Q[j:]) + + return R, num_inversion + + +def main(): + arr_1 = [10, 2, 1, 5, 5, 2, 11] + + # this arr has 8 inversions: + # (10, 2), (10, 1), (10, 5), (10, 5), (10, 2), (2, 1), (5, 2), (5, 2) + + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 8 + + print("number of inversions = ", num_inversions_bf) + + # testing an array with zero inversion (a sorted arr_1) + + arr_1.sort() + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 0 + print("number of inversions = ", num_inversions_bf) + + # an empty list should also have zero inversions + arr_1 = [] + num_inversions_bf = count_inversions_bf(arr_1) + _, num_inversions_recursive = count_inversions_recursive(arr_1) + + assert num_inversions_bf == num_inversions_recursive == 0 + print("number of inversions = ", num_inversions_bf) + + +if __name__ == "__main__": + main() diff --git a/divide_and_conquer/max_subarray_sum.py b/divide_and_conquer/max_subarray_sum.py index 0428f4e13768..9e81c83649a6 100644 --- a/divide_and_conquer/max_subarray_sum.py +++ b/divide_and_conquer/max_subarray_sum.py @@ -40,8 +40,8 @@ def max_cross_array_sum(array, left, mid, right): """ - max_sum_of_left = max_sum_from_start(array[left:mid+1][::-1]) - max_sum_of_right = max_sum_from_start(array[mid+1: right+1]) + max_sum_of_left = max_sum_from_start(array[left : mid + 1][::-1]) + max_sum_of_right = max_sum_from_start(array[mid + 1 : right + 1]) return max_sum_of_left + max_sum_of_right @@ -60,7 +60,7 @@ def max_subarray_sum(array, left, right): # base case: array has only one element if left == right: return array[right] - + # Recursion mid = (left + right) // 2 left_half_sum = max_subarray_sum(array, left, mid) @@ -71,5 +71,6 @@ def max_subarray_sum(array, left, right): array = [-2, -5, 6, -2, -3, 1, 5, -6] array_length = len(array) -print("Maximum sum of contiguous subarray:", max_subarray_sum(array, 0, array_length - 1)) - +print( + "Maximum sum of contiguous subarray:", max_subarray_sum(array, 0, array_length - 1) +) diff --git a/divide_and_conquer/mergesort.py b/divide_and_conquer/mergesort.py new file mode 100644 index 000000000000..d6693eb36a0a --- /dev/null +++ b/divide_and_conquer/mergesort.py @@ -0,0 +1,48 @@ +def merge(a, b, m, e): + l = a[b : m + 1] + r = a[m + 1 : e + 1] + k = b + i = 0 + j = 0 + while i < len(l) and j < len(r): + # change sign for Descending order + if l[i] < r[j]: + a[k] = l[i] + i += 1 + else: + a[k] = r[j] + j += 1 + k += 1 + while i < len(l): + a[k] = l[i] + i += 1 + k += 1 + while j < len(r): + a[k] = r[j] + j += 1 + k += 1 + return a + + +def mergesort(a, b, e): + """ + >>> mergesort([3,2,1],0,2) + [1, 2, 3] + >>> mergesort([3,2,1,0,1,2,3,5,4],0,8) + [0, 1, 1, 2, 2, 3, 3, 4, 5] + """ + if b < e: + m = (b + e) // 2 + # print("ms1",a,b,m) + mergesort(a, b, m) + # print("ms2",a,m+1,e) + mergesort(a, m + 1, e) + # print("m",a,b,m,e) + merge(a, b, m, e) + return a + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/divide_and_conquer/power.py b/divide_and_conquer/power.py new file mode 100644 index 000000000000..f2e023afd536 --- /dev/null +++ b/divide_and_conquer/power.py @@ -0,0 +1,33 @@ +def actual_power(a: int, b: int): + """ + Function using divide and conquer to calculate a^b. + It only works for integer a,b. + """ + if b == 0: + return 1 + if (b % 2) == 0: + return actual_power(a, int(b / 2)) * actual_power(a, int(b / 2)) + else: + return a * actual_power(a, int(b / 2)) * actual_power(a, int(b / 2)) + + +def power(a: int, b: int) -> float: + """ + >>> power(4,6) + 4096 + >>> power(2,3) + 8 + >>> power(-2,3) + -8 + >>> power(2,-3) + 0.125 + >>> power(-2,-3) + -0.125 + """ + if b < 0: + return 1 / actual_power(a, b) + return actual_power(a, b) + + +if __name__ == "__main__": + print(power(-2, -3)) diff --git a/divide_and_conquer/strassen_matrix_multiplication.py b/divide_and_conquer/strassen_matrix_multiplication.py new file mode 100644 index 000000000000..ea54b0f52d29 --- /dev/null +++ b/divide_and_conquer/strassen_matrix_multiplication.py @@ -0,0 +1,170 @@ +import math +from typing import List, Tuple + + +def default_matrix_multiplication(a: List, b: List) -> List: + """ + Multiplication only for 2x2 matrices + """ + if len(a) != 2 or len(a[0]) != 2 or len(b) != 2 or len(b[0]) != 2: + raise Exception("Matrices are not 2x2") + new_matrix = [ + [a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]], + [a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]], + ] + return new_matrix + + +def matrix_addition(matrix_a: List, matrix_b: List): + return [ + [matrix_a[row][col] + matrix_b[row][col] for col in range(len(matrix_a[row]))] + for row in range(len(matrix_a)) + ] + + +def matrix_subtraction(matrix_a: List, matrix_b: List): + return [ + [matrix_a[row][col] - matrix_b[row][col] for col in range(len(matrix_a[row]))] + for row in range(len(matrix_a)) + ] + + +def split_matrix(a: List,) -> Tuple[List, List, List, List]: + """ + Given an even length matrix, returns the top_left, top_right, bot_left, bot_right + quadrant. + + >>> split_matrix([[4,3,2,4],[2,3,1,1],[6,5,4,3],[8,4,1,6]]) + ([[4, 3], [2, 3]], [[2, 4], [1, 1]], [[6, 5], [8, 4]], [[4, 3], [1, 6]]) + >>> split_matrix([ + ... [4,3,2,4,4,3,2,4],[2,3,1,1,2,3,1,1],[6,5,4,3,6,5,4,3],[8,4,1,6,8,4,1,6], + ... [4,3,2,4,4,3,2,4],[2,3,1,1,2,3,1,1],[6,5,4,3,6,5,4,3],[8,4,1,6,8,4,1,6] + ... ]) # doctest: +NORMALIZE_WHITESPACE + ([[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], + [2, 3, 1, 1], [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], [2, 3, 1, 1], + [6, 5, 4, 3], [8, 4, 1, 6]], [[4, 3, 2, 4], [2, 3, 1, 1], [6, 5, 4, 3], + [8, 4, 1, 6]]) + """ + if len(a) % 2 != 0 or len(a[0]) % 2 != 0: + raise Exception("Odd matrices are not supported!") + + matrix_length = len(a) + mid = matrix_length // 2 + + top_right = [[a[i][j] for j in range(mid, matrix_length)] for i in range(mid)] + bot_right = [ + [a[i][j] for j in range(mid, matrix_length)] for i in range(mid, matrix_length) + ] + + top_left = [[a[i][j] for j in range(mid)] for i in range(mid)] + bot_left = [[a[i][j] for j in range(mid)] for i in range(mid, matrix_length)] + + return top_left, top_right, bot_left, bot_right + + +def matrix_dimensions(matrix: List) -> Tuple[int, int]: + return len(matrix), len(matrix[0]) + + +def print_matrix(matrix: List) -> None: + for i in range(len(matrix)): + print(matrix[i]) + + +def actual_strassen(matrix_a: List, matrix_b: List) -> List: + """ + Recursive function to calculate the product of two matrices, using the Strassen + Algorithm. It only supports even length matrices. + """ + if matrix_dimensions(matrix_a) == (2, 2): + return default_matrix_multiplication(matrix_a, matrix_b) + + a, b, c, d = split_matrix(matrix_a) + e, f, g, h = split_matrix(matrix_b) + + t1 = actual_strassen(a, matrix_subtraction(f, h)) + t2 = actual_strassen(matrix_addition(a, b), h) + t3 = actual_strassen(matrix_addition(c, d), e) + t4 = actual_strassen(d, matrix_subtraction(g, e)) + t5 = actual_strassen(matrix_addition(a, d), matrix_addition(e, h)) + t6 = actual_strassen(matrix_subtraction(b, d), matrix_addition(g, h)) + t7 = actual_strassen(matrix_subtraction(a, c), matrix_addition(e, f)) + + top_left = matrix_addition(matrix_subtraction(matrix_addition(t5, t4), t2), t6) + top_right = matrix_addition(t1, t2) + bot_left = matrix_addition(t3, t4) + bot_right = matrix_subtraction(matrix_subtraction(matrix_addition(t1, t5), t3), t7) + + # construct the new matrix from our 4 quadrants + new_matrix = [] + for i in range(len(top_right)): + new_matrix.append(top_left[i] + top_right[i]) + for i in range(len(bot_right)): + new_matrix.append(bot_left[i] + bot_right[i]) + return new_matrix + + +def strassen(matrix1: List, matrix2: List) -> List: + """ + >>> strassen([[2,1,3],[3,4,6],[1,4,2],[7,6,7]], [[4,2,3,4],[2,1,1,1],[8,6,4,2]]) + [[34, 23, 19, 15], [68, 46, 37, 28], [28, 18, 15, 12], [96, 62, 55, 48]] + >>> strassen([[3,7,5,6,9],[1,5,3,7,8],[1,4,4,5,7]], [[2,4],[5,2],[1,7],[5,5],[7,8]]) + [[139, 163], [121, 134], [100, 121]] + """ + if matrix_dimensions(matrix1)[1] != matrix_dimensions(matrix2)[0]: + raise Exception( + f"Unable to multiply these matrices, please check the dimensions. \n" + f"Matrix A:{matrix1} \nMatrix B:{matrix2}" + ) + dimension1 = matrix_dimensions(matrix1) + dimension2 = matrix_dimensions(matrix2) + + if dimension1[0] == dimension1[1] and dimension2[0] == dimension2[1]: + return matrix1, matrix2 + + maximum = max(max(dimension1), max(dimension2)) + maxim = int(math.pow(2, math.ceil(math.log2(maximum)))) + new_matrix1 = matrix1 + new_matrix2 = matrix2 + + # Adding zeros to the matrices so that the arrays dimensions are the same and also + # power of 2 + for i in range(0, maxim): + if i < dimension1[0]: + for j in range(dimension1[1], maxim): + new_matrix1[i].append(0) + else: + new_matrix1.append([0] * maxim) + if i < dimension2[0]: + for j in range(dimension2[1], maxim): + new_matrix2[i].append(0) + else: + new_matrix2.append([0] * maxim) + + final_matrix = actual_strassen(new_matrix1, new_matrix2) + + # Removing the additional zeros + for i in range(0, maxim): + if i < dimension1[0]: + for j in range(dimension2[1], maxim): + final_matrix[i].pop() + else: + final_matrix.pop() + return final_matrix + + +if __name__ == "__main__": + matrix1 = [ + [2, 3, 4, 5], + [6, 4, 3, 1], + [2, 3, 6, 7], + [3, 1, 2, 4], + [2, 3, 4, 5], + [6, 4, 3, 1], + [2, 3, 6, 7], + [3, 1, 2, 4], + [2, 3, 4, 5], + [6, 2, 3, 1], + ] + matrix2 = [[0, 2, 1, 1], [16, 2, 3, 3], [2, 2, 7, 7], [13, 11, 22, 4]] + print(strassen(matrix1, matrix2)) diff --git a/dynamic_programming/abbreviation.py b/dynamic_programming/abbreviation.py index f4d07e402925..5175aa9ed92f 100644 --- a/dynamic_programming/abbreviation.py +++ b/dynamic_programming/abbreviation.py @@ -12,7 +12,13 @@ """ -def abbr(a, b): +def abbr(a: str, b: str) -> bool: + """ + >>> abbr("daBcd", "ABC") + True + >>> abbr("dBcd", "ABC") + False + """ n = len(a) m = len(b) dp = [[False for _ in range(m + 1)] for _ in range(n + 1)] @@ -28,4 +34,6 @@ def abbr(a, b): if __name__ == "__main__": - print(abbr("daBcd", "ABC")) # expect True + import doctest + + doctest.testmod() diff --git a/dynamic_programming/bitmask.py b/dynamic_programming/bitmask.py index 213b22fe9051..2994db5b5e1e 100644 --- a/dynamic_programming/bitmask.py +++ b/dynamic_programming/bitmask.py @@ -1,90 +1,93 @@ """ -This is a python implementation for questions involving task assignments between people. +This is a Python implementation for questions involving task assignments between people. Here Bitmasking and DP are used for solving this. Question :- -We have N tasks and M people. Each person in M can do only certain of these tasks. Also a person can do only one task and a task is performed only by one person. +We have N tasks and M people. Each person in M can do only certain of these tasks. Also +a person can do only one task and a task is performed only by one person. Find the total no of ways in which the tasks can be distributed. - - """ -from __future__ import print_function from collections import defaultdict class AssignmentUsingBitmask: - def __init__(self,task_performed,total): - - self.total_tasks = total #total no of tasks (N) - + def __init__(self, task_performed, total): + + self.total_tasks = total # total no of tasks (N) + # DP table will have a dimension of (2^M)*N # initially all values are set to -1 - self.dp = [[-1 for i in range(total+1)] for j in range(2**len(task_performed))] - - self.task = defaultdict(list) #stores the list of persons for each task - - #finalmask is used to check if all persons are included by setting all bits to 1 - self.finalmask = (1< self.total_tasks: + # if not everyone gets the task and no more tasks are available, return 0 + if task_no > self.total_tasks: return 0 - #if case already considered - if self.dp[mask][taskno]!=-1: - return self.dp[mask][taskno] + # if case already considered + if self.dp[mask][task_no] != -1: + return self.dp[mask][task_no] + + # Number of ways when we don't this task in the arrangement + total_ways_util = self.CountWaysUtil(mask, task_no + 1) - # Number of ways when we dont this task in the arrangement - total_ways_util = self.CountWaysUtil(mask,taskno+1) + # now assign the tasks one by one to all possible persons and recursively + # assign for the remaining tasks. + if task_no in self.task: + for p in self.task[task_no]: - # now assign the tasks one by one to all possible persons and recursively assign for the remaining tasks. - if taskno in self.task: - for p in self.task[taskno]: - # if p is already given a task - if mask & (1<>> dp_count([1, 2, 3], 3, 4) + 4 + >>> dp_count([1, 2, 3], 3, 7) + 8 + >>> dp_count([2, 5, 3, 6], 4, 10) + 5 + >>> dp_count([10], 1, 99) + 0 + >>> dp_count([4, 5, 6], 3, 0) + 1 + """ # table[i] represents the number of ways to get to amount i table = [0] * (n + 1) @@ -26,6 +37,7 @@ def dp_count(S, m, n): return table[n] -if __name__ == '__main__': - print(dp_count([1, 2, 3], 3, 4)) # answer 4 - print(dp_count([2, 5, 3, 6], 4, 10)) # answer 5 +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/edit_distance.py b/dynamic_programming/edit_distance.py index b71f80d68935..56877e0c50a2 100644 --- a/dynamic_programming/edit_distance.py +++ b/dynamic_programming/edit_distance.py @@ -2,12 +2,13 @@ Author : Turfa Auliarachman Date : October 12, 2016 -This is a pure Python implementation of Dynamic Programming solution to the edit distance problem. +This is a pure Python implementation of Dynamic Programming solution to the edit +distance problem. The problem is : -Given two strings A and B. Find the minimum number of operations to string B such that A = B. The permitted operations are removal, insertion, and substitution. +Given two strings A and B. Find the minimum number of operations to string B such that +A = B. The permitted operations are removal, insertion, and substitution. """ -from __future__ import print_function class EditDistance: @@ -20,37 +21,41 @@ class EditDistance: def __init__(self): self.__prepare__() - def __prepare__(self, N = 0, M = 0): - self.dp = [[-1 for y in range(0,M)] for x in range(0,N)] + def __prepare__(self, N=0, M=0): + self.dp = [[-1 for y in range(0, M)] for x in range(0, N)] def __solveDP(self, x, y): - if (x==-1): - return y+1 - elif (y==-1): - return x+1 - elif (self.dp[x][y]>-1): + if x == -1: + return y + 1 + elif y == -1: + return x + 1 + elif self.dp[x][y] > -1: return self.dp[x][y] else: - if (self.A[x]==self.B[y]): - self.dp[x][y] = self.__solveDP(x-1,y-1) + if self.A[x] == self.B[y]: + self.dp[x][y] = self.__solveDP(x - 1, y - 1) else: - self.dp[x][y] = 1+min(self.__solveDP(x,y-1), self.__solveDP(x-1,y), self.__solveDP(x-1,y-1)) + self.dp[x][y] = 1 + min( + self.__solveDP(x, y - 1), + self.__solveDP(x - 1, y), + self.__solveDP(x - 1, y - 1), + ) return self.dp[x][y] def solve(self, A, B): - if isinstance(A,bytes): - A = A.decode('ascii') + if isinstance(A, bytes): + A = A.decode("ascii") - if isinstance(B,bytes): - B = B.decode('ascii') + if isinstance(B, bytes): + B = B.decode("ascii") self.A = str(A) self.B = str(B) self.__prepare__(len(A), len(B)) - return self.__solveDP(len(A)-1, len(B)-1) + return self.__solveDP(len(A) - 1, len(B) - 1) def min_distance_bottom_up(word1: str, word2: str) -> int: @@ -64,46 +69,37 @@ def min_distance_bottom_up(word1: str, word2: str) -> int: """ m = len(word1) n = len(word2) - dp = [[0 for _ in range(n+1) ] for _ in range(m+1)] - for i in range(m+1): - for j in range(n+1): - - if i == 0: #first string is empty + dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)] + for i in range(m + 1): + for j in range(n + 1): + + if i == 0: # first string is empty dp[i][j] = j - elif j == 0: #second string is empty - dp[i][j] = i - elif word1[i-1] == word2[j-1]: #last character of both substing is equal - dp[i][j] = dp[i-1][j-1] - else: - insert = dp[i][j-1] - delete = dp[i-1][j] - replace = dp[i-1][j-1] + elif j == 0: # second string is empty + dp[i][j] = i + elif ( + word1[i - 1] == word2[j - 1] + ): # last character of both substing is equal + dp[i][j] = dp[i - 1][j - 1] + else: + insert = dp[i][j - 1] + delete = dp[i - 1][j] + replace = dp[i - 1][j - 1] dp[i][j] = 1 + min(insert, delete, replace) return dp[m][n] -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - solver = EditDistance() - - print("****************** Testing Edit Distance DP Algorithm ******************") - print() - - print("Enter the first string: ", end="") - S1 = raw_input().strip() - - print("Enter the second string: ", end="") - S2 = raw_input().strip() - print() - print("The minimum Edit Distance is: %d" % (solver.solve(S1, S2))) - print("The minimum Edit Distance is: %d" % (min_distance_bottom_up(S1, S2))) - print() - print("*************** End of Testing Edit Distance DP Algorithm ***************") +if __name__ == "__main__": + solver = EditDistance() + print("****************** Testing Edit Distance DP Algorithm ******************") + print() + S1 = input("Enter the first string: ").strip() + S2 = input("Enter the second string: ").strip() - + print() + print("The minimum Edit Distance is: %d" % (solver.solve(S1, S2))) + print("The minimum Edit Distance is: %d" % (min_distance_bottom_up(S1, S2))) + print() + print("*************** End of Testing Edit Distance DP Algorithm ***************") diff --git a/dynamic_programming/factorial.py b/dynamic_programming/factorial.py index 7c6541ee2a74..0269014e7a18 100644 --- a/dynamic_programming/factorial.py +++ b/dynamic_programming/factorial.py @@ -1,6 +1,8 @@ -#Factorial of a number using memoization -result=[-1]*10 -result[0]=result[1]=1 +# Factorial of a number using memoization +result = [-1] * 10 +result[0] = result[1] = 1 + + def factorial(num): """ >>> factorial(7) @@ -10,19 +12,20 @@ def factorial(num): >>> [factorial(i) for i in range(5)] [1, 1, 2, 6, 24] """ - - if num<0: + + if num < 0: return "Number should not be negative." - if result[num]!=-1: + if result[num] != -1: return result[num] else: - result[num]=num*factorial(num-1) - #uncomment the following to see how recalculations are avoided - #print(result) + result[num] = num * factorial(num - 1) + # uncomment the following to see how recalculations are avoided + # print(result) return result[num] -#factorial of num -#uncomment the following to see how recalculations are avoided + +# factorial of num +# uncomment the following to see how recalculations are avoided ##result=[-1]*10 ##result[0]=result[1]=1 ##print(factorial(5)) @@ -31,4 +34,5 @@ def factorial(num): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/dynamic_programming/fast_fibonacci.py b/dynamic_programming/fast_fibonacci.py index cbc118467b3c..77094a40384b 100644 --- a/dynamic_programming/fast_fibonacci.py +++ b/dynamic_programming/fast_fibonacci.py @@ -1,11 +1,9 @@ #!/usr/bin/python -# encoding=utf8 """ This program calculates the nth Fibonacci number in O(log(n)). It's possible to calculate F(1000000) in less than a second. """ -from __future__ import print_function import sys diff --git a/dynamic_programming/fibonacci.py b/dynamic_programming/fibonacci.py index b453ce255853..45319269f5d4 100644 --- a/dynamic_programming/fibonacci.py +++ b/dynamic_programming/fibonacci.py @@ -1,11 +1,9 @@ """ This is a pure Python implementation of Dynamic Programming solution to the fibonacci sequence problem. """ -from __future__ import print_function class Fibonacci: - def __init__(self, N=None): self.fib_array = [] if N: @@ -16,34 +14,42 @@ def __init__(self, N=None): self.fib_array.append(self.fib_array[i - 1] + self.fib_array[i - 2]) elif N == 0: self.fib_array.append(0) + print(self.fib_array) def get(self, sequence_no=None): - if sequence_no != None: + """ + >>> Fibonacci(5).get(3) + [0, 1, 1, 2, 3, 5] + [0, 1, 1, 2] + >>> Fibonacci(5).get(6) + [0, 1, 1, 2, 3, 5] + Out of bound. + >>> Fibonacci(5).get(-1) + [0, 1, 1, 2, 3, 5] + [] + """ + if sequence_no is not None: if sequence_no < len(self.fib_array): - return print(self.fib_array[:sequence_no + 1]) + return print(self.fib_array[: sequence_no + 1]) else: print("Out of bound.") else: print("Please specify a value") -if __name__ == '__main__': +if __name__ == "__main__": print("\n********* Fibonacci Series Using Dynamic Programming ************\n") - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - print("\n Enter the upper limit for the fibonacci sequence: ", end="") try: - N = eval(raw_input().strip()) + N = int(input().strip()) fib = Fibonacci(N) print( - "\n********* Enter different values to get the corresponding fibonacci sequence, enter any negative number to exit. ************\n") + "\n********* Enter different values to get the corresponding fibonacci " + "sequence, enter any negative number to exit. ************\n" + ) while True: - print("Enter value: ", end=" ") try: - i = eval(raw_input().strip()) + i = int(input("Enter value: ").strip()) if i < 0: print("\n********* Good Bye!! ************\n") break @@ -52,3 +58,7 @@ def get(self, sequence_no=None): print("\nInvalid input, please try again.") except NameError: print("\n********* Invalid input, good bye!! ************\n") + + import doctest + + doctest.testmod() diff --git a/dynamic_programming/floyd_warshall.py b/dynamic_programming/floyd_warshall.py index 038499ca03b6..a4b6c6a82568 100644 --- a/dynamic_programming/floyd_warshall.py +++ b/dynamic_programming/floyd_warshall.py @@ -1,37 +1,42 @@ import math + class Graph: - - def __init__(self, N = 0): # a graph with Node 0,1,...,N-1 + def __init__(self, N=0): # a graph with Node 0,1,...,N-1 self.N = N - self.W = [[math.inf for j in range(0,N)] for i in range(0,N)] # adjacency matrix for weight - self.dp = [[math.inf for j in range(0,N)] for i in range(0,N)] # dp[i][j] stores minimum distance from i to j + self.W = [ + [math.inf for j in range(0, N)] for i in range(0, N) + ] # adjacency matrix for weight + self.dp = [ + [math.inf for j in range(0, N)] for i in range(0, N) + ] # dp[i][j] stores minimum distance from i to j def addEdge(self, u, v, w): self.dp[u][v] = w def floyd_warshall(self): - for k in range(0,self.N): - for i in range(0,self.N): - for j in range(0,self.N): + for k in range(0, self.N): + for i in range(0, self.N): + for j in range(0, self.N): self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j]) def showMin(self, u, v): return self.dp[u][v] - -if __name__ == '__main__': + + +if __name__ == "__main__": graph = Graph(5) - graph.addEdge(0,2,9) - graph.addEdge(0,4,10) - graph.addEdge(1,3,5) - graph.addEdge(2,3,7) - graph.addEdge(3,0,10) - graph.addEdge(3,1,2) - graph.addEdge(3,2,1) - graph.addEdge(3,4,6) - graph.addEdge(4,1,3) - graph.addEdge(4,2,4) - graph.addEdge(4,3,9) + graph.addEdge(0, 2, 9) + graph.addEdge(0, 4, 10) + graph.addEdge(1, 3, 5) + graph.addEdge(2, 3, 7) + graph.addEdge(3, 0, 10) + graph.addEdge(3, 1, 2) + graph.addEdge(3, 2, 1) + graph.addEdge(3, 4, 6) + graph.addEdge(4, 1, 3) + graph.addEdge(4, 2, 4) + graph.addEdge(4, 3, 9) graph.floyd_warshall() - graph.showMin(1,4) - graph.showMin(0,3) + graph.showMin(1, 4) + graph.showMin(0, 3) diff --git a/dynamic_programming/fractional_knapsack.py b/dynamic_programming/fractional_knapsack.py index 74e85b4b4708..15210146bf66 100644 --- a/dynamic_programming/fractional_knapsack.py +++ b/dynamic_programming/fractional_knapsack.py @@ -1,12 +1,27 @@ from itertools import accumulate from bisect import bisect + def fracKnapsack(vl, wt, W, n): + """ + >>> fracKnapsack([60, 100, 120], [10, 20, 30], 50, 3) + 240.0 + """ + + r = list(sorted(zip(vl, wt), key=lambda x: x[0] / x[1], reverse=True)) + vl, wt = [i[0] for i in r], [i[1] for i in r] + acc = list(accumulate(wt)) + k = bisect(acc, W) + return ( + 0 + if k == 0 + else sum(vl[:k]) + (W - acc[k - 1]) * (vl[k]) / (wt[k]) + if k != n + else sum(vl[:k]) + ) + - r = list(sorted(zip(vl,wt), key=lambda x:x[0]/x[1],reverse=True)) - vl , wt = [i[0] for i in r],[i[1] for i in r] - acc=list(accumulate(wt)) - k = bisect(acc,W) - return 0 if k == 0 else sum(vl[:k])+(W-acc[k-1])*(vl[k])/(wt[k]) if k!=n else sum(vl[:k]) +if __name__ == "__main__": + import doctest -print("%.0f"%fracKnapsack([60, 100, 120],[10, 20, 30],50,3)) + doctest.testmod() diff --git a/dynamic_programming/fractional_knapsack_2.py b/dynamic_programming/fractional_knapsack_2.py new file mode 100644 index 000000000000..eadb73c61a39 --- /dev/null +++ b/dynamic_programming/fractional_knapsack_2.py @@ -0,0 +1,60 @@ +# https://en.wikipedia.org/wiki/Continuous_knapsack_problem +# https://www.guru99.com/fractional-knapsack-problem-greedy.html +# https://medium.com/walkinthecode/greedy-algorithm-fractional-knapsack-problem-9aba1daecc93 + +from typing import List, Tuple + + +def fractional_knapsack( + value: List[int], weight: List[int], capacity: int +) -> Tuple[int, List[int]]: + """ + >>> value = [1, 3, 5, 7, 9] + >>> weight = [0.9, 0.7, 0.5, 0.3, 0.1] + >>> fractional_knapsack(value, weight, 5) + (25, [1, 1, 1, 1, 1]) + >>> fractional_knapsack(value, weight, 15) + (25, [1, 1, 1, 1, 1]) + >>> fractional_knapsack(value, weight, 25) + (25, [1, 1, 1, 1, 1]) + >>> fractional_knapsack(value, weight, 26) + (25, [1, 1, 1, 1, 1]) + >>> fractional_knapsack(value, weight, -1) + (-90.0, [0, 0, 0, 0, -10.0]) + >>> fractional_knapsack([1, 3, 5, 7], weight, 30) + (16, [1, 1, 1, 1]) + >>> fractional_knapsack(value, [0.9, 0.7, 0.5, 0.3, 0.1], 30) + (25, [1, 1, 1, 1, 1]) + >>> fractional_knapsack([], [], 30) + (0, []) + """ + index = list(range(len(value))) + ratio = [v / w for v, w in zip(value, weight)] + index.sort(key=lambda i: ratio[i], reverse=True) + + max_value = 0 + fractions = [0] * len(value) + for i in index: + if weight[i] <= capacity: + fractions[i] = 1 + max_value += value[i] + capacity -= weight[i] + else: + fractions[i] = capacity / weight[i] + max_value += value[i] * capacity / weight[i] + break + + return max_value, fractions + + +if __name__ == "__main__": + n = int(input("Enter number of items: ")) + value = input(f"Enter the values of the {n} item(s) in order: ").split() + value = [int(v) for v in value] + weight = input(f"Enter the positive weights of the {n} item(s) in order: ".split()) + weight = [int(w) for w in weight] + capacity = int(input("Enter maximum weight: ")) + + max_value, fractions = fractional_knapsack(value, weight, capacity) + print("The maximum value of items that can be carried:", max_value) + print("The fractions in which the items should be taken:", fractions) diff --git a/dynamic_programming/integer_partition.py b/dynamic_programming/integer_partition.py index 7b27afebaa6c..ec8c5bf62d7d 100644 --- a/dynamic_programming/integer_partition.py +++ b/dynamic_programming/integer_partition.py @@ -1,45 +1,36 @@ -from __future__ import print_function - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -try: - raw_input #Python 2 -except NameError: - raw_input = input #Python 3 - -''' +""" The number of partitions of a number n into at least k parts equals the number of partitions into exactly k parts plus the number of partitions into at least k-1 parts. Subtracting 1 from each part of a partition of n into k parts gives a partition of n-k into k parts. These two facts together are used for this algorithm. -''' +""" + + def partition(m): - memo = [[0 for _ in xrange(m)] for _ in xrange(m+1)] - for i in xrange(m+1): - memo[i][0] = 1 + memo = [[0 for _ in range(m)] for _ in range(m + 1)] + for i in range(m + 1): + memo[i][0] = 1 + + for n in range(m + 1): + for k in range(1, m): + memo[n][k] += memo[n][k - 1] + if n - k > 0: + memo[n][k] += memo[n - k - 1][k] - for n in xrange(m+1): - for k in xrange(1, m): - memo[n][k] += memo[n][k-1] - if n-k > 0: - memo[n][k] += memo[n-k-1][k] + return memo[m][m - 1] - return memo[m][m-1] -if __name__ == '__main__': - import sys +if __name__ == "__main__": + import sys - if len(sys.argv) == 1: - try: - n = int(raw_input('Enter a number: ')) - print(partition(n)) - except ValueError: - print('Please enter a number.') - else: - try: - n = int(sys.argv[1]) - print(partition(n)) - except ValueError: - print('Please pass a number.') \ No newline at end of file + if len(sys.argv) == 1: + try: + n = int(input("Enter a number: ").strip()) + print(partition(n)) + except ValueError: + print("Please enter a number.") + else: + try: + n = int(sys.argv[1]) + print(partition(n)) + except ValueError: + print("Please pass a number.") diff --git a/dynamic_programming/iterating_through_submasks.py b/dynamic_programming/iterating_through_submasks.py new file mode 100644 index 000000000000..edeacc3124fa --- /dev/null +++ b/dynamic_programming/iterating_through_submasks.py @@ -0,0 +1,60 @@ +""" +Author : Syed Faizan (3rd Year Student IIIT Pune) +github : faizan2700 +You are given a bitmask m and you want to efficiently iterate through all of +its submasks. The mask s is submask of m if only bits that were included in +bitmask are set +""" +from typing import List + + +def list_of_submasks(mask: int) -> List[int]: + + """ + Args: + mask : number which shows mask ( always integer > 0, zero does not have any submasks ) + + Returns: + all_submasks : the list of submasks of mask (mask s is called submask of mask + m if only bits that were included in original mask are set + + Raises: + AssertionError: mask not positive integer + + >>> list_of_submasks(15) + [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + >>> list_of_submasks(13) + [13, 12, 9, 8, 5, 4, 1] + >>> list_of_submasks(-7) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError: mask needs to be positive integer, your input -7 + >>> list_of_submasks(0) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AssertionError: mask needs to be positive integer, your input 0 + + """ + + fmt = "mask needs to be positive integer, your input {}" + assert isinstance(mask, int) and mask > 0, fmt.format(mask) + + """ + first submask iterated will be mask itself then operation will be performed + to get other submasks till we reach empty submask that is zero ( zero is not + included in final submasks list ) + """ + all_submasks = [] + submask = mask + + while submask: + all_submasks.append(submask) + submask = (submask - 1) & mask + + return all_submasks + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/k_means_clustering_tensorflow.py b/dynamic_programming/k_means_clustering_tensorflow.py_tf similarity index 51% rename from dynamic_programming/k_means_clustering_tensorflow.py rename to dynamic_programming/k_means_clustering_tensorflow.py_tf index b6813c6a22b3..4fbcedeaa0dc 100644 --- a/dynamic_programming/k_means_clustering_tensorflow.py +++ b/dynamic_programming/k_means_clustering_tensorflow.py_tf @@ -14,24 +14,24 @@ def TFKMeansCluster(vectors, noofclusters): noofclusters = int(noofclusters) assert noofclusters < len(vectors) - #Find out the dimensionality + # Find out the dimensionality dim = len(vectors[0]) - #Will help select random centroids from among the available vectors + # Will help select random centroids from among the available vectors vector_indices = list(range(len(vectors))) shuffle(vector_indices) - #GRAPH OF COMPUTATION - #We initialize a new graph and set it as the default during each run - #of this algorithm. This ensures that as this function is called - #multiple times, the default graph doesn't keep getting crowded with - #unused ops and Variables from previous function calls. + # GRAPH OF COMPUTATION + # We initialize a new graph and set it as the default during each run + # of this algorithm. This ensures that as this function is called + # multiple times, the default graph doesn't keep getting crowded with + # unused ops and Variables from previous function calls. graph = tf.Graph() with graph.as_default(): - #SESSION OF COMPUTATION + # SESSION OF COMPUTATION sess = tf.Session() @@ -39,8 +39,9 @@ def TFKMeansCluster(vectors, noofclusters): ##First lets ensure we have a Variable vector for each centroid, ##initialized to one of the vectors from the available data points - centroids = [tf.Variable((vectors[vector_indices[i]])) - for i in range(noofclusters)] + centroids = [ + tf.Variable(vectors[vector_indices[i]]) for i in range(noofclusters) + ] ##These nodes will assign the centroid Variables the appropriate ##values centroid_value = tf.placeholder("float64", [dim]) @@ -56,26 +57,24 @@ def TFKMeansCluster(vectors, noofclusters): assignment_value = tf.placeholder("int32") cluster_assigns = [] for assignment in assignments: - cluster_assigns.append(tf.assign(assignment, - assignment_value)) + cluster_assigns.append(tf.assign(assignment, assignment_value)) ##Now lets construct the node that will compute the mean - #The placeholder for the input + # The placeholder for the input mean_input = tf.placeholder("float", [None, dim]) - #The Node/op takes the input and computes a mean along the 0th - #dimension, i.e. the list of input vectors + # The Node/op takes the input and computes a mean along the 0th + # dimension, i.e. the list of input vectors mean_op = tf.reduce_mean(mean_input, 0) ##Node for computing Euclidean distances - #Placeholders for input + # Placeholders for input v1 = tf.placeholder("float", [dim]) v2 = tf.placeholder("float", [dim]) - euclid_dist = tf.sqrt(tf.reduce_sum(tf.pow(tf.sub( - v1, v2), 2))) + euclid_dist = tf.sqrt(tf.reduce_sum(tf.pow(tf.sub(v1, v2), 2))) ##This node will figure out which cluster to assign a vector to, ##based on Euclidean distances of the vector from the centroids. - #Placeholder for input + # Placeholder for input centroid_distances = tf.placeholder("float", [noofclusters]) cluster_assignment = tf.argmin(centroid_distances, 0) @@ -87,55 +86,62 @@ def TFKMeansCluster(vectors, noofclusters): ##will be included in the initialization. init_op = tf.initialize_all_variables() - #Initialize all variables + # Initialize all variables sess.run(init_op) ##CLUSTERING ITERATIONS - #Now perform the Expectation-Maximization steps of K-Means clustering - #iterations. To keep things simple, we will only do a set number of - #iterations, instead of using a Stopping Criterion. + # Now perform the Expectation-Maximization steps of K-Means clustering + # iterations. To keep things simple, we will only do a set number of + # iterations, instead of using a Stopping Criterion. noofiterations = 100 for iteration_n in range(noofiterations): ##EXPECTATION STEP ##Based on the centroid locations till last iteration, compute ##the _expected_ centroid assignments. - #Iterate over each vector + # Iterate over each vector for vector_n in range(len(vectors)): vect = vectors[vector_n] - #Compute Euclidean distance between this vector and each - #centroid. Remember that this list cannot be named + # Compute Euclidean distance between this vector and each + # centroid. Remember that this list cannot be named #'centroid_distances', since that is the input to the - #cluster assignment node. - distances = [sess.run(euclid_dist, feed_dict={ - v1: vect, v2: sess.run(centroid)}) - for centroid in centroids] - #Now use the cluster assignment node, with the distances - #as the input - assignment = sess.run(cluster_assignment, feed_dict = { - centroid_distances: distances}) - #Now assign the value to the appropriate state variable - sess.run(cluster_assigns[vector_n], feed_dict={ - assignment_value: assignment}) + # cluster assignment node. + distances = [ + sess.run(euclid_dist, feed_dict={v1: vect, v2: sess.run(centroid)}) + for centroid in centroids + ] + # Now use the cluster assignment node, with the distances + # as the input + assignment = sess.run( + cluster_assignment, feed_dict={centroid_distances: distances} + ) + # Now assign the value to the appropriate state variable + sess.run( + cluster_assigns[vector_n], feed_dict={assignment_value: assignment} + ) ##MAXIMIZATION STEP - #Based on the expected state computed from the Expectation Step, - #compute the locations of the centroids so as to maximize the - #overall objective of minimizing within-cluster Sum-of-Squares + # Based on the expected state computed from the Expectation Step, + # compute the locations of the centroids so as to maximize the + # overall objective of minimizing within-cluster Sum-of-Squares for cluster_n in range(noofclusters): - #Collect all the vectors assigned to this cluster - assigned_vects = [vectors[i] for i in range(len(vectors)) - if sess.run(assignments[i]) == cluster_n] - #Compute new centroid location - new_location = sess.run(mean_op, feed_dict={ - mean_input: array(assigned_vects)}) - #Assign value to appropriate variable - sess.run(cent_assigns[cluster_n], feed_dict={ - centroid_value: new_location}) - - #Return centroids and assignments + # Collect all the vectors assigned to this cluster + assigned_vects = [ + vectors[i] + for i in range(len(vectors)) + if sess.run(assignments[i]) == cluster_n + ] + # Compute new centroid location + new_location = sess.run( + mean_op, feed_dict={mean_input: array(assigned_vects)} + ) + # Assign value to appropriate variable + sess.run( + cent_assigns[cluster_n], feed_dict={centroid_value: new_location} + ) + + # Return centroids and assignments centroids = sess.run(centroids) assignments = sess.run(assignments) return centroids, assignments - diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 27d1cfed799b..1987dc35fd03 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -1,42 +1,150 @@ """ -Given weights and values of n items, put these items in a knapsack of capacity W to get the maximum total value in the knapsack. +Given weights and values of n items, put these items in a knapsack of + capacity W to get the maximum total value in the knapsack. + +Note that only the integer weights 0-1 knapsack problem is solvable + using dynamic programming. """ -def MF_knapsack(i,wt,val,j): - ''' + + +def MF_knapsack(i, wt, val, j): + """ This code involves the concept of memory functions. Here we solve the subproblems which are needed unlike the below example F is a 2D array with -1s filled up - ''' + """ global F # a global dp table for knapsack if F[i][j] < 0: if j < wt[i - 1]: - val = MF_knapsack(i - 1,wt,val,j) + val = MF_knapsack(i - 1, wt, val, j) else: - val = max(MF_knapsack(i - 1,wt,val,j),MF_knapsack(i - 1,wt,val,j - wt[i - 1]) + val[i - 1]) + val = max( + MF_knapsack(i - 1, wt, val, j), + MF_knapsack(i - 1, wt, val, j - wt[i - 1]) + val[i - 1], + ) F[i][j] = val return F[i][j] + def knapsack(W, wt, val, n): - dp = [[0 for i in range(W+1)]for j in range(n+1)] + dp = [[0 for i in range(W + 1)] for j in range(n + 1)] - for i in range(1,n+1): - for w in range(1,W+1): - if(wt[i-1]<=w): - dp[i][w] = max(val[i-1]+dp[i-1][w-wt[i-1]],dp[i-1][w]) + for i in range(1, n + 1): + for w in range(1, W + 1): + if wt[i - 1] <= w: + dp[i][w] = max(val[i - 1] + dp[i - 1][w - wt[i - 1]], dp[i - 1][w]) else: - dp[i][w] = dp[i-1][w] + dp[i][w] = dp[i - 1][w] + + return dp[n][W], dp + + +def knapsack_with_example_solution(W: int, wt: list, val: list): + """ + Solves the integer weights knapsack problem returns one of + the several possible optimal subsets. + + Parameters + --------- + + W: int, the total maximum weight for the given knapsack problem. + wt: list, the vector of weights for all items where wt[i] is the weight + of the i-th item. + val: list, the vector of values for all items where val[i] is the value + of the i-th item + + Returns + ------- + optimal_val: float, the optimal value for the given knapsack problem + example_optional_set: set, the indices of one of the optimal subsets + which gave rise to the optimal value. + + Examples + ------- + >>> knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22]) + (142, {2, 3, 4}) + >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4, 4]) + (8, {3, 4}) + >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4]) + Traceback (most recent call last): + ... + ValueError: The number of weights must be the same as the number of values. + But got 4 weights and 3 values + """ + if not (isinstance(wt, (list, tuple)) and isinstance(val, (list, tuple))): + raise ValueError( + "Both the weights and values vectors must be either lists or tuples" + ) + + num_items = len(wt) + if num_items != len(val): + raise ValueError( + "The number of weights must be the " + "same as the number of values.\nBut " + f"got {num_items} weights and {len(val)} values" + ) + for i in range(num_items): + if not isinstance(wt[i], int): + raise TypeError( + "All weights must be integers but " + f"got weight of type {type(wt[i])} at index {i}" + ) - return dp[n][w] + optimal_val, dp_table = knapsack(W, wt, val, num_items) + example_optional_set = set() + _construct_solution(dp_table, wt, num_items, W, example_optional_set) -if __name__ == '__main__': - ''' + return optimal_val, example_optional_set + + +def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set): + """ + Recursively reconstructs one of the optimal subsets given + a filled DP table and the vector of weights + + Parameters + --------- + + dp: list of list, the table of a solved integer weight dynamic programming problem + + wt: list or tuple, the vector of weights of the items + i: int, the index of the item under consideration + j: int, the current possible maximum weight + optimal_set: set, the optimal subset so far. This gets modified by the function. + + Returns + ------- + None + + """ + # for the current item i at a maximum weight j to be part of an optimal subset, + # the optimal value at (i, j) must be greater than the optimal value at (i-1, j). + # where i - 1 means considering only the previous items at the given maximum weight + if i > 0 and j > 0: + if dp[i - 1][j] == dp[i][j]: + _construct_solution(dp, wt, i - 1, j, optimal_set) + else: + optimal_set.add(i) + _construct_solution(dp, wt, i - 1, j - wt[i - 1], optimal_set) + + +if __name__ == "__main__": + """ Adding test case for knapsack - ''' - val = [3,2,4,4] - wt = [4,3,2,3] + """ + val = [3, 2, 4, 4] + wt = [4, 3, 2, 3] n = 4 w = 6 - F = [[0]*(w + 1)] + [[0] + [-1 for i in range(w + 1)] for j in range(n + 1)] - print(knapsack(w,wt,val,n)) - print(MF_knapsack(n,wt,val,w)) # switched the n and w - + F = [[0] * (w + 1)] + [[0] + [-1 for i in range(w + 1)] for j in range(n + 1)] + optimal_solution, _ = knapsack(w, wt, val, n) + print(optimal_solution) + print(MF_knapsack(n, wt, val, w)) # switched the n and w + + # testing the dynamic programming problem with example + # the optimal subset for the above example are items 3 and 4 + optimal_solution, optimal_subset = knapsack_with_example_solution(w, wt, val) + assert optimal_solution == 8 + assert optimal_subset == {3, 4} + print("optimal_value = ", optimal_solution) + print("An optimal subset corresponding to the optimal value", optimal_subset) diff --git a/dynamic_programming/longest_common_subsequence.py b/dynamic_programming/longest_common_subsequence.py index 0a4771cb2efd..a7206b221d96 100644 --- a/dynamic_programming/longest_common_subsequence.py +++ b/dynamic_programming/longest_common_subsequence.py @@ -1,37 +1,82 @@ """ LCS Problem Statement: Given two sequences, find the length of longest subsequence present in both of them. -A subsequence is a sequence that appears in the same relative order, but not necessarily continious. +A subsequence is a sequence that appears in the same relative order, but not necessarily continuous. Example:"abc", "abg" are subsequences of "abcdefgh". """ -from __future__ import print_function -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 -def lcs_dp(x, y): +def longest_common_subsequence(x: str, y: str): + """ + Finds the longest common subsequence between two strings. Also returns the + The subsequence found + + Parameters + ---------- + + x: str, one of the strings + y: str, the other string + + Returns + ------- + L[m][n]: int, the length of the longest subsequence. Also equal to len(seq) + Seq: str, the subsequence found + + >>> longest_common_subsequence("programming", "gaming") + (6, 'gaming') + >>> longest_common_subsequence("physics", "smartphone") + (2, 'ph') + >>> longest_common_subsequence("computer", "food") + (1, 'o') + """ # find the length of strings + + assert x is not None + assert y is not None + m = len(x) n = len(y) # declaring the array for storing the dp values - L = [[None] * (n + 1) for i in xrange(m + 1)] - seq = [] - - for i in range(m + 1): - for j in range(n + 1): - if i == 0 or j == 0: - L[i][j] = 0 - elif x[i - 1] == y[ j - 1]: - L[i][j] = L[i - 1][j - 1] + 1 - seq.append(x[i -1]) + L = [[0] * (n + 1) for _ in range(m + 1)] + + for i in range(1, m + 1): + for j in range(1, n + 1): + if x[i - 1] == y[j - 1]: + match = 1 else: - L[i][j] = max(L[i - 1][j], L[i][j - 1]) - # L[m][n] contains the length of LCS of X[0..n-1] & Y[0..m-1] + match = 0 + + L[i][j] = max(L[i - 1][j], L[i][j - 1], L[i - 1][j - 1] + match) + + seq = "" + i, j = m, n + while i > 0 and j > 0: + if x[i - 1] == y[j - 1]: + match = 1 + else: + match = 0 + + if L[i][j] == L[i - 1][j - 1] + match: + if match == 1: + seq = x[i - 1] + seq + i -= 1 + j -= 1 + elif L[i][j] == L[i - 1][j]: + i -= 1 + else: + j -= 1 + return L[m][n], seq -if __name__=='__main__': - x = 'AGGTAB' - y = 'GXTXAYB' - print(lcs_dp(x, y)) + +if __name__ == "__main__": + a = "AGGTAB" + b = "GXTXAYB" + expected_ln = 4 + expected_subseq = "GTAB" + + ln, subseq = longest_common_subsequence(a, b) + ## print("len =", ln, ", sub-sequence =", subseq) + import doctest + + doctest.testmod() diff --git a/dynamic_programming/longest_increasing_subsequence.py b/dynamic_programming/longest_increasing_subsequence.py index b6d165909e70..81b7f8f8ff17 100644 --- a/dynamic_programming/longest_increasing_subsequence.py +++ b/dynamic_programming/longest_increasing_subsequence.py @@ -1,42 +1,56 @@ -''' +""" Author : Mehdi ALAOUI This is a pure Python implementation of Dynamic Programming solution to the longest increasing subsequence of a given sequence. The problem is : -Given an ARRAY, to find the longest and increasing sub ARRAY in that given ARRAY and return it. +Given an array, to find the longest and increasing sub-array in that given array and return it. Example: [10, 22, 9, 33, 21, 50, 41, 60, 80] as input will return [10, 22, 33, 41, 60, 80] as output -''' -from __future__ import print_function - -def longestSub(ARRAY): #This function is recursive - - ARRAY_LENGTH = len(ARRAY) - if(ARRAY_LENGTH <= 1): #If the array contains only one element, we return it (it's the stop condition of recursion) - return ARRAY - #Else - PIVOT=ARRAY[0] - isFound=False - i=1 - LONGEST_SUB=[] - while(not isFound and i= ARRAY[i] ] - TEMPORARY_ARRAY = longestSub(TEMPORARY_ARRAY) - if ( len(TEMPORARY_ARRAY) > len(LONGEST_SUB) ): - LONGEST_SUB = TEMPORARY_ARRAY - else: - i+=1 - - TEMPORARY_ARRAY = [ element for element in ARRAY[1:] if element >= PIVOT ] - TEMPORARY_ARRAY = [PIVOT] + longestSub(TEMPORARY_ARRAY) - if ( len(TEMPORARY_ARRAY) > len(LONGEST_SUB) ): - return TEMPORARY_ARRAY - else: - return LONGEST_SUB - -#Some examples - -print(longestSub([4,8,7,5,1,12,2,3,9])) -print(longestSub([9,8,7,6,5,7])) \ No newline at end of file +""" +from typing import List + + +def longest_subsequence(array: List[int]) -> List[int]: # This function is recursive + """ + Some examples + >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80]) + [10, 22, 33, 41, 60, 80] + >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9]) + [1, 2, 3, 9] + >>> longest_subsequence([9, 8, 7, 6, 5, 7]) + [8] + >>> longest_subsequence([1, 1, 1]) + [1, 1, 1] + """ + array_length = len(array) + if ( + array_length <= 1 + ): # If the array contains only one element, we return it (it's the stop condition of recursion) + return array + # Else + pivot = array[0] + isFound = False + i = 1 + longest_subseq = [] + while not isFound and i < array_length: + if array[i] < pivot: + isFound = True + temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + longest_subseq = temp_array + else: + i += 1 + + temp_array = [element for element in array[1:] if element >= pivot] + temp_array = [pivot] + longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + return temp_array + else: + return longest_subseq + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py b/dynamic_programming/longest_increasing_subsequence_o(nlogn).py index 86bec089adc7..46790a5a8d41 100644 --- a/dynamic_programming/longest_increasing_subsequence_o(nlogn).py +++ b/dynamic_programming/longest_increasing_subsequence_o(nlogn).py @@ -1,42 +1,54 @@ -from __future__ import print_function ############################# # Author: Aravind Kashyap # File: lis.py # comments: This programme outputs the Longest Strictly Increasing Subsequence in O(NLogN) -# Where N is the Number of elements in the list +# Where N is the Number of elements in the list ############################# -def CeilIndex(v,l,r,key): - while r-l > 1: - m = (l + r)/2 - if v[m] >= key: - r = m - else: - l = m - - return r - - -def LongestIncreasingSubsequenceLength(v): - if(len(v) == 0): - return 0 - - tail = [0]*len(v) - length = 1 - - tail[0] = v[0] - - for i in range(1,len(v)): - if v[i] < tail[0]: - tail[0] = v[i] - elif v[i] > tail[length-1]: - tail[length] = v[i] - length += 1 - else: - tail[CeilIndex(tail,-1,length-1,v[i])] = v[i] - - return length - +from typing import List + + +def CeilIndex(v, l, r, key): + while r - l > 1: + m = (l + r) // 2 + if v[m] >= key: + r = m + else: + l = m + return r + + +def LongestIncreasingSubsequenceLength(v: List[int]) -> int: + """ + >>> LongestIncreasingSubsequenceLength([2, 5, 3, 7, 11, 8, 10, 13, 6]) + 6 + >>> LongestIncreasingSubsequenceLength([]) + 0 + >>> LongestIncreasingSubsequenceLength([0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]) + 6 + >>> LongestIncreasingSubsequenceLength([5, 4, 3, 2, 1]) + 1 + """ + if len(v) == 0: + return 0 + + tail = [0] * len(v) + length = 1 + + tail[0] = v[0] + + for i in range(1, len(v)): + if v[i] < tail[0]: + tail[0] = v[i] + elif v[i] > tail[length - 1]: + tail[length] = v[i] + length += 1 + else: + tail[CeilIndex(tail, -1, length - 1, v[i])] = v[i] + + return length + if __name__ == "__main__": - v = [2, 5, 3, 7, 11, 8, 10, 13, 6] - print(LongestIncreasingSubsequenceLength(v)) + import doctest + + doctest.testmod() diff --git a/dynamic_programming/longest_sub_array.py b/dynamic_programming/longest_sub_array.py index de2c88a8b525..f3b5705b7de2 100644 --- a/dynamic_programming/longest_sub_array.py +++ b/dynamic_programming/longest_sub_array.py @@ -1,33 +1,32 @@ -''' -Auther : Yvonne +""" +Author : Yvonne This is a pure Python implementation of Dynamic Programming solution to the longest_sub_array problem. The problem is : Given an array, to find the longest and continuous sub array and get the max sum of the sub array in the given array. -''' -from __future__ import print_function +""" class SubArray: - def __init__(self, arr): # we need a list not a string, so do something to change the type - self.array = arr.split(',') + self.array = arr.split(",") print(("the input array is:", self.array)) def solve_sub_array(self): - rear = [int(self.array[0])]*len(self.array) - sum_value = [int(self.array[0])]*len(self.array) + rear = [int(self.array[0])] * len(self.array) + sum_value = [int(self.array[0])] * len(self.array) for i in range(1, len(self.array)): - sum_value[i] = max(int(self.array[i]) + sum_value[i-1], int(self.array[i])) - rear[i] = max(sum_value[i], rear[i-1]) - return rear[len(self.array)-1] + sum_value[i] = max( + int(self.array[i]) + sum_value[i - 1], int(self.array[i]) + ) + rear[i] = max(sum_value[i], rear[i - 1]) + return rear[len(self.array) - 1] -if __name__ == '__main__': +if __name__ == "__main__": whole_array = input("please input some numbers:") array = SubArray(whole_array) re = array.solve_sub_array() print(("the results is:", re)) - diff --git a/dynamic_programming/matrix_chain_order.py b/dynamic_programming/matrix_chain_order.py index b8234a65acbe..9411bc704f1c 100644 --- a/dynamic_programming/matrix_chain_order.py +++ b/dynamic_programming/matrix_chain_order.py @@ -1,46 +1,54 @@ -from __future__ import print_function - import sys -''' + +""" Dynamic Programming Implementation of Matrix Chain Multiplication Time Complexity: O(n^3) Space Complexity: O(n^2) -''' +""" + + def MatrixChainOrder(array): - N=len(array) - Matrix=[[0 for x in range(N)] for x in range(N)] - Sol=[[0 for x in range(N)] for x in range(N)] + N = len(array) + Matrix = [[0 for x in range(N)] for x in range(N)] + Sol = [[0 for x in range(N)] for x in range(N)] - for ChainLength in range(2,N): - for a in range(1,N-ChainLength+1): - b = a+ChainLength-1 + for ChainLength in range(2, N): + for a in range(1, N - ChainLength + 1): + b = a + ChainLength - 1 Matrix[a][b] = sys.maxsize - for c in range(a , b): - cost = Matrix[a][c] + Matrix[c+1][b] + array[a-1]*array[c]*array[b] + for c in range(a, b): + cost = ( + Matrix[a][c] + Matrix[c + 1][b] + array[a - 1] * array[c] * array[b] + ) if cost < Matrix[a][b]: Matrix[a][b] = cost Sol[a][b] = c - return Matrix , Sol -#Print order of matrix with Ai as Matrix -def PrintOptimalSolution(OptimalSolution,i,j): - if i==j: - print("A" + str(i),end = " ") + return Matrix, Sol + + +# Print order of matrix with Ai as Matrix +def PrintOptimalSolution(OptimalSolution, i, j): + if i == j: + print("A" + str(i), end=" ") else: - print("(",end = " ") - PrintOptimalSolution(OptimalSolution,i,OptimalSolution[i][j]) - PrintOptimalSolution(OptimalSolution,OptimalSolution[i][j]+1,j) - print(")",end = " ") + print("(", end=" ") + PrintOptimalSolution(OptimalSolution, i, OptimalSolution[i][j]) + PrintOptimalSolution(OptimalSolution, OptimalSolution[i][j] + 1, j) + print(")", end=" ") + def main(): - array=[30,35,15,5,10,20,25] - n=len(array) - #Size of matrix created from above array will be + array = [30, 35, 15, 5, 10, 20, 25] + n = len(array) + # Size of matrix created from above array will be # 30*35 35*15 15*5 5*10 10*20 20*25 - Matrix , OptimalSolution = MatrixChainOrder(array) + Matrix, OptimalSolution = MatrixChainOrder(array) + + print("No. of Operation required: " + str(Matrix[1][n - 1])) + PrintOptimalSolution(OptimalSolution, 1, n - 1) + - print("No. of Operation required: "+str((Matrix[1][n-1]))) - PrintOptimalSolution(OptimalSolution,1,n-1) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/dynamic_programming/max_sub_array.py b/dynamic_programming/max_sub_array.py index 56983b7d22c2..7350eaf373cb 100644 --- a/dynamic_programming/max_sub_array.py +++ b/dynamic_programming/max_sub_array.py @@ -1,80 +1,93 @@ """ author : Mayank Kumar Jha (mk9440) """ -from __future__ import print_function from typing import List -import time -import matplotlib.pyplot as plt -from random import randint -def find_max_sub_array(A,low,high): - if low==high: - return low,high,A[low] - else : - mid=(low+high)//2 - left_low,left_high,left_sum=find_max_sub_array(A,low,mid) - right_low,right_high,right_sum=find_max_sub_array(A,mid+1,high) - cross_left,cross_right,cross_sum=find_max_cross_sum(A,low,mid,high) - if left_sum>=right_sum and left_sum>=cross_sum: - return left_low,left_high,left_sum - elif right_sum>=left_sum and right_sum>=cross_sum : - return right_low,right_high,right_sum + + +def find_max_sub_array(A, low, high): + if low == high: + return low, high, A[low] + else: + mid = (low + high) // 2 + left_low, left_high, left_sum = find_max_sub_array(A, low, mid) + right_low, right_high, right_sum = find_max_sub_array(A, mid + 1, high) + cross_left, cross_right, cross_sum = find_max_cross_sum(A, low, mid, high) + if left_sum >= right_sum and left_sum >= cross_sum: + return left_low, left_high, left_sum + elif right_sum >= left_sum and right_sum >= cross_sum: + return right_low, right_high, right_sum else: - return cross_left,cross_right,cross_sum + return cross_left, cross_right, cross_sum + -def find_max_cross_sum(A,low,mid,high): - left_sum,max_left=-999999999,-1 - right_sum,max_right=-999999999,-1 - summ=0 - for i in range(mid,low-1,-1): - summ+=A[i] +def find_max_cross_sum(A, low, mid, high): + left_sum, max_left = -999999999, -1 + right_sum, max_right = -999999999, -1 + summ = 0 + for i in range(mid, low - 1, -1): + summ += A[i] if summ > left_sum: - left_sum=summ - max_left=i - summ=0 - for i in range(mid+1,high+1): - summ+=A[i] + left_sum = summ + max_left = i + summ = 0 + for i in range(mid + 1, high + 1): + summ += A[i] if summ > right_sum: - right_sum=summ - max_right=i - return max_left,max_right,(left_sum+right_sum) + right_sum = summ + max_right = i + return max_left, max_right, (left_sum + right_sum) + def max_sub_array(nums: List[int]) -> int: """ - Finds the contiguous subarray (can be empty array) - which has the largest sum and return its sum. + Finds the contiguous subarray which has the largest sum and return its sum. - >>> max_sub_array([-2,1,-3,4,-1,2,1,-5,4]) + >>> max_sub_array([-2, 1, -3, 4, -1, 2, 1, -5, 4]) 6 + + An empty (sub)array has sum 0. >>> max_sub_array([]) 0 - >>> max_sub_array([-1,-2,-3]) + + If all elements are negative, the largest subarray would be the empty array, + having the sum 0. + >>> max_sub_array([-1, -2, -3]) 0 + >>> max_sub_array([5, -2, -3]) + 5 + >>> max_sub_array([31, -41, 59, 26, -53, 58, 97, -93, -23, 84]) + 187 """ - best = 0 - current = 0 - for i in nums: - current += i + best = 0 + current = 0 + for i in nums: + current += i if current < 0: current = 0 best = max(best, current) - return best - -if __name__=='__main__': - inputs=[10,100,1000,10000,50000,100000,200000,300000,400000,500000] - tim=[] - for i in inputs: - li=[randint(1,i) for j in range(i)] - strt=time.time() - (find_max_sub_array(li,0,len(li)-1)) - end=time.time() - tim.append(end-strt) - print("No of Inputs Time Taken") - for i in range(len(inputs)): - print(inputs[i],'\t\t',tim[i]) - plt.plot(inputs,tim) - plt.xlabel("Number of Inputs");plt.ylabel("Time taken in seconds ") - plt.show() + return best +if __name__ == "__main__": + """ + A random simulation of this algorithm. + """ + import time + import matplotlib.pyplot as plt + from random import randint - + inputs = [10, 100, 1000, 10000, 50000, 100000, 200000, 300000, 400000, 500000] + tim = [] + for i in inputs: + li = [randint(1, i) for j in range(i)] + strt = time.time() + (find_max_sub_array(li, 0, len(li) - 1)) + end = time.time() + tim.append(end - strt) + print("No of Inputs Time Taken") + for i in range(len(inputs)): + print(inputs[i], "\t\t", tim[i]) + plt.plot(inputs, tim) + plt.xlabel("Number of Inputs") + plt.ylabel("Time taken in seconds ") + plt.show() diff --git a/dynamic_programming/max_sum_contiguous_subsequence.py b/dynamic_programming/max_sum_contiguous_subsequence.py new file mode 100644 index 000000000000..bac592370c5d --- /dev/null +++ b/dynamic_programming/max_sum_contiguous_subsequence.py @@ -0,0 +1,20 @@ +def max_subarray_sum(nums: list) -> int: + """ + >>> max_subarray_sum([6 , 9, -1, 3, -7, -5, 10]) + 17 + """ + if not nums: + return 0 + n = len(nums) + + res, s, s_pre = nums[0], nums[0], nums[0] + for i in range(1, n): + s = max(nums[i], s_pre + nums[i]) + s_pre = s + res = max(res, s) + return res + + +if __name__ == "__main__": + nums = [6, 9, -1, 3, -7, -5, 10] + print(max_subarray_sum(nums)) diff --git a/dynamic_programming/minimum_partition.py b/dynamic_programming/minimum_partition.py index 18aa1faa2fa6..d5750326fea4 100644 --- a/dynamic_programming/minimum_partition.py +++ b/dynamic_programming/minimum_partition.py @@ -1,28 +1,30 @@ """ Partition a set into two subsets such that the difference of subset sums is minimum """ + + def findMin(arr): n = len(arr) s = sum(arr) - dp = [[False for x in range(s+1)]for y in range(n+1)] + dp = [[False for x in range(s + 1)] for y in range(n + 1)] - for i in range(1, n+1): + for i in range(1, n + 1): dp[i][0] = True - for i in range(1, s+1): + for i in range(1, s + 1): dp[0][i] = False - for i in range(1, n+1): - for j in range(1, s+1): - dp[i][j]= dp[i][j-1] + for i in range(1, n + 1): + for j in range(1, s + 1): + dp[i][j] = dp[i][j - 1] - if (arr[i-1] <= j): - dp[i][j] = dp[i][j] or dp[i-1][j-arr[i-1]] + if arr[i - 1] <= j: + dp[i][j] = dp[i][j] or dp[i - 1][j - arr[i - 1]] - for j in range(int(s/2), -1, -1): + for j in range(int(s / 2), -1, -1): if dp[n][j] == True: - diff = s-2*j - break; + diff = s - 2 * j + break return diff diff --git a/dynamic_programming/optimal_binary_search_tree.py b/dynamic_programming/optimal_binary_search_tree.py new file mode 100644 index 000000000000..f33ca01bd933 --- /dev/null +++ b/dynamic_programming/optimal_binary_search_tree.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 + +# This Python program implements an optimal binary search tree (abbreviated BST) +# building dynamic programming algorithm that delivers O(n^2) performance. +# +# The goal of the optimal BST problem is to build a low-cost BST for a +# given set of nodes, each with its own key and frequency. The frequency +# of the node is defined as how many time the node is being searched. +# The search cost of binary search tree is given by this formula: +# +# cost(1, n) = sum{i = 1 to n}((depth(node_i) + 1) * node_i_freq) +# +# where n is number of nodes in the BST. The characteristic of low-cost +# BSTs is having a faster overall search time than other implementations. +# The reason for their fast search time is that the nodes with high +# frequencies will be placed near the root of the tree while the nodes +# with low frequencies will be placed near the leaves of the tree thus +# reducing search time in the most frequent instances. + +import sys + +from random import randint + + +class Node: + """Binary Search Tree Node""" + + def __init__(self, key, freq): + self.key = key + self.freq = freq + + def __str__(self): + """ + >>> str(Node(1, 2)) + 'Node(key=1, freq=2)' + """ + return f"Node(key={self.key}, freq={self.freq})" + + +def print_binary_search_tree(root, key, i, j, parent, is_left): + """ + Recursive function to print a BST from a root table. + + >>> key = [3, 8, 9, 10, 17, 21] + >>> root = [[0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 3], [0, 0, 2, 3, 3, 3], \ + [0, 0, 0, 3, 3, 3], [0, 0, 0, 0, 4, 5], [0, 0, 0, 0, 0, 5]] + >>> print_binary_search_tree(root, key, 0, 5, -1, False) + 8 is the root of the binary search tree. + 3 is the left child of key 8. + 10 is the right child of key 8. + 9 is the left child of key 10. + 21 is the right child of key 10. + 17 is the left child of key 21. + """ + if i > j or i < 0 or j > len(root) - 1: + return + + node = root[i][j] + if parent == -1: # root does not have a parent + print(f"{key[node]} is the root of the binary search tree.") + elif is_left: + print(f"{key[node]} is the left child of key {parent}.") + else: + print(f"{key[node]} is the right child of key {parent}.") + + print_binary_search_tree(root, key, i, node - 1, key[node], True) + print_binary_search_tree(root, key, node + 1, j, key[node], False) + + +def find_optimal_binary_search_tree(nodes): + """ + This function calculates and prints the optimal binary search tree. + The dynamic programming algorithm below runs in O(n^2) time. + Implemented from CLRS (Introduction to Algorithms) book. + https://en.wikipedia.org/wiki/Introduction_to_Algorithms + + >>> find_optimal_binary_search_tree([Node(12, 8), Node(10, 34), Node(20, 50), \ + Node(42, 3), Node(25, 40), Node(37, 30)]) + Binary search tree nodes: + Node(key=10, freq=34) + Node(key=12, freq=8) + Node(key=20, freq=50) + Node(key=25, freq=40) + Node(key=37, freq=30) + Node(key=42, freq=3) + + The cost of optimal BST for given tree nodes is 324. + 20 is the root of the binary search tree. + 10 is the left child of key 20. + 12 is the right child of key 10. + 25 is the right child of key 20. + 37 is the right child of key 25. + 42 is the right child of key 37. + """ + # Tree nodes must be sorted first, the code below sorts the keys in + # increasing order and rearrange its frequencies accordingly. + nodes.sort(key=lambda node: node.key) + + n = len(nodes) + + keys = [nodes[i].key for i in range(n)] + freqs = [nodes[i].freq for i in range(n)] + + # This 2D array stores the overall tree cost (which's as minimized as possible); + # for a single key, cost is equal to frequency of the key. + dp = [[freqs[i] if i == j else 0 for j in range(n)] for i in range(n)] + # sum[i][j] stores the sum of key frequencies between i and j inclusive in nodes array + sum = [[freqs[i] if i == j else 0 for j in range(n)] for i in range(n)] + # stores tree roots that will be used later for constructing binary search tree + root = [[i if i == j else 0 for j in range(n)] for i in range(n)] + + for l in range(2, n + 1): # l is an interval length + for i in range(n - l + 1): + j = i + l - 1 + + dp[i][j] = sys.maxsize # set the value to "infinity" + sum[i][j] = sum[i][j - 1] + freqs[j] + + # Apply Knuth's optimization + # Loop without optimization: for r in range(i, j + 1): + for r in range(root[i][j - 1], root[i + 1][j] + 1): # r is a temporal root + left = dp[i][r - 1] if r != i else 0 # optimal cost for left subtree + right = dp[r + 1][j] if r != j else 0 # optimal cost for right subtree + cost = left + sum[i][j] + right + + if dp[i][j] > cost: + dp[i][j] = cost + root[i][j] = r + + print("Binary search tree nodes:") + for node in nodes: + print(node) + + print(f"\nThe cost of optimal BST for given tree nodes is {dp[0][n - 1]}.") + print_binary_search_tree(root, keys, 0, n - 1, -1, False) + + +def main(): + # A sample binary search tree + nodes = [Node(i, randint(1, 50)) for i in range(10, 0, -1)] + find_optimal_binary_search_tree(nodes) + + +if __name__ == "__main__": + main() diff --git a/dynamic_programming/rod_cutting.py b/dynamic_programming/rod_cutting.py index c3111dcfc8a1..26af71915833 100644 --- a/dynamic_programming/rod_cutting.py +++ b/dynamic_programming/rod_cutting.py @@ -1,57 +1,200 @@ -from typing import List - -def rod_cutting(prices: List[int],length: int) -> int: - """ - Given a rod of length n and array of prices that indicate price at each length. - Determine the maximum value obtainable by cutting up the rod and selling the pieces - - >>> rod_cutting([1,5,8,9],4) - 10 - >>> rod_cutting([1,1,1],3) - 3 - >>> rod_cutting([1,2,3], -1) - Traceback (most recent call last): - ValueError: Given integer must be greater than 1, not -1 - >>> rod_cutting([1,2,3], 3.2) - Traceback (most recent call last): - TypeError: Must be int, not float - >>> rod_cutting([], 3) - Traceback (most recent call last): - AssertionError: prices list is shorted than length: 3 - - - - Args: - prices: list indicating price at each length, where prices[0] = 0 indicating rod of zero length has no value - length: length of rod - - Returns: - Maximum revenue attainable by cutting up the rod in any way. - """ - - prices.insert(0, 0) - if not isinstance(length, int): - raise TypeError('Must be int, not {0}'.format(type(length).__name__)) - if length < 0: - raise ValueError('Given integer must be greater than 1, not {0}'.format(length)) - assert len(prices) - 1 >= length, "prices list is shorted than length: {0}".format(length) - - return rod_cutting_recursive(prices, length) - -def rod_cutting_recursive(prices: List[int],length: int) -> int: - #base case - if length == 0: - return 0 - value = float('-inf') - for firstCutLocation in range(1,length+1): - value = max(value, prices[firstCutLocation]+rod_cutting_recursive(prices,length - firstCutLocation)) - return value +""" +This module provides two implementations for the rod-cutting problem: +1. A naive recursive implementation which has an exponential runtime +2. Two dynamic programming implementations which have quadratic runtime + +The rod-cutting problem is the problem of finding the maximum possible revenue +obtainable from a rod of length ``n`` given a list of prices for each integral piece +of the rod. The maximum revenue can thus be obtained by cutting the rod and selling the +pieces separately or not cutting it at all if the price of it is the maximum obtainable. + +""" + + +def naive_cut_rod_recursive(n: int, prices: list): + """ + Solves the rod-cutting problem via naively without using the benefit of dynamic programming. + The results is the same sub-problems are solved several times leading to an exponential runtime + + Runtime: O(2^n) + + Arguments + ------- + n: int, the length of the rod + prices: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + + Returns + ------- + The maximum revenue obtainable for a rod of length n given the list of prices for each piece. + + Examples + -------- + >>> naive_cut_rod_recursive(4, [1, 5, 8, 9]) + 10 + >>> naive_cut_rod_recursive(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) + 30 + """ + + _enforce_args(n, prices) + if n == 0: + return 0 + max_revue = float("-inf") + for i in range(1, n + 1): + max_revue = max( + max_revue, prices[i - 1] + naive_cut_rod_recursive(n - i, prices) + ) + + return max_revue + + +def top_down_cut_rod(n: int, prices: list): + """ + Constructs a top-down dynamic programming solution for the rod-cutting problem + via memoization. This function serves as a wrapper for _top_down_cut_rod_recursive + + Runtime: O(n^2) + + Arguments + -------- + n: int, the length of the rod + prices: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + + Note + ---- + For convenience and because Python's lists using 0-indexing, length(max_rev) = n + 1, + to accommodate for the revenue obtainable from a rod of length 0. + + Returns + ------- + The maximum revenue obtainable for a rod of length n given the list of prices for each piece. + + Examples + ------- + >>> top_down_cut_rod(4, [1, 5, 8, 9]) + 10 + >>> top_down_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) + 30 + """ + _enforce_args(n, prices) + max_rev = [float("-inf") for _ in range(n + 1)] + return _top_down_cut_rod_recursive(n, prices, max_rev) + + +def _top_down_cut_rod_recursive(n: int, prices: list, max_rev: list): + """ + Constructs a top-down dynamic programming solution for the rod-cutting problem + via memoization. + + Runtime: O(n^2) + + Arguments + -------- + n: int, the length of the rod + prices: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + max_rev: list, the computed maximum revenue for a piece of rod. + ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i`` + + Returns + ------- + The maximum revenue obtainable for a rod of length n given the list of prices for each piece. + """ + if max_rev[n] >= 0: + return max_rev[n] + elif n == 0: + return 0 + else: + max_revenue = float("-inf") + for i in range(1, n + 1): + max_revenue = max( + max_revenue, + prices[i - 1] + _top_down_cut_rod_recursive(n - i, prices, max_rev), + ) + + max_rev[n] = max_revenue + + return max_rev[n] + + +def bottom_up_cut_rod(n: int, prices: list): + """ + Constructs a bottom-up dynamic programming solution for the rod-cutting problem + + Runtime: O(n^2) + + Arguments + ---------- + n: int, the maximum length of the rod. + prices: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + + Returns + ------- + The maximum revenue obtainable from cutting a rod of length n given + the prices for each piece of rod p. + + Examples + ------- + >>> bottom_up_cut_rod(4, [1, 5, 8, 9]) + 10 + >>> bottom_up_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) + 30 + """ + _enforce_args(n, prices) + + # length(max_rev) = n + 1, to accommodate for the revenue obtainable from a rod of length 0. + max_rev = [float("-inf") for _ in range(n + 1)] + max_rev[0] = 0 + + for i in range(1, n + 1): + max_revenue_i = max_rev[i] + for j in range(1, i + 1): + max_revenue_i = max(max_revenue_i, prices[j - 1] + max_rev[i - j]) + + max_rev[i] = max_revenue_i + + return max_rev[n] + + +def _enforce_args(n: int, prices: list): + """ + Basic checks on the arguments to the rod-cutting algorithms + + n: int, the length of the rod + prices: list, the price list for each piece of rod. + + Throws ValueError: + + if n is negative or there are fewer items in the price list than the length of the rod + """ + if n < 0: + raise ValueError(f"n must be greater than or equal to 0. Got n = {n}") + + if n > len(prices): + raise ValueError( + f"Each integral piece of rod must have a corresponding " + f"price. Got n = {n} but length of prices = {len(prices)}" + ) def main(): - assert rod_cutting([1,5,8,9,10,17,17,20,24,30],10) == 30 - # print(rod_cutting([],0)) + prices = [6, 10, 12, 15, 20, 23] + n = len(prices) + + # the best revenue comes from cutting the rod into 6 pieces, each + # of length 1 resulting in a revenue of 6 * 6 = 36. + expected_max_revenue = 36 + + max_rev_top_down = top_down_cut_rod(n, prices) + max_rev_bottom_up = bottom_up_cut_rod(n, prices) + max_rev_naive = naive_cut_rod_recursive(n, prices) + + assert expected_max_revenue == max_rev_top_down + assert max_rev_top_down == max_rev_bottom_up + assert max_rev_bottom_up == max_rev_naive -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/dynamic_programming/subset_generation.py b/dynamic_programming/subset_generation.py index 4b7a2bf87fd5..196b81c22045 100644 --- a/dynamic_programming/subset_generation.py +++ b/dynamic_programming/subset_generation.py @@ -1,39 +1,40 @@ -# python program to print all subset combination of n element in given set of r element . -#arr[] ---> Input Array -#data[] ---> Temporary array to store current combination +# Python program to print all subset combinations of n element in given set of r element. +# arr[] ---> Input Array +# data[] ---> Temporary array to store current combination # start & end ---> Staring and Ending indexes in arr[] # index ---> Current index in data[] -#r ---> Size of a combination to be printed -def combinationUtil(arr,n,r,index,data,i): -#Current combination is ready to be printed, -# print it - if(index == r): - for j in range(r): - print(data[j],end =" ") - print(" ") - return -# When no more elements are there to put in data[] - if(i >= n): - return -#current is included, put next at next -# location - data[index] = arr[i] - combinationUtil(arr,n,r,index+1,data,i+1) - # current is excluded, replace it with - # next (Note that i+1 is passed, but - # index is not changed) - combinationUtil(arr,n,r,index,data,i+1) - # The main function that prints all combinations - #of size r in arr[] of size n. This function - #mainly uses combinationUtil() -def printcombination(arr,n,r): -# A temporary array to store all combination -# one by one - data = [0]*r -#Print all combination using temprary -#array 'data[]' - combinationUtil(arr,n,r,0,data,0) +# r ---> Size of a combination to be printed +def combination_util(arr, n, r, index, data, i): + # Current combination is ready to be printed, + # print it + if index == r: + for j in range(r): + print(data[j], end=" ") + print(" ") + return + # When no more elements are there to put in data[] + if i >= n: + return + # current is included, put next at next location + data[index] = arr[i] + combination_util(arr, n, r, index + 1, data, i + 1) + # current is excluded, replace it with + # next (Note that i+1 is passed, but + # index is not changed) + combination_util(arr, n, r, index, data, i + 1) + # The main function that prints all combinations + # of size r in arr[] of size n. This function + # mainly uses combinationUtil() + + +def print_combination(arr, n, r): + # A temporary array to store all combination one by one + data = [0] * r + # Print all combination using temporary array 'data[]' + combination_util(arr, n, r, 0, data, 0) + + # Driver function to check for above function -arr = [10,20,30,40,50] -printcombination(arr,len(arr),3) -#This code is contributed by Ambuj sahu +arr = [10, 20, 30, 40, 50] +print_combination(arr, len(arr), 3) +# This code is contributed by Ambuj sahu diff --git a/dynamic_programming/sum_of_subset.py b/dynamic_programming/sum_of_subset.py index f6509a259c5d..9394d29dabc0 100644 --- a/dynamic_programming/sum_of_subset.py +++ b/dynamic_programming/sum_of_subset.py @@ -1,34 +1,36 @@ def isSumSubset(arr, arrLen, requiredSum): - + """ + >>> isSumSubset([2, 4, 6, 8], 4, 5) + False + >>> isSumSubset([2, 4, 6, 8], 4, 14) + True + """ # a subset value says 1 if that subset sum can be formed else 0 - #initially no subsets can be formed hence False/0 - subset = ([[False for i in range(requiredSum + 1)] for i in range(arrLen + 1)]) + # initially no subsets can be formed hence False/0 + subset = [[False for i in range(requiredSum + 1)] for i in range(arrLen + 1)] - #for each arr value, a sum of zero(0) can be formed by not taking any element hence True/1 + # for each arr value, a sum of zero(0) can be formed by not taking any element hence True/1 for i in range(arrLen + 1): subset[i][0] = True - #sum is not zero and set is empty then false + # sum is not zero and set is empty then false for i in range(1, requiredSum + 1): subset[0][i] = False for i in range(1, arrLen + 1): for j in range(1, requiredSum + 1): - if arr[i-1]>j: - subset[i][j] = subset[i-1][j] - if arr[i-1]<=j: - subset[i][j] = (subset[i-1][j] or subset[i-1][j-arr[i-1]]) + if arr[i - 1] > j: + subset[i][j] = subset[i - 1][j] + if arr[i - 1] <= j: + subset[i][j] = subset[i - 1][j] or subset[i - 1][j - arr[i - 1]] - #uncomment to print the subset + # uncomment to print the subset # for i in range(arrLen+1): # print(subset[i]) + print(subset[arrLen][requiredSum]) + - return subset[arrLen][requiredSum] +if __name__ == "__main__": + import doctest -arr = [2, 4, 6, 8] -requiredSum = 5 -arrLen = len(arr) -if isSumSubset(arr, arrLen, requiredSum): - print("Found a subset with required sum") -else: - print("No subset with required sum") \ No newline at end of file + doctest.testmod() diff --git a/file_transfer/mytext.txt b/file_transfer/mytext.txt new file mode 100644 index 000000000000..54cfa7f766c7 --- /dev/null +++ b/file_transfer/mytext.txt @@ -0,0 +1,6 @@ +Hello +This is sample data +«küßî» +“ЌύБЇ” +😀😉 +😋 diff --git a/file_transfer/receive_file.py b/file_transfer/receive_file.py new file mode 100644 index 000000000000..cfba6ed88484 --- /dev/null +++ b/file_transfer/receive_file.py @@ -0,0 +1,23 @@ +if __name__ == "__main__": + import socket # Import socket module + + sock = socket.socket() # Create a socket object + host = socket.gethostname() # Get local machine name + port = 12312 + + sock.connect((host, port)) + sock.send(b"Hello server!") + + with open("Received_file", "wb") as out_file: + print("File opened") + print("Receiving data...") + while True: + data = sock.recv(1024) + print(f"data={data}") + if not data: + break + out_file.write(data) # Write data to a file + + print("Successfully got the file") + sock.close() + print("Connection closed") diff --git a/file_transfer/send_file.py b/file_transfer/send_file.py new file mode 100644 index 000000000000..6494114a9072 --- /dev/null +++ b/file_transfer/send_file.py @@ -0,0 +1,38 @@ +if __name__ == "__main__": + import socket # Import socket module + + ONE_CONNECTION_ONLY = ( + True # Set this to False if you wish to continuously accept connections + ) + + filename = "mytext.txt" + port = 12312 # Reserve a port for your service. + sock = socket.socket() # Create a socket object + host = socket.gethostname() # Get local machine name + sock.bind((host, port)) # Bind to the port + sock.listen(5) # Now wait for client connection. + + print("Server listening....") + + while True: + conn, addr = sock.accept() # Establish connection with client. + print(f"Got connection from {addr}") + data = conn.recv(1024) + print(f"Server received {data}") + + with open(filename, "rb") as in_file: + data = in_file.read(1024) + while data: + conn.send(data) + print(f"Sent {data!r}") + data = in_file.read(1024) + + print("Done sending") + conn.close() + if ( + ONE_CONNECTION_ONLY + ): # This is to make sure that the program doesn't hang while testing + break + + sock.shutdown(1) + sock.close() diff --git a/file_transfer_protocol/ftp_client_server.py b/file_transfer_protocol/ftp_client_server.py deleted file mode 100644 index 414c336dee9f..000000000000 --- a/file_transfer_protocol/ftp_client_server.py +++ /dev/null @@ -1,57 +0,0 @@ -# server - -import socket # Import socket module - -port = 60000 # Reserve a port for your service. -s = socket.socket() # Create a socket object -host = socket.gethostname() # Get local machine name -s.bind((host, port)) # Bind to the port -s.listen(5) # Now wait for client connection. - -print('Server listening....') - -while True: - conn, addr = s.accept() # Establish connection with client. - print('Got connection from', addr) - data = conn.recv(1024) - print('Server received', repr(data)) - - filename = 'mytext.txt' - with open(filename, 'rb') as f: - in_data = f.read(1024) - while in_data: - conn.send(in_data) - print('Sent ', repr(in_data)) - in_data = f.read(1024) - - print('Done sending') - conn.send('Thank you for connecting') - conn.close() - - -# client side server - -import socket # Import socket module - -s = socket.socket() # Create a socket object -host = socket.gethostname() # Get local machine name -port = 60000 # Reserve a port for your service. - -s.connect((host, port)) -s.send("Hello server!") - -with open('received_file', 'wb') as f: - print('file opened') - while True: - print('receiving data...') - data = s.recv(1024) - print('data=%s', (data)) - if not data: - break - # write data to a file - f.write(data) - -f.close() -print('Successfully get the file') -s.close() -print('connection closed') diff --git a/file_transfer_protocol/ftp_send_receive.py b/file_transfer_protocol/ftp_send_receive.py deleted file mode 100644 index 6a9819ef3f21..000000000000 --- a/file_transfer_protocol/ftp_send_receive.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -File transfer protocol used to send and receive files using FTP server. -Use credentials to provide access to the FTP client - -Note: Do not use root username & password for security reasons -Create a seperate user and provide access to a home directory of the user -Use login id and password of the user created -cwd here stands for current working directory -""" - -from ftplib import FTP -ftp = FTP('xxx.xxx.x.x') # Enter the ip address or the domain name here -ftp.login(user='username', passwd='password') -ftp.cwd('/Enter the directory here/') - -""" -The file which will be received via the FTP server -Enter the location of the file where the file is received -""" - -def ReceiveFile(): - FileName = 'example.txt' """ Enter the location of the file """ - with open(FileName, 'wb') as LocalFile: - ftp.retrbinary('RETR ' + FileName, LocalFile.write, 1024) - ftp.quit() - -""" -The file which will be sent via the FTP server -The file send will be send to the current working directory -""" - -def SendFile(): - FileName = 'example.txt' """ Enter the name of the file """ - with open(FileName, 'rb') as LocalFile: - ftp.storbinary('STOR ' + FileName, LocalFile) - ftp.quit() diff --git a/fuzzy_logic/fuzzy_operations.py b/fuzzy_logic/fuzzy_operations.py new file mode 100644 index 000000000000..34dd9c029be7 --- /dev/null +++ b/fuzzy_logic/fuzzy_operations.py @@ -0,0 +1,102 @@ +"""README, Author - Jigyasa Gandhi(mailto:jigsgandhi97@gmail.com) +Requirements: + - scikit-fuzzy + - numpy + - matplotlib +Python: + - 3.5 +""" +import numpy as np +import skfuzzy as fuzz + + +if __name__ == "__main__": + # Create universe of discourse in Python using linspace () + X = np.linspace(start=0, stop=75, num=75, endpoint=True, retstep=False) + + # Create two fuzzy sets by defining any membership function (trapmf(), gbellmf(),gaussmf(), etc). + abc1 = [0, 25, 50] + abc2 = [25, 50, 75] + young = fuzz.membership.trimf(X, abc1) + middle_aged = fuzz.membership.trimf(X, abc2) + + # Compute the different operations using inbuilt functions. + one = np.ones(75) + zero = np.zeros((75,)) + # 1. Union = max(µA(x), µB(x)) + union = fuzz.fuzzy_or(X, young, X, middle_aged)[1] + # 2. Intersection = min(µA(x), µB(x)) + intersection = fuzz.fuzzy_and(X, young, X, middle_aged)[1] + # 3. Complement (A) = (1- min(µA(x)) + complement_a = fuzz.fuzzy_not(young) + # 4. Difference (A/B) = min(µA(x),(1- µB(x))) + difference = fuzz.fuzzy_and(X, young, X, fuzz.fuzzy_not(middle_aged)[1])[1] + # 5. Algebraic Sum = [µA(x) + µB(x) – (µA(x) * µB(x))] + alg_sum = young + middle_aged - (young * middle_aged) + # 6. Algebraic Product = (µA(x) * µB(x)) + alg_product = young * middle_aged + # 7. Bounded Sum = min[1,(µA(x), µB(x))] + bdd_sum = fuzz.fuzzy_and(X, one, X, young + middle_aged)[1] + # 8. Bounded difference = min[0,(µA(x), µB(x))] + bdd_difference = fuzz.fuzzy_or(X, zero, X, young - middle_aged)[1] + + # max-min composition + # max-product composition + + # Plot each set A, set B and each operation result using plot() and subplot(). + import matplotlib.pyplot as plt + + plt.figure() + + plt.subplot(4, 3, 1) + plt.plot(X, young) + plt.title("Young") + plt.grid(True) + + plt.subplot(4, 3, 2) + plt.plot(X, middle_aged) + plt.title("Middle aged") + plt.grid(True) + + plt.subplot(4, 3, 3) + plt.plot(X, union) + plt.title("union") + plt.grid(True) + + plt.subplot(4, 3, 4) + plt.plot(X, intersection) + plt.title("intersection") + plt.grid(True) + + plt.subplot(4, 3, 5) + plt.plot(X, complement_a) + plt.title("complement_a") + plt.grid(True) + + plt.subplot(4, 3, 6) + plt.plot(X, difference) + plt.title("difference a/b") + plt.grid(True) + + plt.subplot(4, 3, 7) + plt.plot(X, alg_sum) + plt.title("alg_sum") + plt.grid(True) + + plt.subplot(4, 3, 8) + plt.plot(X, alg_product) + plt.title("alg_product") + plt.grid(True) + + plt.subplot(4, 3, 9) + plt.plot(X, bdd_sum) + plt.title("bdd_sum") + plt.grid(True) + + plt.subplot(4, 3, 10) + plt.plot(X, bdd_difference) + plt.title("bdd_difference") + plt.grid(True) + + plt.subplots_adjust(hspace=0.5) + plt.show() diff --git a/geodesy/haversine_distance.py b/geodesy/haversine_distance.py new file mode 100644 index 000000000000..de8ac7f88302 --- /dev/null +++ b/geodesy/haversine_distance.py @@ -0,0 +1,56 @@ +from math import asin, atan, cos, radians, sin, sqrt, tan + + +def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """ + Calculate great circle distance between two points in a sphere, + given longitudes and latitudes https://en.wikipedia.org/wiki/Haversine_formula + + We know that the globe is "sort of" spherical, so a path between two points + isn't exactly a straight line. We need to account for the Earth's curvature + when calculating distance from point A to B. This effect is negligible for + small distances but adds up as distance increases. The Haversine method treats + the earth as a sphere which allows us to "project" the two points A and B + onto the surface of that sphere and approximate the spherical distance between + them. Since the Earth is not a perfect sphere, other methods which model the + Earth's ellipsoidal nature are more accurate but a quick and modifiable + computation like Haversine can be handy for shorter range distances. + + Args: + lat1, lon1: latitude and longitude of coordinate 1 + lat2, lon2: latitude and longitude of coordinate 2 + Returns: + geographical distance between two points in metres + >>> from collections import namedtuple + >>> point_2d = namedtuple("point_2d", "lat lon") + >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227) + >>> YOSEMITE = point_2d(37.864742, -119.537521) + >>> f"{haversine_distance(*SAN_FRANCISCO, *YOSEMITE):0,.0f} meters" + '254,352 meters' + """ + # CONSTANTS per WGS84 https://en.wikipedia.org/wiki/World_Geodetic_System + # Distance in metres(m) + AXIS_A = 6378137.0 + AXIS_B = 6356752.314245 + RADIUS = 6378137 + # Equation parameters + # Equation https://en.wikipedia.org/wiki/Haversine_formula#Formulation + flattening = (AXIS_A - AXIS_B) / AXIS_A + phi_1 = atan((1 - flattening) * tan(radians(lat1))) + phi_2 = atan((1 - flattening) * tan(radians(lat2))) + lambda_1 = radians(lon1) + lambda_2 = radians(lon2) + # Equation + sin_sq_phi = sin((phi_2 - phi_1) / 2) + sin_sq_lambda = sin((lambda_2 - lambda_1) / 2) + # Square both values + sin_sq_phi *= sin_sq_phi + sin_sq_lambda *= sin_sq_lambda + h_value = sqrt(sin_sq_phi + (cos(phi_1) * cos(phi_2) * sin_sq_lambda)) + return 2 * RADIUS * asin(h_value) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/geodesy/lamberts_ellipsoidal_distance.py b/geodesy/lamberts_ellipsoidal_distance.py new file mode 100644 index 000000000000..224b9404a5b7 --- /dev/null +++ b/geodesy/lamberts_ellipsoidal_distance.py @@ -0,0 +1,83 @@ +from math import atan, cos, radians, sin, tan +from haversine_distance import haversine_distance + + +def lamberts_ellipsoidal_distance( + lat1: float, lon1: float, lat2: float, lon2: float +) -> float: + + """ + Calculate the shortest distance along the surface of an ellipsoid between + two points on the surface of earth given longitudes and latitudes + https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines + + NOTE: This algorithm uses geodesy/haversine_distance.py to compute central angle, sigma + + Representing the earth as an ellipsoid allows us to approximate distances between points + on the surface much better than a sphere. Ellipsoidal formulas treat the Earth as an + oblate ellipsoid which means accounting for the flattening that happens at the North + and South poles. Lambert's formulae provide accuracy on the order of 10 meteres over + thousands of kilometeres. Other methods can provide millimeter-level accuracy but this + is a simpler method to calculate long range distances without increasing computational + intensity. + + Args: + lat1, lon1: latitude and longitude of coordinate 1 + lat2, lon2: latitude and longitude of coordinate 2 + Returns: + geographical distance between two points in metres + + >>> from collections import namedtuple + >>> point_2d = namedtuple("point_2d", "lat lon") + >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227) + >>> YOSEMITE = point_2d(37.864742, -119.537521) + >>> NEW_YORK = point_2d(40.713019, -74.012647) + >>> VENICE = point_2d(45.443012, 12.313071) + >>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *YOSEMITE):0,.0f} meters" + '254,351 meters' + >>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *NEW_YORK):0,.0f} meters" + '4,138,992 meters' + >>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *VENICE):0,.0f} meters" + '9,737,326 meters' + """ + + # CONSTANTS per WGS84 https://en.wikipedia.org/wiki/World_Geodetic_System + # Distance in metres(m) + AXIS_A = 6378137.0 + AXIS_B = 6356752.314245 + EQUATORIAL_RADIUS = 6378137 + + # Equation Parameters + # https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines + flattening = (AXIS_A - AXIS_B) / AXIS_A + # Parametric latitudes https://en.wikipedia.org/wiki/Latitude#Parametric_(or_reduced)_latitude + b_lat1 = atan((1 - flattening) * tan(radians(lat1))) + b_lat2 = atan((1 - flattening) * tan(radians(lat2))) + + # Compute central angle between two points + # using haversine theta. sigma = haversine_distance / equatorial radius + sigma = haversine_distance(lat1, lon1, lat2, lon2) / EQUATORIAL_RADIUS + + # Intermediate P and Q values + P_value = (b_lat1 + b_lat2) / 2 + Q_value = (b_lat2 - b_lat1) / 2 + + # Intermediate X value + # X = (sigma - sin(sigma)) * sin^2Pcos^2Q / cos^2(sigma/2) + X_numerator = (sin(P_value) ** 2) * (cos(Q_value) ** 2) + X_demonimator = cos(sigma / 2) ** 2 + X_value = (sigma - sin(sigma)) * (X_numerator / X_demonimator) + + # Intermediate Y value + # Y = (sigma + sin(sigma)) * cos^2Psin^2Q / sin^2(sigma/2) + Y_numerator = (cos(P_value) ** 2) * (sin(Q_value) ** 2) + Y_denominator = sin(sigma / 2) ** 2 + Y_value = (sigma + sin(sigma)) * (Y_numerator / Y_denominator) + + return EQUATORIAL_RADIUS * (sigma - ((flattening / 2) * (X_value + Y_value))) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphics/bezier_curve.py b/graphics/bezier_curve.py new file mode 100644 index 000000000000..512efadf86ee --- /dev/null +++ b/graphics/bezier_curve.py @@ -0,0 +1,114 @@ +# https://en.wikipedia.org/wiki/B%C3%A9zier_curve +# https://www.tutorialspoint.com/computer_graphics/computer_graphics_curves.htm + +from typing import List, Tuple +from scipy.special import comb + + +class BezierCurve: + """ + Bezier curve is a weighted sum of a set of control points. + Generate Bezier curves from a given set of control points. + This implementation works only for 2d coordinates in the xy plane. + """ + + def __init__(self, list_of_points: List[Tuple[float, float]]): + """ + list_of_points: Control points in the xy plane on which to interpolate. These + points control the behavior (shape) of the Bezier curve. + """ + self.list_of_points = list_of_points + # Degree determines the flexibility of the curve. + # Degree = 1 will produce a straight line. + self.degree = len(list_of_points) - 1 + + def basis_function(self, t: float) -> List[float]: + """ + The basis function determines the weight of each control point at time t. + t: time value between 0 and 1 inclusive at which to evaluate the basis of + the curve. + returns the x, y values of basis function at time t + + >>> curve = BezierCurve([(1,1), (1,2)]) + >>> curve.basis_function(0) + [1.0, 0.0] + >>> curve.basis_function(1) + [0.0, 1.0] + """ + assert 0 <= t <= 1, "Time t must be between 0 and 1." + output_values: List[float] = [] + for i in range(len(self.list_of_points)): + # basis function for each i + output_values.append( + comb(self.degree, i) * ((1 - t) ** (self.degree - i)) * (t ** i) + ) + # the basis must sum up to 1 for it to produce a valid Bezier curve. + assert round(sum(output_values), 5) == 1 + return output_values + + def bezier_curve_function(self, t: float) -> Tuple[float, float]: + """ + The function to produce the values of the Bezier curve at time t. + t: the value of time t at which to evaluate the Bezier function + Returns the x, y coordinates of the Bezier curve at time t. + The first point in the curve is when t = 0. + The last point in the curve is when t = 1. + + >>> curve = BezierCurve([(1,1), (1,2)]) + >>> curve.bezier_curve_function(0) + (1.0, 1.0) + >>> curve.bezier_curve_function(1) + (1.0, 2.0) + """ + + assert 0 <= t <= 1, "Time t must be between 0 and 1." + + basis_function = self.basis_function(t) + x = 0.0 + y = 0.0 + for i in range(len(self.list_of_points)): + # For all points, sum up the product of i-th basis function and i-th point. + x += basis_function[i] * self.list_of_points[i][0] + y += basis_function[i] * self.list_of_points[i][1] + return (x, y) + + def plot_curve(self, step_size: float = 0.01): + """ + Plots the Bezier curve using matplotlib plotting capabilities. + step_size: defines the step(s) at which to evaluate the Bezier curve. + The smaller the step size, the finer the curve produced. + """ + import matplotlib.pyplot as plt + + to_plot_x: List[float] = [] # x coordinates of points to plot + to_plot_y: List[float] = [] # y coordinates of points to plot + + t = 0.0 + while t <= 1: + value = self.bezier_curve_function(t) + to_plot_x.append(value[0]) + to_plot_y.append(value[1]) + t += step_size + + x = [i[0] for i in self.list_of_points] + y = [i[1] for i in self.list_of_points] + + plt.plot( + to_plot_x, + to_plot_y, + color="blue", + label="Curve of Degree " + str(self.degree), + ) + plt.scatter(x, y, color="red", label="Control Points") + plt.legend() + plt.show() + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + BezierCurve([(1, 2), (3, 5)]).plot_curve() # degree 1 + BezierCurve([(0, 0), (5, 5), (5, 0)]).plot_curve() # degree 2 + BezierCurve([(0, 0), (5, 5), (5, 0), (2.5, -2.5)]).plot_curve() # degree 3 diff --git a/graphs/a_star.py b/graphs/a_star.py index 584222e6f62b..a5d59626b0bc 100644 --- a/graphs/a_star.py +++ b/graphs/a_star.py @@ -1,44 +1,45 @@ -from __future__ import print_function +grid = [ + [0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], # 0 are free path whereas 1's are obstacles + [0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 0], +] -grid = [[0, 1, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0],#0 are free path whereas 1's are obstacles - [0, 1, 0, 0, 0, 0], - [0, 1, 0, 0, 1, 0], - [0, 0, 0, 0, 1, 0]] - -''' +""" heuristic = [[9, 8, 7, 6, 5, 4], [8, 7, 6, 5, 4, 3], [7, 6, 5, 4, 3, 2], [6, 5, 4, 3, 2, 1], - [5, 4, 3, 2, 1, 0]]''' + [5, 4, 3, 2, 1, 0]]""" init = [0, 0] -goal = [len(grid)-1, len(grid[0])-1] #all coordinates are given in format [y,x] +goal = [len(grid) - 1, len(grid[0]) - 1] # all coordinates are given in format [y,x] cost = 1 -#the cost map which pushes the path closer to the goal +# the cost map which pushes the path closer to the goal heuristic = [[0 for row in range(len(grid[0]))] for col in range(len(grid))] -for i in range(len(grid)): - for j in range(len(grid[0])): +for i in range(len(grid)): + for j in range(len(grid[0])): heuristic[i][j] = abs(i - goal[0]) + abs(j - goal[1]) if grid[i][j] == 1: - heuristic[i][j] = 99 #added extra penalty in the heuristic map + heuristic[i][j] = 99 # added extra penalty in the heuristic map -#the actions we can take -delta = [[-1, 0 ], # go up - [ 0, -1], # go left - [ 1, 0 ], # go down - [ 0, 1 ]] # go right +# the actions we can take +delta = [[-1, 0], [0, -1], [1, 0], [0, 1]] # go up # go left # go down # go right -#function to search the path -def search(grid,init,goal,cost,heuristic): +# function to search the path +def search(grid, init, goal, cost, heuristic): - closed = [[0 for col in range(len(grid[0]))] for row in range(len(grid))]# the referrence grid + closed = [ + [0 for col in range(len(grid[0]))] for row in range(len(grid)) + ] # the reference grid closed[init[0]][init[1]] = 1 - action = [[0 for col in range(len(grid[0]))] for row in range(len(grid))]#the action grid + action = [ + [0 for col in range(len(grid[0]))] for row in range(len(grid)) + ] # the action grid x = init[0] y = init[1] @@ -47,29 +48,26 @@ def search(grid,init,goal,cost,heuristic): cell = [[f, g, x, y]] found = False # flag that is set when search is complete - resign = False # flag set if we can't find expand + resign = False # flag set if we can't find expand while not found and not resign: if len(cell) == 0: - resign = True return "FAIL" else: - cell.sort()#to choose the least costliest action so as to move closer to the goal + cell.sort() # to choose the least costliest action so as to move closer to the goal cell.reverse() next = cell.pop() x = next[2] y = next[3] g = next[1] - f = next[0] - if x == goal[0] and y == goal[1]: found = True else: - for i in range(len(delta)):#to try out different valid actions + for i in range(len(delta)): # to try out different valid actions x2 = x + delta[i][0] y2 = y + delta[i][1] - if x2 >= 0 and x2 < len(grid) and y2 >=0 and y2 < len(grid[0]): + if x2 >= 0 and x2 < len(grid) and y2 >= 0 and y2 < len(grid[0]): if closed[x2][y2] == 0 and grid[x2][y2] == 0: g2 = g + cost f2 = g2 + heuristic[x2][y2] @@ -79,7 +77,7 @@ def search(grid,init,goal,cost,heuristic): invpath = [] x = goal[0] y = goal[1] - invpath.append([x, y])#we get the reverse path from here + invpath.append([x, y]) # we get the reverse path from here while x != init[0] or y != init[1]: x2 = x - delta[action[x][y]][0] y2 = y - delta[action[x][y]][1] @@ -89,14 +87,14 @@ def search(grid,init,goal,cost,heuristic): path = [] for i in range(len(invpath)): - path.append(invpath[len(invpath) - 1 - i]) + path.append(invpath[len(invpath) - 1 - i]) print("ACTION MAP") for i in range(len(action)): print(action[i]) - + return path - -a = search(grid,init,goal,cost,heuristic) -for i in range(len(a)): - print(a[i]) + +a = search(grid, init, goal, cost, heuristic) +for i in range(len(a)): + print(a[i]) diff --git a/graphs/articulation_points.py b/graphs/articulation_points.py index 1173c4ea373c..3ecc829946e8 100644 --- a/graphs/articulation_points.py +++ b/graphs/articulation_points.py @@ -33,12 +33,23 @@ def dfs(root, at, parent, outEdgeCount): if not visited[i]: outEdgeCount = 0 outEdgeCount = dfs(i, i, -1, outEdgeCount) - isArt[i] = (outEdgeCount > 1) + isArt[i] = outEdgeCount > 1 for x in range(len(isArt)): if isArt[x] == True: print(x) + # Adjacency list of graph -l = {0:[1,2], 1:[0,2], 2:[0,1,3,5], 3:[2,4], 4:[3], 5:[2,6,8], 6:[5,7], 7:[6,8], 8:[5,7]} +l = { + 0: [1, 2], + 1: [0, 2], + 2: [0, 1, 3, 5], + 3: [2, 4], + 4: [3], + 5: [2, 6, 8], + 6: [5, 7], + 7: [6, 8], + 8: [5, 7], +} computeAP(l) diff --git a/graphs/basic_graphs.py b/graphs/basic_graphs.py index ee63ca995de6..8cdde6abc819 100644 --- a/graphs/basic_graphs.py +++ b/graphs/basic_graphs.py @@ -1,23 +1,10 @@ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - - if __name__ == "__main__": # Accept No. of Nodes and edges - n, m = map(int, raw_input().split(" ")) + n, m = map(int, input().split(" ")) # Initialising Dictionary of edges g = {} - for i in xrange(n): + for i in range(n): g[i + 1] = [] """ @@ -25,8 +12,8 @@ Accepting edges of Unweighted Directed Graphs ---------------------------------------------------------------------------- """ - for _ in xrange(m): - x, y = map(int, raw_input().strip().split(" ")) + for _ in range(m): + x, y = map(int, input().strip().split(" ")) g[x].append(y) """ @@ -34,8 +21,8 @@ Accepting edges of Unweighted Undirected Graphs ---------------------------------------------------------------------------- """ - for _ in xrange(m): - x, y = map(int, raw_input().strip().split(" ")) + for _ in range(m): + x, y = map(int, input().strip().split(" ")) g[x].append(y) g[y].append(x) @@ -44,8 +31,8 @@ Accepting edges of Weighted Undirected Graphs ---------------------------------------------------------------------------- """ - for _ in xrange(m): - x, y, r = map(int, raw_input().strip().split(" ")) + for _ in range(m): + x, y, r = map(int, input().strip().split(" ")) g[x].append([y, r]) g[y].append([x, r]) @@ -61,7 +48,7 @@ def dfs(G, s): - vis, S = set([s]), [s] + vis, S = {s}, [s] print(s) while S: flag = 0 @@ -82,14 +69,14 @@ def dfs(G, s): Args : G - Dictionary of edges s - Starting Node Vars : vis - Set of visited nodes - Q - Traveral Stack + Q - Traversal Stack -------------------------------------------------------------------------------- """ from collections import deque def bfs(G, s): - vis, Q = set([s]), deque([s]) + vis, Q = {s}, deque([s]) print(s) while Q: u = Q.popleft() @@ -141,7 +128,9 @@ def dijk(G, s): from collections import deque -def topo(G, ind=None, Q=[1]): +def topo(G, ind=None, Q=None): + if Q is None: + Q = [1] if ind is None: ind = [0] * (len(G) + 1) # SInce oth Index is ignored for u in G: @@ -170,10 +159,10 @@ def topo(G, ind=None, Q=[1]): def adjm(): - n = raw_input().strip() + n = input().strip() a = [] - for i in xrange(n): - a.append(map(int, raw_input().strip().split())) + for i in range(n): + a.append(map(int, input().strip().split())) return a, n @@ -193,10 +182,10 @@ def adjm(): def floy(A_and_n): (A, n) = A_and_n dist = list(A) - path = [[0] * n for i in xrange(n)] - for k in xrange(n): - for i in xrange(n): - for j in xrange(n): + path = [[0] * n for i in range(n)] + for k in range(n): + for i in range(n): + for j in range(n): if dist[i][j] > dist[i][k] + dist[k][j]: dist[i][j] = dist[i][k] + dist[k][j] path[i][k] = k @@ -245,10 +234,10 @@ def prim(G, s): def edglist(): - n, m = map(int, raw_input().split(" ")) + n, m = map(int, input().split(" ")) l = [] - for i in xrange(m): - l.append(map(int, raw_input().split(' '))) + for i in range(m): + l.append(map(int, input().split(" "))) return l, n @@ -266,16 +255,16 @@ def krusk(E_and_n): # Sort edges on the basis of distance (E, n) = E_and_n E.sort(reverse=True, key=lambda x: x[2]) - s = [set([i]) for i in range(1, n + 1)] + s = [{i} for i in range(1, n + 1)] while True: if len(s) == 1: break print(s) x = E.pop() - for i in xrange(len(s)): + for i in range(len(s)): if x[0] in s[i]: break - for j in xrange(len(s)): + for j in range(len(s)): if x[1] in s[j]: if i == j: break diff --git a/graphs/bellman_ford.py b/graphs/bellman_ford.py index f49157230054..807e0b0fcdb9 100644 --- a/graphs/bellman_ford.py +++ b/graphs/bellman_ford.py @@ -1,37 +1,39 @@ -from __future__ import print_function +from typing import Dict, List + def printDist(dist, V): - print("\nVertex Distance") - for i in range(V): - if dist[i] != float('inf') : - print(i,"\t",int(dist[i]),end = "\t") - else: - print(i,"\t","INF",end="\t") - print() + print("Vertex Distance") + distances = ("INF" if d == float("inf") else d for d in dist) + print("\t".join(f"{i}\t{d}" for i, d in enumerate(distances))) -def BellmanFord(graph, V, E, src): - mdist=[float('inf') for i in range(V)] - mdist[src] = 0.0 - for i in range(V-1): - for j in range(V): - u = graph[j]["src"] - v = graph[j]["dst"] - w = graph[j]["weight"] +def BellmanFord(graph: List[Dict[str, int]], V: int, E: int, src: int) -> int: + """ + Returns shortest paths from a vertex src to all + other vertices. + """ + mdist = [float("inf") for i in range(V)] + mdist[src] = 0.0 - if mdist[u] != float('inf') and mdist[u] + w < mdist[v]: - mdist[v] = mdist[u] + w - for j in range(V): - u = graph[j]["src"] - v = graph[j]["dst"] - w = graph[j]["weight"] + for i in range(V - 1): + for j in range(E): + u = graph[j]["src"] + v = graph[j]["dst"] + w = graph[j]["weight"] - if mdist[u] != float('inf') and mdist[u] + w < mdist[v]: - print("Negative cycle found. Solution not possible.") - return + if mdist[u] != float("inf") and mdist[u] + w < mdist[v]: + mdist[v] = mdist[u] + w + for j in range(E): + u = graph[j]["src"] + v = graph[j]["dst"] + w = graph[j]["weight"] - printDist(mdist, V) + if mdist[u] != float("inf") and mdist[u] + w < mdist[v]: + print("Negative cycle found. Solution not possible.") + return + printDist(mdist, V) + return src if __name__ == "__main__": @@ -40,15 +42,15 @@ def BellmanFord(graph, V, E, src): graph = [dict() for j in range(E)] - for i in range(V): - graph[i][i] = 0.0 + for i in range(E): + graph[i][i] = 0.0 for i in range(E): - print("\nEdge ",i+1) - src = int(input("Enter source:").strip()) - dst = int(input("Enter destination:").strip()) - weight = float(input("Enter weight:").strip()) - graph[i] = {"src": src,"dst": dst, "weight": weight} + print("\nEdge ", i + 1) + src = int(input("Enter source:").strip()) + dst = int(input("Enter destination:").strip()) + weight = float(input("Enter weight:").strip()) + graph[i] = {"src": src, "dst": dst, "weight": weight} gsrc = int(input("\nEnter shortest path source:").strip()) BellmanFord(graph, V, E, gsrc) diff --git a/graphs/bfs.py b/graphs/bfs.py index 6bbdd9e25435..9d9b1ac037d9 100644 --- a/graphs/bfs.py +++ b/graphs/bfs.py @@ -16,10 +16,21 @@ """ -import collections +G = { + "A": ["B", "C"], + "B": ["A", "D", "E"], + "C": ["A", "F"], + "D": ["B"], + "E": ["B", "F"], + "F": ["C", "E"], +} def bfs(graph, start): + """ + >>> ''.join(sorted(bfs(G, 'A'))) + 'ABCDEF' + """ explored, queue = set(), [start] # collections.deque([start]) explored.add(start) while queue: @@ -31,11 +42,5 @@ def bfs(graph, start): return explored -G = {'A': ['B', 'C'], - 'B': ['A', 'D', 'E'], - 'C': ['A', 'F'], - 'D': ['B'], - 'E': ['B', 'F'], - 'F': ['C', 'E']} - -print(bfs(G, 'A')) +if __name__ == "__main__": + print(bfs(G, "A")) diff --git a/graphs/bfs_shortest_path.py b/graphs/bfs_shortest_path.py index 5853351a53a3..c3664796e677 100644 --- a/graphs/bfs_shortest_path.py +++ b/graphs/bfs_shortest_path.py @@ -1,21 +1,47 @@ -graph = {'A': ['B', 'C', 'E'], - 'B': ['A','D', 'E'], - 'C': ['A', 'F', 'G'], - 'D': ['B'], - 'E': ['A', 'B','D'], - 'F': ['C'], - 'G': ['C']} - -def bfs_shortest_path(graph, start, goal): +"""Breadth-first search shortest path implementations. + + doctest: + python -m doctest -v bfs_shortest_path.py + + Manual test: + python bfs_shortest_path.py +""" +graph = { + "A": ["B", "C", "E"], + "B": ["A", "D", "E"], + "C": ["A", "F", "G"], + "D": ["B"], + "E": ["A", "B", "D"], + "F": ["C"], + "G": ["C"], +} + + +def bfs_shortest_path(graph: dict, start, goal) -> str: + """Find shortest path between `start` and `goal` nodes. + + Args: + graph (dict): node/list of neighboring nodes key/value pairs. + start: start node. + goal: target node. + + Returns: + Shortest path between `start` and `goal` nodes as a string of nodes. + 'Not found' string if no path found. + + Example: + >>> bfs_shortest_path(graph, "G", "D") + ['G', 'C', 'A', 'B', 'D'] + """ # keep track of explored nodes explored = [] # keep track of all the paths to be checked queue = [[start]] - + # return path if start is goal if start == goal: return "That was easy! Start = goal" - + # keeps looping until all possible paths have been checked while queue: # pop the first path from the queue @@ -33,11 +59,56 @@ def bfs_shortest_path(graph, start, goal): # return path if neighbour is goal if neighbour == goal: return new_path - + # mark node as explored explored.append(node) - + # in case there's no path between the 2 nodes return "So sorry, but a connecting path doesn't exist :(" - -bfs_shortest_path(graph, 'G', 'D') # returns ['G', 'C', 'A', 'B', 'D'] + + +def bfs_shortest_path_distance(graph: dict, start, target) -> int: + """Find shortest path distance between `start` and `target` nodes. + + Args: + graph: node/list of neighboring nodes key/value pairs. + start: node to start search from. + target: node to search for. + + Returns: + Number of edges in shortest path between `start` and `target` nodes. + -1 if no path exists. + + Example: + >>> bfs_shortest_path_distance(graph, "G", "D") + 4 + >>> bfs_shortest_path_distance(graph, "A", "A") + 0 + >>> bfs_shortest_path_distance(graph, "A", "H") + -1 + """ + if not graph or start not in graph or target not in graph: + return -1 + if start == target: + return 0 + queue = [start] + visited = [start] + # Keep tab on distances from `start` node. + dist = {start: 0, target: -1} + while queue: + node = queue.pop(0) + if node == target: + dist[target] = ( + dist[node] if dist[target] == -1 else min(dist[target], dist[node]) + ) + for adjacent in graph[node]: + if adjacent not in visited: + visited.append(adjacent) + queue.append(adjacent) + dist[adjacent] = dist[node] + 1 + return dist[target] + + +if __name__ == "__main__": + print(bfs_shortest_path(graph, "G", "D")) # returns ['G', 'C', 'A', 'B', 'D'] + print(bfs_shortest_path_distance(graph, "G", "D")) # returns 4 diff --git a/graphs/breadth_first_search.py b/graphs/breadth_first_search.py index 3992e2d4d892..e40ec9d1d06d 100644 --- a/graphs/breadth_first_search.py +++ b/graphs/breadth_first_search.py @@ -1,51 +1,47 @@ #!/usr/bin/python -# encoding=utf8 """ Author: OMKAR PATHAK """ -from __future__ import print_function - -class Graph(): +class Graph: def __init__(self): - self.vertex = {} + self.vertices = {} - # for printing the Graph vertexes def printGraph(self): - for i in self.vertex.keys(): - print(i,' -> ', ' -> '.join([str(j) for j in self.vertex[i]])) + """prints adjacency list representation of graaph""" + for i in self.vertices.keys(): + print(i, " : ", " -> ".join([str(j) for j in self.vertices[i]])) - # for adding the edge beween two vertexes def addEdge(self, fromVertex, toVertex): - # check if vertex is already present, - if fromVertex in self.vertex.keys(): - self.vertex[fromVertex].append(toVertex) + """adding the edge between two vertices""" + if fromVertex in self.vertices.keys(): + self.vertices[fromVertex].append(toVertex) else: - # else make a new vertex - self.vertex[fromVertex] = [toVertex] + self.vertices[fromVertex] = [toVertex] def BFS(self, startVertex): - # Take a list for stoting already visited vertexes - visited = [False] * len(self.vertex) + # initialize set for storing already visited vertices + visited = set() - # create a list to store all the vertexes for BFS + # create a first in first out queue to store all the vertices for BFS queue = [] # mark the source node as visited and enqueue it - visited[startVertex] = True + visited.add(startVertex) queue.append(startVertex) while queue: - startVertex = queue.pop(0) - print(startVertex, end = ' ') + vertex = queue.pop(0) + + # loop through all adjacent vertex and enqueue it if not yet visited + for adjacent_vertex in self.vertices[vertex]: + if adjacent_vertex not in visited: + queue.append(adjacent_vertex) + visited.add(adjacent_vertex) + return visited - # mark all adjacent nodes as visited and print them - for i in self.vertex[startVertex]: - if visited[i] == False: - queue.append(i) - visited[i] = True -if __name__ == '__main__': +if __name__ == "__main__": g = Graph() g.addEdge(0, 1) g.addEdge(0, 2) @@ -55,13 +51,9 @@ def BFS(self, startVertex): g.addEdge(3, 3) g.printGraph() - print('BFS:') - g.BFS(2) + # 0 : 1 -> 2 + # 1 : 2 + # 2 : 0 -> 3 + # 3 : 3 - # OUTPUT: - # 0  ->  1 -> 2 - # 1  ->  2 - # 2  ->  0 -> 3 - # 3  ->  3 - # BFS: - # 2 0 3 1 + assert sorted(g.BFS(2)) == [0, 1, 2, 3] diff --git a/graphs/breadth_first_search_shortest_path.py b/graphs/breadth_first_search_shortest_path.py new file mode 100644 index 000000000000..514aed6d7211 --- /dev/null +++ b/graphs/breadth_first_search_shortest_path.py @@ -0,0 +1,81 @@ +"""Breath First Search (BFS) can be used when finding the shortest path +from a given source node to a target node in an unweighted graph. +""" +graph = { + "A": ["B", "C", "E"], + "B": ["A", "D", "E"], + "C": ["A", "F", "G"], + "D": ["B"], + "E": ["A", "B", "D"], + "F": ["C"], + "G": ["C"], +} + +from typing import Dict + + +class Graph: + def __init__(self, graph: Dict[str, str], source_vertex: str) -> None: + """Graph is implemented as dictionary of adjancency lists. Also, + Source vertex have to be defined upon initialization. + """ + self.graph = graph + # mapping node to its parent in resulting breadth first tree + self.parent = {} + self.source_vertex = source_vertex + + def breath_first_search(self) -> None: + """This function is a helper for running breath first search on this graph. + >>> g = Graph(graph, "G") + >>> g.breath_first_search() + >>> g.parent + {'G': None, 'C': 'G', 'A': 'C', 'F': 'C', 'B': 'A', 'E': 'A', 'D': 'B'} + """ + visited = {self.source_vertex} + self.parent[self.source_vertex] = None + queue = [self.source_vertex] # first in first out queue + + while queue: + vertex = queue.pop(0) + for adjancent_vertex in self.graph[vertex]: + if adjancent_vertex not in visited: + visited.add(adjancent_vertex) + self.parent[adjancent_vertex] = vertex + queue.append(adjancent_vertex) + + def shortest_path(self, target_vertex: str) -> str: + """This shortest path function returns a string, describing the result: + 1.) No path is found. The string is a human readable message to indicate this. + 2.) The shortest path is found. The string is in the form `v1(->v2->v3->...->vn)`, + where v1 is the source vertex and vn is the target vertex, if it exists separately. + + >>> g = Graph(graph, "G") + >>> g.breath_first_search() + + Case 1 - No path is found. + >>> g.shortest_path("Foo") + 'No path from vertex:G to vertex:Foo' + + Case 2 - The path is found. + >>> g.shortest_path("D") + 'G->C->A->B->D' + >>> g.shortest_path("G") + 'G' + """ + if target_vertex == self.source_vertex: + return f"{self.source_vertex}" + elif not self.parent.get(target_vertex): + return f"No path from vertex:{self.source_vertex} to vertex:{target_vertex}" + else: + return self.shortest_path(self.parent[target_vertex]) + f"->{target_vertex}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + g = Graph(graph, "G") + g.breath_first_search() + print(g.shortest_path("D")) + print(g.shortest_path("G")) + print(g.shortest_path("Foo")) diff --git a/graphs/check_bipartite_graph_bfs.py b/graphs/check_bipartite_graph_bfs.py index 1b9c32c6ccc4..1ec3e3d1d45f 100644 --- a/graphs/check_bipartite_graph_bfs.py +++ b/graphs/check_bipartite_graph_bfs.py @@ -11,7 +11,7 @@ def checkBipartite(l): color = [-1] * len(l) def bfs(): - while(queue): + while queue: u = queue.pop(0) visited[u] = True @@ -38,6 +38,7 @@ def bfs(): return True + # Adjacency List of graph -l = {0:[1,3], 1:[0,2], 2:[1,3], 3:[0,2]} +l = {0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]} print(checkBipartite(l)) diff --git a/graphs/check_bipartite_graph_dfs.py b/graphs/check_bipartite_graph_dfs.py index eeb3a84b7a15..6fe54a6723c5 100644 --- a/graphs/check_bipartite_graph_dfs.py +++ b/graphs/check_bipartite_graph_dfs.py @@ -26,8 +26,8 @@ def dfs(v, c): return False return True - + # Adjacency list of graph -l = {0:[1,3], 1:[0,2], 2:[1,3], 3:[0,2], 4: []} +l = {0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: []} print(check_bipartite_dfs(l)) diff --git a/graphs/depth_first_search.py b/graphs/depth_first_search.py index 98faf61354f9..1206d5ae9252 100644 --- a/graphs/depth_first_search.py +++ b/graphs/depth_first_search.py @@ -1,66 +1,59 @@ -#!/usr/bin/python -# encoding=utf8 - -""" Author: OMKAR PATHAK """ -from __future__ import print_function - - -class Graph(): - def __init__(self): - self.vertex = {} - - # for printing the Graph vertexes - def printGraph(self): - print(self.vertex) - for i in self.vertex.keys(): - print(i,' -> ', ' -> '.join([str(j) for j in self.vertex[i]])) - - # for adding the edge beween two vertexes - def addEdge(self, fromVertex, toVertex): - # check if vertex is already present, - if fromVertex in self.vertex.keys(): - self.vertex[fromVertex].append(toVertex) - else: - # else make a new vertex - self.vertex[fromVertex] = [toVertex] - - def DFS(self): - # visited array for storing already visited nodes - visited = [False] * len(self.vertex) - - # call the recursive helper function - for i in range(len(self.vertex)): - if visited[i] == False: - self.DFSRec(i, visited) - - def DFSRec(self, startVertex, visited): - # mark start vertex as visited - visited[startVertex] = True - - print(startVertex, end = ' ') - - # Recur for all the vertexes that are adjacent to this node - for i in self.vertex.keys(): - if visited[i] == False: - self.DFSRec(i, visited) - -if __name__ == '__main__': - g = Graph() - g.addEdge(0, 1) - g.addEdge(0, 2) - g.addEdge(1, 2) - g.addEdge(2, 0) - g.addEdge(2, 3) - g.addEdge(3, 3) - - g.printGraph() - print('DFS:') - g.DFS() - - # OUTPUT: - # 0  ->  1 -> 2 - # 1  ->  2 - # 2  ->  0 -> 3 - # 3  ->  3 - # DFS: - # 0 1 2 3 +"""The DFS function simply calls itself recursively for every unvisited child of +its argument. We can emulate that behaviour precisely using a stack of iterators. +Instead of recursively calling with a node, we'll push an iterator to the node's +children onto the iterator stack. When the iterator at the top of the stack +terminates, we'll pop it off the stack. + +Pseudocode: + all nodes initially unexplored + mark s as explored + for every edge (s, v): + if v unexplored: + DFS(G, v) +""" + +from typing import Set, Dict + + +def depth_first_search(graph: Dict, start: str) -> Set[int]: + """Depth First Search on Graph + + :param graph: directed graph in dictionary format + :param vertex: starting vectex as a string + :returns: the trace of the search + >>> G = { "A": ["B", "C", "D"], "B": ["A", "D", "E"], + ... "C": ["A", "F"], "D": ["B", "D"], "E": ["B", "F"], + ... "F": ["C", "E", "G"], "G": ["F"] } + >>> start = "A" + >>> output_G = list({'A', 'B', 'C', 'D', 'E', 'F', 'G'}) + >>> all(x in output_G for x in list(depth_first_search(G, "A"))) + True + >>> all(x in output_G for x in list(depth_first_search(G, "G"))) + True + """ + explored, stack = set(start), [start] + while stack: + v = stack.pop() + # one difference from BFS is to pop last element here instead of first one + for w in graph[v]: + if w not in explored: + explored.add(w) + stack.append(w) + return explored + + +G = { + "A": ["B", "C", "D"], + "B": ["A", "D", "E"], + "C": ["A", "F"], + "D": ["B", "D"], + "E": ["B", "F"], + "F": ["C", "E", "G"], + "G": ["F"], +} + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(depth_first_search(G, "A")) diff --git a/graphs/depth_first_search_2.py b/graphs/depth_first_search_2.py new file mode 100644 index 000000000000..0593e120b1da --- /dev/null +++ b/graphs/depth_first_search_2.py @@ -0,0 +1,65 @@ +#!/usr/bin/python + +""" Author: OMKAR PATHAK """ + + +class Graph: + def __init__(self): + self.vertex = {} + + # for printing the Graph vertices + def printGraph(self): + print(self.vertex) + for i in self.vertex.keys(): + print(i, " -> ", " -> ".join([str(j) for j in self.vertex[i]])) + + # for adding the edge between two vertices + def addEdge(self, fromVertex, toVertex): + # check if vertex is already present, + if fromVertex in self.vertex.keys(): + self.vertex[fromVertex].append(toVertex) + else: + # else make a new vertex + self.vertex[fromVertex] = [toVertex] + + def DFS(self): + # visited array for storing already visited nodes + visited = [False] * len(self.vertex) + + # call the recursive helper function + for i in range(len(self.vertex)): + if visited[i] == False: + self.DFSRec(i, visited) + + def DFSRec(self, startVertex, visited): + # mark start vertex as visited + visited[startVertex] = True + + print(startVertex, end=" ") + + # Recur for all the vertices that are adjacent to this node + for i in self.vertex.keys(): + if visited[i] == False: + self.DFSRec(i, visited) + + +if __name__ == "__main__": + g = Graph() + g.addEdge(0, 1) + g.addEdge(0, 2) + g.addEdge(1, 2) + g.addEdge(2, 0) + g.addEdge(2, 3) + g.addEdge(3, 3) + + g.printGraph() + print("DFS:") + g.DFS() + + # OUTPUT: + # 0  ->  1 -> 2 + # 1  ->  2 + # 2  ->  0 -> 3 + # 3  ->  3 + # DFS: + #  0 1 2 3 diff --git a/graphs/dfs.py b/graphs/dfs.py deleted file mode 100644 index 68bf60e3c298..000000000000 --- a/graphs/dfs.py +++ /dev/null @@ -1,40 +0,0 @@ -"""pseudo-code""" - -""" -DFS(graph G, start vertex s): -// all nodes initially unexplored -mark s as explored -for every edge (s, v): - if v unexplored: - DFS(G, v) -""" - - -def dfs(graph, start): - """The DFS function simply calls itself recursively for every unvisited child of its argument. We can emulate that - behaviour precisely using a stack of iterators. Instead of recursively calling with a node, we'll push an iterator - to the node's children onto the iterator stack. When the iterator at the top of the stack terminates, we'll pop - it off the stack.""" - explored, stack = set(), [start] - while stack: - v = stack.pop() # one difference from BFS is to pop last element here instead of first one - - if v in explored: - continue - - explored.add(v) - - for w in graph[v]: - if w not in explored: - stack.append(w) - return explored - - -G = {'A': ['B', 'C'], - 'B': ['A', 'D', 'E'], - 'C': ['A', 'F'], - 'D': ['B'], - 'E': ['B', 'F'], - 'F': ['C', 'E']} - -print(dfs(G, 'A')) diff --git a/graphs/dijkstra.py b/graphs/dijkstra.py index 5f09a45cf2c4..f156602beb6e 100644 --- a/graphs/dijkstra.py +++ b/graphs/dijkstra.py @@ -22,7 +22,7 @@ 13 - add (total_cost,V) to H You can think at cost as a distance where Dijkstra finds the shortest distance -between vertexes s and v in a graph G. The use of a min heap as H guarantees +between vertices s and v in a graph G. The use of a min heap as H guarantees that if a vertex has already been explored there will be no other path with shortest distance, that happens because heapq.heappop will always return the next vertex with the shortest distance, considering that the heap stores not @@ -35,7 +35,7 @@ def dijkstra(graph, start, end): - """Return the cost of the shortest path between vertexes start and end. + """Return the cost of the shortest path between vertices start and end. >>> dijkstra(G, "E", "C") 6 @@ -115,4 +115,5 @@ def dijkstra(graph, start, end): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/graphs/dijkstra_2.py b/graphs/dijkstra_2.py index 8f39aec41906..762884136e4a 100644 --- a/graphs/dijkstra_2.py +++ b/graphs/dijkstra_2.py @@ -1,57 +1,58 @@ -from __future__ import print_function - def printDist(dist, V): - print("\nVertex Distance") - for i in range(V): - if dist[i] != float('inf') : - print(i,"\t",int(dist[i]),end = "\t") - else: - print(i,"\t","INF",end="\t") - print() - -def minDist(mdist, vset, V): - minVal = float('inf') - minInd = -1 - for i in range(V): - if (not vset[i]) and mdist[i] < minVal : - minInd = i - minVal = mdist[i] - return minInd + print("\nVertex Distance") + for i in range(V): + if dist[i] != float("inf"): + print(i, "\t", int(dist[i]), end="\t") + else: + print(i, "\t", "INF", end="\t") + print() -def Dijkstra(graph, V, src): - mdist=[float('inf') for i in range(V)] - vset = [False for i in range(V)] - mdist[src] = 0.0 - for i in range(V-1): - u = minDist(mdist, vset, V) - vset[u] = True +def minDist(mdist, vset, V): + minVal = float("inf") + minInd = -1 + for i in range(V): + if (not vset[i]) and mdist[i] < minVal: + minInd = i + minVal = mdist[i] + return minInd - for v in range(V): - if (not vset[v]) and graph[u][v]!=float('inf') and mdist[u] + graph[u][v] < mdist[v]: - mdist[v] = mdist[u] + graph[u][v] +def Dijkstra(graph, V, src): + mdist = [float("inf") for i in range(V)] + vset = [False for i in range(V)] + mdist[src] = 0.0 + for i in range(V - 1): + u = minDist(mdist, vset, V) + vset[u] = True - printDist(mdist, V) + for v in range(V): + if ( + (not vset[v]) + and graph[u][v] != float("inf") + and mdist[u] + graph[u][v] < mdist[v] + ): + mdist[v] = mdist[u] + graph[u][v] + printDist(mdist, V) if __name__ == "__main__": V = int(input("Enter number of vertices: ").strip()) E = int(input("Enter number of edges: ").strip()) - graph = [[float('inf') for i in range(V)] for j in range(V)] + graph = [[float("inf") for i in range(V)] for j in range(V)] for i in range(V): - graph[i][i] = 0.0 + graph[i][i] = 0.0 for i in range(E): - print("\nEdge ",i+1) - src = int(input("Enter source:").strip()) - dst = int(input("Enter destination:").strip()) - weight = float(input("Enter weight:").strip()) - graph[src][dst] = weight + print("\nEdge ", i + 1) + src = int(input("Enter source:").strip()) + dst = int(input("Enter destination:").strip()) + weight = float(input("Enter weight:").strip()) + graph[src][dst] = weight gsrc = int(input("\nEnter shortest path source:").strip()) Dijkstra(graph, V, gsrc) diff --git a/graphs/dijkstra_algorithm.py b/graphs/dijkstra_algorithm.py index 985c7f6c1301..6b64834acd81 100644 --- a/graphs/dijkstra_algorithm.py +++ b/graphs/dijkstra_algorithm.py @@ -2,10 +2,10 @@ # Author: Shubham Malik # References: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm -from __future__ import print_function import math import sys -# For storing the vertex set to retreive node with the lowest distance + +# For storing the vertex set to retrieve node with the lowest distance class PriorityQueue: @@ -13,7 +13,7 @@ class PriorityQueue: def __init__(self): self.cur_size = 0 self.array = [] - self.pos = {} # To store the pos of node in array + self.pos = {} # To store the pos of node in array def isEmpty(self): return self.cur_size == 0 @@ -79,8 +79,8 @@ def decrease_key(self, tup, new_d): class Graph: def __init__(self, num): - self.adjList = {} # To store graph: u -> (v,w) - self.num_nodes = num # Number of nodes in graph + self.adjList = {} # To store graph: u -> (v,w) + self.num_nodes = num # Number of nodes in graph # To store the distance from source vertex self.dist = [0] * self.num_nodes self.par = [-1] * self.num_nodes # To store the path @@ -103,8 +103,7 @@ def add_edge(self, u, v, w): def show_graph(self): # u -> v(w) for u in self.adjList: - print(u, '->', ' -> '.join(str("{}({})".format(v, w)) - for v, w in self.adjList[u])) + print(u, "->", " -> ".join(str(f"{v}({w})") for v, w in self.adjList[u])) def dijkstra(self, src): # Flush old junk values in par[] @@ -136,9 +135,9 @@ def dijkstra(self, src): self.show_distances(src) def show_distances(self, src): - print("Distance from node: {}".format(src)) + print(f"Distance from node: {src}") for u in range(self.num_nodes): - print('Node {} has distance: {}'.format(u, self.dist[u])) + print(f"Node {u} has distance: {self.dist[u]}") def show_path(self, src, dest): # To show the shortest path from src to dest @@ -158,16 +157,16 @@ def show_path(self, src, dest): path.append(src) path.reverse() - print('----Path to reach {} from {}----'.format(dest, src)) + print(f"----Path to reach {dest} from {src}----") for u in path: - print('{}'.format(u), end=' ') + print(f"{u}", end=" ") if u != dest: - print('-> ', end='') + print("-> ", end="") - print('\nTotal cost of path: ', cost) + print("\nTotal cost of path: ", cost) -if __name__ == '__main__': +if __name__ == "__main__": graph = Graph(9) graph.add_edge(0, 1, 4) graph.add_edge(0, 7, 8) diff --git a/graphs/dinic.py b/graphs/dinic.py new file mode 100644 index 000000000000..4f5e81236984 --- /dev/null +++ b/graphs/dinic.py @@ -0,0 +1,94 @@ +INF = float("inf") + + +class Dinic: + def __init__(self, n): + self.lvl = [0] * n + self.ptr = [0] * n + self.q = [0] * n + self.adj = [[] for _ in range(n)] + + """ + Here we will add our edges containing with the following parameters: + vertex closest to source, vertex closest to sink and flow capacity + through that edge ... + """ + + def add_edge(self, a, b, c, rcap=0): + self.adj[a].append([b, len(self.adj[b]), c, 0]) + self.adj[b].append([a, len(self.adj[a]) - 1, rcap, 0]) + + # This is a sample depth first search to be used at max_flow + def depth_first_search(self, vertex, sink, flow): + if vertex == sink or not flow: + return flow + + for i in range(self.ptr[vertex], len(self.adj[vertex])): + e = self.adj[vertex][i] + if self.lvl[e[0]] == self.lvl[vertex] + 1: + p = self.depth_first_search(e[0], sink, min(flow, e[2] - e[3])) + if p: + self.adj[vertex][i][3] += p + self.adj[e[0]][e[1]][3] -= p + return p + self.ptr[vertex] = self.ptr[vertex] + 1 + return 0 + + # Here we calculate the flow that reaches the sink + def max_flow(self, source, sink): + flow, self.q[0] = 0, source + for l in range(31): # l = 30 maybe faster for random data + while True: + self.lvl, self.ptr = [0] * len(self.q), [0] * len(self.q) + qi, qe, self.lvl[source] = 0, 1, 1 + while qi < qe and not self.lvl[sink]: + v = self.q[qi] + qi += 1 + for e in self.adj[v]: + if not self.lvl[e[0]] and (e[2] - e[3]) >> (30 - l): + self.q[qe] = e[0] + qe += 1 + self.lvl[e[0]] = self.lvl[v] + 1 + + p = self.depth_first_search(source, sink, INF) + while p: + flow += p + p = self.depth_first_search(source, sink, INF) + + if not self.lvl[sink]: + break + + return flow + + +# Example to use + +""" +Will be a bipartite graph, than it has the vertices near the source(4) +and the vertices near the sink(4) +""" +# Here we make a graphs with 10 vertex(source and sink includes) +graph = Dinic(10) +source = 0 +sink = 9 +""" +Now we add the vertices next to the font in the font with 1 capacity in this edge +(source -> source vertices) +""" +for vertex in range(1, 5): + graph.add_edge(source, vertex, 1) +""" +We will do the same thing for the vertices near the sink, but from vertex to sink +(sink vertices -> sink) +""" +for vertex in range(5, 9): + graph.add_edge(vertex, sink, 1) +""" +Finally we add the verices near the sink to the vertices near the source. +(source vertices -> sink vertices) +""" +for vertex in range(1, 5): + graph.add_edge(vertex, vertex + 4, 1) + +# Now we can know that is the maximum flow(source -> sink) +print(graph.max_flow(source, sink)) diff --git a/graphs/directed_and_undirected_(weighted)_graph.py b/graphs/directed_and_undirected_(weighted)_graph.py index a31a4a96d6d0..26c87cd8f4b2 100644 --- a/graphs/directed_and_undirected_(weighted)_graph.py +++ b/graphs/directed_and_undirected_(weighted)_graph.py @@ -3,470 +3,495 @@ import math as math import time -# the dfault weight is 1 if not assigend but all the implementation is weighted +# the default weight is 1 if not assigned but all the implementation is weighted + class DirectedGraph: - def __init__(self): - self.graph = {} - - # adding vertices and edges - # adding the weight is optional - # handels repetition - def add_pair(self, u, v, w = 1): - if self.graph.get(u): - if self.graph[u].count([w,v]) == 0: - self.graph[u].append([w, v]) - else: - self.graph[u] = [[w, v]] - if not self.graph.get(v): - self.graph[v] = [] - - def all_nodes(self): - return list(self.graph) - - # handels if the input does not exist - def remove_pair(self, u, v): - if self.graph.get(u): - for _ in self.graph[u]: - if _[1] == v: - self.graph[u].remove(_) - - # if no destination is meant the defaut value is -1 - def dfs(self, s = -2, d = -1): - if s == d: - return [] - stack = [] - visited = [] - if s == -2: - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - ss = s - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) < 1: - if __[1] == d: - visited.append(d) - return visited - else: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return visited - - # c is the count of nodes you want and if you leave it or pass -1 to the funtion the count - # will be random from 10 to 10000 - def fill_graph_randomly(self, c = -1): - if c == -1: - c = (math.floor(rand.random() * 10000)) + 10 - for _ in range(c): - # every vertex has max 100 edges - e = math.floor(rand.random() * 102) + 1 - for __ in range(e): - n = math.floor(rand.random() * (c)) + 1 - if n == _: - continue - self.add_pair(_, n, 1) - - def bfs(self, s = -2): - d = deque() - visited = [] - if s == -2: - s = list(self.graph.keys())[0] - d.append(s) - visited.append(s) - while d: - s = d.popleft() - if len(self.graph[s]) != 0: - for __ in self.graph[s]: - if visited.count(__[1]) < 1: - d.append(__[1]) - visited.append(__[1]) - return visited - def in_degree(self, u): - count = 0 - for _ in self.graph: - for __ in self.graph[_]: - if __[1] == u: - count += 1 - return count - - def out_degree(self, u): - return len(self.graph[u]) - - def topological_sort(self, s = -2): - stack = [] - visited = [] - if s == -2: - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - ss = s - sorted_nodes = [] - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) < 1: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - sorted_nodes.append(stack.pop()) - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return sorted_nodes - - def cycle_nodes(self): - stack = [] - visited = [] - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - parent = -2 - indirect_parents = [] - ss = s - on_the_way_back = False - anticipating_nodes = set() - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) > 0 and __[1] != parent and indirect_parents.count(__[1]) > 0 and not on_the_way_back: - l = len(stack) - 1 - while True and l >= 0: - if stack[l] == __[1]: - anticipating_nodes.add(__[1]) - break - else: - anticipating_nodes.add(stack[l]) - l -= 1 - if visited.count(__[1]) < 1: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - on_the_way_back = True - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - on_the_way_back = False - indirect_parents.append(parent) - parent = s - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return list(anticipating_nodes) - - def has_cycle(self): - stack = [] - visited = [] - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - parent = -2 - indirect_parents = [] - ss = s - on_the_way_back = False - anticipating_nodes = set() - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) > 0 and __[1] != parent and indirect_parents.count(__[1]) > 0 and not on_the_way_back: - l = len(stack) - 1 - while True and l >= 0: - if stack[l] == __[1]: - anticipating_nodes.add(__[1]) - break - else: - return True - anticipating_nodes.add(stack[l]) - l -= 1 - if visited.count(__[1]) < 1: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - on_the_way_back = True - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - on_the_way_back = False - indirect_parents.append(parent) - parent = s - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return False - - def dfs_time(self, s = -2, e = -1): - begin = time.time() - self.dfs(s,e) - end = time.time() - return end - begin - - def bfs_time(self, s = -2): - begin = time.time() - self.bfs(s) - end = time.time() - return end - begin + def __init__(self): + self.graph = {} + + # adding vertices and edges + # adding the weight is optional + # handles repetition + def add_pair(self, u, v, w=1): + if self.graph.get(u): + if self.graph[u].count([w, v]) == 0: + self.graph[u].append([w, v]) + else: + self.graph[u] = [[w, v]] + if not self.graph.get(v): + self.graph[v] = [] + + def all_nodes(self): + return list(self.graph) + + # handles if the input does not exist + def remove_pair(self, u, v): + if self.graph.get(u): + for _ in self.graph[u]: + if _[1] == v: + self.graph[u].remove(_) + + # if no destination is meant the default value is -1 + def dfs(self, s=-2, d=-1): + if s == d: + return [] + stack = [] + visited = [] + if s == -2: + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + ss = s + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if visited.count(__[1]) < 1: + if __[1] == d: + visited.append(d) + return visited + else: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return visited + + # c is the count of nodes you want and if you leave it or pass -1 to the function the count + # will be random from 10 to 10000 + def fill_graph_randomly(self, c=-1): + if c == -1: + c = (math.floor(rand.random() * 10000)) + 10 + for _ in range(c): + # every vertex has max 100 edges + e = math.floor(rand.random() * 102) + 1 + for __ in range(e): + n = math.floor(rand.random() * (c)) + 1 + if n == _: + continue + self.add_pair(_, n, 1) + + def bfs(self, s=-2): + d = deque() + visited = [] + if s == -2: + s = list(self.graph.keys())[0] + d.append(s) + visited.append(s) + while d: + s = d.popleft() + if len(self.graph[s]) != 0: + for __ in self.graph[s]: + if visited.count(__[1]) < 1: + d.append(__[1]) + visited.append(__[1]) + return visited + + def in_degree(self, u): + count = 0 + for _ in self.graph: + for __ in self.graph[_]: + if __[1] == u: + count += 1 + return count + + def out_degree(self, u): + return len(self.graph[u]) + + def topological_sort(self, s=-2): + stack = [] + visited = [] + if s == -2: + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + ss = s + sorted_nodes = [] + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if visited.count(__[1]) < 1: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + sorted_nodes.append(stack.pop()) + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return sorted_nodes + + def cycle_nodes(self): + stack = [] + visited = [] + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + parent = -2 + indirect_parents = [] + ss = s + on_the_way_back = False + anticipating_nodes = set() + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if ( + visited.count(__[1]) > 0 + and __[1] != parent + and indirect_parents.count(__[1]) > 0 + and not on_the_way_back + ): + l = len(stack) - 1 + while True and l >= 0: + if stack[l] == __[1]: + anticipating_nodes.add(__[1]) + break + else: + anticipating_nodes.add(stack[l]) + l -= 1 + if visited.count(__[1]) < 1: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + on_the_way_back = True + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + on_the_way_back = False + indirect_parents.append(parent) + parent = s + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return list(anticipating_nodes) + + def has_cycle(self): + stack = [] + visited = [] + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + parent = -2 + indirect_parents = [] + ss = s + on_the_way_back = False + anticipating_nodes = set() + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if ( + visited.count(__[1]) > 0 + and __[1] != parent + and indirect_parents.count(__[1]) > 0 + and not on_the_way_back + ): + l = len(stack) - 1 + while True and l >= 0: + if stack[l] == __[1]: + anticipating_nodes.add(__[1]) + break + else: + return True + anticipating_nodes.add(stack[l]) + l -= 1 + if visited.count(__[1]) < 1: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + on_the_way_back = True + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + on_the_way_back = False + indirect_parents.append(parent) + parent = s + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return False + + def dfs_time(self, s=-2, e=-1): + begin = time.time() + self.dfs(s, e) + end = time.time() + return end - begin + + def bfs_time(self, s=-2): + begin = time.time() + self.bfs(s) + end = time.time() + return end - begin + class Graph: - def __init__(self): - self.graph = {} - - # adding vertices and edges - # adding the weight is optional - # handels repetition - def add_pair(self, u, v, w = 1): - # check if the u exists - if self.graph.get(u): - # if there already is a edge - if self.graph[u].count([w,v]) == 0: - self.graph[u].append([w, v]) - else: - # if u does not exist - self.graph[u] = [[w, v]] - # add the other way - if self.graph.get(v): - # if there already is a edge - if self.graph[v].count([w,u]) == 0: - self.graph[v].append([w, u]) - else: - # if u does not exist - self.graph[v] = [[w, u]] - - # handels if the input does not exist - def remove_pair(self, u, v): - if self.graph.get(u): - for _ in self.graph[u]: - if _[1] == v: - self.graph[u].remove(_) - # the other way round - if self.graph.get(v): - for _ in self.graph[v]: - if _[1] == u: - self.graph[v].remove(_) - - # if no destination is meant the defaut value is -1 - def dfs(self, s = -2, d = -1): - if s == d: - return [] - stack = [] - visited = [] - if s == -2: - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - ss = s - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) < 1: - if __[1] == d: - visited.append(d) - return visited - else: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return visited - - # c is the count of nodes you want and if you leave it or pass -1 to the funtion the count - # will be random from 10 to 10000 - def fill_graph_randomly(self, c = -1): - if c == -1: - c = (math.floor(rand.random() * 10000)) + 10 - for _ in range(c): - # every vertex has max 100 edges - e = math.floor(rand.random() * 102) + 1 - for __ in range(e): - n = math.floor(rand.random() * (c)) + 1 - if n == _: - continue - self.add_pair(_, n, 1) - - def bfs(self, s = -2): - d = deque() - visited = [] - if s == -2: - s = list(self.graph.keys())[0] - d.append(s) - visited.append(s) - while d: - s = d.popleft() - if len(self.graph[s]) != 0: - for __ in self.graph[s]: - if visited.count(__[1]) < 1: - d.append(__[1]) - visited.append(__[1]) - return visited - def degree(self, u): - return len(self.graph[u]) - - def cycle_nodes(self): - stack = [] - visited = [] - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - parent = -2 - indirect_parents = [] - ss = s - on_the_way_back = False - anticipating_nodes = set() - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) > 0 and __[1] != parent and indirect_parents.count(__[1]) > 0 and not on_the_way_back: - l = len(stack) - 1 - while True and l >= 0: - if stack[l] == __[1]: - anticipating_nodes.add(__[1]) - break - else: - anticipating_nodes.add(stack[l]) - l -= 1 - if visited.count(__[1]) < 1: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - on_the_way_back = True - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - on_the_way_back = False - indirect_parents.append(parent) - parent = s - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return list(anticipating_nodes) - - def has_cycle(self): - stack = [] - visited = [] - s = list(self.graph.keys())[0] - stack.append(s) - visited.append(s) - parent = -2 - indirect_parents = [] - ss = s - on_the_way_back = False - anticipating_nodes = set() - - while True: - # check if there is any non isolated nodes - if len(self.graph[s]) != 0: - ss = s - for __ in self.graph[s]: - if visited.count(__[1]) > 0 and __[1] != parent and indirect_parents.count(__[1]) > 0 and not on_the_way_back: - l = len(stack) - 1 - while True and l >= 0: - if stack[l] == __[1]: - anticipating_nodes.add(__[1]) - break - else: - return True - anticipating_nodes.add(stack[l]) - l -= 1 - if visited.count(__[1]) < 1: - stack.append(__[1]) - visited.append(__[1]) - ss =__[1] - break - - # check if all the children are visited - if s == ss : - stack.pop() - on_the_way_back = True - if len(stack) != 0: - s = stack[len(stack) - 1] - else: - on_the_way_back = False - indirect_parents.append(parent) - parent = s - s = ss - - # check if se have reached the starting point - if len(stack) == 0: - return False - def all_nodes(self): - return list(self.graph) - - def dfs_time(self, s = -2, e = -1): - begin = time.time() - self.dfs(s,e) - end = time.time() - return end - begin - - def bfs_time(self, s = -2): - begin = time.time() - self.bfs(s) - end = time.time() - return end - begin + def __init__(self): + self.graph = {} + + # adding vertices and edges + # adding the weight is optional + # handles repetition + def add_pair(self, u, v, w=1): + # check if the u exists + if self.graph.get(u): + # if there already is a edge + if self.graph[u].count([w, v]) == 0: + self.graph[u].append([w, v]) + else: + # if u does not exist + self.graph[u] = [[w, v]] + # add the other way + if self.graph.get(v): + # if there already is a edge + if self.graph[v].count([w, u]) == 0: + self.graph[v].append([w, u]) + else: + # if u does not exist + self.graph[v] = [[w, u]] + + # handles if the input does not exist + def remove_pair(self, u, v): + if self.graph.get(u): + for _ in self.graph[u]: + if _[1] == v: + self.graph[u].remove(_) + # the other way round + if self.graph.get(v): + for _ in self.graph[v]: + if _[1] == u: + self.graph[v].remove(_) + + # if no destination is meant the default value is -1 + def dfs(self, s=-2, d=-1): + if s == d: + return [] + stack = [] + visited = [] + if s == -2: + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + ss = s + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if visited.count(__[1]) < 1: + if __[1] == d: + visited.append(d) + return visited + else: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return visited + + # c is the count of nodes you want and if you leave it or pass -1 to the function the count + # will be random from 10 to 10000 + def fill_graph_randomly(self, c=-1): + if c == -1: + c = (math.floor(rand.random() * 10000)) + 10 + for _ in range(c): + # every vertex has max 100 edges + e = math.floor(rand.random() * 102) + 1 + for __ in range(e): + n = math.floor(rand.random() * (c)) + 1 + if n == _: + continue + self.add_pair(_, n, 1) + + def bfs(self, s=-2): + d = deque() + visited = [] + if s == -2: + s = list(self.graph.keys())[0] + d.append(s) + visited.append(s) + while d: + s = d.popleft() + if len(self.graph[s]) != 0: + for __ in self.graph[s]: + if visited.count(__[1]) < 1: + d.append(__[1]) + visited.append(__[1]) + return visited + + def degree(self, u): + return len(self.graph[u]) + + def cycle_nodes(self): + stack = [] + visited = [] + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + parent = -2 + indirect_parents = [] + ss = s + on_the_way_back = False + anticipating_nodes = set() + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if ( + visited.count(__[1]) > 0 + and __[1] != parent + and indirect_parents.count(__[1]) > 0 + and not on_the_way_back + ): + l = len(stack) - 1 + while True and l >= 0: + if stack[l] == __[1]: + anticipating_nodes.add(__[1]) + break + else: + anticipating_nodes.add(stack[l]) + l -= 1 + if visited.count(__[1]) < 1: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + on_the_way_back = True + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + on_the_way_back = False + indirect_parents.append(parent) + parent = s + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return list(anticipating_nodes) + + def has_cycle(self): + stack = [] + visited = [] + s = list(self.graph.keys())[0] + stack.append(s) + visited.append(s) + parent = -2 + indirect_parents = [] + ss = s + on_the_way_back = False + anticipating_nodes = set() + + while True: + # check if there is any non isolated nodes + if len(self.graph[s]) != 0: + ss = s + for __ in self.graph[s]: + if ( + visited.count(__[1]) > 0 + and __[1] != parent + and indirect_parents.count(__[1]) > 0 + and not on_the_way_back + ): + l = len(stack) - 1 + while True and l >= 0: + if stack[l] == __[1]: + anticipating_nodes.add(__[1]) + break + else: + return True + anticipating_nodes.add(stack[l]) + l -= 1 + if visited.count(__[1]) < 1: + stack.append(__[1]) + visited.append(__[1]) + ss = __[1] + break + + # check if all the children are visited + if s == ss: + stack.pop() + on_the_way_back = True + if len(stack) != 0: + s = stack[len(stack) - 1] + else: + on_the_way_back = False + indirect_parents.append(parent) + parent = s + s = ss + + # check if se have reached the starting point + if len(stack) == 0: + return False + + def all_nodes(self): + return list(self.graph) + + def dfs_time(self, s=-2, e=-1): + begin = time.time() + self.dfs(s, e) + end = time.time() + return end - begin + + def bfs_time(self, s=-2): + begin = time.time() + self.bfs(s) + end = time.time() + return end - begin diff --git a/graphs/edmonds_karp_multiple_source_and_sink.py b/graphs/edmonds_karp_multiple_source_and_sink.py index d231ac2c4cc3..0f359ff1aea3 100644 --- a/graphs/edmonds_karp_multiple_source_and_sink.py +++ b/graphs/edmonds_karp_multiple_source_and_sink.py @@ -28,14 +28,13 @@ def _normalizeGraph(self, sources, sinks): for i in sources: maxInputFlow += sum(self.graph[i]) - size = len(self.graph) + 1 for room in self.graph: room.insert(0, 0) self.graph.insert(0, [0] * size) for i in sources: self.graph[0][i + 1] = maxInputFlow - self.sourceIndex = 0 + self.sourceIndex = 0 size = len(self.graph) + 1 for room in self.graph: @@ -45,7 +44,6 @@ def _normalizeGraph(self, sources, sinks): self.graph[i + 1][size - 1] = maxInputFlow self.sinkIndex = size - 1 - def findMaximumFlow(self): if self.maximumFlowAlgorithm is None: raise Exception("You need to set maximum flow algorithm before.") @@ -59,7 +57,7 @@ def setMaximumFlowAlgorithm(self, Algorithm): self.maximumFlowAlgorithm = Algorithm(self) -class FlowNetworkAlgorithmExecutor(object): +class FlowNetworkAlgorithmExecutor: def __init__(self, flowNetwork): self.flowNetwork = flowNetwork self.verticesCount = flowNetwork.verticesCount @@ -80,10 +78,9 @@ def _algorithm(self): pass - class MaximumFlowAlgorithmExecutor(FlowNetworkAlgorithmExecutor): def __init__(self, flowNetwork): - super(MaximumFlowAlgorithmExecutor, self).__init__(flowNetwork) + super().__init__(flowNetwork) # use this to save your result self.maximumFlow = -1 @@ -93,9 +90,10 @@ def getMaximumFlow(self): return self.maximumFlow + class PushRelabelExecutor(MaximumFlowAlgorithmExecutor): def __init__(self, flowNetwork): - super(PushRelabelExecutor, self).__init__(flowNetwork) + super().__init__(flowNetwork) self.preflow = [[0] * self.verticesCount for i in range(self.verticesCount)] @@ -112,8 +110,11 @@ def _algorithm(self): self.excesses[nextVertexIndex] += bandwidth # Relabel-to-front selection rule - verticesList = [i for i in range(self.verticesCount) - if i != self.sourceIndex and i != self.sinkIndex] + verticesList = [ + i + for i in range(self.verticesCount) + if i != self.sourceIndex and i != self.sinkIndex + ] # move through list i = 0 @@ -135,15 +136,21 @@ def processVertex(self, vertexIndex): while self.excesses[vertexIndex] > 0: for neighbourIndex in range(self.verticesCount): # if it's neighbour and current vertex is higher - if self.graph[vertexIndex][neighbourIndex] - self.preflow[vertexIndex][neighbourIndex] > 0\ - and self.heights[vertexIndex] > self.heights[neighbourIndex]: + if ( + self.graph[vertexIndex][neighbourIndex] + - self.preflow[vertexIndex][neighbourIndex] + > 0 + and self.heights[vertexIndex] > self.heights[neighbourIndex] + ): self.push(vertexIndex, neighbourIndex) self.relabel(vertexIndex) def push(self, fromIndex, toIndex): - preflowDelta = min(self.excesses[fromIndex], - self.graph[fromIndex][toIndex] - self.preflow[fromIndex][toIndex]) + preflowDelta = min( + self.excesses[fromIndex], + self.graph[fromIndex][toIndex] - self.preflow[fromIndex][toIndex], + ) self.preflow[fromIndex][toIndex] += preflowDelta self.preflow[toIndex][fromIndex] -= preflowDelta self.excesses[fromIndex] -= preflowDelta @@ -152,14 +159,18 @@ def push(self, fromIndex, toIndex): def relabel(self, vertexIndex): minHeight = None for toIndex in range(self.verticesCount): - if self.graph[vertexIndex][toIndex] - self.preflow[vertexIndex][toIndex] > 0: + if ( + self.graph[vertexIndex][toIndex] - self.preflow[vertexIndex][toIndex] + > 0 + ): if minHeight is None or self.heights[toIndex] < minHeight: minHeight = self.heights[toIndex] if minHeight is not None: self.heights[vertexIndex] = minHeight + 1 -if __name__ == '__main__': + +if __name__ == "__main__": entrances = [0] exits = [3] # graph = [ @@ -179,4 +190,4 @@ def relabel(self, vertexIndex): # and calculate maximumFlow = flowNetwork.findMaximumFlow() - print("maximum flow is {}".format(maximumFlow)) + print(f"maximum flow is {maximumFlow}") diff --git a/graphs/eulerian_path_and_circuit_for_undirected_graph.py b/graphs/eulerian_path_and_circuit_for_undirected_graph.py index c6c6a1a25f03..a2e5cf4da26a 100644 --- a/graphs/eulerian_path_and_circuit_for_undirected_graph.py +++ b/graphs/eulerian_path_and_circuit_for_undirected_graph.py @@ -50,32 +50,10 @@ def check_euler(graph, max_node): def main(): - G1 = { - 1: [2, 3, 4], - 2: [1, 3], - 3: [1, 2], - 4: [1, 5], - 5: [4] - } - G2 = { - 1: [2, 3, 4, 5], - 2: [1, 3], - 3: [1, 2], - 4: [1, 5], - 5: [1, 4] - } - G3 = { - 1: [2, 3, 4], - 2: [1, 3, 4], - 3: [1, 2], - 4: [1, 2, 5], - 5: [4] - } - G4 = { - 1: [2, 3], - 2: [1, 3], - 3: [1, 2], - } + G1 = {1: [2, 3, 4], 2: [1, 3], 3: [1, 2], 4: [1, 5], 5: [4]} + G2 = {1: [2, 3, 4, 5], 2: [1, 3], 3: [1, 2], 4: [1, 5], 5: [1, 4]} + G3 = {1: [2, 3, 4], 2: [1, 3, 4], 3: [1, 2], 4: [1, 2, 5], 5: [4]} + G4 = {1: [2, 3], 2: [1, 3], 3: [1, 2]} G5 = { 1: [], 2: [] diff --git a/graphs/even_tree.py b/graphs/even_tree.py index 9383ea9a13c1..c9aef6e7861f 100644 --- a/graphs/even_tree.py +++ b/graphs/even_tree.py @@ -12,7 +12,6 @@ Note: The tree input will be such that it can always be decomposed into components containing an even number of nodes. """ -from __future__ import print_function # pylint: disable=invalid-name from collections import defaultdict @@ -46,23 +45,13 @@ def even_tree(): dfs(1) -if __name__ == '__main__': +if __name__ == "__main__": n, m = 10, 9 tree = defaultdict(list) visited = {} cuts = [] count = 0 - edges = [ - (2, 1), - (3, 1), - (4, 3), - (5, 2), - (6, 1), - (7, 2), - (8, 6), - (9, 8), - (10, 8), - ] + edges = [(2, 1), (3, 1), (4, 3), (5, 2), (6, 1), (7, 2), (8, 6), (9, 8), (10, 8)] for u, v in edges: tree[u].append(v) tree[v].append(u) diff --git a/graphs/finding_bridges.py b/graphs/finding_bridges.py index 56533dd48bde..e18a3bafa9c0 100644 --- a/graphs/finding_bridges.py +++ b/graphs/finding_bridges.py @@ -1,7 +1,7 @@ # Finding Bridges in Undirected Graph def computeBridges(l): id = 0 - n = len(l) # No of vertices in graph + n = len(l) # No of vertices in graph low = [0] * n visited = [False] * n @@ -23,9 +23,20 @@ def dfs(at, parent, bridges, id): bridges = [] for i in range(n): - if (not visited[i]): + if not visited[i]: dfs(i, -1, bridges, id) print(bridges) - -l = {0:[1,2], 1:[0,2], 2:[0,1,3,5], 3:[2,4], 4:[3], 5:[2,6,8], 6:[5,7], 7:[6,8], 8:[5,7]} + + +l = { + 0: [1, 2], + 1: [0, 2], + 2: [0, 1, 3, 5], + 3: [2, 4], + 4: [3], + 5: [2, 6, 8], + 6: [5, 7], + 7: [6, 8], + 8: [5, 7], +} computeBridges(l) diff --git a/graphs/g_topological_sort.py b/graphs/g_topological_sort.py new file mode 100644 index 000000000000..77543d51f61d --- /dev/null +++ b/graphs/g_topological_sort.py @@ -0,0 +1,47 @@ +# Author: Phyllipe Bezerra (https://github.com/pmba) + +clothes = { + 0: "underwear", + 1: "pants", + 2: "belt", + 3: "suit", + 4: "shoe", + 5: "socks", + 6: "shirt", + 7: "tie", + 8: "watch", +} + +graph = [[1, 4], [2, 4], [3], [], [], [4], [2, 7], [3], []] + +visited = [0 for x in range(len(graph))] +stack = [] + + +def print_stack(stack, clothes): + order = 1 + while stack: + current_clothing = stack.pop() + print(order, clothes[current_clothing]) + order += 1 + + +def depth_first_search(u, visited, graph): + visited[u] = 1 + for v in graph[u]: + if not visited[v]: + depth_first_search(v, visited, graph) + + stack.append(u) + + +def topological_sort(graph, visited): + for v in range(len(graph)): + if not visited[v]: + depth_first_search(v, visited, graph) + + +if __name__ == "__main__": + topological_sort(graph, visited) + print(stack) + print_stack(stack, clothes) diff --git a/graphs/graph_list.py b/graphs/graph_list.py index 0c981c39d320..a20940ab1598 100644 --- a/graphs/graph_list.py +++ b/graphs/graph_list.py @@ -1,12 +1,11 @@ #!/usr/bin/python -# encoding=utf8 -from __future__ import print_function # Author: OMKAR PATHAK # We can use Python's dictionary for constructing the graph. -class AdjacencyList(object): + +class AdjacencyList: def __init__(self): self.List = {} @@ -18,10 +17,11 @@ def addEdge(self, fromVertex, toVertex): self.List[fromVertex] = [toVertex] def printList(self): - for i in self.List: - print((i,'->',' -> '.join([str(j) for j in self.List[i]]))) + for i in self.List: + print((i, "->", " -> ".join([str(j) for j in self.List[i]]))) + -if __name__ == '__main__': +if __name__ == "__main__": al = AdjacencyList() al.addEdge(0, 1) al.addEdge(0, 4) diff --git a/graphs/graph_matrix.py b/graphs/graph_matrix.py index de25301d6dd1..987168426ba5 100644 --- a/graphs/graph_matrix.py +++ b/graphs/graph_matrix.py @@ -1,11 +1,7 @@ -from __future__ import print_function - - class Graph: - def __init__(self, vertex): self.vertex = vertex - self.graph = [[0] * vertex for i in range(vertex) ] + self.graph = [[0] * vertex for i in range(vertex)] def add_edge(self, u, v): self.graph[u - 1][v - 1] = 1 @@ -15,18 +11,15 @@ def show(self): for i in self.graph: for j in i: - print(j, end=' ') - print(' ') - - + print(j, end=" ") + print(" ") g = Graph(100) -g.add_edge(1,4) -g.add_edge(4,2) -g.add_edge(4,5) -g.add_edge(2,5) -g.add_edge(5,3) +g.add_edge(1, 4) +g.add_edge(4, 2) +g.add_edge(4, 5) +g.add_edge(2, 5) +g.add_edge(5, 3) g.show() - diff --git a/graphs/graphs_floyd_warshall.py b/graphs/graphs_floyd_warshall.py index a1d12aac02b4..5727a2f21d89 100644 --- a/graphs/graphs_floyd_warshall.py +++ b/graphs/graphs_floyd_warshall.py @@ -4,23 +4,20 @@ have negative edge weights. """ -from __future__ import print_function - def _print_dist(dist, v): - print("\nThe shortest path matrix using Floyd Warshall algorithm\n") - for i in range(v): - for j in range(v): - if dist[i][j] != float('inf') : - print(int(dist[i][j]),end = "\t") - else: - print("INF",end="\t") - print() - + print("\nThe shortest path matrix using Floyd Warshall algorithm\n") + for i in range(v): + for j in range(v): + if dist[i][j] != float("inf"): + print(int(dist[i][j]), end="\t") + else: + print("INF", end="\t") + print() def floyd_warshall(graph, v): - """ + """ :param graph: 2D array calculated from weight[edge[i, j]] :type graph: List[List[float]] :param v: number of vertices @@ -34,69 +31,71 @@ def floyd_warshall(graph, v): 4. The above is repeated for each vertex k in the graph. 5. Whenever distance[i][j] is given a new minimum value, next vertex[i][j] is updated to the next vertex[i][k]. """ - - dist=[[float('inf') for _ in range(v)] for _ in range(v)] - - for i in range(v): - for j in range(v): - dist[i][j] = graph[i][j] - - # check vertex k against all other vertices (i, j) - for k in range(v): - # looping through rows of graph array - for i in range(v): - # looping through columns of graph array - for j in range(v): - if dist[i][k]!=float('inf') and dist[k][j]!=float('inf') and dist[i][k]+dist[k][j] < dist[i][j]: - dist[i][j] = dist[i][k] + dist[k][j] - - _print_dist(dist, v) - return dist, v - - - -if __name__== '__main__': - v = int(input("Enter number of vertices: ")) - e = int(input("Enter number of edges: ")) - - graph = [[float('inf') for i in range(v)] for j in range(v)] - - for i in range(v): - graph[i][i] = 0.0 - - # src and dst are indices that must be within the array size graph[e][v] - # failure to follow this will result in an error - for i in range(e): - print("\nEdge ",i+1) - src = int(input("Enter source:")) - dst = int(input("Enter destination:")) - weight = float(input("Enter weight:")) - graph[src][dst] = weight - - floyd_warshall(graph, v) - - - # Example Input - # Enter number of vertices: 3 - # Enter number of edges: 2 - - # # generated graph from vertex and edge inputs - # [[inf, inf, inf], [inf, inf, inf], [inf, inf, inf]] - # [[0.0, inf, inf], [inf, 0.0, inf], [inf, inf, 0.0]] - - # specify source, destination and weight for edge #1 - # Edge 1 - # Enter source:1 - # Enter destination:2 - # Enter weight:2 - - # specify source, destination and weight for edge #2 - # Edge 2 - # Enter source:2 - # Enter destination:1 - # Enter weight:1 - - # # Expected Output from the vertice, edge and src, dst, weight inputs!! - # 0 INF INF - # INF 0 2 - # INF 1 0 + + dist = [[float("inf") for _ in range(v)] for _ in range(v)] + + for i in range(v): + for j in range(v): + dist[i][j] = graph[i][j] + + # check vertex k against all other vertices (i, j) + for k in range(v): + # looping through rows of graph array + for i in range(v): + # looping through columns of graph array + for j in range(v): + if ( + dist[i][k] != float("inf") + and dist[k][j] != float("inf") + and dist[i][k] + dist[k][j] < dist[i][j] + ): + dist[i][j] = dist[i][k] + dist[k][j] + + _print_dist(dist, v) + return dist, v + + +if __name__ == "__main__": + v = int(input("Enter number of vertices: ")) + e = int(input("Enter number of edges: ")) + + graph = [[float("inf") for i in range(v)] for j in range(v)] + + for i in range(v): + graph[i][i] = 0.0 + + # src and dst are indices that must be within the array size graph[e][v] + # failure to follow this will result in an error + for i in range(e): + print("\nEdge ", i + 1) + src = int(input("Enter source:")) + dst = int(input("Enter destination:")) + weight = float(input("Enter weight:")) + graph[src][dst] = weight + + floyd_warshall(graph, v) + + # Example Input + # Enter number of vertices: 3 + # Enter number of edges: 2 + + # # generated graph from vertex and edge inputs + # [[inf, inf, inf], [inf, inf, inf], [inf, inf, inf]] + # [[0.0, inf, inf], [inf, 0.0, inf], [inf, inf, 0.0]] + + # specify source, destination and weight for edge #1 + # Edge 1 + # Enter source:1 + # Enter destination:2 + # Enter weight:2 + + # specify source, destination and weight for edge #2 + # Edge 2 + # Enter source:2 + # Enter destination:1 + # Enter weight:1 + + # # Expected Output from the vertice, edge and src, dst, weight inputs!! + # 0 INF INF + # INF 0 2 + # INF 1 0 diff --git a/graphs/kahns_algorithm_long.py b/graphs/kahns_algorithm_long.py index 453b5706f6da..0651040365d0 100644 --- a/graphs/kahns_algorithm_long.py +++ b/graphs/kahns_algorithm_long.py @@ -12,19 +12,20 @@ def longestDistance(l): if indegree[i] == 0: queue.append(i) - while(queue): + while queue: vertex = queue.pop(0) for x in l[vertex]: indegree[x] -= 1 if longDist[vertex] + 1 > longDist[x]: - longDist[x] = longDist[vertex] + 1 + longDist[x] = longDist[vertex] + 1 if indegree[x] == 0: queue.append(x) print(max(longDist)) + # Adjacency list of Graph -l = {0:[2,3,4], 1:[2,7], 2:[5], 3:[5,7], 4:[7], 5:[6], 6:[7], 7:[]} +l = {0: [2, 3, 4], 1: [2, 7], 2: [5], 3: [5, 7], 4: [7], 5: [6], 6: [7], 7: []} longestDistance(l) diff --git a/graphs/kahns_algorithm_topo.py b/graphs/kahns_algorithm_topo.py index 8c182c4e902c..d50bc9a43d19 100644 --- a/graphs/kahns_algorithm_topo.py +++ b/graphs/kahns_algorithm_topo.py @@ -13,7 +13,7 @@ def topologicalSort(l): if indegree[i] == 0: queue.append(i) - while(queue): + while queue: vertex = queue.pop(0) cnt += 1 topo.append(vertex) @@ -27,6 +27,7 @@ def topologicalSort(l): else: print(topo) + # Adjacency List of Graph -l = {0:[1,2], 1:[3], 2:[3], 3:[4,5], 4:[], 5:[]} +l = {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [], 5: []} topologicalSort(l) diff --git a/graphs/minimum_spanning_tree_kruskal.py b/graphs/minimum_spanning_tree_kruskal.py index 975151c90ede..91b44f6508e7 100644 --- a/graphs/minimum_spanning_tree_kruskal.py +++ b/graphs/minimum_spanning_tree_kruskal.py @@ -1,34 +1,32 @@ -from __future__ import print_function - if __name__ == "__main__": num_nodes, num_edges = list(map(int, input().strip().split())) edges = [] for i in range(num_edges): - node1, node2, cost = list(map(int, input().strip().split())) - edges.append((i,node1,node2,cost)) + node1, node2, cost = list(map(int, input().strip().split())) + edges.append((i, node1, node2, cost)) edges = sorted(edges, key=lambda edge: edge[3]) parent = list(range(num_nodes)) def find_parent(i): - if i != parent[i]: - parent[i] = find_parent(parent[i]) - return parent[i] + if i != parent[i]: + parent[i] = find_parent(parent[i]) + return parent[i] minimum_spanning_tree_cost = 0 minimum_spanning_tree = [] for edge in edges: - parent_a = find_parent(edge[1]) - parent_b = find_parent(edge[2]) - if parent_a != parent_b: - minimum_spanning_tree_cost += edge[3] - minimum_spanning_tree.append(edge) - parent[parent_a] = parent_b + parent_a = find_parent(edge[1]) + parent_b = find_parent(edge[2]) + if parent_a != parent_b: + minimum_spanning_tree_cost += edge[3] + minimum_spanning_tree.append(edge) + parent[parent_a] = parent_b print(minimum_spanning_tree_cost) for edge in minimum_spanning_tree: - print(edge) + print(edge) diff --git a/graphs/minimum_spanning_tree_prims.py b/graphs/minimum_spanning_tree_prims.py index 0f21b8f494e4..6255b6af64ad 100644 --- a/graphs/minimum_spanning_tree_prims.py +++ b/graphs/minimum_spanning_tree_prims.py @@ -1,16 +1,18 @@ import sys from collections import defaultdict + def PrimsAlgorithm(l): nodePosition = [] - def getPosition(vertex): + + def get_position(vertex): return nodePosition[vertex] - def setPosition(vertex, pos): + def set_position(vertex, pos): nodePosition[vertex] = pos - def topToBottom(heap, start, size, positions): + def top_to_bottom(heap, start, size, positions): if start > size // 2 - 1: return else: @@ -26,52 +28,52 @@ def topToBottom(heap, start, size, positions): heap[m], positions[m] = heap[start], positions[start] heap[start], positions[start] = temp, temp1 - temp = getPosition(positions[m]) - setPosition(positions[m], getPosition(positions[start])) - setPosition(positions[start], temp) + temp = get_position(positions[m]) + set_position(positions[m], get_position(positions[start])) + set_position(positions[start], temp) - topToBottom(heap, m, size, positions) + top_to_bottom(heap, m, size, positions) # Update function if value of any node in min-heap decreases - def bottomToTop(val, index, heap, position): + def bottom_to_top(val, index, heap, position): temp = position[index] - while(index != 0): + while index != 0: if index % 2 == 0: - parent = int( (index-2) / 2 ) + parent = int((index - 2) / 2) else: - parent = int( (index-1) / 2 ) + parent = int((index - 1) / 2) if val < heap[parent]: heap[index] = heap[parent] position[index] = position[parent] - setPosition(position[parent], index) + set_position(position[parent], index) else: heap[index] = val position[index] = temp - setPosition(temp, index) + set_position(temp, index) break index = parent else: heap[0] = val position[0] = temp - setPosition(temp, 0) + set_position(temp, 0) def heapify(heap, positions): start = len(heap) // 2 - 1 for i in range(start, -1, -1): - topToBottom(heap, i, len(heap), positions) + top_to_bottom(heap, i, len(heap), positions) def deleteMinimum(heap, positions): temp = positions[0] heap[0] = sys.maxsize - topToBottom(heap, 0, len(heap), positions) + top_to_bottom(heap, 0, len(heap), positions) return temp visited = [0 for i in range(len(l))] - Nbr_TV = [-1 for i in range(len(l))] # Neighboring Tree Vertex of selected vertex + Nbr_TV = [-1 for i in range(len(l))] # Neighboring Tree Vertex of selected vertex # Minimum Distance of explored vertex with neighboring vertex of partial tree formed in graph - Distance_TV = [] # Heap of Distance of vertices from their neighboring vertex + Distance_TV = [] # Heap of Distance of vertices from their neighboring vertex Positions = [] for x in range(len(l)): @@ -84,8 +86,8 @@ def deleteMinimum(heap, positions): visited[0] = 1 Distance_TV[0] = sys.maxsize for x in l[0]: - Nbr_TV[ x[0] ] = 0 - Distance_TV[ x[0] ] = x[1] + Nbr_TV[x[0]] = 0 + Distance_TV[x[0]] = x[1] heapify(Distance_TV, Positions) for i in range(1, len(l)): @@ -94,12 +96,13 @@ def deleteMinimum(heap, positions): TreeEdges.append((Nbr_TV[vertex], vertex)) visited[vertex] = 1 for v in l[vertex]: - if visited[v[0]] == 0 and v[1] < Distance_TV[ getPosition(v[0]) ]: - Distance_TV[ getPosition(v[0]) ] = v[1] - bottomToTop(v[1], getPosition(v[0]), Distance_TV, Positions) - Nbr_TV[ v[0] ] = vertex + if visited[v[0]] == 0 and v[1] < Distance_TV[get_position(v[0])]: + Distance_TV[get_position(v[0])] = v[1] + bottom_to_top(v[1], get_position(v[0]), Distance_TV, Positions) + Nbr_TV[v[0]] = vertex return TreeEdges + if __name__ == "__main__": # < --------- Prims Algorithm --------- > n = int(input("Enter number of vertices: ").strip()) @@ -107,6 +110,6 @@ def deleteMinimum(heap, positions): adjlist = defaultdict(list) for x in range(e): l = [int(x) for x in input().strip().split()] - adjlist[l[0]].append([ l[1], l[2] ]) - adjlist[l[1]].append([ l[0], l[2] ]) + adjlist[l[0]].append([l[1], l[2]]) + adjlist[l[1]].append([l[0], l[2]]) print(PrimsAlgorithm(adjlist)) diff --git a/graphs/multi_heuristic_astar.py b/graphs/multi_heuristic_astar.py new file mode 100644 index 000000000000..386aab695bb0 --- /dev/null +++ b/graphs/multi_heuristic_astar.py @@ -0,0 +1,312 @@ +import heapq +import numpy as np + + +class PriorityQueue: + def __init__(self): + self.elements = [] + self.set = set() + + def minkey(self): + if not self.empty(): + return self.elements[0][0] + else: + return float("inf") + + def empty(self): + return len(self.elements) == 0 + + def put(self, item, priority): + if item not in self.set: + heapq.heappush(self.elements, (priority, item)) + self.set.add(item) + else: + # update + # print("update", item) + temp = [] + (pri, x) = heapq.heappop(self.elements) + while x != item: + temp.append((pri, x)) + (pri, x) = heapq.heappop(self.elements) + temp.append((priority, item)) + for (pro, xxx) in temp: + heapq.heappush(self.elements, (pro, xxx)) + + def remove_element(self, item): + if item in self.set: + self.set.remove(item) + temp = [] + (pro, x) = heapq.heappop(self.elements) + while x != item: + temp.append((pro, x)) + (pro, x) = heapq.heappop(self.elements) + for (prito, yyy) in temp: + heapq.heappush(self.elements, (prito, yyy)) + + def top_show(self): + return self.elements[0][1] + + def get(self): + (priority, item) = heapq.heappop(self.elements) + self.set.remove(item) + return (priority, item) + + +def consistent_heuristic(P, goal): + # euclidean distance + a = np.array(P) + b = np.array(goal) + return np.linalg.norm(a - b) + + +def heuristic_2(P, goal): + # integer division by time variable + return consistent_heuristic(P, goal) // t + + +def heuristic_1(P, goal): + # manhattan distance + return abs(P[0] - goal[0]) + abs(P[1] - goal[1]) + + +def key(start, i, goal, g_function): + ans = g_function[start] + W1 * heuristics[i](start, goal) + return ans + + +def do_something(back_pointer, goal, start): + grid = np.chararray((n, n)) + for i in range(n): + for j in range(n): + grid[i][j] = "*" + + for i in range(n): + for j in range(n): + if (j, (n - 1) - i) in blocks: + grid[i][j] = "#" + + grid[0][(n - 1)] = "-" + x = back_pointer[goal] + while x != start: + (x_c, y_c) = x + # print(x) + grid[(n - 1) - y_c][x_c] = "-" + x = back_pointer[x] + grid[(n - 1)][0] = "-" + + for i in range(n): + for j in range(n): + if (i, j) == (0, n - 1): + print(grid[i][j], end=" ") + print("<-- End position", end=" ") + else: + print(grid[i][j], end=" ") + print() + print("^") + print("Start position") + print() + print("# is an obstacle") + print("- is the path taken by algorithm") + print("PATH TAKEN BY THE ALGORITHM IS:-") + x = back_pointer[goal] + while x != start: + print(x, end=" ") + x = back_pointer[x] + print(x) + quit() + + +def valid(p): + if p[0] < 0 or p[0] > n - 1: + return False + if p[1] < 0 or p[1] > n - 1: + return False + return True + + +def expand_state( + s, + j, + visited, + g_function, + close_list_anchor, + close_list_inad, + open_list, + back_pointer, +): + for itera in range(n_heuristic): + open_list[itera].remove_element(s) + # print("s", s) + # print("j", j) + (x, y) = s + left = (x - 1, y) + right = (x + 1, y) + up = (x, y + 1) + down = (x, y - 1) + + for neighbours in [left, right, up, down]: + if neighbours not in blocks: + if valid(neighbours) and neighbours not in visited: + # print("neighbour", neighbours) + visited.add(neighbours) + back_pointer[neighbours] = -1 + g_function[neighbours] = float("inf") + + if valid(neighbours) and g_function[neighbours] > g_function[s] + 1: + g_function[neighbours] = g_function[s] + 1 + back_pointer[neighbours] = s + if neighbours not in close_list_anchor: + open_list[0].put(neighbours, key(neighbours, 0, goal, g_function)) + if neighbours not in close_list_inad: + for var in range(1, n_heuristic): + if key(neighbours, var, goal, g_function) <= W2 * key( + neighbours, 0, goal, g_function + ): + open_list[j].put( + neighbours, key(neighbours, var, goal, g_function) + ) + + +def make_common_ground(): + some_list = [] + for x in range(1, 5): + for y in range(1, 6): + some_list.append((x, y)) + + for x in range(15, 20): + some_list.append((x, 17)) + + for x in range(10, 19): + for y in range(1, 15): + some_list.append((x, y)) + + # L block + for x in range(1, 4): + for y in range(12, 19): + some_list.append((x, y)) + for x in range(3, 13): + for y in range(16, 19): + some_list.append((x, y)) + return some_list + + +heuristics = {0: consistent_heuristic, 1: heuristic_1, 2: heuristic_2} + +blocks_blk = [ + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (4, 1), + (5, 1), + (6, 1), + (7, 1), + (8, 1), + (9, 1), + (10, 1), + (11, 1), + (12, 1), + (13, 1), + (14, 1), + (15, 1), + (16, 1), + (17, 1), + (18, 1), + (19, 1), +] +blocks_no = [] +blocks_all = make_common_ground() + + +blocks = blocks_blk +# hyper parameters +W1 = 1 +W2 = 1 +n = 20 +n_heuristic = 3 # one consistent and two other inconsistent + +# start and end destination +start = (0, 0) +goal = (n - 1, n - 1) + +t = 1 + + +def multi_a_star(start, goal, n_heuristic): + g_function = {start: 0, goal: float("inf")} + back_pointer = {start: -1, goal: -1} + open_list = [] + visited = set() + + for i in range(n_heuristic): + open_list.append(PriorityQueue()) + open_list[i].put(start, key(start, i, goal, g_function)) + + close_list_anchor = [] + close_list_inad = [] + while open_list[0].minkey() < float("inf"): + for i in range(1, n_heuristic): + # print(open_list[0].minkey(), open_list[i].minkey()) + if open_list[i].minkey() <= W2 * open_list[0].minkey(): + global t + t += 1 + if g_function[goal] <= open_list[i].minkey(): + if g_function[goal] < float("inf"): + do_something(back_pointer, goal, start) + else: + _, get_s = open_list[i].top_show() + visited.add(get_s) + expand_state( + get_s, + i, + visited, + g_function, + close_list_anchor, + close_list_inad, + open_list, + back_pointer, + ) + close_list_inad.append(get_s) + else: + if g_function[goal] <= open_list[0].minkey(): + if g_function[goal] < float("inf"): + do_something(back_pointer, goal, start) + else: + get_s = open_list[0].top_show() + visited.add(get_s) + expand_state( + get_s, + 0, + visited, + g_function, + close_list_anchor, + close_list_inad, + open_list, + back_pointer, + ) + close_list_anchor.append(get_s) + print("No path found to goal") + print() + for i in range(n - 1, -1, -1): + for j in range(n): + if (j, i) in blocks: + print("#", end=" ") + elif (j, i) in back_pointer: + if (j, i) == (n - 1, n - 1): + print("*", end=" ") + else: + print("-", end=" ") + else: + print("*", end=" ") + if (j, i) == (n - 1, n - 1): + print("<-- End position", end=" ") + print() + print("^") + print("Start position") + print() + print("# is an obstacle") + print("- is the path taken by algorithm") + + +if __name__ == "__main__": + multi_a_star(start, goal, n_heuristic) diff --git a/graphs/multi_hueristic_astar.py b/graphs/multi_hueristic_astar.py deleted file mode 100644 index 1c01fe9aa6d3..000000000000 --- a/graphs/multi_hueristic_astar.py +++ /dev/null @@ -1,269 +0,0 @@ -from __future__ import print_function -import heapq -import numpy as np - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - - -class PriorityQueue: - def __init__(self): - self.elements = [] - self.set = set() - - def minkey(self): - if not self.empty(): - return self.elements[0][0] - else: - return float('inf') - - def empty(self): - return len(self.elements) == 0 - - def put(self, item, priority): - if item not in self.set: - heapq.heappush(self.elements, (priority, item)) - self.set.add(item) - else: - # update - # print("update", item) - temp = [] - (pri, x) = heapq.heappop(self.elements) - while x != item: - temp.append((pri, x)) - (pri, x) = heapq.heappop(self.elements) - temp.append((priority, item)) - for (pro, xxx) in temp: - heapq.heappush(self.elements, (pro, xxx)) - - def remove_element(self, item): - if item in self.set: - self.set.remove(item) - temp = [] - (pro, x) = heapq.heappop(self.elements) - while x != item: - temp.append((pro, x)) - (pro, x) = heapq.heappop(self.elements) - for (prito, yyy) in temp: - heapq.heappush(self.elements, (prito, yyy)) - - def top_show(self): - return self.elements[0][1] - - def get(self): - (priority, item) = heapq.heappop(self.elements) - self.set.remove(item) - return (priority, item) - -def consistent_hueristic(P, goal): - # euclidean distance - a = np.array(P) - b = np.array(goal) - return np.linalg.norm(a - b) - -def hueristic_2(P, goal): - # integer division by time variable - return consistent_hueristic(P, goal) // t - -def hueristic_1(P, goal): - # manhattan distance - return abs(P[0] - goal[0]) + abs(P[1] - goal[1]) - -def key(start, i, goal, g_function): - ans = g_function[start] + W1 * hueristics[i](start, goal) - return ans - -def do_something(back_pointer, goal, start): - grid = np.chararray((n, n)) - for i in range(n): - for j in range(n): - grid[i][j] = '*' - - for i in range(n): - for j in range(n): - if (j, (n-1)-i) in blocks: - grid[i][j] = "#" - - grid[0][(n-1)] = "-" - x = back_pointer[goal] - while x != start: - (x_c, y_c) = x - # print(x) - grid[(n-1)-y_c][x_c] = "-" - x = back_pointer[x] - grid[(n-1)][0] = "-" - - - for i in xrange(n): - for j in range(n): - if (i, j) == (0, n-1): - print(grid[i][j], end=' ') - print("<-- End position", end=' ') - else: - print(grid[i][j], end=' ') - print() - print("^") - print("Start position") - print() - print("# is an obstacle") - print("- is the path taken by algorithm") - print("PATH TAKEN BY THE ALGORITHM IS:-") - x = back_pointer[goal] - while x != start: - print(x, end=' ') - x = back_pointer[x] - print(x) - quit() - -def valid(p): - if p[0] < 0 or p[0] > n-1: - return False - if p[1] < 0 or p[1] > n-1: - return False - return True - -def expand_state(s, j, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer): - for itera in range(n_hueristic): - open_list[itera].remove_element(s) - # print("s", s) - # print("j", j) - (x, y) = s - left = (x-1, y) - right = (x+1, y) - up = (x, y+1) - down = (x, y-1) - - for neighbours in [left, right, up, down]: - if neighbours not in blocks: - if valid(neighbours) and neighbours not in visited: - # print("neighbour", neighbours) - visited.add(neighbours) - back_pointer[neighbours] = -1 - g_function[neighbours] = float('inf') - - if valid(neighbours) and g_function[neighbours] > g_function[s] + 1: - g_function[neighbours] = g_function[s] + 1 - back_pointer[neighbours] = s - if neighbours not in close_list_anchor: - open_list[0].put(neighbours, key(neighbours, 0, goal, g_function)) - if neighbours not in close_list_inad: - for var in range(1,n_hueristic): - if key(neighbours, var, goal, g_function) <= W2 * key(neighbours, 0, goal, g_function): - # print("why not plssssssssss") - open_list[j].put(neighbours, key(neighbours, var, goal, g_function)) - - - # print - -def make_common_ground(): - some_list = [] - # block 1 - for x in range(1, 5): - for y in range(1, 6): - some_list.append((x, y)) - - # line - for x in range(15, 20): - some_list.append((x, 17)) - - # block 2 big - for x in range(10, 19): - for y in range(1, 15): - some_list.append((x, y)) - - # L block - for x in range(1, 4): - for y in range(12, 19): - some_list.append((x, y)) - for x in range(3, 13): - for y in range(16, 19): - some_list.append((x, y)) - return some_list - -hueristics = {0: consistent_hueristic, 1: hueristic_1, 2: hueristic_2} - -blocks_blk = [(0, 1),(1, 1),(2, 1),(3, 1),(4, 1),(5, 1),(6, 1),(7, 1),(8, 1),(9, 1),(10, 1),(11, 1),(12, 1),(13, 1),(14, 1),(15, 1),(16, 1),(17, 1),(18, 1), (19, 1)] -blocks_no = [] -blocks_all = make_common_ground() - - - - -blocks = blocks_blk -# hyper parameters -W1 = 1 -W2 = 1 -n = 20 -n_hueristic = 3 # one consistent and two other inconsistent - -# start and end destination -start = (0, 0) -goal = (n-1, n-1) - -t = 1 -def multi_a_star(start, goal, n_hueristic): - g_function = {start: 0, goal: float('inf')} - back_pointer = {start:-1, goal:-1} - open_list = [] - visited = set() - - for i in range(n_hueristic): - open_list.append(PriorityQueue()) - open_list[i].put(start, key(start, i, goal, g_function)) - - close_list_anchor = [] - close_list_inad = [] - while open_list[0].minkey() < float('inf'): - for i in range(1, n_hueristic): - # print("i", i) - # print(open_list[0].minkey(), open_list[i].minkey()) - if open_list[i].minkey() <= W2 * open_list[0].minkey(): - global t - t += 1 - # print("less prio") - if g_function[goal] <= open_list[i].minkey(): - if g_function[goal] < float('inf'): - do_something(back_pointer, goal, start) - else: - _, get_s = open_list[i].top_show() - visited.add(get_s) - expand_state(get_s, i, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer) - close_list_inad.append(get_s) - else: - # print("more prio") - if g_function[goal] <= open_list[0].minkey(): - if g_function[goal] < float('inf'): - do_something(back_pointer, goal, start) - else: - # print("hoolla") - get_s = open_list[0].top_show() - visited.add(get_s) - expand_state(get_s, 0, visited, g_function, close_list_anchor, close_list_inad, open_list, back_pointer) - close_list_anchor.append(get_s) - print("No path found to goal") - print() - for i in range(n-1,-1, -1): - for j in range(n): - if (j, i) in blocks: - print('#', end=' ') - elif (j, i) in back_pointer: - if (j, i) == (n-1, n-1): - print('*', end=' ') - else: - print('-', end=' ') - else: - print('*', end=' ') - if (j, i) == (n-1, n-1): - print('<-- End position', end=' ') - print() - print("^") - print("Start position") - print() - print("# is an obstacle") - print("- is the path taken by algorithm") - - -if __name__ == "__main__": - multi_a_star(start, goal, n_hueristic) diff --git a/graphs/page_rank.py b/graphs/page_rank.py index 59f15a99e6b2..0f5129146ddf 100644 --- a/graphs/page_rank.py +++ b/graphs/page_rank.py @@ -1,7 +1,7 @@ -''' +""" Author: https://github.com/bhushan-borole -''' -''' +""" +""" The input graph for the algorithm is: A B C @@ -9,11 +9,9 @@ B 0 0 1 C 1 0 0 -''' +""" -graph = [[0, 1, 1], - [0, 0, 1], - [1, 0, 0]] +graph = [[0, 1, 1], [0, 0, 1], [1, 0, 0]] class Node: @@ -21,17 +19,15 @@ def __init__(self, name): self.name = name self.inbound = [] self.outbound = [] - + def add_inbound(self, node): self.inbound.append(node) - + def add_outbound(self, node): self.outbound.append(node) - + def __repr__(self): - return 'Node {}: Inbound: {} ; Outbound: {}'.format(self.name, - self.inbound, - self.outbound) + return f"Node {self.name}: Inbound: {self.inbound} ; Outbound: {self.outbound}" def page_rank(nodes, limit=3, d=0.85): @@ -44,17 +40,19 @@ def page_rank(nodes, limit=3, d=0.85): outbounds[node.name] = len(node.outbound) for i in range(limit): - print("======= Iteration {} =======".format(i+1)) + print(f"======= Iteration {i + 1} =======") for j, node in enumerate(nodes): - ranks[node.name] = (1 - d) + d * sum([ ranks[ib]/outbounds[ib] for ib in node.inbound ]) + ranks[node.name] = (1 - d) + d * sum( + [ranks[ib] / outbounds[ib] for ib in node.inbound] + ) print(ranks) def main(): - names = list(input('Enter Names of the Nodes: ').split()) + names = list(input("Enter Names of the Nodes: ").split()) nodes = [Node(name) for name in names] - + for ri, row in enumerate(graph): for ci, col in enumerate(row): if col == 1: @@ -68,5 +66,5 @@ def main(): page_rank(nodes) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/graphs/prim.py b/graphs/prim.py index f7e08278966d..a1d46a5a12a4 100644 --- a/graphs/prim.py +++ b/graphs/prim.py @@ -1,27 +1,16 @@ -""" -Prim's Algorithm. - -Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm - -Create a list to store x the vertices. -G = [vertex(n) for n in range(x)] +"""Prim's Algorithm. -For each vertex in G, add the neighbors: -G[x].addNeighbor(G[y]) -G[y].addNeighbor(G[x]) + Determines the minimum spanning tree(MST) of a graph using the Prim's Algorithm. -For each vertex in G, add the edges: -G[x].addEdge(G[y], w) -G[y].addEdge(G[x], w) - -To solve run: -MST = prim(G, G[0]) + Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm """ +import heapq as hq import math +from typing import Iterator -class vertex(): +class Vertex: """Class Vertex.""" def __init__(self, id): @@ -36,44 +25,128 @@ def __init__(self, id): self.key = None self.pi = None self.neighbors = [] - self.edges = {} # [vertex:distance] + self.edges = {} # {vertex:distance} def __lt__(self, other): """Comparison rule to < operator.""" - return (self.key < other.key) + return self.key < other.key def __repr__(self): """Return the vertex id.""" return self.id - def addNeighbor(self, vertex): + def add_neighbor(self, vertex): """Add a pointer to a vertex at neighbor's list.""" self.neighbors.append(vertex) - def addEdge(self, vertex, weight): + def add_edge(self, vertex, weight): """Destination vertex and weight.""" self.edges[vertex.id] = weight -def prim(graph, root): +def connect(graph, a, b, edge): + # add the neighbors: + graph[a - 1].add_neighbor(graph[b - 1]) + graph[b - 1].add_neighbor(graph[a - 1]) + # add the edges: + graph[a - 1].add_edge(graph[b - 1], edge) + graph[b - 1].add_edge(graph[a - 1], edge) + + +def prim(graph: list, root: Vertex) -> list: + """Prim's Algorithm. + + Runtime: + O(mn) with `m` edges and `n` vertices + + Return: + List with the edges of a Minimum Spanning Tree + + Usage: + prim(graph, graph[0]) """ - Prim's Algorithm. - Return a list with the edges of a Minimum Spanning Tree - prim(graph, graph[0]) + a = [] + for u in graph: + u.key = math.inf + u.pi = None + root.key = 0 + q = graph[:] + while q: + u = min(q) + q.remove(u) + for v in u.neighbors: + if (v in q) and (u.edges[v.id] < v.key): + v.pi = u + v.key = u.edges[v.id] + for i in range(1, len(graph)): + a.append((int(graph[i].id) + 1, int(graph[i].pi.id) + 1)) + return a + + +def prim_heap(graph: list, root: Vertex) -> Iterator[tuple]: + """Prim's Algorithm with min heap. + + Runtime: + O((m + n)log n) with `m` edges and `n` vertices + + Yield: + Edges of a Minimum Spanning Tree + + Usage: + prim(graph, graph[0]) """ - A = [] for u in graph: u.key = math.inf u.pi = None root.key = 0 - Q = graph[:] - while Q: - u = min(Q) - Q.remove(u) + + h = [v for v in graph] + hq.heapify(h) + + while h: + u = hq.heappop(h) for v in u.neighbors: - if (v in Q) and (u.edges[v.id] < v.key): + if (v in h) and (u.edges[v.id] < v.key): v.pi = u v.key = u.edges[v.id] + hq.heapify(h) + for i in range(1, len(graph)): - A.append([graph[i].id, graph[i].pi.id]) - return(A) + yield (int(graph[i].id) + 1, int(graph[i].pi.id) + 1) + + +def test_vector() -> None: + """ + # Creates a list to store x vertices. + >>> x = 5 + >>> G = [Vertex(n) for n in range(x)] + + >>> connect(G, 1, 2, 15) + >>> connect(G, 1, 3, 12) + >>> connect(G, 2, 4, 13) + >>> connect(G, 2, 5, 5) + >>> connect(G, 3, 2, 6) + >>> connect(G, 3, 4, 6) + >>> connect(G, 0, 0, 0) # Generate the minimum spanning tree: + >>> G_heap = G[:] + >>> MST = prim(G, G[0]) + >>> MST_heap = prim_heap(G, G[0]) + >>> for i in MST: + ... print(i) + (2, 3) + (3, 1) + (4, 3) + (5, 2) + >>> for i in MST_heap: + ... print(i) + (2, 3) + (3, 1) + (4, 3) + (5, 2) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphs/scc_kosaraju.py b/graphs/scc_kosaraju.py index 0d0375203b6d..573c1bf5e363 100644 --- a/graphs/scc_kosaraju.py +++ b/graphs/scc_kosaraju.py @@ -1,29 +1,31 @@ -from __future__ import print_function - - def dfs(u): global g, r, scc, component, visit, stack - if visit[u]: return + if visit[u]: + return visit[u] = True for v in g[u]: dfs(v) stack.append(u) + def dfs2(u): global g, r, scc, component, visit, stack - if visit[u]: return + if visit[u]: + return visit[u] = True component.append(u) for v in r[u]: dfs2(v) + def kosaraju(): global g, r, scc, component, visit, stack for i in range(n): dfs(i) - visit = [False]*n + visit = [False] * n for i in stack[::-1]: - if visit[i]: continue + if visit[i]: + continue component = [] dfs2(i) scc.append(component) @@ -32,18 +34,18 @@ def kosaraju(): if __name__ == "__main__": # n - no of nodes, m - no of edges - n, m = list(map(int,input().strip().split())) + n, m = list(map(int, input().strip().split())) - g = [[] for i in range(n)] #graph - r = [[] for i in range(n)] #reversed graph + g = [[] for i in range(n)] # graph + r = [[] for i in range(n)] # reversed graph # input graph data (edges) for i in range(m): - u, v = list(map(int,input().strip().split())) + u, v = list(map(int, input().strip().split())) g[u].append(v) r[v].append(u) stack = [] - visit = [False]*n + visit = [False] * n scc = [] component = [] print(kosaraju()) diff --git a/graphs/tarjans_scc.py b/graphs/tarjans_scc.py index 89754e593508..4b0a689ea3c0 100644 --- a/graphs/tarjans_scc.py +++ b/graphs/tarjans_scc.py @@ -36,9 +36,13 @@ def strong_connect(v, index, components): for w in g[v]: if index_of[w] == -1: index = strong_connect(w, index, components) - lowlink_of[v] = lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] + lowlink_of[v] = ( + lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] + ) elif on_stack[w]: - lowlink_of[v] = lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] + lowlink_of[v] = ( + lowlink_of[w] if lowlink_of[w] < lowlink_of[v] else lowlink_of[v] + ) if lowlink_of[v] == index_of[v]: component = [] @@ -67,7 +71,7 @@ def create_graph(n, edges): return g -if __name__ == '__main__': +if __name__ == "__main__": # Test n_vertices = 7 source = [0, 0, 1, 2, 3, 3, 4, 4, 6] diff --git a/hashes/chaos_machine.py b/hashes/chaos_machine.py index f0a305bfeade..8d3bbd4c0251 100644 --- a/hashes/chaos_machine.py +++ b/hashes/chaos_machine.py @@ -1,13 +1,9 @@ """example of simple chaos machine""" -from __future__ import print_function - -try: - input = raw_input # Python 2 -except NameError: - pass # Python 3 # Chaos Machine (K, t, m) -K = [0.33, 0.44, 0.55, 0.44, 0.33]; t = 3; m = 5 +K = [0.33, 0.44, 0.55, 0.44, 0.33] +t = 3 +m = 5 # Buffer Space (with Parameters Space) buffer_space, params_space = [], [] @@ -15,75 +11,73 @@ # Machine Time machine_time = 0 + def push(seed): - global buffer_space, params_space, machine_time, \ - K, m, t + global buffer_space, params_space, machine_time, K, m, t + + # Choosing Dynamical Systems (All) + for key, value in enumerate(buffer_space): + # Evolution Parameter + e = float(seed / value) - # Choosing Dynamical Systems (All) - for key, value in enumerate(buffer_space): - # Evolution Parameter - e = float(seed / value) + # Control Theory: Orbit Change + value = (buffer_space[(key + 1) % m] + e) % 1 - # Control Theory: Orbit Change - value = (buffer_space[(key + 1) % m] + e) % 1 + # Control Theory: Trajectory Change + r = (params_space[key] + e) % 1 + 3 - # Control Theory: Trajectory Change - r = (params_space[key] + e) % 1 + 3 + # Modification (Transition Function) - Jumps + buffer_space[key] = round(float(r * value * (1 - value)), 10) + params_space[key] = r # Saving to Parameters Space - # Modification (Transition Function) - Jumps - buffer_space[key] = \ - round(float(r * value * (1 - value)), 10) - params_space[key] = \ - r # Saving to Parameters Space + # Logistic Map + assert max(buffer_space) < 1 + assert max(params_space) < 4 - # Logistic Map - assert max(buffer_space) < 1 - assert max(params_space) < 4 + # Machine Time + machine_time += 1 - # Machine Time - machine_time += 1 def pull(): - global buffer_space, params_space, machine_time, \ - K, m, t + global buffer_space, params_space, machine_time, K, m, t - # PRNG (Xorshift by George Marsaglia) - def xorshift(X, Y): - X ^= Y >> 13 - Y ^= X << 17 - X ^= Y >> 5 - return X + # PRNG (Xorshift by George Marsaglia) + def xorshift(X, Y): + X ^= Y >> 13 + Y ^= X << 17 + X ^= Y >> 5 + return X - # Choosing Dynamical Systems (Increment) - key = machine_time % m + # Choosing Dynamical Systems (Increment) + key = machine_time % m - # Evolution (Time Length) - for i in range(0, t): - # Variables (Position + Parameters) - r = params_space[key] - value = buffer_space[key] + # Evolution (Time Length) + for i in range(0, t): + # Variables (Position + Parameters) + r = params_space[key] + value = buffer_space[key] - # Modification (Transition Function) - Flow - buffer_space[key] = \ - round(float(r * value * (1 - value)), 10) - params_space[key] = \ - (machine_time * 0.01 + r * 1.01) % 1 + 3 + # Modification (Transition Function) - Flow + buffer_space[key] = round(float(r * value * (1 - value)), 10) + params_space[key] = (machine_time * 0.01 + r * 1.01) % 1 + 3 - # Choosing Chaotic Data - X = int(buffer_space[(key + 2) % m] * (10 ** 10)) - Y = int(buffer_space[(key - 2) % m] * (10 ** 10)) + # Choosing Chaotic Data + X = int(buffer_space[(key + 2) % m] * (10 ** 10)) + Y = int(buffer_space[(key - 2) % m] * (10 ** 10)) - # Machine Time - machine_time += 1 + # Machine Time + machine_time += 1 + + return xorshift(X, Y) % 0xFFFFFFFF - return xorshift(X, Y) % 0xFFFFFFFF def reset(): - global buffer_space, params_space, machine_time, \ - K, m, t + global buffer_space, params_space, machine_time, K, m, t + + buffer_space = K + params_space = [0] * m + machine_time = 0 - buffer_space = K; params_space = [0] * m - machine_time = 0 ####################################### @@ -92,15 +86,17 @@ def reset(): # Pushing Data (Input) import random + message = random.sample(range(0xFFFFFFFF), 100) for chunk in message: - push(chunk) + push(chunk) -# for controlling +# for controlling inp = "" # Pulling Data (Output) while inp in ("e", "E"): - print("%s" % format(pull(), '#04x')) - print(buffer_space); print(params_space) - inp = input("(e)exit? ").strip() + print("%s" % format(pull(), "#04x")) + print(buffer_space) + print(params_space) + inp = input("(e)exit? ").strip() diff --git a/hashes/enigma_machine.py b/hashes/enigma_machine.py index bd410c5cb21d..5420bacc1409 100644 --- a/hashes/enigma_machine.py +++ b/hashes/enigma_machine.py @@ -1,5 +1,3 @@ -from __future__ import print_function - alphabets = [chr(i) for i in range(32, 126)] gear_one = [i for i in range(len(alphabets))] gear_two = [i for i in range(len(alphabets))] @@ -42,7 +40,7 @@ def engine(input_character): rotator() -if __name__ == '__main__': +if __name__ == "__main__": decode = input("Type your message:\n") decode = list(decode) while True: @@ -58,4 +56,5 @@ def engine(input_character): print("\n" + "".join(code)) print( f"\nYour Token is {token} please write it down.\nIf you want to decode " - f"this message again you should input same digits as token!") + f"this message again you should input same digits as token!" + ) diff --git a/hashes/hamming_code.py b/hashes/hamming_code.py new file mode 100644 index 000000000000..c1ed7fe1d727 --- /dev/null +++ b/hashes/hamming_code.py @@ -0,0 +1,312 @@ +# Author: João Gustavo A. Amorim & Gabriel Kunz +# Author email: joaogustavoamorim@gmail.com and gabriel-kunz@uergs.edu.br +# Coding date: apr 2019 +# Black: True + +""" + * This code implement the Hamming code: + https://en.wikipedia.org/wiki/Hamming_code - In telecommunication, + Hamming codes are a family of linear error-correcting codes. Hamming + codes can detect up to two-bit errors or correct one-bit errors + without detection of uncorrected errors. By contrast, the simple + parity code cannot correct errors, and can detect only an odd number + of bits in error. Hamming codes are perfect codes, that is, they + achieve the highest possible rate for codes with their block length + and minimum distance of three. + + * the implemented code consists of: + * a function responsible for encoding the message (emitterConverter) + * return the encoded message + * a function responsible for decoding the message (receptorConverter) + * return the decoded message and a ack of data integrity + + * how to use: + to be used you must declare how many parity bits (sizePari) + you want to include in the message. + it is desired (for test purposes) to select a bit to be set + as an error. This serves to check whether the code is working correctly. + Lastly, the variable of the message/word that must be desired to be + encoded (text). + + * how this work: + declaration of variables (sizePari, be, text) + + converts the message/word (text) to binary using the + text_to_bits function + encodes the message using the rules of hamming encoding + decodes the message using the rules of hamming encoding + print the original message, the encoded message and the + decoded message + + forces an error in the coded text variable + decodes the message that was forced the error + print the original message, the encoded message, the bit changed + message and the decoded message +""" + +# Imports +import numpy as np + +# Functions of binary conversion-------------------------------------- +def text_to_bits(text, encoding="utf-8", errors="surrogatepass"): + """ + >>> text_to_bits("msg") + '011011010111001101100111' + """ + bits = bin(int.from_bytes(text.encode(encoding, errors), "big"))[2:] + return bits.zfill(8 * ((len(bits) + 7) // 8)) + + +def text_from_bits(bits, encoding="utf-8", errors="surrogatepass"): + """ + >>> text_from_bits('011011010111001101100111') + 'msg' + """ + n = int(bits, 2) + return n.to_bytes((n.bit_length() + 7) // 8, "big").decode(encoding, errors) or "\0" + + +# Functions of hamming code------------------------------------------- +def emitterConverter(sizePar, data): + """ + :param sizePar: how many parity bits the message must have + :param data: information bits + :return: message to be transmitted by unreliable medium + - bits of information merged with parity bits + + >>> emitterConverter(4, "101010111111") + ['1', '1', '1', '1', '0', '1', '0', '0', '1', '0', '1', '1', '1', '1', '1', '1'] + """ + if sizePar + len(data) <= 2 ** sizePar - (len(data) - 1): + print("ERROR - size of parity don't match with size of data") + exit(0) + + dataOut = [] + parity = [] + binPos = [bin(x)[2:] for x in range(1, sizePar + len(data) + 1)] + + # sorted information data for the size of the output data + dataOrd = [] + # data position template + parity + dataOutGab = [] + # parity bit counter + qtdBP = 0 + # counter position of data bits + contData = 0 + + for x in range(1, sizePar + len(data) + 1): + # Performs a template of bit positions - who should be given, + # and who should be parity + if qtdBP < sizePar: + if (np.log(x) / np.log(2)).is_integer(): + dataOutGab.append("P") + qtdBP = qtdBP + 1 + else: + dataOutGab.append("D") + else: + dataOutGab.append("D") + + # Sorts the data to the new output size + if dataOutGab[-1] == "D": + dataOrd.append(data[contData]) + contData += 1 + else: + dataOrd.append(None) + + # Calculates parity + qtdBP = 0 # parity bit counter + for bp in range(1, sizePar + 1): + # Bit counter one for a given parity + contBO = 0 + # counter to control the loop reading + contLoop = 0 + for x in dataOrd: + if x is not None: + try: + aux = (binPos[contLoop])[-1 * (bp)] + except IndexError: + aux = "0" + if aux == "1": + if x == "1": + contBO += 1 + contLoop += 1 + if contBO % 2 == 0: + parity.append(0) + else: + parity.append(1) + + qtdBP += 1 + + # Mount the message + ContBP = 0 # parity bit counter + for x in range(0, sizePar + len(data)): + if dataOrd[x] is None: + dataOut.append(str(parity[ContBP])) + ContBP += 1 + else: + dataOut.append(dataOrd[x]) + + return dataOut + + +def receptorConverter(sizePar, data): + """ + >>> receptorConverter(4, "1111010010111111") + (['1', '0', '1', '0', '1', '0', '1', '1', '1', '1', '1', '1'], True) + """ + # data position template + parity + dataOutGab = [] + # Parity bit counter + qtdBP = 0 + # Counter p data bit reading + contData = 0 + # list of parity received + parityReceived = [] + dataOutput = [] + + for x in range(1, len(data) + 1): + # Performs a template of bit positions - who should be given, + # and who should be parity + if qtdBP < sizePar: + if (np.log(x) / np.log(2)).is_integer(): + dataOutGab.append("P") + qtdBP = qtdBP + 1 + else: + dataOutGab.append("D") + else: + dataOutGab.append("D") + + # Sorts the data to the new output size + if dataOutGab[-1] == "D": + dataOutput.append(data[contData]) + else: + parityReceived.append(data[contData]) + contData += 1 + + # -----------calculates the parity with the data + dataOut = [] + parity = [] + binPos = [bin(x)[2:] for x in range(1, sizePar + len(dataOutput) + 1)] + + # sorted information data for the size of the output data + dataOrd = [] + # Data position feedback + parity + dataOutGab = [] + # Parity bit counter + qtdBP = 0 + # Counter p data bit reading + contData = 0 + + for x in range(1, sizePar + len(dataOutput) + 1): + # Performs a template position of bits - who should be given, + # and who should be parity + if qtdBP < sizePar: + if (np.log(x) / np.log(2)).is_integer(): + dataOutGab.append("P") + qtdBP = qtdBP + 1 + else: + dataOutGab.append("D") + else: + dataOutGab.append("D") + + # Sorts the data to the new output size + if dataOutGab[-1] == "D": + dataOrd.append(dataOutput[contData]) + contData += 1 + else: + dataOrd.append(None) + + # Calculates parity + qtdBP = 0 # parity bit counter + for bp in range(1, sizePar + 1): + # Bit counter one for a certain parity + contBO = 0 + # Counter to control loop reading + contLoop = 0 + for x in dataOrd: + if x is not None: + try: + aux = (binPos[contLoop])[-1 * (bp)] + except IndexError: + aux = "0" + if aux == "1": + if x == "1": + contBO += 1 + contLoop += 1 + if contBO % 2 == 0: + parity.append("0") + else: + parity.append("1") + + qtdBP += 1 + + # Mount the message + ContBP = 0 # Parity bit counter + for x in range(0, sizePar + len(dataOutput)): + if dataOrd[x] is None: + dataOut.append(str(parity[ContBP])) + ContBP += 1 + else: + dataOut.append(dataOrd[x]) + + if parityReceived == parity: + ack = True + else: + ack = False + + return dataOutput, ack + + +# --------------------------------------------------------------------- +""" +# Example how to use + +# number of parity bits +sizePari = 4 + +# location of the bit that will be forced an error +be = 2 + +# Message/word to be encoded and decoded with hamming +# text = input("Enter the word to be read: ") +text = "Message01" + +# Convert the message to binary +binaryText = text_to_bits(text) + +# Prints the binary of the string +print("Text input in binary is '" + binaryText + "'") + +# total transmitted bits +totalBits = len(binaryText) + sizePari +print("Size of data is " + str(totalBits)) + +print("\n --Message exchange--") +print("Data to send ------------> " + binaryText) +dataOut = emitterConverter(sizePari, binaryText) +print("Data converted ----------> " + "".join(dataOut)) +dataReceiv, ack = receptorConverter(sizePari, dataOut) +print( + "Data receive ------------> " + + "".join(dataReceiv) + + "\t\t -- Data integrity: " + + str(ack) +) + + +print("\n --Force error--") +print("Data to send ------------> " + binaryText) +dataOut = emitterConverter(sizePari, binaryText) +print("Data converted ----------> " + "".join(dataOut)) + +# forces error +dataOut[-be] = "1" * (dataOut[-be] == "0") + "0" * (dataOut[-be] == "1") +print("Data after transmission -> " + "".join(dataOut)) +dataReceiv, ack = receptorConverter(sizePari, dataOut) +print( + "Data receive ------------> " + + "".join(dataReceiv) + + "\t\t -- Data integrity: " + + str(ack) +) +""" diff --git a/hashes/md5.py b/hashes/md5.py index d3f15510874e..85565533d175 100644 --- a/hashes/md5.py +++ b/hashes/md5.py @@ -1,155 +1,241 @@ -from __future__ import print_function import math + def rearrange(bitString32): - """[summary] - Regroups the given binary string. - - Arguments: - bitString32 {[string]} -- [32 bit binary] - - Raises: - ValueError -- [if the given string not are 32 bit binary string] - - Returns: - [string] -- [32 bit binary string] - """ - - if len(bitString32) != 32: - raise ValueError("Need length 32") - newString = "" - for i in [3,2,1,0]: - newString += bitString32[8*i:8*i+8] - return newString + """[summary] + Regroups the given binary string. + + Arguments: + bitString32 {[string]} -- [32 bit binary] + + Raises: + ValueError -- [if the given string not are 32 bit binary string] + + Returns: + [string] -- [32 bit binary string] + >>> rearrange('1234567890abcdfghijklmnopqrstuvw') + 'pqrstuvwhijklmno90abcdfg12345678' + """ + + if len(bitString32) != 32: + raise ValueError("Need length 32") + newString = "" + for i in [3, 2, 1, 0]: + newString += bitString32[8 * i : 8 * i + 8] + return newString + def reformatHex(i): - """[summary] - Converts the given integer into 8-digit hex number. + """[summary] + Converts the given integer into 8-digit hex number. + + Arguments: + i {[int]} -- [integer] + >>> reformatHex(666) + '9a020000' + """ - Arguments: - i {[int]} -- [integer] - """ + hexrep = format(i, "08x") + thing = "" + for i in [3, 2, 1, 0]: + thing += hexrep[2 * i : 2 * i + 2] + return thing - hexrep = format(i,'08x') - thing = "" - for i in [3,2,1,0]: - thing += hexrep[2*i:2*i+2] - return thing def pad(bitString): - """[summary] - Fills up the binary string to a 512 bit binary string - - Arguments: - bitString {[string]} -- [binary string] - - Returns: - [string] -- [binary string] - """ - - startLength = len(bitString) - bitString += '1' - while len(bitString) % 512 != 448: - bitString += '0' - lastPart = format(startLength,'064b') - bitString += rearrange(lastPart[32:]) + rearrange(lastPart[:32]) - return bitString + """[summary] + Fills up the binary string to a 512 bit binary string + + Arguments: + bitString {[string]} -- [binary string] + + Returns: + [string] -- [binary string] + """ + startLength = len(bitString) + bitString += "1" + while len(bitString) % 512 != 448: + bitString += "0" + lastPart = format(startLength, "064b") + bitString += rearrange(lastPart[32:]) + rearrange(lastPart[:32]) + return bitString + def getBlock(bitString): - """[summary] - Iterator: - Returns by each call a list of length 16 with the 32 bit - integer blocks. - - Arguments: - bitString {[string]} -- [binary string >= 512] - """ - - currPos = 0 - while currPos < len(bitString): - currPart = bitString[currPos:currPos+512] - mySplits = [] - for i in range(16): - mySplits.append(int(rearrange(currPart[32*i:32*i+32]),2)) - yield mySplits - currPos += 512 + """[summary] + Iterator: + Returns by each call a list of length 16 with the 32 bit + integer blocks. + + Arguments: + bitString {[string]} -- [binary string >= 512] + """ + + currPos = 0 + while currPos < len(bitString): + currPart = bitString[currPos : currPos + 512] + mySplits = [] + for i in range(16): + mySplits.append(int(rearrange(currPart[32 * i : 32 * i + 32]), 2)) + yield mySplits + currPos += 512 + def not32(i): - i_str = format(i,'032b') - new_str = '' - for c in i_str: - new_str += '1' if c=='0' else '0' - return int(new_str,2) + """ + >>> not32(34) + 4294967261 + """ + i_str = format(i, "032b") + new_str = "" + for c in i_str: + new_str += "1" if c == "0" else "0" + return int(new_str, 2) + + +def sum32(a, b): + """ -def sum32(a,b): - return (a + b) % 2**32 + """ + return (a + b) % 2 ** 32 + + +def leftrot32(i, s): + return (i << s) ^ (i >> (32 - s)) -def leftrot32(i,s): - return (i << s) ^ (i >> (32-s)) def md5me(testString): - """[summary] - Returns a 32-bit hash code of the string 'testString' - - Arguments: - testString {[string]} -- [message] - """ - - bs ='' - for i in testString: - bs += format(ord(i),'08b') - bs = pad(bs) - - tvals = [int(2**32 * abs(math.sin(i+1))) for i in range(64)] - - a0 = 0x67452301 - b0 = 0xefcdab89 - c0 = 0x98badcfe - d0 = 0x10325476 - - s = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, \ - 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, \ - 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, \ - 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 ] - - for m in getBlock(bs): - A = a0 - B = b0 - C = c0 - D = d0 - for i in range(64): - if i <= 15: - #f = (B & C) | (not32(B) & D) - f = D ^ (B & (C ^ D)) - g = i - elif i<= 31: - #f = (D & B) | (not32(D) & C) - f = C ^ (D & (B ^ C)) - g = (5*i+1) % 16 - elif i <= 47: - f = B ^ C ^ D - g = (3*i+5) % 16 - else: - f = C ^ (B | not32(D)) - g = (7*i) % 16 - dtemp = D - D = C - C = B - B = sum32(B,leftrot32((A + f + tvals[i] + m[g]) % 2**32, s[i])) - A = dtemp - a0 = sum32(a0, A) - b0 = sum32(b0, B) - c0 = sum32(c0, C) - d0 = sum32(d0, D) - - digest = reformatHex(a0) + reformatHex(b0) + reformatHex(c0) + reformatHex(d0) - return digest + """[summary] + Returns a 32-bit hash code of the string 'testString' + + Arguments: + testString {[string]} -- [message] + """ + + bs = "" + for i in testString: + bs += format(ord(i), "08b") + bs = pad(bs) + + tvals = [int(2 ** 32 * abs(math.sin(i + 1))) for i in range(64)] + + a0 = 0x67452301 + b0 = 0xEFCDAB89 + c0 = 0x98BADCFE + d0 = 0x10325476 + + s = [ + 7, + 12, + 17, + 22, + 7, + 12, + 17, + 22, + 7, + 12, + 17, + 22, + 7, + 12, + 17, + 22, + 5, + 9, + 14, + 20, + 5, + 9, + 14, + 20, + 5, + 9, + 14, + 20, + 5, + 9, + 14, + 20, + 4, + 11, + 16, + 23, + 4, + 11, + 16, + 23, + 4, + 11, + 16, + 23, + 4, + 11, + 16, + 23, + 6, + 10, + 15, + 21, + 6, + 10, + 15, + 21, + 6, + 10, + 15, + 21, + 6, + 10, + 15, + 21, + ] + + for m in getBlock(bs): + A = a0 + B = b0 + C = c0 + D = d0 + for i in range(64): + if i <= 15: + # f = (B & C) | (not32(B) & D) + f = D ^ (B & (C ^ D)) + g = i + elif i <= 31: + # f = (D & B) | (not32(D) & C) + f = C ^ (D & (B ^ C)) + g = (5 * i + 1) % 16 + elif i <= 47: + f = B ^ C ^ D + g = (3 * i + 5) % 16 + else: + f = C ^ (B | not32(D)) + g = (7 * i) % 16 + dtemp = D + D = C + C = B + B = sum32(B, leftrot32((A + f + tvals[i] + m[g]) % 2 ** 32, s[i])) + A = dtemp + a0 = sum32(a0, A) + b0 = sum32(b0, B) + c0 = sum32(c0, C) + d0 = sum32(d0, D) + + digest = reformatHex(a0) + reformatHex(b0) + reformatHex(c0) + reformatHex(d0) + return digest + def test(): - assert md5me("") == "d41d8cd98f00b204e9800998ecf8427e" - assert md5me("The quick brown fox jumps over the lazy dog") == "9e107d9d372bb6826bd81d3542a419d6" - print("Success.") + assert md5me("") == "d41d8cd98f00b204e9800998ecf8427e" + assert ( + md5me("The quick brown fox jumps over the lazy dog") + == "9e107d9d372bb6826bd81d3542a419d6" + ) + print("Success.") if __name__ == "__main__": - test() + test() + import doctest + + doctest.testmod() diff --git a/hashes/sha1.py b/hashes/sha1.py index 4c78ad3a89e5..c74ec0c853de 100644 --- a/hashes/sha1.py +++ b/hashes/sha1.py @@ -2,13 +2,13 @@ Demonstrates implementation of SHA1 Hash function in a Python class and gives utilities to find hash of string or hash of text from a file. Usage: python sha1.py --string "Hello World!!" - pyhton sha1.py --file "hello_world.txt" + python sha1.py --file "hello_world.txt" When run without any arguments, it prints the hash of the string "Hello World!! Welcome to Cryptography" Also contains a Test class to verify that the generated Hash is same as that returned by the hashlib library SHA1 hash or SHA1 sum of a string is a crytpographic function which means it is easy -to calculate forwards but extemely difficult to calculate backwards. What this means +to calculate forwards but extremely difficult to calculate backwards. What this means is, you can easily calculate the hash of a string, but it is extremely difficult to know the original string if you have its hash. This property is useful to communicate securely, send encrypted messages and is very useful in payment systems, blockchain @@ -25,14 +25,17 @@ import argparse import struct -import hashlib #hashlib is only used inside the Test class +import hashlib # hashlib is only used inside the Test class import unittest class SHA1Hash: """ Class to contain the entire pipeline for SHA1 Hashing Algorithm + >>> SHA1Hash(bytes('Allan', 'utf-8')).final_hash() + '872af2d8ac3d8695387e7c804bf0e02c18df9e6e' """ + def __init__(self, data): """ Inititates the variables data and h. h is a list of 5 8-digit Hexadecimal @@ -47,32 +50,36 @@ def __init__(self, data): def rotate(n, b): """ Static method to be used inside other methods. Left rotates n by b. + >>> SHA1Hash('').rotate(12,2) + 48 """ - return ((n << b) | (n >> (32 - b))) & 0xffffffff + return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF def padding(self): """ Pads the input message with zeros so that padded_data has 64 bytes or 512 bits """ - padding = b'\x80' + b'\x00'*(63 - (len(self.data) + 8) % 64) - padded_data = self.data + padding + struct.pack('>Q', 8 * len(self.data)) + padding = b"\x80" + b"\x00" * (63 - (len(self.data) + 8) % 64) + padded_data = self.data + padding + struct.pack(">Q", 8 * len(self.data)) return padded_data def split_blocks(self): """ Returns a list of bytestrings each of length 64 """ - return [self.padded_data[i:i+64] for i in range(0, len(self.padded_data), 64)] + return [ + self.padded_data[i : i + 64] for i in range(0, len(self.padded_data), 64) + ] # @staticmethod def expand_block(self, block): """ Takes a bytestring-block of length 64, unpacks it to a list of integers and returns a - list of 80 integers pafter some bit operations + list of 80 integers after some bit operations """ - w = list(struct.unpack('>16L', block)) + [0] * 64 + w = list(struct.unpack(">16L", block)) + [0] * 64 for i in range(16, 80): - w[i] = self.rotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1) + w[i] = self.rotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1) return w def final_hash(self): @@ -102,47 +109,61 @@ def final_hash(self): elif 60 <= i < 80: f = b ^ c ^ d k = 0xCA62C1D6 - a, b, c, d, e = self.rotate(a, 5) + f + e + k + expanded_block[i] & 0xffffffff,\ - a, self.rotate(b, 30), c, d - self.h = self.h[0] + a & 0xffffffff,\ - self.h[1] + b & 0xffffffff,\ - self.h[2] + c & 0xffffffff,\ - self.h[3] + d & 0xffffffff,\ - self.h[4] + e & 0xffffffff - return '%08x%08x%08x%08x%08x' %tuple(self.h) + a, b, c, d, e = ( + self.rotate(a, 5) + f + e + k + expanded_block[i] & 0xFFFFFFFF, + a, + self.rotate(b, 30), + c, + d, + ) + self.h = ( + self.h[0] + a & 0xFFFFFFFF, + self.h[1] + b & 0xFFFFFFFF, + self.h[2] + c & 0xFFFFFFFF, + self.h[3] + d & 0xFFFFFFFF, + self.h[4] + e & 0xFFFFFFFF, + ) + return "%08x%08x%08x%08x%08x" % tuple(self.h) class SHA1HashTest(unittest.TestCase): """ Test class for the SHA1Hash class. Inherits the TestCase class from unittest """ + def testMatchHashes(self): - msg = bytes('Test String', 'utf-8') + msg = bytes("Test String", "utf-8") self.assertEqual(SHA1Hash(msg).final_hash(), hashlib.sha1(msg).hexdigest()) def main(): """ Provides option 'string' or 'file' to take input and prints the calculated SHA1 hash. - unittest.main() has been commented because we probably dont want to run + unittest.main() has been commented because we probably don't want to run the test each time. """ # unittest.main() - parser = argparse.ArgumentParser(description='Process some strings or files') - parser.add_argument('--string', dest='input_string', - default='Hello World!! Welcome to Cryptography', - help='Hash the string') - parser.add_argument('--file', dest='input_file', help='Hash contents of a file') + parser = argparse.ArgumentParser(description="Process some strings or files") + parser.add_argument( + "--string", + dest="input_string", + default="Hello World!! Welcome to Cryptography", + help="Hash the string", + ) + parser.add_argument("--file", dest="input_file", help="Hash contents of a file") args = parser.parse_args() input_string = args.input_string - #In any case hash input should be a bytestring + # In any case hash input should be a bytestring if args.input_file: - with open(args.input_file, 'rb') as f: + with open(args.input_file, "rb") as f: hash_input = f.read() else: - hash_input = bytes(input_string, 'utf-8') + hash_input = bytes(input_string, "utf-8") print(SHA1Hash(hash_input).final_hash()) -if __name__ == '__main__': +if __name__ == "__main__": main() + import doctest + + doctest.testmod() diff --git a/images/Travis_CI_fail_1.png b/images/Travis_CI_fail_1.png new file mode 100644 index 000000000000..451e54e4844a Binary files /dev/null and b/images/Travis_CI_fail_1.png differ diff --git a/images/Travis_CI_fail_2.png b/images/Travis_CI_fail_2.png new file mode 100644 index 000000000000..caa406099da1 Binary files /dev/null and b/images/Travis_CI_fail_2.png differ diff --git a/linear_algebra_python/README.md b/linear_algebra/README.md similarity index 66% rename from linear_algebra_python/README.md rename to linear_algebra/README.md index 1e34d0bd7805..9f8d150a72fa 100644 --- a/linear_algebra_python/README.md +++ b/linear_algebra/README.md @@ -1,13 +1,14 @@ # Linear algebra library for Python -This module contains some useful classes and functions for dealing with linear algebra in python 2. +This module contains classes and functions for doing linear algebra. --- ## Overview -- class Vector - - This class represents a vector of arbitray size and operations on it. +### class Vector +- + - This class represents a vector of arbitrary size and related operations. **Overview about the methods:** @@ -32,7 +33,8 @@ This module contains some useful classes and functions for dealing with linear a - function randomVector(N,a,b) - returns a random vector of size N, with random integer components between 'a' and 'b'. -- class Matrix +### class Matrix +- - This class represents a matrix of arbitrary size and operations on it. **Overview about the methods:** @@ -43,7 +45,8 @@ This module contains some useful classes and functions for dealing with linear a - changeComponent(x,y,value) : changes the specified component. - component(x,y) : returns the specified component. - width() : returns the width of the matrix - - height() : returns the height of the matrix + - height() : returns the height of the matrix + - determinate() : returns the determinate of the matrix if it is square - operator + : implements the matrix-addition. - operator - _ implements the matrix-subtraction @@ -55,22 +58,18 @@ This module contains some useful classes and functions for dealing with linear a ## Documentation -The module is well documented. You can use the python in-built ```help(...)``` function. -For instance: ```help(Vector)``` gives you all information about the Vector-class. -Or ```help(unitBasisVector)``` gives you all information you needed about the -global function ```unitBasisVector(...)```. If you need informations about a certain -method you type ```help(CLASSNAME.METHODNAME)```. +This module uses docstrings to enable the use of Python's in-built `help(...)` function. +For instance, try `help(Vector)`, `help(unitBasisVector)`, and `help(CLASSNAME.METHODNAME)`. --- ## Usage -You will find the module in the **src** directory its called ```lib.py```. You need to -import this module in your project. Alternative you can also use the file ```lib.pyc``` in python-bytecode. +Import the module `lib.py` from the **src** directory into your project. +Alternatively, you can directly use the Python bytecode file `lib.pyc`. --- ## Tests -In the **src** directory you also find the test-suite, its called ```tests.py```. -The test-suite uses the built-in python-test-framework **unittest**. +`src/tests.py` contains Python unit tests which can be run with `python3 -m unittest -v`. diff --git a/linear_algebra_python/src/lib.py b/linear_algebra/src/lib.py similarity index 62% rename from linear_algebra_python/src/lib.py rename to linear_algebra/src/lib.py index 281991a93b2d..f4628f1d964a 100644 --- a/linear_algebra_python/src/lib.py +++ b/linear_algebra/src/lib.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Created on Mon Feb 26 14:29:11 2018 @@ -25,9 +24,9 @@ import random -class Vector(object): +class Vector: """ - This class represents a vector of arbitray size. + This class represents a vector of arbitrary size. You need to give the vector components. Overview about the methods: @@ -37,7 +36,7 @@ class Vector(object): __str__() : toString method component(i : int): gets the i-th component (start by 0) __len__() : gets the size of the vector (number of components) - euclidLength() : returns the eulidean length of the vector. + euclidLength() : returns the euclidean length of the vector. operator + : vector addition operator - : vector subtraction operator * : scalar multiplication and dot product @@ -45,13 +44,17 @@ class Vector(object): changeComponent(pos,value) : changes the specified component. TODO: compare-operator """ - def __init__(self,components=[]): + + def __init__(self, components=None): """ input: components or nothing simple constructor for init the vector """ + if components is None: + components = [] self.__components = list(components) - def set(self,components): + + def set(self, components): """ input: new components changes the components of the vector. @@ -61,34 +64,39 @@ def set(self,components): self.__components = list(components) else: raise Exception("please give any vector") + def __str__(self): """ returns a string representation of the vector """ return "(" + ",".join(map(str, self.__components)) + ")" - def component(self,i): + + def component(self, i): """ input: index (start at 0) output: the i-th component of the vector. """ - if type(i) is int and -len(self.__components) <= i < len(self.__components) : + if type(i) is int and -len(self.__components) <= i < len(self.__components): return self.__components[i] else: raise Exception("index out of range") + def __len__(self): """ returns the size of the vector """ return len(self.__components) - def eulidLength(self): + + def euclidLength(self): """ - returns the eulidean length of the vector + returns the euclidean length of the vector """ summe = 0 for c in self.__components: - summe += c**2 + summe += c ** 2 return math.sqrt(summe) - def __add__(self,other): + + def __add__(self, other): """ input: other vector assumes: other vector has the same size @@ -100,83 +108,91 @@ def __add__(self,other): return Vector(result) else: raise Exception("must have the same size") - def __sub__(self,other): + + def __sub__(self, other): """ input: other vector assumes: other vector has the same size - returns a new vector that represents the differenz. + returns a new vector that represents the difference. """ size = len(self) if size == len(other): result = [self.__components[i] - other.component(i) for i in range(size)] - return result - else: # error case + return Vector(result) + else: # error case raise Exception("must have the same size") - def __mul__(self,other): + + def __mul__(self, other): """ mul implements the scalar multiplication and the dot-product """ - if isinstance(other,float) or isinstance(other,int): - ans = [c*other for c in self.__components] - return ans - elif (isinstance(other,Vector) and (len(self) == len(other))): + if isinstance(other, float) or isinstance(other, int): + ans = [c * other for c in self.__components] + return Vector(ans) + elif isinstance(other, Vector) and (len(self) == len(other)): size = len(self) summe = 0 for i in range(size): summe += self.__components[i] * other.component(i) return summe - else: # error case - raise Exception("invalide operand!") + else: # error case + raise Exception("invalid operand!") + def copy(self): """ copies this vector and returns it. """ return Vector(self.__components) - def changeComponent(self,pos,value): + + def changeComponent(self, pos, value): """ input: an index (pos) and a value changes the specified component (pos) with the 'value' """ - #precondition - assert (-len(self.__components) <= pos < len(self.__components)) + # precondition + assert -len(self.__components) <= pos < len(self.__components) self.__components[pos] = value - + + def zeroVector(dimension): """ returns a zero-vector of size 'dimension' - """ - #precondition - assert(isinstance(dimension,int)) - return Vector([0]*dimension) + """ + # precondition + assert isinstance(dimension, int) + return Vector([0] * dimension) -def unitBasisVector(dimension,pos): +def unitBasisVector(dimension, pos): """ returns a unit basis vector with a One at index 'pos' (indexing at 0) """ - #precondition - assert(isinstance(dimension,int) and (isinstance(pos,int))) - ans = [0]*dimension + # precondition + assert isinstance(dimension, int) and (isinstance(pos, int)) + ans = [0] * dimension ans[pos] = 1 return Vector(ans) - -def axpy(scalar,x,y): + +def axpy(scalar, x, y): """ input: a 'scalar' and two vectors 'x' and 'y' output: a vector computes the axpy operation """ # precondition - assert(isinstance(x,Vector) and (isinstance(y,Vector)) \ - and (isinstance(scalar,int) or isinstance(scalar,float))) - return (x*scalar + y) - + assert ( + isinstance(x, Vector) + and (isinstance(y, Vector)) + and (isinstance(scalar, int) or isinstance(scalar, float)) + ) + return x * scalar + y -def randomVector(N,a,b): + +def randomVector(N, a, b): """ input: size (N) of the vector. random range (a,b) @@ -184,11 +200,11 @@ def randomVector(N,a,b): random integer components between 'a' and 'b'. """ random.seed(None) - ans = [random.randint(a,b) for i in range(N)] + ans = [random.randint(a, b) for i in range(N)] return Vector(ans) -class Matrix(object): +class Matrix: """ class: Matrix This class represents a arbitrary matrix. @@ -205,14 +221,16 @@ class Matrix(object): operator + : implements the matrix-addition. operator - _ implements the matrix-subtraction """ - def __init__(self,matrix,w,h): + + def __init__(self, matrix, w, h): """ - simple constructor for initialzes + simple constructor for initializing the matrix with components. """ self.__matrix = matrix self.__width = w self.__height = h + def __str__(self): """ returns a string representation of this @@ -222,102 +240,140 @@ def __str__(self): for i in range(self.__height): ans += "|" for j in range(self.__width): - if j < self.__width -1: + if j < self.__width - 1: ans += str(self.__matrix[i][j]) + "," else: ans += str(self.__matrix[i][j]) + "|\n" return ans - def changeComponent(self,x,y, value): + + def changeComponent(self, x, y, value): """ changes the x-y component of this matrix """ - if x >= 0 and x < self.__height and y >= 0 and y < self.__width: + if 0 <= x < self.__height and 0 <= y < self.__width: self.__matrix[x][y] = value else: - raise Exception ("changeComponent: indices out of bounds") - def component(self,x,y): + raise Exception("changeComponent: indices out of bounds") + + def component(self, x, y): """ returns the specified (x,y) component """ - if x >= 0 and x < self.__height and y >= 0 and y < self.__width: + if 0 <= x < self.__height and 0 <= y < self.__width: return self.__matrix[x][y] else: - raise Exception ("changeComponent: indices out of bounds") + raise Exception("changeComponent: indices out of bounds") + def width(self): """ getter for the width """ return self.__width + def height(self): """ getter for the height """ return self.__height - def __mul__(self,other): + + def determinate(self) -> float: + """ + returns the determinate of an nxn matrix using Laplace expansion + """ + if self.__height == self.__width and self.__width >= 2: + total = 0 + if self.__width > 2: + for x in range(0, self.__width): + for y in range(0, self.__height): + total += ( + self.__matrix[x][y] + * (-1) ** (x + y) + * Matrix( + self.__matrix[0:x] + self.__matrix[x + 1 :], + self.__width - 1, + self.__height - 1, + ).determinate() + ) + else: + return ( + self.__matrix[0][0] * self.__matrix[1][1] + - self.__matrix[0][1] * self.__matrix[1][0] + ) + return total + else: + raise Exception("matrix is not square") + + def __mul__(self, other): """ implements the matrix-vector multiplication. implements the matrix-scalar multiplication """ - if isinstance(other, Vector): # vector-matrix - if (len(other) == self.__width): + if isinstance(other, Vector): # vector-matrix + if len(other) == self.__width: ans = zeroVector(self.__height) for i in range(self.__height): summe = 0 for j in range(self.__width): summe += other.component(j) * self.__matrix[i][j] - ans.changeComponent(i,summe) + ans.changeComponent(i, summe) summe = 0 return ans else: - raise Exception("vector must have the same size as the " + "number of columns of the matrix!") - elif isinstance(other,int) or isinstance(other,float): # matrix-scalar - matrix = [[self.__matrix[i][j] * other for j in range(self.__width)] for i in range(self.__height)] - return Matrix(matrix,self.__width,self.__height) - def __add__(self,other): + raise Exception( + "vector must have the same size as the " + + "number of columns of the matrix!" + ) + elif isinstance(other, int) or isinstance(other, float): # matrix-scalar + matrix = [ + [self.__matrix[i][j] * other for j in range(self.__width)] + for i in range(self.__height) + ] + return Matrix(matrix, self.__width, self.__height) + + def __add__(self, other): """ implements the matrix-addition. """ - if (self.__width == other.width() and self.__height == other.height()): + if self.__width == other.width() and self.__height == other.height(): matrix = [] for i in range(self.__height): row = [] for j in range(self.__width): - row.append(self.__matrix[i][j] + other.component(i,j)) + row.append(self.__matrix[i][j] + other.component(i, j)) matrix.append(row) - return Matrix(matrix,self.__width,self.__height) + return Matrix(matrix, self.__width, self.__height) else: raise Exception("matrix must have the same dimension!") - def __sub__(self,other): + + def __sub__(self, other): """ implements the matrix-subtraction. """ - if (self.__width == other.width() and self.__height == other.height()): + if self.__width == other.width() and self.__height == other.height(): matrix = [] for i in range(self.__height): row = [] for j in range(self.__width): - row.append(self.__matrix[i][j] - other.component(i,j)) + row.append(self.__matrix[i][j] - other.component(i, j)) matrix.append(row) - return Matrix(matrix,self.__width,self.__height) + return Matrix(matrix, self.__width, self.__height) else: raise Exception("matrix must have the same dimension!") - + def squareZeroMatrix(N): """ returns a square zero-matrix of dimension NxN """ - ans = [[0]*N for i in range(N)] - return Matrix(ans,N,N) - - -def randomMatrix(W,H,a,b): + ans = [[0] * N for i in range(N)] + return Matrix(ans, N, N) + + +def randomMatrix(W, H, a, b): """ returns a random matrix WxH with integer components between 'a' and 'b' """ random.seed(None) - matrix = [[random.randint(a,b) for j in range(W)] for i in range(H)] - return Matrix(matrix,W,H) - - + matrix = [[random.randint(a, b) for j in range(W)] for i in range(H)] + return Matrix(matrix, W, H) diff --git a/linear_algebra/src/polynom-for-points.py b/linear_algebra/src/polynom-for-points.py new file mode 100644 index 000000000000..dc0c3d95102e --- /dev/null +++ b/linear_algebra/src/polynom-for-points.py @@ -0,0 +1,129 @@ +def points_to_polynomial(coordinates): + """ + coordinates is a two dimensional matrix: [[x, y], [x, y], ...] + number of points you want to use + + >>> print(points_to_polynomial([])) + The program cannot work out a fitting polynomial. + >>> print(points_to_polynomial([[]])) + The program cannot work out a fitting polynomial. + >>> print(points_to_polynomial([[1, 0], [2, 0], [3, 0]])) + f(x)=x^2*0.0+x^1*-0.0+x^0*0.0 + >>> print(points_to_polynomial([[1, 1], [2, 1], [3, 1]])) + f(x)=x^2*0.0+x^1*-0.0+x^0*1.0 + >>> print(points_to_polynomial([[1, 3], [2, 3], [3, 3]])) + f(x)=x^2*0.0+x^1*-0.0+x^0*3.0 + >>> print(points_to_polynomial([[1, 1], [2, 2], [3, 3]])) + f(x)=x^2*0.0+x^1*1.0+x^0*0.0 + >>> print(points_to_polynomial([[1, 1], [2, 4], [3, 9]])) + f(x)=x^2*1.0+x^1*-0.0+x^0*0.0 + >>> print(points_to_polynomial([[1, 3], [2, 6], [3, 11]])) + f(x)=x^2*1.0+x^1*-0.0+x^0*2.0 + >>> print(points_to_polynomial([[1, -3], [2, -6], [3, -11]])) + f(x)=x^2*-1.0+x^1*-0.0+x^0*-2.0 + >>> print(points_to_polynomial([[1, 5], [2, 2], [3, 9]])) + f(x)=x^2*5.0+x^1*-18.0+x^0*18.0 + """ + try: + check = 1 + more_check = 0 + d = coordinates[0][0] + for j in range(len(coordinates)): + if j == 0: + continue + if d == coordinates[j][0]: + more_check += 1 + solved = "x=" + str(coordinates[j][0]) + if more_check == len(coordinates) - 1: + check = 2 + break + elif more_check > 0 and more_check != len(coordinates) - 1: + check = 3 + else: + check = 1 + + if len(coordinates) == 1 and coordinates[0][0] == 0: + check = 2 + solved = "x=0" + except Exception: + check = 3 + + x = len(coordinates) + + if check == 1: + count_of_line = 0 + matrix = [] + # put the x and x to the power values in a matrix + while count_of_line < x: + count_in_line = 0 + a = coordinates[count_of_line][0] + count_line = [] + while count_in_line < x: + count_line.append(a ** (x - (count_in_line + 1))) + count_in_line += 1 + matrix.append(count_line) + count_of_line += 1 + + count_of_line = 0 + # put the y values into a vector + vector = [] + while count_of_line < x: + vector.append(coordinates[count_of_line][1]) + count_of_line += 1 + + count = 0 + + while count < x: + zahlen = 0 + while zahlen < x: + if count == zahlen: + zahlen += 1 + if zahlen == x: + break + bruch = (matrix[zahlen][count]) / (matrix[count][count]) + for counting_columns, item in enumerate(matrix[count]): + # manipulating all the values in the matrix + matrix[zahlen][counting_columns] -= item * bruch + # manipulating the values in the vector + vector[zahlen] -= vector[count] * bruch + zahlen += 1 + count += 1 + + count = 0 + # make solutions + solution = [] + while count < x: + solution.append(vector[count] / matrix[count][count]) + count += 1 + + count = 0 + solved = "f(x)=" + + while count < x: + remove_e = str(solution[count]).split("E") + if len(remove_e) > 1: + solution[count] = remove_e[0] + "*10^" + remove_e[1] + solved += "x^" + str(x - (count + 1)) + "*" + str(solution[count]) + if count + 1 != x: + solved += "+" + count += 1 + + return solved + + elif check == 2: + return solved + else: + return "The program cannot work out a fitting polynomial." + + +if __name__ == "__main__": + print(points_to_polynomial([])) + print(points_to_polynomial([[]])) + print(points_to_polynomial([[1, 0], [2, 0], [3, 0]])) + print(points_to_polynomial([[1, 1], [2, 1], [3, 1]])) + print(points_to_polynomial([[1, 3], [2, 3], [3, 3]])) + print(points_to_polynomial([[1, 1], [2, 2], [3, 3]])) + print(points_to_polynomial([[1, 1], [2, 4], [3, 9]])) + print(points_to_polynomial([[1, 3], [2, 6], [3, 11]])) + print(points_to_polynomial([[1, -3], [2, -6], [3, -11]])) + print(points_to_polynomial([[1, 5], [2, 2], [3, 9]])) diff --git a/linear_algebra/src/rayleigh_quotient.py b/linear_algebra/src/rayleigh_quotient.py new file mode 100644 index 000000000000..69bbbac119e8 --- /dev/null +++ b/linear_algebra/src/rayleigh_quotient.py @@ -0,0 +1,64 @@ +""" +https://en.wikipedia.org/wiki/Rayleigh_quotient +""" +import numpy as np + + +def is_hermitian(matrix: np.array) -> bool: + """ + Checks if a matrix is Hermitian. + >>> import numpy as np + >>> A = np.array([ + ... [2, 2+1j, 4], + ... [2-1j, 3, 1j], + ... [4, -1j, 1]]) + >>> is_hermitian(A) + True + >>> A = np.array([ + ... [2, 2+1j, 4+1j], + ... [2-1j, 3, 1j], + ... [4, -1j, 1]]) + >>> is_hermitian(A) + False + """ + return np.array_equal(matrix, matrix.conjugate().T) + + +def rayleigh_quotient(A: np.array, v: np.array) -> float: + """ + Returns the Rayleigh quotient of a Hermitian matrix A and + vector v. + >>> import numpy as np + >>> A = np.array([ + ... [1, 2, 4], + ... [2, 3, -1], + ... [4, -1, 1] + ... ]) + >>> v = np.array([ + ... [1], + ... [2], + ... [3] + ... ]) + >>> rayleigh_quotient(A, v) + array([[3.]]) + """ + v_star = v.conjugate().T + return (v_star.dot(A).dot(v)) / (v_star.dot(v)) + + +def tests() -> None: + A = np.array([[2, 2 + 1j, 4], [2 - 1j, 3, 1j], [4, -1j, 1]]) + v = np.array([[1], [2], [3]]) + assert is_hermitian(A), f"{A} is not hermitian." + print(rayleigh_quotient(A, v)) + + A = np.array([[1, 2, 4], [2, 3, -1], [4, -1, 1]]) + assert is_hermitian(A), f"{A} is not hermitian." + assert rayleigh_quotient(A, v) == float(3) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + tests() diff --git a/linear_algebra/src/test_linear_algebra.py b/linear_algebra/src/test_linear_algebra.py new file mode 100644 index 000000000000..8d2170e46da4 --- /dev/null +++ b/linear_algebra/src/test_linear_algebra.py @@ -0,0 +1,156 @@ +""" +Created on Mon Feb 26 15:40:07 2018 + +@author: Christian Bender +@license: MIT-license + +This file contains the test-suite for the linear algebra library. +""" + +import unittest +from lib import Matrix, Vector, axpy, squareZeroMatrix, unitBasisVector, zeroVector + + +class Test(unittest.TestCase): + def test_component(self): + """ + test for method component + """ + x = Vector([1, 2, 3]) + self.assertEqual(x.component(0), 1) + self.assertEqual(x.component(2), 3) + y = Vector() + + def test_str(self): + """ + test for toString() method + """ + x = Vector([0, 0, 0, 0, 0, 1]) + self.assertEqual(str(x), "(0,0,0,0,0,1)") + + def test_size(self): + """ + test for size()-method + """ + x = Vector([1, 2, 3, 4]) + self.assertEqual(len(x), 4) + + def test_euclidLength(self): + """ + test for the eulidean length + """ + x = Vector([1, 2]) + self.assertAlmostEqual(x.euclidLength(), 2.236, 3) + + def test_add(self): + """ + test for + operator + """ + x = Vector([1, 2, 3]) + y = Vector([1, 1, 1]) + self.assertEqual((x + y).component(0), 2) + self.assertEqual((x + y).component(1), 3) + self.assertEqual((x + y).component(2), 4) + + def test_sub(self): + """ + test for - operator + """ + x = Vector([1, 2, 3]) + y = Vector([1, 1, 1]) + self.assertEqual((x - y).component(0), 0) + self.assertEqual((x - y).component(1), 1) + self.assertEqual((x - y).component(2), 2) + + def test_mul(self): + """ + test for * operator + """ + x = Vector([1, 2, 3]) + a = Vector([2, -1, 4]) # for test of dot-product + b = Vector([1, -2, -1]) + self.assertEqual(str(x * 3.0), "(3.0,6.0,9.0)") + self.assertEqual((a * b), 0) + + def test_zeroVector(self): + """ + test for the global function zeroVector(...) + """ + self.assertTrue(str(zeroVector(10)).count("0") == 10) + + def test_unitBasisVector(self): + """ + test for the global function unitBasisVector(...) + """ + self.assertEqual(str(unitBasisVector(3, 1)), "(0,1,0)") + + def test_axpy(self): + """ + test for the global function axpy(...) (operation) + """ + x = Vector([1, 2, 3]) + y = Vector([1, 0, 1]) + self.assertEqual(str(axpy(2, x, y)), "(3,4,7)") + + def test_copy(self): + """ + test for the copy()-method + """ + x = Vector([1, 0, 0, 0, 0, 0]) + y = x.copy() + self.assertEqual(str(x), str(y)) + + def test_changeComponent(self): + """ + test for the changeComponent(...)-method + """ + x = Vector([1, 0, 0]) + x.changeComponent(0, 0) + x.changeComponent(1, 1) + self.assertEqual(str(x), "(0,1,0)") + + def test_str_matrix(self): + A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) + self.assertEqual("|1,2,3|\n|2,4,5|\n|6,7,8|\n", str(A)) + + def test_determinate(self): + """ + test for determinate() + """ + A = Matrix([[1, 1, 4, 5], [3, 3, 3, 2], [5, 1, 9, 0], [9, 7, 7, 9]], 4, 4) + self.assertEqual(-376, A.determinate()) + + def test__mul__matrix(self): + A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 3, 3) + x = Vector([1, 2, 3]) + self.assertEqual("(14,32,50)", str(A * x)) + self.assertEqual("|2,4,6|\n|8,10,12|\n|14,16,18|\n", str(A * 2)) + + def test_changeComponent_matrix(self): + A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) + A.changeComponent(0, 2, 5) + self.assertEqual("|1,2,5|\n|2,4,5|\n|6,7,8|\n", str(A)) + + def test_component_matrix(self): + A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) + self.assertEqual(7, A.component(2, 1), 0.01) + + def test__add__matrix(self): + A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) + B = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) + self.assertEqual("|2,4,10|\n|4,8,10|\n|12,14,18|\n", str(A + B)) + + def test__sub__matrix(self): + A = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) + B = Matrix([[1, 2, 7], [2, 4, 5], [6, 7, 10]], 3, 3) + self.assertEqual("|0,0,-4|\n|0,0,0|\n|0,0,-2|\n", str(A - B)) + + def test_squareZeroMatrix(self): + self.assertEqual( + "|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|" + "\n|0,0,0,0,0|\n", + str(squareZeroMatrix(5)), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/linear_algebra_python/src/tests.py b/linear_algebra_python/src/tests.py deleted file mode 100644 index a26eb92653e2..000000000000 --- a/linear_algebra_python/src/tests.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Feb 26 15:40:07 2018 - -@author: Christian Bender -@license: MIT-license - -This file contains the test-suite for the linear algebra library. -""" - -import unittest -from lib import * - -class Test(unittest.TestCase): - def test_component(self): - """ - test for method component - """ - x = Vector([1,2,3]) - self.assertEqual(x.component(0),1) - self.assertEqual(x.component(2),3) - try: - y = Vector() - self.assertTrue(False) - except: - self.assertTrue(True) - def test_str(self): - """ - test for toString() method - """ - x = Vector([0,0,0,0,0,1]) - self.assertEqual(str(x),"(0,0,0,0,0,1)") - def test_size(self): - """ - test for size()-method - """ - x = Vector([1,2,3,4]) - self.assertEqual(len(x),4) - def test_euclidLength(self): - """ - test for the eulidean length - """ - x = Vector([1,2]) - self.assertAlmostEqual(x.eulidLength(),2.236,3) - def test_add(self): - """ - test for + operator - """ - x = Vector([1,2,3]) - y = Vector([1,1,1]) - self.assertEqual((x+y).component(0),2) - self.assertEqual((x+y).component(1),3) - self.assertEqual((x+y).component(2),4) - def test_sub(self): - """ - test for - operator - """ - x = Vector([1,2,3]) - y = Vector([1,1,1]) - self.assertEqual((x-y).component(0),0) - self.assertEqual((x-y).component(1),1) - self.assertEqual((x-y).component(2),2) - def test_mul(self): - """ - test for * operator - """ - x = Vector([1,2,3]) - a = Vector([2,-1,4]) # for test of dot-product - b = Vector([1,-2,-1]) - self.assertEqual(str(x*3.0),"(3.0,6.0,9.0)") - self.assertEqual((a*b),0) - def test_zeroVector(self): - """ - test for the global function zeroVector(...) - """ - self.assertTrue(str(zeroVector(10)).count("0") == 10) - def test_unitBasisVector(self): - """ - test for the global function unitBasisVector(...) - """ - self.assertEqual(str(unitBasisVector(3,1)),"(0,1,0)") - def test_axpy(self): - """ - test for the global function axpy(...) (operation) - """ - x = Vector([1,2,3]) - y = Vector([1,0,1]) - self.assertEqual(str(axpy(2,x,y)),"(3,4,7)") - def test_copy(self): - """ - test for the copy()-method - """ - x = Vector([1,0,0,0,0,0]) - y = x.copy() - self.assertEqual(str(x),str(y)) - def test_changeComponent(self): - """ - test for the changeComponent(...)-method - """ - x = Vector([1,0,0]) - x.changeComponent(0,0) - x.changeComponent(1,1) - self.assertEqual(str(x),"(0,1,0)") - def test_str_matrix(self): - A = Matrix([[1,2,3],[2,4,5],[6,7,8]],3,3) - self.assertEqual("|1,2,3|\n|2,4,5|\n|6,7,8|\n",str(A)) - def test__mul__matrix(self): - A = Matrix([[1,2,3],[4,5,6],[7,8,9]],3,3) - x = Vector([1,2,3]) - self.assertEqual("(14,32,50)",str(A*x)) - self.assertEqual("|2,4,6|\n|8,10,12|\n|14,16,18|\n",str(A*2)) - def test_changeComponent_matrix(self): - A = Matrix([[1,2,3],[2,4,5],[6,7,8]],3,3) - A.changeComponent(0,2,5) - self.assertEqual("|1,2,5|\n|2,4,5|\n|6,7,8|\n",str(A)) - def test_component_matrix(self): - A = Matrix([[1,2,3],[2,4,5],[6,7,8]],3,3) - self.assertEqual(7,A.component(2,1),0.01) - def test__add__matrix(self): - A = Matrix([[1,2,3],[2,4,5],[6,7,8]],3,3) - B = Matrix([[1,2,7],[2,4,5],[6,7,10]],3,3) - self.assertEqual("|2,4,10|\n|4,8,10|\n|12,14,18|\n",str(A+B)) - def test__sub__matrix(self): - A = Matrix([[1,2,3],[2,4,5],[6,7,8]],3,3) - B = Matrix([[1,2,7],[2,4,5],[6,7,10]],3,3) - self.assertEqual("|0,0,-4|\n|0,0,0|\n|0,0,-2|\n",str(A-B)) - def test_squareZeroMatrix(self): - self.assertEqual('|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|\n|0,0,0,0,0|' - +'\n|0,0,0,0,0|\n',str(squareZeroMatrix(5))) - - -if __name__ == "__main__": - unittest.main() diff --git a/machine_learning/NaiveBayes.ipynb b/machine_learning/NaiveBayes.ipynb deleted file mode 100644 index 5a427c5cb965..000000000000 --- a/machine_learning/NaiveBayes.ipynb +++ /dev/null @@ -1,1659 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "from sklearn import datasets\n", - "import pandas as pd" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "iris = datasets.load_iris()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "df = pd.DataFrame(iris.data)\n", - "df.columns = [\"sl\", \"sw\", 'pl', 'pw']" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def abc(k, *val):\n", - " if k < val[0]:\n", - " return 0\n", - " else:\n", - " return 1" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 1\n", - "1 0\n", - "2 0\n", - "3 0\n", - "4 1\n", - "5 1\n", - "6 0\n", - "7 1\n", - "8 0\n", - "9 0\n", - "10 1\n", - "11 0\n", - "12 0\n", - "13 0\n", - "14 1\n", - "15 1\n", - "16 1\n", - "17 1\n", - "18 1\n", - "19 1\n", - "20 1\n", - "21 1\n", - "22 0\n", - "23 1\n", - "24 0\n", - "25 1\n", - "26 1\n", - "27 1\n", - "28 1\n", - "29 0\n", - " ..\n", - "120 1\n", - "121 1\n", - "122 1\n", - "123 1\n", - "124 1\n", - "125 1\n", - "126 1\n", - "127 1\n", - "128 1\n", - "129 1\n", - "130 1\n", - "131 1\n", - "132 1\n", - "133 1\n", - "134 1\n", - "135 1\n", - "136 1\n", - "137 1\n", - "138 1\n", - "139 1\n", - "140 1\n", - "141 1\n", - "142 1\n", - "143 1\n", - "144 1\n", - "145 1\n", - "146 1\n", - "147 1\n", - "148 1\n", - "149 1\n", - "Name: sl, dtype: int64" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.sl.apply(abc, args=(5,))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def label(val, *boundaries):\n", - " if (val < boundaries[0]):\n", - " return 'a'\n", - " elif (val < boundaries[1]):\n", - " return 'b'\n", - " elif (val < boundaries[2]):\n", - " return 'c'\n", - " else:\n", - " return 'd'\n", - "\n", - "def toLabel(df, old_feature_name):\n", - " second = df[old_feature_name].mean()\n", - " minimum = df[old_feature_name].min()\n", - " first = (minimum + second)/2\n", - " maximum = df[old_feature_name].max()\n", - " third = (maximum + second)/2\n", - " return df[old_feature_name].apply(label, args= (first, second, third))" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
slswplpwsl_labeledsw_labeledpl_labeledpw_labeled
05.13.51.40.2bcaa
14.93.01.40.2abaa
24.73.21.30.2acaa
34.63.11.50.2acaa
45.03.61.40.2acaa
55.43.91.70.4bdaa
64.63.41.40.3acaa
75.03.41.50.2acaa
84.42.91.40.2abaa
94.93.11.50.1acaa
105.43.71.50.2bcaa
114.83.41.60.2acaa
124.83.01.40.1abaa
134.33.01.10.1abaa
145.84.01.20.2bdaa
155.74.41.50.4bdaa
165.43.91.30.4bdaa
175.13.51.40.3bcaa
185.73.81.70.3bdaa
195.13.81.50.3bdaa
205.43.41.70.2bcaa
215.13.71.50.4bcaa
224.63.61.00.2acaa
235.13.31.70.5bcaa
244.83.41.90.2acaa
255.03.01.60.2abaa
265.03.41.60.4acaa
275.23.51.50.2bcaa
285.23.41.40.2bcaa
294.73.21.60.2acaa
...........................
1206.93.25.72.3dcdd
1215.62.84.92.0bbcd
1227.72.86.72.0dbdd
1236.32.74.91.8cbcc
1246.73.35.72.1ccdd
1257.23.26.01.8dcdc
1266.22.84.81.8cbcc
1276.13.04.91.8cbcc
1286.42.85.62.1cbdd
1297.23.05.81.6dbdc
1307.42.86.11.9dbdd
1317.93.86.42.0dddd
1326.42.85.62.2cbdd
1336.32.85.11.5cbcc
1346.12.65.61.4cbdc
1357.73.06.12.3dbdd
1366.33.45.62.4ccdd
1376.43.15.51.8ccdc
1386.03.04.81.8cbcc
1396.93.15.42.1dcdd
1406.73.15.62.4ccdd
1416.93.15.12.3dccd
1425.82.75.11.9bbcd
1436.83.25.92.3ccdd
1446.73.35.72.5ccdd
1456.73.05.22.3cbcd
1466.32.55.01.9cacd
1476.53.05.22.0cbcd
1486.23.45.42.3ccdd
1495.93.05.11.8cbcc
\n", - "

150 rows × 8 columns

\n", - "
" - ], - "text/plain": [ - " sl sw pl pw sl_labeled sw_labeled pl_labeled pw_labeled\n", - "0 5.1 3.5 1.4 0.2 b c a a\n", - "1 4.9 3.0 1.4 0.2 a b a a\n", - "2 4.7 3.2 1.3 0.2 a c a a\n", - "3 4.6 3.1 1.5 0.2 a c a a\n", - "4 5.0 3.6 1.4 0.2 a c a a\n", - "5 5.4 3.9 1.7 0.4 b d a a\n", - "6 4.6 3.4 1.4 0.3 a c a a\n", - "7 5.0 3.4 1.5 0.2 a c a a\n", - "8 4.4 2.9 1.4 0.2 a b a a\n", - "9 4.9 3.1 1.5 0.1 a c a a\n", - "10 5.4 3.7 1.5 0.2 b c a a\n", - "11 4.8 3.4 1.6 0.2 a c a a\n", - "12 4.8 3.0 1.4 0.1 a b a a\n", - "13 4.3 3.0 1.1 0.1 a b a a\n", - "14 5.8 4.0 1.2 0.2 b d a a\n", - "15 5.7 4.4 1.5 0.4 b d a a\n", - "16 5.4 3.9 1.3 0.4 b d a a\n", - "17 5.1 3.5 1.4 0.3 b c a a\n", - "18 5.7 3.8 1.7 0.3 b d a a\n", - "19 5.1 3.8 1.5 0.3 b d a a\n", - "20 5.4 3.4 1.7 0.2 b c a a\n", - "21 5.1 3.7 1.5 0.4 b c a a\n", - "22 4.6 3.6 1.0 0.2 a c a a\n", - "23 5.1 3.3 1.7 0.5 b c a a\n", - "24 4.8 3.4 1.9 0.2 a c a a\n", - "25 5.0 3.0 1.6 0.2 a b a a\n", - "26 5.0 3.4 1.6 0.4 a c a a\n", - "27 5.2 3.5 1.5 0.2 b c a a\n", - "28 5.2 3.4 1.4 0.2 b c a a\n", - "29 4.7 3.2 1.6 0.2 a c a a\n", - ".. ... ... ... ... ... ... ... ...\n", - "120 6.9 3.2 5.7 2.3 d c d d\n", - "121 5.6 2.8 4.9 2.0 b b c d\n", - "122 7.7 2.8 6.7 2.0 d b d d\n", - "123 6.3 2.7 4.9 1.8 c b c c\n", - "124 6.7 3.3 5.7 2.1 c c d d\n", - "125 7.2 3.2 6.0 1.8 d c d c\n", - "126 6.2 2.8 4.8 1.8 c b c c\n", - "127 6.1 3.0 4.9 1.8 c b c c\n", - "128 6.4 2.8 5.6 2.1 c b d d\n", - "129 7.2 3.0 5.8 1.6 d b d c\n", - "130 7.4 2.8 6.1 1.9 d b d d\n", - "131 7.9 3.8 6.4 2.0 d d d d\n", - "132 6.4 2.8 5.6 2.2 c b d d\n", - "133 6.3 2.8 5.1 1.5 c b c c\n", - "134 6.1 2.6 5.6 1.4 c b d c\n", - "135 7.7 3.0 6.1 2.3 d b d d\n", - "136 6.3 3.4 5.6 2.4 c c d d\n", - "137 6.4 3.1 5.5 1.8 c c d c\n", - "138 6.0 3.0 4.8 1.8 c b c c\n", - "139 6.9 3.1 5.4 2.1 d c d d\n", - "140 6.7 3.1 5.6 2.4 c c d d\n", - "141 6.9 3.1 5.1 2.3 d c c d\n", - "142 5.8 2.7 5.1 1.9 b b c d\n", - "143 6.8 3.2 5.9 2.3 c c d d\n", - "144 6.7 3.3 5.7 2.5 c c d d\n", - "145 6.7 3.0 5.2 2.3 c b c d\n", - "146 6.3 2.5 5.0 1.9 c a c d\n", - "147 6.5 3.0 5.2 2.0 c b c d\n", - "148 6.2 3.4 5.4 2.3 c c d d\n", - "149 5.9 3.0 5.1 1.8 c b c c\n", - "\n", - "[150 rows x 8 columns]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df['sl_labeled'] = toLabel(df, 'sl')\n", - "df['sw_labeled'] = toLabel(df, 'sw')\n", - "df['pl_labeled'] = toLabel(df, 'pl')\n", - "df['pw_labeled'] = toLabel(df, 'pw')\n", - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "df.drop(['sl', 'sw', 'pl', 'pw'], axis = 1, inplace = True)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'a', 'b', 'c', 'd'}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "set(df['sl_labeled'])" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "df[\"output\"] = iris.target" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
sl_labeledsw_labeledpl_labeledpw_labeledoutput
0bcaa0
1abaa0
2acaa0
3acaa0
4acaa0
5bdaa0
6acaa0
7acaa0
8abaa0
9acaa0
10bcaa0
11acaa0
12abaa0
13abaa0
14bdaa0
15bdaa0
16bdaa0
17bcaa0
18bdaa0
19bdaa0
20bcaa0
21bcaa0
22acaa0
23bcaa0
24acaa0
25abaa0
26acaa0
27bcaa0
28bcaa0
29acaa0
..................
120dcdd2
121bbcd2
122dbdd2
123cbcc2
124ccdd2
125dcdc2
126cbcc2
127cbcc2
128cbdd2
129dbdc2
130dbdd2
131dddd2
132cbdd2
133cbcc2
134cbdc2
135dbdd2
136ccdd2
137ccdc2
138cbcc2
139dcdd2
140ccdd2
141dccd2
142bbcd2
143ccdd2
144ccdd2
145cbcd2
146cacd2
147cbcd2
148ccdd2
149cbcc2
\n", - "

150 rows × 5 columns

\n", - "
" - ], - "text/plain": [ - " sl_labeled sw_labeled pl_labeled pw_labeled output\n", - "0 b c a a 0\n", - "1 a b a a 0\n", - "2 a c a a 0\n", - "3 a c a a 0\n", - "4 a c a a 0\n", - "5 b d a a 0\n", - "6 a c a a 0\n", - "7 a c a a 0\n", - "8 a b a a 0\n", - "9 a c a a 0\n", - "10 b c a a 0\n", - "11 a c a a 0\n", - "12 a b a a 0\n", - "13 a b a a 0\n", - "14 b d a a 0\n", - "15 b d a a 0\n", - "16 b d a a 0\n", - "17 b c a a 0\n", - "18 b d a a 0\n", - "19 b d a a 0\n", - "20 b c a a 0\n", - "21 b c a a 0\n", - "22 a c a a 0\n", - "23 b c a a 0\n", - "24 a c a a 0\n", - "25 a b a a 0\n", - "26 a c a a 0\n", - "27 b c a a 0\n", - "28 b c a a 0\n", - "29 a c a a 0\n", - ".. ... ... ... ... ...\n", - "120 d c d d 2\n", - "121 b b c d 2\n", - "122 d b d d 2\n", - "123 c b c c 2\n", - "124 c c d d 2\n", - "125 d c d c 2\n", - "126 c b c c 2\n", - "127 c b c c 2\n", - "128 c b d d 2\n", - "129 d b d c 2\n", - "130 d b d d 2\n", - "131 d d d d 2\n", - "132 c b d d 2\n", - "133 c b c c 2\n", - "134 c b d c 2\n", - "135 d b d d 2\n", - "136 c c d d 2\n", - "137 c c d c 2\n", - "138 c b c c 2\n", - "139 d c d d 2\n", - "140 c c d d 2\n", - "141 d c c d 2\n", - "142 b b c d 2\n", - "143 c c d d 2\n", - "144 c c d d 2\n", - "145 c b c d 2\n", - "146 c a c d 2\n", - "147 c b c d 2\n", - "148 c c d d 2\n", - "149 c b c c 2\n", - "\n", - "[150 rows x 5 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def fit(data):\n", - " output_name = data.columns[-1]\n", - " features = data.columns[0:-1]\n", - " counts = {}\n", - " possible_outputs = set(data[output_name])\n", - " for output in possible_outputs:\n", - " counts[output] = {}\n", - " smallData = data[data[output_name] == output]\n", - " counts[output][\"total_count\"] = len(smallData)\n", - " for f in features:\n", - " counts[output][f] = {}\n", - " possible_values = set(smallData[f])\n", - " for value in possible_values:\n", - " val_count = len(smallData[smallData[f] == value])\n", - " counts[output][f][value] = val_count\n", - " return counts" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{0: {'pl_labeled': {'a': 50},\n", - " 'pw_labeled': {'a': 50},\n", - " 'sl_labeled': {'a': 28, 'b': 22},\n", - " 'sw_labeled': {'a': 1, 'b': 7, 'c': 32, 'd': 10},\n", - " 'total_count': 50},\n", - " 1: {'pl_labeled': {'b': 7, 'c': 43},\n", - " 'pw_labeled': {'b': 10, 'c': 40},\n", - " 'sl_labeled': {'a': 3, 'b': 21, 'c': 24, 'd': 2},\n", - " 'sw_labeled': {'a': 13, 'b': 29, 'c': 8},\n", - " 'total_count': 50},\n", - " 2: {'pl_labeled': {'c': 20, 'd': 30},\n", - " 'pw_labeled': {'c': 16, 'd': 34},\n", - " 'sl_labeled': {'a': 1, 'b': 5, 'c': 29, 'd': 15},\n", - " 'sw_labeled': {'a': 5, 'b': 28, 'c': 15, 'd': 2},\n", - " 'total_count': 50}}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "fit(df)" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.5" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/machine_learning/decision_tree.py b/machine_learning/decision_tree.py index 71849904ccf2..fe1d54736563 100644 --- a/machine_learning/decision_tree.py +++ b/machine_learning/decision_tree.py @@ -1,14 +1,13 @@ """ Implementation of a basic regression decision tree. Input data set: The input data set must be 1-dimensional with continuous labels. -Output: The decision tree maps a real number input to a real number output. +Output: The decision tree maps a real number input to a real number output. """ -from __future__ import print_function - import numpy as np + class Decision_Tree: - def __init__(self, depth = 5, min_leaf_size = 5): + def __init__(self, depth=5, min_leaf_size=5): self.depth = depth self.decision_boundary = 0 self.left = None @@ -19,9 +18,23 @@ def __init__(self, depth = 5, min_leaf_size = 5): def mean_squared_error(self, labels, prediction): """ mean_squared_error: - @param labels: a one dimensional numpy array + @param labels: a one dimensional numpy array @param prediction: a floating point value - return value: mean_squared_error calculates the error if prediction is used to estimate the labels + return value: mean_squared_error calculates the error if prediction is used to + estimate the labels + >>> tester = Decision_Tree() + >>> test_labels = np.array([1,2,3,4,5,6,7,8,9,10]) + >>> test_prediction = np.float(6) + >>> tester.mean_squared_error(test_labels, test_prediction) == ( + ... Test_Decision_Tree.helper_mean_squared_error_test(test_labels, + ... test_prediction)) + True + >>> test_labels = np.array([1,2,3]) + >>> test_prediction = np.float(2) + >>> tester.mean_squared_error(test_labels, test_prediction) == ( + ... Test_Decision_Tree.helper_mean_squared_error_test(test_labels, + ... test_prediction)) + True """ if labels.ndim != 1: print("Error: Input labels must be one dimensional") @@ -32,14 +45,15 @@ def train(self, X, y): """ train: @param X: a one dimensional numpy array - @param y: a one dimensional numpy array. + @param y: a one dimensional numpy array. The contents of y are the labels for the corresponding X values train does not have a return value """ """ - this section is to check that the inputs conform to our dimensionality constraints + this section is to check that the inputs conform to our dimensionality + constraints """ if X.ndim != 1: print("Error: Input data set must be one dimensional") @@ -60,13 +74,13 @@ def train(self, X, y): return best_split = 0 - min_error = self.mean_squared_error(X,np.mean(y)) * 2 - + min_error = self.mean_squared_error(X, np.mean(y)) * 2 """ loop over all possible splits for the decision tree. find the best split. if no split exists that is less than 2 * error for the entire array - then the data set is not split and the average for the entire array is used as the predictor + then the data set is not split and the average for the entire array is used as + the predictor """ for i in range(len(X)): if len(X[:i]) < self.min_leaf_size: @@ -88,8 +102,12 @@ def train(self, X, y): right_y = y[best_split:] self.decision_boundary = X[best_split] - self.left = Decision_Tree(depth = self.depth - 1, min_leaf_size = self.min_leaf_size) - self.right = Decision_Tree(depth = self.depth - 1, min_leaf_size = self.min_leaf_size) + self.left = Decision_Tree( + depth=self.depth - 1, min_leaf_size=self.min_leaf_size + ) + self.right = Decision_Tree( + depth=self.depth - 1, min_leaf_size=self.min_leaf_size + ) self.left.train(left_X, left_y) self.right.train(right_X, right_y) else: @@ -115,17 +133,38 @@ def predict(self, x): print("Error: Decision tree not yet trained") return None + +class Test_Decision_Tree: + """Decision Tres test class + """ + + @staticmethod + def helper_mean_squared_error_test(labels, prediction): + """ + helper_mean_squared_error_test: + @param labels: a one dimensional numpy array + @param prediction: a floating point value + return value: helper_mean_squared_error_test calculates the mean squared error + """ + squared_error_sum = np.float(0) + for label in labels: + squared_error_sum += (label - prediction) ** 2 + + return np.float(squared_error_sum / labels.size) + + def main(): """ - In this demonstration we're generating a sample data set from the sin function in numpy. - We then train a decision tree on the data set and use the decision tree to predict the - label of 10 different test values. Then the mean squared error over this test is displayed. + In this demonstration we're generating a sample data set from the sin function in + numpy. We then train a decision tree on the data set and use the decision tree to + predict the label of 10 different test values. Then the mean squared error over + this test is displayed. """ - X = np.arange(-1., 1., 0.005) + X = np.arange(-1.0, 1.0, 0.005) y = np.sin(X) - tree = Decision_Tree(depth = 10, min_leaf_size = 10) - tree.train(X,y) + tree = Decision_Tree(depth=10, min_leaf_size=10) + tree.train(X, y) test_cases = (np.random.rand(10) * 2) - 1 predictions = np.array([tree.predict(x) for x in test_cases]) @@ -135,6 +174,9 @@ def main(): print("Predictions: " + str(predictions)) print("Average error: " + str(avg_error)) - -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() + import doctest + + doctest.testmod(name="mean_squarred_error", verbose=True) diff --git a/machine_learning/gaussian_naive_bayes.py b/machine_learning/gaussian_naive_bayes.py new file mode 100644 index 000000000000..24c884adb98a --- /dev/null +++ b/machine_learning/gaussian_naive_bayes.py @@ -0,0 +1,45 @@ +# Gaussian Naive Bayes Example + +from sklearn.naive_bayes import GaussianNB +from sklearn.metrics import plot_confusion_matrix +from sklearn.datasets import load_iris +from sklearn.model_selection import train_test_split +import matplotlib.pyplot as plt + + +def main(): + + """ + Gaussian Naive Bayes Example using sklearn function. + Iris type dataset is used to demonstrate algorithm. + """ + + # Load Iris dataset + iris = load_iris() + + # Split dataset into train and test data + X = iris["data"] # features + Y = iris["target"] + x_train, x_test, y_train, y_test = train_test_split( + X, Y, test_size=0.3, random_state=1 + ) + + # Gaussian Naive Bayes + NB_model = GaussianNB() + NB_model.fit(x_train, y_train) + + # Display Confusion Matrix + plot_confusion_matrix( + NB_model, + x_test, + y_test, + display_labels=iris["target_names"], + cmap="Blues", + normalize="true", + ) + plt.title("Normalized Confusion Matrix - IRIS Dataset") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/machine_learning/gradient_descent.py b/machine_learning/gradient_descent.py index 6387d4939205..811cc68467f9 100644 --- a/machine_learning/gradient_descent.py +++ b/machine_learning/gradient_descent.py @@ -1,25 +1,31 @@ """ Implementation of gradient descent algorithm for minimizing cost of a linear hypothesis function. """ -from __future__ import print_function, division import numpy # List of input, output pairs -train_data = (((5, 2, 3), 15), ((6, 5, 9), 25), - ((11, 12, 13), 41), ((1, 1, 1), 8), ((11, 12, 13), 41)) +train_data = ( + ((5, 2, 3), 15), + ((6, 5, 9), 25), + ((11, 12, 13), 41), + ((1, 1, 1), 8), + ((11, 12, 13), 41), +) test_data = (((515, 22, 13), 555), ((61, 35, 49), 150)) parameter_vector = [2, 4, 1, 5] m = len(train_data) LEARNING_RATE = 0.009 -def _error(example_no, data_set='train'): +def _error(example_no, data_set="train"): """ :param data_set: train data or test data :param example_no: example number whose error has to be checked :return: error in example pointed by example number. """ - return calculate_hypothesis_value(example_no, data_set) - output(example_no, data_set) + return calculate_hypothesis_value(example_no, data_set) - output( + example_no, data_set + ) def _hypothesis_value(data_input_tuple): @@ -33,7 +39,7 @@ def _hypothesis_value(data_input_tuple): """ hyp_val = 0 for i in range(len(parameter_vector) - 1): - hyp_val += data_input_tuple[i]*parameter_vector[i+1] + hyp_val += data_input_tuple[i] * parameter_vector[i + 1] hyp_val += parameter_vector[0] return hyp_val @@ -44,9 +50,9 @@ def output(example_no, data_set): :param example_no: example whose output is to be fetched :return: output for that example """ - if data_set == 'train': + if data_set == "train": return train_data[example_no][1] - elif data_set == 'test': + elif data_set == "test": return test_data[example_no][1] @@ -76,7 +82,7 @@ def summation_of_cost_derivative(index, end=m): if index == -1: summation_value += _error(i) else: - summation_value += _error(i)*train_data[i][0][index] + summation_value += _error(i) * train_data[i][0][index] return summation_value @@ -86,7 +92,7 @@ def get_cost_derivative(index): :return: derivative wrt to that index Note: If index is -1, this means we are calculating summation wrt to biased parameter. """ - cost_derivative_value = summation_of_cost_derivative(index, m)/m + cost_derivative_value = summation_of_cost_derivative(index, m) / m return cost_derivative_value @@ -100,11 +106,16 @@ def run_gradient_descent(): j += 1 temp_parameter_vector = [0, 0, 0, 0] for i in range(0, len(parameter_vector)): - cost_derivative = get_cost_derivative(i-1) - temp_parameter_vector[i] = parameter_vector[i] - \ - LEARNING_RATE*cost_derivative - if numpy.allclose(parameter_vector, temp_parameter_vector, - atol=absolute_error_limit, rtol=relative_error_limit): + cost_derivative = get_cost_derivative(i - 1) + temp_parameter_vector[i] = ( + parameter_vector[i] - LEARNING_RATE * cost_derivative + ) + if numpy.allclose( + parameter_vector, + temp_parameter_vector, + atol=absolute_error_limit, + rtol=relative_error_limit, + ): break parameter_vector = temp_parameter_vector print(("Number of iterations:", j)) @@ -112,11 +123,11 @@ def run_gradient_descent(): def test_gradient_descent(): for i in range(len(test_data)): - print(("Actual output value:", output(i, 'test'))) - print(("Hypothesis output:", calculate_hypothesis_value(i, 'test'))) + print(("Actual output value:", output(i, "test"))) + print(("Hypothesis output:", calculate_hypothesis_value(i, "test"))) -if __name__ == '__main__': +if __name__ == "__main__": run_gradient_descent() print("\nTesting gradient descent for a linear hypothesis function.\n") test_gradient_descent() diff --git a/machine_learning/k_means_clust.py b/machine_learning/k_means_clust.py index 368739a45fe9..7a4f69eb77ce 100644 --- a/machine_learning/k_means_clust.py +++ b/machine_learning/k_means_clust.py @@ -1,4 +1,4 @@ -'''README, Author - Anurag Kumar(mailto:anuragkumarak95@gmail.com) +"""README, Author - Anurag Kumar(mailto:anuragkumarak95@gmail.com) Requirements: - sklearn @@ -17,157 +17,183 @@ Usage: 1. define 'k' value, 'X' features array and 'hetrogeneity' empty list - + 2. create initial_centroids, initial_centroids = get_initial_centroids( - X, - k, + X, + k, seed=0 # seed value for initial centroid generation, None for randomness(default=None) ) 3. find centroids and clusters using kmeans function. - + centroids, cluster_assignment = kmeans( - X, - k, - initial_centroids, + X, + k, + initial_centroids, maxiter=400, - record_heterogeneity=heterogeneity, + record_heterogeneity=heterogeneity, verbose=True # whether to print logs in console or not.(default=False) ) - - + + 4. Plot the loss function, hetrogeneity values for every iteration saved in hetrogeneity list. plot_heterogeneity( - heterogeneity, + heterogeneity, k ) - + 5. Have fun.. - -''' -from __future__ import print_function + +""" from sklearn.metrics import pairwise_distances import numpy as np -TAG = 'K-MEANS-CLUST/ ' +TAG = "K-MEANS-CLUST/ " + def get_initial_centroids(data, k, seed=None): - '''Randomly choose k data points as initial centroids''' - if seed is not None: # useful for obtaining consistent results + """Randomly choose k data points as initial centroids""" + if seed is not None: # useful for obtaining consistent results np.random.seed(seed) - n = data.shape[0] # number of data points - + n = data.shape[0] # number of data points + # Pick K indices from range [0, N). rand_indices = np.random.randint(0, n, k) - + # Keep centroids as dense format, as many entries will be nonzero due to averaging. # As long as at least one document in a cluster contains a word, # it will carry a nonzero weight in the TF-IDF vector of the centroid. - centroids = data[rand_indices,:] - + centroids = data[rand_indices, :] + return centroids -def centroid_pairwise_dist(X,centroids): - return pairwise_distances(X,centroids,metric='euclidean') + +def centroid_pairwise_dist(X, centroids): + return pairwise_distances(X, centroids, metric="euclidean") + def assign_clusters(data, centroids): - + # Compute distances between each data point and the set of centroids: # Fill in the blank (RHS only) - distances_from_centroids = centroid_pairwise_dist(data,centroids) - + distances_from_centroids = centroid_pairwise_dist(data, centroids) + # Compute cluster assignments for each data point: # Fill in the blank (RHS only) - cluster_assignment = np.argmin(distances_from_centroids,axis=1) - + cluster_assignment = np.argmin(distances_from_centroids, axis=1) + return cluster_assignment + def revise_centroids(data, k, cluster_assignment): new_centroids = [] for i in range(k): # Select all data points that belong to cluster i. Fill in the blank (RHS only) - member_data_points = data[cluster_assignment==i] + member_data_points = data[cluster_assignment == i] # Compute the mean of the data points. Fill in the blank (RHS only) centroid = member_data_points.mean(axis=0) new_centroids.append(centroid) new_centroids = np.array(new_centroids) - + return new_centroids + def compute_heterogeneity(data, k, centroids, cluster_assignment): - + heterogeneity = 0.0 for i in range(k): - + # Select all data points that belong to cluster i. Fill in the blank (RHS only) - member_data_points = data[cluster_assignment==i, :] - - if member_data_points.shape[0] > 0: # check if i-th cluster is non-empty + member_data_points = data[cluster_assignment == i, :] + + if member_data_points.shape[0] > 0: # check if i-th cluster is non-empty # Compute distances from centroid to data points (RHS only) - distances = pairwise_distances(member_data_points, [centroids[i]], metric='euclidean') - squared_distances = distances**2 + distances = pairwise_distances( + member_data_points, [centroids[i]], metric="euclidean" + ) + squared_distances = distances ** 2 heterogeneity += np.sum(squared_distances) - + return heterogeneity + from matplotlib import pyplot as plt + + def plot_heterogeneity(heterogeneity, k): - plt.figure(figsize=(7,4)) + plt.figure(figsize=(7, 4)) plt.plot(heterogeneity, linewidth=4) - plt.xlabel('# Iterations') - plt.ylabel('Heterogeneity') - plt.title('Heterogeneity of clustering over time, K={0:d}'.format(k)) - plt.rcParams.update({'font.size': 16}) + plt.xlabel("# Iterations") + plt.ylabel("Heterogeneity") + plt.title(f"Heterogeneity of clustering over time, K={k:d}") + plt.rcParams.update({"font.size": 16}) plt.show() -def kmeans(data, k, initial_centroids, maxiter=500, record_heterogeneity=None, verbose=False): - '''This function runs k-means on given data and initial set of centroids. + +def kmeans( + data, k, initial_centroids, maxiter=500, record_heterogeneity=None, verbose=False +): + """This function runs k-means on given data and initial set of centroids. maxiter: maximum number of iterations to run.(default=500) record_heterogeneity: (optional) a list, to store the history of heterogeneity as function of iterations if None, do not store the history. - verbose: if True, print how many data points changed their cluster labels in each iteration''' + verbose: if True, print how many data points changed their cluster labels in each iteration""" centroids = initial_centroids[:] prev_cluster_assignment = None - - for itr in range(maxiter): + + for itr in range(maxiter): if verbose: - print(itr, end='') - + print(itr, end="") + # 1. Make cluster assignments using nearest centroids - cluster_assignment = assign_clusters(data,centroids) - + cluster_assignment = assign_clusters(data, centroids) + # 2. Compute a new centroid for each of the k clusters, averaging all data points assigned to that cluster. - centroids = revise_centroids(data,k, cluster_assignment) - + centroids = revise_centroids(data, k, cluster_assignment) + # Check for convergence: if none of the assignments changed, stop - if prev_cluster_assignment is not None and \ - (prev_cluster_assignment==cluster_assignment).all(): + if ( + prev_cluster_assignment is not None + and (prev_cluster_assignment == cluster_assignment).all() + ): break - - # Print number of new assignments + + # Print number of new assignments if prev_cluster_assignment is not None: - num_changed = np.sum(prev_cluster_assignment!=cluster_assignment) + num_changed = np.sum(prev_cluster_assignment != cluster_assignment) if verbose: - print(' {0:5d} elements changed their cluster assignment.'.format(num_changed)) - + print( + " {:5d} elements changed their cluster assignment.".format( + num_changed + ) + ) + # Record heterogeneity convergence metric if record_heterogeneity is not None: # YOUR CODE HERE - score = compute_heterogeneity(data,k,centroids,cluster_assignment) + score = compute_heterogeneity(data, k, centroids, cluster_assignment) record_heterogeneity.append(score) - + prev_cluster_assignment = cluster_assignment[:] - + return centroids, cluster_assignment + # Mock test below -if False: # change to true to run this test case. +if False: # change to true to run this test case. import sklearn.datasets as ds + dataset = ds.load_iris() k = 3 heterogeneity = [] - initial_centroids = get_initial_centroids(dataset['data'], k, seed=0) - centroids, cluster_assignment = kmeans(dataset['data'], k, initial_centroids, maxiter=400, - record_heterogeneity=heterogeneity, verbose=True) + initial_centroids = get_initial_centroids(dataset["data"], k, seed=0) + centroids, cluster_assignment = kmeans( + dataset["data"], + k, + initial_centroids, + maxiter=400, + record_heterogeneity=heterogeneity, + verbose=True, + ) plot_heterogeneity(heterogeneity, k) diff --git a/machine_learning/k_nearest_neighbours.py b/machine_learning/k_nearest_neighbours.py new file mode 100644 index 000000000000..481a8e1dbcd0 --- /dev/null +++ b/machine_learning/k_nearest_neighbours.py @@ -0,0 +1,57 @@ +import numpy as np +from collections import Counter +from sklearn import datasets +from sklearn.model_selection import train_test_split + +data = datasets.load_iris() + +X = np.array(data["data"]) +y = np.array(data["target"]) +classes = data["target_names"] + +X_train, X_test, y_train, y_test = train_test_split(X, y) + + +def euclidean_distance(a, b): + """ + Gives the euclidean distance between two points + >>> euclidean_distance([0, 0], [3, 4]) + 5.0 + >>> euclidean_distance([1, 2, 3], [1, 8, 11]) + 10.0 + """ + return np.linalg.norm(np.array(a) - np.array(b)) + + +def classifier(train_data, train_target, classes, point, k=5): + """ + Classifies the point using the KNN algorithm + k closest points are found (ranked in ascending order of euclidean distance) + Params: + :train_data: Set of points that are classified into two or more classes + :train_target: List of classes in the order of train_data points + :classes: Labels of the classes + :point: The data point that needs to be classifed + + >>> X_train = [[0, 0], [1, 0], [0, 1], [0.5, 0.5], [3, 3], [2, 3], [3, 2]] + >>> y_train = [0, 0, 0, 0, 1, 1, 1] + >>> classes = ['A','B']; point = [1.2,1.2] + >>> classifier(X_train, y_train, classes,point) + 'A' + """ + data = zip(train_data, train_target) + # List of distances of all points from the point to be classified + distances = [] + for data_point in data: + distance = euclidean_distance(data_point[0], point) + distances.append((distance, data_point[1])) + # Choosing 'k' points with the least distances. + votes = [i[1] for i in sorted(distances)[:k]] + # Most commonly occurring class among them + # is the class into which the point is classified + result = Counter(votes).most_common(1)[0][0] + return classes[result] + + +if __name__ == "__main__": + print(classifier(X_train, y_train, classes, [4.4, 3.1, 1.3, 1.4])) diff --git a/machine_learning/knn_sklearn.py b/machine_learning/knn_sklearn.py index 64582564304f..c36a530736cd 100644 --- a/machine_learning/knn_sklearn.py +++ b/machine_learning/knn_sklearn.py @@ -2,27 +2,30 @@ from sklearn.datasets import load_iris from sklearn.neighbors import KNeighborsClassifier -#Load iris file +# Load iris file iris = load_iris() iris.keys() -print('Target names: \n {} '.format(iris.target_names)) -print('\n Features: \n {}'.format(iris.feature_names)) +print(f"Target names: \n {iris.target_names} ") +print(f"\n Features: \n {iris.feature_names}") -#Train set e Test set -X_train, X_test, y_train, y_test = train_test_split(iris['data'],iris['target'], random_state=4) +# Train set e Test set +X_train, X_test, y_train, y_test = train_test_split( + iris["data"], iris["target"], random_state=4 +) -#KNN +# KNN -knn = KNeighborsClassifier (n_neighbors = 1) +knn = KNeighborsClassifier(n_neighbors=1) knn.fit(X_train, y_train) -#new array to test -X_new = [[1,2,1,4], - [2,3,4,5]] +# new array to test +X_new = [[1, 2, 1, 4], [2, 3, 4, 5]] prediction = knn.predict(X_new) -print('\nNew array: \n {}' - '\n\nTarget Names Prediction: \n {}'.format(X_new, iris['target_names'][prediction])) +print( + "\nNew array: \n {}" + "\n\nTarget Names Prediction: \n {}".format(X_new, iris["target_names"][prediction]) +) diff --git a/machine_learning/linear_discriminant_analysis.py b/machine_learning/linear_discriminant_analysis.py new file mode 100644 index 000000000000..6998db1ce4a0 --- /dev/null +++ b/machine_learning/linear_discriminant_analysis.py @@ -0,0 +1,406 @@ +""" + Linear Discriminant Analysis + + + Assumptions About Data : + 1. The input variables has a gaussian distribution. + 2. The variance calculated for each input variables by class grouping is the + same. + 3. The mix of classes in your training set is representative of the problem. + + + Learning The Model : + The LDA model requires the estimation of statistics from the training data : + 1. Mean of each input value for each class. + 2. Probability of an instance belong to each class. + 3. Covariance for the input data for each class + + Calculate the class means : + mean(x) = 1/n ( for i = 1 to i = n --> sum(xi)) + + Calculate the class probabilities : + P(y = 0) = count(y = 0) / (count(y = 0) + count(y = 1)) + P(y = 1) = count(y = 1) / (count(y = 0) + count(y = 1)) + + Calculate the variance : + We can calculate the variance for dataset in two steps : + 1. Calculate the squared difference for each input variable from the + group mean. + 2. Calculate the mean of the squared difference. + ------------------------------------------------ + Squared_Difference = (x - mean(k)) ** 2 + Variance = (1 / (count(x) - count(classes))) * + (for i = 1 to i = n --> sum(Squared_Difference(xi))) + + Making Predictions : + discriminant(x) = x * (mean / variance) - + ((mean ** 2) / (2 * variance)) + Ln(probability) + --------------------------------------------------------------------------- + After calculating the discriminant value for each class, the class with the + largest discriminant value is taken as the prediction. + + Author: @EverLookNeverSee +""" + +from math import log +from os import name, system +from random import gauss +from random import seed + + +# Make a training dataset drawn from a gaussian distribution +def gaussian_distribution(mean: float, std_dev: float, instance_count: int) -> list: + """ + Generate gaussian distribution instances based-on given mean and standard deviation + :param mean: mean value of class + :param std_dev: value of standard deviation entered by usr or default value of it + :param instance_count: instance number of class + :return: a list containing generated values based-on given mean, std_dev and + instance_count + + >>> gaussian_distribution(5.0, 1.0, 20) # doctest: +NORMALIZE_WHITESPACE + [6.288184753155463, 6.4494456086997705, 5.066335808938262, 4.235456349028368, + 3.9078267848958586, 5.031334516831717, 3.977896829989127, 3.56317055489747, + 5.199311976483754, 5.133374604658605, 5.546468300338232, 4.086029056264687, + 5.005005283626573, 4.935258239627312, 3.494170998739258, 5.537997178661033, + 5.320711100998849, 7.3891120432406865, 5.202969177309964, 4.855297691835079] + """ + seed(1) + return [gauss(mean, std_dev) for _ in range(instance_count)] + + +# Make corresponding Y flags to detecting classes +def y_generator(class_count: int, instance_count: list) -> list: + """ + Generate y values for corresponding classes + :param class_count: Number of classes(data groupings) in dataset + :param instance_count: number of instances in class + :return: corresponding values for data groupings in dataset + + >>> y_generator(1, [10]) + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + >>> y_generator(2, [5, 10]) + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + >>> y_generator(4, [10, 5, 15, 20]) # doctest: +NORMALIZE_WHITESPACE + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] + """ + + return [k for k in range(class_count) for _ in range(instance_count[k])] + + +# Calculate the class means +def calculate_mean(instance_count: int, items: list) -> float: + """ + Calculate given class mean + :param instance_count: Number of instances in class + :param items: items that related to specific class(data grouping) + :return: calculated actual mean of considered class + + >>> items = gaussian_distribution(5.0, 1.0, 20) + >>> calculate_mean(len(items), items) + 5.011267842911003 + """ + # the sum of all items divided by number of instances + return sum(items) / instance_count + + +# Calculate the class probabilities +def calculate_probabilities(instance_count: int, total_count: int) -> float: + """ + Calculate the probability that a given instance will belong to which class + :param instance_count: number of instances in class + :param total_count: the number of all instances + :return: value of probability for considered class + + >>> calculate_probabilities(20, 60) + 0.3333333333333333 + >>> calculate_probabilities(30, 100) + 0.3 + """ + # number of instances in specific class divided by number of all instances + return instance_count / total_count + + +# Calculate the variance +def calculate_variance(items: list, means: list, total_count: int) -> float: + """ + Calculate the variance + :param items: a list containing all items(gaussian distribution of all classes) + :param means: a list containing real mean values of each class + :param total_count: the number of all instances + :return: calculated variance for considered dataset + + >>> items = gaussian_distribution(5.0, 1.0, 20) + >>> means = [5.011267842911003] + >>> total_count = 20 + >>> calculate_variance([items], means, total_count) + 0.9618530973487491 + """ + squared_diff = [] # An empty list to store all squared differences + # iterate over number of elements in items + for i in range(len(items)): + # for loop iterates over number of elements in inner layer of items + for j in range(len(items[i])): + # appending squared differences to 'squared_diff' list + squared_diff.append((items[i][j] - means[i]) ** 2) + + # one divided by (the number of all instances - number of classes) multiplied by + # sum of all squared differences + n_classes = len(means) # Number of classes in dataset + return 1 / (total_count - n_classes) * sum(squared_diff) + + +# Making predictions +def predict_y_values( + x_items: list, means: list, variance: float, probabilities: list +) -> list: + """ This function predicts new indexes(groups for our data) + :param x_items: a list containing all items(gaussian distribution of all classes) + :param means: a list containing real mean values of each class + :param variance: calculated value of variance by calculate_variance function + :param probabilities: a list containing all probabilities of classes + :return: a list containing predicted Y values + + >>> x_items = [[6.288184753155463, 6.4494456086997705, 5.066335808938262, + ... 4.235456349028368, 3.9078267848958586, 5.031334516831717, + ... 3.977896829989127, 3.56317055489747, 5.199311976483754, + ... 5.133374604658605, 5.546468300338232, 4.086029056264687, + ... 5.005005283626573, 4.935258239627312, 3.494170998739258, + ... 5.537997178661033, 5.320711100998849, 7.3891120432406865, + ... 5.202969177309964, 4.855297691835079], [11.288184753155463, + ... 11.44944560869977, 10.066335808938263, 9.235456349028368, + ... 8.907826784895859, 10.031334516831716, 8.977896829989128, + ... 8.56317055489747, 10.199311976483754, 10.133374604658606, + ... 10.546468300338232, 9.086029056264687, 10.005005283626572, + ... 9.935258239627313, 8.494170998739259, 10.537997178661033, + ... 10.320711100998848, 12.389112043240686, 10.202969177309964, + ... 9.85529769183508], [16.288184753155463, 16.449445608699772, + ... 15.066335808938263, 14.235456349028368, 13.907826784895859, + ... 15.031334516831716, 13.977896829989128, 13.56317055489747, + ... 15.199311976483754, 15.133374604658606, 15.546468300338232, + ... 14.086029056264687, 15.005005283626572, 14.935258239627313, + ... 13.494170998739259, 15.537997178661033, 15.320711100998848, + ... 17.389112043240686, 15.202969177309964, 14.85529769183508]] + + >>> means = [5.011267842911003, 10.011267842911003, 15.011267842911002] + >>> variance = 0.9618530973487494 + >>> probabilities = [0.3333333333333333, 0.3333333333333333, 0.3333333333333333] + >>> predict_y_values(x_items, means, variance, probabilities) # doctest: +NORMALIZE_WHITESPACE + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2] + + """ + # An empty list to store generated discriminant values of all items in dataset for + # each class + results = [] + # for loop iterates over number of elements in list + for i in range(len(x_items)): + # for loop iterates over number of inner items of each element + for j in range(len(x_items[i])): + temp = [] # to store all discriminant values of each item as a list + # for loop iterates over number of classes we have in our dataset + for k in range(len(x_items)): + # appending values of discriminants for each class to 'temp' list + temp.append( + x_items[i][j] * (means[k] / variance) + - (means[k] ** 2 / (2 * variance)) + + log(probabilities[k]) + ) + # appending discriminant values of each item to 'results' list + results.append(temp) + + return [l.index(max(l)) for l in results] + + +# Calculating Accuracy +def accuracy(actual_y: list, predicted_y: list) -> float: + """ + Calculate the value of accuracy based-on predictions + :param actual_y:a list containing initial Y values generated by 'y_generator' + function + :param predicted_y: a list containing predicted Y values generated by + 'predict_y_values' function + :return: percentage of accuracy + + >>> actual_y = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + ... 1, 1 ,1 ,1 ,1 ,1 ,1] + >>> predicted_y = [0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, + ... 0, 0, 1, 1, 1, 0, 1, 1, 1] + >>> accuracy(actual_y, predicted_y) + 50.0 + + >>> actual_y = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + ... 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + >>> predicted_y = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, + ... 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + >>> accuracy(actual_y, predicted_y) + 100.0 + """ + # iterate over one element of each list at a time (zip mode) + # prediction is correct if actual Y value equals to predicted Y value + correct = sum(1 for i, j in zip(actual_y, predicted_y) if i == j) + # percentage of accuracy equals to number of correct predictions divided by number + # of all data and multiplied by 100 + return (correct / len(actual_y)) * 100 + + +# Main Function +def main(): + """ This function starts execution phase """ + while True: + print(" Linear Discriminant Analysis ".center(50, "*")) + print("*" * 50, "\n") + print("First of all we should specify the number of classes that") + print("we want to generate as training dataset") + # Trying to get number of classes + n_classes = 0 + while True: + try: + user_input = int( + input("Enter the number of classes (Data Groupings): ").strip() + ) + if user_input > 0: + n_classes = user_input + break + else: + print( + f"Your entered value is {user_input} , Number of classes " + f"should be positive!" + ) + continue + except ValueError: + print("Your entered value is not numerical!") + + print("-" * 100) + + std_dev = 1.0 # Default value for standard deviation of dataset + # Trying to get the value of standard deviation + while True: + try: + user_sd = float( + input( + "Enter the value of standard deviation" + "(Default value is 1.0 for all classes): " + ).strip() + or "1.0" + ) + if user_sd >= 0.0: + std_dev = user_sd + break + else: + print( + f"Your entered value is {user_sd}, Standard deviation should " + f"not be negative!" + ) + continue + except ValueError: + print("Your entered value is not numerical!") + + print("-" * 100) + + # Trying to get number of instances in classes and theirs means to generate + # dataset + counts = [] # An empty list to store instance counts of classes in dataset + for i in range(n_classes): + while True: + try: + user_count = int( + input(f"Enter The number of instances for class_{i+1}: ") + ) + if user_count > 0: + counts.append(user_count) + break + else: + print( + f"Your entered value is {user_count}, Number of " + "instances should be positive!" + ) + continue + except ValueError: + print("Your entered value is not numerical!") + print("-" * 100) + + # An empty list to store values of user-entered means of classes + user_means = [] + for a in range(n_classes): + while True: + try: + user_mean = float( + input(f"Enter the value of mean for class_{a+1}: ") + ) + if isinstance(user_mean, float): + user_means.append(user_mean) + break + print(f"You entered an invalid value: {user_mean}") + except ValueError: + print("Your entered value is not numerical!") + print("-" * 100) + + print("Standard deviation: ", std_dev) + # print out the number of instances in classes in separated line + for i, count in enumerate(counts, 1): + print(f"Number of instances in class_{i} is: {count}") + print("-" * 100) + + # print out mean values of classes separated line + for i, user_mean in enumerate(user_means, 1): + print(f"Mean of class_{i} is: {user_mean}") + print("-" * 100) + + # Generating training dataset drawn from gaussian distribution + x = [ + gaussian_distribution(user_means[j], std_dev, counts[j]) + for j in range(n_classes) + ] + print("Generated Normal Distribution: \n", x) + print("-" * 100) + + # Generating Ys to detecting corresponding classes + y = y_generator(n_classes, counts) + print("Generated Corresponding Ys: \n", y) + print("-" * 100) + + # Calculating the value of actual mean for each class + actual_means = [calculate_mean(counts[k], x[k]) for k in range(n_classes)] + # for loop iterates over number of elements in 'actual_means' list and print + # out them in separated line + for i, actual_mean in enumerate(actual_means, 1): + print(f"Actual(Real) mean of class_{i} is: {actual_mean}") + print("-" * 100) + + # Calculating the value of probabilities for each class + probabilities = [ + calculate_probabilities(counts[i], sum(counts)) for i in range(n_classes) + ] + + # for loop iterates over number of elements in 'probabilities' list and print + # out them in separated line + for i, probability in enumerate(probabilities, 1): + print(f"Probability of class_{i} is: {probability}") + print("-" * 100) + + # Calculating the values of variance for each class + variance = calculate_variance(x, actual_means, sum(counts)) + print("Variance: ", variance) + print("-" * 100) + + # Predicting Y values + # storing predicted Y values in 'pre_indexes' variable + pre_indexes = predict_y_values(x, actual_means, variance, probabilities) + print("-" * 100) + + # Calculating Accuracy of the model + print(f"Accuracy: {accuracy(y, pre_indexes)}") + print("-" * 100) + print(" DONE ".center(100, "+")) + + if input("Press any key to restart or 'q' for quit: ").strip().lower() == "q": + print("\n" + "GoodBye!".center(100, "-") + "\n") + break + system("cls" if name == "nt" else "clear") + + +if __name__ == "__main__": + main() diff --git a/machine_learning/linear_regression.py b/machine_learning/linear_regression.py index 03f16629e451..29bb7c48589e 100644 --- a/machine_learning/linear_regression.py +++ b/machine_learning/linear_regression.py @@ -1,14 +1,12 @@ """ Linear regression is the most basic type of regression commonly used for -predictive analysis. The idea is pretty simple, we have a dataset and we have -a feature's associated with it. The Features should be choose very cautiously -as they determine, how much our model will be able to make future predictions. -We try to set these Feature weights, over many iterations, so that they best -fits our dataset. In this particular code, i had used a CSGO dataset (ADR vs +predictive analysis. The idea is pretty simple: we have a dataset and we have +features associated with it. Features should be chosen very cautiously +as they determine how much our model will be able to make future predictions. +We try to set the weight of these features, over many iterations, so that they best +fit our dataset. In this particular code, I had used a CSGO dataset (ADR vs Rating). We try to best fit a line through dataset and estimate the parameters. """ -from __future__ import print_function - import requests import numpy as np @@ -18,21 +16,22 @@ def collect_dataset(): The dataset contains ADR vs Rating of a Player :return : dataset obtained from the link, as matrix """ - response = requests.get('https://raw.githubusercontent.com/yashLadha/' + - 'The_Math_of_Intelligence/master/Week1/ADRvs' + - 'Rating.csv') + response = requests.get( + "https://raw.githubusercontent.com/yashLadha/" + + "The_Math_of_Intelligence/master/Week1/ADRvs" + + "Rating.csv" + ) lines = response.text.splitlines() data = [] for item in lines: - item = item.split(',') + item = item.split(",") data.append(item) data.pop(0) # This is for removing the labels from the list dataset = np.matrix(data) return dataset -def run_steep_gradient_descent(data_x, data_y, - len_data, alpha, theta): +def run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta): """ Run steep gradient descent and updates the Feature vector accordingly_ :param data_x : contains the dataset :param data_y : contains the output associated with each data-entry @@ -81,10 +80,9 @@ def run_linear_regression(data_x, data_y): theta = np.zeros((1, no_features)) for i in range(0, iterations): - theta = run_steep_gradient_descent(data_x, data_y, - len_data, alpha, theta) + theta = run_steep_gradient_descent(data_x, data_y, len_data, alpha, theta) error = sum_of_square_error(data_x, data_y, len_data, theta) - print('At Iteration %d - Error is %.5f ' % (i + 1, error)) + print("At Iteration %d - Error is %.5f " % (i + 1, error)) return theta @@ -99,10 +97,10 @@ def main(): theta = run_linear_regression(data_x, data_y) len_result = theta.shape[1] - print('Resultant Feature vector : ') + print("Resultant Feature vector : ") for i in range(0, len_result): - print('%.5f' % (theta[0, i])) + print("%.5f" % (theta[0, i])) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/machine_learning/logistic_regression.py b/machine_learning/logistic_regression.py index 9a60831862da..1c1906e8e6b2 100644 --- a/machine_learning/logistic_regression.py +++ b/machine_learning/logistic_regression.py @@ -1,7 +1,6 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- -## Logistic Regression from scratch +# Logistic Regression from scratch # In[62]: @@ -9,8 +8,12 @@ # importing all the required libraries -''' Implementing logistic regression for classification problem - Helpful resources : 1.Coursera ML course 2.https://medium.com/@martinpella/logistic-regression-from-scratch-in-python-124c5636b8ac''' +""" +Implementing logistic regression for classification problem +Helpful resources: +Coursera ML course +https://medium.com/@martinpella/logistic-regression-from-scratch-in-python-124c5636b8ac +""" import numpy as np import matplotlib.pyplot as plt @@ -22,7 +25,9 @@ # In[67]: -# sigmoid function or logistic function is used as a hypothesis function in classification problems +# sigmoid function or logistic function is used as a hypothesis function in +# classification problems + def sigmoid_function(z): return 1 / (1 + np.exp(-z)) @@ -31,77 +36,54 @@ def sigmoid_function(z): def cost_function(h, y): return (-y * np.log(h) - (1 - y) * np.log(1 - h)).mean() + def log_likelihood(X, Y, weights): scores = np.dot(X, weights) - return np.sum(Y*scores - np.log(1 + np.exp(scores)) ) + return np.sum(Y * scores - np.log(1 + np.exp(scores))) + # here alpha is the learning rate, X is the feature matrix,y is the target matrix -def logistic_reg( - alpha, - X, - y, - num_steps, - max_iterations=70000, - ): - converged = False - iterations = 0 +def logistic_reg(alpha, X, y, max_iterations=70000): theta = np.zeros(X.shape[1]) - while not converged: + for iterations in range(max_iterations): z = np.dot(X, theta) h = sigmoid_function(z) gradient = np.dot(X.T, h - y) / y.size - theta = theta - alpha * gradient + theta = theta - alpha * gradient # updating the weights z = np.dot(X, theta) h = sigmoid_function(z) J = cost_function(h, y) - iterations += 1 # update iterations - weights = np.zeros(X.shape[1]) - for step in range(num_steps): - scores = np.dot(X, weights) - predictions = sigmoid_function(scores) - if step % 10000 == 0: - print(log_likelihood(X,y,weights)) # Print log-likelihood every so often - return weights - - if iterations == max_iterations: - print ('Maximum iterations exceeded!') - print ('Minimal cost function J=', J) - converged = True + if iterations % 100 == 0: + print(f"loss: {J} \t") # printing the loss after every 100 iterations return theta + # In[68]: -if __name__ == '__main__': +if __name__ == "__main__": iris = datasets.load_iris() X = iris.data[:, :2] y = (iris.target != 0) * 1 alpha = 0.1 - theta = logistic_reg(alpha,X,y,max_iterations=70000,num_steps=30000) - print (theta) - + theta = logistic_reg(alpha, X, y, max_iterations=70000) + print("theta: ", theta) # printing the theta i.e our weights vector def predict_prob(X): - return sigmoid_function(np.dot(X, theta)) # predicting the value of probability from the logistic regression algorithm - + return sigmoid_function( + np.dot(X, theta) + ) # predicting the value of probability from the logistic regression algorithm plt.figure(figsize=(10, 6)) - plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], color='b', label='0') - plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], color='r', label='1') + plt.scatter(X[y == 0][:, 0], X[y == 0][:, 1], color="b", label="0") + plt.scatter(X[y == 1][:, 0], X[y == 1][:, 1], color="r", label="1") (x1_min, x1_max) = (X[:, 0].min(), X[:, 0].max()) (x2_min, x2_max) = (X[:, 1].min(), X[:, 1].max()) - (xx1, xx2) = np.meshgrid(np.linspace(x1_min, x1_max), - np.linspace(x2_min, x2_max)) + (xx1, xx2) = np.meshgrid(np.linspace(x1_min, x1_max), np.linspace(x2_min, x2_max)) grid = np.c_[xx1.ravel(), xx2.ravel()] probs = predict_prob(grid).reshape(xx1.shape) - plt.contour( - xx1, - xx2, - probs, - [0.5], - linewidths=1, - colors='black', - ) + plt.contour(xx1, xx2, probs, [0.5], linewidths=1, colors="black") plt.legend() + plt.show() diff --git a/machine_learning/multilayer_perceptron_classifier.py b/machine_learning/multilayer_perceptron_classifier.py new file mode 100644 index 000000000000..d78d2a9ed8eb --- /dev/null +++ b/machine_learning/multilayer_perceptron_classifier.py @@ -0,0 +1,30 @@ +from sklearn.neural_network import MLPClassifier + + +X = [[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]] +y = [0, 1, 0, 0] + + +clf = MLPClassifier( + solver="lbfgs", alpha=1e-5, hidden_layer_sizes=(5, 2), random_state=1 +) + +clf.fit(X, y) + + +test = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0]] +Y = clf.predict(test) + + +def wrapper(Y): + """ + >>> wrapper(Y) + [0, 0, 1] + """ + return list(Y) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/machine_learning/perceptron.py b/machine_learning/perceptron.py deleted file mode 100644 index fe1032aff4af..000000000000 --- a/machine_learning/perceptron.py +++ /dev/null @@ -1,124 +0,0 @@ -''' - - Perceptron - w = w + N * (d(k) - y) * x(k) - - Using perceptron network for oil analysis, - with Measuring of 3 parameters that represent chemical characteristics we can classify the oil, in p1 or p2 - p1 = -1 - p2 = 1 - -''' -from __future__ import print_function - -import random - - -class Perceptron: - def __init__(self, sample, exit, learn_rate=0.01, epoch_number=1000, bias=-1): - self.sample = sample - self.exit = exit - self.learn_rate = learn_rate - self.epoch_number = epoch_number - self.bias = bias - self.number_sample = len(sample) - self.col_sample = len(sample[0]) - self.weight = [] - - def trannig(self): - for sample in self.sample: - sample.insert(0, self.bias) - - for i in range(self.col_sample): - self.weight.append(random.random()) - - self.weight.insert(0, self.bias) - - epoch_count = 0 - - while True: - erro = False - for i in range(self.number_sample): - u = 0 - for j in range(self.col_sample + 1): - u = u + self.weight[j] * self.sample[i][j] - y = self.sign(u) - if y != self.exit[i]: - - for j in range(self.col_sample + 1): - - self.weight[j] = self.weight[j] + self.learn_rate * (self.exit[i] - y) * self.sample[i][j] - erro = True - #print('Epoch: \n',epoch_count) - epoch_count = epoch_count + 1 - # if you want controle the epoch or just by erro - if erro == False: - print(('\nEpoch:\n',epoch_count)) - print('------------------------\n') - #if epoch_count > self.epoch_number or not erro: - break - - def sort(self, sample): - sample.insert(0, self.bias) - u = 0 - for i in range(self.col_sample + 1): - u = u + self.weight[i] * sample[i] - - y = self.sign(u) - - if y == -1: - print(('Sample: ', sample)) - print('classification: P1') - else: - print(('Sample: ', sample)) - print('classification: P2') - - def sign(self, u): - return 1 if u >= 0 else -1 - - -samples = [ - [-0.6508, 0.1097, 4.0009], - [-1.4492, 0.8896, 4.4005], - [2.0850, 0.6876, 12.0710], - [0.2626, 1.1476, 7.7985], - [0.6418, 1.0234, 7.0427], - [0.2569, 0.6730, 8.3265], - [1.1155, 0.6043, 7.4446], - [0.0914, 0.3399, 7.0677], - [0.0121, 0.5256, 4.6316], - [-0.0429, 0.4660, 5.4323], - [0.4340, 0.6870, 8.2287], - [0.2735, 1.0287, 7.1934], - [0.4839, 0.4851, 7.4850], - [0.4089, -0.1267, 5.5019], - [1.4391, 0.1614, 8.5843], - [-0.9115, -0.1973, 2.1962], - [0.3654, 1.0475, 7.4858], - [0.2144, 0.7515, 7.1699], - [0.2013, 1.0014, 6.5489], - [0.6483, 0.2183, 5.8991], - [-0.1147, 0.2242, 7.2435], - [-0.7970, 0.8795, 3.8762], - [-1.0625, 0.6366, 2.4707], - [0.5307, 0.1285, 5.6883], - [-1.2200, 0.7777, 1.7252], - [0.3957, 0.1076, 5.6623], - [-0.1013, 0.5989, 7.1812], - [2.4482, 0.9455, 11.2095], - [2.0149, 0.6192, 10.9263], - [0.2012, 0.2611, 5.4631] - -] - -exit = [-1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1] - -network = Perceptron(sample=samples, exit = exit, learn_rate=0.01, epoch_number=1000, bias=-1) - -network.trannig() - -while True: - sample = [] - for i in range(3): - sample.insert(i, float(input('value: '))) - network.sort(sample) diff --git a/machine_learning/polymonial_regression.py b/machine_learning/polymonial_regression.py new file mode 100644 index 000000000000..7b080715b762 --- /dev/null +++ b/machine_learning/polymonial_regression.py @@ -0,0 +1,44 @@ +import matplotlib.pyplot as plt +import pandas as pd + +# Importing the dataset +dataset = pd.read_csv( + "https://s3.us-west-2.amazonaws.com/public.gamelab.fun/dataset/position_salaries.csv" +) +X = dataset.iloc[:, 1:2].values +y = dataset.iloc[:, 2].values + + +# Splitting the dataset into the Training set and Test set +from sklearn.model_selection import train_test_split + +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) + + +# Fitting Polynomial Regression to the dataset +from sklearn.preprocessing import PolynomialFeatures +from sklearn.linear_model import LinearRegression + +poly_reg = PolynomialFeatures(degree=4) +X_poly = poly_reg.fit_transform(X) +pol_reg = LinearRegression() +pol_reg.fit(X_poly, y) + + +# Visualizing the Polymonial Regression results +def viz_polymonial(): + plt.scatter(X, y, color="red") + plt.plot(X, pol_reg.predict(poly_reg.fit_transform(X)), color="blue") + plt.title("Truth or Bluff (Linear Regression)") + plt.xlabel("Position level") + plt.ylabel("Salary") + plt.show() + return + + +if __name__ == "__main__": + viz_polymonial() + + # Predicting a new result with Polymonial Regression + pol_reg.predict(poly_reg.fit_transform([[5.5]])) + # output should be 132148.43750003 diff --git a/machine_learning/random_forest_classification/Social_Network_Ads.csv b/machine_learning/random_forest_classification/Social_Network_Ads.csv deleted file mode 100644 index 4a53849c2baf..000000000000 --- a/machine_learning/random_forest_classification/Social_Network_Ads.csv +++ /dev/null @@ -1,401 +0,0 @@ -User ID,Gender,Age,EstimatedSalary,Purchased -15624510,Male,19,19000,0 -15810944,Male,35,20000,0 -15668575,Female,26,43000,0 -15603246,Female,27,57000,0 -15804002,Male,19,76000,0 -15728773,Male,27,58000,0 -15598044,Female,27,84000,0 -15694829,Female,32,150000,1 -15600575,Male,25,33000,0 -15727311,Female,35,65000,0 -15570769,Female,26,80000,0 -15606274,Female,26,52000,0 -15746139,Male,20,86000,0 -15704987,Male,32,18000,0 -15628972,Male,18,82000,0 -15697686,Male,29,80000,0 -15733883,Male,47,25000,1 -15617482,Male,45,26000,1 -15704583,Male,46,28000,1 -15621083,Female,48,29000,1 -15649487,Male,45,22000,1 -15736760,Female,47,49000,1 -15714658,Male,48,41000,1 -15599081,Female,45,22000,1 -15705113,Male,46,23000,1 -15631159,Male,47,20000,1 -15792818,Male,49,28000,1 -15633531,Female,47,30000,1 -15744529,Male,29,43000,0 -15669656,Male,31,18000,0 -15581198,Male,31,74000,0 -15729054,Female,27,137000,1 -15573452,Female,21,16000,0 -15776733,Female,28,44000,0 -15724858,Male,27,90000,0 -15713144,Male,35,27000,0 -15690188,Female,33,28000,0 -15689425,Male,30,49000,0 -15671766,Female,26,72000,0 -15782806,Female,27,31000,0 -15764419,Female,27,17000,0 -15591915,Female,33,51000,0 -15772798,Male,35,108000,0 -15792008,Male,30,15000,0 -15715541,Female,28,84000,0 -15639277,Male,23,20000,0 -15798850,Male,25,79000,0 -15776348,Female,27,54000,0 -15727696,Male,30,135000,1 -15793813,Female,31,89000,0 -15694395,Female,24,32000,0 -15764195,Female,18,44000,0 -15744919,Female,29,83000,0 -15671655,Female,35,23000,0 -15654901,Female,27,58000,0 -15649136,Female,24,55000,0 -15775562,Female,23,48000,0 -15807481,Male,28,79000,0 -15642885,Male,22,18000,0 -15789109,Female,32,117000,0 -15814004,Male,27,20000,0 -15673619,Male,25,87000,0 -15595135,Female,23,66000,0 -15583681,Male,32,120000,1 -15605000,Female,59,83000,0 -15718071,Male,24,58000,0 -15679760,Male,24,19000,0 -15654574,Female,23,82000,0 -15577178,Female,22,63000,0 -15595324,Female,31,68000,0 -15756932,Male,25,80000,0 -15726358,Female,24,27000,0 -15595228,Female,20,23000,0 -15782530,Female,33,113000,0 -15592877,Male,32,18000,0 -15651983,Male,34,112000,1 -15746737,Male,18,52000,0 -15774179,Female,22,27000,0 -15667265,Female,28,87000,0 -15655123,Female,26,17000,0 -15595917,Male,30,80000,0 -15668385,Male,39,42000,0 -15709476,Male,20,49000,0 -15711218,Male,35,88000,0 -15798659,Female,30,62000,0 -15663939,Female,31,118000,1 -15694946,Male,24,55000,0 -15631912,Female,28,85000,0 -15768816,Male,26,81000,0 -15682268,Male,35,50000,0 -15684801,Male,22,81000,0 -15636428,Female,30,116000,0 -15809823,Male,26,15000,0 -15699284,Female,29,28000,0 -15786993,Female,29,83000,0 -15709441,Female,35,44000,0 -15710257,Female,35,25000,0 -15582492,Male,28,123000,1 -15575694,Male,35,73000,0 -15756820,Female,28,37000,0 -15766289,Male,27,88000,0 -15593014,Male,28,59000,0 -15584545,Female,32,86000,0 -15675949,Female,33,149000,1 -15672091,Female,19,21000,0 -15801658,Male,21,72000,0 -15706185,Female,26,35000,0 -15789863,Male,27,89000,0 -15720943,Male,26,86000,0 -15697997,Female,38,80000,0 -15665416,Female,39,71000,0 -15660200,Female,37,71000,0 -15619653,Male,38,61000,0 -15773447,Male,37,55000,0 -15739160,Male,42,80000,0 -15689237,Male,40,57000,0 -15679297,Male,35,75000,0 -15591433,Male,36,52000,0 -15642725,Male,40,59000,0 -15701962,Male,41,59000,0 -15811613,Female,36,75000,0 -15741049,Male,37,72000,0 -15724423,Female,40,75000,0 -15574305,Male,35,53000,0 -15678168,Female,41,51000,0 -15697020,Female,39,61000,0 -15610801,Male,42,65000,0 -15745232,Male,26,32000,0 -15722758,Male,30,17000,0 -15792102,Female,26,84000,0 -15675185,Male,31,58000,0 -15801247,Male,33,31000,0 -15725660,Male,30,87000,0 -15638963,Female,21,68000,0 -15800061,Female,28,55000,0 -15578006,Male,23,63000,0 -15668504,Female,20,82000,0 -15687491,Male,30,107000,1 -15610403,Female,28,59000,0 -15741094,Male,19,25000,0 -15807909,Male,19,85000,0 -15666141,Female,18,68000,0 -15617134,Male,35,59000,0 -15783029,Male,30,89000,0 -15622833,Female,34,25000,0 -15746422,Female,24,89000,0 -15750839,Female,27,96000,1 -15749130,Female,41,30000,0 -15779862,Male,29,61000,0 -15767871,Male,20,74000,0 -15679651,Female,26,15000,0 -15576219,Male,41,45000,0 -15699247,Male,31,76000,0 -15619087,Female,36,50000,0 -15605327,Male,40,47000,0 -15610140,Female,31,15000,0 -15791174,Male,46,59000,0 -15602373,Male,29,75000,0 -15762605,Male,26,30000,0 -15598840,Female,32,135000,1 -15744279,Male,32,100000,1 -15670619,Male,25,90000,0 -15599533,Female,37,33000,0 -15757837,Male,35,38000,0 -15697574,Female,33,69000,0 -15578738,Female,18,86000,0 -15762228,Female,22,55000,0 -15614827,Female,35,71000,0 -15789815,Male,29,148000,1 -15579781,Female,29,47000,0 -15587013,Male,21,88000,0 -15570932,Male,34,115000,0 -15794661,Female,26,118000,0 -15581654,Female,34,43000,0 -15644296,Female,34,72000,0 -15614420,Female,23,28000,0 -15609653,Female,35,47000,0 -15594577,Male,25,22000,0 -15584114,Male,24,23000,0 -15673367,Female,31,34000,0 -15685576,Male,26,16000,0 -15774727,Female,31,71000,0 -15694288,Female,32,117000,1 -15603319,Male,33,43000,0 -15759066,Female,33,60000,0 -15814816,Male,31,66000,0 -15724402,Female,20,82000,0 -15571059,Female,33,41000,0 -15674206,Male,35,72000,0 -15715160,Male,28,32000,0 -15730448,Male,24,84000,0 -15662067,Female,19,26000,0 -15779581,Male,29,43000,0 -15662901,Male,19,70000,0 -15689751,Male,28,89000,0 -15667742,Male,34,43000,0 -15738448,Female,30,79000,0 -15680243,Female,20,36000,0 -15745083,Male,26,80000,0 -15708228,Male,35,22000,0 -15628523,Male,35,39000,0 -15708196,Male,49,74000,0 -15735549,Female,39,134000,1 -15809347,Female,41,71000,0 -15660866,Female,58,101000,1 -15766609,Female,47,47000,0 -15654230,Female,55,130000,1 -15794566,Female,52,114000,0 -15800890,Female,40,142000,1 -15697424,Female,46,22000,0 -15724536,Female,48,96000,1 -15735878,Male,52,150000,1 -15707596,Female,59,42000,0 -15657163,Male,35,58000,0 -15622478,Male,47,43000,0 -15779529,Female,60,108000,1 -15636023,Male,49,65000,0 -15582066,Male,40,78000,0 -15666675,Female,46,96000,0 -15732987,Male,59,143000,1 -15789432,Female,41,80000,0 -15663161,Male,35,91000,1 -15694879,Male,37,144000,1 -15593715,Male,60,102000,1 -15575002,Female,35,60000,0 -15622171,Male,37,53000,0 -15795224,Female,36,126000,1 -15685346,Male,56,133000,1 -15691808,Female,40,72000,0 -15721007,Female,42,80000,1 -15794253,Female,35,147000,1 -15694453,Male,39,42000,0 -15813113,Male,40,107000,1 -15614187,Male,49,86000,1 -15619407,Female,38,112000,0 -15646227,Male,46,79000,1 -15660541,Male,40,57000,0 -15753874,Female,37,80000,0 -15617877,Female,46,82000,0 -15772073,Female,53,143000,1 -15701537,Male,42,149000,1 -15736228,Male,38,59000,0 -15780572,Female,50,88000,1 -15769596,Female,56,104000,1 -15586996,Female,41,72000,0 -15722061,Female,51,146000,1 -15638003,Female,35,50000,0 -15775590,Female,57,122000,1 -15730688,Male,41,52000,0 -15753102,Female,35,97000,1 -15810075,Female,44,39000,0 -15723373,Male,37,52000,0 -15795298,Female,48,134000,1 -15584320,Female,37,146000,1 -15724161,Female,50,44000,0 -15750056,Female,52,90000,1 -15609637,Female,41,72000,0 -15794493,Male,40,57000,0 -15569641,Female,58,95000,1 -15815236,Female,45,131000,1 -15811177,Female,35,77000,0 -15680587,Male,36,144000,1 -15672821,Female,55,125000,1 -15767681,Female,35,72000,0 -15600379,Male,48,90000,1 -15801336,Female,42,108000,1 -15721592,Male,40,75000,0 -15581282,Male,37,74000,0 -15746203,Female,47,144000,1 -15583137,Male,40,61000,0 -15680752,Female,43,133000,0 -15688172,Female,59,76000,1 -15791373,Male,60,42000,1 -15589449,Male,39,106000,1 -15692819,Female,57,26000,1 -15727467,Male,57,74000,1 -15734312,Male,38,71000,0 -15764604,Male,49,88000,1 -15613014,Female,52,38000,1 -15759684,Female,50,36000,1 -15609669,Female,59,88000,1 -15685536,Male,35,61000,0 -15750447,Male,37,70000,1 -15663249,Female,52,21000,1 -15638646,Male,48,141000,0 -15734161,Female,37,93000,1 -15631070,Female,37,62000,0 -15761950,Female,48,138000,1 -15649668,Male,41,79000,0 -15713912,Female,37,78000,1 -15586757,Male,39,134000,1 -15596522,Male,49,89000,1 -15625395,Male,55,39000,1 -15760570,Male,37,77000,0 -15566689,Female,35,57000,0 -15725794,Female,36,63000,0 -15673539,Male,42,73000,1 -15705298,Female,43,112000,1 -15675791,Male,45,79000,0 -15747043,Male,46,117000,1 -15736397,Female,58,38000,1 -15678201,Male,48,74000,1 -15720745,Female,37,137000,1 -15637593,Male,37,79000,1 -15598070,Female,40,60000,0 -15787550,Male,42,54000,0 -15603942,Female,51,134000,0 -15733973,Female,47,113000,1 -15596761,Male,36,125000,1 -15652400,Female,38,50000,0 -15717893,Female,42,70000,0 -15622585,Male,39,96000,1 -15733964,Female,38,50000,0 -15753861,Female,49,141000,1 -15747097,Female,39,79000,0 -15594762,Female,39,75000,1 -15667417,Female,54,104000,1 -15684861,Male,35,55000,0 -15742204,Male,45,32000,1 -15623502,Male,36,60000,0 -15774872,Female,52,138000,1 -15611191,Female,53,82000,1 -15674331,Male,41,52000,0 -15619465,Female,48,30000,1 -15575247,Female,48,131000,1 -15695679,Female,41,60000,0 -15713463,Male,41,72000,0 -15785170,Female,42,75000,0 -15796351,Male,36,118000,1 -15639576,Female,47,107000,1 -15693264,Male,38,51000,0 -15589715,Female,48,119000,1 -15769902,Male,42,65000,0 -15587177,Male,40,65000,0 -15814553,Male,57,60000,1 -15601550,Female,36,54000,0 -15664907,Male,58,144000,1 -15612465,Male,35,79000,0 -15810800,Female,38,55000,0 -15665760,Male,39,122000,1 -15588080,Female,53,104000,1 -15776844,Male,35,75000,0 -15717560,Female,38,65000,0 -15629739,Female,47,51000,1 -15729908,Male,47,105000,1 -15716781,Female,41,63000,0 -15646936,Male,53,72000,1 -15768151,Female,54,108000,1 -15579212,Male,39,77000,0 -15721835,Male,38,61000,0 -15800515,Female,38,113000,1 -15591279,Male,37,75000,0 -15587419,Female,42,90000,1 -15750335,Female,37,57000,0 -15699619,Male,36,99000,1 -15606472,Male,60,34000,1 -15778368,Male,54,70000,1 -15671387,Female,41,72000,0 -15573926,Male,40,71000,1 -15709183,Male,42,54000,0 -15577514,Male,43,129000,1 -15778830,Female,53,34000,1 -15768072,Female,47,50000,1 -15768293,Female,42,79000,0 -15654456,Male,42,104000,1 -15807525,Female,59,29000,1 -15574372,Female,58,47000,1 -15671249,Male,46,88000,1 -15779744,Male,38,71000,0 -15624755,Female,54,26000,1 -15611430,Female,60,46000,1 -15774744,Male,60,83000,1 -15629885,Female,39,73000,0 -15708791,Male,59,130000,1 -15793890,Female,37,80000,0 -15646091,Female,46,32000,1 -15596984,Female,46,74000,0 -15800215,Female,42,53000,0 -15577806,Male,41,87000,1 -15749381,Female,58,23000,1 -15683758,Male,42,64000,0 -15670615,Male,48,33000,1 -15715622,Female,44,139000,1 -15707634,Male,49,28000,1 -15806901,Female,57,33000,1 -15775335,Male,56,60000,1 -15724150,Female,49,39000,1 -15627220,Male,39,71000,0 -15672330,Male,47,34000,1 -15668521,Female,48,35000,1 -15807837,Male,48,33000,1 -15592570,Male,47,23000,1 -15748589,Female,45,45000,1 -15635893,Male,60,42000,1 -15757632,Female,39,59000,0 -15691863,Female,46,41000,1 -15706071,Male,51,23000,1 -15654296,Female,50,20000,1 -15755018,Male,36,33000,0 -15594041,Female,49,36000,1 \ No newline at end of file diff --git a/machine_learning/random_forest_classification/random_forest_classification.py b/machine_learning/random_forest_classification/random_forest_classification.py deleted file mode 100644 index d5dde4b13822..000000000000 --- a/machine_learning/random_forest_classification/random_forest_classification.py +++ /dev/null @@ -1,69 +0,0 @@ -# Random Forest Classification - -# Importing the libraries -import numpy as np -import matplotlib.pyplot as plt -import pandas as pd - -# Importing the dataset -dataset = pd.read_csv('Social_Network_Ads.csv') -X = dataset.iloc[:, [2, 3]].values -y = dataset.iloc[:, 4].values - -# Splitting the dataset into the Training set and Test set -from sklearn.cross_validation import train_test_split -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0) - -# Feature Scaling -from sklearn.preprocessing import StandardScaler -sc = StandardScaler() -X_train = sc.fit_transform(X_train) -X_test = sc.transform(X_test) - -# Fitting Random Forest Classification to the Training set -from sklearn.ensemble import RandomForestClassifier -classifier = RandomForestClassifier(n_estimators = 10, criterion = 'entropy', random_state = 0) -classifier.fit(X_train, y_train) - -# Predicting the Test set results -y_pred = classifier.predict(X_test) - -# Making the Confusion Matrix -from sklearn.metrics import confusion_matrix -cm = confusion_matrix(y_test, y_pred) - -# Visualising the Training set results -from matplotlib.colors import ListedColormap -X_set, y_set = X_train, y_train -X1, X2 = np.meshgrid(np.arange(start = X_set[:, 0].min() - 1, stop = X_set[:, 0].max() + 1, step = 0.01), - np.arange(start = X_set[:, 1].min() - 1, stop = X_set[:, 1].max() + 1, step = 0.01)) -plt.contourf(X1, X2, classifier.predict(np.array([X1.ravel(), X2.ravel()]).T).reshape(X1.shape), - alpha = 0.75, cmap = ListedColormap(('red', 'green'))) -plt.xlim(X1.min(), X1.max()) -plt.ylim(X2.min(), X2.max()) -for i, j in enumerate(np.unique(y_set)): - plt.scatter(X_set[y_set == j, 0], X_set[y_set == j, 1], - c = ListedColormap(('red', 'green'))(i), label = j) -plt.title('Random Forest Classification (Training set)') -plt.xlabel('Age') -plt.ylabel('Estimated Salary') -plt.legend() -plt.show() - -# Visualising the Test set results -from matplotlib.colors import ListedColormap -X_set, y_set = X_test, y_test -X1, X2 = np.meshgrid(np.arange(start = X_set[:, 0].min() - 1, stop = X_set[:, 0].max() + 1, step = 0.01), - np.arange(start = X_set[:, 1].min() - 1, stop = X_set[:, 1].max() + 1, step = 0.01)) -plt.contourf(X1, X2, classifier.predict(np.array([X1.ravel(), X2.ravel()]).T).reshape(X1.shape), - alpha = 0.75, cmap = ListedColormap(('red', 'green'))) -plt.xlim(X1.min(), X1.max()) -plt.ylim(X2.min(), X2.max()) -for i, j in enumerate(np.unique(y_set)): - plt.scatter(X_set[y_set == j, 0], X_set[y_set == j, 1], - c = ListedColormap(('red', 'green'))(i), label = j) -plt.title('Random Forest Classification (Test set)') -plt.xlabel('Age') -plt.ylabel('Estimated Salary') -plt.legend() -plt.show() \ No newline at end of file diff --git a/machine_learning/random_forest_classification/random_forest_classifier.ipynb b/machine_learning/random_forest_classification/random_forest_classifier.ipynb deleted file mode 100644 index 7ee66124c371..000000000000 --- a/machine_learning/random_forest_classification/random_forest_classifier.ipynb +++ /dev/null @@ -1,196 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\Satyam\\AppData\\Roaming\\Python\\Python35\\site-packages\\sklearn\\ensemble\\weight_boosting.py:29: DeprecationWarning: numpy.core.umath_tests is an internal NumPy module and should not be imported. It will be removed in a future NumPy release.\n", - " from numpy.core.umath_tests import inner1d\n" - ] - } - ], - "source": [ - "# Importing the libraries\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from sklearn.model_selection import train_test_split\n", - "from sklearn.preprocessing import StandardScaler\n", - "from sklearn.metrics import confusion_matrix\n", - "from matplotlib.colors import ListedColormap\n", - "from sklearn.ensemble import RandomForestClassifier" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Importing the dataset\n", - "dataset = pd.read_csv('Social_Network_Ads.csv')\n", - "X = dataset.iloc[:, [2, 3]].values\n", - "y = dataset.iloc[:, 4].values" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Splitting the dataset into the Training set and Test set\n", - "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\Satyam\\AppData\\Roaming\\Python\\Python35\\site-packages\\sklearn\\utils\\validation.py:475: DataConversionWarning: Data with input dtype int64 was converted to float64 by StandardScaler.\n", - " warnings.warn(msg, DataConversionWarning)\n" - ] - } - ], - "source": [ - "# Feature Scaling\n", - "sc = StandardScaler()\n", - "X_train = sc.fit_transform(X_train)\n", - "X_test = sc.transform(X_test)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[63 5]\n", - " [ 3 29]]\n" - ] - } - ], - "source": [ - "# Fitting classifier to the Training set\n", - "# Create your classifier here\n", - "classifier = RandomForestClassifier(n_estimators=10,criterion='entropy',random_state=0)\n", - "classifier.fit(X_train,y_train)\n", - "# Predicting the Test set results\n", - "y_pred = classifier.predict(X_test)\n", - "\n", - "# Making the Confusion Matrix\n", - "cm = confusion_matrix(y_test, y_pred)\n", - "print(cm)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\nAfyzKuSV3NT5AAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\nAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Visualising the Training set results\n", - "X_set, y_set = X_train, y_train\n", - "X1, X2 = np.meshgrid(np.arange(start = X_set[:, 0].min() - 1, stop = X_set[:, 0].max() + 1, step = 0.01),\n", - " np.arange(start = X_set[:, 1].min() - 1, stop = X_set[:, 1].max() + 1, step = 0.01))\n", - "plt.contourf(X1, X2, classifier.predict(np.array([X1.ravel(), X2.ravel()]).T).reshape(X1.shape),\n", - " alpha = 0.75, cmap = ListedColormap(('red', 'green')))\n", - "plt.xlim(X1.min(), X1.max())\n", - "plt.ylim(X2.min(), X2.max())\n", - "for i, j in enumerate(np.unique(y_set)):\n", - " plt.scatter(X_set[y_set == j, 0], X_set[y_set == j, 1],\n", - " c = ListedColormap(('red', 'green'))(i), label = j)\n", - "plt.title('Random Forest Classifier (Training set)')\n", - "plt.xlabel('Age')\n", - "plt.ylabel('Estimated Salary')\n", - "plt.legend()\n", - "plt.show()\n", - "\n", - "# Visualising the Test set results\n", - "X_set, y_set = X_test, y_test\n", - "X1, X2 = np.meshgrid(np.arange(start = X_set[:, 0].min() - 1, stop = X_set[:, 0].max() + 1, step = 0.01),\n", - " np.arange(start = X_set[:, 1].min() - 1, stop = X_set[:, 1].max() + 1, step = 0.01))\n", - "plt.contourf(X1, X2, classifier.predict(np.array([X1.ravel(), X2.ravel()]).T).reshape(X1.shape),\n", - " alpha = 0.75, cmap = ListedColormap(('red', 'green')))\n", - "plt.xlim(X1.min(), X1.max())\n", - "plt.ylim(X2.min(), X2.max())\n", - "for i, j in enumerate(np.unique(y_set)):\n", - " plt.scatter(X_set[y_set == j, 0], X_set[y_set == j, 1],\n", - " c = ListedColormap(('red', 'green'))(i), label = j)\n", - "plt.title('Random Forest Classifier (Test set)')\n", - "plt.xlabel('Age')\n", - "plt.ylabel('Estimated Salary')\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/machine_learning/random_forest_classifier.py b/machine_learning/random_forest_classifier.py new file mode 100644 index 000000000000..e7acd91346a1 --- /dev/null +++ b/machine_learning/random_forest_classifier.py @@ -0,0 +1,45 @@ +# Random Forest Classifier Example + +from sklearn.datasets import load_iris +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestClassifier +from sklearn.metrics import plot_confusion_matrix +import matplotlib.pyplot as plt + + +def main(): + + """ + Random Forest Classifier Example using sklearn function. + Iris type dataset is used to demonstrate algorithm. + """ + + # Load Iris dataset + iris = load_iris() + + # Split dataset into train and test data + X = iris["data"] # features + Y = iris["target"] + x_train, x_test, y_train, y_test = train_test_split( + X, Y, test_size=0.3, random_state=1 + ) + + # Random Forest Classifier + rand_for = RandomForestClassifier(random_state=42, n_estimators=100) + rand_for.fit(x_train, y_train) + + # Display Confusion Matrix of Classifier + plot_confusion_matrix( + rand_for, + x_test, + y_test, + display_labels=iris["target_names"], + cmap="Blues", + normalize="true", + ) + plt.title("Normalized Confusion Matrix - IRIS Dataset") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/machine_learning/random_forest_regression/Position_Salaries.csv b/machine_learning/random_forest_regression/Position_Salaries.csv deleted file mode 100644 index 0c752c72a1d1..000000000000 --- a/machine_learning/random_forest_regression/Position_Salaries.csv +++ /dev/null @@ -1,11 +0,0 @@ -Position,Level,Salary -Business Analyst,1,45000 -Junior Consultant,2,50000 -Senior Consultant,3,60000 -Manager,4,80000 -Country Manager,5,110000 -Region Manager,6,150000 -Partner,7,200000 -Senior Partner,8,300000 -C-level,9,500000 -CEO,10,1000000 \ No newline at end of file diff --git a/machine_learning/random_forest_regression/random_forest_regression.ipynb b/machine_learning/random_forest_regression/random_forest_regression.ipynb deleted file mode 100644 index 17f4d42bfb0d..000000000000 --- a/machine_learning/random_forest_regression/random_forest_regression.ipynb +++ /dev/null @@ -1,147 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Importing the libraries\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from sklearn.ensemble import RandomForestRegressor" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Importing the dataset\n", - "dataset = pd.read_csv('Position_Salaries.csv')\n", - "X = dataset.iloc[:, 1:2].values\n", - "y = dataset.iloc[:, 2].values" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,\n", - " max_features='auto', max_leaf_nodes=None,\n", - " min_impurity_split=1e-07, min_samples_leaf=1,\n", - " min_samples_split=2, min_weight_fraction_leaf=0.0,\n", - " n_estimators=300, n_jobs=1, oob_score=False, random_state=0,\n", - " verbose=0, warm_start=False)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Fitting Random Forest Regression to the dataset\n", - "regressor = RandomForestRegressor(n_estimators = 300, random_state = 0)\n", - "regressor.fit(X, y)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Predicting a new result\n", - "y_pred = regressor.predict(6.5)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ 160333.33333333]\n" - ] - } - ], - "source": [ - "print(y_pred)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\nAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Visualising the Random Forest Regression results (higher resolution)\n", - "X_grid = np.arange(min(X), max(X), 0.01)\n", - "X_grid = X_grid.reshape((len(X_grid), 1))\n", - "plt.scatter(X, y, color = 'red')\n", - "plt.plot(X_grid, regressor.predict(X_grid), color = 'blue')\n", - "plt.title('Truth or Bluff (Random Forest Regression)')\n", - "plt.xlabel('Position level')\n", - "plt.ylabel('Salary')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/machine_learning/random_forest_regression/random_forest_regression.py b/machine_learning/random_forest_regression/random_forest_regression.py deleted file mode 100644 index fce58b1fe283..000000000000 --- a/machine_learning/random_forest_regression/random_forest_regression.py +++ /dev/null @@ -1,41 +0,0 @@ -# Random Forest Regression - -# Importing the libraries -import numpy as np -import matplotlib.pyplot as plt -import pandas as pd - -# Importing the dataset -dataset = pd.read_csv('Position_Salaries.csv') -X = dataset.iloc[:, 1:2].values -y = dataset.iloc[:, 2].values - -# Splitting the dataset into the Training set and Test set -"""from sklearn.cross_validation import train_test_split -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)""" - -# Feature Scaling -"""from sklearn.preprocessing import StandardScaler -sc_X = StandardScaler() -X_train = sc_X.fit_transform(X_train) -X_test = sc_X.transform(X_test) -sc_y = StandardScaler() -y_train = sc_y.fit_transform(y_train)""" - -# Fitting Random Forest Regression to the dataset -from sklearn.ensemble import RandomForestRegressor -regressor = RandomForestRegressor(n_estimators = 10, random_state = 0) -regressor.fit(X, y) - -# Predicting a new result -y_pred = regressor.predict(6.5) - -# Visualising the Random Forest Regression results (higher resolution) -X_grid = np.arange(min(X), max(X), 0.01) -X_grid = X_grid.reshape((len(X_grid), 1)) -plt.scatter(X, y, color = 'red') -plt.plot(X_grid, regressor.predict(X_grid), color = 'blue') -plt.title('Truth or Bluff (Random Forest Regression)') -plt.xlabel('Position level') -plt.ylabel('Salary') -plt.show() \ No newline at end of file diff --git a/machine_learning/random_forest_regressor.py b/machine_learning/random_forest_regressor.py new file mode 100644 index 000000000000..f78b6bbd0f42 --- /dev/null +++ b/machine_learning/random_forest_regressor.py @@ -0,0 +1,42 @@ +# Random Forest Regressor Example + +from sklearn.datasets import load_boston +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestRegressor +from sklearn.metrics import mean_absolute_error +from sklearn.metrics import mean_squared_error + + +def main(): + + """ + Random Forest Regressor Example using sklearn function. + Boston house price dataset is used to demonstrate the algorithm. + """ + + # Load Boston house price dataset + boston = load_boston() + print(boston.keys()) + + # Split dataset into train and test data + X = boston["data"] # features + Y = boston["target"] + x_train, x_test, y_train, y_test = train_test_split( + X, Y, test_size=0.3, random_state=1 + ) + + # Random Forest Regressor + rand_for = RandomForestRegressor(random_state=42, n_estimators=300) + rand_for.fit(x_train, y_train) + + # Predict target for test data + predictions = rand_for.predict(x_test) + predictions = predictions.reshape(len(predictions), 1) + + # Error printing + print(f"Mean Absolute Error:\t {mean_absolute_error(y_test, predictions)}") + print(f"Mean Square Error :\t {mean_squared_error(y_test, predictions)}") + + +if __name__ == "__main__": + main() diff --git a/machine_learning/reuters_one_vs_rest_classifier.ipynb b/machine_learning/reuters_one_vs_rest_classifier.ipynb deleted file mode 100644 index 968130a6053a..000000000000 --- a/machine_learning/reuters_one_vs_rest_classifier.ipynb +++ /dev/null @@ -1,405 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "try:\n", - " import nltk\n", - "except ModuleNotFoundError:\n", - " !pip install nltk" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "## This code downloads the required packages.\n", - "## You can run `nltk.download('all')` to download everything.\n", - "\n", - "nltk_packages = [\n", - " (\"reuters\", \"corpora/reuters.zip\")\n", - "]\n", - "\n", - "for pid, fid in nltk_packages:\n", - " try:\n", - " nltk.data.find(fid)\n", - " except LookupError:\n", - " nltk.download(pid)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up corpus" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from nltk.corpus import reuters" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up train/test data" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "train_documents, train_categories = zip(*[(reuters.raw(i), reuters.categories(i)) for i in reuters.fileids() if i.startswith('training/')])\n", - "test_documents, test_categories = zip(*[(reuters.raw(i), reuters.categories(i)) for i in reuters.fileids() if i.startswith('test/')])" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "all_categories = sorted(list(set(reuters.categories())))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following cell defines a function **tokenize** that performs following actions:\n", - "- Receive a document as an argument to the function\n", - "- Tokenize the document using `nltk.word_tokenize()`\n", - "- Use `PorterStemmer` provided by the `nltk` to remove morphological affixes from each token\n", - "- Append stemmed token to an already defined list `stems`\n", - "- Return the list `stems`" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from nltk.stem.porter import PorterStemmer\n", - "def tokenize(text):\n", - " tokens = nltk.word_tokenize(text)\n", - " stems = []\n", - " for item in tokens:\n", - " stems.append(PorterStemmer().stem(item))\n", - " return stems" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To begin, I first used TF-IDF for feature selection on both train as well as test data using `TfidfVectorizer`.\n", - "\n", - "But first, What `TfidfVectorizer` actually does?\n", - "- `TfidfVectorizer` converts a collection of raw documents to a matrix of **TF-IDF** features.\n", - "\n", - "**TF-IDF**?\n", - "- TFIDF (abbreviation of the term *frequency–inverse document frequency*) is a numerical statistic that is intended to reflect how important a word is to a document in a collection or corpus. [tf–idf](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)\n", - "\n", - "**Why `TfidfVectorizer`**?\n", - "- `TfidfVectorizer` scale down the impact of tokens that occur very frequently (e.g., “a”, “the”, and “of”) in a given corpus. [Feature Extraction and Transformation](https://spark.apache.org/docs/latest/mllib-feature-extraction.html#tf-idf)\n", - "\n", - "I gave following two arguments to `TfidfVectorizer`:\n", - "- tokenizer: `tokenize` function\n", - "- stop_words\n", - "\n", - "Then I used `fit_transform` and `transform` on the train and test documents repectively.\n", - "\n", - "**Why `fit_transform` for training data while `transform` for test data**?\n", - "\n", - "To avoid data leakage during cross-validation, imputer computes the statistic on the train data during the `fit`, **stores it** and uses the same on the test data, during the `transform`. This also prevents the test data from appearing in `fit` operation." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.feature_extraction.text import TfidfVectorizer\n", - "\n", - "vectorizer = TfidfVectorizer(tokenizer = tokenize, stop_words = 'english')\n", - "\n", - "vectorised_train_documents = vectorizer.fit_transform(train_documents)\n", - "vectorised_test_documents = vectorizer.transform(test_documents)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the **efficient implementation** of machine learning algorithms, many machine learning algorithms **requires all input variables and output variables to be numeric**. This means that categorical data must be converted to a numerical form.\n", - "\n", - "For this purpose, I used `MultiLabelBinarizer` from `sklearn.preprocessing`." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.preprocessing import MultiLabelBinarizer\n", - "\n", - "mlb = MultiLabelBinarizer()\n", - "train_labels = mlb.fit_transform(train_categories)\n", - "test_labels = mlb.transform(test_categories)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, To **train** the classifier, I used `LinearSVC` in combination with the `OneVsRestClassifier` function in the scikit-learn package.\n", - "\n", - "The strategy of `OneVsRestClassifier` is of **fitting one classifier per label** and the `OneVsRestClassifier` can efficiently do this task and also outputs are easy to interpret. Since each label is represented by **one and only one classifier**, it is possible to gain knowledge about the label by inspecting its corresponding classifier. [OneVsRestClassifier](http://scikit-learn.org/stable/modules/multiclass.html#one-vs-the-rest)\n", - "\n", - "The reason I combined `LinearSVC` with `OneVsRestClassifier` is because `LinearSVC` supports **Multi-class**, while we want to perform **Multi-label** classification." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "from sklearn.multiclass import OneVsRestClassifier\n", - "from sklearn.svm import LinearSVC\n", - "\n", - "classifier = OneVsRestClassifier(LinearSVC())\n", - "classifier.fit(vectorised_train_documents, train_labels)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After fitting the classifier, I decided to use `cross_val_score` to **measure score** of the classifier by **cross validation** on the training data. But the only problem was, I wanted to **shuffle** data to use with `cross_val_score`, but it does not support shuffle argument.\n", - "\n", - "So, I decided to use `KFold` with `cross_val_score` as `KFold` supports shuffling the data.\n", - "\n", - "I also enabled `random_state`, because `random_state` will guarantee the same output in each run. By setting the `random_state`, it is guaranteed that the pseudorandom number generator will generate the same sequence of random integers each time, which in turn will affect the split.\n", - "\n", - "Why **42**?\n", - "- [Why '42' is the preferred number when indicating something random?](https://softwareengineering.stackexchange.com/questions/507/why-42-is-the-preferred-number-when-indicating-something-random)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "from sklearn.model_selection import KFold, cross_val_score\n", - "\n", - "kf = KFold(n_splits=10, random_state = 42, shuffle = True)\n", - "scores = cross_val_score(classifier, vectorised_train_documents, train_labels, cv = kf)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cross-validation scores: [0.83655084 0.86743887 0.8043758 0.83011583 0.83655084 0.81724582\n", - " 0.82754183 0.8030888 0.80694981 0.82731959]\n", - "Cross-validation accuracy: 0.8257 (+/- 0.0368)\n" - ] - } - ], - "source": [ - "print('Cross-validation scores:', scores)\n", - "print('Cross-validation accuracy: {:.4f} (+/- {:.4f})'.format(scores.mean(), scores.std() * 2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the end, I used different methods (`accuracy_score`, `precision_score`, `recall_score`, `f1_score` and `confusion_matrix`) provided by scikit-learn **to evaluate** the classifier. (both *Macro-* and *Micro-averages*)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix\n", - "\n", - "predictions = classifier.predict(vectorised_test_documents)\n", - "\n", - "accuracy = accuracy_score(test_labels, predictions)\n", - "\n", - "macro_precision = precision_score(test_labels, predictions, average='macro')\n", - "macro_recall = recall_score(test_labels, predictions, average='macro')\n", - "macro_f1 = f1_score(test_labels, predictions, average='macro')\n", - "\n", - "micro_precision = precision_score(test_labels, predictions, average='micro')\n", - "micro_recall = recall_score(test_labels, predictions, average='micro')\n", - "micro_f1 = f1_score(test_labels, predictions, average='micro')\n", - "\n", - "cm = confusion_matrix(test_labels.argmax(axis = 1), predictions.argmax(axis = 1))" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Accuracy: 0.8099\n", - "Precision:\n", - "- Macro: 0.6076\n", - "- Micro: 0.9471\n", - "Recall:\n", - "- Macro: 0.3708\n", - "- Micro: 0.7981\n", - "F1-measure:\n", - "- Macro: 0.4410\n", - "- Micro: 0.8662\n" - ] - } - ], - "source": [ - "print(\"Accuracy: {:.4f}\\nPrecision:\\n- Macro: {:.4f}\\n- Micro: {:.4f}\\nRecall:\\n- Macro: {:.4f}\\n- Micro: {:.4f}\\nF1-measure:\\n- Macro: {:.4f}\\n- Micro: {:.4f}\".format(accuracy, macro_precision, micro_precision, macro_recall, micro_recall, macro_f1, micro_f1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In below cell, I used `matplotlib.pyplot` to **plot the confusion matrix** (of first *few results only* to keep the readings readable) using `heatmap` of `seaborn`." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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4E5t2fKqXnnl2abbZs26TyUlz6ik5Bm9/vq/VFLyTFxfa+SI4Vq3dpnWVxRr4rWXJJTDxdkgd5PbndzJzBYVFei5JY9rzeqleq38BV12aatvKdvuuxOdPbRnR/X/XBf1Lex8wPm8PKPCPp31+f5d1f3gvISyRNP70I76XFEXFfbprNxco5Ejhqti7TK99cZL+s2tV6qooLO6donv4l4+Pj6D3E1nP3L72DnI3XT2I7ePnYPcTWc/cvvYOcjdPnYGXGGsTe3TWjt1+lzTgqFDj1ZNzUd68MG7dNRR32zxc++44wb95z+7dNtt90hq/erboVBIleuWa9Tp5ykcrtaKlxdp7LgJqqx8M2YeF2fJTe50dM7P76U++b20qnytunXrqpUli3XOuT/MyNydOoUUiew9SVhV/UmL+e+P/bE2bdysrl1z1dBgtX39C3F9DySpqnqHrv/NFM344+8k/ffq2wcflKsOHXL07nufqFvXXB3ULU9Llz6n0844T1u37t17wYWXqHZXWJu3fJSy71ey8/y6onO25vaxs6u5fezsam4fO7ua28fOrub2sbOruV3oXL+nysQVxjO1t3yf12830/VXf83Yx0lanylZXLxSO3d+0Or95557pubMmRfz6xw9ZJA2bNisTZu2qK6uTnPnztPZZ42MK4OLs+Qmd6pnJWn79re1qnytJKmmplbr17+pwoL8jMz98ccRRRrfJ+RA86PPPl2f7IkO+TCkAAAgAElEQVToQP/mMv8fL+h7P7pC51x4iSb97l41NBz4Hz/217VLrnbV1EmSamrrdeyxR2rDhs3auLF57hGK508/Hx+frub2sbOruX3s7GpuHzu7mtvHzq7m9rGzq7l97Oxqblc7Ay7JmPeUHDr0aO3Y8a42bNgc8+cWFOZra3hb0+fhqmoVxHnyxMXZIHeTO727g+zcXL9+RRr49f9RycpVKd+dzmO1YfMWLX7+n5p1/xQ9OfM+hUIhLVjyYlx7cnON6uv/e8qxV6/eClft3duxY0i7P3pPh3+xr95995N2zdze864cq2yYDXK3j7l97Bzkbjr7kdvHzkHuprMfuX3sHORuHztDUiTCR/OPDJYxV98eM2a05s6N/SxJSTKm5TNP430ZuouzQe4md3p3B9l5n65du2junGm6+pqbtGtXTcp3p/NYlZSVq2L9W/rexVdIkj755BP1+MwhkqTLf3GLqrbtUF19nap3vKNzLtz7vi0HdcvVrpr6qBk++SSi93buUU1NnQ45pIM+2r37gM/SbEvm9p535Vhlw2yQu33M7WPnIHfTObHZIHfTObHZIHfTObHZIHfTObHZIHf72BlwSdSTksaYbpImSjpHUpGkPZI2SLrfWjsjytx4SeMlKTf3M8rJ6RY1RE5OjkaPHqXjjz8jrtBV4Wr1LSpo+ryosI+qq3dk7WyQu8md3t1Bdpak3NxcPT5nmmbPfkpPPx3fRaeS3Z3OY2Wt1dmnnaqrfvqDFvfde/uv9n69Vt5Tsr7eKjfXqKFh718G3n57h4oKP703XLVdNmLVIS+kT/a0/i9SPj4+g9xNZz9y+9g5yN109iO3j52D3E1nP3L72DnI3T52BlwS6+Xbf5W0UdJISZMk3StpnKSTjDG3tTZkrZ1qrR1srR0c64SkJJ188lC98cYGVVVtjyt0aVm5Bgw4TP3791VeXp7GjBmt+QuWZO0sucmd6tl9pk2dosr1b+nue6YmNOfKsTp28EA9u7RY7zW+t+2H/9mlbdvj+8O99qMGHdRt75W8u3XNVUnJKg0YcJgGfOFzTXsXPfOs8vJCqquP/hR5Hx+frub2sbOruX3s7GpuHzu7mtvHzq7m9rGzq7l97Oxqblc7Ay6J9fLt/s2eEfl7Y0yptfZWY8wPJFVI+mUiyx5++A864YTj1LPnZ/TWWyX69a9/rxkz5mjMmLM1Z87f4/46DQ0NuuLKG7Ro4aPKCYU0Y+YcVVS8kbWz5CZ3qmcl6RvHD9G4sedq9ZoKlZXu/QPvxhvv0DOLY1+5Ot25ex3aUZ075Sgnx6iooKMuuujHuuyS78sYoyeemK/XX39T/fp2UShkZK10yrfGat5fH9AXDuuny/7vAo2/8npFbER5ubm6/uoJKsjvHTPnrpo69Tq0kz5X1EUNEasdb+/WFVfeoAXz/6q8vFzNnfu4dr63Re9/sCfm23b4+Ph0NbePnV3N7WNnV3P72NnV3D52djW3j51dze1jZ1dzu9oZcImJ9r4ExpiXJE201hYbY86SdKm1dmTjfa9ba78Ua0GnTp9r8xsf1EfiuyouAESze9vyNs/ue/k2AAAAAGSS+j1VLd98Eqq9+TzegLOZrjfPztjHSaxnSv5E0l+MMYdLWivph5JkjDlU0n0pzgYAAAAAAADEL8I5SVdEPSlprV0t6egD3P6OMWZXylIBAAAAAAAAyFqxLnQTzaR2SwEAAAAAAADAG1GfKWmMWd3aXZJiXxkCAAAAAAAAAPYT6z0le0saKWnnfrcbSS+lJBEAAAAAAACArBbrpOQCSd2steX732GMWRrPggauoA0gYFxBGwAAAAA8YSNBJ0CcYl3o5uIo953f/nEAAAAAAAAAZLtkLnQDAAAAAAAAAAnjpCQAAAAAAACAtOKkJAAAAAAAAIC0CuykZPfuB+uxx6ZqzZp/avXqpTr2mKMSmh85YrjWrV2m9RXFmnjtJVk/G+RucruTO5nZaVOnaFv4NZWvej6hufbY7eKxKioq0HNLHtea1Uv1WvkLuuzSVt+Ct90zJzvv27EKcjbI3T7m9rFzkLvp7EduHzsHuZvOfuT2sXOQu33s7L2I5aP5RwYz1qY2YF6HwgMueOjBu1VcXKKHps9WXl6eunTprA8//M+nfk5ryUKhkCrXLdeo089TOFytFS8v0thxE1RZ+WbMPC7Okpvc6eh8wtBjVFNTq+nT79HAQafENZMJuYPanZ/fS33ye2lV+Vp169ZVK0sW65xzf5jVnX3M7WNnV3P72NnV3D52djW3j51dze1jZ1dz+9jZ1dwudK7fU2XiCuOZ2uu/k9ln4tKs628ez9jHSSDPlDzooG4aOvQYPTR9tiSprq6uxQnJaI4eMkgbNmzWpk1bVFdXp7lz5+nss0Zm7Sy5yZ3qWUlaXlyi93d+EPfPz5TcQe3evv1trSpfK0mqqanV+vVvqrAgP+V7k5338VjR2Y/cPnZ2NbePnV3N7WNnV3P72NnV3D52djW3q50Bl0Q9KWmM6W6MucMYs94Y817jR2XjbYe0dennP99P7777nh78y10qXfkPPXD/nerSpXPc8wWF+doa3tb0ebiqWgVxngxwcTbI3eRO7+4gOyfDx2PVXL9+RRr49f9RycpVadnr6mPMxdw+dg5yN539yO1j5yB309mP3D52DnI3nf3I7WpnwCWxnik5V9JOScOttZ+11n5W0kmNtz3e2pAxZrwxpswYUxaJ1La4PzcnR4MGHaEHHnhYQ44eqdrajzRx4qVxhzam5TNP430ZuouzQe4md3p3B9k5GT4eq326du2iuXOm6eprbtKuXTVp2evqY8zF3D52DnI3nRObDXI3nRObDXI3nRObDXI3nRObDXI3nRObDXK3j50Bl8Q6KdnfWvtba+32fTdYa7dba38r6XOtDVlrp1prB1trB4dCXVvcH66qVjhcrZWle59V9OTfFmrQwCPiDl0VrlbfooKmz4sK+6i6ekfWzga5m9zp3R1k52T4eKwkKTc3V4/PmabZs5/S008/k5bMyc77eKzo7EduHzsHuZvOfuT2sXOQu+nsR24fOwe528fOgEtinZT8tzFmojGm974bjDG9jTE/l7S1rUt37HhH4fA2HX74FyRJJ588VJWVb8Q9X1pWrgEDDlP//n2Vl5enMWNGa/6CJVk7S25yp3o2WT4eK2nvFcsr17+lu++ZGvdMe+x19THmYm4fO7ua28fOrub2sbOruX3s7GpuHzu7mtvHzq7mdrUzJBuJ8NHsI5Plxrj/u5Kuk/TPxhOTVtIOSX+XNCaZxVdedaMenvkHdeiQp42btuhHP7o67tmGhgZdceUNWrTwUeWEQpoxc44qKuI7qeniLLnJnepZSXpk1n06cdhx6tmzhzZvLNOkWyZr+ozHMj53ULu/cfwQjRt7rlavqVBZ6d6/INx44x16ZvELKd2b7LyPx4rOfuT2sbOruX3s7GpuHzu7mtvHzq7m9rGzq7ld7Qy4xMR6XwJjzJclFUlaYa2taXb7KGvt4lgL8joUtvmND3jHBAAAAAAAgJbq91S1fPNJqOYX53A6qZlutz+ZsY+TWFffvlzSPEmXSlprjBnd7O7bUhkMAAAAAAAAQHaK9fLt/5N0lLW2xhjTX9ITxpj+1tp7JGXsmVYAAAAAAAAAmSvWScmcfS/ZttZuNsYM194Tk/3ESUkAAAAAAABkkgiv3nZFrKtvbzfGDNz3SeMJyjMl9ZR0RCqDAQAAAAAAAMhOsU5KXiBpe/MbrLX11toLJA1LWSoAAAAAAAAAWSvqy7etteEo9/2r/eMAAAAAAAAAyHaxnikJAAAAAAAAAO0q1oVuAAAAAAAAADdwoRtn8ExJAAAAAAAAAGkVyEnJww//gspKlzR9vPfuel1+2Y8S+hojRwzXurXLtL6iWBOvvSTrZ4PcTW53cvvYOcjdbZ0tKirQc0se15rVS/Va+Qu67NKLE9qbzO4gZ4PcTWc/cvvYOcjddPYjt4+dg9xNZz9y+9g5yN0+dgZcYaxN7dNa8zoURl0QCoX0782v6BtDz9SWLVWfuq+1wVAopMp1yzXq9PMUDldrxcuLNHbcBFVWvhkzj4uz5CY3nTNvdzKz+fm91Ce/l1aVr1W3bl21smSxzjn3h1nd2dXcPnZ2NbePnV3N7WNnV3P72NnV3D52djW3j51dze1C5/o9VSauMJ6pufbbvH67mW53PpWxj5PAX7598slDtXHjv1uckIzm6CGDtGHDZm3atEV1dXWaO3eezj5rZNbOkpvcqZ4ld3pnt29/W6vK10qSampqtX79myosyI9rNsjcPh4rHzu7mtvHzq7m9rGzq7l97Oxqbh87u5rbx86u5na1M+CSwE9KfnfMaM2Z83RCMwWF+doa3tb0ebiqWgVx/g+9i7NB7iZ3enfT2Z/c+/TrV6SBX/8flaxcFfeMq51dzO1j5yB309mP3D52DnI3nf3I7WPnIHfT2Y/crnaGJBvho/lHBmvzSUljzDPJLs/Ly9OZZ47QE08uSHR3i9vifRm6i7NB7iZ3enfTObHZIHcnm1uSunbtorlzpunqa27Srl01cc+52tnF3D52DnI3nRObDXI3nRObDXI3nRObDXI3nRObDXI3nRObDXK3j50Bl+RGu9MYc2Rrd0kaGGVuvKTxkhTK6a5QqOsBf96oUSdp1ao1evvtd+NL26gqXK2+RQVNnxcV9lF19Y6snQ1yN7nTu5vO/uTOzc3V43Omafbsp/T004n9G4+rnV3M7WPnIHfT2Y/cPnYOcjed/cjtY+cgd9PZj9yudgZcEuuZkqWSJkuast/HZEmHtDZkrZ1qrR1srR3c2glJSfrud7+V8Eu3Jam0rFwDBhym/v37Ki8vT2PGjNb8BUuydpbc5E71LLnTn3va1CmqXP+W7r5natwzQef28Vj52NnV3D52djW3j51dze1jZ1dz+9jZ1dw+dnY1t6udAZdEfaakpEpJP7bWtrg8lDFmazKLO3fupFNPGaYJE36e8GxDQ4OuuPIGLVr4qHJCIc2YOUcVFW9k7Sy5yZ3qWXKnd/Ybxw/RuLHnavWaCpWV7v3LxY033qFnFr+Q0bl9PFY+dnY1t4+dXc3tY2dXc/vY2dXcPnZ2NbePnV3N7WpnwCUm2vsSGGPOlbTGWvv6Ae77lrU25tMc8zoUtvmND3jHBAAAAAAAgJbq91S1fPNJqOaa0ZxOaqbb5HkZ+ziJ+kxJa+0TxpgvG2NOkVRirW1+JYaPUxsNAAAAAAAASECEc5KuiPqeksaYyyXNk3SZpLXGmNHN7r4tlcEAAAAAAAAAZKdY7yn5f5KOstbWGGP6S3rCGNPfWnuP9l6BGwAAAAAAAAASEuukZM6+l2xbazcbY4Zr74nJfuKkJAAAAAAAAIA2iPrybUnbjTED933SeILyTEk9JR2RymAAAAAAAAAAslOsZ0peIKm++Q3W2npJFxhjHohnAW8vCgBtkxOK9e9GrWuIRNoxCQAAAAC4wXKhG2fEuvp2OMp9/2r/OAAAAAAAAACyXdufhgMAAAAAAAAAbcBJSQAAAAAAAABpxUlJAAAAAAAAAGkVyEnJjh076uV/LdArZc/qtfIXdNOvfpbw1xg5YrjWrV2m9RXFmnjtJVk/G+RucruT28fOQe5O5+wDD0zW1i2r9OorzzXddsMNV2njhlKtLFmslSWLNWrkSRmXO1N209mP3D52DnJ3MrPTpk7RtvBrKl/1fEJz7bGbY0XnTN5NZz9y+9g5yN0+dvZexPLR/CODGWtTGzC3Q+EBF3Tt2kW1tR8pNzdXy5Y+pauuvkklK1+N62uGQiFVrluuUaefp3C4WiteXqSx4yaosvLNrJwlN7npnHm70zHb/OrbQ4ceo5qaWj304N068qhTJe09KVlb85HuuvuBFjtau/o2x4rO2Zrbx84u5z6h8fe06dPv0cBBp8Q1E3RuH4+Vj51dze1jZ1dz+9jZ1dwudK7fU2XiCuOZXZefmdln4tLsoHsXZOzjJLCXb9fWfiRJysvLVW5enhI5OXr0kEHasGGzNm3aorq6Os2dO09nnzUya2fJTe5Uz5I782eLi0u0c+cHcX39TMqdCbvp7EduHzu7nHt5cYneb+Pvaa52djG3j51dze1jZ1dz+9jZ1dyudgZcEthJyVAopLLSJaquWq3nn1+mlaWr4p4tKMzX1vC2ps/DVdUqKMjP2tkgd5M7vbvp7EfuZDs395OfXqiy0iV64IHJOuSQ7indzbHyo3OQu+nsT+5kuNrZxdw+dg5yN539yO1j5yB3+9gZcEnUk5LGmIONMbcbY2YZY87f774/JbM4Eolo8JAR6nfYYA0ZPEhf/eqX4p41puUzT+N9pqWLs0HuJnd6d9M5sdkgdwfZeZ+pU2fpK18ZqiFHj9T27W/rt7+9MaW7OVaJzQa528fcPnYOcnd7/T7WFq52djG3j52D3E3nxGaD3E3nxGaD3O1jZ8AlsZ4pOV2SkfSkpO8ZY540xnRsvO/Y1oaMMeONMWXGmLJIpDbqgg8//I/+uewljRwxPO7QVeFq9S0qaPq8qLCPqqt3ZO1skLvJnd7ddPYjd7Kd93n77XcViURkrdVDDz2qIYMHZnRuF7/fPnYOcjed/cmdDFc7u5jbx85B7qazH7l97Bzkbh87Ay6JdVLyC9ba66y1T1trz5b0qqQXjDGfjTZkrZ1qrR1srR0cCnVtcX/Pnj3UvfvBkqROnTrplJNP0Ouvb4g7dGlZuQYMOEz9+/dVXl6exowZrfkLlmTtLLnJnepZcrsz21x+fq+mH48+e5TWrXs9o3O7+P32sbOruX3s7HLuZLja2cXcPnZ2NbePnV3N7WNnV3O72hmSIhE+mn9ksNwY93c0xoSstRFJstb+xhgTlrRMUre2Lu3Tp7ceevBu5eSEFAqF9MQT87Vw0XNxzzc0NOiKK2/QooWPKicU0oyZc1RR8UbWzpKb3KmeJXfmzz788B817IRj1bNnD214a6Vu/fUUDRt2nL7+ta/KWqt//zusSy69LuNyZ8JuOvuR28fOLud+ZNZ9OnHYcerZs4c2byzTpFsma/qMxzI6t4/HysfOrub2sbOruX3s7GpuVzsDLjHR3pfAGPM7SUustc/td/soSX+w1n4x1oLcDoW88QEAtEFOqO3XImvI8H8RAwAAAJCc+j1VLd98Etp16emch2rmoD8uytjHSdT/47XWTpQUNsacYozp1uz2xZIuT3U4AAAAAAAAANkn1tW3L5M0T9JlktYaY0Y3u/s3qQwGAAAAAAAAIDvFek/J8ZKOstbWGGP6S3rCGNPfWnuP9l6VGwAAAAAAAMgMEV697YpYJyVzrLU1kmSt3WyMGa69Jyb7iZOSAAAAAAAAANog1knJ7caYgdbacklqfMbkmZIeknREytMBgMeSuVhNXk6s396jq2uoT2oeAAAAAIBoYl3a9QJJ25vfYK2tt9ZeIGlYylIBAAAAAAAAyFpRn0pjrQ1Hue9f7R8HAAAAAAAAQLZL7vV9AAAAAADg/7N393FWl3X+x9+fwwwqYCoiDjNDjMW2bmVCguYdUpSgibRqtBZo5cavVNJt06z050+3XEspqWwVKiANBN2EVZFQ1ASTgdEZuZkZQYSFGQbvwHTIYm6u3x/cNArMOWfOnHOd61yv5+MxD5kz85nP+z1nRLj8nnMA5Ate6CYYyR6+DQAAAAAAAADdikNJAAAAAAAAADnl5VCyvLxUjy++X6tXPaUXap7Q5CsvS/trjD57pNaueVr1tct07TVXFPysz93kDid3jJ197g6lc3n5AC1adJ+qq5fouece0xVXfOVdH7/66kl6553/1dFHH5VXuQth1ufuGHPH2NnnbjrHkTvGzj530zmO3DF29rk7xs5AKMy57D7Wvqhn2X4LSkr6a0BJf1XXrFGfPr21onKRLrzoq6qrW5/S10wkEqpbu1Rjzr1YDQ1NWv7sQk2YeHlK8yHOkpvcdM6/3fneubjH358yuKSkv0pK+qtmz++5f/rTwxo/fpLq69ervHyAfvnLH+kf//GDOu208/TGGzskSS1trV5yF9IsucOZJXc4s+QOZ5bc4cySO5xZcoczm6vdrbsaLaUwkXn762N4UskODr9rUd7+nHi5UnLbtldVXbNGktTcvFP19etVVlqS8vzJw4dqw4ZN2rhxs1paWjRv3gKdP3Z0wc6Sm9zZniV3OLNdmd+27VXVvOv33JdUWnqsJOnHP/6/+v73/1Op/A8q7qs4OoeaO8bOoeaOsXOouWPsHGruGDuHmjvGzqHmDrUzEJJODyXNrMTM/svM7jSzo83s/5nZajObZ2YDuiPAoEHlGnLiR1W5ojrlmdKyEm1p2Lrv/YbGJpWmeKgZ4qzP3eTO7W46x5HbZ+f3v79cQ4Z8RCtX1uizn/20tm7dptWr6/I+d4izPnfHmDvGzj530zmO3DF29rmbznHkjrGzz90xdobknOOtw1s+S3al5ExJtZK2SHpS0juSPitpqaS7DjZkZpPMrMrMqtrbdx70i/fu3Uvz5k7Xt759o95+uznl0Gb7X3ma6jc6xFmfu8md2910Tm/W5+4QO/fu3Utz5tyla665Wa2trfrOd67UzTf/JOt7u2M+xFmfu2PMHWNnn7vpnN6sz910Tm/W5246pzfrczed05v1uTvGzkBIkh1KHuuc+7lz7lZJRzrnfuSc2+yc+7mkQQcbcs5Nc84Nc84NSyR6H/BzioqKdP/c6Zoz50HNn/9oWqEbG5o0sLx03/vlZQPU1PRKwc763E3u3O6mcxy5fXQuKirSnDl3ae7c+VqwYJE+8IFBGjRooFaseFT19ctUVjZAzz77iI499pi8yh3yrM/dMeaOsbPP3XSOI3eMnX3upnMcuWPs7HN3jJ2BkCQ7lOz48d++52M9Mlk8fdoU1dW/pDumTkt7dmVVjQYPPk4VFQNVXFys8ePH6aGHFxfsLLnJne1Zcocz29X5u+76sV588SX97Ge/kiStXfuiBg06Sccff4aOP/4MNTY26dRTP6tXXnktr3KHPEvucGbJHc4sucOZJXc4s+QOZ5bc4cz63g2EoijJxxeYWR/nXLNz7vq9N5rZYEkvdnXp6acN18QJF2nV6lpVrdz9L9YNN9yqRxc9kdJ8W1ubrrr6ei18ZLZ6JBKaOWuuamvXFewsucmd7VlyhzPblfnTThumL33pQq1eXaflyxdKkm688Tb94Q9PprzTR+7QZ8kdziy5w5kldziz5A5nltzhzJI7nFnfu4FQWLLnJTCz4yWVSap0zjV3uH2Mc25RsgVFPct44gMAyLHiHsn+n1PnWtpauykJAAAAgGxo3dW4/5NPQm997WzOoTp43/TFeftzkuzVtydLWiBpsqQ1Zjauw4dvyWYwAAAAAAAAAIUp2aU0kySd5JxrNrMKSQ+YWYVzbqqkvD1pBQAAAAAAAJC/kh1K9tj7kG3n3CYzG6ndB5ODxKEkAAAAAAAAgC5I9urb28xsyN539hxQniepn6QTshkMAAAAAAAAQGFKdqXkJZLe9WoHzrlWSZeY2d2pLEhY1y+obE/yIjwAgAPL9IVqjj7s8C7PvvHO2xntBgAAAIAua+csKRSdHko65xo6+dgz3R8HAAAAAAAAQKFL9vBtAAAAAAAAAOhWHEoCAAAAAAAAyCkOJQEAAAAAAADkVE4PJafdfbsattSo+vnH99121FFHauHC2Vq7dqkWLpytI488IqWvNfrskVq75mnV1y7TtddckVaOEGd97o4xd3l5qR5ffL9Wr3pKL9Q8oclXXpaz3dxXceQOqfP7jjhcv/rtVC1buVBLVzyiYcOH6MijjtC8+b/Ws88v0rz5v9YRR74v73Lnw6zP3THmjrGzz910jiN3jJ197g6x8/RpU7S14QXVVC9Je2cmezPd7TO3r/vK599xMp0Pcdb3biAE5rL8Ctc9Dynft+CMM05Rc/NOzfjNHRr68U9Lkv7zlu9r+/Y3ddvtd+qab1+ho446Qt/7/i2SDv7q24lEQnVrl2rMuReroaFJy59dqAkTL1dd3fqkeUKcJXfuc5eU9NeAkv6qrlmjPn16a0XlIl140VfzOnes91WIuUPo3PHVt3/2X7eq8tkq/e63D6i4uFiH9TpUV/37/9GbO/6sn/90uib/29d0xJHv0w9unCLp4K++HeL3O4T7itzxdg41d4ydQ80dY+dQc/vsfObev+PNmKohQ0eltK+7cmey21dun/eVr7/jZDof4myudrfuarSUwkTmz1/5NC+/3cERMx7P25+TnF4puWxZpXbsePNdt40de7buufd+SdI9996v888fnfTrnDx8qDZs2KSNGzerpaVF8+Yt0Pljk8+FOkvu3Ofetu1VVdeskSQ1N+9Uff16lZWW5HXuWO+rEHOH1LnP4b116unD9LvfPiBJamlp0Vt/fltjzh2lubPnS5Lmzp6vcz776bzKnQ+z5A5nltzhzJI7nFlyhzOb6fzSZZXa/p6/4+Vib6a7feX2eV/5+jtOpvMhzvreDYQi7UNJM+vfnQH69++nbdtelbT7N8ljjjk66UxpWYm2NGzd935DY5NKU/zNNMRZn7tjzd3RoEHlGnLiR1W5ojrru7mv4sgdUudBFQP1xuvbNfWX/6nHl/5eP/n5f6hXr8N0zDFH69VXXpMkvfrKa+p3TN+8yp0Psz53x5g7xs4+d9M5jtwxdva5O9TOmfC1N1OFcF/l8u84mc6HOOt7NxCKTg8lzazve96OlrTCzI4ys+R/A80Ss/2vPE31YeghzvrcHWvuvXr37qV5c6frW9++UW+/3Zz13dxX6c363B1L56KiIp1w4oc169dz9OkzL9Bfdr6jyf/2tZSzZrI79Fmfu2PMHWNnn7vpnN6sz910Tm/W5+5QO2fC195MhX5f5frvOJnOhzjrezcQimRXSr4u6bkOb1WSyiQ9v+fXB2Rmk8ysysyq2tt2dqdQB78AACAASURBVLrg1VdfV0nJ7osvS0r667XX3kgaurGhSQPLS/e9X142QE1NrySdC3XW5+5Yc0u7D2Punztdc+Y8qPnzH015LtTO5A5jNte7tzZu09bGV/T8c6skSQ8t+INOOPHDeu21N9T/2GMkSf2PPUavv7Y9r3Lnw6zP3THmjrGzz910jiN3jJ197g61cyZ87c1UyPeVj7/jZDof4qzv3UAokh1KXivpRUnnO+eOc84dJ6lhz68/cLAh59w059ww59ywRI/enS546OHHNHHC5yVJEyd8Xg89tDhp6JVVNRo8+DhVVAxUcXGxxo8fp4ceTj4X6iy5c59b2v2KfHX1L+mOqdPSmgu1M7nDmM317tdefV1bG5v0wcHHSZLOPOtUrXtxg/7w6BP6whc/J0n6whc/p0ULk79yZYjf75Duq9hzx9g51Nwxdg41d4ydQ83ts3MmfO3NVMj3lY+/42Q6H+Ks793Ra3e8dXzLY0WdfdA5d7uZ3Sfpp2a2RdKNkrrc6J7f/kIjRpyqfv366uUNK3Xzf0zRbbf9QrNn36Uvf+VftGVLoy6++OtJv05bW5uuuvp6LXxktnokEpo5a65qa9ellCHEWXLnPvfppw3XxAkXadXqWlWt3P2b/w033KpHFz2Rt7ljva9CzB1a5+9d+wP98le3qWdxsf530xZddcX3lLCEps/6qb448UI1NjTpXy+9Ou9y+54ldziz5A5nltzhzJI7nNlM5++9506dtefveJtertJNN9+uGTPvy0nuTHb7yu3zvvL1d5xM50Oc9b0bCIWl8ZwGYyV9X1KFcy7lZ1jteUh5lw8x23nOBADw4ujDDu/y7BvvvN2NSQAAAAAcSOuuxv2ffBL686WjOEzq4IhZS/L25yTpq2+b2fFmNkrSk5I+KenTe24fk+VsAAAAAAAAAApQslff/qakBZImS1oj6Wzn3Jo9H74ly9kAAAAAAAAAFKBOn1NS0tckneScazazCkkPmFmFc26qpLy9/BMAAAAAAAARavcdAKlKdijZwznXLEnOuU1mNlK7DyYHiUNJAAAAAAAAAF2Q7Dklt5nZkL3v7DmgPE9SP0knZDMYAAAAAAAAgMKU7ErJSyS1drzBOdcq6RIzuzuVBbyCNgCEh1fQBgAAAABkU6eHks65hk4+9kz3xwEAAAAAAABQ6JJdKQkAAAAAAAAEwbXziN1QJHtOSQAAAAAAAADoVhxKAgAAAAAAAMgpb4eSo88eqbVrnlZ97TJde80VOZ0PcdbnbnKHkzvGzj530zmO3DF29rmbznHkjrFzJvPTp03R1oYXVFO9JO2dmezNdNbn7hhzx9jZ52465y53eXmpHl98v1avekov1DyhyVdelpO9mc763g2EwFyWXx27qGfZfgsSiYTq1i7VmHMvVkNDk5Y/u1ATJl6uurr1KX3NTOZDnCU3uemcf7vpHEfuGDuHmjvGzqHmjrFzpvNnnnGKmpt3asaMqRoydFRK+7pjL/dVOLlj7Bxq7hg7ZzpfUtJfA0r6q7pmjfr06a0VlYt04UVfLejOqc627mq0lMJE5s0vfYonlezgyN89kbc/J16ulDx5+FBt2LBJGzduVktLi+bNW6Dzx47OyXyIs+Qmd7ZnyR3OLLnDmSV3OLPkDmc21txLl1Vq+443U97VXXu5r8LJHWPnUHPH2DnT+W3bXlV1zRpJUnPzTtXXr1dZaUnW94Z6XwEh8XIoWVpWoi0NW/e939DYpNIUf1PJdD7EWZ+7yZ3b3XSOI3eMnX3upnMcuWPs7HM3nXObOxOhdiY3nfN5N539/R44aFC5hpz4UVWuqM763lDvK0hqd7x1fMtjnR5KmtmYDr8+wsx+bWarzGy2mR3b1aVm+185ms7DyDOZD3HW525y53Y3ndOb9bmbzunN+txN5/Rmfe6mc3qzPnfTOb3Z7pjvqlA7kzt3sz53x5g7xs7dMS9JvXv30ry50/Wtb9+ot99uzvreUO8rICTJrpS8pcOvp0hqkjRW0kpJdx9syMwmmVmVmVW1t+/c7+ONDU0aWF667/3ysgFqanol5dCZzIc463M3uXO7m85x5I6xs8/ddI4jd4ydfe6mc25zZyLUzuSmcz7vpnPufw8sKirS/XOna86cBzV//qM52RvqfQWEJJ2Hbw9zzl3vnPtf59xPJVUc7BOdc9Occ8Occ8MSid77fXxlVY0GDz5OFRUDVVxcrPHjx+mhhxenHCST+RBnyU3ubM+SO5xZcoczS+5wZskdzmysuTMRamdy0zmfd9M5978HTp82RXX1L+mOqdNSnsl0b6j3FRCSoiQf729m35Jkkt5nZub+fs1wl5+Psq2tTVddfb0WPjJbPRIJzZw1V7W163IyH+Isucmd7VlyhzNL7nBmyR3OLLnDmY0197333KmzRpyqfv36atPLVbrp5ts1Y+Z9Wd/LfRVO7hg7h5o7xs6Zzp9+2nBNnHCRVq2uVdXK3QdzN9xwqx5d9ERW94Z6XwEhsc6el8DMbnzPTb90zr1mZiWSfuycuyTZgqKeZTzxAQAAAAAAQDdq3dW4/5NPQm9+4ZOcQ3Vw5Nwn8/bnpNMrJZ1zN5nZ8ZLKJFU655r33L7NzGbnIiAAAAAAAACAwpLs1bcnS1ogabKkNWY2rsOHbznwFAAAAAAAAAAcXLLnlJwk6STnXLOZVUh6wMwqnHNTtft5JgEAAAAAAAAgLckOJXt0eMj2JjMbqd0Hk4PEoSQAAAAAAACALkh2KLnNzIY452okac8Vk+dJ+o2kE7KeDgAAAAAAAEiRa+d1bkLR6XNKSrpE0raONzjnWve86vaIrKUCAAAAAAAAULCSvfp2Qycfe6b74wAAAAAAAAAodMmulAQAAAAAAACAbsWhJAAAAAAAAICc4lASAAAAAAAAQE55O5QcffZIrV3ztOprl+naa67I6XyIsz53kzuc3JnMTp82RVsbXlBN9ZK05rpjN/dVHJ0zmT/kkEP07DMP67mqx/RCzRO68f/+e072Zjrrc3eMuWPs7HM3nePIHWNnn7vpHEfuGDv73B1j5+i18/autzxmzmX3pdKLepbttyCRSKhu7VKNOfdiNTQ0afmzCzVh4uWqq1uf0tfMZD7EWXKTOxedzzzjFDU379SMGVM1ZOiolGbyIXeI3+8YO3fHfO/evbRz519UVFSkp596UP/2rRtVueL5rO7lvgond4ydQ80dY+dQc8fYOdTcMXYONXeMnUPNHULn1l2NllKYyOy4cGR2D7oCc9R/P5W3PydpXylpZkdnuvTk4UO1YcMmbdy4WS0tLZo3b4HOHzs6J/MhzpKb3NmelaSlyyq1fcebKX9+vuQO8fsdY+fumN+58y+SpOLiIhUVFyvV/6kWamdy0zmfd9M5jtwxdg41d4ydQ80dY+dQc4faGQhJp4eSZnarmfXb8+thZvaypEoz+18zO6urS0vLSrSlYeu+9xsam1RaWpKT+RBnfe4md253++ycCe4rOudiPpFIqGrlYjU1rtKSJU9rxcrqrO/lvsrtbjrHkTvGzj530zmO3DF29rmbznHkDrUzEJJkV0p+1jn3+p5f3ybpC865wZI+I2nKwYbMbJKZVZlZVXv7zgN9fL/b0nkYeSbzIc763E3u3O722TkT3Fe5m/W522duSWpvb9ew4Wdr0HHDNHzYUH3kI/+Y9b3cV7ndTef0Zn3upnN6sz530zm9WZ+76ZzerM/ddE5v1ufuGDsDISlK8vFiMytyzrVKOsw5t1KSnHPrzOyQgw0556ZJmiYd+DklGxuaNLC8dN/75WUD1NT0SsqhM5kPcdbnbnLndrfPzpngvqJzLub3+vOf39Ifn/7T7if/XvtiVvdyX+V2N53jyB1jZ5+76RxH7hg7+9xN5zhyh9oZkmvnADcUya6UvFPSQjP7lKRFZnaHmY0ws5sk1XR16cqqGg0efJwqKgaquLhY48eP00MPL87JfIiz5CZ3tmczxX1F52zP9+vXV0cc8T5J0qGHHqpRnzpTL764Iet7ua/CyR1j51Bzx9g51Nwxdg41d4ydQ80dY+dQc4faGQhJp1dKOud+bmarJX1D0of2fP6HJM2X9IOuLm1ra9NVV1+vhY/MVo9EQjNnzVVt7bqczIc4S25yZ3tWku69506dNeJU9evXV5tertJNN9+uGTPvy/vcIX6/Y+yc6fyAAcfqN7++Qz16JJRIJPTAAw/pkYWPZ30v91U4uWPsHGruGDuHmjvGzqHmjrFzqLlj7Bxq7lA7AyGxZM9LYGbHSyqTVOmca+5w+xjn3KJkCw708G0AAAAAAAB0Xeuuxv2ffBLa/s9ncQ7VQd8H/5i3PyfJXn37m5IWSJosaY2Zjevw4VuyGQwAAAAAAABAYUr2Qjdfk3SSc67ZzCokPWBmFc65qZLy9qQVAAAAAAAAEWr3HQCpSnYo2WPvQ7adc5vMbKR2H0wOEoeSAAAAAAAAALog2atvbzOzIXvf2XNAeZ6kfpJOyGYwAAAAAAAAAIUp2aHkJZK2dbzBOdfqnLtE0oispQIAAAAAAABQsDp9+LZzrqGTjz3T/XEAAAAAAAAAFLpkV0oCAAAAAAAAQLdK9kI3AAAAAAAAQBAcr74dDK6UBAAAAAAAAJBT3g4lp0+boq0NL6imekmX5kefPVJr1zyt+tpluvaaK4KYjbGzz90x5o6xs8/ddA4jd3l5qR5ffL9Wr3pKL9Q8oclXXpazzJnOx3Zf+Zz1uTvG3DF29rmbznHkjrGzz910jiN3qJ2BUJhzLqsLinqWHXDBmWecoubmnZoxY6qGDB2V1tdMJBKqW7tUY869WA0NTVr+7EJNmHi56urW5+2sFGdncocxS+5wZsmd/mxJSX8NKOmv6po16tOnt1ZULtKFF321oDvHmDvGzqHmjrFzqLlj7Bxq7hg7h5o7xs6h5g6hc+uuRkspTGTeGHtWdg+6AnP0Q3/M258Tb1dKLl1Wqe073uzS7MnDh2rDhk3auHGzWlpaNG/eAp0/dnRez0pxdiZ3GLPkDmeW3OnPbtv2qqpr1kiSmpt3qr5+vcpKS7K+N9P5GO8rOseRO8bOoeaOsXOouWPsHGruGDuHmjvUzkBIgnxOydKyEm1p2Lrv/YbGJpWm+BdMX7OZCrUzucOY9bk7xtwxdva9e69Bg8o15MSPqnJFdU72cl+FMetzd4y5Y+zsczed48gdY2efu+kcR+5QO0NSO2/vestjnR5KmtnzZna9mX0wV4FSYbb/laepPgzd12ymQu1M7jBmfe6OMXeMnX3vlqTevXtp3tzp+ta3b9TbbzfnZC/3VRizPnfHmDvGzj530zm9WZ+76ZzerM/ddE5v1ufuGDsDIUl2peRRko6U9KSZrTCzfzOz0mRf1MwmmVmVmVW1t+/slqAdNTY0aWD532OUlw1QU9MreT2bqVA7kzuMWZ+7Y8wdY2ffu4uKinT/3OmaM+dBzZ//aE4yZzof431F5zhyx9jZ5246x5E7xs4+d9M5jtyhdgZCkuxQcodz7tvOufdL+ndJ/yDpeTN70swmHWzIOTfNOTfMOTcskejdnXklSSurajR48HGqqBio4uJijR8/Tg89vDivZzMVamdyhzFL7nBmyd213dOnTVFd/Uu6Y+q0lGe6Yy/3VRiz5A5nltzhzJI7nFlyhzNL7nBmfe8GQlGU6ic655ZKWmpmkyV9RtIXJKX3t7sO7r3nTp014lT169dXm16u0k03364ZM+9LabatrU1XXX29Fj4yWz0SCc2cNVe1tevyelaKszO5w5gldziz5E5/9vTThmvihIu0anWtqlbu/sPcDTfcqkcXPZHVvZnOx3hf0TmO3DF2DjV3jJ1DzR1j51Bzx9g51NyhdgZCYp09L4GZ3eec+5dMFhT1LOOJDwAAAAAAALpR667G/Z98Enr9nLM4h+qg36N/zNufk04fvu2c+xczO97MRplZn44fM7Mx2Y0GAAAAAAAAoBAle/XtyZIWSJosaY2Zjevw4VuyGQwAAAAAAABAYUr2nJKTJJ3knGs2swpJD5hZhXNuqqS8vfwTAAAAAAAAQP5KdijZwznXLEnOuU1mNlK7DyYHiUNJAAAAAAAAAF3Q6cO3JW0zsyF739lzQHmepH6STshmMAAAAAAAAACFKdmVkpdIau14g3OuVdIlZnZ31lIBAAAAAAAA6Wr3HQCp6vRQ0jnX0MnHnun+OAAAAAAAAAAKXbKHbwMAAAAAAABAt+JQEgAAAAAAAEBOcSgJAAAAAAAAIKe8HUpOnzZFWxteUE31ki7Njz57pNaueVr1tct07TVXFPysz93kDid3jJ197qZz4ecuLy/V44vv1+pVT+mFmic0+crL0tqbyW6fsz530zmO3DF29rmbznHkjrGzz910jiN3qJ1j59p56/iWz8w5l9UFRT3LDrjgzDNOUXPzTs2YMVVDho5K62smEgnVrV2qMederIaGJi1/dqEmTLxcdXXrC3KW3OSmc/7tpnMcuUtK+mtASX9V16xRnz69taJykS686KsF3TnU3DF2DjV3jJ1DzR1j51Bzx9g51Nwxdg41dwidW3c1WkphIvPaZ87K7kFXYI557I95+3Pi7UrJpcsqtX3Hm12aPXn4UG3YsEkbN25WS0uL5s1boPPHji7YWXKTO9uz5A5nlty5nd227VVV16yRJDU371R9/XqVlZakNOszd4z3VYydQ80dY+dQc8fYOdTcMXYONXeMnUPNHWpnICSdHkqa2TAze9LM7jWzgWb2mJn92cxWmtnQXIV8r9KyEm1p2Lrv/YbGJpWm+JfEEGd97iZ3bnfTOY7cMXb2uTvT3HsNGlSuISd+VJUrqlOeCbVziLlj7OxzN53jyB1jZ5+76RxH7hg7+9wdY2cgJMmulPylpB9LekTSnyTd7Zw7QtJ1ez52QGY2ycyqzKyqvX1nt4Xt8PX3uy3Vh6GHOOtzN7lzu5vO6c363E3n9GZ97s40tyT17t1L8+ZO17e+faPefrs55blQO4eYO8bOPnfTOb1Zn7vpnN6sz910Tm/W5246pzfrc3eMnYGQFCX5eLFz7lFJMrMfOecekCTn3BIzu/1gQ865aZKmSQd/TslMNDY0aWB56b73y8sGqKnplYKd9bmb3LndTec4csfY2efuTHMXFRXp/rnTNWfOg5o//9GU5zLdzX1F53zeTec4csfY2eduOseRO8bOPnfH2Bn5/+Iu+LtkV0r+1czONrPPS3Jm9jlJMrOzJLVlPd1BrKyq0eDBx6miYqCKi4s1fvw4PfTw4oKdJTe5sz1L7nBmyZ373NOnTVFd/Uu6Y+q0lGd8547xvoqxc6i5Y+wcau4YO4eaO8bOoeaOsXOouUPtDIQk2ZWSX9fuh2+3Sxot6RtmNlNSo6SvZbL43nvu1FkjTlW/fn216eUq3XTz7Zox876UZtva2nTV1ddr4SOz1SOR0MxZc1Vbu65gZ8lN7mzPkjucWXLndvb004Zr4oSLtGp1rapW7v6D4A033KpHFz2R17ljvK9i7Bxq7hg7h5o7xs6h5o6xc6i5Y+wcau5QOwMhsWTPS2Bm/ySpVFKlc665w+1jnHOLki3IxsO3AQAAAAAAYta6q3H/J5+EXh11FudQHfRf8se8/TlJ9urb35T0oKTJktaY2bgOH74lm8EAAAAAAAAAFKZkD9/+mqRhzrlmM6uQ9ICZVTjnpkrK25NWAAAAAAAAxIcXuglHskPJHnsfsu2c22RmI7X7YHKQOJQEAAAAAAAA0AXJXn17m5kN2fvOngPK8yT1k3RCNoMBAAAAAAAAKEzJDiUvkbSt4w3OuVbn3CWSRmQtFQAAAAAAAICC1enDt51zDZ187JnujwMAAAAAAACg0CW7UhIAAAAAAAAAulWyF7oBAAAAAAAAwuB4XeZQcKUkAAAAAAAAgJzyeiiZSCS0csUftODBWWnPjj57pNaueVr1tct07TVXFPysz93kDid3jJ197qZzHLlD7Tx92hRtbXhBNdVL0prrjt0hzvrcHWPuGDv73E3nOHLH2NnnbjrHkTvUzkAozDmX1QVFPcsOuuDqqybppJM+pvcdfrjG/fOlKX/NRCKhurVLNebci9XQ0KTlzy7UhImXq65ufUHOkpvcdM6/3XSOI3eonSXpzDNOUXPzTs2YMVVDho5KacZ37hjvqxhzx9g51Nwxdg41d4ydQ80dY+dQc4fQuXVXI49TPoBXRo7M7kFXYI596qm8/TnxdqVkWdkAnXvOKP3mN3PSnj15+FBt2LBJGzduVktLi+bNW6Dzx44u2Flykzvbs+QOZ5bc4cz63r10WaW273gz5c/Ph9wx3lcx5o6xc6i5Y+wcau4YO4eaO8bOoeYOtTMQEm+Hkj+ZcpOu++4P1N7envZsaVmJtjRs3fd+Q2OTSktLCnbW525y53Y3nePIHWNnn7tj7JypEL/fod5XMeaOsbPP3XSOI3eMnX3upnMcuUPtDMm189bxLZ91eihpZn3M7GYzW2tmfzaz18xsuZl9OZOlnz3303r11df1fPXqLs2b7X/laaoPQw9x1uducud2N53Tm/W5m87pzfrcHWPnTIX4/Q71vooxd4ydfe6mc3qzPnfTOb1Zn7vpnN6sz90xdgZCUpTk47+T9KCk0ZLGS+ot6T5J15vZh5xz3zvQkJlNkjRJkqzHEUoker/r46edNkxjzztb54z5lA499BC9732Ha9bMn+nSL38zpdCNDU0aWF667/3ysgFqanqlYGd97iZ3bnfTOY7cMXb2uTvGzpkK8fsd6n0VY+4YO/vcTec4csfY2eduOseRO9TOQEiSPXy7wjk30znX4Jz7iaTznXPrJX1F0gUHG3LOTXPODXPODXvvgaQkff/6W1XxgWEa/KFP6EsTLteTTz6T8oGkJK2sqtHgwcepomKgiouLNX78OD308OKCnSU3ubM9S+5wZskdzqzv3ZkI8fsd6n0VY+4YO4eaO8bOoeaOsXOouWPsHGruUDsDIUl2peROMzvDObfMzMZK2i5Jzrl2O9D1xDnS1tamq66+Xgsfma0eiYRmzpqr2tp1BTtLbnJne5bc4cySO5xZ37vvvedOnTXiVPXr11ebXq7STTffrhkz78vr3DHeVzHmjrFzqLlj7Bxq7hg7h5o7xs6h5g61MxAS6+x5CczsREnTJX1I0hpJlznnXjSzYyRd7Jz7WbIFRT3LeOIDAAAAAACAbtS6q9HbxWL5rOmMT3IO1cGAZU/m7c9Jp1dKOudeMLNLJZVJWu6ca95z+2tmxjE9AAAAAAAAgLQle/Xtb2r3C91cKWmNmY3r8OFbshkMAAAAAAAAQGFK9pySX5M0zDnXbGYVkh4wswrn3FRJeXv5JwAAAAAAAID8lexQskeHh2xvMrOR2n0wOUgcSgIAAAAAAADogk4fvi1pm5kN2fvOngPK8yT1k3RCNoMBAAAAAAAAKEzJrpS8RFJrxxucc62SLjGzu7OWCgAAAAAAAEiTa/edAKlK9urbDZ187JnujwMAAAAAAACg0CV7+DYAAAAAAAAAdCsOJQEAAAAAAADkFIeSAAAAAAAAAHLK26Hk9GlTtLXhBdVUL+nS/OizR2rtmqdVX7tM115zRcHP+txN7nByx9jZ5246x5E7xs4+d9M5jtwxdva5m85x5I6xs8/ddI4jd6idY+ec8dbhLZ+Zcy6rC4p6lh1wwZlnnKLm5p2aMWOqhgwdldbXTCQSqlu7VGPOvVgNDU1a/uxCTZh4uerq1hfkLLnJTef8203nOHLH2DnU3DF2DjV3jJ1DzR1j51Bzx9g51Nwxdg41dwidW3c15veJkyeNp34quwddgSl79om8/TnxdqXk0mWV2r7jzS7Nnjx8qDZs2KSNGzerpaVF8+Yt0PljRxfsLLnJne1ZcoczS+5wZskdziy5w5kldziz5A5nltzhzJI7nFnfu4FQBPmckqVlJdrSsHXf+w2NTSotLSnYWZ+7yZ3b3XSOI3eMnX3upnMcuWPs7HM3nePIHWNnn7vpHEfuGDv73B1jZyAkRZ190MyKJF0m6Z8llUpykrZKWiDp1865lqwnPHCu/W5L9WHoIc763E3u3O6mc3qzPnfTOb1Zn7vpnN6sz910Tm/W5246pzfrczed05v1uZvO6c363E3n9GZ97o6xMxCSTg8lJd0j6U1J/09Sw57byiVdKuleSV840JCZTZI0SZKsxxFKJHp3R9Z9GhuaNLC8dN/75WUD1NT0SsHO+txN7tzupnMcuWPs7HM3nePIHWNnn7vpHEfuGDv73E3nOHLH2Nnn7hg7Q3LtvhMgVckevv1x59w3nHPLnXMNe96WO+e+IWnowYacc9Occ8Occ8O6+0BSklZW1Wjw4ONUUTFQxcXFGj9+nB56eHHBzpKb3NmeJXc4s+QOZ5bc4cySO5xZcoczS+5wZskdziy5w5n1vRsIRbIrJXeY2ecl/bdzu8+azSwh6fOSdmSy+N577tRZI05Vv359tenlKt108+2aMfO+lGbb2tp01dXXa+Ejs9UjkdDMWXNVW7uuYGfJTe5sz5I7nFlyhzNL7nBmyR3OLLnDmSV3OLPkDmeW3OHM+t4NhMI6e14CM6uQ9CNJn9Tuh3FL0pGSnpR0nXNuY7IFRT3LeOIDAAAAAACAbtS6q3H/J5+EGk75FOdQHZRXPpG3PyedXinpnNtkZj+RNEXSBkn/JOkTkmpTOZAEAAAAAAAAgPdK9urbN0o6Z8/nPSbpZEl/lHSdmQ11zv0w+xEBAAAAAAAAFJJkzyl5kaQhkg6RtE1SuXPuLTO7TVKlJA4lAQAAAAAAkBdce94+WhnvkezVt1udc23Oub9I2uCce0uSnHPvSOJF1gEAAAAAAACkLdmVkrvMrNeeQ8mT9t5oZkcoxUPJokSPLodrbW/r8iwAADEo7dO3y7Nbm7d3YxJgf8U9kv1R8+Ba2lq7MQkAAADyTbI/KY5wzv1NkpxzHQ8hiyVd8/PJ6gAAIABJREFUmrVUAAAAAAAAAApWslff/ttBbn9d0utZSQQAAAAAAACgoHX9MTUAAAAAAABAHnHOdwKkKtkL3QAAAAAAAABAt+JQEgAAAAAAAEBO5fRQ8u67b9Pmzc/ruece23fbCSf8k5566kFVVS3Wf//3b3T44X1S+lqjzx6ptWueVn3tMl17zRVp5Qhx1uducoeTO9TO06dN0daGF1RTvSStue7YHeJsqN8vn7tj6XzZNybq8T89qMee+b1+Pv1HOuSQnpp69616svJ/9Ngzv9dtP79ZRUWpPXNLiN/vkO6r7pr1uTud2fLyAVq06D5VVy/Rc889piuu+Iok6YILztVzzz2mnTs36uMfPyHvcnfnrM/ddI4jd4ydfe6mcxy5Q+0MhMJclh9sf+ih79+34IwzTlZz81/061//VCed9BlJ0rJlD+m73/2Bli6t1KWXjldFxUDddNMUSVJre9sBv2YikVDd2qUac+7Famho0vJnF2rCxMtVV7c+aZ4QZ8lN7kLuLElnnnGKmpt3asaMqRoydFRKM75z8/0K52es0DuX9ukrSTp2QH/998JZGnXq5/S3v/5Nv/zN7XrisaV647XtevLxpZKkn0//kSr/9JzunTFPkrS1ebu33Pk0S+7szRb32H0IXlLSXyUl/VVTs0Z9+vTWn/70sMaPnyTnnNrb2/WLX9yi7373h3r++dX7ZlvaWoPsnG+76RxH7hg7h5o7xs6h5g6hc+uuRkspTGQ2DxvFs0p28P6qJXn7c5LTKyWXLVuhHTvefNdtH/rQB7R0aaUkacmSpfrc585N+nVOHj5UGzZs0saNm9XS0qJ58xbo/LGjU8oQ4iy5yZ3tWd+7ly6r1Pb3/N6Q77n5foXzMxZT56KiIh166CHq0aOHDjvsUL2y7dV9B5KSVPP8Gg0oPTbvcvueJXf2Z7dte1U1NWskSc3NO1Vf/5JKS4/Viy++pPXrX05pp4/c3TUbau4YO4eaO8bOoeaOsXOouUPtDMm1G28d3vJZlw8lzWxadwRYu/ZFnXfe7qsmL7jgsyovH5B0prSsRFsatu57v6GxSaWlJSntC3HW525y53Z3jJ0zFeL3O8bvl8/dsXR+pelVTfvFTC1f9Ziq6p7QW281a+mTz+77eFFRkS4Yf57+uOSZvMqdD7M+d8eY+/3vL9eQIR/RypU1KX1+d+7mvqJzPu+mcxy5Y+zsc3eMnYGQdHooaWZ9D/J2tKSDXtJoZpPMrMrMqtramjsN8H/+zzX6+tcv1Z/+9IgOP7yPdu1qSRrabP+T3lQfhh7irM/d5M7t7hg7ZyrE73eM3y+fu2PpfMQR79NnzvmkTh86RsM/PEq9eh2mf/78efs+/sPbv68Vzz6nFcufz6vc+TDrc3dsuXv37qU5c+7SNdfcrLff7vzPiN292+esz910Tm/W5246pzfrczed05v1uTvGzkBIkj3b/WuS/ldSx38j3J73+x9syDk3TdI06d3PKXkg69Zt0HnnTZAkDR58nMaM+VTS0I0NTRpYXrrv/fKyAWpqeiXpXKizPneTO7e7Y+ycqRC/3zF+v3zujqXzGSM/oS2bG7X9jR2SpEUPP66TTj5RD97/sK6+9uvqe3RfXfdvV+dd7nyY9bk7ptxFRUWaM+cuzZ07XwsWLEppT3ft9j3rczed48gdY2efu+kcR+5QOwMhSfbw7ZcljXTOHdfh7QPOueMkdcu/Ecccc7Sk3f8n4Lvf/aZ+9at7k86srKrR4MHHqaJioIqLizV+/Dg99PDilPaFOEtucmd71vfuTIT4/Y7x++VzdyydGxua9PFhH9Ohhx0qSTp9xCl6ad1G/cvECzTiU6fryq9dm/L/YQ/x+x3SfRVr7rvu+rFefPEl/exnv0ppR77k7o7ZUHPH2DnU3DF2DjV3jJ1DzR1qZyAkya6UvEPSUZI2H+BjP0532W9/+3Odeeap6tfvKL30UqV+8IOfqHfv3vr61y+RJM2fv0izZs1L+nXa2tp01dXXa+Ejs9UjkdDMWXNVW7supQwhzpKb3Nme9b373nvu1FkjTlW/fn216eUq3XTz7Zox8768zs33K5yfsVg61zy3Wgv/5zEtfHKe2tpatXZVvWbPul/1DSvUuKVJ8/+w+3/6LXp4iabedlfe5M6HWXJnf/a004bpS1+6UKtX12n58oWSpBtvvE2HHNJTP/nJTerXr69+//sZWrWqVueff0ne5O6u2VBzx9g51Nwxdg41d4ydQ80damegK8zsSEm/kvRR7X6E9FclvShprqQKSZskjXfO7bDdzy8wVbuf1vEvkr7snEv+HFEH2pvsqgkzO1mSc86tNLMPSxojqd45tzCVBckevt2Z1va2ro4CABCF0j59uzy7tXl7NyYB9lfcI9n//z64lrbWbkwCAEDhad3VmN8vrezJpiGf4Qk4O6ioeSzpz4mZzZK01Dn3KzPrKamXpO9J2u6cu9XMrpN0lHPuO2Z2rqTJ2n0oeYqkqc65U7qSrdM/KZrZjZLOkVRkZo/tWfaUpOvMbKhz7oddWQoAAAAAAADALzN7n6QRkr4sSc65XZJ2mdk4SSP3fNos7T4P/I6kcZJ+63Zf5bjczI40swHOuaZ0dyf739cXSRoi6RBJ2ySVO+feMrPbJFVK4lASAAAAAAAAyENmNknSpA43TdvzAtV7fUC7X+h6hpmdKOk5SVdJOnbvQaNzrsnM9r7gdZmkLR3mG/bc1u2Hkq3OuTZJfzGzDc65t/aEecfM2tNdBgAAAAAAACA39hxATuvkU4okfVzSZOdcpZlNlXRdJ59/oIeDd+kh88lefXuXmfXa8+uT9m03O0ISh5IAAAAAAABAuBokNTjnKve8/4B2H1K+YmYDJGnPP1/t8PkDO8yXS9ralcXJrpQc4Zz7myQ55zoeQhZLujSVBbxYDQAA2ZPJi9UkrOvPjd6e5IXyAIkXqwEAALnHH1PT45zbZmZbzOwfnXMvSholqXbP26WSbt3zzwV7Rv5H0pVmdp92v/bMn7vyfJJSkkPJvQeSB7j9dUmvd2UhAAAAAAAAgLwxWdLv9rzy9suSvqLdj66eZ2aXSdos6fN7Pnehdr/y9kuS/rLnc7sk2ZWSAAAAAAAAAAqUc65G0rADfGjUAT7XSbqiO/Yme05JAAAAAAAAAOhWHEoCAAAAAAAAyCkvh5Ll5aV6fPH9Wr3qKb1Q84QmX3lZ2l9j9NkjtXbN06qvXaZrr0nvqtEQZ33uJnc4uWPs7HM3nePIHWPnK6+8TNXPP66a6iWaPDmO/0b73B1j7hg7+9xN5zhyx9jZ5246x5E71M6xc+3GW4e3fGYuyy9LVNSzbL8FJSX9NaCkv6pr1qhPn95aUblIF170VdXVrU/payYSCdWtXaox516shoYmLX92oSZMvDyl+RBnyU1uOuffbjrHkbvQOx/o1bc/8uF/1L333qnTTj9Pu3a16OGH79Xkyd/TSy9tfNfnHezVt/O9c77tjjF3jJ1DzR1j51Bzx9g51Nwxdg41dwidW3c15veJkycvn3A2r7/dwQdWL87bnxMvV0pu2/aqqmvWSJKam3eqvn69ykpLUp4/efhQbdiwSRs3blZLS4vmzVug88eOLthZcpM727PkDmeW3OHMhpr7+OMHq7KyWu+881e1tbVp6dPLNW7cmJRmfeaO8b4KNXeMnUPNHWPnUHPH2DnU3DF2DjV3qJ2BkHh/TslBg8o15MSPqnJFdcozpWUl2tKwdd/7DY1NKk3xUDPEWZ+7yZ3b3XSOI3eMnX3upnN6s2trX9SZZ56ivn2P1GGHHaoxYz6l8vLSlGZ95o7xvvK5m85x5I6xs8/ddI4jd4ydfe6OsTMQkqLOPmhmPST9q6RySYucc890+Nj1zrkfZLK8d+9emjd3ur717Rv19tvNKc/ZAR5ulurD0EOc9bmb3LndTef0Zn3upnN6sz530zm92fr6l3Tb7b/UowvnqLl5p1atrlVra2tKs5nu5r5Kb9bnbjqnN+tzN53Tm/W5m87pzfrcTef0Zn3ujrEzEJJkV0reLeksSW9I+pmZ/aTDxy442JCZTTKzKjOram/fecDPKSoq0v1zp2vOnAc1f/6jaYVubGjSwA5XbZSXDVBT0ysFO+tzN7lzu5vOceSOsbPP3XROP/fMmffplE+co1Gfvkg7tr+53/NJ5mPuWO+rEHPH2NnnbjrHkTvGzj530zmO3KF2huSc8dbhLZ8lO5Q82Tn3RefcHZJOkdTHzH5vZodIOmgz59w059ww59ywRKL3AT9n+rQpqqt/SXdMnZZ26JVVNRo8+DhVVAxUcXGxxo8fp4ceXlyws+Qmd7ZnyR3OLLnDmQ059zHHHC1JGjiwVJ/73DmaO3dByrOhdiZ3GLPkDmeW3OHMkjucWXKHM+t7NxCKTh++Lann3l8451olTTKzGyU9IalPV5eeftpwTZxwkVatrlXVyt3/Yt1ww616dNETKc23tbXpqquv18JHZqtHIqGZs+aqtnZdwc6Sm9zZniV3OLPkDmc25Nxz75umo48+Si0trfrmVd/Xm2/+OeXZUDuTO4xZcoczS+5wZskdziy5w5n1vRsIhXX2vARmdq+ke51zi95z+79K+i/nXHGyBUU9y3jiAwAA8lDCuv5wjnae1wgAAMCr1l2N+f3YXE82fHQ0f1Dt4INr/pC3PyedPnzbOTdB0nYzGy5JZvZhM/uWpK2pHEgCAAAAAAAAwHsle/XtGyWdI6nIzB7T7ueVfErSdWY21Dn3w+xHBAAAAAAAAFBIkj2n5EWShkg6RNI2SeXOubfM7DZJlZI4lAQAAAAAAEBecO2+EyBVyV59u9U51+ac+4ukDc65tyTJOfeOJO5mAAAAAAAAAGlLdii5y8x67fn1SXtvNLMjxKEkAAAAAAAAgC5I9vDtEc65v0mSc++6ALZY0qWpLMjkJX54uSQAALInk1fQ5r/vAAAAADLR6aHk3gPJA9z+uqTXs5IIAAAAAAAAQEFLdqUkAAAAAAAAEIR2l8ljepBLyZ5TEgAAAAAAAAC6FYeSAAAAAAAAAHLK26Hk+nXLVf3846pauVjLn12Y9vzos0dq7ZqnVV+7TNdec0XBz/rcTe5wcsfYOZP56dOmaGvDC6qpXpL2zkz2Zjrrc3eMuWPsnOn8Vd/8mmpqnlB19RLdc8+dOuSQQ3Kyl/sqnNwxdva5m85x5I6xs8/ddI4jd6idgVCYy+CVN1NR3LPsgAvWr1uuT5x6jt54Y8dBZw+WLJFIqG7tUo0592I1NDRp+bMLNWHi5aqrW580T4iz5CY3nbMzf+YZp6i5eadmzJiqIUNHpbSvO/ZyX4WTO8bOqc4f7Jl6SktL9NSTD+pjJ35Sf/3rXzV79l1a9OgT+u098/Z9Tr79993n7hhzx9g51Nwxdg41d4ydQ80dY+dQc4fQuXVXI0+eeADr/mlMdg+6AvOhukV5+3MS5MO3Tx4+VBs2bNLGjZvV0tKiefMW6Pyxowt2ltzkzvZsrLmXLqvU9h1vpryru/ZyX4WTO8bO3TFfVFSkww47VD169FCvww7T1qZtWd/LfRVO7hg7h5o7xs6h5o6xc6i5Y+wcau5QO0Nyznjr8JbPOj2UNLNeZnatmV1jZoea2ZfN7H/M7Mdm1ieTxc45PbpwjiqXP6p/vexLac2WlpVoS8PWfe83NDaptLSkYGd97iZ3bnfTObe5MxFqZ3LTOdvzW7du009/epde3rBCWzZX66233tLjjz+d9b3cV7ndTec4csfY2eduOseRO8bOPnfH2BkISbIrJWdKOlbScZIekTRM0u3a/ait/zrYkJlNMrMqM6tqb995wM85a+TndPIpY3Te2An6xje+rDPOOCXl0Gb7n/Sm+jD0EGd97iZ3bnfTOb3Z7pjvqlA7kzt3sz53+8x95JFHaOzY0fqHD31C7x/0cfXq3Utf/OIFWd/LfZXb3XROb9bnbjqnN+tzN53Tm/W5m87pzfrcHWNnICTJDiU/5Jz7d0lXSPqIpMnOuaclXSvpxIMNOeemOeeGOeeGJRK9D/g5TU2vSJJee+0NzV/wqIYPH5Jy6MaGJg0sL933fnnZgH1frxBnfe4md2530zm3uTMRamdy0znb86NGnalNmzbr9de3q7W1VfPnP6pTPzEs63u5r3K7m85x5I6xs8/ddI4jd4ydfe6OsTMQkpSeU9LtPpJfuOefe9/v8jF9r16HqU+f3vt+/ZlPn6W1a19MeX5lVY0GDz5OFRUDVVxcrPHjx+mhhxcX7Cy5yZ3t2VhzZyLUzuSmc7bnt2xu1MmnfFyHHXaoJOlTnzxD9fWpPSF8qJ3JTed83k3nOHLH2DnU3DF2DjV3qJ2BkBQl+XiVmfVxzjU7576690Yz+6Ckt7u69Nhjj9ED9/9aktSjqIfuu2++Fi9+KuX5trY2XXX19Vr4yGz1SCQ0c9Zc1dauK9hZcpM727Ox5r73njt11ohT1a9fX216uUo33Xy7Zsy8L+t7ua/CyR1j50znV6ys1u9//4hWrPiDWltb9ULNWk3/1e+yvpf7KpzcMXYONXeMnUPNHWPnUHPH2DnU3KF2BkJiyZ6XwMxO1u6LI1ea2YcljZH0ojpcOdmZ4p5lXb6ikmdMAAAgP2XyOn789x0AACBzrbsa8/ullT2p/9C5/HGzg+PXLczbn5NOr5Q0sxslnSOpyMwek3SKpKckfUfSEEk/zHZAAAAAAAAAAIUl2cO3L9Luw8dDJG2TVO6ce8vMbpNUKQ4lAQAAAAAAAKQp2QvdtDrn2pxzf5G0wTn3liQ5596R1J71dAAAAAAAAAAKTrJDyV1m1mvPr0/ae6OZHSEOJQEAAAAAAAB0QbKHb49wzv1NkpxzHQ8hiyVdmsoCnl0UAIDCw3/fAQAAkI+SvyQz8kWnh5J7DyQPcPvrkl7PSiIAAAAAAAAABS3Zw7cBAAAAAAAAoFtxKAkAAAAAAAAgpziUBAAAAAAAAJBT3g4lp0+boq0NL6imekmX5kefPVJr1zyt+tpluvaaKwp+1uducoeTO8bOPnfTOY7cMXb2uZvOceSOsbPP3XSOI3eMnX3upnMcuUPtHDvXbrx1eMtn5rL8skRFPcsOuODMM05Rc/NOzZgxVUOGjkrrayYSCdWtXaox516shoYmLX92oSZMvFx1desLcpbc5KZz/u2mcxy5Y+wcau4YO4eaO8bOoeaOsXOouWPsHGruGDuHmjuEzq27GvP7xMmT2g9+ltff7uDDGx7J258Tb1dKLl1Wqe073uzS7MnDh2rDhk3auHGzWlpaNG/eAp0/dnTBzpKb3NmeJXc4s+QOZ5bc4cySO5xZcoczS+5wZskdziy5w5n1vRsIRZDPKVlaVqItDVv3vd/Q2KTS0pKCnfW5m9y53U3nOHLH2NnnbjrHkTvGzj530zmO3DF29rmbznHkjrGzz90xdgZCkvahpJmty0aQNDPsd1uqD0MPcdbnbnLndjed05v1uZvO6c363E3n9GZ97qZzerM+d9M5vVmfu+mc3qzP3XROb9bnbjqnN+tzd4ydgZAUdfZBM3tb0t6f/L3/VvTae7tz7n0HmZskaZIkWY8jlEj07qa4uzU2NGlgeem+98vLBqip6ZWCnfW5m9y53U3nOHLH2NnnbjrHkTvGzj530zmO3DF29rmbznHkjrGzz90xdgZCkuxKyZmS5kv6B+fc4c65wyVt3vPrAx5ISpJzbppzbphzblh3H0hK0sqqGg0efJwqKgaquLhY48eP00MPLy7YWXKTO9uz5A5nltzhzJI7nFlyhzNL7nBmyR3OLLnDmSV3OLO+d8eu3RlvHd7yWadXSjrnJpvZSZLmmNl8Sb/Q36+czMi999yps0acqn79+mrTy1W66ebbNWPmfSnNtrW16aqrr9fCR2arRyKhmbPmqrZ2XcHOkpvc2Z4ldziz5A5nltzhzJI7nFlyhzNL7nBmyR3OLLnDmfW9GwiFpfK8BGaWkHSlpM9L+qBzrjTVBUU9y3jiAwAAAAAAgG7Uuqsxvy+D82TNB87jHKqDj778cN7+nHR6paQkmdnJ2v38kT8zs2pJnzSzc51zC7MfDwAAAAAAAEChSfZCNzdKOkdSkZk9JulkSX+UdJ2ZDXXO/TAHGQEAAAAAAAAUkGRXSl4kaYikQyRtk1TunHvLzG6TVCmJQ0kAAAAAAADkBZfnL+6Cv0v26tutzrk259xfJG1wzr0lSc65dyS1Zz0dAAAAAAAAgIKT7FByl5n12vPrk/beaGZHiENJAAAAAAAAAF2Q7OHbI5xzf5Mk51zHQ8hiSZdmLRUAAMBBJKzrD8lpd7wYIwAAAJAPOj2U3HsgeYDbX5f0elYSAQAAAAAAAChoya6UBAAAAAAAAILAA2PCkew5JQEAAAAAAACgW3EoCQAAAAAAACCnvBxKlpeX6vHF92v1qqf0Qs0TmnzlZWl/jdFnj9TaNU+rvnaZrr3mioKf9bmb3OHkjrGzz910jiN3jJ197k53dtrdt6thS42qn398320XXvBZ1VQv0V/f2ayPf/xjeZm7u2Z97qZzHLlj7OxzN53jyB1jZ5+7Y+wMhMJclh9sX9SzbL8FJSX9NaCkv6pr1qhPn95aUblIF170VdXVrU/payYSCdWtXaox516shoYmLX92oSZMvDyl+RBnyU1uOuffbjrHkTvGziHk7vjq22eccYqam3dqxm/u0NCPf1qSdPzxg9Xe3q47f/Ejfee6/9Dzz6/a9/kHe/XtfO+cb7vpHEfuGDuHmjvGzqHmjrFzqLlD6Ny6q9EO8iWitqpiLM8q2cHHNj2Utz8nXq6U3LbtVVXXrJEkNTfvVH39epWVlqQ8f/LwodqwYZM2btyslpYWzZu3QOePHV2ws+Qmd7ZnyR3OLLnDmSV3bmaXLavUjh1vvuu2+vqXtG7dyynt9JW7O2ZDzR1j51Bzx9g51Nwxdg41d4ydQ80damcgJN6fU3LQoHINOfGjqlxRnfJMaVmJtjRs3fd+Q2OTSlM81Axx1uducud2N53jyB1jZ5+76RxP7kyE2jnE3DF29rmbznHkjrGzz910jiN3qJ0htTvjrcNbPuv0UNLMPtbh18Vmdr2Z/Y+Z3WJmvTJd3rt3L82bO13f+vaNevvt5pTnzPb/pqb6MPQQZ33uJndud9M5vVmfu+mc3qzP3XROb9bn7kxzZyLUziHmjrGzz910Tm/W5246pzfrczed05v1uTvGzkBIkl0pObPDr2+VNFjSFEmHSbrrYENmNsnMqsysqr195wE/p6ioSPfPna45cx7U/PmPphW6saFJA8tL971fXjZATU2vFOysz93kzu1uOseRO8bOPnfTOZ7cmQi1c4i5Y+zsczed48gdY2efu+kcR+5QOwMhSXYo2fF4fpSkrznn/ijpW5KGHGzIOTfNOTfMOTcskeh9wM+ZPm2K6upf0h1Tp6WbWSurajR48HGqqBio4uJijR8/Tg89vLhgZ8lN7mzPkjucWXKHM0vu3OfORKidQ8wdY+dQc8fYOdTcMXYONXeMnUPNHWpnICRFST5+hJldoN2Hk4c451okyTnnzKzL1w6fftpwTZxwkVatrlXVyt3/Yt1ww616dNETKc23tbXpqquv18JHZqtHIqGZs+aqtnZdwc6Sm9zZniV3OLPkDmeW3LmZvee3v9CIEaeqX7++ennDSt38H1O0Y/ub+ulP/0PHHNNXC+bP0gur1uq88ybkVe7umA01d4ydQ80dY+dQc8fYOdTcMXYONXeonYGQWGfPS2BmM95z03XOuVfMrETS75xzo5ItKOpZxhMfAACAbpOwrj9hdzvPxwQAAApE667G/H4VE0+q3z+OP/B1MHTzgrz9Oen0Sknn3FfM7BRJ7c65lWb2YTP7kqT6VA4kAQAAAAAAAOC9Oj2UNLMbJZ0jqcjMHpN0sqQ/SrrOzIY6536Yg4wAAAAAAAAACkiy55S8SLtf0OYQSdsklbv/z969x0dZ3nkf//6GBBBUFFFDEip22XZf9dmu1IDVeqDiAmJB21W2Vqx23fLseqhuu7q2sPXR7bbdVVbtrl2LB7BQ5OCqCESLopxUDlFiNQkeEAoTAtYqUlBLDtfzB4GNEjIzSWauueb6vF+vvCQTfvl9v7mnSG/vmdu5XWZ2m6Q1kjgpCQAAAAAAACAjqe6+3eSca3bOfSBpo3NulyQ55z6U1JL1dAAAAAAAAAAKTqorJfeaWZ/Wk5Kn7H/QzPqJk5IAAMADblYDAACAQ+GviuFIdVLyLOfcHyXJOdf2JGSxpMuzlgoAAAAAAABAwUp19+0/HuLxdyS9k5VEAAAAAAAAAApaqveUBAAAAAAAAIBuxUlJAAAAAAAAADmV6j0lAQAAAAAAgCC0OPMdAWnydqXkvdOmalvyZVWvX9qp+dGjRqjm1RXaULtKN95wdcHP+txN7nByx9jZ5246x5E7xs4+d9M5s9kY/z7lc3eMuWPs7HM3nePIHWNnn7tj7AyEwlyW75Ve1LOs3QVnnnGqdu/eo+nT79LJQ0dm9D0TiYTqalZqzNhLlEw2aPULlZp42VWqq3ujIGfJTW46599uOseRO8bOoeaOsbMU39+nyB3OLLnDmSV3OLPkDmc2V7ub9tZzSWA7qsovzO6JrsBUJB/L2+eJtyslV65ao3ff29mp2eHDhmrjxs3atGmLGhsbNW/eAo0fN7pgZ8lN7mzPkjucWXKHM0vucGZDzh3b36fIHc4sucOZJXc4s+QOZ9b3biAUHZ6UNLNrzGxA66+HmNkKM9tpZmvM7M9zE/FgpWUl2prcduDzZH2DSktLCnbW525y53Y3nePIHWNnn7vpHEfuGDt3VaidyR3GrM/dMeaOsbPP3XSOI3eonYGQpLpS8u+dc++0/vouSXc4546S9E+S7jnUkJlNMrMqM6tqadnTTVE/9v0Peizdl6GHOOtzN7lzu5vOmc363E3nzGZ97qZzZrPcbKrwAAAgAElEQVQ+d9M5s9muCrUzucOY9bk7xtwxdva5m86ZzfrcHWNnICSp7r7d9uvHOecelSTn3DIzO+JQQ865aZKmSYd+T8muqE82aFB56YHPy8sGqqFhR8HO+txN7tzupnMcuWPs7HM3nePIHWPnrgq1M7nDmPW5O8bcMXb2uZvOceQOtTMkx923g5HqSsmHzWyGmX1a0qNmdr2ZfcrMviVpSw7ytWtdVbWGDDlRgwcPUnFxsSZMuEALFy0p2Flykzvbs+QOZ5bc4cySO5zZkHN3RaidyR3GLLnDmSV3OLPkDmfW924gFB1eKemcm2xmV0h6SNKfSOolaZKkxyRd2pXFs2berbPPOk0DBvTX5reqdMutt2v6jDlpzTY3N+u666eocvFs9UgkNOPBuaqtfb1gZ8lN7mzPkjucWXKHM0vucGZDzh3b36fIHc4sucOZJXc4s+QOZ9b3biAUlup9CcxsuCTnnFtnZidJGiOpzjlXmc6CbLx8GwAAAAAAIGZNe+t5nXI71pV9lfNQbQyrfzRvnycdXilpZjdLOk9SkZk9JWm4pOWSbjKzoc65f81BRgAAAAAAAAAFJNWNbi6SdLL2vWx7u6Ry59wuM7tN0hpJnJQEAAAAAABAXmjhRjfBSHWjmybnXLNz7gNJG51zuyTJOfehpJaspwMAAAAAAABQcFKdlNxrZn1af33K/gfNrJ84KQkAAAAAAACgE1K9fPss59wfJck51/YkZLGky9NZkLDOXzbbkuImPAAAAAAAAADC0+FJyf0nJNt5/B1J72QlEQAAAAAAAICClupKSQAAAAAAACAIvOY2HKneUxIAAAAAAAAAuhUnJQEAAAAAAADkVE5PSk77xe1Kbq3W+peePvDY0UcfpcrK2aqpWanKytk66qh+aX2v0aNGqObVFdpQu0o33nB1RjlCnPW5m9zh5I6xs8/ddI4jd4ydfe6mcxy5Y+zsczed48gdY+d7p03VtuTLql6/NKO57tjNsYojt6/OXX1uA6Ewl+U7XPfsVX5gwRlnnKrdu/do+gN3augXzpUk/eTHk/Xuuzt12+1364Z/vFpHH91PP5j8Y0mHvvt2IpFQXc1KjRl7iZLJBq1+oVITL7tKdXVvpMwT4iy5yU3n/NtN5zhyx9g51Nwxdg41d4ydQ80dY+dQc8fYWZLO3P//L6ffpZOHjkxrxnfuWI9ViLl9dk73ud20t97SChOZ1aVf420l2/jitkfy9nmS0yslV61ao/fe2/mxx8aNG6WZs+ZLkmbOmq/x40en/D7Dhw3Vxo2btWnTFjU2NmrevAUaPy71XKiz5CZ3tmfJHc4sucOZJXc4s+QOZ5bc4cySO5zZkHOvXLVG737i/1+mK9TO5A5jtqvzXXluAyHx/p6Sxx03QNu3vy1J2r79bR177DEpZ0rLSrQ1ue3A58n6BpWWlqS1L8RZn7vJndvddI4jd4ydfe6mcxy5Y+zsczed48gdY2efu+mcee6uCLUzucOY7Y55dF6LMz7afOSzDk9KmtkjZjbRzA7PVaB0mB38Q033ZeghzvrcTe7c7qZzZrM+d9M5s1mfu+mc2azP3XTObNbnbjpnNutzN50zm/W5m86ZzXZVqJ3JHcZsd8wDMUh1peSpki6UtMXM5pnZV82sZ6pvamaTzKzKzKpamvd0+HvffvsdlZQcJ0kqKTlOv/vd71OGrk82aFB56YHPy8sGqqFhR8q5UGd97iZ3bnfTOY7cMXb2uZvOceSOsbPP3XSOI3eMnX3upnPmubsi1M7kDmO2O+aBGKQ6Kfm2c+4iSSdIWijp25LqzWy6mY061JBzbppzrsI5V5Ho0bfDBQsXPaXLJl4sSbps4sVauHBJytDrqqo1ZMiJGjx4kIqLizVhwgVauCj1XKiz5CZ3tmfJHc4sucOZJXc4s+QOZ5bc4cySO5zZkHN3RaidyR3GbHfMAzEoSvF1J0nOuT9Imilpppn1lzRB0k2SMvpf1Mxf/pfOOus0DRjQX29tXKdb/2WqbrvtvzR79j264ltf19at9brkkr9L+X2am5t13fVTVLl4tnokEprx4FzV1r6eVoYQZ8lN7mzPkjucWXKHM0vucGbJHc4sucOZJXc4syHnnjXzbp3d+v8vN79VpVtuvV3TZ8zJ69yxHqsQc/vs3JXnNhAS6+g9DcxshXPurK4s6NmrvNNvmtDC+y0AAAAAAAAcpGlvfX7fxcST50ou4mRSG1/a/nDePk86vFLSOXeWmQ3f90u3zsw+J2mMpA3OucqcJAQAAAAAAABQUDo8KWlmN0s6T1KRmT2lfTe+WSbpJjMb6pz71+xHBAAAAAAAAFBIUr2n5EWSTpbUS9J2SeXOuV1mdpukNZI4KQkAAAAAAAAgI6nuvt3knGt2zn0gaaNzbpckOec+lNSS9XQAAAAAAAAACk6qKyX3mlmf1pOSp+x/0Mz6iZOSAAAAAAAAyCOcrApHqpOSZznn/ihJzrm2x7VY0uXpLOAO2gAAAAAAAADaSnX37T8e4vF3JL2TlUQAAAAAAAAAClqq95QEAAAAAAAAgG7FSUkAAAAAAAAAOcVJSQAAAAAAAAA55e2k5OhRI1Tz6gptqF2lG2+4OqfzIc763E3ucHLH2NnnbjrHkTvGzj53x9a5V69eeuG5RXqx6im9XP2Mbv7h9zKNHeTPO8Rj1dVZn7vpHEfuGDv73E3nOHKH2jl2TsZHm498Zi7Ld8cu6ll20IJEIqG6mpUaM/YSJZMNWv1CpSZedpXq6t5I63t2ZT7EWXKTm875t5vOceSOsXOouUPtLEl9+/bRnj0fqKioSCuWPap/+O7NWrP2pbzOHeOxijF3jJ1DzR1j51Bzx9g51NwhdG7aW5/fZ5w8WVFycXZPdAXmrO3z8/Z54uVKyeHDhmrjxs3atGmLGhsbNW/eAo0fNzon8yHOkpvc2Z4ldziz5A5nltzhzPrevWfPB5Kk4uIiFRUXK5P/YBzizzvUYxVj7hg7h5o7xs6h5o6xc6i5Q+0MhKTDk5Jm9mkze8DMfmRmh5vZvWb2qpnNN7PBnV1aWlaircltBz5P1jeotLQkJ/MhzvrcTe7c7qZzHLlj7OxzN53jyB1qZ2nf1RBV65aoof43Wrp0hdauW5/3uWM8VjHmjrGzz910jiN3jJ197o6xMxCSVFdKzpC0TtJuSaslbZB0nqQnJT3Q2aVmB185mslVAV2ZD3HW525y53Y3nTOb9bmbzpnN+txN58xmfe6OsbMktbS0qGLYKJ1wYoWGVQzVSSd9Nu3ZEH/eoR6rGHPH2NnnbjpnNutzN50zm/W5O8bOQEiKUnz9COfcf0uSmV3lnJva+vj9ZnbNoYbMbJKkSZJkPfopkej7sa/XJxs0qLz0wOflZQPV0LAj7dBdmQ9x1uducud2N53jyB1jZ5+76RxH7lA7t/X++7u0fMXz+97Yvua1rO8Ocdbn7hhzx9jZ5246x5E7xs4+d8fYGVIL52+DkepKyRYz+4yZDZfUx8wqJMnMhkjqcagh59w051yFc67ikyckJWldVbWGDDlRgwcPUnFxsSZMuEALFy1JO3RX5kOcJTe5sz1L7nBmyR3OLLnDmfW5e8CA/urX70hJUu/evTXynDP12msb8z53jMcqxtwxdg41d4ydQ80dY+dQc4faGQhJqislb5S0UFKLpAslfd/MPi+pn6Rvd3Zpc3Ozrrt+iioXz1aPREIzHpyr2trXczIf4iy5yZ3tWXKHM0vucGbJHc6sz90DBx6vB+6/Uz16JJRIJPTwwwu1uPLpvM8d47GKMXeMnUPNHWPnUHPH2DnU3KF2BkJiqd6XwMxOldTinFtnZidp33tK1jrnKtNZUNSzjAtnAQAAAAAAulHT3vqD33wSWnb8xZyHamPEjvl5+zzp8EpJM7tZ+05CFpnZU5KGS1ou6SYzG+qc+9ccZAQAAAAAAABQQFK9fPsiSSdL6iVpu6Ry59wuM7tN0hpJnJQEAAAAAABAXmhR3l4YiE9IdaObJudcs3PuA0kbnXO7JMk596H2vc8kAAAAAAAAAGQk1UnJvWbWp/XXp+x/0Mz6iZOSAAAAAAAAADoh1cu3z3LO/VGSnHNtT0IWS7o8a6kAAAAAAAAAFKwOT0ruPyHZzuPvSHonK4kAAAAAAAAAFLRUL98GAAAAAAAAgG6V6uXbAAAAAAAAQBAcd98OBldKAgAAAAAAAMgpTkoCAAAAAAAAyCmvJyUTiYTWrf21Fjz6YMazo0eNUM2rK7ShdpVuvOHqgp/1uZvc4eSOsbPP3XSOI3eMnX3upnNms+XlpXp6yXy98ptlern6GV17zZU5282xiiN3jJ197qZzHLlj7Oxzd4ydgVCYcy6rC4p6lh1ywfXXTdIpp3xeRx5xhC746uVpf89EIqG6mpUaM/YSJZMNWv1CpSZedpXq6t4oyFlyk5vO+bebznHkjrFzqLlj7CxJJSXHaWDJcVpf/aoOP7yv1q55Un910d/kde5Yj1WIuWPsHGruGDuHmjvGzqHmDqFz09563jyxHUuP/+vsnugKzMgdc/P2eeLtSsmysoEae95IPfDAQxnPDh82VBs3btamTVvU2NioefMWaPy40QU7S25yZ3uW3OHMkjucWXKHMxty7u3b39b66lclSbt379GGDW+orLQkr3PHeqxCzB1j51Bzx9g51Nwxdg41d6idIbXw8bGPfNbhSUkzS5jZ35jZYjN72cxeNLM5Zjaiq4v/Y+otuun7P1JLS+Y/otKyEm1NbjvwebK+QaVp/gU8xFmfu8md2910jiN3jJ197qZzHLlj7PxJJ5xQrpP/4v9ozdr1Wd/NsYojd4ydfe6mcxy5Y+zsc3eMnYGQpLpS8n5Jn5L0E0nPSlrc+tgUM7v2UENmNsnMqsysqqVlz0FfP3/suXr77Xf00vpXOhXa7OArT9N9GXqIsz53kzu3u+mc2azP3XTObNbnbjpnNutzN50zm22rb98+mjf3Xn33H2/WH/6wO+u7OVaZzfrcTefMZn3upnNmsz530zmzWZ+7Y+wMhKQoxddPcc59q/XXq8xstXPuh2a2QlK1pP9sb8g5N03SNKn995Q8/fQKjfvKKJ035hz17t1LRx55hB6c8TNdfsV30gpdn2zQoPLSA5+Xlw1UQ8OOgp31uZvcud1N5zhyx9jZ5246x5E7xs77FRUVaf7ce/XQQ4/qsceeSHsu1M7kDmPW5+4Yc8fY2eduOseRO9TOQEhSXSnZaGZ/Iklm9gVJeyXJOfdHSZ0+TT95yk81+NMVGvKZL+rSiVfp2WefS/uEpCStq6rWkCEnavDgQSouLtaECRdo4aIlBTtLbnJne5bc4cySO5xZcoczG3JuSbp32lTVbXhTd941LaO5UDuTO4xZcoczS+5wZskdzqzv3UAoUl0peYOkZ83sI0nFkr4uSWZ2rKRFWc52SM3Nzbru+imqXDxbPRIJzXhwrmprXy/YWXKTO9uz5A5nltzhzJI7nNmQc3/p9GG6bOJF+s0rtapat+//rPzzP/9UTzz5TN7mjvVYhZg7xs6h5o6xc6i5Y+wcau5QO0NyytubTeMTLNX7EpjZaZKanHPrzOxzksZI2uCcq0xnQXsv3wYAAAAAAEDnNe2t5+xbO5Yc/3XOQ7UxasecvH2edHilpJndLOk8SUVm9pSk4ZKWS7rJzIY65/41BxkBAAAAAAAAFJBUL9++SNLJknpJ2i6p3Dm3y8xuk7RGEiclAQAAAAAAAGQk1Y1umpxzzc65DyRtdM7tkiTn3IeSWrKeDgAAAAAAAEDBSXVScq+Z9Wn99Sn7HzSzfuKkJAAAAAAAAIBOSPXy7bOcc3+UJOdc25OQxZIuz1oqAAAAAAAAIENcQReODk9K7j8h2c7j70h6JyuJAAAAAAAAABS0VC/fBgAAAAAAAIBuxUlJAAAAAAAAADnFSUkAAAAAAAAAOeX1pGQikdC6tb/WgkcfzHh29KgRqnl1hTbUrtKNN1xd8LM+d5M7nNwxdva5m85x5I6xs8/ddM5d7nunTdW25MuqXr80451d2dvVWZ+7Y8wdY2efu+kcR+4YO/vcHWPn2LXw8bGPfGbOuawuKOpZdsgF1183Saec8nkdecQRuuCr6d/MO5FIqK5mpcaMvUTJZINWv1CpiZddpbq6NwpyltzkpnP+7aZzHLlj7Bxq7hg7d3X+zDNO1e7dezR9+l06eejItPZ1x16OVTi5Y+wcau4YO4eaO8bOoeYOoXPT3npLK0xkKo//enZPdAVm7I45efs88XalZFnZQI09b6QeeOChjGeHDxuqjRs3a9OmLWpsbNS8eQs0ftzogp0lN7mzPUvucGbJHc4sucOZjTX3ylVr9O57O9Pe1V17OVbh5I6xc6i5Y+wcau4YO4eaO9TOQEi8nZT8j6m36Kbv/0gtLZlfTFpaVqKtyW0HPk/WN6i0tKRgZ33uJndud9M5jtwxdva5m85x5I6xc3fMd1aonclN53zeTec4csfY2efuGDsDIenwpKSZFZnZ/zWzJ83sN2b2spk9YWZ/Z2bFnV16/thz9fbb7+il9a90at7s4CtP030ZeoizPneTO7e76ZzZrM/ddM5s1uduOmc263M3nTOb7Y75zgq1M7lzN+tzd4y5Y+zsczedM5v1uTvGzkBIilJ8faaknZL+n6Rk62Plki6XNEvSX7c3ZGaTJE2SJOvRT4lE3499/fTTKzTuK6N03phz1Lt3Lx155BF6cMbPdPkV30krdH2yQYPKSw98Xl42UA0NOwp21uducud2N53jyB1jZ5+76RxH7hg7d8d8Z4Xamdx0zufddI4jd4ydfe6OsTMkp7x9C0V8QqqXb3/BOff3zrnVzrlk68dq59zfSxp6qCHn3DTnXIVzruKTJyQlafKUn2rwpys05DNf1KUTr9Kzzz6X9glJSVpXVa0hQ07U4MGDVFxcrAkTLtDCRUsKdpbc5M72LLnDmSV3OLPkDmc21txdEWpnctM5n3fTOY7cMXYONXeonYGQpLpS8j0zu1jS/zjnWiTJzBKSLpb0XrbDHUpzc7Ouu36KKhfPVo9EQjMenKva2tcLdpbc5M72LLnDmSV3OLPkDmc21tyzZt6ts886TQMG9Nfmt6p0y623a/qMOVnfy7EKJ3eMnUPNHWPnUHPH2DnU3KF2BkJiHb0vgZkNlvRvks7RvpOQJqmfpGcl3eSc25RqQVHPMt74AAAAAAAAoBs17a3ndcrtWHz8JZyHauP8HQ/l7fOkwyslnXOb1fq+kWZ2jPadlLzTOTcx+9EAAAAAAAAAFKIOT0qa2ePtPHzO/sedc+OzkgoAAAAAAADIUEveXheIT0r1npLlkmol3SfJad+VksMkTc1yLgAAAAAAAAAFKtXdtyskvShpsqT3nXPLJH3onFvunFue7XAAAAAAAAAACk+q95RskXSHmc1v/eeOVDMAAAAAAAAA0JG0TjA655KSLjaz8yXtym4kAAAAAAAAAIUso6senXOLJS3OUhYAAAAAAAAAEeCl2AAAAAAAACgILeL226FIdaMbAAAAAAAAAOhWnJQEAAAAAAAAkFPeTkqOHjVCNa+u0IbaVbrxhqtzOh/irM/d5A4nd4ydfe6OsfO906ZqW/JlVa9fmtFcd+wOcdbn7hhzx9jZ9+5EIqF1a3+tBY8+mNO9sR2rUP/s9bk7xtwxdva5m85x5A61MxAKc85ldUFRz7KDFiQSCdXVrNSYsZcomWzQ6hcqNfGyq1RX90Za37Mr8yHOkpvcdM6/3TF2lqQzzzhVu3fv0fTpd+nkoSPTmvGdO8ZjFWPuGDv73i1J1183Saec8nkdecQRuuCrl2c9c1fnQz1WIf7Z63N3jLlj7Bxq7hg7h5o7hM5Ne+t588R2LCj5RnZPdAXmgu2z8/Z54uVKyeHDhmrjxs3atGmLGhsbNW/eAo0fNzon8yHOkpvc2Z4ldzizvnevXLVG7763M+3fnw+5YzxWMeaOsbPv3WVlAzX2vJF64IGH0p7pjr0xHqsQ/+z1uTvG3DF2DjV3jJ1DzR1qZ0iOj4995LNOn5Q0s2mdnS0tK9HW5LYDnyfrG1RaWpKT+RBnfe4md2530zmO3KF27qoQf96hHqsYc8fY2ffu/5h6i276/o/U0tKS9kx37I3xWHVFqJ3JTed83k3nOHKH2hkISYcnJc2s/yE+jpE0toO5SWZWZWZVLS172vv6QY9l8jLyrsyHOOtzN7lzu5vOmc363B1j564K8ecd6rGKMXeMnX3uPn/suXr77Xf00vpX0vr93bW3q/OhHquuCLUzuXM363N3jLlj7Oxzd4ydgZAUpfj67yT9VlLb/0W41s+PO9SQc26apGlS++8pWZ9s0KDy0gOfl5cNVEPDjrRDd2U+xFmfu8md2910jiN3qJ27KsSfd6jHKsbcMXb2ufv00ys07iujdN6Yc9S7dy8deeQRenDGz3T5Fd/J6t6uzod6rLoi1M7kpnM+76ZzHLlD7QyEJNXLt9+SNMI5d2Kbj087506U1On/RayrqtaQISdq8OBBKi4u1oQJF2jhoiU5mQ9xltzkzvYsucOZ9b27K0L8eYd6rGLMHWNnn7snT/mpBn+6QkM+80VdOvEqPfvsc2mdkOzq3q7Oh3qsuiLUzuSmcz7vpnMcuUPtDIQk1ZWSd0o6WtKWdr72751d2tzcrOuun6LKxbPVI5HQjAfnqrb29ZzMhzhLbnJne5bc4cz63j1r5t06+6zTNGBAf21+q0q33Hq7ps+Yk9e5YzxWMeaOsbPv3Z0VamefuUP8s9fn7hhzx9g51Nwxdg41d6idIWX2btfwyTJ9XwIz+6Vz7pvp/v72Xr4NAAAAAACAzmvaW3/wm09Cj5R8g/NQbXxt++y8fZ50eKWkmT3+yYckfdnMjpIk59z4bAUDAAAAAAAAUJhSvXx7kKQaSffpf29wUyFpapZzAQAAAAAAAChQqW50c4qkFyVNlvS+c26ZpA+dc8udc8uzHQ4AAAAAAABA4enwSknnXIukO8xsfus/d6SaAQAAAAAAAICOpHWC0TmXlHSxmZ0vaVcmC3oX9exMLknSR017Oz0LAEAMEtb5961uyfBmd0Cmjurdt9OzOz/a041JAABALFq68Pdj5FZGVz065xZLWpylLAAAAAAAAAAikOo9JQEAAAAAAACgW3FSEgAAAAAAAEBOcVISAAAAAAAAQE7l7KRkr149tWzFY3phdaXWVf1ak6dcL0m6/4E79FL1Uq1d96R+fs+/qagovbe5HD1qhGpeXaENtat04w1XZ5QlxFmfu8kdTu4QO/fq1UsvPLdIL1Y9pZern9HNP/xeprGD/HmHeKy6Outzd4ydr7nmSq1/6WlVr1+qa6+9MqPZru4Ocdbn7lhyv/TKM1rxwkI9u2qBnl72P5Kk8ReO0ao1i/X2zg06eej/SWvvvdOmalvyZVWvX5pR3s7m7q5Zn7vpHEfuGDv73E3nOHKH2jl2jo+PfeQzc1m+8+bhfU48sKBv3z7as+cDFRUV6aml83XjP96io/sfpSW/XiZJmj7jLj333Frdd++vJB367tuJREJ1NSs1ZuwlSiYbtPqFSk287CrV1b2RMk+Is+QmdyF3lj7+Z8OKZY/qH757s9asfSmvc8d4rGLMHULn9u6+fdLnPqtZs+7W6V/6ivbubdSiRbN07bU/0JtvbvrY7zvU3bdD/HmHcKxizN327tsvvfKMzj37r/Tuu+8deOxPP/Mnci0tmnrXrbp5yr+pev2rB752qLtvn3nGqdq9e4+mT79LJw8dmTJrrjvn2246x5E7xs6h5o6xc6i5Q+jctLee20y3Y/7AS/P9XFxOXdzwq7x9nuT05dt79nwgSSouLlJxcZGcdOCEpCRVVb2ssrKBKb/P8GFDtXHjZm3atEWNjY2aN2+Bxo8bnVaGEGfJTe5sz/re3fbPhqLiYmXyH0tC/HmHeqxizB1q5z/7syFas2a9PvzwIzU3N2vlitW64IIxeZ87xmMVa+793nh940Eny1NZuWqN3n1vZ8a7JI4VnQs3d4ydQ80dY+dQc4faGQhJTk9KJhIJPb96sTb9tkrPLF2lqnXVB75WVFSkS77xVT21ZHnK71NaVqKtyW0HPk/WN6i0tCStDCHO+txN7tzujrGztO/Phqp1S9RQ/xstXbpCa9etz/vcMR6rGHOH2rmm9jWdeeap6t//KB12WG+NGXOOystL8z53jMcqptzOOT382ANauvwRffOKv05rT3fjWNE5n3fTOY7cMXb2uTvGzkBIOnwDRzPrIelvJZVLetI591ybr01xzv0ok2UtLS06/Yvnq1+/I/TQnF/oc5/7jGprX5ck3XHXv+i5VWv1/PPrUn4fa+elauleWRXirM/d5M7t7hg7S/v+bKgYNkr9+h2p/5l/v0466bOqqXkt67tDnPW5O8bcoXbesOFN3Xb7z/VE5UPavXuPfvNKrZqamtKa7eruEGd97o4p9/mjLtH27W9rwID+enjBDL3x+ka98HxVWvu6C8cqd7M+d8eYO8bOPnfTObNZn7tj7AyEJNWVkr+QdLak30v6mZn9R5uvfe1QQ2Y2ycyqzKyqsekPB339/ff/oJUrV+vcvzxbkvT9H3xHAwb0103/lN45zvpkgwa1ueKjvGygGhp2FOysz93kzu3uGDu39f77u7R8xfMaPWpE2jMh/rxDPVYx5g61syTNmDFHp37xPI089yK99+7OjF4iG+LPO9RjFVPu7dvfliS98867qlz0lL5wyufT2tWdOFZ0zufddI4jd4ydfe6OsTOkFj4+9pHPUp2UHO6c+4Zz7k5Jp0o63MweMbNekg75RpnOuWnOuQrnXEVx0RGSpAED+qtfv32/7t27l7785TP0+usbdfkVf62R556lb13+nbTP/K+rqtaQISdq8OBBKi4u1oQJF2jhoiUFO0tucmd71ufufX82HClJ6t27t0aec6Zee+4HO8wAACAASURBVG1j3ueO8VjFmDvUzpJ07LHHSJIGDSrVhReep7lzF+R97hiPVSy5+/Q5TIcf3vfAr0ec86W0bxTQnThWdM7n3XSOI3eMnUPNHWpnICQdvnxbUs/9v3DONUmaZGY3S3pG0uGZLDq+5DhNu/d29Uj0UCJheuSRxXryiWe0c9cb2rKlXs8se0SS9PiCJ/XTn/xnh9+rublZ110/RZWLZ6tHIqEZD8498DLwVEKcJTe5sz3rc/fAgcfrgfvvVI8eCSUSCT388EItrnw673PHeKxizB1qZ0maO2eajjnmaDU2Nuk7103Wzp3v533uGI9VLLmPPW6AHvzV3ZKkoqIe+p/5C/XM0ys19it/qZ/e9s86ZkB/zZ4/Ta++UqcJX72yw92zZt6ts886TQMG9Nfmt6p0y623a/qMOXnXOV920zmO3DF2DjV3jJ1DzR1qZyAk1tHViWY2S9Is59yTn3j8byX9t3OuONWCw/uc2Ok3PvioaW9nRwEAiELCDvnChZRaeG8iZNlRvft2enbnR3u6MQkAAIWnaW995/8iWMDmDryUv+S28dcNv8rb50mHL992zk1s54TkL51z96VzQhIAAAAAAAAAPinV3bcf/+RDkr5sZkdJknNufLaCAQAAAAAAAChMqd5TcpCkGkn3SXLad1KyQtLULOcCAAAAAAAAMtKSty9Wxieluvv2KZJelDRZ0vvOuWWSPnTOLXfOLc92OAAAAAAAAACFp8MrJZ1zLZLuMLP5rf/ckWoGAAAAAAAAADqS1glG51xS0sVmdr6kXZks+CN30AYAIGu4gzbyWVfuoF3co/P/HbyxuanTswAAAMiNjP6255xbLGlxlrIAAAAAAAAAiAAvxQYAAAAAAEBBaBF3uglFqhvdAAAAAAAAAEC34qQkAAAAAAAAgJzydlLyjddXa/1LT6tq3RKtfqEy4/nRo0ao5tUV2lC7SjfecHXBz/rcTe5wcsfY2eduOseRO9TO906bqm3Jl1W9fmlGc92xO8RZn7tjzJ3p87O8fKCefHKO1q9fqhdffEpXX/0tSdKPf/wDVVcv1dq1T2ru3F+oX78js5o7xmMVY2efu+kcR+4YO/vcHWNnIBTmsnzXzuKeZe0ueOP11friaefp979/75Czh0qWSCRUV7NSY8ZeomSyQatfqNTEy65SXd0bKfOEOEtuctM5/3bTOY7coXaWpDPPOFW7d+/R9Ol36eShI9Oa8Z07xmMVa+50np9t775dUnKcSkqOU3X1qzr88L56/vlFmjBhksrKSrRs2fNqbm7Wj350kyRpypSfHvLu2xwrOhdq7hg7h5o7xs6h5g6hc9Peet48sR2/Kp2Y3RNdgbl026y8fZ4E+fLt4cOGauPGzdq0aYsaGxs1b94CjR83umBnyU3ubM+SO5xZcocz63v3ylVr9O57O9P+/fmQO8ZjFWvuTJ+f27e/rerqVyVJu3fv0YYNb6q09HgtXbpSzc3NkqS1a9errGxg1nLHeKxi7Bxq7hg7h5o7xs6h5g61M/Zd4MbH/37kM28nJZ1zeqLyIa1Z/YT+9spLM5otLSvR1uS2A58n6xtUWlpSsLM+d5M7t7vpHEfuGDv73B1j564K8ecd6rGKNXdXfOpT5Tr55JO0bl31xx7/5jcn6Ne/XtbhLMeKzvm8m85x5I6xs8/dMXYGQlLU0RfNrI+ka7Tv5Op/Svq6pK9J2iDpVufc7s4uPnvEhWpo2KFjjz1GTz4xRxtee1OrVq1Ja9bs4CtP030ZeoizPneTO7e76ZzZrM/ddM5s1ufuGDt3VYg/71CPVay5O6tv3z566KF7dMMNt+oPf/jfv4beeOM1am5u0pw5j3Y4z7HK3azP3THmjrGzz910zmzW5+4YOwMhSXWl5AxJx0s6UdJiSRWSbpdkkv77UENmNsnMqsysqqVlT7u/p6FhhyTpd7/7vR5b8ISGDTs57dD1yQYNKi898Hl52cAD368QZ33uJndud9M5jtwxdva5O8bOXRXizzvUYxVr7s4oKirSQw/do7lzH9OCBU8eePzSS/9KY8eO1BVXXJfye3Cs6JzPu+kcR+4YO/vcHWNnICSpTkp+xjn3PUlXSzpJ0rXOuRWSbpT0F4cacs5Nc85VOOcqEom+B329T5/DdPjhfQ/8+i/PPVs1Na+lHXpdVbWGDDlRgwcPUnFxsSZMuEALFy0p2Flykzvbs+QOZ5bc4cz63t0VIf68Qz1WsebujHvu+Xe99tqb+tnP7jvw2F/+5dn63vf+XhdddKU+/PCjlN+DY0XnfN5N5zhyx9g51NyhdgZC0uHLt/dzzjkzq3St1wu3ft7pa4ePP/5YPTz/fklSj6IemjPnMS1Zsizt+ebmZl13/RRVLp6tHomEZjw4V7W1rxfsLLnJne1ZcoczS+5wZn3vnjXzbp191mkaMKC/Nr9VpVtuvV3TZ8zJ69wxHqtYc2f6/Dz99Apdeulf6ZVX6rR6daUk6eabb9PUqf9PvXr11KJFsyTtu9nNd74zOS87h3isYuwcau4YO4eaO8bOoeYOtTMQEuvofQnM7D5J13/yvSPN7E8kPeicOyPVguKeZZ0+eck7JgAAAMSpuEda/+28XY3NTd2YBACA/NS0t/7gN5+Eflk2kdNJbXyzflbePk86fPm2c+5v2zkh+Uvn3EZJZ2Y1GQAAAAAAAICClOru249/8iFJXzazo1o/H5+VVAAAAAAAAAAKVqrXxQySVCPpPu17NbVp3x24p2Y5FwAAAAAAAIACleru26dIelHSZEnvO+eWSfrQObfcObc82+EAAAAAAAAAFJ4Or5R0zrVIusPM5rf+c0eqmYO+RxfCAQAAIE7crAYAAHRGi+8ASFtaJxidc0lJF5vZ+ZJ2ZTcSAAAAAAAAgEKW2VWPzi2WtDhLWQAAAAAAAABEINV7SgIAAAAAAABAt+KkJAAAAAAAAICcyujl2wAAAAAAAEC+4obL4fB2peS906ZqW/JlVa9f2qn50aNGqObVFdpQu0o33nB1wc/63E3ucHLH2NnnbjrHkTvGzj530zmO3DF29rmbznHkjrGzz910jiN3qJ2BUJhz2T2HXNSzrN0FZ55xqnbv3qPp0+/SyUNHZvQ9E4mE6mpWaszYS5RMNmj1C5WaeNlVqqt7oyBnyU1uOuffbjrHkTvGzqHmjrFzqLlj7Bxq7hg7h5o7xs6h5o6xc6i5Q+jctLfe0goTmellE7lYso1v1c/K2+eJtyslV65ao3ff29mp2eHDhmrjxs3atGmLGhsbNW/eAo0fN7pgZ8lN7mzPkjucWXKHM0vucGbJHc4sucOZJXc4s+QOZ5bc4cz63g2EIuOTkmb2ejaCZKK0rERbk9sOfJ6sb1BpaUnBzvrcTe7c7qZzHLlj7OxzN53jyB1jZ5+76RxH7hg7+9xN5zhyx9jZ5+4YOwMh6fBGN2b2B/3ve4Tuv9yzz/7HnXNHHmJukqRJkmQ9+imR6NtNcQ98/4MeS/dl6CHO+txN7tzupnNmsz530zmzWZ+76ZzZrM/ddM5s1uduOmc263M3nTOb9bmbzpnN+txN58xmfe6OsTOklrx9sTI+KdWVkjMkPSbpT51zRzjnjpC0pfXX7Z6QlCTn3DTnXIVzrqK7T0hKUn2yQYPKSw98Xl42UA0NOwp21uducud2N53jyB1jZ5+76RxH7hg7+9xN5zhyx9jZ5246x5E7xs4+d8fYGQhJhyclnXPXSrpL0kNm9h0zSygP7q6+rqpaQ4acqMGDB6m4uFgTJlyghYuWFOwsucmd7VlyhzNL7nBmyR3OLLnDmSV3OLPkDmeW3OHMkjucWd+7gVB0+PJtSXLOvWhm50q6RtJySb27Y/GsmXfr7LNO04AB/bX5rSrdcuvtmj5jTlqzzc3Nuu76KapcPFs9EgnNeHCuamtfL9hZcpM727PkDmeW3OHMkjucWXKHM0vucGbJHc4sucOZJXc4s753A6GwDN8TYaCkV51zx6Q7U9SzzPuVlQAAAAAAAIWkaW89757YjvvLJ3Ieqo0rk7Py9nmS6kY3j7fzcK/9jzvnxmclFQAAAAAAAICClerl2+WSaiXdp33vJWmShkmamuVcAAAAAAAAQEZafAdA2lLdfbtC0ouSJkt63zm3TNKHzrnlzrnl2Q4HAAAAAAAAoPB0eKWkc65F0h1mNr/1nztSzQAAAAAAAABAR9I6weicS0q62MzOl7Qru5EAAAAAf7rybvC8sz4AAEB6Mrrq0Tm3WNLiLGUBAAAAAAAAEAFeig0AAAAAAICCwI1uwpHqRjcAAAAAAAAA0K04KQkAAAAAAAAgp7yclOzVq5deeG6RXqx6Si9XP6Obf/i9jL/H6FEjVPPqCm2oXaUbb7i64Gd97iZ3OLm7MnvvtKnalnxZ1euXZjTXHbs5VnF09rmbznHkjrGzz90xdr7uO99WdfUzWr9+qWbOvFu9evXK2e4QZ33ujjF3jJ1D/ftrjMfK5+4YOwOhMOeye4/Aop5l7S7o27eP9uz5QEVFRVqx7FH9w3dv1pq1L6X1PROJhOpqVmrM2EuUTDZo9QuVmnjZVaqre6MgZ8lN7lx0PvOMU7V79x5Nn36XTh46Mq2ZfMgd4s87xs6h5o6xc6i5Y+wcau4QOrd39+3S0hIte/ZRff4vvqyPPvpIs2ffoyefeEa/nDnvY7/vUH+zDvHnHcKxIne8naUw//4a67EKMXcInZv21rf3r6zo/aJ8YnZPdAXm/yZn5e3zxNvLt/fs+UCSVFxcpKLiYmVycnT4sKHauHGzNm3aosbGRs2bt0Djx40u2Flykzvbs5K0ctUavfvezrR/f77kDvHnHWPnUHPH2DnU3DF2DjV3qJ0lqaioSIcd1ls9evRQn8MO07aG7XmfO8ZjFWPuGDtLYf79NdZjFWLuUDtDcsZH2490mFkPM1tvZotaPz/RzNaY2RtmNtfMerY+3qv18zdbvz64K8fK20nJRCKhqnVL1FD/Gy1dukJr161Pe7a0rERbk9sOfJ6sb1BpaUnBzvrcTe7c7vbZuSs4VnTO5910jiN3jJ197o6x87Zt23XHHfforY1rtXXLeu3atUtPP70i73PHeKxizB1j564KtTO5w5j1vRvohOsk1bX5/N8k3eGc+1NJ70m6svXxKyW955wbIumO1t/XaR2elDSzz7f5dbGZTTGzx83sx2bWpyuLW1paVDFslE44sULDKobqpJM+m/as2cGnetO90jLEWZ+7yZ3b3T47dwXHKnezPnfHmDvGzj530zmzWZ+7Y+x81FH9NG7caP3pZ76oT53wBfXp20ff+MbX0prt6u4QZ33ujjF3jJ27KtTO5A5j1vduIBNmVi7pfEn3tX5uks6R9HDrb3lQ0oWtv76g9XO1fn2ktfeETVOqKyVntPn1TyUNkTRV0mGS7jnUkJlNMrMqM6tqadnT4YL339+l5Sue1+hRI9IKLEn1yQYNKi898Hl52UA1NOwo2Fmfu8md290+O3cFx4rO+bybznHkjrGzz90xdh458kxt3rxF77zzrpqamvTYY0/otC9W5H3uGI9VjLlj7NxVoXYmdxizvncDGbpT0o2SWlo/P0bSTudcU+vnSUllrb8uk7RVklq//n7r7++UVCcl257tHCnp28655ZK+K+nkQw0556Y55yqccxWJRN+Dvj5gQH/163ekJKl3794aec6Zeu21jWmHXldVrSFDTtTgwYNUXFysCRMu0MJFSwp2ltzkzvZsV3Gs6JzPu+kcR+4YO4eaO9TOW7fUa/ipX9Bhh/WWJJ3z5TO0YUN6NzvwmTvGYxVj7hg7d1Wonckdxqzv3UBbbS8cbP2Y1OZrX5H0tnPuxbYj7Xwbl8bXMlaU4uv9zOyr2nfyspdzrlGSnHPOzDq9dODA4/XA/XeqR4+EEomEHn54oRZXPp32fHNzs667fooqF89Wj0RCMx6cq9ra1wt2ltzkzvasJM2aebfOPus0DRjQX5vfqtItt96u6TPm5H3uEH/eMXYONXeMnUPNHWPnUHOH2nntuvV65JHFWrv212pqatLL1TW6975f5X3uGI9VjLlj7CyF+ffXWI9ViLlD7Qx8knNumqRph/jylySNN7OxknpLOlL7rpw8ysyKWq+GLJe0/01Ok5IGSUqaWZGkfpLe7Ww26+h9Ccxshj5+xvMm59wOMyuR9Cvn3MhUC4p6lvHGBwAAAAhGp98YSV24VAAAgAw17a3vyr+yCtbPB03kX8dtXLV1VlrPEzMbIekfnXNfMbP5kv7HOTfHzO6R9Bvn3M/N7GpJf+6c+zsz+7qkrznnJnQ2W4dXSjrnrmgn5C+dc9/UvpdzAwAAAAAAACgc/yRpjpn9SNJ6Sfe3Pn6/pJlm9qb2XSH59a4s6fCkpJk93s7D55jZUZLknBvfleUAAAAAAAAA/HLOLZO0rPXXb0ka3s7v+UjSxd21M9V7Sg6SVKN9twV32vdqlmHadwduAAAAAAAAAMhYqrtvnyLpRUmTJb3fetb0Q+fc8ta7cAMAAAAAAABARlK9p2SLpDta3+DyDjPbkWoGAAAAAAAA8KHFdwCkLa0TjM65pKSLzex8SbuyGwkAAADwh1t2AgAAZF9GVz065xZLWpylLAAAAAAAAAAikOo9JQEAAAAAAACgW3FSEgAAAAAAAEBOcdMaAAAAAAAAFATeGzoc3q6UvHfaVG1Lvqzq9Us7NT961AjVvLpCG2pX6cYbri74WZ+7yR1O7hg7+9xN5zhyx9jZ5246x5E7xs4+d9M5jtwxdva5m85x5A61MxAKcy6755CLepa1u+DMM07V7t17NH36XTp56MiMvmcikVBdzUqNGXuJkskGrX6hUhMvu0p1dW8U5Cy5yU3n/NtN5zhyx9g51Nwxdg41d4ydQ80dY+dQc8fYOdTcMXYONXcInZv21ltaYSLzn4MmcrFkG9dunZW3zxNvV0quXLVG7763s1Ozw4cN1caNm7Vp0xY1NjZq3rwFGj9udMHOkpvc2Z4ldziz5A5nltzhzJI7nFlyhzNL7nBmyR3OLLnDmfW9GwhFhyclzewaMxvQ+ushZrbCzHaa2Roz+/PcRDxYaVmJtia3Hfg8Wd+g0tKSgp31uZvcud1N5zhyx9jZ5246x5E7xs4+d9M5jtwxdva5m85x5I6xs8/dMXYGQpLqSsm/d8690/rruyTd4Zw7StI/SbrnUENmNsnMqsysqqVlTzdF/dj3P+ixdF+GHuKsz93kzu1uOmc263M3nTOb9bmbzpnN+txN58xmfe6mc2azPnfTObNZn7vpnNmsz910zmzW5+4YOwMhSXX37bZfP84596gkOeeWmdkRhxpyzk2TNE069HtKdkV9skGDyksPfF5eNlANDTsKdtbnbnLndjed48gdY2efu+kcR+4YO/vcTec4csfY2eduOseRO8bOPnfH2BlSS96+gyI+KdWVkg+b2Qwz+7SkR83sejP7lJl9S9KWHORr17qqag0ZcqIGDx6k4uJiTZhwgRYuWlKws+Qmd7ZnyR3OLLnDmSV3OLPkDmeW3OHMkjucWXKHM0vucGZ97wZC0eGVks65ya0nIB+S9CeSekmaJOkxSZd2ZfGsmXfr7LNO04AB/bX5rSrdcuvtmj5jTlqzzc3Nuu76KapcPFs9EgnNeHCuamtfL9hZcpM727PkDmeW3OHMkjucWXKHM0vucGbJHc4sucOZJXc4s753A6GwTN+XwMxmOucuS/f3Z+Pl2wAAAAAAADFr2lvPC5XbcdenJnIeqo3rtszK2+dJh1dKmtnj7Tx8zv7HnXPjs5IKAAAAAAAAQMFKdaObckm1ku6T5CSZpGGSpmY5FwAAAAAAAJCRFt8BkLZUN7qpkPSipMmS3nfOLZP0oXNuuXNuebbDAQAAAAAAACg8qW500yLpDjOb3/rPHalmAAAAAAAAAKAjaZ1gdM4lJV1sZudL2pXdSAAAAEB8Eta196FvyfAGlgAAAD5ldNWjc26xpMVZygIAAAAAAAAgArwUGwAAAAAAAAWBG92EI9WNbgAAAAAAAACgW3FSEgAAAAAAAEBOeTkp2atXL73w3CK9WPWUXq5+Rjf/8HsZf4/Ro0ao5tUV2lC7SjfecHXBz/rcTe5wcsfY2eduOseRO8bOPnfTOY7cMXbOdH7aL25Xcmu11r/09IHHjj76KFVWzlZNzUpVVs7WUUf1y3pujlU4uWPs7HM3nePIHWpnIBTmsnyXvqKeZe0u6Nu3j/bs+UBFRUVasexR/cN3b9aatS+l9T0TiYTqalZqzNhLlEw2aPULlZp42VWqq3ujIGfJTW46599uOseRO8bOoeaOsXOouWPsnO5827tvn3HGqdq9e4+mP3Cnhn7hXEnST348We++u1O33X63bvjHq3X00f30g8k/PjDT3t23871zvs2GmjvGzqHmjrFzqLlD6Ny0t94O8S2iNvVTE7N7oisw39syK2+fJ95evr1nzweSpOLiIhUVFyuTk6PDhw3Vxo2btWnTFjU2NmrevAUaP250wc6Sm9zZniV3OLPkDmeW3OHMkjuc2Zhyr1q1Ru+9t/Njj40bN0ozZ82XJM2cNV/jx6feH1LnfJgNNXeMnUPNHWPnUHOH2hkIibeTkolEQlXrlqih/jdaunSF1q5bn/ZsaVmJtia3Hfg8Wd+g0tKSgp31uZvcud1N5zhyx9jZ5246x5E7xs4+d9M5t7n3O+64Adq+/W1J0vbtb+vYY4/J6l6OVW530zmO3DF29rk7xs6QHB8f+8hnHZ6UNLNHzGyimR3e3YtbWlpUMWyUTjixQsMqhuqkkz6b9qzZwVeepnulZYizPneTO7e76ZzZrM/ddM5s1uduOmc263M3nTOb9bmbzpnNdsd8Z4Xamdy5m/W5O8bcMXb2uTvGzkBIUl0peaqkCyVtMbN5ZvZVM+uZ6pua2SQzqzKzqpaWPR3+3vff36XlK57X6FEj0g5dn2zQoPLSA5+Xlw1UQ8OOgp31uZvcud1N5zhyx9jZ5246x5E7xs4+d9M5t7n3e/vtd1RScpwkqaTkOP3ud7/P6l6OVW530zmO3DF29rk7xs5ASFKdlHzbOXeRpBMkLZT0bUn1ZjbdzEYdasg5N805V+Gcq0gk+h709QED+qtfvyMlSb1799bIc87Ua69tTDv0uqpqDRlyogYPHqTi4mJNmHCBFi5aUrCz5CZ3tmfJHc4sucOZJXc4s+QOZzbW3PstXPSULpt4sSTpsokXa+HC1POhdiY3nfN5N53jyB1qZyAkRSm+7iTJOfcHSTMlzTSz/pImSLpJUqf+VzFw4PF64P471aNHQolEQg8/vFCLK59Oe765uVnXXT9FlYtnq0cioRkPzlVt7esFO0tucmd7ltzhzJI7nFlyhzNL7nBmY8o985f/pbPOOk0DBvTXWxvX6dZ/marbbvsvzZ59j6741te1dWu9Lrnk7wqqcz7Mhpo7xs6h5o6xc6i5Q+0MhMQ6el8CM1vhnDurKwuKepbxxgcAAABACgk7+D3EMtHC+40BQFSa9tZ37V8cBerfT5jIvxDbuPG3s/L2edLhy7fbOyFpZr/MXhwAAAAAAAAAha7Dl2+b2eOffEjSl83sKElyzo3PVjAAAAAAAAAAhSnVe0oOklQj6T7te39Jk1QhaWqWcwEAAAAAAAAoUKnuvn2KpBclTZb0vnNumaQPnXPLnXPLsx0OAAAAAAAAQOHp8EpJ51yLpDvMbH7rP3ekmgEAAAAAAAB8aPEdAGlL6wSjcy4p6WIzO1/SruxGAgAAAOLT1btnd+Xu3dy5GwAA5FpGVz065xZLWpylLAAAAAAAAAAikOo9JQEAAAAAAACgW3FSEgAAAAAAAEBOcdMaAAAAAAAAFATeJTkc3q6UHD1qhGpeXaENtat04w1X53Q+xFmfu8kdTu4YO/vcTec4csfY2eduOseRO8bOPndfc82VWv/S06pev1TXXntlzvZ2dT7GY0XnOHLH2Nnn7hg7A6Ewl+U77RX1LDtoQSKRUF3NSo0Ze4mSyQatfqFSEy+7SnV1b6T1PbsyH+IsuclN5/zbTec4csfYOdTcMXYONXeMnXO1u727b5/0uc9q1qy7dfqXvqK9exu1aNEsXXvtD/Tmm5s+9vvau/t2CJ27ezbU3DF2DjV3jJ1DzR1C56a99Qf/wQ/95ISJXCzZxvd/OytvnyderpQcPmyoNm7crE2btqixsVHz5i3Q+HGjczIf4iy5yZ3tWXKHM0vucGbJHc4sucOZJXfms3/2Z0O0Zs16ffjhR2pubtbKFat1wQVjsr63q/MxHis6x5E7xs6h5g61MxASLyclS8tKtDW57cDnyfoGlZaW5GQ+xFmfu8md2910jiN3jJ197qZzHLlj7OxzN53DyV1T+5rOPPNU9e9/lA47rLfGjDlH5eWlWd/b1fkYjxWd48gdY2efu2PsDISkwxvdmNmnJU2RtE3STyXdIek0SXWSbnDObe7MUmvnpSWZvIy8K/MhzvrcTe7c7qZzZrM+d9M5s1mfu+mc2azP3XTObNbnbjpnNutz94YNb+q223+uJyof0u7de/SbV2rV1NSU9b1dnY/xWNE5s1mfu+mc2azP3TF2BkKS6krJGZLWSdotabWkDZLOk/SkpAcONWRmk8ysysyqWlr2HPT1+mSDBrX5L7TlZQPV0LAj7dBdmQ9x1uducud2N53jyB1jZ5+76RxH7hg7+9xN53ByS9KMGXN06hfP08hzL9J77+486P0ks7WXYxXGrM/dMeaOsbPP3TF2htQix0ebj3yW6qTkEc65/3bO/VTSkc65qc65rc65+yUdfagh59w051yFc64ikeh70NfXVVVryJATNXjwIBUXF2vChAu0cNGStEN3ZT7EWXKTO9uz5A5nltzhzJI7nFlyhzNL7s7tPvbYYyRJgwaV6sILz9PcuQtyspdjFcYsucOZJXc4s753A6Ho8OXbklrM7DOSMqASaAAAIABJREFU+knqY2YVzrkqMxsiqUdnlzY3N+u666eocvFs9UgkNOPBuaqtfT0n8yHOkpvc2Z4ldziz5A5nltzhzJI7nFlyd2733DnTdMwxR6uxsUnfuW6ydu58Pyd7OVZhzJI7nFlyhzPrezcQCuvofQnMbKSkn0tqkfRtSf8g6fPad5JyknPusVQLinqW5fe1ogAAAEABSNjB70GWrhbeqwwAgtO0t77zf/AXsH894VL+pdbG5N/+Km+fJx1eKemcWyrps20eWmVmiySNd861ZDUZAAAAAAAAgIKU6u7bj7fz8AhJj5mZnHPjs5IKAAAAAAAAyBBX0IUj1XtKDpJUI+k+SU6SSRomaWqWcwEAAAAAAAAoUKnuvn2KpBclTZb0vnNumaQPnXPLnXPLsx0OAAAAAAAAQOFJ9Z6SLZLuMLP5rf/ckWoGAAAAAAAAADqS1glG51xS0sVmdr6kXdmNBAAAACBT3EEbAACEJKOrHp1ziyUtzlIWAAAAAAAAoNP4T3ThSPWekgAAAAAAAADQrTgpCQAAAAAAACCnOCkJAAAAAAAAIKe8nZS8d9pUbUu+rOr1Szs1P3rUCNW8ukIbalfpxhuuLvhZn7vJHU7uGDv73E3nOHLH2NnnbjrHkTvGzj530zmO3DF29rmbznHkDrUzEApzWb5LX1HPsnYXnHnGqdq9e4+mT79LJw8dmdH3TCQSqqtZqTFjL1Ey2aDVL1Rq4mVXqa7ujYKcJTe56Zx/u+kcR+4YO4eaO8bOoeaOsXOouWPsHGruGDuHmjvGzqHmDqFz0956SytMZG494VLuddPGD3/7q7x9nni7UnLlqjV6972dnZodPmyoNm7crE2btqixsVHz5i3Q+HGjC3aW3OTO9iy5w5kldziz5A5nltzhzJI7nFlyhzNL7nBmyR3OrO/dsWvh42Mf+azDk5JmljCzvzGzxWb2spm9aGZzzGxEjvK1q7SsRFuT2w58nqxvUGlpScHO+txN7tzupnMcuWPs7HM3nePIHWNnn7vpHEfuGDv73E3nOHLH2Nnn7hg7AyEpSvH1+yX9VtJPJF0kaZeklZKmmNmfO+f+s70hM5skaZIkWY9+SiT6dl/ifd//oMfSfRl6iLM+d5M7t7v/P3t3Hyd1fd77/30NuxhFgzdolt0lrin19OT00YhF1MS7xArGBIiNoVWxuTPkdzSpNid67KmJR/trqy02MYlpAm2AaBAwTbACGuJdhP7kZpVFYZeKCIFZFjRVE8Gk7M31+4OVrC67M7OzM5/5zOf19LGPzO7stdf7vTPywG++M186FzYbcjedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu+lc2GzI3XQubDbk7hQ7AzHJdVDyD9390723V5vZGnf/qpk9KalF0mEPSrr7HElzpIHfU7IY7dkOjWusP/R5Y8NYdXTsrdrZkLvJXd7ddE4jd4qdQ+6mcxq5U+wccjed08idYueQu+mcRu4UO4fcnWJnICa53lOy08x+R5LM7HRJByTJ3f9LUrDD9OubWzR+/Clqahqn2tpazZgxXQ8uW1m1s+Qmd6lnyR3PLLnjmSV3PLPkjmeW3PHMkjueWXLHM0vueGZD7wZiketMyRskPW5m/9X7vZdLkpmdKGlZMYvvvedunX/e2Roz5njteLFZt942W/PmL8prtru7W9ddf7NWLF+oEZmM5i9YrNbW56t2ltzkLvUsueOZJXc8s+SOZ5bc8cySO55ZcsczS+54Zskdz2zo3anrqdhrTePtLNf7EtjBNzM4wd1/0fv59939z/JdUIqXbwMAAAAAAKSs60A7h98O46tNV3Icqo/bdvygYp8ng54paWb/1uf2mzc/ZGbHSpK7TytdNAAAAAAAAADVKNfLt8dJ2izpn3XwPSRN0hmS7ixxLgAAAAAAAABVKteFbv5Q0tOS/krSL939CUm/dvefufvPSh0OAAAAAAAAQPUZ9ExJd++R9DUzu7/3f/fmmgEAAAAAAABC6BFvKRmLvA4wuntW0ifM7COSflXaSAAAAAAAAACqWUFnPbr7cknLS5QFAAAAAAAAQAJyvackAAAAAAAAAAwrDkoCAAAAAAAAKCsOSgIAAAAAAAAoq2AHJadMvkCbNz2pLa2rdeMN15Z1PsbZkLvJHU/uFDuH3E3nNHKn2DnkbjqnkTvFziF3h+wsSZlMRuvX/UQP/HhB2XbzWKXROeRuOqeRO9bOqXM+3vJRycy9tBFrRjb0W5DJZNS2eZUuvuRyZbMdWvPUCs286hq1tW3N62cWMx/jLLnJTefK203nNHKn2DnW3Cl2jjV3ip1jzV1s5zddf90s/eEf/oHeecwxmn7pJ/Oa4bGic7XmTrFzrLlj6Nx1oN3yCpOYv2q6otKPxZXV3+xYWLHPkyBnSk46Y4K2bduh7dt3qrOzU0uWPKBpU6eUZT7GWXKTu9Sz5I5nltzxzJI7nllyxzNL7nhm39TQMFaXfPhCfe979xU0x2NF50reTec0csfaGYhJkIOS9Q112pXdfejzbHuH6uvryjIf42zI3eQu7246p5E7xc4hd9M5jdwpdg65m85p5C62syT945236qa//H/V09NT0ByPFZ0reTed08gda2cgJoMelDSz0WZ2u5ltMbP/7P1o6/3asUNdatb/zNFCXkZezHyMsyF3k7u8u+lc2GzI3XQubDbkbjoXNhtyN50Lmw25m86FzYbcHbLzRy75I7300i/0zIbn8p4Zjt08VoXNhtydYu4UO4fcnWJnICY1Oe5fIukxSRe4+x5JMrM6SZ+UdL+kiw43ZGazJM2SJBsxWpnMqLfc357t0LjG+kOfNzaMVUfH3rxDFzMf42zI3eQu7246p5E7xc4hd9M5jdwpdg65m85p5C628/vfP1FTPzpZH774Q3rHO47QO995jBbM/4Y++ak/r+jcMf6+U+wccjed08gda2dIhZ2bj5ByvXy7yd3vePOApCS5+x53v0PSuwcacvc57j7R3Se+/YCkJK1vbtH48aeoqWmcamtrNWPGdD24bGXeoYuZj3GW3OQu9Sy545kldzyz5I5nltzxzJI7nllJ+qubb1fTeyZq/Kln6cqZ1+jxx/89rwOSoXPH+PtOsXOsuVPsHGvuWDsDMcl1puTPzexGSQvcfa8kmdm7JH1K0q6hLu3u7tZ119+sFcsXakQmo/kLFqu19fmyzMc4S25yl3qW3PHMkjueWXLHM0vueGbJHc9ssXis6FzJu+mcRu5YOwMxscHel8DMjpN0k6Tpkt4lySXtlfRvku5w91dyLagZ2cAbHwAAAAAAAAyjrgPt/d98EvrLpis4DtXH3+1YWLHPk0HPlHT3VyX9794Pmdm5kiZJei6fA5IAAAAAAAAA8HaDHpQ0s3XuPqn39tWSrpW0VNItZna6u99ehowAAAAAAABATj3iRMlY5LrQTW2f25+XNNndb5U0WdKVJUsFAAAAAAAAoGrlutBNpvd9JTM6+P6TL0uSu+83s66SpwMAAAAAAABQdXIdlBwt6WlJJsnNrM7d95jZ0b1fAwAAAAAAAICC5LrQTdMAd/VIujSfBcUcueRdAAAAAAAAAIDqk+tMycNy9zckbR/mLAAAAAAAAAASMKSDkgAAAAAAAECl4VW38ch19W0AAAAAAAAAGFYclAQAAAAAAABQVsEOSm59fo02PPOImtev1JqnVhQ8P2XyBdq86UltaV2tG2+4tupnQ+4mdzy5U+wccjed08idYueQu+mcRu4UO4fcHWPnuXPu1O7sRrVseLTgncXsHY75GGdD7k4xd4qdQ+5OsTMQC3Mv7avta0c2HHbB1ufX6KyzP6z//M9XB5wdKFkmk1Hb5lW6+JLLlc12aM1TKzTzqmvU1rY1Z54YZ8lNbjpX3m46p5E7xc6x5k6xc6y5U+wca+6Qnc8950zt27df8+bdpdMmXJjXvkrIHeMsueOZJXc8s+Xa3XWg3fIKk5gbmy7nbSX7+Psd91Xs8yTKl29POmOCtm3boe3bd6qzs1NLljygaVOnVO0sucld6llyxzNL7nhmyR3PLLnjmSV3PLPFzq9avVavvPpa3rsqJXeMs+SOZ5bc8cyG3p26Hj7e8lHJgh2UdHc9tOI+rV3zkK7+7JUFzdY31GlXdvehz7PtHaqvr6va2ZC7yV3e3XROI3eKnUPupnMauVPsHHI3ndPIHbJzMXis0ugccjed08gda2cgJjVDHTSzh9z9w0OdP/+Cj6mjY69OPPEEPfzQIm35jxe0evXafHf3+1q+L0OPcTbkbnKXdzedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu2PtXAweq8JmQ+5OMXeKnUPuTrEzEJNBD0qa2ekD3SXptEHmZkmaJUmZEaOVyYzq9z0dHXslSS+//J9a+sBDOuOM0/I+KNme7dC4xvpDnzc2jD3086pxNuRucpd3N53TyJ1i55C76ZxG7hQ7h9xN5zRyh+xcDB6rNDqH3E3nNHLH2hmISa6Xb6+XNFvSnW/7mC3p2IGG3H2Ou09094mHOyB51FFH6uijRx26fdEfna/Nm/8j79Drm1s0fvwpamoap9raWs2YMV0PLltZtbPkJnepZ8kdzyy545kldzyz5I5nltzxzA7H/FDxWKXROdbcKXaONXesnYGY5Hr5dpukz7t7v8tDmdmuoS5917tO1A/v/xdJ0oiaEVq0aKlWrnwi7/nu7m5dd/3NWrF8oUZkMpq/YLFaW5+v2llyk7vUs+SOZ5bc8cySO55ZcsczS+54Zoudv/eeu3X+eWdrzJjjtePFZt1622zNm7+o4nPHOEvueGbJHc9s6N2p6xEvdY+FDfa+BGZ2maTn3L3faYxm9jF3X5prQe3IhiE/G3gaAQAAAAAA9Nd1oL3/m09CX2r6Uw4n9fGPOxZV7PNk0DMl3f2HfT83s3MkTZK0KZ8DkgAAAAAAAADwdoO+p6SZretz+3OSviXpGEm3mNlNJc4GAAAAAAAAoArlutBNbZ/bsyRd5O63Spos6cqSpQIAAAAAAABQtXJd6CZjZsfp4MFLc/eXJcnd95tZV8nTAQAAAAAAAHniDSXjkeug5GhJT0sySW5mde6+x8yO7v1aTjwZAAAAAAAAAPSV60I3TQPc1SPp0mFPAwAAAAAAAKDq5TpT8rDc/Q1J24c5CwAAAAAAAIAE5LrQDQAAAAAAAAAMKw5KAgAAAAAAACirIb18GwAAAAAAAKg0PaEDIG9BzpRsbKzXIyvv13PPPqGNLY/pi1/4bME/Y8rkC7R505Pa0rpaN95wbdXPhtxN7nhyp9g55O4YO8+dc6d2ZzeqZcOjBe8sZu9wzMc4G3J3irlT7BxyN53TyJ1i55C76ZxG7hQ7h9ydYmcgFubuJV1QM7Kh34K6upM0tu4kbWjZpKOPHqV1ax/Wxy/7jNratub1MzOZjNo2r9LFl1yubLZDa55aoZlXXZPXfIyz5CY3nStvd6ydzz3nTO3bt1/z5t2l0yZcmNe+Ssgd4yy545kldzyz5I5nltzxzJI7nllyxzNbrt1dB9otrzCJua7pT0t7oCsyd+1YVLHPkyBnSu7Z85I2tGySJO3bt19btmxVQ31d3vOTzpigbdt2aPv2ners7NSSJQ9o2tQpVTtLbnKXepbc8cwWO79q9Vq98upree+qlNwxzpI7nllyxzNL7nhmyR3PLLnjmSV3PLOhdwOxGPSgpJm908z+zszuMbMr3nbft4cjwMknN+q09/2+1q7bkPdMfUOddmV3H/o8296h+jwPasY4G3I3ucu7m85p5A7ZuRg8Vml0DrmbzmnkTrFzyN10TiN3ip1D7qZzGrlj7QzEJNeZkvMkmaR/lfSnZvavZnZE731nDTRkZrPMrNnMmnt69g/4w0eNOkpLFs/Vl758i15/fV/eoc36n3ma78vQY5wNuZvc5d1N58JmQ+6OtXMxeKwKmw25O8XcKXYOuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu1PsDMn55y3/VLJcByV/x91vcvel7j5N0jOSHjOzEwYbcvc57j7R3SdmMqMO+z01NTW6f/Fc3Xffj7V06UMFhW7PdmhcY/2hzxsbxqqjY2/VzobcTe7y7qZzGrlDdi4Gj1UanUPupnMauVPsHHI3ndPInWLnkLvpnEbuWDsDMcl1UPIIMzv0Pe7+N5LmSHpS0qAHJnOZO+dOtW15QV+/a07Bs+ubWzR+/Clqahqn2tpazZgxXQ8uW1m1s+Qmd6lnyR3P7HDMDxWPVRqdY82dYudYc6fYOdbcKXaONXeKnWPNnWLnWHPH2hmISU2O+x+U9CFJj7z5BXdfYGZ7JX1zqEs/8P4zdNXMy/Tsc61qXn/wX6yvfOV2PfTwY3nNd3d367rrb9aK5Qs1IpPR/AWL1dr6fNXOkpvcpZ4ldzyzxc7fe8/dOv+8szVmzPHa8WKzbr1ttubNX1TxuWOcJXc8s+SOZ5bc8cySO55ZcsczS+54ZkPvBmJhBb4nwjmSJkna5O55HaavGdlQ2S9gBwAAAAAAiEzXgfb+bz4J/XnTn3Acqo9v7Fhcsc+TXFffXtfn9uckfUvSMZJuMbObSpwNAAAAAAAAyFsPH2/5qGS53lOyts/tWZIucvdbJU2WdGXJUgEAAAAAAACoWrneUzJjZsfp4MFLc/eXJcnd95tZV8nTAQAAAAAAAKg6uQ5Kjpb0tCST5GZW5+57zOzo3q8BAAAAAAAAQEEGPSjp7k0D3NUj6dJhTwMAAAAAAACg6uU6U/Kw3P0NSduHOQsAAAAAAACABAzpoCQAAAAAAABQaXrkoSMgT7muvg0AAAAAAAAAw4qDkgAAAAAAAADKKshBycbGej2y8n499+wT2tjymL74hc8W/DOmTL5Amzc9qS2tq3XjDddW/WzI3eSOJ3eKnUPupnMauVPsHHI3ndPInWLnkLvpXNjs3Dl3and2o1o2PFrQ3HDs5rFKI3eKnUPuTrEzEAtzL+1r7WtGNvRbUFd3ksbWnaQNLZt09NGjtG7tw/r4ZZ9RW9vWvH5mJpNR2+ZVuviSy5XNdmjNUys086pr8pqPcZbc5KZz5e2mcxq5U+wca+4UO8eaO8XOseZOsbMknXvOmdq3b7/mzbtLp024MK+Z0LlTfaxizJ1i51hzx9C560C75RUmMdc0zeBNJfv49o4lFfs8CXKm5J49L2lDyyZJ0r59+7Vly1Y11NflPT/pjAnatm2Htm/fqc7OTi1Z8oCmTZ1StbPkJnepZ8kdzyy545kldzyz5I5nltzxzMace9XqtXrl1dfy/v5KyJ3qYxVj7hQ7x5o71s6QnI+3fFSy4O8pefLJjTrtfb+vtes25D1T31CnXdndhz7PtneoPs+DmjHOhtxN7vLupnMauVPsHHI3ndPInWLnkLvpnEbuFDsXK9bO5I5jNuTuFHPH2hmIyaAHJc2szsz+yczuNrMTzOz/mtlzZrbEzMYWu3zUqKO0ZPFcfenLt+j11/flPWfW/8zTfF+GHuNsyN3kLu9uOhc2G3I3nQubDbmbzoXNhtxN58JmQ+6mc2GzIXfTubDZYsXamdxxzIbcnWLuWDsDMcl1puR8Sa2Sdkl6XNKvJX1E0ipJ3xloyMxmmVmzmTX39Ow/7PfU1NTo/sVzdd99P9bSpQ8VFLo926FxjfWHPm9sGKuOjr1VOxtyN7nLu5vOaeROsXPI3XROI3eKnUPupnMauVPsXKxYO5M7jtmQu1PMHWtnICa5Dkq+y92/6e63SzrW3e9w953u/k1JJw805O5z3H2iu0/MZEYd9nvmzrlTbVte0NfvmlNw6PXNLRo//hQ1NY1TbW2tZsyYrgeXrazaWXKTu9Sz5I5nltzxzJI7nllyxzNL7nhmY85djFg7kzuOWXLHMxt6NxCLmhz39z1o+f1B7ivIB95/hq6aeZmefa5VzesP/ov1la/crocefiyv+e7ubl13/c1asXyhRmQymr9gsVpbn6/aWXKTu9Sz5I5nltzxzJI7nllyxzNL7nhmY8597z136/zzztaYMcdrx4vNuvW22Zo3f1FF5071sYoxd4qdY80da2dIPRV/eRe8yQZ7XwIzu03S37v7vrd9fbyk2939slwLakY28GwAAAAAAAAYRl0H2vu/+ST0+aZPcByqj+/uuL9inyeDninp7l/t+7mZnSNpkqRN+RyQBAAAAAAAAIC3y3X17XV9bn9O0rckHSPpFjO7qcTZAAAAAAAAAFShXO8LWdvn9ixJF7n7rZImS7qyZKkAAAAAAAAAVK2cF7oxs+N08OClufvLkuTu+82sq+TpAAAAAAAAAFSdXAclR0t6WpJJcjOrc/c9ZnZ079cAAAAAAACAitATOgDylutCN00D3NUj6dJhTwMAAAAAAACg6uU6U/Kw3P0NSduHOQsAAAAAAACABOS60A0AAAAAAAAADCsOSgIAAAAAAAAoqyG9fBsAAAAAAACoNC4PHQF5Cnam5Nw5d2p3dqNaNjw6pPkpky/Q5k1Pakvrat14w7VVPxtyN7njyZ1i55C76ZxG7hQ7h9xN5zRyx9qZv7/G81ilmDvFziF30zmN3LF2BmJh7qU9glwzsuGwC84950zt27df8+bdpdMmXFjQz8xkMmrbvEoXX3K5stkOrXlqhWZedY3a2rZW5Sy5yU3nyttN5zRyp9g51twpdo41d6ydJf7+GstjlWLuFDvHmjvFzrHmjqFz14F2yytMYq5uuoxTJfv45x0/rNjnScFnSprZScOxeNXqtXrl1deGNDvpjAnatm2Htm/fqc7OTi1Z8oCmTZ1StbPkJnepZ8kdzyy545kldzyz5I5nNvRu/v4ax2OVYu4UO8eaO8XOseaOtTMQk0EPSprZ8W/7OEHSOjM7zsyOL1PGfuob6rQru/vQ59n2DtXX11XtbMjd5C7vbjqnkTvFziF30zmN3Cl2Drk7xc7FivH3HetjlWLuFDuH3E3nNHLH2hmISa4L3fxC0s/f9rUGSc9IcknvOdyQmc2SNEuSbMRoZTKjiozZ7+f3+1q+L0OPcTbkbnKXdzedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd6fYuVgx/r5jfaxSzJ1i55C76VzYbMjdKXaG1BM6APKW6+XbN0r6D0nT3P0Udz9FUrb39mEPSEqSu89x94nuPnG4D0hKUnu2Q+Ma6w993tgwVh0de6t2NuRucpd3N53TyJ1i55C76ZxG7hQ7h9ydYudixfj7jvWxSjF3ip1D7qZzGrlj7QzEZNCDku4+W9LVkr5qZv9oZsdI4a+tvr65RePHn6KmpnGqra3VjBnT9eCylVU7S25yl3qW3PHMkjueWXLHM0vueGZD7y5GjL/vWB+rFHOn2DnW3Cl2jjV3rJ2BmOR6+bbcPSvpE2Y2VdJPJR01HIvvvedunX/e2Roz5njteLFZt942W/PmL8prtru7W9ddf7NWLF+oEZmM5i9YrNbW56t2ltzkLvUsueOZJXc8s+SOZ5bc8cyG3s3fX+N4rFLMnWLnWHOn2DnW3LF2BmJiBb4nwrmSzpe0zt3zOkxfM7Ih+JmVAAAAAAAA1aTrQHv/N5+EPtN0Gceh+vjejh9W7PMk19W31/W5/TlJ35A0QtItZnZTibMBAAAAAAAAqEK5Xr5d2+f2LEmT3f1lM5staY2k20uWDAAAAAAAACiAh78UCvKU66BkxsyO08EzKs3dX5Ykd99vZl0lTwcAAAAAAACg6uQ6KDla0tOSTJKbWZ277zGzo3u/BgAAAAAAAAAFGfSgpLs3DXBXj6RLhz0NAAAAAAAAgKqX60zJw3L3NyRtH+YsAAAAAAAAABIwpIOSAAAAAAAAQKXpCR0AecuEDgAAAAAAAAAgLRyUBAAAAAAAAFBWwQ5KTpl8gTZvelJbWlfrxhuuLet8jLMhd5M7ntzFzM6dc6d2ZzeqZcOjBc0Nx24eqzQ6h9xN5zRyp9g55G46p5E7xc4hd9M5jdwpdg65O8XOQCzM3Uu6oGZkQ78FmUxGbZtX6eJLLlc226E1T63QzKuuUVvb1rx+ZjHzMc6Sm9zl6HzuOWdq3779mjfvLp024cK8Ziohd4y/7xQ7x5o7xc6x5k6xc6y5U+wca+4UO8eaO8XOseZOsXOsuWPo3HWg3fIKk5hPNn28tAe6IrNgx79W7PMkyJmSk86YoG3bdmj79p3q7OzUkiUPaNrUKWWZj3GW3OQu9awkrVq9Vq+8+lre318puWP8fafYOdbcKXaONXeKnWPNnWLnWHOn2DnW3Cl2jjV3ip1jzR1rZ0g97nz0+ahkQQ5K1jfUaVd296HPs+0dqq+vK8t8jLMhd5O7vLtDdi4GjxWdK3k3ndPInWLnkLvpnEbuFDuH3E3nNHKn2Dnk7hQ7AzEZ9KCkmV3c5/ZoM/sXM3vWzBaa2buGutSs/5mjhbyMvJj5GGdD7iZ3eXeH7FwMHqvyzYbcnWLuFDuH3E3nwmZD7qZzYbMhd9O5sNmQu+lc2GzI3XQubDbk7hQ7AzHJdabk3/a5faekDklTJa2X9N2Bhsxslpk1m1lzT8/+fve3Zzs0rrH+0OeNDWPV0bE379DFzMc4G3I3ucu7O2TnYvBY0bmSd9M5jdwpdg65m85p5E6xc8jddE4jd4qdQ+5OsTMQk0Jevj3R3W9295+7+9ckNQ30je4+x90nuvvETGZUv/vXN7do/PhT1NQ0TrW1tZoxY7oeXLYy7yDFzMc4S25yl3q2WDxWdK7k3XROI3eKnWPNnWLnWHOn2DnW3Cl2jjV3ip1jzR1rZyAmNTnuP8nMviTJJL3TzMx/e87wkN+Psru7W9ddf7NWLF+oEZmM5i9YrNbW58syH+Msucld6llJuveeu3X+eWdrzJjjtePFZt1622zNm7+o4nPH+PtOsXOsuVPsHGvuFDvHmjvFzrHmTrFzrLlT7Bxr7hQ7x5o71s6QeKF7PGyw9yXz5hVrAAAgAElEQVQws1ve9qVvu/vLZlYn6e/d/c9yLagZ2cDzAQAAAAAAYBh1HWjv/+aT0MyT/5jjUH3c+/MfVezzZNAzJd391r6fm9k5ZnaVpE35HJAEAAAAAAAAgLfLdfXtdX1uXy3pW5KOkXSLmd1U4mwAAAAAAAAAqlCu94Ws7XP785Iu6j17crKkK0uWCgAAAAAAAEDVynWhm4yZHaeDBy/N3V+WJHffb2ZdJU8HAAAAAAAAoOrkOig5WtLTOnj1bTezOnffY2ZH934NAAAAAAAAqAg9XH87GrkudNM0wF09ki4d9jQAAAAAgAFlbOjnhvQ4/6EOAKgcuc6UPCx3f0PS9mHOAgAAAAAAACABuS50AwAAAAAAAADDioOSAAAAAAAAAMpqSC/fBgAAAAAAACqNc6GbaAQ7U3LunDu1O7tRLRseHdL8lMkXaPOmJ7WldbVuvOHaqp8NuZvc8eQuZjbWfydD7qZzGrlT7BxyN53TyJ1i55C76Vy9ued8d7ayu1q04ZlHDn3t43/8EbVseFS/+fVOnX76H1Rk7uGaDbmbzuXLzX+nDG03EAPzEl+BrWZkw2EXnHvOmdq3b7/mzbtLp024sKCfmclk1LZ5lS6+5HJlsx1a89QKzbzqGrW1ba3KWXKTuxydY/x3MuRuOqeRO8XOseZOsXOsuVPsHGvuFDvHkLvv1bfPefPvb9/7uiac/keSpN/7vfHq6enR3d+6Q//7pr/WM888e+j7B7r6dqV3rrTddC5vbv47ZeDZrgPtNsCPSNrlJ3+MUyX7uO/nSyv2eRLsTMlVq9fqlVdfG9LspDMmaNu2Hdq+fac6Ozu1ZMkDmjZ1StXOkpvcpZ6V4vx3MuRuOqeRO8XOseZOsXOsuVPsHGvuFDvHlnv16rV69W1/f9uy5QU9//yLee0MlXs4ZmPNnWLnYuf575TCdwOxKPigpJmdUIoghahvqNOu7O5Dn2fbO1RfX1e1syF3k7u8u0N2LgaPFZ0reTed08idYueQu+mcRu4UO4fcneLf5VJ8rFLsPBzzQxVr55B/HgDlNOhBSTO73czG9N6eaGYvSlprZj83s/PLkvDwufp9Ld+Xocc4G3I3ucu7O2TnYvBYlW825O4Uc6fYOeRuOhc2G3I3nQubDbmbzoXNhtyd4t/lUnysUuw8HPNDFWvnkH8eVIMePt7yUclynSn5EXf/Re/tf5D0J+4+XtJFku4caMjMZplZs5k19/TsH6aov9We7dC4xvpDnzc2jFVHx96qnQ25m9zl3R2yczF4rOhcybvpnEbuFDuH3E3nNHKn2Dnk7hT/LpfiY5Vi5+GYH6pYO4f88wAop1wHJWvNrKb39pHuvl6S3P15SUcMNOTuc9x9ortPzGRGDVPU31rf3KLx409RU9M41dbWasaM6Xpw2cqqnSU3uUs9WyweKzpX8m46p5E7xc6x5k6xc6y5U+wcc+5ixNo5xtwpdh6O+aGKtXPIPw+AcqrJcf/dklaY2e2SHjazr0v6kaQLJbUUs/jee+7W+eedrTFjjteOF5t1622zNW/+orxmu7u7dd31N2vF8oUakclo/oLFam19vmpnyU3uUs9Kcf47GXI3ndPInWLnWHOn2DnW3Cl2jjV3ip1jy33P97+l83r//vbitvW67a/v1KuvvKavfe2vdeKJx+uBpQu08dnN+uhHZ1ZU7uGYjTV3ip2Lnee/UwrfDcTCcr0vgZldIOl/SjpVBw9i7pK0VNI8d+/MtaBmZANvfAAAAAAAwyBj/d9rLl89vCcdUFW6DrQP/Q+EKvYnJ3+MP+z6WPzzpRX7PMl1pqTc/QlJT0iSmZ0raZKkHfkckAQAAAAAAACAtxv0oKSZrXP3Sb23r5Z0rQ6eJXmLmZ3u7reXISMAAAAAAACQU484UTIWOS900+f25yVNdvdbJU2WdGXJUgEAAAAAAACoWrlevp0xs+N08OClufvLkuTu+82sq+TpAAAAAAAAAFSdXAclR0t6WpJJcjOrc/c9ZnZ079cAAAAAAGXCxWoAANVi0IOS7t40wF09ki4d9jQAAAAAAAAAql7Oq28fjru/IWn7MGcBAAAAAAAAhsy50E00cl3oBgAAAAAAAACGFQclAQAAAAAAAJQVByUBAAAAAAAAlFWwg5JTJl+gzZue1JbW1brxhmvLOh/jbMjd5I4nd4qdQ+6mcxq5U+wccjed08idYueQu+mcRu5iZufOuVO7sxvVsuHRguaGYzePVRqdQ+5OsTMQC3Mv7RuA1oxs6Lcgk8mobfMqXXzJ5cpmO7TmqRWaedU1amvbmtfPLGY+xllyk5vOlbebzmnkTrFzrLlT7Bxr7hQ7x5o7xc6x5i6287nnnKl9+/Zr3ry7dNqEC/OaqYTcMf6+U+wca+4YOncdaLe8wiTmj0+expVu+vjRz/+tYp8nQc6UnHTGBG3btkPbt+9UZ2enlix5QNOmTinLfIyz5CZ3qWfJHc8sueOZJXc8s+SOZ5bc8cySO55ZSVq1eq1eefW1vL+/UnLH+PtOsXOsuWPtDMRk0IOSZvaMmd1sZr8znEvrG+q0K7v70OfZ9g7V19eVZT7G2ZC7yV3e3XROI3eKnUPupnMauVPsHHI3ndPInWLnkLtDdi4GjxWdK3l3ip2BmOQ6U/I4ScdKetzM1pnZX5hZfa4famazzKzZzJp7evYf7v5+XyvkZeTFzMc4G3I3ucu7m86FzYbcTefCZkPupnNhsyF307mw2ZC76VzYbMjddC5sNuTukJ2LwWNVvtmQu1PMHWtnICa5Dkq+6u5fdvd3S/pfkn5X0jNm9riZzRpoyN3nuPtEd5+YyYzqd397tkPjGn97bLOxYaw6OvbmHbqY+RhnQ+4md3l30zmN3Cl2DrmbzmnkTrFzyN10TiN3ip1D7g7ZuRg8VnSu5N0pdgZikvd7Srr7Kne/RlKDpDsknT3UpeubWzR+/Clqahqn2tpazZgxXQ8uW1mW+RhnyU3uUs+SO55ZcsczS+54Zskdzyy545kldzyzxeKxonMl706xMxCTmhz3P//2L7h7t6SHez+GpLu7W9ddf7NWLF+oEZmM5i9YrNbWfqtKMh/jLLnJXepZcsczS+54Zskdzyy545kldzyz5I5nVpLuvedunX/e2Roz5njteLFZt942W/PmL6r43DH+vlPsHGvuWDuDl7rHxAp8T4RzJE2StMnd8zpMXzOygWcDAAAAAADAMOo60N7/zSehS989leNQffx454MV+zzJdfXtdX1uf07StyQdI+kWM7upxNkAAAAAAAAAVKFc7ylZ2+f2LEkXufutkiZLurJkqQAAAAAAAABUrVzvKZkxs+N08OClufvLkuTu+82sq+TpAAAAAAAAAFSdXAclR0t6WpJJcjOrc/c9ZnZ079cAAAAAAACAitAj3lIyFoMelHT3pgHu6pF06bCnAQAAAAAAAFD1cp0peVju/oak7cOcBQAAAAAAAEACcl3oBgAAAAAAAACGFQclAQAAAAAAAJTVkF6+DQAAAAAAAFSantABkLdgZ0pOmXyBNm96UltaV+vGG64t63yMsyF3kzue3Cl2DrmbzmnkTrFzyN10TiN3ip1D7qZzGrlT7BxyN53Ll3vunDu1O7tRLRseLXhnMXuLnQ29G4iBuZf2Uuk1Ixv6LchkMmrbvEoXX3K5stkOrXlqhWZedY3a2rbm9TOLmY9xltzkpnPl7aZzGrlT7Bxr7hQ7x5o7xc6x5k6xc6y5U+wca+4UOxc7f+45Z2rfvv2aN+8unTbhwrz2DcfeGB6rrgPtlleYxEx990dLe6ArMg/uXFaxz5MgZ0pOOmOCtm3boe3bd6qzs1NLljygaVOnlGU+xllyk7vUs+SOZ5bc8cySO55ZcsczS+54Zskdzyy545lNNfeq1Wv1yquv5b1ruPbG+lgBMQlyULK+oU67srsPfZ5t71B9fV1Z5mOcDbmb3OXdTec0cqfYOeRuOqeRO8XOIXfTOY3cKXYOuZvOaeROsfNwzA9VrJ1D/b6Achv0oKSZTTSzx83sXjMbZ2Y/NbNfmtl6M5sw1KVm/c8cLeRl5MXMxzgbcje5y7ubzoXNhtxN58JmQ+6mc2GzIXfTubDZkLvpXNhsyN10Lmw25G46FzYbcjedC5sdjvmhirVzqN8XUG65rr79bUm3SDpW0v8n6S/c/SIzu7D3vrMPN2RmsyTNkiQbMVqZzKi33N+e7dC4xvpDnzc2jFVHx968QxczH+NsyN3kLu9uOqeRO8XOIXfTOY3cKXYOuZvOaeROsXPI3XROI3eKnYdjfqhi7Rzq91UtXBzAjUWul2/XuvtD7n6fJHf3H+rgjUclvWOgIXef4+4T3X3i2w9IStL65haNH3+KmprGqba2VjNmTNeDy1bmHbqY+RhnyU3uUs+SO55ZcsczS+54Zskdzyy545kldzyz5I5nNtXcxYi1c6jfF1Buuc6U/I2ZTZY0WpKb2cfcfamZnS+pe6hLu7u7dd31N2vF8oUakclo/oLFam19vizzMc6Sm9ylniV3PLPkjmeW3PHMkjueWXLHM0vueGbJHc9sqrnvvedunX/e2Roz5njteLFZt942W/PmLyr53lgfKyAmNtj7EpjZaZLukNQj6S8k/U9JfyZpt6RZ7v7vuRbUjGzgvFkAAAAAAIBh1HWgvf+bT0IfffdHOA7Vx7Kdyyv2eTLomZLu3iLp0HXnzeyHknZKei6fA5IAAAAAAAAA8HaDHpQ0s3XuPqn39uckXSNpqaRbzOx0d7+9DBkBAAAAAACAnHq40E00cl7ops/tWZImu/utkiZLurJkqQAAAAAAAABUrVwXusmY2XE6ePDS3P1lSXL3/WbWVfJ0AAAAAAAAAKpOroOSoyU9Lcl08Orbde6+x8yO7v0aAAAAAAAAABQk14Vumga4q0fSpcOeBgAAAABQdYo5o4V3hwOA6pTrTMnDcvc3JG0f5iwAAAAAAADAkLnzf2XEIteFbgAAAAAAAABUITMbZ2aPm1mbmW02s+t6v368mf3UzLb2/u9xvV83M/uGmb1gZs+a2elD3c1BSQAAAAAAACBNXZL+l7v/d0lnSbrWzN4r6SZJj7r770p6tPdzSfqwpN/t/Zgl6Z+GupiDkgAAAAAAAECC3L3D3Z/pvf26pDZJDZKmS1rQ+20LJH2s9/Z0Sd/3g9ZIOtbMxg5ld7CDknPn3Knd2Y1q2fDokOanTL5Amzc9qS2tq3XjDddW/WzI3eSOJ3eKnUPupnMauVPsHHI3ndPInWLnkLvpnEbuWDtf9+efU0vLY9qw4VHdc8/dOuKII8q2O8bZkLtTzB1rZ6AvM5tlZs19PmYN8r1NkiZIWivpXe7eIR08cCnppN5va5C0q89YtvdrhWcr9RuA1oxsOOyCc885U/v27de8eXfptAkXFvQzM5mM2jav0sWXXK5stkNrnlqhmVddo7a2rVU5S25y07nydtM5jdwpdo41d4qdY82dYudYc6fYOdbcMXQ+3NW36+vr9MTjP9YfvO+D+s1vfqOFC7+jhx96TN+/Z8lbvm+g/2KN8fcdw2NF7ng6dx1oL+bC9lVryrgPc6WbPn6y66G8nidmdrSkn0n6G3f/kZm95u7H9rn/VXc/zsyWS/o7d1/d+/VHJd3o7k8Xmi3YmZKrVq/VK6++NqTZSWdM0LZtO7R9+051dnZqyZIHNG3qlKqdJTe5Sz1L7nhmyR3PLLnjmSV3PLPkjmeW3PHMht5dU1OjI498h0aMGKGjjjxSuzv2VHzuFB+rFHPH2hkYCjOrlfSvkn7g7j/q/fLeN1+W3fu/L/V+PStpXJ/xRkm7h7I3yveUrG+o067sb/tm2ztUX19XtbMhd5O7vLvpnEbuFDuH3E3nNHKn2DnkbjqnkTvFziF3p9h59+49+trXvqMXt63Trp0b9Ktf/UqPPPJkxedO8bFKMXesnYFCmZlJ+hdJbe7+j33u+jdJn+y9/UlJD/T5+p/1XoX7LEm/fPNl3oUa9KCkmR1tZrf1XhL8l2b2spmtMbNP5Zg79Hr1np79Q8k1qIO/r7fK92XoMc6G3E3u8u6mc2GzIXfTubDZkLvpXNhsyN10Lmw25G46FzYbcjedC5sNuTvFzsceO1pTp07R7556lt598uk6atRRuuKKP85rttjdMc6G3J1i7lg7A0PwAUlXSfqQmbX0flwi6XZJF5nZVkkX9X4uSSskvSjpBUlzJV0z1MU1Oe7/gaQfS5oiaYakUZIWSbrZzE519/9zuCF3nyNpjjTwe0oWoz3boXGN9Yc+b2wYq46OvVU7G3I3ucu7m85p5E6xc8jddE4jd4qdQ+6mcxq5U+wccneKnS+88Fzt2LFTv/jFK5KkpUsf0tlnTdTChT/KMRk2d4qPVYq5Y+0MFKr3vSEHet/JfheB8YNHyIfl6ku5Xr7d5O7z3T3bewrnNHffKunTkvL/v7CG2frmFo0ff4qamsaptrZWM2ZM14PLVlbtLLnJXepZcsczS+54Zskdzyy545kldzyz5I5nNuTuXTvbNenM03Xkke+QJH3og+doy5b8LiISMneKj1WKuWPtDMQk15mS+83sHHdfbWZTJb0iSe7eY4c7n7gA995zt84/72yNGXO8drzYrFtvm6158xflNdvd3a3rrr9ZK5Yv1IhMRvMXLFZr6/NVO0tucpd6ltzxzJI7nllyxzNL7nhmyR3PLLnjmQ25e936DfrRj5Zr3bqfqKurSxtbNmvuP/+g4nOn+FilmDvWzpBcvNQ9FjbY+xKY2ft08PXhp0raJOkz7v68mZ0o6XJ3/0auBaV4+TYAAAAAIB7FnNHCf1ACh9d1oL2ok8Wq1eRxF/PHRh8rdz1csc+TQc+UdPeNkia9+bmZnWNmH5W0KZ8DkgAAAAAAAADwdrmuvr2uz+2rJX1L0jGSbjGzm0qcDQAAAAAAAEAVynWhm9o+tz8v6SJ3v1XSZElXliwVAAAAAAAAgKqV60I3GTM7TgcPXpq7vyxJ7r7fzLpKng4AAAAAAADIUw/vRBuNXAclR0t6Wgffl9jNrM7d95jZ0crzvYp5Q2OgsmRs6P9W9gxyYSwAAABgIMX8LbImM2LIs1093UVsBgCUUq4L3TQNcFePpEuHPQ0AAAAAAACAqpfrTMnDcvc3JG0f5iwAAAAAAAAAEpDrQjcAAAAAAAAAMKyGdKYkAAAAAAAAUGmcayFEI9iZktf9+efU0vKYNmx4VPfcc7eOOOKIguanTL5Amzc9qS2tq3XjDddW/WzI3eSOJ3exnb/whc9qwzOPqGXDo/riFz9btt08Vml0DrmbzmnkTrFzyN10TiN3ip1D7qZz7tnGxrH6yU8WqaXlUT3zzCO69trPSJKOO260li//gTZt+pmWL/+Bjj12dEXlHq7ZYuYbG+v1yMr79dyzT2hjy2P64hfK9/f9YudjnA29G4iBlfoIcu3Ihn4L6uvr9MTjP9YfvO+D+s1vfqOFC7+jhx96TN+/Z8lbvm+gZJlMRm2bV+niSy5XNtuhNU+t0MyrrlFb29aceWKcJTe5h3N2oKtv/4/3/jfde+/dev8HPqoDBzq1bNm9+uIX/49eeOG3bx870NW3eazoXK25U+wca+4UO8eaO8XOseZOsXOsuau9c9+rb9fVnaS6upPU0rJJRx89Sk89tVyf+MTndNVVn9Crr76m2bO/rS9/+Rode+xo3Xzz3w149e1K71yK+bq6kzS27iRt6P3drVv7sD5+2WcqPneMs+Xa3XWg/fD/cZe4Cxsnc6pkH49mV1bs8yTYmZI1NTU68sh3aMSIETrqyCO1u2NP3rOTzpigbdt2aPv2ners7NSSJQ9o2tQpVTtLbnKXelaSfu/3xmvt2g369a9/o+7ubq16co2mT7+44nPH+PtOsXOsuVPsHGvuFDvHmjvFzrHmTrFzrLlT6rxnz0tqadkkSdq3b7+2bHlBDQ11mjr1It177w8lSffe+0NNmza5onIPx2yx83v2vKQNb/ndbVVDfV3F545xNvRuIBZBDkru3r1HX/vad/TitnXatXODfvWrX+mRR57Me76+oU67srsPfZ5t71B9nn+Yxjgbcje5y7s7ZOfNrf+hc889U8cff6yOPPIduvjiD6mxsb7ic8f4+06xc8jddE4jd4qdQ+6mcxq5U+wccjedC8998smNOu20/6F16zbopJPGaM+elyQdPPh24oljKjJ3yMeqr5NPbtRp7/t9rV23oSx7Y/x9x9oZiMmgByXNbLSZ3W5mW8zsP3s/2nq/duxQlx577GhNnTpFv3vqWXr3yafrqFFH6Yor/jjveTvMy0/zfRl6jLMhd5O7vLtDdt6y5QX9w+xv66EV92nZg/fq2eda1dXVVfLdPFaFzYbcnWLuFDuH3E3nwmZD7qZzYbMhd9O5sNmQu+lc2OyoUUfpvvu+qy9/+Va9/vq+vGaGa3esj9WbRo06SksWz9WXvnxL3r+7FJ9jsXYGYpLrTMklkl6VdIG7n+DuJ0j6YO/X7h9oyMxmmVmzmTX39Ozvd/+FF56rHTt26he/eEVdXV1auvQhnX3WxLxDt2c7NK7PGVyNDWPV0bG3amdD7iZ3eXeH7CxJ8+cv0plnfVgX/tFlevWV197yfpKVmjvG33eKnUPupnMauVPsHHI3ndPInWLnkLvpnP9sTU2NFi36rhYt+rEeeOBhSdJLL/1CdXUnSTr43okvv/yListd7OxwzNfU1Oj+xXN1330/1tKlD5Vtb4y/71g7Q+qR89Hno5LlOijZ5O53uPuhN3x09z3ufoekdw805O5z3H2iu0/MZEb1u3/XznZNOvN0HXnkOyRJH/rgOdqyJb83i5Wk9c0tGj/+FDU1jVNtba1mzJiuB5etrNpZcpO71LNvOvHEEyRJ48bV62Mf+7AWL36g4nPH+PtOsXOsuVPsHGvuFDvHmjvFzrHmTrFzrLlT6/zd7/6Dtmx5Qd/4xj8f+tqyZT/VzJmXSZJmzrxMDz7404rLXezscMzPnXOn2ra8oK/fNSfvmdC5Y5wNvRuIRU2O+39uZjdKWuDueyXJzN4l6VOSdg116br1G/SjHy3XunU/UVdXlza2bNbcf/5B3vPd3d267vqbtWL5Qo3IZDR/wWK1tj5ftbPkJnepZ9+0eNEcnXDCcers7NKfX/dXeu21X1Z87hh/3yl2jjV3ip1jzZ1i51hzp9g51twpdo41d0qd3//+M3TllR/Xc8+1ae3ag2f6ffWrf6/Zs7+tH/zgn/SpT/2Jdu3arSuu+H8qKvdwzBY7/4H3n6GrZl6mZ59rVfP6gwe4vvKV2/XQw49VdO4YZ0PvBmJhg70vgZkdJ+kmSdMlvUuSS9or6d8k3eHur+RaUDuyYcjnilb2SaZAnDLW//1J8tXD+5gAAACgzGoyI4Y829XTPYxJgMrSdaB96P9xV8U+2HgR/+Hax+PZn1bs8yTXy7dPlfS37v57khokfUvStt77+NMdAAAAAAAAQMFyHZT8nqQ3r1TzdUnHSLpd0huS5pUwFwAAAAAAAFAQ55+3/FPJcr2nZMbdu3pvT3T303tvrzazlhLmAgAAAAAAAFClcp0pucnMPt17e6OZTZQkMztVUmdJkwEAAAAAAACoSrkOSl4t6Xwz2ybpvZKeMrMXJc3tvQ8AAAAAAAAACjLoy7fd/ZeSPmVmx0h6T+/3Z919b74LKvvV60B6uII2AAAAYsIVtAGgOuV6T0lJkru/LmljibMAAAAAAAAAQ8aJOPHI9fJtAAAAAAAAABhWHJQEAAAAAAAAUFYclAQAAAAAAABQVsEOSk6ZfIE2b3pSW1pX68Ybri3rfIyzIXeTO57cKXYOuZvOaeROsXPI3XROI3eKnUPupnMauVPsHHI3ndPIHWtnIBbmJX4D0JqRDf0WZDIZtW1epYsvuVzZbIfWPLVCM6+6Rm1tW/P6mcXMxzhLbnLTufJ20zmN3Cl2jjV3ip1jzZ1i51hzp9g51twpdo41d4qdY80dQ+euA+2WV5jEnNdwIVe66ePJ9kcr9nkS5EzJSWdM0LZtO7R9+051dnZqyZIHNG3qlLLMxzhLbnKXepbc8cySO55ZcsczS+54Zskdzyy545kldzyz5I5nNvTu1Dkfb/moZEEOStY31GlXdvehz7PtHaqvryvLfIyzIXeTu7y76ZxG7hQ7h9xN5zRyp9g55G46p5E7xc4hd9M5jdwpdg65O8XOQEyGfFDSzB4qYrbf1wp5GXkx8zHOhtxN7vLupnNhsyF307mw2ZC76VzYbMjddC5sNuRuOhc2G3I3nQubDbmbzoXNhtxN58JmQ+5OsTMQk5rB7jSz0we6S9Jpg8zNkjRLkmzEaGUyo95yf3u2Q+Ma6w993tgwVh0de/OMXNx8jLMhd5O7vLvpnEbuFDuH3E3nNHKn2DnkbjqnkTvFziF30zmN3Cl2Drk7xc5ATHKdKble0mxJd77tY7akYwcacvc57j7R3Se+/YCkJK1vbtH48aeoqWmcamtrNWPGdD24bGXeoYuZj3GW3OQu9Sy545kldzyz5I5nltzxzJI7nllyxzNL7nhmyR3PbOjdQCwGPVNSUpukz7t7v8tDmdmuoS7t7u7WddffrBXLF2pEJqP5CxartfX5sszHOEtucpd6ltzxzJI7nllyxzNL7nhmyR3PLLnjmSV3PLPkjmc29O7U9VT85V3wJhvsfQnM7DJJz7n7fxzmvo+5+9JcC2pGNvBsAAAAAAAAGEZdB9r7v/kk9IGGD3Ecqo9/b3+sYp8nuc6U3CWpQ5LM7EhJfylpgqRWSX9b2mgAAAAAAAAAqlGu95T8nqQ3em/fJemdku7o/dq8EuYCAAAAAAAAUKVynSmZcfeu3tsT3f3Nq3GvNrOWEuYCAAAAAAAAUKVyHZTcZGafdvd5kjaa2UR3bzazUyV1liEfAAAAAAAAkBcudBOPXC/fvlrS+Wa2TdJ7JT1lZi9Kmtt7H0Q7nxMAACAASURBVAAAAAAAAAAUZNAzJd39l5I+ZWbHSHpP7/dn3X1vOcIBAAAAAAAAqD65Xr4tSXL31yVtLHEWAAAAAAAAAAnI9fJtAAAAAAAAABhWHJQEAAAAAAAAUFZ5vXwbAAAAAAAAqHTuXH07FkHOlGxsrNcjK+/Xc88+oY0tj+mLX/hswT9jyuQLtHnTk9rSulo33nBt1c+G3E3ueHKn2DnkbjqnkTvFziF30zmN3Cl2DrmbzmnkTrFzyN10TiN3rJ2BWFipjyDXjGzot6Cu7iSNrTtJG1o26eijR2nd2of18cs+o7a2rXn9zEwmo7bNq3TxJZcrm+3QmqdWaOZV1+Q1H+MsuclN58rbTec0cqfYOdbcKXaONXeKnWPNnWLnWHOn2DnW3Cl2jjV3DJ27DrRbXmESc1b9BZwq2cea3U9U7PMkyJmSe/a8pA0tmyRJ+/bt15YtW9VQX5f3/KQzJmjbth3avn2nOjs7tWTJA5o2dUrVzpKb3KWeJXc8s+SOZ5bc8cySO55ZcsczS+54Zskdzyy545kNvRuIxaAHJc3snWb2d2Z2j5ld8bb7vj0cAU4+uVGnve/3tXbdhrxn6hvqtCu7+9Dn2fYO1ed5UDPG2ZC7yV3e3XROI3eKnUPupnMauVPsHHI3ndPInWLnkLvpnEbuFDuH3J1iZyAmuS50M0/SVkn/KukzZvZxSVe4+39JOmugITObJWmWJNmI0cpkRh32+0aNOkpLFs/Vl758i15/fV/eoc36n3ma78vQY5wNuZvc5d1N58JmQ+6mc2GzIXfTubDZkLvpXNhsyN10Lmw25G46FzYbcjedC5sNuZvOhc2G3J1iZ0g94ncVi1wv3/4dd7/J3Ze6+zRJz0h6zMxOGGzI3ee4+0R3nzjQAcmamhrdv3iu7rvvx1q69KGCQrdnOzSusf7Q540NY9XRsbdqZ0PuJnd5d9M5jdwpdg65m85p5E6xc8jddE4jd4qdQ+6mcxq5U+wccneKnYGY5DooeYSZHfoed/8bSXMkPSlp0AOTucydc6fatrygr981p+DZ9c0tGj/+FDU1jVNtba1mzJiuB5etrNpZcpO71LPkjmeW3PHMkjueWXLHM0vueGbJHc8sueOZJXc8s6F3A7HI9fLtByV9SNIjb37B3ReY2V5J3xzq0g+8/wxdNfMyPftcq5rXH/wX6ytfuV0PPfxYXvPd3d267vqbtWL5Qo3IZDR/wWK1tj5ftbPkJnepZ8kdzyy545kldzyz5I5nltzxzJI7nllyxzNL7nhmQ+8GYmGDvS+BmZ0paYu7/9LMjpT0l5ImSGqV9Lfu/stcC2pGNvBifgAAAAAAgGHUdaC9/5tPQpPqz+c4VB/rdv+sYp8nuV6+/T1J+3tv3yXpnZLukPSGDl4EBwAAAAAAAKgIzj9v+aeS5Xr5dsbdu3pvT3T303tvrzazlhLmAgAAAAAAAFClcp0pucnMPt17e6OZTZQkMztVUmdJkwEAAAAAAACoSrkOSl4t6Xwz2ybpvZKeMrMXJc3tvQ8AAAAAAAAACjLoy7d7L2TzKTM7RtJ7er8/6+57yxEOAAAAAAAAQPXJ9Z6SkiR3f13SxhJnAQAAAAAAAIbMvbIv7oLfyvXybQAAAAAAAAAYVhyUBAAAAAAAAFBWHJQEAAAAAAAAUFbBDkpOmXyBNm96UltaV+vGG64t63yMsyF3kzue3Cl2Drk7xc5z59yp3dmNatnwaEFzw7E7xtmQu1PMnWLnkLvpXL7csf7ZG3J3irlT7BxyN53TyB1rZyAWVuo3AK0Z2dBvQSaTUdvmVbr4ksuVzXZozVMrNPOqa9TWtjWvn1nMfIyz5CY3nStvd4qdJencc87Uvn37NW/eXTptwoV5zYTOneJjlWLuFDvHmjvFzsXOx/hnb8jdKeZOsXOsuVPsHGvuGDp3HWi3vMIkZuLYc7nSTR/NHasq9nkS5EzJSWdM0LZtO7R9+051dnZqyZIHNG3qlLLMxzhLbnKXepbc8cyG3r1q9Vq98upreX9/JeRO8bFKMXeKnWPNnWLnYudj/LM35O4Uc6fYOdbcKXaONXesnSH1yPno81HJghyUrG+o067s7kOfZ9s7VF9fV5b5GGdD7iZ3eXfTOY3csXYuVoy/71gfqxRzp9g55G46lzd3MWLtTG46V/JuOqeRO9bOQEwGPShpZnVm9k9mdreZnWBm/9fMnjOzJWY2dqhLzfqfOVrIy8iLmY9xNuRucpd3N50Lmw25O8XOxYrx9x3rY5Vi7hQ7h9xN58Jmh2N+qGLtTO7yzYbcnWLuFDuH3J1iZyAmuc6UnC+pVdIuSY9L+rWkj0haJek7Aw2Z2Swzazaz5p6e/f3ub892aFxj/aHPGxvGqqNjb96hi5mPcTbkbnKXdzed08gda+dixfj7jvWxSjF3ip1D7qZzeXMXI9bO5KZzJe+mcxq5Y+0MxCTXQcl3ufs33f12Sce6+x3uvtPdvynp5IGG3H2Ou09094mZzKh+969vbtH48aeoqWmcamtrNWPGdD24bGXeoYuZj3GW3OQu9Sy545kNvbsYMf6+Y32sUsydYudYc6fYeTjmhyrWzuSmcyXvpnMauWPtDMSkJsf9fQ9afn+Q+wrS3d2t666/WSuWL9SITEbzFyxWa+vzZZmPcZbc5C71LLnjmQ29+9577tb5552tMWOO144Xm3XrbbM1b/6iis6d4mOVYu4UO8eaO8XOxc7H+GdvyN0p5k6xc6y5U+wca+5YO4OXusfEBnuwzOw2SX/v7vve9vXxkm5398tyLagZ2cCzAQAAAAAAYBh1HWjv/+aT0IS6D3Acqo8Ne/69Yp8nuc6UXK7eMyLN7EhJN0k6XQffZ/KzpY0GAAAAAAAAoBrlegn29yS90Xv7LkmjJd3R+7V5JcwFAAAAAAAAoErlfE9Jd+/qvT3R3U/vvb3azFpKmAsAAAAAAABAlcp1UHKTmX3a3edJ2mhmE9292cxOldRZhnwAAAAAAABAXnrEW0rGItfLt6+WdL6ZbZP0XklPmdmLkub23gcAAAAAAAAABRn0TEl3/6WkT5nZMZLe0/v9WXffm++CYi7xw7FtAAAAAAAAoPrkevm2JMndX5e0scRZAAAAAAAAACQg18u3AQAAAAAAAGBYcVASAAAAAAAAQFnl9fJtAAAAAAAAoNI5VyiJRrAzJUePfqcWLZqj5577mZ599gmddeYfFjQ/ZfIF2rzpSW1pXa0bb7i26mdD7iZ3PLlT7BxyN53TyJ1i55C76ZxG7hQ7h9xN5zRyp9g55G46p5E71s5ALMy9tEeQa0c2HHbB9/7l61q9eq2+N+8+1dbW/v/t3Xt03XWZ7/HPs5PdpK1tEcqhTdIrsc4IgxRTrCi2gLaoFHTGU2aGmRFHDmeNKOAwdJyxI96OB0cY0VmytEgpBwbaigr2AhaKYylC20BT6N3ebJOGYkUKtLpIk+f8QSyFptl7J9n7m+/+vl+u31pJ9n76fD7ZlYVf90WDBg3UgQMvveE+x0uWyWS0acNjuvDDf6Xm5lY9+cRS/c3fflqbNv0qZ54YZ8lNbjr3v910TiN3ip1jzZ1i51hzp9g51twpdo41d4qdY82dYudYc8fQ+fCrLZZXmMScMeI9PFXyKM8890S//XsS5JmSQ4a8Re9737s19457JUltbW3HHEh25+xJE7V9+y7t3LlbbW1tWrjwAV08Y3rZzpKb3MWeJXc8s+SOZ5bc8cySO55ZcsczS+54Zskdzyy545kNvRuIRcGHkmb2P3q7dPz4Mdq//7e6/Qff0prVP9P3v/dNDRo0MO/5mtoR2tO898j3zS2tqqkZUbazIXeTu7S76ZxG7hQ7h9xN5zRyp9g55G46p5E7xc4hd9M5jdwpdg65O8XOQEy6PZQ0sxPfdJ0kabWZvdXMTuzp0sqKCk2c+Gf6/vf/nyadPV0HDx7SrFmfyXve7Nhnnub7MvQYZ0PuJndpd9O5sNmQu+lc2GzI3XQubDbkbjoXNhtyN50Lmw25m86FzYbcTefCZkPupnNhsyF3p9gZUoc711FXf5brmZL7JT111NUoqVbS051fd8nMrjSzRjNr7Og4eMztzS2tam5u1eo1ayVJP/rxEk0888/yDt3S3KpRdTVHvq+rHanW1n1lOxtyN7lLu5vOaeROsXPI3XROI3eKnUPupnMauVPsHHI3ndPInWLnkLtT7AzEJNeh5CxJWyRd7O7j3H2cpObOr8cfb8jd57h7g7s3ZDKDj7l9377fqLl5ryZMOFWSdP7579OmTVvzDr2msUn19eM0duwoZbNZzZx5iRYtXla2s+Qmd7FnyR3PLLnjmSV3PLPkjmeW3PHMkjueWXLHM0vueGZD7wZiUdndje5+k5nNl/QtM9sj6QYd/0OxC3Lt5/5N/+/O/9SAAVnt2LlbV1zxj3nPtre365prZ2vpkntUkclo3p0LtHFjfoeaMc6Sm9zFniV3PLPkjmeW3PHMkjueWXLHM0vueGbJHc8sueOZDb0biIUV8J4GMyR9QdJYd8/7HVazA2p7fIjZv1/5DgAAAAAAEMbhV1uOffNJ6PRTJnOcdJT1+57st39Pun2mpJm9W9Imd39J0nJJ50p6xcy+Ienr7n6gBBkBAAAAAACAnJynuEUj13tKzpV0qPPrWyRlJX2p82d3FC8WAAAAAAAAgHLV7TMlJWXc/XDn1w3uflbn1yvNrKmIuQAAAAAAAACUqVzPlFxvZp/s/HqdmTVIkplNkNRW1GQAAAAAAAAAylKuZ0peIenbZjZb0n5JT3R+Cveeztty4pX8AAAAAACURrYi1//M715b++HcdwKAPtDtP606P8jmcjMbIml85/2b3X1fKcIBAAAAAAAAKD95/V8o7v6ypHVFzgIAAAAAAAD0WIfzmt1Y5HpPSQAAAAAAAADoUxxKAgAAAAAAACgpDiUBAAAAAAAAlFSQQ8mqqio98fhiPdX4sNY1PaobvnhdwX/G9GlTtWH9Cm3euFKzrr+q7GdD7iZ3PLlT7BxyN53TyJ1i55C76ZxG7hQ7h9xN5zRyp9g55O5YOtfVjdRDD83X2rXL9dRTD+uqqz4pSfr61/9VTU3LtXr1Q1qw4PsaNmxov8pdDrOhdwNRcPeiXhXZGu/qGnpCvVdka7xq4GhfteopP+e9F3V5v66ubFWdb9u20+snTPbqQWO8ad0GP/2MKWU7S25y07n/7aZzGrlT7Bxr7hQ7x5o7xc6x5k6xc6y5U+wca+5SdK6uHn3kGju2wSdP/rBXV4/24cP/1Ldu3e5nnnmBf+Qjl/ngweO8unq033TTrX7TTbcemeGxiqdzsc9zYr3efnKDc71+hX48uruCvXz74MFDkqRstlKV2azc8/90pLMnTdT27bu0c+dutbW1aeHCB3TxjOllO0tuchd7ltzxzJI7nllyxzNL7nhmyR3PLLnjmSV3PLM9mX/uuefV1LRekvTKKwe1efM21dScouXLH1N7e7skafXqtaqtHdmvcsc+G3o3EItuDyXN7MKjvh5mZreb2TNmdo+ZndKrxZmMGtcsU2vLM1q+fIVWr1mb92xN7Qjtad575PvmllbV1Iwo29mQu8ld2t10TiN3ip1D7qZzGrlT7BxyN53TyJ1i55C76ZxG7pCdR4+u05lnnqY1a5re8PO/+7uZ+tnP/rvf5o5xNvRuIBa5nin59aO+vllSq6QZktZI+v7xhszsSjNrNLPGjo6DXd6no6NDDZOmacy4Bk1qmKjTTnt73qHN7Jif5ftMyxhnQ+4md2l307mw2ZC76VzYbMjddC5sNuRuOhc2G3I3nQubDbmbzoXNhtxN58JmQ+6OsfPgwYN0773f0/XXf0Uvv/zKkZ/PmvUZtbcf1vz5PynK3r6Yj3E29G4gFoW8fLvB3We7+6/d/VuSxh7vju4+x90b3L0hkxnc7R964MBL+sWKX2r6tKl5B2lpbtWoupoj39fVjlRr676ynQ25m9yl3U3nNHKn2DnkbjqnkTvFziF30zmN3Cl2DrmbzmnkDtG5srJS9977PS1YcL8eeOChIz+/7LK/0Ic/fIEuv/yafpk75tnQu4FY5DqU/B9m9o9mdp2kofbG4/oevx/l8OEnHvl0r+rqal1w/rnasmV73vNrGptUXz9OY8eOUjab1cyZl2jR4mVlO0tuchd7ltzxzJI7nllyxzNL7nhmyR3PLLnjmSV3PLM9nf/e9/5dW7Zs03e+84MjP/vgB6fouuv+QR//+Kf0+9//oV/mjnk29O7UdbhzHXX1Z5U5br9N0pDOr++UNFzSb8xshKSm407lMHLkKZp7+y2qqMgok8novvsWacnSR/Keb29v1zXXztbSJfeoIpPRvDsXaOPGrWU7S25yF3uW3PHMkjueWXLHM0vueGbJHc8sueOZJXc8sz2ZP+ecBl122V/o2Wc36cknl0qSbrjhm7r55i+pqmqAFi++W9JrH3Zz9dVf6De5Y58NvRuIhXX3vgRm9m5Jm939gJkNkvR5SRMlbZT0dXc/kGtB5YDa/n0sCwAAAABAmchW5HruUffa2g/3URIU2+FXW45980lowskNnEMdZetvGvvt35NcL8GeK+mPn1Rzi6Shkr4h6ZCkO4qYCwAAAAAAAECZyvV/oWTc/Y//N0mDu5/V+fVKM+vxy7cBAAAAAAAApCvXMyXXm9knO79eZ2YNkmRmEyS1FTUZAAAAAAAAgLKU65mSV0j6tpnNlrRf0hNmtkfSns7bAAAAAAAAgH7BxVtKxqLbQ8nOD7K53MyGSBrfef9md99XinAAAAAAACB/fFANgFjk9bFc7v6ypHVFzgIAAAAAAAAgAbneUxIAAAAAAAAA+hSHkgAAAAAAAABKKq+XbwMAAAAAAAD9XYfzQTexCPJMybq6Gj2y7Id69pn/1rqmR/XZz3yq4D9j+rSp2rB+hTZvXKlZ119V9rMhd5M7ntwpdg65m85p5E6xc8jddE4jd4qdQ+6mcxq5U+wccneKnW+bc7P2Nq9T09rlBc31xe4YZ0PvBqLg7kW9KrI1/uardtSZ3jBpmldka3zYW9/mW7Zu99PPmHLM/Y53ZavqfNu2nV4/YbJXDxrjTes25D0f4yy5yU3n/rebzmnkTrFzrLlT7Bxr7hQ7x5o7xc6x5k6xc6y5Y+1cka3xqed9zBsmTfNn12/KeyZ07nJ/rIp9nhPrNf6kic71+hX68ejuCvJMyeeee15rm9ZLkl555aA2b/6VamtG5D1/9qSJ2r59l3bu3K22tjYtXPiALp4xvWxnyU3uYs+SO55ZcsczS+54Zskdzyy545kldzyz5I5nNvTux1au0gu/ezHv+/eH3Kk+VkAsCj6UNLOT+jLAmDF1OvOdp2vV6rV5z9TUjtCe5r1Hvm9uaVVNnoeaMc6G3E3u0u6mcxq5U+wccjed08idYueQu+mcRu4UO4fcTec0csfaubdi/H2n+lgBpdTtoaSZ3Whmwzu/bjCzHZJWmdmvzWxKb5cPHjxICxfcpn/8pxv08suv5D1nZsf8zD2/NzKNcTbkbnKXdjedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu+lc2GzI3XQubDbk7hQ791aMv+9UH6ty4PznDf/pz3I9U/Ij7r6/8+tvSrrU3eslfVDSzccbMrMrzazRzBo7Og52eZ/Kykr9cMFtuvfen+j++x8sKHRLc6tG1dUc+b6udqRaW/eV7WzI3eQu7W46p5E7xc4hd9M5jdwpdg65m85p5E6xc8jddE4jd6ydeyvG33eqjxVQSrkOJbNmVtn59UB3XyNJ7r5VUtXxhtx9jrs3uHtDJjO4y/vcNudmbdq8Tbd8e07Bodc0Nqm+fpzGjh2lbDarmTMv0aLFy8p2ltzkLvYsueOZJXc8s+SOZ5bc8cySO55ZcsczS+54ZkPv7o0Yf9+pPlZAKVXmuP27kpaa2Y2SHjKzWyT9WNIFkpp6uvS950zS3/7Nx/XMsxvVuOa1/2L927/dqAcfejSv+fb2dl1z7WwtXXKPKjIZzbtzgTZu3Fq2s+Qmd7FnyR3PLLnjmSV3PLPkjmeW3PHMkjueWXLHMxt69913fVdT3v8eDR9+onbtaNSXv3KT7pg3v1/nTvWxAmJhud6XwMymSvoHSRP02iHmHkn3S7rD3dtyLagcUNu/X8AOAAAAAAAQmcOvthz75pPQ+OETOYc6yo79a/vt35NcH3TzbklPu/ulkt4r6SeSOiSdKmlQ8eMBAAAAAAAAKDe5Xr49V9I7O7++RdJBSTfqtZdv3yHpz4sXDQAAAAAAAMife0foCMhTrkPJjLsf7vy6wd3P6vx6pZn1+D0lAQAAAAAAAKQr16dvrzezT3Z+vc7MGiTJzCZIyvl+kgAAAAAAAADwZrkOJa+QNMXMtkt6h6QnzGyHpNs6bwMAAAAAAACAgnT78m13PyDpcjMbIml85/2b3X1fKcIBAAAAAIA49OYjfvm4ZCA9ud5TUpLk7i9LWlfkLAAAAAAAAECPdXDEHY1cL98GAAAAAAAAgD7FoSQAAAAAAACAkuJQEgAAAAAAAEBJBTmUrKqq0hOPL9ZTjQ9rXdOjuuGL1xX8Z0yfNlUb1q/Q5o0rNev6q8p+NuRucseTO8XOIXfTOY3cKXYOuZvOaeROsXPI3XROI3eKnUPupnNhsxMmnKrGNcuOXL/dv1lXf/aKfp871scKiIa7F/WqyNZ4V9fQE+q9IlvjVQNH+6pVT/k5772oy/t1dWWr6nzbtp1eP2GyVw8a403rNvjpZ0wp21lyk5vO/W83ndPInWLnWHOn2DnW3Cl2jjV3ip1jzZ1i51hzl3vnyjyuAVV13tq6z8efOukNP4+1c8jdxT7PifUa9dbTnev1K/Tj0d0V7OXbBw8ekiRls5WqzGblnv+nI509aaK2b9+lnTt3q62tTQsXPqCLZ0wv21lyk7vYs+SOZ5bc8cySO55ZcsczS+54Zskdzyy545mNOffRzj//fdqx49favbulX+eO9bECYhLsUDKTyahxzTK1tjyj5ctXaPWatXnP1tSO0J7mvUe+b25pVU3NiLKdDbmb3KXdTec0cqfYOeRuOqeRO8XOIXfTOY3cKXYOuZvOaeROsfObXTrzEi1YcH/e94+1c3/5fQP9WbeHkmb2tJnNNrNTC/lDzexKM2s0s8aOjoNd3qejo0MNk6ZpzLgGTWqYqNNOe3shf/4xP8v3mZYxzobcTe7S7qZzYbMhd9O5sNmQu+lc2GzI3XQubDbkbjoXNhtyN50Lmw25m86FzYbcTefCZo+WzWZ10UXTdN+PFuc9E2vn/vD7Bvq7XM+UfKukEyT93MxWm9nnzKwm1x/q7nPcvcHdGzKZwd3e98CBl/SLFb/U9GlT8w7d0tyqUXWvx6irHanW1n1lOxtyN7lLu5vOaeROsXPI3XROI3eKnUPupnMauVPsHHI3ndPInWLno1144Xlau/ZZPf/8/rxnYu3cH37fQH+X61Dyd+7+T+4+WtJ1kt4m6Wkz+7mZXdnTpcOHn6hhw4ZKkqqrq3XB+edqy5btec+vaWxSff04jR07StlsVjNnXqJFi5eV7Sy5yV3sWXLHM0vueGbJHc8sueOZJXc8s+SOZ5bc8czGnPuPLr30owW9dDtk7lgfK0gdcq6jrv6sMsftR54z7O6PSXrMzD4r6YOSLpU0pydLR448RXNvv0UVFRllMhndd98iLVn6SN7z7e3tuuba2Vq65B5VZDKad+cCbdy4tWxnyU3uYs+SO55ZcsczS+54Zskdzyy545kldzyz5I5nNubckjRwYLU+cMH79elP/3NBc7F2Dv37BmJg3b0vgZnNd/e/7M2CygG1/ftYFgAAAAAA9Nqx74SYPw4OCnf41Zbe/MrLVt2Jp/PX6SjNL6zvt39Pcr18+1tmNlSSzGygmX3FzBaZ2TfMbFgJ8gEAAAAAAAAoM7kOJedKOtT59bclDZX0jc6f3VHEXAAAAAAAAADKVK73lMy4++HOrxvc/azOr1eaWVMRcwEAAAAAAAAoU7kOJdeb2Sfd/Q5J68yswd0bzWyCpLYS5AMAAAAAAADy0t1np6B/yXUoeYWkb5vZbEn7JT1hZnsk7em8LaeM9fz9NDv4iwQAAAAAQBR687/ghw8a2qvd+w+91Kt5AKXX7aGkux+QdLmZDZE0vvP+ze6+rxThAAAAAAAAAJSfXM+UlCS5+8uS1hU5CwAAAAAAAIAE5Pr0bQAAAAAAAADoU3k9UxIAAAAAAADo7/h8kngEe6bkZz7zKa19+hE1rV2uz372UwXPT582VRvWr9DmjSs16/qrSjJbVVWlJx5frKcaH9a6pkd1wxevK1nm3s6Hmg25O8XcKXYOuZvOaeROsXPI3XROI/dtc27W3uZ1alq7vKC5vtjNY0Xn/rybzmnkTrFzT+aHDhuiH9x5ix5bvUQrVi3WuyadqVlfuFqPPn6/Hnnsx5r/4x/olBEnFzV3rI8VEA13L+qVHVDrb77OPPN8X79+kw8ddqpXDxztjyxf4X/6jvcdc7+KbE2XV7aqzrdt2+n1EyZ79aAx3rRug59+xpTj3r+vZiuyNT70hHqvyNZ41cDRvmrVU37Oey8qyd5QnckdT+4UO8eaO8XOseZOsXOsuVPsHHPuqed9zBsmTfNn12/KeyZ07hQfqxQ7x5o7xc6x5k6xc77zpwz7kzdcC+75iX/uM7P9lGF/4nXD/8zfNnqSn1r3riO3/+usr/m82+898n2MnXs7W+zznFivEcP+1Llev0I/Ht1dQZ4p+Sd/Uq9Vq9bq97//g9rb2/XYiid1ySUX5j1/9qSJ2r59l3bu3K22tjYtXPiALp4xveizknTw4CFJUjZbqcpsVu75PS24t3tDdSZ3PLlT7Bxr7hQ7x5o7xc6x5k6xc8y5H1u5Si/87sW8798fcqf4WKXYOdbcKXaONXeKnXsy/5YhgzX5nAbdFPeRlAAAGdZJREFUc9d9kqS2tja9dOBlvfLywSP3GTRooJTjf47H1LkvdwOxCHIouWHjFp177rt14oknaODAal144fmqq6vJe76mdoT2NO898n1zS6tqakYUfVaSMpmMGtcsU2vLM1q+fIVWr1lbkr2hOpO7tLvpnEbuFDuH3E3nNHKn2Dnk7t7m7o1YO8eYO8XOIXfTOY3cKXbuyfyYsaP02/0v6Nu3fl0Pr/iRbv7OV187hJT0+dnX6Kn1j+ov/ucM/fvXv1O03LE+VkBMuj2UNLMGM/u5md1tZqPM7GEzO2Bma8xsYk+Xbt68Td+86VY9uPReLV50t555dqMOHz6c97yZHfOzfJ+x2JtZSero6FDDpGkaM65Bkxom6rTT3l6SvaE6k7u0u+lc2GzI3XQubDbkbjoXNhtyN50Lmw25u7e5eyPWzjHmTrFzyN10Lmw25G46Fzbbk/nKigr92TvfoXm3z9cH3/8XOnTokD7zuf8lSbrxa9/Wu04/Xz/64SL9/ZWXFS13rI8VJOc/b/hPf5brmZK3Svp3SUsk/VLS9919mKTPd97WJTO70swazayxo/1gl/eZN2++3j35Q7rgAx/X7154Udu27cw7dEtzq0Yd9czKutqRam3dV/TZox048JJ+seKXmj5takn2hupM7tLupnMauVPsHHI3ndPInWLnkLv76t+neiLWzjHmTrFzyN10TiN3ip17Mr937z617t2ntU89I0la/MAynXHGO95wn5/ct0QfmTGtaLljfayAmOQ6lMy6+4Pufq8kd/f79NoXyyVVH2/I3ee4e4O7N2QqBnd5n5NPPkmSNGpUjT760Q9pwYIH8g69prFJ9fXjNHbsKGWzWc2ceYkWLV5W9Nnhw0/UsGFDJUnV1dW64PxztWXL9qLv7e18qFlyxzNL7nhmyR3PLLnjmSV36XP3RqydY8ydYudYc6fYOdbcKXbuyfxvnt+vluZWnVo/VpJ07pTJ2rplm8aNH3PkPtM/dJ62/WpH0XLH+lgBManMcfsfzGyapGGS3Mw+6u73m9kUSe29Wbxg/hyddNJb1dZ2WFdf8wW9+OKBvGfb29t1zbWztXTJParIZDTvzgXauHFr0WdHjjxFc2+/RRUVGWUyGd133yItWfpI0ff2dj7ULLnjmSV3PLPkjmeW3PHMkrv0ue++67ua8v73aPjwE7VrR6O+/JWbdMe8+f06d4qPVYqdY82dYudYc6fYuafzX/jn/6Nbb/umsgOy+vWuPbr201/Qzf/5VdXXj1OHd6h5z17N+tyXipY71scKiIl1974EZvZOvfby7Q5Jn5P0D5L+TtJeSVe6++O5FgyoquvxC9g7eM8EAAAAAADK3vBBQ3s1v//QS32UJB6HX2059s0noREn/CmHSUd57sVN/fbvSa5nSlZLmunuB8xsoKQDkh6XtEHS+mKHAwAAAAAAAFB+ch1KzpX0zs6vvy3poKQbJV0g6Q5Jf168aAAAAAAAAED++KTyeOQ6lMy4++HOrxvc/azOr1eaWVMRcwEAAAAAAAAoU7k+fXu9mX2y8+t1ZtYgSWY2QVJbUZMBAAAAAAAAKEu5DiWvkDTFzLZLeoekJ8xsh6TbOm8DAAAAAAAAgIJ0+/Jtdz8g6XIzGyJpfOf9m919X74L+ARtAAAAAADQnd5+enZvPl6YUwsgjFzvKSlJcveXJa0rchYAAAAAAACgxzo4Zo5GrpdvAwAAAAAAAECf4lASAAAAAAAAQElxKAkAAAAAAACgpIIcSlZVVemJxxfrqcaHta7pUd3wxesK/jOmT5uqDetXaPPGlZp1/VVlPxtyN7njyZ1i55C76ZxG7hQ7h9xN5zRyp9g55G46p5E7xc4hd9M5ntzXXP2/1NT0qNauXa677vquqqqqSrK3t/O93Q1Ewd2LelVka7yra+gJ9V6RrfGqgaN91aqn/Jz3XtTl/bq6slV1vm3bTq+fMNmrB43xpnUb/PQzppTtLLnJTef+t5vOaeROsXOsuVPsHGvuFDvHmjvFzrHmTrFzrLlT7Fyq3ZVdXKPHnOU7dvza3zJkvFdma3zhD3/qf//31x5zv1g7F/s8J9brpCFvc67Xr9CPR3dXsJdvHzx4SJKUzVaqMpuVe/6fjnT2pInavn2Xdu7crba2Ni1c+IAunjG9bGfJTe5iz5I7nllyxzNL7nhmyR3PLLnjmSV3PLPkjmeW3D3bXVlZqYEDq1VRUaFBAwdqb+tzJdkbsjMQi2CHkplMRo1rlqm15RktX75Cq9eszXu2pnaE9jTvPfJ9c0urampGlO1syN3kLu1uOqeRO8XOIXfTOY3cKXYOuZvOaeROsXPI3XROI3eKnUPu3rv3OX3rW9/Tju2rtWf3Wr300kt65JEVRd/b2/ne7gZi0e2hpJm9xcy+YmYbzOyAmf3GzJ40s8t7u7ijo0MNk6ZpzLgGTWqYqNNOe3ves2Z2zM/yfaZljLMhd5O7tLvpXNhsyN10Lmw25G46FzYbcjedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu084YZhmzJiut02YrNFjztKgwYP013/950Xf29v53u4GYpHrmZL/JWmHpOmSvizpO5L+VtJ5Zvb14w2Z2ZVm1mhmjR0dB7tdcODAS/rFil9q+rSpeYduaW7VqLqaI9/X1Y5Ua+u+sp0NuZvcpd1N5zRyp9g55G46p5E7xc4hd9M5jdwpdg65m85p5E6xc8jdF1xwrnbt2q39+1/Q4cOHdf/9D+o9kxuKvre3873dDcQi16HkWHef5+7N7v4fki52919J+qSk4/7fC+4+x90b3L0hkxl8zO3Dh5+oYcOGSpKqq6t1wfnnasuW7XmHXtPYpPr6cRo7dpSy2axmzrxEixYvK9tZcpO72LPkjmeW3PHMkjueWXLHM0vueGbJHc8sueOZJXfhs3t2t+jsd5+lgQOrJUnnn/c+bd78q6Lv7e18b3cDsajMcftBM3ufu680sxmSXpAkd++wrp5PnKeRI0/R3NtvUUVFRplMRvfdt0hLlj6S93x7e7uuuXa2li65RxWZjObduUAbN24t21lyk7vYs+SOZ5bc8cySO55ZcsczS+54Zskdzyy545kld+Gzq9es1Y9/vESrV/9Mhw8f1rqmDbrtB/9V9L29ne/t7tR18FL3aFh370tgZmdI+oGkCZLWS/p7d99qZidL+it3/06uBZUDavnbAAAAAAAAiqbHz5qSFOuhxeFXW3pTu2ydOORtsT6kRfHCy7/qt39Pcj1TcqCkD7r7ATMbJOmfzewsSRslHfc9JQEAAAAAAADgeHK9p+RcSX/8pJpbJA2T9A1JhyTdUcRcAAAAAAAAAMpUrmdKZtz9cOfXDe5+VufXK82sqYi5AAAAAAAAAJSpXIeS683sk+5+h6R1Ztbg7o1mNkFSWwnyAQAAAAAAAHnp7rNT0L/kevn2FZKmmNl2Se+Q9ISZ7ZB0W+dtAAAAAAAAQXkvLuvFBaDnun2mpLsfkHS5mQ2RNL7z/s3uvq8U4QAAAAAAAACUn1wv35YkufvLktYVOQsAAAAAAACABOR6+TYAAAAAAAAA9Km8nikJAAAAAAAA9Hcd4oNuYsEzJQEAAAAAAACUVJBDyaqqKj3x+GI91fiw1jU9qhu+eF3Bf8b0aVO1Yf0Kbd64UrOuv6rsZ0PuJnc8uVPsHHJ3ip1vm3Oz9javU9Pa5QXN9cXuGGdD7k4xd4qdQ+6mcxq5U+wccjed08idYueQu3ub+1dbn9Tapx9R45plevKJpSXb3dvcQBTcvahXRbbGu7qGnlDvFdkarxo42letesrPee9FXd6vqytbVefbtu30+gmTvXrQGG9at8FPP2NK2c6Sm9x07n+7U+xcka3xqed9zBsmTfNn12/KeyZ07hQfqxRzp9g51twpdo41d4qdY82dYudYc6fYOYbcld1cO3fu9lNGnHbc20PmLvZ5TqzX0MHjnev1K/Tj0d0V7OXbBw8ekiRls5WqzGblnv9r/s+eNFHbt+/Szp271dbWpoULH9DFM6aX7Sy5yV3sWXLHMxt692MrV+mF372Y9/37Q+4UH6sUc6fYOdbcKXaONXeKnWPNnWLnWHOn2Dnm3L0Ra26glLo9lDSzYWZ2o5ltNrPfdl6bOn92Qq8WZzJqXLNMrS3PaPnyFVq9Zm3eszW1I7Snee+R75tbWlVTM6JsZ0PuJndpd9M5jdyxdu6tGH/fsT5WKeZOsXPI3XROI3eKnUPupnMauVPsHHJ3X/y7r7vrwaX3atWTD+qKT12W91zo3EAMcn369kJJj0qa6u7PSZKZjZD0CUk/lPTBrobM7EpJV0qSVQxTJjP4mPt0dHSoYdI0DRs2VD/64e067bS3a8OGLXmFNrNjfpbvMy1jnA25m9yl3U3nwmZD7k6xc2/F+PuO9bFKMXeKnUPupnNhsyF307mw2ZC76VzYbMjddC5sNuTuvvh33ylTP6rW1n06+eST9NCD87V5yzatXLmqqLtD/jt7OeB3FY9cL98e6+7f+OOBpCS5+3Pu/g1Jo4835O5z3L3B3Ru6OpA82oEDL+kXK36p6dOm5h26pblVo+pqjnxfVztSra37ynY25G5yl3Y3ndPIHWvn3orx9x3rY5Vi7hQ7h9xN5zRyp9g55G46p5E7xc4hd/fFv/v+8f6/+c1vdf8DD2rSpDOLvjvkv7MDpZTrUPLXZjbLzE754w/M7BQz+2dJe3q6dPjwEzVs2FBJUnV1tS44/1xt2bI97/k1jU2qrx+nsWNHKZvNaubMS7Ro8bKynSU3uYs9S+54ZkPv7o0Yf9+xPlYp5k6xc6y5U+wca+4UO8eaO8XOseZOsXPMuQcNGqi3vGXwka8/+IEpeb/CM9Z/ZwdKKdfLty+V9HlJv+g8mHRJ+yT9VNLMni4dOfIUzb39FlVUZJTJZHTffYu0ZOkjec+3t7frmmtna+mSe1SRyWjenQu0cePWsp0lN7mLPUvueGZD7777ru9qyvvfo+HDT9SuHY368ldu0h3z5vfr3Ck+VinmTrFzrLlT7Bxr7hQ7x5o7xc6x5k6xc8y5TznlZN33w9slSRWVFZo//34tW/bf/T43EAvr7rX2Zna1pJ+4e4+fFVk5oJYX8wMAAAAAgH7p2HdwzF/IA4/Dr7b0JnrZGjp4POdQR3np4I5++/ck16HkAUkHJW2XdI+kH7r7/kIWcCgJAAAAAAD6Kw4ly8tbBo3jHOoorxza2W//nuR6T8kdkuokfVVSg6RNZvaQmX3CzIYUPR0AAAAAAACAspPrUNLdvcPdl7n7pyTVSLpV0oV67cASAAAAAAAAAAqS64Nu3vAUT3dv02sfcvNTMxtYtFQAAAAAAAAAylY+n77dJXf/fR9nAQAAAAAAKCnegBAIo9tDSXfnM+cBAAAAAAAQBeeYORq53lMSAAAAAAAAAPoUh5IAAAAAAAAASopDSQAAAAAAAAAlFexQ8rY5N2tv8zo1rV3eo/np06Zqw/oV2rxxpWZdf1XZz4bcTe54csfamX8exPNYpZg7xc4hd9M5ndyZTEZrVv9MD/zkzoJnY+0cY+4UO4fcTec0cqfYOeTuFDsD0XD3ol4V2Rrv6pp63se8YdI0f3b9pi5v7+7KVtX5tm07vX7CZK8eNMab1m3w08+YUraz5CZ3OXeuyPLPg1geqxRzp9g51twpdo45d0W2xq/7py/5Pff+2BcvfriguVg7x5g7xc6x5k6xc6y5U+wca+4YOhf7PCfWq7p6tHO9foV+PLq7gj1T8rGVq/TC717s0ezZkyZq+/Zd2rlzt9ra2rRw4QO6eMb0sp0lN7mLPRt6N/88iOOxSjF3ip1jzZ1i55hz19aO1Ic/dIHmzr0375nQuVN8rFLsHGvuFDvHmjvFzrHmjrUzEJMo31OypnaE9jTvPfJ9c0urampGlO1syN3kLu3uFDv3Voy/71gfqxRzp9g55G46p5P7P27+sj7/L19TR0dH3jN9sZvHis79eTed08idYueQu1PsDMSkx4eSZvZgXwYpcPcxP3P3sp0NuZvcpd2dYufeivH3HetjlWLuFDuH3E3nwmZD7u7N7Ec+/AE9//x+Pb322bzu35e7eaxKNxtyd4q5U+wccjedC5sNuTvFzkBMKru70czOOt5Nks7sZu5KSVdKklUMUyYzuMcBu9LS3KpRdTVHvq+rHanW1n1lOxtyN7lLuzvFzr0V4+871scqxdwpdg65m85p5D7nnAbNuGiaPnTh+aqurtLQoUN057zv6BOXX92vc6f4WKXYOeRuOqeRO8XOIXen2BmISa5nSq6RdJOkm9903STphOMNufscd29w94a+PpCUpDWNTaqvH6exY0cpm81q5sxLtGjxsrKdJTe5iz0bendvxPj7jvWxSjF3ip1jzZ1i51hzf2H2jRo7vkH1Eybrsr/5tH7+88fzPpAMmTvFxyrFzrHmTrFzrLlT7Bxr7lg7AzHp9pmSkjZJ+t/u/qs332Bme3qz+O67vqsp73+Phg8/Ubt2NOrLX7lJd8ybn9dse3u7rrl2tpYuuUcVmYzm3blAGzduLdtZcpO72LOhd/PPgzgeqxRzp9g51twpdo45d2/E2jnG3Cl2jjV3ip1jzZ1i51hzx9oZvNQ9Jtbdg2VmH5f0rLtv6eK2j7r7/bkWVA6o5W8DAAAAAABAHzr8asuxbz4JVVeP5hzqKH/4w+5++/ck18u3ayQd6uqGfA4kAQAAAAAAAODNch1KflXSKjN7zMw+bWYnlyIUAAAAAAAAgPKV61Byh6Q6vXY4+S5JG83sITP7hJkNKXo6AAAAAAAAAGUn1wfduLt3SFomaZmZZSV9SNJf6bVP4OaZkwAAAAAAAOgXXLylZCxyHUq+4c0w3b1N0k8l/dTMBhYtFQAAAAAAAICylevl25ce7wZ3/30fZwEAAAAAAACQgG4PJd19a6mCAAAAAAAAAEhDrmdKAgAAAAAAAECfyvWekgAAAAAAAEAU3Pmgm1jwTEkAAAAAAAAAJRXsUPK2OTdrb/M6Na1d3qP56dOmasP6Fdq8caVmXX9V2c+G3E3ueHKn2DnkbjqnkTvFziF30zmN3Cl2DrmbzmnkTrFzyN10TiN3yM6SlMlktGb1z/TAT+4seBaIgrsX9arI1nhX19TzPuYNk6b5s+s3dXl7d1e2qs63bdvp9RMme/WgMd60boOffsaUsp0lN7np3P920zmN3Cl2jjV3ip1jzZ1i51hzp9g51twpdo41d4qdY80dsvMfr+v+6Ut+z70/9sWLH+7y9mKf58R6ZQfUOtfrV+jHo7sr2DMlH1u5Si/87sUezZ49aaK2b9+lnTt3q62tTQsXPqCLZ0wv21lyk7vYs+SOZ5bc8cySO55ZcsczS+54Zskdzyy545kldzyzfTFfWztSH/7QBZo79968Z4DYRPmekjW1I7Snee+R75tbWlVTM6JsZ0PuJndpd9M5jdwpdg65m85p5E6xc8jddE4jd4qdQ+6mcxq5U+wccnesnSXpP27+sj7/L19TR0dH3jNAbLo9lDSzoWb2f83sLjP76zfddms3c1eaWaOZNXZ0HOyrrEf/+cf8zD2/T1eKcTbkbnKXdjedC5sNuZvOhc2G3E3nwmZD7qZzYbMhd9O5sNmQu+lc2GzI3XQubDbkbjoXNhtyd6ydP/LhD+j55/fr6bXP5r0Prwv9kuT+dvVnuZ4peYckk/QjSX9pZj8ys6rO2yYfb8jd57h7g7s3ZDKD+yjq61qaWzWqrubI93W1I9Xauq9sZ0PuJndpd9M5jdwpdg65m85p5E6xc8jddE4jd4qdQ+6mcxq5U+wccnesnc85p0EzLpqmbVuf1H/dfavOO++9unPed/LeDcQi16Hkqe7+eXe/390vlvS0pEfN7KQSZDuuNY1Nqq8fp7FjRymbzWrmzEu0aPGysp0lN7mLPUvueGbJHc8sueOZJXc8s+SOZ5bc8cySO55Zcscz29v5L8y+UWPHN6h+wmRd9jef1s9//rg+cfnVee8GYlGZ4/YqM8u4e4ckufv/MbNmSSskvaU3i+++67ua8v73aPjwE7VrR6O+/JWbdMe8+XnNtre365prZ2vpkntUkclo3p0LtHHj1rKdJTe5iz1L7nhmyR3PLLnjmSV3PLPkjmeW3PHMkjueWXLHM9sX80AKrLvXl5vZv0ta5u6PvOnnF0r6T3d/W64FlQNq+/cL2AEAAAAAACJz+NWWY9+4EspyDvUGbf3470muZ0o2S9ry5h+6+0OSch5IAgAAAAAAAKXCiWQ8cr2n5FclrTKzx8zs02Z2cilCAQAAAAAAACg+M7vQzLaY2TYz+3yp9uY6lNwhqU6vHU6+S9JGM3vIzD5hZkOKng4AAAAAAABAUZhZhaTvSvqQpHdI+isze0cpduc6lHR373D3Ze7+KUk1km6VdKFeO7AEAAAAAAAAEKezJW1z9x3u/qqk+ZIuKcXiXO8p+YY3w3T3Nkk/lfRTMxtYtFQAAAAAAAAAiq1W0p6jvm+W9O5SLM51KHnp8W5w99/ns4BPgwIAAAAAAEApcA71RmZ2paQrj/rRHHefc/RduhgryecFdXso6e5bSxECAAAAAAAAQN/qPICc081dmiWNOur7Okl7ixqqU673lAQAAAAAAABQntZIepuZjTOzAZL+Uq+9dWPR5Xr5NgAAAAAAAIAy5O6Hzewzkn4mqULSXHffUIrd5l6Sl4kDAAAAAAAAgCRevg0AAAAAAACgxDiUBAAAAAAAAFBSHEoCAAAAAAAAKCkOJQEAAAAAAACUFIeSAAAAAAAAAEqKQ0kAAAAAAAAAJcWhJAAAAAAAAICS4lASAAAAAAAAQEn9fymVlo281J4BAAAAAElFTkSuQmCC\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "import seaborn as sb\n", - "import pandas as pd\n", - "\n", - "cm_plt = pd.DataFrame(cm[:73])\n", - "\n", - "plt.figure(figsize = (25, 25))\n", - "ax = plt.axes()\n", - "\n", - "sb.heatmap(cm_plt, annot=True)\n", - "\n", - "ax.xaxis.set_ticks_position('top')\n", - "\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Pipeline" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, I took the data from [Coconut - Wikipedia](https://en.wikipedia.org/wiki/Coconut) to check if the classifier is able to **correctly** predict the label(s) or not.\n", - "\n", - "And here is the output:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Example labels: [('coconut', 'oilseed')]\n" - ] - } - ], - "source": [ - "example_text = '''The coconut tree (Cocos nucifera) is a member of the family Arecaceae (palm family) and the only species of the genus Cocos.\n", - "The term coconut can refer to the whole coconut palm or the seed, or the fruit, which, botanically, is a drupe, not a nut.\n", - "The spelling cocoanut is an archaic form of the word.\n", - "The term is derived from the 16th-century Portuguese and Spanish word coco meaning \"head\" or \"skull\", from the three indentations on the coconut shell that resemble facial features.\n", - "Coconuts are known for their versatility ranging from food to cosmetics.\n", - "They form a regular part of the diets of many people in the tropics and subtropics.\n", - "Coconuts are distinct from other fruits for their endosperm containing a large quantity of water (also called \"milk\"), and when immature, may be harvested for the potable coconut water.\n", - "When mature, they can be used as seed nuts or processed for oil, charcoal from the hard shell, and coir from the fibrous husk.\n", - "When dried, the coconut flesh is called copra.\n", - "The oil and milk derived from it are commonly used in cooking and frying, as well as in soaps and cosmetics.\n", - "The husks and leaves can be used as material to make a variety of products for furnishing and decorating.\n", - "The coconut also has cultural and religious significance in certain societies, particularly in India, where it is used in Hindu rituals.'''\n", - "\n", - "example_preds = classifier.predict(vectorizer.transform([example_text]))\n", - "example_labels = mlb.inverse_transform(example_preds)\n", - "print(\"Example labels: {}\".format(example_labels))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/machine_learning/scoring_functions.py b/machine_learning/scoring_functions.py index a2d97b09ded2..2b891d4eb9d5 100755 --- a/machine_learning/scoring_functions.py +++ b/machine_learning/scoring_functions.py @@ -10,12 +10,22 @@ even log is used. Using log and roots can be perceived as tools for penalizing big - erors. However, using appropriate metrics depends on the situations, + errors. However, using appropriate metrics depends on the situations, and types of data """ -#Mean Absolute Error +# Mean Absolute Error def mae(predict, actual): + """ + Examples(rounded for precision): + >>> actual = [1,2,3];predict = [1,4,3] + >>> np.around(mae(predict,actual),decimals = 2) + 0.67 + + >>> actual = [1,1,1];predict = [1,1,1] + >>> mae(predict,actual) + 0.0 + """ predict = np.array(predict) actual = np.array(actual) @@ -24,8 +34,19 @@ def mae(predict, actual): return score -#Mean Squared Error + +# Mean Squared Error def mse(predict, actual): + """ + Examples(rounded for precision): + >>> actual = [1,2,3];predict = [1,4,3] + >>> np.around(mse(predict,actual),decimals = 2) + 1.33 + + >>> actual = [1,1,1];predict = [1,1,1] + >>> mse(predict,actual) + 0.0 + """ predict = np.array(predict) actual = np.array(actual) @@ -35,8 +56,19 @@ def mse(predict, actual): score = square_diff.mean() return score -#Root Mean Squared Error + +# Root Mean Squared Error def rmse(predict, actual): + """ + Examples(rounded for precision): + >>> actual = [1,2,3];predict = [1,4,3] + >>> np.around(rmse(predict,actual),decimals = 2) + 1.15 + + >>> actual = [1,1,1];predict = [1,1,1] + >>> rmse(predict,actual) + 0.0 + """ predict = np.array(predict) actual = np.array(actual) @@ -46,13 +78,24 @@ def rmse(predict, actual): score = np.sqrt(mean_square_diff) return score -#Root Mean Square Logarithmic Error + +# Root Mean Square Logarithmic Error def rmsle(predict, actual): + """ + Examples(rounded for precision): + >>> actual = [10,10,30];predict = [10,2,30] + >>> np.around(rmsle(predict,actual),decimals = 2) + 0.75 + + >>> actual = [1,1,1];predict = [1,1,1] + >>> rmsle(predict,actual) + 0.0 + """ predict = np.array(predict) actual = np.array(actual) - log_predict = np.log(predict+1) - log_actual = np.log(actual+1) + log_predict = np.log(predict + 1) + log_actual = np.log(actual + 1) difference = log_predict - log_actual square_diff = np.square(difference) @@ -62,17 +105,32 @@ def rmsle(predict, actual): return score -#Mean Bias Deviation + +# Mean Bias Deviation def mbd(predict, actual): + """ + This value is Negative, if the model underpredicts, + positive, if it overpredicts. + + Example(rounded for precision): + + Here the model overpredicts + >>> actual = [1,2,3];predict = [2,3,4] + >>> np.around(mbd(predict,actual),decimals = 2) + 50.0 + + Here the model underpredicts + >>> actual = [1,2,3];predict = [0,1,1] + >>> np.around(mbd(predict,actual),decimals = 2) + -66.67 + """ predict = np.array(predict) actual = np.array(actual) difference = predict - actual - numerator = np.sum(difference) / len(predict) - denumerator = np.sum(actual) / len(predict) - print(numerator) - print(denumerator) - + numerator = np.sum(difference) / len(predict) + denumerator = np.sum(actual) / len(predict) + # print(numerator, denumerator) score = float(numerator) / denumerator * 100 return score diff --git a/machine_learning/sequential_minimum_optimization.py b/machine_learning/sequential_minimum_optimization.py new file mode 100644 index 000000000000..a0b99a788cbd --- /dev/null +++ b/machine_learning/sequential_minimum_optimization.py @@ -0,0 +1,626 @@ +""" + Implementation of sequential minimal optimization (SMO) for support vector machines + (SVM). + + Sequential minimal optimization (SMO) is an algorithm for solving the quadratic + programming (QP) problem that arises during the training of support vector + machines. + It was invented by John Platt in 1998. + +Input: + 0: type: numpy.ndarray. + 1: first column of ndarray must be tags of samples, must be 1 or -1. + 2: rows of ndarray represent samples. + +Usage: + Command: + python3 sequential_minimum_optimization.py + Code: + from sequential_minimum_optimization import SmoSVM, Kernel + + kernel = Kernel(kernel='poly', degree=3., coef0=1., gamma=0.5) + init_alphas = np.zeros(train.shape[0]) + SVM = SmoSVM(train=train, alpha_list=init_alphas, kernel_func=kernel, cost=0.4, + b=0.0, tolerance=0.001) + SVM.fit() + predict = SVM.predict(test_samples) + +Reference: + https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/smo-book.pdf + https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-98-14.pdf + http://web.cs.iastate.edu/~honavar/smo-svm.pdf +""" + + +import os +import sys +import urllib.request + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from sklearn.datasets import make_blobs, make_circles +from sklearn.preprocessing import StandardScaler + +CANCER_DATASET_URL = "http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data" + + +class SmoSVM: + def __init__( + self, + train, + kernel_func, + alpha_list=None, + cost=0.4, + b=0.0, + tolerance=0.001, + auto_norm=True, + ): + self._init = True + self._auto_norm = auto_norm + self._c = np.float64(cost) + self._b = np.float64(b) + self._tol = np.float64(tolerance) if tolerance > 0.0001 else np.float64(0.001) + + self.tags = train[:, 0] + self.samples = self._norm(train[:, 1:]) if self._auto_norm else train[:, 1:] + self.alphas = alpha_list if alpha_list is not None else np.zeros(train.shape[0]) + self.Kernel = kernel_func + + self._eps = 0.001 + self._all_samples = list(range(self.length)) + self._K_matrix = self._calculate_k_matrix() + self._error = np.zeros(self.length) + self._unbound = [] + + self.choose_alpha = self._choose_alphas() + + # Calculate alphas using SMO algorithm + def fit(self): + K = self._k + state = None + while True: + + # 1: Find alpha1, alpha2 + try: + i1, i2 = self.choose_alpha.send(state) + state = None + except StopIteration: + print("Optimization done!\nEvery sample satisfy the KKT condition!") + break + + # 2: calculate new alpha2 and new alpha1 + y1, y2 = self.tags[i1], self.tags[i2] + a1, a2 = self.alphas[i1].copy(), self.alphas[i2].copy() + e1, e2 = self._e(i1), self._e(i2) + args = (i1, i2, a1, a2, e1, e2, y1, y2) + a1_new, a2_new = self._get_new_alpha(*args) + if not a1_new and not a2_new: + state = False + continue + self.alphas[i1], self.alphas[i2] = a1_new, a2_new + + # 3: update threshold(b) + b1_new = np.float64( + -e1 + - y1 * K(i1, i1) * (a1_new - a1) + - y2 * K(i2, i1) * (a2_new - a2) + + self._b + ) + b2_new = np.float64( + -e2 + - y2 * K(i2, i2) * (a2_new - a2) + - y1 * K(i1, i2) * (a1_new - a1) + + self._b + ) + if 0.0 < a1_new < self._c: + b = b1_new + if 0.0 < a2_new < self._c: + b = b2_new + if not (np.float64(0) < a2_new < self._c) and not ( + np.float64(0) < a1_new < self._c + ): + b = (b1_new + b2_new) / 2.0 + b_old = self._b + self._b = b + + # 4: update error value,here we only calculate those non-bound samples' error + self._unbound = [i for i in self._all_samples if self._is_unbound(i)] + for s in self.unbound: + if s == i1 or s == i2: + continue + self._error[s] += ( + y1 * (a1_new - a1) * K(i1, s) + + y2 * (a2_new - a2) * K(i2, s) + + (self._b - b_old) + ) + + # if i1 or i2 is non-bound,update there error value to zero + if self._is_unbound(i1): + self._error[i1] = 0 + if self._is_unbound(i2): + self._error[i2] = 0 + + # Predict test samles + def predict(self, test_samples, classify=True): + + if test_samples.shape[1] > self.samples.shape[1]: + raise ValueError( + "Test samples' feature length does not equal to that of train samples" + ) + + if self._auto_norm: + test_samples = self._norm(test_samples) + + results = [] + for test_sample in test_samples: + result = self._predict(test_sample) + if classify: + results.append(1 if result > 0 else -1) + else: + results.append(result) + return np.array(results) + + # Check if alpha violate KKT condition + def _check_obey_kkt(self, index): + alphas = self.alphas + tol = self._tol + r = self._e(index) * self.tags[index] + c = self._c + + return (r < -tol and alphas[index] < c) or (r > tol and alphas[index] > 0.0) + + # Get value calculated from kernel function + def _k(self, i1, i2): + # for test samples,use Kernel function + if isinstance(i2, np.ndarray): + return self.Kernel(self.samples[i1], i2) + # for train samples,Kernel values have been saved in matrix + else: + return self._K_matrix[i1, i2] + + # Get sample's error + def _e(self, index): + """ + Two cases: + 1:Sample[index] is non-bound,Fetch error from list: _error + 2:sample[index] is bound,Use predicted value deduct true value: g(xi) - yi + + """ + # get from error data + if self._is_unbound(index): + return self._error[index] + # get by g(xi) - yi + else: + gx = np.dot(self.alphas * self.tags, self._K_matrix[:, index]) + self._b + yi = self.tags[index] + return gx - yi + + # Calculate Kernel matrix of all possible i1,i2 ,saving time + def _calculate_k_matrix(self): + k_matrix = np.zeros([self.length, self.length]) + for i in self._all_samples: + for j in self._all_samples: + k_matrix[i, j] = np.float64( + self.Kernel(self.samples[i, :], self.samples[j, :]) + ) + return k_matrix + + # Predict test sample's tag + def _predict(self, sample): + k = self._k + predicted_value = ( + np.sum( + [ + self.alphas[i1] * self.tags[i1] * k(i1, sample) + for i1 in self._all_samples + ] + ) + + self._b + ) + return predicted_value + + # Choose alpha1 and alpha2 + def _choose_alphas(self): + locis = yield from self._choose_a1() + if not locis: + return + return locis + + def _choose_a1(self): + """ + Choose first alpha ;steps: + 1:First loop over all sample + 2:Second loop over all non-bound samples till all non-bound samples does not voilate kkt condition. + 3:Repeat this two process endlessly,till all samples does not voilate kkt condition samples after first loop. + """ + while True: + all_not_obey = True + # all sample + print("scanning all sample!") + for i1 in [i for i in self._all_samples if self._check_obey_kkt(i)]: + all_not_obey = False + yield from self._choose_a2(i1) + + # non-bound sample + print("scanning non-bound sample!") + while True: + not_obey = True + for i1 in [ + i + for i in self._all_samples + if self._check_obey_kkt(i) and self._is_unbound(i) + ]: + not_obey = False + yield from self._choose_a2(i1) + if not_obey: + print("all non-bound samples fit the KKT condition!") + break + if all_not_obey: + print("all samples fit the KKT condition! Optimization done!") + break + return False + + def _choose_a2(self, i1): + """ + Choose the second alpha by using heuristic algorithm ;steps: + 1: Choose alpha2 which gets the maximum step size (|E1 - E2|). + 2: Start in a random point,loop over all non-bound samples till alpha1 and + alpha2 are optimized. + 3: Start in a random point,loop over all samples till alpha1 and alpha2 are + optimized. + """ + self._unbound = [i for i in self._all_samples if self._is_unbound(i)] + + if len(self.unbound) > 0: + tmp_error = self._error.copy().tolist() + tmp_error_dict = { + index: value + for index, value in enumerate(tmp_error) + if self._is_unbound(index) + } + if self._e(i1) >= 0: + i2 = min(tmp_error_dict, key=lambda index: tmp_error_dict[index]) + else: + i2 = max(tmp_error_dict, key=lambda index: tmp_error_dict[index]) + cmd = yield i1, i2 + if cmd is None: + return + + for i2 in np.roll(self.unbound, np.random.choice(self.length)): + cmd = yield i1, i2 + if cmd is None: + return + + for i2 in np.roll(self._all_samples, np.random.choice(self.length)): + cmd = yield i1, i2 + if cmd is None: + return + + # Get the new alpha2 and new alpha1 + def _get_new_alpha(self, i1, i2, a1, a2, e1, e2, y1, y2): + K = self._k + if i1 == i2: + return None, None + + # calculate L and H which bound the new alpha2 + s = y1 * y2 + if s == -1: + L, H = max(0.0, a2 - a1), min(self._c, self._c + a2 - a1) + else: + L, H = max(0.0, a2 + a1 - self._c), min(self._c, a2 + a1) + if L == H: + return None, None + + # calculate eta + k11 = K(i1, i1) + k22 = K(i2, i2) + k12 = K(i1, i2) + eta = k11 + k22 - 2.0 * k12 + + # select the new alpha2 which could get the minimal objectives + if eta > 0.0: + a2_new_unc = a2 + (y2 * (e1 - e2)) / eta + # a2_new has a boundary + if a2_new_unc >= H: + a2_new = H + elif a2_new_unc <= L: + a2_new = L + else: + a2_new = a2_new_unc + else: + b = self._b + l1 = a1 + s * (a2 - L) + h1 = a1 + s * (a2 - H) + + # way 1 + f1 = y1 * (e1 + b) - a1 * K(i1, i1) - s * a2 * K(i1, i2) + f2 = y2 * (e2 + b) - a2 * K(i2, i2) - s * a1 * K(i1, i2) + ol = ( + l1 * f1 + + L * f2 + + 1 / 2 * l1 ** 2 * K(i1, i1) + + 1 / 2 * L ** 2 * K(i2, i2) + + s * L * l1 * K(i1, i2) + ) + oh = ( + h1 * f1 + + H * f2 + + 1 / 2 * h1 ** 2 * K(i1, i1) + + 1 / 2 * H ** 2 * K(i2, i2) + + s * H * h1 * K(i1, i2) + ) + """ + # way 2 + Use objective function check which alpha2 new could get the minimal objectives + + """ + if ol < (oh - self._eps): + a2_new = L + elif ol > oh + self._eps: + a2_new = H + else: + a2_new = a2 + + # a1_new has a boundary too + a1_new = a1 + s * (a2 - a2_new) + if a1_new < 0: + a2_new += s * a1_new + a1_new = 0 + if a1_new > self._c: + a2_new += s * (a1_new - self._c) + a1_new = self._c + + return a1_new, a2_new + + # Normalise data using min_max way + def _norm(self, data): + if self._init: + self._min = np.min(data, axis=0) + self._max = np.max(data, axis=0) + self._init = False + return (data - self._min) / (self._max - self._min) + else: + return (data - self._min) / (self._max - self._min) + + def _is_unbound(self, index): + if 0.0 < self.alphas[index] < self._c: + return True + else: + return False + + def _is_support(self, index): + if self.alphas[index] > 0: + return True + else: + return False + + @property + def unbound(self): + return self._unbound + + @property + def support(self): + return [i for i in range(self.length) if self._is_support(i)] + + @property + def length(self): + return self.samples.shape[0] + + +class Kernel: + def __init__(self, kernel, degree=1.0, coef0=0.0, gamma=1.0): + self.degree = np.float64(degree) + self.coef0 = np.float64(coef0) + self.gamma = np.float64(gamma) + self._kernel_name = kernel + self._kernel = self._get_kernel(kernel_name=kernel) + self._check() + + def _polynomial(self, v1, v2): + return (self.gamma * np.inner(v1, v2) + self.coef0) ** self.degree + + def _linear(self, v1, v2): + return np.inner(v1, v2) + self.coef0 + + def _rbf(self, v1, v2): + return np.exp(-1 * (self.gamma * np.linalg.norm(v1 - v2) ** 2)) + + def _check(self): + if self._kernel == self._rbf: + if self.gamma < 0: + raise ValueError("gamma value must greater than 0") + + def _get_kernel(self, kernel_name): + maps = {"linear": self._linear, "poly": self._polynomial, "rbf": self._rbf} + return maps[kernel_name] + + def __call__(self, v1, v2): + return self._kernel(v1, v2) + + def __repr__(self): + return self._kernel_name + + +def count_time(func): + def call_func(*args, **kwargs): + import time + + start_time = time.time() + func(*args, **kwargs) + end_time = time.time() + print(f"smo algorithm cost {end_time - start_time} seconds") + + return call_func + + +@count_time +def test_cancel_data(): + print("Hello!\nStart test svm by smo algorithm!") + # 0: download dataset and load into pandas' dataframe + if not os.path.exists(r"cancel_data.csv"): + request = urllib.request.Request( + CANCER_DATASET_URL, + headers={"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"}, + ) + response = urllib.request.urlopen(request) + content = response.read().decode("utf-8") + with open(r"cancel_data.csv", "w") as f: + f.write(content) + + data = pd.read_csv(r"cancel_data.csv", header=None) + + # 1: pre-processing data + del data[data.columns.tolist()[0]] + data = data.dropna(axis=0) + data = data.replace({"M": np.float64(1), "B": np.float64(-1)}) + samples = np.array(data)[:, :] + + # 2: dividing data into train_data data and test_data data + train_data, test_data = samples[:328, :], samples[328:, :] + test_tags, test_samples = test_data[:, 0], test_data[:, 1:] + + # 3: choose kernel function,and set initial alphas to zero(optional) + mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) + al = np.zeros(train_data.shape[0]) + + # 4: calculating best alphas using SMO algorithm and predict test_data samples + mysvm = SmoSVM( + train=train_data, + kernel_func=mykernel, + alpha_list=al, + cost=0.4, + b=0.0, + tolerance=0.001, + ) + mysvm.fit() + predict = mysvm.predict(test_samples) + + # 5: check accuracy + score = 0 + test_num = test_tags.shape[0] + for i in range(test_tags.shape[0]): + if test_tags[i] == predict[i]: + score += 1 + print(f"\nall: {test_num}\nright: {score}\nfalse: {test_num - score}") + print(f"Rough Accuracy: {score / test_tags.shape[0]}") + + +def test_demonstration(): + # change stdout + print("\nStart plot,please wait!!!") + sys.stdout = open(os.devnull, "w") + + ax1 = plt.subplot2grid((2, 2), (0, 0)) + ax2 = plt.subplot2grid((2, 2), (0, 1)) + ax3 = plt.subplot2grid((2, 2), (1, 0)) + ax4 = plt.subplot2grid((2, 2), (1, 1)) + ax1.set_title("linear svm,cost:0.1") + test_linear_kernel(ax1, cost=0.1) + ax2.set_title("linear svm,cost:500") + test_linear_kernel(ax2, cost=500) + ax3.set_title("rbf kernel svm,cost:0.1") + test_rbf_kernel(ax3, cost=0.1) + ax4.set_title("rbf kernel svm,cost:500") + test_rbf_kernel(ax4, cost=500) + + sys.stdout = sys.__stdout__ + print("Plot done!!!") + + +def test_linear_kernel(ax, cost): + train_x, train_y = make_blobs( + n_samples=500, centers=2, n_features=2, random_state=1 + ) + train_y[train_y == 0] = -1 + scaler = StandardScaler() + train_x_scaled = scaler.fit_transform(train_x, train_y) + train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled)) + mykernel = Kernel(kernel="linear", degree=5, coef0=1, gamma=0.5) + mysvm = SmoSVM( + train=train_data, + kernel_func=mykernel, + cost=cost, + tolerance=0.001, + auto_norm=False, + ) + mysvm.fit() + plot_partition_boundary(mysvm, train_data, ax=ax) + + +def test_rbf_kernel(ax, cost): + train_x, train_y = make_circles( + n_samples=500, noise=0.1, factor=0.1, random_state=1 + ) + train_y[train_y == 0] = -1 + scaler = StandardScaler() + train_x_scaled = scaler.fit_transform(train_x, train_y) + train_data = np.hstack((train_y.reshape(500, 1), train_x_scaled)) + mykernel = Kernel(kernel="rbf", degree=5, coef0=1, gamma=0.5) + mysvm = SmoSVM( + train=train_data, + kernel_func=mykernel, + cost=cost, + tolerance=0.001, + auto_norm=False, + ) + mysvm.fit() + plot_partition_boundary(mysvm, train_data, ax=ax) + + +def plot_partition_boundary( + model, train_data, ax, resolution=100, colors=("b", "k", "r") +): + """ + We can not get the optimum w of our kernel svm model which is different from linear svm. + For this reason, we generate randomly distributed points with high desity and prediced values of these points are + calculated by using our tained model. Then we could use this prediced values to draw contour map. + And this contour map can represent svm's partition boundary. + + """ + train_data_x = train_data[:, 1] + train_data_y = train_data[:, 2] + train_data_tags = train_data[:, 0] + xrange = np.linspace(train_data_x.min(), train_data_x.max(), resolution) + yrange = np.linspace(train_data_y.min(), train_data_y.max(), resolution) + test_samples = np.array([(x, y) for x in xrange for y in yrange]).reshape( + resolution * resolution, 2 + ) + + test_tags = model.predict(test_samples, classify=False) + grid = test_tags.reshape((len(xrange), len(yrange))) + + # Plot contour map which represents the partition boundary + ax.contour( + xrange, + yrange, + np.mat(grid).T, + levels=(-1, 0, 1), + linestyles=("--", "-", "--"), + linewidths=(1, 1, 1), + colors=colors, + ) + # Plot all train samples + ax.scatter( + train_data_x, + train_data_y, + c=train_data_tags, + cmap=plt.cm.Dark2, + lw=0, + alpha=0.5, + ) + + # Plot support vectors + support = model.support + ax.scatter( + train_data_x[support], + train_data_y[support], + c=train_data_tags[support], + cmap=plt.cm.Dark2, + ) + + +if __name__ == "__main__": + test_cancel_data() + test_demonstration() + plt.show() diff --git a/machine_learning/support_vector_machines.py b/machine_learning/support_vector_machines.py new file mode 100644 index 000000000000..3bf54a69128d --- /dev/null +++ b/machine_learning/support_vector_machines.py @@ -0,0 +1,58 @@ +from sklearn.datasets import load_iris +from sklearn import svm +from sklearn.model_selection import train_test_split + + +# different functions implementing different types of SVM's +def NuSVC(train_x, train_y): + svc_NuSVC = svm.NuSVC() + svc_NuSVC.fit(train_x, train_y) + return svc_NuSVC + + +def Linearsvc(train_x, train_y): + svc_linear = svm.LinearSVC(tol=10e-2) + svc_linear.fit(train_x, train_y) + return svc_linear + + +def SVC(train_x, train_y): + # svm.SVC(C=1.0, kernel='rbf', degree=3, gamma=0.0, coef0=0.0, shrinking=True, + # probability=False,tol=0.001, cache_size=200, class_weight=None, verbose=False, + # max_iter=1000, random_state=None) + # various parameters like "kernel","gamma","C" can effectively tuned for a given + # machine learning model. + SVC = svm.SVC(gamma="auto") + SVC.fit(train_x, train_y) + return SVC + + +def test(X_new): + """ + 3 test cases to be passed + an array containing the sepal length (cm), sepal width (cm), petal length (cm), + petal width (cm) based on which the target name will be predicted + >>> test([1,2,1,4]) + 'virginica' + >>> test([5, 2, 4, 1]) + 'versicolor' + >>> test([6,3,4,1]) + 'versicolor' + """ + iris = load_iris() + # splitting the dataset to test and train + train_x, test_x, train_y, test_y = train_test_split( + iris["data"], iris["target"], random_state=4 + ) + # any of the 3 types of SVM can be used + # current_model=SVC(train_x, train_y) + # current_model=NuSVC(train_x, train_y) + current_model = Linearsvc(train_x, train_y) + prediction = current_model.predict([X_new]) + return iris["target_names"][prediction][0] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/3n+1.py b/maths/3n+1.py deleted file mode 100644 index d6c14ff0f47d..000000000000 --- a/maths/3n+1.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Tuple, List - -def n31(a: int) -> Tuple[List[int], int]: - """ - Returns the Collatz sequence and its length of any postiver integer. - >>> n31(4) - ([4, 2, 1], 3) - """ - - if not isinstance(a, int): - raise TypeError('Must be int, not {0}'.format(type(a).__name__)) - if a < 1: - raise ValueError('Given integer must be greater than 1, not {0}'.format(a)) - - path = [a] - while a != 1: - if a % 2 == 0: - a = a // 2 - else: - a = 3*a +1 - path += [a] - return path, len(path) - -def main(): - num = 4 - path , length = n31(num) - print("The Collatz sequence of {0} took {1} steps. \nPath: {2}".format(num,length, path)) - -if __name__ == '__main__': - main() diff --git a/maths/3n_plus_1.py b/maths/3n_plus_1.py new file mode 100644 index 000000000000..d3684561827f --- /dev/null +++ b/maths/3n_plus_1.py @@ -0,0 +1,149 @@ +from typing import Tuple, List + + +def n31(a: int) -> Tuple[List[int], int]: + """ + Returns the Collatz sequence and its length of any positive integer. + >>> n31(4) + ([4, 2, 1], 3) + """ + + if not isinstance(a, int): + raise TypeError("Must be int, not {}".format(type(a).__name__)) + if a < 1: + raise ValueError(f"Given integer must be greater than 1, not {a}") + + path = [a] + while a != 1: + if a % 2 == 0: + a = a // 2 + else: + a = 3 * a + 1 + path += [a] + return path, len(path) + + +def test_n31(): + """ + >>> test_n31() + """ + assert n31(4) == ([4, 2, 1], 3) + assert n31(11) == ([11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1], 15) + assert n31(31) == ( + [ + 31, + 94, + 47, + 142, + 71, + 214, + 107, + 322, + 161, + 484, + 242, + 121, + 364, + 182, + 91, + 274, + 137, + 412, + 206, + 103, + 310, + 155, + 466, + 233, + 700, + 350, + 175, + 526, + 263, + 790, + 395, + 1186, + 593, + 1780, + 890, + 445, + 1336, + 668, + 334, + 167, + 502, + 251, + 754, + 377, + 1132, + 566, + 283, + 850, + 425, + 1276, + 638, + 319, + 958, + 479, + 1438, + 719, + 2158, + 1079, + 3238, + 1619, + 4858, + 2429, + 7288, + 3644, + 1822, + 911, + 2734, + 1367, + 4102, + 2051, + 6154, + 3077, + 9232, + 4616, + 2308, + 1154, + 577, + 1732, + 866, + 433, + 1300, + 650, + 325, + 976, + 488, + 244, + 122, + 61, + 184, + 92, + 46, + 23, + 70, + 35, + 106, + 53, + 160, + 80, + 40, + 20, + 10, + 5, + 16, + 8, + 4, + 2, + 1, + ], + 107, + ) + + +if __name__ == "__main__": + num = 4 + path, length = n31(num) + print(f"The Collatz sequence of {num} took {length} steps. \nPath: {path}") diff --git a/maths/__init__.py b/maths/__init__.py index 8b137891791f..e69de29bb2d1 100644 --- a/maths/__init__.py +++ b/maths/__init__.py @@ -1 +0,0 @@ - diff --git a/maths/abs.py b/maths/abs.py index 2734e58ceee6..68c99a1d51d8 100644 --- a/maths/abs.py +++ b/maths/abs.py @@ -5,22 +5,24 @@ def abs_val(num): """ Find the absolute value of a number. - >>abs_val(-5) - 5 - >>abs_val(0) + >>> abs_val(-5.1) + 5.1 + >>> abs_val(-5) == abs_val(5) + True + >>> abs_val(0) 0 """ - if num < 0: - return -num + return -num if num < 0 else num - # Returns if number is not < 0 - return num - -def main(): - """Print absolute value of -34.""" - print(abs_val(-34)) # = 34 +def test_abs_val(): + """ + >>> test_abs_val() + """ + assert 0 == abs_val(0) + assert 34 == abs_val(34) + assert 100000000000 == abs_val(-100000000000) -if __name__ == '__main__': - main() +if __name__ == "__main__": + print(abs_val(-34)) # --> 34 diff --git a/maths/abs_max.py b/maths/abs_max.py index 28f631f0100e..554e27f6ee66 100644 --- a/maths/abs_max.py +++ b/maths/abs_max.py @@ -1,4 +1,5 @@ -from typing import List +from typing import List + def abs_max(x: List[int]) -> int: """ @@ -7,12 +8,13 @@ def abs_max(x: List[int]) -> int: >>> abs_max([3,-10,-2]) -10 """ - j =x[0] + j = x[0] for i in x: if abs(i) > abs(j): j = i return j + def abs_max_sort(x): """ >>> abs_max_sort([0,5,1,11]) @@ -20,13 +22,14 @@ def abs_max_sort(x): >>> abs_max_sort([3,-10,-2]) -10 """ - return sorted(x,key=abs)[-1] + return sorted(x, key=abs)[-1] + def main(): - a = [1,2,-11] + a = [1, 2, -11] assert abs_max(a) == -11 assert abs_max_sort(a) == -11 -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/maths/abs_min.py b/maths/abs_min.py index abb0c9051b7d..eb84de37ce23 100644 --- a/maths/abs_min.py +++ b/maths/abs_min.py @@ -16,9 +16,9 @@ def absMin(x): def main(): - a = [-3,-1,2,-11] + a = [-3, -1, 2, -11] print(absMin(a)) # = -1 -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/maths/add.py b/maths/add.py new file mode 100644 index 000000000000..0bc7da9697d3 --- /dev/null +++ b/maths/add.py @@ -0,0 +1,19 @@ +""" +Just to check +""" + + +def add(a, b): + """ + >>> add(2, 2) + 4 + >>> add(2, -2) + 0 + """ + return a + b + + +if __name__ == "__main__": + a = 5 + b = 6 + print(f"The sum of {a} + {b} is {add(a, b)}") diff --git a/maths/allocation_number.py b/maths/allocation_number.py new file mode 100644 index 000000000000..fd002b0c4361 --- /dev/null +++ b/maths/allocation_number.py @@ -0,0 +1,58 @@ +from typing import List + + +def allocation_num(number_of_bytes: int, partitions: int) -> List[str]: + """ + Divide a number of bytes into x partitions. + + In a multi-threaded download, this algorithm could be used to provide + each worker thread with a block of non-overlapping bytes to download. + For example: + for i in allocation_list: + requests.get(url,headers={'Range':f'bytes={i}'}) + + parameter + ------------ + : param number_of_bytes + : param partitions + + return + ------------ + : return: list of bytes to be assigned to each worker thread + + Examples: + ------------ + >>> allocation_num(16647, 4) + ['0-4161', '4162-8322', '8323-12483', '12484-16647'] + >>> allocation_num(888, 888) + Traceback (most recent call last): + ... + ValueError: partitions can not >= number_of_bytes! + >>> allocation_num(888, 999) + Traceback (most recent call last): + ... + ValueError: partitions can not >= number_of_bytes! + >>> allocation_num(888, -4) + Traceback (most recent call last): + ... + ValueError: partitions must be a positive number! + """ + if partitions <= 0: + raise ValueError("partitions must be a positive number!") + if partitions >= number_of_bytes: + raise ValueError("partitions can not >= number_of_bytes!") + bytes_per_partition = number_of_bytes // partitions + allocation_list = [f"0-{bytes_per_partition}"] + for i in range(1, partitions - 1): + length = f"{bytes_per_partition * i + 1}-{bytes_per_partition * (i + 1)}" + allocation_list.append(length) + allocation_list.append( + f"{(bytes_per_partition * (partitions - 1)) + 1}-" f"{number_of_bytes}" + ) + return allocation_list + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/area_under_curve.py b/maths/area_under_curve.py new file mode 100644 index 000000000000..2d01e414b63b --- /dev/null +++ b/maths/area_under_curve.py @@ -0,0 +1,59 @@ +""" +Approximates the area under the curve using the trapezoidal rule +""" + +from typing import Callable, Union + + +def trapezoidal_area( + fnc: Callable[[Union[int, float]], Union[int, float]], + x_start: Union[int, float], + x_end: Union[int, float], + steps: int = 100, +) -> float: + """ + Treats curve as a collection of linear lines and sums the area of the + trapezium shape they form + :param fnc: a function which defines a curve + :param x_start: left end point to indicate the start of line segment + :param x_end: right end point to indicate end of line segment + :param steps: an accuracy gauge; more steps increases the accuracy + :return: a float representing the length of the curve + + >>> def f(x): + ... return 5 + >>> f"{trapezoidal_area(f, 12.0, 14.0, 1000):.3f}" + '10.000' + >>> def f(x): + ... return 9*x**2 + >>> f"{trapezoidal_area(f, -4.0, 0, 10000):.4f}" + '192.0000' + >>> f"{trapezoidal_area(f, -4.0, 4.0, 10000):.4f}" + '384.0000' + """ + x1 = x_start + fx1 = fnc(x_start) + area = 0.0 + for i in range(steps): + # Approximates small segments of curve as linear and solve + # for trapezoidal area + x2 = (x_end - x_start) / steps + x1 + fx2 = fnc(x2) + area += abs(fx2 + fx1) * (x2 - x1) / 2 + # Increment step + x1 = x2 + fx1 = fx2 + return area + + +if __name__ == "__main__": + + def f(x): + return x ** 3 + x ** 2 + + print("f(x) = x^3 + x^2") + print("The area between the curve, x = -5, x = 5 and the x axis is:") + i = 10 + while i <= 100000: + print(f"with {i} steps: {trapezoidal_area(f, -5, 5, i)}") + i *= 10 diff --git a/maths/armstrong_numbers.py b/maths/armstrong_numbers.py new file mode 100644 index 000000000000..d30ed2e430a0 --- /dev/null +++ b/maths/armstrong_numbers.py @@ -0,0 +1,69 @@ +""" +An Armstrong number is equal to the sum of the cubes of its digits. +For example, 370 is an Armstrong number because 3*3*3 + 7*7*7 + 0*0*0 = 370. +An Armstrong number is often called Narcissistic number. +""" + + +def armstrong_number(n: int) -> bool: + """ + Return True if n is an Armstrong number or False if it is not. + + >>> armstrong_number(153) + True + >>> armstrong_number(200) + False + >>> armstrong_number(1634) + True + >>> armstrong_number(0) + False + >>> armstrong_number(-1) + False + >>> armstrong_number(1.2) + False + """ + if not isinstance(n, int) or n < 1: + return False + + # Initialization of sum and number of digits. + sum = 0 + number_of_digits = 0 + temp = n + # Calculation of digits of the number + while temp > 0: + number_of_digits += 1 + temp //= 10 + # Dividing number into separate digits and find Armstrong number + temp = n + while temp > 0: + rem = temp % 10 + sum += rem ** number_of_digits + temp //= 10 + return n == sum + + +def narcissistic_number(n: int) -> bool: + """Return True if n is a narcissistic number or False if it is not""" + + expo = len(str(n)) # power, all number will be raised to + # each digit will be multiplied expo times + temp = [(int(i) ** expo) for i in str(n)] + + # check if sum of cube of each digit is equal to number + return n == sum(temp) + + +def main(): + """ + Request that user input an integer and tell them if it is Armstrong number. + """ + num = int(input("Enter an integer to see if it is an Armstrong number: ").strip()) + print(f"{num} is {'' if armstrong_number(num) else 'not '}an Armstrong number.") + print(f"{num} is {'' if narcissistic_number(num) else 'not '}an Armstrong number.") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/maths/average_mean.py b/maths/average_mean.py index 78387111022d..4beca1f741a0 100644 --- a/maths/average_mean.py +++ b/maths/average_mean.py @@ -3,18 +3,18 @@ def average(nums): """Find mean of a list of numbers.""" - sum = 0 - for x in nums: - sum += x - avg = sum / len(nums) - print(avg) - return avg + return sum(nums) / len(nums) -def main(): - """Call average module to find mean of a specific list of numbers.""" - average([2, 4, 6, 8, 20, 50, 70]) +def test_average(): + """ + >>> test_average() + """ + assert 12.0 == average([3, 6, 9, 12, 15, 18, 21]) + assert 20 == average([5, 10, 15, 20, 25, 30, 35]) + assert 4.5 == average([1, 2, 3, 4, 5, 6, 7, 8]) -if __name__ == '__main__': - main() +if __name__ == "__main__": + """Call average module to find mean of a specific list of numbers.""" + print(average([2, 4, 6, 8, 20, 50, 70])) diff --git a/maths/average_median.py b/maths/average_median.py index eab0107d8da8..ccb250d7718c 100644 --- a/maths/average_median.py +++ b/maths/average_median.py @@ -24,11 +24,13 @@ def median(nums): med = sorted_list[mid_index] return med + def main(): print("Odd number of numbers:") print(median([2, 4, 6, 8, 20, 50, 70])) print("Even number of numbers:") print(median([2, 4, 6, 8, 20, 50])) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/maths/average_mode.py b/maths/average_mode.py new file mode 100644 index 000000000000..d472dc04d4bf --- /dev/null +++ b/maths/average_mode.py @@ -0,0 +1,31 @@ +import statistics + + +def mode(input_list): # Defining function "mode." + """This function returns the mode(Mode as in the measures of + central tendency) of the input data. + + The input list may contain any Datastructure or any Datatype. + + >>> input_list = [2, 3, 4, 5, 3, 4, 2, 5, 2, 2, 4, 2, 2, 2] + >>> mode(input_list) + 2 + >>> input_list = [2, 3, 4, 5, 3, 4, 2, 5, 2, 2, 4, 2, 2, 2] + >>> mode(input_list) == statistics.mode(input_list) + True + """ + # Copying input_list to check with the index number later. + check_list = input_list.copy() + result = list() # Empty list to store the counts of elements in input_list + for x in input_list: + result.append(input_list.count(x)) + input_list.remove(x) + y = max(result) # Gets the maximum value in the result list. + # Returns the value with the maximum number of repetitions. + return check_list[result.index(y)] + + +if __name__ == "__main__": + data = [2, 3, 4, 5, 3, 4, 2, 5, 2, 2, 4, 2, 2, 2] + print(mode(data)) + print(statistics.mode(data)) diff --git a/maths/basic_maths.py b/maths/basic_maths.py index cd7bac0113b8..07ee3b3df296 100644 --- a/maths/basic_maths.py +++ b/maths/basic_maths.py @@ -2,83 +2,79 @@ import math -def prime_factors(n): - """Find Prime Factors.""" +def prime_factors(n: int) -> list: + """Find Prime Factors. + >>> prime_factors(100) + [2, 2, 5, 5] + """ pf = [] while n % 2 == 0: pf.append(2) n = int(n / 2) - for i in range(3, int(math.sqrt(n)) + 1, 2): while n % i == 0: pf.append(i) n = int(n / i) - if n > 2: pf.append(n) - return pf -def number_of_divisors(n): - """Calculate Number of Divisors of an Integer.""" +def number_of_divisors(n: int) -> int: + """Calculate Number of Divisors of an Integer. + >>> number_of_divisors(100) + 9 + """ div = 1 - temp = 1 while n % 2 == 0: temp += 1 n = int(n / 2) - div = div * (temp) - + div *= temp for i in range(3, int(math.sqrt(n)) + 1, 2): temp = 1 while n % i == 0: temp += 1 n = int(n / i) - div = div * (temp) - + div *= temp return div -def sum_of_divisors(n): - """Calculate Sum of Divisors.""" +def sum_of_divisors(n: int) -> int: + """Calculate Sum of Divisors. + >>> sum_of_divisors(100) + 217 + """ s = 1 - temp = 1 while n % 2 == 0: temp += 1 n = int(n / 2) if temp > 1: - s *= (2**temp - 1) / (2 - 1) - + s *= (2 ** temp - 1) / (2 - 1) for i in range(3, int(math.sqrt(n)) + 1, 2): temp = 1 while n % i == 0: temp += 1 n = int(n / i) if temp > 1: - s *= (i**temp - 1) / (i - 1) - - return s + s *= (i ** temp - 1) / (i - 1) + return int(s) -def euler_phi(n): - """Calculte Euler's Phi Function.""" - l = prime_factors(n) - l = set(l) +def euler_phi(n: int) -> int: + """Calculate Euler's Phi Function. + >>> euler_phi(100) + 40 + """ s = n - for x in l: + for x in set(prime_factors(n)): s *= (x - 1) / x - return s + return int(s) -def main(): - """Print the Results of Basic Math Operations.""" +if __name__ == "__main__": print(prime_factors(100)) print(number_of_divisors(100)) print(sum_of_divisors(100)) print(euler_phi(100)) - - -if __name__ == '__main__': - main() diff --git a/maths/binary_exp_mod.py b/maths/binary_exp_mod.py new file mode 100644 index 000000000000..67dd1e728b18 --- /dev/null +++ b/maths/binary_exp_mod.py @@ -0,0 +1,28 @@ +def bin_exp_mod(a, n, b): + """ + >>> bin_exp_mod(3, 4, 5) + 1 + >>> bin_exp_mod(7, 13, 10) + 7 + """ + # mod b + assert not (b == 0), "This cannot accept modulo that is == 0" + if n == 0: + return 1 + + if n % 2 == 1: + return (bin_exp_mod(a, n - 1, b) * a) % b + + r = bin_exp_mod(a, n / 2, b) + return (r * r) % b + + +if __name__ == "__main__": + try: + BASE = int(input("Enter Base : ").strip()) + POWER = int(input("Enter Power : ").strip()) + MODULO = int(input("Enter Modulo : ").strip()) + except ValueError: + print("Invalid literal for integer") + + print(bin_exp_mod(BASE, POWER, MODULO)) diff --git a/maths/binary_exponentiation.py b/maths/binary_exponentiation.py index a8d736adfea0..8dda5245cf44 100644 --- a/maths/binary_exponentiation.py +++ b/maths/binary_exponentiation.py @@ -6,10 +6,10 @@ def binary_exponentiation(a, n): - if (n == 0): + if n == 0: return 1 - elif (n % 2 == 1): + elif n % 2 == 1: return binary_exponentiation(a, n - 1) * a else: @@ -25,4 +25,4 @@ def binary_exponentiation(a, n): print("Invalid literal for integer") RESULT = binary_exponentiation(BASE, POWER) - print("{}^({}) : {}".format(BASE, POWER, RESULT)) + print(f"{BASE}^({POWER}) : {RESULT}") diff --git a/maths/binomial_coefficient.py b/maths/binomial_coefficient.py new file mode 100644 index 000000000000..4def041492f3 --- /dev/null +++ b/maths/binomial_coefficient.py @@ -0,0 +1,20 @@ +def binomial_coefficient(n, r): + """ + Find binomial coefficient using pascals triangle. + + >>> binomial_coefficient(10, 5) + 252 + """ + C = [0 for i in range(r + 1)] + # nc0 = 1 + C[0] = 1 + for i in range(1, n + 1): + # to compute current row from previous row. + j = min(i, r) + while j > 0: + C[j] += C[j - 1] + j -= 1 + return C[r] + + +print(binomial_coefficient(n=10, r=5)) diff --git a/maths/bisection.py b/maths/bisection.py new file mode 100644 index 000000000000..93cc2247b64e --- /dev/null +++ b/maths/bisection.py @@ -0,0 +1,63 @@ +""" +Given a function on floating number f(x) and two floating numbers ‘a’ and ‘b’ such that +f(a) * f(b) < 0 and f(x) is continuous in [a, b]. +Here f(x) represents algebraic or transcendental equation. +Find root of function in interval [a, b] (Or find a value of x such that f(x) is 0) + +https://en.wikipedia.org/wiki/Bisection_method +""" + + +def equation(x: float) -> float: + """ + >>> equation(5) + -15 + >>> equation(0) + 10 + >>> equation(-5) + -15 + >>> equation(0.1) + 9.99 + >>> equation(-0.1) + 9.99 + """ + return 10 - x * x + + +def bisection(a: float, b: float) -> float: + """ + >>> bisection(-2, 5) + 3.1611328125 + >>> bisection(0, 6) + 3.158203125 + >>> bisection(2, 3) + Traceback (most recent call last): + ... + ValueError: Wrong space! + """ + # Bolzano theory in order to find if there is a root between a and b + if equation(a) * equation(b) >= 0: + raise ValueError("Wrong space!") + + c = a + while (b - a) >= 0.01: + # Find middle point + c = (a + b) / 2 + # Check if middle point is root + if equation(c) == 0.0: + break + # Decide the side to repeat the steps + if equation(c) * equation(a) < 0: + b = c + else: + a = c + return c + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print(bisection(-2, 5)) + print(bisection(0, 6)) diff --git a/maths/ceil.py b/maths/ceil.py new file mode 100644 index 000000000000..ff136f685524 --- /dev/null +++ b/maths/ceil.py @@ -0,0 +1,20 @@ +def ceil(x) -> int: + """ + Return the ceiling of x as an Integral. + + :param x: the number + :return: the smallest integer >= x. + + >>> import math + >>> all(ceil(n) == math.ceil(n) for n in (1, -1, 0, -0, 1.1, -1.1, 1.0, -1.0, 1_000_000_000)) + True + """ + return ( + x if isinstance(x, int) or x - int(x) == 0 else int(x + 1) if x > 0 else int(x) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/chudnovsky_algorithm.py b/maths/chudnovsky_algorithm.py new file mode 100644 index 000000000000..fb188cd6a3d8 --- /dev/null +++ b/maths/chudnovsky_algorithm.py @@ -0,0 +1,61 @@ +from decimal import Decimal, getcontext +from math import ceil, factorial + + +def pi(precision: int) -> str: + """ + The Chudnovsky algorithm is a fast method for calculating the digits of PI, + based on Ramanujan’s PI formulae. + + https://en.wikipedia.org/wiki/Chudnovsky_algorithm + + PI = constant_term / ((multinomial_term * linear_term) / exponential_term) + where constant_term = 426880 * sqrt(10005) + + The linear_term and the exponential_term can be defined iteratively as follows: + L_k+1 = L_k + 545140134 where L_0 = 13591409 + X_k+1 = X_k * -262537412640768000 where X_0 = 1 + + The multinomial_term is defined as follows: + 6k! / ((3k)! * (k!) ^ 3) + where k is the k_th iteration. + + This algorithm correctly calculates around 14 digits of PI per iteration + + >>> pi(10) + '3.14159265' + >>> pi(100) + '3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706' + >>> pi('hello') + Traceback (most recent call last): + ... + TypeError: Undefined for non-integers + >>> pi(-1) + Traceback (most recent call last): + ... + ValueError: Undefined for non-natural numbers + """ + + if not isinstance(precision, int): + raise TypeError("Undefined for non-integers") + elif precision < 1: + raise ValueError("Undefined for non-natural numbers") + + getcontext().prec = precision + num_iterations = ceil(precision / 14) + constant_term = 426880 * Decimal(10005).sqrt() + multinomial_term = 1 + exponential_term = 1 + linear_term = 13591409 + partial_sum = Decimal(linear_term) + for k in range(1, num_iterations): + multinomial_term = factorial(6 * k) // (factorial(3 * k) * factorial(k) ** 3) + linear_term += 545140134 + exponential_term *= -262537412640768000 + partial_sum += Decimal(multinomial_term * linear_term) / exponential_term + return str(constant_term / partial_sum)[:-1] + + +if __name__ == "__main__": + n = 50 + print(f"The first {n} digits of pi is: {pi(n)}") diff --git a/maths/collatz_sequence.py b/maths/collatz_sequence.py new file mode 100644 index 000000000000..d3eb6e756dcd --- /dev/null +++ b/maths/collatz_sequence.py @@ -0,0 +1,43 @@ +from typing import List + + +def collatz_sequence(n: int) -> List[int]: + """ + Collatz conjecture: start with any positive integer n. The next term is + obtained as follows: + If n term is even, the next term is: n / 2 . + If n is odd, the next term is: 3 * n + 1. + + The conjecture states the sequence will always reach 1 for any starting value n. + Example: + >>> collatz_sequence(2.1) + Traceback (most recent call last): + ... + Exception: Sequence only defined for natural numbers + >>> collatz_sequence(0) + Traceback (most recent call last): + ... + Exception: Sequence only defined for natural numbers + >>> collatz_sequence(43) + [43, 130, 65, 196, 98, 49, 148, 74, 37, 112, 56, 28, 14, 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1] + """ + + if not isinstance(n, int) or n < 1: + raise Exception("Sequence only defined for natural numbers") + + sequence = [n] + while n != 1: + n = 3 * n + 1 if n & 1 else n // 2 + sequence.append(n) + return sequence + + +def main(): + n = 43 + sequence = collatz_sequence(n) + print(sequence) + print(f"collatz sequence from {n} took {len(sequence)} steps.") + + +if __name__ == "__main__": + main() diff --git a/maths/combinations.py b/maths/combinations.py new file mode 100644 index 000000000000..fd98992e6c16 --- /dev/null +++ b/maths/combinations.py @@ -0,0 +1,19 @@ +from math import factorial + + +def combinations(n, k): + """ + >>> combinations(10,5) + 252 + >>> combinations(6,3) + 20 + >>> combinations(20,5) + 15504 + """ + return int(factorial(n) / ((factorial(k)) * (factorial(n - k)))) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/eulers_totient.py b/maths/eulers_totient.py new file mode 100644 index 000000000000..6a35e69bde0b --- /dev/null +++ b/maths/eulers_totient.py @@ -0,0 +1,45 @@ +# Eulers Totient function finds the number of relative primes of a number n from 1 to n +def totient(n: int) -> list: + is_prime = [True for i in range(n + 1)] + totients = [i - 1 for i in range(n + 1)] + primes = [] + for i in range(2, n + 1): + if is_prime[i]: + primes.append(i) + for j in range(0, len(primes)): + if i * primes[j] >= n: + break + is_prime[i * primes[j]] = False + + if i % primes[j] == 0: + totients[i * primes[j]] = totients[i] * primes[j] + break + + totients[i * primes[j]] = totients[i] * (primes[j] - 1) + + return totients + + +def test_totient() -> None: + """ + >>> n = 10 + >>> totient_calculation = totient(n) + >>> for i in range(1, n): + ... print(f"{i} has {totient_calculation[i]} relative primes.") + 1 has 0 relative primes. + 2 has 1 relative primes. + 3 has 2 relative primes. + 4 has 2 relative primes. + 5 has 4 relative primes. + 6 has 2 relative primes. + 7 has 6 relative primes. + 8 has 4 relative primes. + 9 has 6 relative primes. + """ + pass + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/explicit_euler.py b/maths/explicit_euler.py new file mode 100644 index 000000000000..7c780198602b --- /dev/null +++ b/maths/explicit_euler.py @@ -0,0 +1,40 @@ +import numpy as np + + +def explicit_euler(ode_func, y0, x0, step_size, x_end): + """ + Calculate numeric solution at each step to an ODE using Euler's Method + + https://en.wikipedia.org/wiki/Euler_method + + Arguments: + ode_func -- The ode as a function of x and y + y0 -- the initial value for y + x0 -- the initial value for x + stepsize -- the increment value for x + x_end -- the end value for x + + >>> # the exact solution is math.exp(x) + >>> def f(x, y): + ... return y + >>> y0 = 1 + >>> y = explicit_euler(f, y0, 0.0, 0.01, 5) + >>> y[-1] + 144.77277243257308 + """ + N = int(np.ceil((x_end - x0) / step_size)) + y = np.zeros((N + 1,)) + y[0] = y0 + x = x0 + + for k in range(N): + y[k + 1] = y[k] + step_size * ode_func(x, y[k]) + x += step_size + + return y + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/extended_euclidean_algorithm.py b/maths/extended_euclidean_algorithm.py index fc3798e7e432..fe81bcfaf71d 100644 --- a/maths/extended_euclidean_algorithm.py +++ b/maths/extended_euclidean_algorithm.py @@ -61,12 +61,12 @@ def extended_euclidean_algorithm(m, n): def main(): """Call Extended Euclidean Algorithm.""" if len(sys.argv) < 3: - print('2 integer arguments required') + print("2 integer arguments required") exit(1) m = int(sys.argv[1]) n = int(sys.argv[2]) print(extended_euclidean_algorithm(m, n)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/maths/factorial_iterative.py b/maths/factorial_iterative.py new file mode 100644 index 000000000000..64314790c11c --- /dev/null +++ b/maths/factorial_iterative.py @@ -0,0 +1,30 @@ +# factorial of a positive integer -- https://en.wikipedia.org/wiki/Factorial + + +def factorial(n: int) -> int: + """ + >>> import math + >>> all(factorial(i) == math.factorial(i) for i in range(20)) + True + >>> factorial(0.1) + Traceback (most recent call last): + ... + ValueError: factorial() only accepts integral values + >>> factorial(-1) + Traceback (most recent call last): + ... + ValueError: factorial() not defined for negative values + """ + if n != int(n): + raise ValueError("factorial() only accepts integral values") + if n < 0: + raise ValueError("factorial() not defined for negative values") + value = 1 + for i in range(1, n + 1): + value *= i + return value + + +if __name__ == "__main__": + n = int(input("Enter a positive integer: ").strip() or 0) + print(f"factorial{n} is {factorial(n)}") diff --git a/maths/factorial_python.py b/maths/factorial_python.py index 6c1349fd5f4c..46688261af56 100644 --- a/maths/factorial_python.py +++ b/maths/factorial_python.py @@ -1,19 +1,34 @@ -"""Python program to find the factorial of a number provided by the user.""" +def factorial(input_number: int) -> int: + """ + Calculate the factorial of specified number -# change the value for a different result -NUM = 10 + >>> factorial(1) + 1 + >>> factorial(6) + 720 + >>> factorial(0) + 1 + >>> factorial(-1) + Traceback (most recent call last): + ... + ValueError: factorial() not defined for negative values + >>> factorial(0.1) + Traceback (most recent call last): + ... + ValueError: factorial() only accepts integral values + """ -# uncomment to take input from the user -# num = int(input("Enter a number: ")) + if input_number < 0: + raise ValueError("factorial() not defined for negative values") + if not isinstance(input_number, int): + raise ValueError("factorial() only accepts integral values") + result = 1 + for i in range(1, input_number): + result = result * (i + 1) + return result -FACTORIAL = 1 -# check if the number is negative, positive or zero -if NUM < 0: - print("Sorry, factorial does not exist for negative numbers") -elif NUM == 0: - print("The factorial of 0 is 1") -else: - for i in range(1, NUM + 1): - FACTORIAL = FACTORIAL * i - print("The factorial of", NUM, "is", FACTORIAL) +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/factorial_recursive.py b/maths/factorial_recursive.py index 06173dcbcd7d..137112738905 100644 --- a/maths/factorial_recursive.py +++ b/maths/factorial_recursive.py @@ -1,14 +1,28 @@ -def fact(n): +def factorial(n: int) -> int: """ - Return 1, if n is 1 or below, - otherwise, return n * fact(n-1). + Calculate the factorial of a positive integer + https://en.wikipedia.org/wiki/Factorial + + >>> import math + >>> all(factorial(i) == math.factorial(i) for i in range(20)) + True + >>> factorial(0.1) + Traceback (most recent call last): + ... + ValueError: factorial() only accepts integral values + >>> factorial(-1) + Traceback (most recent call last): + ... + ValueError: factorial() not defined for negative values """ - return 1 if n <= 1 else n * fact(n - 1) + if not isinstance(n, int): + raise ValueError("factorial() only accepts integral values") + if n < 0: + raise ValueError("factorial() not defined for negative values") + return 1 if n == 0 or n == 1 else n * factorial(n - 1) + +if __name__ == "__main__": + import doctest -""" -Show factorial for i, -where i ranges from 1 to 20. -""" -for i in range(1, 21): - print(i, ": ", fact(i), sep='') + doctest.testmod() diff --git a/maths/factors.py b/maths/factors.py new file mode 100644 index 000000000000..e2fdc4063a13 --- /dev/null +++ b/maths/factors.py @@ -0,0 +1,18 @@ +def factors_of_a_number(num: int) -> list: + """ + >>> factors_of_a_number(1) + [1] + >>> factors_of_a_number(5) + [1, 5] + >>> factors_of_a_number(24) + [1, 2, 3, 4, 6, 8, 12, 24] + >>> factors_of_a_number(-24) + [] + """ + return [i for i in range(1, num + 1) if num % i == 0] + + +if __name__ == "__main__": + num = int(input("Enter a number to find its factors: ")) + factors = factors_of_a_number(num) + print(f"{num} has {len(factors)} factors: {', '.join(str(f) for f in factors)}") diff --git a/maths/fermat_little_theorem.py b/maths/fermat_little_theorem.py index 8cf60dafe3ca..24d558115795 100644 --- a/maths/fermat_little_theorem.py +++ b/maths/fermat_little_theorem.py @@ -6,10 +6,10 @@ def binary_exponentiation(a, n, mod): - if (n == 0): + if n == 0: return 1 - elif (n % 2 == 1): + elif n % 2 == 1: return (binary_exponentiation(a, n - 1, mod) * a) % mod else: diff --git a/maths/fibonacci.py b/maths/fibonacci.py index 0a0611f21379..5ba9f6636364 100644 --- a/maths/fibonacci.py +++ b/maths/fibonacci.py @@ -21,10 +21,11 @@ def timer_wrapper(*args, **kwargs): func(*args, **kwargs) end = time.time() if int(end - start) > 0: - print(f'Run time for {func.__name__}: {(end - start):0.2f}s') + print(f"Run time for {func.__name__}: {(end - start):0.2f}s") else: - print(f'Run time for {func.__name__}: {(end - start)*1000:0.2f}ms') + print(f"Run time for {func.__name__}: {(end - start)*1000:0.2f}ms") return func(*args, **kwargs) + return timer_wrapper @@ -69,9 +70,13 @@ def _check_number_input(n, min_thresh, max_thresh=None): except ValueLessThanZero: print("Incorrect Input: number must not be less than 0") except ValueTooSmallError: - print(f'Incorrect Input: input number must be > {min_thresh} for the recursive calculation') + print( + f"Incorrect Input: input number must be > {min_thresh} for the recursive calculation" + ) except ValueTooLargeError: - print(f'Incorrect Input: input number must be < {max_thresh} for the recursive calculation') + print( + f"Incorrect Input: input number must be < {max_thresh} for the recursive calculation" + ) return False @@ -86,8 +91,8 @@ def fib_iterative(n): if _check_number_input(n, 2): seq_out = [0, 1] a, b = 0, 1 - for _ in range(n-len(seq_out)): - a, b = b, a+b + for _ in range(n - len(seq_out)): + a, b = b, a + b seq_out.append(b) return seq_out @@ -106,12 +111,14 @@ def fib_formula(n): phi_1 = Decimal(1 + sqrt) / Decimal(2) phi_2 = Decimal(1 - sqrt) / Decimal(2) for i in range(2, n): - temp_out = ((phi_1**Decimal(i)) - (phi_2**Decimal(i))) * (Decimal(sqrt) ** Decimal(-1)) + temp_out = ((phi_1 ** Decimal(i)) - (phi_2 ** Decimal(i))) * ( + Decimal(sqrt) ** Decimal(-1) + ) seq_out.append(int(temp_out)) return seq_out -if __name__ == '__main__': +if __name__ == "__main__": num = 20 # print(f'{fib_recursive(num)}\n') # print(f'{fib_iterative(num)}\n') diff --git a/maths/fibonacci_sequence_recursion.py b/maths/fibonacci_sequence_recursion.py index 9190e7fc7a40..91619600d5b4 100644 --- a/maths/fibonacci_sequence_recursion.py +++ b/maths/fibonacci_sequence_recursion.py @@ -1,21 +1,22 @@ # Fibonacci Sequence Using Recursion + def recur_fibo(n): - if n <= 1: - return n - else: - (recur_fibo(n-1) + recur_fibo(n-2)) + """ + >>> [recur_fibo(i) for i in range(12)] + [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] + """ + return n if n <= 1 else recur_fibo(n - 1) + recur_fibo(n - 2) -def isPositiveInteger(limit): - return limit >= 0 def main(): limit = int(input("How many terms to include in fibonacci series: ")) - if isPositiveInteger(limit): - print("The first {limit} terms of the fibonacci series are as follows:") + if limit > 0: + print(f"The first {limit} terms of the fibonacci series are as follows:") print([recur_fibo(n) for n in range(limit)]) else: print("Please enter a positive integer: ") -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/maths/find_lcm.py b/maths/find_lcm.py deleted file mode 100644 index f7ac958070b5..000000000000 --- a/maths/find_lcm.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Find Least Common Multiple.""" - -# https://en.wikipedia.org/wiki/Least_common_multiple - - -def find_lcm(num_1, num_2): - """Find the least common multiple of two numbers. - >>> find_lcm(5,2) - 10 - >>> find_lcm(12,76) - 228 - """ - if num_1>=num_2: - max_num=num_1 - else: - max_num=num_2 - - lcm = max_num - while True: - if ((lcm % num_1 == 0) and (lcm % num_2 == 0)): - break - lcm += max_num - return lcm - - -def main(): - """Use test numbers to run the find_lcm algorithm.""" - num_1 = int(input().strip()) - num_2 = int(input().strip()) - print(find_lcm(num_1, num_2)) - - -if __name__ == '__main__': - main() diff --git a/maths/find_max.py b/maths/find_max.py index 0ce49a68c348..4d92e37eb2e1 100644 --- a/maths/find_max.py +++ b/maths/find_max.py @@ -1,14 +1,25 @@ # NguyenU + def find_max(nums): - max = nums[0] + """ + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_max(nums) == max(nums) + True + True + True + True + """ + max_num = nums[0] for x in nums: - if x > max: - max = x - print(max) + if x > max_num: + max_num = x + return max_num + def main(): - find_max([2, 4, 9, 7, 19, 94, 5]) + print(find_max([2, 4, 9, 7, 19, 94, 5])) # 94 + -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/maths/find_max_recursion.py b/maths/find_max_recursion.py new file mode 100644 index 000000000000..fc10ecf3757a --- /dev/null +++ b/maths/find_max_recursion.py @@ -0,0 +1,25 @@ +# Divide and Conquer algorithm +def find_max(nums, left, right): + """ + find max value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: max in nums + + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_max(nums, 0, len(nums) - 1) == max(nums) + True + """ + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_max = find_max(nums, left, mid) # find max in range[left, mid] + right_max = find_max(nums, mid + 1, right) # find max in range[mid + 1, right] + + return left_max if left_max >= right_max else right_max + + +if __name__ == "__main__": + nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + assert find_max(nums, 0, len(nums) - 1) == 10 diff --git a/maths/find_min.py b/maths/find_min.py index c720da268a25..2af2e44ba353 100644 --- a/maths/find_min.py +++ b/maths/find_min.py @@ -1,17 +1,26 @@ -"""Find Minimum Number in a List.""" +def find_min(nums): + """ + Find Minimum Number in a List + :param nums: contains elements + :return: min number in list + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_min(nums) == min(nums) + True + True + True + True + """ + min_num = nums[0] + for num in nums: + if min_num > num: + min_num = num + return min_num -def main(): - """Find Minimum Number in a List.""" - def find_min(x): - min_num = x[0] - for i in x: - if min_num > i: - min_num = i - return min_num - print(find_min([0, 1, 2, 3, 4, 5, -3, 24, -56])) # = -56 +def main(): + assert find_min([0, 1, 2, 3, 4, 5, -3, 24, -56]) == -56 -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/maths/find_min_recursion.py b/maths/find_min_recursion.py new file mode 100644 index 000000000000..4488967cc57a --- /dev/null +++ b/maths/find_min_recursion.py @@ -0,0 +1,25 @@ +# Divide and Conquer algorithm +def find_min(nums, left, right): + """ + find min value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: min in nums + + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_min(nums, 0, len(nums) - 1) == min(nums) + True + """ + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_min = find_min(nums, left, mid) # find min in range[left, mid] + right_min = find_min(nums, mid + 1, right) # find min in range[mid + 1, right] + + return left_min if left_min <= right_min else right_min + + +if __name__ == "__main__": + nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + assert find_min(nums, 0, len(nums) - 1) == 1 diff --git a/maths/floor.py b/maths/floor.py new file mode 100644 index 000000000000..ae6e5129a6ff --- /dev/null +++ b/maths/floor.py @@ -0,0 +1,20 @@ +def floor(x) -> int: + """ + Return the floor of x as an Integral. + + :param x: the number + :return: the largest integer <= x. + + >>> import math + >>> all(floor(n) == math.floor(n) for n in (1, -1, 0, -0, 1.1, -1.1, 1.0, -1.0, 1_000_000_000)) + True + """ + return ( + x if isinstance(x, int) or x - int(x) == 0 else int(x) if x > 0 else int(x - 1) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/gamma.py b/maths/gamma.py new file mode 100644 index 000000000000..ef5e7dae6187 --- /dev/null +++ b/maths/gamma.py @@ -0,0 +1,60 @@ +import math +from scipy.integrate import quad +from numpy import inf + + +def gamma(num: float) -> float: + """ + https://en.wikipedia.org/wiki/Gamma_function + In mathematics, the gamma function is one commonly + used extension of the factorial function to complex numbers. + The gamma function is defined for all complex numbers except the non-positive integers + + + >>> gamma(-1) + Traceback (most recent call last): + ... + ValueError: math domain error + + + + >>> gamma(0) + Traceback (most recent call last): + ... + ValueError: math domain error + + + >>> gamma(9) + 40320.0 + + >>> from math import gamma as math_gamma + >>> all(gamma(i)/math_gamma(i) <= 1.000000001 and abs(gamma(i)/math_gamma(i)) > .99999999 for i in range(1, 50)) + True + + + >>> from math import gamma as math_gamma + >>> gamma(-1)/math_gamma(-1) <= 1.000000001 + Traceback (most recent call last): + ... + ValueError: math domain error + + + >>> from math import gamma as math_gamma + >>> gamma(3.3) - math_gamma(3.3) <= 0.00000001 + True + """ + + if num <= 0: + raise ValueError("math domain error") + + return quad(integrand, 0, inf, args=(num))[0] + + +def integrand(x: float, z: float) -> float: + return math.pow(x, z - 1) * math.exp(-x) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/gaussian.py b/maths/gaussian.py index f3a47a3f6a1b..ffea20fb2ba1 100644 --- a/maths/gaussian.py +++ b/maths/gaussian.py @@ -1,4 +1,3 @@ - """ Reference: https://en.wikipedia.org/wiki/Gaussian_function @@ -9,7 +8,6 @@ from numpy import pi, sqrt, exp - def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int: """ >>> gaussian(1) @@ -52,7 +50,7 @@ def gaussian(x, mu: float = 0.0, sigma: float = 1.0) -> int: >>> gaussian(2523, mu=234234, sigma=3425) 0.0 """ - return 1 / sqrt(2 * pi * sigma ** 2) * exp(-(x - mu) ** 2 / 2 * sigma ** 2) + return 1 / sqrt(2 * pi * sigma ** 2) * exp(-((x - mu) ** 2) / 2 * sigma ** 2) if __name__ == "__main__": diff --git a/maths/greater_common_divisor.py b/maths/greater_common_divisor.py deleted file mode 100644 index adc7811e8317..000000000000 --- a/maths/greater_common_divisor.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Greater Common Divisor. - -Wikipedia reference: https://en.wikipedia.org/wiki/Greatest_common_divisor -""" - - -def gcd(a, b): - """Calculate Greater Common Divisor (GCD).""" - return b if a == 0 else gcd(b % a, a) - - -def main(): - """Call GCD Function.""" - try: - nums = input("Enter two Integers separated by comma (,): ").split(',') - num_1 = int(nums[0]) - num_2 = int(nums[1]) - except (IndexError, UnboundLocalError, ValueError): - print("Wrong Input") - print(f"gcd({num_1}, {num_2}) = {gcd(num_1, num_2)}") - - -if __name__ == '__main__': - main() diff --git a/maths/greatest_common_divisor.py b/maths/greatest_common_divisor.py new file mode 100644 index 000000000000..21c427d5b227 --- /dev/null +++ b/maths/greatest_common_divisor.py @@ -0,0 +1,61 @@ +""" +Greatest Common Divisor. + +Wikipedia reference: https://en.wikipedia.org/wiki/Greatest_common_divisor +""" + + +def greatest_common_divisor(a, b): + """ + Calculate Greatest Common Divisor (GCD). + >>> greatest_common_divisor(24, 40) + 8 + >>> greatest_common_divisor(1, 1) + 1 + >>> greatest_common_divisor(1, 800) + 1 + >>> greatest_common_divisor(11, 37) + 1 + >>> greatest_common_divisor(3, 5) + 1 + >>> greatest_common_divisor(16, 4) + 4 + """ + return b if a == 0 else greatest_common_divisor(b % a, a) + + +""" +Below method is more memory efficient because it does not use the stack (chunk of memory). +While above method is good, uses more memory for huge numbers because of the recursive calls +required to calculate the greatest common divisor. +""" + + +def gcd_by_iterative(x, y): + """ + >>> gcd_by_iterative(24, 40) + 8 + >>> greatest_common_divisor(24, 40) == gcd_by_iterative(24, 40) + True + """ + while y: # --> when y=0 then loop will terminate and return x as final GCD. + x, y = y, x % y + return x + + +def main(): + """Call Greatest Common Divisor function.""" + try: + nums = input("Enter two integers separated by comma (,): ").split(",") + num_1 = int(nums[0]) + num_2 = int(nums[1]) + print( + f"greatest_common_divisor({num_1}, {num_2}) = {greatest_common_divisor(num_1, num_2)}" + ) + print(f"By iterative gcd({num_1}, {num_2}) = {gcd_by_iterative(num_1, num_2)}") + except (IndexError, UnboundLocalError, ValueError): + print("Wrong input") + + +if __name__ == "__main__": + main() diff --git a/maths/hardy_ramanujanalgo.py b/maths/hardy_ramanujanalgo.py new file mode 100644 index 000000000000..90e4913c70a7 --- /dev/null +++ b/maths/hardy_ramanujanalgo.py @@ -0,0 +1,45 @@ +# This theorem states that the number of prime factors of n +# will be approximately log(log(n)) for most natural numbers n + +import math + + +def exactPrimeFactorCount(n): + """ + >>> exactPrimeFactorCount(51242183) + 3 + """ + count = 0 + if n % 2 == 0: + count += 1 + while n % 2 == 0: + n = int(n / 2) + # the n input value must be odd so that + # we can skip one element (ie i += 2) + + i = 3 + + while i <= int(math.sqrt(n)): + if n % i == 0: + count += 1 + while n % i == 0: + n = int(n / i) + i = i + 2 + + # this condition checks the prime + # number n is greater than 2 + + if n > 2: + count += 1 + return count + + +if __name__ == "__main__": + n = 51242183 + print(f"The number of distinct prime factors is/are {exactPrimeFactorCount(n)}") + print("The value of log(log(n)) is {:.4f}".format(math.log(math.log(n)))) + + """ + The number of distinct prime factors is/are 3 + The value of log(log(n)) is 2.8765 + """ diff --git a/maths/jaccard_similarity.py b/maths/jaccard_similarity.py new file mode 100644 index 000000000000..4f24d308f340 --- /dev/null +++ b/maths/jaccard_similarity.py @@ -0,0 +1,80 @@ +""" +The Jaccard similarity coefficient is a commonly used indicator of the +similarity between two sets. Let U be a set and A and B be subsets of U, +then the Jaccard index/similarity is defined to be the ratio of the number +of elements of their intersection and the number of elements of their union. + +Inspired from Wikipedia and +the book Mining of Massive Datasets [MMDS 2nd Edition, Chapter 3] + +https://en.wikipedia.org/wiki/Jaccard_index +https://mmds.org + +Jaccard similarity is widely used with MinHashing. +""" + + +def jaccard_similariy(setA, setB, alternativeUnion=False): + """ + Finds the jaccard similarity between two sets. + Essentially, its intersection over union. + + The alternative way to calculate this is to take union as sum of the + number of items in the two sets. This will lead to jaccard similarity + of a set with itself be 1/2 instead of 1. [MMDS 2nd Edition, Page 77] + + Parameters: + :setA (set,list,tuple): A non-empty set/list + :setB (set,list,tuple): A non-empty set/list + :alternativeUnion (boolean): If True, use sum of number of + items as union + + Output: + (float) The jaccard similarity between the two sets. + + Examples: + >>> setA = {'a', 'b', 'c', 'd', 'e'} + >>> setB = {'c', 'd', 'e', 'f', 'h', 'i'} + >>> jaccard_similariy(setA,setB) + 0.375 + + >>> jaccard_similariy(setA,setA) + 1.0 + + >>> jaccard_similariy(setA,setA,True) + 0.5 + + >>> setA = ['a', 'b', 'c', 'd', 'e'] + >>> setB = ('c', 'd', 'e', 'f', 'h', 'i') + >>> jaccard_similariy(setA,setB) + 0.375 + """ + + if isinstance(setA, set) and isinstance(setB, set): + + intersection = len(setA.intersection(setB)) + + if alternativeUnion: + union = len(setA) + len(setB) + else: + union = len(setA.union(setB)) + + return intersection / union + + if isinstance(setA, (list, tuple)) and isinstance(setB, (list, tuple)): + + intersection = [element for element in setA if element in setB] + + if alternativeUnion: + union = len(setA) + len(setB) + else: + union = setA + [element for element in setB if element not in setA] + + return len(intersection) / len(union) + + +if __name__ == "__main__": + + setA = {"a", "b", "c", "d", "e"} + setB = {"c", "d", "e", "f", "h", "i"} + print(jaccard_similariy(setA, setB)) diff --git a/maths/karatsuba.py b/maths/karatsuba.py new file mode 100644 index 000000000000..df29c77a5cf2 --- /dev/null +++ b/maths/karatsuba.py @@ -0,0 +1,32 @@ +""" Multiply two numbers using Karatsuba algorithm """ + + +def karatsuba(a, b): + """ + >>> karatsuba(15463, 23489) == 15463 * 23489 + True + >>> karatsuba(3, 9) == 3 * 9 + True + """ + if len(str(a)) == 1 or len(str(b)) == 1: + return a * b + else: + m1 = max(len(str(a)), len(str(b))) + m2 = m1 // 2 + + a1, a2 = divmod(a, 10 ** m2) + b1, b2 = divmod(b, 10 ** m2) + + x = karatsuba(a2, b2) + y = karatsuba((a1 + a2), (b1 + b2)) + z = karatsuba(a1, b1) + + return (z * 10 ** (2 * m2)) + ((y - z - x) * 10 ** (m2)) + (x) + + +def main(): + print(karatsuba(15463, 23489)) + + +if __name__ == "__main__": + main() diff --git a/maths/kth_lexicographic_permutation.py b/maths/kth_lexicographic_permutation.py new file mode 100644 index 000000000000..1820be7274e3 --- /dev/null +++ b/maths/kth_lexicographic_permutation.py @@ -0,0 +1,40 @@ +def kthPermutation(k, n): + """ + Finds k'th lexicographic permutation (in increasing order) of + 0,1,2,...n-1 in O(n^2) time. + + Examples: + First permutation is always 0,1,2,...n + >>> kthPermutation(0,5) + [0, 1, 2, 3, 4] + + The order of permutation of 0,1,2,3 is [0,1,2,3], [0,1,3,2], [0,2,1,3], + [0,2,3,1], [0,3,1,2], [0,3,2,1], [1,0,2,3], [1,0,3,2], [1,2,0,3], + [1,2,3,0], [1,3,0,2] + >>> kthPermutation(10,4) + [1, 3, 0, 2] + """ + # Factorails from 1! to (n-1)! + factorials = [1] + for i in range(2, n): + factorials.append(factorials[-1] * i) + assert 0 <= k < factorials[-1] * n, "k out of bounds" + + permutation = [] + elements = list(range(n)) + + # Find permutation + while factorials: + factorial = factorials.pop() + number, k = divmod(k, factorial) + permutation.append(elements[number]) + elements.remove(elements[number]) + permutation.append(elements[0]) + + return permutation + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/largest_of_very_large_numbers.py b/maths/largest_of_very_large_numbers.py new file mode 100644 index 000000000000..d2dc0af18126 --- /dev/null +++ b/maths/largest_of_very_large_numbers.py @@ -0,0 +1,35 @@ +# Author: Abhijeeth S + +import math + + +def res(x, y): + if 0 not in (x, y): + # We use the relation x^y = y*log10(x), where 10 is the base. + return y * math.log10(x) + else: + if x == 0: # 0 raised to any number is 0 + return 0 + elif y == 0: + return 1 # any number raised to 0 is 1 + + +if __name__ == "__main__": # Main function + # Read two numbers from input and typecast them to int using map function. + # Here x is the base and y is the power. + prompt = "Enter the base and the power separated by a comma: " + x1, y1 = map(int, input(prompt).split(",")) + x2, y2 = map(int, input(prompt).split(",")) + + # We find the log of each number, using the function res(), which takes two + # arguments. + res1 = res(x1, y1) + res2 = res(x2, y2) + + # We check for the largest number + if res1 > res2: + print("Largest number is", x1, "^", y1) + elif res2 > res1: + print("Largest number is", x2, "^", y2) + else: + print("Both are equal") diff --git a/maths/least_common_multiple.py b/maths/least_common_multiple.py new file mode 100644 index 000000000000..863744e182b6 --- /dev/null +++ b/maths/least_common_multiple.py @@ -0,0 +1,44 @@ +import unittest + + +def find_lcm(first_num: int, second_num: int) -> int: + """Find the least common multiple of two numbers. + + Learn more: https://en.wikipedia.org/wiki/Least_common_multiple + + >>> find_lcm(5,2) + 10 + >>> find_lcm(12,76) + 228 + """ + max_num = first_num if first_num >= second_num else second_num + common_mult = max_num + while (common_mult % first_num > 0) or (common_mult % second_num > 0): + common_mult += max_num + return common_mult + + +class TestLeastCommonMultiple(unittest.TestCase): + + test_inputs = [ + (10, 20), + (13, 15), + (4, 31), + (10, 42), + (43, 34), + (5, 12), + (12, 25), + (10, 25), + (6, 9), + ] + expected_results = [20, 195, 124, 210, 1462, 60, 300, 50, 18] + + def test_lcm_function(self): + for i, (first_num, second_num) in enumerate(self.test_inputs): + actual_result = find_lcm(first_num, second_num) + with self.subTest(i=i): + self.assertEqual(actual_result, self.expected_results[i]) + + +if __name__ == "__main__": + unittest.main() diff --git a/maths/line_length.py b/maths/line_length.py new file mode 100644 index 000000000000..0b1ddb5b7866 --- /dev/null +++ b/maths/line_length.py @@ -0,0 +1,65 @@ +from typing import Callable, Union +import math as m + + +def line_length( + fnc: Callable[[Union[int, float]], Union[int, float]], + x_start: Union[int, float], + x_end: Union[int, float], + steps: int = 100, +) -> float: + + """ + Approximates the arc length of a line segment by treating the curve as a + sequence of linear lines and summing their lengths + :param fnc: a function which defines a curve + :param x_start: left end point to indicate the start of line segment + :param x_end: right end point to indicate end of line segment + :param steps: an accuracy gauge; more steps increases accuracy + :return: a float representing the length of the curve + + >>> def f(x): + ... return x + >>> f"{line_length(f, 0, 1, 10):.6f}" + '1.414214' + + >>> def f(x): + ... return 1 + >>> f"{line_length(f, -5.5, 4.5):.6f}" + '10.000000' + + >>> def f(x): + ... return m.sin(5 * x) + m.cos(10 * x) + x * x/10 + >>> f"{line_length(f, 0.0, 10.0, 10000):.6f}" + '69.534930' + """ + + x1 = x_start + fx1 = fnc(x_start) + length = 0.0 + + for i in range(steps): + + # Approximates curve as a sequence of linear lines and sums their length + x2 = (x_end - x_start) / steps + x1 + fx2 = fnc(x2) + length += m.hypot(x2 - x1, fx2 - fx1) + + # Increment step + x1 = x2 + fx1 = fx2 + + return length + + +if __name__ == "__main__": + + def f(x): + return m.sin(10 * x) + + print("f(x) = sin(10 * x)") + print("The length of the curve from x = -10 to x = 10 is:") + i = 10 + while i <= 100000: + print(f"With {i} steps: {line_length(f, -10, 10, i)}") + i *= 10 diff --git a/maths/lucas_lehmer_primality_test.py b/maths/lucas_lehmer_primality_test.py new file mode 100644 index 000000000000..8dac658f16d1 --- /dev/null +++ b/maths/lucas_lehmer_primality_test.py @@ -0,0 +1,41 @@ +""" + In mathematics, the Lucas–Lehmer test (LLT) is a primality test for Mersenne numbers. + https://en.wikipedia.org/wiki/Lucas%E2%80%93Lehmer_primality_test + + A Mersenne number is a number that is one less than a power of two. + That is M_p = 2^p - 1 + https://en.wikipedia.org/wiki/Mersenne_prime + + The Lucas–Lehmer test is the primality test used by the + Great Internet Mersenne Prime Search (GIMPS) to locate large primes. +""" + + +# Primality test 2^p - 1 +# Return true if 2^p - 1 is prime +def lucas_lehmer_test(p: int) -> bool: + """ + >>> lucas_lehmer_test(p=7) + True + + >>> lucas_lehmer_test(p=11) + False + + # M_11 = 2^11 - 1 = 2047 = 23 * 89 + """ + + if p < 2: + raise ValueError("p should not be less than 2!") + elif p == 2: + return True + + s = 4 + M = (1 << p) - 1 + for i in range(p - 2): + s = ((s * s) - 2) % M + return s == 0 + + +if __name__ == "__main__": + print(lucas_lehmer_test(7)) + print(lucas_lehmer_test(11)) diff --git a/maths/lucas_series.py b/maths/lucas_series.py index 9ae437dc9f54..22ad893a6567 100644 --- a/maths/lucas_series.py +++ b/maths/lucas_series.py @@ -1,5 +1,6 @@ # Lucas Sequence Using Recursion + def recur_luc(n): """ >>> recur_luc(1) diff --git a/maths/matrix_exponentiation.py b/maths/matrix_exponentiation.py new file mode 100644 index 000000000000..56d03b56fc1f --- /dev/null +++ b/maths/matrix_exponentiation.py @@ -0,0 +1,100 @@ +"""Matrix Exponentiation""" + +import timeit + +""" +Matrix Exponentiation is a technique to solve linear recurrences in logarithmic time. +You read more about it here: +http://zobayer.blogspot.com/2010/11/matrix-exponentiation.html +https://www.hackerearth.com/practice/notes/matrix-exponentiation-1/ +""" + + +class Matrix: + def __init__(self, arg): + if isinstance(arg, list): # Initializes a matrix identical to the one provided. + self.t = arg + self.n = len(arg) + else: # Initializes a square matrix of the given size and set the values to zero. + self.n = arg + self.t = [[0 for _ in range(self.n)] for _ in range(self.n)] + + def __mul__(self, b): + matrix = Matrix(self.n) + for i in range(self.n): + for j in range(self.n): + for k in range(self.n): + matrix.t[i][j] += self.t[i][k] * b.t[k][j] + return matrix + + +def modular_exponentiation(a, b): + matrix = Matrix([[1, 0], [0, 1]]) + while b > 0: + if b & 1: + matrix *= a + a *= a + b >>= 1 + return matrix + + +def fibonacci_with_matrix_exponentiation(n, f1, f2): + # Trivial Cases + if n == 1: + return f1 + elif n == 2: + return f2 + matrix = Matrix([[1, 1], [1, 0]]) + matrix = modular_exponentiation(matrix, n - 2) + return f2 * matrix.t[0][0] + f1 * matrix.t[0][1] + + +def simple_fibonacci(n, f1, f2): + # Trivial Cases + if n == 1: + return f1 + elif n == 2: + return f2 + + fn_1 = f1 + fn_2 = f2 + n -= 2 + + while n > 0: + fn_1, fn_2 = fn_1 + fn_2, fn_1 + n -= 1 + + return fn_1 + + +def matrix_exponentiation_time(): + setup = """ +from random import randint +from __main__ import fibonacci_with_matrix_exponentiation +""" + code = "fibonacci_with_matrix_exponentiation(randint(1,70000), 1, 1)" + exec_time = timeit.timeit(setup=setup, stmt=code, number=100) + print("With matrix exponentiation the average execution time is ", exec_time / 100) + return exec_time + + +def simple_fibonacci_time(): + setup = """ +from random import randint +from __main__ import simple_fibonacci +""" + code = "simple_fibonacci(randint(1,70000), 1, 1)" + exec_time = timeit.timeit(setup=setup, stmt=code, number=100) + print( + "Without matrix exponentiation the average execution time is ", exec_time / 100 + ) + return exec_time + + +def main(): + matrix_exponentiation_time() + simple_fibonacci_time() + + +if __name__ == "__main__": + main() diff --git a/maths/miller_rabin.py b/maths/miller_rabin.py new file mode 100644 index 000000000000..fe992027190b --- /dev/null +++ b/maths/miller_rabin.py @@ -0,0 +1,50 @@ +import random + +from .binary_exp_mod import bin_exp_mod + + +# This is a probabilistic check to test primality, useful for big numbers! +# if it's a prime, it will return true +# if it's not a prime, the chance of it returning true is at most 1/4**prec +def is_prime(n, prec=1000): + """ + >>> from .prime_check import prime_check + >>> all(is_prime(i) == prime_check(i) for i in range(1000)) + True + """ + if n < 2: + return False + + if n % 2 == 0: + return n == 2 + + # this means n is odd + d = n - 1 + exp = 0 + while d % 2 == 0: + d /= 2 + exp += 1 + + # n - 1=d*(2**exp) + count = 0 + while count < prec: + a = random.randint(2, n - 1) + b = bin_exp_mod(a, d, n) + if b != 1: + flag = True + for i in range(exp): + if b == n - 1: + flag = False + break + b = b * b + b %= n + if flag: + return False + count += 1 + return True + + +if __name__ == "__main__": + n = abs(int(input("Enter bound : ").strip())) + print("Here's the list of primes:") + print(", ".join(str(i) for i in range(n + 1) if is_prime(i))) diff --git a/maths/mobius_function.py b/maths/mobius_function.py index 15fb3d4380f4..df0f66177501 100644 --- a/maths/mobius_function.py +++ b/maths/mobius_function.py @@ -1,5 +1,5 @@ """ -Refrences: https://en.wikipedia.org/wiki/M%C3%B6bius_function +References: https://en.wikipedia.org/wiki/M%C3%B6bius_function References: wikipedia:square free number python/black : True flake8 : True diff --git a/maths/modular_exponential.py b/maths/modular_exponential.py index 750de7cba99e..8b7b17575a33 100644 --- a/maths/modular_exponential.py +++ b/maths/modular_exponential.py @@ -1,8 +1,22 @@ -"""Modular Exponential.""" +""" + Modular Exponential. + Modular exponentiation is a type of exponentiation performed over a modulus. + For more explanation, please check https://en.wikipedia.org/wiki/Modular_exponentiation +""" +"""Calculate Modular Exponential.""" + + +def modular_exponential(base: int, power: int, mod: int): + """ + >>> modular_exponential(5, 0, 10) + 1 + >>> modular_exponential(2, 8, 7) + 4 + >>> modular_exponential(3, -2, 9) + -1 + """ -def modular_exponential(base, power, mod): - """Calculate Modular Exponential.""" if power < 0: return -1 base %= mod @@ -13,6 +27,7 @@ def modular_exponential(base, power, mod): result = (result * base) % mod power = power >> 1 base = (base * base) % mod + return result @@ -21,5 +36,9 @@ def main(): print(modular_exponential(3, 200, 13)) -if __name__ == '__main__': +if __name__ == "__main__": + import doctest + + doctest.testmod() + main() diff --git a/maths/monte_carlo.py b/maths/monte_carlo.py new file mode 100644 index 000000000000..dedca9f6cdf5 --- /dev/null +++ b/maths/monte_carlo.py @@ -0,0 +1,129 @@ +""" +@author: MatteoRaso +""" +from math import pi, sqrt +from random import uniform +from statistics import mean +from typing import Callable + + +def pi_estimator(iterations: int): + """ + An implementation of the Monte Carlo method used to find pi. + 1. Draw a 2x2 square centred at (0,0). + 2. Inscribe a circle within the square. + 3. For each iteration, place a dot anywhere in the square. + a. Record the number of dots within the circle. + 4. After all the dots are placed, divide the dots in the circle by the total. + 5. Multiply this value by 4 to get your estimate of pi. + 6. Print the estimated and numpy value of pi + """ + # A local function to see if a dot lands in the circle. + def is_in_circle(x: float, y: float) -> bool: + distance_from_centre = sqrt((x ** 2) + (y ** 2)) + # Our circle has a radius of 1, so a distance + # greater than 1 would land outside the circle. + return distance_from_centre <= 1 + + # The proportion of guesses that landed in the circle + proportion = mean( + int(is_in_circle(uniform(-1.0, 1.0), uniform(-1.0, 1.0))) + for _ in range(iterations) + ) + # The ratio of the area for circle to square is pi/4. + pi_estimate = proportion * 4 + print(f"The estimated value of pi is {pi_estimate}") + print(f"The numpy value of pi is {pi}") + print(f"The total error is {abs(pi - pi_estimate)}") + + +def area_under_curve_estimator( + iterations: int, + function_to_integrate: Callable[[float], float], + min_value: float = 0.0, + max_value: float = 1.0, +) -> float: + """ + An implementation of the Monte Carlo method to find area under + a single variable non-negative real-valued continuous function, + say f(x), where x lies within a continuous bounded interval, + say [min_value, max_value], where min_value and max_value are + finite numbers + 1. Let x be a uniformly distributed random variable between min_value to + max_value + 2. Expected value of f(x) = + (integrate f(x) from min_value to max_value)/(max_value - min_value) + 3. Finding expected value of f(x): + a. Repeatedly draw x from uniform distribution + b. Evaluate f(x) at each of the drawn x values + c. Expected value = average of the function evaluations + 4. Estimated value of integral = Expected value * (max_value - min_value) + 5. Returns estimated value + """ + + return mean( + function_to_integrate(uniform(min_value, max_value)) for _ in range(iterations) + ) * (max_value - min_value) + + +def area_under_line_estimator_check( + iterations: int, min_value: float = 0.0, max_value: float = 1.0 +) -> None: + """ + Checks estimation error for area_under_curve_estimator function + for f(x) = x where x lies within min_value to max_value + 1. Calls "area_under_curve_estimator" function + 2. Compares with the expected value + 3. Prints estimated, expected and error value + """ + + def identity_function(x: float) -> float: + """ + Represents identity function + >>> [function_to_integrate(x) for x in [-2.0, -1.0, 0.0, 1.0, 2.0]] + [-2.0, -1.0, 0.0, 1.0, 2.0] + """ + return x + + estimated_value = area_under_curve_estimator( + iterations, identity_function, min_value, max_value + ) + expected_value = (max_value * max_value - min_value * min_value) / 2 + + print("******************") + print(f"Estimating area under y=x where x varies from {min_value} to {max_value}") + print(f"Estimated value is {estimated_value}") + print(f"Expected value is {expected_value}") + print(f"Total error is {abs(estimated_value - expected_value)}") + print("******************") + + +def pi_estimator_using_area_under_curve(iterations: int) -> None: + """ + Area under curve y = sqrt(4 - x^2) where x lies in 0 to 2 is equal to pi + """ + + def function_to_integrate(x: float) -> float: + """ + Represents semi-circle with radius 2 + >>> [function_to_integrate(x) for x in [-2.0, 0.0, 2.0]] + [0.0, 2.0, 0.0] + """ + return sqrt(4.0 - x * x) + + estimated_value = area_under_curve_estimator( + iterations, function_to_integrate, 0.0, 2.0 + ) + + print("******************") + print("Estimating pi using area_under_curve_estimator") + print(f"Estimated value is {estimated_value}") + print(f"Expected value is {pi}") + print(f"Total error is {abs(estimated_value - pi)}") + print("******************") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/monte_carlo_dice.py b/maths/monte_carlo_dice.py new file mode 100644 index 000000000000..c36c3e83e00b --- /dev/null +++ b/maths/monte_carlo_dice.py @@ -0,0 +1,46 @@ +import random +from typing import List + + +class Dice: + NUM_SIDES = 6 + + def __init__(self): + """ Initialize a six sided dice """ + self.sides = list(range(1, Dice.NUM_SIDES + 1)) + + def roll(self): + return random.choice(self.sides) + + def _str_(self): + return "Fair Dice" + + +def throw_dice(num_throws: int, num_dice: int = 2) -> List[float]: + """ + Return probability list of all possible sums when throwing dice. + + >>> random.seed(0) + >>> throw_dice(10, 1) + [10.0, 0.0, 30.0, 50.0, 10.0, 0.0] + >>> throw_dice(100, 1) + [19.0, 17.0, 17.0, 11.0, 23.0, 13.0] + >>> throw_dice(1000, 1) + [18.8, 15.5, 16.3, 17.6, 14.2, 17.6] + >>> throw_dice(10000, 1) + [16.35, 16.89, 16.93, 16.6, 16.52, 16.71] + >>> throw_dice(10000, 2) + [2.74, 5.6, 7.99, 11.26, 13.92, 16.7, 14.44, 10.63, 8.05, 5.92, 2.75] + """ + dices = [Dice() for i in range(num_dice)] + count_of_sum = [0] * (len(dices) * Dice.NUM_SIDES + 1) + for i in range(num_throws): + count_of_sum[sum([dice.roll() for dice in dices])] += 1 + probability = [round((count * 100) / num_throws, 2) for count in count_of_sum] + return probability[num_dice:] # remove probability of sums that never appear + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/newton_raphson.py b/maths/newton_raphson.py index d89f264acdd8..c4975c73e037 100644 --- a/maths/newton_raphson.py +++ b/maths/newton_raphson.py @@ -1,4 +1,4 @@ -''' +""" Author: P Shreyas Shetty Implementation of Newton-Raphson method for solving equations of kind f(x) = 0. It is an iterative method where solution is found by the expression @@ -6,27 +6,29 @@ If no solution exists, then either the solution will not be found when iteration limit is reached or the gradient f'(x[n]) approaches zero. In both cases, exception is raised. If iteration limit is reached, try increasing maxiter. - ''' + """ import math as m + def calc_derivative(f, a, h=0.001): - ''' + """ Calculates derivative at point a for function f using finite difference method - ''' - return (f(a+h)-f(a-h))/(2*h) + """ + return (f(a + h) - f(a - h)) / (2 * h) + + +def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6, logsteps=False): -def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6,logsteps=False): - - a = x0 #set the initial guess + a = x0 # set the initial guess steps = [a] error = abs(f(a)) - f1 = lambda x:calc_derivative(f, x, h=step) #Derivative of f(x) + f1 = lambda x: calc_derivative(f, x, h=step) # Derivative of f(x) for _ in range(maxiter): if f1(a) == 0: raise ValueError("No converging solution found") - a = a - f(a)/f1(a) #Calculate the next estimate + a = a - f(a) / f1(a) # Calculate the next estimate if logsteps: steps.append(a) if error < maxerror: @@ -34,16 +36,20 @@ def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6,logsteps=Fal else: raise ValueError("Iteration limit reached, no converging solution found") if logsteps: - #If logstep is true, then log intermediate steps + # If logstep is true, then log intermediate steps return a, error, steps return a, error - -if __name__ == '__main__': + + +if __name__ == "__main__": import matplotlib.pyplot as plt - f = lambda x:m.tanh(x)**2-m.exp(3*x) - solution, error, steps = newton_raphson(f, x0=10, maxiter=1000, step=1e-6, logsteps=True) + + f = lambda x: m.tanh(x) ** 2 - m.exp(3 * x) + solution, error, steps = newton_raphson( + f, x0=10, maxiter=1000, step=1e-6, logsteps=True + ) plt.plot([abs(f(x)) for x in steps]) plt.xlabel("step") plt.ylabel("error") plt.show() - print("solution = {%f}, error = {%f}" % (solution, error)) + print(f"solution = {{{solution:f}}}, error = {{{error:f}}}") diff --git a/maths/numerical_integration.py b/maths/numerical_integration.py new file mode 100644 index 000000000000..67fbc0ddbf30 --- /dev/null +++ b/maths/numerical_integration.py @@ -0,0 +1,66 @@ +""" +Approximates the area under the curve using the trapezoidal rule +""" + +from typing import Callable, Union + + +def trapezoidal_area( + fnc: Callable[[Union[int, float]], Union[int, float]], + x_start: Union[int, float], + x_end: Union[int, float], + steps: int = 100, +) -> float: + + """ + Treats curve as a collection of linear lines and sums the area of the + trapezium shape they form + :param fnc: a function which defines a curve + :param x_start: left end point to indicate the start of line segment + :param x_end: right end point to indicate end of line segment + :param steps: an accuracy gauge; more steps increases the accuracy + :return: a float representing the length of the curve + + >>> def f(x): + ... return 5 + >>> '%.3f' % trapezoidal_area(f, 12.0, 14.0, 1000) + '10.000' + + >>> def f(x): + ... return 9*x**2 + >>> '%.4f' % trapezoidal_area(f, -4.0, 0, 10000) + '192.0000' + + >>> '%.4f' % trapezoidal_area(f, -4.0, 4.0, 10000) + '384.0000' + """ + x1 = x_start + fx1 = fnc(x_start) + area = 0.0 + + for i in range(steps): + + # Approximates small segments of curve as linear and solve + # for trapezoidal area + x2 = (x_end - x_start) / steps + x1 + fx2 = fnc(x2) + area += abs(fx2 + fx1) * (x2 - x1) / 2 + + # Increment step + x1 = x2 + fx1 = fx2 + return area + + +if __name__ == "__main__": + + def f(x): + return x ** 3 + + print("f(x) = x^3") + print("The area between the curve, x = -10, x = 10 and the x axis is:") + i = 10 + while i <= 100000: + area = trapezoidal_area(f, -5, 5, i) + print("with {} steps: {}".format(i, area)) + i *= 10 diff --git a/maths/perfect_square.py b/maths/perfect_square.py new file mode 100644 index 000000000000..3e7a1c07a75f --- /dev/null +++ b/maths/perfect_square.py @@ -0,0 +1,27 @@ +import math + + +def perfect_square(num: int) -> bool: + """ + Check if a number is perfect square number or not + :param num: the number to be checked + :return: True if number is square number, otherwise False + + >>> perfect_square(9) + True + >>> perfect_square(16) + True + >>> perfect_square(1) + True + >>> perfect_square(0) + True + >>> perfect_square(10) + False + """ + return math.sqrt(num) * math.sqrt(num) == num + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/pi_monte_carlo_estimation.py b/maths/pi_monte_carlo_estimation.py new file mode 100644 index 000000000000..20b46dddc6e5 --- /dev/null +++ b/maths/pi_monte_carlo_estimation.py @@ -0,0 +1,67 @@ +import random + + +class Point: + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y + + def is_in_unit_circle(self) -> bool: + """ + True, if the point lies in the unit circle + False, otherwise + """ + return (self.x ** 2 + self.y ** 2) <= 1 + + @classmethod + def random_unit_square(cls): + """ + Generates a point randomly drawn from the unit square [0, 1) x [0, 1). + """ + return cls(x=random.random(), y=random.random()) + + +def estimate_pi(number_of_simulations: int) -> float: + """ + Generates an estimate of the mathematical constant PI. + See https://en.wikipedia.org/wiki/Monte_Carlo_method#Overview + + The estimate is generated by Monte Carlo simulations. Let U be uniformly drawn from + the unit square [0, 1) x [0, 1). The probability that U lies in the unit circle is: + + P[U in unit circle] = 1/4 PI + + and therefore + + PI = 4 * P[U in unit circle] + + We can get an estimate of the probability P[U in unit circle]. + See https://en.wikipedia.org/wiki/Empirical_probability by: + + 1. Draw a point uniformly from the unit square. + 2. Repeat the first step n times and count the number of points in the unit + circle, which is called m. + 3. An estimate of P[U in unit circle] is m/n + """ + if number_of_simulations < 1: + raise ValueError("At least one simulation is necessary to estimate PI.") + + number_in_unit_circle = 0 + for simulation_index in range(number_of_simulations): + random_point = Point.random_unit_square() + + if random_point.is_in_unit_circle(): + number_in_unit_circle += 1 + + return 4 * number_in_unit_circle / number_of_simulations + + +if __name__ == "__main__": + # import doctest + + # doctest.testmod() + from math import pi + + prompt = "Please enter the desired number of Monte Carlo simulations: " + my_pi = estimate_pi(int(input(prompt).strip())) + print(f"An estimate of PI is {my_pi} with an error of {abs(my_pi - pi)}") diff --git a/maths/polynomial_evaluation.py b/maths/polynomial_evaluation.py new file mode 100644 index 000000000000..d2394f398c36 --- /dev/null +++ b/maths/polynomial_evaluation.py @@ -0,0 +1,53 @@ +from typing import Sequence + + +def evaluate_poly(poly: Sequence[float], x: float) -> float: + """Evaluate a polynomial f(x) at specified point x and return the value. + + Arguments: + poly -- the coeffiecients of a polynomial as an iterable in order of + ascending degree + x -- the point at which to evaluate the polynomial + + >>> evaluate_poly((0.0, 0.0, 5.0, 9.3, 7.0), 10.0) + 79800.0 + """ + return sum(c * (x ** i) for i, c in enumerate(poly)) + + +def horner(poly: Sequence[float], x: float) -> float: + """Evaluate a polynomial at specified point using Horner's method. + + In terms of computational complexity, Horner's method is an efficient method + of evaluating a polynomial. It avoids the use of expensive exponentiation, + and instead uses only multiplication and addition to evaluate the polynomial + in O(n), where n is the degree of the polynomial. + + https://en.wikipedia.org/wiki/Horner's_method + + Arguments: + poly -- the coeffiecients of a polynomial as an iterable in order of + ascending degree + x -- the point at which to evaluate the polynomial + + >>> horner((0.0, 0.0, 5.0, 9.3, 7.0), 10.0) + 79800.0 + """ + result = 0.0 + for coeff in reversed(poly): + result = result * x + coeff + return result + + +if __name__ == "__main__": + """ + Example: + >>> poly = (0.0, 0.0, 5.0, 9.3, 7.0) # f(x) = 7.0x^4 + 9.3x^3 + 5.0x^2 + >>> x = -13.0 + >>> print(evaluate_poly(poly, x)) # f(-13) = 7.0(-13)^4 + 9.3(-13)^3 + 5.0(-13)^2 = 180339.9 + 180339.9 + """ + poly = (0.0, 0.0, 5.0, 9.3, 7.0) + x = 10.0 + print(evaluate_poly(poly, x)) + print(horner(poly, x)) diff --git a/maths/prime_check.py b/maths/prime_check.py index 9249834dc069..e60281228fda 100644 --- a/maths/prime_check.py +++ b/maths/prime_check.py @@ -40,12 +40,13 @@ def test_primes(self): self.assertTrue(prime_check(29)) def test_not_primes(self): - self.assertFalse(prime_check(-19), - "Negative numbers are not prime.") - self.assertFalse(prime_check(0), - "Zero doesn't have any divider, primes must have two") - self.assertFalse(prime_check(1), - "One just have 1 divider, primes must have two.") + self.assertFalse(prime_check(-19), "Negative numbers are not prime.") + self.assertFalse( + prime_check(0), "Zero doesn't have any divider, primes must have two" + ) + self.assertFalse( + prime_check(1), "One just have 1 divider, primes must have two." + ) self.assertFalse(prime_check(2 * 2)) self.assertFalse(prime_check(2 * 3)) self.assertFalse(prime_check(3 * 3)) @@ -53,5 +54,5 @@ def test_not_primes(self): self.assertFalse(prime_check(3 * 5 * 7)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/maths/prime_numbers.py b/maths/prime_numbers.py new file mode 100644 index 000000000000..f9325996500c --- /dev/null +++ b/maths/prime_numbers.py @@ -0,0 +1,34 @@ +from typing import Generator + + +def primes(max: int) -> Generator[int, None, None]: + """ + Return a list of all primes numbers up to max. + >>> list(primes(0)) + [] + >>> list(primes(-1)) + [] + >>> list(primes(-10)) + [] + >>> list(primes(25)) + [2, 3, 5, 7, 11, 13, 17, 19, 23] + >>> list(primes(11)) + [2, 3, 5, 7, 11] + >>> list(primes(33)) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] + >>> list(primes(10000))[-1] + 9973 + """ + numbers: Generator = (i for i in range(1, (max + 1))) + for i in (n for n in numbers if n > 1): + for j in range(2, i): + if (i % j) == 0: + break + else: + yield i + + +if __name__ == "__main__": + number = int(input("Calculate primes up to:\n>> ").strip()) + for ret in primes(number): + print(ret) diff --git a/maths/prime_sieve_eratosthenes.py b/maths/prime_sieve_eratosthenes.py new file mode 100644 index 000000000000..05363cf62953 --- /dev/null +++ b/maths/prime_sieve_eratosthenes.py @@ -0,0 +1,42 @@ +""" +Sieve of Eratosthenes + +Input : n =10 +Output : 2 3 5 7 + +Input : n = 20 +Output: 2 3 5 7 11 13 17 19 + +you can read in detail about this at +https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes +""" + + +def prime_sieve_eratosthenes(num): + """ + print the prime numbers up to n + + >>> prime_sieve_eratosthenes(10) + 2 3 5 7 + >>> prime_sieve_eratosthenes(20) + 2 3 5 7 11 13 17 19 + """ + + primes = [True for i in range(num + 1)] + p = 2 + + while p * p <= num: + if primes[p]: + for i in range(p * p, num + 1, p): + primes[i] = False + p += 1 + + for prime in range(2, num + 1): + if primes[prime]: + print(prime, end=" ") + + +if __name__ == "__main__": + num = int(input()) + + prime_sieve_eratosthenes(num) diff --git a/maths/pythagoras.py b/maths/pythagoras.py new file mode 100644 index 000000000000..69a17731a0fd --- /dev/null +++ b/maths/pythagoras.py @@ -0,0 +1,33 @@ +"""Uses Pythagoras theorem to calculate the distance between two points in space.""" + +import math + + +class Point: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __repr__(self) -> str: + return f"Point({self.x}, {self.y}, {self.z})" + + +def distance(a: Point, b: Point) -> float: + return math.sqrt(abs((b.x - a.x) ** 2 + (b.y - a.y) ** 2 + (b.z - a.z) ** 2)) + + +def test_distance() -> None: + """ + >>> point1 = Point(2, -1, 7) + >>> point2 = Point(1, -3, 5) + >>> print(f"Distance from {point1} to {point2} is {distance(point1, point2)}") + Distance from Point(2, -1, 7) to Point(1, -3, 5) is 3.0 + """ + pass + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/qr_decomposition.py b/maths/qr_decomposition.py new file mode 100644 index 000000000000..5e15fede4f2a --- /dev/null +++ b/maths/qr_decomposition.py @@ -0,0 +1,71 @@ +import numpy as np + + +def qr_householder(A): + """Return a QR-decomposition of the matrix A using Householder reflection. + + The QR-decomposition decomposes the matrix A of shape (m, n) into an + orthogonal matrix Q of shape (m, m) and an upper triangular matrix R of + shape (m, n). Note that the matrix A does not have to be square. This + method of decomposing A uses the Householder reflection, which is + numerically stable and of complexity O(n^3). + + https://en.wikipedia.org/wiki/QR_decomposition#Using_Householder_reflections + + Arguments: + A -- a numpy.ndarray of shape (m, n) + + Note: several optimizations can be made for numeric efficiency, but this is + intended to demonstrate how it would be represented in a mathematics + textbook. In cases where efficiency is particularly important, an optimized + version from BLAS should be used. + + >>> A = np.array([[12, -51, 4], [6, 167, -68], [-4, 24, -41]], dtype=float) + >>> Q, R = qr_householder(A) + + >>> # check that the decomposition is correct + >>> np.allclose(Q@R, A) + True + + >>> # check that Q is orthogonal + >>> np.allclose(Q@Q.T, np.eye(A.shape[0])) + True + >>> np.allclose(Q.T@Q, np.eye(A.shape[0])) + True + + >>> # check that R is upper triangular + >>> np.allclose(np.triu(R), R) + True + """ + m, n = A.shape + t = min(m, n) + Q = np.eye(m) + R = A.copy() + + for k in range(t - 1): + # select a column of modified matrix A': + x = R[k:, [k]] + # construct first basis vector + e1 = np.zeros_like(x) + e1[0] = 1.0 + # determine scaling factor + alpha = np.linalg.norm(x) + # construct vector v for Householder reflection + v = x + np.sign(x[0]) * alpha * e1 + v /= np.linalg.norm(v) + + # construct the Householder matrix + Q_k = np.eye(m - k) - 2.0 * v @ v.T + # pad with ones and zeros as necessary + Q_k = np.block([[np.eye(k), np.zeros((k, m - k))], [np.zeros((m - k, k)), Q_k]]) + + Q = Q @ Q_k.T + R = Q_k @ R + + return Q, R + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/quadratic_equations_complex_numbers.py b/maths/quadratic_equations_complex_numbers.py new file mode 100644 index 000000000000..7c47bdef2297 --- /dev/null +++ b/maths/quadratic_equations_complex_numbers.py @@ -0,0 +1,37 @@ +from cmath import sqrt +from typing import Tuple + + +def quadratic_roots(a: int, b: int, c: int) -> Tuple[complex, complex]: + """ + Given the numerical coefficients a, b and c, + calculates the roots for any quadratic equation of the form ax^2 + bx + c + + >>> quadratic_roots(a=1, b=3, c=-4) + (1.0, -4.0) + >>> quadratic_roots(5, 6, 1) + (-0.2, -1.0) + >>> quadratic_roots(1, -6, 25) + ((3+4j), (3-4j)) + """ + + if a == 0: + raise ValueError("Coefficient 'a' must not be zero.") + delta = b * b - 4 * a * c + + root_1 = (-b + sqrt(delta)) / (2 * a) + root_2 = (-b - sqrt(delta)) / (2 * a) + + return ( + root_1.real if not root_1.imag else root_1, + root_2.real if not root_2.imag else root_2, + ) + + +def main(): + solutions = quadratic_roots(a=5, b=6, c=1) + print("The solutions are: {} and {}".format(*solutions)) + + +if __name__ == "__main__": + main() diff --git a/maths/radians.py b/maths/radians.py new file mode 100644 index 000000000000..3788b3e8a3a0 --- /dev/null +++ b/maths/radians.py @@ -0,0 +1,29 @@ +from math import pi + + +def radians(degree: float) -> float: + """ + Coverts the given angle from degrees to radians + https://en.wikipedia.org/wiki/Radian + + >>> radians(180) + 3.141592653589793 + >>> radians(92) + 1.6057029118347832 + >>> radians(274) + 4.782202150464463 + >>> radians(109.82) + 1.9167205845401725 + + >>> from math import radians as math_radians + >>> all(abs(radians(i)-math_radians(i)) <= 0.00000001 for i in range(-2, 361)) + True + """ + + return degree / (180 / pi) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/radix2_fft.py b/maths/radix2_fft.py new file mode 100644 index 000000000000..3911fea1d04d --- /dev/null +++ b/maths/radix2_fft.py @@ -0,0 +1,180 @@ +""" +Fast Polynomial Multiplication using radix-2 fast Fourier Transform. +""" + +import mpmath # for roots of unity +import numpy as np + + +class FFT: + """ + Fast Polynomial Multiplication using radix-2 fast Fourier Transform. + + Reference: + https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#The_radix-2_DIT_case + + For polynomials of degree m and n the algorithms has complexity + O(n*logn + m*logm) + + The main part of the algorithm is split in two parts: + 1) __DFT: We compute the discrete fourier transform (DFT) of A and B using a + bottom-up dynamic approach - + 2) __multiply: Once we obtain the DFT of A*B, we can similarly + invert it to obtain A*B + + The class FFT takes two polynomials A and B with complex coefficients as arguments; + The two polynomials should be represented as a sequence of coefficients starting + from the free term. Thus, for instance x + 2*x^3 could be represented as + [0,1,0,2] or (0,1,0,2). The constructor adds some zeros at the end so that the + polynomials have the same length which is a power of 2 at least the length of + their product. + + Example: + + Create two polynomials as sequences + >>> A = [0, 1, 0, 2] # x+2x^3 + >>> B = (2, 3, 4, 0) # 2+3x+4x^2 + + Create an FFT object with them + >>> x = FFT(A, B) + + Print product + >>> print(x.product) # 2x + 3x^2 + 8x^3 + 4x^4 + 6x^5 + [(-0+0j), (2+0j), (3+0j), (8+0j), (6+0j), (8+0j)] + + __str__ test + >>> print(x) + A = 0*x^0 + 1*x^1 + 2*x^0 + 3*x^2 + B = 0*x^2 + 1*x^3 + 2*x^4 + A*B = 0*x^(-0+0j) + 1*x^(2+0j) + 2*x^(3+0j) + 3*x^(8+0j) + 4*x^(6+0j) + 5*x^(8+0j) + """ + + def __init__(self, polyA=[0], polyB=[0]): + # Input as list + self.polyA = list(polyA)[:] + self.polyB = list(polyB)[:] + + # Remove leading zero coefficients + while self.polyA[-1] == 0: + self.polyA.pop() + self.len_A = len(self.polyA) + + while self.polyB[-1] == 0: + self.polyB.pop() + self.len_B = len(self.polyB) + + # Add 0 to make lengths equal a power of 2 + self.C_max_length = int( + 2 ** np.ceil(np.log2(len(self.polyA) + len(self.polyB) - 1)) + ) + + while len(self.polyA) < self.C_max_length: + self.polyA.append(0) + while len(self.polyB) < self.C_max_length: + self.polyB.append(0) + # A complex root used for the fourier transform + self.root = complex(mpmath.root(x=1, n=self.C_max_length, k=1)) + + # The product + self.product = self.__multiply() + + # Discrete fourier transform of A and B + def __DFT(self, which): + if which == "A": + dft = [[x] for x in self.polyA] + else: + dft = [[x] for x in self.polyB] + # Corner case + if len(dft) <= 1: + return dft[0] + # + next_ncol = self.C_max_length // 2 + while next_ncol > 0: + new_dft = [[] for i in range(next_ncol)] + root = self.root ** next_ncol + + # First half of next step + current_root = 1 + for j in range(self.C_max_length // (next_ncol * 2)): + for i in range(next_ncol): + new_dft[i].append(dft[i][j] + current_root * dft[i + next_ncol][j]) + current_root *= root + # Second half of next step + current_root = 1 + for j in range(self.C_max_length // (next_ncol * 2)): + for i in range(next_ncol): + new_dft[i].append(dft[i][j] - current_root * dft[i + next_ncol][j]) + current_root *= root + # Update + dft = new_dft + next_ncol = next_ncol // 2 + return dft[0] + + # multiply the DFTs of A and B and find A*B + def __multiply(self): + dftA = self.__DFT("A") + dftB = self.__DFT("B") + inverseC = [[dftA[i] * dftB[i] for i in range(self.C_max_length)]] + del dftA + del dftB + + # Corner Case + if len(inverseC[0]) <= 1: + return inverseC[0] + # Inverse DFT + next_ncol = 2 + while next_ncol <= self.C_max_length: + new_inverseC = [[] for i in range(next_ncol)] + root = self.root ** (next_ncol // 2) + current_root = 1 + # First half of next step + for j in range(self.C_max_length // next_ncol): + for i in range(next_ncol // 2): + # Even positions + new_inverseC[i].append( + ( + inverseC[i][j] + + inverseC[i][j + self.C_max_length // next_ncol] + ) + / 2 + ) + # Odd positions + new_inverseC[i + next_ncol // 2].append( + ( + inverseC[i][j] + - inverseC[i][j + self.C_max_length // next_ncol] + ) + / (2 * current_root) + ) + current_root *= root + # Update + inverseC = new_inverseC + next_ncol *= 2 + # Unpack + inverseC = [round(x[0].real, 8) + round(x[0].imag, 8) * 1j for x in inverseC] + + # Remove leading 0's + while inverseC[-1] == 0: + inverseC.pop() + return inverseC + + # Overwrite __str__ for print(); Shows A, B and A*B + def __str__(self): + A = "A = " + " + ".join( + f"{coef}*x^{i}" for coef, i in enumerate(self.polyA[: self.len_A]) + ) + B = "B = " + " + ".join( + f"{coef}*x^{i}" for coef, i in enumerate(self.polyB[: self.len_B]) + ) + C = "A*B = " + " + ".join( + f"{coef}*x^{i}" for coef, i in enumerate(self.product) + ) + + return "\n".join((A, B, C)) + + +# Unit tests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/relu.py b/maths/relu.py new file mode 100644 index 000000000000..2c41d2e9dad9 --- /dev/null +++ b/maths/relu.py @@ -0,0 +1,39 @@ +""" +This script demonstrates the implementation of the ReLU function. + +It's a kind of activation function defined as the positive part of its argument in the context of neural network. +The function takes a vector of K real numbers as input and then argmax(x, 0). +After through ReLU, the element of the vector always 0 or real number. + +Script inspired from its corresponding Wikipedia article +https://en.wikipedia.org/wiki/Rectifier_(neural_networks) +""" + +import numpy as np +from typing import List + + +def relu(vector: List[float]): + """ + Implements the relu function + + Parameters: + vector (np.array,list,tuple): A numpy array of shape (1,n) + consisting of real values or a similar list,tuple + + + Returns: + relu_vec (np.array): The input numpy array, after applying + relu. + + >>> vec = np.array([-1, 0, 5]) + >>> relu(vec) + array([0, 0, 5]) + """ + + # compare two arrays and then return element-wise maxima. + return np.maximum(0, vector) + + +if __name__ == "__main__": + print(np.array(relu([-1, 0, 5]))) # --> [0, 0, 5] diff --git a/maths/runge_kutta.py b/maths/runge_kutta.py new file mode 100644 index 000000000000..383797daa5ac --- /dev/null +++ b/maths/runge_kutta.py @@ -0,0 +1,44 @@ +import numpy as np + + +def runge_kutta(f, y0, x0, h, x_end): + """ + Calculate the numeric solution at each step to the ODE f(x, y) using RK4 + + https://en.wikipedia.org/wiki/Runge-Kutta_methods + + Arguments: + f -- The ode as a function of x and y + y0 -- the initial value for y + x0 -- the initial value for x + h -- the stepsize + x_end -- the end value for x + + >>> # the exact solution is math.exp(x) + >>> def f(x, y): + ... return y + >>> y0 = 1 + >>> y = runge_kutta(f, y0, 0.0, 0.01, 5) + >>> y[-1] + 148.41315904125113 + """ + N = int(np.ceil((x_end - x0) / h)) + y = np.zeros((N + 1,)) + y[0] = y0 + x = x0 + + for k in range(N): + k1 = f(x, y[k]) + k2 = f(x + 0.5 * h, y[k] + 0.5 * h * k1) + k3 = f(x + 0.5 * h, y[k] + 0.5 * h * k2) + k4 = f(x + h, y[k] + h * k3) + y[k + 1] = y[k] + (1 / 6) * h * (k1 + 2 * k2 + 2 * k3 + k4) + x += h + + return y + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/segmented_sieve.py b/maths/segmented_sieve.py index b15ec2480678..c1cc497ad33e 100644 --- a/maths/segmented_sieve.py +++ b/maths/segmented_sieve.py @@ -48,4 +48,4 @@ def sieve(n): return prime -print(sieve(10**6)) +print(sieve(10 ** 6)) diff --git a/maths/series/geometric_series.py b/maths/series/geometric_series.py new file mode 100644 index 000000000000..d12382e6d8c4 --- /dev/null +++ b/maths/series/geometric_series.py @@ -0,0 +1,63 @@ +""" +This is a pure Python implementation of the Geometric Series algorithm +https://en.wikipedia.org/wiki/Geometric_series + +Run the doctests with the following command: +python3 -m doctest -v geometric_series.py +or +python -m doctest -v geometric_series.py +For manual testing run: +python3 geometric_series.py +""" + + +def geometric_series(nth_term: int, start_term_a: int, common_ratio_r: int) -> list: + """Pure Python implementation of Geometric Series algorithm + :param nth_term: The last term (nth term of Geometric Series) + :param start_term_a : The first term of Geometric Series + :param common_ratio_r : The common ratio between all the terms + :return: The Geometric Series starting from first term a and multiple of common + ration with first term with increase in power till last term (nth term) + Examples: + >>> geometric_series(4, 2, 2) + [2, '4.0', '8.0', '16.0'] + >>> geometric_series(4.0, 2.0, 2.0) + [2.0, '4.0', '8.0', '16.0'] + >>> geometric_series(4.1, 2.1, 2.1) + [2.1, '4.41', '9.261000000000001', '19.448100000000004'] + >>> geometric_series(4, 2, -2) + [2, '-4.0', '8.0', '-16.0'] + >>> geometric_series(4, -2, 2) + [-2, '-4.0', '-8.0', '-16.0'] + >>> geometric_series(-4, 2, 2) + [] + >>> geometric_series(0, 100, 500) + [] + >>> geometric_series(1, 1, 1) + [1] + >>> geometric_series(0, 0, 0) + [] + """ + if "" in (nth_term, start_term_a, common_ratio_r): + return "" + series = [] + power = 1 + multiple = common_ratio_r + for _ in range(int(nth_term)): + if series == []: + series.append(start_term_a) + else: + power += 1 + series.append(str(float(start_term_a) * float(multiple))) + multiple = pow(float(common_ratio_r), power) + return series + + +if __name__ == "__main__": + nth_term = input("Enter the last number (n term) of the Geometric Series") + start_term_a = input("Enter the starting term (a) of the Geometric Series") + common_ratio_r = input( + "Enter the common ratio between two terms (r) of the Geometric Series" + ) + print("Formula of Geometric Series => a + ar + ar^2 ... +ar^n") + print(geometric_series(nth_term, start_term_a, common_ratio_r)) diff --git a/maths/series/harmonic_series.py b/maths/series/harmonic_series.py new file mode 100644 index 000000000000..91b5944583e4 --- /dev/null +++ b/maths/series/harmonic_series.py @@ -0,0 +1,46 @@ +""" +This is a pure Python implementation of the Harmonic Series algorithm +https://en.wikipedia.org/wiki/Harmonic_series_(mathematics) + +For doctests run following command: +python -m doctest -v harmonic_series.py +or +python3 -m doctest -v harmonic_series.py + +For manual testing run: +python3 harmonic_series.py +""" + + +def harmonic_series(n_term: str) -> list: + """Pure Python implementation of Harmonic Series algorithm + + :param n_term: The last (nth) term of Harmonic Series + :return: The Harmonic Series starting from 1 to last (nth) term + + Examples: + >>> harmonic_series(5) + ['1', '1/2', '1/3', '1/4', '1/5'] + >>> harmonic_series(5.0) + ['1', '1/2', '1/3', '1/4', '1/5'] + >>> harmonic_series(5.1) + ['1', '1/2', '1/3', '1/4', '1/5'] + >>> harmonic_series(-5) + [] + >>> harmonic_series(0) + [] + >>> harmonic_series(1) + ['1'] + """ + if n_term == "": + return n_term + series = [] + for temp in range(int(n_term)): + series.append(f"1/{temp + 1}" if series else "1") + return series + + +if __name__ == "__main__": + nth_term = input("Enter the last number (nth term) of the Harmonic Series") + print("Formula of Harmonic Series => 1+1/2+1/3 ..... 1/n") + print(harmonic_series(nth_term)) diff --git a/maths/series/p_series.py b/maths/series/p_series.py new file mode 100644 index 000000000000..04019aed5a85 --- /dev/null +++ b/maths/series/p_series.py @@ -0,0 +1,48 @@ +""" +This is a pure Python implementation of the P-Series algorithm +https://en.wikipedia.org/wiki/Harmonic_series_(mathematics)#P-series + +For doctests run following command: +python -m doctest -v p_series.py +or +python3 -m doctest -v p_series.py + +For manual testing run: +python3 p_series.py +""" + + +def p_series(nth_term: int, power: int) -> list: + """Pure Python implementation of P-Series algorithm + + :return: The P-Series starting from 1 to last (nth) term + + Examples: + >>> p_series(5, 2) + [1, '1/4', '1/9', '1/16', '1/25'] + >>> p_series(-5, 2) + [] + >>> p_series(5, -2) + [1, '1/0.25', '1/0.1111111111111111', '1/0.0625', '1/0.04'] + >>> p_series("", 1000) + '' + >>> p_series(0, 0) + [] + >>> p_series(1, 1) + [1] + """ + if nth_term == "": + return nth_term + nth_term = int(nth_term) + power = int(power) + series = [] + for temp in range(int(nth_term)): + series.append(f"1/{pow(temp + 1, int(power))}" if series else 1) + return series + + +if __name__ == "__main__": + nth_term = input("Enter the last number (nth term) of the P-Series") + power = input("Enter the power for P-Series") + print("Formula of P-Series => 1+1/2^p+1/3^p ..... 1/n^p") + print(p_series(nth_term, power)) diff --git a/maths/sieve_of_eratosthenes.py b/maths/sieve_of_eratosthenes.py index cedd04f92aa0..4761c9339ea0 100644 --- a/maths/sieve_of_eratosthenes.py +++ b/maths/sieve_of_eratosthenes.py @@ -1,19 +1,51 @@ -"""Sieve of Eratosthones.""" +""" +Sieve of Eratosthones + +The sieve of Eratosthenes is an algorithm used to find prime numbers, less than or equal to a given value. +Illustration: https://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif +Reference: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes + +doctest provider: Bruno Simas Hadlich (https://github.com/brunohadlich) +Also thanks Dmitry (https://github.com/LizardWizzard) for finding the problem +""" + import math + def sieve(n): - """Sieve of Eratosthones.""" + """ + Returns a list with all prime numbers up to n. + + >>> sieve(50) + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] + >>> sieve(25) + [2, 3, 5, 7, 11, 13, 17, 19, 23] + >>> sieve(10) + [2, 3, 5, 7] + >>> sieve(9) + [2, 3, 5, 7] + >>> sieve(2) + [2] + >>> sieve(1) + [] + """ + l = [True] * (n + 1) prime = [] start = 2 end = int(math.sqrt(n)) + while start <= end: + # If start is a prime if l[start] is True: prime.append(start) + + # Set multiples of start be False for i in range(start * start, n + 1, start): if l[i] is True: l[i] = False + start += 1 for j in range(end + 1, n + 1): diff --git a/maths/simpson_rule.py b/maths/simpson_rule.py index 2b237d2e1a4e..d66dc39a7171 100644 --- a/maths/simpson_rule.py +++ b/maths/simpson_rule.py @@ -1,49 +1,51 @@ - """ Numerical integration or quadrature for a smooth function f with known values at x_i -This method is the classical approch of suming 'Equally Spaced Abscissas' +This method is the classical approach of suming 'Equally Spaced Abscissas' method 2: "Simpson Rule" """ -from __future__ import print_function def method_2(boundary, steps): -# "Simpson Rule" -# int(f) = delta_x/2 * (b-a)/3*(f1 + 4f2 + 2f_3 + ... + fn) + # "Simpson Rule" + # int(f) = delta_x/2 * (b-a)/3*(f1 + 4f2 + 2f_3 + ... + fn) h = (boundary[1] - boundary[0]) / steps a = boundary[0] b = boundary[1] - x_i = make_points(a,b,h) + x_i = make_points(a, b, h) y = 0.0 - y += (h/3.0)*f(a) + y += (h / 3.0) * f(a) cnt = 2 for i in x_i: - y += (h/3)*(4-2*(cnt%2))*f(i) + y += (h / 3) * (4 - 2 * (cnt % 2)) * f(i) cnt += 1 - y += (h/3.0)*f(b) + y += (h / 3.0) * f(b) return y -def make_points(a,b,h): + +def make_points(a, b, h): x = a + h - while x < (b-h): + while x < (b - h): yield x x = x + h -def f(x): #enter your function here - y = (x-0)*(x-0) + +def f(x): # enter your function here + y = (x - 0) * (x - 0) return y + def main(): - a = 0.0 #Lower bound of integration - b = 1.0 #Upper bound of integration - steps = 10.0 #define number of steps or resolution - boundary = [a, b] #define boundary of integration + a = 0.0 # Lower bound of integration + b = 1.0 # Upper bound of integration + steps = 10.0 # define number of steps or resolution + boundary = [a, b] # define boundary of integration y = method_2(boundary, steps) - print('y = {0}'.format(y)) + print(f"y = {y}") + -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/maths/softmax.py b/maths/softmax.py new file mode 100644 index 000000000000..92ff4ca27b88 --- /dev/null +++ b/maths/softmax.py @@ -0,0 +1,56 @@ +""" +This script demonstrates the implementation of the Softmax function. + +Its a function that takes as input a vector of K real numbers, and normalizes +it into a probability distribution consisting of K probabilities proportional +to the exponentials of the input numbers. After softmax, the elements of the +vector always sum up to 1. + +Script inspired from its corresponding Wikipedia article +https://en.wikipedia.org/wiki/Softmax_function +""" + +import numpy as np + + +def softmax(vector): + """ + Implements the softmax function + + Parameters: + vector (np.array,list,tuple): A numpy array of shape (1,n) + consisting of real values or a similar list,tuple + + + Returns: + softmax_vec (np.array): The input numpy array after applying + softmax. + + The softmax vector adds up to one. We need to ceil to mitigate for + precision + >>> np.ceil(np.sum(softmax([1,2,3,4]))) + 1.0 + + >>> vec = np.array([5,5]) + >>> softmax(vec) + array([0.5, 0.5]) + + >>> softmax([0]) + array([1.]) + """ + + # Calculate e^x for each x in your vector where e is Euler's + # number (approximately 2.718) + exponentVector = np.exp(vector) + + # Add up the all the exponentials + sumOfExponents = np.sum(exponentVector) + + # Divide every exponent by the sum of all exponents + softmax_vector = exponentVector / sumOfExponents + + return softmax_vector + + +if __name__ == "__main__": + print(softmax((0,))) diff --git a/maths/square_root.py b/maths/square_root.py new file mode 100644 index 000000000000..d4c5e311b0b7 --- /dev/null +++ b/maths/square_root.py @@ -0,0 +1,63 @@ +import math + + +def fx(x: float, a: float) -> float: + return math.pow(x, 2) - a + + +def fx_derivative(x: float) -> float: + return 2 * x + + +def get_initial_point(a: float) -> float: + start = 2.0 + + while start <= a: + start = math.pow(start, 2) + + return start + + +def square_root_iterative( + a: float, max_iter: int = 9999, tolerance: float = 0.00000000000001 +) -> float: + """ + Square root is aproximated using Newtons method. + https://en.wikipedia.org/wiki/Newton%27s_method + + >>> all(abs(square_root_iterative(i)-math.sqrt(i)) <= .00000000000001 for i in range(0, 500)) + True + + >>> square_root_iterative(-1) + Traceback (most recent call last): + ... + ValueError: math domain error + + >>> square_root_iterative(4) + 2.0 + + >>> square_root_iterative(3.2) + 1.788854381999832 + + >>> square_root_iterative(140) + 11.832159566199232 + """ + + if a < 0: + raise ValueError("math domain error") + + value = get_initial_point(a) + + for i in range(max_iter): + prev_value = value + value = value - fx(value, a) / fx_derivative(value) + if abs(prev_value - value) < tolerance: + return value + + return value + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/maths/sum_of_arithmetic_series.py b/maths/sum_of_arithmetic_series.py new file mode 100755 index 000000000000..74eef0f18a12 --- /dev/null +++ b/maths/sum_of_arithmetic_series.py @@ -0,0 +1,23 @@ +# DarkCoder +def sum_of_series(first_term, common_diff, num_of_terms): + """ + Find the sum of n terms in an arithmetic progression. + + >>> sum_of_series(1, 1, 10) + 55.0 + >>> sum_of_series(1, 10, 100) + 49600.0 + """ + sum = (num_of_terms / 2) * (2 * first_term + (num_of_terms - 1) * common_diff) + # formula for sum of series + return sum + + +def main(): + print(sum_of_series(1, 1, 10)) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/maths/test_prime_check.py b/maths/test_prime_check.py new file mode 100644 index 000000000000..b6389684af9e --- /dev/null +++ b/maths/test_prime_check.py @@ -0,0 +1,8 @@ +""" +Minimalist file that allows pytest to find and run the Test unittest. For details, see: +http://doc.pytest.org/en/latest/goodpractices.html#conventions-for-python-test-discovery +""" + +from .prime_check import Test + +Test() diff --git a/maths/trapezoidal_rule.py b/maths/trapezoidal_rule.py index 789f263c6991..9a4ddc8af66b 100644 --- a/maths/trapezoidal_rule.py +++ b/maths/trapezoidal_rule.py @@ -1,46 +1,50 @@ """ Numerical integration or quadrature for a smooth function f with known values at x_i -This method is the classical approch of suming 'Equally Spaced Abscissas' +This method is the classical approach of suming 'Equally Spaced Abscissas' method 1: "extended trapezoidal rule" """ -from __future__ import print_function + def method_1(boundary, steps): -# "extended trapezoidal rule" -# int(f) = dx/2 * (f1 + 2f2 + ... + fn) - h = (boundary[1] - boundary[0]) / steps - a = boundary[0] - b = boundary[1] - x_i = make_points(a,b,h) - y = 0.0 - y += (h/2.0)*f(a) - for i in x_i: - #print(i) - y += h*f(i) - y += (h/2.0)*f(b) - return y - -def make_points(a,b,h): - x = a + h - while x < (b-h): - yield x - x = x + h - -def f(x): #enter your function here - y = (x-0)*(x-0) - return y + # "extended trapezoidal rule" + # int(f) = dx/2 * (f1 + 2f2 + ... + fn) + h = (boundary[1] - boundary[0]) / steps + a = boundary[0] + b = boundary[1] + x_i = make_points(a, b, h) + y = 0.0 + y += (h / 2.0) * f(a) + for i in x_i: + # print(i) + y += h * f(i) + y += (h / 2.0) * f(b) + return y + + +def make_points(a, b, h): + x = a + h + while x < (b - h): + yield x + x = x + h + + +def f(x): # enter your function here + y = (x - 0) * (x - 0) + return y + def main(): - a = 0.0 #Lower bound of integration - b = 1.0 #Upper bound of integration - steps = 10.0 #define number of steps or resolution - boundary = [a, b] #define boundary of integration - y = method_1(boundary, steps) - print('y = {0}'.format(y)) - -if __name__ == '__main__': - main() + a = 0.0 # Lower bound of integration + b = 1.0 # Upper bound of integration + steps = 10.0 # define number of steps or resolution + boundary = [a, b] # define boundary of integration + y = method_1(boundary, steps) + print(f"y = {y}") + + +if __name__ == "__main__": + main() diff --git a/maths/volume.py b/maths/volume.py index 171bc538f5a4..04283743d71f 100644 --- a/maths/volume.py +++ b/maths/volume.py @@ -3,84 +3,116 @@ Wikipedia reference: https://en.wikipedia.org/wiki/Volume """ +from typing import Union +from math import pi, pow -from math import pi -PI = pi +def vol_cube(side_length: Union[int, float]) -> float: + """ + Calculate the Volume of a Cube. + >>> vol_cube(1) + 1.0 + >>> vol_cube(3) + 27.0 + """ + return pow(side_length, 3) -def vol_cube(side_length): - """Calculate the Volume of a Cube.""" - # Cube side_length. - return float(side_length ** 3) +def vol_cuboid(width: float, height: float, length: float) -> float: + """ + Calculate the Volume of a Cuboid. + :return multiple of width, length and height -def vol_cuboid(width, height, length): - """Calculate the Volume of a Cuboid.""" - # Multiply lengths together. + >>> vol_cuboid(1, 1, 1) + 1.0 + >>> vol_cuboid(1, 2, 3) + 6.0 + """ return float(width * height * length) -def vol_cone(area_of_base, height): +def vol_cone(area_of_base: float, height: float) -> float: """ Calculate the Volume of a Cone. Wikipedia reference: https://en.wikipedia.org/wiki/Cone - volume = (1/3) * area_of_base * height + :return (1/3) * area_of_base * height + + >>> vol_cone(10, 3) + 10.0 + >>> vol_cone(1, 1) + 0.3333333333333333 """ - return (float(1) / 3) * area_of_base * height + return area_of_base * height / 3.0 -def vol_right_circ_cone(radius, height): +def vol_right_circ_cone(radius: float, height: float) -> float: """ Calculate the Volume of a Right Circular Cone. Wikipedia reference: https://en.wikipedia.org/wiki/Cone - volume = (1/3) * pi * radius^2 * height - """ - - import math + :return (1/3) * pi * radius^2 * height - return (float(1) / 3) * PI * (radius ** 2) * height + >>> vol_right_circ_cone(2, 3) + 12.566370614359172 + """ + return pi * pow(radius, 2) * height / 3.0 -def vol_prism(area_of_base, height): +def vol_prism(area_of_base: float, height: float) -> float: """ Calculate the Volume of a Prism. - - V = Bh Wikipedia reference: https://en.wikipedia.org/wiki/Prism_(geometry) + :return V = Bh + + >>> vol_prism(10, 2) + 20.0 + >>> vol_prism(11, 1) + 11.0 """ return float(area_of_base * height) -def vol_pyramid(area_of_base, height): +def vol_pyramid(area_of_base: float, height: float) -> float: """ - Calculate the Volume of a Prism. - - V = (1/3) * Bh + Calculate the Volume of a Pyramid. Wikipedia reference: https://en.wikipedia.org/wiki/Pyramid_(geometry) + :return (1/3) * Bh + + >>> vol_pyramid(10, 3) + 10.0 + >>> vol_pyramid(1.5, 3) + 1.5 """ - return (float(1) / 3) * area_of_base * height + return area_of_base * height / 3.0 -def vol_sphere(radius): +def vol_sphere(radius: float) -> float: """ Calculate the Volume of a Sphere. - - V = (4/3) * pi * r^3 Wikipedia reference: https://en.wikipedia.org/wiki/Sphere + :return (4/3) * pi * r^3 + + >>> vol_sphere(5) + 523.5987755982989 + >>> vol_sphere(1) + 4.1887902047863905 """ - return (float(4) / 3) * PI * radius ** 3 + return 4 / 3 * pi * pow(radius, 3) -def vol_circular_cylinder(radius, height): +def vol_circular_cylinder(radius: float, height: float) -> float: """Calculate the Volume of a Circular Cylinder. - Wikipedia reference: https://en.wikipedia.org/wiki/Cylinder - volume = pi * radius^2 * height + :return pi * radius^2 * height + + >>> vol_circular_cylinder(1, 1) + 3.141592653589793 + >>> vol_circular_cylinder(4, 3) + 150.79644737231007 """ - return PI * radius ** 2 * height + return pi * pow(radius, 2) * height def main(): diff --git a/maths/zellers_congruence.py b/maths/zellers_congruence.py new file mode 100644 index 000000000000..9c13c29210b1 --- /dev/null +++ b/maths/zellers_congruence.py @@ -0,0 +1,159 @@ +import datetime +import argparse + + +def zeller(date_input: str) -> str: + + """ + Zellers Congruence Algorithm + Find the day of the week for nearly any Gregorian or Julian calendar date + + >>> zeller('01-31-2010') + 'Your date 01-31-2010, is a Sunday!' + + Validate out of range month + >>> zeller('13-31-2010') + Traceback (most recent call last): + ... + ValueError: Month must be between 1 - 12 + >>> zeller('.2-31-2010') + Traceback (most recent call last): + ... + ValueError: invalid literal for int() with base 10: '.2' + + Validate out of range date: + >>> zeller('01-33-2010') + Traceback (most recent call last): + ... + ValueError: Date must be between 1 - 31 + >>> zeller('01-.4-2010') + Traceback (most recent call last): + ... + ValueError: invalid literal for int() with base 10: '.4' + + Validate second separator: + >>> zeller('01-31*2010') + Traceback (most recent call last): + ... + ValueError: Date separator must be '-' or '/' + + Validate first separator: + >>> zeller('01^31-2010') + Traceback (most recent call last): + ... + ValueError: Date separator must be '-' or '/' + + Validate out of range year: + >>> zeller('01-31-8999') + Traceback (most recent call last): + ... + ValueError: Year out of range. There has to be some sort of limit...right? + + Test null input: + >>> zeller() + Traceback (most recent call last): + ... + TypeError: zeller() missing 1 required positional argument: 'date_input' + + Test length of date_input: + >>> zeller('') + Traceback (most recent call last): + ... + ValueError: Must be 10 characters long + >>> zeller('01-31-19082939') + Traceback (most recent call last): + ... + ValueError: Must be 10 characters long +""" + + # Days of the week for response + days = { + "0": "Sunday", + "1": "Monday", + "2": "Tuesday", + "3": "Wednesday", + "4": "Thursday", + "5": "Friday", + "6": "Saturday", + } + + convert_datetime_days = {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 0} + + # Validate + if not 0 < len(date_input) < 11: + raise ValueError("Must be 10 characters long") + + # Get month + m: int = int(date_input[0] + date_input[1]) + # Validate + if not 0 < m < 13: + raise ValueError("Month must be between 1 - 12") + + sep_1: str = date_input[2] + # Validate + if sep_1 not in ["-", "/"]: + raise ValueError("Date separator must be '-' or '/'") + + # Get day + d: int = int(date_input[3] + date_input[4]) + # Validate + if not 0 < d < 32: + raise ValueError("Date must be between 1 - 31") + + # Get second separator + sep_2: str = date_input[5] + # Validate + if sep_2 not in ["-", "/"]: + raise ValueError("Date separator must be '-' or '/'") + + # Get year + y: int = int(date_input[6] + date_input[7] + date_input[8] + date_input[9]) + # Arbitrary year range + if not 45 < y < 8500: + raise ValueError( + "Year out of range. There has to be some sort of limit...right?" + ) + + # Get datetime obj for validation + dt_ck = datetime.date(int(y), int(m), int(d)) + + # Start math + if m <= 2: + y = y - 1 + m = m + 12 + # maths var + c: int = int(str(y)[:2]) + k: int = int(str(y)[2:]) + t: int = int(2.6 * m - 5.39) + u: int = int(c / 4) + v: int = int(k / 4) + x: int = int(d + k) + z: int = int(t + u + v + x) + w: int = int(z - (2 * c)) + f: int = round(w % 7) + # End math + + # Validate math + if f != convert_datetime_days[dt_ck.weekday()]: + raise AssertionError("The date was evaluated incorrectly. Contact developer.") + + # Response + response: str = f"Your date {date_input}, is a {days[str(f)]}!" + return response + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + parser = argparse.ArgumentParser( + description=( + "Find out what day of the week nearly any date is or was. Enter " + "date as a string in the mm-dd-yyyy or mm/dd/yyyy format" + ) + ) + parser.add_argument( + "date_input", type=str, help="Date as a string (mm-dd-yyyy or mm/dd/yyyy)" + ) + args = parser.parse_args() + zeller(args.date_input) diff --git a/matrix/matrix_class.py b/matrix/matrix_class.py new file mode 100644 index 000000000000..57a2fc45ffd1 --- /dev/null +++ b/matrix/matrix_class.py @@ -0,0 +1,358 @@ +# An OOP approach to representing and manipulating matrices + + +class Matrix: + """ + Matrix object generated from a 2D array where each element is an array representing + a row. + Rows can contain type int or float. + Common operations and information available. + >>> rows = [ + ... [1, 2, 3], + ... [4, 5, 6], + ... [7, 8, 9] + ... ] + >>> matrix = Matrix(rows) + >>> print(matrix) + [[1. 2. 3.] + [4. 5. 6.] + [7. 8. 9.]] + + Matrix rows and columns are available as 2D arrays + >>> print(matrix.rows) + [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + >>> print(matrix.columns()) + [[1, 4, 7], [2, 5, 8], [3, 6, 9]] + + Order is returned as a tuple + >>> matrix.order + (3, 3) + + Squareness and invertability are represented as bool + >>> matrix.is_square + True + >>> matrix.is_invertable() + False + + Identity, Minors, Cofactors and Adjugate are returned as Matrices. Inverse can be + a Matrix or Nonetype + >>> print(matrix.identity()) + [[1. 0. 0.] + [0. 1. 0.] + [0. 0. 1.]] + >>> print(matrix.minors()) + [[-3. -6. -3.] + [-6. -12. -6.] + [-3. -6. -3.]] + >>> print(matrix.cofactors()) + [[-3. 6. -3.] + [6. -12. 6.] + [-3. 6. -3.]] + >>> # won't be apparent due to the nature of the cofactor matrix + >>> print(matrix.adjugate()) + [[-3. 6. -3.] + [6. -12. 6.] + [-3. 6. -3.]] + >>> print(matrix.inverse()) + None + + Determinant is an int, float, or Nonetype + >>> matrix.determinant() + 0 + + Negation, scalar multiplication, addition, subtraction, multiplication and + exponentiation are available and all return a Matrix + >>> print(-matrix) + [[-1. -2. -3.] + [-4. -5. -6.] + [-7. -8. -9.]] + >>> matrix2 = matrix * 3 + >>> print(matrix2) + [[3. 6. 9.] + [12. 15. 18.] + [21. 24. 27.]] + >>> print(matrix + matrix2) + [[4. 8. 12.] + [16. 20. 24.] + [28. 32. 36.]] + >>> print(matrix - matrix2) + [[-2. -4. -6.] + [-8. -10. -12.] + [-14. -16. -18.]] + >>> print(matrix ** 3) + [[468. 576. 684.] + [1062. 1305. 1548.] + [1656. 2034. 2412.]] + + Matrices can also be modified + >>> matrix.add_row([10, 11, 12]) + >>> print(matrix) + [[1. 2. 3.] + [4. 5. 6.] + [7. 8. 9.] + [10. 11. 12.]] + >>> matrix2.add_column([8, 16, 32]) + >>> print(matrix2) + [[3. 6. 9. 8.] + [12. 15. 18. 16.] + [21. 24. 27. 32.]] + >>> print(matrix * matrix2) + [[90. 108. 126. 136.] + [198. 243. 288. 304.] + [306. 378. 450. 472.] + [414. 513. 612. 640.]] + + """ + + def __init__(self, rows): + error = TypeError( + "Matrices must be formed from a list of zero or more lists containing at " + "least one and the same number of values, each of which must be of type " + "int or float." + ) + if len(rows) != 0: + cols = len(rows[0]) + if cols == 0: + raise error + for row in rows: + if len(row) != cols: + raise error + for value in row: + if not isinstance(value, (int, float)): + raise error + self.rows = rows + else: + self.rows = [] + + # MATRIX INFORMATION + def columns(self): + return [[row[i] for row in self.rows] for i in range(len(self.rows[0]))] + + @property + def num_rows(self): + return len(self.rows) + + @property + def num_columns(self): + return len(self.rows[0]) + + @property + def order(self): + return (self.num_rows, self.num_columns) + + @property + def is_square(self): + return self.order[0] == self.order[1] + + def identity(self): + values = [ + [0 if column_num != row_num else 1 for column_num in range(self.num_rows)] + for row_num in range(self.num_rows) + ] + return Matrix(values) + + def determinant(self): + if not self.is_square: + return None + if self.order == (0, 0): + return 1 + if self.order == (1, 1): + return self.rows[0][0] + if self.order == (2, 2): + return (self.rows[0][0] * self.rows[1][1]) - ( + self.rows[0][1] * self.rows[1][0] + ) + else: + return sum( + self.rows[0][column] * self.cofactors().rows[0][column] + for column in range(self.num_columns) + ) + + def is_invertable(self): + return bool(self.determinant()) + + def get_minor(self, row, column): + values = [ + [ + self.rows[other_row][other_column] + for other_column in range(self.num_columns) + if other_column != column + ] + for other_row in range(self.num_rows) + if other_row != row + ] + return Matrix(values).determinant() + + def get_cofactor(self, row, column): + if (row + column) % 2 == 0: + return self.get_minor(row, column) + return -1 * self.get_minor(row, column) + + def minors(self): + return Matrix( + [ + [self.get_minor(row, column) for column in range(self.num_columns)] + for row in range(self.num_rows) + ] + ) + + def cofactors(self): + return Matrix( + [ + [ + self.minors().rows[row][column] + if (row + column) % 2 == 0 + else self.minors().rows[row][column] * -1 + for column in range(self.minors().num_columns) + ] + for row in range(self.minors().num_rows) + ] + ) + + def adjugate(self): + values = [ + [self.cofactors().rows[column][row] for column in range(self.num_columns)] + for row in range(self.num_rows) + ] + return Matrix(values) + + def inverse(self): + determinant = self.determinant() + return None if not determinant else self.adjugate() * (1 / determinant) + + def __repr__(self): + return str(self.rows) + + def __str__(self): + if self.num_rows == 0: + return "[]" + if self.num_rows == 1: + return "[[" + ". ".join(self.rows[0]) + "]]" + return ( + "[" + + "\n ".join( + [ + "[" + ". ".join([str(value) for value in row]) + ".]" + for row in self.rows + ] + ) + + "]" + ) + + # MATRIX MANIPULATION + def add_row(self, row, position=None): + type_error = TypeError("Row must be a list containing all ints and/or floats") + if not isinstance(row, list): + raise type_error + for value in row: + if not isinstance(value, (int, float)): + raise type_error + if len(row) != self.num_columns: + raise ValueError( + "Row must be equal in length to the other rows in the matrix" + ) + if position is None: + self.rows.append(row) + else: + self.rows = self.rows[0:position] + [row] + self.rows[position:] + + def add_column(self, column, position=None): + type_error = TypeError( + "Column must be a list containing all ints and/or floats" + ) + if not isinstance(column, list): + raise type_error + for value in column: + if not isinstance(value, (int, float)): + raise type_error + if len(column) != self.num_rows: + raise ValueError( + "Column must be equal in length to the other columns in the matrix" + ) + if position is None: + self.rows = [self.rows[i] + [column[i]] for i in range(self.num_rows)] + else: + self.rows = [ + self.rows[i][0:position] + [column[i]] + self.rows[i][position:] + for i in range(self.num_rows) + ] + + # MATRIX OPERATIONS + def __eq__(self, other): + if not isinstance(other, Matrix): + raise TypeError("A Matrix can only be compared with another Matrix") + return self.rows == other.rows + + def __ne__(self, other): + return not self == other + + def __neg__(self): + return self * -1 + + def __add__(self, other): + if self.order != other.order: + raise ValueError("Addition requires matrices of the same order") + return Matrix( + [ + [self.rows[i][j] + other.rows[i][j] for j in range(self.num_columns)] + for i in range(self.num_rows) + ] + ) + + def __sub__(self, other): + if self.order != other.order: + raise ValueError("Subtraction requires matrices of the same order") + return Matrix( + [ + [self.rows[i][j] - other.rows[i][j] for j in range(self.num_columns)] + for i in range(self.num_rows) + ] + ) + + def __mul__(self, other): + if isinstance(other, (int, float)): + return Matrix([[element * other for element in row] for row in self.rows]) + elif isinstance(other, Matrix): + if self.num_columns != other.num_rows: + raise ValueError( + "The number of columns in the first matrix must " + "be equal to the number of rows in the second" + ) + return Matrix( + [ + [Matrix.dot_product(row, column) for column in other.columns()] + for row in self.rows + ] + ) + else: + raise TypeError( + "A Matrix can only be multiplied by an int, float, or another matrix" + ) + + def __pow__(self, other): + if not isinstance(other, int): + raise TypeError("A Matrix can only be raised to the power of an int") + if not self.is_square: + raise ValueError("Only square matrices can be raised to a power") + if other == 0: + return self.identity() + if other < 0: + if self.is_invertable: + return self.inverse() ** (-other) + raise ValueError( + "Only invertable matrices can be raised to a negative power" + ) + result = self + for i in range(other - 1): + result *= self + return result + + @classmethod + def dot_product(cls, row, column): + return sum(row[i] * column[i] for i in range(len(row))) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/matrix/matrix_operation.py b/matrix/matrix_operation.py index b32a4dcf7af3..307e8b6ba32e 100644 --- a/matrix/matrix_operation.py +++ b/matrix/matrix_operation.py @@ -39,8 +39,10 @@ def multiply(matrix_a, matrix_b): rows, cols = _verify_matrix_sizes(matrix_a, matrix_b) if cols[0] != rows[1]: - raise ValueError(f'Cannot multiply matrix of dimensions ({rows[0]},{cols[0]}) ' - f'and ({rows[1]},{cols[1]})') + raise ValueError( + f"Cannot multiply matrix of dimensions ({rows[0]},{cols[0]}) " + f"and ({rows[1]},{cols[1]})" + ) for i in range(rows[0]): list_1 = [] for j in range(cols[1]): @@ -59,7 +61,7 @@ def identity(n): :return: Identity matrix of shape [n, n] """ n = int(n) - return [[int(row == column) for column in range(n)] for row in range(n)] + return [[int(row == column) for column in range(n)] for row in range(n)] def transpose(matrix, return_map=True): @@ -75,15 +77,15 @@ def transpose(matrix, return_map=True): def minor(matrix, row, column): - minor = matrix[:row] + matrix[row + 1:] - minor = [row[:column] + row[column + 1:] for row in minor] + minor = matrix[:row] + matrix[row + 1 :] + minor = [row[:column] + row[column + 1 :] for row in minor] return minor def determinant(matrix): if len(matrix) == 1: return matrix[0][0] - + res = 0 for x in range(len(matrix)): res += matrix[0][x] * determinant(minor(matrix, 0, x)) * (-1) ** x @@ -99,19 +101,19 @@ def inverse(matrix): for i in range(len(matrix)): for j in range(len(matrix)): matrix_minor[i].append(determinant(minor(matrix, i, j))) - - cofactors = [[x * (-1) ** (row + col) for col, x in enumerate(matrix_minor[row])] for row in range(len(matrix))] + + cofactors = [ + [x * (-1) ** (row + col) for col, x in enumerate(matrix_minor[row])] + for row in range(len(matrix)) + ] adjugate = transpose(cofactors) - return scalar_multiply(adjugate, 1/det) + return scalar_multiply(adjugate, 1 / det) def _check_not_integer(matrix): - try: - rows = len(matrix) - cols = len(matrix[0]) + if not isinstance(matrix, int) and not isinstance(matrix[0], int): return True - except TypeError: - raise TypeError("Cannot input an integer value, it must be a matrix") + raise TypeError("Expected a matrix, got int/list instead") def _shape(matrix): @@ -122,8 +124,10 @@ def _verify_matrix_sizes(matrix_a, matrix_b): shape = _shape(matrix_a) shape += _shape(matrix_b) if shape[0] != shape[2] or shape[1] != shape[3]: - raise ValueError(f"operands could not be broadcast together with shape " - f"({shape[0], shape[1]}), ({shape[2], shape[3]})") + raise ValueError( + f"operands could not be broadcast together with shape " + f"({shape[0], shape[1]}), ({shape[2], shape[3]})" + ) return [shape[0], shape[2]], [shape[1], shape[3]] @@ -132,13 +136,19 @@ def main(): matrix_b = [[3, 4], [7, 4]] matrix_c = [[11, 12, 13, 14], [21, 22, 23, 24], [31, 32, 33, 34], [41, 42, 43, 44]] matrix_d = [[3, 0, 2], [2, 0, -2], [0, 1, 1]] - print('Add Operation, %s + %s = %s \n' %(matrix_a, matrix_b, (add(matrix_a, matrix_b)))) - print('Multiply Operation, %s * %s = %s \n' %(matrix_a, matrix_b, multiply(matrix_a, matrix_b))) - print('Identity: %s \n' %identity(5)) - print('Minor of %s = %s \n' %(matrix_c, minor(matrix_c, 1, 2))) - print('Determinant of %s = %s \n' %(matrix_b, determinant(matrix_b))) - print('Inverse of %s = %s\n'%(matrix_d, inverse(matrix_d))) - - -if __name__ == '__main__': + print( + "Add Operation, %s + %s = %s \n" + % (matrix_a, matrix_b, (add(matrix_a, matrix_b))) + ) + print( + "Multiply Operation, %s * %s = %s \n" + % (matrix_a, matrix_b, multiply(matrix_a, matrix_b)) + ) + print("Identity: %s \n" % identity(5)) + print("Minor of {} = {} \n".format(matrix_c, minor(matrix_c, 1, 2))) + print("Determinant of {} = {} \n".format(matrix_b, determinant(matrix_b))) + print("Inverse of {} = {}\n".format(matrix_d, inverse(matrix_d))) + + +if __name__ == "__main__": main() diff --git a/matrix/nth_fibonacci_using_matrix_exponentiation.py b/matrix/nth_fibonacci_using_matrix_exponentiation.py index 7491abcae031..222779f454f9 100644 --- a/matrix/nth_fibonacci_using_matrix_exponentiation.py +++ b/matrix/nth_fibonacci_using_matrix_exponentiation.py @@ -13,7 +13,6 @@ So we just need the n times multiplication of the matrix [1,1],[1,0]]. We can decrease the n times multiplication by following the divide and conquer approach. """ -from __future__ import print_function def multiply(matrix_a, matrix_b): diff --git a/matrix/rotate_matrix.py b/matrix/rotate_matrix.py index e3495e647954..14a9493cd12f 100644 --- a/matrix/rotate_matrix.py +++ b/matrix/rotate_matrix.py @@ -1,99 +1,98 @@ -# -*- coding: utf-8 -*- - """ - In this problem, we want to rotate the matrix elements by 90, 180, 270 (counterclockwise) - Discussion in stackoverflow: - https://stackoverflow.com/questions/42519/how-do-you-rotate-a-two-dimensional-array +In this problem, we want to rotate the matrix elements by 90, 180, 270 (counterclockwise) +Discussion in stackoverflow: +https://stackoverflow.com/questions/42519/how-do-you-rotate-a-two-dimensional-array """ -def rotate_90(matrix: [[]]): +def make_matrix(row_size: int = 4) -> [[int]]: """ - >>> rotate_90([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) - [[4, 8, 12, 16], [3, 7, 11, 15], [2, 6, 10, 14], [1, 5, 9, 13]] + >>> make_matrix() + [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] + >>> make_matrix(1) + [[1]] + >>> make_matrix(-2) + [[1, 2], [3, 4]] + >>> make_matrix(3) + [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + >>> make_matrix() == make_matrix(4) + True """ - - transpose(matrix) - reverse_row(matrix) - return matrix - + row_size = abs(row_size) or 4 + return [[1 + x + y * row_size for x in range(row_size)] for y in range(row_size)] -def rotate_180(matrix: [[]]): + +def rotate_90(matrix: [[]]) -> [[]]: """ - >>> rotate_180([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) - [[16, 15, 14, 13], [12, 11, 10, 9], [8, 7, 6, 5], [4, 3, 2, 1]] + >>> rotate_90(make_matrix()) + [[4, 8, 12, 16], [3, 7, 11, 15], [2, 6, 10, 14], [1, 5, 9, 13]] + >>> rotate_90(make_matrix()) == transpose(reverse_column(make_matrix())) + True """ - - reverse_column(matrix) - reverse_row(matrix) - + + return reverse_row(transpose(matrix)) + # OR.. transpose(reverse_column(matrix)) + + +def rotate_180(matrix: [[]]) -> [[]]: """ - OR - - reverse_row(matrix) - reverse_column(matrix) + >>> rotate_180(make_matrix()) + [[16, 15, 14, 13], [12, 11, 10, 9], [8, 7, 6, 5], [4, 3, 2, 1]] + >>> rotate_180(make_matrix()) == reverse_column(reverse_row(make_matrix())) + True """ - - return matrix - -def rotate_270(matrix: [[]]): + return reverse_row(reverse_column(matrix)) + # OR.. reverse_column(reverse_row(matrix)) + + +def rotate_270(matrix: [[]]) -> [[]]: """ - >>> rotate_270([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) + >>> rotate_270(make_matrix()) [[13, 9, 5, 1], [14, 10, 6, 2], [15, 11, 7, 3], [16, 12, 8, 4]] + >>> rotate_270(make_matrix()) == transpose(reverse_row(make_matrix())) + True """ - - transpose(matrix) - reverse_column(matrix) - - """ - OR - - reverse_row(matrix) - transpose(matrix) - """ - - return matrix + + return reverse_column(transpose(matrix)) + # OR.. transpose(reverse_row(matrix)) -def transpose(matrix: [[]]): +def transpose(matrix: [[]]) -> [[]]: matrix[:] = [list(x) for x in zip(*matrix)] return matrix - - -def reverse_row(matrix: [[]]): + + +def reverse_row(matrix: [[]]) -> [[]]: matrix[:] = matrix[::-1] return matrix -def reverse_column(matrix: [[]]): +def reverse_column(matrix: [[]]) -> [[]]: matrix[:] = [x[::-1] for x in matrix] return matrix - - -def print_matrix(matrix: [[]]): + + +def print_matrix(matrix: [[]]) -> [[]]: for i in matrix: print(*i) -if __name__ == '__main__': - matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] +if __name__ == "__main__": + matrix = make_matrix() print("\norigin:\n") print_matrix(matrix) - rotate_90(matrix) print("\nrotate 90 counterclockwise:\n") - print_matrix(matrix) + print_matrix(rotate_90(matrix)) - matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] + matrix = make_matrix() print("\norigin:\n") print_matrix(matrix) - rotate_180(matrix) print("\nrotate 180:\n") - print_matrix(matrix) + print_matrix(rotate_180(matrix)) - matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]] + matrix = make_matrix() print("\norigin:\n") print_matrix(matrix) - rotate_270(matrix) print("\nrotate 270 counterclockwise:\n") - print_matrix(matrix) + print_matrix(rotate_270(matrix)) diff --git a/matrix/searching_in_sorted_matrix.py b/matrix/searching_in_sorted_matrix.py index 54913b350803..3897ffb1d9c6 100644 --- a/matrix/searching_in_sorted_matrix.py +++ b/matrix/searching_in_sorted_matrix.py @@ -2,26 +2,21 @@ def search_in_a_sorted_matrix(mat, m, n, key): i, j = m - 1, 0 while i >= 0 and j < n: if key == mat[i][j]: - print('Key %s found at row- %s column- %s' % (key, i + 1, j + 1)) + print("Key {} found at row- {} column- {}".format(key, i + 1, j + 1)) return if key < mat[i][j]: i -= 1 else: j += 1 - print('Key %s not found' % (key)) + print("Key %s not found" % (key)) def main(): - mat = [ - [2, 5, 7], - [4, 8, 13], - [9, 11, 15], - [12, 17, 20] - ] + mat = [[2, 5, 7], [4, 8, 13], [9, 11, 15], [12, 17, 20]] x = int(input("Enter the element to be searched:")) print(mat) search_in_a_sorted_matrix(mat, len(mat), len(mat[0]), x) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/matrix/sherman_morrison.py b/matrix/sherman_morrison.py new file mode 100644 index 000000000000..257cf33712d5 --- /dev/null +++ b/matrix/sherman_morrison.py @@ -0,0 +1,268 @@ +class Matrix: + """ + + Matrix structure. + """ + + def __init__(self, row: int, column: int, default_value: float = 0): + """ + + Initialize matrix with given size and default value. + + Example: + >>> a = Matrix(2, 3, 1) + >>> a + Matrix consist of 2 rows and 3 columns + [1, 1, 1] + [1, 1, 1] + """ + + self.row, self.column = row, column + self.array = [[default_value for c in range(column)] for r in range(row)] + + def __str__(self): + """ + + Return string representation of this matrix. + """ + + # Prefix + s = "Matrix consist of %d rows and %d columns\n" % (self.row, self.column) + + # Make string identifier + max_element_length = 0 + for row_vector in self.array: + for obj in row_vector: + max_element_length = max(max_element_length, len(str(obj))) + string_format_identifier = "%%%ds" % (max_element_length,) + + # Make string and return + def single_line(row_vector): + nonlocal string_format_identifier + line = "[" + line += ", ".join(string_format_identifier % (obj,) for obj in row_vector) + line += "]" + return line + + s += "\n".join(single_line(row_vector) for row_vector in self.array) + return s + + def __repr__(self): + return str(self) + + def validateIndices(self, loc: tuple): + """ + + Check if given indices are valid to pick element from matrix. + + Example: + >>> a = Matrix(2, 6, 0) + >>> a.validateIndices((2, 7)) + False + >>> a.validateIndices((0, 0)) + True + """ + if not (isinstance(loc, (list, tuple)) and len(loc) == 2): + return False + elif not (0 <= loc[0] < self.row and 0 <= loc[1] < self.column): + return False + else: + return True + + def __getitem__(self, loc: tuple): + """ + + Return array[row][column] where loc = (row, column). + + Example: + >>> a = Matrix(3, 2, 7) + >>> a[1, 0] + 7 + """ + assert self.validateIndices(loc) + return self.array[loc[0]][loc[1]] + + def __setitem__(self, loc: tuple, value: float): + """ + + Set array[row][column] = value where loc = (row, column). + + Example: + >>> a = Matrix(2, 3, 1) + >>> a[1, 2] = 51 + >>> a + Matrix consist of 2 rows and 3 columns + [ 1, 1, 1] + [ 1, 1, 51] + """ + assert self.validateIndices(loc) + self.array[loc[0]][loc[1]] = value + + def __add__(self, another): + """ + + Return self + another. + + Example: + >>> a = Matrix(2, 1, -4) + >>> b = Matrix(2, 1, 3) + >>> a+b + Matrix consist of 2 rows and 1 columns + [-1] + [-1] + """ + + # Validation + assert isinstance(another, Matrix) + assert self.row == another.row and self.column == another.column + + # Add + result = Matrix(self.row, self.column) + for r in range(self.row): + for c in range(self.column): + result[r, c] = self[r, c] + another[r, c] + return result + + def __neg__(self): + """ + + Return -self. + + Example: + >>> a = Matrix(2, 2, 3) + >>> a[0, 1] = a[1, 0] = -2 + >>> -a + Matrix consist of 2 rows and 2 columns + [-3, 2] + [ 2, -3] + """ + + result = Matrix(self.row, self.column) + for r in range(self.row): + for c in range(self.column): + result[r, c] = -self[r, c] + return result + + def __sub__(self, another): + return self + (-another) + + def __mul__(self, another): + """ + + Return self * another. + + Example: + >>> a = Matrix(2, 3, 1) + >>> a[0,2] = a[1,2] = 3 + >>> a * -2 + Matrix consist of 2 rows and 3 columns + [-2, -2, -6] + [-2, -2, -6] + """ + + if isinstance(another, (int, float)): # Scalar multiplication + result = Matrix(self.row, self.column) + for r in range(self.row): + for c in range(self.column): + result[r, c] = self[r, c] * another + return result + elif isinstance(another, Matrix): # Matrix multiplication + assert self.column == another.row + result = Matrix(self.row, another.column) + for r in range(self.row): + for c in range(another.column): + for i in range(self.column): + result[r, c] += self[r, i] * another[i, c] + return result + else: + raise TypeError( + "Unsupported type given for another ({})".format(type(another)) + ) + + def transpose(self): + """ + + Return self^T. + + Example: + >>> a = Matrix(2, 3) + >>> for r in range(2): + ... for c in range(3): + ... a[r,c] = r*c + ... + >>> a.transpose() + Matrix consist of 3 rows and 2 columns + [0, 0] + [0, 1] + [0, 2] + """ + + result = Matrix(self.column, self.row) + for r in range(self.row): + for c in range(self.column): + result[c, r] = self[r, c] + return result + + def ShermanMorrison(self, u, v): + """ + + Apply Sherman-Morrison formula in O(n^2). + To learn this formula, please look this: https://en.wikipedia.org/wiki/Sherman%E2%80%93Morrison_formula + This method returns (A + uv^T)^(-1) where A^(-1) is self. Returns None if it's impossible to calculate. + Warning: This method doesn't check if self is invertible. + Make sure self is invertible before execute this method. + + Example: + >>> ainv = Matrix(3, 3, 0) + >>> for i in range(3): ainv[i,i] = 1 + ... + >>> u = Matrix(3, 1, 0) + >>> u[0,0], u[1,0], u[2,0] = 1, 2, -3 + >>> v = Matrix(3, 1, 0) + >>> v[0,0], v[1,0], v[2,0] = 4, -2, 5 + >>> ainv.ShermanMorrison(u, v) + Matrix consist of 3 rows and 3 columns + [ 1.2857142857142856, -0.14285714285714285, 0.3571428571428571] + [ 0.5714285714285714, 0.7142857142857143, 0.7142857142857142] + [ -0.8571428571428571, 0.42857142857142855, -0.0714285714285714] + """ + + # Size validation + assert isinstance(u, Matrix) and isinstance(v, Matrix) + assert self.row == self.column == u.row == v.row # u, v should be column vector + assert u.column == v.column == 1 # u, v should be column vector + + # Calculate + vT = v.transpose() + numerator_factor = (vT * self * u)[0, 0] + 1 + if numerator_factor == 0: + return None # It's not invertable + return self - ((self * u) * (vT * self) * (1.0 / numerator_factor)) + + +# Testing +if __name__ == "__main__": + + def test1(): + # a^(-1) + ainv = Matrix(3, 3, 0) + for i in range(3): + ainv[i, i] = 1 + print(f"a^(-1) is {ainv}") + # u, v + u = Matrix(3, 1, 0) + u[0, 0], u[1, 0], u[2, 0] = 1, 2, -3 + v = Matrix(3, 1, 0) + v[0, 0], v[1, 0], v[2, 0] = 4, -2, 5 + print(f"u is {u}") + print(f"v is {v}") + print("uv^T is %s" % (u * v.transpose())) + # Sherman Morrison + print("(a + uv^T)^(-1) is {}".format(ainv.ShermanMorrison(u, v))) + + def test2(): + import doctest + + doctest.testmod() + + test2() diff --git a/matrix/spiral_print.py b/matrix/spiral_print.py index 447881e508e7..21dab76156e9 100644 --- a/matrix/spiral_print.py +++ b/matrix/spiral_print.py @@ -1,11 +1,13 @@ """ -This program print the matix in spiral form. +This program print the matrix in spiral form. This problem has been solved through recursive way. Matrix must satisfy below conditions i) matrix should be only one or two dimensional ii)column of all the row should be equal """ + + def checkMatrix(a): # must be if type(a) == list and len(a) > 0: @@ -51,7 +53,7 @@ def spiralPrint(a): # vertical printing up for i in range(matRow - 2, 0, -1): print(a[i][0]), - remainMat = [row[1:matCol - 1] for row in a[1:matRow - 1]] + remainMat = [row[1 : matCol - 1] for row in a[1 : matRow - 1]] if len(remainMat) > 0: spiralPrint(remainMat) else: @@ -62,5 +64,5 @@ def spiralPrint(a): # driver code -a = [[1 , 2, 3, 4],[5, 6, 7, 8],[9, 10, 11, 12]] +a = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] spiralPrint(a) diff --git a/matrix/tests/test_matrix_operation.py b/matrix/tests/test_matrix_operation.py index 8b81b65d0fc8..f9f72cf59af8 100644 --- a/matrix/tests/test_matrix_operation.py +++ b/matrix/tests/test_matrix_operation.py @@ -29,8 +29,9 @@ @pytest.mark.mat_ops -@pytest.mark.parametrize(('mat1', 'mat2'), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), - (mat_f, mat_h)]) +@pytest.mark.parametrize( + ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)] +) def test_addition(mat1, mat2): if (np.array(mat1)).shape < (2, 2) or (np.array(mat2)).shape < (2, 2): with pytest.raises(TypeError): @@ -48,8 +49,9 @@ def test_addition(mat1, mat2): @pytest.mark.mat_ops -@pytest.mark.parametrize(('mat1', 'mat2'), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), - (mat_f, mat_h)]) +@pytest.mark.parametrize( + ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)] +) def test_subtraction(mat1, mat2): if (np.array(mat1)).shape < (2, 2) or (np.array(mat2)).shape < (2, 2): with pytest.raises(TypeError): @@ -67,8 +69,9 @@ def test_subtraction(mat1, mat2): @pytest.mark.mat_ops -@pytest.mark.parametrize(('mat1', 'mat2'), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), - (mat_f, mat_h)]) +@pytest.mark.parametrize( + ("mat1", "mat2"), [(mat_a, mat_b), (mat_c, mat_d), (mat_d, mat_e), (mat_f, mat_h)] +) def test_multiplication(mat1, mat2): if (np.array(mat1)).shape < (2, 2) or (np.array(mat2)).shape < (2, 2): logger.info(f"\n\t{test_multiplication.__name__} returned integer") @@ -81,7 +84,9 @@ def test_multiplication(mat1, mat2): assert theo == act else: with pytest.raises(ValueError): - logger.info(f"\n\t{test_multiplication.__name__} does not meet dim requirements") + logger.info( + f"\n\t{test_multiplication.__name__} does not meet dim requirements" + ) assert matop.subtract(mat1, mat2) @@ -100,7 +105,7 @@ def test_identity(): @pytest.mark.mat_ops -@pytest.mark.parametrize('mat', [mat_a, mat_b, mat_c, mat_d, mat_e, mat_f]) +@pytest.mark.parametrize("mat", [mat_a, mat_b, mat_c, mat_d, mat_e, mat_f]) def test_transpose(mat): if (np.array(mat)).shape < (2, 2): with pytest.raises(TypeError): diff --git a/networking_flow/ford_fulkerson.py b/networking_flow/ford_fulkerson.py index d51f1f0661b3..0028c7cc577f 100644 --- a/networking_flow/ford_fulkerson.py +++ b/networking_flow/ford_fulkerson.py @@ -4,14 +4,15 @@ (1) Start with initial flow as 0; (2) Choose augmenting path from source to sink and add path to flow; """ - + + def BFS(graph, s, t, parent): # Return True if there is node that has not iterated. - visited = [False]*len(graph) - queue=[] + visited = [False] * len(graph) + queue = [] queue.append(s) visited[s] = True - + while queue: u = queue.pop(0) for ind in range(len(graph[u])): @@ -21,36 +22,40 @@ def BFS(graph, s, t, parent): parent[ind] = u return True if visited[t] else False - + + def FordFulkerson(graph, source, sink): # This array is filled by BFS and to store path - parent = [-1]*(len(graph)) - max_flow = 0 - while BFS(graph, source, sink, parent) : + parent = [-1] * (len(graph)) + max_flow = 0 + while BFS(graph, source, sink, parent): path_flow = float("Inf") s = sink - while(s != source): + while s != source: # Find the minimum value in select path - path_flow = min (path_flow, graph[parent[s]][s]) + path_flow = min(path_flow, graph[parent[s]][s]) s = parent[s] - max_flow += path_flow + max_flow += path_flow v = sink - while(v != source): + while v != source: u = parent[v] graph[u][v] -= path_flow graph[v][u] += path_flow v = parent[v] return max_flow -graph = [[0, 16, 13, 0, 0, 0], - [0, 0, 10 ,12, 0, 0], - [0, 4, 0, 0, 14, 0], - [0, 0, 9, 0, 0, 20], - [0, 0, 0, 7, 0, 4], - [0, 0, 0, 0, 0, 0]] + +graph = [ + [0, 16, 13, 0, 0, 0], + [0, 0, 10, 12, 0, 0], + [0, 4, 0, 0, 14, 0], + [0, 0, 9, 0, 0, 20], + [0, 0, 0, 7, 0, 4], + [0, 0, 0, 0, 0, 0], +] source, sink = 0, 5 -print(FordFulkerson(graph, source, sink)) \ No newline at end of file +print(FordFulkerson(graph, source, sink)) diff --git a/networking_flow/minimum_cut.py b/networking_flow/minimum_cut.py index 8ad6e03b00c6..0f6781fbb88c 100644 --- a/networking_flow/minimum_cut.py +++ b/networking_flow/minimum_cut.py @@ -1,12 +1,21 @@ # Minimum cut on Ford_Fulkerson algorithm. - + +test_graph = [ + [0, 16, 13, 0, 0, 0], + [0, 0, 10, 12, 0, 0], + [0, 4, 0, 0, 14, 0], + [0, 0, 9, 0, 0, 20], + [0, 0, 0, 7, 0, 4], + [0, 0, 0, 0, 0, 0], +] + + def BFS(graph, s, t, parent): # Return True if there is node that has not iterated. - visited = [False]*len(graph) - queue=[] - queue.append(s) + visited = [False] * len(graph) + queue = [s] visited[s] = True - + while queue: u = queue.pop(0) for ind in range(len(graph[u])): @@ -16,26 +25,30 @@ def BFS(graph, s, t, parent): parent[ind] = u return True if visited[t] else False - + + def mincut(graph, source, sink): - # This array is filled by BFS and to store path - parent = [-1]*(len(graph)) - max_flow = 0 + """This array is filled by BFS and to store path + >>> mincut(test_graph, source=0, sink=5) + [(1, 3), (4, 3), (4, 5)] + """ + parent = [-1] * (len(graph)) + max_flow = 0 res = [] - temp = [i[:] for i in graph] # Record orignial cut, copy. - while BFS(graph, source, sink, parent) : + temp = [i[:] for i in graph] # Record original cut, copy. + while BFS(graph, source, sink, parent): path_flow = float("Inf") s = sink - while(s != source): + while s != source: # Find the minimum value in select path - path_flow = min (path_flow, graph[parent[s]][s]) + path_flow = min(path_flow, graph[parent[s]][s]) s = parent[s] - max_flow += path_flow + max_flow += path_flow v = sink - - while(v != source): + + while v != source: u = parent[v] graph[u][v] -= path_flow graph[v][u] += path_flow @@ -44,16 +57,10 @@ def mincut(graph, source, sink): for i in range(len(graph)): for j in range(len(graph[0])): if graph[i][j] == 0 and temp[i][j] > 0: - res.append((i,j)) + res.append((i, j)) return res -graph = [[0, 16, 13, 0, 0, 0], - [0, 0, 10 ,12, 0, 0], - [0, 4, 0, 0, 14, 0], - [0, 0, 9, 0, 0, 20], - [0, 0, 0, 7, 0, 4], - [0, 0, 0, 0, 0, 0]] -source, sink = 0, 5 -print(mincut(graph, source, sink)) \ No newline at end of file +if __name__ == "__main__": + print(mincut(test_graph, source=0, sink=5)) diff --git a/neural_network/back_propagation_neural_network.py b/neural_network/back_propagation_neural_network.py index 92deaee19c6e..c771dc46afc2 100644 --- a/neural_network/back_propagation_neural_network.py +++ b/neural_network/back_propagation_neural_network.py @@ -1,7 +1,6 @@ #!/usr/bin/python -# encoding=utf8 -''' +""" A Framework of Back Propagation Neural Network(BP) model @@ -17,7 +16,7 @@ Github : https://github.com/RiptideBo Date: 2017.11.23 -''' +""" import numpy as np import matplotlib.pyplot as plt @@ -26,18 +25,22 @@ def sigmoid(x): return 1 / (1 + np.exp(-1 * x)) -class DenseLayer(): - ''' + +class DenseLayer: + """ Layers of BP neural network - ''' - def __init__(self,units,activation=None,learning_rate=None,is_input_layer=False): - ''' + """ + + def __init__( + self, units, activation=None, learning_rate=None, is_input_layer=False + ): + """ common connected layer of bp network :param units: numbers of neural units :param activation: activation function :param learning_rate: learning rate for paras :param is_input_layer: whether it is input layer or not - ''' + """ self.units = units self.weight = None self.bias = None @@ -47,21 +50,22 @@ def __init__(self,units,activation=None,learning_rate=None,is_input_layer=False) self.learn_rate = learning_rate self.is_input_layer = is_input_layer - def initializer(self,back_units): - self.weight = np.asmatrix(np.random.normal(0,0.5,(self.units,back_units))) - self.bias = np.asmatrix(np.random.normal(0,0.5,self.units)).T + def initializer(self, back_units): + self.weight = np.asmatrix(np.random.normal(0, 0.5, (self.units, back_units))) + self.bias = np.asmatrix(np.random.normal(0, 0.5, self.units)).T if self.activation is None: self.activation = sigmoid def cal_gradient(self): + # activation function may be sigmoid or linear if self.activation == sigmoid: - gradient_mat = np.dot(self.output ,(1- self.output).T) + gradient_mat = np.dot(self.output, (1 - self.output).T) gradient_activation = np.diag(np.diag(gradient_mat)) else: gradient_activation = 1 return gradient_activation - def forward_propagation(self,xdata): + def forward_propagation(self, xdata): self.xdata = xdata if self.is_input_layer: # input layer @@ -69,57 +73,56 @@ def forward_propagation(self,xdata): self.output = xdata return xdata else: - self.wx_plus_b = np.dot(self.weight,self.xdata) - self.bias + self.wx_plus_b = np.dot(self.weight, self.xdata) - self.bias self.output = self.activation(self.wx_plus_b) return self.output - def back_propagation(self,gradient): - - gradient_activation = self.cal_gradient() # i * i 维 - gradient = np.asmatrix(np.dot(gradient.T,gradient_activation)) + def back_propagation(self, gradient): + gradient_activation = self.cal_gradient() # i * i 维 + gradient = np.asmatrix(np.dot(gradient.T, gradient_activation)) self._gradient_weight = np.asmatrix(self.xdata) self._gradient_bias = -1 self._gradient_x = self.weight - self.gradient_weight = np.dot(gradient.T,self._gradient_weight.T) + self.gradient_weight = np.dot(gradient.T, self._gradient_weight.T) self.gradient_bias = gradient * self._gradient_bias - self.gradient = np.dot(gradient,self._gradient_x).T - # ----------------------upgrade - # -----------the Negative gradient direction -------- + self.gradient = np.dot(gradient, self._gradient_x).T + # upgrade: the Negative gradient direction self.weight = self.weight - self.learn_rate * self.gradient_weight self.bias = self.bias - self.learn_rate * self.gradient_bias.T - + # updates the weights and bias according to learning rate (0.3 if undefined) return self.gradient -class BPNN(): - ''' +class BPNN: + """ Back Propagation Neural Network model - ''' + """ + def __init__(self): self.layers = [] self.train_mse = [] self.fig_loss = plt.figure() - self.ax_loss = self.fig_loss.add_subplot(1,1,1) + self.ax_loss = self.fig_loss.add_subplot(1, 1, 1) - def add_layer(self,layer): + def add_layer(self, layer): self.layers.append(layer) def build(self): - for i,layer in enumerate(self.layers[:]): + for i, layer in enumerate(self.layers[:]): if i < 1: layer.is_input_layer = True else: - layer.initializer(self.layers[i-1].units) + layer.initializer(self.layers[i - 1].units) def summary(self): - for i,layer in enumerate(self.layers[:]): - print('------- layer %d -------'%i) - print('weight.shape ',np.shape(layer.weight)) - print('bias.shape ',np.shape(layer.bias)) + for i, layer in enumerate(self.layers[:]): + print("------- layer %d -------" % i) + print("weight.shape ", np.shape(layer.weight)) + print("bias.shape ", np.shape(layer.bias)) - def train(self,xdata,ydata,train_round,accuracy): + def train(self, xdata, ydata, train_round, accuracy): self.train_round = train_round self.accuracy = accuracy @@ -129,8 +132,8 @@ def train(self,xdata,ydata,train_round,accuracy): for round_i in range(train_round): all_loss = 0 for row in range(x_shape[0]): - _xdata = np.asmatrix(xdata[row,:]).T - _ydata = np.asmatrix(ydata[row,:]).T + _xdata = np.asmatrix(xdata[row, :]).T + _ydata = np.asmatrix(ydata[row, :]).T # forward propagation for layer in self.layers: @@ -139,57 +142,59 @@ def train(self,xdata,ydata,train_round,accuracy): loss, gradient = self.cal_loss(_ydata, _xdata) all_loss = all_loss + loss - # back propagation - # the input_layer does not upgrade + # back propagation: the input_layer does not upgrade for layer in self.layers[:0:-1]: gradient = layer.back_propagation(gradient) - mse = all_loss/x_shape[0] + mse = all_loss / x_shape[0] self.train_mse.append(mse) self.plot_loss() if mse < self.accuracy: - print('----达到精度----') + print("----达到精度----") return mse - def cal_loss(self,ydata,ydata_): - self.loss = np.sum(np.power((ydata - ydata_),2)) + def cal_loss(self, ydata, ydata_): + self.loss = np.sum(np.power((ydata - ydata_), 2)) self.loss_gradient = 2 * (ydata_ - ydata) # vector (shape is the same as _ydata.shape) - return self.loss,self.loss_gradient + return self.loss, self.loss_gradient def plot_loss(self): if self.ax_loss.lines: self.ax_loss.lines.remove(self.ax_loss.lines[0]) - self.ax_loss.plot(self.train_mse, 'r-') + self.ax_loss.plot(self.train_mse, "r-") plt.ion() - plt.xlabel('step') - plt.ylabel('loss') + plt.xlabel("step") + plt.ylabel("loss") plt.show() plt.pause(0.1) - - def example(): - - x = np.random.randn(10,10) - y = np.asarray([[0.8,0.4],[0.4,0.3],[0.34,0.45],[0.67,0.32], - [0.88,0.67],[0.78,0.77],[0.55,0.66],[0.55,0.43],[0.54,0.1], - [0.1,0.5]]) - + x = np.random.randn(10, 10) + y = np.asarray( + [ + [0.8, 0.4], + [0.4, 0.3], + [0.34, 0.45], + [0.67, 0.32], + [0.88, 0.67], + [0.78, 0.77], + [0.55, 0.66], + [0.55, 0.43], + [0.54, 0.1], + [0.1, 0.5], + ] + ) model = BPNN() - model.add_layer(DenseLayer(10)) - model.add_layer(DenseLayer(20)) - model.add_layer(DenseLayer(30)) - model.add_layer(DenseLayer(2)) - + for i in (10, 20, 30, 2): + model.add_layer(DenseLayer(i)) model.build() - model.summary() + model.train(xdata=x, ydata=y, train_round=100, accuracy=0.01) - model.train(xdata=x,ydata=y,train_round=100,accuracy=0.01) -if __name__ == '__main__': +if __name__ == "__main__": example() diff --git a/neural_network/convolution_neural_network.py b/neural_network/convolution_neural_network.py index 0e72f0c0dca2..ac0eddeb5cb6 100644 --- a/neural_network/convolution_neural_network.py +++ b/neural_network/convolution_neural_network.py @@ -1,30 +1,28 @@ -#-*- coding: utf-8 -*- - -''' +""" - - - - - -- - - - - - - - - - - - - - - - - - - - - - - Name - - CNN - Convolution Neural Network For Photo Recognizing - Goal - - Recognize Handing Writting Word Photo + Goal - - Recognize Handing Writing Word Photo Detail:Total 5 layers neural network * Convolution layer * Pooling layer * Input layer layer of BP - * Hiden layer of BP + * Hidden layer of BP * Output layer of BP Author: Stephen Lee Github: 245885195@qq.com Date: 2017.9.20 - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -''' -from __future__ import print_function - +""" import pickle import numpy as np import matplotlib.pyplot as plt -class CNN(): - def __init__(self, conv1_get, size_p1, bp_num1, bp_num2, bp_num3, rate_w=0.2, rate_t=0.2): - ''' +class CNN: + def __init__( + self, conv1_get, size_p1, bp_num1, bp_num2, bp_num3, rate_w=0.2, rate_t=0.2 + ): + """ :param conv1_get: [a,c,d],size, number, step of convolution kernel :param size_p1: pooling size :param bp_num1: units number of flatten layer @@ -32,7 +30,7 @@ def __init__(self, conv1_get, size_p1, bp_num1, bp_num2, bp_num3, rate_w=0.2, ra :param bp_num3: units number of output layer :param rate_w: rate of weight learning :param rate_t: rate of threshold learning - ''' + """ self.num_bp1 = bp_num1 self.num_bp2 = bp_num2 self.num_bp3 = bp_num3 @@ -41,241 +39,288 @@ def __init__(self, conv1_get, size_p1, bp_num1, bp_num2, bp_num3, rate_w=0.2, ra self.size_pooling1 = size_p1 self.rate_weight = rate_w self.rate_thre = rate_t - self.w_conv1 = [np.mat(-1*np.random.rand(self.conv1[0],self.conv1[0])+0.5) for i in range(self.conv1[1])] + self.w_conv1 = [ + np.mat(-1 * np.random.rand(self.conv1[0], self.conv1[0]) + 0.5) + for i in range(self.conv1[1]) + ] self.wkj = np.mat(-1 * np.random.rand(self.num_bp3, self.num_bp2) + 0.5) - self.vji = np.mat(-1*np.random.rand(self.num_bp2, self.num_bp1)+0.5) - self.thre_conv1 = -2*np.random.rand(self.conv1[1])+1 - self.thre_bp2 = -2*np.random.rand(self.num_bp2)+1 - self.thre_bp3 = -2*np.random.rand(self.num_bp3)+1 - + self.vji = np.mat(-1 * np.random.rand(self.num_bp2, self.num_bp1) + 0.5) + self.thre_conv1 = -2 * np.random.rand(self.conv1[1]) + 1 + self.thre_bp2 = -2 * np.random.rand(self.num_bp2) + 1 + self.thre_bp3 = -2 * np.random.rand(self.num_bp3) + 1 def save_model(self, save_path): - #save model dict with pickle - model_dic = {'num_bp1':self.num_bp1, - 'num_bp2':self.num_bp2, - 'num_bp3':self.num_bp3, - 'conv1':self.conv1, - 'step_conv1':self.step_conv1, - 'size_pooling1':self.size_pooling1, - 'rate_weight':self.rate_weight, - 'rate_thre':self.rate_thre, - 'w_conv1':self.w_conv1, - 'wkj':self.wkj, - 'vji':self.vji, - 'thre_conv1':self.thre_conv1, - 'thre_bp2':self.thre_bp2, - 'thre_bp3':self.thre_bp3} - with open(save_path, 'wb') as f: + # save model dict with pickle + model_dic = { + "num_bp1": self.num_bp1, + "num_bp2": self.num_bp2, + "num_bp3": self.num_bp3, + "conv1": self.conv1, + "step_conv1": self.step_conv1, + "size_pooling1": self.size_pooling1, + "rate_weight": self.rate_weight, + "rate_thre": self.rate_thre, + "w_conv1": self.w_conv1, + "wkj": self.wkj, + "vji": self.vji, + "thre_conv1": self.thre_conv1, + "thre_bp2": self.thre_bp2, + "thre_bp3": self.thre_bp3, + } + with open(save_path, "wb") as f: pickle.dump(model_dic, f) - print('Model saved: %s'% save_path) + print("Model saved: %s" % save_path) @classmethod def ReadModel(cls, model_path): - #read saved model - with open(model_path, 'rb') as f: + # read saved model + with open(model_path, "rb") as f: model_dic = pickle.load(f) - conv_get= model_dic.get('conv1') - conv_get.append(model_dic.get('step_conv1')) - size_p1 = model_dic.get('size_pooling1') - bp1 = model_dic.get('num_bp1') - bp2 = model_dic.get('num_bp2') - bp3 = model_dic.get('num_bp3') - r_w = model_dic.get('rate_weight') - r_t = model_dic.get('rate_thre') - #create model instance - conv_ins = CNN(conv_get,size_p1,bp1,bp2,bp3,r_w,r_t) - #modify model parameter - conv_ins.w_conv1 = model_dic.get('w_conv1') - conv_ins.wkj = model_dic.get('wkj') - conv_ins.vji = model_dic.get('vji') - conv_ins.thre_conv1 = model_dic.get('thre_conv1') - conv_ins.thre_bp2 = model_dic.get('thre_bp2') - conv_ins.thre_bp3 = model_dic.get('thre_bp3') + conv_get = model_dic.get("conv1") + conv_get.append(model_dic.get("step_conv1")) + size_p1 = model_dic.get("size_pooling1") + bp1 = model_dic.get("num_bp1") + bp2 = model_dic.get("num_bp2") + bp3 = model_dic.get("num_bp3") + r_w = model_dic.get("rate_weight") + r_t = model_dic.get("rate_thre") + # create model instance + conv_ins = CNN(conv_get, size_p1, bp1, bp2, bp3, r_w, r_t) + # modify model parameter + conv_ins.w_conv1 = model_dic.get("w_conv1") + conv_ins.wkj = model_dic.get("wkj") + conv_ins.vji = model_dic.get("vji") + conv_ins.thre_conv1 = model_dic.get("thre_conv1") + conv_ins.thre_bp2 = model_dic.get("thre_bp2") + conv_ins.thre_bp3 = model_dic.get("thre_bp3") return conv_ins - def sig(self, x): - return 1 / (1 + np.exp(-1*x)) + return 1 / (1 + np.exp(-1 * x)) def do_round(self, x): return round(x, 3) def convolute(self, data, convs, w_convs, thre_convs, conv_step): - #convolution process + # convolution process size_conv = convs[0] - num_conv =convs[1] + num_conv = convs[1] size_data = np.shape(data)[0] - #get the data slice of original image data, data_focus + # get the data slice of original image data, data_focus data_focus = [] for i_focus in range(0, size_data - size_conv + 1, conv_step): for j_focus in range(0, size_data - size_conv + 1, conv_step): - focus = data[i_focus:i_focus + size_conv, j_focus:j_focus + size_conv] + focus = data[ + i_focus : i_focus + size_conv, j_focus : j_focus + size_conv + ] data_focus.append(focus) - #caculate the feature map of every single kernel, and saved as list of matrix + # calculate the feature map of every single kernel, and saved as list of matrix data_featuremap = [] Size_FeatureMap = int((size_data - size_conv) / conv_step + 1) for i_map in range(num_conv): featuremap = [] for i_focus in range(len(data_focus)): - net_focus = np.sum(np.multiply(data_focus[i_focus], w_convs[i_map])) - thre_convs[i_map] + net_focus = ( + np.sum(np.multiply(data_focus[i_focus], w_convs[i_map])) + - thre_convs[i_map] + ) featuremap.append(self.sig(net_focus)) - featuremap = np.asmatrix(featuremap).reshape(Size_FeatureMap, Size_FeatureMap) + featuremap = np.asmatrix(featuremap).reshape( + Size_FeatureMap, Size_FeatureMap + ) data_featuremap.append(featuremap) - #expanding the data slice to One dimenssion + # expanding the data slice to One dimenssion focus1_list = [] for each_focus in data_focus: focus1_list.extend(self.Expand_Mat(each_focus)) focus_list = np.asarray(focus1_list) - return focus_list,data_featuremap + return focus_list, data_featuremap - def pooling(self, featuremaps, size_pooling, type='average_pool'): - #pooling process + def pooling(self, featuremaps, size_pooling, type="average_pool"): + # pooling process size_map = len(featuremaps[0]) - size_pooled = int(size_map/size_pooling) + size_pooled = int(size_map / size_pooling) featuremap_pooled = [] for i_map in range(len(featuremaps)): map = featuremaps[i_map] map_pooled = [] - for i_focus in range(0,size_map,size_pooling): + for i_focus in range(0, size_map, size_pooling): for j_focus in range(0, size_map, size_pooling): - focus = map[i_focus:i_focus + size_pooling, j_focus:j_focus + size_pooling] - if type == 'average_pool': - #average pooling + focus = map[ + i_focus : i_focus + size_pooling, + j_focus : j_focus + size_pooling, + ] + if type == "average_pool": + # average pooling map_pooled.append(np.average(focus)) - elif type == 'max_pooling': - #max pooling + elif type == "max_pooling": + # max pooling map_pooled.append(np.max(focus)) - map_pooled = np.asmatrix(map_pooled).reshape(size_pooled,size_pooled) + map_pooled = np.asmatrix(map_pooled).reshape(size_pooled, size_pooled) featuremap_pooled.append(map_pooled) return featuremap_pooled - def _expand(self, datas): - #expanding three dimension data to one dimension list + def _expand(self, data): + # expanding three dimension data to one dimension list data_expanded = [] - for i in range(len(datas)): - shapes = np.shape(datas[i]) - data_listed = datas[i].reshape(1,shapes[0]*shapes[1]) + for i in range(len(data)): + shapes = np.shape(data[i]) + data_listed = data[i].reshape(1, shapes[0] * shapes[1]) data_listed = data_listed.getA().tolist()[0] data_expanded.extend(data_listed) data_expanded = np.asarray(data_expanded) return data_expanded def _expand_mat(self, data_mat): - #expanding matrix to one dimension list + # expanding matrix to one dimension list data_mat = np.asarray(data_mat) shapes = np.shape(data_mat) - data_expanded = data_mat.reshape(1,shapes[0]*shapes[1]) + data_expanded = data_mat.reshape(1, shapes[0] * shapes[1]) return data_expanded - def _calculate_gradient_from_pool(self, out_map, pd_pool,num_map, size_map, size_pooling): - ''' - calcluate the gradient from the data slice of pool layer + def _calculate_gradient_from_pool( + self, out_map, pd_pool, num_map, size_map, size_pooling + ): + """ + calculate the gradient from the data slice of pool layer pd_pool: list of matrix out_map: the shape of data slice(size_map*size_map) return: pd_all: list of matrix, [num, size_map, size_map] - ''' + """ pd_all = [] i_pool = 0 for i_map in range(num_map): pd_conv1 = np.ones((size_map, size_map)) for i in range(0, size_map, size_pooling): for j in range(0, size_map, size_pooling): - pd_conv1[i:i + size_pooling, j:j + size_pooling] = pd_pool[i_pool] + pd_conv1[i : i + size_pooling, j : j + size_pooling] = pd_pool[ + i_pool + ] i_pool = i_pool + 1 - pd_conv2 = np.multiply(pd_conv1,np.multiply(out_map[i_map],(1-out_map[i_map]))) + pd_conv2 = np.multiply( + pd_conv1, np.multiply(out_map[i_map], (1 - out_map[i_map])) + ) pd_all.append(pd_conv2) return pd_all - def train(self, patterns, datas_train, datas_teach, n_repeat, error_accuracy, draw_e = bool): - #model traning - print('----------------------Start Training-------------------------') - print((' - - Shape: Train_Data ',np.shape(datas_train))) - print((' - - Shape: Teach_Data ',np.shape(datas_teach))) + def train( + self, patterns, datas_train, datas_teach, n_repeat, error_accuracy, draw_e=bool + ): + # model traning + print("----------------------Start Training-------------------------") + print((" - - Shape: Train_Data ", np.shape(datas_train))) + print((" - - Shape: Teach_Data ", np.shape(datas_teach))) rp = 0 all_mse = [] - mse = 10000 + mse = 10000 while rp < n_repeat and mse >= error_accuracy: - alle = 0 - print('-------------Learning Time %d--------------'%rp) + error_count = 0 + print("-------------Learning Time %d--------------" % rp) for p in range(len(datas_train)): - #print('------------Learning Image: %d--------------'%p) + # print('------------Learning Image: %d--------------'%p) data_train = np.asmatrix(datas_train[p]) data_teach = np.asarray(datas_teach[p]) - data_focus1,data_conved1 = self.convolute(data_train,self.conv1,self.w_conv1, - self.thre_conv1,conv_step=self.step_conv1) - data_pooled1 = self.pooling(data_conved1,self.size_pooling1) + data_focus1, data_conved1 = self.convolute( + data_train, + self.conv1, + self.w_conv1, + self.thre_conv1, + conv_step=self.step_conv1, + ) + data_pooled1 = self.pooling(data_conved1, self.size_pooling1) shape_featuremap1 = np.shape(data_conved1) - ''' + """ print(' -----original shape ', np.shape(data_train)) print(' ---- after convolution ',np.shape(data_conv1)) print(' -----after pooling ',np.shape(data_pooled1)) - ''' + """ data_bp_input = self._expand(data_pooled1) bp_out1 = data_bp_input - bp_net_j = np.dot(bp_out1,self.vji.T) - self.thre_bp2 + bp_net_j = np.dot(bp_out1, self.vji.T) - self.thre_bp2 bp_out2 = self.sig(bp_net_j) - bp_net_k = np.dot(bp_out2 ,self.wkj.T) - self.thre_bp3 + bp_net_k = np.dot(bp_out2, self.wkj.T) - self.thre_bp3 bp_out3 = self.sig(bp_net_k) - #--------------Model Leaning ------------------------ - # calcluate error and gradient--------------- - pd_k_all = np.multiply((data_teach - bp_out3), np.multiply(bp_out3, (1 - bp_out3))) - pd_j_all = np.multiply(np.dot(pd_k_all,self.wkj), np.multiply(bp_out2, (1 - bp_out2))) - pd_i_all = np.dot(pd_j_all,self.vji) + # --------------Model Leaning ------------------------ + # calculate error and gradient--------------- + pd_k_all = np.multiply( + (data_teach - bp_out3), np.multiply(bp_out3, (1 - bp_out3)) + ) + pd_j_all = np.multiply( + np.dot(pd_k_all, self.wkj), np.multiply(bp_out2, (1 - bp_out2)) + ) + pd_i_all = np.dot(pd_j_all, self.vji) - pd_conv1_pooled = pd_i_all / (self.size_pooling1*self.size_pooling1) + pd_conv1_pooled = pd_i_all / (self.size_pooling1 * self.size_pooling1) pd_conv1_pooled = pd_conv1_pooled.T.getA().tolist() - pd_conv1_all = self._calculate_gradient_from_pool(data_conved1,pd_conv1_pooled,shape_featuremap1[0], - shape_featuremap1[1],self.size_pooling1) - #weight and threshold learning process--------- - #convolution layer + pd_conv1_all = self._calculate_gradient_from_pool( + data_conved1, + pd_conv1_pooled, + shape_featuremap1[0], + shape_featuremap1[1], + self.size_pooling1, + ) + # weight and threshold learning process--------- + # convolution layer for k_conv in range(self.conv1[1]): pd_conv_list = self._expand_mat(pd_conv1_all[k_conv]) - delta_w = self.rate_weight * np.dot(pd_conv_list,data_focus1) + delta_w = self.rate_weight * np.dot(pd_conv_list, data_focus1) - self.w_conv1[k_conv] = self.w_conv1[k_conv] + delta_w.reshape((self.conv1[0],self.conv1[0])) + self.w_conv1[k_conv] = self.w_conv1[k_conv] + delta_w.reshape( + (self.conv1[0], self.conv1[0]) + ) - self.thre_conv1[k_conv] = self.thre_conv1[k_conv] - np.sum(pd_conv1_all[k_conv]) * self.rate_thre - #all connected layer + self.thre_conv1[k_conv] = ( + self.thre_conv1[k_conv] + - np.sum(pd_conv1_all[k_conv]) * self.rate_thre + ) + # all connected layer self.wkj = self.wkj + pd_k_all.T * bp_out2 * self.rate_weight self.vji = self.vji + pd_j_all.T * bp_out1 * self.rate_weight self.thre_bp3 = self.thre_bp3 - pd_k_all * self.rate_thre self.thre_bp2 = self.thre_bp2 - pd_j_all * self.rate_thre # calculate the sum error of all single image - errors = np.sum(abs((data_teach - bp_out3))) - alle = alle + errors - #print(' ----Teach ',data_teach) - #print(' ----BP_output ',bp_out3) + errors = np.sum(abs(data_teach - bp_out3)) + error_count += errors + # print(' ----Teach ',data_teach) + # print(' ----BP_output ',bp_out3) rp = rp + 1 - mse = alle/patterns + mse = error_count / patterns all_mse.append(mse) + def draw_error(): yplot = [error_accuracy for i in range(int(n_repeat * 1.2))] - plt.plot(all_mse, '+-') - plt.plot(yplot, 'r--') - plt.xlabel('Learning Times') - plt.ylabel('All_mse') + plt.plot(all_mse, "+-") + plt.plot(yplot, "r--") + plt.xlabel("Learning Times") + plt.ylabel("All_mse") plt.grid(True, alpha=0.5) plt.show() - print('------------------Training Complished---------------------') - print((' - - Training epoch: ', rp, ' - - Mse: %.6f' % mse)) + + print("------------------Training Complished---------------------") + print((" - - Training epoch: ", rp, " - - Mse: %.6f" % mse)) if draw_e: draw_error() return mse def predict(self, datas_test): - #model predict + # model predict produce_out = [] - print('-------------------Start Testing-------------------------') - print((' - - Shape: Test_Data ',np.shape(datas_test))) + print("-------------------Start Testing-------------------------") + print((" - - Shape: Test_Data ", np.shape(datas_test))) for p in range(len(datas_test)): data_test = np.asmatrix(datas_test[p]) - data_focus1, data_conved1 = self.convolute(data_test, self.conv1, self.w_conv1, - self.thre_conv1, conv_step=self.step_conv1) + data_focus1, data_conved1 = self.convolute( + data_test, + self.conv1, + self.w_conv1, + self.thre_conv1, + conv_step=self.step_conv1, + ) data_pooled1 = self.pooling(data_conved1, self.size_pooling1) data_bp_input = self._expand(data_pooled1) @@ -285,21 +330,25 @@ def predict(self, datas_test): bp_net_k = bp_out2 * self.wkj.T - self.thre_bp3 bp_out3 = self.sig(bp_net_k) produce_out.extend(bp_out3.getA().tolist()) - res = [list(map(self.do_round,each)) for each in produce_out] + res = [list(map(self.do_round, each)) for each in produce_out] return np.asarray(res) def convolution(self, data): - #return the data of image after convoluting process so we can check it out + # return the data of image after convoluting process so we can check it out data_test = np.asmatrix(data) - data_focus1, data_conved1 = self.convolute(data_test, self.conv1, self.w_conv1, - self.thre_conv1, conv_step=self.step_conv1) + data_focus1, data_conved1 = self.convolute( + data_test, + self.conv1, + self.w_conv1, + self.thre_conv1, + conv_step=self.step_conv1, + ) data_pooled1 = self.pooling(data_conved1, self.size_pooling1) - return data_conved1,data_pooled1 + return data_conved1, data_pooled1 -if __name__ == '__main__': - pass - ''' +if __name__ == "__main__": + """ I will put the example on other file -''' + """ diff --git a/neural_network/fully_connected_neural_network.ipynb b/neural_network/fully_connected_neural_network.ipynb deleted file mode 100644 index a8bcf4beeea1..000000000000 --- a/neural_network/fully_connected_neural_network.ipynb +++ /dev/null @@ -1,327 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Standard (Fully Connected) Neural Network" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "#Use in Markup cell type\n", - "#![alt text](imagename.png \"Title\") " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Implementing Fully connected Neural Net" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Loading Required packages and Data" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Using TensorFlow backend.\n" - ] - } - ], - "source": [ - "###1. Load Data and Splot Data\n", - "from keras.datasets import mnist\n", - "from keras.models import Sequential \n", - "from keras.layers.core import Dense, Activation\n", - "from keras.utils import np_utils\n", - "(X_train, Y_train), (X_test, Y_test) = mnist.load_data()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Preprocessing" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "n = 10 # how many digits we will display\n", - "plt.figure(figsize=(20, 4))\n", - "for i in range(n):\n", - " # display original\n", - " ax = plt.subplot(2, n, i + 1)\n", - " plt.imshow(X_test[i].reshape(28, 28))\n", - " plt.gray()\n", - " ax.get_xaxis().set_visible(False)\n", - " ax.get_yaxis().set_visible(False)\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Previous X_train shape: (60000, 28, 28) \n", - "Previous Y_train shape:(60000,)\n", - "New X_train shape: (60000, 784) \n", - "New Y_train shape:(60000, 10)\n" - ] - } - ], - "source": [ - "print(\"Previous X_train shape: {} \\nPrevious Y_train shape:{}\".format(X_train.shape, Y_train.shape))\n", - "X_train = X_train.reshape(60000, 784) \n", - "X_test = X_test.reshape(10000, 784)\n", - "X_train = X_train.astype('float32') \n", - "X_test = X_test.astype('float32') \n", - "X_train /= 255 \n", - "X_test /= 255\n", - "classes = 10\n", - "Y_train = np_utils.to_categorical(Y_train, classes) \n", - "Y_test = np_utils.to_categorical(Y_test, classes)\n", - "print(\"New X_train shape: {} \\nNew Y_train shape:{}\".format(X_train.shape, Y_train.shape))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Setting up parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "input_size = 784\n", - "batch_size = 200 \n", - "hidden1 = 400\n", - "hidden2 = 20\n", - "epochs = 2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Building the FCN Model" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "_________________________________________________________________\n", - "Layer (type) Output Shape Param # \n", - "=================================================================\n", - "dense_1 (Dense) (None, 400) 314000 \n", - "_________________________________________________________________\n", - "dense_2 (Dense) (None, 20) 8020 \n", - "_________________________________________________________________\n", - "dense_3 (Dense) (None, 10) 210 \n", - "=================================================================\n", - "Total params: 322,230\n", - "Trainable params: 322,230\n", - "Non-trainable params: 0\n", - "_________________________________________________________________\n" - ] - } - ], - "source": [ - "###4.Build the model\n", - "model = Sequential() \n", - "model.add(Dense(hidden1, input_dim=input_size, activation='relu'))\n", - "# output = relu (dot (W, input) + bias)\n", - "model.add(Dense(hidden2, activation='relu'))\n", - "model.add(Dense(classes, activation='softmax')) \n", - "\n", - "# Compilation\n", - "model.compile(loss='categorical_crossentropy', \n", - " metrics=['accuracy'], optimizer='sgd')\n", - "model.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Training The Model" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1/10\n", - " - 12s - loss: 1.4482 - acc: 0.6251\n", - "Epoch 2/10\n", - " - 3s - loss: 0.6239 - acc: 0.8482\n", - "Epoch 3/10\n", - " - 3s - loss: 0.4582 - acc: 0.8798\n", - "Epoch 4/10\n", - " - 3s - loss: 0.3941 - acc: 0.8936\n", - "Epoch 5/10\n", - " - 3s - loss: 0.3579 - acc: 0.9011\n", - "Epoch 6/10\n", - " - 4s - loss: 0.3328 - acc: 0.9070\n", - "Epoch 7/10\n", - " - 3s - loss: 0.3138 - acc: 0.9118\n", - "Epoch 8/10\n", - " - 3s - loss: 0.2980 - acc: 0.9157\n", - "Epoch 9/10\n", - " - 3s - loss: 0.2849 - acc: 0.9191\n", - "Epoch 10/10\n", - " - 3s - loss: 0.2733 - acc: 0.9223\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Fitting on Data\n", - "model.fit(X_train, Y_train, batch_size=batch_size, epochs=10, verbose=2)\n", - "###5.Test " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "#### Testing The Model" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "10000/10000 [==============================] - 1s 121us/step\n", - "\n", - "Test accuracy: 0.9257\n", - "[0 6 9 0 1 5 9 7 3 4]\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "score = model.evaluate(X_test, Y_test, verbose=1)\n", - "print('\\n''Test accuracy:', score[1])\n", - "mask = range(10,20)\n", - "X_valid = X_test[mask]\n", - "y_pred = model.predict_classes(X_valid)\n", - "print(y_pred)\n", - "plt.figure(figsize=(20, 4))\n", - "for i in range(n):\n", - " # display original\n", - " ax = plt.subplot(2, n, i + 1)\n", - " plt.imshow(X_valid[i].reshape(28, 28))\n", - " plt.gray()\n", - " ax.get_xaxis().set_visible(False)\n", - " ax.get_yaxis().set_visible(False)\n", - "plt.show()\n", - "plt.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/neural_network/gan.py_tf b/neural_network/gan.py_tf new file mode 100644 index 000000000000..deb062c48dc7 --- /dev/null +++ b/neural_network/gan.py_tf @@ -0,0 +1,506 @@ +import matplotlib.gridspec as gridspec +import matplotlib.pyplot as plt +import numpy as np +from sklearn.utils import shuffle +import input_data + +random_numer = 42 + +np.random.seed(random_numer) + + +def ReLu(x): + mask = (x > 0) * 1.0 + return mask * x + + +def d_ReLu(x): + mask = (x > 0) * 1.0 + return mask + + +def arctan(x): + return np.arctan(x) + + +def d_arctan(x): + return 1 / (1 + x ** 2) + + +def log(x): + return 1 / (1 + np.exp(-1 * x)) + + +def d_log(x): + return log(x) * (1 - log(x)) + + +def tanh(x): + return np.tanh(x) + + +def d_tanh(x): + return 1 - np.tanh(x) ** 2 + + +def plot(samples): + fig = plt.figure(figsize=(4, 4)) + gs = gridspec.GridSpec(4, 4) + gs.update(wspace=0.05, hspace=0.05) + + for i, sample in enumerate(samples): + ax = plt.subplot(gs[i]) + plt.axis("off") + ax.set_xticklabels([]) + ax.set_yticklabels([]) + ax.set_aspect("equal") + plt.imshow(sample.reshape(28, 28), cmap="Greys_r") + + return fig + + +if __name__ == "__main__": + # 1. Load Data and declare hyper + print("--------- Load Data ----------") + mnist = input_data.read_data_sets("MNIST_data", one_hot=False) + temp = mnist.test + images, labels = temp.images, temp.labels + images, labels = shuffle(np.asarray(images), np.asarray(labels)) + num_epoch = 10 + learing_rate = 0.00009 + G_input = 100 + hidden_input, hidden_input2, hidden_input3 = 128, 256, 346 + hidden_input4, hidden_input5, hidden_input6 = 480, 560, 686 + + print("--------- Declare Hyper Parameters ----------") + # 2. Declare Weights + D_W1 = ( + np.random.normal(size=(784, hidden_input), scale=(1.0 / np.sqrt(784 / 2.0))) + * 0.002 + ) + # D_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + D_b1 = np.zeros(hidden_input) + + D_W2 = ( + np.random.normal( + size=(hidden_input, 1), scale=(1.0 / np.sqrt(hidden_input / 2.0)) + ) + * 0.002 + ) + # D_b2 = np.random.normal(size=(1),scale=(1. / np.sqrt(1 / 2.))) *0.002 + D_b2 = np.zeros(1) + + G_W1 = ( + np.random.normal( + size=(G_input, hidden_input), scale=(1.0 / np.sqrt(G_input / 2.0)) + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b1 = np.zeros(hidden_input) + + G_W2 = ( + np.random.normal( + size=(hidden_input, hidden_input2), + scale=(1.0 / np.sqrt(hidden_input / 2.0)), + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b2 = np.zeros(hidden_input2) + + G_W3 = ( + np.random.normal( + size=(hidden_input2, hidden_input3), + scale=(1.0 / np.sqrt(hidden_input2 / 2.0)), + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b3 = np.zeros(hidden_input3) + + G_W4 = ( + np.random.normal( + size=(hidden_input3, hidden_input4), + scale=(1.0 / np.sqrt(hidden_input3 / 2.0)), + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b4 = np.zeros(hidden_input4) + + G_W5 = ( + np.random.normal( + size=(hidden_input4, hidden_input5), + scale=(1.0 / np.sqrt(hidden_input4 / 2.0)), + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b5 = np.zeros(hidden_input5) + + G_W6 = ( + np.random.normal( + size=(hidden_input5, hidden_input6), + scale=(1.0 / np.sqrt(hidden_input5 / 2.0)), + ) + * 0.002 + ) + # G_b1 = np.random.normal(size=(128),scale=(1. / np.sqrt(128 / 2.))) *0.002 + G_b6 = np.zeros(hidden_input6) + + G_W7 = ( + np.random.normal( + size=(hidden_input6, 784), scale=(1.0 / np.sqrt(hidden_input6 / 2.0)) + ) + * 0.002 + ) + # G_b2 = np.random.normal(size=(784),scale=(1. / np.sqrt(784 / 2.))) *0.002 + G_b7 = np.zeros(784) + + # 3. For Adam Optimzier + v1, m1 = 0, 0 + v2, m2 = 0, 0 + v3, m3 = 0, 0 + v4, m4 = 0, 0 + + v5, m5 = 0, 0 + v6, m6 = 0, 0 + v7, m7 = 0, 0 + v8, m8 = 0, 0 + v9, m9 = 0, 0 + v10, m10 = 0, 0 + v11, m11 = 0, 0 + v12, m12 = 0, 0 + + v13, m13 = 0, 0 + v14, m14 = 0, 0 + + v15, m15 = 0, 0 + v16, m16 = 0, 0 + + v17, m17 = 0, 0 + v18, m18 = 0, 0 + + beta_1, beta_2, eps = 0.9, 0.999, 0.00000001 + + print("--------- Started Training ----------") + for iter in range(num_epoch): + + random_int = np.random.randint(len(images) - 5) + current_image = np.expand_dims(images[random_int], axis=0) + + # Func: Generate The first Fake Data + Z = np.random.uniform(-1.0, 1.0, size=[1, G_input]) + Gl1 = Z.dot(G_W1) + G_b1 + Gl1A = arctan(Gl1) + Gl2 = Gl1A.dot(G_W2) + G_b2 + Gl2A = ReLu(Gl2) + Gl3 = Gl2A.dot(G_W3) + G_b3 + Gl3A = arctan(Gl3) + + Gl4 = Gl3A.dot(G_W4) + G_b4 + Gl4A = ReLu(Gl4) + Gl5 = Gl4A.dot(G_W5) + G_b5 + Gl5A = tanh(Gl5) + Gl6 = Gl5A.dot(G_W6) + G_b6 + Gl6A = ReLu(Gl6) + Gl7 = Gl6A.dot(G_W7) + G_b7 + + current_fake_data = log(Gl7) + + # Func: Forward Feed for Real data + Dl1_r = current_image.dot(D_W1) + D_b1 + Dl1_rA = ReLu(Dl1_r) + Dl2_r = Dl1_rA.dot(D_W2) + D_b2 + Dl2_rA = log(Dl2_r) + + # Func: Forward Feed for Fake Data + Dl1_f = current_fake_data.dot(D_W1) + D_b1 + Dl1_fA = ReLu(Dl1_f) + Dl2_f = Dl1_fA.dot(D_W2) + D_b2 + Dl2_fA = log(Dl2_f) + + # Func: Cost D + D_cost = -np.log(Dl2_rA) + np.log(1.0 - Dl2_fA) + + # Func: Gradient + grad_f_w2_part_1 = 1 / (1.0 - Dl2_fA) + grad_f_w2_part_2 = d_log(Dl2_f) + grad_f_w2_part_3 = Dl1_fA + grad_f_w2 = grad_f_w2_part_3.T.dot(grad_f_w2_part_1 * grad_f_w2_part_2) + grad_f_b2 = grad_f_w2_part_1 * grad_f_w2_part_2 + + grad_f_w1_part_1 = (grad_f_w2_part_1 * grad_f_w2_part_2).dot(D_W2.T) + grad_f_w1_part_2 = d_ReLu(Dl1_f) + grad_f_w1_part_3 = current_fake_data + grad_f_w1 = grad_f_w1_part_3.T.dot(grad_f_w1_part_1 * grad_f_w1_part_2) + grad_f_b1 = grad_f_w1_part_1 * grad_f_w1_part_2 + + grad_r_w2_part_1 = -1 / Dl2_rA + grad_r_w2_part_2 = d_log(Dl2_r) + grad_r_w2_part_3 = Dl1_rA + grad_r_w2 = grad_r_w2_part_3.T.dot(grad_r_w2_part_1 * grad_r_w2_part_2) + grad_r_b2 = grad_r_w2_part_1 * grad_r_w2_part_2 + + grad_r_w1_part_1 = (grad_r_w2_part_1 * grad_r_w2_part_2).dot(D_W2.T) + grad_r_w1_part_2 = d_ReLu(Dl1_r) + grad_r_w1_part_3 = current_image + grad_r_w1 = grad_r_w1_part_3.T.dot(grad_r_w1_part_1 * grad_r_w1_part_2) + grad_r_b1 = grad_r_w1_part_1 * grad_r_w1_part_2 + + grad_w1 = grad_f_w1 + grad_r_w1 + grad_b1 = grad_f_b1 + grad_r_b1 + + grad_w2 = grad_f_w2 + grad_r_w2 + grad_b2 = grad_f_b2 + grad_r_b2 + + # ---- Update Gradient ---- + m1 = beta_1 * m1 + (1 - beta_1) * grad_w1 + v1 = beta_2 * v1 + (1 - beta_2) * grad_w1 ** 2 + + m2 = beta_1 * m2 + (1 - beta_1) * grad_b1 + v2 = beta_2 * v2 + (1 - beta_2) * grad_b1 ** 2 + + m3 = beta_1 * m3 + (1 - beta_1) * grad_w2 + v3 = beta_2 * v3 + (1 - beta_2) * grad_w2 ** 2 + + m4 = beta_1 * m4 + (1 - beta_1) * grad_b2 + v4 = beta_2 * v4 + (1 - beta_2) * grad_b2 ** 2 + + D_W1 = D_W1 - (learing_rate / (np.sqrt(v1 / (1 - beta_2)) + eps)) * ( + m1 / (1 - beta_1) + ) + D_b1 = D_b1 - (learing_rate / (np.sqrt(v2 / (1 - beta_2)) + eps)) * ( + m2 / (1 - beta_1) + ) + + D_W2 = D_W2 - (learing_rate / (np.sqrt(v3 / (1 - beta_2)) + eps)) * ( + m3 / (1 - beta_1) + ) + D_b2 = D_b2 - (learing_rate / (np.sqrt(v4 / (1 - beta_2)) + eps)) * ( + m4 / (1 - beta_1) + ) + + # Func: Forward Feed for G + Z = np.random.uniform(-1.0, 1.0, size=[1, G_input]) + Gl1 = Z.dot(G_W1) + G_b1 + Gl1A = arctan(Gl1) + Gl2 = Gl1A.dot(G_W2) + G_b2 + Gl2A = ReLu(Gl2) + Gl3 = Gl2A.dot(G_W3) + G_b3 + Gl3A = arctan(Gl3) + + Gl4 = Gl3A.dot(G_W4) + G_b4 + Gl4A = ReLu(Gl4) + Gl5 = Gl4A.dot(G_W5) + G_b5 + Gl5A = tanh(Gl5) + Gl6 = Gl5A.dot(G_W6) + G_b6 + Gl6A = ReLu(Gl6) + Gl7 = Gl6A.dot(G_W7) + G_b7 + + current_fake_data = log(Gl7) + + Dl1 = current_fake_data.dot(D_W1) + D_b1 + Dl1_A = ReLu(Dl1) + Dl2 = Dl1_A.dot(D_W2) + D_b2 + Dl2_A = log(Dl2) + + # Func: Cost G + G_cost = -np.log(Dl2_A) + + # Func: Gradient + grad_G_w7_part_1 = ((-1 / Dl2_A) * d_log(Dl2).dot(D_W2.T) * (d_ReLu(Dl1))).dot( + D_W1.T + ) + grad_G_w7_part_2 = d_log(Gl7) + grad_G_w7_part_3 = Gl6A + grad_G_w7 = grad_G_w7_part_3.T.dot(grad_G_w7_part_1 * grad_G_w7_part_1) + grad_G_b7 = grad_G_w7_part_1 * grad_G_w7_part_2 + + grad_G_w6_part_1 = (grad_G_w7_part_1 * grad_G_w7_part_2).dot(G_W7.T) + grad_G_w6_part_2 = d_ReLu(Gl6) + grad_G_w6_part_3 = Gl5A + grad_G_w6 = grad_G_w6_part_3.T.dot(grad_G_w6_part_1 * grad_G_w6_part_2) + grad_G_b6 = grad_G_w6_part_1 * grad_G_w6_part_2 + + grad_G_w5_part_1 = (grad_G_w6_part_1 * grad_G_w6_part_2).dot(G_W6.T) + grad_G_w5_part_2 = d_tanh(Gl5) + grad_G_w5_part_3 = Gl4A + grad_G_w5 = grad_G_w5_part_3.T.dot(grad_G_w5_part_1 * grad_G_w5_part_2) + grad_G_b5 = grad_G_w5_part_1 * grad_G_w5_part_2 + + grad_G_w4_part_1 = (grad_G_w5_part_1 * grad_G_w5_part_2).dot(G_W5.T) + grad_G_w4_part_2 = d_ReLu(Gl4) + grad_G_w4_part_3 = Gl3A + grad_G_w4 = grad_G_w4_part_3.T.dot(grad_G_w4_part_1 * grad_G_w4_part_2) + grad_G_b4 = grad_G_w4_part_1 * grad_G_w4_part_2 + + grad_G_w3_part_1 = (grad_G_w4_part_1 * grad_G_w4_part_2).dot(G_W4.T) + grad_G_w3_part_2 = d_arctan(Gl3) + grad_G_w3_part_3 = Gl2A + grad_G_w3 = grad_G_w3_part_3.T.dot(grad_G_w3_part_1 * grad_G_w3_part_2) + grad_G_b3 = grad_G_w3_part_1 * grad_G_w3_part_2 + + grad_G_w2_part_1 = (grad_G_w3_part_1 * grad_G_w3_part_2).dot(G_W3.T) + grad_G_w2_part_2 = d_ReLu(Gl2) + grad_G_w2_part_3 = Gl1A + grad_G_w2 = grad_G_w2_part_3.T.dot(grad_G_w2_part_1 * grad_G_w2_part_2) + grad_G_b2 = grad_G_w2_part_1 * grad_G_w2_part_2 + + grad_G_w1_part_1 = (grad_G_w2_part_1 * grad_G_w2_part_2).dot(G_W2.T) + grad_G_w1_part_2 = d_arctan(Gl1) + grad_G_w1_part_3 = Z + grad_G_w1 = grad_G_w1_part_3.T.dot(grad_G_w1_part_1 * grad_G_w1_part_2) + grad_G_b1 = grad_G_w1_part_1 * grad_G_w1_part_2 + + # ---- Update Gradient ---- + m5 = beta_1 * m5 + (1 - beta_1) * grad_G_w1 + v5 = beta_2 * v5 + (1 - beta_2) * grad_G_w1 ** 2 + + m6 = beta_1 * m6 + (1 - beta_1) * grad_G_b1 + v6 = beta_2 * v6 + (1 - beta_2) * grad_G_b1 ** 2 + + m7 = beta_1 * m7 + (1 - beta_1) * grad_G_w2 + v7 = beta_2 * v7 + (1 - beta_2) * grad_G_w2 ** 2 + + m8 = beta_1 * m8 + (1 - beta_1) * grad_G_b2 + v8 = beta_2 * v8 + (1 - beta_2) * grad_G_b2 ** 2 + + m9 = beta_1 * m9 + (1 - beta_1) * grad_G_w3 + v9 = beta_2 * v9 + (1 - beta_2) * grad_G_w3 ** 2 + + m10 = beta_1 * m10 + (1 - beta_1) * grad_G_b3 + v10 = beta_2 * v10 + (1 - beta_2) * grad_G_b3 ** 2 + + m11 = beta_1 * m11 + (1 - beta_1) * grad_G_w4 + v11 = beta_2 * v11 + (1 - beta_2) * grad_G_w4 ** 2 + + m12 = beta_1 * m12 + (1 - beta_1) * grad_G_b4 + v12 = beta_2 * v12 + (1 - beta_2) * grad_G_b4 ** 2 + + m13 = beta_1 * m13 + (1 - beta_1) * grad_G_w5 + v13 = beta_2 * v13 + (1 - beta_2) * grad_G_w5 ** 2 + + m14 = beta_1 * m14 + (1 - beta_1) * grad_G_b5 + v14 = beta_2 * v14 + (1 - beta_2) * grad_G_b5 ** 2 + + m15 = beta_1 * m15 + (1 - beta_1) * grad_G_w6 + v15 = beta_2 * v15 + (1 - beta_2) * grad_G_w6 ** 2 + + m16 = beta_1 * m16 + (1 - beta_1) * grad_G_b6 + v16 = beta_2 * v16 + (1 - beta_2) * grad_G_b6 ** 2 + + m17 = beta_1 * m17 + (1 - beta_1) * grad_G_w7 + v17 = beta_2 * v17 + (1 - beta_2) * grad_G_w7 ** 2 + + m18 = beta_1 * m18 + (1 - beta_1) * grad_G_b7 + v18 = beta_2 * v18 + (1 - beta_2) * grad_G_b7 ** 2 + + G_W1 = G_W1 - (learing_rate / (np.sqrt(v5 / (1 - beta_2)) + eps)) * ( + m5 / (1 - beta_1) + ) + G_b1 = G_b1 - (learing_rate / (np.sqrt(v6 / (1 - beta_2)) + eps)) * ( + m6 / (1 - beta_1) + ) + + G_W2 = G_W2 - (learing_rate / (np.sqrt(v7 / (1 - beta_2)) + eps)) * ( + m7 / (1 - beta_1) + ) + G_b2 = G_b2 - (learing_rate / (np.sqrt(v8 / (1 - beta_2)) + eps)) * ( + m8 / (1 - beta_1) + ) + + G_W3 = G_W3 - (learing_rate / (np.sqrt(v9 / (1 - beta_2)) + eps)) * ( + m9 / (1 - beta_1) + ) + G_b3 = G_b3 - (learing_rate / (np.sqrt(v10 / (1 - beta_2)) + eps)) * ( + m10 / (1 - beta_1) + ) + + G_W4 = G_W4 - (learing_rate / (np.sqrt(v11 / (1 - beta_2)) + eps)) * ( + m11 / (1 - beta_1) + ) + G_b4 = G_b4 - (learing_rate / (np.sqrt(v12 / (1 - beta_2)) + eps)) * ( + m12 / (1 - beta_1) + ) + + G_W5 = G_W5 - (learing_rate / (np.sqrt(v13 / (1 - beta_2)) + eps)) * ( + m13 / (1 - beta_1) + ) + G_b5 = G_b5 - (learing_rate / (np.sqrt(v14 / (1 - beta_2)) + eps)) * ( + m14 / (1 - beta_1) + ) + + G_W6 = G_W6 - (learing_rate / (np.sqrt(v15 / (1 - beta_2)) + eps)) * ( + m15 / (1 - beta_1) + ) + G_b6 = G_b6 - (learing_rate / (np.sqrt(v16 / (1 - beta_2)) + eps)) * ( + m16 / (1 - beta_1) + ) + + G_W7 = G_W7 - (learing_rate / (np.sqrt(v17 / (1 - beta_2)) + eps)) * ( + m17 / (1 - beta_1) + ) + G_b7 = G_b7 - (learing_rate / (np.sqrt(v18 / (1 - beta_2)) + eps)) * ( + m18 / (1 - beta_1) + ) + + # --- Print Error ---- + # print("Current Iter: ",iter, " Current D cost:",D_cost, " Current G cost: ", G_cost,end='\r') + + if iter == 0: + learing_rate = learing_rate * 0.01 + if iter == 40: + learing_rate = learing_rate * 0.01 + + # ---- Print to Out put ---- + if iter % 10 == 0: + + print( + "Current Iter: ", + iter, + " Current D cost:", + D_cost, + " Current G cost: ", + G_cost, + end="\r", + ) + print("--------- Show Example Result See Tab Above ----------") + print("--------- Wait for the image to load ---------") + Z = np.random.uniform(-1.0, 1.0, size=[16, G_input]) + + Gl1 = Z.dot(G_W1) + G_b1 + Gl1A = arctan(Gl1) + Gl2 = Gl1A.dot(G_W2) + G_b2 + Gl2A = ReLu(Gl2) + Gl3 = Gl2A.dot(G_W3) + G_b3 + Gl3A = arctan(Gl3) + + Gl4 = Gl3A.dot(G_W4) + G_b4 + Gl4A = ReLu(Gl4) + Gl5 = Gl4A.dot(G_W5) + G_b5 + Gl5A = tanh(Gl5) + Gl6 = Gl5A.dot(G_W6) + G_b6 + Gl6A = ReLu(Gl6) + Gl7 = Gl6A.dot(G_W7) + G_b7 + + current_fake_data = log(Gl7) + + fig = plot(current_fake_data) + fig.savefig( + "Click_Me_{}.png".format( + str(iter).zfill(3) + + "_Ginput_" + + str(G_input) + + "_hiddenone" + + str(hidden_input) + + "_hiddentwo" + + str(hidden_input2) + + "_LR_" + + str(learing_rate) + ), + bbox_inches="tight", + ) + # for complete explanation visit https://towardsdatascience.com/only-numpy-implementing-gan-general-adversarial-networks-and-adam-optimizer-using-numpy-with-2a7e4e032021 + # -- end code -- diff --git a/neural_network/input_data.py_tf b/neural_network/input_data.py_tf new file mode 100644 index 000000000000..0e22ac0bcda5 --- /dev/null +++ b/neural_network/input_data.py_tf @@ -0,0 +1,345 @@ +# Copyright 2016 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Functions for downloading and reading MNIST data (deprecated). + +This module and all its submodules are deprecated. +""" + + +import collections +import gzip +import os + +import numpy +from six.moves import urllib +from six.moves import xrange # pylint: disable=redefined-builtin + +from tensorflow.python.framework import dtypes +from tensorflow.python.framework import random_seed +from tensorflow.python.platform import gfile +from tensorflow.python.util.deprecation import deprecated + +_Datasets = collections.namedtuple("_Datasets", ["train", "validation", "test"]) + +# CVDF mirror of http://yann.lecun.com/exdb/mnist/ +DEFAULT_SOURCE_URL = "https://storage.googleapis.com/cvdf-datasets/mnist/" + + +def _read32(bytestream): + dt = numpy.dtype(numpy.uint32).newbyteorder(">") + return numpy.frombuffer(bytestream.read(4), dtype=dt)[0] + + +@deprecated(None, "Please use tf.data to implement this functionality.") +def _extract_images(f): + """Extract the images into a 4D uint8 numpy array [index, y, x, depth]. + + Args: + f: A file object that can be passed into a gzip reader. + + Returns: + data: A 4D uint8 numpy array [index, y, x, depth]. + + Raises: + ValueError: If the bytestream does not start with 2051. + + """ + print("Extracting", f.name) + with gzip.GzipFile(fileobj=f) as bytestream: + magic = _read32(bytestream) + if magic != 2051: + raise ValueError( + "Invalid magic number %d in MNIST image file: %s" % (magic, f.name) + ) + num_images = _read32(bytestream) + rows = _read32(bytestream) + cols = _read32(bytestream) + buf = bytestream.read(rows * cols * num_images) + data = numpy.frombuffer(buf, dtype=numpy.uint8) + data = data.reshape(num_images, rows, cols, 1) + return data + + +@deprecated(None, "Please use tf.one_hot on tensors.") +def _dense_to_one_hot(labels_dense, num_classes): + """Convert class labels from scalars to one-hot vectors.""" + num_labels = labels_dense.shape[0] + index_offset = numpy.arange(num_labels) * num_classes + labels_one_hot = numpy.zeros((num_labels, num_classes)) + labels_one_hot.flat[index_offset + labels_dense.ravel()] = 1 + return labels_one_hot + + +@deprecated(None, "Please use tf.data to implement this functionality.") +def _extract_labels(f, one_hot=False, num_classes=10): + """Extract the labels into a 1D uint8 numpy array [index]. + + Args: + f: A file object that can be passed into a gzip reader. + one_hot: Does one hot encoding for the result. + num_classes: Number of classes for the one hot encoding. + + Returns: + labels: a 1D uint8 numpy array. + + Raises: + ValueError: If the bystream doesn't start with 2049. + """ + print("Extracting", f.name) + with gzip.GzipFile(fileobj=f) as bytestream: + magic = _read32(bytestream) + if magic != 2049: + raise ValueError( + "Invalid magic number %d in MNIST label file: %s" % (magic, f.name) + ) + num_items = _read32(bytestream) + buf = bytestream.read(num_items) + labels = numpy.frombuffer(buf, dtype=numpy.uint8) + if one_hot: + return _dense_to_one_hot(labels, num_classes) + return labels + + +class _DataSet: + """Container class for a _DataSet (deprecated). + + THIS CLASS IS DEPRECATED. + """ + + @deprecated( + None, + "Please use alternatives such as official/mnist/_DataSet.py" + " from tensorflow/models.", + ) + def __init__( + self, + images, + labels, + fake_data=False, + one_hot=False, + dtype=dtypes.float32, + reshape=True, + seed=None, + ): + """Construct a _DataSet. + + one_hot arg is used only if fake_data is true. `dtype` can be either + `uint8` to leave the input as `[0, 255]`, or `float32` to rescale into + `[0, 1]`. Seed arg provides for convenient deterministic testing. + + Args: + images: The images + labels: The labels + fake_data: Ignore inages and labels, use fake data. + one_hot: Bool, return the labels as one hot vectors (if True) or ints (if + False). + dtype: Output image dtype. One of [uint8, float32]. `uint8` output has + range [0,255]. float32 output has range [0,1]. + reshape: Bool. If True returned images are returned flattened to vectors. + seed: The random seed to use. + """ + seed1, seed2 = random_seed.get_seed(seed) + # If op level seed is not set, use whatever graph level seed is returned + numpy.random.seed(seed1 if seed is None else seed2) + dtype = dtypes.as_dtype(dtype).base_dtype + if dtype not in (dtypes.uint8, dtypes.float32): + raise TypeError("Invalid image dtype %r, expected uint8 or float32" % dtype) + if fake_data: + self._num_examples = 10000 + self.one_hot = one_hot + else: + assert ( + images.shape[0] == labels.shape[0] + ), f"images.shape: {images.shape} labels.shape: {labels.shape}" + self._num_examples = images.shape[0] + + # Convert shape from [num examples, rows, columns, depth] + # to [num examples, rows*columns] (assuming depth == 1) + if reshape: + assert images.shape[3] == 1 + images = images.reshape( + images.shape[0], images.shape[1] * images.shape[2] + ) + if dtype == dtypes.float32: + # Convert from [0, 255] -> [0.0, 1.0]. + images = images.astype(numpy.float32) + images = numpy.multiply(images, 1.0 / 255.0) + self._images = images + self._labels = labels + self._epochs_completed = 0 + self._index_in_epoch = 0 + + @property + def images(self): + return self._images + + @property + def labels(self): + return self._labels + + @property + def num_examples(self): + return self._num_examples + + @property + def epochs_completed(self): + return self._epochs_completed + + def next_batch(self, batch_size, fake_data=False, shuffle=True): + """Return the next `batch_size` examples from this data set.""" + if fake_data: + fake_image = [1] * 784 + if self.one_hot: + fake_label = [1] + [0] * 9 + else: + fake_label = 0 + return ( + [fake_image for _ in xrange(batch_size)], + [fake_label for _ in xrange(batch_size)], + ) + start = self._index_in_epoch + # Shuffle for the first epoch + if self._epochs_completed == 0 and start == 0 and shuffle: + perm0 = numpy.arange(self._num_examples) + numpy.random.shuffle(perm0) + self._images = self.images[perm0] + self._labels = self.labels[perm0] + # Go to the next epoch + if start + batch_size > self._num_examples: + # Finished epoch + self._epochs_completed += 1 + # Get the rest examples in this epoch + rest_num_examples = self._num_examples - start + images_rest_part = self._images[start : self._num_examples] + labels_rest_part = self._labels[start : self._num_examples] + # Shuffle the data + if shuffle: + perm = numpy.arange(self._num_examples) + numpy.random.shuffle(perm) + self._images = self.images[perm] + self._labels = self.labels[perm] + # Start next epoch + start = 0 + self._index_in_epoch = batch_size - rest_num_examples + end = self._index_in_epoch + images_new_part = self._images[start:end] + labels_new_part = self._labels[start:end] + return ( + numpy.concatenate((images_rest_part, images_new_part), axis=0), + numpy.concatenate((labels_rest_part, labels_new_part), axis=0), + ) + else: + self._index_in_epoch += batch_size + end = self._index_in_epoch + return self._images[start:end], self._labels[start:end] + + +@deprecated(None, "Please write your own downloading logic.") +def _maybe_download(filename, work_directory, source_url): + """Download the data from source url, unless it's already here. + + Args: + filename: string, name of the file in the directory. + work_directory: string, path to working directory. + source_url: url to download from if file doesn't exist. + + Returns: + Path to resulting file. + """ + if not gfile.Exists(work_directory): + gfile.MakeDirs(work_directory) + filepath = os.path.join(work_directory, filename) + if not gfile.Exists(filepath): + urllib.request.urlretrieve(source_url, filepath) + with gfile.GFile(filepath) as f: + size = f.size() + print("Successfully downloaded", filename, size, "bytes.") + return filepath + + +@deprecated( + None, "Please use alternatives such as:" " tensorflow_datasets.load('mnist')" +) +def read_data_sets( + train_dir, + fake_data=False, + one_hot=False, + dtype=dtypes.float32, + reshape=True, + validation_size=5000, + seed=None, + source_url=DEFAULT_SOURCE_URL, +): + if fake_data: + + def fake(): + return _DataSet( + [], [], fake_data=True, one_hot=one_hot, dtype=dtype, seed=seed + ) + + train = fake() + validation = fake() + test = fake() + return _Datasets(train=train, validation=validation, test=test) + + if not source_url: # empty string check + source_url = DEFAULT_SOURCE_URL + + train_images_file = "train-images-idx3-ubyte.gz" + train_labels_file = "train-labels-idx1-ubyte.gz" + test_images_file = "t10k-images-idx3-ubyte.gz" + test_labels_file = "t10k-labels-idx1-ubyte.gz" + + local_file = _maybe_download( + train_images_file, train_dir, source_url + train_images_file + ) + with gfile.Open(local_file, "rb") as f: + train_images = _extract_images(f) + + local_file = _maybe_download( + train_labels_file, train_dir, source_url + train_labels_file + ) + with gfile.Open(local_file, "rb") as f: + train_labels = _extract_labels(f, one_hot=one_hot) + + local_file = _maybe_download( + test_images_file, train_dir, source_url + test_images_file + ) + with gfile.Open(local_file, "rb") as f: + test_images = _extract_images(f) + + local_file = _maybe_download( + test_labels_file, train_dir, source_url + test_labels_file + ) + with gfile.Open(local_file, "rb") as f: + test_labels = _extract_labels(f, one_hot=one_hot) + + if not 0 <= validation_size <= len(train_images): + raise ValueError( + f"Validation size should be between 0 and {len(train_images)}. Received: {validation_size}." + ) + + validation_images = train_images[:validation_size] + validation_labels = train_labels[:validation_size] + train_images = train_images[validation_size:] + train_labels = train_labels[validation_size:] + + options = dict(dtype=dtype, reshape=reshape, seed=seed) + + train = _DataSet(train_images, train_labels, **options) + validation = _DataSet(validation_images, validation_labels, **options) + test = _DataSet(test_images, test_labels, **options) + + return _Datasets(train=train, validation=validation, test=test) diff --git a/neural_network/perceptron.py b/neural_network/perceptron.py index 787ea8f73bf1..23b409b227c4 100644 --- a/neural_network/perceptron.py +++ b/neural_network/perceptron.py @@ -1,64 +1,115 @@ -''' - +""" Perceptron w = w + N * (d(k) - y) * x(k) - Using perceptron network for oil analysis, - with Measuring of 3 parameters that represent chemical characteristics we can classify the oil, in p1 or p2 + Using perceptron network for oil analysis, with Measuring of 3 parameters + that represent chemical characteristics we can classify the oil, in p1 or p2 p1 = -1 p2 = 1 - -''' -from __future__ import print_function - +""" import random class Perceptron: - def __init__(self, sample, exit, learn_rate=0.01, epoch_number=1000, bias=-1): + def __init__(self, sample, target, learning_rate=0.01, epoch_number=1000, bias=-1): + """ + Initializes a Perceptron network for oil analysis + :param sample: sample dataset of 3 parameters with shape [30,3] + :param target: variable for classification with two possible states -1 or 1 + :param learning_rate: learning rate used in optimizing. + :param epoch_number: number of epochs to train network on. + :param bias: bias value for the network. + + >>> p = Perceptron([], (0, 1, 2)) + Traceback (most recent call last): + ... + ValueError: Sample data can not be empty + >>> p = Perceptron(([0], 1, 2), []) + Traceback (most recent call last): + ... + ValueError: Target data can not be empty + >>> p = Perceptron(([0], 1, 2), (0, 1)) + Traceback (most recent call last): + ... + ValueError: Sample data and Target data do not have matching lengths + """ self.sample = sample - self.exit = exit - self.learn_rate = learn_rate + if len(self.sample) == 0: + raise ValueError("Sample data can not be empty") + self.target = target + if len(self.target) == 0: + raise ValueError("Target data can not be empty") + if len(self.sample) != len(self.target): + raise ValueError("Sample data and Target data do not have matching lengths") + self.learning_rate = learning_rate self.epoch_number = epoch_number self.bias = bias self.number_sample = len(sample) - self.col_sample = len(sample[0]) + self.col_sample = len(sample[0]) # number of columns in dataset self.weight = [] - def training(self): + def training(self) -> None: + """ + Trains perceptron for epochs <= given number of epochs + :return: None + >>> data = [[2.0149, 0.6192, 10.9263]] + >>> targets = [-1] + >>> perceptron = Perceptron(data,targets) + >>> perceptron.training() # doctest: +ELLIPSIS + ('\\nEpoch:\\n', ...) + ... + """ for sample in self.sample: sample.insert(0, self.bias) for i in range(self.col_sample): - self.weight.append(random.random()) + self.weight.append(random.random()) self.weight.insert(0, self.bias) epoch_count = 0 while True: - erro = False + has_misclassified = False for i in range(self.number_sample): u = 0 for j in range(self.col_sample + 1): u = u + self.weight[j] * self.sample[i][j] y = self.sign(u) - if y != self.exit[i]: - + if y != self.target[i]: for j in range(self.col_sample + 1): - - self.weight[j] = self.weight[j] + self.learn_rate * (self.exit[i] - y) * self.sample[i][j] - erro = True - #print('Epoch: \n',epoch_count) + self.weight[j] = ( + self.weight[j] + + self.learning_rate + * (self.target[i] - y) + * self.sample[i][j] + ) + has_misclassified = True + # print('Epoch: \n',epoch_count) epoch_count = epoch_count + 1 - # if you want controle the epoch or just by erro - if erro == False: - print(('\nEpoch:\n',epoch_count)) - print('------------------------\n') - #if epoch_count > self.epoch_number or not erro: + # if you want control the epoch or just by error + if not has_misclassified: + print(("\nEpoch:\n", epoch_count)) + print("------------------------\n") + # if epoch_count > self.epoch_number or not error: break - def sort(self, sample): + def sort(self, sample) -> None: + """ + :param sample: example row to classify as P1 or P2 + :return: None + >>> data = [[2.0149, 0.6192, 10.9263]] + >>> targets = [-1] + >>> perceptron = Perceptron(data,targets) + >>> perceptron.training() # doctest: +ELLIPSIS + ('\\nEpoch:\\n', ...) + ... + >>> perceptron.sort([-0.6508, 0.1097, 4.0009]) # doctest: +ELLIPSIS + ('Sample: ', ...) + classification: P... + """ + if len(self.sample) == 0: + raise ValueError("Sample data can not be empty") sample.insert(0, self.bias) u = 0 for i in range(self.col_sample + 1): @@ -66,14 +117,28 @@ def sort(self, sample): y = self.sign(u) - if y == -1: - print(('Sample: ', sample)) - print('classification: P1') + if y == -1: + print(("Sample: ", sample)) + print("classification: P1") else: - print(('Sample: ', sample)) - print('classification: P2') - - def sign(self, u): + print(("Sample: ", sample)) + print("classification: P2") + + def sign(self, u: float) -> int: + """ + threshold function for classification + :param u: input number + :return: 1 if the input is greater than 0, otherwise -1 + >>> data = [[0],[-0.5],[0.5]] + >>> targets = [1,-1,1] + >>> perceptron = Perceptron(data,targets) + >>> perceptron.sign(0) + 1 + >>> perceptron.sign(-0.5) + -1 + >>> perceptron.sign(0.5) + 1 + """ return 1 if u >= 0 else -1 @@ -107,19 +172,60 @@ def sign(self, u): [-0.1013, 0.5989, 7.1812], [2.4482, 0.9455, 11.2095], [2.0149, 0.6192, 10.9263], - [0.2012, 0.2611, 5.4631] + [0.2012, 0.2611, 5.4631], +] +exit = [ + -1, + -1, + -1, + 1, + 1, + -1, + 1, + -1, + 1, + 1, + -1, + 1, + -1, + -1, + -1, + -1, + 1, + 1, + 1, + 1, + -1, + 1, + 1, + 1, + 1, + -1, + -1, + 1, + -1, + 1, ] -exit = [-1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, -1, 1] -if __name__ == '__main__': - network = Perceptron(sample=samples, exit = exit, learn_rate=0.01, epoch_number=1000, bias=-1) +if __name__ == "__main__": + import doctest - network.training() + doctest.testmod() + network = Perceptron( + sample=samples, target=exit, learning_rate=0.01, epoch_number=1000, bias=-1 + ) + network.training() + print("Finished training perceptron") + print("Enter values to predict or q to exit") while True: sample = [] - for i in range(3): - sample.insert(i, float(input('value: ').strip())) + for i in range(len(samples[0])): + observation = input("value: ").strip() + if observation == "q": + break + observation = float(observation) + sample.insert(i, observation) network.sort(sample) diff --git a/optimization/requirements.txt b/optimization/requirements.txt new file mode 100644 index 000000000000..4b43f7e68658 --- /dev/null +++ b/optimization/requirements.txt @@ -0,0 +1 @@ +matplotlib \ No newline at end of file diff --git a/other/Food wastage analysis from 1961-2013 (FAO).ipynb b/other/Food wastage analysis from 1961-2013 (FAO).ipynb deleted file mode 100644 index 384314c7e8f1..000000000000 --- a/other/Food wastage analysis from 1961-2013 (FAO).ipynb +++ /dev/null @@ -1,5916 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "1eecdb4a-89ca-4a1e-9c4c-7c44b2e628a1", - "_uuid": "110a8132a8179a9bed2fc8f1096592dc791f1661" - }, - "source": [ - "# About the dataset\n", - "\n", - "Context\n", - "Our world population is expected to grow from 7.3 billion today to 9.7 billion in the year 2050. Finding solutions for feeding the growing world population has become a hot topic for food and agriculture organizations, entrepreneurs and philanthropists. These solutions range from changing the way we grow our food to changing the way we eat. To make things harder, the world's climate is changing and it is both affecting and affected by the way we grow our food – agriculture. This dataset provides an insight on our worldwide food production - focusing on a comparison between food produced for human consumption and feed produced for animals.\n", - "\n", - "Content\n", - "The Food and Agriculture Organization of the United Nations provides free access to food and agriculture data for over 245 countries and territories, from the year 1961 to the most recent update (depends on the dataset). One dataset from the FAO's database is the Food Balance Sheets. It presents a comprehensive picture of the pattern of a country's food supply during a specified reference period, the last time an update was loaded to the FAO database was in 2013. The food balance sheet shows for each food item the sources of supply and its utilization. This chunk of the dataset is focused on two utilizations of each food item available:\n", - "\n", - "Food - refers to the total amount of the food item available as human food during the reference period.\n", - "Feed - refers to the quantity of the food item available for feeding to the livestock and poultry during the reference period.\n", - "Dataset's attributes:\n", - "\n", - "Area code - Country name abbreviation\n", - "Area - County name\n", - "Item - Food item\n", - "Element - Food or Feed\n", - "Latitude - geographic coordinate that specifies the north–south position of a point on the Earth's surface\n", - "Longitude - geographic coordinate that specifies the east-west position of a point on the Earth's surface\n", - "Production per year - Amount of food item produced in 1000 tonnes\n", - "\n", - "This is a simple exploratory notebook that heavily expolits pandas and seaborn" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19", - "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5" - }, - "outputs": [], - "source": [ - "# Importing libraries\n", - "import numpy as np\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "%matplotlib inline\n", - "# importing data\n", - "df = pd.read_csv(\"FAO.csv\", encoding = \"ISO-8859-1\")\n", - "pd.options.mode.chained_assignment = None\n", - "from sklearn.linear_model import LinearRegression" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Area AbbreviationArea CodeAreaItem CodeItemElement CodeElementUnitlatitudelongitude...Y2004Y2005Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013
0AFG2Afghanistan2511Wheat and products5142Food1000 tonnes33.9467.71...3249.03486.03704.04164.04252.04538.04605.04711.048104895
1AFG2Afghanistan2805Rice (Milled Equivalent)5142Food1000 tonnes33.9467.71...419.0445.0546.0455.0490.0415.0442.0476.0425422
2AFG2Afghanistan2513Barley and products5521Feed1000 tonnes33.9467.71...58.0236.0262.0263.0230.0379.0315.0203.0367360
3AFG2Afghanistan2513Barley and products5142Food1000 tonnes33.9467.71...185.043.044.048.062.055.060.072.07889
4AFG2Afghanistan2514Maize and products5521Feed1000 tonnes33.9467.71...120.0208.0233.0249.0247.0195.0178.0191.0200200
5AFG2Afghanistan2514Maize and products5142Food1000 tonnes33.9467.71...231.067.082.067.069.071.082.073.07776
6AFG2Afghanistan2517Millet and products5142Food1000 tonnes33.9467.71...15.021.011.019.021.018.014.014.01412
7AFG2Afghanistan2520Cereals, Other5142Food1000 tonnes33.9467.71...2.01.01.00.00.00.00.00.000
8AFG2Afghanistan2531Potatoes and products5142Food1000 tonnes33.9467.71...276.0294.0294.0260.0242.0250.0192.0169.0196230
9AFG2Afghanistan2536Sugar cane5521Feed1000 tonnes33.9467.71...50.029.061.065.054.0114.083.083.06981
10AFG2Afghanistan2537Sugar beet5521Feed1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
11AFG2Afghanistan2542Sugar (Raw Equivalent)5142Food1000 tonnes33.9467.71...124.0152.0169.0192.0217.0231.0240.0240.0250255
12AFG2Afghanistan2543Sweeteners, Other5142Food1000 tonnes33.9467.71...9.015.012.06.011.02.09.021.02416
13AFG2Afghanistan2745Honey5142Food1000 tonnes33.9467.71...3.03.03.03.03.03.03.02.022
14AFG2Afghanistan2549Pulses, Other and products5521Feed1000 tonnes33.9467.71...3.02.03.03.03.05.04.05.044
15AFG2Afghanistan2549Pulses, Other and products5142Food1000 tonnes33.9467.71...17.035.037.040.054.080.066.081.06374
16AFG2Afghanistan2551Nuts and products5142Food1000 tonnes33.9467.71...11.013.024.034.042.028.066.071.07044
17AFG2Afghanistan2560Coconuts - Incl Copra5142Food1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
18AFG2Afghanistan2561Sesame seed5142Food1000 tonnes33.9467.71...16.016.013.016.016.016.019.017.01616
19AFG2Afghanistan2563Olives (including preserved)5142Food1000 tonnes33.9467.71...1.01.00.00.02.03.02.02.022
20AFG2Afghanistan2571Soyabean Oil5142Food1000 tonnes33.9467.71...6.035.018.021.011.06.015.016.01616
21AFG2Afghanistan2572Groundnut Oil5142Food1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
22AFG2Afghanistan2573Sunflowerseed Oil5142Food1000 tonnes33.9467.71...4.06.05.09.03.08.015.016.01723
23AFG2Afghanistan2574Rape and Mustard Oil5142Food1000 tonnes33.9467.71...0.01.03.05.06.06.01.02.022
24AFG2Afghanistan2575Cottonseed Oil5142Food1000 tonnes33.9467.71...2.03.03.03.03.04.03.03.034
25AFG2Afghanistan2577Palm Oil5142Food1000 tonnes33.9467.71...71.069.056.051.036.053.059.051.06164
26AFG2Afghanistan2579Sesameseed Oil5142Food1000 tonnes33.9467.71...1.01.01.02.02.01.01.02.011
27AFG2Afghanistan2580Olive Oil5142Food1000 tonnes33.9467.71...0.00.00.00.00.01.01.01.011
28AFG2Afghanistan2586Oilcrops Oil, Other5142Food1000 tonnes33.9467.71...0.01.00.00.03.01.02.02.022
29AFG2Afghanistan2601Tomatoes and products5142Food1000 tonnes33.9467.71...2.02.08.01.00.00.00.00.000
..................................................................
21447ZWE181Zimbabwe2765Crustaceans5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21448ZWE181Zimbabwe2766Cephalopods5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21449ZWE181Zimbabwe2767Molluscs, Other5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.01.00.000
21450ZWE181Zimbabwe2775Aquatic Plants5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21451ZWE181Zimbabwe2680Infant food5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21452ZWE181Zimbabwe2905Cereals - Excluding Beer5521Feed1000 tonnes-19.0229.15...75.054.075.055.063.062.055.055.05555
21453ZWE181Zimbabwe2905Cereals - Excluding Beer5142Food1000 tonnes-19.0229.15...1844.01842.01944.01962.01918.01980.02011.02094.020712016
21454ZWE181Zimbabwe2907Starchy Roots5142Food1000 tonnes-19.0229.15...223.0236.0238.0228.0245.0258.0258.0269.0272276
21455ZWE181Zimbabwe2908Sugar Crops5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21456ZWE181Zimbabwe2909Sugar & Sweeteners5142Food1000 tonnes-19.0229.15...335.0313.0339.0302.0285.0287.0314.0336.0396416
21457ZWE181Zimbabwe2911Pulses5142Food1000 tonnes-19.0229.15...63.059.061.057.069.078.068.056.05255
21458ZWE181Zimbabwe2912Treenuts5142Food1000 tonnes-19.0229.15...1.02.01.02.02.03.04.02.043
21459ZWE181Zimbabwe2913Oilcrops5521Feed1000 tonnes-19.0229.15...36.046.041.033.031.019.024.017.02730
21460ZWE181Zimbabwe2913Oilcrops5142Food1000 tonnes-19.0229.15...60.059.061.062.048.044.041.040.03838
21461ZWE181Zimbabwe2914Vegetable Oils5142Food1000 tonnes-19.0229.15...111.0114.0112.0114.0134.0135.0137.0147.0159160
21462ZWE181Zimbabwe2918Vegetables5142Food1000 tonnes-19.0229.15...161.0166.0208.0185.0137.0179.0215.0217.0227227
21463ZWE181Zimbabwe2919Fruits - Excluding Wine5142Food1000 tonnes-19.0229.15...191.0134.0167.0177.0185.0184.0211.0230.0246217
21464ZWE181Zimbabwe2922Stimulants5142Food1000 tonnes-19.0229.15...7.021.014.024.016.011.023.011.01010
21465ZWE181Zimbabwe2923Spices5142Food1000 tonnes-19.0229.15...7.011.07.012.016.016.014.011.01212
21466ZWE181Zimbabwe2924Alcoholic Beverages5142Food1000 tonnes-19.0229.15...294.0290.0316.0355.0398.0437.0448.0476.0525516
21467ZWE181Zimbabwe2943Meat5142Food1000 tonnes-19.0229.15...222.0228.0233.0238.0242.0265.0262.0277.0280258
21468ZWE181Zimbabwe2945Offals5142Food1000 tonnes-19.0229.15...20.020.021.021.021.021.021.021.02222
21469ZWE181Zimbabwe2946Animal fats5142Food1000 tonnes-19.0229.15...26.026.029.029.027.031.030.025.02620
21470ZWE181Zimbabwe2949Eggs5142Food1000 tonnes-19.0229.15...15.018.018.021.022.027.027.024.02425
21471ZWE181Zimbabwe2948Milk - Excluding Butter5521Feed1000 tonnes-19.0229.15...21.021.021.021.021.023.025.025.03031
21472ZWE181Zimbabwe2948Milk - Excluding Butter5142Food1000 tonnes-19.0229.15...373.0357.0359.0356.0341.0385.0418.0457.0426451
21473ZWE181Zimbabwe2960Fish, Seafood5521Feed1000 tonnes-19.0229.15...5.04.09.06.09.05.015.015.01515
21474ZWE181Zimbabwe2960Fish, Seafood5142Food1000 tonnes-19.0229.15...18.014.017.014.015.018.029.040.04040
21475ZWE181Zimbabwe2961Aquatic Products, Other5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21476ZWE181Zimbabwe2928Miscellaneous5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
\n", - "

21477 rows × 63 columns

\n", - "
" - ], - "text/plain": [ - " Area Abbreviation Area Code Area Item Code \\\n", - "0 AFG 2 Afghanistan 2511 \n", - "1 AFG 2 Afghanistan 2805 \n", - "2 AFG 2 Afghanistan 2513 \n", - "3 AFG 2 Afghanistan 2513 \n", - "4 AFG 2 Afghanistan 2514 \n", - "5 AFG 2 Afghanistan 2514 \n", - "6 AFG 2 Afghanistan 2517 \n", - "7 AFG 2 Afghanistan 2520 \n", - "8 AFG 2 Afghanistan 2531 \n", - "9 AFG 2 Afghanistan 2536 \n", - "10 AFG 2 Afghanistan 2537 \n", - "11 AFG 2 Afghanistan 2542 \n", - "12 AFG 2 Afghanistan 2543 \n", - "13 AFG 2 Afghanistan 2745 \n", - "14 AFG 2 Afghanistan 2549 \n", - "15 AFG 2 Afghanistan 2549 \n", - "16 AFG 2 Afghanistan 2551 \n", - "17 AFG 2 Afghanistan 2560 \n", - "18 AFG 2 Afghanistan 2561 \n", - "19 AFG 2 Afghanistan 2563 \n", - "20 AFG 2 Afghanistan 2571 \n", - "21 AFG 2 Afghanistan 2572 \n", - "22 AFG 2 Afghanistan 2573 \n", - "23 AFG 2 Afghanistan 2574 \n", - "24 AFG 2 Afghanistan 2575 \n", - "25 AFG 2 Afghanistan 2577 \n", - "26 AFG 2 Afghanistan 2579 \n", - "27 AFG 2 Afghanistan 2580 \n", - "28 AFG 2 Afghanistan 2586 \n", - "29 AFG 2 Afghanistan 2601 \n", - "... ... ... ... ... \n", - "21447 ZWE 181 Zimbabwe 2765 \n", - "21448 ZWE 181 Zimbabwe 2766 \n", - "21449 ZWE 181 Zimbabwe 2767 \n", - "21450 ZWE 181 Zimbabwe 2775 \n", - "21451 ZWE 181 Zimbabwe 2680 \n", - "21452 ZWE 181 Zimbabwe 2905 \n", - "21453 ZWE 181 Zimbabwe 2905 \n", - "21454 ZWE 181 Zimbabwe 2907 \n", - "21455 ZWE 181 Zimbabwe 2908 \n", - "21456 ZWE 181 Zimbabwe 2909 \n", - "21457 ZWE 181 Zimbabwe 2911 \n", - "21458 ZWE 181 Zimbabwe 2912 \n", - "21459 ZWE 181 Zimbabwe 2913 \n", - "21460 ZWE 181 Zimbabwe 2913 \n", - "21461 ZWE 181 Zimbabwe 2914 \n", - "21462 ZWE 181 Zimbabwe 2918 \n", - "21463 ZWE 181 Zimbabwe 2919 \n", - "21464 ZWE 181 Zimbabwe 2922 \n", - "21465 ZWE 181 Zimbabwe 2923 \n", - "21466 ZWE 181 Zimbabwe 2924 \n", - "21467 ZWE 181 Zimbabwe 2943 \n", - "21468 ZWE 181 Zimbabwe 2945 \n", - "21469 ZWE 181 Zimbabwe 2946 \n", - "21470 ZWE 181 Zimbabwe 2949 \n", - "21471 ZWE 181 Zimbabwe 2948 \n", - "21472 ZWE 181 Zimbabwe 2948 \n", - "21473 ZWE 181 Zimbabwe 2960 \n", - "21474 ZWE 181 Zimbabwe 2960 \n", - "21475 ZWE 181 Zimbabwe 2961 \n", - "21476 ZWE 181 Zimbabwe 2928 \n", - "\n", - " Item Element Code Element Unit \\\n", - "0 Wheat and products 5142 Food 1000 tonnes \n", - "1 Rice (Milled Equivalent) 5142 Food 1000 tonnes \n", - "2 Barley and products 5521 Feed 1000 tonnes \n", - "3 Barley and products 5142 Food 1000 tonnes \n", - "4 Maize and products 5521 Feed 1000 tonnes \n", - "5 Maize and products 5142 Food 1000 tonnes \n", - "6 Millet and products 5142 Food 1000 tonnes \n", - "7 Cereals, Other 5142 Food 1000 tonnes \n", - "8 Potatoes and products 5142 Food 1000 tonnes \n", - "9 Sugar cane 5521 Feed 1000 tonnes \n", - "10 Sugar beet 5521 Feed 1000 tonnes \n", - "11 Sugar (Raw Equivalent) 5142 Food 1000 tonnes \n", - "12 Sweeteners, Other 5142 Food 1000 tonnes \n", - "13 Honey 5142 Food 1000 tonnes \n", - "14 Pulses, Other and products 5521 Feed 1000 tonnes \n", - "15 Pulses, Other and products 5142 Food 1000 tonnes \n", - "16 Nuts and products 5142 Food 1000 tonnes \n", - "17 Coconuts - Incl Copra 5142 Food 1000 tonnes \n", - "18 Sesame seed 5142 Food 1000 tonnes \n", - "19 Olives (including preserved) 5142 Food 1000 tonnes \n", - "20 Soyabean Oil 5142 Food 1000 tonnes \n", - "21 Groundnut Oil 5142 Food 1000 tonnes \n", - "22 Sunflowerseed Oil 5142 Food 1000 tonnes \n", - "23 Rape and Mustard Oil 5142 Food 1000 tonnes \n", - "24 Cottonseed Oil 5142 Food 1000 tonnes \n", - "25 Palm Oil 5142 Food 1000 tonnes \n", - "26 Sesameseed Oil 5142 Food 1000 tonnes \n", - "27 Olive Oil 5142 Food 1000 tonnes \n", - "28 Oilcrops Oil, Other 5142 Food 1000 tonnes \n", - "29 Tomatoes and products 5142 Food 1000 tonnes \n", - "... ... ... ... ... \n", - "21447 Crustaceans 5142 Food 1000 tonnes \n", - "21448 Cephalopods 5142 Food 1000 tonnes \n", - "21449 Molluscs, Other 5142 Food 1000 tonnes \n", - "21450 Aquatic Plants 5142 Food 1000 tonnes \n", - "21451 Infant food 5142 Food 1000 tonnes \n", - "21452 Cereals - Excluding Beer 5521 Feed 1000 tonnes \n", - "21453 Cereals - Excluding Beer 5142 Food 1000 tonnes \n", - "21454 Starchy Roots 5142 Food 1000 tonnes \n", - "21455 Sugar Crops 5142 Food 1000 tonnes \n", - "21456 Sugar & Sweeteners 5142 Food 1000 tonnes \n", - "21457 Pulses 5142 Food 1000 tonnes \n", - "21458 Treenuts 5142 Food 1000 tonnes \n", - "21459 Oilcrops 5521 Feed 1000 tonnes \n", - "21460 Oilcrops 5142 Food 1000 tonnes \n", - "21461 Vegetable Oils 5142 Food 1000 tonnes \n", - "21462 Vegetables 5142 Food 1000 tonnes \n", - "21463 Fruits - Excluding Wine 5142 Food 1000 tonnes \n", - "21464 Stimulants 5142 Food 1000 tonnes \n", - "21465 Spices 5142 Food 1000 tonnes \n", - "21466 Alcoholic Beverages 5142 Food 1000 tonnes \n", - "21467 Meat 5142 Food 1000 tonnes \n", - "21468 Offals 5142 Food 1000 tonnes \n", - "21469 Animal fats 5142 Food 1000 tonnes \n", - "21470 Eggs 5142 Food 1000 tonnes \n", - "21471 Milk - Excluding Butter 5521 Feed 1000 tonnes \n", - "21472 Milk - Excluding Butter 5142 Food 1000 tonnes \n", - "21473 Fish, Seafood 5521 Feed 1000 tonnes \n", - "21474 Fish, Seafood 5142 Food 1000 tonnes \n", - "21475 Aquatic Products, Other 5142 Food 1000 tonnes \n", - "21476 Miscellaneous 5142 Food 1000 tonnes \n", - "\n", - " latitude longitude ... Y2004 Y2005 Y2006 Y2007 Y2008 \\\n", - "0 33.94 67.71 ... 3249.0 3486.0 3704.0 4164.0 4252.0 \n", - "1 33.94 67.71 ... 419.0 445.0 546.0 455.0 490.0 \n", - "2 33.94 67.71 ... 58.0 236.0 262.0 263.0 230.0 \n", - "3 33.94 67.71 ... 185.0 43.0 44.0 48.0 62.0 \n", - "4 33.94 67.71 ... 120.0 208.0 233.0 249.0 247.0 \n", - "5 33.94 67.71 ... 231.0 67.0 82.0 67.0 69.0 \n", - "6 33.94 67.71 ... 15.0 21.0 11.0 19.0 21.0 \n", - "7 33.94 67.71 ... 2.0 1.0 1.0 0.0 0.0 \n", - "8 33.94 67.71 ... 276.0 294.0 294.0 260.0 242.0 \n", - "9 33.94 67.71 ... 50.0 29.0 61.0 65.0 54.0 \n", - "10 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "11 33.94 67.71 ... 124.0 152.0 169.0 192.0 217.0 \n", - "12 33.94 67.71 ... 9.0 15.0 12.0 6.0 11.0 \n", - "13 33.94 67.71 ... 3.0 3.0 3.0 3.0 3.0 \n", - "14 33.94 67.71 ... 3.0 2.0 3.0 3.0 3.0 \n", - "15 33.94 67.71 ... 17.0 35.0 37.0 40.0 54.0 \n", - "16 33.94 67.71 ... 11.0 13.0 24.0 34.0 42.0 \n", - "17 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "18 33.94 67.71 ... 16.0 16.0 13.0 16.0 16.0 \n", - "19 33.94 67.71 ... 1.0 1.0 0.0 0.0 2.0 \n", - "20 33.94 67.71 ... 6.0 35.0 18.0 21.0 11.0 \n", - "21 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "22 33.94 67.71 ... 4.0 6.0 5.0 9.0 3.0 \n", - "23 33.94 67.71 ... 0.0 1.0 3.0 5.0 6.0 \n", - "24 33.94 67.71 ... 2.0 3.0 3.0 3.0 3.0 \n", - "25 33.94 67.71 ... 71.0 69.0 56.0 51.0 36.0 \n", - "26 33.94 67.71 ... 1.0 1.0 1.0 2.0 2.0 \n", - "27 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "28 33.94 67.71 ... 0.0 1.0 0.0 0.0 3.0 \n", - "29 33.94 67.71 ... 2.0 2.0 8.0 1.0 0.0 \n", - "... ... ... ... ... ... ... ... ... \n", - "21447 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21448 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21449 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21450 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21451 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21452 -19.02 29.15 ... 75.0 54.0 75.0 55.0 63.0 \n", - "21453 -19.02 29.15 ... 1844.0 1842.0 1944.0 1962.0 1918.0 \n", - "21454 -19.02 29.15 ... 223.0 236.0 238.0 228.0 245.0 \n", - "21455 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21456 -19.02 29.15 ... 335.0 313.0 339.0 302.0 285.0 \n", - "21457 -19.02 29.15 ... 63.0 59.0 61.0 57.0 69.0 \n", - "21458 -19.02 29.15 ... 1.0 2.0 1.0 2.0 2.0 \n", - "21459 -19.02 29.15 ... 36.0 46.0 41.0 33.0 31.0 \n", - "21460 -19.02 29.15 ... 60.0 59.0 61.0 62.0 48.0 \n", - "21461 -19.02 29.15 ... 111.0 114.0 112.0 114.0 134.0 \n", - "21462 -19.02 29.15 ... 161.0 166.0 208.0 185.0 137.0 \n", - "21463 -19.02 29.15 ... 191.0 134.0 167.0 177.0 185.0 \n", - "21464 -19.02 29.15 ... 7.0 21.0 14.0 24.0 16.0 \n", - "21465 -19.02 29.15 ... 7.0 11.0 7.0 12.0 16.0 \n", - "21466 -19.02 29.15 ... 294.0 290.0 316.0 355.0 398.0 \n", - "21467 -19.02 29.15 ... 222.0 228.0 233.0 238.0 242.0 \n", - "21468 -19.02 29.15 ... 20.0 20.0 21.0 21.0 21.0 \n", - "21469 -19.02 29.15 ... 26.0 26.0 29.0 29.0 27.0 \n", - "21470 -19.02 29.15 ... 15.0 18.0 18.0 21.0 22.0 \n", - "21471 -19.02 29.15 ... 21.0 21.0 21.0 21.0 21.0 \n", - "21472 -19.02 29.15 ... 373.0 357.0 359.0 356.0 341.0 \n", - "21473 -19.02 29.15 ... 5.0 4.0 9.0 6.0 9.0 \n", - "21474 -19.02 29.15 ... 18.0 14.0 17.0 14.0 15.0 \n", - "21475 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21476 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "\n", - " Y2009 Y2010 Y2011 Y2012 Y2013 \n", - "0 4538.0 4605.0 4711.0 4810 4895 \n", - "1 415.0 442.0 476.0 425 422 \n", - "2 379.0 315.0 203.0 367 360 \n", - "3 55.0 60.0 72.0 78 89 \n", - "4 195.0 178.0 191.0 200 200 \n", - "5 71.0 82.0 73.0 77 76 \n", - "6 18.0 14.0 14.0 14 12 \n", - "7 0.0 0.0 0.0 0 0 \n", - "8 250.0 192.0 169.0 196 230 \n", - "9 114.0 83.0 83.0 69 81 \n", - "10 0.0 0.0 0.0 0 0 \n", - "11 231.0 240.0 240.0 250 255 \n", - "12 2.0 9.0 21.0 24 16 \n", - "13 3.0 3.0 2.0 2 2 \n", - "14 5.0 4.0 5.0 4 4 \n", - "15 80.0 66.0 81.0 63 74 \n", - "16 28.0 66.0 71.0 70 44 \n", - "17 0.0 0.0 0.0 0 0 \n", - "18 16.0 19.0 17.0 16 16 \n", - "19 3.0 2.0 2.0 2 2 \n", - "20 6.0 15.0 16.0 16 16 \n", - "21 0.0 0.0 0.0 0 0 \n", - "22 8.0 15.0 16.0 17 23 \n", - "23 6.0 1.0 2.0 2 2 \n", - "24 4.0 3.0 3.0 3 4 \n", - "25 53.0 59.0 51.0 61 64 \n", - "26 1.0 1.0 2.0 1 1 \n", - "27 1.0 1.0 1.0 1 1 \n", - "28 1.0 2.0 2.0 2 2 \n", - "29 0.0 0.0 0.0 0 0 \n", - "... ... ... ... ... ... \n", - "21447 0.0 0.0 0.0 0 0 \n", - "21448 0.0 0.0 0.0 0 0 \n", - "21449 0.0 1.0 0.0 0 0 \n", - "21450 0.0 0.0 0.0 0 0 \n", - "21451 0.0 0.0 0.0 0 0 \n", - "21452 62.0 55.0 55.0 55 55 \n", - "21453 1980.0 2011.0 2094.0 2071 2016 \n", - "21454 258.0 258.0 269.0 272 276 \n", - "21455 0.0 0.0 0.0 0 0 \n", - "21456 287.0 314.0 336.0 396 416 \n", - "21457 78.0 68.0 56.0 52 55 \n", - "21458 3.0 4.0 2.0 4 3 \n", - "21459 19.0 24.0 17.0 27 30 \n", - "21460 44.0 41.0 40.0 38 38 \n", - "21461 135.0 137.0 147.0 159 160 \n", - "21462 179.0 215.0 217.0 227 227 \n", - "21463 184.0 211.0 230.0 246 217 \n", - "21464 11.0 23.0 11.0 10 10 \n", - "21465 16.0 14.0 11.0 12 12 \n", - "21466 437.0 448.0 476.0 525 516 \n", - "21467 265.0 262.0 277.0 280 258 \n", - "21468 21.0 21.0 21.0 22 22 \n", - "21469 31.0 30.0 25.0 26 20 \n", - "21470 27.0 27.0 24.0 24 25 \n", - "21471 23.0 25.0 25.0 30 31 \n", - "21472 385.0 418.0 457.0 426 451 \n", - "21473 5.0 15.0 15.0 15 15 \n", - "21474 18.0 29.0 40.0 40 40 \n", - "21475 0.0 0.0 0.0 0 0 \n", - "21476 0.0 0.0 0.0 0 0 \n", - "\n", - "[21477 rows x 63 columns]" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "731a952c-b292-46e3-be7a-4afffe2b4ff1", - "_uuid": "5d165c279ce22afc0a874e32931d7b0ebb0717f9" - }, - "source": [ - "Let's see what the data looks like..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "_cell_guid": "79c7e3d0-c299-4dcb-8224-4455121ee9b0", - "_uuid": "d629ff2d2480ee46fbb7e2d37f6b5fab8052498a", - "scrolled": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "25c3f986-fd14-4a3f-baff-02571ad665eb", - "_uuid": "5a7da58320ab35ab1bcf83a62209afbe40b672fe" - }, - "source": [ - "# Plot for annual produce of different countries with quantity in y-axis and years in x-axis" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Area AbbreviationArea CodeAreaItem CodeItemElement CodeElementUnitlatitudelongitude...Y2004Y2005Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013
0AFG2Afghanistan2511Wheat and products5142Food1000 tonnes33.9467.71...3249.03486.03704.04164.04252.04538.04605.04711.048104895
1AFG2Afghanistan2805Rice (Milled Equivalent)5142Food1000 tonnes33.9467.71...419.0445.0546.0455.0490.0415.0442.0476.0425422
2AFG2Afghanistan2513Barley and products5521Feed1000 tonnes33.9467.71...58.0236.0262.0263.0230.0379.0315.0203.0367360
3AFG2Afghanistan2513Barley and products5142Food1000 tonnes33.9467.71...185.043.044.048.062.055.060.072.07889
4AFG2Afghanistan2514Maize and products5521Feed1000 tonnes33.9467.71...120.0208.0233.0249.0247.0195.0178.0191.0200200
5AFG2Afghanistan2514Maize and products5142Food1000 tonnes33.9467.71...231.067.082.067.069.071.082.073.07776
6AFG2Afghanistan2517Millet and products5142Food1000 tonnes33.9467.71...15.021.011.019.021.018.014.014.01412
7AFG2Afghanistan2520Cereals, Other5142Food1000 tonnes33.9467.71...2.01.01.00.00.00.00.00.000
8AFG2Afghanistan2531Potatoes and products5142Food1000 tonnes33.9467.71...276.0294.0294.0260.0242.0250.0192.0169.0196230
9AFG2Afghanistan2536Sugar cane5521Feed1000 tonnes33.9467.71...50.029.061.065.054.0114.083.083.06981
10AFG2Afghanistan2537Sugar beet5521Feed1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
11AFG2Afghanistan2542Sugar (Raw Equivalent)5142Food1000 tonnes33.9467.71...124.0152.0169.0192.0217.0231.0240.0240.0250255
12AFG2Afghanistan2543Sweeteners, Other5142Food1000 tonnes33.9467.71...9.015.012.06.011.02.09.021.02416
13AFG2Afghanistan2745Honey5142Food1000 tonnes33.9467.71...3.03.03.03.03.03.03.02.022
14AFG2Afghanistan2549Pulses, Other and products5521Feed1000 tonnes33.9467.71...3.02.03.03.03.05.04.05.044
15AFG2Afghanistan2549Pulses, Other and products5142Food1000 tonnes33.9467.71...17.035.037.040.054.080.066.081.06374
16AFG2Afghanistan2551Nuts and products5142Food1000 tonnes33.9467.71...11.013.024.034.042.028.066.071.07044
17AFG2Afghanistan2560Coconuts - Incl Copra5142Food1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
18AFG2Afghanistan2561Sesame seed5142Food1000 tonnes33.9467.71...16.016.013.016.016.016.019.017.01616
19AFG2Afghanistan2563Olives (including preserved)5142Food1000 tonnes33.9467.71...1.01.00.00.02.03.02.02.022
20AFG2Afghanistan2571Soyabean Oil5142Food1000 tonnes33.9467.71...6.035.018.021.011.06.015.016.01616
21AFG2Afghanistan2572Groundnut Oil5142Food1000 tonnes33.9467.71...0.00.00.00.00.00.00.00.000
22AFG2Afghanistan2573Sunflowerseed Oil5142Food1000 tonnes33.9467.71...4.06.05.09.03.08.015.016.01723
23AFG2Afghanistan2574Rape and Mustard Oil5142Food1000 tonnes33.9467.71...0.01.03.05.06.06.01.02.022
24AFG2Afghanistan2575Cottonseed Oil5142Food1000 tonnes33.9467.71...2.03.03.03.03.04.03.03.034
25AFG2Afghanistan2577Palm Oil5142Food1000 tonnes33.9467.71...71.069.056.051.036.053.059.051.06164
26AFG2Afghanistan2579Sesameseed Oil5142Food1000 tonnes33.9467.71...1.01.01.02.02.01.01.02.011
27AFG2Afghanistan2580Olive Oil5142Food1000 tonnes33.9467.71...0.00.00.00.00.01.01.01.011
28AFG2Afghanistan2586Oilcrops Oil, Other5142Food1000 tonnes33.9467.71...0.01.00.00.03.01.02.02.022
29AFG2Afghanistan2601Tomatoes and products5142Food1000 tonnes33.9467.71...2.02.08.01.00.00.00.00.000
..................................................................
21447ZWE181Zimbabwe2765Crustaceans5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21448ZWE181Zimbabwe2766Cephalopods5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21449ZWE181Zimbabwe2767Molluscs, Other5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.01.00.000
21450ZWE181Zimbabwe2775Aquatic Plants5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21451ZWE181Zimbabwe2680Infant food5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21452ZWE181Zimbabwe2905Cereals - Excluding Beer5521Feed1000 tonnes-19.0229.15...75.054.075.055.063.062.055.055.05555
21453ZWE181Zimbabwe2905Cereals - Excluding Beer5142Food1000 tonnes-19.0229.15...1844.01842.01944.01962.01918.01980.02011.02094.020712016
21454ZWE181Zimbabwe2907Starchy Roots5142Food1000 tonnes-19.0229.15...223.0236.0238.0228.0245.0258.0258.0269.0272276
21455ZWE181Zimbabwe2908Sugar Crops5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21456ZWE181Zimbabwe2909Sugar & Sweeteners5142Food1000 tonnes-19.0229.15...335.0313.0339.0302.0285.0287.0314.0336.0396416
21457ZWE181Zimbabwe2911Pulses5142Food1000 tonnes-19.0229.15...63.059.061.057.069.078.068.056.05255
21458ZWE181Zimbabwe2912Treenuts5142Food1000 tonnes-19.0229.15...1.02.01.02.02.03.04.02.043
21459ZWE181Zimbabwe2913Oilcrops5521Feed1000 tonnes-19.0229.15...36.046.041.033.031.019.024.017.02730
21460ZWE181Zimbabwe2913Oilcrops5142Food1000 tonnes-19.0229.15...60.059.061.062.048.044.041.040.03838
21461ZWE181Zimbabwe2914Vegetable Oils5142Food1000 tonnes-19.0229.15...111.0114.0112.0114.0134.0135.0137.0147.0159160
21462ZWE181Zimbabwe2918Vegetables5142Food1000 tonnes-19.0229.15...161.0166.0208.0185.0137.0179.0215.0217.0227227
21463ZWE181Zimbabwe2919Fruits - Excluding Wine5142Food1000 tonnes-19.0229.15...191.0134.0167.0177.0185.0184.0211.0230.0246217
21464ZWE181Zimbabwe2922Stimulants5142Food1000 tonnes-19.0229.15...7.021.014.024.016.011.023.011.01010
21465ZWE181Zimbabwe2923Spices5142Food1000 tonnes-19.0229.15...7.011.07.012.016.016.014.011.01212
21466ZWE181Zimbabwe2924Alcoholic Beverages5142Food1000 tonnes-19.0229.15...294.0290.0316.0355.0398.0437.0448.0476.0525516
21467ZWE181Zimbabwe2943Meat5142Food1000 tonnes-19.0229.15...222.0228.0233.0238.0242.0265.0262.0277.0280258
21468ZWE181Zimbabwe2945Offals5142Food1000 tonnes-19.0229.15...20.020.021.021.021.021.021.021.02222
21469ZWE181Zimbabwe2946Animal fats5142Food1000 tonnes-19.0229.15...26.026.029.029.027.031.030.025.02620
21470ZWE181Zimbabwe2949Eggs5142Food1000 tonnes-19.0229.15...15.018.018.021.022.027.027.024.02425
21471ZWE181Zimbabwe2948Milk - Excluding Butter5521Feed1000 tonnes-19.0229.15...21.021.021.021.021.023.025.025.03031
21472ZWE181Zimbabwe2948Milk - Excluding Butter5142Food1000 tonnes-19.0229.15...373.0357.0359.0356.0341.0385.0418.0457.0426451
21473ZWE181Zimbabwe2960Fish, Seafood5521Feed1000 tonnes-19.0229.15...5.04.09.06.09.05.015.015.01515
21474ZWE181Zimbabwe2960Fish, Seafood5142Food1000 tonnes-19.0229.15...18.014.017.014.015.018.029.040.04040
21475ZWE181Zimbabwe2961Aquatic Products, Other5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
21476ZWE181Zimbabwe2928Miscellaneous5142Food1000 tonnes-19.0229.15...0.00.00.00.00.00.00.00.000
\n", - "

21477 rows × 63 columns

\n", - "
" - ], - "text/plain": [ - " Area Abbreviation Area Code Area Item Code \\\n", - "0 AFG 2 Afghanistan 2511 \n", - "1 AFG 2 Afghanistan 2805 \n", - "2 AFG 2 Afghanistan 2513 \n", - "3 AFG 2 Afghanistan 2513 \n", - "4 AFG 2 Afghanistan 2514 \n", - "5 AFG 2 Afghanistan 2514 \n", - "6 AFG 2 Afghanistan 2517 \n", - "7 AFG 2 Afghanistan 2520 \n", - "8 AFG 2 Afghanistan 2531 \n", - "9 AFG 2 Afghanistan 2536 \n", - "10 AFG 2 Afghanistan 2537 \n", - "11 AFG 2 Afghanistan 2542 \n", - "12 AFG 2 Afghanistan 2543 \n", - "13 AFG 2 Afghanistan 2745 \n", - "14 AFG 2 Afghanistan 2549 \n", - "15 AFG 2 Afghanistan 2549 \n", - "16 AFG 2 Afghanistan 2551 \n", - "17 AFG 2 Afghanistan 2560 \n", - "18 AFG 2 Afghanistan 2561 \n", - "19 AFG 2 Afghanistan 2563 \n", - "20 AFG 2 Afghanistan 2571 \n", - "21 AFG 2 Afghanistan 2572 \n", - "22 AFG 2 Afghanistan 2573 \n", - "23 AFG 2 Afghanistan 2574 \n", - "24 AFG 2 Afghanistan 2575 \n", - "25 AFG 2 Afghanistan 2577 \n", - "26 AFG 2 Afghanistan 2579 \n", - "27 AFG 2 Afghanistan 2580 \n", - "28 AFG 2 Afghanistan 2586 \n", - "29 AFG 2 Afghanistan 2601 \n", - "... ... ... ... ... \n", - "21447 ZWE 181 Zimbabwe 2765 \n", - "21448 ZWE 181 Zimbabwe 2766 \n", - "21449 ZWE 181 Zimbabwe 2767 \n", - "21450 ZWE 181 Zimbabwe 2775 \n", - "21451 ZWE 181 Zimbabwe 2680 \n", - "21452 ZWE 181 Zimbabwe 2905 \n", - "21453 ZWE 181 Zimbabwe 2905 \n", - "21454 ZWE 181 Zimbabwe 2907 \n", - "21455 ZWE 181 Zimbabwe 2908 \n", - "21456 ZWE 181 Zimbabwe 2909 \n", - "21457 ZWE 181 Zimbabwe 2911 \n", - "21458 ZWE 181 Zimbabwe 2912 \n", - "21459 ZWE 181 Zimbabwe 2913 \n", - "21460 ZWE 181 Zimbabwe 2913 \n", - "21461 ZWE 181 Zimbabwe 2914 \n", - "21462 ZWE 181 Zimbabwe 2918 \n", - "21463 ZWE 181 Zimbabwe 2919 \n", - "21464 ZWE 181 Zimbabwe 2922 \n", - "21465 ZWE 181 Zimbabwe 2923 \n", - "21466 ZWE 181 Zimbabwe 2924 \n", - "21467 ZWE 181 Zimbabwe 2943 \n", - "21468 ZWE 181 Zimbabwe 2945 \n", - "21469 ZWE 181 Zimbabwe 2946 \n", - "21470 ZWE 181 Zimbabwe 2949 \n", - "21471 ZWE 181 Zimbabwe 2948 \n", - "21472 ZWE 181 Zimbabwe 2948 \n", - "21473 ZWE 181 Zimbabwe 2960 \n", - "21474 ZWE 181 Zimbabwe 2960 \n", - "21475 ZWE 181 Zimbabwe 2961 \n", - "21476 ZWE 181 Zimbabwe 2928 \n", - "\n", - " Item Element Code Element Unit \\\n", - "0 Wheat and products 5142 Food 1000 tonnes \n", - "1 Rice (Milled Equivalent) 5142 Food 1000 tonnes \n", - "2 Barley and products 5521 Feed 1000 tonnes \n", - "3 Barley and products 5142 Food 1000 tonnes \n", - "4 Maize and products 5521 Feed 1000 tonnes \n", - "5 Maize and products 5142 Food 1000 tonnes \n", - "6 Millet and products 5142 Food 1000 tonnes \n", - "7 Cereals, Other 5142 Food 1000 tonnes \n", - "8 Potatoes and products 5142 Food 1000 tonnes \n", - "9 Sugar cane 5521 Feed 1000 tonnes \n", - "10 Sugar beet 5521 Feed 1000 tonnes \n", - "11 Sugar (Raw Equivalent) 5142 Food 1000 tonnes \n", - "12 Sweeteners, Other 5142 Food 1000 tonnes \n", - "13 Honey 5142 Food 1000 tonnes \n", - "14 Pulses, Other and products 5521 Feed 1000 tonnes \n", - "15 Pulses, Other and products 5142 Food 1000 tonnes \n", - "16 Nuts and products 5142 Food 1000 tonnes \n", - "17 Coconuts - Incl Copra 5142 Food 1000 tonnes \n", - "18 Sesame seed 5142 Food 1000 tonnes \n", - "19 Olives (including preserved) 5142 Food 1000 tonnes \n", - "20 Soyabean Oil 5142 Food 1000 tonnes \n", - "21 Groundnut Oil 5142 Food 1000 tonnes \n", - "22 Sunflowerseed Oil 5142 Food 1000 tonnes \n", - "23 Rape and Mustard Oil 5142 Food 1000 tonnes \n", - "24 Cottonseed Oil 5142 Food 1000 tonnes \n", - "25 Palm Oil 5142 Food 1000 tonnes \n", - "26 Sesameseed Oil 5142 Food 1000 tonnes \n", - "27 Olive Oil 5142 Food 1000 tonnes \n", - "28 Oilcrops Oil, Other 5142 Food 1000 tonnes \n", - "29 Tomatoes and products 5142 Food 1000 tonnes \n", - "... ... ... ... ... \n", - "21447 Crustaceans 5142 Food 1000 tonnes \n", - "21448 Cephalopods 5142 Food 1000 tonnes \n", - "21449 Molluscs, Other 5142 Food 1000 tonnes \n", - "21450 Aquatic Plants 5142 Food 1000 tonnes \n", - "21451 Infant food 5142 Food 1000 tonnes \n", - "21452 Cereals - Excluding Beer 5521 Feed 1000 tonnes \n", - "21453 Cereals - Excluding Beer 5142 Food 1000 tonnes \n", - "21454 Starchy Roots 5142 Food 1000 tonnes \n", - "21455 Sugar Crops 5142 Food 1000 tonnes \n", - "21456 Sugar & Sweeteners 5142 Food 1000 tonnes \n", - "21457 Pulses 5142 Food 1000 tonnes \n", - "21458 Treenuts 5142 Food 1000 tonnes \n", - "21459 Oilcrops 5521 Feed 1000 tonnes \n", - "21460 Oilcrops 5142 Food 1000 tonnes \n", - "21461 Vegetable Oils 5142 Food 1000 tonnes \n", - "21462 Vegetables 5142 Food 1000 tonnes \n", - "21463 Fruits - Excluding Wine 5142 Food 1000 tonnes \n", - "21464 Stimulants 5142 Food 1000 tonnes \n", - "21465 Spices 5142 Food 1000 tonnes \n", - "21466 Alcoholic Beverages 5142 Food 1000 tonnes \n", - "21467 Meat 5142 Food 1000 tonnes \n", - "21468 Offals 5142 Food 1000 tonnes \n", - "21469 Animal fats 5142 Food 1000 tonnes \n", - "21470 Eggs 5142 Food 1000 tonnes \n", - "21471 Milk - Excluding Butter 5521 Feed 1000 tonnes \n", - "21472 Milk - Excluding Butter 5142 Food 1000 tonnes \n", - "21473 Fish, Seafood 5521 Feed 1000 tonnes \n", - "21474 Fish, Seafood 5142 Food 1000 tonnes \n", - "21475 Aquatic Products, Other 5142 Food 1000 tonnes \n", - "21476 Miscellaneous 5142 Food 1000 tonnes \n", - "\n", - " latitude longitude ... Y2004 Y2005 Y2006 Y2007 Y2008 \\\n", - "0 33.94 67.71 ... 3249.0 3486.0 3704.0 4164.0 4252.0 \n", - "1 33.94 67.71 ... 419.0 445.0 546.0 455.0 490.0 \n", - "2 33.94 67.71 ... 58.0 236.0 262.0 263.0 230.0 \n", - "3 33.94 67.71 ... 185.0 43.0 44.0 48.0 62.0 \n", - "4 33.94 67.71 ... 120.0 208.0 233.0 249.0 247.0 \n", - "5 33.94 67.71 ... 231.0 67.0 82.0 67.0 69.0 \n", - "6 33.94 67.71 ... 15.0 21.0 11.0 19.0 21.0 \n", - "7 33.94 67.71 ... 2.0 1.0 1.0 0.0 0.0 \n", - "8 33.94 67.71 ... 276.0 294.0 294.0 260.0 242.0 \n", - "9 33.94 67.71 ... 50.0 29.0 61.0 65.0 54.0 \n", - "10 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "11 33.94 67.71 ... 124.0 152.0 169.0 192.0 217.0 \n", - "12 33.94 67.71 ... 9.0 15.0 12.0 6.0 11.0 \n", - "13 33.94 67.71 ... 3.0 3.0 3.0 3.0 3.0 \n", - "14 33.94 67.71 ... 3.0 2.0 3.0 3.0 3.0 \n", - "15 33.94 67.71 ... 17.0 35.0 37.0 40.0 54.0 \n", - "16 33.94 67.71 ... 11.0 13.0 24.0 34.0 42.0 \n", - "17 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "18 33.94 67.71 ... 16.0 16.0 13.0 16.0 16.0 \n", - "19 33.94 67.71 ... 1.0 1.0 0.0 0.0 2.0 \n", - "20 33.94 67.71 ... 6.0 35.0 18.0 21.0 11.0 \n", - "21 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "22 33.94 67.71 ... 4.0 6.0 5.0 9.0 3.0 \n", - "23 33.94 67.71 ... 0.0 1.0 3.0 5.0 6.0 \n", - "24 33.94 67.71 ... 2.0 3.0 3.0 3.0 3.0 \n", - "25 33.94 67.71 ... 71.0 69.0 56.0 51.0 36.0 \n", - "26 33.94 67.71 ... 1.0 1.0 1.0 2.0 2.0 \n", - "27 33.94 67.71 ... 0.0 0.0 0.0 0.0 0.0 \n", - "28 33.94 67.71 ... 0.0 1.0 0.0 0.0 3.0 \n", - "29 33.94 67.71 ... 2.0 2.0 8.0 1.0 0.0 \n", - "... ... ... ... ... ... ... ... ... \n", - "21447 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21448 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21449 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21450 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21451 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21452 -19.02 29.15 ... 75.0 54.0 75.0 55.0 63.0 \n", - "21453 -19.02 29.15 ... 1844.0 1842.0 1944.0 1962.0 1918.0 \n", - "21454 -19.02 29.15 ... 223.0 236.0 238.0 228.0 245.0 \n", - "21455 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21456 -19.02 29.15 ... 335.0 313.0 339.0 302.0 285.0 \n", - "21457 -19.02 29.15 ... 63.0 59.0 61.0 57.0 69.0 \n", - "21458 -19.02 29.15 ... 1.0 2.0 1.0 2.0 2.0 \n", - "21459 -19.02 29.15 ... 36.0 46.0 41.0 33.0 31.0 \n", - "21460 -19.02 29.15 ... 60.0 59.0 61.0 62.0 48.0 \n", - "21461 -19.02 29.15 ... 111.0 114.0 112.0 114.0 134.0 \n", - "21462 -19.02 29.15 ... 161.0 166.0 208.0 185.0 137.0 \n", - "21463 -19.02 29.15 ... 191.0 134.0 167.0 177.0 185.0 \n", - "21464 -19.02 29.15 ... 7.0 21.0 14.0 24.0 16.0 \n", - "21465 -19.02 29.15 ... 7.0 11.0 7.0 12.0 16.0 \n", - "21466 -19.02 29.15 ... 294.0 290.0 316.0 355.0 398.0 \n", - "21467 -19.02 29.15 ... 222.0 228.0 233.0 238.0 242.0 \n", - "21468 -19.02 29.15 ... 20.0 20.0 21.0 21.0 21.0 \n", - "21469 -19.02 29.15 ... 26.0 26.0 29.0 29.0 27.0 \n", - "21470 -19.02 29.15 ... 15.0 18.0 18.0 21.0 22.0 \n", - "21471 -19.02 29.15 ... 21.0 21.0 21.0 21.0 21.0 \n", - "21472 -19.02 29.15 ... 373.0 357.0 359.0 356.0 341.0 \n", - "21473 -19.02 29.15 ... 5.0 4.0 9.0 6.0 9.0 \n", - "21474 -19.02 29.15 ... 18.0 14.0 17.0 14.0 15.0 \n", - "21475 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "21476 -19.02 29.15 ... 0.0 0.0 0.0 0.0 0.0 \n", - "\n", - " Y2009 Y2010 Y2011 Y2012 Y2013 \n", - "0 4538.0 4605.0 4711.0 4810 4895 \n", - "1 415.0 442.0 476.0 425 422 \n", - "2 379.0 315.0 203.0 367 360 \n", - "3 55.0 60.0 72.0 78 89 \n", - "4 195.0 178.0 191.0 200 200 \n", - "5 71.0 82.0 73.0 77 76 \n", - "6 18.0 14.0 14.0 14 12 \n", - "7 0.0 0.0 0.0 0 0 \n", - "8 250.0 192.0 169.0 196 230 \n", - "9 114.0 83.0 83.0 69 81 \n", - "10 0.0 0.0 0.0 0 0 \n", - "11 231.0 240.0 240.0 250 255 \n", - "12 2.0 9.0 21.0 24 16 \n", - "13 3.0 3.0 2.0 2 2 \n", - "14 5.0 4.0 5.0 4 4 \n", - "15 80.0 66.0 81.0 63 74 \n", - "16 28.0 66.0 71.0 70 44 \n", - "17 0.0 0.0 0.0 0 0 \n", - "18 16.0 19.0 17.0 16 16 \n", - "19 3.0 2.0 2.0 2 2 \n", - "20 6.0 15.0 16.0 16 16 \n", - "21 0.0 0.0 0.0 0 0 \n", - "22 8.0 15.0 16.0 17 23 \n", - "23 6.0 1.0 2.0 2 2 \n", - "24 4.0 3.0 3.0 3 4 \n", - "25 53.0 59.0 51.0 61 64 \n", - "26 1.0 1.0 2.0 1 1 \n", - "27 1.0 1.0 1.0 1 1 \n", - "28 1.0 2.0 2.0 2 2 \n", - "29 0.0 0.0 0.0 0 0 \n", - "... ... ... ... ... ... \n", - "21447 0.0 0.0 0.0 0 0 \n", - "21448 0.0 0.0 0.0 0 0 \n", - "21449 0.0 1.0 0.0 0 0 \n", - "21450 0.0 0.0 0.0 0 0 \n", - "21451 0.0 0.0 0.0 0 0 \n", - "21452 62.0 55.0 55.0 55 55 \n", - "21453 1980.0 2011.0 2094.0 2071 2016 \n", - "21454 258.0 258.0 269.0 272 276 \n", - "21455 0.0 0.0 0.0 0 0 \n", - "21456 287.0 314.0 336.0 396 416 \n", - "21457 78.0 68.0 56.0 52 55 \n", - "21458 3.0 4.0 2.0 4 3 \n", - "21459 19.0 24.0 17.0 27 30 \n", - "21460 44.0 41.0 40.0 38 38 \n", - "21461 135.0 137.0 147.0 159 160 \n", - "21462 179.0 215.0 217.0 227 227 \n", - "21463 184.0 211.0 230.0 246 217 \n", - "21464 11.0 23.0 11.0 10 10 \n", - "21465 16.0 14.0 11.0 12 12 \n", - "21466 437.0 448.0 476.0 525 516 \n", - "21467 265.0 262.0 277.0 280 258 \n", - "21468 21.0 21.0 21.0 22 22 \n", - "21469 31.0 30.0 25.0 26 20 \n", - "21470 27.0 27.0 24.0 24 25 \n", - "21471 23.0 25.0 25.0 30 31 \n", - "21472 385.0 418.0 457.0 426 451 \n", - "21473 5.0 15.0 15.0 15 15 \n", - "21474 18.0 29.0 40.0 40 40 \n", - "21475 0.0 0.0 0.0 0 0 \n", - "21476 0.0 0.0 0.0 0 0 \n", - "\n", - "[21477 rows x 63 columns]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "_cell_guid": "347e620f-b0e4-448e-81c7-e164f560c5a3", - "_uuid": "0acdd759950f5df3298224b0804562973663a11d", - "scrolled": false - }, - "outputs": [ - { - "data": { - "image/png": "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D37wg6vmzZtXfKZthwwZUrVgwYKYH/7wh6Uvv/zy125w9PDDD5eMHDmyX3x8fGNGRkadJP3617/uVFVV5T71pkG5ubnBaWlpDfPmzSuprq52bdq0KVzSMY/HY+vr601ISIg9088LgaglM3Uvp4SEBO/UqVNL33zzzfi77777mCRVVla6U1JSGiWp+dIeF8rn8+mVV16JmT17dumCBQviRowY8bWbcdXU1LgkKTExsam8vNy1ZMmS0842RCvkwO8dWVlZIS6XS80zaDdv3hyW1cPc5AAAIABJREFUlJTUcLHnUVRUlLeiouKsBeb48eOrn3322f3Tpk3rlZmZuevUG2hGR0d7T/0jSnR0tLeqqurk/rKzs4P79etXn56efmTv3r0hW7ZsCbv11ltPPhfO9rPImYwaNary5Zdfjvuv//qv4nfeeafDufKHhITYOXPmHP7d736XeO2111ZK0o033ljx29/+NmHBggUHXC6XPv/887DRo0fXStKdd955/D//8z8TKysr3SNGjPjWf2rs2LEjZOzYsdVjx46tXr58ece9e/cGJyYmnvY/OgAAuJzaZwHskOZrAEsn/pL8wgsv7PN4vv0pmD59evmLL77YqXfv3v179OhRN3jw4GpJ+uMf/xhXXl7uvu2223pKUkJCQsOaNWu4lhROy+VyafHixXseeeSR5GeffTYxJCTEJiUl1dfW1rqOHDkSNGTIkH6SNHHixLJnn3226F//9V8P3nTTTb19Pp+CgoLs/PnzD9x0003V8+fP33f77bf39Pl8iouLa1y7du3ulhz/wQcfPD5p0qSeAwYM6Jeenl6TlpZ21jud48pxuVxasmTJnh/96EfJ8+fPT4yNjW0KDw/3/vKXvywcP3581fPPP1/fp0+f9D59+tT279+/RpKuueaa2gEDBtT06tUrPSUlpf7UyzrMmjWrZNKkSb06d+7cuH79+tw//vGP+2bMmHFVQ0ODkaRf/OIXB1taALfkvOHcwpWSkJDgXbZsWe6YMWP6durUqem+++4re+CBB4499dRT3R544IFvzfRq9vvf/77g3nvvTZs/f37ihAkTyiIjI0/+4Sw5ObmpR48edVOnTi1rHnvuuecSg4KCbPPPCPfff//RsLAw3/z58xM9Ho8NDw/3vvHGG/mSdO+99x7t169f/wEDBtS8/fbb+0738wIuj3/5l385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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "area_list = list(df['Area'].unique())\n", - "year_list = list(df.iloc[:,10:].columns)\n", - "\n", - "plt.figure(figsize=(24,12))\n", - "for ar in area_list:\n", - " yearly_produce = []\n", - " for yr in year_list:\n", - " yearly_produce.append(df[yr][df['Area'] == ar].sum())\n", - " plt.plot(yearly_produce, label=ar)\n", - "plt.xticks(np.arange(53), tuple(year_list), rotation=60)\n", - "plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=8, mode=\"expand\", borderaxespad=0.)\n", - "plt.savefig('p.png')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "
" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(24,12))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "2ebe07e3-739b-4f39-8736-a512426c05bf", - "_uuid": "70900ec0ff5e248cd382ee53b5927cb671efa80e", - "collapsed": true - }, - "source": [ - "Clearly, China, India and US stand out here. So, these are the countries with most food and feed production.\n", - "\n", - "Now, let's have a close look at their food and feed data\n", - "\n", - "# Food and feed plot for the whole dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "_cell_guid": "ec0c911d-e154-4f8a-a79f-ced4896d5115", - "_uuid": "683dc56125b3a4c66b1e140098ec91490cbbe96f", - "scrolled": true - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/seaborn/categorical.py:3666: UserWarning: The `factorplot` function has been renamed to `catplot`. The original name will be removed in a future release. Please update your code. Note that the default `kind` in `factorplot` (`'point'`) has changed `'strip'` in `catplot`.\n", - " warnings.warn(msg)\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "sns.factorplot(\"Element\", data=df, kind=\"count\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "189c74af-e6e4-4ddd-a73c-3725f3aa8124", - "_uuid": "bfd404fb5dbb48c3e3bd1dcd45fb27a5fb475a00" - }, - "source": [ - "So, there is a huge difference in food and feed production. Now, we have obvious assumptions about the following plots after looking at this huge difference.\n", - "\n", - "# Food and feed plot for the largest producers(India, USA, China)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "_cell_guid": "0bf44e4e-d4c4-4f74-ae9f-82f52139d182", - "_uuid": "be1bc3d49c8cee62f48a09ada0db3170adcedc17" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/seaborn/categorical.py:3666: UserWarning: The `factorplot` function has been renamed to `catplot`. The original name will be removed in a future release. Please update your code. Note that the default `kind` in `factorplot` (`'point'`) has changed `'strip'` in `catplot`.\n", - " warnings.warn(msg)\n", - "/anaconda3/lib/python3.7/site-packages/seaborn/categorical.py:3672: UserWarning: The `size` paramter has been renamed to `height`; please update your code.\n", - " warnings.warn(msg, UserWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "sns.factorplot(\"Area\", data=df[(df['Area'] == \"India\") | (df['Area'] == \"China, mainland\") | (df['Area'] == \"United States of America\")], kind=\"count\", hue=\"Element\", size=8, aspect=.8)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "94c19dc8-b1e7-4b61-b81f-422c27184c4e", - "_uuid": "0d1cfc7acc74847dbc5813b9b3bd0eb9db450985" - }, - "source": [ - "Though, there is a huge difference between feed and food production, these countries' total production and their ranks depend on feed production." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "9dba87b4-fa51-43ef-95ae-f31396c20146", - "_uuid": "43e0f00abf706ab1782ebb78cefc38aca17316e6" - }, - "source": [ - "Now, we create a dataframe with countries as index and their annual produce as columns from 1961 to 2013." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "_cell_guid": "c4a5f859-0384-4c8e-b894-3f747aec8cf9", - "_uuid": "84dd7a2b601479728dd172d3100951553c2daff5", - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
AfghanistanAlbaniaAlgeriaAngolaAntigua and BarbudaArgentinaArmeniaAustraliaAustriaAzerbaijan...United Republic of TanzaniaUnited States of AmericaUruguayUzbekistanVanuatuVenezuela (Bolivarian Republic of)Viet NamYemenZambiaZimbabwe
09481.01706.07488.04834.092.043402.00.025795.022542.00.0...12367.0559347.04631.00.097.09523.023856.02982.02976.03260.0
19414.01749.07235.04775.094.040784.00.027618.022627.00.0...12810.0556319.04448.00.0101.09369.025220.03038.03057.03503.0
29194.01767.06861.05240.0105.040219.00.028902.023637.00.0...13109.0552630.04682.00.0103.09788.026053.03147.03069.03479.0
310170.01889.07255.05286.095.041638.00.029107.024099.00.0...12965.0555677.04723.00.0102.010539.026377.03224.03121.03738.0
410473.01884.07509.05527.084.044936.00.028961.022664.00.0...13742.0589288.04581.00.0107.010641.026961.03328.03236.03940.0
\n", - "

5 rows × 174 columns

\n", - "
" - ], - "text/plain": [ - " Afghanistan Albania Algeria Angola Antigua and Barbuda Argentina \\\n", - "0 9481.0 1706.0 7488.0 4834.0 92.0 43402.0 \n", - "1 9414.0 1749.0 7235.0 4775.0 94.0 40784.0 \n", - "2 9194.0 1767.0 6861.0 5240.0 105.0 40219.0 \n", - "3 10170.0 1889.0 7255.0 5286.0 95.0 41638.0 \n", - "4 10473.0 1884.0 7509.0 5527.0 84.0 44936.0 \n", - "\n", - " Armenia Australia Austria Azerbaijan ... \\\n", - "0 0.0 25795.0 22542.0 0.0 ... \n", - "1 0.0 27618.0 22627.0 0.0 ... \n", - "2 0.0 28902.0 23637.0 0.0 ... \n", - "3 0.0 29107.0 24099.0 0.0 ... \n", - "4 0.0 28961.0 22664.0 0.0 ... \n", - "\n", - " United Republic of Tanzania United States of America Uruguay Uzbekistan \\\n", - "0 12367.0 559347.0 4631.0 0.0 \n", - "1 12810.0 556319.0 4448.0 0.0 \n", - "2 13109.0 552630.0 4682.0 0.0 \n", - "3 12965.0 555677.0 4723.0 0.0 \n", - "4 13742.0 589288.0 4581.0 0.0 \n", - "\n", - " Vanuatu Venezuela (Bolivarian Republic of) Viet Nam Yemen Zambia \\\n", - "0 97.0 9523.0 23856.0 2982.0 2976.0 \n", - "1 101.0 9369.0 25220.0 3038.0 3057.0 \n", - "2 103.0 9788.0 26053.0 3147.0 3069.0 \n", - "3 102.0 10539.0 26377.0 3224.0 3121.0 \n", - "4 107.0 10641.0 26961.0 3328.0 3236.0 \n", - "\n", - " Zimbabwe \n", - "0 3260.0 \n", - "1 3503.0 \n", - "2 3479.0 \n", - "3 3738.0 \n", - "4 3940.0 \n", - "\n", - "[5 rows x 174 columns]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_df_dict = {}\n", - "for ar in area_list:\n", - " yearly_produce = []\n", - " for yr in year_list:\n", - " yearly_produce.append(df[yr][df['Area']==ar].sum())\n", - " new_df_dict[ar] = yearly_produce\n", - "new_df = pd.DataFrame(new_df_dict)\n", - "\n", - "new_df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "15fbe29c-5cea-4ac3-9b95-f92acd89b336", - "_uuid": "ea48f75e9824a0c4c1a5f19cbd63e59a6cb44fe1" - }, - "source": [ - "Now, this is not perfect so we transpose this dataframe and add column names." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "_cell_guid": "145f751e-4f5b-4811-a68c-9d20b3c36e10", - "_uuid": "28e765d82bb4ebec3be49200a30fc4e0eabb24d7" - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Y1961Y1962Y1963Y1964Y1965Y1966Y1967Y1968Y1969Y1970...Y2004Y2005Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013
Afghanistan9481.09414.09194.010170.010473.010169.011289.011508.011815.010454.0...16542.017658.018317.019248.019381.020661.021030.021100.022706.023007.0
Albania1706.01749.01767.01889.01884.01995.02046.02169.02230.02395.0...6637.06719.06911.06744.07168.07316.07907.08114.08221.08271.0
Algeria7488.07235.06861.07255.07509.07536.07986.08839.09003.09355.0...48619.049562.051067.049933.050916.057505.060071.065852.069365.072161.0
Angola4834.04775.05240.05286.05527.05677.05833.05685.06219.06460.0...25541.026696.028247.029877.032053.036985.038400.040573.038064.048639.0
Antigua and Barbuda92.094.0105.095.084.073.064.059.068.077.0...92.0115.0110.0122.0115.0114.0115.0118.0113.0119.0
\n", - "

5 rows × 53 columns

\n", - "
" - ], - "text/plain": [ - " Y1961 Y1962 Y1963 Y1964 Y1965 Y1966 \\\n", - "Afghanistan 9481.0 9414.0 9194.0 10170.0 10473.0 10169.0 \n", - "Albania 1706.0 1749.0 1767.0 1889.0 1884.0 1995.0 \n", - "Algeria 7488.0 7235.0 6861.0 7255.0 7509.0 7536.0 \n", - "Angola 4834.0 4775.0 5240.0 5286.0 5527.0 5677.0 \n", - "Antigua and Barbuda 92.0 94.0 105.0 95.0 84.0 73.0 \n", - "\n", - " Y1967 Y1968 Y1969 Y1970 ... Y2004 \\\n", - "Afghanistan 11289.0 11508.0 11815.0 10454.0 ... 16542.0 \n", - "Albania 2046.0 2169.0 2230.0 2395.0 ... 6637.0 \n", - "Algeria 7986.0 8839.0 9003.0 9355.0 ... 48619.0 \n", - "Angola 5833.0 5685.0 6219.0 6460.0 ... 25541.0 \n", - "Antigua and Barbuda 64.0 59.0 68.0 77.0 ... 92.0 \n", - "\n", - " Y2005 Y2006 Y2007 Y2008 Y2009 Y2010 \\\n", - "Afghanistan 17658.0 18317.0 19248.0 19381.0 20661.0 21030.0 \n", - "Albania 6719.0 6911.0 6744.0 7168.0 7316.0 7907.0 \n", - "Algeria 49562.0 51067.0 49933.0 50916.0 57505.0 60071.0 \n", - "Angola 26696.0 28247.0 29877.0 32053.0 36985.0 38400.0 \n", - "Antigua and Barbuda 115.0 110.0 122.0 115.0 114.0 115.0 \n", - "\n", - " Y2011 Y2012 Y2013 \n", - "Afghanistan 21100.0 22706.0 23007.0 \n", - "Albania 8114.0 8221.0 8271.0 \n", - "Algeria 65852.0 69365.0 72161.0 \n", - "Angola 40573.0 38064.0 48639.0 \n", - "Antigua and Barbuda 118.0 113.0 119.0 \n", - "\n", - "[5 rows x 53 columns]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_df = pd.DataFrame.transpose(new_df)\n", - "new_df.columns = year_list\n", - "\n", - "new_df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "57929d23-e3d7-4955-92d1-6fa388eb774d", - "_uuid": "605f908af9ff88120fce2a2b59160816fcdcfa67" - }, - "source": [ - "Perfect! Now, we will do some feature engineering.\n", - "\n", - "# First, a new column which indicates mean produce of each state over the given years. Second, a ranking column which ranks countries on the basis of mean produce." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "_cell_guid": "ab91a322-0cb9-4edf-b5a2-cde82a237824", - "_uuid": "979f875019abef3ed85af75e000fe59d1de5a381" - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Y1961Y1962Y1963Y1964Y1965Y1966Y1967Y1968Y1969Y1970...Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013Mean_ProduceRank
Afghanistan9481.09414.09194.010170.010473.010169.011289.011508.011815.010454.0...18317.019248.019381.020661.021030.021100.022706.023007.013003.05660469.0
Albania1706.01749.01767.01889.01884.01995.02046.02169.02230.02395.0...6911.06744.07168.07316.07907.08114.08221.08271.04475.509434104.0
Algeria7488.07235.06861.07255.07509.07536.07986.08839.09003.09355.0...51067.049933.050916.057505.060071.065852.069365.072161.028879.49056638.0
Angola4834.04775.05240.05286.05527.05677.05833.05685.06219.06460.0...28247.029877.032053.036985.038400.040573.038064.048639.013321.05660468.0
Antigua and Barbuda92.094.0105.095.084.073.064.059.068.077.0...110.0122.0115.0114.0115.0118.0113.0119.083.886792172.0
\n", - "

5 rows × 55 columns

\n", - "
" - ], - "text/plain": [ - " Y1961 Y1962 Y1963 Y1964 Y1965 Y1966 \\\n", - "Afghanistan 9481.0 9414.0 9194.0 10170.0 10473.0 10169.0 \n", - "Albania 1706.0 1749.0 1767.0 1889.0 1884.0 1995.0 \n", - "Algeria 7488.0 7235.0 6861.0 7255.0 7509.0 7536.0 \n", - "Angola 4834.0 4775.0 5240.0 5286.0 5527.0 5677.0 \n", - "Antigua and Barbuda 92.0 94.0 105.0 95.0 84.0 73.0 \n", - "\n", - " Y1967 Y1968 Y1969 Y1970 ... Y2006 \\\n", - "Afghanistan 11289.0 11508.0 11815.0 10454.0 ... 18317.0 \n", - "Albania 2046.0 2169.0 2230.0 2395.0 ... 6911.0 \n", - "Algeria 7986.0 8839.0 9003.0 9355.0 ... 51067.0 \n", - "Angola 5833.0 5685.0 6219.0 6460.0 ... 28247.0 \n", - "Antigua and Barbuda 64.0 59.0 68.0 77.0 ... 110.0 \n", - "\n", - " Y2007 Y2008 Y2009 Y2010 Y2011 Y2012 \\\n", - "Afghanistan 19248.0 19381.0 20661.0 21030.0 21100.0 22706.0 \n", - "Albania 6744.0 7168.0 7316.0 7907.0 8114.0 8221.0 \n", - "Algeria 49933.0 50916.0 57505.0 60071.0 65852.0 69365.0 \n", - "Angola 29877.0 32053.0 36985.0 38400.0 40573.0 38064.0 \n", - "Antigua and Barbuda 122.0 115.0 114.0 115.0 118.0 113.0 \n", - "\n", - " Y2013 Mean_Produce Rank \n", - "Afghanistan 23007.0 13003.056604 69.0 \n", - "Albania 8271.0 4475.509434 104.0 \n", - "Algeria 72161.0 28879.490566 38.0 \n", - "Angola 48639.0 13321.056604 68.0 \n", - "Antigua and Barbuda 119.0 83.886792 172.0 \n", - "\n", - "[5 rows x 55 columns]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mean_produce = []\n", - "for i in range(174):\n", - " mean_produce.append(new_df.iloc[i,:].values.mean())\n", - "new_df['Mean_Produce'] = mean_produce\n", - "\n", - "new_df['Rank'] = new_df['Mean_Produce'].rank(ascending=False)\n", - "\n", - "new_df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "6f7c4fb7-1475-439f-9929-4cf4b29d8de7", - "_uuid": "da6c9c98eaff45edba1179103ae539bbfbe9753b" - }, - "source": [ - "Now, we create another dataframe with items and their total production each year from 1961 to 2013" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "_cell_guid": "bfd692bc-dce4-4870-9ab9-9775cf69a87f", - "_uuid": "9e11017d381f175eee714643bc5fa763600aaa0b" - }, - "outputs": [], - "source": [ - "item_list = list(df['Item'].unique())\n", - "\n", - "item_df = pd.DataFrame()\n", - "item_df['Item_Name'] = item_list\n", - "\n", - "for yr in year_list:\n", - " item_produce = []\n", - " for it in item_list:\n", - " item_produce.append(df[yr][df['Item']==it].sum())\n", - " item_df[yr] = item_produce\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "_cell_guid": "3b7ed0c2-6140-4285-861c-d0cd2324a1f5", - "_uuid": "cb4641df5ce90f516f88c536e8a6c6870c5b4f65" - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Item_NameY1961Y1962Y1963Y1964Y1965Y1966Y1967Y1968Y1969...Y2004Y2005Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013
0Wheat and products138829.0144643.0147325.0156273.0168822.0169832.0171469.0179530.0189658.0...527394.0532263.0537279.0529271.0562239.0557245.0549926.0578179.0576597587492
1Rice (Milled Equivalent)122700.0131842.0139507.0148304.0150056.0155583.0158587.0164614.0167922.0...361107.0366025.0372629.0378698.0389708.0394221.0398559.0404152.0406787410880
2Barley and products46180.048915.051642.054184.054945.055463.056424.060455.065501.0...102055.097185.0100981.093310.098209.099135.092563.092570.08876699452
3Maize and products168039.0168305.0172905.0175468.0190304.0200860.0213050.0215613.0221953.0...545024.0549036.0543280.0573892.0592231.0557940.0584337.0603297.0608730671300
4Millet and products19075.019019.019740.020353.018377.020860.022997.021785.023966.0...25789.025496.025997.026750.026373.024575.027039.025740.02610526346
\n", - "

5 rows × 54 columns

\n", - "
" - ], - "text/plain": [ - " Item_Name Y1961 Y1962 Y1963 Y1964 Y1965 \\\n", - "0 Wheat and products 138829.0 144643.0 147325.0 156273.0 168822.0 \n", - "1 Rice (Milled Equivalent) 122700.0 131842.0 139507.0 148304.0 150056.0 \n", - "2 Barley and products 46180.0 48915.0 51642.0 54184.0 54945.0 \n", - "3 Maize and products 168039.0 168305.0 172905.0 175468.0 190304.0 \n", - "4 Millet and products 19075.0 19019.0 19740.0 20353.0 18377.0 \n", - "\n", - " Y1966 Y1967 Y1968 Y1969 ... Y2004 Y2005 \\\n", - "0 169832.0 171469.0 179530.0 189658.0 ... 527394.0 532263.0 \n", - "1 155583.0 158587.0 164614.0 167922.0 ... 361107.0 366025.0 \n", - "2 55463.0 56424.0 60455.0 65501.0 ... 102055.0 97185.0 \n", - "3 200860.0 213050.0 215613.0 221953.0 ... 545024.0 549036.0 \n", - "4 20860.0 22997.0 21785.0 23966.0 ... 25789.0 25496.0 \n", - "\n", - " Y2006 Y2007 Y2008 Y2009 Y2010 Y2011 Y2012 Y2013 \n", - "0 537279.0 529271.0 562239.0 557245.0 549926.0 578179.0 576597 587492 \n", - "1 372629.0 378698.0 389708.0 394221.0 398559.0 404152.0 406787 410880 \n", - "2 100981.0 93310.0 98209.0 99135.0 92563.0 92570.0 88766 99452 \n", - "3 543280.0 573892.0 592231.0 557940.0 584337.0 603297.0 608730 671300 \n", - "4 25997.0 26750.0 26373.0 24575.0 27039.0 25740.0 26105 26346 \n", - "\n", - "[5 rows x 54 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item_df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "3fa01e1f-bedd-431b-90c3-8d7d70545f34", - "_uuid": "56a647293f1c1aba7c184f249021e008a4d5a8f2" - }, - "source": [ - "# Some more feature engineering\n", - "\n", - "This time, we will use the new features to get some good conclusions.\n", - "\n", - "# 1. Total amount of item produced from 1961 to 2013\n", - "# 2. Providing a rank to the items to know the most produced item" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "_cell_guid": "3a6bb102-6749-4818-860d-59aaad6de07f", - "_uuid": "9e816786e7a161227ae72d164b25c0029e01e5b4", - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Item_NameY1961Y1962Y1963Y1964Y1965Y1966Y1967Y1968Y1969...Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013SumProduction_Rank
0Wheat and products138829.0144643.0147325.0156273.0168822.0169832.0171469.0179530.0189658.0...537279.0529271.0562239.0557245.0549926.0578179.057659758749219194671.06.0
1Rice (Milled Equivalent)122700.0131842.0139507.0148304.0150056.0155583.0158587.0164614.0167922.0...372629.0378698.0389708.0394221.0398559.0404152.040678741088014475448.08.0
2Barley and products46180.048915.051642.054184.054945.055463.056424.060455.065501.0...100981.093310.098209.099135.092563.092570.088766994524442742.020.0
3Maize and products168039.0168305.0172905.0175468.0190304.0200860.0213050.0215613.0221953.0...543280.0573892.0592231.0557940.0584337.0603297.060873067130019960640.05.0
4Millet and products19075.019019.019740.020353.018377.020860.022997.021785.023966.0...25997.026750.026373.024575.027039.025740.026105263461225400.038.0
\n", - "

5 rows × 56 columns

\n", - "
" - ], - "text/plain": [ - " Item_Name Y1961 Y1962 Y1963 Y1964 Y1965 \\\n", - "0 Wheat and products 138829.0 144643.0 147325.0 156273.0 168822.0 \n", - "1 Rice (Milled Equivalent) 122700.0 131842.0 139507.0 148304.0 150056.0 \n", - "2 Barley and products 46180.0 48915.0 51642.0 54184.0 54945.0 \n", - "3 Maize and products 168039.0 168305.0 172905.0 175468.0 190304.0 \n", - "4 Millet and products 19075.0 19019.0 19740.0 20353.0 18377.0 \n", - "\n", - " Y1966 Y1967 Y1968 Y1969 ... Y2006 \\\n", - "0 169832.0 171469.0 179530.0 189658.0 ... 537279.0 \n", - "1 155583.0 158587.0 164614.0 167922.0 ... 372629.0 \n", - "2 55463.0 56424.0 60455.0 65501.0 ... 100981.0 \n", - "3 200860.0 213050.0 215613.0 221953.0 ... 543280.0 \n", - "4 20860.0 22997.0 21785.0 23966.0 ... 25997.0 \n", - "\n", - " Y2007 Y2008 Y2009 Y2010 Y2011 Y2012 Y2013 \\\n", - "0 529271.0 562239.0 557245.0 549926.0 578179.0 576597 587492 \n", - "1 378698.0 389708.0 394221.0 398559.0 404152.0 406787 410880 \n", - "2 93310.0 98209.0 99135.0 92563.0 92570.0 88766 99452 \n", - "3 573892.0 592231.0 557940.0 584337.0 603297.0 608730 671300 \n", - "4 26750.0 26373.0 24575.0 27039.0 25740.0 26105 26346 \n", - "\n", - " Sum Production_Rank \n", - "0 19194671.0 6.0 \n", - "1 14475448.0 8.0 \n", - "2 4442742.0 20.0 \n", - "3 19960640.0 5.0 \n", - "4 1225400.0 38.0 \n", - "\n", - "[5 rows x 56 columns]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sum_col = []\n", - "for i in range(115):\n", - " sum_col.append(item_df.iloc[i,1:].values.sum())\n", - "item_df['Sum'] = sum_col\n", - "item_df['Production_Rank'] = item_df['Sum'].rank(ascending=False)\n", - "\n", - "item_df.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "7e20740c-565b-4969-a52e-d986e462b750", - "_uuid": "f483c9add5f6af9af9162b5425f6d65eb1c5f4aa" - }, - "source": [ - "# Now, we find the most produced food items in the last half-century" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "_cell_guid": "3130fe83-404c-4b3c-addc-560b2e2f32bf", - "_uuid": "0403e9ab2e13587588e3a30d64b8b6638571d3d5" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "56 Cereals - Excluding Beer\n", - "65 Fruits - Excluding Wine\n", - "3 Maize and products\n", - "53 Milk - Excluding Butter\n", - "6 Potatoes and products\n", - "1 Rice (Milled Equivalent)\n", - "57 Starchy Roots\n", - "64 Vegetables\n", - "27 Vegetables, Other\n", - "0 Wheat and products\n", - "Name: Item_Name, dtype: object" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item_df['Item_Name'][item_df['Production_Rank'] < 11.0].sort_values()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "b6212fed-588b-426e-9271-6d857cd6aacb", - "_uuid": "e2c83f4c851b755ea6cf19f1bca168e705bd4edd" - }, - "source": [ - "So, cereals, fruits and maize are the most produced items in the last 50 years\n", - "\n", - "# Food and feed plot for most produced items " - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "_cell_guid": "493f9940-1762-4718-acb4-fba5c4c73f4b", - "_uuid": "f8454c5200bdeb3995b9a0ada3deb5ca1c31f181" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/seaborn/categorical.py:3666: UserWarning: The `factorplot` function has been renamed to `catplot`. The original name will be removed in a future release. Please update your code. Note that the default `kind` in `factorplot` (`'point'`) has changed `'strip'` in `catplot`.\n", - " warnings.warn(msg)\n", - "/anaconda3/lib/python3.7/site-packages/seaborn/categorical.py:3672: UserWarning: The `size` paramter has been renamed to `height`; please update your code.\n", - " warnings.warn(msg, UserWarning)\n" - ] - }, - { - "data": { - "image/png": "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AAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAAAAC0JY4BAAAA0JY4BgAAAEBb4hgAAAAAbYljAAAAALQljgEAAADQljgGAAAAQFviGAAAAABtiWMAAAAAtCWOAQAAANCWOAYAAABAW+IYAAAAAG2JYwAA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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "sns.factorplot(\"Item\", data=df[(df['Item']=='Wheat and products') | (df['Item']=='Rice (Milled Equivalent)') | (df['Item']=='Maize and products') | (df['Item']=='Potatoes and products') | (df['Item']=='Vegetables, Other') | (df['Item']=='Milk - Excluding Butter') | (df['Item']=='Cereals - Excluding Beer') | (df['Item']=='Starchy Roots') | (df['Item']=='Vegetables') | (df['Item']=='Fruits - Excluding Wine')], kind=\"count\", hue=\"Element\", size=20, aspect=.8)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "45dda825-49a0-41ab-9ebd-eaa609aac986", - "_uuid": "ce5b2d38ff24ea08da632c4e2773dbd0bd026b9d", - "collapsed": true - }, - "source": [ - "# Now, we plot a heatmap of correlation of produce in difference years" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "_cell_guid": "b1bab0ec-6615-452c-8d06-a81d4f2ae252", - "_uuid": "a2ed2aae2364810ce640648cf50880adcf2cdcc4" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "year_df = df.iloc[:,10:]\n", - "fig, ax = plt.subplots(figsize=(16,10))\n", - "sns.heatmap(year_df.corr(), ax=ax)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "43e1af94-ba07-4b95-8da3-1d774db940cd", - "_uuid": "70d2b0a7db9b8a5535b3c5b3c2eb927b904bf6d3" - }, - "source": [ - "So, we gather that a given year's production is more similar to its immediate previous and immediate following years." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "_cell_guid": "58cde27d-5ddc-4ebe-a8e1-80a8257f44c1", - "_uuid": "6f48b52c09ea6a207644044cace5a88c983bf316" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/scipy/stats/stats.py:1713: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.\n", - " return np.add.reduce(sorted[indexer] * weights, axis=axis) / sumval\n" - ] - }, - { - "data": { - "image/png": "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/tndf2FmW4AnzOw24B3g48H+TwK/A+wGuoBbANy9xcz+GtgS7Hevu7cErz8HPArUAD8PvgC+foJziIiIjNgDG/cQCRu1lZlfn7WVFXTFkzywcQ8rlmi6u5SPvAWI7r4HOHeA9qPAygHaHfj8CY71CPDIAO1bgXOGeg4REZHRaGztYnJNJKutJhJmf2tXgXokkh+qpCIiIjJE86bU0p1IZbV1J1LMnVJboB6J5IcCRBERkSG686pFJFJOVzyJe+Z7IuXcedWiQndNJKcUIIqIiAzRiiUN3Hvt2TTUV3O8O0FDfTX3Xnu25h9K2cl3HkQREZGysmJJgwJCKXsaQRQRERGRLAoQRURERCSLAkQRERERyaIAUURERESyKEAUERERkSwKEEVEREQkiwJEEREREcmiAFFEREREsihAFBEREZEsChBFREREJIsCRBERERHJogBRRERERLIoQBQRERGRLAoQRURERCSLAkQRERERyaIAUURERESyKEAUERERkSwKEEVEREQkiwJEEREREclSUegOiIiIFNqGHU08sHEPja1dzJtSy51XLWLFkoZCd0ukYDSCKCIi49qGHU18ec02mtqjTK6J0NQe5ctrtrFhR1OhuyZSMAoQRURkXHtg4x4iYaO2sgKzzPdI2Hhg455Cd02kYBQgiojIuNbY2kVNJJzVVhMJs7+1q0A9Eik8BYgiIjKuzZtSS3cildXWnUgxd0ptgXokMjh3z/s5FCCKiMi4dudVi0iknK54EvfM90TKufOqRYXumsj7xJNpDh6P5v08ChBFRGRcW7GkgXuvPZuG+mqOdydoqK/m3mvP1ipmKTodsSQHj3WTSKbzfi6luRERkXFvxZIGBYRStNydo51x2roTAITM8n5OBYgiIiIiRSqRStPUHiPWb55svilAFBERESlCXfEkze0xUun8L0rpL+9zEM0sbGa/NrOfBe8XmtkLZrbLzH5oZpVBe1XwfnewfUGfY3wxaH/TzD7Sp31V0LbbzO7p0z7gOURERERKQWtnnHePRwsSHMLYLFK5C9je5/03gG+5+2KgFbgtaL8NaHX304FvBfthZmcBNwBnA6uA/x0EnWHgH4GPAmcBNwb7DnYOERERkaKVSjuHjnfT2hUvaD/yGiCa2Vzgd4GHgvcGXA38ONjlMeD64PV1wXuC7SuD/a8DVrt7zN33AruBi4Ov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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "f, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex='col', sharey='row', figsize=(10,10))\n", - "ax1.set(xlabel='Y1968', ylabel='Y1961')\n", - "ax2.set(xlabel='Y1968', ylabel='Y1963')\n", - "ax3.set(xlabel='Y1968', ylabel='Y1986')\n", - "ax4.set(xlabel='Y1968', ylabel='Y2013')\n", - "sns.jointplot(x=\"Y1968\", y=\"Y1961\", data=df, kind=\"reg\", ax=ax1)\n", - "sns.jointplot(x=\"Y1968\", y=\"Y1963\", data=df, kind=\"reg\", ax=ax2)\n", - "sns.jointplot(x=\"Y1968\", y=\"Y1986\", data=df, kind=\"reg\", ax=ax3)\n", - "sns.jointplot(x=\"Y1968\", y=\"Y2013\", data=df, kind=\"reg\", ax=ax4)\n", - "plt.close(2)\n", - "plt.close(3)\n", - "plt.close(4)\n", - "plt.close(5)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "8a297a06-977f-4ff7-a9ad-c7e8804930a8", - "_uuid": "6b738ce8b15a764fab90fac96f9534f94c14342e" - }, - "source": [ - "# Heatmap of production of food items over years\n", - "\n", - "This will detect the items whose production has drastically increased over the years" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "_cell_guid": "588cebd9-e97c-460d-8ed5-e663ac293711", - "_uuid": "16ce47d43a3038874a74d8bbb9a2e26f6ee54437" - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "new_item_df = item_df.drop([\"Item_Name\",\"Sum\",\"Production_Rank\"], axis = 1)\n", - "fig, ax = plt.subplots(figsize=(12,24))\n", - "sns.heatmap(new_item_df,ax=ax)\n", - "ax.set_yticklabels(item_df.Item_Name.values[::-1])\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "825620f9-7ab5-4fe2-9529-c4f1a300138e", - "_uuid": "5c42595537332ea71089d8c3dc041d3bf7d41b55" - }, - "source": [ - "There is considerable growth in production of Palmkernel oil, Meat/Aquatic animals, ricebran oil, cottonseed, seafood, offals, roots, poultry meat, mutton, bear, cocoa, coffee and soyabean oil.\n", - "There has been exceptional growth in production of onions, cream, sugar crops, treenuts, butter/ghee and to some extent starchy roots." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "80428f51-2fd4-468d-9530-9279215b4218", - "_uuid": "4c9bb27cd76099c5348243a99448c509ef0c5ded" - }, - "source": [ - "Now, we look at clustering." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "a3f1db3a-1b82-4e42-8e7d-f1a26915693b", - "_uuid": "da167de5a5b92e164fc6993b32ebbfab4ef9a6e3", - "collapsed": true - }, - "source": [ - "# What is clustering?\n", - "Cluster analysis or clustering is the task of grouping a set of objects in such a way that objects in the same group (called a cluster) are more similar (in some sense) to each other than to those in other groups (clusters). It is a main task of exploratory data mining, and a common technique for statistical data analysis, used in many fields, including machine learning, pattern recognition, image analysis, information retrieval, bioinformatics, data compression, and computer graphics." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "136315a0-b37d-4d89-bd0d-037727062c34", - "_uuid": "04ab802ec92eaf6a27706f2008933dcf3865855a" - }, - "source": [ - "# Today, we will form clusters to classify countries based on productivity scale" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "27ba0b5d-c57e-485d-9588-017e16fe1904", - "_uuid": "659afdada04e8854765b5e7208394915b30f859a" - }, - "source": [ - "For this, we will use k-means clustering algorithm.\n", - "# K-means clustering\n", - "(Source [Wikipedia](https://en.wikipedia.org/wiki/K-means_clustering#Standard_algorithm) )\n", - "![http://gdurl.com/5BbP](http://gdurl.com/5BbP)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "7aeb3175-33bd-4f49-903a-57d43380e90e", - "_uuid": "6b0b4881e623ed3c133b68b98e6fb6755e18fd78" - }, - "source": [ - "This is the data we will use." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "_cell_guid": "a5b99ea8-975f-4467-9895-bffe1db876eb", - "_uuid": "57aba4000bfc422e848b14ad24b02a570d6c0554" - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Y1961Y1962Y1963Y1964Y1965Y1966Y1967Y1968Y1969Y1970...Y2006Y2007Y2008Y2009Y2010Y2011Y2012Y2013Mean_ProduceRank
Afghanistan9481.09414.09194.010170.010473.010169.011289.011508.011815.010454.0...18317.019248.019381.020661.021030.021100.022706.023007.013003.05660469.0
Albania1706.01749.01767.01889.01884.01995.02046.02169.02230.02395.0...6911.06744.07168.07316.07907.08114.08221.08271.04475.509434104.0
Algeria7488.07235.06861.07255.07509.07536.07986.08839.09003.09355.0...51067.049933.050916.057505.060071.065852.069365.072161.028879.49056638.0
Angola4834.04775.05240.05286.05527.05677.05833.05685.06219.06460.0...28247.029877.032053.036985.038400.040573.038064.048639.013321.05660468.0
Antigua and Barbuda92.094.0105.095.084.073.064.059.068.077.0...110.0122.0115.0114.0115.0118.0113.0119.083.886792172.0
\n", - "

5 rows × 55 columns

\n", - "
" - ], - "text/plain": [ - " Y1961 Y1962 Y1963 Y1964 Y1965 Y1966 \\\n", - "Afghanistan 9481.0 9414.0 9194.0 10170.0 10473.0 10169.0 \n", - "Albania 1706.0 1749.0 1767.0 1889.0 1884.0 1995.0 \n", - "Algeria 7488.0 7235.0 6861.0 7255.0 7509.0 7536.0 \n", - "Angola 4834.0 4775.0 5240.0 5286.0 5527.0 5677.0 \n", - "Antigua and Barbuda 92.0 94.0 105.0 95.0 84.0 73.0 \n", - "\n", - " Y1967 Y1968 Y1969 Y1970 ... Y2006 \\\n", - "Afghanistan 11289.0 11508.0 11815.0 10454.0 ... 18317.0 \n", - "Albania 2046.0 2169.0 2230.0 2395.0 ... 6911.0 \n", - "Algeria 7986.0 8839.0 9003.0 9355.0 ... 51067.0 \n", - "Angola 5833.0 5685.0 6219.0 6460.0 ... 28247.0 \n", - "Antigua and Barbuda 64.0 59.0 68.0 77.0 ... 110.0 \n", - "\n", - " Y2007 Y2008 Y2009 Y2010 Y2011 Y2012 \\\n", - "Afghanistan 19248.0 19381.0 20661.0 21030.0 21100.0 22706.0 \n", - "Albania 6744.0 7168.0 7316.0 7907.0 8114.0 8221.0 \n", - "Algeria 49933.0 50916.0 57505.0 60071.0 65852.0 69365.0 \n", - "Angola 29877.0 32053.0 36985.0 38400.0 40573.0 38064.0 \n", - "Antigua and Barbuda 122.0 115.0 114.0 115.0 118.0 113.0 \n", - "\n", - " Y2013 Mean_Produce Rank \n", - "Afghanistan 23007.0 13003.056604 69.0 \n", - "Albania 8271.0 4475.509434 104.0 \n", - "Algeria 72161.0 28879.490566 38.0 \n", - "Angola 48639.0 13321.056604 68.0 \n", - "Antigua and Barbuda 119.0 83.886792 172.0 \n", - "\n", - "[5 rows x 55 columns]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_df.head()" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "_cell_guid": "66964df2-892d-4e55-a4b1-f94d10e4c7dd", - "_uuid": "19bdd89a3ad9df962959ad6b996946f6f3916d58" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:4: FutureWarning: convert_objects is deprecated. To re-infer data dtypes for object columns, use DataFrame.infer_objects()\n", - "For all other conversions use the data-type specific converters pd.to_datetime, pd.to_timedelta and pd.to_numeric.\n", - " after removing the cwd from sys.path.\n" - ] - } - ], - "source": [ - "X = new_df.iloc[:,:-2].values\n", - "\n", - "X = pd.DataFrame(X)\n", - "X = X.convert_objects(convert_numeric=True)\n", - "X.columns = year_list" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "461e5bcc-0101-4ea1-ae13-20600f883929", - "_uuid": "0d3e50235c9505ebc255053d4a5aae547fc17d8d" - }, - "source": [ - "# Elbow method to select number of clusters\n", - "This method looks at the percentage of variance explained as a function of the number of clusters: One should choose a number of clusters so that adding another cluster doesn't give much better modeling of the data. More precisely, if one plots the percentage of variance explained by the clusters against the number of clusters, the first clusters will add much information (explain a lot of variance), but at some point the marginal gain will drop, giving an angle in the graph. The number of clusters is chosen at this point, hence the \"elbow criterion\". This \"elbow\" cannot always be unambiguously identified. Percentage of variance explained is the ratio of the between-group variance to the total variance, also known as an F-test. A slight variation of this method plots the curvature of the within group variance.\n", - "# Basically, number of clusters = the x-axis value of the point that is the corner of the \"elbow\"(the plot looks often looks like an elbow)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "_cell_guid": "06271223-bd32-48ac-a373-6c1e6bbf7c7b", - "_uuid": "c57d7277510a8c11fdc3d311e4d8a22539617ed9" - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "from sklearn.cluster import KMeans\n", - "wcss = []\n", - "for i in range(1,11):\n", - " kmeans = KMeans(n_clusters=i,init='k-means++',max_iter=300,n_init=10,random_state=0)\n", - " kmeans.fit(X)\n", - " wcss.append(kmeans.inertia_)\n", - "plt.plot(range(1,11),wcss)\n", - "plt.title('The Elbow Method')\n", - "plt.xlabel('Number of clusters')\n", - "plt.ylabel('WCSS')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "ad4bc40a-9540-497d-95e3-3fee6088ea95", - "_uuid": "6450dd1c3d7a8114931dc358d2f09a0424b52fd7" - }, - "source": [ - "As the elbow corner coincides with x=2, we will have to form **2 clusters**. Personally, I would have liked to select 3 to 4 clusters. But trust me, only selecting 2 clusters can lead to best results.\n", - "Now, we apply k-means algorithm." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "_cell_guid": "eed3f672-e089-4dbb-aad8-b9618967abf3", - "_uuid": "d92d758ee7213ddcd84e9b8b2f61c9e260ed6ba2" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:4: FutureWarning: Method .as_matrix will be removed in a future version. Use .values instead.\n", - " after removing the cwd from sys.path.\n" - ] - } - ], - "source": [ - "kmeans = KMeans(n_clusters=2,init='k-means++',max_iter=300,n_init=10,random_state=0) \n", - "y_kmeans = kmeans.fit_predict(X)\n", - "\n", - "X = X.as_matrix(columns=None)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "ef07bd6d-679d-4375-b7b3-abeca3421e37", - "_uuid": "6f93a4bd3f17427f4b2dbe08af8e015a1e4a2f89" - }, - "source": [ - "Now, let's visualize the results." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "_cell_guid": "5a7fe139-13df-453b-8c16-891929bc595e", - "_uuid": "a57e0a38f4c0f0385be75fd9f71d4a2d8213aea3" - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.scatter(X[y_kmeans == 0, 0], X[y_kmeans == 0,1],s=100,c='red',label='Others')\n", - "plt.scatter(X[y_kmeans == 1, 0], X[y_kmeans == 1,1],s=100,c='blue',label='China(mainland),USA,India')\n", - "plt.scatter(kmeans.cluster_centers_[:,0],kmeans.cluster_centers_[:,1],s=300,c='yellow',label='Centroids')\n", - "plt.title('Clusters of countries by Productivity')\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "923d4536-2bce-4b99-b98a-33b801a56a8b", - "_uuid": "fe531e8c41eec0eb5dc52a9890871841f5d27211" - }, - "source": [ - "So, the blue cluster represents China(Mainland), USA and India while the red cluster represents all the other countries.\n", - "This result was highly probable. Just take a look at the plot of cell 3 above. See how China, USA and India stand out. That has been observed here in clustering too.\n", - "\n", - "You should try this algorithm for 3 or 4 clusters. Looking at the distribution, you will realise why 2 clusters is the best choice for the given data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "_cell_guid": "6dee7acb-0f08-4ae1-85b4-f4704026694a", - "_uuid": "179a1ede21ae330664a0b7c63e36574acdc0428c" - }, - "source": [ - "This is not the end! More is yet to come." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Now, lets try to predict the production using regression for 2020. We will predict the production for USA,India and Pakistan.**\n" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, - { - "ename": "ValueError", - "evalue": "Expected 2D array, got scalar array instead:\narray=2020.\nReshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 27\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mplot\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreset\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mpredictions\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 29\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpredict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2020\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 30\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0mdf\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mArea\u001b[0m\u001b[0;34m==\u001b[0m\u001b[0;34m'India'\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m&\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mElement\u001b[0m\u001b[0;34m==\u001b[0m\u001b[0;34m'Food'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'Y1961'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmean\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/anaconda3/lib/python3.7/site-packages/sklearn/linear_model/base.py\u001b[0m in \u001b[0;36mpredict\u001b[0;34m(self, X)\u001b[0m\n\u001b[1;32m 211\u001b[0m \u001b[0mReturns\u001b[0m \u001b[0mpredicted\u001b[0m \u001b[0mvalues\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 212\u001b[0m \"\"\"\n\u001b[0;32m--> 213\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_decision_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mX\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 214\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 215\u001b[0m \u001b[0m_preprocess_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mstaticmethod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_preprocess_data\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/anaconda3/lib/python3.7/site-packages/sklearn/linear_model/base.py\u001b[0m in \u001b[0;36m_decision_function\u001b[0;34m(self, X)\u001b[0m\n\u001b[1;32m 194\u001b[0m \u001b[0mcheck_is_fitted\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"coef_\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 195\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 196\u001b[0;31m \u001b[0mX\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcheck_array\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mX\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maccept_sparse\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'csr'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'csc'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'coo'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 197\u001b[0m return safe_sparse_dot(X, self.coef_.T,\n\u001b[1;32m 198\u001b[0m dense_output=True) + self.intercept_\n", - "\u001b[0;32m/anaconda3/lib/python3.7/site-packages/sklearn/utils/validation.py\u001b[0m in \u001b[0;36mcheck_array\u001b[0;34m(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, warn_on_dtype, estimator)\u001b[0m\n\u001b[1;32m 543\u001b[0m \u001b[0;34m\"Reshape your data either using array.reshape(-1, 1) if \"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 544\u001b[0m \u001b[0;34m\"your data has a single feature or array.reshape(1, -1) \"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 545\u001b[0;31m \"if it contains a single sample.\".format(array))\n\u001b[0m\u001b[1;32m 546\u001b[0m \u001b[0;31m# If input is 1D raise error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 547\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0marray\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mndim\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: Expected 2D array, got scalar array instead:\narray=2020.\nReshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample." - ] - } - ], - "source": [ - "india_list=[]\n", - "year_list = list(df.iloc[:,10:].columns)\n", - "for i in year_list:\n", - " x=df[(df.Area=='India') & (df.Element=='Food')][i].mean()\n", - " india_list.append(x) \n", - "\n", - "reset=[]\n", - "for i in year_list:\n", - " reset.append(int(i[1:]))\n", - "\n", - "\n", - "reset=np.array(reset)\n", - "reset=reset.reshape(-1,1)\n", - "\n", - "\n", - "india_list=np.array(india_list)\n", - "india_list=india_list.reshape(-1,1)\n", - "\n", - "\n", - "reg = LinearRegression()\n", - "reg.fit(reset,india_list)\n", - "predictions = reg.predict(reset)\n", - "plt.title(\"India\")\n", - "plt.xlabel(\"Year\")\n", - "plt.ylabel(\"Production\")\n", - "plt.scatter(reset,india_list)\n", - "plt.plot(reset,predictions)\n", - "plt.show()\n", - "print(reg.predict(2020))\n", - "\n", - "df[(df.Area=='India') & (df.Element=='Food')]['Y1961'].mean()\n", - "\n", - "df[(df.Area=='Pakistan') & (df.Element=='Food')]\n", - "\n", - "Pak_list=[]\n", - "year_list = list(df.iloc[:,10:].columns)\n", - "for i in year_list:\n", - " yx=df[(df.Area=='Pakistan') & (df.Element=='Food')][i].mean()\n", - " Pak_list.append(yx) \n", - "\n", - "Pak_list=np.array(Pak_list)\n", - "Pak_list=Pak_list.reshape(-1,1)\n", - "Pak_list\n", - "reg = LinearRegression()\n", - "reg.fit(reset,Pak_list)\n", - "predictions = reg.predict(reset)\n", - "plt.title(\"Pakistan\")\n", - "plt.xlabel(\"Year\")\n", - "plt.ylabel(\"Production\")\n", - "plt.scatter(reset,Pak_list)\n", - "plt.plot(reset,predictions)\n", - "plt.show()\n", - "print(reg.predict(2020))\n", - "\n", - "\n", - "\n", - "usa_list=[]\n", - "year_list = list(df.iloc[:,10:].columns)\n", - "for i in year_list:\n", - " xu=df[(df.Area=='United States of America') & (df.Element=='Food')][i].mean()\n", - " usa_list.append(xu)\n", - "\n", - "usa_list=np.array(usa_list)\n", - "usa_list=india_list.reshape(-1,1)\n", - "\n", - "\n", - "reg = LinearRegression()\n", - "reg.fit(reset,usa_list)\n", - "predictions = reg.predict(reset)\n", - "plt.title(\"USA\")\n", - "plt.xlabel(\"Year\")\n", - "plt.ylabel(\"Production\")\n", - "plt.scatter(reset,usa_list)\n", - "plt.plot(reset,predictions)\n", - "plt.show()\n", - "print(reg.predict(2020))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/other/activity_selection.py b/other/activity_selection.py new file mode 100644 index 000000000000..4e8e6c78e3f5 --- /dev/null +++ b/other/activity_selection.py @@ -0,0 +1,45 @@ +"""The following implementation assumes that the activities +are already sorted according to their finish time""" + +"""Prints a maximum set of activities that can be done by a +single person, one at a time""" +# n --> Total number of activities +# start[]--> An array that contains start time of all activities +# finish[] --> An array that contains finish time of all activities + + +def printMaxActivities(start, finish): + """ + >>> start = [1, 3, 0, 5, 8, 5] + >>> finish = [2, 4, 6, 7, 9, 9] + >>> printMaxActivities(start, finish) + The following activities are selected: + 0 1 3 4 + """ + n = len(finish) + print("The following activities are selected:") + + # The first activity is always selected + i = 0 + print(i, end=" ") + + # Consider rest of the activities + for j in range(n): + + # If this activity has start time greater than + # or equal to the finish time of previously + # selected activity, then select it + if start[j] >= finish[i]: + print(j, end=" ") + i = j + + +# Driver program to test above function +start = [1, 3, 0, 5, 8, 5] +finish = [2, 4, 6, 7, 9, 9] +printMaxActivities(start, finish) + +""" +The following activities are selected: +0 1 3 4 +""" diff --git a/other/anagrams.py b/other/anagrams.py index 29b34fbdc5d3..471413194498 100644 --- a/other/anagrams.py +++ b/other/anagrams.py @@ -1,30 +1,32 @@ -from __future__ import print_function import collections, pprint, time, os start_time = time.time() -print('creating word list...') +print("creating word list...") path = os.path.split(os.path.realpath(__file__)) -with open(path[0] + '/words') as f: - word_list = sorted(list(set([word.strip().lower() for word in f]))) +with open(path[0] + "/words") as f: + word_list = sorted(list({word.strip().lower() for word in f})) + def signature(word): - return ''.join(sorted(word)) + return "".join(sorted(word)) + word_bysig = collections.defaultdict(list) for word in word_list: word_bysig[signature(word)].append(word) -def anagram(myword): - return word_bysig[signature(myword)] -print('finding anagrams...') -all_anagrams = {word: anagram(word) - for word in word_list if len(anagram(word)) > 1} +def anagram(my_word): + return word_bysig[signature(my_word)] + + +print("finding anagrams...") +all_anagrams = {word: anagram(word) for word in word_list if len(anagram(word)) > 1} -print('writing anagrams to file...') -with open('anagrams.txt', 'w') as file: - file.write('all_anagrams = ') +print("writing anagrams to file...") +with open("anagrams.txt", "w") as file: + file.write("all_anagrams = ") file.write(pprint.pformat(all_anagrams)) total_time = round(time.time() - start_time, 2) -print(('Done [', total_time, 'seconds ]')) +print(("Done [", total_time, "seconds ]")) diff --git a/other/autocomplete_using_trie.py b/other/autocomplete_using_trie.py new file mode 100644 index 000000000000..8aa0dc223680 --- /dev/null +++ b/other/autocomplete_using_trie.py @@ -0,0 +1,64 @@ +END = "#" + + +class Trie: + def __init__(self): + self._trie = {} + + def insert_word(self, text): + trie = self._trie + for char in text: + if char not in trie: + trie[char] = {} + trie = trie[char] + trie[END] = True + + def find_word(self, prefix): + trie = self._trie + for char in prefix: + if char in trie: + trie = trie[char] + else: + return [] + return self._elements(trie) + + def _elements(self, d): + result = [] + for c, v in d.items(): + if c == END: + sub_result = [" "] + else: + sub_result = [c + s for s in self._elements(v)] + result.extend(sub_result) + return tuple(result) + + +trie = Trie() +words = ("depart", "detergent", "daring", "dog", "deer", "deal") +for word in words: + trie.insert_word(word) + + +def autocomplete_using_trie(s): + """ + >>> trie = Trie() + >>> for word in words: + ... trie.insert_word(word) + ... + >>> matches = autocomplete_using_trie("de") + + "detergent " in matches + True + "dog " in matches + False + """ + suffixes = trie.find_word(s) + return tuple(s + w for w in suffixes) + + +def main(): + print(autocomplete_using_trie("de")) + + +if __name__ == "__main__": + main() diff --git a/other/binary_exponentiation.py b/other/binary_exponentiation.py index 1a30fb8fd266..dd4e70e74129 100644 --- a/other/binary_exponentiation.py +++ b/other/binary_exponentiation.py @@ -14,7 +14,7 @@ def b_expo(a, b): res = 1 while b > 0: - if b&1: + if b & 1: res *= a a *= a @@ -26,14 +26,15 @@ def b_expo(a, b): def b_expo_mod(a, b, c): res = 1 while b > 0: - if b&1: - res = ((res%c) * (a%c)) % c + if b & 1: + res = ((res % c) * (a % c)) % c a *= a b >>= 1 return res + """ * Wondering how this method works ! * It's pretty simple. diff --git a/other/binary_exponentiation_2.py b/other/binary_exponentiation_2.py index 217a616c99fb..51ec4baf2598 100644 --- a/other/binary_exponentiation_2.py +++ b/other/binary_exponentiation_2.py @@ -14,7 +14,7 @@ def b_expo(a, b): res = 0 while b > 0: - if b&1: + if b & 1: res += a a += a @@ -26,8 +26,8 @@ def b_expo(a, b): def b_expo_mod(a, b, c): res = 0 while b > 0: - if b&1: - res = ((res%c) + (a%c)) % c + if b & 1: + res = ((res % c) + (a % c)) % c a += a b >>= 1 diff --git a/other/detecting_english_programmatically.py b/other/detecting_english_programmatically.py index 8b73ff6cf0c3..4b0bb37ce520 100644 --- a/other/detecting_english_programmatically.py +++ b/other/detecting_english_programmatically.py @@ -1,18 +1,21 @@ import os -UPPERLETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' -LETTERS_AND_SPACE = UPPERLETTERS + UPPERLETTERS.lower() + ' \t\n' +UPPERLETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +LETTERS_AND_SPACE = UPPERLETTERS + UPPERLETTERS.lower() + " \t\n" + def loadDictionary(): path = os.path.split(os.path.realpath(__file__)) englishWords = {} - with open(path[0] + '/dictionary.txt') as dictionaryFile: - for word in dictionaryFile.read().split('\n'): + with open(path[0] + "/dictionary.txt") as dictionaryFile: + for word in dictionaryFile.read().split("\n"): englishWords[word] = None return englishWords + ENGLISH_WORDS = loadDictionary() + def getEnglishCount(message): message = message.upper() message = removeNonLetters(message) @@ -28,14 +31,16 @@ def getEnglishCount(message): return float(matches) / len(possibleWords) + def removeNonLetters(message): lettersOnly = [] for symbol in message: if symbol in LETTERS_AND_SPACE: lettersOnly.append(symbol) - return ''.join(lettersOnly) + return "".join(lettersOnly) + -def isEnglish(message, wordPercentage = 20, letterPercentage = 85): +def isEnglish(message, wordPercentage=20, letterPercentage=85): """ >>> isEnglish('Hello World') True @@ -51,4 +56,5 @@ def isEnglish(message, wordPercentage = 20, letterPercentage = 85): import doctest + doctest.testmod() diff --git a/other/dictionary.txt b/other/dictionary.txt index 14528efe844f..75838996ba08 100644 --- a/other/dictionary.txt +++ b/other/dictionary.txt @@ -26131,6 +26131,7 @@ MICROECONOMICS MICROELECTRONICS MICROFILM MICROFILMS +MICROFINANCE MICROGRAMMING MICROINSTRUCTION MICROINSTRUCTIONS @@ -45330,4 +45331,4 @@ ZOROASTER ZOROASTRIAN ZULU ZULUS -ZURICH \ No newline at end of file +ZURICH diff --git a/other/dijkstra_bankers_algorithm.py b/other/dijkstra_bankers_algorithm.py new file mode 100644 index 000000000000..1f78941d3afc --- /dev/null +++ b/other/dijkstra_bankers_algorithm.py @@ -0,0 +1,222 @@ +# A Python implementation of the Banker's Algorithm in Operating Systems using +# Processes and Resources +# { +# "Author: "Biney Kingsley (bluedistro@github.io), bineykingsley36@gmail.com", +# "Date": 28-10-2018 +# } +""" +The Banker's algorithm is a resource allocation and deadlock avoidance algorithm +developed by Edsger Dijkstra that tests for safety by simulating the allocation of +predetermined maximum possible amounts of all resources, and then makes a "s-state" +check to test for possible deadlock conditions for all other pending activities, +before deciding whether allocation should be allowed to continue. +[Source] Wikipedia +[Credit] Rosetta Code C implementation helped very much. + (https://rosettacode.org/wiki/Banker%27s_algorithm) +""" + +import time +from typing import Dict, List + +import numpy as np + +test_claim_vector = [8, 5, 9, 7] +test_allocated_res_table = [ + [2, 0, 1, 1], + [0, 1, 2, 1], + [4, 0, 0, 3], + [0, 2, 1, 0], + [1, 0, 3, 0], +] +test_maximum_claim_table = [ + [3, 2, 1, 4], + [0, 2, 5, 2], + [5, 1, 0, 5], + [1, 5, 3, 0], + [3, 0, 3, 3], +] + + +class BankersAlgorithm: + def __init__( + self, + claim_vector: List[int], + allocated_resources_table: List[List[int]], + maximum_claim_table: List[List[int]], + ) -> None: + """ + :param claim_vector: A nxn/nxm list depicting the amount of each resources + (eg. memory, interface, semaphores, etc.) available. + :param allocated_resources_table: A nxn/nxm list depicting the amount of each + resource each process is currently holding + :param maximum_claim_table: A nxn/nxm list depicting how much of each resource + the system currently has available + """ + self.__claim_vector = claim_vector + self.__allocated_resources_table = allocated_resources_table + self.__maximum_claim_table = maximum_claim_table + + def __processes_resource_summation(self) -> List[int]: + """ + Check for allocated resources in line with each resource in the claim vector + """ + return [ + sum(p_item[i] for p_item in self.__allocated_resources_table) + for i in range(len(self.__allocated_resources_table[0])) + ] + + def __available_resources(self) -> List[int]: + """ + Check for available resources in line with each resource in the claim vector + """ + return np.array(self.__claim_vector) - np.array( + self.__processes_resource_summation() + ) + + def __need(self) -> List[List[int]]: + """ + Implement safety checker that calculates the needs by ensuring that + max_claim[i][j] - alloc_table[i][j] <= avail[j] + """ + return [ + list(np.array(self.__maximum_claim_table[i]) - np.array(allocated_resource)) + for i, allocated_resource in enumerate(self.__allocated_resources_table) + ] + + def __need_index_manager(self) -> Dict[int, List[int]]: + """ + This function builds an index control dictionary to track original ids/indices + of processes when altered during execution of method "main" + Return: {0: [a: int, b: int], 1: [c: int, d: int]} + >>> BankersAlgorithm(test_claim_vector, test_allocated_res_table, + ... test_maximum_claim_table)._BankersAlgorithm__need_index_manager() + {0: [1, 2, 0, 3], 1: [0, 1, 3, 1], 2: [1, 1, 0, 2], 3: [1, 3, 2, 0], 4: [2, 0, 0, 3]} + """ + return {self.__need().index(i): i for i in self.__need()} + + def main(self, **kwargs) -> None: + """ + Utilize various methods in this class to simulate the Banker's algorithm + Return: None + >>> BankersAlgorithm(test_claim_vector, test_allocated_res_table, + ... test_maximum_claim_table).main(describe=True) + Allocated Resource Table + P1 2 0 1 1 + + P2 0 1 2 1 + + P3 4 0 0 3 + + P4 0 2 1 0 + + P5 1 0 3 0 + + System Resource Table + P1 3 2 1 4 + + P2 0 2 5 2 + + P3 5 1 0 5 + + P4 1 5 3 0 + + P5 3 0 3 3 + + Current Usage by Active Processes: 8 5 9 7 + Initial Available Resources: 1 2 2 2 + __________________________________________________ + + Process 3 is executing. + Updated available resource stack for processes: 5 2 2 5 + The process is in a safe state. + + Process 1 is executing. + Updated available resource stack for processes: 7 2 3 6 + The process is in a safe state. + + Process 2 is executing. + Updated available resource stack for processes: 7 3 5 7 + The process is in a safe state. + + Process 4 is executing. + Updated available resource stack for processes: 7 5 6 7 + The process is in a safe state. + + Process 5 is executing. + Updated available resource stack for processes: 8 5 9 7 + The process is in a safe state. + + """ + need_list = self.__need() + alloc_resources_table = self.__allocated_resources_table + available_resources = self.__available_resources() + need_index_manager = self.__need_index_manager() + for kw, val in kwargs.items(): + if kw and val is True: + self.__pretty_data() + print("_" * 50 + "\n") + while need_list: + safe = False + for each_need in need_list: + execution = True + for index, need in enumerate(each_need): + if need > available_resources[index]: + execution = False + break + if execution: + safe = True + # get the original index of the process from ind_ctrl db + for original_need_index, need_clone in need_index_manager.items(): + if each_need == need_clone: + process_number = original_need_index + print(f"Process {process_number + 1} is executing.") + # remove the process run from stack + need_list.remove(each_need) + # update available/freed resources stack + available_resources = np.array(available_resources) + np.array( + alloc_resources_table[process_number] + ) + print( + "Updated available resource stack for processes: " + + " ".join([str(x) for x in available_resources]) + ) + break + if safe: + print("The process is in a safe state.\n") + else: + print("System in unsafe state. Aborting...\n") + break + + def __pretty_data(self): + """ + Properly align display of the algorithm's solution + """ + print(" " * 9 + "Allocated Resource Table") + for item in self.__allocated_resources_table: + print( + f"P{self.__allocated_resources_table.index(item) + 1}" + + " ".join(f"{it:>8}" for it in item) + + "\n" + ) + print(" " * 9 + "System Resource Table") + for item in self.__maximum_claim_table: + print( + f"P{self.__maximum_claim_table.index(item) + 1}" + + " ".join(f"{it:>8}" for it in item) + + "\n" + ) + print( + "Current Usage by Active Processes: " + + " ".join(str(x) for x in self.__claim_vector) + ) + print( + "Initial Available Resources: " + + " ".join(str(x) for x in self.__available_resources()) + ) + time.sleep(1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/euclidean_gcd.py b/other/euclidean_gcd.py index 30853e172076..c6c11f947a08 100644 --- a/other/euclidean_gcd.py +++ b/other/euclidean_gcd.py @@ -1,6 +1,6 @@ -from __future__ import print_function # https://en.wikipedia.org/wiki/Euclidean_algorithm + def euclidean_gcd(a, b): while b: t = b @@ -8,6 +8,7 @@ def euclidean_gcd(a, b): a = t return a + def main(): print("GCD(3, 5) = " + str(euclidean_gcd(3, 5))) print("GCD(5, 3) = " + str(euclidean_gcd(5, 3))) @@ -15,5 +16,6 @@ def main(): print("GCD(3, 6) = " + str(euclidean_gcd(3, 6))) print("GCD(6, 3) = " + str(euclidean_gcd(6, 3))) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/other/finding_primes.py b/other/finding_primes.py deleted file mode 100644 index 035a14f4a335..000000000000 --- a/other/finding_primes.py +++ /dev/null @@ -1,21 +0,0 @@ -''' --The sieve of Eratosthenes is an algorithm used to find prime numbers, less than or equal to a given value. --Illustration: https://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif -''' -from __future__ import print_function - - -from math import sqrt -def SOE(n): - check = round(sqrt(n)) #Need not check for multiples past the square root of n - - sieve = [False if i <2 else True for i in range(n+1)] #Set every index to False except for index 0 and 1 - - for i in range(2, check): - if(sieve[i] == True): #If i is a prime - for j in range(i+i, n+1, i): #Step through the list in increments of i(the multiples of the prime) - sieve[j] = False #Sets every multiple of i to False - - for i in range(n+1): - if(sieve[i] == True): - print(i, end=" ") diff --git a/other/fischer_yates_shuffle.py b/other/fischer_yates_shuffle.py index d87792f45558..99121ba632c7 100644 --- a/other/fischer_yates_shuffle.py +++ b/other/fischer_yates_shuffle.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# encoding=utf8 """ The Fisher–Yates shuffle is an algorithm for generating a random permutation of a finite sequence. For more details visit @@ -7,16 +6,18 @@ """ import random -def FYshuffle(LIST): - for i in range(len(LIST)): - a = random.randint(0, len(LIST)-1) - b = random.randint(0, len(LIST)-1) - LIST[a], LIST[b] = LIST[b], LIST[a] - return LIST -if __name__ == '__main__': - integers = [0,1,2,3,4,5,6,7] - strings = ['python', 'says', 'hello', '!'] - print ('Fisher-Yates Shuffle:') - print ('List',integers, strings) - print ('FY Shuffle',FYshuffle(integers), FYshuffle(strings)) +def FYshuffle(list): + for i in range(len(list)): + a = random.randint(0, len(list) - 1) + b = random.randint(0, len(list) - 1) + list[a], list[b] = list[b], list[a] + return list + + +if __name__ == "__main__": + integers = [0, 1, 2, 3, 4, 5, 6, 7] + strings = ["python", "says", "hello", "!"] + print("Fisher-Yates Shuffle:") + print("List", integers, strings) + print("FY Shuffle", FYshuffle(integers), FYshuffle(strings)) diff --git a/other/frequency_finder.py b/other/frequency_finder.py index 6264b25bf303..48760a9deb09 100644 --- a/other/frequency_finder.py +++ b/other/frequency_finder.py @@ -1,29 +1,78 @@ # Frequency Finder # frequency taken from http://en.wikipedia.org/wiki/Letter_frequency -englishLetterFreq = {'E': 12.70, 'T': 9.06, 'A': 8.17, 'O': 7.51, 'I': 6.97, - 'N': 6.75, 'S': 6.33, 'H': 6.09, 'R': 5.99, 'D': 4.25, - 'L': 4.03, 'C': 2.78, 'U': 2.76, 'M': 2.41, 'W': 2.36, - 'F': 2.23, 'G': 2.02, 'Y': 1.97, 'P': 1.93, 'B': 1.29, - 'V': 0.98, 'K': 0.77, 'J': 0.15, 'X': 0.15, 'Q': 0.10, - 'Z': 0.07} -ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ' -LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +englishLetterFreq = { + "E": 12.70, + "T": 9.06, + "A": 8.17, + "O": 7.51, + "I": 6.97, + "N": 6.75, + "S": 6.33, + "H": 6.09, + "R": 5.99, + "D": 4.25, + "L": 4.03, + "C": 2.78, + "U": 2.76, + "M": 2.41, + "W": 2.36, + "F": 2.23, + "G": 2.02, + "Y": 1.97, + "P": 1.93, + "B": 1.29, + "V": 0.98, + "K": 0.77, + "J": 0.15, + "X": 0.15, + "Q": 0.10, + "Z": 0.07, +} +ETAOIN = "ETAOINSHRDLCUMWFGYPBVKJXQZ" +LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + def getLetterCount(message): - letterCount = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'G': 0, 'H': 0, - 'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0, 'O': 0, 'P': 0, - 'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0, 'W': 0, 'X': 0, - 'Y': 0, 'Z': 0} + letterCount = { + "A": 0, + "B": 0, + "C": 0, + "D": 0, + "E": 0, + "F": 0, + "G": 0, + "H": 0, + "I": 0, + "J": 0, + "K": 0, + "L": 0, + "M": 0, + "N": 0, + "O": 0, + "P": 0, + "Q": 0, + "R": 0, + "S": 0, + "T": 0, + "U": 0, + "V": 0, + "W": 0, + "X": 0, + "Y": 0, + "Z": 0, + } for letter in message.upper(): if letter in LETTERS: letterCount[letter] += 1 return letterCount + def getItemAtIndexZero(x): return x[0] + def getFrequencyOrder(message): letterToFreq = getLetterCount(message) freqToLetter = {} @@ -34,23 +83,24 @@ def getFrequencyOrder(message): freqToLetter[letterToFreq[letter]].append(letter) for freq in freqToLetter: - freqToLetter[freq].sort(key = ETAOIN.find, reverse = True) - freqToLetter[freq] = ''.join(freqToLetter[freq]) + freqToLetter[freq].sort(key=ETAOIN.find, reverse=True) + freqToLetter[freq] = "".join(freqToLetter[freq]) freqPairs = list(freqToLetter.items()) - freqPairs.sort(key = getItemAtIndexZero, reverse = True) + freqPairs.sort(key=getItemAtIndexZero, reverse=True) freqOrder = [] for freqPair in freqPairs: freqOrder.append(freqPair[1]) - return ''.join(freqOrder) + return "".join(freqOrder) + def englishFreqMatchScore(message): - ''' + """ >>> englishFreqMatchScore('Hello World') 1 - ''' + """ freqOrder = getFrequencyOrder(message) matchScore = 0 for commonLetter in ETAOIN[:6]: @@ -63,6 +113,8 @@ def englishFreqMatchScore(message): return matchScore -if __name__ == '__main__': + +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/other/game_of_life.py b/other/game_of_life.py index 1fdaa21b4a7b..2b4d1116fa8c 100644 --- a/other/game_of_life.py +++ b/other/game_of_life.py @@ -1,4 +1,4 @@ -'''Conway's Game Of Life, Author Anurag Kumar(mailto:anuragkumarak95@gmail.com) +"""Conway's Game Of Life, Author Anurag Kumar(mailto:anuragkumarak95@gmail.com) Requirements: - numpy @@ -26,28 +26,31 @@ 4. Any dead cell with exactly three live neighbours be- comes a live cell, as if by reproduction. - ''' + """ import numpy as np import random, sys from matplotlib import pyplot as plt from matplotlib.colors import ListedColormap -usage_doc='Usage of script: script_nama ' +usage_doc = "Usage of script: script_nama " -choice = [0]*100 + [1]*10 +choice = [0] * 100 + [1] * 10 random.shuffle(choice) + def create_canvas(size): - canvas = [ [False for i in range(size)] for j in range(size)] + canvas = [[False for i in range(size)] for j in range(size)] return canvas + def seed(canvas): - for i,row in enumerate(canvas): - for j,_ in enumerate(row): - canvas[i][j]=bool(random.getrandbits(1)) + for i, row in enumerate(canvas): + for j, _ in enumerate(row): + canvas[i][j] = bool(random.getrandbits(1)) + def run(canvas): - ''' This function runs the rules of game through all points, and changes their status accordingly.(in the same canvas) + """ This function runs the rules of game through all points, and changes their status accordingly.(in the same canvas) @Args: -- canvas : canvas of population to run the rules on. @@ -55,63 +58,71 @@ def run(canvas): @returns: -- None - ''' + """ canvas = np.array(canvas) next_gen_canvas = np.array(create_canvas(canvas.shape[0])) for r, row in enumerate(canvas): for c, pt in enumerate(row): # print(r-1,r+2,c-1,c+2) - next_gen_canvas[r][c] = __judge_point(pt,canvas[r-1:r+2,c-1:c+2]) - + next_gen_canvas[r][c] = __judge_point( + pt, canvas[r - 1 : r + 2, c - 1 : c + 2] + ) + canvas = next_gen_canvas - del next_gen_canvas # cleaning memory as we move on. - return canvas.tolist() + del next_gen_canvas # cleaning memory as we move on. + return canvas.tolist() + -def __judge_point(pt,neighbours): - dead = 0 +def __judge_point(pt, neighbours): + dead = 0 alive = 0 # finding dead or alive neighbours count. for i in neighbours: for status in i: - if status: alive+=1 - else: dead+=1 + if status: + alive += 1 + else: + dead += 1 # handling duplicate entry for focus pt. - if pt : alive-=1 - else : dead-=1 - + if pt: + alive -= 1 + else: + dead -= 1 + # running the rules of game here. state = pt if pt: - if alive<2: - state=False - elif alive==2 or alive==3: - state=True - elif alive>3: - state=False + if alive < 2: + state = False + elif alive == 2 or alive == 3: + state = True + elif alive > 3: + state = False else: - if alive==3: - state=True + if alive == 3: + state = True return state -if __name__=='__main__': - if len(sys.argv) != 2: raise Exception(usage_doc) - +if __name__ == "__main__": + if len(sys.argv) != 2: + raise Exception(usage_doc) + canvas_size = int(sys.argv[1]) # main working structure of this module. - c=create_canvas(canvas_size) + c = create_canvas(canvas_size) seed(c) fig, ax = plt.subplots() - fig.show() - cmap = ListedColormap(['w','k']) + fig.show() + cmap = ListedColormap(["w", "k"]) try: while True: - c = run(c) - ax.matshow(c,cmap=cmap) + c = run(c) + ax.matshow(c, cmap=cmap) fig.canvas.draw() - ax.cla() + ax.cla() except KeyboardInterrupt: # do nothing. pass diff --git a/other/greedy.py b/other/greedy.py new file mode 100644 index 000000000000..4b78bf1c0415 --- /dev/null +++ b/other/greedy.py @@ -0,0 +1,63 @@ +class things: + def __init__(self, name, value, weight): + self.name = name + self.value = value + self.weight = weight + + def __repr__(self): + return f"{self.__class__.__name__}({self.name}, {self.value}, {self.weight})" + + def get_value(self): + return self.value + + def get_name(self): + return self.name + + def get_weight(self): + return self.weight + + def value_Weight(self): + return self.value / self.weight + + +def build_menu(name, value, weight): + menu = [] + for i in range(len(value)): + menu.append(things(name[i], value[i], weight[i])) + return menu + + +def greedy(item, maxCost, keyFunc): + itemsCopy = sorted(item, key=keyFunc, reverse=True) + result = [] + totalValue, total_cost = 0.0, 0.0 + for i in range(len(itemsCopy)): + if (total_cost + itemsCopy[i].get_weight()) <= maxCost: + result.append(itemsCopy[i]) + total_cost += itemsCopy[i].get_weight() + totalValue += itemsCopy[i].get_value() + return (result, totalValue) + + +def test_greedy(): + """ + >>> food = ["Burger", "Pizza", "Coca Cola", "Rice", + ... "Sambhar", "Chicken", "Fries", "Milk"] + >>> value = [80, 100, 60, 70, 50, 110, 90, 60] + >>> weight = [40, 60, 40, 70, 100, 85, 55, 70] + >>> foods = build_menu(food, value, weight) + >>> foods # doctest: +NORMALIZE_WHITESPACE + [things(Burger, 80, 40), things(Pizza, 100, 60), things(Coca Cola, 60, 40), + things(Rice, 70, 70), things(Sambhar, 50, 100), things(Chicken, 110, 85), + things(Fries, 90, 55), things(Milk, 60, 70)] + >>> greedy(foods, 500, things.get_value) # doctest: +NORMALIZE_WHITESPACE + ([things(Chicken, 110, 85), things(Pizza, 100, 60), things(Fries, 90, 55), + things(Burger, 80, 40), things(Rice, 70, 70), things(Coca Cola, 60, 40), + things(Milk, 60, 70)], 570.0) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/integeration_by_simpson_approx.py b/other/integeration_by_simpson_approx.py new file mode 100644 index 000000000000..0f7bfacf030a --- /dev/null +++ b/other/integeration_by_simpson_approx.py @@ -0,0 +1,123 @@ +""" +Author : Syed Faizan ( 3rd Year IIIT Pune ) +Github : faizan2700 + +Purpose : You have one function f(x) which takes float integer and returns +float you have to integrate the function in limits a to b. +The approximation proposed by Thomas Simpsons in 1743 is one way to calculate integration. + +( read article : https://cp-algorithms.com/num_methods/simpson-integration.html ) + +simpson_integration() takes function,lower_limit=a,upper_limit=b,precision and +returns the integration of function in given limit. +""" + +# constants +# the more the number of steps the more accurate +N_STEPS = 1000 + + +def f(x: float) -> float: + return x * x + + +""" +Summary of Simpson Approximation : + +By simpsons integration : +1.integration of fxdx with limit a to b is = f(x0) + 4 * f(x1) + 2 * f(x2) + 4 * f(x3) + 2 * f(x4)..... + f(xn) +where x0 = a +xi = a + i * h +xn = b +""" + + +def simpson_integration(function, a: float, b: float, precision: int = 4) -> float: + + """ + Args: + function : the function which's integration is desired + a : the lower limit of integration + b : upper limit of integraion + precision : precision of the result,error required default is 4 + + Returns: + result : the value of the approximated integration of function in range a to b + + Raises: + AssertionError: function is not callable + AssertionError: a is not float or integer + AssertionError: function should return float or integer + AssertionError: b is not float or integer + AssertionError: precision is not positive integer + + >>> simpson_integration(lambda x : x*x,1,2,3) + 2.333 + + >>> simpson_integration(lambda x : x*x,'wrong_input',2,3) + Traceback (most recent call last): + ... + AssertionError: a should be float or integer your input : wrong_input + + >>> simpson_integration(lambda x : x*x,1,'wrong_input',3) + Traceback (most recent call last): + ... + AssertionError: b should be float or integer your input : wrong_input + + >>> simpson_integration(lambda x : x*x,1,2,'wrong_input') + Traceback (most recent call last): + ... + AssertionError: precision should be positive integer your input : wrong_input + >>> simpson_integration('wrong_input',2,3,4) + Traceback (most recent call last): + ... + AssertionError: the function(object) passed should be callable your input : wrong_input + + >>> simpson_integration(lambda x : x*x,3.45,3.2,1) + -2.8 + + >>> simpson_integration(lambda x : x*x,3.45,3.2,0) + Traceback (most recent call last): + ... + AssertionError: precision should be positive integer your input : 0 + + >>> simpson_integration(lambda x : x*x,3.45,3.2,-1) + Traceback (most recent call last): + ... + AssertionError: precision should be positive integer your input : -1 + + """ + assert callable( + function + ), f"the function(object) passed should be callable your input : {function}" + assert isinstance(a, float) or isinstance( + a, int + ), f"a should be float or integer your input : {a}" + assert isinstance(function(a), float) or isinstance( + function(a), int + ), f"the function should return integer or float return type of your function, {type(a)}" + assert isinstance(b, float) or isinstance( + b, int + ), f"b should be float or integer your input : {b}" + assert ( + isinstance(precision, int) and precision > 0 + ), f"precision should be positive integer your input : {precision}" + + # just applying the formula of simpson for approximate integraion written in + # mentioned article in first comment of this file and above this function + + h = (b - a) / N_STEPS + result = function(a) + function(b) + + for i in range(1, N_STEPS): + a1 = a + h * i + result += function(a1) * (4 if i % 2 else 2) + + result *= h / 3 + return round(result, precision) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/other/largest_subarray_sum.py b/other/largest_subarray_sum.py new file mode 100644 index 000000000000..0449e72e64e3 --- /dev/null +++ b/other/largest_subarray_sum.py @@ -0,0 +1,23 @@ +from sys import maxsize + + +def max_sub_array_sum(a: list, size: int = 0): + """ + >>> max_sub_array_sum([-13, -3, -25, -20, -3, -16, -23, -12, -5, -22, -15, -4, -7]) + -3 + """ + size = size or len(a) + max_so_far = -maxsize - 1 + max_ending_here = 0 + for i in range(0, size): + max_ending_here = max_ending_here + a[i] + if max_so_far < max_ending_here: + max_so_far = max_ending_here + if max_ending_here < 0: + max_ending_here = 0 + return max_so_far + + +if __name__ == "__main__": + a = [-13, -3, -25, -20, 1, -16, -23, -12, -5, -22, -15, -4, -7] + print(("Maximum contiguous sum is", max_sub_array_sum(a, len(a)))) diff --git a/other/least_recently_used.py b/other/least_recently_used.py new file mode 100644 index 000000000000..e1b5ab5bd380 --- /dev/null +++ b/other/least_recently_used.py @@ -0,0 +1,64 @@ +from abc import abstractmethod +import sys +from collections import deque + + +class LRUCache: + """ Page Replacement Algorithm, Least Recently Used (LRU) Caching.""" + + dq_store = object() # Cache store of keys + key_reference_map = object() # References of the keys in cache + _MAX_CAPACITY: int = 10 # Maximum capacity of cache + + @abstractmethod + def __init__(self, n: int): + """ Creates an empty store and map for the keys. + The LRUCache is set the size n. + """ + self.dq_store = deque() + self.key_reference_map = set() + if not n: + LRUCache._MAX_CAPACITY = sys.maxsize + elif n < 0: + raise ValueError("n should be an integer greater than 0.") + else: + LRUCache._MAX_CAPACITY = n + + def refer(self, x): + """ + Looks for a page in the cache store and adds reference to the set. + Remove the least recently used key if the store is full. + Update store to reflect recent access. + """ + if x not in self.key_reference_map: + if len(self.dq_store) == LRUCache._MAX_CAPACITY: + last_element = self.dq_store.pop() + self.key_reference_map.remove(last_element) + else: + index_remove = 0 + for idx, key in enumerate(self.dq_store): + if key == x: + index_remove = idx + break + self.dq_store.remove(index_remove) + + self.dq_store.appendleft(x) + self.key_reference_map.add(x) + + def display(self): + """ + Prints all the elements in the store. + """ + for k in self.dq_store: + print(k) + + +if __name__ == "__main__": + lru_cache = LRUCache(4) + lru_cache.refer(1) + lru_cache.refer(2) + lru_cache.refer(3) + lru_cache.refer(1) + lru_cache.refer(4) + lru_cache.refer(5) + lru_cache.display() diff --git a/other/linear_congruential_generator.py b/other/linear_congruential_generator.py index 34abdf34eaf3..058f270d0584 100644 --- a/other/linear_congruential_generator.py +++ b/other/linear_congruential_generator.py @@ -1,26 +1,26 @@ -from __future__ import print_function __author__ = "Tobias Carryer" from time import time -class LinearCongruentialGenerator(object): + +class LinearCongruentialGenerator: """ A pseudorandom number generator. """ - - def __init__( self, multiplier, increment, modulo, seed=int(time()) ): + + def __init__(self, multiplier, increment, modulo, seed=int(time())): """ These parameters are saved and used when nextNumber() is called. - + modulo is the largest number that can be generated (exclusive). The most - efficent values are powers of 2. 2^32 is a common value. + efficient values are powers of 2. 2^32 is a common value. """ self.multiplier = multiplier self.increment = increment self.modulo = modulo self.seed = seed - - def next_number( self ): + + def next_number(self): """ The smallest number that can be generated is zero. The largest number that can be generated is modulo-1. modulo is set in the constructor. @@ -28,8 +28,9 @@ def next_number( self ): self.seed = (self.multiplier * self.seed + self.increment) % self.modulo return self.seed + if __name__ == "__main__": # Show the LCG in action. - lcg = LinearCongruentialGenerator(1664525, 1013904223, 2<<31) - while True : - print(lcg.next_number()) \ No newline at end of file + lcg = LinearCongruentialGenerator(1664525, 1013904223, 2 << 31) + while True: + print(lcg.next_number()) diff --git a/other/magicdiamondpattern.py b/other/magicdiamondpattern.py new file mode 100644 index 000000000000..4ca698d80c28 --- /dev/null +++ b/other/magicdiamondpattern.py @@ -0,0 +1,54 @@ +# Python program for generating diamond pattern in Python 3.7+ + +# Function to print upper half of diamond (pyramid) +def floyd(n): + """ + Parameters: + n : size of pattern + """ + for i in range(0, n): + for j in range(0, n - i - 1): # printing spaces + print(" ", end="") + for k in range(0, i + 1): # printing stars + print("* ", end="") + print() + + +# Function to print lower half of diamond (pyramid) +def reverse_floyd(n): + """ + Parameters: + n : size of pattern + """ + for i in range(n, 0, -1): + for j in range(i, 0, -1): # printing stars + print("* ", end="") + print() + for k in range(n - i + 1, 0, -1): # printing spaces + print(" ", end="") + + +# Function to print complete diamond pattern of "*" +def pretty_print(n): + """ + Parameters: + n : size of pattern + """ + if n <= 0: + print(" ... .... nothing printing :(") + return + floyd(n) # upper half + reverse_floyd(n) # lower half + + +if __name__ == "__main__": + print(r"| /\ | |- | |- |--| |\ /| |-") + print(r"|/ \| |- |_ |_ |__| | \/ | |_") + K = 1 + while K: + user_number = int(input("enter the number and , and see the magic : ")) + print() + pretty_print(user_number) + K = int(input("press 0 to exit... and 1 to continue...")) + + print("Good Bye...") diff --git a/other/nested_brackets.py b/other/nested_brackets.py index 76677d56439a..c712bc21b5ce 100644 --- a/other/nested_brackets.py +++ b/other/nested_brackets.py @@ -1,27 +1,26 @@ -''' +""" The nested brackets problem is a problem that determines if a sequence of brackets are properly nested. A sequence of brackets s is considered properly nested if any of the following conditions are true: - - s is empty - - s has the form (U) or [U] or {U} where U is a properly nested string - - s has the form VW where V and W are properly nested strings + - s is empty + - s has the form (U) or [U] or {U} where U is a properly nested string + - s has the form VW where V and W are properly nested strings For example, the string "()()[()]" is properly nested but "[(()]" is not. The function called is_balanced takes as input a string S which is a sequence of brackets and returns true if S is nested and false otherwise. -''' -from __future__ import print_function +""" def is_balanced(S): stack = [] - open_brackets = set({'(', '[', '{'}) - closed_brackets = set({')', ']', '}'}) - open_to_closed = dict({'{':'}', '[':']', '(':')'}) + open_brackets = set({"(", "[", "{"}) + closed_brackets = set({")", "]", "}"}) + open_to_closed = dict({"{": "}", "[": "]", "(": ")"}) for i in range(len(S)): @@ -29,7 +28,9 @@ def is_balanced(S): stack.append(S[i]) elif S[i] in closed_brackets: - if len(stack) == 0 or (len(stack) > 0 and open_to_closed[stack.pop()] != S[i]): + if len(stack) == 0 or ( + len(stack) > 0 and open_to_closed[stack.pop()] != S[i] + ): return False return len(stack) == 0 diff --git a/other/palindrome.py b/other/palindrome.py index 990ec844f9fb..dd1fe316f479 100644 --- a/other/palindrome.py +++ b/other/palindrome.py @@ -1,9 +1,31 @@ -# Program to find whether given string is palindrome or not -def is_palindrome(str): +# Algorithms to determine if a string is palindrome + +test_data = { + "MALAYALAM": True, + "String": False, + "rotor": True, + "level": True, + "A": True, + "BB": True, + "ABC": False, + "amanaplanacanalpanama": True, # "a man a plan a canal panama" +} +# Ensure our test data is valid +assert all((key == key[::-1]) is value for key, value in test_data.items()) + + +def is_palindrome(s: str) -> bool: + """ + Return True if s is a palindrome otherwise return False. + + >>> all(is_palindrome(key) is value for key, value in test_data.items()) + True + """ + start_i = 0 - end_i = len(str) - 1 + end_i = len(s) - 1 while start_i < end_i: - if str[start_i] == str[end_i]: + if s[start_i] == s[end_i]: start_i += 1 end_i -= 1 else: @@ -11,21 +33,34 @@ def is_palindrome(str): return True -# Recursive method -def recursive_palindrome(str): - if len(str) <= 1: +def is_palindrome_recursive(s: str) -> bool: + """ + Return True if s is a palindrome otherwise return False. + + >>> all(is_palindrome_recursive(key) is value for key, value in test_data.items()) + True + """ + if len(s) <= 1: return True - if str[0] == str[len(str) - 1]: - return recursive_palindrome(str[1:-1]) + if s[0] == s[len(s) - 1]: + return is_palindrome_recursive(s[1:-1]) else: return False -def main(): - str = 'ama' - print(recursive_palindrome(str.lower())) - print(is_palindrome(str.lower())) +def is_palindrome_slice(s: str) -> bool: + """ + Return True if s is a palindrome otherwise return False. + + >>> all(is_palindrome_slice(key) is value for key, value in test_data.items()) + True + """ + return s == s[::-1] -if __name__ == '__main__': - main() +if __name__ == "__main__": + for key, value in test_data.items(): + assert is_palindrome(key) is is_palindrome_recursive(key) + assert is_palindrome(key) is is_palindrome_slice(key) + print(f"{key:21} {value}") + print("a man a plan a canal panama") diff --git a/other/password_generator.py b/other/password_generator.py index fd0701041240..35e11e4dfb78 100644 --- a/other/password_generator.py +++ b/other/password_generator.py @@ -1,6 +1,5 @@ """Password generator allows you to generate a random password of length N.""" -from __future__ import print_function -from random import choice +from random import choice, shuffle from string import ascii_letters, digits, punctuation @@ -18,7 +17,7 @@ def password_generator(length=8): 0 """ chars = tuple(ascii_letters) + tuple(digits) + tuple(punctuation) - return ''.join(choice(chars) for x in range(length)) + return "".join(choice(chars) for x in range(length)) # ALTERNATIVE METHODS @@ -27,7 +26,27 @@ def password_generator(length=8): def alternative_password_generator(ctbi, i): # Password generator = full boot with random_number, random_letters, and # random_character FUNCTIONS - pass # Put your code here... + # Put your code here... + i = i - len(ctbi) + quotient = int(i / 3) + remainder = i % 3 + # chars = ctbi + random_letters(ascii_letters, i / 3 + remainder) + + # random_number(digits, i / 3) + random_characters(punctuation, i / 3) + chars = ( + ctbi + + random(ascii_letters, quotient + remainder) + + random(digits, quotient) + + random(punctuation, quotient) + ) + chars = list(chars) + shuffle(chars) + return "".join(chars) + + # random is a generalised function for letters, characters and numbers + + +def random(ctbi, i): + return "".join(choice(ctbi) for x in range(i)) def random_number(ctbi, i): @@ -43,11 +62,16 @@ def random_characters(ctbi, i): def main(): - length = int( - input('Please indicate the max length of your password: ').strip()) - print('Password generated:', password_generator(length)) - print('[If you are thinking of using this passsword, You better save it.]') + length = int(input("Please indicate the max length of your password: ").strip()) + ctbi = input( + "Please indicate the characters that must be in your password: " + ).strip() + print("Password generated:", password_generator(length)) + print( + "Alternative Password generated:", alternative_password_generator(ctbi, length) + ) + print("[If you are thinking of using this passsword, You better save it.]") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/other/primelib.py b/other/primelib.py index c371bc1b9861..a6d1d7dfb324 100644 --- a/other/primelib.py +++ b/other/primelib.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- """ Created on Thu Oct 5 16:44:23 2017 @author: Christian Bender -This python library contains some useful functions to deal with +This Python library contains some useful functions to deal with prime numbers and whole numbers. Overview: @@ -16,7 +15,7 @@ greatestPrimeFactor(number) smallestPrimeFactor(number) getPrime(n) -getPrimesBetween(pNumber1, pNumber2) +getPrimesBetween(pNumber1, pNumber2) ---- @@ -39,479 +38,507 @@ """ +from math import sqrt + + def isPrime(number): """ input: positive integer 'number' returns true if 'number' is prime otherwise false. """ - import math # for function sqrt - + # precondition - assert isinstance(number,int) and (number >= 0) , \ - "'number' must been an int and positive" - + assert isinstance(number, int) and ( + number >= 0 + ), "'number' must been an int and positive" + status = True - - # 0 and 1 are none primes. + + # 0 and 1 are none primes. if number <= 1: status = False - - for divisor in range(2,int(round(math.sqrt(number)))+1): - + + for divisor in range(2, int(round(sqrt(number))) + 1): + # if 'number' divisible by 'divisor' then sets 'status' - # of false and break up the loop. + # of false and break up the loop. if number % divisor == 0: status = False break - + # precondition - assert isinstance(status,bool), "'status' must been from type bool" - + assert isinstance(status, bool), "'status' must been from type bool" + return status + # ------------------------------------------ + def sieveEr(N): """ input: positive integer 'N' > 2 returns a list of prime numbers from 2 up to N. - + This function implements the algorithm called - sieve of erathostenes. - + sieve of erathostenes. + """ - + # precondition - assert isinstance(N,int) and (N > 2), "'N' must been an int and > 2" - - # beginList: conatins all natural numbers from 2 upt to N - beginList = [x for x in range(2,N+1)] + assert isinstance(N, int) and (N > 2), "'N' must been an int and > 2" + + # beginList: contains all natural numbers from 2 up to N + beginList = [x for x in range(2, N + 1)] + + ans = [] # this list will be returns. - ans = [] # this list will be returns. - # actual sieve of erathostenes for i in range(len(beginList)): - - for j in range(i+1,len(beginList)): - - if (beginList[i] != 0) and \ - (beginList[j] % beginList[i] == 0): + + for j in range(i + 1, len(beginList)): + + if (beginList[i] != 0) and (beginList[j] % beginList[i] == 0): beginList[j] = 0 - - # filters actual prime numbers. + + # filters actual prime numbers. ans = [x for x in beginList if x != 0] - + # precondition - assert isinstance(ans,list), "'ans' must been from type list" - + assert isinstance(ans, list), "'ans' must been from type list" + return ans - + # -------------------------------- + def getPrimeNumbers(N): """ input: positive integer 'N' > 2 returns a list of prime numbers from 2 up to N (inclusive) This function is more efficient as function 'sieveEr(...)' - """ - - # precondition - assert isinstance(N,int) and (N > 2), "'N' must been an int and > 2" - - ans = [] - - # iterates over all numbers between 2 up to N+1 + """ + + # precondition + assert isinstance(N, int) and (N > 2), "'N' must been an int and > 2" + + ans = [] + + # iterates over all numbers between 2 up to N+1 # if a number is prime then appends to list 'ans' - for number in range(2,N+1): - + for number in range(2, N + 1): + if isPrime(number): - + ans.append(number) - + # precondition - assert isinstance(ans,list), "'ans' must been from type list" - + assert isinstance(ans, list), "'ans' must been from type list" + return ans # ----------------------------------------- - + + def primeFactorization(number): """ - input: positive integer 'number' + input: positive integer 'number' returns a list of the prime number factors of 'number' """ - import math # for function sqrt - # precondition - assert isinstance(number,int) and number >= 0, \ - "'number' must been an int and >= 0" - - ans = [] # this list will be returns of the function. + assert isinstance(number, int) and number >= 0, "'number' must been an int and >= 0" + + ans = [] # this list will be returns of the function. # potential prime number factors. - factor = 2 + factor = 2 quotient = number - - + if number == 0 or number == 1: - + ans.append(number) - - # if 'number' not prime then builds the prime factorization of 'number' + + # if 'number' not prime then builds the prime factorization of 'number' elif not isPrime(number): - - while (quotient != 1): - + + while quotient != 1: + if isPrime(factor) and (quotient % factor == 0): - ans.append(factor) - quotient /= factor + ans.append(factor) + quotient /= factor else: - factor += 1 - + factor += 1 + else: ans.append(number) - + # precondition - assert isinstance(ans,list), "'ans' must been from type list" - + assert isinstance(ans, list), "'ans' must been from type list" + return ans - + # ----------------------------------------- - + + def greatestPrimeFactor(number): """ input: positive integer 'number' >= 0 returns the greatest prime number factor of 'number' """ - + # precondition - assert isinstance(number,int) and (number >= 0), \ - "'number' bust been an int and >= 0" - - ans = 0 - + assert isinstance(number, int) and ( + number >= 0 + ), "'number' bust been an int and >= 0" + + ans = 0 + # prime factorization of 'number' primeFactors = primeFactorization(number) - ans = max(primeFactors) - + ans = max(primeFactors) + # precondition - assert isinstance(ans,int), "'ans' must been from type int" - + assert isinstance(ans, int), "'ans' must been from type int" + return ans - + # ---------------------------------------------- - - + + def smallestPrimeFactor(number): """ input: integer 'number' >= 0 returns the smallest prime number factor of 'number' """ - + # precondition - assert isinstance(number,int) and (number >= 0), \ - "'number' bust been an int and >= 0" - - ans = 0 - + assert isinstance(number, int) and ( + number >= 0 + ), "'number' bust been an int and >= 0" + + ans = 0 + # prime factorization of 'number' primeFactors = primeFactorization(number) - + ans = min(primeFactors) # precondition - assert isinstance(ans,int), "'ans' must been from type int" - + assert isinstance(ans, int), "'ans' must been from type int" + return ans - - + + # ---------------------- - + + def isEven(number): """ input: integer 'number' returns true if 'number' is even, otherwise false. - """ + """ # precondition - assert isinstance(number, int), "'number' must been an int" + assert isinstance(number, int), "'number' must been an int" assert isinstance(number % 2 == 0, bool), "compare bust been from type bool" - + return number % 2 == 0 - + + # ------------------------ - + + def isOdd(number): """ input: integer 'number' returns true if 'number' is odd, otherwise false. - """ + """ # precondition - assert isinstance(number, int), "'number' must been an int" + assert isinstance(number, int), "'number' must been an int" assert isinstance(number % 2 != 0, bool), "compare bust been from type bool" - + return number % 2 != 0 - + + # ------------------------ - - + + def goldbach(number): """ Goldbach's assumption input: a even positive integer 'number' > 2 returns a list of two prime numbers whose sum is equal to 'number' """ - + # precondition - assert isinstance(number,int) and (number > 2) and isEven(number), \ - "'number' must been an int, even and > 2" - - ans = [] # this list will returned - + assert ( + isinstance(number, int) and (number > 2) and isEven(number) + ), "'number' must been an int, even and > 2" + + ans = [] # this list will returned + # creates a list of prime numbers between 2 up to 'number' primeNumbers = getPrimeNumbers(number) - lenPN = len(primeNumbers) + lenPN = len(primeNumbers) # run variable for while-loops. i = 0 j = None - + # exit variable. for break up the loops loop = True - - while (i < lenPN and loop): - - j = i+1 - - - while (j < lenPN and loop): - + + while i < lenPN and loop: + + j = i + 1 + + while j < lenPN and loop: + if primeNumbers[i] + primeNumbers[j] == number: loop = False ans.append(primeNumbers[i]) ans.append(primeNumbers[j]) - + j += 1 i += 1 - + # precondition - assert isinstance(ans,list) and (len(ans) == 2) and \ - (ans[0] + ans[1] == number) and isPrime(ans[0]) and isPrime(ans[1]), \ - "'ans' must contains two primes. And sum of elements must been eq 'number'" - + assert ( + isinstance(ans, list) + and (len(ans) == 2) + and (ans[0] + ans[1] == number) + and isPrime(ans[0]) + and isPrime(ans[1]) + ), "'ans' must contains two primes. And sum of elements must been eq 'number'" + return ans - + + # ---------------------------------------------- -def gcd(number1,number2): + +def gcd(number1, number2): """ Greatest common divisor input: two positive integer 'number1' and 'number2' returns the greatest common divisor of 'number1' and 'number2' """ - + # precondition - assert isinstance(number1,int) and isinstance(number2,int) \ - and (number1 >= 0) and (number2 >= 0), \ - "'number1' and 'number2' must been positive integer." + assert ( + isinstance(number1, int) + and isinstance(number2, int) + and (number1 >= 0) + and (number2 >= 0) + ), "'number1' and 'number2' must been positive integer." + + rest = 0 - rest = 0 - while number2 != 0: - + rest = number1 % number2 number1 = number2 number2 = rest # precondition - assert isinstance(number1,int) and (number1 >= 0), \ - "'number' must been from type int and positive" - + assert isinstance(number1, int) and ( + number1 >= 0 + ), "'number' must been from type int and positive" + return number1 - + + # ---------------------------------------------------- - + + def kgV(number1, number2): """ Least common multiple input: two positive integer 'number1' and 'number2' returns the least common multiple of 'number1' and 'number2' """ - + # precondition - assert isinstance(number1,int) and isinstance(number2,int) \ - and (number1 >= 1) and (number2 >= 1), \ - "'number1' and 'number2' must been positive integer." - - ans = 1 # actual answer that will be return. - + assert ( + isinstance(number1, int) + and isinstance(number2, int) + and (number1 >= 1) + and (number2 >= 1) + ), "'number1' and 'number2' must been positive integer." + + ans = 1 # actual answer that will be return. + # for kgV (x,1) if number1 > 1 and number2 > 1: - + # builds the prime factorization of 'number1' and 'number2' primeFac1 = primeFactorization(number1) primeFac2 = primeFactorization(number2) - + elif number1 == 1 or number2 == 1: - + primeFac1 = [] primeFac2 = [] - ans = max(number1,number2) - + ans = max(number1, number2) + count1 = 0 count2 = 0 - - done = [] # captured numbers int both 'primeFac1' and 'primeFac2' - + + done = [] # captured numbers int both 'primeFac1' and 'primeFac2' + # iterates through primeFac1 for n in primeFac1: - + if n not in done: - + if n in primeFac2: - + count1 = primeFac1.count(n) count2 = primeFac2.count(n) - - for i in range(max(count1,count2)): + + for i in range(max(count1, count2)): ans *= n - + else: - + count1 = primeFac1.count(n) - + for i in range(count1): ans *= n - + done.append(n) - + # iterates through primeFac2 for n in primeFac2: - + if n not in done: - + count2 = primeFac2.count(n) - + for i in range(count2): ans *= n - + done.append(n) - + # precondition - assert isinstance(ans,int) and (ans >= 0), \ - "'ans' must been from type int and positive" - + assert isinstance(ans, int) and ( + ans >= 0 + ), "'ans' must been from type int and positive" + return ans - + + # ---------------------------------- - + + def getPrime(n): """ Gets the n-th prime number. input: positive integer 'n' >= 0 returns the n-th prime number, beginning at index 0 """ - + # precondition - assert isinstance(n,int) and (n >= 0), "'number' must been a positive int" - + assert isinstance(n, int) and (n >= 0), "'number' must been a positive int" + index = 0 - ans = 2 # this variable holds the answer - + ans = 2 # this variable holds the answer + while index < n: - + index += 1 - - ans += 1 # counts to the next number - + + ans += 1 # counts to the next number + # if ans not prime then - # runs to the next prime number. + # runs to the next prime number. while not isPrime(ans): ans += 1 - + # precondition - assert isinstance(ans,int) and isPrime(ans), \ - "'ans' must been a prime number and from type int" - + assert isinstance(ans, int) and isPrime( + ans + ), "'ans' must been a prime number and from type int" + return ans - + + # --------------------------------------------------- - + + def getPrimesBetween(pNumber1, pNumber2): """ input: prime numbers 'pNumber1' and 'pNumber2' pNumber1 < pNumber2 - returns a list of all prime numbers between 'pNumber1' (exclusiv) - and 'pNumber2' (exclusiv) - """ - - # precondition - assert isPrime(pNumber1) and isPrime(pNumber2) and (pNumber1 < pNumber2), \ - "The arguments must been prime numbers and 'pNumber1' < 'pNumber2'" - - number = pNumber1 + 1 # jump to the next number - - ans = [] # this list will be returns. - + returns a list of all prime numbers between 'pNumber1' (exclusive) + and 'pNumber2' (exclusive) + """ + + # precondition + assert ( + isPrime(pNumber1) and isPrime(pNumber2) and (pNumber1 < pNumber2) + ), "The arguments must been prime numbers and 'pNumber1' < 'pNumber2'" + + number = pNumber1 + 1 # jump to the next number + + ans = [] # this list will be returns. + # if number is not prime then - # fetch the next prime number. + # fetch the next prime number. while not isPrime(number): number += 1 - + while number < pNumber2: - + ans.append(number) - + number += 1 - - # fetch the next prime number. + + # fetch the next prime number. while not isPrime(number): number += 1 - + # precondition - assert isinstance(ans,list) and ans[0] != pNumber1 \ - and ans[len(ans)-1] != pNumber2, \ - "'ans' must been a list without the arguments" - + assert ( + isinstance(ans, list) and ans[0] != pNumber1 and ans[len(ans) - 1] != pNumber2 + ), "'ans' must been a list without the arguments" + # 'ans' contains not 'pNumber1' and 'pNumber2' ! return ans - + + # ---------------------------------------------------- + def getDivisors(n): """ input: positive integer 'n' >= 1 returns all divisors of n (inclusive 1 and 'n') """ - + # precondition - assert isinstance(n,int) and (n >= 1), "'n' must been int and >= 1" + assert isinstance(n, int) and (n >= 1), "'n' must been int and >= 1" + + ans = [] # will be returned. + + for divisor in range(1, n + 1): - from math import sqrt - - ans = [] # will be returned. - - for divisor in range(1,n+1): - if n % divisor == 0: ans.append(divisor) - - - #precondition - assert ans[0] == 1 and ans[len(ans)-1] == n, \ - "Error in function getDivisiors(...)" - - + + # precondition + assert ans[0] == 1 and ans[len(ans) - 1] == n, "Error in function getDivisiors(...)" + return ans @@ -523,82 +550,95 @@ def isPerfectNumber(number): input: positive integer 'number' > 1 returns true if 'number' is a perfect number otherwise false. """ - + # precondition - assert isinstance(number,int) and (number > 1), \ - "'number' must been an int and >= 1" - + assert isinstance(number, int) and ( + number > 1 + ), "'number' must been an int and >= 1" + divisors = getDivisors(number) - + # precondition - assert isinstance(divisors,list) and(divisors[0] == 1) and \ - (divisors[len(divisors)-1] == number), \ - "Error in help-function getDivisiors(...)" - + assert ( + isinstance(divisors, list) + and (divisors[0] == 1) + and (divisors[len(divisors) - 1] == number) + ), "Error in help-function getDivisiors(...)" + # summed all divisors up to 'number' (exclusive), hence [:-1] return sum(divisors[:-1]) == number + # ------------------------------------------------------------ + def simplifyFraction(numerator, denominator): """ input: two integer 'numerator' and 'denominator' assumes: 'denominator' != 0 returns: a tuple with simplify numerator and denominator. - """ - + """ + # precondition - assert isinstance(numerator, int) and isinstance(denominator,int) \ - and (denominator != 0), \ - "The arguments must been from type int and 'denominator' != 0" - + assert ( + isinstance(numerator, int) + and isinstance(denominator, int) + and (denominator != 0) + ), "The arguments must been from type int and 'denominator' != 0" + # build the greatest common divisor of numerator and denominator. gcdOfFraction = gcd(abs(numerator), abs(denominator)) # precondition - assert isinstance(gcdOfFraction, int) and (numerator % gcdOfFraction == 0) \ - and (denominator % gcdOfFraction == 0), \ - "Error in function gcd(...,...)" - + assert ( + isinstance(gcdOfFraction, int) + and (numerator % gcdOfFraction == 0) + and (denominator % gcdOfFraction == 0) + ), "Error in function gcd(...,...)" + return (numerator // gcdOfFraction, denominator // gcdOfFraction) - + + # ----------------------------------------------------------------- - + + def factorial(n): """ input: positive integer 'n' returns the factorial of 'n' (n!) """ - + # precondition - assert isinstance(n,int) and (n >= 0), "'n' must been a int and >= 0" - - ans = 1 # this will be return. - - for factor in range(1,n+1): + assert isinstance(n, int) and (n >= 0), "'n' must been a int and >= 0" + + ans = 1 # this will be return. + + for factor in range(1, n + 1): ans *= factor - + return ans - + + # ------------------------------------------------------------------- - + + def fib(n): """ input: positive integer 'n' returns the n-th fibonacci term , indexing by 0 - """ - + """ + # precondition assert isinstance(n, int) and (n >= 0), "'n' must been an int and >= 0" - + tmp = 0 fib1 = 1 - ans = 1 # this will be return - - for i in range(n-1): - + ans = 1 # this will be return + + for i in range(n - 1): + tmp = ans ans += fib1 fib1 = tmp - + return ans diff --git a/other/sdes.py b/other/sdes.py new file mode 100644 index 000000000000..3038ff193ae9 --- /dev/null +++ b/other/sdes.py @@ -0,0 +1,97 @@ +def apply_table(inp, table): + """ + >>> apply_table("0123456789", list(range(10))) + '9012345678' + >>> apply_table("0123456789", list(range(9, -1, -1))) + '8765432109' + """ + res = "" + for i in table: + res += inp[i - 1] + return res + + +def left_shift(data): + """ + >>> left_shift("0123456789") + '1234567890' + """ + return data[1:] + data[0] + + +def XOR(a, b): + """ + >>> XOR("01010101", "00001111") + '01011010' + """ + res = "" + for i in range(len(a)): + if a[i] == b[i]: + res += "0" + else: + res += "1" + return res + + +def apply_sbox(s, data): + row = int("0b" + data[0] + data[-1], 2) + col = int("0b" + data[1:3], 2) + return bin(s[row][col])[2:] + + +def function(expansion, s0, s1, key, message): + left = message[:4] + right = message[4:] + temp = apply_table(right, expansion) + temp = XOR(temp, key) + l = apply_sbox(s0, temp[:4]) + r = apply_sbox(s1, temp[4:]) + l = "0" * (2 - len(l)) + l + r = "0" * (2 - len(r)) + r + temp = apply_table(l + r, p4_table) + temp = XOR(left, temp) + return temp + right + + +if __name__ == "__main__": + + key = input("Enter 10 bit key: ") + message = input("Enter 8 bit message: ") + + p8_table = [6, 3, 7, 4, 8, 5, 10, 9] + p10_table = [3, 5, 2, 7, 4, 10, 1, 9, 8, 6] + p4_table = [2, 4, 3, 1] + IP = [2, 6, 3, 1, 4, 8, 5, 7] + IP_inv = [4, 1, 3, 5, 7, 2, 8, 6] + expansion = [4, 1, 2, 3, 2, 3, 4, 1] + s0 = [[1, 0, 3, 2], [3, 2, 1, 0], [0, 2, 1, 3], [3, 1, 3, 2]] + s1 = [[0, 1, 2, 3], [2, 0, 1, 3], [3, 0, 1, 0], [2, 1, 0, 3]] + + # key generation + temp = apply_table(key, p10_table) + left = temp[:5] + right = temp[5:] + left = left_shift(left) + right = left_shift(right) + key1 = apply_table(left + right, p8_table) + left = left_shift(left) + right = left_shift(right) + left = left_shift(left) + right = left_shift(right) + key2 = apply_table(left + right, p8_table) + + # encryption + temp = apply_table(message, IP) + temp = function(expansion, s0, s1, key1, temp) + temp = temp[4:] + temp[:4] + temp = function(expansion, s0, s1, key2, temp) + CT = apply_table(temp, IP_inv) + print("Cipher text is:", CT) + + # decryption + temp = apply_table(CT, IP) + temp = function(expansion, s0, s1, key2, temp) + temp = temp[4:] + temp[:4] + temp = function(expansion, s0, s1, key1, temp) + PT = apply_table(temp, IP_inv) + print("Plain text after decypting is:", PT) diff --git a/other/sierpinski_triangle.py b/other/sierpinski_triangle.py index fc22aad96059..e27db3a2e8c4 100644 --- a/other/sierpinski_triangle.py +++ b/other/sierpinski_triangle.py @@ -1,16 +1,18 @@ #!/usr/bin/python -# encoding=utf8 -'''Author Anurag Kumar | anuragkumarak95@gmail.com | git/anuragkumarak95 +"""Author Anurag Kumar | anuragkumarak95@gmail.com | git/anuragkumarak95 Simple example of Fractal generation using recursive function. What is Sierpinski Triangle? ->>The Sierpinski triangle (also with the original orthography Sierpinski), also called the Sierpinski gasket or the Sierpinski Sieve, -is a fractal and attractive fixed set with the overall shape of an equilateral triangle, subdivided recursively into smaller -equilateral triangles. Originally constructed as a curve, this is one of the basic examples of self-similar sets, i.e., -it is a mathematically generated pattern that can be reproducible at any magnification or reduction. It is named after -the Polish mathematician Wacław Sierpinski, but appeared as a decorative pattern many centuries prior to the work of Sierpinski. +>>The Sierpinski triangle (also with the original orthography Sierpinski), also called +the Sierpinski gasket or the Sierpinski Sieve, is a fractal and attractive fixed set +with the overall shape of an equilateral triangle, subdivided recursively into smaller +equilateral triangles. Originally constructed as a curve, this is one of the basic +examples of self-similar sets, i.e., it is a mathematically generated pattern that can +be reproducible at any magnification or reduction. It is named after the Polish +mathematician Wacław Sierpinski, but appeared as a decorative pattern many centuries +prior to the work of Sierpinski. Requirements(pip): - turtle @@ -21,48 +23,54 @@ Usage: - $python sierpinski_triangle.py -Credits: This code was written by editing the code from http://www.riannetrujillo.com/blog/python-fractal/ +Credits: This code was written by editing the code from +http://www.riannetrujillo.com/blog/python-fractal/ -''' +""" import turtle import sys -PROGNAME = 'Sierpinski Triangle' -points = [[-175,-125],[0,175],[175,-125]] #size of triangle +PROGNAME = "Sierpinski Triangle" -def getMid(p1,p2): - return ( (p1[0]+p2[0]) / 2, (p1[1] + p2[1]) / 2) #find midpoint +points = [[-175, -125], [0, 175], [175, -125]] # size of triangle -def triangle(points,depth): + +def getMid(p1, p2): + return ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2) # find midpoint + + +def triangle(points, depth): myPen.up() - myPen.goto(points[0][0],points[0][1]) + myPen.goto(points[0][0], points[0][1]) myPen.down() - myPen.goto(points[1][0],points[1][1]) - myPen.goto(points[2][0],points[2][1]) - myPen.goto(points[0][0],points[0][1]) - - if depth>0: - triangle([points[0], - getMid(points[0], points[1]), - getMid(points[0], points[2])], - depth-1) - triangle([points[1], - getMid(points[0], points[1]), - getMid(points[1], points[2])], - depth-1) - triangle([points[2], - getMid(points[2], points[1]), - getMid(points[0], points[2])], - depth-1) - - -if __name__ == '__main__': - if len(sys.argv) !=2: - raise ValueError('right format for using this script: ' - '$python fractals.py ') + myPen.goto(points[1][0], points[1][1]) + myPen.goto(points[2][0], points[2][1]) + myPen.goto(points[0][0], points[0][1]) + + if depth > 0: + triangle( + [points[0], getMid(points[0], points[1]), getMid(points[0], points[2])], + depth - 1, + ) + triangle( + [points[1], getMid(points[0], points[1]), getMid(points[1], points[2])], + depth - 1, + ) + triangle( + [points[2], getMid(points[2], points[1]), getMid(points[0], points[2])], + depth - 1, + ) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise ValueError( + "right format for using this script: " + "$python fractals.py " + ) myPen = turtle.Turtle() myPen.ht() myPen.speed(5) - myPen.pencolor('red') - triangle(points,int(sys.argv[1])) + myPen.pencolor("red") + triangle(points, int(sys.argv[1])) diff --git a/other/tower_of_hanoi.py b/other/tower_of_hanoi.py index 9cc5b9e40543..3cc0e40b369f 100644 --- a/other/tower_of_hanoi.py +++ b/other/tower_of_hanoi.py @@ -1,6 +1,5 @@ -from __future__ import print_function -def moveTower(height, fromPole, toPole, withPole): - ''' +def moveTower(height, fromPole, toPole, withPole): + """ >>> moveTower(3, 'A', 'B', 'C') moving disk from A to B moving disk from A to C @@ -9,18 +8,21 @@ def moveTower(height, fromPole, toPole, withPole): moving disk from C to A moving disk from C to B moving disk from A to B - ''' + """ if height >= 1: - moveTower(height-1, fromPole, withPole, toPole) + moveTower(height - 1, fromPole, withPole, toPole) moveDisk(fromPole, toPole) - moveTower(height-1, withPole, toPole, fromPole) + moveTower(height - 1, withPole, toPole, fromPole) + + +def moveDisk(fp, tp): + print("moving disk from", fp, "to", tp) -def moveDisk(fp,tp): - print('moving disk from', fp, 'to', tp) def main(): - height = int(input('Height of hanoi: ').strip()) - moveTower(height, 'A', 'B', 'C') + height = int(input("Height of hanoi: ").strip()) + moveTower(height, "A", "B", "C") + -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/other/two_sum.py b/other/two_sum.py index d4484aa85505..70d5c5375026 100644 --- a/other/two_sum.py +++ b/other/two_sum.py @@ -9,7 +9,7 @@ Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1]. """ -from __future__ import print_function + def twoSum(nums, target): """ @@ -19,11 +19,11 @@ def twoSum(nums, target): """ chk_map = {} for index, val in enumerate(nums): - compl = target - val - if compl in chk_map: - indices = [chk_map[compl], index] - print(indices) - return [indices] - else: - chk_map[val] = index + compl = target - val + if compl in chk_map: + indices = [chk_map[compl], index] + print(indices) + return [indices] + else: + chk_map[val] = index return False diff --git a/other/word_patterns.py b/other/word_patterns.py index c33d520087f7..d229954dea93 100644 --- a/other/word_patterns.py +++ b/other/word_patterns.py @@ -1,39 +1,44 @@ -from __future__ import print_function -import pprint, time - -def getWordPattern(word): +def get_word_pattern(word: str) -> str: + """ + >>> get_word_pattern("pattern") + '0.1.2.2.3.4.5' + >>> get_word_pattern("word pattern") + '0.1.2.3.4.5.6.7.7.8.2.9' + >>> get_word_pattern("get word pattern") + '0.1.2.3.4.5.6.7.3.8.9.2.2.1.6.10' + """ word = word.upper() - nextNum = 0 - letterNums = {} - wordPattern = [] + next_num = 0 + letter_nums = {} + word_pattern = [] for letter in word: - if letter not in letterNums: - letterNums[letter] = str(nextNum) - nextNum += 1 - wordPattern.append(letterNums[letter]) - return '.'.join(wordPattern) + if letter not in letter_nums: + letter_nums[letter] = str(next_num) + next_num += 1 + word_pattern.append(letter_nums[letter]) + return ".".join(word_pattern) -def main(): - startTime = time.time() - allPatterns = {} - with open('Dictionary.txt') as fo: - wordList = fo.read().split('\n') +if __name__ == "__main__": + import pprint + import time - for word in wordList: - pattern = getWordPattern(word) + start_time = time.time() + with open("dictionary.txt") as in_file: + wordList = in_file.read().splitlines() - if pattern not in allPatterns: - allPatterns[pattern] = [word] + all_patterns = {} + for word in wordList: + pattern = get_word_pattern(word) + if pattern in all_patterns: + all_patterns[pattern].append(word) else: - allPatterns[pattern].append(word) - - with open('Word Patterns.txt', 'w') as fo: - fo.write(pprint.pformat(allPatterns)) + all_patterns[pattern] = [word] - totalTime = round(time.time() - startTime, 2) - print(('Done! [', totalTime, 'seconds ]')) + with open("word_patterns.txt", "w") as out_file: + out_file.write(pprint.pformat(all_patterns)) -if __name__ == '__main__': - main() + totalTime = round(time.time() - start_time, 2) + print(f"Done! {len(all_patterns):,} word patterns found in {totalTime} seconds.") + # Done! 9,581 word patterns found in 0.58 seconds. diff --git a/project_euler/README.md b/project_euler/README.md index 9f77f719f0f1..f80d58ea0038 100644 --- a/project_euler/README.md +++ b/project_euler/README.md @@ -8,51 +8,4 @@ insights to solve. Project Euler is ideal for mathematicians who are learning to Here the efficiency of your code is also checked. I've tried to provide all the best possible solutions. -PROBLEMS: - -1. If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3,5,6 and 9. The sum of these multiples is 23. - Find the sum of all the multiples of 3 or 5 below N. - -2. Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, - the first 10 terms will be: - 1,2,3,5,8,13,21,34,55,89,.. - By considering the terms in the Fibonacci sequence whose values do not exceed n, find the sum of the even-valued terms. - e.g. for n=10, we have {2,8}, sum is 10. - -3. The prime factors of 13195 are 5,7,13 and 29. What is the largest prime factor of a given number N? - e.g. for 10, largest prime factor = 5. For 17, largest prime factor = 17. - -4. A palindromic number reads the same both ways. The largest palindrome made from the product of two 2-digit numbers is 9009 = 91 × 99. - Find the largest palindrome made from the product of two 3-digit numbers which is less than N. - -5. 2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder. - What is the smallest positive number that is evenly divisible(divisible with no remainder) by all of the numbers from 1 to N? - -6. The sum of the squares of the first ten natural numbers is, - 1^2 + 2^2 + ... + 10^2 = 385 - The square of the sum of the first ten natural numbers is, - (1 + 2 + ... + 10)^2 = 552 = 3025 - Hence the difference between the sum of the squares of the first ten natural numbers and the square of the sum is 3025 − 385 = 2640. - Find the difference between the sum of the squares of the first N natural numbers and the square of the sum. - -7. By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13. - What is the Nth prime number? - -9. A Pythagorean triplet is a set of three natural numbers, a < b < c, for which, - a^2 + b^2 = c^2 - There exists exactly one Pythagorean triplet for which a + b + c = 1000. - Find the product abc. - -14. The following iterative sequence is defined for the set of positive integers: - n → n/2 (n is even) - n → 3n + 1 (n is odd) - Using the rule above and starting with 13, we generate the following sequence: - 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1 - Which starting number, under one million, produces the longest chain? - -16. 2^15 = 32768 and the sum of its digits is 3 + 2 + 7 + 6 + 8 = 26. - What is the sum of the digits of the number 2^1000? -20. n! means n × (n − 1) × ... × 3 × 2 × 1 - For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, - and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. - Find the sum of the digits in the number 100! +For description of the problem statements, kindly visit https://projecteuler.net/show=all diff --git a/project_euler/problem_01/sol1.py b/project_euler/problem_01/sol1.py index 1433129af303..e81156edaee4 100644 --- a/project_euler/problem_01/sol1.py +++ b/project_euler/problem_01/sol1.py @@ -4,17 +4,11 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -31,4 +25,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol2.py b/project_euler/problem_01/sol2.py index e58fb03a8fb0..8041c7ffa589 100644 --- a/project_euler/problem_01/sol2.py +++ b/project_euler/problem_01/sol2.py @@ -4,17 +4,11 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -36,4 +30,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol3.py b/project_euler/problem_01/sol3.py index 013ce5e54fdf..c0bcbc06ec83 100644 --- a/project_euler/problem_01/sol3.py +++ b/project_euler/problem_01/sol3.py @@ -4,12 +4,6 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): @@ -17,7 +11,7 @@ def solution(n): This solution is based on the pattern that the successive numbers in the series follow: 0+3,+2,+1,+3,+1,+2,+3. Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -63,4 +57,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol4.py b/project_euler/problem_01/sol4.py index 90403c3bd6a3..e01dc977d8cf 100644 --- a/project_euler/problem_01/sol4.py +++ b/project_euler/problem_01/sol4.py @@ -4,17 +4,11 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -50,4 +44,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol5.py b/project_euler/problem_01/sol5.py index 302fe44f8bfa..bd96d965f92d 100644 --- a/project_euler/problem_01/sol5.py +++ b/project_euler/problem_01/sol5.py @@ -4,19 +4,13 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 """A straightforward pythonic solution using list comprehension""" def solution(n): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -31,4 +25,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol6.py b/project_euler/problem_01/sol6.py index cf6e751d4c05..c9f94b9f77c8 100644 --- a/project_euler/problem_01/sol6.py +++ b/project_euler/problem_01/sol6.py @@ -4,17 +4,11 @@ we get 3,5,6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution(3) 0 >>> solution(4) @@ -37,4 +31,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_01/sol7.py b/project_euler/problem_01/sol7.py new file mode 100644 index 000000000000..a0510b54c409 --- /dev/null +++ b/project_euler/problem_01/sol7.py @@ -0,0 +1,32 @@ +""" +Problem Statement: +If we list all the natural numbers below 10 that are multiples of 3 or 5, +we get 3,5,6 and 9. The sum of these multiples is 23. +Find the sum of all the multiples of 3 or 5 below N. +""" + + +def solution(n): + """Returns the sum of all the multiples of 3 or 5 below n. + + >>> solution(3) + 0 + >>> solution(4) + 3 + >>> solution(10) + 23 + >>> solution(600) + 83700 + """ + + result = 0 + for i in range(n): + if i % 3 == 0: + result += i + elif i % 5 == 0: + result += i + return result + + +if __name__ == "__main__": + print(solution(int(input().strip()))) diff --git a/project_euler/problem_02/sol1.py b/project_euler/problem_02/sol1.py index f61d04e3dfce..ec89ddaeb2b5 100644 --- a/project_euler/problem_02/sol1.py +++ b/project_euler/problem_02/sol1.py @@ -9,18 +9,12 @@ n, find the sum of the even-valued terms. e.g. for n=10, we have {2,8}, sum is 10. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all fibonacci sequence even elements that are lower or equals to n. - + >>> solution(10) 10 >>> solution(15) @@ -44,4 +38,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_02/sol2.py b/project_euler/problem_02/sol2.py index 3e103a6a4373..bc5040cc6b3b 100644 --- a/project_euler/problem_02/sol2.py +++ b/project_euler/problem_02/sol2.py @@ -9,18 +9,12 @@ n, find the sum of the even-valued terms. e.g. for n=10, we have {2,8}, sum is 10. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all fibonacci sequence even elements that are lower or equals to n. - + >>> solution(10) [2, 8] >>> solution(15) @@ -42,4 +36,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_02/sol3.py b/project_euler/problem_02/sol3.py index abd9d6c753b8..f29f21c287e5 100644 --- a/project_euler/problem_02/sol3.py +++ b/project_euler/problem_02/sol3.py @@ -9,18 +9,12 @@ n, find the sum of the even-valued terms. e.g. for n=10, we have {2,8}, sum is 10. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the sum of all fibonacci sequence even elements that are lower or equals to n. - + >>> solution(10) 10 >>> solution(15) @@ -44,4 +38,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_02/sol4.py b/project_euler/problem_02/sol4.py index 5e8c04899f3d..92ea0a51e026 100644 --- a/project_euler/problem_02/sol4.py +++ b/project_euler/problem_02/sol4.py @@ -9,20 +9,14 @@ n, find the sum of the even-valued terms. e.g. for n=10, we have {2,8}, sum is 10. """ -from __future__ import print_function import math from decimal import Decimal, getcontext -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - def solution(n): """Returns the sum of all fibonacci sequence even elements that are lower or equals to n. - + >>> solution(10) 10 >>> solution(15) @@ -68,4 +62,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_02/sol5.py b/project_euler/problem_02/sol5.py new file mode 100644 index 000000000000..180906cf8717 --- /dev/null +++ b/project_euler/problem_02/sol5.py @@ -0,0 +1,46 @@ +""" +Problem: +Each new term in the Fibonacci sequence is generated by adding the previous two +terms. By starting with 1 and 2, the first 10 terms will be: + + 1,2,3,5,8,13,21,34,55,89,.. + +By considering the terms in the Fibonacci sequence whose values do not exceed +n, find the sum of the even-valued terms. e.g. for n=10, we have {2,8}, sum is +10. +""" + + +def solution(n): + """Returns the sum of all fibonacci sequence even elements that are lower + or equals to n. + + >>> solution(10) + 10 + >>> solution(15) + 10 + >>> solution(2) + 2 + >>> solution(1) + 0 + >>> solution(34) + 44 + """ + + a = [0, 1] + i = 0 + while a[i] <= n: + a.append(a[i] + a[i + 1]) + if a[i + 2] > n: + break + i += 1 + sum = 0 + for j in range(len(a) - 1): + if a[j] % 2 == 0: + sum += a[j] + + return sum + + +if __name__ == "__main__": + print(solution(int(input().strip()))) diff --git a/project_euler/problem_03/sol1.py b/project_euler/problem_03/sol1.py index ab19d8b30457..9f8ecc5e6565 100644 --- a/project_euler/problem_03/sol1.py +++ b/project_euler/problem_03/sol1.py @@ -5,14 +5,8 @@ e.g. for 10, largest prime factor = 5. For 17, largest prime factor = 17. """ -from __future__ import print_function, division import math -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - def isprime(no): if no == 2: @@ -81,4 +75,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_03/sol2.py b/project_euler/problem_03/sol2.py index f93a0b75f4e0..b6fad079fa31 100644 --- a/project_euler/problem_03/sol2.py +++ b/project_euler/problem_03/sol2.py @@ -5,12 +5,6 @@ e.g. for 10, largest prime factor = 5. For 17, largest prime factor = 17. """ -from __future__ import print_function, division - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): @@ -60,4 +54,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_03/sol3.py b/project_euler/problem_03/sol3.py new file mode 100644 index 000000000000..5fe45df59984 --- /dev/null +++ b/project_euler/problem_03/sol3.py @@ -0,0 +1,63 @@ +""" +Problem: +The prime factors of 13195 are 5,7,13 and 29. What is the largest prime factor +of a given number N? + +e.g. for 10, largest prime factor = 5. For 17, largest prime factor = 17. +""" + + +def solution(n: int) -> int: + """Returns the largest prime factor of a given number n. + + >>> solution(13195) + 29 + >>> solution(10) + 5 + >>> solution(17) + 17 + >>> solution(3.4) + 3 + >>> solution(0) + Traceback (most recent call last): + ... + ValueError: Parameter n must be greater or equal to one. + >>> solution(-17) + Traceback (most recent call last): + ... + ValueError: Parameter n must be greater or equal to one. + >>> solution([]) + Traceback (most recent call last): + ... + TypeError: Parameter n must be int or passive of cast to int. + >>> solution("asd") + Traceback (most recent call last): + ... + TypeError: Parameter n must be int or passive of cast to int. + """ + try: + n = int(n) + except (TypeError, ValueError): + raise TypeError("Parameter n must be int or passive of cast to int.") + if n <= 0: + raise ValueError("Parameter n must be greater or equal to one.") + i = 2 + ans = 0 + if n == 2: + return 2 + while n > 2: + while n % i != 0: + i += 1 + ans = i + while n % i == 0: + n = n / i + i += 1 + + return int(ans) + + +if __name__ == "__main__": + # print(solution(int(input().strip()))) + import doctest + + doctest.testmod() diff --git a/project_euler/problem_04/sol1.py b/project_euler/problem_04/sol1.py index 7a255f7308e6..599345b5ab79 100644 --- a/project_euler/problem_04/sol1.py +++ b/project_euler/problem_04/sol1.py @@ -6,18 +6,12 @@ Find the largest palindrome made from the product of two 3-digit numbers which is less than N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the largest palindrome made from the product of two 3-digit numbers which is less than n. - + >>> solution(20000) 19591 >>> solution(30000) @@ -25,7 +19,7 @@ def solution(n): >>> solution(40000) 39893 """ - # fetchs the next number + # fetches the next number for number in range(n - 1, 10000, -1): # converts number into string. @@ -39,12 +33,10 @@ def solution(n): # if 'number' is a product of two 3-digit numbers # then number is the answer otherwise fetch next number. while divisor != 99: - if (number % divisor == 0) and ( - len(str(int(number / divisor))) == 3 - ): + if (number % divisor == 0) and (len(str(int(number / divisor))) == 3): return number divisor -= 1 if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_04/sol2.py b/project_euler/problem_04/sol2.py index 45c6b256daf8..0f185f1216ab 100644 --- a/project_euler/problem_04/sol2.py +++ b/project_euler/problem_04/sol2.py @@ -6,18 +6,12 @@ Find the largest palindrome made from the product of two 3-digit numbers which is less than N. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the largest palindrome made from the product of two 3-digit numbers which is less than n. - + >>> solution(20000) 19591 >>> solution(30000) @@ -26,7 +20,7 @@ def solution(n): 39893 """ answer = 0 - for i in range(999, 99, -1): # 3 digit nimbers range from 999 down to 100 + for i in range(999, 99, -1): # 3 digit numbers range from 999 down to 100 for j in range(999, 99, -1): t = str(i * j) if t == t[::-1] and i * j < n: @@ -35,4 +29,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_05/sol1.py b/project_euler/problem_05/sol1.py index e2deb91fb6aa..b3a231f4dcf5 100644 --- a/project_euler/problem_05/sol1.py +++ b/project_euler/problem_05/sol1.py @@ -6,12 +6,6 @@ What is the smallest positive number that is evenly divisible(divisible with no remainder) by all of the numbers from 1 to N? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): @@ -66,4 +60,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_05/sol2.py b/project_euler/problem_05/sol2.py index 293dd96f2294..5aa84d21c8e8 100644 --- a/project_euler/problem_05/sol2.py +++ b/project_euler/problem_05/sol2.py @@ -6,13 +6,6 @@ What is the smallest positive number that is evenly divisible(divisible with no remainder) by all of the numbers from 1 to N? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - """ Euclidean GCD Algorithm """ @@ -30,7 +23,7 @@ def lcm(x, y): def solution(n): """Returns the smallest positive number that is evenly divisible(divisible with no remainder) by all of the numbers from 1 to n. - + >>> solution(10) 2520 >>> solution(15) @@ -47,4 +40,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_06/sol1.py b/project_euler/problem_06/sol1.py index 728701e167c3..513b354679a9 100644 --- a/project_euler/problem_06/sol1.py +++ b/project_euler/problem_06/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Problem: @@ -14,18 +13,12 @@ Find the difference between the sum of the squares of the first N natural numbers and the square of the sum. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the difference between the sum of the squares of the first n natural numbers and the square of the sum. - + >>> solution(10) 2640 >>> solution(15) @@ -45,4 +38,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_06/sol2.py b/project_euler/problem_06/sol2.py index 2c64812d56f8..18cdb51752ea 100644 --- a/project_euler/problem_06/sol2.py +++ b/project_euler/problem_06/sol2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Problem: @@ -14,18 +13,12 @@ Find the difference between the sum of the squares of the first N natural numbers and the square of the sum. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): """Returns the difference between the sum of the squares of the first n natural numbers and the square of the sum. - + >>> solution(10) 2640 >>> solution(15) @@ -42,4 +35,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_06/sol3.py b/project_euler/problem_06/sol3.py index 7d94b1e2254f..ee739c9a1293 100644 --- a/project_euler/problem_06/sol3.py +++ b/project_euler/problem_06/sol3.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Problem: @@ -14,19 +13,13 @@ Find the difference between the sum of the squares of the first N natural numbers and the square of the sum. """ -from __future__ import print_function import math -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - def solution(n): """Returns the difference between the sum of the squares of the first n natural numbers and the square of the sum. - + >>> solution(10) 2640 >>> solution(15) @@ -42,4 +35,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_06/sol4.py b/project_euler/problem_06/sol4.py new file mode 100644 index 000000000000..07eed57ba9b5 --- /dev/null +++ b/project_euler/problem_06/sol4.py @@ -0,0 +1,39 @@ +""" +Problem: + +The sum of the squares of the first ten natural numbers is, + 1^2 + 2^2 + ... + 10^2 = 385 + +The square of the sum of the first ten natural numbers is, + (1 + 2 + ... + 10)^2 = 552 = 3025 + +Hence the difference between the sum of the squares of the first ten natural +numbers and the square of the sum is 3025 − 385 = 2640. + +Find the difference between the sum of the squares of the first N natural +numbers and the square of the sum. +""" + + +def solution(n): + """Returns the difference between the sum of the squares of the first n + natural numbers and the square of the sum. + + >>> solution(10) + 2640 + >>> solution(15) + 13160 + >>> solution(20) + 41230 + >>> solution(50) + 1582700 + >>> solution(100) + 25164150 + """ + sum_of_squares = n * (n + 1) * (2 * n + 1) / 6 + square_of_sum = (n * (n + 1) / 2) ** 2 + return int(square_of_sum - sum_of_squares) + + +if __name__ == "__main__": + print(solution(int(input("Enter a number: ").strip()))) diff --git a/project_euler/problem_07/sol1.py b/project_euler/problem_07/sol1.py index 403ded568dda..373915f886e1 100644 --- a/project_euler/problem_07/sol1.py +++ b/project_euler/problem_07/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ By listing the first six prime numbers: @@ -6,16 +5,10 @@ We can see that the 6th prime is 13. What is the Nth prime number? """ -from __future__ import print_function from math import sqrt -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -def isprime(n): +def is_prime(n): if n == 2: return True elif n % 2 == 0: @@ -30,7 +23,7 @@ def isprime(n): def solution(n): """Returns the n-th prime number. - + >>> solution(6) 13 >>> solution(1) @@ -48,14 +41,14 @@ def solution(n): j = 1 while i != n and j < 3: j += 1 - if isprime(j): + if is_prime(j): i += 1 while i != n: j += 2 - if isprime(j): + if is_prime(j): i += 1 return j if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_07/sol2.py b/project_euler/problem_07/sol2.py index 67336f7c1c96..6bfc5881f548 100644 --- a/project_euler/problem_07/sol2.py +++ b/project_euler/problem_07/sol2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ By listing the first six prime numbers: @@ -6,12 +5,6 @@ We can see that the 6th prime is 13. What is the Nth prime number? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def isprime(number): @@ -23,7 +16,7 @@ def isprime(number): def solution(n): """Returns the n-th prime number. - + >>> solution(6) 13 >>> solution(1) @@ -73,4 +66,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_07/sol3.py b/project_euler/problem_07/sol3.py index bc94762604b3..9b02ea87ec49 100644 --- a/project_euler/problem_07/sol3.py +++ b/project_euler/problem_07/sol3.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ By listing the first six prime numbers: @@ -6,15 +5,9 @@ We can see that the 6th prime is 13. What is the Nth prime number? """ -from __future__ import print_function import math import itertools -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - def primeCheck(number): if number % 2 == 0 and number > 2: @@ -32,7 +25,7 @@ def prime_generator(): def solution(n): """Returns the n-th prime number. - + >>> solution(6) 13 >>> solution(1) @@ -50,4 +43,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_08/sol1.py b/project_euler/problem_08/sol1.py index 6752fae3de60..e7582d46c351 100644 --- a/project_euler/problem_08/sol1.py +++ b/project_euler/problem_08/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The four adjacent digits in the 1000-digit number that have the greatest product are 9 × 9 × 8 × 9 = 5832. diff --git a/project_euler/problem_08/sol2.py b/project_euler/problem_08/sol2.py index bae96e373d6c..bf8afa8379ee 100644 --- a/project_euler/problem_08/sol2.py +++ b/project_euler/problem_08/sol2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The four adjacent digits in the 1000-digit number that have the greatest product are 9 × 9 × 8 × 9 = 5832. diff --git a/project_euler/problem_08/sol3.py b/project_euler/problem_08/sol3.py new file mode 100644 index 000000000000..dfbef5755dd7 --- /dev/null +++ b/project_euler/problem_08/sol3.py @@ -0,0 +1,82 @@ +""" +The four adjacent digits in the 1000-digit number that have the greatest +product are 9 × 9 × 8 × 9 = 5832. + +73167176531330624919225119674426574742355349194934 +96983520312774506326239578318016984801869478851843 +85861560789112949495459501737958331952853208805511 +12540698747158523863050715693290963295227443043557 +66896648950445244523161731856403098711121722383113 +62229893423380308135336276614282806444486645238749 +30358907296290491560440772390713810515859307960866 +70172427121883998797908792274921901699720888093776 +65727333001053367881220235421809751254540594752243 +52584907711670556013604839586446706324415722155397 +53697817977846174064955149290862569321978468622482 +83972241375657056057490261407972968652414535100474 +82166370484403199890008895243450658541227588666881 +16427171479924442928230863465674813919123162824586 +17866458359124566529476545682848912883142607690042 +24219022671055626321111109370544217506941658960408 +07198403850962455444362981230987879927244284909188 +84580156166097919133875499200524063689912560717606 +05886116467109405077541002256983155200055935729725 +71636269561882670428252483600823257530420752963450 + +Find the thirteen adjacent digits in the 1000-digit number that have the +greatest product. What is the value of this product? +""" +import sys + +N = """73167176531330624919225119674426574742355349194934\ +96983520312774506326239578318016984801869478851843\ +85861560789112949495459501737958331952853208805511\ +12540698747158523863050715693290963295227443043557\ +66896648950445244523161731856403098711121722383113\ +62229893423380308135336276614282806444486645238749\ +30358907296290491560440772390713810515859307960866\ +70172427121883998797908792274921901699720888093776\ +65727333001053367881220235421809751254540594752243\ +52584907711670556013604839586446706324415722155397\ +53697817977846174064955149290862569321978468622482\ +83972241375657056057490261407972968652414535100474\ +82166370484403199890008895243450658541227588666881\ +16427171479924442928230863465674813919123162824586\ +17866458359124566529476545682848912883142607690042\ +24219022671055626321111109370544217506941658960408\ +07198403850962455444362981230987879927244284909188\ +84580156166097919133875499200524063689912560717606\ +05886116467109405077541002256983155200055935729725\ +71636269561882670428252483600823257530420752963450""" + + +def streval(s: str) -> int: + ret = 1 + for it in s: + ret *= int(it) + return ret + + +def solution(n: str) -> int: + """Find the thirteen adjacent digits in the 1000-digit number n that have + the greatest product and returns it. + + >>> solution(N) + 23514624000 + """ + LargestProduct = -sys.maxsize - 1 + substr = n[:13] + cur_index = 13 + while cur_index < len(n) - 13: + if int(n[cur_index]) >= int(substr[0]): + substr = substr[1:] + n[cur_index] + cur_index += 1 + else: + LargestProduct = max(LargestProduct, streval(substr)) + substr = n[cur_index : cur_index + 13] + cur_index += 13 + return LargestProduct + + +if __name__ == "__main__": + print(solution(N)) diff --git a/project_euler/problem_09/sol1.py b/project_euler/problem_09/sol1.py index d9ebe8760861..3bb5c968115d 100644 --- a/project_euler/problem_09/sol1.py +++ b/project_euler/problem_09/sol1.py @@ -7,7 +7,6 @@ There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the product abc. """ -from __future__ import print_function def solution(): @@ -17,7 +16,7 @@ def solution(): 1. a < b < c 2. a**2 + b**2 = c**2 3. a + b + c = 1000 - + # The code below has been commented due to slow execution affecting Travis. # >>> solution() # 31875000 diff --git a/project_euler/problem_09/sol2.py b/project_euler/problem_09/sol2.py index 674daae9ec8e..de7b12d40c09 100644 --- a/project_euler/problem_09/sol2.py +++ b/project_euler/problem_09/sol2.py @@ -7,12 +7,6 @@ There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the product abc. """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): @@ -22,7 +16,7 @@ def solution(n): 1. a < b < c 2. a**2 + b**2 = c**2 3. a + b + c = 1000 - + >>> solution(1000) 31875000 """ @@ -41,4 +35,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_09/sol3.py b/project_euler/problem_09/sol3.py index 006029c8a30d..a6df46a3a66b 100644 --- a/project_euler/problem_09/sol3.py +++ b/project_euler/problem_09/sol3.py @@ -10,7 +10,6 @@ There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the product abc. """ -from __future__ import print_function def solution(): diff --git a/project_euler/problem_10/sol1.py b/project_euler/problem_10/sol1.py index 49384d7c78f0..c81085951ecf 100644 --- a/project_euler/problem_10/sol1.py +++ b/project_euler/problem_10/sol1.py @@ -4,22 +4,11 @@ Find the sum of all the primes below two million. """ -from __future__ import print_function from math import sqrt -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - def is_prime(n): - for i in xrange(2, int(sqrt(n)) + 1): + for i in range(2, int(sqrt(n)) + 1): if n % i == 0: return False @@ -32,7 +21,7 @@ def sum_of_primes(n): else: return 0 - for i in xrange(3, n, 2): + for i in range(3, n, 2): if is_prime(i): sumOfPrimes += i @@ -41,7 +30,7 @@ def sum_of_primes(n): def solution(n): """Returns the sum of all the primes below n. - + # The code below has been commented due to slow execution affecting Travis. # >>> solution(2000000) # 142913828922 @@ -58,4 +47,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_10/sol2.py b/project_euler/problem_10/sol2.py index 451a4ae5e8f3..b2e2b6e1adf3 100644 --- a/project_euler/problem_10/sol2.py +++ b/project_euler/problem_10/sol2.py @@ -4,15 +4,9 @@ Find the sum of all the primes below two million. """ -from __future__ import print_function import math from itertools import takewhile -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - def primeCheck(number): if number % 2 == 0 and number > 2: @@ -30,7 +24,7 @@ def prime_generator(): def solution(n): """Returns the sum of all the primes below n. - + # The code below has been commented due to slow execution affecting Travis. # >>> solution(2000000) # 142913828922 @@ -47,4 +41,4 @@ def solution(n): if __name__ == "__main__": - print(solution(int(raw_input().strip()))) + print(solution(int(input().strip()))) diff --git a/project_euler/problem_10/sol3.py b/project_euler/problem_10/sol3.py new file mode 100644 index 000000000000..e5bc0731d8ab --- /dev/null +++ b/project_euler/problem_10/sol3.py @@ -0,0 +1,58 @@ +""" +https://projecteuler.net/problem=10 + +Problem Statement: +The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17. + +Find the sum of all the primes below two million using Sieve_of_Eratosthenes: + +The sieve of Eratosthenes is one of the most efficient ways to find all primes +smaller than n when n is smaller than 10 million. Only for positive numbers. +""" + + +def prime_sum(n: int) -> int: + """ Returns the sum of all the primes below n. + + >>> prime_sum(2_000_000) + 142913828922 + >>> prime_sum(1_000) + 76127 + >>> prime_sum(5_000) + 1548136 + >>> prime_sum(10_000) + 5736396 + >>> prime_sum(7) + 10 + >>> prime_sum(7.1) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: 'float' object cannot be interpreted as an integer + >>> prime_sum(-7) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + IndexError: list assignment index out of range + >>> prime_sum("seven") # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: can only concatenate str (not "int") to str + """ + list_ = [0 for i in range(n + 1)] + list_[0] = 1 + list_[1] = 1 + + for i in range(2, int(n ** 0.5) + 1): + if list_[i] == 0: + for j in range(i * i, n + 1, i): + list_[j] = 1 + s = 0 + for i in range(n): + if list_[i] == 0: + s += i + return s + + +if __name__ == "__main__": + # import doctest + # doctest.testmod() + print(prime_sum(int(input().strip()))) diff --git a/project_euler/problem_11/sol1.py b/project_euler/problem_11/sol1.py index 3bdddc89d917..4e49013c8210 100644 --- a/project_euler/problem_11/sol1.py +++ b/project_euler/problem_11/sol1.py @@ -24,14 +24,8 @@ 01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48 """ -from __future__ import print_function import os -try: - xrange # Python 2 -except NameError: - xrange = range # Python 2 - def largest_product(grid): nColumns = len(grid[0]) @@ -43,14 +37,10 @@ def largest_product(grid): # Check vertically, horizontally, diagonally at the same time (only works # for nxn grid) - for i in xrange(nColumns): - for j in xrange(nRows - 3): - vertProduct = ( - grid[j][i] * grid[j + 1][i] * grid[j + 2][i] * grid[j + 3][i] - ) - horzProduct = ( - grid[i][j] * grid[i][j + 1] * grid[i][j + 2] * grid[i][j + 3] - ) + for i in range(nColumns): + for j in range(nRows - 3): + vertProduct = grid[j][i] * grid[j + 1][i] * grid[j + 2][i] * grid[j + 3][i] + horzProduct = grid[i][j] * grid[i][j + 1] * grid[i][j + 2] * grid[i][j + 3] # Left-to-right diagonal (\) product if i < nColumns - 3: @@ -70,9 +60,7 @@ def largest_product(grid): * grid[i - 3][j + 3] ) - maxProduct = max( - vertProduct, horzProduct, lrDiagProduct, rlDiagProduct - ) + maxProduct = max(vertProduct, horzProduct, lrDiagProduct, rlDiagProduct) if maxProduct > largest: largest = maxProduct @@ -81,7 +69,7 @@ def largest_product(grid): def solution(): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution() 70600674 """ @@ -90,7 +78,7 @@ def solution(): for line in file: grid.append(line.strip("\n").split(" ")) - grid = [[int(i) for i in grid[j]] for j in xrange(len(grid))] + grid = [[int(i) for i in grid[j]] for j in range(len(grid))] return largest_product(grid) diff --git a/project_euler/problem_11/sol2.py b/project_euler/problem_11/sol2.py index 0a5785b42b2c..64702e852b0f 100644 --- a/project_euler/problem_11/sol2.py +++ b/project_euler/problem_11/sol2.py @@ -24,63 +24,47 @@ 01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48 """ -from __future__ import print_function import os -try: - xrange # Python 2 -except NameError: - xrange = range # Python 2 - def solution(): """Returns the sum of all the multiples of 3 or 5 below n. - + >>> solution() 70600674 """ with open(os.path.dirname(__file__) + "/grid.txt") as f: l = [] - for i in xrange(20): + for i in range(20): l.append([int(x) for x in f.readline().split()]) maximum = 0 # right - for i in xrange(20): - for j in xrange(17): + for i in range(20): + for j in range(17): temp = l[i][j] * l[i][j + 1] * l[i][j + 2] * l[i][j + 3] if temp > maximum: maximum = temp # down - for i in xrange(17): - for j in xrange(20): + for i in range(17): + for j in range(20): temp = l[i][j] * l[i + 1][j] * l[i + 2][j] * l[i + 3][j] if temp > maximum: maximum = temp # diagonal 1 - for i in xrange(17): - for j in xrange(17): - temp = ( - l[i][j] - * l[i + 1][j + 1] - * l[i + 2][j + 2] - * l[i + 3][j + 3] - ) + for i in range(17): + for j in range(17): + temp = l[i][j] * l[i + 1][j + 1] * l[i + 2][j + 2] * l[i + 3][j + 3] if temp > maximum: maximum = temp # diagonal 2 - for i in xrange(17): - for j in xrange(3, 20): - temp = ( - l[i][j] - * l[i + 1][j - 1] - * l[i + 2][j - 2] - * l[i + 3][j - 3] - ) + for i in range(17): + for j in range(3, 20): + temp = l[i][j] * l[i + 1][j - 1] * l[i + 2][j - 2] * l[i + 3][j - 3] if temp > maximum: maximum = temp return maximum diff --git a/project_euler/problem_12/sol1.py b/project_euler/problem_12/sol1.py index 54476110b503..7e080c4e45a1 100644 --- a/project_euler/problem_12/sol1.py +++ b/project_euler/problem_12/sol1.py @@ -21,18 +21,12 @@ What is the value of the first triangle number to have over five hundred divisors? """ -from __future__ import print_function from math import sqrt -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - def count_divisors(n): nDivisors = 0 - for i in xrange(1, int(sqrt(n)) + 1): + for i in range(1, int(sqrt(n)) + 1): if n % i == 0: nDivisors += 2 # check if n is perfect square @@ -44,7 +38,7 @@ def count_divisors(n): def solution(): """Returns the value of the first triangle number to have over five hundred divisors. - + # The code below has been commented due to slow execution affecting Travis. # >>> solution() # 76576500 diff --git a/project_euler/problem_12/sol2.py b/project_euler/problem_12/sol2.py index 0d1502830bee..5ff0d8349b90 100644 --- a/project_euler/problem_12/sol2.py +++ b/project_euler/problem_12/sol2.py @@ -21,7 +21,6 @@ What is the value of the first triangle number to have over five hundred divisors? """ -from __future__ import print_function def triangle_number_generator(): @@ -30,22 +29,18 @@ def triangle_number_generator(): def count_divisors(n): - return sum( - [2 for i in range(1, int(n ** 0.5) + 1) if n % i == 0 and i * i != n] - ) + return sum([2 for i in range(1, int(n ** 0.5) + 1) if n % i == 0 and i * i != n]) def solution(): """Returns the value of the first triangle number to have over five hundred divisors. - + # The code below has been commented due to slow execution affecting Travis. # >>> solution() # 76576500 """ - return next( - i for i in triangle_number_generator() if count_divisors(i) > 500 - ) + return next(i for i in triangle_number_generator() if count_divisors(i) > 500) if __name__ == "__main__": diff --git a/project_euler/problem_13/sol1.py b/project_euler/problem_13/sol1.py index 983347675b3f..e36065ec8e11 100644 --- a/project_euler/problem_13/sol1.py +++ b/project_euler/problem_13/sol1.py @@ -3,18 +3,12 @@ Work out the first ten digits of the sum of the following one-hundred 50-digit numbers. """ -from __future__ import print_function -import os - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(array): """Returns the first ten digits of the sum of the array elements. - + + >>> import os >>> sum = 0 >>> array = [] >>> with open(os.path.dirname(__file__) + "/num.txt","r") as f: diff --git a/project_euler/problem_14/sol1.py b/project_euler/problem_14/sol1.py index 8d3efbc59eb5..fda45bc94bb7 100644 --- a/project_euler/problem_14/sol1.py +++ b/project_euler/problem_14/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Problem Statement: The following iterative sequence is defined for the set of positive integers: @@ -16,12 +15,6 @@ Which starting number, under one million, produces the longest chain? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def solution(n): @@ -29,7 +22,7 @@ def solution(n): formula: n → n/2 (n is even) n → 3n + 1 (n is odd) - + # The code below has been commented due to slow execution affecting Travis. # >>> solution(1000000) # {'counter': 525, 'largest_number': 837799} @@ -62,13 +55,7 @@ def solution(n): if __name__ == "__main__": - result = solution(int(raw_input().strip())) + result = solution(int(input().strip())) print( - ( - "Largest Number:", - result["largest_number"], - "->", - result["counter"], - "digits", - ) + ("Largest Number:", result["largest_number"], "->", result["counter"], "digits") ) diff --git a/project_euler/problem_14/sol2.py b/project_euler/problem_14/sol2.py index 0ec80e221f09..375a34c72f57 100644 --- a/project_euler/problem_14/sol2.py +++ b/project_euler/problem_14/sol2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Collatz conjecture: start with any positive integer n. Next term obtained from the previous term as follows: @@ -24,12 +23,6 @@ Which starting number, under one million, produces the longest chain? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def collatz_sequence(n): @@ -63,7 +56,7 @@ def solution(n): if __name__ == "__main__": - result = solution(int(raw_input().strip())) + result = solution(int(input().strip())) print( "Longest Collatz sequence under one million is %d with length %d" % (result["largest_number"], result["counter"]) diff --git a/project_euler/problem_15/sol1.py b/project_euler/problem_15/sol1.py index de58bb436d68..1be7d10ed674 100644 --- a/project_euler/problem_15/sol1.py +++ b/project_euler/problem_15/sol1.py @@ -35,9 +35,7 @@ def lattice_paths(n): 2 """ - n = ( - 2 * n - ) # middle entry of odd rows starting at row 3 is the solution for n = 1, + n = 2 * n # middle entry of odd rows starting at row 3 is the solution for n = 1, # 2, 3,... k = n / 2 diff --git a/project_euler/problem_18/solution.py b/project_euler/problem_18/solution.py new file mode 100644 index 000000000000..38593813901e --- /dev/null +++ b/project_euler/problem_18/solution.py @@ -0,0 +1,64 @@ +""" +By starting at the top of the triangle below and moving to adjacent numbers on +the row below, the maximum total from top to bottom is 23. + +3 +7 4 +2 4 6 +8 5 9 3 + +That is, 3 + 7 + 4 + 9 = 23. + +Find the maximum total from top to bottom of the triangle below: + +75 +95 64 +17 47 82 +18 35 87 10 +20 04 82 47 65 +19 01 23 75 03 34 +88 02 77 73 07 63 67 +99 65 04 28 06 16 70 92 +41 41 26 56 83 40 80 70 33 +41 48 72 33 47 32 37 16 94 29 +53 71 44 65 25 43 91 52 97 51 14 +70 11 33 28 77 73 17 78 39 68 17 57 +91 71 52 38 17 14 91 43 58 50 27 29 48 +63 66 04 68 89 53 67 30 73 16 69 87 40 31 +04 62 98 27 23 09 70 98 73 93 38 53 60 04 23 +""" +import os + + +def solution(): + """ + Finds the maximum total in a triangle as described by the problem statement + above. + + >>> solution() + 1074 + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + triangle = os.path.join(script_dir, "triangle.txt") + + with open(triangle, "r") as f: + triangle = f.readlines() + + a = [[int(y) for y in x.rstrip("\r\n").split(" ")] for x in triangle] + + for i in range(1, len(a)): + for j in range(len(a[i])): + if j != len(a[i - 1]): + number1 = a[i - 1][j] + else: + number1 = 0 + if j > 0: + number2 = a[i - 1][j - 1] + else: + number2 = 0 + a[i][j] += max(number1, number2) + return max(a[-1]) + + +if __name__ == "__main__": + print(solution()) diff --git a/project_euler/problem_18/triangle.txt b/project_euler/problem_18/triangle.txt new file mode 100644 index 000000000000..e236c2ff7ee2 --- /dev/null +++ b/project_euler/problem_18/triangle.txt @@ -0,0 +1,15 @@ +75 +95 64 +17 47 82 +18 35 87 10 +20 04 82 47 65 +19 01 23 75 03 34 +88 02 77 73 07 63 67 +99 65 04 28 06 16 70 92 +41 41 26 56 83 40 80 70 33 +41 48 72 33 47 32 37 16 94 29 +53 71 44 65 25 43 91 52 97 51 14 +70 11 33 28 77 73 17 78 39 68 17 57 +91 71 52 38 17 14 91 43 58 50 27 29 48 +63 66 04 68 89 53 67 30 73 16 69 87 40 31 +04 62 98 27 23 09 70 98 73 93 38 53 60 04 23 diff --git a/project_euler/problem_20/sol3.py b/project_euler/problem_20/sol3.py new file mode 100644 index 000000000000..13f9d7831c47 --- /dev/null +++ b/project_euler/problem_20/sol3.py @@ -0,0 +1,39 @@ +""" +n! means n × (n − 1) × ... × 3 × 2 × 1 + +For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. + +Find the sum of the digits in the number 100! +""" +from math import factorial + + +def solution(n): + """Returns the sum of the digits in the number 100! + >>> solution(1000) + 10539 + >>> solution(200) + 1404 + >>> solution(100) + 648 + >>> solution(50) + 216 + >>> solution(10) + 27 + >>> solution(5) + 3 + >>> solution(3) + 6 + >>> solution(2) + 2 + >>> solution(1) + 1 + >>> solution(0) + 1 + """ + return sum(map(int, str(factorial(n)))) + + +if __name__ == "__main__": + print(solution(int(input("Enter the Number: ").strip()))) diff --git a/project_euler/problem_20/sol4.py b/project_euler/problem_20/sol4.py new file mode 100644 index 000000000000..4c597220f09b --- /dev/null +++ b/project_euler/problem_20/sol4.py @@ -0,0 +1,40 @@ +""" +n! means n × (n − 1) × ... × 3 × 2 × 1 + +For example, 10! = 10 × 9 × ... × 3 × 2 × 1 = 3628800, +and the sum of the digits in the number 10! is 3 + 6 + 2 + 8 + 8 + 0 + 0 = 27. + +Find the sum of the digits in the number 100! +""" + + +def solution(n): + """Returns the sum of the digits in the number 100! + >>> solution(100) + 648 + >>> solution(50) + 216 + >>> solution(10) + 27 + >>> solution(5) + 3 + >>> solution(3) + 6 + >>> solution(2) + 2 + >>> solution(1) + 1 + """ + fact = 1 + result = 0 + for i in range(1, n + 1): + fact *= i + + for j in str(fact): + result += int(j) + + return result + + +if __name__ == "__main__": + print(solution(int(input("Enter the Number: ").strip()))) diff --git a/project_euler/problem_21/sol1.py b/project_euler/problem_21/sol1.py index 9cf2a64cf2a9..f01c9d0dad73 100644 --- a/project_euler/problem_21/sol1.py +++ b/project_euler/problem_21/sol1.py @@ -1,4 +1,3 @@ -# -.- coding: latin-1 -.- from math import sqrt """ @@ -16,15 +15,11 @@ Evaluate the sum of all the amicable numbers under 10000. """ -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 def sum_of_divisors(n): total = 0 - for i in xrange(1, int(sqrt(n) + 1)): + for i in range(1, int(sqrt(n) + 1)): if n % i == 0 and i != sqrt(n): total += i + n // i elif i == sqrt(n): @@ -50,8 +45,7 @@ def solution(n): [ i for i in range(1, n) - if sum_of_divisors(sum_of_divisors(i)) == i - and sum_of_divisors(i) != i + if sum_of_divisors(sum_of_divisors(i)) == i and sum_of_divisors(i) != i ] ) return total diff --git a/project_euler/problem_22/sol1.py b/project_euler/problem_22/sol1.py index aa779f222eaa..982906245e87 100644 --- a/project_euler/problem_22/sol1.py +++ b/project_euler/problem_22/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: latin-1 -*- """ Name scores Problem 22 @@ -18,12 +17,6 @@ import os -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - - def solution(): """Returns the total of all the name scores in the file. diff --git a/project_euler/problem_22/sol2.py b/project_euler/problem_22/sol2.py index 69acd2fb8ef3..5ae41c84686e 100644 --- a/project_euler/problem_22/sol2.py +++ b/project_euler/problem_22/sol2.py @@ -1,4 +1,3 @@ -# -*- coding: latin-1 -*- """ Name scores Problem 22 diff --git a/project_euler/problem_23/sol1.py b/project_euler/problem_23/sol1.py new file mode 100644 index 000000000000..a72b6123e3ee --- /dev/null +++ b/project_euler/problem_23/sol1.py @@ -0,0 +1,52 @@ +""" +A perfect number is a number for which the sum of its proper divisors is exactly +equal to the number. For example, the sum of the proper divisors of 28 would be +1 + 2 + 4 + 7 + 14 = 28, which means that 28 is a perfect number. + +A number n is called deficient if the sum of its proper divisors is less than n +and it is called abundant if this sum exceeds n. + +As 12 is the smallest abundant number, 1 + 2 + 3 + 4 + 6 = 16, the smallest +number that can be written as the sum of two abundant numbers is 24. By +mathematical analysis, it can be shown that all integers greater than 28123 +can be written as the sum of two abundant numbers. However, this upper limit +cannot be reduced any further by analysis even though it is known that the +greatest number that cannot be expressed as the sum of two abundant numbers +is less than this limit. + +Find the sum of all the positive integers which cannot be written as the sum +of two abundant numbers. +""" + + +def solution(limit=28123): + """ + Finds the sum of all the positive integers which cannot be written as + the sum of two abundant numbers + as described by the statement above. + + >>> solution() + 4179871 + """ + sumDivs = [1] * (limit + 1) + + for i in range(2, int(limit ** 0.5) + 1): + sumDivs[i * i] += i + for k in range(i + 1, limit // i + 1): + sumDivs[k * i] += k + i + + abundants = set() + res = 0 + + for n in range(1, limit + 1): + if sumDivs[n] > n: + abundants.add(n) + + if not any((n - a in abundants) for a in abundants): + res += n + + return res + + +if __name__ == "__main__": + print(solution()) diff --git a/project_euler/problem_234/sol1.py b/project_euler/problem_234/sol1.py index c0d2949285e9..28d82b550c85 100644 --- a/project_euler/problem_234/sol1.py +++ b/project_euler/problem_234/sol1.py @@ -17,18 +17,19 @@ What is the sum of all semidivisible numbers not exceeding 999966663333 ? """ + def fib(a, b, n): - - if n==1: + + if n == 1: return a - elif n==2: + elif n == 2: return b - elif n==3: - return str(a)+str(b) - + elif n == 3: + return str(a) + str(b) + temp = 0 - for x in range(2,n): - c=str(a) + str(b) + for x in range(2, n): + c = str(a) + str(b) temp = b b = c a = temp @@ -39,14 +40,14 @@ def solution(n): """Returns the sum of all semidivisible numbers not exceeding n.""" semidivisible = [] for x in range(n): - l=[i for i in input().split()] - c2=1 - while(1): - if len(fib(l[0],l[1],c2))>> solution(1000) + 4782 + >>> solution(100) + 476 + >>> solution(50) + 237 + >>> solution(3) + 12 + """ + f1, f2 = 1, 1 + index = 2 + while True: + i = 0 + f = f1 + f2 + f1, f2 = f2, f + index += 1 + for j in str(f): + i += 1 + if i == n: + break + return index + + +if __name__ == "__main__": + print(solution(int(str(input()).strip()))) diff --git a/project_euler/problem_26/__init__.py b/project_euler/problem_26/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_26/sol1.py b/project_euler/problem_26/sol1.py new file mode 100644 index 000000000000..cab8e0eb580b --- /dev/null +++ b/project_euler/problem_26/sol1.py @@ -0,0 +1,42 @@ +""" +Euler Problem 26 +https://projecteuler.net/problem=26 +Find the value of d < 1000 for which 1/d contains the longest recurring cycle +in its decimal fraction part. +""" + + +def find_digit(numerator: int, digit: int) -> int: + """ + Considering any range can be provided, + because as per the problem, the digit d < 1000 + >>> find_digit(1, 10) + 7 + >>> find_digit(10, 100) + 97 + >>> find_digit(10, 1000) + 983 + """ + the_digit = 1 + longest_list_length = 0 + + for divide_by_number in range(numerator, digit + 1): + has_been_divided = [] + now_divide = numerator + for division_cycle in range(1, digit + 1): + if now_divide in has_been_divided: + if longest_list_length < len(has_been_divided): + longest_list_length = len(has_been_divided) + the_digit = divide_by_number + else: + has_been_divided.append(now_divide) + now_divide = now_divide * 10 % divide_by_number + + return the_digit + + +# Tests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/project_euler/problem_27/problem_27_sol1.py b/project_euler/problem_27/problem_27_sol1.py new file mode 100644 index 000000000000..84b007a0bc88 --- /dev/null +++ b/project_euler/problem_27/problem_27_sol1.py @@ -0,0 +1,69 @@ +""" +Euler discovered the remarkable quadratic formula: +n2 + n + 41 +It turns out that the formula will produce 40 primes for the consecutive values +n = 0 to 39. However, when n = 40, 402 + 40 + 41 = 40(40 + 1) + 41 is divisible +by 41, and certainly when n = 41, 412 + 41 + 41 is clearly divisible by 41. +The incredible formula n2 − 79n + 1601 was discovered, which produces 80 primes +for the consecutive values n = 0 to 79. The product of the coefficients, −79 and +1601, is −126479. +Considering quadratics of the form: +n² + an + b, where |a| < 1000 and |b| < 1000 +where |n| is the modulus/absolute value of ne.g. |11| = 11 and |−4| = 4 +Find the product of the coefficients, a and b, for the quadratic expression that +produces the maximum number of primes for consecutive values of n, starting with +n = 0. +""" + +import math + + +def is_prime(k: int) -> bool: + """ + Determine if a number is prime + >>> is_prime(10) + False + >>> is_prime(11) + True + """ + if k < 2 or k % 2 == 0: + return False + elif k == 2: + return True + else: + for x in range(3, int(math.sqrt(k) + 1), 2): + if k % x == 0: + return False + return True + + +def solution(a_limit: int, b_limit: int) -> int: + """ + >>> solution(1000, 1000) + -59231 + >>> solution(200, 1000) + -59231 + >>> solution(200, 200) + -4925 + >>> solution(-1000, 1000) + 0 + >>> solution(-1000, -1000) + 0 + """ + longest = [0, 0, 0] # length, a, b + for a in range((a_limit * -1) + 1, a_limit): + for b in range(2, b_limit): + if is_prime(b): + count = 0 + n = 0 + while is_prime((n ** 2) + (a * n) + b): + count += 1 + n += 1 + if count > longest[0]: + longest = [count, a, b] + ans = longest[1] * longest[2] + return ans + + +if __name__ == "__main__": + print(solution(1000, 1000)) diff --git a/project_euler/problem_28/sol1.py b/project_euler/problem_28/sol1.py index 63386ce3058c..11b48fea9adf 100644 --- a/project_euler/problem_28/sol1.py +++ b/project_euler/problem_28/sol1.py @@ -16,11 +16,6 @@ from math import ceil -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - def diagonal_sum(n): """Returns the sum of the numbers on the diagonals in a n by n spiral @@ -39,7 +34,7 @@ def diagonal_sum(n): """ total = 1 - for i in xrange(1, int(ceil(n / 2.0))): + for i in range(1, int(ceil(n / 2.0))): odd = 2 * i + 1 even = 2 * i total = total + 4 * odd ** 2 - 6 * even diff --git a/project_euler/problem_29/solution.py b/project_euler/problem_29/solution.py index e67dafe4639d..4313a7b06392 100644 --- a/project_euler/problem_29/solution.py +++ b/project_euler/problem_29/solution.py @@ -14,13 +14,12 @@ How many distinct terms are in the sequence generated by ab for 2 <= a <= 100 and 2 <= b <= 100? """ -from __future__ import print_function def solution(n): """Returns the number of distinct terms in the sequence generated by a^b for 2 <= a <= 100 and 2 <= b <= 100. - + >>> solution(100) 9183 >>> solution(50) diff --git a/project_euler/problem_30/soln.py b/project_euler/problem_30/soln.py new file mode 100644 index 000000000000..9d45739845a3 --- /dev/null +++ b/project_euler/problem_30/soln.py @@ -0,0 +1,34 @@ +""" Problem Statement (Digit Fifth Power ): https://projecteuler.net/problem=30 + +Surprisingly there are only three numbers that can be written as the sum of fourth powers of their digits: + +1634 = 1^4 + 6^4 + 3^4 + 4^4 +8208 = 8^4 + 2^4 + 0^4 + 8^4 +9474 = 9^4 + 4^4 + 7^4 + 4^4 +As 1 = 1^4 is not a sum it is not included. + +The sum of these numbers is 1634 + 8208 + 9474 = 19316. + +Find the sum of all the numbers that can be written as the sum of fifth powers of their digits. + +(9^5)=59,049‬ +59049*7=4,13,343 (which is only 6 digit number ) +So, number greater than 9,99,999 are rejected +and also 59049*3=1,77,147 (which exceeds the criteria of number being 3 digit) +So, n>999 +and hence a bound between (1000,1000000) +""" + + +def digitsum(s: str) -> int: + """ + >>> all(digitsum(str(i)) == (1 if i == 1 else 0) for i in range(100)) + True + """ + i = sum(pow(int(c), 5) for c in s) + return i if i == int(s) else 0 + + +if __name__ == "__main__": + count = sum(digitsum(str(i)) for i in range(1000, 1000000)) + print(count) # --> 443839 diff --git a/project_euler/problem_31/sol1.py b/project_euler/problem_31/sol1.py index e2a209e5df5a..09b60cdae89d 100644 --- a/project_euler/problem_31/sol1.py +++ b/project_euler/problem_31/sol1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Coin sums Problem 31 @@ -11,12 +10,6 @@ 1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p How many different ways can £2 be made using any number of coins? """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def one_pence(): @@ -52,7 +45,7 @@ def two_pound(x): def solution(n): - """Returns the number of different ways can £n be made using any number of + """Returns the number of different ways can n pence be made using any number of coins? >>> solution(500) diff --git a/project_euler/problem_31/sol2.py b/project_euler/problem_31/sol2.py new file mode 100644 index 000000000000..1f006f1a1824 --- /dev/null +++ b/project_euler/problem_31/sol2.py @@ -0,0 +1,56 @@ +"""Coin sums + +In England the currency is made up of pound, £, and pence, p, and there are +eight coins in general circulation: + +1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p). +It is possible to make £2 in the following way: + +1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p +How many different ways can £2 be made using any number of coins? + +Hint: + > There are 100 pence in a pound (£1 = 100p) + > There are coins(in pence) are available: 1, 2, 5, 10, 20, 50, 100 and 200. + > how many different ways you can combine these values to create 200 pence. + +Example: + to make 6p there are 5 ways + 1,1,1,1,1,1 + 1,1,1,1,2 + 1,1,2,2 + 2,2,2 + 1,5 + to make 5p there are 4 ways + 1,1,1,1,1 + 1,1,1,2 + 1,2,2 + 5 +""" + + +def solution(pence: int) -> int: + """Returns the number of different ways to make X pence using any number of coins. + The solution is based on dynamic programming paradigm in a bottom-up fashion. + + >>> solution(500) + 6295434 + >>> solution(200) + 73682 + >>> solution(50) + 451 + >>> solution(10) + 11 + """ + coins = [1, 2, 5, 10, 20, 50, 100, 200] + number_of_ways = [0] * (pence + 1) + number_of_ways[0] = 1 # base case: 1 way to make 0 pence + + for coin in coins: + for i in range(coin, pence + 1, 1): + number_of_ways[i] += number_of_ways[i - coin] + return number_of_ways[pence] + + +if __name__ == "__main__": + assert solution(200) == 73682 diff --git a/project_euler/problem_32/sol32.py b/project_euler/problem_32/sol32.py new file mode 100644 index 000000000000..393218339e9f --- /dev/null +++ b/project_euler/problem_32/sol32.py @@ -0,0 +1,58 @@ +""" +We shall say that an n-digit number is pandigital if it makes use of all the +digits 1 to n exactly once; for example, the 5-digit number, 15234, is 1 through +5 pandigital. + +The product 7254 is unusual, as the identity, 39 × 186 = 7254, containing +multiplicand, multiplier, and product is 1 through 9 pandigital. + +Find the sum of all products whose multiplicand/multiplier/product identity can +be written as a 1 through 9 pandigital. + +HINT: Some products can be obtained in more than one way so be sure to only +include it once in your sum. +""" +import itertools + + +def isCombinationValid(combination): + """ + Checks if a combination (a tuple of 9 digits) + is a valid product equation. + + >>> isCombinationValid(('3', '9', '1', '8', '6', '7', '2', '5', '4')) + True + + >>> isCombinationValid(('1', '2', '3', '4', '5', '6', '7', '8', '9')) + False + + """ + return ( + int("".join(combination[0:2])) * int("".join(combination[2:5])) + == int("".join(combination[5:9])) + ) or ( + int("".join(combination[0])) * int("".join(combination[1:5])) + == int("".join(combination[5:9])) + ) + + +def solution(): + """ + Finds the sum of all products whose multiplicand/multiplier/product identity + can be written as a 1 through 9 pandigital + + >>> solution() + 45228 + """ + + return sum( + { + int("".join(pandigital[5:9])) + for pandigital in itertools.permutations("123456789") + if isCombinationValid(pandigital) + } + ) + + +if __name__ == "__main__": + print(solution()) diff --git a/project_euler/problem_33/__init__.py b/project_euler/problem_33/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_33/sol1.py b/project_euler/problem_33/sol1.py new file mode 100644 index 000000000000..73a49023ae41 --- /dev/null +++ b/project_euler/problem_33/sol1.py @@ -0,0 +1,55 @@ +""" +Problem: + +The fraction 49/98 is a curious fraction, as an inexperienced +mathematician in attempting to simplify it may incorrectly believe +that 49/98 = 4/8, which is correct, is obtained by cancelling the 9s. + +We shall consider fractions like, 30/50 = 3/5, to be trivial examples. + +There are exactly four non-trivial examples of this type of fraction, +less than one in value, and containing two digits in the numerator +and denominator. + +If the product of these four fractions is given in its lowest common +terms, find the value of the denominator. +""" + + +def isDigitCancelling(num, den): + if num != den: + if num % 10 == den // 10: + if (num // 10) / (den % 10) == num / den: + return True + + +def solve(digit_len: int) -> str: + """ + >>> solve(2) + '16/64 , 19/95 , 26/65 , 49/98' + >>> solve(3) + '16/64 , 19/95 , 26/65 , 49/98' + >>> solve(4) + '16/64 , 19/95 , 26/65 , 49/98' + >>> solve(0) + '' + >>> solve(5) + '16/64 , 19/95 , 26/65 , 49/98' + """ + solutions = [] + den = 11 + last_digit = int("1" + "0" * digit_len) + for num in range(den, last_digit): + while den <= 99: + if (num != den) and (num % 10 == den // 10) and (den % 10 != 0): + if isDigitCancelling(num, den): + solutions.append(f"{num}/{den}") + den += 1 + num += 1 + den = 10 + solutions = " , ".join(solutions) + return solutions + + +if __name__ == "__main__": + print(solve(2)) diff --git a/project_euler/problem_36/sol1.py b/project_euler/problem_36/sol1.py index 38b60420992b..39088cf25dd4 100644 --- a/project_euler/problem_36/sol1.py +++ b/project_euler/problem_36/sol1.py @@ -9,10 +9,6 @@ (Please note that the palindromic number, in either base, may not include leading zeros.) """ -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 def is_palindrome(n): @@ -47,7 +43,7 @@ def solution(n): """ total = 0 - for i in xrange(1, n): + for i in range(1, n): if is_palindrome(i) and is_palindrome(bin(i).split("b")[1]): total += i return total diff --git a/project_euler/problem_40/sol1.py b/project_euler/problem_40/sol1.py index accd7125354c..69be377723a5 100644 --- a/project_euler/problem_40/sol1.py +++ b/project_euler/problem_40/sol1.py @@ -1,4 +1,3 @@ -# -.- coding: latin-1 -.- """ Champernowne's constant Problem 40 @@ -14,11 +13,10 @@ d1 × d10 × d100 × d1000 × d10000 × d100000 × d1000000 """ -from __future__ import print_function def solution(): - """Returns + """Returns >>> solution() 210 diff --git a/project_euler/problem_42/solution42.py b/project_euler/problem_42/solution42.py new file mode 100644 index 000000000000..2380472153c6 --- /dev/null +++ b/project_euler/problem_42/solution42.py @@ -0,0 +1,48 @@ +""" +The nth term of the sequence of triangle numbers is given by, tn = ½n(n+1); so +the first ten triangle numbers are: + +1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ... + +By converting each letter in a word to a number corresponding to its +alphabetical position and adding these values we form a word value. For example, +the word value for SKY is 19 + 11 + 25 = 55 = t10. If the word value is a +triangle number then we shall call the word a triangle word. + +Using words.txt (right click and 'Save Link/Target As...'), a 16K text file +containing nearly two-thousand common English words, how many are triangle +words? +""" +import os + + +# Precomputes a list of the 100 first triangular numbers +TRIANGULAR_NUMBERS = [int(0.5 * n * (n + 1)) for n in range(1, 101)] + + +def solution(): + """ + Finds the amount of triangular words in the words file. + + >>> solution() + 162 + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + wordsFilePath = os.path.join(script_dir, "words.txt") + + words = "" + with open(wordsFilePath, "r") as f: + words = f.readline() + + words = list(map(lambda word: word.strip('"'), words.strip("\r\n").split(","))) + words = list( + filter( + lambda word: word in TRIANGULAR_NUMBERS, + map(lambda word: sum(map(lambda x: ord(x) - 64, word)), words), + ) + ) + return len(words) + + +if __name__ == "__main__": + print(solution()) diff --git a/project_euler/problem_42/words.txt b/project_euler/problem_42/words.txt new file mode 100644 index 000000000000..af3aeb42f151 --- /dev/null +++ b/project_euler/problem_42/words.txt @@ -0,0 +1 @@ +"A","ABILITY","ABLE","ABOUT","ABOVE","ABSENCE","ABSOLUTELY","ACADEMIC","ACCEPT","ACCESS","ACCIDENT","ACCOMPANY","ACCORDING","ACCOUNT","ACHIEVE","ACHIEVEMENT","ACID","ACQUIRE","ACROSS","ACT","ACTION","ACTIVE","ACTIVITY","ACTUAL","ACTUALLY","ADD","ADDITION","ADDITIONAL","ADDRESS","ADMINISTRATION","ADMIT","ADOPT","ADULT","ADVANCE","ADVANTAGE","ADVICE","ADVISE","AFFAIR","AFFECT","AFFORD","AFRAID","AFTER","AFTERNOON","AFTERWARDS","AGAIN","AGAINST","AGE","AGENCY","AGENT","AGO","AGREE","AGREEMENT","AHEAD","AID","AIM","AIR","AIRCRAFT","ALL","ALLOW","ALMOST","ALONE","ALONG","ALREADY","ALRIGHT","ALSO","ALTERNATIVE","ALTHOUGH","ALWAYS","AMONG","AMONGST","AMOUNT","AN","ANALYSIS","ANCIENT","AND","ANIMAL","ANNOUNCE","ANNUAL","ANOTHER","ANSWER","ANY","ANYBODY","ANYONE","ANYTHING","ANYWAY","APART","APPARENT","APPARENTLY","APPEAL","APPEAR","APPEARANCE","APPLICATION","APPLY","APPOINT","APPOINTMENT","APPROACH","APPROPRIATE","APPROVE","AREA","ARGUE","ARGUMENT","ARISE","ARM","ARMY","AROUND","ARRANGE","ARRANGEMENT","ARRIVE","ART","ARTICLE","ARTIST","AS","ASK","ASPECT","ASSEMBLY","ASSESS","ASSESSMENT","ASSET","ASSOCIATE","ASSOCIATION","ASSUME","ASSUMPTION","AT","ATMOSPHERE","ATTACH","ATTACK","ATTEMPT","ATTEND","ATTENTION","ATTITUDE","ATTRACT","ATTRACTIVE","AUDIENCE","AUTHOR","AUTHORITY","AVAILABLE","AVERAGE","AVOID","AWARD","AWARE","AWAY","AYE","BABY","BACK","BACKGROUND","BAD","BAG","BALANCE","BALL","BAND","BANK","BAR","BASE","BASIC","BASIS","BATTLE","BE","BEAR","BEAT","BEAUTIFUL","BECAUSE","BECOME","BED","BEDROOM","BEFORE","BEGIN","BEGINNING","BEHAVIOUR","BEHIND","BELIEF","BELIEVE","BELONG","BELOW","BENEATH","BENEFIT","BESIDE","BEST","BETTER","BETWEEN","BEYOND","BIG","BILL","BIND","BIRD","BIRTH","BIT","BLACK","BLOCK","BLOOD","BLOODY","BLOW","BLUE","BOARD","BOAT","BODY","BONE","BOOK","BORDER","BOTH","BOTTLE","BOTTOM","BOX","BOY","BRAIN","BRANCH","BREAK","BREATH","BRIDGE","BRIEF","BRIGHT","BRING","BROAD","BROTHER","BUDGET","BUILD","BUILDING","BURN","BUS","BUSINESS","BUSY","BUT","BUY","BY","CABINET","CALL","CAMPAIGN","CAN","CANDIDATE","CAPABLE","CAPACITY","CAPITAL","CAR","CARD","CARE","CAREER","CAREFUL","CAREFULLY","CARRY","CASE","CASH","CAT","CATCH","CATEGORY","CAUSE","CELL","CENTRAL","CENTRE","CENTURY","CERTAIN","CERTAINLY","CHAIN","CHAIR","CHAIRMAN","CHALLENGE","CHANCE","CHANGE","CHANNEL","CHAPTER","CHARACTER","CHARACTERISTIC","CHARGE","CHEAP","CHECK","CHEMICAL","CHIEF","CHILD","CHOICE","CHOOSE","CHURCH","CIRCLE","CIRCUMSTANCE","CITIZEN","CITY","CIVIL","CLAIM","CLASS","CLEAN","CLEAR","CLEARLY","CLIENT","CLIMB","CLOSE","CLOSELY","CLOTHES","CLUB","COAL","CODE","COFFEE","COLD","COLLEAGUE","COLLECT","COLLECTION","COLLEGE","COLOUR","COMBINATION","COMBINE","COME","COMMENT","COMMERCIAL","COMMISSION","COMMIT","COMMITMENT","COMMITTEE","COMMON","COMMUNICATION","COMMUNITY","COMPANY","COMPARE","COMPARISON","COMPETITION","COMPLETE","COMPLETELY","COMPLEX","COMPONENT","COMPUTER","CONCENTRATE","CONCENTRATION","CONCEPT","CONCERN","CONCERNED","CONCLUDE","CONCLUSION","CONDITION","CONDUCT","CONFERENCE","CONFIDENCE","CONFIRM","CONFLICT","CONGRESS","CONNECT","CONNECTION","CONSEQUENCE","CONSERVATIVE","CONSIDER","CONSIDERABLE","CONSIDERATION","CONSIST","CONSTANT","CONSTRUCTION","CONSUMER","CONTACT","CONTAIN","CONTENT","CONTEXT","CONTINUE","CONTRACT","CONTRAST","CONTRIBUTE","CONTRIBUTION","CONTROL","CONVENTION","CONVERSATION","COPY","CORNER","CORPORATE","CORRECT","COS","COST","COULD","COUNCIL","COUNT","COUNTRY","COUNTY","COUPLE","COURSE","COURT","COVER","CREATE","CREATION","CREDIT","CRIME","CRIMINAL","CRISIS","CRITERION","CRITICAL","CRITICISM","CROSS","CROWD","CRY","CULTURAL","CULTURE","CUP","CURRENT","CURRENTLY","CURRICULUM","CUSTOMER","CUT","DAMAGE","DANGER","DANGEROUS","DARK","DATA","DATE","DAUGHTER","DAY","DEAD","DEAL","DEATH","DEBATE","DEBT","DECADE","DECIDE","DECISION","DECLARE","DEEP","DEFENCE","DEFENDANT","DEFINE","DEFINITION","DEGREE","DELIVER","DEMAND","DEMOCRATIC","DEMONSTRATE","DENY","DEPARTMENT","DEPEND","DEPUTY","DERIVE","DESCRIBE","DESCRIPTION","DESIGN","DESIRE","DESK","DESPITE","DESTROY","DETAIL","DETAILED","DETERMINE","DEVELOP","DEVELOPMENT","DEVICE","DIE","DIFFERENCE","DIFFERENT","DIFFICULT","DIFFICULTY","DINNER","DIRECT","DIRECTION","DIRECTLY","DIRECTOR","DISAPPEAR","DISCIPLINE","DISCOVER","DISCUSS","DISCUSSION","DISEASE","DISPLAY","DISTANCE","DISTINCTION","DISTRIBUTION","DISTRICT","DIVIDE","DIVISION","DO","DOCTOR","DOCUMENT","DOG","DOMESTIC","DOOR","DOUBLE","DOUBT","DOWN","DRAW","DRAWING","DREAM","DRESS","DRINK","DRIVE","DRIVER","DROP","DRUG","DRY","DUE","DURING","DUTY","EACH","EAR","EARLY","EARN","EARTH","EASILY","EAST","EASY","EAT","ECONOMIC","ECONOMY","EDGE","EDITOR","EDUCATION","EDUCATIONAL","EFFECT","EFFECTIVE","EFFECTIVELY","EFFORT","EGG","EITHER","ELDERLY","ELECTION","ELEMENT","ELSE","ELSEWHERE","EMERGE","EMPHASIS","EMPLOY","EMPLOYEE","EMPLOYER","EMPLOYMENT","EMPTY","ENABLE","ENCOURAGE","END","ENEMY","ENERGY","ENGINE","ENGINEERING","ENJOY","ENOUGH","ENSURE","ENTER","ENTERPRISE","ENTIRE","ENTIRELY","ENTITLE","ENTRY","ENVIRONMENT","ENVIRONMENTAL","EQUAL","EQUALLY","EQUIPMENT","ERROR","ESCAPE","ESPECIALLY","ESSENTIAL","ESTABLISH","ESTABLISHMENT","ESTATE","ESTIMATE","EVEN","EVENING","EVENT","EVENTUALLY","EVER","EVERY","EVERYBODY","EVERYONE","EVERYTHING","EVIDENCE","EXACTLY","EXAMINATION","EXAMINE","EXAMPLE","EXCELLENT","EXCEPT","EXCHANGE","EXECUTIVE","EXERCISE","EXHIBITION","EXIST","EXISTENCE","EXISTING","EXPECT","EXPECTATION","EXPENDITURE","EXPENSE","EXPENSIVE","EXPERIENCE","EXPERIMENT","EXPERT","EXPLAIN","EXPLANATION","EXPLORE","EXPRESS","EXPRESSION","EXTEND","EXTENT","EXTERNAL","EXTRA","EXTREMELY","EYE","FACE","FACILITY","FACT","FACTOR","FACTORY","FAIL","FAILURE","FAIR","FAIRLY","FAITH","FALL","FAMILIAR","FAMILY","FAMOUS","FAR","FARM","FARMER","FASHION","FAST","FATHER","FAVOUR","FEAR","FEATURE","FEE","FEEL","FEELING","FEMALE","FEW","FIELD","FIGHT","FIGURE","FILE","FILL","FILM","FINAL","FINALLY","FINANCE","FINANCIAL","FIND","FINDING","FINE","FINGER","FINISH","FIRE","FIRM","FIRST","FISH","FIT","FIX","FLAT","FLIGHT","FLOOR","FLOW","FLOWER","FLY","FOCUS","FOLLOW","FOLLOWING","FOOD","FOOT","FOOTBALL","FOR","FORCE","FOREIGN","FOREST","FORGET","FORM","FORMAL","FORMER","FORWARD","FOUNDATION","FREE","FREEDOM","FREQUENTLY","FRESH","FRIEND","FROM","FRONT","FRUIT","FUEL","FULL","FULLY","FUNCTION","FUND","FUNNY","FURTHER","FUTURE","GAIN","GAME","GARDEN","GAS","GATE","GATHER","GENERAL","GENERALLY","GENERATE","GENERATION","GENTLEMAN","GET","GIRL","GIVE","GLASS","GO","GOAL","GOD","GOLD","GOOD","GOVERNMENT","GRANT","GREAT","GREEN","GREY","GROUND","GROUP","GROW","GROWING","GROWTH","GUEST","GUIDE","GUN","HAIR","HALF","HALL","HAND","HANDLE","HANG","HAPPEN","HAPPY","HARD","HARDLY","HATE","HAVE","HE","HEAD","HEALTH","HEAR","HEART","HEAT","HEAVY","HELL","HELP","HENCE","HER","HERE","HERSELF","HIDE","HIGH","HIGHLY","HILL","HIM","HIMSELF","HIS","HISTORICAL","HISTORY","HIT","HOLD","HOLE","HOLIDAY","HOME","HOPE","HORSE","HOSPITAL","HOT","HOTEL","HOUR","HOUSE","HOUSEHOLD","HOUSING","HOW","HOWEVER","HUGE","HUMAN","HURT","HUSBAND","I","IDEA","IDENTIFY","IF","IGNORE","ILLUSTRATE","IMAGE","IMAGINE","IMMEDIATE","IMMEDIATELY","IMPACT","IMPLICATION","IMPLY","IMPORTANCE","IMPORTANT","IMPOSE","IMPOSSIBLE","IMPRESSION","IMPROVE","IMPROVEMENT","IN","INCIDENT","INCLUDE","INCLUDING","INCOME","INCREASE","INCREASED","INCREASINGLY","INDEED","INDEPENDENT","INDEX","INDICATE","INDIVIDUAL","INDUSTRIAL","INDUSTRY","INFLUENCE","INFORM","INFORMATION","INITIAL","INITIATIVE","INJURY","INSIDE","INSIST","INSTANCE","INSTEAD","INSTITUTE","INSTITUTION","INSTRUCTION","INSTRUMENT","INSURANCE","INTEND","INTENTION","INTEREST","INTERESTED","INTERESTING","INTERNAL","INTERNATIONAL","INTERPRETATION","INTERVIEW","INTO","INTRODUCE","INTRODUCTION","INVESTIGATE","INVESTIGATION","INVESTMENT","INVITE","INVOLVE","IRON","IS","ISLAND","ISSUE","IT","ITEM","ITS","ITSELF","JOB","JOIN","JOINT","JOURNEY","JUDGE","JUMP","JUST","JUSTICE","KEEP","KEY","KID","KILL","KIND","KING","KITCHEN","KNEE","KNOW","KNOWLEDGE","LABOUR","LACK","LADY","LAND","LANGUAGE","LARGE","LARGELY","LAST","LATE","LATER","LATTER","LAUGH","LAUNCH","LAW","LAWYER","LAY","LEAD","LEADER","LEADERSHIP","LEADING","LEAF","LEAGUE","LEAN","LEARN","LEAST","LEAVE","LEFT","LEG","LEGAL","LEGISLATION","LENGTH","LESS","LET","LETTER","LEVEL","LIABILITY","LIBERAL","LIBRARY","LIE","LIFE","LIFT","LIGHT","LIKE","LIKELY","LIMIT","LIMITED","LINE","LINK","LIP","LIST","LISTEN","LITERATURE","LITTLE","LIVE","LIVING","LOAN","LOCAL","LOCATION","LONG","LOOK","LORD","LOSE","LOSS","LOT","LOVE","LOVELY","LOW","LUNCH","MACHINE","MAGAZINE","MAIN","MAINLY","MAINTAIN","MAJOR","MAJORITY","MAKE","MALE","MAN","MANAGE","MANAGEMENT","MANAGER","MANNER","MANY","MAP","MARK","MARKET","MARRIAGE","MARRIED","MARRY","MASS","MASTER","MATCH","MATERIAL","MATTER","MAY","MAYBE","ME","MEAL","MEAN","MEANING","MEANS","MEANWHILE","MEASURE","MECHANISM","MEDIA","MEDICAL","MEET","MEETING","MEMBER","MEMBERSHIP","MEMORY","MENTAL","MENTION","MERELY","MESSAGE","METAL","METHOD","MIDDLE","MIGHT","MILE","MILITARY","MILK","MIND","MINE","MINISTER","MINISTRY","MINUTE","MISS","MISTAKE","MODEL","MODERN","MODULE","MOMENT","MONEY","MONTH","MORE","MORNING","MOST","MOTHER","MOTION","MOTOR","MOUNTAIN","MOUTH","MOVE","MOVEMENT","MUCH","MURDER","MUSEUM","MUSIC","MUST","MY","MYSELF","NAME","NARROW","NATION","NATIONAL","NATURAL","NATURE","NEAR","NEARLY","NECESSARILY","NECESSARY","NECK","NEED","NEGOTIATION","NEIGHBOUR","NEITHER","NETWORK","NEVER","NEVERTHELESS","NEW","NEWS","NEWSPAPER","NEXT","NICE","NIGHT","NO","NOBODY","NOD","NOISE","NONE","NOR","NORMAL","NORMALLY","NORTH","NORTHERN","NOSE","NOT","NOTE","NOTHING","NOTICE","NOTION","NOW","NUCLEAR","NUMBER","NURSE","OBJECT","OBJECTIVE","OBSERVATION","OBSERVE","OBTAIN","OBVIOUS","OBVIOUSLY","OCCASION","OCCUR","ODD","OF","OFF","OFFENCE","OFFER","OFFICE","OFFICER","OFFICIAL","OFTEN","OIL","OKAY","OLD","ON","ONCE","ONE","ONLY","ONTO","OPEN","OPERATE","OPERATION","OPINION","OPPORTUNITY","OPPOSITION","OPTION","OR","ORDER","ORDINARY","ORGANISATION","ORGANISE","ORGANIZATION","ORIGIN","ORIGINAL","OTHER","OTHERWISE","OUGHT","OUR","OURSELVES","OUT","OUTCOME","OUTPUT","OUTSIDE","OVER","OVERALL","OWN","OWNER","PACKAGE","PAGE","PAIN","PAINT","PAINTING","PAIR","PANEL","PAPER","PARENT","PARK","PARLIAMENT","PART","PARTICULAR","PARTICULARLY","PARTLY","PARTNER","PARTY","PASS","PASSAGE","PAST","PATH","PATIENT","PATTERN","PAY","PAYMENT","PEACE","PENSION","PEOPLE","PER","PERCENT","PERFECT","PERFORM","PERFORMANCE","PERHAPS","PERIOD","PERMANENT","PERSON","PERSONAL","PERSUADE","PHASE","PHONE","PHOTOGRAPH","PHYSICAL","PICK","PICTURE","PIECE","PLACE","PLAN","PLANNING","PLANT","PLASTIC","PLATE","PLAY","PLAYER","PLEASE","PLEASURE","PLENTY","PLUS","POCKET","POINT","POLICE","POLICY","POLITICAL","POLITICS","POOL","POOR","POPULAR","POPULATION","POSITION","POSITIVE","POSSIBILITY","POSSIBLE","POSSIBLY","POST","POTENTIAL","POUND","POWER","POWERFUL","PRACTICAL","PRACTICE","PREFER","PREPARE","PRESENCE","PRESENT","PRESIDENT","PRESS","PRESSURE","PRETTY","PREVENT","PREVIOUS","PREVIOUSLY","PRICE","PRIMARY","PRIME","PRINCIPLE","PRIORITY","PRISON","PRISONER","PRIVATE","PROBABLY","PROBLEM","PROCEDURE","PROCESS","PRODUCE","PRODUCT","PRODUCTION","PROFESSIONAL","PROFIT","PROGRAM","PROGRAMME","PROGRESS","PROJECT","PROMISE","PROMOTE","PROPER","PROPERLY","PROPERTY","PROPORTION","PROPOSE","PROPOSAL","PROSPECT","PROTECT","PROTECTION","PROVE","PROVIDE","PROVIDED","PROVISION","PUB","PUBLIC","PUBLICATION","PUBLISH","PULL","PUPIL","PURPOSE","PUSH","PUT","QUALITY","QUARTER","QUESTION","QUICK","QUICKLY","QUIET","QUITE","RACE","RADIO","RAILWAY","RAIN","RAISE","RANGE","RAPIDLY","RARE","RATE","RATHER","REACH","REACTION","READ","READER","READING","READY","REAL","REALISE","REALITY","REALIZE","REALLY","REASON","REASONABLE","RECALL","RECEIVE","RECENT","RECENTLY","RECOGNISE","RECOGNITION","RECOGNIZE","RECOMMEND","RECORD","RECOVER","RED","REDUCE","REDUCTION","REFER","REFERENCE","REFLECT","REFORM","REFUSE","REGARD","REGION","REGIONAL","REGULAR","REGULATION","REJECT","RELATE","RELATION","RELATIONSHIP","RELATIVE","RELATIVELY","RELEASE","RELEVANT","RELIEF","RELIGION","RELIGIOUS","RELY","REMAIN","REMEMBER","REMIND","REMOVE","REPEAT","REPLACE","REPLY","REPORT","REPRESENT","REPRESENTATION","REPRESENTATIVE","REQUEST","REQUIRE","REQUIREMENT","RESEARCH","RESOURCE","RESPECT","RESPOND","RESPONSE","RESPONSIBILITY","RESPONSIBLE","REST","RESTAURANT","RESULT","RETAIN","RETURN","REVEAL","REVENUE","REVIEW","REVOLUTION","RICH","RIDE","RIGHT","RING","RISE","RISK","RIVER","ROAD","ROCK","ROLE","ROLL","ROOF","ROOM","ROUND","ROUTE","ROW","ROYAL","RULE","RUN","RURAL","SAFE","SAFETY","SALE","SAME","SAMPLE","SATISFY","SAVE","SAY","SCALE","SCENE","SCHEME","SCHOOL","SCIENCE","SCIENTIFIC","SCIENTIST","SCORE","SCREEN","SEA","SEARCH","SEASON","SEAT","SECOND","SECONDARY","SECRETARY","SECTION","SECTOR","SECURE","SECURITY","SEE","SEEK","SEEM","SELECT","SELECTION","SELL","SEND","SENIOR","SENSE","SENTENCE","SEPARATE","SEQUENCE","SERIES","SERIOUS","SERIOUSLY","SERVANT","SERVE","SERVICE","SESSION","SET","SETTLE","SETTLEMENT","SEVERAL","SEVERE","SEX","SEXUAL","SHAKE","SHALL","SHAPE","SHARE","SHE","SHEET","SHIP","SHOE","SHOOT","SHOP","SHORT","SHOT","SHOULD","SHOULDER","SHOUT","SHOW","SHUT","SIDE","SIGHT","SIGN","SIGNAL","SIGNIFICANCE","SIGNIFICANT","SILENCE","SIMILAR","SIMPLE","SIMPLY","SINCE","SING","SINGLE","SIR","SISTER","SIT","SITE","SITUATION","SIZE","SKILL","SKIN","SKY","SLEEP","SLIGHTLY","SLIP","SLOW","SLOWLY","SMALL","SMILE","SO","SOCIAL","SOCIETY","SOFT","SOFTWARE","SOIL","SOLDIER","SOLICITOR","SOLUTION","SOME","SOMEBODY","SOMEONE","SOMETHING","SOMETIMES","SOMEWHAT","SOMEWHERE","SON","SONG","SOON","SORRY","SORT","SOUND","SOURCE","SOUTH","SOUTHERN","SPACE","SPEAK","SPEAKER","SPECIAL","SPECIES","SPECIFIC","SPEECH","SPEED","SPEND","SPIRIT","SPORT","SPOT","SPREAD","SPRING","STAFF","STAGE","STAND","STANDARD","STAR","START","STATE","STATEMENT","STATION","STATUS","STAY","STEAL","STEP","STICK","STILL","STOCK","STONE","STOP","STORE","STORY","STRAIGHT","STRANGE","STRATEGY","STREET","STRENGTH","STRIKE","STRONG","STRONGLY","STRUCTURE","STUDENT","STUDIO","STUDY","STUFF","STYLE","SUBJECT","SUBSTANTIAL","SUCCEED","SUCCESS","SUCCESSFUL","SUCH","SUDDENLY","SUFFER","SUFFICIENT","SUGGEST","SUGGESTION","SUITABLE","SUM","SUMMER","SUN","SUPPLY","SUPPORT","SUPPOSE","SURE","SURELY","SURFACE","SURPRISE","SURROUND","SURVEY","SURVIVE","SWITCH","SYSTEM","TABLE","TAKE","TALK","TALL","TAPE","TARGET","TASK","TAX","TEA","TEACH","TEACHER","TEACHING","TEAM","TEAR","TECHNICAL","TECHNIQUE","TECHNOLOGY","TELEPHONE","TELEVISION","TELL","TEMPERATURE","TEND","TERM","TERMS","TERRIBLE","TEST","TEXT","THAN","THANK","THANKS","THAT","THE","THEATRE","THEIR","THEM","THEME","THEMSELVES","THEN","THEORY","THERE","THEREFORE","THESE","THEY","THIN","THING","THINK","THIS","THOSE","THOUGH","THOUGHT","THREAT","THREATEN","THROUGH","THROUGHOUT","THROW","THUS","TICKET","TIME","TINY","TITLE","TO","TODAY","TOGETHER","TOMORROW","TONE","TONIGHT","TOO","TOOL","TOOTH","TOP","TOTAL","TOTALLY","TOUCH","TOUR","TOWARDS","TOWN","TRACK","TRADE","TRADITION","TRADITIONAL","TRAFFIC","TRAIN","TRAINING","TRANSFER","TRANSPORT","TRAVEL","TREAT","TREATMENT","TREATY","TREE","TREND","TRIAL","TRIP","TROOP","TROUBLE","TRUE","TRUST","TRUTH","TRY","TURN","TWICE","TYPE","TYPICAL","UNABLE","UNDER","UNDERSTAND","UNDERSTANDING","UNDERTAKE","UNEMPLOYMENT","UNFORTUNATELY","UNION","UNIT","UNITED","UNIVERSITY","UNLESS","UNLIKELY","UNTIL","UP","UPON","UPPER","URBAN","US","USE","USED","USEFUL","USER","USUAL","USUALLY","VALUE","VARIATION","VARIETY","VARIOUS","VARY","VAST","VEHICLE","VERSION","VERY","VIA","VICTIM","VICTORY","VIDEO","VIEW","VILLAGE","VIOLENCE","VISION","VISIT","VISITOR","VITAL","VOICE","VOLUME","VOTE","WAGE","WAIT","WALK","WALL","WANT","WAR","WARM","WARN","WASH","WATCH","WATER","WAVE","WAY","WE","WEAK","WEAPON","WEAR","WEATHER","WEEK","WEEKEND","WEIGHT","WELCOME","WELFARE","WELL","WEST","WESTERN","WHAT","WHATEVER","WHEN","WHERE","WHEREAS","WHETHER","WHICH","WHILE","WHILST","WHITE","WHO","WHOLE","WHOM","WHOSE","WHY","WIDE","WIDELY","WIFE","WILD","WILL","WIN","WIND","WINDOW","WINE","WING","WINNER","WINTER","WISH","WITH","WITHDRAW","WITHIN","WITHOUT","WOMAN","WONDER","WONDERFUL","WOOD","WORD","WORK","WORKER","WORKING","WORKS","WORLD","WORRY","WORTH","WOULD","WRITE","WRITER","WRITING","WRONG","YARD","YEAH","YEAR","YES","YESTERDAY","YET","YOU","YOUNG","YOUR","YOURSELF","YOUTH" diff --git a/project_euler/problem_48/sol1.py b/project_euler/problem_48/sol1.py index 95af951c0e8a..06ad1408dcef 100644 --- a/project_euler/problem_48/sol1.py +++ b/project_euler/problem_48/sol1.py @@ -7,11 +7,6 @@ Find the last ten digits of the series, 11 + 22 + 33 + ... + 10001000. """ -try: - xrange -except NameError: - xrange = range - def solution(): """Returns the last 10 digits of the series, 11 + 22 + 33 + ... + 10001000. @@ -20,7 +15,7 @@ def solution(): '9110846700' """ total = 0 - for i in xrange(1, 1001): + for i in range(1, 1001): total += i ** i return str(total)[-10:] diff --git a/project_euler/problem_53/sol1.py b/project_euler/problem_53/sol1.py index c72e0b993a34..0692bbe0ebb8 100644 --- a/project_euler/problem_53/sol1.py +++ b/project_euler/problem_53/sol1.py @@ -1,4 +1,3 @@ -# -.- coding: latin-1 -.- """ Combinatoric selections Problem 53 @@ -17,14 +16,8 @@ How many, not necessarily distinct, values of nCr, for 1 ≤ n ≤ 100, are greater than one-million? """ -from __future__ import print_function from math import factorial -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 - def combinations(n, r): return factorial(n) / (factorial(r) * factorial(n - r)) @@ -39,8 +32,8 @@ def solution(): """ total = 0 - for i in xrange(1, 101): - for j in xrange(1, i + 1): + for i in range(1, 101): + for j in range(1, i + 1): if combinations(i, j) > 1e6: total += 1 return total diff --git a/project_euler/problem_551/__init__.py b/project_euler/problem_551/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_551/sol1.py b/project_euler/problem_551/sol1.py new file mode 100644 index 000000000000..873e520cc9b4 --- /dev/null +++ b/project_euler/problem_551/sol1.py @@ -0,0 +1,204 @@ +""" +Sum of digits sequence +Problem 551 + +Let a(0), a(1),... be an integer sequence defined by: + a(0) = 1 + for n >= 1, a(n) is the sum of the digits of all preceding terms + +The sequence starts with 1, 1, 2, 4, 8, ... +You are given a(10^6) = 31054319. + +Find a(10^15) +""" + +ks = [k for k in range(2, 20 + 1)] +base = [10 ** k for k in range(ks[-1] + 1)] +memo = {} + + +def next_term(a_i, k, i, n): + """ + Calculates and updates a_i in-place to either the n-th term or the + smallest term for which c > 10^k when the terms are written in the form: + a(i) = b * 10^k + c + + For any a(i), if digitsum(b) and c have the same value, the difference + between subsequent terms will be the same until c >= 10^k. This difference + is cached to greatly speed up the computation. + + Arguments: + a_i -- array of digits starting from the one's place that represent + the i-th term in the sequence + k -- k when terms are written in the from a(i) = b*10^k + c. + Term are calulcated until c > 10^k or the n-th term is reached. + i -- position along the sequence + n -- term to calculate up to if k is large enough + + Return: a tuple of difference between ending term and starting term, and + the number of terms calculated. ex. if starting term is a_0=1, and + ending term is a_10=62, then (61, 9) is returned. + """ + # ds_b - digitsum(b) + ds_b = 0 + for j in range(k, len(a_i)): + ds_b += a_i[j] + c = 0 + for j in range(min(len(a_i), k)): + c += a_i[j] * base[j] + + diff, dn = 0, 0 + max_dn = n - i + + sub_memo = memo.get(ds_b) + + if sub_memo is not None: + jumps = sub_memo.get(c) + + if jumps is not None and len(jumps) > 0: + # find and make the largest jump without going over + max_jump = -1 + for _k in range(len(jumps) - 1, -1, -1): + if jumps[_k][2] <= k and jumps[_k][1] <= max_dn: + max_jump = _k + break + + if max_jump >= 0: + diff, dn, _kk = jumps[max_jump] + # since the difference between jumps is cached, add c + new_c = diff + c + for j in range(min(k, len(a_i))): + new_c, a_i[j] = divmod(new_c, 10) + if new_c > 0: + add(a_i, k, new_c) + + else: + sub_memo[c] = [] + else: + sub_memo = {c: []} + memo[ds_b] = sub_memo + + if dn >= max_dn or c + diff >= base[k]: + return diff, dn + + if k > ks[0]: + while True: + # keep doing smaller jumps + _diff, terms_jumped = next_term(a_i, k - 1, i + dn, n) + diff += _diff + dn += terms_jumped + + if dn >= max_dn or c + diff >= base[k]: + break + else: + # would be too small a jump, just compute sequential terms instead + _diff, terms_jumped = compute(a_i, k, i + dn, n) + diff += _diff + dn += terms_jumped + + jumps = sub_memo[c] + + # keep jumps sorted by # of terms skipped + j = 0 + while j < len(jumps): + if jumps[j][1] > dn: + break + j += 1 + + # cache the jump for this value digitsum(b) and c + sub_memo[c].insert(j, (diff, dn, k)) + return (diff, dn) + + +def compute(a_i, k, i, n): + """ + same as next_term(a_i, k, i, n) but computes terms without memoizing results. + """ + if i >= n: + return 0, i + if k > len(a_i): + a_i.extend([0 for _ in range(k - len(a_i))]) + + # note: a_i -> b * 10^k + c + # ds_b -> digitsum(b) + # ds_c -> digitsum(c) + start_i = i + ds_b, ds_c, diff = 0, 0, 0 + for j in range(len(a_i)): + if j >= k: + ds_b += a_i[j] + else: + ds_c += a_i[j] + + while i < n: + i += 1 + addend = ds_c + ds_b + diff += addend + ds_c = 0 + for j in range(k): + s = a_i[j] + addend + addend, a_i[j] = divmod(s, 10) + + ds_c += a_i[j] + + if addend > 0: + break + + if addend > 0: + add(a_i, k, addend) + return diff, i - start_i + + +def add(digits, k, addend): + """ + adds addend to digit array given in digits + starting at index k + """ + for j in range(k, len(digits)): + s = digits[j] + addend + if s >= 10: + quotient, digits[j] = divmod(s, 10) + addend = addend // 10 + quotient + else: + digits[j] = s + addend = addend // 10 + + if addend == 0: + break + + while addend > 0: + addend, digit = divmod(addend, 10) + digits.append(digit) + + +def solution(n): + """ + returns n-th term of sequence + + >>> solution(10) + 62 + + >>> solution(10**6) + 31054319 + + >>> solution(10**15) + 73597483551591773 + """ + + digits = [1] + i = 1 + dn = 0 + while True: + diff, terms_jumped = next_term(digits, 20, i + dn, n) + dn += terms_jumped + if dn == n - i: + break + + a_n = 0 + for j in range(len(digits)): + a_n += digits[j] * 10 ** j + return a_n + + +if __name__ == "__main__": + print(solution(10 ** 15)) diff --git a/project_euler/problem_56/__init__.py b/project_euler/problem_56/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_56/sol1.py b/project_euler/problem_56/sol1.py new file mode 100644 index 000000000000..d5225efcb9e5 --- /dev/null +++ b/project_euler/problem_56/sol1.py @@ -0,0 +1,32 @@ +def maximum_digital_sum(a: int, b: int) -> int: + """ + Considering natural numbers of the form, a**b, where a, b < 100, + what is the maximum digital sum? + :param a: + :param b: + :return: + >>> maximum_digital_sum(10,10) + 45 + + >>> maximum_digital_sum(100,100) + 972 + + >>> maximum_digital_sum(100,200) + 1872 + """ + + # RETURN the MAXIMUM from the list of SUMs of the list of INT converted from STR of BASE raised to the POWER + return max( + [ + sum([int(x) for x in str(base ** power)]) + for base in range(a) + for power in range(b) + ] + ) + + +# Tests +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/project_euler/problem_67/__init__.py b/project_euler/problem_67/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_67/sol1.py b/project_euler/problem_67/sol1.py new file mode 100644 index 000000000000..9494ff7bbabd --- /dev/null +++ b/project_euler/problem_67/sol1.py @@ -0,0 +1,49 @@ +""" +Problem Statement: +By starting at the top of the triangle below and moving to adjacent numbers on +the row below, the maximum total from top to bottom is 23. +3 +7 4 +2 4 6 +8 5 9 3 +That is, 3 + 7 + 4 + 9 = 23. +Find the maximum total from top to bottom in triangle.txt (right click and +'Save Link/Target As...'), a 15K text file containing a triangle with +one-hundred rows. +""" +import os + + +def solution(): + """ + Finds the maximum total in a triangle as described by the problem statement + above. + + >>> solution() + 7273 + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + triangle = os.path.join(script_dir, "triangle.txt") + + with open(triangle, "r") as f: + triangle = f.readlines() + + a = map(lambda x: x.rstrip("\r\n").split(" "), triangle) + a = list(map(lambda x: list(map(lambda y: int(y), x)), a)) + + for i in range(1, len(a)): + for j in range(len(a[i])): + if j != len(a[i - 1]): + number1 = a[i - 1][j] + else: + number1 = 0 + if j > 0: + number2 = a[i - 1][j - 1] + else: + number2 = 0 + a[i][j] += max(number1, number2) + return max(a[-1]) + + +if __name__ == "__main__": + print(solution()) diff --git a/project_euler/problem_67/triangle.txt b/project_euler/problem_67/triangle.txt new file mode 100644 index 000000000000..00aa2bc6382d --- /dev/null +++ b/project_euler/problem_67/triangle.txt @@ -0,0 +1,100 @@ +59 +73 41 +52 40 09 +26 53 06 34 +10 51 87 86 81 +61 95 66 57 25 68 +90 81 80 38 92 67 73 +30 28 51 76 81 18 75 44 +84 14 95 87 62 81 17 78 58 +21 46 71 58 02 79 62 39 31 09 +56 34 35 53 78 31 81 18 90 93 15 +78 53 04 21 84 93 32 13 97 11 37 51 +45 03 81 79 05 18 78 86 13 30 63 99 95 +39 87 96 28 03 38 42 17 82 87 58 07 22 57 +06 17 51 17 07 93 09 07 75 97 95 78 87 08 53 +67 66 59 60 88 99 94 65 55 77 55 34 27 53 78 28 +76 40 41 04 87 16 09 42 75 69 23 97 30 60 10 79 87 +12 10 44 26 21 36 32 84 98 60 13 12 36 16 63 31 91 35 +70 39 06 05 55 27 38 48 28 22 34 35 62 62 15 14 94 89 86 +66 56 68 84 96 21 34 34 34 81 62 40 65 54 62 05 98 03 02 60 +38 89 46 37 99 54 34 53 36 14 70 26 02 90 45 13 31 61 83 73 47 +36 10 63 96 60 49 41 05 37 42 14 58 84 93 96 17 09 43 05 43 06 59 +66 57 87 57 61 28 37 51 84 73 79 15 39 95 88 87 43 39 11 86 77 74 18 +54 42 05 79 30 49 99 73 46 37 50 02 45 09 54 52 27 95 27 65 19 45 26 45 +71 39 17 78 76 29 52 90 18 99 78 19 35 62 71 19 23 65 93 85 49 33 75 09 02 +33 24 47 61 60 55 32 88 57 55 91 54 46 57 07 77 98 52 80 99 24 25 46 78 79 05 +92 09 13 55 10 67 26 78 76 82 63 49 51 31 24 68 05 57 07 54 69 21 67 43 17 63 12 +24 59 06 08 98 74 66 26 61 60 13 03 09 09 24 30 71 08 88 70 72 70 29 90 11 82 41 34 +66 82 67 04 36 60 92 77 91 85 62 49 59 61 30 90 29 94 26 41 89 04 53 22 83 41 09 74 90 +48 28 26 37 28 52 77 26 51 32 18 98 79 36 62 13 17 08 19 54 89 29 73 68 42 14 08 16 70 37 +37 60 69 70 72 71 09 59 13 60 38 13 57 36 09 30 43 89 30 39 15 02 44 73 05 73 26 63 56 86 12 +55 55 85 50 62 99 84 77 28 85 03 21 27 22 19 26 82 69 54 04 13 07 85 14 01 15 70 59 89 95 10 19 +04 09 31 92 91 38 92 86 98 75 21 05 64 42 62 84 36 20 73 42 21 23 22 51 51 79 25 45 85 53 03 43 22 +75 63 02 49 14 12 89 14 60 78 92 16 44 82 38 30 72 11 46 52 90 27 08 65 78 03 85 41 57 79 39 52 33 48 +78 27 56 56 39 13 19 43 86 72 58 95 39 07 04 34 21 98 39 15 39 84 89 69 84 46 37 57 59 35 59 50 26 15 93 +42 89 36 27 78 91 24 11 17 41 05 94 07 69 51 96 03 96 47 90 90 45 91 20 50 56 10 32 36 49 04 53 85 92 25 65 +52 09 61 30 61 97 66 21 96 92 98 90 06 34 96 60 32 69 68 33 75 84 18 31 71 50 84 63 03 03 19 11 28 42 75 45 45 +61 31 61 68 96 34 49 39 05 71 76 59 62 67 06 47 96 99 34 21 32 47 52 07 71 60 42 72 94 56 82 83 84 40 94 87 82 46 +01 20 60 14 17 38 26 78 66 81 45 95 18 51 98 81 48 16 53 88 37 52 69 95 72 93 22 34 98 20 54 27 73 61 56 63 60 34 63 +93 42 94 83 47 61 27 51 79 79 45 01 44 73 31 70 83 42 88 25 53 51 30 15 65 94 80 44 61 84 12 77 02 62 02 65 94 42 14 94 +32 73 09 67 68 29 74 98 10 19 85 48 38 31 85 67 53 93 93 77 47 67 39 72 94 53 18 43 77 40 78 32 29 59 24 06 02 83 50 60 66 +32 01 44 30 16 51 15 81 98 15 10 62 86 79 50 62 45 60 70 38 31 85 65 61 64 06 69 84 14 22 56 43 09 48 66 69 83 91 60 40 36 61 +92 48 22 99 15 95 64 43 01 16 94 02 99 19 17 69 11 58 97 56 89 31 77 45 67 96 12 73 08 20 36 47 81 44 50 64 68 85 40 81 85 52 09 +91 35 92 45 32 84 62 15 19 64 21 66 06 01 52 80 62 59 12 25 88 28 91 50 40 16 22 99 92 79 87 51 21 77 74 77 07 42 38 42 74 83 02 05 +46 19 77 66 24 18 05 32 02 84 31 99 92 58 96 72 91 36 62 99 55 29 53 42 12 37 26 58 89 50 66 19 82 75 12 48 24 87 91 85 02 07 03 76 86 +99 98 84 93 07 17 33 61 92 20 66 60 24 66 40 30 67 05 37 29 24 96 03 27 70 62 13 04 45 47 59 88 43 20 66 15 46 92 30 04 71 66 78 70 53 99 +67 60 38 06 88 04 17 72 10 99 71 07 42 25 54 05 26 64 91 50 45 71 06 30 67 48 69 82 08 56 80 67 18 46 66 63 01 20 08 80 47 07 91 16 03 79 87 +18 54 78 49 80 48 77 40 68 23 60 88 58 80 33 57 11 69 55 53 64 02 94 49 60 92 16 35 81 21 82 96 25 24 96 18 02 05 49 03 50 77 06 32 84 27 18 38 +68 01 50 04 03 21 42 94 53 24 89 05 92 26 52 36 68 11 85 01 04 42 02 45 15 06 50 04 53 73 25 74 81 88 98 21 67 84 79 97 99 20 95 04 40 46 02 58 87 +94 10 02 78 88 52 21 03 88 60 06 53 49 71 20 91 12 65 07 49 21 22 11 41 58 99 36 16 09 48 17 24 52 36 23 15 72 16 84 56 02 99 43 76 81 71 29 39 49 17 +64 39 59 84 86 16 17 66 03 09 43 06 64 18 63 29 68 06 23 07 87 14 26 35 17 12 98 41 53 64 78 18 98 27 28 84 80 67 75 62 10 11 76 90 54 10 05 54 41 39 66 +43 83 18 37 32 31 52 29 95 47 08 76 35 11 04 53 35 43 34 10 52 57 12 36 20 39 40 55 78 44 07 31 38 26 08 15 56 88 86 01 52 62 10 24 32 05 60 65 53 28 57 99 +03 50 03 52 07 73 49 92 66 80 01 46 08 67 25 36 73 93 07 42 25 53 13 96 76 83 87 90 54 89 78 22 78 91 73 51 69 09 79 94 83 53 09 40 69 62 10 79 49 47 03 81 30 +71 54 73 33 51 76 59 54 79 37 56 45 84 17 62 21 98 69 41 95 65 24 39 37 62 03 24 48 54 64 46 82 71 78 33 67 09 16 96 68 52 74 79 68 32 21 13 78 96 60 09 69 20 36 +73 26 21 44 46 38 17 83 65 98 07 23 52 46 61 97 33 13 60 31 70 15 36 77 31 58 56 93 75 68 21 36 69 53 90 75 25 82 39 50 65 94 29 30 11 33 11 13 96 02 56 47 07 49 02 +76 46 73 30 10 20 60 70 14 56 34 26 37 39 48 24 55 76 84 91 39 86 95 61 50 14 53 93 64 67 37 31 10 84 42 70 48 20 10 72 60 61 84 79 69 65 99 73 89 25 85 48 92 56 97 16 +03 14 80 27 22 30 44 27 67 75 79 32 51 54 81 29 65 14 19 04 13 82 04 91 43 40 12 52 29 99 07 76 60 25 01 07 61 71 37 92 40 47 99 66 57 01 43 44 22 40 53 53 09 69 26 81 07 +49 80 56 90 93 87 47 13 75 28 87 23 72 79 32 18 27 20 28 10 37 59 21 18 70 04 79 96 03 31 45 71 81 06 14 18 17 05 31 50 92 79 23 47 09 39 47 91 43 54 69 47 42 95 62 46 32 85 +37 18 62 85 87 28 64 05 77 51 47 26 30 65 05 70 65 75 59 80 42 52 25 20 44 10 92 17 71 95 52 14 77 13 24 55 11 65 26 91 01 30 63 15 49 48 41 17 67 47 03 68 20 90 98 32 04 40 68 +90 51 58 60 06 55 23 68 05 19 76 94 82 36 96 43 38 90 87 28 33 83 05 17 70 83 96 93 06 04 78 47 80 06 23 84 75 23 87 72 99 14 50 98 92 38 90 64 61 58 76 94 36 66 87 80 51 35 61 38 +57 95 64 06 53 36 82 51 40 33 47 14 07 98 78 65 39 58 53 06 50 53 04 69 40 68 36 69 75 78 75 60 03 32 39 24 74 47 26 90 13 40 44 71 90 76 51 24 36 50 25 45 70 80 61 80 61 43 90 64 11 +18 29 86 56 68 42 79 10 42 44 30 12 96 18 23 18 52 59 02 99 67 46 60 86 43 38 55 17 44 93 42 21 55 14 47 34 55 16 49 24 23 29 96 51 55 10 46 53 27 92 27 46 63 57 30 65 43 27 21 20 24 83 +81 72 93 19 69 52 48 01 13 83 92 69 20 48 69 59 20 62 05 42 28 89 90 99 32 72 84 17 08 87 36 03 60 31 36 36 81 26 97 36 48 54 56 56 27 16 91 08 23 11 87 99 33 47 02 14 44 73 70 99 43 35 33 +90 56 61 86 56 12 70 59 63 32 01 15 81 47 71 76 95 32 65 80 54 70 34 51 40 45 33 04 64 55 78 68 88 47 31 47 68 87 03 84 23 44 89 72 35 08 31 76 63 26 90 85 96 67 65 91 19 14 17 86 04 71 32 95 +37 13 04 22 64 37 37 28 56 62 86 33 07 37 10 44 52 82 52 06 19 52 57 75 90 26 91 24 06 21 14 67 76 30 46 14 35 89 89 41 03 64 56 97 87 63 22 34 03 79 17 45 11 53 25 56 96 61 23 18 63 31 37 37 47 +77 23 26 70 72 76 77 04 28 64 71 69 14 85 96 54 95 48 06 62 99 83 86 77 97 75 71 66 30 19 57 90 33 01 60 61 14 12 90 99 32 77 56 41 18 14 87 49 10 14 90 64 18 50 21 74 14 16 88 05 45 73 82 47 74 44 +22 97 41 13 34 31 54 61 56 94 03 24 59 27 98 77 04 09 37 40 12 26 87 09 71 70 07 18 64 57 80 21 12 71 83 94 60 39 73 79 73 19 97 32 64 29 41 07 48 84 85 67 12 74 95 20 24 52 41 67 56 61 29 93 35 72 69 +72 23 63 66 01 11 07 30 52 56 95 16 65 26 83 90 50 74 60 18 16 48 43 77 37 11 99 98 30 94 91 26 62 73 45 12 87 73 47 27 01 88 66 99 21 41 95 80 02 53 23 32 61 48 32 43 43 83 14 66 95 91 19 81 80 67 25 88 +08 62 32 18 92 14 83 71 37 96 11 83 39 99 05 16 23 27 10 67 02 25 44 11 55 31 46 64 41 56 44 74 26 81 51 31 45 85 87 09 81 95 22 28 76 69 46 48 64 87 67 76 27 89 31 11 74 16 62 03 60 94 42 47 09 34 94 93 72 +56 18 90 18 42 17 42 32 14 86 06 53 33 95 99 35 29 15 44 20 49 59 25 54 34 59 84 21 23 54 35 90 78 16 93 13 37 88 54 19 86 67 68 55 66 84 65 42 98 37 87 56 33 28 58 38 28 38 66 27 52 21 81 15 08 22 97 32 85 27 +91 53 40 28 13 34 91 25 01 63 50 37 22 49 71 58 32 28 30 18 68 94 23 83 63 62 94 76 80 41 90 22 82 52 29 12 18 56 10 08 35 14 37 57 23 65 67 40 72 39 93 39 70 89 40 34 07 46 94 22 20 05 53 64 56 30 05 56 61 88 27 +23 95 11 12 37 69 68 24 66 10 87 70 43 50 75 07 62 41 83 58 95 93 89 79 45 39 02 22 05 22 95 43 62 11 68 29 17 40 26 44 25 71 87 16 70 85 19 25 59 94 90 41 41 80 61 70 55 60 84 33 95 76 42 63 15 09 03 40 38 12 03 32 +09 84 56 80 61 55 85 97 16 94 82 94 98 57 84 30 84 48 93 90 71 05 95 90 73 17 30 98 40 64 65 89 07 79 09 19 56 36 42 30 23 69 73 72 07 05 27 61 24 31 43 48 71 84 21 28 26 65 65 59 65 74 77 20 10 81 61 84 95 08 52 23 70 +47 81 28 09 98 51 67 64 35 51 59 36 92 82 77 65 80 24 72 53 22 07 27 10 21 28 30 22 48 82 80 48 56 20 14 43 18 25 50 95 90 31 77 08 09 48 44 80 90 22 93 45 82 17 13 96 25 26 08 73 34 99 06 49 24 06 83 51 40 14 15 10 25 01 +54 25 10 81 30 64 24 74 75 80 36 75 82 60 22 69 72 91 45 67 03 62 79 54 89 74 44 83 64 96 66 73 44 30 74 50 37 05 09 97 70 01 60 46 37 91 39 75 75 18 58 52 72 78 51 81 86 52 08 97 01 46 43 66 98 62 81 18 70 93 73 08 32 46 34 +96 80 82 07 59 71 92 53 19 20 88 66 03 26 26 10 24 27 50 82 94 73 63 08 51 33 22 45 19 13 58 33 90 15 22 50 36 13 55 06 35 47 82 52 33 61 36 27 28 46 98 14 73 20 73 32 16 26 80 53 47 66 76 38 94 45 02 01 22 52 47 96 64 58 52 39 +88 46 23 39 74 63 81 64 20 90 33 33 76 55 58 26 10 46 42 26 74 74 12 83 32 43 09 02 73 55 86 54 85 34 28 23 29 79 91 62 47 41 82 87 99 22 48 90 20 05 96 75 95 04 43 28 81 39 81 01 28 42 78 25 39 77 90 57 58 98 17 36 73 22 63 74 51 +29 39 74 94 95 78 64 24 38 86 63 87 93 06 70 92 22 16 80 64 29 52 20 27 23 50 14 13 87 15 72 96 81 22 08 49 72 30 70 24 79 31 16 64 59 21 89 34 96 91 48 76 43 53 88 01 57 80 23 81 90 79 58 01 80 87 17 99 86 90 72 63 32 69 14 28 88 69 +37 17 71 95 56 93 71 35 43 45 04 98 92 94 84 96 11 30 31 27 31 60 92 03 48 05 98 91 86 94 35 90 90 08 48 19 33 28 68 37 59 26 65 96 50 68 22 07 09 49 34 31 77 49 43 06 75 17 81 87 61 79 52 26 27 72 29 50 07 98 86 01 17 10 46 64 24 18 56 +51 30 25 94 88 85 79 91 40 33 63 84 49 67 98 92 15 26 75 19 82 05 18 78 65 93 61 48 91 43 59 41 70 51 22 15 92 81 67 91 46 98 11 11 65 31 66 10 98 65 83 21 05 56 05 98 73 67 46 74 69 34 08 30 05 52 07 98 32 95 30 94 65 50 24 63 28 81 99 57 +19 23 61 36 09 89 71 98 65 17 30 29 89 26 79 74 94 11 44 48 97 54 81 55 39 66 69 45 28 47 13 86 15 76 74 70 84 32 36 33 79 20 78 14 41 47 89 28 81 05 99 66 81 86 38 26 06 25 13 60 54 55 23 53 27 05 89 25 23 11 13 54 59 54 56 34 16 24 53 44 06 +13 40 57 72 21 15 60 08 04 19 11 98 34 45 09 97 86 71 03 15 56 19 15 44 97 31 90 04 87 87 76 08 12 30 24 62 84 28 12 85 82 53 99 52 13 94 06 65 97 86 09 50 94 68 69 74 30 67 87 94 63 07 78 27 80 36 69 41 06 92 32 78 37 82 30 05 18 87 99 72 19 99 +44 20 55 77 69 91 27 31 28 81 80 27 02 07 97 23 95 98 12 25 75 29 47 71 07 47 78 39 41 59 27 76 13 15 66 61 68 35 69 86 16 53 67 63 99 85 41 56 08 28 33 40 94 76 90 85 31 70 24 65 84 65 99 82 19 25 54 37 21 46 33 02 52 99 51 33 26 04 87 02 08 18 96 +54 42 61 45 91 06 64 79 80 82 32 16 83 63 42 49 19 78 65 97 40 42 14 61 49 34 04 18 25 98 59 30 82 72 26 88 54 36 21 75 03 88 99 53 46 51 55 78 22 94 34 40 68 87 84 25 30 76 25 08 92 84 42 61 40 38 09 99 40 23 29 39 46 55 10 90 35 84 56 70 63 23 91 39 +52 92 03 71 89 07 09 37 68 66 58 20 44 92 51 56 13 71 79 99 26 37 02 06 16 67 36 52 58 16 79 73 56 60 59 27 44 77 94 82 20 50 98 33 09 87 94 37 40 83 64 83 58 85 17 76 53 02 83 52 22 27 39 20 48 92 45 21 09 42 24 23 12 37 52 28 50 78 79 20 86 62 73 20 59 +54 96 80 15 91 90 99 70 10 09 58 90 93 50 81 99 54 38 36 10 30 11 35 84 16 45 82 18 11 97 36 43 96 79 97 65 40 48 23 19 17 31 64 52 65 65 37 32 65 76 99 79 34 65 79 27 55 33 03 01 33 27 61 28 66 08 04 70 49 46 48 83 01 45 19 96 13 81 14 21 31 79 93 85 50 05 +92 92 48 84 59 98 31 53 23 27 15 22 79 95 24 76 05 79 16 93 97 89 38 89 42 83 02 88 94 95 82 21 01 97 48 39 31 78 09 65 50 56 97 61 01 07 65 27 21 23 14 15 80 97 44 78 49 35 33 45 81 74 34 05 31 57 09 38 94 07 69 54 69 32 65 68 46 68 78 90 24 28 49 51 45 86 35 +41 63 89 76 87 31 86 09 46 14 87 82 22 29 47 16 13 10 70 72 82 95 48 64 58 43 13 75 42 69 21 12 67 13 64 85 58 23 98 09 37 76 05 22 31 12 66 50 29 99 86 72 45 25 10 28 19 06 90 43 29 31 67 79 46 25 74 14 97 35 76 37 65 46 23 82 06 22 30 76 93 66 94 17 96 13 20 72 +63 40 78 08 52 09 90 41 70 28 36 14 46 44 85 96 24 52 58 15 87 37 05 98 99 39 13 61 76 38 44 99 83 74 90 22 53 80 56 98 30 51 63 39 44 30 91 91 04 22 27 73 17 35 53 18 35 45 54 56 27 78 48 13 69 36 44 38 71 25 30 56 15 22 73 43 32 69 59 25 93 83 45 11 34 94 44 39 92 +12 36 56 88 13 96 16 12 55 54 11 47 19 78 17 17 68 81 77 51 42 55 99 85 66 27 81 79 93 42 65 61 69 74 14 01 18 56 12 01 58 37 91 22 42 66 83 25 19 04 96 41 25 45 18 69 96 88 36 93 10 12 98 32 44 83 83 04 72 91 04 27 73 07 34 37 71 60 59 31 01 54 54 44 96 93 83 36 04 45 +30 18 22 20 42 96 65 79 17 41 55 69 94 81 29 80 91 31 85 25 47 26 43 49 02 99 34 67 99 76 16 14 15 93 08 32 99 44 61 77 67 50 43 55 87 55 53 72 17 46 62 25 50 99 73 05 93 48 17 31 70 80 59 09 44 59 45 13 74 66 58 94 87 73 16 14 85 38 74 99 64 23 79 28 71 42 20 37 82 31 23 +51 96 39 65 46 71 56 13 29 68 53 86 45 33 51 49 12 91 21 21 76 85 02 17 98 15 46 12 60 21 88 30 92 83 44 59 42 50 27 88 46 86 94 73 45 54 23 24 14 10 94 21 20 34 23 51 04 83 99 75 90 63 60 16 22 33 83 70 11 32 10 50 29 30 83 46 11 05 31 17 86 42 49 01 44 63 28 60 07 78 95 40 +44 61 89 59 04 49 51 27 69 71 46 76 44 04 09 34 56 39 15 06 94 91 75 90 65 27 56 23 74 06 23 33 36 69 14 39 05 34 35 57 33 22 76 46 56 10 61 65 98 09 16 69 04 62 65 18 99 76 49 18 72 66 73 83 82 40 76 31 89 91 27 88 17 35 41 35 32 51 32 67 52 68 74 85 80 57 07 11 62 66 47 22 67 +65 37 19 97 26 17 16 24 24 17 50 37 64 82 24 36 32 11 68 34 69 31 32 89 79 93 96 68 49 90 14 23 04 04 67 99 81 74 70 74 36 96 68 09 64 39 88 35 54 89 96 58 66 27 88 97 32 14 06 35 78 20 71 06 85 66 57 02 58 91 72 05 29 56 73 48 86 52 09 93 22 57 79 42 12 01 31 68 17 59 63 76 07 77 +73 81 14 13 17 20 11 09 01 83 08 85 91 70 84 63 62 77 37 07 47 01 59 95 39 69 39 21 99 09 87 02 97 16 92 36 74 71 90 66 33 73 73 75 52 91 11 12 26 53 05 26 26 48 61 50 90 65 01 87 42 47 74 35 22 73 24 26 56 70 52 05 48 41 31 18 83 27 21 39 80 85 26 08 44 02 71 07 63 22 05 52 19 08 20 +17 25 21 11 72 93 33 49 64 23 53 82 03 13 91 65 85 02 40 05 42 31 77 42 05 36 06 54 04 58 07 76 87 83 25 57 66 12 74 33 85 37 74 32 20 69 03 97 91 68 82 44 19 14 89 28 85 85 80 53 34 87 58 98 88 78 48 65 98 40 11 57 10 67 70 81 60 79 74 72 97 59 79 47 30 20 54 80 89 91 14 05 33 36 79 39 +60 85 59 39 60 07 57 76 77 92 06 35 15 72 23 41 45 52 95 18 64 79 86 53 56 31 69 11 91 31 84 50 44 82 22 81 41 40 30 42 30 91 48 94 74 76 64 58 74 25 96 57 14 19 03 99 28 83 15 75 99 01 89 85 79 50 03 95 32 67 44 08 07 41 62 64 29 20 14 76 26 55 48 71 69 66 19 72 44 25 14 01 48 74 12 98 07 +64 66 84 24 18 16 27 48 20 14 47 69 30 86 48 40 23 16 61 21 51 50 26 47 35 33 91 28 78 64 43 68 04 79 51 08 19 60 52 95 06 68 46 86 35 97 27 58 04 65 30 58 99 12 12 75 91 39 50 31 42 64 70 04 46 07 98 73 98 93 37 89 77 91 64 71 64 65 66 21 78 62 81 74 42 20 83 70 73 95 78 45 92 27 34 53 71 15 +30 11 85 31 34 71 13 48 05 14 44 03 19 67 23 73 19 57 06 90 94 72 57 69 81 62 59 68 88 57 55 69 49 13 07 87 97 80 89 05 71 05 05 26 38 40 16 62 45 99 18 38 98 24 21 26 62 74 69 04 85 57 77 35 58 67 91 79 79 57 86 28 66 34 72 51 76 78 36 95 63 90 08 78 47 63 45 31 22 70 52 48 79 94 15 77 61 67 68 +23 33 44 81 80 92 93 75 94 88 23 61 39 76 22 03 28 94 32 06 49 65 41 34 18 23 08 47 62 60 03 63 33 13 80 52 31 54 73 43 70 26 16 69 57 87 83 31 03 93 70 81 47 95 77 44 29 68 39 51 56 59 63 07 25 70 07 77 43 53 64 03 94 42 95 39 18 01 66 21 16 97 20 50 90 16 70 10 95 69 29 06 25 61 41 26 15 59 63 35 diff --git a/project_euler/problem_76/sol1.py b/project_euler/problem_76/sol1.py index c9e3c452fbc4..ed0ee6b507e9 100644 --- a/project_euler/problem_76/sol1.py +++ b/project_euler/problem_76/sol1.py @@ -14,12 +14,6 @@ How many different ways can one hundred be written as a sum of at least two positive integers? """ -from __future__ import print_function - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 def partition(m): @@ -43,12 +37,12 @@ def partition(m): >>> partition(1) 0 """ - memo = [[0 for _ in xrange(m)] for _ in xrange(m + 1)] - for i in xrange(m + 1): + memo = [[0 for _ in range(m)] for _ in range(m + 1)] + for i in range(m + 1): memo[i][0] = 1 - for n in xrange(m + 1): - for k in xrange(1, m): + for n in range(m + 1): + for k in range(1, m): memo[n][k] += memo[n][k - 1] if n > k: memo[n][k] += memo[n - k - 1][k] diff --git a/project_euler/problem_99/__init__.py b/project_euler/problem_99/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_99/base_exp.txt b/project_euler/problem_99/base_exp.txt new file mode 100644 index 000000000000..abe95aa86036 --- /dev/null +++ b/project_euler/problem_99/base_exp.txt @@ -0,0 +1,1000 @@ +519432,525806 +632382,518061 +78864,613712 +466580,530130 +780495,510032 +525895,525320 +15991,714883 +960290,502358 +760018,511029 +166800,575487 +210884,564478 +555151,523163 +681146,515199 +563395,522587 +738250,512126 +923525,503780 +595148,520429 +177108,572629 +750923,511482 +440902,532446 +881418,505504 +422489,534197 +979858,501616 +685893,514935 +747477,511661 +167214,575367 +234140,559696 +940238,503122 +728969,512609 +232083,560102 +900971,504694 +688801,514772 +189664,569402 +891022,505104 +445689,531996 +119570,591871 +821453,508118 +371084,539600 +911745,504251 +623655,518600 +144361,582486 +352442,541775 +420726,534367 +295298,549387 +6530,787777 +468397,529976 +672336,515696 +431861,533289 +84228,610150 +805376,508857 +444409,532117 +33833,663511 +381850,538396 +402931,536157 +92901,604930 +304825,548004 +731917,512452 +753734,511344 +51894,637373 +151578,580103 +295075,549421 +303590,548183 +333594,544123 +683952,515042 +60090,628880 +951420,502692 +28335,674991 +714940,513349 +343858,542826 +549279,523586 +804571,508887 +260653,554881 +291399,549966 +402342,536213 +408889,535550 +40328,652524 +375856,539061 +768907,510590 +165993,575715 +976327,501755 +898500,504795 +360404,540830 +478714,529095 +694144,514472 +488726,528258 +841380,507226 +328012,544839 +22389,690868 +604053,519852 +329514,544641 +772965,510390 +492798,527927 +30125,670983 +895603,504906 +450785,531539 +840237,507276 +380711,538522 +63577,625673 +76801,615157 +502694,527123 +597706,520257 +310484,547206 +944468,502959 +121283,591152 +451131,531507 +566499,522367 +425373,533918 +40240,652665 +39130,654392 +714926,513355 +469219,529903 +806929,508783 +287970,550487 +92189,605332 +103841,599094 +671839,515725 +452048,531421 +987837,501323 +935192,503321 +88585,607450 +613883,519216 +144551,582413 +647359,517155 +213902,563816 +184120,570789 +258126,555322 +502546,527130 +407655,535678 +401528,536306 +477490,529193 +841085,507237 +732831,512408 +833000,507595 +904694,504542 +581435,521348 +455545,531110 +873558,505829 +94916,603796 +720176,513068 +545034,523891 +246348,557409 +556452,523079 +832015,507634 +173663,573564 +502634,527125 +250732,556611 +569786,522139 +216919,563178 +521815,525623 +92304,605270 +164446,576167 +753413,511364 +11410,740712 +448845,531712 +925072,503725 +564888,522477 +7062,780812 +641155,517535 +738878,512100 +636204,517828 +372540,539436 +443162,532237 +571192,522042 +655350,516680 +299741,548735 +581914,521307 +965471,502156 +513441,526277 +808682,508700 +237589,559034 +543300,524025 +804712,508889 +247511,557192 +543486,524008 +504383,526992 +326529,545039 +792493,509458 +86033,609017 +126554,589005 +579379,521481 +948026,502823 +404777,535969 +265767,554022 +266876,553840 +46631,643714 +492397,527958 +856106,506581 +795757,509305 +748946,511584 +294694,549480 +409781,535463 +775887,510253 +543747,523991 +210592,564536 +517119,525990 +520253,525751 +247926,557124 +592141,520626 +346580,542492 +544969,523902 +506501,526817 +244520,557738 +144745,582349 +69274,620858 +292620,549784 +926027,503687 +736320,512225 +515528,526113 +407549,535688 +848089,506927 +24141,685711 +9224,757964 +980684,501586 +175259,573121 +489160,528216 +878970,505604 +969546,502002 +525207,525365 +690461,514675 +156510,578551 +659778,516426 +468739,529945 +765252,510770 +76703,615230 +165151,575959 +29779,671736 +928865,503569 +577538,521605 +927555,503618 +185377,570477 +974756,501809 +800130,509093 +217016,563153 +365709,540216 +774508,510320 +588716,520851 +631673,518104 +954076,502590 +777828,510161 +990659,501222 +597799,520254 +786905,509727 +512547,526348 +756449,511212 +869787,505988 +653747,516779 +84623,609900 +839698,507295 +30159,670909 +797275,509234 +678136,515373 +897144,504851 +989554,501263 +413292,535106 +55297,633667 +788650,509637 +486748,528417 +150724,580377 +56434,632490 +77207,614869 +588631,520859 +611619,519367 +100006,601055 +528924,525093 +190225,569257 +851155,506789 +682593,515114 +613043,519275 +514673,526183 +877634,505655 +878905,505602 +1926,914951 +613245,519259 +152481,579816 +841774,507203 +71060,619442 +865335,506175 +90244,606469 +302156,548388 +399059,536557 +478465,529113 +558601,522925 +69132,620966 +267663,553700 +988276,501310 +378354,538787 +529909,525014 +161733,576968 +758541,511109 +823425,508024 +149821,580667 +269258,553438 +481152,528891 +120871,591322 +972322,501901 +981350,501567 +676129,515483 +950860,502717 +119000,592114 +392252,537272 +191618,568919 +946699,502874 +289555,550247 +799322,509139 +703886,513942 +194812,568143 +261823,554685 +203052,566221 +217330,563093 +734748,512313 +391759,537328 +807052,508777 +564467,522510 +59186,629748 +113447,594545 +518063,525916 +905944,504492 +613922,519213 +439093,532607 +445946,531981 +230530,560399 +297887,549007 +459029,530797 +403692,536075 +855118,506616 +963127,502245 +841711,507208 +407411,535699 +924729,503735 +914823,504132 +333725,544101 +176345,572832 +912507,504225 +411273,535308 +259774,555036 +632853,518038 +119723,591801 +163902,576321 +22691,689944 +402427,536212 +175769,572988 +837260,507402 +603432,519893 +313679,546767 +538165,524394 +549026,523608 +61083,627945 +898345,504798 +992556,501153 +369999,539727 +32847,665404 +891292,505088 +152715,579732 +824104,507997 +234057,559711 +730507,512532 +960529,502340 +388395,537687 +958170,502437 +57105,631806 +186025,570311 +993043,501133 +576770,521664 +215319,563513 +927342,503628 +521353,525666 +39563,653705 +752516,511408 +110755,595770 +309749,547305 +374379,539224 +919184,503952 +990652,501226 +647780,517135 +187177,570017 +168938,574877 +649558,517023 +278126,552016 +162039,576868 +658512,516499 +498115,527486 +896583,504868 +561170,522740 +747772,511647 +775093,510294 +652081,516882 +724905,512824 +499707,527365 +47388,642755 +646668,517204 +571700,522007 +180430,571747 +710015,513617 +435522,532941 +98137,602041 +759176,511070 +486124,528467 +526942,525236 +878921,505604 +408313,535602 +926980,503640 +882353,505459 +566887,522345 +3326,853312 +911981,504248 +416309,534800 +392991,537199 +622829,518651 +148647,581055 +496483,527624 +666314,516044 +48562,641293 +672618,515684 +443676,532187 +274065,552661 +265386,554079 +347668,542358 +31816,667448 +181575,571446 +961289,502320 +365689,540214 +987950,501317 +932299,503440 +27388,677243 +746701,511701 +492258,527969 +147823,581323 +57918,630985 +838849,507333 +678038,515375 +27852,676130 +850241,506828 +818403,508253 +131717,587014 +850216,506834 +904848,504529 +189758,569380 +392845,537217 +470876,529761 +925353,503711 +285431,550877 +454098,531234 +823910,508003 +318493,546112 +766067,510730 +261277,554775 +421530,534289 +694130,514478 +120439,591498 +213308,563949 +854063,506662 +365255,540263 +165437,575872 +662240,516281 +289970,550181 +847977,506933 +546083,523816 +413252,535113 +975829,501767 +361540,540701 +235522,559435 +224643,561577 +736350,512229 +328303,544808 +35022,661330 +307838,547578 +474366,529458 +873755,505819 +73978,617220 +827387,507845 +670830,515791 +326511,545034 +309909,547285 +400970,536363 +884827,505352 +718307,513175 +28462,674699 +599384,520150 +253565,556111 +284009,551093 +343403,542876 +446557,531921 +992372,501160 +961601,502308 +696629,514342 +919537,503945 +894709,504944 +892201,505051 +358160,541097 +448503,531745 +832156,507636 +920045,503924 +926137,503675 +416754,534757 +254422,555966 +92498,605151 +826833,507873 +660716,516371 +689335,514746 +160045,577467 +814642,508425 +969939,501993 +242856,558047 +76302,615517 +472083,529653 +587101,520964 +99066,601543 +498005,527503 +709800,513624 +708000,513716 +20171,698134 +285020,550936 +266564,553891 +981563,501557 +846502,506991 +334,1190800 +209268,564829 +9844,752610 +996519,501007 +410059,535426 +432931,533188 +848012,506929 +966803,502110 +983434,501486 +160700,577267 +504374,526989 +832061,507640 +392825,537214 +443842,532165 +440352,532492 +745125,511776 +13718,726392 +661753,516312 +70500,619875 +436952,532814 +424724,533973 +21954,692224 +262490,554567 +716622,513264 +907584,504425 +60086,628882 +837123,507412 +971345,501940 +947162,502855 +139920,584021 +68330,621624 +666452,516038 +731446,512481 +953350,502619 +183157,571042 +845400,507045 +651548,516910 +20399,697344 +861779,506331 +629771,518229 +801706,509026 +189207,569512 +737501,512168 +719272,513115 +479285,529045 +136046,585401 +896746,504860 +891735,505067 +684771,514999 +865309,506184 +379066,538702 +503117,527090 +621780,518717 +209518,564775 +677135,515423 +987500,501340 +197049,567613 +329315,544673 +236756,559196 +357092,541226 +520440,525733 +213471,563911 +956852,502490 +702223,514032 +404943,535955 +178880,572152 +689477,514734 +691351,514630 +866669,506128 +370561,539656 +739805,512051 +71060,619441 +624861,518534 +261660,554714 +366137,540160 +166054,575698 +601878,519990 +153445,579501 +279899,551729 +379166,538691 +423209,534125 +675310,515526 +145641,582050 +691353,514627 +917468,504026 +284778,550976 +81040,612235 +161699,576978 +616394,519057 +767490,510661 +156896,578431 +427408,533714 +254849,555884 +737217,512182 +897133,504851 +203815,566051 +270822,553189 +135854,585475 +778805,510111 +784373,509847 +305426,547921 +733418,512375 +732087,512448 +540668,524215 +702898,513996 +628057,518328 +640280,517587 +422405,534204 +10604,746569 +746038,511733 +839808,507293 +457417,530938 +479030,529064 +341758,543090 +620223,518824 +251661,556451 +561790,522696 +497733,527521 +724201,512863 +489217,528217 +415623,534867 +624610,518548 +847541,506953 +432295,533249 +400391,536421 +961158,502319 +139173,584284 +421225,534315 +579083,521501 +74274,617000 +701142,514087 +374465,539219 +217814,562985 +358972,540995 +88629,607424 +288597,550389 +285819,550812 +538400,524385 +809930,508645 +738326,512126 +955461,502535 +163829,576343 +826475,507891 +376488,538987 +102234,599905 +114650,594002 +52815,636341 +434037,533082 +804744,508880 +98385,601905 +856620,506559 +220057,562517 +844734,507078 +150677,580387 +558697,522917 +621751,518719 +207067,565321 +135297,585677 +932968,503404 +604456,519822 +579728,521462 +244138,557813 +706487,513800 +711627,513523 +853833,506674 +497220,527562 +59428,629511 +564845,522486 +623621,518603 +242689,558077 +125091,589591 +363819,540432 +686453,514901 +656813,516594 +489901,528155 +386380,537905 +542819,524052 +243987,557841 +693412,514514 +488484,528271 +896331,504881 +336730,543721 +728298,512647 +604215,519840 +153729,579413 +595687,520398 +540360,524240 +245779,557511 +924873,503730 +509628,526577 +528523,525122 +3509,847707 +522756,525555 +895447,504922 +44840,646067 +45860,644715 +463487,530404 +398164,536654 +894483,504959 +619415,518874 +966306,502129 +990922,501212 +835756,507474 +548881,523618 +453578,531282 +474993,529410 +80085,612879 +737091,512193 +50789,638638 +979768,501620 +792018,509483 +665001,516122 +86552,608694 +462772,530469 +589233,520821 +891694,505072 +592605,520594 +209645,564741 +42531,649269 +554376,523226 +803814,508929 +334157,544042 +175836,572970 +868379,506051 +658166,516520 +278203,551995 +966198,502126 +627162,518387 +296774,549165 +311803,547027 +843797,507118 +702304,514032 +563875,522553 +33103,664910 +191932,568841 +543514,524006 +506835,526794 +868368,506052 +847025,506971 +678623,515342 +876139,505726 +571997,521984 +598632,520198 +213590,563892 +625404,518497 +726508,512738 +689426,514738 +332495,544264 +411366,535302 +242546,558110 +315209,546555 +797544,509219 +93889,604371 +858879,506454 +124906,589666 +449072,531693 +235960,559345 +642403,517454 +720567,513047 +705534,513858 +603692,519870 +488137,528302 +157370,578285 +63515,625730 +666326,516041 +619226,518883 +443613,532186 +597717,520257 +96225,603069 +86940,608450 +40725,651929 +460976,530625 +268875,553508 +270671,553214 +363254,540500 +384248,538137 +762889,510892 +377941,538833 +278878,551890 +176615,572755 +860008,506412 +944392,502967 +608395,519571 +225283,561450 +45095,645728 +333798,544090 +625733,518476 +995584,501037 +506135,526853 +238050,558952 +557943,522972 +530978,524938 +634244,517949 +177168,572616 +85200,609541 +953043,502630 +523661,525484 +999295,500902 +840803,507246 +961490,502312 +471747,529685 +380705,538523 +911180,504275 +334149,544046 +478992,529065 +325789,545133 +335884,543826 +426976,533760 +749007,511582 +667067,516000 +607586,519623 +674054,515599 +188534,569675 +565185,522464 +172090,573988 +87592,608052 +907432,504424 +8912,760841 +928318,503590 +757917,511138 +718693,513153 +315141,546566 +728326,512645 +353492,541647 +638429,517695 +628892,518280 +877286,505672 +620895,518778 +385878,537959 +423311,534113 +633501,517997 +884833,505360 +883402,505416 +999665,500894 +708395,513697 +548142,523667 +756491,511205 +987352,501340 +766520,510705 +591775,520647 +833758,507563 +843890,507108 +925551,503698 +74816,616598 +646942,517187 +354923,541481 +256291,555638 +634470,517942 +930904,503494 +134221,586071 +282663,551304 +986070,501394 +123636,590176 +123678,590164 +481717,528841 +423076,534137 +866246,506145 +93313,604697 +783632,509880 +317066,546304 +502977,527103 +141272,583545 +71708,618938 +617748,518975 +581190,521362 +193824,568382 +682368,515131 +352956,541712 +351375,541905 +505362,526909 +905165,504518 +128645,588188 +267143,553787 +158409,577965 +482776,528754 +628896,518282 +485233,528547 +563606,522574 +111001,595655 +115920,593445 +365510,540237 +959724,502374 +938763,503184 +930044,503520 +970959,501956 +913658,504176 +68117,621790 +989729,501253 +567697,522288 +820427,508163 +54236,634794 +291557,549938 +124961,589646 +403177,536130 +405421,535899 +410233,535417 +815111,508403 +213176,563974 +83099,610879 +998588,500934 +513640,526263 +129817,587733 +1820,921851 +287584,550539 +299160,548820 +860621,506386 +529258,525059 +586297,521017 +953406,502616 +441234,532410 +986217,501386 +781938,509957 +461247,530595 +735424,512277 +146623,581722 +839838,507288 +510667,526494 +935085,503327 +737523,512167 +303455,548204 +992779,501145 +60240,628739 +939095,503174 +794368,509370 +501825,527189 +459028,530798 +884641,505363 +512287,526364 +835165,507499 +307723,547590 +160587,577304 +735043,512300 +493289,527887 +110717,595785 +306480,547772 +318593,546089 +179810,571911 +200531,566799 +314999,546580 +197020,567622 +301465,548487 +237808,559000 +131944,586923 +882527,505449 +468117,530003 +711319,513541 +156240,578628 +965452,502162 +992756,501148 +437959,532715 +739938,512046 +614249,519196 +391496,537356 +62746,626418 +688215,514806 +75501,616091 +883573,505412 +558824,522910 +759371,511061 +173913,573489 +891351,505089 +727464,512693 +164833,576051 +812317,508529 +540320,524243 +698061,514257 +69149,620952 +471673,529694 +159092,577753 +428134,533653 +89997,606608 +711061,513557 +779403,510081 +203327,566155 +798176,509187 +667688,515963 +636120,517833 +137410,584913 +217615,563034 +556887,523038 +667229,515991 +672276,515708 +325361,545187 +172115,573985 +13846,725685 \ No newline at end of file diff --git a/project_euler/problem_99/sol1.py b/project_euler/problem_99/sol1.py new file mode 100644 index 000000000000..0148a80ef481 --- /dev/null +++ b/project_euler/problem_99/sol1.py @@ -0,0 +1,34 @@ +""" +Problem: + +Comparing two numbers written in index form like 2'11 and 3'7 is not difficult, as any +calculator would confirm that 2^11 = 2048 < 3^7 = 2187. + +However, confirming that 632382^518061 > 519432^525806 would be much more difficult, as +both numbers contain over three million digits. + +Using base_exp.txt, a 22K text file containing one thousand lines with a base/exponent +pair on each line, determine which line number has the greatest numerical value. + +NOTE: The first two lines in the file represent the numbers in the example given above. +""" + +import os +from math import log10 + + +def find_largest(data_file: str = "base_exp.txt") -> int: + """ + >>> find_largest() + 709 + """ + largest = [0, 0] + for i, line in enumerate(open(os.path.join(os.path.dirname(__file__), data_file))): + a, x = list(map(int, line.split(","))) + if x * log10(a) > largest[0]: + largest = [x * log10(a), i + 1] + return largest[1] + + +if __name__ == "__main__": + print(find_largest()) diff --git a/quantum/README.md b/quantum/README.md new file mode 100644 index 000000000000..be5bd0843f4f --- /dev/null +++ b/quantum/README.md @@ -0,0 +1,8 @@ +# Welcome to Quantum Algorithms + +Started at https://github.com/TheAlgorithms/Python/issues/1831 + +* D-Wave: https://www.dwavesys.com and https://github.com/dwavesystems +* Google: https://research.google/teams/applied-science/quantum +* IBM: https://qiskit.org and https://github.com/Qiskit +* Rigetti: https://rigetti.com and https://github.com/rigetti diff --git a/requirements.txt b/requirements.txt index 91d3df33323d..9d6cc502bde1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,18 @@ +beautifulsoup4 black +fake_useragent flake8 matplotlib mypy -numpy +numpy>=1.17.4 opencv-python pandas +pillow pytest +pytest-cov +requests +scikit-fuzzy sklearn sympy -tensorflow +tensorflow; python_version < '3.8' +xgboost diff --git a/scheduling/first_come_first_served.py b/scheduling/first_come_first_served.py new file mode 100644 index 000000000000..f52c4243dec3 --- /dev/null +++ b/scheduling/first_come_first_served.py @@ -0,0 +1,107 @@ +# Implementation of First Come First Served scheduling algorithm +# In this Algorithm we just care about the order that the processes arrived +# without carring about their duration time +# https://en.wikipedia.org/wiki/Scheduling_(computing)#First_come,_first_served +from typing import List + + +def calculate_waiting_times(duration_times: List[int]) -> List[int]: + """ + This function calculates the waiting time of some processes that have a + specified duration time. + Return: The waiting time for each process. + >>> calculate_waiting_times([5, 10, 15]) + [0, 5, 15] + >>> calculate_waiting_times([1, 2, 3, 4, 5]) + [0, 1, 3, 6, 10] + >>> calculate_waiting_times([10, 3]) + [0, 10] + """ + waiting_times = [0] * len(duration_times) + for i in range(1, len(duration_times)): + waiting_times[i] = duration_times[i - 1] + waiting_times[i - 1] + return waiting_times + + +def calculate_turnaround_times( + duration_times: List[int], waiting_times: List[int] +) -> List[int]: + """ + This function calculates the turnaround time of some processes. + Return: The time difference between the completion time and the + arrival time. + Practically waiting_time + duration_time + >>> calculate_turnaround_times([5, 10, 15], [0, 5, 15]) + [5, 15, 30] + >>> calculate_turnaround_times([1, 2, 3, 4, 5], [0, 1, 3, 6, 10]) + [1, 3, 6, 10, 15] + >>> calculate_turnaround_times([10, 3], [0, 10]) + [10, 13] + """ + return [ + duration_time + waiting_times[i] + for i, duration_time in enumerate(duration_times) + ] + + +def calculate_average_turnaround_time(turnaround_times: List[int]) -> float: + """ + This function calculates the average of the turnaround times + Return: The average of the turnaround times. + >>> calculate_average_turnaround_time([0, 5, 16]) + 7.0 + >>> calculate_average_turnaround_time([1, 5, 8, 12]) + 6.5 + >>> calculate_average_turnaround_time([10, 24]) + 17.0 + """ + return sum(turnaround_times) / len(turnaround_times) + + +def calculate_average_waiting_time(waiting_times: List[int]) -> float: + """ + This function calculates the average of the waiting times + Return: The average of the waiting times. + >>> calculate_average_waiting_time([0, 5, 16]) + 7.0 + >>> calculate_average_waiting_time([1, 5, 8, 12]) + 6.5 + >>> calculate_average_waiting_time([10, 24]) + 17.0 + """ + return sum(waiting_times) / len(waiting_times) + + +if __name__ == "__main__": + # process id's + processes = [1, 2, 3] + + # ensure that we actually have processes + if len(processes) == 0: + print("Zero amount of processes") + exit() + + # duration time of all processes + duration_times = [19, 8, 9] + + # ensure we can match each id to a duration time + if len(duration_times) != len(processes): + print("Unable to match all id's with their duration time") + exit() + + # get the waiting times and the turnaround times + waiting_times = calculate_waiting_times(duration_times) + turnaround_times = calculate_turnaround_times(duration_times, waiting_times) + + # get the average times + average_waiting_time = calculate_average_waiting_time(waiting_times) + average_turnaround_time = calculate_average_turnaround_time(turnaround_times) + + # print all the results + print("Process ID\tDuration Time\tWaiting Time\tTurnaround Time") + for i, process in enumerate(processes): + print( + f"{process}\t\t{duration_times[i]}\t\t{waiting_times[i]}\t\t{turnaround_times[i]}" + ) + print(f"Average waiting time = {average_waiting_time}") + print(f"Average turn around time = {average_turnaround_time}") diff --git a/scripts/build_directory_md.py b/scripts/build_directory_md.py old mode 100644 new mode 100755 index 47192701880d..9e26ee81a323 --- a/scripts/build_directory_md.py +++ b/scripts/build_directory_md.py @@ -1,71 +1,45 @@ -""" -This is a simple script that will scan through the current directory -and generate the corresponding DIRECTORY.md file, can also specify -files or folders to be ignored. -""" +#!/usr/bin/env python3 + import os +from typing import Iterator + +URL_BASE = "https://github.com/TheAlgorithms/Python/blob/master" + +def good_file_paths(top_dir: str = ".") -> Iterator[str]: + for dir_path, dir_names, filenames in os.walk(top_dir): + dir_names[:] = [d for d in dir_names if d != "scripts" and d[0] not in "._"] + for filename in filenames: + if filename == "__init__.py": + continue + if os.path.splitext(filename)[1] in (".py", ".ipynb"): + yield os.path.join(dir_path, filename).lstrip("./") -# Target URL (master) -URL = "https://github.com/TheAlgorithms/Python/blob/master/" +def md_prefix(i): + return f"{i * ' '}*" if i else "\n##" -def tree(d, ignores, ignores_ext): - return _markdown(d, ignores, ignores_ext, 0) - -def _markdown(parent, ignores, ignores_ext, depth): - out = "" - dirs, files = [], [] - for i in os.listdir(parent): - full = os.path.join(parent, i) - name, ext = os.path.splitext(i) - if i not in ignores and ext not in ignores_ext: - if os.path.isfile(full): - # generate list - pre = parent.replace("./", "").replace(" ", "%20") - # replace all spaces to safe URL - child = i.replace(" ", "%20") - files.append((pre, child, name)) - else: - dirs.append(i) - # Sort files - files.sort(key=lambda e: e[2].lower()) - for f in files: - pre, child, name = f - out += " " * depth + "* [" + name.replace("_", " ") + "](" + URL + pre + "/" + child + ")\n" - # Sort directories - dirs.sort() - for i in dirs: - full = os.path.join(parent, i) - i = i.replace("_", " ").title() - if depth == 0: - out += "## " + i + "\n" - else: - out += " " * depth + "* " + i + "\n" - out += _markdown(full, ignores, ignores_ext, depth+1) - return out +def print_path(old_path: str, new_path: str) -> str: + old_parts = old_path.split(os.sep) + for i, new_part in enumerate(new_path.split(os.sep)): + if i + 1 > len(old_parts) or old_parts[i] != new_part: + if new_part: + print(f"{md_prefix(i)} {new_part.replace('_', ' ').title()}") + return new_path -# Specific files or folders with the given names will be ignored -ignores = [".vs", - ".gitignore", - ".git", - "scripts", - "__init__.py", - "requirements.txt", - ".github" -] -# Files with given entensions will be ignored -ignores_ext = [ - ".md", - ".ipynb", - ".png", - ".jpg", - ".yml" -] +def print_directory_md(top_dir: str = ".") -> None: + old_path = "" + for filepath in sorted(good_file_paths()): + filepath, filename = os.path.split(filepath) + if filepath != old_path: + old_path = print_path(old_path, filepath) + indent = (filepath.count(os.sep) + 1) if filepath else 0 + url = "/".join((URL_BASE, filepath, filename)).replace(" ", "%20") + filename = os.path.splitext(filename.replace("_", " ").title())[0] + print(f"{md_prefix(indent)} [{filename}]({url})") if __name__ == "__main__": - with open("DIRECTORY.md", "w+") as f: - f.write(tree(".", ignores, ignores_ext)) + print_directory_md(".") diff --git a/scripts/validate_filenames.py b/scripts/validate_filenames.py new file mode 100755 index 000000000000..f22fda4149f3 --- /dev/null +++ b/scripts/validate_filenames.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import os +from build_directory_md import good_file_paths + +filepaths = list(good_file_paths()) +assert filepaths, "good_file_paths() failed!" + + +upper_files = [file for file in filepaths if file != file.lower()] +if upper_files: + print(f"{len(upper_files)} files contain uppercase characters:") + print("\n".join(upper_files) + "\n") + +space_files = [file for file in filepaths if " " in file] +if space_files: + print(f"{len(space_files)} files contain space characters:") + print("\n".join(space_files) + "\n") + +nodir_files = [file for file in filepaths if os.sep not in file] +if nodir_files: + print(f"{len(nodir_files)} files are not in a directory:") + print("\n".join(nodir_files) + "\n") + +bad_files = len(upper_files + space_files + nodir_files) +if bad_files: + import sys + + sys.exit(bad_files) diff --git a/searches/binary_search.py b/searches/binary_search.py index e658dac2a3ef..0d9e730258d7 100644 --- a/searches/binary_search.py +++ b/searches/binary_search.py @@ -1,5 +1,5 @@ """ -This is pure python implementation of binary search algorithm +This is pure Python implementation of binary search algorithms For doctests run following command: python -m doctest -v binary_search.py @@ -9,13 +9,176 @@ For manual testing run: python binary_search.py """ -from __future__ import print_function import bisect -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 + +def bisect_left(sorted_collection, item, lo=0, hi=None): + """ + Locates the first element in a sorted array that is larger or equal to a given + value. + + It has the same interface as + https://docs.python.org/3/library/bisect.html#bisect.bisect_left . + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item to bisect + :param lo: lowest index to consider (as in sorted_collection[lo:hi]) + :param hi: past the highest index to consider (as in sorted_collection[lo:hi]) + :return: index i such that all values in sorted_collection[lo:i] are < item and all + values in sorted_collection[i:hi] are >= item. + + Examples: + >>> bisect_left([0, 5, 7, 10, 15], 0) + 0 + + >>> bisect_left([0, 5, 7, 10, 15], 6) + 2 + + >>> bisect_left([0, 5, 7, 10, 15], 20) + 5 + + >>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3) + 3 + + >>> bisect_left([0, 5, 7, 10, 15], 6, 2) + 2 + """ + if hi is None: + hi = len(sorted_collection) + + while lo < hi: + mid = (lo + hi) // 2 + if sorted_collection[mid] < item: + lo = mid + 1 + else: + hi = mid + + return lo + + +def bisect_right(sorted_collection, item, lo=0, hi=None): + """ + Locates the first element in a sorted array that is larger than a given value. + + It has the same interface as + https://docs.python.org/3/library/bisect.html#bisect.bisect_right . + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item to bisect + :param lo: lowest index to consider (as in sorted_collection[lo:hi]) + :param hi: past the highest index to consider (as in sorted_collection[lo:hi]) + :return: index i such that all values in sorted_collection[lo:i] are <= item and + all values in sorted_collection[i:hi] are > item. + + Examples: + >>> bisect_right([0, 5, 7, 10, 15], 0) + 1 + + >>> bisect_right([0, 5, 7, 10, 15], 15) + 5 + + >>> bisect_right([0, 5, 7, 10, 15], 6) + 2 + + >>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3) + 3 + + >>> bisect_right([0, 5, 7, 10, 15], 6, 2) + 2 + """ + if hi is None: + hi = len(sorted_collection) + + while lo < hi: + mid = (lo + hi) // 2 + if sorted_collection[mid] <= item: + lo = mid + 1 + else: + hi = mid + + return lo + + +def insort_left(sorted_collection, item, lo=0, hi=None): + """ + Inserts a given value into a sorted array before other values with the same value. + + It has the same interface as + https://docs.python.org/3/library/bisect.html#bisect.insort_left . + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item to insert + :param lo: lowest index to consider (as in sorted_collection[lo:hi]) + :param hi: past the highest index to consider (as in sorted_collection[lo:hi]) + + Examples: + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_left(sorted_collection, 6) + >>> sorted_collection + [0, 5, 6, 7, 10, 15] + + >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] + >>> item = (5, 5) + >>> insort_left(sorted_collection, item) + >>> sorted_collection + [(0, 0), (5, 5), (5, 5), (7, 7), (10, 10), (15, 15)] + >>> item is sorted_collection[1] + True + >>> item is sorted_collection[2] + False + + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_left(sorted_collection, 20) + >>> sorted_collection + [0, 5, 7, 10, 15, 20] + + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_left(sorted_collection, 15, 1, 3) + >>> sorted_collection + [0, 5, 7, 15, 10, 15] + """ + sorted_collection.insert(bisect_left(sorted_collection, item, lo, hi), item) + + +def insort_right(sorted_collection, item, lo=0, hi=None): + """ + Inserts a given value into a sorted array after other values with the same value. + + It has the same interface as + https://docs.python.org/3/library/bisect.html#bisect.insort_right . + + :param sorted_collection: some ascending sorted collection with comparable items + :param item: item to insert + :param lo: lowest index to consider (as in sorted_collection[lo:hi]) + :param hi: past the highest index to consider (as in sorted_collection[lo:hi]) + + Examples: + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_right(sorted_collection, 6) + >>> sorted_collection + [0, 5, 6, 7, 10, 15] + + >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] + >>> item = (5, 5) + >>> insort_right(sorted_collection, item) + >>> sorted_collection + [(0, 0), (5, 5), (5, 5), (7, 7), (10, 10), (15, 15)] + >>> item is sorted_collection[1] + False + >>> item is sorted_collection[2] + True + + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_right(sorted_collection, 20) + >>> sorted_collection + [0, 5, 7, 10, 15, 20] + + >>> sorted_collection = [0, 5, 7, 10, 15] + >>> insort_right(sorted_collection, 15, 1, 3) + >>> sorted_collection + [0, 5, 7, 15, 10, 15] + """ + sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item) def binary_search(sorted_collection, item): @@ -49,11 +212,10 @@ def binary_search(sorted_collection, item): current_item = sorted_collection[midpoint] if current_item == item: return midpoint + elif item < current_item: + right = midpoint - 1 else: - if item < current_item: - right = midpoint - 1 - else: - left = midpoint + 1 + left = midpoint + 1 return None @@ -85,6 +247,7 @@ def binary_search_std_lib(sorted_collection, item): return index return None + def binary_search_by_recursion(sorted_collection, item, left, right): """Pure implementation of binary search algorithm in Python by recursion @@ -110,18 +273,19 @@ def binary_search_by_recursion(sorted_collection, item, left, right): >>> binary_search_std_lib([0, 5, 7, 10, 15], 6) """ - if (right < left): + if right < left: return None - + midpoint = left + (right - left) // 2 if sorted_collection[midpoint] == item: return midpoint elif sorted_collection[midpoint] > item: - return binary_search_by_recursion(sorted_collection, item, left, midpoint-1) + return binary_search_by_recursion(sorted_collection, item, left, midpoint - 1) else: - return binary_search_by_recursion(sorted_collection, item, midpoint+1, right) - + return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right) + + def __assert_sorted(collection): """Check if collection is ascending sorted, if not - raises :py:class:`ValueError` @@ -139,23 +303,24 @@ def __assert_sorted(collection): ValueError: Collection must be ascending sorted """ if collection != sorted(collection): - raise ValueError('Collection must be ascending sorted') + raise ValueError("Collection must be ascending sorted") return True -if __name__ == '__main__': +if __name__ == "__main__": import sys - user_input = raw_input('Enter numbers separated by comma:\n').strip() - collection = [int(item) for item in user_input.split(',')] + + user_input = input("Enter numbers separated by comma:\n").strip() + collection = [int(item) for item in user_input.split(",")] try: __assert_sorted(collection) except ValueError: - sys.exit('Sequence must be ascending sorted to apply binary search') + sys.exit("Sequence must be ascending sorted to apply binary search") - target_input = raw_input('Enter a single number to be found in the list:\n') + target_input = input("Enter a single number to be found in the list:\n") target = int(target_input) result = binary_search(collection, target) if result is not None: - print('{} found at positions: {}'.format(target, result)) + print(f"{target} found at positions: {result}") else: - print('Not found') + print("Not found") diff --git a/searches/fibonacci_search.py b/searches/fibonacci_search.py new file mode 100644 index 000000000000..67f2df505d4e --- /dev/null +++ b/searches/fibonacci_search.py @@ -0,0 +1,54 @@ +# run using python fibonacci_search.py -v + +""" +@params +arr: input array +val: the value to be searched +output: the index of element in the array or -1 if not found +return 0 if input array is empty +""" + + +def fibonacci_search(arr, val): + + """ + >>> fibonacci_search([1,6,7,0,0,0], 6) + 1 + >>> fibonacci_search([1,-1, 5, 2, 9], 10) + -1 + >>> fibonacci_search([], 9) + 0 + """ + fib_N_2 = 0 + fib_N_1 = 1 + fibNext = fib_N_1 + fib_N_2 + length = len(arr) + if length == 0: + return 0 + while fibNext < len(arr): + fib_N_2 = fib_N_1 + fib_N_1 = fibNext + fibNext = fib_N_1 + fib_N_2 + index = -1 + while fibNext > 1: + i = min(index + fib_N_2, (length - 1)) + if arr[i] < val: + fibNext = fib_N_1 + fib_N_1 = fib_N_2 + fib_N_2 = fibNext - fib_N_1 + index = i + elif arr[i] > val: + fibNext = fib_N_2 + fib_N_1 = fib_N_1 - fib_N_2 + fib_N_2 = fibNext - fib_N_1 + else: + return i + if (fib_N_1 and index < length - 1) and (arr[index + 1] == val): + return index + 1 + return -1 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/searches/hill_climbing.py b/searches/hill_climbing.py new file mode 100644 index 000000000000..324097ef5a24 --- /dev/null +++ b/searches/hill_climbing.py @@ -0,0 +1,197 @@ +# https://en.wikipedia.org/wiki/Hill_climbing +import math + + +class SearchProblem: + """ + An interface to define search problems. + The interface will be illustrated using the example of mathematical function. + """ + + def __init__(self, x: int, y: int, step_size: int, function_to_optimize): + """ + The constructor of the search problem. + + x: the x coordinate of the current search state. + y: the y coordinate of the current search state. + step_size: size of the step to take when looking for neighbors. + function_to_optimize: a function to optimize having the signature f(x, y). + """ + self.x = x + self.y = y + self.step_size = step_size + self.function = function_to_optimize + + def score(self) -> int: + """ + Returns the output of the function called with current x and y coordinates. + >>> def test_function(x, y): + ... return x + y + >>> SearchProblem(0, 0, 1, test_function).score() # 0 + 0 = 0 + 0 + >>> SearchProblem(5, 7, 1, test_function).score() # 5 + 7 = 12 + 12 + """ + return self.function(self.x, self.y) + + def get_neighbors(self): + """ + Returns a list of coordinates of neighbors adjacent to the current coordinates. + + Neighbors: + | 0 | 1 | 2 | + | 3 | _ | 4 | + | 5 | 6 | 7 | + """ + step_size = self.step_size + return [ + SearchProblem(x, y, step_size, self.function) + for x, y in ( + (self.x - step_size, self.y - step_size), + (self.x - step_size, self.y), + (self.x - step_size, self.y + step_size), + (self.x, self.y - step_size), + (self.x, self.y + step_size), + (self.x + step_size, self.y - step_size), + (self.x + step_size, self.y), + (self.x + step_size, self.y + step_size), + ) + ] + + def __hash__(self): + """ + hash the string represetation of the current search state. + """ + return hash(str(self)) + + def __eq__(self, obj): + """ + Check if the 2 objects are equal. + """ + if isinstance(obj, SearchProblem): + return hash(str(self)) == hash(str(obj)) + return False + + def __str__(self): + """ + string representation of the current search state. + >>> str(SearchProblem(0, 0, 1, None)) + 'x: 0 y: 0' + >>> str(SearchProblem(2, 5, 1, None)) + 'x: 2 y: 5' + """ + return f"x: {self.x} y: {self.y}" + + +def hill_climbing( + search_prob, + find_max: bool = True, + max_x: float = math.inf, + min_x: float = -math.inf, + max_y: float = math.inf, + min_y: float = -math.inf, + visualization: bool = False, + max_iter: int = 10000, +) -> SearchProblem: + """ + Implementation of the hill climbling algorithm. + We start with a given state, find all its neighbors, + move towards the neighbor which provides the maximum (or minimum) change. + We keep doing this until we are at a state where we do not have any + neighbors which can improve the solution. + Args: + search_prob: The search state at the start. + find_max: If True, the algorithm should find the maximum else the minimum. + max_x, min_x, max_y, min_y: the maximum and minimum bounds of x and y. + visualization: If True, a matplotlib graph is displayed. + max_iter: number of times to run the iteration. + Returns a search state having the maximum (or minimum) score. + """ + current_state = search_prob + scores = [] # list to store the current score at each iteration + iterations = 0 + solution_found = False + visited = set() + while not solution_found and iterations < max_iter: + visited.add(current_state) + iterations += 1 + current_score = current_state.score() + scores.append(current_score) + neighbors = current_state.get_neighbors() + max_change = -math.inf + min_change = math.inf + next_state = None # to hold the next best neighbor + for neighbor in neighbors: + if neighbor in visited: + continue # do not want to visit the same state again + if ( + neighbor.x > max_x + or neighbor.x < min_x + or neighbor.y > max_y + or neighbor.y < min_y + ): + continue # neighbor outside our bounds + change = neighbor.score() - current_score + if find_max: # finding max + # going to direction with greatest ascent + if change > max_change and change > 0: + max_change = change + next_state = neighbor + else: # finding min + # to direction with greatest descent + if change < min_change and change < 0: + min_change = change + next_state = neighbor + if next_state is not None: + # we found at least one neighbor which improved the current state + current_state = next_state + else: + # since we have no neighbor that improves the solution we stop the search + solution_found = True + + if visualization: + import matplotlib.pyplot as plt + + plt.plot(range(iterations), scores) + plt.xlabel("Iterations") + plt.ylabel("Function values") + plt.show() + + return current_state + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + def test_f1(x, y): + return (x ** 2) + (y ** 2) + + # starting the problem with initial coordinates (3, 4) + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) + local_min = hill_climbing(prob, find_max=False) + print( + "The minimum score for f(x, y) = x^2 + y^2 found via hill climbing: " + f"{local_min.score()}" + ) + + # starting the problem with initial coordinates (12, 47) + prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1) + local_min = hill_climbing( + prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True + ) + print( + "The minimum score for f(x, y) = x^2 + y^2 with the domain 100 > x > 5 " + f"and 50 > y > - 5 found via hill climbing: {local_min.score()}" + ) + + def test_f2(x, y): + return (3 * x ** 2) - (6 * y) + + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) + local_min = hill_climbing(prob, find_max=True) + print( + "The maximum score for f(x, y) = x^2 + y^2 found via hill climbing: " + f"{local_min.score()}" + ) diff --git a/searches/interpolation_search.py b/searches/interpolation_search.py index 329596d340a5..f4fa8e1203df 100644 --- a/searches/interpolation_search.py +++ b/searches/interpolation_search.py @@ -1,12 +1,6 @@ """ -This is pure python implementation of interpolation search algorithm +This is pure Python implementation of interpolation search algorithm """ -from __future__ import print_function - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def interpolation_search(sorted_collection, item): @@ -21,36 +15,39 @@ def interpolation_search(sorted_collection, item): right = len(sorted_collection) - 1 while left <= right: - #avoid devided by 0 during interpolation - if sorted_collection[left]==sorted_collection[right]: - if sorted_collection[left]==item: + # avoid divided by 0 during interpolation + if sorted_collection[left] == sorted_collection[right]: + if sorted_collection[left] == item: return left else: return None - point = left + ((item - sorted_collection[left]) * (right - left)) // (sorted_collection[right] - sorted_collection[left]) - - #out of range check - if point<0 or point>=len(sorted_collection): + point = left + ((item - sorted_collection[left]) * (right - left)) // ( + sorted_collection[right] - sorted_collection[left] + ) + + # out of range check + if point < 0 or point >= len(sorted_collection): return None current_item = sorted_collection[point] if current_item == item: return point else: - if pointright: - left = right + elif point > right: + left = right right = point - else: + else: if item < current_item: right = point - 1 else: left = point + 1 return None + def interpolation_search_by_recursion(sorted_collection, item, left, right): """Pure implementation of interpolation search algorithm in Python by recursion @@ -62,31 +59,38 @@ def interpolation_search_by_recursion(sorted_collection, item, left, right): :return: index of found item or None if item is not found """ - #avoid devided by 0 during interpolation - if sorted_collection[left]==sorted_collection[right]: - if sorted_collection[left]==item: + # avoid divided by 0 during interpolation + if sorted_collection[left] == sorted_collection[right]: + if sorted_collection[left] == item: return left else: return None - point = left + ((item - sorted_collection[left]) * (right - left)) // (sorted_collection[right] - sorted_collection[left]) - - #out of range check - if point<0 or point>=len(sorted_collection): + point = left + ((item - sorted_collection[left]) * (right - left)) // ( + sorted_collection[right] - sorted_collection[left] + ) + + # out of range check + if point < 0 or point >= len(sorted_collection): return None if sorted_collection[point] == item: return point - elif pointright: + elif point > right: return interpolation_search_by_recursion(sorted_collection, item, right, left) else: if sorted_collection[point] > item: - return interpolation_search_by_recursion(sorted_collection, item, left, point-1) + return interpolation_search_by_recursion( + sorted_collection, item, left, point - 1 + ) else: - return interpolation_search_by_recursion(sorted_collection, item, point+1, right) - + return interpolation_search_by_recursion( + sorted_collection, item, point + 1, right + ) + + def __assert_sorted(collection): """Check if collection is ascending sorted, if not - raises :py:class:`ValueError` :param collection: collection @@ -101,36 +105,36 @@ def __assert_sorted(collection): ValueError: Collection must be ascending sorted """ if collection != sorted(collection): - raise ValueError('Collection must be ascending sorted') + raise ValueError("Collection must be ascending sorted") return True -if __name__ == '__main__': +if __name__ == "__main__": import sys - + """ - user_input = raw_input('Enter numbers separated by comma:\n').strip() + user_input = input('Enter numbers separated by comma:\n').strip() collection = [int(item) for item in user_input.split(',')] try: __assert_sorted(collection) except ValueError: sys.exit('Sequence must be ascending sorted to apply interpolation search') - target_input = raw_input('Enter a single number to be found in the list:\n') + target_input = input('Enter a single number to be found in the list:\n') target = int(target_input) - """ + """ debug = 0 if debug == 1: - collection = [10,30,40,45,50,66,77,93] + collection = [10, 30, 40, 45, 50, 66, 77, 93] try: __assert_sorted(collection) except ValueError: - sys.exit('Sequence must be ascending sorted to apply interpolation search') + sys.exit("Sequence must be ascending sorted to apply interpolation search") target = 67 - + result = interpolation_search(collection, target) if result is not None: - print('{} found at positions: {}'.format(target, result)) + print(f"{target} found at positions: {result}") else: - print('Not found') + print("Not found") diff --git a/searches/jump_search.py b/searches/jump_search.py index 10cb933f2f35..5ba80e9d35be 100644 --- a/searches/jump_search.py +++ b/searches/jump_search.py @@ -1,10 +1,11 @@ -from __future__ import print_function import math + + def jump_search(arr, x): n = len(arr) step = int(math.floor(math.sqrt(n))) prev = 0 - while arr[min(step, n)-1] < x: + while arr[min(step, n) - 1] < x: prev = step step += int(math.floor(math.sqrt(n))) if prev >= n: @@ -19,8 +20,7 @@ def jump_search(arr, x): return -1 - -arr = [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] -x = 55 -index = jump_search(arr, x) -print("\nNumber " + str(x) +" is at index " + str(index)); +if __name__ == "__main__": + arr = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] + x = 55 + print(f"Number {x} is at index {jump_search(arr, x)}") diff --git a/searches/linear_search.py b/searches/linear_search.py index 058322f21d09..adf22056b575 100644 --- a/searches/linear_search.py +++ b/searches/linear_search.py @@ -1,5 +1,5 @@ """ -This is pure python implementation of linear search algorithm +This is pure Python implementation of linear search algorithm For doctests run following command: python -m doctest -v linear_search.py @@ -9,12 +9,7 @@ For manual testing run: python linear_search.py """ -from __future__ import print_function -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 def linear_search(sequence, target): """Pure implementation of linear search algorithm in Python @@ -42,14 +37,14 @@ def linear_search(sequence, target): return None -if __name__ == '__main__': - user_input = raw_input('Enter numbers separated by comma:\n').strip() - sequence = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by comma:\n").strip() + sequence = [int(item) for item in user_input.split(",")] - target_input = raw_input('Enter a single number to be found in the list:\n') + target_input = input("Enter a single number to be found in the list:\n") target = int(target_input) result = linear_search(sequence, target) if result is not None: - print('{} found at positions: {}'.format(target, result)) + print(f"{target} found at position : {result}") else: - print('Not found') + print("Not found") diff --git a/searches/quick_select.py b/searches/quick_select.py index 76d09cb97f97..17dca395f73c 100644 --- a/searches/quick_select.py +++ b/searches/quick_select.py @@ -1,10 +1,13 @@ -import random - """ -A python implementation of the quick select algorithm, which is efficient for calculating the value that would appear in the index of a list if it would be sorted, even if it is not already sorted +A Python implementation of the quick select algorithm, which is efficient for +calculating the value that would appear in the index of a list if it would be +sorted, even if it is not already sorted https://en.wikipedia.org/wiki/Quickselect """ -def _partition(data, pivot): +import random + + +def _partition(data: list, pivot) -> tuple: """ Three way partition the data into smaller, equal and greater lists, in relationship to the pivot @@ -21,29 +24,39 @@ def _partition(data, pivot): else: equal.append(element) return less, equal, greater - -def quickSelect(list, k): - #k = len(list) // 2 when trying to find the median (index that value would be when list is sorted) - - #invalid input - if k>=len(list) or k<0: + + +def quick_select(items: list, index: int): + """ + >>> quick_select([2, 4, 5, 7, 899, 54, 32], 5) + 54 + >>> quick_select([2, 4, 5, 7, 899, 54, 32], 1) + 4 + >>> quick_select([5, 4, 3, 2], 2) + 4 + >>> quick_select([3, 5, 7, 10, 2, 12], 3) + 7 + """ + # index = len(items) // 2 when trying to find the median + # (value of index when items is sorted) + + # invalid input + if index >= len(items) or index < 0: return None - - smaller = [] - larger = [] - pivot = random.randint(0, len(list) - 1) - pivot = list[pivot] + + pivot = random.randint(0, len(items) - 1) + pivot = items[pivot] count = 0 - smaller, equal, larger =_partition(list, pivot) + smaller, equal, larger = _partition(items, pivot) count = len(equal) m = len(smaller) - #k is the pivot - if m <= k < m + count: + # index is the pivot + if m <= index < m + count: return pivot # must be in smaller - elif m > k: - return quickSelect(smaller, k) - #must be in larger + elif m > index: + return quick_select(smaller, index) + # must be in larger else: - return quickSelect(larger, k - (m + count)) \ No newline at end of file + return quick_select(larger, index - (m + count)) diff --git a/searches/sentinel_linear_search.py b/searches/sentinel_linear_search.py index 336cc5ab3b74..69c1cf9f351a 100644 --- a/searches/sentinel_linear_search.py +++ b/searches/sentinel_linear_search.py @@ -1,5 +1,5 @@ """ -This is pure python implementation of sentinel linear search algorithm +This is pure Python implementation of sentinel linear search algorithm For doctests run following command: python -m doctest -v sentinel_linear_search.py @@ -10,6 +10,7 @@ python sentinel_linear_search.py """ + def sentinel_linear_search(sequence, target): """Pure implementation of sentinel linear search algorithm in Python @@ -44,19 +45,14 @@ def sentinel_linear_search(sequence, target): return index -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by comma:\n').strip() - sequence = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by comma:\n").strip() + sequence = [int(item) for item in user_input.split(",")] - target_input = raw_input('Enter a single number to be found in the list:\n') + target_input = input("Enter a single number to be found in the list:\n") target = int(target_input) result = sentinel_linear_search(sequence, target) if result is not None: - print('{} found at positions: {}'.format(target, result)) + print(f"{target} found at positions: {result}") else: - print('Not found') \ No newline at end of file + print("Not found") diff --git a/searches/simple-binary-search.py b/searches/simple-binary-search.py new file mode 100644 index 000000000000..80e43ea346b2 --- /dev/null +++ b/searches/simple-binary-search.py @@ -0,0 +1,26 @@ +# A binary search implementation to test if a number is in a list of elements + + +def binary_search(a_list, item): + """ + >>> test_list = [0, 1, 2, 8, 13, 17, 19, 32, 42] + >>> print(binary_search(test_list, 3)) + False + >>> print(binary_search(test_list, 13)) + True + """ + if len(a_list) == 0: + return False + midpoint = len(a_list) // 2 + if a_list[midpoint] == item: + return True + if item < a_list[midpoint]: + return binary_search(a_list[:midpoint], item) + else: + return binary_search(a_list[midpoint + 1 :], item) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/searches/simulated_annealing.py b/searches/simulated_annealing.py new file mode 100644 index 000000000000..c24adc1ddb41 --- /dev/null +++ b/searches/simulated_annealing.py @@ -0,0 +1,132 @@ +# https://en.wikipedia.org/wiki/Simulated_annealing +import math, random +from hill_climbing import SearchProblem + + +def simulated_annealing( + search_prob, + find_max: bool = True, + max_x: float = math.inf, + min_x: float = -math.inf, + max_y: float = math.inf, + min_y: float = -math.inf, + visualization: bool = False, + start_temperate: float = 100, + rate_of_decrease: float = 0.01, + threshold_temp: float = 1, +) -> SearchProblem: + """ + implementation of the simulated annealing algorithm. We start with a given state, find + all its neighbors. Pick a random neighbor, if that neighbor improves the solution, we move + in that direction, if that neighbor does not improve the solution, we generate a random + real number between 0 and 1, if the number is within a certain range (calculated using + temperature) we move in that direction, else we pick another neighbor randomly and repeat the process. + Args: + search_prob: The search state at the start. + find_max: If True, the algorithm should find the minimum else the minimum. + max_x, min_x, max_y, min_y: the maximum and minimum bounds of x and y. + visualization: If True, a matplotlib graph is displayed. + start_temperate: the initial temperate of the system when the program starts. + rate_of_decrease: the rate at which the temperate decreases in each iteration. + threshold_temp: the threshold temperature below which we end the search + Returns a search state having the maximum (or minimum) score. + """ + search_end = False + current_state = search_prob + current_temp = start_temperate + scores = [] + iterations = 0 + best_state = None + + while not search_end: + current_score = current_state.score() + if best_state is None or current_score > best_state.score(): + best_state = current_state + scores.append(current_score) + iterations += 1 + next_state = None + neighbors = current_state.get_neighbors() + while ( + next_state is None and neighbors + ): # till we do not find a neighbor that we can move to + index = random.randint(0, len(neighbors) - 1) # picking a random neighbor + picked_neighbor = neighbors.pop(index) + change = picked_neighbor.score() - current_score + + if ( + picked_neighbor.x > max_x + or picked_neighbor.x < min_x + or picked_neighbor.y > max_y + or picked_neighbor.y < min_y + ): + continue # neighbor outside our bounds + + if not find_max: + change = change * -1 # in case we are finding minimum + if change > 0: # improves the solution + next_state = picked_neighbor + else: + probability = (math.e) ** ( + change / current_temp + ) # probability generation function + if random.random() < probability: # random number within probability + next_state = picked_neighbor + current_temp = current_temp - (current_temp * rate_of_decrease) + + if current_temp < threshold_temp or next_state is None: + # temperature below threshold, or could not find a suitable neighbor + search_end = True + else: + current_state = next_state + + if visualization: + import matplotlib.pyplot as plt + + plt.plot(range(iterations), scores) + plt.xlabel("Iterations") + plt.ylabel("Function values") + plt.show() + return best_state + + +if __name__ == "__main__": + + def test_f1(x, y): + return (x ** 2) + (y ** 2) + + # starting the problem with initial coordinates (12, 47) + prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1) + local_min = simulated_annealing( + prob, find_max=False, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True + ) + print( + "The minimum score for f(x, y) = x^2 + y^2 with the domain 100 > x > 5 " + f"and 50 > y > - 5 found via hill climbing: {local_min.score()}" + ) + + # starting the problem with initial coordinates (12, 47) + prob = SearchProblem(x=12, y=47, step_size=1, function_to_optimize=test_f1) + local_min = simulated_annealing( + prob, find_max=True, max_x=100, min_x=5, max_y=50, min_y=-5, visualization=True + ) + print( + "The maximum score for f(x, y) = x^2 + y^2 with the domain 100 > x > 5 " + f"and 50 > y > - 5 found via hill climbing: {local_min.score()}" + ) + + def test_f2(x, y): + return (3 * x ** 2) - (6 * y) + + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) + local_min = simulated_annealing(prob, find_max=False, visualization=True) + print( + "The minimum score for f(x, y) = 3*x^2 - 6*y found via hill climbing: " + f"{local_min.score()}" + ) + + prob = SearchProblem(x=3, y=4, step_size=1, function_to_optimize=test_f1) + local_min = simulated_annealing(prob, find_max=True, visualization=True) + print( + "The maximum score for f(x, y) = 3*x^2 - 6*y found via hill climbing: " + f"{local_min.score()}" + ) diff --git a/searches/tabu_search.py b/searches/tabu_search.py index ffd84f8ac031..9ddc5e8dee7f 100644 --- a/searches/tabu_search.py +++ b/searches/tabu_search.py @@ -1,5 +1,5 @@ """ -This is pure python implementation of Tabu search algorithm for a Travelling Salesman Problem, that the distances +This is pure Python implementation of Tabu search algorithm for a Travelling Salesman Problem, that the distances between the cities are symmetric (the distance between city 'a' and city 'b' is the same between city 'b' and city 'a'). The TSP can be represented into a graph. The cities are represented by nodes and the distance between them is represented by the weight of the ark between the nodes. @@ -25,7 +25,6 @@ import copy import argparse -import sys def generate_neighbours(path): @@ -55,13 +54,17 @@ def generate_neighbours(path): _list.append([line.split()[1], line.split()[2]]) dict_of_neighbours[line.split()[0]] = _list else: - dict_of_neighbours[line.split()[0]].append([line.split()[1], line.split()[2]]) + dict_of_neighbours[line.split()[0]].append( + [line.split()[1], line.split()[2]] + ) if line.split()[1] not in dict_of_neighbours: _list = list() _list.append([line.split()[0], line.split()[2]]) dict_of_neighbours[line.split()[1]] = _list else: - dict_of_neighbours[line.split()[1]].append([line.split()[0], line.split()[2]]) + dict_of_neighbours[line.split()[1]].append( + [line.split()[0], line.split()[2]] + ) return dict_of_neighbours @@ -111,8 +114,11 @@ def generate_first_solution(path, dict_of_neighbours): break position += 1 - distance_of_first_solution = distance_of_first_solution + int( - dict_of_neighbours[first_solution[-2]][position][1]) - 10000 + distance_of_first_solution = ( + distance_of_first_solution + + int(dict_of_neighbours[first_solution[-2]][position][1]) + - 10000 + ) return first_solution, distance_of_first_solution @@ -167,7 +173,9 @@ def find_neighborhood(solution, dict_of_neighbours): return neighborhood_of_solution -def tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, iters, size): +def tabu_search( + first_solution, distance_of_first_solution, dict_of_neighbours, iters, size +): """ Pure implementation of Tabu search algorithm for a Travelling Salesman Problem in Python. @@ -179,7 +187,7 @@ def tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, and the cost (distance) for each neighbor. :param iters: The number of iterations that Tabu search will execute. :param size: The size of Tabu List. - :return best_solution_ever: The solution with the lowest distance that occured during the execution of Tabu search. + :return best_solution_ever: The solution with the lowest distance that occurred during the execution of Tabu search. :return best_cost: The total distance that Travelling Salesman will travel, if he follows the path in best_solution ever. @@ -207,8 +215,10 @@ def tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, break i = i + 1 - if [first_exchange_node, second_exchange_node] not in tabu_list and [second_exchange_node, - first_exchange_node] not in tabu_list: + if [first_exchange_node, second_exchange_node] not in tabu_list and [ + second_exchange_node, + first_exchange_node, + ] not in tabu_list: tabu_list.append([first_exchange_node, second_exchange_node]) found = True solution = best_solution[:-1] @@ -231,22 +241,40 @@ def tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, def main(args=None): dict_of_neighbours = generate_neighbours(args.File) - first_solution, distance_of_first_solution = generate_first_solution(args.File, dict_of_neighbours) + first_solution, distance_of_first_solution = generate_first_solution( + args.File, dict_of_neighbours + ) - best_sol, best_cost = tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, args.Iterations, - args.Size) + best_sol, best_cost = tabu_search( + first_solution, + distance_of_first_solution, + dict_of_neighbours, + args.Iterations, + args.Size, + ) - print("Best solution: {0}, with total distance: {1}.".format(best_sol, best_cost)) + print(f"Best solution: {best_sol}, with total distance: {best_cost}.") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Tabu Search") parser.add_argument( - "-f", "--File", type=str, help="Path to the file containing the data", required=True) + "-f", + "--File", + type=str, + help="Path to the file containing the data", + required=True, + ) parser.add_argument( - "-i", "--Iterations", type=int, help="How many iterations the algorithm should perform", required=True) + "-i", + "--Iterations", + type=int, + help="How many iterations the algorithm should perform", + required=True, + ) parser.add_argument( - "-s", "--Size", type=int, help="Size of the tabu list", required=True) + "-s", "--Size", type=int, help="Size of the tabu list", required=True + ) # Pass the arguments to main method - sys.exit(main(parser.parse_args())) + main(parser.parse_args()) diff --git a/searches/ternary_search.py b/searches/ternary_search.py index c610f9b3c6da..5ecc47644248 100644 --- a/searches/ternary_search.py +++ b/searches/ternary_search.py @@ -1,107 +1,103 @@ -''' +""" This is a type of divide and conquer algorithm which divides the search space into -3 parts and finds the target value based on the property of the array or list +3 parts and finds the target value based on the property of the array or list (usually monotonic property). Time Complexity : O(log3 N) Space Complexity : O(1) -''' -from __future__ import print_function - +""" import sys -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 - # This is the precision for this function which can be altered. # It is recommended for users to keep this number greater than or equal to 10. precision = 10 # This is the linear search that will occur after the search space has become smaller. def lin_search(left, right, A, target): - for i in range(left, right+1): - if(A[i] == target): + for i in range(left, right + 1): + if A[i] == target: return i + # This is the iterative method of the ternary search algorithm. def ite_ternary_search(A, target): left = 0 - right = len(A) - 1; - while(True): - if(left a[j]) agrees with the direction, -# then a[i] and a[j] are interchanged.*/ +# The parameter dir indicates the sorting direction, ASCENDING +# or DESCENDING; if (a[i] > a[j]) agrees with the direction, +# then a[i] and a[j] are interchanged.*/ def compAndSwap(a, i, j, dire): if (dire == 1 and a[i] > a[j]) or (dire == 0 and a[i] < a[j]): a[i], a[j] = a[j], a[i] @@ -12,38 +12,38 @@ def compAndSwap(a, i, j, dire): # if dir = 1, and in descending order otherwise (means dir=0). -# The sequence to be sorted starts at index position low, -# the parameter cnt is the number of elements to be sorted. -def bitonicMerge(a, low, cnt, dire): +# The sequence to be sorted starts at index position low, +# the parameter cnt is the number of elements to be sorted. +def bitonic_merge(a, low, cnt, dire): if cnt > 1: k = int(cnt / 2) for i in range(low, low + k): compAndSwap(a, i, i + k, dire) - bitonicMerge(a, low, k, dire) - bitonicMerge(a, low + k, k, dire) + bitonic_merge(a, low, k, dire) + bitonic_merge(a, low + k, k, dire) - # This funcion first produces a bitonic sequence by recursively + # This function first produces a bitonic sequence by recursively # sorting its two halves in opposite sorting orders, and then -# calls bitonicMerge to make them in the same order -def bitonicSort(a, low, cnt, dire): +# calls bitonic_merge to make them in the same order +def bitonic_sort(a, low, cnt, dire): if cnt > 1: k = int(cnt / 2) - bitonicSort(a, low, k, 1) - bitonicSort(a, low + k, k, 0) - bitonicMerge(a, low, cnt, dire) + bitonic_sort(a, low, k, 1) + bitonic_sort(a, low + k, k, 0) + bitonic_merge(a, low, cnt, dire) - # Caller of bitonicSort for sorting the entire array of length N + # Caller of bitonic_sort for sorting the entire array of length N # in ASCENDING order def sort(a, N, up): - bitonicSort(a, 0, N, up) + bitonic_sort(a, 0, N, up) if __name__ == "__main__": - # Driver code to test above + a = [] n = int(input().strip()) diff --git a/sorts/bogo_sort.py b/sorts/bogo_sort.py index 056e8e68a92e..b72f2089f3d2 100644 --- a/sorts/bogo_sort.py +++ b/sorts/bogo_sort.py @@ -1,5 +1,10 @@ """ -This is a pure python implementation of the bogosort algorithm +This is a pure Python implementation of the bogosort algorithm, +also known as permutation sort, stupid sort, slowsort, shotgun sort, or monkey sort. +Bogosort generates random permutations until it guesses the correct one. + +More info on: https://en.wikipedia.org/wiki/Bogosort + For doctests run following command: python -m doctest -v bogo_sort.py or @@ -8,7 +13,6 @@ python bogo_sort.py """ -from __future__ import print_function import random @@ -26,7 +30,7 @@ def bogo_sort(collection): [-45, -5, -2] """ - def isSorted(collection): + def is_sorted(collection): if len(collection) < 2: return True for i in range(len(collection) - 1): @@ -34,16 +38,12 @@ def isSorted(collection): return False return True - while not isSorted(collection): + while not is_sorted(collection): random.shuffle(collection) return collection -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(bogo_sort(unsorted)) diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 4e2c19b65e02..eb356bc7dcad 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -1,6 +1,3 @@ -from __future__ import print_function - - def bubble_sort(collection): """Pure implementation of bubble sort algorithm in Python @@ -9,34 +6,38 @@ def bubble_sort(collection): :return: the same collection ordered by ascending Examples: - >>> bubble_sort([0, 5, 3, 2, 2]) + >>> bubble_sort([0, 5, 2, 3, 2]) [0, 2, 2, 3, 5] >>> bubble_sort([]) [] - >>> bubble_sort([-2, -5, -45]) + >>> bubble_sort([-2, -45, -5]) [-45, -5, -2] - - >>> bubble_sort([-23,0,6,-4,34]) + + >>> bubble_sort([-23, 0, 6, -4, 34]) [-23, -4, 0, 6, 34] + + >>> bubble_sort([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34]) + True """ length = len(collection) - for i in range(length-1): + for i in range(length - 1): swapped = False - for j in range(length-1-i): - if collection[j] > collection[j+1]: + for j in range(length - 1 - i): + if collection[j] > collection[j + 1]: swapped = True - collection[j], collection[j+1] = collection[j+1], collection[j] - if not swapped: break # Stop iteration if the collection is sorted. + collection[j], collection[j + 1] = collection[j + 1], collection[j] + if not swapped: + break # Stop iteration if the collection is sorted. return collection -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - user_input = raw_input('Enter numbers separated by a comma:').strip() - unsorted = [int(item) for item in user_input.split(',')] - print(*bubble_sort(unsorted), sep=',') +if __name__ == "__main__": + import time + + user_input = input("Enter numbers separated by a comma:").strip() + unsorted = [int(item) for item in user_input.split(",")] + start = time.process_time() + print(*bubble_sort(unsorted), sep=",") + print(f"Processing time: {time.process_time() - start}") diff --git a/sorts/bucket_sort.py b/sorts/bucket_sort.py index 0678b1194657..217ee5893c4b 100644 --- a/sorts/bucket_sort.py +++ b/sorts/bucket_sort.py @@ -17,8 +17,8 @@ # number of buckets. # Time Complexity of Solution: -# Worst case scenario occurs when all the elements are placed in a single bucket. The overall performance -# would then be dominated by the algorithm used to sort each bucket. In this case, O(n log n), because of TimSort +# Worst case scenario occurs when all the elements are placed in a single bucket. The overall performance +# would then be dominated by the algorithm used to sort each bucket. In this case, O(n log n), because of TimSort # # Average Case O(n + (n^2)/k + k), where k is the number of buckets # @@ -32,18 +32,18 @@ def bucket_sort(my_list, bucket_size=DEFAULT_BUCKET_SIZE): raise Exception("Please add some elements in the array.") min_value, max_value = (min(my_list), max(my_list)) - bucket_count = ((max_value - min_value) // bucket_size + 1) + bucket_count = (max_value - min_value) // bucket_size + 1 buckets = [[] for _ in range(int(bucket_count))] for i in range(len(my_list)): - buckets[int((my_list[i] - min_value) // bucket_size) - ].append(my_list[i]) + buckets[int((my_list[i] - min_value) // bucket_size)].append(my_list[i]) - return sorted([buckets[i][j] for i in range(len(buckets)) - for j in range(len(buckets[i]))]) + return sorted( + [buckets[i][j] for i in range(len(buckets)) for j in range(len(buckets[i]))] + ) if __name__ == "__main__": - user_input = input('Enter numbers separated by a comma:').strip() - unsorted = [float(n) for n in user_input.split(',') if len(user_input) > 0] + user_input = input("Enter numbers separated by a comma:").strip() + unsorted = [float(n) for n in user_input.split(",") if len(user_input) > 0] print(bucket_sort(unsorted)) diff --git a/sorts/cocktail_shaker_sort.py b/sorts/cocktail_shaker_sort.py index 8ad3383bbe9f..ab624421a3d6 100644 --- a/sorts/cocktail_shaker_sort.py +++ b/sorts/cocktail_shaker_sort.py @@ -1,32 +1,26 @@ -from __future__ import print_function - def cocktail_shaker_sort(unsorted): """ Pure implementation of the cocktail shaker sort algorithm in Python. """ - for i in range(len(unsorted)-1, 0, -1): + for i in range(len(unsorted) - 1, 0, -1): swapped = False - + for j in range(i, 0, -1): - if unsorted[j] < unsorted[j-1]: - unsorted[j], unsorted[j-1] = unsorted[j-1], unsorted[j] + if unsorted[j] < unsorted[j - 1]: + unsorted[j], unsorted[j - 1] = unsorted[j - 1], unsorted[j] swapped = True for j in range(i): - if unsorted[j] > unsorted[j+1]: - unsorted[j], unsorted[j+1] = unsorted[j+1], unsorted[j] + if unsorted[j] > unsorted[j + 1]: + unsorted[j], unsorted[j + 1] = unsorted[j + 1], unsorted[j] swapped = True - + if not swapped: return unsorted - -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] + + +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] cocktail_shaker_sort(unsorted) print(unsorted) diff --git a/sorts/comb_sort.py b/sorts/comb_sort.py index 22b6f66f04cc..416fd4552697 100644 --- a/sorts/comb_sort.py +++ b/sorts/comb_sort.py @@ -1,8 +1,13 @@ """ +This is pure Python implementation of comb sort algorithm. Comb sort is a relatively simple sorting algorithm originally designed by Wlodzimierz Dobosiewicz in 1980. -Later it was rediscovered by Stephen Lacey and Richard Box in 1991. Comb sort improves on bubble sort. +It was rediscovered by Stephen Lacey and Richard Box in 1991. Comb sort improves on bubble sort algorithm. +In bubble sort, distance (or gap) between two compared elements is always one. +Comb sort improvement is that gap can be much more than 1, in order to prevent slowing down by small values +at the end of a list. + +More info on: https://en.wikipedia.org/wiki/Comb_sort -This is pure python implementation of comb sort algorithm For doctests run following command: python -m doctest -v comb_sort.py or @@ -12,47 +17,46 @@ python comb_sort.py """ -def comb_sort(data): + +def comb_sort(data: list) -> list: """Pure implementation of comb sort algorithm in Python - :param collection: some mutable ordered collection with heterogeneous - comparable items inside - :return: the same collection ordered by ascending + :param data: mutable collection with comparable items + :return: the same collection in ascending order Examples: >>> comb_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] >>> comb_sort([]) [] - >>> comb_sort([-2, -5, -45]) - [-45, -5, -2] + >>> comb_sort([99, 45, -7, 8, 2, 0, -15, 3]) + [-15, -7, 0, 2, 3, 8, 45, 99] """ shrink_factor = 1.3 gap = len(data) - swapped = True - i = 0 + completed = False - while gap > 1 or swapped: - # Update the gap value for a next comb - gap = int(float(gap) / shrink_factor) + while not completed: - swapped = False - i = 0 + # Update the gap value for a next comb + gap = int(gap / shrink_factor) + if gap <= 1: + completed = True - while gap + i < len(data): - if data[i] > data[i+gap]: + index = 0 + while index + gap < len(data): + if data[index] > data[index + gap]: # Swap values - data[i], data[i+gap] = data[i+gap], data[i] - swapped = True - i += 1 + data[index], data[index + gap] = data[index + gap], data[index] + completed = False + index += 1 return data -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 +if __name__ == "__main__": + import doctest + + doctest.testmod() - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(comb_sort(unsorted)) diff --git a/sorts/counting_sort.py b/sorts/counting_sort.py index ad98f1a0da4c..892ec5d5f344 100644 --- a/sorts/counting_sort.py +++ b/sorts/counting_sort.py @@ -1,5 +1,5 @@ """ -This is pure python implementation of counting sort algorithm +This is pure Python implementation of counting sort algorithm For doctests run following command: python -m doctest -v counting_sort.py or @@ -8,8 +8,6 @@ python counting_sort.py """ -from __future__ import print_function - def counting_sort(collection): """Pure implementation of counting sort algorithm in Python @@ -44,7 +42,7 @@ def counting_sort(collection): # sum each position with it's predecessors. now, counting_arr[i] tells # us how many elements <= i has in the collection for i in range(1, counting_arr_length): - counting_arr[i] = counting_arr[i] + counting_arr[i-1] + counting_arr[i] = counting_arr[i] + counting_arr[i - 1] # create the output collection ordered = [0] * coll_len @@ -52,24 +50,24 @@ def counting_sort(collection): # place the elements in the output, respecting the original order (stable # sort) from end to begin, updating counting_arr for i in reversed(range(0, coll_len)): - ordered[counting_arr[collection[i] - coll_min]-1] = collection[i] + ordered[counting_arr[collection[i] - coll_min] - 1] = collection[i] counting_arr[collection[i] - coll_min] -= 1 return ordered + def counting_sort_string(string): - return ''.join([chr(i) for i in counting_sort([ord(c) for c in string])]) + """ + >>> counting_sort_string("thisisthestring") + 'eghhiiinrsssttt' + """ + return "".join([chr(i) for i in counting_sort([ord(c) for c in string])]) -if __name__ == '__main__': +if __name__ == "__main__": # Test string sort assert "eghhiiinrsssttt" == counting_sort_string("thisisthestring") - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(counting_sort(unsorted)) diff --git a/sorts/cycle_sort.py b/sorts/cycle_sort.py index 492022164427..4ce6a2a0e757 100644 --- a/sorts/cycle_sort.py +++ b/sorts/cycle_sort.py @@ -1,7 +1,4 @@ # Code contributed by Honey Sharma -from __future__ import print_function - - def cycle_sort(array): ans = 0 @@ -44,17 +41,12 @@ def cycle_sort(array): # Main Code starts here -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] n = len(unsorted) cycle_sort(unsorted) print("After sort : ") for i in range(0, n): - print(unsorted[i], end=' ') + print(unsorted[i], end=" ") diff --git a/sorts/double_sort.py b/sorts/double_sort.py new file mode 100644 index 000000000000..04e18682017c --- /dev/null +++ b/sorts/double_sort.py @@ -0,0 +1,42 @@ +def double_sort(lst): + """this sorting algorithm sorts an array using the principle of bubble sort, + but does it both from left to right and right to left, + hence i decided to call it "double sort" + :param collection: mutable ordered sequence of elements + :return: the same collection in ascending order + Examples: + >>> double_sort([-1 ,-2 ,-3 ,-4 ,-5 ,-6 ,-7]) + [-7, -6, -5, -4, -3, -2, -1] + >>> double_sort([]) + [] + >>> double_sort([-1 ,-2 ,-3 ,-4 ,-5 ,-6]) + [-6, -5, -4, -3, -2, -1] + >>> double_sort([-3, 10, 16, -42, 29]) == sorted([-3, 10, 16, -42, 29]) + True + """ + no_of_elements = len(lst) + for i in range( + 0, int(((no_of_elements - 1) / 2) + 1) + ): # we don't need to traverse to end of list as + for j in range(0, no_of_elements - 1): + if ( + lst[j + 1] < lst[j] + ): # applying bubble sort algorithm from left to right (or forwards) + temp = lst[j + 1] + lst[j + 1] = lst[j] + lst[j] = temp + if ( + lst[no_of_elements - 1 - j] < lst[no_of_elements - 2 - j] + ): # applying bubble sort algorithm from right to left (or backwards) + temp = lst[no_of_elements - 1 - j] + lst[no_of_elements - 1 - j] = lst[no_of_elements - 2 - j] + lst[no_of_elements - 2 - j] = temp + return lst + + +if __name__ == "__main__": + print("enter the list to be sorted") + lst = [int(x) for x in input().split()] # inputing elements of the list in one line + sorted_lst = double_sort(lst) + print("the sorted list is") + print(sorted_lst) diff --git a/sorts/external_sort.py b/sorts/external_sort.py index 1638e9efafee..e5b2c045fa95 100644 --- a/sorts/external_sort.py +++ b/sorts/external_sort.py @@ -6,8 +6,9 @@ import os import argparse -class FileSplitter(object): - BLOCK_FILENAME_FORMAT = 'block_{0}.dat' + +class FileSplitter: + BLOCK_FILENAME_FORMAT = "block_{0}.dat" def __init__(self, filename): self.filename = filename @@ -15,7 +16,7 @@ def __init__(self, filename): def write_block(self, data, block_number): filename = self.BLOCK_FILENAME_FORMAT.format(block_number) - with open(filename, 'w') as file: + with open(filename, "w") as file: file.write(data) self.block_filenames.append(filename) @@ -36,14 +37,14 @@ def split(self, block_size, sort_key=None): else: lines.sort(key=sort_key) - self.write_block(''.join(lines), i) + self.write_block("".join(lines), i) i += 1 def cleanup(self): map(lambda f: os.remove(f), self.block_filenames) -class NWayMerge(object): +class NWayMerge: def select(self, choices): min_index = -1 min_str = None @@ -55,7 +56,7 @@ def select(self, choices): return min_index -class FilesArray(object): +class FilesArray: def __init__(self, files): self.files = files self.empty = set() @@ -63,14 +64,16 @@ def __init__(self, files): self.buffers = {i: None for i in range(self.num_buffers)} def get_dict(self): - return {i: self.buffers[i] for i in range(self.num_buffers) if i not in self.empty} + return { + i: self.buffers[i] for i in range(self.num_buffers) if i not in self.empty + } def refresh(self): for i in range(self.num_buffers): if self.buffers[i] is None and i not in self.empty: self.buffers[i] = self.files[i].readline() - if self.buffers[i] == '': + if self.buffers[i] == "": self.empty.add(i) self.files[i].close() @@ -86,13 +89,13 @@ def unshift(self, index): return value -class FileMerger(object): +class FileMerger: def __init__(self, merge_strategy): self.merge_strategy = merge_strategy def merge(self, filenames, outfilename, buffer_size): buffers = FilesArray(self.get_file_handles(filenames, buffer_size)) - with open(outfilename, 'w', buffer_size) as outfile: + with open(outfilename, "w", buffer_size) as outfile: while buffers.refresh(): min_index = self.merge_strategy.select(buffers.get_dict()) outfile.write(buffers.unshift(min_index)) @@ -101,13 +104,12 @@ def get_file_handles(self, filenames, buffer_size): files = {} for i in range(len(filenames)): - files[i] = open(filenames[i], 'r', buffer_size) + files[i] = open(filenames[i], "r", buffer_size) return files - -class ExternalSort(object): +class ExternalSort: def __init__(self, block_size): self.block_size = block_size @@ -118,7 +120,7 @@ def sort(self, filename, sort_key=None): merger = FileMerger(NWayMerge()) buffer_size = self.block_size / (num_blocks + 1) - merger.merge(splitter.get_block_filenames(), filename + '.out', buffer_size) + merger.merge(splitter.get_block_filenames(), filename + ".out", buffer_size) splitter.cleanup() @@ -127,32 +129,29 @@ def get_number_blocks(self, filename, block_size): def parse_memory(string): - if string[-1].lower() == 'k': + if string[-1].lower() == "k": return int(string[:-1]) * 1024 - elif string[-1].lower() == 'm': + elif string[-1].lower() == "m": return int(string[:-1]) * 1024 * 1024 - elif string[-1].lower() == 'g': + elif string[-1].lower() == "g": return int(string[:-1]) * 1024 * 1024 * 1024 else: return int(string) - def main(): parser = argparse.ArgumentParser() - parser.add_argument('-m', - '--mem', - help='amount of memory to use for sorting', - default='100M') - parser.add_argument('filename', - metavar='', - nargs=1, - help='name of file to sort') + parser.add_argument( + "-m", "--mem", help="amount of memory to use for sorting", default="100M" + ) + parser.add_argument( + "filename", metavar="", nargs=1, help="name of file to sort" + ) args = parser.parse_args() sorter = ExternalSort(parse_memory(args.mem)) sorter.sort(args.filename[0]) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/sorts/gnome_sort.py b/sorts/gnome_sort.py index 075749e37663..58a44c94da43 100644 --- a/sorts/gnome_sort.py +++ b/sorts/gnome_sort.py @@ -1,7 +1,5 @@ """Gnome Sort Algorithm.""" -from __future__ import print_function - def gnome_sort(unsorted): """Pure implementation of the gnome sort algorithm in Python.""" @@ -16,17 +14,12 @@ def gnome_sort(unsorted): else: unsorted[i - 1], unsorted[i] = unsorted[i], unsorted[i - 1] i -= 1 - if (i == 0): + if i == 0: i = 1 -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] gnome_sort(unsorted) print(unsorted) diff --git a/sorts/heap_sort.py b/sorts/heap_sort.py index 3c72abca8059..4dca879bd89c 100644 --- a/sorts/heap_sort.py +++ b/sorts/heap_sort.py @@ -1,5 +1,5 @@ -''' -This is a pure python implementation of the heap sort algorithm. +""" +This is a pure Python implementation of the heap sort algorithm. For doctests run following command: python -m doctest -v heap_sort.py @@ -8,9 +8,7 @@ For manual testing run: python heap_sort.py -''' - -from __future__ import print_function +""" def heapify(unsorted, index, heap_size): @@ -29,7 +27,7 @@ def heapify(unsorted, index, heap_size): def heap_sort(unsorted): - ''' + """ Pure implementation of the heap sort algorithm in Python :param collection: some mutable ordered collection with heterogeneous comparable items inside @@ -44,7 +42,7 @@ def heap_sort(unsorted): >>> heap_sort([-2, -5, -45]) [-45, -5, -2] - ''' + """ n = len(unsorted) for i in range(n // 2 - 1, -1, -1): heapify(unsorted, i, n) @@ -53,12 +51,8 @@ def heap_sort(unsorted): heapify(unsorted, 0, i) return unsorted -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(heap_sort(unsorted)) diff --git a/sorts/i_sort.py b/sorts/i_sort.py new file mode 100644 index 000000000000..f6100a8d0819 --- /dev/null +++ b/sorts/i_sort.py @@ -0,0 +1,21 @@ +def insertionSort(arr): + """ + >>> a = arr[:] + >>> insertionSort(a) + >>> a == sorted(a) + True + """ + for i in range(1, len(arr)): + key = arr[i] + j = i - 1 + while j >= 0 and key < arr[j]: + arr[j + 1] = arr[j] + j -= 1 + arr[j + 1] = key + + +arr = [12, 11, 13, 5, 6] +insertionSort(arr) +print("Sorted array is:") +for i in range(len(arr)): + print("%d" % arr[i]) diff --git a/sorts/insertion_sort.py b/sorts/insertion_sort.py index e088705947d4..ca678381b431 100644 --- a/sorts/insertion_sort.py +++ b/sorts/insertion_sort.py @@ -1,5 +1,5 @@ """ -This is a pure python implementation of the insertion sort algorithm +This is a pure Python implementation of the insertion sort algorithm For doctests run following command: python -m doctest -v insertion_sort.py @@ -9,7 +9,6 @@ For manual testing run: python insertion_sort.py """ -from __future__ import print_function def insertion_sort(collection): @@ -32,19 +31,20 @@ def insertion_sort(collection): for loop_index in range(1, len(collection)): insertion_index = loop_index - while insertion_index > 0 and collection[insertion_index - 1] > collection[insertion_index]: - collection[insertion_index], collection[insertion_index - 1] = collection[insertion_index - 1], collection[insertion_index] + while ( + insertion_index > 0 + and collection[insertion_index - 1] > collection[insertion_index] + ): + collection[insertion_index], collection[insertion_index - 1] = ( + collection[insertion_index - 1], + collection[insertion_index], + ) insertion_index -= 1 return collection -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(insertion_sort(unsorted)) diff --git a/sorts/merge_sort.py b/sorts/merge_sort.py index 714861e72642..e8031a1cb97c 100644 --- a/sorts/merge_sort.py +++ b/sorts/merge_sort.py @@ -1,5 +1,5 @@ """ -This is a pure python implementation of the merge sort algorithm +This is a pure Python implementation of the merge sort algorithm For doctests run following command: python -m doctest -v merge_sort.py @@ -9,7 +9,6 @@ For manual testing run: python merge_sort.py """ -from __future__ import print_function def merge_sort(collection): @@ -29,28 +28,25 @@ def merge_sort(collection): >>> merge_sort([-2, -5, -45]) [-45, -5, -2] """ + def merge(left, right): - '''merge left and right + """merge left and right :param left: left collection :param right: right collection :return: merge result - ''' + """ result = [] while left and right: result.append((left if left[0] <= right[0] else right).pop(0)) return result + left + right + if len(collection) <= 1: return collection mid = len(collection) // 2 return merge(merge_sort(collection[:mid]), merge_sort(collection[mid:])) -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] - print(*merge_sort(unsorted), sep=',') +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + print(*merge_sort(unsorted), sep=",") diff --git a/sorts/odd_even_transposition_parallel.py b/sorts/odd_even_transposition_parallel.py index 9bf81a39e27a..5de7a016c628 100644 --- a/sorts/odd_even_transposition_parallel.py +++ b/sorts/odd_even_transposition_parallel.py @@ -10,116 +10,135 @@ They are synchronized with locks and message passing but other forms of synchronization could be used. """ -from multiprocessing import Process, Pipe, Lock +from multiprocessing import Lock, Pipe, Process -#lock used to ensure that two processes do not access a pipe at the same time +# lock used to ensure that two processes do not access a pipe at the same time processLock = Lock() """ The function run by the processes that sorts the list -position = the position in the list the prcoess represents, used to know which +position = the position in the list the process represents, used to know which neighbor we pass our value to value = the initial value at list[position] LSend, RSend = the pipes we use to send to our left and right neighbors LRcv, RRcv = the pipes we use to receive from our left and right neighbors resultPipe = the pipe used to send results back to main """ + + def oeProcess(position, value, LSend, RSend, LRcv, RRcv, resultPipe): global processLock - #we perform n swaps since after n swaps we know we are sorted - #we *could* stop early if we are sorted already, but it takes as long to - #find out we are sorted as it does to sort the list with this algorithm + # we perform n swaps since after n swaps we know we are sorted + # we *could* stop early if we are sorted already, but it takes as long to + # find out we are sorted as it does to sort the list with this algorithm for i in range(0, 10): - if( (i + position) % 2 == 0 and RSend != None): - #send your value to your right neighbor + if (i + position) % 2 == 0 and RSend is not None: + # send your value to your right neighbor processLock.acquire() RSend[1].send(value) processLock.release() - #receive your right neighbor's value + # receive your right neighbor's value processLock.acquire() temp = RRcv[0].recv() processLock.release() - #take the lower value since you are on the left + # take the lower value since you are on the left value = min(value, temp) - elif( (i + position) % 2 != 0 and LSend != None): - #send your value to your left neighbor + elif (i + position) % 2 != 0 and LSend is not None: + # send your value to your left neighbor processLock.acquire() LSend[1].send(value) processLock.release() - #receive your left neighbor's value + # receive your left neighbor's value processLock.acquire() temp = LRcv[0].recv() processLock.release() - #take the higher value since you are on the right + # take the higher value since you are on the right value = max(value, temp) - #after all swaps are performed, send the values back to main + # after all swaps are performed, send the values back to main resultPipe[1].send(value) + """ the function which creates the processes that perform the parallel swaps arr = the list to be sorted """ -def OddEvenTransposition(arr): - processArray = [] +def OddEvenTransposition(arr): + processArray = [] resultPipe = [] - - #initialize the list of pipes where the values will be retrieved + # initialize the list of pipes where the values will be retrieved for _ in arr: resultPipe.append(Pipe()) - - #creates the processes - #the first and last process only have one neighbor so they are made outside - #of the loop + # creates the processes + # the first and last process only have one neighbor so they are made outside + # of the loop tempRs = Pipe() tempRr = Pipe() - processArray.append(Process(target = oeProcess, args = (0, arr[0], None, tempRs, None, tempRr, resultPipe[0]))) + processArray.append( + Process( + target=oeProcess, + args=(0, arr[0], None, tempRs, None, tempRr, resultPipe[0]), + ) + ) tempLr = tempRs tempLs = tempRr for i in range(1, len(arr) - 1): tempRs = Pipe() tempRr = Pipe() - processArray.append(Process(target = oeProcess, args = (i, arr[i], tempLs, tempRs, tempLr, tempRr, resultPipe[i]))) + processArray.append( + Process( + target=oeProcess, + args=(i, arr[i], tempLs, tempRs, tempLr, tempRr, resultPipe[i]), + ) + ) tempLr = tempRs tempLs = tempRr - processArray.append(Process(target = oeProcess, args = (len(arr) - 1, arr[len(arr) - 1], tempLs, None, tempLr, None, resultPipe[len(arr) - 1]))) - - #start the processes + processArray.append( + Process( + target=oeProcess, + args=( + len(arr) - 1, + arr[len(arr) - 1], + tempLs, + None, + tempLr, + None, + resultPipe[len(arr) - 1], + ), + ) + ) + + # start the processes for p in processArray: p.start() - #wait for the processes to end and write their values to the list + # wait for the processes to end and write their values to the list for p in range(0, len(resultPipe)): arr[p] = resultPipe[p][0].recv() processArray[p].join() + return arr - return(arr) - -#creates a reverse sorted list and sorts it +# creates a reverse sorted list and sorts it def main(): - arr = [] - - for i in range(10, 0, -1): - arr.append(i) + arr = list(range(10, 0, -1)) print("Initial List") print(*arr) - - list = OddEvenTransposition(arr) - + arr = OddEvenTransposition(arr) print("Sorted List\n") print(*arr) + if __name__ == "__main__": main() diff --git a/sorts/odd_even_transposition_single_threaded.py b/sorts/odd_even_transposition_single_threaded.py index ec5f3cf14e55..ec045d9dd08d 100644 --- a/sorts/odd_even_transposition_single_threaded.py +++ b/sorts/odd_even_transposition_single_threaded.py @@ -5,6 +5,7 @@ is no better than bubble sort. """ + def OddEvenTransposition(arr): for i in range(0, len(arr)): for i in range(i % 2, len(arr) - 1, 2): @@ -14,7 +15,8 @@ def OddEvenTransposition(arr): return arr -#creates a list and sorts it + +# creates a list and sorts it def main(): list = [] @@ -28,5 +30,6 @@ def main(): print("Sorted List\n") print(*list) + if __name__ == "__main__": main() diff --git a/sorts/pancake_sort.py b/sorts/pancake_sort.py index 3b48bc6e46d9..e5d600738435 100644 --- a/sorts/pancake_sort.py +++ b/sorts/pancake_sort.py @@ -1,20 +1,39 @@ -"""Pancake Sort Algorithm.""" -# Only can reverse array from 0 to i +""" +This is a pure Python implementation of the pancake sort algorithm +For doctests run following command: +python3 -m doctest -v pancake_sort.py +or +python -m doctest -v pancake_sort.py +For manual testing run: +python pancake_sort.py +""" def pancake_sort(arr): - """Sort Array with Pancake Sort.""" + """Sort Array with Pancake Sort. + :param arr: Collection containing comparable items + :return: Collection ordered in ascending order of items + Examples: + >>> pancake_sort([0, 5, 3, 2, 2]) + [0, 2, 2, 3, 5] + >>> pancake_sort([]) + [] + >>> pancake_sort([-2, -5, -45]) + [-45, -5, -2] + """ cur = len(arr) while cur > 1: # Find the maximum number in arr mi = arr.index(max(arr[0:cur])) # Reverse from 0 to mi - arr = arr[mi::-1] + arr[mi + 1:len(arr)] + arr = arr[mi::-1] + arr[mi + 1 : len(arr)] # Reverse whole list - arr = arr[cur - 1::-1] + arr[cur:len(arr)] + arr = arr[cur - 1 :: -1] + arr[cur : len(arr)] cur -= 1 return arr -if __name__ == '__main__': - print(pancake_sort([0, 10, 15, 3, 2, 9, 14, 13])) +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + print(pancake_sort(unsorted)) diff --git a/sorts/pigeon_sort.py b/sorts/pigeon_sort.py index 65eb8896ea9c..cf900699bc8d 100644 --- a/sorts/pigeon_sort.py +++ b/sorts/pigeon_sort.py @@ -1,35 +1,59 @@ -''' +""" This is an implementation of Pigeon Hole Sort. -''' + For doctests run following command: + + python3 -m doctest -v pigeon_sort.py + or + python -m doctest -v pigeon_sort.py + + For manual testing run: + python pigeon_sort.py +""" -from __future__ import print_function def pigeon_sort(array): + """ + Implementation of pigeon hole sort algorithm + :param array: Collection of comparable items + :return: Collection sorted in ascending order + >>> pigeon_sort([0, 5, 3, 2, 2]) + [0, 2, 2, 3, 5] + >>> pigeon_sort([]) + [] + >>> pigeon_sort([-2, -5, -45]) + [-45, -5, -2] + """ + if len(array) == 0: + return array + # Manually finds the minimum and maximum of the array. min = array[0] max = array[0] for i in range(len(array)): - if(array[i] < min): min = array[i] - elif(array[i] > max): max = array[i] + if array[i] < min: + min = array[i] + elif array[i] > max: + max = array[i] # Compute the variables - holes_range = max-min + 1 + holes_range = max - min + 1 holes = [0 for _ in range(holes_range)] holes_repeat = [0 for _ in range(holes_range)] # Make the sorting. for i in range(len(array)): index = array[i] - min - if(holes[index] != array[i]): + if holes[index] != array[i]: holes[index] = array[i] holes_repeat[index] += 1 - else: holes_repeat[index] += 1 + else: + holes_repeat[index] += 1 # Makes the array back by replacing the numbers. index = 0 for i in range(holes_range): - while(holes_repeat[i] > 0): + while holes_repeat[i] > 0: array[index] = holes[i] index += 1 holes_repeat[i] -= 1 @@ -37,14 +61,8 @@ def pigeon_sort(array): # Returns the sorted array. return array -if __name__ == '__main__': - try: - raw_input # Python2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by comma:\n') - unsorted = [int(x) for x in user_input.split(',')] - sorted = pigeon_sort(unsorted) - print(sorted) +if __name__ == "__main__": + user_input = input("Enter numbers separated by comma:\n") + unsorted = [int(x) for x in user_input.split(",")] + print(pigeon_sort(unsorted)) diff --git a/sorts/pigeonhole_sort.py b/sorts/pigeonhole_sort.py new file mode 100644 index 000000000000..bfa9bb11b8a6 --- /dev/null +++ b/sorts/pigeonhole_sort.py @@ -0,0 +1,45 @@ +# Python program to implement Pigeonhole Sorting in python + +# Algorithm for the pigeonhole sorting + + +def pigeonhole_sort(a): + """ + >>> a = [8, 3, 2, 7, 4, 6, 8] + >>> b = sorted(a) # a nondestructive sort + >>> pigeonhole_sort(a) # a destructive sort + >>> a == b + True + """ + # size of range of values in the list (ie, number of pigeonholes we need) + + min_val = min(a) # min() finds the minimum value + max_val = max(a) # max() finds the maximum value + + size = max_val - min_val + 1 # size is difference of max and min values plus one + + # list of pigeonholes of size equal to the variable size + holes = [0] * size + + # Populate the pigeonholes. + for x in a: + assert isinstance(x, int), "integers only please" + holes[x - min_val] += 1 + + # Putting the elements back into the array in an order. + i = 0 + for count in range(size): + while holes[count] > 0: + holes[count] -= 1 + a[i] = count + min_val + i += 1 + + +def main(): + a = [8, 3, 2, 7, 4, 6, 8] + pigeonhole_sort(a) + print("Sorted order is:", " ".join(a)) + + +if __name__ == "__main__": + main() diff --git a/sorts/quick_sort.py b/sorts/quick_sort.py index 7e8c868ebb06..f2a55c58b437 100644 --- a/sorts/quick_sort.py +++ b/sorts/quick_sort.py @@ -1,5 +1,5 @@ """ -This is a pure python implementation of the quick sort algorithm +This is a pure Python implementation of the quick sort algorithm For doctests run following command: python -m doctest -v quick_sort.py @@ -9,7 +9,6 @@ For manual testing run: python quick_sort.py """ -from __future__ import print_function def quick_sort(collection): @@ -46,12 +45,7 @@ def quick_sort(collection): return quick_sort(lesser) + [pivot] + quick_sort(greater) -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [ int(item) for item in user_input.split(',') ] - print( quick_sort(unsorted) ) +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + print(quick_sort(unsorted)) diff --git a/sorts/quick_sort_3_partition.py b/sorts/quick_sort_3_partition.py index def646cdbc50..a25ac7def802 100644 --- a/sorts/quick_sort_3_partition.py +++ b/sorts/quick_sort_3_partition.py @@ -1,5 +1,3 @@ -from __future__ import print_function - def quick_sort_3partition(sorting, left, right): if right <= left: return @@ -19,13 +17,9 @@ def quick_sort_3partition(sorting, left, right): quick_sort_3partition(sorting, left, a - 1) quick_sort_3partition(sorting, b + 1, right) -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [ int(item) for item in user_input.split(',') ] - quick_sort_3partition(unsorted,0,len(unsorted)-1) +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + quick_sort_3partition(unsorted, 0, len(unsorted) - 1) print(unsorted) diff --git a/sorts/radix_sort.py b/sorts/radix_sort.py index 8dfc66b17b23..2990247a0ac0 100644 --- a/sorts/radix_sort.py +++ b/sorts/radix_sort.py @@ -6,21 +6,21 @@ def radix_sort(lst): max_digit = max(lst) while placement < max_digit: - # declare and initialize buckets - buckets = [list() for _ in range( RADIX )] + # declare and initialize buckets + buckets = [list() for _ in range(RADIX)] - # split lst between lists - for i in lst: - tmp = int((i / placement) % RADIX) - buckets[tmp].append(i) + # split lst between lists + for i in lst: + tmp = int((i / placement) % RADIX) + buckets[tmp].append(i) - # empty lists into lst array - a = 0 - for b in range( RADIX ): - buck = buckets[b] - for i in buck: - lst[a] = i - a += 1 + # empty lists into lst array + a = 0 + for b in range(RADIX): + buck = buckets[b] + for i in buck: + lst[a] = i + a += 1 - # move to next - placement *= RADIX + # move to next + placement *= RADIX diff --git a/sorts/random_normal_distribution_quicksort.py b/sorts/random_normal_distribution_quicksort.py index dfa37da61e26..be3b90190407 100644 --- a/sorts/random_normal_distribution_quicksort.py +++ b/sorts/random_normal_distribution_quicksort.py @@ -1,66 +1,62 @@ -from __future__ import print_function from random import randint from tempfile import TemporaryFile import numpy as np - -def _inPlaceQuickSort(A,start,end): +def _inPlaceQuickSort(A, start, end): count = 0 - if start list: + """ + >>> for data in ([2, 1, 0], [2.2, 1.1, 0], "quick_sort"): + ... quick_sort(data) == sorted(data) + True + True + True + """ + if len(data) <= 1: + return data + else: + return ( + quick_sort([e for e in data[1:] if e <= data[0]]) + + [data[0]] + + quick_sort([e for e in data[1:] if e > data[0]]) + ) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/sorts/recursive_bubble_sort.py b/sorts/recursive_bubble_sort.py new file mode 100644 index 000000000000..616044778a4a --- /dev/null +++ b/sorts/recursive_bubble_sort.py @@ -0,0 +1,41 @@ +def bubble_sort(list1): + """ + It is similar is bubble sort but recursive. + :param list1: mutable ordered sequence of elements + :return: the same list in ascending order + + >>> bubble_sort([0, 5, 2, 3, 2]) + [0, 2, 2, 3, 5] + + >>> bubble_sort([]) + [] + + >>> bubble_sort([-2, -45, -5]) + [-45, -5, -2] + + >>> bubble_sort([-23, 0, 6, -4, 34]) + [-23, -4, 0, 6, 34] + + >>> bubble_sort([-23, 0, 6, -4, 34]) == sorted([-23, 0, 6, -4, 34]) + True + + >>> bubble_sort(['z','a','y','b','x','c']) + ['a', 'b', 'c', 'x', 'y', 'z'] + + """ + + for i, num in enumerate(list1): + try: + if list1[i + 1] < num: + list1[i] = list1[i + 1] + list1[i + 1] = num + bubble_sort(list1) + except IndexError: + pass + return list1 + + +if __name__ == "__main__": + list1 = [33, 99, 22, 11, 66] + bubble_sort(list1) + print(list1) diff --git a/sorts/recursive_insertion_sort.py b/sorts/recursive_insertion_sort.py new file mode 100644 index 000000000000..5b14c2a6c139 --- /dev/null +++ b/sorts/recursive_insertion_sort.py @@ -0,0 +1,75 @@ +""" +A recursive implementation of the insertion sort algorithm +""" + +from typing import List + + +def rec_insertion_sort(collection: List, n: int): + """ + Given a collection of numbers and its length, sorts the collections + in ascending order + + :param collection: A mutable collection of comparable elements + :param n: The length of collections + + >>> col = [1, 2, 1] + >>> rec_insertion_sort(col, len(col)) + >>> print(col) + [1, 1, 2] + + >>> col = [2, 1, 0, -1, -2] + >>> rec_insertion_sort(col, len(col)) + >>> print(col) + [-2, -1, 0, 1, 2] + + >>> col = [1] + >>> rec_insertion_sort(col, len(col)) + >>> print(col) + [1] + """ + # Checks if the entire collection has been sorted + if len(collection) <= 1 or n <= 1: + return + + insert_next(collection, n - 1) + rec_insertion_sort(collection, n - 1) + + +def insert_next(collection: List, index: int): + """ + Inserts the '(index-1)th' element into place + + >>> col = [3, 2, 4, 2] + >>> insert_next(col, 1) + >>> print(col) + [2, 3, 4, 2] + + >>> col = [3, 2, 3] + >>> insert_next(col, 2) + >>> print(col) + [3, 2, 3] + + >>> col = [] + >>> insert_next(col, 1) + >>> print(col) + [] + """ + # Checks order between adjacent elements + if index >= len(collection) or collection[index - 1] <= collection[index]: + return + + # Swaps adjacent elements since they are not in ascending order + collection[index - 1], collection[index] = ( + collection[index], + collection[index - 1], + ) + + insert_next(collection, index + 1) + + +if __name__ == "__main__": + numbers = input("Enter integers separated by spaces: ") + numbers = [int(num) for num in numbers.split()] + rec_insertion_sort(numbers, len(numbers)) + print(numbers) diff --git a/sorts/selection_sort.py b/sorts/selection_sort.py index c1d66c5a8b38..f3beb31b7070 100644 --- a/sorts/selection_sort.py +++ b/sorts/selection_sort.py @@ -1,5 +1,5 @@ """ -This is a pure python implementation of the selection sort algorithm +This is a pure Python implementation of the selection sort algorithm For doctests run following command: python -m doctest -v selection_sort.py @@ -9,7 +9,6 @@ For manual testing run: python selection_sort.py """ -from __future__ import print_function def selection_sort(collection): @@ -36,18 +35,12 @@ def selection_sort(collection): for k in range(i + 1, length): if collection[k] < collection[least]: least = k - collection[least], collection[i] = ( - collection[i], collection[least] - ) + if least != i: + collection[least], collection[i] = (collection[i], collection[least]) return collection -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(selection_sort(unsorted)) diff --git a/sorts/shell_sort.py b/sorts/shell_sort.py index dc1846758243..80d95870f95b 100644 --- a/sorts/shell_sort.py +++ b/sorts/shell_sort.py @@ -1,5 +1,5 @@ """ -This is a pure python implementation of the shell sort algorithm +This is a pure Python implementation of the shell sort algorithm For doctests run following command: python -m doctest -v shell_sort.py @@ -9,7 +9,6 @@ For manual testing run: python shell_sort.py """ -from __future__ import print_function def shell_sort(collection): @@ -43,12 +42,8 @@ def shell_sort(collection): return collection -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] print(shell_sort(unsorted)) diff --git a/sorts/stooge_sort.py b/sorts/stooge_sort.py new file mode 100644 index 000000000000..de997a85df12 --- /dev/null +++ b/sorts/stooge_sort.py @@ -0,0 +1,40 @@ +def stooge_sort(arr): + """ + Examples: + >>> stooge_sort([18.1, 0, -7.1, -1, 2, 2]) + [-7.1, -1, 0, 2, 2, 18.1] + + >>> stooge_sort([]) + [] + """ + stooge(arr, 0, len(arr) - 1) + return arr + + +def stooge(arr, i, h): + + if i >= h: + return + + # If first element is smaller than the last then swap them + if arr[i] > arr[h]: + arr[i], arr[h] = arr[h], arr[i] + + # If there are more than 2 elements in the array + if h - i + 1 > 2: + t = (int)((h - i + 1) / 3) + + # Recursively sort first 2/3 elements + stooge(arr, i, (h - t)) + + # Recursively sort last 2/3 elements + stooge(arr, i + t, (h)) + + # Recursively sort first 2/3 elements + stooge(arr, i, (h - t)) + + +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + print(stooge_sort(unsorted)) diff --git a/sorts/tim_sort.py b/sorts/tim_sort.py index b4032b91aec1..b95ff34cf384 100644 --- a/sorts/tim_sort.py +++ b/sorts/tim_sort.py @@ -1,10 +1,6 @@ -from __future__ import print_function def binary_search(lst, item, start, end): if start == end: - if lst[start] > item: - return start - else: - return start + 1 + return start if lst[start] > item else start + 1 if start > end: return start @@ -23,7 +19,7 @@ def insertion_sort(lst): for index in range(1, length): value = lst[index] pos = binary_search(lst, value, 0, index - 1) - lst = lst[:pos] + [value] + lst[pos:index] + lst[index+1:] + lst = lst[:pos] + [value] + lst[pos:index] + lst[index + 1 :] return lst @@ -42,30 +38,34 @@ def merge(left, right): def tim_sort(lst): - runs, sorted_runs = [], [] + """ + >>> tim_sort("Python") + ['P', 'h', 'n', 'o', 't', 'y'] + >>> tim_sort((1.1, 1, 0, -1, -1.1)) + [-1.1, -1, 0, 1, 1.1] + >>> tim_sort(list(reversed(list(range(7))))) + [0, 1, 2, 3, 4, 5, 6] + >>> tim_sort([3, 2, 1]) == insertion_sort([3, 2, 1]) + True + >>> tim_sort([3, 2, 1]) == sorted([3, 2, 1]) + True + """ length = len(lst) + runs, sorted_runs = [], [] new_run = [lst[0]] sorted_array = [] - - for i in range(1, length): - if i == length - 1: - new_run.append(lst[i]) - runs.append(new_run) - break - + i = 1 + while i < length: if lst[i] < lst[i - 1]: - if not new_run: - runs.append([lst[i - 1]]) - new_run.append(lst[i]) - else: - runs.append(new_run) - new_run = [] + runs.append(new_run) + new_run = [lst[i]] else: new_run.append(lst[i]) + i += 1 + runs.append(new_run) for run in runs: sorted_runs.append(insertion_sort(run)) - for run in sorted_runs: sorted_array = merge(sorted_array, run) @@ -74,9 +74,10 @@ def tim_sort(lst): def main(): - lst = [5,9,10,3,-4,5,178,92,46,-18,0,7] + lst = [5, 9, 10, 3, -4, 5, 178, 92, 46, -18, 0, 7] sorted_lst = tim_sort(lst) print(sorted_lst) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/sorts/topological_sort.py b/sorts/topological_sort.py index 400cfb4ca270..e7a52f7c7714 100644 --- a/sorts/topological_sort.py +++ b/sorts/topological_sort.py @@ -1,13 +1,12 @@ """Topological Sort.""" -from __future__ import print_function # a # / \ # b c # / \ # d e -edges = {'a': ['c', 'b'], 'b': ['d', 'e'], 'c': [], 'd': [], 'e': []} -vertices = ['a', 'b', 'c', 'd', 'e'] +edges = {"a": ["c", "b"], "b": ["d", "e"], "c": [], "d": [], "e": []} +vertices = ["a", "b", "c", "d", "e"] def topological_sort(start, visited, sort): @@ -31,6 +30,6 @@ def topological_sort(start, visited, sort): return sort -if __name__ == '__main__': - sort = topological_sort('a', [], []) +if __name__ == "__main__": + sort = topological_sort("a", [], []) print(sort) diff --git a/sorts/tree_sort.py b/sorts/tree_sort.py index baa4fc1acc20..716170a94fd1 100644 --- a/sorts/tree_sort.py +++ b/sorts/tree_sort.py @@ -5,7 +5,7 @@ """ -class node(): +class node: # BST data structure def __init__(self, val): self.val = val @@ -49,5 +49,5 @@ def tree_sort(arr): return res -if __name__ == '__main__': +if __name__ == "__main__": print(tree_sort([10, 1, 3, 2, 9, 14, 13])) diff --git a/sorts/merge_sort_fastest.py b/sorts/unknown_sort.py similarity index 58% rename from sorts/merge_sort_fastest.py rename to sorts/unknown_sort.py index bd356c935ca0..5ecc55e9cf69 100644 --- a/sorts/merge_sort_fastest.py +++ b/sorts/unknown_sort.py @@ -1,10 +1,8 @@ -''' -Python implementation of the fastest merge sort algorithm. -Takes an average of 0.6 microseconds to sort a list of length 1000 items. +""" +Python implementation of a sort algorithm. Best Case Scenario : O(n) -Worst Case Scenario : O(n^2) because native python functions:min, max and remove are already O(n) -''' -from __future__ import print_function +Worst Case Scenario : O(n^2) because native Python functions:min, max and remove are already O(n) +""" def merge_sort(collection): @@ -35,12 +33,7 @@ def merge_sort(collection): return start + collection + end -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - user_input = raw_input('Enter numbers separated by a comma:\n').strip() - unsorted = [int(item) for item in user_input.split(',')] - print(*merge_sort(unsorted), sep=',') +if __name__ == "__main__": + user_input = input("Enter numbers separated by a comma:\n").strip() + unsorted = [int(item) for item in user_input.split(",")] + print(*merge_sort(unsorted), sep=",") diff --git a/sorts/wiggle_sort.py b/sorts/wiggle_sort.py index 606feb4d3dd1..5e5220ffbf05 100644 --- a/sorts/wiggle_sort.py +++ b/sorts/wiggle_sort.py @@ -16,7 +16,7 @@ def wiggle_sort(nums): nums[i - 1], nums[i] = nums[i], nums[i - 1] -if __name__ == '__main__': +if __name__ == "__main__": print("Enter the array elements:\n") array = list(map(int, input().split())) print("The unsorted array is:\n") diff --git a/strings/aho-corasick.py b/strings/aho-corasick.py new file mode 100644 index 000000000000..bb6955bdd423 --- /dev/null +++ b/strings/aho-corasick.py @@ -0,0 +1,92 @@ +from collections import deque + + +class Automaton: + def __init__(self, keywords): + self.adlist = list() + self.adlist.append( + {"value": "", "next_states": [], "fail_state": 0, "output": []} + ) + + for keyword in keywords: + self.add_keyword(keyword) + self.set_fail_transitions() + + def find_next_state(self, current_state, char): + for state in self.adlist[current_state]["next_states"]: + if char == self.adlist[state]["value"]: + return state + return None + + def add_keyword(self, keyword): + current_state = 0 + for character in keyword: + if self.find_next_state(current_state, character): + current_state = self.find_next_state(current_state, character) + else: + self.adlist.append( + { + "value": character, + "next_states": [], + "fail_state": 0, + "output": [], + } + ) + self.adlist[current_state]["next_states"].append(len(self.adlist) - 1) + current_state = len(self.adlist) - 1 + self.adlist[current_state]["output"].append(keyword) + + def set_fail_transitions(self): + q = deque() + for node in self.adlist[0]["next_states"]: + q.append(node) + self.adlist[node]["fail_state"] = 0 + while q: + r = q.popleft() + for child in self.adlist[r]["next_states"]: + q.append(child) + state = self.adlist[r]["fail_state"] + while ( + self.find_next_state(state, self.adlist[child]["value"]) is None + and state != 0 + ): + state = self.adlist[state]["fail_state"] + self.adlist[child]["fail_state"] = self.find_next_state( + state, self.adlist[child]["value"] + ) + if self.adlist[child]["fail_state"] is None: + self.adlist[child]["fail_state"] = 0 + self.adlist[child]["output"] = ( + self.adlist[child]["output"] + + self.adlist[self.adlist[child]["fail_state"]]["output"] + ) + + def search_in(self, string): + """ + >>> A = Automaton(["what", "hat", "ver", "er"]) + >>> A.search_in("whatever, err ... , wherever") + {'what': [0], 'hat': [1], 'ver': [5, 25], 'er': [6, 10, 22, 26]} + """ + result = dict() # returns a dict with keywords and list of its occurrences + current_state = 0 + for i in range(len(string)): + while ( + self.find_next_state(current_state, string[i]) is None + and current_state != 0 + ): + current_state = self.adlist[current_state]["fail_state"] + current_state = self.find_next_state(current_state, string[i]) + if current_state is None: + current_state = 0 + else: + for key in self.adlist[current_state]["output"]: + if not (key in result): + result[key] = [] + result[key].append(i - len(key) + 1) + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/strings/boyer_moore_search.py b/strings/boyer_moore_search.py index 781ff0ca6106..bd777c7c7e05 100644 --- a/strings/boyer_moore_search.py +++ b/strings/boyer_moore_search.py @@ -20,49 +20,44 @@ class BoyerMooreSearch: - - def __init__(self, text, pattern): self.text, self.pattern = text, pattern self.textLen, self.patLen = len(text), len(pattern) - def match_in_pattern(self, char): """ finds the index of char in pattern in reverse order - Paremeters : + Parameters : char (chr): character to be searched Returns : i (int): index of char from last in pattern -1 (int): if char is not found in pattern - """ + """ - for i in range(self.patLen-1, -1, -1): + for i in range(self.patLen - 1, -1, -1): if char == self.pattern[i]: return i return -1 - def mismatch_in_text(self, currentPos): """ finds the index of mis-matched character in text when compared with pattern from last - Paremeters : + Parameters : currentPos (int): current index position of text Returns : i (int): index of mismatched char from last in text - -1 (int): if there is no mis-match between pattern and text block + -1 (int): if there is no mismatch between pattern and text block """ - for i in range(self.patLen-1, -1, -1): + for i in range(self.patLen - 1, -1, -1): if self.pattern[i] != self.text[currentPos + i]: return currentPos + i return -1 - def bad_character_heuristic(self): - # searches pattern in text and returns index positions + # searches pattern in text and returns index positions positions = [] for i in range(self.textLen - self.patLen + 1): mismatch_index = self.mismatch_in_text(i) @@ -70,12 +65,14 @@ def bad_character_heuristic(self): positions.append(i) else: match_index = self.match_in_pattern(self.text[mismatch_index]) - i = mismatch_index - match_index #shifting index + i = ( + mismatch_index - match_index + ) # shifting index lgtm [py/multiple-definition] return positions - + text = "ABAABA" -pattern = "AB" +pattern = "AB" bms = BoyerMooreSearch(text, pattern) positions = bms.bad_character_heuristic() @@ -84,5 +81,3 @@ def bad_character_heuristic(self): else: print("Pattern found in following positions: ") print(positions) - - diff --git a/strings/check_pangram.py b/strings/check_pangram.py new file mode 100644 index 000000000000..410afd8cc609 --- /dev/null +++ b/strings/check_pangram.py @@ -0,0 +1,30 @@ +# Created by sarathkaul on 12/11/19 + + +def check_pangram( + input_str: str = "The quick brown fox jumps over the lazy dog", +) -> bool: + """ + A Pangram String contains all the alphabets at least once. + >>> check_pangram("The quick brown fox jumps over the lazy dog") + True + >>> check_pangram("My name is Unknown") + False + >>> check_pangram("The quick brown fox jumps over the la_y dog") + False + """ + frequency = set() + input_str = input_str.replace( + " ", "" + ) # Replacing all the Whitespaces in our sentence + for alpha in input_str: + if "a" <= alpha.lower() <= "z": + frequency.add(alpha.lower()) + + return True if len(frequency) == 26 else False + + +if __name__ == "main": + check_str = "INPUT STRING" + status = check_pangram(check_str) + print(f"{check_str} is {'not ' if status else ''}a pangram string") diff --git a/strings/is_palindrome.py b/strings/is_palindrome.py new file mode 100644 index 000000000000..3070970ca6d0 --- /dev/null +++ b/strings/is_palindrome.py @@ -0,0 +1,19 @@ +def is_palindrome(s): + """ + Determine whether the string is palindrome + :param s: + :return: Boolean + >>> is_palindrome("a man a plan a canal panama".replace(" ", "")) + True + >>> is_palindrome("Hello") + False + """ + return s == s[::-1] + + +if __name__ == "__main__": + s = input("Enter string to determine whether its palindrome or not: ").strip() + if is_palindrome(s): + print("Given string is palindrome") + else: + print("Given string is not palindrome") diff --git a/strings/knuth_morris_pratt.py b/strings/knuth_morris_pratt.py index 4553944284be..c7e96887c387 100644 --- a/strings/knuth_morris_pratt.py +++ b/strings/knuth_morris_pratt.py @@ -46,14 +46,14 @@ def get_failure_array(pattern): if pattern[i] == pattern[j]: i += 1 elif i > 0: - i = failure[i-1] + i = failure[i - 1] continue j += 1 failure.append(i) return failure -if __name__ == '__main__': +if __name__ == "__main__": # Test 1) pattern = "abc1abc12" text1 = "alskfjaldsabc1abc1abc12k23adsfabcabc" diff --git a/strings/levenshtein_distance.py b/strings/levenshtein_distance.py index 274dfd7ccf9b..9b8793544a99 100644 --- a/strings/levenshtein_distance.py +++ b/strings/levenshtein_distance.py @@ -64,15 +64,13 @@ def levenshtein_distance(first_word, second_word): return previous_row[-1] -if __name__ == '__main__': - try: - raw_input # Python 2 - except NameError: - raw_input = input # Python 3 - - first_word = raw_input('Enter the first word:\n').strip() - second_word = raw_input('Enter the second word:\n').strip() +if __name__ == "__main__": + first_word = input("Enter the first word:\n").strip() + second_word = input("Enter the second word:\n").strip() result = levenshtein_distance(first_word, second_word) - print('Levenshtein distance between {} and {} is {}'.format( - first_word, second_word, result)) + print( + "Levenshtein distance between {} and {} is {}".format( + first_word, second_word, result + ) + ) diff --git a/strings/lower.py b/strings/lower.py new file mode 100644 index 000000000000..c3a6e598b9ea --- /dev/null +++ b/strings/lower.py @@ -0,0 +1,29 @@ +def lower(word: str) -> str: + + """ + Will convert the entire string to lowecase letters + + >>> lower("wow") + 'wow' + >>> lower("HellZo") + 'hellzo' + >>> lower("WHAT") + 'what' + + >>> lower("wh[]32") + 'wh[]32' + >>> lower("whAT") + 'what' + """ + + # converting to ascii value int value and checking to see if char is a capital letter + # if it is a capital letter it is getting shift by 32 which makes it a lower case letter + return "".join( + chr(ord(char) + 32) if 65 <= ord(char) <= 90 else char for char in word + ) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/manacher.py b/strings/manacher.py index e73e173b43e0..95aba1fbe65d 100644 --- a/strings/manacher.py +++ b/strings/manacher.py @@ -1,52 +1,103 @@ -# calculate palindromic length from center with incrementing difference -def palindromic_length( center, diff, string): - if center-diff == -1 or center+diff == len(string) or string[center-diff] != string[center+diff] : - return 0 - return 1 + palindromic_length(center, diff+1, string) - -def palindromic_string( input_string ): +def palindromic_string(input_string): """ - Manacher’s algorithm which finds Longest Palindromic Substring in linear time. - - 1. first this conver input_string("xyx") into new_string("x|y|x") where odd positions are actual input - characters. - 2. for each character in new_string it find corresponding length and store, - a. max_length - b. max_length's center - 3. return output_string from center - max_length to center + max_length and remove all "|" + >>> palindromic_string('abbbaba') + 'abbba' + >>> palindromic_string('ababa') + 'ababa' + + Manacher’s algorithm which finds Longest palindromic Substring in linear time. + + 1. first this convert input_string("xyx") into new_string("x|y|x") where odd + positions are actual input characters. + 2. for each character in new_string it find corresponding length and store the length + and l,r to store previously calculated info.(please look the explanation for details) + + 3. return corresponding output_string by removing all "|" """ max_length = 0 - + # if input_string is "aba" than new_input_string become "a|b|a" new_input_string = "" output_string = "" # append each character + "|" in new_string for range(0, length-1) - for i in input_string[:len(input_string)-1] : + for i in input_string[: len(input_string) - 1]: new_input_string += i + "|" - #append last character + # append last character new_input_string += input_string[-1] + # we will store the starting and ending of previous furthest ending palindromic substring + l, r = 0, 0 + + # length[i] shows the length of palindromic substring with center i + length = [1 for i in range(len(new_input_string))] # for each character in new_string find corresponding palindromic string - for i in range(len(new_input_string)) : + for i in range(len(new_input_string)): + k = 1 if i > r else min(length[l + r - i] // 2, r - i + 1) + while ( + i - k >= 0 + and i + k < len(new_input_string) + and new_input_string[k + i] == new_input_string[i - k] + ): + k += 1 + + length[i] = 2 * k - 1 - # get palindromic length from ith position - length = palindromic_length(i, 1, new_input_string) + # does this string is ending after the previously explored end (that is r) ? + # if yes the update the new r to the last index of this + if i + k - 1 > r: + l = i - k + 1 + r = i + k - 1 # update max_length and start position - if max_length < length : - max_length = length + if max_length < length[i]: + max_length = length[i] start = i - - #create that string - for i in new_input_string[start-max_length:start+max_length+1] : + + # create that string + s = new_input_string[start - max_length // 2 : start + max_length // 2 + 1] + for i in s: if i != "|": output_string += i - + return output_string -if __name__ == '__main__': - n = input() - print(palindromic_string(n)) +if __name__ == "__main__": + import doctest + + doctest.testmod() + +""" +...a0...a1...a2.....a3......a4...a5...a6.... + +consider the string for which we are calculating the longest palindromic substring is shown above where ... +are some characters in between and right now we are calculating the length of palindromic substring with +center at a5 with following conditions : +i) we have stored the length of palindromic substring which has center at a3 (starts at l ends at r) and it + is the furthest ending till now, and it has ending after a6 +ii) a2 and a4 are equally distant from a3 so char(a2) == char(a4) +iii) a0 and a6 are equally distant from a3 so char(a0) == char(a6) +iv) a1 is corresponding equal character of a5 in palindrome with center a3 (remember that in below derivation of a4==a6) + +now for a5 we will calculate the length of palindromic substring with center as a5 but can we use previously +calculated information in some way? +Yes, look the above string we know that a5 is inside the palindrome with center a3 and previously we have +have calculated that +a0==a2 (palindrome of center a1) +a2==a4 (palindrome of center a3) +a0==a6 (palindrome of center a3) +so a4==a6 + +so we can say that palindrome at center a5 is at least as long as palindrome at center a1 +but this only holds if a0 and a6 are inside the limits of palindrome centered at a3 so finally .. + +len_of_palindrome__at(a5) = min(len_of_palindrome_at(a1), r-a5) +where a3 lies from l to r and we have to keep updating that + +and if the a5 lies outside of l,r boundary we calculate length of palindrome with bruteforce and update +l,r. + +it gives the linear time complexity just like z-function +""" diff --git a/strings/min_cost_string_conversion.py b/strings/min_cost_string_conversion.py index de7f9f727283..abc9d2c65158 100644 --- a/strings/min_cost_string_conversion.py +++ b/strings/min_cost_string_conversion.py @@ -1,121 +1,118 @@ -from __future__ import print_function - -try: - xrange #Python 2 -except NameError: - xrange = range #Python 3 - -''' +""" Algorithm for calculating the most cost-efficient sequence for converting one string into another. The only allowed operations are ---Copy character with cost cC ---Replace character with cost cR ---Delete character with cost cD ---Insert character with cost cI -''' +""" + + def compute_transform_tables(X, Y, cC, cR, cD, cI): - X = list(X) - Y = list(Y) - m = len(X) - n = len(Y) + X = list(X) + Y = list(Y) + m = len(X) + n = len(Y) + + costs = [[0 for _ in range(n + 1)] for _ in range(m + 1)] + ops = [[0 for _ in range(n + 1)] for _ in range(m + 1)] - costs = [[0 for _ in xrange(n+1)] for _ in xrange(m+1)] - ops = [[0 for _ in xrange(n+1)] for _ in xrange(m+1)] + for i in range(1, m + 1): + costs[i][0] = i * cD + ops[i][0] = "D%c" % X[i - 1] - for i in xrange(1, m+1): - costs[i][0] = i*cD - ops[i][0] = 'D%c' % X[i-1] + for i in range(1, n + 1): + costs[0][i] = i * cI + ops[0][i] = "I%c" % Y[i - 1] - for i in xrange(1, n+1): - costs[0][i] = i*cI - ops[0][i] = 'I%c' % Y[i-1] + for i in range(1, m + 1): + for j in range(1, n + 1): + if X[i - 1] == Y[j - 1]: + costs[i][j] = costs[i - 1][j - 1] + cC + ops[i][j] = "C%c" % X[i - 1] + else: + costs[i][j] = costs[i - 1][j - 1] + cR + ops[i][j] = "R%c" % X[i - 1] + str(Y[j - 1]) - for i in xrange(1, m+1): - for j in xrange(1, n+1): - if X[i-1] == Y[j-1]: - costs[i][j] = costs[i-1][j-1] + cC - ops[i][j] = 'C%c' % X[i-1] - else: - costs[i][j] = costs[i-1][j-1] + cR - ops[i][j] = 'R%c' % X[i-1] + str(Y[j-1]) + if costs[i - 1][j] + cD < costs[i][j]: + costs[i][j] = costs[i - 1][j] + cD + ops[i][j] = "D%c" % X[i - 1] - if costs[i-1][j] + cD < costs[i][j]: - costs[i][j] = costs[i-1][j] + cD - ops[i][j] = 'D%c' % X[i-1] + if costs[i][j - 1] + cI < costs[i][j]: + costs[i][j] = costs[i][j - 1] + cI + ops[i][j] = "I%c" % Y[j - 1] - if costs[i][j-1] + cI < costs[i][j]: - costs[i][j] = costs[i][j-1] + cI - ops[i][j] = 'I%c' % Y[j-1] + return costs, ops - return costs, ops def assemble_transformation(ops, i, j): - if i == 0 and j == 0: - seq = [] - return seq - else: - if ops[i][j][0] == 'C' or ops[i][j][0] == 'R': - seq = assemble_transformation(ops, i-1, j-1) - seq.append(ops[i][j]) - return seq - elif ops[i][j][0] == 'D': - seq = assemble_transformation(ops, i-1, j) - seq.append(ops[i][j]) - return seq - else: - seq = assemble_transformation(ops, i, j-1) - seq.append(ops[i][j]) - return seq - -if __name__ == '__main__': - _, operations = compute_transform_tables('Python', 'Algorithms', -1, 1, 2, 2) - - m = len(operations) - n = len(operations[0]) - sequence = assemble_transformation(operations, m-1, n-1) - - string = list('Python') - i = 0 - cost = 0 - - with open('min_cost.txt', 'w') as file: - for op in sequence: - print(''.join(string)) - - if op[0] == 'C': - file.write('%-16s' % 'Copy %c' % op[1]) - file.write('\t\t\t' + ''.join(string)) - file.write('\r\n') - - cost -= 1 - elif op[0] == 'R': - string[i] = op[2] - - file.write('%-16s' % ('Replace %c' % op[1] + ' with ' + str(op[2]))) - file.write('\t\t' + ''.join(string)) - file.write('\r\n') - - cost += 1 - elif op[0] == 'D': - string.pop(i) - - file.write('%-16s' % 'Delete %c' % op[1]) - file.write('\t\t\t' + ''.join(string)) - file.write('\r\n') - - cost += 2 - else: - string.insert(i, op[1]) - - file.write('%-16s' % 'Insert %c' % op[1]) - file.write('\t\t\t' + ''.join(string)) - file.write('\r\n') - - cost += 2 - - i += 1 - - print(''.join(string)) - print('Cost: ', cost) - - file.write('\r\nMinimum cost: ' + str(cost)) + if i == 0 and j == 0: + seq = [] + return seq + else: + if ops[i][j][0] == "C" or ops[i][j][0] == "R": + seq = assemble_transformation(ops, i - 1, j - 1) + seq.append(ops[i][j]) + return seq + elif ops[i][j][0] == "D": + seq = assemble_transformation(ops, i - 1, j) + seq.append(ops[i][j]) + return seq + else: + seq = assemble_transformation(ops, i, j - 1) + seq.append(ops[i][j]) + return seq + + +if __name__ == "__main__": + _, operations = compute_transform_tables("Python", "Algorithms", -1, 1, 2, 2) + + m = len(operations) + n = len(operations[0]) + sequence = assemble_transformation(operations, m - 1, n - 1) + + string = list("Python") + i = 0 + cost = 0 + + with open("min_cost.txt", "w") as file: + for op in sequence: + print("".join(string)) + + if op[0] == "C": + file.write("%-16s" % "Copy %c" % op[1]) + file.write("\t\t\t" + "".join(string)) + file.write("\r\n") + + cost -= 1 + elif op[0] == "R": + string[i] = op[2] + + file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2]))) + file.write("\t\t" + "".join(string)) + file.write("\r\n") + + cost += 1 + elif op[0] == "D": + string.pop(i) + + file.write("%-16s" % "Delete %c" % op[1]) + file.write("\t\t\t" + "".join(string)) + file.write("\r\n") + + cost += 2 + else: + string.insert(i, op[1]) + + file.write("%-16s" % "Insert %c" % op[1]) + file.write("\t\t\t" + "".join(string)) + file.write("\r\n") + + cost += 2 + + i += 1 + + print("".join(string)) + print("Cost: ", cost) + + file.write("\r\nMinimum cost: " + str(cost)) diff --git a/strings/naive_string_search.py b/strings/naive_string_search.py index 04c0d8157b24..a8c2ea584399 100644 --- a/strings/naive_string_search.py +++ b/strings/naive_string_search.py @@ -7,23 +7,26 @@ n=length of main string m=length of pattern string """ -def naivePatternSearch(mainString,pattern): - patLen=len(pattern) - strLen=len(mainString) - position=[] - for i in range(strLen-patLen+1): - match_found=True + + +def naivePatternSearch(mainString, pattern): + patLen = len(pattern) + strLen = len(mainString) + position = [] + for i in range(strLen - patLen + 1): + match_found = True for j in range(patLen): - if mainString[i+j]!=pattern[j]: - match_found=False + if mainString[i + j] != pattern[j]: + match_found = False break if match_found: position.append(i) return position -mainString="ABAAABCDBBABCDDEBCABC" -pattern="ABC" -position=naivePatternSearch(mainString,pattern) + +mainString = "ABAAABCDBBABCDDEBCABC" +pattern = "ABC" +position = naivePatternSearch(mainString, pattern) print("Pattern found in position ") for x in position: - print(x) \ No newline at end of file + print(x) diff --git a/strings/rabin_karp.py b/strings/rabin_karp.py index 04a849266ead..22da0de80f4c 100644 --- a/strings/rabin_karp.py +++ b/strings/rabin_karp.py @@ -1,6 +1,11 @@ +# Numbers of alphabet which we call base +alphabet_size = 256 +# Modulus to hash a string +modulus = 1000003 + + def rabin_karp(pattern, text): """ - The Rabin-Karp Algorithm for finding a pattern within a piece of text with complexity O(nm), most efficient when it is used with multiple patterns as it is able to check if any of a set of patterns match a section of text in o(1) given the precomputed hashes. @@ -12,22 +17,42 @@ def rabin_karp(pattern, text): 2) Step through the text one character at a time passing a window with the same length as the pattern calculating the hash of the text within the window compare it with the hash of the pattern. Only testing equality if the hashes match - """ p_len = len(pattern) - p_hash = hash(pattern) + t_len = len(text) + if p_len > t_len: + return False + + p_hash = 0 + text_hash = 0 + modulus_power = 1 - for i in range(0, len(text) - (p_len - 1)): + # Calculating the hash of pattern and substring of text + for i in range(p_len): + p_hash = (ord(pattern[i]) + p_hash * alphabet_size) % modulus + text_hash = (ord(text[i]) + text_hash * alphabet_size) % modulus + if i == p_len - 1: + continue + modulus_power = (modulus_power * alphabet_size) % modulus - # written like this t - text_hash = hash(text[i:i + p_len]) - if text_hash == p_hash and \ - text[i:i + p_len] == pattern: + for i in range(0, t_len - p_len + 1): + if text_hash == p_hash and text[i : i + p_len] == pattern: return True + if i == t_len - p_len: + continue + # Calculate the https://en.wikipedia.org/wiki/Rolling_hash + text_hash = ( + (text_hash - ord(text[i]) * modulus_power) * alphabet_size + + ord(text[i + p_len]) + ) % modulus return False -if __name__ == '__main__': +def test_rabin_karp(): + """ + >>> test_rabin_karp() + Success. + """ # Test 1) pattern = "abc1abc12" text1 = "alskfjaldsabc1abc1abc12k23adsfabcabc" @@ -48,3 +73,15 @@ def rabin_karp(pattern, text): pattern = "abcdabcy" text = "abcxabcdabxabcdabcdabcy" assert rabin_karp(pattern, text) + + # Test 5) + pattern = "Lü" + text = "Lüsai" + assert rabin_karp(pattern, text) + pattern = "Lue" + assert not rabin_karp(pattern, text) + print("Success.") + + +if __name__ == "__main__": + test_rabin_karp() diff --git a/strings/remove_duplicate.py b/strings/remove_duplicate.py new file mode 100644 index 000000000000..0462292b78d2 --- /dev/null +++ b/strings/remove_duplicate.py @@ -0,0 +1,20 @@ +# Created by sarathkaul on 14/11/19 + + +def remove_duplicates(sentence: str) -> str: + """ + Reomove duplicates from sentence + >>> remove_duplicates("Python is great and Java is also great") + 'Java Python also and great is' + """ + sen_list = sentence.split(" ") + check = set() + + for a_word in sen_list: + check.add(a_word) + + return " ".join(sorted(check)) + + +if __name__ == "__main__": + print(remove_duplicates("INPUT_SENTENCE")) diff --git a/strings/reverse_words.py b/strings/reverse_words.py new file mode 100644 index 000000000000..6b5cc6b04039 --- /dev/null +++ b/strings/reverse_words.py @@ -0,0 +1,21 @@ +# Created by sarathkaul on 18/11/19 +# Edited by farnswj1 on 4/4/20 + + +def reverse_words(input_str: str) -> str: + """ + Reverses words in a given string + >>> sentence = "I love Python" + >>> reverse_words(sentence) == " ".join(sentence.split()[::-1]) + True + >>> reverse_words(sentence) + 'Python love I' + """ + input_str = input_str.split(" ") + new_str = list() + + return " ".join(reversed(input_str)) + + +if __name__ == "__main__": + print(reverse_words("INPUT STRING")) diff --git a/strings/split.py b/strings/split.py new file mode 100644 index 000000000000..d5bff316429f --- /dev/null +++ b/strings/split.py @@ -0,0 +1,33 @@ +def split(string: str, separator: str = " ") -> list: + """ + Will split the string up into all the values separated by the separator (defaults to spaces) + + >>> split("apple#banana#cherry#orange",separator='#') + ['apple', 'banana', 'cherry', 'orange'] + + >>> split("Hello there") + ['Hello', 'there'] + + >>> split("11/22/63",separator = '/') + ['11', '22', '63'] + + >>> split("12:43:39",separator = ":") + ['12', '43', '39'] + """ + + split_words = [] + + last_index = 0 + for index, char in enumerate(string): + if char == separator: + split_words.append(string[last_index:index]) + last_index = index + 1 + elif index + 1 == len(string): + split_words.append(string[last_index : index + 1]) + return split_words + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/upper.py b/strings/upper.py new file mode 100644 index 000000000000..59b16096af0b --- /dev/null +++ b/strings/upper.py @@ -0,0 +1,26 @@ +def upper(word: str) -> str: + """ + Will convert the entire string to uppercase letters + + >>> upper("wow") + 'WOW' + >>> upper("Hello") + 'HELLO' + >>> upper("WHAT") + 'WHAT' + + >>> upper("wh[]32") + 'WH[]32' + """ + + # converting to ascii value int value and checking to see if char is a lower letter + # if it is a capital letter it is getting shift by 32 which makes it a capital case letter + return "".join( + chr(ord(char) - 32) if 97 <= ord(char) <= 122 else char for char in word + ) + + +if __name__ == "__main__": + from doctest import testmod + + testmod() diff --git a/strings/word_occurrence.py b/strings/word_occurrence.py new file mode 100644 index 000000000000..ef612e12dfa4 --- /dev/null +++ b/strings/word_occurrence.py @@ -0,0 +1,26 @@ +# Created by sarathkaul on 17/11/19 +# Modified by Arkadip Bhattacharya(@darkmatter18) on 20/04/2020 +from collections import defaultdict + + +def word_occurence(sentence: str) -> dict: + """ + >>> from collections import Counter + >>> SENTENCE = "a b A b c b d b d e f e g e h e i e j e 0" + >>> occurence_dict = word_occurence(SENTENCE) + >>> all(occurence_dict[word] == count for word, count + ... in Counter(SENTENCE.split()).items()) + True + >>> dict(word_occurence("Two spaces")) + {'Two': 1, 'spaces': 1} + """ + occurrence = defaultdict(int) + # Creating a dictionary containing count of each word + for word in sentence.split(): + occurrence[word] += 1 + return occurrence + + +if __name__ == "__main__": + for word, count in word_occurence("INPUT STRING").items(): + print(f"{word}: {count}") diff --git a/traversals/binary_tree_traversals.py b/traversals/binary_tree_traversals.py index 393664579146..c522ecebc0ff 100644 --- a/traversals/binary_tree_traversals.py +++ b/traversals/binary_tree_traversals.py @@ -1,14 +1,8 @@ """ -This is pure python implementation of tree traversal algorithms +This is pure Python implementation of tree traversal algorithms """ -from __future__ import print_function - import queue - -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 +from typing import List class TreeNode: @@ -20,35 +14,45 @@ def __init__(self, data): def build_tree(): print("\n********Press N to stop entering at any point of time********\n") - print("Enter the value of the root node: ", end="") - check = raw_input().strip().lower() - if check == 'n': + check = input("Enter the value of the root node: ").strip().lower() or "n" + if check == "n": return None - data = int(check) - q = queue.Queue() - tree_node = TreeNode(data) + q: queue.Queue = queue.Queue() + tree_node = TreeNode(int(check)) q.put(tree_node) while not q.empty(): node_found = q.get() - print("Enter the left node of %s: " % node_found.data, end="") - check = raw_input().strip().lower() - if check == 'n': + msg = "Enter the left node of %s: " % node_found.data + check = input(msg).strip().lower() or "n" + if check == "n": return tree_node - left_data = int(check) - left_node = TreeNode(left_data) + left_node = TreeNode(int(check)) node_found.left = left_node q.put(left_node) - print("Enter the right node of %s: " % node_found.data, end="") - check = raw_input().strip().lower() - if check == 'n': + msg = "Enter the right node of %s: " % node_found.data + check = input(msg).strip().lower() or "n" + if check == "n": return tree_node - right_data = int(check) - right_node = TreeNode(right_data) + right_node = TreeNode(int(check)) node_found.right = right_node q.put(right_node) -def pre_order(node): +def pre_order(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> pre_order(root) + 1 2 4 5 3 6 7 + """ if not isinstance(node, TreeNode) or not node: return print(node.data, end=" ") @@ -56,7 +60,21 @@ def pre_order(node): pre_order(node.right) -def in_order(node): +def in_order(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> in_order(root) + 4 2 5 1 6 3 7 + """ if not isinstance(node, TreeNode) or not node: return in_order(node.left) @@ -64,7 +82,21 @@ def in_order(node): in_order(node.right) -def post_order(node): +def post_order(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> post_order(root) + 4 5 2 6 7 3 1 + """ if not isinstance(node, TreeNode) or not node: return post_order(node.left) @@ -72,10 +104,24 @@ def post_order(node): print(node.data, end=" ") -def level_order(node): +def level_order(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> level_order(root) + 1 2 3 4 5 6 7 + """ if not isinstance(node, TreeNode) or not node: return - q = queue.Queue() + q: queue.Queue = queue.Queue() q.put(node) while not q.empty(): node_dequeued = q.get() @@ -86,10 +132,26 @@ def level_order(node): q.put(node_dequeued.right) -def level_order_actual(node): +def level_order_actual(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> level_order_actual(root) + 1 + 2 3 + 4 5 6 7 + """ if not isinstance(node, TreeNode) or not node: return - q = queue.Queue() + q: queue.Queue = queue.Queue() q.put(node) while not q.empty(): list = [] @@ -106,10 +168,24 @@ def level_order_actual(node): # iteration version -def pre_order_iter(node): +def pre_order_iter(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> pre_order_iter(root) + 1 2 4 5 3 6 7 + """ if not isinstance(node, TreeNode) or not node: return - stack = [] + stack: List[TreeNode] = [] n = node while n or stack: while n: # start from root node, find its left child @@ -122,10 +198,24 @@ def pre_order_iter(node): n = n.right -def in_order_iter(node): +def in_order_iter(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> in_order_iter(root) + 4 2 5 1 6 3 7 + """ if not isinstance(node, TreeNode) or not node: return - stack = [] + stack: List[TreeNode] = [] n = node while n or stack: while n: @@ -136,7 +226,21 @@ def in_order_iter(node): n = n.right -def post_order_iter(node): +def post_order_iter(node: TreeNode) -> None: + """ + >>> root = TreeNode(1) + >>> tree_node2 = TreeNode(2) + >>> tree_node3 = TreeNode(3) + >>> tree_node4 = TreeNode(4) + >>> tree_node5 = TreeNode(5) + >>> tree_node6 = TreeNode(6) + >>> tree_node7 = TreeNode(7) + >>> root.left, root.right = tree_node2, tree_node3 + >>> tree_node2.left, tree_node2.right = tree_node4 , tree_node5 + >>> tree_node3.left, tree_node3.right = tree_node6 , tree_node7 + >>> post_order_iter(root) + 4 5 2 6 7 3 1 + """ if not isinstance(node, TreeNode) or not node: return stack1, stack2 = [], [] @@ -153,38 +257,48 @@ def post_order_iter(node): print(stack2.pop().data, end=" ") -if __name__ == '__main__': - print("\n********* Binary Tree Traversals ************\n") +def prompt(s: str = "", width=50, char="*") -> str: + if not s: + return "\n" + width * char + left, extra = divmod(width - len(s) - 2, 2) + return f"{left * char} {s} {(left + extra) * char}" + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + print(prompt("Binary Tree Traversals")) node = build_tree() - print("\n********* Pre Order Traversal ************") + print(prompt("Pre Order Traversal")) pre_order(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* In Order Traversal ************") + print(prompt("In Order Traversal")) in_order(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* Post Order Traversal ************") + print(prompt("Post Order Traversal")) post_order(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* Level Order Traversal ************") + print(prompt("Level Order Traversal")) level_order(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* Actual Level Order Traversal ************") + print(prompt("Actual Level Order Traversal")) level_order_actual(node) - print("\n******************************************\n") + print("*" * 50 + "\n") - print("\n********* Pre Order Traversal - Iteration Version ************") + print(prompt("Pre Order Traversal - Iteration Version")) pre_order_iter(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* In Order Traversal - Iteration Version ************") + print(prompt("In Order Traversal - Iteration Version")) in_order_iter(node) - print("\n******************************************\n") + print(prompt() + "\n") - print("\n********* Post Order Traversal - Iteration Version ************") + print(prompt("Post Order Traversal - Iteration Version")) post_order_iter(node) - print("\n******************************************\n") + print(prompt()) diff --git a/web_programming/crawl_google_results.py b/web_programming/crawl_google_results.py new file mode 100644 index 000000000000..7d2be7c03c22 --- /dev/null +++ b/web_programming/crawl_google_results.py @@ -0,0 +1,25 @@ +import sys +import webbrowser + +from bs4 import BeautifulSoup +from fake_useragent import UserAgent +import requests + + +if __name__ == "__main__": + print("Googling.....") + url = "https://www.google.com/search?q=" + " ".join(sys.argv[1:]) + res = requests.get(url, headers={"UserAgent": UserAgent().random}) + # res.raise_for_status() + with open("project1a.html", "wb") as out_file: # only for knowing the class + for data in res.iter_content(10000): + out_file.write(data) + soup = BeautifulSoup(res.text, "html.parser") + links = list(soup.select(".eZt8xd"))[:5] + + print(len(links)) + for link in links: + if link.text == "Maps": + webbrowser.open(link.get("href")) + else: + webbrowser.open(f"http://google.com{link.get('href')}") diff --git a/web_programming/current_stock_price.py b/web_programming/current_stock_price.py new file mode 100644 index 000000000000..df44da4ef351 --- /dev/null +++ b/web_programming/current_stock_price.py @@ -0,0 +1,14 @@ +import requests +from bs4 import BeautifulSoup + + +def stock_price(symbol: str = "AAPL") -> str: + url = f"https://in.finance.yahoo.com/quote/{symbol}?s={symbol}" + soup = BeautifulSoup(requests.get(url).text, "html.parser") + class_ = "My(6px) Pos(r) smartphone_Mt(6px)" + return soup.find("div", class_=class_).find("span").text + + +if __name__ == "__main__": + for symbol in "AAPL AMZN IBM GOOG MSFT ORCL".split(): + print(f"Current {symbol:<4} stock price is {stock_price(symbol):>8}") diff --git a/web_programming/emails_from_url.py b/web_programming/emails_from_url.py new file mode 100644 index 000000000000..01dee274f015 --- /dev/null +++ b/web_programming/emails_from_url.py @@ -0,0 +1,103 @@ +"""Get the site emails from URL.""" +__author__ = "Muhammad Umer Farooq" +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Muhammad Umer Farooq" +__email__ = "contact@muhammadumerfarooq.me" +__status__ = "Alpha" + +import re +from html.parser import HTMLParser +from urllib import parse + +import requests + + +class Parser(HTMLParser): + def __init__(self, domain: str): + HTMLParser.__init__(self) + self.data = [] + self.domain = domain + + def handle_starttag(self, tag: str, attrs: str) -> None: + """ + This function parse html to take takes url from tags + """ + # Only parse the 'anchor' tag. + if tag == "a": + # Check the list of defined attributes. + for name, value in attrs: + # If href is defined, and not empty nor # print it. + if name == "href" and value != "#" and value != "": + # If not already in data. + if value not in self.data: + url = parse.urljoin(self.domain, value) + self.data.append(url) + + +# Get main domain name (example.com) +def get_domain_name(url: str) -> str: + """ + This function get the main domain name + + >>> get_domain_name("https://a.b.c.d/e/f?g=h,i=j#k") + 'c.d' + >>> get_domain_name("Not a URL!") + '' + """ + return ".".join(get_sub_domain_name(url).split(".")[-2:]) + + +# Get sub domain name (sub.example.com) +def get_sub_domain_name(url: str) -> str: + """ + >>> get_sub_domain_name("https://a.b.c.d/e/f?g=h,i=j#k") + 'a.b.c.d' + >>> get_sub_domain_name("Not a URL!") + '' + """ + return parse.urlparse(url).netloc + + +def emails_from_url(url: str = "https://github.com") -> list: + """ + This function takes url and return all valid urls + """ + # Get the base domain from the url + domain = get_domain_name(url) + + # Initialize the parser + parser = Parser(domain) + + try: + # Open URL + r = requests.get(url) + + # pass the raw HTML to the parser to get links + parser.feed(r.text) + + # Get links and loop through + valid_emails = set() + for link in parser.data: + # open URL. + # read = requests.get(link) + try: + read = requests.get(link) + # Get the valid email. + emails = re.findall("[a-zA-Z0-9]+@" + domain, read.text) + # If not in list then append it. + for email in emails: + valid_emails.add(email) + except ValueError: + pass + except ValueError: + exit(-1) + + # Finally return a sorted list of email addresses with no duplicates. + return sorted(valid_emails) + + +if __name__ == "__main__": + emails = emails_from_url("https://github.com") + print(f"{len(emails)} emails found:") + print("\n".join(sorted(emails))) diff --git a/web_programming/fetch_bbc_news.py b/web_programming/fetch_bbc_news.py new file mode 100644 index 000000000000..7f8bc57b69f5 --- /dev/null +++ b/web_programming/fetch_bbc_news.py @@ -0,0 +1,17 @@ +# Created by sarathkaul on 12/11/19 + +import requests + +_NEWS_API = "https://newsapi.org/v1/articles?source=bbc-news&sortBy=top&apiKey=" + + +def fetch_bbc_news(bbc_news_api_key: str) -> None: + # fetching a list of articles in json format + bbc_news_page = requests.get(_NEWS_API + bbc_news_api_key).json() + # each article in the list is a dict + for i, article in enumerate(bbc_news_page["articles"], 1): + print(f"{i}.) {article['title']}") + + +if __name__ == "__main__": + fetch_bbc_news(bbc_news_api_key="") diff --git a/web_programming/fetch_github_info.py b/web_programming/fetch_github_info.py new file mode 100644 index 000000000000..f6626770e833 --- /dev/null +++ b/web_programming/fetch_github_info.py @@ -0,0 +1,17 @@ +# Created by sarathkaul on 14/11/19 + +import requests + +_GITHUB_API = "https://api.github.com/user" + + +def fetch_github_info(auth_user: str, auth_pass: str) -> None: + # fetching github info using requests + info = requests.get(_GITHUB_API, auth=(auth_user, auth_pass)) + + for a_info, a_detail in info.json().items(): + print(f"{a_info}: {a_detail}") + + +if __name__ == "main": + fetch_github_info("", "") diff --git a/web_programming/get_imdb_top_250_movies_csv.py b/web_programming/get_imdb_top_250_movies_csv.py new file mode 100644 index 000000000000..811c21fb00e4 --- /dev/null +++ b/web_programming/get_imdb_top_250_movies_csv.py @@ -0,0 +1,29 @@ +import csv +from typing import Dict + +import requests +from bs4 import BeautifulSoup + + +def get_imdb_top_250_movies(url: str = "") -> Dict[str, float]: + url = url or "https://www.imdb.com/chart/top/?ref_=nv_mv_250" + soup = BeautifulSoup(requests.get(url).text, "html.parser") + titles = soup.find_all("td", attrs="titleColumn") + ratings = soup.find_all("td", class_="ratingColumn imdbRating") + return { + title.a.text: float(rating.strong.text) + for title, rating in zip(titles, ratings) + } + + +def write_movies(filename: str = "IMDb_Top_250_Movies.csv") -> None: + movies = get_imdb_top_250_movies() + with open(filename, "w", newline="") as out_file: + writer = csv.writer(out_file) + writer.writerow(["Movie title", "IMDb rating"]) + for title, rating in movies.items(): + writer.writerow([title, rating]) + + +if __name__ == "__main__": + write_movies() diff --git a/web_programming/get_imdbtop.py b/web_programming/get_imdbtop.py new file mode 100644 index 000000000000..522e423b4eab --- /dev/null +++ b/web_programming/get_imdbtop.py @@ -0,0 +1,20 @@ +from bs4 import BeautifulSoup +import requests + + +def imdb_top(imdb_top_n): + base_url = ( + f"https://www.imdb.com/search/title?title_type=" + f"feature&sort=num_votes,desc&count={imdb_top_n}" + ) + source = BeautifulSoup(requests.get(base_url).content, "html.parser") + for m in source.findAll("div", class_="lister-item mode-advanced"): + print("\n" + m.h3.a.text) # movie's name + print(m.find("span", attrs={"class": "genre"}).text) # genre + print(m.strong.text) # movie's rating + print(f"https://www.imdb.com{m.a.get('href')}") # movie's page link + print("*" * 40) + + +if __name__ == "__main__": + imdb_top(input("How many movies would you like to see? ")) diff --git a/web_programming/slack_message.py b/web_programming/slack_message.py new file mode 100644 index 000000000000..8ea9d5d0add2 --- /dev/null +++ b/web_programming/slack_message.py @@ -0,0 +1,19 @@ +# Created by sarathkaul on 12/11/19 + +import requests + + +def send_slack_message(message_body: str, slack_url: str) -> None: + headers = {"Content-Type": "application/json"} + response = requests.post(slack_url, json={"text": message_body}, headers=headers) + if response.status_code != 200: + raise ValueError( + f"Request to slack returned an error {response.status_code}, " + f"the response is:\n{response.text}" + ) + + +if __name__ == "main": + # Set the slack url to the one provided by Slack when you create the webhook at + # https://my.slack.com/services/new/incoming-webhook/ + send_slack_message("", "")