2235 字
11 分钟
经验-2025年南大智科预推免
⚠️ 注:本文信息借助不可信回忆+AI生成,不构成任何形式的指南或保证。本文信息可能过时或不适用于你的具体情况。因参考本文产生的任何后果,作者概不负责。
时间:2025年9月12日-9月13日

形式和考察范围等和夏令营基本一致

笔试#

考察了:

  1. 命题逻辑、一阶逻辑

  2. 已知p1, p2, p3 = softmax(z1, z2, z3),真实标签为y1, y2, y3,求 softmax 关于输入求导,并结合交叉熵损失

    设:

    • 模型输出(logits)为:

      z=[z1,z2,z3]\mathbf{z} = [z_1, z_2, z_3]
    • 经过 Softmax 后的预测概率为:

      pi=softmax(zi)=ezik=13ezk=eziS,S=k=13ezkp_i = \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{k=1}^3 e^{z_k}} = \frac{e^{z_i}}{S}, \quad S = \sum_{k=1}^3 e^{z_k}
    • 真实标签为 one-hot 编码形式:

      y=[y1,y2,y3],其中 yi{0,1},yi=1\mathbf{y} = [y_1, y_2, y_3], \quad \text{其中 } y_i \in \{0,1\}, \sum y_i = 1
    • 交叉熵损失函数为:

      L=i=13yilogpiL = -\sum_{i=1}^3 y_i \log p_i

    我们的目标是求:损失 LL 对 logits zjz_j 的导数 Lzj\frac{\partial L}{\partial z_j},这在反向传播中非常重要。

    先计算 pizj\frac{\partial p_i}{\partial z_j}

    情况 1:当 i=ji = j

    pizi=zi(eziS)=eziSezieziS2=eziSe2ziS2=pipi2=pi(1pi)\frac{\partial p_i}{\partial z_i} = \frac{\partial}{\partial z_i} \left( \frac{e^{z_i}}{S} \right) = \frac{e^{z_i} \cdot S - e^{z_i} \cdot e^{z_i}}{S^2} = \frac{e^{z_i}}{S} - \frac{e^{2z_i}}{S^2} = p_i - p_i^2 = p_i(1 - p_i)

    情况 2:当 iji \neq j

    pizj=zj(eziS)=ezi(ezjS2)=eziezjS2=pipj\frac{\partial p_i}{\partial z_j} = \frac{\partial}{\partial z_j} \left( \frac{e^{z_i}}{S} \right) = e^{z_i} \cdot \left( -\frac{e^{z_j}}{S^2} \right) = -\frac{e^{z_i} e^{z_j}}{S^2} = -p_i p_j

    所以统一写成:

    pizj={pi(1pi),i=jpipj,ij=pi(δijpj)\frac{\partial p_i}{\partial z_j} = \begin{cases} p_i(1 - p_i), & i = j \\ -p_i p_j, & i \neq j \end{cases} = p_i (\delta_{ij} - p_j)

    其中 δij\delta_{ij} 是 Kronecker delta 函数(i=ji=j 时为 1,否则为 0)。

    Lzj=zj(i=13yilogpi)=i=13yi1pipizj\frac{\partial L}{\partial z_j} = \frac{\partial}{\partial z_j} \left( -\sum_{i=1}^3 y_i \log p_i \right) = -\sum_{i=1}^3 y_i \cdot \frac{1}{p_i} \cdot \frac{\partial p_i}{\partial z_j}

    代入上面的导数表达式:

    Lzj=i=13yi1pipi(δijpj)=i=13yi(δijpj)\frac{\partial L}{\partial z_j} = -\sum_{i=1}^3 y_i \cdot \frac{1}{p_i} \cdot p_i (\delta_{ij} - p_j) = -\sum_{i=1}^3 y_i (\delta_{ij} - p_j)

    拆开求和:

    =(i=13yiδiji=13yipj)=(yjpji=13yi)= -\left( \sum_{i=1}^3 y_i \delta_{ij} - \sum_{i=1}^3 y_i p_j \right) = -\left( y_j - p_j \sum_{i=1}^3 y_i \right)

    由于 yi=1\sum y_i = 1(one-hot 标签),所以:

    Lzj=(yjpj)=pjyj\frac{\partial L}{\partial z_j} = -(y_j - p_j) = p_j - y_j

    总结

    令:

    • p=softmax(z)\mathbf{p} = \text{softmax}(\mathbf{z})
    • y\mathbf{y} 为 one-hot 真实标签

    则:

    zL=py\nabla_{\mathbf{z}} L = \mathbf{p} - \mathbf{y}

    这个结果意味着,在实现分类网络时,Softmax + CrossEntropyLoss 的反向传播梯度就是预测概率与真实标签之差

    此外,现代框架(如 PyTorch)提供 LogSoftmaxNLLLoss 或直接 CrossEntropyLoss,内部已融合优化,避免显式计算 log(pi)\log(p_i) 带来的数值问题。

  3. 动态规划:nxm 矩阵,左下角到右上角,每一步可以→↑↗,求方法总数

  4. 判断完全二叉树

  5. 关系分类(关系是不是反射的?)、判断映射(满射?)

  6. 贝叶斯定理

  7. 线性回归 wx+b

  8. CNN 参数量计算

机试#

前两题为强制使用python的算法题,后两题为强制使用C++的算法题。

Q1#

略,较为简单

Q2 最长“山形”子序列#

题目描述#

给定一个长度为 nn 的整数数组 aa,求最长的子序列长度,该子序列满足:先严格递增,后严格递减

特别地,以下情况也被视为合法的“山形”子序列:

  • 仅包含严格递增部分(不下降部分)
  • 仅包含严格递减部分(不上升部分)
  • 仅包含一个元素

子序列不要求连续。

输入格式

第一行一个正整数 nn,表示数组长度。
第二行 nn 个整数 a1,a2,,ana_1, a_2, \dots, a_n,表示数组元素。

输出格式

输出一个整数,表示满足条件的最长子序列的长度。

样例

样例输入 1

7
1 3 2 5 7 6 4

样例输出 1

5

样例解释 1 一个最长的合法子序列为:1 3 5 7 41 3 5 6 4,长度为 5。

样例输入 2

5
5 4 3 2 1

样例输出 2

5

样例解释 2 整个序列严格递减,是合法的(只有下降部分)。

样例输入 3

5
1 2 3 4 5

样例输出 3

5

样例解释 3 整个序列严格递增,是合法的(只有上升部分)。

数据范围

  • 对于 30%30\% 的数据,1n1001 \leq n \leq 100
  • 对于 60%60\% 的数据,1n10001 \leq n \leq 1000
  • 对于 100%100\% 的数据,1n1051 \leq n \leq 10^51ai1091 \leq a_i \leq 10^9

提示 子序列可以从原序列中不连续地选取元素,但必须保持原有顺序。

解题思路#

我们可以使用动态规划:

  1. 计算以每个位置 i 结尾的最长严格递增子序列长度 dp_inc[i]
  2. 计算以每个位置 i 开始的最长严格递减子序列长度 dp_dec[i]
  3. 对于每个位置 i,它可以作为“山顶”,则通过它的最长山形子序列长度为:dp_inc[i] + dp_dec[i] - 1(减1是因为山顶被计算了两次)
  4. 答案就是所有位置中的最大值
def solve():
n = int(input())
if n == 0:
return 0
a = list(map(int, input().split()))
# dp_inc[i]: 以位置 i 结尾的最长严格递增子序列长度
dp_inc = [1] * n
for i in range(1, n):
for j in range(i):
if a[j] < a[i]:
dp_inc[i] = max(dp_inc[i], dp_inc[j] + 1)
# dp_dec[i]: 从位置 i 开始的最长严格递减子序列长度
dp_dec = [1] * n
for i in range(n-2, -1, -1):
for j in range(i+1, n):
if a[i] > a[j]:
dp_dec[i] = max(dp_dec[i], dp_dec[j] + 1)
# 找到最长的山形子序列
max_length = 0
for i in range(n):
length = dp_inc[i] + dp_dec[i] - 1
max_length = max(max_length, length)
return max_length
# 读取输入并输出结果
print(solve())

Q3 字符串平移拼接序列的第 K 个字符#

题目描述#

定义一个字符串生成序列 S0,S1,S2,S_0, S_1, S_2, \dots,规则如下:

  • 初始值:S0="a"S_0 = \texttt{"a"}
  • 递推规则:对于 i1i \geq 1,有 Si=Si1+shift(Si1)S_i = S_{i-1} + \text{shift}(S_{i-1})
    • 其中 shift(T)\text{shift}(T) 表示将字符串 TT 中每个字符按字母表向后平移一位:
      • ’a’’b’, ’b’’c’, , ’y’’z’, ’z’’a’\texttt{'a'} \to \texttt{'b'},\ \texttt{'b'} \to \texttt{'c'},\ \dots,\ \texttt{'y'} \to \texttt{'z'},\ \texttt{'z'} \to \texttt{'a'}
      • 即循环右移:字符 cc 变为 (c’a’+1)mod26+’a’(c - \texttt{'a'} + 1) \bmod 26 + \texttt{'a'}

例如:

  • S0="a"S_0 = \texttt{"a"}
  • S1="a" + shift("a")="a" + "b"="ab"S_1 = \texttt{"a" + shift("a")} = \texttt{"a" + "b"} = \texttt{"ab"}
  • S2="ab" + shift("ab")="ab" + "bc"="abbc"S_2 = \texttt{"ab" + shift("ab")} = \texttt{"ab" + "bc"} = \texttt{"abbc"}
  • S3="abbc" + shift("abbc")="abbc" + "bccd"="abbcbccd"S_3 = \texttt{"abbc" + shift("abbc")} = \texttt{"abbc" + "bccd"} = \texttt{"abbcbccd"}
  • S4="abbcbccd" + shift("abbcbccd")="abbcbccd" + "bccdcdee"="abbcbccdbccdcdee"S_4 = \texttt{"abbcbccd" + shift("abbcbccd")} = \texttt{"abbcbccd" + "bccdcdee"} = \texttt{"abbcbccdbccdcdee"}

给定一个正整数 kk,请求出在某个足够大的 nn 下,SnS_n 的第 kk 个字符(1-indexed)。

注意:由于 SnS_n 的长度随 nn 指数增长,你不需要构造整个字符串,而应设计高效算法直接定位第 kk 个字符。

输入格式

一行一个正整数 kk,表示查询位置(从 1 开始计数)。

输出格式

输出一个字符,表示序列中第 kk 个位置上的小写字母。

样例

样例输入 1

1

样例输出 1

a

样例输入 2

3

样例输出 2

b

样例解释 2 S2="abbc"S_2 = \texttt{"abbc"},第 3 个字符是 ’b’\texttt{'b'}

样例输入 3

7

样例输出 3

c

样例解释 3 S3="abbcbccd"S_3 = \texttt{"abbcbccd"},第 7 个字符是 ’c’\texttt{'c'}

数据范围

  • 对于 30%30\% 的数据,1k1031 \leq k \leq 10^3
  • 对于 60%60\% 的数据,1k1061 \leq k \leq 10^6
  • 对于 100%100\% 的数据,1k10181 \leq k \leq 10^{18}

提示

  • 字符串长度满足:len(S0)=1\text{len}(S_0) = 1len(Si)=2×len(Si1)\text{len}(S_i) = 2 \times \text{len}(S_{i-1})
    • len(Si)=2i\text{len}(S_i) = 2^i
  • 可通过递归方式求解:若 k2n1k \leq 2^{n-1},则答案在左半部分;否则在右半部分(需还原平移操作)。

解题思路#

我们观察到:

  • S0="a"S_0 = \texttt{"a"}, 长度为 20=12^0 = 1
  • Si=Si1+shift(Si1)S_i = S_{i-1} + \text{shift}(S_{i-1}),长度为 2i2^i

关键性质:

  • 每次操作将字符串长度翻倍
  • 前半部分就是 Si1S_{i-1}
  • 后半部分是前半部分整体字符向后平移一位(z→a)

因此我们可以递归求解第 kk 个字符:

  1. 找到最小的 nn 使得 2nk2^n \geq k,即确定 kk 所在的字符串层级
  2. 如果 k==1k == 1 且当前视为 S0S_0,返回 'a'
  3. k2n1k \leq 2^{n-1},说明在左半部分,递归处理 Sn1S_{n-1}
  4. k>2n1k > 2^{n-1},说明在右半部分,对应位置为 k2n1k - 2^{n-1},递归得到该位置在左半部分的原始字符,再将其逆平移(向前移一位)

注意:右半部分比左半部分每个字符大1,所以我们找到对应位置的字符后要还原。

#include <iostream>
using namespace std;
char solve(long long k) {
if (k == 1) {
return 'a';
}
// 找到最小的 n 使得 2^n >= k
long long length = 1;
while (length < k) {
length <<= 1; // 相当于 length *= 2
}
long long half = length >> 1; // 左半部分长度 = length / 2
if (k <= half) {
// 在左半部分,递归处理
return solve(k);
} else {
// 在右半部分,对应左半部分的位置是 k - half
char c = solve(k - half);
// 右半部分字符 = 左半部分字符 + 1 (循环)
return 'a' + (c - 'a' + 1) % 26;
}
}
int main() {
long long k;
cin >> k;
cout << solve(k) << endl;
return 0;
}

复杂度分析#

  • 时间复杂度:O(logk)O(\log k),每次递归将问题规模减半
  • 空间复杂度:O(logk)O(\log k),递归栈深度

该算法可以高效处理 k1018k \leq 10^{18} 的情况。

Q4#

略,较为简单

面试#

自我介绍 英文:未来研究计划 项目 为何项目和科研兴趣不一致 机器学习 决策树 对三维重建有什么了解 自己的优点和缺点 transformer

经验-2025年南大智科预推免
https://stivine.github.io/posts/experience-nju-is-early-test-for-postgraduate-recommendation/
作者
藤君
发布于
2025-09-13
许可协议
CC BY-NC-SA 4.0