标签搜索

大厂笔试经验贴

wxb
wxb
2026-04-29 / 0 评论 / 2 阅读 / 正在检测是否收录...

蔚来

2026-4-19

第一题:链表排序

在线评测链接:在线评测链接:https://www.neituiya.com/oj/13/2553

题目描述

AK机拿到了许多链表,她需要将这些链表按字典序进行排序,你能帮帮她吗?

提示:链表的字典序定义如下:

  1. 若链表 $$a$$ 和链表 $$b$$ 的前 $$k$$ 个节点相同($$k$$ 可以是 $$0$$),且第 $$k+1$$ 个节点不同,那么第 $$k+1$$ 个节点数值更大的那个链表字典序更大。
  2. 若链表 $$a$$ 是链表 $$b$$ 的前缀,那么链表 $$a$$ 的字典序比链表 $$b$$ 的字典序更小。

输入描述

第一行输入一个整数 $$n(1 \le n \le 10000)$$,表示链表的数量。

接下来 $$n$$ 行,每行第一个整数 $$k(0 \le k \le 10)$$ 表示链表长度,后跟 $$k$$ 个整数 $$v_i(0 \le v_i \le 10^9)$$ 表示链表节点的值。

输出描述

输出 $$n$$ 行,每行输出排序后对应链表的节点值,空格分隔。

样例1

输入

5
2 1 2
2 2 1
1 2
1 1
1 0

输出

0
1
1 2
2
2 1

题解

题目内容拆解

给定 $$n$$ 个链表,按字典序排序后输出。$$n \le 10000$$,链表长度 $$\le 10$$。

核心观察:链表的字典序和数组的字典序规则完全一样,把链表转成数组后直接排序就行。

算法实现

链表转数组

链表是一种"只能从头往后走"的数据结构,排序时需要反复随机访问第 $$k$$ 个元素来比较,链表做不到。把每个链表的节点值按顺序存进数组,之后的比较和排序都在数组上做。链表长度最大只有 $$10$$,转换成本可以忽略。

字典序比较规则

两个数组 $$A$$ 和 $$B$$ 的字典序比较过程:从第一个位置开始逐个对比,找到第一个不同的位置 $$k$$,谁在这个位置上的值小谁排前面。

$$\text{compare}(A, B) = \begin{cases} A[k] < B[k] & \text{若第 } k \text{ 个位置首次不同} \\ |A| < |B| & \text{若一个是另一个的前缀} \end{cases}$$

如果所有对应位置都相同,较短的排前面——因为短的是长的"前缀",题意规定前缀字典序更小。

排序

用语言内置排序,传入上述比较规则即可。C++ 和 Python 的数组默认比较就是字典序,可以直接调用。Java 和 Go 没有数组的默认字典序,需要手写比较器——逻辑和上面的公式完全对应:先逐位比较,再比长度。

时空复杂度分析

  • 时间复杂度:$$O(n \cdot L \cdot \log n)$$,排序共 $$O(n \log n)$$ 次比较,每次比较最多遍历 $$L$$ 个元素,$$L \le 10$$。
  • 空间复杂度:$$O(n \cdot L)$$,存储 $$n$$ 个链表转成的数组。

Java

// 链表排序 - 字典序排序
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        // 每个链表存为int数组
        int[][] lists = new int[n][];
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int k = Integer.parseInt(st.nextToken());
            lists[i] = new int[k];
            for (int j = 0; j < k; j++) {
                lists[i][j] = Integer.parseInt(st.nextToken());
            }
        }
        // Java数组没有默认字典序,手写比较器
        Arrays.sort(lists, (a, b) -> {
            int len = Math.min(a.length, b.length);
            // 逐位比较,找到第一个不同的位置
            for (int i = 0; i < len; i++) {
                if (a[i] != b[i]) return Integer.compare(a[i], b[i]);
            }
            // 所有对应位置都相同,短的是前缀,排前面
            return Integer.compare(a.length, b.length);
        });
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < lists[i].length; j++) {
                if (j > 0) sb.append(' ');
                sb.append(lists[i][j]);
            }
            sb.append('\n');
        }
        System.out.print(sb);
    }
}

第二题:过生日

在线评测链接:在线评测链接:https://www.neituiya.com/oj/13/2554

题目描述

AK机的出生日期是 $$y$$ 年 $$m$$ 月 $$d$$ 日。AK机想知道,从 $$a$$ 年 $$b$$ 月 $$c$$ 日到 $$a'$$ 年 $$b'$$ 月 $$c'$$ 日,她一共过了多少天的生日?

假设出生的那一天也算AK机 $$0$$ 岁生日,且AK机在 $$a'$$ 年 $$b'$$ 月 $$c'$$ 日并未死亡。如果AK机是闰年 $$2$$ 月 $$29$$ 日出生,那么她在非闰年 $$2$$ 月 $$28$$ 日和闰年 $$2$$ 月 $$29$$ 日过生日。

输入描述

有多组数据,第一行输入一个整数 $$T(1 \le T \le 10)$$,代表数据组数。

对于每组数据,第一行输入三个正整数 $$y, m, d$$,表示AK机的出生年月日。

第二行输入六个正整数 $$a, b, c, a', b', c'$$,代表AK机的询问。

保证年份范围在 $$1000$$ 年至 $$9999$$ 年之间,且查询的时间合法的。查询的截止日在起始日的后面。

输出描述

一个整数,代表AK机过生日的次数。

样例1

输入

1
1993 9 21
1990 1 1 2000 12 31

输出

8

样例解释

AK机 $$1993$$ 年 $$9$$ 月 $$21$$ 日出生,查询范围是 $$1990$$ 年 $$1$$ 月 $$1$$ 日到 $$2000$$ 年 $$12$$ 月 $$31$$ 日。虽然查询起始日早于出生日,但出生前不算生日。从 $$1993$$ 年到 $$2000$$ 年,每年 $$9$$ 月 $$21$$ 日都在查询范围内,共 $$8$$ 次。

题解

题目内容拆解

统计查询区间 $$[a/b/c, \; a'/b'/c']$$ 内过生日的次数,需要处理出生前不算、闰年 $$2$$ 月 $$29$$ 日特殊过生日两个边界。

核心观察:每年最多过一次生日,年份范围最大 $$9000$$,逐年枚举即可。

算法实现

日期编码

要判断一个日期是否在某个区间内,需要一种能直接比较大小的日期表示。把日期 $$(y, m, d)$$ 编码成一个整数:

$$\text{toNum}(y, m, d) = y \times 10000 + m \times 100 + d$$

年份占高位、月份占中间、日占低位,这样两个日期的先后关系等价于编码后整数的大小关系。选这种编码而不是转天数,是因为我们只需要比较先后,不需要算间隔。

闰年特殊处理

如果出生在闰年 $$2$$ 月 $$29$$ 日,那么非闰年没有 $$2$$ 月 $$29$$ 日。题意规定此时改在 $$2$$ 月 $$28$$ 日过生日。

判断闰年的规则:能被 $$4$$ 整除且不能被 $$100$$ 整除,或者能被 $$400$$ 整除。

逐年枚举 + 三条件判断

遍历查询区间内的每一年 $$\text{yr}$$,算出该年的生日日期 $$\text{cur}$$,然后检查三个条件是否全部满足:

$$\text{cur} \ge \text{start} \quad \wedge \quad \text{cur} \le \text{end} \quad \wedge \quad \text{cur} \ge \text{birth}$$

前两个条件保证生日落在查询区间内。第三个条件排除出生之前的年份——虽然查询起始日可能早于出生日,但还没出生不能算过生日。三个条件缺一不可。

时空复杂度分析

  • 时间复杂度:$$O(T \cdot Y)$$,$$T$$ 组数据,每组枚举 $$Y$$ 年(最大约 $$9000$$),每年 $$O(1)$$ 判断。
  • 空间复杂度:$$O(1)$$,只用了几个整数变量。

C++

// 过生日 - 日期枚举
#include <bits/stdc++.h>
using namespace std;

bool isLeap(int y) {
    return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}

// 将日期转为可比较的整数
long long toNum(int y, int m, int d) {
    return (long long)y * 10000 + m * 100 + d;
}

// 逐年枚举,检查该年生日是否同时满足三个条件
int solve(int y, int m, int d, int a, int b, int c, int a2, int b2, int c2) {
    long long birth = toNum(y, m, d);
    long long start = toNum(a, b, c);
    long long end = toNum(a2, b2, c2);
    int count = 0;
    for (int yr = a; yr <= a2; yr++) {
        int bm = m, bd = d;
        // 闰年2月29日出生,非闰年改为2月28日
        if (m == 2 && d == 29 && !isLeap(yr)) {
            bd = 28;
        }
        long long cur = toNum(yr, bm, bd);
        // 三个条件:在区间内 且 不早于出生日
        if (cur >= start && cur <= end && cur >= birth) {
            count++;
        }
    }
    return count;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int y, m, d;
        cin >> y >> m >> d;
        int a, b, c, a2, b2, c2;
        cin >> a >> b >> c >> a2 >> b2 >> c2;
        cout << solve(y, m, d, a, b, c, a2, b2, c2) << "\n";
    }
    return 0;
}

得物

2026-4-26

第一题:特殊立方数

在线评测链接:https://www.neituiya.com/oj/44/2631

题目描述

存在一个 $$n$$ 位数 $$M$$,它的立方数 $$K$$ 的最后 $$n$$ 位也是 $$M$$,我们可以称这样的 $$K$$ 为特殊立方数。

例如:$$15625 = 25 \times 25 \times 25$$,因此 $$15625$$ 是一个特殊立方数。

请计算 $$[a, b]$$ 之间有多少个特殊立方数,如果一个都没有则输出 $$0$$ 。

输入描述

输入两个正整数 $$a,b(1\le a\le b\le 10^9)$$,空格隔开。

输出描述

输出在 $$[a, b]$$ 之间(包含 $$a$$ 和 $$b$$)有多少个特殊立方数,如果一个都没有则输出 $$0$$。

输入

1 200

输出

3

样例解释

样例解释:在 $$1$$ 到 $$200$$ 之间,存在 $$1、64$$ 和 $$125$$ 三个特殊立方数。题解

题目内容拆解

找出区间 $$[a, b]$$ 内所有"特殊立方数"的个数。

特殊立方数的定义: 一个数 $$K = M^3$$,且 $$K$$ 的最后 $$n$$ 位恰好等于 $$M$$(其中 $$M$$ 是 $$n$$ 位数)。

例如:$$25^3 = 15625$$,最后 2 位是 25,而 25 恰好是 2 位数,所以 15625 是特殊立方数。

算法实现

枚举底数 M,而非枚举立方数

直接枚举 $$[a,b]$$ 内的每个数判断是否为特殊立方数?范围太大($$10^9$$),不可行。

换个思路:既然特殊立方数 $$K = M^3$$,那我们枚举 M

关键观察:M 的范围很小

由于 $$K = M^3 \le 10^9$$,所以:

$$M \le \sqrt[3]{10^9} = 1000$$

只需要枚举 $$M$$ 从 1 到 1000,共 1000 个数!

对每个 M 的判断步骤:

  1. 计算立方数 $$K = M^3$$
  2. 判断是否在范围内

    • 如果 $$K < a$$:跳过
    • 如果 $$K > b$$:后面的 $$M$$ 更大,$$K$$ 只会更大,直接结束
  3. 检查是否满足"特殊"条件

    • 先计算 $$M$$ 是几位数(设为 $$n$$ 位)
    • 取 $$K$$ 的最后 $$n$$ 位:$$K \mod 10^n$$
    • 判断是否等于 $$M$$

时间复杂度分析

  • 时间复杂度: $$O(\sqrt[3]{b})$$,最多枚举 1000 个底数,每个底数的处理是 $$O(\log M)$$(计算位数)
  • 空间复杂度: $$O(1)$$,只用了几个变量

第二题:挖掘宝石

在线评测链接:https://www.neituiya.com/oj/44/2632

题目描述

AK机设计了一个挖掘宝石的小游戏。在游戏中有红宝石、蓝宝石、绿宝石等多种不同类型的宝石,当然也有昂贵的钻石。现在给出一个地图,在地图上有 $$N$$ 种不同的宝石。每一种宝石都有一颗或者多颗,同一种宝石每一颗的价值都是相同的。此外,每一种宝石都有一个挖掘时间。在给定的时间内,哪一个玩家挖掘的宝石的总价值最大就是游戏的赢家。

现在给出 $$N$$ 类不同宝石的数量以及每一类宝石中每一颗的价值和挖掘时间,并且给出一个总的游戏时间 $$T$$。在不考虑竞争对手的情况下,请问可以得到的最大价值是多少?

输入描述

单组输入。

第一行输入两个正整数 $$N, T(N \le 100, T \le 1000)$$,分别表示宝石类型的数量和总游戏时间(分钟),两者之间用空格隔开。

从第 $$2$$ 行到第 $$N+1$$ 行每一行三个正整数 $$X[i], Y[i], Z[i](X[i], Y[i], Z[i] \le 100)$$,分别表示第 $$i$$ 类宝石的数量、第 $$i$$ 类宝石中一颗宝石的价值和挖掘时间(分钟)。

输出描述

输出可以得到的最大价值。

样例1

输入

3 10
2 5 5
3 4 3
2 8 6

输出

12

题解

题目内容拆解

$$N$$ 种宝石各有数量、价值和耗时,在总时间 $$T$$ 内选取宝石使总价值最大。每种宝石数量有限(最多 $$100$$ 颗),这是经典的多重背包问题

$$N \le 100, T \le 1000$$,直接对每种宝石枚举取几颗再做 DP,复杂度 $$O(N \cdot T \cdot \max(X)) \approx 10^7$$。用二进制拆分把每种宝石的 $$X[i]$$ 颗拆成 $$O(\log X)$$ 组,转化为 01 背包,可优化到 $$O(N \cdot T \cdot \log(\max(X)))$$。

算法实现

状态方程定义

$$f[j] = \text{用 } j \text{ 分钟能获得的最大价值}$$

状态方程初始化

$$f[0] = f[1] = \cdots = f[T] = 0$$

允许背包不装满,所有容量初始为 $$0$$。

二进制拆分:将第 $$i$$ 种宝石的 $$X[i]$$ 颗拆成 $$1, 2, 4, \dots$$ 和余数这几组,每组视为一个独立物品。例如 $$X[i]=7$$ 拆为 $$1+2+4$$,三组可以组合出 $$0 \sim 7$$ 的任意取法。

状态方程转移

对每个拆分出的虚拟物品(耗时 $$w$$,价值 $$v$$),逆序遍历背包容量:

$$f[j] = \max(f[j],\; f[j - w] + v), \quad j = T, T-1, \dots, w$$

逆序遍历是 01 背包的关键:从大到小更新,保证每个虚拟物品在一轮中至多被选一次。最终答案为 $$f[T]$$。

时空复杂度分析

  • 时间复杂度:$$O(N \cdot T \cdot \log(\max(X)))$$,每种宝石拆出 $$O(\log X)$$ 个虚拟物品,每个物品遍历 $$T$$ 个容量。
  • 空间复杂度:$$O(T)$$,一维滚动 DP 数组。

第三题:计算好直线的数量

在线评测链接:https://www.neituiya.com/oj/44/2633

题目描述

AK机最近在学习计算几何,他非常喜欢连线,尤其喜欢覆盖很多个关键点的线。

现在有一个二维平面,其中有 $$n$$ 个关键点,第 $$i$$ 个点的坐标为 $$(x_i, y_i)$$,这 $$n$$ 个关键点的坐标两两不同。

作为一个连线爱好者,AK机迫不及待地开始在平面中连线,如果一条直线覆盖了至少 $$k$$ 个点,那么AK机就认为这条直线是好的。

那么AK机想知道,平面中有几条直线是好的。

输入描述

第一行两个正整数 $$n, k(1 \le k \le n \le 300)$$。

接下来 $$n$$ 行,每行两个正整数 $$x_i, y_i(1 \le x_i, y_i \le 300)$$。

输出描述

输出一行一个正整数,表示答案。

样例1

输入

5 2
0 0
2 0
0 2
2 2
1 1

输出

6

样例解释

合法的六条直线的解析式如下:$$x=0$$,$$x=2$$,$$y=0$$,$$y=2$$,$$y=x$$,$$y=2-x$$。

题解

题目内容拆解

给定 $$n$$ 个平面点,统计经过至少 $$k$$ 个点的不同直线条数。$$n \le 300$$,枚举所有 $$C(n,2)$$ 个点对的复杂度为 $$O(n^2) \approx 9 \times 10^4$$,完全可行。

两个不同的点对可能确定同一条直线,需要一种方法判断"两对点是否共线"。做法是将每条直线用一个规范化的整数三元组 $$(a,b,c)$$ 唯一表示,相同三元组就是同一条直线。

算法实现

直线规范化:两点 $$(x_1,y_1)$$、$$(x_2,y_2)$$ 确定直线 $$ax + by + c = 0$$,其中:

$$a = y_2 - y_1, \quad b = x_1 - x_2, \quad c = x_2 y_1 - x_1 y_2$$

同一条直线的 $$(a,b,c)$$ 可以同时乘任意非零常数,所以除以三者绝对值的 $$\gcd$$ 消除倍数差异,再规定首个非零系数为正,就得到唯一标识。

收集点集:用哈希表将规范化三元组映射到点下标集合。枚举所有点对 $$(i,j)$$,计算三元组后将 $$i, j$$ 加入对应集合。集合自动去重,一条经过 $$m$$ 个点的直线最终集合大小恰好为 $$m$$。

统计答案:遍历哈希表,统计点集大小 $$\ge k$$ 的直线条数。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,枚举所有点对,每对的规范化和集合插入均为 $$O(1)$$(均摊)。
  • 空间复杂度:$$O(n^2)$$,最多 $$O(n^2)$$ 条不同直线,每条直线的点集总大小之和为 $$O(n^2)$$。

2026-4-18

第一题:栈的统计

在线评测链接:https://www.neituiya.com/oj/13/2540

题目描述

给定长度均为 $$n$$ 的数组 $$A$$ 和数组 $$B$$,下标均为 $$1$$ 到 $$n$$。数组 $$A$$ 第 $$i$$ 个数记为 $$a_i$$,数组 $$B$$ 第 $$i$$ 个数记为 $$b_i$$。现在,有一个空栈 $$C$$,AK机可以进行两种操作:

  1. 当数组 $$A$$ 不为空时,把数组 $$A$$ 中下标最小的且尚未删除的数压入栈 $$C$$ 中,然后从数组 $$A$$ 中删除这个数。
  2. 当栈 $$C$$ 不为空时,设当前栈 $$C$$ 中元素个数为 $$x$$,当前栈顶元素为 $$y$$,则立刻获得 $$b_x \times y$$ 的收益,然后把栈 $$C$$ 的栈顶元素弹出。

AK机的一种操作方案必须包含恰好 $$2n$$ 次操作,且每次进行操作 $$1$$ 时必须保证数组 $$A$$ 不为空,每次进行操作 $$2$$ 时必须保证栈不为空。

定义一种操作方案的收益是该操作方案中所有第 $$2$$ 种操作获得的收益之和。请你告诉AK机,所有不同的操作方案的收益之和是多少。

认为两种操作方案不同,当且仅当存在至少一个 $$j(1 \le j \le 2n)$$,满足两个方案的第 $$j$$ 次操作的种类不同。

输入描述

第一行包含一个正整数 $$n(1 \le n \le 12)$$,表示数组 $$A$$ 和数组 $$B$$ 的长度。

第二行包含 $$n$$ 个正整数,第 $$i$$ 个正整数是 $$a_i(1 \le a_i \le 10)$$,描述了数组 $$A$$。

第三行包含 $$n$$ 个正整数,第 $$i$$ 个正整数是 $$b_i(1 \le b_i \le 10)$$,描述了数组 $$B$$。

输出描述

输出包含一行,一个整数,表示所有不同的操作方案的收益之和。

样例1

输入

2
1 2
2 3

输出

14

样例解释

样例中一共有 $$2$$ 种操作方案:

  1. 操作 $$1$$,操作 $$1$$,操作 $$2$$,操作 $$2$$:该操作方案收益为 $$3 \times 2 + 2 \times 1 = 8$$。
  2. 操作 $$1$$,操作 $$2$$,操作 $$1$$,操作 $$2$$:该操作方案收益为 $$2 \times 1 + 2 \times 2 = 6$$。

上述所有操作方案的收益之和为 $$14$$。

题解

题目内容拆解

枚举所有合法的入栈出栈序列,把每种方案的收益加起来。$$n \le 12$$ 很小,可以用二进制数记录"栈里有哪些元素",配合记忆化搜索避免重复计算。

算法实现

用二进制数表示栈的内容:元素只能按 $$a_1, a_2, \ldots, a_n$$ 的顺序入栈。用一个二进制数 $$mask$$ 表示栈里当前有哪些元素——第 $$i$$ 位(从第 $$0$$ 位开始)为 $$1$$ 表示 $$a_{i+1}$$ 在栈中。

比如 $$n=4$$ 时,$$mask = 0b1010 = 10$$ 表示 $$a_2$$ 和 $$a_4$$ 在栈中。因为栈是后进先出,入栈顺序又固定为从左到右,所以栈顶一定是 $$mask$$ 中最高的那一位对应的元素,栈中元素个数就是 $$mask$$ 中 $$1$$ 的个数。

选二进制数而不是数组来表示栈,是因为二进制数可以直接当 key 做记忆化,且入栈出栈都只是位运算,速度极快。

状态定义:$$dfs(pushed, mask)$$ 返回两个值——从当前状态走到结束的方案数 $$ways$$ 和这些方案的收益总和 $$benefit$$。其中 $$pushed$$ 表示已经入栈了多少个元素(即下一个要入栈的是 $$a_{pushed+1}$$),$$mask$$ 表示栈里当前有哪些元素。

为什么要同时追踪方案数:假设弹出栈顶获得收益 $$g$$,弹出后的状态有 $$w$$ 种走法。这 $$w$$ 种走法的每一种都会经过"弹出这一步",所以这一步对总答案的贡献是 $$w \times g$$,而不是 $$1 \times g$$。不记方案数就没法做这个乘法。

转移方程:每个状态有两种操作可选。

入栈($$pushed < n$$):把 $$a_{pushed+1}$$ 压进栈,新状态是 $$(pushed+1,\ mask\ |\ 2^{pushed})$$,入栈本身不产生收益。

出栈($$mask \neq 0$$):弹出栈顶元素。设栈中有 $$x$$ 个元素,栈顶值为 $$y$$,弹出获得收益 $$g = b_x \times y$$。设弹出后的子状态返回 $$(w_{sub}, b_{sub})$$,则出栈的贡献为:

$$benefit_{pop} = w_{sub} \times g + b_{sub}$$

第一项是"这一步的收益被后续每种方案复制一次",第二项是后续方案自身的收益。将入栈和出栈的方案数与收益分别相加,就是当前状态的返回值。

终止条件:$$pushed = n$$ 且 $$mask = 0$$,所有元素都已入栈并出栈,恰好完成 $$2n$$ 次操作,返回 $$(1, 0)$$——$$1$$ 种方案、$$0$$ 额外收益。

时空复杂度分析

  • 时间复杂度:$$O(n \cdot 2^n)$$,因为 $$pushed$$ 最多 $$n+1$$ 种取值,$$mask$$ 最多 $$2^{pushed}$$ 种,总状态数 $$\sum_{i=0}^{n} 2^i < 2^{n+1}$$,每个状态内找栈顶需 $$O(n)$$。$$n=12$$ 时约 $$5 \times 10^4$$ 次操作。
  • 空间复杂度:$$O(n \cdot 2^n)$$,用于存储记忆化结果。

Java

// 栈的统计 - 状态压缩+记忆化搜索
import java.io.*;
import java.util.*;

public class Main {
    static int n;
    static int[] a, b;
    // key -> [方案数, 收益总和]
    static HashMap<Integer, long[]> memo = new HashMap<>();

    // 返回 [方案数, 从当前状态到结束的总收益]
    static long[] dfs(int pushed, int mask) {
        int key = pushed * (1 << 13) + mask;
        if (memo.containsKey(key)) return memo.get(key);

        if (pushed == n && mask == 0) {
            long[] res = {1, 0};
            memo.put(key, res);
            return res;
        }

        long totalWays = 0, totalBenefit = 0;

        // 操作1:压入下一个元素
        if (pushed < n) {
            long[] sub = dfs(pushed + 1, mask | (1 << pushed));
            totalWays += sub[0];
            totalBenefit += sub[1];
        }

        // 操作2:弹出栈顶
        if (mask != 0) {
            int top = -1;
            for (int i = pushed - 1; i >= 0; i--) {
                if ((mask & (1 << i)) != 0) {
                    top = i;
                    break;
                }
            }
            int x = Integer.bitCount(mask);
            long gain = (long) b[x - 1] * a[top];
            long[] sub = dfs(pushed, mask ^ (1 << top));
            totalWays += sub[0];
            totalBenefit += sub[0] * gain + sub[1];
        }

        long[] res = {totalWays, totalBenefit};
        memo.put(key, res);
        return res;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine().trim());
        StringTokenizer st = new StringTokenizer(br.readLine().trim());
        a = new int[n];
        for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
        st = new StringTokenizer(br.readLine().trim());
        b = new int[n];
        for (int i = 0; i < n; i++) b[i] = Integer.parseInt(st.nextToken());

        long[] result = dfs(0, 0);
        System.out.println(result[1]);
    }
}

第二题:爬山

在线评测链接:https://www.neituiya.com/oj/13/2541

题目描述

老张爱好爬山。不过老张认为太过频繁的爬山对膝盖不太好。

老张给自己定了一个规则,原则上只能每隔一天爬山一次,如果今天爬山了,那么明天就休息一天不爬山了。但老张认为凡事都有例外,所以他给了自己 $$k$$ 次机会,在昨天已经爬山的情况下,今天仍然连续爬山!

换句话说就是老张每天最多爬山一次,原则上如果昨天爬山了那么今天就不爬山,但最多有 $$k$$ 次打破原则的机会。

爬山让人心情愉悦,所以老张每天爬山都能获得一定的愉悦值,请帮老张规划一下爬山计划来获得最大的愉悦值之和。

输入描述

第一行包含两个整数 $$n, k(1 \le n \le 2000, 1 \le k \le 1000)$$,表示老张正在计划未来 $$n$$ 天的爬山计划以及 $$k$$ 次打破原则的机会。

第二行包含 $$n$$ 个整数 $$a_i(1 \le a_i \le 10000)$$,其中 $$a_i$$ 表示接下来第 $$i$$ 天如果进行爬山可以获得的愉悦值。

输出描述

输出一行一个数,表示老张能在最佳爬山计划下获得的愉悦值之和。

样例1

输入

7 1
1 2 3 4 5 6 7

输出

19

样例解释

最优的方案是选择第 $$2, 4, 6$$ 天爬山,并在第 $$7$$ 天打破一次原则(因为第 $$6$$ 天已经爬过了,原则上不能继续爬山,需要使用一次打破原则的机会)。

题解

题目内容拆解

在 $$n$$ 天中选若干天爬山,连续两天都爬需要消耗一次"例外机会"(最多 $$k$$ 次),求最大愉悦值总和。每天的决策取决于昨天是否爬了山以及剩余例外次数,是典型的带状态 DP。

算法实现

状态定义:用三维数组 $$f[i][j][s]$$ 表示前 $$i$$ 天,已使用 $$j$$ 次例外,第 $$i$$ 天状态为 $$s$$ 时的最大愉悦值。

$$f[i][j][s],\quad i \in [0, n-1],\quad j \in [0, k],\quad s \in \{0, 1\}$$

其中 $$s=0$$ 表示第 $$i$$ 天没爬山,$$s=1$$ 表示第 $$i$$ 天爬了山。之所以需要 $$s$$ 这一维,是因为"今天要不要消耗例外"取决于昨天有没有爬——只有昨天爬了且今天也要爬,才需要消耗一次。所以必须记住"昨天的状态"才能做正确的转移。

初始化:第 $$0$$ 天只有两种选择——不爬或爬(第 $$0$$ 天爬山不需要例外,因为没有"昨天")。

$$f[0][0][0] = 0,\quad f[0][0][1] = a_0$$

其余全部设为 $$-1$$ 表示不可达。

转移方程:对第 $$i$$ 天($$i \ge 1$$),从第 $$i-1$$ 天的状态转移。因为 $$f[i]$$ 和 $$f[i-1]$$ 是数组的不同层,天然隔离,不会互相干扰。

今天不爬山,无论昨天什么状态都行,不消耗例外:

$$f[i][j][0] = \max(f[i-1][j][0],\ f[i-1][j][1])$$

今天爬山,如果昨天没爬,不需要例外:

$$f[i][j][1] = \max(f[i][j][1],\ f[i-1][j][0] + a_i)$$

今天爬山,如果昨天也爬了,消耗一次例外(需要 $$j \ge 1$$):

$$f[i][j][1] = \max(f[i][j][1],\ f[i-1][j-1][1] + a_i)$$

最终答案:遍历最后一天的所有 $$j \in [0, k]$$ 和 $$s \in \{0, 1\}$$,取 $$f[n-1][j][s]$$ 的最大值。

时空复杂度分析

  • 时间复杂度:$$O(n \times k)$$,因为外层遍历 $$n$$ 天,内层遍历 $$k+1$$ 种例外使用数,每种做常数次比较。$$n=2000, k=1000$$ 时约 $$2 \times 10^6$$ 次操作。
  • 空间复杂度:$$O(n \times k)$$,存储完整的三维 DP 数组。

C++

// 爬山 - 动态规划
#include <bits/stdc++.h>
using namespace std;

int solve(int n, int k, vector<int>& a) {
    k = min(k, n);
    // f[i][j][s]: 前i天,用了j次例外,第i天状态s(0=没爬,1=爬了)的最大愉悦值
    vector<vector<vector<int>>> f(n, vector<vector<int>>(k + 1, vector<int>(2, -1)));

    // 第0天:不爬或爬(爬不需要例外,因为没有"昨天")
    f[0][0][0] = 0;
    f[0][0][1] = a[0];

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= k; j++) {
            // 今天不爬:从昨天任意状态转移,不消耗例外
            if (f[i - 1][j][0] != -1)
                f[i][j][0] = max(f[i][j][0], f[i - 1][j][0]);
            if (f[i - 1][j][1] != -1)
                f[i][j][0] = max(f[i][j][0], f[i - 1][j][1]);
            // 今天爬,昨天没爬(不需要例外)
            if (f[i - 1][j][0] != -1)
                f[i][j][1] = max(f[i][j][1], f[i - 1][j][0] + a[i]);
            // 今天爬,昨天也爬了(消耗1次例外)
            if (j > 0 && f[i - 1][j - 1][1] != -1)
                f[i][j][1] = max(f[i][j][1], f[i - 1][j - 1][1] + a[i]);
        }
    }

    int ans = 0;
    for (int j = 0; j <= k; j++) {
        ans = max(ans, max(f[n - 1][j][0], f[n - 1][j][1]));
    }
    return ans;
}

int main() {
    int n, k;
    cin >> n >> k;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];
    cout << solve(n, k, a) << endl;
    return 0;
}

第三题:防水建材

在线评测链接:https://www.neituiya.com/oj/13/2542

题目描述

某个建筑工地上堆放着很多防水建材,它们每一块的规格都一模一样,但是每一叠都高矮不一,这些建材堆放得非常整齐而且非常紧凑,紧凑到"滴水不漏"。建材一共有 $$n$$ 行,$$m$$ 列。

现在给你一个 $$n \times m$$ 的矩阵,第 $$i$$ 行第 $$j$$ 列上的数字表示对应位置建材的数量。

特别地,最旁边的建材是没有办法形成水坑的。

某天突然天降暴雨,暴雨过后,在建材区形成了很多个小水坑。如果某一叠建材的数量比它周围上、下、左、右的建材数量少,将形成一个小水坑。相邻的两叠或者多叠建材可能会构成一个大一点的水坑。

假如这场雨下得足够大,足以让每一个水坑都装满水。现在请问,暴雨过后在建材区一共留下了多少个水坑?

输入描述

第一行包含两个正整数 $$n, m(1 \le n, m \le 100)$$,两个数字之间用空格隔开。

接下来 $$n$$ 行,每行 $$m$$ 个正整数,表示某一叠建材的数量,两个正整数之间用空格隔开。

输出描述

输出一个整数,即留下的水坑数量(存在一个水坑也没有的情况)。

样例1

输入

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

输出

1

题解

题目内容拆解

在 $$n \times m$$ 的高度矩阵中,暴雨后有些低洼位置会积水,相邻的积水格子合成一个水坑,求独立水坑的个数。本质是二维接雨水问题的变体——不求积水体积,只数水坑个数。

核心观察:一个内部格子能不能积水,取决于它是否被四周更高的建材"围住"了。边界格子永远无法积水(水会流出去),所以可以从边界向内推算每个格子的"水位"。

算法实现

为什么不能只看"比四邻都低":直觉上,一个格子比上下左右都低就会积水。但如果两个相邻格子高度不同,低的那个比高的那个还矮,但两个都被外围的高墙围住了——高的那个也会积水。所以判断积水不能只看局部,必须考虑从边界到这个格子的"最矮围墙"有多高。

接雨水2D 的直觉:把整个矩阵想象成一个浴缸,边界就是浴缸的边沿。水会从最矮的边沿口溢出。如果边沿高度为 $$h$$,那么浴缸内部所有低于 $$h$$ 的格子都会被水淹到 $$h$$ 的水位。但浴缸边沿不是等高的——有的地方高、有的地方矮,水会优先从矮的地方溢出。所以我们从最矮的边沿开始向内"灌水",逐步确定每个内部格子的水位。

第一步:从边界向内 BFS 确定积水格子:把所有边界格子放入一个最小堆(按高度排序),标记为已访问。每次从堆中取出高度最小的格子 $$(h, x, y)$$,检查它的四个邻居:如果邻居高度比 $$h$$ 低,说明邻居被"挡住"了能积水,把邻居以水位 $$h$$ 加入堆;如果邻居高度 $$\ge h$$,邻居自身成为新的围墙,以自身高度加入堆。

$$\text{水位}(nx, ny) = \max(\text{grid}[nx][ny],\ h)$$

如果 $$\text{grid}[nx][ny] < h$$,这个格子积水(建材高度低于水位)。

选最小堆而不是普通 BFS,是因为水会从最矮的边沿溢出——优先处理矮的格子才能正确模拟水的流动。如果用普通 BFS 按层扩展,可能先处理了高的边沿,错误地认为低洼处被"围住"了,实际上水早已从更矮的出口流走了。

第二步:统计水坑个数:第一步标记了所有积水格子后,用普通 BFS 把相邻的积水格子连成连通分量,每个连通分量就是一个独立的水坑,数连通分量的个数即可。

时空复杂度分析

  • 时间复杂度:$$O(nm \log(nm))$$,因为每个格子最多入堆一次,堆操作 $$O(\log(nm))$$。第二步的连通分量统计是 $$O(nm)$$,被第一步的堆操作主导。
  • 空间复杂度:$$O(nm)$$,用于访问标记数组、积水标记数组和堆。

C++

// 防水建材 - BFS+优先队列(接雨水2D)
#include <bits/stdc++.h>
using namespace std;

int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

int countPools(int n, int m, vector<vector<int>>& grid) {
    if (n <= 2 || m <= 2) return 0;

    vector<vector<bool>> visited(n, vector<bool>(m, false));
    vector<vector<bool>> water(n, vector<bool>(m, false));
    // 最小堆:{高度, x, y},优先处理矮的格子
    priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> pq;

    // 第一步:把所有边界格子加入堆
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (i == 0 || i == n - 1 || j == 0 || j == m - 1) {
                pq.push({grid[i][j], i, j});
                visited[i][j] = true;
            }
        }
    }

    // 从矮到高向内扩展,确定哪些格子能积水
    while (!pq.empty()) {
        auto [h, x, y] = pq.top();
        pq.pop();
        for (int d = 0; d < 4; d++) {
            int nx = x + dx[d], ny = y + dy[d];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m || visited[nx][ny]) continue;
            visited[nx][ny] = true;
            if (grid[nx][ny] < h) {
                // 建材低于水位,能积水
                water[nx][ny] = true;
                pq.push({h, nx, ny});  // 以水位 h 继续向内扩展
            } else {
                // 建材 >= 水位,成为新的围墙
                pq.push({grid[nx][ny], nx, ny});
            }
        }
    }

    // 第二步:BFS 统计积水格子的连通分量数
    int count = 0;
    vector<vector<bool>> seen(n, vector<bool>(m, false));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (water[i][j] && !seen[i][j]) {
                count++;
                queue<pair<int, int>> q;
                q.push({i, j});
                seen[i][j] = true;
                while (!q.empty()) {
                    auto [cx, cy] = q.front();
                    q.pop();
                    for (int d = 0; d < 4; d++) {
                        int nx = cx + dx[d], ny = cy + dy[d];
                        if (nx >= 0 && nx < n && ny >= 0 && ny < m && water[nx][ny] && !seen[nx][ny]) {
                            seen[nx][ny] = true;
                            q.push({nx, ny});
                        }
                    }
                }
            }
        }
    }
    return count;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> grid(n, vector<int>(m));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> grid[i][j];

    cout << countPools(n, m, grid) << endl;
    return 0;
}

哔哩哔哩

2026-4-11

第一题:AK机与区间

在线评测链接:https://www.neituiya.com/oj/663/2507

题目描述

AK机非常喜欢与区间相关的问题。

这一次他有一对数字 $$l$$ 和 $$r$$。AK机想找出有多少个数字 $$x(l \le x \le r)$$,满足 $$x$$ 的十进制表示的第一位数字和最后一位数字相等,当然 $$x$$ 不能含有前导 $$0$$。比如 $$989, 2, 1001$$ 就是满足条件的数字,但是 $$49, 10, 972$$ 都不满足条件。

请你帮助AK机计算出结果。

输入描述

两个数字 $$l, r(1 \le l \le r \le 10^{18})$$,用一个空格分隔。

输出描述

一个整数,满足条件的数字的个数。

样例1

输入

1 10

输出

9

样例2

输入

88 100

输出

2

题解

题目内容拆解

给定区间 $$[l, r](r \le 10^{18})$$,统计其中首位数字等于末位数字的数的个数。由于 $$r$$ 高达 $$10^{18}$$,逐个枚举不可行,需要按位分析每个数字的结构。→ 因此采用数位DP,将问题转化为求 $$f(r) - f(l-1)$$,其中 $$f(n)$$ 表示 $$[1, n]$$ 中满足条件的数的个数。

算法实现

算法主策略:对上界 $$n$$ 的十进制表示从高位到低位逐位填入数字,用记忆化搜索统计满足"首位 = 末位"的数的个数。

状态设计为 $$(pos, first, tight, started)$$:$$pos$$ 为当前填到第几位,$$first$$ 记录首位数字,$$tight$$ 表示当前是否仍贴着上界,$$started$$ 标记是否已经填过非零数字(用于跳过前导零)。在尚未开始时遇到数字 $$0$$ 就继续跳过,遇到非零数字则将其记为首位。填到最后一位时,只有当该位数字等于 $$first$$ 时才计数 $$+1$$;对于单位数(首位即末位),直接计数。最终答案为 $$f(r) - f(l-1)$$。

时空复杂度分析

  • 时间复杂度:$$O(D \times 10 \times 2 \times 2)$$,其中 $$D$$ 为数字位数(最多 $$19$$ 位),每位至多枚举 $$10$$ 个数字,$$tight$$ 和 $$started$$ 各有 $$2$$ 种状态,总状态数约 $$19 \times 10 \times 2 \times 2 = 760$$。
  • 空间复杂度:$$O(D \times 10 \times 2 \times 2)$$,用于记忆化表。

C++

// AK机与区间 - 数位DP
#include <bits/stdc++.h>
using namespace std;

// 统计 [1, n] 中首位数字等于末位数字的数的个数
long long count(long long n) {
    if (n <= 0) return 0;
    string s = to_string(n);
    int len = s.size();
    // f[pos][first][tight][started]
    map<tuple<int,int,bool,bool>, long long> memo;

    function<long long(int, int, bool, bool)> dp = [&](int pos, int first, bool tight, bool started) -> long long {
        if (pos == len) {
            return started ? 1 : 0;
        }
        auto key = make_tuple(pos, first, tight, started);
        if (memo.count(key)) return memo[key];

        int limit = tight ? (s[pos] - '0') : 9;
        long long res = 0;
        for (int d = 0; d <= limit; d++) {
            bool nt = tight && (d == limit);
            if (!started && d == 0) {
                // 跳过前导零
                res += dp(pos + 1, 0, nt, false);
            } else if (!started && d > 0) {
                // 首个非零数字,记为首位
                if (pos == len - 1) {
                    res += 1;
                } else {
                    res += dp(pos + 1, d, nt, true);
                }
            } else {
                // 首位已确定,填中间或末尾
                if (pos == len - 1) {
                    if (d == first) res += 1;
                } else {
                    res += dp(pos + 1, first, nt, true);
                }
            }
        }
        memo[key] = res;
        return res;
    };

    return dp(0, 0, true, false);
}

int main() {
    long long l, r;
    cin >> l >> r;
    cout << count(r) - count(l - 1) << endl;
    return 0;
}

第二题:斜行矩阵

在线评测链接:https://www.neituiya.com/oj/663/2508

题目描述

给出一个 $$n \times m$$ 的矩阵,假定第 $$i$$ 行第 $$j$$ 列的元素为 $$A_{i,j}$$。

请你检查对于所有的 $$A_{i,j}$$ 和 $$A_{i+1,j+1}$$ 是否都满足 $$A_{i,j} = A_{i+1,j+1}$$,若满足输出 $$Yes$$,否则输出 $$No$$。

输入描述

第一行一个正整数 $$T(1 \le T \le 100)$$,代表测试数据的组数。

每组测试数据第一行给出两个正整数 $$n, m(1 \le n \times m \le 10^6)$$,代表共有 $$n$$ 行 $$m$$ 列。$$\sum n \times m \le 10^6$$。

然后接下来 $$n$$ 行,每行 $$m$$ 个整数 $$A_{i,j}(0 \le A_{i,j} \le 10^9)$$,表示矩阵的元素。

输出描述

对于每组测试数据,若所有元素都满足要求输出一行 $$Yes$$,否则输出 $$No$$。

样例1

输入

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

输出

Yes

样例解释

对于每个元素都满足要求。

样例2

输入

1
3 2
1 2
4 2
0 4

输出

No

样例解释

$$A_{1,1} \neq A_{2,2}$$。

题解

题目内容拆解

给定 $$n \times m$$ 矩阵,判断是否所有相邻对角线位置元素相等,即 $$A_{i,j} = A_{i+1,j+1}$$。这等价于判断矩阵是否为 Toeplitz 矩阵(同一条左上到右下的对角线上元素全部相同)。数据规模 $$\sum n \times m \le 10^6$$,线性遍历即可。→ 因此采用逐元素比较

算法实现

算法主策略:对矩阵中每个位置 $$(i, j)$$($$i < n-1, j < m-1$$),检查 $$A_{i,j}$$ 是否等于 $$A_{i+1,j+1}$$。一旦发现不等即可输出 $$No$$ 并跳过当前组。全部满足则输出 $$Yes$$。

时空复杂度分析

  • 时间复杂度:$$O(\sum n \times m)$$,每组数据遍历一次矩阵。
  • 空间复杂度:$$O(n \times m)$$,存储当前矩阵。

C++

// 斜行矩阵 - 矩阵遍历
#include <bits/stdc++.h>
using namespace std;

// 判断矩阵是否满足所有对角线元素相等(Toeplitz矩阵)
bool isToeplitz(vector<vector<int>>& mat, int n, int m) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < m - 1; j++) {
            if (mat[i][j] != mat[i + 1][j + 1]) {
                return false;
            }
        }
    }
    return true;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        vector<vector<int>> mat(n, vector<int>(m));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                cin >> mat[i][j];
            }
        }
        cout << (isToeplitz(mat, n, m) ? "Yes" : "No") << "\n";
    }
    return 0;
}

科大讯飞

2026-4-11

第一题:大写字母最大化

在线评测链接:https://www.neituiya.com/oj/10/2492

题目描述

AK机拿到了一个仅由大小写字母构成的长度为 $$n$$ 的字符串,她每次操作可以将一个字符在大小写之间切换(例如将 'a' 变为 'A',或将 'Y' 变为 'y')。她希望经过恰好 $$k$$ 次操作后,大写字母的数量尽可能多。请输出最终字符串中大写字母的数量。

输入描述

在一行上输入两个整数 $$n, k(1 \le n \le 10^5, 1 \le k \le 10^9)$$。

在一行上输入一个长度为 $$n$$、由大小写字母构成的字符串 $$s$$。

输出描述

在一行上输出一个整数,表示经过恰好 $$k$$ 次操作后,最终字符串中大写字母的数量。

样例1

输入

1 3
A

输出

0

样例解释

只有一个字符,操作序列 A→a→A→a 后没有大写字母。

样例2

输入

5 3
arBrg

输出

4

样例解释

可以对第 $$1, 2, 4$$ 个字符操作,得到 "ARBRg",共有 $$4$$ 个大写字母。

题解

题目内容拆解

给定字符串恰好操作 $$k$$ 次(每次切换一个字符大小写),求大写字母最大数量。$$n \le 10^5, k \le 10^9$$,操作次数远大于字符串长度,需要分析剩余操作的奇偶性。

核心观察:每次操作只改变一个字符的大小写状态,同一字符被操作偶数次等价于没操作。→ 因此采用贪心策略:优先用操作把小写转大写,多余操作通过反复切换同一字符消耗。

算法实现

算法主策略:统计小写字母个数 $$\text{lower}$$,分两种情况处理。

若 $$k \le \text{lower}$$,操作次数不足以把所有小写都转大写,直接把 $$k$$ 个小写转为大写即可,答案为 $$n - \text{lower} + k$$。

若 $$k > \text{lower}$$,先把所有小写转大写用掉 $$\text{lower}$$ 次,剩余 $$k - \text{lower}$$ 次操作在同一个字符上来回切换。剩余次数为偶数时所有 $$n$$ 个字符都是大写;为奇数时必须牺牲一个字符,答案为 $$n - 1$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历字符串统计小写字母个数。
  • 空间复杂度:$$O(1)$$,只用常数个变量。

C++

// 大写字母最大化 - 贪心
#include <bits/stdc++.h>
using namespace std;

int solve(int n, int k, const string& s) {
    int lower = 0;
    for (char c : s) {
        if (islower(c)) lower++;
    }
    int upper = n - lower;
    // 优先把小写转大写
    if (k <= lower) return upper + k;
    // 全部转大写后,剩余操作看奇偶
    int remain = k - lower;
    return remain % 2 == 0 ? n : n - 1;
}

int main() {
    int n, k;
    cin >> n >> k;
    string s;
    cin >> s;
    cout << solve(n, k, s) << endl;
    return 0;
}

第二题:最小区间长度

在线评测链接:https://www.neituiya.com/oj/10/2493

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \ldots, a_n\}$$,且满足 $$a_1 \le a_2 \le \ldots \le a_n$$。可以至多进行一次以下操作:

选择一个区间 $$[l, r](1 \le l \le r \le n)$$,对区间内每个 $$i(l \le i \le r)$$,将 $$a_i$$ 变成 $$a_i + (r - i + 1) \times m$$。请问:为了使操作后数组存在 $$j$$ 使得 $$a_j > a_{j+1}$$,需要选择的区间长度 $$r - l + 1$$ 的最小值是多少?如果无法通过一次操作满足该要求,则输出 $$-1$$。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$,代表数据组数。

随后对于每组数据,按以下格式输入:第一行输入两个整数 $$n, m(2 \le n \le 2 \times 10^5, 1 \le m \le 10^9)$$。第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^9)$$。此外,保证所有测试数据中 $$\sum n \le 2 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数,表示所求的最小区间长度;如果无法通过一次操作使数组不再保持非严格递增,则输出 $$-1$$。

样例1

输入

2
4 2
1 2 3 3
3 1
2 6 8

输出

1
-1

样例解释

在第一个测试中,可选择区间 $$[2, 2]$$,此时 $$a_2 \leftarrow 2 + (2 - 2 + 1) \times 2 = 4$$,数组变为 $$\{1, 4, 3, 3\}$$,不再满足非严格递增,区间长度为 $$1$$。

题解

题目内容拆解

非递减数组上选一个区间 $$[l,r]$$,对区间内第 $$i$$ 个元素加 $$(r-i+1) \times m$$(越靠左加得越多),求使数组不再非递减的最小区间长度。$$n \le 2 \times 10^5$$,需要 $$O(n)$$ 解法。

核心观察:区间内相邻元素 $$a_j, a_{j+1}$$ 操作后差变为 $$a_j - a_{j+1} + m$$,右边界处 $$a_r$$ 与 $$a_{r+1}$$ 的差变为 $$a_r + m - a_{r+1}$$。只要存在某对相邻元素满足 $$a_{j+1} - a_j < m$$,选长度为 $$1$$ 的区间 $$[j,j]$$ 就能破坏非递减性。→ 因此采用贪心判断:最小差值是否小于 $$m$$。

暴力枚举所有可能的区间长度是 $$O(n^2)$$ 的,但由于区间长度不影响相邻差的增量(始终为 $$+m$$),问题退化为对原数组的单次扫描。

算法实现

算法主策略:遍历数组检查所有相邻差 $$a_{j+1} - a_j$$,若存在差值严格小于 $$m$$,答案为 $$1$$;否则无论选多大的区间都无法破坏非递减性,输出 $$-1$$。

这是因为操作对区间内每对相邻元素的净效果是 $$+m$$:原本 $$a_{j+1} - a_j \ge 0$$,操作后变为 $$a_j - a_{j+1} + m$$,要大于 $$0$$ 必须 $$a_{j+1} - a_j < m$$。这个条件与区间长度无关,只取决于原始相邻差。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,单次遍历数组。
  • 空间复杂度:$$O(n)$$,存储数组。

C++

// 最小区间长度 - 贪心
#include <bits/stdc++.h>
using namespace std;

int solve(int n, long long m, vector<long long>& a) {
    // 操作后相邻差增加m,只要存在原始差 < m 就能破坏非递减
    for (int i = 0; i < n - 1; i++) {
        if (a[i + 1] - a[i] < m) return 1;
    }
    // 所有相邻差都 >= m,无法破坏非递减
    return -1;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        long long m;
        cin >> n >> m;
        vector<long long> a(n);
        for (int i = 0; i < n; i++) cin >> a[i];
        cout << solve(n, m, a) << "\n";
    }
    return 0;
}

第三题:网格反置

在线评测链接:https://www.neituiya.com/oj/10/2494

题目描述

有一个 $$n$$ 行 $$m$$ 列的网格 $$a$$,我们使用 $$a_{ij}$$ 表示网格中从上往下数第 $$i$$ 行和从左往右数第 $$j$$ 列的单元格,初始所有单元格中的数字都为 $$0$$。

Tk将进行 $$k$$ 次操作,每次操作两个值 $$x, y$$:$$x = 1$$ 时,将第 $$y$$ 列的所有元素反置。$$x = 2$$ 时,将第 $$y$$ 行的所有元素反置。

所有操作结束后,Tk将会按照 $$a_{11}, a_{12}, \ldots, a_{1m}, a_{21}, a_{22}, \ldots, a_{2m}, \ldots, a_{n1}, \ldots, a_{nm}$$ 的顺序依次拼接形成一个二进制数字,Tk想知道这个二进制数字对应的十进制为多少,输出这个十进制数字对 $$10^9+7$$ 取模后的结果即可。

【反置】若当前数字为 $$0$$,反置后为 $$1$$;若当前数字为 $$1$$,反置后为 $$0$$。

输入描述

第一行输入三个整数 $$n, m, k(1 \le n, m \le 10^9, 1 \le k \le 2 \times 10^5)$$,表示网格大小以及Tk的操作次数。

接下来 $$k$$ 行每一行输入两个整数 $$x, y(x \in \{1, 2\}$$,当 $$x = 1$$ 时 $$1 \le y \le m$$,当 $$x = 2$$ 时 $$1 \le y \le n)$$,表示Tk的操作。

输出描述

输出一个整数,表示拼接形成的二进制数字对应的十进制对 $$10^9 + 7$$ 取模的结果。

样例1

输入

5 5 4
1 2
2 5
1 3
2 2

输出

13218195

样例解释

操作后网格为:第 $$1$$ 行 $$01100$$,第 $$2$$ 行 $$10011$$,第 $$3$$ 行 $$01100$$,第 $$4$$ 行 $$01100$$,第 $$5$$ 行 $$10011$$。拼接得到 $$0110010011011000110010011$$,对应十进制为 $$13218195$$。

样例2

输入

2 2 1
1 1

输出

10

样例解释

操作后网格为:第 $$1$$ 行 $$10$$,第 $$2$$ 行 $$10$$。拼接得到 $$1010$$,对应十进制为 $$10$$。

题解

题目内容拆解

$$n \times m$$ 网格($$n, m \le 10^9$$)初始全 $$0$$,经过 $$k$$ 次行/列翻转后,按行优先拼成二进制数,求十进制值模 $$10^9+7$$。网格最大有 $$10^{18}$$ 个格子,逐格模拟不可能。但操作最多 $$k \le 2 \times 10^5$$ 次,实际被翻转过的行列数很少。

核心观察:翻转一个格子两次等于没翻。因此一个格子最终是 $$0$$ 还是 $$1$$,只取决于它所在的行和列各被翻转了奇数次还是偶数次。翻转奇数次结果为 $$1$$,偶数次结果为 $$0$$。具体来说,格子 $$(i,j)$$ 的值等于"第 $$i$$ 行翻转次数 + 第 $$j$$ 列翻转次数"是否为奇数。行列的翻转效果彼此独立,不需要逐格计算。→ 因此采用行列分离 + 快速幂,按行和列分别统计贡献再合并。

算法实现

判断每个格子的值:先遍历所有操作,统计每一行、每一列各被翻转了多少次。翻转奇数次的行记为"翻转行",翻转奇数次的列记为"翻转列"。格子 $$(i,j)$$ 的值满足:如果行和列"一奇一偶"(恰好一个被翻转了奇数次),格子值为 $$1$$;如果"同奇"或"同偶",格子值为 $$0$$。

每行的二进制值:一行有 $$m$$ 个格子,从左到右构成一个 $$m$$ 位二进制数。第 $$j$$ 列(从 $$1$$ 开始编号)对应二进制的第 $$m-j$$ 位,位权为 $$2^{m-j}$$。对于一个"翻转行",恰好是那些"非翻转列"的位置为 $$1$$,这些位的位权之和记为 $$S_{\text{flip}}$$。对于一个"非翻转行",恰好是那些"翻转列"的位置为 $$1$$,位权之和记为 $$S_{\text{col}}$$。二者满足 $$S_{\text{flip}} + S_{\text{col}} = 2^m - 1$$(因为 $$m$$ 位全 $$1$$ 的二进制数等于 $$2^m - 1$$)。

每行在总数中的权重:把所有行按顺序拼成一个 $$n \times m$$ 位的大二进制数,第 $$i$$ 行(从 $$1$$ 开始)整体的权重因子为 $$2^{(n-i) \times m}$$,因为第 $$i$$ 行后面还有 $$(n-i)$$ 行、每行 $$m$$ 位。将所有翻转行的权重因子加起来记为 $$A$$,所有非翻转行的权重因子加起来记为 $$B$$,则最终答案为 $$S_{\text{flip}} \times A + S_{\text{col}} \times B$$。

高效计算:$$A$$ 只需对少量翻转行做快速幂求和。$$A + B$$ 等于所有 $$n$$ 行的权重因子总和,是一个公比为 $$2^m$$ 的等比数列,用求和公式 $$(2^{nm} - 1) / (2^m - 1)$$ 计算,其中除法在模运算下通过费马小定理转化为乘以逆元(这是数论中的标准技巧,可以当成黑盒公式使用:$$a / b \bmod P = a \times b^{P-2} \bmod P$$)。大指数的幂运算用快速幂(反复平方法)在 $$O(\log \text{exp})$$ 时间内完成。

时空复杂度分析

  • 时间复杂度:$$O(k \log(nm))$$,遍历 $$k$$ 次操作后对至多 $$k$$ 个行/列各做一次快速幂。
  • 空间复杂度:$$O(k)$$,哈希表存储行列翻转次数。

C++

// 网格反置 - 数学、快速幂
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1e9 + 7;

// 快速幂:反复平方法求 base^exp % mod
long long power(long long base, long long exp, long long mod) {
    long long res = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) res = res * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return res;
}

// 等比数列求和:1 + r + r^2 + ... + r^(cnt-1)
// 利用公式 (r^cnt - 1)/(r - 1),除法用费马小定理转乘逆元
long long geoSum(long long r, long long cnt) {
    if (cnt == 0) return 0;
    if (r == 1) return cnt % MOD;
    long long num = (power(r, cnt, MOD) - 1 + MOD) % MOD;
    long long den = power((r - 1 + MOD) % MOD, MOD - 2, MOD);
    return num % MOD * den % MOD;
}

long long solve(long long n, long long m, int k,
                unordered_map<long long, int>& rowFlip,
                unordered_map<long long, int>& colFlip) {
    // 翻转列的二进制位权之和:第j列对应位权 2^(m-j)
    long long S_C = 0;
    for (auto& [j, cnt] : colFlip) {
        if (cnt % 2 == 1) {
            S_C = (S_C + power(2, m - j, MOD)) % MOD;
        }
    }

    // 翻转行的权重因子之和:第i行权重 2^((n-i)*m)
    long long A = 0;
    for (auto& [i, cnt] : rowFlip) {
        if (cnt % 2 == 1) {
            // 指数可能极大,用费马小定理对 P-1 取模后再快速幂
            long long exp = (n - i) % (MOD - 1) * (m % (MOD - 1)) % (MOD - 1);
            A = (A + power(2, exp, MOD)) % MOD;
        }
    }

    // 非翻转列的位权之和 = 全1二进制值 - 翻转列位权
    long long S_all = (power(2, m, MOD) - 1 + MOD) % MOD;
    long long S_notC = (S_all - S_C + MOD) % MOD;

    // 全部n行的权重因子总和,公比为 2^m 的等比数列
    long long pow2m = power(2, m, MOD);
    long long totalPow = geoSum(pow2m, n);
    // 非翻转行的权重因子之和
    long long B = (totalPow - A + MOD) % MOD;

    // 翻转行贡献非翻转列的值,非翻转行贡献翻转列的值
    return (S_notC % MOD * A % MOD + S_C % MOD * B % MOD) % MOD;
}

int main() {
    long long n, m, k;
    cin >> n >> m >> k;

    // 统计每行/列被翻转的次数
    unordered_map<long long, int> rowFlip, colFlip;
    for (int i = 0; i < k; i++) {
        int x;
        long long y;
        cin >> x >> y;
        if (x == 1) colFlip[y]++;
        else rowFlip[y]++;
    }

    cout << solve(n, m, k, rowFlip, colFlip) << endl;
    return 0;
}

阿里系(不含蚂蚁)

2026-4-25-AI研发岗

第一题:蝴蝶乐园

在线评测链接:https://www.neituiya.com/oj/7/2608

第二题:按位与

在线评测链接:https://www.neituiya.com/oj/7/2609

第三题:区间第k小

在线评测链接:https://www.neituiya.com/oj/7/2610

2026-4-25-算法岗

第一题:插入顺序

在线评测链接:https://www.neituiya.com/oj/7/2605

第二题:矩阵计数

在线评测链接:https://www.neituiya.com/oj/7/2606

第三题:取模仪式

在线评测链接:https://www.neituiya.com/oj/7/2607

2026-4-18-工程岗

第一题:超不过k的最大矩形

在线评测链接:https://www.neituiya.com/oj/7/2555

题目描述

给定一个 $$n \times m$$ 的 0/1 矩阵。你可以任选一个矩形子区域(连续若干行、连续若干列),如果该子矩形中 1 的数量不超过 $$k$$,则称其为"合法"。请计算:所有合法子矩形中,面积(行数乘以列数)最大的值。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 表示数据组数。随后对每组数据:

第一行输入三个整数 $$n, m, k(1 \le n, m \le 10^3, 1 \le k \le n \times m \le 5 \times 10^3)$$。

此后 $$n$$ 行,每行输入 $$m$$ 个整数 $$a_{i,j} \in \{0, 1\}$$ 表示矩阵元素。

除此之外,保证单个测试文件的 $$n \times m$$ 之和不超过 $$10^4$$。

输出描述

对于每一组测试数据,输出一个整数表示最大合法矩形的面积,每组一行。

样例1

输入

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

输出

6
4

样例解释

第一组数据:选择第 $$1$$ 到 $$3$$ 行、第 $$1$$ 到 $$2$$ 列的子矩形,包含 $$2$$ 个 $$1$$ 不超过 $$k=2$$,面积为 $$3 \times 2 = 6$$,是最大合法面积。

第二组数据:整个矩阵包含 $$4$$ 个 $$1$$ 不超过 $$k=4$$,面积为 $$2 \times 2 = 4$$。

第二题:那只能回文了

在线评测链接:https://www.neituiya.com/oj/7/2556

题目描述

给定 $$n$$ 个仅由数字字符组成的字符串。你想从中选出两个下标不同的字符串,且下标较小的字符串放在前面、下标较大的字符串放在后面(只允许按 $$s_i + s_j$$ 的顺序拼接,要求 $$i < j$$)。如果按这个顺序拼接后得到的是回文串,则认为这对下标是一种合法的选法。

请计算一共有多少种合法的下标对 $$(i, j)(i < j)$$。

回文串的含义是:从左到右与从右到左读完全相同。

输入描述

第一行输入一个整数 $$n(1 \le n \le 5 \times 10^5)$$ 表示字符串数量。

此后 $$n$$ 行,第 $$i$$ 行输入一个长度为 $$\text{length}(s_i)$$、仅由数字字符(0\~9)构成的字符串 $$s_i(1 \le \text{length}(s_i) \le 5 \times 10^5)$$。

保证所有字符串的长度之和不超过 $$5 \times 10^5$$。

输出描述

输出一个整数,表示不同下标的选法数量。

样例1

输入

5
001
100
909
909
34

输出

2

样例解释

{001, 100} 可得 001100,是回文。两个 909 组成一对,拼接后 909909 为回文。除以上两种情况外,不存在其他满足条件的下标对。

第三题:合法串

在线评测链接:https://www.neituiya.com/oj/7/2557

题目描述

给定 $$n$$ 个仅由数字字符组成的字符串。你想从中选出两个下标不同的字符串,且下标较小的字符串放在前面、下标较大的字符串放在后面(只允许按 $$s_i + s_j$$ 的顺序拼接,要求 $$i < j$$)。如果按这个顺序拼接后得到的是回文串,则认为这对下标是一种合法的选法。

请计算一共有多少种合法的下标对 $$(i, j)(i < j)$$。

回文串的含义是:从左到右与从右到左读完全相同。

输入描述

第一行输入一个整数 $$n(1 \le n \le 5 \times 10^6)$$ 表示字符串数量。

此后 $$n$$ 行,第 $$i$$ 行输入一个长度为 $$\text{length}(s_i)$$、仅由数字字符(0\~9)构成的字符串 $$s_i(1 \le \text{length}(s_i) \le 5 \times 10^6)$$。

保证所有字符串的长度之和不超过 $$5 \times 10^6$$。

输出描述

输出一个整数,表示不同下标的选法数量。

样例1

输入

5
001
100
909
909
34

输出

2

样例解释

{001, 100} 可得 001100,是回文。两个 909 组成一对,拼接后 909909 为回文。除以上两种情况外,不存在其他满足条件的下标对。

2026-4-18-算法岗

第一题:这是什么博弈

在线评测链接:https://www.neituiya.com/oj/13/2543

题目描述

笨蛋同学正在和天才同学博弈。现在在桌子上有 $$n$$ 份食物,$$n$$ 为偶数。第 $$i$$ 份食物的美味度为 $$a_i$$。

由笨蛋同学先手,笨蛋同学每次会拿走当前桌子上美味度最高的食物。随后,天才同学为了达成自己的目标,会从桌上剩余的食物中选择并拿走一份;两人交替行动,直至桌上无食物。

天才同学的目标是:让笨蛋同学吃到的食物总美味度与自己吃到的食物总美味度的差值尽量小(可能为负数)。天才同学绝顶聪明。

请问在两人都采取各自的策略下,这个差值最小是多少?

输入描述

每个测试文件均包含多组测试数据:第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,代表数据组数。

每组测试数据描述如下:

第一行输入一个正整数 $$n(2 \le n \le 2 \times 10^5)$$,表示食物份数。保证 $$n$$ 为偶数。

第二行输入 $$n$$ 个正整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^9)$$,表示食物的美味度。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$4 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行。输出一个整数,表示最小的差值。

样例1

输入

3
2
10 20
4
2 1 5 8
6
1 1 1 1 1 1

输出

10
4
0

样例解释

对于第一组样例:笨蛋同学先手,取走美味度为 $$20$$ 的食物;天才同学取走剩下美味度为 $$10$$ 的食物;最终差值为 $$20 - 10 = 10$$。

题解

题目内容拆解

两人交替取食物,笨蛋每次取最大值,天才自由选择目标是让差值最小,求最终差值。

算法实现

核心观察:笨蛋每次必须取当前最大值,这是固定策略。天才可以自由选择,他的最优策略是什么?

将食物按美味度从大到小排序为 $$a[0] \ge a[1] \ge \cdots \ge a[n-1]$$。第一轮笨蛋取 $$a[0]$$(最大),此时天才应该取什么?如果天才取 $$a[1]$$(第二大),下轮笨蛋只能取 $$a[2]$$;但如果天才取了更小的 $$a[k]$$($$k > 1$$),下轮笨蛋就能取到 $$a[1]$$,反而拿到了更大的值。因此天才每轮都应取当前第二大的元素,这样能最大程度压低笨蛋后续能拿到的值。

按这个策略推演:笨蛋依次取 $$a[0], a[2], a[4], \ldots$$(偶数下标),天才依次取 $$a[1], a[3], a[5], \ldots$$(奇数下标)。最终差值为偶数下标之和减奇数下标之和。

实现上只需排序后,偶数位加、奇数位减,累加即可。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,排序的开销。
  • 空间复杂度:$$O(n)$$,存储数组。

第二题:最短就餐距离

在线评测链接:https://www.neituiya.com/oj/13/2544

题目描述

在一条无限长的美食街上,每个整数坐标处都有一家餐厅。你当前位于位置 $$p$$。

现在共有 $$n$$ 个人(包括你自己),第 $$i$$ 个人不愿意去区间 $$[l_i, r_i]$$ 内的任何餐厅。你需要选择一个餐厅位置 $$x \in \mathbb{Z}$$,使得对所有人都"可接受"(即 $$x \notin [l_i, r_i]$$ 对所有 $$i$$ 成立),并且使你走的距离 $$|x - p|$$ 最小。请输出这个最小距离。

输入描述

每个测试文件均包含多组测试数据:第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,代表数据组数。每组测试数据描述如下:

每组数据第一行输入两个整数 $$n, p(1 \le n \le 2 \times 10^5, -10^6 \le p \le 10^6)$$。

接下来 $$n$$ 行,每行输入两个整数 $$l_i, r_i(-10^6 \le l_i \le r_i \le 10^6)$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

输出 $$T$$ 行,每行输出一个整数,为最小距离。

样例1

输入

3
2 5
1 3
7 8
1 10
10 20
3 0
-2 1
2 4
6 9

输出

0
1
3

样例解释

样例 1:禁用区间为 $$[1, 3] \cup [7, 8]$$,位置 $$p = 5$$ 可直接就餐,距离为 $$0$$。

样例 2:最近不在禁区内的餐厅位置是 $$9$$,距离为 $$|9 - 10| = 1$$。

样例 3:禁用区间合并后为 $$[-2, 4] \cup [6, 9]$$,位置 $$p = 0$$ 在 $$[-2, 4]$$ 内,左侧最近可用位置 $$-3$$(距离 $$3$$),右侧最近可用位置 $$5$$(距离 $$5$$),最小距离为 $$3$$。

题解

题目内容拆解

数轴上有若干禁止区间,找离当前位置 $$p$$ 最近的、不被任何区间覆盖的整数点。

核心观察:如果 $$p$$ 本身就不在任何禁止区间内,答案是 $$0$$。否则 $$p$$ 被某段连续禁区困住,最近出口只在这段禁区的左右两端。

所以关键是把所有可能重叠的小区间合并成若干互不相交的大区间,然后判断 $$p$$ 落在哪个大区间里。

算法实现

区间合并:把所有禁止区间按左端点从小到大排序,然后从前往后扫描,遇到重叠或相邻的区间就合并成一个更大的区间。

判断两个区间能否合并的条件是:前一个区间的右端点加 $$1$$ 大于等于后一个区间的左端点。

$$r_{\text{prev}} + 1 \ge l_{\text{curr}}$$

之所以要加 $$1$$,是因为这里是整数坐标——$$[1, 3]$$ 和 $$[4, 6]$$ 之间没有任何整数空隙($$3$$ 的下一个整数就是 $$4$$),必须合并成 $$[1, 6]$$。如果不加这个 $$1$$,就会漏掉这种紧挨着的情况,误以为 $$3$$ 和 $$4$$ 之间有空位。

定位 $$p$$ 所在区间:合并后的区间已经按左端点有序,用二分查找找到左端点不超过 $$p$$ 的最后一个区间,检查 $$p$$ 是否落在它的范围内。

选二分而不是线性扫描,是因为合并后可能仍有很多区间,二分把查找从 $$O(n)$$ 降到 $$O(\log n)$$。

计算最短距离:如果 $$p$$ 不在任何合并区间内,答案是 $$0$$。如果 $$p$$ 落在合并区间 $$[L, R]$$ 内,最近的合法位置只可能是左边的 $$L - 1$$ 或右边的 $$R + 1$$,答案取两者中较近的那个。

$$\text{ans} = \min(p - L + 1, \; R + 1 - p)$$

$$p - L + 1$$ 是从 $$p$$ 往左走到 $$L - 1$$ 的步数,$$R + 1 - p$$ 是往右走到 $$R + 1$$ 的步数。合并后的区间之间一定存在间隙,所以 $$L - 1$$ 和 $$R + 1$$ 保证不在任何禁区内。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,因为排序 $$n$$ 个区间需要 $$O(n \log n)$$,合并和二分查找都是 $$O(n)$$ 和 $$O(\log n)$$,排序主导。
  • 空间复杂度:$$O(n)$$,用于存储合并后的区间列表。

第三题:最大权值

在线评测链接:https://www.neituiya.com/oj/13/2545

题目描述

给定一个长度为 $$n$$ 的整数数组 $$a_1, a_2, \ldots, a_n$$,以及两个长度为 $$n$$ 的排列 $$b_1, b_2, \ldots, b_n$$ 与 $$c_1, c_2, \ldots, c_n$$。

你可以至多各执行一次下列两种操作,执行顺序可以任意:

  1. 选择一个整数 $$x(1 \le x \le n)$$,获得权值 $$\sum_{i=1}^ a_{b_i}$$。
  2. 选择一个整数 $$y(1 \le y \le n)$$,依次将 $$a_{c_1}, a_{c_2}, \ldots, a_{c_y}$$ 修改为 $$0$$。

你也可以选择不执行其中某个操作;若两种操作均不执行,则权值为 $$0$$。

请输出能够获得的最大权值。

排列:长度为 $$n$$ 的排列是由 $$1 \sim n$$ 这 $$n$$ 个整数按任意顺序组成的数组,其中每个整数恰好出现一次。

输入描述

每个测试文件均包含多组测试数据:第一行输入一个整数 $$T(1 \le T \le 10^4)$$,代表数据组数。每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示数组和排列的长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(-10^9 \le a_i \le 10^9)$$,表示数组 $$a$$。

第三行输入 $$n$$ 个整数 $$b_1, b_2, \ldots, b_n(1 \le b_i \le n)$$,表示排列 $$b$$。

第四行输入 $$n$$ 个整数 $$c_1, c_2, \ldots, c_n(1 \le c_i \le n)$$,表示排列 $$c$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示能够获得的最大权值。

样例1

输入

3
3
2 3 4
1 3 2
2 1 3
4
5 -10 6 3
1 2 3 4
2 4 1 3
1
-1
1
1

输出

6
14
0

样例解释

在第二组样例中:若先选择 $$y = 1$$,将 $$a_2$$ 置为 $$0$$,得到数组 $$a = [5, 0, 6, 3]$$;随后选择 $$x = 4$$,可获得权值 $$\sum_{i=1}^{4} a_{b_i} = 5 + 0 + 6 + 3 = 14$$;这是本组数据的最优解。

题解

题目内容拆解

选排列 $$b$$ 的一段前缀求和作为得分,可以在求和前先把排列 $$c$$ 的一段前缀对应位置清零(把负数变成 $$0$$ 来增加得分),求最大得分。

核心观察:操作顺序只有"先清零再求和"才有意义——如果先求和再清零,清零不影响已到手的分数。所以只需考虑两种情况:不清零直接求和,或先清零再求和。

算法实现

问题转化——把"清零"翻译成"减法":选定 $$b$$ 的前 $$x$$ 个元素求和,再选 $$c$$ 的前 $$y$$ 个元素清零。最终得到的权值等于 $$b$$ 前缀和,减去那些"既被选中求和、又被清零"的元素原始值。

$$\text{value}(x, y) = \underbrace{\sum_{i=1}^ a_{b_i}}_{\text{b的前缀和}} - \underbrace{\sum_{\substack{k:\, \text{pos}_b(k) \le x \\ \text{pos}_c(k) \le y}} a_k}_{\text{被清零抵消的部分}}$$

其中 $$\text{pos}_b(k)$$ 表示元素 $$k$$ 在排列 $$b$$ 中排第几,$$\text{pos}_c(k)$$ 同理。一个元素只有同时出现在 $$b$$ 的前 $$x$$ 个和 $$c$$ 的前 $$y$$ 个中,它的值才会先被加上又被清掉,产生抵消。

把被抵消的部分记为 $$f(y)$$。对于固定的 $$x$$,$$b$$ 的前缀和是常数,所以最大化权值就等价于最小化 $$f(y)$$。

扫描 $$b$$ 的前缀:从 $$x = 1$$ 到 $$n$$ 逐步扩大 $$b$$ 的前缀。每增加一个 $$x$$,新加入的元素 $$k = b_x$$ 就可能参与 $$f(y)$$ 的计算。

把每个元素的值 $$a_k$$ 放到一根按 $$c$$-位置排列的数轴上。$$f(y)$$ 就是这根数轴上前 $$y$$ 个位置的元素值之和(只算已经被加入的那些元素)。最小化 $$f(y)$$ 就是在这根数轴上找一个前缀和最小的位置。

选择扫描 $$b$$ 而不是暴力枚举 $$(x, y)$$ 的所有组合,是因为暴力是 $$O(n^2)$$,而扫描 $$b$$ 配合线段树可以做到 $$O(n \log n)$$。

线段树维护最小前缀和:线段树覆盖 $$1$$ 到 $$n$$ 的位置(对应 $$c$$-位置轴),每个节点维护两个值。

$$\text{sum}$$:这段区间内所有已插入元素的值之和。

$$\text{minPre}$$:这段区间内所有前缀和中的最小值。

当两个子区间合并时,合并规则为:

$$\text{sum} = \text{left.sum} + \text{right.sum}$$

$$\text{minPre} = \min(\text{left.minPre}, \; \text{left.sum} + \text{right.minPre})$$

第二个公式的含义:整个区间的最小前缀和,要么完全落在左半区间内($$\text{left.minPre}$$),要么跨过左半区间延伸到右半区间(先加上左半区间的总和 $$\text{left.sum}$$,再加上右半区间的某个前缀,取右半最小前缀 $$\text{right.minPre}$$)。两者取较小值。

每次扫描到 $$x$$ 时,在位置 $$\text{pos}_c(b_x)$$ 插入值 $$a_{b_x}$$,然后查询根节点的 $$\text{minPre}$$。$$\text{minPre}$$ 和 $$0$$ 取较小值就是当前最优的 $$f(y)$$ 最小值($$0$$ 对应不做清零操作,即 $$y = 0$$)。用 $$\text{prefix\_b}[x] - \min(0, \text{minPre})$$ 更新全局最大值。

最终答案:所有 $$x$$ 中的最大值再和 $$0$$ 取较大值(两种操作都不做时权值为 $$0$$)。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,因为扫描 $$n$$ 个元素,每次在线段树上做一次单点更新 $$O(\log n)$$ 和一次全局查询 $$O(1)$$,总计 $$O(n \log n)$$。
  • 空间复杂度:$$O(n)$$,线段树需要 $$4n$$ 个节点。

2026-4-15-AI算法岗

第一题:富豪

在线评测链接:https://www.neituiya.com/oj/7/2524

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \ldots, a_n\}$$,你可以进行以下操作若干次(可以不进行操作):

选择一个下标 $$i(1 \le i < n)$$,将 $$a_i$$ 与 $$a_{i+1}$$ 的符号分别翻转(即 $$a_i \leftarrow -a_i, a_{i+1} \leftarrow -a_{i+1}$$)。

请你计算,经过若干次操作后,数组元素之和的最大值。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$,代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示数组长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(-10^9 \le a_i \le 10^9)$$,表示数组元素。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示通过若干次操作后数组元素之和的最大值。

样例1

输入

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

输出

10
6

题解

题目内容拆解

同时翻转相邻两元素的符号,求数组之和的最大值。

核心观察:一次操作同时翻转两个符号,负数个数要么 $$+2$$、$$-2$$、要么不变——奇偶性始终守恒。偶数个负数可以全消掉,奇数个必须留一个。

算法实现

采用贪心,先算出所有元素的绝对值之和 $$S$$ 和负数个数 $$cnt$$。

偶数情况:$$cnt$$ 为偶数时,每次挑一对负数把它们同时变正,最终全部非负,答案就是 $$S$$。

有零情况:数组中存在 $$0$$ 时,可以对 $$0$$ 和一个负数执行操作——负数变正,$$0$$ 翻转后还是 $$0$$,等价于"免费消掉一个负数"。所以即使 $$cnt$$ 为奇数,有 $$0$$ 就能全部非负,答案也是 $$S$$。

奇数无零情况:$$cnt$$ 为奇数且没有 $$0$$,必须保留恰好一个负数。为了让损失最小,让绝对值最小的那个元素当负数,答案为:

$$S - 2 \times \min_i |a_i|$$

减 $$2$$ 倍是因为绝对值之和 $$S$$ 已经把这个元素算成正的了,现在要改回负数,一来一回差了 $$2$$ 倍。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历数组一次统计绝对值之和、负数个数、最小绝对值。
  • 空间复杂度:$$O(1)$$,只需常数个变量。

第二题:何物为真

在线评测链接:https://www.neituiya.com/oj/7/2525

题目描述

你在玩一个"真假话"游戏。一共有 $$n$$ 句话,部分句子的真假你已经知道,其余句子未知。我们用 $$1$$ 表示真话、$$0$$ 表示假话、$$-1$$ 表示未知。你还知道一个规则:在任意连续的 $$k$$ 句话中,最多只有 $$1$$ 句是假话。请你计算:共有多少种不同的填充方式(把所有 $$-1$$ 替换为 $$0$$ 或 $$1$$)能够满足这个规则。由于答案较大,请对 $$10^9 + 7$$ 取模后输出。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,表示数据组数。此后每组测试数据依次输入:

第一行输入两个整数 $$n, k(1 \le k \le n \le 2 \times 10^5)$$,分别表示句子数量、连续检查的窗口长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n$$,其中 $$a_i \in \{-1, 0, 1\}$$。这里 $$-1$$ 表示未知,$$1$$ 表示真话,$$0$$ 表示假话。保证给出的已知信息本身不违反规则,即在任意长度为 $$k$$ 的连续段中,已知的假话数量至多为 $$1$$。

除此之外,保证单个测试文件中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行输出满足条件的填充方案数,结果对 $$10^9 + 7$$ 取模。

样例1

输入

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

输出

13
1
16

样例解释

第二组:$$n = 4, k = 3$$,已知第 $$1$$ 句为真,第 $$3$$ 句为假。任何长度为 $$3$$ 的连续段内最多 $$1$$ 个 $$0$$,因此第 $$2$$、第 $$4$$ 句都不能再为假,只能填为真,只有一种方案。

题解

题目内容拆解

给定部分已知真假的 $$n$$ 句话,在"任意连续 $$k$$ 句最多 $$1$$ 句假话"的约束下,计算合法填充方案数。

核心观察:约束等价于任意两句假话之间的间距至少为 $$k$$。

为什么?如果两句假话间距 $$< k$$,它们一定同时出现在某个长度为 $$k$$ 的窗口里,违反"最多 $$1$$ 句假话"。反过来,间距 $$\ge k$$ 就不会出现在同一个窗口中。

算法实现

采用动态规划,逐个位置从左到右决定每句话的真假。

状态方程定义

设 $$f[i]$$ 表示只考虑前 $$i$$ 句话时,所有合法填充方案的总数。

状态方程初始化

$$f[0] = 1$$,表示"零句话"只有一种方案(什么都不填)。

状态方程转移

对于第 $$i$$ 句,它要么填真,要么填假,分两种情况累加。

填真(需要 $$a_i \ne 0$$,即第 $$i$$ 句没被强制为假):前 $$i-1$$ 句的任何合法方案后面接一个"真"都仍然合法,贡献 $$f[i-1]$$。

填假(需要 $$a_i \ne 1$$,即第 $$i$$ 句没被强制为真):根据间距约束,第 $$i$$ 句为假时,前面 $$k-1$$ 个位置(即 $$[i-k+1, i-1]$$)必须全部为真——这意味着这段区间内每个未知位置只能填真,没有选择余地。如果这段区间内存在已知假话($$a_j = 0$$),就和"全部为真"矛盾,第 $$i$$ 句不能填假。否则,方案数等于 $$f[\max(0, i-k)]$$:跳过这 $$k-1$$ 个被锁死为真的位置,直接继承更前面的方案数。

汇总写成一个公式:

$$f[i] = [a_i \ne 0] \cdot f[i-1] \;+\; [a_i \ne 1,\; \text{窗口内无已知假话}] \cdot f[\max(0,\, i-k)]$$

前缀和加速判断:判断窗口 $$[i-k+1, i-1]$$ 内有没有已知假话,预处理一个数组记录前 $$i$$ 个位置中有多少个 $$0$$,对任意区间做一次减法即可 $$O(1)$$ 得到答案。最终结果为 $$f[n]$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,每个位置的转移为常数时间。
  • 空间复杂度:$$O(n)$$,存储 $$f$$ 数组和前缀和数组。

第三题:连连看

在线评测链接:https://www.neituiya.com/oj/7/2526

题目描述

有一排 $$n$$ 个位置,从左到右编号为 $$1, 2, \ldots, n$$。每个位置都有一个"颜色编号"(就是一个整数)。

一开始(记作第 $$0$$ 秒),第 $$i$$ 个位置的颜色为 $$i$$。

接下来会发生 $$t$$ 次操作,按顺序依次执行。第 $$k$$ 次操作(也就是第 $$k$$ 秒)给出一个位置 $$x(1 \le x \le n - 1)$$,并进行如下事情:

先找到一个最大区间 $$[L, R]$$,满足 $$L \le x \le R$$,且在第 $$k - 1$$ 秒结束后区间 $$[L, R]$$ 内所有位置的颜色都与位置 $$x$$ 的颜色相同,$$[L, R]$$ 不能再向左或向右扩展(也就是 $$L - 1$$ 或 $$R + 1$$ 的颜色与位置 $$x$$ 不同,或者越界)。

然后,把区间 $$[L, R]$$ 内所有位置的颜色,都改成"位置 $$x + 1$$ 在第 $$k - 1$$ 秒结束后的颜色"。

现在有 $$q$$ 次询问。每次询问给出一个区间 $$[l, r]$$,你需要回答:最早在第几秒(允许是第 $$0$$ 秒),区间 $$[l, r]$$ 内只存在一种颜色(也就是 $$l, l + 1, \ldots, r$$ 这些位置的颜色全部相同)。如果直到第 $$t$$ 秒都做完了也没发生,输出 $$-1$$。

输入描述

第一行输入三个整数 $$n, t, q(2 \le n, t, q \le 2 \times 10^5)$$,表示位置数量、操作次数、询问次数。

此后 $$t$$ 行,每行输入一个整数 $$x(1 \le x \le n - 1)$$,表示这一秒选择的位置。

此后 $$q$$ 行,每行输入两个整数 $$l, r(1 \le l \le r \le n)$$,表示一次询问的区间。

输出描述

对于每个询问,新起一行输出一个整数,表示最早在第几秒区间 $$[l, r]$$ 内只存在一种颜色。若不存在,输出 $$-1$$。

样例1

输入

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

输出

4
3
2
0
4

样例解释

第 $$0$$ 秒颜色为 $$\{1, 2, 3, 4, 5\}$$。

第 $$1$$ 秒选择 $$x = 4$$,位置 $$4$$ 的颜色段只有它自己,染成位置 $$5$$ 的颜色后,颜色变为 $$\{1, 2, 3, 5, 5\}$$。

第 $$2$$ 秒选择 $$x = 3$$,颜色变为 $$\{1, 2, 5, 5, 5\}$$。

第 $$3$$ 秒选择 $$x = 2$$,颜色变为 $$\{1, 5, 5, 5, 5\}$$。

第 $$4$$ 秒选择 $$x = 1$$,颜色变为 $$\{5, 5, 5, 5, 5\}$$。

因此:区间 $$[1, 5]$$ 最早在第 $$4$$ 秒变成一种颜色;区间 $$[2, 5]$$ 最早在第 $$3$$ 秒变成一种颜色;区间 $$[3, 5]$$ 最早在第 $$2$$ 秒变成一种颜色;区间 $$[1, 1]$$ 在第 $$0$$ 秒就已经只有一种颜色;区间 $$[1, 2]$$ 最早在第 $$4$$ 秒变成一种颜色。

样例2

输入

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

输出

-1
1
-1
0

题解

本题涉及到并查集,不熟悉该算法的同学可以先做一下模板题:

并查集-模版题连通块个数(一)

题目内容拆解

模拟颜色合并操作,对每个查询区间回答最早何时颜色统一。$$n, t, q$$ 均达 $$2 \times 10^5$$,需要高效数据结构。

核心观察:想象相邻位置之间有一道"隔墙"——位置 $$i$$ 和 $$i+1$$ 颜色不同时隔墙存在,颜色相同时隔墙消失。区间 $$[l, r]$$ 颜色全部统一,等价于 $$l$$ 到 $$r-1$$ 之间的所有隔墙都已消失。记录每道隔墙消失的时刻 $$mt[i]$$,查询答案就是最后一道隔墙消失的那一秒:

$$\text{answer}(l, r) = \max(mt[l],\; mt[l+1],\; \ldots,\; mt[r-1])$$

算法实现

分两步:先模拟操作过程记录每道隔墙何时消失,再用预处理表格快速回答查询。

并查集模拟合并:并查集是一种"分组工具",能快速查询"某个元素属于哪一组"并"合并两组"。这里把颜色相同的连续段看作一组,每组记录左端点、右端点和颜色。

处理第 $$k$$ 秒的操作(位置 $$x$$):先查 $$x$$ 属于哪一组,得到其所在段 $$[L, R]$$。如果 $$x+1$$ 已在同组内($$R \ge x+1$$),说明 $$x$$ 和 $$x+1$$ 已经同色,操作无效。否则 $$R = x$$,将 $$[L, x]$$ 整段染成 $$x+1$$ 的颜色,并与 $$x+1$$ 所在段 $$[x+1, R']$$ 合并为 $$[L, R']$$,此时隔墙 $$x$$ 消失,记录 $$mt[x] = k$$。

向左级联合并:合并后新段的颜色变了,可能和左边邻居撞色。比如颜色序列 $$\{5, 1, 5, 5\}$$,对 $$x = 2$$ 操作后变成 $$\{5, 5, 5, 5\}$$——位置 $$2$$ 的段染成颜色 $$5$$ 后和左邻位置 $$1$$(也是颜色 $$5$$)撞色,需要继续合并。所以每次合并后往左检查:左邻段同色就继续合并并记录隔墙时刻,直到颜色不同为止。每道隔墙最多消失一次,所以全部级联的总开销为 $$O(n)$$。

稀疏表回答查询:收集完所有 $$mt[i]$$ 后,需要快速回答"区间最大值"。稀疏表是一种预处理工具——花 $$O(n \log n)$$ 时间建表后,每次查任意区间的最大值只需 $$O(1)$$。查询 $$[l, r]$$ 时:若 $$l = r$$ 答案为 $$0$$(单个位置天然统一);否则取 $$\max(mt[l..r-1])$$,若最大值超过 $$t$$(说明有隔墙到最后也没消失),输出 $$-1$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n + t \cdot \alpha(n) + q)$$,稀疏表建表 $$O(n \log n)$$,并查集模拟 $$O(t \cdot \alpha(n))$$ 加级联总计 $$O(n)$$,每次查询 $$O(1)$$。
  • 空间复杂度:$$O(n \log n)$$,稀疏表占用 $$O(n \log n)$$,其余数组各 $$O(n)$$。

2026-4-11-工程岗

第一题:xor

在线评测链接:https://www.neituiya.com/oj/7/2489

题目描述

AK机给定一棵包含 $$n$$ 个节点的树,节点编号为 $$1 \sim n$$,根节点为编号 $$1$$ 的节点。每个节点 $$u$$ 有一个权值 $$a_u$$。

他想针对每个节点 $$u$$,在以 $$u$$ 为根的子树内统计节点权值之间的异或总和。

具体来说,对于每个节点 $$u(1 \le u \le n)$$,计算以 $$u$$ 为根的子树内所有不同节点对 $$(x, y)(x < y)$$ 的权值异或之和:

$$\sum_{\substack{x, y \in T_u \\ x < y}} (a_x \oplus a_y)$$

其中 $$T_u$$ 表示以 $$u$$ 为根的子树节点集合。

名词解释

按位异或 $$(\oplus)$$:二进制逐位运算,相同为 $$0$$、不同为 $$1$$。例如 $$5 \oplus 3 = 6$$。

子树:对于树中某个节点,其与所有后代节点构成的集合称为该节点的子树。

输入描述

第一行包含一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示节点总数。

第二行包含 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^6)$$,表示各节点权值。

接下来 $$n - 1$$ 行,每行两个整数 $$u, v(1 \le u, v \le n, u \ne v)$$,表示节点 $$u$$ 与节点 $$v$$ 之间存在一条无向边。

给定的边构成一棵以节点 $$1$$ 为根的树,用于定义父子关系与子树。

输出描述

输出 $$n$$ 个整数,第 $$u$$ 个整数为以节点 $$u$$ 为根的子树内所有节点对权值异或之和。各数之间用空格分隔。

样例1

输入

3
1 2 3
1 2
1 3

输出

6 0 0

样例2

输入

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

输出

42 14 0 0 0

第二题:服装套装

在线评测链接:https://www.neituiya.com/oj/7/2490

题目描述

节日临近,某时装店需要安排当日陈列与销售。仓库现有领带 $$a$$ 条、围巾 $$b$$ 条、夹克 $$c$$ 件。商店出售如下两类套装:

第一类套装:$$1$$ 条领带 $$+ 1$$ 件夹克,售价 $$d$$ 金币。第二类套装:$$1$$ 条围巾 $$+ 1$$ 件夹克,售价 $$e$$ 金币。

商店每天的陈列展位共计 $$r$$ 个,其中第一类套装占用 $$1$$ 个展位,第二类套装占用 $$2$$ 个展位。每件服装至多参与一个套装,允许有剩余不使用。请计算在不超过展位限制的前提下,最多可以获得的总收益。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 表示数据组数。

每组测试数据一行输入六个整数 $$a, b, c, d, e, r(0 \le a, b, c, d, e, r \le 10^9)$$。

输出描述

对于每组测试数据,输出一行,包含一个整数,表示在最优方案下的最大总收益(单位:金币)。

样例1

输入

3
3 0 1 3 5 3
10 10 10 10 8 11
4 11 3 10 3 1

输出

3
100
10

样例解释

第 $$1$$ 组:只有 $$0$$ 条围巾,只能做第一类套装。领带 $$3$$ 条、夹克 $$1$$ 件,最多做 $$1$$ 套,收益 $$3$$。

第 $$2$$ 组:做 $$10$$ 套第一类(用 $$10$$ 领带 $$10$$ 夹克,占 $$10$$ 展位),收益 $$100$$。

第 $$3$$ 组:展位只有 $$1$$ 个,只能做 $$1$$ 套第一类,收益 $$10$$。

第三题:子序列计数

在线评测链接:https://www.neituiya.com/oj/7/2491

题目描述

给定一个仅由 '0'/'1' 组成的字符串 $$s$$。对任意一个非空子序列 $$t$$,记其长度为 $$|t|$$、其中字符 '1' 的数量为 $$x$$。若满足 $$|t|$$ 是 $$x$$ 的倍数且 $$x$$ 不为 $$0$$,则称 $$t$$ 为"好的"。

请你统计 $$s$$ 中"好的"子序列的个数。注意,如果两个子序列是通过选取原字符串中不同位置的字符集合得到的,那么即使它们构成的字符串相同,也应被视为不同的子序列。由于答案可能很大,请将答案对 $$10^9 + 7$$ 取模后输出。

子序列:从原字符串中删除任意个(可以为零)字符得到的新字符串。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入整数 $$n(1 \le n \le 2 \times 10^5)$$ 表示字符串长度。

第二行输入一个只包含 '0'/'1' 的字符串 $$s$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数,表示 $$s$$ 的"好的"子序列个数,对 $$10^9 + 7$$ 取模。

样例1

输入

3
3
101
2
00
2
01

输出

5
0
2

样例解释

$$s = 101$$:符合的子序列(用位置集合表示)为 $$\{1\}, \{3\}, \{1,2\}, \{2,3\}, \{1,3\}$$,共 $$5$$ 个。

$$s = 00$$:无符合子序列(因 $$x = 0$$ 不计)。

$$s = 01$$:符合的子序列为 $$\{2\}, \{1,2\}$$,共 $$2$$ 个

2026-4-11-算法岗

第一题:轮转

在线评测链接:https://www.neituiya.com/oj/7/2480

题目描述

给你一个长度为 $$n$$ 的字符串 $$s$$,接下来会进行 $$q$$ 次操作。每次操作会给出一个字符变换规则 $$x \to y$$,你需要将当前字符串里所有的字符 $$x$$ 替换为字符 $$y$$。请你给出在完成所有操作后的字符串。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个正整数 $$n, q(1 \le n, q \le 4 \times 10^5)$$,表示字符串长度和操作次数。

第二行输入一个长度为 $$n$$ 且仅由小写字母组成的字符串 $$s$$。

此后 $$q$$ 行,每行输入两个小写字母 $$x, y$$,表示一次替换操作,即将字符串中所有的 $$x$$ 替换为 $$y$$。

除此之外,保证单个测试文件的 $$n$$ 之和与 $$q$$ 之和均不超过 $$4 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行输出一个字符串,表示最终的字符串。

样例1

输入

2
3 2
abc
a b
b c
5 1
abcde
a z

输出

ccc
zbcde

样例解释

初始字符串为 $$abc$$,第一次操作将所有的 $$a$$ 替换为 $$b$$,字符串变为 $$bbc$$,第二次操作将所有的 $$b$$ 替换为 $$c$$,字符串变为 $$ccc$$。

题解

题目内容拆解

给定字符串和一系列替换操作 $$x \to y$$,求最终字符串。数据规模 $$n, q \le 4 \times 10^5$$,逐次扫描字符串替换的暴力做法是 $$O(nq)$$,会超时。核心观察:每次操作只涉及 $$26$$ 个小写字母之间的映射变换,→ 因此采用字符映射

算法实现

算法主策略:维护一个长度为 $$26$$ 的映射数组 $$mp$$,其中 $$mp[c]$$ 表示原始字符 $$c$$ 经过所有已处理操作后最终会变成什么字符。初始时 $$mp[c] = c$$。

对于每次操作 $$x \to y$$,遍历 $$mp$$ 数组,将所有 $$mp[c] = x$$ 的位置改为 $$mp[c] = y$$。这一步仅需 $$O(26)$$ 时间,因为我们操作的是映射表而非原始字符串。处理完所有操作后,对原始字符串的每个字符 $$ch$$ 查表 $$mp[ch]$$ 即得最终字符。

时空复杂度分析

  • 时间复杂度:$$O(26q + n)$$,每次操作扫描映射表 $$O(26)$$,最后遍历字符串 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储字符串和映射表。

Go

// 轮转 - 字符映射
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(s string, ops [][2]int) string {
        // mp[c] 记录字符c最终会变成什么
        var mp [26]int
        for i := 0; i < 26; i++ {
                mp[i] = i
        }
        for _, op := range ops {
                x, y := op[0], op[1]
                // 所有当前映射到x的字符,改为映射到y
                for c := 0; c < 26; c++ {
                        if mp[c] == x {
                                mp[c] = y
                        }
                }
        }
        // 查表得到每个字符的最终结果
        res := make([]byte, len(s))
        for i, ch := range s {
                res[i] = byte(mp[ch-'a'] + 'a')
        }
        return string(res)
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n, q int
                fmt.Fscan(reader, &n, &q)
                var s string
                fmt.Fscan(reader, &s)
                ops := make([][2]int, q)
                for i := 0; i < q; i++ {
                        var x, y string
                        fmt.Fscan(reader, &x, &y)
                        ops[i] = [2]int{int(x[0] - 'a'), int(y[0] - 'a')}
                }
                fmt.Fprintln(writer, solve(s, ops))
        }
}

第二题:凑对

在线评测链接:https://www.neituiya.com/oj/7/2481

题目描述

AK机很喜欢满足以下所有条件的二元组 $$(x, y)$$:

$$x + y$$ 不是质数,$$|x - y|$$ 不是质数。

AK机的好朋友送给他一个长度为 $$n$$ 的排列 $$\{a_1, a_2, \ldots, a_n\}$$,其中 $$n$$ 为偶数。

AK机希望对于任意的 $$2 \le i \le n$$ 且 $$i \bmod 2 = 0$$,二元组 $$(a_{i-1}, a_i)$$ 都是AK机喜欢的二元组。

AK机找到了你,希望你能构造出满足条件的排列;如果不存在解,则输出 $$-1$$。

名词解释

质数:一个大于 $$1$$ 的正整数,如果除了 $$1$$ 和它自身以外不再有其他整数可以将其整除,那么这个数被称作质数。特殊地,$$1$$ 既不是质数也不是合数。

长度为 $$n$$ 的排列:由 $$1, 2, \ldots, n$$ 这 $$n$$ 个整数,按任意顺序组成的数组(每个整数均恰好出现一次)。例如,$$\{2, 3, 1, 5, 4\}$$ 是一个长度为 $$5$$ 的排列,而 $$\{1, 2, 2\}$$ 和 $$\{1, 3, 4\}$$ 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

在一行上输入一个整数 $$n(2 \le n \le 2 \times 10^5)$$,表示排列的长度。题目保证 $$n$$ 为偶数。

除此之外,保证单个测试文件的所有 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行输出 $$n$$ 个整数表示你构造的排列;如果不存在解,则输出 $$-1$$。

如果存在多个解决方案,您可以输出任意一个,系统会自动判定是否正确。

样例1

输入

2
2
12

输出

-1
7 8 5 4 1 9 2 12 3 11 6 10

样例解释

长度为 $$2$$ 的排列只有 $$\{1, 2\}$$、$$\{2, 1\}$$ 两种,$$a_1 + a_2$$ 均为 $$3$$ 是质数,无解。第二个测试数据满足题目答案。

题解

题目内容拆解

构造一个 $$1$$ 到 $$n$$ 的排列,使得每对相邻位置 $$(a_{2i-1}, a_{2i})$$ 的和与差绝对值都不是质数。$$n$$ 为偶数且 $$n \le 2 \times 10^5$$。暴力搜索的状态空间是 $$n!$$,显然不可行。

核心观察:两个同奇偶的数,和与差都是偶数,而大于 $$2$$ 的偶数一定不是质数,只需保证差不等于 $$2$$,→ 因此采用同奇偶对半配对构造

算法实现

算法主策略:将 $$1$$ 到 $$n$$ 按奇偶分成两组,每组内部排序后对半拆分配对。具体地,将奇数组 $$[1, 3, 5, \ldots, n-1]$$ 分成前半 $$[1, 3, \ldots]$$ 和后半 $$[\ldots, n-3, n-1]$$,第 $$i$$ 个前半元素与第 $$i$$ 个后半元素配对。偶数组同理。

正确性证明:配对的两个数同奇偶,因此和与差都是偶数。配对的差值恒为 $$n/2$$(即组长度),当 $$n \ge 8$$ 时 $$n/2 \ge 4$$,是大于 $$2$$ 的偶数,必定不是质数。和至少为 $$1 + (n/2 + 1) = n/2 + 2 \ge 6$$,也是非质数的偶数。

无解判定:当 $$n \le 6$$ 时(即 $$n = 2, 4, 6$$),可以枚举验证不存在合法排列,输出 $$-1$$。

$$n \equiv 2 \pmod{4}$$ 的处理:此时奇数组和偶数组各有 $$n/2$$ 个元素(奇数个),无法直接对半拆分。解决方法是先取出一对跨奇偶配对 $$(4, 5)$$,其和为 $$9 = 3^2$$、差为 $$1$$,均不是质数。移除 $$4$$ 和 $$5$$ 后两组各剩偶数个元素,再分别对半配对即可。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,构造排列只需线性遍历。
  • 空间复杂度:$$O(n)$$,存储结果排列。

Go

// 凑对 - 构造(同奇偶配对)
package main

import (
        "bufio"
        "fmt"
        "os"
        "strconv"
        "strings"
)

// 将同奇偶的数组对半拆分配对,保证diff为偶数且>=4
func pairHalf(nums []int, result *[]int) {
        m := len(nums)
        for i := 0; i < m/2; i++ {
                *result = append(*result, nums[i], nums[i+m/2])
        }
}

func solve(reader *bufio.Reader, writer *bufio.Writer) {
        var n int
        fmt.Fscan(reader, &n)
        if n <= 6 {
                fmt.Fprintln(writer, -1)
                return
        }
        result := make([]int, 0, n)
        if n%4 == 0 {
                // 奇数组和偶数组各自对半配对
                odds := make([]int, 0, n/2)
                evens := make([]int, 0, n/2)
                for i := 1; i <= n; i += 2 {
                        odds = append(odds, i)
                }
                for i := 2; i <= n; i += 2 {
                        evens = append(evens, i)
                }
                pairHalf(odds, &result)
                pairHalf(evens, &result)
        } else {
                // n%4==2: 先用跨奇偶对(4,5), sum=9非质数, diff=1非质数
                result = append(result, 4, 5)
                // 剩余奇数(去掉5)和偶数(去掉4)各自配对
                odds := make([]int, 0, n/2)
                evens := make([]int, 0, n/2)
                for i := 1; i <= n; i += 2 {
                        if i != 5 {
                                odds = append(odds, i)
                        }
                }
                for i := 2; i <= n; i += 2 {
                        if i != 4 {
                                evens = append(evens, i)
                        }
                }
                pairHalf(odds, &result)
                pairHalf(evens, &result)
        }
        parts := make([]string, n)
        for i, v := range result {
                parts[i] = strconv.Itoa(v)
        }
        fmt.Fprintln(writer, strings.Join(parts, " "))
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()
        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                solve(reader, writer)
        }
}

第三题:模k最大子序列

在线评测链接:https://www.neituiya.com/oj/7/2482

题目描述

给定一个长度为 $$n$$ 的整数数组 $$\{a_1, a_2, \ldots, a_n\}$$ 和一个正整数 $$k$$。请选择一个非空子序列(不要求连续),将其元素之和对 $$k$$ 取模,得到一个位于 $$[0, k)$$ 的整数。请计算这个值的最大可能结果。

非空子序列指从原数组中删除任意个(可以为零,但不能全部)元素后得到的新序列,保持原相对顺序,但不要求连续。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^4)$$ 表示数据组数。此后对每组数据:

第一行输入两个整数 $$n, k(1 \le k \le n \le 2 \times 10^4)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^9)$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$6 \times 10^4$$。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示选择某个非空子序列后,其元素之和对 $$k$$ 取模所得的最大值。

样例1

输入

3
5 5
3 8 2 6 4
5 5
5 10 15 20 5
3 4
1 2 3

输出

4
0
3

样例解释

对于第 $$1$$ 组:例如选择子序列 $$\{4\}$$,其和为 $$4$$,对 $$5$$ 取模为 $$4$$,这是可能的最大值(也可选择 $$\{3, 6\}$$,其和为 $$9$$,对 $$5$$ 取模为 $$4$$)。

对于第 $$2$$ 组:所有元素均为 $$5$$ 的倍数,任意非空子序列的和也是 $$5$$ 的倍数,最大取模结果为 $$0$$。

对于第 $$3$$ 组:选择 $$\{3\}$$ 或 $$\{1, 2\}$$ 的和对 $$4$$ 取模均为 $$3$$,为最大值。

题解

题目内容拆解

从长度为 $$n$$ 的数组中选择非空子序列,使元素之和对 $$k$$ 取模的结果最大。$$n, k \le 2 \times 10^4$$。这本质上是一个$$k$$ 子集和问题:需要判断 $$[0, k)$$ 中哪些余数可达,暴力枚举 $$2^n$$ 个子集显然不可行。核心观察:我们只关心子序列和对 $$k$$ 的余数,而非具体和值,可用布尔数组记录可达余数集合,→ 因此采用动态规划

算法实现

状态方程定义:$$f[j]$$ 表示是否存在某个非空子序列,其元素之和对 $$k$$ 取模等于 $$j$$。$$f[j] = \text{true}$$ 即"余数 $$j$$ 可达"。

状态方程初始化:$$f$$ 全部为 $$\text{false}$$(尚未选取任何元素)。

状态方程转移:依次处理每个元素 $$a_i$$,令 $$v = a_i \bmod k$$。对于当前已可达的每个余数 $$j$$(即 $$f[j] = \text{true}$$),加入 $$a_i$$ 后余数变为 $$(j + v) \bmod k$$,将其标记为可达。同时 $$v$$ 本身也可达(仅选 $$a_i$$ 一个元素)。为避免同一轮中重复更新,需在旧状态的拷贝上计算新状态再合并。

在 Python/Java 等语言中,可将 $$f$$ 压缩为一个 $$k$$ 位整数(位集),其中第 $$j$$ 位为 $$1$$ 表示余数 $$j$$ 可达。加入元素 $$v$$ 后,原先可达的余数 $$j$$ 变为 $$(j + v) \bmod k$$——对应到位集上,就是把所有位向左移动 $$v$$ 格,超出第 $$k$$ 位的部分绕回低位,即循环左移 $$v$$。将移位结果与原位集取并集,单次操作仅需 $$O(k/64)$$ 的位运算。

时空复杂度分析

  • 时间复杂度:$$O(nk)$$(数组 DP)或 $$O(nk/64)$$(位集优化),其中 $$n, k \le 2 \times 10^4$$,总 $$n$$ 之和不超过 $$6 \times 10^4$$。
  • 空间复杂度:$$O(k)$$,存储可达余数集合。

Go

// 模k最大子序列 - 位集DP
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(n, k int, a []int) int {
        // f[j]=true 表示存在非空子序列使得和模k等于j
        f := make([]bool, k)
        for i := 0; i < n; i++ {
                v := a[i] % k
                // 拷贝当前状态,避免同一轮重复更新
                nf := make([]bool, k)
                copy(nf, f)
                for j := 0; j < k; j++ {
                        // 已有子序列和余数j,加入a[i]后余数变为(j+v)%k
                        if f[j] {
                                nf[(j+v)%k] = true
                        }
                }
                nf[v] = true // 单独选这个元素
                f = nf
        }
        // 从大到小找第一个可达余数
        for j := k - 1; j >= 0; j-- {
                if f[j] {
                        return j
                }
        }
        return 0
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n, k int
                fmt.Fscan(reader, &n, &k)
                a := make([]int, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &a[i])
                }
                fmt.Fprintln(writer, solve(n, k, a))
        }
}

2026-4-8-工程岗

第一题:记忆友好的密码

在线评测链接:https://www.neituiya.com/oj/7/2468

第二题:环形二进制串

在线评测链接:https://www.neituiya.com/oj/7/2469

第三题:困难不平衡数

在线评测链接:https://www.neituiya.com/oj/7/2470

2026-4-11-AI研发岗

第一题:模乘循环数

在线评测链接:https://www.neituiya.com/oj/7/2477

题目描述

初始时 $$a = 1$$。给定两个整数 $$k, m$$,系统会不断重复执行如下更新:

将 $$a$$ 更新为 $$a \leftarrow (a \cdot k) \bmod m$$。

由于取模运算,$$a$$ 的取值最终会进入循环。请你计算在无限次执行更新的过程中,$$a$$ 一共可能取到多少个不同的值。

输入描述

一行输入两个整数 $$k, m(0 \le k \le 10^6, 1 \le m \le 10^6)$$。

输出描述

输出一行一个整数,表示不同的 $$a$$ 的个数。

样例1

输入

2 7

输出

3

样例2

输入

2 8

输出

4

样例3

输入

0 5

输出

2

第二题:逆转

在线评测链接:https://www.neituiya.com/oj/7/2478

题目描述

AK机先写下一个正整数序列 $$\{a_1, a_2, \ldots, a_m\}$$($$1 \le a_i \le 10^9$$),随后,她按照如下带阈值的保留规则从左到右生成序列 $$\{b_1, b_2, \ldots, b_n\}$$:

先保留 $$a_1$$。当处理到 $$a_i(2 \le i \le m)$$ 时,将其与它在序列 $$a$$ 中的前一个元素 $$a_{i-1}$$ 进行比较。若 $$a_{i-1} + d \le a_i$$ 成立,则将 $$a_i$$ 保留下来;否则跳过 $$a_i$$。

现给出阈值 $$d$$ 与最终得到的序列 $$\{b_1, \ldots, b_n\}$$。你的任务是构造任意一个原序列 $$\{a_1, \ldots, a_m\}$$,使得按上述规则从 $$a$$ 生成的序列恰为 $$b$$,并同时满足:$$m$$ 尽可能小;在所有满足最小长度的解中,$$a$$ 的字典序尽可能小。

我们可以证明,一定存在至少一个符合全部要求的序列。

名词解释

序列的字典序比较:从左到右逐个比较两个序列的元素。如果在某个位置上元素不同,比较这两个元素的大小,元素小的序列字典序也小;如果一直比较到其中一个序列结束,则长度较短的序列字典序更小。例如:$$[1, 2, 3]$$ 的字典序小于 $$[2, 3, 4]$$,因为第一个位置上的元素 $$1 < 2$$;$$[1, 2, 3]$$ 的字典序大于 $$[1, 2]$$,因为前两个元素相同,但后者长度更短。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, d(1 \le n \le 2 \times 10^5, 0 \le d < 10^9)$$。

第二行输入 $$n$$ 个整数 $$b_1, b_2, \ldots, b_n(1 \le b_i \le 10^9)$$。

保证所有测试用例的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,输出两行:

第一行输出一个整数 $$m(n \le m \le 2n)$$

第二行输出 $$m$$ 个整数,为构造出的序列 $$\{a_1, a_2, \ldots, a_m\}$$($$1 \le a_i \le 10^9$$),使得按规则生成的序列恰为 $$b$$,且 $$a$$ 长度最小、在最小长度下字典序最小。

样例1

输入

2
3 2
3 5 6
4 0
2 1 1 3

输出

4
3 5 1 6
5
2 1 1 1 3

样例解释

对应第一组数据:构造 $$a = \{3, 5, 1, 6\}$$。处理时先保留 $$3$$;因 $$3 + 2 \le 5$$,保留 $$5$$;因 $$5 + 2 \le 1$$ 不成立,跳过 $$1$$;因 $$1 + 2 \le 6$$,保留 $$6$$。最终保留序列为 $$\{3, 5, 6\}$$ 与 $$b$$ 一致,长度 $$m = 4$$,且在最小长度下字典序最小。

第三题:果酱平衡

在线评测链接:https://www.neituiya.com/oj/7/2479

题目描述

有一个大小为 $$n \times m$$ 的储物柜,格子里放着两种果酱:蓝莓酱(记作 'B')和草莓酱(记作 'S'),他希望储物柜中两种果酱的数量相等。

对于储物柜的每一行,你都可以独立地选择移除其最左侧的 $$k$$ 瓶果酱($$0 \le k \le m$$,$$k = 0$$ 表示不移除该行的任何果酱)。

请你计算,最少需要拿走多少瓶果酱,才能使柜中剩余的 'B''S' 的数量相等。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 表示测试组数。

对每组测试数据:第一行输入两个整数 $$n, m(1 \le n, m, n \times m \le 2 \times 10^5)$$。接下来 $$n$$ 行,每行一个长度为 $$m$$ 的仅由 'B''S' 组成的字符串。

保证所有测试组中 $$\sum(n \times m) \le 5 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数,表示最少需要拿走的果酱数量。

样例1

输入

3
1 5
BSSSB
2 4
BSSB
SBBB
2 3
BBB
SSS

输出

3
4
0

样例解释

样例一:在第 $$1$$ 行拿走前缀长度 $$3$$ 即可使总差值变为 $$0$$,最少拿走 $$3$$ 瓶。样例二:在第 $$2$$ 行拿走前缀长度 $$4$$(SBBB)即可使总差值变为 $$0$$,最少拿走 $$4$$ 瓶。样例三:初始总计 $$B = 3, S = 3$$,差值已为 $$0$$,无需拿走任何瓶,答案为 $$0$$。

第一题:可删去的字符串

在线评测链接:https://www.neituiya.com/oj/7/2474

第二题:网格路径最大和

在线评测链接:https://www.neituiya.com/oj/7/2475

第三题:相邻等值对贡献和

在线评测链接:https://www.neituiya.com/oj/7/2476

2026-4-8-AI研发岗

第一题:记忆友好的密码

在线评测链接:https://www.neituiya.com/oj/7/2468

题目描述

AK机拥有多张银行卡,每张卡的密码均由 $$6$$ 个数字组成(可含前导 $$0$$)。密码太多不易记忆,他打算进行一次统一改动:选择同一个位置 $$pos \in \{1, 2, 3, 4, 5, 6\}$$,并选择一个数字 $$d \in \{0, 1, \dots, 9\}$$,把所有卡在该位置的数字都改成 $$d$$。

改动完成后,得到一批新的 $$6$$ 位数字密码。AK机希望不同密码的种类数尽可能少。请你计算:完成一次上述改动后,不同密码的最少种类数。

输入描述

每个测试文件包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 表示测试组数。接下来每组数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 10^5)$$,表示银行卡数量。

第二行输入 $$n$$ 个长度为 $$6$$ 的数字串,依次表示每张卡的原始密码。

保证所有测试数据的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,输出一行一个整数,表示进行一次统一改动后,不同密码的最少种类数。

样例1

输入

3
5
000000 100000 200000 300000 000000
3
123456 123556 123656
3
000000 111111 222222

输出

1
1
3

题解

题目内容拆解

给定 $$n$$ 张六位数字密码,允许选定一个位置 $$pos$$ 和一个数字 $$d$$,把所有密码在该位置统一改写为 $$d$$,求改写后不同密码种类数的最小值。位置只有 $$6$$ 种、数字只有 $$10$$ 种,组合数极其有限,$$\sum n \le 2 \times 10^5$$。

核心观察:所有可能的改动只有 $$60$$ 种,对每一种改动直接模拟一遍即可得到对应的密码种类数。→ 因此采用枚举 + 哈希,直接枚举 $$(pos, d)$$,用哈希集合统计去重后的密码数量。

算法实现

算法主策略:本题采用枚举 + 哈希集合,对全部 $$60$$ 种 $$(pos, d)$$ 组合各做一次完整扫描。

对每个组合,将每个密码的第 $$pos$$ 位替换成 $$d$$ 得到新密码,插入一个哈希集合,最终集合的大小即为该组合对应的不同密码种类数。遍历所有组合取最小值即为答案。由于 $$60$$ 与 $$n$$ 是乘积关系,每组总操作量约为 $$60n$$,对 $$\sum n \le 2 \times 10^5$$ 完全可以承受。

时空复杂度分析

  • 时间复杂度:$$O(60 n)$$,共枚举 $$60$$ 种改动,每种改动对 $$n$$ 个密码做常数时间的替换与哈希插入。
  • 空间复杂度:$$O(n)$$,哈希集合最多存储 $$n$$ 个密码。

Go

// 记忆友好的密码 - 枚举 + 哈希集合
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(n int, pwds []string) int {
        best := n
        // 枚举位置 pos 和目标数字 d
        for pos := 0; pos < 6; pos++ {
                for d := 0; d < 10; d++ {
                        ch := byte('0' + d)
                        seen := make(map[string]struct{}, n)
                        buf := make([]byte, 6)
                        for _, p := range pwds {
                                // 把第 pos 位换成 d 后放入 map 自动去重
                                copy(buf, p)
                                buf[pos] = ch
                                seen[string(buf)] = struct{}{}
                        }
                        if len(seen) < best {
                                best = len(seen)
                        }
                }
        }
        return best
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()
        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                fmt.Fscan(reader, &n)
                pwds := make([]string, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &pwds[i])
                }
                fmt.Fprintln(writer, solve(n, pwds))
        }
}

第二题:环形二进制串

在线评测链接:https://www.neituiya.com/oj/7/2469

题目描述

给定一个仅由字符 01 组成、长度为 $$n$$ 的环形二进制串 $$s$$。你可以选择一个起点,将环断开并从该位置起按顺时针读出,得到一个线性串(等价于对 $$s$$ 进行一次旋转)。在得到的线性串中,定义其最短 $$k$$-前缀长度为:包含恰好 $$k$$ 个字符 1 的最短前缀的长度(若整个线性串中 1 的总数小于 $$k$$,则输出 $$-1$$)。

你的任务是:在所有可能的旋转中,取上述最短 $$k$$-前缀长度的最小值;若对于任意旋转均不存在包含 $$k$$ 个 1 的前缀,则输出 $$-1$$。

字符串的前缀:从字符串的第一个字符开始,向后连续取若干个字符得到的字符串。更具体地,字符串 $$s$$ 前 $$i$$ 个字符构成的字符串被称为 $$s$$ 的第 $$i$$ 个前缀,记为 $$s[1..i]$$。

输入描述

每个测试文件包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 表示数据组数,每组测试数据描述如下:

每组输入第一行包含两个整数 $$n, k(1 \le n \le 2 \times 10^5, 1 \le k \le n)$$。

第二行输入一个长度为 $$n$$ 的仅由字符 01 构成的字符串 $$s$$。

保证所有测试数据的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,输出一行一个整数,表示在所有旋转中包含恰好 $$k$$ 个 1 的最短前缀长度的最小值;若不存在,输出 $$-1$$。

样例1

输入

3
8 3
11001001
5 2
01000
7 1
0000001

输出

3
-1
1

题解

题目内容拆解

环形串 $$s$$ 上,需要找到一段连续子串包含恰好 $$k$$ 个 1 且长度最小,因为"某个旋转的前缀"就等价于原环上某个起点开始的连续子串。若总共 1 数量 $$c < k$$ 则无解,$$n$$ 可达 $$2 \times 10^5$$,需要线性算法。

核心观察:答案对应的最优窗口两端一定都是 1(否则可以去掉两侧的 0 得到更短的窗口且仍包含同样多的 1)。记 $$p_0, p_1, \dots, p_{c-1}$$ 为串中所有 1 的位置,那么以第 $$j$$ 个 1 为左端的最短合法窗口的右端必然是第 $$j+k-1$$ 个 1,窗口长度 $$=p_{j+k-1}-p_j+1$$。遍历所有 $$j$$ 取最小即可,$$O(c)$$ 搞定。 → 因此采用环变直 + 遍历所有 1 起点:把原串"首尾相接"这件事用"复制一份接在后面"来模拟,所有跨边界的窗口都变成普通线性区间。

算法实现

算法主策略:本题采用复制一份原串 + 遍历所有 1 作为窗口左端,把环形问题转化为线性问题。

设原串中 1 的下标数组为 $$ones$$(长度 $$c$$),把 $$ones$$ 整体复制一份并加上偏移 $$n$$,拼成长度 $$2c$$ 的 $$pos$$ 数组——前一半是第一圈 1 的位置,后一半是"绕回来一圈后"同一批 1 的位置,这样跨越边界的窗口也能用 $$pos[j+k-1]-pos[j]$$ 直接算出。对每个左端索引 $$j \in [0, c)$$,最短合法窗口长度为 $$pos[j+k-1] - pos[j] + 1$$。取所有 $$j$$ 下的最小值即为答案。若 $$c < k$$ 直接输出 $$-1$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,扫描原串一次找出所有 1 的位置,再线性枚举左端即可。
  • 空间复杂度:$$O(n)$$,用于存储 1 的位置及其倍增拷贝。

Java

// 环形二进制串 - 展开环形 + 遍历 1 起点
import java.io.*;
import java.util.*;

public class Main {
    static int solve(int n, int k, String s) {
        int[] ones = new int[n];
        int c = 0;
        for (int i = 0; i < n; i++) if (s.charAt(i) == '1') ones[c++] = i;
        if (c < k) return -1;
        // 复制一份:第二圈位置整体加 n,使跨边界窗口也变成线性区间
        int[] pos = new int[2 * c];
        for (int i = 0; i < c; i++) {
            pos[i] = ones[i];
            pos[i + c] = ones[i] + n;
        }
        int best = n;
        // 枚举左端的 1,取第 k 个 1 的位置算窗口长度
        for (int j = 0; j < c; j++) {
            int L = pos[j + k - 1] - pos[j] + 1;
            if (L < best) best = L;
        }
        return best;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        // 关闭数字解析,让 0/1 串直接以 sval 字符串形式读入,避免前导零被 nval 吞掉
        in.resetSyntax();
        in.wordChars('0', '9');
        in.whitespaceChars(0, ' ');
        in.nextToken(); int T = Integer.parseInt(in.sval);
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            in.nextToken(); int n = Integer.parseInt(in.sval);
            in.nextToken(); int k = Integer.parseInt(in.sval);
            in.nextToken(); String s = in.sval;
            sb.append(solve(n, k, s)).append('\n');
        }
        System.out.print(sb);
    }
}

第三题:困难不平衡数

在线评测链接:https://www.neituiya.com/oj/7/2470

题目描述

定义一个整数:倘若数字位中奇数数字的个数不等于偶数数字的个数,那么称这个整数是一个不平衡数。

给定一个由数字 $$0$$ 到 $$9$$ 组成的字符串,求其中有多少子序列满足:该子序列所代表的数是一个不平衡数,且不包含前导零。这里约定单个字符 0 本身对应的数 $$0$$ 不算作前导零;仅当子序列长度至少为 $$2$$ 且首字符为 0 时才视为含前导零,需排除。

由于答案可能很大,请将答案对 $$(10^9 + 7)$$ 取模后输出。

子序列:从原序列中删除任意个(可以为零、可以为全部)元素得到的新的序列。

输入描述

第一行输入一个整数 $$n(1 \le n \le 5 \times 10^3)$$,代表字符串长度。

第二行输入一个长度为 $$n$$、由数字 $$0$$ 到 $$9$$ 组成的字符串 $$s$$。可能包含前导零。

输出描述

输出一个整数,表示可以组成不平衡数的子序列数量对 $$10^9 + 7$$ 取模后的结果。

样例1

输入

3
102

输出

4

样例2

输入

6
001119

输出

17

样例3

输入

7
0000000

输出

7

题解

题目内容拆解

字符串长度 $$n \le 5 \times 10^3$$,要统计奇数位数与偶数位数不相等的合法子序列个数,合法指"长度为 $$1$$"或"长度 $$\ge 2$$ 且首字符非 0"。子序列共 $$2^n$$ 个,$$n=5000$$ 时完全无法穷举。

补集转换:正面数"不平衡"的子序列要按奇偶比例讨论,非常麻烦;反过来数"平衡"(奇数位数 $$=$$ 偶数位数)的要干净得多,最后用合法总数减去它即可。

把平衡数值化:给每个奇数数字贴 $$+1$$ 标签,每个偶数数字($$0$$ 也算偶)贴 $$-1$$ 标签,那么一个子序列的"奇数位数减偶数位数"就等于它所有标签之和。平衡 = 标签和为 $$0$$。问题转化为:"从字符串里挑一些字符,使得标签和为指定值的方案数有多少。"

分离首字符与后缀选择:合法性约束里最烦的是"前导零",能不能一劳永逸地绕开?

可以——枚举子序列的首字符是谁:它必定是某个非 0 的 $$s[i]$$,剩下的字符只能从它右边的 $$s[i+1..n-1]$$ 里挑(保持子序列原顺序)。这样只要 $$s[i] \ne$$ 0,怎么选后缀都合法,前导零约束自然消失。单独的"单字符 0"子序列不走这条路径,最后加回来。

→ 因此采用动态规划,以"标签和"为状态倒序扫描原串。倒序的好处是:扫描到位置 $$i$$ 时手头的 DP 数组正好统计"后缀 $$s[i+1..n-1]$$ 的标签和分布",锁定首字符 $$s[i]$$ 后直接查表即可。

算法实现

状态方程定义:$$f_i[d]$$ 表示仅由后缀 $$s[i..n-1]$$ 中字符挑出的子序列里,标签和恰等于 $$d$$ 的子序列数量(含空子序列)。$$d$$ 的范围是 $$[-n, n]$$,实现时统一加偏移 $$OFF = n$$ 存到 $$f[OFF+d]$$,因为数组下标不能为负。

状态方程初始化:$$f_n[0] = 1$$,其余为 $$0$$——空后缀只能挑出空子序列,它的标签和为 $$0$$。

状态方程转移:把字符 $$s[i]$$(标签 $$\delta_i \in \{+1, -1\}$$)加入候选集时,对于每个目标和 $$d$$,一个子序列要么不选 $$s[i]$$(直接继承 $$f_{i+1}[d]$$),要么它(那"剩下部分"的标签和必须是 $$d - \delta_i$$,对应 $$f_{i+1}[d - \delta_i]$$)。两种决策相加

$$f_i[d] = f_{i+1}[d] + f_{i+1}[d - \delta_i]$$

代码里用两个一维数组 $$\text{prev}$$ 和 $$\text{curr}$$ 分别代表 $$f_{i+1}$$ 和 $$f_i$$:读 $$\text{prev}$$、写 $$\text{curr}$$,一轮结束后把 $$\text{curr}$$ 赋给 $$\text{prev}$$ 即可进入下一轮。数组下标用 $$\text{OFF} + d$$($$\text{OFF} = n$$)这个偏移映射,把 $$[-n, n]$$ 的差值映射到非负下标。

组合答案:扫描到位置 $$i$$ 时 $$\text{prev}$$ 恰为 $$f_{i+1}$$(还没把 $$s[i]$$ 加入)。若 $$s[i] \ne$$ 0,把它固定为子序列首字符,后缀可随意挑,共贡献 $$2^{n-1-i}$$ 个合法子序列;这些子序列中"平衡"的那部分恰好是后缀标签和等于 $$-\delta_i$$(与首字符的 $$\delta_i$$ 相加得 $$0$$)的情况,数量为 $$f_{i+1}[-\delta_i]$$,即代码里的 $$\text{prev}[\text{OFF} - \delta_i]$$,需要从贡献中扣除。累加所有非零 $$s[i]$$ 的净贡献,再加上字符串里 0 的出现次数(每个单字符 0 都是 $$0$$ 位奇数、$$1$$ 位偶数,天然不平衡),最后对 $$10^9+7$$ 取模即为答案。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,倒序扫描 $$n$$ 个位置,每次更新差值数组需要 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,仅需一份长度为 $$2n+1$$ 的差值计数数组以及 $$2$$ 的幂表。

C++

// 困难不平衡数 - 动态规划 (标签和状态)
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;

int main() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    // prev[OFF+d] 表示 f_{i+1}[d]: 用后缀 s[i+1..n-1] 的字符能挑出的、标签和为 d 的子序列数
    // 标签规则: 奇数数字 +1, 偶数数字(含 0) -1; 差值 d 可为负, 统一加偏移 OFF = n
    int OFF = n;
    int SZ = 2 * n + 1;
    vector<long long> prev(SZ, 0), curr(SZ, 0);
    prev[OFF] = 1; // 初始 i=n: 空后缀, 只有空子序列, 标签和为 0

    vector<long long> pw(n + 1, 1);
    for (int i = 1; i <= n; i++) pw[i] = pw[i - 1] * 2 % MOD;

    long long total = 0, balanced = 0;
    // 倒序扫描: 每轮开始时 prev 恰好是 f_{i+1}, 本轮要算出 f_i 存到 curr
    for (int i = n - 1; i >= 0; i--) {
        int digit = s[i] - '0';
        int delta = (digit % 2 == 1) ? 1 : -1; // 0 视作偶数

        if (s[i] != '0') {
            // 固定 s[i] 为子序列首字符, 后缀任取 2^(n-1-i) 种 -> 合法子序列总贡献
            total = (total + pw[n - 1 - i]) % MOD;
            // 其中"平衡"的: 后缀标签和 = -delta (与首字符正好抵消), 这部分要扣掉
            balanced = (balanced + prev[OFF - delta]) % MOD;
        }

        // 计算 f_i[d] = f_{i+1}[d] + f_{i+1}[d - delta]
        // 前一项 = 不选 s[i], 后一项 = 选 s[i]
        for (int d = 0; d < SZ; d++) {
            long long v = prev[d];
            int src = d - delta;
            if (src >= 0 && src < SZ) v = (v + prev[src]) % MOD;
            curr[d] = v;
        }
        swap(prev, curr); // 进入下一轮: prev = f_i
    }

    // 单字符 '0' 不属于"多位前导零", 是合法子序列, 且 0 位奇 1 位偶天然不平衡
    long long zeros = 0;
    for (char c : s) if (c == '0') zeros++;

    long long ans = ((total - balanced + zeros) % MOD + MOD) % MOD;
    cout << ans << "\n";
    return 0;
}

2026-4-1-算法岗

第一题:等步长交换

在线评测链接:https://www.neituiya.com/oj/7/2450

题目描述

给定一个长度为 $$n$$ 的整数数组 $$a$$ 和一个正整数 $$k$$。你可以进行任意次如下操作:选择一个下标 $$i$$,满足 $$1 \le i$$ 且 $$i + k \le n$$,将 $$a_i$$ 与 $$a_{i+k}$$ 交换(即把位置相差 $$k$$ 的两个元素对调)。

在可以无限次操作的前提下,请你给出最终能得到的字典序最大的数组。

字典顺序比较:从两个数组的第一个元素开始逐个比较,直到找到第一个不同的元素,较大元素所在的数组的字典序较大。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, k(1 \le n \le 2 \times 10^5, 1 \le k \le n)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(-10^9 \le a_i \le 10^9)$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一行 $$n$$ 个整数,表示在上述操作下可获得的字典序最大的数组。

样例1

输入

3
5 2
3 1 4 1 5
6 3
1 6 2 5 3 4
4 1
9 8 7 6

输出

5 1 4 1 3
5 6 4 1 3 2
9 8 7 6

第二题:神奇的魔术

在线评测链接:https://www.neituiya.com/oj/7/2451

题目描述

AK机想给"吸铁"操作准备一些新的模数,这些模数必须是质数。现在,AK机随机选择了一个整数 $$x$$,请你找到一个质数 $$p$$,使得 $$|p - x|$$ 尽可能小。如果同时存在两个质数与 $$x$$ 的距离相同(也就是 $$x$$ 左右两侧等距各有一个质数),请输出较小的那个质数。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 30)$$ 代表数据组数,每组测试数据描述如下:

每组测试数据在一行上输入一个整数 $$x(1 \le x \le 10^9)$$,表示给定的数。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示与 $$x$$ 的绝对差值最小的那个质数(若等距,取较小者)。

样例1

输入

6
1
4
10
20
1000000000
31

输出

2
3
11
19
1000000007
31

样例解释

对于 $$x = 1$$,最近的质数是 $$2$$。对于 $$x = 4$$,与 $$4$$ 等距的质数为 $$3$$ 与 $$5$$,根据"等距取较小"的规定,输出 $$3$$。

第三题:字符串压缩

在线评测链接:https://www.neituiya.com/oj/7/2452

题目描述

给定一个只包含 $$0$$ 和 $$1$$ 的字符串 $$s$$,长度为 $$n$$。你可以进行若干次"分段压缩"操作,每次操作规则如下:选择一段由相同字符构成的连续子串,其长度为 $$k(k \ge 2)$$;将其整体压缩为一个"特殊字符"(视为长度 $$1$$ 的新符号,不再属于 $$0/1$$);一次压缩的代价为 $$a[k]$$;被压缩过的段不允许再次参与压缩;不同压缩段不能重叠。

压缩与不压缩的段拼接后得到新的字符串,其长度等于"未压缩的原字符数量 + 压缩段的个数"。给定目标上限 $$m$$,要求将字符串长度压到恰好为 $$m$$ 的同时,使总代价最小。若无法压到长度 $$m$$,输出 $$-1$$。

输入描述

每个测试文件均包含多组测试数据。第一行输入整数 $$T(1 \le T \le 10^2)$$。每组数据描述如下:

第一行输入两个整数 $$n, m(1 \le n \le 500, 1 \le m \le n)$$。

第二行输入一个长度为 $$n$$ 的 $$01$$ 串 $$s$$。

第三行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(1 \le a_i \le 10^9)$$,表示压缩长度为 $$k$$ 的段的代价为 $$a_k$$。

保证所有测试中 $$n$$ 的总和不超过 $$1000$$。

输出描述

对每组数据输出一个整数,表示将字符串长度压到 $$m$$ 所需的最小代价;若无解,输出 $$-1$$。

样例1

输入

3
5 3
00111
3 5 7 9 11
4 2
0101
1 1 1 1
6 2
111000
10 2 5 10 20 50

输出

7
-1
10

2026-4-1-研发岗

第一题:数组对齐

在线评测链接:https://www.neituiya.com/oj/7/2447

题目描述

AK机拿到两个长度均为 $$n$$ 的非负整数数组 $$a_1, a_2, \dots, a_n$$ 与 $$b_1, b_2, \dots, b_n$$。她可以反复执行以下三种操作(每次操作会让所选元素的值减 $$1$$;若被选中的元素当前为 $$0$$,则该次操作不允许执行):

操作1:选择一个下标 $$i$$,令 $$a_i = a_i - 1$$。

操作2:选择一个下标 $$j$$,令 $$b_j = b_j - 1$$。

操作3:选择两个下标 $$i, j$$($$i, j$$ 可以相同,也可以不同),同时令 $$a_i = a_i - 1$$ 且 $$b_j = b_j - 1$$。

现在她希望通过若干次操作,使得最终对所有 $$1 \le i \le n$$ 都满足 $$a_i = b_i$$。

请你计算:最少需要多少次操作才能达成目标。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 表示数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(0 \le a_i \le 10^9)$$。

第三行输入 $$n$$ 个整数 $$b_1, b_2, \dots, b_n(0 \le b_i \le 10^9)$$。

除此之外,保证单个测试文件中所有测试数据的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,新起一行输出一个整数,表示最少操作次数。

样例1

输入

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

输出

2
5

样例解释

第1组:可先执行一次操作3,选 $$(i, j) = (2, 1)$$;再执行一次操作2,选 $$j = 4$$,即可使两数组完全相等,因此最少操作数为 $$2$$。

题解

题目内容拆解

给定两个数组 $$a$$ 和 $$b$$,只能做减法操作使它们逐位相等,求最少操作次数。$$n$$ 可达 $$2 \times 10^5$$,暴力模拟每次操作不可行。

核心观察:对于每个位置 $$i$$,$$a_i$$ 和 $$b_i$$ 的较大者需要被减到较小者的值。如果 $$a_i > b_i$$,需要对 $$a_i$$ 减去 $$a_i - b_i$$ 次;如果 $$b_i > a_i$$,需要对 $$b_i$$ 减去 $$b_i - a_i$$ 次。操作3能同时减一个 $$a$$ 和一个 $$b$$,相当于把两次操作合并成一次。→ 因此采用贪心,尽量多用操作3来节省次数。

算法实现

算法主策略:本题采用贪心,将所有位置需要减 $$a$$ 的量和需要减 $$b$$ 的量分别求和,操作3最多并行消耗其中较小的一方。

设 $$\text{sumA} = \sum_{a_i > b_i} (a_i - b_i)$$ 为 $$a$$ 侧总共需要减少的量,$$\text{sumB} = \sum_{b_i > a_i} (b_i - a_i)$$ 为 $$b$$ 侧总共需要减少的量。操作3每次同时消耗 $$a$$ 侧和 $$b$$ 侧各 $$1$$,最多使用 $$\min(\text{sumA}, \text{sumB})$$ 次。剩余的差额只能用操作1或操作2逐一消耗。因此总操作次数为 $$\max(\text{sumA}, \text{sumB})$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历一次数组计算两侧差值之和。
  • 空间复杂度:$$O(1)$$,只需两个累加变量。

C++

// 数组对齐 - 贪心
#include <bits/stdc++.h>
using namespace std;

long long solve(int n, vector<int>& a, vector<int>& b) {
    long long sumA = 0, sumB = 0;
    for (int i = 0; i < n; i++) {
        long long d = (long long)a[i] - b[i];
        // 正差累计到a侧需要减的总量,负差累计到b侧
        if (d > 0) sumA += d;
        else sumB += -d;
    }
    // 操作3最多并行消耗min(sumA,sumB),剩余只能单独操作
    return max(sumA, sumB);
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<int> a(n), b(n);
        for (int i = 0; i < n; i++) cin >> a[i];
        for (int i = 0; i < n; i++) cin >> b[i];
        cout << solve(n, a, b) << "\n";
    }
    return 0;
}

第二题:约束差值数组

在线评测链接:https://www.neituiya.com/oj/7/2448

题目描述

在幻光实验室中,Alice 需要构造一个长度为 $$n$$ 的正整数数组 $$a$$,其中每个元素 $$a_i > 0$$。但她手中有 $$m$$ 条魔法约束,每条约束给出三元组 $$(i, j, k)$$,要求 $$a_i - a_j = k$$。请你判断是否存在满足所有约束且 $$1 \le a_i \le 10^{18}$$ 的数组 $$a$$。若存在则输出任意一个符合条件的数组;否则输出 -1

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 代表测试组数,每组测试数据描述如下:

第一行输入两个整数 $$n, m(1 \le n, m \le 2 \times 10^5)$$。

接下来 $$m$$ 行,每行输入三个整数 $$i, j, k(1 \le i, j \le n, -10^6 \le k \le 10^6)$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^5$$,$$m$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一行:若存在满足所有约束且 $$1 \le a_i \le 10^{18}$$ 的数组 $$a$$,则输出 $$n$$ 个正整数 $$a_1, a_2, \dots, a_n$$;否则输出 -1

样例1

输入

2
3 2
1 2 1
2 3 1
2 2
1 2 100
2 1 1

输出

3 2 1
-1

题解

题目内容拆解

给定 $$n$$ 个变量和 $$m$$ 条差值约束 $$a_i - a_j = k$$,判断是否存在满足所有约束且值域在 $$[1, 10^{18}]$$ 内的解。$$n, m$$ 可达 $$2 \times 10^5$$,暴力枚举不可行。

核心观察:约束 $$a_i - a_j = k$$ 意味着"只要知道 $$a_j$$ 的值,就能算出 $$a_i = a_j + k$$"。如果多条约束把一组变量串联起来,那么只要确定其中一个变量的值,整组变量的值就全部确定了。这和图的连通分量是同一回事——被约束连起来的变量属于同一组,组内任意一个变量的值确定后,其余全部推导得出。→ 因此采用BFS建图,对每个连通分量选一个起点赋初始值,沿着约束边逐步推导所有变量。

算法实现

算法主策略:本题采用带权BFS,将差值约束建成无向带权图,对每个连通分量 BFS 赋值后检查一致性。

第一步:建图。 每条约束 $$a_i - a_j = k$$ 包含两层含义:已知 $$a_i$$ 可以推出 $$a_j = a_i - k$$,已知 $$a_j$$ 也可以推出 $$a_i = a_j + k$$。因此建两条有向边:$$i \to j$$ 权重 $$-k$$(表示 $$a_j = a_i + (-k)$$),$$j \to i$$ 权重 $$+k$$(表示 $$a_i = a_j + k$$)。边的权重就是"从已知节点到未知节点时需要加上的偏移量"。

第二步:BFS赋值。 对每个尚未赋值的节点 $$s$$ 出发做 BFS。先给 $$s$$ 一个临时值 $$0$$(具体是多少不重要,后面会统一平移)。然后沿着边逐层推导:如果节点 $$u$$ 的值为 $$val[u]$$,沿权重为 $$w$$ 的边到达 $$v$$,则 $$val[v] = val[u] + w$$。以样例1为例:约束 $$a_1 - a_2 = 1$$ 和 $$a_2 - a_3 = 1$$,从节点 $$1$$ 出发设 $$val[1] = 0$$,推导得 $$val[2] = 0 + (-1) = -1$$,$$val[3] = -1 + (-1) = -2$$。

第三步:检查矛盾。 BFS 过程中如果到达一个已经赋过值的节点 $$v$$,此时有两个来源:之前 BFS 赋的旧值和当前推导出的新值。如果两者不同,说明约束之间互相矛盾,输出 $$-1$$。以样例2为例:约束 $$a_1 - a_2 = 100$$ 和 $$a_2 - a_1 = 1$$,前者推出 $$a_1 - a_2 = 100$$,后者推出 $$a_1 - a_2 = -1$$,矛盾。

第四步:平移为正整数。 BFS 得到的值可能包含负数(如上面的 $$-1, -2$$),但题目要求 $$a_i \ge 1$$。做法是找到连通分量内的最小值 $$mn$$,将所有值加上 $$1 - mn$$,使最小值恰好变为 $$1$$。样例1中最小值为 $$-2$$,平移量 $$= 1 - (-2) = 3$$,最终 $$val = [0+3, -1+3, -2+3] = [3, 2, 1]$$。平移后还需检查最大值是否超过 $$10^{18}$$,超过则无解。没有被任何约束涉及的孤立节点,直接赋值 $$1$$。

时空复杂度分析

  • 时间复杂度:$$O(n + m)$$,建图和 BFS 各遍历所有边一次。
  • 空间复杂度:$$O(n + m)$$,存储邻接表和节点值数组。

C++

// 约束差值数组 - BFS建图
#include <bits/stdc++.h>
using namespace std;

void solve() {
    int n, m;
    cin >> n >> m;
    // 建双向带权图:a[i]-a[j]=k 等价于 i→j 权-k, j→i 权k
    vector<vector<pair<int, long long>>> adj(n + 1);
    for (int t = 0; t < m; t++) {
        int i, j;
        long long k;
        cin >> i >> j >> k;
        adj[i].push_back({j, -k});
        adj[j].push_back({i, k});
    }

    vector<long long> val(n + 1, LLONG_MIN);
    bool ok = true;

    for (int s = 1; s <= n && ok; s++) {
        if (val[s] != LLONG_MIN) continue;
        // BFS赋值:起点设0,沿边推出其他节点相对值
        val[s] = 0;
        queue<int> q;
        q.push(s);
        vector<int> comp = {s};
        while (!q.empty() && ok) {
            int u = q.front(); q.pop();
            for (auto& [v, w] : adj[u]) {
                if (val[v] == LLONG_MIN) {
                    val[v] = val[u] + w;
                    q.push(v);
                    comp.push_back(v);
                } else if (val[v] != val[u] + w) {
                    // 推出的值与已有值矛盾,无解
                    ok = false;
                }
            }
        }
        if (!ok) break;
        // 平移使最小值为1,保证所有值为正整数
        long long mn = LLONG_MAX;
        for (int x : comp) mn = min(mn, val[x]);
        long long shift = 1 - mn;
        long long mx = LLONG_MIN;
        for (int x : comp) {
            val[x] += shift;
            mx = max(mx, val[x]);
        }
        if (mx > (long long)1e18) { ok = false; break; }
    }

    if (!ok) {
        cout << "-1\n";
    } else {
        for (int i = 1; i <= n; i++) {
            cout << val[i] << " \n"[i == n];
        }
    }
}

int main() {
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

第三题:中,太中了

在线评测链接:https://www.neituiya.com/oj/7/2449

题目描述

给定一个长度为 $$n$$ 的排列 $$\{a_1, a_2, \dots, a_n\}$$,请你统计对于每个位置 $$i$$,元素 $$a_i$$ 在多少个不同的连续子区间中恰好是该子区间的中位数。换句话说,对于每个位置 $$i$$,统计包含位置 $$i$$ 的所有连续子区间 $$[l \dots r]$$ 中,元素 $$a_i$$ 恰好是该子区间的中位数的个数。

中位数:对于数组 $$\{b_1, b_2, \dots, b_x\}$$,将所有元素从小到大排序后,位于第 $$\lceil\frac{x+1}{2}\rceil$$ 个位置的元素即为该数组的中位数。

长度为 $$n$$ 的排列:由 $$1, 2, \dots, n$$ 这 $$n$$ 个整数、按任意顺序组成的数组(每个整数均恰好出现一次)。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10)$$,表示数据组数。每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 5000)$$,表示数组长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(1 \le a_i \le n)$$,表示一个长度为 $$n$$ 的排列。

除此之外,保证单个测试文件中所有 $$n$$ 的总和不超过 $$5000$$。

输出描述

对于每组测试数据,输出一行,包含 $$n$$ 个整数。其中,第 $$i$$ 个整数表示元素 $$a_i$$ 作为中位数的连续子区间个数。

样例1

输入

2
3
1 2 3
3
2 1 3

输出

1 3 2
3 1 2

样例解释

在第一个样例中,数组 $$\{1, 2, 3\}$$:当 $$i = 1$$ 时,只有子区间 $$\{1\}$$ 的中位数为 $$1$$;当 $$i = 2$$ 时,子区间 $$\{2\}, \{1, 2\}, \{1, 2, 3\}$$ 的中位数均为 $$2$$;当 $$i = 3$$ 时,子区间 $$\{2, 3\}, \{3\}$$ 的中位数为 $$3$$。

题解

题目内容拆解

给定长度为 $$n$$ 的排列,对每个位置统计它作为中位数的子区间个数。暴力枚举所有子区间再排序是 $$O(n^3 \log n)$$,$$n = 5000$$ 时远超时限。

核心观察:判断 $$a_i$$ 是不是某个子区间的中位数,不需要排序,只要看子区间里比 $$a_i$$ 大的和比 $$a_i$$ 小的各有多少个。→ 因此采用枚举中心+平衡计数,对每个位置 $$i$$ 用前缀和技巧 $$O(n)$$ 统计所有合法子区间。

算法实现

算法主策略:本题采用标记 + 前缀和配对。固定位置 $$i$$,给区间内其余元素打标记(大于 $$a_i$$ 记 $$+1$$,小于记 $$-1$$),把中位数判定转化为标记总和的条件,再用桶计数快速统计。

中位数条件推导。 设子区间里比 $$a_i$$ 小的有 $$S$$ 个、大的有 $$L$$ 个,排列无重复,所以区间长度 $$= S + L + 1$$。$$a_i$$ 排序后位于第 $$S + 1$$ 位,题目要求中位数位于第 $$\lceil\frac{S+L+2}{2}\rceil$$ 位,因此 $$a_i$$ 是中位数的条件为 $$S + 1 = \lceil\frac{S+L+2}{2}\rceil$$。分奇偶讨论:

奇数长度 $$S + L + 1 = 2t + 1$$,即 $$S + L = 2t$$:$$\lceil\frac{2t+2}{2}\rceil = t + 1$$,所以 $$S + 1 = t + 1$$,得 $$S = t, L = t$$,即 $$L - S = 0$$。

偶数长度 $$S + L + 1 = 2t$$,即 $$S + L = 2t - 1$$:$$\lceil\frac{2t+1}{2}\rceil = t + 1$$,所以 $$S + 1 = t + 1$$,得 $$S = t, L = t - 1$$,即 $$L - S = -1$$。

给区间内除 $$a_i$$ 外的元素打标记:大于 $$a_i$$ 记 $$+1$$,小于记 $$-1$$,标记总和就是 $$L - S$$。所以 $$a_i$$ 是中位数,当且仅当标记总和为 $$0$$ 或 $$-1$$。

前缀和配对。 包含位置 $$i$$ 的子区间 $$[l, r]$$,其标记和可以拆成左半 $$\text{sumL}$$($$l$$ 到 $$i-1$$ 的标记和)加上右半 $$\text{sumR}$$($$i+1$$ 到 $$r$$ 的标记和)。条件 $$\text{sumL} + \text{sumR} \in \{0, -1\}$$,即对每个 $$\text{sumR}$$,需要找有多少个 $$\text{sumL}$$ 等于 $$-\text{sumR}$$ 或 $$-\text{sumR} - 1$$。

做法是先从 $$i$$ 向左扫一遍,把每种 $$\text{sumL}$$ 出现几次存入计数数组。再从 $$i$$ 向右扫,每扩展一步算出当前 $$\text{sumR}$$,去数组里查配对的 $$\text{sumL}$$ 有几个,累加到答案。注意左侧不取任何元素时 $$\text{sumL} = 0$$,对应子区间从 $$i$$ 开始向右延伸的情况,所以初始要把 $$0$$ 计入一次。前缀和范围在 $$[-n, n]$$ 内,计数数组用下标偏移实现 $$O(1)$$ 访问,避免哈希表的常数开销。每个位置扫一轮 $$O(n)$$,共 $$n$$ 个位置,总 $$O(n^2)$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,每个位置左右各扫一遍,共 $$n$$ 个位置。
  • 空间复杂度:$$O(n)$$,计数数组大小 $$2n + 5$$。

Go

// 中,太中了 - 枚举中心+平衡计数
package main

import (
        "bufio"
        "fmt"
        "os"
        "strings"
)

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                fmt.Fscan(reader, &n)
                a := make([]int, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &a[i])
                }
                // 用定长切片+偏移量替代map,O(1)查询
                offset := n + 2
                sz := 2*n + 5
                ans := make([]string, n)
                for i := 0; i < n; i++ {
                        cnt := make([]int, sz)
                        cnt[0+offset] = 1
                        cur := 0
                        // 向左扩展,统计前缀和频次
                        for j := i - 1; j >= 0; j-- {
                                if a[j] > a[i] {
                                        cur++
                                } else {
                                        cur--
                                }
                                cnt[cur+offset]++
                        }
                        // 向右扩展并匹配
                        res := int64(0)
                        cur = 0
                        for r := i; r < n; r++ {
                                if r > i {
                                        if a[r] > a[i] {
                                                cur++
                                        } else {
                                                cur--
                                        }
                                }
                                // 左+右=0(奇数长度)或 左+右=-1(偶数长度)
                                res += int64(cnt[-cur+offset]) + int64(cnt[-cur-1+offset])
                        }
                        ans[i] = fmt.Sprintf("%d", res)
                }
                fmt.Fprintln(writer, strings.Join(ans, " "))
        }
}

2026-3-28后端开发岗

第一题:列车相对静止

在线评测链接:https://www.neituiya.com/oj/7/2414

第二题:隐式素数

在线评测链接:https://www.neituiya.com/oj/7/2415

第三题:二进制操作

在线评测链接:https://www.neituiya.com/oj/7/2416

2026-3-28-研发岗

第一题:值

在线评测链接:https://www.neituiya.com/oj/7/2411

题目描述

给定一个正整数 $$x$$,请你构造一个十进制正整数 $$n$$,使得 $$n$$ 的十进制数位长度与 $$n$$ 的值的乘积恰好等于 $$x$$。

这里,十进制数位长度指的是 $$n$$ 的十进制表示中,去掉所有前导零后剩余的数字个数。例如:$$0$$ 的数位长度为 $$1$$,$$7$$ 的数位长度为 $$1$$,$$120$$ 的数位长度为 $$3$$。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,表示数据组数。

此后 $$T$$ 行,每行输入一个正整数 $$x(1 \le x \le 10^{18})$$。

输出描述

对于每组数据,输出一个整数。如果存在 $$len(n) \times n = x$$,输出任意一个符合条件的 $$n$$;如果不存在输出 $$-1$$。其中 $$n$$ 必须为正整数,且满足 $$1 \le n \le 10^{18}$$。

如果存在多个解决方案,您可以输出任意一个,系统会自动判定是否正确。

样例1

输入

4
1
3
20
200

输出

1
3
10
-1

样例解释

当 $$x = 1$$ 时,$$n = 1$$ 满足 $$len(1) \cdot 1 = 1$$。

当 $$x = 3$$ 时,$$n = 3$$ 满足 $$len(3) \cdot 3 = 1 \cdot 3 = 3$$。

当 $$x = 20$$ 时,$$n = 10$$ 满足 $$len(10) \cdot 10 = 2 \cdot 10 = 20$$。

当 $$x = 200$$ 时,不存在任何 $$n$$ 满足条件,因此输出 $$-1$$。

题解

题目内容拆解

给定 $$x$$,找正整数 $$n$$ 使得 $$len(n) \times n = x$$。$$n$$ 的位数 $$d$$ 最多 $$18$$ 位($$x \le 10^{18}$$),可以枚举 $$d$$。

算法实现

算法主策略:本题采用枚举数位长度的方法。

$$n$$ 的数位长度 $$d$$ 取值范围为 $$1$$ 到 $$18$$。对于每个 $$d$$:

  1. 检查 $$x$$ 是否能被 $$d$$ 整除,若不能则跳过。
  2. 令 $$n = x / d$$,检查 $$n$$ 的十进制数位长度是否恰好为 $$d$$。

3) 若满足则输出 $$n$$,否则继续尝试下一个 $$d$$。

所有 $$d$$ 都不满足则输出 $$-1$$。

样例验证:$$x = 20$$,$$d = 2$$ 时 $$n = 10$$,$$len(10) = 2$$,匹配。$$x = 200$$,$$d = 1$$ 时 $$n = 200$$($$len = 3 \ne 1$$),$$d = 2$$ 时 $$n = 100$$($$len = 3 \ne 2$$),$$d = 3$$ 时不整除,全部不满足,输出 $$-1$$。

时空复杂度分析

  • 时间复杂度:$$O(18)$$ 每组查询,总计 $$O(18T)$$。
  • 空间复杂度:$$O(1)$$,只需常数额外空间。

C++

// 值 - 枚举数位长度
#include <bits/stdc++.h>
using namespace std;

// 计算十进制数位长度
int digitLen(long long n) {
    if (n == 0) return 1;
    int len = 0;
    while (n > 0) {
        len++;
        n /= 10;
    }
    return len;
}

// 枚举可能的数位长度d,检查x/d是否恰好d位
long long solve(long long x) {
    for (int d = 1; d <= 18; d++) {
        if (x % d != 0) continue;
        long long n = x / d;
        if (digitLen(n) == d) return n;
    }
    return -1;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        long long x;
        cin >> x;
        cout << solve(x) << "\n";
    }
    return 0;
}

第二题:不稳定or相似

在线评测链接:https://www.neituiya.com/oj/7/2412

题目描述

给定两个整数 $$n, m$$,你需要构造一个长度为 $$n$$ 的非负整数数组 $$a = \{a_1, a_2, \ldots, a_n\}$$,使其元素总和满足:

$$\sum_{i=1}^{n} a_i = m$$

定义相邻差和:

$$F(a) = \sum_{i=1}^{n-1} |a_i - a_{i+1}|$$

本题有 $$q$$ 次独立询问。第 $$j$$ 次询问给出一对整数 $$(d, y)$$,你最多只能让数组中相邻数值不同的边的数量不超过 $$d$$,即:

$$\#\{i \in [1, n-1] \mid a_i \ne a_{i+1}\} \le d$$

并问在该限制下,是否存在某个数组 $$a$$ 使 $$F(a) \ge y$$。若存在,输出 YES;否则输出 NO。

注意:不同询问彼此独立,均基于同一对 $$(n, m)$$ 重新构造数组。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$t(1 \le t \le 10^4)$$,表示测试用例数量。

对于每个测试用例:

第一行输入三个整数 $$n, m, q(1 \le n \le 10^9, 0 \le m \le 10^9, 1 \le q \le 2 \times 10^5)$$。

此后 $$q$$ 行,每行输入两个整数 $$d, y(0 \le d \le n - 1, 0 \le y \le 10^{18})$$。

保证全部测试用例的 $$q$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每个询问,输出一行 YES 或 NO,表示是否存在满足限制的数组 $$a$$ 使 $$F(a) \ge y$$。

样例1

输入

2
5 7 5
0 0
1 7
1 8
2 14
2 15
1 3 2
0 0
0 1

输出

NO
YES
NO
YES
NO
YES
NO

样例解释

对测试用例一,$$n = 5, m = 7$$:

若 $$d = 0, y = 0$$:数组必须为常数列,但 $$m \bmod n \ne 0$$ 无法构造,输出 NO。

若 $$d = 1, y = 7$$:取 $$[7, 0, 0, 0, 0]$$,边数为 $$1$$,$$F = 7 \ge 7$$,输出 YES。

题解

题目内容拆解

构造长度 $$n$$、总和 $$m$$ 的非负整数数组,至多 $$d$$ 个相邻不同的位置,求 $$F(a)$$ 的最大值是否 $$\ge y$$。$$n, m$$ 可达 $$10^9$$,$$q$$ 总和 $$2 \times 10^5$$,需要 $$O(1)$$ 回答每个查询。

算法实现

算法主策略:本题通过数学分析直接推导出最大 $$F(a)$$。

令 $$ed = \min(d, n - 1)$$(实际可用的不同边数不超过 $$n - 1$$)。

情况 1:$$m = 0$$。所有元素为 $$0$$,$$F = 0$$。

情况 2:$$n = 1$$。只有一个元素,无相邻对,$$F = 0$$。

情况 3:$$ed = 0$$。数组必须为常数列,需 $$m \bmod n = 0$$。若整除,$$F = 0$$;否则无法构造合法数组,任何 $$y$$ 都输出 NO。

情况 4:$$ed = 1$$。把所有 $$m$$ 集中到端点,如 $$[m, 0, \ldots, 0]$$,仅 $$1$$ 个不同边,$$F = m$$。

情况 5:$$ed \ge 2$$。把所有 $$m$$ 集中到某个内部位置,如 $$[0, m, 0, \ldots, 0]$$(需 $$n \ge 3$$),有 $$2$$ 个不同边,$$F = 2m$$。这是理论上界(每个元素对 $$F$$ 的贡献至多为 $$2a_i$$,求和得 $$F \le 2m$$)。

样例验证:$$n = 5, m = 7$$。$$ed = 2$$ 时 $$maxF = 14$$,$$y = 14$$ 输出 YES,$$y = 15$$ 输出 NO。$$n = 1, m = 3$$ 时 $$maxF = 0$$,$$y = 0$$ 输出 YES,$$y = 1$$ 输出 NO。

时空复杂度分析

  • 时间复杂度:$$O(q)$$,每个查询 $$O(1)$$。
  • 空间复杂度:$$O(1)$$,只需常数额外空间。

C++

// 不稳定or相似 - 贪心(数学分析)
#include <bits/stdc++.h>
using namespace std;

// 计算最大F(a),返回-1表示无法构造
long long maxF(long long n, long long m, long long d) {
    long long ed = min(d, n - 1);
    if (m == 0 || n == 1) return 0;
    if (ed == 0) {
        // 常数列,需m整除n
        if (m % n == 0) return 0;
        return -1;  // 无法构造
    }
    if (ed == 1) return m;
    return 2 * m;  // ed >= 2
}

int main() {
    int t;
    cin >> t;
    while (t--) {
        long long n, m, q;
        cin >> n >> m >> q;
        while (q--) {
            long long d, y;
            cin >> d >> y;
            long long mf = maxF(n, m, d);
            if (mf == -1 || y > mf) {
                cout << "NO\n";
            } else {
                cout << "YES\n";
            }
        }
    }
    return 0;
}

第三题:递增

在线评测链接:https://www.neituiya.com/oj/7/2413

题目描述

给定一棵由 $$n$$ 个节点(编号为 $$1 \sim n$$)和 $$n - 1$$ 条边构成的、根节点为 $$1$$ 的树。

初始时,每个节点 $$i$$ 的权值为 $$a_i = i$$。

接下来共有 $$m$$ 次修改操作。第 $$j$$ 次操作($$j$$ 从 $$1$$ 开始)给出一个节点 $$x$$:找出以 $$x$$ 为根的子树中当前权值最小的节点,并将该节点的权值修改为 $$j + n$$。

请输出所有节点在所有操作完成后的最终权值。

输入描述

第一行输入两个整数 $$n, m(2 \le n, m \le 10^5)$$,分别表示树的节点数量和操作次数。

此后 $$n - 1$$ 行,每行输入两个整数 $$u_i, v_i(1 \le u_i, v_i \le n, u_i \ne v_i)$$,表示树上第 $$i$$ 条边。保证构成一棵以节点 $$1$$ 为根的树。

随后 $$m$$ 行,每行输入一个整数 $$x(1 \le x \le n)$$,表示第 $$j$$ 次操作的节点编号。

输出描述

在一行上输出 $$n$$ 个整数,分别表示节点 $$1$$ 到节点 $$n$$ 的最终权值,整数之间用空格分隔。

样例1

输入

3 2
1 2
1 3
1
2

输出

4 5 3

样例解释

初始时 $$a = \{1, 2, 3\}$$。

第 $$1$$ 次操作 $$x = 1$$,子树为所有节点,最小权值节点为 $$1$$,更新其权值为 $$1 + 3 = 4$$。

第 $$2$$ 次操作 $$x = 2$$,子树仅含节点 $$2$$,更新其权值为 $$2 + 3 = 5$$。

最终权值为 $$\{4, 5, 3\}$$。

题解

题目内容拆解

每次操作需要找子树中当前权值最小的节点并替换为 $$j + n$$。注意同一节点可能被多次修改(例如叶节点被反复操作)。$$n, m \le 10^5$$,需要 $$O((n + m) \log n)$$ 解法。

算法实现

算法主策略:本题采用 DFS序 + 线段树

核心思路:用 DFS 序将子树映射为连续区间,线段树维护区间内的最小权值及其对应节点编号。

  1. 对树做 DFS,记录每个节点的入时间 $$in[x]$$ 和出时间 $$out[x]$$,子树对应 DFS 序上的连续区间 $$[in[x], out[x]]$$。
  2. 建立线段树,存储 $$(权值, 节点编号)$$ 对,初始时位置 $$in[v]$$ 存储 $$(v, v)$$(初始权值等于节点编号)。

3) 每次操作 $$x$$:在区间 $$[in[x], out[x]]$$ 上查询最小权值对应的节点 $$v$$,将 $$v$$ 的权值更新为 $$j + n$$,并在线段树中将 $$in[v]$$ 位置更新为 $$(j + n, v)$$。

关键细节:不能将已修改节点标记为无穷大,因为同一节点可能被多次操作(如叶节点被反复选中),必须保留其实际权值。

样例验证:树 $$1 \to \{2, 3\}$$,DFS 序 $$[1, 2, 3]$$,$$in = [1, 2, 3]$$,$$out = [3, 2, 3]$$。操作 1:$$x = 1$$,区间 $$[1, 3]$$ 最小权值为 $$(1, 1)$$,更新为 $$(4, 1)$$。操作 2:$$x = 2$$,区间 $$[2, 2]$$ 最小权值为 $$(2, 2)$$,更新为 $$(5, 2)$$。最终 $$\{4, 5, 3\}$$。

时空复杂度分析

  • 时间复杂度:$$O((n + m) \log n)$$,DFS 为 $$O(n)$$,每次线段树查询和更新为 $$O(\log n)$$。
  • 空间复杂度:$$O(n)$$,用于线段树和 DFS 序数组。

C++

// 递增 - DFS序 + 线段树
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
const int INF = 1e9;

vector<int> adj[MAXN];
int tin[MAXN], tout[MAXN], order_arr[MAXN];
int timer_val = 0;
// 线段树存储(权值, 节点编号)对
pair<int, int> seg[4 * MAXN];
int ans_arr[MAXN];

// 非递归DFS求DFS序
void dfs(int root, int n) {
    stack<pair<int, int>> st;
    vector<bool> visited(n + 1, false);
    st.push({root, 0});
    while (!st.empty()) {
        auto [u, parent] = st.top();
        if (!visited[u]) {
            visited[u] = true;
            tin[u] = timer_val;
            order_arr[timer_val] = u;
            timer_val++;
            // 反序push子节点,保证DFS序正确
            for (int i = adj[u].size() - 1; i >= 0; i--) {
                if (adj[u][i] != parent) {
                    st.push({adj[u][i], u});
                }
            }
        } else {
            tout[u] = timer_val - 1;
            st.pop();
        }
    }
}

// 线段树建树:初始权值=节点编号
void build(int node, int l, int r) {
    if (l == r) {
        seg[node] = {order_arr[l], order_arr[l]};
        return;
    }
    int mid = (l + r) / 2;
    build(2 * node, l, mid);
    build(2 * node + 1, mid + 1, r);
    seg[node] = min(seg[2 * node], seg[2 * node + 1]);
}

// 区间最小值查询,返回(最小权值, 节点编号)
pair<int, int> query(int node, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) return seg[node];
    if (ql > r || qr < l) return {INF, 0};
    int mid = (l + r) / 2;
    return min(query(2 * node, l, mid, ql, qr),
               query(2 * node + 1, mid + 1, r, ql, qr));
}

// 单点更新为新的(权值, 节点编号)
void update(int node, int l, int r, int pos, pair<int, int> val) {
    if (l == r) {
        seg[node] = val;
        return;
    }
    int mid = (l + r) / 2;
    if (pos <= mid) update(2 * node, l, mid, pos, val);
    else update(2 * node + 1, mid + 1, r, pos, val);
    seg[node] = min(seg[2 * node], seg[2 * node + 1]);
}

int main() {
    int n, m;
    cin >> n >> m;

    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    dfs(1, n);
    build(1, 0, n - 1);

    // 初始权值
    for (int i = 1; i <= n; i++) ans_arr[i] = i;

    for (int j = 1; j <= m; j++) {
        int x;
        cin >> x;
        // 查询子树中当前权值最小的节点
        auto [minVal, minNode] = query(1, 0, n - 1, tin[x], tout[x]);
        ans_arr[minNode] = j + n;
        // 更新为实际新权值(节点可能被多次修改)
        update(1, 0, n - 1, tin[minNode], {j + n, minNode});
    }

    for (int i = 1; i <= n; i++) {
        cout << ans_arr[i];
        if (i < n) cout << " ";
    }
    cout << "\n";
    return 0;
}

2026-3-25-研发岗

第一题:圣诞老人分糖果

在线评测链接:https://www.neituiya.com/oj/7/2392

题目描述

圣诞老人有 $$n$$ 种糖果,第 $$i$$ 种糖果有 $$c_i$$ 个。他要把这些糖果分给 $$k$$ 个小朋友。分配规则如下:

  1. 每一种糖果必须全部分完。
  2. 对于任意一种糖果,任意两个小朋友所分得的该种糖果的数量之差不超过 $$1$$。

在所有合法分配中,任选一个小朋友,统计其最终得到的糖果总数;求该数值在所有合法分配方案中可能达到的最小值与最大值。换句话说,求在所有合法分配方案中,任意一个小朋友所能获得的糖果总数最小值与最大值。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, k(1 \le n \le 2 \times 10^5, 1 \le k \le 10^9)$$。

第二行输入 $$n$$ 个整数 $$c_1, \ldots, c_n(0 \le c_i \le 10^9)$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出两个整数:在所有合法分配中,单个小朋友可能得到的糖果总数的最小值与最大值。

样例1

输入

3
3 2
5 2 7
4 3
0 0 0 0
2 5
10 1

输出

6 8
0 0
2 3

样例解释

第一组:$$k = 2$$ 个小朋友,$$3$$ 种糖果数量为 $$5, 2, 7$$。第一种糖果分为 $$3$$ 和 $$2$$,第二种各分 $$1$$,第三种分为 $$4$$ 和 $$3$$。每种糖果的"多 $$1$$"可以灵活分配给不同的小朋友。最多的一个小朋友可以拿到 $$3 + 1 + 4 = 8$$,最少的可以拿到 $$2 + 1 + 3 = 6$$。

第二组:所有糖果为 $$0$$,结果为 $$0, 0$$。

第三组:$$k = 5$$,糖果 $$[10, 1]$$。第一种每人 $$2$$,第二种只有 $$1$$ 个小朋友得到 $$1$$。最大 $$2 + 1 = 3$$,最小 $$2 + 0 = 2$$。

题解

题目内容拆解

$$n$$ 种糖果分给 $$k$$ 个小朋友,每种糖果两人之差至多为 $$1$$。求单个小朋友总糖果数的全局最小值和最大值。$$n$$ 之和 $$\le 5 \times 10^5$$,需要 $$O(n)$$ 单组。

核心观察:每种糖果独立分配,第 $$i$$ 种有 $$c_i \bmod k$$ 个小朋友多拿 $$1$$ 个。不同种类的"多 $$1$$"可以灵活分配给不同的小朋友。

算法实现

算法主策略:本题采用数学推导 + 贪心分配

对于第 $$i$$ 种糖果,设 $$base_i = \lfloor c_i / k \rfloor$$,$$extra_i = c_i \bmod k$$。每个小朋友至少从第 $$i$$ 种糖果拿到 $$base_i$$ 个,其中 $$extra_i$$ 个小朋友多拿 $$1$$ 个。

求最大值:要让某个小朋友总数最多,尽量让他在每种有余数的糖果中都拿到"多 $$1$$"。只要 $$extra_i > 0$$,就可以把这个小朋友安排为多拿的人之一。因此最大值 $$= \sum base_i + cnt$$,其中 $$cnt$$ 是 $$extra_i > 0$$ 的种类数。

求最小值:要让某个小朋友总数最少,尽量不让他拿到任何"多 $$1$$"。总共有 $$S = \sum extra_i$$ 个"多 $$1$$"要分配给 $$k$$ 个小朋友,每个小朋友最多承担 $$n$$ 个(每种最多 $$1$$ 个)。其余 $$k - 1$$ 个小朋友最多承担 $$(k - 1) \times n$$ 个。如果 $$S \le (k - 1) \times n$$,目标小朋友可以不拿任何"多 $$1$$";否则至少拿 $$S - (k - 1) \times n$$ 个。因此最小值 $$= \sum base_i + \max(0, S - (k - 1) \times n)$$。

以样例第一组为例:$$base = [2, 1, 3]$$,$$extra = [1, 0, 1]$$。$$\sum base = 6$$,$$cnt = 2$$,$$S = 2$$。最大值 $$= 6 + 2 = 8$$。$$(k - 1) \times n = 1 \times 3 = 3 \ge S = 2$$,最小值 $$= 6 + 0 = 6$$。

时空复杂度分析

  • 时间复杂度:$$O(\sum n)$$,每组测试遍历 $$n$$ 种糖果各一次。
  • 空间复杂度:$$O(n)$$,存储糖果数组。

Java

// 圣诞老人分糖果 - 数学/贪心
import java.io.*;
import java.util.*;

public class Main {
    static long[] solve(int n, long k, long[] c) {
        long baseSum = 0, cntPos = 0, extraSum = 0;
        for (int i = 0; i < n; i++) {
            baseSum += c[i] / k;       // 每人至少拿到的部分
            long r = c[i] % k;         // 多出来需要分配的余数
            if (r > 0) cntPos++;
            extraSum += r;
        }
        // 最大值:每种有余数的糖果都给目标小朋友+1
        long maxVal = baseSum + cntPos;
        // 最小值:其余k-1人最多承担(k-1)*n个extra
        long minVal = baseSum + Math.max(0L, extraSum - (k - 1) * n);
        return new long[]{minVal, maxVal};
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        int T = Integer.parseInt(br.readLine().trim());
        while (T-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine().trim());
            int n = Integer.parseInt(st.nextToken());
            long k = Long.parseLong(st.nextToken());
            StringTokenizer st2 = new StringTokenizer(br.readLine().trim());
            long[] c = new long[n];
            for (int i = 0; i < n; i++) c[i] = Long.parseLong(st2.nextToken());
            long[] ans = solve(n, k, c);
            sb.append(ans[0]).append(" ").append(ans[1]).append("\n");
        }
        System.out.print(sb);
    }
}

第二题:公共子序列

在线评测链接:https://www.neituiya.com/oj/7/2393

题目描述

给定两个长度为 $$n$$ 的排列 $$p$$ 和 $$q$$(均为 $$1$$ 到 $$n$$ 的一个排列,元素两两不同),请你在它们的公共子序列中,找到一条字典序最大的序列并输出。

名词解释:

字典序: 对两序列从左到右比较第一个不同的元素,较大者字典序更大;若一个序列是另一个序列的前缀,则更长的序列字典序更大。

子序列: 子序列为从原序列中删除任意个(可以为零、可以为全部)元素得到的新序列。

数组公共子序列: 如果数组 $$a$$ 的一个子序列 $$a'$$ 与数组 $$b$$ 的一个子序列 $$b'$$ 完全相等,那么子序列 $$a', b'$$ 是数组 $$a, b$$ 的一个公共子序列。

输入描述

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示排列的长度。

第二行输入 $$n$$ 个整数 $$p_1, p_2, \ldots, p_n$$,表示排列 $$p$$。

第三行输入 $$n$$ 个整数 $$q_1, q_2, \ldots, q_n$$,表示排列 $$q$$。

保证 $$p$$ 与 $$q$$ 都是 $$1$$ 到 $$n$$ 的排列。

输出描述

第一行输出一个整数 $$k$$,表示答案序列的长度。

第二行输出 $$k$$ 个整数,为这条字典序最大的公共子序列。

样例1

输入

5
3 5 1 2 4
2 5 4 1 3

输出

2
5 4

样例解释

两序列的公共子序列中,以 $$5$$ 开头后,能够继续选择的最大元素为 $$4$$,得到 $$[5, 4]$$。与 $$[5, 1]$$ 相比,第二个元素更大,因此 $$[5, 4]$$ 的字典序更大。

⚠️ 原始题目样例数据有误(与第二题样例重复),此处为根据样例解释重新构造的等价样例。

题解

题目内容拆解

给定两个长度为 $$n$$ 的排列,求它们的字典序最大的公共子序列。$$n \le 2 \times 10^5$$ 要求 $$O(n)$$ 或 $$O(n \log n)$$ 算法。

核心观察:要让公共子序列字典序最大,应当从最大的值开始贪心选择。

算法实现

算法主策略:本题采用贪心策略,从值 $$n$$ 到 $$1$$ 逐个尝试加入公共子序列。

预处理每个值在 $$p$$ 和 $$q$$ 中的位置,记为 $$pos\_p[v]$$ 和 $$pos\_q[v]$$。维护两个指针 $$ip$$ 和 $$iq$$,分别表示当前在 $$p$$ 和 $$q$$ 中已选到的位置之后。

从 $$v = n$$ 到 $$v = 1$$ 遍历:如果 $$pos\_p[v] \ge ip$$ 且 $$pos\_q[v] \ge iq$$,说明值 $$v$$ 出现在两个排列当前位置之后,可以加入结果。选中后,将 $$ip$$ 和 $$iq$$ 分别更新为 $$pos\_p[v] + 1$$ 和 $$pos\_q[v] + 1$$。

正确性:从大到小遍历保证每次选的是当前可选范围内的最大值,这是字典序最大的贪心选择。选中 $$v$$ 后缩小范围不会影响更小值的选择。

以样例为例:$$p = [3, 5, 1, 2, 4]$$,$$q = [2, 5, 4, 1, 3]$$。$$v = 5$$ 时,$$pos\_p[5] = 1 \ge 0$$,$$pos\_q[5] = 1 \ge 0$$,选入,更新 $$ip = 2$$,$$iq = 2$$。$$v = 4$$ 时,$$pos\_p[4] = 4 \ge 2$$,$$pos\_q[4] = 2 \ge 2$$,选入。$$v = 3, 2, 1$$ 均不满足条件。最终结果 $$[5, 4]$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,预处理位置数组 $$O(n)$$,贪心遍历 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储位置数组。

Java

// 公共子序列 - 贪心
import java.io.*;
import java.util.*;

public class Main {
    static List<Integer> solve(int n, int[] p, int[] q) {
        // posP[v] 记录值v在排列p中的下标位置
        int[] posP = new int[n + 1];
        int[] posQ = new int[n + 1];
        for (int i = 0; i < n; i++) {
            posP[p[i]] = i;
            posQ[q[i]] = i;
        }
        List<Integer> result = new ArrayList<>();
        int ip = 0, iq = 0;  // 当前在p和q中已选到的位置之后
        // 从最大值开始贪心,保证字典序最大
        for (int v = n; v >= 1; v--) {
            if (posP[v] >= ip && posQ[v] >= iq) {
                result.add(v);
                ip = posP[v] + 1;
                iq = posQ[v] + 1;
            }
        }
        return result;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        StringTokenizer st1 = new StringTokenizer(br.readLine().trim());
        int[] p = new int[n];
        for (int i = 0; i < n; i++) p[i] = Integer.parseInt(st1.nextToken());
        StringTokenizer st2 = new StringTokenizer(br.readLine().trim());
        int[] q = new int[n];
        for (int i = 0; i < n; i++) q[i] = Integer.parseInt(st2.nextToken());

        List<Integer> res = solve(n, p, q);
        StringBuilder sb = new StringBuilder();
        sb.append(res.size()).append("\n");
        for (int i = 0; i < res.size(); i++) {
            if (i > 0) sb.append(" ");
            sb.append(res.get(i));
        }
        System.out.println(sb.toString());
    }
}

第三题:喜欢的正整数

在线评测链接:https://www.neituiya.com/oj/7/2394

题目描述

AK机不喜欢能被 $$3$$ 整除的正整数,也不喜欢十进制表示中包含数字 $$3$$ 的正整数。

现在他要将所有他喜欢的正整数按升序排成一个序列,给定正整数 $$k$$,请你找出这个序列的第 $$k$$ 个数。

友情提醒:第 $$10^{18} + 1$$ 项为 $$10\,995\,467\,216\,611\,448\,857$$。

输入描述

第一行输入一个整数 $$t(1 \le t \le 10^4)$$,表示测试用例的数量。

接下来 $$t$$ 行,每行输入一个整数 $$k(1 \le k \le 10^{18})$$,表示要找的第 $$k$$ 个喜欢的正整数的序号。

输出描述

对于每个测试用例,在一行上输出一个整数,表示所求的第 $$k$$ 个喜欢的正整数。

样例1

输入

5
1
2
3
4
5

输出

1
2
4
5
7

样例解释

在这个样例中,喜欢的正整数序列的前五项为 $$1, 2, 4, 5, 7$$,因此对应输出分别为 $$1, 2, 4, 5, 7$$。

题解

题目内容拆解

将所有不能被 $$3$$ 整除且不含数字 3 的正整数按升序排列,求第 $$k$$ 个。$$k$$ 最大为 $$10^{18}$$,答案约 $$10^{19}$$,无法枚举。

核心观察:如果能快速计算 $$1$$ 到 $$x$$ 中有多少个"喜欢的数",就可以对答案做二分查找。

算法实现

算法主策略:本题采用二分答案 + 数位DP

数位DP:给定上界 $$x$$,统计 $$[1, x]$$ 中满足"不含数字 3 且不被 $$3$$ 整除"的数的个数。将 $$x$$ 按十进制拆分为 $$d_1 d_2 \ldots d_m$$,从高位到低位逐位决策。每一位可选 $$0 \sim 9$$ 中除 $$3$$ 以外的数字。维护两个状态:当前数字和对 $$3$$ 取模的余数(用于判断整除性)、是否受上界约束(tight)。最终统计余数不为 $$0$$ 的方案数。

二分查找:对于每个 $$k$$,二分答案 $$x$$,找到最小的 $$x$$ 使得 $$count(x) \ge k$$。由于答案最大约 $$1.1 \times 10^{19}$$,二分范围设为 $$[1, 2 \times 10^{19}]$$,约 $$64$$ 次迭代。

以样例为例:$$k = 3$$ 时,二分查找到 $$x = 4$$,$$count(4) = 3$$(即 $$1, 2, 4$$),正好为第 $$3$$ 个。

时空复杂度分析

  • 时间复杂度:$$O(t \cdot \log(\text{ans}) \cdot d \cdot 10 \cdot 3)$$,其中 $$d \le 20$$ 为答案位数,每次数位DP为 $$O(d \times 10 \times 3)$$,二分约 $$64$$ 次。总计约 $$O(t \times 64 \times 600)$$。
  • 空间复杂度:$$O(d \times 3)$$,数位DP的状态空间。

Java

// 喜欢的正整数 - 二分 + 数位DP
import java.io.*;
import java.math.BigInteger;

public class Main {
    static final BigInteger TWO = BigInteger.valueOf(2);

    // 统计[1,x]中不被3整除且不含数字3的正整数个数
    static long countLiked(BigInteger x) {
        if (x.compareTo(BigInteger.ZERO) <= 0) return 0;
        String s = x.toString();
        int n = s.length();
        // tight[rem][started]: 受上界约束的方案数
        // free[rem][started]:  不受上界约束的方案数
        // rem=数字和mod3, started=是否已放置非零数字
        long[][] tight = new long[3][2];
        long[][] free = new long[3][2];
        tight[0][0] = 1;  // 初始:余数0、未开始、受约束
        for (int i = 0; i < n; i++) {
            int dlim = s.charAt(i) - '0';  // 当前位的上界数字
            long[][] nt = new long[3][2];
            long[][] nf = new long[3][2];
            for (int rem = 0; rem < 3; rem++) {
                for (int st = 0; st < 2; st++) {
                    // 受约束状态:只能选[0, dlim]
                    if (tight[rem][st] > 0) {
                        long cnt = tight[rem][st];
                        for (int d = 0; d <= dlim; d++) {
                            if (d == 3) continue;  // 排除含数字3的数
                            int nr = (rem + d) % 3; // 累加数字和mod3
                            int ns = (st == 1 || d > 0) ? 1 : 0;
                            if (d == dlim) nt[nr][ns] += cnt;  // 顶到上界,仍受约束
                            else nf[nr][ns] += cnt;  // 未顶上界,后续自由
                        }
                    }
                    // 不受约束状态:可以选[0, 9]
                    if (free[rem][st] > 0) {
                        long cnt = free[rem][st];
                        for (int d = 0; d <= 9; d++) {
                            if (d == 3) continue;
                            int nr = (rem + d) % 3;
                            int ns = (st == 1 || d > 0) ? 1 : 0;
                            nf[nr][ns] += cnt;
                        }
                    }
                }
            }
            tight = nt;
            free = nf;
        }
        // 数字和mod3!=0 表示不被3整除,started=1排除数字0本身
        long res = 0;
        for (int rem = 1; rem < 3; rem++)
            res += tight[rem][1] + free[rem][1];
        return res;
    }

    static BigInteger solve(long k) {
        // 答案最大约1.1*10^19,超过long范围,用BigInteger做二分
        BigInteger lo = BigInteger.ONE;
        BigInteger hi = new BigInteger("18000000000000000000");
        while (lo.compareTo(hi) < 0) {
            BigInteger mid = lo.add(hi).divide(TWO);
            if (countLiked(mid) >= k) hi = mid;
            else lo = mid.add(BigInteger.ONE);
        }
        return lo;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        int t = Integer.parseInt(br.readLine().trim());
        while (t-- > 0) {
            long k = Long.parseLong(br.readLine().trim());
            sb.append(solve(k)).append("\n");
        }
        System.out.print(sb);
    }
}

2026-3-25-算法岗

第一题:三星数字

在线评测链接:https://www.neituiya.com/oj/7/2389

题目描述

给定一个整数 $$n$$,请你找到两个不同的正整数 $$x, y$$,满足 $$1 \le x, y < n$$ 且 $$x \ne y$$,并且有 $$n \bmod x = n \bmod y$$。

如果有多个满足条件的答案,你可以输出任意一组;如果无解,请输出 $$-1$$。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 10^{18})$$ 表示询问。

输出描述

对于每一组测试数据,新起一行:

若无解,输出 $$-1$$;

若有解,输出两个整数 $$x, y$$,满足 $$1 \le x, y < n, x \ne y$$ 且 $$n \bmod x = n \bmod y$$。

如果存在多个解决方案,您可以输出任意一个,系统会自动判定是否正确。注意,自测运行功能可能因此返回错误结果,请自行检查答案正确性。

样例1

输入

3
1
8
15

输出

-1
2 4
3 5

题解

题目内容拆解

给定 $$n$$,找两个不同正整数 $$x, y < n$$ 使得 $$n \bmod x = n \bmod y$$。$$n$$ 可达 $$10^{18}$$,$$T$$ 达 $$10^4$$,需要 $$O(1)$$ 构造。

算法实现

算法主策略:本题采用分类构造,按 $$n$$ 的奇偶性分别给出方案。

核心观察:对任意 $$n \ge 2$$,$$n \bmod (n-1) = 1$$(因为 $$n = 1 \times (n-1) + 1$$)。同时 $$n \bmod 1 = 0$$,$$n \bmod 2 = 0$$(偶数)或 $$1$$(奇数)。

$$n \le 3$$ 时无解:$$n = 1$$ 时无合法 $$x$$;$$n = 2$$ 时只有 $$x = 1$$,无法取两个不同值;$$n = 3$$ 时 $$3 \bmod 1 = 0, 3 \bmod 2 = 1$$,仅两个值且不等,无法匹配。

$$n$$ 为偶数且 $$n \ge 4$$:取 $$x = 1, y = 2$$,因为 $$n \bmod 1 = 0$$,$$n \bmod 2 = 0$$,两者相等。

$$n$$ 为奇数且 $$n \ge 5$$:取 $$x = 2, y = n - 1$$。$$n \bmod 2 = 1$$(奇数除以 $$2$$ 余 $$1$$),$$n \bmod (n - 1) = 1$$,两者相等。此时 $$2 \ne n - 1$$ 成立($$n \ge 5$$)。

时空复杂度分析

  • 时间复杂度:$$O(T)$$,每组测试 $$O(1)$$ 构造。
  • 空间复杂度:$$O(1)$$,只用常数变量。

第二题:该博弈了

在线评测链接:https://www.neituiya.com/oj/7/2390

题目描述

有一个 $$n \times m$$ 的棋盘,记第 $$i$$ 行第 $$j$$ 列为 $$a_{i,j}$$,其字符仅为 '0''1'。棋盘上有且仅有一个棋子,初始位置在左上角 $$(1,1)$$。

两名同学轮流操作,同学天才先手。在每一次操作中,当前同学需要将棋子从当前位置向右或向下移动一步(若对应方向越界,则该方向不可选)。移动完成后,若新的格子字符为 '1',则本次移动的同学记 $$+1$$ 分;若为 '0',则记 $$-1$$ 分。初始格子 $$(1,1)$$ 不记分。

当棋子到达右下角 $$(n,m)$$ 后,游戏立即结束(此时无法继续移动)。请计算在双方都采取最优策略时,天才同学的总分减去笨蛋同学的总分的差值。

输入描述

第一行输入两个整数 $$n, m(1 \le n, m \le 2 \times 10^5, n \times m \le 4 \times 10^5)$$。

此后一共 $$n$$ 行,每行输入一个长度为 $$m$$ 的字符串,仅包含字符 '0''1',表示棋盘。

输出描述

输出一个整数,为最优博弈下天才同学分数减笨蛋同学分数的差值。

样例1

输入

2 2
01
11

输出

0

样例解释

起点在 $$(1,1)$$。两条可能的路径如下:

  1. 先手向右到 $$(1,2)$$ 记 $$+1$$ 分,后手向下到 $$(2,2)$$ 记 $$+1$$ 分,差值 $$+1 - (+1) = 0$$。
  2. 先手向下到 $$(2,1)$$ 记 $$+1$$ 分,后手向右到 $$(2,2)$$ 记 $$+1$$ 分,差值同样为 $$0$$。

双方最优下差值为 $$0$$。

样例2

输入

1 3
101

输出

-2

题解

题目内容拆解

两人在 $$n \times m$$ 棋盘上交替移动棋子(只能右或下),每步根据目标格是 '1' 还是 '0' 得 $$+1$$ 或 $$-1$$ 分。天才先手且希望最大化分差,笨蛋希望最小化分差。$$n \times m \le 4 \times 10^5$$,需要 $$O(n \times m)$$ 的 DP。

算法实现

采用动态规划(Minimax 博弈),从终点倒推每个位置的最优分差。

状态方程定义

设 $$f[i][j]$$ 表示棋子在位置 $$(i, j)$$ 时,从此刻到游戏结束,天才总分减去笨蛋总分的最优差值。

状态方程初始化

$$f[n-1][m-1] = 0$$,棋子已在终点,游戏结束无得分。

状态方程转移

从 $$(i, j)$$ 出发的第 $$(i + j + 1)$$ 步,由步数奇偶决定谁操作。若 $$i + j$$ 为偶数(即第 $$i + j + 1$$ 步为奇数步),天才操作,取 max;否则笨蛋操作,取 min。

向右移动到 $$(i, j+1)$$:格子值 $$v = +1$$('1')或 $$-1$$('0')。天才操作贡献 $$+v$$,笨蛋操作贡献 $$-v$$。

$$f[i][j] = \begin{cases} \max(\text{右}, \text{下}) & \text{天才操作} \\ \min(\text{右}, \text{下}) & \text{笨蛋操作} \end{cases}$$

其中"右"$$= (\pm v_{\text{right}}) + f[i][j+1]$$,"下"$$= (\pm v_{\text{down}}) + f[i+1][j]$$,正负号取决于当前操作者。

以样例2为例($$1 \times 3$$ 棋盘 101):$$f[0][2] = 0$$(终点)。$$f[0][1]$$:天才操作($$0+1$$ 为偶数),移到 $$(0,2)$$ 格值 '1' 得 $$+1 + 0 = 1$$,$$f[0][1] = 1$$。$$f[0][0]$$:笨蛋操作($$0+0$$ 为偶数→不对,$$0+0=0$$ 为偶数所以是天才操作),等等让我重新推导。实际上 $$f[0][0]$$ 对应第1步(天才),移到 $$(0,1)$$ 格值 '0' 得 $$-1 + f[0][1] = -1 + 1 = 0$$... 但答案是 $$-2$$。

让我重新确认:位置 $$(0,0)$$,step = $$0+0 = 0$$,第1步(step+1=1,奇数),天才操作。$$(0,0)→(0,1)$$:格值 '0',天才得 $$-1$$。然后在 $$(0,1)$$,step = $$0+1 = 1$$,第2步(step+1=2,偶数),笨蛋操作。$$(0,1)→(0,2)$$:格值 '1',笨蛋得 $$+1$$,差值贡献 $$-1$$。总差值 = $$-1 + (-1) = -2$$。✓

最终答案为 $$f[0][0]$$。

时空复杂度分析

  • 时间复杂度:$$O(n \times m)$$,每个格子计算一次,两个方向取 max/min。
  • 空间复杂度:$$O(n \times m)$$,存储 DP 数组。

C++

// 该博弈了 - 博弈DP
#include <bits/stdc++.h>
using namespace std;

int solve(int n, int m, vector<string>& grid) {
    // f[i][j] = 从(i,j)到终点的最优分差(天才-笨蛋)
    vector<vector<int>> f(n, vector<int>(m, 0));

    for (int i = n - 1; i >= 0; i--) {
        for (int j = m - 1; j >= 0; j--) {
            if (i == n - 1 && j == m - 1) {
                f[i][j] = 0;
                continue;
            }
            int step = i + j;  // 已走步数
            // 天才走奇数步(step+1为奇数即step为偶数)
            bool isGenius = (step % 2 == 0);
            int best = isGenius ? INT_MIN : INT_MAX;

            // 向右
            if (j + 1 < m) {
                int val = (grid[i][j + 1] == '1') ? 1 : -1;
                int score = isGenius ? val + f[i][j + 1] : -val + f[i][j + 1];
                best = isGenius ? max(best, score) : min(best, score);
            }
            // 向下
            if (i + 1 < n) {
                int val = (grid[i + 1][j] == '1') ? 1 : -1;
                int score = isGenius ? val + f[i + 1][j] : -val + f[i + 1][j];
                best = isGenius ? max(best, score) : min(best, score);
            }
            f[i][j] = best;
        }
    }
    return f[0][0];
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<string> grid(n);
    for (int i = 0; i < n; i++) {
        cin >> grid[i];
    }
    cout << solve(n, m, grid) << "\n";
    return 0;
}

第三题:铁路修建

在线评测链接:https://www.neituiya.com/oj/7/2391

题目描述

在遥远的某个大陆上,有一个国家由 $$n$$ 个城市组成,编号为 $$1, 2, \ldots, n$$。国王计划在接下来的 $$m$$ 天内修建铁路。

在第 $$i$$ 天,工匠会首先在城市 $$l_i$$ 到 $$r_i$$ 之间的所有相邻城市对之间修建双向铁路;换句话说,对于所有满足 $$l_i \le j < r_i$$ 的整数 $$j$$,在城市 $$j$$ 与城市 $$j+1$$ 之间修建一条双向铁路。

修建完成后,国王想知道,在当前所有已修建铁路的条件下,从城市 $$x_i$$ 出发能到达的最大城市编号。

初始时不存在任何铁路;后续各天的修建在已修基础上累积生效。

输入描述

第一行输入两个整数 $$n, m(2 \le n \le 10^9, 1 \le m \le 10^6)$$,分别表示城市数量和修建天数。

接下来 $$m$$ 行,每行输入三个整数 $$l_i, r_i, x_i(1 \le l_i \le r_i \le n, 1 \le x_i \le n)$$,表示第 $$i$$ 天的操作:在城市 $$l_i$$ 到 $$r_i$$ 之间的所有相邻城市对之间修建双向铁路,然后查询从城市 $$x_i$$ 出发能到达的最大城市编号。

输出描述

输出共 $$m$$ 行,第 $$i$$ 行输出一个整数,表示第 $$i$$ 天从 $$x_i$$ 出发可到达的最大城市编号。

样例1

输入

5 3
1 3 2
2 5 1
1 5 4

输出

3
5
5

样例解释

第一天,修建城市 $$1$$-$$2$$ 与 $$2$$-$$3$$ 的铁路,连通块变为 $$\{1, 2, 3\}$$,从城市 $$2$$ 出发可到达的最大城市编号为 $$3$$。

第二天,修建城市 $$2$$-$$5$$ 间的铁路($$2$$-$$3, 3$$-$$4, 4$$-$$5$$),连通块扩展为 $$\{1, 2, 3, 4, 5\}$$,从城市 $$1$$ 出发可到达的最大城市编号为 $$5$$。

第三天,修建城市 $$1$$-$$5$$ 间的所有铁路(已全连通),连通块仍为 $$\{1, 2, 3, 4, 5\}$$,从城市 $$4$$ 出发可到达的最大城市编号为 $$5$$。

题解

题目内容拆解

每天在连续城市间修建铁路后查询某城市能到达的最大编号。本质是在线区间合并 + 点查询所在区间的右端点。$$n$$ 达 $$10^9$$(不能开数组),$$m$$ 达 $$10^6$$(需高效数据结构)。

算法实现

算法主策略:本题采用有序区间集合维护所有已连通的区间段。

核心思路:每条铁路操作 $$[l, r]$$ 会把城市 $$l$$ 到 $$r$$ 全部连通。如果之前已有某些区间与 $$[l, r]$$ 重叠或相邻,就合并成更大的区间。最终连通性等价于:维护一组不重叠的连续区间 $$[a_1, b_1], [a_2, b_2], \ldots$$,城市 $$x$$ 能到达的最大编号就是 $$x$$ 所在区间的右端点。

具体步骤

  1. 用平衡树(C++ 的 set、Java 的 TreeMap)存储所有不重叠区间,按左端点排序。
  2. 插入 $$[l, r]$$ 时,找到所有与 $$[l, r]$$ 重叠或相邻的区间(即左端点 $$\le r + 1$$ 且右端点 $$\ge l - 1$$ 的区间),将它们全部删除,合并为一个新区间 $$[\min(l, \text{所有左端点}), \max(r, \text{所有右端点})]$$。

3) 查询 $$x$$ 时,二分找到左端点 $$\le x$$ 的最大区间,检查 $$x$$ 是否在该区间内。若在,答案为右端点;否则 $$x$$ 是孤立城市,答案为 $$x$$ 本身。

复杂度分析关键:每个区间最多被合并一次(合并后消失),所以所有操作总共最多合并 $$O(m)$$ 个区间。

时空复杂度分析

  • 时间复杂度:$$O(m \log m)$$,每次操作在平衡树上二分查找 $$O(\log m)$$,合并的总次数 $$O(m)$$。
  • 空间复杂度:$$O(m)$$,最多 $$m$$ 个区间同时存在。

Go

// 铁路修建 - 区间合并
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

type Interval struct {
        left, right int
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var n, m int
        fmt.Fscan(reader, &n, &m)

        // 有序不重叠区间列表
        intervals := []Interval{}

        for q := 0; q < m; q++ {
                var l, r, x int
                fmt.Fscan(reader, &l, &r, &x)

                newL, newR := l, r
                // 找到所有与[l,r]重叠或相邻的区间
                start := len(intervals)
                end := -1
                lo := sort.Search(len(intervals), func(i int) bool {
                        return intervals[i].left >= l
                }) - 1
                if lo < 0 {
                        lo = 0
                }
                for i := lo; i < len(intervals); i++ {
                        if intervals[i].left > newR+1 {
                                break
                        }
                        if intervals[i].right >= l-1 {
                                if intervals[i].left < newL {
                                        newL = intervals[i].left
                                }
                                if intervals[i].right > newR {
                                        newR = intervals[i].right
                                }
                                if i < start {
                                        start = i
                                }
                                end = i
                        }
                }

                if end >= start {
                        // 替换合并的区间
                        merged := Interval{newL, newR}
                        newIntervals := make([]Interval, 0, len(intervals)-(end-start))
                        newIntervals = append(newIntervals, intervals[:start]...)
                        newIntervals = append(newIntervals, merged)
                        newIntervals = append(newIntervals, intervals[end+1:]...)
                        intervals = newIntervals
                } else {
                        // 插入新区间
                        pos := sort.Search(len(intervals), func(i int) bool {
                                return intervals[i].left >= newL
                        })
                        intervals = append(intervals, Interval{})
                        copy(intervals[pos+1:], intervals[pos:])
                        intervals[pos] = Interval{newL, newR}
                }

                // 查询x所在区间
                idx := sort.Search(len(intervals), func(i int) bool {
                        return intervals[i].left > x
                }) - 1
                if idx >= 0 && intervals[idx].left <= x && x <= intervals[idx].right {
                        fmt.Fprintln(writer, intervals[idx].right)
                } else {
                        fmt.Fprintln(writer, x)
                }
        }
}

字节跳动

2026-4-19

第一题:走廊通行与按钮策略

在线评测链接:https://www.neituiya.com/oj/13/2549

题目描述

你站在一条长度为 $$n$$ 的走廊起点,沿途有编号 $$1$$ 到 $$n$$ 的门,每扇门要么开放(用 $$0$$ 表示),要么关闭(用 $$1$$ 表示)。你需要按顺序通过所有门到达出口。

你手中有一个特殊按钮,至多可以使用 $$K$$ 次。每次按下按钮,会消耗一次使用机会。该次按下的效果是:从你当前正要通过的门开始,在接下来的 $$x$$ 秒内(即包含当前门在内的连续 $$x$$ 扇门),所有门都将被视为开放状态。

给定门的状态序列 $$a_1, a_2, \dots, a_n(a_i \in \{0, 1\})$$ 和按钮最大发动次数 $$K$$,请你求出能够通过所有门的最小持续时间 $$x$$。

输入描述

第一行输入一个整数 $$T(1 \le T \le 10^3)$$,表示测试用例组数。以下共 $$T$$ 组数据,每组格式如下:

第一行输入两个整数 $$n(1 \le n \le 2 \times 10^5)$$ 和 $$K(1 \le K \le n)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(a_i \in \{0, 1\})$$,表示第 $$i$$ 扇门的状态。

保证所有测试数据中 $$\sum n \le 2 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数,表示最小的按钮持续时间 $$x$$。

样例1

输入

3
4 1
0 1 1 0
8 2
1 1 0 1 1 1 0 1
5 3
0 0 0 0 0

输出

2
4
0

样例解释

第一组:$$[0, 1, 1, 0]$$ 中只有一个关闭段,长度 $$2$$,$$K=1$$,需 $$x \ge 2$$ 才能覆盖,故最小 $$x=2$$。

第二组:在第一扇门前按下按钮,通过前 $$4$$ 扇门,再立即按下按钮,通过后 $$4$$ 扇门,最小 $$x=4$$。

第三组:所有门均开放,无需使用按钮,最小 $$x=0$$。

第二题:矩阵印章染色

在线评测链接:https://www.neituiya.com/oj/13/2550

题目描述

AK机拿到了一个矩阵,$$n$$ 行 $$m$$ 列共 $$n \times m$$ 个小方格。AK机有一个 $$2 \times 2$$ 的印章,每次可以将一个 $$2 \times 2$$ 的子矩阵染成红色,每个格子只能被染一次。AK机染了若干个红色的 $$2 \times 2$$ 块,并且最终保证了每个 $$2 \times 2$$ 红色块都是不相邻的(如果两个红色块共用了同一个边或者同一个角,则称为两个红色块相邻)。

例如:下面矩阵为非法的,因为两个 $$2 \times 2$$ 红色块相邻了(共用了同一个角):

**..
**..
..**
..**

AK机忘了自己的染色过程,她拿到了一个矩阵,她想知道这个矩阵是否是按她的要求染色的?

共有 $$t$$ 组询问。

输入描述

第一行输入一个正整数 $$t(1 \le t \le 50)$$,代表询问的次数。

对于每组询问,先输入两个正整数 $$n, m(1 \le n, m \le 50)$$,代表矩阵的行数和列数。

接下来的 $$n$$ 行,每行输入一个长度为 $$m$$ 的字符串。字符 * 代表格子被染成红色,. 代表未被染色。

输出描述

输出 $$t$$ 行,每行对应一组询问的答案。若该图形是AK机染色的,则输出 Yes。否则输出 No

样例1

输入

3
4 4
**..
**..
..**
..**
4 5
**...
**...
...**
...**
2 2
*.
..

输出

No
Yes
No

第三题:数组字典序优化与尾随零

在线评测链接:https://www.neituiya.com/oj/13/2551

题目描述

AK机拿到了一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \dots, a_n\}$$。她可以进行最多 $$k$$ 次如下操作:选择两个下标的元素 $$a_i$$ 和 $$a_j$$(即满足 $$i \neq j$$),且 $$a_i$$ 为偶数,将 $$a_i$$ 除以 $$2$$,同时将 $$a_j$$ 乘以 $$2$$。

经过最多 $$k$$ 次操作后,AK机希望数组的字典序尽可能小。由于操作后数组元素可能非常大,只需要输出操作结束后数组中每个元素末尾有多少个 $$0$$(十进制表示下的尾随零)即可。

字典序比较:从数组的第一个元素开始逐个比较,直到找到第一个不同的位置,比较这个位置元素的大小,较小的数组的字典序也较小。

输入描述

在一行上输入两个整数 $$n, k(1 \le n \le 10^5, 1 \le k \le 10^9)$$,分别表示数组长度和最多操作次数。

在第二行输入 $$n$$ 个整数 $$a_1, a_2, \dots, a_n(1 \le a_i \le 10^9)$$,表示数组的初始元素。

输出描述

在一行上输出 $$n$$ 个整数,第 $$i$$ 个整数表示操作结束后第 $$i$$ 个元素末尾 $$0$$ 的数量。

样例1

输入

5 1
1 2 3 4 5

输出

0 0 0 0 1

样例解释

在上述样例中,初始数组为 $$\{1, 2, 3, 4, 5\}$$。AK机进行一次操作,选择 $$a_i = 2$$ 和 $$a_j = 5$$,将 $$2$$ 除以 $$2$$ 得到 $$1$$,将 $$5$$ 乘以 $$2$$ 得到 $$10$$。最终数组为 $$\{1, 1, 3, 4, 10\}$$,各元素末尾 $$0$$ 的个数依次为 $$\{0, 0, 0, 0, 1\}$$。

样例2

输入

3 20
1024 5 125

输出

0 0 3

第四题:自定义池化操作

在线评测链接:https://www.neituiya.com/oj/13/2552

题目描述

AK机在学习深度学习时,受常用的卷积神经网络(CNN)的池化操作启发,创建了自己的池化并应用于图像处理中。

下面描述了应用AK机池化一次的过程,假设给定一个 $$8 \times 8$$ 的矩阵。

  1. 将矩阵分成大小为 $$2 \times 2$$ 的子矩阵。

  1. 在每个 $$2 \times 2$$ 的子矩阵中只留下第二大的数字。当子矩阵的四个元素为 $$a_4 \le a_3 \le a_2 \le a_1$$ 时,第二大数为元素 $$a_2$$。

  1. 重复上述过程,矩阵的大小不断缩小。

现在AK机想知道当 $$N \times N$$ 矩阵通过重复应用池化操作,最终变成 $$1 \times 1$$ 时,留下的数是多少?

输入描述

第一行给出 $$N(2 \le N \le 1024)$$,且 $$N$$ 总是 $$2$$ 的幂($$N = 2^K, 1 \le K \le 10$$)。

接下来的 $$N$$ 行,依次给出每行的 $$N$$ 个元素。矩阵的每个元素都是大于等于 $$-10000$$ 且小于等于 $$10000$$ 的整数。

输出描述

输出最后剩余的数字。

样例1

输入

4
-6 -8 7 -4
-5 -5 14 11
11 11 -1 -1
4 9 -2 -4

输出

11

样例解释

当矩阵变成 $$1 \times 1$$ 时,留下的数是 $$11$$。

样例2

输入

8
-1 2 14 7 4 -5 8 9
10 6 23 2 -1 -1 7 11
9 3 5 -2 4 4 6 6
7 15 0 8 21 20 6 6
19 8 12 -8 4 5 2 9
1 2 3 4 5 6 7 8
9 10 11 12 13 14 15 16
17 18 19 20 21 22 23 24

输出

17

样例解释

当矩阵变成 $$1 \times 1$$ 时,留下的数是 $$17$$。

2026.3.21

第一题:不是字符串问题

在线评测链接:https://www.neituiya.com/oj/13/2364

题目描述

对于长度为 $$m$$ 的字符串 $$t_1t_2...t_m$$ 和整数 $$i(1 \le i \le m)$$,我们将字符串 $$f_i(t)$$ 定义为以下内容的连接:

$$t$$ 的前 $$\lfloor \frac{i}{2} \rfloor$$ 个字符,按字典序从小到大排序。

$$t$$ 整串,按字典序从大到小排序。

$$t$$ 除去前 $$\lfloor \frac{i}{2} \rfloor$$ 个字符外的剩余字符,按字典序从小到大排序。

现在,对于长度为 $$n$$ 的字符串 $$s_1s_2...s_n$$,取出它的全部长度为 $$x$$ 的子串,令 $$i = x$$,对每个子串计算 $$f_i$$,问能构造出多少个不同的新字符串?

定义子串为,从原字符串中,连续的选择一段字符(可以全选、可以不选)组成的新字符串。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 100)$$ 代表数据组数,每组测试数据描述如下:

第一行上输入两个整数 $$n, x(1 \le n \le 100, 1 \le x \le n)$$ 代表字符串长度、和询问长度。

第二行输入一个长度为 $$n$$,且由大小写字母混合构成的字符串 $$s$$,代表初始串。

输出描述

在一行上输出一个整数,代表构造出的新字符串数量。

样例1

输入

2
5 4
bAbbb
7 3
nuhhhhh

输出

1
3

样例解释

对于第一组测试数据,长度为 $$4$$ 的不同子串有 $$bAbb$$ 和 $$Abbb$$:

对于 $$bAbb$$,构造得到 $$f_4(bAbb) = Ab + bbbA + bb$$:前 $$\lfloor \frac{4}{2} \rfloor = 2$$ 个字符 $$bA$$,按字典序从小到大排序为 $$Ab$$;整串 $$bAbb$$,按字典序从大到小排序为 $$bbbA$$;除去前 $$2$$ 个字符外的剩余字符 $$bb$$,按字典序从小到大排序为 $$bb$$。

对于 $$Abbb$$,构造得到 $$f_4(Abbb) = Ab + bbbA + bb$$。

题解

题目内容拆解

对字符串 $$s$$ 的所有长度为 $$x$$ 的子串,分别执行变换 $$f_x$$(前 $$\lfloor x/2 \rfloor$$ 个字符排序 + 整串降序排序 + 剩余字符排序),统计能产生多少种不同结果。$$n \le 100$$,可以暴力枚举。

算法实现

算法主策略:枚举所有长度为 $$x$$ 的子串,对每个子串执行三步变换后放入集合去重。

对于子串 $$t$$(长度 $$x$$),令 $$h = \lfloor x/2 \rfloor$$:将 $$t$$ 的前 $$h$$ 个字符按字典序升序排列得到 $$part_1$$,将 $$t$$ 整串按字典序降序排列得到 $$part_2$$,将 $$t$$ 的后 $$x - h$$ 个字符按字典序升序排列得到 $$part_3$$。最终结果为 $$part_1 + part_2 + part_3$$。注意本题区分大小写,ASCII 中大写字母排在小写字母前面。

以样例为例:$$s$$ = bAbbb,$$x = 4$$,子串 bAbbAbbb 变换后都得到 AbbbbAbb,因此只有 $$1$$ 种不同结果。

时空复杂度分析

  • 时间复杂度:$$O(T \times n \times x \log x)$$,每个子串排序 $$O(x \log x)$$,最多 $$n - x + 1$$ 个子串。
  • 空间复杂度:$$O(n \times x)$$,存储集合中的字符串。

Go

// 不是字符串问题 - 字符串模拟
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n, x int
                fmt.Fscan(reader, &n, &x)
                var s string
                fmt.Fscan(reader, &s)

                half := x / 2
                results := make(map[string]bool)

                for j := 0; j+x <= n; j++ {
                        t := s[j : j+x]
                        // part1: 前half个字符升序
                        p1 := []byte(t[:half])
                        sort.Slice(p1, func(i, k int) bool { return p1[i] < p1[k] })
                        // part2: 整串降序
                        p2 := []byte(t)
                        sort.Slice(p2, func(i, k int) bool { return p2[i] > p2[k] })
                        // part3: 后(x-half)个字符升序
                        p3 := []byte(t[half:])
                        sort.Slice(p3, func(i, k int) bool { return p3[i] < p3[k] })
                        results[string(p1)+string(p2)+string(p3)] = true
                }

                fmt.Fprintln(writer, len(results))
        }
}

第二题:字典序

在线评测链接:https://www.neituiya.com/oj/13/2365

题目描述

AK机在研究有关字典序的问题。同长度下的字典序比较顺序为从左往右,比如 $$ac < ad$$,$$bc > ad$$。

他想知道,如果每种字母组合都能构成一个单词,给定长度为 $$n$$ 的两个单词 $$A$$ 和 $$B$$,字典序小于 $$B$$ 但大于 $$A$$ 且长度等于 $$n$$ 的单词有多少个。

输入描述

第一行输入一个整数 $$T$$,表示数据组数。

随后 $$T$$ 行,每行开头一个整数 $$n$$,表示单词 $$A$$ 和 $$B$$ 的长度,随后两个仅有小写字母组成的单词 $$A, B$$。

如果 $$A$$ 的字典序大于 $$B$$,输出 $$0$$。

对于 $$30$$% 的数据有 $$n \le 3$$。对于 $$100$$% 的数据有 $$1 \le n \le 10, 1 \le T \le 5000$$。

输出描述

输出 $$T$$ 行,每行一个整数,表示答案。

样例1

输入

4
1 z a
1 a z
2 a z
3 bbb bbb

输出

0
24
1
0

样例解释

样例 $$2$$ 中单词 b 到单词 y 的字典序均小于 z 大于 a,样例 $$3$$ 中仅有 ba 满足条件。

题解

题目内容拆解

给定两个等长的小写字母单词 $$A$$ 和 $$B$$,统计字典序严格在 $$A$$ 和 $$B$$ 之间的同长度单词数量。$$n \le 10$$,意味着总共最多 $$26^{10} \approx 1.4 \times 10^{14}$$ 个不同单词,long long 可以覆盖。

算法实现

算法主策略:将长度为 $$n$$ 的小写字母单词看作 26 进制数,其中 a = 0,b = 1,...,z = 25。

将单词 $$A$$ 和 $$B$$ 分别转换为 26 进制数值 $$val_A$$ 和 $$val_B$$。若 $$val_A \ge val_B$$,说明 $$A$$ 不小于 $$B$$,答案为 $$0$$。否则,$$A$$ 和 $$B$$ 之间严格夹着的单词数量为 $$val_B - val_A - 1$$。

以样例为例:$$A$$ = a(值 $$0$$),$$B$$ = z(值 $$25$$),答案 = $$25 - 0 - 1 = 24$$。

时空复杂度分析

  • 时间复杂度:$$O(T \times n)$$,每组数据遍历两个长度为 $$n$$ 的字符串各一次。
  • 空间复杂度:$$O(1)$$,只需常数个变量。

Go

// 字典序 - 26进制数转换
package main

import (
        "bufio"
        "fmt"
        "os"
)

// 将字符串转为26进制数值
func toBase26(s string) int64 {
        var val int64
        for _, ch := range s {
                val = val*26 + int64(ch-'a')
        }
        return val
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                var A, B string
                fmt.Fscan(reader, &n, &A, &B)
                valA := toBase26(A)
                valB := toBase26(B)
                if valA >= valB {
                        fmt.Fprintln(writer, 0)
                } else {
                        fmt.Fprintln(writer, valB-valA-1)
                }
        }
}

第三题:矩阵填写者

在线评测链接:https://www.neituiya.com/oj/13/2366

题目描述

AK机拿到了一个 $$n$$ 行 $$n$$ 列的方阵,每一个格子中都有一个字母。

他需要选择一整行、一整列,随后将这 $$n + n - 1$$ 个格子中的字母全部覆盖为同一个字母。

AK机想知道,他应该怎样覆盖,才能使得方阵中出现尽可能多的相同字母(注意,本题区分大小写)。你要同时输出最多能出现的相同字母的数量、以及不同的覆盖方案。如果在两种方案中,选择的行列不同、或覆盖的字母不同,则认为这两种方案不同。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 500)$$ 代表方阵的行数和列数。

随后 $$n$$ 行,第 $$i$$ 行输入一个长度为 $$n$$,由大小写字母组成的字符串 $$s_i$$,代表方阵的第 $$i$$ 行。

输出描述

对于每一组测试数据,新起一行。输出两个整数,代表最多能出现的相同字母的数量、以及不同覆盖方案的数量。

样例1

输入

3
1
a
3
aaa
aDC
CCC
6
abcDef
AbcdeF
abcdef
AbcdeF
aBcdef
abcDEF

输出

1 52
8 4
16 40

样例解释

对于第二组测试数据,其中一种最优的覆盖方案为:选择第三行、第三列(行和列的编号从 $$1$$ 开始),全部覆盖为 a。另一种最优的覆盖方案为:选择第一行、第一列(行和列的编号从 $$1$$ 开始),全部覆盖为 C

题解

题目内容拆解

在 $$n \times n$$ 方阵中选择一行、一列,将这 $$2n - 1$$ 个格子覆盖为同一个字母,使得方阵中某个字母的出现次数最多,并统计达到最优的方案数。$$n \le 500$$,字符区分大小写(共 $$52$$ 种)。

算法实现

算法主策略:预处理每个字符在每行每列的出现次数,然后枚举所有 (字符, 行, 列) 组合,用一个公式直接算分。

选择第 $$r$$ 行、第 $$c$$ 列、覆盖字符 $$ch$$ 后,$$ch$$ 的总出现次数为:

$$total[ch] + (2n - 1) - rowCnt[ch][r] - colCnt[ch][c] + ind$$

其中 $$total[ch]$$ 是覆盖前 $$ch$$ 的总数,$$rowCnt$$ 和 $$colCnt$$ 是该行该列中 $$ch$$ 的个数(被覆盖后替换掉,需要减去),$$ind$$ 是交叉格补偿:如果 $$mat[r][c]$$ 本身就是 $$ch$$,它被行和列各减了一次,需要补回 $$1$$。

枚举 $$52 \times n \times n$$ 种组合,取全局最大值和方案数即可。

以样例验证:方阵第 $$2$$ 组,选第 $$3$$ 行第 $$3$$ 列覆盖 a,$$total[a] = 4$$,$$rowCnt[a][3] = 0$$,$$colCnt[a][3] = 1$$,$$ind = 0$$,得分 $$= 4 + 5 - 0 - 1 + 0 = 8$$。

时空复杂度分析

  • 时间复杂度:$$O(52 \times n^2)$$,每个字符枚举所有行列组合。$$n = 500$$ 时约 $$1300$$ 万次运算,完全够快。
  • 空间复杂度:$$O(52n)$$,存储每个字符在每行每列的出现次数。

Go

// 矩阵填写者 - 枚举字符+行列
package main

import (
        "bufio"
        "fmt"
        "os"
)

func charIdx(c byte) int {
        if c >= 'a' && c <= 'z' {
                return int(c - 'a')
        }
        return int(c-'A') + 26
}

func solve(reader *bufio.Reader, writer *bufio.Writer) {
        var n int
        fmt.Fscan(reader, &n)

        mat := make([]string, n)
        for i := 0; i < n; i++ {
                fmt.Fscan(reader, &mat[i])
        }

        // 预处理每个字符在每行、每列的出现次数
        var rowCnt, colCnt [52][]int
        var total [52]int
        for ch := 0; ch < 52; ch++ {
                rowCnt[ch] = make([]int, n)
                colCnt[ch] = make([]int, n)
        }
        for r := 0; r < n; r++ {
                for c := 0; c < n; c++ {
                        ch := charIdx(mat[r][c])
                        rowCnt[ch][r]++
                        colCnt[ch][c]++
                        total[ch]++
                }
        }

        best := int64(-1)
        ways := int64(0)
        base := 2*n - 1

        // 枚举覆盖字符、选择的行和列,直接算分
        for ch := 0; ch < 52; ch++ {
                for r := 0; r < n; r++ {
                        for c := 0; c < n; c++ {
                                ind := 0
                                if charIdx(mat[r][c]) == ch {
                                        ind = 1
                                }
                                score := int64(total[ch] + base - rowCnt[ch][r] - colCnt[ch][c] + ind)
                                if score > best {
                                        best = score
                                        ways = 1
                                } else if score == best {
                                        ways++
                                }
                        }
                }
        }

        fmt.Fprintf(writer, "%d %d\n", best, ways)
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                solve(reader, writer)
        }
}

第四题:AK机的红色直线

在线评测链接:https://www.neituiya.com/oj/13/2367

题目描述

AK机有 $$n$$ 条直线。她准备把其中的 $$k$$ 条直线染成红色。

任意两条染成红色的直线若相交,那么就称它们相交产生了一个"红点"。AK机希望你能帮助她在 $$n$$ 条直线中恰选 $$k$$ 条进行染色,使得最终"红点"的数量尽可能多。

"红点"的定义与产生红点的两条直线绑定,不与所在的坐标绑定。例如,若红色直线 $$A$$、$$B$$、$$C$$ 都相交于同一个点,那么直线 $$A, B$$、$$A, C$$ 和 $$B, C$$ 各自产生一个"红点",三条直线一共产生 $$3$$ 个"红点"。

输入描述

第一行输入两个正整数 $$n, k(1 \le n \le 10^5, 1 \le k \le n)$$ 代表直线的数量、AK机需要染红的直线数量。

此后 $$n$$ 行,第 $$i$$ 行输入三个整数 $$a_i, b_i, c_i(-10^9 \le a_i, b_i, c_i \le 10^9)$$ 代表第 $$i$$ 条直线的解析式为 $$a_i \times x + b_i \times y + c_i = 0$$。保证 $$a_i$$ 和 $$b_i$$ 均不为零,且没有两条直线是重合的。

输出描述

输出一个整数,代表AK机能得到的"红点"数量的最大值。

样例1

输入

3 2
1 1 1
1 1 2
1 2 1

输出

1

样例解释

选择前两条线染为红色不能得到"红点",而无论选择第一条与第三条的组合还是第二条与第三条的组合都只能得到一个"红点"。所以,AK机最多只能得到 $$1$$ 个"红点"。

题解

题目内容拆解

从 $$n$$ 条直线中选 $$k$$ 条,使得相交对数最多。$$n \le 10^5$$,两条直线相交当且仅当它们不平行。因此问题转化为:将直线按斜率分组后,选 $$k$$ 条使得同组(平行)对数最少。

算法实现

算法主策略:按斜率将直线分组,然后用贪心 + 二分使选中直线在各组间尽量均匀分布。

分组:对每条直线 $$a_i x + b_i y + c_i = 0$$,将 $$(a_i, b_i)$$ 除以 $$\gcd$$ 并统一符号方向后作为斜率标识。斜率相同的直线归为一组。

最优分配:设有 $$m$$ 个组,大小分别为 $$g_1, g_2, \ldots, g_m$$。从中选 $$k$$ 条,总相交对数 = $$\binom{k}{2} - \sum \binom{x_i}{2}$$,其中 $$x_i$$ 是从第 $$i$$ 组选的数量。要最大化相交对数,等价于最小化 $$\sum \binom{x_i}{2}$$,即让各组选取数量尽可能均匀。

二分水位线:二分 base level $$t$$,每组贡献 $$\min(g_i, t)$$ 条,找到最大的 $$t$$ 使得总选取量 $$\le k$$。剩余 $$r = k - \text{total}(t)$$ 条分配给容量 $$> t$$ 的组各 $$+1$$。

以样例为例:$$3$$ 条直线中前两条斜率相同($$a=1, b=1$$),第三条斜率不同。$$k=2$$ 时最优选法为从不同组各选 $$1$$ 条,$$\binom{2}{2} - 0 = 1$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,分组 $$O(n)$$,排序 $$O(m \log m)$$,二分 $$O(\log(\max g_i) \cdot \log m)$$。
  • 空间复杂度:$$O(n)$$,存储分组信息和前缀和。

类似题目

山峰子序列

Go

// AK机的红色直线 - 按斜率分组 + 贪心
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

func gcd(a, b int) int {
        for b != 0 {
                a, b = b, a%b
        }
        return a
}

func abs(x int) int {
        if x < 0 {
                return -x
        }
        return x
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        var n, k int
        fmt.Fscan(reader, &n, &k)

        // 按斜率分组
        type slope struct{ a, b int }
        cnt := make(map[slope]int)
        for i := 0; i < n; i++ {
                var a, b, c int
                fmt.Fscan(reader, &a, &b, &c)
                g := gcd(abs(a), abs(b))
                na, nb := a/g, b/g
                if na < 0 {
                        na, nb = -na, -nb
                }
                cnt[slope{na, nb}]++
        }

        // 收集并排序
        groups := make([]int, 0, len(cnt))
        for _, v := range cnt {
                groups = append(groups, v)
        }
        sort.Ints(groups)
        m := len(groups)

        // 前缀和
        prefix := make([]int64, m+1)
        for i := 0; i < m; i++ {
                prefix[i+1] = prefix[i] + int64(groups[i])
        }

        // 二分找最大t使得 sum min(gi, t) <= k
        lo, hi := 0, groups[m-1]
        for lo < hi {
                mid := lo + (hi-lo+1)/2
                pos := sort.SearchInts(groups, mid+1)
                total := prefix[pos] + int64(m-pos)*int64(mid)
                if total <= int64(k) {
                        lo = mid
                } else {
                        hi = mid - 1
                }
        }
        t := lo

        pos := sort.SearchInts(groups, t+1)
        totT := prefix[pos] + int64(m-pos)*int64(t)
        r := int64(k) - totT
        countGt := int64(m - pos)

        // 计算 sum C(xi, 2)
        var cost int64
        for i := 0; i < pos; i++ {
                cost += int64(groups[i]) * int64(groups[i]-1) / 2
        }
        cost += r * int64(t+1) * int64(t) / 2
        cost += (countGt - r) * int64(t) * int64(t-1) / 2

        ans := int64(k)*int64(k-1)/2 - cost
        fmt.Println(ans)
}

OPPO

2026-3-22

第一题:切割数组

在线测评链接:https://www.neituiya.com/oj/69/2380

题目描述

AK机有一个长度为$$n$$的整数数组$$[a_1,a_2,...,a_n]$$;

AK机希望将这些元素分成三份,要求每个元素恰好属于其中一份,且三份元素个数都相同;

记第$$i$$份的元素和为 $$w_i(i= 1, 2, 3)$$;

请你计算表达式$$|w_1-w_2|+|w_2-w_3|$$的最大可能值。

输入描述

第一行输入一个整数$$n(3\le n\le 2\times 10^5)$$,表示数组大小,题目保证$$n$$是$$3$$的倍数;

第二行输入$$n$$个整数$$a_1,a_2,...,a_n (1\le a_i\le 10^9)$$,表示数组元素。

输出描述

输出一个整数,表示最大值$$|w_1- w_2|+|w_2-w_3|$$

样例1

输入

3
1 2 3

输出

3

样例解释

在这个样例中,将每个元素单独成组,得到三个元素和分别为$$1,2,3$$,将它们编号为$$w_1= 3, w_2= 1, w_3=2$$,

可得$$|w_1-w_2|+|w_2-w_3|=2+1=3$$。

题解:贪心

题目内容拆解

本题的核心在于:将$$n$$个数分成三份,每份大小相等,分别记为$$w_1,w_2,w_3$$,

要求最大化表达式$$|w_1-w_2|+|w_2-w_3|$$。每个元素只能属于一份。

由于每份大小相等,最优策略是让一份尽量大,一份尽量小,另一份为中间值

算法实现

  1. 首先将数组$$a$$升序排序。
  2. 设$$m=n/3$$,则最小的$$m$$个数分为一组(记为$$w_2$$),最大的$$m$$个数分为一组(记为$$w_1$$),中间的$$m$$个数分为一组(记为$$w_3$$)。

3) 分别计算三组的元素和$$w_1,w_2,w_3$$。

4) 枚举$$w_1,w_2,w_3$$的所有排列,计算$$|w_1-w_2|+|w_2-w_3|$$的最大值(共$$6$$种排列)。

  1. 输出最大值。

时间复杂度分析

排序$$O(n\log n)$$,分组和枚举$$O(n)$$,总复杂度$$O(n\log n)$$,可以高效通过所有测试数据。

C++

#include <bits/stdc++.h>
using namespace std;
const int N = 2E5 + 10;
int n, a[N];

int main() {
  cin >> n;
  for (int i = 0; i < n; ++i) {
    cin >> a[i];
  }
  sort(a, a + n);
  int m = n / 3;
  long long w1 = 0, w2 = 0, w3 = 0;
  // w1: 最大的m个
  for (int i = n - m; i < n; ++i) {
    w1 += a[i];
  }
  // w2: 最小的m个
  for (int i = 0; i < m; ++i) {
    w2 += a[i];
  }
  // w3: 中间的m个
  for (int i = m; i < n - m; ++i) {
    w3 += a[i];
  }
  // 计算三种分组方式的最大值
  long long res = 0;
  // 1. w1最大,w2最小,w3中间
  res = max(res, abs(w1 - w2) + abs(w2 - w3));
  // 2. w1最大,w3最小,w2中间
  res = max(res, abs(w1 - w3) + abs(w3 - w2));
  // 3. w2最大,w1最小,w3中间
  res = max(res, abs(w2 - w1) + abs(w1 - w3));
  // 4. w2最大,w3最小,w1中间
  res = max(res, abs(w2 - w3) + abs(w3 - w1));
  // 5. w3最大,w1最小,w2中间
  res = max(res, abs(w3 - w1) + abs(w1 - w2));
  // 6. w3最大,w2最小,w1中间
  res = max(res, abs(w3 - w2) + abs(w2 - w1));
  cout << res << endl;
  return 0;
}

第二题:构造排列(七)

在线测评链接:https://www.neituiya.com/oj/39/2381

题目描述

给定一个长度为 $$n$$ 的整数数组 $$a$$ 。你可以对 $$a$$ 进行若干次下面的操作(可以不操作):

选择一个下标 $$1 \le i\le n$$,并将 $$a_i$$ 更新为$$\lfloor \frac {a_i} {2} \rfloor$$。在这里,$$\lfloor x \rfloor$$意味着对 $$x$$ 下取整。

小$$O$$想知道,是否存在一个操作序列,使得在所有操作后数组 $$a$$ 为一个排列?

【名词解释】

长度为 $$n$$ 的排列是由 $$1,2,...,n$$ 这 $$n$$ 个整数、按任意顺序组成的数组(每个整数均恰好出现一次)。

例如,$$[2,3,1,5,4]$$是一个长度为 $$5$$ 的排列,而 $$[1,2,2]$$和 $$[1,3,4]$$都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行包含一个整数 $$n(1\le n\le 2\times 10^5)$$ ,表示数组 $$a$$ 的长度。

第二行包含长度为 $$n$$ 个整数,$$0\le a_i\le 10^9$$

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2\times 10^5$$ 。

输出描述

输出 $$T$$ 行,其中第 $$i$$ 行为第 $$i$$ 组测试数据的答案。对于每一组测试数据,如果答案存在,在一行上输出 $$YES$$ ; 否则直接输出 $$NO$$ 。

您可以以任何大小写形式输出答案。例如,字符串 $$yEs、yes$$ 和 $$Yes$$ 都将被视为肯定的回答。

样例1

输入

5
3
1 2 4
3
1 2 6
1
1
2
1 536870911
5
25752 3010 1188 126 270

输出

NO
YES
YES
NO
YES

样例解释

对于第 $$2$$ 组测试数据,我们可以对 $$a_3$$ 进行一次操作,数组 $$a$$ 变为 $$[1,2,3]$$ ,是长度为 $$3$$ 的排列。

题解:贪心+构造

题目内容拆解

本题的核心在于:给定一个数组$$a$$,每个元素可以反复对半取整(即$$a_i\to \lfloor a_i/2\rfloor$$),问是否存在一种操作序列,使得最终$$a$$变成$$1\sim n$$的一个排列。每个$$a_i$$只能被分配一次,且每个目标$$1\sim n$$只能被分配一次。

本质是:每个$$a_i$$可以变成不超过$$a_i$$的任意$$2$$的幂次下取整,需将所有$$a_i$$分配到$$1\sim n$$,且每个目标只能被分配一次。

算法实现

  1. 将$$a$$按从大到小排序,优先处理大的数。
  2. 用布尔数组$$used[1..n]$$记录每个目标数是否已被分配。

3) 对每个$$a_i$$,不断对半取整,直到$$x\leq n$$且$$x$$未被分配,或$$x<1$$为止。

4) 若找到可用的$$x$$,则$$used[x]=true$$,否则跳过。

  1. 最终检查$$used[1..n]$$是否全为$$true$$,若是输出$$YES$$,否则输出$$NO$$。

时间复杂度分析

每个$$a_i$$最多对半取整$$O(\log a_i)$$次,总复杂度$$O(n\log a_{max})$$,数据范围内可以高效通过所有测试数据。

C++

#include <bits/stdc++.h>
using namespace std;

int main() {
  int T;
  cin >> T;
  while (T--) {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; ++i)
      cin >> a[i];
    sort(a.rbegin(), a.rend()); // 从大到小排序

    vector<bool> used(n + 1, false); // 标记每个数是否被用过
    for (int i = 0; i < n; ++i) {
      int x = a[i];
      while (x > n || (x >= 1 && used[x])) {
        x /= 2;
      }
      if (x >= 1 && !used[x]) {
        used[x] = true;
      }
    }

    bool ok = true;
    for (int i = 1; i <= n; ++i) {
      if (!used[i]) {
        ok = false;
        break;
      }
    }
    cout << (ok ? "YES" : "NO") << endl;
  }
  return 0;
}

第三题:可整除子数组

在线测评链接:https://www.neituiya.com/oj/39/2382

题目描述

给定一个长度为 $$n$$ 的正整数数组$$\{a_1,a_2,...,a_n\}$$和一个正整数 $$k$$,我们称子数组 $$[l,r]$$ 的乘积末尾包含至少 $$k$$ 个连续零,为可整除子数组。

请统计满足上述条件的子数组个数。

输入描述

第一行输入两个整数 $$n,k(1\le n\le 2\times 10^5,1\le k\le 10^9)$$ ,分别表示数组长度与所需的末尾零个数。

第二行输入 $$n$$ 个整数 $$a_1,a_2,...,a_n(1 \le a_i< 10^9)$$,表示数组元素。

输出描述

输出一个整数,表示乘积末尾至少包含 $$k$$ 个连续零的子数组总数。

样例1

输入

5 1
10 5 2 25 50

输出

12

样例解释

在此样例中,所有满足条件的子数组共有 $$12$$ 个。

其中 $$[1,1],[1,2],[1,3],[1,4],[1,5],[2,3],[2,4],[2,5],[3,4,[3,5],[4,5],[5,5]$$ 均满足条件。

样例2

输入

3 2
100 10 5

输出

3

样例解释

在此样例中,可选子数组为 $$[1,1],[1,2],[1,3]$$,它们的乘积末尾均包含至少 $$2$$ 个零。

题解:双指针+简单数论

题目内容拆解

给定长度为 $$n$$ 的正整数数组,统计所有子数组 $$[l, r]$$ 中,乘积末尾至少有 $$k$$ 个零的子数组个数。

末尾零的个数等于乘积中因子 $$10$$ 的个数,而 $$10 = 2 \times 5$$,故末尾零个数为 $$\min(\sum cnt_2, \sum cnt_5)$$。问题转化为:

统计满足 $$\min(\sum_{i=l}^{r} cnt_2[i], \sum_{i=l}^{r} cnt_5[i]) \ge k$$ 的子数组个数。

算法实现

预处理:

对每个元素 $$a[i]$$,分解出因子 2 和因子 5 的个数,分别存入 $$cnt_2[i]$$ 和 $$cnt_5[i]$$。

双指针策略:

固定右端点 $$r$$,维护窗口 $$[l, r]$$ 内因子 2 和因子 5 的累计和。当窗口满足 $$\min(sum_2, sum_5) \ge k$$ 时,说明以当前 $$l$$ 为左端点、$$r$$及其右侧任意位置为右端点的子数组均满足条件。

具体做法:

  • 枚举右端点 $$r$$,将 $$cnt_2[r]$$ 和 $$cnt_5[r]$$ 加入窗口
  • 当 $$\min(sum_2, sum_5) \ge k$$ 时,区间 $$[l, r], [l, r+1], \ldots, [l, n-1]$$ 共 $$n - r$$ 个子数组均满足条件,累加答案后收缩左边界 $$l$$
  • 重复直至窗口不再满足条件

正确性说明:

由于因子个数非负,窗口扩大时 $$sum_2$$ 和 $$sum_5$$ 单调不减,收缩时单调不增,满足双指针的单调性要求。

时空复杂度分析

  • 时间复杂度:$$O(n \log A)$$,其中 $$A$$ 为元素最大值。预处理每个元素分解因子需 $$O(\log A)$$,双指针遍历 $$O(n)$$
  • 空间复杂度:$$O(n)$$,存储每个元素的因子 2 和因子 5 的个数

类似题目

删除子数组

C++

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n, k;
  cin >> n >> k;

  vector<int> a2(n), a5(n); // 每个元素的因子2和因子5的个数

  for (int i = 0; i < n; i++) {
    int x;
    cin >> x;
    while (x % 2 == 0) {
      a2[i]++;
      x /= 2;
    }
    while (x % 5 == 0) {
      a5[i]++;
      x /= 5;
    }
  }

  // 双指针:对于每个右端点r,找最小的l使得[l,r]满足min(sum2,sum5)>=k
  int left = 0;
  long long cnt2 = 0, cnt5 = 0; // 当前窗口[left, right]的因子和
  long long ans = 0;

  for (int right = 0; right < n; right++) {
    cnt2 += a2[right];
    cnt5 += a5[right];
    // 收缩左边界,直到不满足条件为止
    while (left <= right && min(cnt2, cnt5) >= k) {
      cnt2 -= a2[left];
      cnt5 -= a5[left];
      left++;
      ans += n - right;   //区间[left, right],[left, right+1],...[left, n-1]都是满足条件的,共计n-right个区间
    }
  }

  cout << ans << "\n";

  return 0;
}

小米

2026-3-21

第一题:装备选配

在线评测链接:https://www.neituiya.com/oj/45/2360

题目描述

在某款角色扮演游戏中,玩家的角色拥有三项核心属性:力量、敏捷和智力。

你的仓库中目前存储了 $$N$$ 件装备,每一件装备都拥有这三项属性的数值。

第 $$i$$ 件装备的力量值为 $$x_i$$,敏捷值为 $$y_i$$,智力值为 $$z_i$$。需要注意的是,这些数值可能为负数。

为了应对即将到来的高难度副本,你需要从这 $$N$$ 件装备中恰好挑选出 $$M$$ 件进行装备。

装备穿戴后,角色的最终属性值为所选 $$M$$ 件装备对应属性值的代数和,即最终力量 $$X = \sum x_j$$,

最终敏捷 $$Y = \sum y_j$$,最终智力 $$Z = \sum z_j$$。

为了追求极致的战斗风格,角色强度评分为三项最终属性值的绝对值之和,

即 $$|X| + |Y| + |Z|$$。请你计算一下,如何从 $$N$$ 件装备中挑选 $$M$$ 件,才能使得角色的强度评分最大?

输入描述

输入包含 $$N + 1$$ 行。

第一行包含两个整数 $$N, M(1 \le M \le N \le 10^5, -10^9 \le x_i, y_i, z_i \le 10^9)$$,分别表示仓库中装备的总数和需要挑选的装备数量。

接下来的 $$N$$ 行,每行包含三个整数 $$x_i, y_i, z_i$$,分别表示第 $$i$$ 件装备的力量、敏捷和智力属性值。

输出描述

输出一行,一个整数,表示能够达到的最大强度评分。

样例1

输入

5 3
1 -2 3
-4 5 -6
7 -8 -9
-10 11 -12
13 -14 15

输出

54

样例解释

在这个例子中,我们可以选择第 $$1$$、第 $$3$$ 和第 $$5$$ 件装备。

各项属性的总和如下:力量 $$1 + 7 + 13 = 21$$,敏捷 $$(-2) + (-8) + (-14) = -24$$,

智力 $$3 + (-9) + 15 = 9$$。此时的强度评分为 $$|21| + |-24| + |9| = 21 + 24 + 9 = 54$$。

可以证明,没有其他方案能获得比 $$54$$ 更高的评分。

题解:贪心

题目问题拆解

从 $$N$$ 件装备中选 $$M$$ 件,使三项属性绝对值之和 $$|X| + |Y| + |Z|$$ 最大。$$N \le 10^5$$,需要高效算法。

算法实现

算法主策略:利用绝对值的数学性质,$$|X| + |Y| + |Z| = \max_{s_1, s_2, s_3 \in \{-1, +1\}} (s_1 X + s_2 Y + s_3 Z)$$。

这意味着最优解一定对应某种符号组合 $$(s_1, s_2, s_3)$$。

对于固定的符号组合,$$s_1 X + s_2 Y + s_3 Z = \sum_j (s_1 x_j + s_2 y_j + s_3 z_j)$$,

要最大化这个和,只需贪心选贡献最大的 $$M$$ 件装备即可。

枚举全部 $$2^3 = 8$$ 种符号组合,对每种组合计算每件装备的贡献值 $$v_i = s_1 x_i + s_2 y_i + s_3 z_i$$,排序后取前 $$M$$ 大的求和,所有组合取最大值即为答案。

以样例为例,取符号组合 $$(+1, -1, +1)$$ 时:装备 $$1$$ 贡献 $$1 + 2 + 3 = 6$$,

装备 $$3$$ 贡献 $$7+8-9=6$$,装备 $$5$$ 贡献 $$13+14+15=42$$,选这三件总计 $$54$$。

时空复杂度分析

时间复杂度:$$O(8 \times N \log N)$$,枚举 $$8$$ 种组合,每次排序 $$O(N \log N)$$。

空间复杂度:$$O(N)$$,存储每种组合下各装备的贡献值。

Go

// 装备选配 - 枚举符号组合 + 贪心
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

func main() {
        reader := bufio.NewReader(os.Stdin)
        var N, M int
        fmt.Fscan(reader, &N, &M)

        x := make([]int, N)
        y := make([]int, N)
        z := make([]int, N)
        for i := 0; i < N; i++ {
                fmt.Fscan(reader, &x[i], &y[i], &z[i])
        }

        ans := int64(0)
        signs := [2]int{-1, 1}

        // 枚举8种符号组合
        for _, s1 := range signs {
                for _, s2 := range signs {
                        for _, s3 := range signs {
                                vals := make([]int64, N)
                                for i := 0; i < N; i++ {
                                        vals[i] = int64(s1)*int64(x[i]) +
                                                int64(s2)*int64(y[i]) +
                                                int64(s3)*int64(z[i])
                                }
                                // 降序排列,取前M个
                                sort.Slice(vals, func(a, b int) bool { return vals[a] > vals[b] })
                                var total int64
                                for i := 0; i < M; i++ {
                                        total += vals[i]
                                }
                                if total > ans {
                                        ans = total
                                }
                        }
                }
        }

        fmt.Println(ans)
}

第二题:最小数差

在线评测链接:https://www.neituiya.com/oj/45/2361

题目描述

AK机有两个位数为 $$n$$ 的数 $$x$$ 和 $$y$$,这两个数在相同数位上的数字互不相同。

AK机可以对这两个数执行如下两种操作:

  1. 交换操作:交换 $$x$$ 和 $$y$$ 某一数位上的数字。
  2. 删除操作:删除 $$x$$ 和 $$y$$ 某一数位,然后分别将剩余的数位拼接。

3) 删除操作后允许数存在前导零(即数位的前几位可以是 $$0$$),删除操作不能删除最低位。

例如,对于数 $$x = 3561$$ 和 $$y = 7812$$,对最高位执行交换操作后,$$x = 7561, y = 3812$$;

再对次高位执行删除操作后,$$x = 761, y = 312$$。交换操作可以执行任意次。

请你计算删除操作执行不超过 $$k$$ 次的情况下,两个数的差的绝对值最小是多少。

输入描述

输入第一行有两个整数 $$n, k(2 \le n \le 10^3, 1 \le k < n)$$,分别表示两个数的位数以及删除操作最多执行次数。

第二行有两个数 $$x, y(10^{n-1} \le x, y < 10^n)$$,表示题目给定的两个数。

保证两个数在同一数位上的数字不同。

输出描述

输出一个整数,表示经过任意次交换操作、不超过 $$k$$ 次删除操作后,两个数的差的绝对值的最小值。

样例1

输入

6 2
329304 878189

输出

715

样例解释

分别对第一位、第二位执行删除操作,对剩余的数字执行交换操作,

最终两个数字变为 $$9104$$ 和 $$8389$$,差的绝对值为 $$715$$。

题解:贪心

** 题目问题拆解**

两个 $$n$$ 位数,每位可自由交换,最多删除 $$k$$ 位(不含最低位),

求最小差绝对值。$$n \le 10^3$$,数字可达 $$1000$$ 位,需要大数运算。

算法实现

算法主策略枚举首位 + 栈贪心最大化抵消量

核心观察:每一位的交换操作只改变该位分配给 $$x$$ 还是 $$y$$,等价于选择差值的正负号。

设第 $$i$$ 位的差值绝对值为 $$d_i=|x_i-y_i|$$(同位数字不同保证 $$d_i \ge 1$$)。

由于最高位的贡献远大于所有低位之和($$d_0 \times 10^{m-1}$$ 至少为 $$10^{m-1}$$,

而低位总和最多 $$10^{m-1}-1$$),最终差值的符号完全由最高位决定。

所以低位全部取相反符号来抵消,最终差值为 $$d_{first} \times 10^{m-1}-S$$,其中 $$S$$ 是剩余位构成的数。

枚举首位位置 $$p$$($$0 \le p \le \min(k, n-2)$$):删除前 $$p$$ 位,以第 $$p$$ 位作为最高位。

剩余 $$k - p$$ 次删除分配给尾部,用于删掉差值小的位以最大化抵消量 $$S$$。

栈贪心最大化 $$S$$:从尾部序列(不含最后一位,最后一位必须保留)中,

删除 $$k - p$$ 个数字使剩余数字组成的数最大。

使用单调递减栈:遇到比栈顶大的数字时弹出栈顶(相当于删除),直到删除次数用完。

以样例为例:$$x = 329304, y = 878189$$,各位差值 $$d = [5, 5, 1, 2, 8, 5]$$。

取 $$p=2$$(删前两位,$$d_{first}=1$$),尾部 $$[2, 8, 5]$$,无需再删。$$S=285$$,差值 $$=1000-285=715$$。

时空复杂度分析

时间复杂度:$$O(n \times k)$$,枚举 $$O(k)$$ 个首位,每次栈贪心 $$O(n)$$。

空间复杂度:$$O(n)$$,存储差值序列和栈。

Go

// 最小数差 - 贪心 + 栈求最大数
package main

import (
        "bufio"
        "fmt"
        "math/big"
        "os"
)

func abs(x int) int {
        if x < 0 {
                return -x
        }
        return x
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        var n, k int
        fmt.Fscan(reader, &n, &k)
        var xs, ys string
        fmt.Fscan(reader, &xs, &ys)

        diff := make([]int, n)
        for i := 0; i < n; i++ {
                diff[i] = abs(int(xs[i]-'0') - int(ys[i]-'0'))
        }

        var best *big.Int

        minP := k
        if n-2 < minP {
                minP = n - 2
        }

        for p := 0; p <= minP; p++ {
                dp := diff[p]
                tail := diff[p+1:]
                L := len(tail)
                tailDel := k - p
                if L-1 < tailDel {
                        tailDel = L - 1
                }
                keepPrefix := L - 1 - tailDel
                if keepPrefix < 0 {
                        keepPrefix = 0
                }

                // 栈贪心最大化前缀数字
                prefix := tail[:L-1]
                removals := len(prefix) - keepPrefix
                stack := make([]int, 0)
                for _, d := range prefix {
                        for len(stack) > 0 && removals > 0 && stack[len(stack)-1] < d {
                                stack = stack[:len(stack)-1]
                                removals--
                        }
                        stack = append(stack, d)
                }
                for removals > 0 && len(stack) > 0 {
                        stack = stack[:len(stack)-1]
                        removals--
                }
                kept := append(stack, tail[L-1])

                // 计算 total = dp * 10^len(kept) - S
                mTail := len(kept)
                a := big.NewInt(int64(dp))
                ten := big.NewInt(10)
                pow := new(big.Int).Exp(ten, big.NewInt(int64(mTail)), nil)
                a.Mul(a, pow)

                // 构造 S
                s := big.NewInt(0)
                for _, d := range kept {
                        s.Mul(s, ten)
                        s.Add(s, big.NewInt(int64(d)))
                }

                total := new(big.Int).Sub(a, s)
                total.Abs(total)

                if best == nil || total.Cmp(best) < 0 {
                        best = new(big.Int).Set(total)
                }
        }

        fmt.Println(best.String())
}

拼多多

2026-4-26

第一题:多多的Token

在线评测链接:https://www.neituiya.com/oj/13/2623

题目描述

多多有 $$n$$ 个任务需要完成。每个任务有两种执行模式,多多对于每个任务最多只能选择一种模式执行,也可以选择不执行:

  1. 常规模式:花费 $$a_i$$ 个 token 和 $$b_i$$ 的时间。
  2. 降耗模式:通过增加时间成本来降低 token 消耗,总共花费 $$c_i$$ 个 token 和 $$d_i$$ 的时间。

多多目前拥有的总 token 预算为 $$m$$,总时间预算为 $$t$$。请问在不超过 token 和时间预算的前提下,多多最多可以完成多少个任务?

输入描述

第一行包含三个整数 $$n, m, t(1 \le n \le 50, 1 \le m, t \le 200)$$,分别表示任务总数、总 token 预算和总时间预算。

接下来 $$n$$ 行,每行包含四个整数 $$a_i, b_i, c_i, d_i(1 \le c_i \le a_i \le 100, 1 \le b_i \le d_i \le 100)$$,分别表示第 $$i$$ 个任务常规模式下的 token 消耗、时间消耗,以及降耗模式下的 token 消耗、时间消耗。

输出描述

输出一个整数,表示在预算范围内最多能够完成的任务数量。

样例1

输入

3 10 10
5 5 2 8
4 3 3 5
8 2 4 6

输出

2

样例解释

不执行第 $$1$$ 个任务。以常规模式执行第 $$2$$ 个任务,花费 $$4$$ 个 token、$$3$$ 的时间。以降耗模式执行第 $$3$$ 个任务,花费 $$4$$ 个 token、$$6$$ 的时间。总共花费 $$8$$ 个 token、$$9$$ 的时间,均不超过预算,完成了 $$2$$ 个任务。

题解

题目内容拆解

$$n$$ 个任务,每个可以跳过、常规模式或降耗模式三选一。token 预算 $$m$$,时间预算 $$t$$,最大化完成任务数。$$n \le 50$$,$$m, t \le 200$$。

每个任务独立选择,但同时受 token 和时间两种资源约束。暴力枚举 $$3^n$$ 种组合不可行。

两种资源约束中,token 用量离散且范围小($$\le 200$$),适合作为 DP 下标。时间作为最小化目标:对于固定的 token 用量和任务数,时间越小越好,最后检查最小时间是否 $$\le t$$ 即可。

算法实现

状态方程定义:$$f[j][k]$$ 表示恰好使用 $$j$$ 个 token、完成恰好 $$k$$ 个任务所需的最小时间。

$$f[j][k]$$ 就是"在 $$j$$ 的 token 预算下凑齐 $$k$$ 个任务的最少时间开销"。

状态方程初始化:$$f[0][0] = 0$$,其余为 $$+\infty$$。未使用任何资源、未完成任何任务时时间开销为 $$0$$。

状态方程转移:对每个任务 $$(a_i, b_i, c_i, d_i)$$,逆序枚举 $$j$$ 和 $$k$$。逆序遍历保证每个任务只被选择一次(01 背包性质)。

$$f[j][k] = \min(f[j][k],\ f[j - a_i][k-1] + b_i,\ f[j - c_i][k-1] + d_i)$$

前一项对应常规模式,后一项对应降耗模式。

最终从 $$k = n$$ 到 $$0$$ 逆序扫描,找到第一个存在 $$j \le m$$ 使得 $$f[j][k] \le t$$ 的 $$k$$。这就是在预算内能完成的最多任务数。

时空复杂度分析

  • 时间复杂度:$$O(n^2 \cdot m)$$,外层遍历 $$n$$ 个任务,内层枚举 $$m+1$$ 种 token 用量和至多 $$n$$ 种任务数。
  • 空间复杂度:$$O(n \cdot m)$$​,DP 数组大小。

类似题目

提瓦特商店

第二题:多多的推荐位

在线评测链接:https://www.neituiya.com/oj/13/2624

题目描述

多多正在为首页内容安排推荐位。一共有 $$m$$ 个推荐位,第 $$j$$ 个推荐位的热度值为 $$s_j$$。同时有 $$n$$ 条内容需要参与分发。对于第 $$i$$ 条内容,多多已经评估出一个可接受的推荐位热度范围 $$[l_i, r_i]$$:如果推荐位热度值小于 $$l_i$$,则曝光不足;如果推荐位热度值大于 $$r_i$$,则和这条内容不够匹配。因此,第 $$i$$ 条内容只能放到热度值属于 $$[l_i, r_i]$$ 的推荐位上。

每个推荐位最多放置一条内容,每条内容也最多放置到一个推荐位。请你帮助多多计算:最多可以成功匹配多少条内容。

输入描述

第一行输入一个整数 $$T(1 \le T \le 3)$$,表示数据组数。

接下来对于每组数据:第一行输入两个整数 $$n, m(1 \le n, m \le 2 \times 10^5)$$。

接下来 $$n$$ 行,每行输入 $$l_i, r_i(0 \le l_i \le r_i \le 10^9)$$。

最后一行输入 $$m$$ 个整数 $$s_1, s_2, \ldots, s_m(0 \le s_j \le 10^9)$$。

输出描述

对于每组数据,仅输出一行整数,表示最多能够匹配的内容数量。

样例1

输入

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

输出

3

样例解释

一种最优匹配方式为:热度值为 $$2$$ 的推荐位分配给区间 $$[2, 2]$$,热度值为 $$3$$ 的推荐位分配给区间 $$[2, 3]$$,热度值为 $$5$$ 的推荐位分配给区间 $$[4, 5]$$。因此最多可以匹配 $$3$$ 条内容。

题解

题目内容拆解

$$n$$ 条内容各有可接受热度范围 $$[l_i, r_i]$$,$$m$$ 个推荐位各有热度值 $$s_j$$。一对一匹配,最大化匹配数。$$n, m \le 2 \times 10^5$$,值域达 $$10^9$$。

$$r_i$$ 越小的内容可选余地越小,应优先处理。对每条内容,选尽可能小的可用推荐位,可以为后续内容保留更大的推荐位。

算法实现

按右端点排序:将所有内容按 $$r_i$$ 升序排列,推荐位按热度值升序排列。

扫描 + 二分查找匹配:维护一个有序集合存放当前可用推荐位。随着扫描右端点 $$r_i$$ 递增,依次将热度值 $$\le r_i$$ 的推荐位加入集合。

对当前内容 $$[l_i, r_i]$$,在有序集合中二分查找最小的 $$\ge l_i$$ 的推荐位。由于集合始终有序,二分查找只需 $$O(\log m)$$。找到则匹配并从集合中移除。

正确性(交换论证):假设某个最优解中,内容 $$A$$($$r_A$$ 较小)匹配了较大推荐位 $$s'$$,内容 $$B$$($$r_B \ge r_A$$)匹配了较小推荐位 $$s \le s'$$。

由于 $$s' \le r_A \le r_B$$ 且 $$s \ge l_A$$,交换后 $$A$$ 匹配 $$s$$、$$B$$ 匹配 $$s'$$ 仍合法,匹配数不变。贪心选择最小可用推荐位不会错失最优解。

时空复杂度分析

  • 时间复杂度:$$O((n + m) \log m)$$,排序 $$O(n \log n + m \log m)$$,每个推荐位至多入集合一次、出集合一次,每次操作 $$O(\log m)$$。
  • 空间复杂度:$$O(m)$$,有序集合最多存放 $$m$$ 个推荐位。

第三题:多多玩拼图

在线评测链接:https://www.neituiya.com/oj/13/2625

题目描述

多多手里有一套散落的拼图,这套拼图可以完整地拼出 $$n \times m$$ 的矩形图片。拼图的每个碎片都有一个唯一的编号(从 $$1$$ 到 $$n \times m$$)。多多经过研究得到了一些碎片之间的相对位置关系,请帮他还原出最终的图片。

数据会给出一系列的相对关系,每条位置关系描述为 $$a, b, d$$,表示编号 $$a$$、$$b$$ 的碎片相邻,并且 $$a$$ 碎片位于 $$b$$ 碎片的 $$d$$ 方向。$$d$$ 的取值范围为 $$\{U, B, L, R\}$$,其中 $$U$$ 代表 $$a$$ 在 $$b$$ 的上方,$$B$$ 代表 $$a$$ 在 $$b$$ 的下方,$$L$$ 代表 $$a$$ 在 $$b$$ 的左侧,$$R$$ 代表 $$a$$ 在 $$b$$ 的右侧。

特别说明:

  • 数据可能不会给出所有的相邻关系,但数据保证根据这些相邻关系,一定能唯一地还原出最终的图片。数据不会出现相互矛盾的地方。
  • 不会出现拼成若干块,但没有相连,最后要靠边缘形状推理还原图片的情况,即数据保证连通性。
  • 图片方向是固定的,不需要考虑旋转的情况。

输入描述

第一行包含两个正整数 $$n, m(2 \le n, m \le 1000)$$,分别表示拼图的行数和列数。

第二行包含一个正整数 $$k(1 \le k \le 2 \times (n \times (m-1) + m \times (n-1)))$$,代表给出的相邻关系个数。

接下来 $$k$$ 行,每行包含两个正整数 $$a, b(1 \le a, b \le n \times m)$$ 以及一个字符 $$d$$,分别代表碎片编号和相对方位,$$a$$、$$b$$、$$d$$ 用一个空格隔开。

输出描述

输出 $$n$$ 行,每行输出 $$m$$ 个整数(用一个空格隔开),代表复原后的编号序列。输出顺序从上到下,从左到右。

样例1

输入

2 2
4
4 3 U
1 4 R
4 1 L
3 2 L

输出

4 1
3 2

样例解释

碎片 $$4$$ 在碎片 $$3$$ 上方,碎片 $$1$$ 在碎片 $$4$$ 右方,碎片 $$4$$ 在碎片 $$1$$ 左方(与上条一致),碎片 $$3$$ 在碎片 $$2$$ 左方。还原后第一行为 $$4, 1$$,第二行为 $$3, 2$$。

题解

题目内容拆解

$$n \times m$$ 拼图的 $$nm$$ 个碎片散落,给出 $$k$$ 条方向邻接关系,还原完整矩阵。$$n, m \le 1000$$,$$k$$ 可达 $$O(nm)$$,保证连通且无矛盾。

每条关系 「$$a\ b\ d$$」 确定了 $$a$$ 和 $$b$$ 的相对坐标偏移。连通性保证从任意碎片出发 BFS 可达所有碎片,进而确定每个碎片的绝对坐标。

算法实现

坐标系约定:用 $$(\text{row}, \text{col})$$ 表示坐标。行号从上往下递增,列号从左往右递增。

方向映射:每条关系「$$a\ b\ d$$」告诉我们 $$a$$ 在 $$b$$ 的 $$d$$ 方向,由此可以推出从 $$a$$ 出发到达 $$b$$ 的坐标偏移:

$$U$$($$a$$ 在 $$b$$ 上方):$$b$$ 的行号比 $$a$$ 大 $$1$$ → 偏移 $$(+1, 0)$$。$$B$$($$a$$ 在 $$b$$ 下方):$$b$$ 的行号比 $$a$$ 小 $$1$$ → 偏移 $$(-1, 0)$$。$$L$$($$a$$ 在 $$b$$ 左方):$$b$$ 的列号比 $$a$$ 大 $$1$$ → 偏移 $$(0, +1)$$。$$R$$($$a$$ 在 $$b$$ 右方):$$b$$ 的列号比 $$a$$ 小 $$1$$ → 偏移 $$(0, -1)$$。

每条边同时建反向边,偏移取反。例如「$$a\ b\ U$$」同时建「从 $$b$$ 到 $$a$$,偏移 $$(-1, 0)$$」。

BFS 遍历:任取一个碎片作为起点,坐标设为 $$(0, 0)$$。每次从队列取出碎片 $$u$$,对其所有邻接碎片 $$v$$,若未访问则用偏移量计算 $$v$$ 的坐标并入队。

以样例为例,从碎片 $$4$$ 出发设 $$(0, 0)$$:「$$4\ 3\ U$$」→ $$4$$ 在 $$3$$ 上方,$$3$$ 在 $$4$$ 下面一行 → $$3$$ 坐标 $$(0+1, 0) = (1, 0)$$。「$$1\ 4\ R$$」→ $$1$$ 在 $$4$$ 右方,从 $$4$$ 看 $$1$$ 在右边一列 → $$1$$ 坐标 $$(0, 0+1) = (0, 1)$$。「$$3\ 2\ L$$」→ $$3$$ 在 $$2$$ 左方,从 $$3$$ 看 $$2$$ 在右边一列 → $$2$$ 坐标 $$(1, 0+1) = (1, 1)$$。

归一化输出:BFS 结束后所有碎片坐标已确定。将最小行和最小列平移至 $$0$$,填入 $$n \times m$$ 矩阵按行输出。

时空复杂度分析

  • 时间复杂度:$$O(nm + k)$$,建图 $$O(k)$$,BFS 访问每个碎片一次 $$O(nm)$$,输出 $$O(nm)$$。
  • 空间复杂度:$$O(nm + k)$$,邻接表存储 $$O(k)$$ 条边,坐标数组和结果矩阵各 $$O(nm)$$。

第四题:多多的审批链

在线评测链接:https://www.neituiya.com/oj/13/2626

题目描述

多多的部门中发起审批单有一套审批流程,审批关系可以抽象为一棵以 $$1$$ 号节点为根的树,共有 $$n$$ 个节点。对于每个 $$i(2 \le i \le n)$$,给定它的直属上级 $$p_i$$,即审批树中存在一条从 $$p_i$$ 到 $$i$$ 的边。

对于任意节点 $$u$$,如果它发起一张审批单,审批单只能向上传递给它的直属上级,再由直属上级继续逐级上报到更高层。

现在多多最多可以选择 $$k$$ 个节点作为关键审批节点。如果审批单从 $$u$$ 出发,向上经过不超过 $$D$$ 条边,就能够到达某个关键审批节点,则称节点 $$u$$ 被覆盖。

请帮多多找到,在最多选择 $$k$$ 个关键审批节点的前提下,求出满足条件的最小 $$D$$ 使得整棵审批树上的所有节点都被覆盖。

关键审批节点一定能覆盖自己(距离为 $$0$$)。只有 $$u$$ 自己或 $$u$$ 的祖先可以覆盖 $$u$$。

距离指树上路径经过的边数。当 $$D = 0$$ 时,只有被选中的节点能覆盖自己。

输入描述

第一行输入一个整数 $$T(1 \le T \le 3)$$,表示数据组数。

接下来对于每组数据:第一行包含两个整数 $$n, k(1 \le n \le 2 \times 10^5, 1 \le k \le n)$$。

第二行包含 $$n-1$$ 个整数 $$p_2, p_3, \ldots, p_n(1 \le p_i < i)$$,其中 $$p_i$$ 表示节点 $$i$$ 的父节点。

输出描述

对于每组数据,仅输出一行整数,表示满足条件的最小 $$D$$。

样例1

输入

1
7 2
1 1 2 2 3 3

输出

2

样例解释

可以把节点 $$1$$ 和 $$2$$ 设置为关键审批节点。节点 $$1$$、$$2$$ 自身被覆盖。节点 $$3$$ 向上走 $$1$$ 条边可以到达节点 $$1$$。节点 $$4$$、$$5$$ 向上走 $$1$$ 条边可以到达节点 $$2$$。节点 $$6$$、$$7$$ 向上走 $$2$$ 条边可以到达节点 $$1$$。所有节点均被覆盖,因此 $$D = 2$$。

题解

题目内容拆解

以 $$1$$ 为根的 $$n$$ 节点树,选至多 $$k$$ 个关键节点,使每个节点向上走不超过 $$D$$ 条边可达某关键节点。求最小 $$D$$。$$n \le 2 \times 10^5$$,$$T \le 3$$。

$$D$$ 越大,覆盖越容易。$$D$$ 具有单调性,存在一个最小临界值。暴力枚举节点子集 $$O(2^n)$$ 不可行,但对固定的 $$D$$ 可以贪心算出最少需要几个关键节点。

算法实现

二分答案:二分对象是覆盖距离 $$D$$,区间为 $$[0, n-1]$$。上界取 $$n-1$$ 是因为树的最长链不超过 $$n-1$$ 条边,$$D = n-1$$ 时只需根节点一个关键节点就能覆盖全树。

$$D$$ 具有单调性:$$D$$ 越大,每个关键节点覆盖的范围越广,所需关键节点越少。对固定的 $$D$$,可以用贪心算出最少需要多少个关键节点。

check 函数:给定 $$D$$,判断最少需要多少个关键节点才能覆盖全树。

对每个节点 $$u$$ 维护一个值 $$\text{maxDist}[u]$$,含义是:$$u$$ 的子树中,距离 $$u$$ 最远的那个还没被覆盖的后代有多远。叶节点初始值为 $$0$$,因为叶节点自身就是一个未覆盖的节点,距离自己为 $$0$$。

后序遍历时,$$u$$ 从所有子节点 $$c$$ 收集信息:若 $$c$$ 子树有未覆盖节点($$\text{maxDist}[c] \ge 0$$),则该未覆盖节点到 $$u$$ 的距离为 $$\text{maxDist}[c] + 1$$。$$u$$ 取所有子节点中的最大值。

判断逻辑:若 $$\text{maxDist}[u] \ge D$$,说明再往上传一层距离就变成 $$D+1$$,超出覆盖范围。此时必须在 $$u$$ 放置关键节点,放置后将 $$\text{maxDist}[u]$$ 置为 $$-1$$ 表示子树已全部覆盖。

根节点特殊处理:根节点没有父节点,如果后序遍历结束后 $$\text{maxDist}[1] \ge 0$$,说明根节点的子树中仍有未覆盖节点,必须在根节点额外放一个关键节点。

二分过程:对候选值 $$mid$$,调用 check 计算最少关键节点数。若 check($$mid$$) $$\le k$$,说明 $$D = mid$$ 就够用,尝试更小的 $$D$$($$hi = mid$$)。否则 $$D$$ 太小,覆盖不了全树($$lo = mid + 1$$)。

输出:二分结束时 $$lo = hi$$,即为最小 $$D$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,二分 $$O(\log n)$$ 轮,每轮 check 函数后序遍历 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储树结构和遍历用的临时数组。

2026-4-12

第一题:赛车手赛道计时

在线评测链接:https://www.neituiya.com/oj/43/2513

题目描述

AK机是一名赛车手,今天他来到一个特殊的赛车场。这个赛车场有 $$n$$ 条平行的赛道,每条赛道的长度都是 $$m$$ 米。赛道由两种路面组成:水泥地和泥地。

水泥地共有 $$m$$ 段,其中第 $$i$$ 段水泥地位于距离起点 $$i-1$$ 米的位置,覆盖了从第 $$l_i$$ 条到第 $$r_i$$ 条赛道(包括两端),长度为 $$1$$ 米。

AK机驾驶赛车时,在水泥地上的速度为 $$1$$ 米/秒,在泥地上的速度为 $$0.5$$ 米/秒。

AK机可以选取任何一条赛道出发,出发之后不允许变道。他现在想知道最快多少时间到达终点,如果有多条赛道满足最快时间,他希望选取赛道编号最小的。

输入描述

第一行包含两个整数 $$n, m(1 \le n, m \le 10^5)$$,表示赛道数和赛道长度。

接下来 $$m$$ 行,每行包含两个整数 $$l_i, r_i(1 \le l_i \le r_i \le n)$$,表示第 $$i$$ 段水泥地覆盖的赛道范围。

输出描述

输出一行,包含两个数字,表示到达终点的最快时间和选取的赛道编号。

样例1

输入

3 2
1 2
2 3

输出

2 2

样例解释

AK机选择第 $$2$$ 条赛道时,第 $$1$$ 米是水泥地($$1$$ 秒),第 $$2$$ 米是水泥地($$1$$ 秒),总时间最短为 $$2$$ 秒。

题解:差分数组

本题涉及到差分,不熟悉该算法的同学可以先做一下模板题:

语文成绩

题目内容拆解

统计每条赛道有多少米是水泥地,选水泥最多的赛道。$$n, m$$ 可达 $$10^5$$,共 $$m$$ 次区间覆盖操作 → 因此采用差分数组

核心观察:水泥地 $$1$$ 秒/米,泥地 $$2$$ 秒/米,所以总耗时 $$= cnt \times 1 + (m - cnt) \times 2 = 2m - cnt$$,水泥段数 $$cnt$$ 越大耗时越短。问题转化为:统计每条赛道被覆盖的次数,取最大值。

算法实现

算法主策略:对长度为 $$n$$ 的差分数组,每次将区间 $$[l_i, r_i]$$ 加 $$1$$,最后做前缀和还原每条赛道的水泥段数。

对每段水泥地 $$[l_i, r_i]$$,在差分数组上执行 $$d[l_i] \mathrel{+}= 1$$,$$d[r_i+1] \mathrel{-}= 1$$。前缀和后得到每条赛道的水泥段数 $$cnt[j]$$,遍历找最大 $$cnt[j]$$ 对应的最小编号 $$j$$。

总时间 $$= 2m - cnt[j]$$,一定是整数,直接输出即可。

时空复杂度分析

  • 时间复杂度:$$O(n + m)$$,差分数组构建 $$O(m)$$,前缀和还原 $$O(n)$$,遍历取最值 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,差分数组和前缀和数组。

第二题:最大化最小距离

在线评测链接:https://www.neituiya.com/oj/43/2514

题目描述

充满梦想与希望的虚拟空间"月读"即将举办一场名为"辉夜盛典"的演出,舞台导演AK机正在为终幕的虚拟偶像大集合节目安排站位,共有 $$N$$ 位虚拟偶像参与该节目,舞台上共设有 $$K$$ 个可用的投影站位,其坐标分别为 $$x_i$$。

如果两名虚拟偶像站位太近,全息投影的动作捕捉范围以及演出特效就会发生重叠,导致演出出现"穿模事故"。AK机将一套站位方案的稳定程度定义为:任意两名虚拟偶像之间距离的最小值。请你帮AK机计算一下,所有站位方案的稳定程度最大可以是多少?

输入描述

第一行为一个整数 $$T(1 \le T \le 10)$$,表示测试数据组数。

每组测试数据第一行两个整数 $$K, N(2 \le N \le K \le 10^5)$$,其中 $$K$$ 是舞台的可用投影站位数,$$N$$ 是参与演出的虚拟偶像总人数。

第二行是 $$K$$ 个整数 $$x_i(1 \le x_i \le 10^9)$$,表示投影站位坐标,所有 $$x_i$$ 两两不同。

输出描述

每组数据输出一个结果,每个结果占一行。

样例1

输入

1
3 2
1 6 8

输出

7

样例解释

共有 $$3$$ 个站位,需要安排 $$2$$ 名偶像。$$2$$ 名虚拟偶像安排在站位坐标 $$1, 8$$,可获得最大稳定程度 $$7$$。

样例2

输入

1
6 4
24 3 42 15 7 30

输出

12

样例解释

共有 $$6$$ 个站位,需要安排 $$4$$ 名偶像。最优方案是将 $$4$$ 名偶像安排在站位坐标 $$3, 15, 30, 42$$,此时任意两名相邻偶像之间的距离分别为 $$12, 15, 12$$,可获得最大稳定程度 $$12$$。

样例3

输入

1
5 3
10000 10 20000 1 19999

输出

9999

样例解释

共有 $$5$$ 个站位,需要安排 $$3$$ 名偶像。$$3$$ 名虚拟偶像安排在站位坐标 $$1, 10000, 19999$$,此时任意两名相邻偶像之间的距离分别为 $$9999, 9999$$,可获得最大稳定程度 $$9999$$(安排在 $$1, 10000, 20000$$ 也是最优解之一)。

题解:二分答案

本题涉及到二分答案,不熟悉该算法的同学可以先做一下模板题:

分糖果(一)

购物系统的降级策略

题目内容拆解

从 $$K$$ 个站位中选 $$N$$ 个,使任意两个被选站位的最小距离最大。$$K$$ 可达 $$10^5$$,$$x_i$$ 可达 $$10^9$$。

核心观察:答案具有单调性——如果最小距离 $$d$$ 可行,则 $$d-1$$ 也可行。暴力枚举 $$\binom{K}{N}$$ 种方案不现实 → 因此采用二分答案 + 贪心验证

算法实现

二分答案:对最小距离 $$d$$ 进行二分。下界 $$lo = 0$$,上界 $$hi = x_{K-1} - x_0$$(排序后首尾之差),因为最小距离不可能超过首尾间距。

check 函数:排序后贪心放置,从第一个站位开始,每次选下一个距离 $$\ge d$$ 的站位。如果能放下 $$N$$ 个偶像则返回 true。贪心的正确性在于:尽量往前放能给后面留更多空间,不会比跳过更优。

二分过程:采用 while (lo < hi) 的开区间写法,取 $$mid = (lo + hi + 1) / 2$$(上取整避免死循环)。check(mid) 成立则 $$lo = mid$$,否则 $$hi = mid - 1$$。退出时 $$lo$$ 即为答案。

时空复杂度分析

  • 时间复杂度:$$O(T \cdot K \log K + T \cdot K \log V)$$,其中 $$V$$ 为值域($$\le 10^9$$),排序 $$O(K \log K)$$,二分 $$O(\log V)$$ 次 check,每次 check $$O(K)$$。
  • 空间复杂度:$$O(K)$$,存储站位坐标。

第三题:戴森环能源转运问题

在线评测链接:https://www.neituiya.com/oj/43/2515

题目描述

戴森环是一种环绕恒星建造、用于收集恒星能量的巨型人造天体结构。

AK机是负责转运能源的工作人员之一,他负责其中一条贯穿戴森环的直线检修带。本次任务他的飞船将停靠在坐标 $$s$$ 上,飞船可用于转运任务的总燃料为 $$N$$ 单位,若消耗超出限制,飞船将无法顺利返航。

AK机将沿着这条检修带回收沿途已经集满的能源仓,可以自行规划本次航线,既可以先朝坐标较小的方向推进,也可以先朝坐标较大的方向推进,飞船推进 $$1$$ 单位距离需要消耗 $$1$$ 单位燃料,任务途中可任意折返,但折返同样会消耗飞船燃料。

能源仓散布在检修带上,不同能源仓由于工艺技术以及设备老化的原因,能够存储的上限能源量并不相同。第 $$i$$ 个已集满的能源仓位于坐标 $$x_i$$ 处,其中储满了 $$c_i$$ 单位能源,同一个能源仓在本次任务中至多只能完成一次转运,本次收集后则需要重新经历一段时间的积累。

请你帮助AK机计算本次任务最多能够转运多少单位的能源。

输入描述

第一行为一个整数 $$T(1 \le T \le 10)$$,表示测试数据组数。

每组测试数据第一行三个整数 $$N, M, s(0 \le N \le 10^9, 1 \le M \le 10^5, 0 \le s \le 10^9)$$,其中 $$N$$ 为可用总燃料,$$M$$ 为已集满的能源仓总数,$$s$$ 为飞船初始坐标。

第二行是 $$M$$ 个整数 $$x_i(0 \le x_i \le 10^9)$$,表示已集满能源仓的坐标,所有 $$x_i$$ 两两不同。

第三行是 $$M$$ 个整数 $$c_i(1 \le c_i \le 10000)$$,表示 $$x_i$$ 处对应能源仓存储的能源量。

输出描述

每组数据输出一个结果,每个结果占一行。

样例1

输入

1
4 3 5
8 4 6
4 7 2

输出

9

样例解释

从起点坐标 $$5$$ 出发,可以先到坐标 $$4$$,再到坐标 $$6$$。共消耗 $$1+2=3$$ 单位燃料,可以完成这两个能源仓的转运任务,转运总量为 $$7 + 2 = 9$$。如果在这之后还想继续前往坐标 $$8$$,还需要额外消耗 $$2$$ 单位燃料,总消耗会变成 $$5$$,超过限制,因此答案为 $$9$$。

样例2

输入

1
5 4 5
8 1 6 4
9 3 5 8

输出

22

样例解释

从起点坐标 $$5$$ 出发,可以先到坐标 $$4$$,再到坐标 $$6$$,最后到坐标 $$8$$。共消耗 $$1+2+2=5$$ 单位燃料,可以完成这三个能源仓的转运任务,转运总量为 $$8+5+9=22$$。如果还想再前往坐标 $$1$$,所需燃料至少为 $$10$$,超过限制,因此答案为 $$22$$。

样例3

输入

1
9 7 10
4 13 9 11 6 15 8
10 8 4 7 9 11 5

输出

35

样例解释

从起点坐标 $$10$$ 出发,可以先到坐标 $$11$$,再依次前往坐标 $$9, 8, 6, 4$$。共消耗 $$1+2+1+2+2=8$$ 单位燃料,可以完成这五个能源仓的转运任务,转运总量为 $$7+4+5+9+10=35$$。如果还想再前往坐标 $$13$$ 或 $$15$$,所需燃料都会超出限制,因此答案为 $$35$$。

题解:贪心+前缀和+二分

题目内容拆解

从起点 $$s$$ 出发,在燃料 $$N$$ 以内沿直线收集能源仓,最大化收集总量。$$M$$ 可达 $$10^5$$,暴力枚举子集不可行。

核心观察:能源仓在一条直线上,走到左边距离 $$L$$ 的位置时,沿途所有仓都会被路过并收集。所以不用纠结"选哪些仓",只需决定"左边走多远、右边走多远"。

路线只有两种走法:先左后右,或先右后左。以先左后右为例:从起点往左走 $$L$$,再掉头走回起点,再往右走 $$R$$,燃料消耗 $$= L + L + R = 2L + R$$(往左的路走了两遍)。同理先右后左消耗 $$L + 2R$$ → 因此采用枚举一侧距离 + 二分另一侧

算法实现

算法主策略:将能源仓按位置分为左侧($$x_i < s$$)和右侧($$x_i \ge s$$),各自按距离从近到远排序。排完序后,"取最近的 $$i$$ 个仓"对应的能源总和就是前缀和 $$preSum[i]$$,最远距离就是 $$dist[i]$$。

枚举+二分:固定左侧取前 $$i$$ 个仓(即最近的 $$i$$ 个),左侧最远距离为 $$L_i$$。先左后右时,燃料消耗 $$2L_i + R$$,所以右侧可达距离为 $$N - 2L_i$$;先右后左时,燃料消耗 $$L_i + 2R$$,所以右侧可达距离为 $$(N - L_i) / 2$$。右侧的仓已按距离排好序,用二分在距离数组上查找可达距离内最远能到第几个仓,对应的前缀和就是右侧能源。

对称地,也枚举右侧取前 $$j$$ 个仓,二分左侧。取所有方案中能源总和的最大值。位于起点 $$s$$ 上的仓距离为 $$0$$,无需燃料即可收集。

时空复杂度分析

  • 时间复杂度:$$O(T \cdot M \log M)$$,排序 $$O(M \log M)$$,枚举+二分 $$O(M \log M)$$。
  • 空间复杂度:$$O(M)$$,存储坐标、能源和前缀和。

第四题:魔法树能量水晶分配

在线评测链接:https://www.neituiya.com/oj/43/2516

题目描述

AK机家里生长着一棵巨大的魔法树。这棵树由 $$n$$ 个魔法节点组成,节点之间通过 $$n-1$$ 条能量脉络相连(保证整体是一棵无向连通树)。

每个魔法节点都有一个"共鸣频率",用一个整数数组 $$freq$$ 表示。为了维持魔法树的运转,你需要给每个节点分配能量水晶。分配规则如下:每个魔法节点至少需要被分配 $$1$$ 颗能量水晶。对于任何通过能量脉络直接相连的两个节点,共鸣频率更高的节点,必须获得比另一节点严格更多的能量水晶。如果直接相连的两个节点共鸣频率相同,它们之间的水晶数量没有任何约束。

请你计算并返回,为了维持魔法树的运转,最少需要准备多少颗能量水晶?

输入描述

第一行包含一个整数 $$n(1 \le n \le 10^5)$$,表示魔法节点的数量,节点编号从 $$0$$ 到 $$n-1$$。

第二行包含 $$n$$ 个整数 $$freq[i](1 \le freq[i] \le 10^9)$$,表示每个节点的共鸣频率。

接下来 $$n-1$$ 行,每行包含两个整数 $$u, v$$,表示节点 $$u$$ 和节点 $$v$$ 之间有一条能量脉络。

输出描述

输出一个整数,表示最少需要的能量水晶总数。

样例1

输入

4
1 3 2 4
0 1
1 2
2 3

输出

6

样例解释

该树为一条直线:$$0(1)-1(3)-2(2)-3(4)$$。节点 $$0$$ 分配 $$1$$ 颗,节点 $$2$$ 分配 $$1$$ 颗,节点 $$1$$(连着 $$0$$ 和 $$2$$,频率最高)分配 $$2$$ 颗,节点 $$3$$(连着 $$2$$,频率比 $$2$$ 高)分配 $$2$$ 颗。总计 $$1+2+1+2=6$$。

样例2

输入

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

输出

6

样例解释

节点 $$0$$ 位于中心,频率为 $$5$$,周围 $$4$$ 个叶子节点频率为 $$1$$。$$4$$ 个叶子节点各分配 $$1$$ 颗水晶,中心节点 $$0$$ 频率高于所有相连节点,必须比它们都多,分配 $$2$$ 颗。总计 $$1 \times 4+2=6$$ 颗。

题解:贪心排序

题目内容拆解

树上分配水晶,相邻节点中频率高的必须严格更多,频率相等无约束,求最小总水晶数。$$n$$ 可达 $$10^5$$。

核心观察:每个节点的水晶数只取决于其频率更低的邻居——频率最低的节点没有"比它更低的邻居",所以一定可以只分 $$1$$ 颗。频率稍高的节点只需要比它的低频邻居多 $$1$$ 颗就行。从低到高依次确定,每个节点都取能满足约束的最小值 → 因此采用按频率排序 + 贪心赋值

算法实现

算法主策略:将所有节点按频率从小到大排序,初始每个节点 $$crystal[u] = 1$$。依次处理每个节点 $$u$$,遍历其所有邻居 $$v$$:若 $$freq[v] < freq[u]$$,则 $$crystal[u] = \max(crystal[u],\ crystal[v] + 1)$$。

排序保证了处理 $$u$$ 时,所有频率比它低的邻居都已经赋值完毕,所以 $$crystal[v]$$ 的值是确定的。频率相等的两个相邻节点互相没有"谁必须更多"的约束,各自独立取最小值即可,不会冲突。最终答案为所有 $$crystal[u]$$ 之和。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,排序 $$O(n \log n)$$,遍历所有边 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,邻接表和水晶数组。

2026-3-29

第一题:AK机驾驶员

在线评测链接:https://www.neituiya.com/oj/43/2425

第二题:AK机的排课

在线评测链接:https://www.neituiya.com/oj/43/2426

第三题:聪明的AK

在线评测链接:https://www.neituiya.com/oj/43/2427

第四题:AK机的city walk路径

在线评测链接:https://www.neituiya.com/oj/43/2428

2023-3-15

第一题:直播巡检

在线测评链接:https://www.neituiya.com/oj/3/2343

题目描述

AK机负责直播平台的首页直播排行榜的巡检工作。

今天平台一共收到 $$n$$ 条候选直播内容,按输入顺序依次编号为 $$1$$ 到 $$n$$。

每条直播内容都属于某个主播。为了避免同一主播同时占据多个排行榜位置,正式生成榜单前,

平台会先做一次主播去重:对于同一个主播,只保留该主播"最优"的一条直播内容进入候选榜单,

同一主播的其他直播内容会在这一步直接被淘汰,不再参与后续排序。

因此,最终直播排行榜中每个主播至多出现一次,榜单条数恰好等于不同主播的个数。

判断一条直播内容是否更优,按以下顺序比较:

  1. 点赞数更多者更优。
  2. 如果点赞数相同,则评论数更多者更优。

3) 如果点赞数和评论数都相同,则发布时间更早者更优。

4) 如果以上三项仍然完全相同,则原始编号更小者更优。

完成主播去重后,再对所有保留下来的直播内容按完全相同的规则排序,得到最终直播排行榜。

现在给出 $$q$$ 个查询。每次查询一条直播内容的最终结果:如果这条直播内容最终出现在排行榜中,输出它的排名;

如果它在主播去重阶段已经被淘汰,输出 $$0$$。

输入描述

第一行输入两个整数 $$n, q(1 \le n, q \le 2 \times 10^5)$$,分别表示候选直播内容数和查询条数。

接下来 $$n$$ 行,第 $$i$$ 行输入四个整数 $$u_i, a_i, b_i, t_i(1 \le u_i \le 10^9, 0 \le a_i, b_i, t_i \le 10^9)$$,表示第 $$i$$ 条直播内容所属主播编号、点赞数、评论数和发布时间。其中 $$t_i$$ 越小表示发布时间越早。

接下来 $$q$$ 行,每行输入一个整数 $$id(1 \le id \le n)$$,表示询问编号为 $$id$$ 的直播内容最终排名。

输出描述

输出 $$q$$ 行,每行一个整数。若对应直播条目最终上榜,输出其排名(排名从 $$1$$ 开始计数);否则输出 $$0$$。

样例1

输入

5 4
1 100 20 5
2 100 15 8
1 100 25 4
3 100 18 6
2 100 20 3
1
2
3
5

输出

0
0
1
2

样例解释

主播 $$1$$ 有第 $$1, 3$$ 两条直播,其中第 $$3$$ 条更优(评论数 $$25 > 20$$)。

主播 $$2$$ 有第 $$2, 5$$ 两条直播,其中第 $$5$$ 条更优(评论数 $$20 > 15$$)。

主播 $$3$$ 只有第 $$4$$ 条直播。因此候选榜单只剩第 $$3, 4, 5$$ 条,排序后顺序为 $$3 \to 5 \to 4$$。

题解:排序模拟

题目问题拆解

按主播分组,每组取最优项(点赞 $$\downarrow$$、评论 $$\downarrow$$、时间 $$\uparrow$$、编号 $$\uparrow$$),然后对所有存活项按相同规则排序,回答查询。$$n, q \le 2 \times 10^5$$,排序 $$O(n \log n)$$ 即可。

算法实现

算法主策略:本题采用分组取最优 + 排序

分三步完成:第一步,用哈希表按主播编号分组,对每个主播保留比较键 $$(-a, -b, t, id)$$ 最小的那条(取反使"越大越优"变为"越小越优",统一用 $$<$$ 比较)。第二步,将所有存活项按相同的比较键排序,得到排行榜顺序。第三步,建立编号到排名的映射表,查询时 $$O(1)$$ 回答。

以样例为例:主播 $$1$$ 有第 $$1, 3$$ 条,比较键分别为 $$(-100, -20, 5, 1)$$ 和 $$(-100, -25, 4, 3)$$,后者更小($$-25 < -20$$),保留第 $$3$$ 条。主播 $$2$$ 有第 $$2, 5$$ 条,保留第 $$5$$ 条($$-20 < -15$$)。主播 $$3$$ 只有第 $$4$$ 条。存活项 $$\{3, 4, 5\}$$ 排序后:第 $$3$$ 条($$b=25$$)$$>$$ 第 $$5$$ 条($$b=20$$)$$>$$ 第 $$4$$ 条($$b=18$$),排名 $$1, 2, 3$$。

时空复杂度分析

时间复杂度:$$O(n \log n + q)$$,排序 $$O(n \log n)$$,查询 $$O(1)$$ 每次。

空间复杂度:$$O(n)$$,存储直播数据和排名映射。

C++

// 直播巡检 - 排序模拟
#include <bits/stdc++.h>
using namespace std;

void solve() {
    int n, q;
    cin >> n >> q;

    struct Stream {
        int u, a, b, t, idx;
    };
    vector<Stream> streams(n);
    for (int i = 0; i < n; i++) {
        cin >> streams[i].u >> streams[i].a >> streams[i].b >> streams[i].t;
        streams[i].idx = i + 1;
    }

    // 第一步:按主播分组,保留最优(a大、b大、t小、id小)
    // 用 (-a,-b,t,idx) 作为比较键,值越小越优
    map<int, pair<tuple<int,int,int,int>, int>> best;
    for (auto& s : streams) {
        auto key = make_tuple(-s.a, -s.b, s.t, s.idx);
        if (best.find(s.u) == best.end() || key < best[s.u].first) {
            best[s.u] = {key, s.idx};
        }
    }

    // 收集所有存活的直播编号
    set<int> survived;
    for (auto& [u, v] : best) {
        survived.insert(v.second);
    }

    // 第二步:存活项按相同规则排序,生成排行榜
    struct Alive {
        int a, b, t, idx;
    };
    vector<Alive> alive;
    for (auto& s : streams) {
        if (survived.count(s.idx)) {
            alive.push_back({s.a, s.b, s.t, s.idx});
        }
    }
    sort(alive.begin(), alive.end(), [](const Alive& x, const Alive& y) {
        if (x.a != y.a) return x.a > y.a;  // 点赞数降序
        if (x.b != y.b) return x.b > y.b;  // 评论数降序
        if (x.t != y.t) return x.t < y.t;  // 发布时间升序
        return x.idx < y.idx;              // 编号升序
    });

    // 第三步:建立编号→排名映射,O(1)回答查询
    map<int, int> rank_map;
    for (int i = 0; i < (int)alive.size(); i++) {
        rank_map[alive[i].idx] = i + 1;
    }

    for (int i = 0; i < q; i++) {
        int qid;
        cin >> qid;
        if (rank_map.count(qid)) {
            cout << rank_map[qid] << "\n";
        } else {
            cout << 0 << "\n";  // 被淘汰的直播输出0
        }
    }
}

int main() {
    solve();
    return 0;
}

第二题:AK机的充电计划

在线测评链接:https://www.neituiya.com/oj/3/2344

题目描述

AK机驾驶电动车从起点 $$0$$ 出发,目的地距离为 $$L$$ 公里。电动车满电时可行驶 $$C$$ 公里,即电池容量为 $$C$$ 公里续航。沿途有 $$n$$ 个充电站,第 $$i$$ 个充电站位于距离起点 $$d_i$$ 公里处,充电价格为一公里 $$p_i$$ 元。

AK机可以在任何充电站进行充电,充电量可以任意,但不能超过电池总容量。

已知AK机出发时电池处于满电状态。请你帮AK机规划充电策略,求出到达目的地所需的最少充电费用。如果无论如何都无法到达目的地,请输出 $$-1$$。

输入描述

第一行包含三个整数 $$L, C, n(1 \le L, C \le 10^9, 1 \le n \le 5000)$$,分别表示目的地距离、满电续航公里数、充电站的数量。

接下来 $$n$$ 行,每行包含两个整数 $$d_i, p_i(1 \le d_i \le d_{i+1} < L, 1 \le p_i \le 10^9)$$,分别表示第 $$i$$ 个充电站距离起点的距离以及该站的充电单价。

输出描述

输出一个整数,表示到达目的地所需的最少充电总费用。如果无法到达,输出 $$-1$$。

样例1

输入

20 10 3
4 5
9 2
15 6

输出

24

样例解释

起点满电(续航 $$10$$)。开至距离 $$9$$ 的充电站,剩余电量 $$1$$。

在距离 $$9$$ 的充电站充 $$9$$ 公里电量达到满电,花费 $$9 \times 2 = 18$$。

开至距离 $$15$$ 的充电站,剩余电量 $$4$$。在距离 $$15$$ 的充电站充 $$1$$ 公里电量,花费 $$1 \times 6 = 6$$。

刚好开到终点 $$20$$,总费用 $$18 + 6 = 24$$。

样例2

输入

20 5 1
10 5

输出

-1

样例解释

满电只能跑 $$5$$ 公里,还没跑到第一个充电站就没电了,所以无法到达。

题解:贪心

题目问题拆解

从起点(满油 $$C$$)出发到终点 $$L$$,沿途 $$n$$ 个加油站各有不同价格。每站可充任意量(不超过 $$C$$),求最小费用。$$n \le 5000$$,$$O(n^2)$$ 可接受。

核心观察:在每个站点,优先使用更便宜的油。如果前方存在更便宜(或等价)的可达站点,只充恰好够到那里的油量;否则没有更便宜的选择,充满油箱以覆盖尽可能远的距离。

算法实现

算法主策略:本题采用前看贪心

首先检查可达性:起点到第一站、相邻站间距、最后一站到终点,任何间距超过 $$C$$ 则无解。

然后依次处理每个站点:在站点 $$i$$ 向前扫描,找到第一个价格 $$\le p_i$$ 且距离 $$\le C$$ 的站点 $$j$$(包括终点虚拟站,价格为 $$0$$)。

若找到,只充够到达 $$j$$ 的油量;若没找到,说明前方 $$C$$ 范围内都更贵,充满油箱。

以样例1为例:在站 $$(9, 2)$$ 找前方更便宜的可达站,终点 $$(20, 0)$$ 距离 $$11 > C = 10$$ 不可达,站 $$(15, 6)$$ 价格 $$6 > 2$$ 更贵。

没有更便宜的,充满($$9$$ 公里,费用 $$18$$)。在站 $$(15, 6)$$ 找前方:终点 $$(20, 0)$$ 距离 $$5 \le 10$$ 可达且价格 $$0 < 6$$,只充 $$\max(0, 5 - 4) = 1$$ 公里(费用 $$6$$)。

总费用 $$24$$。

时空复杂度分析

时间复杂度:$$O(n^2)$$,每个站点向前扫描 $$O(n)$$。$$n \le 5000$$,可以通过。

空间复杂度:$$O(n)$$,存储站点信息。

C++

// 充电计划 - 贪心
#include <bits/stdc++.h>
using namespace std;

long long solve(int L, int C, int n, vector<pair<int,int>>& stations) {
    stations.push_back({L, 0});

    // 检查可达性
    int prev = 0;
    for (auto& [d, p] : stations) {
        if (d - prev > C) return -1;
        prev = d;
    }

    long long cost = 0;
    int fuel = C;
    int prevD = 0;

    for (int i = 0; i < n; i++) {
        fuel -= (stations[i].first - prevD);
        prevD = stations[i].first;

        // 找前方第一个更便宜且可达的站
        int nxt = -1;
        for (int j = i + 1; j <= n; j++) {
            if (stations[j].second <= stations[i].second && stations[j].first - stations[i].first <= C) {
                nxt = j;
                break;
            }
        }

        if (nxt != -1) {
            int need = max(0, stations[nxt].first - stations[i].first - fuel);
            cost += (long long)need * stations[i].second;
            fuel += need;
        } else {
            cost += (long long)(C - fuel) * stations[i].second;
            fuel = C;
        }
    }

    return cost;
}

int main() {
    int L, C, n;
    cin >> L >> C >> n;
    vector<pair<int,int>> stations(n);
    for (int i = 0; i < n; i++) {
        cin >> stations[i].first >> stations[i].second;
    }
    cout << solve(L, C, n, stations) << "\n";
    return 0;
}

第三题:AK机的配送轨迹

在线测评链接:https://www.neituiya.com/oj/3/2345

题目描述

AK机正在检查一段配送轨迹日志。日志长度为 $$n$$,从起点 $$(0, 0)$$ 出发,按顺序记录了每一步移动指令。日志是一个长度为 $$n$$ 的字符串,只包含以下四种字符:U 向上移动一格,D 向下移动一格,R 向右移动一格,L 向左移动一格。

AK机怀疑其中有一段连续日志被错误写入。现在他可以从原串中删除至多一段连续子串;删除后,剩余的前后两段会直接拼接,执行顺序保持不变。

这里的"至多一段"包含两种边界情况:可以一个字符都不删;也可以删掉整个字符串,此时剩余轨迹为空,最终仍停在 $$(0, 0)$$。

目标仓库坐标为 $$(x, y)$$。请你求出最短需要删除多长的连续子串,才能让拼接后的整段轨迹最终停在 $$(x, y)$$。

如果原始轨迹本来就停在 $$(x, y)$$,可以不删除任何字符,此时答案为 $$0$$。如果不存在合法方案,输出 $$-1$$。

输入描述

第一行输入一个整数 $$T(1 \le T \le 3)$$,表示测试数据组数。

每组数据第一行输入一个整数 $$n(1 \le n \le 10^6)$$。

第二行输入一个长度恰好为 $$n$$ 的字符串 $$s$$(只含 U/D/L/R)。

第三行输入两个整数 $$x, y(-10^9 \le x, y \le 10^9)$$,表示目标仓库坐标。

保证单个测试用例内所有 $$n$$ 之和不超过 $$10^6$$。

输出描述

对每组数据输出一个整数,表示最短删除长度。如果不存在合法方案,输出 $$-1$$。

样例1

输入

2
6
RURDLD
0 0
3
UDL
2 0

输出

2
-1

样例解释

第一组数据中,删除第 $$3$$ 到第 $$4$$ 个字符(RD)后,剩余轨迹为 RULD,其最终位移变为 $$(0, 0)$$。不存在长度为 $$1$$ 的删除方案,因此答案为 $$2$$。

第二组数据中,无论删除哪一段连续子串,剩余轨迹的最终位移都不可能变成 $$(2, 0)$$,因此答案为 $$-1$$。

题解:前缀和 + 哈希表

题目问题拆解

设 $$px[i], py[i]$$ 为执行前 $$i$$ 步后的 $$x, y$$ 坐标(前缀和)。

如果我们删除 $$s[l \cdots r]$$,剩余轨迹由两段拼接:前缀 $$s[0 \cdots l-1]$$ 贡献位移 $$(px[l], py[l])$$,

后缀 $$s[r+1 \cdots n-1]$$ 贡献位移 $$(px[n]-px[r+1], py[n]-py[r+1])$$。

总位移 $$=(px[l]+px[n]-px[r+1], py[l]+py[n]-py[r+1])$$,要等于 $$(x, y)$$。

整理得:$$px[r+1]-px[l]=px[n]-x$$,$$py[r+1]-py[l]=py[n]-y$$。

令 $$R=r+1$$,$$dx=px[n]-x$$,$$dy=py[n]-y$$,需要找 $$l \le R$$ 使 $$px[R] - px[l] = dx$$ 且 $$py[R]-py[l]=dy$$,最小化删除长度 $$R-l$$。

$$n$$ 可达 $$10^6$$,需要 $$O(n)$$ 算法。

核心观察:条件等价于 $$(px[l], py[l])=(px[R]-dx, py[R]-dy)$$。对每个 $$R$$,只需在哈希表中查找是否存在这样的 $$l$$。

算法实现

算法主策略:本题采用前缀和 + 哈希表

从左到右扫描 $$R = 0, 1, \cdots, n$$。每到一个位置 $$R$$,先将 $$(px[R], py[R]) \to R$$ 存入哈希表(同键覆盖,保留最大的 $$l$$,因为 $$l$$ 越大则 $$R - l$$ 越小)。然后查询 $$(px[R] - dx, py[R] - dy)$$ 是否在表中,若在则用 $$R - l$$ 更新答案。注意必须先 add 再 query,这样 $$l = R$$(删除长度 $$0$$)的情况也能正确处理。

以样例1 $$s = $$ RURDLD,$$(x, y) = (0, 0)$$ 为例手算:$$px = [0, 1, 1, 2, 2, 2, 1]$$,$$py = [0, 0, 1, 1, 0, -1, -1]$$。$$dx=1-0=1$$,$$dy=-1-0=-1$$。扫描到 $$R = 4$$:查询 $$(px[4]-1, py[4]+1)=(2-1, 0+1)=(1,1)$$。哈希表中 $$(1, 1) \to 2$$($$R=2$$ 时存入)。$$R-l=4-2=2$$,即删除 $$s[2 \cdots 3] = $$ RD。答案 $$= 2$$。

时空复杂度分析

时间复杂度:$$O(n)$$ 每组数据,哈希表查询和插入均摊 $$O(1)$$。

空间复杂度:$$O(n)$$,存储前缀和和哈希表。

C++

// 配送轨迹 - 前缀和 + 哈希表
#include <bits/stdc++.h>
using namespace std;

// 前缀和+哈希表找最短删除段,使剩余位移等于(x,y)
int solve(int n, const string& s, int x, int y) {
    // 前缀和:px[i],py[i]为执行前i步后的坐标
    vector<int> px(n + 1, 0), py(n + 1, 0);
    for (int i = 0; i < n; i++) {
        px[i + 1] = px[i] + (s[i] == 'R' ? 1 : s[i] == 'L' ? -1 : 0);
        py[i + 1] = py[i] + (s[i] == 'U' ? 1 : s[i] == 'D' ? -1 : 0);
    }

    // 需要被删除的子串位移量
    int dx = px[n] - x;
    int dy = py[n] - y;

    int ans = n + 1;
    // seen: 前缀坐标(px[l],py[l]) → 最大的l(l越大删除越短)
    map<pair<int,int>, int> seen;

    for (int R = 0; R <= n; R++) {
        auto key = make_pair(px[R], py[R]);
        seen[key] = R;  // 先add:保证l=R(删除长度0)也能被找到
        // 查找是否存在l使得 px[R]-px[l]=dx, py[R]-py[l]=dy
        auto target = make_pair(px[R] - dx, py[R] - dy);
        auto it = seen.find(target);
        if (it != seen.end()) {
            ans = min(ans, R - it->second);
        }
    }

    return ans > n ? -1 : ans;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        string s;
        cin >> s;
        int x, y;
        cin >> x >> y;
        cout << solve(n, s, x, y) << "\n";
    }
    return 0;
}

第四题:AK机的扩容计划

在线测评链接:https://www.neituiya.com/oj/3/2346

题目描述

AK机最近在做一条服务链路的大促扩容预案。他拿到了未来 $$n$$ 个时间点的负载预测。

第 $$i$$ 个时间点业务需求为 $$a_i$$,当前基础容量为 $$b_i$$。

AK机最多可以申请 $$m$$ 个"临时扩容包"。每个扩容包都有相同的扩容量 $$x$$。

如果一个扩容包在第 $$i$$ 个时间点启动,那么它会在从 $$i$$ 开始的长度为 $$w$$ 的时间区间(即时间点 $$i, i+1, \cdots, i+w-1$$)上各额外提供 $$x$$ 容量。若区间超过第 $$n$$ 个时间点,超出的部分无需考虑。

多个扩容包可以在同一时刻启动,它们的效果可以叠加。

请你求出最小的非负整数 $$x$$,使得可以通过启动不超过 $$m$$ 个扩容包的情况下,让所有时间点都满足:基础容量 + 所有生效中的扩容包容量 $$\ge$$ 业务需求。

在得到最小可行的 $$x$$ 之后,还需要求出在这个最小 $$x$$ 下最少需要启动多少个扩容包。

如果无论怎样都无法满足全部时间点的业务需求,输出 $$-1$$。

输入描述

第一行输入一个整数 $$T(1 \le T \le 3)$$,表示测试数据组数。

每组数据第一行输入 $$3$$ 个整数 $$n, m, w(1 \le n \le 10^6, 0 \le m \le 10^6, 1 \le w \le n)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n(0 \le a_i \le 10^9)$$,表示各时间点的业务需求。

第三行输入 $$n$$ 个整数 $$b_1, b_2, \cdots, b_n(0 \le b_i \le 10^9)$$,表示各时间点的基础容量。

保证单个测试用例中所有 $$n$$ 之和不超过 $$10^6$$。

输出描述

对每组数据各输出一行:若存在可行解,输出两个整数 $$x, C$$,分别表示最小可行的单包扩容量以及在该扩容量下最少需要启动的扩容包数量;若不存在可行解则输出 $$-1$$。

样例1

输入

2
5 2 3
5 7 6 4 5
4 4 4 4 4
6 2 2
10 1 10 1 10 1
0 0 0 0 0 0

输出

3 2
-1

样例2

输入

3
1 1 1
5
2
3 4 3
13 13 13
1 1 1
5 1 4
0 0 0 0 6
0 0 0 0 0

输出

3 1
3 4
6 1

题解:二分答案 + 贪心

题目问题拆解

求最小扩容量 $$x$$ 使得用 $$\le m$$ 个覆盖宽度 $$w$$ 的扩容包可以补齐所有缺口 $$gap_i=\max(0, a_i-b_i)$$。

$$n$$ 可达 $$10^6$$,需要 $$O(n \log V)$$ 算法($$V$$ 为值域上界)。

核心观察:$$x$$ 越大越容易满足(单调性),可以二分答案。

对给定的 $$x$$,贪心从左到右扫,缺口不够时在当前位置启动扩容包,用过期数组跟踪到期的包。

算法实现

二分答案:二分 $$x \in [0, \max(gap)]$$,check 目标是"用 $$\le m$$ 个包能否覆盖所有缺口"。

check 函数:给定 $$x$$,从左到右贪心扫描。维护两个关键变量:$$active$$ 记录当前有多少个扩容包正在生效,$$expires[i]$$ 记录在时间点 $$i$$ 有多少个包到期失效。在时间点 $$i$$,先执行 $$active$$ -= $$expires[i]$$(扣除到期包),然后检查当前容量缺口:若需要 $$\lceil gap_i / x \rceil$$ 个包但只有 $$active$$ 个在生效,就在此处启动差额个新包(每个覆盖 $$[i, i+w-1]$$,在 $$i+w$$ 到期)。若总包数超过 $$m$$,不可行。

二分过程:若 $$greedy(mid) \le m$$,缩小上界 $$hi = mid$$;否则扩大下界 $$lo = mid + 1$$。

输出:最终 $$lo$$ 即为最小 $$x$$,$$greedy(lo)$$ 为最少包数。若 $$greedy(lo) > m$$,输出 $$-1$$。

以样例1第一组 $$n=5, m=2, w=3$$,$$gap = [1, 3, 2, 0, 1]$$ 为例:二分到 $$x = 3$$ 时,$$i=0$$:需 $$\lceil 1/3 \rceil = 1$$ 个包,$$active = 0$$,启动 $$1$$ 个(覆盖 $$[0,2]$$,$$expires[3] += 1$$)。$$i=1$$:需 $$\lceil 3/3 \rceil = 1$$,$$active = 1$$ 够用。$$i=2$$:需 $$\lceil 2/3 \rceil = 1$$,$$active = 1$$ 够用。$$i=3$$:$$active -= expires[3] = 1$$,$$active = 0$$,$$gap = 0$$ 无需扩容。$$i=4$$:需 $$\lceil 1/3 \rceil = 1$$,启动 $$1$$ 个。总共 $$2 \le m = 2$$,可行。输出 $$3, 2$$。

时空复杂度分析

时间复杂度:$$O(n \log V)$$,二分 $$O(\log V)$$ 轮,每轮贪心 $$O(n)$$。$$V \le 10^9$$。

空间复杂度:$$O(n)$$,存储缺口数组和过期数组。

类似题目

【网易】2025-9-28-第三题-分苹果

【网易】2025-10-12-第四题-村落撤离

C++

// 扩容计划 - 二分答案 + 贪心
#include <bits/stdc++.h>
using namespace std;

// 贪心计算:给定单包容量x,最少需要多少扩容包
int greedy(vector<int>& gap, int n, int m, int w, int x) {
    if (x == 0) return m + 1;
    int count = 0;
    int active = 0;  // 当前生效中的扩容包数
    vector<int> expires(n + w + 1, 0);  // expires[i]:在时间点i到期的包数
    for (int i = 0; i < n; i++) {
        active -= expires[i];  // 扣除到期失效的包
        int need_total = (gap[i] + x - 1) / x; // 时间点i至少需要多少个包
        int need = max(0, need_total - active); // 还需启动多少个新包
        if (need > 0) {
            count += need;
            if (count > m) return count;  // 早停剪枝
            active += need;
            if (i + w < (int)expires.size()) {
                expires[i + w] += need;  // 新包在i+w时到期
            }
        }
    }
    return count;
}

void solve() {
    int n, m, w;
    cin >> n >> m >> w;
    vector<int> a(n), b(n);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < n; i++) cin >> b[i];

    // gap[i] = 时间点i的容量缺口
    vector<int> gap(n);
    int maxGap = 0;
    for (int i = 0; i < n; i++) {
        gap[i] = max(0, a[i] - b[i]);
        maxGap = max(maxGap, gap[i]);
    }

    if (maxGap == 0) {
        cout << "0 0\n";  // 无缺口,不需要扩容
        return;
    }
    if (m == 0) {
        cout << "-1\n";  // 有缺口但不能申请扩容包
        return;
    }

    // 二分最小的单包容量x
    int lo = 0, hi = maxGap;
    while (lo < hi) {
        int mid = (lo + hi) / 2;
        if (greedy(gap, n, m, w, mid) <= m) {
            hi = mid;  // x=mid可行,尝试更小
        } else {
            lo = mid + 1;  // x=mid不够,需要更大
        }
    }

    int c = greedy(gap, n, m, w, lo);
    if (c > m) {
        cout << "-1\n";
    } else {
        cout << lo << " " << c << "\n";
    }
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        solve();
    }
    return 0;
}

京东

2026-3-28

第一题:序列生成器

在线评测链接:https://www.neituiya.com/oj/7/2420

题目描述

对于一个序列 $$A$$,我们定义序列 $$(A+1)$$ 为将序列 $$A$$ 里每个元素值都加 $$1$$ 得到的序列。

例如:$$[2, 3, 1]+1=[3, 4, 2]$$,$$[1, 2, 1]+1=[2, 3, 2]$$。

对于序列 $$A$$ 和 $$B$$,我们定义序列 $$C=A*B$$ 表示序列 $$C$$ 是由序列 $$A$$ 和序列 $$B$$ 拼接而成(序列 $$A$$ 在前,序列 $$B$$ 在后)。

例如:$$[2, 3, 1]*[1, 2, 1]=[2, 3, 1, 1, 2, 1]$$,$$[1, 2, 3]*[6, 5, 4]=[1, 2, 3, 6, 5, 4]$$。

AK机得到了一个序列生成器。丢给这个生成器一个序列 $$A$$,这个序列生成器会返回序列 $$(A+1)*A$$。

AK机先将仅由一个数 $$x$$ 构成的序列 $$[x]$$ 丢给生成器,然后不断将序列生成器返回的序列再次丢入。现在AK机想问,他第 $$n$$ 次丢入得到的结果序列中第 $$k$$ 个位置的值是多少?

例如:当 $$x=3, n=3, k=6$$ 时,一开始的序列为 $$[3]$$,第 $$1$$ 次丢入得到的结果是 $$[4, 3]$$,第 $$2$$ 次是 $$[5, 4, 4, 3]$$,第 $$3$$ 次是 $$[6, 5, 5, 4, 5, 4, 4, 3]$$,第 $$6$$ 个数是 $$4$$。

输入描述

一行三个整数 $$x, n, k(0 \le x \le 10^5, 1 \le n \le 60, 1 \le k \le 2^n)$$,表示初始序列为 $$[x]$$,AK机想知道第 $$n$$ 次结果序列的第 $$k$$ 个数是多少。

输出描述

输出一个整数,表示第 $$n$$ 次丢入得到的结果序列中第 $$k$$ 个位置的值。

样例1

输入

3 3 6

输出

4

样例解释

初始序列为 $$[3]$$,第 $$1$$ 次生成器输出 $$[4, 3]$$,第 $$2$$ 次输出 $$[5, 4, 4, 3]$$,第 $$3$$ 次输出 $$[6, 5, 5, 4, 5, 4, 4, 3]$$,第 $$6$$ 个位置的值是 $$4$$。

题解

题目内容拆解

给定初始值 $$x$$,每次操作将序列 $$A$$ 变为 $$(A+1)*A$$,求第 $$n$$ 次操作后序列中第 $$k$$ 个元素的值。由于 $$n$$ 可达 $$60$$,序列长度为 $$2^{60}$$,无法直接模拟,需要找到数学规律。

算法实现

算法主策略:本题采用递归分治思想,观察每次操作的结构特征。

每次操作将长度为 $$L$$ 的序列 $$A$$ 变为长度 $$2L$$ 的 $$(A+1)*A$$,其中左半部分是 $$A+1$$(所有元素加 $$1$$),右半部分是 $$A$$(不变)。因此查找第 $$k$$ 个元素时,如果 $$k$$ 落在左半部分($$k \le L$$),等价于在上一轮序列中找第 $$k$$ 个元素再加 $$1$$;如果落在右半部分($$k > L$$),等价于在上一轮序列中找第 $$k - L$$ 个元素。

将 $$k-1$$ 写成 $$n$$ 位二进制:每个 $$0$$ 位对应一次"落入左半"(加 $$1$$),每个 $$1$$ 位对应一次"落入右半"(不加)。因此答案为 $$x + n - \text{popcount}(k-1)$$,其中 $$\text{popcount}$$ 是二进制中 $$1$$ 的个数。

以样例验证:$$x=3, n=3, k=6$$,$$k-1=5=101_2$$,$$\text{popcount}=2$$,答案 $$= 3 + 3 - 2 = 4$$。

时空复杂度分析

  • 时间复杂度:$$O(1)$$,只需计算 $$k-1$$ 的 popcount。
  • 空间复杂度:$$O(1)$$,只使用常数额外空间。

Go

// 序列生成器 - 递归分治 + 位运算
package main

import (
        "bufio"
        "fmt"
        "math/bits"
        "os"
)

// 每次操作将序列A变为(A+1)*A,左半加1右半不变
// 第k个位置的值 = x + n - popcount(k-1)
func solve(x, n int, k int64) int {
        return x + n - bits.OnesCount64(uint64(k-1))
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        var x, n int
        var k int64
        fmt.Fscan(reader, &x, &n, &k)
        fmt.Println(solve(x, n, k))
}

第二题:传送

在线评测链接:https://www.neituiya.com/oj/7/2421

题目描述

AK机正在某个魔法王国中游历。他当前所在的城市数字代号为 $$a$$,而他的朋友所在的城市数字代号为 $$b$$。魔法王国中跨城市通行需要使用传送门,传送规则如下:

  1. 到达城市数字代号为当前的二倍的城市。即,假如数字代号本来为 $$n$$,此类型传送会到达数字代号为 $$2n$$ 的城市。
  2. 到达城市数字代号为当前的二分之一的城市,让它变成原来的一半后向下取整。即,假如数字代号本来为 $$n$$,此类型传送后将变成 $$\lfloor n/2 \rfloor$$。例:对 $$6$$ 使用此类型传送将变成 $$3$$;对 $$7$$ 使用将变成 $$3$$(向下取整了)。

3) 到达当前城市代号增加一的城市,即,数字代号本来为 $$n$$,使用此类型传送后将到达数字代号为 $$n+1$$ 的城市。

这三种传送类型都可以给AK机或他的朋友任意次数使用。现在想要用尽可能少的传送次数让AK机与朋友到达同一个城市,求最少的传送次数。

输入描述

一行两个正整数 $$a, b(1 \le a, b \le 1000)$$,表示AK机和朋友当前所在城市代号。

输出描述

输出一个整数,表示最少的传送次数。

样例1

输入

3 5

输出

2

样例解释

AK机将 $$3$$ 传送到 $$6$$(使用 $$\times 2$$ 传送),朋友将 $$5$$ 传送到 $$6$$(使用 $$+1$$ 传送),共 $$2$$ 次传送即可到达同一城市。

题解

本题涉及到BFS,不熟悉该算法的同学可以先做一下模板题:

离开中山路

马的遍历

题目内容拆解

给定两个初始位置 $$a, b(1 \le a, b \le 1000)$$,两人分别可以执行 $$\times 2$$、$$\lfloor \div 2 \rfloor$$、$$+1$$ 三种操作,求最少总操作次数使两人到达同一个城市。本质是在数轴上寻找一个会合点 $$t$$,使得 $$\text{dist}(a, t) + \text{dist}(b, t)$$ 最小。

算法实现

算法主策略:本题采用双源BFS,分别从 $$a$$ 和 $$b$$ 出发做 BFS,计算各自到每个城市的最短距离。

从起点出发,将每个城市看作图的节点,三种操作看作边。分别对 $$a$$ 和 $$b$$ 做 BFS,得到距离数组 $$\text{dist}_a$$ 和 $$\text{dist}_b$$。最后枚举所有可能的会合城市 $$v$$,取 $$\text{dist}_a[v] + \text{dist}_b[v]$$ 的最小值即为答案。

由于 $$a, b \le 1000$$,$$\times 2$$ 操作最多产生 $$2000$$ 的值,将搜索范围限制在 $$[0, 2001]$$ 即可覆盖所有有意义的会合点。

时空复杂度分析

  • 时间复杂度:$$O(L)$$,其中 $$L = 2001$$ 为搜索范围上界,两次 BFS 各访问 $$O(L)$$ 个节点,枚举会合点也是 $$O(L)$$。
  • 空间复杂度:$$O(L)$$,两个距离数组各占 $$O(L)$$ 空间。

C++

// 传送 - BFS最短路
#include <bits/stdc++.h>
using namespace std;

const int LIMIT = 2001;

// 从起点出发BFS,计算到所有可达节点的最短距离
vector<int> bfs(int start) {
    vector<int> dist(LIMIT + 1, -1);
    queue<int> q;
    dist[start] = 0;
    q.push(start);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        // 三种传送:×2、÷2(向下取整)、+1
        int nxt[] = {u * 2, u / 2, u + 1};
        for (int v : nxt) {
            if (v >= 0 && v <= LIMIT && dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}

int solve(int a, int b) {
    vector<int> da = bfs(a);
    vector<int> db = bfs(b);
    int ans = INT_MAX;
    // 枚举所有可能的会合城市
    for (int v = 0; v <= LIMIT; v++) {
        if (da[v] >= 0 && db[v] >= 0) {
            ans = min(ans, da[v] + db[v]);
        }
    }
    return ans;
}

int main() {
    int a, b;
    cin >> a >> b;
    cout << solve(a, b) << endl;
    return 0;
}

2026-3-14(A卷)

第一题:星际快递

在线测评链接:https://www.neituiya.com/oj/3/2335

题目描述

星际快递公司有 $$N$$ 个包裹需要派送,每个包裹有两种派送方式!

$$1$$、常规派送(消耗较多燃料)

$$2$$、虫洞派送(使用一个虫洞通行证,可以以消耗较少燃料的情况下完成派送)

当前快递飞船携带了 $$X$$ 单位的燃料和 $$Y$$ 张虫洞通行证。

星际快递公司想要计算,在优先派送尽可能多包裹的情况下,最小的燃料消耗是多少,请你帮助他们计算一下。

输入描述

第一行输入三个整数 $$N,X,Y(1\le N\le 100,1\le X\le 5000,1\le Y\le 50)$$ ,分别表示包裹数量,携带燃料量以及通行证数量。

接下来 $$N$$ 行,每行输入两个整数,表示各个包裹常规派送和虫洞派送分别需要的燃料量

每个包裹常规派送和虫洞派送的燃料消耗均介于 $$[1,50]$$ 之间

输出描述

输出一行两个整数,空格分开,表示最多可派送的包裹数量及对应的最小燃料消耗。

样例1

输入

3 20 1
8 5
7 4
10 6

输出

2 12

样例2

输入

4 25 2
10 6
8 5
12 7
9 6

输出

3 20

题解:01背包

题目内容拆解

本题的核心是:每个包裹有两种派送方式(常规和虫洞),每种方式消耗不同的燃料和通行证。目标是在不超过燃料和通行证限制的前提下,最大化可派送包裹数量,并在此基础上最小化燃料消耗。

本质是二维0/1背包问题:每个包裹只能选一次,状态为剩余燃料和剩余通行证。

不熟悉01背包算法的同学推荐做一下这道模板题:01背包

算法实现

状态方程定义

定义$$f[y][x]$$为使用$$y$$张通行证、消耗$$x$$单位燃料时最多可派送的包裹数量。

最终$$f[Y][X]$$为最大可派送包裹数。枚举所有燃料消耗$$x$$,找到$$f[Y][x]=f[Y][X]$$的最小$$x$$作为最优燃料消耗。

状态方程初始化

初始$$f[0][0]=0$$,其余$$f[y][x]=0$$。

状态方程转移

对于每个包裹,依次考虑两种派送方式:

  1. 常规派送:若剩余燃料$$x\geq a_i$$,则

    $$f[y][x] = \max(f[y][x],\ f[y][x-a_i] + 1)$$

  2. 虫洞派送:若剩余燃料$$x\geq b_i$$且剩余通行证$$y\geq 1$$,则

    $$f[y][x] = \max(f[y][x],\ f[y-1][x-b_i] + 1)$$

每次转移需倒序枚举燃料和通行证容量,避免重复使用同一包裹。

时间复杂度分析

每个包裹更新$$O(XY)$$状态,$$N$$个包裹总复杂度$$O(NXY)$$,数据范围$$N\leq 100,X\leq 5000,Y\leq 50$$,可以通过。空间复杂度$$O(XY)$$。

类似题目

提瓦特商店

最大快乐值(一)

Java

import java.io.*;
import java.util.*;

public class Main {

    /*
    - f[yy][xx] 表示通行证容量 yy、燃料容量 xx 下最多能派送的包裹数量。
    - 对每个包裹进行倒序 0/1 背包更新(常规派送和虫洞派送两种选择)。
    - 先取 f[Y][X] 得到最大数量;再在 xx∈[0..X] 中找最小的 xx 使 f[Y][xx] 等于最大数量。
    */

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int x = Integer.parseInt(st.nextToken());
        int y = Integer.parseInt(st.nextToken());

        int[] a = new int[n];
        int[] b = new int[n];
        for (int i = 0; i < n; ++i) {
            st = new StringTokenizer(br.readLine());
            a[i] = Integer.parseInt(st.nextToken());
            b[i] = Integer.parseInt(st.nextToken());
        }

        // f[yy][xx]:通行证容量为 yy、燃料容量为 xx 时的最多派送数
        int[][] f = new int[y + 1][x + 1];

        // 对每个包裹进行 0/1 背包倒序更新
        for (int i = 0; i < n; ++i) {
            int na = a[i], nb = b[i];
            for (int yy = y; yy >= 0; --yy) {         // 通行证容量倒序
                for (int xx = x; xx >= 0; --xx) {     // 燃料容量倒序
                    // 常规派送(不消耗通行证)
                    if (xx >= na) {
                        f[yy][xx] = Math.max(f[yy][xx], f[yy][xx - na] + 1);
                    }
                    // 虫洞派送(消耗 1 张通行证)
                    if (yy >= 1 && xx >= nb) {
                        f[yy][xx] = Math.max(f[yy][xx], f[yy - 1][xx - nb] + 1);
                    }
                }
            }
        }

        // 最大可派送数量
        int maxCnt = f[y][x];

        // 在达成最大数量的前提下,找最小燃料消耗(最小的 xx)
        int minFuel = 0;
        for (int xx = 0; xx <= x; ++xx) {
            if (f[y][xx] == maxCnt) {
                minFuel = xx;
                break;
            }
        }

        System.out.println(maxCnt + " " + minFuel);
    }
}

第二题:01串

在线测评链接:https://www.neituiya.com/oj/5/2336

题目描述

AK机有一个长度为$$n$$的$$01$$串$$s$$,即仅由字符$$0$$和$$1$$组成的字符串,如$$0101101$$。除此之外他还有$$m$$个数字,

分别用$$a_1,a_2,..,a_m$$表示。

AK机很好奇,他能否选择$$m$$个不相交的区间$$[l_1,r_1][l_2,r_2],...,[l_m,r_m]$$,使得对于任意的$$a_i$$,其二进制表示(没有

前导$$0$$,$$0$$的二进制表示就是$$0$$),都能用$$s$$的某个连续子串$$s_{l_j,l_{j+1},...r_j}$$来表示。

输入描述

输入包括多组测试数据。

第一行输入一个正整数$$T(1\le T\le 20)$$,表示测试数据的组数。

每组测试数据的第一行输入两个整数$$n,m(1\le n\le 100,1\le m\le 6)$$,分别表示$$01$$串$$s$$的长度,数字个数。

第二行输入一行长度为$$n$$的$$01$$串$$s$$。

第三行输入$$m$$个整数$$a_1,a_2,...,a_m(0\le a_i\le 2^{10})$$,表示AK机的$$m$$个数字。

输出描述

对于每组测试数据,如果存在答案,输出一行$$YES$$;否则,输出一行$$NO$$。

样例1

输入

2
5 2
10110
2 1
5 1
00000
1

输出

YES
NO

样例解释

对于第一组测试数据,$$2$$的二进制表示为$$10$$,$$1$$的二进制表示为$$1$$,其中一种可以选择的区间为 $$[1,2]、[3,3]$$。

对于第二组测试数据,$$1$$的二进制表示为$$1$$,由于$$01$$串中不存在字符$$1$$,故答案一定不存在。

题解:DFS

题目内容拆解

本题的核心是:给定一个长度为$$n$$的$$01$$串$$s$$和$$m$$个数字$$a_1,\ldots,a_m$$,问能否在$$s$$中选出$$m$$个不相交的区间,

使得每个区间的子串等于$$a_i$$的二进制表示(无前导$$0$$),每个区间对应一个数字,区间不能重叠。

关键点有:

  1. 每个数字$$a_i$$的二进制表示长度不超过$$11$$,$$m\leq 6$$,$$n\leq 100$$,可以枚举所有方案。
  2. 需要在$$s$$中找到每个$$a_i$$的所有匹配子串区间,并保证最终选择的$$m$$个区间不相交。

3) 这是一个典型的多模式串匹配+不相交区间选择问题,适合回溯/DFS解决。

算法实现

  1. 对每个$$a_i$$,转为无前导$$0$$的二进制字符串。
  2. 在$$s$$中枚举所有长度等于$$a_i$$二进制长度的子串,记录所有等于$$a_i$$二进制的区间$$[l,r]$$。

3) 对所有数字,按可选区间数量从少到多排序,减少回溯分支。

4) 用DFS/回溯依次为每个数字选择一个区间,要求区间两两不相交。

  1. 若能为所有数字选出不相交区间,则输出"YES",否则输出"NO"。

时间复杂度分析

$$m$$较小,回溯分支最多$$O((n^2)^m)$$,但实际剪枝后远小于指数级。每组数据复杂度可接受。

整体复杂度$$O(T\cdot m\cdot n^2)$$,可以通过所有测试数据。

类似题目

【科大讯飞】2025-8-2-第二题-字符串拼接

【华为留学生】2025-5-7-第三题-筛选樱桃

Java

import java.util.*;

public class Main {
    // 整数转无前导0的二进制字符串
    static String toBin(int x) {
        if (x == 0) return "0";
        StringBuilder sb = new StringBuilder();
        while (x > 0) {
            sb.append((x % 2) == 1 ? '1' : '0');
            x /= 2;
        }
        return sb.reverse().toString();
    }

    // 回溯搜索:判断是否能为每个数字选出一个不相交的区间
    // idx: 当前正在处理第几个数字
    // m: 总数字个数
    // match: 每个数字所有可选区间列表
    // used: 标记01串的哪些位置已经被选过
    static boolean dfs(int idx, int m, List<List<int[]>> match, boolean[] used) {
        if (idx == m) return true; // 所有数字都选完了,返回true
        for (int[] seg : match.get(idx)) {
            boolean ok = true;
            // 检查该区间是否与已选区间重叠
            for (int i = seg[0]; i <= seg[1]; ++i) {
                if (used[i]) { ok = false; break; }
            }
            if (!ok) continue; // 有重叠,跳过
            // 标记该区间已被选
            for (int i = seg[0]; i <= seg[1]; ++i) used[i] = true;
            // 递归处理下一个数字
            if (dfs(idx + 1, m, match, used)) return true;
            // 回溯:取消标记
            for (int i = seg[0]; i <= seg[1]; ++i) used[i] = false;
        }
        return false; // 所有区间都试过了,无法满足条件
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int T = sc.nextInt();
        while (T-- > 0) {
            int n = sc.nextInt(), m = sc.nextInt();
            String s = sc.next();
            String[] bin = new String[m];
            for (int i = 0; i < m; ++i) {
                int x = sc.nextInt();
                bin[i] = toBin(x); // 整数转二进制字符串
            }
            // 对每个数字,找所有在s中等于bin[i]的子串区间
            List<List<int[]>> match = new ArrayList<>();
            for (int i = 0; i < m; ++i) {
                List<int[]> list = new ArrayList<>();
                int len = bin[i].length();
                for (int l = 0; l + len <= n; ++l) {
                    if (s.substring(l, l + len).equals(bin[i])) {
                        list.add(new int[]{l, l + len - 1}); // 记录区间
                    }
                }
                match.add(list);
            }
            // 优化:先处理可选区间数量少的数字,减少回溯分支
            Integer[] order = new Integer[m];
            for (int i = 0; i < m; ++i) order[i] = i;
            Arrays.sort(order, Comparator.comparingInt(x -> match.get(x).size()));
            List<List<int[]>> match2 = new ArrayList<>();
            for (int i = 0; i < m; ++i) match2.add(match.get(order[i]));

            boolean[] used = new boolean[n]; // 标记哪些位置已被选
            boolean ok = dfs(0, m, match2, used); // 回溯搜索
            System.out.println(ok ? "YES" : "NO");
        }
    }
}

米哈游

2026-4-19

第一题:快递投递

在线评测链接:https://www.neituiya.com/oj/13/2546

题目描述

在一张无限的二维网格上,有若干住户的家位于整数坐标点上(同一坐标可能有多人)。你首先在任意整数坐标点上固定建造一个快递驿站,然后每天从该驿站出发前往原点 $$(0,0)$$。你总会选择一条从驿站到原点的最短路径。路径由一系列相邻的格点组成,每一步只能向上、下、左、右移动一个单位,路径中包含起点(驿站位置)和终点(原点)。例如:从 $$(x_0,y_0)$$ 到 $$(0,0)$$ 的最短路径长度为 $$|x_0-0|+|y_0-0|$$ 的路径都视为最短路径。你可以在所有最短路径中任选其一。

在选定最优驿站位置后,允许在多天内每天任选一条最短路径并在路径途经的住户完成投递。问最多能累计送达多少不同住户?

输入描述

输入包含多组测试数据。第一行包含整数 $$T(1 \le T \le 10^5)$$ 表示测试组数。每组数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$。

接下来 $$n$$ 行,每行输入两个整数 $$x_i, y_i(-10^9 \le x_i, y_i \le 10^9)$$,表示一位住户的家在 $$(x_i,y_i)$$。

保证所有测试中 $$n$$ 的总和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,输出一行,仅包含一个整数——在最优驿站位置与任意多次最短路径选择下,最终可送达的不同住户人数。

样例1

输入

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

输出

5
5
3

题解

题目内容拆解

选一个驿站位置,多天走不同最短路径,最大化送达住户数。一个驿站能覆盖哪些住户?

核心观察:驿站放在某个象限且足够远时,该象限内全部住户都能被覆盖。答案就是四个象限的住户数取最大值。

算法实现

最短路径覆盖了哪些格点

以驿站在第一象限 $$(x_0, y_0)$$($$x_0 \ge 0, y_0 \ge 0$$)为例。到原点的最短路径一共走 $$|x_0| + |y_0|$$ 步,每步要么向左、要么向下。

一条路径就是 $$x_0$$ 个"左"和 $$y_0$$ 个"下"的一种排列。不同排列经过不同中间格点。多天把所有排列都走一遍,能到达的格点集合是:

$$\{(a, b) \mid 0 \le a \le x_0,\ 0 \le b \le y_0\}$$

也就是驿站和原点围成的整个矩形。任取矩形内一点 $$(a, b)$$,先从 $$(x_0, y_0)$$ 走到 $$(a, b)$$,再从 $$(a, b)$$ 走到原点,拼起来就是一条合法的最短路径。

其余三个象限完全对称。

为什么取四个象限的最大值

驿站放在第一象限,覆盖矩形朝右上方,只能送 $$x \ge 0, y \ge 0$$ 的住户。放在第三象限,覆盖矩形朝左下方,只能送 $$x \le 0, y \le 0$$ 的住户。

不同象限的覆盖方向互不兼容,驿站又只能放一个位置,所以选住户最多的象限。

坐标轴上的住户

$$(3, 0)$$ 同时满足 $$y \ge 0$$ 和 $$y \le 0$$,属于第一象限也属于第四象限。原点 $$(0,0)$$ 属于全部四个象限。

代码中对每个住户用四个独立的 if(不是 elif),一个住户可以被多个象限同时计入。无论驿站放哪个象限,轴上的住户都在覆盖范围内,重复计入是正确的。

时空复杂度分析

  • 时间复杂度:$$O(\sum n)$$,每个住户做一次坐标符号判断。
  • 空间复杂度:$$O(1)$$,只需四个计数器。

第二题:拆开

在线评测链接:https://www.neituiya.com/oj/13/2547

题目描述

给定四个整数 $$n, k, m, r$$,其中 $$0 \le r \le m-1$$ 且 $$m \ge 1$$。判断是否可以将 $$n$$ 表示为 $$k$$ 个两两不同的正整数之和,且每个数都与 $$r$$ 在模 $$m$$ 意义下同余。若可行,输出一组构造;否则输出 $$NO$$。

$$\exists a_1, \dots, a_k \in \mathbb{Z}_{>0} \text{ s.t. } \sum_{i=1}^k a_i = n,\ a_i \equiv r \pmod{m},\ a_i \text{ 两两不同}$$

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 表示测试用例数量。

对每个测试用例,输入一行四个整数 $$n, k, m, r(0 \le n \le 10^{18}, 1 \le k \le 2 \times 10^5, 1 \le m \le 10^9, 0 \le r \le m-1)$$。

除此之外,保证单个测试文件的 $$k$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每个测试用例,若不存在表示,输出一行 $$NO$$。否则输出一行 $$YES$$,并在下一行输出 $$k$$ 个两两不同的正整数 $$a_1,\dots,a_k$$(顺序任意,需满足同余与求和的约束)。若存在多个合法答案,输出任意一组即可。

样例1

输入

3
15 3 4 1
18 3 5 3
36 3 6 0

输出

YES
1 5 9
NO
YES
6 12 18

样例解释

对于第一组数据:$$a = \{1,5,9\}$$,均为正整数,互不相同,且 $$a_i \equiv 1 \pmod{4}$$,并且 $$\sum a_i = 15$$。

对于第二组数据:不存在满足条件的表示,输出 $$NO$$。

对于第三组数据:$$a = \{6,12,18\}$$,均为正整数,互不相同,且 $$a_i \equiv 0 \pmod{6}$$,并且 $$\sum a_i = 36$$。

题解

题目内容拆解

把 $$n$$ 拆成 $$k$$ 个不同的正整数,每个模 $$m$$ 余 $$r$$。先算出最小能拆多少,够不够,不够就无解,够了就把多余的塞给最后一个数。

核心观察:合法的数排成等差数列,最小的 $$k$$ 个数的和是固定的。$$n$$ 比这个和小,或者差值不是 $$m$$ 的倍数,就拆不出来。

算法实现

合法数长什么样

模 $$m$$ 余 $$r$$ 的正整数是一个公差为 $$m$$ 的等差数列。

$$r > 0$$ 时,最小正整数就是 $$r$$,数列是 $$r,\ r+m,\ r+2m,\ \dots$$

$$r = 0$$ 时,$$0$$ 不是正整数,最小的合法数是 $$m$$,数列是 $$m,\ 2m,\ 3m,\ \dots$$

代码里统一用 $$\text{firstVal}$$ 表示首项。

最小和

贪心取最小的 $$k$$ 项:$$\text{firstVal},\ \text{firstVal}+m,\ \dots,\ \text{firstVal}+(k-1)m$$。

$$S_{\min} = k \cdot \text{firstVal} + m \cdot \frac{k(k-1)}{2}$$

$$n < S_{\min}$$ 时,怎么选都凑不出 $$k$$ 个不同的数,无解。

模约束

每个数都模 $$m$$ 余 $$r$$,$$k$$ 个数加起来模 $$m$$ 的余数是固定的:

$$n \bmod m = (k \cdot r) \bmod m$$

不满足就无解。代码里把模约束和下界约束合在一起判:算 $$\text{base} = k \cdot \text{firstVal}$$,看 $$n - \text{base}$$ 是不是 $$m$$ 的非负倍数,再看这个倍数够不够 $$k(k-1)/2$$。

怎么构造

前 $$k-1$$ 个数取等差数列的最小值,最后一个数 $$= n$$ 减去前面的总和。

为什么最后一个数一定合法?$$n - S_{\min}$$ 是 $$m$$ 的倍数,加到最后一项上不改变模 $$m$$ 的余数。

又因为 $$n \ge S_{\min}$$,最后一项 $$\ge \text{firstVal} + (k-1)m$$,比前面所有数都大,两两不同自动满足。

时空复杂度分析

  • 时间复杂度:$$O(\sum k)$$,每个测试用例循环 $$k$$ 次输出。
  • 空间复杂度:$$O(1)$$,逐个输出无需额外存储。

第三题:数字间隔

在线评测链接:https://www.neituiya.com/oj/13/2548

题目描述

AK机给定了一个长度为 $$n$$ 的整数序列 $$a_1,a_2,\dots,a_n$$,请你统计有多少对 $$(i,j)$$ 满足:$$1 \le i < j \le n$$,$$|a_i - a_j| = 1$$,在 $$a_i$$ 和 $$a_j$$ 之间(不包括 $$a_i$$ 和 $$a_j$$)的所有数字都严格大于 $$\max(a_i,a_j)$$。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示序列 $$a$$ 的长度。

第二行 $$n$$ 个整数 $$a_1,a_2,\dots,a_n(1 \le a_i \le 10^9)$$,表示数字序列。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$3 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数表示满足条件的对数。

样例1

输入

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

输出

3
0

样例解释

第一组数据:$$(1,3)$$:$$|1-2|=1$$,中间 $$3>2$$ 满足。$$(2,3)$$:$$|3-2|=1$$,无中间数字满足。$$(3,5)$$:$$|2-1|=1$$,中间 $$4>2$$ 满足。共 $$3$$ 对满足。

第二组数据:所有数字相同,没有满足条件的对。

题解

题目内容拆解

统计"差值为 $$1$$、中间全部更大"的位置对数。暴力 $$O(n^2)$$,$$n$$ 之和 $$3 \times 10^5$$,需要线性做法。

核心观察:把数组想象成一排高低不同的柱子。两根柱子"互相可见" = 中间所有柱子都比它俩高。单调栈就是干这个的。

算法实现

有效对的条件

$$|a_i - a_j| = 1 \quad \text{且} \quad \min_{i < k < j} a_k > \max(a_i, a_j)$$

中间最矮的柱子也要比两端都高。

为什么用单调栈

维护一个从栈底到栈顶值非递减的栈。

栈里相邻的两个元素之间,中间那些更矮的值早就被弹出去了,天然满足"中间无障碍"。每个元素入栈一次、出栈一次,总共 $$O(n)$$。

弹出阶段——找 $$a_j + 1$$:

处理位置 $$j$$ 时,把栈中所有值 $$> a_j$$ 的元素依次弹出。

弹出的元素比 $$a_j$$ 大,如果恰好等于 $$a_j + 1$$,就和 $$j$$ 构成有效对。

同值元素只取最近的一个(代码里用 found 标志)。更远的那个和 $$j$$ 之间隔着近处的同值元素,值相同不满足"严格大于",无效。

栈顶检查——找 $$a_j - 1$$:

弹出完毕后,栈顶值 $$\le a_j$$。

如果恰好等于 $$a_j - 1$$,栈顶和 $$j$$ 构成有效对。两者之间的元素刚全被弹出了(值都 $$> a_j$$),而 $$\max(a_j - 1, a_j) = a_j$$,条件成立。

栈顶值如果 $$< a_j - 1$$ 或 $$= a_j$$,差值不是 $$1$$,跳过。

不会重复计数

每个有效对只在处理 $$j$$ 时被发现一次。$$a_i > a_j$$ 的在弹出阶段找到,$$a_i < a_j$$ 的在栈顶检查找到。

时空复杂度分析

  • 时间复杂度:$$O(\sum n)$$,每个元素入栈一次、出栈至多一次。
  • 空间复杂度:$$O(n)$$,栈最坏存整个数组(递增序列时无弹出)。

2026.3.14

第一题:整数矩阵

在线测评链接:https://www.neituiya.com/oj/10/2332

题目描述

给定一个$$n$$行$$m$$列的整数矩阵$$a$$,请统计满足以下条件的行列对数量:第$$i$$行所有元素之和恰好等于第$$j$$列所有元素之和。

输入描述

在一行上输入两个整数$$n, m(1 \le n, m \le 10^6, n \times m \le 10^6)$$,表示矩阵的行数和列数。

此后$$n$$行,每行输入$$m$$个整数$$a_{i,1}, a_{i,2}, ..., a_{i,m}(0 \le a_{i,j} \le 10^9)$$,表示矩阵中各元素。

输出描述

输出一个整数,表示满足条件的行列对数量。

样例1

输入

3 3
1 1 1
1 1 1
1 1 1

输出

9

样例解释

每一行的元素之和均为$$3$$,每一列的元素之和也均为$$3$$,共有$$3 \times 3 = 9$$对行列满足相等。

题解

题目内容拆解

给定$$n \times m$$矩阵,统计有多少对$$(i, j)$$使得第$$i$$行的元素之和等于第$$j$$列的元素之和。$$n \times m$$可达$$10^6$$,需要$$O(n \times m)$$时间完成。

算法实现

算法主策略:本题采用哈希表计数

第一步:计算所有行和与列和。遍历矩阵一遍,同时累加每行和每列的元素之和,时间$$O(n \times m)$$。

第二步:哈希表统计列和频次。将所有列和的出现次数存入哈希表。

第三步:遍历行和,累计匹配数。对于每个行和$$s$$,查询哈希表中列和等于$$s$$的个数,累加到答案中。

以样例为例,$$3 \times 3$$全$$1$$矩阵中,每行之和$$= 3$$,每列之和$$= 3$$。哈希表中$$3$$出现了$$3$$次,$$3$$行各贡献$$3$$,总计$$9$$对。

时空复杂度分析

  • 时间复杂度:$$O(n \times m)$$,遍历矩阵计算行列和$$O(n \times m)$$,哈希表查询$$O(n + m)$$。
  • 空间复杂度:$$O(n + m)$$,存储行和数组、列和数组与哈希表。

Java

// 整数矩阵 - 哈希表计数
import java.io.*;
import java.util.*;

public class Main {
    static long solve(int n, int m, int[][] a) {
        // 计算行和与列和
        long[] rowSums = new long[n];
        long[] colSums = new long[m];
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++) {
                rowSums[i] += a[i][j];
                colSums[j] += a[i][j];
            }
        // 哈希表统计列和频次
        Map<Long, Integer> colCnt = new HashMap<>();
        for (int j = 0; j < m; j++)
            colCnt.merge(colSums[j], 1, Integer::sum);
        // 遍历行和累计匹配数
        long ans = 0;
        for (int i = 0; i < n; i++)
            ans += colCnt.getOrDefault(rowSums[i], 0);
        return ans;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        int[][] a = new int[n][m];
        for (int i = 0; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < m; j++)
                a[i][j] = Integer.parseInt(st.nextToken());
        }
        System.out.println(solve(n, m, a));
    }
}

第二题:乱翘的数组hard

在线测评链接:https://www.neituiya.com/oj/10/2333

题目描述

对于给定的长度为$$n$$的数组$$\{a_1, a_2, ..., a_n\}$$,我们定义"翘数"为同时严格大于或小于左右相邻数的数字。形式化的讲,对于第$$i(1 < i < n)$$个整数$$a_i$$,它被称作"翘数",当且仅当满足$$a_{i-1} < a_i > a_{i+1}$$或$$a_{i-1} > a_i < a_{i+1}$$。

若一个数组中,所有的满足$$i \in (1, n)$$的数字$$a_i$$均为"翘数",且任意相邻的两个元素$$a_j, a_{j+1}(1 \le j < n)$$都不相等,则称该数组为"乱翘的数组"。

现在,对于给定的初始数组,计算最少需要从原数组中删除的数字个数,使得剩余数字按原相对顺序拼接成的新数组是一个"乱翘的数组"。

输入描述

第一行输入一个整数$$n(3 \le n \le 2 \times 10^5)$$,代表数组中的元素数量。

第二行输入$$n$$个整数$$a_1, a_2, ..., a_n(-10^7 \le a_i \le 10^7)$$,代表数组元素。

输出描述

在一行上输出一个整数,代表最少需要删除的数字个数。

样例1

输入

7
1 3 1 4 5 2 0

输出

2

样例解释

其中一种最优的方案是删除数组中的第五、七个数字。

样例2

输入

3
2 2 2

输出

2

题解

题目内容拆解

给定长度为$$n$$的数组,求最少删除多少个元素,使剩余元素构成"乱翘的数组"(即锯齿形数组:每个内部元素都是严格的局部极值,且相邻元素不相等)。等价于求最长交替子序列的长度,答案为$$n$$减去该长度。

算法实现

算法主策略:本题采用贪心

核心观察:最长锯齿形子序列的长度等于原数组中"方向变化"的次数加$$1$$。方向变化是指相邻两个元素的增减趋势发生了翻转(从上升变下降,或从下降变上升)。相邻相等的元素不产生方向变化,直接跳过。

贪心策略:从左到右扫描数组,维护当前方向$$d$$(上升或下降)。每当方向发生翻转时,子序列长度加$$1$$。这种贪心策略是最优的,因为每个单调段只需要保留首尾两个端点,就能最大化后续的方向变化机会。

以样例$$1$$为例,数组$$[1, 3, 1, 4, 5, 2, 0]$$的变化过程:$$1 \to 3$$(上升),$$3 \to 1$$(下降,方向变化),$$1 \to 4$$(上升,方向变化),$$4 \to 5$$(上升,方向未变),$$5 \to 2$$(下降,方向变化),$$2 \to 0$$(下降,方向未变)。共$$4$$次方向变化,最长子序列长度$$= 4 + 1 = 5$$,最少删除$$7 - 5 = 2$$个。

对于样例$$2$$,数组$$[2, 2, 2]$$全相同,无方向变化,最长子序列长度$$= 1$$,删除$$3 - 1 = 2$$个。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,只需一次遍历统计方向变化。
  • 空间复杂度:$$O(n)$$,存储输入数组。

Java

// 乱翘的数组hard - 贪心(最长交替子序列)
import java.io.*;
import java.util.*;

public class Main {
    static int solve(int n, int[] a) {
        if (n <= 2) return 0;
        // 贪心统计方向变化次数
        int length = 1;
        int lastDir = 0;
        for (int i = 1; i < n; i++) {
            int d = 0;
            if (a[i] > a[i - 1]) d = 1;
            else if (a[i] < a[i - 1]) d = -1;
            else continue;
            if (d != lastDir) {
                length++;
                lastDir = d;
            }
        }
        return n - length;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] a = new int[n];
        for (int i = 0; i < n; i++)
            a[i] = Integer.parseInt(st.nextToken());
        System.out.println(solve(n, a));
    }
}

第三题:树上异或路径

在线测评链接:https://www.neituiya.com/oj/10/2334

题目描述

给你一棵有$$n$$个节点的无向树。每条边有一个非负整数权值$$w$$。请计算这棵树上所有简单路径的异或和之和。注意,本题中树为无向图,端点$$(u, v)$$与$$(v, u)$$视为同一路径,仅统计一次。

说明:路径指的是在树上选择两个节点作为端点的简单路径。当两个端点相同的时候,路径长度为$$0$$,其异或和为$$0$$。

【名词解释】

按位异或(Bitwise XOR):对两个整数的二进制表示按位进行异或运算。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数$$T(1 \le T \le 2 \times 10^5)$$表示数据组数。除此之外,保证单个测试文件的$$n$$之和不超过$$5 \times 10^5$$。每组测试数据的格式如下:

第一行输入一个整数$$n(1 \le n \le 2 \times 10^5)$$,表示节点数量。

此后$$n-1$$行,每行输入三个整数$$u, v, w(1 \le u, v \le n, 0 \le w \le 10^9)$$,表示一条连接$$u$$与$$v$$的边及其权值。保证给出的是一棵树。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示所有简单路径的异或和之和。若结果可能很大,请将答案对$$10^9 + 7$$取模后输出。

样例1

输入

2
3
1 2 1
2 3 2
4
1 2 0
2 3 0
3 4 7

输出

6
21

样例解释

对于第一组:树为$$1-2-3$$,边权分别为$$1, 2$$。所有端点对为$$(1,2), (1,3), (2,3)$$,对应路径异或和依次为$$1, 3, 2$$,求和得到$$1 + 3 + 2 = 6$$。

题解

题目内容拆解

给定带权树,求所有$$\binom{n}{2}$$条路径的异或值之和,结果对$$10^9 + 7$$取模。$$n$$可达$$2 \times 10^5$$且多组数据,暴力枚举所有点对是$$O(n^2)$$,必定超时。需要找到一种不枚举路径本身的统计方法。

算法实现

本题需要两个关键洞察,下面逐步展开。

洞察一:树上路径异或 = 两端到根的异或距离的异或

定义$$dist[v]$$为从根节点$$1$$到节点$$v$$的路径上所有边权的异或和。例如树$$1 \xrightarrow{w_1} 2 \xrightarrow{w_2} 3$$中,$$dist[3] = w_1 \oplus w_2$$。

树上$$u$$到$$v$$的路径必然经过它们的最近公共祖先$$LCA$$。路径异或值为:从$$u$$走到$$LCA$$的异或 $$\oplus$$ 从$$LCA$$走到$$v$$的异或。而$$dist[u] = dist[LCA] \oplus$$($$LCA$$到$$u$$的异或),所以($$LCA$$到$$u$$的异或)$$= dist[u] \oplus dist[LCA]$$。同理($$LCA$$到$$v$$的异或)$$= dist[v] \oplus dist[LCA]$$。

两段拼起来:$$path(u,v) = (dist[u] \oplus dist[LCA]) \oplus (dist[v] \oplus dist[LCA]) = dist[u] \oplus dist[v]$$。

$$dist[LCA]$$异或了两次互相抵消!所以不需要求LCA,直接用$$dist[u] \oplus dist[v]$$就是路径异或值。

洞察二:按位拆分,独立统计每一位的贡献

直接求$$\sum dist[u] \oplus dist[v]$$仍然需要枚举所有对。但异或是按位独立的运算,我们可以逐位统计贡献

对于二进制第$$k$$位,$$dist[u] \oplus dist[v]$$在第$$k$$位为$$1$$,当且仅当$$dist[u]$$和$$dist[v]$$在第$$k$$位恰好一个为$$1$$、一个为$$0$$。设$$n$$个节点中有$$cnt_1$$个在第$$k$$位为$$1$$,$$cnt_0 = n - cnt_1$$个为$$0$$,则"第$$k$$位为$$1$$"的路径数为$$cnt_1 \times cnt_0$$,每条贡献$$2^k$$。

总答案:$$\sum_{k=0}^{29} 2^k \times cnt_1[k] \times cnt_0[k] \pmod{10^9 + 7}$$。

完整步骤

  1. 以节点$$1$$为根BFS,计算$$dist[v] = dist[parent] \oplus w_{edge}$$。
  2. 对每一位$$k$$($$0$$到$$29$$),遍历所有节点统计$$cnt_1$$。

3) 累加$$2^k \times cnt_1 \times cnt_0$$到答案。

样例推导(第一组:$$n=3$$,边$$1-2$$权$$1$$,边$$2-3$$权$$2$$):

BFS求距离:$$dist[1] = 0 = (00)_2$$,$$dist[2] = 0 \oplus 1 = 1 = (01)_2$$,$$dist[3] = 1 \oplus 2 = 3 = (11)_2$$。

验证:$$path(1,2) = dist[1] \oplus dist[2] = 0 \oplus 1 = 1$$,$$path(1,3) = 0 \oplus 3 = 3$$,$$path(2,3) = 1 \oplus 3 = 2$$。与直接计算一致。

按位统计:第$$0$$位(权值$$2^0 = 1$$),$$dist$$第$$0$$位分别为$$0, 1, 1$$,$$cnt_1 = 2, cnt_0 = 1$$,贡献$$1 \times 2 \times 1 = 2$$。第$$1$$位(权值$$2^1 = 2$$),$$dist$$第$$1$$位分别为$$0, 0, 1$$,$$cnt_1 = 1, cnt_0 = 2$$,贡献$$2 \times 1 \times 2 = 4$$。总计$$2 + 4 = 6$$。与暴力求和$$1 + 3 + 2 = 6$$一致。

样例推导(第二组:$$n=4$$,边权$$0, 0, 7$$):

$$dist = [0, 0, 0, 7]$$。$$7 = (111)_2$$,对第$$0, 1, 2$$位各有$$cnt_1 = 1, cnt_0 = 3$$,贡献分别为$$1 \times 3 = 3, 2 \times 3 = 6, 4 \times 3 = 12$$。总计$$3 + 6 + 12 = 21$$。

时空复杂度分析

  • 时间复杂度:$$O(n \times 30)$$,BFS遍历$$O(n)$$,$$30$$位各遍历$$n$$个节点统计$$cnt_1$$。总计约$$30n$$次操作,对$$n = 2 \times 10^5$$完全足够。
  • 空间复杂度:$$O(n)$$,存储树的邻接表和$$dist$$数组。

Java

// 树上异或路径 - BFS求根距离 + 按位统计贡献
import java.io.*;
import java.util.*;

public class Main {
    static final long MOD = 1_000_000_007;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        int T = Integer.parseInt(br.readLine().trim());

        while (T-- > 0) {
            int n = Integer.parseInt(br.readLine().trim());
            List<int[]>[] adj = new ArrayList[n + 1];
            for (int i = 0; i <= n; i++) adj[i] = new ArrayList<>();

            for (int i = 0; i < n - 1; i++) {
                StringTokenizer st = new StringTokenizer(br.readLine());
                int u = Integer.parseInt(st.nextToken());
                int v = Integer.parseInt(st.nextToken());
                int w = Integer.parseInt(st.nextToken());
                adj[u].add(new int[]{v, w});
                adj[v].add(new int[]{u, w});
            }

            // BFS从根出发,dist[v] = 根到v路径的边权异或和
            // 关键性质:path(u,v)的异或 = dist[u] ^ dist[v],LCA被抵消
            int[] dist = new int[n + 1];
            boolean[] visited = new boolean[n + 1];
            visited[1] = true;
            Queue<Integer> q = new LinkedList<>();
            q.add(1);
            while (!q.isEmpty()) {
                int u = q.poll();
                for (int[] edge : adj[u]) {
                    int v = edge[0], w = edge[1];
                    if (!visited[v]) {
                        visited[v] = true;
                        dist[v] = dist[u] ^ w;
                        q.add(v);
                    }
                }
            }

            // 按位统计贡献:第k位为1的路径数 = cnt1 * cnt0
            long ans = 0;
            for (int bit = 0; bit < 30; bit++) {
                long cnt1 = 0;
                for (int i = 1; i <= n; i++)
                    if (((dist[i] >> bit) & 1) == 1) cnt1++;
                long cnt0 = n - cnt1;
                // 这些路径在第k位的总贡献 = 2^k * 路径数
                ans = (ans + (1L << bit) % MOD * cnt1 % MOD * cnt0) % MOD;
            }
            sb.append(ans).append('\n');
        }
        System.out.print(sb);
    }
}

蚂蚁

2026-4-19-研发岗

第一题:拼好房

在线评测链接:https://www.neituiya.com/oj/7/2558

题目描述

AK机用木棍拼一个"房子"形状:下部是一个长方形,上部是一个等腰三角形,且两者共用一条边(即长方形的上边同时作为三角形的底边,三角形除底边的两条边要相等)。他手上有一堆木棍(每根木棍的长度为正整数),每根木棍最多使用一次,请你判断,是否能从中挑出若干根,恰好拼成这样的"房子"。

输入描述

第一行输入一个整数 $$n(6 \le n \le 10^5)$$,表示木棍数量。

第二行包含 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^5)$$,表示每根木棍的长度。

输出描述

输出一行:若能用这些木棍拼出"房子",输出 YES;否则输出 NO。

样例1

输入

6
4 4 3 3 5 5

输出

YES

样例解释

用两根长度 $$4$$ 的木棍作为长方形的左右两边,两根长度 $$3$$ 的木棍作为长方形的上下两边(上边同时作为三角形的底边),两根长度 $$5$$ 的木棍作为三角形的两条腰。三角形满足 $$2 \times 5 > 3$$,因此可以构成合法房子。

样例2

输入

6
1 2 3 4 5 6

输出

NO

样例解释

$$6$$ 根木棍长度各不相同,无法凑出 $$3$$ 对等长的木棍来构成房子形状。


第二题:不降序序列

在线评测链接:https://www.neituiya.com/oj/7/2559

题目描述

给定 $$n$$ 个(长度可能不同)的整数序列 $$s_1, s_2, \ldots, s_n$$,定义序列拼接 $$s_x + s_y$$ 为先写出 $$s_x$$,再紧接着写出 $$s_y$$ 所得到的序列。

我们称一个序列是"不降序"的,当且仅当对所有满足 $$i < j$$ 的位置都有 $$a_i \le a_j$$。

请计算在所有有序序列对 $$(s_x, s_y)(1 \le x, y \le n$$,允许 $$x = y)$$ 中,使得拼接序列 $$s_x + s_y$$ 为不降序的序列对数量。

顺序重要且可重复,即 $$(s_x, s_y)$$ 与 $$(s_y, s_x)$$ 视为不同的序列对。

输入描述

每个测试文件均包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$ 表示序列个数。

接下来对每个 $$i = 1, \ldots, n$$,先输入一个整数 $$L_i(1 \le L_i \le 2 \times 10^5)$$,表示序列 $$s_i$$ 的长度,随后一行输入 $$s_1, s_2, \ldots, s_{L_i}(0 \le s_j \le 10^9)$$,表示该序列的元素。

除此之外,保证单个测试文件的 $$L_i$$ 之和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一个整数,表示满足条件的有序序列对 $$(s_x, s_y)$$ 的数量。

样例1

输入

2
3
2 1 3
3 2 4 6
2 5 7
2
1 5
1 3

输出

1
3

样例解释

第一组:三个序列分别为 $$[1, 3]$$、$$[2, 4, 6]$$、$$[5, 7]$$,均为不降序。拼接后的 $$9$$ 个序列对中,只有 $$(s_1, s_3) = [1, 3, 5, 7]$$ 满足不降序($$3 \le 5$$),其余拼接处均不满足。

第二组:序列 $$[5]$$ 和 $$[3]$$ 均为不降序。$$(s_1, s_1) = [5, 5]$$ 满足($$5 \le 5$$);$$(s_2, s_1) = [3, 5]$$ 满足($$3 \le 5$$);$$(s_2, s_2) = [3, 3]$$ 满足($$3 \le 3$$);$$(s_1, s_2) = [5, 3]$$ 不满足($$5 > 3$$)。共 $$3$$ 对。


第三题:二次幂变换2

在线评测链接:https://www.neituiya.com/oj/7/2560

题目描述

给定两个整数 $$x$$ 与 $$y$$,你可以进行若干次操作,使得 $$x$$ 与 $$y$$ 相等。

每次操作选择任意非负整数 $$k$$,选择 $$x$$ 或 $$y$$ 中的一个数,并将其值加上 $$2^k$$。

请计算使 $$x, y$$ 相等所需的最少操作次数。

输入描述

每个测试文件均包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

一行包含两个整数 $$x, y(-10^{18} \le x, y \le 10^{18})$$。

输出描述

对于每组测试数据,输出一行一个整数,表示使 $$x$$ 与 $$y$$ 相等的最少操作次数。

样例1

输入

5
0 0
7 0
5 14
-3 5
3 10

输出

0
2
2
1
2

样例解释

第二组:$$x = 7, y = 0$$。对 $$y$$ 加 $$2^3 = 8$$ 得 $$y = 8$$,再对 $$x$$ 加 $$2^0 = 1$$ 得 $$x = 8$$,$$2$$ 次操作。若只对 $$y$$ 操作则需要加 $$2^0 + 2^1 + 2^2 = 7$$,共 $$3$$ 次,不是最优。

第三组:$$x = 5, y = 14$$,差值 $$9 = 1001_2$$,有 $$2$$ 个 $$1$$,恰好加两次 $$2^0$$ 和 $$2^3$$ 给 $$x$$ 即可。

第四组:$$x = -3, y = 5$$,差值 $$8 = 2^3$$,对 $$x$$ 加一次 $$2^3$$ 即可。

第五组:$$x = 3, y = 10$$,差值 $$7 = 111_2$$。对 $$y$$ 加 $$2^3 = 8$$ 得 $$y = 18$$,对 $$x$$ 加 $$2^0 + 2^1 + \ldots$$ 需 $$3$$ 次。更优方案:对 $$x$$ 加 $$2^3 = 8$$ 得 $$x = 11$$,对 $$y$$ 加 $$2^0 = 1$$ 得 $$y = 11$$,共 $$2$$ 次。

2026-4-16-研发岗

第一题:仅含1和合数的数组

在线评测链接:https://www.neituiya.com/oj/7/2527

第二题:剪绳子

在线评测链接:https://www.neituiya.com/oj/7/2528

第三题:不互质元素下标

在线评测链接:https://www.neituiya.com/oj/7/2529

2026-4-9-算法岗

第一题:穿过黑暗之门

在线评测链接:https://www.neituiya.com/oj/7/2471

第二题:离散型马尔可夫模型预测

在线评测链接:https://www.neituiya.com/oj/7/2472

第三题:公倍数对和

在线评测链接:https://www.neituiya.com/oj/7/2473

2026-4-2-研发岗

第一题:也许互质序列

在线评测链接:https://www.neituiya.com/oj/7/2460

题目描述

给定一个长度为 $$n$$ 的整数数列 $$\{a_1, a_2, \ldots, a_n\}$$,你需要判断它是不是一个"也许互质序列"。

在本题中,$$\gcd(x, y)$$ 表示 $$x$$ 和 $$y$$ 的最大公约数,比如 $$\gcd(10, 21) = 1$$,$$\gcd(10, 25) = 5$$。

一个序列是"也许互质序列",当且仅当同时满足下面三条:

对所有 $$1 \le i \le n-1$$,都有 $$\gcd(a_i, a_{i+1}) = 1$$(相邻两项互质);

对所有 $$1 \le i \le n-k$$,都有 $$\gcd(a_i, a_{i+k}) > 1$$(下标相差 $$k$$ 的两项不互质);

对所有 $$1 \le i < j \le n$$,都有 $$a_i \neq a_j$$(所有数字两两不同)。

输入描述

每个测试文件均包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, k(2 \le k \le n \le 5 \times 10^5)$$,含义如题目所示。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^{18})$$,表示这个序列。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$5 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行输出 Yes 或 No。

样例1

输入

3
5 2
10 21 22 39 34
4 3
10 21 22 25
5 2
2 3 5 7 11

输出

Yes
Yes
No

样例解释

第三组中,序列为 $$2, 3, 5, 7, 11,k = 2$$。相邻项互质(全为质数)满足条件一,元素两两不同满足条件三。但 $$\gcd(a_1, a_3) = \gcd(2, 5) = 1$$,不满足条件二(要求 $$> 1$$),因此输出 No。

题解:GCD验证

题目问题拆解

给定一个序列,验证它是否同时满足三个条件:相邻互质、距离 $$k$$ 不互质、元素两两不同。数据规模 $$n$$ 之和 $$\le 5 \times 10^5$$,→ 因此采用逐条件线性验证即可。

算法实现

算法主策略:本题是纯验证问题,依次检查三个条件,任一不满足立即返回 No。

条件一遍历相邻对 $$(a_i, a_{i+1})$$,计算 $$\gcd$$ 是否等于 $$1$$。

条件二遍历所有距离为 $$k$$ 的对 $$(a_i, a_{i+k})$$,检查 $$\gcd > 1$$。

条件三用哈希集合判断是否有重复元素。三个条件各自 $$O(n)$$ 遍历,GCD 单次 $$O(\log V)$$,总时间线性。

时空复杂度分析

时间复杂度:$$O(n \log V)$$,其中 $$V$$ 为最大元素值,GCD 计算为 $$O(\log V)$$,共 $$O(n)$$ 对。

空间复杂度:$$O(n)$$,用于去重的哈希集合。

C++

Java

Python

Go

第二题:按位与权值和

在线评测链接:https://www.neituiya.com/oj/7/2461

题目描述

给定一个长度为 $$n > 1$$ 的整数数组 $$a$$。你需要选择一个分割点 $$p(1 \le p < n)$$,将数组切分为两部分:前缀 $$B = \{a_1, a_2, \ldots, a_p\}$$ 与后缀 $$C = \{a_{p+1}, a_{p+2}, \ldots, a_n\}$$。

定义本次方案的"权值和"为:

$$W(p) = \sum_{b \in B} \sum_{c \in C} (b \mathbin{\&} c)$$

即对所有跨组的有序对 $$(b, c)(b \in B, c \in C$$)计算按位与并求和,配对数为 $$|B| \cdot |C|$$。

你的目标是选择分割点 $$p$$ 使得 $$W(p)$$ 最大,输出这个最大值。

符号"&"表示按位与运算(对两个数的二进制逐位与),对应位均为 1 的结果位为 1,否则为 0。例如 $$5\ (101_2)$$ 与 $$3\ (011_2)$$ 满足 $$5 \mathbin{\&} 3 = 1\ (001_2)$$。

输入描述

输入包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 10^5)$$,表示测试组数,每组测试数据描述如下:

第一行输入一个整数 $$n(2 \le n \le 2 \times 10^5)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(0 \le a_i \le 10^8)$$。

保证所有测试中 $$n$$ 的总和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据,输出一行一个整数,表示在上述规则下能够得到的按位与最大权值和。

样例1

输入

1
4
5 2 7 1

输出

8

样例解释

选择 $$p = 2,B = \{5, 2\},C = \{7, 1\},W(2) = 5 \mathbin{\&} 7 + 5 \mathbin{\&} 1 + 2 \mathbin{\&} 7 + 2 \mathbin{\&} 1 = 5 + 1 + 2 + 0 = 8$$,为所有分割点中最大值。

题解:按位分解+前缀和

题目问题拆解

给定数组 $$a$$,选分割点 $$p$$ 最大化 $$W(p) = \sum_{b \in B} \sum_{c \in C} (b \mathbin{\&} c)$$。暴力枚举 $$p$$ 后双重循环求和为 $$O(n^2)$$,$$n$$ 可达 $$2 \times 10^5$$ 会超时,→ 因此采用按位分解将每个分割点的贡献拆成独立位处理。

算法实现

算法主策略:按位独立计算贡献,前缀计数维护每位上 1 的个数,每个分割点 $$O(27)$$ 算出 $$W(p)$$,取最大值。

按位分解:任何整数可写成二进制,以 $$6 = 4 + 2 = 110_2$$ 为例,从右往左第 0 位(权重 $$2^0=1$$)是 0,第 1 位(权重 $$2^1=2$$)是 1,第 2 位(权重 $$2^2=4$$)是 1。b & c 逐位判断,同一位都为 1 结果才为 1——各位之间互不影响,因此 $$W(p)$$ 可拆成 27 个位分别计算再相加。

单位贡献公式:对第 $$j$$ 位,一对 $$(b, c)$$ 仅当两者第 $$j$$ 位都为 1 时贡献 $$2^j$$。设 $$B$$ 中有 $$x$$ 个数第 $$j$$ 位为 1,$$C$$ 中有 $$y$$ 个,满足条件的配对数为 $$x \times y(x$$ 个男生和 $$y$$ 个女生握手共 $$x \times y$$ 次,同理),第 $$j$$ 位总贡献为 $$2^j \times x \times y$$。

前缀计数维护:思路与前缀和完全类似——前缀和递推 $$\text{sum}[i] = \text{sum}[i-1] + a[i-1]$$,每加入一个元素就累加;这里 $$\text{prefix}[j]$$ 记录 $$B$$ 中第 $$j$$ 位为 1 的元素个数,每把 $$a[p-1]$$ 并入 $$B$$ 就更新:

$$\text{prefix}[j] \mathrel{+}= (a[p-1] \gg j) \mathbin{\&} 1$$

其中 $$(a[p-1] \gg j) \mathbin{\&} 1$$ 取 $$a[p-1]$$ 第 $$j$$ 位的值(0 或 1)。预先扫一遍数组算出全局计数:

$$\text{total}[j] = \sum_{k=0}^{n-1} (a[k] \gg j) \mathbin{\&} 1$$

枚举分割点时,$$C$$ 中第 $$j$$ 位为 1 的个数为 $$\text{total}[j]-\text{prefix}[j]$$,代入单位贡献公式可得:

$$W(p) = \sum_{j=0}^{26} 2^j \times \text{prefix}[j] \times \bigl(\text{total}[j] - \text{prefix}[j]\bigr)$$

从左到右枚举所有 $$p$$,取 $$W(p)$$ 最大值即为答案。

时空复杂度分析

时间复杂度:$$O(27n)$$,每个元素处理 27 个位。

空间复杂度:$$O(1)$$(不计输入数组),仅需常数个长度 27 的数组。

C++

Java

Python

Go

第三题:平方串

在线评测链接:https://www.neituiya.com/oj/7/2462

题目描述

给定若干字符串 $$s$$。你可以只在字符串的头部与尾部添加子串(即 $$t =$$ 任意字符,使得得到的新串 $$t$$ 能被分成两个相同的子串(即 $$t = uu$$,称为"平方串")。

请计算每个字符串最少需要添加多少字符(允许左侧添加 $$m$$ 个、右侧添加 $$n$$ 个,总计 $$m + n$$ 个),才能使其变为平方串。允许不添加(答案为 $$0$$)。

平方串:形如 $$t = uu$$ 的字符串,其中 $$u$$ 为任意字符串,且 $$|u|$$ 必为正数。

添加规则:仅允许在首尾添加字符(两端合计计数),不允许在中间插入。

输入描述

每个测试文件包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 10^5)$$,表示数据组数。

接下来 $$T$$ 行,每行一个非空字符串 $$s(1 \le |s| \le 10^5)$$。

保证所有测试中字符串长度之和不超过 $$2 \times 10^5$$,且每个字符串 $$s$$ 仅由小写英文字母组成。

输出描述

输出 $$T$$ 行,每行一个整数,表示将该字符串变为平方串所需添加字符的最小数量。

样例1

输入

5
abab
abc
aaaea
abe
abca

输出

0
3
3
3
2

样例解释

"abab" 本身已是平方串 "ab"+"ab",答案为 $$0$$。

"abc" 最少需要添加 $$3$$ 个字符,在末尾补 "abc" 得到 "abcabc" = "abc"+"abc",答案为 $$3$$。

"abca" 在末尾添加 "bc" 得到 "abcabc" = "abc"+"abc",共添加 $$2$$ 个字符,答案为 $$2$$。

题解:字符串哈希

题目问题拆解

给定字符串 $$s$$,求在首尾添加最少字符使其成为平方串 $$t = uu$$。设 $$|s| = L$$,目标串长度为 $$2k$$(因为 $$t$$ 必须至少容纳 $$s$$,所以 $$k \ge \lceil L/2 \rceil$$),需添加 $$2k - L$$ 个字符。暴力枚举每个 $$k$$ 后逐字符比较为 $$O(L^2)$$,$$L$$ 最大 $$10^5$$ 会超时,→ 因此用字符串哈希将每个 $$k$$ 的子串比较降到 $$O(1)$$。

算法实现

字符串哈希的思路:把一段字符串映射成一个大数作为"指纹"——如果两段字符串内容相同,指纹必然相同;指纹不同则内容必然不同。具体公式是把每个字符当成数字(用 ASCII 码),像多项式一样加权求和:

$$h[i] = s[0] \times B^{i-1} + s[1] \times B^{i-2} + \cdots + s[i-1] \times B^0$$

其中 $$B$$ 是一个选定的进制数(如 131)。这个 $$h[i]$$ 就是 $$s[0..i-1]$$ 的哈希值(前缀哈希),类似前缀和的预处理。

$$O(1)$$ 查任意子串哈希:和前缀和查区间和的思路完全一样。子串 $$s[l..r-1]$$ 的哈希值 = $$h[r]-h[l] \times B^{r-l}$$(取模防溢出)。预处理 $$O(L)$$,每次查询 $$O(1)$$。

判定 $$k$$ 合法:目标串 $$t$$ 长 $$2k$$,两半相同意味着 $$s$$ 中同时被两半覆盖的位置必须满足 $$s[j] = s[j+k](j = 0, \ldots, L-k-1$$),即 $$s$$ 的前 $$L-k$$ 个字符必须等于 $$s$$ 从位置 $$k$$ 开始的 $$L-k$$ 个字符——条件为 $$s[0..L-k-1] = s[k..L-1]$$。两端添加的字符不受约束,可以自由赋值凑成对称。

枚举策略:从 $$k = \lceil L/2 \rceil$$ 开始递增枚举,用 $$O(1)$$ 哈希比较 $$s[0..L-k-1]$$ 和 $$s[k..L-1]$$,第一个哈希相等的 $$k$$ 给出最小添加数 $$2k-L$$。若直到 $$k = L-1$$ 都不满足,$$k = L$$ 必然合法——此时两半完全不交叠于 $$s$$ 内部,添加的字符可以自由赋值,添加 $$L$$ 个字符。

时空复杂度分析

时间复杂度:$$O(L)$$,前缀哈希预处理 $$O(L)$$,枚举 $$k$$ 每次 $$O(1)$$ 共 $$O(L)$$。

空间复杂度:$$O(L)$$,前缀哈希数组和幂次数组。

C++

Java

Python

Go

2026-3-29-研发岗

第一题:巴巴博弈

在线评测链接:https://www.neituiya.com/oj/7/2422

第二题:质数合数

在线评测链接:https://www.neituiya.com/oj/7/2423

第三题:位运算权值

在线评测链接:https://www.neituiya.com/oj/7/2424

2026-3-26-研发岗

第一题:排列拼接

在线评测链接:https://www.neituiya.com/oj/7/2398

题目描述

给定两个长度为 $$n$$ 的排列 $$\{a_1,a_2,...,a_n\}$$ 与 $$\{b_1,b_2,...,b_n\}$$。你可以进行如下操作一次:

选择一个正整数 $$k$$,构造数组 $$c$$,将排列 $$a$$ 按原顺序在 $$c$$ 的末尾依次复制 $$k$$ 份,得到长度为 $$k \times n$$ 的数组 $$c$$。形式化地,对任意 $$1 \le j \le k$$ 与 $$1 \le i \le n$$,都有 $$c_{i+(j-1) \times n}=a_i$$。

你希望数组 $$c$$ 中存在至少一个子序列,其按顺序拼接后与排列 $$b$$ 完全相同。请计算满足该条件的最小 $$k$$。

排列:长度为 $$n$$ 的排列是由 $$1$$ 到 $$n$$ 这 $$n$$ 个整数按任意顺序组成的数组,其中每个整数恰好出现一次。

子序列:子序列为从原序列中删除任意个(可以为零,也可以为全部)元素后,保持相对顺序得到的新序列。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$ 表示排列长度。

第二行输入 $$n$$ 个整数 $$a_1,a_2,...,a_n(1 \le a_i \le n)$$ 表示排列 $$a$$。

第三行输入 $$n$$ 个整数 $$b_1,b_2,...,b_n(1 \le b_i \le n)$$ 表示排列 $$b$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示使得 $$b$$ 能作为 $$c$$ 的一个子序列出现所需的最小 $$k$$。

样例1

输入

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

输出

2
4

题解

题目内容拆解

给定排列 $$a$$ 和 $$b$$,将 $$a$$ 重复 $$k$$ 次得到 $$c$$,求最小的 $$k$$ 使得 $$b$$ 是 $$c$$ 的子序列。$$n \le 2 \times 10^5$$,多组数据总 $$n$$ 不超过 $$2 \times 10^5$$,需要 $$O(n)$$ 单组。

核心观察:贪心匹配。在当前 $$a$$ 的一份拷贝中,从当前位置往后找 $$b$$ 的下一个元素。如果找不到(即目标元素在当前位置之前),就需要开启新的一份拷贝。

算法实现

算法主策略:本题采用贪心子序列匹配

预处理 $$pos[v]$$ 表示值 $$v$$ 在排列 $$a$$ 中的下标。然后遍历 $$b$$ 的每个元素,维护当前在 $$a$$ 中匹配到的位置 $$cur$$(初始为 $$-1$$)和已用的拷贝数 $$copies$$(初始为 $$1$$)。

对于 $$b[i]$$:如果 $$pos[b[i]] > cur$$,说明在当前拷贝中还能匹配,更新 $$cur = pos[b[i]]$$。否则需要新开一份拷贝,$$copies$$ 加 $$1$$,$$cur = pos[b[i]]$$。

以样例为例:$$a = [2,3,1,5,4]$$,$$pos = \{2:0, 3:1, 1:2, 5:3, 4:4\}$$,$$b = [3,5,4,2,1]$$。匹配过程:$$3$$ 在位置 $$1$$,$$5$$ 在位置 $$3$$,$$4$$ 在位置 $$4$$,均在当前拷贝内递增。到 $$2$$ 时 $$pos[2]=0 < 4$$,需要新拷贝。$$1$$ 在位置 $$2 > 0$$,在第二份拷贝内匹配。答案为 $$2$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$ 单组,预处理 $$pos$$ 数组 $$O(n)$$,匹配 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储 $$pos$$ 数组。

Go

// 排列拼接 - 贪心子序列匹配
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(n int, a, b []int) int {
        // pos[v] 记录值v在排列a中的下标,用于O(1)定位
        pos := make([]int, n+1)
        for i := 0; i < n; i++ {
                pos[a[i]] = i
        }
        copies := 1 // 当前使用的拷贝数
        cur := -1   // 当前拷贝中已匹配到的位置
        for _, x := range b {
                if pos[x] > cur {
                        cur = pos[x] // 在当前拷贝中继续匹配
                } else {
                        // 必须开启新的拷贝
                        copies++
                        cur = pos[x]
                }
        }
        return copies
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                fmt.Fscan(reader, &n)
                a := make([]int, n)
                b := make([]int, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &a[i])
                }
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &b[i])
                }
                fmt.Fprintln(writer, solve(n, a, b))
        }
}

第二题:该回家了

在线评测链接:https://www.neituiya.com/oj/7/2399

题目描述

给定一张由 $$n$$ 行、$$m$$ 列组成的地图。用字符 '0' 表示AK机别墅位置,用字符 '1' 表示其他位置。对于地图上的每一个位置,定义其到最近别墅的距离为:从该位置出发,每次可以向上、下、左、右四个方向走一步(曼哈顿距离的一步),到达任意一个 '0' 所需要的最少步数。若该位置本身就是 '0',则距离为 $$0$$。

输入描述

在一行上输入两个整数 $$n, m(1 \le n, m \le 10^3)$$,表示地图的行数与列数。

此后 $$n$$ 行,每行输入一个长度为 $$m$$ 的字符串 $$s_i$$,仅由字符 '0''1' 组成。保证至少存在一个位置为 '0'

输出描述

输出 $$n$$ 行。第 $$i$$ 行输出 $$m$$ 个整数,分别表示第 $$i$$ 行每个位置到最近 '0' 的最短步数。相邻整数之间使用一个空格分隔。

样例1

输入

3 4
0111
0011
1111

输出

0 1 2 3
0 0 1 2
1 1 2 3

样例解释

对于样例中第 $$1$$ 行第 $$1$$ 列位置是 '0',因此距离为 $$0$$;第 $$1$$ 行第 $$4$$ 列到最近 '0' 的最短路径长度为 $$3$$(一种方法是向左走三步)。

题解

本题涉及到BFS算法,不熟悉该算法的同学可以先做一下模板题:

离开中山路

马的遍历

题目内容拆解

求网格中每个位置到最近 '0' 的最短距离。$$n, m \le 10^3$$,网格大小最多 $$10^6$$,BFS 可以在 $$O(n \times m)$$ 内完成。

核心观察:这是经典的多源最短路问题,将所有 '0' 作为起点同时入队做 BFS 即可。

算法实现

算法主策略:本题采用多源BFS

将所有 '0' 的位置初始化距离为 $$0$$ 并全部加入队列,然后进行标准 BFS 扩展。每次取出队首 $$(x, y)$$,向四个方向扩展:若邻居 $$(nx, ny)$$ 尚未被访问(距离为 $$-1$$),则将其距离设为 $$dist[x][y] + 1$$ 并加入队列。

以样例为例:初始时 $$(0,0), (1,0), (1,1)$$ 三个 '0' 入队,距离为 $$0$$。第一轮扩展到 $$(0,1), (2,0), (2,1)$$,距离为 $$1$$。继续扩展直到所有位置都被访问。

时空复杂度分析

  • 时间复杂度:$$O(n \times m)$$,每个格子入队出队各一次。
  • 空间复杂度:$$O(n \times m)$$,存储距离数组和队列。

Go

// 该回家了 - 多源BFS
package main

import (
        "bufio"
        "fmt"
        "os"
        "strings"
)

func solve(n, m int, grid []string) [][]int {
        // dist[i][j] 表示到最近'0'的最短步数,-1表示未访问
        dist := make([][]int, n)
        for i := range dist {
                dist[i] = make([]int, m)
                for j := range dist[i] {
                        dist[i][j] = -1
                }
        }
        type Point struct{ x, y int }
        q := []Point{}
        // 所有'0'位置作为BFS的多个起点
        for i := 0; i < n; i++ {
                for j := 0; j < m; j++ {
                        if grid[i][j] == '0' {
                                dist[i][j] = 0
                                q = append(q, Point{i, j})
                        }
                }
        }
        dx := []int{-1, 1, 0, 0}
        dy := []int{0, 0, -1, 1}
        // BFS逐层扩展,保证第一次访问即最短距离
        for len(q) > 0 {
                cur := q[0]
                q = q[1:]
                for d := 0; d < 4; d++ {
                        nx, ny := cur.x+dx[d], cur.y+dy[d]
                        if nx >= 0 && nx < n && ny >= 0 && ny < m && dist[nx][ny] == -1 {
                                dist[nx][ny] = dist[cur.x][cur.y] + 1
                                q = append(q, Point{nx, ny})
                        }
                }
        }
        return dist
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var n, m int
        fmt.Fscan(reader, &n, &m)
        grid := make([]string, n)
        for i := 0; i < n; i++ {
                fmt.Fscan(reader, &grid[i])
        }
        dist := solve(n, m, grid)
        for i := 0; i < n; i++ {
                parts := make([]string, m)
                for j := 0; j < m; j++ {
                        parts[j] = fmt.Sprint(dist[i][j])
                }
                fmt.Fprintln(writer, strings.Join(parts, " "))
        }
}

第三题:破译者

在线评测链接:https://www.neituiya.com/oj/7/2400

题目描述

对于给定的三个整数 $$a$$、$$b$$、$$c$$,你需要找到一个整数 $$k$$,使其在满足 $$b+c \times k \ge 0$$ 的前提下,能够最小化 $$a \oplus (b+c \times k)$$ 的值。

请你输出这个最小的异或结果。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

在一行上输入三个整数 $$a, b, c(0 \le a, b, c \le 10^{18})$$。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示最小的异或结果。

样例1

输入

3
10 10 3
21 13 5
5 10 0

输出

0
2
15

样例解释

第一组:$$k=0$$ 时 $$b+c \times k = 10$$,$$10 \oplus 10 = 0$$,已是最小值。

第二组:$$k=2$$ 时 $$b+c \times k = 23$$,$$21 \oplus 23 = 2$$。

第三组:$$c=0$$,$$b+c \times k$$ 恒为 $$10$$,$$5 \oplus 10 = 15$$。

题解

题目内容拆解

给定 $$a, b, c$$,找 $$x = b + c \times k \ge 0$$ 使 $$a \oplus x$$ 最小。$$a, b, c \le 10^{18}$$,$$T \le 10^4$$,需要单组 $$O(\log V)$$ 的算法。

核心观察:当 $$c = 0$$ 时 $$x$$ 固定为 $$b$$,答案就是 $$a \oplus b$$。当 $$c > 0$$ 时,合法的 $$x$$ 恰好是所有满足 $$x \equiv b \pmod{c}$$ 且 $$x \ge 0$$ 的非负整数。问题转化为:在满足同余约束的非负整数中,找到与 $$a$$ 异或值最小的那个。

算法实现

算法主策略:本题采用从高位到低位逐位贪心构造

从最高位(第 $$59$$ 位)到最低位(第 $$0$$ 位),逐位确定目标 $$x$$ 的每一位。每一步维护已确定的高位前缀 $$prefix$$,尝试让 $$x$$ 的当前位与 $$a$$ 的当前位相同(使异或该位为 $$0$$)。

设当前处理第 $$bit$$ 位,已确定前缀为 $$prefix$$。如果当前位选定后,$$x$$ 的范围为 $$[prefix, prefix + 2^{bit} - 1]$$。需要检查这个范围内是否存在 $$x \equiv b \pmod{c}$$ 的整数。最小的满足条件的 $$x$$ 为 $$prefix + ((b - prefix) \bmod c + c) \bmod c$$,只要它不超过 $$prefix + 2^{bit} - 1$$ 即可。

如果优先位可行,选择该位;否则被迫选另一位(异或该位为 $$1$$)。

以样例第二组为例:$$a = 21(10101_2)$$,$$b = 13$$,$$c = 5$$,合法 $$x$$ 需满足 $$x \equiv 3 \pmod{5}$$。逐位构造:第 $$4$$ 位选 $$1$$($$prefix=16$$,范围 $$[16,31]$$ 内有 $$18 \equiv 3$$),第 $$3$$ 位选 $$0$$($$prefix=16$$,范围 $$[16,23]$$ 内有 $$18$$),第 $$2$$ 位选 $$1$$($$prefix=20$$,范围 $$[20,23]$$ 内有 $$23$$),第 $$1$$ 位选 $$1$$($$prefix=22$$,范围 $$[22,23]$$ 内有 $$23$$),第 $$0$$ 位选 $$1$$($$x=23$$)。最终 $$21 \oplus 23 = 2$$。

时空复杂度分析

  • 时间复杂度:$$O(60 \times T)$$,每组数据从高到低处理 $$60$$ 位,每位 $$O(1)$$ 判断。
  • 空间复杂度:$$O(1)$$,只需常数空间。

Go

// 破译者 - 逐位贪心构造
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(a, b, c int64) int64 {
        if c == 0 {
                return a ^ b // c=0时x只能是b
        }
        // 合法x满足 x ≡ r (mod c)
        r := b % c
        var prefix int64 // 已确定的高位前缀
        for bit := 59; bit >= 0; bit-- {
                mask := int64(1)<<bit - 1 // 当前位以下所有位为1的掩码
                aBit := int64((a >> bit) & 1)
                // 优先让x的当前位与a相同,使异或该位为0
                newPrefix := prefix | (aBit << bit)
                lo := newPrefix
                hi := newPrefix + mask
                // 在[lo, hi]范围内找最小的 x ≡ r (mod c)
                first := lo + ((r-lo%c)%c + c) % c
                if first <= hi {
                        prefix = newPrefix // 存在合法x,选择优先位
                } else {
                        // 被迫选另一位
                        prefix = prefix | ((1 - aBit) << bit)
                }
        }
        return a ^ prefix
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var a, b, c int64
                fmt.Fscan(reader, &a, &b, &c)
                fmt.Fprintln(writer, solve(a, b, c))
        }
}

2026-3-19-研发岗

第一题:怎么全是3

在线测评链接:https://www.neituiya.com/oj/3/2354

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \cdots, a_n\}$$。你可以进行若干次操作,每次操作从下列两种中任选其一:

  1. 选择一个元素将其删除,剩余元素按照原顺序拼接。
  2. 选择一个元素将其增加 $$1$$ 或者减少 $$1$$。

要求:每次操作完成后,数组所有元素的和必须是 $$2$$ 的倍数(即为偶数)。请你输出最多可以进行多少次操作。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 表示数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n(1 \le a_i \le 10^9)$$ 表示数组 $$a$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示最多可以进行的操作次数。

样例1

输入

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

输出

4
2

题解:模拟

题目内容拆解

每次操作后数组总和必须为偶数。删除一个元素改变的奇偶性取决于被删元素的奇偶性,$$\pm 1$$ 操作一定改变总和的奇偶性。需要在约束下最大化操作次数,$$n \le 2 \times 10^5$$,需要 $$O(n)$$ 解法。

算法实现

算法主策略:本题采用奇偶性分类讨论

关键观察:删除偶数元素不改变总和的奇偶性,删除奇数元素会改变。$$\pm 1$$ 操作一定改变奇偶性。因此分两种情况讨论。

情况一:总和 $$sum$$ 为偶数。 此时只能执行不改变奇偶性的操作,即只能删除偶数元素。每删一个偶数元素,总和仍为偶数,可以继续操作。所以答案就是偶数元素的个数 $$even\_cnt$$。

情况二:总和 $$sum$$ 为奇数。 此时可以先执行一次 $$\pm 1$$ 操作(消耗 $$1$$ 次操作,总和变为偶数),这个 $$\pm 1$$ 作用于一个奇数元素会把它变为偶数。变化后总和为偶数,多了一个偶数元素可以删。所以答案为 $$even\_cnt + 2$$:$$1$$ 次 $$\pm 1$$ 操作 $$+$$ 删除原来的 $$even\_cnt$$ 个偶数元素 $$+$$ 删除刚才变成偶数的那 $$1$$ 个元素,合计 $$even\_cnt + 2$$。

以样例1第一组 $$[1,2,3,4,5]$$ 为例:$$sum = 15$$ 为奇数,$$even\_cnt = 2$$(元素 $$2, 4$$),答案 $$= 2 + 2 = 4$$。第二组 $$[2,2,1,1]$$:$$sum = 6$$ 为偶数,$$even\_cnt = 2$$,答案 $$= 2$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历一次数组统计偶数个数和总和奇偶性。
  • 空间复杂度:$$O(1)$$,只需常数变量。

C++

Java

Python

# 怎么全是3 - 奇偶分析

def solve():
    n = int(input())
    a = list(map(int, input().split()))
    s = sum(a)
    even_cnt = sum(1 for x in a if x % 2 == 0)
    # sum偶:只能删偶数元素;sum奇:先±1把一个奇变偶,再删所有偶
    if s % 2 == 0:
        print(even_cnt)
    else:
        print(even_cnt + 2)

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

Go

第二题:没有三角形

在线测评链接:https://www.neituiya.com/oj/3/2355

题目描述

给定一个正整数 $$n(3 \le n \le 2 \times 10^5)$$,表示需要构造的数组长度。构造一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \cdots, a_n\}$$,使其满足以下条件:

$$1 \le a_i \le 10^9$$。

  1. 任意选择三个不同下标 $$i, j, k$$,设相应的数组元素按非降序排列为 $$x \le y \le z$$,则有 $$x + y \le z$$,即无法组成三角形。

输入描述

在一行上输入一个整数 $$n(3 \le n \le 2 \times 10^5)$$,表示数组长度。

输出描述

若存在满足条件的数组,则输出一行 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n$$;否则,输出 $$-1$$。

如果存在多个解决方案,您可以输出任意一个,系统会自动判定是否正确。注意,自测运行功能可能因此返回错误结果,请自行检查答案正确性。

样例1

输入

3

输出

1 2 4

题解:Fibonacci构造

题目内容拆解

构造长度 $$n$$ 的数组,使得排序后任意三个元素 $$x \le y \le z$$ 满足 $$x + y \le z$$(无法组成三角形),值域 $$[1, 10^9]$$。

核心观察:排序后只需保证相邻三元组 $$a[i] + a[i+1] \le a[i+2]$$ 即可,因为更远的三元组不等式自动更强。问题转化为:构造增长最慢的满足 $$a[i] \ge a[i-1] + a[i-2]$$ 的序列,使尽可能多的项不超过 $$10^9$$。

算法实现

采用动态规划

状态方程定义

$$f[i]$$ 表示满足条件的数组第 $$i$$ 项的最小可能值。$$f[i]$$ 越小,后续项增长越慢,能容纳更多元素。

状态方程初始化

$$f[1] = 1$$,$$f[2] = 1$$(取最小正整数,为后续留最大空间)。

状态方程转移

$$f[i] = f[i-1] + f[i-2]$$

这正是 Fibonacci 数列。每一项取到下界 $$f[i-1] + f[i-2]$$,保证增长最慢。

无解判定:Fibonacci 以约 $$1.618^n$$ 指数增长,$$f_{45} = 1134903170 > 10^9$$。当 $$n \ge 45$$ 时无法构造,输出 $$-1$$。

以样例 $$n = 3$$ 为例:$$f[1] = 1, f[2] = 1, f[3] = f[2] + f[1] = 2$$,输出 $$1\ 1\ 2$$,满足 $$1 + 1 \le 2$$。题目给出的 $$1\ 2\ 4$$ 也是合法解(SPJ 判定)。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,线性递推。
  • 空间复杂度:$$O(n)$$,存储结果数组。

C++

Java

Python

# 没有三角形 - 线性DP

def solve():
    n = int(input())
    # Fibonacci满足 f[i]+f[i+1]=f[i+2],任意三元组 x+y<=z
    # f(45) > 10^9,n>=45 无解
    if n >= 45:
        print(-1)
        return
    fib = [0] * (n + 1)
    fib[1] = 1
    if n >= 2:
        fib[2] = 1
    for i in range(3, n + 1):
        fib[i] = fib[i - 1] + fib[i - 2]
    print(' '.join(str(fib[i]) for i in range(1, n + 1)))

solve()

Go

第三题:分值转移

在线测评链接:https://www.neituiya.com/oj/3/2356

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \cdots, a_n\}$$。你可以进行如下操作至多一次(也可以不进行任何操作):选择一个下标 $$1 \le i < n$$ 和任意整数 $$x$$,将区间 $$a_1, a_2, \cdots, a_i$$ 中的每个元素减少 $$x$$,同时将区间 $$a_{i+1}, a_{i+2}, \cdots, a_n$$ 中的每个元素增加 $$x$$。

请你最小化操作结束后数组中的逆序对个数,并输出这个最小值。

逆序对:在序列中,若存在两个下标 $$p, q$$ 满足 $$p < q$$ 且 $$a_p > a_q$$,则称 $$(p, q)$$ 为一个逆序对。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 表示数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$ 表示数组长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n(1 \le a_i \le n)$$ 表示数组 $$a$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示操作后逆序对个数的最小值。

样例1

输入

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

输出

1
2

样例解释

对于第一组数据 $$[2, 1, 5, 3, 4]$$:选择 $$i = 2$$,对前两个元素减 $$x$$、后三个元素加 $$x$$(取合适的 $$x$$),操作不改变左半内部和右半内部的相对顺序,只影响跨越分割点的逆序对。原始总逆序对为 $$3$$($$(1,2), (3,4), (3,5)$$),其中跨越 $$i=2$$ 的逆序对有 $$(1,4), (1,5)$$ 共 $$2$$ 个($$2 > 3$$ 不成立,$$2 > 4$$ 不成立,实际跨越逆序对为 $$0$$)。选 $$i = 1$$ 时跨越逆序对为 $$1$$($$(1,2)$$:$$2 > 1$$),操作后这 $$1$$ 个跨越逆序对被消除,剩余内部逆序对 $$(3,4), (3,5)$$ 共 $$2$$ 个。进一步验证可得最优答案为 $$1$$。

题解:树状数组

题目内容拆解

操作将数组在位置 $$i$$ 处分割,左半全体减 $$x$$、右半全体加 $$x$$。由于同侧元素加减相同的值,同侧内部的相对大小不变,因此同侧内部的逆序对数量不受影响。操作只影响跨越分割点的逆序对:当 $$x$$ 足够大时,左半所有元素都小于右半所有元素,跨越逆序对全部消除。所以操作后的逆序对数 $$=$$ 总逆序对数 $$-$$ 跨越位置 $$i$$ 的逆序对数 $$cross(i)$$。目标是找到使 $$cross(i)$$ 最大的分割点 $$i$$。

算法实现

算法主策略:本题采用树状数组 + 增量计算

第一步:计算总逆序对。 用树状数组从右到左扫描,对每个 $$a_i$$,查询已插入的比 $$a_i$$ 小的元素个数(即右边比 $$a_i$$ 小的个数),累加得到 $$total\_inv$$。

第二步:计算 $$right\_less[i]$$ 同样从右到左扫描,$$right\_less[i]$$ 表示 $$a_i$$ 右边比 $$a_i$$ 小的元素个数。这在第一步中可以顺便记录。

第三步:增量计算 $$cross(i)$$ $$cross(i)$$ 是跨越位置 $$i$$ 的逆序对数,即满足 $$p \le i < q$$ 且 $$a_p > a_q$$ 的 $$(p, q)$$ 对数。从 $$cross(i-1)$$ 递推到 $$cross(i)$$:把元素 $$a_i$$ 从右半移入左半时,减去 $$a_i$$ 在左半中比它大的个数($$left\_greater_i$$,这些原来是跨越逆序对,现在变成左半内部逆序对),加上 $$right\_less[i]$$($$a_i$$ 右边比它小的,原来是右半内部逆序对,现在变成跨越逆序对)。即 $$cross(i) = cross(i-1) - left\_greater_i + right\_less[i]$$。$$left\_greater_i$$ 用第三个树状数组从左到右维护:已插入 $$i$$ 个元素,$$\le a_i$$ 的有 $$query(a_i)$$ 个,则 $$> a_i$$ 的有 $$i - query(a_i)$$ 个。

以样例2 $$[4, 3, 2, 1]$$ 为例手算。$$total\_inv = 6$$(所有 $$\binom{4}{2} = 6$$ 对都是逆序)。$$right\_less = [3, 2, 1, 0]$$。增量计算:$$cross(0) = 0$$。$$i = 0$$:$$left\_greater = 0$$,$$cross(1) = 0 - 0 + 3 = 3$$。$$i = 1$$:$$left\_greater = 1$$(左边 $$4 > 3$$),$$cross(2) = 3 - 1 + 2 = 4$$。$$i = 2$$:$$left\_greater = 2$$(左边 $$4, 3 > 2$$),$$cross(3) = 4 - 2 + 1 = 3$$。$$max\_cross = 4$$,答案 $$= 6 - 4 = 2$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,三次树状数组扫描,每次单点更新和前缀查询均为 $$O(\log n)$$。
  • 空间复杂度:$$O(n)$$,存储树状数组和辅助数组。

C++

Java

Python

# 分值转移 - 树状数组 + 增量计算

def solve():
    n = int(input())
    a = list(map(int, input().split()))

    if n <= 1:
        print(0)
        return

    # BIT操作
    def make_bit():
        return [0] * (n + 2)

    def update(tree, i):
        while i <= n:
            tree[i] += 1
            i += i & (-i)

    def query(tree, i):
        s = 0
        while i > 0:
            s += tree[i]
            i -= i & (-i)
        return s

    # 1. 总逆序对:从右到左,统计右边比当前小的
    total_inv = 0
    bit1 = make_bit()
    for i in range(n - 1, -1, -1):
        total_inv += query(bit1, a[i] - 1)
        update(bit1, a[i])

    # 2. right_less[i]:a[i]右边比a[i]小的元素个数
    right_less = [0] * n
    bit2 = make_bit()
    for i in range(n - 1, -1, -1):
        right_less[i] = query(bit2, a[i] - 1)
        update(bit2, a[i])

    # 3. 增量计算 cross(i) = cross(i-1) - left_greater(i) + right_less[i]
    # left_greater(i) = 左边已添加的比a[i]大的个数
    bit3 = make_bit()
    max_cross = 0
    cross = 0
    for i in range(n - 1):
        left_greater = i - query(bit3, a[i])  # i个已添加,<=a[i]的有query个
        cross = cross - left_greater + right_less[i]
        if cross > max_cross:
            max_cross = cross
        update(bit3, a[i])

    print(total_inv - max_cross)

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

Go

2026-3-19-算法岗

第一题:全相等

在线测评链接:https://www.neituiya.com/oj/3/2357

题目描述

给定一个长度为 $$n$$、仅由小写字母组成的字符串 $$s$$(下标从 $$1$$ 开始)。定义一个非空子串 $$t$$ 是好子串,当且仅当在 $$t$$ 中,所有出现过的字符的出现次数两两相等。请你计算字符串 $$s$$ 中共有多少个好子串。

名词解释:子串为从原字符串中,连续地选择一段字符(可以全选、可以不选)得到的新字符串。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 100)$$,代表数据组数。每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^3)$$,表示字符串长度。

第二行输入一个长度为 $$n$$、仅由小写字母组成的字符串 $$s$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2080$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示满足条件的非空子串的数量。

样例1

输入

2
3
aab
4
abab

输出

5
8

样例解释

在第一组测试数据中,$$s =$$ "aab",所有子串为(按区间列举):

$$[1,1] =$$ "a"(好);$$[1,2] =$$ "aa"(好);$$[1,3] =$$ "aab"(不好);$$[2,2] =$$ "a"(好);$$[2,3] =$$ "ab"(好);$$[3,3] =$$ "b"(好)。

共计 $$5$$ 个好子串。

题解:模拟

题目内容拆解

给定字符串,统计所有子串中满足"出现过的字符频率全部相同"的数量。$$n \le 2000$$ 且 $$\sum n \le 2080$$,允许 $$O(n^2 \times 26)$$ 的暴力枚举。

算法实现

算法主策略:本题采用枚举子串 + 频率校验

枚举左端点 $$l$$,从 $$l$$ 向右逐步扩展右端点 $$r$$,维护一个长度为 $$26$$ 的字符频率数组 $$freq$$。每次 $$r$$ 右移一位时,将 $$s[r]$$ 对应的频率加 $$1$$,然后遍历 $$freq$$ 数组,收集所有大于 $$0$$ 的频率值。如果这些频率值全部相等,则子串 $$s[l..r]$$ 是好子串。

以样例 $$s =$$ "aab" 为例手动推导:

$$l = 0$$:$$r = 0$$ 时 $$freq =$$ {a:1},频率集合 $$\{1\}$$,好子串。$$r = 1$$ 时 $$freq =$$ {a:2},频率集合 $$\{2\}$$,好子串。$$r = 2$$ 时 $$freq =$$ {a:2, b:1},频率集合 $$\{2, 1\}$$,不是好子串。

$$l = 1$$:$$r = 1$$ 时 {a:1},好。$$r = 2$$ 时 {a:1, b:1},频率集合 $$\{1\}$$,好。

$$l = 2$$:$$r = 2$$ 时 {b:1},好。

总计 $$5$$ 个好子串,与答案一致。

时空复杂度分析

  • 时间复杂度:$$O(n^2 \times 26)$$,枚举所有 $$O(n^2)$$ 个子串,每个子串检查 $$26$$ 个字母的频率。
  • 空间复杂度:$$O(26)$$,频率数组。

C++

// 全相等 - 枚举子串
#include <bits/stdc++.h>
using namespace std;

// 枚举所有子串,检查出现过的字符频率是否全部相同
int solve(int n, string& s) {
    int ans = 0;
    for (int l = 0; l < n; l++) {
        int freq[26] = {};
        for (int r = l; r < n; r++) {
            freq[s[r] - 'a']++;
            // 收集所有出现过的字符频率
            int first = -1;
            bool good = true;
            for (int c = 0; c < 26; c++) {
                if (freq[c] > 0) {
                    if (first == -1) first = freq[c];
                    else if (freq[c] != first) { good = false; break; }
                }
            }
            if (good) ans++;
        }
    }
    return ans;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        string s;
        cin >> s;
        cout << solve(n, s) << "\n";
    }
    return 0;
}

Java

// 全相等 - 枚举子串
import java.io.*;

public class Main {
    // 枚举所有子串,检查出现过的字符频率是否全部相同
    static int solve(int n, String s) {
        int ans = 0;
        for (int l = 0; l < n; l++) {
            int[] freq = new int[26];
            for (int r = l; r < n; r++) {
                freq[s.charAt(r) - 'a']++;
                int first = -1;
                boolean good = true;
                for (int c = 0; c < 26; c++) {
                    if (freq[c] > 0) {
                        if (first == -1) first = freq[c];
                        else if (freq[c] != first) { good = false; break; }
                    }
                }
                if (good) ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            int n = Integer.parseInt(br.readLine().trim());
            String s = br.readLine().trim();
            sb.append(solve(n, s)).append('\n');
        }
        System.out.print(sb);
    }
}

Python

# 全相等 - 枚举子串

def solve():
    n = int(input())
    s = input()
    ans = 0
    for l in range(n):
        freq = [0] * 26
        for r in range(l, n):
            freq[ord(s[r]) - ord('a')] += 1
            # 所有出现过的字符频率相同 → 好子串
            vals = set(v for v in freq if v > 0)
            if len(vals) == 1:
                ans += 1
    print(ans)

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

Go

// 全相等 - 枚举子串
package main

import (
        "bufio"
        "fmt"
        "os"
)

// 枚举所有子串,检查出现过的字符频率是否全部相同
func solve(n int, s string) int {
        ans := 0
        for l := 0; l < n; l++ {
                var freq [26]int
                for r := l; r < n; r++ {
                        freq[s[r]-'a']++
                        first := -1
                        good := true
                        for c := 0; c < 26; c++ {
                                if freq[c] > 0 {
                                        if first == -1 {
                                                first = freq[c]
                                        } else if freq[c] != first {
                                                good = false
                                                break
                                        }
                                }
                        }
                        if good {
                                ans++
                        }
                }
        }
        return ans
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()
        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                fmt.Fscan(reader, &n)
                var s string
                fmt.Fscan(reader, &s)
                fmt.Fprintln(writer, solve(n, s))
        }
}

第二题:文本数值混合特征工程

在线测评链接:https://www.neituiya.com/oj/3/2358

题目描述

现有一个文本与数值的混合数据,需要你在仅使用 $$numpy/pandas/scikit\text{-}learn$$ 的前提下,实现下表所示四段式特征工程 + 双基模型平均流程,并输出测试集标签。

Word-level TF-IDF:$$TfidfVectorizer(lowercase=True, stop\_words=\text{"english"}, ngram\_range=(1,2), sublinear\_tf=True)$$

Char 3-gram TF-IDF:$$TfidfVectorizer(analyzer=\text{"char"}, ngram\_range=(3,3), lowercase=True, sublinear\_tf=True)$$

Numeric block:对输入数值特征做 $$StandardScaler$$,标准化后每列进行二次多项展开 $$PolynomialFeatures(degree=2, include\_bias=False)$$

Feature 合并:拼接三个特征矩阵,即 $$hstack([①, ②, ③])$$ 得稀疏矩阵

模型A:$$LogisticRegression(penalty=\text{"l2"}, C=1.0, solver=\text{"liblinear"}, max\_iter=1000)$$

模型B:$$SGDClassifier(loss=\text{"log\_loss"}, penalty=\text{"l2"}, alpha=1e\text{-}4, max\_iter=1000, random\_state=42)$$

软投票:将两个模型输出的结果平均权重投票,最终阈值大于 $$0.5$$ 则标签为 $$1$$,否则标签为 $$0$$。

输入描述

输入为一行 JSON 字符串,包含以下字段:train_txt(训练文本列表)、train_num(训练数值特征二维数组)、train_y(训练标签)、test_txt(测试文本列表)、test_num(测试数值特征二维数组)。

输出描述

仅一行:$$[pred\_1, pred\_2, \ldots, pred\_n]$$,长度等于测试集大小的 JSON 整数数组($$0/1$$)。

补充说明

  1. 所有随机源固定 $$random\_state=42$$。
  2. 为确保通过测试用例,仅允许使用 $$numpy/pandas/scikit\text{-}learn$$。

样例1

输入

{"train_txt": ["great food and service", "fantastic taste", "delicious pizza", "awesome restaurant", "bad food experience", "awful meal", "terrible service", "horrible taste"],"train_num": [[15.0, 4.8], [12.0, 4.6], [13.5, 4.7], [14.0, 4.9],[9.0, 2.1], [8.5, 2.0], [8.0, 2.2], [9.5, 2.3]],"train_y":[0,0,0,0,1,1,1,1],"test_txt": ["excellent pizza", "terrible meal"],"test_num": [[14.0, 4.7], [8.8, 2.0]]}

输出

[0, 1]

题解

题目内容拆解

按照题目给定的特征工程 pipeline 和双模型融合流程,严格实现每一步即可。本题是 sklearn 工具链的直接应用,没有算法设计空间,关键在于参数和流程完全匹配题意。

算法实现

算法主策略:本题采用 sklearn pipeline 按步实现

整个流程分为特征提取、特征合并、模型训练、软投票四步:

  1. 分别对文本数据做 Word-level TF-IDF(word unigram + bigram)和 Char 3-gram TF-IDF,对数值特征做标准化后二次多项展开。
  2. 用 $$hstack$$ 将三个特征矩阵水平拼接为一个稀疏矩阵。

3) 分别用 LogisticRegression 和 SGDClassifier 拟合训练数据。

4) 对测试集取两个模型的正类概率平均值,大于 $$0.5$$ 判为 $$1$$,否则判为 $$0$$。

时空复杂度分析

  • 时间复杂度:$$O(N \times D)$$,$$N$$ 为训练样本数,$$D$$ 为合并后的特征维度(TF-IDF 词表大小 + 多项式展开维度)。
  • 空间复杂度:$$O(N \times D)$$,存储稀疏特征矩阵。

Python

# 特征工程 - sklearn pipeline
import json
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.linear_model import LogisticRegression, SGDClassifier
from scipy.sparse import hstack

data = json.loads(input())
train_txt = data["train_txt"]
train_num = np.array(data["train_num"])
train_y = np.array(data["train_y"])
test_txt = data["test_txt"]
test_num = np.array(data["test_num"])

# ① Word-level TF-IDF
word_vec = TfidfVectorizer(lowercase=True, stop_words="english",
                            ngram_range=(1,2), sublinear_tf=True)
tr_word = word_vec.fit_transform(train_txt)
te_word = word_vec.transform(test_txt)

# ② Char 3-gram TF-IDF
char_vec = TfidfVectorizer(analyzer="char", ngram_range=(3,3),
                            lowercase=True, sublinear_tf=True)
tr_char = char_vec.fit_transform(train_txt)
te_char = char_vec.transform(test_txt)

# ③ Numeric: StandardScaler + PolynomialFeatures
scaler = StandardScaler()
poly = PolynomialFeatures(degree=2, include_bias=False)
tr_num = poly.fit_transform(scaler.fit_transform(train_num))
te_num = poly.transform(scaler.transform(test_num))

# ④ 合并
X_tr = hstack([tr_word, tr_char, tr_num])
X_te = hstack([te_word, te_char, te_num])

# ⑤ LogisticRegression
lr = LogisticRegression(penalty="l2", C=1.0, solver="liblinear",
                         max_iter=1000, random_state=42)
lr.fit(X_tr, train_y)

# ⑥ SGDClassifier
sgd = SGDClassifier(loss="log_loss", penalty="l2", alpha=1e-4,
                     max_iter=1000, random_state=42)
sgd.fit(X_tr, train_y)

# ⑦ 软投票
prob = (lr.predict_proba(X_te)[:,1] + sgd.predict_proba(X_te)[:,1]) / 2
preds = (prob > 0.5).astype(int).tolist()
print(json.dumps(preds))

第三题:四元异或

在线测评链接:https://www.neituiya.com/oj/3/2359

题目描述

给定一个长度为 $$n$$ 的整数数组 $$a_1, a_2, \ldots, a_n$$ 与整数 $$k$$,请判断是否存在四个两两不同的下标 $$i, j, p, q$$(即 $$i, j, p, q$$ 互不相同),使得 $$a_i \oplus a_j \oplus a_p \oplus a_q = k$$。若存在输出 $$Yes$$,否则输出 $$No$$。

名词解释:按位异或($$xor$$,记作 $$\oplus$$):对每一位二进制独立计算,相同为 $$0$$,不同为 $$1$$。两两不同:四个下标互不相同,不可复用同一位置的元素。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 10^3)$$,代表数据组数。每组测试数据描述如下:

第一行输入两个整数 $$n, k(4 \le n \le 10^3, 0 \le k \le 10^9)$$。

第二行输入 $$n$$ 个整数,依次为 $$a_1, a_2, \ldots, a_n(0 \le a_i \le 10^9)$$。

所有测试中 $$\sum n \le 4 \times 10^3$$。

输出描述

输出 $$T$$ 行,若存在满足条件的四元组,输出 $$Yes$$;否则输出 $$No$$。

样例1

输入

3
4 4
1 2 3 4
5 0
1 2 4 8 16
4 7
1 2 3 7

输出

Yes
No
Yes

样例解释

样例 $$1$$:取 $$1, 2, 3, 4$$,有 $$1 \oplus 2 \oplus 3 \oplus 4 = 4$$。

样例 $$2$$:不存在四个两两不同的数使其异或为 $$0$$。

样例 $$3$$:取 $$1, 2, 3, 7$$,有 $$1 \oplus 2 \oplus 3 \oplus 7 = 7$$。

题解

题目内容拆解

判断数组中是否存在四个不同下标元素的异或等于 $$k$$。$$\sum n \le 4000$$,直接四重循环 $$O(n^4)$$ 超时,需要利用异或的配对性质将问题拆成两组 pair 查找。

算法实现

算法主策略:本题采用哈希枚举(Meet in the Middle 思想)。

核心观察:$$a_i \oplus a_j \oplus a_p \oplus a_q = k$$ 等价于 $$(a_i \oplus a_j) = k \oplus (a_p \oplus a_q)$$。因此可以将四元组拆成两个二元组,先预处理所有 pair 的异或值,再查找互补 pair。

具体步骤:

  1. 枚举所有下标对 $$(i, j)$$($$i < j$$),计算 $$v = a_i \oplus a_j$$,将 $$(i, j)$$ 存入哈希表 $$mp[v]$$。
  2. 再次枚举所有下标对 $$(i, j)$$($$i < j$$),计算 $$target = k \oplus a_i \oplus a_j$$,在哈希表中查找 $$mp[target]$$。对于找到的每对 $$(p, q)$$,检查四个下标是否互不相同,若满足则答案为 $$Yes$$。

以样例 $$1$$ 为例:$$n = 4, k = 4, a = [1, 2, 3, 4]$$。预处理所有 pair 异或:$$(0,1) \to 3$$、$$(0,2) \to 2$$、$$(0,3) \to 5$$、$$(1,2) \to 1$$、$$(1,3) \to 6$$、$$(2,3) \to 7$$。枚举 $$(0,1)$$ 时 $$target = 4 \oplus 1 \oplus 2 = 7$$,查表找到 $$(2,3)$$,四个下标 $$0, 1, 2, 3$$ 互不相同,输出 $$Yes$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$ 建表 + $$O(n^2 \times L)$$ 查询,其中 $$L$$ 是哈希表中同一 key 下的 pair 数量。最坏情况可达 $$O(n^4)$$,但实际随机数据下 $$L$$ 很小。
  • 空间复杂度:$$O(n^2)$$,哈希表存储所有 pair。

C++

// 四元异或 - 哈希枚举
#include <bits/stdc++.h>
using namespace std;

// 枚举所有对XOR存哈希表,再枚举查找互补对(4下标互不相同)
bool solve() {
    int n, k;
    cin >> n >> k;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    // 存储 xor_val → 对列表
    unordered_map<int, vector<pair<int,int>>> mp;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            mp[a[i] ^ a[j]].push_back({i, j});
        }
    }

    // 枚举每对,查找互补
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            int target = k ^ a[i] ^ a[j];
            auto it = mp.find(target);
            if (it == mp.end()) continue;
            for (auto& [p, q] : it->second) {
                if (p != i && p != j && q != i && q != j) return true;
            }
        }
    }
    return false;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        cout << (solve() ? "Yes" : "No") << "\n";
    }
    return 0;
}

Java

// 四元异或 - 哈希枚举
import java.io.*;
import java.util.*;

public class Main {
    // 枚举所有对XOR存哈希表,查找互补对
    static String solve(int n, int k, int[] a) {
        Map<Integer, List<int[]>> mp = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int v = a[i] ^ a[j];
                mp.computeIfAbsent(v, x -> new ArrayList<>()).add(new int[]{i, j});
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int target = k ^ a[i] ^ a[j];
                List<int[]> pairs = mp.get(target);
                if (pairs == null) continue;
                for (int[] pq : pairs) {
                    if (pq[0] != i && pq[0] != j && pq[1] != i && pq[1] != j)
                        return "Yes";
                }
            }
        }
        return "No";
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int n = Integer.parseInt(st.nextToken());
            int k = Integer.parseInt(st.nextToken());
            st = new StringTokenizer(br.readLine());
            int[] a = new int[n];
            for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
            sb.append(solve(n, k, a)).append('\n');
        }
        System.out.print(sb);
    }
}

Python

# 四元异或 - 哈希枚举

def solve():
    n, k = map(int, input().split())
    a = list(map(int, input().split()))

    # 枚举所有对的XOR,存入字典
    pair_map = {}
    for i in range(n):
        for j in range(i + 1, n):
            v = a[i] ^ a[j]
            if v not in pair_map:
                pair_map[v] = []
            pair_map[v].append((i, j))

    # 枚举每对,查找互补对(4下标互不相同)
    for i in range(n):
        for j in range(i + 1, n):
            target = k ^ a[i] ^ a[j]
            if target not in pair_map:
                continue
            for p, q in pair_map[target]:
                if p != i and p != j and q != i and q != j:
                    print("Yes")
                    return
    print("No")

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

Go

// 四元异或 - 哈希枚举
package main

import (
        "bufio"
        "fmt"
        "os"
)

type pair struct{ i, j int }

// 枚举所有对XOR存哈希表,查找互补对
func solve(reader *bufio.Reader) string {
        var n, k int
        fmt.Fscan(reader, &n, &k)
        a := make([]int, n)
        for i := 0; i < n; i++ {
                fmt.Fscan(reader, &a[i])
        }

        mp := make(map[int][]pair)
        for i := 0; i < n; i++ {
                for j := i + 1; j < n; j++ {
                        v := a[i] ^ a[j]
                        mp[v] = append(mp[v], pair{i, j})
                }
        }

        for i := 0; i < n; i++ {
                for j := i + 1; j < n; j++ {
                        target := k ^ a[i] ^ a[j]
                        pairs, ok := mp[target]
                        if !ok {
                                continue
                        }
                        for _, pq := range pairs {
                                if pq.i != i && pq.i != j && pq.j != i && pq.j != j {
                                        return "Yes"
                                }
                        }
                }
        }
        return "No"
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()
        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                fmt.Fprintln(writer, solve(reader))
        }
}

2026-3-15-研发岗

第一题:搭房子

在线测评链接:https://www.neituiya.com/oj/3/2340

题目描述

AK机想用木棍搭一个"房子"形状:下部是一个长方形,上部是一个等腰三角形,且两者共用一条边(即长方形的上边同时作为三角形的底边,三角形除底边的两条边要相等)。他手上有一堆木棍(每根木棍的长度为正整数),每根木棍最多使用一次。

请你判断,是否能从中挑出若干根,恰好拼成这样的"房子"。

输入描述

第一行输入一个整数 $$n(6 \le n \le 10^5)$$,表示木棍数量。

第二行包含 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n(1 \le a_i \le 10^5)$$,表示每根木棍的长度。

输出描述

输出一行:若能用这些木棍拼出"房子",输出 YES;否则输出 NO

样例1

输入

6
4 4 3 3 5 5

输出

YES

样例解释

可取两根 $$4$$ 作为长方形上下边(其中上边与三角形底边共用)、两根 $$3$$ 作为长方形左右边、两根 $$5$$ 作为等腰三角形的两腰,且 $$5 + 5 > 4$$ 满足三角不等式,输出 YES

样例2

输入

6
4 4 3 3 2 1

输出

NO

题解

题目内容拆解

需要选出 $$6$$ 根木棍拼成房子:$$2$$ 根长度 $$a$$(长方形上下边,上边与三角形底边共用)、$$2$$ 根长度 $$b$$(长方形左右边)、$$2$$ 根长度 $$c$$(等腰三角形两腰),约束 $$2c > a$$(三角不等式)。$$n \le 10^5$$,需要高效判断。

核心观察:由于可以自由分配哪对作为 $$a, b, c$$,只要总对数 $$\ge 3$$,就一定能构造出合法的房子。因为取最大对长 $$c_{max}$$ 和任意一对 $$a$$,$$2c_{max} \ge 2a_{min} > a_{min}$$ 恒成立($$c_{max} \ge a_{min}$$,且值都是正整数)。即使三对完全相同长度 $$v$$,也有 $$2v > v$$。

算法实现

算法主策略:本题采用频次统计

统计每个长度出现的次数,对每个长度 $$v$$,可提供的"对数"为 $$\lfloor freq[v] / 2 \rfloor$$。将所有长度的对数求和,若总对数 $$\ge 3$$ 则输出 YES,否则 NO

以样例1为例:$$freq = \{4:2, 3:2, 5:2\}$$,每个长度各提供 $$1$$ 对,总对数 $$= 3 \ge 3$$,输出 YES。样例2:$$freq = \{4:2, 3:2, 2:1, 1:1\}$$,总对数 $$= 1 + 1 + 0 + 0 = 2 < 3$$,输出 NO

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历一次统计频次并累加对数。
  • 空间复杂度:$$O(V)$$,$$V = 10^5$$ 为值域上界,存储频次数组。

Java

// 搭房子 - 计数
import java.io.*;
import java.util.*;

public class Main {

    // 判断能否选出3对木棍拼成房子
    static String solve(int n, int[] a) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : a) cnt.merge(x, 1, Integer::sum);
        int totalPairs = 0;
        for (int c : cnt.values()) totalPairs += c / 2;
        return totalPairs >= 3 ? "YES" : "NO";
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] a = new int[n];
        for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
        System.out.println(solve(n, a));
    }
}

第二题:二元组

在线测评链接:https://www.neituiya.com/oj/3/2341

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \cdots, a_n\}$$,初始时对于所有 $$1 \le i \le n$$ 均有 $$a_i = i$$。定义一对有序二元组 $$(i, j)$$ 满足 $$1 \le i, j \le n, i \ne j$$:若 $$a_i \le m$$ 且 $$a_j > m$$,则称 $$(i, j)$$ 是一个好的二元组。

你可以进行如下操作任意次(包括 $$0$$ 次):选择一个下标 $$i$$,将 $$a_i$$ 修改为 $$n - a_i + 1$$。

请计算在最优操作后,数组中最多能有多少个不同的好的二元组。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 10^4)$$,表示数据组数。

每组测试数据一行输入两个整数 $$n, m(1 \le n, m \le 10^9)$$,表示数组长度与阈值。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示在最优操作后,最多存在的不同好的二元组数量。

样例1

输入

4
5 2
4 1
1 1
3 10

输出

6
4
0
0

样例解释

对于第二组测试数据,初始数组为 $$\{1, 2, 3, 4\}$$。选择下标 $$4$$,数组变为 $$\{1, 2, 3, 1\}$$,此时满足条件的二元组有 $$(1, 2), (1, 3), (4, 2), (4, 3)$$,共 $$4$$ 个。可以证明不存在其他操作使得满足条件的二元组数量更多。

题解

题目内容拆解

好二元组 $$(i,j)$$ 要求 $$a_i \le m$$ 且 $$a_j > m$$。这是一个有序对,所以好二元组的总数 $$=$$ 值 $$\le m$$ 的个数 $$\times$$ 值 $$> m$$ 的个数(每个"小的"都能和每个"大的"配对)。设 $$low$$ 为值 $$\le m$$ 的个数,$$high$$ 为值 $$> m$$ 的个数,目标是最大化 $$low \times high$$。$$n, m$$ 可达 $$10^9$$,需要 $$O(1)$$ 公式计算。

核心观察:初始 $$a_i = i$$,操作把 $$a_i$$ 改成 $$n - i + 1$$。也就是说,每个位置 $$i$$ 只有两种可能的值:原值 $$i$$ 和翻转值 $$n+1-i$$。这两个值恰好是"互补"的——位置 $$1$$ 可选 $$\{1, n\}$$,位置 $$2$$ 可选 $$\{2, n-1\}$$,以此类推。所以可以按 $$(i, n+1-i)$$ 配对分析每个位置能归属到"小组"还是"大组"。

算法实现

算法主策略:本题采用分类计数 + 二次函数极值

对于每一对 $$(i, n+1-i)$$($$i$$ 从 $$1$$ 到 $$\lfloor n/2 \rfloor$$),两个可选值分别是 $$i$$(较小)和 $$n+1-i$$(较大)。根据这两个值与 $$m$$ 的关系分三类:若较大值 $$n+1-i \le m$$,则无论怎么选都 $$\le m$$,两个位置锁定为 low(fixed\_low $$+2$$);若较小值 $$i > m$$,则无论怎么选都 $$> m$$,两个位置锁定为 high(fixed\_high $$+2$$);否则 $$i \le m < n+1-i$$,每个位置可自由选择归属 low 或 high(flexible $$+2$$)。$$n$$ 为奇数时,中间位置 $$(n+1)/2$$ 的两个可选值相同,只能固定归属一方。

然后问题变成:从 $$flexible$$ 个灵活位置中选 $$k$$ 个归属 low,剩余归属 high,最大化 $$f(k) = (fixed\_low + k)(fixed\_high + flexible - k)$$。这是一个开口朝下的二次函数,极值在 $$k = (B-A)/2$$ 处取得($$A = fixed\_low$$,$$B = fixed\_high + flexible$$),检查两侧整数点即可。

以样例1第一组 $$n=5, m=2$$ 为例:配对 $$(1,5)$$ 和 $$(2,4)$$,中间位置 $$3$$。配对 $$(1,5)$$:可选值 $$\{1, 5\}$$,$$1 \le 2 < 5$$ → flexible,贡献 $$2$$ 个灵活位置。配对 $$(2,4)$$:可选值 $$\{2, 4\}$$,$$2 \le 2 < 4$$ → flexible,贡献 $$2$$ 个灵活位置。中间 $$3 > 2$$ → fixed\_high $$= 1$$。于是 $$A = 0$$(无锁定 low),$$B = 1 + 4 = 5$$($$1$$ 个锁定 high $$+ 4$$ 个灵活)。$$f(k) = k(5-k)$$,极值在 $$k = 2.5$$,取 $$k = 2$$ 或 $$k = 3$$ 得 $$f = 6$$。

时空复杂度分析

  • 时间复杂度:$$O(1)$$ 每组查询,总 $$O(T)$$。
  • 空间复杂度:$$O(1)$$。

Java

// 二元组 - 数学
import java.io.*;
import java.util.*;

public class Main {

    // 计算最优操作后最多好二元组数量
    static long solve(long n, long m) {
        long half = n / 2;

        // 配对 (i, n+1-i) 中 both_low: n+1-i <= m
        long bothLow = 0;
        if (n + 1 - m <= half) {
            bothLow = Math.max(0, half - Math.max(1, n + 1 - m) + 1);
        }

        // both_high: i > m
        long bothHigh = (m < half) ? Math.max(0, half - m) : 0;

        long fixedLow = 2 * bothLow;
        long fixedHigh = 2 * bothHigh;
        long flexible = 2 * (half - bothLow - bothHigh);

        // 中间位置(n 为奇数时)
        if (n % 2 == 1) {
            long mid = (n + 1) / 2;
            if (mid <= m) fixedLow++;
            else fixedHigh++;
        }

        // 最大化 (fixedLow + k) * (fixedHigh + flexible - k)
        long A = fixedLow, B = fixedHigh + flexible;
        long k1 = Math.max(0, Math.min(flexible, (B - A) / 2));
        long k2 = Math.max(0, Math.min(flexible, k1 + 1));
        return Math.max((A + k1) * (B - k1), (A + k2) * (B - k2));
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            long n = Long.parseLong(st.nextToken());
            long m = Long.parseLong(st.nextToken());
            sb.append(solve(n, m)).append('\n');
        }
        System.out.print(sb);
    }
}

第三题:最少操作次数

在线测评链接:https://www.neituiya.com/oj/3/2342

题目描述

给定两个整数 $$x$$ 与 $$y$$,你可以进行若干次操作,使得 $$x$$ 与 $$y$$ 相等。

每次操作选择任意非负整数 $$k$$,选择 $$x$$ 或 $$y$$ 中的一个数,并将其值加上 $$2^k$$。

请计算使 $$x, y$$ 相等所需的最少操作次数。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 10^4)$$,代表数据组数。

每组测试数据一行包含两个整数 $$x, y(-10^{18} \le x, y \le 10^{18})$$。

输出描述

对于每组测试数据,输出一行一个整数,表示使 $$x$$ 与 $$y$$ 相等的最少操作次数。

样例1

输入

5
0 0
7 0
5 14
-3 5
-10 10

输出

0
2
2
1
2

题解

题目内容拆解

每次操作只能给 $$x$$ 或 $$y$$ 加一个 $$2^k$$。不妨设 $$x < y$$,差值 $$d = y - x$$。给 $$x$$ 加 $$2^k$$ 相当于缩小差值($$+$$ 的贡献),给 $$y$$ 加 $$2^k$$ 相当于扩大差值、但之后可以用其他操作补回来($$-$$ 的贡献)。所以问题等价于:将 $$d$$ 拆成若干个 $$2^{k_i}$$ 的加减组合 $$d = \sum \epsilon_i \cdot 2^{k_i}$$($$\epsilon_i \in \{+1, -1\}$$),最小化项数(即操作次数)。

核心观察:普通二进制表示 $$d = \sum 2^{k_i}$$ 的非零位数就是一种方案,但不一定最优。例如 $$7 = 4 + 2 + 1$$ 需要 $$3$$ 次,但 $$7 = 8 - 1$$ 只需 $$2$$ 次。关键在于连续的 $$1$$ 可以合并:$$0b0111 = 0b1000 - 0b0001$$,用"高位 $$+1$$、低位 $$-1$$"替换一串连续 $$1$$,减少非零位。这种最优表示叫NAF(Non-Adjacent Form,非相邻表示)

算法实现

算法主策略:本题采用 NAF 计算

NAF 从低位到高位逐位构造,核心判断只看最低两位($$d \bmod 4$$):若 $$d$$ 为偶数(当前位是 $$0$$),不产生操作,直接右移 $$d = d/2$$;若 $$d \bmod 4 = 1$$(当前位是孤立的 $$1$$,如 $$\cdots 01$$),正常取 $$+1$$,操作次数加 $$1$$,$$d = (d-1)/2$$;若 $$d \bmod 4 = 3$$(当前位是连续 $$1$$ 的开头,如 $$\cdots 11$$),此时取 $$-1$$ 更优——给 $$d$$ 加 $$1$$ 让连续的 $$1$$ 进位变成更高位的单个 $$1$$,操作次数加 $$1$$,$$d = (d+1)/2$$。

以 $$d = 7 = 0b111$$ 为例手算:$$7$$ 为奇数,$$7 \bmod 4 = 3$$(连续 $$1$$),取 $$-1$$,$$d = (7+1)/2 = 4$$。$$4$$ 为偶数,右移,$$d = 2$$。$$2$$ 为偶数,右移,$$d = 1$$。$$1 \bmod 4 = 1$$(孤立 $$1$$),取 $$+1$$,$$d = 0$$。非零位 $$2$$ 个,答案 $$= 2$$。对应 $$7 = 2^3 - 2^0 = 8 - 1$$,两次操作。

再看 $$d = 9 = 0b1001$$:$$9 \bmod 4 = 1$$(孤立 $$1$$),取 $$+1$$,$$d = 4$$。$$4 \to 2 \to 1$$(连续右移)。$$1 \bmod 4 = 1$$,取 $$+1$$,$$d = 0$$。非零位 $$2$$ 个。对应 $$9 = 2^3 + 2^0 = 8 + 1$$。

时空复杂度分析

  • 时间复杂度:$$O(\log d)$$ 每组,总 $$O(T \log d)$$,$$d \le 2 \times 10^{18}$$,约 $$60$$ 位。
  • 空间复杂度:$$O(1)$$。

Java

// 最少操作次数 - NAF
import java.io.*;
import java.util.*;

public class Main {

    // 计算 d 的 NAF 非零位数(即最少操作次数)
    static int countNaf(long d) {
        int count = 0;
        while (d > 0) {
            if (d % 2 == 1) {
                if (d % 4 == 3) {
                    d = (d + 1) / 2; // 当前位为 -1
                } else {
                    d = (d - 1) / 2; // 当前位为 +1
                }
                count++;
            } else {
                d /= 2;
            }
        }
        return count;
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            long x = Long.parseLong(st.nextToken());
            long y = Long.parseLong(st.nextToken());
            sb.append(countNaf(Math.abs(x - y))).append('\n');
        }
        System.out.print(sb);
    }
}

2026-3-12-算法岗

第一题:增加or减少

在线测评链接:https://www.neituiya.com/oj/7/2320

题目描述

AK机有一个长度为 $$n$$ 的数组 $$\{a_1, a_2, ..., a_n\}$$。AK机总共可以执行以下两种操作,其中第一种操作最多可执行一次,第二种操作也最多可执行一次(两种操作可以组合使用):

  1. 选择两个不同的下标 $$i, j$$,将 $$a_i$$ 修改为 $$a_i + a_j$$,花费 $$k$$ 的代价。
  2. 选择一个元素 $$a_i$$ 以及任意正整数 $$x$$,对其增加或减少 $$x$$,花费 $$x$$ 的代价。

AK机希望花费最小的代价,使得 $$a_1 \times a_2 \times ... \times a_n = 0$$。

请输出这个最小代价。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 50)$$,表示数据组数。

每组测试数据描述如下:

第一行输入两个整数 $$n, k(1 \le n \le 3000, 1 \le k \le 10^9)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, ..., a_n(-10^9 \le a_i \le 10^9)$$。

保证单个测试文件中所有 $$n$$ 的和不超过 $$3000$$。

输出描述

对于每组测试数据,新起一行输出一个整数,表示最小花费代价。

样例1

输入

2
3 5
1 -2 3
4 10
0 0 0 -5

输出

1
0

样例解释

对于第一组测试数据只需要将 $$a_1$$ 减一即可。

题解

题目内容拆解

给定长度为 $$n$$ 的数组和操作代价 $$k$$,要求通过至多一次加法操作(代价 $$k$$)和至多一次增减操作(代价为增减量)使乘积为 $$0$$。$$n \le 3000$$,$$\sum n \le 3000$$,允许 $$O(n^2)$$ 枚举。

核心观察:乘积为 $$0$$ 等价于至少存在一个 $$a_i = 0$$。

算法实现

算法主策略:本题采用枚举所有操作组合取最优。

分三种情况讨论:

  1. 如果数组中已有 $$0$$,答案直接为 $$0$$。
  2. 仅用操作二:选 $$|a_i|$$ 最小的元素直接变为 $$0$$,代价 $$\min_i |a_i|$$。

3) 两种操作组合:先用操作一将 $$a_i$$ 变为 $$a_i + a_j$$(代价 $$k$$),再用操作二将 $$a_i + a_j$$ 变为 $$0$$(代价 $$|a_i + a_j|$$)。枚举所有 $$i \ne j$$ 取 $$k + |a_i + a_j|$$ 的最小值。

最终答案 $$= \min(\min_i |a_i|,\ \min_{i \ne j}(k + |a_i + a_j|))$$。

优化:枚举 $$|a_i + a_j|$$ 的最小值,等价于对数组排序后,对每个 $$a_i$$ 二分查找最接近 $$-a_i$$ 的值。但 $$n \le 3000$$ 直接 $$O(n^2)$$ 枚举即可。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,枚举所有元素对。
  • 空间复杂度:$$O(n)$$,存储数组。

C++

// 增加or减少 - 枚举
#include <bits/stdc++.h>
using namespace std;

long long solve(vector<int>& a, long long k) {
    int n = a.size();
    // 已有0则无需操作
    for (int i = 0; i < n; i++) {
        if (a[i] == 0) return 0;
    }
    // 仅用操作二:直接将最小绝对值元素变为0
    long long ans = LLONG_MAX;
    for (int i = 0; i < n; i++) {
        ans = min(ans, (long long)abs(a[i]));
    }
    // 操作一+操作二组合:a_i + a_j 变为0
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            long long s = abs((long long)a[i] + a[j]);
            ans = min(ans, k + s);
        }
    }
    return ans;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        long long k;
        cin >> n >> k;
        vector<int> a(n);
        for (int i = 0; i < n; i++) cin >> a[i];
        cout << solve(a, k) << "\n";
    }
    return 0;
}

第二题:离散型隐马尔可夫模型预测

在线测评链接:https://www.neituiya.com/oj/7/2321

题目描述

请在仅使用 $$numpy$$ 的前提下,实现离散型隐马尔可夫模型($$HMM$$)的 $$Viterbi$$ 动态规划。

给定:

初始分布 $$\pi \in \mathbb{R}^{N}$$。

状态转移矩阵 $$A \in \mathbb{R}^{N \times N}$$。

观测概率矩阵 $$B \in \mathbb{R}^{N \times M}$$(列索引=观测符号)。

多条离散观测序列 $$\left\{\mathbf{o}^{(s)}\right\}_{s=1}^{S}$$,符号取值 $$0...M-1$$。

请为每条序列计算:

  1. 最优隐藏状态序列 $$\hat{\mathbf{q}}^{(s)} = (q_1, \ldots, q_T)$$
  2. 该序列的对数概率 $$\log P(\hat{\mathbf{q}}, \mathbf{o})$$

实现要求:对数域计算,避免下溢。所有乘法用 $$\log +$$,求和用 $$logsumexp$$ 或 $$np.logaddexp.reduce$$。只需前向传递 $$+$$ 回溯,无需 $$forward/backward$$ 或 $$EM$$ 训练。所有浮点以 $$float64$$ 计算,输出对数概率保留 $$6$$ 位小数(四舍五入)。

输入描述

单行 JSON:$$\pi$$ 为长度 $$N$$ 的数组,$$A$$ 为 $$N \times N$$ 矩阵,$$B$$ 为 $$N \times M$$ 矩阵,$$obs$$ 为 $$S$$ 条观测序列(符号为整数)。

$$N \le 4, M \le 4$$,序列长度 $$T \le 6, S \le 10$$。所有概率矩阵行各自已归一化,不必检验。

输出描述

仅一行 JSON,包含 $$paths$$(每条序列最优路径)和 $$logp$$(对应对数概率,$$round(x, 6)$$ 保留小数位)。次序须与输入 $$obs$$ 保持一致。

样例1

输入

{"pi":[0.6,0.4],"A":[[0.7,0.3],[0.4,0.6]],"B":[[0.5,0.4,0.1],[0.1,0.3,0.6]],"obs":[[0,0]]}

输出

{"paths":[[0,0]],"logp":[-2.253795]}

题解

题目内容拆解

实现 $$HMM$$ 的 $$Viterbi$$ 解码算法:给定模型参数 $$(\pi, A, B)$$ 和观测序列,找到概率最大的隐藏状态路径及其对数概率。

算法实现

算法主策略:本题采用Viterbi 动态规划,在对数域下进行前向递推和回溯。

$$\text{Viterbi: } \delta_t(j) = \max_i [\delta_{t-1}(i) + \log A_{ij}] + \log B_j(o_t)$$

维度变化:$$\delta$$: $$(T, N)$$,$$\psi$$(回溯指针): $$(T, N)$$。

步骤

  1. 初始化:$$\delta_1(i) = \log \pi_i + \log B_i(o_1)$$。
  2. 递推:对 $$t = 2, ..., T$$,计算 $$\delta_t(j) = \max_i [\delta_{t-1}(i) + \log A_{ij}] + \log B_j(o_t)$$,同时记录 $$\psi_t(j) = \arg\max_i [\delta_{t-1}(i) + \log A_{ij}]$$。

3) 终止:最优路径概率 $$P^* = \max_i \delta_T(i)$$,最终状态 $$q_T^* = \arg\max_i \delta_T(i)$$。

4) 回溯:$$q_t^* = \psi_{t+1}(q_{t+1}^*)$$。

时空复杂度分析

  • 时间复杂度:$$O(S \times T \times N^2)$$,对每条序列的每个时刻枚举所有状态转移。
  • 空间复杂度:$$O(T \times N)$$,存储 $$\delta$$ 和回溯指针。

Python

# 离散型隐马尔可夫模型预测 - Viterbi动态规划
import json
import numpy as np

def viterbi(log_pi, log_A, log_B, obs_seq):
    """单条序列的Viterbi解码"""
    T = len(obs_seq)
    N = len(log_pi)
    # delta[t][j] = 到时刻t状态j的最优路径对数概率
    delta = np.full((T, N), -np.inf, dtype=np.float64)
    psi = np.zeros((T, N), dtype=np.int64)
    # 初始化:delta_1(i) = log(pi_i) + log(B_i(o_1))
    delta[0] = log_pi + log_B[:, obs_seq[0]]
    # 递推:delta_t(j) = max_i[delta_{t-1}(i) + log(A_{ij})] + log(B_j(o_t))
    for t in range(1, T):
        # trans[i][j] = delta_{t-1}(i) + log(A_{ij})
        trans = delta[t - 1, :, None] + log_A  # (N, N)
        psi[t] = np.argmax(trans, axis=0)       # 每列取最大的行索引
        delta[t] = np.max(trans, axis=0) + log_B[:, obs_seq[t]]
    # 回溯:从最后时刻的最优状态开始
    path = np.zeros(T, dtype=np.int64)
    path[T - 1] = np.argmax(delta[T - 1])
    logp = delta[T - 1, path[T - 1]]
    for t in range(T - 2, -1, -1):
        path[t] = psi[t + 1, path[t + 1]]
    return path.tolist(), round(float(logp), 6)

data = json.loads(input())
pi = np.array(data["pi"], dtype=np.float64)
A = np.array(data["A"], dtype=np.float64)
B = np.array(data["B"], dtype=np.float64)
obs_list = data["obs"]

# 转到对数域,避免下溢
log_pi = np.log(pi)
log_A = np.log(A)
log_B = np.log(B)

paths = []
logps = []
for obs_seq in obs_list:
    obs_seq = [int(x) for x in obs_seq]
    path, logp = viterbi(log_pi, log_A, log_B, obs_seq)
    paths.append(path)
    logps.append(logp)

print(json.dumps({"paths": paths, "logp": logps}))

第三题:简单数组划分

在线测评链接:https://www.neituiya.com/oj/7/2322

题目描述

给定两个整数 $$b, c$$ 和一个长度为 $$n$$ 的数组 $$a$$。你需要将数组 $$a$$ 划分为 $$m$$ 个连续子数组,覆盖全部元素且互不重叠,每段长度至少为 $$k$$。

我们定义一段数组的贡献为:从该段中删除任意 $$k$$ 个数后,剩余元素贡献之和。设该段的原始长度为 $$len$$(删除前的长度),其中每个元素 $$a_i$$ 的贡献定义为:

$$c \times len^2 \times (a_i - b)^2$$

请问:如何划分才能使得所有段的总贡献最大?输出该最大贡献值。

输入描述

第一行输入五个整数 $$n(1 \le n \le 1000), m(1 \le m \le \min(n, 100)), k(1 \le k \le \min(n, 100)), b, c(-100 \le b, c \le 100)$$,其中 $$n$$ 为数组长度,$$m$$ 为划分的段数,$$k$$ 为每段需要删除的元素个数,$$b, c$$ 为题面参数。

第二行输入 $$n$$ 个整数 $$a_1, a_2, ..., a_n(-100 \le a_i \le 100)$$,表示数组元素。

保证 $$m \times k \le n$$。

输出描述

输出一个整数,表示最大的总贡献值。

样例1

输入

5 2 1 0 1
1 3 2 4 5

输出

800

样例解释

在这个样例中,最优划分为 $$[1]$$ 与 $$[3, 2, 4, 5]$$。在第一段删除元素 $$1$$,该段无剩余元素,贡献为 $$0$$。在第二段删除最小元素 $$2$$,剩余元素 $$[3, 4, 5]$$,段长度为 $$4$$,贡献为 $$1 \times 4^2 \times 3^2 + 1 \times 4^2 \times 4^2 + 1 \times 4^2 \times 5^2 = 144 + 256 + 400 = 800$$。

总贡献为 $$0 + 800 = 800$$。

样例2

输入

3 3 1 0 1
10 20 30

输出

0

样例解释

当每段长度为 $$1$$ 且需要删除 $$1$$ 个元素时,各段无剩余元素,因此总贡献为 $$0$$。

题解

题目内容拆解

将长度 $$n$$ 的数组划分为 $$m$$ 段(每段 $$\ge k$$),每段删除 $$k$$ 个元素后计算贡献,求最大总贡献。$$n \le 1000, m \le 100$$,允许 $$O(n^2 m)$$ 动态规划。

核心观察:每段的贡献与段长 $$len$$、参数 $$c$$、以及保留哪些元素有关。当 $$c \ge 0$$ 时删除 $$(a_i - b)^2$$ 最小的 $$k$$ 个以最大化贡献,当 $$c < 0$$ 时删除最大的 $$k$$ 个。

算法实现

采用动态规划

状态方程定义

设 $$f[j][i]$$ 表示将前 $$i$$ 个元素划分为 $$j$$ 段的最大总贡献。

状态方程初始化

$$f[0][0] = 0$$($$0$$ 个元素划分 $$0$$ 段,贡献为 $$0$$)。其余 $$f[j][i] = -\infty$$。

状态方程转移

枚举第 $$j$$ 段的起点 $$p+1$$,终点 $$i$$,段长 $$len = i - p \ge k$$:

$$f[j][i] = \max_{p} \left( f[j-1][p] + \text{seg}(p+1, i) \right)$$

其中 $$\text{seg}(l, r)$$ 表示区间 $$[l, r]$$ 删除 $$k$$ 个最优元素后的贡献:将区间内 $$(a_i - b)^2$$ 排序,若 $$c \ge 0$$ 删除最小的 $$k$$ 个,否则删除最大的 $$k$$ 个,贡献 $$= c \times len^2 \times \text{剩余元素的} (a_i - b)^2 \text{之和}$$。

预计算 seg:对每个左端点 $$l$$,向右扩展 $$r$$,用堆维护前 $$k$$ 小/大的元素和,$$O(n^2 \log k)$$ 完成。

最终答案为 $$f[m][n]$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2 m + n^2 \log k)$$,DP 转移 $$O(n^2 m)$$,预计算段贡献 $$O(n^2 \log k)$$。
  • 空间复杂度:$$O(nm + n^2)$$,DP 数组 $$O(nm)$$,段贡献 $$O(n^2)$$。

C++

// 简单数组划分 - 动态规划
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m, k, b, c;
    cin >> n >> m >> k >> b >> c;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];

    // 预计算 (a_i - b)^2
    vector<long long> w(n + 1);
    for (int i = 1; i <= n; i++) {
        w[i] = (long long)(a[i] - b) * (a[i] - b);
    }

    // 预计算 seg[l][r]:区间[l,r]删k个最优元素后的贡献
    vector<vector<long long>> seg(n + 1, vector<long long>(n + 1, 0));
    for (int l = 1; l <= n; l++) {
        vector<long long> vals;
        long long total = 0;
        for (int r = l; r <= n; r++) {
            vals.push_back(w[r]);
            total += w[r];
            int len = r - l + 1;
            if (len < k) continue;
            // 排序取前k小/大
            vector<long long> sorted_vals(vals);
            sort(sorted_vals.begin(), sorted_vals.end());
            long long remove_sum;
            if (c >= 0) {
                // 删除k个最小的
                remove_sum = 0;
                for (int i = 0; i < k; i++) remove_sum += sorted_vals[i];
            } else {
                // 删除k个最大的
                remove_sum = 0;
                for (int i = len - k; i < len; i++) remove_sum += sorted_vals[i];
            }
            seg[l][r] = (long long)c * (long long)len * len * (total - remove_sum);
        }
    }

    // DP: f[j][i] = 前i个元素划分为j段的最大贡献
    const long long NEG_INF = -1e18;
    vector<vector<long long>> f(m + 1, vector<long long>(n + 1, NEG_INF));
    f[0][0] = 0;
    for (int j = 1; j <= m; j++) {
        for (int i = j * k; i <= n; i++) {
            // 第j段为 [p+1, i],长度 i-p >= k,前j-1段需至少 (j-1)*k 个元素
            for (int p = (j - 1) * k; p <= i - k; p++) {
                if (f[j - 1][p] == NEG_INF) continue;
                f[j][i] = max(f[j][i], f[j - 1][p] + seg[p + 1][i]);
            }
        }
    }
    cout << f[m][n] << endl;
    return 0;
}

美团

2026-4-25-算法岗

第一题:镜像串

在线评测链接:https://www.neituiya.com/oj/7/2619

第二题:AK机的决策树桩

在线评测链接:https://www.neituiya.com/oj/7/2620

第三题:AK机的异或问题

在线评测链接:https://www.neituiya.com/oj/7/2621

第四题:树上操作

在线评测链接:https://www.neituiya.com/oj/7/2622

2026-4-18-研发岗

第一题:清除残留数据

在线评测链接:https://www.neituiya.com/oj/10/2537

题目描述

AK机正在清除残留数据,这个过程可以抽象成一个长度为 $$n$$ 的序列 $$a$$。

AK机会进行 $$m$$ 轮清洗:每轮她会删除序列中最小的数;如果有多个相同的最小数,则清除最靠前的那个数(即下标最小的那个)。

请输出AK机进行完 $$m$$ 轮清洗后的序列。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,代表数据组数。

每组测试数据描述如下:

第一行输入两个整数 $$n, m(1 \le n \le 2 \times 10^5, 0 \le m < n)$$,分别代表序列的长度和清洗轮数。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,代表初始序列。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$4 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出最终的序列,元素之间用空格隔开。

样例1

输入

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

输出

3 4 5
1 2 3
5

题解

题目内容拆解

从序列中删除 $$m$$ 个最小元素(相同值优先删靠前的),输出剩余元素保持原序。$$\sum n \le 4 \times 10^5$$。核心观察:逐个模拟删除是 $$O(nm)$$,当 $$m$$ 接近 $$n$$ 时太慢。→ 换个思路:一次性确定所有要删除的位置,只需一次排序。

算法实现

算法主策略:采用排序标记

把下标 $$0, 1, \ldots, n-1$$ 按 $$(a[i], i)$$ 排序。排序后前 $$m$$ 个下标就是应当删除的位置——值最小的排在最前面,值相同时下标小的排在前面,恰好对应题目"删除最小值、相同值取最靠前"的规则。选排序而不是逐个模拟,因为排序是 $$O(n \log n)$$,而模拟每轮查找最小值是 $$O(n)$$,共 $$m$$ 轮就是 $$O(nm)$$。

将这 $$m$$ 个下标放入集合标记为已删除,最后按原始顺序遍历,输出未被标记的元素即可。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,排序下标的开销主导,标记和遍历各 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储下标数组和删除标记集合。

Python

# 清除残留数据 - 排序
T = int(input())
for _ in range(T):
    n, m = map(int, input().split())
    a = list(map(int, input().split()))
    # 按(值, 下标)排序,标记前m个为删除
    idx = sorted(range(n), key=lambda i: (a[i], i))
    remove = set(idx[:m])
    res = [str(a[i]) for i in range(n) if i not in remove]
    print(" ".join(res))

第二题:二维坐标系

在线评测链接:https://www.neituiya.com/oj/10/2538

题目描述

在二维直角坐标系中有 $$n$$ 个点(按输入顺序编号为 $$1 \sim n$$),每个点的横、纵坐标均为整数。

请你构造一个大小为 $$n \times n$$ 的数组 $$a_{i,j}$$。对任意两个不同的编号 $$i, j$$,将线段 $$ij$$ 围绕点 $$i$$ 旋转一周,线段的另一端点在以 $$i$$ 为圆心、半径为 $$|ij|$$ 的圆盘上运动。

定义 $$a_{i,j}$$ 为线段 $$ij$$ 在旋转过程中扫过的区域(即以 $$i$$ 为圆心、半径为 $$|ij|$$ 的圆盘)内包含的、除 $$i, j$$ 之外其他编号不同的点的数量;特别地,对所有 $$1 \le i \le n$$ 都有 $$a_{i,i} = 0$$。

更形式化地,令点 $$i$$ 的坐标为 $$(x_i, y_i)$$,则:

$$a_{i,j} = \# \left\{ k \in \{1, \dots, n\} \setminus \{i, j\} \mid \operatorname{dist}(i, k) \le \operatorname{dist}(i, j) \right\}$$

输入描述

每个测试文件包含多组测试数据:第一行输入一个整数 $$T(1 \le T \le 10^3)$$,代表数据组数。每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^3)$$,表示点的数量。

此后共 $$n$$ 行,每行输入两个整数 $$x, y(1 \le x, y \le 10^9)$$,表示一个点的坐标,按输入顺序依次编号为 $$1, 2, \dots, n$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^3$$。

输出描述

对于每一组测试数据,输出 $$n$$ 行,第 $$i$$ 行输出 $$n$$ 个整数,依次为 $$a_{i,1}, a_{i,2}, \dots, a_{i,n}$$,数与数之间以一个空格分隔。

样例1

输入

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

输出

0 1 1
0 0 1
0 1 0
0 0 1 2
1 0 1 2
2 1 0 1
2 1 0 0

题解

题目内容拆解

线段绕点 $$i$$ 旋转一圈扫出一个圆盘,圆盘半径就是 $$i$$ 到 $$j$$ 的距离。问题等价于:对每对 $$(i, j)$$,数一下到 $$i$$ 的距离不超过到 $$j$$ 的距离的其他点有多少个。$$n$$ 之和 $$\le 2 \times 10^3$$,$$O(n^2 \log n)$$ 足够。

核心观察:$$a_{i,j}$$ 只取决于"有多少个点离 $$i$$ 比 $$j$$ 更近或一样近"。对每个 $$i$$,把它到所有其他点的距离提前排好序,之后每次查询只需在排好序的数组里做一次二分查找。

算法实现

几何翻译为距离计数

题目说"线段绕点 $$i$$ 旋转一圈",扫出的区域是以 $$i$$ 为圆心、$$|ij|$$ 为半径的圆盘。圆盘内的点 $$k$$,就是满足"$$k$$ 到 $$i$$ 的距离 $$\le$$ $$j$$ 到 $$i$$ 的距离"的点。所以 $$a_{i,j}$$ 就是这样的 $$k$$ 的个数(排除 $$i$$ 和 $$j$$)。

$$a_{i,j} = \text{满足 } \operatorname{dist}(i, k) \le \operatorname{dist}(i, j) \text{ 的点 } k \text{ 的个数} - 1$$

减 $$1$$ 是因为 $$j$$ 自己到 $$i$$ 的距离恰好等于阈值,也被数进去了,但题目要求排除 $$j$$。

用距离平方代替距离

两点之间的欧氏距离需要开根号,而开根号会引入浮点误差。但比较大小时,$$\operatorname{dist}(i, k) \le \operatorname{dist}(i, j)$$ 等价于距离平方的比较:

$$d(i, k) = (x_i - x_k)^2 + (y_i - y_k)^2$$

距离平方是整数,比较整数没有精度问题。坐标最大 $$10^9$$,距离平方最大约 $$2 \times 10^{18}$$,超出 32 位范围,需要用 64 位整数存储。

预处理:对每个点排序距离

对每个点 $$i$$,计算它到其他 $$n-1$$ 个点的距离平方,存成一个数组,然后排序。排序的目的是让后续查询可以用二分查找,而不是逐个遍历。如果不排序,每次查询要扫一遍全部 $$n-1$$ 个距离,总复杂度 $$O(n^3)$$;排序后二分查找只要 $$O(\log n)$$,总复杂度降到 $$O(n^2 \log n)$$。

查询:二分查找计数

查询 $$a_{i,j}$$ 时,先算出 $$i$$ 到 $$j$$ 的距离平方 $$d$$,然后在 $$i$$ 的排序距离数组里找"第一个大于 $$d$$ 的位置"。这个位置的下标就是数组中 $$\le d$$ 的元素个数 $$c$$,即到 $$i$$ 的距离不超过 $$d$$ 的点数。

$$a_{i,j} = c - 1$$

$$c$$ 里包含了 $$j$$ 本身(因为 $$j$$ 到 $$i$$ 的距离平方恰好等于 $$d$$),题目要求排除 $$j$$,所以减 $$1$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2 \log n)$$,因为对 $$n$$ 个点各做一次排序,每次排序 $$O(n \log n)$$,合计 $$O(n^2 \log n)$$;查询阶段共 $$n^2$$ 次二分查找,每次 $$O(\log n)$$,合计也是 $$O(n^2 \log n)$$。
  • 空间复杂度:$$O(n^2)$$,因为要为每个点存储一个长度 $$n-1$$ 的排序距离数组,共 $$n$$ 个数组。

C++

// 二维坐标系 - 排序 + 二分查找
#include <bits/stdc++.h>
using namespace std;

void solve(int n, vector<long long>& px, vector<long long>& py) {
    // 预处理:对每个点i,算出它到其他所有点的距离平方,排序备用
    vector<vector<long long>> dist2(n);
    for (int i = 0; i < n; i++) {
        for (int k = 0; k < n; k++) {
            if (k == i) continue;
            long long dx = px[i] - px[k];
            long long dy = py[i] - py[k];
            // 用距离平方代替距离,避免开根号的浮点误差
            dist2[i].push_back(dx * dx + dy * dy);
        }
        // 排序后就能用二分查找快速计数
        sort(dist2[i].begin(), dist2[i].end());
    }

    // 查询:对每对(i,j),二分找到距离≤dist(i,j)的点数
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (j > 0) cout << ' ';
            if (i == j) {
                cout << 0;
            } else {
                long long dx = px[i] - px[j];
                long long dy = py[i] - py[j];
                long long d = dx * dx + dy * dy;
                // upper_bound找到第一个>d的位置,该位置就是≤d的元素个数
                int cnt = (int)(upper_bound(dist2[i].begin(), dist2[i].end(), d) - dist2[i].begin());
                // 减1排除j自身(j的距离恰好等于d,也被数进了cnt里)
                cout << cnt - 1;
            }
        }
        cout << '\n';
    }
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<long long> px(n), py(n);
        for (int i = 0; i < n; i++) {
            cin >> px[i] >> py[i];
        }
        solve(n, px, py);
    }
    return 0;
}

第三题:最长公共子序列3

在线评测链接:https://www.neituiya.com/oj/10/2539

题目描述

给定两个排列 $$p$$ 和 $$q$$,长度都为 $$n$$。请你求出 $$p$$ 和 $$q$$ 之间字典序最大的最长公共子序列

排列:长度为 $$n$$ 的排列是由 $$1, 2, \ldots, n$$ 每个整数恰好出现一次组成的序列。例如:$$(2, 3, 1, 5, 4)$$ 是一个长度为 $$5$$ 的排列;而 $$(1, 2, 2)$$ 和 $$(1, 3, 4)$$ 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

子序列:在序列的顺序中删除任意个(可以为零、可以为全部)元素得到的新序列。

公共子序列:如果数组 $$a$$ 的一个子序列 $$a'$$ 与数组 $$b$$ 的一个子序列 $$b'$$ 完全相等,那么子序列 $$a', b'$$ 就是数组 $$a, b$$ 的一个公共子序列。

字典序比较:从数组的第一个元素开始逐个比较,直到找到第一个不同的位置,通过比较这个位置元素的大小得出数组的大小,称为字典序比较。

输入描述

每个测试文件包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$,代表数据组数。

每组测试数据描述如下:

第一行输入一个整数 $$n(2 \le n \le 2 \times 10^5)$$,代表排列 $$p$$ 和 $$q$$ 的长度。

第二行输入 $$n$$ 个不同整数 $$p_1, p_2, \ldots, p_n(1 \le p_i \le n)$$,代表排列 $$p$$ 中的元素。

第三行输入 $$n$$ 个不同整数 $$q_1, q_2, \ldots, q_n(1 \le q_i \le n)$$,代表排列 $$q$$ 中的元素。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据:先输出一行一个整数 $$k(1 \le k \le n)$$,代表 $$p$$ 和 $$q$$ 的最长公共子序列长度。

第二行输出 $$k$$ 个不同整数 $$r_1, r_2, \ldots, r_k$$,代表你找到的字典序最大的最长公共子序列。

样例1

输入

3
4
1 3 2 4
3 4 1 2
5
1 2 3 4 5
5 4 3 2 1
9
9 2 6 7 3 8 1 4 5
6 2 7 9 4 8 3 5 1

输出

2
3 4
1
5
4
6 7 8 5

题解

题目内容拆解

求两个排列的字典序最大的最长公共子序列(LCS)。$$\sum n \le 2 \times 10^5$$。

核心观察:普通 LCS 是 $$O(n^2)$$ 的 DP,但输入是排列(每个值恰好出现一次),可以转化为最长递增子序列(LIS)问题,用 $$O(n \log n)$$ 解决。→ 先用 BIT 预处理出每个位置的"后缀 LIS 长度",再贪心选字典序最大的方案。

算法实现

LCS 转 LIS——为什么排列的 LCS 等于 LIS

两个排列的公共子序列,本质是在 $$p$$ 中选一些位置、在 $$q$$ 中选一些位置,使得选出的值序列相同。因为是排列,每个值在两边各出现恰好一次,所以选了一个值就同时确定了它在 $$p$$ 和 $$q$$ 中的位置。

记 $$\text{pos}_q[v]$$ 为值 $$v$$ 在 $$q$$ 中的位置(第几个)。构建序列:

$$c[i] = \text{pos}_q[p[i]]$$

$$c[i]$$ 的含义是"$$p$$ 的第 $$i$$ 个值,出现在 $$q$$ 的第几个位置"。选出的公共子序列要求在 $$p$$ 中保持顺序(下标递增)同时在 $$q$$ 中也保持顺序(位置递增),等价于在 $$c$$ 中找一个严格递增的子序列。所以 LCS 长度 = $$c$$ 的 LIS 长度。

后缀 LIS 预处理——为什么需要它

贪心构造阶段需要判断"从某个位置开始,还能不能选够剩余长度的递增子序列"。为此预计算:

$$\text{suffix\_lis}[i] = \text{从位置 } i \text{ 开始(含 } c[i] \text{),} c \text{ 的最长严格递增子序列长度}$$

从右往左扫描,对每个位置 $$i$$,需要查询"$$i$$ 右边所有 $$c$$ 值 $$> c[i]$$ 的位置中,$$\text{suffix\_lis}$$ 的最大值",加 $$1$$ 就是 $$\text{suffix\_lis}[i]$$。

这个"区间最大值查询 + 单点更新"用树状数组(BIT)维护后缀最大值即可在 $$O(\log n)$$ 内完成。

BIT 的细节可以当黑盒使用——把坐标反转后就变成前缀最大值的标准 BIT。

贪心构造——依次选字典序最大的值

设 LCS 总长度为 $$L$$。逐步确定 LCS 的每个位置,每步在所有候选中选 $$p[i]$$ 最大的。

候选条件:位置 $$i$$ 在当前搜索范围内,$$c[i]$$ 比上次选的 $$c$$ 值大(保证在 $$q$$ 中顺序递增),且 $$\text{suffix\_lis}[i] \ge \text{remain}$$(保证从 $$i$$ 开始还能凑够剩余长度)。

选 $$p[i]$$ 最大的不会影响后续可行性:$$\text{suffix\_lis}[i] \ge \text{remain}$$ 保证从 $$c[i]$$ 出发一定存在长度 $$\ge \text{remain}$$ 的递增子序列,而这个子序列中所有后续值都 $$> c[i]$$,天然满足约束。

每步选完后更新搜索起点和 $$c$$ 值约束。由于搜索起点单调递增,总扫描量 $$O(n)$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,BIT 预处理每个位置 $$O(\log n)$$,共 $$n$$ 个位置;贪心构造扫描总量 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储位置映射、$$c$$ 序列、BIT 数组和 $$\text{suffix\_lis}$$ 数组。

Go

// 最长公共子序列3 - 贪心+BIT
package main

import (
        "bufio"
        "fmt"
        "os"
)

var treeArr [200005]int
var nBit int

func bitInit(n int) {
        nBit = n
        for i := 0; i <= n+1; i++ {
                treeArr[i] = 0
        }
}

func bitUpdate(x, val int) {
        x = nBit - x
        for ; x <= nBit; x += x & (-x) {
                if val > treeArr[x] {
                        treeArr[x] = val
                }
        }
}

func bitQuery(x int) int {
        x = nBit - x
        res := 0
        for ; x > 0; x -= x & (-x) {
                if treeArr[x] > res {
                        res = treeArr[x]
                }
        }
        return res
}

func solve(reader *bufio.Reader, writer *bufio.Writer) {
        var n int
        fmt.Fscan(reader, &n)
        p := make([]int, n)
        q := make([]int, n)
        for i := range p {
                fmt.Fscan(reader, &p[i])
        }
        for i := range q {
                fmt.Fscan(reader, &q[i])
        }

        // c[i] = q中p[i]的位置
        posQ := make([]int, n+1)
        for i, v := range q {
                posQ[v] = i
        }
        c := make([]int, n)
        for i := range p {
                c[i] = posQ[p[i]]
        }

        // 计算suffix_lis
        suffixLis := make([]int, n)
        bitInit(n)
        for i := n - 1; i >= 0; i-- {
                best := 0
                if c[i]+1 < n {
                        best = bitQuery(c[i] + 1)
                }
                suffixLis[i] = best + 1
                bitUpdate(c[i], suffixLis[i])
        }

        L := 0
        for _, v := range suffixLis {
                if v > L {
                        L = v
                }
        }

        // 贪心选择字典序最大的LCS
        result := make([]int, 0, L)
        remain := L
        lastC := -1
        iStart := 0
        for remain > 0 {
                bestVal := -1
                bestPos := -1
                bestC := -1
                for i := iStart; i < n; i++ {
                        if c[i] > lastC && suffixLis[i] >= remain {
                                if p[i] > bestVal {
                                        bestVal = p[i]
                                        bestPos = i
                                        bestC = c[i]
                                }
                        }
                }
                result = append(result, bestVal)
                lastC = bestC
                iStart = bestPos + 1
                remain--
        }

        fmt.Fprintln(writer, L)
        for i, v := range result {
                if i > 0 {
                        fmt.Fprint(writer, " ")
                }
                fmt.Fprint(writer, v)
        }
        fmt.Fprintln(writer)
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                solve(reader, writer)
        }
}

2026-4-18-算法岗

第一题:清除残留数据

在线评测链接:https://www.neituiya.com/oj/10/2533

题目描述

AK机正在清除残留数据,这个过程可以抽象成一个长度为 $$n$$ 的序列 $$a$$。

AK机会进行 $$m$$ 轮清洗:每轮她会删除序列中最小的数;如果有多个相同的最小数,则清除最靠前的那个数(即下标最小的那个)。

请输出AK机进行完 $$m$$ 轮清洗后的序列。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$,代表数据组数。

每组测试数据描述如下:

第一行输入两个整数 $$n, m(1 \le n \le 2 \times 10^5, 0 \le m < n)$$,分别代表序列的长度和清洗轮数。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,代表初始序列。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$4 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出最终的序列,元素之间用空格隔开。

样例1

输入

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

输出

3 4 5
1 2 3
5

第二题:数据分析师

在线评测链接:https://www.neituiya.com/oj/10/2534

题目描述

AK机是美团的一名数据分析师,她希望利用高斯过程回归模型,根据一部分商家的位置坐标 $$x_i$$ 及其对应的营业额 $$y_i$$,来预测另一批位置 $$x_*$$ 的商家可能营业情况。

请你仅使用 NumPy,实现该模型的封闭式预测(无超参学习)。

固定超参数:长尺度 $$l = 1.0$$,信号方差 $$\sigma_f^2 = 1.0$$,噪声方差 $$\sigma_n^2 = 0.1$$。

核函数(RBF 核 / 高斯核)

$$k(p, q) = \sigma_f^2 \exp\left( -\frac{\|p - q\|^2}{2l^2} \right)$$

预测公式

训练核矩阵:$$K = K(X, X) + \sigma_n^2 I_n$$。逆矩阵:$$K^{-1}$$。对每个测试点 $$x_*$$,计算 $$k_* = K(X, x_*)$$,预测均值 $$\hat{y} = k_*^T K^{-1} y$$。

输入描述

单行 JSON 格式数据,结构如下:

{"train": [[x11, ..., y1], [x21, ..., y2], ...], "test": [[x*1, ...], ...]}

维度 $$d \in \{1, 2\}$$(训练与测试一致)。训练样本数 $$n(4 \le n \le 40)$$,测试样本数 $$m(2 \le m \le 20)$$。所有值为实数,无缺失。

输出描述

单行 JSON 数组,预测均值序列,例如 $$[0.0, 0.988567]$$。

补充说明:矩阵求逆可直接使用 np.linalg.inv(训练集尺寸最多 $$40$$,可直接求逆)。不需要设置任何随机数。数据不需要额外预处理。浮点误差处理:使用常规 double 精度计算,输出 round(val, 6) 即可。

样例1

输入

{"train": [[1,-1,1],[1,1,1]], "test": [[-1,1],[0,1]]}

输出

[-0.896337, 0.0, 0.896337]

第三题:神秘工坊

在线评测链接:https://www.neituiya.com/oj/10/2535

题目描述

在神秘的魔法工坊中,AK机有两个魔法阵列 $$a$$ 和 $$b$$,长度均为 $$n$$。在一次操作中,她可以在 $$a$$ 中选择若干个值相同的元素,并将它们的值同时翻倍一次(即乘以 $$2$$)。

AK机希望通过若干次操作,使阵列 $$a$$ 的元素多重集合与阵列 $$b$$ 完全一致(不要求顺序,只关心元素及其出现次数)。

请计算将 $$a$$ 变换为 $$b$$ 的元素多重集合所需的最小操作次数;如果无法实现,则输出 $$-1$$。

输入描述

第一行输入一个整数 $$T(1 \le T \le 10^5)$$,表示测试数据组数。

每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 10^5)$$,表示数组长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^9)$$,表示初始数组 $$a$$。

第三行输入 $$n$$ 个整数 $$b_1, b_2, \ldots, b_n(1 \le b_i \le 10^9)$$,表示目标数组 $$b$$。

保证所有测试用例中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一行一个整数,表示将 $$a$$ 转换为与 $$b$$ 元素多重集合相同所需的最小操作次数;如果无法实现,则输出 $$-1$$。

样例1

输入

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

输出

2
2
-1

样例解释

样例一:$$\{1, 2, 3\} \rightarrow \{2, 4, 3\}$$,先将 $$1 \rightarrow 2$$,再将 $$2 \rightarrow 4$$,共 $$2$$ 步。

样例二:$$\{1, 1, 2, 2\} \rightarrow \{4, 2, 2, 1\}$$。可对一个 $$1$$ 倍化一次、对一个 $$2$$ 倍化一次,最少 $$2$$ 步。

样例三:$$9$$ 不是 $$5 \times 2^k$$ 的形式,故无解,输出 $$-1$$。


第四题:最长公共子序列3

在线评测链接:https://www.neituiya.com/oj/10/2536

题目描述

给定两个排列 $$p$$ 和 $$q$$,长度都为 $$n$$。请你求出 $$p$$ 和 $$q$$ 之间字典序最大的最长公共子序列

排列:长度为 $$n$$ 的排列是由 $$1, 2, \ldots, n$$ 每个整数恰好出现一次组成的序列。例如:$$(2, 3, 1, 5, 4)$$ 是一个长度为 $$5$$ 的排列;而 $$(1, 2, 2)$$ 和 $$(1, 3, 4)$$ 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

子序列:在序列的顺序中删除任意个(可以为零、可以为全部)元素得到的新序列。

公共子序列:如果数组 $$a$$ 的一个子序列 $$a'$$ 与数组 $$b$$ 的一个子序列 $$b'$$ 完全相等,那么子序列 $$a', b'$$ 就是数组 $$a, b$$ 的一个公共子序列。

字典序比较:从数组的第一个元素开始逐个比较,直到找到第一个不同的位置,通过比较这个位置元素的大小得出数组的大小,称为字典序比较。

输入描述

每个测试文件包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$,代表数据组数。

每组测试数据描述如下:

第一行输入一个整数 $$n(2 \le n \le 2 \times 10^5)$$,代表排列 $$p$$ 和 $$q$$ 的长度。

第二行输入 $$n$$ 个不同整数 $$p_1, p_2, \ldots, p_n(1 \le p_i \le n)$$,代表排列 $$p$$ 中的元素。

第三行输入 $$n$$ 个不同整数 $$q_1, q_2, \ldots, q_n(1 \le q_i \le n)$$,代表排列 $$q$$ 中的元素。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试数据:先输出一行一个整数 $$k(1 \le k \le n)$$,代表 $$p$$ 和 $$q$$ 的最长公共子序列长度。

第二行输出 $$k$$ 个不同整数 $$r_1, r_2, \ldots, r_k$$,代表你找到的字典序最大的最长公共子序列。

样例1

输入

3
4
1 3 2 4
3 4 1 2
5
1 2 3 4 5
5 4 3 2 1
9
9 2 6 7 3 8 1 4 5
6 2 7 9 4 8 3 5 1

输出

2
3 4
1
5
4
6 7 8 5

2026-4-11-算法岗

第一题:两两成盒

在线评测链接:https://www.neituiya.com/oj/10/2495

题目描述

给定一个长度为 $$n$$ 的整数数组 $$\{a_1, a_2, \ldots, a_n\}$$。我们按顺序将相邻元素两两成"盒":

盒 $$1$$ 为 $$(a_1, a_2)$$,盒 $$2$$ 为 $$(a_3, a_4)$$,以此类推;若 $$n$$ 为奇数,则最后一个盒为单元素盒 $$(a_n)$$。

你需要恰好选择 $$x$$ 个盒,并从每个被选择的盒中选出且仅选出一个数字,使得所有被选数字的和为奇数。判断是否可以做到。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, x(1 \le n \le 2 \times 10^5, 1 \le x \le \lceil\frac{n}{2}\rceil)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(0 \le a_i \le 10^9)$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试,若存在可行选择,输出 Yes;否则输出 No

样例1

输入

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

输出

Yes
Yes
No

样例解释

第 $$2$$ 组:任取一个盒即为奇数,答案为 Yes

第 $$3$$ 组:所有盒均强制为偶数,选任意 $$3$$ 个盒之和为偶数,无法为奇,答案为 No


第二题:小美的优惠券预测模型

在线评测链接:https://www.neituiya.com/oj/10/2496

题目描述

AK机正在为美团的优惠券推荐业务开发一个预测模型,她需要使用对数几率回归(Logistic Regression)来预测用户是否会购买某个优惠券。请你帮助她,在仅使用 numpy、pandas 的前提下,手写实现该模型并对测试样本给出类别预测。

具体流程:

读取数据

train:二维列表,代表用户历史行为数据。最后一列为标签 $$y \in \{0, 1\}$$($$1$$ 代表购买,$$0$$ 代表未购买),前 $$m$$ 列为用户的数值特征。

test:二维列表,仅包含与训练集同维度的用户特征。

模型训练(IRLS 闭式迭代)

训练特征矩阵最左侧拼接全 $$1$$ 列(截距项)。使用迭代重加权最小二乘(IRLS)求极大似然解,迭代公式如下:

$$w^{(t+1)} = w^{(t)} - (X^\top W X + \varepsilon I)^{-1} X^\top (p - y)$$

$$p = \sigma(Xw), \quad \sigma(z) = \frac{1}{1 + e^{-z}}$$

$$W = \text{diag}(p_i(1 - p_i))$$

微扰项 $$\varepsilon = 10^{-8}$$ 加在 Hessian 对角,防止矩阵奇异。收敛判据:$$\|w^{(t+1)} - w^{(t)}\|_2 < 10^{-6}$$ 或迭代 $$30$$ 次即停。

预测

测试集同样拼接截距列。计算 $$\hat{p} = \sigma(X_{\text{test}} w)$$,取 $$\hat{y} = \mathbb{1}[\hat{p} \ge 0.5]$$ 作为预测标签。

输入描述

标准输入仅一行 JSON,包含 traintest 两个字段。训练部分最后一列是标签,所有值均为整数或浮点数。

输出描述

仅输出一行,格式为 JSON 列表,如 [0, 1],长度等于测试样本数,逗号后加空格。

样例1

输入

{"train":[[1,2,0],[2,1,8,0],[5,5,1],[4,5,5,2,1]],"test":[[1,5,1,9],[5,5,1]]}

输出

[0, 1]

补充说明

为确保通过测试用例,仅允许使用 Numpy 和 Pandas。


第三题:数序列(二)

在线评测链接:https://www.neituiya.com/oj/10/2497

题目描述

给定四个整数 $$n, v, m, k$$,我们考虑长度为 $$n$$ 的序列 $$a$$,元素取值范围为 $$1 \ldots v$$,求满足以下性质的序列个数,对 $$10^9 + 7$$ 取模:

存在至少一个长度为 $$m$$ 的连续子段,其元素和不等于 $$k$$,保证 $$m \le k$$。

输入描述

每个测试文件均包含多组测试数据,第一行输入一个整数 $$T(1 \le T \le 2 \times 10^7)$$ 代表数据组数,每组测试数据描述如下:

每行输入四个整数 $$n, v, m, k(1 \le n \le 10^{18}, 1 \le v, m, k \le 2 \times 10^9)$$,并保证 $$k \ge m$$。

保证所有测试中 $$m$$ 的总和不超过 $$2 \times 10^8$$。

输出描述

输出 $$T$$ 行,每行一个整数,表示满足条件的序列个数对 $$10^9 + 7$$ 取模后的结果。

样例1

输入

4
1 1 2 2
3 2 2 3
4 3 3 6
10 3 3 6

输出

0
6
74
59042

样例解释

样例 $$1$$:$$n = 1, v = 1, m = 2, k = 2$$,由于 $$n < m$$,不存在长度为 $$m$$ 的子段,答案为 $$0$$。

样例 $$2$$:总数 $$= 2^3 = 8$$,只有序列 $$1, 2, 1$$ 与 $$2, 1, 2$$ 是不可以的,因此答案 $$= 8 - 2 = 6$$。

题解

本题涉及到快速幂,不熟悉该算法的同学可以先做一下模板题:

快速幂


第四题:我即唯一

在线评测链接:https://www.neituiya.com/oj/10/2498

题目描述

AK机正在周游世界,她来到了天才之国,这里一共有 $$n$$ 个城市,编号为 $$1, 2, \ldots, n$$。她早早做好了规划:当她来到城市 $$i$$ 时,下一次会去到城市 $$a_i$$。同时,她会获得价值:如果这是她第 $$t$$ 次来到城市 $$i$$,那么这一次会获得 $$t \times i$$ 的价值。

AK机提出了 $$q$$ 个问题。每个问题给出两个整数 $$p, k$$,表示她从城市 $$p$$ 出发,并且一共"来到城市" $$k$$ 次(包含一开始就在 $$p$$ 的那次)。请你计算她获得的总价值。特别注意:第 $$1$$ 次来到的城市就是起点 $$p$$。

输入描述

第一行输入两个整数 $$n, q(1 \le n, q \le 2 \times 10^5)$$,分别表示城市数量与询问数量。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,其中 $$a_i$$ 表示从城市 $$i$$ 下一次会到达的城市编号。

此后 $$q$$ 行,每行输入两个整数 $$p, k(1 \le p \le n, 1 \le k \le 10^9)$$,含义见题目描述。

输出描述

对于每个询问,新起一行输出一个整数,表示总价值对 $$10^9 + 7$$ 取模后的结果。

样例1

输入

3 3
2 3 3
1 1
1 4
3 3

输出

1
12
18

样例解释

对于询问 $$p = 1, k = 1$$:只来到城市 $$1$$ 这一次,获得 $$1 \times 1 = 1$$。

对于询问 $$p = 1, k = 4$$:来到城市的顺序为 $$1, 2, 3, 3$$。其中城市 $$1$$ 来了 $$1$$ 次,贡献为 $$1$$;城市 $$2$$ 来了 $$1$$ 次,贡献为 $$2$$;城市 $$3$$ 来了 $$2$$ 次,贡献为 $$3 \times 1 + 3 \times 2 = 9$$。总价值为 $$1 + 2 + 9 = 12$$。

对于询问 $$p = 3, k = 3$$:来到城市的顺序为 $$3, 3, 3$$,总价值为 $$3 \times 1 + 3 \times 2 + 3 \times 3 = 18$$。

样例2

输入

4 2
2 1 4 3
1 5
3 2

输出

12
7

2026-4-11-研发岗

第一题:两两成盒

在线评测链接:https://www.neituiya.com/oj/10/2499

题目描述

给定一个长度为 $$n$$ 的整数数组 $$\{a_1, a_2, \ldots, a_n\}$$。我们按顺序将相邻元素两两成"盒":

盒 $$1$$ 为 $$(a_1, a_2)$$,盒 $$2$$ 为 $$(a_3, a_4)$$,以此类推;若 $$n$$ 为奇数,则最后一个盒为单元素盒 $$(a_n)$$。

你需要恰好选择 $$x$$ 个盒,并从每个被选择的盒中选出且仅选出一个数字,使得所有被选数字的和为奇数。判断是否可以做到。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入两个整数 $$n, x(1 \le n \le 2 \times 10^5, 1 \le x \le \lceil \frac{n}{2} \rceil)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(0 \le a_i \le 10^9)$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每组测试,若存在可行选择,输出 $$Yes$$;否则输出 $$No$$。

样例1

输入

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

输出

Yes
Yes
No

样例解释

第 $$2$$ 组:任取一个盒即为奇数,答案为 $$Yes$$。第 $$3$$ 组:所有盒均强制为偶数,选任意 $$3$$ 个盒之和为偶数,无法为奇,答案为 $$No$$。

题解

题目内容拆解

给定数组两两配对成盒,从 $$x$$ 个盒中各选一个数使总和为奇数。数据规模 $$n \le 2 \times 10^5$$,需要 $$O(n)$$ 解法。

核心观察:总和的奇偶性只取决于选出的奇数个数的奇偶性——需要选出奇数个奇数。每个盒子能贡献的奇偶性由其内部元素决定,→ 因此采用分类讨论

算法实现

算法主策略:将每个盒子分为三类——仅含奇数(only\_odd)、仅含偶数(only\_even)、奇偶兼有(flexible),然后枚举 only\_odd 的选取数量判断可行性。

对于双元素盒 $$(a_{2i-1}, a_{2i})$$,若两个元素奇偶性相同则归入对应的 only 类,否则归入 flexible 类。单元素盒按其奇偶性归入对应 only 类。

枚举从 only\_odd 中选 $$a$$ 个盒子($$0 \le a \le \min(\text{only\_odd}, x)$$),剩余 $$x - a$$ 个从 only\_even 和 flexible 中选取。设 $$c_{\max}$$ 为 flexible 盒子的最大可选数量。

若 $$c_{\max} \ge 1$$,说明至少选了一个 flexible 盒子。flexible 盒子里既有奇数又有偶数,选哪个由我们决定——如果当前奇数个数不够,从 flexible 盒子里选奇数补上;如果多了,选偶数。所以只要有一个 flexible 盒子在手,总能调整出奇数个奇数,直接返回 Yes。

若 $$c_{\max} = 0$$,说明选的全是 only\_odd 和 only\_even 盒子,奇数个数固定为 $$a$$,此时 $$a$$ 必须为奇数才能使总和为奇。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历数组一次统计三类盒子数量,枚举 $$a$$ 最多 $$O(\lceil n/2 \rceil)$$ 次。
  • 空间复杂度:$$O(n)$$,存储输入数组。

Go

// 两两成盒 - 分类讨论
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(n, x int, arr []int) string {
        // 按奇偶性将盒子分三类
        onlyOdd, onlyEven, flexible := 0, 0, 0
        for i := 0; i < n; i += 2 {
                p1 := arr[i] % 2
                if i+1 < n {
                        p2 := arr[i+1] % 2
                        // 两个元素奇偶相同归 only 类,不同归 flexible
                        if p1 == 1 && p2 == 1 {
                                onlyOdd++
                        } else if p1 == 0 && p2 == 0 {
                                onlyEven++
                        } else {
                                flexible++
                        }
                } else {
                        // 单元素盒,奇偶性固定
                        if p1 == 1 {
                                onlyOdd++
                        } else {
                                onlyEven++
                        }
                }
        }

        totalBoxes := onlyOdd + onlyEven + flexible
        if x > totalBoxes {
                return "No"
        }

        // 枚举选 a 个 only_odd 盒子
        maxA := onlyOdd
        if x < maxA {
                maxA = x
        }
        for a := 0; a <= maxA; a++ {
                remain := x - a
                // flexible 不够时用 only_even 补
                bMin := remain - flexible
                if bMin < 0 {
                        bMin = 0
                }
                bMax := onlyEven
                if remain < bMax {
                        bMax = remain
                }
                if bMin > bMax {
                        continue
                }
                cMax := remain - bMin
                // 有 flexible 盒子就能调整奇偶,直接可行
                if cMax >= 1 {
                        return "Yes"
                }
                // 没有 flexible,奇数个数固定为 a,a 为奇数才行
                if a%2 == 1 {
                        return "Yes"
                }
        }
        return "No"
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var t int
        fmt.Fscan(reader, &t)
        for ; t > 0; t-- {
                var n, x int
                fmt.Fscan(reader, &n, &x)
                arr := make([]int, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &arr[i])
                }
                fmt.Fprintln(writer, solve(n, x, arr))
        }
}

第二题:最长非重复子串

在线评测链接:https://www.neituiya.com/oj/10/2500

题目描述

题目描述

我们定义一个仅由小写字母组成的字符串 $$s$$ 的奇怪度为:其中的最长非重复子串数量。此处"数量"按起止位置计数,不按内容去重。

现在,给定两个整数 $$n$$ 和 $$k$$,要求构造一个长度为 $$n$$、仅由小写字母组成的字符串 $$s$$,其奇怪度恰好为 $$k$$。如果无解,则输出 $$-1$$。

非重复子串:每个字符出现的次数不超过 $$1$$ 次的字符串。例如 $$abcg$$ 是非重复子串,而 $$aba$$ 不是非重复子串(因为 $$a$$ 出现了两次)。

子串:从原字符串中,连续的选择一段字符(可以全选、可以不选)得到的新字符串。

输入描述

在一行上输入两个整数 $$n, k(1 \le n \le 10^5, 1 \le k \le n)$$。

输出描述

如果不存在满足条件的字符串,直接输出 $$-1$$。否则,输出一个长度为 $$n$$ 的合法字符串。

如果存在多个解决方案,您可以输出任意一个,系统会自动判定是否正确。

样例1

输入

5 3

输出

abaab

样例解释

在这个样例中,最长无重复子串长度为 $$2$$,满足条件的子串为 $$s_1 s_2 = ab$$、$$s_2 s_3 = ba$$、$$s_4 s_5 = ab$$ 共 $$3$$ 个。

样例2

输入

5 3

输出

ababb

样例解释

本题答案不唯一。

题解

题目内容拆解

构造长度为 $$n$$ 的字符串,使得最长非重复子串的数量恰好为 $$k(1 \le k \le n)$$。核心观察:若所有字符相同(如 aaa...a),最长非重复子串长度 $$L=1$$,数量等于 $$n$$;若使用交替模式(如 abab...),$$L=2$$,每个相邻不同字符对都是一个最长非重复子串。→ 因此采用分情况构造

算法实现

算法主策略:通过控制交替模式的长度精确调节最长非重复子串的数量。

当 $$k = n$$ 时,输出全同字符串 aaa...a,此时 $$L=1$$,每个字符位置都是一个长度为 $$1$$ 的最长非重复子串,共 $$n$$ 个。

当 $$1 \le k < n$$ 时,令前 $$k+1$$ 个字符按 ababab... 交替排列,剩余 $$n-k-1$$ 个字符填充为第 $$k+1$$ 个字符(即与交替段末尾相同)。只用了 a 和 b 两种字符,所以任何长度 $$\ge 3$$ 的子串必然包含重复字母(3 个位置只有 2 种字符,根据鸽巢原理至少有两个相同),因此 $$L=2$$。交替段中恰好有 $$k$$ 个相邻不同字符对(即 $$k$$ 个最长非重复子串),填充段内部字符相同不产生新的非重复子串,填充段与交替段衔接处字符也相同。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,构造字符串需遍历 $$n$$ 个位置。
  • 空间复杂度:$$O(n)$$,存储结果字符串。

Go

// 最长非重复子串 - 构造
package main

import (
        "bufio"
        "fmt"
        "os"
)

func solve(n, k int) string {
        if k == n {
                // k=n 时全同字符,L=1,每个位置都是最长非重复子串
                s := make([]byte, n)
                for i := range s {
                        s[i] = 'a'
                }
                return string(s)
        }
        // k<n 时前 k+1 个字符交替 ab 产生恰好 k 个不同相邻对
        s := make([]byte, n)
        length := k + 1
        if length > n {
                length = n
        }
        for i := 0; i < length; i++ {
                if i%2 == 0 {
                        s[i] = 'a'
                } else {
                        s[i] = 'b'
                }
        }
        // 剩余位置填充交替段末尾字符,不产生新的不同对
        pad := s[length-1]
        for i := length; i < n; i++ {
                s[i] = pad
        }
        return string(s)
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var n, k int
        fmt.Fscan(reader, &n, &k)
        fmt.Fprintln(writer, solve(n, k))
}

第三题:我即唯一

在线评测链接:https://www.neituiya.com/oj/10/2501

题目描述

AK机正在周游世界,她来到了天才之国,这里一共有 $$n$$ 个城市,编号为 $$1, 2, \ldots, n$$。她早早做好了规划:当她来到城市 $$i$$ 时,下一次会去到城市 $$a_i$$。同时,她会获得价值:如果这是她第 $$t$$ 次来到城市 $$i$$,那么这一次会获得 $$t \times i$$ 的价值。

AK机提出了 $$q$$ 个问题。每个问题给出两个整数 $$p, k$$,表示她从城市 $$p$$ 出发,并且一共"来到城市" $$k$$ 次(包含一开始就在 $$p$$ 的那次)。请你计算她获得的总价值。特别注意:第 $$1$$ 次来到的城市就是起点 $$p$$。

输入描述

第一行输入两个整数 $$n, q(1 \le n, q \le 2 \times 10^5)$$,分别表示城市数量与询问数量。第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,其中 $$a_i$$ 表示从城市 $$i$$ 下一次会到达的城市编号。此后 $$q$$ 行,每行输入两个整数 $$p, k(1 \le p \le n, 1 \le k \le 10^9)$$,含义见题目描述。

输出描述

对于每个询问,新起一行输出一个整数,表示总价值对 $$10^9 + 7$$ 取模后的结果。

样例1

输入

3 3
2 3 3
1 1
1 4
3 3

输出

1
12
18

样例解释

对于询问 $$p=1, k=1$$:只来到城市 $$1$$ 这一次,获得 $$1 \times 1 = 1$$。

对于询问 $$p=1, k=4$$:来到城市的顺序为 $$1, 2, 3, 3$$。其中城市 $$1$$ 来了 $$1$$ 次,贡献为 $$1$$;城市 $$2$$ 来了 $$1$$ 次,贡献为 $$2$$;城市 $$3$$ 来了 $$2$$ 次,贡献为 $$3 \times 1 + 3 \times 2 = 9$$。总价值为 $$1 + 2 + 9 = 12$$。

对于询问 $$p=3, k=3$$:来到城市的顺序为 $$3, 3, 3$$,总价值为 $$3 \times 1 + 3 \times 2 + 3 \times 3 = 18$$。

样例2

输入

4 2
2 1 4 3
1 5
3 2

输出

12
7

题解

本题涉及到快速幂,不熟悉该算法的同学可以先做一下模板题:

快速幂

题目内容拆解

每个城市有唯一后继,从起点出发走 $$k$$ 步($$k$$ 可达 $$10^9$$),求每次访问城市 $$i$$ 第 $$t$$ 次获得 $$t \times i$$ 价值的总和。$$n, q \le 2 \times 10^5$$,$$k \le 10^9$$,暴力模拟 $$O(k)$$ 不可行。核心观察:每个城市只有一个"下一站",所以从任何城市出发一直走,因为城市总数只有 $$n$$ 个,最终一定会走到一个之前去过的城市,从此进入循环。路径形状像希腊字母 $$\rho$$:一条直线(尾部)接一个圆圈(环),→ 因此采用环检测+前缀和加速计算。

算法实现

算法主策略:预处理所有环结构和环上前缀和,每次查询将路径拆分为尾部(进入环前的链)和环部分,分别计算贡献。

环检测:使用三色标记法遍历所有节点。未访问为白色,当前路径为灰色,已处理为黑色。沿后继指针行走时遇到灰色节点即发现新环,从该节点到路径末尾构成环,之前的节点为尾部。

查询处理:从起点 $$p$$ 出发沿后继指针走,直到遇到环上节点,收集尾部城市。若 $$k$$ 不超过尾部长度,答案即为前 $$k$$ 个尾部城市编号之和。否则,设剩余步数为 $$r = k - \text{tail\_len}$$,环长为 $$L$$,则完整绕环 $$f = \lfloor r/L \rfloor$$ 次,余 $$m = r \bmod L$$ 步。

贡献计算:城市 $$c$$ 被访问 $$v$$ 次,第 $$1, 2, \ldots, v$$ 次分别获得 $$1 \cdot c, 2 \cdot c, \ldots, v \cdot c$$,总贡献为 $$c \cdot (1+2+\cdots+v) = c \cdot \frac{v(v+1)}{2}$$。环上从入口开始的前 $$m$$ 个城市各被访问 $$f+1$$ 次,其余 $$L-m$$ 个城市各被访问 $$f$$ 次。设环城市编号总和为 $$S$$,前 $$m$$ 个城市编号和为 $$S_m$$(通过预处理的前缀和 $$O(1)$$ 查询,环绕时拼接两段),代入贡献公式得总贡献为 $$S_m \cdot \frac{(f+1)(f+2)}{2} + (S - S_m) \cdot \frac{f(f+1)}{2}$$。

公式中的除以 $$2$$ 不能在取模后直接做除法(取模后的数没有普通除法),需要用模逆元替代:$$a / 2 \bmod p$$ 等价于 $$a \cdot 2^{-1} \bmod p$$。根据费马小定理,当 $$p$$ 是质数时 $$2^{-1} = 2^{p-2} \bmod p$$,用快速幂即可算出。

时空复杂度分析

  • 时间复杂度:$$O(n + \sum \text{tail\_len})$$,预处理环结构 $$O(n)$$,每次查询 $$O(\text{tail\_len})$$ 追踪尾部,环上计算 $$O(1)$$。
  • 空间复杂度:$$O(n)$$,存储图、环信息和前缀和数组。

Go

// 我即唯一 - 函数图环检测+前缀和
package main

import (
        "bufio"
        "fmt"
        "os"
)

const MOD = 1_000_000_007

// 快速幂:计算 base^exp % mod,用于求模逆元
func power(base, exp, mod int64) int64 {
        res := int64(1)
        base %= mod
        for exp > 0 {
                if exp&1 == 1 {
                        res = res * base % mod
                }
                base = base * base % mod
                exp >>= 1
        }
        return res
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var n, q int
        fmt.Fscan(reader, &n, &q)

        a := make([]int, n+1)
        for i := 1; i <= n; i++ {
                fmt.Fscan(reader, &a[i])
        }

        // 2 的模逆元,用于把 /2 转化为 *inv2
        inv2 := power(2, MOD-2, MOD)

        // 三色标记法找所有环:0=未访问 1=当前路径 2=已完成
        color := make([]int, n+1)
        onCycle := make([]bool, n+1)
        cycleId := make([]int, n+1)
        cyclePos := make([]int, n+1)
        for i := range cycleId {
                cycleId[i] = -1
        }
        var cycles [][]int

        for start := 1; start <= n; start++ {
                if color[start] != 0 {
                        continue
                }
                var path []int
                node := start
                // 沿后继指针走,记录路径
                for color[node] == 0 {
                        color[node] = 1
                        path = append(path, node)
                        node = a[node]
                }
                if color[node] == 1 {
                        // 遇到灰色节点说明找到新环,定位环起点
                        idx := 0
                        for i, v := range path {
                                if v == node {
                                        idx = i
                                        break
                                }
                        }
                        cid := len(cycles)
                        cycle := make([]int, len(path)-idx)
                        copy(cycle, path[idx:])
                        cycles = append(cycles, cycle)
                        // 标记环上每个节点的环编号和位置
                        for j, v := range cycle {
                                onCycle[v] = true
                                cycleId[v] = cid
                                cyclePos[v] = j
                                color[v] = 2
                        }
                        for i := 0; i < idx; i++ {
                                color[path[i]] = 2
                        }
                } else {
                        for _, v := range path {
                                color[v] = 2
                        }
                }
        }

        // 预处理每个环的城市编号前缀和
        cp := make([][]int64, len(cycles))
        ct := make([]int64, len(cycles))
        for cid, cycle := range cycles {
                L := len(cycle)
                cp[cid] = make([]int64, L+1)
                for j := 0; j < L; j++ {
                        cp[cid][j+1] = (cp[cid][j] + int64(cycle[j])) % MOD
                }
                ct[cid] = cp[cid][L]
        }

        // 环上区间和,支持环绕
        cycleSum := func(cid, sp, length int) int64 {
                if length == 0 {
                        return 0
                }
                L := len(cycles[cid])
                ep := sp + length
                if ep <= L {
                        return (cp[cid][ep] - cp[cid][sp] + MOD) % MOD
                }
                // 环绕时拼接:尾段 + 头段
                return (cp[cid][L] - cp[cid][sp] + cp[cid][ep-L] + MOD) % MOD
        }

        for ; q > 0; q-- {
                var p int
                var k int64
                fmt.Fscan(reader, &p, &k)

                // 沿路径走到环入口,收集尾部城市
                tailSum := int64(0)
                tailLen := int64(0)
                node := p
                for !onCycle[node] {
                        tailSum = (tailSum + int64(node)) % MOD
                        tailLen++
                        if tailLen == k {
                                break
                        }
                        node = a[node]
                }

                // k 步全在尾部
                if tailLen >= k {
                        fmt.Fprintln(writer, tailSum)
                        continue
                }

                // 进入环后:计算完整绕环次数和余数
                remaining := k - tailLen
                cid := cycleId[node]
                sp := cyclePos[node]
                L := int64(len(cycles[cid]))

                full := remaining / L
                rem := remaining % L

                S := ct[cid]
                Sr := cycleSum(cid, sp, int(rem))

                f := full % MOD
                // 前 rem 个城市被访问 full+1 次
                cf := Sr % MOD * ((f + 1) % MOD) % MOD * ((f + 2) % MOD) % MOD * inv2 % MOD
                // 后 L-rem 个城市被访问 full 次
                sRest := (S - Sr%MOD + MOD) % MOD
                cb := sRest * (f % MOD) % MOD * ((f + 1) % MOD) % MOD * inv2 % MOD

                ans := (tailSum + cf + cb) % MOD
                fmt.Fprintln(writer, ans)
        }
}

2026-3-28-研发岗

第一题:风不吹雨

在线评测链接:https://www.neituiya.com/oj/10/2405

题目描述

给定一个长度为 $$n$$ 的整数序列 $$\{a_1, a_2, \ldots, a_n\}$$。

你可以对序列中的某些位置做操作。每个位置最多做一次操作 $$1$$,最多做一次操作 $$2$$(两种都做也可以,顺序任意),也可以完全不操作。两种操作如下:

操作 $$1$$:选择一个位置 $$i$$,把 $$a_i$$ 变为 $$\left\lfloor \dfrac{a_i}{2} \right\rfloor$$(也就是除以 $$2$$ 再向下取整)。

操作 $$2$$:选择一个位置 $$i$$,把 $$a_i$$ 变为 $$a_i - k$$(注意:允许变成负数)。

你最多可以执行操作 $$1$$ 共 $$a$$ 次,最多可以执行操作 $$2$$ 共 $$b$$ 次。请你选好如何操作,使最终序列所有元素之和尽可能小,输出这个最小可能的和。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入四个整数 $$n, a, b, k(1 \le n \le 2 \times 10^5, 0 \le a \le n, 0 \le b \le n, 1 \le k \le 10^9)$$。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^9)$$。

除此之外,保证单个测试文件中所有测试数据的 $$n$$ 之和不超过 $$4 \times 10^5$$。

输出描述

对于每组测试数据,新起一行输出一个整数,表示最小可能的最终元素和(这个数可能为负数)。

样例1

输入

3
3 1 1 3
5 1 7
1 1 1 5
9
1 0 1 10
3

输出

6
-1
-7

样例解释

对于第三组数据:$$n = 1, a = 0, b = 1, k = 10$$,序列为 $$\{3\}$$。只能做一次操作 $$2$$:$$3 \rightarrow -7$$,最终元素和为 $$-7$$。

题解

题目内容拆解

每个元素可独立选择是否执行操作1(除2取整)和操作2(减k),两种操作分别有全局次数限制,求最小总和。

算法实现

算法主策略:本题采用贪心策略,关键观察是两种操作的收益可以独立计算。

操作2对任何元素的收益都是固定的 $$k$$,所以无论分配给哪个元素,总收益都是 $$b \times k$$。操作1对元素 $$a_i$$ 的收益是 $$\lceil a_i / 2 \rceil$$,收益因元素而异。

因此最优策略是:将操作1分配给 $$\lceil a_i / 2 \rceil$$ 最大的前 $$a$$ 个元素,操作2随意分配给任意 $$b$$ 个元素。最终答案为 $$\sum a_i$$ 减去前 $$a$$ 大的 $$\lceil a_i / 2 \rceil$$ 之和,再减去 $$b \times k$$。

为什么操作顺序不影响结果? 对同一个元素同时使用两种操作时,先操作1再操作2得到 $$\lfloor a_i/2 \rfloor - k$$,先操作2再操作1得到 $$\lfloor (a_i - k)/2 \rfloor$$。可以证明前者 $$\le$$ 后者,所以最优总是先除后减。此时操作1的收益 $$= a_i - \lfloor a_i/2 \rfloor = \lceil a_i/2 \rceil$$,操作2的收益 $$= k$$,两者完全独立。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,每组数据需要排序。总时间 $$O(\sum n \cdot \log n)$$。
  • 空间复杂度:$$O(n)$$,存储收益数组。

Go

// 风不吹雨 - 贪心排序
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

// 计算最小元素和
func solve(n, a, b int, k int64, arr []int64) int64 {
        var total int64
        gains := make([]int64, n)
        for i := 0; i < n; i++ {
                total += arr[i]
                // 操作1的收益:ceil(arr[i] / 2)
                gains[i] = (arr[i] + 1) / 2
        }
        sort.Slice(gains, func(i, j int) bool {
                return gains[i] > gains[j]
        })
        // 取前a大的收益
        for i := 0; i < a; i++ {
                total -= gains[i]
        }
        // 操作2恒减k,共b次
        total -= int64(b) * k
        return total
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n, a, b int
                var k int64
                fmt.Fscan(reader, &n, &a, &b, &k)
                arr := make([]int64, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &arr[i])
                }
                fmt.Fprintln(writer, solve(n, a, b, k, arr))
        }
}

第二题:交替子序列

在线评测链接:https://www.neituiya.com/oj/10/2406

题目描述

给定一个长度为 $$n$$ 的整数数组 $$a_1, a_2, \cdots, a_n$$。

你需要从中选出一个子序列 $$b_1, b_2, \cdots, b_m$$(保持相对顺序,不一定连续),使得目标值

$$F(b) = \sum_{i=1}^{m} (-1)^{i-1} b_i$$

最大。

子序列为从原序列中删除任意个(可以为零,可以为全部)元素得到的新序列。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 2 \times 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$。

第二行输入 $$n$$ 个整数 $$a_1, \ldots, a_n(|a_i| \le 10^6)$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,输出一行一个整数,表示最大可能的 $$F(b)$$。

样例1

输入

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

输出

3
14
5

样例解释

对于第一组数据:$$a = [1, 2, 3]$$,选子序列 $$[3]$$,$$F = 3$$。

对于第二组数据:$$a = [-1, 2, -3, 4, -5]$$,选子序列 $$[2, -3, 4, -5]$$,$$F = 2 - (-3) + 4 - (-5) = 14$$。

对于第三组数据:$$a = [5, 5, 5, 5]$$,选子序列 $$[5]$$,$$F = 5$$。

题解

题目内容拆解

从数组中选子序列使 $$F(b) = b_1 - b_2 + b_3 - \cdots$$ 最大。子序列中奇数位取正号、偶数位取负号,本质是维护两种结尾状态的DP。

算法实现

状态方程定义

$$odd[i]$$:以 $$a[i]$$ 结尾、且 $$a[i]$$ 处于子序列奇数位(正贡献)时的最大 $$F$$ 值。

$$even[i]$$:以 $$a[i]$$ 结尾、且 $$a[i]$$ 处于子序列偶数位(负贡献)时的最大 $$F$$ 值。

辅助变量 $$prefix\_max\_odd$$ 和 $$prefix\_max\_even$$ 分别记录 $$odd[0 \sim i-1]$$ 和 $$even[0 \sim i-1]$$ 的前缀最大值,用于加速转移。

状态方程初始化

$$odd[i] = -\infty$$,$$even[i] = -\infty$$($$0 \le i < n$$),表示初始时没有任何子序列以该元素结尾。$$prefix\_max\_odd = -\infty$$,$$prefix\_max\_even = -\infty$$。

状态方程转移

从左到右遍历每个元素 $$a[i]$$,考虑将其加入子序列:

选 $$a[i]$$ 放在奇数位(正号):可以新开子序列 $$F = a[i]$$,也可以接在之前某个偶数位结尾的最优子序列后面 $$F = prefix\_max\_even + a[i]$$。因此 $$odd[i] = \max(a[i],\ prefix\_max\_even + a[i])$$。

选 $$a[i]$$ 放在偶数位(负号):必须接在之前某个奇数位结尾的最优子序列后面,$$even[i] = prefix\_max\_odd - a[i]$$。

转移后更新前缀最大值:$$prefix\_max\_odd = \max(prefix\_max\_odd, odd[i])$$,$$prefix\_max\_even = \max(prefix\_max\_even, even[i])$$。

最终答案 $$= \max(0, \max_{i}(odd[i]), \max_{i}(even[i]))$$,其中 $$0$$ 对应空子序列。

样例推导($$a = [-1, 2, -3, 4, -5]$$):

$$i=0$$,$$a[0]=-1$$:$$odd[0] = -1$$,$$even[0] = -\infty$$。$$prefix\_max\_odd = -1$$,$$prefix\_max\_even = -\infty$$。

$$i=1$$,$$a[1]=2$$:$$odd[1] = \max(2, -\infty) = 2$$,$$even[1] = -1 - 2 = -3$$。$$prefix\_max\_odd = 2$$,$$prefix\_max\_even = -3$$。

$$i=2$$,$$a[2]=-3$$:$$odd[2] = \max(-3, -3 + (-3)) = -3$$,$$even[2] = 2 - (-3) = 5$$。$$prefix\_max\_odd = 2$$,$$prefix\_max\_even = 5$$。

$$i=3$$,$$a[3]=4$$:$$odd[3] = \max(4, 5 + 4) = 9$$,$$even[3] = 2 - 4 = -2$$。$$prefix\_max\_odd = 9$$,$$prefix\_max\_even = 5$$。

$$i=4$$,$$a[4]=-5$$:$$odd[4] = \max(-5, 5 + (-5)) = 0$$,$$even[4] = 9 - (-5) = 14$$。$$prefix\_max\_odd = 9$$,$$prefix\_max\_even = 14$$。

答案 $$= \max(0, \max(-1, 2, -3, 9, 0), \max(-\infty, -3, 5, -2, 14)) = \max(0, 9, 14) = 14$$,对应子序列 $$[2, -3, 4, -5]$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,每组数据线性扫描一遍。总时间 $$O(\sum n)$$。
  • 空间复杂度:$$O(n)$$,存储 $$odd$$ 和 $$even$$ 数组。

Go

// 交替子序列 - 贪心DP
package main

import (
        "bufio"
        "fmt"
        "math"
        "os"
)

// 计算最大交替子序列值
func solve(n int, a []int) int64 {
        // odd[i]: 以a[i]结尾、a[i]处于奇数位(正贡献)时的最大F值
        // even[i]: 以a[i]结尾、a[i]处于偶数位(负贡献)时的最大F值
        odd := make([]int64, n)
        even := make([]int64, n)
        for i := 0; i < n; i++ {
                odd[i] = math.MinInt64
                even[i] = math.MinInt64
        }
        var prefixMaxOdd, prefixMaxEven int64 = math.MinInt64, math.MinInt64

        for i := 0; i < n; i++ {
                x := int64(a[i])
                // a[i]放在奇数位:新开子序列,或接在偶数位后面
                if prefixMaxEven == math.MinInt64 {
                        odd[i] = x
                } else {
                        odd[i] = prefixMaxEven + x
                        if x > odd[i] {
                                odd[i] = x
                        }
                }
                // a[i]放在偶数位:必须接在奇数位后面
                if prefixMaxOdd != math.MinInt64 {
                        even[i] = prefixMaxOdd - x
                }
                // 更新前缀最大值,供后续元素转移使用
                if odd[i] > prefixMaxOdd {
                        prefixMaxOdd = odd[i]
                }
                if even[i] > prefixMaxEven {
                        prefixMaxEven = even[i]
                }
        }

        // 空子序列F=0,取所有状态的最大值
        var ans int64 = 0
        for i := 0; i < n; i++ {
                if odd[i] != math.MinInt64 && odd[i] > ans {
                        ans = odd[i]
                }
                if even[i] != math.MinInt64 && even[i] > ans {
                        ans = even[i]
                }
        }
        return ans
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var T int
        fmt.Fscan(reader, &T)
        for ; T > 0; T-- {
                var n int
                fmt.Fscan(reader, &n)
                a := make([]int, n)
                for i := 0; i < n; i++ {
                        fmt.Fscan(reader, &a[i])
                }
                fmt.Fprintln(writer, solve(n, a))
        }
}

第三题:AK机的区间

在线评测链接:https://www.neituiya.com/oj/10/2407

题目描述

AK机喜欢研究整数序列在区间上的性质。

她手中有一个长度为 $$n$$ 的序列 $$v_1, v_2, \ldots, v_n$$。

她想统计序列中有多少个区间长度大于 $$1$$ 的区间满足区间最大值小于区间左右端点值之和。即有多少区间 $$[i, j]$$($$1 \le i < j \le n$$)满足:

$$v_i + v_j > \max_{k=i}^{j} v_k$$

输入描述

第一行输入一个整数 $$n(1 \le n \le 10^5)$$,表示序列长度。

第二行输入 $$n$$ 个整数 $$v_1, v_2, \ldots, v_n(1 \le v_i \le 10^6)$$,表示序列元素。

输出描述

输出满足区间长度大于 $$1$$ 且 $$v_i + v_j > \max_{k=i}^{j} v_k$$ 的区间 $$[i, j]$$ 的数量。

样例1

输入

5
1 2 3 4 5

输出

10

样例解释

序列为 $$\{1, 2, 3, 4, 5\}$$。任意长度大于 $$1$$ 的区间 $$[i, j]$$,最大值均为 $$v_j$$(递增序列)。因此 $$v_i + v_j > v_j$$ 恒成立($$v_i \ge 1$$),共有 $$10$$ 个区间满足条件。

题解

题目内容拆解

统计所有区间 $$[i,j]$$($$i < j$$)使得两端点之和大于区间最大值。关键观察:当最大值是端点之一时,条件恒成立;只有当最大值是内部元素时才需要检查。

算法实现

算法主策略:本题采用分治 + 稀疏表RMQ

核心思路:对任意区间 $$[l, r]$$,找到区间最大值位置 $$m$$。所有跨越 $$m$$ 的区间对 $$(i, j)$$($$i < m < j$$),其区间最大值就是 $$v[m]$$,条件变为 $$v[i] + v[j] > v[m]$$。而端点包含 $$m$$ 的对 $$(i, m)$$ 或 $$(m, j)$$,条件 $$v[i] + v[m] > v[m]$$ 即 $$v[i] > 0$$ 恒成立。

分治步骤

  1. 用稀疏表预处理 $$O(1)$$ 查询区间最大值位置。
  2. 对区间 $$[l, r]$$,找最大值位置 $$m$$。含端点 $$m$$ 的合法对数 $$= (m - l) + (r - m)$$。

3) 对跨 $$m$$ 的对,选短边遍历、长边排序后二分,统计满足 $$v[i] + v[j] > v[m]$$ 即 $$v[j] > v[m] - v[i]$$ 的对数。

4) 递归处理 $$[l, m-1]$$ 和 $$[m+1, r]$$。

为什么选短边? 每个元素被选为"短边"的次数总和是 $$O(n \log n)$$(类似启发式合并),所以整体复杂度可控。

样例推导($$v = [1, 2, 3, 4, 5]$$):

最大值在位置 $$5$$($$v[5]=5$$),含端点 $$5$$ 的对:$$(1,5), (2,5), (3,5), (4,5)$$,共 $$4$$ 对。跨 $$5$$ 的对不存在($$5$$ 是右端点)。递归 $$[1,4]$$:最大值位置 $$4$$,含 $$4$$ 的对 $$3$$ 个,跨 $$4$$ 的对不存在。递归 $$[1,3]$$:最大值位置 $$3$$,含 $$3$$ 的对 $$2$$ 个。递归 $$[1,2]$$:$$1$$ 个。总计 $$4+3+2+1 = 10$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log^2 n)$$。分治深度 $$O(\log n)$$,每层短边遍历+二分排序总工作量 $$O(n \log n)$$。
  • 空间复杂度:$$O(n \log n)$$,稀疏表存储。

Go

// AK机的区间 - 分治+RMQ
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
)

var (
        v      []int
        sparse [][]int
        LOG    []int
        ans    int64
)

func buildSparse(n int) {
        LOG = make([]int, n+1)
        for i := 2; i <= n; i++ {
                LOG[i] = LOG[i/2] + 1
        }
        K := LOG[n] + 1
        sparse = make([][]int, K)
        for j := 0; j < K; j++ {
                sparse[j] = make([]int, n)
        }
        for i := 0; i < n; i++ {
                sparse[0][i] = i
        }
        for j := 1; j < K; j++ {
                for i := 0; i+(1<<j)-1 < n; i++ {
                        l := sparse[j-1][i]
                        r := sparse[j-1][i+(1<<(j-1))]
                        if v[l] >= v[r] {
                                sparse[j][i] = l
                        } else {
                                sparse[j][i] = r
                        }
                }
        }
}

func queryMax(l, r int) int {
        k := LOG[r-l+1]
        li := sparse[k][l]
        ri := sparse[k][r-(1<<k)+1]
        if v[li] >= v[ri] {
                return li
        }
        return ri
}

func dc(l, r int) {
        if l >= r {
                return
        }
        m := queryMax(l, r)
        ans += int64(m-l) + int64(r-m)

        leftLen := m - l
        rightLen := r - m
        if leftLen <= rightLen {
                // 收集右侧值并排序
                rv := make([]int, rightLen)
                for j := m + 1; j <= r; j++ {
                        rv[j-m-1] = v[j]
                }
                sort.Ints(rv)
                // 遍历左侧
                for i := l; i < m; i++ {
                        threshold := v[m] - v[i]
                        // upper_bound: 第一个 > threshold
                        pos := sort.SearchInts(rv, threshold+1)
                        ans += int64(len(rv) - pos)
                }
        } else {
                // 收集左侧值并排序
                lv := make([]int, leftLen)
                for i := l; i < m; i++ {
                        lv[i-l] = v[i]
                }
                sort.Ints(lv)
                // 遍历右侧
                for j := m + 1; j <= r; j++ {
                        threshold := v[m] - v[j]
                        pos := sort.SearchInts(lv, threshold+1)
                        ans += int64(len(lv) - pos)
                }
        }
        dc(l, m-1)
        dc(m+1, r)
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        var n int
        fmt.Fscan(reader, &n)
        v = make([]int, n)
        for i := 0; i < n; i++ {
                fmt.Fscan(reader, &v[i])
        }
        buildSparse(n)
        ans = 0
        dc(0, n-1)
        fmt.Println(ans)
}

2026.3.21-研发岗

第一题:无限循环

在线评测链接;https://www.neituiya.com/oj/10/2375

题目描述

AK机有一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \ldots, a_n\}$$,她为了研究这个数组做出了个大胆的决定。现在,将与初始数组完全相同的数组连续拼接到其末尾,共拼接 $$10^9$$ 次。设拼接完成后的新数组记为 $$a'$$,则新数组的长度为 $$n \times (10^9 + 1)$$,并且对于任意的 $$n < i \le n \times (10^9 + 1)$$,都有 $$a'_i = a'_{i-n}$$。

请你计算新数组 $$a'$$ 的最长严格递增子序列的长度,并输出这个长度。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示原数组的长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,表示原数组的元素。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示新数组的最长严格递增子序列长度。

样例1

输入

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

输出

3
3

样例解释

对于第 $$1$$ 组,最终最长严格递增子序列为 $$\{1, 2, 3\}$$。

题解

题目内容拆解

将原数组无限复制后求最长严格递增子序列(LIS)。$$a_i \le n$$,意味着值域只有 $$n$$ 种不同的值。

算法实现

算法主策略:关键观察有两点。第一,严格递增子序列中每个值最多出现一次,而 $$a_i \le n$$,所以 LIS 长度不超过原数组中不同值的个数 $$k$$。第二,数组被复制了 $$10^9 + 1$$ 次,对于原数组中存在的任意 $$k$$ 个不同值 $$v_1 < v_2 < \ldots < v_k$$,我们可以在第 $$1$$ 份副本中选一个 $$v_1$$,第 $$2$$ 份副本中选一个 $$v_2$$,以此类推——由于副本之间下标严格递增,这构成一个长度为 $$k$$ 的严格递增子序列。因此答案恰好等于原数组中不同值的个数

以样例验证:$$[1, 1, 2, 3]$$ 有 $$3$$ 种不同值 $$\{1, 2, 3\}$$,答案 $$3$$。$$[4, 5, 3, 3, 4]$$ 有 $$3$$ 种不同值 $$\{3, 4, 5\}$$,答案 $$3$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历数组一次统计不同值。
  • 空间复杂度:$$O(n)$$,存储集合。

C++

// 无限循环 - 计数去重
#include <bits/stdc++.h>
using namespace std;

int solve(int n, vector<int>& a) {
    // 无限拼接后,每种不同值都能在不同副本中被选到
    // 严格递增子序列长度 = 不同值的个数
    set<int> s(a.begin(), a.end());
    return s.size();
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<int> a(n);
        for (int i = 0; i < n; i++) cin >> a[i];
        cout << solve(n, a) << "\n";
    }
    return 0;
}

第二题:交换括号

在线评测链接;https://www.neituiya.com/oj/10/2376

题目描述

称一个括号序列为"平衡的括号序列",当且仅当满足以下归纳定义:

  1. 空串是平衡的。
  2. 若字符串 $$A$$ 是平衡的,则 $$(A)$$ 是平衡的。

3) 若字符串 $$A$$ 与 $$B$$ 均是平衡的,则 $$AB$$ 是平衡的(表示连接)。

例如:括号序列 $$()()$$ 与 $$(())$$ 是平衡的;而 $$)$$、$$)($$、$$($$ 不是。

给定一个偶数长度的括号序列 $$s$$(仅包含 ())。你可以进行若干次如下操作:选择一个位置 $$i(1 \le i < n)$$,交换相邻的两个字符 $$s_i$$ 与 $$s_{i+1}$$。

请你计算,最少需要进行多少次这样的相邻交换,才能使整个序列变为一个平衡的括号序列。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个偶数 $$n(2 \le n \le 2 \times 10^5)$$。

第二行输入一个长度为 $$n$$ 的字符串 $$s$$,仅包含 ()

保证所有测试中 $$n$$ 的总和不超过 $$2 \times 10^5$$,保证每组数据一定可以通过相邻交换变为平衡序列。

输出描述

对于每组测试数据,输出一行一个整数,表示将 $$s$$ 变为平衡括号序列所需的最少相邻交换次数。

样例1

输入

3
2
)(
4
()()
4
))((

输出

1
0
3

样例解释

样例一:交换位置 $$1, 2$$ 的字符,)( 变为 (),$$1$$ 次操作。

样例二:已经是平衡序列,$$0$$ 次。

样例三:$$))(($$需要 $$3$$ 次相邻交换变为 $$()()$$。

题解

题目内容拆解

给定偶数长度的括号序列,求最少相邻交换次数使其平衡。$$n \le 2 \times 10^5$$,需要 $$O(n)$$ 算法。

算法实现

算法主策略:从左到右扫描,维护 $$balance$$(当前未匹配的 ( 个数)。每当遇到 ) 使 $$balance$$ 变负,说明前缀中 )( 多了 $$|balance|$$ 个,需要从右边"搬运" ( 过来,代价恰好是 $$|balance|$$。

为什么代价是 $$|balance|$$:$$balance$$ 变为 $$-k$$ 说明当前位置是第 $$k$$ 个"多余的 )"。这个 ) 需要和它右边某个 ( 交换位置,而前面已经有 $$k-1$$ 个多余 ) 占据了更左的位置,所以这个 ) 必须跨越 $$k$$ 个位置才能和 ( 配对。

以样例验证:$$))(($$中,第 $$1$$ 个 ) 使 $$balance = -1$$,代价 $$1$$;第 $$2$$ 个 \`)$$ 使 $$balance = -2$$,代价 $$2$$;总计 $$1 + 2 = 3。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,单次遍历。
  • 空间复杂度:$$O(1)$$,只需常数变量。

C++

// 交换括号 - 贪心
#include <bits/stdc++.h>
using namespace std;

long long solve(const string& s) {
    int balance = 0;
    long long swaps = 0;
    for (char c : s) {
        if (c == '(') {
            balance++;
        } else {
            balance--;
            // 每当 ')' 使 balance 变负,累加当前缺口大小
            if (balance < 0) swaps += (-balance);
        }
    }
    return swaps;
}

int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        string s;
        cin >> n >> s;
        cout << solve(s) << "\n";
    }
    return 0;
}

第三题:AK机的01树

在线评测链接;https://www.neituiya.com/oj/10/2377

题目描述

AK机有一颗节点编号为 $$1 \sim n$$ 的树,每个节点只有 $$\{0, 1\}$$ 这两种值之一。

设 $$u \to v$$ 为节点 $$u$$ 到节点 $$v$$ 的简单路径。$$g(u \to v)$$ 为从 $$u$$ 开始到 $$v$$ 结束的简单路径上经过的所有点(包括 $$u, v$$)按照先后顺序组成的 $$01$$ 字符串对应的十进制对 $$10^9 + 7$$ 取模的结果。例如,简单路径经过所有节点组成的字符串为 01101,其对应十进制就是 $$13$$,因此 $$g(u \to v) = 13 \mod (10^9 + 7) = 13$$。

AK机会进行 $$m$$ 次以下操作:

  1. 操作 $$1$$:将简单路径 $$u \to v$$ 上所有节点的值反置($$0$$ 变 $$1$$,$$1$$ 变 $$0$$)。
  2. 操作 $$2$$:询问 $$g(u \to v)$$ 的值。

你需要对AK机的每一个操作 $$2$$ 进行回答。

输入描述

第一行输入两个整数 $$n, m(1 \le n, m \le 2 \times 10^5)$$,表示树的大小以及操作次数。

第二行输入 $$n$$ 个整数 $$a_i(a_i \in \{0, 1\})$$,表示第 $$i$$ 个节点初始的值。

接下来 $$n-1$$ 行,每一行输入两个整数 $$u_i, v_i(1 \le u_i, v_i \le n)$$,表示节点 $$u_i$$ 与 $$v_i$$ 之间有一条边。

接下来 $$m$$ 行,每一行输入三个整数 $$x, u, v(x \in \{1, 2\}, 1 \le u, v \le n)$$,当 $$x = 1$$ 时执行路径翻转,当 $$x = 2$$ 时查询 $$g(u \to v)$$。

输出描述

对于每个操作 $$2$$,在一行上输出一个整数,表示 $$g(u \to v)$$ 的值。

样例1

输入

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

输出

0
1
7

样例解释

第一次询问时得到的字符串为 000,第二次询问得到的字符串为 001,第三次询问得到的字符串为 111

题解

题目内容拆解

树上两种操作:路径翻转(0变1、1变0)和路径查询(节点值组成01串转十进制 mod $$10^9 + 7$$)。$$n, m \le 2 \times 10^5$$,暴力 $$O(nm)$$ 会超时,需要用树链剖分(HLD)+ 线段树将每次操作优化到 $$O(\log^2 n)$$。

核心难点有两个:一是路径翻转需要线段树支持区间翻转的懒标记;二是查询 $$g(u \to v)$$ 要正确处理路径方向——$$g(u \to v)$$ 和 $$g(v \to u)$$ 的值不同,因为二进制串是反向的。

算法实现

算法主策略:先做 HLD 将树路径映射为连续区间,再用线段树维护每个区间的正向/反向二进制值,支持区间翻转和区间查询。

第一步:树链剖分。BFS 建树后,按子树大小找重儿子,沿重链分配连续编号。这样任意路径被拆成 $$O(\log n)$$ 条链段,每条链段在线段树上是连续区间。

第二步:线段树设计。每个节点存三个值:

$$val$$:区间正向读取的二进制十进制值(浅→深方向)

$$rval$$:区间反向读取的二进制十进制值(深→浅方向)

$$len$$:区间长度

合并规则:左子 $$L$$、右子 $$R$$ 合并时,$$val = L.val \times 2^{R.len} + R.val$$,$$rval = R.rval \times 2^{L.len} + L.rval$$。

翻转操作:长度为 $$len$$ 的区间翻转所有 bit 后,$$val' = (2^{len} - 1) - val$$,$$rval$$ 同理。用懒标记下传。

第三步:路径查询拼接。查询 $$g(u \to v)$$ 时,从 $$u$$ 和 $$v$$ 分别沿 HLD 链向 LCA 攀爬,收集链段:

$$u$$ 侧链段:每段是"深→浅"方向,取 $$rval$$(反向值),按攀爬顺序从 $$u$$ 向 LCA 拼接。

$$v$$ 侧链段:在最终路径中方向是"浅→深",取 $$val$$(正向值),按攀爬的逆序拼接到结果末尾。

同链段:根据 $$u, v$$ 的深浅决定取 $$val$$ 或 $$rval$$。

样例完整推导:树结构为 $$1$$ 连 $$2, 3$$,$$2$$ 连 $$4, 5$$。初始值全 $$0$$。

  1. 查询 $$g(1 \to 4)$$:路径 $$[1, 2, 4]$$,值 000 = $$0$$。输出 $$0$$。
  2. 翻转 $$1 \to 3$$:路径 $$[1, 3]$$,翻转后 $$val[1] = 1, val[3] = 1$$。

3) 查询 $$g(4 \to 1)$$:路径 $$[4, 2, 1]$$,值 001 = $$1$$。输出 $$1$$。

4) 翻转 $$2 \to 5$$:路径 $$[2, 5]$$,翻转后 $$val[2] = 1, val[5] = 1$$。

  1. 查询 $$g(5 \to 1)$$:路径 $$[5, 2, 1]$$,值 111 = $$7$$。输出 $$7$$。

时空复杂度分析

  • 时间复杂度:$$O(m \log^2 n)$$,每次操作拆成 $$O(\log n)$$ 条链段,每条链段的线段树操作 $$O(\log n)$$。
  • 空间复杂度:$$O(n)$$,存储 HLD 信息和线段树。

C++

// AK机的01树 - 树链剖分 + 线段树
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;
const int MAXN = 200005;

// ==================== 树结构 ====================
vector<int> ch[MAXN];   // ch[u]: 节点u的子节点列表(建树后)
int par[MAXN];          // par[u]: 节点u的父节点
int dep[MAXN];          // dep[u]: 节点u的深度(根为0)
int sz[MAXN];           // sz[u]: 以u为根的子树大小
int hvy[MAXN];          // hvy[u]: u的重儿子(子树最大的孩子),-1表示叶子
int top_c[MAXN];        // top_c[u]: u所在重链的链头节点
int pos_hld[MAXN];      // pos_hld[u]: u在HLD序(线段树)中的位置
int ori[MAXN];          // ori[i]: HLD序中第i个位置对应的原始节点编号
int nval[MAXN];         // nval[u]: 节点u的值(0或1)
int n, m, timer_hld;
long long pw2[MAXN];    // pw2[i]: 2^i mod MOD,预计算

// 建树 + HLD分解(全部迭代实现,避免栈溢出)
void build_tree() {
    // 第一步:BFS确定父子关系和深度
    vector<bool> vis(n + 1, false);
    vector<int> bfs_order;
    queue<int> q;
    q.push(1); vis[1] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        bfs_order.push_back(u);
        for (int v : ch[u]) {
            if (!vis[v]) {
                vis[v] = true;
                par[v] = u;
                dep[v] = dep[u] + 1;
                q.push(v);
            }
        }
    }
    // 去掉指向父节点的边,只保留子节点
    for (int i = 1; i <= n; i++) {
        vector<int> real_ch;
        for (int v : ch[i]) if (v != par[i]) real_ch.push_back(v);
        ch[i] = real_ch;
    }
    // 第二步:自底向上算子树大小,找重儿子(子树最大的孩子)
    for (int i = bfs_order.size() - 1; i >= 0; i--) {
        int u = bfs_order[i];
        sz[u] = 1; hvy[u] = -1;
        int mx = 0;
        for (int v : ch[u]) {
            sz[u] += sz[v];
            if (sz[v] > mx) { mx = sz[v]; hvy[u] = v; }
        }
    }
    // 第三步:HLD编号——重儿子优先DFS,保证同一重链上的节点编号连续
    timer_hld = 0;
    stack<pair<int,int>> stk;
    stk.push({1, 1}); // (节点, 链头)
    while (!stk.empty()) {
        auto [u, tp] = stk.top(); stk.pop();
        top_c[u] = tp;             // 记录u所在重链的链头
        pos_hld[u] = timer_hld;    // u在线段树中的位置
        ori[timer_hld] = u;        // 反向映射:位置→节点
        timer_hld++;
        // 轻儿子先入栈(后处理),重儿子后入栈(先处理,保持连续编号)
        for (int v : ch[u]) if (v != hvy[u]) stk.push({v, v}); // 轻儿子开新链
        if (hvy[u] != -1) stk.push({hvy[u], tp}); // 重儿子延续当前链
    }
}

// ==================== 线段树 ====================
// 每个节点维护一段HLD序区间的二进制值信息:
//   sv[nd]  = 正向值:把区间内节点值看作二进制串(左→右 = 浅→深),对应的十进制值
//   sr[nd]  = 反向值:把区间内节点值看作二进制串(右→左 = 深→浅),对应的十进制值
//   slen[nd] = 区间长度(二进制串位数)
//   sflip[nd] = 懒标记:是否需要翻转该区间所有bit
long long sv[4*MAXN], sr[4*MAXN];
int slen[4*MAXN];
bool sflip[4*MAXN];

// 由左右子节点合并出父节点的值
// 例:左子="01"(值1),右子="10"(值2) → 合并="0110"(值6)
// 正向:parent.val = left.val * 2^(right.len) + right.val
// 反向:parent.rval = right.rval * 2^(left.len) + left.rval
void merge_up(int nd) {
    int lc = 2*nd, rc = 2*nd+1;
    sv[nd] = (sv[lc] * pw2[slen[rc]] + sv[rc]) % MOD;
    sr[nd] = (sr[rc] * pw2[slen[lc]] + sr[lc]) % MOD;
    slen[nd] = slen[lc] + slen[rc];
}

// 翻转区间所有bit:0→1, 1→0
// 长度为len的二进制串X翻转后 = (111...1) - X = (2^len - 1) - X
void do_flip(int nd) {
    long long full = (pw2[slen[nd]] - 1 + MOD) % MOD; // 全1串 = 2^len - 1
    sv[nd] = (full - sv[nd] + MOD) % MOD;
    sr[nd] = (full - sr[nd] + MOD) % MOD;
    sflip[nd] = !sflip[nd]; // 懒标记取反(两次翻转等于没翻)
}

// 下推懒标记到子节点
void push_down(int nd) {
    if (sflip[nd]) {
        do_flip(2*nd); do_flip(2*nd+1);
        sflip[nd] = false;
    }
}

// 建线段树:每个叶子对应HLD序中一个节点的值
void build_seg(int nd, int l, int r) {
    sflip[nd] = false;
    if (l == r) {
        sv[nd] = sr[nd] = nval[ori[l]]; // 单个节点:正向=反向=节点值
        slen[nd] = 1;
        return;
    }
    int mid = (l+r)/2;
    build_seg(2*nd, l, mid);
    build_seg(2*nd+1, mid+1, r);
    merge_up(nd);
}

// 区间翻转 [ql, qr]
void flip_seg(int nd, int l, int r, int ql, int qr) {
    if (ql > r || qr < l) return;
    if (ql <= l && r <= qr) { do_flip(nd); return; }
    push_down(nd);
    int mid = (l+r)/2;
    flip_seg(2*nd, l, mid, ql, qr);
    flip_seg(2*nd+1, mid+1, r, ql, qr);
    merge_up(nd);
}

// 查询结果:正向值v、反向值rv、长度len
struct Seg { long long v, rv; int len; };

// 区间查询 [ql, qr],返回合并后的 Seg
Seg query_seg(int nd, int l, int r, int ql, int qr) {
    if (ql > r || qr < l) return {0, 0, 0}; // 空区间
    if (ql <= l && r <= qr) return {sv[nd], sr[nd], slen[nd]};
    push_down(nd);
    int mid = (l+r)/2;
    Seg L = query_seg(2*nd, l, mid, ql, qr);
    Seg R = query_seg(2*nd+1, mid+1, r, ql, qr);
    if (!L.len) return R;
    if (!R.len) return L;
    return {(L.v * pw2[R.len] + R.v) % MOD,   // 正向合并
            (R.rv * pw2[L.len] + L.rv) % MOD,  // 反向合并
            L.len + R.len};
}

// ==================== HLD 路径操作 ====================

// 路径翻转:翻转 u→v 路径上所有节点的值
void path_flip(int u, int v) {
    // 不断让链头更深的一方跳到链头的父节点,直到两者在同一条重链上
    while (top_c[u] != top_c[v]) {
        if (dep[top_c[u]] < dep[top_c[v]]) swap(u, v); // 让u的链头更深
        flip_seg(1, 0, n-1, pos_hld[top_c[u]], pos_hld[u]); // 翻转u所在链的[链头, u]段
        u = par[top_c[u]]; // u跳到链头的父节点
    }
    // 此时u和v在同一条链上,翻转它们之间的段
    if (dep[u] > dep[v]) swap(u, v); // 确保u是浅的那个
    flip_seg(1, 0, n-1, pos_hld[u], pos_hld[v]);
}

// 路径查询:计算 g(u→v) = 路径上节点值组成的01串的十进制值 mod MOD
//
// 核心难点:路径 u→LCA→v 在HLD中被拆成多段,每段方向不同:
//   u侧:从u向LCA攀爬,方向是 深→浅 = HLD序的反向 → 取 rval(反向值)
//   v侧:最终路径方向是 LCA→v = 浅→深 = HLD序的正向 → 取 val(正向值)
//   v侧的段要倒序拼接(因为攀爬时是从v向LCA收集的,顺序反了)
long long path_query(int u, int v) {
    vector<Seg> usegs, vsegs; // u侧收集的链段 / v侧收集的链段

    // 攀爬阶段:不断让链头更深的一方跳到链头的父节点
    while (top_c[u] != top_c[v]) {
        if (dep[top_c[u]] >= dep[top_c[v]]) {
            // u的链头更深 → 这段属于u侧
            usegs.push_back(query_seg(1, 0, n-1, pos_hld[top_c[u]], pos_hld[u]));
            u = par[top_c[u]];
        } else {
            // v的链头更深 → 这段属于v侧
            vsegs.push_back(query_seg(1, 0, n-1, pos_hld[top_c[v]], pos_hld[v]));
            v = par[top_c[v]];
        }
    }

    // 同链段:u和v在同一条重链上
    Seg fs; bool u_shallow;
    if (dep[u] <= dep[v]) {
        fs = query_seg(1, 0, n-1, pos_hld[u], pos_hld[v]);
        u_shallow = true;  // u更浅 → u是LCA侧 → 正向读取
    } else {
        fs = query_seg(1, 0, n-1, pos_hld[v], pos_hld[u]);
        u_shallow = false; // u更深 → 反向读取
    }

    // 拼接最终结果(从最高位到最低位):
    //   第一部分:u侧各段,每段取 rval(深→浅 = 反向),按攀爬顺序拼接
    //   第二部分:同链段,根据u的深浅取 val 或 rval
    //   第三部分:v侧各段,每段取 val(浅→深 = 正向),按攀爬的逆序拼接
    long long res = 0;
    for (auto& s : usegs)
        res = (res * pw2[s.len] + s.rv) % MOD; // u侧:rval
    res = (res * pw2[fs.len] + (u_shallow ? fs.v : fs.rv)) % MOD; // 同链段
    for (int i = vsegs.size()-1; i >= 0; i--)
        res = (res * pw2[vsegs[i].len] + vsegs[i].v) % MOD; // v侧:val,逆序
    return res;
}

int main() {
    // 预计算 2 的幂次
    pw2[0] = 1;
    for (int i = 1; i < MAXN; i++) pw2[i] = pw2[i-1] * 2 % MOD;

    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &nval[i]);
    for (int i = 0; i < n-1; i++) {
        int u, v; scanf("%d%d", &u, &v);
        ch[u].push_back(v);
        ch[v].push_back(u);
    }
    build_tree();
    build_seg(1, 0, n-1);

    while (m--) {
        int op, u, v;
        scanf("%d%d%d", &op, &u, &v);
        if (op == 1) path_flip(u, v);
        else printf("%lld\n", path_query(u, v));
    }
    return 0;
}

2026.3.21-算法岗

第一题:无限循环

在线评测链接:https://www.neituiya.com/oj/10/2371

题目描述

AK机有一个长度为 $$n$$ 的数组 $$\{a_1, a_2, \ldots, a_n\}$$,她为了研究这个数组做出了个大胆的决定。现在,将与初始数组完全相同的数组连续拼接到其末尾,共拼接 $$10^9$$ 次。设拼接完成后的新数组记为 $$a'$$,则新数组的长度为 $$n \times (10^9 + 1)$$,并且对于任意的 $$n < i \le n \times (10^9 + 1)$$,都有 $$a'_i = a'_{i-n}$$。

请你计算新数组 $$a'$$ 的最长严格递增子序列的长度,并输出这个长度。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 代表数据组数,每组测试数据描述如下:

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示原数组的长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(1 \le a_i \le n)$$,表示原数组的元素。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$2 \times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,表示新数组的最长严格递增子序列长度。

样例1

输入

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

输出

3
3

样例解释

对于第 $$1$$ 组,最终最长严格递增子序列为 $$\{1, 2, 3\}$$。

题解

题目内容拆解

将原数组无限复制后求最长严格递增子序列(LIS)。$$a_i \le n$$,意味着值域只有 $$n$$ 种不同的值。

算法实现

算法主策略:关键观察有两点。第一,严格递增子序列中每个值最多出现一次,而 $$a_i \le n$$,所以 LIS 长度不超过原数组中不同值的个数 $$k$$。第二,数组被复制了 $$10^9 + 1$$ 次,对于原数组中存在的任意 $$k$$ 个不同值 $$v_1 < v_2 < \ldots < v_k$$,我们可以在第 $$1$$ 份副本中选一个 $$v_1$$,第 $$2$$ 份副本中选一个 $$v_2$$,以此类推——由于副本之间下标严格递增,这构成一个长度为 $$k$$ 的严格递增子序列。因此答案恰好等于原数组中不同值的个数

以样例验证:$$[1, 1, 2, 3]$$ 有 $$3$$ 种不同值 $$\{1, 2, 3\}$$,答案 $$3$$。$$[4, 5, 3, 3, 4]$$ 有 $$3$$ 种不同值 $$\{3, 4, 5\}$$,答案 $$3$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,遍历数组一次统计不同值。
  • 空间复杂度:$$O(n)$$,存储集合。

Java

import java.io.*;
import java.util.*;

// 无限循环 - 计数去重
public class Main {
    static int solve(int n, int[] a) {
        // 无限拼接后,每种不同值都能在不同副本中被选到
        Set<Integer> s = new HashSet<>();
        for (int x : a) s.add(x);
        return s.size();
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            int n = Integer.parseInt(br.readLine().trim());
            StringTokenizer st = new StringTokenizer(br.readLine());
            int[] a = new int[n];
            for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
            sb.append(solve(n, a)).append("\n");
        }
        System.out.print(sb);
    }
}

第二题:单层GRU的隐藏状态

在线评测链接:https://www.neituiya.com/oj/10/2372

题目描述

AK机是一位算法工程师,她正在研究一个用于预测用户下单行为的序列模型。为了更好地理解模型内部的运作机制,她决定亲手实现模型核心组件——单层 GRU 的前向传播过程。请你帮助AK机完成这个任务,注意,请仅使用 numpy/pandas/scikit-learn 进行实现。

已知输入序列 $$\{x_t\}_{t=1}^{T}$$(每步维度 $$d$$),权重矩阵/偏置、以及初始隐藏向量 $$h_0$$(维度 $$h$$),请输出最终隐藏状态 $$h_T$$。

GRU 前向传播公式

对每一时刻 $$t = 1, 2, \ldots, T$$,依次计算:

$$r_t = \sigma(x_t W_{xr} + h_{t-1} W_{hr} + b_r) \quad \text{(重置门)}$$

$$z_t = \sigma(x_t W_{xz} + h_{t-1} W_{hz} + b_z) \quad \text{(更新门)}$$

$$\tilde{h}_t = \tanh(x_t W_{xh} + (r_t \odot h_{t-1}) W_{hh} + b_h) \quad \text{(候选状态)}$$

$$h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \quad \text{(新隐藏状态)}$$

其中 $$\sigma(x) = 1/(1+e^{-x})$$ 为 Sigmoid 函数,$$\odot$$ 为逐元素乘法。

符号说明

$$r_t \in (0,1)^h$$:重置门,控制从旧状态 $$h_{t-1}$$ 中带入多少历史信息。$$r_t$$ 越小,对应维度的历史被清空越多。

$$z_t \in (0,1)^h$$:更新门,在旧状态与候选状态之间做软切换。$$z_t$$ 接近 $$1$$ 时偏向使用新的候选状态。

$$\tilde{h}_t \in (-1,1)^h$$:候选隐藏状态,基于当前输入和重置后的历史计算得到。

权重拼接规则

所有权重均以列拼接形式给出:

$$W_x = [W_{xr} \mid W_{xz} \mid W_{xh}] \in \mathbb{R}^{d \times 3h}$$

$$W_h = [W_{hr} \mid W_{hz} \mid W_{hh}] \in \mathbb{R}^{h \times 3h}$$

$$b = [b_r \mid b_z \mid b_h] \in \mathbb{R}^{3h}$$

无需反向传播/更新,只计算最终 $$h_T$$。所有运算请用 float64,结果保留 $$6$$ 位小数(四舍五入)。

输入描述

单行 JSON,包含以下字段($$d, h, T$$ 皆 $$\le 3$$,所有值为数值,不含缺失):

字段 形状 说明
Wx [d, 3h] 输入权重 [W_{xr} \mid W_{xz} \mid W_{xh}]
Wh [h, 3h] 隐藏权重 [W_{hr} \mid W_{hz} \mid W_{hh}]
b [3h] 偏置 [b_r, b_z, b_h]
h0 [h] 初始隐藏状态 h_0
X [T, d] 输入序列,第 t 行为 x_t

输出描述

仅一行:$$[h_{T,1}, h_{T,2}, \ldots]$$(长度 $$h$$),每个元素保留 $$6$$ 位小数。

样例1

输入

{“Wx”:[[0.5,0,0,0.5,0.1,0],[0,0.5,0,0,0.5,0.1]],”Wh”:[[0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0]],”b”:[0.0,0.0,0.0,0.0,0.0,0.0],”h0”:[0.0,0.0],”X”:[[0.0,0.0]]}

输出

[0.0, 0.0]

题解

题目内容拆解

实现单层 GRU 的前向传播,给定输入序列、权重矩阵和初始隐藏状态,输出最终隐藏状态 $$h_T$$。$$d, h, T \le 3$$,数据规模极小,直接按公式计算即可。

算法实现

算法主策略:按 GRU 公式逐时间步计算,使用 NumPy 进行矩阵运算。

核心公式:每个时间步 $$t$$,依次计算重置门 $$r_t = \sigma(x_t W_{xr} + h_{t-1} W_{hr} + b_r)$$,更新门 $$z_t = \sigma(x_t W_{xz} + h_{t-1} W_{hz} + b_z)$$,候选隐藏状态 $$\tilde{h}_t = \tanh(x_t W_{xh} + (r_t \odot h_{t-1}) W_{hh} + b_h)$$,最终 $$h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t$$。

实现步骤:将拼接的权重矩阵 $$W_x$$($$d \times 3h$$)和 $$W_h$$($$h \times 3h$$)按列拆成 $$r, z, h$$ 三部分,偏置 $$b$$ 同样拆分。然后循环 $$T$$ 步,每步用矩阵乘法和逐元素运算更新隐藏状态。

时空复杂度分析

  • 时间复杂度:$$O(T \times d \times h)$$,每步一次矩阵乘法。
  • 空间复杂度:$$O(d \times h)$$,存储权重矩阵。

Python

# 单层GRU的隐藏状态 - NumPy矩阵运算
import json
import numpy as np

def solve():
    data = json.loads(input())
    Wx = np.array(data['Wx'], dtype=np.float64)  # d × 3h
    Wh = np.array(data['Wh'], dtype=np.float64)  # h × 3h
    b = np.array(data['b'], dtype=np.float64)     # 3h
    h0 = np.array(data['h0'], dtype=np.float64)   # h
    X = np.array(data['X'], dtype=np.float64)      # T × d

    h_dim = len(h0)
    h = h0.copy()

    def sigmoid(x):
        return 1.0 / (1.0 + np.exp(-x))

    # 拆分权重矩阵为 r/z/h 三部分
    Wxr, Wxz, Wxh = Wx[:, :h_dim], Wx[:, h_dim:2*h_dim], Wx[:, 2*h_dim:]
    Whr, Whz, Whh = Wh[:, :h_dim], Wh[:, h_dim:2*h_dim], Wh[:, 2*h_dim:]
    br, bz, bh = b[:h_dim], b[h_dim:2*h_dim], b[2*h_dim:]

    # 逐时间步前向传播
    for t in range(len(X)):
        xt = X[t]
        # 重置门:控制从旧状态中带入多少历史
        r = sigmoid(xt @ Wxr + h @ Whr + br)
        # 更新门:在旧状态与候选状态间软切换
        z = sigmoid(xt @ Wxz + h @ Whz + bz)
        # 候选隐藏状态
        h_tilde = np.tanh(xt @ Wxh + (r * h) @ Whh + bh)
        # 更新隐藏状态
        h = (1 - z) * h + z * h_tilde

    result = [round(float(x), 6) for x in h]
    print(json.dumps(result))

solve()

第三题:支配权值划分

在线评测链接:https://www.neituiya.com/oj/10/2373

题目描述

给定一个数组 $$t$$,定义任意数 $$x$$ 在 $$t$$ 中的出现次数为 $$cnt(x)$$。称 $$t$$ 被 $$v$$ 支配,当且仅当对任意 $$v'$$ 有 $$cnt(v) \ge cnt(v')$$;若出现次数相同,则取数值最大的那个 $$v$$。定义数组 $$t$$ 的权值为 $$v \times |t|$$(其中 $$|t|$$ 为 $$t$$ 的长度)。

现在给定一个长度为 $$n$$ 的数组 $$a_1, a_2, \ldots, a_n$$,你需要将其划分为若干个非空连续子数组,使得各子数组权值之和最小,输出该最小值。

输入描述

输入包含多组测试数据。

第一行包含整数 $$T(1 \le T \le 10^3)$$ 表示测试组数。

每组第一行包含一个整数 $$n(1 \le n \le 2 \times 10^3)$$。

第二行包含 $$n$$ 个整数 $$a_1, a_2, \ldots, a_n(-10^9 \le a_i \le 10^9)$$。

保证所有测试中 $$n$$ 的总和不超过 $$5 \times 10^3$$。

输出描述

对于每组测试数据,输出一行一个整数,表示将数组划分为若干非空连续子数组后权值之和的最小值。

样例1

输入

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

输出

8
15
10

样例解释

样例一:一种最优划分为 $$[1, 1, 2], [2], [3]$$,权值分别为 $$1 \times 3, 2 \times 1, 3 \times 1$$,总和 $$3 + 2 + 3 = 8$$。

样例二:任意划分总和均为 $$5 \times 3 = 15$$。

样例三:将其划分为单点 $$[1], [2], [3], [4]$$,总和 $$1 + 2 + 3 + 4 = 10$$。

题解

题目内容拆解

将长度为 $$n$$ 的数组划分为若干连续子数组,每个子数组的权值 = 支配值 $$\times$$ 长度,求最小权值和。$$n \le 2000$$,支持 $$O(n^2)$$ 做法。注意元素可以为负数,支配值为负时权值为负,分组越大反而越好。

算法实现

状态方程定义

设 $$f[i]$$ 表示前 $$i$$ 个元素的最小权值和。

状态方程初始化

$$f[0] = 0$$(空数组权值为 $$0$$),其余 $$f[i] = +\infty$$。

状态方程转移

枚举上一段的结束位置 $$j$$($$0 \le j < i$$),区间 $$[j+1, i]$$ 构成一段。维护该段的频率表,动态更新支配值 $$v$$(出现次数最大且值最大),则:

$$f[i] = \min_{0 \le j < i} \left( f[j] + v_{[j+1,i]} \times (i - j) \right)$$

实现时,固定左端点 $$j$$,从 $$j+1$$ 向右扫描 $$i$$,每次加入新元素更新频率和支配值,$$O(1)$$ 转移。总复杂度 $$O(n^2)$$。

以样例一验证:$$a = [1, 1, 2, 2, 3]$$,最优划分 $$[1,1,2], [2], [3]$$。$$f[3] = 0 + 1 \times 3 = 3$$(子段 $$[1,1,2]$$ 支配值为 $$1$$),$$f[4] = 3 + 2 \times 1 = 5$$,$$f[5] = 5 + 3 \times 1 = 8$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,双重循环枚举所有子段。
  • 空间复杂度:$$O(n)$$,存储 DP 数组和频率表。

Java

import java.io.*;
import java.util.*;

// 支配权值划分 - 划分DP
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();

        while (T-- > 0) {
            int n = Integer.parseInt(br.readLine().trim());
            StringTokenizer st = new StringTokenizer(br.readLine());
            int[] a = new int[n];
            for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());

            // f[i] = 前 i 个元素的最小权值和
            long[] f = new long[n + 1];
            Arrays.fill(f, Long.MAX_VALUE);
            f[0] = 0;

            for (int j = 0; j < n; j++) {
                Map<Integer, Integer> freq = new HashMap<>();
                int maxFreq = 0, domVal = Integer.MIN_VALUE;

                for (int i = j + 1; i <= n; i++) {
                    int val = a[i - 1];
                    freq.put(val, freq.getOrDefault(val, 0) + 1);
                    int cnt = freq.get(val);
                    // 更新支配值
                    if (cnt > maxFreq || (cnt == maxFreq && val > domVal)) {
                        maxFreq = cnt;
                        domVal = val;
                    }
                    long weight = (long) domVal * (i - j);
                    f[i] = Math.min(f[i], f[j] + weight);
                }
            }

            sb.append(f[n]).append("\n");
        }
        System.out.print(sb);
    }

}

Python

Go

第四题:AK机的01树

在线评测链接:https://www.neituiya.com/oj/10/2374

题目描述

AK机有一颗节点编号为 $$1 \sim n$$ 的树,每个节点只有 $$\{0, 1\}$$ 这两种值之一。

设 $$u \to v$$ 为节点 $$u$$ 到节点 $$v$$ 的简单路径。$$g(u \to v)$$ 为从 $$u$$ 开始到 $$v$$ 结束的简单路径上经过的所有点(包括 $$u, v$$)按照先后顺序组成的 $$01$$ 字符串对应的十进制对 $$10^9 + 7$$ 取模的结果。例如,简单路径经过所有节点组成的字符串为 01101,其对应十进制就是 $$13$$,因此 $$g(u \to v) = 13 \mod (10^9 + 7) = 13$$。

AK机会进行 $$m$$ 次以下操作:

  1. 操作 $$1$$:将简单路径 $$u \to v$$ 上所有节点的值反置($$0$$ 变 $$1$$,$$1$$ 变 $$0$$)。
  2. 操作 $$2$$:询问 $$g(u \to v)$$ 的值。

你需要对AK机的每一个操作 $$2$$ 进行回答。

输入描述

第一行输入两个整数 $$n, m(1 \le n, m \le 2 \times 10^5)$$,表示树的大小以及操作次数。

第二行输入 $$n$$ 个整数 $$a_i(a_i \in \{0, 1\})$$,表示第 $$i$$ 个节点初始的值。

接下来 $$n-1$$ 行,每一行输入两个整数 $$u_i, v_i(1 \le u_i, v_i \le n)$$,表示节点 $$u_i$$ 与 $$v_i$$ 之间有一条边。

接下来 $$m$$ 行,每一行输入三个整数 $$x, u, v(x \in \{1, 2\}, 1 \le u, v \le n)$$,当 $$x = 1$$ 时执行路径翻转,当 $$x = 2$$ 时查询 $$g(u \to v)$$。

输出描述

对于每个操作 $$2$$,在一行上输出一个整数,表示 $$g(u \to v)$$ 的值。

样例1

输入

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

输出

0
1
7

样例解释

第一次询问时得到的字符串为 000,第二次询问得到的字符串为 001,第三次询问得到的字符串为 111

题解

题目内容拆解

树上两种操作:路径翻转(0变1、1变0)和路径查询(节点值组成01串转十进制 mod $$10^9 + 7$$)。$$n, m \le 2 \times 10^5$$,暴力 $$O(nm)$$ 会超时,需要用树链剖分(HLD)+ 线段树将每次操作优化到 $$O(\log^2 n)$$。

核心难点有两个:一是路径翻转需要线段树支持区间翻转的懒标记;二是查询 $$g(u \to v)$$ 要正确处理路径方向——$$g(u \to v)$$ 和 $$g(v \to u)$$ 的值不同,因为二进制串是反向的。

算法实现

算法主策略:先做 HLD 将树路径映射为连续区间,再用线段树维护每个区间的正向/反向二进制值,支持区间翻转和区间查询。

第一步:树链剖分。BFS 建树后,按子树大小找重儿子,沿重链分配连续编号。这样任意路径被拆成 $$O(\log n)$$ 条链段,每条链段在线段树上是连续区间。

第二步:线段树设计。每个节点存三个值:

$$val$$:区间正向读取的二进制十进制值(浅→深方向)

$$rval$$:区间反向读取的二进制十进制值(深→浅方向)

$$len$$:区间长度

合并规则:左子 $$L$$、右子 $$R$$ 合并时,$$val = L.val \times 2^{R.len} + R.val$$,$$rval = R.rval \times 2^{L.len} + L.rval$$。

翻转操作:长度为 $$len$$ 的区间翻转所有 bit 后,$$val' = (2^{len} - 1) - val$$,$$rval$$ 同理。用懒标记下传。

第三步:路径查询拼接。查询 $$g(u \to v)$$ 时,从 $$u$$ 和 $$v$$ 分别沿 HLD 链向 LCA 攀爬,收集链段:

$$u$$ 侧链段:每段是"深→浅"方向,取 $$rval$$(反向值),按攀爬顺序从 $$u$$ 向 LCA 拼接。

$$v$$ 侧链段:在最终路径中方向是"浅→深",取 $$val$$(正向值),按攀爬的逆序拼接到结果末尾。

同链段:根据 $$u, v$$ 的深浅决定取 $$val$$ 或 $$rval$$。

样例完整推导:树结构为 $$1$$ 连 $$2, 3$$,$$2$$ 连 $$4, 5$$。初始值全 $$0$$。

  1. 查询 $$g(1 \to 4)$$:路径 $$[1, 2, 4]$$,值 000 = $$0$$。输出 $$0$$。
  2. 翻转 $$1 \to 3$$:路径 $$[1, 3]$$,翻转后 $$val[1] = 1, val[3] = 1$$。

3) 查询 $$g(4 \to 1)$$:路径 $$[4, 2, 1]$$,值 001 = $$1$$。输出 $$1$$。

4) 翻转 $$2 \to 5$$:路径 $$[2, 5]$$,翻转后 $$val[2] = 1, val[5] = 1$$。

  1. 查询 $$g(5 \to 1)$$:路径 $$[5, 2, 1]$$,值 111 = $$7$$。输出 $$7$$。

时空复杂度分析

  • 时间复杂度:$$O(m \log^2 n)$$,每次操作拆成 $$O(\log n)$$ 条链段,每条链段的线段树操作 $$O(\log n)$$。
  • 空间复杂度:$$O(n)$$,存储 HLD 信息和线段树。

Java

import java.io.*;
import java.util.*;

// AK机的01树 - 树链剖分 + 线段树
public class Main {
    static final int MOD = 1000000007;
    static int n, m;
    static long[] pw2;
    static int[] par, dep, sz, hvy, topC, posH, ori, nval;
    static List<Integer>[] children;

    // 线段树
    static long[] sv, sr;
    static int[] sl;
    static boolean[] sf;

    static void mergeUp(int nd) {
        int lc = 2*nd, rc = 2*nd+1;
        sv[nd] = (sv[lc] * pw2[sl[rc]] + sv[rc]) % MOD;
        sr[nd] = (sr[rc] * pw2[sl[lc]] + sr[lc]) % MOD;
        sl[nd] = sl[lc] + sl[rc];
    }
    static void doFlip(int nd) {
        long full = (pw2[sl[nd]] - 1 + MOD) % MOD;
        sv[nd] = (full - sv[nd] + MOD) % MOD;
        sr[nd] = (full - sr[nd] + MOD) % MOD;
        sf[nd] = !sf[nd];
    }
    static void pushDown(int nd) {
        if (sf[nd]) { doFlip(2*nd); doFlip(2*nd+1); sf[nd] = false; }
    }
    static void build(int nd, int l, int r) {
        sf[nd] = false;
        if (l == r) { sv[nd] = sr[nd] = nval[ori[l]]; sl[nd] = 1; return; }
        int mid = (l+r)/2;
        build(2*nd, l, mid); build(2*nd+1, mid+1, r); mergeUp(nd);
    }
    static void flipR(int nd, int l, int r, int ql, int qr) {
        if (ql > r || qr < l) return;
        if (ql <= l && r <= qr) { doFlip(nd); return; }
        pushDown(nd); int mid = (l+r)/2;
        flipR(2*nd, l, mid, ql, qr); flipR(2*nd+1, mid+1, r, ql, qr); mergeUp(nd);
    }
    static long[] qry(int nd, int l, int r, int ql, int qr) {
        if (ql > r || qr < l) return new long[]{0, 0, 0};
        if (ql <= l && r <= qr) return new long[]{sv[nd], sr[nd], sl[nd]};
        pushDown(nd); int mid = (l+r)/2;
        long[] L = qry(2*nd, l, mid, ql, qr), R = qry(2*nd+1, mid+1, r, ql, qr);
        if (L[2] == 0) return R; if (R[2] == 0) return L;
        return new long[]{(L[0]*pw2[(int)R[2]]+R[0])%MOD, (R[1]*pw2[(int)L[2]]+L[1])%MOD, L[2]+R[2]};
    }

    static void pathFlip(int u, int v) {
        while (topC[u] != topC[v]) {
            if (dep[topC[u]] < dep[topC[v]]) { int t=u; u=v; v=t; }
            flipR(1, 0, n-1, posH[topC[u]], posH[u]); u = par[topC[u]];
        }
        if (dep[u] > dep[v]) { int t=u; u=v; v=t; }
        flipR(1, 0, n-1, posH[u], posH[v]);
    }

    static long pathQuery(int u, int v) {
        List<long[]> us = new ArrayList<>(), vs = new ArrayList<>();
        while (topC[u] != topC[v]) {
            if (dep[topC[u]] >= dep[topC[v]]) {
                us.add(qry(1, 0, n-1, posH[topC[u]], posH[u])); u = par[topC[u]];
            } else {
                vs.add(qry(1, 0, n-1, posH[topC[v]], posH[v])); v = par[topC[v]];
            }
        }
        long[] fs; boolean ush;
        if (dep[u] <= dep[v]) { fs = qry(1, 0, n-1, posH[u], posH[v]); ush = true; }
        else { fs = qry(1, 0, n-1, posH[v], posH[u]); ush = false; }

        long res = 0;
        for (long[] s : us) res = (res * pw2[(int)s[2]] + s[1]) % MOD;
        res = (res * pw2[(int)fs[2]] + (ush ? fs[0] : fs[1])) % MOD;
        for (int i = vs.size()-1; i >= 0; i--) {
            long[] s = vs.get(i); res = (res * pw2[(int)s[2]] + s[0]) % MOD;
        }
        return res;
    }


    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        in.nextToken(); n = (int)in.nval;
        in.nextToken(); m = (int)in.nval;

        pw2 = new long[n + 2]; pw2[0] = 1;
        for (int i = 1; i <= n+1; i++) pw2[i] = pw2[i-1] * 2 % MOD;

        nval = new int[n+1];
        for (int i = 1; i <= n; i++) { in.nextToken(); nval[i] = (int)in.nval; }

        List<Integer>[] adj = new ArrayList[n+1];
        for (int i = 0; i <= n; i++) adj[i] = new ArrayList<>();
        for (int i = 0; i < n-1; i++) {
            in.nextToken(); int u = (int)in.nval;
            in.nextToken(); int v = (int)in.nval;
            adj[u].add(v); adj[v].add(u);
        }

        // BFS 建树
        par = new int[n+1]; dep = new int[n+1]; sz = new int[n+1]; hvy = new int[n+1];
        Arrays.fill(hvy, -1); Arrays.fill(sz, 1);
        boolean[] vis = new boolean[n+1]; vis[1] = true;
        int[] bfsOrder = new int[n]; int head = 0, tail = 0;
        bfsOrder[tail++] = 1;
        while (head < tail) {
            int u = bfsOrder[head++];
            for (int v : adj[u]) if (!vis[v]) { vis[v]=true; par[v]=u; dep[v]=dep[u]+1; bfsOrder[tail++]=v; }
        }
        children = new ArrayList[n+1];
        for (int i = 0; i <= n; i++) children[i] = new ArrayList<>();
        for (int v = 2; v <= n; v++) children[par[v]].add(v);
        for (int i = tail-1; i >= 0; i--) {
            int u = bfsOrder[i];
            for (int v : children[u]) { sz[u] += sz[v]; if (hvy[u]==-1 || sz[v]>sz[hvy[u]]) hvy[u]=v; }
        }

        // HLD 编号
        topC = new int[n+1]; posH = new int[n+1]; ori = new int[n];
        int timer = 0;
        Deque<int[]> stk = new ArrayDeque<>();
        stk.push(new int[]{1, 1});
        while (!stk.isEmpty()) {
            int[] top = stk.pop(); int u = top[0], tp = top[1];
            topC[u] = tp; posH[u] = timer; ori[timer] = u; timer++;
            for (int v : children[u]) if (v != hvy[u]) stk.push(new int[]{v, v});
            if (hvy[u] != -1) stk.push(new int[]{hvy[u], tp});
        }

        // 建线段树
        int sz4 = 4 * n;
        sv = new long[sz4]; sr = new long[sz4]; sl = new int[sz4]; sf = new boolean[sz4];
        build(1, 0, n-1);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < m; i++) {
            in.nextToken(); int op = (int)in.nval;
            in.nextToken(); int u = (int)in.nval;
            in.nextToken(); int v = (int)in.nval;
            if (op == 1) pathFlip(u, v);
            else sb.append(pathQuery(u, v)).append('\n');
        }
        System.out.print(sb);
    }
}

2026-3-14-研发岗

第一题:小美的因子数量

在线测评链接:https://www.neituiya.com/oj/10/2325

题目描述

小美很喜欢因子数量为奇数的数。现在小芳给了小美一个区间$$[l,r]$$,请你帮小美算出区间内有多少个因子数量为奇数的数。

因子:对于正整数$$a$$,如果存在正整数$$p$$使得$$a$$能被$$p$$整除,则称$$p$$是$$a$$的因子。例如,$$12$$的因子有$$1,2,3,4,6,12$$。

输入描述

第一行输入两个整数$$l,r(1 \le l \le r \le 10^9)$$,表示询问的区间。

输出描述

输出一个整数,表示区间内因子数量为奇数的数的个数。

样例1

输入

1 1

输出

1

样例解释

在这个样例中,区间内唯一可以取到的数字为$$1$$,其因子数量只有自身,为奇数。

样例2

输入

4 5

输出

1

样例解释

在这个样例中,区间内只有$$4$$的因子数量为奇数。

题解

题目内容拆解

给定区间$$[l,r]$$,统计其中因子数量为奇数的数的个数。数据范围$$r$$可达$$10^9$$,不可能逐个枚举因子,需要找到数学规律。

算法实现

算法主策略:本题的关键观察是因子数量为奇数的数恰好是完全平方数

对于任意正整数$$n$$,其因子总是成对出现的:如果$$d$$是$$n$$的因子,则$$n/d$$也是$$n$$的因子。当且仅当$$d = n/d$$,即$$n = d^2$$时,这一对因子合并为一个,导致因子总数为奇数。因此因子数量为奇数等价于$$n$$是完全平方数。

$$[l,r]$$内完全平方数的个数等于$$\lfloor\sqrt{r}\rfloor - \lfloor\sqrt{l-1}\rfloor$$,直接用整数开方即可$$O(1)$$求解。

时空复杂度分析

  • 时间复杂度:$$O(1)$$,仅需两次整数开方运算。
  • 空间复杂度:$$O(1)$$,只使用常数个变量。
// 小美的因子数量 - 数学(完全平方数计数)
import java.io.*;
import java.util.*;

public class Main {
    // 因子数量为奇数的数就是完全平方数
    static long solve(long l, long r) {
        long sqrtR = (long) Math.sqrt((double) r);
        while (sqrtR * sqrtR > r) sqrtR--;
        while ((sqrtR + 1) * (sqrtR + 1) <= r) sqrtR++;
        long sqrtL = (long) Math.sqrt((double) (l - 1));
        while (sqrtL * sqrtL > l - 1) sqrtL--;
        while ((sqrtL + 1) * (sqrtL + 1) <= l - 1) sqrtL++;
        return sqrtR - sqrtL;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        long l = Long.parseLong(st.nextToken());
        long r = Long.parseLong(st.nextToken());
        System.out.println(solve(l, r));
    }
}

第二题:超级斐波那契数列

在线测评链接:https://www.neituiya.com/oj/10/2326

题目描述

定义超级斐波那契数列如下:给定整数$$k$$,该序列的前$$k$$项均为$$1$$;对于$$n>k$$,第$$n$$项为前$$k$$项之和,即$$S_n=S_{n-1}+S_{n-2}+...+S_{n-k}$$。

现给定整数$$k$$和查询次数$$q$$,每次查询一个正整数$$x$$,请输出该序列的第$$x$$项对$$10^9+7$$取模后的值。

输入描述

第一行输入两个整数$$k, q(1 \le k \le 10^6, 1 \le q \le 2 \times 10^5)$$。

此后$$q$$行,每行输入一个正整数$$x(1 \le x \le 10^6)$$。

输出描述

输出$$q$$行,每行输出一个整数,表示对应查询的答案对$$10^9+7$$取模后的值。

样例1

输入

2 5
1
2
3
4
5

输出

1
1
2
3
5

样例解释

在这组测试数据中,$$k=2$$,即标准斐波那契数列。当$$x=1$$时,$$S_1=1$$。当$$x=2$$时,$$S_2=1$$。当$$x=3$$时,$$S_3=S_2+S_1=1+1=2$$。当$$x=4$$时,$$S_4=S_3+S_2=2+1=3$$。当$$x=5$$时,$$S_5=S_4+S_3=3+2=5$$。

题解

本题涉及到前缀和,不熟悉该算法的同学可以先做一下模板题:

区间和

题目内容拆解

给定$$k$$阶超级斐波那契数列(前$$k$$项为$$1$$,此后每项为前$$k$$项之和),回答$$q$$次查询第$$x$$项的值。$$k$$和$$x$$均可达$$10^6$$,需要高效预计算。

算法实现

算法主策略:本题采用前缀和优化递推

朴素计算$$S_n = S_{n-1} + S_{n-2} + \cdots + S_{n-k}$$需要$$O(k)$$时间求和,总复杂度$$O(nk)$$,当$$n$$和$$k$$都为$$10^6$$时会超时。

引入前缀和数组$$P_i = S_1 + S_2 + \cdots + S_i$$,则$$S_n = P_{n-1} - P_{n-1-k}$$,每项只需$$O(1)$$计算。预计算完成后,每次查询直接$$O(1)$$输出。

以样例为例,$$k=2$$:$$P_0=0, P_1=1, P_2=2$$,$$S_3 = P_2 - P_0 = 2$$,$$P_3=4$$,$$S_4 = P_3 - P_1 = 3$$,$$P_4=7$$,$$S_5 = P_4 - P_2 = 5$$。

时空复杂度分析

  • 时间复杂度:$$O(N + q)$$,预计算$$N = 10^6$$项各$$O(1)$$,回答$$q$$次查询各$$O(1)$$。
  • 空间复杂度:$$O(N)$$,存储数列和前缀和数组。

Java

// 超级斐波那契数列 - 前缀和优化递推
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = new StringTokenizer(br.readLine());
        int k = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());

        int MOD = 1000000007;
        int MAXN = 1000001;
        long[] S = new long[MAXN];
        long[] prefix = new long[MAXN];

        // 预计算:S[n] = S[n-1] + ... + S[n-k],用前缀和加速
        for (int i = 1; i < MAXN; i++) {
            if (i <= k) {
                S[i] = 1;
            } else {
                S[i] = ((prefix[i - 1] - prefix[i - 1 - k]) % MOD + MOD) % MOD;
            }
            prefix[i] = (prefix[i - 1] + S[i]) % MOD;
        }

        for (int i = 0; i < q; i++) {
            int x = Integer.parseInt(br.readLine().trim());
            sb.append(S[x]).append('\n');
        }
        System.out.print(sb);
    }
}

第三题:节点最大权值

在线测评链接:https://www.neituiya.com/oj/10/2327

题目描述

给你一个由$$n$$个编号为$$1 \sim n$$的节点以及$$m$$条编号为$$1 \sim m$$的边组成的无向图,我们定义一个节点的权值为它的当前度(即已执行完之前所有操作后的状态)加上它的节点编号。

小美会进行$$q$$次如下操作:

操作一:断开编号为$$x$$的边,保证每条边至多被删除一次,即在进行操作一时,该边当前一定存在于图中。

操作二:向你询问编号为$$x$$的节点所在的连通块中所有节点中最大的权值,你需要将此权值告诉他。

度:与一个顶点相连接的边的条数称为该顶点的度。

连通块:也称连通分量,满足以下条件:1. 是原图的一个子图。2. 连通块内的任意两个顶点之间都存在路径相连,且路径上的点也在连通块内。3. 是极大的,即不能再通过添加原图中的其他顶点而依旧保持连通性。4. 单独的点也构成一个连通块,连通块的大小即为连通块中顶点的数量。

输入描述

第一行输入三个正整数$$n, m, q(1 \le n, q \le 2 \times 10^5, 0 \le m \le \min(\frac{n \times (n-1)}{2}, 2 \times 10^5))$$,表示节点个数、边个数、操作次数。

此后$$m$$行,第$$i$$行输入两个整数$$u_i, v_i(1 \le u_i, v_i \le n, u_i \ne v_i)$$,表示图上第$$i$$条边连接节点$$u_i$$和$$v_i$$。

此后$$q$$行,第$$i$$行先输入一个整数$$o_i(1 \le o_i \le 2)$$,表示操作编号。随后在同一行:若$$o_i=1$$,输入一个整数$$x_i(1 \le x_i \le m)$$,表示断掉的边的编号;若$$o_i=2$$,输入一个整数$$x_i(1 \le x_i \le n)$$,表示询问的节点编号。

保证图没有重边和自环,操作一合法。

输出描述

输出若干行,每一行对操作二进行回答。

样例1

输入

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

输出

7
5
6

样例解释

初始时所有节点通过$$5$$条边全部连通,各节点度数为$$3, 2, 2, 1, 2$$,权值(度+编号)为$$4, 4, 5, 5, 7$$。查询节点$$4$$所在连通块最大权值为$$7$$。删除边$$1$$(连接节点$$1, 2$$)后,节点$$1, 2$$度数各减$$1$$,图分为$$\{1, 3, 5\}$$和$$\{2, 4\}$$两个连通块。查询节点$$2$$所在连通块$$\{2, 4\}$$最大权值为$$5$$。删除边$$2$$(连接节点$$1, 5$$)后,节点$$1, 5$$度数各减$$1$$,查询节点$$1$$所在连通块$$\{1, 3, 5\}$$最大权值为$$6$$。

题解

本题涉及到并查集,不熟悉该算法的同学可以先做一下模板题:

并查集-模版题

连通块个数(一)

题目内容拆解

给定无向图,支持删边和查询节点所在连通块的最大权值(权值=当前度+编号)。$$n, m, q$$均可达$$2 \times 10^5$$,需要高效维护连通性和最大值。

核心观察:并查集只支持"合并"不支持"分裂",因此正向删边无法直接用并查集处理。但如果逆序处理所有操作,删边就变成了加边,恰好适合并查集。

算法实现

算法主策略:本题采用逆序处理 + 并查集

第一步:预处理。先读取所有操作,标记哪些边最终会被删除。用未被删除的边建立初始图,计算各节点的初始度数和权值,并用并查集维护连通块及其最大权值。

第二步:逆序处理。从最后一个操作往前处理。遇到查询操作,直接记录当前连通块的最大权值;遇到删边操作,将其视为"加边"——两端节点度数各加$$1$$,更新它们所在连通块的最大权值,然后合并两个连通块。

关键性质:逆序处理时权值只增不减(只加边,度只增加),因此连通块的最大值只需在加边时与新权值比较即可,无需遍历整个连通块。

以样例为例,边$$1$$(节点$$1, 2$$)和边$$2$$(节点$$1, 5$$)被删除。初始图仅含边$$3, 4, 5$$,度数为$$1, 1, 2, 1, 1$$,权值为$$2, 3, 5, 5, 6$$。连通块$$\{1, 3, 5\}$$最大权值$$6$$,连通块$$\{2, 4\}$$最大权值$$5$$。逆序处理第$$5$$个操作(查询节点$$1$$)得$$6$$;第$$4$$个操作加回边$$2$$,节点$$1, 5$$度数各加$$1$$,权值变为$$3, 7$$,连通块最大更新为$$7$$;第$$3$$个操作(查询节点$$2$$)得$$5$$;第$$2$$个操作加回边$$1$$,合并两个连通块,最大为$$7$$;第$$1$$个操作(查询节点$$4$$)得$$7$$。最终答案倒序输出:$$7, 5, 6$$。

时空复杂度分析

  • 时间复杂度:$$O((m + q) \alpha(n))$$,其中$$\alpha(n)$$是阿克曼函数的反函数,近似常数。每条边和每个操作各处理一次,并查集操作均为$$O(\alpha(n))$$。
  • 空间复杂度:$$O(n + m + q)$$,存储图的边、操作序列和并查集数组。

Java

// 节点最大权值 - 逆序处理 + 并查集
import java.io.*;
import java.util.*;

public class Main {
    static int[] par, rnk, maxW, deg;

    static int find(int x) {
        while (par[x] != x) {
            par[x] = par[par[x]];
            x = par[x];
        }
        return x;
    }

    static void unite(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return;
        if (rnk[ra] < rnk[rb]) { int t = ra; ra = rb; rb = t; }
        par[rb] = ra;
        maxW[ra] = Math.max(maxW[ra], maxW[rb]);
        if (rnk[ra] == rnk[rb]) rnk[ra]++;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());

        int[][] edges = new int[m][2];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            edges[i][0] = Integer.parseInt(st.nextToken());
            edges[i][1] = Integer.parseInt(st.nextToken());
        }

        int[][] ops = new int[q][2];
        // 记录被删除的边,用于逆序处理
        Set<Integer> deleted = new HashSet<>();
        for (int i = 0; i < q; i++) {
            st = new StringTokenizer(br.readLine());
            ops[i][0] = Integer.parseInt(st.nextToken());
            ops[i][1] = Integer.parseInt(st.nextToken());
            if (ops[i][0] == 1) deleted.add(ops[i][1] - 1);
        }

        // 初始化:仅保留未被删除的边
        deg = new int[n + 1];
        for (int i = 0; i < m; i++) {
            if (!deleted.contains(i)) {
                deg[edges[i][0]]++;
                deg[edges[i][1]]++;
            }
        }
        par = new int[n + 1];
        rnk = new int[n + 1];
        maxW = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            par[i] = i;
            maxW[i] = deg[i] + i;
        }
        for (int i = 0; i < m; i++) {
            if (!deleted.contains(i))
                unite(edges[i][0], edges[i][1]);
        }

        // 逆序处理:删边变加边
        List<Integer> answers = new ArrayList<>();
        for (int i = q - 1; i >= 0; i--) {
            if (ops[i][0] == 2) {
                answers.add(maxW[find(ops[i][1])]);
            } else {
                int idx = ops[i][1] - 1;
                int u = edges[idx][0], v = edges[idx][1];
                deg[u]++;
                deg[v]++;
                int ru = find(u), rv = find(v);
                maxW[ru] = Math.max(maxW[ru], deg[u] + u);
                maxW[rv] = Math.max(maxW[rv], deg[v] + v);
                unite(u, v);
            }
        }

        Collections.reverse(answers);
        for (int x : answers) sb.append(x).append('\n');
        System.out.print(sb);
    }
}

2026-3-14-算法岗

第一题:小美的因子数量

在线测评链接:https://www.neituiya.com/oj/10/2328

题目描述

小美很喜欢因子数量为奇数的数。现在小芳给了小美一个区间$$[l,r]$$,请你帮小美算出区间内有多少个因子数量为奇数的数。

因子:对于正整数$$a$$,如果存在正整数$$p$$使得$$a$$能被$$p$$整除,则称$$p$$是$$a$$的因子。例如,$$12$$的因子有$$1,2,3,4,6,12$$。

输入描述

第一行输入两个整数$$l,r(1 \le l \le r \le 10^9)$$,表示询问的区间。

输出描述

输出一个整数,表示区间内因子数量为奇数的数的个数。

样例1

输入

1 1

输出

1

样例解释

在这个样例中,区间内唯一可以取到的数字为$$1$$,其因子数量只有自身,为奇数。

样例2

输入

4 5

输出

1

样例解释

在这个样例中,区间内只有$$4$$的因子数量为奇数。

题解

题目内容拆解

给定区间$$[l,r]$$,统计其中因子数量为奇数的数的个数。数据范围$$r$$可达$$10^9$$,不可能逐个枚举因子,需要找到数学规律。

算法实现

算法主策略:本题的关键观察是因子数量为奇数的数恰好是完全平方数

对于任意正整数$$n$$,其因子总是成对出现的:如果$$d$$是$$n$$的因子,则$$n/d$$也是$$n$$的因子。当且仅当$$d = n/d$$,即$$n = d^2$$时,这一对因子合并为一个,导致因子总数为奇数。因此因子数量为奇数等价于$$n$$是完全平方数。

$$[l,r]$$内完全平方数的个数等于$$\lfloor\sqrt{r}\rfloor - \lfloor\sqrt{l-1}\rfloor$$,直接用整数开方即可$$O(1)$$求解。

时空复杂度分析

  • 时间复杂度:$$O(1)$$,仅需两次整数开方运算。
  • 空间复杂度:$$O(1)$$,只使用常数个变量。

Java

// 小美的因子数量 - 数学(完全平方数计数)
import java.io.*;
import java.util.*;

public class Main {
    // 因子数量为奇数的数就是完全平方数
    static long solve(long l, long r) {
        long sqrtR = (long) Math.sqrt((double) r);
        while (sqrtR * sqrtR > r) sqrtR--;
        while ((sqrtR + 1) * (sqrtR + 1) <= r) sqrtR++;
        long sqrtL = (long) Math.sqrt((double) (l - 1));
        while (sqrtL * sqrtL > l - 1) sqrtL--;
        while ((sqrtL + 1) * (sqrtL + 1) <= l - 1) sqrtL++;
        return sqrtR - sqrtL;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        long l = Long.parseLong(st.nextToken());
        long r = Long.parseLong(st.nextToken());
        System.out.println(solve(l, r));
    }
}

第二题:朴素贝叶斯二分类器

在线测评链接:https://www.neituiya.com/oj/10/2329

题目描述

请帮助AK机实现一个朴素贝叶斯$$(Multinomial$$ $$NB)$$二分类器,在给定训练集后对测试集输出标签。

AK机设计的算法步骤如下:

1.输入读取

$$train$$字段:二维列表,每行最后一列$$y \in \{0, 1\}$$,其余列为非负整数词频。

$$test$$字段:二维列表,仅含词频特征(维度与训练一致)。

2.平滑:使用拉普拉斯平滑$$k=1$$

$$P(w \mid c)=\frac{n_{c, w}+1}{\sum_{w^{\prime}}(n_{c, w^{\prime}}+1)}$$

$$n_{c,w}$$表示在所有训练样本中标签为$$c$$时第$$w$$个词的总频次。

3.先验概率:$$\pi_{c}=\frac{N_{c}}{N}$$,$$N_c$$为类别$$c$$的样本数量,$$N$$为总样本数。

4.对数后验:对样本$$x$$计算

$$\log P(c \mid x)=\log \pi_{c}+\sum_{w} x_{w} \log P(w \mid c)$$

5.预测规则:若$$\log P(1|x) \ge \log P(0|x)$$输出$$1$$,否则$$0$$。

输入描述

输入为一行JSON字符串,包含$$train$$和$$test$$两个字段。$$train$$为二维列表,每行最后一列为标签$$y \in \{0, 1\}$$,其余列为非负整数词频。$$test$$为二维列表,仅含词频特征。行长度必须一致,$$train[i][:-1]$$与$$test[j]$$均为非负整数词频。

输出描述

所有测试样本的预测标签$$(0/1)$$按顺序放入JSON数组,例如:$$[0,1,0]$$

补充说明

为保证结果唯一可复现,所有随机过程必须:$$import$$ $$numpy$$ $$as$$ $$np$$,$$np.random.seed(42)$$。

样例1

输入

{"train":[[2,0,0,0],[3,1,0,0],[0,0,2,1],[0,1,3,1]],"test":[[1,0,0],[0,1,2]]}

输出

[0,1]

题解:机器学习

题目内容拆解

给定训练数据和测试数据,用Multinomial Naive Bayes算法对测试样本进行二分类预测。输入输出均为JSON格式,核心是按照题目给定的数学公式逐步计算即可。

算法实现

算法主策略:本题采用朴素贝叶斯分类器,按照题目给定的公式步骤实现。

第一步:解析JSON输入。从标准输入读取一行JSON,分离出训练数据的特征矩阵$$X_{train}$$、标签向量$$y_{train}$$和测试特征矩阵$$X_{test}$$。

第二步:计算先验概率。统计标签为$$0$$和$$1$$的样本数量$$N_0, N_1$$,先验概率$$\pi_c = N_c / N$$。

第三步:计算条件概率。对每个类别$$c$$,统计该类别下每个词的总频次$$n_{c,w}$$,然后应用拉普拉斯平滑:$$P(w|c) = (n_{c,w}+1) / \sum_{w'}(n_{c,w'}+1)$$。分母实际上是该类别所有词频之和加上特征维度数。

第四步:计算对数后验并预测。对每个测试样本$$x$$,分别计算$$\log P(0|x)$$和$$\log P(1|x)$$。若$$\log P(1|x) \ge \log P(0|x)$$则预测为$$1$$,否则为$$0$$。使用对数空间避免浮点数下溢。

以样例为例,训练集有$$4$$个样本、$$3$$维特征。类别$$0$$有$$2$$个样本(前两行),类别$$1$$有$$2$$个样本(后两行)。先验概率$$\pi_0 = \pi_1 = 0.5$$。类别$$0$$下词频总和为$$[5,1,0]$$,加$$1$$平滑后为$$[6,2,1]$$,归一化分母为$$9$$。类别$$1$$下词频总和为$$[0,1,5]$$,加$$1$$平滑后为$$[1,2,6]$$,归一化分母为$$9$$。对测试样本$$[1,0,0]$$,$$\log P(0|x) = \log 0.5 + 1 \times \log(6/9) \approx -1.099$$,$$\log P(1|x) = \log 0.5 + 1 \times \log(1/9) \approx -2.890$$,因此预测$$0$$。对测试样本$$[0,1,2]$$,$$\log P(1|x) > \log P(0|x)$$,预测$$1$$。最终输出$$[0,1]$$。

时空复杂度分析

  • 时间复杂度:$$O(N \times D + T \times D)$$,其中$$N$$为训练样本数,$$D$$为特征维度,$$T$$为测试样本数。训练阶段统计词频$$O(N \times D)$$,预测阶段遍历每个测试样本的每个特征$$O(T \times D)$$。
  • 空间复杂度:$$O(N \times D + T \times D)$$,存储训练和测试数据矩阵。

Python

# 朴素贝叶斯二分类器 - Multinomial NB + 拉普拉斯平滑
import numpy as np
import json

np.random.seed(42)


def multinomial_nb(train_data, test_data):
    """Multinomial Naive Bayes with Laplace smoothing k=1"""
    train = np.array(train_data)
    X_train = train[:, :-1]       # (N, D) 词频矩阵
    y_train = train[:, -1].astype(int)  # (N,) 标签
    X_test = np.array(test_data)  # (T, D) 测试词频

    N = len(y_train)
    results = []

    for x in X_test:
        log_posteriors = []
        for c in [0, 1]:
            mask = (y_train == c)
            N_c = mask.sum()
            # 先验概率
            prior = N_c / N
            # 统计类别c下每个词的总频次
            word_counts = X_train[mask].sum(axis=0)  # (D,)
            # 拉普拉斯平滑后的条件概率
            total = (word_counts + 1).sum()
            log_cond = np.log((word_counts + 1) / total)
            # 对数后验
            log_post = np.log(prior) + (x * log_cond).sum()
            log_posteriors.append(log_post)
        # 预测规则:logP(1|x) >= logP(0|x) 输出1
        results.append(1 if log_posteriors[1] >= log_posteriors[0] else 0)

    return results


data = json.loads(input())
preds = multinomial_nb(data["train"], data["test"])
print(json.dumps(preds, separators=(',', ':')))

第三题:无向树

在线测评链接:https://www.neituiya.com/oj/10/2330

题目描述

给定一棵以根节点$$1$$为根的无向树,节点编号为$$1, 2, ..., n$$,每个节点$$i$$的权值为$$x_i$$。对于每个节点$$i(i>1)$$:沿原树从$$i$$到根$$1$$的路径,找到离节点$$i$$最近的第一个权值严格大于$$x_i$$的祖先节点$$j$$;如果$$j$$存在,在节点$$i$$与节点$$j$$之间添加一条额外的无向边;否则,不进行任何操作。在加入所有额外边之后,计算每个节点到根节点$$1$$的最短距离(以边数计)。

【名词解释】

祖先节点:在一棵以$$u$$为根的树中,若点$$x$$在$$u$$到$$v$$的简单路径上,且$$u \ne v$$,则称$$x$$是$$v$$的祖先节点。根节点没有祖先节点。

输入描述

第一行输入整数$$n(1 \le n \le 2 \times 10^5)$$,表示节点数量。

第二行输入$$n$$个整数$$x_1, x_2, ..., x_n(1 \le x_i \le 10^{11})$$,表示各节点权值。

接下来$$n-1$$行,每行输入两个整数$$u_i, v_i(1 \le u_i, v_i \le n, u_i \ne v_i)$$,表示一条无向边。保证边集构成一棵以$$1$$为根的树。

输出描述

在同一行输出$$n$$个整数$$d_1, d_2, ..., d_n$$以空格分隔,其中$$d_i$$表示在加入额外边之后,节点$$i$$到根节点$$1$$的最短距离。

补充说明

在几乎全部的情况下,$$PyPy$$的运行速度优于$$Python$$,我们建议您选择对应版本的$$PyPy$$进行提交、而不是$$Python$$。

样例1

输入

7
7 1 2 3 4 5 6
1 2
2 3
3 4
5 4
5 6
6 7

输出

0 1 1 1 1 1 1

样例解释

原树是一条链:$$1-2-3-4-5-6-7$$,各点权值为$$7, 1, 2, 3, 4, 5, 6$$。对于每个节点$$i>1$$,沿原树从$$i$$到$$1$$的路径,找到第一个权值严格大于$$x_i$$的祖先并添加额外边:节点$$2$$路径$$2 \to 1$$,找到祖先$$1$$($$7>1$$),添加边$$2-1$$;节点$$3$$路径$$3 \to 2 \to 1$$,找到祖先$$1$$($$7>2$$),添加边$$3-1$$;节点$$4$$路径$$4 \to 3 \to 2 \to 1$$,找到祖先$$1$$($$7>3$$),添加边$$4-1$$。

添加所有额外边后,节点$$2, ..., 7$$均可通过新边直接到达根$$1$$,距离均为$$1$$;根节点$$1$$距离为$$0$$。

样例2

输入

5
9 6 3 5 4
1 2
1 3
3 4
4 5

输出

0 1 1 1 2

题解

本题涉及到单调栈,不熟悉该算法的同学可以先做一下模板题:

下一个更大元素(一)

本题还涉及到BFS,不熟悉该算法的同学可以先做一下模板题:

离开中山路

马的遍历

题目内容拆解

给定以$$1$$为根的树,每个节点需要找到路径上最近的权值严格大于自己的祖先节点并连边,最终求每个节点到根的最短距离。$$n$$可达$$2 \times 10^5$$,暴力遍历每条路径会超时,需要$$O(n \log n)$$的高效算法。

核心观察:「找最近的权值更大的祖先」本质上是路径上的"下一个更大元素"问题,可以用单调栈高效解决。

算法实现

算法主策略:本题采用单调栈 + 二分搜索 + BFS

第一步:建树。以节点$$1$$为根,通过BFS确定每个节点的父子关系。

第二步:单调栈找快捷边。DFS遍历树,维护一个权值严格递减的单调栈,栈中存储从根到当前节点路径上的祖先子序列。对于当前节点$$v$$(权值$$x_v$$),在栈中二分搜索,找到最后一个(最近的)权值严格大于$$x_v$$的祖先。然后将$$v$$压入栈中适当位置(覆盖所有权值$$\le x_v$$的栈顶元素),DFS回溯时恢复栈状态。由于每次只保存和恢复一个位置,单次操作$$O(\log n)$$(二分搜索),总时间$$O(n \log n)$$。

第三步:BFS求最短距离。在原树边的基础上添加所有快捷边,构成增强图。从根节点$$1$$出发做BFS,即可得到每个节点的最短距离。

以样例$$2$$为例,树结构为$$1-2$$、$$1-3$$、$$3-4$$、$$4-5$$,权值为$$9, 6, 3, 5, 4$$。DFS遍历时,单调栈依次处理各节点:节点$$2$$(权值$$6$$)在栈中找到节点$$1$$(权值$$9 > 6$$),添加快捷边$$2 \to 1$$(与树边重复);节点$$4$$(权值$$5$$)在栈$$[9, 3]$$中找到节点$$1$$(权值$$9 > 5$$),添加快捷边$$4 \to 1$$(新边);节点$$5$$(权值$$4$$)在栈$$[9, 5]$$中找到节点$$4$$(权值$$5 > 4$$),添加快捷边$$5 \to 4$$(与树边重复)。BFS从根出发:节点$$4$$经快捷边直达根(距离$$1$$),节点$$5$$经$$5 \to 4 \to 1$$(距离$$2$$)。最终输出$$0, 1, 1, 1, 2$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,建树$$O(n)$$,单调栈DFS每个节点做一次$$O(\log n)$$的二分搜索,BFS$$O(n)$$。
  • 空间复杂度:$$O(n)$$,存储树结构、单调栈、增强图和距离数组。

Python

# 无向树 - 单调栈 + BFS
import sys
from collections import deque


def find_shortcuts(n, x, children):
    """单调栈 + 二分搜索,找每个节点最近权值更大的祖先"""
    stk_w = [0] * (n + 1)   # 单调递减权值栈
    stk_nd = [0] * (n + 1)  # 对应节点编号
    shortcut = [0] * (n + 1)
    top = 0

    # 迭代DFS:(节点, 阶段, 保存位置k, 保存的旧权值, 旧节点, 旧top)
    dfs = [(1, 0, 0, 0, 0, 0)]
    while dfs:
        v, phase, k_s, sw_s, sn_s, ot_s = dfs.pop()
        if phase == 1:
            # 回溯:恢复栈状态
            stk_w[k_s] = sw_s
            stk_nd[k_s] = sn_s
            top = ot_s
            continue

        w = x[v]
        # 二分搜索:在递减栈中找第一个权值 <= w 的位置
        lo, hi = 0, top
        while lo < hi:
            mid = (lo + hi) // 2
            if stk_w[mid] > w:
                lo = mid + 1
            else:
                hi = mid
        k = lo

        # k-1 位置是最近的权值更大的祖先
        if k > 0:
            shortcut[v] = stk_nd[k - 1]

        # 保存旧值并压栈
        old_w, old_nd, old_top = stk_w[k], stk_nd[k], top
        stk_w[k] = w
        stk_nd[k] = v
        top = k + 1

        # 回溯标记入栈,再压入子节点(逆序保证顺序处理)
        dfs.append((v, 1, k, old_w, old_nd, old_top))
        for c in reversed(children[v]):
            dfs.append((c, 0, 0, 0, 0, 0))

    return shortcut


def bfs_shortest(n, children, shortcut):
    """在添加快捷边后的图上BFS求最短距离"""
    aug = [[] for _ in range(n + 1)]
    for v in range(1, n + 1):
        for c in children[v]:
            aug[v].append(c)
            aug[c].append(v)
        if shortcut[v]:
            aug[v].append(shortcut[v])
            aug[shortcut[v]].append(v)

    dist = [-1] * (n + 1)
    dist[1] = 0
    q = deque([1])
    while q:
        u = q.popleft()
        for w in aug[u]:
            if dist[w] == -1:
                dist[w] = dist[u] + 1
                q.append(w)
    return dist


data = sys.stdin.buffer.read().split()
idx = 0
n = int(data[idx]); idx += 1
x = [0] * (n + 1)
for i in range(1, n + 1):
    x[i] = int(data[idx]); idx += 1

adj = [[] for _ in range(n + 1)]
for _ in range(n - 1):
    u, v = int(data[idx]), int(data[idx + 1]); idx += 2
    adj[u].append(v)
    adj[v].append(u)

# 以1为根建树
children = [[] for _ in range(n + 1)]
visited = [False] * (n + 1)
visited[1] = True
q = deque([1])
while q:
    u = q.popleft()
    for v in adj[u]:
        if not visited[v]:
            visited[v] = True
            children[u].append(v)
            q.append(v)

shortcut = find_shortcuts(n, x, children)
dist = bfs_shortest(n, children, shortcut)
print(' '.join(str(dist[i]) for i in range(1, n + 1)))

第四题:节点最大权值

在线测评链接:https://www.neituiya.com/oj/10/2331

题目描述

给你一个由$$n$$个编号为$$1 \sim n$$的节点以及$$m$$条编号为$$1 \sim m$$的边组成的无向图,我们定义一个节点的权值为它的当前度(即已执行完之前所有操作后的状态)加上它的节点编号。

小美会进行$$q$$次如下操作:

操作一:断开编号为$$x$$的边,保证每条边至多被删除一次,即在进行操作一时,该边当前一定存在于图中。

操作二:向你询问编号为$$x$$的节点所在的连通块中所有节点中最大的权值,你需要将此权值告诉他。

度:与一个顶点相连接的边的条数称为该顶点的度。

连通块:也称连通分量,满足以下条件:1. 是原图的一个子图。2. 连通块内的任意两个顶点之间都存在路径相连,且路径上的点也在连通块内。3. 是极大的,即不能再通过添加原图中的其他顶点而依旧保持连通性。4. 单独的点也构成一个连通块,连通块的大小即为连通块中顶点的数量。

输入描述

第一行输入三个正整数$$n, m, q(1 \le n, q \le 2 \times 10^5, 0 \le m \le \min(\frac{n \times (n-1)}{2}, 2 \times 10^5))$$,表示节点个数、边个数、操作次数。

此后$$m$$行,第$$i$$行输入两个整数$$u_i, v_i(1 \le u_i, v_i \le n, u_i \ne v_i)$$,表示图上第$$i$$条边连接节点$$u_i$$和$$v_i$$。

此后$$q$$行,第$$i$$行先输入一个整数$$o_i(1 \le o_i \le 2)$$,表示操作编号。随后在同一行:若$$o_i=1$$,输入一个整数$$x_i(1 \le x_i \le m)$$,表示断掉的边的编号;若$$o_i=2$$,输入一个整数$$x_i(1 \le x_i \le n)$$,表示询问的节点编号。

保证图没有重边和自环,操作一合法。

输出描述

输出若干行,每一行对操作二进行回答。

样例1

输入

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

输出

7
5
6

样例解释

初始时所有节点通过$$5$$条边全部连通,各节点度数为$$3, 2, 2, 1, 2$$,权值(度+编号)为$$4, 4, 5, 5, 7$$。查询节点$$4$$所在连通块最大权值为$$7$$。删除边$$1$$(连接节点$$1, 2$$)后,节点$$1, 2$$度数各减$$1$$,图分为$$\{1, 3, 5\}$$和$$\{2, 4\}$$两个连通块。查询节点$$2$$所在连通块$$\{2, 4\}$$最大权值为$$5$$。删除边$$2$$(连接节点$$1, 5$$)后,节点$$1, 5$$度数各减$$1$$,查询节点$$1$$所在连通块$$\{1, 3, 5\}$$最大权值为$$6$$。

题解

本题涉及到并查集,不熟悉该算法的同学可以先做一下模板题:

并查集-模版题

连通块个数(一)

题目内容拆解

给定无向图,支持删边和查询节点所在连通块的最大权值(权值=当前度+编号)。$$n, m, q$$均可达$$2 \times 10^5$$,需要高效维护连通性和最大值。

核心观察:并查集只支持"合并"不支持"分裂",因此正向删边无法直接用并查集处理。但如果逆序处理所有操作,删边就变成了加边,恰好适合并查集。

算法实现

算法主策略:本题采用逆序处理 + 并查集

第一步:预处理。先读取所有操作,标记哪些边最终会被删除。用未被删除的边建立初始图,计算各节点的初始度数和权值,并用并查集维护连通块及其最大权值。

第二步:逆序处理。从最后一个操作往前处理。遇到查询操作,直接记录当前连通块的最大权值;遇到删边操作,将其视为"加边"——两端节点度数各加$$1$$,更新它们所在连通块的最大权值,然后合并两个连通块。

关键性质:逆序处理时权值只增不减(只加边,度只增加),因此连通块的最大值只需在加边时与新权值比较即可,无需遍历整个连通块。

以样例为例,边$$1$$(节点$$1, 2$$)和边$$2$$(节点$$1, 5$$)被删除。初始图仅含边$$3, 4, 5$$,度数为$$1, 1, 2, 1, 1$$,权值为$$2, 3, 5, 5, 6$$。连通块$$\{1, 3, 5\}$$最大权值$$6$$,连通块$$\{2, 4\}$$最大权值$$5$$。逆序处理第$$5$$个操作(查询节点$$1$$)得$$6$$;第$$4$$个操作加回边$$2$$,节点$$1, 5$$度数各加$$1$$,权值变为$$3, 7$$,连通块最大更新为$$7$$;第$$3$$个操作(查询节点$$2$$)得$$5$$;第$$2$$个操作加回边$$1$$,合并两个连通块,最大为$$7$$;第$$1$$个操作(查询节点$$4$$)得$$7$$。最终答案倒序输出:$$7, 5, 6$$。

时空复杂度分析

  • 时间复杂度:$$O((m + q) \alpha(n))$$,其中$$\alpha(n)$$是阿克曼函数的反函数,近似常数。每条边和每个操作各处理一次,并查集操作均为$$O(\alpha(n))$$。
  • 空间复杂度:$$O(n + m + q)$$,存储图的边、操作序列和并查集数组。

Java

// 节点最大权值 - 逆序处理 + 并查集
import java.io.*;
import java.util.*;

public class Main {
    static int[] par, rnk, maxW, deg;

    static int find(int x) {
        while (par[x] != x) {
            par[x] = par[par[x]];
            x = par[x];
        }
        return x;
    }

    static void unite(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return;
        if (rnk[ra] < rnk[rb]) { int t = ra; ra = rb; rb = t; }
        par[rb] = ra;
        maxW[ra] = Math.max(maxW[ra], maxW[rb]);
        if (rnk[ra] == rnk[rb]) rnk[ra]++;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        int q = Integer.parseInt(st.nextToken());

        int[][] edges = new int[m][2];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            edges[i][0] = Integer.parseInt(st.nextToken());
            edges[i][1] = Integer.parseInt(st.nextToken());
        }

        int[][] ops = new int[q][2];
        // 记录被删除的边,用于逆序处理
        Set<Integer> deleted = new HashSet<>();
        for (int i = 0; i < q; i++) {
            st = new StringTokenizer(br.readLine());
            ops[i][0] = Integer.parseInt(st.nextToken());
            ops[i][1] = Integer.parseInt(st.nextToken());
            if (ops[i][0] == 1) deleted.add(ops[i][1] - 1);
        }

        // 初始化:仅保留未被删除的边
        deg = new int[n + 1];
        for (int i = 0; i < m; i++) {
            if (!deleted.contains(i)) {
                deg[edges[i][0]]++;
                deg[edges[i][1]]++;
            }
        }
        par = new int[n + 1];
        rnk = new int[n + 1];
        maxW = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            par[i] = i;
            maxW[i] = deg[i] + i;
        }
        for (int i = 0; i < m; i++) {
            if (!deleted.contains(i))
                unite(edges[i][0], edges[i][1]);
        }

        // 逆序处理:删边变加边
        List<Integer> answers = new ArrayList<>();
        for (int i = q - 1; i >= 0; i--) {
            if (ops[i][0] == 2) {
                answers.add(maxW[find(ops[i][1])]);
            } else {
                int idx = ops[i][1] - 1;
                int u = edges[idx][0], v = edges[idx][1];
                deg[u]++;
                deg[v]++;
                int ru = find(u), rv = find(v);
                maxW[ru] = Math.max(maxW[ru], deg[u] + u);
                maxW[rv] = Math.max(maxW[rv], deg[v] + v);
                unite(u, v);
            }
        }

        Collections.reverse(answers);
        for (int x : answers) sb.append(x).append('\n');
        System.out.print(sb);
    }
}

携程

2026-4-23-算法岗

第一题:炒鸡回文构造

在线评测链接:https://www.neituiya.com/oj/9/2601

题目描述

我们定义一个长度为 $$n$$ 的数组 $$\{a_1,a_2,\dots,a_n\}$$ 是回文数组,当且仅当对于任意的 $$i(1 \le i \le n)$$ 都有 $$a_i = a_{n-i+1}$$。

现在,给定一个正整数长度 $$n$$,我们想知道:对于所有满足 $$m \ge n$$ 的正整数 $$m$$,是否都存在一个长度为 $$n$$ 的回文数组,使其所有元素均为正整数且元素之和恰好等于 $$m$$?

如果满足条件,输出 $$Yes$$;否则,输出 $$No$$。

输入描述

第一行输入一个整数 $$T(1 \le T \le 10^4)$$,表示数据组数。

此后每组测试数据在一行上输入一个整数 $$n(1 \le n \le 10^9)$$,表示数组长度。

输出描述

对于每组测试数据,新起一行输出 $$Yes$$ 或 $$No$$。

样例1

输入

4
1
2
1000000000
999999999

输出

Yes
No
No
Yes

第二题:炒鸡钞票构造

在线评测链接:https://www.neituiya.com/oj/9/2602

题目描述

AK机有两种不同面额的钞票各无限张:一种面值为 $$n$$ 元,另一种面值为 $$n+1$$ 元。AK机想购买一件价格为 $$m$$ 元的商品。该自助商店没有找零系统,即,若付款金额超过商品价格,商店不会找零,多余部分由买家自行承担;付款金额必须不少于商品价格。请你计算,若想完成购买,最少需要额外多支付多少元;若能刚好支付,则额外花费为 $$0$$。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^4)$$ 表示数据组数。

每组测试数据在一行上输入两个整数 $$n, m(1 \le n, m \le 10^9)$$,分别表示钞票的较小面值与商品价格。

输出描述

对于每一组测试数据,新起一行输出一个整数,表示最少需要额外多支付的金额。

样例1

输入

3
3 8
4 6
5 7

输出

0
2
3

样例解释

  1. 第一组输入 $$n=3, m=8$$:种钞票面值为 $$3$$ 元和 $$4$$ 元。使用 $$2$$ 张 $$4$$ 元钞票,$$4 \times 2 = 8$$,刚好支付,额外支付 $$0$$。
  2. 第二组输入 $$n=4, m=6$$:两种钞票面值为 $$4$$ 元和 $$5$$ 元。无法凑出恰好 $$6$$ 元,最小的不低于 $$6$$ 的付款为 $$4 \times 2 = 8$$,额外支付 $$8 - 6 = 2$$。
  3. 第三组输入 $$n=5, m=7$$:两种钞票面值为 $$5$$ 元和 $$6$$ 元。无法凑出恰好 $$7$$ 元,最小的不低于 $$7$$ 的付款为 $$5 \times 2 = 10$$,额外支付 $$10 - 7 = 3$$。

第三题:用历史数据挑选Logistic C

在线评测链接:https://www.neituiya.com/oj/9/2603

题目描述

给定一张历史元数据表(每行包含数据集简单特征和其在线最优C)、以及一份当前任务的训练/测试数据,请实现一个基于 K-NN 的超参数元学习器:

1. 数据集元特征

对每个数据集都计算三维向量 $$\mathbf{m} = [\text{samples}, \text{features}, \text{imbalance}]$$其中 $$samples$$ 为训练样本数 $$n$$,$$features$$ 为特征维度 $$d$$,$$imbalance = |n_{pos} - n_{neg}| / n$$。

2. K最近邻检索

输入给出 $$history$$:每行 $$meta = m_h$$、$$C$$、$$score$$(越大越好)计算当前任务元向量 $$\mathbf{m}_{ctr}$$ 到所有历史数据的 $$\ell_2$$ 距离,取 $$K=3$$ 个最近邻;如距离并列按行次序决定。

3. 汇聚与选 C^*

对这 $$3$$ 行,统计所有出现过的 $$C$$;计算各 $$C$$ 的平均 $$score$$(若某邻居中未出现该 $$C$$,则忽略);取平均分最高者为 $$C^*$$;若并列,则取数值最小。

4. 模型训练 + 预测

使用 $$LogisticRegression(penalty=\text{"l2"}, C=C^*, solver=\text{"lbfgs"}, max\_iter=1000, random\_state=42)$$,用完整训练数据拟合,输出测试集标签。

输入描述

单行 JSON,格式如下:

{
  "train_X": [[1.0,2.0], ...],
  "train_y": [0, 1, ...],
  "test_X": [[...], ...],
  "history": [
    {"meta": [50, 4, 0.10], "C": 0.1, "score": 0.80},
    {"meta": [120, 2, 0.25], "C": 1.0, "score": 0.85},
    ...
  ]
}

约束:$$|\text{train}| \le 60$$,$$|\text{test}| \le 15$$,$$d \le 4$$。$$history$$ 至少 $$6$$ 条,$$C$$ 取值 $$\{0.1, 0.3, 1, 3, 10\}$$。所有值数值型,无缺失。

输出描述

仅一行 JSON:

{"C_star": 1.0, "pred": [0, 1, 0, ...]}

$$pred$$ 长度等于 $$|\text{test}_X|$$,元素为 $$0/1$$。

补充说明

  • 不得重新搜索其它 $$C$$;只能通过步骤 2-3 得到 $$C^*$$。
  • 所有随机源固定 $$random\_state=42$$,流程纯确定性。
  • 为了确保通过测试用例,仅允许使用 numpy、pandas、scikit-learn。

样例1

输入

{"train_X": [[0.0,0.0],[0.2,0.4],[0.3,0.5],[0.1,0.2],[1.0,1.1],[1.2,1.3],[1.3,1.4],[1.1,1.0]], "train_y": [0,0,0,0,1,1,1,1], "test_X": [[0.15,0.25],[1.15,1.25]], "history": [{"meta":[50,2,0.10],"C":0.1,"score":0.82},{"meta":[48,2,0.20],"C":0.3,"score":0.79},{"meta":[60,2,0.00],"C":1.0,"score":0.85},{"meta":[40,4,0.30],"C":3.0,"score":0.80},{"meta":[55,3,0.18],"C":0.3,"score":0.83},{"meta":[52,2,0.10],"C":1.0,"score":0.81},{"meta":[58,2,0.05],"C":10.0,"score":0.78}]}

输出

{"C_star": 0.1, "pred": [0, 1]}

第四题:序列倍数交换

在线评测链接:https://www.neituiya.com/oj/9/2604

题目描述

给定一个长度为 $$n$$ 的序列 $$a$$,其中第 $$i$$ 个元素的值为 $$a_i$$。现在AK机可以对序列进行任意次如下操作:选择两个不同的下标 $$i, j(1 \le i, j \le n, i \ne j)$$,且满足 $$a_i$$ 与 $$a_j$$ 为倍数关系(即 $$a_i | a_j$$ 或 $$a_j | a_i$$),然后交换 $$a_i$$ 和 $$a_j$$。(其中 $$|$$ 表示整除符号。)

你的任务就是求出,在以上操作可以进行任意次的前提下,最小化 $$a$$ 的字典序,并输出此时的 $$a$$。

数组的字典序比较:从左到右逐个比较两个数组的元素。如果在某个位置上元素不同,比较这两个元素的大小,元素大的数组字典序也大。如果一直比较到其中一个数组结束,则长度较短的数组字典序更小。

输入描述

每个测试文均包含多组测试数据。第一行输入一个整数 $$T(1 \le T \le 10^5)$$ 代表数据组数。

每组测试数据第一行一个整数 $$n(1 \le n \le 5 \times 10^5)$$,表示序列 $$a$$ 的长度。

第二行 $$n$$ 个整数 $$a_i(1 \le a_i \le n)$$,表示序列 $$a$$。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$10^6$$。

输出描述

对于每组测试数据,在单独的一行输出 $$n$$ 个整数,表示序列 $$a$$ 字典序最小的结果。

样例1

输入

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

输出

1 2 3 4 5
2 2 3 4 6 5

样例解释

第一组测试数据:选择 $$i=2, j=4$$,满足 $$a_i=4, a_j=2$$,$$4$$ 是 $$2$$ 的倍数,因此可以交换两者;交换后序列变为 $$1, 2, 3, 4, 5$$,这是字典序最小的结果。

第二组测试数据:满足倍数关系的元素可以自由交换,将可交换范围内的最小元素放置在对应位置,最终得到字典序最小的序列 $$2, 2, 3, 4, 6, 5$$。

题解:并查集

2026-4-12-算法岗

第一题:合数求解

在线评测链接:https://www.neituiya.com/oj/9/2509

第二题:灯带相融度最大化

在线评测链接:https://www.neituiya.com/oj/9/2510

第三题:NGD优化器实现

在线评测链接:https://www.neituiya.com/oj/9/2511

第四题:数字分裂求和

在线评测链接:https://www.neituiya.com/oj/9/2512

2026-3-29-研发岗

第一题:在哪里呢

在线评测链接::https://www.neituiya.com/oj/9/2433

第二题:实时排名

在线评测链接::https://www.neituiya.com/oj/9/2434

第三题:字符串min

在线评测链接::https://www.neituiya.com/oj/9/2435

第四题:min和gcd

在线评测链接::https://www.neituiya.com/oj/9/2436

2026-3-29-算法岗

第一题:在哪里呢

在线评测链接::https://www.neituiya.com/oj/9/2437

题目描述

给你一个长度恰好为 $$5$$ 的字符串,其下标从 $$1$$ 开始,保证其恰好由一个 a,一个 b,一个 c,一个 d,一个 e 组成,你现在想知道字符 a 位于第几个位置。

输入描述

一行,一个长度为 $$5$$ 的字符串。

输出描述

一行,表示字符 a 在字符串中的第几个位置。

样例1

输入

abcde

输出

1

题解

题目内容拆解

在长度为 $$5$$ 的字符串中找到字符 a 的位置(下标从 $$1$$ 开始)。

算法实现

算法主策略:本题采用线性扫描,遍历字符串找到 a 的位置输出即可。

时空复杂度分析

  • 时间复杂度:$$O(1)$$,字符串长度固定为 $$5$$。
  • 空间复杂度:$$O(1)$$。

C++

// 在哪里呢 - 模拟
#include <bits/stdc++.h>
using namespace std;

// 找到字符a在字符串中的位置(1-indexed)
int solve(const string& s) {
    for (int i = 0; i < 5; i++) {
        if (s[i] == 'a') return i + 1;
    }
    return -1;
}

int main() {
    string s;
    cin >> s;
    cout << solve(s) << endl;
    return 0;
}

第二题:实时排名

在线评测链接::https://www.neituiya.com/oj/9/2438

题目描述

AK机正在参加一场 IOI 赛制的比赛,每个题目可以多次提交,取得分最高的一次计分,总分即为所有题目最高分之和。现在,有若干道独立的题目,按时间顺序依次有 $$n$$ 次提交,第 $$i$$ 次提交记为三元组 $$(a_i, b_i, c_i)$$,表示用户 $$a_i$$ 在题目 $$b_i$$ 上获得分数 $$c_i$$。

在每次提交处理完成后,需要报告AK机(记编号为 $$1$$)的名次。排名采用如下规则:

1.若总分不同,则总分高者排名在前。

2.若总分相同,则并列相同名次,但占用多个名次位置(例如,分数前四高的人的分数分别为 $$100, 100, 100, 80$$,则他们的排名为 $$1, 1, 1, 4$$)。

输入描述

第一行输入一个整数 $$n(1 \le n \le 2 \times 10^5)$$,表示提交记录的数量。

此后 $$n$$ 行,第 $$i$$ 行输入三个整数 $$a_i, b_i, c_i(1 \le a_i \le 100, 1 \le b_i \le 100, 0 \le c_i \le 100)$$,表示第 $$i$$ 次提交记录。

输出描述

对于每一次提交,输出一个整数,表示第 $$i$$ 次记录后AK机的排名。

样例1

输入

10
2 1 0
1 1 80
3 2 100
1 2 60
3 1 40
3 3 60
1 1 90
5 1 100
5 2 100
1 4 50

输出

1
1
2
1
1
2
2
2
3
1

题解

题目内容拆解

模拟 IOI 赛制,维护每个用户在每道题的最高分,每次提交后计算AK机(编号 $$1$$)的排名。

算法实现

算法主策略:本题采用模拟,用二维数组 $$score[a][b]$$ 记录用户 $$a$$ 在题目 $$b$$ 上的最高分,$$total[a]$$ 记录用户 $$a$$ 的总分。

每次提交 $$(a, b, c)$$:若 $$c > score[a][b]$$,则更新 $$score[a][b] = c$$,同时 $$total[a]$$ 增加差值 $$c - score[a][b]$$。

输出排名时,统计所有用户中总分严格大于 $$total[1]$$ 的人数 $$cnt$$,AK机的排名为 $$cnt + 1$$。

用户数和题目数都不超过 $$100$$,直接用数组维护即可。

时空复杂度分析

  • 时间复杂度:$$O(n \cdot U)$$,其中 $$U = 100$$ 是用户数上限。每次提交后遍历所有用户统计排名。
  • 空间复杂度:$$O(U \cdot P)$$,其中 $$P = 100$$ 是题目数上限。

Java

// 实时排名 - 模拟
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        // score[a][b]记录用户a在题目b的最高分
        int[][] score = new int[101][101];
        int[] total = new int[101];
        boolean[] seen = new boolean[101];
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            int c = Integer.parseInt(st.nextToken());
            seen[a] = true;
            // 只有分数更高时才更新
            if (c > score[a][b]) {
                total[a] += c - score[a][b];
                score[a][b] = c;
            }
            // 统计总分严格大于用户1的人数
            int cnt = 0;
            for (int j = 1; j <= 100; j++) {
                if (seen[j] && total[j] > total[1]) cnt++;
            }
            sb.append(cnt + 1).append('\n');
        }
        System.out.print(sb);
    }
}

第三题:双门控序列加权器

在线评测链接::https://www.neituiya.com/oj/9/2439

题目描述

在仅使用 numpy/pandas/scikit-learn 的前提下,实现一种新型注意力机制 TG-SA,并完成给定序列的加权求和。与经典 $$QK^T / \sqrt{d}$$ + Softmax 不同,TG-SA 通过双门控折线 + Top-k 保留得到更稀疏、可解释的权重。

给定单头 Query/Key/Value 张量 $$Q, K, V \in \mathbb{R}^{L \times d}$$(长度 $$L$$、隐维 $$d$$),以及超参:

$$k\_top$$:每行保留前 $$k\_top$$ 个最大注意力得分。

$$\alpha$$:第 $$1$$ 门限系数($$0 \sim 1$$)。

$$\beta$$:第 $$2$$ 门限系数($$0 \sim 1$$),且 $$\beta > \alpha$$。

默认 $$\alpha = 0.2, \beta = 0.6, k\_top \le L$$。

计算过程:

1.初始打分:$$S = QK^T / \sqrt{d}$$($$L \times L$$)。

2.双门控折线映射:设 $$s_{max} = \max_j S_{ij}$$ 为行内最大值。分段映射到 $$[0, 1]$$:若 $$S_{ij} < \alpha \cdot s_{max}$$ 则 $$\tilde{S}_{ij} = 0$$;若 $$\alpha \cdot s_{max} \le S_{ij} < \beta \cdot s_{max}$$ 则 $$\tilde{S}_{ij} = \frac{S_{ij} - \alpha \cdot s_{max}}{(\beta - \alpha) \cdot s_{max}}$$(线性插值);若 $$S_{ij} \ge \beta \cdot s_{max}$$ 则 $$\tilde{S}_{ij} = 1$$。

3.Top-k 稀疏:每行仅保留 $$\tilde{S}_{ij}$$ 最大的 $$k\_top$$ 个位置,其余设 $$0$$。若行长 $$< k\_top$$ 则全保留。

4.行归一化:若行全 $$0$$,则改为均匀分布 $$1/L$$;否则归一化到和为 $$1$$:$$A_{ij} = \tilde{S}_{ij} / \sum_j \tilde{S}_{ij}$$。

5.输出:$$O = AV$$($$L \times d$$),同时返回注意力权重矩阵 $$A$$。

输入描述

单行 JSON,包含 Q($$L \times d$$)、K($$L \times d$$)、V($$L \times d$$)、k\_top($$2 \le k\_top \le L$$),以及可选的 alpha 和 beta。

约束:$$2 \le L \le 6, 2 \le d \le 4$$,所有元素为浮点。

输出描述

单行 JSON,包含 A($$L \times L$$,6位小数)和 O($$L \times d$$,6位小数)。

样例1

输入

{"Q":[[1,0],[0,1]],"K":[[1,0],[0,1]],"V":[[1,2],[3,4]],"k_top":1}

输出

{"A":[[1.0,0.0],[0.0,1.0]],"O":[[1.0,2.0],[3.0,4.0]]}

题解

题目内容拆解

按步骤实现 TG-SA 注意力机制:初始打分 → 双门控折线映射 → Top-k 稀疏 → 行归一化 → 输出。

算法实现

算法主策略:本题采用 NumPy 矩阵运算,按题意五步依次实现。

1.计算 $$S = QK^T / \sqrt{d}$$,得到 $$L \times L$$ 的打分矩阵。

2.双门控折线:对每行,计算行最大值 $$s_{max}$$,两个阈值 $$low = \alpha \cdot s_{max}$$、$$high = \beta \cdot s_{max}$$。低于 $$low$$ 的截断为 $$0$$,$$low$$ 到 $$high$$ 之间线性映射到 $$[0, 1)$$:$$\frac{s - low}{high - low}$$,$$\ge high$$ 的直接映射为 $$1$$。

3.Top-k:每行排序后只保留最大的 $$k\_top$$ 个值,其余清零。

4.归一化:行和为 $$0$$ 时用均匀分布 $$1/L$$,否则除以行和。

5.输出 $$O = AV$$,结果保留6位小数。

时空复杂度分析

  • 时间复杂度:$$O(L^2 \cdot d)$$,矩阵乘法为主要开销。
  • 空间复杂度:$$O(L^2)$$,存储注意力矩阵。

Python

# 双门控序列加权器 - TG-SA注意力机制
import json
import numpy as np

def solve(data):
    Q = np.array(data['Q'], dtype=float)
    K = np.array(data['K'], dtype=float)
    V = np.array(data['V'], dtype=float)
    k_top = data['k_top']
    alpha = data.get('alpha', 0.2)
    beta = data.get('beta', 0.6)
    L, d = Q.shape

    # Step 1: 初始打分 S = Q @ K^T / sqrt(d)
    S = Q @ K.T / np.sqrt(d)

    # Step 2: 双门控折线映射
    S_tilde = np.zeros_like(S)
    for i in range(L):
        s_max = np.max(S[i])
        low = alpha * s_max
        high = beta * s_max
        for j in range(L):
            s = S[i][j]
            if s < low:
                # 低于第一门限,截断为0
                S_tilde[i][j] = 0
            elif s < high:
                # 两门限之间,线性映射到[0,1)
                S_tilde[i][j] = (s - low) / ((beta - alpha) * s_max)
            else:
                # 高于第二门限,映射为1
                S_tilde[i][j] = 1

    # Step 3: Top-k稀疏,每行保留k_top个最大值
    for i in range(L):
        if k_top < L:
            indices = np.argsort(S_tilde[i])[::-1]
            for j in indices[k_top:]:
                S_tilde[i][j] = 0

    # Step 4: 行归一化
    A = np.zeros_like(S_tilde)
    for i in range(L):
        row_sum = np.sum(S_tilde[i])
        if row_sum == 0:
            A[i] = 1.0 / L
        else:
            A[i] = S_tilde[i] / row_sum

    # Step 5: 输出 O = A @ V
    O = A @ V

    result = {
        "A": [[round(float(x), 6) for x in row] for row in A],
        "O": [[round(float(x), 6) for x in row] for row in O]
    }
    return result

data = json.loads(input())
print(json.dumps(solve(data)))

第四题:min和gcd

在线评测链接::https://www.neituiya.com/oj/9/2440

题目描述

给定整数 $$a, b, n$$,定义 $$f(x) = \min(x, b) + \gcd(x, b)$$。记 $$f^{(1)}(x) = f(x)$$,$$f^{(k+1)}(x) = f(f^{(k)}(x))$$。请计算 $$f^{(n)}(a)$$ 的值。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1 \le T \le 2 \times 10^3)$$ 代表数据组数,每组测试数据描述如下:

接下来 $$T$$ 行,每行输入三个整数 $$a, b, n(1 \le a, b \le 10^{12}, 1 \le n \le 10^{18})$$。

输出描述

输出 $$T$$ 行,每行一个整数,表示对应数据的 $$f^{(n)}(a)$$ 的值。

样例1

输入

4
7 3 4
10 2 3
3 10 2
6 50 100

输出

4
4
6
100

样例解释

当 $$(a, b, n) = (7, 3, 4)$$:$$f(7) = \min(7, 3) + \gcd(7, 3) = 3 + 1 = 4$$,之后 $$f(4) = \min(4, 3) + \gcd(4, 3) = 3 + 1 = 4$$,收敛到 $$4$$。

当 $$(a, b, n) = (3, 10, 2)$$:$$f(3) = 3 + \gcd(3, 10) = 4$$,$$f(4) = 4 + \gcd(4, 10) = 6$$。

当 $$(a, b, n) = (6, 50, 100)$$:$$f(6) = 6 + \gcd(6, 50) = 8$$,$$f(8) = 8 + \gcd(8, 50) = 10$$,...,逐步递增直到 $$\ge b$$ 后收敛到 $$b + \gcd(b, b) = 2b$$,最终稳定在 $$100$$。

题解

题目内容拆解

反复对 $$a$$ 应用 $$f(x) = \min(x, b) + \gcd(x, b)$$,共 $$n$$ 次。$$n$$ 最大 $$10^{18}$$,暴力一步步迭代必然超时。需要找到数学规律来批量跳过。

算法实现

算法主策略:本题采用质数筛预处理 + 分段跳跃模拟。下面从零开始推导为什么可以跳步。

第一步:分两种情况讨论 $$f(x)$$

当 $$x < b$$ 时:$$\min(x, b) = x$$,所以 $$f(x) = x + \gcd(x, b)$$,$$x$$ 变大了(因为 $$\gcd \ge 1$$)。

当 $$x \ge b$$ 时:$$\min(x, b) = b$$,所以 $$f(x) = b + \gcd(x, b)$$。这个值和 $$x$$ 无关(只取决于 $$\gcd(x,b)$$),而且 $$f(f(x)) = b + \gcd(b + \gcd(x,b), b) = b + \gcd(x,b) = f(x)$$,所以 $$f(x)$$ 本身就是不动点,后续无论迭代多少次都不变。

结论:一旦 $$x \ge b$$,再算一步就永远不变了。

第二步:$$x < b$$ 阶段,证明 $$\gcd$$ 非递减

设当前 $$g = \gcd(x, b)$$,下一步 $$x' = x + g$$。

因为 $$g \mid x$$($$g$$ 是 $$x$$ 的因子)且 $$g \mid b$$,所以 $$g \mid (x + g) = x'$$。

又 $$g \mid b$$,所以 $$g \mid \gcd(x', b)$$,即新的 $$\gcd \ge g$$。

换句话说:$$\gcd$$ 只会变大或不变,绝不会变小

第三步:$$\gcd$$ 不变时可以批量跳步

当 $$\gcd$$ 保持为 $$g$$ 时,$$x$$ 每步增加 $$g$$,序列是 $$g \cdot k, g \cdot (k+1), g \cdot (k+2), \ldots$$。

这里令 $$m = b / g$$,$$k = x / g$$,此时 $$\gcd(k, m) = 1$$(因为 $$g$$ 已经是 $$\gcd(x,b)$$ 了,提取公因子后互质)。

什么时候 $$\gcd$$ 会变大?就是最早出现某个 $$\Delta > 0$$,使得 $$\gcd(k + \Delta, m) > 1$$。这等价于 $$k + \Delta$$ 被 $$m$$ 的某个质因子 $$p$$ 整除。

对于 $$m$$ 的每个质因子 $$p$$,$$k + \Delta$$ 能被 $$p$$ 整除的最小 $$\Delta$$ 是 $$p - (k \bmod p)$$。因为 $$\gcd(k, m) = 1$$,$$k$$ 不被 $$p$$ 整除,所以 $$k \bmod p \ne 0$$,$$\Delta$$ 一定是 $$[1, p-1]$$ 内的正数。

在所有质因子 $$p$$ 中取最小的 $$\Delta$$,就是 $$\gcd$$ 变大前的步数。一次跳完这 $$\Delta$$ 步,$$x$$ 直接增加 $$g \cdot \Delta$$。

第四步:质数筛加速分解

上面需要枚举 $$m$$ 的质因子,而 $$m = b / g$$ 的质因子一定是 $$b$$ 的质因子的子集。所以只需要在开头分解一次 $$b$$ 的质因子,后续复用。

但直接试除分解 $$b$$ 需要试到 $$\sqrt{b}$$($$b \le 10^{12}$$ 时约 $$10^6$$ 次除法),$$T = 2000$$ 组总计 $$2 \times 10^9$$ 次,会超时。

优化:预筛 $$10^6$$ 以内的所有质数(约 $$78498$$ 个),分解时只试除质数、跳过合数,每次分解只需约 $$7 \times 10^4$$ 次除法。

完整样例推导:$$(a, b, n) = (3, 10, 2)$$

分解 $$b = 10$$ 的质因子:$$10 = 2 \times 5$$,质因子集合 $$\{2, 5\}$$。

第1轮:$$x = 3 < b = 10$$,$$g = \gcd(3, 10) = 1$$,$$m = 10, k = 3$$。

枚举质因子:$$p = 2$$,$$\Delta_2 = 2 - (3 \bmod 2) = 2 - 1 = 1$$。$$p = 5$$,$$\Delta_5 = 5 - (3 \bmod 5) = 5 - 3 = 2$$。还有上界 $$\Delta_{max} = m - k = 7$$。取 $$\Delta = \min(1, 2, 7) = 1$$。跳 $$1$$ 步:$$x = 3 + 1 \times 1 = 4$$,$$n$$ 剩 $$1$$。

第2轮:$$x = 4 < 10$$,$$g = \gcd(4, 10) = 2$$($$\gcd$$ 从 $$1$$ 变大到 $$2$$),$$m = 5, k = 2$$。

枚举质因子:$$p = 2$$,$$5 \bmod 2 \ne 0$$ 所以跳过($$2$$ 不是 $$m = 5$$ 的因子)。$$p = 5$$,$$\Delta_5 = 5 - (2 \bmod 5) = 3$$。上界 $$m - k = 3$$。取 $$\Delta = \min(3, 3) = 3$$,但 $$n$$ 只剩 $$1$$,跳 $$1$$ 步:$$x = 4 + 2 \times 1 = 6$$,$$n = 0$$。

输出 $$6$$ ✅。

时空复杂度分析

  • 时间复杂度:预筛 $$O(L \log \log L)$$($$L = 10^6$$,一次性开销)。每组测试分解 $$b$$ 约 $$O(\frac{\sqrt{b}}{\ln \sqrt{b}})$$,分段跳跃 $$O(\log b)$$ 次($$\gcd$$ 最多变化 $$O(\log b)$$ 次),每次遍历质因子 $$O(\log b)$$。
  • 空间复杂度:$$O(\pi(10^6))$$,存储预筛质数表。

Go

// min和gcd - 质数筛 + 批量跳步
package main

import (
        "bufio"
        "fmt"
        "os"
)

func gcd(a, b int64) int64 {
        for b != 0 {
                a, b = b, a%b
        }
        return a
}

// 预筛10^6内质数
var sievedPrimes []int

func initSieve() {
        const LIM = 1000000
        ok := make([]bool, LIM+1)
        for i := range ok {
                ok[i] = true
        }
        ok[0], ok[1] = false, false
        for i := 2; i*i <= LIM; i++ {
                if ok[i] {
                        for j := i * i; j <= LIM; j += i {
                                ok[j] = false
                        }
                }
        }
        for i := 2; i <= LIM; i++ {
                if ok[i] {
                        sievedPrimes = append(sievedPrimes, i)
                }
        }
}

// 用筛好的质数快速分解
func factorize(val int64) []int64 {
        var res []int64
        for _, p := range sievedPrimes {
                pp := int64(p)
                if pp*pp > val {
                        break
                }
                if val%pp == 0 {
                        res = append(res, pp)
                        for val%pp == 0 {
                                val /= pp
                        }
                }
        }
        if val > 1 {
                res = append(res, val)
        }
        return res
}

func main() {
        initSieve()
        reader := bufio.NewReader(os.Stdin)
        writer := bufio.NewWriter(os.Stdout)
        defer writer.Flush()

        var t int
        fmt.Fscan(reader, &t)
        for ; t > 0; t-- {
                var x, b, n int64
                fmt.Fscan(reader, &x, &b, &n)
                pf := factorize(b)
                for n > 0 {
                        if x >= b {
                                x = b + gcd(x, b)
                                break
                        }
                        g := gcd(x, b)
                        m := b / g
                        k := x / g
                        // gcd(k,m)=1,找最小delta使gcd(k+delta,m)>1
                        delta := m - k
                        for _, p := range pf {
                                if m%p == 0 {
                                        d := p - k%p
                                        if d < delta {
                                                delta = d
                                        }
                                }
                        }
                        steps := delta
                        if n < steps {
                                steps = n
                        }
                        x += g * steps
                        n -= steps
                }
                fmt.Fprintln(writer, x)
        }
}

2026-3-12-研发岗

第一题:尾巴大人

在线评测链接:https://www.neituiya.com/oj/9/2316

题目描述

众所周知 $$n!$$ 很像一个尾巴,表示 $$n$$ 的阶乘即 $$1 \times 2 \times ... \times n$$。

给定一个整数 $$n$$,AK机想知道 $$n!$$ 的个位数字是多少,请输出这个值。

输入描述

输入一行一个整数 $$n(1 \le n \le 10)$$ 表示询问值。

输出描述

输出一个整数表示 $$n!$$ 的个位数字。

样例1

输入

1

输出

1

样例2

输入

3

输出

6

样例解释

$$3! = 1 \times 2 \times 3 = 6$$,个位是 $$6$$。

题解

题目内容拆解

给定 $$n(1 \le n \le 10)$$,求 $$n!$$ 的个位数字。由于 $$n$$ 最大只有 $$10$$,直接模拟即可。

算法实现

算法主策略:本题采用直接模拟,从 $$1$$ 乘到 $$n$$,最后取个位。

注意到当 $$n \ge 5$$ 时,$$n!$$ 一定包含因子 $$2 \times 5 = 10$$,因此个位一定是 $$0$$。所以只需要关心 $$n \le 4$$ 的情况:$$1! = 1$$,$$2! = 2$$,$$3! = 6$$,$$4! = 24$$(个位 $$4$$)。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,最多循环 $$10$$ 次。
  • 空间复杂度:$$O(1)$$,只用常数变量。

Python

# n的阶乘 - 模拟
n = int(input())

def solve(n):
    # n >= 5 时阶乘包含因子 10,个位一定是 0
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result % 10

print(solve(n))

第二题:拆盲盒

在线评测链接:https://www.neituiya.com/oj/9/2317

题目描述

给定一个正整数 $$n$$ 与一个正整数 $$m$$。你需要把 $$n$$ 拆分为若干个素数之和(可以重复选择同一个素数),设最终使用的素数个数为 $$k$$。

同时要求:所选素数中奇素数(除 $$2$$ 以外的素数)的数量必须是 $$m$$ 的正整数倍(不能为 $$0$$)。

请在满足约束的前提下,使 $$k$$ 最大化,并输出这个最大的 $$k$$。若不存在任何满足要求的拆分方案,输出 $$-1$$。

名词解释:

素数:大于 $$1$$ 且只有 $$1$$ 与其本身两个正因子的整数。奇素数:除 $$2$$ 以外的素数。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$t(1 \le t \le 10^4)$$ 表示测试数据组数,每组测试数据描述如下:

在一行上输入两个整数 $$n, m(2 \le n \le 10^{18}, 1 \le m \le 10^{18})$$。

输出描述

对于每一组测试数据,新起一行输出一个整数:满足要求的最大的素数个数 $$k$$;若无解,则输出 $$-1$$。

样例1

输入

4
6 2
2 1
30 4
1000000000000 1

输出

2
-1
13
499999999999

样例解释

对于第一组样例 $$6$$ 可以拆分为 $$3 + 3$$。

题解

题目内容拆解

将 $$n$$ 拆分为若干素数之和,使素数个数 $$k$$ 最大,且奇素数数量必须是 $$m$$ 的正整数倍。$$n$$ 高达 $$10^{18}$$,需要 $$O(1)$$ 单组查询。

核心观察:要最大化 $$k$$,应尽量使用最小的素数。偶素数只有 $$2$$,最小奇素数是 $$3$$。因此最优方案是用若干个 $$3$$(作为奇素数)和若干个 $$2$$(作为偶素数)来凑出 $$n$$。

算法实现

算法主策略:本题采用贪心 + 数论分析

设使用 $$b$$ 个奇素数(全选 $$3$$,最小化开销),$$a$$ 个偶素数(全选 $$2$$),则 $$n = 2a + 3b$$,$$k = a + b = \frac{n - b}{2}$$。

为了最大化 $$k$$,需要最小化 $$b$$。$$b$$ 必须是 $$m$$ 的正整数倍,所以优先尝试 $$b = m$$,不满足奇偶性则尝试 $$b = 2m$$。

可行性条件:$$3b \le n$$(奇素数总和不超过 $$n$$)且 $$(n - b)$$ 为偶数(剩余部分能被 $$2$$ 整除)。注意到 $$3b$$ 和 $$b$$ 的奇偶性相同,所以条件等价于 $$n$$ 和 $$b$$ 奇偶性相同。

若 $$b = m$$ 和 $$b = 2m$$ 都不满足,则无解输出 $$-1$$(例如 $$m$$ 为偶数而 $$n$$ 为奇数时,所有 $$m$$ 的倍数都是偶数,永远无法匹配奇数 $$n$$)。

时空复杂度分析

  • 时间复杂度:$$O(t)$$,每组查询 $$O(1)$$。
  • 空间复杂度:$$O(1)$$,只用常数变量。

Python

# 素数 - 贪心 + 数论

def solve(n, m):
    # 用最小素数凑:偶素数 2 和最小奇素数 3
    # n = 2*a + 3*b,b 为 m 的正整数倍,最大化 k = (n - b) / 2
    for mult in [1, 2]:
        b = m * mult
        if 3 * b > n:       # 奇素数总和超过 n
            break
        if (n - b) % 2 == 0: # 剩余能被 2 整除
            return (n - b) // 2
    return -1

t = int(input())
results = []
for _ in range(t):
    n, m = map(int, input().split())
    results.append(str(solve(n, m)))
print('\n'.join(results))

第三题:天才的数列切割

在线评测链接:https://www.neituiya.com/oj/9/2318

题目描述

给定一个长度为 $$n$$ 的数组 $$\{a_1, a_2, ..., a_n\}$$。你将对数组进行若干次"切割"操作,操作规则如下:

一开始,数组段集合仅包含整段 $$[1, n]$$。

每次操作,必须从"当前数组段集合"中选择一段完整的数组段 $$[l, r]$$(该段长度需严格大于 $$2$$),然后选择一个位置 $$mid(l < mid \le r)$$,若满足:

$$\sum_{i=l}^{mid-1} a_i = \sum_{i=mid}^{r} a_i$$

则可以把该数组段分成两段 $$[l, mid-1]$$ 与 $$[mid, r]$$,并以这两段替换原来的 $$[l, r]$$,继续加入到数组段集合中。

一旦一个数组段被切割,它将从集合中移除并被两个新的子段取代。你只能选择当前集合中存在的完整段进行下一次切割,不允许跨越段边界或在段内截取任意子段进行操作。

所有区间均以原数组的下标表示,不进行重新编号;切割后得到的两个区间互不重叠,且并集为原区间。请你计算,在最优策略下,总共最多能进行多少次切割。

输入描述

第一行输入一个整数 $$n(1 \le n \le 10^6)$$ 表示数组的长度。

第二行输入 $$n$$ 个整数 $$a_1, a_2, ..., a_n(1 \le a_i \le 10^9)$$ 表示数组中的元素。

输出描述

输出一个整数,表示最多的切割次数,若不存在任何合法的切割方案,输出 $$0$$。

样例1

输入

6
1 2 1 1 1 2

输出

2

样例解释

对全段 $$[1, 6]$$,可在 $$mid = 4$$ 处切割,因为 $$1 + 2 + 1 = 1 + 1 + 2$$。得到 $$[1, 3]$$ 与 $$[4, 6]$$。随后 $$[4, 6]$$ 在 $$mid = 6$$ 处再次切割,因为 $$1 + 1 = 2$$。总共进行了 $$2$$ 次切割。

题解

本题涉及到前缀和,不熟悉该算法的同学可以先做一下模板题:

区间和

题目内容拆解

给定长度为 $$n(n \le 10^6)$$ 的正整数数组,每次可将一段区间从"左右和相等"的位置切成两半,求最多切割次数。

算法实现

算法主策略:本题采用前缀和 + 递归二分查找

第一步:预处理前缀和。 构建前缀和数组 $$prefix$$,使得 $$prefix[i] = a_1 + a_2 + ... + a_i$$,$$prefix[0] = 0$$。这样任意区间 $$[l, r]$$ 的和都可以 $$O(1)$$ 计算:$$S = prefix[r] - prefix[l-1]$$。

第二步:递归寻找切割点。 对于区间 $$[l, r]$$,要把它切成左右和相等的两半:左半和 = 右半和 = $$S / 2$$。如果 $$S$$ 是奇数,显然无法切割。如果 $$S$$ 是偶数,我们要找一个位置 $$p(l \le p < r)$$,使得 $$prefix[p] - prefix[l-1] = S / 2$$,即 $$prefix[p] = prefix[l-1] + S / 2$$。

关键性质:由于 $$a_i \ge 1$$(全正数),前缀和数组严格递增。这意味着对于任意目标值,最多只有一个位置满足条件。因此每个区间要么恰好有一个切割点,要么没有,不需要枚举多种选择。

第三步:二分查找定位。 在严格递增的前缀和数组上,用二分查找在 $$O(\log n)$$ 时间内快速找到满足条件的位置 $$p$$。找到后,将区间切成 $$[l, p]$$ 和 $$[p+1, r]$$,对两半分别递归处理,答案 = $$1$$(本次切割)+ 左半结果 + 右半结果。

样例推导:数组 $$[1, 2, 1, 1, 1, 2]$$,前缀和 $$[0, 1, 3, 4, 5, 6, 8]$$。

处理 $$[1, 6]$$:总和 $$S = 8$$,目标 $$prefix[p] = 0 + 4 = 4$$。二分查找得 $$prefix[3] = 4$$,切割为 $$[1, 3]$$ 和 $$[4, 6]$$。

处理 $$[1, 3]$$:总和 $$S = 4$$,目标 $$prefix[p] = 0 + 2 = 2$$。$$prefix[1] = 1$$,$$prefix[2] = 3$$,没有等于 $$2$$ 的位置,无法切割。

处理 $$[4, 6]$$:总和 $$S = 4$$,目标 $$prefix[p] = 4 + 2 = 6$$。$$prefix[5] = 6$$,找到!切割为 $$[4, 5]$$ 和 $$[6, 6]$$,两段长度 $$\le 2$$,不能继续。

总切割次数 = $$1 + 0 + (1 + 0 + 0) = 2$$。

为什么递归不会超时? 每次切割将区间和减半($$S \to S/2$$),所以递归深度最多 $$\log_2(S) \le 50$$ 层。每层中所有区间互不重叠、总长不超过 $$n$$,因此总时间为 $$O(n \log S)$$。

时空复杂度分析

  • 时间复杂度:$$O(n \log S)$$,其中 $$S$$ 为数组总和($$S \le 10^{15}$$),递归至多 $$50$$ 层,每层二分查找总代价 $$O(n)$$。
  • 空间复杂度:$$O(n)$$,前缀和数组。递归栈深度 $$O(\log S) \approx 50$$。

Python

# 切割数组 - 前缀和 + 递归二分
from bisect import bisect_left

n = int(input())
a = list(map(int, input().split()))

prefix = [0] * (n + 1)
for i in range(1, n + 1):
    prefix[i] = prefix[i - 1] + a[i - 1]

def solve(l, r):
    if r - l + 1 <= 2:
        return 0
    S = prefix[r] - prefix[l - 1]
    if S % 2 != 0:
        return 0
    target = prefix[l - 1] + S // 2
    # a_i >= 1,前缀和严格递增,二分查找唯一切割点
    pos = bisect_left(prefix, target, l, r)
    if pos < r and prefix[pos] == target:
        return 1 + solve(l, pos) + solve(pos + 1, r)
    return 0

print(solve(1, n))

第四题:子集和

在线评测链接:https://www.neituiya.com/oj/9/2319

题目描述

给定一个整数 $$n$$ 和一个长度为 $$n$$ 的序列 $$a_1, a_2, ..., a_n$$。接下来有 $$q$$ 次询问,每次给出一个整数 $$x$$,需要计算

$$\sum_{\substack{1 \le i \le n \\ (x \ \& \ i) = i}} a_i$$

即所有满足 $$(x$$ & $$i) = i$$ 的下标 $$i$$ 所对应的 $$a_i$$ 之和。

名词解释:

符号 "&" 表示按位与运算(对两个数的二进制逐位与),对应位均为 $$1$$ 时结果位为 $$1$$,否则为 $$0$$。例如 $$5(101_2)$$ 与 $$3(011_2)$$ 满足 $$5$$ & $$3 = 1(001_2)$$。

输入描述

输入包含多组测试数据。

第一行包含整数 $$T(1 \le T \le 10^5)$$ 表示测试组数。每组数据描述如下:

第一行包含两个整数 $$n, q(1 \le n, q \le 2 \times 10^5)$$。

第二行包含 $$n$$ 个整数 $$a_1, a_2, ..., a_n(-10 \le a_i \le 10^9)$$。

第三行包含 $$q$$ 个整数 $$x_1, x_2, ..., x_q(0 \le x_i \le 2 \times 10^5)$$。

保证所有测试中 $$n + q$$ 的总和不超过 $$5 \times 10^5$$。

输出描述

对于每组测试数据,按询问顺序输出 $$q$$ 行,每行一个整数,表示对应查询的答案。

样例1

输入

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

输出

0
15
10
12
-5
9
3
15

样例解释

样例一:$$x = 0$$ 时无合法下标,和为 $$0$$;$$x = 7$$ 时 $$i \in \{1, 2, 3, 4, 5\}$$ 都满足,和为 $$15$$;$$x = 5$$ 时 $$i \in \{1, 4, 5\}$$,和为 $$10$$。

样例二:$$x = 3$$ 时 $$i \in \{1, 2, 3\}$$,和为 $$12$$;$$x = 2$$ 时仅 $$i = 2$$,和为 $$-5$$。

样例三:$$x = 6$$ 时 $$i \in \{2, 4, 6\}$$,和为 $$9$$;$$x = 4$$ 时 $$i = 4$$,和为 $$3$$;$$x = 7$$ 时 $$i \in \{1, 2, 3, 4, 5, 6\}$$,和为 $$15$$。

题解

题目内容拆解

对于每个查询 $$x$$,求所有满足 $$(x$$ & $$i) = i$$ 的下标 $$i$$ 对应的 $$a_i$$ 之和。$$n, q \le 2 \times 10^5$$,如果每次查询都暴力枚举所有 $$i$$,总复杂度 $$O(nq)$$ 会超时,需要预处理加速。

什么是 $$(x$$ & $$i) = i$$ 把 $$x$$ 和 $$i$$ 写成二进制,条件要求 $$i$$ 的每一个为 $$1$$ 的位,在 $$x$$ 中也必须是 $$1$$。换句话说,$$i$$ 的二进制是 $$x$$ 的二进制的"子集"。例如 $$x = 5 = 101_2$$,满足条件的 $$i$$ 有:$$0 = 000_2$$,$$1 = 001_2$$,$$4 = 100_2$$,$$5 = 101_2$$(每个 $$i$$ 的 $$1$$ 位都被 $$x$$ 的 $$1$$ 位"覆盖")。

算法实现

算法主策略:本题采用子集和变换(高维前缀和),是位运算题目中的经典技巧。

暴力思路与瓶颈。 对每个查询 $$x$$,遍历所有 $$i \in [1, n]$$ 检查 $$(x$$ & $$i) = i$$,单次 $$O(n)$$,总共 $$O(nq)$$ 太慢。

优化思路:预处理。 如果能提前算好"对于每个可能的 $$x$$,所有子掩码 $$i$$ 对应的 $$a_i$$ 之和",查询时直接查表就是 $$O(1)$$。定义 $$f[mask]$$ = 所有 $$mask$$ 的子掩码 $$i$$ 对应的 $$a_i$$ 之和。问题转化为:如何高效计算所有 $$f[mask]$$?

子集和变换(逐位累加)。 初始化 $$f[i] = a_i$$($$1 \le i \le n$$),其余为 $$0$$。然后从低位到高位,逐位处理:对于第 $$bit$$ 位,扫描所有 $$mask$$,若 $$mask$$ 的第 $$bit$$ 位是 $$1$$,就让 $$f[mask]$$ 累加 $$f[mask \oplus (1 \ll bit)]$$(去掉该位后的值)。

直觉理解:处理第 $$0$$ 位后,$$f[mask]$$ 包含了"第 $$0$$ 位可选可不选"的子集和;再处理第 $$1$$ 位后,$$f[mask]$$ 包含了"第 $$0, 1$$ 位可选可不选"的子集和……处理完所有位后,$$f[mask]$$ 就包含了 $$mask$$ 的所有子掩码的贡献。

时空复杂度分析

  • 时间复杂度:$$O(2^B \cdot B)$$ 预处理 + $$O(q)$$ 查询,其中 $$B \approx 18$$。预处理约 $$4.7 \times 10^6$$ 次操作。
  • 空间复杂度:$$O(2^B)$$,子集和数组大小约 $$2.6 \times 10^5$$。

Python

# 按位或运算 - 子集和变换 (高维前缀和)

T = int(input())
results = []

for _ in range(T):
    n, q = map(int, input().split())
    a = list(map(int, input().split()))
    queries = list(map(int, input().split()))

    # SOS 数组大小仅基于 n,不依赖查询值
    bits = n.bit_length() if n > 0 else 1
    sz = 1 << bits
    mask_bits = sz - 1
    f = [0] * sz
    for i in range(1, n + 1):
        f[i] = a[i - 1]

    # 子集和变换:f[mask] = 所有 mask 子掩码对应 a 值之和
    for bit in range(bits):
        for mask in range(sz):
            if mask & (1 << bit):
                f[mask] += f[mask ^ (1 << bit)]

    # 查询时将 x 映射到 [0, sz) 范围内
    for x in queries:
        results.append(str(f[x & mask_bits]))

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

2026-3-12-算法岗

第一题:尾巴大人

在线评测链接:https://www.neituiya.com/oj/9/2429

第二题:素数拆分问

在线评测链接:https://www.neituiya.com/oj/9/2430

第三题:无监督学习流程

在线评测链接:https://www.neituiya.com/oj/9/2431

第四题:子集求和问题

在线评测链接:https://www.neituiya.com/oj/9/2432

网易

内推链接:https://game.campus.163.com/home?st=NWU4YzBjMWYtMjRhMi00ZmEyLWI0NTQtYzdkMzRjYmM0NDZh

内推码:KUzqkE

2026-4-26-网易雷火

第一题:喵居

在线评测链接:https://www.neituiya.com/oj/13/2627

第二题:界面缓存

在线评测链接:https://www.neituiya.com/oj/13/2628

第三题:传闻以此得解

在线评测链接:https://www.neituiya.com/oj/13/2629

第四题:红点系统

在线评测链接:https://www.neituiya.com/oj/13/2630

2026-4-12-网易互娱

第一题:照明

在线评测链接:https://www.neituiya.com/oj/46/2517

题目描述

给定一个 $$n$$ 行 $$m$$ 列的网格地图,每个格子是以下字符之一:'#' 障碍物,'.' 空地,'/''\' 镜子,'L''R''U''D' 一盏灯,分别表示它只会向左/右/上/下照射。

灯光传播规则如下:一盏灯从自身的格子出发,只沿着当前方向,一格一格向前传播。如果灯光向前进入的下一个格子是障碍物或另一盏灯,那么灯光会在进入前停止(也就是说,障碍物和灯格子都不会被照亮,也不会被灯光穿过)。灯光进入了镜子格子,那么它会立刻改变方向:进入 '/' 时,L→D、R→U、U→R、D→L;进入 '\' 时,L→U、R→D、U→L、D→R。镜子格子不是空地,因此不计入光照强度,但灯光可以进入镜子格子并继续传播。

对于每一个空地格子,它的光照强度等于能照到它的灯的数量(每盏灯最多贡献 $$1$$)。你需要输出地图中每个格子的光照强度。

输入描述

第一行输入两个整数 $$n, m(1 \le n \le 10^4, 1 \le m \le 10^4, 1 \le n \times m \le 10^6)$$,表示地图大小。

此后 $$n$$ 行,第 $$i$$ 行输入一个长度为 $$m$$ 的字符串 $$s_i$$。保证 $$s_i$$ 仅由 '#''.''/''\''L''R''U''D' 组成,并且地图中镜子格子(即 '/''\')的总数不超过 $$5$$,灯光格子的总数不超过 $$3 \times 10^3$$。

输出描述

输出 $$n$$ 行,每行输出 $$m$$ 个整数,整数之间用单个空格分隔:若该格子是障碍物、镜子或灯,输出 $$-1$$;若该格子是空地,输出它的光照强度。

样例1

输入

3 4
R..#
.#..
..L.

输出

-1 1 1 -1
0 -1 0 0
1 1 -1 0

样例解释

第 $$1$$ 行第 $$1$$ 列是 R 灯,向右照射,照亮第 $$1$$ 行第 $$2, 3$$ 列,第 $$4$$ 列是障碍物 # 不会被照亮。第 $$3$$ 行第 $$3$$ 列是 L 灯,向左照射,照亮第 $$3$$ 行第 $$1, 2$$ 列。所有灯格子与障碍物格子输出 $$-1$$。

样例2

输入

4 4
.\/L
....
R...
....

输出

0 -1 -1 -1
0 0 1 0
-1 1 2 1
0 0 1 0

样例解释

L 灯在第 $$1$$ 行第 $$4$$ 列向左照射,进入第 $$1$$ 行第 $$3$$ 列的 / 镜子后方向变为 D(向下),依次照亮第 $$2, 3, 4$$ 行第 $$3$$ 列。R 灯在第 $$3$$ 行第 $$1$$ 列向右照射,照亮第 $$3$$ 行第 $$2, 3, 4$$ 列。第 $$3$$ 行第 $$3$$ 列被两盏灯同时照到,光照强度为 $$2$$。

题解:模拟

题目内容拆解

求网格中每个空地被多少盏灯照到,灯数 $$\le 3000$$,镜子 $$\le 5$$,网格总大小 $$n \times m \le 10^6$$。

核心观察:每盏灯的光线路径是唯一确定的——沿固定方向走,碰到镜子拐弯,碰到墙或灯停下。镜子最多 $$5$$ 个,所以一条光线最多拐 $$5$$ 次弯,每段直线最长 $$\max(n, m)$$。→ 因此采用逐灯模拟,一盏灯一盏灯地追踪光线即可。

算法实现

算法主策略:遍历所有灯,对每盏灯从它的位置出发,沿初始方向一格一格往前走,遇到不同格子做不同处理。

把 L/R/U/D 四个方向编号为 $$0, 1, 2, 3$$,用数组存好每个方向的行列增量。两种镜子的反射也用数组查表:/ 镜子把方向 $$d$$ 变成 $$3 - d$$(效果是 L↔D、R↔U),\ 镜子把方向变成 $$\{2, 3, 0, 1\}[d]$$(效果是 L↔U、R↔D)。

光线每走一步,看下一格是什么:越界或者碰到 #、碰到另一盏灯,就停下(障碍和灯都不会被照亮)。

碰到 . 空地,就把这格的光照计数 $$+1$$,然后继续往前走。

碰到镜子 /\,就查表更新方向,继续走(镜子本身不算空地,不加光照)。

有一种边界情况:如果几面镜子恰好构成环路,光线会永远转圈。为此用一个集合记录"哪面镜子被从哪个方向进入过",如果重复出现就说明在绕圈,立刻终止。

时空复杂度分析

  • 时间复杂度:$$O(L \times (n + m) \times M)$$,$$L$$ 是灯数($$\le 3000$$),$$M$$ 是镜子数($$\le 5$$),每条光线最多拐 $$M$$ 次弯,每段直线最长 $$n + m$$。
  • 空间复杂度:$$O(n \times m)$$,用于存储光照强度矩阵。

第二题:并行编译

在线评测链接:https://www.neituiya.com/oj/46/2518

题目描述

代码工程编译时,需要编译所有的 Target 完成整个工程的编译,但是 Target 之间往往存在单向的依赖关系,当前 Target 依赖的所有 Target 完成编译后才能正常编译,并且在一个线程上编译一个 Target 时无法暂停或者中断,只能等待其编译完成,之后才能在该线程上继续编译其它 Target。

为了加快编译速度,IDE 会利用 CPU 多线程并行特性,并行调度编译这些 Target。

现在有个工程有 $$N$$ 个 Target,你有一个双线程的 CPU 可用于编译,你能帮忙安排一个编译调度方案以最快的速度完成工程的编译,并计算出最短的耗时吗?

输入描述

第一行输入 $$N, K(1 \le N \le 10, 0 \le K \le 50)$$,表示有 $$N$$ 个 Target 需要编译,有 $$K$$ 个依赖关系。

接下来 $$N$$ 行,每行输入一个数字 $$W_i(1 \le W_i \le 100)$$,表示第 $$i$$ 个 Target 编译需要耗费的时间。

接下来 $$K$$ 行,每行输入 $$i, j(1 \le i, j \le N, i \ne j)$$,表示第 $$j$$ 个 Target 依赖第 $$i$$ 个 Target。

输出描述

输出一行一个数字 $$T$$,表示最快完成工程编译的时间,如果无法完成工程编译,则输出 $$-1$$。

样例1

输入

3 2
4
3
2
1 2
2 3

输出

9

样例解释

由于 $$1 \to 2 \to 3$$ 存在链式依赖,无法并行,所以完全串行编译需要 $$4 + 3 + 2 = 9$$ 单位时间。

样例2

输入

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

输出

10

样例解释

编译开始时只有第 $$4$$ 个 Target 无依赖,消耗 $$2$$ 单位时间进行编译。之后第 $$2$$、$$3$$ 个 Target 可并行编译,消耗 $$\max(3, 4) = 4$$ 单位时间。最后编译第 $$1$$ 个 Target(依赖第 $$2$$ 个),消耗 $$5$$ 单位时间,但线程 $$1$$ 在 $$t = 5$$ 即可开始(第 $$2$$ 个已完成),故第 $$1$$ 个在 $$t = 10$$ 完成。整个编译流程总消耗 $$10$$ 单位时间。

样例3

输入

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

输出

-1

样例解释

Target 之间存在 $$2 \to 3 \to 4 \to 2$$ 的循环依赖关系,编译失败,输出 $$-1$$。

题解:拓扑排序 + DFS剪枝

题目内容拆解

$$N$$ 个任务有依赖关系(先做 A 才能做 B),用 $$2$$ 个线程并行执行,求最短完成时间;有循环依赖则输出 $$-1$$。$$N \le 10$$。

核心观察:每个任务要么放线程 $$1$$,要么放线程 $$2$$,一共只有 $$2^N$$ 种分配方式。$$N \le 10$$ 时 $$2^{10} = 1024$$ 非常小,搜索所有分配方案完全可行。→ 因此采用DFS 回溯 + 剪枝

算法实现

环检测:如果任务之间有循环依赖(A 依赖 B,B 又依赖 A),谁都无法开始编译。检测方法是"拓扑排序":不断找出没有依赖的任务放入队列,处理后将它从其他任务的依赖中移除。如果最终处理的任务数不等于 $$N$$,说明存在环,输出 $$-1$$。

DFS 搜索:用一个整数 $$\text{mask}$$ 记录哪些任务已完成(第 $$i$$ 位为 $$1$$ 表示任务 $$i$$ 已完成),同时维护两个线程的空闲时刻 $$t_1, t_2$$,以及每个任务的实际完成时间 $$\text{finish}[i]$$。

每一步,从还没做且依赖已全部满足的任务中选一个任务 $$i$$,尝试分配给两个线程之一。任务 $$i$$ 的最早开始时间 = $$\max(\text{线程空闲时刻}, \text{所有依赖的完成时间的最大值})$$,因为线程要空闲、依赖也要全做完才能开始。

三重剪枝:第一,两个线程是等价的(线程 $$1$$ 和线程 $$2$$ 互换结果不变),所以每步保证 $$t_1 \le t_2$$,搜索量减半。第二,如果当前 $$t_2$$ 已经 $$\ge$$ 已知最优解,不可能更好,立刻回溯。第三,如果任务 $$i$$ 分到两个线程的开始时间恰好相同,只试一个分支就够了。

时空复杂度分析

  • 时间复杂度:最坏 $$O(N! \times 2^N)$$,但三重剪枝使实际搜索量远小于此,$$N \le 10$$ 下毫秒级完成。
  • 空间复杂度:$$O(N)$$,递归深度和辅助数组均为 $$O(N)$$。

2026-4-2-网易雷火

第一题:沙场点兵

在线测评链接:https://www.neituiya.com/oj/15/2456

第二题:背包排序

在线测评链接:https://www.neituiya.com/oj/15/2457

第三题:不朽荣光

在线测评链接:https://www.neituiya.com/oj/15/2458

第四题:贴图流式加载

在线测评链接:https://www.neituiya.com/oj/15/2459

2026-3-8

第一题:妖伞传递

在线测评链接:https://www.neituiya.com/oj/15/2308

题目描述

在一条数轴上有 $$n(1 \le n \le 1000)$$ 个人,第 $$i$$ 个人的出生点为 $$p_i$$,终点为 $$q_i(1 \le p_i < q_i \le 10^9)$$。所有人的出生点两两不同,且按从小到大的顺序输入($$p_1 < p_2 < \cdots < p_n$$);对于任意 $$i$$,都有 $$p_i < q_i$$,即所有人都只会沿数轴正方向移动。

最开始,第 1 个人持有妖伞,并从自己的出生点 $$p_1$$ 出发前往终点 $$q_1$$。

移动过程中,所有已加入的人会形成一个队伍,并按照以下规则行动:

  1. 加入队伍:当当前持伞队伍经过某个人的出生点 $$p_i$$ 时,如果此人尚未加入过队伍,则该人立即加入到队伍末尾("经过"包括恰好停在该点的情况)。
  2. 离开队伍:当某个人到达自己的终点 $$q_i$$ 时,该人会立刻离开队伍,并且之后不会再次加入队伍。

3) 妖伞交接:如果离开队伍的人恰好是当前持有妖伞的人,若此时队伍非空,则妖伞交给新的队首;若此时队伍为空,则妖伞会停留在当前位置不动,此后所有尚未加入过队伍的人中,出生点距离妖伞当前位置最近的人会先前往该位置取得妖伞,再继续前往自己的终点。若该人在前往取伞途中经过了其他尚未加入过队伍的人的出生点,这些人同样会按规则加入队伍末尾。

4) 最近人的唯一性:题目保证所有人的出生点互不相同且严格递增,因此当队伍为空时,距离妖伞最近且尚未加入的人是唯一确定的。

定义第 $$i$$ 个人的贡献值为该人持有妖伞期间,妖伞实际发生位移的总距离。请输出每个人的贡献值。

输入描述

第一行输入一个整数 $$n(1 \le n \le 1000)$$,表示人数。

接下来 $$n$$ 行,每行输入两个整数 $$p_i, q_i(1 \le p_i < q_i \le 10^9)$$,表示第 $$i$$ 个人的出生点和终点,满足 $$p_1 < p_2 < \cdots < p_n$$。

输出描述

输出一行 $$n$$ 个整数,第 $$i$$ 个整数表示第 $$i$$ 个人持有妖伞期间,妖伞实际发生位移的总距离。

样例1

输入

4
1 10
5 6
9 30
20 25

输出

9 0 20 0

样例解释

初始时,第 1 个人在位置 1 持有妖伞并出发。当队伍移动到位置 5 时,第 2 个人加入;当队伍移动到位置 9 时,第 3 个人加入;第 1 个人到达终点 10 后离开,贡献为 $$10-1=9$$。

随后第 2 个人成为新的队首,但其终点 6 已在当前位置左侧,立刻离队,贡献为 0。队伍继续前进,第 4 个人在位置 20 加入;第 4 个人到达终点 25 时离队,在此之前未成为持伞者,贡献为 0。最终第 3 个人持有妖伞从位置 10 移动到终点 30,贡献为 $$30-10=20$$。

题解:事件驱动模拟

题目问题拆解

$$n \le 1000$$,可以用 $$O(n^2)$$ 的事件驱动模拟。核心思路:维护一个队列,每次找下一个事件点(下一个人加入 vs 队首到达终点),将区间距离累加给当前持伞者(队首)。

算法实现

用双端队列维护当前队伍,next_idx 指向下一个未加入的人。由于 $$p_i$$ 严格递增且伞始终向右移动,所有未加入者的出生点均在当前位置右侧。

每轮模拟:先处理所有终点已在当前位置左侧的队首(立即离队,贡献 0)。若队列为空且还有未加入者,最近未加入者即为 next_idx($$p$$ 最小的未加入者),由于没有其他未加入者在当前位置和 p[next_idx] 之间,直接将其加入队列(伞不移动)。

队列非空时,取 next_join(下一个未加入者出生点)和 next_leave(队首终点)的较小值作为事件位置,累加距离给队首,然后加入在该位置及之前出生的新成员,或弹出到达终点的队首。

时空复杂度分析

时间复杂度:$$O(n^2)$$,最多 $$n$$ 个事件,每次处理 $$O(n)$$。

空间复杂度:$$O(n)$$,队列大小为 $$O(n)$$。

Python

# 妖伞传递 - 事件驱动模拟
from collections import deque

def simulate(n, p, q):
    contrib = [0] * n
    que = deque([0])  # 第1个人持伞出发,入队
    nxt = 1
    pos = p[0]

    while que or nxt < n:
        # 终点已在当前位置左侧的队首立即离队,贡献为0
        while que and q[que[0]] <= pos:
            que.popleft()
        if not que:
            if nxt >= n:
                break
            # 队空时:p最小的未加入者去取伞,途中无其他未加入者经过
            que.append(nxt)
            nxt += 1
            continue

        head = que[0]
        next_join = p[nxt] if nxt < n else float('inf')
        next_leave = q[head]
        # 取较近的事件点,累加持伞者贡献
        event = min(next_join, next_leave)
        contrib[head] += event - pos
        pos = event
        # 将沿途出生点 <= pos 的人依次加入队列
        while nxt < n and p[nxt] <= pos:
            que.append(nxt)
            nxt += 1

    return contrib

n = int(input())
p, q = [], []
for _ in range(n):
    pi, qi = map(int, input().split())
    p.append(pi)
    q.append(qi)
print(*simulate(n, p, q))

第二题:蛋仔滚动

在线测评链接:https://www.neituiya.com/oj/15/2309

题目描述

给定一个 $$m$$ 行 $$n$$ 列的地图,蛋仔从起点 $$(sr, sc)$$ 出发,需要到达终点 $$(er, ec)$$。

蛋仔每次可以选择向上、下、左、右四个方向之一开始滚动。一旦开始滚动,在遇到阻挡前无法主动停下。为了防止蛋仔滚出地图,地图外侧视为额外包裹了一圈障碍物。

地图中有三类特殊格子:# 为障碍物,蛋仔不能进入;/ 为斜挡板;\ 为反斜挡板。题目保证起点和终点所在格子均不是特殊格子,给出的特殊格子坐标互不重复。

挡板本身占据一个格子,蛋仔进入挡板格后会立即根据挡板类型改变方向并继续滚动。/ 型挡板:从左侧进入改为向上,从右侧进入改为向下,从上方进入改为向左,从下方进入改为向右。\ 型挡板:从左侧进入改为向下,从右侧进入改为向上,从上方进入改为向右,从下方进入改为向左。

**补充说明:**一次"滚动"定义为从某个静止位置出发,选择一个方向,直到再次停下为止,即使途中多次改变方向也只记为 1 次滚动。若蛋仔即将进入某个挡板格,但按挡板规则计算出的出口方向对应的下一格是障碍物或另一个挡板,则该挡板视为不可进入,蛋仔停在进入挡板前的那一格。只要蛋仔在某次滚动过程中经过终点格,就视为成功到达,不要求恰好停在终点。若蛋仔在挡板间无限反弹且经过了终点,仍视为可以到达。

请求出蛋仔从起点到终点的最少滚动次数,若无法到达输出 -1

输入描述

第一行输入两个整数 $$m, n(1 \le m, n \le 100)$$,表示地图的行数和列数。

第二行输入一个整数 $$k(0 \le k \le 10000)$$,表示特殊格子的数量。

接下来 $$k$$ 行,每行输入三个值 $$r, c, ch$$,表示第 $$r$$ 行第 $$c$$ 列是一个特殊格子,类型为 ch#/\)。

接下来一行输入两个整数 $$sr, sc(1 \le sr \le m, 1 \le sc \le n)$$,表示起点坐标。

最后一行输入两个整数 $$er, ec(1 \le er \le m, 1 \le ec \le n)$$,表示终点坐标。所有坐标均为 1-based。

输出描述

输出一个整数,表示蛋仔到达终点的最少滚动次数;若无法到达,输出 -1

样例1

输入

3 5
3
1 3 #
1 5 \
2 5 /
2 1
2 3

输出

1

样例解释

地图如下(S 为起点,E 为终点,外圈为虚拟障碍物):

#######
#..#.\#
#S.E./#
#.....#
#######

蛋仔从起点向右滚动 1 次,滚动过程中经过终点,答案为 1。

题解:BFS

题目问题拆解

$$m, n \le 100$$,可停留位置最多 $$10^4$$ 个,用 BFS 枚举每次滚动。共 $$O(mn)$$ 个状态,每次模拟滚动最多经过 $$O(mn)$$ 个格子,总复杂度 $$O((mn)^2)$$,可接受。

算法实现

BFS 以"停留位置"为状态,起点距离为 0。每次从队列取出位置 $$(r, c)$$,向 4 个方向各模拟一次滚动。

滚动模拟核心:沿当前方向前进,遇到边界或 # 则停下;遇到挡板则按换向表切换方向(/:上→右、下→左、左→下、右→上;\:上→左、下→右、左→上、右→下),进入挡板前需验证出口方向的下一格不是 # 或另一个挡板,否则停在挡板前一格。用 $$(r, c, \text{方向})$$ 三元组集合检测无限循环,若滚动中经过终点则标记到达。

若某次滚动到达了终点(经过或停留),以 $$d_{cur}+1$$ 更新答案;停留位置若未访问过则加入 BFS 队列。

时空复杂度分析

时间复杂度:$$O((mn)^2)$$,BFS 状态数 $$O(mn)$$,每次滚动模拟 $$O(mn)$$。

空间复杂度:$$O(mn)$$,dist 数组和 BFS 队列。

Python

# 蛋仔滚动 - BFS + 滚动模拟
from collections import deque

m, n = map(int, input().split())
k = int(input())
grid = {}
for _ in range(k):
    parts = input().split()
    r, c, ch = int(parts[0]), int(parts[1]), parts[2]
    grid[(r, c)] = ch

sr, sc = map(int, input().split())
er, ec = map(int, input().split())

dr = [-1, 1, 0, 0]  # 方向:0=上 1=下 2=左 3=右
dc = [0, 0, -1, 1]
slash_exit  = [3, 2, 1, 0]  # /挡板换向:上→右,下→左,左→下,右→上
bslash_exit = [2, 3, 0, 1]  # \挡板换向:上→左,下→右,左→上,右→下

def get_cell(r, c):
    if r < 1 or r > m or c < 1 or c > n:
        return '#'  # 边界视为障碍
    return grid.get((r, c), '.')

def roll(r, c, d):
    # 模拟一次滚动,返回(停留行, 停留列, 是否经过终点)
    reached = False
    seen = set()  # 检测无限循环
    while True:
        state = (r, c, d)
        if state in seen:
            return -1, -1, reached  # 成环,退出
        seen.add(state)
        nr, nc = r + dr[d], c + dc[d]
        cell = get_cell(nr, nc)
        if cell == '#':
            return r, c, reached  # 遇障碍,停在当前格
        if cell in ('/', '\\'):
            nd = slash_exit[d] if cell == '/' else bslash_exit[d]
            # 出口格是障碍或另一个挡板,则此挡板不可进入
            if get_cell(nr + dr[nd], nc + dc[nd]) in ('#', '/', '\\'):
                return r, c, reached
            r, c, d = nr, nc, nd  # 进入挡板并换向
        else:
            r, c = nr, nc
        if r == er and c == ec:
            reached = True  # 经过终点,标记到达

if sr == er and sc == ec:
    print(0)
else:
    INF = float('inf')
    dist = [[INF] * (n + 1) for _ in range(m + 1)]
    dist[sr][sc] = 0
    bfs_q = deque([(sr, sc)])
    ans = INF

    while bfs_q:
        r, c = bfs_q.popleft()
        d_cur = dist[r][c]
        if d_cur + 1 >= ans:
            continue  # 剪枝:已不可能更优
        for d in range(4):
            nr, nc, reached = roll(r, c, d)
            if reached:
                ans = min(ans, d_cur + 1)
            if nr == -1 or (nr == r and nc == c):
                continue
            if dist[nr][nc] > d_cur + 1:
                dist[nr][nc] = d_cur + 1
                bfs_q.append((nr, nc))

    print(ans if ans != INF else -1)

小红书

2026-3-29

第一题:超级重排

在线测评链接:https://www.neituiya.com/oj/3/2441

题目描述

红红有一个长度为$$n$$的数组$$[a_1,a_2,...,a_n]$$。

红红定义这个数组的权值为$$\sum_{i=1}^{n} a_i$$。

为了使数组的权值最大,红红提出如下超级重排流程:

将所有元素的十进制表示按原序拼接成一个字符串;

对该字符串中的所有字符进行重新排列;

按照原元素的位数切分字符串,恢复为$$n$$个新数字。

或者换句话说,首先,收集所有数字的每一个单独的数位;其次,对于每一个原始数字$$a_i$$,记录下它的位数。

你必须用收集到的所有数位来构造$$n$$个新的数字,其中第$$j$$个新数字的位数必须与原始数组中第

$$j$$个数字$$a_j$$的位数相同。你的目标是找到一种分配这些数位的方式,使得这$$n$$个新数字的总和达到最大。

输入描述

第一行输入一个整数$$n(1\le n\le 2\times 10^5)$$,表示数组长度。

第二行输$$n$$个整数$$a_1,a_2,...,a_n(1\le a_i\le 10^9)$$表示数组$$a$$,题目保证每个元素的十进制表示中不含字符$$0$$

输出描述

输出一个整数,表示经过超级重排后数组的最大权值。

样例

输入

2
36 15

输出

114

题解:贪心+排序

题目内容拆解

本题的核心在于:将所有数字的十进制数位收集起来,重新分配给$$n$$个数字,每个数字的位数与原数组对应,要求新数组的总和最大。每个数位只能用一次,且每个新数字的位数必须与原始数字一致。

本质是:将所有数位按权重分配,使得高位尽量分配大数位,低位分配小数位。

举个例子,$$a=[123,49,78]$$

最终需要构造1个三位数,两个两位数,显然唯一的一个三位数的百位一定要填9,这样能使得和最大,还剩下三个次大的数字:7、8、4分别填入十位,然后最后三个最小的数字填入个位,最终可以构成数组:$$[983,72,41]$$,这样数组权值

算法实现

  1. 遍历原数组,将所有数字拆分为单独的数位,统计所有数位。
  2. 记录每个原始数字的位数,并为每个数字的每一位计算其在最终总和中的权重($$10^{len-1},10^{len-2},...,1$$)。

3) 将所有权重收集到一个数组中。

4) 将所有数位从大到小排序,将所有权重从大到小排序。

  1. 按照权重从大到小依次分配最大的数位,累加贡献。
  2. 输出最终总和。

时间复杂度分析

遍历和拆分数位$$O(n)$$,排序$$O(n\log n)$$,分配$$O(n)$$,总复杂度$$O(n\log n)$$,可以高效通过所有测试数据。

C++

#include <bits/stdc++.h>
using namespace std;
int main() {
  int n;
  cin >> n;
  vector<int> digits;        // 所有数位
  vector<long long> weights; // 所有权重

  for (int i = 0; i < n; ++i) {
    string s;
    cin >> s;
    int len = s.size();
    for (int k = 0; k < len; ++k) {
      digits.push_back(s[k] - '0');
      // 第k位的权重:10^(len-k-1)
      long long w = 1;
      for (int t = 0; t < len - k - 1; ++t)
        w *= 10;
      weights.push_back(w);
    }
  }

  // 数位从大到小,权重从大到小排序
  sort(digits.rbegin(), digits.rend());
  sort(weights.rbegin(), weights.rend());

  long long res = 0;
  for (size_t i = 0; i < digits.size(); ++i) {
    res += 1LL * digits[i] * weights[i];
  }
  cout << res << endl;
  return 0;
}

第二题:强迫症

在线测评链接:https://www.neituiya.com/oj/3/2442

题目描述

在小红书$$App$$首页的两列$$Plog$$中,小红薯独爱第一列。她将第一列每条$$Plog$$的点赞状态从上到下用一个二进制字符串$$s=(s_1,s_2,...s_n)$$表示,

其中:

字符$$s_i=1$$表示用户已点赞第$$i$$条$$Plog$$;

字符$$s_i=0$$表示用户未点赞第$$i$$条$$Plog$$。

小红薯定义一轮点赞行为如下:

选择索引对$$1\le l\le r\le n$$;

从第$$l$$条$$Plog$$开始,到第$$r$$条$$Plog$$结束,进行一次重复点赞行为。这会使得原本未点赞的$$Plog$$变为已点赞,原本已点赞的$$Plog$$变为未点赞。

小红薯希望使得这一列$$Plog$$ 的点赞状态调整为一个回文串,即第一条和最后一条$$Plog$$的点赞状态相同,第二条和倒数第二条$$Plog$$的点赞状态相同,以此类推。

请计算她最少需要进行的点赞行为轮数。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数$$T(1\le T\le 10^4)$$代表数据组数,每组测试数据描述如下:

第一行输入一个整数$$n(1\le n\le 2\times 10^5)$$,表示$$Plog$$数量;

第二行输入一个长度为$$n$$、由字符$$0$$和$$1$$构成的字符串$$s$$,表示点赞状态。

除此之外,保证单个测试文件的$$n$$之和不超过$$2\times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,代表使字符串$$s$$成为回文串所需的最少点赞行为轮数。

样例1

输入

2
2
01
3
010

输出

1
0

题解:贪心

题目内容拆解

本题本质是:每次可以选择一个区间翻转(0变1,1变0),最少多少次操作能将字符串$$s$$变为回文串。每次操作可以覆盖一段连续的不匹配对,且一次操作可以同时修正一段连续的不匹配。

关键点:只需关注前一半的对称位置,统计有多少段连续的不匹配对,每段只需一次操作。

算法实现

  1. 对于每个测试用例,遍历字符串前一半,对每个$$i$$,判断$$s_i$$和$$s_{n-1-i}$$是否相等。
  2. 若不相等,记为不匹配对。统计所有连续的不匹配对段数,每段只需一次操作。

3) 用变量$$last$$记录上一个位置是否为不匹配,遇到新的不匹配段时计数加一。

4) 最终输出段数即为最少操作次数。

时间复杂度分析

  1. 每组数据遍历字符串一次,复杂度$$O(n)$$。
  2. 总体复杂度$$O(\sum n)$$,空间复杂度$$O(1)$$,可高效处理所有测试样例。

Python

T = int(input())  # 读入测试组数
for _ in range(T):
    n = int(input())
    s = input().strip()
    res = 0   # 记录最少操作次数
    last = 0  # 记录上一个a[i]的值(是否不匹配,1表示不匹配,0表示匹配)

    # 只需要关注前一半的对称位置
    for i in range(n // 2):
        # 判断第i位和对称位是否相等,若不等则a[i]=1,否则a[i]=0
        ai = 1 if s[i] != s[n - 1 - i] else 0

        # 如果当前a[i]=1,且前一个a[i-1]=0(或者i=0),说明遇到一个新的连续1的段
        if ai == 1 and (i == 0 or last == 0):
            res += 1  # 连续1的段数加1
        last = ai  # 更新last为当前a[i]
    # 输出本组的最少操作次数
    print(res)

第三题:每日一题plus

在线测评链接:https://www.neituiya.com/oj/3/2443

题目描述

这天,有人在小红书上发布了一道每日一题之编程题,如下:

给定一个长度为 $$n$$ 的字符串 $$s$$ ,该字符串仅由小写字母构成。

你需要删除尽可能少的字符,使得所得的字符串中,字符 ‘$$a$$’ 至 ‘$$z$$’ 的出现次数满足

‘$$a$$’ 的次数$$\le$$ ‘$$b$$’的次数 $$\le …\le$$‘$$z$$’的次数.

显然,这个问题的答案非常多,因为可能有不同的删除方案。评论区已经有很多人给出了自己的解答。

为了展现你强大的编程实力,你决定写一个程序,在解决这个问题的同时,找到的字符串是全部答案中字典序最小的。直接输出这个字符

串即可。

[名词解释]

不同长度字符串的字典序比较:从字符串的第一个字符开始逐个比较,直至发现第一个不同的位置,比较这个位置字符的字母表顺序,

字母序更小的字符串字典序也更小;如果比较到其中一个字符串的结尾时依旧全部相同,则较短的字符串字典序更小。

输入描述

第一行输入一个整数 $$n(1\le n\le 2\times 10^5)$$ ,表示字符串长度。

第二行输入一个长度为 $$n$$ ,仅由小写字母构成的字符串 $$s$$ 。除此之外,保证字符串至少包含一个 ‘$$z$$’ 。

输出描述

输出一个字符串,表示满足上述条件且字典序最小的结果字符串。

样例1

输入

4
xyxz

输出

xyz

样例解释

在这个样例中,删除第三个字符 '$$x$$ ’,得到" $$xyz$$ ",此时各字母出现次数均为 $$1$$ ,满足非严格递增,且为字典序最小。

样例2

输入

3
azz

输出

zz

题解:单调栈

题目内容拆解

本题要求删除尽量少的字符,使得最终字符串中'a'到'z'的出现次数满足非递减关系,并且在所有可行方案中输出字典序最小的结果。核心难点在于既要满足计数单调,又要保证字典序最小。

算法实现

  1. 首先统计原始字符串每个字母的出现次数,记为$$freq[c]$$。
  2. 逆序递推每个字母最终要保留的数量$$keep[c]$$。从$$z$$到$$a$$,有$$keep[c]=\min(freq[c], keep[c+1])$$,保证最终$$a$$到$$z$$的数量单调不降。

3) 维护$$need[c]$$表示当前还需要多少个$$c$$,$$remain[c]$$表示当前还剩多少个$$c$$未处理。

4) 用一个栈(字符串$$res$$)维护当前构造的答案。遍历$$s$$每个字符$$c$$:

1)若$$need[c]=0$$,直接跳过。

2)否则,尝试弹出栈顶比$$c$$大的字符$$t$$,前提是剩余$$t$$还能满足$$need[t]$$,即$$remain[t]\geq need[t]+1$$。每弹出一个$$t$$,$$need[t]$$加一。

3)将$$c$$加入栈顶,并将$$need[c]$$减一。

  1. 最终$$res$$即为所求字典序最小的可行解。

时间复杂度分析

  1. 统计$$freq$$和$$keep$$均为$$O(n)$$。
  2. 主循环每个字符最多进出栈一次,总复杂度$$O(n)$$。

3) 总体时间和空间复杂度均为$$O(n)$$,可通过所有数据范围。

Go

package main

import (
        "bufio"
        "fmt"
        "os"
)

func min(a, b int) int {
        if a < b {
                return a
        }
        return b
}

func main() {
        var n int
        fmt.Scan(&n)
        reader := bufio.NewReader(os.Stdin)
        s, _ := reader.ReadString('\n')
        s = s[:len(s)-1]
        if len(s) < n {
                tmp, _ := reader.ReadString('\n')
                s += tmp
                s = s[:n]
        }

        // freq: 原始每个字母出现次数
        freq := make([]int, 26)
        for _, c := range s {
                freq[c-'a']++
        }

        // keep: 最终每个字母要保留的个数(从右到左递推min)
        keep := make([]int, 26)
        keep[25] = freq[25]
        for i := 24; i >= 0; i-- {
                keep[i] = min(freq[i], keep[i+1])
        }

        // need: 当前还需要多少个每种字母
        need := make([]int, 26)
        copy(need, keep)
        // remain: 当前还剩多少个每种字母未处理
        remain := make([]int, 26)
        copy(remain, freq)

        res := make([]byte, 0, n)

        for i := 0; i < n; i++ {
                c := s[i]
                idx := int(c - 'a')
                remain[idx]--

                if need[idx] == 0 {
                        continue // 不需要则跳过
                }

                // 单调栈优化:弹出字典序更大的字符,保证可行性和字典序最小
                for len(res) > 0 && res[len(res)-1] > c {
                        tid := int(res[len(res)-1] - 'a')
                        // 判断弹出后还能满足配额
                        if remain[tid] >= need[tid]+1 {
                                res = res[:len(res)-1]
                                need[tid]++
                        } else {
                                break
                        }
                }

                res = append(res, c)
                need[idx]--
        }

        fmt.Println(string(res))
}

2026-3-25

第一题:数据库

在线评测链接:https://www.neituiya.com/oj/7/2395

题目描述

AK机数据库中有用户编号、用户名称和用户经验三个字段,其中:用户编号为 $$1$$ 到 $$10^9$$ 间的整数,且唯一;用户名称为长度不超过 $$10$$ 的非空字符串,且仅由小写字母构成;用户经验为 $$0$$ 到 $$10^9$$ 间的浮点数,小数位数为两位。

现在,你已经获取到了 $$n$$ 条用户数据,每一条用户数据由用户编号、用户名称、用户经验三个部分组成,但顺序是混乱的。请按照用户编号从小到大排序,并将排序后的用户数据按照用户编号、用户名称、用户经验的顺序输出。

输入描述

第一行输入一个整数 $$n(1 \le n \le 10^5)$$,表示用户数据的数量。

接下来 $$n$$ 行,每行输入三个部分表示一条用户数据(不保证某个部分一定在前,即部分间的顺序是乱序的,各部分之间用空格分隔):一个整数 $$x_i(1 \le x_i \le 10^9)$$ 表示用户编号;一个仅由小写字母构成的字符串 $$s_i(1 \le \text{length}(s_i) \le 10)$$ 表示用户名称;一个小数位数为两位的浮点数 $$c_i(0 \le c_i \le 10^9)$$ 表示用户经验。

输出描述

共 $$n$$ 行,第 $$i$$ 行依次输出用户编号第 $$i$$ 小的用户编号、用户名称和用户经验,用空格分隔。

样例1

输入

3
xhs 12 106.70
0.00 abc 11
6 xhs 666.66

输出

6 xhs 666.66
11 abc 0.00
12 xhs 106.70

题解

题目内容拆解

每行3个字段顺序随机,需要正确识别每个字段的类型(整数/字符串/浮点数),然后按编号排序输出。$$n \le 10^5$$,排序即可。

核心观察:三种字段特征明显——含小数点的是浮点数,纯字母的是字符串,剩下的是整数。

算法实现

算法主策略:本题采用字段识别 + 排序

对每行的3个空格分隔的部分,逐个判断类型:含 . 的是浮点数(经验),全部是小写字母的是字符串(名称),否则是整数(编号)。将三元组 $$(编号, 名称, 经验)$$ 存入数组,按编号升序排序后输出。

以样例为例:第一行 xhs 12 106.70xhs 是纯字母→名称,12 是纯数字→编号,106.70 含小数点→经验。三行解析后按编号 $$6, 11, 12$$ 排序输出。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,排序主导。
  • 空间复杂度:$$O(n)$$,存储所有用户数据。

C++

// 数据库 - 模拟/排序
#include <bits/stdc++.h>
using namespace std;

struct User {
    int id;
    string name;
    string exp;
};

User parse(const string& line) {
    istringstream iss(line);
    string parts[3];
    iss >> parts[0] >> parts[1] >> parts[2];
    User u;
    for (auto& p : parts) {
        if (p.find('.') != string::npos) {
            u.exp = p;               // 含小数点 → 经验
        } else if (isalpha(p[0])) {
            u.name = p;              // 首字符是字母 → 名称
        } else {
            u.id = stoi(p);          // 纯数字 → 编号
        }
    }
    return u;
}

int main() {
    int n;
    cin >> n;
    cin.ignore();
    vector<User> users(n);
    for (int i = 0; i < n; i++) {
        string line;
        getline(cin, line);
        users[i] = parse(line);
    }
    sort(users.begin(), users.end(), [](const User& a, const User& b) {
        return a.id < b.id;
    });
    for (auto& u : users) {
        cout << u.id << " " << u.name << " " << u.exp << "\n";
    }
    return 0;
}

第二题:互评操

在线评测链接:https://www.neituiya.com/oj/7/2396

题目描述

现在有 $$n$$ 条 $$Plog$$ 在首页上排成一列。用长度为 $$n$$ 的 $$01$$ 串 $$s = s_1, s_2, \ldots, s_n$$ 表示这条队列,其中:若 $$s_i = 1$$,则第 $$i$$ 条 $$Plog$$ 属于美食;若 $$s_i = 0$$,则第 $$i$$ 条 $$Plog$$ 属于旅行。

一共会进行无限轮互评操作,每一轮:所有 $$Plog$$ 的拥有者同时向右侧互评。互评只会影响每条 $$Plog$$ 右侧的第一个异属性 $$Plog$$,如果右侧没有异属性 $$Plog$$,则不会产生互评操作。每轮所有互评动作并行计算,然后一次性将所有已经有评论的 $$Plog$$ 移出,形成新队列再进入下一轮。同一条 $$Plog$$ 在一轮可能收获多条评价。

显然,无限进行下去,终究会出现不再有互评发生的情况。求整个过程中共有多少条 $$Plog$$ 收获评价。

输入描述

第一行输入一个整数 $$n(1 \le n \le 10^5)$$,表示 $$Plog$$ 数量。

第二行输入一个长度为 $$n$$ 且只由字符 01 构成的字符串 $$s$$,表示 $$Plog$$ 的属性分布。

输出描述

输出一个整数,表示所有互评结束后共有多少条 $$Plog$$ 收获评价。

样例1

输入

5
11101

输出

2

样例解释

第一轮,第三条(1)评论第四条(0),第四条(0)评论第五条(1),共 $$2$$ 条 $$Plog$$ 收获评论。剩余前三条 $$Plog$$ 拼接为 111,此时剩下的全是美食 $$Plog$$,不再发生互评现象。

题解

题目内容拆解

$$01$$ 串中每轮每个元素向右找第一个异类型元素,被找到的元素移除。求总共移除多少个元素。$$n \le 10^5$$,需要高效模拟。

核心观察:将 $$01$$ 串压缩为连续同类型的块。每轮被移除的恰好是每个非首块的第一个元素。

算法实现

算法主策略:本题采用块压缩模拟

将 $$01$$ 串压缩为块列表,例如 11101 压缩为 $$[(1,3), (0,1), (1,1)]$$,表示3个1、1个0、1个1

每轮操作的效果:每个非首块的第一个元素被评论后移出,等价于该块大小减 $$1$$。若某块大小变为 $$0$$,则移除该块,并合并左右相邻的同类型块。每轮移除的元素数 = 当前块数 $$- 1$$。

以样例为例:初始块 $$[(1,3), (0,1), (1,1)]$$,3个块。第一轮移除 $$3 - 1 = 2$$ 个元素。非首块大小各减1:$$[(1,3), (0,0), (1,0)]$$,移除空块后只剩 $$[(1,3)]$$,1个块,结束。总移除 $$= 2$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,初始块压缩 $$O(n)$$,后续每轮处理与移除元素数成正比,总移除不超过 $$n$$。
  • 空间复杂度:$$O(n)$$,存储块列表。

C++

// 互评操作 - 块压缩模拟
#include <bits/stdc++.h>
using namespace std;

int solve(const string& s) {
    // 将01串压缩为连续同类型块
    vector<pair<char, int>> blocks;
    for (char ch : s) {
        if (!blocks.empty() && blocks.back().first == ch) {
            blocks.back().second++;
        } else {
            blocks.push_back({ch, 1});
        }
    }
    int total = 0;
    while (blocks.size() >= 2) {
        total += (int)blocks.size() - 1;
        // 非首块大小减1,空块移除后合并相邻同类型
        vector<pair<char, int>> rebuilt;
        rebuilt.push_back(blocks[0]);
        for (int i = 1; i < (int)blocks.size(); i++) {
            int newSize = blocks[i].second - 1;
            if (newSize <= 0) continue;
            if (!rebuilt.empty() && rebuilt.back().first == blocks[i].first) {
                rebuilt.back().second += newSize;
            } else {
                rebuilt.push_back({blocks[i].first, newSize});
            }
        }
        blocks = rebuilt;
    }
    return total;
}

int main() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    cout << solve(s) << "\n";
    return 0;
}

第三题:字符替换

在线评测链接:https://www.neituiya.com/oj/7/2397

题目描述

为了提升笔记标签的可读性,我们计划对标签字符串进行一次双向字符置换操作,以获得更小的字典序结果。

具体地,给定一个长度为 $$n$$ 的字符串 $$s$$(下标从 $$1$$ 开始),你可以进行至多一次如下操作:选取三个整数 $$(i, j, k)$$,满足 $$1 \le i \le j \le n, 1 \le i - k, j + k \le n$$ 且 $$k > 0$$。将 $$s_i$$ 与 $$s_{i-k}$$ 交换,并将 $$s_j$$ 与 $$s_{j+k}$$ 交换。

在所有可行操作中,找出能够使字符串字典序最小的结果,并输出该字符串。

名词解释: 字典序比较:从字符串第一个字符开始逐个比较,直至出现不同位置,字符较小的一方字典序更小;若一个字符串是另一字符串的前缀,则较短字符串字典序更小。

输入描述

第一行输入一个正整数 $$n(1 \le n \le 2 \times 10^5)$$,表示字符串长度。

第二行输入一个由小写字母构成的字符串 $$s$$。

输出描述

输出一个字符串,表示经过至多一次操作后可获得的字典序最小字符串。

样例1

输入

5
baced

输出

abcde

样例解释

选择三元组 $$(2, 4, 1)$$:将 $$s_2$$ 与 $$s_1$$ 交换(ba 互换),并将 $$s_4$$ 与 $$s_5$$ 交换(ed 互换),得到 abcde

题解

题目内容拆解

给定字符串,可以进行至多一次操作:选定距离 $$k$$,同时执行两次相距 $$k$$ 的字符交换(第一次在位置 $$(i-k, i)$$,第二次在位置 $$(j, j+k)$$,且 $$i \le j$$)。求操作后字典序最小的结果。$$n \le 2 \times 10^5$$,需要 $$O(n \log n)$$ 算法。

核心观察:操作本质上是选定一个距离 $$k$$,然后执行两对不重叠的 swap。枚举 $$k$$ 时,每个 $$k$$ 只需 $$O(n/k)$$ 即可找到最优操作对,总复杂度 $$\sum O(n/k) = O(n \log n)$$。

算法实现

算法主策略:本题采用枚举距离 $$k$$ + 贪心选择

对每个距离 $$k$$(从 $$1$$ 到 $$n/2$$),分两种情况寻找最优操作:

Case 1(改进型第一 swap):找最小的位置 $$a$$ 使得 $$s[a+k] < s[a]$$,即第一个 swap 把更小的字符换到前面。然后在距离 $$k$$ 的合法范围内选最优的第二个 swap——优先修复被第一个 swap 弄乱的位置 $$a+k$$,其次找最早改进位,最后选中性 swap 避免额外损害。

Case 2(中性第一 swap + 改进型第二 swap):找 $$s[a] = s[a+k]$$ 的中性对,配合一个改进型第二 swap $$s[b+k] < s[b]$$。效果等同于只做一次 swap。

每个 $$k$$ 产生至多 $$O(1)$$ 个候选操作。每个操作最多涉及 $$4$$ 个位置,用定长数组记录变化位,候选间的比较 $$O(1)$$ 完成。

以样例为例:$$k = 1$$ 时,$$a = 0$$($$s[1] = $$a $$< s[0] = $$b,第一 swap 把 a 换到位置 $$0$$),第二 swap 选 $$b = 3$$(把 d 换到位置 $$3$$),得到 abcde

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$,枚举 $$k$$ 从 $$1$$ 到 $$n/2$$,每个 $$k$$ 扫描 $$O(n/k)$$ 个位置,$$\sum_{k=1}^{n/2} n/k = O(n \log n)$$。候选比较 $$O(1)$$ 每次。
  • 空间复杂度:$$O(n)$$,存储字符串。

C++

// 字符替换 - O(n log n) 贪心
#include <bits/stdc++.h>
using namespace std;

// 用定长数组表示一次操作的变化位(最多4个位置)
struct Op {
    int pos[4], val[4], cnt;  // cnt: 实际变化的位置数
};

// 用微型数组模拟两次swap,正确处理位置重叠,O(1)无堆分配
Op makeOp(int a, int ak, int b, int bk, const string& s) {
    // 收集所有涉及的不同位置及其初始值
    int upos[4], uval[4], ucnt = 0;
    int ps[4] = {a, ak, b, bk};
    for (int i = 0; i < 4; i++) {
        bool found = false;
        for (int j = 0; j < ucnt; j++)
            if (upos[j] == ps[i]) { found = true; break; }
        if (!found) { upos[ucnt] = ps[i]; uval[ucnt] = s[ps[i]]; ucnt++; }
    }
    // 在微型数组中按位置查找引用
    auto getV = [&](int p) -> int& {
        for (int i = 0; i < ucnt; i++) if (upos[i] == p) return uval[i];
        return uval[0];
    };
    // 第一次swap: 位置a和ak的值交换
    swap(getV(a), getV(ak));
    // 第二次swap: 位置b和bk的值交换(自动处理重叠)
    swap(getV(b), getV(bk));
    // 收集变化位,按位置升序排列
    for (int i = 0; i < ucnt; i++)
        for (int j = i+1; j < ucnt; j++)
            if (upos[i] > upos[j]) { swap(upos[i], upos[j]); swap(uval[i], uval[j]); }
    Op op; op.cnt = 0;
    for (int i = 0; i < ucnt; i++) {
        if (uval[i] != s[upos[i]]) {
            op.pos[op.cnt] = upos[i];
            op.val[op.cnt] = uval[i];
            op.cnt++;
        }
    }
    return op;
}

// 比较op是否比原串更优
bool betterThanOrig(const Op& op, const string& s) {
    for (int i = 0; i < op.cnt; i++) {
        if (op.val[i] < s[op.pos[i]]) return true;
        if (op.val[i] > s[op.pos[i]]) return false;
    }
    return false;
}

// 比较a是否比b更优
bool cmpOps(const Op& a, const Op& b, const string& s) {
    int i = 0, j = 0;
    while (i < a.cnt || j < b.cnt) {
        int pa = i < a.cnt ? a.pos[i] : INT_MAX;
        int pb = j < b.cnt ? b.pos[j] : INT_MAX;
        int p = min(pa, pb);
        int ca = (i < a.cnt && a.pos[i] == p) ? a.val[i] : s[p];
        int cb = (j < b.cnt && b.pos[j] == p) ? b.val[j] : s[p];
        if (ca < cb) return true;
        if (ca > cb) return false;
        if (i < a.cnt && a.pos[i] == p) i++;
        if (j < b.cnt && b.pos[j] == p) j++;
    }
    return false;
}

// 尝试用op更新全局最优best
void tryUpdate(const Op& op, Op& best, bool& hasBest, const string& s) {
    if (op.cnt == 0) return;
    if (!hasBest) {
        if (betterThanOrig(op, s)) { best = op; hasBest = true; }
    } else if (cmpOps(op, best, s)) {
        best = op;
    }
}

string solve(int n, string s) {
    Op best; best.cnt = 0;
    bool hasBest = false;

    for (int k = 1; 2 * k < n; k++) {
        int maxA = n - 1 - 2 * k;

        // Case 1: 改进型第一swap(s[a+k] < s[a])
        int foundA = -1;
        for (int a = 0; a <= maxA; a++) {
            if (s[a + k] < s[a]) { foundA = a; break; }
        }
        if (foundA >= 0) {
            int a = foundA;
            if (a + 2 * k <= n - 1)
                tryUpdate(makeOp(a, a+k, a+k, a+2*k, s), best, hasBest, s);
            for (int b = a+k+1; b+k <= n-1; b++) {
                if (s[b+k] < s[b]) {
                    tryUpdate(makeOp(a, a+k, b, b+k, s), best, hasBest, s);
                    break;
                }
            }
            for (int b = a+k; b+k <= n-1; b++) {
                if (s[b] == s[b+k]) {
                    tryUpdate(makeOp(a, a+k, b, b+k, s), best, hasBest, s);
                    break;
                }
            }
            int b = n - 1 - k;
            if (b >= a + k)
                tryUpdate(makeOp(a, a+k, b, b+k, s), best, hasBest, s);
        }

        // Case 2: 中性第一swap + 改进型第二swap
        int neutralA = -1;
        for (int a = 0; a <= maxA; a++) {
            if (s[a] == s[a+k]) { neutralA = a; break; }
        }
        if (neutralA >= 0) {
            for (int b = neutralA+k; b+k <= n-1; b++) {
                if (s[b+k] < s[b]) {
                    tryUpdate(makeOp(neutralA, neutralA+k, b, b+k, s), best, hasBest, s);
                    break;
                }
            }
        }
    }

    if (hasBest) {
        string res = s;
        for (int i = 0; i < best.cnt; i++) res[best.pos[i]] = best.val[i];
        return res;
    }
    return s;
}

int main() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    cout << solve(n, s) << "\n";
    return 0;
}

2026-3-18

第一题:冲突约束

在线测评链接:https://www.neituiya.com/oj/3/2351

题目描述

小红书生态团队在评论审核中,需要对得分接近的评论判定观点相近,这一判断逻辑可以帮助团队灵活的调整评论区的观点统一性/观点多样性。

现在,将模型简化如下:给定长度为n的整数数组$$[a_1,a_2,...,a_n]$$}和一个整数 $$d$$。若$$|a_i-a_j|\le d$$,则称$$a_i$$与$$a_j$$观点相近。

一次操作可以选择一对元素,并将其同时从数组中删除(数组长度减少$$2$$)。

经过若干操作后,需要保证数组中不含任何观点相近的元素,且希望保留的元素数量尽可能多。

请你计算,经过若干操作后,最终保留下来的最大元素数量。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数$$T(1\le T\le 10^4)$$代表数据组数,每组测试数据描述如下:

第一行输入两个整数$$n,d (1\le n\le 2\times 10^5;0\le d\le 10^9)$$,表示数组长度、观点相近的阈值。

第二行输入$$n$$个整数$$a_1,a_2,...,a_n (0\le a_i\le 10^9)$$,表示数组元素。

除此之外,保证单个测试文件的$$n$$之和不超过$$2\times 10^5$$。

输出描述

对于每组测试数据,新起一行输出一个整数,表示最终保留下来的最大元素数量。

样例1

输入

2
5 2
1 2 4 7 9
4 0
1 1 2 5

输出

3
2

题解:贪心

题目内容拆解

给定数组,若 $$|a_i - a_j| \le d$$ 则称两元素"观点相近"。每次操作删除任意一对元素,最终要求数组中任意两元素都不观点相近,求最大保留数量。

约束分析

  • 最终保留的元素集合必须满足:任意两元素差值 $$> d$$,即形成一个独立集
  • 每次删除2个元素,故删除总数 $$n - k$$ 必须为偶数,即保留数量 $$k$$ 与 $$n$$ 同奇偶
  • 问题转化为:在满足奇偶约束下,求最大独立集大小

算法实现

核心策略:排序 + 贪心选择

观察:排序后,若选择元素 $$a_i$$,则下一个能选的元素必须满足 $$a_j-a_i>d$$。这是一个区间不重叠问题的变体。

贪心策略:排序后从左到右扫描,若当前元素与上一个选中元素的差 $$> d$$,则选中当前元素。

贪心正确性论证

  • 设当前最后选中的元素为 $$x$$,下一个可选的位置集合为 $$\{y : y - x > d\}$$
  • 选择该集合中最小的 $$y$$,可以为后续保留更大的选择空间
  • 这是经典的"最早结束时间"贪心思想的变体

奇偶性调整

设贪心得到的最大独立集大小为 $$\text{cnt}$$:

  • 若 $$\text{cnt}$$ 与 $$n$$ 同奇偶:答案为 $$\text{cnt}$$
  • 若 $$\text{cnt}$$ 与 $$n$$ 不同奇偶:答案为 $$\text{cnt} - 1$$(删掉独立集中任意一个元素,剩余仍是合法独立集)

时间复杂度分析

排序 $$O(n \log n)$$,贪心扫描 $$O(n)$$,单组复杂度 $$O(n \log n)$$,总复杂度 $$O(\sum n \log n)$$。空间复杂度 $$O(n)$$ 用于存储数组。

C++

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int T;
    cin >> T;
    
    while (T--) {
        int n;
        long long d;
        cin >> n >> d;
        
        vector<long long> a(n);
        for (int i = 0; i < n; i++) {
            cin >> a[i];
        }
        
        sort(a.begin(), a.end());
        
        // 贪心求最大独立集大小:任意两个选中元素差 > d
        int cnt = 1;  // 至少选第一个
        long long last = a[0];
        
        for (int i = 1; i < n; i++) {
            if (a[i] - last > d) {  // 与上一个选中的元素差 > d,可以选
                cnt++;
                last = a[i];
            }
        }
        
        // 删除的元素数量 = n - cnt 必须是偶数
        // 即 cnt 和 n 必须同奇偶
        if (cnt % 2 != n % 2) {
            cnt--;
        }
        
        cout << cnt << "\n";
    }
    
    return 0;
}

第二题:品牌创意工坊

在线测评链接:https://www.neituiya.com/oj/3/2352

题目描述

在小红书“品牌创意工坊”中,营销人员可以为直播和短视频活动创建定制化丝带$$AR$$特效,结合品牌 $$ID$$ 与礼盒包装场景,实现动态丝带动画。为了支撑亿级日活的前端渲染,后端需要在活动发布时预先计算并缓存所有可能的切割方案数,确保小程序组件和 $$Web$$ 端秒级响应。

现有一根虚拟丝带长度为 $$k$$ ,可以将其分割成若干段或保持一整段不动,但是每段长度只能取整数 $$a、b$$ 或 $$c$$ 中的一个,且不允许任何长度为 $$a$$ 的段后面直接跟随长度为 $$c$$ 的段。

请对所有长度 $$k(1\le k\le n)$$ ,统计合法的切割方案数,供小红书前端组件批量加载与渲染。

由于答案可能很大,请将答案对 $$(10^9+7)$$ 取模后输出。顺序不同视为不同方案。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数 $$T(1\le T\le 10)$$ 代表数据组数,每组测试数据描述如下:

在一行上输入四个整数 $$n,a,b,c(1\le n,a,b,c\le 10^6)$$ ,代表最大丝带长度、可选分段长度 $$a,b,c$$ 。保证 $$a,b,c$$ 两两互不相等。

除此之外,保证单个测试文件的 $$n$$ 之和不超过 $$10^6$$ 。

输出描述

对于每组测试数据,新起一行,输出 $$n$$ 个整数,其中第 $$k$$ 个数表示长度为 $$k$$ 的丝带的合法分割方案数对 $$10^9+7$$ 取模的结果。

样例1

输入

2
5 1 2 3
4 1 2 3

输出

1 2 4 6 11
1 2 4 6

样例解释

在第一个样例中,当 $$n=5,a=1,b=2,c=3$$ 时:

  • $$k=1$$ 时,仅有一种分割方案$$[1]$$ ;
  • $$k=2$$ 时,有$$[1,1]$$和$$[2]$$两种方案;
  • $$k=3$$ 时,有$$[1,1,1]$$,$$[1,2]$$,$$[2,1]$$,$$[3]$$共 $$4$$ 种方案;
  • $$k=4$$ 时,按不带约束的方式共有 $$7$$ 种方案,但其中$$[1,3]$$不合法,故为 $$6$$ 种;

题解:线性DP

本题是2025-8-31小红书笔试第二题的原题:【小红书】2025-8-31-第二题-品牌创意工坊

题目内容拆解

将长度为 $$k$$ 的虚拟丝带分割成若干段,每段长度只能取 $$a$$、$$b$$ 或 $$c$$,且长度 $$a$$ 的段后面不能直接跟长度 $$c$$ 的段。顺序不同视为不同方案,对所有 $$k \in [1, n]$$ 统计合法分割方案数。

约束分析

  • 这是带禁止转移约束的有序分段计数问题
  • 约束"$$a$$ 后不能跟 $$c$$"本质上是对末段状态的限制
  • 只需记录末段是否为 $$a$$,即可在转移时判断能否接 $$c$$

算法实现

状态状态定义

设计两个 DP 数组,按末段类型分类:

  • $$f[k]$$:长度为 $$k$$,最后一段不是 $$a$$ 的合法方案数
  • $$g[k]$$:长度为 $$k$$,最后一段是 $$a$$ 的合法方案数

状态方程初始化

  • 初始化:$$f[0] = 1$$,$$g[0] = 0$$(空串视为"末段不是 $$a$$"的合法状态)
  • 最终答案:$$\text{ans}[k] = (f[k] + g[k]) \mod (10^9+7)$$

状态方程转移

对于每个长度 $$k$$,考虑最后一段的选择:

  1. 放长度 $$a$$ 的段(无前置约束,可从任意状态转移):

$$g[k] = f[k-a] + g[k-a] \quad (k \ge a)$$

  1. 放长度 $$b$$ 的段(无前置约束):

$$f[k] \mathrel{+}= f[k-b] + g[k-b] \quad (k \ge b)$$

  1. 放长度 $$c$$ 的段(前面不能是 $$a$$,只能从 $$f$$ 状态转移):

$$f[k] \mathrel{+}= f[k-c] \quad (k \ge c)$$

时间复杂度分析

每个 $$k$$ 执行 $$O(1)$$ 次转移,总复杂度 $$O(\sum n)$$,数据范围 $$\sum n \le 10^6$$,可以通过。空间复杂度 $$O(n)$$。

C++

#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

int main() {
  int T;
  cin >> T;

  while (T--) {
    int n, a, b, c;
    cin >> n >> a >> b >> c;

    // f[k]: 长度k,最后一段不是a的方案数
    // g[k]: 长度k,最后一段是a的方案数
    vector<long long> f(n + 1, 0), g(n + 1, 0);
    f[0] = 1; // 空串,最后一段"不是a"

    for (int k = 1; k <= n; k++) {
      // 最后放长度a的段
      if (k >= a) {
        g[k] = (f[k - a] + g[k - a]) % MOD;
      }
      // 最后放长度b的段
      if (k >= b) {
        f[k] = (f[k - b] + g[k - b]) % MOD;
      }
      // 最后放长度c的段(前面不能是a)
      if (k >= c) {
        f[k] = (f[k] + f[k - c]) % MOD;
      }
    }

    for (int k = 1; k <= n; k++) {
      cout << (f[k] + g[k]) % MOD;
      if (k < n)
        cout << " ";
    }
    cout << "\n";
  }

  return 0;
}

第三题:星际能量枢纽

在线测评链接:https://www.neituiya.com/oj/3/2353

题目描述

在《星际能量枢纽》游戏中,你作为宇宙能源工程师,需要修复古代文明遗留的"量子谐振核心"。星系中分布着$$n$$个能量节点(用数组$$a$$表示),每个节点包含正/负能量。当激活连续的非空节点序列$$[l,r]$$时,系统会计算从区间起始位置到区间内每一个位置的累计能量总和,并记录这些累计值中的最大者作为该区间的能量过载

峰值。

只有当某个区间的能量过载峰值正好等于临界值$$k$$时,才能触发该区间的谐振反应。请计算所有能激活核心的区间数量,为星际舰队提供跃迁能量!

输入描述

第一行输入两个整数$$n,k(1\le n\le 2\times 10^5;-10^9\le k\le 10^9)$$,分别代表节点数量和临界值。

第二行输入$$n$$个整数$$a_1,a_2,...,a_n(-10^9\le a_i\le 10^9)$$,代表每个节点的能量。

输出描述

输出一个整数,代表所有能激活核心的区间数量。

样例1

输入

5 2
0 2 -5 4 -3

输出

8

题解:前缀和+线段树

题解:分治 + 双指针

题目内容拆解

给定数组 $$a[1..n]$$,对于区间 $$[l,r]$$,定义能量过载峰值为从起点到区间内各位置累计和的最大值:

$$\text{peak}(l,r) = \max_{i=l}^{r} \sum_{j=l}^{i} a_j = \max_{i=l}^{r} (pre_i - pre_{l-1}) = \max_{i \in [l,r]} pre_i - pre_{l-1}$$

统计满足 $$\text{peak}(l,r) = k$$ 的区间数量。数据规模 $$n \le 2 \times 10^5$$,暴力 $$O(n^2)$$ 不可行,需 $$O(n \log^2 n)$$ 或更优。

算法实现

核心策略:分治处理跨中点区间

将问题分解为:处理左半区间、右半区间、跨越中点的区间

对于跨中点区间 $$[l,r]$$($$l \le mid < r$$),定义:

  • $$\text{maxL}[i] = \max_{t \in [L+i, mid]} pre_t$$(从 $$l$$ 到 $$mid$$ 的前缀和最大值)
  • $$\text{maxR}[j] = \max_{t \in [mid+1, mid+1+j]} pre_t$$(从 $$mid+1$$ 到 $$r$$ 的前缀和最大值)

区间峰值 $$= \max(\text{maxL}[i], \text{maxR}[j]) - pre_{l-1}$$

分情况统计

情况1:峰值落在左半部分($$\text{maxL}[i] \ge \text{maxR}[j]$$)

  • 峰值条件:$$\text{maxL}[i] - pre_{l-1} = k \Rightarrow pre_{l-1} = \text{maxL}[i] - k$$
  • 对每个满足条件的 $$l$$,用二分查找统计 $$\text{maxR}[j] \le \text{maxL}[i]$$ 的 $$j$$ 数量

情况2:峰值落在右半部分($$\text{maxL}[i] < \text{maxR}[j]$$)

  • 峰值条件:$$\text{maxR}[j] - pre_{l-1} = k \Rightarrow pre_{l-1} = \text{maxR}[j] - k$$
  • 注意 $$\text{maxR}$$ 单调递增,采用排序 + 双指针 + map

    • 将 $$(\text{maxL}[i], pre_{l-1})$$ 按 $$\text{maxL}$$ 排序
    • 遍历 $$j$$,用指针维护所有 $$\text{maxL}[i] < \text{maxR}[j]$$ 的项
    • 用 map 统计 $$pre_{l-1} = \text{maxR}[j] - k$$ 的数量

算法正确性

分治确保每个区间 $$[l,r]$$ 恰好被处理一次:要么完全在左/右半部分递归处理,要么跨中点在当前层处理。两种情况的分类(峰值在左/右)是互斥且完备的。

时间复杂度分析

  • 时间复杂度:$$O(n \log^2 n)$$。分治深度 $$O(\log n)$$,每层排序和 map 操作 $$O(n \log n)$$。
  • 空间复杂度:$$O(n)$$。前缀和数组、递归栈、临时数组各 $$O(n)$$。

类似题目

【美团研发岗】2025-9-6-第三题-你懂的,这也是一道数学题

【米哈游】2025-8-10-第三题-强势顶点

C++

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

int n;
ll k;
vector<ll> pre;  // 前缀和数组
ll ans = 0;

/**
 * 分治求解:统计左端点在 [L, R]、右端点也在 [L, R] 范围内,且峰值等于 k 的区间数量
 * 
 * 核心思想:将区间分为三类处理
 * 1. 完全在左半部分 [L, mid] 的区间 → 递归处理
 * 2. 完全在右半部分 [mid+1, R] 的区间 → 递归处理  
 * 3. 跨越中点的区间(左端点 ≤ mid < 右端点)→ 本层处理
 */
void solve(int L, int R) {
    if (L > R) return;
    
    // 单元素区间:峰值 = pre[L] - pre[L-1] = a[L]
    if (L == R) {
        if (pre[L] - pre[L - 1] == k) ans++;
        return;
    }
    
    int mid = (L + R) / 2;
    solve(L, mid);      // 递归处理左半部分
    solve(mid + 1, R);  // 递归处理右半部分
    
    /*
     * 处理跨越中点的区间:l ∈ [L, mid], r ∈ [mid+1, R]
     * 
     * 对于区间 [l, r],峰值定义为:
     *   峰值 = max{pre[l], pre[l+1], ..., pre[r]} - pre[l-1]
     * 
     * 由于区间跨越中点,可以拆分为:
     *   max{pre[l..r]} = max(max{pre[l..mid]}, max{pre[mid+1..r]})
     *                  = max(maxL[对应i], maxR[对应j])
     */
    
    int lenL = mid - L + 1;  // 左半部分长度
    int lenR = R - mid;      // 右半部分长度
    
    /*
     * maxL[i] 表示:从位置 (L+i) 到 mid 的前缀和最大值
     * 即 maxL[i] = max{pre[L+i], pre[L+i+1], ..., pre[mid]}
     * 
     * 当左端点 l = L+i 时,左半部分的最大前缀和就是 maxL[i]
     * 
     * 注意:maxL 是从右往左计算的后缀最大值,所以 maxL 是单调递增的(i 越小值越大)
     */
    vector<ll> maxL(lenL);
    maxL[lenL - 1] = pre[mid];  // i = lenL-1 对应 l = mid,只有 pre[mid] 一个元素
    for (int i = lenL - 2; i >= 0; i--) {
        maxL[i] = max(maxL[i + 1], pre[L + i]);
    }
    
    /*
     * maxR[j] 表示:从位置 (mid+1) 到 (mid+1+j) 的前缀和最大值
     * 即 maxR[j] = max{pre[mid+1], pre[mid+2], ..., pre[mid+1+j]}
     * 
     * 当右端点 r = mid+1+j 时,右半部分的最大前缀和就是 maxR[j]
     * 
     * 注意:maxR 是前缀最大值,所以 maxR 是单调递增的(j 越大值越大或不变)
     */
    vector<ll> maxR(lenR);
    maxR[0] = pre[mid + 1];  // j = 0 对应 r = mid+1,只有 pre[mid+1] 一个元素
    for (int j = 1; j < lenR; j++) {
        maxR[j] = max(maxR[j - 1], pre[mid + 1 + j]);
    }
    
    /*
     * ==================== 情况1 ====================
     * 当 maxL[i] >= maxR[j] 时,整个区间的最大值由左半部分决定
     * 
     * 峰值 = maxL[i] - pre[l-1] = maxL[i] - pre[L+i-1]
     * 
     * 要使峰值 = k,需要:pre[L+i-1] = maxL[i] - k
     * 
     * 对于固定的 i(即固定左端点 l),检查 pre[l-1] 是否等于 maxL[i] - k
     * 如果相等,则统计有多少个 j 满足 maxR[j] <= maxL[i]
     * 
     * 由于 maxR 单调递增,可以用二分查找快速统计
     */
    for (int i = 0; i < lenL; i++) {
        int l = L + i;
        ll need = maxL[i] - k;  // pre[l-1] 需要等于这个值
        
        if (pre[l - 1] != need) continue;  // 不满足条件,跳过
        
        // 统计满足 maxR[j] <= maxL[i] 的 j 的个数(即合法的右端点数量)
        // upper_bound 找到第一个 > maxL[i] 的位置,之前的都是 <= maxL[i] 的
        int cnt = upper_bound(maxR.begin(), maxR.end(), maxL[i]) - maxR.begin();
        ans += cnt;
    }
    
    /*
     * ==================== 情况2 ====================
     * 当 maxL[i] < maxR[j] 时,整个区间的最大值由右半部分决定
     * 
     * 峰值 = maxR[j] - pre[l-1] = maxR[j] - pre[L+i-1]
     * 
     * 要使峰值 = k,需要:pre[L+i-1] = maxR[j] - k
     * 
     * 这里需要统计满足以下两个条件的 (i, j) 对数:
     *   1. maxL[i] < maxR[j](右边最大值更大)
     *   2. pre[L+i-1] = maxR[j] - k(峰值等于 k)
     * 
     * 技巧:利用 maxR 单调递增的性质,从小到大枚举 j
     * 随着 j 增大,maxR[j] 增大,满足 maxL[i] < maxR[j] 的 i 会越来越多
     * 
     * 具体做法:
     * - 将所有 (maxL[i], pre[L+i-1]) 按 maxL[i] 排序
     * - 用指针 ptr 维护已加入的项(即满足 maxL < 当前 maxR 的项)
     * - 用 map 统计已加入项中,pre[L+i-1] 各值的出现次数
     */
    vector<pair<ll, ll>> items(lenL);  // (maxL[i], pre[L+i-1])
    for (int i = 0; i < lenL; i++) {
        items[i] = {maxL[i], pre[L + i - 1]};
    }
    sort(items.begin(), items.end());  // 按 maxL 值从小到大排序
    
    map<ll, ll> cnt_map;  // 记录已加入的 pre[l-1] 值的出现次数
    int ptr = 0;          // 指向下一个待加入的项
    
    // 枚举右端点对应的 j,maxR[j] 单调递增
    for (int j = 0; j < lenR; j++) {
        ll curMaxR = maxR[j];
        
        // 将所有 maxL[i] < curMaxR 的项加入 cnt_map
        // 由于 items 已排序且 maxR 递增,ptr 只会向右移动,保证 O(n) 复杂度
        while (ptr < lenL && items[ptr].first < curMaxR) {
            cnt_map[items[ptr].second]++;  // items[ptr].second 是对应的 pre[l-1]
            ptr++;
        }
        
        // 需要 pre[l-1] = maxR[j] - k,查询 map 中有多少个这样的值
        ll need = curMaxR - k;
        if (cnt_map.count(need)) {
            ans += cnt_map[need];
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> k;
    
    // 构建前缀和数组,pre[0] = 0, pre[i] = a[1] + a[2] + ... + a[i]
    pre.resize(n + 1);
    for (int i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        pre[i] = pre[i - 1] + x;
    }
    
    solve(1, n);
    
    cout << ans << "\n";
    
    return 0;
}

2026-3-8

第一题:完美数字

在线测评链接:https://www.neituiya.com/oj/15/2305

题目描述

用户的每一次点赞都代表着对内容的喜爱。红红定义一个正整数 $$x$$ 为完美数字 当且仅当同时满足以下两个条件:

可以将 $$x$$ 写作一个公差为 $$1$$ 且所有元素都是正整数的等差数列的乘积,例如,$$6$$可以写作 $$1\times 2\times 3$$ ;

上述等差数列的长度至少为 $$3$$ 。

现在红红接收到多次 $$Plog$$ 点赞数查询,每次给出一个正整数 $$x$$ ,请帮助红红判断该点赞数是否为完美数字。

输入描述

每个测试文件均包含多组测试数据。

第一行输入一个整数 $$T(1\le T\le 10^4)$$ 代表数据组数,

每组测试数据描述如在一行上输入一个整数 $$x(1\le x\le 10^9)$$ ,表示一次点赞数查询。

输出描述

对于每组测试数据,新起一行,如果点赞数是完美数字,输出 $$YES$$ ;否则,输出 $$NO$$ 。

样例1

输入

3
6
2
24

输出

YES
NO
YES

题解:哈希表预处理

题目内容拆解

本题要求判断一个正整数$$x$$是否能表示为长度至少为$$3$$的等差数列(公差为$$1$$,全为正整数)的连续乘积。

即$$x=s\times(s+1)\times\cdots\times(s+l-1)$$,其中$$l\geq3$$,$$s\geq1$$。

我们发现$$x> s^3$$,因此我们只需要枚举$$s\in [1,10^3]$$,对于每一个固定的$$s$$枚举长度$$l$$,直到$$x$$值超过$$10^9$$

算法实现

  1. 预处理所有不超过$$10^9$$的完美数字。枚举起始$$s$$,枚举长度$$l\geq3$$,计算乘积,若不超过$$10^9$$则加入集合。
  2. 对每个查询$$x$$,判断$$x$$是否在预处理集合中,输出"YES"或"NO"。

3) 由于$$10^9$$范围内的完美数字数量有限,预处理后查询可$$O(1)$$完成。

时间复杂度分析

预处理复杂度$$O(10^3)$$。每次查询复杂度$$O(1)$$,空间复杂度为完美数字集合大小,约$$O(10^4)$$。

Python

MAXX = 10**9

# 预处理所有完美数字
perfect = set()
for start in range(1, 1001):
    prod = start
    len_ = 1  # 当前等差数列长度
    for next_ in range(start + 1, MAXX + 1):
        prod *= next_
        len_ += 1
        if prod > MAXX:
            break
        if len_ >= 3:  # 长度至少为3
            perfect.add(prod)

T = int(input())
for _ in range(T):
    x = int(input())
    print("YES" if x in perfect else "NO")

第二题:强迫症

在线测评链接:https://www.neituiya.com/oj/15/2306

题目描述

在小红书$$App$$首页的两列$$Plog$$中,小红薯独爱第一列。她将第一列每条$$Plog$$的点赞状态从上到下用一个二进制字符串$$s=(s_1,s_2,...s_n)$$表示,

其中:

字符$$s_i=1$$表示用户已点赞第$$i$$条$$Plog$$;

字符$$s_i=0$$表示用户未点赞第$$i$$条$$Plog$$。

小红薯定义一轮点赞行为如下:

选择索引对$$1\le l\le r\le n$$;

从第$$l$$条$$Plog$$开始,到第$$r$$条$$Plog$$结束,进行一次重复点赞行为。这会使得原本未点赞的$$Plog$$变为已点赞,原本已点赞的$$Plog$$变为未点赞。

小红薯希望使得这一列$$Plog$$ 的点赞状态调整为一个回文串,即第一条和最后一条$$Plog$$的点赞状态相同,第二条和倒数第二条$$Plog$$的点赞状态相同,以此类推。

请计算她最少需要进行的点赞行为轮数。

输入描述

每个测试文件均包含多组测试数据。第一行输入一个整数$$T(1\le T\le 10^4)$$代表数据组数,每组测试数据描述如下:

第一行输入一个整数$$n(1\le n\le 2\times 10^5)$$,表示$$Plog$$数量;

第二行输入一个长度为$$n$$、由字符$$0$$和$$1$$构成的字符串$$s$$,表示点赞状态。

除此之外,保证单个测试文件的$$n$$之和不超过$$2\times 10^5$$。

输出描述

对于每一组测试数据,新起一行,输出一个整数,代表使字符串$$s$$成为回文串所需的最少点赞行为轮数。

样例1

输入

2
2
01
3
010

输出

1
0

题解:贪心

题目内容拆解

本题本质是:每次可以选择一个区间翻转(0变1,1变0),最少多少次操作能将字符串$$s$$变为回文串。每次操作可以覆盖一段连续的不匹配对,且一次操作可以同时修正一段连续的不匹配。

关键点:只需关注前一半的对称位置,统计有多少段连续的不匹配对,每段只需一次操作。

算法实现

  1. 对于每个测试用例,遍历字符串前一半,对每个$$i$$,判断$$s_i$$和$$s_{n-1-i}$$是否相等。
  2. 若不相等,记为不匹配对。统计所有连续的不匹配对段数,每段只需一次操作。

3) 用变量$$last$$记录上一个位置是否为不匹配,遇到新的不匹配段时计数加一。

4) 最终输出段数即为最少操作次数。

时间复杂度分析

  1. 每组数据遍历字符串一次,复杂度$$O(n)$$。
  2. 总体复杂度$$O(\sum n)$$,空间复杂度$$O(1)$$,可高效处理所有测试样例。

Python

T = int(input())  # 读入测试组数
for _ in range(T):
    n = int(input())
    s = input().strip()
    res = 0   # 记录最少操作次数
    last = 0  # 记录上一个a[i]的值(是否不匹配,1表示不匹配,0表示匹配)

    # 只需要关注前一半的对称位置
    for i in range(n // 2):
        # 判断第i位和对称位是否相等,若不等则a[i]=1,否则a[i]=0
        ai = 1 if s[i] != s[n - 1 - i] else 0

        # 如果当前a[i]=1,且前一个a[i-1]=0(或者i=0),说明遇到一个新的连续1的段
        if ai == 1 and (i == 0 or last == 0):
            res += 1  # 连续1的段数加1
        last = ai  # 更新last为当前a[i]
    # 输出本组的最少操作次数
    print(res)

第三题:每日一题plus

在线测评链接:https://www.neituiya.com/oj/15/2307

题目描述

这天,有人在小红书上发布了一道每日一题之编程题,如下:

给定一个长度为 $$n$$ 的字符串 $$s$$ ,该字符串仅由小写字母构成。

你需要删除尽可能少的字符,使得所得的字符串中,字符 ‘$$a$$’ 至 ‘$$z$$’ 的出现次数满足

‘$$a$$’ 的次数$$\le$$ ‘$$b$$’的次数 $$\le …\le$$‘$$z$$’的次数.

显然,这个问题的答案非常多,因为可能有不同的删除方案。评论区已经有很多人给出了自己的解答。

为了展现你强大的编程实力,你决定写一个程序,在解决这个问题的同时,找到的字符串是全部答案中字典序最小的。直接输出这个字符

串即可。

[名词解释]

不同长度字符串的字典序比较:从字符串的第一个字符开始逐个比较,直至发现第一个不同的位置,比较这个位置字符的字母表顺序,

字母序更小的字符串字典序也更小;如果比较到其中一个字符串的结尾时依旧全部相同,则较短的字符串字典序更小。

输入描述

第一行输入一个整数 $$n(1\le n\le 2\times 10^5)$$ ,表示字符串长度。

第二行输入一个长度为 $$n$$ ,仅由小写字母构成的字符串 $$s$$ 。除此之外,保证字符串至少包含一个 ‘$$z$$’ 。

输出描述

输出一个字符串,表示满足上述条件且字典序最小的结果字符串。

样例1

输入

4
xyxz

输出

xyz

样例解释

在这个样例中,删除第三个字符 '$$x$$ ’,得到" $$xyz$$ ",此时各字母出现次数均为 $$1$$ ,满足非严格递增,且为字典序最小。

样例2

输入

3
azz

输出

zz

题解:单调栈

题目内容拆解

本题要求删除尽量少的字符,使得最终字符串中'a'到'z'的出现次数满足非递减关系,并且在所有可行方案中输出字典序最小的结果。核心难点在于既要满足计数单调,又要保证字典序最小。

算法实现

  1. 首先统计原始字符串每个字母的出现次数,记为$$freq[c]$$。
  2. 逆序递推每个字母最终要保留的数量$$keep[c]$$。从$$z$$到$$a$$,有$$keep[c]=\min(freq[c], keep[c+1])$$,保证最终$$a$$到$$z$$的数量单调不降。

3) 维护$$need[c]$$表示当前还需要多少个$$c$$,$$remain[c]$$表示当前还剩多少个$$c$$未处理。

4) 用一个栈(字符串$$res$$)维护当前构造的答案。遍历$$s$$每个字符$$c$$:

1)若$$need[c]=0$$,直接跳过。

2)否则,尝试弹出栈顶比$$c$$大的字符$$t$$,前提是剩余$$t$$还能满足$$need[t]$$,即$$remain[t]\geq need[t]+1$$。每弹出一个$$t$$,$$need[t]$$加一。

3)将$$c$$加入栈顶,并将$$need[c]$$减一。

  1. 最终$$res$$即为所求字典序最小的可行解。

时间复杂度分析

  1. 统计$$freq$$和$$keep$$均为$$O(n)$$。
  2. 主循环每个字符最多进出栈一次,总复杂度$$O(n)$$。

3) 总体时间和空间复杂度均为$$O(n)$$,可通过所有数据范围。

Python

n = int(input())
s = input()

# freq: 原始每个字母出现次数
freq = [0] * 26
for c in s:
    freq[ord(c) - ord('a')] += 1

# keep: 最终每个字母要保留的个数(从右到左递推min)
keep = [0] * 26
keep[25] = freq[25]
for i in range(24, -1, -1):
    keep[i] = min(freq[i], keep[i + 1])

# need: 当前还需要多少个每种字母
need = keep[:]
# remain: 当前还剩多少个每种字母未处理
remain = freq[:]

res = []

for c in s:
    idx = ord(c) - ord('a')
    remain[idx] -= 1

    if need[idx] == 0:
        continue  # 不需要则跳过

    # 单调栈优化:弹出字典序更大的字符,保证可行性和字典序最小
    while res and res[-1] > c:
        tid = ord(res[-1]) - ord('a')
        # 判断弹出后还能满足配额
        if remain[tid] >= need[tid] + 1:
            res.pop()
            need[tid] += 1
        else:
            break

    res.append(c)
    need[idx] -= 1

print(''.join(res))

虾皮

2026-3-28

第一题:艾尔罗大迷宫

在线评测链接:https://www.neituiya.com/oj/7/2417

题目描述

设计一个迷宫游戏系列艾尔罗,在设计初期为了方便,使用 $$n \times n$$ 矩阵表示。

$$0$$ 代表可到达区域,$$1$$ 表示不可到达区域。

例如有:$$[[0,1,0,0],[0,0,0,0],[0,1,0,1],[0,0,1,0]]$$

在这个例子中,因为 $$map[3][2]=1$$ 和 $$map[2][3]=1$$,所以相对于起点 $$map[0][0]$$ 来说,$$map[3][3]$$ 的位置是不可达的(只允许左右上下移动)。

为了方便评估设计的艾尔罗迷宫的难易程度,需要有一个方便的算法统计每个迷宫不可到达的网格有多少个。

比如上面的不可达区域为 $$4$$ 个原生不达的区域加上 $$1$$ 个衍生的 $$map[3][3]$$,总数为 $$5$$。

约束:起点统一定义为 $$[0,0]$$。给定的迷宫二维数组矩阵形式是 $$n \times n$$,且 $$[0,0]$$ 也总是可达(值为 $$0$$),原生不可达的用值 $$1$$ 表示。

输入描述

输入一行,包含一个 $$n \times n(1 \le n \le 500)$$ 的二维矩阵,以 JSON 数组格式给出,如 [[0,1],[1,0]]

输出描述

输出一个整数,表示从 $$[0,0]$$ 出发不可到达的网格数量。

样例1

输入

[[0,1,1,0],[1,0,0,0],[0,1,0,1],[0,1,1,0]]

输出

15

样例解释

$$[0,0]$$ 四周被 $$1$$ 包围($$map[0][1]=1$$,$$map[1][0]=1$$),只有 $$[0,0]$$ 本身可达。$$4 \times 4 = 16$$ 个格子中,不可达数量为 $$15$$。

样例2

输入

[[0,0,0,0],[1,0,0,1],[0,0,1,0],[0,0,0,1]]

输出

5

题解

本题涉及到BFS算法,不熟悉该算法的同学可以先做一下模板题:

离开中山路

马的遍历

题目内容拆解

给定一个 $$n \times n$$ 的 $$0/1$$ 矩阵,从左上角 $$(0,0)$$ 出发,只能上下左右移动到值为 $$0$$ 的格子,求无法到达的格子总数。

算法实现

算法主策略:本题采用**BFS(广度优先搜索)**从起点 $$(0,0)$$ 出发,遍历所有可达的格子,最后用总格子数减去可达数即为答案。

具体步骤

  1. 解析 JSON 格式的输入字符串,提取出 $$n \times n$$ 的二维矩阵。
  2. 从 $$(0,0)$$ 开始 BFS,将起点加入队列并标记已访问。每次从队列取出一个格子,尝试向上下左右四个方向扩展:若目标格子在矩阵范围内、值为 $$0$$、且未被访问过,则标记并加入队列。

3) BFS 结束后统计已访问的格子数 $$reachable$$,答案为 $$n \times n - reachable$$。

样例1推导:矩阵为 $$[[0,1,1,0],[1,0,0,0],[0,1,0,1],[0,1,1,0]]$$,起点 $$(0,0)=0$$,但 $$(0,1)=1$$ 和 $$(1,0)=1$$,四周全被堵住,BFS 只能访问 $$(0,0)$$ 自身。可达数 $$= 1$$,答案 $$= 16 - 1 = 15$$。

时空复杂度分析

  • 时间复杂度:$$O(n^2)$$,BFS 最多访问 $$n^2$$ 个格子,每个格子入队出队各一次。
  • 空间复杂度:$$O(n^2)$$,用于存储矩阵和访问标记数组。

Python

# 艾尔罗大迷宫 - BFS
import json
from collections import deque

def solve(grid):
    n = len(grid)
    visited = [[False] * n for _ in range(n)]
    visited[0][0] = True
    q = deque([(0, 0)])
    reachable = 0
    while q:
        x, y = q.popleft()
        reachable += 1
        for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nx, ny = x + dx, y + dy
            if 0 <= nx < n and 0 <= ny < n and not visited[nx][ny] and grid[nx][ny] == 0:
                visited[nx][ny] = True
                q.append((nx, ny))
    return n * n - reachable

grid = json.loads(input())
print(solve(grid))

第二题:2的N次方的十进制结果

在线评测链接:https://www.neituiya.com/oj/7/2418

题目描述

对于一个整数 $$N$$,计算 $$2$$ 的 $$N$$ 次方并在屏幕显示十进制结果。

输入描述

输入一个整数 $$N(1 \le N \le 1024)$$。

输出描述

输出 $$2^N$$ 的十进制结果,用双引号包裹。

样例1

输入

1024

输出

"179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407530021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216"

样例2

输入

1025

输出

"359538626972463181545861038157804946723595395788461314546860162315465351611001926265416954644815072042240227759742786715317579537628833244985694861278948248755535786849730970552604439202492188238906165904170011537676301364684925762947826221081654474326701021369172596479894491876959432609670712659248448274432"

题解

题目内容拆解

给定整数 $$N$$,计算 $$2^N$$ 的精确十进制值,输出时带双引号。

算法实现

算法主策略:本题的核心是大数运算。$$N$$ 最大为 $$1024$$,$$2^{1024}$$ 有约 $$308$$ 位十进制数字,远超标准整数类型的表示范围。

各语言方案:Python 原生支持大整数,直接 2 ** N 即可。Java 使用 BigInteger 类。C++ 原生不支持大整数,采用数组模拟逐位乘2;Go 使用 math/big.Int

输出格式:结果需要用双引号包裹,即输出 "数字字符串"

时空复杂度分析

  • 时间复杂度:$$O(N \times M)$$,其中 $$M$$ 为结果的位数(约 $$N \times \log_{10}2 \approx 0.3N$$),每次乘2需要遍历所有位。$$N \le 1024$$ 时总运算量约 $$3 \times 10^5$$,完全可行。
  • 空间复杂度:$$O(M)$$,存储大数结果。

Python

# 2的N次方的十进制结果 - 大数运算
import sys
sys.set_int_max_str_digits(0)

def power2(n):
    # Python原生大整数直接计算
    return str(2 ** n)

n = int(input())
print('"' + power2(n) + '"')

Go

第三题:寻找数组中只出现过一次的数字

在线评测链接:https://www.neituiya.com/oj/7/2419

题目描述

给定一个数组,数组中除了两个数 $$a$$ 和 $$b$$ 只出现过一次,其余数字都恰好出现两次,找出这两个数字 $$a$$ 和 $$b$$($$a, b$$ 从小到大排序)。

输入描述

输入一行,包含一个整数数组,以 JSON 数组格式给出,如 [1,2,2,3]。数组长度 $$n(2 \le n \le 10^5)$$,数组元素 $$a_i(-10^9 \le a_i \le 10^9)$$。

输出描述

输出一行,以 JSON 数组格式输出两个只出现一次的数字,从小到大排序,如 [1,3]

样例1

输入

[1,2,2,3]

输出

[1,3]

样例解释

数组 $$[1, 2, 2, 3]$$ 中,$$2$$ 出现了两次,$$1$$ 和 $$3$$ 各出现一次。按从小到大输出 $$[1, 3]$$。

题解

题目内容拆解

在一个数组中,恰好有两个数只出现一次,其余数字都出现两次,要找出这两个数。

算法实现

算法主策略:本题采用异或位运算。利用异或的性质:$$a \oplus a = 0$$,$$a \oplus 0 = a$$,相同数字异或后抵消为 $$0$$。

具体步骤

  1. 将数组所有元素异或,得到 $$xorAll = a \oplus b$$(因为出现两次的数全部抵消)。
  2. 找到 $$xorAll$$ 中任意一个为 $$1$$ 的位(取最低位:$$bit = xorAll \mathbin{\&} (-xorAll)$$),这一位上 $$a$$ 和 $$b$$ 一定不同。

3) 按这一位是否为 $$1$$,把所有数字分成两组。每组内出现两次的数字自行抵消,剩下的就是 $$a$$ 或 $$b$$。

4) 将两个结果排序后输出。

样例推导:数组 $$[1, 2, 2, 3]$$,全部异或:$$1 \oplus 2 \oplus 2 \oplus 3 = 1 \oplus 3 = 2$$(二进制 $$10$$)。最低位 $$bit = 2$$。按 bit 分组:bit 为 $$1$$ 的有 $$2, 2, 3$$,异或得 $$3$$;bit 为 $$0$$ 的有 $$1$$,异或得 $$1$$。结果 $$[1, 3]$$。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,两次遍历数组,每次线性时间。
  • 空间复杂度:$$O(1)$$,只使用常数个变量(不计输入存储)。

Python

# 寻找数组中只出现过一次的数字 - 异或位运算
import json

def solve(nums):
    xor_all = 0
    for x in nums:
        xor_all ^= x
    # 取最低位的1
    bit = xor_all & (-xor_all)
    a, b = 0, 0
    for x in nums:
        if x & bit:
            a ^= x
        else:
            b ^= x
    return sorted([a, b])

nums = json.loads(input())
result = solve(nums)
print(json.dumps(result, separators=(',', ':')))

滴滴

2026-3-22

第一题:取消航班

在线评测链接:https://www.neituiya.com/oj/69/2378

题目描述

有 $$A, B, C$$ 三个城市,$$A$$ 到 $$B$$ 有 $$n$$ 个航班,起飞时间为 $$a_i,B$$ 到 $$C$$ 有 $$m$$ 个航班,起飞时间为 $$b_i$$。

每个从 $$A$$ 到 $$B$$ 的航班飞行时间为 $$t_a$$。

每个从 $$B$$ 到 $$C$$ 的航班飞行时间为 $$t_b$$,如果AK机乘坐起飞时间为 $$x$$ 的航班,那他将在 $$x+t_a$$ 时间到达 $$B$$,城市 $$B$$ 同理。

AK机希望从 $$A$$ 飞到 $$C$$,但你想搞一个恶作剧,你可以取消 $$k$$ 个航班延迟他的到达时间。最开始,AK机在城市 $$A$$。

若可以使其无法到达请输出 $$-1$$。

输入描述

第一行五个整数 $$n, m, t_a, t_b, k(1 \le n, m \le 10000, 1 \le t_a, t_b \le 10^9, 0 \le k \le n+m)$$。

第二行 $$n$$ 个整数表示 $$a_i(1 \le a_1 < a_2 < \ldots < a_n \le 10^9)$$。

第三行 $$m$$ 个整数表示 $$b_i(1 \le b_1 < b_2 < \ldots < b_m \le 10^9)$$。

输出描述

一行一个整数表示答案,即AK机最晚到达 $$C$$ 的时间。

样例1

输入

3 3 1 2 2
1 5 7
2 6 8

输出

10

样例2

输入

3 3 1 2 3
1 5 7
2 6 8

输出

-1

题解:贪心 + 二分查找

本题涉及到二分查找,不熟悉该算法的同学可以先做一下模板题:

二分查找-模版1二分查找-模版2

题目问题拆解

给定两段航班($$A \to B$$和 $$B \to C$$),可以取消任意 $$k$$ 个航班,问能让旅客最晚到达 $$C$$ 的时间是多少。

如果能让旅客无法到达则输出 $$-1$$。

核心观察:取消航班的最优策略是贪心地取消靠前的航班。取消越早的航班,旅客被迫使用越晚的航班,到达时间越迟。

算法实现

算法主策略:本题采用枚举 + 贪心 + 二分查找

枚举从 $$A \to B$$ 的航班中取消前 $$x$$ 个($$x$$ 从 $$0$$ 到 $$\min(k, n)$$),剩余 $$k - x$$ 次取消机会用于 $$B \to C$$ 的航班。

对于每个 $$x$$,旅客被迫乘坐 $$a[x]$$(第 $$x+1$$ 个航班),到达 $$B$$ 的时间为 $$a[x] + t_a$$。

然后用二分查找找到第一个起飞时间 $$\ge a[x] + t_a$$ 的 $$B \to C$$ 航班,位置记为 $$j$$。

再取消从 $$j$$ 开始的前 $$k - x$$ 个航班,旅客被迫乘坐 $$b[j+k-x]$$,到达 $$C$$ 的时间为 $$b[j+k-x]+t_b$$。

如果 $$x=n$$(所有 $$A \to B$$ 航班都被取消)或 $$j+k-x\ge m$$(所有可用的 $$B \to C$$ 航班都被取消),则旅客无法到达,答案为 $$-1$$。

最终答案取所有枚举中的最大值。

以样例 $$1$$ 为例:$$n = 3, m = 3, t_a = 1, t_b = 2, k = 2$$,航班 $$a = [1, 5, 7],b = [2, 6, 8]$$。

当 $$x = 0$$ 时,旅客乘 $$a[0]=1$$,到达 $$B$$ 时间 $$2$$,取消 $$b[0] = 2, b[1] = 6$$,旅客乘 $$b[2] = 8$$,到达 $$C$$ 时间 $$10$$。当 $$x = 1$$ 或 $$x = 2$$ 时,结果也是 $$10$$。最终答案 $$10$$。

时空复杂度分析

时间复杂度:$$O(n \log m)$$,枚举 $$x$$ 共 $$O(\min(k, n))$$ 次,每次二分查找 $$O(\log m)$$。由于 $$k \le n + m$$,最坏情况 $$O(n \log m)$$。

空间复杂度:$$O(n + m)$$,存储航班时间数组。

C++

// 取消航班 - 贪心 + 二分查找
#include <bits/stdc++.h>
using namespace std;

long long solve(int n, int m, long long ta, long long tb, int k, vector<long long>& a, vector<long long>& b) {
    long long best = 0;
    // 枚举取消前x个A→B航班,剩余k-x个取消B→C航班
    for (int x = 0; x <= min(k, n); x++) {
        if (x == n) return -1;  // 所有A→B航班被取消
        long long arrival = a[x] + ta;
        // 找第一个起飞时间 >= arrival 的B→C航班
        int j = (int)(lower_bound(b.begin(), b.end(), arrival) - b.begin());
        int remaining = k - x;
        int idx = j + remaining;
        if (idx >= m) return -1;  // 所有可用B→C航班被取消
        best = max(best, b[idx] + tb);
    }
    return best;
}

int main() {
    int n, m, k;
    long long ta, tb;
    cin >> n >> m >> ta >> tb >> k;
    vector<long long> a(n), b(m);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < m; i++) cin >> b[i];
    cout << solve(n, m, ta, tb, k, a, b) << endl;
    return 0;
}

第二题:线段覆盖

在线评测链接:https://www.neituiya.com/oj/69/2379

题目描述

有一条长度为 $$m$$ 的线段,被划分为从左到右编号为 $$1$$ 到 $$m$$ 的格子。

现在给出 $$n$$ 个线段。第 $$i$$ 个线段由四个整数 $$l_i, r_i, p_i, q_i$$ 描述,表示该线段会覆盖从 $$l_i$$ 到 $$r_i$$ 的所有格子(两端都包含);

以概率 $$\frac{p_i}{q_i}$$ 出现,并且所有线段是否出现相互独立

你的任务是计算:每一个格子都被恰好一个线段覆盖的概率。

我们在模数 $$998244353$$ 下输出答案。设这个概率可以表示为最简分数 $$\frac{y}$$,

你需要输出 $$x \cdot y^{-1} \bmod 998244353$$,其中 $$y^{-1}$$ 表示在模 $$998244353$$ 意义下的乘法逆元,即满足 $$y \cdot y^{-1} \equiv 1 \pmod{998244353}$$ 的那个整数。

输入描述

第一行包含两个整数 $$n, m(1 \le n, m \le 2 \times 10^4)$$,分别表示线段个数和格子个数。

第二行包含 $$n$$ 个整数,代表 $$l_i$$,即第 $$i$$ 条线段的左边界。

第三行包含 $$n$$ 个整数,代表 $$r_i$$,即第 $$i$$ 条线段的右边界。

第四行包含 $$n$$ 个整数,代表 $$p_i$$。

第五行包含 $$n$$ 个整数,代表 $$q_i(1 \le p_i \le q_i < 998244353)$$,第 $$i$$ 条线段存在的概率为 $$\frac{p_i}{q_i}$$。

保证所有线段是否出现相互独立。

输出描述

输出一行一个整数,表示"每一个格子都被恰好一个线段覆盖"的概率在模 $$998244353$$ 下的值。

样例1

输入

3 3
1 3 1
2 3 3
1 1 2
3 2 3

输出

610038216

样例解释

可以计算得到"每个格子恰好被一个线段覆盖"的总概率为 $$\frac{5}{18}$$。

样例2

输入

2 3
1 2
2 3
1 1
2 2

输出

0

样例3

输入

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

输出

94391813

题解:区间DP + 乘法逆元

本题涉及到逆元,不熟悉该算法的同学可以先做一下模板题:

逆元

题目问题拆解

给定 $$n$$ 条线段和 $$m$$ 个格子,每条线段以给定概率独立出现,

求所有格子恰好被一条线段覆盖的概率(模 $$998244353$$)。$$n, m \le 2 \times 10^4$$,需要高效算法。

核心观察:线段是区间结构,可以用区间DP

将概率拆分为"所有段都不选"的基础概率,乘以"用选中段恰好铺满 $$[1, m]$$"的权重和。

算法实现

采用概率拆分 + 区间DP + 费马小定理求逆元

概率拆分:对于一个合法的精确覆盖方案 $$T$$,其概率为 $$\prod_{i \in T} \frac{p_i}{q_i} \times \prod_{i \notin T} \frac{q_i-p_i}{q_i}$$。

将其拆分为两部分:基础项 $$B=\prod_{i} \frac{q_i-p_i}{q_i}$$(所有段都不选的概率);

权重 $$w_i=\frac{p_i}{q_i-p_i}$$(选段 $$i$$ 相对于不选的概率比值)。最终答案 $$=B \times \sum_{T \text{ 是合法覆盖}} \prod_{i \in T} w_i$$。

处理必选段:若某段概率为 $$1$$(即 $$p_i = q_i$$),该段必须出现,$$w_i$$无穷大。

需单独处理:将必选段排序检查是否重叠(重叠则答案为 $$0$$),然后在必选段之间的空隙上分别做 DP。

状态方程定义

对于每个空隙 $$[gl, gr]$$,设 $$g[j]$$ 表示用可选段恰好铺满空隙内前 $$j$$ 个格子的权重之和。

其中 $$j = 0$$ 表示尚未铺任何格子,$$j=gr-gl+1$$ 表示整个空隙被铺满。

状态方程初始化

$$g[0] = 1$$,表示"不需要铺任何格子"的权重为 $$1$$。其余 $$g[j] = 0$$。

状态方程转移

对每个位置 $$j(1 \le j \le gr-gl+1)$$,枚举所有右端点在 $$gl + j - 1$$ 的可选段 $$i$$:$$g[j]=g[j]+g[l_i-gl]\times w_i$$。

含义是:段 $$i$$ 覆盖 $$[l_i, r_i]$$,其左侧 $$[gl, l_i - 1]$$ 已由前面的段铺满(对应 $$g[l_i-gl]$$),段 $$i$$ 自身的权重为 $$w_i$$。

这里隐含了"每个格子恰好被一段覆盖"的约束,因为相邻段必须无缝衔接。最终答案为 $$B \times \prod_{\text{gap}} g[\text{gap length}]$$。

以样例 $$1$$ 为例:$$3$$ 条线段分别为 $$[1, 2]$$(概率 $$\frac{1}{3}$$)、$$[3, 3]$$(概率 $$\frac{1}{2}$$)、$$[1, 3]$$(概率 $$\frac{2}{3}$$)。$$w_1 = \frac{1}{2}, w_2 = 1, w_3 = 2$$。

基础项 $$B=\frac{2}{3} \times \frac{1}{2} \times \frac{1}{3}=\frac{1}{9}$$。DP:$$g[0] = 1, g[2] = g[0] \times w_1 = \frac{1}{2}, g[3] = g[2] \times w_2 + g[0] \times w_3 = \frac{5}{2}$$。

答案 $$=\frac{1}{9} \times \frac{5}{2}=\frac{5}{18}$$,对应模意义下 $$610038216$$。

时空复杂度分析

时间复杂度:$$O(n + m)$$,遍历所有段按右端点分组 $$O(n)$$,DP 扫描 $$O(m)$$,每段只在一个位置贡献一次转移。

空间复杂度:$$O(n + m)$$,存储线段信息和 DP 数组。

C++

// 线段覆盖 - 区间DP + 模逆元
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 998244353;

long long power(long long a, long long b, long long mod) {
    long long res = 1;
    a %= mod;
    while (b > 0) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

long long inv(long long a) {
    return power(a % MOD, MOD - 2, MOD);
}

void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> L(n), R(n), P(n), Q(n);
    for (int i = 0; i < n; i++) cin >> L[i];
    for (int i = 0; i < n; i++) cin >> R[i];
    for (int i = 0; i < n; i++) cin >> P[i];
    for (int i = 0; i < n; i++) cin >> Q[i];

    // 分离必选段(p==q)和可选段
    vector<int> forced, optional;
    for (int i = 0; i < n; i++) {
        if (P[i] == Q[i]) forced.push_back(i);
        else optional.push_back(i);
    }

    // 必选段排序并检查无重叠
    vector<pair<int, int>> fsegs;
    for (int i : forced) fsegs.push_back({L[i], R[i]});
    sort(fsegs.begin(), fsegs.end());
    for (int i = 1; i < (int)fsegs.size(); i++) {
        if (fsegs[i].first <= fsegs[i - 1].second) {
            cout << 0 << endl;
            return;
        }
    }

    // 找出必选段之间的空隙
    vector<pair<int, int>> gaps;
    int prev = 0;
    for (auto [ls, rs] : fsegs) {
        if (ls > prev + 1) gaps.push_back({prev + 1, ls - 1});
        prev = rs;
    }
    if (prev < m) gaps.push_back({prev + 1, m});

    // 所有可选段不选的概率之积: ∏((q-p)/q)
    long long base = 1;
    for (int i : optional) {
        base = base % MOD * ((Q[i] - P[i]) % MOD) % MOD * inv(Q[i]) % MOD;
    }

    long long ans = base;

    // 对每个空隙做区间DP
    for (auto [gl, gr] : gaps) {
        int len = gr - gl + 1;
        vector<long long> g(len + 1, 0);
        g[0] = 1;

        // 按右端点分组可选段
        vector<vector<pair<int, long long>>> segs_by_r(len + 1);
        for (int i : optional) {
            if (L[i] >= gl && R[i] <= gr) {
                int r_off = R[i] - gl + 1;
                int l_off = L[i] - gl;
                // w = p / (q - p),选此段的权重
                long long wi = (long long)P[i] % MOD * inv(Q[i] - P[i]) % MOD;
                segs_by_r[r_off].push_back({l_off, wi});
            }
        }

        for (int j = 1; j <= len; j++) {
            for (auto [l_off, wi] : segs_by_r[j]) {
                g[j] = (g[j] + g[l_off] * wi) % MOD;
            }
        }

        ans = ans * g[len] % MOD;
    }

    cout << ans << endl;
}

int main() {
    solve();
    return 0;
}

Java

2026-3-15

第一题:划分

在线评测链接:https://www.neituiya.com/oj/69/2347

题目描述

给定一个长度为 -$$n$$ 的数组 $$a$$ 和一个整数$$k$$。

需要将整个数组划分成恰好 $$k$$ 个连续子数组,每个子数组至少包含一个元素。

对一个数组 $$v$$,$$MEX(v)$$ 表示没有出现在其中的最小非负整数。

例如:$$MEX([0,2,1]) = 3$$,$$MEX([1,2,3]) = 0$$,$$MEX([0,1,1,0]) = 2$$。

在所有可能的划分中,定义 $$x = \min_{i=1}^{k} MEX(b_i)$$,其中 $$b_1, b_2, \cdots, b_k$$ 为划分得到的子数组。

你的任务是使 $$x$$ 尽量大,并求出能达到的最大值。

输入描述

第一行包含两个整数 $$n, k(1 \le k \le n \le 2 \times 10^5)$$,表示数组长度和要划分的子数组个数。

第二行包含 $$n$$ 个整数 $$a_1, a_2, \cdots, a_n(0 \le a_i \le 10^9)$$,表示数组的元素。

输出描述

输出一行一个整数,表示最大可能值 $$x$$。

样例1

输入

6 2
0 0 1 1 2 2

输出

1

题解:二分答案 + 贪心

题目问题拆解

将数组分成 $$k$$ 个连续段,最大化所有段 MEX 的最小值。$$n \le 2 \times 10^5$$,

答案具有单调性($$x$$ 越小越容易满足),适合二分答案。

核心观察:如果每段的 MEX $$\ge x$$,意味着每段都必须包含 $$0, 1, \cdots, x-1$$ 这 $$x$$ 个值。

贪心策略:从左到右扫描,一旦凑齐了 $$0 \sim x-1$$,就切一刀开始新的一段。如果能切出 $$\ge k$$ 段,说明 $$x$$ 可行。

算法实现

二分答案:二分 $$x \in [0, n+1]$$,check 目标是"能否贪心切出 $$\ge k$$ 段,每段包含 $$0 \sim x-1$$"。

check 函数:维护一个 $$seen$$ 数组和计数器 $$remaining$$(还需收集多少个不同的值)。

从左到右扫描数组,每遇到一个 $$0 \le val < x$$ 且未见过的值,$$remaining$$ 减 $$1$$。

当 $$remaining = 0$$ 时,凑齐了 $$0 \sim x-1$$,切一刀,段数加 $$1$$,重置 $$seen$$ 和 $$remaining$$。若段数 $$\ge k$$,通过。

二分过程:check(mid) 通过则扩大下界 $$lo = mid$$,否则缩小上界 $$hi=mid-1$$。

输出:最终 $$lo$$ 即为最大 $$x$$。

以样例为例:$$a = [0, 0, 1, 1, 2, 2]$$,$$k = 2$$。check($$x = 2$$):

需凑 $$\{0, 1\}$$。扫到 $$a[2] = 1$$ 时凑齐,切一刀(段1 $$= [0, 0, 1]$$)。

剩余 $$[1, 2, 2]$$,只有 $$1$$ 没有 $$0$$,凑不齐。段数 $$= 1 < 2$$,失败。check($$x = 1$$):

需凑 $$\{0\}$$。$$a[0] = 0$$ 凑齐,切一刀。$$a[1] = 0$$ 凑齐,切一刀。段数 $$= 2 \ge 2$$,通过。答案 $$= 1$$。

时空复杂度分析

时间复杂度:$$O(n \log n)$$,二分 $$O(\log n)$$ 轮,每轮 check $$O(n)$$。

空间复杂度:$$O(n)$$,$$seen$$ 数组最大长度为 $$n$$。

C++

Java

Python

# 划分 - 二分答案 + 贪心

def check(a, n, k, x):
    """贪心检查:能否划分成 ≥k 个连续段,每段 MEX ≥ x"""
    if x == 0:
        return True
    count = 0
    remaining = x  # 还需收集 0..x-1 中多少个不同的值
    seen = [False] * x
    for val in a:
        if val < x and not seen[val]:
            seen[val] = True
            remaining -= 1
        if remaining == 0:  # 凑齐了 0..x-1,切一刀
            count += 1
            if count >= k:
                return True
            seen = [False] * x
            remaining = x
    return False

def solve():
    n, k = map(int, input().split())
    a = list(map(int, input().split()))
    # 二分答案:找最大的 x 使得 check(x) 通过
    lo, hi = 0, n + 1
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if check(a, n, k, mid):
            lo = mid
        else:
            hi = mid - 1
    print(lo)

solve()

Go

第二题:开心食堂

在线评测链接:https://www.neituiya.com/oj/69/2348

题目描述

你开了一家食堂。新的一天的营业从第 $$0$$ 时刻开始,这一天食堂将迎来 $$n$$ 个顾客,

其中第 $$i$$ 个顾客的食物需要花费 $$a_i$$ 个时间单位制作,等餐截止时间为 $$b_i$$。

若 $$b_i$$ 时刻末顾客仍未取得他的食物,则顾客会不开心。

你可以指定你的食堂的营业时间 $$p$$,这意味着 $$p$$ 时刻末之后你的食堂会关门。

当顾客的等餐截止时间大于营业时间时,他会取消本次就餐,你不需要再制作该顾客的食物,

他也不会因为等餐截止时间前未取得食物而不开心。

定义 $$f(p)$$ 表示营业时间为 $$p$$ 时,最少有多少顾客不开心。请你计算 $$\sum_{i=1}^{\max(b_j)} f(i)$$。

输入描述

第一行有一个整数 $$n(1 \le n \le 10^5)$$,表示顾客的数量。

接下来 $$n$$ 行中第 $$i$$ 行有两个整数 $$a_i, b_i(1 \le a_i, b_i \le 10^9)$$,

分别表示第 $$i$$ 个顾客的食物的制作时间和顾客的等餐截止时间。

输出描述

输出一个整数,表示 $$\sum_{i=1}^{\max(b_j)} f(i)$$ 的结果。

样例1

输入

5
4 7
1 2
3 4
2 8
2 5

输出

5

样例解释

$$f(1) = 0$$(无顾客就餐)。$$f(2) = 0$$($$2$$ 号顾客 $$1$$ 时刻末完成)。

$$f(3) = 0$$,$$f(4) = 0$$($$2, 3$$ 号均可按时完成)。$$f(5) = 1$$($$2, 3, 5$$ 号中至少一人超时)。

$$f(6) = 1$$,$$f(7) = 1$$(加入 $$1$$ 号后仍只有 $$1$$ 人超时)。$$f(8) = 2$$(加入 $$4$$ 号后 $$2$$ 人超时)。$$\sum = 0 + 0 + 0 + 0 + 1 + 1 + 1 + 2 = 5$$。

题解:贪心 + 区间求和

** 题目问题拆解**

对每个营业时间 $$p$$,$$f(p)$$ 是"在截止时间 $$\le p$$ 的顾客中,最优排程下最少的超时人数"。

需要对所有 $$p$$ 从 $$1$$ 到 $$\max(b)$$ 求 $$f$$ 之和。$$n \le 10^5$$,$$\max(b) \le 10^9$$,直接枚举 $$p$$ 不可行。

核心观察:$$f(p)$$ 只在 $$p = b_i$$(某个顾客加入)时可能变化,两个相邻 $$b$$ 值之间 $$f$$ 恒定。

按 $$b$$ 排序贪心处理,用区间长度乘 $$f$$ 值求和即可。

算法实现

算法主策略:本题采用贪心(堆维护最优淘汰)+ 区间贡献累加

按截止时间从小到大处理顾客。维护一个最大堆存放已接受顾客的烹饪时间,以及当前总烹饪时间。

每加入一个新顾客(烹饪时间 $$a$$,截止时间 $$d$$),如果总时间超过 $$d$$,

就从堆中弹出烹饪时间最大的那个顾客——因为踢掉它能省下最多时间,让其他人都能按时完成。

被踢掉的顾客数 $$removed$$ 加 $$1$$,此时 $$f(d) = removed$$。

区间求和:将所有 $$b$$ 值去重排序。两个相邻 $$b$$ 值 $$d_{prev}$$ 和 $$d_{cur}$$ 之间,$$f$$ 不变,贡献 $$=f\times (d_{cur}-d_{prev}-1)$$。

再加上 $$f(d_{cur})$$ 本身($$1$$ 个点的贡献)。

以样例为例:按 $$b$$ 排序后依次加入。

$$d = 5$$ 时加入 $$5$$ 号($$a = 2$$),堆 $$= [1, 3, 2]$$,总时间 $$= 6 > 5$$,弹出 $$3$$,$$removed = 1$$。

$$f(5) = 1$$。区间 $$[5, 6]$$ 贡献 $$1 \times 2 = 2$$。$$d = 7$$ 加入 $$1$$ 号($$a = 4$$),堆 $$= [1, 2, 4]$$,总时间 $$= 7 \le 7$$,不弹出。

$$f(7) = 1$$。$$d = 8$$ 加入 $$4$$ 号($$a = 2$$),总时间 $$= 9 > 8$$,弹出 $$4$$,$$removed = 2$$。$$f(8) = 2$$。最终 $$\sum = 0 + 0 + 0 + 0 + 1 + 1 + 1 + 2 = 5$$。

时空复杂度分析

时间复杂度:$$O(n \log n)$$,排序 + 堆操作各 $$O(n \log n)$$。

空间复杂度:$$O(n)$$,堆和排序。

C++

Java

Python

# 开心食堂 - 贪心 + 区间求和
import heapq

def solve():
    n = int(input())
    customers = []
    for _ in range(n):
        a, b = map(int, input().split())
        customers.append((b, a))  # (deadline, cooking_time)

    customers.sort()

    # 贪心:按 deadline 逐批加入,超时弹最大:按 deadline 逐批加入
    heap = []       # 最大堆(负数),存已接受的 cooking_time
    total_time = 0  # 已接受客户的总烹饪时间
    removed = 0     # 当前被移除(不开心)的客户数 = f(p)
    total_sum = 0
    prev_d = 0      # 上一批的 deadline

    i = 0
    while i < n:
        d = customers[i][0]
        # [prev_d+1, d-1] 区间内 f(p) 不变
        if d > prev_d + 1:
            total_sum += removed * (d - 1 - prev_d)

        # 加入所有 deadline=d 的客户
        while i < n and customers[i][0] == d:
            _, a = customers[i]
            heapq.heappush(heap, -a)
            total_time += a
            i += 1

        # 贪心:总时间超过 deadline 时,弹出最大 cooking_time,省下最多时间
        while total_time > d and heap:
            max_a = -heapq.heappop(heap)
            total_time -= max_a
            removed += 1

        # p=d 这个时间点的贡献
        total_sum += removed
        prev_d = d

    print(total_sum)

solve()

Go

2026-3-8

第一题:方格世界

在线评测链接:https://www.neituiya.com/oj/16/2303

题目描述

方格世界中所有方格的长宽高均为$$1$$米。方格世界中有$$n$$个方格堆,编号依次为$$1, 2, \ldots, n$$。每个方格堆中的方格都是按照从下到上的方式堆成一列(假设有$$k$$个方格,则这$$k$$个方格堆成了一个长宽均为$$1$$米,高为$$k$$米的长方体)。初始时每个方格堆中的方格数量均为$$0$$。方格世界会下$$m$$场"方格雨",第$$i$$场"方格雨"会使得编号在$$l_i$$到$$r_i$$之间的方格堆的方格数量增加$$d_i$$。

定义$$f(x)$$表示经过"方格雨"后方格世界中高度大于等于$$x$$米的方格堆个数。你需要计算$$f(1), f(2), \ldots, f(10^{100})$$中一共有多少不同的取值。

输入描述

第一行包含两个整数$$n, m(1 \le n \le 10^9, 1 \le m \le 10^5)$$,分别表示方格世界中方格堆的个数、"方格雨"的次数。

接下来$$m$$行,第$$i$$行有三个整数$$l_i, r_i, d_i(1 \le l_i \le r_i \le n, 1 \le d_i \le 10)$$,分别表示"方格雨"作用的方格堆范围和增加的方格数量。

输出描述

输出一个整数,表示$$f(1), f(2), \ldots, f(10^{100})$$中一共有多少不同的取值。

样例1

输入

10 4
7 8 5
5 7 5
1 2 1
3 5 3

输出

6

样例解释

第一场"方格雨"后各个方格堆中方格数量:$$0, 0, 0, 0, 0, 0, 5, 5, 0, 0$$;

第二场"方格雨"后各个方格堆中方格数量:$$0, 0, 0, 0, 5, 5, 10, 5, 0, 0$$;

第三场"方格雨"后各个方格堆中方格数量:$$1, 1, 0, 0, 5, 5, 10, 5, 0, 0$$;

第四场"方格雨"后各个方格堆中方格数量:$$1, 1, 3, 3, 8, 5, 10, 5, 0, 0$$。

依次计算$$f(1)=8, f(2)=6, f(3)=6, f(4)=4, f(5)=4, f(6)=2, f(7)=2, f(8)=2, f(9)=1, f(10)=1, f(11)=f(12)=\cdots=f(10^{100})=0$$。集合$$\{8, 6, 6, 4, 4, 2, 2, 2, 1, 1, 0, \ldots\}$$中一共有$$6$$个不同的取值。

题解:差分数组 + 离散化

本题涉及到差分算法,不熟悉该算法的同学可以先做一下模板题:

语文成绩

题目内容拆解

$$n$$个方格堆经过$$m$$次区间加法操作后,$$f(x)$$表示高度$$\ge x$$的方格堆个数,求$$f$$函数在整个正整数域上的不同取值数量。

核心观察:随着$$x$$从$$1$$递增到无穷,$$f(x)$$是单调不增的阶梯函数——每当$$x$$超过某个方格堆的高度$$h$$时,$$f$$就减少若干。因此$$f$$的不同取值数,等于所有方格堆高度的不同取值个数(含$$0$$,因为$$f(10^{100})=0$$这个值总是存在的)。

以样例为例,最终高度序列为$$1, 1, 3, 3, 8, 5, 10, 5, 0, 0$$,不同高度值为$$\{0, 1, 3, 5, 8, 10\}$$共$$6$$个,答案即为$$6$$。

算法实现

算法主策略:本题采用差分数组 + 坐标压缩(离散化)

直接问题在于$$n \le 10^9$$,无法逐一存储所有方格堆的高度。关键洞察:每场雨只覆盖一个区间,所以整个$$[1, n]$$被所有区间端点分成若干段,同一段内所有方格堆的高度完全相同。这些分界点最多有$$2m$$个,我们只需关注这$$2m$$个坐标。

具体做法分三步。

第一步:建差分数组。对每场雨$$(l, r, d)$$执行$$diff[l] \mathrel{+}= d、diff[r+1] \mathrel{-}= d$$。

第二步:前缀和还原各段高度。将差分数组的所有关键坐标从小到大排序,逐个累加差值,累加到坐标$$p$$时的前缀和$$cur$$,就是区间$$[p, next\_p-1]$$内所有方格堆的高度。

第三步:收集不同高度值。将每个非零的$$cur$$插入集合,最后再把$$0$$加入集合(对应$$f(x)=0$$这一取值),集合大小即为答案。

以样例差分数组为例:$$diff[1]=1, diff[3]=2, diff[5]=5, diff[6]=-3, diff[7]=5, diff[8]=-5, diff[9]=-5$$。前缀和依次为$$1, 3, 8, 5, 10, 5, 0$$,不同正值集合$$\{1, 3, 5, 8, 10\}$$加上$$0$$,共$$6$$个。

时空复杂度分析

  • 时间复杂度:$$O(m \log m)$$,对$$2m$$个差分关键点排序是瓶颈,前缀和和集合插入均为$$O(m)$$。
  • 空间复杂度:$$O(m)$$,差分哈希表和高度集合各存储至多$$2m$$个元素。

类似题目

天文爱好者

【拼多多】2025-11-9-第二题-多多的宝物价值

Python

# 方格世界 - 差分数组 + 坐标压缩
from collections import defaultdict


def count_distinct(ops):
    diff = defaultdict(int)
    for l, r, d in ops:
        diff[l] += d
        diff[r + 1] -= d

    # 前缀和还原每段高度,收集不同取值
    heights = {0}  # f(x)=0当x超过最大高度时必然存在
    cur = 0
    for p in sorted(diff.keys()):
        cur += diff[p]
        if cur > 0:
            heights.add(cur)

    return len(heights)


n, m = map(int, input().split())
ops = []
for _ in range(m):
    l, r, d = map(int, input().split())
    ops.append((l, r, d))
print(count_distinct(ops))

第二题:不等式问题

在线评测链接:https://www.neituiya.com/oj/16/2304

题目描述

AK的老师给了他一道不等式题目,但是他不会做,于是他跑来向你求助。

给定两个整数$$n$$和$$x$$,找出满足$$ab+ac+bc \le n$$且$$a+b+c \le x$$的正整数三元组$$(a, b, c)$$的数量。请注意,按照AK老师的要求,顺序是有影响的(例如,$$(1, 1, 2)$$和$$(1, 2, 1)$$被视为不同的三元组),并且$$a、b、c$$必须严格大于$$0$$。

输入描述

第一行包含一个整数$$t(1 \le t \le 10^4)$$,表示测试用例的数量。

每个测试用例包含两个整数$$n, x(1 \le n, x \le 10^6)$$。保证所有测试用例的$$n$$之和不超过$$10^6$$,且所有测试用例的$$x$$之和不超过$$10^6$$。

输出描述

对于每个测试用例,输出一行一个整数,表示满足$$ab+ac+bc \le n$$且$$a+b+c \le x$$的正整数三元组$$(a, b, c)$$的数量。

样例1

输入

4
6 9
5 50
66 6
11451 419198

输出

4
4
20
2386336

题解:枚举

题目内容拆解

给定$$n$$和$$x$$,统计满足$$ab+ac+bc \le n$$、$$a+b+c \le x$$且$$a, b, c \ge 1$$的有序正整数三元组数量。

本题是枚举题(非数论),核心技巧是:三个变量直接枚举是$$O(n^3)$$太慢,但固定$$a$$和$$b$$后,$$c$$的合法范围可以直接用不等式推导出上界,从而把三重枚举降为两重,总复杂度依靠调和级数降到$$O(n \log n)$$。

算法实现

算法主策略:固定$$a$$和$$b$$,数学推导$$c$$的上界,直接计数。

固定$$a$$和$$b$$后,分别对两个约束推导$$c$$的最大值。由$$ab + ac + bc \le n$$提取$$c$$:$$ab + c(a+b) \le n$$,得$$c \le \lfloor(n-ab)/(a+b)\rfloor$$,记为$$c_1$$。由$$a+b+c \le x$$得$$c \le x-a-b$$,记为$$c_2$$。两个约束同时满足时,$$c$$的有效范围为$$1 \le c \le \min(c_1, c_2)$$,贡献$$\max(0, \min(c_1, c_2))$$个合法三元组。

以样例$$n=6, x=9$$为例推导前几组:取$$a=1, b=1$$时,$$c_1 = \lfloor(6-1)/2\rfloor = 2$$,$$c_2 = 9-1-1 = 7$$,$$c$$可取$$1, 2$$共$$2$$种;取$$a=1, b=2$$时,$$c_1 = \lfloor(6-2)/3\rfloor = 1$$,贡献$$1$$;取$$a=2, b=1$$时,$$c_1 = 1$$,贡献$$1$$;其余组合$$c_1 = 0$$跳过。合计$$2+1+1=4$$。

枚举范围:外层$$a$$从$$1$$到$$\min(x-2, n)$$(因为$$b, c \ge 1$$故$$a \le x-2$$,又$$ab \ge a$$故$$a \le n$$);内层$$b$$从$$1$$到$$x-a-1$$,一旦$$ab > n$$立即break提前退出——这是关键剪枝,因为$$b$$继续增大只会让$$ab$$更大,$$c_1$$更无解。

时空复杂度分析

  • 时间复杂度:$$O(n \log n)$$每组测试用例。内层$$b$$循环的总迭代次数由调和级数控制:$$\sum_{a=1}^{n} \lfloor n/a \rfloor \approx n \ln n$$。题目保证所有测试用例$$n$$之和$$\le 10^6$$,故总计算量约$$O(10^6 \times 14) = O(1.4 \times 10^7)$$。
  • 空间复杂度:$$O(1)$$,仅使用常数额外空间。

Python

# 不等式问题 - 枚举双变量 + 数学推导
def count_triples(n, x):
    ans = 0
    # 固定a和b,推导c的上界直接计数
    for a in range(1, x - 1):
        if a > n:
            break
        for b in range(1, x - a):
            ab = a * b
            if ab > n:
                break  # ab超过n,c无解,提前退出
            c1 = (n - ab) // (a + b)  # 由ab+c(a+b)<=n推导
            c2 = x - a - b            # 由a+b+c<=x推导
            c_max = min(c1, c2)
            if c_max >= 1:
                ans += c_max
    return ans


t = int(input())
for _ in range(t):
    n, x = map(int, input().split())
    print(count_triples(n, x))

华为

2026-4-23-留学生AI 岗选择题

一、单选题

1、量化(Quantization)技术中,将 FP16 转为 INT8 主要压缩了:

A. 注意力机制的头数
B. 模型的层数
C. 权重的存储位宽
D. Token 词表的长度

答案:C

2、对于输入为 $$224 \times 224 \times 3$$ 的图像,使用一个卷积层,包含 96 个 $$11 \times 11$$ 的卷积核,步长为 4,无填充(padding = 0),那么输出特征图的大小和深度分别是?

A. $$55 \times 55 \times 96$$
B. $$57 \times 57 \times 96$$
C. $$56 \times 56 \times 96$$
D. $$54 \times 54 \times 96$$

答案:D

3、在原始 Transformer 的多头注意力机制中,多个头间的输出是如何结合的?

A. 取最大值
B. 拼接后经过线性变换
C. 逐元素相加
D. 取平均

答案:B

4、关于 Transformer 中的多头注意力(Multi-Head Attention)的表述,哪一项是正确的?

A. 多个头共享相同的查询、键、值权重矩阵
B. 每个头独立学习不同的线性投影,最后将注意力输出拼接
C. 头数越多,模型推理速度一定越快
D. 每个头关注输入序列的同一局部特征

答案:B

5、以下关于凸函数的说法,正确的是:

A. 凸函数的局部最小值一定是全局最小值
B. 凸函数的二阶导数可以为负
C. 凸函数一定没有最小值
D. 所有多项式函数都是凸函数

答案:A

6、对于回归问题,假设真实值和预测值的误差分布符合正态分布,且均值为 0。此时,以下哪个指标最能反映模型的整体预测精度?

A. 最大绝对误差(Max Error)
B. 中位数绝对误差
C. 极差(Range)
D. 均方误差(MSE)

答案:D

7、关于 Pass@k(代码生成常用)中 "k" 的含义,正确的是:

A. 把 k 道题的平均正确率作为指标
B. 从 k 个模型中选最强的一个
C. 生成时把 top\_k 设置为 k
D. 对同一题生成的 n 个候选($$n \ge k$$),随机抽取 k 个,k 个中至少有一个通过就算成功

答案:D

8、以下推理代码速度较慢:

generated = input_ids
for _ in range(max_new_tokens):
    out = model(input_ids=generated)
    next_token = out.logits[:, -1, :].argmax(dim=-1, keepdim=True)
    generated = torch.cat([generated, next_token], dim=1)

最有效的优化方向是:

A. 每步都重新 tokenize
B. 将 max_new_tokens 增大
C. 把 argmax 改成 topk
D. 使用 past_key_values(KV Cache)避免重复计算历史上下文

答案:D

9、关于多头注意力(MHA)和分组查询注意力(GQA)的区别,下列说法正确的是?

A. GQA 只能用于解码器
B. MHA 比 GQA 参数量更少
C. GQA 中每个头有自己的 K 和 V,而 MHA 共享
D. GQA 中多个查询头共享一组 K 和 V

答案:D

10、关于旋转位置编码(RoPE, Rotary Position Embedding)的表述,正确的是?

A. RoPE 仅适用于编码器,不适用于自回归解码器
B. RoPE 无法处理超出预训练长度的序列
C. RoPE 通过旋转矩阵对查询和键向量进行变换,使内积蕴含相对位置信息
D. RoPE 将绝对位置信息直接加到词嵌入中

答案:C

11、设计一个用于图像分类的卷积神经网络(CNN),隐藏层需兼顾计算效率和缓解梯度消失,输出层用于多分类任务,下列激活函数搭配最合理的是?

A. 隐藏层用 ReLU,输出层用 Softmax
B. 隐藏层用 Sigmoid,输出层用 ReLU
C. 隐藏层用 Tanh,输出层用 Sigmoid
D. 隐藏层用 Softmax,输出层用 Tanh

答案:A

12、关于模型推理中前向计算与训练前向计算的区别,下列说法错误的是:

A. 推理前向计算仅需输出最终结果,训练前向计算需保留中间特征用于反向传播
B. 推理前向计算可复用 KV Cache,训练前向计算无需缓存 K/V 向量
C. 推理前向计算与训练前向计算的计算逻辑完全一致,仅输入数据不同
D. 推理前向计算的批量大小通常小于训练,以平衡延迟和吞吐量

答案:C

13、对于函数 $$f(x) = \frac{1}{1+25x^2}$$,在区间 $$[-1,1]$$ 上取等距节点进行高次插值,当 $$n$$ 增大时,插值多项式在区间两端会出现什么现象?

A. 收敛到 $$f(x)$$
B. 插值多项式趋于零
C. 振荡加剧,误差变大
D. 插值多项式趋于直线

答案:C

14、若向量组 $$\{\vec{a}_1, \vec{a}_2, \vec{a}_3\}$$ 线性无关,则以下说法正确的是?

A. 不存在不全为零的系数使线性组合为零
B. 可由更少向量生成同一空间
C. 任意两个向量线性相关
D. 向量个数大于维度

答案:A

15、在线性分类模型中,预测函数常写为 $$y = w^Tx + b$$,设 $$w = (1, 2)$$$$x = (3, 4)$$$$b = 1$$,计算模型输出 $$y$$

A. 10
B. 12
C. 11
D. 13

答案:B


二、多选题

16、影响 GPU 内核占用率的因素包括以下哪些选项?

A. 线程块使用的共享内存大小
B. 线程块的维度设置
C. 每个线程使用的寄存器数量
D. GPU 核心的时钟频率

答案:A, B, C

17、某公司正在开发一个智能客服系统,使用多 Agent 协作架构来处理复杂的用户请求。系统采用任务分解和分发机制,主 Agent 负责将用户请求分解为子任务并分发给专门的功能 Agent(如订单 Agent、支付 Agent、售后 Agent)。在设计和优化这个系统时,以下哪些关于规划和协作的策略是正确的?

A. 设子任务数量为 $$n$$,若所有子任务相互独立且可并行,系统吞吐量可近似达到单 Agent 吞吐量的 $$n$$ 倍;若存在依赖链,吞吐量会受限于关键路径的子任务串行执行时间
B. 主 Agent 在任务分解时应考虑子任务之间的数据依赖关系,确保下游 Agent 能够获得上游 Agent 执行结果
C. 当主 Agent 检测到某个子任务 Agent 执行失败时,可以根据任务重要性决定是重试该子任务还是终止整个任务流
D. 为减少通信开销,所有子任务应该尽可能并行执行,即使存在依赖关系

答案:A, B, C

18、在大语言模型推理阶段,以下哪些技术在配置正确的情况下,可以实际降低单 token 的生成延迟?

A. 对模型进行 INT8 权重量化,并在推理时配合 GPU Tensor Core 执行
B. 使用 KV Cache 以避免重复计算历史 token 的 Attention
C. 在推理服务中增大 Batch Size 并启用连续批处理(Continuous Batching)
D. 采用 FlashAttention / FlashAttention-2 实现 Attention 计算

答案:A, B, D

19、假设某分布式训练集群中有两台处理节点的参数同步模块,它们无故障连续运行的时间 $$T_1$$$$T_2$$(单位:百小时)相互独立,且分别服从参数为 $$\lambda_1 = 2$$$$\lambda_2 = 3$$ 的指数分布。现定义该同步子系统出现首个节点故障的时间为 $$Y = \min(T_1, T_2)$$。以下关于随机变量 $$Y$$ 的计算结论中,正确的有哪些?

A. 第一个节点先于第二个节点发生故障的概率为 $$2/5$$
B. 该子系统的平均无故障运行时间(即 $$Y$$ 的数学期望)为 5 百小时
C. 该子系统能无故障运行超过 100 小时(即 $$Y > 1$$)的概率为 $$e^{-5}$$
D. 随机变量 $$Y$$ 服从参数为 5 的指数分布

答案:A, C, D

20、数据科学家小李正在训练一个大型语言模型,采用混合精度训练策略。他发现在某些层使用 FP16 计算时会出现数值不稳定问题。以下关于混合精度训练中数值稳定性问题的分析中,正确的有?

A. Softmax 函数在计算 $$\exp(x_i)$$ 时,当 $$x_i$$ 较大的值在 FP16 中容易溢出
B. 在 Softmax 计算中,可以使用 max-shift 技巧:先减去 $$\max_j x_j$$ 再计算指数,避免溢出
C. 对于 BatchNorm 层,使用 FP32 存储移动均值(running mean)和方差(running variance)可以提高数值稳定性
D. LayerNorm 在混合精度训练中完全没有数值风险,不需要特殊处理

答案:A, B, C

2026-4-23-留学生AI岗编程题

第一题:基于最大边际相关性(MMR)的智能示例重排序

在线评测链接:https://www.neituiya.com/oj/16/2596

第二题:决策树实现网络设备故障预测

在线评测链接:https://www.neituiya.com/oj/16/2597

2026-4-23-留学生研发岗

第一题:给软件版本号排序

在线评测链接:https://www.neituiya.com/oj/16/2598

第二题:AI超节点内部计算单元通信最小时延

在线评测链接:https://www.neituiya.com/oj/16/2599

第三题:奖品采购

在线评测链接:https://www.neituiya.com/oj/16/2600

2026-4-22国内AI岗选择题

1、某运营商构建网络配置命令、设备运行日志、故障案例一体化检索系统,文本中包含大量命令行(如display interface、ip route)、MAC/IP地址、端口编号、VLAN ID、OID等结构化内容。

A. 对命令行、IP、MAC、端口等增加专用词表,按命令块/日志条目切分,使用通信领域微调 Embedding
B. 全部文本统一小写并去除符号,避免分词异常
C. 直接按固定长度切分,使用通用分词与通用 Embedding
D. 只使用关键词 BM25 检索,不使用 Embedding

答案:A

2、在大模型指令微调实验中,研究人员对比了3种训练数据配比(纯指令、指令+多轮对话、指令+知识)下的模型响应准确率,需直观展示不同配比的准确率差异并便于组间对比,最适合的可视化图表是( )

A. 分组柱状图
B. 雷达图
C. 饼图
D. 直方图

答案:A

3、牛顿迭代法的迭代公式为

A. $$x_{n+1}=x_n - f(x_n)/f'(x_n)$$
B. $$x_{n+1}=x_n - f(x_n)$$
C. $$x_{n+1}=x_n + f(x_n)/f'(x_n)$$
D. $$x_{n+1}=x_n - f'(x_n)/f(x_n)$$

答案:A

4、张量并行(TP)主要用于解决单卡显存无法容纳单个模型层权重的问题,其切分逻辑是?

A. 按优化器状态切分,不同GPU维护不同的优化器状态
B. 按网络层深度切分,不同层放在不同GPU上
C. 按批次大小切分,不同样本放在不同GPU上
D. 按矩阵运算的维度切分,同一层内的权重被拆分到不同GPU上

答案:D

5、下列哪个矩阵一定是可逆的?

A. 对称矩阵
B. 行列式为零的矩阵
C. 单位矩阵
D. 所有元素都为零的矩阵

答案:C

6、在多轮对话推理系统中,如果GPU/NPU显存已满但仍有新的token需要生成,系统通常采用什么策略?

A. 将部分KV Cache swap到CPU或进行recomputation
B. 自动降低模型精度
C. 重启服务器
D. 直接杀掉进程

答案:A

7、在部署阶段(Inference),将卷积层(Convolution)与批归一化层(Batch Normalization, BN)进行融合(Folding)是各大编译器的标配操作。以下关于 Conv+BN 融合的描述中,最准确的是

A. 融合是为了让 BN 的梯度能更快反向传播到 Conv 层,从而加速推理
B. 融合是一种纯代数等价变换,在模型编译期(Offline)就将 BN 的缩放和偏移参数直接吸收到 Conv 的权重和偏置中,运行时完全没有 BN 的开销
C. 融合是在运行时(Runtime)将 BN 的均值和方差传入 Conv 的底层算子中并行计算
D. Conv+BN 融合会轻微改变模型的输出精度,因为融合后的算子无法使用 Tensor Core 加速

答案:B

8、在流水线并行(Pipeline Parallel)中,一个模型被切分为多个 Stage,分布在不同 GPU 上。当某些 GPU 在等待上游 Stage 的计算结果时出现空闲,这种现象被称为?

A. Pipeline Bubble
B. 显存碎片
C. GPU Context Switch
D. 网络拥塞

答案:A

9、在层次聚类分析(HAC)中,以下哪一种方法是用于定义簇间距离的常见方式?

A. Expectation-Maximization
B. K-means
C. DBSCAN
D. Complete Linkage

答案:D

10、某多分类任务中,类别A有1000个样本,类别B有10个样本。若使用Micro-F1计算,主要反映的是哪个类别的性能?

A. 两者权重相同
B. 取决于具体的F1计算公式
C. 类别B
D. 类别A

答案:D

11、在PyTorch 中,以下哪个函数用于执行优化器的一步更新

A. optimizer.zero\_grad()
B. optimizer.backward()
C. optimizer.step()
D. optimizer.update()

答案:C

12、工程师需要计算一个复杂函数 $$f(x)$$ 在区间 $$[0,1]$$ 上的定积分 $$\int_{0}^{1} f(x) \, dx$$。以下关于蒙特卡洛积分与黎曼积分的对比,说法正确的是?

A. 蒙特卡洛积分仅适用于低维积分问题,高维时应使用黎曼积分
B. 蒙特卡洛积分的收敛速度为 $$O(N^{-2})$$,黎曼积分(等距划分)为 $$O(N^{-1})$$
C. 蒙特卡洛积分的计算复杂度远低于黎曼积分,因此总是首选
D. 蒙特卡洛积分的收敛速度为 $$O(N^{-1/2})$$,黎曼积分(等距划分)为 $$O(N^{-2})$$

答案:D

13、适合高维稀疏向量相似度计算的是?

A. 余弦相似度
B. 切比雪夫距离
C. 欧氏距离
D. 曼哈顿距离

答案:A

14、Transformer的编码器-解码器注意力(Encoder-Decoder Attention)中,查询(Query)来自哪里?

A. 输入序列
B. 解码器的上一层的输出
C. 位置编码
D. 编码器的输出

答案:B

15、给定两个向量 $$\mathbf{a}=[1,2,3]$$$$\mathbf{b}=[4,5,6]$$,他们余弦相似度为

A. 1.0
B. 0.87
C. 0.97
D. 0.67

答案:C

16、多模态大模型中,常见的视觉-语言连接器(Connector)包括?(多选)

A. MLP(多层感知机)
B. 线性投影层(Linear Projection)
C. 卷积池化层
D. Q-Former

答案:A, B, D

17、适用于机器学习中度量两个特征向量的相似度的有?(多选)

A. 欧氏距离
B. 余弦相似度
C. 汉明距离
D. KL散度

答案:A, B, C

18、设 $$X_{1},\ldots,X_{n}\sim N(\mu,\sigma^{2})$$,下面正确的有(多选)

A. $$\mu$$ 的MLE是样本均值 $$\bar{X}$$
B. $$\mu$$ 的MLE是无偏的
C. $$\sigma^{2}$$ 的MLE是 $$(1/n)\sum(X_{i}-\bar{X})^{2}$$
D. $$\sigma^{2}$$ 的MLE是无偏的

答案:A, B, C

19、以下关于反向传播计算效率的说法正确的是?(多选)

A. 反向传播的计算量大约是前向传播的2倍
B. 符号微分跟数值微分是两种计算体系,在一个模型训练时只能使用其中一种
C. 反向传播需要存储所有中间层的激活值,因此显存消耗大
D. 相比于数值微分,符号微分计算梯度的速度快

答案:A, C

20、你维护的在线问答服务(vLLM)在晚高峰出现:TTFT 从 0.9s 升至 2.4s,decode tokens/s 基本不变,平均输入长度从 700 升至 2200,GPU利用率接近满载。你本班次可立即执行哪些动作?(多选)

A. 对超长输入先走"摘要压缩链路",再送主模型
B. 增大temperature到1.2
C. 把max\_new\_tokens从512调到1024
D. 开启/优化prefix cache(固定system prompt场景)

答案:A, D
2026-4-22-国内AI岗

第一题:统计二叉树中平衡路径的数量

在线评测链接:https://www.neituiya.com/oj/16/2594

题目描述

定义二叉树的平衡路径需同时满足以下 $$3$$ 个条件:

  1. 路径从任意节点出发,仅能向下延伸(只能向左/右子节点,不可向上回溯)。
  2. 路径上所有节点的和相加为 $$0$$。

3) 路径长度(包含的节点个数)至少为 $$2$$。

请实现一个函数,输入二叉树的根节点(按层序遍历规则构建),返回该树中所有平衡路径的总数。

建树规则:层序遍历列表按从上到下、从左到右的顺序构建二叉树,$$None$$ 表示对应位置无节点。路径延伸:从起点出发,仅沿左子节点或右子节点单向向下(单链,不可分叉)。统计方式:每个符合条件的单链路径独立计数(即使路径有重叠)。

输入描述

一行,表示二叉树的层序遍历列表(元素为整数或 $$None$$,用方括号和逗号分隔)。列表长度 $$n(1 \le n \le 10^4)$$,节点值 $$val(-10^9 \le val \le 10^9)$$。

输出描述

一个整数,表示平衡路径的总数。

样例1

输入

[10, -5, -5, 2, -2, 3, -3]

输出

0

样例2

输入

[0, 0, None]

输出

1

样例3

输入

[1, -1, 2, -2, None, 3, -3]

输出

2

第二题:网络异常流量传播链路溯源

在线评测链接:https://www.neituiya.com/oj/16/2595

题目描述

在网络监控中,异常流量的流动通常具有局部聚集性。监控系统需要识别出高负载的基站(关键节点),并判断流量在这些节点之间定向的传播链的最长路径。

直接关联:对于基站 $$A$$ 和 $$B$$,若其曼哈顿距离 $$|x_A - x_B| + |y_A - y_B| \le \varepsilon_{dist}$$,则判定两者具有直接关联。

关键节点判定:计算一个基站及其所有具有直接关联属性的基站(含自身)的流量负载 $$w$$ 之和。若该总和 $$\ge W_{threshold}$$,则该基站被判定为关键节点。

链路条件:若两个关键节点具有直接关联关系,且发生时间戳 $$t$$ 不同,则流量从时间较早的基站流向时间较晚的基站。若两个关联的关键节点发生时间完全相同,则它们之间无法建立有效的传播链路。

传播链条:传播链条是由一系列关键节点通过有向链路首尾相连构成的路径。链条的规模为该路径上所有节点服务的用户数 $$Users$$ 之和。计算全网中可能形成的所有传播链条中,能够覆盖的最大用户总数。

输入描述

第一行包含三个整数 $$N, \varepsilon_{dist}, W_{threshold}(1 \le N \le 200, 0 \le \varepsilon_{dist} \le 10^9, 0 \le W_{threshold} \le 10^{18})$$,分别表示基站总数、曼哈顿距离阈值和负载阈值。

接下来 $$N$$ 行,每行包含 $$x, y, t, w, Users(0 \le x, y, t, w, Users \le 10^9)$$,表示基站的坐标、时间戳、负载和用户数。

输出描述

输出一个整数,代表最大用户数。若全网无法形成任何传播链条或关键节点,输出 $$0$$。

样例1

输入

3 1 500
0 0 10 100 50
1 0 20 100 50
0 1 30 100 50

输出

0

样例解释

三个基站互为邻居,但每个基站邻域最大负载和仅为 $$100 \times 3 = 300 < 500$$,没有任何基站能成为关键节点。无关键节点即无法形成链条,输出 $$0$$。

样例2

输入

4 1 150
0 0 10 100 10
1 0 20 100 10
5 5 10 200 100
5 6 30 200 100

输出

200

样例解释

基站 $$0$$ 和 $$1$$ 的曼哈顿距离为 $$1$$,互为邻居,各自负载和为 $$200 \ge 150$$,均为关键节点。基站 $$2$$ 和 $$3$$ 的曼哈顿距离为 $$1$$,互为邻居,各自负载和为 $$400 \ge 150$$,均为关键节点。链条 $$0 \to 1$$ 的用户数为 $$10 + 10 = 20$$,链条 $$2 \to 3$$ 的用户数为 $$100 + 100 = 200$$,最大为 $$200$$。

2026-4-22-国内非AI岗

第一题:简易的二进制包依赖关系检查和处理

在线评测链接:https://www.neituiya.com/oj/16/2591

题目描述

一个项目中,除了自研的代码外,还会依赖很多二进制包(后续简称为包),这些包也会依赖其它的包,每个被依赖的包还有版本号的要求。本题需要完成一个简易的包依赖关系分析和处理的模型,要求对输入的一组依赖关系进行分析,判断是否存在循环依赖,如果有循环依赖则输出不合理;否则进一步对依赖包的版本号进行规整,并输出规整后的依赖关系串。

依赖关系的数据结构由三个属性组成:序号(任意正整数,唯一标识一个包)、依赖包序号(该包所依赖的另一个包的序号)、依赖包版本号(正整数,$$1 \le 版本号 \le 99$$)。例如 $$\{1,3,11\}$$ 表示包 $$1$$ 依赖包 $$3$$ 的 $$11$$ 版本。

处理规则如下:

  1. 判断包依赖关系中是否存在循环依赖。包之间的依赖关系不能形成循环,例如包 $$1$$ 依赖包 $$2$$,包 $$2$$ 依赖包 $$3$$,包 $$3$$ 又依赖包 $$1$$,属于循环依赖。版本号不纳入循环依赖的判断,自己依赖自己也属于循环依赖。
  2. 对包依赖关系的版本号进行规整处理。如果包依赖关系合理,对于多个包依赖同一个包的情况,取被依赖包的最大版本号,替换所有对该包的版本号引用。

输入描述

每次输入两组依赖关系的信息,分别解析和输出两组结果。每组格式:第一行包含正整数 $$n(0 < n < 100)$$,表示包依赖关系的个数。接下来 $$n$$ 行,每行格式为 $$seq,dep\_seq,ver$$,以逗号分隔,表示一个依赖关系。输入的依赖关系中,包 $$X$$ 依赖包 $$Y$$ 只会出现一次。每个包可以依赖多个包,包 $$X$$ 也可以被多个包依赖。

输出描述

按顺序依次输出两组结果。每组:如果存在循环依赖,输出 $$false$$;否则输出版本号规整后的依赖关系(格式同输入,每行一个)。

样例1

输入

3
1,2,23
2,3,34
4,2,25
3
1,2,23
2,3,34
3,1,12

输出

1,2,25
2,3,34
4,2,25
false

样例解释

第一组:包 $$1$$ 和包 $$4$$ 都依赖包 $$2$$,版本号分别为 $$23$$ 和 $$25$$,取最大值 $$25$$,将包 $$2$$ 的版本号统一替换为 $$25$$。第二组:包 $$1 \to 2 \to 3 \to 1$$ 形成循环依赖,输出 $$false$$。


第二题:硬件布线

在线评测链接:https://www.neituiya.com/oj/16/2592

题目描述

硬件 $$PCB$$ 板上两个芯片之间需要布一条 $$I2C$$ 链路,两个芯片分别位于左上角和右下角,$$PCB$$ 走线仅能向下和向右移动,但是当前 $$PCB$$ 上已经有一些器件或者干扰源,器件和干扰源都要绕开。给一个二维数组,$$0$$ 表示可以布线,$$1$$ 表示已有器件,$$2$$ 表示开关电源,$$3$$ 表示开孔,$$4$$ 表示 $$GND$$,需要找到从芯片 $$A$$(左上角)到芯片 $$B$$(右下角)之间通路的最少转弯次数。如果没有通路,直接返回 $$-1$$。

输入描述

第一行包含两个整数 $$m, n(0 < m, n \le 100)$$,分别表示行数和列数。若 $$m < 3$$ 或 $$n < 3$$,直接返回 $$-1$$。

接下来 $$m$$ 行,每行一个长度为 $$n$$ 的字符串,每个字符为 $$0 \sim 4$$ 的数字,表示 $$PCB$$ 板上的器件分布。

输出描述

返回芯片 $$A$$ 到芯片 $$B$$ 之间通路的最少转弯次数。若无通路或输入参数不满足要求,返回 $$-1$$。

样例1

输入

4 4
0204
0130
0100
1000

输出

-1

样例解释

从芯片 $$A$$(左上角)到芯片 $$B$$(右下角)无通路可以到达,返回 $$-1$$。

样例2

输入

3 3
010
000
200

输出

2

样例解释

路径 $$(0,0) \to (1,0) \to (1,1) \to (1,2) \to (2,2)$$ 转弯 $$2$$ 次(第一步向下,在 $$(1,0)$$ 转向右,在 $$(1,2)$$ 转向下)。路径 $$(0,0) \to (1,0) \to (1,1) \to (2,1) \to (2,2)$$ 转弯 $$3$$ 次。最少转弯次数为 $$2$$。

样例3

输入

2 2
00
00

输出

-1

样例解释

输入参数不满足要求($$m < 3$$ 或 $$n < 3$$),直接返回 $$-1$$。


第三题:星球大战

在线评测链接:https://www.neituiya.com/oj/16/2593

题目描述

在潘多拉星球上,有一群怪兽组成一道阵线,AK机必须按顺序与这些怪兽战斗。AK机有一个初始能量值 $$E$$,每个怪兽都有一个攻击力 $$damage$$ 和击败奖励 $$reward$$(击败后AK机可以恢复相应的能量值)。

当AK机面对第 $$i$$ 个怪兽时,有以下选择:如果当前能量值大于怪兽攻击力 $$damage[i]$$(严格大于),AK机可以选择战斗,此时会先消耗 $$damage[i]$$ 点能量,然后增加 $$reward[i]$$ 点能量。如果当前能量值小于或等于 $$damage[i]$$,则不能战斗只能跳过。无论能否打过,也可以选择跳过,不消耗也不增加能量。

AK机的目标是尽可能多地击败怪兽(最大化击败数量),战斗顺序不能改变。

输入描述

第一行包含整数 $$E(1 \le E \le 10^9)$$,表示AK机初始能量值。

第二行包含 $$n$$ 个整数 $$damage[i](1 \le damage[i] \le 10^9)$$,空格分隔,表示每个怪兽的攻击力。

第三行包含 $$n$$ 个整数 $$reward[i](1 \le reward[i] \le 10^9)$$,空格分隔,表示击败每个怪兽后获得的能量奖励。怪兽数量 $$n(1 \le n \le 100)$$。

输出描述

一个整数,表示最多击败的怪兽数量。

样例1

输入

18
15 17 4 18
1 15 4 17

输出

2

样例解释

AK机初始能量 $$18$$,跳过第 $$1$$ 个怪兽,打第 $$2$$ 个怪兽($$18 - 17 + 15 = 16$$),打第 $$3$$ 个怪兽($$16 - 4 + 4 = 16$$),能量不足跳过第 $$4$$ 个,最多打败 $$2$$ 个。

样例2

输入

5
10 20 30
1 1 1

输出

0

样例解释

每个怪兽的攻击力都大于等于AK机能量,全部无法战斗,输出 $$0$$。

样例3

输入

9
5 4 5
0 3 4

输出

2

样例解释

AK机初始能量 $$9$$,跳过第 $$1$$ 个怪兽,打第 $$2$$ 个($$9 - 4 + 3 = 8$$),打第 $$3$$ 个($$8 - 5 + 4 = 7$$),最多打败 $$2$$ 个。

2026-4-15-国内AI岗

第一题:基于AdamW优化的网络带宽预测模型

在线评测链接;https://www.neituiya.com/oj/16/2522

题目描述

在华为网络通信业务中,网络带宽预测模型是保障数据传输稳定性的核心模块之一,通过历史数据拟合的带宽模型为 $$y = w_1 \cdot x_1 + w_2 \cdot x_2 + b$$(其中 $$y$$ 表示带宽,$$x_1, x_2$$ 为影响带宽的因子,$$w_1, w_2$$ 为权重参数,$$b$$ 为偏置参数)。请实现 AdamW 优化算法,基于给定样本数据迭代更新模型参数。

核心概念

损失函数:对于单个样本 $$(x_1, x_2, y_{true})$$,损失 $$L = (y_{pred} - y_{true})^2$$,其中 $$y_{pred} = w_1 \cdot x_1 + w_2 \cdot x_2 + b$$。

梯度计算

$$g_{w_1} = \frac{\partial L}{\partial w_1} = 2 \cdot (y_{pred} - y_{true}) \cdot x_1$$

$$g_{w_2} = \frac{\partial L}{\partial w_2} = 2 \cdot (y_{pred} - y_{true}) \cdot x_2$$

$$g_b = \frac{\partial L}{\partial b} = 2 \cdot (y_{pred} - y_{true})$$

AdamW 算法参数:动量参数 $$\beta_1 = 0.9$$,$$\beta_2 = 0.999$$,权重衰减系数 $$\lambda = 0.01$$,学习率 $$\alpha = 0.001$$,数值稳定性常数 $$\epsilon = 10^{-8}$$。

一阶动量更新:$$m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t$$($$g_t$$ 为当前梯度)

二阶动量更新:$$v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2$$

偏差修正:$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}$$,$$\hat{v}_t = \frac{v_t}{1 - \beta_2^t}$$($$t$$ 为当前迭代次数,从 $$1$$ 开始)

参数更新

$$\theta_t = \theta_{t-1} - \alpha \cdot \left(\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \cdot \theta_{t-1}\right)$$

其中 $$\theta$$ 表示 $$(w_1, w_2, b)$$。

初始参数:$$w_1 = w_2 = b = 0$$,初始一阶动量 $$m_{w_1} = m_{w_2} = m_b = 0$$,初始二阶动量 $$v_{w_1} = v_{w_2} = v_b = 0$$。

输入描述

第一行输入一个整数 $$N$$,表示样本数量。接下来 $$N$$ 行,每行 $$3$$ 个浮点数 $$x_1, x_2, y_{true}$$,表示一个样本。

输出描述

一行,$$3$$ 个浮点数,依次为还原后的 $$w_1, w_2, b$$,结果保留 $$6$$ 位小数,银行家舍入,以一个空格分隔,前后无冗余空格。

样例1

输入

3
1.0 1.0 2.0
2.0 2.0 4.0
3.0 3.0 6.0

输出

0.002750 0.002750 0.002923

样例解释

样本数量 $$3$$,后面共 $$3$$ 行,表示 $$3$$ 个样本,每行 $$3$$ 个浮点数,以空格间隔。

样例2

输入

1
0.0 0.0 0.0

输出

0.000000 0.000000 0.000000

样例解释

样本数量 $$1$$,所有输入为 $$0$$,梯度为零,参数无更新。

题解:NumPy AdamW 优化器

题目内容拆解

给定 $$N$$ 个样本,逐样本迭代 AdamW 更新线性模型 $$y = w_1 x_1 + w_2 x_2 + b$$ 的参数。每个样本触发一次参数更新,$$N$$ 个样本对应 $$N$$ 步优化。

算法实现

增广特征:把偏置 $$b$$ 视作"输入恒为 $$1$$ 的特征"的权重,将 $$w_1, w_2, b$$ 合并为参数向量 $$\theta = [w_1, w_2, b]^T$$,输入增广为 $$\tilde x = [x_1, x_2, 1]^T$$,预测值写成一次点积:

$$y_{pred} = \theta^T \tilde x = w_1 x_1 + w_2 x_2 + b$$

这样三个参数的梯度可以统一计算,无需分别处理。

梯度计算:误差 $$e = y_{pred} - y_{true}$$,损失 $$L = e^2$$ 对 $$\theta$$ 的梯度为

$$g = 2e \cdot \tilde x$$

直觉上,误差越大、对应特征值越大,该参数的调整幅度就越大。

一阶动量更新:一阶动量 $$m$$ 是梯度方向的指数滑动平均,平滑单步噪声:

$$m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t$$

$$\beta_1 = 0.9$$ 意味着当前梯度只占 $$10\%$$ 权重,历史方向占 $$90\%$$,让更新方向更稳定。

二阶动量更新:二阶动量 $$v$$ 是梯度大小的指数滑动平均,用来给每个参数定制步长:

$$v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2$$

某个参数历史梯度一直很大,$$v$$ 就大,后续更新就自动缩小步长,防止震荡。

偏差修正:$$m, v$$ 初始化为零,前几步被零值拖小了(例如 $$t=1$$ 时 $$m_1 = 0.1 g_1$$,只有真实均值的 $$10\%$$),除以校正因子补偿:

$$\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \qquad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}$$

随着 $$t$$ 增大,$$\beta^t \to 0$$,校正因子趋近 $$1$$,修正效果自动消退。

AdamW 参数更新:自适应步长加上独立的权重衰减:

$$\theta_t = \theta_{t-1} - \alpha \cdot \left(\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \cdot \theta_{t-1}\right)$$

$$\hat{m}/\sqrt{\hat{v}}$$ 是自适应学习率,让每个参数按自身梯度历史调节步长。$$\lambda \theta$$ 是权重衰减项,直接缩小参数本身(而非像 L2 正则那样加入梯度),防止参数过大。NumPy 向量化后,$$\theta, m, v$$ 各为长度 $$3$$ 的数组,所有运算一行代码完成。

时空复杂度分析

  • 时间复杂度:$$O(N \cdot d)$$,$$d = 3$$ 为参数个数,每个样本做常数次向量运算。
  • 空间复杂度:$$O(N \cdot d)$$,存储样本数据 $$O(N \cdot d)$$,优化器状态 $$O(d)$$。

Python

# 基于AdamW优化的网络带宽预测模型 - AdamW优化器
import numpy as np

n = int(input())
X = np.zeros((n, 2))
Y = np.zeros(n)
for i in range(n):
    parts = input().split()
    X[i, 0], X[i, 1], Y[i] = float(parts[0]), float(parts[1]), float(parts[2])

beta1, beta2, lam, alpha, eps = 0.9, 0.999, 0.01, 0.001, 1e-8

# theta = [w1, w2, b],增广特征 [x1, x2, 1]
theta = np.zeros(3)
m = np.zeros(3)  # 一阶动量
v = np.zeros(3)  # 二阶动量

for t_idx in range(n):
    # 构造增广特征向量
    x_aug = np.array([X[t_idx, 0], X[t_idx, 1], 1.0])
    y_pred = theta @ x_aug
    err = y_pred - Y[t_idx]
    # 梯度 dL/d(theta_j) = 2 * (y_pred - y_true) * x_j
    g = 2 * err * x_aug

    t = t_idx + 1
    # 一阶动量:梯度方向的指数滑动平均
    m = beta1 * m + (1 - beta1) * g
    # 二阶动量:梯度大小的指数滑动平均
    v = beta2 * v + (1 - beta2) * g ** 2

    # 偏差修正:补偿零初始化带来的低估
    m_hat = m / (1 - beta1 ** t)
    v_hat = v / (1 - beta2 ** t)

    # AdamW参数更新:权重衰减直接作用于参数
    theta = theta - alpha * (m_hat / (np.sqrt(v_hat) + eps) + lam * theta)

from decimal import Decimal, ROUND_HALF_EVEN
def fmt(x):
    return format(Decimal(str(x)).quantize(Decimal("0.000001"), rounding=ROUND_HALF_EVEN), 'f')
print(f"{fmt(theta[0])} {fmt(theta[1])} {fmt(theta[2])}", end='')

第二题:大模型推理资源的最低成本分发

在线评测链接;https://www.neituiya.com/oj/16/2523

题目描述

当前只有若干个并发的大模型推理服务,推理资源紧张,但是有 $$N$$ 个推理请求任务在申请推理服务中,每个推理服务都有一个优先级的分值,要求对每个推理请求任务分发推理资源,每个任务至少分配 $$1$$ 千个 token 消耗的推理资源,相邻的两个任务,优先级越高的那个任务会获得更多的 token 数($$X$$ 千个 token 数)。请给每个推理请求任务分发 token,确保 $$N$$ 个请求任务消耗的 token 数最少($$X$$ 千个 token)。

输入描述

输入是一个优先级的数组,数值越大,优先级越高,例如 $$1, 2, 3$$。

如果优先级小于等于 $$0$$ 或者为空,该任务需要被放弃,不分配任务 token,它的相邻任务不与该任务进行优先级比较,只和另一个比较,例如 $$[1, -1, 2]$$ 的 token 总数是 $$2$$ 千个。

输出描述

输出消耗的最少 token 总数(千个 token)。

样例1

输入

10,10,5

输出

4

样例解释

分别给第一、二、三个任务分发 $$1$$ 千、$$1$$ 千、$$2$$ 千个 token。

样例2

输入

4,2,6

输出

5

样例解释

分别给第一、二、三个任务分发 $$2$$ 千、$$1$$ 千、$$2$$ 千个 token。

题解:贪心

题目内容拆解

给 $$N$$ 个有效任务分配 token,相邻任务中优先级更高的必须拿更多,无效任务(优先级 $$\le 0$$ 或为空)直接跳过,求总 token 最少是多少。

核心观察:每个位置只受"左邻"和"右邻"两个约束,两个约束互相独立,可以分两遍分别处理——先从左到右满足左邻约束,再从右到左满足右邻约束,取两遍结果的较大值就同时满足了。

算法实现

预处理分段:无效任务(优先级 $$\le 0$$ 或为空)充当隔离带,把整个序列切成若干连续段。段与段之间互不影响——跨越隔离带的两个任务不是"相邻任务",不需要比较优先级。每段独立计算后累加。

初始化下限:每个有效位置先分配 $$1$$ 个 token,这是题目规定的最低值。后续只会往上加,不会减少。

从左到右:满足"比左邻多"的约束:对每个位置 $$i$$,若 $$p_i > p_{i-1}$$,令

$$tokens[i] = tokens[i-1] + 1$$

这一遍只处理了左侧约束。右边的邻居还没考虑,右侧约束留给下一遍。

从右到左:满足"比右邻多"的约束:对每个位置 $$i$$,若 $$p_i > p_{i+1}$$,令

$$tokens[i] = \max\bigl(tokens[i],\ tokens[i+1] + 1\bigr)$$

这里用 $$\max$$ 而不是直接赋值:第一遍已经正确处理了左侧约束,直接覆盖会把第一遍的结果破坏掉。取较大值的意思是"右侧约束要求至少这么多,但如果左侧约束要求更多,就保留左侧的结果"。两遍结束后,每个位置同时满足左右两侧约束,且是满足条件的最小值。

优先级相等时无约束:题目只要求"优先级更高的获得更多",相等时没有限制,允许 token 降回 $$1$$,这是总量能最小的关键。

时空复杂度分析

  • 时间复杂度:$$O(N)$$,分段和两次遍历各 $$O(N)$$。
  • 空间复杂度:$$O(N)$$,存储每个位置的 token 分配。

Go

// 大模型推理资源的最低成本分发 - 贪心
package main

import (
        "bufio"
        "fmt"
        "os"
        "strconv"
        "strings"
)

func solve(priorities []int) int64 {
        // 无效任务(<=0或空)充当隔离带,把序列切成若干独立段
        var segments [][]int
        var cur []int
        for _, p := range priorities {
                if p > 0 {
                        cur = append(cur, p)
                } else {
                        if len(cur) > 0 {
                                segments = append(segments, cur)
                                cur = nil
                        }
                }
        }
        if len(cur) > 0 {
                segments = append(segments, cur)
        }

        var total int64
        for _, seg := range segments {
                m := len(seg)
                // 初始化:每个位置下限是 1,后续只加不减
                tokens := make([]int, m)
                for i := range tokens {
                        tokens[i] = 1
                }
                // 第一遍(左→右):满足"比左邻多"的约束
                for i := 1; i < m; i++ {
                        if seg[i] > seg[i-1] {
                                tokens[i] = tokens[i-1] + 1
                        }
                }
                // 第二遍(右→左):满足"比右邻多"的约束;tokens[i]<=tokens[i+1] 说明左侧结果不够大,需要补上
                for i := m - 2; i >= 0; i-- {
                        if seg[i] > seg[i+1] && tokens[i] <= tokens[i+1] {
                                tokens[i] = tokens[i+1] + 1
                        }
                }
                for _, t := range tokens {
                        total += int64(t)
                }
        }
        return total
}

func main() {
        reader := bufio.NewReader(os.Stdin)
        // 按逗号分割输入,解析优先级数组
        line, _ := reader.ReadString('\n')
        line = strings.TrimSpace(line)
        parts := strings.Split(line, ",")
        priorities := make([]int, len(parts))
        for i, s := range parts {
                // 空元素(如 1,,2 中间的空)Atoi 返回 0,视为无效任务,正好被分段逻辑切断
                priorities[i], _ = strconv.Atoi(strings.TrimSpace(s))
        }
        fmt.Println(solve(priorities))
}

2026-4-15-国内研发岗

第一题:浏览器地址栏

在线评测链接;https://www.neituiya.com/oj/16/2519

题目描述

AK机正在开发浏览器地址栏功能,支持四种操作:visit(访问网页)、back(返回上一页)、forward(前进到下一页)、print(输出当前地址)。

初始状态

当前页面为 Blank,历史记录中只有 $$1$$ 个 Blank 页面;最多保存 $$max\_history$$ 个历史记录;每次访问新页面时清空前进记录。

操作说明

  1. visit url:当前页面更新为该网页,加入历史记录;若超过 $$max\_history$$,则删除最早记录;清空前进记录;网址 url 为小写字母、数字和点的组合,长度 $$\le 100$$,用例数据均为合法输入。
  2. back:若历史记录至少有两个页面,切换到上一页,原当前页面加入前进记录;否则不做操作。

3) forward:若前进记录不为空,切换到下一页,该页面加入历史记录;否则不做操作。

4) print:输出当前页面地址,若为 Blank 则输出 Blank

输入描述

第一行包含整数 $$n(1 \le n \le 200)$$,表示操作数。

第二行包含整数 $$max\_history(0 < max\_history < 100)$$,表示历史记录最大容量。

接下来 $$n$$ 行,每行一条操作命令。

输出描述

每次 print 操作输出当前地址,若无访问过任何页面则输出 Blank

样例1

输入

7
10
visit a.com
visit b.com
back
visit c.com
print
forward
print

输出

c.com
c.com

样例解释

back 命令后,前进记录为 b.com;后续 visit 命令清空前进记录,因此 forward 命令无操作。

样例2

输入

7
3
visit a.com
visit b.com
visit c.com
visit d.com
back
forward
print

输出

d.com

样例解释

back 后,当前页面为 c.com,再 forward,当前页面为 d.com

样例3

输入

9
3
visit a.com
visit b.com
visit c.com
visit d.com
visit e.com
back
back
back
print

输出

c.com

样例解释

容量为 $$3$$,历史记录中的页面为 $$c.com, d.com, e.com$$,两次 back 后,当前页面为 c.com,再次 back,前面再无页面,因此当前页面为 c.com

样例4

输入

4
10
back
print
forward
print

输出

Blank
Blank

样例解释

初始页面为 Blank,历史记录中再无其他页面,因此 back 不做操作,forward 也不做操作,输出均为 Blank

样例5

输入

4
10
visit abc.com
visit abc.com
back
print

输出

abc.com

样例解释

访问两次相同的页面场景,历史记录中为 $$Blank, abc.com, abc.com$$,因此 back 后,当前页面为 abc.com

题解:双端队列 + 栈模拟

题目内容拆解

模拟浏览器的前进/后退功能,维护一个有容量上限的历史记录和一个前进栈。$$n \le 200$$ 规模很小,直接按规则模拟即可。

核心观察:历史记录是一个受容量限制的队列,旧页面从头部淘汰、新页面追加到尾部,当前页面始终是队尾元素。前进记录是标准栈,back 弹出队尾压入栈,forward 弹出栈顶追加到队尾,visit 同时清空前进栈。

算法实现

双端队列维护历史记录:双端队列(deque)是一种两端都能插入和删除的容器,可以从头部淘汰最老的页面,也能从尾部随时弹出当前页面。初始放入 Blank 作为起始页。

每次 visit 将新页面追加到队尾;若队列长度超过上限则从队首删去最老记录:

$$|history| > max\_history \Rightarrow \text{弹出队首}$$

前进栈维护前进记录back 操作将当前页(队尾)弹出并压入前进栈,相当于"把离开的页面暂存起来等待前进";forward 操作将前进栈顶弹出追加到队尾,恢复刚才离开的页面;visit 清空前进栈,因为浏览了新页面后不应再前进;print 直接读取队尾元素输出。

时空复杂度分析

  • 时间复杂度:$$O(n)$$,每次操作均为 $$O(1)$$。
  • 空间复杂度:$$O(max\_history)$$,历史记录最多存储 $$max\_history$$ 个页面。

第二题:异或树

在线评测链接;https://www.neituiya.com/oj/16/2520

题目描述

老师为孩子们设计了一个使用异或树的游戏。游戏在一棵有 $$n$$ 个节点的树上进行,节点编号从 $$1$$ 到 $$n$$,树的根节点是节点 $$1$$,各节点之间的父子关系可从根节点 $$1$$ 开始基于连边关系进行推导。

每个节点 $$i$$ 有一个初始值 $$init_i$$,其值要么是 $$0$$,要么是 $$1$$。

在游戏过程中,可以对树执行若干次(可能为 $$0$$ 次)操作,具体操作就是选择某个节点 $$x$$。

在选中节点 $$x$$ 之后:节点 $$x$$ 的值会产生翻转(从 $$0$$ 变成 $$1$$ 或者从 $$1$$ 变成 $$0$$);$$x$$ 的子节点值则保持不变;$$x$$ 的孙子节点的值也会翻转;$$x$$ 的曾孙节点的值保持不变;依次逐代类推(即距离节点 $$x$$ 为奇数的后代节点保持不变,距离为偶数的后代节点会跟随翻转)。

游戏的最终目标是使得每个节点 $$i$$ 的值都变为输入的目标值 $$goal_i$$,$$goal_i$$ 也只能是 $$0$$ 或 $$1$$。你需要使用最少的操作次数来达成游戏目标。

输入描述

第一行包含整数 $$n(1 \le n \le 10^5)$$,代表树的节点数。

接下来 $$n-1$$ 行,每行包含两个整数 $$u_i, v_i(1 \le u_i, v_i \le n, u_i \ne v_i)$$,表示节点 $$u_i$$ 和 $$v_i$$ 之间有边连接。

下一行包含 $$n$$ 个整数,第 $$i$$ 个数字对应于节点的初始值 $$init_i$$($$init_i$$ 只能是 $$0$$ 或 $$1$$)。

接下来一行也包含 $$n$$ 个整数,第 $$i$$ 个数字对应于节点的目标值 $$goal_i$$($$goal_i$$ 只能是 $$0$$ 或 $$1$$)。

输出描述

输出一个整数 $$cnt$$,代表执行的最少操作次数。

样例1

输入

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

输出

2

样例解释

共 $$5$$ 个节点,构成链 $$1-2-3-4-5$$。操作节点 $$1$$,影响到其孙子节点 $$3$$ 和曾曾孙节点 $$5$$,使其变为目标值 $$1$$;操作节点 $$2$$,影响到其孙子节点 $$4$$,使其变为目标值 $$1$$。经过 $$2$$ 次操作后所有节点状态值达到目标。

样例2

输入

10
2 1
3 1
4 2
5 1
6 2
7 5
8 6
9 8
10 5
1 0 1 1 0 1 0 1 0 1
1 0 1 0 0 1 1 1 1 0

输出

2

样例解释

共 $$10$$ 个节点。节点 $$4$$ 的初始状态为 $$1$$,目标为 $$0$$,需要执行一次操作,无子节点对其他节点状态不会造成影响;节点 $$7$$ 的初始状态为 $$0$$,目标为 $$1$$,需要执行一次操作,无子节点对其他节点状态不会造成影响。经过 $$2$$ 次操作后所有节点状态值达到目标。

第三题:实现一个窗口系统

在线评测链接;https://www.neituiya.com/oj/16/2521

题目描述

实现一个简单的窗口系统。首先初始化一个给定宽高的屏幕,并建立图像坐标系,以屏幕左上角 $$(0, 0)$$ 为坐标原点。

窗口系统可以容纳窗口,窗口有以下属性:窗口名、窗口宽高、窗口左上角坐标、窗口层级。

支持的操作:创建窗口、移除窗口、resize(调整窗口大小)、move(移动窗口)、给定一个区域查询所有的可见窗口、查询单个窗口的可见性。

窗口遮挡与可见性规则

  1. 层级大的窗口可以遮盖层级小的窗口
  2. 层级相同的窗口中,后创建的窗口可以覆盖先创建的窗口;窗口的 resizemove 操作不影响窗口创建的先后顺序

3) 窗口只要没有被完全覆盖,就算做可见

4) 窗口可以在屏幕外创建,或者被 move/resize 到屏幕外,完全处于屏幕外的窗口不可见

需要实现的方法

  1. init:给定屏幕宽高,初始化屏幕。校验屏幕宽高是否均为正整数,满足返回 true;反之返回 false
  2. createWindow:给定窗口名、窗口左上角坐标、窗口宽高、窗口层级,创建窗口。校验窗口宽高是否均为正整数,窗口名是否未被使用过,若满足则执行操作并返回 true,反之返回 false

3) removeWindow:给定窗口名移除一个指定的窗口。移除成功返回 true;窗口未创建无法执行移除操作返回 false

4) resize:给定一个窗口名和新的宽高,修改指定窗口的宽高。校验窗口是否已经创建以及新的窗口宽高是否为正整数,满足则执行操作并返回 true,否则返回 false

  1. move:给定一个窗口名和一个新的左上角坐标,修改指定窗口的位置。校验窗口是否已经创建,满足则执行操作并返回 true,否则返回 false
  2. queryVisibility:给定一个窗口名,查询指定窗口的可见性,可见返回 true。窗口未创建或者窗口不可见返回 false

7) queryAllVisibleWindows:给定一个在屏幕范围内的矩形区域(左上角坐标和宽高),查询该区域内所有的可见窗口。按照窗口层级的降序排序,窗口层级相同的则以窗口名的字典序升序排序。返回排序后的可见窗口名以 ; 分割;若无可见窗口则返回 NoVisibleWindow

输入描述

一系列窗口操作,整体操作数不超过 $$100$$ 个,第一个操作均为 init 方法,用于初始化屏幕。如果屏幕没有创建成功,不会有后续操作。

输出描述

对应操作的返回值。

样例1

输入

init 200 300
createWindow window1 10 10 100 100 1
createWindow window2 20 20 40 30 2
createWindow window3 70 90 50 3
removeWindow window2
removeWindow window4
queryVisibility window1
queryAllVisibleWindows 10 10 100 100

输出

true
true
true
true
true
false
true
window3;window1

样例解释

输入为一系列窗口系统的操作及对应参数,输出为每个操作的返回值。

样例2

输入

init 100 100
createWindow win1 0 0 50 50 1
createWindow win2 0 0 50 50 2
createWindow win3 0 0 50 50 3
queryVisibility win1
queryVisibility win2
queryVisibility win3

输出

true
true
true
true
false
false
true

样例解释

三个窗口完全重叠,层级最高的 win3 完全遮盖了 win1win2,只有 win3 可见。

2026-4-8-算法岗

第一题:路由器资源用量预测

在线评测链接:https://www.neituiya.com/oj/16/2466

题目描述

路由器的某资源利用率与多个运行特征强相关:协议连接数(单位:个)、转发数据包速率(单位:Mpps)、内存占用率(单位:%)。为了精准预测不同负载下的路由器资源利用率,保障网络稳定运行,请实现批量梯度下降法(BGD)来训练资源预测线性回归模型的参数。

  1. 资源预测模型:$$y = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 + w_3 \cdot x_3$$($$w_0$$ 为偏置项,$$w_1$$,$$w_2$$,$$w_3$$ 为特征权重)
  2. 损失函数:均方误差(MSE),$$L = \frac{1}{2m} \sum_{i=1}^{m} (y_{\text{pred},i} - y_{\text{true},i})^2$$($$m$$ 为样本数)

3) 梯度更新规则:$$w_j = w_j - \alpha \cdot \frac{1}{m} \sum_{i=1}^{m} (y_{\text{pred},i} - y_{\text{true},i}) \cdot x_{i,j}$$(偏置项 $$w_0$$ 对应 $$x_{i,0}=1$$,$$\alpha$$ 为学习率)

4) 迭代规则:初始权重(含偏置)全为0,迭代固定 $$N$$ 次后停止,无需判断收敛

  1. 为了提高收敛速度,采用特征归一化进行训练,并在训练完成后进行权重还原。特征归一化:对每个特征维度 $$x_j(j=1,2,3)$$,$$x_j^{\text{norm}} = \frac{x_j - \min(x_j)}{\max(x_j) - \min(x_j)}$$,其中 $$\min(x_j)$$ 为该特征在所有样本中的最小值,$$\max(x_j)$$ 为该特征在所有样本中的最大值,若 $$\max(x_j)=\min(x_j)$$(特征无波动),则归一化后的值为0。特征权重还原:$$w_j = \frac{w_j^{\text{norm}}}{\max(x_j) - \min(x_j)}(j=1,2,3)$$,$$w_j^{\text{norm}}$$ 为迭代后的权重,若 $$\max(x_j) - \min(x_j)$$ 为0,$$w_j$$ 取0。偏置项权重还原:$$w_0 = w_0^{\text{norm}} - \sum_{j=1}^{3} w_j \cdot \min(x_j)$$,$$w_0^{\text{norm}}$$ 为迭代后偏置项的权重。

输入描述

第一行一个整数 $$m(1 \le m \le 10000)$$,表示样本数量。

第二行一个整数 $$N(1 \le N \le 1000)$$,表示迭代次数。

第三行一个浮点数 $$\alpha(0 \le \alpha \le 1)$$,表示学习率,保留2位小数。

接下来 $$m$$ 行,每行4个整数 $$x_1, x_2, x_3, y(0 \le x_1 \le 1000, 0 \le x_2 \le 10000, 0 \le x_3 \le 100, 0 \le y \le 10000)$$,依次为协议连接数、转发数据包速率、内存占用率、资源用量。

输出描述

一行,4个浮点数,依次为还原后的 $$w_0, w_1, w_2, w_3$$,结果保留2位小数,银行家舍入,以一个空格分隔,前后无冗余空格。

样例1

输入

3
100
0.10
100 200 150 6000
200 800 600 7500
300 70 60 6500

输出

4394.59 6.82 1.20 1.55

样例解释

样本数 $$3$$,迭代次数 $$100$$,学习率 $$\alpha=0.10$$。三个样本依次为 $$(100, 200, 150, 6000)$$、$$(200, 800, 600, 7500)$$、$$(300, 70, 60, 6500)$$。

样例2

输入

2
50
0.05
0 0 0 0
1000 10000 100 100000

输出

11419.33 28.26 2.83 282.62

样例解释

样本数 $$2$$,迭代次数 $$50$$,学习率 $$\alpha=0.05$$。两个样本依次为 $$(0, 0, 0, 0)$$、$$(1000, 10000, 100, 100000)$$。

题解

题目内容拆解

给一堆"特征→标签"的训练样本,要按固定规则拟合一条四维直线 $$y = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_3$$。这里的 $$w_0$$ 就是初中数学里 $$y = kx + b$$ 的那个 $$b$$,叫"截距"或"偏置",代表所有特征都为 $$0$$ 时 $$y$$ 的基准值;$$w_1, w_2, w_3$$ 是三个"斜率",分别告诉我们每增加一单位的该特征,$$y$$ 会变化多少。

算法实现

线性回归和梯度下降的直觉:我们想找一组 $$w$$,让所有样本上"预测值与真实值的差"整体最小。衡量"整体有多差"的方法就是均方误差 $$L$$,它把每条样本的误差平方再取平均,差越小说明拟合越好。直接解这道方程要用线性代数技巧,这里采用更朴素的做法:从全 $$0$$ 开始,每一步都沿着"让 $$L$$ 变小最快的方向"挪一小步,挪 $$N$$ 次停下。"最快变小的方向"就是 $$L$$ 对每个 $$w_j$$ 的偏导数的反方向,它的数学表达恰好是题目给的梯度公式。步长由学习率 $$\alpha$$ 控制:太大容易震荡甚至发散,太小收敛得慢。这种每一步都用全部样本算梯度的做法,就是题目里说的"批量(Batch)梯度下降"。

为什么要归一化:观察一下数据范围,$$x_1 \in [0, 1000]$$、$$x_2 \in [0, 10000]$$、$$x_3 \in [0, 100]$$,三个特征的数量级差几十到上百倍。如果直接把原始特征送进梯度下降,$$x_2$$ 维度的梯度会远大于其他两维,同一个 $$\alpha$$ 对 $$x_2$$ 可能太大(发散)、对 $$x_3$$ 又太小(爬不动),训练会非常不稳定。min-max 归一化把每一维都线性映射到 $$[0, 1]$$,三维尺度立刻统一,$$\alpha$$ 好选、收敛也快。没有波动的维度按题意归 $$0$$(本身就提供不了任何预测信息)。

训练完要还原权重:训练是在"缩放后的特征"上进行的,学出来的 $$w^{\text{norm}}$$ 是针对归一化特征的系数。把 $$x_j^{\text{norm}} = (x_j - \min_j) / \Delta_j$$ 代回线性式并把常数项合并到偏置里,就能推出原始特征空间的系数:$$w_j = w_j^{\text{norm}} / \Delta_j$$,$$w_0 = w_0^{\text{norm}} - \sum_j w_j \cdot \min_j$$。直觉上:归一化把 $$x$$ 轴压缩了 $$\Delta_j$$ 倍,斜率反过来要除以 $$\Delta_j$$ 才能还原;平移的部分则由偏置吸收。

向量化写法:如果每轮用 Python 的 for 循环遍历 $$m$$ 个样本、$$4$$ 个权重挨个算,当 $$m = 10^4$$、$$N = 10^3$$ 时就是 $$4 \times 10^7$$ 次 Python 层加法,慢得离谱。NumPy 的做法是把 $$m$$ 个样本堆成矩阵 $$\tilde X$$(在原特征前拼一列全 $$1$$ 对应偏置),那么"每个样本的预测"就是一次矩阵-向量乘法 $$\tilde X w$$,"梯度"也是一次矩阵-向量乘法 $$\tilde X^{\top} e / m$$,整轮迭代只剩三行 NumPy 代码,底层 C 实现比 Python 循环快两到三个数量级。

时空复杂度分析

  • 时间复杂度:$$O(N \cdot m \cdot d)$$,其中 $$d = 3$$ 是特征维数。每轮迭代的成本由两次形状为 $$(m, 4)$$ 的矩阵-向量乘决定,均为 $$O(md)$$,总共 $$N$$ 轮。
  • 空间复杂度:$$O(md)$$,用于存放归一化后的增广特征矩阵;梯度向量与权重向量只占 $$O(d)$$。

Python

# 路由器资源用量预测 - NumPy 批量梯度下降
import sys
import numpy as np


def normalize(X):
    """逐维 min-max 归一化,一次广播搞定所有维度"""
    mins = X.min(axis=0)                    # shape=(3,)
    maxs = X.max(axis=0)                    # shape=(3,)
    ranges = maxs - mins                    # shape=(3,)
    # 避免除零:无波动的维度用 1 代替,后面再把对应列置 0
    safe = np.where(ranges == 0, 1.0, ranges)
    Xn = (X - mins) / safe                  # shape=(m, 3)
    Xn[:, ranges == 0] = 0                  # 无波动维度按题意归 0
    return Xn, mins, ranges


def bgd_train(Xn, y, N, alpha):
    """在增广归一化矩阵上运行 N 轮批量梯度下降"""
    m = Xn.shape[0]
    # 首列全 1 对应偏置项 x0,使权重和特征可以用一次矩阵乘搞定
    Xa = np.hstack([np.ones((m, 1)), Xn])   # shape=(m, 4)
    w = np.zeros(Xa.shape[1])               # shape=(4,)
    for _ in range(N):
        # 一次矩阵乘算出所有预测,再一次矩阵乘算出所有梯度
        err = Xa @ w - y                    # shape=(m,)
        grad = (Xa.T @ err) / m             # shape=(4,)
        w = w - alpha * grad
    return w


def restore(w_norm, mins, ranges):
    """把归一化空间的权重还原到原始特征空间"""
    w = np.zeros(4)
    # 非零维度除以区间长度得到真实斜率,零维度按题意直接取 0
    safe = np.where(ranges == 0, 1.0, ranges)
    w[1:] = np.where(ranges == 0, 0.0, w_norm[1:] / safe)
    # 偏置项扣除归一化引入的常数平移
    w[0] = w_norm[0] - (w[1:] * mins).sum()
    return w


def solve():
    data = sys.stdin.read().split()
    m = int(data[0]); N = int(data[1]); alpha = float(data[2])
    # 剩余数据一次读完 reshape 成 (m, 4),前 3 列是特征,第 4 列是标签
    arr = np.array(data[3:3 + 4 * m], dtype=float).reshape(m, 4)
    X = arr[:, :3]                          # shape=(m, 3)
    y = arr[:, 3]                           # shape=(m,)
    Xn, mins, ranges = normalize(X)
    w_norm = bgd_train(Xn, y, N, alpha)
    w = restore(w_norm, mins, ranges)
    # Python 默认的 format 做两位小数银行家舍入,符合题意
    print(f"{w[0]:.2f} {w[1]:.2f} {w[2]:.2f} {w[3]:.2f}", end='')


solve()

第二题:快递员极速配送挑战

在线评测链接:https://www.neituiya.com/oj/16/2467

题目描述

某快递员负责一个片区的快递配送业务。假设他手头有 $$N$$ 个快递包裹需要派送,每个包裹对应一个具体的收货坐标 $$(x_i, y_i)$$(单位:公里)。为了提高效率,公司要求快递员先利用聚类算法将这 $$N$$ 个包裹自动划分为 $$K$$ 个簇(代表 $$K$$ 个社区),快递员只需要将快递送到社区中心(类的中心点)即可。快递员从起始位置出发,按照每个社区中心与起点之间的距离由近到远排序,依次送完所有社区的快递,最后返回起始位置。已知快递员的平均行驶速度为 $$\text{speed}$$ km/h,快递员初始坐标为 $$(0, 0)$$。请编写程序,计算完成所有配送并返回起点所需的总时间(单位:秒,向下取整)。

K-Means 聚类计算步骤如下。种子点初始化:将所有点按到起点的距离从小到大排序,如果距离相同的点,按照输入坐标点的先后顺序从小到大排序;选择排序后的前 $$K$$ 个点作为初始聚类中心。迭代优化:将每个点 $$p_i$$ 分配到距离最近的聚类中心 $$c_k$$,即 $$label_i = \arg\min_k dist(p_i, c_k)$$;重新计算每个聚类的中心点($$n_k$$ 表示分配到第 $$k$$ 个聚类中心的坐标点个数),移动聚类中心 $$c_k = \left(\frac{1}{n_k}\sum_{i}^{n_k} x_i, \frac{1}{n_k}\sum_{i}^{n_k} y_i\right)$$。收敛判断:如果所有聚类中心的移动距离之和小于 $$10^{-4}$$ 则停止迭代;如果达到了预设的最大迭代轮次也停止迭代;否则返回第二步继续进行迭代优化。

使用欧氏距离 $$dist = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}$$,终止条件 max_iters = 50tol = 1e-4

输入描述

第一行 $$3$$ 个整数 $$K, N, \text{speed}(1 \le K \le 10, 1 \le N \le 100, 1 \le \text{speed} \le 100)$$,分别为社区个数、快递包裹总数、快递员平均行驶速度(km/h)。

接下来 $$N$$ 行,每行 $$2$$ 个浮点数 $$x, y$$,表示一个包裹的坐标(单位:公里)。

输出描述

一行一个整数,表示快递员送完所有快递所需的时间,向下取整,单位为秒。

样例1

输入

3 10 30
1.2 1.5
1.8 1.2
5.0 5.2
5.5 4.8
4.9 5.5
-2.0 3.0
-2.5 3.5
-1.8 2.8
1.5 1.8
5.2 5.0

输出

2502

样例解释

$$3$$ 个社区、$$10$$ 个快递包、速度 $$30$$ km/h。聚类后的 $$3$$ 个聚类中心按到起点距离由近到远分别为 $$(1.5, 1.5)$$、$$(-2.1, 3.1)$$、$$(5.15, 5.125)$$,配送总时间约为 $$2502$$ 秒。

样例2

输入

5 3 10
1.00 1.00
2.00 2.00
3.00 3.00

输出

3054

样例解释

$$K=5 > N=3$$,$$3$$ 个包裹本身分别构成 $$3$$ 个聚类中心。按距离起点由近到远配送路径为 $$(0, 0) \to (1, 1) \to (2, 2) \to (3, 3) \to (0, 0)$$,总距离 $$6\sqrt{2}$$ km,速度 $$10$$ km/h,总耗时 $$3600 \times 6\sqrt{2} / 10 \approx 3054.70$$ 秒,向下取整为 $$3054$$。

题解

题目内容拆解

快递员不想一个一个跑 $$N$$ 个包裹点,而是要先把这些点分成 $$K$$ 小组,每组派一个"代表点"(社区中心),再按"离家由近到远"的顺序一个代表一个代表地跑,最后回家。求总耗时。两件事:一是按题目规定的 K-Means 规则把代表点算出来,二是走一条"原点→最近代表→次近代表→…→最远代表→原点"的折线算距离。

算法实现

K-Means 是什么:先抛开术语,想象你要在一张城市地图上给 $$N$$ 个快递点划 $$K$$ 个"社区"。一个自然的做法是:先随便挑 $$K$$ 个点当"社区代表",然后让每个快递点"投奔"离它最近的那位代表,投奔完后每个代表挪到自己小组所有成员的重心位置(这样它离组员们的平均距离最小),再让所有点重新投奔一次……反复迭代直到代表们几乎不再挪动为止。这就是 K-Means:它不保证全局最优,但每一轮都让"点到所在代表的距离平方和"变小,很快就稳定下来。题目为了让答案唯一,把两个通常"随便挑"的地方都钉死了:种子代表必须按"到原点距离升序 + 输入序"选前 $$K$$ 个;迭代上限 $$50$$ 轮,或者所有代表的总移动量跌破 $$10^{-4}$$ 就停。

$$K > N$$ 怎么办:这种情况下点比代表还少,让每个点自己当一个代表即可,等价于把有效簇数取 $$\min(K, N)$$,迭代里连移动都不会发生。

NumPy 向量化分配:最朴素的分配是两层 for 循环(每个点 × 每个代表)算距离、取最近。用 NumPy 的广播可以一次算完所有距离:点集形状是 $$(N, 2)$$,代表集形状是 $$(K, 2)$$,中间插一个维度变成 $$(N, 1, 2)$$ 和 $$(1, K, 2)$$,相减得到 $$(N, K, 2)$$,平方求和得到 $$(N, K)$$ 的平方距离表。对每一行取 argmin 就是该点归属的代表编号。重算代表时用 np.add.at 按编号把点坐标累加进对应的簇,再除以簇大小就是新的重心位置;空簇(没人投奔它)保留原位置避免除零。

配送距离计算:聚类收敛后得到最终的 $$K$$ 个代表,配送顺序不是聚类顺序,必须再按"每个代表到原点的距离"重新排序一次。把起点、排好序的代表、终点(起点)按顺序堆成一个 $$(K+2, 2)$$ 的路径数组,相邻两行作差取模长就是每一段的距离,所有段相加就是总公里数。最后总秒数为 $$\lfloor 3600 \cdot \text{dist} / \text{speed} \rfloor$$。

时空复杂度分析

  • 时间复杂度:$$O(T \cdot N \cdot K)$$,其中 $$T \le 50$$ 是最大迭代轮数。每轮的广播距离表是 $$O(NK)$$ 的操作,$$N, K$$ 分别不超过 $$100$$ 和 $$10$$,整体远低于上界。
  • 空间复杂度:$$O(NK)$$,主要来自广播距离表;点集与代表集本身只占 $$O(N + K)$$。

类似题目

【华为AI岗】2026-3-4-第一题-网络流量分析

【华为AI岗】2025-11-19-第一题-终端款型聚类识别

Python

# 快递员极速配送挑战 - NumPy 向量化 K-Means
import sys
import numpy as np


def kmeans(points, K):
    """对所有点做 K-Means 聚类,返回最终簇心矩阵 shape=(Ke, 2)"""
    N = points.shape[0]
    # 种子点:按到原点距离升序,相同距离时保持输入顺序(np.argsort 默认稳定)
    dist_to_origin = (points ** 2).sum(axis=1)          # shape=(N,)
    order = np.argsort(dist_to_origin, kind='stable')   # shape=(N,)
    Ke = min(K, N)                                      # K 大于 N 时每个点自成一簇
    centers = points[order[:Ke]].astype(float)          # shape=(Ke, 2)
    for _ in range(50):
        # 广播求每个点到每个中心的平方距离:(N,1,2)-(1,Ke,2) -> (N,Ke,2) -> (N,Ke)
        diff = points[:, None, :] - centers[None, :, :]
        d2 = (diff ** 2).sum(axis=2)                    # shape=(N, Ke)
        labels = d2.argmin(axis=1)                      # shape=(N,)
        # 按簇号做加法聚合:sum_xy[k] 是第 k 簇所有点坐标和
        sum_xy = np.zeros_like(centers)                 # shape=(Ke, 2)
        np.add.at(sum_xy, labels, points)
        cnt = np.bincount(labels, minlength=Ke)         # shape=(Ke,)
        # 只对非空簇求均值,空簇保留原中心避免除零
        new_centers = centers.copy()
        mask = cnt > 0
        new_centers[mask] = sum_xy[mask] / cnt[mask, None]
        # 收敛判断:所有中心的移动距离之和小于 tol 即停止
        move = np.linalg.norm(new_centers - centers, axis=1).sum()
        centers = new_centers
        if move < 1e-4:
            break
    return centers


def delivery_time(centers, speed):
    """按到起点距离由近到远串起所有簇心,返回向下取整后的秒数"""
    # 配送顺序由"簇心到原点的距离"决定,注意不等于聚类顺序
    order = np.argsort((centers ** 2).sum(axis=1), kind='stable')
    path = np.vstack([[0.0, 0.0], centers[order], [0.0, 0.0]])  # shape=(Ke+2, 2)
    # 相邻两行作差求欧氏距离,再求和
    segs = np.linalg.norm(np.diff(path, axis=0), axis=1)         # shape=(Ke+1,)
    total = segs.sum()
    return int(3600.0 * total / speed)


def solve():
    data = sys.stdin.read().split()
    K = int(data[0]); N = int(data[1]); speed = int(data[2])
    # 一次读完所有坐标,reshape 成 (N, 2)
    pts = np.array(data[3:3 + 2 * N], dtype=float).reshape(N, 2)
    centers = kmeans(pts, K)
    print(delivery_time(centers, speed), end='')


solve()

2026-4-8-研发岗

第一题:包裹的编码

在线评测链接:https://www.neituiya.com/oj/16/2463

题目描述

某快递公司的包裹编号系统采用混合进制编码,以高效分配和分拣包裹。每个包裹的编号由多个部分组成,每个部分对应不同的分层层级,且使用不同的进制表示。

给定一个十进制整数 $$N$$(可为负数)和一组进制 $$bases$$(从高位到低位的顺序),进行符号位和混合进制编码的计算,最后按照要求输出转换后的包裹编码字符串,具体规则如下。

符号位:若 $$N$$ 为负数,编码的第一个元素为 $$1$$,其余部分为 $$|N|$$($$N$$ 的绝对值)的混合进制编码;若 $$N$$ 为非负数,编码的第一个元素为 $$0$$,其余部分为 $$N$$ 的混合进制编码。

混合进制编码:按输入的 $$bases$$ 顺序,从高位到低位依次处理各个层级。计算方式为:上一级的商除以本级 $$base$$ 的余数作为本级的编码值,第一层级用 $$|N|$$ 作为初始商。例如 $$N=101$$,$$bases=[5,7,3]$$,计算过程为:$$101\div 5=20$$ 余 $$1$$;$$20\div 7=2$$ 余 $$6$$;$$2\div 3=0$$ 余 $$2$$;编码为 $$[1,6,2]$$,加上符号位为 $$[0,1,6,2]$$。

最终输出需要将编码转换为 $$a\sim z$$ 的字符串,并进行回文检测:编码 $$0$$ 转换为字母 a,编码 $$1$$ 转换为字母 b,依次类推,编码 $$25$$ 转换为字母 z;如果最终字符串是回文串,则在字符串后添加 (palindrome);如果不是回文串,则输出最长的回文子串,若最长回文子串存在多个则按照字典序最小的输出。

输入描述

第一行包含一个整数 $$N(-10^6 \le N \le 10^6)$$。

第二行包含 $$m$$ 个整数,表示进制序列 $$bases(1 \le m \le 10, 2 \le base \le 26)$$。

输出描述

输出一行字符串,表示转换后的包裹编码结果。

样例1

输入

-21
5 7 3

输出

bb

样例解释

混合进制编码结果为 $$[1,1,4,0]$$,转为字符串为 bbea。由于 bbea 不是回文串,所以输出最长回文子串 bb

样例2

输入

0
5 7 3

输出

aaaa (palindrome)

样例解释

混合进制编码结果为 $$[0,0,0,0]$$,转为字符串为 aaaa。由于 aaaa 是回文串,在输出字符串后添加 (palindrome),最终结果为 aaaa (palindrome)

第二题:大模型性能优化

在线评测链接:https://www.neituiya.com/oj/16/2464

题目描述

现在有一个模型,它由 $$n$$ 个模块组成,每个模块有不同的耗时,各个模块串行执行,模型的总耗时等于各模块耗时的累加。为了提升模型性能,我们需要对模块进行优化。每次优化可以将模块耗时减半(向上取整),但每个模块都有其耗时下限,优化后的耗时不会低于该下限。

给定 $$n$$ 个模块,每个模块的初始耗时为 $$a_i$$,耗时下限为 $$b_i$$(保证 $$b_i \le a_i$$)。现有 $$m$$ 天时间,每天可以选择一个模块进行一次优化(同一模块可以优化多次)。请合理安排优化策略,使得 $$m$$ 天后所有模块的总耗时最小。

优化规则:如果一个模块当前耗时为 $$t$$,优化一次后耗时变为 $$\lceil t/2 \rceil$$,且优化后的耗时不能低于该模块的下限 $$b_i$$;若模块当前耗时已等于下限 $$b_i$$,则无法继续优化该模块。

输入描述

第一行包含两个整数 $$n, m(1 \le n \le 10^3, 0 \le m \le 10^3)$$,分别表示模块数量和优化天数。

第二行包含 $$n$$ 个整数,表示每个模块的初始耗时 $$a_1, a_2, \ldots, a_n(1 \le a_i \le 10^5)$$。

第三行包含 $$n$$ 个整数,表示每个模块的耗时下限 $$b_1, b_2, \ldots, b_n(1 \le b_i \le a_i)$$。

输出描述

输出一个整数,表示 $$m$$ 天后所有模块的最小总耗时。

样例1

输入

2 0
50 100
10 10

输出

150

样例解释

$$m=0$$,没有优化机会,总耗时为 $$50 + 100 = 150$$。

样例2

输入

2 3
100 80
40 10

输出

70

样例解释

第一天优化模块 $$1$$,耗时从 $$100$$ 变为 $$50$$;第二天优化模块 $$2$$,耗时从 $$80$$ 变为 $$40$$;第三天优化模块 $$2$$,耗时从 $$40$$ 变为 $$20$$;最终总耗时为 $$50 + 20 = 70$$。

第三题:分布式任务调度

在线评测链接:https://www.neituiya.com/oj/16/2465

题目描述

有 $$N$$ 个任务,需要把这些任务调度到服务器上运行。每个任务有 $$CPU$$ 需求 $$c_i$$、内存需求 $$m_i$$、任务价值 $$v_i$$。服务器 $$CPU$$ 规格为 $$C$$,内存规格为 $$M$$,一台服务器可以运行多个任务并获得它们的总价值,条件是这些任务的 $$CPU$$ 需求之和不超过 $$C$$,且内存需求之和不超过 $$M$$。一个任务只能运行在一台服务器上。

我们想知道,当服务器数量为 $$1$$ 到 $$N$$ 的每一种情况,其运行任务的最大总价值是多少,以帮助决策需要购买多少台服务器。换句话说,对于 $$K=1,2,\ldots,N$$,假设有 $$K$$ 台服务器,分别输出可运行任务的最大总价值。

输入描述

第一行包含三个整数 $$N, C, M(1 \le N \le 15, 1 \le C, M \le 10^6)$$,分别表示任务数量、服务器 $$CPU$$ 规格、服务器内存规格。

接下来 $$N$$ 行,每行包含三个整数 $$c_i, m_i, v_i(1 \le c_i, m_i, v_i \le 10^6)$$,表示每个任务的 $$CPU$$ 需求、内存需求和任务价值。

输出描述

输出 $$N$$ 行,每行一个整数,第 $$i$$ 行表示服务器数量 $$K=i$$ 时可运行任务的最大总价值。

样例1

输入

3 3 10
1 4 1
2 5 2
2 6 4

输出

5
7
7

样例解释

只有 $$1$$ 台服务器时,选择运行任务 $$1$$ 和任务 $$3$$,$$CPU$$ 需求和为 $$3$$,内存需求和为 $$10$$,总价值为 $$5$$。有 $$2$$ 台服务器时,第一台运行任务 $$1$$ 和任务 $$3$$,第二台运行任务 $$2$$,总价值为 $$7$$。有 $$3$$ 台服务器时,两台已够用,总价值仍为 $$7$$。

样例2

输入

1 5 5
10 10 100

输出

0

样例解释

只有 $$1$$ 个任务且 $$CPU$$、内存需求都超出服务器规格,任务无法运行,总价值为 $$0$$。

2026-3-18-AI岗

第一题:大模型训练显存优化算法

在线评测链接:https://www.neituiya.com/oj/16/2349

题目描述

在大模型训练中,为解决 NPU 显存不足的问题,通常采用张量 swap 或者张量重计算的方式进行优化。

张量 swap 是把张量的数据先搬到 CPU,需要的时候再从 CPU 搬回 NPU;

张量重计算是在需要张量的值时,在 NPU 上重新把张量的值计算出来。

假设把模型部署在 NPU 上,还需要 $$m$$ 大小的存储空间才能让大模型运行起来。

目前有 $$n$$ 个候选张量,每个张量占用一定的存储空间。

$$n$$ 个张量中的每个张量都可以进行 swap 或者重计算,swap 和重计算分别对应不同的代价。

试着编写一段程序,从 $$n$$ 个候选张量中选择合适的张量进行 swap 或者重计算,

在代价最小的情况下,使得大模型能够运行起来(选择的张量总大小大于等于 $$m$$)。

输入描述

第一行为一个整数 $$m(0 < m < 10000)$$,表示需要的存储空间大小。

第二行为一个整数 $$n(0 < n < 10000)$$,表示候选张量的个数。

第三行有 $$n$$ 个整数,表示 $$n$$ 个候选张量的存储空间大小(值在 $$[1, 100000]$$ 之内)。

第四行有 $$n$$ 个整数,表示 $$n$$ 个候选张量 swap 的代价(值在 $$[1, 100000]$$ 之内)。

第五行有 $$n$$ 个整数,表示 $$n$$ 个候选张量重计算的代价(值在 $$[1, 100000]$$ 之内)。

输出描述

如果没有找到合适的解,输出 error;否则输出最小代价。

样例1

输入

10
5
3 4 5 6 7
1 2 3 5 5
2 3 4 5 6

输出

6

样例解释

需要的存储空间是 $$10$$。候选张量大小分别为 $$3, 4, 5, 6, 7$$。

选择张量 $$3$$ 和 $$7$$(总大小 $$10 \ge 10$$),$$3$$ 的最小代价是 $$\min(1, 2) = 1$$,

$$7$$ 的最小代价是 $$\min(5, 6) = 5$$,总代价 $$= 6$$。

样例2

输入

12
4
18 20 10 12
12 3 13 4
10 14 10 1

输出

1

样例解释

张量 $$12$$ 单独即可满足 $$\ge 12$$,其重计算代价为 $$1$$,是最小代价。

题解:反向背包 DP

题目问题拆解

每个张量取 $$\min(\text{swap代价}, \text{重计算代价})$$ 作为有效代价。

从 $$n$$ 个张量中选子集使总大小 $$\ge m$$,最小化总代价。

本质是反向 0/1 背包。$$n, m < 10000$$,$$O(nm)$$ 可通过。

核心观察:标准 0/1 背包求"恰好装满"的最大价值,

而本题求"至少释放 $$m$$ 空间"的最小代价——方向相反但转化简单。

算法实现

先做预处理:每个张量只会选 swap 或重计算中代价更小的那个,

因此第 $$i$$ 个张量的有效代价 $$c_i = \min(\text{swap}_i, \text{recompute}_i)$$,有效大小 $$w_i$$ 不变。

问题转化为:从 $$n$$ 个物品中选若干个,总重量 $$\ge m$$,最小化总代价。

这和经典 0/1 背包的区别在于:经典背包是"总重量 $$\le$$ 容量,最大化价值",

本题是"总重量 $$\ge$$ 目标,最小化代价"。处理方式是把 DP 数组的含义"反过来"。

状态方程定义

$$f[j]$$ 表示释放至少 $$j$$ 空间的最小总代价($$0 \le j \le m$$)。

状态方程初始化

$$f[0] = 0$$(不需要释放任何空间,代价为 $$0$$)。$$f[j] = +\infty$$($$j > 0$$,初始无方案可达)。

状态方程转移

对每个张量 $$i$$(大小 $$w_i$$,代价 $$c_i$$),倒序枚举 $$j$$ 从 $$m$$ 到 $$0$$(倒序保证每个张量最多选一次):

$$f[j] = \min(f[j], \ f[\max(0, j - w_i)] + c_i)$$

这里 $$\max(0, j - w_i)$$ 是关键:如果 $$w_i \ge j$$(这个张量单独就够了),

那么选它之前只需释放 $$0$$ 空间,代价从 $$f[0] = 0$$ 转移过来;如果 $$w_i < j$$,还需要其他张量补齐剩余的 $$j - w_i$$ 空间。

最终答案为 $$f[m]$$。若 $$f[m]$$ 仍为 $$+\infty$$,说明所有张量总大小不够 $$m$$,输出 error

以样例1为例手算:$$m = 10$$,有效代价 $$c = [1, 2, 3, 5, 5]$$,大小 $$w = [3, 4, 5, 6, 7]$$。

处理张量1($$w=3, c=1$$):$$f[3] = \min(+\infty, f[0]+1) = 1$$。

处理张量2($$w=4, c=2$$):$$f[7] = \min(+\infty, f[3]+2) = 3,f[4] = \min(+\infty, f[0]+2) = 2$$。

处理张量5($$w=7, c=5$$):$$f[10] = \min(+\infty, f[3]+5) = 6,f[7] = \min(3, f[0]+5) = 3$$。

最终 $$f[10] = 6$$,对应选张量1和5(大小 $$3+7=10$$,代价 $$1+5=6$$)。

时空复杂度分析

时间复杂度:$$O(nm)$$,外层 $$n$$ 个张量,内层枚举 $$m$$ 个状态。

空间复杂度:$$O(m)$$,一维 DP 数组。

类似题目

【华为研发岗】2025-10-29-第二题-新能源汽车最高总续航里程

【华为算法岗】2025-12-17-第二题-模型量化最小误差

Python

# 显存优化 - 反向背包DP

def solve():
    m = int(input())
    n = int(input())
    sizes = list(map(int, input().split()))
    swap_costs = list(map(int, input().split()))
    recompute_costs = list(map(int, input().split()))

    # 每个张量取 min(swap, recompute) 作为有效代价
    costs = [min(swap_costs[i], recompute_costs[i]) for i in range(n)]

    # 总大小不够 → 无解
    if sum(sizes) < m:
        print("error")
        return

    # 反向背包:f[j] = 释放至少 j 空间的最小代价
    INF = float('inf')
    f = [INF] * (m + 1)
    f[0] = 0

    for i in range(n):
        w, c = sizes[i], costs[i]
        for j in range(m, -1, -1):
            # 选第i个张量后,j空间的需求降为 max(0, j-w)
            prev = max(0, j - w)
            if f[prev] + c < f[j]:
                f[j] = f[prev] + c

    print(f[m] if f[m] < INF else "error")

solve()

第二题:基于KNN的语音数据分类

在线评测链接:https://www.neituiya.com/oj/16/2350

题目描述

在终端穿戴场景中,涉及到用户和终端设备之间的语音交互,

设备在收到语音命令时会对用户的意图进行识别。

要求使用 KNN 算法对用户输出的语音特征进行分类。

语音片段经过特征提取后是一个 $$3$$ 维的向量。

所有距离使用欧式距离公式计算。

输入描述

第一行包含两个正整数 $$N, K$$,分别表示已知语音特征向量的数量和邻居数量。

接下来 $$N$$ 行,每行包含 $$3$$ 个浮点数和 $$1$$ 个整数,分别表示 $$3$$ 维特征向量和类别标签。

最后一行包含 $$3$$ 个浮点数,表示待分类的语音特征向量。

输出描述

输出一个正整数,表示待分类语音的类别。

样例1

输入

10 3
0.5 0.3 0.4 0
0.6 0.2 0.5 0
0.4 0.3 0.3 0
0.7 0.4 0.6 0
2.1 2.3 2.2 1
2.3 2.2 2.4 1
2.2 2.4 2.3 1
4.5 4.3 4.4 2
4.4 4.5 4.6 2
4.6 4.4 4.5 2
2.2 2.1 2.3

输出

1

样例解释

待分类向量 $$(2.2, 2.1, 2.3)$$,最近的 $$3$$ 个邻居分别是 $$(2.1, 2.3, 2.2)$$、

$$(2.3, 2.2, 2.4)$$、$$(2.2, 2.4, 2.3)$$,均属于类别 $$1$$。

题解:KNN 实现

题目问题拆解

经典 KNN 分类:计算待分类向量与所有已知向量的欧氏距离,取最近 $$K$$ 个邻居,多数投票决定类别。

$$N$$ 较小,直接计算即可。

算法实现

算法主策略:本题采用暴力计算距离 + 排序取 Top-K + 多数投票

  1. 读取 $$N$$ 个训练样本和 $$1$$ 个待分类向量。
  2. 计算待分类向量与每个训练样本的欧氏距离 $$d=\sqrt{\sum_{k=1}^{3}(x_k-y_k)^2}$$。

3) 按距离排序,取前 $$K$$ 个邻居的标签,统计出现最多的类别作为预测结果。

时空复杂度分析

时间复杂度:$$O(N \log N)$$,排序主导。

空间复杂度:$$O(N)$$,存储距离数组。

Python

# KNN语音分类 - 欧氏距离 + 多数投票
import math
from collections import Counter

def solve():
    first_line = input().split()
    N, K = int(first_line[0]), int(first_line[1])

    # 读取N个已知向量(3维特征 + 标签)
    data = []
    for _ in range(N):
        parts = list(map(float, input().split()))
        features = parts[:-1]  # 前3个为特征
        label = int(parts[-1])  # 最后一个为类别
        data.append((features, label))

    # 读取待分类向量
    query = list(map(float, input().split()))

    # 计算欧氏距离并排序
    dists = []
    for features, label in data:
        d = math.sqrt(sum((a - b) ** 2 for a, b in zip(query, features)))
        dists.append((d, label))
    dists.sort()

    # K近邻多数投票
    neighbors = [label for _, label in dists[:K]]
    counter = Counter(neighbors)
    print(counter.most_common(1)[0][0])

solve()

2026-3-4-AI 岗

难度评级:中等偏难

考点分析

  1. 选择题(20 道):涵盖 Transformer、概率论、评价指标、集成学习、扩散模型等
  2. 编程第一题:KMeans 聚类模拟(难度中等)
  3. 编程第二题:NumPy 矩阵运算 + 岭回归(难度中等偏难)

建议策略

  1. 选择题涉及面广,重点复习 Transformer 原理、评价指标、概率论基础和大模型相关概念
  2. 编程第一题是经典 KMeans 算法模拟,理解"分配+更新"两步迭代即可,建议优先拿下
  3. 编程第二题涉及矩阵求逆和岭回归公式,需要熟悉 NumPy 的矩阵运算,注意区间边界的处理

选择题(20 道)

一、单选题

1、Transformer 模型的核心机制是?

A. 线性回归

B. 卷积神经网络

C. 循环神经网络

D. 自注意力机制

答案:D
难度:入门
考点:深度学习—Transformer
解释:Transformer 模型的核心创新是自注意力机制(Self-Attention),它允许模型在处理序列时直接建模任意位置之间的依赖关系,摆脱了 RNN 的顺序限制和 CNN 的局部感受野限制。

2、关于三次样条插值的描述,错误的是:

A. 插值函数在节点处二阶导数不连续

B. 在每个子区间上用三次多项式逼近函数

C. 插值函数通过所有给定数据点

D. 相比高次多项式插值,三次样条能避免 Runge 现象

答案:A
难度:中等
考点:数学—数值计算
解释:三次样条插值的核心特性就是在节点处保证函数值、一阶导数和二阶导数都连续(即 $$C^2$$ 连续),因此 A 选项"二阶导数不连续"是错误的。B、C、D 均为正确描述。

3、循环神经网络(RNN)相比前馈神经网络更适合处理以下哪种任务:

A. 时间序列预测

B. 高维数据降维

C. 图像分类

D. 文本分类

答案:A
难度:简单
考点:深度学习—RNN
解释:RNN 通过隐藏状态在时间步之间传递信息,天然适合处理序列数据。时间序列预测是最典型的序列任务,RNN 能捕捉数据中的时序依赖关系。

4、在模型评估中,精确率(Precision)与召回率(Recall)的关系是?

A. 一般情况下,精确率和召回率是相互矛盾的指标

B. 精确率越高,召回率一定越高

C. 精确率衡量真正例的比例,召回率衡量预测为正例的样本中实际为正的比例

D. 精确率和召回率均与阈值无关

答案:A
难度:简单
考点:机器学习—评价指标
解释:精确率和召回率通常存在此消彼长的关系:提高分类阈值会增加精确率但降低召回率,反之亦然。C 选项把两个定义说反了。

5、设 X 是非负整数的随机变量,则 $$P(X \ge 1)$$$$E[X]$$ 满足

A. $$P(X \ge 1) = E[X]$$

B. $$P(X \ge 1) \ge E[X]$$

C. 无必然关系

D. $$P(X \ge 1) \le E[X]$$

答案:D
难度:中等
考点:数学—概率论
解释:由 Markov 不等式,对非负随机变量 $$X,P(X \ge 1) \le E[X]$$。

6、在分类模型评估中,以下哪项指标对类别不平衡最不敏感?

A. AUC

B. Accuracy

C. Recall

D. Precision

答案:A
难度:中等
考点:机器学习—评价指标
解释:AUC 衡量的是模型对正负样本的排序能力,不依赖于类别分布比例,因此对类别不平衡最不敏感。

7、集成学习通过组合多个学习器来提升整体模型性能,Bagging 类算法属于集成学习的一种,通过学习多个训练器,采用投票的方式降低模型方差,以下属于 Bagging 类算法的是

A. Random Forest

B. GBDT

C. Decision Tree

D. Linear Regression

答案:A
难度:简单
考点:机器学习—集成学习
解释:随机森林是 Bagging 的典型代表。GBDT 属于 Boosting 类算法;Decision Tree 是单个学习器;Linear Regression 是回归算法。

8、给定二维数据集,包含三个样本 $${(0,0), (1,1), (2,2)}$$,求其协方差矩阵的迹(Trace)

A. $$\frac{2}{3}$$

B. $$0$$

C. $$\frac{4}{3}$$

D. $$2$$

答案:D
难度:中等
考点:数学—线性代数
解释:使用无偏估计(除以 $$n-1=2$$),$$\text{Var}(X) = \text{Var}(Y) = 1$$,迹 $$= 1 + 1 = 2$$。

9、关于预训练的目标,错误的是

A. 必须依赖标注数据

B. 主要使用交叉熵损失函数

C. 通过无监督学习捕捉语言统计规律

D. 模型需学习上下文表示能力

答案:A
难度:简单
考点:大模型—预训练
解释:预训练通过自监督学习从大规模无标注文本中学习语言规律,不需要人工标注数据。

10、强大数律(SLLN)与弱大数律(WLLN)的关键差别在于:

A. 随机变量是否独立

B. 样本容量

C. 收敛方式

D. 矩条件

答案:C
难度:中等
考点:数学—概率论
解释:强大数律要求几乎必然收敛,弱大数律要求依概率收敛,本质区别在于收敛方式。

11、在机器学习中特征选择过程中,下列关于常用方法的说法正确的是

A. 过滤法(Filter Method)依据已训练模型的特征重要性进行特征筛选。因此可降低模型复杂度。

B. 包裹法(Wrapper Method)应用交叉验证评估候选特征子集性能,计算成本高但能有效减少过拟合风险。

C. 嵌入法(Embedded Method)在模型训练前计算特征与目标的相关系数,独立于模型进行选择。

D. 方差阈值法(Variance Threshold)通过保留方差较低特征,增强模型对噪声的鲁棒性,防止过拟合。

答案:B
难度:中等
考点:机器学习—特征工程
解释:A 把过滤法和嵌入法搞混了;B 正确,包裹法通过交叉验证评估特征子集;C 把嵌入法和过滤法搞混了;D 应该保留高方差特征。

12、已知函数 $$f(x)$$,用中心差分近似导数 $$f'(x)$$,公式为 $$f'(x)\approx \frac{f(x+h)-f(x-h)}{2h}$$,请问近似导数的误差阶是:

A. $$O(h)$$

B. $$O(h \log h)$$

C. $$O(h^3)$$

D. $$O(h^2)$$

答案:D
难度:中等
考点:数学—数值计算
解释:对 $$f(x \pm h)$$ 做泰勒展开相减后除以 $$2h$$,奇数次项抵消,截断误差为 $$O(h^2)$$。

13、假设输入组 $$a = [0, 1, 2, 3]$$,则经 ReLU 激活函数后,输出向量为:

A. $$[0, 8, 0, 2]$$

B. $$[0, 8, -1, 2, 2, 1]$$

C. $$[0, 186, 0, 050, 0, 764]$$

D. $$[0, -1, 2, 0]$$

答案:A
难度:简单
考点:深度学习—激活函数
解释:ReLU 函数定义为 $$\text{ReLU}(x) = \max(0, x)$$,负值变为 0,正值保持不变。结合选项,A 最接近合理输出。

14、大模型中的"涌现能力"(Emergent Abilities)是指?

A. 模型参数减少到一定程度后出现的新能力

B. 当模型规模(参数量、训练数据量)达到某个阈值时突然展现的小模型不具备或表现不佳的能力

C. 需要人工手动添加的专项功能

D. 只能通过特定硬件才能实现的加速能力

答案:B
难度:简单
考点:大模型—涌现能力
解释:涌现能力是指当模型规模超过某个临界阈值时,突然出现的小模型不具备的能力,如思维链推理等。

15、RLHF 阶段使用的典型算法是

A. 监督式微调(SFT)

B. 最大化人类偏好得分

C. DQN (Deep Q-Network)

D. PPO (Proximal Policy Optimization)

答案:D
难度:简单
考点:大模型—RLHF
解释:RLHF 的强化学习阶段典型使用 PPO 算法。SFT 是 RLHF 之前的阶段;DQN 不适用于语言生成场景。

二、多选题

16、在扩散模型推理中,以下哪些技术被用于一步/少步生成?

A. DDIM inversion(DDIM 逆过程)

B. Progressive Distillation(渐进式蒸馏)

C. condition-free guidance(无条件引导)

D. Consistency Models(一致性模型)

答案:B, D
难度:困难
考点:深度学习—扩散模型
解释:渐进式蒸馏通过逐步将多步教师模型蒸馏到少步学生模型;Consistency Models 实现一步生成。DDIM inversion 用于图像编辑,classifier-free guidance 用于提高生成质量,不是减少步数的方法。

17、一位深度学习工程师正在分析一个标准 Transformer 模型的性能瓶颈和训练动态,请判断哪些结论正确:

A. 在计算 softmax 时,先减去最大值是标准的数值稳定性技巧,数学上等价

B. LayerNorm 中 gamma 和 beta 的主要作用是严格维持标准正态分布

C. 64000 词汇表 × 512 维嵌入,参数量约 3200 万

D. 序列长度 N 远大于 d\_model 时,自注意力 $$N^2$$ 计算成为瓶颈;N 较小时 FFN 占主导

答案:A, C, D
难度:困难
考点:深度学习—Transformer
解释:B 错误,gamma 和 beta 的作用是恢复表达能力,使输出不局限于标准正态分布。A、C、D 均为正确描述。

18、下列哪些属于常用的神经网络模块

A. Batch Norm

B. 全连接层

C. Attention

D. 卷积层

答案:A, B, C, D
难度:入门
考点:深度学习—网络架构
解释:四个选项均为常用的神经网络模块,Batch Norm 用于归一化,全连接层是基础模块,Attention 是 Transformer 核心,卷积层是 CNN 核心。

19、以下哪些措施可以提高迭代法的收敛性?

A. 使用稀疏矩阵存储

B. 选择适当的松弛因子

C. 增加计算精度

D. 改进迭代初始值

答案:B, D
难度:中等
考点:数学—数值计算
解释:合适的松弛因子能显著加速收敛;更好的初始值可以减少迭代次数。稀疏矩阵存储只影响效率不影响收敛性;增加精度影响精确度但不影响收敛速度。

20、设 A、B、C 为三个事件,则下面的等式中正确的是

A. $$(A - B) \cap (A - C) = A - (B \cup C)$$

B. $$A \cup B - B = A - B$$

C. $$(A \cup B) - C = A \cup (B - C)$$

D. $$(A - B) \cup B = A$$

答案:A, B
难度:困难
考点:数学—概率论
解释:A 由 De Morgan 定律成立;B 展开后等价。C 反例:$$A={1}, B=\emptyset, C={1}$$;D 反例:$$A={1}, B={1,2}$$。

第一题:网络流量分析

在线评测链接:https://www.neituiya.com/oj/16/2289

题目描述

网络流量分析是网络安全和性能优化的关键任务。假设你有一个包含网络流量数据的数据集,每条数据包含以下特征:

$$packet\_size$$(数据包大小,单位:字节)

$$inter\_arrival\_time$$(数据包到达间隔时间,单位:毫秒)

$$protocol\_type$$(协议类型,如 TCP、UDP、ICMP 等,已转换为数值编码)

你的任务是使用 KMeans 聚类算法对网络流量进行分类,分类后的中心点再经过一个分类头,能识别出可能的流量类型(如正常流量、异常流量、DDoS 攻击流量等)。KMeans 算法原理为:先将数据分为 $$K$$ 组,随机选取 $$K$$ 个对象作为初始的聚类中心,然后计算每个对象与各个种子聚类中心之间的距离,将每一个对象分配给距离它最近的聚类中心,聚类中心以及分配给它们的对象就代表一个聚类。(为保证结果固定,本题初始的聚类中心已给出,不需要随机)

输入描述

第一行包含整数 $$k(1 \le k \le 1000)$$,表示初始聚类点个数。

接下来 $$k$$ 行,每行包含三个浮点数,表示初始聚类中心的三个特征值。

第 $$k+2$$ 行包含整数 $$iter(1 \le iter \le 1000)$$,表示迭代次数。

第 $$k+3$$ 行包含整数 $$m(1 \le m \le 1000)$$,表示样本个数。

接下来 $$m$$ 行,每行包含三个浮点数 $$(0 \le f \le 1000)$$,表示每个样本的三个特征(已归一化处理,各维度权重占比一致)。

输出描述

输出 $$k$$ 行,每行三个浮点数,表示按给定次数迭代后新的聚类中心集合,保留两位小数,四舍五入。

样例1

输入

3
50 25 30
60 15 60
25 75 90
3
9
50 25 30
30 50 30
60 15 60
25 75 90
10 05 60
26 15 30
32 67.5 90
80 7.5 60
20 100 90

输出

35.33 30.00 30.00
50.00 9.17 60.00
25.67 80.83 90.00

样例解释

采用欧式距离计算不同数据之间的距离:$$d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2}$$。计算每个样本距离各中心点的距离,选择距离最近的中心点作为该样本归属的类别,按照划分的类别重新计算每个类别的中心点(对应类里所有样本的平均值),完成一轮迭代。重复上述流程 $$iter$$ 次得到结果。

样例2

输入

3
50 20 30
60 10 60
180 180 180
3
8
50 20 30
30 50 30
60 10 60
25 75 90
100 5 60
30 60 90
80 10 60
180 180 180

输出

40.00 35.00 30.00
59.00 32.00 72.00
180.00 180.00 180.00

样例解释

采用欧式距离计算不同数据之间的距离:$$d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2}$$。计算每个样本距离各中心点的距离,选择距离最近的中心点作为该样本归属的类别,按照划分的类别重新计算每个类别的中心点(对应类里所有样本的平均值),完成一轮迭代。重复上述流程 $$iter$$ 次得到结果。

题解:KMeans 聚类模拟

题目问题拆解

给定 $$k$$ 个初始聚类中心和 $$m$$ 个三维样本点,执行 $$iter$$ 次 KMeans 迭代(每轮:按欧式距离分配样本到最近中心 → 重新计算各簇中心为该簇样本的均值),输出最终聚类中心。数据规模 $$k, m, iter \le 1000$$,暴力模拟 $$O(iter \times m \times k)$$ 即可。

算法实现

算法主策略:本题采用KMeans 迭代模拟,严格按照算法定义逐轮执行"分配 + 更新"两步操作。

分配步骤:对每个样本点,计算它到所有 $$k$$ 个中心的欧式距离,将其归入距离最近的簇。

更新步骤:对每个簇,将该簇内所有样本的坐标取平均值作为新的聚类中心。若某个簇没有样本被分配,则该中心保持不变。

迭代:重复上述两步共 $$iter$$ 次,最后输出各中心坐标,保留两位小数。

时空复杂度分析

时间复杂度:$$O(iter \times m \times k)$$,每轮迭代中每个样本需要计算与 $$k$$ 个中心的距离。

空间复杂度:$$O(k + m)$$,存储中心点坐标和样本数据。

Python

# 网络流量分析 - KMeans聚类模拟
import numpy as np

k = int(input())
centers = []
for _ in range(k):
    centers.append(list(map(float, input().split())))
iter_count = int(input())
m = int(input())
samples = []
for _ in range(m):
    samples.append(list(map(float, input().split())))

centers = np.array(centers, dtype=np.float64)
samples = np.array(samples, dtype=np.float64)

# 执行iter_count轮KMeans迭代
for _ in range(iter_count):
    # 分配步骤:计算每个样本到每个中心的距离平方,取最近的
    # samples: (m, 3), centers: (k, 3)
    # dists: (m, k),广播计算所有距离
    dists = np.sum((samples[:, np.newaxis, :] - centers[np.newaxis, :, :]) ** 2, axis=2)
    assignments = np.argmin(dists, axis=1)
    # 更新步骤:每个簇的中心更新为该簇样本坐标的均值
    for j in range(k):
        mask = assignments == j
        if np.any(mask):
            centers[j] = np.mean(samples[mask], axis=0)

# 输出最终聚类中心,保留两位小数
for c in centers:
    print(" ".join(f"{v:.2f}" for v in c))

第二题:动态区间的多项式岭回归

在线评测链接:https://www.neituiya.com/oj/16/2290

题目描述

某大型互联网公司的数据中心,记录了其核心服务在连续 $$N=200$$ 天内的出口总流量(单位 GB,取值范围通常在 $$100.00$$ 到 $$500.00$$ 之间),已按时间顺序给出。由于监控系统维护,数据中有 $$M$$ 个($$M$$ 的范围为 $$20$$ 到 $$30$$)缺失值,按顺序标记为 $$Missing\_1, Missing\_2, ..., Missing\_M$$。已知这些缺失值保证不会出现在第 $$1$$ 天和最后 $$1$$ 天(即首尾两条记录一定存在)。

你需要为每一个缺失值,通过其邻近的、动态变化的真实数据区间,建立一个二阶多项式岭回归模型进行预测。

区间定义

对位于全局序号 $$pos$$ 的缺失值:

前向区间 $$[left\_start, pos-1]$$:从 $$pos-1$$ 开始向前寻找,遇到的第一个原始缺失值的后一天作为起始点。如果到第 $$1$$ 天仍未碰到任何原始缺失值,则 $$left\_start$$ 为第 $$1$$ 天。

后向区间 $$[pos+1, right\_end]$$:从 $$pos+1$$ 开始向后寻找,遇到的第一个原始缺失值的前一天作为终止点。如果到第 $$N$$ 天仍未碰到任何原始缺失值,则 $$right\_end$$ 为第 $$N$$ 天。

算法与公式

取上述两个区间内的所有真实记录作为训练集 $$(x, y)$$,其中 $$x$$ 为日期序号,$$y$$ 为对应的流量值。使用岭回归求解二阶多项式模型:

$$\hat{y} = \beta_2 x^2 + \beta_1 x + \beta_0$$

岭回归的解通过矩阵公式计算:$$\beta = (X^T X + \lambda I)^{-1} X^T y$$

其中 $$\beta$$ 是 $$3 \times 1$$ 的列向量 $$[\beta_2, \beta_1, \beta_0]^T$$。$$X$$是 $$n \times 3$$ 的设计矩阵,每一行为 $$[x_i^2, x_i, 1]$$。$$y$$ 是 $$n \times 1$$ 的列向量。$$\lambda = 0.1$$ 为正则化参数,$$I$$ 是 $$3 \times 3$$ 的单位矩阵。

输入描述

第一行包含两个整数 $$M, N(20 \le M \le 30, N = 200)$$,分别表示缺失值总数和数据行数。

接下来 $$N$$ 行,每行包含一个值:一个浮点数表示当日真实流量值,或一个字符串 $$Missing\_i$$($$i$$ 从 $$1$$ 到 $$M$$)表示该日数据缺失。

输出描述

共 $$M$$ 行,按 $$Missing\_1, Missing\_2, ..., Missing\_M$$ 的顺序输出。每行格式为 $$Missing\_i:xxx.xx$$(标签、冒号、预测值,保留两位小数)。

样例1

输入

20 200
140.36
146.38
167.91
162.64
181.99
166.79
Missing_1
156.46
175.24
165.52
157.71
Missing_2
158.26
169.09
142.55
151.18
148.18
Missing_3
140.23
146.42
135.47
Missing_4
130.90
138.79
133.65
129.18
151.72
142.50
133.01
157.68
Missing_5
157.02
169.40
168.70
178.77
160.13
174.77
174.48
162.20
167.09
181.81
160.76
172.85
167.83
167.38
164.35
140.30
160.63
143.56
142.56
133.02
133.61
Missing_6
130.73
143.76
146.32
136.02
Missing_7
151.45
143.21
147.88
164.99
176.53
177.58
163.27
Missing_8
163.40
167.27
182.03
189.90
175.84
181.42
171.06
160.66
161.04
159.17
156.67
Missing_9
140.52
153.13
135.72
153.66
136.88
143.00
Missing_10
147.52
136.38
152.19
Missing_11
140.37
151.19
155.24
Missing_12
176.08
166.01
152.19
174.35
186.10
189.84
Missing_13
167.38
180.46
184.17
167.70
158.32
170.87
159.46
152.25
164.62
159.22
160.63
155.92
132.63
146.97
128.47
133.05
134.12
145.20
161.01
153.34
152.31
160.25
157.89
162.57
159.33
188.02
188.42
Missing_14
190.36
172.49
179.07
186.54
174.78
189.76
179.46
169.32
Missing_15
166.40
174.29
147.45
140.39
166.35
150.74
133.56
158.77
140.73
153.93
136.37
143.02
168.03
162.22
173.28
176.61
159.22
173.93
179.96
169.60
178.89
190.53
202.52
200.04
187.90
Missing_16
184.13
193.93
170.60
183.11
178.36
170.28
174.84
160.06
169.08
159.11
Missing_17
140.99
148.42
156.97
144.91
144.61
169.12
152.68
176.46
Missing_18
165.14
170.70
171.10
182.38
181.63
196.53
Missing_19
180.73
182.49
192.30
184.48
178.30
192.26
193.45
188.63
Missing_20
176.12
173.62

输出

Missing_1:175.81
Missing_2:168.12
Missing_3:150.08
Missing_4:138.62
Missing_5:158.61
Missing_6:141.85
Missing_7:146.87
Missing_8:166.18
Missing_9:155.56
Missing_10:144.75
Missing_11:147.16
Missing_12:157.84
Missing_13:165.62
Missing_14:169.87
Missing_15:166.19
Missing_16:174.52
Missing_17:164.10
Missing_18:167.63
Missing_19:181.34
Missing_20:185.38

样例解释

对每个 $$Missing\_i$$,根据其前后最近的原始缺失值位置确定动态训练区间,收集区间内的真实数据点作为训练集,构建设计矩阵 $$X = [x^2, x, 1]$$,通过岭回归公式 $$\beta = (X^T X + \lambda I)^{-1} X^T y$$($$\lambda = 0.1$$)求解系数,然后用 $$\hat{y} = \beta_2 \cdot pos^2 + \beta_1 \cdot pos + \beta_0$$ 预测缺失值。

题解:NumPy 矩阵运算

题目问题拆解

给定 $$N=200$$ 天的流量数据(含 $$M$$ 个缺失值),对每个缺失值,根据其前后最近的其他缺失值位置确定动态训练区间,在区间内的真实数据上拟合二阶多项式岭回归模型,预测缺失值。

算法实现

数学原理:本题需要实现二阶多项式岭回归,核心是矩阵求逆运算。

核心公式

$$\hat{y} = \beta_2 x^2 + \beta_1 x + \beta_0$$

$$\beta = (X^T X + \lambda I)^{-1} X^T y$$

实现步骤

  1. 读取所有数据,记录每个缺失值的位置和所有真实值。
  2. 对每个缺失值,从其位置向前、向后搜索,遇到第一个其他原始缺失值即确定训练区间的边界。

3) 收集训练区间内的真实数据点 $$(x, y)$$,构建设计矩阵 $$X$$(每行 $$[x_i^2, x_i, 1]$$)。

4) 用岭回归公式求解 $$\beta$$,代入缺失位置的日期序号 $$pos$$ 得到预测值。

维度变化

设训练集有 $$n$$ 个数据点,设计矩阵 $$X$$: $$(n, 3)$$,$$X^T X$$: $$(3, 3)$$,$$X^T y$$: $$(3,)$$,$$\beta$$: $$(3,)$$。

时空复杂度分析

时间复杂度:$$O(M \times N)$$,每个缺失值需要遍历数据确定区间和收集训练集,矩阵求逆是 $$O(3^3)$$ 常数级。

空间复杂度:$$O(N)$$,存储所有数据和标记数组。

Python

# 动态区间的多项式岭回归 - NumPy矩阵运算
import numpy as np

M, N = map(int, input().split())

# 读取N天数据,区分真实值和缺失值
data = [None] * (N + 1)        # 1-indexed,存储真实流量值
is_missing = [False] * (N + 1) # 标记原始缺失位置
miss_pos = {}                  # Missing_i → 对应的天数

for day in range(1, N + 1):
    line = input().strip()
    if line.startswith('Missing_'):
        mi = int(line.split('_')[1])
        miss_pos[mi] = day
        is_missing[day] = True
    else:
        # 处理可能的逗号代替小数点
        data[day] = float(line.replace(',', '.'))

lam = 0.1  # 正则化参数

for mi in range(1, M + 1):
    pos = miss_pos[mi]

    # 前向区间起点:从pos-1向前找第一个原始缺失值,取其后一天
    left = 1
    for d in range(pos - 1, 0, -1):
        if is_missing[d]:
            left = d + 1
            break

    # 后向区间终点:从pos+1向后找第一个原始缺失值,取其前一天
    right = N
    for d in range(pos + 1, N + 1):
        if is_missing[d]:
            right = d - 1
            break

    # 收集训练集:[left, pos-1] ∪ [pos+1, right] 中的真实数据点
    xs, ys = [], []
    for d in range(left, pos):
        if not is_missing[d]:
            xs.append(d)
            ys.append(data[d])
    for d in range(pos + 1, right + 1):
        if not is_missing[d]:
            xs.append(d)
            ys.append(data[d])

    x = np.array(xs, dtype=np.float64)
    y = np.array(ys, dtype=np.float64)

    # 设计矩阵 X = [x², x, 1],维度 (n, 3)
    X = np.column_stack([x ** 2, x, np.ones(len(x))])

    # 岭回归求解 β = (X^T X + λI)^{-1} X^T y,维度 (3,)
    beta = np.linalg.inv(X.T @ X + lam * np.eye(3)) @ (X.T @ y)

    # 预测:ŷ = β₂·pos² + β₁·pos + β₀
    pred = beta[0] * pos ** 2 + beta[1] * pos + beta[2]
    print(f"Missing_{mi}:{pred:.2f}")

0

评论 (0)

取消