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).
from collections import namedtuple |
class Point(namedtuple( "Point" , "x y" )): |
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 ): |
def __init__( self , points): |
self .points = [Point( * coords) for coords in points] |
new_points = [ self .points[ 0 ]] * 3 + self .points + [ self .points[ - 1 ]] * 3 |
return CubicBSpline(new_points) |
class BSplineDrawer( object ): |
def __init__( self , context): |
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] |
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) |
poly_points.append(start) |
self .context.stroke_preserve() |
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 ]) |
def paint_card(card_code, ctx, top_x, top_y, width, height): |
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) |
def shape_oval(min_x, min_y, max_x, max_y): |
med_y = (min_y + max_y) / 2 |
ctx.arc(min_x + radius, med_y, radius, |
ctx.rel_line_to(max_x - min_x - 2 * radius, 0 ) |
ctx.arc(max_x - radius, med_y, radius, |
def shape_tilde(min_x, min_y, max_x, max_y): |
(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) |
(max_x + min_x + 0.1 * hsize - x, min_y + max_y - y) |
points = [ (x - 0.05 * hsize, y) for (x,y) in points ] |
spline = CubicBSpline(points).clamped() |
BSplineDrawer(ctx).draw(spline) |
def fill_empty(min_x, min_y, max_x, max_y): |
def fill_filled(min_x, min_y, max_x, max_y): |
def fill_hashed(min_x, min_y, max_x, max_y): |
while x < = max_x + line_angl: |
ctx.rel_line_to( - line_angl, vsize) |
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] |
ctx.set_source_rgb ( 0 , 0 , 0 ) |
ctx.rectangle(top_x + 5 , top_y + 5 , width - 10 , height - 10 ) |
ctx.set_source_rgb( * color) |
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 |
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): |
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) |
ctx.set_source_rgb ( 1 , 1 , 1 ) |
ctx.set_operator (cairo.OPERATOR_SOURCE) |
for card_no, card in enumerate (cards): |
row = card_no / / cards_per_row |
pos = card_no % cards_per_row |
paint_card(card, ctx, xloc, yloc, card_width, card_height) |
surface.write_to_png(output_file) |
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) |
create_cards_image( all , 300 , 300 , "OUT/all.png" ) |