大厂真题 / 阿里巴巴

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

本场考试概述

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

考点分析

  1. 第一题:前缀和 + 组合计数(难度简单)
  2. 第二题:位运算观察题(难度简单)
  3. 第三题:差分扫描线 + 区间覆盖判定(难度困难)

建议策略

  1. 第一题枚举中间字符 p,用前后缀计数把三层循环压成一次扫描
  2. 第二题快速识别”按位或不超过加法和”这一关键不等式,单元素贡献上界即 $a_i$ 自身,答案就是所有元素按位或
  3. 第三题把一次移动拆成”删除节省 + 插入开销”两部分,关键判据是”剩余数组里存在相邻对覆盖 $v$”,用差分扫描线 $O(n \log n)$ 解决

第一题:密钥密码

题目描述

给定一个只由字符 opc 组成的字符串 $s$,长度为 $n$。

从 $s$ 中选择 3 个位置 $i < j < k$,满足:

  • 第 $i$ 个字符必须是 o
  • 第 $j$ 个字符必须是 p
  • 第 $k$ 个字符可以是 c,也可以是 o(因为有时会把 o 看成 c

把所有满足条件的方案数记为 $W$。如果不存在任何方案,则 $W = 0$。

输出 $W$ 对 $10^9 + 7$ 取模的结果。

样例

输入

3
4
oppc
3
opc
6
oooppc

输出

2
1
6

思路分析

第一步:暴力的瓶颈在哪里

直接枚举三元组 $(i, j, k)$ 是 $O(n^3)$,$n$ 最大 $10^5$ 级别必然超时。关键在于发现三个位置之间的约束只是”左中右顺序”——如果固定中间的 p,左右两侧互相独立,可以将乘法原理用上。

第二步:枚举中间字符拆成独立子问题

对每个位置 $j$(当 $s_j = $ p),它能形成的三元组数量 = 左侧 o 的数量 $\times$ 右侧合法字符的数量。合法字符是 oc(即除 p 以外的字符)。

第三步:一次扫描同时维护前后缀计数器

定义两个变量:

  • \(\text{cnt\_o}\):当前位置严格左侧已出现的 o 数量
  • \(\text{suf\_oc}\):当前位置严格右侧的 oc 总数

初始时 \(\text{suf\_oc}\) 等于整个字符串中 oc 的总数。从左到右扫描:

  1. 先将当前位置从后缀中扣除(如果是 oc 则 \(\text{suf\_oc}\) 减 1),使其严格表示”右侧”
  2. 如果当前字符是 p,把 \(\text{cnt\_o} \times \text{suf\_oc}\) 累加到答案
  3. 如果当前字符是 o,\(\text{cnt\_o}\) 加 1

这样只需一次线性扫描,每步 $O(1)$,整体 $O(n)$。

第四步:取模

\(\text{cnt\_o}\) 和 \(\text{suf\_oc}\) 均不超过 $n$,乘积最大约 $n^2 \approx 10^{10}$,64 位整数能装下,每次累加后对 $10^9+7$ 取模即可。

题解代码

import sys

input = sys.stdin.readline

MOD = 10**9 + 7


def solve(s):
    # suf_oc 初始为整个串中 'o' 或 'c' 的总数
    suf_oc = 0
    for ch in s:
        if ch == 'o' or ch == 'c':
            suf_oc += 1
    cnt_o = 0  # 当前下标左侧已出现的 'o' 数
    ans = 0
    for ch in s:
        # 先把当前位从后缀剩余中扣掉,使 suf_oc 严格表示「右侧」合法 k 的数量
        if ch == 'o' or ch == 'c':
            suf_oc -= 1
        # 当前是 'p' 才贡献方案:左侧 'o' 数 * 右侧合法 k 数
        if ch == 'p':
            ans = (ans + cnt_o * suf_oc) % MOD
        if ch == 'o':
            cnt_o += 1
    return ans


T = int(input())
out = []
for _ in range(T):
    input()
    s = input().strip()
    out.append(str(solve(s)))
print('\n'.join(out), end='')

复杂度分析

时间复杂度:$O(n)$,单组数据扫一遍字符串,多组合计 $O(\sum n)$ 空间复杂度:$O(1)$ 额外空间(只用两个计数器)


第二题:按位或最大化

题目描述

给定一个长度为 $n$ 的数组 $a$,初始时令 $x = 0$。

可以进行若干次如下操作(可以为 0 次):选择一个下标 $i$,再选择一个整数 $y$,满足 $1 \leq y \leq a_i$。令 $a_i = a_i - y$,同时令 $x = x \mathbin{ } y$(按位或)。

目标是最大化最终得到的 $x$,输出这个最大值。

样例

输入

3
3
1 2 4
1
5
4
3 3 3 3

输出

7
5
3

思路分析

第一步:单个元素能给 $x$ 贡献多少

假设对 $a_i$ 操作了 $k$ 次,每次取出 $y_1, y_2, \ldots, y_k$。有两个约束:

  • 总量约束:$y_1 + y_2 + \cdots + y_k \leq a_i$
  • 按位或不超过加法和:对任意非负数,$y_1 \mathbin{ } y_2 \mathbin{ } \cdots \mathbin{ } y_k \leq y_1 + y_2 + \cdots + y_k$

原因很直观——按位或中重叠的位只算一次,而加法中重叠位会进位变成更高的 1,因此加法和总是 $\geq$ 按位或。

串联两个不等式:

\[x_i = y_1 \mathbin{|} \cdots \mathbin{|} y_k \leq y_1 + \cdots + y_k \leq a_i\]

所以单个 $a_i$ 对 $x$ 的贡献上界就是 $a_i$ 自身。

第二步:这个上界达得到吗

达得到——直接一次操作取 $y = a_i$,$a_i$ 清零,$x$ 被或上完整的 $a_i$。单次贡献恰好等于 $a_i$。

第三步:多个元素叠加

不同下标的操作互相独立。把每个 $a_i$ 都”一次取完”,最终:

\[x = a_1 \mathbin{|} a_2 \mathbin{|} \cdots \mathbin{|} a_n\]

所以答案就是所有元素的按位或。

题解代码

import sys

input = sys.stdin.readline


def solve(arr):
    # 单元素能贡献给 x 的最大值即 a[i] 自身
    # 多元素累积时,答案就是所有元素按位或
    res = 0
    for v in arr:
        res |= v
    return res


data = sys.stdin.buffer.read().split()
idx = 0
T = int(data[idx]); idx += 1
out = []
for _ in range(T):
    n = int(data[idx]); idx += 1
    a = [int(data[idx + j]) for j in range(n)]
    idx += n
    out.append(str(solve(a)))
print('\n'.join(out), end='')

复杂度分析

时间复杂度:$O(n)$,多组合计 $O(\sum n)$ 空间复杂度:$O(1)$ 额外空间


第三题:最小陡峭值

题目描述

定义一个数组的”陡峭值”为所有相邻元素的差的绝对值之和:

\[S = \sum_{i=1}^{n-1} |a_{i+1} - a_i|\]
当数组长度为 1 时,陡峭值定义为 0。例如数组 $[2, 3, 1]$ 的陡峭值为 $ 3-2 + 1-3 = 3$。

现有一个长度为 $n$ 的数组 $a$,可以进行至多一次操作:从数组中取出一个元素(其他元素的相对顺序保持不变),再将该元素插入到数组的任意位置(含两端)。

求经过至多一次操作后能够得到的最小陡峭值。

样例

输入

3
3
2 3 1
4
3 1 4 2
1
1

输出

2
4
0

思路分析

第一步:把移动拆成”删除 + 插入”

一次操作等价于:先删除某个元素 $a_i$,再把它插回剩余数组的某个位置。新陡峭值 = 原始陡峭值 $S$ - 删除节省量 + 插入开销。目标是让”节省 - 开销”最大化。

第二步:分析删除的节省量

删除内部元素 $a_i$($0 < i < n-1$)时:原先的贡献是 $ a_i - a_{i-1} + a_{i+1} - a_i $,删除后左右邻居直接拼接产生 $ a_{i+1} - a_{i-1} $。节省量为:
\[\text{save} = |a_i - a_{i-1}| + |a_{i+1} - a_i| - |a_{i+1} - a_{i-1}|\]

由三角不等式知 $\text{save} \geq 0$。对端点元素,节省量就是该端与唯一邻居的差值。

第三步:分析插入的开销

把值 $v = a_i$ 插入剩余数组某相邻对 $(l, r)$ 之间。原来这对贡献 $ r - l $,插入后变成 $ v - l + r - v $。插入开销为:
\[\text{cost} = |v - l| + |r - v| - |r - l|\]

关键观察:当 $\min(l, r) \leq v \leq \max(l, r)$ 时,由绝对值展开可知三项恰好抵消,$\text{cost} = 0$。也就是说,只要存在一对相邻元素”夹住” $v$,就可以零代价插入。

第四步:用差分扫描线快速判定”是否存在覆盖 $v$ 的相邻对”

每对相邻元素 $(a_k, a_{k+1})$ 对应数轴上的区间 $[\min(a_k, a_{k+1}),\ \max(a_k, a_{k+1})]$。问题变成:数轴上有 $n-1$ 个区间,查询点 $v$ 被多少个区间覆盖。

用差分 + 排序去重 + 二分查找:对每个区间 $[lo, hi]$,在 $lo$ 处 $+1$、$hi+1$ 处 $-1$,按坐标排序做前缀和。查询 $v$ 时二分找到最后一个 $\leq v$ 的坐标,其前缀和就是覆盖数。

第五步:修正移除 $a_i$ 对覆盖数的影响

移走 $a_i$ 后,剩余数组的相邻对 = 原来的 $n-1$ 对中删掉 $(a_{i-1}, a_i)$ 和 $(a_i, a_{i+1})$ 两对,再补上桥接对 $(a_{i-1}, a_{i+1})$。对覆盖计数做修正:

\[\text{cnt} = \text{cover}(v) - \text{covers}(\text{pair}_{i-1}, v) - \text{covers}(\text{pair}_i, v) + \text{covers}(\text{bridge}, v)\]

若 $\text{cnt} \geq 1$,说明可以零代价插入,$\text{cost} = 0$。

第六步:极值元素的特判

当 $v$ 是数组的严格最大或严格最小值时,没有任何相邻对能跨过 $v$,此时 $\text{cnt} = 0$。此时需要暴力枚举所有有效相邻对计算最小插入开销。但这种情况最多出现常数次(全局最大值/最小值),所以总体仍然高效。

第七步:汇总答案

枚举每个 $i$ 的 $\text{base} - \text{save} + \text{ins}$,取全局最小值,并与不操作的 $S$ 比较。

题解代码

import sys
from bisect import bisect_right


def solve(n, a):
    if n == 1:
        return 0
    if n == 2:
        return abs(a[1] - a[0])

    base = 0
    plo = [0] * (n - 1)
    phi = [0] * (n - 1)
    for k in range(n - 1):
        base += abs(a[k + 1] - a[k])
        plo[k] = a[k] if a[k] < a[k + 1] else a[k + 1]
        phi[k] = a[k] if a[k] > a[k + 1] else a[k + 1]

    # 扫描线统计:每个值被多少个相邻对的区间 [plo, phi] 覆盖
    events = {}
    for k in range(n - 1):
        events[plo[k]] = events.get(plo[k], 0) + 1
        events[phi[k] + 1] = events.get(phi[k] + 1, 0) - 1
    keys = sorted(events.keys())
    cum = 0
    cum_at = []
    for k in keys:
        cum += events[k]
        cum_at.append(cum)

    def count_cover(v):
        idx = bisect_right(keys, v) - 1
        if idx < 0:
            return 0
        return cum_at[idx]

    def covers(lo, hi, v):
        return 1 if lo <= v <= hi else 0

    ans = base
    for i in range(n):
        v = a[i]
        has_bridge = False
        blo = bhi = 0
        if i == 0:
            save = abs(a[1] - a[0])
            cnt = count_cover(v) - covers(plo[0], phi[0], v)
            head_b, tail_b = a[1], a[n - 1]
        elif i == n - 1:
            save = abs(a[n - 1] - a[n - 2])
            cnt = count_cover(v) - covers(plo[n - 2], phi[n - 2], v)
            head_b, tail_b = a[0], a[n - 2]
        else:
            blo = a[i - 1] if a[i - 1] < a[i + 1] else a[i + 1]
            bhi = a[i - 1] if a[i - 1] > a[i + 1] else a[i + 1]
            has_bridge = True
            save = abs(a[i] - a[i - 1]) + abs(a[i + 1] - a[i]) - abs(a[i + 1] - a[i - 1])
            cnt = (count_cover(v)
                   - covers(plo[i - 1], phi[i - 1], v)
                   - covers(plo[i], phi[i], v)
                   + covers(blo, bhi, v))
            head_b, tail_b = a[0], a[n - 1]

        if cnt >= 1:
            ins = 0
        else:
            # v 是数组的严格极值,暴力枚举有效相邻对求最小插入开销
            best = min(abs(v - head_b), abs(v - tail_b))
            for k in range(n - 1):
                if i > 0 and k == i - 1:
                    continue
                if i < n - 1 and k == i:
                    continue
                lo, hi = plo[k], phi[k]
                cost = 0 if lo <= v <= hi else abs(v - lo) + abs(v - hi) - (hi - lo)
                if cost < best:
                    best = cost
            if has_bridge:
                cost = 0 if blo <= v <= bhi else abs(v - blo) + abs(v - bhi) - (bhi - blo)
                if cost < best:
                    best = cost
            ins = best

        cand = base - save + ins
        if cand < ans:
            ans = cand
    return ans


data = sys.stdin.buffer.read().split()
idx = 0
T = int(data[idx]); idx += 1
out = []
for _ in range(T):
    n = int(data[idx]); idx += 1
    a = [int(data[idx + j]) for j in range(n)]
    idx += n
    out.append(str(solve(n, a)))
print('\n'.join(out), end='')

复杂度分析

时间复杂度:$O(n \log n)$,建差分前缀和需要排序 $O(n \log n)$,每次二分查询 $O(\log n)$,严格极值 fallback 最多触发常数次暴力 $O(n)$。多组合计 $O(\sum n \log n)$ 空间复杂度:$O(n)$,存所有相邻对的区间端点和差分前缀


小结

题号 考点 难度 核心思路
T1 前缀和 + 组合计数 简单 固定中间 p,左侧 o 数 $\times$ 右侧合法字符数
T2 位运算观察 简单 OR $\leq$ SUM $\leq a_i$,上界可达,答案 = 全体 OR
T3 差分扫描线 + 区间覆盖 困难 移动 = 删除节省 + 插入开销;零代价插入等价于存在覆盖对

本场笔试前两题属于思维观察题,代码量极小,关键在于快速抓住性质。第三题是典型的”拆分代价 + 快速判定”模型:先用代数推导把插入代价归约为区间覆盖判定,再用差分扫描线将暴力 $O(n^2)$ 优化到 $O(n \log n)$。面对这类题目,建议先手推小样例确认公式正确性,再考虑数据结构加速。