Take It or Leave It II: teleturniej na komputery


20

To druga z serii łamigłówek, które będę publikować w każdy poniedziałek o północy czasu PST. Pierwsza zagadka znajduje się tutaj .

Kontekst:

Samotny miliarder stworzył teleturniej, aby przyciągnąć najlepszych i najzdolniejszych programistów na świecie. W poniedziałki o północy wybiera jedną osobę z grupy kandydatów na uczestnika tygodnia i zapewnia im grę. Jesteś szczęśliwym uczestnikiem tego tygodnia!

Gra w tym tygodniu:

Host zapewnia dostęp API do stosu 10 000 cyfrowych kopert. Te koperty są losowo sortowane i zawierają w nich wartość od 1 do 10 000 USD (żadne dwie koperty nie zawierają tej samej wartości w dolarach).

Do dyspozycji masz 4 polecenia:

  1. Czytaj (): Przeczytaj cyfrę dolara w kopercie u góry stosu.

  2. Take (): dodaj cyfrę dolara z koperty do portfela teleturnieju i zdejmij kopertę ze stosu.

  3. Pass (): Zdejmij kopertę na górze stosu.

  4. Oracle (M): Zwraca średnią wartość następnych M kopert w stosie, nie wliczając tej, którą możesz obecnie odczytać ().

Zasady:

  1. Jeśli użyjesz Pass () na kopercie, pieniądze wewnątrz zostaną utracone na zawsze.

  2. Jeśli użyjesz Take () na kopercie zawierającej $ X, od tego momentu nie możesz nigdy używać Take () na kopercie zawierającej <$ X. Take () na jednej z tych kopert doda 0 USD do Twojego portfela.

  3. Jeśli użyjesz Wyroczni (M) podczas tury T, koperty od T + 1 do T + M zostaną zwrócone. Wyrocznia () jest wyłączona do momentu, aż T + M.

Napisz algorytm, który kończy grę z maksymalną kwotą pieniędzy.

Jeśli piszesz swój algorytm w Pythonie, możesz użyć tego kontrolera dostarczonego przez @Maltysen: https://gist.github.com/livinginformation/70ae3f2a57ecba4387b5

Uwagi 1: „Maksymalne” w tym przypadku oznacza medianę w portfelu po N> = 1000 uruchomień. Oczekuję, choć chciałbym, aby udowodniono, że się mylę, że wartość mediany dla danego algorytmu zbiegnie się, gdy N wzrośnie do nieskończoności. Spróbuj zamiast tego zmaksymalizować średnią, ale mam wrażenie, że średnia jest bardziej narażona na wyrzucenie przez małe N niż mediana.

Uwaga 2: ponieważ wszystkie rozwiązania z poprzedniej części tej układanki są tutaj ważne, ich ponowne opublikowanie ma niewielką wartość. Tylko ulepszenia algorytmiczne poprzednich łamigłówek będą brane pod uwagę w części II.

Edycja: Warunek nagrody został usunięty w świetle tego postu na stronie meta.


Wow, nie mogę uwierzyć, że zaspałem: O
Beta Decay

@ Beta Zegar rozpadu tyka! :)
LivingInformation

Jaki jest sens wyścigu? Możesz zbudować własną bezpłatną wyrocznię, po prostu gromadząc wszystkie wcześniej przeczytane koperty. Co się mylę?
Luis Mendo,

1
@LuisMendo Dzięki własnemu zestawieniu możesz poznać jedynie średnią wszystkich pozostałych wartości. Z wyrocznią możesz uzyskać średnią z następnych Mwartości, gdzie możesz dokonać wyboru M.
Reto Koradi

1
Ponieważ wszystkie rozwiązania poprzedniego wyzwania są również poprawnymi rozwiązaniami tego wyzwania, czy możemy uznać je za dorozumiane?
Reto Koradi

Odpowiedzi:


9

Groovy 713337 817829 818227 USD

Kod bootstrap:

class Instance {
    List values = new ArrayList(1..10000); {
        Collections.shuffle(values)
    }
    int i = 0
    int value = 0
    int max = 0
    int nextOracle = 0

    def pass() {
        if (i >= 10000)
            throw new NoSuchElementException()
        i++
    }

    def take() {
        if (i >= 10000)
            throw new NoSuchElementException()
        int v = values[i]
        if (v > max) {
            max = v
            value += v
        }
        i++
    }

    double oracle(int m) {
        if (m <= 0 || i < nextOracle || i + m >= 10000)
            throw new NoSuchElementException()

        nextOracle = i + m
        values.subList(i + 1, i + m + 1).stream().reduce { l, r -> r+l }.get() / m
    }

    int read() {
        if (i >= 10000)
            throw new NoSuchElementException()
        values[i]
    }
}

Algorytm

double square(double v) { v * v }
final double factor = Math.pow(1.5, 1.1)
int attempts = 5000
(1..attempts).stream().parallel().mapToLong {
    def puzzle = new Instance()

    int[] memory = 1..10000 // We will remember every envelope
    int memStart = 0

    while (memStart < 10000 - 3) {
        int value = puzzle.read()
        int i = Arrays.binarySearch(memory, memStart, 10000, value) - memStart
        if (i < 0) { // We can't use the money
            puzzle.pass()
            continue
        }
        if (i == 0) { // Of course we take the lowest
            puzzle.take()
            memStart++
            continue
        }
        int remaining = Arrays.stream(memory, i + 1 + memStart, 10000).sum() // Money we could win if taken
        int losing = Arrays.stream(memory, memStart, memStart + i).sum() // Money we cna't win if taken
        if (value > losing) { // If we pass, we lose money automatically
            puzzle.take()
            memStart += i + 1
        } else if ((losing - value * 16 / 7) * square(Math.log(i)) > remaining / factor) {
            System.arraycopy(memory, memStart, memory, ++memStart, i)
            puzzle.pass()
        } else {
            puzzle.take()
            memStart += i + 1
        }
    }

    // It's broken down to last three elements
    List values = Arrays.copyOfRange(memory, 10000 - 3, 10000)
    while (!values.contains(puzzle.read())) // Skip values we can't use
        puzzle.pass()
    int value1 = puzzle.read()
    int value2 = puzzle.oracle(1)
    if (value1 == values.max() && (
            values.contains(value2)
            ? (value1 * 2 < values.sum() && values.min() == value2)
            : (value1 < values.min() / 2 + (values - [value1]).max())
            )) {
        puzzle.pass()
    }

    // Finish it
    while (puzzle.i < puzzle.values.size()) {
        puzzle.take()
    }

    puzzle.value as Long
}.sum() / attempts // Sum runs and average

Porównuję pozostałe wartości z możliwymi wartościami. Ten skrypt nie jest szybki (zajmuje 1 minutę na 1000x symulacji) ... ale wykona symulacje jednocześnie.

Nie mam pojęcia, dlaczego mój algorytm działa, ale było to tylko próba i błąd: łączenie operacji matematycznych razem i manipulowanie stałymi. Uruchomiłem go 5000x dla bieżącego wyniku, próbując zmniejszyć wahania wyniku (to +/- 4000 $ w zależności od liczby iteracji).

Nawet bez wyroczni na końcu powinno być (ledwo) pokonanie rozwiązania @ orlp dla poprzedniej układanki.


7

C # - 803,603 USD teraz -> 804,760 USD (z wyrocznią)

Kod ładujący

public static class ShuffleExtension
{
    private static Random rng = new Random();  

    public static void Shuffle<T>(this IList<T> list)  
    {  
        int n = list.Count;
        while (n > 1) {  
            n--;  
            int k = rng.Next(n + 1);  
            T value = list[k];  
            list[k] = list[n];  
            list[n] = value;  
        }  
    }
}

public class Puzzle
{
    public List<int> Values = new List<int>(10000);

    public Puzzle()
    {
        for ( int i = 1; i <= 10000; i++ )
        {
            Values.Add(i);
        }
        Values.Shuffle();
    }

    public int i = 0;
    public int value = 0;
    public int max = 0;
    public int nextOracle = 0;

    public void Pass() {
        if ( i >= Values.Count )
            throw new IndexOutOfRangeException();
        i++;
    }

    public void Take() {
        if (i >= Values.Count )
            throw new IndexOutOfRangeException();
        int v = Values[i];
        if (v > max) {
            max = v;
            value += v;
        }
        i++;
    }

    public double oracle(int m) {
    if (m <= 0) { 
        throw new IndexOutOfRangeException();
    }
    if ( i < nextOracle ) {
        throw new IndexOutOfRangeException();
    }
    if ( i + 1 + m > Values.Count ) {
        throw new IndexOutOfRangeException();
    }

    nextOracle = i + m;
    var oracleValues = new List<int>();
    for ( int l = 0; l < m; l++ )
    {
        oracleValues.Add(Values[i + 1 + l]);
    }
    return oracleValues.Average (v => v);
}

    public int Read() {
        if (i >= Values.Count )
            throw new IndexOutOfRangeException();
        return Values[i];
    }
}

Kod gry:

    void Main()
{
    var m = 0;
    for ( int l = 0; l < 1000; l++ )
    {
        var game = new Puzzle();
        var maxVal = 0;
        var lastOracle = 0;
        var lastOracleValue = 0.0m;
        var oracleValueForIOf = 0;

        for ( int i = 0; i < 10000; i++ )
        {
            var val = game.Read();
            var oracleStep = 1;
            var canUseOracle = (i - lastOracle >= oracleStep) && i + oracleStep + 1 <= 10000;
            if ( canUseOracle )
            {
                var oracle = game.oracle(oracleStep);
                lastOracle = i;
                lastOracleValue = (decimal)oracle;
                oracleValueForIOf = i + 1;
            }
            if ( TakeTheMoney(val, maxVal, oracleValueForIOf, lastOracleValue, i) )
            {
                maxVal = val;
                game.Take();
            }
            else
            {
                game.Pass();
            }
        }
        m += game.value;
    }
    ((int)(m / 1000)).Dump();
}

private bool TakeTheMoney(int val, int maxVal, int oracleValueForIOf, decimal lastOracleValue, int i)
{
    if ( val > maxVal )
    {
        if ( oracleValueForIOf != i + 1
            &&
            (val < 466.7m + (0.9352m * maxVal) + (0.0275m * i))
            )
        {
            return true;
        }

        if (oracleValueForIOf == i + 1)
        {
            if ( val < 466.7m + (0.9352m * maxVal) + (0.0275m * i) )
            {
                return true;
            }
            if ( lastOracleValue > 466.7m + (0.9352m * val) + (0.0275m * i + 1) )
            {
                if ( val < 466.7m + (0.9352m * maxVal) + (0.0275m * i + 1) )
                {
                    return true;
                }
            }
        }
    }
    return false;
}

Kredyt należy do Reto Koradi ( /codegolf//a/54181/30910 )

Edycja: zaimplementowano podstawowe użycie Oracle. Jeśli następna wyrocznia jest powyżej progu, aby użyć, rozwiń bieżącą obwiednię do indeksu Oracle Index. To nie trafia często, ale TO JEST Ulepszenie ;-)


4
Nie sądzę, aby repostowanie rozwiązań z poprzedniego wyzwania było bardzo produktywne. Wszyscy zdaliśmy sobie sprawę, że rozwiązania te mogą być wykorzystane jako podstawa do tego wyzwania, a ja już zostawiłem komentarz dla PO z pytaniem, jak powinniśmy sobie z tym poradzić. Chodzi o to, że wymyślisz własne rozwiązanie, które jest idealnie lepsze niż rozwiązania poprzedniego wyzwania.
Reto Koradi,

proszę przestać głosować :) po moim zgłoszeniu dodano notatkę nr 2. a ponieważ jest bardziej skuteczny niż inne rozwiązania - zamieściłem go tutaj. nie trzeba używać Oracle, aby pokonać istniejące rozwiązania.
Stephan Schinkel,

@StephanSchinkel Masz mój głos, jeśli uda Ci się dołączyć Wyrocznię, aby poprawić bieżący wynik. Nawet o 1 USD.
Dorus

@BetaDecay, co dokładnie spotyka społeczność? Właśnie podążyłem za pytaniem z op. Jeszcze raz uwaga 2 została dodana PO moim zgłoszeniu.
Stephan Schinkel

Nie używać rozwiązania z części I quizu.
Stephan Schinkel

4

Python - 74112 USD

Weź tylko, jeśli bieżąca wartość jest niższa niż następna wartość (tzn. Możesz wziąć obie).

def algo():
  try:
    o=oracle(1)
  except ValueError:
    take()
  r=read()
  if r>o:
    passe()
  else:
    take()

Python - (wciąż oblicza średnią)

Ta odpowiedź trwa BARDZO DŁUGO. Osiąga około 670 000 $ . Pamiętam każdą kopertę, którą widziałem. Za każdym razem, gdy muszę podjąć decyzję, generuję dwie listy pozostałych kopert, które potencjalnie mogę dodać do mojego portfela, jeśli odpowiednio wezmę bieżącą kopertę lub ją zostawię.

Nie zoptymalizowałem kodu.

def algo_2():
  global max_taken, past
  weight=0.92 #Empirically chosen.
  r=read()
  if len(past)==0:
    past.append(r)
    passe()
    return
  if r<max_taken:
    past.append(r)
    take() #the same as passe
    return
  coming=[x for x in range(1,10001) if x not in past and x>max_taken and x!=r ]
  comingIfTake=[x for x in range(1,10001) if x not in past and x>r ]
  if sum(coming)*weight<=sum(comingIfTake)+r:
    past.append(r)
    take()
  else:
    past.append(r)
    passe()

I init_game zaczyna się tak:

def init_game():
    global stack, wallet, max_taken, oracle_turns, past
    past=[]

3
Jeśli użyjesz zestawów do reprezentowania przeszłości, nadchodzącej i nadchodzącej funkcji If i zastosuj skrzyżowania, kod będzie znacznie szybszy.
Nathan Merrill,

4

C # - 780,176 USD

Sprawdź, czy następna wartość nie przekracza 5% wszystkich pozostałych wartości. Zbliżamy się do końca.

public class Taker
{
    private List<int> remaining;
    private Game game;

    public Taker(Game game)
    {
        this.game = game;
        remaining = Enumerable.Range(1, game.Size + 100).ToList();
    }

    int score = 0;

    public int PlayGame()
    {
        for (int i = 0; i < game.Size; i++)
        {
            if (game.Read() < game.Max ||
                game.Read() > selectThreshold() ||
                doOracle()
                )
            {
                remaining.Remove(game.Read());
                game.Pass();
                continue;
            }
            remaining = remaining.SkipWhile(j => j < game.Read()).ToList();
            score += game.Take();
        }
        return score;
    }

    private bool doOracle()
    {
        return game.Oracle(1) < game.Read() &&
            game.Oracle(1) > game.Max;
    }

    private int selectThreshold()
    {
        int selector = (int)(remaining.Count * 0.05);
        return remaining.ElementAt(selector);
    }
}

Moja klasa gry, bardzo brzydka, nie sprawdza nawet, czy wyrocznia jest dozwolona, ​​ale ponieważ używam tylko Oracle (1), nie powinno to stanowić problemu.

public class Game
{
    private int[] list;
    private int position = 0;
    private int max = 0;
    public int Max { get { return max; } }
    public int Size { get { return list.Length; } }

    public Game(int[] list)
    {
        this.list = list;
    }

    public int Read()
    {
        return list[position];
    }

    public int Take()
    {
        if (list[position] < max)
        {
            position++;
            return 0;
        }
        max = list[position];
        return list[position++];
    }

    public void Pass()
    {
        position++;
    }

    public int Oracle(int M)
    {
        int next = position + 1;
        M = Math.Max(0, Math.Min(M, list.Length - next));
        return new ArraySegment<int>(list, next, M).Sum();
    }
}

4

Java, 804,991 USD

Wynik pochodzi z 1001 rund. Prawdopodobnie jest zbyt blisko, aby zadzwonić między tą odpowiedzią a odpowiedzią Stephana Schinkla .

Opiera się to na mojej odpowiedzi z poprzedniego wyzwania, ponieważ używa tego samego obliczenia opartego na entropii do oszacowania wypłat. Główną różnicą jest to, że po prostu teraz pobiera koperty parami (1 i 2, a następnie 3 i 4 itd.) I analizuje możliwe kombinacje odbioru, przyjęcia, podania itp. Oblicza również dokładny szacunkowy wynik, gdy liczba prawidłowych kopert jest naprawdę mała.

„Opakowanie”, które napisałem, tak naprawdę nie jest prawdziwym opakowaniem, po prostu daje koperty parami zamiast wywoływać Oracle(1)funkcję co drugą rundę.

Ogólnie rzecz biorąc, powiedziałbym, że pomimo zwiększonej złożoności, ten bot naprawdę nie jest lepszy niż mój poprzedni.

Gracz

import java.lang.Math;
public class Player2
{
    public int[] V;

    public Player2(int s)
    {
        V = new int[s];
        for(int i = 0; i<V.length; i++)
        {
            V[i] = i+1;
        }
        ////System.out.println();
    }

    public boolean [] takeQ(int x, int y)
    {
        //System.out.println("Look: " + x + " " + y);
        boolean [] move = new boolean[]{false,false};
        double max = 0;
        double val = 0;
        int[] nextV = V;

        ////System.out.println("look " + x);
        int i = find(V,x);
        if(i >= 0)  //if found
        {
            //try taking first envelope
            int[] newVt = takeSlice(V,i);
            //System.out.println("  T: " + ats(newVt));
            int j = find(newVt,y);
            if(j >= 0)
            {
                //try taking first and second
                int[] newVtt = takeSlice(newVt,j);
                val = x + y + calcVal(newVtt);
                //System.out.println("  TT: " + ats(newVtt) + " " + val);
                if(val > max)
                {
                    move = new boolean[]{true,true};
                    max = val;
                    nextV = newVtt;
                }
            }
            //try taking first and passing second
            int[] newVtp = passSlice(newVt,j);

            val = x + calcVal(newVtp);
            //System.out.println("  TP: " + ats(newVtp) + " " + val);
            if(val > max)
            {
                move = new boolean[]{true,false};
                max = val;
                nextV = newVtp;
            }
        }
        int[] newVp = passSlice(V,i);
        //System.out.println("  V: " + ats(V));
        //System.out.println("  P: " + ats(newVp));
        int j = find(newVp,y);
        if(j >= 0)
        {
            //try passing first and taking second
            int[] newVpt = takeSlice(newVp,j);
            val = y + calcVal(newVpt);
            //System.out.println("  PT: " + ats(newVpt) + " " + val);
            if(val > max)
            {
                move = new boolean[]{false,true};
                max = val;
                nextV = newVpt;
            }
        }
        //try taking first and passing second
        int[] newVpp = passSlice(newVp,j);

        val = calcVal(newVpp);
        //System.out.println("  PP: " + ats(newVpp) + " " + val);
        if(val > max)
        {
            move = new boolean[]{false,false};
            max = val;
            nextV = newVpp;
        }
        V = nextV;
        //System.out.println("  NEW: " + ats(V));
        return move;
    }

    public static String ats(int [] a)
    {
        String s = "";
        for(int i = 0; i < a.length; i++)
        {
            s += a[i] + ",";
        }
        return s;
    }

    public static int[] takeSlice (int[] list, int loc)
    {
        int [] newlist = new int[list.length - loc - 1];
        for(int j = loc + 1; j < list.length; j++)
        {
            newlist[j - loc - 1] = list[j];
        }
        return newlist;
    }

    public static int[] passSlice (int[] list, int loc)
    {
        int [] newlist = list;
        if(loc >= 0)
        {
            newlist = new int[list.length-1];
            for(int k = 0; k < loc; k++)
            {
                newlist[k] = list[k];
            }
            for(int k = loc + 1; k < list.length; k++)
            {
                newlist[k-1] = list[k];
            }
        }
        return newlist;
    }

    public static double calcVal(int [] list)
    {
        if(list.length < 8)
        {
            for(int i : list)
            {
                ////System.out.print(i + ",");
            }

                ////System.out.println();
            return computeMean(list);

        }
        return smoothEstimate(list);
    }

    public static double computeMean(int[] V)
    {
        if(V.length == 1)
        {
            return V[0];
        }
        else if(V.length > 1)
        {
            double[] Es = new double[V.length];
            for(int i = 0; i < V.length; i++)
            {
                int[] newVp = new int[V.length - 1];
                for(int j = 0; j < i; j++)
                {
                    newVp[j] = V[j];
                }
                for(int j = i + 1; j < V.length; j++)
                {
                    newVp[j-1] = V[j];
                }
                double pass = computeMean(newVp);
                int[] newVt = new int[V.length - i - 1];
                for(int j = i + 1; j < V.length; j++)
                {
                    newVt[j - i - 1] = V[j];
                }
                double take = V[i] + computeMean(newVt);
                if(take > pass)
                {
                    Es[i] = take;
                }
                else
                {
                    Es[i] = pass;
                }
            }
            double sum = 0;
            for(double d : Es)
            {
                sum += d;
            }
            return sum/V.length;
        }
        else
        {
            return 0;
        }
    }

    public static double smoothEstimate(int [] list)
    {
        double total = 0;
        for(int i : list)
        {
            total+=i;
        }
        double ent = 0;
        for(int i : list)
        {
            if(i > 0)
            {
                ent -= i/total * Math.log(i/total);
            }
        }
        ////System.out.println("      total " + total);
        ////System.out.println("      entro " + Math.exp(ent));
        ////System.out.println("      count " + list.length);
        return total * Math.pow(Math.exp(ent),-0.5) * 4.0/3;// * 1.1287 + 0.05284);
    }

    public static int find(int[] list, int search)
    {
        int first  = 0;
        int last   = list.length - 1;
        int middle = (first + last)/2;

        while( first <= last )
        {
            if ( list[middle] < search )
                first = middle + 1;    
            else if ( list[middle] == search )
                break;
            else
                last = middle - 1;

            middle = (first + last)/2;
        }

        if(first > last)
        {
            return -1;
        }
        return middle;
    }
}

Kontroler

import java.lang.Math;
import java.util.Random;
import java.util.ArrayList;
import java.util.Collections;
public class Controller2
{
    public static void main(String [] args)
    {
        int size = 10000;
        int rounds = 1001;
        ArrayList<Integer> results = new ArrayList<Integer>();
        for(int round = 0; round < rounds; round++)
        {
            int[] envelopes = new int[size];
            for(int i = 0; i<envelopes.length; i++)
            {
                envelopes[i] = i+1;
            }
            shuffleArray(envelopes);
            Player2 p = new Player2(size);
            int cutoff = 0;
            int winnings = 0;
            for(int i = 0; i<envelopes.length; i+=2)
            {
                boolean [] take = p.takeQ(envelopes[i],envelopes[i+1]);
                if(take[0] && envelopes[i] >= cutoff)
                {
                    winnings += envelopes[i];
                    cutoff = envelopes[i];
                }
                if(take[1] && envelopes[i+1] >= cutoff)
                {
                    winnings += envelopes[i+1];
                    cutoff = envelopes[i+1];
                }
            }
            results.add(winnings);
        }
        Collections.sort(results);
        System.out.println(rounds + " rounds, median is " + results.get(results.size()/2));

    }

    //stol... I mean borrowed from http://stackoverflow.com/questions/1519736/random-shuffling-of-an-array
    static void shuffleArray(int[] ar)
    {
        Random rnd = new Random();
        for (int i = ar.length - 1; i > 0; i--)
        {
            int index = rnd.nextInt(i + 1);
            // Simple swap
            int a = ar[index];
            ar[index] = ar[i];
            ar[i] = a;
        }
    }
}

Adres Bitcoin: 1BVBs9ZEP8YY4EpV868nxi2R23YfL7hdMq


3

Python 3 - 615570 USD

W rzeczywistości nie używa wyroczni ... Eh :)

def algo():
    global prevs

    try:
        prevs.append(read())
    except NameError:
        prevs = [read()]

    if len(prevs) > 10000:
        prevs = [prevs[-1]]

    if read() < round(len(prevs),-1):
        take()
    else:
        passe()

Tworzy listę wszystkich poprzednich kopert i sprawdza, czy bieżąca koperta jest mniejsza niż liczba poprzednich kopert w 10 przyrostach kopert.


0

Python, 87 424

Oto prosty i łatwy algorytm, szczęśliwa siódemka.

def LuckyNumber7():
Test = read()
if "7" in str(Test):
    take()
else:
    passe()

test(LuckyNumber7)

Zasadniczo konwertuje read () na ciąg znaków i sprawdza, czy jest w nim siedem. Jeśli tak, bierze kopertę. Jeśli nie, to przechodzi.

Średnio wynosi 81 000, nie śledziłem.


To pokazuje, że poleganie na szczęściu nie jest skuteczną strategią? ;)
Reto Koradi

@RetoKoradi Yep: D
The_Basset_Hound
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.