11

Can a rectangle $9\times 7$ be tiled by "L-blocks" (an L-block consists of $3$ unit squares)?

Although the problem seems to be easy, coloring didn't help me. The general theory is interesting, but I'm looking for an elementary and relatively simple solution (suitable for a high school olympiad).

Ievgen
  • 477
  • 3
  • 12
  • 1
    You'd require 21 tiles to fill the area (9x7/3). 21 is an odd number. Now if we can establish that only even number of L shaped tiles can make a rectangle, then we are done. It's easy to visualize and realize that that's the case. But I'd love to see a formal proof of the same. – Deepak Gupta Dec 18 '15 at 11:25
  • @Servaes Thanks. But how can one prove rigorously that an even number of tiles is needed? – Ievgen Dec 18 '15 at 11:26
  • The colouring argument in this post suffices to show an even number of tiles is needed. – Frentos Dec 18 '15 at 12:00
  • FWIW, using Knuth's Algorithm X I've found a total of 22656 solutions, including reflections. – PM 2Ring Jan 19 '16 at 14:45

2 Answers2

9

Here's an example I found by hand:

enter image description here

  • Thanks. I was already almost 100% sure that the answer would be negative. :) It – Ievgen Dec 18 '15 at 13:30
  • It probably can be interesting to describe n and k for which nx(3k) rectangle can be tiled by L-blocks. (Obviously the answer is positive for 1) rectangles nx(9l), where n is odd and greater than 5 (based on the given example); 2) rectangles nx(3k), where n is even.) – Ievgen Dec 18 '15 at 13:48
6

Here's Python code to find the solutions for this puzzle for any grid size. It outputs the solutions both as text and as PPM files. This code was tested on Python 2.6.6 but it should run correctly on Python 2.5 or any later version.

#! /usr/bin/env python

''' Knuth's Algorithm X for the exact cover problem, using dicts instead of doubly linked circular lists. Written by Ali Assaf

From http://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html and http://www.cs.mcgill.ca/~aassaf9/python/sudoku.txt

Converted to Python 2.5+ syntax by PM 2Ring 2013.01.27

Trominoes version Fill a rectangular grid with L-trominoes 22656 solutions for 9 x 7

See http://math.stackexchange.com/q/1580934/207316

Now with PPM output in 4 colours; graph colouring also done via Algorithm X '''

from future import print_function import sys from itertools import product from operator import itemgetter

#Algorithm X functions def solve(X, Y, solution): if not X: yield list(solution) else: c = min(X, key=lambda c: len(X[c])) for r in list(X[c]): solution.append(r) cols = select(X, Y, r) for s in solve(X, Y, solution): yield s deselect(X, Y, r, cols) solution.pop()

def select(X, Y, r): cols = [] for j in Y[r]: for i in X[j]: for k in Y[i]: if k != j: X[k].remove(i) cols.append(X.pop(j)) return cols

def deselect(X, Y, r, cols): for j in reversed(Y[r]): X[j] = cols.pop() for i in X[j]: for k in Y[i]: if k != j: X[k].add(i)

#Invert subset collection def exact_cover(X, Y): newX = dict((j, set()) for j in X) for i, row in Y.items(): for j in row: newX[j].add(i) return newX

#----------------------------------------------------------------------

#Solve tromino puzzle def fill_grid(width, height): #A 2x2 block of grid cells cells =((0,0), (1,0), (0,1), (1,1))

#Set to cover
X = product(range(width), range(height))

#Subsets to cover X with. All possible L-block at each grid location
Y = {}
for x, y, i in product(range(width - 1), range(height - 1), range(4)):
    #Turn the 2x2 block into an L-block by dropping the cell at j==i
    Y[(x, y, i)] = [(x+u, y+v) for j,(u,v) in enumerate(cells) if j != i]

#Invert subset collection
X = exact_cover(X, Y)

#An empty grid to hold solutions
empty = [[0] * width for _ in range(height)]

keyfunc = itemgetter(1, 0, 2)
for s in solve(X, Y, []):
    #Convert cell tuple list into grid form
    s.sort(key=keyfunc)
    grid = empty[:]
    for k, (x, y, i) in enumerate(s):
        for j, (u,v) in enumerate(cells):
            if j != i:
                grid[y+v][x+u] = k
    yield grid

#----------------------------------------------------------------------

#Colour a graph given its nodes and edges def colour_map(nodes, edges, ncolours=4): colours = range(ncolours)

#The edges that meet each node
node_edges = dict((n, set()) for n in nodes)
for e in edges:
    n0, n1 = e
    node_edges[n0].add(e)
    node_edges[n1].add(e)

for n in nodes:
    node_edges[n] = list(node_edges[n])

#Set to cover
coloured_edges = list(product(colours, edges))
X = nodes + coloured_edges

#Subsets to cover X with
Y = {}
#Primary rows
for n in nodes:
    ne = node_edges[n]
    for c in colours:
        Y[(n, c)] = [n] + [(c, e) for e in ne]

#Dummy rows
for i, ce in enumerate(coloured_edges):
    Y[i] = [ce]

X = exact_cover(X, Y)

#Set first two nodes
partial = [(nodes[0], 0), (nodes[1], 1)]
for s in partial:
    select(X, Y, s)

for s in solve(X, Y, []):
    s = partial + [u for u in s if not isinstance(u, int)]
    s.sort()
    yield s

#Extract the nodes and edges from a grid def gridtograph(grid): gridheight = len(grid) gridwidth = len(grid[0])

#Find regions.
nodes = list(set(c for row in grid for c in row))
nodes.sort()
#print 'nodes =', nodes

#Find neighbours
#Verticals
edges = set()
for y in range(gridheight):
    for x in range(gridwidth - 1):
        c0, c1 = grid[y][x], grid[y][x+1]
        if c0 != c1 and (c1, c0) not in edges:
            edges.add((c0, c1))

#Horizontals
for y in range(gridheight - 1):
    for x in range(gridwidth):
        c0, c1 = grid[y][x], grid[y+1][x]
        if c0 != c1 and (c1, c0) not in edges:
            edges.add((c0, c1))

edges = list(edges)
edges.sort()
#print 'edges =', edges
return nodes, edges

#----------------------------------------------------------------------

def show_grid(grid, strwidth): for row in grid: print(' '.join([str(k).zfill(strwidth) for k in row])) print()

pal = ( b'\xff\x00\x00', b'\x00\xff\x00', b'\x00\x00\xff', b'\xff\xff\x00', )

#----------------------------------------------------------------------

def main(): if len(sys.argv) < 3: print ("Solve tromino grid puzzle\nUsage:\n" "%s width height [max_solutions]" % sys.argv[0]) exit()

width = int(sys.argv[1])
height = int(sys.argv[2])
maxcount = int(sys.argv[3]) if len(sys.argv) &gt; 3 else 0
ncolours = 4

strwidth = len(str(width * height // 3 - 1))

#Constants used for PPM output
fnamebase = 'grid_%dx%d' % (width, height)
scale = 16
scalerange = range(scale)
imgwidth, imgheight = scale * width, scale * height

print('\nSolutions:')
count = 1
try:
    for grid in fill_grid(width, height):
        print('\n%2d:' % count)
        show_grid(grid, strwidth)
        nodes, edges = gridtograph(grid)

        #Find a colouring for this grid
        gen = colour_map(nodes, edges, ncolours=ncolours)
        solution = next(gen)
        colourmap = dict(solution)
        #print colourmap
        grid = [[colourmap[u] for u in row] for row in grid]
        #show_grid(grid, 1)

        #Convert to PPM
        data = []
        for row in grid:
            row = [u for u in row for _ in scalerange]
            data.extend(row * scale)
        ppmstr = b''.join([pal[u] for u in data])

        fname = '%s_%03d.ppm' % (fnamebase, count)
        with open(fname, 'wb') as f:
            f.write(b'P6\n%d %d\n255\n%s' % (imgwidth, imgheight, ppmstr))
        print('Saved to', fname)

        count += 1
        if maxcount and count &gt; maxcount:
            break
    print(count - 1)
except KeyboardInterrupt:
    print('\nAborted')


if name == 'main': main()

And here are the first 100 solutions for the 9 x 7 grid as an animated GIF.

Tiling a 9x7 grid with L-trominoes

These images all use 4 colours, however, it is possible to colour some of them with 3 colours.

(If the image doesn't animate for you, try your browser's "View Image Info" context menu item).

PM 2Ring
  • 4,844