优化二分查找:前缀和降复杂度

收集晨露

错误代码:

check(mid) 函数在每次二分查找时都循环求 mid 个元素的和,而 mid 最大可以是 n。

这样,一次 check 最坏是 O(n),一次二分查找复杂度 O(nlog n),q 次询问就是 O(nqlog n),必然超时(10^5 * 10^5 量级)。

python 复制代码
n, q = map(int, input().split())
a = list(map(int, input().split()))

def check(y):
    num = 0
    for i in range(y):
        num += a[i]
    if num >= x:
        return True
    else:
        return False

for _ in range(q):
    x = int(input())
    left, right = 1, n
    ans = 0
    flag = False
    while left <= right:
        mid = (left + right) // 2
        if check(mid):
            ans = mid
            flag = True
            right = mid - 1
        else:
            left = mid + 1
    print(ans if flag else -1)

实际上,需要多次求前缀和,可以用前缀和数组把 check 降到 O(1)。

正确代码如下:

python 复制代码
n, q = map(int, input().split())
a = list(map(int, input().split()))

prev = [0] * n
prev[0] = a[0]
for i in range(1, n):
    prev[i] = prev[i - 1] + a[i]

for _ in range(q):
    x = int(input())
    left, right = 0, n - 1
    ans = -1
    while left <= right:
        mid = (left + right) // 2
        if prev[mid] >= x:
            ans = mid + 1#因为索引从0开始,所以叶子数要+1
            right = mid - 1
        else:
            left = mid + 1
    print(ans)

智慧农场灌溉

代码如下:

python 复制代码
n, v = map(int, input().split())
w = list(map(int, input().split()))

def check(y):
    total = 0
    for x in w:
        total += min(x, y)
    if total <= v:
        return True
    else:
        return False

left, right = 1, 1000000000000
ans = 0
while left <= right:
    mid = (left + right) // 2
    if check(mid):
        ans = mid
        left = mid + 1
    else:
        right = mid - 1
print(ans)

博物馆奇妙之旅

代码如下:

python 复制代码
n, q = map(int, input().split())
a = list(map(int, input().split()))

prev = [0] * n
prev[0] = a[0]
for i in range(1, n):
    prev[i] = prev[i - 1] + a[i]
def get_prev(prev, l, r):
    if l == 0:
        return prev[r]
    else:
        return prev[r] - prev[l - 1]


for _ in range(q):
    l, t = map(int, input().split())
    l_act = l - 1
    left, right = l - 1, n - 1
    ans = -1
    while left <= right:
        mid = (left + right) // 2
        if get_prev(prev, l_act, mid) >= t:
            ans = mid + 1
            right = mid - 1
        else:
            left = mid + 1
    print(ans)

爱心图书室

代码如下:

python 复制代码
import sys
def solve():
    data = sys.stdin.buffer.read().split()#快速输入
    it = iter(data)
    n = int(next(it))
    q = int(next(it))
    a = [int(next(it)) for _ in range(n)]
    #前缀和
    prev = [0] * n
    prev[0] = a[0]
    for i in range(1, n):
        prev[i] = prev[i - 1] + a[i]

    for _ in range(q):
        m = int(next(it))
        left, right = 0, n - 1
        ans = 0
        while left <= right:
            mid = (left + right) // 2
            if prev[mid] <= m:
                ans = mid + 1
                left = mid + 1
            else:
                right = mid - 1
        print(ans)
if __name__ == '__main__':
    solve()

寻找完美假期

代码如下:

python 复制代码
import sys
def solve():
    data = sys.stdin.buffer.read().split()
    it = iter(data)
    n = int(next(it))
    a = int(next(it))
    b = int(next(it))
    t = [int(next(it)) for _ in range(n)]

    left, right = 0, 0
    ans = 0
    tot = 0
    #注意不能加上and right < n,当 right == n 时,说明已经处理完所有元素,但还可以继续移动 left
    #left 控制窗口左边界,必须遍历完所有可能的左边界
    #right 记录已经探索到的位置,可以提前到达末尾
    while left < n:
        # 扩大右边界直到和 >= a
        while right < n and tot < a:
            tot += t[right]
            right += 1
        # 如果找到了符合条件的窗口
        if tot >= a:
            current_tot = tot
            current_right = right
            # 探索所有可能的右边界
            #用 <= n 确保:统计最后一个窗口:当 current_right = n 时,还能进入循环统计
            while current_tot <= b and current_right <= n:
                ans += 1
                #不能省略该if条件,因为当 current_right == n 时,current_right[r] 是索引越界的
                if current_right < n:
                    current_tot += t[current_right]
                current_right += 1
        # 移动左指针
        tot -= t[left]
        left += 1

    print(ans)

if __name__ == '__main__':
    solve()

摄影师的完美相册

代码如下:

python 复制代码
import sys
def solve():
    data = sys.stdin.buffer.read().split()
    it = iter(data)
    n = int(next(it))
    q = int(next(it))
    p = [int(next(it)) for _ in range(n)]

    prev = [0] * n
    prev[0] = p[0]
    for i in range(1, n):
        prev[i] = prev[i - 1] + p[i]
    def get_sum(prev, a, b):
        if a == 0:
            return prev[b]
        else:
            return prev[b] - prev[a - 1]

    for _ in range(q):
        l = int(next(it))
        k = int(next(it))
        l_act = l - 1
        left, right = l_act, n - 1
        num = 0
        while left <= right:
            mid = (left + right) // 2
            if get_sum(prev, l_act, mid) <= k:
                num = mid + 1
                left = mid + 1
            else:
                right = mid - 1
        print(num - l_act if num - l_act > 0 else 0)
if __name__ == '__main__':
    solve()

快乐登山步数计

代码如下:

python 复制代码
import sys
def solve():
    data = sys.stdin.buffer.read().split()#快速输入
    it = iter(data)
    n = int(next(it))
    q = int(next(it))
    a = [int(next(it)) for _ in range(n)]
    #前缀和
    prev = [0] * n
    prev[0] = a[0]
    for i in range(1, n):
        prev[i] = prev[i - 1] + a[i]

    for _ in range(q):
        h = int(next(it))
        left, right = 0, n - 1
        ans = 0
        flag = False
        while left <= right:
            mid = (left + right) // 2
            if prev[mid] >= h:
                ans = mid + 1
                flag = True
                right = mid - 1
            else:
                left = mid + 1
        print(ans if flag else "Target Unreachable")
if __name__ == '__main__':
    solve()

城市最美花圃

纯暴力解法:(会超时)

python 复制代码
import sys

def solve():
    data = sys.stdin.buffer.read().split()
    it = iter(data)
    n = int(next(it))
    k = int(next(it))
    a = [int(next(it)) for _ in range(n)]

    # 前缀和
    prefix = [0] * n
    prefix[0] = a[0]
    for i in range(1, n):
        prefix[i] = prefix[i - 1] + a[i]
    
    max_avg = 0
    
    # 枚举所有可能的左端点
    for left in range(n):
        # 枚举所有可能的右端点(保证长度 >= k)
        for right in range(left + k - 1, n):
            if left == 0:
                total = prefix[right]
            else:
                total = prefix[right] - prefix[left - 1]
            length = right - left + 1
            avg = total // length  # 向下取整
            if avg > max_avg:
                max_avg = avg
    
    print(max_avg)

if __name__ == '__main__':
    solve()

正确代码如下:

python 复制代码
import sys

def solve():
    data = sys.stdin.buffer.read().split()
    it = iter(data)
    n = int(next(it))
    k = int(next(it))
    a = [int(next(it)) for _ in range(n)]

    def check(avg):
        # 检查是否存在长度 >= k 的子段,平均高度 >= avg
        # 将问题转化为:是否存在子段和 >= avg * 长度
        # 即 sum(a[i]) - avg * len >= 0
        # 令 b[i] = a[i] - avg
        # 找长度 >= k 的最大子段和是否 >= 0
        
        prefix = [0] * n
        prefix[0] = a[0] - avg
        for i in range(1, n):
            prefix[i] = prefix[i - 1] + (a[i] - avg)
        
        # 找长度 >= k 的最大子段和
        # 修改3:调整区间和的计算逻辑
        # 对于右端点i,左端点可以是 -1 到 i-k
        # 使用一个变量记录左端点的最小值
        min_prefix = 0  # 对应左端点为-1的情况(即从0开始)
        
        # 需要同时考虑左端点为-1的情况(即prefix[-1] = 0)
        # 所以对于右端点 i,考虑所有可能的左端点 j ∈ [-1, i-k]
        # 其中 prefix[-1] 视为 0
        
        for i in range(k - 1, n):#右端点
            # 更新可能的左端点最小值
            # 当 i = k-1 时,左端点可以是 -1
            # 当 i > k-1 时,左端点可以是 i-k
            if i > k - 1:
                min_prefix = min(min_prefix, prefix[i - k])
            
            # 检查以i为右端点的子段和是否 >= 0
            if prefix[i] - min_prefix >= 0:
                return True
        
        # 还需要检查一种特殊情况:从0开始的长度为k的子段
        # 这已经被上面的循环覆盖了,因为当 i = k-1 时,min_prefix=0,检查的就是 prefix[k-1] >= 0
        
        return False

    # 二分答案
    left, right = 1, max(a)
    ans = 0
    while left <= right:
        mid = (left + right) // 2
        if check(mid):
            ans = mid
            left = mid + 1
        else:
            right = mid - 1
    
    print(ans)

if __name__ == '__main__':
    solve()

为什么是prefix[i-k]?

当右端点是i时,新的可能的左端点是i-k+1,对应的left-1就是i-k,所以需要把这个新的前缀和纳入最小值候选

相关推荐
qyzm1 小时前
天梯赛练习(3月13日)
开发语言·数据结构·python·算法·贪心算法
Qt学视觉3 小时前
AI2-Paddle环境搭建
c++·人工智能·python·opencv·paddle
廋到被风吹走3 小时前
【LangChain4j】特点功能及使用场景
后端·python·flask
Eward-an3 小时前
LeetCode 239. 滑动窗口最大值(详细技术解析)
python·算法·leetcode
皙然4 小时前
彻底吃透红黑树
数据结构·算法
喵手4 小时前
Python爬虫实战:用代码守护地球,追踪WWF濒危物种保护动态!
爬虫·python·爬虫实战·濒危物种·零基础python爬虫教学·wwf·濒危物种保护动态追踪
梦想的旅途24 小时前
如何通过 QiWe API 实现企业微信主动发消息
开发语言·python
喵手4 小时前
Python爬虫实战:自动化抓取 Pinterest 热门趋势与创意!
爬虫·python·爬虫实战·pinterest·零基础python爬虫教学·采集pinterest热门趋势·热门趋势预测
凌晨一点的秃头猪4 小时前
Python文件操作
开发语言·python