大厂真题 / 百度

百度 2026-7-23 笔试真题 - 算法岗

本场考试概述

考试时间:2026年7月23日

考试岗位:算法岗

难度评级:中等偏难

考点分析

  • 第一题:相邻翻转与被迫决策贪心(难度简单)
  • 第二题:前缀最值与线性动态规划(难度中等)
  • 第三题:质数筛、动态颜色维护与增量路径(难度困难)

建议策略

  • 第一题从左向右消除已经无法由后续开关修正的灯,决策是唯一的
  • 第二题先抓住“剩余藤蔓始终是一个前缀”,再把操作写成前缀长度之间的转移
  • 第三题先证明相邻两人的路径只有最后至多两步需要重算,再考虑如何查询右侧最近的目标颜色

原始公开题解中的部分公式和数据范围以 SVG 呈现,转换后未完整保留。本文不猜测缺失的数值约束,只保留能够从题意、样例与代码中确认的内容。


第 1 题:开关控制灯

题目描述

有 $n$ 盏灯和 $n$ 个开关,均从 $1$ 到 $n$ 编号。对于 $1 \le i < n$,按下开关 $i$ 会同时翻转灯 $i$ 和灯 $i+1$;开关 $n$ 只翻转灯 $n$。翻转表示亮变灭、灭变亮。

给定每盏灯的初始状态,$1$ 表示亮,$0$ 表示灭。求使所有灯熄灭所需的最少按键次数。

输入包含多组测试。每组先输入灯数 $n$,再输入 $n$ 个初始状态。

样例

输入

2
5
1 1 0 1 1
3
1 1 1

输出

2
2

第二组可以按下开关 $1$ 和开关 $3$:第一次把前两盏灯变为 0 0,第二次熄灭最后一盏灯。

思路分析

第一步:观察一盏灯由哪些开关控制

灯 $i$ 只会被开关 $i-1$ 和开关 $i$ 影响。按从左到右的顺序处理时,开关 $i-1$ 的选择已经结束,而更靠右的开关又不会影响灯 $i$。

因此,扫描到灯 $i$ 时,开关 $i$ 是最后一个还能改变它的开关。

第二步:得到被迫的贪心决策

  • 如果灯 $i$ 当前为亮,必须按下开关 $i$,否则它以后再也无法熄灭。
  • 如果灯 $i$ 当前为灭,就不能按开关 $i$,否则会把它重新点亮。

每个位置的选择都是唯一的,不存在用后续操作补救的空间,所以这一线性扫描自然得到最少操作次数。

第三步:把影响传给下一盏灯

按下开关 $i$ 后,灯 $i$ 已经确定为灭。代码无需再修改它,只需在 $i<n$ 时用异或翻转灯 $i+1$,供下一轮读取最新状态。

题解代码

import sys
input = sys.stdin.readline


def solve():
    test_count = int(input())
    answers = []

    for _ in range(test_count):
        n = int(input())
        lights = list(map(int, input().split()))
        operation_count = 0

        for i in range(n):
            if lights[i] == 1:
                operation_count += 1
                if i + 1 < n:
                    lights[i + 1] ^= 1

        answers.append(str(operation_count))

    print('\n'.join(answers))


solve()

复杂度分析

时间复杂度:每组为 $O(n)$。

空间复杂度:输入数组占 $O(n)$,除输入外的额外空间为 $O(1)$。


第 2 题:魔法藤蔓

题目描述

一根藤蔓由 $n$ 个魔法段组成,从头到尾编号为 $1$ 到 $n$,第 $i$ 段的硬度为 $a_i$。每次可以选择以下一种操作:

  1. 在当前剩余藤蔓中找到硬度最大值最后一次出现的位置,将该位置及其右侧全部剪掉;
  2. 在当前剩余藤蔓中找到硬度最小值最后一次出现的位置,将该位置及其右侧全部剪掉。

“最后一次出现”就是并列时选择下标最大的魔法段。求至少操作多少次,才能让开头的第 $1$ 段也被剪掉。

输入包含多组测试。每组先输入魔法段数量 $n$,再输入 $n$ 个硬度。

样例

输入

2
3
1 3 2
4
4 2 4 1

输出

1
2

第一组当前最大硬度是 $3$、最小硬度是 $1$。直接选择最小值所在的第一段,就会一次剪掉整根藤蔓。

思路分析

第一步:把动态过程压缩成前缀长度

每次都从某个位置开始,将它和右侧全部剪掉。因此无论执行多少次,剩余部分一定是原数组的一个前缀 $[1,k]$。

不需要记录具体剪过哪些位置,只需要记录当前前缀长度 $k$。

第二步:定义动态规划状态

令 $f[k]$ 表示从只剩前缀 $[1,k]$ 开始,到剪掉第 $1$ 段所需的最少操作次数。空前缀已经完成目标,所以 $f[0]=0$。

再定义:

  • $max_pos(k)$:前缀 $[1,k]$ 中最大值最右侧的位置;
  • $min_pos(k)$:前缀 $[1,k]$ 中最小值最右侧的位置。

选择最大值操作后,剩余前缀长度变成 $max_pos(k)-1$;选择最小值操作后,剩余长度变成 $min_pos(k)-1$。于是有:

\[f[k] = 1 + \min\left(f[max\_pos(k)-1],\ f[min\_pos(k)-1]\right)\]

第三步:扫描时同步维护前缀最值位置

从左到右扫描数组即可维护最大值、最小值及其最右下标。并列时要求选择下标最大的元素,因此更新条件必须包含等号:

  • 当前值大于或等于前缀最大值时,更新最大值位置;
  • 当前值小于或等于前缀最小值时,更新最小值位置。

每个状态只做常数次运算,总体是线性动态规划。

题解代码

import sys
input = sys.stdin.readline


def min_operations(hardness):
    n = len(hardness)
    dp = [0] * (n + 1)

    max_value = float('-inf')
    min_value = float('inf')
    max_pos = 0
    min_pos = 0

    for k in range(1, n + 1):
        value = hardness[k - 1]

        if value >= max_value:
            max_value = value
            max_pos = k
        if value <= min_value:
            min_value = value
            min_pos = k

        dp[k] = 1 + min(dp[max_pos - 1], dp[min_pos - 1])

    return dp[n]


def solve():
    test_count = int(input())
    answers = []

    for _ in range(test_count):
        n = int(input())
        hardness = list(map(int, input().split()))
        answers.append(str(min_operations(hardness)))

    print('\n'.join(answers))


solve()

复杂度分析

时间复杂度:每组为 $O(n)$。

空间复杂度:$O(n)$,用于保存动态规划数组。


第 3 题:格子行走翻转

题目描述

有一条从 $1$ 开始编号、向右无限延伸的一维格子。初始时,编号为质数的格子是黑色,其余格子是白色,因此格子 $1$ 为白色。

给定长度为 $n$、只包含字符 01 的指令串 $s$。共有 $n$ 个人依次行动,第 $k$ 个人都从格子 $1$ 出发,只执行前 $k$ 条指令:

  • 0:移动到当前位置严格右侧、编号最小的白格;
  • 1:移动到当前位置严格右侧、编号最小的黑格。

第 $k$ 个人完成移动后,会翻转终点格子的颜色。后面的人在已经发生这些翻转的棋盘上行动。

对于每组数据,输出两行:第一行是最终颜色与初始颜色不同的格子数量,第二行按升序输出这些格子的编号;若答案为空,第二行仍输出一个空行。

样例

输入

2
1
0
2
11

输出

1
4
2
2 5

当 $s=\texttt{0}$ 时,从格子 $1$ 向右遇到的第一个白格是 $4$,因此格子 $4$ 被翻转。

当 $s=\texttt{11}$ 时,第一个人落在质数格 $2$ 并把它翻成白色;第二个人重新从 $1$ 出发,依次走到黑格 $3$、黑格 $5$。最终与初始颜色不同的是格子 $2$ 和 $5$。

思路分析

第一步:直接模拟为什么太慢

第 $k$ 个人需要执行 $k$ 条指令。若每个人都从头重走,指令执行次数为 $1+2+\cdots+n=O(n^2)$。此外,每一步还要在动态变化的棋盘上寻找右侧最近的目标颜色,不能直接用静态的质数表代替。

需要同时解决两个问题:

  1. 复用相邻两个人的大部分路径;
  2. 动态维护颜色,并快速查找右侧第一个白格或黑格。

第二步:证明只需重算最后至多两步

第 $k-1$ 个人结束后,棋盘只改变了一个位置:他的终点。由于行走始终严格向右,这个终点比此前路径上的所有位置都大。

因此,第 $k$ 个人执行前 $k-2$ 条指令时,查询范围都位于旧终点左侧,结果不会受到这次翻转影响。可能变化的只有第 $k-1$ 步;重算这一步后,再计算第 $k$ 个人新增的第 $k$ 步即可。

用数组 path[j] 保存当前人员执行前 $j$ 条指令后的落点。每增加一个人,只更新 path[k-1]path[k],于是所有人的路径计算总量从平方级降为线性级。

第三步:用树状数组维护动态黑格

先在一个足够大的有限区间 $[1,B]$ 内用埃氏筛标记质数。树状数组 black_tree 维护每个位置当前是否为黑色:

  • 单点翻转:若当前位置由白变黑则加 $1$,由黑变白则减 $1$;
  • 前缀黑格数:树状数组前缀和;
  • 前缀白格数:位置数量减去前缀黑格数。

要找位置 $x$ 右侧的第一个黑格,可以先计算前缀 $[1,x]$ 中有多少黑格,再在树状数组上寻找第“下一”个黑格。

白格查询也可以变成顺序统计。对任意前缀 $[1,p]$,白格数等于 $p-black(p)$。树状数组二进制提升时,若候选前缀的白格数仍小于目标序号,就跳过该段;否则向更小的范围继续定位。

每次查找和翻转均为 $O(\log B)$。

第四步:如何处理无限棋盘

无限棋盘不能一次建完。实现从一个估计上界开始;如果查询发现区间内不存在目标颜色,就将边界扩大一倍、重新筛质数并重建树状数组,同时保留此前所有翻转状态,再继续查询。

这样正确性不依赖固定经验系数。由于每次扩容至少翻倍,重建次数是对数级的;在常见数据范围下通常只会扩容很少几次。

第五步:维护最终答案

一个格子被翻转奇数次时,最终颜色才与初始颜色不同。使用集合 changed:每次翻转一个位置时,若它已在集合中就删除,否则加入。最后排序输出即可。

题解代码

import sys
input = sys.stdin.readline


def sieve(limit):
    is_prime = bytearray(b'\x01') * (limit + 1)
    is_prime[0] = 0
    if limit >= 1:
        is_prime[1] = 0

    p = 2
    while p * p <= limit:
        if is_prime[p]:
            start = p * p
            count = (limit - start) // p + 1
            is_prime[start:limit + 1:p] = b'\x00' * count
        p += 1

    return is_prime


class DynamicBoard:
    def __init__(self, initial_limit):
        self.limit = max(16, initial_limit)
        self.flipped = set()
        self._rebuild()

    def _add(self, index, delta):
        while index <= self.limit:
            self.tree[index] += delta
            index += index & -index

    def _prefix_black(self, index):
        total = 0
        while index > 0:
            total += self.tree[index]
            index -= index & -index
        return total

    def _rebuild(self):
        self.is_prime = sieve(self.limit)
        current = bytearray(self.is_prime)
        for position in self.flipped:
            if position <= self.limit:
                current[position] ^= 1

        self.current = current
        self.tree = [0] * (self.limit + 1)
        for position in range(1, self.limit + 1):
            self.tree[position] += current[position]
            parent = position + (position & -position)
            if parent <= self.limit:
                self.tree[parent] += self.tree[position]

    def _expand(self):
        self.limit *= 2
        self._rebuild()

    def toggle(self, position):
        old_color = self.current[position]
        self.current[position] ^= 1
        self._add(position, 1 if old_color == 0 else -1)

        if position in self.flipped:
            self.flipped.remove(position)
        else:
            self.flipped.add(position)

    def _find_by_order(self, order, want_black):
        index = 0
        black_before = 0
        step = 1 << (self.limit.bit_length() - 1)

        while step:
            next_index = index + step
            if next_index <= self.limit:
                next_black = black_before + self.tree[next_index]
                next_count = next_black if want_black else next_index - next_black
                if next_count < order:
                    index = next_index
                    black_before = next_black
            step >>= 1

        return index + 1

    def next_position(self, position, want_black):
        while True:
            black_before = self._prefix_black(position)
            count_before = black_before if want_black else position - black_before

            total_black = self._prefix_black(self.limit)
            total_count = total_black if want_black else self.limit - total_black

            if total_count > count_before:
                return self._find_by_order(count_before + 1, want_black)

            self._expand()


def solve_case(instructions):
    n = len(instructions)
    board = DynamicBoard(max(64, 8 * n + 16))
    path = [0] * (n + 1)
    path[0] = 1

    for k in range(1, n + 1):
        if k > 1:
            path[k - 1] = board.next_position(
                path[k - 2], instructions[k - 2] == '1'
            )

        path[k] = board.next_position(
            path[k - 1], instructions[k - 1] == '1'
        )
        board.toggle(path[k])

    answer = sorted(board.flipped)
    return answer


def solve():
    test_count = int(input())
    output = []

    for _ in range(test_count):
        n = int(input())
        instructions = input().strip()
        answer = solve_case(instructions[:n])
        output.append(str(len(answer)))
        output.append(' '.join(map(str, answer)))

    print('\n'.join(output))


solve()

复杂度分析

设最终扩容后的边界为 $B$。

时间复杂度:筛法与历次扩容重建合计为 $O(B\log\log B)$;每个人只计算至多两个新落点并执行一次翻转,合计为 $O(n\log B)$。

空间复杂度:$O(B+n)$,用于质数标记、当前颜色、树状数组和路径数组。


小结

  • 第一题的核心不是尝试不同开关组合,而是识别“当前开关是修正当前灯的最后机会”,从而得到唯一贪心决策。
  • 第二题先把剪切过程压缩为前缀长度,再维护前缀最大值和最小值的最右位置,就能在线性时间内完成动态规划。
  • 第三题的关键是路径增量引理:相邻两个人的前 $k-2$ 步不变,只需重算最后至多两步;动态颜色查询则通过质数筛、树状数组顺序统计和按需扩容完成。