当前位置:首页 » 《资源分享》 » 正文

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

29 人参与  2021年09月27日 11:03  分类 : 《资源分享》  评论

点击全文阅读


目录

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: 不缠绕的纸杯电话

        下一篇:Q27: 禁止右转

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


点击全文阅读


本文链接:http://zhangshiyu.com/post/28803.html

硬币  解法  路径  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1