程序员的算法趣题Q53: 同数包夹

目录

1. 问题描述

2. 解题分析

2.1 节点表示

2.2 邻节点搜索

2.3 Memoization

3. 递归实现:从小到大插入

运行结果:

4. 递归实现:从大到小插入

5:递归实现:二进制表示

6:非递归实现


1. 问题描述

        有分别标了数字1~n的两副牌,共2n张。把这些牌排成一排,确保两张1的中间夹一张牌,两张2的中间夹2张牌,。。。,两张n的中间夹n张牌。

        请问,当n=11时,有多少种排列方法?

2. 解题分析

        考虑有2n个位置,按顺序地且按照以上规则的约束,安放每个数字的两张牌,直到2n张牌将2n个位置占满。这样的话这个问题可以看成(转换成)图搜索问题中的路径遍历问题。即从“空”状态开始到达“满”状态的所有不同路径的数量。这可以通过深度优先搜索来解决。

        但是要注意的是,这里的终止状态并不是唯一的,所有的“满”状态都是终止状态。所以这不是从图中的某个起始节点到另一终止节点之间的路径遍历,而是遍历搜索从“空”状态节点到达所有合法的“满”状态节点的可能路径。

        深度优先路径遍历搜索的要点如下所述。

2.1 节点表示

        以长度2n的数组表示,“空”状态指数组中的元素为全“0”。“满”状态指数组中的元素全部为非零的状态,但是并非所有的“满”状态都是合法的状态。但是在本题中单独的状态还不能完全表示一个节点,还要加上接下来要插入的数据才能足以唯一地表示节点信息,即{state, m}。

2.2 邻节点搜索

        基于当前状态curState和接下来要插入的数据m,可以按如下方式确定邻节点:从数组头部开始搜索,搜索到两个相隔m个位置都为空的时候,即可认为找到一个合法的邻节点{newState, m+1},newState为将m插入前述两个位置,而接下来要插入的数也相应地更新为m+1。

2.3 Memoization

3. 递归实现:从小到大插入

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

class Solution:
    def numberArrangement(self, N: int) -> int:
        """
        :N:    
        :ret:  The number of arrangement
        """        
        start   = 2*N*[0]    # Nested list to represent state/node
                                            
        def explore(curState, num):
            """
            :curState: current state to explore
            :num:     the number to be arranged
            :ret:     the number of arrangements
            """
            # print('explore({0}, {1})'.format(curState, num))
            # Judge whether reach the goal or final state.
            if num > N:
                return 1
            
            sum = 0
            for k in range(0,2*N-num-1):
                if curState[k]==0 and curState[k+num+1]==0:
                    # nxtState    = curState # Both points to the same object! NG
                    nxtState    = curState.copy()
                    nxtState[k] = num
                    nxtState[k+num+1] = num
                    sum += explore(nxtState,num+1)            
            return sum

        return explore(start,1)
if __name__ == '__main__':        
            
    sln = Solution()    

    for N in range(2,12):
        tStart = time.time()
        ans    = sln.numberArrangement(N)
        tCost  = time.time() - tStart
        print('N={0}, ans = {1}, tCost = {2}(sec)'.format(N,ans,tCost))   

运行结果:

        N=2, ans = 0, tCost = 0.0(sec)
        N=3, ans = 2, tCost = 0.0(sec)
        N=4, ans = 2, tCost = 0.0(sec)
        N=5, ans = 0, tCost = 0.0(sec)
        N=6, ans = 0, tCost = 0.0010356903076171875(sec)
        N=7, ans = 52, tCost = 0.0029900074005126953(sec)
        N=8, ans = 300, tCost = 0.02193927764892578(sec)
        N=9, ans = 0, tCost = 0.1495983600616455(sec)
        N=10, ans = 0, tCost = 1.1013171672821045(sec)
        N=11, ans = 35584, tCost = 8.87007999420166(sec)

        令人意外的是,N=5,6,9,10居然没有合法的安排方式!是程序的bug吗(N=11的结果与原书的结果一致,所以应该概率比较低)?如果是正确的运行结果的话,那么有没有可能从数学上证明满足什么条件才能有合法的安排方式呢?

        另外,运行时间略长,需要进一步优化。

4. 递归实现:从大到小插入

        To be added

5:递归实现:二进制表示

        To be added

6:非递归实现

        To be added

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

        下一篇:程序员的算法趣题Q54: 偷懒的算盘

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

笨牛慢耕

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

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

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

打赏作者

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

抵扣说明:

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

余额充值