大厂真题 / 阿里巴巴

阿里 5.9 笔试真题 - AI研发岗

本场考试概述

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

考点分析

  1. 第一题:贪心 + 数位枚举(难度中等)
  2. 第二题:二分答案 + 滑窗贪心匹配(难度困难)
  3. 第三题:状压DP(难度中等)

建议策略

  1. 第一题抓住”前缀照抄、当前位减 1、后面全填 9”的贪心结构,线性扫一遍即可
  2. 第二题先识别可行性单调性从而二分答案,内层用最小堆按桩位置最早过期优先匹配
  3. 第三题看到 $n \leq 16$ 立刻锁定状压 DP,按 mask 升序转移

第一题:最大数位之和

题目描述

给定一个正整数 $x$,在所有严格小于 $x$ 的非负整数中,找到数位之和最大的那个数并输出。如果有多个数位之和相同,输出任意一个即可。

$x$ 以十进制字符串给出,长度可达 $10^5$。

样例

输入

4
1
5
99
1234

输出

0
4
89
999

思路分析

第一步:观察最优解的结构

要让数位之和最大,直觉上每一位都尽量大(填 9)。但必须严格小于 $x$,所以至少在某个位置首次比 $x$ 小——这意味着最优解一定形如”前 $i$ 位原样保留、第 $i$ 位减 1、后面全填 9”。

第二步:枚举断点

设 $x$ 的第 $i$ 位数字为 $d_i$,前 $i$ 位数位之和为 $\text{prefix}_i$。在位置 $i$ 处减 1 后,候选答案的数位之和为:

\[\text{prefix}_i + (d_i - 1) + 9 \times (\text{len} - 1 - i)\]

线性扫描所有位置,取最大值对应的断点。注意 $d_i = 0$ 时无法减 1(会变负),跳过。

第三步:重建答案

取到最优断点 $i$ 后,把前 $i$ 位原样保留、第 $i$ 位减 1、之后全部置 9。最后去掉前导零。

题解代码

import sys

input = sys.stdin.readline

def solve(x):
    n = len(x)
    if n == 1:
        return str(int(x) - 1)
    digits = [ord(c) - 48 for c in x]
    prefix = 0
    best_sum = -1
    best_i = -1
    for i in range(n):
        if digits[i] >= 1:
            cur = prefix + (digits[i] - 1) + 9 * (n - 1 - i)
            if cur > best_sum:
                best_sum = cur
                best_i = i
        prefix += digits[i]
    parts = [chr(48 + digits[k]) for k in range(best_i)]
    parts.append(chr(48 + digits[best_i] - 1))
    parts.extend(['9'] * (n - 1 - best_i))
    s = ''.join(parts).lstrip('0')
    return s if s else '0'

T = int(input())
out = []
for _ in range(T):
    out.append(solve(input().strip()))
sys.stdout.write('\n'.join(out))

复杂度分析

时间复杂度:$O(L)$,$L$ 为所有 $x$ 的数位长度总和 空间复杂度:$O(L)$


第二题:充电桩阈值

题目描述

数轴上有 $n$ 个机器人与 $m$ 个充电桩。第 $j$ 个充电桩位于 $c_j$,可同时为至多 $\text{cap}_j$ 个机器人充电。若机器人与被分配充电桩的距离不超过阈值 $E$,则视为可达。求最小阈值 $E$ 使所有机器人都能被分配到某一充电桩且不超过容量限制;无法覆盖全部则输出 $-1$。

样例

输入

2
3 2
1 5 8
2 7
2 2
2 1
0 100
50
2

输出

2
50

思路分析

第一步:识别单调性

$E$ 越大,每个机器人的可达范围越广,越容易分配。存在一个临界点:$E$ 从某个值开始变得可行,之后都可行——具备单调性,适合二分答案。

第二步:二分框架

二分 $E \in [0, 10^6]$,每次调用 check 函数判定当前 $E$ 是否可行。

第三步:check 函数——滑动窗口 + 贪心

把机器人按位置升序处理。对当前机器人 $r$,可用的桩必须满足 $\lvert c - r \rvert \leq E$,即 $c \in [r - E, r + E]$。

随着 $r$ 增大,可用桩的窗口单调右移。维护一个按位置升序的最小堆作为候选集:

  • 新进入窗口的桩($c \leq r + E$)push 入堆
  • 过期的桩($c < r - E$)或容量耗尽的桩从堆顶弹出

贪心策略:每次给当前机器人分配位置最小的可用桩。因为位置最小的桩最先过期(离开窗口),留着只会浪费。

第四步:无解判断

若所有桩容量之和 $< n$,无论 $E$ 多大都输出 $-1$。

题解代码

import sys
import heapq

input = sys.stdin.readline

def feasible(robots, stations, caps_orig, E):
    m = len(stations)
    p = 0
    heap = []
    caps = list(caps_orig)
    for r in robots:
        while p < m and stations[p][0] <= r + E:
            heapq.heappush(heap, (stations[p][0], stations[p][1]))
            p += 1
        while heap:
            c, idx = heap[0]
            if c < r - E or caps[idx] == 0:
                heapq.heappop(heap)
                continue
            break
        if not heap:
            return False
        c, idx = heap[0]
        caps[idx] -= 1
        if caps[idx] == 0:
            heapq.heappop(heap)
    return True

def solve():
    n, m = map(int, input().split())
    robots = list(map(int, input().split()))
    c = list(map(int, input().split()))
    cap = list(map(int, input().split()))
    if sum(cap) < n:
        return -1
    robots.sort()
    stations = sorted([(c[j], j) for j in range(m)])
    lo, hi = 0, 10 ** 6
    while lo < hi:
        mid = (lo + hi) // 2
        if feasible(robots, stations, cap, mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

T = int(input())
out = []
for _ in range(T):
    out.append(str(solve()))
sys.stdout.write('\n'.join(out))

复杂度分析

时间复杂度:$O((n + m) \log n \log V)$,$V$ 为值域上界 空间复杂度:$O(n + m)$


第三题:游历 n 个城市

题目描述

有 $n$ 个城市($n \leq 16$),已知从出发点到每个城市的距离 $d_i$,以及城市间的距离矩阵 $a_{ij}$($-1$ 表示不可达)。从出发点出发依次游历所有城市,每个城市最多去一次,离开出发点后不再回来。求最小总距离;无法游历完则输出 $-1$。

样例

输入

3
1 5 -1
0 2 3
4 0 6
7 8 0

输出

9

思路分析

第一步:识别问题模型

这是经典的 Hamiltonian Path 问题——从出发点访问所有节点恰好一次,求最短路径。$n \leq 16$ 直接提示状压 DP。

第二步:状态定义

用 $n$ 位二进制整数 mask 表示已访问城市集合。定义 $f[\text{mask}][i]$ = 已访问集合为 mask 且当前停在城市 $i$ 的最小总距离。

第三步:初始化与转移

  • 初始化:第一步必须从出发点直达某城市 $i$,若 $d_i \neq -1$,则 $f[1 \ll i][i] = d_i$
  • 转移:从状态 $(\text{mask}, i)$ 扩展到尚未访问的城市 $j$(要求 $a_{i][j} \neq -1$):$f[\text{mask} (1 \ll j)][j] = \min(f[\text{mask}][i] + a_{i][j})$

第四步:枚举顺序

mask 从小到大枚举,保证转移时来源已算好。最终答案为 $\min_i f[2^n - 1][i]$。

题解代码

import sys

input = sys.stdin.readline
INF = float('inf')

def solve():
    n = int(input())
    d = list(map(int, input().split()))
    a = []
    for _ in range(n):
        a.append(list(map(int, input().split())))

    full = 1 << n
    f = [[INF] * n for _ in range(full)]

    for i in range(n):
        if d[i] != -1:
            f[1 << i][i] = d[i]

    for mask in range(1, full):
        for i in range(n):
            if not (mask >> i) & 1:
                continue
            cur = f[mask][i]
            if cur == INF:
                continue
            for j in range(n):
                if (mask >> j) & 1:
                    continue
                if a[i][j] == -1:
                    continue
                nm = mask | (1 << j)
                cand = cur + a[i][j]
                if cand < f[nm][j]:
                    f[nm][j] = cand

    ans = min(f[full - 1])
    print(-1 if ans == INF else ans)

solve()

复杂度分析

时间复杂度:$O(2^n \cdot n^2)$,$n = 16$ 时约 $1.7 \times 10^7$ 空间复杂度:$O(2^n \cdot n)$


小结

  • 第一题是贪心数位构造,核心观察是”在某位减 1 后面全填 9”一定最优,线性扫描即可
  • 第二题是二分答案经典题,内层 check 用滑动窗口 + 最小堆实现”最早过期优先”贪心
  • 第三题是 Hamiltonian Path 的状压 DP,$n \leq 16$ 是强烈暗示,按 mask 升序转移即可