题目描述与示例
题目描述
攀登者喜欢寻找各种地图,并且尝试攀登到最高的山峰。
地图表示为一维数组,数组的索引代表水平位置,数组的高度代表相对海拔高度。其中数组元素 0
代表地面。
例如[0,1,2,4,3,1,0,0,1,2,3,1,2,1,0]
, 代表如下图所示的地图。,地图中有两个山脉位置分别为 1,2,3,4,5
和8,9,10,11,12,13
,最高峰高度分别为4,3
。最高峰位置分别为3,10
。
一个山脉可能有多座山峰(高度大于相邻位置的高度,或在地图边界且高度大于相邻的高度)。
4
+---+
| |
| | 3 3
| |
| +---+ -----
| | | |
2 | | 2 | | 2
| | | |
+---+ | ----+ | +---+
| | | | | |
1 | | 1 1 | | 1 | | 1
| | | | | |
+---+ +---+ +---+ +---+ +---+
| | | |
0 | | 0 0 | | 0
| | | |
+---+ +-------+ +---+
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
登山时会消耗登山者的体力(整数),上坡时,消耗相邻高度差两倍的体力,下坡时消耗相邻高度差一倍的体力,平地不消耗体力,登山者体力消耗到零时会有生命危险。
例如,上图所示的山峰:从索引 0
,走到索引 1
,高度差为 1
,需要消耗 2*1=2
的体力;从索引 2
高度 2
走到高度 4
索引 3
需要消耗 2*2=4
的体力;从索引 3
走到索引 4
则消耗 1*1=1
的体力。
攀登者想要评估一张地图内有多少座山峰可以进行攀登,且可以安全返回到地面,且无生命危险。
例如上图中的教组,有3
个不同的山峰,登上位置在3
的山可以从位置0
或者位置6
开始,从位置0
登到山顶需要消耗体力1*2+1*2+2*2=8
,从山顶返回到地面0
需要消耗体力 2*1+1*1+1*1=4
的体力,按照登山路线0->3->0
需要消耗体力 12
。攀登者至少需要12
以上的体力(大于12
)才能安全返回。
输入描述
第一行输入 一个长度为N
的数组,表示地图。
第二行输入最大体力。
输出描述
输出一个数字,地图中可以攀登到达的山峰数量
示例
输入
0,1,4,3,1,0,0,1,2,3,1,2,1,0
11
输出
2
解题思路
和题目7-python华为原题题库-攀登者的区别在于,本题除了需要查看峰值的位置,还需要查看这个峰值是否能够在平地位置攀登过去。
本题需要严格地区分上/下山和上/下坡的区别。在后面的题解中:
- 上/下山表示从平地到达某个山峰或反之
- 上/下坡表示从低海拔位置走到高海拔位置或反之
原路返回和非原路返回
攀登山峰的方式,一共可以分为四种情况
- 从左边上山,往左边下山
- 从右边上山,往右边下山
- 从左边上山,往右边下山
- 从右边上山,从左边下山
其中前两者可以统称为原路返回的攀登方式,后两者可以统称为非原路返回的攀登方式。
在本题中,只需要考虑原路返回的攀登方式,无需考虑非原路返回的攀登方式。
因为两条原路返回路径所花费的体力,一定有一条是小于另外两条非原路返回的攀登方式。
(贪心思想不要尝试在考试的时候证明正确性,相信数学直觉)
原路返回走过的总路程
考虑原路返回的攀登方式,这种登山方式,上坡和下坡走过的总路程是一样多的。也是最方便我们计算的。
以题目所给例子为例,如果我们想从索引7
的空地位置向右攀登来到索引12
的高度为2
的山峰并且原路返回到原来的空地,来回的路线分别用红色和绿色标记出来,会发现上山和下山所走过的总路程其实是一样多的。这个结论对于任何一个山峰、任何一个方向都成立。
因此,这里花费的总体力为相邻元素高度差的绝对值的总和乘以3
。
其中,相邻元素高度差的绝对值的总和表示走过的总路程,而之所以系数为3
是因为上坡和下坡各自要花费2
倍和1
倍高度差的体力。
所以,对于每一个山峰,我们都可以很容易地计算出从左边的某处空地出发,向右攀登到达这个山峰并且沿原路返回到达空地所需要花费的体力。
如果这个这个体力超过了所给的初始体力,无法到达该山峰。
从左边空地出发原路返回
那么应该如何计算从从左边某处空地出发向右攀登(后面原路返回),能够攀登到的山峰数量?
寻找空地
我们需要找到若干空地作为出发点,这个空地但必须是上山前的最后一个空地。如
[0, 1, 2, 4, 3, 1, 0, 0, 1, 2, 3, 1, 2, 1, 0]
⬆ ⬆
可以通过以下代码实现
space_list = list()
for i in range(n-1):
if nums[i] == 0 and nums[i+1] > 0:
space_list.append(i)
向右登山
已知最大体力值为power_max
,设在攀登过程中所累积的高度差为route_num
,对应花费的体力为route_num * 3
。
从某一个空地start
出发,在一个while
循环中不断向右移动,同时更新所经历的高度差route_num
。若
route_num * 3 > power_max
,说明会在攀登过程中体力降为0
,无法到达i
位置,退出循环- 找到了一个山峰,则将位置
i
记录在数组ans
中- 注意山峰需要区分边界
i == n-1
和非边界情况i != n-1
- 注意山峰需要区分边界
for start in space_list:
i = start + 1
route_num = 0
while i < n and nums[i] != 0:
route_num += abs(nums[i] - nums[i-1])
if route_num * 3 > power_max:
break
if i == n-1 and nums[i] > nums[i-1]:
ans.append(i)
elif nums[i] > nums[i-1] and nums[i] > nums[i+1]:
ans.append(i)
i += 1
构建函数
上述两部分合并在一起,可以构建出函数cal_peak_from_left(nums, power_max,, n)
,具体实现如下
def cal_peak_from_left(nums, power_max, n):
ans = list()
space_list = list()
for i in range(n-1):
if nums[i] == 0 and nums[i+1] > 0:
space_list.append(i)
for start in space_list:
i = start + 1
route_num = 0
while i < n and nums[i] != 0:
route_num += abs(nums[i] - nums[i-1])
if route_num * 3 > power_max:
break
if i == n-1 and nums[i] > nums[i-1]:
ans.append(i)
elif nums[i] > nums[i-1] and nums[i] > nums[i+1]:
ans.append(i)
i += 1
return ans
其中返回的列表ans
,为从左边空地出发并原路返回,能够攀登到的所有山峰的索引。可以做如下调用
ans_from_left = cal_peak_from_left(heights, power_max, n)
从右边空地出发原路返回
对于是从右边的某处空地出发,向左攀登到某个山峰并沿原路返回到达空地的情况,也存在上述类似的结论。
很容易想到,整个过程是类似的,只不过整个方向反过来了。
一种方法是,可以构建出一个完全类似的函数cal_peak_from_right(nums, power_max, n)
但一种更加简便、代码复用性更强的做法是,我们可以将从右边空地出发向左攀登,看作是对原高度数组进行反转之后,即对heights[::-1]
进行从左边空地出发向右攀登得到的结果。即做如下调用
ans_from_right = cal_peak_from_left(heights[::-1], power_max, n)
由于使用了反转,得到计算得到的索引也是反转的,需要进一步处理才能得到原heights
数组中的索引,即
ans_from_right = [n-i-1 for i in ans_from_right]
最终取ans_from_left
和ans_from_right
的交集的长度,即为所有能够攀登到的山峰(不管从左出发还是从右出发原路返回)的索引了。再取并集的长度即为答案。
ans = len(set(ans_from_left + ans_from_right))
*为什么一定是原路返回(严谨证明)
可能有同学就会提出疑问,为什么只考虑原路返回的情况。
这其实是涉及到了贪心思想。
贪心不问出处,问就是数学直觉。
假设到达某个山峰,从左边上山、从右边上山、从左边下山、从右边下山各自花费的体力为A, B, C, D
四种方式所对应的体力值分别为
左上左下 = A + C
左上右下 = A + D
右上右下 = B + D
右上左下 = B + C
每段花费的体力都可以分割为两部分,一部分是上坡、一部分是下坡,分别用下标1
和2
来表示。那么存在以下的若干式子成立
A = A1 + A2
B = B1 + B2
C = C1 + C2
D = D1 + D2
其中A2, B2, C2, D2
不仅表示下坡花费的体力,也同时表示高度
A2
表示从左边上山经历的下坡高度B2
表示从右边上山经历的下坡高度C2
表示往左边下山经历的下坡高度D2
表示往右边下山经历的下坡高度
由于最终的山峰高度一致,因此存在以下式子成立
C2 - A2 = D2 - B2 > 0
即
C2 + B2 = A2 + D2
如下图所示
另外,上坡和下坡的体力消耗也存在倍数关系。譬如,从左边上山的上坡体力消耗A1
,是往左边下山的下坡体力消耗C2
的两倍。因此存在以下关系
A1 = 2 * C2
C1 = 2 * A2
B1 = 2 * D2
D1 = 2 * B2
代入上述式子可以得到
A = 2 * C2 + A2
B = 2 * D2 + B2
C = 2 * A2 + C2
D = 2 * B2 + D2
进一步可以得到
左上左下 = A + C = (2 * C2 + A2) + (2 * A2 + C2) = 3 * (A2 + C2)
左上右下 = A + D = (2 * C2 + A2) + (2 * B2 + D2) = 2 * (B2 + C2) + (A2 + D2)
右上右下 = B + D = (2 * D2 + B2) + (2 * B2 + D2) = 3 * (B2 + D2)
右上左下 = B + C = (2 * D2 + B2) + (2 * A2 + C2) = 2 * (A2 + D2) + (B2 + C2)
将以下式子带入
C2 + B2 = A2 + D2
可以得到
左上左下 = A + C = (2 * C2 + A2) + (2 * A2 + C2) = 3 * (A2 + C2)
左上右下 = A + D = (2 * C2 + A2) + (2 * B2 + D2) = 3 * (B2 + C2)
右上右下 = B + D = (2 * D2 + B2) + (2 * B2 + D2) = 3 * (B2 + D2)
右上左下 = B + C = (2 * D2 + B2) + (2 * A2 + C2) = 3 * (A2 + D2)
上述方程是非常对称的。又因为
C2 - A2 = D2 - B2 > 0
不妨设C2 >= D2 > A2 >= B2
成立(设D2 >= C2 > B2 >= A2
也是一样的,会得到(A2 + C2)
是最小值)
很容易看出,以下四组数中
(A2 + C2)
(B2 + C2)
(B2 + D2)
(A2 + D2)
一定存在(B2 + D2)
是最小值,这里对应着右上右下原路返回的情况。
我们只需要考虑到达山峰的最小情况即可。
代码
python
import sys
hill_map = [0, 1, 4, 3, 1, 0, 0, 1, 2, 3, 1, 2, 1, 0]
energy = 11
if len(hill_map) <= 1:
print(0)
exit(0)
peak_index_list = []
peak_count = 0
zt = ''
for i in range(len(hill_map) - 1):
if hill_map[i] < hill_map[i + 1]:
zt = 'up'
elif hill_map[i] > hill_map[i + 1]:
if zt == 'up' or zt == '':
peak_index_list.append(i)
zt = 'down'
if zt == 'up':
peak_index_list.append(len(sg) - 1)
def find_left_right(dirt, tmp_index):
tmp_high = hill_map[i]
way_num = 0
while True:
tmp_index += dirt
if 0 <= tmp_index < len(hill_map):
way_num += abs(tmp_high - hill_map[tmp_index])
tmp_high = hill_map[tmp_index]
if hill_map[tmp_index] == 0:
return way_num
else:
return sys.maxsize
# print(peak_index_list)
for i in peak_index_list:
left = find_left_right(-1, i)
right = find_left_right(1, i)
# print(left, right)
if 3 * min(left, right) < energy:
peak_count += 1
print(peak_count)