Pod artykułem o grze SET rozie postawił problem wśród ilu kart SET na pewno jest.
Pierwotnie spodziewałem się, że we dwa-trzy wieczory znajdę jakieś mocne ograniczenie na ilość kart bez seta i będę mógł się pochwalić autorskim rozwiązaniem. No i intuicyjnie oczekiwałem odpowiedzi rzędu 13, może 15.
Jeśli ktoś jeszcze próbuje samodzielnie problem rozwiązać, proszę dalej nie czytać, ciąg dalszy zawiera informacje psujące zabawę.
Pierwszy moment niepokoju nastąpił, gdy mimo rozlicznych prób nie udawało mi się wymyślić żadnego sposobu na poszufladkowanie dającego ograniczenie jakkolwiek zbliżone do spodziewanego.
Dość szybko narzuciła mi się notacja, w której zamiast własnościami kart posługiwałem się czwórkami cyfr z zakresu 0-2 (np. pierwszą kodując oznacza kolor, druga kształt itd). Trzy takie wektory są setem jeśli ich suma na każdej współrzędnej dzieli się przez trzy (albo, jak kto woli, jest zerem przy obliczaniu w Z3).
Próbowałem na różne sposoby te układy porządkować ale nie udało mi się dojść do niczego ciekawego.
Potem w jednej z partii nie potrafiliśmy znaleźć seta wśród dwunastu kart, dołożyliśmy trzy i ... dalej nie potrafiliśmy.
Absolutnej pewności czy tu nie ma odpowiedniej trójki - nie mam (skryptu szukającego seta w zadanej kombinacji kart na razie nie chciało mi się pisać). Jeśli ktoś takową tu znajdzie, proszę o wzmiankę w komentarzu.
Wreszcie, programista zwyciężył we mnie matematyka i napisałem parędziesiąt wierszy kodu w C++ próbującego budować jak największy zbiór kart bez seta. Kod jest dość brzydki i chwalić się nim nie mam powodu (proste wyszukiwanie w głąb w którym dla zbioru kart utrzymuję oprócz listy kart doń należących także listę kart, które jeszcze można dołożyć, tj. które nie tworzą seta z żadną parą już należącą do zbioru) ale szybko znalazł mi następujący zbiorek:
Jak widać, nawet dwadzieścia kart może nie wystarczyć.
Wykluczenie 21 jest programistycznie trudniejsze (siłowa analiza 13636219405675529520 wariantów odpada z miejsca, proste pogłębianie jakie napisałem też w realnym czasie się nie kończy.
W kolejce mam pomysł by znaleźć wszystkie bezsetowe grupy cztero-, pięcio- czy może sześciokartowe i później próbować kombinować je ze sobą, dodatkowo starając się wyzyskać różne symetrie układu. Gdyby w ten sposób uzyskać wszystkie grupy - powiedzmy - 15 czy 16 elementowe, dalej można je pogłębiać siłowo, nie powinno być ich wiele.
Ostatecznie, dzięki Arturowi Popławskiemu (warto było wpaść na Buzzie w towarzystwo matematyków) dowiedziałem się, że problem zyskał status całkiem poważnego problemu matematycznego a jednym z rozwiązań jest ta oto publikacja matematyków z Wenezueli dowodząca min. że faktycznie, 21-elementowych zbiorów kart bez seta nie ma (a wszystkie 20-elementowe rozwiązania są modulo symetrie identyczne). Aha, dla większej niż 4 liczby wymiarów (cech kart) zagadnienie jest otwarte.
Uczciwie mówiąc, trochę ciężki i żmudny wydaje mi się ten dowód i ciągle wierzę, że może istnieć jakiś cwany, zgrabniejszy sposób.
Dalsza zabawa
Poza próbami znalezienia krótszego, ładniejszego dowodu czy napisania kodu wydajnie wykluczającego 21-elementowe sety interesująca wydaje mi się próba poszacowania ilości dużych zbiorów bez setów - np. ustalenia, ile jest takich zbiorów 18-o czy 15-o kartowych albo przynajmniej oszacowania tej wartości. Końcem takich prac mogłoby być formalne wyprowadzenie prawdopodobieństwa wylosowania 18, 15 i 12 kart bez seta.
Bonus
Jako bonus załączam kawałek kodu w Pythonie, przy pomocy którego namalowałem powyższy obrazek (zamieniający kodowe oznaczenia setów na malunek z kartami).
# -*- coding: utf-8 -*-
#
# SET-painter. Paints image with given SET cards
######################################################################
# Painting B-spline, according to
# http://stackoverflow.com/questions/2534786/drawing-a-clamped-uniform-cubic-b-spline-using-cairo
# with slight modifications (to make it possible to paint
# filled shape made of splines)
######################################################################
from collections import namedtuple
class Point(namedtuple("Point", "x y")):
__slots__ = ()
def interpolate(self, other, ratio = 0.5):
return Point(x = self.x * (1.0-ratio) + other.x * float(ratio), \
y = self.y * (1.0-ratio) + other.y * float(ratio))
class CubicBSpline(object):
__slots__ = ("points", )
def __init__(self, points):
self.points = [Point(*coords) for coords in points]
def clamped(self):
new_points = [self.points[0]] * 3 + self.points + [self.points[-1]] * 3
return CubicBSpline(new_points)
class BSplineDrawer(object):
def __init__(self, context):
self.context = context
def draw(self, bspline):
pairs = zip(bspline.points[:-1], bspline.points[1:])
one_thirds = [p1.interpolate(p2, 1/3.) for p1, p2 in pairs]
two_thirds = [p2.interpolate(p1, 1/3.) for p1, p2 in pairs]
poly_points = []
coords = [None] * 6
for i in xrange(len(bspline.points) - 3):
start = two_thirds[i].interpolate(one_thirds[i+1])
coords[0:2] = one_thirds[i+1]
coords[2:4] = two_thirds[i+1]
coords[4:6] = two_thirds[i+1].interpolate(one_thirds[i+2])
self.context.move_to(*start)
self.context.curve_to(*coords)
#self.context.stroke()
poly_points.append(start)
# Painting the border before extending spline with interior
self.context.stroke_preserve()
# Adds internal polygon to selection to have fill/clip
# working properly
self.context.move_to(* poly_points[0])
for pp in poly_points[1:]:
self.context.line_to(*pp)
self.context.line_to(* poly_points[0])
######################################################################
# Card painting
######################################################################
import cairo, math
def paint_card(card_code, ctx, top_x, top_y, width, height):
"""
Paints card of given code (code is a string of 4 digits)
on given Cairo context in the specified place. Card is painted
with both border and symbols, but without background.
"""
# Shape routines. All draw given shape border and leave
# it's interior as selection
def shape_diamond(min_x, min_y, max_x, max_y):
half_hsize = (max_x - min_x)/2
half_vsize = (max_y - min_y)/2
ctx.move_to(min_x, min_y + half_vsize)
ctx.rel_line_to(half_hsize, -half_vsize)
ctx.rel_line_to(half_hsize, half_vsize)
ctx.rel_line_to(-half_hsize, half_vsize)
ctx.close_path()
ctx.stroke_preserve()
def shape_oval(min_x, min_y, max_x, max_y):
#ctx.new_sub_path()
med_y = (min_y + max_y) / 2
radius = med_y - min_y
ctx.arc(min_x + radius, med_y, radius,
math.pi/2, 3*math.pi/2 )
ctx.rel_line_to(max_x - min_x - 2 * radius, 0)
ctx.arc(max_x - radius, med_y, radius,
3*math.pi/2, math.pi/2 )
ctx.close_path()
ctx.stroke_preserve()
def shape_tilde(min_x, min_y, max_x, max_y):
hsize = max_x - min_x
vsize = max_y - min_y
# Experimentally picked control points for b-spline
points = [
(min_x + 0.1 * hsize, max_y),
(min_x, max_y - 0.24 * vsize),
(min_x + 0.3 * hsize, min_y),
(min_x + 0.58 * hsize, min_y + 0.14*vsize),
(min_x + 0.68 * hsize, min_y + 0.19*vsize),
(max_x - 0.07 * hsize, min_y)
]
# Bottom points mirror top
points += [
(max_x + min_x + 0.1*hsize - x, min_y + max_y -y)
for (x,y) in points ]
# Wrapping shape up
points.append(points[0])
# Fix for small dilatation observed
points = [ (x-0.05 * hsize, y) for (x,y) in points ]
spline = CubicBSpline(points).clamped()
BSplineDrawer(ctx).draw(spline)
ctx.close_path()
# Fill routines. All assume selection to fill
def fill_empty(min_x, min_y, max_x, max_y):
pass
def fill_filled(min_x, min_y, max_x, max_y):
ctx.fill()
def fill_hashed(min_x, min_y, max_x, max_y):
hsize = max_x - min_x
vsize = max_y - min_y
line_step = hsize/8 # space between lines
line_angl = hsize/10 # space between end and start of single line
ctx.save()
ctx.clip()
x = min_x + 2
while x <= max_x + line_angl:
ctx.move_to(x, min_y)
ctx.rel_line_to(- line_angl, vsize)
x += line_step
ctx.stroke()
ctx.restore()
# Main work
CARD_COLORS = [ (1.0,0,0), (0,1.0,0), (0.5,0.1,0.7) ]
SHAPE_ROUTINES = [ shape_diamond, shape_oval, shape_tilde ]
FILL_ROUTINES = [ fill_empty, fill_filled, fill_hashed ]
START_RATIOS = [ (0.5,), (0.3,0.7), (0.18,0.5,0.82) ]
color_code, shape_code, fill_code, count = [int(x) for x in card_code]
color = CARD_COLORS[color_code]
shape_rtn = SHAPE_ROUTINES[shape_code]
fill_rtn = FILL_ROUTINES[fill_code]
start_locations = START_RATIOS[count]
# Border
ctx.new_path()
ctx.set_source_rgb (0, 0, 0)
ctx.set_line_width(3.0)
ctx.rectangle(top_x + 5, top_y + 5, width-10, height-10)
ctx.stroke()
vsize = 0.25 * height
hsize = 0.75 * width
ctx.set_line_width(5.0)
ctx.set_source_rgb(*color)
spacer = (width-hsize)/2
min_x = top_x + spacer
max_x = top_x + width - spacer
for start in start_locations:
med_y = top_y + start*height
min_y = med_y - 0.5 * vsize
max_y = med_y + 0.5 * vsize
ctx.new_path() # To avoid lines to first arc etc
shape_rtn(min_x, min_y, max_x, max_y)
fill_rtn(min_x, min_y, max_x, max_y)
def create_cards_image(cards, card_width, card_height, output_file):
"""
Paints image of given cards and saves it inside given file (as .png)
"""
cards_count = len(cards)
cards_per_row = max(1, int(math.sqrt(cards_count)))
rows = (cards_count + cards_per_row - 1) // cards_per_row
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
card_width * cards_per_row, card_height * rows)
ctx = cairo.Context(surface)
# White background
ctx.set_source_rgb (1, 1, 1)
ctx.set_operator (cairo.OPERATOR_SOURCE)
ctx.paint()
for card_no, card in enumerate(cards):
row = card_no // cards_per_row
pos = card_no % cards_per_row
xloc = pos * card_width
yloc = row * card_height
paint_card(card, ctx, xloc, yloc, card_width, card_height)
surface.write_to_png(output_file)
######################################################################
# Main
######################################################################
if __name__ == "__main__":
best = ["0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1012", "1022", "1102", "1202", "2012", "2102", "2110", "2111", "2122", "2212"]
create_cards_image(best, 300, 300, "OUT/best.png")
all = list( "%d%d%d%d" % (a,b,c,d)
for a in [0,1,2]
for b in [0,1,2]
for c in [0,1,2]
for d in [0,1,2] )
create_cards_image(all, 300, 300, "OUT/all.png")

