{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
Peter Norvig, Feb 2021
\n", "\n", "# CrossProduct Puzzle\n", "\n", "The 538 Riddler [poses a type of puzzle](https://fivethirtyeight.com/features/can-you-cross-like-a-boss/) called ***CrossProduct***, which works like this:\n", "\n", ">*Replace each \"?\" in the table with a single digit so that the product of the digits in each row equals the number to the right of the row, and the product of the digits in each column equals the number above the column.*\n", "\n", "\n", "Sample puzzle:\n", "\n", " \n", "| 6615 | 15552 |   420 | [6x3] |\n", "|-------|-------|-------|---|\n", "| ? | ? | ? |**210**|\n", "| ? | ? | ? |**144**|\n", "| ? | ? | ? |**54**|\n", "| ? | ? | ? |**135**|\n", "| ? | ? | ? |**4**|\n", "| ? | ? | ? |**49**|\n", "\n", "\n", "Solution:\n", "\n", "|6615|15552|  420| [6x3]|\n", "|---|---|---|---|\n", "|7|6|5|**210**|\n", "|9|8|2|**144**|\n", "|3|9|2|**54**|\n", "|5|9|3|**135**|\n", "|1|4|1|**4**|\n", "|7|1|7|**49**|\n", " \n", "\n", "\n", "\n", "\n", "\n", "\n", " \n", "# Data type definitions\n", " \n", "Here are the data types we will use in trying to solve CrossProduct puzzles: \n", "- `Digit`: a single digit, from 1 to 9 (but not 0).\n", "- `Row`: a sequence of digits that forms a row in the table, e.g. `(7, 6, 5)`.\n", "- `Table`: a table of digits that fill in for the \"?\"s; a list of rows, e.g. `[(7, 6, 5), (9, 8, 2), ...]`.\n", "- `Products`: a list of the numbers that corresponding digits must multiply to, e.g. in the puzzle above:\n", " - `[6615, 15552, 420]` for the column products;\n", " - `[210, 144, 54, 135, 4, 49]` for the row products.\n", "- `Puzzle`: a puzzle to be solved, as defined by the row products and column products." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from typing import Tuple, List, Set, Iterable, Optional\n", "from numpy import divide, prod, transpose\n", "from random import randint\n", "from collections import namedtuple, Counter\n", "\n", "Digit = int\n", "Row = Tuple[Digit, ...] \n", "Table = List[Row] \n", "Product = int\n", "Products = List[Product] \n", "Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# The puzzles\n", "\n", "Here are the puzzles given by 538 Riddler (they promised one a week for four weeks; so far we've seen three):" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "puzzles = (\n", " Puzzle([135, 45, 64, 280, 70], [3000, 3969, 640]),\n", " Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420]),\n", " Puzzle([280, 168, 162, 360, 60, 256, 126], [183708, 245760, 117600]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Filling in one row\n", "\n", "- A first step in solving the puzzle is filling in a single row of the table.\n", "- We will need to respect the row- and column-product constraints.\n", "- `fill_one_row(row_prod=210, col_prods=[6615, 15552, 420])` will return a set of 3-digit tuples where each tuple multiplies to 210, and each digit of the tuple evenly divides the corresponding number in `col_prods`.\n", " - If `col_prods` is `[]`, then there is one solution (the 0-length tuple) if `row_prod` is 1, and no solution otherwise.\n", " - Otherwise, try each digit `d` that divides both the `row_prod` and the first `col_prods`, and then try all ways to fill the rest of the row." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def fill_one_row(row_prod: Product, col_prods: Products) -> Set[Row]:\n", " \"All permutations of digits that multiply to `row_prod` and evenly divide `col_prods`.\"\n", " return ({()} if not col_prods and row_prod == 1 else\n", " set() if not col_prods and row_prod != 1 else\n", " {(d, *rest) for d in range(1, 10)\n", " if (row_prod / d).is_integer() and (col_prods[0] / d).is_integer()\n", " for rest in fill_one_row(row_prod // d, col_prods[1:])})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some examples:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(5, 6, 7), (7, 6, 5)}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fill_one_row(210, [6615, 15552, 420]) # There are two ways to fill the first row" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(9, 9, 9)}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fill_one_row(729, [90, 90, 90])" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(1, 9, 9, 9),\n", " (3, 3, 9, 9),\n", " (3, 9, 3, 9),\n", " (3, 9, 9, 3),\n", " (9, 1, 9, 9),\n", " (9, 3, 3, 9),\n", " (9, 3, 9, 3),\n", " (9, 9, 1, 9),\n", " (9, 9, 3, 3),\n", " (9, 9, 9, 1)}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fill_one_row(729, [90, 90, 90, 90])" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(1, 9, 9, 9), (3, 3, 9, 9), (3, 9, 3, 9), (3, 9, 9, 3)}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fill_one_row(729, [30, 90, 90, 90])" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{(1, 7, 7, 7, 7, 7, 7, 7, 7),\n", " (7, 1, 7, 7, 7, 7, 7, 7, 7),\n", " (7, 7, 1, 7, 7, 7, 7, 7, 7),\n", " (7, 7, 7, 1, 7, 7, 7, 7, 7),\n", " (7, 7, 7, 7, 1, 7, 7, 7, 7),\n", " (7, 7, 7, 7, 7, 1, 7, 7, 7),\n", " (7, 7, 7, 7, 7, 7, 1, 7, 7),\n", " (7, 7, 7, 7, 7, 7, 7, 1, 7),\n", " (7, 7, 7, 7, 7, 7, 7, 7, 1)}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fill_one_row(7**8, [7]*9)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Solving a whole puzzle\n", "\n", "- We can solve a whole puzzle with a similar strategy to filling a row.\n", "- For every possible way of filling the first row, try every way of recursively solving the rest of the puzzle. \n", "- `solve` finds the first solution to a puzzle. (A well-formed puzzle has exactly one solution, but some might have more or less.)\n", "- `solutions` yields all possible solutions to a puzzle. There are three main cases to consider:\n", " - A puzzle with no rows has the empty table, `[]`, as a solution, as long as the column products are all 1.\n", " - A puzzle with rows might have solutions, as long as the column products are all integers. Call `fill_row` to get all possible ways to fill the first row, and for each one recursively call `solutions` to get all the possible ways of filling the rest of the rows (making sure to pass in an altered `col_prods` where each element is divided by the corresponding element in the first row).\n", " - Otherwise there are no solutions." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def solve(puzzle) -> Optional[Table]: return next(solutions(puzzle), None)\n", "\n", "def solutions(puzzle) -> Iterable[Table]:\n", " \"\"\"Yield all tables that solve the puzzle.\n", " The product of the digits in row r must equal row_prods[r], for all r.\n", " The product of the digits in column c must equal col_prods[c], for all c.\"\"\"\n", " row_prods, col_prods = puzzle\n", " if not row_prods and all(c == 1 for c in col_prods):\n", " yield []\n", " elif row_prods and all(c == int(c) for c in col_prods):\n", " yield from ([row1, *rows]\n", " for row1 in fill_one_row(row_prods[0], col_prods)\n", " for rows in solutions(Puzzle(row_prods[1:], list(divide(col_prods, row1)))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Solutions\n", "\n", "Here are solutions to the puzzles posed by *The Riddler*:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)],\n", " [(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)],\n", " [(7, 8, 5), (3, 8, 7), (9, 6, 3), (9, 8, 5), (3, 5, 4), (4, 8, 8), (9, 2, 7)]]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[solve(p) for p in puzzles]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Prettier solutions\n", "\n", "Those are the correct solutions. However, we could make them look nicer:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "from IPython.display import Markdown\n", "\n", "def pretty(puzzle, table=None) -> str:\n", " \"\"\"A puzzle and the filled-in table as a str that will be pretty in Markdown.\"\"\"\n", " row_prods, col_prods = puzzle\n", " table = table or solve(puzzle)\n", " head = surround(col_prods + [f'[{len(row_prods)}x{len(col_prods)}]'])\n", " dash = surround(['---'] * (1 + len(col_prods)))\n", " rest = [surround(row + (f'**{rp}**',))\n", " for row, rp in zip(table, row_prods)]\n", " return '\\n'.join([head, dash, *rest])\n", "\n", "def surround(items, delim='|') -> str: \n", " \"\"\"Like str.join, but delimiter is outside items as well as between.\"\"\"\n", " return delim + delim.join(map(str, items)) + delim" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|3000|3969|640|[5x3]|\n", "|---|---|---|---|\n", "|3|9|5|**135**|\n", "|5|9|1|**45**|\n", "|8|1|8|**64**|\n", "|5|7|8|**280**|\n", "|5|7|2|**70**|\n", "\n", "|6615|15552|420|[6x3]|\n", "|---|---|---|---|\n", "|7|6|5|**210**|\n", "|9|8|2|**144**|\n", "|3|9|2|**54**|\n", "|5|9|3|**135**|\n", "|1|4|1|**4**|\n", "|7|1|7|**49**|\n", "\n", "|183708|245760|117600|[7x3]|\n", "|---|---|---|---|\n", "|7|8|5|**280**|\n", "|3|8|7|**168**|\n", "|9|6|3|**162**|\n", "|9|8|5|**360**|\n", "|3|5|4|**60**|\n", "|4|8|8|**256**|\n", "|9|2|7|**126**|" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Markdown('\\n\\n'.join(map(pretty, puzzles)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Making new puzzles\n", "\n", "Can we make new puzzles? Can we make well-formed ones (those with exactly one solution)? Here is an approach:\n", "- Make a table filled with random digits (`random_table`).\n", "- Make a puzzle from the row and column products of the table (`table_puzzle`).\n", "- Repeat `N` times (`random_puzzles`).\n", "- Optionally, check if puzzles are `well-formed`.\n" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def random_table(nrows, ncols) -> Table:\n", " \"Make a table of random digits of the given size.\"\n", " return [tuple(randint(1, 9) for c in range(ncols))\n", " for r in range(nrows)]\n", "\n", "def table_puzzle(table) -> Puzzle:\n", " \"Given a table, compute the puzzle it is a solution for.\"\n", " return Puzzle([prod(row) for row in table], \n", " [prod(col) for col in transpose(table)])\n", "\n", "def random_puzzles(N, nrows, ncols) -> List[Puzzle]: \n", " \"Return a list of `N` random puzzles.\"\n", " return [table_puzzle(random_table(nrows, ncols)) for _ in range(N)]\n", "\n", "def well_formed(puzzle) -> bool: \n", " \"Does the puzzle have exactly one solution?\"\n", " S = solutions(puzzle)\n", " first, second = next(S, None), next(S, None)\n", " return first is not None and second is None" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[(9, 4, 9), (7, 4, 9), (3, 8, 3), (8, 6, 1), (3, 4, 5)]" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "random_table(nrows=5, ncols=3)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "puz = table_puzzle(_)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "well_formed(puz)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|4536|3072|1215|[5x3]|\n", "|---|---|---|---|\n", "|6|6|9|**324**|\n", "|7|4|9|**252**|\n", "|9|8|1|**72**|\n", "|2|8|3|**48**|\n", "|6|2|5|**60**|" ], "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Markdown(pretty(puz))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How likely are random puzzles (of various sizes) to be well-formed?" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "38% of random puzzles with 3 rows and 3 cols ( 9 cells) are well-formed\n", "15% of random puzzles with 3 rows and 4 cols (12 cells) are well-formed\n", "15% of random puzzles with 4 rows and 3 cols (12 cells) are well-formed\n", " 4% of random puzzles with 3 rows and 5 cols (15 cells) are well-formed\n", " 4% of random puzzles with 5 rows and 3 cols (15 cells) are well-formed\n", " 4% of random puzzles with 4 rows and 4 cols (16 cells) are well-formed\n", " 2% of random puzzles with 6 rows and 3 cols (18 cells) are well-formed\n" ] } ], "source": [ "N = 200\n", "for r, c in [(3, 3), (3, 4), (4, 3), (3, 5), (5, 3), (4, 4), (6, 3)]:\n", " w = sum(map(well_formed, random_puzzles(N, r, c))) / N\n", " print(f'{w:3.0%} of random puzzles with {r} rows and {c} cols ({r * c:2} cells) are well-formed')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that most puzzles are not well-formed. Smaller sizes are more likely to yield well-formed puzzles.\n", "\n", "# Speed\n", "\n", "How long does it take to solve a random puzzle? We can do a thousand small (5x3) puzzles in about two seconds:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 2 s, sys: 76.7 ms, total: 2.08 s\n", "Wall time: 2.02 s\n" ] }, { "data": { "text/plain": [ "1000" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%time len([solve(p) for p in random_puzzles(1000, 5, 3)])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Puzzles that are even a little bit larger can be a lot slower, and there is huge variability in the time to solve. For example, a single 10 x 4 puzzle can take from a few milliseconds to several seconds:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 2.83 s, sys: 38.1 ms, total: 2.87 s\n", "Wall time: 2.87 s\n" ] }, { "data": { "text/markdown": [ "|470400|53760|8294400|226800|[10x4]|\n", "|---|---|---|---|---|\n", "|7|1|5|4|**140**|\n", "|5|1|5|4|**100**|\n", "|4|5|4|7|**560**|\n", "|5|8|2|5|**400**|\n", "|7|7|2|1|**98**|\n", "|3|8|4|3|**288**|\n", "|1|1|9|1|**9**|\n", "|8|6|8|5|**1920**|\n", "|4|4|8|3|**384**|\n", "|1|1|9|9|**81**|" ], "text/plain": [ "" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[puzz] = random_puzzles(1, 10, 4)\n", "%time Markdown(pretty(puzz))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In general, the time to solve a puzzle can grow exponentially in the number of cells. Consider this one row in a six-column puzzle, with 3,960 possibilities:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3960" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n = 5 * 7 * 8 * 9\n", "len(fill_one_row(2 * n, [n] * 6))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If the first three rows all had a similar number of possibilities, that would be tens of billions of combinations to try. What could we do to speed things up?\n", "- We could treat it as a constraint satisfaction problem (CSP), and use a highly-optimized [CSP solver](https://developers.google.com/optimization/cp/cp_solver). A good CSP representation would be to make each cell be a variable with range {1, ... 9}, and with the constraints being that the digit in each cell must evenly divide both the row- and column- constraint for the cell, and the product of the row (or column) must equal the corresponding value.\n", "- Even without using a professional CSP solver, we could borrow the heuristics they use. In `solve`, we fill in cells in strict top-to-bottom, left-to-right order. It is better to fill in first the cell with the minimum number of possible values. For each cell, find the greatest common divisor of the row- and column-products. For a cell whose gcd is 72, the possible digits are {2, 3, 4, 6, 9}. For a cell whose gcd is 21, the possible digits are {3, 7}. Thus, it is better to fill in the 21 cell first, because you have a 1/2 chance of guessing right, not a 1/5 chance." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Tests\n", "\n", "A suite of unit tests:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def test():\n", " \"Test suite for CrossProduct functions.\"\n", " assert fill_one_row(1, []) == {()}\n", " assert fill_one_row(2, []) == set()\n", " assert fill_one_row(9, [9]) == {(9,)}\n", " assert fill_one_row(10, [10]) == set()\n", " assert fill_one_row(73, [360, 360, 360]) == set()\n", " \n", " assert solve(Puzzle([], [])) == []\n", " assert solve(Puzzle([], [1])) == []\n", " assert solve(Puzzle([], [2])) == None\n", " assert solve(Puzzle([5], [5])) == [(5,)]\n", " assert solve(Puzzle([0], [0])) == None # Maybe should allow zero as a digit?\n", " assert solve(Puzzle([2, 12], [3, 8])) == [(1, 2), (3, 4)]\n", "\n", " assert fill_one_row(729, [90, 126, 81]) == {(9, 9, 9)} # Unique fill\n", " \n", " assert fill_one_row(729, [90, 126, 81, 30]) == {\n", " (3, 9, 9, 3), (9, 3, 9, 3), (9, 9, 3, 3), (9, 9, 9, 1)}\n", " \n", " # 72 has the most ways to fill a 3-digit row\n", " assert max(range(1, 100), key=lambda n: len(fill_one_row(n, [5*7*8*9]*3))) == 72\n", " assert fill_one_row(72, [72, 72, 72]) == { \n", " (1, 8, 9),\n", " (1, 9, 8),\n", " (2, 4, 9),\n", " (2, 6, 6),\n", " (2, 9, 4),\n", " (3, 3, 8),\n", " (3, 4, 6),\n", " (3, 6, 4),\n", " (3, 8, 3),\n", " (4, 2, 9),\n", " (4, 3, 6),\n", " (4, 6, 3),\n", " (4, 9, 2),\n", " (6, 2, 6),\n", " (6, 3, 4),\n", " (6, 4, 3),\n", " (6, 6, 2),\n", " (8, 1, 9),\n", " (8, 3, 3),\n", " (8, 9, 1),\n", " (9, 1, 8),\n", " (9, 2, 4),\n", " (9, 4, 2),\n", " (9, 8, 1)}\n", " \n", " assert solve(Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420])) == [\n", " (7, 6, 5), \n", " (9, 8, 2), \n", " (3, 9, 2), \n", " (5, 9, 3), \n", " (1, 4, 1), \n", " (7, 1, 7)]\n", " \n", " assert sorted(solutions(Puzzle([8, 8, 1], [8, 8, 1]))) == [ # Multi-solution puzzle\n", " [(1, 8, 1), \n", " (8, 1, 1), \n", " (1, 1, 1)],\n", " [(2, 4, 1), \n", " (4, 2, 1), \n", " (1, 1, 1)],\n", " [(4, 2, 1), \n", " (2, 4, 1), \n", " (1, 1, 1)],\n", " [(8, 1, 1), \n", " (1, 8, 1), \n", " (1, 1, 1)]]\n", " \n", " assert not list(solutions(Puzzle([8, 8, 1], [8, 8, 2]))) # Unsolvable puzzle\n", " \n", " assert solve(Puzzle([1470, 720, 270, 945, 12, 343], \n", " [6615, 15552, 420, 25725])) == [ # 4 column puzzle\n", " (7, 6, 5, 7),\n", " (9, 8, 2, 5),\n", " (3, 9, 2, 5),\n", " (5, 9, 3, 7),\n", " (1, 4, 1, 3),\n", " (7, 1, 7, 7)]\n", " \n", " puzz = Puzzle([6, 120, 504], [28, 80, 162])\n", " table = [(1, 2, 3), \n", " (4, 5, 6), \n", " (7, 8, 9)]\n", " assert solve(puzz) == table\n", " assert table_puzzle(table) == puzz\n", " assert well_formed(puzz)\n", " \n", " assert not well_formed(Puzzle([7, 7], [7, 7]))\n", " assert well_formed(Puzzle([64, 224, 189, 270, 405, 144, 105], \n", " [308700, 12960, 1119744]))\n", " \n", " assert surround((1, 2, 3)) == '|1|2|3|'\n", " \n", " return True\n", " \n", "test()" ] } ], "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.7" } }, "nbformat": 4, "nbformat_minor": 4 }