蓝桥杯 ALGO-1005 数字游戏 DFS记忆化搜索+剪枝+杨辉三角 python

题目

问题描述

给定一个1~N的排列a[i],每次将相邻两个数相加,得到新序列,再对新序列重复这样的操作,显然每次得到的序列都比上一次的序列长度少1,最终只剩一个数字。
  例如:
  3 1 2 4
  4 3 6
  7 9
  16
  
现在如果知道N和最后得到的数字sum,请求出最初序列a[i],为1~N的一个排列。若有多种答案,则输出字典序最小的那一个。数据保证有解。
  
输入格式

第1行为两个正整数n,sum
  
输出格式

一个1~N的一个排列

样例输入

4 16

样例输出

3 1 2 4

数据规模和约定

0

感觉这题目不够规范呀:n和N都出现了,应该统一字母的;也用了sum作为变量声明,在python中sum是一个函数,应避免使用函数名作为变量名,也没有给出最后数字s的取值范围…除此以外还要区分开题目中“一个1~N的排列”,“1~N的一个排列”,“一个1~N的一个排列”的意思,因为理解错题目,我还特地在问答区求助了,呜呜呜,说多都是泪…

分析

我们看一下题目,他大致的意思就是给一个数字n,通过1 ~ n的各种排列(假设n是5,那么就是1,2,3,4,5的各种排列)不断两两相加最终得到一个数,如果这个数与总和值相等,那么就返回这个排列,存在多个序列时返回字典序最小的序列。([2,1,3],[1,3,2],[3,1,2]中字典序最小的序列是[1,3,2])。

最容易想到的做法是按字典序由低到高不断穷举每一个序列,让每一个序列不断两两求和相加,最终得到一个数,让这个数与目标值比较看是否相等,若相等,则直接返回该序列,否则继续让下一个序列相加计算,直到遇到符合条件的序列为止。这种方法可以拿到70分,不想在这题浪费太多时间的同学,这么做足够了。

穷举每一个序列,一般就是使用DFS来寻找的。就我而言,我尝试过很多方法去优化上面的算法均已失败告终,(AC拿的真不容易啊!!)即使在遍历搜索过程中添加了如当序列两两相加的值大于目标值时,提前终止递归的条件,还是只能拿个70分。说明这条题还是得找出规律才能更深层次的优化此算法。

这道题跟杨辉三角有异曲同工之妙,都是一个数的值等于它左右肩膀值之和,如果看这道题例子的同时,结合杨辉三角一起看,各位就能找出其中的规律了。
蓝桥杯 ALGO-1005 数字游戏 DFS记忆化搜索+剪枝+杨辉三角 python_第1张图片
字看不清没关系,图看的清吧?看得清我就要说了。咳咳,ab图中的每一行对应元素相乘之和相等。 这条件相当good,有了它,不会再有中间商赚差价,直接一步到位。

思路

得知本题与杨辉三角有密不可分的关系后,我们就能通过杨辉三角来优化搜索算法了。

因为我们穷举是穷举最后一行序列,也就有n个元素,所以我们也要获取杨辉三角第n行的所有元素。因为n值不大,我们就可以直接通过一个数的值等于它肩膀两个数的值之和这个关系,不断一行一行的推出第n行的所有元素。

接着让每个长度为n的序列与该行对应元素相乘求和并与目标值比较,若相等,则打印序列并终止递归,否则继续搜索。到这里,经过优化后的算法效率显著提高了,将该思路的算法提交能拿90分,已经非常不错了。

还差10分,这10分差就差在没有剪枝。我们穷举序列不应该让长度为n的序列和杨辉三角相乘求和的结果与目标值s比较。而是应该在它生成的过程中就要与目标值s相比了,如果生成过程中的长度小于n的序列与杨辉三角相乘求和都已经大于目标值,那么完整序列必定大于目标值,可以提前终止递归。

代码

深度优先搜索就是按字典序由低到高的顺序开始试错寻找,当找到第一个符合条件的序列,即可直接返回,因为这个序列必定是字典序最小的序列。

# 杨辉三角
def triangle():
    # 存储杨辉三角的每行内容
    lines = [[1], [1, 1]]
    if n <= 2:
        # 当n小于等于2时,直接返回对应行,因为下标从0开始,所以是n-1
        return lines[n-1]
    # 从第三行开始遍历,因为已经有两个现成的,所以可以少遍历两行,故n-2
    for i in range(n - 2):
        temp = []
        # 遍历lines的最后一行以求取下一行的元素
        for j in range(len(lines[-1]) - 1):
            # 一个数的值等于它肩膀两个数的值之和
            val = lines[-1][j] + lines[-1][j + 1]
            temp.append(val)
        # 然后将特殊情况的两个1放进行里面
        lines.append([1] + temp + [1])
    # 返回杨辉三角最后一行,该行元素长度与序列元素长度相等
    return lines[-1]


# 深度优先搜索  temp保存着生成的序列  cnt表示生成的序列元素与杨辉三角对应元素相乘之和  ind表示最后一个序列的下标
def search(temp, cnt, ind):
    # 不满足条件提前终止条件,实现剪枝
    if cnt > s:
        return False
    if len(temp) == n:
        # 当长度为n且cnt与目标值相等时,打印该序列所有元素
        if cnt == s:
            for i in temp:
                print(i, end=" ")
            # 已找到元素返回True,不用继续递归搜索
            return True
        # 如果不相符则返回False继续寻找
        return False
    # 遍历基序列,i表示索引,j表示对应索引下的值
    for i, j in enumerate(nums):
        # 如果该数字已使用,也就是生成的序列中已存在该数字,则不执行下面语句
        # 如果没有使用该数字,则运行下面程序
        if not used[i]:
            # 将未使用的数字标记为已使用
            used[i] = True
            # 将该数字加入到生成序列中
            temp.append(j)
            # 计算生成序列这个位置的值与杨辉三角对应位置的值乘积之和
            val = temp[ind] * yanghui[ind]
            # 将其加入到总和中
            cnt += val
            # 移动到下一个元素位置
            ind += 1
            # 继续回溯添加数字到生成数列中,如果已找到符合条件的序列直接返回Ture结束本次递归
            if search(temp, cnt, ind):
                return True
            # 状态重置,这是深度优先搜索的一个特点,如果没有找到符合条件的序列,就要取消选择最后选择的数字以选取其他数字
            used[i] = False
            temp.pop()
            cnt -= val
            ind -= 1


n, s = map(int, input().split())
# nums是所有序列的根基,包含着序列中出现的所有元素
nums = [i for i in range(1, n + 1)]
# used为标记数组,标记数字是否使用过,used数组配合nums数组一起使用,用来标记第1~第n个数字是否使用过
used = [False for i in range(n)]
# 调用杨辉三角函数,获取最后一行所有元素
yanghui = triangle()
# 开始进行深度优先搜索
search([], 0, 0)


蓝桥杯 ALGO-1005 数字游戏 DFS记忆化搜索+剪枝+杨辉三角 python_第2张图片

你可能感兴趣的:(算法,蓝桥,深度优先,蓝桥杯)