接受还是保留:计算机游戏展


28

内容:

一位隐居的亿万富翁创建了一个游戏节目来吸引世界上最优秀,最聪明的程序员。在星期一的午夜时分,他从一组申请人中选择一个人作为本周的竞赛者,并为他们提供游戏。您是本周的幸运选手!

本周比赛:

主机为您提供对10,000个数字信封堆栈的API访问。这些信封是随机排序的,并在其中包含1美元到10,000美元之间的美元价值(没有两个信封包含相同的美元价值)。

您可以使用3个命令:

  1. Read():读取堆栈顶部信封中的美元图形。

  2. Take():将信封中的美元数字添加到游戏节目钱包中,然后将信封弹出堆栈。

  3. Pass():弹出堆栈顶部的信封。

规则:

  1. 如果在信封上使用Pass(),则其中的钱将永远丢失。

  2. 从那时起,如果在包含$ X的信封上使用Take(),则永远不要在包含<$ X的信封上使用Take()。这些信封之一上的Take()将为您的钱包增加$ 0。

编写算法,以最大的金额完成游戏。

如果您正在使用Python编写解决方案,请随意使用此控制器来测试算法,这由@Maltysen提供:https://gist.github.com/Maltysen/5a4a33691cd603e9aeca

如果使用控制器,则无法访问全局变量,只能使用3个提供的API命令和局部作用域变量。(@Beta衰变)

注意:在这种情况下,“最大值”表示N> 50运行后您钱包中的中间值。我希望,尽管我很乐意被证明是错误的,但是当N增加到无穷大时,给定算法的中值将收敛。可以随意尝试最大化均值,但我有一种感觉,即平均数比中位数更容易被小N抛弃。

编辑:将信封数更改为10k,以便于处理,并使Take()更明确。

编辑2:鉴于有关meta的帖子,奖赏条件已被删除。

当前高分:

PhiNotPi-$ 805,479

Reto Koradi-$ 803,960

丹尼斯-$ 770,272(修订版)

Alex L.-$ 714,962(修订本)


我以仅返回False的方式实现。既然你可以读它有没有在失败的取整场比赛没有真正的点()
OganM

4
如果有人要使用它,这是我一直在测试我的算法的控制器:gist.github.com/Maltysen/5a4a33691cd603e9aeca
Maltysen

8
PS尼斯问题,欢迎阅读“编程难题和代码高尔夫球:)
trichoplax

3
@Maltysen我将您的控制器放入OP中,感谢您的贡献!
LivingInformation 2015年

1
我找不到关于比特币奖励的明确规则,但是人们可以为现实世界的奖励进行一些元讨论
trichoplax

Answers:


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是内容和中号是已采取的量可以被估计为最大R(R-1)/ 2 - M(M + 1)/ 2,其给出了钱所有的包络与内容X在间隔(M,R)包含。

    如果尚未通过任何信封,则估算将是完美的。

  • 计算通过信封不可避免地会损失的金钱量。

    这只是信封里的钱。

  • 检查两者的商是否小于110 + 0.016E,其中E是剩余信封数(不计算不能再取的信封)。

    如果是这样,请采取。否则,通过。


5
因为使用高尔夫语言会以任何方式提供帮助。; P +1为算法。
马蒂森(Maltysen)2015年

2
我无法使用Python克隆复制您的结果:gist.github.com/orlp/f9b949d60c766430fe9c。您得分约为$ 50,000。这差了一个数量级。
orlp

1
@LivingInformation试用和错误。我目前正在使用确切的数量而不是估算值,但是生成的代码非常慢。
丹尼斯

2
这个答案需要比我更多的投票!它更聪明,得分更高,甚至可以打高尔夫球!
Alex 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 2015年

2
已确认。我刚刚设置了一个控制器,并为您的解决方案获得了非常相似的数字。严格来说,我认为您必须维护max_taken自己代码中的价值,因为它不是官方游戏API的一部分。但这是微不足道的。
Reto Koradi

1
是的,max_taken在@Maltysen的控制器中。如果有用,我可以将整个解决方案(控制器+算法)发布在一个块中。
Alex L

真的没什么大不了的。但我认为最干净的方法是只使用read()take()pass()在投稿码的方法,因为这些是基于问题的定义中,“在您的处置3级的命令”。
Reto Koradi 2015年

@Reto我愿意将问题修改为最有意义的命令。Read,Take和Pass都是4个字符,感觉很合适,但是我愿意提出建议(例如,我考虑过将“ pass”更改为“ leave”,因为我将帖子标题为“ take it or let it”) ”)。
LivingInformation 2015年

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 2015年

您是否认为在每个点进行动态计算会更有效,还是您认为此方法正在接近您可以收到的最大值?
LivingInformation 2015年

我没有理由相信这是理想的策略。我希望这是具有这些参数的线性函数的最大值。我一直在尝试允许各种非线性项,但是到目前为止,还没有发现任何明显更好的东西。
Reto Koradi

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

有趣。我已经想到,应该可以通过查看最后几个信封的剩余值来进行改进。我认为您在切换策略的临界点玩过吗?如果您早点切换会变得太慢吗?还是结果实际上变得更糟?
Reto Koradi 2015年

@RetoKoradi我确实玩过临界点,并且早期的临界点都变得太慢和更糟。不算稀奇说实话,在100个信封我们已经提供样品,仅1000个排列出可能93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000.的
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,那么有两种可能性:

  • 拿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。我有个想法,就是使用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。谢谢!(这是挑战作者发放奖品的时间。)


意外地向您发送了超过805479的2万聪。供参考,该金额为您的分数。享受我的错误:)
LivingInformation 2015年

您会在更多回合中运行数字吗?根据我所看到的,会有很大的差异,而500不足以得到稳定的中位数。如果我只跑500发子弹,我的分数将非常接近您的分数,但这取决于随机数如何下降。如果我使用可变种子,并进行了500次运行,那么我可能会获得更高的分数。
Reto Koradi

@RetoKoradi我肯定会做更多的回合。
PhiNotPi 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.