Programmer's interesting algorithm Q20: suffering facade magic square

Keywords: Algorithm Dynamic Programming

1. Problem description

         There is a famous scenic spot in Spain called the holy family hall. The "crucifixion Facade" mainly depicts the scene of Jesus from "the last supper" to "Ascension". There is also a magic cube array as shown in the figure below, which is famous for "the sum of vertical, horizontal and diagonal numbers is 33" (it is said that Jesus was 33 when he died).

         If it is not limited to the addition of vertical, horizontal and diagonal numbers, there are 310 combinations with a sum of 33 (there are many problems such as "adding four numbers..." on the Internet. If it is limited that only four numbers can be added, there are 88).

         Problem: use this magic square matrix to perform addition operations that meet the following conditions to find the sum with the largest number of "and the same combination".

         Conditions:

  1. It is not limited to the addition of numbers on the vertical, horizontal and diagonal lines
  2. The number of addends is not limited to 4

         In fact, it is to find the sum of any combination among the given 16 numbers, and the sum with the largest number of combination types that get the same sum.

2. Solution 1 -- stupid method

         This is the first method based on intuition (usually I will start with the most na ï ve method, which is clumsy, but it also has its value). After all, it is not what ordinary people can expect. Step by step may be more in line with the pace of ordinary people.

         For each possible sum target (the possible number of sum is the sum of all the above numbers plus 1, and 0 is also considered as a possibility, but only when the number in the given array is non negative), scan all combinations and confirm that the sum is the number of combination types of target.

         Then compare the number of combination types corresponding to all possible target s. The solution process is as follows:

         See shouNanMoFang1() below for the code

3. Solution 2 -- reverse thinking

         Solution 1 is too slow. It takes seconds when there are 16 numbers in the num array. What is the number of combinations of all possible lengths of 16 numbers? Exactly binomial formula, as follows:

         When the number of Nums is 16, there are only 65536 combinations. If the magic cube array is 5 * 5 = 25, the number of combinations will become , which is 512 times larger than the case of 4 * 4 = 16. Obviously, it is unbearable according to solution 1.

         The problem of solution 1 is that taking target as the focus, each target is scanned in full combination, which leads to huge waste. If you look at it in reverse, you only need to perform a full combination scan, and count each combination according to its sum to the counter of the corresponding target! In this way, the running time is reduced to (1 / sum (Num)).

         In the following code, a hash table (python dict) is used to record the combination counter corresponding to each possible target.

         See shouNanMoFang2() below for the code

         Although the efficiency of solution 2 is about two orders of magnitude higher than that of solution 1, it still takes tens of seconds in the case of 5 * 5 magic square array (i.e. 25 numbers). There is nothing to do with a larger magic cube array (such as 6 * 6).  

  

4. Solution 3 -- dynamic programming

         The dynamic programming method mentioned here refers to the tips of the original book. But the hint in the original book is just a few words "set a number at a time...". How fat? It's a little similar to the "obvious and easy to prove" in mathematical proof (the brain circuits between people are so different). After consulting the solutions on several CSDN blogs, I basically put a piece of code directly... Liver pain... (copy ing a piece of code is very simple, or translating ruby code into code in other languages is also very simple). However, I still hope to make it clear in my own words. The key points of the dynamic programming solution of this problem are as follows:

         Consider the processing of each number of the magic square matrix (nums in the code) in order.

         Make targetCnt a two-dimensional array (or a table - yes, the most basic method of dynamic programming is to fill in a table!) and record the number of possible combinations of targetSum and values when using different subsets of num. targetCnt[k, m] represents the number of combination types whose sum is m under the condition that only the first k+1 numbers in num are considered (i.e. {num [0],..., Num [k]}).

         Let's say we've finished processing nums[0],..., nums[k-1]. The content of the current targetCnt[k-1,:] is equivalent, so only the results obtained by all possible combinations of {num [0],..., Num [k-1]} are considered. Next, add num [k]. Obviously, for targetSum=m, only all combinations of {num [0],..., Num [k-1]} and the sum is equal to (m-num [k]) plus num [k] constitute the combination of sum and M. from this, we can get the recursive relationship as follows (to make the formula simpler, use the first coordinate as the subscript and the second coordinate as the superscript):

         In this way, the problem-solving process has become a classic dynamic planning and filling in the form. Further, different from the general dynamic programming problem such as knapsack problem, in this problem Rely only on And Therefore, in fact, we do not need to maintain a 2-dimensional table, but only a 1-dimensional array. Just note that when updating tangetCnt, you should update from large to small according to superscript.

        Finally, as a dynamic programming solution, of course, the initial conditions or boundary conditions must be defined. The boundary condition of this problem can be defined as: when the empty combination (or select 0 from nums), the number of combinations when targetSum=0 is 1, and when targetsum > 0 is 0, that is:

         See shouNanMoFang3() below for code

5. Code and test

# -*- coding: utf-8 -*-
"""
Created on Thu Sep  9 17:45:33 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 shouNanMoFang1(self, nums: List):
        totalSum = sum(nums)
        # cnts     = []
        targetSum   = 0  # The target sum which has the greatest number of combination sum
        targetCnt   = 0  # The number combination sum of the target sum
     
        for target in range(1,totalSum+1):
            cnt  = 0
            # ansLst = []
        
            for k in range(1,len(nums)+1):
                # print('k = ',k)
                for p in it.combinations(nums,k):
                    p_sum = sum(p)
                    if p_sum == target:
                        cnt += 1
                        # ansLst.append(p)
            if cnt > targetCnt:
                targetCnt = cnt
                targetSum = target
            # cnts.append(cnt)
        return targetCnt, targetSum

    def shouNanMoFang2(self, nums: List):
        
        numCombSum = dict()
        for k in range(1,len(nums)+1):
            for p in it.combinations(nums,k):
                p_sum = sum(p)
                if sum(p) in numCombSum:
                    numCombSum[sum(p)] += 1
                else:
                    numCombSum[sum(p)] = 1
        targetSum = max(numCombSum, key=numCombSum.get)
        targetCnt = numCombSum[targetSum]
        
        return targetCnt, targetSum

    def shouNanMoFang3(self, nums: List):
        totalSum     = sum(nums)
        targetCnt    = [0] * (totalSum + 1)
        targetCnt[0] = 1
    
        for n in nums:
            for i in range(totalSum - n, -1, -1):
                targetCnt[i + n] += targetCnt[i]
    
        targetSum = max(targetCnt)
        return targetSum, targetCnt.index(targetSum)

 

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

    nums     = [1,14,14,4,11,7,6,9,8,10,10,5,13,2,3,15]
    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang1(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution1: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))
    
    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang2(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution2: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))            

    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang3(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution2: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))                                
            
    nums     = [1,14,14,4,11,7,6,9,8,10,10,5,13,2,3,15,34,19,21,44,12,31,47,13,41]    
    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang2(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution2: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))                    

    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang3(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution2: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))                                
        
    nums     = [1,14,14,4,11,7,6,9,8,10,10,5,13,2,3,15,34,19,21,44,12,31,47,13,41,\
                2,15,17,5,12,37,26,19,28,20,30]    
    tStart = time.perf_counter()
    targetSum, targetCnt = sln.shouNanMoFang3(nums)
    tCost  = time.perf_counter() - tStart    
    print('Solution2: len(nums)={0}, targetSum = {1}, targetCnt = {2}, tCost = {3:5.3f}(sec)'\
          .format(len(nums),targetSum,targetCnt,tCost))  

The operation results are as follows:

         From the operation results, it can be seen that solution 2 is improved by two orders of magnitude compared with solution 1, and solution 3 is further improved by four orders of magnitude compared with solution 2! The power of reasonable algorithm design is so great!  

        Previous: Q19: friends or friends?

        Next: Q21: XOR Yang Hui triangle

         For the general catalogue of this series, see: Programmer's interesting algorithm: detailed analysis and Python complete solution

Posted by space1 on Sat, 20 Nov 2021 14:45:52 -0800