가져 가거나 떠나라 : 컴퓨터를위한 게임 쇼


28

문맥:

독점 억만 장자는 세계 최고의 밝고 프로그래머를 유치하기 위해 게임 쇼를 만들었습니다. 월요일 자정이 시작되면 월요일에는 지원자 풀에서 한 사람을 선택하여 주중 참가자로 선택하고 게임을 제공합니다. 이번 주 행운의 참가자입니다.

이번 주 게임 :

호스트는 10,000 개의 디지털 봉투 스택에 대한 API 액세스를 제공합니다. 이 봉투는 무작위로 정렬되며 그 안에 1 달러에서 10,000 달러 사이의 달러 값을 포함합니다 (두 봉투에 동일한 달러 값이 포함되어 있지 않음).

귀하는 3 가지 명령을 사용할 수 있습니다.

  1. Read () : 스택 상단의 봉투에서 달러 수치를 읽습니다.

  2. Take () : 봉투에있는 달러 그림을 게임 쇼 지갑에 추가하고 봉투를 스택에서 빼냅니다.

  3. 통과 () : 스택 상단의 봉투를 튀어 나옵니다.

규칙:

  1. 봉투에 Pass ()를 사용하면 돈이 영원히 손실됩니다.

  2. $ X가 들어있는 봉투에서 Take ()를 사용하는 경우 그 시점부터는 $ $가 들어있는 봉투에서 Take ()를 사용할 수 없습니다. 이 봉투 중 하나의 Take ()는 지갑에 $ 0를 추가합니다.

최대한의 돈으로 게임을 끝내는 알고리즘을 작성하십시오.

Python으로 솔루션을 작성하는 경우 @Maltysen이 제공하는이 컨트롤러를 사용하여 알고리즘을 자유롭게 테스트 하십시오 . https://gist.github.com/Maltysen/5a4a33691cd603e9aeca

컨트롤러를 사용하면 전역에 액세스 할 수 없으며 제공된 3 가지 API 명령과 로컬 범위 변수 만 사용할 수 있습니다. (@ 베타 부패)

참고 :이 경우 "최대"는 N> 50이 실행 된 후 지갑의 중앙값을 의미합니다. 나는 틀린 것으로 입증되고 싶지만 N이 무한대로 증가함에 따라 주어진 알고리즘의 중간 값이 수렴 할 것으로 기대합니다. 평균을 최대한 최대화하려고 노력하십시오. 그러나 평균이 중간 값보다 작은 N에 의해 ​​평균이 제거 될 가능성이 더 높습니다.

편집 : 처리를 쉽게하기 위해 봉투 수를 10k로 변경하고 Take ()를보다 명확하게 만들었습니다.

편집 2 : 이 게시물 메타 에 비추어 상 조건이 제거되었습니다 .

현재 최고 점수 :

PhiNotPi-$ 805,479

레토 코라디-$ 803,960

데니스-$ 770,272 (개정)

Alex L.-$ 714,962 (개정)


False를 반환하는 방식으로 구현했습니다. 읽을 수 있기 때문에 실패한 take ()에서 전체 게임을 실패 할 실제 지점이 없습니다
OganM

4
누군가 그것을 사용하고 싶다면, 여기 내 알고리즘을 테스트하는 데 사용했던 컨트롤러가 있습니다 : gist.github.com/Maltysen/5a4a33691cd603e9aeca
Maltysen

8
추신 : 좋은 질문과 프로그래밍 퍼즐과 코드 골프에 오신 것을 환영합니다 :)
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
골프 언어를 사용하면 어떤 식 으로든 도움이되기 때문입니다. Algo에 대한; P +1.
Maltysen

2
파이썬 클론 gist.github.com/orlp/f9b949d60c766430fe9c를 사용하여 결과를 복제 할 수 없습니다 . 당신은 약 $ 50,000를 득점합니다. 그 정도의 차이입니다.
orlp

1
@LivingInformation 평가판 및 오류. 나는 현재 추정 대신 정확한 양을 사용하려고하지만 결과 코드는 매우 느립니다.
Dennis 5

2
이 답변은 내 것보다 더 많은 투표가 필요합니다! 더 영리하고 점수가 높으며 골프까지!
Alex L

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 = 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의 일부가 아니기 때문에 자신의 코드에서 가치를 유지해야한다고 생각합니다 . 그러나 그것은 사소한 일입니다.
Reto Koradi

1
예, max_taken은 @Maltysen의 컨트롤러에 있습니다. 유용한 경우 전체 솔루션 (제어기 + 알고리즘)을 한 블록에 게시 할 수 있습니다.
Alex L

정말 큰 문제가 아닙니다. 그러나 가장 깨끗한 접근법은 게시 된 코드에서 read(), take()pass()메소드 만 사용하는 것입니다 . 왜냐하면 그것들은 질문의 정의에 따라 "3 가지 명령"으로 사용되기 때문입니다.
Reto Koradi

@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

상수를 결정하기 위해 최적화 알고리즘을 실행했습니다.
Reto Koradi

각 지점의 동적 계산이 더 효과적이라고 생각합니까, 아니면 이것이받을 수있는 최대 값에 근접한다고 생각하십니까?
LivingInformation

그것이 이상적인 전략이라고 믿을 이유가 없습니다. 이 매개 변수를 사용하는 선형 함수의 최대 값이 되길 바랍니다. 나는 다양한 종류의 비선형 용어를 허용하려고 노력했지만 지금까지는 훨씬 더 나은 것을 찾지 못했습니다.
레토 코라디

1
이 시뮬레이션을 통해보고 된 점수가 $ 800,000보다 약간 높다는 것을 확인할 수 있습니다.
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;
}

흥미 롭군 지난 몇 봉투에 남은 값을 보면 개선이 가능해야한다는 생각이 들었습니다. 전략을 바꾸는 컷오프 지점에서 뛰었다 고 생각합니까? 더 일찍 전환하면 너무 느려 집니까? 아니면 결과가 실제로 악화되고 있습니까?
Reto Koradi

@RetoKoradi 나는 컷오프 지점을 가지고 놀았고 이전 컷오프는 너무 느리고 악화되었습니다. 100 봉투에서 우리는 이미 가능한 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000에서 1000 순열을 샘플링하고 있습니다.
orlp

3

자바, $ 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이면 두 가지 가능성이 있습니다.

  • {7,8,9}와 함께 5를 가지고 게임을하세요
  • 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 번을 통과하지 않으므로 {1,2,234,235,236}는 실제로는 5가 아닌 3으로 계산해야합니다. 나중에 1 또는 2를 선택할 수 있습니다. 나는 유효 봉투 수를 결정하기 위해 Shannon 엔트로피를 사용하는 아이디어를 가졌습니다.

엔벨로프의 값이 일정한 간격으로 균일하게 분포되는 상황, 즉 게임 중에 발생하는 상황을 계산 대상으로 삼았습니다. {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.