蓝桥杯真题(区间问题)
区间问题
青蛙过河
因为如果跳跃能力越强,就越有可能完成2x次来回,如果跳跃能力越弱,就越不可能完成2x次来回。因此首先马上可以想到二分。
难的是check函数。首先想一个必要条件,如果check(y)返回True,那么对于所有以i开始长度为y的区间,区间中所有数之和必然不少于2x。可以想象,因为最多跨y个长度,因此来回肯定都要经过这个区间,这个区间肯定要能被踩至少2x次。这个条件也是充分条件,因为如果所有长度为y的区间都能至少踩2x次,那么肯定能够完成2x次来回。
二分时l为0,表示无法跳跃,r为河的宽度,表示不需要借助石头也可以到对岸。
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
alloc = lambda *s: len(s) != 1 and [alloc(*s[1:]) for i in range(int(s[0]) + 2)] or [0] * int(s[0] + 2)
width, x = read()
n = width - 1
arr = [0] + read()
s = [0] * (n + 1)
for i in range(1, n + 1): s[i] = s[i - 1] + arr[i]
check = lambda y : all(s[i] - s[i - y] >= 2 * x for i in range(y, n + 1))
l = 0; r = width
while l < r:
mid = l + r >> 1
if check(mid): r = mid
else: l = mid + 1
print(l)
最少刷题数
看起来要先排序然后用二分。
一种做法是对每个学生二分找到他至少还要刷多少题,但是有可能超时。
所以要先二分找到一个中位数,然后根据这个中位数得到每个学生还要刷多少题。
但是因为可能有重复的元素,所以不能用简单的中位数。
需要通过二分来找到一个最小的数,使得大于这个数的元素数不超过小于这个数的元素数。
这句话中加粗了两个词,最小也就是要用l + r >> 1(下取整),不超过也就是要在greater > lesser时令l = mid + 1,从区间[l, r]中排除掉mid。
这里import了一个python库:bisect,可以记一下以下模板:
# arr是排好序的列表
n = len(arr)
greater = n - bisect.bisect_right(arr, x) # arr中有多少个数严格大于x
lesser = bisect.bisect_left(arr, x) # arr中有多少个数严格小于x
我们通过二分找到了一个数m。
-
如果一个学生的刷题数x大于等于m,则根据我们二分的条件,他已经不用刷题了。
-
如果一个学生的刷题数x小于m,则需要提高刷题数,因为m已经是最小的满足要求的刷题数了。
还要至少刷到多少题呢?m是个临界点,如果大于m(至少为m+1),则显然满足条件,即至少还需要刷m + 1 - x道题。
那能不能刚好刷到m道题呢,毕竟小于m肯定不符合要求。
根据我们的二分条件,greater <= lesser,如果有greater < lesser,那么有 greater <= lesser - 1,因此这种情况下是可以刚好刷到m道题的,因为刷到m题后,比他小的元素数刚好就是lesser - 1。
因此,如果 greater == lesser,则还需要至少刷m + 1 - x道题,如果greater < lesser,则还需要至少刷m - x道题。
import sys; readline = sys.stdin.readline
import bisect
read = lambda: [int(x) for x in readline().split()]
n, = read()
arr = read()
ori = arr[:]
arr.sort()
l = 0; r = max(arr)
while l < r:
mid = l + r >> 1
greater = n - bisect.bisect_right(arr, mid)
lesser = bisect.bisect_left(arr, mid)
if greater > lesser: l = mid + 1
else: r = mid
greater = n - bisect.bisect_right(arr, l)
lesser = bisect.bisect_left(arr, l)
t = 1 if greater == lesser else 0
ans = [0 if x >= l else l + t - x for x in ori]
print(' '.join(str(x) for x in ans))
最优清零方案
可以看出每个操作的先后顺序是不影响结果的。
因此可以先考虑进行连续K个数减去1的操作,直到不能进行这种操作时,再把所有数加起来即为总操作数。
先看最多可以进行多少次区间操作。因为区间操作肯定优于单点操作。
如果可以对区间[l, r]
的每个数减去1,则该区间的最小值必须大于0,假设这个最小值为minv
,则最多可以进行minv次区间操作,使得最小值变为0。假设变为0的位置为pivot
, l <= pivot <= r
,那么我们接下来只能在(0, pivot)
和(pivot, N)
这两个区间中进行连续K个数的修改。
这里运用贪心,让每次操作时pivot的值尽可能小。所以我们确定了要从左往右扫描区间,使用长度为K的滑动窗口,每次查询窗口内的最小值minv,给窗口内的每个数减去minv,并给ans(总操作次数)加上minv。
一开始想的是用线段树,但是一下就超时了。
这题的优化非常巧妙。因为进行操作后[l, pivot)
和(pivot, r]
肯定不能进行区间操作了,所以我们可以直接将滑动窗口的左端移动到pivot + 1
的位置。实际中[l, r]
中可能有多个0,我们取最后一个0的位置作为pivot。
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
N, K = read()
arr = [0] + read()
i = 1
ans = 0
while i <= N:
if i + K - 1 <= N:
minv = min(arr[i:i+K])
if minv != 0:
ans += minv
for t in range(i, i+K): arr[t] -= minv
for t in range(i, i+K): # 关键优化
if arr[t] == 0: i = t
i += 1
ans += sum(arr)
print(ans)
左移右移
L x操作相当于把x的下标改成负无穷。R x操作相当于把x的下标改成正无穷。
我们需要记录当前的负无穷和正无穷。
初始化一个x到其下标的映射,每次操作相当于重新映射。
最后使用下标来排序即可。
import sys; readline = sys.stdin.readline; from functools import reduce
read = lambda: [int(x) for x in readline().split()]
N, M = read()
remap = {i + 1:i for i in range(N)}
l_idx = -1
r_idx = N + 1
arr = list(range(1, N + 1))
for _ in range(M):
op, x = readline().split()
x = int(x)
if op == 'L':
remap[x] = l_idx; l_idx -= 1
else:
remap[x] = r_idx; r_idx += 1
ans = sorted(list(range(1, N + 1)), key=lambda x : remap[x])
print(' '.join(str(x) for x in ans))
整数拼接
先想个暴力,如下:
read = lambda: [int(x) for x in input().split()]
n, K = read()
arr = read()
ans = 0
for i in range(n):
for j in range(n):
if i == j: continue
if int(str(arr[j]) + str(arr[i])) % K == 0: ans += 1
print(ans)
会超时。考虑优化。一个拼接的数ajai可以表示为 aj * 10**k + ai,注意如果固定ai,k也是固定的。
(aj * 10**k + ai) % K == 0
则 (aj * 10**k) % K == (-ai) % K
因为1 <= Ai <= 10**9
,所以k可以从1到10中取,设t = (aj * 10**k) % K,可以预处理一下t的计数,这样只需要查询有多少个数对应的t 为 (-ai) % K即可。
注意,预处理计数时会把ai也计入,因此要特判一下ai对应的t是不是也为(-ai) % K,如果是就减一,避免多计入。
read = lambda: [int(x) for x in input().split()]
n, K = read()
arr = read()
f = [[0] * (100001) for _ in range(11)]
for i in range(1, 11):
for a in arr:
t = a * (10 ** i) % K
f[i][t] += 1
ans = 0
for a in arr:
k = len(str(a))
t = -a % K
ans += f[k][t]
if t == a * (10 ** k) % K: ans -= 1
print(ans)
和与乘积
解题思路:
这题很容易想到用两层循环,暴力求解。但是因为n最大取到2e5,所以会超时。
所以考虑能不能优化一重循环。这里从乘积的增长速度比较快入手,当乘积已经大于所有数的和时,可以直接退出循环。当所有数都大于等于2时,时间复杂度可以降低O(nlogn)。但是如果有很多1,则还是会超时。
我们可以把所有的1挑出来,单独存储,这样剩下的数都大于等于2了,可以实现优化。
用一个new_arr存储大于等于2的数,用one_cnt存储new_arr中每个数前面有多少个1。
当预处理前缀和时,需要考虑one_cnt;当预处理前缀积时,只需要考虑new_arr。
因为我们要把1用上,当区间的和sum不等于区间的积prod时,可以尝试把区间左右两边的1加到sum中,同时不影响prod。
心得:看起来是两重循环,实际上时间复杂度也可以降到O(nlogn)
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
alloc = lambda *s: len(s) != 1 and [alloc(*s[1:]) for i in range(int(s[0]) + 2)] or [0] * int(s[0] + 2)
bet = lambda a, b : a <= b and range(a, b + 1) or range(a, b - 1, -1)
n, = read()
arr = read()
tot = sum(arr)
one_cnt = alloc(n)
new_arr = alloc(n)
idx = 1
for x in arr:
if x == 1:
one_cnt[idx] += 1
else:
new_arr[idx] = x; idx += 1
n = idx - 1
presum = alloc(n)
preprod = alloc(n); preprod[0] = 1
for i in range(1, n + 1):
presum[i] = presum[i - 1] + new_arr[i] + one_cnt[i]
preprod[i] = preprod[i - 1] * new_arr[i]
ans = len(arr)
for i in range(1, n + 1):
for j in range(i + 1, n + 1):
p = preprod[j] // preprod[i - 1]
if p > tot: break
s = presum[j] - presum[i - 1] - one_cnt[i]
if s == p: ans += 1
else:
d = p - s # 用左右两边的1来补s
if d > 0 and one_cnt[i] + one_cnt[j + 1] >= d:
ans += min(d, one_cnt[i]) + min(d, one_cnt[j + 1]) - d + 1
print(ans)
k倍区间
这题的正规做法是用前缀和。用cnt[x]
表示模K余数为x的所有前缀和的个数。
因为所有以i结尾的后缀的和可以表示为s[i] - s[j]
,如果[i,j]
是K倍区间,则有 (s[i] - s[j]) % K == 0
,则有s[i] % K == s[j] % K
,因此,以i结尾的K倍区间数即为cnt[s[i] % K]
我用的是另一种方法,用cnt[i]
表示当前所有模K余数为i的所有后缀的个数,当考虑添加一个数x时,需要先对所有数进行转移:cnt[(i + x) % K] = cnt[i % K]
,这步转移完全可以用一个offset来表示,从而优化掉一重循环。
# 前缀和
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
alloc = lambda *s: len(s) != 1 and [alloc(*s[1:]) for i in range(int(s[0]) + 2)] or [0] * int(s[0] + 2)
N, K = read()
arr = [read()[0] for _ in range(N)]
ans = 0
cnt = alloc(K)
cnt[0] = 1
s = 0
for x in arr:
s += x
ans += cnt[s % K]
cnt[s % K] += 1
print(ans)
# 滚动区间
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
alloc = lambda *s: len(s) != 1 and [alloc(*s[1:]) for i in range(int(s[0]) + 2)] or [0] * int(s[0] + 2)
N, K = read()
arr = [read()[0] for _ in range(N)]
ans = 0
cnt = alloc(K)
# mods[i] 表示模K余数为i的所有后缀的个数
# 多考虑一个数x即为把区间向右滚动x,我们记录一个offset来寻找原始位置
offset = 0
for x in arr:
ans += cnt[(-x - offset) % K]
if x % K == 0: ans += 1
offset = (offset + x) % K
# 用offset优化掉了如下循环
## tcnt = cnt[:]
## for i in range(0, K):
## tcnt[(i + x) % K] = cnt[i]
## cnt = tcnt
cnt[(x - offset) % K] += 1
print(ans)
双向排序
降序排列可以称为前缀操作,升序排列可以称为后缀操作。
这题需要先观察操作的性质:如果连续出现相同类型的操作,则只需要保留长度最大的操作。
因此有效的操作应该是左右交替的,并且因为初始状态为升序,所以第一个有效操作为前缀操作。
考虑所有的前缀操作,如果出现了比之前长度更大的前缀操作,那么之前的长度更小的前缀操作以及紧随其后的后缀操作都将没有意义,可以直接去掉。
因此,去掉所有无意义的操作后,对于一次前缀操作未影响到的区间,之后都不会再被前缀操作影响到,可以直接确定结果。后缀操作同理。
import sys; readline = sys.stdin.readline
read = lambda: [int(x) for x in readline().split()]
alloc = lambda *s: len(s) != 1 and [alloc(*s[1:]) for i in range(int(s[0]) + 2)] or [0] * int(s[0] + 2)
n, m = read()
stk = [0] * (m + 1); top = 0
for i in range(m):
op, x = read()
if op == 0:
while top > 0 and stk[top][0] == 0: x = max(x, stk[top][1]); top -= 1
while top >= 2 and stk[top - 1][1] <= x: top -= 2
top += 1; stk[top] = (op, x)
elif top > 0:
while top > 0 and stk[top][0] == 1: x = min(x, stk[top][1]); top -= 1
while top >= 2 and stk[top - 1][1] >= x: top -= 2
top += 1; stk[top] = (op, x)
ans = [0] * (n + 1)
l = 1; r = n; t = n
for i in range(1, top + 1):
op, x = stk[i]
if op == 0:
while l <= r and r > x: ans[r] = t; r -= 1; t -= 1
else:
while l <= r and l < x: ans[l] = t; l += 1; t -= 1
if top % 2:
while l <= r: ans[l] = t; l += 1; t -= 1
else:
while l <= r: ans[r] = t; r -= 1; t -= 1
print(' '.join(map(str, ans[1:])))