From f0bca255ee7982f599f6c41743fd96ccfac2c878 Mon Sep 17 00:00:00 2001 From: Donne Martin Date: Mon, 27 Mar 2017 05:22:19 -0400 Subject: [PATCH] Add k max profit challenge --- recursion_dynamic/max_profit_k/__init__.py | 0 .../max_profit_k/max_profit_challenge.ipynb | 240 ++++++++++++ .../max_profit_k/max_profit_solution.ipynb | 346 ++++++++++++++++++ .../max_profit_k/test_max_profit.py | 45 +++ 4 files changed, 631 insertions(+) create mode 100644 recursion_dynamic/max_profit_k/__init__.py create mode 100644 recursion_dynamic/max_profit_k/max_profit_challenge.ipynb create mode 100644 recursion_dynamic/max_profit_k/max_profit_solution.ipynb create mode 100644 recursion_dynamic/max_profit_k/test_max_profit.py diff --git a/recursion_dynamic/max_profit_k/__init__.py b/recursion_dynamic/max_profit_k/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/recursion_dynamic/max_profit_k/max_profit_challenge.ipynb b/recursion_dynamic/max_profit_k/max_profit_challenge.ipynb new file mode 100644 index 0000000..94291e6 --- /dev/null +++ b/recursion_dynamic/max_profit_k/max_profit_challenge.ipynb @@ -0,0 +1,240 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook was prepared by [Donne Martin](https://github.com/donnemartin). Source and license info is on [GitHub](https://github.com/donnemartin/interactive-coding-challenges)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Challenge Notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: Given a list of stock prices on each consecutive day, determine the max profits with k transactions.\n", + "\n", + "* [Constraints](#Constraints)\n", + "* [Test Cases](#Test-Cases)\n", + "* [Algorithm](#Algorithm)\n", + "* [Code](#Code)\n", + "* [Unit Test](#Unit-Test)\n", + "* [Solution Notebook](#Solution-Notebook)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constraints\n", + "\n", + "* Is k the number of sell transactions?\n", + " * Yes\n", + "* Can we assume the prices input is an array of ints?\n", + " * Yes\n", + "* Can we assume the inputs are valid?\n", + " * No\n", + "* If the prices are all decreasing and there is no opportunity to make a profit, do we just return 0?\n", + " * Yes\n", + "* Should the output be the max profit and days to buy and sell?\n", + " * Yes\n", + "* Can we assume this fits memory?\n", + " * Yes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test Cases\n", + "\n", + "
\n",
+    "* Prices: None or k: None -> None\n",
+    "* Prices: [] or k <= 0 -> []\n",
+    "* Prices: [0, -1, -2, -3, -4, -5]\n",
+    "    * (max profit, list of transactions)\n",
+    "    * (0, [])\n",
+    "* Prices: [2, 5, 7, 1, 4, 3, 1, 3] k: 3\n",
+    "    * (max profit, list of transactions)\n",
+    "    * (10, [Type.SELL day: 7 price: 3, \n",
+    "            Type.BUY  day: 6 price: 1, \n",
+    "            Type.SELL day: 4 price: 4, \n",
+    "            Type.BUY  day: 3 price: 1, \n",
+    "            Type.SELL day: 2 price: 7, \n",
+    "            Type.BUY  day: 0 price: 2])\n",
+    "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Algorithm\n", + "\n", + "Refer to the [Solution Notebook](). If you are stuck and need a hint, the solution notebook's algorithm discussion might be a good place to start." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from enum import Enum # Python 2 users: Run pip install enum34\n", + "\n", + "\n", + "class Type(Enum):\n", + " SELL = 0\n", + " BUY = 1\n", + "\n", + "\n", + "class Transaction(object):\n", + "\n", + " def __init__(self, type, day, price):\n", + " self.type = type\n", + " self.day = day\n", + " self.price = price\n", + "\n", + " def __eq__(self, other):\n", + " return self.type == other.type and \\\n", + " self.day == other.day and \\\n", + " self.price == other.price\n", + "\n", + " def __repr__(self):\n", + " return str(self.type) + ' day: ' + \\\n", + " str(self.day) + ' price: ' + \\\n", + " str(self.price)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "class StockTrader(object):\n", + "\n", + " def find_max_profit(self, prices, k):\n", + " # TODO: Implement me\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unit Test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**The following unit test is expected to fail until you solve the challenge.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# %load test_max_profit.py\n", + "from nose.tools import assert_equal\n", + "from nose.tools import assert_raises\n", + "from nose.tools import assert_true\n", + "\n", + "\n", + "class TestMaxProfit(object):\n", + "\n", + " def test_max_profit(self):\n", + " stock_trader = StockTrader()\n", + " assert_raises(TypeError, stock_trader.find_max_profit, None, None)\n", + " assert_equal(stock_trader.find_max_profit(prices=[], k=0), [])\n", + " prices = [5, 4, 3, 2, 1]\n", + " k = 3\n", + " assert_equal(stock_trader.find_max_profit(prices, k), (0, []))\n", + " prices = [2, 5, 7, 1, 4, 3, 1, 3]\n", + " profit, transactions = stock_trader.find_max_profit(prices, k)\n", + " assert_equal(profit, 10)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=7,\n", + " price=3) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=6,\n", + " price=1) in transactions)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=4,\n", + " price=4) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=3,\n", + " price=1) in transactions)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=2,\n", + " price=7) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=0,\n", + " price=2) in transactions)\n", + " print('Success: test_max_profit')\n", + "\n", + "\n", + "def main():\n", + " test = TestMaxProfit()\n", + " test.test_max_profit()\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solution Notebook\n", + "\n", + "Review the [Solution Notebook]() for a discussion on algorithms and code solutions." + ] + } + ], + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/recursion_dynamic/max_profit_k/max_profit_solution.ipynb b/recursion_dynamic/max_profit_k/max_profit_solution.ipynb new file mode 100644 index 0000000..fe9b866 --- /dev/null +++ b/recursion_dynamic/max_profit_k/max_profit_solution.ipynb @@ -0,0 +1,346 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook was prepared by [Donne Martin](https://github.com/donnemartin). Source and license info is on [GitHub](https://github.com/donnemartin/interactive-coding-challenges)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solution Notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: Given a list of stock prices on each consecutive day, determine the max profits with k transactions.\n", + "\n", + "* [Constraints](#Constraints)\n", + "* [Test Cases](#Test-Cases)\n", + "* [Algorithm](#Algorithm)\n", + "* [Code](#Code)\n", + "* [Unit Test](#Unit-Test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constraints\n", + "\n", + "* Is k the number of sell transactions?\n", + " * Yes\n", + "* Can we assume the prices input is an array of ints?\n", + " * Yes\n", + "* Can we assume the inputs are valid?\n", + " * No\n", + "* If the prices are all decreasing and there is no opportunity to make a profit, do we just return 0?\n", + " * Yes\n", + "* Should the output be the max profit and days to buy and sell?\n", + " * Yes\n", + "* Can we assume this fits memory?\n", + " * Yes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test Cases\n", + "\n", + "
\n",
+    "* Prices: None or k: None -> None\n",
+    "* Prices: [] or k <= 0 -> []\n",
+    "* Prices: [0, -1, -2, -3, -4, -5]\n",
+    "    * (max profit, list of transactions)\n",
+    "    * (0, [])\n",
+    "* Prices: [2, 5, 7, 1, 4, 3, 1, 3] k: 3\n",
+    "    * (max profit, list of transactions)\n",
+    "    * (10, [Type.SELL day: 7 price: 3, \n",
+    "            Type.BUY  day: 6 price: 1, \n",
+    "            Type.SELL day: 4 price: 4, \n",
+    "            Type.BUY  day: 3 price: 1, \n",
+    "            Type.SELL day: 2 price: 7, \n",
+    "            Type.BUY  day: 0 price: 2])\n",
+    "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Algorithm\n", + "\n", + "We'll use bottom up dynamic programming to build a table.\n", + "\n", + "
\n",
+    "\n",
+    "The rows (i) represent the prices.\n",
+    "The columns (j) represent the number of transactions (k).\n",
+    "\n",
+    "T[i][j] = max(T[i][j - 1],\n",
+    "              prices[j] - price[m] + T[i - 1][m])\n",
+    "\n",
+    "m = 0...j-1\n",
+    "\n",
+    "      0   1   2   3   4   5   6   7\n",
+    "--------------------------------------\n",
+    "|   | 2 | 5 | 7 | 1 | 4 | 3 | 1 | 3  |\n",
+    "--------------------------------------\n",
+    "| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0  |\n",
+    "| 1 | 0 | 3 | 5 | 5 | 5 | 5 | 5 | 5  |\n",
+    "| 2 | 0 | 3 | 5 | 5 | 8 | 8 | 8 | 8  |\n",
+    "| 3 | 0 | 3 | 5 | 5 | 8 | 8 | 8 | 10 |\n",
+    "--------------------------------------\n",
+    "\n",
+    "Optimization:\n",
+    "\n",
+    "max_diff = max(max_diff,\n",
+    "               T[i - 1][j - 1] - prices[j - 1])\n",
+    "\n",
+    "T[i][j] = max(T[i][j - 1],\n",
+    "              prices[j] + max_diff)\n",
+    "\n",
+    "
\n", + "\n", + "Complexity:\n", + "* Time: O(n * k)\n", + "* Space: O(n * k)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from enum import Enum # Python 2 users: Run pip install enum34\n", + "\n", + "\n", + "class Type(Enum):\n", + " SELL = 0\n", + " BUY = 1\n", + "\n", + "\n", + "class Transaction(object):\n", + "\n", + " def __init__(self, type, day, price):\n", + " self.type = type\n", + " self.day = day\n", + " self.price = price\n", + "\n", + " def __eq__(self, other):\n", + " return self.type == other.type and \\\n", + " self.day == other.day and \\\n", + " self.price == other.price\n", + "\n", + " def __repr__(self):\n", + " return str(self.type) + ' day: ' + \\\n", + " str(self.day) + ' price: ' + \\\n", + " str(self.price)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "\n", + "class StockTrader(object):\n", + "\n", + " def find_max_profit(self, prices, k):\n", + " if prices is None or k is None:\n", + " raise TypeError('prices or k cannot be None')\n", + " if not prices or k <= 0:\n", + " return []\n", + " num_rows = k + 1 # 0th transaction for dp table\n", + " num_cols = len(prices)\n", + " T = [[None] * num_cols for _ in range(num_rows)]\n", + " for i in range(num_rows):\n", + " for j in range(num_cols):\n", + " if i == 0 or j == 0:\n", + " T[i][j] = 0\n", + " continue\n", + " max_profit = -sys.maxsize\n", + " for m in range(j):\n", + " profit = prices[j] - prices[m] + T[i - 1][m]\n", + " if profit > max_profit:\n", + " max_profit = profit\n", + " T[i][j] = max(T[i][j - 1], max_profit)\n", + " return self._find_max_profit_transactions(T, prices)\n", + "\n", + " def find_max_profit_optimized(self, prices, k):\n", + " if prices is None or k is None:\n", + " raise TypeError('prices or k cannot be None')\n", + " if not prices or k <= 0:\n", + " return []\n", + " num_rows = k + 1\n", + " num_cols = len(prices)\n", + " T = [[None] * num_cols for _ in range(num_rows)]\n", + " for i in range(num_rows):\n", + " max_diff = prices[0] * -1\n", + " for j in range(num_cols):\n", + " if i == 0 or j == 0:\n", + " T[i][j] = 0\n", + " continue\n", + " max_diff = max(\n", + " max_diff,\n", + " T[i - 1][j - 1] - prices[j - 1])\n", + " T[i][j] = max(\n", + " T[i][j - 1],\n", + " prices[j] + max_diff)\n", + " return self._find_max_profit_transactions(T, prices)\n", + "\n", + " def _find_max_profit_transactions(self, T, prices):\n", + " results = []\n", + " i = len(T) - 1\n", + " j = len(T[0]) - 1\n", + " max_profit = T[i][j]\n", + " while i != 0 and j != 0:\n", + " if T[i][j] == T[i][j - 1]:\n", + " j -= 1\n", + " else:\n", + " sell_price = prices[j]\n", + " results.append(Transaction(Type.SELL, j, sell_price))\n", + " profit = T[i][j] - T[i - 1][j - 1]\n", + " i -= 1\n", + " j -= 1\n", + " for m in range(j + 1)[::-1]:\n", + " if sell_price - prices[m] == profit:\n", + " results.append(Transaction(Type.BUY, m, prices[m]))\n", + " break\n", + " return (max_profit, results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unit Test" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting test_max_profit.py\n" + ] + } + ], + "source": [ + "%%writefile test_max_profit.py\n", + "from nose.tools import assert_equal\n", + "from nose.tools import assert_raises\n", + "from nose.tools import assert_true\n", + "\n", + "\n", + "class TestMaxProfit(object):\n", + "\n", + " def test_max_profit(self):\n", + " stock_trader = StockTrader()\n", + " assert_raises(TypeError, stock_trader.find_max_profit, None, None)\n", + " assert_equal(stock_trader.find_max_profit(prices=[], k=0), [])\n", + " prices = [5, 4, 3, 2, 1]\n", + " k = 3\n", + " assert_equal(stock_trader.find_max_profit(prices, k), (0, []))\n", + " prices = [2, 5, 7, 1, 4, 3, 1, 3]\n", + " profit, transactions = stock_trader.find_max_profit(prices, k)\n", + " assert_equal(profit, 10)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=7,\n", + " price=3) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=6,\n", + " price=1) in transactions)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=4,\n", + " price=4) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=3,\n", + " price=1) in transactions)\n", + " assert_true(Transaction(Type.SELL,\n", + " day=2,\n", + " price=7) in transactions)\n", + " assert_true(Transaction(Type.BUY,\n", + " day=0,\n", + " price=2) in transactions)\n", + " print('Success: test_max_profit')\n", + "\n", + "\n", + "def main():\n", + " test = TestMaxProfit()\n", + " test.test_max_profit()\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " main()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Success: test_max_profit\n" + ] + } + ], + "source": [ + "%run -i test_max_profit.py" + ] + } + ], + "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.4.3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/recursion_dynamic/max_profit_k/test_max_profit.py b/recursion_dynamic/max_profit_k/test_max_profit.py new file mode 100644 index 0000000..bd21cc1 --- /dev/null +++ b/recursion_dynamic/max_profit_k/test_max_profit.py @@ -0,0 +1,45 @@ +from nose.tools import assert_equal +from nose.tools import assert_raises +from nose.tools import assert_true + + +class TestMaxProfit(object): + + def test_max_profit(self): + stock_trader = StockTrader() + assert_raises(TypeError, stock_trader.find_max_profit, None, None) + assert_equal(stock_trader.find_max_profit(prices=[], k=0), []) + prices = [5, 4, 3, 2, 1] + k = 3 + assert_equal(stock_trader.find_max_profit(prices, k), (0, [])) + prices = [2, 5, 7, 1, 4, 3, 1, 3] + profit, transactions = stock_trader.find_max_profit(prices, k) + assert_equal(profit, 10) + assert_true(Transaction(Type.SELL, + day=7, + price=3) in transactions) + assert_true(Transaction(Type.BUY, + day=6, + price=1) in transactions) + assert_true(Transaction(Type.SELL, + day=4, + price=4) in transactions) + assert_true(Transaction(Type.BUY, + day=3, + price=1) in transactions) + assert_true(Transaction(Type.SELL, + day=2, + price=7) in transactions) + assert_true(Transaction(Type.BUY, + day=0, + price=2) in transactions) + print('Success: test_max_profit') + + +def main(): + test = TestMaxProfit() + test.test_max_profit() + + +if __name__ == '__main__': + main() \ No newline at end of file