【蓝桥杯Python-无聊的逗】解题思路和参考代码


前言

最近在准备蓝桥杯的过程中,遇到了一个题目。对于还是新手的我来说还是挺有难度的,我花了两天时间才比较清晰地解出来哈哈。这条题目还是涉及到了挺多内容的,我觉得这题不错,也想将解题思路保存下来,所以写此文章。解题的过程中参考了多篇优秀的文章,在此对其作者表示感谢。如果我没能清楚阐述出的地方,不妨读者亲自看看,必受益匪浅。


题目

问题描述:

逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的情况下长度最长是多少。

输入格式:

第一行一个数n,表示n个棍子。第二行n个数,每个数表示一根棍子的长度。

输出格式:

一个数,最大的长度。

样例输入:

4
1 2 3 1

扫描二维码关注公众号,回复: 14776510 查看本文章

样例输出:

3

数据规模和约定:

n<=15

刚开始遇到这个题目是真的没有思路。
要注意的是,在这个题目中,并不是所有的木棍都会用到,也就是说符合题意的是这些木棍的一个子集

解题思路

我在CSDN上看到了一位大佬的文章:蓝桥杯 无聊的逗 Python题解
其中的思路是:

LeetCode 78. 数列子集 + LeetCode 416. 分割等和子集
本题算是LeetCode 416的升级版吧,唯一的不同就是:不是考虑所有的数,而是可以有一些数不取。所以,我就想先求棍子数列的子集吧,再求子集的等和子集。

在上述文章中,提到了LeeCode的两道题。因为先前没有刷过这两题,所以我打算先把这两题找出来。

1.LeetCode 78

题目:LeetCode 78. 子集

大致题意是求出已知集合的所有子集。

可能确实是我实力不够哈哈,这道题我也没有思路。所以就进一步地去查找资料。
接着我就查到了下面这篇,文章思路清晰,非常受益!
Python算法——求集合的所有子集

在这篇文章中,大致有两种求已知集合子集的方式:

方法一:位图法

因为一个元素个数为n的集合,它的子集个数为 2 n 2^n 2n 。 此时假设存在一个n位2进制数,它的每一位对应原集合一个元素。对于每一位来说,为0时相当于该位对应的元素不在一个子集中,为1则在这个子集中。那么这个n位2进制数就可以表示原集合的各个元素在或不在一个子集中,从而表示这个子集。

如:有个集合为[0, 1, 2]
则2进制数011(3)即可表示子集[0, 1]。注意对应关系
由此思路可以写出代码:

def function_1 (input_list):
    n = len(input_list)
    for i in range(2 ** n): # 子集的个数,每次循环形成一个子集
        combo = []
        for j in range(n):
            if (i >> j) % 2:
                combo.append(input_list[j])
        print(combo)

方法二:迭代法

该方法的主要思路是先理清楚各个子集之间的迭代关系,再利用这个迭代关系来生成子集。

假设原始集合s = [1,2,3],
第一次迭代:<1>;
第二次迭代:<1,12,2>;
第三次迭代:<1,12,2,13,123,23,3>;
每次迭代,都是上一次迭代的结果+上次迭代结果中每个元素都加当前迭代元素+当前迭代元素

def function_2(input_list):
    combo = [[]]
    for i in input_list:
        combo = combo + [[i] + j for j in combo]
    for i in combo:
        print(i)

好,到目前为止,我们得到了原集合的所有子集,其中肯定包括了能够符合题意的子集。接着我们就需要判断的是,能够被划分成相等的部分的子集中即该子集中存在几个元素之和为该子集元素总和的一半,子集元素总和一半最大的是哪个子集。

2.LeetCode 416

题目:LeetCode 416. 分割等和子集
不出意料的,我还是没有思路。
于是我继续查找资料,并再次发现了宝藏文章:
动态规划之0-1背包问题(详解+分析+原码)
在该文章中,解释了分割等和子集其实就是背包问题的变型。
要想解决分割等和子集,则需要先学习背包问题。

背包问题

目前有N个价值和重量不同物品,并有容量为Room的背包。
求如何选择物品装入背包,能使背包中物品的总价值最大

如:该n个物品的价值分别为value = [1, 2, 3], 重量为wieght = [2, 3, 1]。背包的容量room为4。则当装入物品2和物品3时能够获得最大的价值总和5。
背包问题的实质是动态规划问题

我的理解是,动态规划问题的特点就是复杂问题的解决依赖于简单一级的问题即复杂问题可由简单问题推导解决。因此,解决复杂问题可由通过先解决简单问题,再通过推导关系推出复杂问题的解。

因此,我们可以先假设存在一个 (n+1)*(room+1) 的列表dp[i][j]+1是要考虑为0的情况
解决背包问题的关键在于理解这个列表横纵坐标的意义。

纵坐标i的意义是假设当前只考虑前 i 个物品。
横坐标j的意义是假设当前的背包容量为j。
dp[i][j]的值即是在考虑前 i 个物品且背包容量为 j 时,书包所能装下最大价值总和。

这样来看,大容量背包和多物品的问题,就变成了较小容量背包和较少物品的问题。
而我们要解决的问题实际上就变成了退出这个二维列表的各项值,最后一项值dp[n][room]就表示了我们所要求的。

动态规划问题英语:Dynamic programming,简称 DP的关键在以下三点:
1.定义状态
就像我上面所述的,建立一个列表,并确定列表各个坐标值的意义。

2.确定初始状态
在此背包问题中,初始状态就是当 i 或 j 为 0 时,显而易见背包装下的最大价值总和必为 0。在动态规划问题中,最初几项的值往往比较特殊,需要自行逻辑推理得出。至于如何确定一个动态规划问题有多少个初始状态,听说做多了相关题目就会有所经验。

3.推出状态转移方程
以这个背包问题为例,我们来推导状态转移方程:
设当前的坐标值为[i][j]。

如果是将第 i 个物品放入背包中,那么此时背包中的最大价值就相当于dp[i-1][j-weight[i]]+value[i]。理解这个式子的关键是,塞入第 i 个物品,背包容量变为j-weight[i],那么此时dp[i][j]就与dp[i-1][j-weight[i]]相关联了。

我们可以理解dp[i][j]dp[i-1][j]的区别是是否要将第 i 个物品放入背包中。
如果没有将第 i 个物品放入背包中,那么考虑前 i 个物品和考虑前 i-1 个物品实质上是一样的,即dp[i][j] = dp[i-1][j]

还有一种特殊情况是j < weight[i],那么无论如何第 i 件物品都不可能放入背包。

因此我们要确定第 i 间物品到底要不要放入背包中,就要对比dp[i-1][j-weight[i]]+value[i]dp[i][j] = dp[i-1][j]的大小关系,并取大者。
至此我们就得到了状态转移方程。

以下是dp表前几步的变化
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

[0, 0, 0, 0, 0]
[0, 0, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]

'''理解背包问题的关键在于理解建立的表格横纵坐标的意义'''
'''是指当有前i件物品可选且背包容量为j时,第i件物品是否要放入'''
'''关键是定义好初始状态,然后对比判断是否要放入第i个物品'''
n = int(input())    # 可选物品的个数
room = int(input()) # 背包的容量
weight_list = list(map(int, input().split()))   # 各个物品的重量
value_list = list(map(int, input().split()))    # 各个物品的价值

mid_map = [[0 for i in range(room+1)]for j in range(n+1)] # dp表

for i in range(1, n+1):
    for j in range(1, room+1):
        if weight_list[i-1] > j:
            mid_map[i][j] = mid_map[i-1][j]
        else:
            mid_map[i][j] = max(mid_map[i-1][j], mid_map[i-1][j-weight_list[i-1]]+value_list[i-1])   
print(mid_map[n][room])

至此,我们解决了背包问题。

分割等和子集

分割等和子集是背包问题的变型。

首先集合元素总和为偶数才存在分割。
求出集合元素的总和后,我们可以假设有一个背包的容量大小为该总和的一半,而各个元素看成价值等于重量的物品。
那么背包中的最大价值和必定小于或等于该总和的一半。如果存在几个元素的和为总和的一半,那么其他元素的和也为总和的一半。背包中的最大价值也为总和的一半。
利用与背包问题相同的解法求解,最后我们看dp的最后一项是否等于总和的一半便知道该集合是否能分割成等和子集了。

n = int(input())    # 集合元素的个数
input_list = list(map(int, input().split()))    # 待求的集合
m = sum(input_list) # 集合元素的总和
if m % 2 != 0:
    print('false')
    exit()

mid_map = [[0 for j in range(m//2+1)] for i in range(n+1)]  # dp表
for i in range(1, n+1):
    for j in range(1, m//2+1):
        if input_list[i-1] > j:
            mid_map[i][j] = mid_map[i-1][j]
        else:
            mid_map[i][j] = max(mid_map[i-1][j], mid_map[i-1][j-input_list[i-1]]+input_list[i-1])   
if mid_map[n][m//2] != m/2:
    print('false')
else:
    print('true')

至此,我们解决了分割等和子集的问题。

3.综合上述解题

就像最前面说的,无聊的逗这个问题的实质是求:

木棍各个能分割成等和子集的子集中,子集元素总和÷2的最大值是多少

可能听起来有点绕,可以结合代码理解。

pub_list = []

def DenHeZiJi(input_list):
    list_len = len(input_list)
    if sum(input_list) % 2 != 0:    # 不能少了这一步,不然会出现意外的错误
        return 0
    n = sum(input_list)//2
    
    mid_list = [[0 for j in range(n+1)] for i in range(list_len+1)]
    for i in range(1, list_len+1):
        for j in range(1, n+1):
            if input_list[i-1] > j:
                mid_list[i][j] = mid_list[i-1][j]
            else:
                mid_list[i][j] = max(mid_list[i-1][j], mid_list[i-1][j-input_list[i-1]]+input_list[i-1])
    if mid_list[list_len][n] != n:   # 该子集不能再划分为等和子集
        return 0
    else:
        return n   # 该子集能够划分为等和子集

    
def ZiJi(input_list):   # 求出结合的所有子集
    global pub_list
    n = len(input_list)
    combo = [[]]    # 储存所有子集的列表
    for i in input_list:
        combo = combo + [[i] + j for j in combo]
    for i in combo:
        pub_list.append(DenHeZiJi(i)) # 判定每个子集能否划分为等和子集,并储存该子集元素总和一半的值

N = int(input())
ori_list = list(map(int, input().split()))
ZiJi(ori_list)
print(max(pub_list))

至此,解决完所有问题。


总结

个人认为这道题具有一定难度,且涉及到多条经典题目的联合求解,值得单独拿出来记录,对于动态规划的理解很有帮助。也让我认识到了自己的水平有多么的不足,更应该努力备赛。

如果这篇文章对你有所帮助,请你点个赞哈哈。
如果你发现有哪里存在纰漏,也请不吝赐教。
感谢你的阅读。

猜你喜欢

转载自blog.csdn.net/weixin_52126342/article/details/128885547