PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Problem ośmiu hetmanów Metoda prób i błędów Drzewo przeszukiwań algorytmu Formularz aplikacji Implementacja metody prób i błędów w C# Program w C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Problem ośmiu hetmanów

Zgodnie z regułami szachowymi hetman szachuje inne figury ustawione na szacho­wnicy w tym samym wierszu, kolumnie i na przeką­tnych, na których stoi (rys.). Problem ośmiu hetmanów polega na usta­wieniu na szacho­wnicy ośmiu hetmanów tak, aby żaden nie szachował innego. Naszym zadaniem jest opraco­wanie programu okienko­wego bez wejścia (brak wczytywania danych), który znajduje wszystkie ustawienia ośmiu hetmanów stano­wiące rozwią­zanie problemu.

Warto przy okazji wspomnieć, że problemem ośmiu hetmanów zajmował się znako­mity niemiecki matematyk Carl Friedrich Gauss (1777-1855), ale nie udało mu się znaleźć pełnego rozwią­zania. Trudno się temu dziwić, gdyż problemu nie da się rozwiązać anality­cznie, lecz metodą prób i błędów, która ze względów czasowych wymaga użycia komputera. Liczba możliwych ustawień hetmanów na szachownicy po jednym w kolumnie jest przeo­gromna, wynosi 88=16777216. Chociaż tylko niektóre stanowią rozwią­zanie problemu, to te pozostałe trzeba jakoś wyelimi­nować.

Metoda prób i błędów

Problem ośmiu hetmanów można rozwiązać za pomocą algorytmu z powrotami polega­jącego na poszuki­waniu rozwią­zania metodą prób i błędów. Jest oczywiste, że w jednej kolumnie szacho­wnicy można ustawić tylko jednego hetmana. Wynika stąd, że po ustawieniu k–1 hetmanów w kolu­mnach od 1 do k–1 należy podjąć próbę znale­zienia takiego wiersza w kolumnie k, żeby pole na skrzyżo­waniu tego wiersza i kolumny nie było szacho­wane przez ustawio­nych już hetmanów. Rozumo­wanie to prowadzi do wstępnego sformuło­wania metody podejmu­jącej próbę ustawienia hetmana w kolumnie k i ewentu­alnie kolejnych hetmanów w nastę­pnych kolumnach:

void Próbuj(int k)
{
    for (int w = 1; w <= 8; w++)
        if (pole (w,k) nie jest szachowane)
        {
            ustaw hetmana w polu (w,k);
            if (k < 8)
                Próbuj(k + 1);
            else
                zapisz ustawienie hetmanów;
            usuń hetmana z pola (w,k);
        }
}

Metoda przebiega wszystkie wiersze w kolumny okre­ślonej w argu­mencie w poszuki­waniu wolnego pola. Jeśli takie pole zostaje znale­zione, umieszcza w nim hetmama i wywołuje rekuren­cyjnie samą siebie dla nastę­pnej kolumny, gdy taka kolumna istnieje, albo zapamię­tuje ustawienie wszystkich hetmanów (8 numerów wierszy), gdy jest to ostatnia kolumna szacho­wnicy. Na koniec metoda usuwa hetmana z tego pola, by było wolne przy rekuren­cyjnym podejmo­waniu prób dla następnych pól tej samej kolumny i kolumn o niższych numerach.

Algorytm uaktywniamy za pomocą wywołania metody Próbuj z argu­mentem 1, które rozpo­czyna poszuki­wanie ustawień ośmiu hetmanów od kolumny 1, czyli dla szacho­wnicy, na której żaden hetman nie jest ustawiony.

Drzewo przeszukiwań algorytmu

Działanie algorytmu możemy prześledzić na jego drzewie przeszu­kiwań, którego początkowy fragment jest pokazany na poniż­szym rysunku. Z każdego wierz­chołka, którego numer jest wartością argu­mentu k wywołania metody Próbuj, wiedzie osiem krawędzi do wierz­chołków o numerach równych wartości wyrażenia k+1. Krawędzie te odpowia­dają wartościom od 1 do 8 zmiennej w steru­jącej pętlą for, czyli numerom przeglą­danych wierszy szacho­wnicy w poszuki­waniu wolnego pola w kolu­mnie k. Część tych krawędzi jest obcinana, przez co wywodzące się od nich poddrzewa są elimi­nowane z dalszych poszukiwań, ponieważ prowadzą do "ślepego zaułka".

Pierwszego hetmana możemy ustawić w każdym z ośmiu wierszy pierwszej kolumny, dlatego z wierz­chołka 1 wychodzi osiem krawędzi do wierz­chołków 2. Gdy ustawimy go w pierwszym wierszu, drugiego hetmana możemy ustawić w wierszach od trzeciego do ósmego drugiej kolumny, dlatego dwie krawędzie wychodzące z wierz­chołka 2 są obcięte. Z kolei trzeciego hetmana, przy usta­wieniu pierwszego w wierszu pierwszym i drugiego w trzecim, możemy ustawić dopiero w piątym wierszu trzeciej kolumny, skutkiem tego cztery krawędzie wychodzące z wierz­chołka 3 są obcięte. Generalnie obcinane są te krawędzie, a wraz z nimi całe poddrzewa, które opisują drogę nieprowa­dzącą do rozwią­zania, gdyż określają ustawienie kolejnego hetmana w polu szacho­wanym.

Dotarcie do wierzchołka o numerze 8 oznacza, że osiem hetmanów zostało ustawionych na szacho­wnicy w taki sposób, że żaden z nich nie szachuje żadnego innego. Znalezione ustawienie hetmanów stanowi zatem rozwiązanie problemu, dlatego zamiast kolejnego wywołania rekuren­cyjnego zostaje zapisane w rejestrze wszystkich rozwiązań.

Formularz aplikacji

Utworzonemu przez Visual Studio formularzowi aplikacji nadajemy tytuł Problem ośmiu hetmanów, a jego właści­wości FormBorder­StyleMaxi­mizeBox ustawiamy na Fixed­Single (pojedyncza stała ramka) i False (zablo­kowany przycisk maksyma­lizacji). Następnie wstawiamy do niego dwie kontrolki – siatkę DataGrid­View do prezen­tacji tabela­rycznej listy wszystkich znale­zionych rozwiązań po jednym w wierszu i kontrolkę PictureBox do wizuali­zacji grafi­cznej wybranego w siatce rozwią­zania, a także dwie opisujące te kontrolki etykiety Label. Obie kontrolki prezenta­cyjne, którym Visual Studio nadało nazwy dataGrid­View1picture­Box1, wymagają dostoso­wania do potrzeb programu (rys.). Najprościej zacząć od określenia rozmiaru (właści­wość Size) i stylu obramo­wania (Border­Style) drugiej z nich. Kontrolka ma pokazywać układ hetmanów na szacho­wnicy. Jeśli przyjmiemy, że pola szacho­wnicy są kwadratami o boku 36 pikseli i ramka wokół niej ma grubość 1 piksela (styl Fixed­Single), to należyty rozmiar kontrolki wynosi 290x290 pikseli.

Nieco skomplikowane jest konfigurowanie wyglądu i zacho­wania kontrolki dataGrid­View1. Każdy jej wiersz ma zawierać komórkę nagłów­kową z numerem kolejnym rozwią­zania i osiem komórek z numerami wierszy szacho­wnicy określa­jących pozycje hetmanów w kolumnach od 1 do 8. Siatka rozpo­czyna się wierszem nagłówka kolumn (usta­wienie domyślne), który nie jest na początku widoczny, gdyż wstępnie nie ma ona żadnych kolumn. Zatem dodajemy do siatki osiem kolumn, posłu­gując się oknem Dodaj kolumnę przywo­łanym za pomocą łącza Dodaj kolumnę... dostę­pnego w oknie Właściwości kontrolki. Za każdym razem zapropo­nowany tekst nagłówka zmieniamy na jedno­cyfrowy numer kolumny i naciskamy przycisk Dodaj (rys.), a po dodaniu ósmej kolumny zamykamy okno przyci­skiem Zamknij.

Wszystkie kolumny siatki mają domyślną szerokość 100 pikseli, bez wątpienia zbyt dużą dla jednocy­frowych numerów wierszy szacho­wnicy. Korygujemy ją w oknie Edytuj kolumny, które otwieramy za pomocą łącza Edytuj kolumny... dostę­pnego w oknie Właściwości kontrolki dataGrid­View1. Szerokość kolumny zmniej­szamy, nadając jej właści­wości Width np. wartość 21 (rys.). Okno zawiera panel wyboru kolumny, toteż nie trzeba go zamykać po każdej korekcie, można go zamknąć po edycji wszystkich kolumn, zatwier­dzając zmiany przyciskiem OK.

Ze względów estetycznych pożądane jest określenie sposobu wyrównania tekstu komórek siatki. Właściwe wyrównanie wskazujemy w oknie Konstru­ktor elementu CellStyle (rys.) udostę­pnionym za pomocą przycisku, który pojawia się po wybraniu właści­wości Default­CellStyle kontrolki. Odpowie­dnią wartością, którą znajdujemy na liście właści­wości Alignment, jest Middle­Center (wyrównanie w pionie i w poziomie). Analogi­cznie określamy sposób wyrównania tekstu komórek nagłów­kowych wierszy, wybie­rając właści­wość RowHeaderDefault­CellStyle kontrolki i wartość MiddleRight (wyrównanie w pionie i w poziomie po prawej stronie).

Szerokość komórek nagłówkowych wierszy siatki propono­waną we właści­wości RowHeaders­Width zmieniamy na 50. Konse­kwencją zmian szero­kości wszystkich komórek jest ustalenie rozmiaru kontrolki dataGrid­View1 na 238x290. Większa o 20 od obli­czonej wartości szerokość wynika z uwzglę­dnienia domyślnej cienkiej ramki i miejsca dla pionowego paska przewi­jania. Pasek poziomy jest zbyteczny, dlatego odpowiednią wartością właści­wości ScrollBars jest Vertical (pionowy). Nie jest to bynaj­mniej koniec konfigu­racji kontrolki, ponieważ ma ona jedynie udostę­pniać wyniki programu do przeglą­dania bez możliwości ich modyfiko­wania. Aby uzyskać takie jej zacho­wanie, wybie­ramy następu­jące właści­wości i ich ustawienia:

Implementacja metody prób i błędów w C#

Narzucającą się reprezentacją szachownicy jest tablica dwuwymia­rowa typu bool o rozmiarze 8x8, której elementy o wartości false oznaczają brak, a true obecność hetmana w określo­nych przez nie polach. Wówczas sprawdzenie, czy pole, na którym ma być ustawiony hetman, nie jest szachowane, można by sprecy­zować w postaci metody C#. Zważywszy jednak na fakt, że będzie to najczęściej wykonywana operacja, warto pokusić się o lepszą reprezen­tację danych, która pozwala­łaby na bardziej bezpo­średni test, czy w danym polu wolno ustawić hetmana.

Interesującą reprezentację danych zaproponował Niklaus Wirth w książce pt. Algorytmy + struktury danych = programy (WNT, Warszawa 1980 i nowsze wydania, obecnie niedo­stępne). Wykorzystując fakt, że wszystkie pola leżące na przeką­tnej o kierunku ⬈ charakte­ryzują się stałą wartością k+w (numer kolumny + numer wiersza), a pola na przeką­tnej ⬊ stałą wartością k–w, użył czterech tablic do określenia pozycji hetmana i stanu pola. Zaprezen­towany w notacji języka Pascal pomysł prowadzi w C#, przy uwzglę­dnieniu natu­ralnej w tym języku indeksacji elementów tablic oraz nume­racji kolumn i wierszy szachownicy od zera wzwyż, do dekla­racji czterech tablic:

private int[]  x = new int[8];      // Ustawienie (wiersz) hetmana
private bool[] a = new bool[8];     // Brak hetmana w wierszu
private bool[] b = new bool[15];    // Brak hetmana na przekątnej /
private bool[] c = new bool[15];    // Brak hetmana na przekątnej \

Dokładniej, znaczenie elementów tych tablic jest następujące:

Nietrudno zauważyć, że test sprawdzający, czy pole na skrzyżo­waniu kolumny k i wiersza w nie jest szacho­wane, sprowadza się teraz do obliczenia wartości wyrażenia logicznego

a[w] & b[k + w] & c[k - w + 7]

Operację ustawienia w tym polu hetmana można zapisać za pomocą następu­jących przypisań:

x[k] = w;
a[w] = b[k + w] = c[k - w + 7] = false;

Prosta jest również operacja usunięcia hetmana:

a[w] = b[k + w] = c[k - w + 7] = true;

Przed sformułowaniem ostatecznej wersji metody Próbuj powinniśmy zdecy­dować, gdzie ma ona zapisywać znajdowane rozwią­zania reprezen­towane przez ciągi ośmiu numerów wierszy określa­jących ustawienie hetmanów na szacho­wnicy. Ponieważ liczba wszystkich rozwiązań nie jest znana, rozsądnym wydaje się groma­dzenie ich w liście klasy genery­cznej, której dekla­racja ma postać:

private List<int> wiersz = null;    // Wszystkie ustawienia hetmanów

Elementami listy wiersz są liczby całkowite od 0 do 7 określa­jące wiersze szachownicy, w których są ustawione hetmany, po 8 liczb na jedno rozwią­zanie. Za każdym razem po wygene­rowaniu ustawienia ośmiu hetmanów w tablicy x można jej elementy dodać na końcu listy wiersz za pomocą metody AddRange. Zatem nume­rując kolumny i wiersze szacho­wnicy od 0 do 7, ostateczną wersję metody Próbuj możemy sformu­łować następu­jąco:

private void Próbuj(int k)
{
    for (int w = 0; w < 8; w++)
        if (a[w] & b[k + w] & c[k - w + 7])
        {
            x[k] = w;
            a[w] = b[k + w] = c[k - w + 7] = false;
            if (k < 7)
                Próbuj(k + 1);
            else
                wiersz.AddRange(x);
            a[w] = b[k + w] = c[k - w + 7] = true;
        }
}

Metodę Próbuj należy wywołać po raz pierwszy z argumen­tem 0 określa­jącym rozpo­częcie poszuki­wania rozwiązań od pustej szacho­wnicy. Naturalnie powinno to nastąpić po utworzeniu pustej listy wiersz i zainicja­lizowaniu wszystkich elementów tablic a, bc wartościami true oznacza­jącymi, że wszystkie pola szacho­wnicy są wolne. Całą tę pracę wykona metoda obsługi zdarzenia Load formu­larza, której niepełna jeszcze wersja ma postać:

private void Form1_Load(object sender, EventArgs e)
{
    ...                             // Utwórz bitmapę hetmana
    wiersz = new List<int>();
    for (int k = 0; k < 8; k++)
        a[k] = true;
    for (int k = 0; k < 15; k++)
        b[k] = c[k] = true;
    Próbuj(0);
    ...                             // Pokaż listę rozwiązań
}

Program w C#

Aby uatrakcyjnić wizualizację szachownicy z usta­wionymi na niej hetmanami, tworzymy mapę bitową o rozmiarze 15x32 pikseli wyobra­żającą figurę hetmana (rys. obok) na przezro­czystym tle. Możemy przy tym posłużyć się jakimkol­wiek narzędziem graficznym do edycji bitmap z obsługą warstwy przezro­czystości, np. popularnym programem GIMP. Obrazek zapisujemy w pliku queen.png w dowolnym folderze i dodajemy do zasobów programu. Dekla­rujemy również w klasie Form1 pole umożli­wiające dostęp do bitmapy:

private Bitmap Hetman = null;       // Obrazek hetmana

Następnie wracamy do przedstawionej powyżej niedokoń­czonej wersji metody Form1_Load, by uzupełnić zapowie­dziane w niej dwa fragmenty kodu. Mapę bitową reprezen­tującą hetmana tworzymy, korzystając z konstru­ktora klasy Bitmap i zasobu queen, a refe­rencję do utworzo­nego obiektu przypi­sujemy zmiennej Hetman:

Hetman = new Bitmap(Properties.Resources.queen);

Z kolei listę znalezionych rozwiązań problemu ośmiu hetmanów wyświetlamy w kontrolce dataGrid­View1, wywołując nieistnie­jącą jeszcze bezargu­mentową metodę Pokaż, która ma utworzyć wiersze siatki wypeł­nione tekstami określa­jącymi pozycje hetmanów na szacho­wnicy. Metodę tę formu­łujemy następu­jąco:

private void Pokaż()
{
    string[] s = new string[8];
    for (int p = 0; p < wiersz.Count; p += 8)
    {
        for (int k = 0; k < 8; k++)
            s[k] = (wiersz[p + k] + 1).ToString();
        int n = dataGridView1.Rows.Add(s);
        dataGridView1.Rows[n].HeaderCell.Value = (n + 1).ToString();
    }
}

Jak widać, metoda w kolejnych krokach zewnętrznej pętli for konwer­tuje powiększone o 1 elementy listy wiersz, po 8 w jednym kroku, na łańcuchy tablicy s, które dodaje na końcu siatki za pomocą metody Add właści­wości Rows (wiersze). Zwrócony przez Add numer utworzonego w ten sposób nowego wiersza siatki zostaje po zwiększeniu o 1 przekształ­cony na tekst komórki nagłówka wiersza. Rzecz jasna powiększanie numerów wierszy ma na celu zastą­pienie ich wewnę­trznej numeracji od zera wzwyż na bardziej naturalną dla człowieka numerację od 1 wzwyż.

Ostatnim etapem konstruowania programu jest precyzo­wanie operacji malowania kontrolki paintBox1 i wymu­szenia jej przemalo­wania, gdy na siatce dataGrid­View1 zostanie zazna­czony (wybrany) wiersz. Pierwsze zazna­czenie nastąpi, gdy w metodzie Pokaż do siatki zawiera­jącej jedynie wiersz nagłówka kolumn zostanie dodany pierwszy wiersz danych, a dalsze za każdym razem, gdy użytko­wnik wybierze za pomocą myszki lub klawia­tury inny wiersz (inne rozwią­zanie). Każda zmiana zazna­czenia powoduje wygenero­wanie zdarzenia Selection­Changed kontrolki dataGrid­View1. Pusty szkielet metody obsługi tego zdarzenia utworzony przez Visual Studio uzupeł­niamy do postaci:

private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
    int p = 8 * dataGridView1.CurrentRow.Index;
    for (int k = 0; k < 8; k++)
        x[k] = wiersz[p + k];
    pictureBox1.Invalidate();
}

Na począku metoda oblicza indeks pierwszego z ośmiu elementów listy wiersz określa­jących to samo ustawienie ośmiu hetmanów, co wyróżniony strzałką wiersz bieżący siatki o indeksie określonym w podwłaści­wości Index właści­wości zagnież­dżonej CurrentRow. Wyjaśnijmy, że ogólnie wierszy zazna­czonych może być więcej niż jeden, ale tylko jeden z nich jest wierszem bieżącym. Ze względu na przyjęte wcześniej ustawienia właści­wości Multi­SelectSelection­Mode na siatce daje się w rozpatry­wanym przypadku zaznaczyć tylko jeden wiersz, który staje się tym samym wierszem bieżącym. Zatem skopiowane do tablicy x elementy listy wiersz określają to samo rozwią­zanie, co zazna­czony wiersz siatki. Na koniec metoda unieważnia obszar roboczy kontrolki paintBox1, powiada­miając w ten sposób system opera­cyjny komputera, że obszar ten wymaga przemalo­wania.

Przystępujemy wreszcie do sprecyzowania metody obsługi zdarzenia Paint kontrolki paintBox1. Metoda ma narysować na powierz­chni kontrolki szacho­wnicę z ośmioma hetmanami ustawio­nymi na niej po jednym w kolumnie i wierszu o numerze określonym w elemencie tablicy x o indeksie równym numerowi kolumny. Zadanie rozwią­zujemy, rysując pola szacho­wnicy w podwójnej pętli przebie­gającej wiersze i kolumny o numerach od 0 do 7:

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    const int D = 36;
    int dx = (D - Hetman.Width) / 2;
    int dy = (D - Hetman.Height) / 2;
    Brush[] brush = { Brushes.White, Brushes.LightGray };
    for (int w = 0; w < 8; w++)
        for (int k = 0; k < 8; k++)
        {
            Rectangle r = new Rectangle(D * k, D * w, D, D);
            e.Graphics.FillRectangle(brush[(k + w) % 2], r);
            if (x[k] == w)
                e.Graphics.DrawImage(Hetman, r.Left + dx, r.Top + dy);
        }
}

Każde pole jest reprezentowane na obszarze roboczym kontrolki przez prostokąt o rozmiarze 36x36 pikseli wypełniony za pomocą metody FillRectangle klasy Graphics kolorem białym, gdy suma numeru kolumny i wiersza pola jest liczbą parzystą, lub jasno­szarym, gdy jest liczbą niepa­rzystą. Jeśli numery te określają lokali­zację hetmana zanoto­waną w tablicy x, pośrodku prosto­kąta rysowana jest bitmapa Hetman przedsta­wiająca obrazek hetmana.

Pełny program okienkowy w języku C# znajdujący wszystkie rozwią­zania problemu ośmiu hetmanów metodą prób i błędów jest przedsta­wiony na poniższym listingu. Aby uniknąć wysieku pamięci, w programie zdefinio­wano metodę obsługi zdarzenia FormClosed formu­larza wyzwala­nego tuż po zamknięciu okna, której zadaniem jest zwolnienie pamięci przydzie­lonej bitmapie Hetman.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Hetmany
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Bitmap Hetman = null;       // Obrazek hetmana
        private List<int> wiersz = null;    // Wszystkie ustawienia hetmanów
        private int[] x = new int[8];       // Ustawienie (wiersz) hetmana
        private bool[] a = new bool[8];     // Brak hetmana w wierszu
        private bool[] b = new bool[15];    // Brak hetmana na przekątnej /
        private bool[] c = new bool[15];    // Brak hetmana na przekątnej \

        private void Form1_Load(object sender, EventArgs e)
        {
            Hetman = new Bitmap(Properties.Resources.queen);
            wiersz = new List<int>();
            for (int k = 0; k < 8; k++)
                a[k] = true;
            for (int k = 0; k < 15; k++)
                b[k] = c[k] = true;
            Próbuj(0);
            Pokaż();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (Hetman != null) Hetman.Dispose();
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            const int D = 36;
            int dx = (D - Hetman.Width) / 2;
            int dy = (D - Hetman.Height) / 2;
            Brush[] brush = { Brushes.White, Brushes.LightGray };
            for (int w = 0; w < 8; w++)
                for (int k = 0; k < 8; k++)
                {
                    Rectangle r = new Rectangle(D * k, D * w, D, D);
                    e.Graphics.FillRectangle(brush[(k + w) % 2], r);
                    if (x[k] == w)
                        e.Graphics.DrawImage(Hetman, r.Left + dx, r.Top + dy);
                }
        }

        private void dataGridView1_SelectionChanged(object sender, EventArgs e)
        {
            int p = 8 * dataGridView1.CurrentRow.Index;
            for (int k = 0; k < 8; k++)
                x[k] = wiersz[p + k];
            pictureBox1.Invalidate();
        }

        private void Próbuj(int k)
        {
            for (int w = 0; w < 8; w++)
                if (a[w] & b[k + w] & c[k - w + 7])
                {
                    x[k] = w;
                    a[w] = b[k + w] = c[k - w + 7] = false;
                    if (k < 7)
                        Próbuj(k + 1);
                    else
                        wiersz.AddRange(x);
                    a[w] = b[k + w] = c[k - w + 7] = true;
                }
        }

        private void Pokaż()
        {
            string[] s = new string[8];
            for (int p = 0; p < wiersz.Count; p += 8)
            {
                for (int k = 0; k < 8; k++)
                    s[k] = (wiersz[p + k] + 1).ToString();
                int n = dataGridView1.Rows.Add(s);
                dataGridView1.Rows[n].HeaderCell.Value = (n + 1).ToString();
            }
        }
    }
}

Efekt końcowy wykonania programu można zobaczyć na poniższym rysunku, który ukazuje początek listy 92 rozwiązań problemu ośmiu hetmanów wygenero­wanych przez algorytm i pierwsze znale­zione rozwią­zanie graficznie. Ze względu na symetrię szacho­wnicy istotnie różnych rozwiązań jest tylko 12.


Opracowanie przykładu: kwiecień 2020