程序员的算法趣题Q52: 糖果恶作剧

本文详细解析了一个关于糖果错配的算法问题,当有N种糖果,每种M颗,如何计算完全错配的情况。问题转化为错排问题的扩展,通过动态规划策略解决。博主分享了思考过程、解题分析和Python实现,探讨了如何在M>1时进行子问题分解,并使用memoization优化运行时间。最后展示了代码运行结果。
摘要由CSDN通过智能技术生成

目录

1. 问题描述

2. 解题分析

3. 代码及测试


1. 问题描述

        假设有N种糖果,每种糖果有M颗。不同种类的糖果有不同颜色的糖纸和不同味道。同种糖果的糖纸可以区分,但是糖果本身无法区分。每颗糖果都用自己匹配的糖纸包裹叫做完全匹配;每颗糖果都用与自己不匹配的糖纸包裹叫做完全错配;介乎于两者之间的叫部分错配。

        请问,当N=5,M=6时有多少种完全错配的情况?

        注意,即便同种糖果的糖纸也是可以区分,而同种糖果之间不可区分。比如说,‘苹果味的糖纸1包裹草莓味的糖果1’与‘苹果味的糖纸1包裹草莓味的糖果2’就不可区分算作相同的组合;‘苹果味的糖纸1包裹草莓味的糖果1’与‘苹果味的糖纸2包裹草莓味的糖果2’则由于糖纸不同因而是可以区分的。

2. 解题分析

        在M=1时,这个问题实质上是一个“错排问题”。等价于“n个人互换礼物,每个人最终拿的都不是原本自己送出的礼物的组合方式一共多少种”的问题。

        在M>1时问题变得要复杂得多。考虑动态规划策略,先进行子问题分解。

        考虑用candy表示当前各种糖果尚未分配的数量的数组;paper表示各种糖纸尚余数量的数组。各数组的序号可以理解为各种糖果/糖纸的种类序号。以candy和paper联合表示当前分配状态,记为{candy,paper}。比如说,candy=[3,3,3]表示共有3种糖果,且每种有3颗糖果未分配,paper=[3,2,1]则表示0号糖纸还有3张,1号糖纸还有2张,2号糖纸还有1张…

        为了方便考虑(但并不失一般性),考虑接下来取candy中尚未分配的糖果中种类序号最低的一枚糖果(可能有多枚,但是同种糖果不能区分所以任取一枚)进行分配,假设为k号糖果,那它可以分配给哪种糖纸呢?假定分配j号糖纸,首先不能是k号糖纸,即j!=k;其次,该j号糖纸必须还有未分配的,即paper[j]>0。做完这枚糖果分配后,分配状态需要如下更新:k号糖果数要减一;j号糖纸数也要减一。然后可以基于更新后的{candy,paper}进行递归调用(solving the sub-problem)。

        当前状态{candy,paper}下的分配数等于对所有满足条件的j的子问题解之和。由此可得处理流程(伪代码)如下:

         但是以上流程中尚未考虑memoization,实现时需要追加进去,否则的话运行时间会变得无法承受。

        【吐槽】老实讲这道题断断续续想了两三天理不清,原书没有解说直接上来一段代码,看半天也没看懂,不是因为Ruby语言的问题,而是确实没有看懂他的思路(廉颇老矣。。。但是不服气)。还是得以自己能够理解的方式想清楚、用自己的语言能解说清楚、并用代码实现了才能有最大的收获。从这个意义上来说,原书解说匮乏以及代码看不懂倒是一件好事,逼着自己只能硬着头皮干。。。

3. 代码及测试

# -*- coding: utf-8 -*-
"""
Created on Fri Aug 27 08:41:49 2021

@author: chenxy
"""

"""
import sys
import time
import datetime
# import random
from   typing import List
# from   queue import Queue
# from   collections import deque

class Solution:
    def candyMisMatch(self, N: int, M: int) -> int:
        """
        :N:    The number of the kinds of candy
        :M:    The number of candied for each kind
        :ret:  The total number of complete-mismatch
        """                
        memo = dict()
        def explore(candy, paper):
            # print('explore:', candy, paper)
            if candy[-1] == 0:
                return 1
            
            if tuple(candy+paper) in memo:
                return memo[tuple(candy+paper)]
            
            for k in range(N):
                if candy[k] > 0:
                    break
            sum = 0
            for j in range(N):
                if j!=k and paper[j]>0:
                    candy[k] -= 1
                    paper[j] -= 1
                    sum += explore(candy,paper)
                    candy[k] += 1
                    paper[j] += 1
            memo[tuple(candy+paper)] = sum
            
            return sum
        return explore(N*[M], N*[M])
if __name__ == '__main__':        
            
    sln    = Solution()    

    N, M   = 5,6
    tStart = time.time()
    ans    = sln.candyMisMatch(N,M)
    tCost  = time.time() - tStart
    print('N={0}, M={1}, ans={2}, tCost={3:6.2f}(sec)'.format(N,M,ans,tCost))

运行结果:

        N=5, M=6, ans=1926172117389136, tCost=  0.06(sec)

        上一篇:Q51: 同时结束的沙漏

        下一篇:Q53: 同数包夹

        程序员的算法趣题:详细分析和Python全解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

笨牛慢耕

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

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

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

打赏作者

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

抵扣说明:

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

余额充值