Skip to content

M08210: 河中跳房子

binary search/greedy, http://cs101.openjudge.cn/practice/08210

每年奶牛们都要举办各种特殊版本的跳房子比赛,包括在河里从一个岩石跳到另一个岩石。这项激动人心的活动在一条长长的笔直河道中进行,在起点和离起点L远 (1 ≤ L≤ 1,000,000,000) 的终点处均有一个岩石。在起点和终点之间,有N (0 ≤ N ≤ 50,000) 个岩石,每个岩石与起点的距离分别为Di(0<Di<L)

在比赛过程中,奶牛轮流从起点出发,尝试到达终点,每一步只能从一个岩石跳到另一个岩石。当然,实力不济的奶牛是没有办法完成目标的。

农夫约翰为他的奶牛们感到自豪并且年年都观看了这项比赛。但随着时间的推移,看着其他农夫的胆小奶牛们在相距很近的岩石之间缓慢前行,他感到非常厌烦。他计划移走一些岩石,使得从起点到终点的过程中,最短的跳跃距离最长。他可以移走除起点和终点外的至多M (0 ≤ MN) 个岩石。

请帮助约翰确定移走这些岩石后,最长可能的最短跳跃距离是多少?

输入

第一行包含三个整数L, N, M,相邻两个整数之间用单个空格隔开。 接下来N行,每行一个整数,表示每个岩石与起点的距离。岩石按与起点距离从近到远给出,且不会有两个岩石出现在同一个位置。

输出

一个整数,最长可能的最短跳跃距离。

样例输入

25 5 2
2
11
14
17
21

样例输出

4

提示:在移除位于2和14的两个岩石之后,最短跳跃距离为4(从17到21或从21到25)。

python
def maxMinJump(L, N, M, rocks):
    # 先将岩石位置排序,并加入起点和终点
    rocks = [0] + rocks + [L]

    left, right = 0, L + 1  # 可能的最小跳跃距离范围。所以二分是在 [left, right) 区间内进行的

    def canAchieve(min_dist):
        """ 判断是否能移除至多 M 个岩石,使最短跳跃距离至少为 min_dist """
        removed = 0  # 记录移除的岩石数量
        prev = 0  # 记录上一个岩石位置(起点)

        for i in range(1, len(rocks)):
            if rocks[i] - rocks[prev] < min_dist:
                removed += 1
                if removed > M:
                    return False  # 不能满足要求
            else:
                prev = i  # 更新上一个岩石位置

        return True  # 可以满足要求

    # 二分查找最长可能的最短跳跃距离
    ans = -1
    while left < right:
        mid = (left + right) // 2  # 取中间偏右值
        if canAchieve(mid):
            ans = mid   # 记录可行的 `mid`
            left = mid + 1  # 继续尝试更大的值
        else:
            right = mid

    return ans


# 读取输入
L, N, M = map(int, input().split())
rocks = [int(input()) for _ in range(N)]

# 计算并输出答案
print(maxMinJump(L, N, M, rocks))

二分法思路参考:https://blog.csdn.net/gyxx1998/article/details/103831426

用两分法去推求最长可能的最短跳跃距离。 最初,待求结果的可能范围是[0,L]的全程区间,因此暂定取其半程(L/2),作为当前的最短跳跃距离,以这个标准进行岩石的筛选。 筛选过程是: 先以起点为基点,如果从基点到第1块岩石的距离小于这个最短跳跃距离,则移除第1块岩石,再看接下来那块岩石(原序号是第2块),如果还够不上最小跳跃距离,就继续移除。。。直至找到一块距离基点超过最小跳跃距离的岩石,保留这块岩石,并将它作为新的基点,再重复前面过程,逐一考察和移除在它之后的那些距离不足的岩石,直至找到下一个基点予以保留。。。 当这个筛选过程最终结束时,那些幸存下来的基点,彼此之间的距离肯定是大于当前设定的最短跳跃距离的。 这个时候要看一下被移除岩石的总数:

  • 如果总数>M,则说明被移除的岩石数量太多了(已超过上限值),进而说明当前设定的最小跳跃距离(即L/2)是过大的,其真实值应该是在[0, L/2]之间,故暂定这个区间的中值(L/4)作为接下来的最短跳跃距离,并以其为标准重新开始一次岩石筛选过程。。。
  • 如果总数≤M,则说明被移除的岩石数量并未超过上限值,进而说明当前设定的最小跳跃距离(即L/2)很可能过小,准确值应该是在[L/2, L]之间,故暂定这个区间的中值(3/4L)作为接下来的最短跳跃距离
python
L,n,m = map(int,input().split())
rock = [0]
for i in range(n):
    rock.append(int(input()))
rock.append(L)

def check(x):
    num = 0
    now = 0
    for i in range(1, n+2):
        if rock[i] - now < x:
            num += 1
        else:
            now = rock[i]
            
    if num > m:
        return True
    else:
        return False

# https://github.com/python/cpython/blob/main/Lib/bisect.py
'''
2022fall-cs101,刘子鹏,元培。
源码的二分查找逻辑是给定一个可行的下界和不可行的上界,通过二分查找,将范围缩小同时保持下界可行而区间内上界不符合,
但这种最后print(lo-1)的写法的基础是最后夹出来一个不可行的上界,但其实L在这种情况下有可能是可行的
(考虑所有可以移除所有岩石的情况),所以我觉得应该将上界修改为不可能的 L+1 的逻辑才是正确。
例如:
25 5 5
1
2
3
4
5

应该输出 25
'''
# lo, hi = 0, L
lo, hi = 0, L+1	# 二分是在 [left, right) 区间内进行的
ans = -1
while lo < hi:
    mid = (lo + hi) // 2
    
    if check(mid):
        hi = mid
    else:               
        ans = mid      # 记录可行的 `mid`
        lo = mid + 1	# 继续尝试更大的值
        
#print(lo-1)
print(ans)