Take It or Leave It:コンピューター向けゲームショー


28

コンテキスト:

世捨て人の億万長者がゲーム番組を作成し、世界で最も優秀で優秀なプログラマーを引き付けました。月曜日の真夜中のストロークで、彼は応募者のプールから1人をその週の出場者として選び、彼らにゲームを提供します。あなたは今週の幸運な出場者です!

今週のゲーム:

ホストは、10,000個のデジタルエンベロープのスタックへのAPIアクセスを提供します。これらのエンベロープはランダムにソートされ、その中に1ドルから10,000ドルの間のドル値が含まれます(同じドル値を含むエンベロープは2つありません)。

次の3つのコマンドを自由に使用できます。

  1. Read():スタックの一番上にある封筒のドルの数字を読み取ります。

  2. Take():封筒にドル記号をゲームショーウォレットに追加し、封筒をスタックから取り出します。

  3. Pass():スタックの一番上のエンベロープからポップします。

ルール:

  1. 封筒でPass()を使用すると、内部のお金は永久に失われます。

  2. $ Xを含むエンベロープでTake()を使用する場合、それ以降、$ Xを含むエンベロープでTake()を使用することはできません。これらの封筒の1つでTake()を実行すると、ウォレットに0ドルが追加されます。

最大限の金額でゲームを終了するアルゴリズムを作成します。

Pythonでソリューションを作成している場合は、@ Maltysenの好意により、このコントローラーを使用してアルゴリズムをテストしてください。https://gist.github.com/Maltysen/5a4a33691cd603e9aeca

コントローラを使用する場合、グローバルにアクセスできず、提供されている3つのAPIコマンドとローカルスコープ変数のみを使用できます。(@Beta Decay)

注:この場合の「最大」とは、N> 50が実行された後のウォレットの中央値を意味します。Nが無限大になると、特定のアルゴリズムの中央値が収束することが間違っていると証明されたいと思います。代わりに平均値を最大化することをお気軽に行ってください。しかし、平均値は中央値よりも小さなNによって放り出される可能性が高いと感じています。

編集:エンベロープの数を10kに変更して処理を容易にし、Take()をより明確にしました。

編集2:メタに関するこの投稿を考慮して、賞品の条件は削除されました。

現在のハイスコア:

PhiNotPi-$ 805,479

レトコラディ-$ 803,960

デニス-$ 770,272(改訂版)

アレックスL.-714,962ドル(改訂)


Falseを返すように実装しました。あなたはそれを読むことができるので、失敗したtake()でゲーム全体を失敗する本当のポイントはありません
-OganM

4
誰かがそれを使用したい場合、ここに私のアルゴリズムをテストするために使用しているコントローラーがあります:gist.github.com/Maltysen/5a4a33691cd603e9aeca
Maltysen

8
PS素敵な質問とプログラミングパズルとコードゴルフへようこそ:)
trichoplax

3
@Maltysen私はあなたのコントローラーをOPに入れました、貢献してくれてありがとう!
-LivingInformation

1
ビットコインの賞金に関する明確なルールは見つかりませんでしたが、人々が貢献できる現実世界の賞金に関するメタディスカッションがいくつかあります。
-trichoplax

回答:


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
この答えには、私よりも多くの賛成票が必要です!より賢く、スコアが高く、ゴルフもできます!
アレックスL

1
@LivingInformationこれは私の住所です:17uLHRfdD5JZ2QjSqPGQ1B12LoX4CgLGuV
デニス

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()

125ドルから190ドルの間のサイズのステップでますます多くなります。N = 10,000でランし、中央値は$ 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)

ビットコインアドレス:1CBzYPCFFBW1FX9sBTmNYUJyMxMcmL4BZ7

うわー、OPが配信されました!ありがとう@LivingInformation!


1
コントローラーは、私のものではなく、Maltysenのものです。
-orlp

2
確認済み。コントローラーをセットアップしたばかりで、ソリューションに非常に似た数値が得られました。厳密に言えば、max_taken公式のゲームAPIの一部ではないため、独自のコードの価値を維持する必要があると思います。しかし、それは簡単なことです。
レトコラディ

1
ええ、max_takenは@Maltysenのコントローラーにあります。便利な場合は、ソリューション全体(コントローラー+アルゴリズム)を1つのブロックに投稿できます。
アレックスL

それは本当に大したことではありません。しかし、最もクリーンなアプローチは、投稿されたコードで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
これをシミュレートすると、報告されたスコアが80万ドルをわずかに上回ることを確認できます。
-orlp

3

C ++、最大$ 815,000

Reto Koradiのソリューションに基づきますが、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カットオフポイントでプレイしましたが、以前のカットオフは両方とも遅くなり、悪化しました。あまりにも我々はすでに可能93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000.のうち単なる1000個の順列サンプリングしている100枚の封筒で、正直驚くべきことではない
orlp

3

Java、806,899ドル

これは2501ラウンドの試行からです。私はまだ最適化に取り組んでいます。ラッパーとプレーヤーの2つのクラスを作成しました。ラッパーは、エンベロープの数(本物の場合は常に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である場合、2つの可能性があります。

  • 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}のようなものは間違いなく3つの封筒、{231,232,233,234,235}は間違いなく5ですが、{1,2,234,235,236}は5でなく3として実際にカウントされるはずです。後で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。ありがとう!(これは、チャレンジ作成者が賞品を配っていたときからここにありました。)


誤って805,479を超えて20kのサトシを送信しました。参考までに、金額はあなたのスコアとなるはずでした。私の間違いをお楽しみください:)
LivingInformation

あなたはより多くのラウンドで数字を実行しますか?私が見ていることに基づいて、かなりの変動があり、安定した中央値を得るには500では十分ではありません。500ラウンドしか実行しない場合、私のスコアはあなたのスコアに非常に近くなりますが、それはすべて乱数がどのように落ちるかによって異なります。変数シードを使用し、数回500回実行した場合、おそらくより高いスコアを取得できます。
レトコラディ

@RetoKoradi私は間違いなくもっとラウンドをするつもりです。
PhiNotPi
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.