大厂真题 / 携程
携程 2026 春招笔试真题 - 研发岗(3.29)
本场考试概述
考试时间:2026年3月29日 考试岗位:研发岗 难度评级:中等偏难
考点分析:
- 第一题:模拟(难度简单)
- 第二题:模拟 + 排名维护(难度中等)
- 第三题:后缀数组 + 贪心(难度困难)
- 第四题:质数筛 + 分段跳跃(难度困难)
建议策略:
- 前两题是纯模拟,务必快速拿满分
- 第三题后缀数组是字符串进阶考点,关键在于理解后缀排序与贪心分配的关系
- 第四题数论味很浓,需要分析 gcd 的周期性质来优化迭代
第一题:在哪里呢
题目描述
给定一个长度为 5 的字符串,保证其中恰好包含一个字符 'a'。请输出 'a' 在字符串中的位置(1-indexed)。
样例
输入
bxaxz
输出
3
思路分析
逐字符扫描,找到 'a' 就返回其下标 + 1。字符串长度固定为 5,直接线性遍历即可。没有任何坑点,属于签到题。
时间复杂度:O(1)(字符串长度固定为 5)。 空间复杂度:O(1)。
import sys
input = sys.stdin.readline
s = input().strip()
for i in range(5):
if s[i] == 'a':
print(i + 1)
break
第二题:实时排名
题目描述
IOI 赛制评分系统。共有 n 次提交,每次提交格式为 (user, problem, score)。每道题目的得分取该用户在该题所有提交中的最高分。用户的总分为所有题目得分之和。
每次提交后,输出用户 1 的实时排名。排名定义:总分严格高于用户 1 的人数 + 1。
样例
输入
10
2 1 0
1 1 80
3 2 100
1 2 60
3 1 40
3 3 60
1 1 90
5 1 100
5 2 100
1 4 50
输出
1 1 2 1 1 2 2 2 3 1
思路分析
本题的核心在于高效维护每个用户的”每题最高分”和”总分”。
关键观察:每次提交只会影响一个用户的一道题。如果本次提交的分数高于该用户该题的历史最高分,就更新最高分,同时更新该用户的总分(加上差值)。如果分数没有提高,总分不变。
排名计算:每次提交后,遍历所有已出现的用户,统计总分严格大于用户 1 的人数,加 1 即为排名。由于 n 最多是常规笔试范围(约 10^5),且每次只需遍历已有用户数,直接暴力统计即可。
具体实现使用两层字典:best[user][problem] 记录最高分,total[user] 记录总分。每次提交时先算差值 delta = max(0, score - best[user][problem]),再更新 total[user] += delta。
时间复杂度:O(n^2) 最坏情况(每次遍历所有用户),但笔试数据规模下完全够用。 空间复杂度:O(n)。
import sys
input = sys.stdin.readline
from collections import defaultdict
n = int(input())
best = defaultdict(lambda: defaultdict(int)) # best[user][problem]
total = defaultdict(int) # total[user]
results = []
for _ in range(n):
u, p, s = map(int, input().split())
old = best[u][p]
if s > old:
total[u] += s - old
best[u][p] = s
# 计算用户 1 的排名
score1 = total[1]
rank = 1
for uid in total:
if uid != 1 and total[uid] > score1:
rank += 1
results.append(str(rank))
print(" ".join(results))
第三题:字符串min
题目描述
给定一个长度为 n 的字符串 s 和一个长度为 n 的正整数数组 a。你可以将 a 重新排列为 a’,然后构造字符串 T = s[0] * a’[0] + s[1] * a’[1] + … + s[n-1] * a’[n-1](即每个字符重复 a’[i] 次后拼接)。请输出使 T 字典序最小的排列 a’。
样例
输入
3
bac
1 2 3
输出
3 1 2
思路分析
这道题乍看是一个排列优化问题,但核心思想可以通过后缀比较 + 贪心来解决。
第一步:理解目标。T 由 s[0] 重复 a’[0] 次、s[1] 重复 a’[1] 次、… 拼接而成。我们希望 T 的字典序尽可能小。直觉上,如果某个位置 i 的后续部分(从位置 i 开始到末尾的后缀)在字典序上越”小”,那么分配给它的重复次数应该越”大”——因为重复次数大意味着这个字符占据更长的前缀位置,而一个字典序小的后缀占据更多位置不会使 T 变大。
第二步:为什么用后缀排序。考虑两个位置 i 和 j,假设 suffix(i) < suffix(j)(字典序)。如果 a’[i] 更大,则位置 i 的字符 s[i] 会重复更多次。由于 suffix(i) 整体更小,让它”扩展”更长不会让 T 变差。反之,如果把大的 a’ 值分配给后缀字典序大的位置,T 的后续部分会更早出现大的字符序列。
更严格地说,后缀数组给出了所有后缀的字典序排名。排名越小(后缀越小),分配的 a’ 值应该越大。这是因为:
- 排名小的后缀意味着从该位置往后看,字符串整体偏小
- 给它分配大的重复次数,相当于让这个”小”的模式在 T 中占据更多空间
- 这正好使得 T 的字典序最小
第三步:实现细节。
- 对字符串 s 计算后缀数组(Suffix Array),得到每个位置的后缀排名
rank[i] - 将 a 数组排序(降序)
- 按后缀排名从小到大的顺序,依次分配最大的 a 值、次大的 a 值、…
- 即:后缀排名第 1 小的位置得到 a 中最大值,后缀排名第 2 小的位置得到次大值,以此类推
这里使用倍增法构建后缀数组,时间复杂度 O(n log n)。
时间复杂度:O(n log n)(后缀数组构建) + O(n log n)(排序) = O(n log n)。 空间复杂度:O(n)。
import sys
input = sys.stdin.readline
def suffix_array(s):
"""倍增法构建后缀数组,返回 sa 和 rank 数组"""
n = len(s)
sa = list(range(n))
rank = [ord(c) for c in s]
tmp = [0] * n
k = 1
while k < n:
def cmp_key(i):
return (rank[i], rank[i + k] if i + k < n else -1)
sa.sort(key=cmp_key)
tmp[sa[0]] = 0
for i in range(1, n):
tmp[sa[i]] = tmp[sa[i - 1]]
if cmp_key(sa[i]) != cmp_key(sa[i - 1]):
tmp[sa[i]] += 1
rank = tmp[:]
if rank[sa[-1]] == n - 1:
break
k *= 2
return sa, rank
def solve():
n = int(input())
s = input().strip()
a = list(map(int, input().split()))
sa, rank = suffix_array(s)
# 将 a 降序排列
a_sorted = sorted(a, reverse=True)
# sa[i] 是排名第 i 的后缀的起始位置
# 排名第 0 小的后缀分配最大值,排名第 1 小的分配次大值...
ans = [0] * n
for i in range(n):
ans[sa[i]] = a_sorted[i]
print(" ".join(map(str, ans)))
solve()
第四题:min和gcd
题目描述
定义函数 f(x) = min(x + gcd(x, b), b + gcd(x, b))。给定 x, b, n,求 f 迭代 n 次后的值,即 f^n(x)。
多组测试数据。
样例
输入
2
7 3 4
10 2 3
输出
4
4
思路分析
这道题需要仔细分析函数 f 的行为,找到优化迭代的方法。
第一步:理解 f(x) 的行为。
f(x) = min(x + gcd(x, b), b + gcd(x, b))
设 g = gcd(x, b),则 f(x) = min(x + g, b + g) = g + min(x, b)。
- 当 x <= b 时:f(x) = g + x = x + gcd(x, b)
- 当 x > b 时:f(x) = g + b = b + gcd(x, b)
第二步:分析 x >= b 的情况。
| 若 x >= b,则 f(x) = b + gcd(x, b)。注意 gcd(x, b) 是 b 的因子(因为 gcd(x, b) | b),所以 f(x) = b + d,其中 d | b。 |
| 接下来 f(b + d):gcd(b + d, b) = gcd(d, b) = d(因为 d | b),所以 f(b + d) = b + d + d = b + 2d?不对,再看:min(b + d + d, b + d) = b + d。所以 f(b + d) = b + d。 |
这说明一旦 x >= b,再迭代一次就到达不动点 b + gcd(x, b),之后不再变化。更准确地说:当 x >= b 时,f(x) = b + gcd(x, b),且这个值本身也 >= b,所以 f(f(x)) = f(b + gcd(x, b)) = b + gcd(b + gcd(x,b), b) = b + gcd(x, b) = f(x)。因此 x >= b 后最多再迭代一次就稳定。
第三步:分析 x < b 的核心情况。
当 x < b 时,f(x) = x + gcd(x, b)。设 g = gcd(x, b),则 x’ = x + g。
关键问题是:从 x 开始,每次加 gcd(当前值, b),需要多少步才能使得 x >= b?
每一步的增量 gcd(x, b) 取决于 x 和 b 的公因子。如果 b 只有少量不同的质因子,那么 gcd(x, b) 的可能取值也有限。
第四步:分段跳跃优化。
考虑 b 的质因子分解 b = p1^e1 * p2^e2 * … * pk^ek。gcd(x, b) 的值取决于 x 包含了 b 的哪些质因子的幂次。
核心观察:当 x 是 b 的某个因子 d 的倍数时,gcd(x, b) 至少为 d,每步至少增加 d。从 x 到下一个 x 满足更高 gcd 的过程中,增量是固定的(等于当前的 gcd(x, b))。
因此可以这样优化:
- 求出 b 的所有因子并排序
- 对于当前的 x,计算 g = gcd(x, b)
- x 每次增加 g,直到 gcd 发生变化或 x >= b
- 在 gcd 不变的区间内,可以直接算出需要多少步跳到下一个 gcd 变化点,然后一次跳完
具体来说,如果当前 gcd(x, b) = g 且 g 固定不变,x 每次 +g,那么 x 经过 t 步变为 x + tg。我们需要找到最小的 t 使得 gcd(x + tg, b) != g 或 x + t*g >= b。
| gcd 什么时候会变化?当 x + t*g 成为 b 的某个更大因子的倍数时。枚举 b 的所有大于 g 的因子 d,计算最小的 t 使得 d | (x + t*g)。取所有这些 t 的最小值即为跳跃步数。 |
由于 b 的因子个数通常不大(约 O(sqrt(b)) 级别),每次跳跃后 gcd 至少翻倍(变为更大的因子),所以跳跃次数是 O(log b) 级别。总复杂度远优于逐步模拟。
时间复杂度:O(sqrt(b) + d(b) * log(b)),其中 d(b) 是 b 的因子个数。 空间复杂度:O(d(b))。
import sys
input = sys.stdin.readline
from math import gcd
def get_divisors(b):
"""获取 b 的所有因子,升序排列"""
divs = []
i = 1
while i * i <= b:
if b % i == 0:
divs.append(i)
if i != b // i:
divs.append(b // i)
i += 1
divs.sort()
return divs
def solve():
x, b, n = map(int, input().split())
for _ in range(n):
if x >= b:
# 不动点: f(x) = b + gcd(x, b), 之后不再变化
x = b + gcd(x, b)
break
g = gcd(x, b)
# x < b: f(x) = x + g
# 计算在 gcd 不变的情况下可以跳多少步
# x, x+g, x+2g, ... 直到 gcd 变化或 x+t*g >= b
steps_to_b = (b - x - 1) // g # 最多这么多步还保持 x < b
# 找最小步数使 gcd 变化: 枚举 b 的因子中大于 g 的
divs = get_divisors(b)
min_steps = steps_to_b + 1 # 默认跳到 >= b
for d in divs:
if d <= g:
continue
# 找最小 t >= 1 使得 d | (x + t * g)
rem = x % d
if rem == 0:
# 已经整除,但 gcd(x,b) = g < d,说明 d 不整除 b 或
# x 包含 d 但 gcd(x,b) 不等于 d —— 不可能,因为 d|b 且 d|x 则 d|gcd(x,b)
continue
need = d - rem # 需要增加 need 使得 (x + need) % d == 0
if need % g != 0:
continue
t = need // g
if t == 0:
t = d // g # 下一个周期
if t < min_steps:
min_steps = t
# 实际可跳步数受剩余 n 限制
actual = min(min_steps, n - (_ + 1 - 1))
# 重新计算: 当前是第 _+1 步(从0开始计数的第_步),还剩 n-1-_ 步可用
# 但我们在循环里逐步处理,这里需要改为批量跳跃
# 简化:直接跳 min_steps 步或剩余步数
break # 这个方法在循环结构里不好写,改用 while 循环
print(x)
def solve_case():
x, b, n = map(int, input().split())
step = 0
while step < n:
if x >= b:
g = gcd(x, b)
x = b + g # 不动点
step += 1
break
g = gcd(x, b)
# x < b, 每次 x += g (在 g 不变期间)
# 还需 n - step 步
remaining = n - step
# 最多跳多少步 x 还 < b
steps_to_b = (b - x - 1) // g
# 找最小步数使 gcd 变化
divs = get_divisors(b)
min_change = steps_to_b + 1
for d in divs:
if d <= g:
continue
rem = x % d
if rem == 0:
continue
need = d - rem
if need % g != 0:
continue
t = need // g
if t == 0:
t = d // g
if 0 < t < min_change:
min_change = t
jump = min(min_change, remaining)
x += jump * g
step += jump
print(x)
t = int(input())
for _ in range(t):
solve_case()
样例验证
样例 1:x=7, b=3, n=4
- 步骤 1:x=7 >= b=3,gcd(7,3)=1,f(7) = min(8, 4) = 4
- 步骤 2:x=4 >= b=3,gcd(4,3)=1,f(4) = min(5, 4) = 4(不动点)
- 步骤 3:f(4) = 4
- 步骤 4:f(4) = 4
- 输出 4
样例 2:x=10, b=2, n=3
- 步骤 1:x=10 >= b=2,gcd(10,2)=2,f(10) = min(12, 4) = 4
- 步骤 2:x=4 >= b=2,gcd(4,2)=2,f(4) = min(6, 4) = 4(不动点)
- 步骤 3:f(4) = 4
- 输出 4
小结
- 第一题纯签到,遍历 5 个字符找
'a'即可 - 第二题 IOI 赛制模拟,维护每题最高分和用户总分,每次提交后暴力统计排名
- 第三题后缀数组 + 贪心是字符串竞赛经典套路:后缀排名小的位置分配大的重复次数,使拼接结果字典序最小
- 第四题分析 f(x) 的数学结构是关键:x >= b 时一步到达不动点,x < b 时利用 gcd 的因子结构做分段跳跃,避免逐步模拟