如何处理数字猜谜游戏(带扭曲)算法?

时间:2011-10-08 05:20:53

标签: java python algorithm machine-learning data-mining

我正在学习编程(Python和算法),并且正在尝试处理我感兴趣的项目。我已经创建了一些基本的Python脚本,但我不确定如何处理我正在尝试构建的游戏的解决方案。

以下是游戏的运作方式:

将为用户提供具有值的项目。例如,

Apple = 1
Pears = 2
Oranges  = 3

然后他们将有机会选择他们喜欢的任何组合(即100个苹果,20个梨和一个橙色)。计算机获得的唯一输出是总值(在此示例中,它当前为143美元)。电脑会试着猜猜他们有什么。显然,第一次转弯时无法正确使用。

         Value    quantity(day1)    value(day1)
Apple      1        100                100
Pears      2         20                 40
Orange     3          1                  3
Total               121                143

下一轮用户可以修改他们的数量但不超过总数量的5%(或者我们可能选择的其他百分比。例如,我将使用5%)。水果价格可以随意变化,因此总价值也可能因此而变化(为简单起见,本例中我没有改变水果价格)。使用上面的例子,在游戏的第2天,用户在第3天返回$ 152和$ 164的值。这是一个例子:

Quantity (day2)   %change (day2)    Value (day2)   Quantity (day3)   %change (day3)   Value(day3)
 104                                 104            106                                106
  21                                  42             23                                 46
   2                                   6              4                                 12
 127               4.96%             152            133               4.72%            164

*(我希望表格显示正确,我不得不手动分隔它们,所以希望它不仅仅是在我的屏幕上进行,如果它不起作用,请告诉我,我会尝试上传截图。)

我试图看看我是否可以弄清楚数量是多少(假设用户有耐心继续输入数字)。我现在知道我的唯一限制是总价值不能超过5%所以我现在不能在5%的准确度内,所以用户将永远进入它。

到目前为止我做了什么

到目前为止,这是我的解决方案(不多)。基本上,我采取所有的价值观并找出他们所有可能的组合(我完成了这一部分)。然后我拿出所有可能的组合并将它们作为字典放入数据库中(例如,对于143美元,可能有一个字典条目{apple:143,Pears:0,Oranges:0} ..一直到{apple :0,Pears:1,Oranges:47}。每次我得到一个新号码时都会这样做,所以我列出了所有可能性。

这是我被困的地方。在使用上述规则时,我如何找出最佳解决方案?我想我需要一个健身功能,可以自动比较两天的数据,并消除前几天数据差异超过5%的任何可能性。

问题:

所以我的问题是用户改变了总数而且我有一个所有概率的列表,我该如何处理?我需要学习什么?是否有任何算法或我可以使用的理论适用?或者,为了帮助我理解我的错误,你能否建议我可以添加哪些规则以使这个目标可行(如果它不在当前状态。我正在考虑添加更多水果并说他们必须选择至少3个等等。) ?另外,我对遗传算法只有模糊的理解,但我想我可以在这里使用它们,如果有什么我可以使用的吗?

我非常渴望学习,所以任何建议或提示都会受到高度赞赏(请不要告诉我这个游戏是不可能的)。

更新:获得这很难解决的反馈。所以我认为我会在游戏中添加另一个条件,它不会干扰玩家正在做的事情(游戏对他们来说保持不变),但每天水果的价值会随机变化(随机)。这会让它更容易解决吗?因为在5%的运动和某些果实值变化中,随着时间的推移,可能只有少数组合。

第1天,任何事都有可能并且获得足够接近的范围几乎是不可能的,但随着水果价格的变化而用户只能选择5%的变化,那么不应该(随着时间的推移)范围变窄狭窄。在上面的例子中,如果价格足够波动,我想我可以强行推出一个解决方案给我一个范围来猜测,但我想弄清楚是否有一个更优雅的解决方案或其他解决方案来保持缩小这个范围时间。

UPDATE2:在阅读并询问后,我认为这是一个隐藏的马尔可夫/维特比问题,它跟踪水果价格和总和的变化(加权最后一个数据点最重)。我不知道如何应用这种关系。我认为情况确实如此,但至少我开始怀疑这是某种类型的机器学习问题。

更新3:我创建了一个测试用例(数字较小)和一个生成器来帮助自动生成用户生成的数据,我正在尝试从中创建一个图表以查看更有可能的数据。

以下是代码,以及用户实际结果数量的总值和注释。

#!/usr/bin/env python
import itertools

# Fruit price data
fruitPriceDay1 = {'Apple':1, 'Pears':2, 'Oranges':3}
fruitPriceDay2 = {'Apple':2, 'Pears':3, 'Oranges':4}
fruitPriceDay3 = {'Apple':2, 'Pears':4, 'Oranges':5}

# Generate possibilities for testing (warning...will not scale with large numbers)
def possibilityGenerator(target_sum, apple, pears, oranges):
    allDayPossible = {}
    counter = 1
    apple_range = range(0, target_sum + 1, apple)
    pears_range = range(0, target_sum + 1, pears)
    oranges_range = range(0, target_sum + 1, oranges)
    for i, j, k in itertools.product(apple_range, pears_range, oranges_range):
        if i + j + k == target_sum:
            currentPossible = {}

            #print counter
            #print 'Apple', ':', i/apple, ',', 'Pears', ':', j/pears, ',', 'Oranges', ':', k/oranges
            currentPossible['apple'] = i/apple
            currentPossible['pears'] = j/pears
            currentPossible['oranges'] = k/oranges

            #print currentPossible
            allDayPossible[counter] = currentPossible
            counter = counter +1
    return allDayPossible

# Total sum being returned by user for value of fruits
totalSumDay1=26 # Computer does not know this but users quantities are apple: 20, pears 3, oranges 0 at the current prices of the day
totalSumDay2=51 # Computer does not know this but users quantities are apple: 21, pears 3, oranges 0 at the current prices of the day
totalSumDay3=61 # Computer does not know this but users quantities are apple: 20, pears 4, oranges 1 at the current prices of the day
graph = {}
graph['day1'] = possibilityGenerator(totalSumDay1, fruitPriceDay1['Apple'], fruitPriceDay1['Pears'], fruitPriceDay1['Oranges'] )
graph['day2'] = possibilityGenerator(totalSumDay2, fruitPriceDay2['Apple'], fruitPriceDay2['Pears'], fruitPriceDay2['Oranges'] )
graph['day3'] = possibilityGenerator(totalSumDay3, fruitPriceDay3['Apple'], fruitPriceDay3['Pears'], fruitPriceDay3['Oranges'] )

# Sample of dict = 1 : {'oranges': 0, 'apple': 0, 'pears': 0}..70 : {'oranges': 8, 'apple': 26, 'pears': 13}
print graph

7 个答案:

答案 0 :(得分:12)

我们将图论与概率结合起来:

在第一天,建立一套所有可行的解决方案。让我们将解决方案表示为A1 = {a1(1),a1(2),...,a1(n)}。

在第二天,您可以再次构建解决方案集A2。

现在,对于A2中的每个元素,您需要检查是否可以从A1的每个元素到达(给定x%容差)。如果是这样 - 将A2(n)连接到A1(m)。如果无法从A1(m)中的任何节点到达 - 您可以删除此节点。

基本上我们正在构建一个连通的有向无环图。

图表中的所有路径都具有相同的可能性。只有当存在从Am到Am + 1的单个边(从Am中的节点到Am + 1中的节点)时,才能找到精确的解。

当然,某些节点出现在比其他节点更多的路径中。可以根据包含该节点的路径数直接推导出每个节点的概率。

通过为每个节点分配权重(等于通向此节点的路径数),无需保留所有历史记录,只需保留前一天。

另外,看看non-negative-values linear diphantine equations - 我刚才问过的一个问题。接受的答案是在每个步骤中使用所有组合的好方法。

答案 1 :(得分:7)

这个问题无法解决。

假设你确切知道物品的比例增加了多少,而不仅仅是最大比例是多少。

用户有N个水果,你有D天的猜测。

在每一天你得到N个新变量,然后你得到总共D * N个变量。

每天只能生成两个方程式。一个等式是n_item *价格的总和,而另一个是基于已知比率。如果它们都是独立的,那么总共有2 * D个方程式。

2 * D<所有N的N * D> 2

答案 2 :(得分:6)

答案 3 :(得分:3)

我写了一个玩游戏的程序。当然,我必须使人类自动化,但我相信我这样做是为了在与真正的人类对抗时不应该使我的方法失效。

我从机器学习的角度来看待这个问题并将问题视为隐藏的马尔可夫模型,其中总价格是观察值。我的解决方案是使用粒子滤波器。该解决方案使用NumPy和SciPy在Python 2.7中编写。

我说明了我在评论中明确或在代码中隐含的任何假设。为了让代码以自动方式运行,我还设置了一些额外的约束。它没有特别优化,因为我试图在侧面可理解性而不是速度上犯错误。

每次迭代输出当前的真实数量和猜测。我只是将输出传输到一个文件,所以我可以轻松地查看它。一个有趣的扩展是绘制图表上的输出2D(2个水果)或3D(3个水果)。然后,您将能够在解决方案中看到粒子滤波器。

更新

编辑代码以在调整后包含更新的参数。包括使用matplotlib(通过pylab)绘制调用。绘图适用于Linux-Gnome,您的里程可能会有所不同。默认NUM_FRUITS为2,用于绘制支持。只需注释掉所有pylab调用以删除绘图,并能够将NUM_FRUITS更改为任何内容。

很好地估计由UnknownQuantities X Price = TotalPrice表示的当前fxn。在2D(2个水果)中,这是一条线,在3D(3个水果)中它是一个平面。对于粒子滤波器而言,似乎数据太少,无法可靠地确定正确的数量。在粒子滤波器之上需要更多智能才能真正汇集历史信息。您可以尝试将粒子滤波器转换为二阶或三阶。

更新2:

我一直在玩我的代码,很多。我尝试了很多东西,现在提出了我将要制作的最终节目(开始讨论这个想法)。

的变化:

粒子现在使用浮点而不是整数。不确定这是否有任何有意义的影响,但它是一个更通用的解决方案。只有在猜测时才能舍入到整数。

绘图显示真实数量为绿色方块,当前猜测为红色方块。目前认为颗粒显示为蓝点(根据我们相信它们的大小来确定)。这使得很容易看出算法的运行情况。 (绘图也测试并在Win 7 64位上工作)。

添加了关闭/开启数量变更和价格变动的参数。当然,两个'关'都不是很有趣。

它做得非常好,但是,正如已经指出的那样,这是一个非常棘手的问题,所以得到确切答案很难。关闭CHANGE_QUANTITIES会产生最简单的情况。您可以通过关闭CHANGE_QUANTITIES运行2个水果来了解问题的难度。看看它在正确答案上的速度有多快,然后看看当你增加水果的数量时会有多难。

您还可以通过保持CHANGE_QUANTITIES,但将MAX_QUANTITY_CHANGE从非常小的值(.001)调整为“大”值(.05)来获得有关难度的视角。

如果维度(一个水果数量)接近于零,那么它遇到的一种情况就是挣扎。因为它使用平均粒子来猜测它总是偏离硬边界,如零。

一般来说,这是一个很棒的粒子滤镜教程。


from __future__ import division
import random
import numpy
import scipy.stats
import pylab

# Assume Guesser knows prices and total
# Guesser must determine the quantities

# All of pylab is just for graphing, comment out if undesired
#   Graphing only graphs first 2 FRUITS (first 2 dimensions)

NUM_FRUITS = 3
MAX_QUANTITY_CHANGE = .01 # Maximum percentage change that total quantity of fruit can change per iteration
MAX_QUANTITY = 100 # Bound for the sake of instantiating variables
MIN_QUANTITY_TOTAL = 10 # Prevent degenerate conditions where quantities all hit 0
MAX_FRUIT_PRICE = 1000 # Bound for the sake of instantiating variables
NUM_PARTICLES = 5000
NEW_PARTICLES = 500 # Num new particles to introduce each iteration after guessing
NUM_ITERATIONS = 20 # Max iterations to run
CHANGE_QUANTITIES = True
CHANGE_PRICES = True

'''
  Change individual fruit quantities for a random amount of time
  Never exceed changing fruit quantity by more than MAX_QUANTITY_CHANGE
'''
def updateQuantities(quantities):
  old_total = max(sum(quantities), MIN_QUANTITY_TOTAL)
  new_total = old_total
  max_change = int(old_total * MAX_QUANTITY_CHANGE)

  while random.random() > .005: # Stop Randomly    
    change_index = random.randint(0, len(quantities)-1)
    change_val = random.randint(-1*max_change,max_change)

    if quantities[change_index] + change_val >= 0: # Prevent negative quantities
      quantities[change_index] += change_val
      new_total += change_val

      if abs((new_total / old_total) - 1) > MAX_QUANTITY_CHANGE:
        quantities[change_index] -= change_val # Reverse the change

def totalPrice(prices, quantities):
  return sum(prices*quantities)

def sampleParticleSet(particles, fruit_prices, current_total, num_to_sample):
  # Assign weight to each particle using observation (observation is current_total)
  # Weight is the probability of that particle (guess) given the current observation
  # Determined by looking up the distance from the hyperplane (line, plane, hyperplane) in a
  #   probability density fxn for a normal distribution centered at 0 
  variance = 2
  distances_to_current_hyperplane = [abs(numpy.dot(particle, fruit_prices)-current_total)/numpy.linalg.norm(fruit_prices) for particle in particles]
  weights = numpy.array([scipy.stats.norm.pdf(distances_to_current_hyperplane[p], 0, variance) for p in range(0,NUM_PARTICLES)])

  weight_sum = sum(weights) # No need to normalize, as relative weights are fine, so just sample un-normalized

  # Create new particle set weighted by weights
  belief_particles = []
  belief_weights = []
  for p in range(0, num_to_sample):
    sample = random.uniform(0, weight_sum)
    # sum across weights until we exceed our sample, the weight we just summed is the index of the particle we'll use
    p_sum = 0
    p_i = -1
    while p_sum < sample:
      p_i += 1
      p_sum += weights[p_i]
    belief_particles.append(particles[p_i])
    belief_weights.append(weights[p_i])

  return belief_particles, numpy.array(belief_weights)

'''
  Generates new particles around the equation of the current prices and total (better particle generation than uniformly random)
'''
def generateNewParticles(current_total, fruit_prices, num_to_generate):
  new_particles = []
  max_values = [int(current_total/fruit_prices[n]) for n in range(0,NUM_FRUITS)]
  for p in range(0, num_to_generate):
    new_particle = numpy.array([random.uniform(1,max_values[n]) for n in range(0,NUM_FRUITS)])
    new_particle[-1] = (current_total - sum([new_particle[i]*fruit_prices[i] for i in range(0, NUM_FRUITS-1)])) / fruit_prices[-1]
    new_particles.append(new_particle)
  return new_particles


# Initialize our data structures:
# Represents users first round of quantity selection
fruit_prices = numpy.array([random.randint(1,MAX_FRUIT_PRICE) for n in range(0,NUM_FRUITS)])
fruit_quantities = numpy.array([random.randint(1,MAX_QUANTITY) for n in range(0,NUM_FRUITS)])
current_total = totalPrice(fruit_prices, fruit_quantities)
success = False

particles = generateNewParticles(current_total, fruit_prices, NUM_PARTICLES) #[numpy.array([random.randint(1,MAX_QUANTITY) for n in range(0,NUM_FRUITS)]) for p in range(0,NUM_PARTICLES)]
guess = numpy.average(particles, axis=0)
guess = numpy.array([int(round(guess[n])) for n in range(0,NUM_FRUITS)])

print "Truth:", str(fruit_quantities)
print "Guess:", str(guess)

pylab.ion()
pylab.draw()
pylab.scatter([p[0] for p in particles], [p[1] for p in particles])
pylab.scatter([fruit_quantities[0]], [fruit_quantities[1]], s=150, c='g', marker='s')
pylab.scatter([guess[0]], [guess[1]], s=150, c='r', marker='s')
pylab.xlim(0, MAX_QUANTITY)
pylab.ylim(0, MAX_QUANTITY)
pylab.draw()

if not (guess == fruit_quantities).all():
  for i in range(0,NUM_ITERATIONS):
    print "------------------------", i

    if CHANGE_PRICES:
      fruit_prices = numpy.array([random.randint(1,MAX_FRUIT_PRICE) for n in range(0,NUM_FRUITS)])

    if CHANGE_QUANTITIES:
      updateQuantities(fruit_quantities)
      map(updateQuantities, particles) # Particle Filter Prediction

    print "Truth:", str(fruit_quantities)
    current_total = totalPrice(fruit_prices, fruit_quantities)

    # Guesser's Turn - Particle Filter:
    # Prediction done above if CHANGE_QUANTITIES is True

    # Update
    belief_particles, belief_weights = sampleParticleSet(particles, fruit_prices, current_total, NUM_PARTICLES-NEW_PARTICLES)
    new_particles = generateNewParticles(current_total, fruit_prices, NEW_PARTICLES)

    # Make a guess:
    guess = numpy.average(belief_particles, axis=0, weights=belief_weights) # Could optimize here by removing outliers or try using median
    guess = numpy.array([int(round(guess[n])) for n in range(0,NUM_FRUITS)]) # convert to integers
    print "Guess:", str(guess)

    pylab.cla()
    #pylab.scatter([p[0] for p in new_particles], [p[1] for p in new_particles], c='y') # Plot new particles
    pylab.scatter([p[0] for p in belief_particles], [p[1] for p in belief_particles], s=belief_weights*50) # Plot current particles
    pylab.scatter([fruit_quantities[0]], [fruit_quantities[1]], s=150, c='g', marker='s') # Plot truth
    pylab.scatter([guess[0]], [guess[1]], s=150, c='r', marker='s') # Plot current guess
    pylab.xlim(0, MAX_QUANTITY)
    pylab.ylim(0, MAX_QUANTITY)
    pylab.draw()

    if (guess == fruit_quantities).all():
      success = True
      break

    # Attach new particles to existing particles for next run:
    belief_particles.extend(new_particles)
    particles = belief_particles
else:
  success = True

if success:
  print "Correct Quantities guessed"
else:
  print "Unable to get correct answer within", NUM_ITERATIONS, "iterations"

pylab.ioff()
pylab.show()

答案 4 :(得分:1)

根据您的初始规则:

从我的学年开始,我会说如果我们对5%的变化进行抽象,我们每天都会得到一个具有三个未知值的等式(抱歉,我不知道英语中的数学词汇),它们是相同的前一天的价值。 在第3天,您有三个方程式,三个未知值,解决方案应该是直接的。

我想如果三个元素的值足够不同,每天5%的变化可能会被遗忘,因为正如你所说,我们将使用近似值并对数字进行舍入。

适用于您的改编规则:

在这种情况下,有太多未知数 - 并且正在改变 - 因此我没有直接的解决方案。我会相信Lior;他的方法看起来很好! (如果您的价格和数量范围有限。)

答案 5 :(得分:1)

我意识到我的回答变得很冗长,所以我将代码移到了顶部(这可能是大多数人感兴趣的地方)。在它下面有两件事:

  1. 解释(为什么)(深度)神经网络不是解决此问题的好方法,并且
  2. 解释为什么我们不能利用给定的信息来唯一地确定人的选择。

对于那些对任一主题感兴趣的人,请参见下文。对于其余的人,这里是代码。


找到所有可能解决方案的代码

正如我在答案中进一步解释的那样,您的问题未得到确定。在一般情况下,有许多可能的解决方案,并且该数目至少随着天数的增加而呈指数增长。原始问题和扩展问题均是如此。不过,我们可以(多种)有效地找到所有解决方案(这很困难NP,因此不要期望太大)。

Backtracking(从1960年代开始,所以并不完全是现代的)是这里选择的算法。在python中,我们可以将其编写为递归生成器,实际上非常优雅:

def backtrack(pos, daily_total, daily_item_value, allowed_change, iterator_bounds, history=None):
    if pos == len(daily_total):
        yield np.array(history)
        return

    it = [range(start, stop, step) for start, stop, step in iterator_bounds[pos][:-1]]
    for partial_basket in product(*it):
        if history is None:
            history = [partial_basket]
        else:
            history.append(partial_basket)

        # ensure we only check items that match the total basket value
        # for that day
        partial_value = np.sum(np.array(partial_basket) * daily_item_value[pos, :-1])
        if (daily_total[pos] - partial_value) % daily_item_value[pos, -1] != 0:
            history.pop()
            continue

        last_item = (daily_total[pos] - partial_value) // daily_item_value[pos, -1]
        if last_item < 0:
            history.pop()
            continue

        basket = np.array([*partial_basket] + [int(last_item)])
        basket_value = np.sum(basket * daily_item_value[pos])
        history[-1] = basket
        if len(history) > 1:
            # ensure that today's basket stays within yesterday's range
            previous_basket = history[-2]
            previous_basket_count = np.sum(previous_basket)
            current_basket_count = np.sum(basket)
            if (np.abs(current_basket_count - previous_basket_count) > allowed_change * previous_basket_count):
                history.pop()
                continue

        yield from backtrack(pos + 1, daily_total, daily_item_value, allowed_change, iterator_bounds, history)
        history.pop()

这种方法实质上将所有可能的候选对象构造成一棵大树,然后在违反约束时通过修剪执行深度优先搜索。每当遇到叶节点时,我们都会产生结果。

树搜索(通常)可以并行化,但这在本文范围之外。如果没有太多其他洞察力,它将使解决方案的可读性降低。减少代码的固定开销也是如此,例如,将约束if ...: continue处理到iterator_bounds变量中,并进行较少的检查。

我将完整的代码示例(包括用于游戏人的模拟器)放在该答案的底部。


针对此问题的现代机器学习

问题已经有9年的历史了,但仍然是我深感兴趣的问题。自那时以来,机器学习(RNN,CNN,GANS等),新方法和廉价GPU的兴起使新方法成为可能。我认为重新审视这个问题,看看是否有新方法是很有趣的。

我真的很喜欢您对深度神经网络世界的热情;不幸的是,由于某些原因,它们根本没有在这里申请:

  1. 精确度)如果您需要精确的解决方案,例如游戏中的NN,NN则无法提供。
  2. 整数约束)当前主要的NN训练方法是基于梯度下降的,因此问题必须是可区分的,或者您需要以使其可区分的方式重新构造它;将自己限制为整数会杀死摇篮中的GD方法。您可以尝试进化算法来搜索参数化。确实存在,但是这些方法目前还没有建立。
  3. 非凸性)在典型的公式中,训练NN是一种局部方法,这意味着如果算法收敛,您将找到1个(局部最优)解决方案。通常,您的游戏对于原始版本和扩展版本都有许多可能的解决方案。这不仅意味着-平均而言-您无法弄清人类的选择(购物篮),而且还无法控制NN将找到的众多解决方案中的哪一个。当前的NN成功故事也遭受着同样的命运,但是它们并不十分在意,因为他们只想要某种解决方案,而不是特定的解决方案。一些可以解决的解决方案彻底解决了所有问题。
  4. 专家领域知识)对于此游戏,您拥有很多领域知识,可以利用这些知识来改进优化/学习。充分利用NN中的任意领域知识并不是一件容易的事,对于此游戏,构建自定义ML模型(而非神经网络)将更容易,更高效。

为什么无法唯一解决游戏-第1部分

让我们首先考虑一个替代问题,并提高整数要求,即,篮子(在一天中人工选择N水果)可以有分数水果(0.3个橙子)。

总价值约束np.dot(basket, daily_price) == total_value限制了购物篮的可能解决方案;它将问题减少了一维。自由选择N-1个水果的数量,您总是可以找到第N个水果的值来满足约束条件。因此,尽管似乎一天有N个选择,但实际上我们只能自由选择N-1,最后一个将完全由我们先前的选择决定。因此,在游戏进行的每一天,我们都需要估算另外的N-1个选择/变量。

我们可能要强制所有选择都大于0,但这只会缩短我们选择数字的时间间隔;任何实数的开放区间都具有无限多个数字,因此我们永远不会因此而用尽期权。仍然N-1个选择。

在两天之间,购物篮的总量np.sum(basket)仅在前一天的some_percent(即np.abs(np.sum(previous_basket) - np.sum(basket)) <= some_percent * np.sum(previous_basket))之间发生变化。我们在给定日期可能会做出的一些选择将使购物篮变化超过前一天的some_percent。为确保我们从不违反此规则,我们可以自由选择N-2,然后必须选择第N-1个变量,以便将其添加并添加N-变量(即固定在我们先前的选择中)保持在some_percent之内。 (注意:这是一个不等式约束,因此只有在我们具有相等性的情况下,它才会减少选择的数量,即,篮子的变化正好some_percent。在优化理论中,这称为有效约束。)< / p>

我们可以再次考虑所有选择均应大于0的约束,但是仍然存在这样的争论,即这只是改变了我们现在可以自由选择N-2变量的时间间隔。

因此,在D天之后,我们有N-1个选项可以从第一天开始估算(无变化约束),还有(D-1)*(N-2)个选项可以在第二天进行估算。不幸的是,我们没有足够的限制来进一步减少该数目,并且未知数每天至少增加N-2。这实质上就是Luka Rahne所说的“ 2*D < N*D for all N > 2”。我们很可能会发现很多可能性都相同的候选人。

每天的确切食品价格与此无关。只要它们具有一定的价值,它们就会限制选择之一。因此,如果您以指定的方式扩展游戏,则总是有无限的解决方案的机会。不管天数如何。


为什么仍无法唯一解决游戏问题-第2部分

有一个限制条件我们没有考虑到哪个可能可以解决此问题:仅允许整数解决方案供选择。整数约束的问题在于它们处理起来非常复杂。但是,这里我们主要关心的是,如果添加此约束,将使我们能够在足够的时间里唯一地解决问题。为此,有一个相当直观的反例。假设您连续3天,并且对于第一天和第三天,总价值约束只允许一个购物篮。换句话说,我们知道第1天和第3天的篮子,但不是第2天。在这里,我们只知道它的总价值,它在第1天的some_percent内,并且那第三天就在第二天的some_percent之内。这是否足以使第二天总算出篮子里的东西?

some_percent = 0.05
Day 1: basket: [3 2]  prices: [10 7]  total_value: 44
Day 2: basket: [x y]  prices: [5  5]  total_value: 25
Day 3: basket: [2 3]  prices: [9  5]  total_value: 33

Possible Solutions Day 2: [2 3], [3 2]

上面是一个示例,由于总值限制,我们知道两天的值,但这仍然无法让我们计算出第二天篮子的确切组成因此,尽管在某些情况下有可能进行计算,但总体上是不可能的。在第3天之后添加更多天根本无法弄清楚第2天。它可能有助于缩小第3天的选项(然后缩小第2天的选项),但是对于第3天我们只剩下1个选择,因此没有用。


完整代码

import numpy as np
from itertools import product
import tqdm


def sample_uniform(n, r):
    # check out: http://compneuro.uwaterloo.ca/files/publications/voelker.2017.pdf
    sample = np.random.rand(n + 2)
    sample_norm = np.linalg.norm(sample)
    unit_sample = (sample / sample_norm)
    change = np.floor(r * unit_sample[:-2]).astype(np.int)
    return change


def human(num_fruits, allowed_change=0.05, current_distribution=None):
    allowed_change = 0.05
    if current_distribution is None:
        current_distribution = np.random.randint(1, 50, size=num_fruits)
    yield current_distribution.copy()

    # rejection sample a suitable change
    while True:
        current_total = np.sum(current_distribution)
        maximum_change = np.floor(allowed_change * current_total)

        change = sample_uniform(num_fruits, maximum_change)
        while np.sum(change) > maximum_change:
            change = sample_uniform(num_fruits, maximum_change)

        current_distribution += change
        yield current_distribution.copy()


def prices(num_fruits, alter_prices=False):
    current_prices = np.random.randint(1, 10, size=num_fruits)
    while True:
        yield current_prices.copy()
        if alter_prices:
            current_prices = np.random.randint(1, 10, size=num_fruits)


def play_game(num_days, num_fruits=3, alter_prices=False):
    human_choice = human(num_fruits)
    price_development = prices(num_fruits, alter_prices=alter_prices)

    history = {
        "basket": list(),
        "prices": list(),
        "total": list()
    }
    for day in range(num_days):
        choice = next(human_choice)
        price = next(price_development)
        total_price = np.sum(choice * price)

        history["basket"].append(choice)
        history["prices"].append(price)
        history["total"].append(total_price)

    return history


def backtrack(pos, daily_total, daily_item_value, allowed_change, iterator_bounds, history=None):
    if pos == len(daily_total):
        yield np.array(history)
        return

    it = [range(start, stop, step) for start, stop, step in iterator_bounds[pos][:-1]]
    for partial_basket in product(*it):
        if history is None:
            history = [partial_basket]
        else:
            history.append(partial_basket)

        # ensure we only check items that match the total basket value
        # for that day
        partial_value = np.sum(np.array(partial_basket) * daily_item_value[pos, :-1])
        if (daily_total[pos] - partial_value) % daily_item_value[pos, -1] != 0:
            history.pop()
            continue

        last_item = (daily_total[pos] - partial_value) // daily_item_value[pos, -1]
        if last_item < 0:
            history.pop()
            continue

        basket = np.array([*partial_basket] + [int(last_item)])
        basket_value = np.sum(basket * daily_item_value[pos])
        history[-1] = basket
        if len(history) > 1:
            # ensure that today's basket stays within relative tolerance
            previous_basket = history[-2]
            previous_basket_count = np.sum(previous_basket)
            current_basket_count = np.sum(basket)
            if (np.abs(current_basket_count - previous_basket_count) > allowed_change * previous_basket_count):
                history.pop()
                continue

        yield from backtrack(pos + 1, daily_total, daily_item_value, allowed_change, iterator_bounds, history)
        history.pop()


if __name__ == "__main__":
    np.random.seed(1337)
    num_fruits = 3
    allowed_change = 0.05
    alter_prices = False
    history = play_game(15, num_fruits=num_fruits, alter_prices=alter_prices)

    total_price = np.stack(history["total"]).astype(np.int)
    daily_price = np.stack(history["prices"]).astype(np.int)
    basket = np.stack(history["basket"]).astype(np.int)

    maximum_fruits = np.floor(total_price[:, np.newaxis] / daily_price).astype(np.int)
    iterator_bounds = [[[0, maximum_fruits[pos, fruit], 1] for fruit in range(num_fruits)] for pos in range(len(basket))]
    # iterator_bounds = np.array(iterator_bounds)
    # import pdb; pdb.set_trace()

    pbar = tqdm.tqdm(backtrack(0, total_price,
                               daily_price, allowed_change, iterator_bounds), desc="Found Solutions")
    for solution in pbar:
        # test price guess
        calculated_price = np.sum(np.stack(solution) * daily_price, axis=1)
        assert np.all(calculated_price == total_price)

        # test basket change constraint
        change = np.sum(np.diff(solution, axis=0), axis=1)
        max_change = np.sum(solution[:-1, ...], axis=1) * allowed_change
        assert np.all(change <= max_change)

        # indicate that we found the original solution
        if not np.any(solution - basket):
            pbar.set_description("Found Solutions (includes original)")

答案 6 :(得分:0)

当玩家选择一个组合时,将可能的数量减少到1,则计算机将获胜。否则,玩家可以选择一种组合,其总数的约束在一定百分比之内变化,这可能导致计算机永远无法获胜。

import itertools
import numpy as np


def gen_possible_combination(total, prices):
    """
    Generates all possible combinations of numbers of items for
    given prices constraint by total
    """
    nitems = [range(total//p + 1) for p in prices]
    prices_arr = np.array(prices)
    combo = [x for x in itertools.product(
        *nitems) if np.dot(np.array(x), prices_arr) == total]

    return combo


def reduce(combo1, combo2, pct):
    """
    Filters impossible transitions which are greater than pct
    """
    combo = {}
    for x in combo1:
        for y in combo2:
            if abs(sum(x) - sum(y))/sum(x) <= pct:
                combo[y] = 1

    return list(combo.keys())


def gen_items(n, total):
    """
    Generates a list of items
    """
    nums = [0] * n
    t = 0
    i = 0
    while t < total:
        if i < n - 1:
            n1 = np.random.randint(0, total-t)
            nums[i] = n1
            t += n1
            i += 1
        else:
            nums[i] = total - t
            t = total

    return nums


def main():
    pct = 0.05
    i = 0
    done = False
    n = 3
    total_items = 26  # np.random.randint(26)
    combo = None
    while not done:
        prices = [np.random.randint(1, 10) for _ in range(n)]
        items = gen_items(n, total_items)

        total = np.dot(np.array(prices),  np.array(items))
        combo1 = gen_possible_combination(total, prices)

        if combo:
            combo = reduce(combo, combo1, pct)
        else:
            combo = combo1
        i += 1
        print(i, 'Items:', items, 'Prices:', prices, 'Total:',
              total, 'No. Possibilities:', len(combo))

        if len(combo) == 1:
            print('Solution', combo)
            break
        if np.random.random() < 0.5:
            total_items = int(total_items * (1 + np.random.random()*pct))
        else:
            total_items = int(
                np.ceil(total_items * (1 - np.random.random()*pct)))


if __name__ == "__main__":
    main()