大厂真题 / 百度
百度 2026-7-30 笔试真题 - 算法岗
本场考试概述
考试时间:2026年7月30日
考试岗位:算法岗
难度评级:中等
考点分析:
- 第一题:括号深度与线性扫描(难度简单)
- 第二题:前缀余数与双指针(难度中等)
- 第三题:乘积贪心、二分答案与快速幂(难度中等偏难)
建议策略:
- 第一题不要逐轮删除括号;先求整串最大深度,再把每个字符的深度直接换算成消失轮次。
- 第二题的关键是消去与起点有关的常数,把“轨迹余数互异”转化为前缀余数窗口内无重复。
- 第三题先证明每次递减当前最大值的乘积损失最小,再二分最终被削平的水平线,避免按操作次数模拟。
第 1 题:消失的括号层
题目描述
给定一个长度为 $n$ 的合法括号串 $s$。括号串按轮次逐渐消失:每一轮中,当前串里深度最大的所有括号对同时删除;剩余字符保持原有相对顺序,并继续下一轮。
一个括号对的深度,等于包围它的括号对数量再加 $1$。请对原串中的每个字符,输出该字符在第几轮消失。同一对左右括号的答案相同。
输入包含 $T$ 组测试数据,其中 $1\le T\le 10^4$。每组先给出括号串长度 $n$,满足 $2\le n\le 2\times 10^5$;再给出长度为 $n$、仅由 ( 和 ) 构成的合法括号串。保证 $n$ 为偶数,且单个测试文件中所有测试数据的 $n$ 之和不超过 $2\times 10^5$。
样例
输入
3
6
(()())
6
((()))
10
(()((())))
输出
2 1 1 1 1 2
3 2 1 1 2 3
4 3 3 3 2 1 1 2 3 4
第一组 (()()) 的两个内层括号对在第 $1$ 轮同时删除,最外层括号对随后在第 $2$ 轮删除。
思路分析
第一步:一次扫描求每个字符的原始深度
扫描括号串并维护当前深度 current_depth:
- 遇到左括号时,先令深度加 $1$,再记录该字符深度;
- 遇到右括号时,先记录当前深度,再令深度减 $1$。
这样一对匹配括号会记录到同一个深度。扫描过程中同时求出整串最大深度 $D$。
用公式表示,第 $i$ 个字符的深度为
\[depth[i]= \begin{cases} current\_depth+1, & s[i]=\texttt{(},\\ current\_depth, & s[i]=\texttt{)}. \end{cases}\]第二步:理解删除一层后的变化
第一轮删除所有深度为 $D$ 的括号对。删除它们不会改变更浅括号对之间的包含关系:一个深度较小的括号对只被它外侧的括号包围,而删除的是它内侧、更深的括号。
只要仍有括号存在,深度大于 $1$ 的括号对必然被上一层括号包围。因此删除当前最深层后,最大深度恰好从 $D$ 降为 $D-1$,不会跨过某一层。
第三步:把深度直接换成轮次
深度为 $D$ 的字符第 $1$ 轮消失;深度为 $D-1$ 的字符第 $2$ 轮消失。一般地,深度为 $d$ 的字符消失轮次为
\[D-d+1.\]因此无需真正执行任何一轮删除。求完深度后,再线性换算答案即可。
题解代码
import sys
input = sys.stdin.readline
def vanish_rounds(brackets):
depths = []
current_depth = 0
max_depth = 0
for char in brackets:
if char == '(':
current_depth += 1
depth = current_depth
else:
depth = current_depth
current_depth -= 1
depths.append(depth)
max_depth = max(max_depth, depth)
return [max_depth - depth + 1 for depth in depths]
def solve():
test_count = int(input())
output = []
for _ in range(test_count):
n = int(input())
brackets = input().strip()
answer = vanish_rounds(brackets[:n])
output.append(' '.join(map(str, answer)))
print('\n'.join(output))
solve()
复杂度分析
设当前测试的括号串长度为 $n$。
时间复杂度:$O(n)$,扫描求深度和换算轮次各进行一次。
空间复杂度:$O(n)$,用于保存每个字符的深度和答案。
第 2 题:余数游走
题目描述
给定长度为 $n$ 的整数序列 $a_1,a_2,\ldots,a_n$ 和正整数模数 $M$。序列首尾相接,视为一个环。
对于起点 $s$,从 $a_s$ 开始沿环依次取数,最多取 $n$ 个。走出第 $t$ 步后的余数轨迹值定义为
\[S_t=\left(\sum_{i=0}^{t-1}a_{(s+i-1)\bmod n+1}\right)\bmod M,\qquad t\ge 1.\]余数采用标准非负表示,落在 $[0,M-1]$。轨迹不包含尚未取元素时的初始余数 $0$。
只要下一步得到的余数与轨迹中已有余数重复,游走就停止;若一直没有重复,则取满 $n$ 步后停止。记从起点 $s$ 出发能取到的最大步数为 $L_s$,求
\[\sum_{s=1}^{n} L_s.\]输入包含 $T$ 组测试数据,其中 $1\le T\le 10^5$。每组输入满足 $1\le n,M\le 2\times 10^5$,随后给出 $n$ 个整数,且 $-10^9\le a_i\le 10^9$。保证所有测试数据的 $n$ 之和不超过 $2\times 10^5$。
样例
输入
2
5 3
1 2 2 1 2
4 5
5 0 5 0
输出
11
4
第二组的每个元素对 $5$ 都等价于 $0$。任一起点走第一步得到余数 $0$,第二步仍得到 $0$,所以每个起点的游走长度都是 $1$,总和为 $4$。
思路分析
第一步:破环成链
把数组在逻辑上复制一遍。由于起点只有 $n$ 个,且每个起点最多走 $n$ 步,只需处理覆盖所有这些环形区间的 $2n-1$ 个元素。
定义前缀余数
\[P_0=0,\qquad P_i=(P_{i-1}+a_{(i-1)\bmod n+1})\bmod M.\]代码计算 $P_0,P_1,\ldots,P_{2n-1}$。
第二步:消去起点带来的常数
若从位置 $s$ 出发,走 $t$ 步后的轨迹值为
\[S_t=(P_{s+t-1}-P_{s-1})\bmod M.\]对于固定起点 $s$,每个轨迹值都减去了同一个常数 $P_{s-1}$。因此两个步数的轨迹值相等,当且仅当对应的前缀余数相等:
\[S_x=S_y\iff P_{s+x-1}=P_{s+y-1}.\]也就是说,从起点 $s$ 出发走 $L$ 步合法,等价于
\[P_s,P_{s+1},\ldots,P_{s+L-1}\]两两不同。于是
\[L_s=\max\left\{L\le n: P_s,P_{s+1},\ldots,P_{s+L-1}\text{ 两两不同}\right\}.\]原问题由“环上每个起点的余数轨迹”转化为“前缀余数数组中,每个左端点开始的最长无重复窗口”。
第三步:双指针维护最长无重复窗口
对每个起点 $s=0,1,\ldots,n-1$,窗口左端对应 $s+1$。维护右端指针 right 和集合 seen:
- 只要窗口长度尚未达到 $n$,且下一个前缀余数不在集合中,就把右端向右扩展;
- 当前窗口长度就是该起点的 $L_i$,累加到答案;
- 左端右移前,从集合中删除离开的前缀余数。
当左端右移时,窗口只会少一个元素,原有右端仍然合法,所以右端不需要回退。每个前缀余数最多入窗、出窗各一次。
第四步:累加窗口长度
当前窗口长度 right - left + 1 就是对应起点的 $L_s$。累加后删除左端余数,再处理下一个起点。答案最大可达 $n^2=4\times10^{10}$,在固定宽度语言中必须使用 64 位整数保存。本文代码使用哈希集合维护窗口,空间不超过 $O(n)$。
题解代码
import sys
input = sys.stdin.readline
def total_walk_length(n, modulus, values):
prefix = [0] * (2 * n)
current = 0
for i in range(1, 2 * n):
current = (current + values[(i - 1) % n]) % modulus
prefix[i] = current
seen = set()
right = 0
total = 0
for left in range(1, n + 1):
limit = left + n - 1
while right < limit and prefix[right + 1] not in seen:
right += 1
seen.add(prefix[right])
total += right - left + 1
seen.remove(prefix[left])
return total
def solve():
test_count = int(input())
output = []
for _ in range(test_count):
n, modulus = map(int, input().split())
values = list(map(int, input().split()))
output.append(str(total_walk_length(n, modulus, values)))
print('\n'.join(output))
solve()
复杂度分析
设当前测试的序列长度为 $n$。
时间复杂度:期望 $O(n)$。前缀余数计算为 $O(n)$;双指针中每个位置至多进入和离开集合各一次。哈希集合单次操作的期望复杂度为 $O(1)$。
空间复杂度:$O(n)$,用于前缀余数数组和至多包含 $n$ 个元素的集合。
第 3 题:最大乘积操作
题目描述
给定长度为 $n$ 的正整数序列 $a$ 和操作次数 $k$。每次操作选择一个当前值大于 $1$ 的元素,将它减 $1$。如果所有元素都已经等于 $1$,则无法继续操作并提前停止,即使仍有剩余操作次数。
在上述规则下,使最终序列的乘积
\[\prod_{i=1}^{n}a_i\]尽可能大,并输出最大乘积对 $10^9+7$ 取模的结果。
输入包含 $T$ 组测试数据,其中 $1\le T\le 10^5$。每组满足 $1\le n\le 2\times10^5$、$0\le k\le10^{18}$,随后给出 $n$ 个正整数,满足 $1\le a_i\le10^9$。保证所有测试中 $n$ 的总和不超过 $4\times10^5$。
样例
输入
3
3 3
2 2 3
4 2
5 1 3 2
1 100
10
输出
2
18
1
第一组可依次把当前最大元素减小,最终得到 [1, 1, 2],乘积为 $2$。第三组把唯一元素从 $10$ 减到 $1$ 后无法继续,答案为 $1$。
思路分析
第一步:一次操作应该减哪个元素
假设当前乘积为 $P$,选择值为 $v>1$ 的元素递减后,新乘积相当于乘上
\[\frac{v-1}{v}=1-\frac{1}{v}.\]$v$ 越大,这一比例越大,乘积损失越小。因此每次操作都应选择当前最大的元素。若有多个最大元素,选择其中任意一个都等价。
这一结论也可通过交换论证理解:如果某一步减了较小值 $x$,却保留了更大的值 $y$,把这一步改为减 $y$,其乘积保留比例不会更差。
第二步:最终序列一定呈“削平”形态
持续递减当前最大值,会把所有冒尖元素逐渐压到同一水平附近。于是可以先选择整数水平线 $x$,把所有大于 $x$ 的元素削到 $x$。削平后的中间状态为
\[b_i=\min(a_i,x).\]达到水平线 $x$ 所需操作数为
\[cost(x)=\sum_{a_i>x}(a_i-x).\]$x$ 越低,cost(x) 越大。因此“cost(x) <= k”具有单调性,可以二分找到操作次数能够达到的最低水平线 $x$。
第三步:快速计算削平代价
先将数组升序排序,并计算前缀和。对给定 $x$,用二分查找定位第一个大于 $x$ 的元素。若其下标为 $j$,则
\[cost(x)=\left(\sum_{i=j}^{n-1}a_i\right)-x(n-j).\]这样每次检查只需一次二分查找,不必扫描整个数组。
第四步:分配削平后的剩余操作
令 $x$ 为最低可达到的水平线,并设
\[r=k-cost(x).\]把所有原值不小于 $x$ 的元素压到 $x$ 后,还剩 $r$ 次操作。由于 $x$ 已经是最低可行水平线,若把所有这些 $x$ 都再减一次就会达到更低水平线 $x-1$,与最低性的定义矛盾。因此 $r$ 小于削平后等于 $x$ 的元素个数。
剩余操作应分别落在 $r$ 个值为 $x$ 的元素上,把它们变成 $x-1$;其余仍为 $x$。原本小于 $x$ 的元素不变。最终乘积可分成三部分:
- 原值小于 $x$ 的元素之积;
- 若干个 $x$ 的幂;
- $r$ 个 $x-1$ 的幂。
若削平后等于 $x$ 的元素个数为 $c$,最终乘积为
\[ans=\left(\prod_{a_i<x}a_i\right)\cdot x^{c-r}\cdot(x-1)^r\pmod{10^9+7}.\]用 Python 内置三参数 pow 完成快速幂取模。
第五步:处理所有元素都能减到 1 的情况
把所有元素减到 $1$ 最多能执行
\[\sum_{i=1}^{n}(a_i-1)\]次。若 $k$ 不小于这个值,操作会因所有元素均为 $1$ 而提前停止,最终乘积直接为 $1$。这个分支也避免后续出现水平线以下的无效计算。
题解代码
import sys
from bisect import bisect_left, bisect_right
input = sys.stdin.readline
MOD = 10 ** 9 + 7
def max_product(values, operation_count):
values.sort()
n = len(values)
prefix_sum = [0] * (n + 1)
for i, value in enumerate(values):
prefix_sum[i + 1] = prefix_sum[i] + value
if operation_count >= prefix_sum[n] - n:
return 1
def cost_to_level(level):
first_greater = bisect_right(values, level)
high_sum = prefix_sum[n] - prefix_sum[first_greater]
high_count = n - first_greater
return high_sum - level * high_count
low, high = 1, values[-1]
while low < high:
middle = (low + high) // 2
if cost_to_level(middle) <= operation_count:
high = middle
else:
low = middle + 1
level = low
remaining = operation_count - cost_to_level(level)
first_level = bisect_left(values, level)
level_count = n - first_level
result = 1
for value in values[:first_level]:
result = result * value % MOD
result = result * pow(level, level_count - remaining, MOD) % MOD
result = result * pow(level - 1, remaining, MOD) % MOD
return result
def solve():
test_count = int(input())
output = []
for _ in range(test_count):
n, operation_count = map(int, input().split())
values = list(map(int, input().split()))
output.append(str(max_product(values[:n], operation_count)))
print('\n'.join(output))
solve()
复杂度分析
设当前测试的序列长度为 $n$,最大元素为 $V$。
时间复杂度:排序为 $O(n\log n)$;二分水平线进行 $O(\log V)$ 次检查,每次用二分查找计算代价,耗时 $O(\log n)$;最后计算未改变部分的乘积为 $O(n)$。总时间复杂度为 $O(n\log n+\log V\log n)$。
空间复杂度:$O(n)$,用于排序后的数组和前缀和。
小结
- 第一题中,删除最深层后剩余括号的包含关系不变,最大深度每轮恰好减 $1$,所以答案可由原始深度直接换算。
- 第二题把轨迹余数写成两个前缀余数之差,消去固定起点对应的常数后,就得到最长无重复窗口问题,可用双指针在线性期望时间内解决。
- 第三题利用“递减越大的元素,乘积相对损失越小”的贪心性质,把最终状态描述成削平后的序列;二分水平线并用前缀和求代价,即可处理很大的操作次数。