Prendilo o lascialo: uno spettacolo per computer


28

Contesto:

Un miliardario solitario ha creato uno spettacolo di gioco per attirare i programmatori migliori e più brillanti del mondo. Il lunedì allo scoccare della mezzanotte, sceglie una persona da un pool di candidati come concorrente della settimana e fornisce loro un gioco. Sei il fortunato concorrente di questa settimana!

Il gioco di questa settimana:

L'host ti fornisce l'accesso API a una pila di 10.000 buste digitali. Queste buste sono ordinate casualmente e contengono al loro interno un valore in dollari, compreso tra $ 1 e $ 10.000 (non esistono due buste con lo stesso valore in dollari).

Hai 3 comandi a tua disposizione:

  1. Leggi (): leggi la cifra in dollari nella busta nella parte superiore della pila.

  2. Prendi (): aggiungi la cifra in dollari nella busta al portafoglio del tuo game show e togli la busta dallo stack.

  3. Pass (): espelle la busta in cima alla pila.

Le regole:

  1. Se usi Pass () su una busta, il denaro all'interno viene perso per sempre.

  2. Se usi Take () su una busta contenente $ X, da quel momento in poi, non puoi mai usare Take () su una busta contenente <$ X. Take () su una di queste buste aggiungerà $ 0 al tuo portafoglio.

Scrivi un algoritmo che termina il gioco con la massima quantità di denaro.

Se stai scrivendo una soluzione in Python, non esitare a utilizzare questo controller per testare algoritmi, per gentile concessione di @Maltysen: https://gist.github.com/Maltysen/5a4a33691cd603e9aeca

Se si utilizza il controller, non è possibile accedere ai globi, è possibile utilizzare solo i 3 comandi API forniti e le variabili con ambito locale. (@Beta Decay)

Note: "Massimo" in questo caso indica il valore mediano nel tuo portafoglio dopo N> 50 corse. Mi aspetto, anche se mi piacerebbe essere smentito, che il valore mediano di un determinato algoritmo converge quando N aumenta all'infinito. Sentiti libero di provare a massimizzare la media invece, ma ho la sensazione che la media abbia più probabilità di essere scartata da una piccola N rispetto alla mediana.

Modifica: ha modificato il numero di buste in 10k per semplificarne l'elaborazione e reso Take () più esplicito.

Modifica 2: la condizione del premio è stata rimossa, alla luce di questo post su meta.

Punteggi attuali:

PhiNotPi - $ 805.479

Reto Koradi - $ 803.960

Dennis - $ 770.272 (rivisto)

Alex L. - $ 714.962 (rivisto)


Ho implementato in modo da restituire False. Dato che puoi leggerlo, non c'è alcun vero punto di fallire l'intero gioco in un take fallito ()
OganM

4
Nel caso qualcuno voglia usarlo, ecco il controller che ho usato per testare i miei algoritmi: gist.github.com/Maltysen/5a4a33691cd603e9aeca
Maltysen

8
PS Bella domanda e benvenuta in Programming Puzzle and Code Golf :)
trichoplax,

3
@Maltysen Ho inserito il tuo controller nell'OP, grazie per il contributo!
LivingInformation

1
Non sono riuscito a trovare una regola esplicita sui premi bitcoin, ma c'è qualche meta discussione sui premi del mondo reale a cui le persone possono contribuire.
trichoplax,

Risposte:


9

CJam, $ 87,143 $ 700,424 $ 720,327 $ 727,580 $ 770,272

{0:T:M;1e4:E,:)mr{RM>{RR(*MM)*-E0.032*220+R*<{ERM--:E;R:MT+:T;}{E(:E;}?}&}fRT}
[easi*]$easi2/=N

Questo programma simula l'intero gioco più volte e calcola la mediana.

Come correre

Ho segnato il mio invio facendo 100.001 prove:

$ time java -jar cjam-0.6.5.jar take-it-or-leave-it.cjam 100001
770272

real    5m7.721s
user    5m15.334s
sys     0m0.570s

Approccio

Per ogni busta, facciamo quanto segue:

  • Stimare la quantità di denaro che inevitabilmente andrà persa prendendo la busta.

    Se R è il contenuto e M è il massimo che è stato preso, l'importo può essere stimato come R (R-1) / 2 - M (M + 1) / 2 , il che dà al denaro tutte le buste con contenuto X nel intervallo (M, R) contiene.

    Se non fosse stata ancora superata alcuna busta, la stima sarebbe perfetta.

  • Calcola la quantità di denaro che andrà inevitabilmente perduta passando la busta.

    Questo è semplicemente il denaro contenuto nella busta.

  • Controllare se il quoziente di entrambi è inferiore a 110 + 0,016 E , dove E è il numero di buste rimanenti (senza contare le buste che non possono più essere prese).

    Se è così, prendi. Altrimenti, passa.


5
Perché l'uso di un linguaggio da golf aiuta in qualsiasi modo. ; P +1 per l'algo.
Maltysen,

2
Non riesco a replicare i tuoi risultati usando un clone di Python: gist.github.com/orlp/f9b949d60c766430fe9c . Ottieni circa $ 50.000. Questo è un ordine di grandezza fuori.
orlp

1
@LivingInformation Prova ed errore. Attualmente sto cercando di utilizzare l'importo esatto anziché le stime, ma il codice risultante è molto lento.
Dennis

2
Questa risposta richiede più voti della mia! È più intelligente, segna più in alto ed è persino golfato!
Alex L

1
@LivingInformation Questo è il mio indirizzo: 17uLHRfdD5JZ2QjSqPGQ1B12LoX4CgLGuV
Dennis

7

Python, $ 680.646 $ 714.962

f = (float(len(stack)) / 10000)
step = 160
if f<0.5: step = 125
if f>0.9: step = 190
if read() < max_taken + step:
    take()
else:
    passe()

Prende quantità sempre maggiori in passi di dimensioni comprese tra $ 125 e $ 190. Ha funzionato con N = 10.000 e ha ottenuto una mediana di $ 714962. Queste dimensioni del passo provengono da tentativi ed errori e non sono certamente ottimali.

Il codice completo, inclusa una versione modificata del controller di @ Maltysen che stampa un grafico a barre mentre è in esecuzione:

import random
N = 10000


def init_game():
    global stack, wallet, max_taken
    stack = list(range(1, 10001))
    random.shuffle(stack)
    wallet = max_taken = 0

def read():
    return stack[0]

def take():
    global wallet, max_taken
    amount = stack.pop(0)
    if amount > max_taken:
        wallet += amount
        max_taken = amount

def passe():
    stack.pop(0)

def test(algo):
    results = []
    for _ in range(N):
        init_game()
        for i in range(10000):
            algo()
        results += [wallet]
        output(wallet)
    import numpy
    print 'max: '
    output(max(results))
    print 'median: '
    output(numpy.median(results))
    print 'min: '
    output(min(results))

def output(n):
    print n
    result = ''
    for _ in range(int(n/20000)):
        result += '-'
    print result+'|'

def alg():
    f = (float(len(stack)) / 10000)
    step = 160
    if f<0.5: step = 125
    if f>0.9: step = 190
    if read() < max_taken + step:
        #if read()>max_taken: print read(), step, f
        take()
    else:
        passe()

test(alg)

Indirizzo BitCoin: 1CBzYPCFFBW1FX9sBTmNYUJyMxMcmL4BZ7

Wow OP consegnato! Grazie @LivingInformation!


1
Il controller è di Maltysen, non mio.
orlp

2
Confermato. Avevo appena impostato un controller e ho ottenuto numeri molto simili per la tua soluzione. A rigor di termini, penso che devi mantenere il valore di max_takennel tuo codice, poiché non fa parte dell'API di gioco ufficiale. Ma è banale da fare.
Reto Koradi,

1
Sì, max_taken è nel controller di @ Maltysen. Se è utile posso pubblicare l'intera soluzione (controller + algoritmo) in un blocco.
Alex L

Non è davvero un grosso problema. Ma penso che l'approccio più pulito sarebbe utilizzare solo la read(), take()e pass()metodi nel codice inviato, in quanto tali sono le "3 comandi a vostra disposizione" in base alla definizione della questione.
Reto Koradi,

@Reto Sono disposto a rivedere la domanda per qualsiasi comando abbia più senso. Leggi, Take e Pass erano tutti e 4 i caratteri e mi sono sentito appropriato, ma sono aperto ai suggerimenti (per esempio, ho considerato di cambiare "passa" in "lasciare", perché ho intitolato il post "prendilo o lascialo ").
LivingInformation

5

C ++, $ 803.960

for (int iVal = 0; iVal < 10000; ++iVal)
{
    int val = game.read();
    if (val > maxVal &&
        val < 466.7f + 0.9352f * maxVal + 0.0275f * iVal)
    {
        maxVal = val;
        game.take();
    }
    else
    {
        game.pass();
    }
}

Il risultato riportato è la mediana di 10.000 partite.


Indovina e controlla, lo prendo? O hai usato una sorta di fuzzer di input per le costanti?
LivingInformation

Ho eseguito un algoritmo di ottimizzazione per determinare le costanti.
Reto Koradi,

Pensi che un calcolo dinamico in ogni punto sarebbe più efficace o pensi che questo si avvicini al valore massimo che puoi ricevere?
LivingInformation

Non ho motivo di credere che sia la strategia ideale. Spero che sia il massimo per una funzione lineare con questi parametri. Ho cercato di consentire vari tipi di termini non lineari, ma finora non ho trovato nulla di significativamente migliore.
Reto Koradi,

1
Posso confermare che la simulazione fornisce un punteggio leggermente superiore a $ 800.000.
orlp

3

C ++, ~ $ 815.000

Basato sulla soluzione di Reto Koradi, ma passa a un algoritmo più sofisticato una volta che sono rimaste 100 buste (valide), mescolando permutazioni casuali e calcolando la sottosequenza crescente più pesante di esse. Confronterà i risultati di prendere e non prendere la busta e selezionerà avidamente la scelta migliore.

#include <algorithm>
#include <iostream>
#include <vector>
#include <set>


void setmax(std::vector<int>& h, int i, int v) {
    while (i < h.size()) { h[i] = std::max(v, h[i]); i |= i + 1; }
}

int getmax(std::vector<int>& h, int n) {
    int m = 0;
    while (n > 0) { m = std::max(m, h[n-1]); n &= n - 1; }
    return m;
}

int his(const std::vector<int>& l, const std::vector<int>& rank) {
    std::vector<int> h(l.size());
    for (int i = 0; i < l.size(); ++i) {
        int r = rank[i];
        setmax(h, r, l[i] + getmax(h, r));
    }

    return getmax(h, l.size());
}

template<class RNG>
void shuffle(std::vector<int>& l, std::vector<int>& rank, RNG& rng) {
    for (int i = l.size() - 1; i > 0; --i) {
        int j = std::uniform_int_distribution<int>(0, i)(rng);
        std::swap(l[i], l[j]);
        std::swap(rank[i], rank[j]);
    }
}

std::random_device rnd;
std::mt19937_64 rng(rnd());

struct Algo {
    Algo(int N) {
        for (int i = 1; i < N + 1; ++i) left.insert(i);
        ival = maxval = 0;
    }

    static double get_p(int n) { return 1.2 / std::sqrt(8 + n) + 0.71; }

    bool should_take(int val) {
        ival++;
        auto it = left.find(val);
        if (it == left.end()) return false;

        if (left.size() > 100) {
            if (val > maxval && val < 466.7f + 0.9352f * maxval + 0.0275f * (ival - 1)) {
                maxval = val;
                left.erase(left.begin(), std::next(it));
                return true;
            }

            left.erase(it);
            return false;
        }

        take.assign(std::next(it), left.end());
        no_take.assign(left.begin(), it);
        no_take.insert(no_take.end(), std::next(it), left.end());
        take_rank.resize(take.size());
        no_take_rank.resize(no_take.size());
        for (int i = 0; i < take.size(); ++i) take_rank[i] = i;
        for (int i = 0; i < no_take.size(); ++i) no_take_rank[i] = i;

        double take_score, no_take_score;
        take_score = no_take_score = 0;
        for (int i = 0; i < 1000; ++i) {
            shuffle(take, take_rank, rng);
            shuffle(no_take, no_take_rank, rng);
            take_score += val + his(take, take_rank) * get_p(take.size());
            no_take_score += his(no_take, no_take_rank) * get_p(no_take.size());
        }

        if (take_score > no_take_score) {
            left.erase(left.begin(), std::next(it));
            return true;
        }

        left.erase(it);
        return false;
    }

    std::set<int> left;
    int ival, maxval;
    std::vector<int> take, no_take, take_rank, no_take_rank;
};


struct Game {
    Game(int N) : score_(0), max_taken(0) {
        for (int i = 1; i < N + 1; ++i) envelopes.push_back(i);
        std::shuffle(envelopes.begin(), envelopes.end(), rng);
    }

    int read() { return envelopes.back(); }
    bool done() { return envelopes.empty(); }
    int score() { return score_; }
    void pass() { envelopes.pop_back(); }

    void take() {
        if (read() > max_taken) {
            score_ += read();
            max_taken = read();
        }
        envelopes.pop_back();
    }

    int score_;
    int max_taken;
    std::vector<int> envelopes;
};


int main(int argc, char** argv) {
    std::vector<int> results;
    std::vector<int> max_results;
    int N = 10000;
    for (int i = 0; i < 1000; ++i) {
        std::cout << "Simulating game " << (i+1) << ".\n";
        Game game(N);
        Algo algo(N);

        while (!game.done()) {
            if (algo.should_take(game.read())) game.take();
            else game.pass();
        }
        results.push_back(game.score());
    }

    std::sort(results.begin(), results.end());
    std::cout << results[results.size()/2] << "\n";

    return 0;
}

Interessante. Mi era passato per la mente che sarebbe stato possibile migliorare osservando i valori lasciati per le ultime buste. Immagino che tu abbia giocato con il punto limite in cui cambi strategia? Sta diventando troppo lento se passi prima? O i risultati stanno effettivamente peggiorando?
Reto Koradi,

@RetoKoradi Ho giocato con il punto di cutoff, e i cutoff precedenti sono diventati entrambi troppo lenti e peggiori. Non è troppo sorprendente, onestamente, a 100 buste stiamo già campionamento di soli 1000 permutazioni su un massimo di 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000.
orlp

3

Java, $ 806.899

Questo proviene da una prova di 2501 colpi. Sto ancora lavorando per ottimizzarlo. Ho scritto due classi, un wrapper e un giocatore. Il wrapper crea un'istanza per il giocatore con il numero di buste (sempre 10000 per la cosa reale), quindi chiama il metodo takeQcon il valore della busta superiore. Il giocatore quindi ritorna truese lo prende, falsese lo supera.

Giocatore

import java.lang.Math;

public class Player {
  public int[] V;

  public Player(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) {

    // System.out.println("look " + x);

    // http://www.programmingsimplified.com/java/source-code/java-program-for-binary-search
    int first = 0;
    int last = V.length - 1;
    int middle = (first + last) / 2;
    int search = x;

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

      middle = (first + last) / 2;
    }

    int i = middle;

    if (first > last) {
      // System.out.println(" PASS");
      return false; // value not found, so the envelope must not be in the list
                    // of acceptable ones
    }

    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 = calcVal(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] + calcVal(newVt);
    // System.out.println(" take " + take);
    // System.out.println(" pass " + pass);

    if (take > pass) {
      V = newVt;
      // System.out.println(" TAKE");
      return true;
    } else {
      V = newVp;
      // System.out.println(" PASS");
      return false;
    }
  }

  public double calcVal(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);
  }
}

involucro

import java.lang.Math;
import java.util.Random;
import java.util.ArrayList;
import java.util.Collections;

public class Controller {
  public static void main(String[] args) {
    int size = 10000;
    int rounds = 2501;
    ArrayList<Integer> results = new ArrayList<Integer>();
    int[] envelopes = new int[size];
    for (int i = 0; i < envelopes.length; i++) {
      envelopes[i] = i + 1;
    }
    for (int round = 0; round < rounds; round++) {
      shuffleArray(envelopes);

      Player p = new Player(size);
      int cutoff = 0;
      int winnings = 0;
      for (int i = 0; i < envelopes.length; i++) {
        boolean take = p.takeQ(envelopes[i]);
        if (take && envelopes[i] >= cutoff) {
          winnings += envelopes[i];
          cutoff = envelopes[i];
        }
      }
      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 Random rnd = new Random();

  static void shuffleArray(int[] ar) {
    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;
    }
  }
}

Una spiegazione più dettagliata arriverà presto, dopo che avrò finito le ottimizzazioni.

L'idea principale è quella di essere in grado di stimare la ricompensa giocando a un determinato set di buste. Se l'attuale set di buste è {2,4,5,7,8,9} e la busta superiore è 5, allora ci sono due possibilità:

  • Prendi il 5 e gioca con {7,8,9}
  • Passa il 5 e gioca una partita di {2,4,7,8,9}

Se calcoliamo la ricompensa attesa di {7,8,9} e la confrontiamo con la ricompensa attesa di {2,4,7,8,9}, saremo in grado di dire se vale la pena prendere il 5.

Ora la domanda è, dato un set di buste come {2,4,7,8,9} qual è il valore atteso? Ho scoperto che il valore atteso sembra essere proporzionale alla quantità totale di denaro nell'insieme, ma inversamente proporzionale alla radice quadrata del numero di buste in cui è diviso il denaro. Questo deriva dal "perfetto" gioco di diversi piccoli giochi in cui tutte le buste hanno un valore quasi identico.

Il prossimo problema è come determinare il " numero effettivo di buste". In tutti i casi, il numero di buste è noto esattamente tenendo traccia di ciò che hai visto e fatto. Qualcosa come {234.235.236} è sicuramente tre buste, {231.232.233.234.235} è sicuramente 5, ma {1.2.234.235.236} dovrebbe davvero contare come 3 e non 5 buste perché l'1 e il 2 sono quasi senza valore e non passeresti mai su un 234 quindi in seguito potresti prendere un 1 o 2. Ho avuto l'idea di usare l'entropia di Shannon per determinare il numero effettivo di buste.

Ho indirizzato i miei calcoli a situazioni in cui i valori della busta sono distribuiti uniformemente su un certo intervallo, che è ciò che accade durante il gioco. Se prendo {2,4,7,8,9} e lo considero una distribuzione di probabilità, la sua entropia è 1,50242. Quindi faccio exp()4.49254 come numero effettivo di buste.

La ricompensa stimata da {2,4,7,8,9} è 30 * 4.4925^-0.5 * 4/3 = 18.87

Il numero esatto è 18.1167.

Questa non è una stima esatta, ma in realtà sono davvero orgoglioso di quanto bene si adatta ai dati quando le buste sono distribuite uniformemente su un intervallo. Non sono sicuro del moltiplicatore corretto (sto usando 4/3 per ora) ma ecco una tabella di dati che esclude il moltiplicatore.

Set of Envelopes                    Total * (e^entropy)^-0.5      Actual Score

{1,2,3,4,5,6,7,8,9,10}              18.759                        25.473
{2,3,4,5,6,7,8,9,10,11}             21.657                        29.279
{3,4,5,6,7,8,9,10,11,12}            24.648                        33.125
{4,5,6,7,8,9,10,11,12,13}           27.687                        37.002
{5,6,7,8,9,10,11,12,13,14}          30.757                        40.945
{6,7,8,9,10,11,12,13,14,15}         33.846                        44.900
{7,8,9,10,11,12,13,14,15,16}        36.949                        48.871
{8,9,10,11,12,13,14,15,16,17}       40.062                        52.857
{9,10,11,12,13,14,15,16,17,18}      43.183                        56.848
{10,11,12,13,14,15,16,17,18,19}     46.311                        60.857

La regressione lineare tra atteso e reale fornisce un valore R ^ 2 di 0.999994 .

Il mio prossimo passo per migliorare questa risposta è migliorare la stima quando il numero di buste inizia a ridursi, ovvero quando le buste non sono distribuite approssimativamente in modo uniforme e quando il problema inizia a diventare granulare.


Modifica: se questo è considerato degno di bitcoin, ho appena ricevuto un indirizzo su 1PZ65cXxUEEcGwd7E8i7g6qmvLDGqZ5JWg. Grazie! (Questo è stato da quando l'autore della sfida distribuiva i premi.)


Ti ho inviato accidentalmente 20k satoshi oltre 805.479. Per riferimento, l'importo doveva essere il tuo punteggio. Goditi il ​​mio errore :)
LivingInformation

Farai numeri con più round? Sulla base di quello che sto vedendo, ci sono molte variazioni e 500 non sono sufficienti per ottenere una mediana stabile. Il mio punteggio è molto vicino al tuo se corro solo 500 round, ma tutto dipende da come i numeri casuali cadono. Se avessi usato un seme variabile e avessi fatto 500 corse alcune volte, avrei potuto ottenere un punteggio più alto.
Reto Koradi,

@RetoKoradi Farò sicuramente più giri.
PhiNotPi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.