首页
友链
Search
1
本网站搭建
24 阅读
2
原神!启动!0v0
22 阅读
3
25/26大模型面试经典题
13 阅读
4
常用代码模板4——数学知识
6 阅读
5
LLM
5 阅读
经验贴
从零开始系列
学生时代
工作
claude code
C++
登录
/
注册
Search
标签搜索
0-1
web
justu
Wxb
累计撰写
40
篇文章
累计收到
0
条评论
首页
栏目
经验贴
从零开始系列
学生时代
工作
claude code
C++
页面
友链
搜索到
40
篇与
的结果
microgpt
以下内容翻译自Andrej Karpathy 的博客microgptAndrej Karpathy | 2026年2月12日原文链接:http://karpathy.github.io/2026/02/12/microgpt/这是我新的艺术项目 microgpt 的简要指南——一个仅有200行纯Python代码、零依赖的文件,可以训练和推理一个GPT模型。这个文件包含了所需的完整算法内容:文档数据集、分词器、自动微分引擎、类GPT-2的神经网络架构、Adam优化器、训练循环和推理循环。除此之外的一切都只是为了效率。我已经无法再进一步简化了。这个脚本是多个项目(micrograd、makemore、nanogpt等)的最终结晶,也是我十年来将LLM简化到最本质的执念,我觉得它很美 🥹。它甚至完美地分成了3列:在哪里可以找到它:GitHub Gist上有完整源代码:microgpt.py也可以在这个网页上查看:https://karpathy.ai/microgpt.html还可以作为 Google Colab 笔记本 使用以下是我为有兴趣的读者逐步讲解代码的指南。数据集(Dataset)大语言模型的燃料是文本数据流,可以选择性地分成一组文档。在生产级应用中,每个文档会是一个互联网网页,但对于 microgpt,我们使用一个更简单的例子——32,000个名字,每行一个:# 让这里有一个输入数据集 `docs`:list[str] 的文档列表(例如一个名字数据集) if not os.path.exists('input.txt'): import urllib.request names_url = 'https://raw.githubusercontent.com/karpathy/makemore/refs/heads/master/names.txt' urllib.request.urlretrieve(names_url, 'input.txt') docs = [l.strip() for l in open('input.txt').read().strip().split('\n') if l.strip()] # list[str] 文档列表 random.shuffle(docs) print(f"num docs: {len(docs)}")数据集看起来是这样的。每个名字就是一个文档:emma olivia ava isabella sophia charlotte mia amelia harper ...(约32,000个名字)模型的目标是学习数据中的模式,然后生成共享相同统计模式的新文档。作为预览,在脚本运行结束时,我们的模型将会生成("幻觉"出!)新的、听起来合理的名字。提前剧透一下,我们会得到:sample 1: kamon sample 2: ann sample 3: karai sample 4: jaire sample 5: vialan sample 6: karia sample 7: yeran sample 8: anna sample 9: areli sample 10: kaina sample 11: konna sample 12: keylen sample 13: liole sample 14: alerin sample 15: earan sample 16: lenne sample 17: kana sample 18: lara sample 19: alela sample 20: anton看起来不算什么,但从像ChatGPT这样的模型的角度来看,你和它的对话只不过是一种形式特殊的"文档"。当你用提示词(prompt)初始化文档时,模型的回复从它的视角来看只是一种统计上的文档补全。分词器(Tokenizer)在底层,神经网络处理的是数字而非字符,因此我们需要一种方法将文本转换为整数token id的序列,然后再转回来。生产级分词器如 tiktoken(GPT-4使用的)为了效率会操作字符块,但最简单的分词器只是为数据集中每个唯一字符分配一个整数:# 让这里有一个分词器,将字符串翻译为离散符号,再翻译回来 uchars = sorted(set(''.join(docs))) # 数据集中的唯一字符成为 token id 0..n-1 BOS = len(uchars) # 特殊的序列开始(BOS)token 的 id vocab_size = len(uchars) + 1 # 唯一 token 的总数,+1 是给 BOS 的 print(f"vocab size: {vocab_size}")在上面的代码中,我们收集数据集中所有唯一字符(即所有小写字母 a-z),排序后每个字母通过其索引获得一个 id。注意,整数值本身没有任何意义;每个 token 只是一个独立的离散符号。它们不是0、1、2,用不同的emoji来代替也一样。此外,我们创建了一个额外的特殊token叫BOS(Beginning of Sequence,序列开始),它充当分隔符:告诉模型"一个新文档从这里开始/结束"。后面在训练时,每个文档的两侧都会用BOS包裹:[BOS, e, m, m, a, BOS]。模型学会BOS意味着开始一个新名字,另一个BOS意味着结束它。因此,我们最终的词汇表大小为27(26个可能的小写字母 a-z,加上1个BOS token)。自动微分(Autograd)训练神经网络需要梯度:对于模型中的每个参数,我们需要知道"如果我把这个数字稍微增大一点,损失是上升还是下降,变化了多少?"。计算图有很多输入(模型参数和输入token),但最终汇聚到一个标量输出:损失(我们将在下面准确定义损失是什么)。反向传播从那个单一输出开始,沿着计算图反向工作,计算损失相对于每个输入的梯度。它依赖于微积分中的链式法则。在生产中,PyTorch等库会自动处理这些。这里,我们在一个叫做 Value 的类中从头实现它:class Value: __slots__ = ('data', 'grad', '_children', '_local_grads') def __init__(self, data, children=(), local_grads=()): self.data = data # 前向传播中计算的标量值 self.grad = 0 # 损失对该节点的导数,在反向传播中计算 self._children = children # 计算图中该节点的子节点 self._local_grads = local_grads # 该节点对其子节点的局部导数 def __add__(self, other): other = other if isinstance(other, Value) else Value(other) return Value(self.data + other.data, (self, other), (1, 1)) def __mul__(self, other): other = other if isinstance(other, Value) else Value(other) return Value(self.data * other.data, (self, other), (other.data, self.data)) def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),)) def log(self): return Value(math.log(self.data), (self,), (1/self.data,)) def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),)) def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),)) def __neg__(self): return self * -1 def __radd__(self, other): return self + other def __sub__(self, other): return self + (-other) def __rsub__(self, other): return other + (-self) def __rmul__(self, other): return self * other def __truediv__(self, other): return self * other**-1 def __rtruediv__(self, other): return other * self**-1 def backward(self): topo = [] visited = set() def build_topo(v): if v not in visited: visited.add(v) for child in v._children: build_topo(child) topo.append(v) build_topo(self) self.grad = 1 for v in reversed(topo): for child, local_grad in zip(v._children, v._local_grads): child.grad += local_grad * v.grad我知道这是数学和算法上最密集的部分,我有一个 2.5小时的视频 专门讲解它:micrograd视频。简单来说,一个 Value 包装了一个标量数字(.data),并追踪它是如何被计算出来的。把每个操作想象成一块小乐高积木:它接收一些输入,产生一个输出(前向传播),并且知道它的输出相对于每个输入会如何变化(局部梯度)。这就是自动微分从每个积木块中所需的全部信息。其余的一切只是链式法则,把这些积木串在一起。每当你用 Value 对象做数学运算(加法、乘法等),结果是一个新的 Value,它记住了它的输入(_children)以及该操作的局部导数(_local_grads)。例如,__mul__ 记录了 ∂(a·b)/∂a = b 和 ∂(a·b)/∂b = a。完整的乐高积木集合:运算前向局部梯度a + ba + b∂/∂a = 1, ∂/∂b = 1a * ba · b∂/∂a = b, ∂/∂b = aa ** naⁿ∂/∂a = n · aⁿ⁻¹log(a)ln(a)∂/∂a = 1/aexp(a)eᵃ∂/∂a = eᵃrelu(a)max(0, a)∂/∂a = 1_{a>0}backward() 方法按照反向拓扑排序遍历计算图(从损失开始,到参数结束),在每一步应用链式法则。如果损失是 L,一个节点 v 有一个子节点 c,局部梯度为 ∂v/∂c,那么:∂L/∂c += ∂v/∂c · ∂L/∂v如果你对微积分不太熟悉,这看起来可能有点吓人,但这实际上就是以直觉的方式将两个数字相乘。一种理解方式是:"如果一辆汽车的速度是自行车的两倍,而自行车的速度是步行者的四倍,那么汽车的速度就是步行者的 2 × 4 = 8 倍。"链式法则就是同样的道理:沿着路径乘以变化率。我们通过在损失节点处设置 self.grad = 1 来启动,因为 ∂L/∂L = 1:损失相对于自身的变化率显然是1。从那里开始,链式法则只需沿每条路径将局部梯度相乘回传到参数。注意 +=(累加,而非赋值)。当一个值在计算图中多处被使用(即图产生分支)时,梯度沿每个分支独立回传,必须求和。这是多元链式法则的结果:如果 c 通过多条路径对 L 有贡献,总导数是每条路径贡献的总和。backward() 完成后,图中的每个 Value 都有一个 .grad,包含 ∂L/∂v,告诉我们如果微调该值,最终损失会如何变化。这里有一个具体的例子。注意 a 被使用了两次(图产生分支),所以它的梯度是两条路径的总和:a = Value(2.0) b = Value(3.0) c = a * b # c = 6.0 L = c + a # L = 8.0 L.backward() print(a.grad) # 4.0 (dL/da = b + 1 = 3 + 1,通过两条路径) print(b.grad) # 2.0 (dL/db = a = 2)这和 PyTorch 的 .backward() 给出的结果完全一致:import torch a = torch.tensor(2.0, requires_grad=True) b = torch.tensor(3.0, requires_grad=True) c = a * b L = c + a L.backward() print(a.grad) # tensor(4.) print(b.grad) # tensor(2.)这和 PyTorch 的 loss.backward() 运行的是同一个算法,只不过是在标量而非张量(标量数组)上——算法完全相同,规模显著更小更简单,但当然效率低得多。让我们详细说明上面 .backward() 给出的结果。自动微分计算出,如果 L = a*b + a,且 a=2, b=3,那么 a.grad = 4.0 告诉我们 a 对 L 的局部影响。如果你微调输入 a,L 会往哪个方向变化?这里,L 对 a 的导数是4.0,意味着如果我们将 a 增加一个微小量(比如0.001),L 将增加大约4倍(0.004)。类似地,b.grad = 2.0 意味着对 b 的同样微调会使 L 增加大约2倍(0.002)。换句话说,这些梯度告诉我们每个单独输入对最终输出(损失)的影响方向(正或负取决于符号)和陡度(幅度)。这然后允许我们迭代地微调神经网络的参数以降低损失,从而改善其预测。参数(Parameters)参数是模型的知识。它们是一大堆浮点数(用 Value 包装以支持自动微分),初始为随机值,在训练过程中被迭代优化。每个参数的确切角色在我们定义模型架构后会更有意义,但现在我们只需要初始化它们:n_embd = 16 # 嵌入维度 n_head = 4 # 注意力头数 n_layer = 1 # 层数 block_size = 16 # 最大序列长度 head_dim = n_embd // n_head # 每个头的维度 matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)] state_dict = { 'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd), 'lm_head': matrix(vocab_size, n_embd) } for i in range(n_layer): state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd) state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd) params = [p for mat in state_dict.values() for row in mat for p in row] print(f"num params: {len(params)}")每个参数被初始化为从高斯分布中采样的小随机数。state_dict 将它们组织成命名矩阵(借用PyTorch的术语):嵌入表、注意力权重、MLP权重和最终输出投影。我们还将所有参数展平成一个列表 params,以便优化器稍后遍历它们。在我们的微型模型中,这共有4,192个参数。GPT-2有16亿个,现代LLM有数千亿个。架构(Architecture)模型架构是一个无状态函数:它接收一个 token、一个位置、参数以及之前位置缓存的 key/value,返回 logits(分数),表示模型认为序列中下一个最可能出现的 token。我们遵循GPT-2并做了小幅简化:RMSNorm替代LayerNorm,没有偏置,ReLU替代GeLU。首先,三个小的辅助函数:def linear(x, w): return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]linear 是矩阵-向量乘法。它接收一个向量 x 和一个权重矩阵 w,对 w 的每一行计算一个点积。这是神经网络的基本构建块:一个学习到的线性变换。def softmax(logits): max_val = max(val.data for val in logits) exps = [(val - max_val).exp() for val in logits] total = sum(exps) return [e / total for e in exps]softmax 将一个原始分数向量(logits)——范围可以从 -∞ 到 +∞——转换为概率分布:所有值都在 [0, 1] 之间且和为1。我们先减去最大值以保证数值稳定(这在数学上不改变结果,但防止 exp 溢出)。def rmsnorm(x): ms = sum(xi * xi for xi in x) / len(x) scale = (ms + 1e-5) ** -0.5 return [xi * scale for xi in x]rmsnorm(均方根归一化)重新缩放一个向量,使其值具有单位均方根。这使得激活值在网络中流动时不会增长或缩小,从而稳定训练。它是原始GPT-2中使用的 LayerNorm 的简化版本。现在是模型本身:def gpt(token_id, pos_id, keys, values): tok_emb = state_dict['wte'][token_id] # token 嵌入 pos_emb = state_dict['wpe'][pos_id] # 位置嵌入 x = [t + p for t, p in zip(tok_emb, pos_emb)] # token 和位置的联合嵌入 x = rmsnorm(x) for li in range(n_layer): # 1) 多头注意力块 x_residual = x x = rmsnorm(x) q = linear(x, state_dict[f'layer{li}.attn_wq']) k = linear(x, state_dict[f'layer{li}.attn_wk']) v = linear(x, state_dict[f'layer{li}.attn_wv']) keys[li].append(k) values[li].append(v) x_attn = [] for h in range(n_head): hs = h * head_dim q_h = q[hs:hs+head_dim] k_h = [ki[hs:hs+head_dim] for ki in keys[li]] v_h = [vi[hs:hs+head_dim] for vi in values[li]] attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 for t in range(len(k_h))] attn_weights = softmax(attn_logits) head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)] x_attn.extend(head_out) x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) x = [a + b for a, b in zip(x, x_residual)] # 2) MLP块 x_residual = x x = rmsnorm(x) x = linear(x, state_dict[f'layer{li}.mlp_fc1']) x = [xi.relu() for xi in x] x = linear(x, state_dict[f'layer{li}.mlp_fc2']) x = [a + b for a, b in zip(x, x_residual)] logits = linear(x, state_dict['lm_head']) return logits这个函数处理一个 token(id 为 token_id),在时间上的特定位置(pos_id),以及由之前迭代中 key 和 value 的激活值总结的上下文,即 KV Cache。以下是逐步发生的事情:嵌入(Embeddings)。神经网络不能直接处理像5这样的原始 token id。它只能处理向量(数字列表)。所以我们为每个可能的 token 关联一个学习到的向量,并将其作为 token 的神经签名输入。token id 和 position id 各自在相应的嵌入表(wte 和 wpe)中查找一行。这两个向量相加,给模型一个同时编码了 token 是什么以及它在序列中位置的表示。现代LLM通常跳过位置嵌入,引入其他基于相对位置的方案,例如 RoPE。注意力块(Attention block)。当前 token 被投影为三个向量:查询(Q)、键(K)和值(V)。直觉上,查询说"我在找什么?",键说"我包含什么?",值说"如果被选中,我提供什么?"。例如,在名字"emma"中,当模型在第二个"m"处试图预测下一个字符时,它可能学到一个类似"最近出现了什么元音?"的查询。较早的"e"会有一个与此查询匹配良好的键,因此它获得高注意力权重,它的值(关于是元音的信息)就流入当前位置。键和值被追加到 KV cache 中,以便之前的位置可用。每个注意力头计算其查询和所有缓存键之间的点积(除以 √d_head 进行缩放),应用 softmax 得到注意力权重,然后对缓存值取加权和。所有头的输出被拼接后通过 attn_wo 投影。值得强调的是,注意力块是位置 t 的 token "查看"过去 0..t-1 位置 token 的唯一且精确的位置。注意力是一种 token 通信机制。MLP块。MLP是多层感知机(multilayer perceptron)的缩写,是一个两层前馈网络:先投影到4倍嵌入维度,应用 ReLU,再投影回来。这是模型在每个位置进行大部分"思考"的地方。与注意力不同,这个计算完全局限于时间 t。Transformer 交替使用通信(注意力)和计算(MLP)。残差连接(Residual connections)。注意力和MLP块都将其输出加回其输入(x = [a + b for ...])。这让梯度可以直接流过网络,使更深的模型可以训练。输出。最终的隐藏状态通过 lm_head 投影到词汇表大小,产生词汇表中每个 token 的一个 logit。在我们的例子中,这只是27个数字。更高的 logit = 模型认为对应的 token 更可能是下一个。你可能注意到我们在训练过程中也使用了 KV cache,这并不常见。人们通常将 KV cache 与推理联系在一起。但 KV cache 在概念上一直存在,即使在训练中也是如此。在生产实现中,它只是隐藏在高度向量化的注意力计算中,该计算同时处理序列中的所有位置。由于 microgpt 一次处理一个 token(没有批次维度,没有并行时间步),我们显式构建 KV cache。与典型推理设置中 KV cache 持有分离张量不同,这里缓存的 key 和 value 是计算图中活跃的 Value 节点,所以我们实际上通过它们进行反向传播。训练循环(Training Loop)现在我们把所有东西串联起来。训练循环重复执行:(1) 选择一个文档,(2) 将模型在其 token 上前向运行,(3) 计算损失,(4) 反向传播得到梯度,(5) 更新参数。# 让这里有 Adam,神圣的优化器及其缓冲区 learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8 m = [0.0] * len(params) # 一阶矩缓冲区 v = [0.0] * len(params) # 二阶矩缓冲区 # 按顺序重复 num_steps = 1000 # 训练步数 for step in range(num_steps): # 取单个文档,分词,两侧用BOS特殊token包裹 doc = docs[step % len(docs)] tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS] n = min(block_size, len(tokens) - 1) # 将 token 序列通过模型前向传播,一路构建计算图直到损失 keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] losses = [] for pos_id in range(n): token_id, target_id = tokens[pos_id], tokens[pos_id + 1] logits = gpt(token_id, pos_id, keys, values) probs = softmax(logits) loss_t = -probs[target_id].log() losses.append(loss_t) loss = (1 / n) * sum(losses) # 文档序列上的最终平均损失。愿你的损失很低。 # 反向传播损失,计算所有模型参数的梯度 loss.backward() # Adam 优化器更新:基于对应梯度更新模型参数 lr_t = learning_rate * (1 - step / num_steps) # 线性学习率衰减 for i, p in enumerate(params): m[i] = beta1 * m[i] + (1 - beta1) * p.grad v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2 m_hat = m[i] / (1 - beta1 ** (step + 1)) v_hat = v[i] / (1 - beta2 ** (step + 1)) p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam) p.grad = 0 print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}")让我们逐一讲解每个部分:分词。每个训练步选取一个文档,两侧用BOS包裹:名字"emma"变成 [BOS, e, m, m, a, BOS]。模型的任务是根据前面的 token 预测下一个 token。前向传播和损失。我们将 token 一个接一个地送入模型,同时构建 KV cache。在每个位置,模型输出27个 logits,通过 softmax 转换为概率。每个位置的损失是正确下一个 token 的负对数概率:-log p(target)。这叫做交叉熵损失。直觉上,损失衡量了误预测的程度:模型对实际出现的下一个 token 有多惊讶。如果模型将概率1.0赋给正确的 token,它完全不惊讶,损失为0。如果它赋予接近0的概率,模型非常惊讶,损失趋向 +∞。我们对文档中各位置的损失取平均得到一个标量损失。反向传播。一次 loss.backward() 调用就能通过整个计算图运行反向传播,从损失一直回到 softmax、模型和每个参数。之后,每个参数的 .grad 告诉我们如何改变它来降低损失。Adam 优化器。我们可以直接做 p.data -= lr * p.grad(梯度下降),但 Adam 更智能。它为每个参数维护两个运行平均值:m 跟踪最近梯度的均值(动量,像滚动的球),v 跟踪最近平方梯度的均值(每个参数自适应学习率)。m_hat 和 v_hat 是偏差修正,考虑到 m 和 v 初始化为零需要预热。学习率在训练过程中线性衰减。更新后,我们将 .grad 重置为0以备下一步。经过1,000步训练,损失从约3.3(在27个token中随机猜测:-log(1/27) ≈ 3.3)下降到约2.37。越低越好,最低可能是0(完美预测),所以仍有改进空间,但模型显然在学习名字的统计模式。推理(Inference)训练完成后,我们可以从模型中采样新名字。参数被冻结,我们只需在循环中运行前向传播,将每个生成的 token 反馈作为下一个输入:temperature = 0.5 # 在 (0, 1] 之间,控制生成文本的"创造力",从低到高 print("\n--- inference (new, hallucinated names) ---") for sample_idx in range(20): keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] token_id = BOS sample = [] for pos_id in range(block_size): logits = gpt(token_id, pos_id, keys, values) probs = softmax([l / temperature for l in logits]) token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0] if token_id == BOS: break sample.append(uchars[token_id]) print(f"sample {sample_idx+1:2d}: {''.join(sample)}")每个样本从BOS token开始,告诉模型"开始一个新名字"。模型产生27个 logits,我们转换为概率,然后按这些概率随机采样一个 token。该 token 被反馈作为下一个输入,重复直到模型产生BOS(意味着"我完成了")或达到最大序列长度。温度(temperature) 参数控制随机性。在 softmax 之前,我们将 logits 除以温度。温度为1.0时直接从模型学到的分布中采样。较低的温度(如这里的0.5)使分布更尖锐,让模型更保守,更可能选择其首选项。接近0的温度会总是选择最可能的 token(贪心解码)。较高的温度使分布更平坦,产生更多样但可能不太连贯的输出。运行它你只需要Python(不需要pip install,没有依赖):python train.py脚本在我的MacBook上大约运行1分钟。你会看到每一步打印的损失:train.py num docs: 32033 vocab size: 27 num params: 4192 step 1 / 1000 | loss 3.3660 step 2 / 1000 | loss 3.4243 step 3 / 1000 | loss 3.1778 step 4 / 1000 | loss 3.0664 step 5 / 1000 | loss 3.2209 step 6 / 1000 | loss 2.9452 step 7 / 1000 | loss 3.2894 step 8 / 1000 | loss 3.3245 step 9 / 1000 | loss 2.8990 step 10 / 1000 | loss 3.2229 step 11 / 1000 | loss 2.7964 step 12 / 1000 | loss 2.9345 step 13 / 1000 | loss 3.0544 ...观察它从约3.3(随机)下降到约2.37。这个数字越低,说明网络对序列中下一个 token 的预测已经越准确。训练结束时,训练 token 序列的统计模式知识被蒸馏到模型参数中。固定这些参数,我们现在可以生成新的、幻觉出的名字。作为替代方案,你可以直接在 Google Colab 笔记本 上运行它,并向 Gemini 提问。试着玩一下这个脚本!你可以尝试不同的数据集。或者你可以训练更长时间(增加 num_steps)或增大模型来获得越来越好的结果。进阶路径(Progression)要查看代码逐步构建的过程(像洋葱一样一层层剥开),建议的进阶路径如下:文件新增内容train0.py二元组(Bigram)计数表——无神经网络,无梯度train1.pyMLP + 手动梯度(数值和解析)+ SGDtrain2.py自动微分(Value类)——替代手动梯度train3.py位置嵌入 + 单头注意力 + rmsnorm + 残差连接train4.py多头注意力 + 层循环——完整GPT架构train5.pyAdam优化器——这就是 train.py我创建了一个叫 build_microgpt.py 的 Gist,在其修订历史中你可以看到所有这些版本以及每一步之间的差异。我认为这可能是逐步了解代码库的一种有用方式,你一次添加一个组件。真实世界(Real Stuff)microgpt 包含了训练和运行GPT的完整算法精髓。但从这到像ChatGPT这样的生产级LLM,有一长串需要改变的东西。这些都不会改变核心算法和整体布局,但它们是使其在规模上真正工作的关键。按同样的章节顺序:数据。与32K短名字不同,生产模型训练于数万亿 token 的互联网文本:网页、书籍、代码等。数据经过去重、质量过滤,并在不同领域之间仔细混合。分词器。与单个字符不同,生产模型使用子词分词器如BPE(字节对编码),它学习将频繁共同出现的字符序列合并为单个 token。常见单词如"the"变成一个 token,稀有单词被拆分成片段。这给出约100K token 的词汇表,效率更高,因为模型每个位置看到更多内容。自动微分。microgpt 在纯Python中操作标量 Value 对象。生产系统使用张量(大型多维数字数组),在GPU/TPU上运行,每秒执行数十亿次浮点运算。PyTorch等库处理张量上的自动微分,FlashAttention等CUDA内核融合多个操作以提速。数学是相同的,只是对应于并行处理的许多标量。架构。microgpt有4,192个参数。GPT-4级别的模型有数千亿个。总体来说,它是一个非常相似的Transformer神经网络,只是更宽(嵌入维度10,000+)和更深(100+层)。现代LLM还引入了更多类型的乐高块并改变它们的顺序:例如 RoPE(旋转位置嵌入)替代学习的位置嵌入,GQA(分组查询注意力)减少 KV cache 大小,门控线性激活替代 ReLU,专家混合(MoE)层等。但注意力(通信)和 MLP(计算)在残差流上交替的核心结构保持良好。训练。与每步一个文档不同,生产训练使用大批次(每步数百万 token)、梯度累积、混合精度(float16/bfloat16)和仔细的超参数调优。训练一个前沿模型需要数千个GPU运行数月。优化。microgpt使用简单的线性学习率衰减的Adam,仅此而已。在规模上,优化本身成为一门学科。模型以降低精度(bfloat16甚至fp8)在大型GPU集群上训练以提高效率,这引入了自己的数值挑战。优化器设置(学习率、权重衰减、beta参数、预热计划、衰减计划)必须精确调优,正确值取决于模型大小、批次大小和数据集组成。缩放法则(如Chinchilla)指导如何在模型大小和训练 token 数之间分配固定的计算预算。在规模上任何这些细节出错都可能浪费数百万美元的计算,因此团队在投入完整训练之前会运行大量较小规模的实验来预测正确设置。后训练。从训练中产生的基础模型(称为"预训练"模型)是一个文档补全器,不是聊天机器人。将其变成ChatGPT分两个阶段。第一,SFT(监督微调):你只需将文档替换为精心策划的对话并继续训练。算法上没有任何变化。第二,RL(强化学习):模型生成回复,回复被评分(由人类、另一个"裁判"模型或算法),模型从该反馈中学习。从根本上说,模型仍然在文档上训练,但这些文档现在由模型自身产生的 token 组成。推理。为数百万用户提供模型服务需要自己的工程栈:请求批处理、KV cache管理和分页(vLLM等)、推测解码加速、量化(以int8/int4代替float16运行)减少内存,以及将模型分布到多个GPU。从根本上说,我们仍然在预测序列中的下一个 token,只是花了大量工程来使其更快。所有这些都是重要的工程和研究贡献,但如果你理解了 microgpt,你就理解了算法的精髓。常见问题(FAQ)模型"理解"了什么吗?这是一个哲学问题,但从机制上看:没有魔法发生。模型是一个大型数学函数,将输入 token 映射到下一个 token 的概率分布。在训练过程中,参数被调整以使正确的下一个 token 概率更高。这是否构成"理解"由你来判断,但机制完全包含在上面的200行代码中。为什么它有效?模型有数千个可调参数,优化器每步微调它们以使损失下降。经过许多步骤,参数稳定到捕获数据统计规律性的值。对于名字来说,这意味着:名字通常以辅音开头,"qu"倾向于一起出现,名字很少有三个连续辅音等。模型不学习显式规则,它学习一个恰好反映这些规则的概率分布。这和ChatGPT有什么关系?ChatGPT是同样的核心循环(预测下一个 token、采样、重复)的大规模放大版,加上后训练使其具有对话能力。当你和它聊天时,系统提示词、你的消息和它的回复都只是序列中的 token。模型在一个 token 接一个 token 地补全文档,就像 microgpt 补全一个名字一样。"幻觉"是怎么回事?模型通过从概率分布中采样来生成 token。它没有真理的概念,它只知道什么序列在训练数据的统计意义上是合理的。microgpt"幻觉"出一个像"karia"这样的名字,和ChatGPT自信地说出一个错误事实是同样的现象。两者都是听起来合理但碰巧不是真实的补全。为什么这么慢?microgpt在纯Python中一次处理一个标量。一个训练步需要几秒钟。在GPU上执行相同的数学运算可以并行处理数百万个标量,速度快几个数量级。我能让它生成更好的名字吗?可以。训练更长时间(增加 num_steps),增大模型(n_embd、n_layer、n_head),或使用更大的数据集。这些是在规模上同样重要的旋钮。如果我更换数据集会怎样?模型会学习数据中的任何模式。换成城市名、宝可梦名、英语单词或短诗的文件,模型就会学习生成那些。其余代码不需要改变。社区评论与讨论总结microgpt 发布后在技术社区引起了广泛关注和热烈讨论。以下是来自 Hacker News、Twitter/X 和 GitHub 的主要评论总结:核心反馈高度赞誉教育价值:社区普遍认为 microgpt 是理解LLM的最佳教育资源之一。有评论指出,许多使用LLM两年的开发者在阅读这200行代码后,才真正理解了"黑盒"内部到底发生了什么。正如一位评论者所说,在 MicroGPT、nanoGPT 和 Zero to Hero 系列之间,Karpathy 为机器学习教育所做的贡献可能超过了大多数大学课程。社区移植热潮:发布后两周内,开发者们将 microgpt 移植到了 Rust、C++、Go 和 Zig 等多种语言。这说明了代码的清晰度和教育意义使得不同语言背景的开发者都能理解并重新实现它。Hacker News 讨论要点关于简化与实用:一些讨论者注意到,microgpt 出色地展示了 GPT 的核心思想其实相当简单。正如一位评论者所言,要做有用的事情需要大量数据,然后一切开始变得越来越复杂。关于去除自动微分的优化:有用户分享了一个有趣的发现——如果去掉自动微分并编写显式的反向传播,训练时间从40秒降到了5秒。关于可视化:受 microgpt 启发,有开发者创建了浏览器内可视化工具,让用户可以实时观察网络中的激活传播,并点击各个组件获得解释。社区认为这比 bbycroft.net/llm 的LLM可视化更容易理解,因为可以实际运行训练循环。关于字符级 vs token 级分词:部分评论者建议文章应更明确地指出 microgpt 使用字符级分词而非 token 级分词的区别和权衡。关于 AI 民主化:多位评论者强调这个项目让AI世界变得更有趣、更民主化。一位博士研究者认为这是"AI透明性的基础性时刻",展示了"智能"不需要依赖于复杂的技术栈。Twitter/X 讨论Karpathy 的原推文获得了大量转发和讨论。许多知名AI从业者赞赏了该项目将GPT完整算法浓缩到一个屏幕可显示的代码量中的优雅性。人们特别欣赏的是,这个项目证明了你可以在一次阅读中真正理解LLM的工作原理,而不是把它们当作黑盒。本文由 Andrej Karpathy 撰写,翻译整理自原始博客文章。社区评论总结来源于 Hacker News、Hacker News 原帖 以及 Twitter/X 上的讨论。{lamp/}microgpt —— 用200行纯Python从零实现GPT的训练和推理=================================================这是 Andrej Karpathy 的 microgpt 项目的详细注释版本。原始代码地址:https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95【适合谁看?】对 AI / 大语言模型(LLM)好奇的编程初学者想搞清楚 ChatGPT 底层到底在干什么的人有一些 Python 基础但没有机器学习背景的朋友【一句话总结】这个文件做了什么:读入一堆英文名字 → 训练一个迷你 GPT 模型 → 让模型"编造"出新的名字。ChatGPT 做的是完全一样的事——只不过它的"名字"换成了整个互联网的文本,模型大了一千万倍。【核心思路(5步)】数据准备:把文本变成数字序列自动微分:让计算机自动算出"每个参数该往哪个方向调"模型定义:搭建一个 Transformer 神经网络训练循环:反复喂数据、算误差、调参数推理生成:用训练好的模型生成新文本@karpathy 原作 | 中文详细注释版{lamp/}# ============================================================================ # 第0部分:导入标准库(注意:没有任何第三方依赖!不需要 pip install 任何东西) # ============================================================================ import os # 用于检查文件是否存在(os.path.exists) import math # 用于数学运算(math.log 对数, math.exp 指数) import random # 用于生成随机数(初始化参数、采样等) # 设置随机种子,保证每次运行结果一致(方便调试和复现) # 如果去掉这行,每次运行生成的名字会不一样 random.seed(42) # ============================================================================ # 第1部分:数据集(Dataset) # ============================================================================ # 【目标】准备训练数据——32,000个英文名字 # # 想象一下:你要教一个完全不懂英语的外星人"什么样的字母组合看起来像人名"。 # 你的做法就是给它看几万个真实名字,让它自己找规律。 # 这里的 GPT 模型就是那个"外星人"。 # ============================================================================ # 如果本地没有数据文件,就从网上下载 if not os.path.exists('input.txt'): import urllib.request # Python 内置的网络下载工具 names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt' urllib.request.urlretrieve(names_url, 'input.txt') # 下载完成后,input.txt 里的内容长这样: # emma # olivia # ava # isabella # ... (共约32,000个名字,每行一个) # 读取文件,每行一个名字,去掉空白字符,存成列表 # 结果示例:docs = ["emma", "olivia", "ava", "isabella", ...] docs = [line.strip() for line in open('input.txt') if line.strip()] # 随机打乱顺序(让训练时每次看到的名字顺序不同,有助于学习) random.shuffle(docs) print(f"num docs: {len(docs)}") # 打印:num docs: 32033 # ============================================================================ # 第2部分:分词器(Tokenizer) # ============================================================================ # 【目标】把文字转换成数字,因为神经网络只能处理数字 # # 类比:每个字母相当于一个"代号" # a → 0, b → 1, c → 2, ..., z → 25 # BOS(特殊标记)→ 26 # # 为什么需要 BOS? # BOS = Beginning of Sequence(序列开始标记) # 它就像一个"开始/结束信号"。训练时,每个名字两边都加上 BOS: # "emma" → [BOS, e, m, m, a, BOS] # 这样模型就知道:看到 BOS 就意味着"一个新名字要开始了"或"名字结束了" # ============================================================================ # sorted(set(...)) 收集所有出现过的字符并排序 # 对于名字数据集,结果就是 ['a', 'b', 'c', ..., 'z'] uchars = sorted(set(''.join(docs))) # BOS 的 token id 设为字符总数(这里是 26) BOS = len(uchars) # 词汇表大小 = 26个字母 + 1个BOS = 27 vocab_size = len(uchars) + 1 print(f"vocab size: {vocab_size}") # 打印:vocab size: 27 # ============================================================================ # 第3部分:自动微分引擎(Autograd) # ============================================================================ # 【这是整个代码中最核心、最难理解的部分,但也是最优雅的部分】 # # ★ 问题:我们怎么知道该如何调整模型的参数? # # 举个生活例子: # 假设你在调收音机的旋钮想收到一个电台。你稍微往右拧了一点,信号变好了。 # 那你就知道:应该继续往右拧。 # 如果信号变差了,你就往左拧。 # "信号变好还是变差"以及"变化了多少"——这就是"梯度"。 # # 自动微分做的事情: # 1. 记录所有计算过程(构建"计算图") # 2. 从最终结果(损失)往回推,自动算出每个参数的梯度 # 3. 梯度告诉我们:这个参数该增大还是减小,以及幅度多大 # # 这就是 PyTorch 的 loss.backward() 在做的事情,只不过这里我们自己从头实现。 # ============================================================================ class Value: """ Value 类:包装一个数字,让它具备自动求梯度的能力。 你可以把 Value 想象成一个"智能数字": - 它知道自己的值是多少(data) - 它知道自己是怎么被计算出来的(_children, _local_grads) - 训练时,它能自动算出"如果我变大一点点,最终损失会怎么变"(grad) 生活类比: 普通数字就像一张照片——只有最终结果。 Value 就像一段录像——记录了整个计算过程,可以倒放(反向传播)。 """ # __slots__ 是 Python 的内存优化技巧 # 告诉 Python:"这个类只有这4个属性,不需要为其他属性预留空间" # 因为我们会创建成千上万个 Value 对象,这能节省不少内存 __slots__ = ('data', 'grad', '_children', '_local_grads') def __init__(self, data, children=(), local_grads=()): self.data = data # ↑ 这个节点的实际数值(前向传播时计算得到) # 例如:如果 c = a + b,且 a.data=3, b.data=4,则 c.data=7 self.grad = 0 # ↑ 梯度:损失函数对这个节点的导数 ∂Loss/∂self # 初始为0,在反向传播(backward)时被计算 # 它的含义是:"如果把这个值增大一丢丢,损失会变化多少" # grad > 0 → 增大此值会增大损失 → 应该减小它 # grad < 0 → 增大此值会减小损失 → 应该增大它 self._children = children # ↑ 这个节点的"父母"(产生它的输入节点) # 例如:c = a + b,则 c._children = (a, b) # 这形成了一个计算图(有向无环图 DAG) self._local_grads = local_grads # ↑ 局部梯度:这个运算对每个输入的偏导数 # 例如:c = a + b # ∂c/∂a = 1, ∂c/∂b = 1 → local_grads = (1, 1) # 例如:c = a * b(假设 a=3, b=4) # ∂c/∂a = b = 4, ∂c/∂b = a = 3 → local_grads = (4, 3) # ======================== # 6种基本运算("乐高积木") # ======================== # 整个 GPT 不管多复杂,都是由这6种基本运算组合而成的。 # 每种运算做两件事: # 1. 计算结果(前向传播) # 2. 记录局部梯度(为反向传播做准备) def __add__(self, other): """ 加法:c = a + b 前向:c.data = a.data + b.data 局部梯度:∂c/∂a = 1, ∂c/∂b = 1 直觉:a 或 b 增加1,c 也增加1(一比一传递) """ other = other if isinstance(other, Value) else Value(other) # ↑ 如果 other 是普通数字(如 a + 3),先包装成 Value return Value(self.data + other.data, (self, other), (1, 1)) # ↑ 计算结果 ↑ 子节点 ↑ 局部梯度都是1 def __mul__(self, other): """ 乘法:c = a * b 前向:c.data = a.data * b.data 局部梯度:∂c/∂a = b, ∂c/∂b = a 直觉:a * b 对 a 的敏感度是 b 的大小(反过来也一样) 比如 3 * 4 = 12,如果 a 从3变成4,c 变成 16,增加了4(= b 的值) """ other = other if isinstance(other, Value) else Value(other) return Value(self.data * other.data, (self, other), (other.data, self.data)) # ↑ ∂c/∂a=b ↑ ∂c/∂b=a def __pow__(self, other): """ 幂运算:c = a^n (other 是一个普通数字,不是 Value) 前向:c.data = a.data ^ n 局部梯度:∂c/∂a = n * a^(n-1) (幂函数求导法则) 例子:a^3 的导数是 3*a^2 """ return Value(self.data**other, (self,), (other * self.data**(other-1),)) def log(self): """ 自然对数:c = ln(a) 前向:c.data = ln(a.data) 局部梯度:∂c/∂a = 1/a 用途:计算交叉熵损失时需要 -log(概率) """ return Value(math.log(self.data), (self,), (1/self.data,)) def exp(self): """ 指数函数:c = e^a 前向:c.data = e^(a.data) 局部梯度:∂c/∂a = e^a (指数函数的导数还是自己!) 用途:softmax 中需要对 logits 取 exp """ return Value(math.exp(self.data), (self,), (math.exp(self.data),)) def relu(self): """ ReLU(Rectified Linear Unit,修正线性单元):c = max(0, a) 这是神经网络中最常用的"激活函数"之一。 作用:如果输入是正数,原样输出;如果是负数,输出0。 就像一个"只让正数通过"的阀门。 前向:c.data = max(0, a.data) 局部梯度:a > 0 时为1,a ≤ 0 时为0 直觉:正数区域梯度畅通无阻,负数区域梯度被"关闭" """ return Value(max(0, self.data), (self,), (float(self.data > 0),)) # ======================== # 辅助运算(由上面6种基本运算组合得到) # ======================== # 这些方法让 Value 对象可以像普通数字一样使用 +, -, *, / 运算符 def __neg__(self): return self * -1 # -a = a * (-1) def __radd__(self, other): return self + other # 3 + a → a + 3 def __sub__(self, other): return self + (-other) # a - b = a + (-b) def __rsub__(self, other): return other + (-self) # 3 - a → 3 + (-a) def __rmul__(self, other): return self * other # 3 * a → a * 3 def __truediv__(self, other): return self * other**-1 # a / b = a * b^(-1) def __rtruediv__(self, other): return other * self**-1 # 3 / a = 3 * a^(-1) # ======================== # 反向传播(Backward Pass)—— 自动求梯度的核心 # ======================== def backward(self): """ 反向传播:从当前节点(通常是损失函数)开始,自动计算所有节点的梯度。 【算法流程】 1. 构建拓扑排序(确保处理某个节点时,所有依赖它的下游节点已处理完) 2. 从损失节点开始,设 grad = 1(∂L/∂L = 1) 3. 按逆拓扑序遍历每个节点,用链式法则传递梯度 【链式法则直觉】 假设有连锁反应:a → b → c → Loss - Loss 对 c 的敏感度是 ∂L/∂c(已知) - c 对 b 的敏感度是 ∂c/∂b(局部梯度,前向时已记录) - 那么 Loss 对 b 的敏感度 = ∂L/∂c × ∂c/∂b(两个敏感度相乘) 就像多米诺骨牌:推倒第一张牌的力量,会沿着链条传递下去。 """ # 第1步:拓扑排序 # 把计算图中的所有节点排成一个线性序列,使得每个节点排在它的所有子节点之后 # 这样反向遍历时,处理到某个节点时,它的"下游"(离损失更近的方向)都已算完了 topo = [] visited = set() # 记录已访问的节点,避免重复 def build_topo(v): """深度优先搜索,后序遍历,构建拓扑排序""" if v not in visited: visited.add(v) for child in v._children: # 先递归处理所有子节点 build_topo(child) topo.append(v) # 子节点都处理完了,再把自己加入 build_topo(self) # 第2步:起点——损失对自身的梯度是1 # 因为 ∂L/∂L = 1(任何东西对自身的变化率是1) self.grad = 1 # 第3步:反向遍历,传递梯度 for v in reversed(topo): # 从损失节点开始,往输入方向走 for child, local_grad in zip(v._children, v._local_grads): # 链式法则核心公式: # ∂L/∂child += ∂v/∂child × ∂L/∂v # 即:子节点的梯度 += 局部梯度 × 当前节点的梯度 # # 为什么是 += 而不是 = ? # 因为一个节点可能被多个下游节点使用(图分叉了) # 比如 a 同时参与了 c = a*b 和 d = a+b # 那么 a 的梯度 = 通过 c 传来的 + 通过 d 传来的 child.grad += local_grad * v.grad # ============================================================================ # 第4部分:模型参数初始化 # ============================================================================ # 【目标】创建模型的所有可学习参数,初始化为小随机数 # # 类比:这些参数就像收音机上的几千个旋钮,初始时随机拨了一下。 # 训练过程就是不断微调这些旋钮,直到收音机能放出好听的音乐。 # # 为什么不初始化为0? # 如果所有参数都是0,那所有神经元的输出都一样,梯度也一样, # 它们就永远无法分化出不同的功能——就像一个合唱团所有人唱同一个音。 # 小随机数打破了这种"对称性"。 # ============================================================================ # --- 超参数(Hyperparameters)--- # 这些是我们手动设定的"设计图纸"参数,控制模型的大小和形状 n_layer = 1 # Transformer 的层数(深度)。GPT-3 有96层,我们只用1层 n_embd = 16 # 嵌入维度(宽度)。GPT-3 是 12288,我们只用16 block_size = 16 # 最长能处理的序列长度。最长的名字是15个字符,16够用了 n_head = 4 # 注意力头的数量。多个头可以关注不同类型的模式 head_dim = n_embd // n_head # 每个头的维度 = 16 / 4 = 4 # 创建参数矩阵的工具函数 # 每个参数是一个 Value 对象,初始值从 N(0, 0.08²) 高斯分布中采样 # nout × nin 的矩阵 = nout 行、nin 列 matrix = lambda nout, nin, std=0.08: [ [Value(random.gauss(0, std)) for _ in range(nin)] # 一行有 nin 个参数 for _ in range(nout) # 共 nout 行 ] # --- 参数字典(state_dict)--- # 借用 PyTorch 的命名习惯,按名字存储所有参数矩阵 state_dict = { 'wte': matrix(vocab_size, n_embd), # Token嵌入表:27×16 # ↑ 每个 token(字母或BOS)对应一个16维向量 # 你可以理解为:给26个字母+BOS 各分配一个"身份证",身份证上有16个数字 # 这些数字一开始是随机的,训练后会变得有意义(相似的字母距离更近) 'wpe': matrix(block_size, n_embd), # 位置嵌入表:16×16 # ↑ 每个位置(0到15)对应一个16维向量 # 告诉模型"这个字母在名字中的第几个位置" # 位置很重要!名字开头和结尾的字母分布完全不同 'lm_head': matrix(vocab_size, n_embd) # 输出投影:27×16 # ↑ 把模型内部的16维向量转换回27个分数(logits) # 每个分数对应一个 token,分数越高 → 模型越觉得这个 token 应该出现 } # 每一层 Transformer 的参数 for i in range(n_layer): # --- 注意力(Attention)的参数 --- state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd) # Query 权重:16×16 state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd) # Key 权重:16×16 state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd) # Value 权重:16×16 state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd) # 输出投影:16×16 # ↑ Q/K/V 是注意力机制的三个核心角色(后面会详细解释) # --- MLP(多层感知机)的参数 --- state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd) # 第一层:64×16(扩展4倍) state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd) # 第二层:16×64(压缩回来) # ↑ MLP 先把16维扩展到64维(给模型更大的"思考空间"),再压缩回16维 # 把所有参数展平成一个大列表,方便优化器统一遍历 # 想象把所有旋钮编了号,优化器按编号一个一个调 params = [p for mat in state_dict.values() for row in mat for p in row] print(f"num params: {len(params)}") # 打印:num params: 4192 # 我们的模型有 4,192 个参数。GPT-2 有 16 亿个,GPT-4 有数千亿个。 # 算法完全一样,只是规模天差地别。 # ============================================================================ # 第5部分:模型架构(GPT Model) # ============================================================================ # 【目标】定义 GPT 模型的计算过程 # # 架构遵循 GPT-2,做了一些简化: # - LayerNorm → RMSNorm(更简单的归一化) # - GeLU → ReLU(更简单的激活函数) # - 去掉了所有偏置(bias) # # 数据流: # 输入 token → 嵌入 → [归一化 → 注意力 → 残差] → [归一化 → MLP → 残差] → 输出 logits # # 直觉: # 注意力(Attention)= 不同位置的 token 之间"互相交流信息" # MLP = 每个 token 自己"思考消化"刚得到的信息 # 两者交替进行,就像一个讨论会:先讨论(注意力),再各自思考(MLP),再讨论... # ============================================================================ def linear(x, w): """ 线性变换:y = W × x(矩阵乘向量) 参数: x: 输入向量,长度为 nin 的列表 [Value, Value, ...] w: 权重矩阵,nout × nin 的二维列表 返回: 输出向量,长度为 nout 的列表 例子: 如果 x = [1, 2, 3],w = [[1,0,0], [0,1,0]] 结果 = [1*1+0*2+0*3, 0*1+1*2+0*3] = [1, 2] 这是神经网络最基本的操作。每一行做一个点积(dot product)。 """ return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w] # ↑ 对 w 的每一行 wo,计算 wo·x(点积) def softmax(logits): """ Softmax 函数:把任意数字变成概率分布 输入:一组"分数"(logits),可以是任意实数,比如 [2.0, 1.0, 0.1] 输出:概率分布,所有值在0-1之间且求和为1,比如 [0.66, 0.24, 0.10] 公式:P(i) = exp(z_i) / Σ exp(z_j) 为什么要减去 max_val?(log-sum-exp trick) 假设 logits 里有个很大的数比如 1000, exp(1000) 会直接变成无穷大(数值溢出)! 但 exp(1000 - 1000) = exp(0) = 1,完全没问题。 减去最大值不改变 softmax 的结果(因为分子分母同时乘除相同的数)。 """ max_val = max(val.data for val in logits) # 找到最大值 exps = [(val - max_val).exp() for val in logits] # 减去最大值后取 exp total = sum(exps) # 求和 return [e / total for e in exps] # 归一化为概率 def rmsnorm(x): """ RMSNorm(Root Mean Square Normalization,均方根归一化) 作用:把向量的"大小"归一化到大约为1。 为什么需要它? 想象你在传话游戏中传一个数字。每传一次可能放大或缩小一点。 传100次后,数字可能变得巨大或微小到接近0。 归一化就像每传一次后重新校准大小,防止数字失控。 在神经网络中,数据经过很多层变换,如果不归一化, 激活值可能会"爆炸"(变得极大)或"消失"(变得极小),导致训练失败。 公式:x̂_i = x_i / √(mean(x²) + ε) 其中 ε = 1e-5 是个很小的数,防止除以0。 """ ms = sum(xi * xi for xi in x) / len(x) # 计算均方值(mean square) scale = (ms + 1e-5) ** -0.5 # 1/√(ms + ε) return [xi * scale for xi in x] # 每个元素除以 RMS def gpt(token_id, pos_id, keys, values): """ GPT 模型:给定一个 token 和它的位置,预测下一个 token 的概率分布。 参数: token_id: 当前输入 token 的编号(0-26) pos_id: 当前 token 在序列中的位置(0-15) keys: KV缓存中的 Key(之前位置的"钥匙") values: KV缓存中的 Value(之前位置的"信息") 返回: logits: 27个分数,每个对应词汇表中的一个 token 分数越高 → 模型越认为该 token 应该出现在下一个位置 【完整数据流】 1. 嵌入(Embedding) "我是字母 e,我在第2个位置" → 变成一个16维数字向量 2. 注意力(Attention)—— token 之间的"对话" 每个 token 问自己:"之前的 token 中,哪些和我相关?" 然后从相关的 token 那里获取信息 3. MLP —— 每个 token 自己"思考" 消化刚从其他 token 获取的信息 4. 输出 把最终的16维向量转换为27个分数 """ # ---- 第1步:嵌入(Embedding)---- # 把 token_id 和 pos_id 分别查表,得到两个16维向量,然后相加 tok_emb = state_dict['wte'][token_id] # Token 嵌入:查找"这个字母的身份证" pos_emb = state_dict['wpe'][pos_id] # 位置嵌入:查找"这个位置的特征" x = [t + p for t, p in zip(tok_emb, pos_emb)] # 两者相加 → 16维向量 # 现在 x 同时包含了"我是什么字母"和"我在第几个位置"的信息 x = rmsnorm(x) # 归一化,稳定数值 # ---- 第2步 & 第3步:Transformer 层 ---- for li in range(n_layer): # 遍历每一层(我们只有1层) # ======================================== # 2A) 多头注意力(Multi-Head Attention) # ======================================== # 【核心直觉】 # 注意力机制让当前 token "看到"之前所有 token 的信息。 # # 三个角色(QKV): # Q (Query, 查询): "我在找什么样的信息?" # K (Key, 键/钥匙):"我拥有什么样的信息?" # V (Value, 值): "如果你选中我,我能给你什么?" # # 过程: # 1. 当前 token 生成一个 Query:"我在找以元音开头的信息" # 2. 每个历史 token 都有一个 Key:"我包含辅音相关信息" # 3. Query 和每个 Key 做点积 → 得到"相关度分数" # 4. Softmax 归一化分数 → 注意力权重(加权比例) # 5. 用权重对所有历史 token 的 Value 加权求和 → 获取信息 # # 多头(Multi-Head): # 4个头各自独立做注意力,关注不同类型的模式。 # 比如头1关注"前一个字母是什么",头2关注"名字开头是什么"。 # 最后把4个头的结果拼起来。 x_residual = x # 保存输入,用于残差连接 x = rmsnorm(x) # 归一化 # 生成 Q, K, V(三次线性变换) q = linear(x, state_dict[f'layer{li}.attn_wq']) # Query:16维 k = linear(x, state_dict[f'layer{li}.attn_wk']) # Key:16维 v = linear(x, state_dict[f'layer{li}.attn_wv']) # Value:16维 # 把当前位置的 K 和 V 加入缓存 # 这样下一个 token 处理时,可以"看到"当前 token 的信息 keys[li].append(k) values[li].append(v) x_attn = [] # 存储所有注意力头的输出 for h in range(n_head): # 遍历每个注意力头 # 每个头只看16维中自己负责的那4维(head_dim = 4) hs = h * head_dim # 起始索引 q_h = q[hs:hs+head_dim] # 当前 token 的 Query 片段 k_h = [ki[hs:hs+head_dim] for ki in keys[li]] # 所有历史 token 的 Key 片段 v_h = [vi[hs:hs+head_dim] for vi in values[li]] # 所有历史 token 的 Value 片段 # 计算注意力分数:Q 和每个 K 的点积,除以 √(head_dim) 缩放 # 为什么要除以 √(head_dim)? # 点积的结果大小和维度成正比。如果不缩放,维度大时点积会很大, # softmax 后分布会极端尖锐(接近 one-hot),梯度接近0,训练不动。 # 除以 √d 让方差回到1,softmax 分布适度平滑。 attn_logits = [ sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 for t in range(len(k_h)) ] # Softmax → 注意力权重(加起来等于1的概率分布) attn_weights = softmax(attn_logits) # 例如:attn_weights = [0.1, 0.3, 0.6] # 意味着当前 token 对位置0关注10%,位置1关注30%,位置2关注60% # 用注意力权重对 V 加权求和 → 该头的输出 head_out = [ sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim) ] # 直觉:从历史 token 中"提取"信息,关注度高的贡献更大 x_attn.extend(head_out) # 把这个头的4维输出追加到总输出 # 所有头的输出拼接后(4头×4维=16维),做一次线性变换混合 x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) # ★ 残差连接(Residual Connection)★ # x = attention_output + original_input # 为什么?两个好处: # 1. 梯度直通:反向传播时,梯度可以直接跳过注意力层回到输入 # (加法的梯度是1,不会衰减),防止"梯度消失" # 2. 学的是"增量":注意力层只需要学"该在原始信息上加什么",而不是从零开始 x = [a + b for a, b in zip(x, x_residual)] # ======================================== # 2B) MLP(多层感知机) # ======================================== # 注意力负责 token 之间"交流",MLP 负责每个 token 独立"思考"。 # # 结构:16维 → 64维(扩展,获得更大的表达空间) # → ReLU(非线性激活,让网络能学复杂模式) # → 16维(压缩回来) # # 为什么需要非线性(ReLU)? # 如果只有线性变换(矩阵乘法),不管叠多少层,效果都等于一个矩阵。 # 加入 ReLU 后,网络就能学习复杂的非线性模式。 x_residual = x # 再次保存用于残差连接 x = rmsnorm(x) # 归一化 x = linear(x, state_dict[f'layer{li}.mlp_fc1']) # 16维 → 64维 x = [xi.relu() for xi in x] # ReLU 激活 x = linear(x, state_dict[f'layer{li}.mlp_fc2']) # 64维 → 16维 x = [a + b for a, b in zip(x, x_residual)] # 残差连接 # ---- 第4步:输出层 ---- # 把16维的隐藏状态投影到27维(词汇表大小) # 每个维度对应一个 token 的"分数"(logit) logits = linear(x, state_dict['lm_head']) return logits # logits 示例:[2.1, -0.5, 1.3, ..., 0.8](27个数字) # 数字越大 → 模型越觉得对应的字母应该是下一个 # ============================================================================ # 第6部分:训练循环(Training Loop) # ============================================================================ # 【目标】通过反复看数据来调整参数,让模型学会名字的统计规律 # # 每一步训练做4件事: # 1. 选一个名字,转成数字序列 # 2. 让模型逐个预测下一个字母(前向传播) # 3. 计算预测有多差(损失),然后反向传播算梯度 # 4. 用 Adam 优化器微调所有参数 # # 类比: # 这就像背单词:看一个单词 → 尝试拼写 → 对答案 → 调整记忆 → 重复 # ============================================================================ # --- Adam 优化器 --- # Adam 是目前最流行的优化算法之一(几乎所有LLM都用它) # 比普通梯度下降更智能,因为它有两个"记忆": # m(动量/momentum):梯度的移动平均 → 平滑方向,减少震荡 # v(自适应学习率):梯度平方的移动平均 → 让每个参数有自己合适的步长 # 梯度一直很大的参数 → 步子小一点(已经在快速变化了) # 梯度一直很小的参数 → 步子大一点(需要加速) learning_rate = 0.01 # 学习率:每步调整参数的"步幅" beta1 = 0.85 # 动量的衰减系数(通常0.9左右) beta2 = 0.99 # 二阶矩的衰减系数(通常0.999左右) eps_adam = 1e-8 # 防止除以0的小数 m = [0.0] * len(params) # 一阶矩缓冲区(梯度的移动平均),初始为0 v = [0.0] * len(params) # 二阶矩缓冲区(梯度平方的移动平均),初始为0 # --- 开始训练 --- num_steps = 1000 # 总共训练1000步(可以增大来获得更好的效果) for step in range(num_steps): # ---- 步骤1:准备数据 ---- # 选一个名字,加上 BOS 标记 doc = docs[step % len(docs)] # 循环使用数据集中的名字 tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS] # 例如 "emma" → [26, 4, 12, 12, 0, 26] # BOS e m m a BOS n = min(block_size, len(tokens) - 1) # n = 需要预测的位置数(= token数 - 1,因为最后一个没有"下一个"需要预测) # ---- 步骤2:前向传播(Forward Pass)---- # 逐个 token 送入模型,让它预测下一个 token keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] # ↑ 清空 KV 缓存(每个新名字从头开始) losses = [] # 记录每个位置的损失 for pos_id in range(n): token_id = tokens[pos_id] # 当前输入 token target_id = tokens[pos_id + 1] # 正确答案:下一个 token # 模型预测 logits = gpt(token_id, pos_id, keys, values) # 得到27个分数 probs = softmax(logits) # 转成概率 # 计算损失:-log(正确答案的概率) loss_t = -probs[target_id].log() # 为什么是 -log? # 如果模型很确定(概率=0.9):-log(0.9) = 0.105(损失小 ✓) # 如果模型很不确定(概率=0.01):-log(0.01) = 4.6(损失大 ✗) # 如果模型完美预测(概率=1.0):-log(1.0) = 0(损失为0 ★) # 所以:概率越高 → 损失越低 → 我们的目标就是让损失尽可能低 losses.append(loss_t) # 平均损失 = 所有位置损失的平均 loss = (1 / n) * sum(losses) # ---- 步骤3:反向传播(Backward Pass)---- # 一行代码,从损失出发,自动算出所有4192个参数的梯度 loss.backward() # 执行完后,每个参数的 .grad 都被填上了值 # 告诉我们:"要降低损失,这个参数应该增大还是减小,幅度多大" # ---- 步骤4:Adam 优化器更新参数 ---- lr_t = learning_rate * (1 - step / num_steps) # 学习率线性衰减 # ↑ 训练后期减小步幅,让模型"精细调整"而不是大幅跳动 for i, p in enumerate(params): # 更新一阶矩(梯度的指数移动平均 → 平滑方向) m[i] = beta1 * m[i] + (1 - beta1) * p.grad # ↑ 85% 保留旧的方向 + 15% 融入新的梯度 # 更新二阶矩(梯度平方的指数移动平均 → 衡量波动性) v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2 # ↑ 99% 保留旧的波动估计 + 1% 融入新的 # 偏差修正(Bias correction) # m 和 v 初始为0,前几步的估计值偏小,需要放大 # 随着 step 增大,修正系数趋近于1(不再需要修正) m_hat = m[i] / (1 - beta1 ** (step + 1)) v_hat = v[i] / (1 - beta2 ** (step + 1)) # ★ 核心更新公式 ★ # 参数 -= 学习率 × 梯度方向 / √(波动性) # 梯度方向(m_hat)决定往哪走 # 波动性(√v_hat)决定步子多大(波动大→小步,波动小→大步) p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam) # 梯度清零,为下一步做准备 p.grad = 0 # ↑ 不清零的话,下一步的梯度会累加到旧梯度上,结果就错了 # 打印训练进度 print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}", end='\r') # 初始 loss ≈ 3.3(随机猜测 27 选 1:-log(1/27) ≈ 3.3) # 训练后 loss ≈ 2.37(模型学到了一些规律,但还不完美) # loss 越低,说明模型预测得越准 # ============================================================================ # 第7部分:推理 / 生成(Inference / Generation) # ============================================================================ # 【目标】用训练好的模型生成新名字 # # 过程(自回归生成): # 1. 输入 BOS("开始一个新名字") # 2. 模型输出27个概率 → 按概率随机选一个字母 # 3. 把选中的字母作为下一步的输入 # 4. 重复,直到模型输出 BOS("名字结束")或达到最大长度 # # 这和 ChatGPT 的工作方式完全一样! # 只不过 ChatGPT 的 token 是词/词块,生成的是句子而非名字。 # ============================================================================ # 温度(Temperature):控制生成的"创造力" # temperature = 0.5 → 比较保守,倾向选概率高的字母(生成的名字更"正常") # temperature = 1.0 → 原始分布,多样性适中 # temperature = 2.0 → 很随机,会产生奇怪的名字 # temperature → 0 → 每次都选概率最高的那个(贪心解码,完全没有随机性) # # 原理:在 softmax 之前,把 logits 除以 temperature # 小温度 → logits 的差距被放大 → softmax 更尖锐 → 更确定 # 大温度 → logits 的差距被缩小 → softmax 更平坦 → 更随机 temperature = 0.5 print("\n--- inference (new, hallucinated names) ---") for sample_idx in range(20): # 每个新名字都从空白开始 keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] token_id = BOS # 以 BOS 开始 sample = [] # 收集生成的字母 for pos_id in range(block_size): # 最多生成 block_size 个字符 # 前向传播:让模型预测下一个字母的概率 logits = gpt(token_id, pos_id, keys, values) # 应用温度缩放后做 softmax probs = softmax([l / temperature for l in logits]) # 按概率分布随机采样一个 token token_id = random.choices( range(vocab_size), # 候选:0-26 weights=[p.data for p in probs] # 权重:每个候选的概率 )[0] # 如果采样到 BOS,说明模型认为名字应该结束了 if token_id == BOS: break # 否则,把对应的字母加入结果 sample.append(uchars[token_id]) print(f"sample {sample_idx+1:2d}: {''.join(sample)}") # 大多数生成的名字不在原始数据集中——它们是模型"编造"的! # 但它们听起来像真名字,因为模型学到了英文名字的统计规律。 # 这就是所谓的"幻觉"(hallucination),和 ChatGPT 编造事实是同一个现象。 # ============================================================================ # 总结 # ============================================================================ # # 恭喜你看完了!你刚刚理解了 ChatGPT 的核心算法。 # # 回顾一下这 200 行代码做了什么: # # ┌──────────────────────────────────────────────────────────────────┐ # │ 数据集 32,000个英文名字 │ # │ ↓ │ # │ 分词器 字母 → 数字(a=0, b=1, ..., z=25, BOS=26) │ # │ ↓ │ # │ 自动微分 Value 类,自动算梯度(反向传播) │ # │ ↓ │ # │ 模型 Transformer:嵌入 → 注意力 → MLP → 输出 │ # │ ↓ │ # │ 训练 1000步:前向 → 算损失 → 反向 → Adam更新 │ # │ ↓ │ # │ 推理 用训练好的模型生成新名字 │ # └──────────────────────────────────────────────────────────────────┘ # # 从 microgpt 到 ChatGPT,算法完全一样,区别只在于: # - 数据量:32K 名字 → 数万亿 token 的互联网文本 # - 模型大小:4,192 参数 → 数千亿参数 # - 训练资源:你的笔记本1分钟 → 数千GPU跑几个月 # - 后训练:无 → SFT(监督微调)+ RLHF(人类反馈强化学习) # # 正如 Karpathy 所说: # "This file is the complete algorithm. Everything else is just efficiency." # "这个文件就是完整的算法。其他一切都只是为了效率。"
2026年04月29日
4 阅读
0 评论
0 点赞
大厂笔试经验贴
蔚来2026-4-19第一题:链表排序在线评测链接:在线评测链接:https://www.neituiya.com/oj/13/2553题目描述AK机拿到了许多链表,她需要将这些链表按字典序进行排序,你能帮帮她吗?提示:链表的字典序定义如下:若链表 $$a$$ 和链表 $$b$$ 的前 $$k$$ 个节点相同($$k$$ 可以是 $$0$$),且第 $$k+1$$ 个节点不同,那么第 $$k+1$$ 个节点数值更大的那个链表字典序更大。若链表 $$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 的判断步骤:计算立方数 $$K = M^3$$判断是否在范围内如果 $$K < a$$:跳过如果 $$K > b$$:后面的 $$M$$ 更大,$$K$$ 只会更大,直接结束检查是否满足"特殊"条件先计算 $$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机可以进行两种操作:当数组 $$A$$ 不为空时,把数组 $$A$$ 中下标最小的且尚未删除的数压入栈 $$C$$ 中,然后从数组 $$A$$ 中删除这个数。当栈 $$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$$,操作 $$2$$,操作 $$2$$:该操作方案收益为 $$3 \times 2 + 2 \times 1 = 8$$。操作 $$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/26102026-4-25-算法岗第一题:插入顺序在线评测链接:https://www.neituiya.com/oj/7/2605第二题:矩阵计数在线评测链接:https://www.neituiya.com/oj/7/2606第三题:取模仪式在线评测链接:https://www.neituiya.com/oj/7/26072026-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$$。你可以至多各执行一次下列两种操作,执行顺序可以任意:选择一个整数 $$x(1 \le x \le n)$$,获得权值 $$\sum_{i=1}^{x} a_{b_i}$$。选择一个整数 $$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}^{x} 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/24702026-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/24762026-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题目描述给定一个仅由字符 0 与 1 组成、长度为 $$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$$ 的仅由字符 0 与 1 构成的字符串 $$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 102026-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/24162026-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$$:检查 $$x$$ 是否能被 $$d$$ 整除,若不能则跳过。令 $$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 序将子树映射为连续区间,线段树维护区间内的最小权值及其对应节点编号。对树做 DFS,记录每个节点的入时间 $$in[x]$$ 和出时间 $$out[x]$$,子树对应 DFS 序上的连续区间 $$[in[x], out[x]]$$。建立线段树,存储 $$(权值, 节点编号)$$ 对,初始时位置 $$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$$。在所有合法分配中,任选一个小朋友,统计其最终得到的糖果总数;求该数值在所有合法分配方案中可能达到的最小值与最大值。换句话说,求在所有合法分配方案中,任意一个小朋友所能获得的糖果总数最小值与最大值。输入描述每个测试文件均包含多组测试数据。第一行输入一个整数 $$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,2)$$ 记 $$+1$$ 分,后手向下到 $$(2,2)$$ 记 $$+1$$ 分,差值 $$+1 - (+1) = 0$$。先手向下到 $$(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$$ 所在区间的右端点。具体步骤:用平衡树(C++ 的 set、Java 的 TreeMap)存储所有不重叠区间,按左端点排序。插入 $$[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$$ 的矩阵。将矩阵分成大小为 $$2 \times 2$$ 的子矩阵。在每个 $$2 \times 2$$ 的子矩阵中只留下第二大的数字。当子矩阵的四个元素为 $$a_4 \le a_3 \le a_2 \le a_1$$ 时,第二大数为元素 $$a_2$$。重复上述过程,矩阵的大小不断缩小。现在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$$,子串 bAbb 和 Abbb 变换后都得到 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) }OPPO2026-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|$$。每个元素只能属于一份。由于每份大小相等,最优策略是让一份尽量大,一份尽量小,另一份为中间值。算法实现首先将数组$$a$$升序排序。设$$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$$种排列)。输出最大值。时间复杂度分析排序$$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$$,且每个目标只能被分配一次。算法实现将$$a$$按从大到小排序,优先处理大的数。用布尔数组$$used[1..n]$$记录每个目标数是否已被分配。3) 对每个$$a_i$$,不断对半取整,直到$$x\leq n$$且$$x$$未被分配,或$$x<1$$为止。4) 若找到可用的$$x$$,则$$used[x]=true$$,否则跳过。最终检查$$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机可以对这两个数执行如下两种操作:交换操作:交换 $$x$$ 和 $$y$$ 某一数位上的数字。删除操作:删除 $$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$$ 个任务需要完成。每个任务有两种执行模式,多多对于每个任务最多只能选择一种模式执行,也可以选择不执行:常规模式:花费 $$a_i$$ 个 token 和 $$b_i$$ 的时间。降耗模式:通过增加时间成本来降低 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/24282023-3-15第一题:直播巡检在线测评链接:https://www.neituiya.com/oj/3/2343题目描述AK机负责直播平台的首页直播排行榜的巡检工作。今天平台一共收到 $$n$$ 条候选直播内容,按输入顺序依次编号为 $$1$$ 到 $$n$$。每条直播内容都属于某个主播。为了避免同一主播同时占据多个排行榜位置,正式生成榜单前,平台会先做一次主播去重:对于同一个主播,只保留该主播"最优"的一条直播内容进入候选榜单,同一主播的其他直播内容会在这一步直接被淘汰,不再参与后续排序。因此,最终直播排行榜中每个主播至多出现一次,榜单条数恰好等于不同主播的个数。判断一条直播内容是否更优,按以下顺序比较:点赞数更多者更优。如果点赞数相同,则评论数更多者更优。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$$。魔法王国中跨城市通行需要使用传送门,传送规则如下:到达城市数字代号为当前的二倍的城市。即,假如数字代号本来为 $$n$$,此类型传送会到达数字代号为 $$2n$$ 的城市。到达城市数字代号为当前的二分之一的城市,让它变成原来的一半后向下取整。即,假如数字代号本来为 $$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$$。状态方程转移对于每个包裹,依次考虑两种派送方式:常规派送:若剩余燃料$$x\geq a_i$$,则$$f[y][x] = \max(f[y][x],\ f[y][x-a_i] + 1)$$虫洞派送:若剩余燃料$$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)$$。类似题目提瓦特商店最大快乐值(一)Javaimport 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$$),每个区间对应一个数字,区间不能重叠。关键点有:每个数字$$a_i$$的二进制表示长度不超过$$11$$,$$m\leq 6$$,$$n\leq 100$$,可以枚举所有方案。需要在$$s$$中找到每个$$a_i$$的所有匹配子串区间,并保证最终选择的$$m$$个区间不相交。3) 这是一个典型的多模式串匹配+不相交区间选择问题,适合回溯/DFS解决。算法实现对每个$$a_i$$,转为无前导$$0$$的二进制字符串。在$$s$$中枚举所有长度等于$$a_i$$二进制长度的子串,记录所有等于$$a_i$$二进制的区间$$[l,r]$$。3) 对所有数字,按可选区间数量从少到多排序,减少回溯分支。4) 用DFS/回溯依次为每个数字选择一个区间,要求区间两两不相交。若能为所有数字选出不相交区间,则输出"YES",否则输出"NO"。时间复杂度分析$$m$$较小,回溯分支最多$$O((n^2)^m)$$,但实际剪枝后远小于指数级。每组数据复杂度可接受。整体复杂度$$O(T\cdot m\cdot n^2)$$,可以通过所有测试数据。类似题目【科大讯飞】2025-8-2-第二题-字符串拼接【华为留学生】2025-5-7-第三题-筛选樱桃Javaimport 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$$为根BFS,计算$$dist[v] = dist[parent] \oplus w_{edge}$$。对每一位$$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/25292026-4-9-算法岗第一题:穿过黑暗之门在线评测链接:https://www.neituiya.com/oj/7/2471第二题:离散型马尔可夫模型预测在线评测链接:https://www.neituiya.com/oj/7/2472第三题:公倍数对和在线评测链接:https://www.neituiya.com/oj/7/24732026-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++JavaPythonGo第二题:按位与权值和在线评测链接: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++JavaPythonGo第三题:平方串在线评测链接: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++JavaPythonGo2026-3-29-研发岗第一题:巴巴博弈在线评测链接:https://www.neituiya.com/oj/7/2422第二题:质数合数在线评测链接:https://www.neituiya.com/oj/7/2423第三题:位运算权值在线评测链接:https://www.neituiya.com/oj/7/24242026-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$$ 或者减少 $$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++JavaPython# 怎么全是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$$。任意选择三个不同下标 $$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++JavaPython# 没有三角形 - 线性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++JavaPython# 分值转移 - 树状数组 + 增量计算 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()Go2026-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$$)。补充说明所有随机源固定 $$random\_state=42$$。为确保通过测试用例,仅允许使用 $$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 按步实现。整个流程分为特征提取、特征合并、模型训练、软投票四步:分别对文本数据做 Word-level TF-IDF(word unigram + bigram)和 Char 3-gram TF-IDF,对数值特征做标准化后二次多项展开。用 $$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。具体步骤:枚举所有下标对 $$(i, j)$$($$i < j$$),计算 $$v = a_i \oplus a_j$$,将 $$(i, j)$$ 存入哈希表 $$mp[v]$$。再次枚举所有下标对 $$(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机总共可以执行以下两种操作,其中第一种操作最多可执行一次,第二种操作也最多可执行一次(两种操作可以组合使用):选择两个不同的下标 $$i, j$$,将 $$a_i$$ 修改为 $$a_i + a_j$$,花费 $$k$$ 的代价。选择一个元素 $$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$$。算法实现算法主策略:本题采用枚举所有操作组合取最优。分三种情况讨论:如果数组中已有 $$0$$,答案直接为 $$0$$。仅用操作二:选 $$|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$$。请为每条序列计算:最优隐藏状态序列 $$\hat{\mathbf{q}}^{(s)} = (q_1, \ldots, q_T)$$该序列的对数概率 $$\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)$$。步骤:初始化:$$\delta_1(i) = \log \pi_i + \log B_i(o_1)$$。递推:对 $$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/26222026-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 52026-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,包含 train 和 test 两个字段。训练部分最后一列是标签,所有值均为整数或浮点数。输出描述仅输出一行,格式为 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 72026-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$$ 恒成立。分治步骤:用稀疏表预处理 $$O(1)$$ 查询区间最大值位置。对区间 $$[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题目描述称一个括号序列为"平衡的括号序列",当且仅当满足以下归纳定义:空串是平衡的。若字符串 $$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$$:将简单路径 $$u \to v$$ 上所有节点的值反置($$0$$ 变 $$1$$,$$1$$ 变 $$0$$)。操作 $$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$$。查询 $$g(1 \to 4)$$:路径 $$[1, 2, 4]$$,值 000 = $$0$$。输出 $$0$$。翻转 $$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$$。查询 $$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)$$,存储集合。Javaimport 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 数组和频率表。Javaimport 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); } }PythonGo第四题: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$$:将简单路径 $$u \to v$$ 上所有节点的值反置($$0$$ 变 $$1$$,$$1$$ 变 $$0$$)。操作 $$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$$。查询 $$g(1 \to 4)$$:路径 $$[1, 2, 4]$$,值 000 = $$0$$。输出 $$0$$。翻转 $$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$$。查询 $$g(5 \to 1)$$:路径 $$[5, 2, 1]$$,值 111 = $$7$$。输出 $$7$$。时空复杂度分析时间复杂度:$$O(m \log^2 n)$$,每次操作拆成 $$O(\log n)$$ 条链段,每条链段的线段树操作 $$O(\log n)$$。空间复杂度:$$O(n)$$,存储 HLD 信息和线段树。Javaimport 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 样例解释第一组输入 $$n=3, m=8$$:种钞票面值为 $$3$$ 元和 $$4$$ 元。使用 $$2$$ 张 $$4$$ 元钞票,$$4 \times 2 = 8$$,刚好支付,额外支付 $$0$$。第二组输入 $$n=4, m=6$$:两种钞票面值为 $$4$$ 元和 $$5$$ 元。无法凑出恰好 $$6$$ 元,最小的不低于 $$6$$ 的付款为 $$4 \times 2 = 8$$,额外支付 $$8 - 6 = 2$$。第三组输入 $$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/25122026-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/24362026-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内推码:KUzqkE2026-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/26302026-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/24592026-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$$。移动过程中,所有已加入的人会形成一个队伍,并按照以下规则行动:加入队伍:当当前持伞队伍经过某个人的出生点 $$p_i$$ 时,如果此人尚未加入过队伍,则该人立即加入到队伍末尾("经过"包括恰好停在该点的情况)。离开队伍:当某个人到达自己的终点 $$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]$$,这样数组权值算法实现遍历原数组,将所有数字拆分为单独的数位,统计所有数位。记录每个原始数字的位数,并为每个数字的每一位计算其在最终总和中的权重($$10^{len-1},10^{len-2},...,1$$)。3) 将所有权重收集到一个数组中。4) 将所有数位从大到小排序,将所有权重从大到小排序。按照权重从大到小依次分配最大的数位,累加贡献。输出最终总和。时间复杂度分析遍历和拆分数位$$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$$变为回文串。每次操作可以覆盖一段连续的不匹配对,且一次操作可以同时修正一段连续的不匹配。关键点:只需关注前一半的对称位置,统计有多少段连续的不匹配对,每段只需一次操作。算法实现对于每个测试用例,遍历字符串前一半,对每个$$i$$,判断$$s_i$$和$$s_{n-1-i}$$是否相等。若不相等,记为不匹配对。统计所有连续的不匹配对段数,每段只需一次操作。3) 用变量$$last$$记录上一个位置是否为不匹配,遇到新的不匹配段时计数加一。4) 最终输出段数即为最少操作次数。时间复杂度分析每组数据遍历字符串一次,复杂度$$O(n)$$。总体复杂度$$O(\sum n)$$,空间复杂度$$O(1)$$,可高效处理所有测试样例。PythonT = 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'的出现次数满足非递减关系,并且在所有可行方案中输出字典序最小的结果。核心难点在于既要满足计数单调,又要保证字典序最小。算法实现首先统计原始字符串每个字母的出现次数,记为$$freq[c]$$。逆序递推每个字母最终要保留的数量$$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]$$减一。最终$$res$$即为所求字典序最小的可行解。时间复杂度分析统计$$freq$$和$$keep$$均为$$O(n)$$。主循环每个字符最多进出栈一次,总复杂度$$O(n)$$。3) 总体时间和空间复杂度均为$$O(n)$$,可通过所有数据范围。Gopackage 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.70,xhs 是纯字母→名称,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$$ 且只由字符 0 和 1 构成的字符串 $$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$$ 交换(b 与 a 互换),并将 $$s_4$$ 与 $$s_5$$ 交换(e 与 d 互换),得到 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$$,考虑最后一段的选择:放长度 $$a$$ 的段(无前置约束,可从任意状态转移):$$g[k] = f[k-a] + g[k-a] \quad (k \ge a)$$放长度 $$b$$ 的段(无前置约束):$$f[k] \mathrel{+}= f[k-b] + g[k-b] \quad (k \ge b)$$放长度 $$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$$算法实现预处理所有不超过$$10^9$$的完美数字。枚举起始$$s$$,枚举长度$$l\geq3$$,计算乘积,若不超过$$10^9$$则加入集合。对每个查询$$x$$,判断$$x$$是否在预处理集合中,输出"YES"或"NO"。3) 由于$$10^9$$范围内的完美数字数量有限,预处理后查询可$$O(1)$$完成。时间复杂度分析预处理复杂度$$O(10^3)$$。每次查询复杂度$$O(1)$$,空间复杂度为完美数字集合大小,约$$O(10^4)$$。PythonMAXX = 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$$变为回文串。每次操作可以覆盖一段连续的不匹配对,且一次操作可以同时修正一段连续的不匹配。关键点:只需关注前一半的对称位置,统计有多少段连续的不匹配对,每段只需一次操作。算法实现对于每个测试用例,遍历字符串前一半,对每个$$i$$,判断$$s_i$$和$$s_{n-1-i}$$是否相等。若不相等,记为不匹配对。统计所有连续的不匹配对段数,每段只需一次操作。3) 用变量$$last$$记录上一个位置是否为不匹配,遇到新的不匹配段时计数加一。4) 最终输出段数即为最少操作次数。时间复杂度分析每组数据遍历字符串一次,复杂度$$O(n)$$。总体复杂度$$O(\sum n)$$,空间复杂度$$O(1)$$,可高效处理所有测试样例。PythonT = 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'的出现次数满足非递减关系,并且在所有可行方案中输出字典序最小的结果。核心难点在于既要满足计数单调,又要保证字典序最小。算法实现首先统计原始字符串每个字母的出现次数,记为$$freq[c]$$。逆序递推每个字母最终要保留的数量$$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]$$减一。最终$$res$$即为所求字典序最小的可行解。时间复杂度分析统计$$freq$$和$$keep$$均为$$O(n)$$。主循环每个字符最多进出栈一次,总复杂度$$O(n)$$。3) 总体时间和空间复杂度均为$$O(n)$$,可通过所有数据范围。Pythonn = 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)$$ 出发,遍历所有可达的格子,最后用总格子数减去可达数即为答案。具体步骤:解析 JSON 格式的输入字符串,提取出 $$n \times n$$ 的二维矩阵。从 $$(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$$。具体步骤:将数组所有元素异或,得到 $$xorAll = a \oplus b$$(因为出现两次的数全部抵消)。找到 $$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{x}{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; }Java2026-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++JavaPython# 划分 - 二分答案 + 贪心 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++JavaPython# 开心食堂 - 贪心 + 区间求和 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()Go2026-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 词表的长度答案:C2、对于输入为 $$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$$答案:D3、在原始 Transformer 的多头注意力机制中,多个头间的输出是如何结合的?A. 取最大值B. 拼接后经过线性变换C. 逐元素相加D. 取平均答案:B4、关于 Transformer 中的多头注意力(Multi-Head Attention)的表述,哪一项是正确的?A. 多个头共享相同的查询、键、值权重矩阵B. 每个头独立学习不同的线性投影,最后将注意力输出拼接C. 头数越多,模型推理速度一定越快D. 每个头关注输入序列的同一局部特征答案:B5、以下关于凸函数的说法,正确的是:A. 凸函数的局部最小值一定是全局最小值B. 凸函数的二阶导数可以为负C. 凸函数一定没有最小值D. 所有多项式函数都是凸函数答案:A6、对于回归问题,假设真实值和预测值的误差分布符合正态分布,且均值为 0。此时,以下哪个指标最能反映模型的整体预测精度?A. 最大绝对误差(Max Error)B. 中位数绝对误差C. 极差(Range)D. 均方误差(MSE)答案:D7、关于 Pass@k(代码生成常用)中 "k" 的含义,正确的是:A. 把 k 道题的平均正确率作为指标B. 从 k 个模型中选最强的一个C. 生成时把 top\_k 设置为 kD. 对同一题生成的 n 个候选($$n \ge k$$),随机抽取 k 个,k 个中至少有一个通过就算成功答案:D8、以下推理代码速度较慢: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. 每步都重新 tokenizeB. 将 max_new_tokens 增大C. 把 argmax 改成 topkD. 使用 past_key_values(KV Cache)避免重复计算历史上下文答案:D9、关于多头注意力(MHA)和分组查询注意力(GQA)的区别,下列说法正确的是?A. GQA 只能用于解码器B. MHA 比 GQA 参数量更少C. GQA 中每个头有自己的 K 和 V,而 MHA 共享D. GQA 中多个查询头共享一组 K 和 V答案:D10、关于旋转位置编码(RoPE, Rotary Position Embedding)的表述,正确的是?A. RoPE 仅适用于编码器,不适用于自回归解码器B. RoPE 无法处理超出预训练长度的序列C. RoPE 通过旋转矩阵对查询和键向量进行变换,使内积蕴含相对位置信息D. RoPE 将绝对位置信息直接加到词嵌入中答案:C11、设计一个用于图像分类的卷积神经网络(CNN),隐藏层需兼顾计算效率和缓解梯度消失,输出层用于多分类任务,下列激活函数搭配最合理的是?A. 隐藏层用 ReLU,输出层用 SoftmaxB. 隐藏层用 Sigmoid,输出层用 ReLUC. 隐藏层用 Tanh,输出层用 SigmoidD. 隐藏层用 Softmax,输出层用 Tanh答案:A12、关于模型推理中前向计算与训练前向计算的区别,下列说法错误的是:A. 推理前向计算仅需输出最终结果,训练前向计算需保留中间特征用于反向传播B. 推理前向计算可复用 KV Cache,训练前向计算无需缓存 K/V 向量C. 推理前向计算与训练前向计算的计算逻辑完全一致,仅输入数据不同D. 推理前向计算的批量大小通常小于训练,以平衡延迟和吞吐量答案:C13、对于函数 $$f(x) = \frac{1}{1+25x^2}$$,在区间 $$[-1,1]$$ 上取等距节点进行高次插值,当 $$n$$ 增大时,插值多项式在区间两端会出现什么现象?A. 收敛到 $$f(x)$$B. 插值多项式趋于零C. 振荡加剧,误差变大D. 插值多项式趋于直线答案:C14、若向量组 $$\{\vec{a}_1, \vec{a}_2, \vec{a}_3\}$$ 线性无关,则以下说法正确的是?A. 不存在不全为零的系数使线性组合为零B. 可由更少向量生成同一空间C. 任意两个向量线性相关D. 向量个数大于维度答案:A15、在线性分类模型中,预测函数常写为 $$y = w^Tx + b$$,设 $$w = (1, 2)$$,$$x = (3, 4)$$,$$b = 1$$,计算模型输出 $$y$$:A. 10B. 12C. 11D. 13答案:B二、多选题16、影响 GPU 内核占用率的因素包括以下哪些选项?A. 线程块使用的共享内存大小B. 线程块的维度设置C. 每个线程使用的寄存器数量D. GPU 核心的时钟频率答案:A, B, C17、某公司正在开发一个智能客服系统,使用多 Agent 协作架构来处理复杂的用户请求。系统采用任务分解和分发机制,主 Agent 负责将用户请求分解为子任务并分发给专门的功能 Agent(如订单 Agent、支付 Agent、售后 Agent)。在设计和优化这个系统时,以下哪些关于规划和协作的策略是正确的?A. 设子任务数量为 $$n$$,若所有子任务相互独立且可并行,系统吞吐量可近似达到单 Agent 吞吐量的 $$n$$ 倍;若存在依赖链,吞吐量会受限于关键路径的子任务串行执行时间B. 主 Agent 在任务分解时应考虑子任务之间的数据依赖关系,确保下游 Agent 能够获得上游 Agent 执行结果C. 当主 Agent 检测到某个子任务 Agent 执行失败时,可以根据任务重要性决定是重试该子任务还是终止整个任务流D. 为减少通信开销,所有子任务应该尽可能并行执行,即使存在依赖关系答案:A, B, C18、在大语言模型推理阶段,以下哪些技术在配置正确的情况下,可以实际降低单 token 的生成延迟?A. 对模型进行 INT8 权重量化,并在推理时配合 GPU Tensor Core 执行B. 使用 KV Cache 以避免重复计算历史 token 的 AttentionC. 在推理服务中增大 Batch Size 并启用连续批处理(Continuous Batching)D. 采用 FlashAttention / FlashAttention-2 实现 Attention 计算答案:A, B, D19、假设某分布式训练集群中有两台处理节点的参数同步模块,它们无故障连续运行的时间 $$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, D20、数据科学家小李正在训练一个大型语言模型,采用混合精度训练策略。他发现在某些层使用 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, C2026-4-23-留学生AI岗编程题第一题:基于最大边际相关性(MMR)的智能示例重排序在线评测链接:https://www.neituiya.com/oj/16/2596第二题:决策树实现网络设备故障预测在线评测链接:https://www.neituiya.com/oj/16/25972026-4-23-留学生研发岗第一题:给软件版本号排序在线评测链接:https://www.neituiya.com/oj/16/2598第二题:AI超节点内部计算单元通信最小时延在线评测链接:https://www.neituiya.com/oj/16/2599第三题:奖品采购在线评测链接:https://www.neituiya.com/oj/16/26002026-4-22国内AI岗选择题1、某运营商构建网络配置命令、设备运行日志、故障案例一体化检索系统,文本中包含大量命令行(如display interface、ip route)、MAC/IP地址、端口编号、VLAN ID、OID等结构化内容。A. 对命令行、IP、MAC、端口等增加专用词表,按命令块/日志条目切分,使用通信领域微调 EmbeddingB. 全部文本统一小写并去除符号,避免分词异常C. 直接按固定长度切分,使用通用分词与通用 EmbeddingD. 只使用关键词 BM25 检索,不使用 Embedding答案:A2、在大模型指令微调实验中,研究人员对比了3种训练数据配比(纯指令、指令+多轮对话、指令+知识)下的模型响应准确率,需直观展示不同配比的准确率差异并便于组间对比,最适合的可视化图表是( )A. 分组柱状图B. 雷达图C. 饼图D. 直方图答案:A3、牛顿迭代法的迭代公式为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)$$答案:A4、张量并行(TP)主要用于解决单卡显存无法容纳单个模型层权重的问题,其切分逻辑是?A. 按优化器状态切分,不同GPU维护不同的优化器状态B. 按网络层深度切分,不同层放在不同GPU上C. 按批次大小切分,不同样本放在不同GPU上D. 按矩阵运算的维度切分,同一层内的权重被拆分到不同GPU上答案:D5、下列哪个矩阵一定是可逆的?A. 对称矩阵B. 行列式为零的矩阵C. 单位矩阵D. 所有元素都为零的矩阵答案:C6、在多轮对话推理系统中,如果GPU/NPU显存已满但仍有新的token需要生成,系统通常采用什么策略?A. 将部分KV Cache swap到CPU或进行recomputationB. 自动降低模型精度C. 重启服务器D. 直接杀掉进程答案:A7、在部署阶段(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 加速答案:B8、在流水线并行(Pipeline Parallel)中,一个模型被切分为多个 Stage,分布在不同 GPU 上。当某些 GPU 在等待上游 Stage 的计算结果时出现空闲,这种现象被称为?A. Pipeline BubbleB. 显存碎片C. GPU Context SwitchD. 网络拥塞答案:A9、在层次聚类分析(HAC)中,以下哪一种方法是用于定义簇间距离的常见方式?A. Expectation-MaximizationB. K-meansC. DBSCAND. Complete Linkage答案:D10、某多分类任务中,类别A有1000个样本,类别B有10个样本。若使用Micro-F1计算,主要反映的是哪个类别的性能?A. 两者权重相同B. 取决于具体的F1计算公式C. 类别BD. 类别A答案:D11、在PyTorch 中,以下哪个函数用于执行优化器的一步更新A. optimizer.zero\_grad()B. optimizer.backward()C. optimizer.step()D. optimizer.update()答案:C12、工程师需要计算一个复杂函数 $$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})$$答案:D13、适合高维稀疏向量相似度计算的是?A. 余弦相似度B. 切比雪夫距离C. 欧氏距离D. 曼哈顿距离答案:A14、Transformer的编码器-解码器注意力(Encoder-Decoder Attention)中,查询(Query)来自哪里?A. 输入序列B. 解码器的上一层的输出C. 位置编码D. 编码器的输出答案:B15、给定两个向量 $$\mathbf{a}=[1,2,3]$$ 和 $$\mathbf{b}=[4,5,6]$$,他们余弦相似度为A. 1.0B. 0.87C. 0.97D. 0.67答案:C16、多模态大模型中,常见的视觉-语言连接器(Connector)包括?(多选)A. MLP(多层感知机)B. 线性投影层(Linear Projection)C. 卷积池化层D. Q-Former答案:A, B, D17、适用于机器学习中度量两个特征向量的相似度的有?(多选)A. 欧氏距离B. 余弦相似度C. 汉明距离D. KL散度答案:A, B, C18、设 $$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, C19、以下关于反向传播计算效率的说法正确的是?(多选)A. 反向传播的计算量大约是前向传播的2倍B. 符号微分跟数值微分是两种计算体系,在一个模型训练时只能使用其中一种C. 反向传播需要存储所有中间层的激活值,因此显存消耗大D. 相比于数值微分,符号微分计算梯度的速度快答案:A, C20、你维护的在线问答服务(vLLM)在晚高峰出现:TTFT 从 0.9s 升至 2.4s,decode tokens/s 基本不变,平均输入长度从 700 升至 2200,GPU利用率接近满载。你本班次可立即执行哪些动作?(多选)A. 对超长输入先走"摘要压缩链路",再送主模型B. 增大temperature到1.2C. 把max\_new\_tokens从512调到1024D. 开启/优化prefix cache(固定system prompt场景)答案:A, D2026-4-22-国内AI岗第一题:统计二叉树中平衡路径的数量在线评测链接:https://www.neituiya.com/oj/16/2594题目描述定义二叉树的平衡路径需同时满足以下 $$3$$ 个条件:路径从任意节点出发,仅能向下延伸(只能向左/右子节点,不可向上回溯)。路径上所有节点的和相加为 $$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$$ 依赖包 $$2$$,包 $$2$$ 依赖包 $$3$$,包 $$3$$ 又依赖包 $$1$$,属于循环依赖。版本号不纳入循环依赖的判断,自己依赖自己也属于循环依赖。对包依赖关系的版本号进行规整处理。如果包依赖关系合理,对于多个包依赖同一个包的情况,取被依赖包的最大版本号,替换所有对该包的版本号引用。输入描述每次输入两组依赖关系的信息,分别解析和输出两组结果。每组格式:第一行包含正整数 $$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$$ 个历史记录;每次访问新页面时清空前进记录。操作说明visit url:当前页面更新为该网页,加入历史记录;若超过 $$max\_history$$,则删除最早记录;清空前进记录;网址 url 为小写字母、数字和点的组合,长度 $$\le 100$$,用例数据均为合法输入。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(移动窗口)、给定一个区域查询所有的可见窗口、查询单个窗口的可见性。窗口遮挡与可见性规则层级大的窗口可以遮盖层级小的窗口层级相同的窗口中,后创建的窗口可以覆盖先创建的窗口;窗口的 resize 和 move 操作不影响窗口创建的先后顺序3) 窗口只要没有被完全覆盖,就算做可见4) 窗口可以在屏幕外创建,或者被 move/resize 到屏幕外,完全处于屏幕外的窗口不可见需要实现的方法init:给定屏幕宽高,初始化屏幕。校验屏幕宽高是否均为正整数,满足返回 true;反之返回 false。createWindow:给定窗口名、窗口左上角坐标、窗口宽高、窗口层级,创建窗口。校验窗口宽高是否均为正整数,窗口名是否未被使用过,若满足则执行操作并返回 true,反之返回 false。3) removeWindow:给定窗口名移除一个指定的窗口。移除成功返回 true;窗口未创建无法执行移除操作返回 false。4) resize:给定一个窗口名和新的宽高,修改指定窗口的宽高。校验窗口是否已经创建以及新的窗口宽高是否为正整数,满足则执行操作并返回 true,否则返回 false。move:给定一个窗口名和一个新的左上角坐标,修改指定窗口的位置。校验窗口是否已经创建,满足则执行操作并返回 true,否则返回 false。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 完全遮盖了 win1 和 win2,只有 win3 可见。2026-4-8-算法岗第一题:路由器资源用量预测在线评测链接:https://www.neituiya.com/oj/16/2466题目描述路由器的某资源利用率与多个运行特征强相关:协议连接数(单位:个)、转发数据包速率(单位:Mpps)、内存占用率(单位:%)。为了精准预测不同负载下的路由器资源利用率,保障网络稳定运行,请实现批量梯度下降法(BGD)来训练资源预测线性回归模型的参数。资源预测模型:$$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$$ 为特征权重)损失函数:均方误差(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$$ 次后停止,无需判断收敛为了提高收敛速度,采用特征归一化进行训练,并在训练完成后进行权重还原。特征归一化:对每个特征维度 $$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 = 50,tol = 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 + 多数投票。读取 $$N$$ 个训练样本和 $$1$$ 个待分类向量。计算待分类向量与每个训练样本的欧氏距离 $$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 岗 难度评级:中等偏难考点分析:选择题(20 道):涵盖 Transformer、概率论、评价指标、集成学习、扩散模型等编程第一题:KMeans 聚类模拟(难度中等)编程第二题:NumPy 矩阵运算 + 岭回归(难度中等偏难)建议策略:选择题涉及面广,重点复习 Transformer 原理、评价指标、概率论基础和大模型相关概念编程第一题是经典 KMeans 算法模拟,理解"分配+更新"两步迭代即可,建议优先拿下编程第二题涉及矩阵求逆和岭回归公式,需要熟悉 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. AUCB. AccuracyC. RecallD. Precision答案:A难度:中等考点:机器学习—评价指标解释:AUC 衡量的是模型对正负样本的排序能力,不依赖于类别分布比例,因此对类别不平衡最不敏感。7、集成学习通过组合多个学习器来提升整体模型性能,Bagging 类算法属于集成学习的一种,通过学习多个训练器,采用投票的方式降低模型方差,以下属于 Bagging 类算法的是A. Random ForestB. GBDTC. Decision TreeD. 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 NormB. 全连接层C. AttentionD. 卷积层答案: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$$实现步骤:读取所有数据,记录每个缺失值的位置和所有真实值。对每个缺失值,从其位置向前、向后搜索,遇到第一个其他原始缺失值即确定训练区间的边界。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}")
2026年04月29日
2 阅读
0 评论
0 点赞
大厂笔试经验贴——CPP改写
2026.4.26 pdd下面是四道题的 C++ 改写代码,保持原有算法逻辑,使用 C++ 标准库实现。第一题:二维费用背包#include <iostream> #include <vector> #include <algorithm> using namespace std; const int INF = 1e9; int main() { int n, m, t; cin >> n >> m >> t; vector<tuple<int, int, int, int>> tasks(n); for (int i = 0; i < n; ++i) { int a, b, c, d; cin >> a >> b >> c >> d; tasks[i] = {a, b, c, d}; } // f[j][k] = 使用恰好j个token完成恰好k个任务的最小时间 vector<vector<int>> f(m + 1, vector<int>(n + 1, INF)); f[0][0] = 0; for (auto [a, b, c, d] : tasks) { for (int j = m; j >= 0; --j) { for (int k = n; k >= 1; --k) { if (j >= a) f[j][k] = min(f[j][k], f[j - a][k - 1] + b); if (j >= c) f[j][k] = min(f[j][k], f[j - c][k - 1] + d); } } } int ans = 0; for (int k = n; k >= 0; --k) { for (int j = 0; j <= m; ++j) { if (f[j][k] <= t) { ans = k; break; } } if (ans) break; } cout << ans << endl; return 0; }第二题:贪心 + 二分查找(使用 multiset)#include <iostream> #include <vector> #include <algorithm> #include <set> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { int n, m; cin >> n >> m; vector<pair<int, int>> contents(n); // (r, l) for (int i = 0; i < n; ++i) { int l, r; cin >> l >> r; contents[i] = {r, l}; } vector<int> slots(m); for (int i = 0; i < m; ++i) cin >> slots[i]; // 将推荐位排序并放入 multiset(允许重复) multiset<int> slotSet(slots.begin(), slots.end()); // 按右端点升序排序 sort(contents.begin(), contents.end()); int ans = 0; for (auto [r, l] : contents) { auto it = slotSet.lower_bound(l); if (it != slotSet.end() && *it <= r) { ++ans; slotSet.erase(it); } } cout << ans << '\n'; } return 0; }第三题:BFS 坐标重建#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; int main() { int n, m; cin >> n >> m; int total = n * m; int k; cin >> k; // 方向映射 // from_a: 从 a 看 b 的偏移 // from_b: 从 b 看 a 的偏移 vector<vector<tuple<int, int, int>>> adj(total + 1); int first = 0; for (int i = 0; i < k; ++i) { int a, b; string d; cin >> a >> b >> d; if (first == 0) first = a; int dr1, dc1, dr2, dc2; if (d == "U") { dr1 = 1; dc1 = 0; dr2 = -1; dc2 = 0; } else if (d == "B") { dr1 = -1; dc1 = 0; dr2 = 1; dc2 = 0; } else if (d == "L") { dr1 = 0; dc1 = 1; dr2 = 0; dc2 = -1; } else { // "R" dr1 = 0; dc1 = -1; dr2 = 0; dc2 = 1; } adj[a].emplace_back(b, dr1, dc1); adj[b].emplace_back(a, dr2, dc2); } vector<int> row(total + 1, 0), col(total + 1, 0); vector<bool> vis(total + 1, false); queue<int> q; vis[first] = true; q.push(first); while (!q.empty()) { int u = q.front(); q.pop(); for (auto [v, dr, dc] : adj[u]) { if (!vis[v]) { vis[v] = true; row[v] = row[u] + dr; col[v] = col[u] + dc; q.push(v); } } } // 归一化坐标 int min_r = *min_element(row.begin() + 1, row.end()); int min_c = *min_element(col.begin() + 1, col.end()); vector<vector<int>> grid(n, vector<int>(m, 0)); for (int i = 1; i <= total; ++i) { int r = row[i] - min_r; int c = col[i] - min_c; grid[r][c] = i; } for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { cout << grid[i][j] << (j == m-1 ? '\n' : ' '); } } return 0; }第四题:二分答案 + 树上贪心#include <iostream> #include <vector> #include <algorithm> using namespace std; int check(int D, int n, const vector<vector<int>>& children) { int cnt = 0; vector<int> max_dist(n + 1, 0); // 后序遍历顺序:先获取前序顺序再反转 vector<int> order; vector<int> stack = {1}; while (!stack.empty()) { int u = stack.back(); stack.pop_back(); order.push_back(u); for (int v : children[u]) { stack.push_back(v); } } reverse(order.begin(), order.end()); for (int u : order) { int md = 0; for (int v : children[u]) { if (max_dist[v] >= 0) { md = max(md, max_dist[v] + 1); } } if (md >= D) { cnt++; max_dist[u] = -1; // 被覆盖 } else { max_dist[u] = md; } } // 根节点若未被覆盖,需要额外放置 if (max_dist[1] >= 0) cnt++; return cnt; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { int n, k; cin >> n >> k; vector<vector<int>> children(n + 1); if (n > 1) { for (int i = 2; i <= n; ++i) { int p; cin >> p; children[p].push_back(i); } } int lo = 0, hi = n - 1; while (lo < hi) { int mid = (lo + hi) / 2; if (check(mid, n, children) <= k) { hi = mid; } else { lo = mid + 1; } } cout << lo << '\n'; } return 0; }以上代码均保持了原有算法的核心思想,并适配了 C++ 的语法和常用数据结构。
2026年04月29日
1 阅读
0 评论
0 点赞
古老的谏言——程序员
https://www.cnblogs.com/bo083/archive/2012/03/25/2416627.html#!comments posted on 2012-03-25 15:08题记 近来一直担心毕业需要写论文的问题,基本都没碰过编程的东西了。要写论文才发现做研究真的很难,在此向奋斗在科研一线的xdjm们致敬了!言归正传,论文刚有了一点思路就像放松一下,最近刚入了一个android手机就想试试android开发,于是花了将近半天时间搭好开发环境,写了一个helloworld,就想找本书看看,下载了《android应用开发揭秘》,打包这本书的网友提到计算机基础的重要性,推荐了http://bbs.theithome.com/read-htm-tid-123.html的帖子,但是网站已经上不去了,从别处搜来看了,觉得很有道理,与大家共享一下。 我始终认为,对一个初学者来说,IT界的技术风潮是不可以追赶的,而且也没有能力去追赶。 我时常看见自己的DDMM们把课本扔了,去卖些价格不菲的诸如C#, VB.Net 这样的大部头,这让我感到非常痛心。而许多搞不清指针是咋回事的BBS站友眉飞色舞的讨论C#里面可以不用指针等等则让我觉得好笑。C#就象当年的ASP 一样,“忽如一夜春风来,千树万树梨花开”,结果许多学校的信息学院成了“Web 学院”96,97级的不少大学生都去做Web 了。当然我没有任何歧视某一行业的意识。我只是觉得如果他们把追赶这些时髦技术的时间多花一点在基础的课程上应该是可以走得更远的.几个误区初学者对C#风潮的追赶其实也只是学习过程中经常遇到的几个误区之一。我将用一些实际的例子来说明这些现象,你可以按部就班的看看自己是不是属于其中的一种或者 几种:认为计算机技术等于编程技术:有些人即使没有这个想法,在潜意识中也有这样的冲动。让我奇怪的是,许多信息学院的学生也有这样的念头。认为计算机专业就是编程专业,与编程无关的,或者不太相 关的课程他统统都不管,极端的学生只要书上没带“编程”两个字他就不看。其 实编程只是计算机技术应用过程中一种复杂性最低的劳动,这就是为什么IT业最底层的人是程序员(CODER)。计算机技术包括了多媒体,计算机网络,人工 智能 ,模式识别,管理信息系统等等这些方面。编程工作只是在这些具体技术在理论研究或者工程实践的过程中表达算法的过程。编程的人不一定对计算机技术的了解就 一定很 高。而一个有趣的现象是,不少大师级的计算机技术研究者是不懂编程的。网上的炒作和现实中良好的工作待遇把编程这种劳动神秘化了。其实每一个程序员心里都 明白, 自己这些东西,学的时候并不比其它专业难,所以自然也不会高档到哪里去。咬文嚼字的孔已己作风:我见过一本女生 的《计算机网络原理》教材,这个女生像小学生一样在书上划满了横杠杠,笔记做得满满的,打印出来一定比教材还厚。我不明白的是,像计算机网络原理 这样的课程有必要做笔记?我们的应试教育的确害了不少学生,在上《原理》这一类课程的时候许多学生像学《马列原理》一样逐字背诵记忆。这乃是我见过的最愚 蠢的行 为。所谓《原理》,即是需要掌握它为什么这样做,学习why,而不是how(怎样做)。极端认真的学生背下以太网的网线最大长度,数据帧的长度,每个字段 的意义 ,IP报头的格式等等,但是忘了路由的原则,忘了TCP/IP协议设计的宗旨。总之许多人花了大量的时间把书背得滚瓜烂熟却等于什么也没学。在 学习编程的时候这些学生也是这样,他们确切的记得C++语法的各个细节。看完了C++教程后看《Thinking in C++》(确实是好书),《Inside C++》,《C++ reference》,this C++, that C++……,然后是网上各种各样的关于C++语法的奇闻逸事,然后发现自己又忘了C++的一些语法,最后回头继续恶补…。有个师弟就跟我说:“C++ 太难了,学了这里忘了那里,学了继承忘了模板。” 我的回答道:“你不去学就容易了”。我并没有教坏他,只是告诉他,死抠C++的语法就和孔已己炫耀茴香豆的茴字有几种写法一样毫无意义。你根本不需要对的 C++ 语法太关心,动手编程就是了,有不记得的地方一查MSDN就立马搞定。我有个结论就是,实际的开发过程中对程序语法的了解是最微不足道的知识。这是为什么 我在为 同学用Basic(我以前从没有学过它)写一个小程序的时候,只花了半个小时看了看语法,然后再用半个小时完成了程序,而一个小时后我又完全忘记了 Basic 的所有关键字。不顾基础,盲目追赶时髦技术:终于点到题目上来了。大多数的人都希望自己的东西能够马上跑起来,变成钱。这种 想法对一个已经进入职业领域的程序员或者项目经理来说是合理的,而且IT技术进步 是如此的快,不跟进就是失业。但是对于初学者来说(尤其是时间充裕的大中专在校生),这种想法是另人费解的。一个并未进入到行业竞争中来的初学者最大的资 本便是 他有足够的时间沉下心来学习基础性的东西,学习why 而不是how。时髦的技术往往容易掌握,而且越来越容易掌握,这是商业利益的驱使,为了最大化的降低软件开发的成本。但在IT领域内的现实就是这样,越容 易掌握的东西,学习的人越多 ,而且淘汰得越快。每一次新的技术出来,都有许多初学者跟进,这些初学者由于缺乏必要的基础而使得自己在跟进的过程中花费大量的时间,而等他学会了,这种 技术也 快淘汰了。基础的课程,比方数据结构,操作系统原理等等虽然不能让你立马就实现一个linux(这是许多人嘲笑理论课程无用的原因),但它们能够显著的减 少你在 学习新技术时学习曲线的坡度。而且对于许多关键的技术(比方Win32 SDK 程序的设计,DDK的编程)来说甚至是不可或缺的。一个活生生的例子我和我的一个同学,在大一时我还找不到开机按纽,他已经会写些简单的汇编程序了。我把大二的所有时间花在了汇编,计算机体系结构,数据结构, 操作系统原理等等这些课程的学习上,而他则开始学习HTML和VB,并追赶ASP的潮流。大三的时候我开始学习Windows 操作系统原理,学习SDK编程,时间是漫长的,这时我才能够用VC开发出象模象样的应用程序。我曾一度因为同学的程序已经能够运行而自己还在学习如何创建 对话框而懊恼不已,但临到毕业才发现自己的选择是何等的正确。和我谈判的公司开出的 薪水是他的两倍还多。下面有一个不很恰当的比方:假设学习VB编程需要4个月,学习基础课程和VC的程序设计需要1年。那么如果你先学VB,再来学习后 者,时间不会减少,还是1年,而反过来,如果先学习后者,再来学VB,也许你只需要1个 星期就能学得非常熟练。几个重要的基础课程如果你是学生,或者如果你有充足的时间。我建议你仔细的掌握下面的知识。我的建议是针对那些希望在IT技术上有所成就的初学者。同时我还列出了一些书目,这些书 应该都还可以在书店买到。说实在的,我在读其他人的文章时最大的心愿就是希望作者列出一个书单。大学英语-不要觉得好笑。我极力推荐这门课程是因为没有专业文档的阅读能力是不可想象的。中文的翻译往往在猴年马月才会出来,而现在的许多出版社干脆就直接 把E 文印刷上去。学习的方法是强迫自己看原版的教材,开始会看不懂,用多了自然熟练。吃得苦下得狠心绝对是任何行业都需要的品质。计算机体系结构和汇编语言-关于体系结构的书遍地都是,而且也大同小异,倒是汇编有一本非常好的书《80x86汇编语言程序设计教程》(清华大学出版社,黑色封面,杨季文著)。你需要着重学习386后保护模式的程序设计。否则你在学习现代操作系统底层的一些东西的时候会觉得是在看天书。计算机操作系统原理-我们的开发总是在特定的操作系统上进行,如果不是,只有一种可能:你在自己实现一个操作系统。无论如何,操作系统原理是必读的。这就象我 们 为一个芯片制作外围设备时,芯片基本的工作时序是必需了解的。这一类书也很多,我没有发现哪一本书非常出众。只是觉得在看完了这些书后如果有空就应该看看 《In side Windows 2000》(微软出版社,我看的是E文版的,中文的书名想必是Windows 2000 技术内幕之类吧)。关于学习它的必要性,ZDNET上的另一篇文章已经有过论述。数据结构和算法-这门课程能够决定一个人程序设计水平的 高低,是一门核心课程。我首选的是清华版的(朱战立,刘天时)。很多人喜欢买C++版的,但我觉得没有必 要。C++的语法让算法实现过程变得复杂多了,而且许多老师喜欢用模块这一东西让算法变得更复杂。倒是在学完了C版的书以后再来浏览一下C++的版的书是 最好的 。软件工程-这门课程是越到后来就越发现它的重要,虽然刚开始看时就象看马哲一样不知所云。我的建议是看《实用软件工程》(黄色,清华)。不要花太多的时间去记条 条框框,看不懂就跳过去。在每次自己完成了一个软件设计任务(不管是练习还是工作)以后再来回顾回顾,每次都会有收获。Windows 程序设计-《北京大学出版社,Petzold著》我建议任何企图设计Windows 程序的人在学习VC以前仔细的学完它。而且前面的那本《Inside Windows 2000》也最好放到这本书的后面读。在这本书中, 没有C++,没有GUI,没有控件。有的就是如何用原始的C语言来完成Windows 程序设计。在学完了它以后,你才会发现VC其实是很容易学的。千万不要在没有看完这本书以前提前学习VC,你最好碰都不要碰。我知道的许多名校甚至都已经 用它作 为教材进行授课。可见其重要。上面的几门课程我认为是必学的重要课程(如果你想做Windows 程序员)。对 于其它的课程有这样简单的选择方法:如果你是计算机系的,请学好你所有的专业基础课。如果不是,请参照计算机系的课程表。如果你发现自己看一本书时无法看 下去了,请翻到书的最后,看看它的参考文献,找到它们并学习它们,再回头看这本书。如果一本书的书名中带有“原理”两个字,你一定不要去记忆它其中的细 节,你应该以一天至少50页的速度掌握其要领。尽可能多的在计算机上实践一种理论或者算法。你还可以在CSDN上阅读到许多书评。这些书评能够帮助你决定读什么样的书。日三省乎己每天读的书太多,容易让人迷失方向。一定要在每天晚上想想自己学了些什么,还有些什么相关的东西需要掌握,自己对什么最感兴趣,在一本书上花的时间太长还是 不够等等。同时也应该多想想未来最有可能出现的应用,这样能够让你不是追赶技术潮流而是引领技术潮流。同时,努力使用现在已经掌握的技术和理论去制作具有 一定新意的东西。坚持这样做能够让你真正成为一个软件“研发者”而不仅仅是一个CODER。把最多的时间花在学习上这是 对初学者最后的忠告。把每个星期玩CS或者CS的时间压缩到最少,不玩它们是最好的。同时,如果你的ASP技术已经能够来钱,甚至有公司请你兼职的话,这 就证明你的天分能够保证你在努力的学习之后取得更好的收益,你应该去做更复杂的东西。眼光放长远一些,这无论是对谁都是适用的。相信你已经能够决定是否学习C#或者什么时候去学它了。基础的重要性(程序员之路)学习编程有几年了,感觉走了不少弯路,而不少的学弟学妹又在重蹈我当初的覆辙,不免有些痛心。最近在网上也看了许多前辈们的经验建议,再结合自己的学习经历在这里谈谈基础的重要性,希望帮助大家少走些弯路。什么是基础呢?就是要把我们大学所学的离散数学,算法与数据结构,操作系统,计算机体系结构,编译原理等课程学好,对计算机的体系,CPU本身,操作系统内核,系统平台,面向对象编程,程序的性能等要有深层次的掌握。初学者可能体会不到这些基础的重要性,学习jsp,donet,mfc,vb的朋友甚至会对这些嗤之以鼻,但是一开始没学好基础就去学jsp或donet会产生很坏的影响,而且陷入其中不能自拔。我上大二的时候还对编程没什么概念,就上了门C++也不知道能干什么,老师说MFC也不知道是什么东西,看别的同学在学asp.net就跟着学了,然后就了解到.net,j2ee,php是什么了,就觉得软件开发就是用这些了,而上的那些专业课又与我们学的sqlserver啊,css啊,ajax啊,毫无关系,就感慨啊,还不如回家自学去就为一个文凭吗?还不如去培训,浪费这么多钱.于是天天基本上没去上什么课,天天就在做网站,几个学期就做了三个网站。感觉做这些网站就是学到些技巧,没什么进步,这些技巧就好比别人的名字,告诉你你就知道了,网上也都可以搜到。那时候就觉得把.net学好就行了,搞j2ee的比较难,搞api编程就别想了,操作系统更是望尘莫及了。后来随着学习的深入和看了网上许多前辈们的建议才对这些基础的重要性有所体会。虽然.net或java的开发并不直接用到汇编,操作系统这些,但是不掌握这些基础是有很大问题的,因为你只知其然不知其所有然,在mfc和.net里面控件一拖什么都做好了,很方便,但是出了问题可能就解决不了,有些在网上搜都搜不到。这就是基础没打好,不知道它的原理就不知道出错的原因。在学.net的时候常会讨论那些控件该不该用别人说尽量别用也不知道为什么?不让用是因为你在高层开发,你不知道它的原理出错了你可能解决不了,但其实是应该用的,不然人家开发它干嘛,但要在了解它的原理后去用就会很方便。要编写出优秀的代码同样要扎实的基础,如果数据结构和算法学的不好,怎么对程序的性能进行优化,怎样从类库中选择合适的数据结构。如果不了解操作系统,怎样能了解这些开发工具的原理,它们都是基于操作系统的。不了解汇编,编译原理,怎么知道程序运行时要多长时间要多少内存,就不能编出高效的代码。如果没有学好基础一开始就去学.net,java这些越往后就会觉得越吃力,它们涉及的技术太多了,而且不但在更新,对于三层啊,mvc,orm这些架构,你只会用也不明白为什么用,就感觉心里虚,感觉没学好。而你把面向对象,软件工程,设计模式这些基础学好了再去看这些就可以一不变应万变。大家不要被新名词、新技术所迷惑.NET、XML等等技术固然诱人,可是如果自己的基础不扎实,就像是在云里雾里行走一样,只能看到眼前,不能看到更远的地方。这些新鲜的技术掩盖了许多底层的原理,要想真正的学习技术还是走下云端,扎扎实实的把基础知识学好,有了这些基础,要掌握那些新技术也就很容易了。开始编程应该先学C/C++,系统api编程,因为它们更接近底层,学习他们更能搞清楚原理。学好了c/C++编程和基础,再去学习mfc,.net这些就会比较轻松,而且很踏实。假设学习VB编程需要4个月,学习基础课程和VC的程序设计需要1年。那么如果你先学VB,再来学习后者,时间不会减少,还是1年,而反过来,如果先学习后者,再来学VB,也许你只需要1个星期就能学得非常熟练。
2026年04月28日
2 阅读
0 评论
0 点赞
0-代码模板目录
基础算法 —— 代码模板链接 常用代码模板1——基础算法排序二分高精度前缀和与差分双指针算法位运算离散化区间合并数据结构 —— 代码模板链接 常用代码模板2——数据结构链表与邻接表:树与图的存储栈与队列:单调队列、单调栈kmpTrie并查集堆Hash表搜索与图论 —— 代码模板链接 常用代码模板3——搜索与图论DFS与BFS树与图的遍历:拓扑排序最短路最小生成树二分图:染色法、匈牙利算法数学知识 —— 代码模板链接 常用代码模板4——数学知识质数约数欧拉函数快速幂扩展欧几里得算法中国剩余定理高斯消元组合计数容斥原理简单博弈论
2026年04月18日
3 阅读
0 评论
0 点赞
常用代码模板4——数学知识
试除法判定质数 —— 模板题 AcWing 866. 试除法判定质数bool is_prime(int x) { if (x < 2) return false; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) return false; return true; }试除法分解质因数 —— 模板题 AcWing 867. 分解质因数void divide(int x) { for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { int s = 0; while (x % i == 0) x /= i, s ++ ; cout << i << ' ' << s << endl; } if (x > 1) cout << x << ' ' << 1 << endl; cout << endl; }朴素筛法求素数 —— 模板题 AcWing 868. 筛质数int primes[N], cnt; // primes[]存储所有素数 bool st[N]; // st[x]存储x是否被筛掉 void get_primes(int n) { for (int i = 2; i <= n; i ++ ) { if (st[i]) continue; primes[cnt ++ ] = i; for (int j = i + i; j <= n; j += i) st[j] = true; } }线性筛法求素数 —— 模板题 AcWing 868. 筛质数int primes[N], cnt; // primes[]存储所有素数 bool st[N]; // st[x]存储x是否被筛掉 void get_primes(int n) { for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++ ] = i; for (int j = 0; primes[j] <= n / i; j ++ ) { st[primes[j] * i] = true; if (i % primes[j] == 0) break; } } }试除法求所有约数 —— 模板题 AcWing 869. 试除法求约数vector<int> get_divisors(int x) { vector<int> res; for (int i = 1; i <= x / i; i ++ ) if (x % i == 0) { res.push_back(i); if (i != x / i) res.push_back(x / i); } sort(res.begin(), res.end()); return res; }约数个数和约数之和 —— 模板题 AcWing 870. 约数个数, AcWing 871. 约数之和如果 N = p1^c1 p2^c2 ... * pk^ck约数个数: (c1 + 1) (c2 + 1) ... * (ck + 1)约数之和: (p1^0 + p1^1 + ... + p1^c1) ... (pk^0 + pk^1 + ... + pk^ck)欧几里得算法 —— 模板题 AcWing 872. 最大公约数int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }求欧拉函数 —— 模板题 AcWing 873. 欧拉函数int phi(int x) { int res = x; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { res = res / i * (i - 1); while (x % i == 0) x /= i; } if (x > 1) res = res / x * (x - 1); return res; }筛法求欧拉函数 —— 模板题 AcWing 874. 筛法求欧拉函数int primes[N], cnt; // primes[]存储所有素数 int euler[N]; // 存储每个数的欧拉函数 bool st[N]; // st[x]存储x是否被筛掉 void get_eulers(int n) { euler[1] = 1; for (int i = 2; i <= n; i ++ ) { if (!st[i]) { primes[cnt ++ ] = i; euler[i] = i - 1; } for (int j = 0; primes[j] <= n / i; j ++ ) { int t = primes[j] * i; st[t] = true; if (i % primes[j] == 0) { euler[t] = euler[i] * primes[j]; break; } euler[t] = euler[i] * (primes[j] - 1); } } }快速幂 —— 模板题 AcWing 875. 快速幂求 m^k mod p,时间复杂度 O(logk)。int qmi(int m, int k, int p) { int res = 1 % p, t = m; while (k) { if (k&1) res = res * t % p; t = t * t % p; k >>= 1; } return res; }扩展欧几里得算法 —— 模板题 AcWing 877. 扩展欧几里得算法// 求x, y,使得ax + by = gcd(a, b) int exgcd(int a, int b, int &x, int &y) { if (!b) { x = 1; y = 0; return a; } int d = exgcd(b, a % b, y, x); y -= (a/b) * x; return d; }高斯消元 —— 模板题 AcWing 883. 高斯消元解线性方程组// a[N][N]是增广矩阵 int gauss() { int c, r; for (c = 0, r = 0; c < n; c ++ ) { int t = r; for (int i = r; i < n; i ++ ) // 找到绝对值最大的行 if (fabs(a[i][c]) > fabs(a[t][c])) t = i; if (fabs(a[t][c]) < eps) continue; for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 将绝对值最大的行换到最顶端 for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c]; // 将当前行的首位变成1 for (int i = r + 1; i < n; i ++ ) // 用当前行将下面所有的列消成0 if (fabs(a[i][c]) > eps) for (int j = n; j >= c; j -- ) a[i][j] -= a[r][j] * a[i][c]; r ++ ; } if (r < n) { for (int i = r; i < n; i ++ ) if (fabs(a[i][n]) > eps) return 2; // 无解 return 1; // 有无穷多组解 } for (int i = n - 1; i >= 0; i -- ) for (int j = i + 1; j < n; j ++ ) a[i][n] -= a[i][j] * a[j][n]; return 0; // 有唯一解 }递推法求组合数 —— 模板题 AcWing 885. 求组合数 I// c[a][b] 表示从a个苹果中选b个的方案数 for (int i = 0; i < N; i ++ ) for (int j = 0; j <= i; j ++ ) if (!j) c[i][j] = 1; else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;通过预处理逆元的方式求组合数 —— 模板题 AcWing 886. 求组合数 II首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]如果取模的数是质数,可以用费马小定理求逆元int qmi(int a, int k, int p) // 快速幂模板 { int res = 1; while (k) { if (k & 1) res = (LL)res * a % p; a = (LL)a * a % p; k >>= 1; } return res; } // 预处理阶乘的余数和阶乘逆元的余数 fact[0] = infact[0] = 1; for (int i = 1; i < N; i ++ ) { fact[i] = (LL)fact[i - 1] * i % mod; infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod; }Lucas定理 —— 模板题 AcWing 887. 求组合数 III若p是质数,则对于任意整数 1 <= m <= n,有:C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p) int qmi(int a, int k, int p) // 快速幂模板 { int res = 1 % p; while (k) { if (k & 1) res = (LL)res * a % p; a = (LL)a * a % p; k >>= 1; } return res; } int C(int a, int b, int p) // 通过定理求组合数C(a, b) { if (a < b) return 0; LL x = 1, y = 1; // x是分子,y是分母 for (int i = a, j = 1; j <= b; i --, j ++ ) { x = (LL)x * i % p; y = (LL) y * j % p; } return x * (LL)qmi(y, p - 2, p) % p; } int lucas(LL a, LL b, int p) { if (a < p && b < p) return C(a, b, p); return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p; }分解质因数法求组合数 —— 模板题 AcWing 888. 求组合数 IV当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:1. 筛法求出范围内的所有质数 2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ... 3. 用高精度乘法将所有质因子相乘 int primes[N], cnt; // 存储所有质数 int sum[N]; // 存储每个质数的次数 bool st[N]; // 存储每个数是否已被筛掉 void get_primes(int n) // 线性筛法求素数 { for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++ ] = i; for (int j = 0; primes[j] <= n / i; j ++ ) { st[primes[j] * i] = true; if (i % primes[j] == 0) break; } } } int get(int n, int p) // 求n!中的次数 { int res = 0; while (n) { res += n / p; n /= p; } return res; } vector<int> mul(vector<int> a, int b) // 高精度乘低精度模板 { vector<int> c; int t = 0; for (int i = 0; i < a.size(); i ++ ) { t += a[i] * b; c.push_back(t % 10); t /= 10; } while (t) { c.push_back(t % 10); t /= 10; } return c; } get_primes(a); // 预处理范围内的所有质数 for (int i = 0; i < cnt; i ++ ) // 求每个质因数的次数 { int p = primes[i]; sum[i] = get(a, p) - get(b, p) - get(a - b, p); } vector<int> res; res.push_back(1); for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘 for (int j = 0; j < sum[i]; j ++ ) res = mul(res, primes[i]);卡特兰数 —— 模板题 AcWing 889. 满足条件的01序列给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)NIM游戏 —— 模板题 AcWing 891. Nim游戏给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。NIM博弈不存在平局,只有先手必胜和先手必败两种情况。定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0公平组合游戏ICG若一个游戏满足:由两名玩家交替行动;在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;不能行动的玩家判负;则称该游戏为一个公平组合游戏。NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。有向图游戏给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。Mex运算设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:mex(S) = min{x}, x属于自然数,且x不属于SSG函数在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)定理有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
2026年04月18日
6 阅读
0 评论
0 点赞
常用代码模板3——搜索与图论
树与图的存储树是一种特殊的图,与图的存储方式相同。对于无向图中的边ab,存储两条有向边a->b, b->a。因此我们可以只考虑有向图的存储。(1) 邻接矩阵:ga 存储边a->b(2) 邻接表:// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点 int h[N], e[N], ne[N], idx; // 添加一条边a->b void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } // 初始化 idx = 0; memset(h, -1, sizeof h);树与图的遍历时间复杂度 O(n+m), n 表示点数,m 表示边数(1) 深度优先遍历 —— 模板题 AcWing 846. 树的重心int dfs(int u) { st[u] = true; // st[u] 表示点u已经被遍历过 for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) dfs(j); } }(2) 宽度优先遍历 —— 模板题 AcWing 847. 图中点的层次queue<int> q; st[1] = true; // 表示1号点已经被遍历过 q.push(1); while (q.size()) { int t = q.front(); q.pop(); for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true; // 表示点j已经被遍历过 q.push(j); } } }拓扑排序 —— 模板题 AcWing 848. 有向图的拓扑序列时间复杂度 O(n+m), n 表示点数,m 表示边数bool topsort() { int hh = 0, tt = -1; // d[i] 存储点i的入度 for (int i = 1; i <= n; i ++ ) if (!d[i]) q[ ++ tt] = i; while (hh <= tt) { int t = q[hh ++ ]; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (-- d[j] == 0) q[ ++ tt] = j; } } // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。 return tt == n - 1; }朴素dijkstra算法 —— 模板题 AcWing 849. Dijkstra求最短路 I时间复杂度 O(n^2+m), n 表示点数,m 表示边数int g[N][N]; // 存储每条边 int dist[N]; // 存储1号点到每个点的最短距离 bool st[N]; // 存储每个点的最短路是否已经确定 // 求1号点到n号点的最短路,如果不存在则返回-1 int dijkstra() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; for (int i = 0; i < n - 1; i ++ ) { int t = -1; // 在还未确定最短路的点中,寻找距离最小的点 for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // 用t更新其他点的距离 for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], dist[t] + g[t][j]); st[t] = true; } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }堆优化版dijkstra —— 模板题 AcWing 850. Dijkstra求最短路 II时间复杂度 O(mlogn), n 表示点数,m 表示边数typedef pair<int, int> PII; int n; // 点的数量 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N]; // 存储所有点到1号点的距离 bool st[N]; // 存储每个点的最短距离是否已确定 // 求1号点到n号点的最短距离,如果不存在,则返回-1 int dijkstra() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push({0, 1}); // first存储距离,second存储节点编号 while (heap.size()) { auto t = heap.top(); heap.pop(); int ver = t.second, distance = t.first; if (st[ver]) continue; st[ver] = true; for (int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > distance + w[i]) { dist[j] = distance + w[i]; heap.push({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }Bellman-Ford算法 —— 模板题 AcWing 853. 有边数限制的最短路时间复杂度 O(nm), n 表示点数,m 表示边数注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。int n, m; // n表示点数,m表示边数 int dist[N]; // dist[x]存储1到x的最短路距离 struct Edge // 边,a表示出点,b表示入点,w表示边的权重 { int a, b, w; }edges[M]; // 求1到n的最短路距离,如果无法从1走到n,则返回-1。 int bellman_ford() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。 for (int i = 0; i < n; i ++ ) { for (int j = 0; j < m; j ++ ) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; if (dist[b] > dist[a] + w) dist[b] = dist[a] + w; } } if (dist[n] > 0x3f3f3f3f / 2) return -1; return dist[n]; }spfa 算法(队列优化的Bellman-Ford算法) —— 模板题 AcWing 851. spfa求最短路时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数int n; // 总点数 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N]; // 存储每个点到1号点的最短距离 bool st[N]; // 存储每个点是否在队列中 // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1 int spfa() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; queue<int> q; q.push(1); st[1] = true; while (q.size()) { auto t = q.front(); q.pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入 { q.push(j); st[j] = true; } } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; }spfa判断图中是否存在负环 —— 模板题 AcWing 852. spfa判断负环时间复杂度 O(nm), n 表示点数,m 表示边数int n; // 总点数 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数 bool st[N]; // 存储每个点是否在队列中 // 如果存在负环,则返回true,否则返回false。 bool spfa() { // 不需要初始化dist数组 // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。 queue<int> q; for (int i = 1; i <= n; i ++ ) { q.push(i); st[i] = true; } while (q.size()) { auto t = q.front(); q.pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1; if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环 if (!st[j]) { q.push(j); st[j] = true; } } } } return false; }floyd算法 —— 模板题 AcWing 854. Floyd求最短路时间复杂度 O(n^3), n 表示点数初始化: for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) if (i == j) d[i][j] = 0; else d[i][j] = INF; // 算法结束后,d[a][b]表示a到b的最短距离 void floyd() { for (int k = 1; k <= n; k ++ ) for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }朴素版prim算法 —— 模板题 AcWing 858. Prim算法求最小生成树时间复杂度 O(n^2+m), n 表示点数,m 表示边数int n; // n表示点数 int g[N][N]; // 邻接矩阵,存储所有边 int dist[N]; // 存储其他点到当前最小生成树的距离 bool st[N]; // 存储每个点是否已经在生成树中 // 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和 int prim() { memset(dist, 0x3f, sizeof dist); int res = 0; for (int i = 0; i < n; i ++ ) { int t = -1; for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; if (i && dist[t] == INF) return INF; if (i) res += dist[t]; st[t] = true; for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]); } return res; }Kruskal算法 —— 模板题 AcWing 859. Kruskal算法求最小生成树时间复杂度 O(mlogm), n 表示点数,m 表示边数int n, m; // n是点数,m是边数 int p[N]; // 并查集的父节点数组 struct Edge // 存储边 { int a, b, w; bool operator< (const Edge &W)const { return w < W.w; } }edges[M]; int find(int x) // 并查集核心操作 { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int kruskal() { sort(edges, edges + m); for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集 int res = 0, cnt = 0; for (int i = 0; i < m; i ++ ) { int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find(a), b = find(b); if (a != b) // 如果两个连通块不连通,则将这两个连通块合并 { p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1) return INF; return res; }染色法判别二分图 —— 模板题 AcWing 860. 染色法判定二分图时间复杂度 O(n+m), n 表示点数,m 表示边数int n; // n表示点数 int h[N], e[M], ne[M], idx; // 邻接表存储图 int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色 // 参数:u表示当前节点,c表示当前点的颜色 bool dfs(int u, int c) { color[u] = c; for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (color[j] == -1) { if (!dfs(j, !c)) return false; } else if (color[j] == c) return false; } return true; } bool check() { memset(color, -1, sizeof color); bool flag = true; for (int i = 1; i <= n; i ++ ) if (color[i] == -1) if (!dfs(i, 0)) { flag = false; break; } return flag; }匈牙利算法 —— 模板题 AcWing 861. 二分图的最大匹配时间复杂度 O(nm), n 表示点数,m 表示边数int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数 int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边 int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个 bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过 bool find(int x) { for (int i = h[x]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true; if (match[j] == 0 || find(match[j])) { match[j] = x; return true; } } } return false; } // 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点 int res = 0; for (int i = 1; i <= n1; i ++ ) { memset(st, false, sizeof st); if (find(i)) res ++ ; }
2026年04月18日
4 阅读
0 评论
0 点赞
常用代码模板2——数据结构
单链表 —— 模板题 AcWing 826. 单链表// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点 int head, e[N], ne[N], idx; // 初始化 void init() { head = -1; idx = 0; } // 在链表头插入一个数a void insert(int a) { e[idx] = a, ne[idx] = head, head = idx ++ ; } // 将头结点删除,需要保证头结点存在 void remove() { head = ne[head]; }双链表 —— 模板题 AcWing 827. 双链表// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点 int e[N], l[N], r[N], idx; // 初始化 void init() { //0是左端点,1是右端点 r[0] = 1, l[1] = 0; idx = 2; } // 在节点a的右边插入一个数x void insert(int a, int x) { e[idx] = x; l[idx] = a, r[idx] = r[a]; l[r[a]] = idx, r[a] = idx ++ ; } // 删除节点a void remove(int a) { l[r[a]] = l[a]; r[l[a]] = r[a]; }栈 —— 模板题 AcWing 828. 模拟栈// tt表示栈顶 int stk[N], tt = 0; // 向栈顶插入一个数 stk[ ++ tt] = x; // 从栈顶弹出一个数 tt -- ; // 栈顶的值 stk[tt]; // 判断栈是否为空,如果 tt > 0,则表示不为空 if (tt > 0) { }队列 —— 模板题 AcWing 829. 模拟队列1. 普通队列:// hh 表示队头,tt表示队尾 int q[N], hh = 0, tt = -1; // 向队尾插入一个数 q[ ++ tt] = x; // 从队头弹出一个数 hh ++ ; // 队头的值 q[hh]; // 判断队列是否为空,如果 hh <= tt,则表示不为空 if (hh <= tt) { }2. 循环队列// hh 表示队头,tt表示队尾的后一个位置 int q[N], hh = 0, tt = 0; // 向队尾插入一个数 q[tt ++ ] = x; if (tt == N) tt = 0; // 从队头弹出一个数 hh ++ ; if (hh == N) hh = 0; // 队头的值 q[hh]; // 判断队列是否为空,如果hh != tt,则表示不为空 if (hh != tt) { }单调栈 —— 模板题 AcWing 830. 单调栈常见模型:找出每个数左边离它最近的比它大/小的数int tt = 0; for (int i = 1; i <= n; i ++ ) { while (tt && check(stk[tt], i)) tt -- ; stk[ ++ tt] = i; }单调队列 —— 模板题 AcWing 154. 滑动窗口常见模型:找出滑动窗口中的最大值/最小值int hh = 0, tt = -1; for (int i = 0; i < n; i ++ ) { while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口 while (hh <= tt && check(q[tt], i)) tt -- ; q[ ++ tt] = i; }KMP —— 模板题 AcWing 831. KMP字符串// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度 // 求模式串的next数组: for (int i = 2, j = 0; i <= m; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; } // 匹配 for (int i = 1, j = 0; i <= n; i ++ ) { while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == m) { j = ne[j]; // 匹配成功后的逻辑 } }Trie树 —— 模板题 AcWing 835. Trie字符串统计int son[N][26], cnt[N], idx; // 0号点既是根节点,又是空节点 // son[][]存储树中每个节点的子节点 // cnt[]存储以每个节点结尾的单词数量 // 插入一个字符串 void insert(char *str) { int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - 'a'; if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } cnt[p] ++ ; } // 查询字符串出现的次数 int query(char *str) { int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - 'a'; if (!son[p][u]) return 0; p = son[p][u]; } return cnt[p]; }并查集 —— 模板题 AcWing 836. 合并集合, AcWing 837. 连通块中点的数量(1) 朴素并查集:int p[N]; //存储每个点的祖宗节点 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) p[i] = i; // 合并a和b所在的两个集合: p[find(a)] = find(b);(2) 维护size的并查集:int p[N], size[N]; //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; size[i] = 1; } // 合并a和b所在的两个集合: size[find(b)] += size[find(a)]; p[find(a)] = find(b);(3) 维护到祖宗节点距离的并查集:int p[N], d[N]; //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离 // 返回x的祖宗节点 int find(int x) { if (p[x] != x) { int u = find(p[x]); d[x] += d[p[x]]; p[x] = u; } return p[x]; } // 初始化,假定节点编号是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; d[i] = 0; } // 合并a和b所在的两个集合: p[find(a)] = find(b); d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量堆 —— 模板题 AcWing 838. 堆排序, AcWing 839. 模拟堆// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1 // ph[k]存储第k个插入的点在堆中的位置 // hp[k]存储堆中下标是k的点是第几个插入的 int h[N], ph[N], hp[N], size; // 交换两个点,及其映射关系 void heap_swap(int a, int b) { swap(ph[hp[a]],ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]); } void down(int u) { int t = u; if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; if (u != t) { heap_swap(u, t); down(t); } } void up(int u) { while (u / 2 && h[u] < h[u / 2]) { heap_swap(u, u / 2); u >>= 1; } } // O(n)建堆 for (int i = n / 2; i; i -- ) down(i);一般哈希 —— 模板题 AcWing 840. 模拟散列表(1) 拉链法int h[N], e[N], ne[N], idx; // 向哈希表中插入一个数 void insert(int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } // 在哈希表中查询某个数是否存在 bool find(int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1; i = ne[i]) if (e[i] == x) return true; return false; }(2) 开放寻址法int h[N]; // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置 int find(int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0; } return t; }字符串哈希 —— 模板题 AcWing 841. 字符串哈希核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果typedef unsigned long long ULL; ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64 // 初始化 p[0] = 1; for (int i = 1; i <= n; i ++ ) { h[i] = h[i - 1] * P + str[i]; p[i] = p[i - 1] * P; } // 计算子串 str[l ~ r] 的哈希值 ULL get(int l, int r) { return h[r] - h[l - 1] * p[r - l + 1]; }C++ STL简介- **vector**, 变长数组,倍增的思想 - `size()` 返回元素个数 - `empty()` 返回是否为空 - `clear()` 清空 - `front()` / `back()` - `push_back()` / `pop_back()` - `begin()` / `end()` - `[]` - 支持比较运算,按字典序 - **pair<int, int>** - `first`, 第一个元素 - `second`, 第二个元素 - 支持比较运算,以first为第一关键字,以second为第二关键字(字典序) - **string**,字符串 - `size()` / `length()` 返回字符串长度 - `empty()` - `clear()` - `substr(起始下标,(子串长度))` 返回子串 - `c_str()` 返回字符串所在字符数组的起始地址 - **queue**, 队列 - `size()` - `empty()` - `push()` 向队尾插入一个元素 - `front()` 返回队头元素 - `back()` 返回队尾元素 - `pop()` 弹出队头元素 - **priority_queue**, 优先队列,默认是大根堆 - `size()` - `empty()` - `push()` 插入一个元素 - `top()` 返回堆顶元素 - `pop()` 弹出堆顶元素 - 定义成小根堆的方式:`priority_queue<int, vector<int>, greater<int>> q;` - **stack**, 栈 - `size()` - `empty()` - `push()` 向栈顶插入一个元素 - `top()` 返回栈顶元素 - `pop()` 弹出栈顶元素 - **deque**, 双端队列 - `size()` - `empty()` - `clear()` - `front()` / `back()` - `push_back()` / `pop_back()` - `push_front()` / `pop_front()` - `begin()` / `end()` - `[]` - **set, map, multiset, multimap**, 基于平衡二叉树(红黑树),动态维护有序序列 - `size()` - `empty()` - `clear()` - `begin()` / `end()` - `++`, `--` 返回前驱和后继,时间复杂度 O(logn) - **set / multiset** - `insert()` 插入一个数 - `find()` 查找一个数 - `count()` 返回某一个数的个数 - `erase()` - (1) 输入是一个数x,删除所有x O(k + logn) - (2) 输入一个迭代器,删除这个迭代器 - `lower_bound()` / `upper_bound()` - `lower_bound(x)` 返回大于等于x的最小的数的迭代器 - `upper_bound(x)` 返回大于x的最小的数的迭代器 - **map / multimap** - `insert()` 插入的数是一个pair - `erase()` 输入的参数是pair或者迭代器 - `find()` - `[]` 注意multimap不支持此操作。 时间复杂度是 O(logn) - `lower_bound()` / `upper_bound()` - **unordered_set, unordered_map, unordered_multiset, unordered_multimap**, 哈希表 - 和上面类似,增删改查的时间复杂度是 O(1) - 不支持 `lower_bound()` / `upper_bound()`, 迭代器的`++`,`--` - **bitset**, 圧位 - `bitset<10000> s;` - `~`, `&`, `|`, `^` - `>>`, `<<` - `==`, `!=` - `[]` - `count()` 返回有多少个1 - `any()` 判断是否至少有一个1 - `none()` 判断是否全为0 - `set()` 把所有位置成1 - `set(k, v)` 将第k位变成v - `reset()` 把所有位变成0 - `flip()` 等价于`~` - `flip(k)` 把第k位取反
2026年04月18日
3 阅读
0 评论
0 点赞
1
2
...
5