Візьміть або залиште: Ігрове шоу для комп’ютерів


28

Контекст:

Повторний мільярдер створив ігрове шоу, щоб залучити найкращих та найяскравіших програмістів у світі. По понеділках під час півночі він обирає одну людину з пулу претендентів, щоб бути учасником тижня, і забезпечує їм гру. Ви щасливий учасник цього тижня!

Гра цього тижня:

Хост надає вам доступ до API до стопки 10 000 цифрових конвертів. Ці конверти розбиваються випадковим чином і містять у собі значення долара від 1 до 10 000 доларів (жоден два конверти не містять однакового значення долара).

У вас є 3 команди:

  1. Читати (): Прочитайте цифру долара в конверті у верхній частині стека.

  2. Take (): Додайте цифру долара в конверт до гаманця з ігровим шоу та висуньте конверт зі стопки.

  3. Pass (): висуньте конверт у верхній частині стека.

Правила:

  1. Якщо ви використовуєте Pass () на конверті, гроші всередині втрачаються назавжди.

  2. Якщо ви використовуєте Take () на конверті, що містить $ X, з цього моменту вперед, ви ніколи не можете використовувати Take () на конверті, що містить <$ X. Візьміть () на одному з цих конвертів, ви додасте 0 доларів у ваш гаманець.

Напишіть алгоритм, який закінчує гру максимальною сумою грошей.

Якщо ви пишете рішення в Python, сміливо використовуйте цей контролер для перевірки алгоритмів, люб’язно надавши @Maltysen: https://gist.github.com/Maltysen/5a4a33691cd603e9aeca

Якщо ви використовуєте контролер, ви не можете отримати доступ до глобальних даних, ви можете використовувати лише 3 надані команди API та локальні змінні. (@Beta Decay)

Примітки: "Максимальне значення" в цьому випадку означає середнє значення у вашому гаманці після N> 50 пробігів. Очікую, хоча хотілося б, щоб я виявився неправильним, що середнє значення для заданого алгоритму буде збігатися в міру збільшення N до нескінченності. Не соромтеся намагатися максимізувати середнє натомість, але у мене таке відчуття, що середнє значення швидше скине на малий N, ніж медіана.

Редагувати: змінив кількість конвертів на 10 к для легшої обробки та зробив Take () більш явним.

Редагувати 2: Умови призу було видалено, зважаючи на цю публікацію на мета.

Поточні високі бали:

PhiNotPi - $ 805 479

Ретто Кораді - 803 960 дол

Денніс - 770 272 дол. США (переглянуто)

Алекс Л. - $ 714 962 (переглянуто)


Я реалізував таким чином, що він просто повертає False. Оскільки ви можете прочитати це, немає реального сенсу провалити всю гру при невдалому
взяті

4
Якщо хтось хоче його використовувати, ось контролер, який я використовував для тестування своїх алгоритмів: gist.github.com/Maltysen/5a4a33691cd603e9aeca
Maltysen

8
PS Приємне запитання та ласкаво просимо до програмування Головоломки та Коду Гольфу :)
trichoplax

3
@Maltysen Я поставив ваш контролер в ОП, дякую за внесок!
LivingInformation

1
Я не зміг знайти явного правила щодо призів на біткойн, але є мета-дискусія щодо призів у реальному світі, до яких можуть сприяти люди.
трихоплакс

Відповіді:


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

Ця програма кілька разів імітує всю гру та обчислює медіану.

Як бігати

Я оцінив свою заявку, виконавши тестування 100,001:

$ 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

Підхід

Для кожного конверта ми робимо наступне:

  • Оцініть суму грошей, яка неминуче буде втрачена, взявши конверт.

    Якщо R - вміст, а M - максимальний розмір, який було прийнято, суму можна оцінити як R (R-1) / 2 - M (M + 1) / 2 , що дає гроші всі конверти із вмістом X у інтервал (M, R) містять.

    Якби ще не було прийнято конвертів, оцінка була б ідеальною.

  • Обчисліть суму грошей, яка неминуче буде втрачена, передаючи конверт.

    Це просто гроші, які містить конверт.

  • Перевірте, чи коефіцієнт обох значень менше 110 + 0,016E , де E - кількість залишилися конвертів (не рахуючи конвертів, які більше не можна брати).

    Якщо так, візьміть. Інакше проходьте.


5
Тому що використання мови для гольфу допомагає будь-яким чином. ; P +1 для альго.
Мальтісен

2
Я не можу повторити ваші результати за допомогою клону Python: gist.github.com/orlp/f9b949d60c766430fe9c . Ви набираєте близько 50 000 доларів. Це вимкнено на порядок.
orlp

1
@LivingInformation Пробні та помилки. Зараз я дивлюсь на використання точної суми замість оцінок, але отриманий код дуже повільний.
Денніс

2
Ця відповідь потребує більшої кількості результатів, ніж моя! Він розумніший, забиває вище і навіть гольфує!
Алекс Л

1
@LivingInformation Це моя адреса: 17uLHRfdD5JZ2QjSqPGQ1B12LoX4CgLGuV
Dennis

7

Пітон, $ 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()

Беруть великі та більші суми у розмірі кроків від 125 до 190 доларів. Побіг з N = 10000 і отримав медіану 714962 доларів. Ці розміри кроків були результатом спроб та помилок і, безумовно, не є оптимальними.

Повний код, включаючи модифіковану версію контролера @ Maltysen, який друкує діаграму під час її роботи:

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)

Адреса BitCoin: 1CBzYPCFFBW1FX9sBTmNYUJyMxMcmL4BZ7

Вау ОП доставлено! Дякуємо @LivingInformation!


1
Контролер - Мальтісен, а не мій.
orlp

2
Підтверджено. Я щойно встановив контролер і отримав дуже схожі номери для свого рішення. Власне кажучи, я думаю, що вам потрібно зберегти значення max_takenвласного коду, оскільки це не є частиною офіційного API гри. Але це банально робити.
Рето Коради

1
Так, max_taken знаходиться в контролері @ Maltysen. Якщо це корисно, я можу розмістити все рішення (контролер + алгоритм) в одному блоці.
Алекс Л

Це насправді не велика справа. Але я думаю , що найчистіший підхід буде використовувати тільки read(), take()і pass()методи Опублікована коду, оскільки ті є «3 команди в вашому розпорядженні» , заснований на визначенні питання.
Рето Коради

@Reto Я готовий переглянути питання щодо тих команд, які мають найбільш сенс. Прочитайте, візьміть та пропустіть усі 4 символи, і мені здається, що я підходив, але я відкритий для пропозицій (наприклад, я вважав за зміну "перейти" на "залишити", тому що я назвав пост "взяти або залишити його" ").
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();
    }
}

Повідомлений результат - медіана від 10 001 гри.


Відгадай і перевіри, я його беру? Або ви використовували якийсь вхідний запальник для констант?
LivingInformation

Я запустив алгоритм оптимізації для визначення констант.
Рето Коради

Ви вважаєте, що динамічний розрахунок у кожній точці був би більш ефективним, чи ви вважаєте, що це наближається до максимального значення, яке ви можете отримати?
LivingInformation

У мене немає підстав вважати, що це ідеальна стратегія. Я сподіваюся, що це максимум для лінійної функції з цими параметрами. Я намагався дозволити різні види нелінійних термінів, але поки що не знайшов нічого кращого.
Рето Коради

1
Я можу підтвердити, що моделювання цього результату дає трохи більше 800 000 доларів США.
orlp

3

C ++, ~ 815 000 доларів

На основі рішення Рето Кораді, але перемикається на більш досконалий алгоритм, коли залишилося 100 (дійсних) конвертів, перетасовуючи випадкові перестановки та обчислюючи найбільш важку послідовність їх збільшення. Він порівнює результати взяття та не беруть конверт і жадібно обере найкращий вибір.

#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;
}

Цікаво. Мені було зрозуміло, що можна покращити, переглянувши значення, залишені для останніх кількох конвертів. Я думаю, ви грали з точкою відсічення, де ви перемикаєте стратегії? Це стає занадто повільно, якщо перейти раніше? Або результати насправді погіршуються?
Рето Коради

@RetoKoradi Я грав із точкою відсічення, і раніше обрізи ставали занадто повільними та гіршими. Чи не занадто дивно , якщо чесно, на 100 конвертах ми вже вибірок Простих 1000 перестановок з можливого 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000.
orlp

3

Java, 806 899 дол

Це з випробування в 2501 раунд. Я все ще працюю над її оптимізацією. Я написав два класи, обгортку та програвач. Обгортка створює програвач кількість конвертів (завжди 10000 для реальної речі), а потім викликає метод takeQзі значенням верхнього конверта. Потім гравець повертається, trueякщо він бере його, falseякщо він передає його.

Гравець

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);
  }
}

Обгортка

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;
    }
  }
}

Більш детальне пояснення незабаром, коли я закінчу оптимізацію.

Основна ідея полягає в тому, щоб можна було оцінити нагороду від гри в гру із заданого набору конвертів. Якщо поточний набір конвертів становить {2,4,5,7,8,9}, а верхній конверт - 5, то є дві можливості:

  • Візьміть 5 і пограйте в гру з {7,8,9}
  • Пройдіть 5 і пограйте в гру {2,4,7,8,9}

Якщо обчислити очікувану винагороду в розмірі {7,8,9} і порівняти її з очікуваною нагородою {2,4,7,8,9}, ми зможемо сказати, чи варто взяти 5

Тепер питання, враховуючи набір конвертів типу {2,4,7,8,9}, яке очікуване значення? Я виявив, що очікуване значення здається пропорційним загальній кількості грошей у наборі, але обернено пропорційним квадратному кореню кількості конвертів, на які діляться гроші. Це стало результатом того, що "ідеально" грали кілька невеликих ігор, у яких усі конверти мають майже однакове значення.

Наступна проблема - як визначити " ефективну кількість конвертів". У всіх випадках кількість конвертів точно відома, відслідковуючи побачене та зроблене. Щось на зразок {234,235,236} - це, безумовно, три конверти, {231,232,233,234,235}, безумовно, 5, але {1,2,234,235,236} насправді слід рахувати як 3, а не 5 конвертів, оскільки 1 і 2 майже нічого не варті, і ти ніколи не підеш на 234 так пізніше ви можете забрати 1 або 2. У мене виникла ідея використовувати ентропію Шеннона для визначення ефективної кількості конвертів.

Я орієнтував свої розрахунки на ситуації, коли значення конвертів рівномірно розподіляються протягом певного інтервалу, що і відбувається під час гри. Якщо я візьму {2,4,7,8,9} і розглядаю це як розподіл ймовірностей, його ентропія становить 1,50242. Тоді я exp()отримую 4.49254 як ефективну кількість конвертів.

Орієнтовна винагорода від {2,4,7,8,9} становить 30 * 4.4925^-0.5 * 4/3 = 18.87

Точне число - 18.1167.

Це не точна оцінка, але я насправді пишаюся тим, наскільки це добре відповідає даним, коли конверти рівномірно розподіляються через інтервал. Я не впевнений у правильному множнику (я зараз використовую 4/3), але ось таблиця даних, що виключає множник.

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

Лінійна регресія між очікуваною та фактичною дає значення R ^ 2 0,999994 .

Наступним моїм кроком для вдосконалення цієї відповіді є поліпшення оцінки, коли кількість конвертів починає зменшуватися, тобто коли конверти не є рівномірно розподіленими і коли проблема починає деталізуватися.


Редагувати: Якщо це вважається гідним біткойнів, я щойно отримав адресу 1PZ65cXxUEEcGwd7E8i7g6qmvLDGqZ5JWg. Спасибі! (Це було з того часу, коли автор виклику роздавав призи.)


Випадково надіслали вам 20 к. Сатоші за 805 479. Для довідки, сума повинна була бути вашою оцінкою. Насолоджуйтесь моєю помилкою :)
LivingInformation

Чи будете ви вести числа з більшою кількістю раундів? Виходячи з того, що я бачу, варіацій існує зовсім небагато, і 500 недостатньо, щоб отримати стабільну медіану. Моя оцінка дуже близька до вашої, якщо я пробігаю лише 500 раундів, але все залежить від того, як трапляються випадкові числа. Якби я використав змінний насіння і зробив 500 пробігів кілька разів, я, ймовірно, міг би отримати більшу оцінку.
Рето Коради

@RetoKoradi Я точно буду робити більше раундів.
PhiNotPi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.