大厂真题 / 阿里巴巴

阿里 3.25 笔试真题 - 研发岗

本场考试概述

考试时间:2026年3月25日 考试岗位:研发岗 难度评级:中等偏难

考点分析

  1. 第一题:数学/贪心 — 糖果分配的最大最小值(难度中等)
  2. 第二题:贪心 — 两个排列的最大公共子序列(难度中等)
  3. 第三题:二分查找 + 数位DP — 第 k 个”喜欢的正整数”(难度困难)

建议策略

  1. 前两题是思维题,想清楚贪心策略后代码量很小,务必快速拿下
  2. 第三题数位DP是经典难点,建议提前练熟数位DP模板,考场上才能稳定输出

第一题:圣诞老人分糖果

题目描述

圣诞老人有 n 种糖果,第 i 种糖果有 c[i] 颗。现在要把所有糖果分给 k 个小朋友,要求对于每一种糖果,任意两个小朋友分到的该种糖果数量之差不超过 1。

求某个小朋友分到的糖果总数的最小值和最大值。

样例

输入

1
3 2
5 2 7

输出

6 8

样例解释:3 种糖果分给 2 个小朋友。

糖果种类 总数 每人基础量 (c[i]//k) 余数 (c[i]%k) 分配方式
1 5 2 1 一人得 3,一人得 2
2 2 1 0 每人得 1
3 7 3 1 一人得 4,一人得 3
  • 最大值:让同一个小朋友尽可能多地拿到”+1”,糖果 1 和糖果 3 都有余数,所以最大值 = (2+1+3) + 2 = 8
  • 最小值:让这个小朋友尽可能少地拿到”+1”,最小值 = (2+1+3) + 0 = 6

思路分析

对于第 i 种糖果,每个小朋友至少分到 base_i = c[i] // k 颗,还剩余 r_i = c[i] % k 颗需要分给 r_i 个小朋友(每人多得 1 颗)。

最大值:我们希望目标小朋友从尽可能多的糖果种类中拿到”+1”。每种糖果只要 r_i > 0,就存在至少一个名额可以给目标小朋友。因此:

\[\text{max} = \sum \text{base}_i + \text{count}(r_i > 0)\]

最小值:我们希望目标小朋友尽可能少地被分到”+1”。对于每种糖果 i,有 r_i 个”+1”需要分配给 k 个人中的 r_i 个。我们优先把这些”+1”分给其他 k-1 个人。但每种糖果最多只能让 k-1 个”其他人”各吸收 1 个”+1”,所以 n 种糖果的其他人总共最多吸收 (k-1) * n 个”+1”。如果所有余数之和 sum(r_i) 超过了这个上限,目标小朋友就不得不承担多出来的部分:

\[\text{min} = \sum \text{base}_i + \max(0,\ \sum r_i - (k-1) \times n)\]

时间复杂度:O(n)。 空间复杂度:O(n)。

import sys
input = sys.stdin.readline

def solve():
    n, k = map(int, input().split())
    c = list(map(int, input().split()))
    base_sum = 0
    cnt_pos = 0
    extra_sum = 0
    for ci in c:
        base_sum += ci // k
        r = ci % k
        if r > 0:
            cnt_pos += 1
        extra_sum += r
    max_val = base_sum + cnt_pos
    min_val = base_sum + max(0, extra_sum - (k - 1) * n)
    print(min_val, max_val)

T = int(input())
for _ in range(T):
    solve()

第二题:公共子序列

题目描述

给定两个长度为 n 的排列 p 和 q(均为 1~n 的排列),请找出它们的字典序最大的公共子序列。

样例

输入

5
3 5 1 2 4
2 5 4 1 3

输出

2
5 4

样例解释

  • 在 p = [3, 5, 1, 2, 4] 中,5 在位置 1,4 在位置 4,子序列 [5, 4] 合法
  • 在 q = [2, 5, 4, 1, 3] 中,5 在位置 1,4 在位置 2,子序列 [5, 4] 合法
  • [5, 4] 是字典序最大的公共子序列

思路分析

要求字典序最大,自然希望尽可能先选大的值。贪心策略如下:

  1. 预处理每个值在 p 和 q 中的位置,记为 pos_p[v] 和 pos_q[v]
  2. 维护两个指针 ip 和 iq,分别表示在 p 和 q 中”当前已选到的位置之后”的起始位置
  3. 从大到小遍历值 v = n, n-1, …, 1:
    • 如果 pos_p[v] >= ip 且 pos_q[v] >= iq,说明 v 在两个排列中都出现在当前指针之后,可以选入结果
    • 选入后,更新 ip = pos_p[v] + 1,iq = pos_q[v] + 1

为什么贪心是正确的? 因为我们是按值从大到小考虑的。对于当前值 v,如果它在两个排列中的位置都合法,选它一定不会比跳过它更差 — 选了它只会”消耗”两个排列中 v 之前的位置,而比 v 小的值无论如何都排在 v 后面(字典序意义上),不会因为选了 v 而产生更优的替代方案。

时间复杂度:O(n)。 空间复杂度:O(n)。

import sys
input = sys.stdin.readline

def solve():
    n = int(input())
    p = list(map(int, input().split()))
    q = list(map(int, input().split()))
    pos_p = [0] * (n + 1)
    pos_q = [0] * (n + 1)
    for i in range(n):
        pos_p[p[i]] = i
        pos_q[q[i]] = i
    result = []
    ip = iq = 0
    for v in range(n, 0, -1):
        if pos_p[v] >= ip and pos_q[v] >= iq:
            result.append(v)
            ip = pos_p[v] + 1
            iq = pos_q[v] + 1
    print(len(result))
    print(' '.join(map(str, result)))

solve()

第三题:喜欢的正整数

题目描述

定义一个正整数是”喜欢的”,当且仅当它同时满足以下两个条件:

  1. 不能被 3 整除
  2. 十进制表示中不包含数字 ‘3’

给定 k,求第 k 个”喜欢的”正整数。

样例

输入

5
1
2
3
4
5

输出

1
2
4
5
7

样例解释

  • 1: 不被 3 整除,不含数字 3 → 喜欢
  • 2: 不被 3 整除,不含数字 3 → 喜欢
  • 3: 被 3 整除 → 不喜欢
  • 4: 不被 3 整除,不含数字 4… 不含数字 3 → 喜欢
  • 5: 不被 3 整除,不含数字 3 → 喜欢
  • 6: 被 3 整除 → 不喜欢
  • 7: 不被 3 整除,不含数字 3 → 喜欢

所以前 5 个喜欢的正整数是 1, 2, 4, 5, 7。

思路分析

本题是经典的”求第 k 个满足条件的数”问题,标准解法是 二分答案 + 数位DP 计数。

第一步:二分答案

如果我们能快速计算 “1 到 x 之间有多少个喜欢的正整数”(记为 count_liked(x)),那么就可以二分找到最小的 x 使得 count_liked(x) >= k,这个 x 就是答案。

二分范围:下界 lo = 1,上界 hi 取一个足够大的值(如 2 * 10^19)。

第二步:数位DP计数

核心问题变成:给定上界 x,计算 [1, x] 中满足”不含数字 3 且不被 3 整除”的正整数个数。

状态设计

逐位枚举 x 的每一位数字,维护以下状态:

状态 含义 取值范围
rem 当前已确定的数位之和对 3 取模的余数 0, 1, 2
started 是否已经填过非零数字(用于处理前导零) 0, 1
tight 当前是否还受到上界 x 的约束 受限 / 自由

其中 tight 状态决定了当前位可以填的数字范围:

  • 受限(tight):当前位只能填 0 到 s[i](x 的第 i 位数字)
  • 自由(free):当前位可以填 0 到 9

状态转移

对于每一位,我们枚举当前位填的数字 d(跳过 d=3),然后更新状态:

  • new_rem = (rem + d) % 3
  • new_started = 1 if (started or d > 0) else 0
  • 如果当前是 tight 且 d == s[i],则下一位仍然是 tight;否则下一位变为 free

实现细节

用两组二维数组 tight[rem][started] 和 free[rem][started] 分别记录处于受限/自由状态下各种 (rem, started) 组合的方案数。逐位推进,每一位产生新的 nt(新的受限状态)和 nf(新的自由状态)。

统计答案

处理完所有位后,满足条件的数 = 所有 rem != 0 且 started == 1 的状态之和。rem != 0 保证不被 3 整除,started == 1 保证是正整数(排除了”全选 0”的情况,即排除 0 本身)。

\[\text{count\_liked}(x) = \sum_{r \in \{1,2\}} (\text{tight}[r][1] + \text{free}[r][1])\]

时间复杂度:二分 O(log(2 * 10^19)) 轮,每轮数位DP遍历 O(20) 位,每位枚举 9 个数字(跳过3),状态数 3 * 2 = 6。总复杂度 O(T * 60 * 20 * 9 * 6),非常快。

import sys
input = sys.stdin.readline

def count_liked(x):
    if x <= 0:
        return 0
    s = str(x)
    n = len(s)
    # tight[rem][started], free[rem][started]
    tight = [[0] * 2 for _ in range(3)]
    free = [[0] * 2 for _ in range(3)]
    tight[0][0] = 1  # 初始状态:余数0,未开始,受限
    for i in range(n):
        dlim = int(s[i])
        nt = [[0] * 2 for _ in range(3)]
        nf = [[0] * 2 for _ in range(3)]
        for rem in range(3):
            for st in range(2):
                # 处理 tight 状态的转移
                if tight[rem][st]:
                    cnt = tight[rem][st]
                    for d in range(dlim + 1):
                        if d == 3:
                            continue
                        nr = (rem + d) % 3
                        ns = 1 if (st or d > 0) else 0
                        if d == dlim:
                            nt[nr][ns] += cnt
                        else:
                            nf[nr][ns] += cnt
                # 处理 free 状态的转移
                if free[rem][st]:
                    cnt = free[rem][st]
                    for d in range(10):
                        if d == 3:
                            continue
                        nr = (rem + d) % 3
                        ns = 1 if (st or d > 0) else 0
                        nf[nr][ns] += cnt
        tight = nt
        free = nf
    # 统计:rem != 0 且 started == 1
    return sum(tight[r][1] + free[r][1] for r in range(1, 3))

def solve(k):
    lo, hi = 1, 2 * 10**19
    while lo < hi:
        mid = (lo + hi) // 2
        if count_liked(mid) >= k:
            hi = mid
        else:
            lo = mid + 1
    return lo

t = int(input())
out = []
for _ in range(t):
    k = int(input())
    out.append(str(solve(k)))
print('\n'.join(out))

小结

  • 第一题糖果分配是数学/贪心题,关键在于分析余数的分配策略,推导出最大值和最小值的计算公式
  • 第二题公共子序列利用排列的性质,从大到小贪心选取,O(n) 即可解决
  • 第三题是数位DP经典应用,”第 k 个满足条件的数”= 二分答案 + 数位DP计数,状态设计(余数 + 是否开始 + 是否受限)是数位DP的通用模板,建议熟练掌握