程序员的算法趣题Q23: 二十一点通吃

本文探讨了一种二十一点赌博游戏的数学问题,玩家初始有1枚硬币,每次投注后赢或输一枚,目标是在24次投注后仍有硬币剩余。通过暴力搜索、深度优先搜索和动态规划三种方法求解在拥有10枚硬币时,24回合后的硬币变化情况。动态规划解法给出了递推公式并实现了高效计算,但实际运行性能并未显著优于其他方法。
摘要由CSDN通过智能技术生成

目录

1. 问题描述

2. 解法1--暴力搜索

3. 解法2--深度优先路径搜索

4. 解法3--动态规划

 5. 代码及测试

6. 后记


1. 问题描述

        赌场经典的二十一点游戏中,每回合下注 1 枚硬币,赢了可以得到 2 枚硬币(+1枚),输了硬币会被收走(-1枚)。

        假设最开始只拥有 1 枚硬币,并且每回合下注 1 枚,那么 4 回合后还能剩余硬币(即没有输光)的硬币枚数变化情况如图所示,共有 6 种(圆形中间的数字代表硬币枚数)

        求最开始拥有10枚硬币时,持续24回合后硬币还能剩余的硬币枚数变化情况共有多少种?

2. 解法1--暴力搜索

        每次投注有赢和输两种情况,则24次投注有总共 种组合情况。注意,由于输光即游戏终止(即不准透支),因此输赢的顺序是有关系的。有些组合虽然从24把投注的总成绩来看是有硬币剩余的,但是在前面部分已经出现了输超过赢导致游戏提前终止了。

        代码参见:point21game1()

        由于没有把中间可以提前退出的情况考虑进去,本算法存在比较大的运算效率浪费。

3. 解法2--深度优先路径搜索

        本问题可以按照深度优先路径搜索的思路来解决。这样可以有效地解决“解法1”的问题,即中间碰到已经输光的情况时,就不必沿着当前路径继续向下探索了。本系列有很多类似的题目,比如说,Q14: 国名接龙, Q18: 水果酥饼日 ,基本上可以用相同的框架来解决,这里不再做过多说明。

        代码参见:point21game2()

        意外的是,运行时间相比解法1并没有质的提升。不过从最终结果来看,在{10,24}的条件下,总的可能路径数接近 ,或者可以说本题的合法路径解是比较稠密的,因此DFS策略能带来的效率提升也就有限了。

4. 解法3--动态规划

        进一步,本题还可以用动态规划的策略来解决。

        考虑{ steps=k, coin=c}条件下的总可能路径数记为f(k,c),当前下注的结果有两种可能,赢了则硬币数变为c+1(相应地步数k减一),输了则硬币数变为c-1(相应地步数k减一),因此可以得到以下递推关系式:

        考虑本游戏的规则,当硬币变为0就表示失败了;另一方面,在硬币变为0之前步数用完了,就表示赢了。因此可以得到以上递推关系式的初始或边界条件:

        代码参见:point21game3() 

 5. 代码及测试

# -*- coding: utf-8 -*-
"""
Created on Sat Sep 11 07:56:17 2021

@author: chenxy
"""

import sys
import time
import datetime
import math
# import random
from   typing import List
# from   queue import Queue
# from   collections import deque
import itertools as it

class Solution:
    def point21game1(self, coin:int, steps:int)->int:
        """
        Parameters
        ----------
        coin : The money for the start
        steps    : The number of steps of game

        Returns  : The number of paths for which there is money left        
        -------
        """
        k = 0
        count    = 0
        for item in it.product([1,-1],repeat=steps):
            # print(item)
            # k+=1
            # if k%(65536*4) == 0:
            #     print('k = {0}'.format(k//(65536*4)))
            
            balance = 0
            flag = True
            for i in item:                
                balance += i
                if balance == -coin:
                    flag = False
                    break
            if flag:
                count += 1
        return count

    def point21game2(self, coin:int, steps:int)->int:
        """
        Parameters
        ----------
        coin : The money for the start
        steps   : The number of steps of game

        Returns : The number of paths for which there is money left        
        -------
        """
        # path  = []
        # balance  = 0
        def explore(path, balance):
            if len(path)==steps and balance > (-coin):
                return 1
            
            count = 0
            for stake in [1,-1]:
                if (balance + stake) > (-coin):
                    count += explore(path+[stake],balance+stake)
                    
            return count
        
        return explore([],0)

    def point21game3(self, coin:int, steps:int)->int:
        """
        Parameters
        ----------
        coin : The money for the start
        steps    : The number of steps of game

        Returns  : The number of paths for which there is money left        
        -------
        """
        
        memo    = dict()
        def dp(k, c):
            # print('k={0},c={1}'.format(k,c))
            if (k,c) in memo:
                return memo[(k,c)]
            if c == 0:
                return 0
            if k == 0:
                return 1
                                
            return dp(k-1,c+1) + dp(k-1,c-1)
        
        return dp(steps,coin)
if __name__ == '__main__':        
            
    sln     = Solution()   
    coin = 1
    steps   = 4
    tStart  = time.perf_counter()
    count1  = sln.point21game1(coin, steps)
    count2  = sln.point21game2(coin, steps)
    count3  = sln.point21game3(coin, steps)
    tCost   = time.perf_counter() - tStart
    print('({0}, {1}), count1 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count1,tCost))        
    print('({0}, {1}), count2 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count2,tCost))        
    print('({0}, {1}), count3 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count3,tCost))        

    coin = 10
    steps    = 24
    tStart = time.perf_counter()
    count1 = sln.point21game1(coin, steps)
    tCost  = time.perf_counter() - tStart
    print('({0}, {1}), count1 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count1,tCost))            

    tStart = time.perf_counter()
    count2 = sln.point21game2(coin, steps)
    tCost  = time.perf_counter() - tStart
    print('({0}, {1}), count2 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count2,tCost))            
    
    tStart = time.perf_counter()
    count3 = sln.point21game3(coin, steps)
    tCost  = time.perf_counter() - tStart
    print('({0}, {1}), count3 = {2}, tCost = {3:6.3f}(sec)'.format(coin,steps,count3,tCost))   

运行结果:

         意外的是,以递归调用方式实现的动态规划相比前两种解法也并没有看到运行性能的质的提升。。。是不是改用循环的方式开始实现会有更好的运行性能呢?

6. 后记

        本题还可以转换为以下问题。考虑从(0,0)出发,只能往右(对应于输)或向上(对应于赢),考虑总步数24的前提下,限于在图中阴影区域中移动到达反斜对角线上各点{(0,24), (1,23),…, (16,8)}的总的可能路径数。

        上一篇:Q22: 不缠绕的纸杯电话

        下一篇:Q24: 完美的三振出局

        本系列总目录参见:程序员的算法趣题:详细分析和Python全解

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值