大厂真题 / 阿里巴巴

阿里 3.25 笔试真题 - 算法岗

本场考试概述

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

考点分析

  • 第一题:构造/数论(难度简单)
  • 第二题:博弈DP / Minimax(难度中等偏难)
  • 第三题:区间合并(难度中等偏难)

建议策略

  • 第一题分类讨论 n 的奇偶性,直接构造答案,O(1)
  • 第二题经典 Minimax 博弈 DP,从终点倒推,注意步数奇偶决定谁在操作
  • 第三题在线区间合并,用 bisect 维护有序区间列表,每次合并后二分查询

第一题:三星数字

题目描述

给定正整数 n,找到两个不同的正整数 a, b,使得 a != b,a < n,b < n,且 n % a == n % b。如果不存在这样的 a, b,输出 -1。

样例

输入

3
8
15
1

输出

2 4
3 5
-1

思路分析

本题的关键在于分类讨论 n 的大小和奇偶性

首先考虑边界:当 n <= 3 时,满足 a < n 且 b < n 的不同正整数对极为有限。

  • n = 1:不存在小于 1 的正整数,无解。
  • n = 2:只有 a = 1,无法找到第二个,无解。
  • n = 3:a, b 只能取 1 和 2,但 3 % 1 = 0,3 % 2 = 1,余数不同,无解。

当 n >= 4 时,分两种情况:

偶数 n >= 4:取 a = 1, b = n - 1。由于 n 是偶数且 n >= 4,n - 1 >= 3 > 1,两者不同且都小于 n。n % 1 = 0,n % (n-1) = n - (n-1) = 1?不对。换一个构造:取 a = 2, b = n/2。当 n 为偶数时 n % 2 = 0,n % (n/2) = 0(因为 n/2 整除 n)。需要 a != b,即 n/2 != 2,即 n != 4。当 n = 4 时 n/2 = 2,此时取 a = 1, b = 3:4 % 1 = 0, 4 % 3 = 1,不行。4 % 1 = 0, 4 % 2 = 0, 4 % 3 = 1。所以 a=1, b=2 即可(都余 0)。

其实更简单的构造:对任意 n >= 4,取 a = 1, b = 2。n % 1 = 0 恒成立。n % 2 = 0 当且仅当 n 为偶数。所以偶数直接输出 (1, 2)。

奇数 n >= 5:取 a = 2, b = n - 2。n % 2 = 1(n 为奇数),n % (n-2) = n - (n-2) = 2?需要更仔细地想。

实际上最简洁的构造是:取 a = (n-1)/2, b = n - 1。因为 n % (n-1) = 1,而 n % ((n-1)/2):当 n 为奇数时 n-1 为偶数,(n-1)/2 是整数,n = 2 * (n-1)/2 + 1,所以 n % ((n-1)/2) = 1。两者余数都是 1,且 (n-1)/2 != n-1(因为 n >= 5 时 (n-1)/2 >= 2 而 n-1 >= 4)。

归纳:n <= 3 输出 -1;n 为偶数输出 1 2(余数都为 0);n 为奇数且 n >= 5 输出 {(n-1)//2} {n-1}(余数都为 1)。

时间复杂度:O(1)。

import sys
input = sys.stdin.readline

def main():
    T = int(input())
    out = []
    for _ in range(T):
        n = int(input())
        if n <= 3:
            out.append("-1")
        elif n % 2 == 0:
            out.append("1 2")
        else:
            out.append(f"{(n - 1) // 2} {n - 1}")
    print("\n".join(out))

main()

第二题:该博弈了

题目描述

给定一个 n x m 的网格,每个格子是 ‘0’ 或 ‘1’。一个棋子从 (1,1) 出发,两名玩家轮流操作,每次可以将棋子向右或向下移动一步。棋子经过(到达)一个格子时,若该格子为 ‘1’ 则得分 +1,若为 ‘0’ 则得分 -1。

玩家一(天才)希望最大化最终总得分,玩家二(笨蛋)希望最小化最终总得分。两人都采用最优策略。求最优博弈下的最终得分。

注意:起点 (1,1) 的值也计入得分,但由题意,起点的得分在游戏开始时就确定了(不由任何人选择),第一步移动由玩家一执行。棋子从 (1,1) 走到 (n,m),共移动 (n-1)+(m-1) = n+m-2 步。

样例

输入

1
2 2
01
11

输出

0

思路分析

这是一道经典的零和博弈 + 网格 DP 问题,使用 Minimax 思想求解。

定义 f[i][j] 为棋子从格子 (i, j) 出发到终点 (n, m) 的最优得分差。从 (1,1) 到 (i,j) 需要走 (i-1)+(j-1) = i+j-2 步,因此到达 (i,j) 后,下一步是第 i+j-2+1 = i+j-1 步(1-indexed)。若这一步的编号为奇数,则由玩家一(最大化者)操作;若为偶数,则由玩家二(最小化者)操作。

状态转移

从 (i, j) 可以移动到 (i+1, j) 或 (i, j+1)(如果不越界)。设当前格子的贡献为 val = +1 if grid[i][j] == '1' else -1

  • f[n][m] = val(n, m)(终点,无需再移动)
  • 对于其他格子,先计算当前格子的得分 val,然后看下一步由谁操作:
    • 如果下一步由玩家一操作(最大化):f[i][j] = val + max(f[i+1][j], f[i][j+1])
    • 如果下一步由玩家二操作(最小化):f[i][j] = val + min(f[i+1][j], f[i][j+1])
    • 若只有一个方向可走,则没有选择。

从 (i,j) 出发,已经走了 step = (i-1)+(j-1) = i+j-2 步到达此处。下一步是第 step+1 = i+j-1 步。第 1 步由玩家一操作,所以第 k 步:k 为奇数时玩家一,k 为偶数时玩家二。即下一步编号 i+j-1 为奇数时(i+j 为偶数),玩家一选择;为偶数时(i+j 为奇数),玩家二选择。

答案为 f[1][1],即 f[0][0](0-indexed)。

时间复杂度:O(n * m)。

import sys
input = sys.stdin.readline

def main():
    T = int(input())
    out = []
    for _ in range(T):
        n, m = map(int, input().split())
        grid = []
        for i in range(n):
            grid.append(input().strip())

        INF = float('inf')
        f = [[0] * m for _ in range(n)]

        # 从右下角倒推
        for i in range(n - 1, -1, -1):
            for j in range(m - 1, -1, -1):
                val = 1 if grid[i][j] == '1' else -1
                if i == n - 1 and j == m - 1:
                    f[i][j] = val
                    continue

                # 下一步的编号是 (i + j + 1),1-indexed
                # 奇数步由玩家一操作(最大化),偶数步由玩家二操作(最小化)
                next_step = i + j + 1  # 1-indexed
                candidates = []
                if i + 1 < n:
                    candidates.append(f[i + 1][j])
                if j + 1 < m:
                    candidates.append(f[i][j + 1])

                if next_step % 2 == 1:
                    # 玩家一(天才)选择,最大化
                    f[i][j] = val + max(candidates)
                else:
                    # 玩家二(笨蛋)选择,最小化
                    f[i][j] = val + min(candidates)

        out.append(str(f[0][0]))
    print("\n".join(out))

main()

第三题:铁路修建

题目描述

有 n 座城市排成一行,编号 1 到 n。进行 m 天的操作,每天给出三个整数 l, r, x:

  1. 修建铁路:在城市 l 到 r 之间修建铁路,即 l 到 r 之间所有相邻城市对都被连通(如果之前已经连通则不变)。
  2. 查询:从城市 x 出发,能到达的编号最大的城市是多少。

注意操作是累积的,之前修建的铁路不会消失。

样例

输入

5 3
1 3 2
2 5 1
1 5 4

输出

3
5
5

思路分析

本题的核心是在线区间合并 + 查询某点所在区间的右端点

每次操作 (l, r, x) 表示将区间 [l, r] 内的所有城市连通。由于铁路累积,实质上我们需要维护一组不相交的已连通区间,每次加入新区间 [l, r] 时,将其与所有有重叠或相邻的已有区间合并。查询时,找到包含 x 的区间,返回其右端点。

数据结构选择

n 可达 10^9,不能开数组。但 m 只有 10^5,所以区间数量有限。用一个有序列表存储所有不相交区间(按左端点排序),利用 bisect 模块进行二分查找,实现高效的合并与查询。

合并流程

  1. bisect 找到新区间 [l, r] 可能重叠的起始位置。
  2. 向后扫描,将所有与 [l, r] 重叠或相邻(即已有区间的左端点 <= r+1 且右端点 >= l-1)的区间全部合并。
  3. 合并后的新区间左端点取所有参与合并区间左端点的最小值,右端点取最大值。
  4. 删除被合并的旧区间,插入新区间。

查询流程

bisect_right 找到左端点 <= x 的最后一个区间,检查 x 是否在该区间内。若在,返回右端点;否则 x 是孤立点,返回 x 本身。

时间复杂度:每个区间最多被合并一次后消失,总合并次数 O(m),每次操作 O(log m) 的二分查找加上合并扫描的均摊代价,整体 O(m log m)。

import sys
input = sys.stdin.readline
from bisect import bisect_left, bisect_right, insort

def main():
    n, m = map(int, input().split())
    # intervals: 存储 (left, right) 的有序列表,按 left 排序,互不重叠
    intervals = []  # list of [left, right]
    # 为了方便 bisect,单独维护一个 lefts 数组
    lefts = []

    out = []
    for _ in range(m):
        l, r, x = map(int, input().split())

        # 合并区间 [l, r]
        new_l, new_r = l, r

        # 找到第一个左端点 <= r 的区间中,可能与 [l, r] 重叠的
        # 区间 [a, b] 与 [l, r] 重叠或相邻的条件:a <= r + 1 且 b >= l - 1
        # 即:a <= new_r + 1 且 b >= new_l - 1

        # 找到 lefts 中第一个 > new_r + 1 的位置
        right_bound = bisect_right(lefts, new_r + 1)
        # 从 right_bound - 1 往前找,找到所有 b >= new_l - 1 的区间
        # 但更简单的做法:找到所有可能重叠的区间

        # 找起始位置:第一个右端点 >= new_l - 1 的区间
        # 由于我们按 left 排序,需要从 bisect_right(lefts, new_l - 1) - 1 开始往左检查
        # 但往左检查效率不高,换一种方式

        # 找所有 left <= new_r + 1 的区间(索引 0..right_bound-1)
        # 在其中找 right >= new_l - 1 的
        # 从最左边的可能重叠区间开始

        # 先找第一个可能重叠的:left <= new_r + 1 的区间中,
        # 我们从 bisect_left(lefts, new_l) 往前一个开始检查
        start = bisect_right(lefts, new_l) - 1
        if start < 0:
            start = 0

        # 收集所有要合并的区间索引
        to_remove = []
        for idx in range(start, len(intervals)):
            a, b = intervals[idx]
            if a > new_r + 1:
                break
            if b >= new_l - 1:
                # 重叠或相邻
                new_l = min(new_l, a)
                new_r = max(new_r, b)
                to_remove.append(idx)

        # 从后往前删除
        for idx in reversed(to_remove):
            intervals.pop(idx)
            lefts.pop(idx)

        # 插入新区间
        pos = bisect_left(lefts, new_l)
        intervals.insert(pos, (new_l, new_r))
        lefts.insert(pos, new_l)

        # 查询 x 所在的区间
        qi = bisect_right(lefts, x) - 1
        if qi >= 0 and intervals[qi][0] <= x <= intervals[qi][1]:
            out.append(str(intervals[qi][1]))
        else:
            out.append(str(x))

    print("\n".join(out))

main()

小结

  • 第一题构造题,分类讨论 n 的奇偶性:偶数取 (1, 2) 余数均为 0,奇数取 ((n-1)/2, n-1) 余数均为 1,n <= 3 无解
  • 第二题 Minimax 博弈 DP,关键是判断每一步由谁操作(按步数奇偶),从终点倒推即可
  • 第三题在线区间合并,用有序列表 + bisect 维护不相交区间集合,每次合并后二分查询所在区间的右端点