{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Subsequence strings\n", "\n", "Given two strings `a` and `b`, is `a` a subsequence of `b`?\n", "\n", "For example,\n", "Given:\n", "a = \"aabcc\",\n", "b = \"dabaabcacb\",\n", "\n", "return True : dAbAaBCaCb\n", "\n" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import random" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "s1 = \"aabcc\"\n", "s2t = \"dabaabcacb\"\n", "s2f = \"dabacc\"\n", "\n", "s2 = s2t" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`dp_table[i, j]` is True if first `i` characters of `s1` can be found in first `j` characters of `s2`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "{(0, 0): False,\n", " (0, 1): False,\n", " (0, 2): False,\n", " (0, 3): False,\n", " (0, 4): False,\n", " (0, 5): False,\n", " (0, 6): False,\n", " (0, 7): False,\n", " (0, 8): False,\n", " (0, 9): False,\n", " (0, 10): False,\n", " (1, 0): False,\n", " (1, 1): False,\n", " (1, 2): False,\n", " (1, 3): False,\n", " (1, 4): False,\n", " (1, 5): False,\n", " (1, 6): False,\n", " (1, 7): False,\n", " (1, 8): False,\n", " (1, 9): False,\n", " (1, 10): False,\n", " (2, 0): False,\n", " (2, 1): False,\n", " (2, 2): False,\n", " (2, 3): False,\n", " (2, 4): False,\n", " (2, 5): False,\n", " (2, 6): False,\n", " (2, 7): False,\n", " (2, 8): False,\n", " (2, 9): False,\n", " (2, 10): False,\n", " (3, 0): False,\n", " (3, 1): False,\n", " (3, 2): False,\n", " (3, 3): False,\n", " (3, 4): False,\n", " (3, 5): False,\n", " (3, 6): False,\n", " (3, 7): False,\n", " (3, 8): False,\n", " (3, 9): False,\n", " (3, 10): False,\n", " (4, 0): False,\n", " (4, 1): False,\n", " (4, 2): False,\n", " (4, 3): False,\n", " (4, 4): False,\n", " (4, 5): False,\n", " (4, 6): False,\n", " (4, 7): False,\n", " (4, 8): False,\n", " (4, 9): False,\n", " (4, 10): False,\n", " (5, 0): False,\n", " (5, 1): False,\n", " (5, 2): False,\n", " (5, 3): False,\n", " (5, 4): False,\n", " (5, 5): False,\n", " (5, 6): False,\n", " (5, 7): False,\n", " (5, 8): False,\n", " (5, 9): False,\n", " (5, 10): False}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dp_table = {(i, j): False\n", " for i in range(len(s1)+1)\n", " for j in range(len(s2)+1)}\n", "dp_table" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def show_table(table):\n", " return '\\n'.join(\n", " ' '.join(str(table[i, j])[0] for j in sorted(set([k[1] for k in table])))\n", " for i in sorted(set([k[0] for k in table]))) " ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "F F F F F F F F F F F\n", "F F F F F F F F F F F\n", "F F F F F F F F F F F\n", "F F F F F F F F F F F\n", "F F F F F F F F F F F\n", "F F F F F F F F F F F\n" ] } ], "source": [ "print(show_table(dp_table))" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "aaaa aaababb\n", "aa 0 0 ! ! True\n", "aa 0 1 ! ! True\n", "aa 0 2 ! ! True\n", "aa 0 3 ! ! True\n", "aa 0 4 ! ! True\n", "aa 0 5 ! ! True\n", "aa 0 6 ! ! True\n", "aa 0 7 ! ! True\n", "s1 1 1 a a True\n", "s2 1 2 a a True\n", "s1 1 2 a a True\n", "s2 1 3 a a True\n", "s1 1 3 a a True\n", "s2 1 4 a b True\n", "s2 1 5 a a True\n", "s1 1 5 a a True\n", "s2 1 6 a b True\n", "s2 1 7 a b True\n", "s1 2 2 a a True\n", "s2 2 3 a a True\n", "s1 2 3 a a True\n", "s2 2 4 a b True\n", "s2 2 5 a a True\n", "s1 2 5 a a True\n", "s2 2 6 a b True\n", "s2 2 7 a b True\n", "s1 3 3 a a True\n", "s2 3 4 a b True\n", "s2 3 5 a a True\n", "s1 3 5 a a True\n", "s2 3 6 a b True\n", "s2 3 7 a b True\n", "xx 4 4 a b False\n", "s1 4 5 a a True\n", "s2 4 6 a b True\n", "s2 4 7 a b True\n", "T T T T T T T T\n", "F T T T T T T T\n", "F F T T T T T T\n", "F F F T T T T T\n", "F F F F F T T T\n" ] }, { "data": { "text/plain": [ "{(1, 1): (0, 0, 'a', 's1'),\n", " (1, 2): (0, 1, 'a', 's1'),\n", " (1, 3): (0, 2, 'a', 's1'),\n", " (1, 4): (1, 3, 'b', 's2'),\n", " (1, 5): (0, 4, 'a', 's1'),\n", " (1, 6): (1, 5, 'b', 's2'),\n", " (1, 7): (1, 6, 'b', 's2'),\n", " (2, 2): (1, 1, 'a', 's1'),\n", " (2, 3): (1, 2, 'a', 's1'),\n", " (2, 4): (2, 3, 'b', 's2'),\n", " (2, 5): (1, 4, 'a', 's1'),\n", " (2, 6): (2, 5, 'b', 's2'),\n", " (2, 7): (2, 6, 'b', 's2'),\n", " (3, 3): (2, 2, 'a', 's1'),\n", " (3, 4): (3, 3, 'b', 's2'),\n", " (3, 5): (2, 4, 'a', 's1'),\n", " (3, 6): (3, 5, 'b', 's2'),\n", " (3, 7): (3, 6, 'b', 's2'),\n", " (4, 5): (3, 4, 'a', 's1'),\n", " (4, 6): (4, 5, 'b', 's2'),\n", " (4, 7): (4, 6, 'b', 's2')}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s2 = s2f\n", "\n", "s1 = 'aaaa'\n", "s2 = 'aaababb'\n", "\n", "print(s1, s2)\n", "\n", "dp_table = {(i, j): False\n", " for i in range(len(s1)+1)\n", " for j in range(len(s2)+1)}\n", "\n", "backpointers = {}\n", "\n", "for i in range(len(s1)+1):\n", " for j in range(i, len(s2)+1):\n", " if i == 0 or j == 0:\n", " dp_table[i, j] = True\n", " print('aa', i, j, '!', '!', dp_table[i, j])\n", " else:\n", " # extend by character from s2\n", " if dp_table[i, j-1]:\n", " dp_table[i, j] = True\n", " backpointers[i, j] = (i, j-1, s2[j-1], 's2')\n", " print('s2', i, j, s1[i-1], s2[j-1], dp_table[i, j]) \n", " # extend by character from s1\n", " if dp_table[i-1, j-1] and s1[i-1] == s2[j-1]:\n", " dp_table[i, j] = True\n", " backpointers[i, j] = (i-1, j-1, s1[i-1], 's1') \n", " print('s1', i, j, s1[i-1], s2[j-1], dp_table[i, j])\n", " if not dp_table[i, j]:\n", " print('xx', i, j, s1[i-1], s2[j-1], dp_table[i, j])\n", "\n", "print(show_table(dp_table))\n", "backpointers" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def show_backtrace(bps):\n", " i = max([0] + [k[0] for k in bps])\n", " j = max([0] + [k[1] for k in bps])\n", " chars = ''\n", " if (i, j) in bps:\n", " while i != 0 and j != 0:\n", " if bps[i, j][3] == 's1':\n", " chars += bps[i, j][2].upper()\n", " else:\n", " chars += bps[i, j][2]\n", " i, j = bps[i, j][0], bps[i, j][1] \n", " return ''.join(list(reversed(chars)))\n", " else:\n", " return ''" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'AAAbAbb'" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show_backtrace(backpointers)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def is_subseq(seq1, seq2, return_backpointers=False, return_table=False, debug=False):\n", " \"\"\"Return true if seq1 is a subsequence of seq2.\n", " If return_backpointers, also return the set of backpointers to\n", " reconstruct the subsequence\"\"\"\n", " \n", " # dp_table[i, j] is True if first i characters of seq1 can\n", " # be found in the first j characters of seq2\n", " \n", " dp_table = {(i, j): False\n", " for i in range(len(seq1)+1)\n", " for j in range(len(seq2)+1)}\n", "\n", " backpointers = {}\n", " \n", " for i in range(len(seq1)+1):\n", " for j in range(i, len(seq2)+1):\n", " if i == 0 or j == 0:\n", " dp_table[i, j] = True\n", " if debug: print('aa', i, j, '!', '!', dp_table[i, j])\n", " else:\n", " # extend by character from s2\n", " if dp_table[i, j-1]:\n", " dp_table[i, j] = True\n", " backpointers[i, j] = (i, j-1, seq2[j-1], 's2')\n", " if debug: print('s2', i, j, seq1[i-1], seq2[j-1], dp_table[i, j]) \n", " # extend by character from s1\n", " if dp_table[i-1, j-1] and seq1[i-1] == seq2[j-1]:\n", " dp_table[i, j] = True\n", " backpointers[i, j] = (i-1, j-1, seq1[i-1], 's1') \n", " if debug: print('s1', i, j, seq1[i-1], seq2[j-1], dp_table[i, j])\n", " if not dp_table[i, j]:\n", " if debug: print('xx', i, j, seq1[i-1], seq2[j-1], dp_table[i, j]) \n", " \n", "# if return_backpointers:\n", "# return dp_table[len(seq1), len(seq2)], backpointers\n", "# else:\n", "# return dp_table[len(seq1), len(seq2)]\n", " \n", " if return_backpointers or return_table:\n", " retval = [dp_table[len(seq1), len(seq2)]]\n", " if return_backpointers:\n", " retval += [backpointers]\n", " if return_table:\n", " retval += [dp_table]\n", " return tuple(retval)\n", " else:\n", " return dp_table[len(seq1), len(seq2)]" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "aa 0 0 ! ! True\n", "aa 0 1 ! ! True\n", "aa 0 2 ! ! True\n", "aa 0 3 ! ! True\n", "aa 0 4 ! ! True\n", "aa 0 5 ! ! True\n", "aa 0 6 ! ! True\n", "aa 0 7 ! ! True\n", "aa 0 8 ! ! True\n", "aa 0 9 ! ! True\n", "aa 0 10 ! ! True\n", "xx 1 1 a d False\n", "s1 1 2 a a True\n", "s2 1 3 a b True\n", "s2 1 4 a a True\n", "s1 1 4 a a True\n", "s2 1 5 a a True\n", "s1 1 5 a a True\n", "s2 1 6 a b True\n", "s2 1 7 a c True\n", "s2 1 8 a a True\n", "s1 1 8 a a True\n", "s2 1 9 a c True\n", "s2 1 10 a b True\n", "xx 2 2 a a False\n", "xx 2 3 a b False\n", "s1 2 4 a a True\n", "s2 2 5 a a True\n", "s1 2 5 a a True\n", "s2 2 6 a b True\n", "s2 2 7 a c True\n", "s2 2 8 a a True\n", "s1 2 8 a a True\n", "s2 2 9 a c True\n", "s2 2 10 a b True\n", "xx 3 3 a b False\n", "xx 3 4 a a False\n", "s1 3 5 a a True\n", "s2 3 6 a b True\n", "s2 3 7 a c True\n", "s2 3 8 a a True\n", "s1 3 8 a a True\n", "s2 3 9 a c True\n", "s2 3 10 a b True\n", "xx 4 4 a a False\n", "xx 4 5 a a False\n", "xx 4 6 a b False\n", "xx 4 7 a c False\n", "s1 4 8 a a True\n", "s2 4 9 a c True\n", "s2 4 10 a b True\n" ] }, { "data": { "text/plain": [ "True" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq(s1, s2t, debug=True)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def show_annotated_table(table, bps):\n", " return '\\n'.join(' '.join('.' if (i, j) not in bps else bps[i, j][2] if table[i, j] else '.' for j in sorted(set([k[1] for k in table])))\n", " for i in sorted(set([k[0] for k in table])))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tf, bps, tb = is_subseq(s1, s2t, return_backpointers=True, return_table=True)\n", "tf" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'AbAAbcAcb'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "show_backtrace(bps)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "T T T T T T T T T T T\n", "F F T T T T T T T T T\n", "F F F F T T T T T T T\n", "F F F F F T T T T T T\n", "F F F F F F F F T T T\n" ] } ], "source": [ "print(show_table(tb))" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ". . . . . . . . . . .\n", ". . a b a a b c a c b\n", ". . . . a a b c a c b\n", ". . . . . a b c a c b\n", ". . . . . . . . a c b\n" ] } ], "source": [ "print(show_annotated_table(tb, bps))" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(show_backtrace(bps)) == len(s2t)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('aaaa', 'dabaabcacb')" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s1, s2t" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(1, 2): (0, 1, 'a', 's1'),\n", " (1, 3): (1, 2, 'b', 's2'),\n", " (1, 4): (0, 3, 'a', 's1'),\n", " (1, 5): (0, 4, 'a', 's1'),\n", " (1, 6): (1, 5, 'b', 's2'),\n", " (1, 7): (1, 6, 'c', 's2'),\n", " (1, 8): (0, 7, 'a', 's1'),\n", " (1, 9): (1, 8, 'c', 's2'),\n", " (1, 10): (1, 9, 'b', 's2'),\n", " (2, 4): (1, 3, 'a', 's1'),\n", " (2, 5): (1, 4, 'a', 's1'),\n", " (2, 6): (2, 5, 'b', 's2'),\n", " (2, 7): (2, 6, 'c', 's2'),\n", " (2, 8): (1, 7, 'a', 's1'),\n", " (2, 9): (2, 8, 'c', 's2'),\n", " (2, 10): (2, 9, 'b', 's2'),\n", " (3, 5): (2, 4, 'a', 's1'),\n", " (3, 6): (3, 5, 'b', 's2'),\n", " (3, 7): (3, 6, 'c', 's2'),\n", " (3, 8): (2, 7, 'a', 's1'),\n", " (3, 9): (3, 8, 'c', 's2'),\n", " (3, 10): (3, 9, 'b', 's2'),\n", " (4, 8): (3, 7, 'a', 's1'),\n", " (4, 9): (4, 8, 'c', 's2'),\n", " (4, 10): (4, 9, 'b', 's2')}" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bps" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq(s1, s2f)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "aa 0 0 ! ! True\n", "aa 0 1 ! ! True\n", "aa 0 2 ! ! True\n", "aa 0 3 ! ! True\n", "aa 0 4 ! ! True\n", "aa 0 5 ! ! True\n", "aa 0 6 ! ! True\n", "aa 0 7 ! ! True\n", "s1 1 1 a a True\n", "s2 1 2 a a True\n", "s1 1 2 a a True\n", "s2 1 3 a a True\n", "s1 1 3 a a True\n", "s2 1 4 a b True\n", "s2 1 5 a a True\n", "s1 1 5 a a True\n", "s2 1 6 a b True\n", "s2 1 7 a b True\n", "s1 2 2 a a True\n", "s2 2 3 a a True\n", "s1 2 3 a a True\n", "s2 2 4 a b True\n", "s2 2 5 a a True\n", "s1 2 5 a a True\n", "s2 2 6 a b True\n", "s2 2 7 a b True\n", "s1 3 3 a a True\n", "s2 3 4 a b True\n", "s2 3 5 a a True\n", "s1 3 5 a a True\n", "s2 3 6 a b True\n", "s2 3 7 a b True\n", "xx 4 4 a b False\n", "s1 4 5 a a True\n", "s2 4 6 a b True\n", "s2 4 7 a b True\n" ] }, { "data": { "text/plain": [ "(True,\n", " {(1, 1): (0, 0, 'a', 's1'),\n", " (1, 2): (0, 1, 'a', 's1'),\n", " (1, 3): (0, 2, 'a', 's1'),\n", " (1, 4): (1, 3, 'b', 's2'),\n", " (1, 5): (0, 4, 'a', 's1'),\n", " (1, 6): (1, 5, 'b', 's2'),\n", " (1, 7): (1, 6, 'b', 's2'),\n", " (2, 2): (1, 1, 'a', 's1'),\n", " (2, 3): (1, 2, 'a', 's1'),\n", " (2, 4): (2, 3, 'b', 's2'),\n", " (2, 5): (1, 4, 'a', 's1'),\n", " (2, 6): (2, 5, 'b', 's2'),\n", " (2, 7): (2, 6, 'b', 's2'),\n", " (3, 3): (2, 2, 'a', 's1'),\n", " (3, 4): (3, 3, 'b', 's2'),\n", " (3, 5): (2, 4, 'a', 's1'),\n", " (3, 6): (3, 5, 'b', 's2'),\n", " (3, 7): (3, 6, 'b', 's2'),\n", " (4, 5): (3, 4, 'a', 's1'),\n", " (4, 6): (4, 5, 'b', 's2'),\n", " (4, 7): (4, 6, 'b', 's2')})" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq('aaaa', 'aaababb', return_backpointers=True, debug=True)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def is_subseq_recursive(s1, s2):\n", " if not s1:\n", " return True\n", " elif len(s1) > len(s2):\n", " return False\n", " else:\n", " if s1[-1] == s2[-1]:\n", " return is_subseq_recursive(s1[:-1], s2[:-1]) or is_subseq_recursive(s1, s2[:-1])\n", " else:\n", " return is_subseq_recursive(s1, s2[:-1])" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq_recursive(s1, s2t)" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq_recursive(s1, s2f)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def make_string(length, alphabet=None):\n", " if not alphabet:\n", " alphabet = 'abcdefgh'\n", " return ''.join(random.choice(alphabet) for _ in range(length)) " ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def interleave(s1, s2, wander_limit=10, debug=False):\n", " i1 = i2 = wander = 0\n", " interleaved = []\n", " while i1 <= len(s1) and i2 <= len(s2):\n", " if i1 == len(s1):\n", " if debug: print(i1, i2, wander, 'remaining s2', s2[i2:])\n", " interleaved += s2[i2:]\n", " i2 = len(s2) + 1\n", " elif i2 == len(s2):\n", " if debug: print(i1, i2, wander, 'remaining s1', s1[i1:])\n", " interleaved += s1[i1:]\n", " i1 = len(s1) + 1\n", " else:\n", " if wander == wander_limit:\n", " step = -1\n", " elif wander == -wander_limit:\n", " step = +1\n", " else:\n", " step = random.choice([+1, -1])\n", " if step == +1:\n", " if debug: print(i1, i2, wander, 'adding', s1[i1])\n", " interleaved += s1[i1]\n", " i1 += 1\n", " wander += 1\n", " else:\n", " if debug: print(i1, i2, wander, 'adding', s2[i2])\n", " interleaved += s2[i2]\n", " i2 += 1\n", " wander -= 1\n", " return ''.join(interleaved)" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": true }, "outputs": [], "source": [ "sl1 = make_string(200)\n", "sl2 = make_string(200)\n", "sl3 = make_string(200)\n", "sl12 = interleave(sl1, sl2)\n", "sl23 = interleave(sl2, sl3)" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(True, False)" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "is_subseq(sl1, sl12), is_subseq(sl1, sl23)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "is_subseq_recursive(sl1, sl12), is_subseq_recursive(sl1, sl23)" ] }, { "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.2+" } }, "nbformat": 4, "nbformat_minor": 1 }