Shuffle cards in reverse order - breadth first search BFS

Posted by derwert on Thu, 30 Dec 2021 01:58:58 +0100

1. Problem description

This question comes from question 42 in the interesting question of programmer's algorithm.

Suppose there are 2n playing cards. Each time we draw n cards from them (not scattered, but a continuous stack of cards) and place them on the top of the stack. Then repeat this operation until the order of cards is opposite to the original order.

Excuse me, when n=5, how many steps does it take to arrange 10 cards in reverse order?

2. Analysis

        N=1. Obviously, it takes only one operation to reverse the order.

        N=2. Assume that the initial order is [1,2,3,4], and assume that the left represents the position above the queue. Then the original sorting can be reversed through the following operation steps: [1,2,3,4] à [2,3,1,4] à [3,1,2,4] à [2,4,3,1] à [4,3,2,1], that is, four operations can reverse the original sorting (see Figure 1 below). However, can it be proved that four operations are the least required times?

The case of N=3 needs to be calculated only with paper and pen, which is beyond the scope of ordinary people's brain.

Each sort of deck represents a state. When there are 2N cards, there are (2N)! Two states. When N=3, the number of States has increased to (6!)= 720, so it is almost impossible to solve it by hand.

The problem can be transformed into the minimum number of steps required to reach the termination state [2*N,2*N-1,..., 3,2,1] through the operation agreed by the problem from the initial state (without losing generality, which can be recorded as [1,2,3,..., 2*N]). This is actually the problem of the shortest distance between two nodes of the graph. The "standard" algorithm to solve this shortest distance problem is breadth first search (BFS: Breadth First Search).

Several elements of breadth first search:

  1. Status representation. In this question, an arrangement of 2*N numbers can be used to represent a state. In order to calculate and give the shortest distance, it is also necessary to record the distance between a state and the initial state.
  2. The transfer between States, or the adjacent node of a node. In this problem, starting from each arrangement, there are n ways to draw consecutive N cards, and each way gets a new state. Therefore, each node has N adjacent nodes.
  3. The memory of the accessed node. This is to avoid repeated access to nodes that have been accessed.

3. Solution#1-- breadth first search is basically realized

Figure 1 (top) moving steps when N=2; (II) pseudo code of the basic implementation of breadth first search

The implementation code is as follows:

"""
Created on Wed Aug  4 09:06:49 2021

@author: chenxy
"""
import sys
import time
import random
from   typing import List
from   queue import Queue

class Solution:
    def reverseCardSet(self, N: int) -> List:
        stateQue = Queue(maxsize = 0) # an infinite queue for storing card set state
        distQue  = Queue(maxsize = 0) # an infinite queue for storing distance of each corresponding state
        
        s_start  = [k for k in range(1,2*N+1)]
        s_end    = [k for k in range(2*N,0,-1)]
        # print('s_start = ', s_start)
        # print('s_end = ', s_end)
        distQue.put(0)
        stateQue.put(s_start)
        visited = []
        
        while not stateQue.empty():
            curState = stateQue.get()
            curDist  = distQue.get()
            visited.append(curState)
            # print('curState: ',curState)
            if curState == s_end:
                return [curDist,len(visited)]
            else:                
                for k in range(1,N+1):
                    childState = curState[k:k+N] + curState[0:k] + curState[k+N:]
                    # print('    childState: ',childState)
                    if childState not in visited:
                        distQue.put(curDist+1)
                        stateQue.put(childState)

Test with the following code:

if __name__ == '__main__':        
            
    sln = Solution()

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

Operation results:

N = 1, ans = [1, 2], tCost = 0.0(sec)
N = 2, ans = [4, 11], tCost = 0.0(sec)
N = 3, ans = [7, 841], tCost = 0.029889583587646484(sec)
N = 4, ans = [8, 28889], tCost = 29.551127672195435(sec)

The first item of ANS represents the number of steps required, and the second item represents the number of nodes or states accessed.

Result analysis:

The time consumption of N=4 is about 1000 times higher than that of N=3! For qualitative analysis, on the one hand, the number of nodes / states that need to be accessed has increased significantly; On the other hand, the representation length of each state also changes from 6 to 8, so the time for single access and state comparison also increases. But this doesn't seem to fully explain the 1000 times ratio.

If we go down at this growth rate, it is obvious that the running calculation of N=5 will become unbearably long. Optimization needs to be considered from two aspects: (1) optimization of algorithm; (2) optimization of implementation mode, such as optimization of storage data structure for storing intermediate data.

In addition, when N=3, there are only (2*N)=720 node states in total. Why does the number of access node states reach 841? Does this mean that there are still bug s in the current basic implementation itself?

Next, further optimization schemes will be considered.

Topics: Python Algorithm