Add files via upload
This commit is contained in:
@@ -48,8 +48,8 @@
|
||||
" \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",
|
||||
"- `Row`: a tuple of digits that forms a row in the table, e.g. `(7, 6, 5)`.\n",
|
||||
"- `Table`: a table of digits that fill in all 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",
|
||||
@@ -62,10 +62,10 @@
|
||||
"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",
|
||||
"from typing import Tuple, List, Set, Iterable, Optional\n",
|
||||
"from numpy import divide, prod, transpose\n",
|
||||
"from collections import namedtuple\n",
|
||||
"import random\n",
|
||||
"\n",
|
||||
"Digit = int\n",
|
||||
"Row = Tuple[Digit, ...] \n",
|
||||
@@ -102,10 +102,10 @@
|
||||
"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",
|
||||
"A first step in solving the puzzle is filling in a single row of the table.\n",
|
||||
"\n",
|
||||
"`fill_one_row(row_prod=210, col_prods=[6615, 15552, 420])` will return a set of 3-digit tuples where each digit of a tuple evenly divides both `row_prod` and the corresponding number in `col_prods`, and together the digits in a tuple multiply to `row_prod`.\n",
|
||||
" - If `col_prods` is empty, 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."
|
||||
]
|
||||
},
|
||||
@@ -117,11 +117,12 @@
|
||||
"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:])})"
|
||||
" if not col_prods:\n",
|
||||
" return {()} if row_prod == 1 else set()\n",
|
||||
" else:\n",
|
||||
" return {(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:])}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -332,8 +333,10 @@
|
||||
"source": [
|
||||
"from IPython.display import Markdown\n",
|
||||
"\n",
|
||||
"def show(puzzles): return Markdown('\\n\\n'.join(map(pretty, puzzles)))\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",
|
||||
" \"\"\"A puzzle with the filled-in table as a pretty Markdown str.\"\"\"\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",
|
||||
@@ -343,7 +346,7 @@
|
||||
" 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",
|
||||
" \"\"\"Like delim.join, but delimiter is outside items as well as between.\"\"\"\n",
|
||||
" return delim + delim.join(map(str, items)) + delim"
|
||||
]
|
||||
},
|
||||
@@ -392,7 +395,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Markdown('\\n\\n'.join(map(pretty, puzzles)))"
|
||||
"show(puzzles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -416,7 +419,7 @@
|
||||
"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",
|
||||
" return [tuple(random.randint(1, 9) for c in range(ncols))\n",
|
||||
" for r in range(nrows)]\n",
|
||||
"\n",
|
||||
"def table_puzzle(table) -> Puzzle:\n",
|
||||
@@ -424,8 +427,9 @@
|
||||
" 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",
|
||||
"def random_puzzles(N, nrows, ncols, seed=42) -> List[Puzzle]: \n",
|
||||
" \"Return a list of `N` random puzzles.\"\n",
|
||||
" random.seed(seed) # For reproducability\n",
|
||||
" return [table_puzzle(random_table(nrows, ncols)) for _ in range(N)]\n",
|
||||
"\n",
|
||||
"def well_formed(puzzle) -> bool: \n",
|
||||
@@ -443,7 +447,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[(9, 4, 9), (7, 4, 9), (3, 8, 3), (8, 6, 1), (3, 4, 5)]"
|
||||
"[(2, 8, 7), (3, 3, 7), (9, 3, 3), (6, 1, 1), (3, 5, 9)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
@@ -492,13 +496,13 @@
|
||||
{
|
||||
"data": {
|
||||
"text/markdown": [
|
||||
"|4536|3072|1215|[5x3]|\n",
|
||||
"|972|360|1323|[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**|"
|
||||
"|2|8|7|**112**|\n",
|
||||
"|3|3|7|**63**|\n",
|
||||
"|3|3|9|**81**|\n",
|
||||
"|6|1|1|**6**|\n",
|
||||
"|9|5|3|**135**|"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
@@ -510,7 +514,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Markdown(pretty(puz))"
|
||||
"show([puz])"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -529,13 +533,13 @@
|
||||
"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",
|
||||
"33% of random puzzles with 3 rows and 3 cols ( 9 cells) are well-formed\n",
|
||||
"18% of random puzzles with 3 rows and 4 cols (12 cells) are well-formed\n",
|
||||
"14% of random puzzles with 4 rows and 3 cols (12 cells) are well-formed\n",
|
||||
" 8% of random puzzles with 3 rows and 5 cols (15 cells) are well-formed\n",
|
||||
" 2% 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"
|
||||
" 0% of random puzzles with 6 rows and 3 cols (18 cells) are well-formed\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -554,7 +558,7 @@
|
||||
"\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:"
|
||||
"How long does it take to solve random puzzles? We can do a thousand small (5x3) puzzles in about two seconds:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -566,14 +570,14 @@
|
||||
"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"
|
||||
"CPU times: user 2.01 s, sys: 25.5 ms, total: 2.04 s\n",
|
||||
"Wall time: 2.05 s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1000"
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 19,
|
||||
@@ -582,14 +586,14 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time len([solve(p) for p in random_puzzles(1000, 5, 3)])"
|
||||
"%time all(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:"
|
||||
"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 6 puzzle can take from a few milliseconds to tens of seconds:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -601,25 +605,25 @@
|
||||
"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"
|
||||
"CPU times: user 3.64 s, sys: 26.5 ms, total: 3.67 s\n",
|
||||
"Wall time: 3.69 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**|"
|
||||
"|24576|979776|274400|2177280|1792000|524880|[10x6]|\n",
|
||||
"|---|---|---|---|---|---|---|\n",
|
||||
"|4|1|5|6|2|2|**480**|\n",
|
||||
"|1|7|1|3|4|3|**252**|\n",
|
||||
"|8|9|2|9|2|1|**2592**|\n",
|
||||
"|8|3|7|8|5|6|**40320**|\n",
|
||||
"|1|6|7|3|5|3|**1890**|\n",
|
||||
"|1|8|1|7|4|6|**1344**|\n",
|
||||
"|3|9|2|8|5|6|**12960**|\n",
|
||||
"|4|6|5|1|7|9|**7560**|\n",
|
||||
"|1|1|8|2|4|5|**320**|\n",
|
||||
"|8|2|7|5|8|3|**13440**|"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
@@ -631,15 +635,14 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"[puzz] = random_puzzles(1, 10, 4)\n",
|
||||
"%time Markdown(pretty(puzz))"
|
||||
"%time show(random_puzzles(1, 10, 6))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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:"
|
||||
"In general, the time to solve a puzzle can grow exponentially in the number of cells. Consider a row in a six-column puzzle, where the products are all 5040. That gives us 3,960 possibilities: "
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -659,17 +662,161 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"n = 5 * 7 * 8 * 9\n",
|
||||
"len(fill_one_row(2 * n, [n] * 6))"
|
||||
"n = 5040\n",
|
||||
"len(fill_one_row(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."
|
||||
"If four rows all had a similar number of possibilities and didn't constrain each other, that would be hundreds of trillions of combinations to try—an infeasible number. We will need a faster algorithm for larger puzzles.\n",
|
||||
"\n",
|
||||
"# Faster Speed\n",
|
||||
"\n",
|
||||
"To speed things up, we could encode the puzzle as a constraint satisfaction problem (CSP), and use a highly-optimized [CSP solver](https://developers.google.com/optimization/cp/cp_solver). But even without going to a professional-grade CSP solver, we could borrow the heuristics they use. There are four main considerations in CSP solving:\n",
|
||||
"- **Variable definition**: In `solutions`, we are treating each row as a variable, and asking \"which of the possible values returned by `fill_one_row` will work as the value of this row? An alternative would be to treat each cell as a variable, and fill in the puzzle one cell at a time rather than one row at a time. This has the advantage that each variable has only 9 possible values, not thousands of possibilities.\n",
|
||||
"- **Variable ordering**: In `solutions`, we consider the variables (the rows) in strict top-to-bottom order. It is usually more efficient to reorder the variables, filling in first the variable with the minimum number of possible values. The reasoning is that if you have a variable with only 2 possibilities, you have a 50% chance of guessing right the first time, whereas if there were 100 possibilities, you have only a 1% chance of guessing right.\n",
|
||||
"- **Value ordering**: The function `fill_one_row` returns values in sorted lexicographic order, lowest first. If we only care about finding the first solution, then we should reorder the values to pick the one that imposes the least constraints first (that is, allows the most possibilities for the other variables).\n",
|
||||
"- **Domain-specific heuristics**: CSP solvers are general, but sometimes knowledge that is specific to a problem can be helpful. One fact about CrossProduct is that the digits 5 and 7 are special in the sense that if a row (or column) product is divisible by 5 (or 7), then the digit 5 (or 7) must appear in the row (or column). That is not true for the other digits (for example, if a row product is divisible by 8, then an 8 may appear in the row, or it might be a 2 and a 4, or three 6s, etc.).\n",
|
||||
"\n",
|
||||
"Usually variable ordering has the biggest effect on run time. Let's try it. The function `reorder` takes a puzzle and returns a version of the puzzle with the row products permuted so that the rows with the fewest possible fillers come first:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def reorder(puzzle) -> Puzzle:\n",
|
||||
" \"\"\"Create a version of puzzle with the rows reordered so the rows with the fewest\n",
|
||||
" number of possible fillers come first.\"\"\"\n",
|
||||
" def fillers(r): return len(fill_one_row(r, puzzle.col_prods))\n",
|
||||
" rows = sorted(puzzle.row_prods, key=fillers)\n",
|
||||
" return Puzzle(rows, puzzle.col_prods)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(Puzzle(row_prods=[280, 168, 162, 360, 60, 256, 126], col_prods=[183708, 245760, 117600]),\n",
|
||||
" Puzzle(row_prods=[256, 280, 162, 360, 126, 168, 60], col_prods=[183708, 245760, 117600]))"
|
||||
]
|
||||
},
|
||||
"execution_count": 23,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"p2 = puzzles[2]\n",
|
||||
"p2, reorder(p2)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{256: 1, 280: 2, 162: 2, 360: 2, 126: 5, 168: 7, 60: 8}"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# How many ways are there to fill each row?\n",
|
||||
"{r: len(fill_one_row(r, p2.col_prods)) for r in reorder(p2).row_prods}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now I'll define a set of test puzzles and see how long it takes to solve them all, and compare that to the time to solve the reordered versions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"test_puzzles = random_puzzles(20, 10, 3)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 24.6 s, sys: 317 ms, total: 24.9 s\n",
|
||||
"Wall time: 24.8 s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 26,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time all(solve(p) for p in test_puzzles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 27,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 156 ms, sys: 4.68 ms, total: 160 ms\n",
|
||||
"Wall time: 157 ms\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 27,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time all(solve(reorder(p)) for p in test_puzzles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That's a nice improvement—over 100 times faster! I'm curious whether we would get even more speedup by treating each cell as a separate variable, but I'll leave that as an exercise for the reader."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -683,7 +830,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"execution_count": 28,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -692,7 +839,7 @@
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 22,
|
||||
"execution_count": 28,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -793,6 +940,9 @@
|
||||
" \n",
|
||||
" assert surround((1, 2, 3)) == '|1|2|3|'\n",
|
||||
" \n",
|
||||
" col_prods = [193536, 155520, 793800]\n",
|
||||
" assert (reorder(Puzzle([10, 48, 36, 7, 32, 81, 252, 160, 21, 90], col_prods)) == \n",
|
||||
" Puzzle([ 7, 10, 160, 21, 81, 252, 90, 32, 48, 36], col_prods))\n",
|
||||
" return True\n",
|
||||
" \n",
|
||||
"test()"
|
||||
|
||||
Reference in New Issue
Block a user