[{"content":"本文不会严格地从泛函分析或者调和分析的角度展开，而是从直觉上理解傅里叶变换和谱域的含义。重点不是证明定理，而是弄清楚为什么换到谱域之后，很多原本缠在一起的问题会突然变得清楚。\n谱域 一个信号最直接的表示方式，是看它在每个位置或者每个时刻的取值。\n比如一个关于时间的函数：\n$$\rf(t)\r$$这时候我们关心的是：在时间 $t$ 这个位置上，信号的值是多少。\n但还有另一种看法：不再直接盯着每个时刻的值，而是问这个信号由哪些基本的振动模式组成。也就是说，我们不问“这里的值是多少”，而是问“这个信号里面有哪些频率”。\n这就是谱域的视角。\n时域关心的是：\n某个时刻发生了什么。\n频域或者谱域关心的是：\n某种频率成分有多强。\n同一个信号，在两个域里描述的是同一件事，只是观察的坐标系不同。\n从波开始 最简单的周期波可以写成：\n$$\r\\sin(\\omega t)\r$$其中 $\\omega$ 控制振动的快慢。\n如果 $\\omega$ 很小，函数变化得比较慢；如果 $\\omega$ 很大，函数变化得就比较快。\n从图像上看，低频信号通常比较平滑，高频信号则变化得更剧烈。\n更一般地，可以写成：\n$$\rA\\sin(\\omega t + \\varphi)\r$$这里有三个比较重要的量：\n$A$ 是振幅，控制这段波有多强 $\\omega$ 是频率，控制这段波变化得多快 $\\varphi$ 是相位，控制这段波整体偏移到哪里 如果只看一个简单的正弦波，这些东西并不复杂。但问题在于，现实中的信号通常不是这么干净的一个波，而是很多波混在一起。\n复杂信号是简单波的叠加 傅里叶分析最核心的想法是：\n很多复杂信号都可以看成一堆简单波的叠加。\n比如我们可以把一个周期函数写成类似这样的形式：\n$$\rf(t) = a_0 + \\sum_{n=1}^{\\infty} a_n\\cos(n\\omega t) + b_n\\sin(n\\omega t)\r$$这里每一项都对应一个频率。\n$a_n$ 和 $b_n$ 这些系数描述的是：某个频率的成分在原信号中占多少。\n所以原来的 $f(t)$ 是在时间上描述信号，而这些系数是在频率上描述信号。傅里叶分析做的事情，就是在这两种描述之间切换。\n这件事情和向量的坐标表示很像。一个二维向量可以用标准基表示，也可以换一组基表示。向量本身没有变，变的是我们描述它的方式。\n傅里叶变换也是类似的：信号本身没有变，只是从“按时间描述”变成了“按频率描述”。\n频谱 频谱可以理解成一个信号在不同频率上的分布。\n如果某个频率对应的幅值很大，就说明这个信号里这个频率的成分很强。\n如果某个频率对应的幅值接近 $0$，就说明这个信号里几乎没有这个频率。\n因此频谱并不是原信号之外额外多出来的东西，而是原信号的另一种表达方式。\n举个比较直观的例子，假设一个信号是两个正弦波叠加出来的：\n$$\rf(t) = \\sin(t) + 0.5\\sin(5t)\r$$在时域里，我们看到的是一个起伏的曲线。\n但在频域里，它主要就只有两个成分：一个频率是 $1$，另一个频率是 $5$，而且频率 $5$ 的强度更弱一些。\n时域里的图像可能看起来有些复杂，但频域里反而很清楚。\n低频和高频 低频通常代表变化缓慢的整体结构。\n高频通常代表变化剧烈的局部细节。\n对于图像来说，低频部分通常包含大致的明暗和轮廓，高频部分则包含边缘、纹理、噪声这些东西。\n对于声音来说，低频可能更接近低沉的部分，高频则更接近尖锐的部分。\n当然这不是绝对的，只是一种常见的直觉。真正重要的是：频率描述的是变化的尺度。\n低频变化慢，高频变化快。\n所以很多去噪、压缩、平滑方法都会在频域里做文章。因为在频域里，我们可以比较自然地说：\n哪些频率要保留 哪些频率要削弱 哪些频率可以直接丢掉 这比直接在原始信号上动手，有时候要清楚很多。\n傅里叶变换 傅里叶级数主要用来处理周期信号，而傅里叶变换可以处理更一般的非周期信号。\n连续傅里叶变换通常写成：\n$$\r\\hat{f}(\\omega) = \\int_{-\\infty}^{+\\infty} f(t)e^{-i\\omega t}dt\r$$这里的 $\\hat{f}(\\omega)$ 表示信号 $f(t)$ 在频率 $\\omega$ 上的成分。\n也可以通过反变换从频域回到时域：\n$$\rf(t) = \\frac{1}{2\\pi}\\int_{-\\infty}^{+\\infty}\\hat{f}(\\omega)e^{i\\omega t}d\\omega\r$$所以傅里叶变换不是把信息丢掉了，而是把同一个对象换了一种写法。\n只要条件合适，从时域到频域，再从频域回到时域，理论上是可以恢复原信号的。\n为什么会出现复数 傅里叶变换里经常会出现：\n$$\re^{i\\omega t}\r$$这看起来好像突然把问题变复杂了，但其实复指数只是把正弦和余弦统一写在了一起。\n根据欧拉公式：\n$$\re^{i\\theta} = \\cos\\theta + i\\sin\\theta\r$$因此 $e^{i\\omega t}$ 本质上仍然是在描述振动。\n使用复数的好处是很多运算会变得更统一。比如求导、积分、平移、卷积这些操作，在复指数形式下通常会有很漂亮的表达。\n这也是数学里经常出现的一种情况：引入一个看起来更抽象的对象，不是为了让问题更玄，而是为了让结构更清楚。\n离散傅里叶变换 实际计算机里处理的通常不是连续函数，而是一串离散采样值。\n比如：\n$$\rx_0,x_1,\\cdots,x_{N-1}\r$$这时候用的是离散傅里叶变换，也就是 DFT：\n$$\rX_k = \\sum_{n=0}^{N-1} x_n e^{-i2\\pi kn/N}\r$$其中 $X_k$ 表示第 $k$ 个离散频率上的成分。\n这个式子的含义和连续傅里叶变换是一致的，都是把原信号拆到不同频率上。区别只是：连续傅里叶变换面对的是连续频率，而 DFT 面对的是有限个离散频率。\n如果把一串长度为 $N$ 的数据看成一个向量，那么 DFT 也可以看成一次矩阵乘法。这个矩阵的每一行都对应一种频率模式。\n因此从线性代数的角度看，DFT 其实也是在换基。\nFFT 直接计算 DFT 的复杂度是 $O(n^2)$。\n因为每一个频率点都要和所有采样点做一次求和。\n快速傅里叶变换，也就是 FFT，利用了复指数的周期性和对称性，把复杂度降到了：\n$$\rO(n\\log n)\r$$这也是傅里叶变换在工程里特别重要的原因之一。它不只是一个漂亮的数学表达，而且真的算得动。\n很多信号处理、图像处理、音频处理、科学计算中的算法，背后都离不开 FFT。\n从谱域看滤波 滤波可以理解成对不同频率成分分别加权。\n如果一个信号的频谱是 $\\hat{f}(\\omega)$，滤波器是 $H(\\omega)$，那么滤波后的频谱可以写成：\n$$\r\\hat{g}(\\omega) = H(\\omega)\\hat{f}(\\omega)\r$$这里的 $H(\\omega)$ 决定了每个频率要保留多少。\n比如低通滤波器会保留低频，压制高频。\n高通滤波器则反过来，压制低频，保留高频。\n从这个角度看，滤波器本质上就是一个作用在谱上的函数。\n这也是谱域很有用的地方：很多在原空间里比较纠缠的操作，换到谱域之后就变成了简单的乘法。\n卷积和乘法 傅里叶变换里有一个非常重要的性质：\n时域中的卷积，对应频域中的乘法。\n也就是：\n$$\r\\widehat{f * g} = \\hat{f}\\hat{g}\r$$卷积在原空间里看起来像一个滑动加权平均，每个位置都要和附近的一片区域发生关系。\n但到了频域里，它就变成了每个频率上的逐点相乘。\n这件事情非常重要，因为它说明谱域不只是换一种方式观察信号，有时候还可以直接改变计算方式。\n原来复杂的整体耦合，变成了每个频率分量上的独立处理。\n采样和混叠 傅里叶变换还有一个很实际的问题：计算机里的信号是采样得到的。\n如果采样频率不够高，那么高频信号可能会伪装成低频信号，这种现象叫混叠。\n直观上说，就是你看得不够密，结果把快速变化的东西误认为是慢速变化。\n所以在把连续信号离散化的时候，采样频率非常重要。\n这也是为什么很多实际系统在采样前会先做低通滤波，先把过高的频率压掉，避免它们在离散化之后混进低频里。\n图上的傅里叶变换 傅里叶变换里，信号通常定义在一条时间轴或者一片规则网格上。\n但如果信号不是定义在规则空间里，而是定义在图上呢？\n比如一个社交网络里，每个节点有一个用户特征；或者一个分子图里，每个节点有一个原子特征。这时候信号的定义域不是时间轴，也不是二维平面，而是一张图。\n这就是图信号处理（Graph Signal Processing, GSP）关心的问题。\n如果图上有 $n$ 个节点，那么一个图信号可以写成：\n$$\rx \\in \\mathbb{R}^n\r$$其中 $x_i$ 表示第 $i$ 个节点上的信号值。\n如果是在普通的一维信号里，相邻位置是天然存在的，比如 $t$ 和 $t + \\Delta t$ 很接近。但图上没有这么规整的坐标，节点之间的邻近关系由边决定。\n所以图上的问题是：我们能不能也像普通傅里叶变换那样，找到一组“基本振动模式”，然后把图信号分解到这些模式上？\n这就是图傅里叶变换的出发点。\n图拉普拉斯矩阵 在普通傅里叶变换里，频率基来自正弦和余弦；而在图上，没有天然的正弦波，所以需要从图本身构造一组“频率基”。\n一个常见的选择是图拉普拉斯矩阵：\n$$\rL = D - W\r$$其中 $W$ 是邻接矩阵，$D$ 是度矩阵。\n直觉上，$W$ 描述的是哪些节点相连，以及连接有多强；$D$ 则记录每个节点总共连出去多少权重。\n图拉普拉斯矩阵最重要的作用之一，是刻画一个图信号在图上是否平滑。\n所谓平滑，指的是相连的节点上，信号值不要差太多。\n如果两个节点之间有边，而且边权很大，那么我们就倾向于认为这两个节点应该更相似。反过来，如果这两个节点上的信号值差很多，就说明这个信号在图上变化得比较剧烈。\n这个“不平滑程度”可以用下面这个式子衡量：\n$$\rx^TLx = \\frac{1}{2}\\sum_{i,j}W_{ij}(x_i - x_j)^2\r$$这个式子很直观：\n如果 $i$ 和 $j$ 没有边，那么 $W_{ij}=0$，这两个节点的差异不参与计算 如果 $i$ 和 $j$ 有边，而且 $x_i$ 和 $x_j$ 很接近，那么贡献很小 如果 $i$ 和 $j$ 有边，但 $x_i$ 和 $x_j$ 差很多，那么贡献会很大 因此 $x^TLx$ 越小，说明图信号越平滑；$x^TLx$ 越大，说明图信号在相邻节点之间变化越剧烈。\n图上的频率 如果图是无向图，那么 $L$ 是对称矩阵，可以进行特征分解：\n$$\rL = U\\Lambda U^T\r$$这里的 $U$ 就可以看成图上的傅里叶基，$\\Lambda$ 中的特征值则对应图上的频率。\n为什么可以这么理解？\n因为这些特征向量是由图本身决定的基本模式。每一个特征向量 $u_i$ 都表示一种图上的变化形态，而对应的特征值 $\\lambda_i$ 描述这种变化形态有多“不平滑”。\n特别地，如果图是连通的，那么最小的特征值是：\n$$\r\\lambda_1 = 0\r$$它对应的特征向量是一个常数向量，也就是所有节点上的取值都一样。\n这个模式当然是最平滑的，因为相邻节点之间没有任何差异。\n随着特征值变大，对应的特征向量在相邻节点之间通常会变化得更剧烈。因此在图信号处理中，较小的特征值对应低频，较大的特征值对应高频。\n对于一个图信号 $x$，它的图傅里叶变换可以写成：\n$$\r\\hat{x} = U^Tx\r$$反变换则是：\n$$\rx = U\\hat{x}\r$$这个形式和普通傅里叶变换的味道是一样的：原信号还是那个原信号，只是从节点空间换到了谱空间。\n在节点空间里，$x_i$ 表示第 $i$ 个节点上的信号值。\n在谱空间里，$\\hat{x}_i$ 表示这个信号在第 $i$ 个图频率上的成分。\n所以图傅里叶变换做的事情，就是把“每个节点上的值”，换成“每种图频率上的强度”。\n不过这里需要注意：图傅里叶变换和普通傅里叶变换并不是完全同一个东西。\n普通傅里叶变换里的频率来自规则空间上的平移结构。时间轴或者二维网格是很规整的，所以正弦、余弦、复指数这些函数有非常明确的“振动频率”。\n但一般的图没有天然的方向、距离和周期结构。图上的“频率”不是先验给定的，而是从图拉普拉斯矩阵的特征分解里定义出来的。\n所以更准确地说，GSP 不是把普通傅里叶变换原封不动搬到图上，而是保留了傅里叶分析里的一个核心思想：\n找一组由空间结构决定的基本模式，然后把信号分解到这些模式上。\n在规则网格上，这组模式是正弦和余弦；在图上，这组模式是图拉普拉斯矩阵的特征向量。\n正则化和谱域 把 $x = U\\hat{x}$ 代进去，还可以得到：\n$$\rx^TLx = \\hat{x}^T\\Lambda\\hat{x} = \\sum_i \\lambda_i \\hat{x}_i^2\r$$这就很有意思了：图上的平滑性，在谱域里变成了对不同频率成分的加权惩罚。\n特征值 $\\lambda_i$ 越大，对应的频率越高，惩罚也越重。\n所以很多图上的正则化，其实都可以从谱域理解成：压制图信号中的高频成分，让相邻节点上的表示不要变化得太剧烈。\n比如我们有一个带噪声的图信号 $y$，希望得到一个更平滑的信号 $z$，就可以考虑：\n$$\r\\min_z \\Vert z - y \\Vert_2^2 + \\alpha z^TLz\r$$第一项表示 $z$ 不要离原始信号 $y$ 太远。\n第二项表示 $z$ 在图上要尽量平滑。\n其中 $\\alpha \u003e 0$ 控制平滑的强度。\n这个形式和普通正则化非常像：一边要求拟合原数据，一边要求解满足某种稳定性或者平滑性。\n如果换到图谱域里看，这个问题会更清楚。由于\n$$\rz^TLz = \\sum_i \\lambda_i \\hat{z}_i^2\r$$所以正则项对每个频率的惩罚并不一样。\n低频对应的 $\\lambda_i$ 小，惩罚也小；高频对应的 $\\lambda_i$ 大，惩罚也大。\n因此这个正则化会更愿意保留低频成分，同时压制高频成分。\n如果把解写出来，可以得到类似这样的谱域形式：\n$$\r\\hat{z}_i = \\frac{1}{1 + \\alpha \\lambda_i}\\hat{y}_i\r$$这就非常像一个低通滤波器。\n当 $\\lambda_i$ 很小时，$\\frac{1}{1 + \\alpha \\lambda_i}$ 接近 $1$，对应的低频成分基本保留。\n当 $\\lambda_i$ 很大时，$\\frac{1}{1 + \\alpha \\lambda_i}$ 会变小，对应的高频成分被压下去。\n所以从 GSP 的角度看，图上的平滑正则化不是一个抽象的惩罚项，而是在谱域里做了一次低通滤波。\n这也解释了为什么 GNN 里经常会出现“平滑”“低通”“过平滑”这些说法：很多消息传递操作，本质上都会让节点表示更接近邻居，也就是压制图上的高频变化。\n和数值计算的关系 虽然傅里叶变换最开始看起来像是信号处理里的东西，但谱域的思想在数值计算里也会反复出现。\n一个很典型的例子是微分。\n如果对 $f(t)$ 求导，那么在傅里叶域中有：\n$$\r\\widehat{f'}(\\omega) = i\\omega \\hat{f}(\\omega)\r$$也就是说，求导在时域里是一个局部变化率的问题，但到了频域里，就变成了乘上 $i\\omega$。\n二阶导数则对应：\n$$\r\\widehat{f''}(\\omega) = -\\omega^2 \\hat{f}(\\omega)\r$$这解释了为什么高频部分在微分运算下会被放大得更厉害。因为频率越高，乘上的 $\\omega$ 或 $\\omega^2$ 就越大。\n所以很多微分方程、偏微分方程、数值稳定性问题，都可以从谱域里得到更清楚的解释。\n再比如卷积。\n如果一个数值问题里有大量卷积计算，那么直接在原空间里算可能很慢。但如果先用 FFT 变到频域，把卷积变成乘法，再变回来，计算速度就可能快很多。\n这就是很多快速算法的基本思路。\n从傅里叶谱到矩阵谱 傅里叶分析里的谱，描述的是信号在不同频率上的成分。\n矩阵里的谱，通常指的是特征值或者奇异值这些东西。\n它们看起来不是一回事，但背后的想法有相似之处：都是想找到一组更自然的基本方向，然后把问题拆开来看。\n在傅里叶变换中，基本方向是不同频率的振动模式。\n在线性代数中，基本方向可能是特征向量或者奇异向量。\n如果一个问题在原来的表示下是耦合的，那么换到合适的谱域后，它可能会变成每个分量上的独立变化。\n这也是为什么谱会在很多地方出现：\n傅里叶变换中，谱描述不同频率的强度 图信号处理中，谱描述图上不同平滑程度的变化模式 矩阵分析中，谱描述线性变换在不同方向上的伸缩 正则化中，谱可以用来判断哪些方向不稳定 迭代法中，谱半径会影响迭代是否收敛 所以谱域本质上不是某个固定公式，而是一种观察方式。\n小结 傅里叶变换告诉我们，一个复杂信号可以被拆成许多简单的频率成分。\n时域里看到的是信号在每个时刻的取值，频域里看到的是信号由哪些频率组成。\n这种视角的价值在于：很多原空间中复杂的操作，在谱域里会变得更简单。滤波可以看成对不同频率加权，卷积可以变成乘法，微分可以变成乘上频率相关的系数。\n因此谱域不是一个脱离原问题的新世界，而是原问题的一种更适合分析和计算的表达方式。\n从傅里叶变换到矩阵谱，再到数值计算中的稳定性和正则化，背后反复出现的都是同一个想法：\n换到一组更自然的基下，把复杂的整体问题拆成许多可以单独理解的分量。\n","permalink":"https://solarain.net.cn/posts/%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2%E5%92%8C%E8%B0%B1%E5%9F%9F/","summary":"从傅里叶变换出发理解谱域：一个信号如何被拆成不同频率的基本成分，以及这种视角为什么会在滤波、FFT 和数值计算中反复出现。","title":"傅里叶变换和谱域"},{"content":"第一次接触强化学习的时候会有一种很明显的割裂感：监督学习看起来很正常，输入一个东西，输出一个标签；强化学习则是智能体和环境反复交互，奖励还经常不是立刻给的，因此很容易一开始就被各种符号和算法名字绕进去。这里还是先把最基础的框架串起来，目标是先想明白：强化学习到底在解决什么问题，价值函数和 Bellman 方程在干什么，以及 DQN、Policy Gradient、PPO 这些方法之间到底是什么关系。\n强化学习在做什么 如果说监督学习是在学：\n$$\rf_{\\theta}(x) \\approx y\r$$那么强化学习更像是在学一个决策规则：\n$$\r\\pi(a \\mid s)\r$$也就是在状态 $s$ 下应该采取什么动作 $a$。\n它和监督学习最大的不同不只是“没有现成标签”，而是：\n数据是智能体自己和环境交互得到的 当前动作会影响未来看到的数据分布 奖励往往是延迟到未来才体现的 因此强化学习的核心问题不是“这道题的正确答案是什么”，而是：\n如果我现在这样做，未来一连串结果加起来到底值不值。\n这也是为什么强化学习里“长期回报”会比“当前一步对不对”更重要。\n马尔可夫决策过程 强化学习最常见的形式化框架是 MDP（Markov Decision Process，马尔可夫决策过程）。\n一个 MDP 通常由下面几部分组成：\n$S$：状态空间 $A$：动作空间 $P(s' \\mid s, a)$：状态转移概率 $R(s, a)$ 或 $R(s, a, s')$：奖励函数 $\\gamma$：折扣因子 其中折扣因子 $\\gamma \\in [0, 1)$ 用来平衡“眼前收益”和“长期收益”。\n如果 $\\gamma$ 很小，那么模型更看重眼前； 如果 $\\gamma$ 很大，那么模型会更在意未来。\n为什么叫马尔可夫 所谓马尔可夫性，意思是：\n当前状态已经包含了做决策所需的全部信息，未来只和当前状态以及当前动作有关，而和更久远的历史无关。\n也就是说：\n$$\rP(s_{t+1} \\mid s_t, a_t, s_{t-1}, a_{t-1}, \\dots) = P(s_{t+1} \\mid s_t, a_t)\r$$现实问题里这个假设不一定总是严格成立，但它是强化学习建模中非常核心的起点。\n回报 强化学习不会只盯着一步奖励，而是更关心一条轨迹上的累计收益。\n从时间步 $t$ 开始的回报一般定义为：\n$$\rG_t = r_t + \\gamma r_{t+1} + \\gamma^2 r_{t+2} + \\cdots\r$$也可以写成：\n$$\rG_t = \\sum_{k=0}^{\\infty} \\gamma^k r_{t+k}\r$$这个量的含义很直接：越靠后的奖励，权重越小，但仍然会被考虑进去。\n所以强化学习本质上是在做：\n$$\r\\max_{\\pi} \\mathbb{E}[G_t]\r$$也就是寻找一条策略，使得长期期望回报尽可能大。\n策略、价值函数与 Q 函数 策略 策略（policy）就是智能体的行为规则。\n确定性策略：给定状态，动作唯一确定 随机策略：给定状态，动作按照某个概率分布采样 随机策略通常写成：\n$$\r\\pi(a \\mid s)\r$$状态价值函数 状态价值函数表示：在状态 $s$ 下，如果后面都按照策略 $\\pi$ 行动，那么期望回报是多少。\n$$\rV^{\\pi}(s) = \\mathbb{E}_{\\pi}[G_t \\mid s_t = s]\r$$它回答的是：这个状态本身值多少钱。\n动作价值函数 动作价值函数表示：在状态 $s$ 下先做动作 $a$，然后后续都按照策略 $\\pi$ 行动，期望回报是多少。\n$$\rQ^{\\pi}(s, a) = \\mathbb{E}_{\\pi}[G_t \\mid s_t = s, a_t = a]\r$$它回答的是：在这个状态下，某个动作值多少钱。\n很多强化学习算法本质上都在围绕 $V$ 或 $Q$ 做文章。\n价值函数为什么重要 因为如果我们真的知道了最优动作价值函数 $Q^*(s, a)$，那么决策就变得非常简单：\n$$\r\\pi^*(s) = \\arg\\max_a Q^*(s, a)\r$$也就是说，在每个状态下直接选价值最高的动作就行。\n所以很多算法其实都可以理解成在做两件事中的一件：\n想办法估计价值函数 根据价值函数改进策略 Bellman 方程 Bellman 方程是强化学习里最核心的递推关系之一。它的思想其实很朴素：\n一个状态的价值，等于当前一步奖励 + 下一状态的折扣价值。\nBellman 期望方程 对于给定策略 $\\pi$，状态价值函数满足：\n$$\rV^{\\pi}(s)=\r\\sum_a \\pi(a \\mid s)\\sum_{s', r} p(s', r \\mid s, a)\\left[r + \\gamma V^{\\pi}(s')\\right]\r$$动作价值函数满足：\n$$\rQ^{\\pi}(s, a)=\r\\sum_{s', r} p(s', r \\mid s, a)\\left[r + \\gamma \\sum_{a'}\\pi(a' \\mid s')Q^{\\pi}(s', a')\\right]\r$$这两个式子本质上都在表达“当前价值可以通过下一步递推出来”。\nBellman 最优方程 最优状态价值函数满足：\n$$\rV^*(s) = \\max_a \\sum_{s', r} p(s', r \\mid s, a)\\left[r + \\gamma V^*(s')\\right]\r$$最优动作价值函数满足：\n$$\rQ^*(s, a)=\r\\sum_{s', r} p(s', r \\mid s, a)\\left[r + \\gamma \\max_{a'}Q^*(s', a')\\right]\r$$这个式子非常关键，因为很多经典方法，例如 Q-learning，本质上就是在逼近这个关系。\n探索与利用 强化学习里一个永恒的问题是 exploration 和 exploitation 的平衡。\nexploration：去尝试没怎么试过的动作，看看会不会更好 exploitation：优先选择当前已知最优的动作 如果只利用，不探索，就可能永远停留在一个局部还行但不够好的策略上； 如果只探索，不利用，训练效率又会很差。\n最常见的例子是 $\\epsilon$-greedy：\n以 $1-\\epsilon$ 的概率选择当前价值最高的动作 以 $\\epsilon$ 的概率随机探索 这其实是一种非常朴素但有效的折中方案。\n几条经典方法主线 强化学习里算法看起来很多，但从脉络上大致可以分成下面几条线：\nDynamic Programming Monte Carlo Temporal Difference Value-based Policy-based Actor-Critic Dynamic Programming 动态规划方法默认环境模型已知，也就是转移概率和奖励函数都能拿到。\n例如：\nPolicy Evaluation：在策略固定时计算 $V^{\\pi}$ Policy Improvement：根据当前价值函数改进策略 Policy Iteration：交替做 evaluation 和 improvement Value Iteration：直接对最优价值函数做迭代 这条线更像理论出发点，因为现实里很多环境模型并不知道。\nMonte Carlo Monte Carlo 方法不依赖环境模型，它直接通过完整轨迹的实际回报来估计价值。\n优点：\n思想直接 无偏 缺点：\n必须等一整局结束才能更新 方差比较大 所以它适合当作理解强化学习的一个中间站，但在很多实际问题里不够高效。\nTemporal Difference TD 方法结合了动态规划和 Monte Carlo 的特点。\n它不必等一整条轨迹结束，而是一步一步更新：\n$$\rV(s_t) \\leftarrow V(s_t) + \\alpha \\left[r_t + \\gamma V(s_{t+1}) - V(s_t)\\right]\r$$其中：\n$$\r\\delta_t = r_t + \\gamma V(s_{t+1}) - V(s_t)\r$$被称为 TD error。\n这个量可以理解成“现实反馈和当前估计之间的差值”。很多后续算法都会围绕这个量展开。\nValue-based 方法 这条线的核心思路是：先学动作价值函数，再由价值函数导出策略。\nQ-learning Q-learning 是最经典的无模型强化学习算法之一，其更新公式是：\n$$\rQ(s_t, a_t) \\leftarrow Q(s_t, a_t) + \\alpha \\left[r_t + \\gamma \\max_{a'}Q(s_{t+1}, a') - Q(s_t, a_t)\\right]\r$$这里的目标值：\n$$\rr_t + \\gamma \\max_{a'}Q(s_{t+1}, a')\r$$就是在用 Bellman 最优方程做一步逼近。\nQ-learning 是 off-policy 的，意思是：\n采样动作时可以用带探索的行为策略 更新目标时指向的是贪心最优策略 因此它学习的是“最优策略的价值”，而不是“当前采样策略的价值”。\nSARSA SARSA 的更新公式是：\n$$\rQ(s_t, a_t) \\leftarrow Q(s_t, a_t) + \\alpha \\left[r_t + \\gamma Q(s_{t+1}, a_{t+1}) - Q(s_t, a_t)\\right]\r$$和 Q-learning 最大的区别在于，Q-learning 用的是下一状态的最优动作价值，而 SARSA 用的是当前行为策略真实选出来的 $a_{t+1}$。\n因此 SARSA 是 on-policy 的。\n如果说得更直白一点：\nQ-learning 学的是“理论上最优该怎么走” SARSA 学的是“我现在这套带探索的策略实际上会怎么走” DQN 传统 Q-learning 在状态空间小的时候可以直接维护一张 Q 表，但状态一旦很大，比如图像输入，就不可能再用表来存了。\n这时候可以用神经网络去近似：\n$$\rQ_{\\theta}(s, a)\r$$这就是 DQN 的基本想法。\nDQN 之所以重要，是因为它把“强化学习 + 深度神经网络”真正结合起来了。\n经典 DQN 为了稳定训练，引入了两个关键技巧：\nExperience Replay Target Network Experience Replay 把交互得到的 $(s, a, r, s')$ 存进经验池，再随机采样小批量数据训练。\n这样做的好处是：\n打破样本之间的强相关性 提高数据利用率 Target Network 训练目标中的下一状态价值不要每一步都用当前网络自己立刻算，而是用一个延迟更新的目标网络：\n$$\ry = r + \\gamma \\max_{a'}Q_{\\theta^-}(s', a')\r$$这样能让训练目标相对稳定一些，不至于“边追边跑”。\nPolicy-based 方法 这条线的核心思路是：不再绕一圈去学价值函数，而是直接优化策略本身。\nPolicy Gradient 如果策略参数化为 $\\pi_{\\theta}(a \\mid s)$，那么我们想最大化目标函数：\n$$\rJ(\\theta) = \\mathbb{E}_{\\pi_{\\theta}}[G_t]\r$$策略梯度定理告诉我们，可以沿着下面的方向更新参数：\n$$\r\\nabla_{\\theta} J(\\theta)=\r\\mathbb{E}_{\\pi_{\\theta}}\r\\left[\r\\sum_t \\nabla_{\\theta}\\log \\pi_{\\theta}(a_t \\mid s_t) G_t\r\\right]\r$$这个式子的直觉很重要：\n如果某个动作最后带来了高回报，就提高它出现的概率 如果某个动作最后效果很差，就降低它出现的概率 REINFORCE REINFORCE 可以看作最经典的 Policy Gradient 算法。\n它直接使用整条轨迹的回报作为权重更新策略，思路非常干净，但缺点也很明显：\n方差大 收敛慢 所以它更像是很多策略梯度方法的起点，而不是最终最好用的方法。\nActor-Critic Actor-Critic 可以理解成把 value-based 和 policy-based 的思路揉在一起。\nActor：负责输出策略 Critic：负责评估当前动作或状态到底好不好 比较典型的做法是让 critic 去估计价值函数，再用这个估计结果帮助 actor 更新。\n为什么这样做？\n因为纯策略梯度虽然直接，但方差通常太大；引入 critic 之后，相当于给策略更新提供了一个更稳定的参考。\nAdvantage 一个很常见的量是 advantage：\n$$\rA(s, a) = Q(s, a) - V(s)\r$$它表示“这个动作比当前状态的平均水平到底好多少”。\n如果 $A(s, a) \u003e 0$，说明这个动作比平均更好； 如果 $A(s, a) \u003c 0$，说明这个动作不太行。\n很多 actor-critic 方法都在估计这个量。\nA2C / A3C 这类方法本质上还是 actor-critic，只是工程形式和并行方式不同。它们的重点在于：\n用 critic 降低方差 用多步回报或并行采样提高训练效率 PPO PPO（Proximal Policy Optimization）是现在实践里非常常见的一类方法。\n它的核心动机不是“让策略改得越快越好”，而是：\n每次更新都别走太大步，避免策略突然崩掉。\nPPO 常见的 clipped objective 可以写成：\n$$\rL^{CLIP}(\\theta)=\r\\mathbb{E}\r\\left[\r\\min\r\\left(\rr_t(\\theta)\\hat A_t,\r\\text{clip}(r_t(\\theta), 1-\\epsilon, 1+\\epsilon)\\hat A_t\r\\right)\r\\right]\r$$其中：\n$$\rr_t(\\theta) = \\frac{\\pi_{\\theta}(a_t \\mid s_t)}{\\pi_{\\theta_{old}}(a_t \\mid s_t)}\r$$这个比值表示新旧策略在同一个动作上的概率变化。\nPPO 的核心思想就是：如果更新过猛，就把它裁掉，不让策略跳得太离谱。\n这也是它在工程上比较稳定、比较好用的原因之一。\n一个最小的 Q-learning 例子 下面给一个非常小的 Q-learning 伪代码例子，主要是为了把更新流程串起来：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import numpy as np n_state = 10 n_action = 2 Q = np.zeros((n_state, n_action)) alpha = 0.1 gamma = 0.99 eps = 0.1 for episode in range(1000): s = reset() done = False while not done: if np.random.rand() \u0026lt; eps: a = np.random.randint(n_action) else: a = np.argmax(Q[s]) s_next, r, done = step(a) td_target = r if not done: td_target += gamma * np.max(Q[s_next]) Q[s, a] += alpha * (td_target - Q[s, a]) s = s_next 这段代码里最关键的逻辑其实就两步：\n用 $\\epsilon$-greedy 去采样，解决探索问题 用 TD target 去更新 Q 值 如果把表格换成神经网络，再加上 replay buffer 和 target network，基本就走到 DQN 的思路上去了。\n强化学习里经常踩的坑 奖励设计 很多时候模型学不会，不一定是算法不够强，而是 reward 本身就有问题。\n例如：\n奖励过于稀疏，模型很难知道什么行为是好的 奖励设计有漏洞，模型学会“刷分”但没学会真正目标 奖励尺度变化太大，训练不稳定 这类问题在实际项目里非常常见。\n样本效率低 监督学习里一份数据可以反复训练很多轮，但强化学习的数据分布会随着策略改变而变化，而且很多样本还得自己在线采。\n因此强化学习经常面临一个大问题：太费交互。\n这也是为什么：\nreplay buffer model-based 方法 offline RL 这些方向都很重要。\n训练不稳定 强化学习常常同时在做几件事：\n数据分布在变 目标在变 策略在变 所以比普通监督学习更容易震荡、发散或者突然退化。\n这也是为什么 target network、advantage normalization、reward normalization、clipping 这些技巧会大量出现。\nCredit Assignment 有时一个动作的好坏要过很久才体现出来，这就会带来 credit assignment problem：\n最后拿到奖励之后，究竟应该把功劳或责任分配给前面哪一步动作。\n这个问题其实就是强化学习困难的核心之一。\nExploration 很难 在一些复杂环境里，随机乱试根本试不到真正有用的行为。\n比如需要很多步连续正确动作才能第一次拿到正奖励的任务，简单的 $\\epsilon$-greedy 往往不太够，这时就会需要更强的探索机制。\n强化学习和深度学习的关系 强化学习本身并不等于深度学习。\n强化学习：解决的是序列决策问题 深度学习：提供的是强大的函数拟合能力 当状态空间和动作空间很大时，就会把深度神经网络拿来近似策略函数、价值函数或者环境模型，于是就有了深度强化学习（Deep RL）。\n所以 DQN、DDPG、SAC、PPO 这些方法，本质上都是在强化学习框架里引入神经网络。\n小结 如果把一堆术语先放下，强化学习的主线其实不算复杂：\n用 MDP 去描述智能体和环境的交互 用回报去定义“长期来看好不好” 用价值函数或策略函数去表达决策规则 用 Bellman 方程或策略梯度去指导更新 在探索、稳定性和样本效率之间不断做权衡 后面看到各种算法时，也可以大致先问自己几个问题：\n它是在学 value，还是直接学 policy？ 它是 on-policy 还是 off-policy？ 它靠什么降低方差、提高稳定性？ 它的探索机制是什么？ 把这些东西先理顺，再去看具体 paper 或代码会轻松很多。\n","permalink":"https://solarain.net.cn/posts/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/","summary":"强化学习入门笔记，主要整理 MDP、价值函数、Bellman 方程、Q-learning、Policy Gradient、Actor-Critic 以及强化学习训练中常见的问题。","title":"强化学习"},{"content":"这里先尽量把最基础的东西串起来，目标不是把所有模型都讲完，而是先弄清楚：深度学习到底在学什么，模型是怎么训练起来的，以及平时写代码时到底在做什么。\n深度学习在做什么 如果只看表面，深度学习好像就是“堆很多层 + 喂很多数据 + 跑很久”，但它真正做的事情本质上还是在做一个函数拟合：\n$$\rf_{\\theta}(x) \\approx y\r$$其中：\n$x$ 是输入，例如图片、文本、语音、时序数据 $y$ 是我们想预测的目标，例如类别、数值、下一个 token $\\theta$ 是模型参数，也就是网络中的权重和偏置 和传统机器学习相比，深度学习最大的特点不只是模型更大，而是特征提取和任务求解被放在了同一个可训练系统里，这使得模型能够得到一些非线性的特征信息。\n例如图像分类中，传统方法可能需要手工提边缘、纹理、角点等特征，再送给分类器；深度学习则倾向于直接从原始输入出发，让网络自己去学“什么特征有用”。\n所以很多时候可以把深度学习理解成：\n用一个参数化很强的函数去表示任务 用数据去约束这个函数 用优化算法不断调整参数，让模型输出越来越接近目标 神经网络的基本单位 感知机 神经网络里最基础的计算单元可以写成：\n$$\rz = w^T x + b\r$$然后再接一个非线性激活函数：\n$$\ry = \\sigma(z)\r$$其中：\n$x$ 是输入向量 $w$ 是权重 $b$ 是偏置 $\\sigma$ 是激活函数 如果没有激活函数，那么无论堆多少层线性层，最后都等价于一层线性变换，这样表达能力会非常弱。因此非线性是神经网络能够拟合复杂关系的关键。\n常见激活函数 Sigmoid $$\r\\sigma(x) = \\frac{1}{1 + e^{-x}}\r$$输出范围是 $(0, 1)$，早期用得很多，常用于二分类输出概率。但它有一个比较明显的问题，就是输入绝对值过大时梯度会变得很小，容易出现梯度消失。\nTanh $$\r\\tanh(x) = \\frac{e^x - e^{-x}}{e^x + e^{-x}}\r$$输出范围是 $(-1, 1)$，相比 sigmoid 以 $0$ 为中心，优化时有时会更自然一些，但同样可能出现梯度消失。\nReLU $$\r\\text{ReLU}(x) = max(0, x)\r$$这个函数非常简单，但在实践里特别常用。原因也很直接：\n计算便宜 正区间梯度稳定 在深层网络中通常比 sigmoid 更容易训练 当然 ReLU 也有缺点，比如当神经元长期落在负半轴时，梯度恒为 $0$，可能出现“神经元死亡”。\nLeaky ReLU $$\r\\text{LeakyReLU}(x)=\r\\begin{cases}\rx, \u0026 x \u003e 0 \\\\\\\\\r\\alpha x, \u0026 x \\le 0\r\\end{cases}\r$$可以理解为给负半轴留一点斜率，缓解 ReLU 的“死掉”问题。\n神经网络为什么能工作 把一个神经元看成一次“线性变换 + 非线性变换”，多层网络就是不断重复这个过程。\n例如一个两层全连接网络可以写成：\n$$\rh = \\sigma(W_1x + b_1)\r$$$$\r\\hat y = W_2h + b_2\r$$这里 $h$ 可以理解为中间表示，也就是模型自动学出来的特征。层数更深时，本质上是不断把原始输入映射成更适合当前任务的表示。\n如果说得更直白一点，前面的层一般在学“局部和基础的模式”，后面的层则更偏向“更抽象、更任务相关的语义”。\n训练流程 深度学习最核心的部分其实就是训练流程。大部分代码归根结底都在围绕下面这几步转：\n前向传播 计算损失 反向传播 参数更新 前向传播 前向传播就是把输入一路送进网络，得到预测值。\n例如分类任务里：\n$$\r\\hat y = f_{\\theta}(x)\r$$这一步本身没有“学习”，只是根据当前参数计算输出。\n损失函数 模型输出之后，需要一个标准来衡量“这次预测得有多差”，这个标准就是损失函数。\n训练的目标通常可以写成：\n$$\r\\min_{\\theta} \\mathcal{L}(f_{\\theta}(x), y)\r$$也就是让预测值和真实值之间的差距尽可能小。\n均方误差 回归问题中常见：\n$$\r\\mathcal{L}_{MSE} = \\frac{1}{n}\\sum_{i=1}^{n}(\\hat y_{i} - y_{i})\r$$误差越大，惩罚越重。\n交叉熵 分类问题中更常见。二分类时常写成：\n$$\r\\mathcal{L}_{CE} = -[y\\log p + (1-y)\\log(1-p)]\r$$其中 $p$ 是模型预测为正类的概率。它的直觉意义是：如果模型对错误答案很自信，那么惩罚会很重。\n梯度下降 有了损失函数之后，接下来要做的就是调整参数，让损失下降。\n最基础的更新公式是：\n$$\r\\theta \\leftarrow \\theta - \\eta \\nabla_{\\theta}\\mathcal{L}\r$$其中：\n$\\eta$ 是学习率 $\\nabla_{\\theta}\\mathcal{L}$ 是损失对参数的梯度 梯度告诉我们“当前参数往哪个方向改，损失会下降得最快”。所以训练的过程可以理解为：不断沿着损失下降的方向走。\n反向传播 反向传播并不神秘，本质上就是链式法则在计算图上的系统应用。\n如果有复合函数：\n$$\rL = L(y), \\quad y = f(z), \\quad z = g(x)\r$$那么：\n$$\r\\frac{\\partial L}{\\partial x}=\r\\frac{\\partial L}{\\partial y}\r\\cdot\r\\frac{\\partial y}{\\partial z}\r\\cdot\r\\frac{\\partial z}{\\partial x}\r$$神经网络是很多层复合函数叠起来的，因此完全可以从输出层开始，一层层把梯度传回去。\nPyTorch 这类框架帮我们把这件事自动化了，所以平时看到的：\n1 loss.backward() 本质上就是在自动求整张计算图上的梯度。\n常见网络结构 全连接层 也叫 Linear 层或者 Dense 层，本质上就是：\n$$\ry = Wx + b\r$$这是最基础也最通用的一层，适合处理已经被整理成向量的特征。\n优点是表达直接，缺点也很明显：\n参数量容易很大 不利用输入的局部结构 因此在图像、语音等高维结构化数据上，一般不会只靠全连接层。\n卷积神经网络 卷积层的核心思想是用一个局部感受野在输入上滑动，提取局部模式。\n它适合图像任务的原因主要有两个：\n图像的局部区域通常有强相关性 同一种模式可能会在不同位置重复出现 卷积核共享参数，所以相比直接全连接，参数量会小很多，同时也更符合图像的空间结构。\n如果说全连接是在“看整张图后直接做判断”，那卷积更像是“先看局部纹理，再逐步组合成更高层语义”。\n池化层 池化层一般用于降采样，例如 max pooling。\n它的作用通常包括：\n降低特征图尺寸 减少计算量 提升一定程度的平移鲁棒性 不过现在很多网络也会直接用步长卷积替代池化。\n循环神经网络 RNN 适合处理序列数据，它会把上一步的隐藏状态传给下一步，因此天然适合建模“前后有关联”的数据。\n最基本的形式可以写成：\n$$\rh_t = \\phi(W_xx_t + W_hh_{t-1} + b)\r$$它的问题在于长序列训练时容易梯度消失或爆炸，所以后来又有了 LSTM、GRU 这种带门控的结构。\nLSTM / GRU 它们的核心想法不是“变得更复杂”，而是通过门控机制让模型自己决定：\n什么该记住 什么该遗忘 什么该输出 因此在时序任务、NLP 早期模型里用得很多。\nAttention 和 Transformer Attention 的核心思想是：当前 token 在编码时，不一定只依赖邻近位置，而是可以直接去关注序列中和自己相关的其他位置。\n自注意力的公式一般写成：\n$$\r\\text{Attention}(Q, K, V)=\\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V\r$$其中：\n$Q$ 是 query $K$ 是 key $V$ 是 value Transformer 可以理解为“以 attention 为核心搭起来的一整套结构”，它非常适合并行训练，这也是它后来在 NLP、CV、多模态里全面铺开的重要原因之一。\n训练时经常遇到的问题 欠拟合与过拟合 欠拟合 模型太简单，或者训练不够，导致训练集上都学不好。\n常见表现：\n训练误差高 验证误差也高 过拟合 模型把训练集记得太细了，泛化能力差。\n常见表现：\n训练误差很低 验证误差明显更高 这两者是训练时最常见的判断依据之一。\n数据集划分 通常会把数据分成：\n训练集：用来更新参数 验证集：用来调超参数和观察泛化 测试集：最后做一次相对客观的评估 测试集最好不要参与调参，不然最后的结果会越来越像“对测试集做了适配”。\n学习率 学习率太小，训练会很慢；学习率太大，又可能直接震荡甚至发散。\n它是最重要的超参数之一。很多时候模型训不起来，不一定是结构有问题，而是学习率没设对。\n一个很常见的经验是：\n先找一个能稳定下降的学习率 再考虑调度器逐步衰减 Batch、Epoch、Iteration Batch：一次送进模型的样本数 Iteration：一次参数更新 Epoch：整个训练集被完整看过一轮 如果训练集大小为 $N$，batch size 为 $B$，那么一个 epoch 大约有 $\\frac{N}{B}$ 次 iteration。\n初始化 如果初始化过大，可能导致激活值爆炸；如果初始化过小，又可能导致信号在深层网络里迅速衰减。\n因此才会有 Xavier 初始化、Kaiming 初始化这类更适合不同激活函数的初始化方式。\n归一化 输入数据做归一化通常很重要，例如图像常见的做法是减均值、除标准差。\n网络内部常见的归一化层有：\nBatchNorm LayerNorm 它们的目的不完全一样，但都能在不同程度上帮助训练更稳定。\n正则化 为了缓解过拟合，常见手段有：\nL2 正则化 Dropout 数据增强 提前停止（early stopping） 其中 dropout 可以理解为在训练时随机“屏蔽掉”一部分神经元，避免网络过度依赖某些局部路径。\n优化器 最基本的是 SGD：\n$$\r\\theta \\leftarrow \\theta - \\eta \\nabla_{\\theta}\\mathcal{L}\r$$但实际训练中更常见的是带动量的 SGD、Adam、AdamW 等。\nSGD 优点是简单直接，有时泛化也很好；缺点是对学习率比较敏感，调参不一定轻松。\nAdam Adam 会同时利用梯度的一阶矩和二阶矩信息，自适应地调整不同参数的学习率，因此通常更容易训起来。\n很多入门任务直接上 Adam 往往就能得到一个还不错的结果。\n不过“更容易优化”不一定等于“最终泛化一定更好”，具体还是得看任务。\n一个最小的 PyTorch 训练例子 下面给一个非常小的二分类例子，用纯 torch 生成二维数据并训练一个简单的 MLP。这个例子主要是为了把训练流程串起来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import torch from torch import nn from torch.utils.data import DataLoader, TensorDataset torch.manual_seed(42) # 生成二维点，判断它是否落在单位圆外 n = 4000 x = torch.randn(n, 2) y = ((x[:, 0] ** 2 + x[:, 1] ** 2) \u0026gt; 1.0).float().unsqueeze(1) train_x, valid_x = x[:3200], x[3200:] train_y, valid_y = y[:3200], y[3200:] train_loader = DataLoader( TensorDataset(train_x, train_y), batch_size=128, shuffle=True ) valid_loader = DataLoader( TensorDataset(valid_x, valid_y), batch_size=256, shuffle=False ) model = nn.Sequential( nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 1) ) criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(20): model.train() total_loss = 0.0 for batch_x, batch_y in train_loader: logits = model(batch_x) loss = criterion(logits, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * batch_x.size(0) model.eval() correct = 0 total = 0 with torch.no_grad(): for batch_x, batch_y in valid_loader: logits = model(batch_x) pred = (torch.sigmoid(logits) \u0026gt; 0.5).float() correct += (pred == batch_y).sum().item() total += batch_y.numel() train_loss = total_loss / len(train_loader.dataset) valid_acc = correct / total print(f\u0026#34;epoch={epoch + 1}, train_loss={train_loss:.4f}, valid_acc={valid_acc:.4f}\u0026#34;) 这段代码里最关键的几步分别是：\n定义模型 定义损失函数 定义优化器 前向传播得到 logits 用 loss.backward() 反向传播 用 optimizer.step() 更新参数 如果是分类任务，需要特别注意最后一层和损失函数是否匹配。\n例如：\n多分类常见组合是 Linear + CrossEntropyLoss 二分类常见组合是 Linear + BCEWithLogitsLoss 像 BCEWithLogitsLoss 这种接口已经把 sigmoid 合进去了，所以训练时不要手动再加一层 sigmoid 再送进去，不然数值上反而不太好。\n平时写模型代码时到底在关注什么 如果只是为了把程序跑起来，深度学习代码很容易写成“面向 API 编程”，但真正训练模型的时候，通常要一直盯着下面这些东西：\nloss 有没有正常下降 如果 loss 完全不动，优先检查：\n学习率是否合适 标签是否错位 输出层和损失函数是否匹配 数据预处理是否有问题 训练集和验证集是否分离 如果训练效果很好，但验证效果很差，多半是过拟合，或者数据分布不一致。\n梯度是否异常 有时 loss 是 nan，或者训练突然炸掉，可能是：\n学习率过大 数值溢出 梯度爆炸 输入尺度不稳定 这时可以考虑梯度裁剪、归一化输入、减小学习率等方法。\n数据往往比模型更重要 很多时候并不是模型结构不够“高级”，而是：\n数据量不够 标签质量不高 特征分布有偏 训练目标定义不清楚 这个问题在实际项目里比“模型选哪一篇 paper 的结构”更常见。\n","permalink":"https://solarain.net.cn/posts/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/","summary":"深度学习入门笔记，主要整理神经网络、损失函数、反向传播、常见网络结构以及 PyTorch 的基本训练流程。","title":"深度学习"},{"content":"事先叠甲：本文中的所有公式都较为简单，且可能不够严谨。本文的作用主要是在几何上去理解矩阵论中的一些重要概念，并给出一些常用的代数上的公式。\n范数 (Norm) 一种具有 “长度” 概念的抽象数学概念，本质是函数，用于量化的抽象概念。范数最容易被定义的数学概念是向量的模长，也就是向量的长度，因此可以说抽象的范数概念事实上就是向量模长的延伸，或者说向量的长度是一种特殊的常见范数。\n半范数(seminorm) $p$，满足：\n$p(\\bm{v}) \\geq 0$ $p(a\\bm{v}) = \\vert a \\vert p(\\bm{v})$ $p(\\bm{u} + \\bm{v}) \\leq p(\\bm{u}) + p(\\bm{v})$ 其中，$\\bm{v}$ 可以是任意的代数概念，常见的可以是实数（对应绝对值），向量（对应模长）等等。\n半并非 1/2 的含义，而是接近（准）的含义，半范数对比范数缺少了一些核心的要求，性质更弱，但保留了核心框架；类似的概念还有群和半群等等。因此我认为半x存在的含义是因为场景允许非平凡的零空间，且零空间具有明确的含义，可以为特定的场景定制所需的零空间。\n我的理解上，这个“半”的可能用“准”来描述更贴切。\n范数在半范数的基础上，由于零空间的特殊性，需要额外满足：\n$p(\\bm{v}) = 0$，当且仅当 $\\bm{v}$ 是零向量。 符号上，通常用 $\\Vert \\bm{x} \\Vert$ 来表示范数。\n矩阵范数 在满足上述范数条件的基础上，对常见的算子范数，会因为矩阵的性质而自然地额外满足：\n$\\Vert AB \\Vert \\leq \\Vert A \\Vert \\Vert B \\Vert$ $\\Vert A \\Vert = \\Vert A^* \\Vert$，$A^*$ 为共轭转置，实数范围内等价于$A^T$ 矩阵范数和行列式的区别 行列式的几何含义是体积的缩放因子（可正可负可零），范数的含义考虑最极端的缩放（大于等于零）。\n常见的矩阵范数 矩阵的范数会因为具体的应用场景不同而有不同的计算方式，主要原因还是说对这玩意的长度认识可以有不同的角度去求出一个满足范数定义的函数。但虽然种类很多，常用的却只有这么几个。\n向量范数诱导的矩阵范数 “诱导”（induced），指从一个数学结构中自然的衍生出另一个结构。\n“诱导范数” （induced norm）代表了矩阵对向量的放大能力，可以说此时的矩阵代表了一种线性变化，例如，有向量 $x$ 和矩阵 $A$，$x$ 的长度为 $\\Vert x \\Vert$，那么 $Ax$ 的长度为$\\Vert Ax \\Vert$。\n因此会如有如下的等式：\n$\\Vert A \\Vert_{p} = max\\lbrace \\frac{\\Vert Ax\\Vert_{p}}{\\Vert x \\Vert_{p}} \\rbrace, x \\neq 0$\np-范数诱导的矩阵范数 $p = 1$ 时，为最大列和。\n$p = \\infty$ 时，为最大行和。\n$p = 2$ （欧几里得范数）时，又称为谱范数，$\\Vert A \\Vert_{2} = \\sqrt{\\lambda_{max}(A^{*}A)}$\n谱范数应该是一个最为常用的范数，代表的含义就是向量模长的放大能力，很直观地符合向量范数的常规定义。\n矩阵元范数 有别于诱导范数，矩阵元范数是将矩阵看成了一个 $m×n$ 的向量，用类似的向量范数。\n$\\Vert A \\Vert_{l}=(\\sum_{i=1}^{n}\\sum_{j=1}^{m}\\vert a_{ij}\\vert^{l})^{\\frac{1}{l}}$\n弗罗贝尼乌斯范数 $l = 2$ 的矩阵元范数，很直观地理解就是每个元素的平方和开根，当 $n$ 或者 $m$ 等于 $1$ 的时候就退化为了向量模长。\n这个范数有很多好用的性质，但这里不展现讲，详细请看“希尔伯特空间”。\n极大值范数 $l = \\infty$ 的矩阵元范数。\n谱半径 矩阵的谱半径是矩阵任何诱导范数的下界，但是不属于任何一种范数。\n矩阵或有界线性算子的谱半径是其特征值绝对值中的最大值，记作 $\\rho(·)$ 。\n$$\r\\rho(A) = max\\lbrace \\vert\\lambda_{1}\\vert, \\vert \\lambda_{2}\\vert, ..., \\vert\\lambda_{n}\\vert \\rbrace\r$$条件数 该数量在数值计算中的容易程度的衡量，也就是该问题的适定性。一个低条件数的问题称为良置的，而高条件数的问题称为病态（或者说非良置）的。\n条件数刻画的是线性变换后被拉伸的“扁率”。沿短轴方向的分量会被极度压缩，这意味着，输入中的微小误差，经过逆变换后，会被成倍放大，从而导致解对扰动高度敏感。\n举个栗子：\n上图是原始的网格，以及经过 $A$ 变换后的结果，原始网格是低条件数的也就是 $1$，正交方向比值都为 $1$，变换后的网格是高条件数的（我随机把原网格压扁的）。现在对这个网格参数化完后输入一组向量 $uv$，在原网格上很容易找到对应的点，但是在变换后的网格下，你很难说凭借 $uv$ 并且伴有一些误差，还能找到那个原始网格上真正想要的那个点。\n$\\kappa(A) = \\Vert A^{-1} \\Vert · \\Vert A \\Vert$\n若$\\Vert·\\Vert$ 为 $l_2$ 矩阵范数，则：\n$\\kappa(A) = \\frac{\\sigma_{max}(A)}{\\sigma_{min}(A)}$，其中 $\\sigma_{max}(A)$， $\\sigma_{min}(A)$ 分别为最大最小奇异值\n若 $A$ 是正规矩阵，则： $\\kappa(A) = \\vert\\frac{\\lambda_{max}(A)}{\\lambda_{min}(A)}\\vert = \\rho(A) \\rho(A^{-1}) $\n正则化 为了改善条件数于是就有了正则化。\n在数值计算里，一个很常见的问题是去解\n$$\rAx \\approx b\r$$如果 $A$ 是病态矩阵，那么 $b$ 中很小的扰动也可能让最后的解变化很大。特别是某些奇异值很小的时候，求逆或者做最小二乘时就会把这些方向上的误差一起放大。\n因此正则化做的事情其实很直接：不再只追求“拟合得最好”，而是同时限制解本身不要太极端。\nTikhonov正则化 对最小二乘问题\n$$\r\\min_{x} \\Vert Ax - b \\Vert_2^2\r$$一种最常见的正则化方式就是额外加一个惩罚项：\n$$\r\\min_{x} \\Vert Ax - b \\Vert_2^2 + \\lambda \\Vert x \\Vert_2^2\r$$其中 $\\lambda \u003e 0$ 是正则化参数。\n这个式子的含义比较自然：\n第一项要求结果尽量拟合原数据 第二项要求解不要过大 对其求导并令导数为 $0$，可以得到：\n$$\r(A^TA + \\lambda I)x = A^Tb\r$$于是：\n$$\rx = (A^TA + \\lambda I)^{-1}A^Tb\r$$相比普通最小二乘的正规方程\n$$\rA^TAx = A^Tb\r$$这里多出来了一个 $\\lambda I$，而这个东西就是稳定性的来源。\n几何理解 普通最小二乘会尽可能逼近 $b$，哪怕某些方向非常不稳定，也可能硬着头皮往这些方向上“追”。\n但如果某个方向上的奇异值很小，那么沿这个方向稍微动一点，解向量的长度就可能暴涨。这样得到的解虽然残差可能更小，但数值上非常脆弱。\n加上 $\\lambda \\Vert x \\Vert_2^2$ 后，相当于在说：\n可以拟合，但是不要为了拟合一点点误差，把解拉得特别长。\n所以正则化解通常会更平滑，也更抗噪声。\n为什么会改善条件数 设 $A$ 的奇异值为 $\\sigma_1,\\sigma_2,\\cdots,\\sigma_r$，那么 $A^TA$ 的特征值为\n$$\r\\sigma_1^2,\\sigma_2^2,\\cdots,\\sigma_r^2\r$$而 $A^TA + \\lambda I$ 的特征值就变成了\n$$\r\\sigma_1^2 + \\lambda,\\sigma_2^2 + \\lambda,\\cdots,\\sigma_r^2 + \\lambda\r$$也就是说，原本接近 $0$ 的特征值被整体抬高了。因此矩阵不会那么接近奇异，逆也会稳定很多。\n若 $A$ 满列秩，在 $l_2$ 范数下有：\n$$\r\\kappa(A^TA + \\lambda I) = \\frac{\\sigma_{max}^2 + \\lambda}{\\sigma_{min}^2 + \\lambda}\r$$当 $\\lambda \u003e 0$ 时，分母不再那么小，所以条件数通常会比 $\\kappa(A^TA)$ 更好看。\n从SVD的角度看 设\n$$\rA = U\\Sigma V^T\r$$则 Tikhonov 正则化的解可以写成\n$$\rx = \\sum_{i=1}^{r}\\frac{\\sigma_i}{\\sigma_i^2 + \\lambda}(u_i^Tb)v_i\r$$普通最小二乘里，小奇异值方向容易出现很大的放大效应；而这里的系数变成了\n$$\r\\frac{\\sigma_i}{\\sigma_i^2 + \\lambda}\r$$当 $\\sigma_i$ 很小时，这一项也会很小，因此这些不稳定方向上的贡献被压下去了。\n从谱变换的角度看 上面的形式其实已经很接近谱变换的视角了。\n普通最小二乘在每个奇异值方向上的放大系数大约是\n$$\r\\frac{1}{\\sigma_i}\r$$这也是病态问题不稳定的来源：当 $\\sigma_i$ 很小时，这个方向上的一点点噪声都会被放得很大。\n而 Tikhonov 正则化把这个系数换成了\n$$\r\\frac{\\sigma_i}{\\sigma_i^2 + \\lambda}\r$$也就是说，它没有改变奇异向量这些方向本身，而是改变了每个方向上的权重。大的奇异值方向基本保留，小的奇异值方向会被压下去。\n从这个意义上看，正则化可以理解成一种谱滤波：\n普通最小二乘：完全相信所有奇异值方向 Tikhonov 正则化：连续地压低小奇异值方向 截断 SVD：直接丢掉小奇异值方向 所以 Tikhonov 和截断 SVD 的区别，不是目标完全不同，而是一个更像“软压制”，一个更像“硬截断”。\n参数 $\\lambda$ 的作用 $\\lambda$ 控制的是“拟合原数据”和“让解更稳定”之间的平衡。\n$\\lambda$ 太小，效果接近普通最小二乘，抗噪声能力有限 $\\lambda$ 太大，解会被压得太厉害，结果可能偏得比较多 两个极端情况也很直观：\n当 $\\lambda \\to 0$ 时，趋近普通最小二乘解 当 $\\lambda \\to +\\infty$ 时，解会逐渐趋近于 $0$ 截断SVD 除了 Tikhonov 正则化以外，还有一种也很常见的思路，就是直接把过小的奇异值对应的方向丢掉。\n若\n$$\rA = U\\Sigma V^T\r$$只保留前 $k$ 个较大的奇异值，则得到截断后的近似矩阵\n$$\rA_k = U_k \\Sigma_k V_k^T\r$$对应的近似解为\n$$\rx_k = \\sum_{i=1}^{k}\\frac{1}{\\sigma_i}(u_i^Tb)v_i\r$$它和 Tikhonov 的想法其实差不多，都是少去相信那些特别小的奇异值方向，因为这些方向往往最容易把噪声带进来。\n小结 正则化本质上不是让原问题“变简单”了，而是主动放弃一部分过度拟合，换一个更稳定的解。\n所以在病态问题里，正则化求出来的解不一定是残差最小的那个，但通常是数值上更可信的那个。\n","permalink":"https://solarain.net.cn/posts/%E6%95%B0%E5%80%BC%E8%AE%A1%E7%AE%97-%E7%9F%A9%E9%98%B5%E8%AE%BA/","summary":"数值计算中经常会用到线代来处理一些大规模的数据处理，其中的一些线性方程组、矩阵的概念以及一些处理手段需要掌握","title":"数值计算-矩阵论"},{"content":"数学定义 吸收马尔科夫链 基本概念 设马尔可夫链 $\\{X_n, n \\geq 0\\}$ 的状态空间为 $S$，如果存在状态 $i \\in S$ 满足：\n$P_{ii} = 1$（一旦进入就永远停留） $P_{ij} = 0$ 对于所有 $j \\neq i$（无法转移到其他状态） 则状态 $i$ 称为吸收状态, 其余状态则为瞬时状态。\n标准形式 吸收马尔可夫链的转移概率矩阵可以写成如下标准形式：\n$$\rP = \\begin{bmatrix}\rQ \u0026 R \\\\\\\\\r0 \u0026 I\r\\end{bmatrix}\r$$其中：\n$Q$: 瞬态之间的转移概率子矩阵 ($t \\times t$) $R$: 瞬态到吸收态的转移概率子矩阵 ($t \\times r$) $I$: 吸收态之间的转移概率子矩阵 ($r \\times r$)，为单位矩阵 $0$: 吸收态到瞬态的转移概率子矩阵 ($r \\times t$)，为零矩阵 基本性质 $P^1$ 为转移 $1$ 次的转移概率 $P^2$ 为转移 $2$ 次的转移概率 …… $P^\\infty$ 为转移 $\\infty$ 次的转移概率，可以表示为：\n$$\rP^\\infty = \\begin{bmatrix}\r0 \u0026 (I-Q)^{-1}R \\\\\\\\\r0 \u0026 I\r\\end{bmatrix}\r$$基本矩阵 $$\rN:=\\sum_{k=0}^{\\infty}Q^{k}=(I_{t}-Q)^{-1}\r$$基本矩阵 $N$ 定义为：\n$$\rN = (I - Q)^{-1}\r$$基本矩阵 $N = [n_{ij}]$ 的元素 $n_{ij}$ 表示从瞬态 $i$ 出发，在吸收前访问瞬态 $j$ 的期望次数。\n吸收概率 从瞬态 $i$ 出发被吸收态 $j$ 吸收的概率矩阵为： $$B = NR$$其中 $B = [b_{ij}]$，$b_{ij}$ 表示从瞬态 $i$ 出发最终被吸收态 $j$ 吸收的概率。\nMVC 均值坐标 $$\rw_i = \\frac{tan(\\frac{\\alpha_i}{2})+ tan(\\frac{\\beta_i}{2})}{\\Vert v_i-v_0 \\Vert}\r$$广义重心坐标的一种，在2d平面内具备几何上平衡的性质。\n拉普拉斯微分坐标 (Laplace Differential Coordinates) 直觉上微分坐标做到了用相对坐标将网格表述出来，数学定义为：\n$$\\delta_i = v_i - \\sum_{j \\in N_i} w_{ij} v_j$$其中权重可以考虑简单的平均、MVC均值坐标、余切权重等，都在一定的范围内可以达到理想的效果，从理论上余切权重应该是最合适的，但是由于可能会存在负权重导致后续的计算困难。\n最终得到的坐标形式是具有大小和方向的，可以说拉普拉斯微分坐标记录了网格的所有局部细节，是网格的压缩形式，可以通过这些细节去还原、编辑网格。\nMorphing 应用 本文的 morph 算法的核心思想是网格中的每一个节点的位置通过无穷次的迭代转移之后，最后达到各个handle的概率，并计算加权和作为新的坐标。因此点与点之间的权重的计算尤其重要。本方法与 Laplacian Surface Editing 文章中的实现方法基本接近，区别主要在于: a. 优化了权重计算的方式，b. 省略了方程的右端项以及特征旋转的特性。\n权重的计算按照均值坐标的形式计算，并且通过加权的方式保证矩阵中每行的和小于等于1\n$P$ 构建规则 为了确保整体算法的可行性，矩阵的构建需要满足以下几点要求：\n对于 handle 点，定义为吸收状态，其余点为瞬时状态 对于 edge 点，只能转移到相邻的 edge 点，确定狄利克雷边界条件 其余点可以向拓扑邻接的点进行转移 转移的概率为邻接网格MVC坐标权重加权 所有约束都能够在 $P$ 上被定义 行为 状态包含具有哪些 handle 点，哪些是 edge 点，哪些点具备额外的约束，节点间的邻接关系以及每个节点的空间位置 每次在确定移动前的状态前需要先构建出 $P^{\\infty}$，并存储矩阵 $B$ 每次移动 hanlde 点，可以根据 $B$ 计算出所有自由点的移动矢量 ","permalink":"https://solarain.net.cn/posts/%E5%90%B8%E6%94%B6%E9%A9%AC%E5%B0%94%E7%A7%91%E5%A4%AB%E9%93%BE/","summary":"介绍吸收马尔科夫链链的数学概念，并在几何计算中运用其思想","title":"吸收马尔科夫链 (Absorbing Markov Chain)"},{"content":"异面直线公垂线 有两个线段 $L_1$, $L_2$ 的法向分别为 $\\bm{u}$，$\\bm{v}$, 则有直线方程。\n$ L_1: \\bm{r} = \\bm{a} + t\\bm{u} $\n$ L_2: \\bm{r} = \\bm{b} + s\\bm{v} $\n则存在两个点 $P(t) = \\bm{a} + t\\bm{u}, Q(s) = \\bm{b} + s\\bm{v}$，有 $ \\overrightarrow{PQ} · \\bm{u} = 0，\\overrightarrow{PQ} · \\bm{v} = 0 $。\n令 $ \\bm{w} = \\bm{b} - \\bm{a} $, 可以构建线性方程组：\n$$\r\\begin{cases}\rt - s\\bm{u}\\bm{v} = \\bm{w} · \\bm{u} \\\\\\\\\rt\\bm{u}\\bm{v} - s = \\bm{w} · \\bm{v}\r\\end{cases}\r$$由于 $\\bm{u}$, $\\bm{v}$ 不平行，显然有系数行列式不为零，存在唯一解及有且仅有一个公垂线：\n$$\r\\begin{vmatrix}\r1 \u0026 -\\bm{u}\\bm{v} \\\\\\\\\r\\bm{u}\\bm{v} \u0026 -1\r\\end{vmatrix} \\not={0} $$公垂线距离为直线最近距离。\n","permalink":"https://solarain.net.cn/posts/%E5%87%A0%E4%BD%95%E6%A6%82%E5%BF%B5/","summary":"记录计算几何中遇到的一些常见的几何概念","title":"几何概念"},{"content":"介绍一些我在使用 Linux 过程中使用的软件，基本仅涉及安装、配置部分，不涉及软件的原理。\ndwm 基于 X11 的一个 WM ，简单好用。\n官方仓库: https://git.suckless.org/dwm/\n我修改后的: https://github.com/RyanTsi/dwm/\n主要基于自己习惯的快捷键进行了调整，安装了一些补丁。之后也决定自己修改部分代码上去。\n可以通过 xrandr 来进行多显示器的配置。\n可以安装 xdg-user-dirs 来生成 Desktop、Music、Pictures 等文件夹，运行 xdg-user-dirs-update。\nalacritty 一个利用 GPU 进行渲染 Terminal 。当时记得用 suckless 的 st 有一些显示问题就换称了 alacritty。\n官方wiki: https://alacritty.org/config-alacritty.html\nranger 文件管理器。\nfcitx5 输入法。\n安装:\nfcitx5 本体\nfcirx5-chinese-addon 包含中文输入法\nfcitx5-pinyin-zhwiki 中文词库\nfcitx5-configtool 用于设置的 GUI\n配置：\n在 /etc/environment 写入如下变量使得 fcitx 在不同界面上能正常运行\n1 2 3 4 5 6 GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx SDL_IM_MODULE=fcitx INPUT_METHOD=fcitx GLFW_IM_MODULE=ibus 随桌面环境自动启动：在 .xinitrc 中添加一行 fcitx5 -d \u0026amp;。\n在某些软件可能会遇到输入法不工作的情况，这大概率是跟那个软件的界面有关。\nzsh 一个 shell 解释器，Tab 相对于 bash 好用太多，同时插件丰富。\n安装 oh-my-zsh 以及 autosuggestions、syntax-highlighting 这两个插件。\n1 2 3 4 5 sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting 在 ~/.zshrc 的 Plugins 处添加上这两个插件。\nALSA 全称 Advanced Linux Sound Architecture。需要安装一些程序来控制音频设置。\nalsa-utils 包含alsamixer（GUI）、amixer（Shell）。\n设置默认声卡：\n在 ~/.asoundrc 中写入\n1 2 defaults.pcm.card 1 defaults.ctl.card 1 可以通过amixer编写一些脚本同时绑定快捷键来方面地控制音量。\n1 2 3 amixer sset Master 5%- # 音量减少 5% amixer sset Master 5%+ # 音量增加 5% amixer sset Master toggle # 静音解除/解除静音 rofi 可以用来当作应用启动器、窗口选择器等。\npicom X11 合成器。\n随桌面环境自动启动：在 .xinitrc 中添加一行 picom -b。\n字体 ttf-hack-nerd\nttf-dejavu\nnoto-fonts-cjk\nnoto-fonts-emoji\nttf-wps-fonts\nwps-office-mui-zh-cn\nwyq-microhei\ndunst X11 的 notification daemon。\n随桌面环境自动启动：在 .xinitrc 中添加一行 dunst。\n可以用 notify-send (String) 发送一条通知。\nfeh、imv 图片查看工具。\nfeh 还能对图像进行简单的编辑，一般用来当作显示桌面壁纸的工具。\nmpv 视频播放器。\n可以在参数上添加 --gpu-api=vulkan 来选择用图形 API，--hwdec=nvdec 来选择硬件解码器。\n配置文件通常在 ~/.config/mpv/mpv.config。\nbtop、nvtop btop 资源管理窗口。\nnvtop GPU 资源管理窗口。\ncpupower 一组辅助 CPU 频率调节的工具，配置文件在 /etc/default/cpupower 下，可以用 systemd 启动\nClash 内容参考 https://a76yyyy.github.io/clash/\n创建 daemon\n用 systemd 创建一个 daemon，在 /etc/systemd/system/clash.service 中写入如下内容\n1 2 3 4 5 6 7 8 9 10 11 [Unit] Description=Clash daemon, A rule-based proxy in Go. After=network.target [Service] Type=simple Restart=always ExecStart=/usr/bin/clash -d /etc/clash # /usr/bin/clash 为绝对路径，请根据你实际情况修改 [Install] WantedBy=multi-user.target 网络代理\n~/.zshrc 或 ~/.bashrc 写入环境变量\n1 2 3 export http_proxy=127.0.0.1:7890 export https_proxy=127.0.0.1:7890 export socks_proxy=127.0.0.1:7891 单独配置 ssh，在 ~/.ssh/config 中写入\n1 2 3 Host github.com HostName github.com ProxyCommand socat - PROXY:127.0.0.1:%h:%p,proxyport=7890 配置 web 管理\nweb 管理面板代码: https://go.runba.cyou/ssr-download/clash-dashboard.tar.gz\n在 config.yaml 中添加如下代码\n1 2 3 external-controller: 127.0.0.1:9090 # ip + port external-ui: /etc/clash/clash-dashboard # clash-dashboard的路径； secret: \u0026#39;PaaRwW3B1Kj9\u0026#39; # PaaRwW3B1Kj9 是登录web管理界面的密码。 一些常用软件 1 2 3 4 5 6 obs-studio wps-office virtualbox yesplaymusic google-chrome visual-studio-code dotfiles https://github.com/RyanTsi/dotfiles.git\n","permalink":"https://solarain.net.cn/posts/linux-%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%8F%8A%E5%85%B6%E9%85%8D%E7%BD%AE/","summary":"介绍一些我在使用 linux 过程中使用的软件，做一个资料的整合，避免日后需要的时候重复去网上搜索。","title":"Linux 常用软件安装及其配置"},{"content":"算法原理： 对于序列上的查询问题, 如果 $[l, r]$ 的答案可以$O(1)$扩展到$[l - 1, r], [l + 1, r], [l, r - 1], [l, r + 1]$, 那么对于所有询问, 离线处理后可以在$n\\sqrt{q}$的时间内找到所有问题的答案。\n莫队擅长处理的问题是这么一类问题：区间 $[l, r]$ 中有多少个二元组 $(i, j)$ 满足条件，常规的做法是将区间子区间问题通过前缀和差分的形式转会为区间二元组的问题，这个技巧会经常用到。\n具体实现： 对于区间$[l,r]$, 以$l$所在块的编号为第一关键字升序, $r$为第二关键字排序升序。 优化: 若$l$所在块编号为奇数, 则按$r$为第二关键字升序, 反正则按降序。\n具体实现见例题, 主要是编写$O(1)$扩展部分的代码\n例题: Powerful artay 题目大意:\n对于给定数列$a$, 每次询问一个区间$[l, r]$, 区间的值是每个数字出现的次数的平方乘这个数本身的和,即$\\sum k_s^2 \\times s$。\n这个问题可以被转化为一个区间二元组的问题，即：\n$$\r\\sum_{l \\leq i, j \\leq r} a_i[a_i = a_j]\r$$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 int main () { ios::sync_with_stdio(0), cin.tie(0); int n, t; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; t; vector\u0026lt;int\u0026gt; a(n); int B = sqrt(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } struct query { int l, r, id; }; vector\u0026lt;query\u0026gt; q(t); for(int i = 0; i \u0026lt; t; i ++) { auto \u0026amp;[l, r, id] = q[i]; cin \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; l --, r --; id = i; } sort(q.begin(), q.end(), [\u0026amp;] (query a, query b) { if(a.l / B != b.l / B) return a.l \u0026lt; b.l; return (a.l / B) \u0026amp; 1 ? a.r \u0026lt; b.r : a.r \u0026gt; b.r; }); ll res = 0; vector\u0026lt;ll\u0026gt; ans(t); vector\u0026lt;int\u0026gt; cnt(MAXN); for(int i = 0, lt = 0, rt = -1; i \u0026lt; t; i ++) { auto \u0026amp;[l, r, id] = q[i]; while (rt \u0026lt; r) { int x = a[++ rt]; ll fi = 1ll * cnt[x] * cnt[x] * x; cnt[x] ++; ll se = 1ll * cnt[x] * cnt[x] * x; res = res - fi + se; } while(rt \u0026gt; r) { int x = a[rt --]; ll fi = 1ll * cnt[x] * cnt[x] * x; cnt[x] --; ll se = 1ll * cnt[x] * cnt[x] * x; res = res - fi + se; } while(lt \u0026lt; l) { int x = a[lt ++]; ll fi = 1ll * cnt[x] * cnt[x] * x; cnt[x] --; ll se = 1ll * cnt[x] * cnt[x] * x; res = res - fi + se; } while(lt \u0026gt; l) { int x = a[-- lt]; ll fi = 1ll * cnt[x] * cnt[x] * x; cnt[x] ++; ll se = 1ll * cnt[x] * cnt[x] * x; res = res - fi + se; } ans[id] = res; } for(ll i : ans) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } XOR and Favorite Number 题目大意\n$q$ 次询问，每次询问 $[l, r]$ 求有其中有多少个子区间满足区间异或和为 $k$ 。\n由于是子区间问题，所以应该需要将此题中的子区间问题化简成为区间二元组的问题，可以通过做前缀和来做此题。\n对原数组进行一次前缀异或和操作（新数组记为$s$），那么可以想到满足条件的二元组为\n$$\rs_r \\bigoplus s_{l - 1} = k\r$$维护一个桶记录 $s_i \\bigoplus k$ 的数量就可以用莫队解决这个问题。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 struct qry { int l, r, id; }; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; int a[MAXN], s[MAXN \u0026lt;\u0026lt; 4], cnt[MAXN \u0026lt;\u0026lt; 4]; void solve() { int n, k, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; k; for(int i = 1; i \u0026lt;= n; i ++) { cin \u0026gt;\u0026gt; a[i]; s[i] = s[i - 1] ^ a[i]; } vector\u0026lt;qry\u0026gt; q(m); for(int i = 0; i \u0026lt; m; i ++) { cin \u0026gt;\u0026gt; q[i].l \u0026gt;\u0026gt; q[i].r; q[i].id = i; } int B = sqrt(n); sort(q.begin(), q.end(), [\u0026amp;](qry a, qry b) { int xa = a.l / B, xb = b.l / B; if(xa != xb) return xa \u0026lt; xb; int ya = a.r / B , yb = b.r / B; if(xa \u0026amp; 1) { return ya \u0026gt; yb; } else { return ya \u0026lt; yb; } }); vector\u0026lt;ll\u0026gt; res(m); int lt = 0, rt = -1; ll w = 0; for(const auto \u0026amp;[l, r, id] : q) { while(rt \u0026lt; r) { rt ++; int x = s[rt]; w += cnt[x]; cnt[x ^ k] ++; } while(rt \u0026gt; r) { int x = s[rt]; cnt[x ^ k] --; w -= cnt[x]; rt --; } while(lt \u0026gt; l - 1) { lt --; int x = s[lt]; w += cnt[x]; cnt[x ^ k] ++; } while(lt \u0026lt; l - 1) { int x = s[lt]; cnt[x ^ k] --; w -= cnt[x]; lt ++; } res[id] = w; } for(auto i : res) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 一个简单的询问 题目大意\n$q$ 次询问，每次询问 $(l_1, r_1, l_2, r_2)$ ，求 $\\sum_{x} get(l_1, r_1, x) \\times get(l_2, r_2, x)$，其中 $get(l, r, x)$ 表示的区间 $[l, r]$ 中$x$ 出现的次数。\n莫队是处理区间二元组的一种手法，而此题中的询问有 $4$ 个参数，我们希望能够通过一些手段转化这个问题。\n差分是莫队常见的降低参数个数的手法，将 $get(l, r, x)$ 做一次差分实际上就转化为了 $get(1, r, x) - get(1, l - 1, x)$。\n这样原问题就可以化简为 $$\r\\sum_{x} get(1, r_1, x)get(1, r_2, x) - get(1, r_1, x)get(1, l_2 - 1, x) + get(1, l_1 - 1, x) get(1, l2 - 1, x) - get(1, l_1 - 1, x)get(1, r_2, x)\r$$经此化简，我们将原题转化为了四个不相关的莫队进行求解。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; struct qry { int l, r, id; }; int a[MAXN], n, m; void solve() { cin \u0026gt;\u0026gt; n; for(int i = 1; i \u0026lt;= n; i ++) { cin \u0026gt;\u0026gt; a[i]; } cin \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;qry\u0026gt;\u0026gt; q(4, vector\u0026lt;qry\u0026gt; (m)); for(int i = 0; i \u0026lt; m; i ++) { int l1, r1, l2, r2; cin \u0026gt;\u0026gt; l1 \u0026gt;\u0026gt; r1 \u0026gt;\u0026gt; l2 \u0026gt;\u0026gt; r2; q[0][i].l = r1, q[0][i].r = r2, q[0][i].id = i; q[1][i].l = r1, q[1][i].r = l2 - 1, q[1][i].id = i; q[2][i].l = l1 - 1, q[2][i].r = l2 - 1, q[2][i].id = i; q[3][i].l = l1 - 1, q[3][i].r = r2, q[3][i].id = i; for(int j = 0; j \u0026lt; 4; j ++) { if(q[j][i].l \u0026gt; q[j][i].r) { swap(q[j][i].l, q[j][i].r); } } } int B = sqrt(n); vector\u0026lt;ll\u0026gt; res(m); for(int i = 0; i \u0026lt; 4; i ++) { sort(q[i].begin(), q[i].end(), [\u0026amp;](qry a, qry b) { int xa = a.l / B, xb = b.l / B; if(xa != xb) return xa \u0026lt; xb; int ya = a.r / B , yb = b.r / B; if(xa \u0026amp; 1) { return ya \u0026gt; yb; } else { return ya \u0026lt; yb; } }); ll w = 0; int lt = 0, rt = 0; vector\u0026lt;int\u0026gt; cnt1(n + 1), cnt2(n + 1); for(const auto \u0026amp;[l, r, id] : q[i]) { while(rt \u0026lt; r) { rt ++; int x = a[rt]; w -= 1ll * cnt1[x] * cnt2[x]; cnt2[x] ++; w += 1ll * cnt1[x] * cnt2[x]; } while(rt \u0026gt; r) { int x = a[rt]; w -= 1ll * cnt1[x] * cnt2[x]; cnt2[x] --; w += 1ll * cnt1[x] * cnt2[x]; rt --; } while(lt \u0026lt; l) { lt ++; int x = a[lt]; w -= 1ll * cnt1[x] * cnt2[x]; cnt1[x] ++; w += 1ll * cnt1[x] * cnt2[x]; } while(lt \u0026gt; l) { int x = a[lt]; w -= 1ll * cnt1[x] * cnt2[x]; cnt1[x] --; w += 1ll * cnt1[x] * cnt2[x]; lt --; } if(i \u0026amp; 1) { res[id] -= w; } else { res[id] += w; } } } for(auto i : res) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 大数 题目大意\n给定一个质数 $p$ 与一个大数 $S$，进行$q$ 次询问，每次询问 $[l, r]$ 中有多少个子串满足 $S[l, r]$ 能够被 $p$ 整除。\n首先因为是基于 $10$ 进制的，因此 $2, 5$ 是唯二能够整除 $10$ 的数，所以这两个数需要进行特殊讨论。\n因为是子串，所以首先想到的是将字串转化为区间二元组进行计算。因为对于前缀和实际上没有意义，我们维护一个后缀和 $suf$，对于一个区间 $S[l, r]$，实际上 $suf_l - suf_{r + 1} = xxx000...0$，那么区间 $S[l, r]$ 实际上所对应的数字就是 $(suf_l - suf_{r + 1}) / 10^k$，已知 $10$ 不能够被 $p$ 整除，那么有且仅有$suf_l - suf_{r + 1} = 0$ 时满足条件，即 $suf_l = suf_{r + 1}$。接下来用莫队就能解决这个问题。\n当 $p = 2\\ or\\ 5$ 时，实际上仅需考虑末位能够被 $p$ 整除的位置到左端点的个数， 即当 $p = 5$ 时，仅需统计 $xxx5$ 和 $xxx0$ 的个数即可。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; ll suf[MAXN]; struct qry { int l, r, id; }; void solve() { int p; string s; cin \u0026gt;\u0026gt; p \u0026gt;\u0026gt; s; int n = s.size(); int m; cin \u0026gt;\u0026gt; m; vector\u0026lt;qry\u0026gt; q(m); for(int i = 0; i \u0026lt; m; i ++) { cin \u0026gt;\u0026gt; q[i].l \u0026gt;\u0026gt; q[i].r; q[i].l --, q[i].r --; q[i].id = i; } vector\u0026lt;int\u0026gt; temp; temp.push_back(0); for(int i = n - 1, d = 1; i \u0026gt;= 0; i --) { suf[i] = (suf[i + 1] + 1ll * (s[i] - \u0026#39;0\u0026#39;) * d) % p; temp.push_back(suf[i]); d = 1ll * d * 10 % p; } sort(temp.begin(), temp.end()); temp.resize(unique(temp.begin(), temp.end()) - temp.begin()); for(int i = 0; i \u0026lt; n; i ++) { suf[i] = lower_bound(temp.begin(), temp.end(), suf[i]) - temp.begin(); } int B = sqrt(n); vector\u0026lt;ll\u0026gt; res(m); sort(q.begin(), q.end(), [\u0026amp;](qry a, qry b) { int xa = a.l / B, xb = b.l / B; if(xa != xb) return xa \u0026lt; xb; int ya = a.r / B, yb = b.r / B; if(xa \u0026amp; 1) return ya \u0026gt; yb; else return ya \u0026lt; yb; }); if(p == 2 || p == 5) { int lt = 0, rt = -1; int cnt = 0; ll w = 0; for(const auto \u0026amp;[l, r, id] : q) { while(rt \u0026lt; r) { rt ++; int x = s[rt] - \u0026#39;0\u0026#39;; if(x % p == 0) { cnt ++; w += rt - lt + 1; } } while(rt \u0026gt; r) { int x = s[rt] - \u0026#39;0\u0026#39;; if(x % p == 0) { w -= rt - lt + 1; cnt --; } rt --; } while(lt \u0026gt; l) { lt --; int x = s[lt] - \u0026#39;0\u0026#39;; if(x % p == 0) { cnt ++; } w += cnt; } while(lt \u0026lt; l) { int x = s[lt] - \u0026#39;0\u0026#39;; w -= cnt; if(x % p == 0) { cnt --; } lt ++; } res[id] = w; } } else { int lt = 0, rt = -1; vector\u0026lt;int\u0026gt; cnt(temp.size()); ll w = 0; for(const auto \u0026amp;[l, r, id] : q) { while(rt \u0026lt; r + 1) { rt ++; int x = suf[rt]; w += cnt[x]; cnt[x] ++; } while(rt \u0026gt; r + 1) { int x = suf[rt]; cnt[x] --; w -= cnt[x]; rt --; } while(lt \u0026gt; l) { lt --; int x = suf[lt]; w += cnt[x]; cnt[x] ++; } while(lt \u0026lt; l) { int x = suf[lt]; cnt[x] --; w -= cnt[x]; lt ++; } res[id] = w; } } for(auto i : res) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } ","permalink":"https://solarain.net.cn/posts/%E8%8E%AB%E9%98%9F/","summary":"一种离线计算区间二元组个数的方法，一般用于无法用线段树高效进行区间合并的情况下。","title":"莫队"},{"content":"在算法竞赛中，有一种特殊的处理方式针对一组数的异或运算。在 $mod\\ 2$ 域内，任意一个维度仅有零向量与单位向量，而异或正是一种非常便利的在 $mod\\ 2$ 域内的”高斯消元“。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 struct LBase { int dims = 0; vector\u0026lt;ll\u0026gt; d; bool zero = false; LBase(int n = 62) : dims(n) { d.resize(n); } bool insert(ll x) { for (int i = dims - 1; i \u0026gt;= 0; i --) { if (x \u0026amp; (1ll \u0026lt;\u0026lt; i)) { if(d[i]) { x ^= d[i]; } else { d[i] = x; /* 下面的代码需要用到最小构造时加入 for(int j = i - 1; j \u0026gt;= 0; j --) { if(d[j] \u0026amp;\u0026amp; (d[i] \u0026gt;\u0026gt; j \u0026amp; 1) ) d[i] ^= d[j]; } for(int j = dims - 1; j \u0026gt; i; j --) { if(d[j] \u0026gt;\u0026gt; i \u0026amp; 1) d[j] ^= d[i]; } ------------------------------ */ break; } } } zero |= x == 0; return x \u0026gt; 0; } ll get_max() { ll x = 0; for (int i = dims - 1; i \u0026gt;= 0; i --) if(d[i]) { x = max(x, x ^ d[i]); } return x; } ll get_min() { ll x = 0; for(int i = 0; i \u0026lt; dims; i ++) if(d[i]) { x = d[i]; break; } return x; } bool check_ex(ll x) { for(int i = dims - 1; i \u0026gt;= 0; i --) { if(x \u0026amp; (1 \u0026lt;\u0026lt; i)) { if(!d[i]) { return false; } x ^= d[i]; } } return true; } void merge(const LBase \u0026amp;a) { for (int i = a.dims - 1; i \u0026gt;= 0; i --) if(a.d[i]) { insert(a.d[i]); } } int get_ranks() { int ranks = 0; for(int i = 0; i \u0026lt; dims; i ++) { ranks += (d[i] \u0026gt; 0); } return ranks; } // 求第 k 小， 需要用到最小构造 ll kth(ll k) { ll res = 0, rks = get_ranks(); if(zero) k --; if(k \u0026gt;= (1 \u0026lt;\u0026lt; rks)) return -1; for(int i = 0; i \u0026lt; dims; i ++) { if(k \u0026amp; (1ll \u0026lt;\u0026lt; i)) res ^= d[i]; } return res; } }; 2023 牛客多校 9 G Non-Puzzle: Game\n题目大意\nA,B 两人博弈，初始有 $n$ 个数，$a_1, a_2, ..., a_n$，A 先手，每人可以任意选择两个数 $a_i,a_j$（$1\\leq i,j\\leq n$），并将 $a_i \\bigoplus a_j$ 加入原数组中，先得到 $k$ 的人获胜。\n题解\n事实上，如果 A 第一回合无法获胜，那么就只有平局和 B 胜的情况。\n那么 B 胜的情况事实上非常好考虑，即\n$$\r\\forall i, j, t, a_i \\bigoplus a_j \\bigoplus a_t = k\r$$但是题目所给的 $n$ 非常大，显然上式不能用常规的方法证明，需要另寻他法。\n通过对上式进行一些转化，可以写出：\n$$\r\\forall i, j, t, (a_i \\bigoplus k) \\bigoplus (a_j \\bigoplus k) = a_t\\bigoplus k\r$$可以定义一种新的运算 $F$ 来定义上式，即：\n$$\ra_i F a_j = a_t\r$$原题就转化成了代数系统 $(a, F)$ 是闭包的。\n最后根据异或线性基的性质，仅需证明由 $a_i \\bigoplus k$ 构成的集合的秩的二次幂等于集合大小。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 struct LBase { int dims = 0; vector\u0026lt;ll\u0026gt; d; LBase(int n = 62) : dims(n) { d.resize(n); } bool insert(ll x) { for (int i = dims - 1; i \u0026gt;= 0; i --) if (x \u0026amp; ((ll)1 \u0026lt;\u0026lt; i)) { if(d[i]) { x ^= d[i]; } else { d[i] = x; break; } } return x \u0026gt; 0; } int get_ranks() { int ranks = 0; for(int i = 0; i \u0026lt; dims; i ++) { ranks += (d[i] \u0026gt; 0); } return ranks; } }; void solve() { int n, k; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; k; set\u0026lt;int\u0026gt; s; for(int i = 0; i \u0026lt; n; i ++) { int x; cin \u0026gt;\u0026gt; x; s.insert(x); } for(int i : s) { if(s.find(i ^ k) != s.end()) { cout \u0026lt;\u0026lt; \u0026#34;Alice\\n\u0026#34;; return; } } LBase lb; int cnt = 0; set\u0026lt;int\u0026gt; T; for(int x : s) { lb.insert(x ^ k); T.insert(x ^ k); } int r = lb.get_ranks(); if(((1ll \u0026lt;\u0026lt; r) == T.size())) { cout \u0026lt;\u0026lt; \u0026#34;Bob\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;Draw\\n\u0026#34;; } } ","permalink":"https://solarain.net.cn/posts/%E7%BA%BF%E6%80%A7%E5%9F%BA/","summary":"线性基通常指一组多维向量的极大线性无关组，通常可以用高斯消元法求出。","title":"线性基"},{"content":"https://acm.hdu.edu.cn/showproblem.php?pid=7311 gcd\n题目大意\n给定 $n$ 个二元组（称为 $N_i$），$q$ 次询问，每次询问给出二元组 $(A, B)$ ，每次操作可让 $(A, B)$ 变成 $(A + B, B)$ 或 $(A, A + B)$，最多能够到达几个 $N_i$。\n题解\n发现给定的 $N_i$ 可以进行辗转相减，且存在唯一的前驱和后驱，例如： $$\r(17, 12) \\rightarrow (5, 12) \\rightarrow (5, 7) \\rightarrow (5, 2) \\rightarrow (3,2) \\rightarrow (1, 2) \\rightarrow (1, 1)\r$$然而二元组的值域范围给得很大，这样做显然过不了，但是由辗转相减很容易想到辗转相除，这样的复杂度就降到了 $O(log\\ w)$。上面的例子就会被压缩成：\n$$\r(17, 12) \\rightarrow (5, 12) \\rightarrow (5, 2) \\rightarrow (1, 2) \\rightarrow (1, 1)\r$$将上述二元组每一个看成一个单独的节点，然后存放他们所有的后继，因为每一个后继对应一个 $N_i$，因此对于这样的一个二元组而言，后继的个数即是能够到达 $N_i$ 的个数。\n对于每次询问，只要在对应的二元组中找到位次就能轻松计算答案。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 typedef pair\u0026lt;ll, ll\u0026gt; PLL; void solve() { map\u0026lt;PLL, vector\u0026lt;PLL\u0026gt;\u0026gt; mp; int n, q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q; for(int i = 0; i \u0026lt; n; i ++) { ll a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; while(a \u0026amp;\u0026amp; b) { if(a \u0026gt; b) { mp[{a % b, b}].push_back({a, b}); a %= b; } else { mp[{a, b % a}].push_back({a, b}); b %= a; } } } for(auto \u0026amp;[a, b] : mp) { sort(b.begin(), b.end()); } for(int i = 0; i \u0026lt; q; i ++) { ll a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; int res = 0; if(a \u0026gt;= b) { res += mp[{a % b, b}].end() - lower_bound(mp[{a % b, b}].begin(), mp[{a % b, b}].end(), make_pair(a, b)); } if(b \u0026gt;= a) { res += mp[{a, b % a}].end() - lower_bound(mp[{a, b % a}].begin(), mp[{a, b % a}].end(), make_pair(a, b)); } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } https://acm.hdu.edu.cn/showproblem.php?pid=7318 mobius\nhttps://acm.hdu.edu.cn/showproblem.php?pid=7308 2-sat\n题目大意\n给定 $2n$ 个三元组 $A_i(a_i,b_i,c_i)$ 和 $A_i'(a_i',b_i',c_i')$，保证 $a_i \\leq a_i', b_i \\leq b_i', c_i \\leq c_i'$，对于$\\forall i$，可以选择 $A_i$或者 $A_i'$，求任意两个三元组每项差的最大可能的最小值。\n题解\n由于 $a_i \\leq a_i', b_i \\leq b_i', c_i \\leq c_i'$ 的存在，可以首先让每个人选择 $A_i$ ，每次贪心地将极值最大的那个人换成 $A_i'$，记录过程中答案的最小值。\n复杂度 $O(n log n)$。但是这个贪心的正确性难以证明。\n更能说明正确性的是二分答案， 优化建图，$2-sat$ check 的方法。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 typedef pair\u0026lt;int, int\u0026gt; PII; void solve() { int n; cin \u0026gt;\u0026gt; n; struct Z { int a, b, c; }; vector\u0026lt;Z\u0026gt; A(n), B(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; A[i].a \u0026gt;\u0026gt; A[i].b \u0026gt;\u0026gt; A[i].c \u0026gt;\u0026gt; B[i].a \u0026gt;\u0026gt; B[i].b \u0026gt;\u0026gt; B[i].c; } vector\u0026lt;bool\u0026gt; vis(n); set\u0026lt;PII\u0026gt; sa, sb, sc; for(int i = 0; i \u0026lt; n; i ++) { sa.insert({A[i].a, i}); sb.insert({A[i].b, i}); sc.insert({A[i].c, i}); } int res = INF; do { int id = -1; int difa = sa.rbegin() -\u0026gt; first - sa.begin() -\u0026gt; first; int difb = sb.rbegin() -\u0026gt; first - sb.begin() -\u0026gt; first; int difc = sc.rbegin() -\u0026gt; first - sc.begin() -\u0026gt; first; res = min(res, max({difa, difb, difc})); if(difa \u0026gt;= difb \u0026amp;\u0026amp; difa \u0026gt;= difc) { id = sa.begin() -\u0026gt; second; } else if (difb \u0026gt;= difa \u0026amp;\u0026amp; difb \u0026gt;= difc) { id = sb.begin() -\u0026gt; second; } else if (difc \u0026gt;= difa \u0026amp;\u0026amp; difc \u0026gt;= difb) { id = sc.begin() -\u0026gt; second; } if(id == -1 || vis[id]) break; vis[id] = true; sa.erase({A[id].a, id}); sb.erase({A[id].b, id}); sc.erase({A[id].c, id}); sa.insert({B[id].a, id}); sb.insert({B[id].b, id}); sc.insert({B[id].c, id}); } while(true); cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } https://ac.nowcoder.com/acm/contest/57358/J\n题目大意\n构造一个长度为 $n$ 的数组，满足\n$$\r\\forall\\ i, -m\\leq a_i\\leq m\\\\\r\\forall\\ 1 \\leq l \u003c r \\leq n, \\sum_{i=l}^r a_i \\geq 0\\\\\r$$求所有可能的数组的个数。\n题解\n考虑一个线性 $dp$，发现第 $i$ 位的选数只与之前的后缀最小值有关，因此可以设计一个二维的$dp$，第一维度表示选数，第二维表示当前选数状态下的后缀最小值。\n可以写出 $dp$ 递推关系\n$$\rdp_{i,j} \\rightarrow dp_{i + 1, x}, 0 \\leq j, j - m \\leq x \\leq j + m \\\\\\\\\rdp_{i,j} \\rightarrow dp_{i + 1, x} 0 \u003e j, 0 \\leq x \\leq j + m \\\\\\\\\r$$利用差分前缀和优化就能 $O(n^2)$ 求出上面这个式子。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; dp(n, vector\u0026lt;ll\u0026gt; (2 * m + 1)); for(int i = 0; i \u0026lt;= 2 * m; i ++) dp[0][i] = 1; for(int i = 1; i \u0026lt; n; i ++) { for(int j = - m; j \u0026lt;= m; j ++) { if(j \u0026gt;= 0) { dp[i][m - j] += dp[i - 1][j + m]; } else { dp[i][m - 0] += dp[i - 1][j + m]; dp[i][2 * m + j + 1] -= dp[i - 1][j + m]; } } (dp[i][0] += mod) %=mod; for(int j = -m + 1; j \u0026lt;= m; j ++) { (dp[i][j + m] += mod) %= mod; (dp[i][j + m] += dp[i][j + m - 1]) %= mod; } } ll res = 0; for(int i = 0; i \u0026lt;= 2 * m; i ++) { res += dp[n - 1][i]; } cout \u0026lt;\u0026lt; res % mod \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } https://ac.nowcoder.com/acm/contest/57356/H\nhttps://ac.nowcoder.com/acm/contest/57357/B\nhttps://ac.nowcoder.com/acm/contest/57357/E https://acm.hdu.edu.cn/showproblem.php?pid=7309\nhttps://acm.hdu.edu.cn/showproblem.php?pid=7301 https://acm.hdu.edu.cn/showproblem.php?pid=7294\nhttps://acm.hdu.edu.cn/showproblem.php?pid=7320 https://acm.hdu.edu.cn/showproblem.php?pid=7319\n","permalink":"https://solarain.net.cn/posts/2023%E5%A4%9A%E6%A0%A1%E6%9D%82%E9%A2%98/","summary":"2023 杭电多校以及牛客多校的一些杂题目。","title":"2023多校杂题"},{"content":"以下是我自己在真机上安装过多次可行的方法。\n在真机安装和在虚拟机上安装还是有很大的区别，此文章简单说明了其中的一些必要的步骤。\n准备安装介质 从镜像网站上 https://mirrors.ustc.edu.cn/ 获取映像文件。\n用 rufus 等烧录文件将映像以 DD 形式烧录到U盘，ISO 不行，至少在我的电脑上存在这样的问题。\n启动到Live环境 Live 环境中包含的 Archlinux 安装所需的工具。\n电脑重启后按 F2 进入 BIOS（不同厂商的电脑进入 BIOS 的按键可能不一样）。\n如果是双系统安装，需要在 BIOS 中将 Sevure Boot 设置为 Disable，装完后也可以重新启用。\n联网 利用 iwctl 进行联网\ndevice list 查看已安装网卡\nstation DEVICE scan 扫描周边的 wifi\nstation DEVICE get-networks 显示扫描到的 wifi\nstation --passphrase=PASSPHRASE station DEVICE connect SSID 连接到所选的 wifi\n启动方式的选择 查看启动发生BIOS or UEFI\n1 [ -d /sys/firmware/efi ] \u0026amp;\u0026amp; echo UEFI || echo BIOS 磁盘分区 创建磁盘分区：需要一个根分区和一个EFI系统分区（UEFI 启动方式需要这个分区），可以选择创建一个交换分区。\n如果是双系统， EFI 分区可以直接挂载 Windows 已经创建好的 EFI 分区。\n交换分区是一种虚拟内存的表现形式，系统为了应付需要用到大量内存的场景，将磁盘上的一部分空间当作内存使用。\nswap文件创建规则（参照oracle官方文档设定的标准）:\n4G以内的物理内存，SWAP 设置为内存的2倍。 4-8G的物理内存，SWAP 等于内存大小。 8-64G 的物理内存，SWAP 设置为8G。 64-256G物理内存，SWAP 设置为16G。 利用fdisk -l查看当前磁盘分区状况，找到所需要安装系统的磁盘，利用cfdisk进行分盘（这是一个图形化的分区修改界面）。\n如果是一块未分区的磁盘，会提示使用哪种分区表，按照官方文档的说法 BIOS 启动应采用 MAR 分区表，UEFI 启动应采用 GPT 分区表。\n具体详见 wiki-引导加载程序\n虚拟机分区例子（传统引导）：\n格式化分区 详见 wiki-文件系统\n1 2 3 mkfs.ext4 /dev/root_partition（根分区） mkswap /dev/swap_partition（交换空间分区） mkfs.fat -F 32 /dev/efi_system_partition（EFI 分区） 挂载分区 挂载：指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录（最好是空目录），访问此目录就等同于访问设备文件。\n1 2 3 mount /dev/root_partition（根分区） /mnt mount --mkdir /dev/efi_system_partition（EFI 系统分区） /mnt/boot swapon /dev/swap_partition（交换空间分区） 要注意挂载顺序，一定是先挂载根分区再挂载 EFI 分区。\n安装 选择镜像 官方所有的镜像源：https://archlinux.org/download/\n1 2 3 4 5 6 7 8 9 10 # 备份 cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bk # 选择在中国的最快镜像源 reflector --verbose --country \u0026#39;China\u0026#39; -l 200 -p https --sort rate --save /etc/pacman.d/mirrorlist # 清华园镜像 Server = https：//mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os$arch # 中科大镜像 Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os$arch # 更新 pacman -Sy 文件 /etc/pacman.d/mirrorlist 定义了软件包会从哪个镜像下载，优先级从上向下依次递减。\n安装基本系统 1 pacstrap /mnt base base-devel linux linux-firmware linux-headers 其中 pacstryp 是一个安装脚本。\n其中 base 和 base-devel 包含一系列的系统软件，必须安装。linux是内核，linux-firmware 是一些驱动，linux-headers 是内核头文件。\n当然，内核也可以选择不是原版的 Linux，比如 Linux-lts，可以在 wiki 上找到其他版本的内核 wiki-内核。\n配置新系统 创建Fstab文件\n用以下命令生成 fstab 文件 (用 -U 或 -L 选项设置 UUID 或卷标)：\n1 genfstab -U /mnt \u0026gt;\u0026gt; /mnt/etc/fstab fastab 该文件包含了当前分区挂载情况，生成之后建议检查里面的内容是否正确。\nchroot 到新安装的系统：\n从 Live 环境进入挂载点。\n1 arch-chroot /mnt 下载一些必要的软件\n利用 pacman 进行安装。\n1 pacman -Syy # 更新软件包列表 networkmanager 连接互联网\nnet-tools 包含 ifconfig 等命令\nvim 文本编辑器\ndhcpcd 分配 ip 地址\nopenssh ssh 服务\ngit\ngrub 用来引导系统\nos-prober 双系统可选安装，可以选择进入那个系统\nefibootmgr UEFI引导必装\nintel-ucode/amd-ucode CPU 微码必装\nman 查看软件包的文档\nntfs-3g 访问 ntfs 格式的磁盘，双系统必装\nnoto-fonts-cjk 和 noto-fonts-emoji 谷歌设计的中文字体\n设置中文字符\n编辑 locale.gen 将中文（zh_CN_UTF-8）以及英文（en_US_UTF-8）前的注释去掉。\n输入locale-gen设置字符集。\n在文件 /etc/locale.conf 中写入 LANG=en_US.UTF-8 保存。\n1 echo LANG=en_US.UTF-8 \u0026gt; /etc/locale.conf 这里需要设置成英文，在 TTY 状态下中文会乱码。\nTTY 没有安装图形界面时的状态。\n设置时区(以上海为例)\n1 ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 生成/etc/adjtime\n1 hwclock --systohc 配置主机名\n1 echo YOURPC \u0026gt; /etc/hostname 以及配置密码\n1 passwd 自启动 NetworkManager， ssh，dhcpcd\n1 systemctl enable NetworkManager sshd dhcpcd 安装引导程序 GRUB 默认不支持 os-prober，需要做如下修改：\n编辑/etc/default/grub，将最后一行#GRUB_DISABLE_OS_PROBER=false取消注释\n对于UEFI\n1 2 grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB grub-mkconfig -o /boot/grub/grub.cfg 对于BIOS\n1 2 grub-install --target=i386-pc /dev/sda grub-mkconfig -o /boot/grub/grub.cfg 退出重启\n1 2 exit reboot 双系统重启后可能还是找不到 Windows 的启动引导，可以在进入系统后再执行一次grub-mkconfig -o /boot/grub/grub.cfg，也可以自己手动在grub里添加引导入口。\n配置本地用户 进入新系统后可以用 NetworkManager 联网\n显示 wifi 列表\n1 nmcli dev wifi list 连接指定 wifi\n1 nmcli dev wifi connect SSID password PASSWORD 创建普通用户\n1 2 useradd -m -G wheel YOURNAME passwd YOURNAME wheel 是 linux 本身包含的一个用户组。\n配置 sudo\n编辑/etc/sudoers，取消#wheel ALL=(ALL:ALL) ALL的注释。\nsu YOURNAME 进入本地用户，下文中的命令中若出现$则表示在本地用户执行的。\n网络配置 将下面内容写入到 /etc/hosts\n1 2 3 127.0.0.1 localhost ::1 localhost 127.0.1.1 yourPCname.localdomain yourPCname 配置AUR 全称 Arch User Repository, 简称 AUR。\n修改 /etc/pacman.conf\n将 [multilib] 的两行取消注释，这个仓库包含 Arch 官方软件仓库的 32 位软件和链接库。\n配置 Archlinuxcn\n添加两行\n1 2 [archlinuxcn] Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch 这个仓库是 Arch Linux 中文社区驱动的非官方用户仓库。包含中文用户常用软件、工具、字体\\美化包等。\n安装 archlinuxcn-keyring 导入 GPG 密钥\n安装paru 或者 yay\n1 $ sudo pacman -S paru 安装驱动 显卡驱动 我的笔记本是 Amd 的核显和 Nvidia 的独显，装的内核是 Linux，这里举个例子。\n1 2 $ sudo pacman -S xf86-video-amdgpu mesa lib32-mesa $ sudo pacman -S nvidia nvidia-utils lib32-nvidia-utils 同时把 kms 从 /etc/mkinitcpio.conf 里的 HOOKS 数组中移除，并重新生成 initramfs。 这能防止 initramfs 包含 nouveau 模块，以确保内核在早启动阶段不会加载它。\n1 $ mkinitcpio -p linux Nvidia 驱动的安装详见 wiki-Nvidia\n安装桌面环境 这里简单介绍对 Gnome 的一些配置。\n在我的电脑上 Gnomo 账户注销再登陆就会直接卡住 QAQ，考虑找别的桌面替代。\n安装 Gnome 桌面 1 $ sudo pacman -S gnome gnome-tweaks gnome-extra gdm 开机自启动\n1 $ sudo systemctl enable gdm 新安装好的Gnomo用起来有诸多不便，没有系统托盘，没有 Dock 栏，没有桌面图标等等，我们需要安装拓展插件来弥补这些功能的缺失。\n安装支持库\n1 $ sudo pacman -S gnome-browser-connector 以下是我比较建议安装的几个插件:\n自定义主题\nUser Themes\n显示桌面图标\nDesktop Icons NG (DING)\n显示暂时回到桌面图标\nShow Desktop Button\nDock栏\nDash to Dock\n系统托盘\nAppIndicator and KStatusNotifierItem Support\n","permalink":"https://solarain.net.cn/posts/archlinux%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97/","summary":"ArchLinux入指南，简述了一些步骤以及其中的一些概念。","title":"ArchLinux安装指南"},{"content":"双连通分量 在一个无向连通图中，对于两个点 $u, v$ 如果删除任意一条边都不能使其不连通，则称 $u, v$ 边双连通。\n在以 $u$ 为源点， $v$ 为汇点的网络流模型中，其具象化的表现为 $u \\rightarrow v$ 的流量至少为 $2$。\n在一个无向连通图中，对于两个点 $u, v$ 如果删除任意一个点都不能使其不连通，则称 $u, v$ 点双连通。\n在以 $u$ 为源点， $v$ 为汇点的网络流模型中，通过对（非源、汇点）拆点的操作——每个点拆成中间连一条容量为 $1$ 的边，其具象化的表现为 $u \\rightarrow v$ 的流量至少为 $2$。\n点双连通图 图上任意两个点 $u, v$ 都是点双连通的。一个近乎等价的定义——一张连通图上不存在割点（仅在两个点一条边的情况下实失效）。 圆方树 将一张图分成 $k$ 个极大点双连通分量，每一个点双建一个方点，这个方点和该点双内的所有圆点连一条边（原图上的点成为圆点，每个点双创建的新的点称为方点）。\n在圆方树中与多个方点所相连的圆点是原图上的割点。\n下面是一张无向图转化为圆方树的形态。\n例题 2023 杭电多校2 1003 Counter Strike\n题目大意\n题解\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 int n, m, k, cnt; vector\u0026lt;int\u0026gt; gra[MAXN], tre[MAXN \u0026lt;\u0026lt; 1]; int dfn[MAXN], low[MAXN], tim; stack\u0026lt;int\u0026gt; stk; void init() { for(int i = 0; i \u0026lt; n * 2; i ++) { tre[i].clear(); } for(int i = 0; i \u0026lt; n; i ++) { dfn[i] = low[i] = -1; } tim = 0; cnt = n; while (!stk.empty()) { stk.pop(); } } int Tarjan(int u, const vector\u0026lt;int\u0026gt;\u0026amp; del) { int res = (del[u] == 2); dfn[u] = low[u] = ++ tim; stk.push(u); for(int v : gra[u]) { if(dfn[v] == -1 \u0026amp;\u0026amp; del[v] != 1) { res += Tarjan(v, del); low[u] = min(low[u], low[v]); if(low[v] == dfn[u]) { for(int x = 0; x != v; stk.pop()) { x = stk.top(); tre[cnt].push_back(x); tre[x].push_back(cnt); } tre[u].push_back(cnt); tre[cnt].push_back(u); cnt ++; } } else if(del[v] != 1) { low[u] = min(low[u], dfn[v]); } } return res; } PII dfs(int u, int f, const vector\u0026lt;int\u0026gt;\u0026amp; del) { int res = - 1, count = ((u \u0026lt; n) \u0026amp;\u0026amp; (del[u] == 2)); for(int v : tre[u]) if(v != f) { auto x = dfs(v, u, del); count += x.second; if(x.first != -1) { res = x.first; } else if(res == -1 \u0026amp;\u0026amp; count \u0026gt;= 2) { res = u; } } return {res, count}; } PII dfs2(int u, int f, const vector\u0026lt;int\u0026gt;\u0026amp; del) { int res = 0, count = ((u \u0026lt; n) \u0026amp;\u0026amp; (del[u] == 2)); for(int v : tre[u]) if(v != f) { auto x = dfs(v, u, del); count += x.second; res |= (x.second \u0026gt;= 2); } return {res, count}; } void solve() { cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; k; for(int i = 0; i \u0026lt; m; i ++) { int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; a --, b --; gra[a].push_back(b); gra[b].push_back(a); } vector\u0026lt;int\u0026gt; h(k); for(int i = 0; i \u0026lt; k; i ++) { cin \u0026gt;\u0026gt; h[i]; h[i] --; } int l = -1, r = k + 1; vector\u0026lt;int\u0026gt; del(n); auto check = [\u0026amp;](int m) -\u0026gt; bool { init(); for(int i = 0; i \u0026lt; n; i ++) { del[i] = 0; } for(int i = 0; i \u0026lt; k; i ++) { if(i \u0026lt; m) { del[h[i]] = 1; } else { del[h[i]] = 2; } } int counter = 0; bool ok = true; for(int i = 0; i \u0026lt; n; i ++) if(dfn[i] == -1 \u0026amp;\u0026amp; del[i] != 1) { int w = Tarjan(i, del); stk.pop(); counter += (w \u0026gt;= 2); if(w \u0026gt; 2) { int X = dfs(i, i, del).first; if(X \u0026gt;= n) { ok = false; } else { ok = !dfs2(X, X, del).first; } } if(counter \u0026gt;= 2) { ok = false; } if(! ok) { break; } } return ok; }; while(r - l \u0026gt; 1) { int m = l + r \u0026gt;\u0026gt; 1; if(check(m)) { r = m; } else { l = m; } } cout \u0026lt;\u0026lt; r \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(int i = 0; i \u0026lt; n; i ++) { gra[i].clear(); } } ","permalink":"https://solarain.net.cn/posts/%E5%9C%86%E6%96%B9%E6%A0%91/","summary":"将图上的点双连通新建一个方点，与其他原图上的圆点建立菊花图，并以此为基础构建重构树。具有树的性质，可以配合树上的各种算法，比较方便得处理图上的割点。","title":"圆方树"},{"content":"2023 杭电多校 2 1006 Fencing the cows\n题目大意\n给定两组点$A, B$，求在 $A$ 中至少选择 $k$ 个点围成的凸包 $T$ 使得 $\\forall b_i \\subseteq T, b_i \\in B$\n题解\n对于任意两两点组成的线段 $s$，可以通过 $B$ 中的所有点是否在 $s$ 的 $CCW$ 方向来判断线段 $s$ 是否合法，可以 $O(n^2)$ 找出所有这样的线段。\n处理所有的线段，可以利用 $dp$ 或者 $Floyd$ 找最小环来求出答案。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; using ll = long long; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; struct Point { ll x, y; Point() {}; Point(int x, int y) : x(x), y(y) {} }; ll crs(Point a, Point b) { return a.x * b.y - a.y * b.x; } typedef vector\u0026lt;Point\u0026gt; Points; void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; Points ps1(n); Points ps2(m); for(auto \u0026amp;[x, y] : ps1) { cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; } for(auto \u0026amp;[x, y] : ps2) { cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; } auto check = [\u0026amp;](Point a, Point b) { bool ok = true; for(Point p : ps2) { Point x = Point(b.x - a.x, b.y - a.y); Point y = Point(p.x - a.x, p.y - a.y); if(!(ok = crs(x, y) \u0026gt; 0)) { break; } } return ok; }; struct Edge { int from, to; }; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; dis(n, vector\u0026lt;int\u0026gt; (n, INF)); for(int i = 0; i \u0026lt; n; i ++) { for(int j = 0; j \u0026lt; n; j ++) { if(i != j \u0026amp;\u0026amp; check(ps1[i], ps1[j])) { dis[i][j] = min(dis[i][j], 1); } } } for(int i = 0; i \u0026lt; n; i ++) { dis[i][i] = INF; } for(int k = 0; k \u0026lt; n; k ++) { for(int i = 0; i \u0026lt; n; i ++) { for(int j = 0; j \u0026lt; n; j ++) { dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); } } } int res = INF; for(int i = 0; i \u0026lt; n; i ++) { for(int j = i; j \u0026lt; n; j ++) { res = min(res, dis[i][j] + dis[j][i]); } } if(res \u0026gt;= INF) { res = -1; } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } int main() { ios::sync_with_stdio(false); cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { solve(); } } ","permalink":"https://solarain.net.cn/posts/%E8%AE%A1%E7%AE%97%E5%87%A0%E4%BD%95%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/","summary":"一些做过的计算几何题。","title":"计算几何做题记录"},{"content":"CF510 E Fox And Dinner\n2022 江苏省赛 B Prime Ring Plus\n2023 杭电多校 2 1012 Coin\n题目大意\n$n$ 个人初始时每人手里的一个硬币，每个人手中的硬币数量不能超过$a_i$个，进行 $m$ 轮操作，每轮给出 $A, B$，可以选择 $A$ 给 $B$ 一个硬币，或者 $B$ 给 $A$ 一个硬币，或者什么都不做，$n$ 个人中有 $k$ 个人是我的朋友，每轮操作都由我控制，希望让我所有的朋友能够拿到的硬币和最大，求出最大值。\n题解\n给定初始条件（源点与每个点的容量），$A \\rightarrow B$ 存在一条权为 $1$ 的边（带容量限制的有向边），点存在容量限制（网络流经典拆点），求和的最大值（汇点与最大流）。通过以上这些特征基本上可以确定此题是一道最大流。\n由于 $m$ 个操作存在时间上的先后顺序，我们可以考虑对每个点拆成 $m$ 个点（代表该点每个时间上的状态），这 $m$ 个点串联（单向边，并且所有边容量为 $a_i$），对于每个时间上的操作，我们将 $A, B$ 在对于的时间状态上连一条容量为 $1$ 的双向边，最后将所有点的最后时间状态连接到汇点（边权容量为$a_i$）。\n当然，发现此时创建的点的数量太多了，并且很多点都是冗余的，因此可以在需要用到该点的时间状态时再创建。最终得到的点的个数为 $m$ , 边的数量为 $n + m + k$。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; using ll = long long; typedef pair\u0026lt;int, int\u0026gt; PII; const int MAX_N = 2e5 + 10, INF = 1e9 + 7, mod = 998244353; namespace Dinic { using ll = long long; const int MAX_V = 2e4 + 10; struct Edge { int from, to; ll cup, flow; Edge(int u, int v, ll c, ll f) : from(u), to(v), cup(c), flow(f) {} }; struct Graph { int s, t, n, m; vector\u0026lt;Edge\u0026gt; es; vector\u0026lt;int\u0026gt; G[MAX_V]; int level[MAX_V], iter[MAX_V]; bool vis[MAX_V]; Graph(int s, int t, int n = MAX_V) : n(n), s(s), t(t) { es.clear(); for(int i = 0; i \u0026lt; n; i ++) { G[i].clear(); } }; void addEdge(int u, int v, ll c) { es.push_back({u, v, c, 0}), es.push_back({v, u, 0, 0}); m = es.size(); G[u].push_back(m - 2), G[v].push_back(m - 1); } ll dfs(int v, ll f) { if(v == t || f == 0) return f; ll flow = 0, d; for(int \u0026amp;i = iter[v]; i \u0026lt; G[v].size(); i ++) { auto \u0026amp;e = es[G[v][i]], \u0026amp;reve = es[G[v][i] ^ 1]; if(level[v] + 1 == level[e.to] \u0026amp;\u0026amp; (d = dfs(e.to, min(f, e.cup - e.flow))) \u0026gt; 0) { e.flow += d, reve.flow -= d; flow += d; f -= d; if(! f) break; } } return flow; } bool bfs() { memset(vis, false, sizeof vis); queue\u0026lt;int\u0026gt; q; q.push(s); level[s] = 0; vis[s] = true; while(q.size()) { int v = q.front(); q.pop(); for(int i = 0; i \u0026lt; G[v].size(); i ++) { auto \u0026amp;e = es[G[v][i]], \u0026amp;reve = es[G[v][i] ^ 1]; if(!vis[e.to] \u0026amp;\u0026amp; e.cup \u0026gt; e.flow) { vis[e.to] = true; level[e.to] = level[v] + 1; q.push(e.to); } } } return vis[t]; } ll maxflow() { ll flow = 0; while(bfs()) { memset(iter, 0, sizeof iter); flow += dfs(s, INF); } return flow; } }; }; using namespace Dinic; void solve() { int n, m, k; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; k; int s = 0, t = 2e4 + 1; Graph gra(s, t); vector\u0026lt;int\u0026gt; a(n + 1); vector\u0026lt;int\u0026gt; cur(n + 1); for(int i = 1; i \u0026lt;= n; i ++) { cin \u0026gt;\u0026gt; a[i]; gra.addEdge(s, i, 1); cur[i] = i; } int id = n + 1; for(int i = 1; i \u0026lt;= m; i ++) { int x, y; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; int n1 = cur[x], n2 = cur[y]; gra.addEdge(n1, n2, 1); gra.addEdge(n2, n1, 1); gra.addEdge(n1, cur[x] = id ++, a[x]); gra.addEdge(n2, cur[y] = id ++, a[y]); } for(int i = 0; i \u0026lt; k; i ++) { int x; cin \u0026gt;\u0026gt; x; int mp = cur[x]; gra.addEdge(mp, t, a[x]); } cout \u0026lt;\u0026lt; gra.maxflow() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } int main () { ios::sync_with_stdio(0); cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { solve(); } } 其中在做这道题的时候，有一个假的做法也是值得做一些思考的，如果所有的每轮所能做的操作在时间上没有先后顺序，那么只需要设置出两个点，这两个点中连一条容量为 $m$ 的有向边作为操作总数的瓶颈，每次操作都需要经过这两个点。\n","permalink":"https://solarain.net.cn/posts/%E7%BD%91%E7%BB%9C%E6%B5%81%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/","summary":"记录一些经典的网络流模型，做过的网络流题目。","title":"网络流做题记录"},{"content":"公平组合游戏 （Impartial Combinatorial Games，ICG） 两名玩家轮流对当前游戏状态在一个有限集合中进行一次操作。 一个局面的合法操作，只取决于游戏本身固然存在，而与玩家的次序或其他因素无关。 对于无法进行任何操作的玩家，即操作集合为空，则另一方获胜。 Nim游戏 $n$ 堆物品，每堆有 $a_i$ 个，两个玩家轮流取走任意一堆的任意个物品，但不能不取，不能操作的玩家为负。\nNim 游戏的结论是所有的 $\\bigoplus _{i = 1} ^ {n} a_i = 0$ 为先手必败。\nSG函数 将游戏状态和所有的合法操作抽象为有向无环图（DAG）。\n定义没有后继的状态为必败状态。\n一个必胜状态的后继中存在一个必败状态。\n一个必败状态的后继中不存在一个必败状态。\n这里给出一个不是非常严格的理解，上面所有的必胜状态和必败状态都是对于当前所需进行操作的玩家来说的，没有后继的状态为必败状态是游戏结束的前提，对于一个必胜状态，该玩家一定可以通过一种操作到达一种必败状态使自己胜利；对于一个必败状态，如果存在一个必败状态的后继，那么当前操作的玩家就可以通过一种操作到达一个必败状态使自己胜利，这与当前是必败状态所矛盾，因此一个必败状态的后继一定不存在一个必败状态。\n给所有的状态设置一个 $SG$ 值，对于 $SG$ 函数有如下定义：\n$$\rSG(x) = MEX\\lbrace SG(y_1), SG(y_2), SG(y_3), ..., SG(y_n)\\rbrace, \\forall i, x \\rightarrow y_i\r$$如果只进行一次博弈的话，求 SG 函数没有任何优势，但是对于一个博弈包含多个子博弈有如下定理：\n$$\rSG(x_1 + x_2 + ... + x_n) = SG(x_1) \\bigoplus SG(x_2) \\bigoplus ... \\bigoplus SG(x_n)\r$$ 这里对该结论不进行详细证明，详见OI-wiki\n其中，将 $n$ 个子博弈的并行关系用 $+$ 运算连接，每个子博弈都能求出一个 SG 值。\n例如在 Nim 游戏中将每堆物品看成一个子游戏，那么整个游戏的 SG 值为所有子游戏的SG值的异或和。\nSG值为 $0$ 为先手必败。\nMEX\n1 2 3 4 5 6 7 8 9 10 11 12 // v可以是vector、set等容器 int mex(auto v) { unordered_set\u0026lt;int\u0026gt; S; for (auto e : v) { S.insert(e); } for (int i = 0;; ++i) { if (S.find(i) == S.end()) { return i; } } } 例题 2023 杭电多校 1001 Alice Game\n题目大意：\n两人轮流操作，其中 $op1$ 表示消除连续不超过 $k$ 个怪物（且这段连续区间左右均为 $e$ 表示空），op2 表示消除连续 $k$ 个怪物，要求这段连续区间删除完后左右至少各有一个怪物。\n对于任意一段长度为 $X ,k + 2 \\leq X$ 的连续区间，都可以分成 $X_1, X_2$ 两部分，那么 $SG(X) = SG(X_1) \\bigoplus SG(X_2)$\n基于此，我们通过可以打表发现此题的结论是存在一个 $4k + 2$ 的循环节。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; using ll = long long; typedef pair\u0026lt;int, int\u0026gt; PII; const int MAX_N = 2e5 + 10, INF = 1e9 + 7, mod = 998244353; int mex(auto v) { unordered_set\u0026lt;int\u0026gt; S; for (auto e : v) { S.insert(e); } for (int i = 0;; ++i) { if (S.find(i) == S.end()) { return i; } } } void solve() { int k = 2; int n = 100; vector\u0026lt;int\u0026gt; dp(n + 1, -1); auto sg = [\u0026amp;](auto self, int x) -\u0026gt; int { if(dp[x] != -1) { return dp[x]; } set\u0026lt;int\u0026gt; s; for(int i = 1;; i ++) { int l = i, r = x - i - k; if(r \u0026lt;= 0) break; s.insert(self(self, l) ^ self(self, r)); } return dp[x] = mex(s); }; dp[0] = 0; for(int i = 1; i \u0026lt;= min(n, k); i ++) { dp[i] = 1; } sg(sg, n); for(int i = 1; i \u0026lt;= n; i ++) { cout \u0026lt;\u0026lt; dp[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39;; if(i % (4 * k + 2) == 0) { cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } void ac() { int k, n; cin \u0026gt;\u0026gt; k \u0026gt;\u0026gt; n; if(!n || (n \u0026gt;= k + 1 \u0026amp;\u0026amp; !((n - k - 1) % (4 * k + 2)))) { cout \u0026lt;\u0026lt; \u0026#34;Bob\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;Alice\\n\u0026#34;; } } int main () { ios::sync_with_stdio(0); cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { ac(); } } 2023 牛客多校 F Link with Chess Game\n题目大意：\n一个长度为 $n$ 的棋盘上有三颗棋子$r, g, b$，两个玩家轮流操作，每次操作都不能到达之前已经经过的三元组$(r_i, g_i, b_i)$，不能进行操作的玩家为负。\n对于每种操作进行搜索，因为不能到达之前已经经过的状态，可以进行记忆化搜索打表求出SG函数。\n结果发现打表的结果非常有规律，遂AC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 1:1 1 1!! 0 2:1 1 1!! 1 2:1 1 2!! 1 2:1 2 1!! 1 2:1 2 2!! 1 2:2 1 1!! 1 2:2 1 2!! 1 2:2 2 1!! 1 2:2 2 2!! 1 3:1 1 1!! 0 3:1 1 2!! 1 3:1 1 3!! 0 3:1 2 1!! 1 3:1 2 2!! 0 3:1 2 3!! 1 3:1 3 1!! 0 3:1 3 2!! 1 3:1 3 3!! 0 3:2 1 1!! 1 3:2 1 2!! 0 3:2 1 3!! 1 3:2 2 1!! 0 3:2 2 2!! 1 3:2 2 3!! 0 3:2 3 1!! 1 3:2 3 2!! 0 3:2 3 3!! 1 3:3 1 1!! 0 3:3 1 2!! 1 3:3 1 3!! 0 3:3 2 1!! 1 3:3 2 2!! 0 3:3 2 3!! 1 3:3 3 1!! 0 3:3 3 2!! 1 3:3 3 3!! 0 参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; using ll = long long; typedef pair\u0026lt;int, int\u0026gt; PII; const int MAX_N = 2e5 + 10, INF = 1e9 + 7, mod = 998244353; bool dp[100][100][100]; bool vis[100][100][100]; int mex(auto v) { unordered_set\u0026lt;int\u0026gt; S; for (auto e : v) S.insert(e); for (int i = 0;; ++i) if (S.find(i) == S.end()) return i; } int dx[6] = {-1,1,0,0,0,0}, dy[6] = {0,0,-1,1,0,0}, dz[6] = {0,0,0,0,1,-1}; void solve(int n) { for(int x = 1; x \u0026lt;= n; x ++) { for(int y = 1; y \u0026lt;= n; y ++) { for(int z = 1; z \u0026lt;= n; z ++) { memset(dp, 0, sizeof dp); memset(vis,0, sizeof vis); auto sg = [\u0026amp;](auto self, int r, int g, int b) -\u0026gt; void { vector\u0026lt;int\u0026gt; s; vis[r][g][b] = true; for(int k = 0; k \u0026lt; 6; k ++) { int X = r + dx[k], Y = g + dy[k], Z = b + dz[k]; if(X \u0026gt;= 1 \u0026amp;\u0026amp; X \u0026lt;= n \u0026amp;\u0026amp; Y \u0026gt;= 1 \u0026amp;\u0026amp; Y \u0026lt;= n \u0026amp;\u0026amp; Z \u0026gt;= 1 \u0026amp;\u0026amp; Z \u0026lt;= n \u0026amp;\u0026amp; !vis[X][Y][Z]) { self(self, X, Y, Z); s.push_back(dp[X][Y][Z]); } } dp[r][g][b] = mex(s); }; sg(sg, x, y, z); cout \u0026lt;\u0026lt; n \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; y \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; z \u0026lt;\u0026lt; \u0026#34;!! \u0026#34; \u0026lt;\u0026lt; dp[x][y][z] \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } void ac() { ll n, a, b, c; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; c; if(n % 2 == 0){ cout \u0026lt;\u0026lt; \u0026#34;Alice\\n\u0026#34;; } else{ if((a + b + c) % 2 == 1){ cout \u0026lt;\u0026lt; \u0026#34;Bob\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;Alice\\n\u0026#34;; } } } int main () { ios::sync_with_stdio(0); cin.tie(); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { ac(); } } ","permalink":"https://solarain.net.cn/posts/%E5%8D%9A%E5%BC%88%E8%AE%BA/","summary":"简单介绍了Nim游戏SG函数以及一些博弈论的例题","title":"博弈论"},{"content":"这算是人生经历的第一次历时最长的旅程了（真的是顺路打个比赛qaq）。\nday1 奔赴上海，跃入人海。\n第一站的目标是上海汽车会展中心参加崩坏三夏日嘉年华。15号下午到达上海南，这里原本想直接去上海大学找xy，但是想先在外滩溜达（和同学瞎逛）就没去成，之后从苏州回上海的时候去找的xy。恰逢上海下雨，南京路上每人都打把伞，体验了一波“张袂成阴”。\n南京路上一份米粉买36，是我消费不起的价格qwq。\n在外滩拍了几张照片之后坐11号线去了安亭（人太多了没去江边）。\n到达会展中心附近的旅馆，自从下火车之后不是在走路就是在地铁罚站（上海晚高峰的地铁就没一节车厢是空的），到了旅馆直接摊床上了。\n不得不说，上海夜景真是太美丽啦。\nday2 part 1 朝露晨曦，与君共惜。\n终于到了最喜欢的二次元环节。早上六点钟起床吃完早饭去崩三嘉年华现场排队（嘉年华原本应该八点半开门，结果是提早了半个小时开门）。\n选了运气最好的一张票了，和大伟哥“史诗级会晤”，激动死我了（虽然当时在和排队和希儿拍照没抢到前排的位置），那天大伟哥喊得超大声，现场感觉应该很真诚和动容了。\n游戏里的几个场景复刻得真不错，最喜欢的应该是梦呓花海和虚数之树了，据说梦呓花海的的花原本是塑料花，后来感觉效果不好就都换成了手工折的纸花；虚数之树那边说是有 17 吨的红色沙子。\n当然也有很多coser，像是凯文，姬子老师，呆鹅和丽塔，帕door，爱莉，大鸭鸭，蛇姐和克莱因，黑白希，虚三家等等，还有隔壁客串的三月七，罗刹，还有刻晴（大声）。\n逛的是米哈游千万级漫展，比之前在杭州逛漫展感觉好上太多了。\npart 2 上午逛完展就匆匆赶往苏州与我的队友们汇合，结果去火车站太早了，在候车室等了近两个小时（就吃了一点午饭）。\nCCF中心有一扇门外面的标题是计算的未来，然后里结果面空无一物，就挺尬的，计算没有未来 qaq（论计科乃是赛博搬砖）。\n逛完CCF中心就去附件商场干了顿饭（那时我的早已饥肠辘辘）。\n随手拍了张晚上在旅馆的夜景。\nday3 拜托了，另一个我\n上午打个比赛闹麻了，试图摆个希儿说出“拜托了，另一个我！”。之前以为这个比赛跟网络赛没什么难度，遂不是很重视。\n队友一个很简单的 dp 写了快 3 个小时（具体按他们的话来说是题面看错了），然后最后一个小时压力来到了我这边，半个小时把启发式合并写完，但最后结果还是没调过。赛后另一个队的同学跟我说他们 $n^2$ 过的，我直接？？？？？按理来说 $n^2$ 肯定过不了，启发式合并也是 $n log^2 n$ 。最后得出结论：如果我同学没有说谎，那一定是数据太弱了！！！\n比完赛下午去苏州园林玩了一圈。\n一些路上随便拍的景色。\n湖心亭！\nday4 上午睡大觉，下午去寺庙\n逛了一波苏州园林之一的留园，\n最后去了趟寒山寺感觉很应景。\n","permalink":"https://solarain.net.cn/posts/%E4%B8%8A%E6%B5%B7-%E8%8B%8F%E5%B7%9E%E6%B8%B8%E8%AE%B0/","summary":"这算是人生经历的第一次历时最长的旅程了（真的是顺路打个比赛qaq）。","title":"上海-苏州游记"},{"content":"2021 CCPC桂林 B A Plus B Problem\n2023 ICPC香港 E Goose, Goose, DUCK?\n洛谷 P5490 扫描线\n","permalink":"https://solarain.net.cn/posts/%E7%BA%BF%E6%AE%B5%E6%A0%91%E9%A2%98%E5%8D%95/","summary":"一些用到线段树的题目集合以及部分题解","title":"线段树题单"},{"content":"线段树以及部分区间问题的板子\n自从去年香港站因为没过线段树题而打铁第一次系统地重新认识线段树。\n简介 线段树是一种维护含幺半群的数据结构（含幺半群就是包含幺元以及满足结合律的代数系统），因此线段树可以维护矩阵乘法。线段树上的每一个点都具有相同的性质，可以看作树的叶子是我们最开始所需处理的数据，而每向上一层，数据量减少一半，而所解出的值仍等价于最开始的数据，因此复杂度是$O(log)$。\n能添加懒标记的线段树在线段树的基础上，还需满足下面几个点：懒标记能与线段树所需维护的量进行运算（后面会看到的mapping），懒标记之间能够进行运算（后面会看到的composition）\n一般线段树 如上面所说的，这份板子中的$e$表示幺元，$f$表示求值的二元运算。 同时，这份板子中所维护的线段是$[l, r)$，在使用的时候需要注意。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int INF = 2147483647, mod = 998244353, MAXN = 1e5 + 10; // [l, r) template\u0026lt;typename T, typename F\u0026gt; struct SegmentTree { int n; const F f; // 二元运算 const T e = T(); // 单位元 vector\u0026lt;T\u0026gt; tree; SegmentTree(int n, F f) : n(n), tree(4 \u0026lt;\u0026lt; __lg(n)), f(f) {} SegmentTree(vector\u0026lt;T\u0026gt; a, F f) : SegmentTree(a.size(), f) { function\u0026lt;void(int, int, int)\u0026gt; build = [\u0026amp;](int p, int l, int r) { if(r - l == 1) { tree[p] = a[l]; return; } int m = l + r \u0026gt;\u0026gt; 1; build(p \u0026lt;\u0026lt; 1, l, m), build(p \u0026lt;\u0026lt; 1 | 1, m, r); pull(p); }; build(1, 0, n); }; void pull(int p) { tree[p] = f(tree[p \u0026lt;\u0026lt; 1], tree[p \u0026lt;\u0026lt; 1 | 1]); } void modify(int p, int l, int r, int x, const T\u0026amp; v) { if(r - l == 1) { tree[p] = v; return; } int m = l + r \u0026gt;\u0026gt; 1; if(x \u0026lt; m) modify(p \u0026lt;\u0026lt; 1, l, m, x, v); else modify(p \u0026lt;\u0026lt; 1 | 1, m, r, x, v); pull(p); } T query(int p, int l, int r, int s, int t) { if(l \u0026gt;= t || r \u0026lt;= s) { return T(); } if(l \u0026gt;= s \u0026amp;\u0026amp; r \u0026lt;= t) { return tree[p]; } int m = l + r \u0026gt;\u0026gt; 1; T res = f(query(p \u0026lt;\u0026lt; 1, l, m, s, t), query(p \u0026lt;\u0026lt; 1 | 1, m, r, s, t)); return res; } }; struct node { int v; node() : v(2147483647) {} node(int v) : v(v) {} bool operator\u0026lt;(const node\u0026amp; p) const{ return v \u0026lt; p.v; } }; auto f = [](node a, node b) { return min(a, b); }; void solve() { int n, q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q; SegmentTree\u0026lt;node, decltype(f)\u0026gt; st(n + 1, f); while(q --) { int op, x, y; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; if(op == 0) { st.modify(1, 0, n, x, node(y)); } else { cout \u0026lt;\u0026lt; st.query(1, 0, n, x, y + 1).v \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } signed main () { ios::sync_with_stdio(0), cin.tie(0); solve(); } 以上的代码是用来处理区间最小值的，若需要处理区间和，仅需做如下修改。\n1 2 3 4 5 6 7 8 struct node { int v; node() : v(0) {} node(int v) : v(v) {} }; auto f = [](node a, node b) { return node(a.v + b.v); }; 接下来是线段树的一个变式——维护大于$0$的数据个数。代表应用就是扫描线。\n下面的代码主要改动的部分就是修改，以及空间开大了一倍。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; template\u0026lt;typename T, typename F\u0026gt; struct SegmentTree { int n; const F f; // 二元运算 const T e = T(); // 单位元 vector\u0026lt;T\u0026gt; tree; SegmentTree(int n, F f) : n(n), tree(8 \u0026lt;\u0026lt; __lg(n)), f(f) {} SegmentTree(vector\u0026lt;T\u0026gt; a, F f) : SegmentTree(a.size(), f) { function\u0026lt;void(int, int, int)\u0026gt; build = [\u0026amp;](int p, int l, int r) { if(r - l == 1) { tree[p] = a[l]; return; } int m = l + r \u0026gt;\u0026gt; 1; build(p \u0026lt;\u0026lt; 1, l, m), build(p \u0026lt;\u0026lt; 1 | 1, m, r); pull(p, l, r); }; build(1, 0, n); }; void pull(int p, int l, int r) { tree[p] = f(tree[p], tree[p \u0026lt;\u0026lt; 1], tree[p \u0026lt;\u0026lt; 1 | 1], l, r); } // 每次修改都需要数据pull一次进行数据更新 void modify(int p, int l, int r, int s, int t, const int\u0026amp; v) { if(l \u0026gt;= t || r \u0026lt;= s) { return; } if(l \u0026gt;= s \u0026amp;\u0026amp; r \u0026lt;= t) { tree[p].v += v; pull(p, l, r); return; } int m = l + r \u0026gt;\u0026gt; 1; modify(p \u0026lt;\u0026lt; 1, l, m, s, t, v); modify(p \u0026lt;\u0026lt; 1 | 1, m, r, s, t, v); pull(p, l, r); } T query(int p, int l, int r, int s, int t) { if(l \u0026gt;= t || r \u0026lt;= s) { return T(); } if(l \u0026gt;= s \u0026amp;\u0026amp; r \u0026lt;= t) { return tree[p]; } int m = l + r \u0026gt;\u0026gt; 1; T res = f(query(p \u0026lt;\u0026lt; 1, l, m, s, t), query(p \u0026lt;\u0026lt; 1 | 1, m, r, s, t)); return res; } }; // 其中v是区间上元素的值，len表示区间上大于0的长度。 struct node { int v; int len; node() : v(0), len(0) {} node(int v, int len) : v(v), len(len) {} }; auto f = [](node k, node a, node b, int l, int r) { if(r \u0026gt;= Y.size()) r --; return node(k.v, k.v ? r - l : a.len + b.len); }; 添加懒标记的线段树 一些注记在上面的简介以及下面的注释都写了，就不再过多赘述。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long i64; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 2147483647, mod = 998244353, MAXN = 1e5 + 10; // [l, r) template\u0026lt;typename T, typename F, typename L, typename Map, typename Com\u0026gt; struct Lazy_Segtre { int n; const F f; // 二元运算 const T e = T(); // 单位元 const L e1 = L(); // 懒标记单位元 const Map mapping; // 解懒标记 const Com composition; // 添加(合并)懒标记 vector\u0026lt;T\u0026gt; tree; vector\u0026lt;L\u0026gt; lazy; Lazy_Segtre(int n, F f, Map mapping, Com c) : n(n), tree(4 \u0026lt;\u0026lt; __lg(n)), lazy(4 \u0026lt;\u0026lt; __lg(n)), f(f), mapping(mapping), composition(c) {} Lazy_Segtre(vector\u0026lt;T\u0026gt; a, F f, Map m, Com c) : Lazy_Segtre(a.size(), f, m, c) { function\u0026lt;void(int, int, int)\u0026gt; build = [\u0026amp;](int p, int l, int r) { if(r - l == 1) { tree[p] = a[l]; return; } int m = l + r \u0026gt;\u0026gt; 1; build(p \u0026lt;\u0026lt; 1, l, m), build(p \u0026lt;\u0026lt; 1 | 1, m, r); push_up(p); }; build(1, 0, n); }; void push_up(int p) { tree[p] = f(tree[p \u0026lt;\u0026lt; 1], tree[p \u0026lt;\u0026lt; 1 | 1]); } void push_down(int p, int l, int r) { tree[p] = mapping(tree[p], lazy[p], l, r); if(r - l \u0026gt; 1) { int m = l + r \u0026gt;\u0026gt; 1; lazy[p \u0026lt;\u0026lt; 1] = composition(lazy[p \u0026lt;\u0026lt; 1], lazy[p]); lazy[p \u0026lt;\u0026lt; 1 | 1] = composition(lazy[p \u0026lt;\u0026lt; 1 | 1], lazy[p]); } lazy[p] = e1; } void modify(int p, int l, int r, int s, int t, const L v) { push_down(p, l, r); if(l \u0026gt;= t || r \u0026lt;= s) { return; } if(l \u0026gt;= s \u0026amp;\u0026amp; r \u0026lt;= t) { lazy[p] = v; push_down(p, l, r); return; } int m = l + r \u0026gt;\u0026gt; 1; modify(p \u0026lt;\u0026lt; 1, l, m, s, t, v); modify(p \u0026lt;\u0026lt; 1 | 1, m, r, s, t, v); push_up(p); } T query(int p, int l, int r, int s, int t) { push_down(p, l, r); if(l \u0026gt;= t || r \u0026lt;= s) { return e; } if(l \u0026gt;= s \u0026amp;\u0026amp; r \u0026lt;= t) { return tree[p]; } int m = l + r \u0026gt;\u0026gt; 1; T res = f(query(p \u0026lt;\u0026lt; 1, l, m, s, t), query(p \u0026lt;\u0026lt; 1 | 1, m, r, s, t)); return res; } }; struct node { i64 v; node() : v(0) {} node(i64 v) : v(v) {} node\u0026amp; operator+= (const node\u0026amp; p) { v += p.v; return *this; } }; struct Lazy : node { Lazy() : node() {} Lazy(int v) : node(v) {} }; auto f = [\u0026amp;](node a, node b) { return node(a.v + b.v); }; auto Map = [\u0026amp;](node a, Lazy b, int l, int r) { return node((r - l) * b.v + a.v); }; auto Com = [\u0026amp;](Lazy a, Lazy b) { return Lazy(a.v + b.v); }; void solve() { int n, q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q; Lazy_Segtre\u0026lt;node, decltype(f), Lazy, decltype(Map), decltype(Com)\u0026gt; st(n, f, Map, Com); while(q --) { int op, x, y, z; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; x --, y --; if(op == 0) { cin \u0026gt;\u0026gt; z; st.modify(1, 0, n, x, y + 1, Lazy(z)); } else { cout \u0026lt;\u0026lt; st.query(1, 0, n, x, y + 1).v \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } signed main () { ios::sync_with_stdio(0), cin.tie(0); solve(); } 以上代码是区间加和求区间和，区间修改以及RMQ都是大同小异。\n下面是区间修改，区间求和的所需做的修改。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct node { i64 v; node() : v(0) {} node(i64 v) : v(v) {} }; struct Lazy : node { bool used; Lazy() : node(), used(false) {} Lazy(int v) : node(v), used(true) {} }; auto f = [\u0026amp;](node a, node b) { return node(a.v + b.v); }; auto Map = [\u0026amp;](node a, Lazy b, int l, int r) -\u0026gt; node { return node((b.used ? b.v * (r - l) : a.v)); }; auto Com = [\u0026amp;](Lazy a, Lazy b) { return (b.used ? b : a); }; 网络流细节 网络：\n带权有向图，含有源点s和汇点t 流（flow）：\n容量限制：$f(u, v) \\leq c(u, v)$ 反对称性：$f(u, v) = f(v,u)$ 流量守恒：$\\forall x \\in V - \\{s, t\\}, \\sum_{u, x} f(u, x) = \\sum_{v, x} f(x, v)$，即对于任意一点，流入等于流出 剩余容量：\n$c_f(u, v) = c(u, v) - f(u, v)$ 残量网络：\n有剩余容量构成的网络，包括两部分内容：1. 原图边、2. 原图的反向边。 残留网络记作$G_f$ 增广路：\n残留网络上一条从$s$到$t$的路径。 退流：\n增量一条边的流量的同时要减少等量的反向边的流量。 最小割：\n如果在残量网络上找不到一条增广路，则说明已经求出了最大流/最小割，此时原图被分为$S-T$两个点集合，称为最小割，其中最小割的容量（$S$连向$T$的有向边权和）等于最大流；$S$连向$T$的边集成为最小割边集。 可行边：\n$\\exists$ 一个最小割边集 $E_s$, $e \\subseteq E_s$。简单来说就是一条可行边属于所有最小割边集的并集。 性质： 在某个最小割上满流，这是必要条件。 对于$e(u, v)$，残量网络上不存在另一条从$u$到$v$的路径（简言之就是重边）。 体现在图上就是$u$和$v$不在同一个$scc$中，因为反向边的存在，不满流的边相当于无向边，满流的边相当于没有边。 必须边：\n$\\forall$ 最小割边集 $E_s$, $e \\subseteq E_s$。简单来说就是一条可行边属于所有最小割边集的交集。 性质： 必须边 $\\subseteq$ 可行边 在满足可行边的基础上，源点$s$和$u$属于同一个$scc$，汇点$t$和$v$属于同一个$scc$，直观上看就是从源点$bfs$找到的割边等于从汇点$bfs$找到的割边。 图的几个概念：\n匹配： 边覆集： 独立集： 顶点覆集： 网络流题讲究对着限制找flow，对着代价找 cost。\n","permalink":"https://solarain.net.cn/posts/%E7%BA%BF%E6%AE%B5%E6%A0%91/","summary":"线段树以及部分区间问题的板子，自从去年香港站因为没过线段树题而打铁第一次系统地重新认识线段树。","title":"线段树"},{"content":"一、计算机的抽象和计算 冯诺依曼结构(存储结构计算机) 特点:\n由输入设备, 运算器 (数据通路) , 控制器, 存储器, 输出设备 以运算器为核心 程序以二进制数据的形式进行编码 ( 包括指令和地址码 ) 从计算机中逐条读取指令加以执行 结构框图： 计算机硬件组成 CPU: CU (control unit) 控制单元\nData path 数据通路 ( 运算器 ) :\nALU寄存器(Arithmetic and Logic unit 算术逻辑单元) PC寄存器(Program counter 程序计数器) 用于存放指令的地址 IR寄存器(Instruction Register 指令寄存器) 用于存放当前正在执行的指令 完成一条指令的步骤：PC取指令，IR分析指令，CU执行指令 一般来说PC取完指令后，通常要进行(PC + 1 -\u0026gt; PC)来取出下一条指令\n存储器 内存 ( RAM random access memory ) : MDR ( memory data register 数据寄存器 ), MAR ( memory address register 地址寄存器 )\n外存 ( ROM Read-Only Memory )\n内存大小容量=存储单元个数×存储字长\n即$2^{MAR} \\times MDR$\n存储字长：存储单元的二进制位数 存储字：存储单元的二进制代码组合 I/O设备 计算机硬件通过总线连接, 计算机的物理层抽象, 将门电路封装抽象成了这些计算机硬件\n计算机性能评价 字长的概念 字长 一个字中的二进制的位数\n机器字长 CPU一次能处理数据的位数，与CPU中的寄存器位数有关，通常与主存单元位数一致。\n指令字长 一个指令字中包含二进制的位数\n存储字长 一个存储单元中包含二进制的位数\n衡量计算机性能的基本指标 响应时间 (Response Time) : 执行时间, 等待时间 主频 吞吐量 (Throughout) : 带宽 (Bandwidth) 指令执行速度 (CPI 每执行一条指令所需的时钟周期数, MIPS 每秒执行百万指令数, MFLOPS 每秒执行百万浮点运算数) 功耗 (Power) 制造成本 $$\r一般来说的计算机性能 : 响应时间 (执行时间) 的倒数\r$$ CPU 执行时间 时钟周期 : 固定的频率的硬件时钟, 决定计算机硬件各种事件发生的时间和顺序 时钟频率: 时钟周期数的倒数, 一般计算机的单位都为GHz\n​\t一个程序的CPU时钟周期数 = 程序的指令数×每条指令的时钟周期数 (CPI)\n​\t一个程序的CPU执行时间 = 一个程序的CPU时钟周期数×时钟周期\n​\t总执行时间 = sum (执行时间×频率)\nMIPS (每秒百万指令数)\n$MIPS = \\frac{指令数}{执行时间\\times10^6} = \\frac{时钟频率}{CPI\\times10^6}$\n功耗\n略\n二、系统指令 计算机硬件和软件的接口及界面 两种类型指令系统：CISC，RISC\n设计原则 ：完备性，有效性，规整性，兼容性\n一个较为完善的指令系统应该包括的指令有：数据传送指令，输入输出指令，算术逻辑运算指令，系统控制与程序控制指令\n指令格式 基本指令操作格式：操作码+地址码 PS:操作码只能有一个，地址码可以有0或者多个\n操作码设计：定长操作码，扩展操作码 定长操作码:指令的操作码部分固定长度 扩展操作码:指令的操作码部分长度分成几种固定长度, 地址码位数随操作码位数增加而减少\n寻址方式 扩大访存范围 提高访问数据的灵活性和有效性 支持软件技术的发展:多道程序设计\n1 2 指令寻址(简单):PC增值, 跳转 操作数寻址(复杂):来源-寄存器, 外设端口, 内存, 栈顶;数据结构-位,字节,半字,字,双字,一维表 立即数寻址\n例: MOV AX, 1000H\n将1000H存放到寄存器AX中\n存储器直接寻址\n例: MOV AX, [1000H]\n将存储器中地址为[1000H]单元中存放的数存放到寄存器AX中\n寄存器直接寻址\n例: MOV BX, AX\n将寄存器AX中存放的数存放到寄存器BX中\n存储器间接寻址\n例: MOV AX, @(1000H)\n寄存器间接寻址\n例: MOV AX, [BX]\n偏移寻址\n堆栈寻址\n复合寻址\n数据在计算机中的表示形式 1Byte = 8bit\n字长: CPU中数据通路的宽度\n表示实数:\n符号位 s, 阶码(指数，移码) e, 尾数 f\n阶码表示小数点的浮动，浮点数是对尾数和小数进行分别编码\n移码：将补码加上一个偏置常数，方便进行正数和负数大小的比较（可以直接进行硬件比对）。\n规格化：$1/2 \u003c= |f|\u003c1$\n右规：$|f| \u003e 1$ 右移一位，阶码加一\n左规：$f| \u003c 1 / 2|$ 左移一位，阶码减一\n尾数为原码\n单精度（SP）：$(-1)^s\\times(1 + Significand) \\times 2^{Exponent-127}$\n双精度（DP）：$(-1)^s\\times(1 + Significand) \\times 2^{Exponent-1023}$\n数据存放：\n多字节数据采用大端或小端方式存放。\n大端指的是存放的数据的最高有效位放在低地址单元中；\n小端指存放数据的最高有效位放在高地址单元中。\n三、系统总线 总线分类（按连接部件） 片内总线（芯片内部的总线）\n系统总线（计算机各部件的信息传输线）： 数据总线（双向）， 地址总线（单向），控制总线（有出有入）\n一般通过控制总线的信号： 时钟，复位，总线请求，总线允许，中断请求，中断回答，存储器读，存储器写，I/O读，I/O写，传输确认\n通信总线（用于计算机系统之间或者计算机与其他系统之间的通信）：串行通信总线，并行通信总线 总线特性及性能指标 总线特性： 机械特性：尺寸，形状，管脚数，排列顺序\n电气特性：方向，电平范围\n功能特性：地址，数据，控制\n时间特性：信号的时序关系\n总线的性能指标 总线的传输周期(总线周期) 一次总线操作所需要的时间(包括申请阶段，寻址阶段，传输阶段和结束阶段)，通常由若干个总线时钟周期构成。\n总线时钟周期 即机器的时钟周期。计算机有一个统一的时钟周期，以控制整个计算机的各个部件总线也要受此时钟周期的控制。\n总线的工作频率 总线上各种操作的频率，为总线周期的倒数，实际上指一秒内传送几次数据。\n总线的时钟 即机器的时钟频率，为时钟周期的倒数。实际上指一秒内有多少个时钟周期\n总线宽度 又称总线位数，并行一次传输数据量，它是总线上能同时传输的数据位数，通常是数据总线的根数，如32根称为32位(bit)总线\n总线带宽 可理解为总线的数据传输率，即单位时间内总线上可传输数据的位数，通常用每秒传送信息的字节数来衡量，单位可用字节/秒(B/s)表示\n总线带宽 = 总线工作频率X总线宽度(bit/s) = 总线工作频率X（总线宽度/8）(B/s)\n总线复用 总线复用是指一种信号线在不同的时间传输不同的信息。可以用较少的线传输更多的信息，从而节省空间成本\n信号线数 地址总线，数据总线，控制总线3种总线数和称为信号线数\n时钟同步/异步\n总线控制方式 并发，自动，仲裁，逻辑，计数\n负载能力\n总线标准 PCI，EISA，VESA(LV-BUS)，ISA，AGP，RS-232，USB\n总线标准 数据线 总线时钟 带宽 ISA 16 8 MHz（独立） 33 MBps EISA 32 8 MHz（独立） 33 MBps VESA (VL-BUS) 32 32 MHz（CPU） 133 MBps PCI 32 64 33 MHz（独立） 64 MHz（独立） 132 MBps 528 MBps AGP 32 66.7 MHz（独立） 133 MHz（独立） 266 MBps 533 MBps RS-232 串行通信 总线标准 数据终端设备（计算机）和数据通信设备（调制解调器）之间的标准接口 USB 串行接口 总线标准 普通无屏蔽双绞线 带屏蔽双绞线 最高 12 Mbps (USB1.1) 480 Mbps (USB2.0) 4.8 Gbps (USB3.0) 总线结构 单总线结构 结构：CPU，主存，I/O设备(通过I/O接口)都连接在一组总线，允许I/O设备之间，I/O设备和CPU之间或I/O设备与主存之间直接交换信息。\n优点：结构简单，成本低，易于接入新的设备\n缺点：总线在某一时间只能允许一对部件之间传送数据，限制信息的吞吐量\n(注)单总线并不是指只有一根信号线，系统总线按传送信息的不同可以细分为地址总线，数据总线和控制总线\n双总线结构 结构：双总线结构有两条总线，一条是主存总线，用于CPU，主存和通道之间进行传送，另一条是I/O总线，用于多个外部设备与通道之间进行数据传送。\n优点：将较低速的I/O设备从单总线上分离出来。实现存储器总线和I/O总线分离\n缺点：需要增加硬件等通道设备\n支持突发(猝发)传送，送出一个地址，收到多个地址连续的数据\n通道是具有特殊功能的处理器，能对I/O设备进行统一管理。通道程序存放在主存中\n双总线B\n三总线结构 结构：三总线结构是在计算机系统各部件之间采用3条各自独立的总线来构成信息的通路。这三条总线分别为主存总线，I/O总线和直接内存访问DMA总线 (DMA:Direct Memory Access 直接内存访问)\n优点：提高了I/O设备的性能，使其更快的相应命令，提高系统的吞吐量\n缺点：系统工作效率较低\n四总线结构 总线控制 总线控制： 判优控制（优先级）：集中式（链式查询，计数器定时查询，独立请求方式），分布式\n主设备，从设备\n(16条消息) 计算机组成原理——总线控制（总线判优控制、总线通信控制）_总线判优控制的三种方式_小草dym的博客-CSDN博客\n链式查询方式 注：在查询链中离总线控制器最近的部件具有最高优先权，离总线控制器越远，优先权越低。\n1 2 3 4 5 链式查询通过接口的优先权排队电路实现。 优点：结构简单，主要运用在简单的嵌入式系统中 缺点：速度慢，需要一直向下查询，对电路故障特别敏感，查询链的优先级固定 计数器定时查询方式： 注：计数器的初值可用程序设置，以方便地改变优先次序。当然这种灵活性是以增加控制线数为代价的。\n1 2 3 4 5 优点\t1 计数器的初始值可由程序来设置，因而设备的优先级可以通过设置不同的计数初始值来改变。若每次计数总是从0开始，此时设备的优先次序是固定的；若每次计数的初始值总是上次得到控制权的设备的设备号，那么所有设备的优先级就是相等的，是一种循环优先级方式。 2 它对电路故障也不如菊花链查询那样敏感。 缺点 增加了一组设备线，并且每个设备要对设备线的信号进行译码处理，因而控制也变复杂了。 独立请求方式 1 2 3 优点：响应时间快，对优先次序的控制相当灵活 缺点：控制线数增加了很多 总线通信控制 总线传输周期的四个阶段 1. 申请分配阶段\n由需要使用总线的主设备提出申请，经总线仲裁机构决定将下一代传输周期的总线使用授权给予某一申请着。也可将此阶段细分为传输请求和**总线仲裁(总线判优)**两个阶段。\n2. 寻址阶段\n获得使用权的主设备通过总线发出本次要访问的从设备的地址及有关命令。启动参与本次传输的从设备\n3. 传输阶段\n主设备和从设备进行数据交换，可单向或双向进行数据传送\n4. 结束阶段\n主设备的有关信息均从系统总线上撤除，让出总线使用权\n总线通信的四种方式 同步通信(同步定时方式) 由统一时钟控制数据传送\n优点：规定明确、统一，模块间配合简单 缺点：主从模块时间配合强制“同步”，造成对不同速度的部件而言，必须按最慢速度的部件设计公共时钟。 适用于总线长度短、部件存取时间比较一致的场合。\n异步通信(异步定时方式) 采用应答方式，没有公共时钟标准\n半同步通信 同步异步结合，发送方用系统时钟前沿发信号，接收方用系统时钟后沿判断、识别，增加一个wait信号\n半同步通信集同步与异步通信之优点，既保留了同步通信的基本特点，如所有的地址、命令、数据信号的发出时间，都严格参照系统时钟的某个前沿开始，而接收方都采用系统时钟后沿时刻来进行判断识别。同时又像异步通信那样，允许不同速度的模块和谐地工作。为此增设了一条“等待”响应信号线。\n分离式通信 充分挖掘系统总线每瞬间的潜力\n四、IO设备 显示设备：分辨率，灰度级，刷新率（显存）\nIO接口：\n实现设备的徐泽 实现数据缓冲达到速度匹配 实现数据串-并格式转化 实现电平转化 传送控制命令 反应设备状态 IO总线\n设备选择线 数据线 命令线 （主机 -\u0026gt; 外设） 状态线 （外设 -\u0026gt; 主机） 轮询，中断\n请求 判优 响应 服务 返回\nDMA工作过程\n预处理，数据传输，后处理\n五、存储器 存储器的分类 按存储介质分类 半导体存储器：TTL、MOS 磁表面存储器：磁头，磁载体 磁芯存储器：硬磁材料、环状元件 光盘存储器：激光、磁光材料 按存取方式分类 随机存储器RAM 只读存储器ROM 串行访问存储器\n按在计算机中的作用分类 主存 Flash 高速缓存（Cache） 辅助存储器\n存储器的层次结构 （寄存器，缓存，主存，磁盘，……）自顶向下容量逐渐增大，速度逐渐减慢\n存储器的技术指标 存储容量=$2^{MAR} \\times MDR$ 存储速度（存取时间，存取周期） 存储器的带宽=$\\frac{数据宽度}{存储周期}$\nRAM半导体存储芯片 基本结构 地址线，片选线，数据线，I/O控制线\nSRAM和DRAM SRAM：利用双稳态触发器来保存信息，只要不断电，信息不会丢失，因为其不需要进行动态刷新，故称为“静态”存储器。 DRAM：利用MOS电容存储电荷来保存信息，使用时需要给电容充电才能使信息保持，即要定期刷新。\nDRAM 刷新\n集中刷新 分散刷新 分散刷新与集中刷新相结合 存储器与CPU连接 位扩展（增加存储字长）\n字扩展（增加存储数量）\n存储器的校验 汉明码 第$2^k$位为校验码，$C_k$为检测组，那么组内所有位置与$k$等于$k$，例如$C_1$组包含$1, 3, 5, 7, 9, 11, ...$ 偶校验每组异或和为$0$则为无误，奇校验反之。\n提高访存速度的措施 采用高速器件 采用层次结构 Cache - 主存 调整主存结构 单体多字系统、多体并行系统、双端口存储器\nCache $$\rh = \\frac{C_c}{C_c + C_m}\r$$ 其中$h$表示Cache命中率，$C_c$表示执行某段程序中访问Cache的命中次数，$C_m$b表示访问主次的次数。\n$$\rt = h \\times t_c + (1 - h) \\times t_m\r$$ 其中$t$平均访存时间，$t_c$表示Cache的存取周期，$t_m$表示主存的存取周期\n$$\re = \\frac{t_c}{t}\r$$ 其中$e$表示效率\n$$\r性能提高 = \\frac{t_m}{t}\r$$映射方法 直接映射 高位标记 低位索引 块内地址\n组相联映射 高位标记 低位索引 块内地址\n全相联映射 标记 块内地址\n替换算法 先进先出、近期最少使用、随机法\n辅助存储器 不直接与CPU交换信息 硬盘（磁道，位密度）\n","permalink":"https://solarain.net.cn/posts/%E8%AE%A1%E7%BB%84%E7%AC%94%E8%AE%B0/","summary":"计组用来什么？能不能以吃？可以拯救主板吗？","title":"计组笔记"},{"content":"2023 杭电多校4 1011 Circuit\n题目大意\n给定一张有向图，求出图上的最小环的长度以及个数。\n题解\n在做这题之前先来回忆一下 $Floyd$ 的做法——在最开始的做法中首先定义了一个数组 f[k][x][y] ，表示在子图 $V_k' = \\lbrace 1,2,3,...,k\\rbrace$ 上 $x, y$ 间的最短路。\n计数不重复的关键在于 $k$ 上，在子图 $V_k'$ 上，只记录点 $k$ 此时所在的环，从前往后枚举 $k$ 可以保证计数做到不重不漏，因为点 $k$ 在子图 $V_1',V_2',...,V_{k-1}'$ 上不存在，因此先前的计数与与之后的计数一定不重复，同时子图 $V_k'$ 对于子图 $V_{k - 1}'$ 多了点 $k$，找到点 $k$ 在 $V_k'$ 上环就是新增的环个数，保证不漏。\n找最小环只需要求出 $i, j$ 的最短路 $d_{i,j}$ 以及一条边 $j \\rightarrow i, w$ ，那么这个环的长度就是 $d_{i, j} + w$。\n比赛的时候发现只知道 $Floyd$ 怎么用，不知道实现的原理，故未通过。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; gra(n, vector\u0026lt;int\u0026gt; (n)); vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; dis(n, vector\u0026lt;ll\u0026gt; (n, INF)); vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; cnt(n, vector\u0026lt;ll\u0026gt;(n)); for(int i = 0; i \u0026lt; n; i ++) { dis[i][i] = 0; } for(int i = 0; i \u0026lt; m; i ++) { int a, b, w; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; w; a --, b --; dis[a][b] = w; gra[a][b] = w; cnt[a][b] = 1; } ll mindis = INF, counter = 0; for(int k = 0; k \u0026lt; n; k ++) { for(int i = 0; i \u0026lt; n; i ++) { for(int j = 0; j \u0026lt; n; j ++) { if(dis[i][j] \u0026gt; dis[i][k] + dis[k][j]) { cnt[i][j] = cnt[i][k] * cnt[k][j]; dis[i][j] = dis[i][k] + dis[k][j]; } else if(dis[i][j] == dis[i][k] + dis[k][j]) { cnt[i][j] += cnt[i][k] * cnt[k][j]; } cnt[i][j] %= mod; } } for(int i = 0; i \u0026lt; k; i ++) { if(gra[k][i]) { if(mindis \u0026gt; dis[i][k] + gra[k][i]) { mindis = dis[i][k] + gra[k][i]; counter = cnt[i][k]; } else if(mindis == dis[i][k] + gra[k][i]) { counter += cnt[i][k]; } counter %= mod; } } } if(mindis == INF) { mindis = counter = -1; } cout \u0026lt;\u0026lt; mindis \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; counter \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 洛谷 5304 旅行者\n题目大意\n给定 $n$ 个点 $m$ 条边的有向图，每条边都有对应的权值，这 $n$ 个点中有 $k$ 个点是我们感兴趣的，求 $k$ 个点中每两个点最短路的最小值。\n题解\n此题相当于建立一个源点，源点向这 $k$ 个点连一条权为 $0$ 的边，同时建立一个汇点，这 $k$ 个点向汇点连一条权为 $0$ 的边。 ，当然，需要将这 $k$ 个点拆成两份，一份作为起点，一份作为终点。在形式上等同于 $Dij$ 最开始将所有 $k$ 个点的起点放入堆中，最后求最短路的时候枚举每个点的终点。但如果只是这样，存在一个非常严重的问题，我们最后得到的结果可能不是每两个点的最短路的最小值，而可能是一个环。如果想用上述方式通过该题，可能需要带着 $set$ 跑 $Dij$。这里介绍一下两种简单的实现方面。\n假设这个最短路径存在，那么一定存在一条边有别与其他所有等距离的环，可以考虑跑两次 $Dij$，一次跑正向边，一次跑反向边，可以考虑枚举每条边 $(u \\rightarrow v, w)$，同时 $check$ 下 $u$ 扩展过来的点和 $v$ 扩展过来的点是否是同一个，那么 $res = min\\lbrace dis_1(u) + dis_2(v) + w \\rbrace$。可以保证一定能够找到一个非环的最短路径。 参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 const ll INF = 1e18; void solve() { int n, m, k; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; k; n ++; struct Edge { int from, to, dis; }; vector\u0026lt;Edge\u0026gt; es; vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt; g(n * 2), revg(n * 2); vector\u0026lt;ll\u0026gt; d(n * 2, INF), revd(n * 2, INF); vector\u0026lt;int\u0026gt; org(n * 2, -1), revorg(n * 2, -1); for(int i = 0; i \u0026lt; m; i ++) { int a, b, c; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; c; es.push_back({a, b, c}); g[a].push_back({a, b + n, c}); g[a + n].push_back({a + n, b + n, c}); revg[b].push_back({b, a + n, c}); revg[b + n].push_back({b + n, a + n, c}); } vector\u0026lt;int\u0026gt; a(k); for(int\u0026amp; i : a) { cin \u0026gt;\u0026gt; i; } auto Dij = [\u0026amp;](const vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt;\u0026amp; gra, vector\u0026lt;ll\u0026gt;\u0026amp; dis, vector\u0026lt;int\u0026gt;\u0026amp; orgin) { vector\u0026lt;bool\u0026gt; vis(2 * n); priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, greater\u0026lt;PII\u0026gt;\u0026gt; pq; for(const int\u0026amp; i : a) { dis[i] = 0; orgin[i] = i; pq.push({dis[i], i}); } while(pq.size()) { int u = pq.top().second; pq.pop(); if(vis[u]) continue; vis[u] = true; for(auto \u0026amp;[z, v, d] : gra[u]) { if(dis[v] \u0026gt; dis[u] + d) { dis[v] = dis[u] + d; orgin[v] = orgin[u]; pq.push({dis[v], v}); } } } }; Dij(g, d, org); Dij(revg, revd, revorg); ll res = INF; for(const auto \u0026amp;[u, v, dis] : es) { if(org[u + n] != revorg[v + n]) { res = min(res, d[u + n] + revd[v + n] + dis); } if(org[u] != revorg[v + n]) { res = min(res, d[u] + revd[v + n] + dis); } if(org[u + n] != revorg[v]) { res = min(res, d[u + n] + revd[v] + dis); } if(org[u] != revorg[v]) { res = min(res, d[u] + revd[v] + dis); } } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 考虑将 $k$ 个点成两组分别放在起点和终点，可以证明最多只需要进行 $log\\ k$ 次 $Dij$ 就能够找到最短路的最小值。$x$ 从 $1$ 枚举到 $log\\ k$，若 $x$ 第 $i$ 位上为 $1$ ，则将第 $p_i$ 个点放在起点，反之放在终点。保证，任意两个点至少分别被放到起点和终点一次。 类似汉明码的做法\nCF1818 D Fish Graph\n题目大意\n要求在给定的图上找一个子图，满足“鱼图”的性质，“鱼图”的定义是仅存在一个简单环，并且存在一个环上的特殊点满足连接两条额外的边，另一端点不在环上。\n题解\n发现这个特殊点有很明显的特征，度大于等于$4$且在一个环上。判断一个点是否在环上可以用tarjan的做法来实现，这里给出一份代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 vector\u0026lt;int\u0026gt; dfn(n), low(n); vector\u0026lt;bool\u0026gt; vis(n), in_cir(n); int tim = 0; stack\u0026lt;int\u0026gt; stk; auto tarjan = [\u0026amp;](auto self, int u, int f) -\u0026gt; void { dfn[u] = low[u] = ++ tim; vis[u] = 1; stk.push(u); for(auto \u0026amp;v : e[u]) if(v != f) { if(!dfn[v]) { self(self, v, u); low[u] = min(low[u], low[v]); } else { if(vis[v]) { low[u] = min(low[u], low[v]); } } } if(low[u] == dfn[u]) { int now; vector\u0026lt;int\u0026gt; cand; do { now = stk.top(); cand.push_back(now); stk.pop(); vis[now] = 0; } while(now != u); if(cand.size() \u0026gt;= 2) { for(int i : cand) { in_cir[i] = true; } } } }; for(int i = 0; i \u0026lt; n; i ++) { if(!dfn[i]) { tarjan(tarjan, i, i); } } 当我们找一个特殊点后，发现还有一个比较棘手的问题，我们通过dfs找到的环可能不是最小环，比如说 找到类似红色边所围成的环。这会影响我们最终能否找到答案，因此这个问题是必须要解决的，赛场写的时候正是因为处理不了这一点而没做出来。\n但是本题实际上并不需要找到最小环。可以通过这个特殊点找到两条不在环上的边，如果边的另一端不在环上，那么不需要进行任何操作，如何这个点在环上，那么实际上可以通过这条边来达到缩小环的目的，最多只需要通过缩小两次环就可以达到最终所需要的结果。比如说可以将上图转化成下面这种样子 时间复杂度$O(n + m)$\n参考代码：204409376\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; e(n); for(int i = 0; i \u0026lt; m; i ++) { int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; a --, b --; e[a].push_back(b); e[b].push_back(a); } vector\u0026lt;int\u0026gt; dfn(n), low(n); vector\u0026lt;bool\u0026gt; vis(n), in_cir(n); int tim = 0; stack\u0026lt;int\u0026gt; stk; auto tarjan = [\u0026amp;](auto self, int u, int f) -\u0026gt; void { dfn[u] = low[u] = ++ tim; vis[u] = 1; stk.push(u); for(auto \u0026amp;v : e[u]) if(v != f) { if(!dfn[v]) { // 父子边 self(self, v, u); low[u] = min(low[u], low[v]); // 更新low } else { if(vis[v]) { low[u] = min(low[u], low[v]); } } } if(low[u] == dfn[u]) { int now; vector\u0026lt;int\u0026gt; cand; do { now = stk.top(); cand.push_back(now); stk.pop(); vis[now] = 0; } while(now != u); if(cand.size() \u0026gt;= 2) { for(int i : cand) { in_cir[i] = true; } } } }; for(int i = 0; i \u0026lt; n; i ++) { if(!dfn[i]) { tarjan(tarjan, i, i); } } for(int i = 0; i \u0026lt; n; i ++) { if(e[i].size() \u0026gt;= 4 \u0026amp;\u0026amp; in_cir[i]) { vector\u0026lt;int\u0026gt; path; vector\u0026lt;int\u0026gt; loop; vector\u0026lt;bool\u0026gt; vis(n); auto dfs = [\u0026amp;](auto self, int u, int fa) -\u0026gt; void { vis[u] = true; path.push_back(u); for(int v : e[u]) if(v != fa) { if(v == i) { loop = path; return; } if(!vis[v]) { self(self, v, u); } if(loop.size()) { return; } } path.pop_back(); vis[u] = false; }; dfs(dfs, i, -1); cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; vector\u0026lt;int\u0026gt; ex; for(int v : e[i]) { if(v != loop[1] \u0026amp;\u0026amp; v != loop.back()) { ex.push_back(v); } if(ex.size() == 2) { break; } } set\u0026lt;int\u0026gt; s; for(int v : loop) { s.insert(v); } int temp ; if(s.count(ex[0])) { temp = loop.back(); while(loop.back() != ex[0]) { s.erase(s.find(loop.back())); loop.pop_back(); } ex[0] = temp; } if(s.count(ex[1])) { temp = loop.back(); while(loop.back() != ex[1]) { s.erase(s.find(loop.back())); loop.pop_back(); } ex[1] = temp; } vector\u0026lt;PII\u0026gt; res; for(int i = 0; i \u0026lt; loop.size(); i ++) { res.push_back({loop[i] + 1, loop[(i + 1) % loop.size()] + 1}); } res.push_back({loop[0] + 1, ex[0] + 1}); res.push_back({loop[0] + 1, ex[1] + 1}); cout \u0026lt;\u0026lt; res.size() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto \u0026amp;[a, b] : res) { cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; b \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } return; } } cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } CF1822 F Gardening Friends\n题目大意\n给定一棵有根树，根为$1$，可以通过花费$c$的价值使根改变成当前根的邻居，以$i$为根的树的价值是$树的深度 * k$，求可能的最大价值。\n题解\n发现当且仅有根移动到叶子节点时才有意义。最开始的想法是以为叶子结点的个数为$log\\ n$（误当成满二叉树树算了），然后以每一个叶子结点为根算出树的深度计算答案，发现 TLE7，遇到菊花图就铁寄了。\n于是开始想$O(1)$转移的方法。考虑到答案实际上可以由两条链构成的，那么可以枚举两条链的交点（两个叶子节点的最近祖先）。那么对于任何一个交点，其实只需要考虑三条链：子树的最长链，子树的次长链，通过根到达另一棵子树的最长链，其中第三条链是不需要考虑的，因为如果根结点有多于两个儿子的时候，我们通过根的最长链和次长链（不是同一个儿子的）就将得到了一个下限值，通过第三条链得到的所有值都小于等于这个下限。\n那么对于任何一个交点可以得到$O(1)$的转移方法： $$\rres = max(res, (max1[i] + max2[i] - dep[i] * 2) * k - (max2[i] - 1) * c)\r$$其中$max1[i]$表示$i$结点的最长链的最远端深度，$max2[i]$则表示次长链的最远端深度，$dep[i]$表示$i$结点的深度。\n参考代码：204391563\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void solve() { int n, k, c; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; k \u0026gt;\u0026gt; c; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; g(n); for(int i = 1; i \u0026lt; n; i ++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u --, v --; g[u].push_back(v); g[v].push_back(u); } vector\u0026lt;int\u0026gt; dep(n); vector\u0026lt;int\u0026gt; max1(n, -1), max2(n, -1); auto dfs = [\u0026amp;](auto self, int u, int fa) -\u0026gt; void { dep[u] = dep[fa] + 1; for(int v : g[u]) if(v != fa) { self(self, v, u); int m1 = max1[v]; if(m1 == -1) m1 = dep[v]; if(max1[u] == -1) { max1[u] = m1; } else if(max2[u] == -1 || max2[u] \u0026lt; m1) { max2[u] = m1; if(max2[u] \u0026gt; max1[u]) { swap(max2[u], max1[u]); } } } }; dfs(dfs, 0, 0); i64 res = 1ll * (*max_element(dep.begin(), dep.end()) - 1) * k; for(int i = 0; i \u0026lt; n; i ++) { if(max2[i] != -1 \u0026amp;\u0026amp; max1[i] != -1) res = max(res, 1ll * (max1[i] + max2[i] - dep[i] * 2) * k - 1ll * (max2[i] - 1) * c); } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 2021 ICPC上海 H Life is a Game\n题目大意:\n给定一张$n$个点$m$条边的无向连通图, 每个点拥有点权$a$, 每条边拥有边权$w$. 有$q$次询问, 每次询问给定初始点$x$和初始能力值$k$, 从$x$点出发, 可以通过小于等于当前能力值的边到达另一个点, 每次到达一个未经过的点便获得这个点的点权, 求最大的能力值.\n题解\n对于任意一张图, 所走过的边一定属于该图的最小生成树, 这一点用Kruskal很容易证明, 对于一条$u, v$的边, 如果点$u$和点$v$已经连通, 这条边的边权一定大于等于$u, v$路径上的所有边的边权.\n那么这题的一个朴素想法便是构建该图的最小生成树, 对于每次询问, 从$x$出发, 跑一个优先队列维护的bfs即可, 然而时限显然不够.\n对于这类多次询问的问题的优化, 有两种基本的思路: 其一是离线查询；其二是对数据进行预处理（一般是建立数据结构）, 从而降低每次查询的复杂度. 于是对于最小生成树的预处理的 Kruskal 重构树呼之欲出. 我们让重构树的每个点维护其子树的能力增加量和所需的能力值, 分别记作$add$和$limit$, 从点$x$开始, 一直向上搜索它的父节点, 直到无法向上搜素或者搜索到根节点为止. 但是重构树的高度最坏为$n$, 这意味着每次查询的最坏复杂度仍为$O(n)$!\n我们需要能够更快得找到最大的限制, 考虑一条树链, 从叶子到根路径上的限制只会增大, 并且对每个结点而言, 计算出结点本身的限制是可以$O(1)$做到的, 那么如果仅有一条树链又是区间求$max$, 那么很容易就想到 ST 表倍增来实现(既然ST表能做, 那么树剖线段树应该也可以! 但是可能会多个 log ). 尝试写一个长度为$2$的转移: $bz[i][1] = max(bz[i][0], bz[fa[i][0]][0])$, 似乎不难理解并且可行, 由此得出倍增的运算 $$\rbz[i][j] = max(bz[i][j - 1], bz[fa[i][j - 1]][j - 1])\r$$ 那么至此此题的最终复杂度$O(qlog\\ n)$.\n参考代码：205493766\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 struct EDGE { int u, v, w; bool operator\u0026lt; (const EDGE \u0026amp;a) const { return w \u0026lt; a.w; }; }; struct DSU { int N; vector\u0026lt;int\u0026gt; p; DSU(int n) { N = n; p.resize(n); for(int i = 0; i \u0026lt; n; i ++) { p[i] = i; } } int root(int x) { return p[x] == x ? x : p[x] = root(p[x]); } bool merge(int x, int y) { int X = root(x), Y = root(y); if(X == Y) return 1; p[X] = Y; return 0; } }; struct Node { int f = -1, w = 0; ll add = 0; }; Node nd[MAXN \u0026lt;\u0026lt; 1]; vector\u0026lt;EDGE\u0026gt; e; ll bz[MAXN \u0026lt;\u0026lt; 1][17]; int fa[MAXN \u0026lt;\u0026lt; 1][17]; int n, m, q; void solve () { cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; q; e.resize(m); vector\u0026lt;int\u0026gt; a(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; nd[i].add = a[i]; } for(auto \u0026amp;[u, v, w] : e) { cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u --, v --; } sort(e.begin(), e.end()); DSU dsu(2 * n + 10); for(auto \u0026amp;[u, v, w] : e) { int U = dsu.root(u), V = dsu.root(v); if(U != V) { dsu.merge(U, n), dsu.merge(V, n); nd[U].f = nd[V].f = n; nd[n].add = nd[U].add + nd[V].add; nd[n].w = w; n ++; } } for(int i = 0; i \u0026lt; n; i ++) { fa[i][0] = nd[i].f; bz[i][0] = nd[nd[i].f].w - nd[i].add; } for(int i = 1; i \u0026lt; 17; i ++) { for(int j = 0; j \u0026lt; n; j ++) { if(fa[j][i - 1] == - 1) { fa[j][i] = -1; continue; } fa[j][i] = fa[fa[j][i - 1]][i - 1]; bz[j][i] = max(bz[j][i - 1], bz[fa[j][i - 1]][i - 1]); } } while(q --) { int x, k; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; k; x --; for(int i = 16; i \u0026gt;= 0; i --) { if(fa[x][i] != -1) { if(bz[x][i] \u0026lt;= k) { x = fa[x][i]; } } } ll res = k + nd[x].add; cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 2019 ICPC银川 H Delivery Route\n题目大意\n给定一张图，求所有点对于源点$s$的最短路，图中存在$x$条双向边，$y$条单向边，保证双向边的权一定为正整数，单向边的权可能为负，且若$u\\rightarrow v$ 存在一条单向边，那么$v$一定无法到达$u$。\n题解\n因为存在负权边，所以不能直接使用 Dijkstra，且数据大小不允许用 Bellman-Ford（SPFA 已死）。此时注意到第二类边仅会出现在联通分量之间，且联通分量之间是拓扑有序的，因此可以在联通分量间跑拓扑 dp，在联通分量内部跑 Dij。一般我们在跑 Dij 的时候首先往往确定源点的距离为$0$，其他点的距离为$inf$，但此时除了源点所在的联通分量，其他的联通分量似乎找不到一个合适的源点，并且有一些点的值已经确定了一个初值而不是$inf$，因此，可以假设这些已经有初值的点和源点存在一条边，然后略去第一步的过程，将这些点都加入到优先队列之后再跑 Dij。这算考 Dij 的一个变式。\n参考代码：205419690\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long i64; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, mod = 998244353, MAXN = 1e5 + 10; struct DSU { int n; vector\u0026lt;int\u0026gt; pr; DSU(int n) : n(n) { pr.resize(n); for(int i = 0; i \u0026lt; n; i ++) { pr[i] = i; } } int root(int x) { return pr[x] == x ? x : pr[x] = root(pr[x]); } bool merge(int x, int y) { int X = root(x), Y = root(y); if(X == Y) return false; pr[Y] = X; return true; } }; struct Edge { int from, to, w; }; void solve() { int n, x, y, s; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; s; s --; vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt; e(n); vector\u0026lt;i64\u0026gt; dis(n, 1e17); DSU dsu(n); vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; bel(n); auto Dij = [\u0026amp;](int s) { vector\u0026lt;bool\u0026gt; vis(n); priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, greater\u0026lt;PII\u0026gt;\u0026gt; q; for(int u : bel[s]) { if(dis[u] \u0026lt; 1e9) { q.push({dis[u], u}); } } while(q.size()) { int d = q.top().first; int u = q.top().second; q.pop(); if(vis[u]) continue; vis[u] = true; for(auto \u0026amp;[fr, to, w] : e[u]) { if(dis[to] \u0026gt; dis[u] + w) { dis[to] = dis[u] + w; q.push({dis[to], to}); } } } }; for(int i = 0; i \u0026lt; x; i ++) { int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u --, v --; e[u].push_back({u, v, w}); e[v].push_back({v, u, w}); dsu.merge(u, v); } for(int i = 0; i \u0026lt; n; i ++) { bel[dsu.root(i)].push_back(i); } vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt; e2(n); vector\u0026lt;int\u0026gt; deg(n); for(int i = 0; i \u0026lt; y; i ++) { int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u --, v --; e2[u].push_back({u, v, w}); deg[dsu.root(v)] ++; } queue\u0026lt;int\u0026gt; q; for(int i = 0; i \u0026lt; n; i ++) { if(deg[i] == 0 \u0026amp;\u0026amp; dsu.root(i) == i) { q.push(i); } } dis[s] = 0; while(q.size()) { int com = q.front(); q.pop(); Dij(com); for(int nd : bel[com]) { for(auto \u0026amp;[fr, to, w] : e2[nd]) { if(dis[to] \u0026gt; dis[fr] + w) { dis[to] = dis[fr] + w; // Dij(to); } if(! --deg[dsu.root(to)]) { q.push(dsu.root(to)); } } } } for(int i = 0; i \u0026lt; n; i ++) { if(dis[i] \u0026gt; 1e15) { cout \u0026lt;\u0026lt; \u0026#34;NO PATH\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; dis[i] \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } signed main () { ios::sync_with_stdio(0), cin.tie(0); solve(); } 2022 杭电多校 B Link with Running\n2022 ICPC南京 J Perfect Matching\n题目大意\n给定一张图，存在$n$个点，每个点都有权值$a_i$，如果$|i - j| = |a_i - a_j|$，则$i$和$j$之间存在一条边，构造出图的一个完美匹配，若不能输出No。\n题解\n由$|i - j| = |a_i - a_j|$可知，连边有两种情况： $$\r\\begin{cases}\ra_i + i = a_j + j \\\\\\\\\ra_i - i = a_j - j\r\\end{cases}\r$$显然可以建立一张二分图，将第一种情况视为左侧的点，第二种情况的点视为右侧的点，连边的边权是$i$，即原图中的点的编号。接下来考虑如从二分图中构造出答案，将图划分成长度为$2$的不重合链（点可以相同），如果一个联通分量的边数为偶数，那么一定可以构造出来，首先考虑图为树的情况，我们每次考虑链的中间的点，首先考虑$i$节点未被用过的与儿子节点的边，如果个数为奇数，则要加上自己与父节点的边，而在图上只要找到一个$dfn$树，额外考虑返祖边，视为$i$的未被用过的儿子。\n可以先解决一下这个子问题，参考这题CF858 F Wizard\u0026rsquo;s Tour。\n这题比较直接，给定一张图，问最多能构造出长度为$2$的不相交链。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long i64; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; struct Edge { int form, to; }; vector\u0026lt;vector\u0026lt;Edge\u0026gt;\u0026gt; G(n); for(int i = 0; i \u0026lt; m; i ++) { int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; a --, b --; G[a].push_back({a, b}); G[b].push_back({b, a}); } vector\u0026lt;bool\u0026gt; vis(n); struct nd { int a, b, c; }; vector\u0026lt;nd\u0026gt; res; vector\u0026lt;int\u0026gt; dep(n); auto dfs = [\u0026amp;](auto self, int u, int fa, int d) -\u0026gt; bool { vis[u] = true; dep[u] = d; vector\u0026lt;int\u0026gt; cand; for(auto \u0026amp;[from, to] : G[u]) if(to != fa) { if(dep[to]) { if(dep[to] \u0026lt; dep[u]) { cand.push_back(to); continue; } } else { if(self(self, to, u, d + 1)) { cand.push_back(to); } } } if(u != fa){ cand.push_back(fa); } for(int i = 0; i + 1 \u0026lt; cand.size(); i += 2) { res.push_back({cand[i], u, cand[i + 1]}); } if(cand.size() \u0026amp; 1) return true; else return false; }; for(int i = 0; i \u0026lt; n; i ++) { if(vis[i]) continue; dfs(dfs, i, i, 1); } cout \u0026lt;\u0026lt; res.size() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto [a, b, c] : res) { cout \u0026lt;\u0026lt; a + 1 \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; b + 1 \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; c + 1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } int main() { ios::sync_with_stdio(false); cin.tie(0); int _ = 1; while(_ --) { solve(); } } 这题的代码比上面难点在于建图以及得到反向边的边权，下面的dfs多向下传了顺序的边权解决这个问题，但是下面的代码未进行离散化处理，比标程多了一个$log$。\n参考代码： 206725399\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long i64; typedef pair\u0026lt;i64, int\u0026gt; PII; const int INF = 1e9 + 7, mod = 998244353, MAXN = 1e5 + 10; void solve() { int n; cin \u0026gt;\u0026gt; n; vector\u0026lt;int\u0026gt; a(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } vector\u0026lt;PII\u0026gt; res; map\u0026lt;i64, vector\u0026lt;PII\u0026gt;\u0026gt; G; map\u0026lt;i64, bool\u0026gt; vis; map\u0026lt;i64, int\u0026gt; dep; for(int i = 0; i \u0026lt; n; i ++) { G[a[i] - i - 1].push_back({2ll * INF + a[i] + i + 1, i}); G[a[i] + i + 1 + 2ll * INF].push_back({a[i] - i - 1, i}); } auto dfs = [\u0026amp;](auto self, i64 u, i64 fa, int d, int rev) -\u0026gt; int { vis[u] = true; dep[u] = d; vector\u0026lt;i64\u0026gt; cand; for(auto \u0026amp;[to, id] : G[u]) if(to != fa) { if(dep[to]) { if(dep[to] \u0026lt; dep[u]) { cand.push_back(id); } } else { if(self(self, to, u, d + 1, id) \u0026gt;= 0) { cand.push_back(id); } } } if(fa != u) { cand.push_back(rev); } for(int i = 0; i + 1 \u0026lt; cand.size(); i += 2) { res.push_back({cand[i], cand[i + 1]}); } if(cand.size() % 2 == 0) { return -2; } else { return rev; } }; for(auto \u0026amp;[id, vec] : G) { if(vis[id]) continue; if(dfs(dfs, id, id, 1, -1) != -2) { cout \u0026lt;\u0026lt; \u0026#34;No\\n\u0026#34;; return; } } cout \u0026lt;\u0026lt; \u0026#34;Yes\\n\u0026#34;; for(auto \u0026amp;[x, y] : res) { cout \u0026lt;\u0026lt; x + 1 \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; y + 1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } signed main () { ios::sync_with_stdio(0), cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_--) { solve(); } } ","permalink":"https://solarain.net.cn/posts/%E5%9B%BE%E8%AE%BA%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/","summary":"一些图论相关的题目及其详细题解。","title":"图论做题记录"},{"content":" 单纯形： 几何学上，单纯形或者$n$-单纯形是和三角形类似的$n$维几何体。精确的讲，单纯形是某个$n$维以上的欧几里得空间中的（$n+1$）个仿射无关（也就是没有$m-1$维平面包含$m+1$个点；这样的点集被称为处于一般位置）的点的集合的凸包。 仿射组合: 线性组合的子集, 要求在满足线性组合的条件下, 同时满足系数之和等于$1$. 例如: 一个元素的仿射组合为一个点, 两个元素的仿射组合为一条直线, 三个元素(仿射无关)的仿射组合为一个二维平面 仿射变换: 简言之就是线性变换 + 平移 In Euclidean geometry, an affine transformation or affinity (from the Latin, affinis, \u0026ldquo;connected with\u0026rdquo;) is a geometric transformation that preserves lines and parallelism, but not necessarily Euclidean distances and angles.\nwikipedia\n","permalink":"https://solarain.net.cn/posts/%E4%B8%89%E8%A7%92%E5%89%96%E5%88%86/","summary":"计算几何的一部分，简单介绍三角剖分, 未完待续\u0026hellip;","title":"三角剖分"},{"content":" 问题描述：平面上$n$个点, 要求找到一个半径最小的圆, 能覆盖所有的点. 算法思想: 随机增量法 (理论实践复杂度O(n) ? 但是最坏貌似还是$n^3$) 每次进入下一曾 for的概率都小于$\\frac{3}{n}$. 因此每次随机化点可以尽可能地避免最坏情况的发生.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int n; Points ps(n); random_shuffle(ps.begin(), ps.end()); // 随机化 Circle res(Point(0, 0), 0); for(int i = 0; i \u0026lt; n; i ++) { // 圆是否包含点 i if(CCP(res, ps[i]) == out) { res = Circle(ps[i], 0); for(int j = 0; j \u0026lt; i; j ++) { if(CCP(res, ps[j]) == out) { // 两点确定一个圆 res = Circle((ps[i] + ps[j]) / 2, dis(ps[i], ps[j]) / 2); for(int k = 0; k \u0026lt; j; k ++) { if(CCP(res, ps[k]) == out) { // 三点确定一个圆 res = circumcircle(ps[i], ps[j], ps[k]); } } } } } } ","permalink":"https://solarain.net.cn/posts/%E6%9C%80%E5%B0%8F%E5%9C%86%E8%A6%86%E7%9B%96/","summary":"可以计算出能覆盖平面上多个点的最小圆，基于概率优化的算法，在随机数据的情况下表现良好，复杂度比较玄学，各种写到这个算法的地方复杂度都是含糊其辞或者一笔带过，因此这里也不会深究这个复杂到底是多少。","title":"最小圆覆盖"},{"content":"定义: 对于欧式空间上的两个点集 $A, B$, 存在点集 $C$ 满足 $\\{a + b \\in C|a \\in A,b\\in B \\}$ （$a, b$均为向量的形式）, 则称 $C$ 为$A, B$的闵可夫斯基和, 即 $$\rC = A + B\r$$差 (different): $C = A - B = \\{a - b | a \\in A, b \\in B\\} = A + (-B)$\n表示对$\\forall a \\in A, \\forall b \\in B, \\exists c \\in C (c = a - b)$\n或者可以这样理解$\\forall c \\in C, \\exists b \\in B, \\exists a \\in A (a = c + b)$\n==warning==: 这里的加号和减号不是一对可以互逆的运算, $(A - B) + B \\neq A$\nwikipedia\n例题(第一道过的洛谷黑题): 战争\n题目大意: 有两个部落$A,B$, 两个部落分别有$n, m$个人, 如两个部落所围成的凸包没有重合的点, 则不会发生战争. 现在$B$部落的全部人打算向$(x, y)$向量迁移, 有$q$次询问, 问是否会发生战争.\n思路是找到一个点集$C$, 满足对 $\\forall c \\in C, \\exists b \\in B, c + b \\in A$, 这和前面差的定义是相同的, 故此题转化为了求$C = A - B$, 然后每次询问二分判断点是否在$C$内.\n参考代码 (包含了闵可夫斯基和以及判断一个点是否在凸包内):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; using Real = long long; struct Point { Real x, y; Point() {} Point(Real x, Real y) : x(x), y(y) {} friend istream\u0026amp; operator \u0026gt;\u0026gt; (istream \u0026amp;is, Point\u0026amp; v) { return is \u0026gt;\u0026gt; v.x \u0026gt;\u0026gt; v.y; } Point\u0026amp; operator+= (const Point\u0026amp; p) { x += p.x, y += p.y; return *this; } Point\u0026amp; operator-= (const Point\u0026amp; p) { x -= p.x, y -= p.y; return *this; } Point operator+ (const Point\u0026amp; p) const { return Point(*this) += p; } Point operator- (const Point\u0026amp; p) const { return Point(*this) -= p; } Point operator- () { return Point(-x, -y); } friend Real crs(const Point\u0026amp; a, const Point\u0026amp; b) { return a.x * b.y - a.y * b.x; } bool operator\u0026lt; (const Point\u0026amp; p) const { if(x != p.x) return x \u0026lt; p.x; return y \u0026lt; p.y; } }; using Points = vector\u0026lt;Point\u0026gt;; enum position { CCW = 1, CW = -1, ON = 0 }; int ccw(const Point\u0026amp; a, Point b, Point c) { b -= a, c -= a; if(crs(b, c) \u0026gt; 0) return CCW; if(crs(b, c) \u0026lt; 0) return CW; return ON; } int ccw(Point a, Point b) { return ccw(Point(0, 0), a, b); } Points Convexhell(Points\u0026amp; ps) { int n = ps.size(), k = 0; Points res(2 * n); sort(ps.begin(), ps.end()); for(int i = 0; i \u0026lt; n; res[k ++] = ps[i ++]) { while(k \u0026gt;= 2 \u0026amp;\u0026amp; ccw(res[k - 2], res[k - 1], ps[i]) != CCW) { k --; } } for(int i = n - 2, t = k + 1; i \u0026gt;= 0; res[k ++] = ps[i --]) { while(k \u0026gt;= t \u0026amp;\u0026amp; ccw(res[k - 2], res[k - 1], ps[i]) != CCW) { k --; } } res.resize(k - 1); return res; } Points Minkowski(Points\u0026amp; A, Points\u0026amp; B) { int n = A.size(), m = B.size(); Points res (n + m + 1); res[0] = A[0] + B[0]; int k = 1, i = 0, j = 0; int cA = 0, cB = 0; while(cA \u0026lt; n \u0026amp;\u0026amp; cB \u0026lt; m) { int nxi = (i + 1) % n, nxj = (j + 1) % m; Point x = A[nxi] - A[i]; Point y = B[nxj] - B[j]; if(ccw(x, y) == CCW) { res[k ++] = res[k - 1] + x; i = nxi, cA ++; } else if(ccw(x, y) == CW) { res[k ++] = res[k - 1] + y; j = nxj, cB ++; } else { res[k ++] = res[k - 1] + x + y; j = nxj, i = nxi; cA ++, cB ++; } } while(cA \u0026lt; n) { int nxi = (i + 1) % n; res[k ++] = res[k - 1] + A[nxi] - A[i]; cA ++, i = nxi; } while(cB \u0026lt; m) { int nxj = (j + 1) % m; res[k ++] = res[k - 1] + B[nxj] - B[j]; cB ++, j = nxj; } res.resize(k - 1); return res; } bool check_in(Point\u0026amp; p, Points\u0026amp; ps) { int n = ps.size(); int O = 0, l = 1, r = n - 1; if(ccw(ps[O], ps[r], p) == CCW || ccw(ps[O], ps[l], p) == CW) { return false; } while(l + 1 \u0026lt; r) { int m = l + r \u0026gt;\u0026gt; 1; if(ccw(ps[O], ps[m], p) == CCW) { l = m; } else { r = m; } } if(ccw(ps[l], ps[r], p) == CW) { return false; } else { return true; } } void solve() { int n, m, q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; q; Points A(n), B(m); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; A[i]; } for(int i = 0; i \u0026lt; m; i ++) { cin \u0026gt;\u0026gt; B[i]; B[i] = -B[i]; } A = Convexhell(A); n = A.size(); B = Convexhell(B); m = B.size(); Points C = Minkowski(A, B); while(q --) { Point query; cin \u0026gt;\u0026gt; query; if(check_in(query, C)) { cout \u0026lt;\u0026lt; \u0026#34;1\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;0\\n\u0026#34;; } } } int main() { ios::sync_with_stdio(false), cin.tie(0); int _ = 1; while(_ --) solve(); } ","permalink":"https://solarain.net.cn/posts/%E9%97%B5%E5%8F%AF%E5%A4%AB%E6%96%AF%E5%9F%BA%E5%92%8C/","summary":"计算几何进阶! 点集的加法! 凸包合并!","title":"闵可夫斯基和"},{"content":"2023 杭电多校 7 1008 HEX-A-GONE Trails\n题目大意\n给定一棵无根树，两个玩家$(X, Y)$在上面移动，双方只能移动到与自己相邻且没有被任何玩家走过的节点，无法移动的玩家输掉游戏。\n题解\n首先需要对这个博弈模型进行一些化简，首先我们将 $X, Y$ 之间的路径标记出来，并求出这些路径上的点的价值，这里所谓的价值是以该点为根，不经过 $X, Y$ 路径上的边能够达的点的深度的最大值。例如下面这图能够如此化简：\n现在用两个指针表示出两个玩家所在的位置，那么可以计算出两个玩家所获得的值（值为所在位置的值加上与初始点的距离的和），以及胜负关系（玩家一获得的值严格大于玩家而获得的值为胜）。\n可以用当前局面的胜负关系来判断两个指针的移动方向，例如在上图的状态中，玩家一获得的值显然小于玩家二的值，那么如果玩家一想要胜利，那么 $i$ 指针就必须向靠近 $x$ 初始点的方向移动，那么玩家二的选择实际上就成了一个区间。\n不断重复上述过程，直到两个指针移出被标记的点。\n因为对于一个玩家来说，指针到达过的地方不会再次经过，因此可以用链表来维护 $i, j$ 两个指针的前驱和后继，区间的最值可以直接通过前缀得到，不需要数据结构。\n时间复杂度 $O(n)$。\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 void solve() { int n; cin \u0026gt;\u0026gt; n; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; tre(n); vector\u0026lt;int\u0026gt; path, col(n); int x, y; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; x --, y --; for(int i = 0; i \u0026lt; n - 1; i ++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u --, v --; tre[u].push_back(v); tre[v].push_back(u); } auto dfs = [\u0026amp;](auto self, int u, int f) -\u0026gt; bool { if(u == y) { path.push_back(u), col[u] = true; return true; } for(int v : tre[u]) if(v != f) { if(self(self, v, u)) { path.push_back(u), col[u] = true; return true; } } return false; }; dfs(dfs, x, x); reverse(path.begin(), path.end()); int w = path.size(); vector\u0026lt;int\u0026gt; val(n), dep(n); auto get_v = [\u0026amp;](auto self, int u, int f) -\u0026gt; int { dep[u] = dep[f] + 1; int res = 0; for(auto v : tre[u]) if(v != f \u0026amp;\u0026amp; !col[v]) { res = max(res, self(self, v, u)); } res ++; return res; }; for(int i : path) { val[i] = get_v(get_v, i, i); } int mxx = 0, mxy = 0, curx = 0, cury = w - 1; for(;curx + 1 \u0026lt; cury;) { if(curx + 1 \u0026lt; cury) curx ++; if(cury - 1 \u0026gt; curx) cury --; } mxx = val[path[curx]] + curx; mxy = val[path[cury]] + w - 1 - cury; bool res = mxx \u0026gt; mxy; vector\u0026lt;int\u0026gt; nxtA(w, -1), preA(w, -1), nxtB(w, -1), preB(w, -1); for(int i = 0; i \u0026lt; w - 1; i ++) { nxtA[i] = i + 1; preB[i] = i + 1; } for(int i = w - 1; i \u0026gt;= 1; i --) { preA[i] = i - 1; nxtB[i] = i - 1; } auto del =[\u0026amp;] () { if(nxtA[curx] != - 1) preA[nxtA[curx]] = preA[curx]; if(preA[curx] != - 1) nxtA[preA[curx]] = nxtA[curx]; if(nxtB[cury] != - 1) preB[nxtB[cury]] = preB[cury]; if(preB[cury] != - 1) nxtB[preB[cury]] = nxtB[cury]; }; while(cury != -1 \u0026amp;\u0026amp; curx != -1) { if(res) { del(); curx = nxtA[curx]; cury = preB[cury]; if(cury == -1) { break; } mxx = max(mxx, val[path[curx]] + curx); mxy = val[path[cury]] + w - 1 - cury; } else { del(); cury = nxtB[cury]; curx = preA[curx]; if(curx == -1) { break; } mxx = val[path[curx]] + curx; mxy = max(mxy, val[path[cury]] + w - 1 - cury); } res = mxx \u0026gt; mxy; } cout \u0026lt;\u0026lt; (res ? 1 : 0) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 2023 牛客多校 4 H Merge the squares!\n题目大意\n给定一个 $n \\times n$ 个 $1 \\times 1$ 的小正方形组成的一个大正方形。\n要求每次选取不超过 $50$ 个正方形拼接成一个新大正方形，最终需要得到一个 $n \\times n$ 的正方形。\n输出一种可行的方案。\n题解\n考虑分治的做法。\n对于一个正方形，可以分割成如下图的形式：\n其中 $A, D$ 是两个正方形，$B, C$ 是两个大小相等的矩形。\n显然对于正方形 $A, D$ 可以直接递归处理，处理比较棘手的是两个矩形该如何处理。\n对于一个矩阵而言，分割成最少的正方形的方案是对其长宽进行辗转相减，那么可以预处理出对于任意一个矩阵能够分成的正方形数量。\n我们可以枚举 $B, C$ 的长宽，找到分割最少的矩阵，分治这个矩阵即可。\n当然如果确定了矩形的长宽，这四个矩形可以任意摆放，例如另一种等价的摆放方式：\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 int g[MAXN][MAXN]; vector\u0026lt;tuple\u0026lt;int, int, int\u0026gt;\u0026gt; res; int dp(int x, int y) { if(x == y) { return g[x][y] = 1; } if(g[x][y]) return g[x][y]; if(x \u0026gt; y) { return g[x][y] = 1 + dp(x - y, y); } else { return g[x][y] = 1 + dp(x, y - x); } } void div2(int x1, int y1, int x2, int y2); void div1(int x1, int y1, int x2, int y2) { int len = x2 - x1 + 1; if(len \u0026lt;= 7) { if(len \u0026gt; 1) { res.push_back({x1, y1, len}); } return; } int div = 0, mxcnt = INF; for(int i = 1; i \u0026lt;= len / 2; i ++) { int x = dp(i, len - i); if(x \u0026lt; mxcnt) { mxcnt = x, div = i; } } len = div; div1(x1, y1, x1 + len - 1, y1 + len - 1); div1(x1 + len, y1 + len, x2, y2); div2(x1 + len, y1, x2, y1 + len - 1); div2(x1, y1 + len, x1 + len - 1, y2); res.push_back({x1, y1, x2 - x1 + 1}); } void div2(int x1, int y1, int x2, int y2) { int lenx = x2 - x1 + 1, leny = y2 - y1 + 1; if(lenx == 1 || leny == 1) { return; } if(lenx == leny) { div1(x1, y1, x2, y2); return; } if(lenx \u0026gt; leny) { div1(x1, y1, x1 + leny - 1, y1 + leny - 1); div2(x1 + leny, y1, x2, y2); } else { div1(x1, y1, x1 + lenx - 1, y1 + lenx - 1); div2(x1, y1 + lenx, x2, y2); } } void solve() { int n; cin \u0026gt;\u0026gt; n; div1(1, 1, n, n); cout \u0026lt;\u0026lt; res.size() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto [x, y, len] : res) { cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; y \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; len \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } Harmony in Harmony\n题目大意: $n$个人将大小为$1$的地均分两次, 第一次每人分得$1/n$, 第二次同样分为大小相同的$n$份, $n$人自行分配, 要求使得第一次和第二次区域的交集最小的人的值最大, 第二次均分地时有一种分法, 使得这个值最小, 求出这个值.\n首先可以猜测此题的答案存在一个上界$\\frac{1}{n^2}$. 可以将此过程看作一个完全二分图匹配, 我们可以列出一个$n\\times n$的表格, 其中每行的值之和为$\\frac{1}{n}$, 每列的值之和为$1$, 所求的值为表格中坐标为 $(i, i)$ 的最小值. 我们假定 $(i, i)$ 为我们要找的最小值, 那么为了让这个值最小, 可以让 $\\sum_{k = 1}^{i - 1}(k, i)$ 最大, 又因为要让每人分得的土地最大, 可以让前$i-1$个人分得的土地一样大都为$\\frac{1}{in}$, 那么$i$分得的土地大小应为$\\frac{\\frac{1}{n}-\\frac{i-1}{in}}{n-i+1}$, 验证此值小于等于$\\frac{1}{n^2}$, 故枚举 $i$ 求出答案.\n参考代码:\n1 2 3 4 5 6 7 8 9 int main () { int n; scanf(\u0026#34;%d\u0026#34;, \u0026amp;n); double res = 1; for(int i = 0; i \u0026lt;= n; i ++) { res = min(res, 1.0 / (n * (n + 1 - i) * i)); } printf(\u0026#34;%.9f\u0026#34;, res); } Paimon Sorting\n题目大意: 给出一种$n^2$的排序方式, 询问数组长度为$k$时, 在排序的过程中交换了几次$k\\in [1, n]$. 假设我们已经知道长度为$x$的结果, 那么怎么样可以推出$x + 1$的结果就是此题的关键. 设已经出现的最大值为$mx$, 考虑下面几种情况: $a_x \u003c mx$; $a_x = mx$; $a_x \u003e mx$. 首先考虑第一种情况, 注意到该排序每轮是将最大值交换到$i$位置, 因此对于这种情况只需要考虑最后一轮, 从$1$到$x$位置,每次遇到大于$a_x$的数便交换, 实际交换的次数为大于$a_x$的数的集合的大小. 其次考虑第三种情况, 首先$a_x$会走所有前一个最大值所有经过的路径, 此过程的价值为$0$, 其次$a_x$ 会被移动到$1$位置, 再被移动到$x$位置, 此段的价值为 $2$. 最后考虑第二中情况, 显然它本身的价值应该为$0$, 但是它会对下一个 mx 移动到末位置产生阻碍(增加那一段的价值).这段增量为此时的位置到下一个mx位置的长度.\n参考代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 struct BIT { int N; vector\u0026lt;int\u0026gt; c; BIT(int n) { N = n; c.resize(N); }; void add(int x) { for(int i = x; i \u0026lt; N; i += (i \u0026amp; (-i))) { c[i] += 1; } } ll sum(int x) { ll res = 0; for(int i = x; i; i -= (i \u0026amp; (-i))) { res += c[i]; } return res; } ll sum(int l, int r) { return sum(r) - sum(l - 1); } }; void solve() { int n; cin \u0026gt;\u0026gt; n; vector\u0026lt;int\u0026gt; a(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } ll res = 0; BIT bit(n + 10); vector\u0026lt;int\u0026gt; tol(n + 1); int len = -2; int mx = 0; for(int i = 0; i \u0026lt; n; i ++) { if(mx \u0026lt; a[i]) { res += 2 + len; mx = a[i]; len = 0; } else if(mx == a[i]) { len ++; } else { if(len) len ++; } res += bit.sum(a[i] + 1, n + 5); if(!tol[a[i]]) { tol[a[i]] = 1; bit.add(a[i]); } cout \u0026lt;\u0026lt; res; if(i == n - 1) { cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } else { cout \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } } } Exercise\n题目大意: 给定$2n$个数, 原本(1, 2), (3, 4) \u0026hellip; 是一组, 现在要重新分组, 但不能维持原来的组, 问重新分组后每组两个值差的绝对值和的最小值.\n显然从小到大排序之后, 相邻的两个数分为一个组会使得绝对值和最小, 但不一定符合条件, 于是我们希望能通过某种方法使得所有不合法的组都合法. 发现需要被最优的重组方式一定是与相邻的组进行重组, 且增量为$2(p_{i + 1,l} - p_{i,r})$. 可以考虑一种这样的DP, $dp_{i,j}$, 其中$i$表示考虑到第$i$组, $j$表示该组是否需要被重组. 可以写出转移方程: $$\r\\begin{matrix}\rdp_{i, 1} = dp_{i-1, 0}\\\\\rdp_{i, 0} = min(dp_{i-1,0},\\ dp_{i,0},\\ dp_{i,1} + 2(p_{i + 1,l} - p_{i,r}))\r\\end{matrix}\r$$参考代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const ll INF = (1ll \u0026lt;\u0026lt; 60); struct PAIR { ll l, r; bool bad = 0; }; void solve() { int n; cin \u0026gt;\u0026gt; n; vector\u0026lt;PII\u0026gt; a(2 * n); for(int i = 0; i \u0026lt; 2 * n; i ++) { cin \u0026gt;\u0026gt; a[i].first; a[i].second = i + 1; } sort(a.begin(), a.end()); ll res = 0; vector\u0026lt;PAIR\u0026gt; p(n); for(int i = 0; i \u0026lt; n; i ++) { p[i].l = a[2 * i].first; p[i].r = a[2 * i + 1].first; res += p[i].r - p[i].l; int mx = max(a[2 * i].second, a[2 * i + 1].second); int mi = min(a[2 * i].second, a[2 * i + 1].second); if(mx - 1 == mi \u0026amp;\u0026amp; (mx % 2 == 0)) { p[i].bad = 1; } } vector\u0026lt;array\u0026lt;ll, 2\u0026gt;\u0026gt; dp(n); dp[0][0] = dp[0][1] = INF; dp[0][p[0].bad] = 0; for(int i = 1; i \u0026lt; n; i ++) { dp[i][0] = dp[i][1] = INF; dp[i][p[i].bad] = dp[i - 1][0]; dp[i][0] = min(dp[i][0], min(dp[i - 1][0], dp[i - 1][1]) + 2 * (p[i].l - p[i - 1].r)); } res += dp[n - 1][0]; cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } Fortune over Sportsmanship\n题目大意: $n$人参见一个网球比赛, 一个$n\\times n$的方格表示$(i, j)$进行比赛时收获的人气, 比赛总共进行$n - 1$轮, 胜方获得败方的人气(二者取max), 且败方淘汰出局.现在希望求出比赛可能收获的人气最大值, 并找出赛程.\n考虑一种贪心的过程, 每次使当前状态下人气最大的两名选手进行比赛, 因为会继承败方的人气, 所以无论哪名选手淘汰都没关系. 可以用优先队列来模拟比赛.\n参考代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 struct Z { int val; int x, y; bool operator\u0026lt; (const Z \u0026amp;a) const { if(val != a.val) { return val \u0026lt; a.val; } else { return x \u0026lt; y; } } }; void solve() { int n; cin \u0026gt;\u0026gt; n; ll res = 0; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; p(n, vector\u0026lt;int\u0026gt; (n)); vector\u0026lt;bool\u0026gt; over(n); vector\u0026lt;PII\u0026gt; op; for(int i = 0; i \u0026lt; n; i ++) { for(int j = 0; j \u0026lt; n; j ++) { cin \u0026gt;\u0026gt; p[i][j]; } } priority_queue\u0026lt;Z\u0026gt; q; for(int i = 0; i \u0026lt; n; i ++) { for(int j = i + 1; j \u0026lt; n; j ++) { q.push({p[i][j], i, j}); } } while(q.size()) { int v = q.top().val; int x = q.top().x; int y = q.top().y; q.pop(); if(!over[x] \u0026amp;\u0026amp; !over[y]) { op.push_back({x, y}); res += v; over[y] = 1; for(int i = 0; i \u0026lt; n; i ++) { if(over[i] || i == x) continue; p[i][x] = p[x][i] = max(p[x][i], p[y][i]); q.push({p[x][i], min(x, i), max(x ,i)}); } } } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto \u0026amp;[x, y] : op) { cout \u0026lt;\u0026lt; x + 1 \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; y + 1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } Gaining Rating\n题目大意: Monocarp 想提高自己在某下棋网站上的rating, 希望从$x$上升到$y$. 现在有$n$个对手, 每个对手有相应的rating $a_i$, rating 变化的规则为, 获胜加$1$, 失败减$1$但是对手的rating不变, Monocarp 只能下赢rating不大于自己的对手, 同时他可以自己挑选对手, 但是任意两个对手进行对弈的次数差不能大于$1$. 问需要多少轮才能达到目标或者说是不可能的.\n显然, 首先需要对所有对手的rating进行一次排序. 我们考虑模拟一轮获得增量, 首先能够赢过$t$个人, 再败给$n-t$个人, 一轮总共获得的分数为$2t-n$, 如果距离所需达成的最终目标小于等于$t$时, 我们便可以不进入下一轮, 反之, 如果这一轮获得的分数小于等于$0$时, 无论如何都无法达到目标. 我们考虑如何求出$t$, 显然直接对$a$进行upper_bound是错误的, 因为我们的分数是在变化的. 可以考虑维护一个$b_i = a_i - i$表示想一路赢到$i$所需的最小rating. 但是这样的$b$不是单调的, 我们将不单调的$b_i$向下合并, 并增设权值, 这样我们得到了一个可以$log$求出$t$的数组.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 sort(a.begin(), a.end()); vector\u0026lt;ll\u0026gt; b = a; for(int i = 0; i \u0026lt; n; i ++) { b[i] -= i; } vector\u0026lt;int\u0026gt; s(1, 1); vector\u0026lt;ll\u0026gt; v(1, b[0]); for(int i = 1; i \u0026lt; n; i ++) { if(b[i] \u0026lt;= v.back()) { s.back() ++; } else { v.push_back(b[i]); s.push_back(1); } } v.push_back(1e18); for(int i = 1; i \u0026lt; s.size(); i ++) { s[i] += s[i - 1]; } 上述代码中$v$表示所说的单调数组, $s$表示增量, 接下来我们还不能一下子求出最后的答案, 我们将$v$中数值的大小视为分段的分界线, 已经知道了当前一轮所获得的增量, 我们要判断增量发生变化的值与$y-s_i$哪一个先到, 依次模拟每个分段便能求出最后的答案.\n参考代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 void solve() { int n; ll x, y; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; vector\u0026lt;ll\u0026gt; a(n); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } sort(a.begin(), a.end()); vector\u0026lt;ll\u0026gt; b = a; for(int i = 0; i \u0026lt; n; i ++) { b[i] -= i; } vector\u0026lt;int\u0026gt; s(1, 1); vector\u0026lt;ll\u0026gt; v(1, b[0]); for(int i = 1; i \u0026lt; n; i ++) { if(b[i] \u0026lt;= v.back()) { s.back() ++; } else { v.push_back(b[i]); s.push_back(1); } } v.push_back(1e18); for(int i = 1; i \u0026lt; s.size(); i ++) { s[i] += s[i - 1]; } for(int i = 1; i \u0026lt; s.size(); i ++) { s[i] += s[i - 1]; } ll res = 0; while(1) { int i = upper_bound(v.begin(), v.end(), x) - v.begin(); if(!i) { cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return ; } if(x + s[i - 1] \u0026gt;= y) { cout \u0026lt;\u0026lt; res + y - x \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return ; } else { int add = 2 * s[i - 1] - n; if(add \u0026lt;= 0) { cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return; } else { ll ter = min(y - s[i - 1], v[i]); ll cnt = (teg - x + add - 1) / add; res += cnt * n; x += cnt * add; } } } } https://codeforces.com/contest/1754/problem/F https://codeforces.com/contest/822/problem/C https://codeforces.com/contest/229/problem/D https://codeforces.com/contest/1288/problem/D https://codeforces.com/gym/104114/problem/G https://codeforces.com/gym/103446/problem/J https://codeforces.com/gym/103470/problem/I\nhttps://codeforces.com/gym/103447/problem/G https://codeforces.com/gym/104023/problem/J https://codeforces.com/gym/104023/problem/D\n","permalink":"https://solarain.net.cn/posts/%E9%AB%98%E8%B4%A8%E9%87%8F%E9%A2%98%E8%A7%A3%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0%E4%B8%AD/","summary":"遇到一些有趣的题目又懒得分类或者没有时间就都到了这里\u0026hellip;","title":"高质量题解（持续更新中）"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 int root, tot; const int MAXN = 1e5 + 10; struct node { int ch[2], // 儿子 f, // 父亲 siz, // 子树大小 val, // 值 cnt; // 个数 } nd[MAXN]; void update(int x) { nd[x].siz = nd[nd[x].ch[0]].siz + nd[nd[x].ch[1]].siz + nd[x].cnt; } void rotate(int x) { int y = nd[x].f; int z = nd[y].f; int k = nd[y].ch[1] == x; nd[z].ch[nd[z].ch[1] == y] = x; nd[x].f = z; nd[y].ch[k] = nd[x].ch[k ^ 1]; nd[nd[x].ch[k ^ 1]].f = y; nd[x].ch[k ^ 1] = y; nd[y].f = x; update(y), update(x); } void splay(int x, int goal) { while(nd[x].f != goal) { int y = nd[x].f; int z = nd[y].f; if(z != goal) { (nd[z].ch[1] == y) == (nd[y].ch[1] == x) ? rotate(y) : rotate(x); } rotate(x); } if(goal == 0) { root = x; } } void find(int x) { int u = root; if(!u) { return; } while(nd[u].ch[x \u0026gt; nd[u].val] \u0026amp;\u0026amp; x != nd[u].val) { u = nd[u].ch[x \u0026gt; nd[u].val]; } splay(u, 0); } void insert(int x) { int u = root, f = 0; while(u \u0026amp;\u0026amp; nd[u].val != x) { f = u; u = nd[u].ch[x \u0026gt; nd[u].val]; } if(u) { nd[u].cnt ++; } else { u = ++ tot; if(f) { nd[f].ch[x \u0026gt; nd[f].val] = u; } nd[u].ch[0] = nd[u].ch[1] = 0; nd[u].f = f; nd[u].val = x; nd[u].cnt = 1; nd[u].siz = 1; } splay(u, 0); } int Next(int x) { find(x); int u = root; if(nd[u].val \u0026gt; x) return u; u = nd[u].ch[1]; while(nd[u].ch[0]) u = nd[u].ch[0]; return u; } int Pre(int x) { find(x); int u = root; if(nd[u].val \u0026lt; x) return u; u = nd[u].ch[0]; while(nd[u].ch[1]) u = nd[u].ch[1]; return u; } void del(int x) { int pre = Pre(x), next = Next(x); splay(pre, 0), splay(next, pre); int del = nd[next].ch[0]; if(nd[del].cnt \u0026gt; 1) { nd[del].cnt --; splay(del, 0); } else { nd[next].ch[0] = 0; } } int kth(int k) { int u = root; if(nd[u].siz \u0026lt; k) { return 0; } while(1) { int y = nd[u].ch[0]; if(k \u0026gt; nd[y].siz + nd[u].cnt) { k -= nd[y].siz + nd[u].cnt; u = nd[u].ch[1]; } else { if(k \u0026lt;= nd[y].siz) { u = y; } else { return nd[u].val; } } } } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int main() { ios::sync_with_stdio(0), cin.tie(0); int q; cin \u0026gt;\u0026gt; q; insert(1e9), insert(-1e9); while(q --) { int op, x; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; x; switch (op) { case 1: insert(x); break; case 2: del(x); break; case 3: find(x); cout \u0026lt;\u0026lt; nd[nd[root].ch[0]].siz \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; break; case 4: cout \u0026lt;\u0026lt; kth(x + 1) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; break; case 5: cout \u0026lt;\u0026lt; nd[Pre(x)].val \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; break; case 6: cout \u0026lt;\u0026lt; nd[Next(x)].val \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; break; } } } ","permalink":"https://solarain.net.cn/posts/splay/","summary":"平衡树的一种实现，是 Link Cut Tree 的基础。","title":"Splay"},{"content":"入门 环境配置 安装 Linux 1 $ curl --proto \u0026#39;=https\u0026#39; --tlsv1.3 https://sh.rustup.rs -sSf | sh windows 前往 https://www.rust-lang.org/install.html 并按照说明安装 Rust 检查 1 $ rustc --version 更新 1 $ rustup update 卸载 1 $ rustup self uninstall 本地文档 1 rustup doc 编译命令与Cargo rustc 和gcc差不多的用法\n1 $ rust [-g] {filename.rs} [-o] Cargo 创建一个项目 1 $ cargo new {dirname} 构建 1 $ cargo build [--release] 只确保通过编译\n1 cargo check 运行 1 $ cargo run 变量 rust中变量默认设置为不可变, 只能被覆盖, 需要加mut前缀设置为可变. const修饰常量, 常量可以是个算式\n整型 长度 有符号 无符号 8-bit i8 u8 16-bit i16 u16 32-bit i32\tu32 64-bit i64 u64 128-bit i128\tu128 arch isize usize 向$0$取整\n浮点型 f32, f64\n布尔型 bool: enum { true, false }\n字符类型 UTF-8编码(2-bit) char\n元组 (i32, i32, i32) .访问\n数组 [i32; 10] $10$个i32变量\n枚举类型 1 2 3 4 enum IpAddr{ V4: (u8, u8, u8, u8), V6: String, } 枚举类型中的值是可选项，用::来构造一个实例\n1 let home = IpAddr::V4(192, 0, 0, 10); 特殊的枚举类型Option 这是一个定义在标准库内的类型 1 2 3 4 enum Option\u0026lt;T\u0026gt; { None, Some(T), } 函数 无返回值 (void) 的函数\n1 2 3 fn function() { // -- skip -- } 带返回值的函数\n1 2 3 4 5 fn function() -\u0026gt; i32 { // 两中返回值的方式都是可以的 5 //return 5; } 控制流 if 兼容 C 风格, 可以带()\n1 2 3 4 5 6 7 if x \u0026lt; 5 { x += 1; } else if x \u0026lt; 10 { x += 2; } else { x += 3; } loop 无限循环体\n1 2 3 4 5 6 loop { x += 1; if x \u0026gt;= 100 { break; } } while 兼容 C 风格, 可以带()\n1 2 3 while x \u0026lt; 5 { x += 1; } for 遍历集合\n1 2 3 4 let a = [0, 1, 2, 3, 4, 5]; for i in a { print!(\u0026#34;{}\u0026#34;, i); } 逆序遍历索引\n1 2 3 for i in (0..5).rev() { // --skip-- } match 类似 C 语言中的switch\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from(\u0026#34;192, 0, 0, 1\u0026#34;)); let ip = match home { IpAddr::V4(s) =\u0026gt; { s }, IpAddr::V6(s) =\u0026gt; s // =\u0026gt; 表示产生的效果 other =\u0026gt; String::from(\u0026#34;0, 0, 0, 0\u0026#34;), //match 是穷尽的 }; if let 弱化版的match\n1 2 3 4 5 6 if let IpAddr::V4(s) = home{ println!(\u0026#34;{}\u0026#34;, s); } else { cnt += 1; } println!(\u0026#34;{}\u0026#34;, cnt); 结构体定义与实例 贴个自己写的 BIT 基本就会用了\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 fn main () { let mut bit = BIT::new(100); for i in (1..bit.N) { bit.add(i, 1); } for i in (1..bit.N) { println!(\u0026#34;{}\u0026#34;, bit.get(i)); } } struct BIT { N: usize, p: Vec\u0026lt;i32\u0026gt;, } impl BIT { fn new(n: usize) -\u0026gt; Self { BIT { N: n, p: vec![0; n + 1], } } fn add(\u0026amp;mut self, mut x: usize, y: i32) { while x \u0026lt; self.N { self.p[x] += y; x += x \u0026amp; (!x + 1); } } fn get(\u0026amp;self, mut x: usize) -\u0026gt; i32 { let mut res = 0; while x \u0026gt; 0 { res += self.p[x]; x -= x \u0026amp; (!x + 1); } res } } 几种常见的封装集合 Vector 定义\n1 2 3 let v1 = vec![1, 2, 3, 4]; let v2 = vec![0; 5]; let mut v3: Vec\u0026lt;i32\u0026gt; = Vec::new(); 几个常用的函数\npush pop append 1 2 // 将 v2 中的元素移动到 v1 v1.append(\u0026amp;mut v2); sort | sort_by 1 2 // 逆序排序 v1.sort_by(|a, b| b.cmp(a)); is_empty len resize String 定义\n1 2 3 let s1 = String::from(\u0026#34;hello, \u0026#34;); let s2 = \u0026#34;world!\u0026#34;.to_string(); let s3 = s1 + \u0026amp;s2; 采用 UTF-8 编码，利用索引引用的时候两个连续的索引表示一位char。\nHash Map 定义\n1 2 3 4 5 6 7 8 let mut map = HashMap::new(); map.insert(\u0026#34;red\u0026#34;.to_string(), 1); map.insert(\u0026#34;blue\u0026#34;.to_string(), 2); map.insert(\u0026#34;red\u0026#34;.to_string(), 10); // 覆盖 map.entry(\u0026#34;red\u0026#34;.to_string()).or_insert(10); // 不存在则插入一个值 let conut = map.entry(\u0026#34;bule\u0026#34;.to_string()).or_insert(0); *conut += 1; // 更新map中的数据 let x = map.get(\u0026amp;\u0026#34;red\u0026#34;.to_string()).copied().unwrap_or(0); // 得到一个不可变的值 ","permalink":"https://solarain.net.cn/posts/rust%E5%B0%8F%E8%AE%B0/","summary":"rust是一种比较新的语言，对标cpp，拥有优越的性能和安全的内存，非常适合高并发的软件，相较于其他语言来说比较难入门，这里给出了部分简单语法。","title":"rust 小记"},{"content":"这里多数问题只做简单解释，对部分题目给出链接细说。\nAdd One\nZero-One\nSubsequence Path\nHanging Hearts\nFlat Subsequence\nincrement of coins\n2023 杭电多校 2 1010 Klee likes making friends\n题目大意：\n给定一个长度为 $n$ 的序列，要求每 $m$ 个长度的串中至少选择了两个，要求所选值和的最小值。\n题解：\n我们已经熟练得掌握了当题目为每 $m$ 个长度的串中至少选择一个的情况，可以直接用单调队列的解决这个问题，其中第 $i$ 个选数和 $i - 1$ 个选数的间隔不超过 $m$ ($p_i - p_{i - 1} - 1 \u003c m$)。而在此问题中第 $i$ 个选数和 $i- 2$个选数的间隔不超过 $m$ ($p_i - p_{i -2} - 1 \u003c m$)。那么 $p_{i-1}$ 作为状态参数参与 $dp$ 转移方程。\n故有：\n$$\rdp_{i,j} = min_{i - k - 1 \u003c m} \\lbrace dp_{j,k} + a_i\\rbrace\r$$其中 $i$ 为最后一个选数的位置，$j$ 为i倒数第二个选数的位置。\n发现该 $dp$ 方程无论在时间还是空间上都无法通过此题，空间为$O(n^2)$ ，时间为 $O(nm^2)$。\n将第二个参数设置为相对于第一个位置的距离，该转移方程就变成了：\n$$\rdp_{i,j} = min_{1\\leq k\\leq m - j} \\lbrace dp_{i - j,k} + a_i\\rbrace\r$$维护一个 $g_{i, j} = min_{1\\leq x\\leq j} \\{dp_{i, x}\\}$，发现这是一个前缀最小值， 可以预处理后 $O(1)$ 取值。\n同时注意到 $dp_i$ 只能从 $dp_x, (i - m \u003c x \u003c i - 1)$ 得到，因此可以用一个滚动数组来维护第一维。\n故得出最终的 $DP$ 转移方程：\n$$\rdp_{i,j} = g_{i - j, m - j} + a_i\r$$$$\rg_{i,j} = min\\lbrace g_{i,j}, g_{i, j - 1}\\rbrace\r$$同时为了方便统计答案，我们可以在原数组末尾增加一位权值为 $0$ 的项，以及发现 $g$ 实际上没有必要增设，可以直接从$dp_{i,j-1}$转移得到。\n最终空间复杂度$O(m^2)$，时间复杂度$O(nm)$。\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const int MAX_N = 2e3 + 10; int dp[MAX_N][MAX_N]; void solve() { int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;int\u0026gt; a(n + 1); for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } memset(dp, 63, sizeof dp); for(int i = 0; i \u0026lt; m; i ++) { for(int j = 1; j \u0026lt; m; j ++) { if(i - j \u0026gt;= 0) { dp[i][j] = a[i] + a[i - j]; } dp[i][j] = min(dp[i][j], dp[i][j - 1]); } } for(int i = m; i \u0026lt;= n; i ++) { memset(dp[i % m], 63, sizeof dp[i % m]); for(int j = 1; j \u0026lt; m; j ++) { if(i - j \u0026gt;= 0) { dp[i % m][j] = dp[(i - j) % m][m - j] + a[i]; } dp[i % m][j] = min(dp[i % m][j], dp[i % m][j - 1]); } } int res = INF; for(int i = 1; i \u0026lt; m; i ++) { res = min(res, dp[n % m][i]); } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 2020 ICPC银川 B The Great Wall\n题目大意：\n给定$n$个数，将这$n$个数分成连续的$k$段，每段的价值是段内的最大值减最小值，求每段价值和可能的最大值，输出$k\\in [1, n]$的所有结果。\n题解：\n由于每段的价值是段内的最大值减去最小值，因此这段内任选两个数的差作为价值都小于等于段内的最大值减最小值，这意味着每段有且仅有一个数的符号为$+1$，一个数的符号为$-1$，其余数的符号为$0$，或者段内仅有一个数且符号为$0$，同时只要dp出所有的方案就能找出最优解。\n设计dp状态： $dp[i][0,1,2]$，其中第一维表示考虑第$k$段，第二位的三种状态分别对应三种段内的三种情况：$0$，未选权为$+1$和$-1$的数；$1$，选了权为$+1$的数；$2$选了权为$-1$的数。\n可以列出dp转移方程： $$\r\\begin{cases}\rdp[i][0] = max\r\\begin{cases}\rdp[i][0] \\\\\\\\\rdp[i - 1][0] \\\\\\\\\rdp[i - 1][1] - a_x \\\\\\\\\rdp[i - 1][2] + a_x \\\\\\\\\r\\end{cases} \\\\\\\\\rdp[i][1] = max\r\\begin{cases}\rdp[i][1] \\\\\\\\\rdp[i - 1][0] + a_x\r\\end{cases} \\\\\\\\\rdp[i][2] = max\r\\begin{cases}\rdp[i][2] \\\\\\\\\rdp[i - 1][0] - a_x\r\\end{cases}\r\\end{cases}\r$$上式中，等号左边表示要转移到的dp状态（选第$x$个数），等号右边的dp表示上一层的dp状态（选第$x-1$个数）\n参考代码： 203525741\n2019 ICPC银川 A Girls Band Party\n题目大意：\n有$n$张牌，每张牌有它的名字，颜色和权值，同时有$5$种有名字享受名字加成，所选的牌的和增加10%，$1$种颜色享受颜色加成，所选的牌的和增加20%，效果可以叠加（做加法），要求选$5$张名字不相同的牌，最多能得到的权值是多少。\n题解： 注意到每种名字的牌只需要考虑有颜色加成和没有颜色加成的两张牌就可以了，因此 dp 转移的时候只需考虑两种情况即可。\n下面是两种DP状态设计\n$$\rdp[i][j][k] = max\\begin{cases}\rdp[i-1][j - 1][k - 1] \u0026\\\\\\\\\rdp[i-1][j - 1][k] \u0026\\\\\\\\\rdp[i-1][j][k - 1] \u0026\\\\\\\\\rdp[i-1][j][k] \u0026\r\\end{cases} \\qquad + w\r$$其中第一个参数表示选了几种名字，第二个参数表示有几个名字加成，第三个参数表示有几个颜色加成\n$$\rdp[i][j] = max\\begin{cases}\rdp[i - 1][j - k] \u0026j\\in[k, 15]\r\\end{cases} \\qquad + w\r$$其中第一个参数表示选了几种名字，第二个参数表示加成的大小，$k$表示选该张卡片能得到的加成。\n参考代码：\n第一种205394270（虽然代码看起来比较复杂，但是是赛时想到的）\n第二种205394205\n2022 ICPC南京 B Ropeway\n题目大意：\n在距离索道入口$0$和$(n + 1)$单位距离的位置有索道站，给出在$1, 2, · · · , n$单位距离架设支撑塔的成本，分别是$a_1, a_2, ... , a_n$。要求相邻支撑塔或索道站之间的距离必须小于等于$k$，同时给出$s$，若$s_i = 1$，则$i$位置必须架设支撑塔。成本序列会进行$q$次临时的修改（之后会复原），求出架设支撑塔的最小总成本。\n题解：\n如果不考虑修改，那么$dp_i = min_{max(0, i-k) \\leq j \u003c i}\\{ dp_j + a_i\\}$，此时可以利用单调队列优化$dp$使得在$O(n)$时间复杂度内可以求出。\n考虑$q$次修改，如果每次修改都按照$O(n)$来求出答案，那么显然时限不够，此时注意到$k$的值域较小，若每次询问都重新计算整个$dp$数组，显然有很多冗余的计算，因此可以考虑重构其中的一段$dp$，如果从前往后$dp$，那么如果$x$发生修改，那么只有$[x, x + k]$这段可能会发生改变，因此我们只需要重新计算这部分的值即可。\n同时此题中从前往后和从后向前$dp$的效果是相同的，因此可以维护两个$dp$数组，$dp1$和$dp2$，其中，每次修改时，同过重构$dp1$的部分内容。 由此可以得到最终的结果为： $$\rmax_{x \\leq i \\leq min(x + k, n + 1)}(dp1_i + dp2_i - a_i)\r$$时间复杂度为$O(kq)$\n参考代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long i64; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, mod = 998244353, MAXN = 1e5 + 10; void solve() { int n, k, q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; k; vector\u0026lt;i64\u0026gt; a(n + 2, 0); for(int i = 1; i \u0026lt;= n; i ++) { cin \u0026gt;\u0026gt; a[i]; } string s; cin \u0026gt;\u0026gt; s; s = \u0026#34;1\u0026#34; + s + \u0026#34;1\u0026#34;; vector\u0026lt;i64\u0026gt; f(n + 2), g(n + 2); deque\u0026lt;int\u0026gt; que; que.push_back(0); for(int i = 1; i \u0026lt;= n + 1; i ++) { while(que.size() \u0026amp;\u0026amp; i - que.front() \u0026gt; k) { que.pop_front(); } f[i] = a[i] + f[que.front()]; if(s[i] == \u0026#39;1\u0026#39;) que.clear(); while(que.size() \u0026amp;\u0026amp; f[que.back()] \u0026gt;= f[i]) { que.pop_back(); } que.push_back(i); } que.clear(); que.push_back(n + 1); for(int i = n; i \u0026gt;= 0; i --) { while(que.size() \u0026amp;\u0026amp; que.front() - i \u0026gt; k) { que.pop_front(); } g[i] = a[i] + g[que.front()]; if(s[i] == \u0026#39;1\u0026#39;) que.clear(); while(que.size() \u0026amp;\u0026amp; g[que.back()] \u0026gt;= g[i]) { que.pop_back(); } que.push_back(i); } for(int i = 1; i \u0026lt;= n; i ++) { g[i] -= a[i]; } cin \u0026gt;\u0026gt; q; vector\u0026lt;i64\u0026gt; f2 = f; while(q --) { int x, v; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; v; i64 old = a[x]; a[x] = v; que.clear(); for(int i = max(0, x - k); i \u0026lt; x; i ++) { if(s[i] == \u0026#39;1\u0026#39;) { que.clear(); } while(que.size() \u0026amp;\u0026amp; f[que.back()] \u0026gt;= f[i]) { que.pop_back(); } que.push_back(i); } i64 res = 1e18; for(int i = x; i \u0026lt;= min(n + 1, x + k); i ++) { while(que.size() \u0026amp;\u0026amp; i - que.front() \u0026gt; k) { que.pop_front(); } f2[i] = a[i] + f2[que.front()]; if(s[i] == \u0026#39;1\u0026#39;) que.clear(); while(que.size() \u0026amp;\u0026amp; f2[que.back()] \u0026gt; f2[i]) { que.pop_back(); } que.push_back(i); res = min(res, f2[i] + g[i]); } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(int i = x; i \u0026lt;= min(n + 1, x + k); i ++) { f2[i] = f[i]; } a[x] = old; } } signed main () { ios::sync_with_stdio(0), cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { solve(); } } ","permalink":"https://solarain.net.cn/posts/dp%E9%A2%98%E5%8D%95/","summary":"一个dp题单，后续会加新的题进来\u0026hellip;","title":"DP 做题记录"},{"content":"表达式求值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 namespace CAL { int expression(char*\u0026amp;s); // 计算一个不带括号的表达式值 int token(char*\u0026amp;s) { // 取出一个值加入到栈中 if(*s == \u0026#39;(\u0026#39;) { s ++; int ret = expression(s); s ++; return ret; } int ret = 0; while(\u0026#39;0\u0026#39; \u0026lt;= *s \u0026amp;\u0026amp; *s \u0026lt;= \u0026#39;9\u0026#39;){ ret = ret * 10 + *s++ - \u0026#39;0\u0026#39;; } return ret % mod; } int rank(char op) { switch(op) { case \u0026#39;-\u0026#39;: return 1; case \u0026#39;+\u0026#39;: return 1; case \u0026#39;*\u0026#39;: return 0; } return 2; } int expression(char*\u0026amp;s){ stack\u0026lt;int\u0026gt; svar; stack\u0026lt;char\u0026gt; sop; svar.push(token(s)); while(true){ char op = *s; if(!op || op == \u0026#39;)\u0026#39;) op = \u0026#39;$\u0026#39;; else s++; while(!sop.empty() \u0026amp;\u0026amp; rank(sop.top()) \u0026lt;= rank(op)){ int b = svar.top(); svar.pop(); int a = svar.top(); svar.pop(); switch(sop.top()){ case \u0026#39;+\u0026#39;:{ svar.push(a + b); break; } case \u0026#39;-\u0026#39;:{ svar.push(a - b); break; } case \u0026#39;*\u0026#39;: { svar.push(a * b); break; } } sop.pop(); } if(op == \u0026#39;$\u0026#39;) break; svar.push(token(s)); sop.push(op); } return svar.top(); } } char s[MAXN]; int main() { ios::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; s; char *test = s; cout \u0026lt;\u0026lt; CAL::expression(test); } ","permalink":"https://solarain.net.cn/posts/%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E6%A8%A1%E6%9D%BF%E5%80%BC/","summary":"之前网络赛要写个表达式求值的算法, 写寄了, 以此铭记。","title":"表达式求值"},{"content":"传送门\nA Mocha 上小班啦 签到，交换$01$位置输出即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; string tab = \u0026#34;1023456789\u0026#34;; int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin \u0026gt;\u0026gt; n; if(n \u0026gt; tab.size()) { cout \u0026lt;\u0026lt; \u0026#34;-1\\n\u0026#34;; } else { for(int i = 0; i \u0026lt; n; i ++) { cout \u0026lt;\u0026lt; tab[i]; } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } B Hash 考虑子串的长度不大于$15$的情况(官方题解说只要考虑这种情况就行了)，然后dp就行了。只需证明长度为$16$的字串必然会存在一个划分使得产生的价值大于不划分产生的价值。具体看官方题解吧，这个证明我也说不太清（现场的话应该可以猜字串长度不会太大来做）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; int mp(char x) { int res = 0; if(x == \u0026#39;a\u0026#39;) res = 1; else if(x == \u0026#39;e\u0026#39;) res = 2; else if(x == \u0026#39;h\u0026#39;) res = 3; else if(x == \u0026#39;n\u0026#39;) res = 4; return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); string s; cin \u0026gt;\u0026gt; s; int n = s.size(); s = s + s; vector\u0026lt;ll\u0026gt; d(16); for(int i = 0; i \u0026lt; 16; i ++) { if(!i) d[i] = 1; else d[i] = 31 * d[i - 1]; d[i] %= mod; } ll res = 0; for(int l = 1; l \u0026lt;= 15; l ++) { int r = l + n; vector\u0026lt;ll\u0026gt; dp (n + 1); for(int i = 1; i \u0026lt;= n; i ++) { ll t = 0; for(int j = i, x = 0; j \u0026gt; max(0, i - 15); j --, x ++) { t += mp(s[l + j - 1]) * d[x]; t %= mod; dp[i] = max(dp[i], dp[j - 1] + t); } } res = max(res, dp[n]); } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } E Serval 的俳句 记录每个字母出现的位置，考虑三段组成的字母，第一段取字母的前5个并记录最后一个字母的位置，第二段找到从记录的位置开始，取7个，再记录最后一个字母的位置，第三段同理。时间复杂度$O(26^3log|S|)$。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin \u0026gt;\u0026gt; n; string s; cin \u0026gt;\u0026gt; s; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; pos(26); for(int i = 0; i \u0026lt; n; i ++) { pos[s[i] - \u0026#39;a\u0026#39;].push_back(i); } for(int i = 0; i \u0026lt; 26; i ++) if(pos[i].size() \u0026gt;= 5) { int x1 = 4; for(int j = 0; j \u0026lt; 26; j ++) { int c = upper_bound(pos[j].begin(), pos[j].end(), pos[i][x1]) - pos[j].begin(); if(pos[j].size() - c \u0026lt; 7) { continue; } int x2 = c + 6; for(int k = 0; k \u0026lt; 26; k ++) { int c = upper_bound(pos[k].begin(), pos[k].end(), pos[j][x2]) - pos[k].begin(); if(pos[k].size() - c \u0026lt; 5) { continue; } int x3 = c + 4; char x = \u0026#39;a\u0026#39; + i, y = \u0026#39;a\u0026#39; + j, z = \u0026#39;a\u0026#39; + k; for(int t = 0; t \u0026lt; 5; t ++) { cout \u0026lt;\u0026lt; x; } for(int t = 0; t \u0026lt; 7; t ++) { cout \u0026lt;\u0026lt; y; } for(int t = 0; t \u0026lt; 5; t ++) { cout \u0026lt;\u0026lt; z; } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return 0; } } } cout \u0026lt;\u0026lt; \u0026#34;none\\n\u0026#34;; return 0; } F 集合之和 这题卡了很久，以为要求使$n$最小。。。 如果是一个等差数列的话，矩阵中的任意一点的值为$O(2a_0 + (i + j)k)$，发现$n$大小的数列能够产生$2n - 1$个不同的数，故对所有的奇数都成立。若前三个数不等差，那么会产生$6$个数，后面每多一个数与前一个数等差，会多产生两个数，故对于所有大于等于$6$的偶数都有解。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin \u0026gt;\u0026gt; n; if(n % 2 == 1) { int x = (n + 1) / 2; cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(int i = 1; i \u0026lt;= x; i ++) { cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } else { if(n \u0026gt;= 6) { int x = n / 2; cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(int i = 1, j = 1; j \u0026lt;= x; i ++, j ++) { if(i == 2) { i ++; } cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } } else { cout \u0026lt;\u0026lt; \u0026#34;-1\\n\u0026#34;; } } } G Mocha 上大班啦 诈骗题。\u0026amp;运算只会使$0$减少，不会增加，那么对全部数进行\u0026amp;运算就是求每一位都为$1$的个数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; int main() { ios::sync_with_stdio(false); cin.tie(0); int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; v(n, vector\u0026lt;int\u0026gt; (m)); for(int i = 0; i \u0026lt; n; i ++) { string s; cin \u0026gt;\u0026gt; s; for(int j = 0; j \u0026lt; m; j ++) { v[i][j] = s[j] - \u0026#39;0\u0026#39;; } } struct OP { int i, j, l, r, p; }; int q; cin \u0026gt;\u0026gt; q; vector\u0026lt;OP\u0026gt; op(q); for(auto \u0026amp;[i, j, l, r, p] : op) { cin \u0026gt;\u0026gt; i \u0026gt;\u0026gt; j \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r \u0026gt;\u0026gt; p; } int res = 0; for(int i = 0; i \u0026lt; m; i ++) { int c = 1; for(int j = 0; j \u0026lt; n; j ++) { c \u0026amp;= v[j][i]; } res += c; } cout \u0026lt;\u0026lt; res \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } H 旋转水管 dfs模拟，分类讨论即可, 根据当前位置的水管形状以及上一位置水管的方向来判断下一位置的方向, 时间复杂度$O(m)$。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 1e5 + 10, mod = 998244353; int m, s, t; char G[2][MAXN]; bool vis[3][MAXN]; /* 0 向下 1 向上 2 向左 3 向右 */ bool res = 0; void dfs(int x, int y, int type) { if(vis[x][y]) { return; } if(x \u0026lt; 0 || x \u0026gt;= 2 || y \u0026lt; 0 || y \u0026gt;= m) { if(x == 2 \u0026amp;\u0026amp; y == t) { res = 1; } return; } vis[x][y] = 1; if(type == 1) { if(G[x][y] == \u0026#39;I\u0026#39;) { dfs(x + 1, y, type); } else { if(y \u0026gt;= 1) dfs(x, y - 1, 2); dfs(x, y + 1, 3); } } else if(type == 2) { if(G[x][y] == \u0026#39;I\u0026#39;) { if(y \u0026gt;= 1) dfs(x, y - 1, type); } else { if(x \u0026gt;= 1) dfs(x - 1, y, 0); dfs(x + 1, y, 1); } } else if(type == 3) { if(G[x][y] == \u0026#39;I\u0026#39;) { dfs(x, y + 1, type); } else { if(x \u0026gt;= 1) dfs(x - 1, y, 0); dfs(x + 1, y, 1); } } else { if(G[x][y] == \u0026#39;I\u0026#39;) { if(x \u0026gt;= 1) dfs(x - 1, y, type); } else { if(y \u0026gt;= 1) dfs(x, y - 1, 2); dfs(x, y + 1, 3); } } vis[x][y] = 0; } void solve() { cin \u0026gt;\u0026gt; m \u0026gt;\u0026gt; s \u0026gt;\u0026gt; t; s --, t --; for(int i = 0; i \u0026lt; 2; i ++) { for(int j = 0; j \u0026lt; m; j ++) { cin \u0026gt;\u0026gt; G[i][j]; } } res = 0; dfs(0, s, 1); if(res) { cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } } int main() { ios::sync_with_stdio(false); cin.tie(0); int _ = 1; cin \u0026gt;\u0026gt; _; while(_ --) { solve(); } return 0; } I Oshwiciqwq 的电梯 大模拟，由于数据范围非常的小，可以确定每一秒的状态。\n代码写得又臭又长，好在写了注释，应该可以看懂。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 10, mod = 998244353; PII t[3][10][10]; // 0 X, 1 Y ,2 Z; first 位置， second 编号 int v[510]; // 0 未上电梯，1在x电梯上，2在y电梯上，3在z电梯上 struct P { int tim; // 出现时间 int x, y, z; // 当前位置 int tx, ty, tz; // 目标位置 } p[60]; int n, m, h, q, k; void work() { for(int i = 1; i \u0026lt;= m; i ++) { for(int j = 1; j \u0026lt;= h; j ++) { if(t[0][i][j].first \u0026gt; 0) { t[0][i][j].first %= n; t[0][i][j].first ++; } } } for(int i = 1; i \u0026lt;= n; i ++) { for(int j = 1; j \u0026lt;= h; j ++) { if(t[1][i][j].first \u0026gt;= 0) { t[1][i][j].first %= m; t[1][i][j].first ++; } } } for(int i = 1; i \u0026lt;= n; i ++) { for(int j = 1; j \u0026lt;= m; j ++) { if(t[2][i][j].first \u0026gt;= 0) { t[2][i][j].first %= h; t[2][i][j].first ++; } } } } struct RES { int tt; int p; int io; int xl; int x, y, z; const bool operator \u0026lt; (const RES\u0026amp; a) const { if(tt != a.tt) return tt \u0026lt; a.tt; if(xl != a.xl) return xl \u0026lt; a.xl; if(io != a.io) return io \u0026lt; a.io; if(p != a.p) return p \u0026lt; a.p; } }; vector\u0026lt;RES\u0026gt; res; int main() { ios::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; h \u0026gt;\u0026gt; k; for(int i = 0; i \u0026lt; k; i ++) { int type, ex, ey, ez; cin \u0026gt;\u0026gt; type \u0026gt;\u0026gt; ex \u0026gt;\u0026gt; ey \u0026gt;\u0026gt; ez; if(type == 0) { t[type][ey][ez].first = ex; t[type][ey][ez].second = i; } else if(type == 1) { t[type][ex][ez].first = ey; t[type][ex][ez].second = i; } else { t[type][ex][ey].first = ez; t[type][ex][ey].second = i; } } cin \u0026gt;\u0026gt; q; for(int i = 0; i \u0026lt; q; i ++) { auto \u0026amp;[tim, x, y, z, tx, ty, tz] = p[i]; cin \u0026gt;\u0026gt; tim \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; z \u0026gt;\u0026gt; tx \u0026gt;\u0026gt; ty \u0026gt;\u0026gt; tz; } int tt = 0; // 当前时间戳 bool ok = 0; do { ok = 0; for(int i = 0; i \u0026lt; q; i ++) { if(p[i].tim \u0026lt;= tt) { // 未进电梯 if(v[i] == 0) { if(p[i].x != p[i].tx) { ok = 1; if(t[0][p[i].y][p[i].z].first == p[i].x) { res.push_back({tt, i + 1, 1 , t[0][p[i].y][p[i].z].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 1; p[i].x %= n; p[i].x ++; } } else if(p[i].y != p[i].ty) { ok = 1; if(t[1][p[i].x][p[i].z].first == p[i].y) { res.push_back({tt, i + 1, 1 , t[1][p[i].x][p[i].z].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 2; p[i].y %= m; p[i].y ++; } } else if(p[i].z != p[i].tz) { ok = 1; if(t[2][p[i].x][p[i].y].first == p[i].z) { res.push_back({tt, i + 1, 1 , t[2][p[i].x][p[i].y].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 3; p[i].z %= h; p[i].z ++; } } } else if(v[i] == 1) { ok = 1; if(p[i].x == p[i].tx) { res.push_back({tt, i + 1, 0, t[0][p[i].y][p[i].z].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 0; } else { p[i].x %= n; p[i].x ++; } } else if(v[i] == 2) { ok = 1; if(p[i].y == p[i].ty) { res.push_back({tt, i + 1, 0, t[1][p[i].x][p[i].z].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 0; } else { p[i].y %= m; p[i].y ++; } } else if(v[i] == 3) { ok = 1; if(p[i].z == p[i].tz) { res.push_back({tt, i + 1, 0, t[2][p[i].x][p[i].y].second + 1, p[i].x, p[i].y, p[i].z}); v[i] = 0; } else { p[i].z %= h; p[i].z ++; } } } else { ok = 1; } } work(); tt ++; } while (ok); sort(res.begin(), res.end()); for(auto i : res) { // [time] Person person_id IN / OUT Elevator elevator_id at (x, y, z); if(i.io == 1) { cout \u0026lt;\u0026lt; \u0026#34;[\u0026#34; \u0026lt;\u0026lt; i.tt \u0026lt;\u0026lt; \u0026#34;s] Person \u0026#34; \u0026lt;\u0026lt; i.p \u0026lt;\u0026lt; \u0026#34; IN Elevator \u0026#34; \u0026lt;\u0026lt; i.xl \u0026lt;\u0026lt; \u0026#34; at (\u0026#34; \u0026lt;\u0026lt; i.x \u0026lt;\u0026lt; \u0026#34;, \u0026#34; \u0026lt;\u0026lt; i.y \u0026lt;\u0026lt; \u0026#34;, \u0026#34; \u0026lt;\u0026lt; i.z \u0026lt;\u0026lt; \u0026#34;)\\n\u0026#34;; } else { cout \u0026lt;\u0026lt; \u0026#34;[\u0026#34; \u0026lt;\u0026lt; i.tt \u0026lt;\u0026lt; \u0026#34;s] Person \u0026#34; \u0026lt;\u0026lt; i.p \u0026lt;\u0026lt; \u0026#34; OUT Elevator \u0026#34; \u0026lt;\u0026lt; i.xl \u0026lt;\u0026lt; \u0026#34; at (\u0026#34; \u0026lt;\u0026lt; i.x \u0026lt;\u0026lt; \u0026#34;, \u0026#34; \u0026lt;\u0026lt; i.y \u0026lt;\u0026lt; \u0026#34;, \u0026#34; \u0026lt;\u0026lt; i.z \u0026lt;\u0026lt; \u0026#34;)\\n\u0026#34;; } } } ","permalink":"https://solarain.net.cn/posts/2022ccpc%E6%B2%B3%E5%8D%97%E7%9C%81%E8%B5%9B%E9%A2%98%E8%A7%A3/","summary":"这场的题目不是特别难，题目用到的算法也不是特别多，算比较基础的一场。","title":"2022CCPC河南省赛题解"},{"content":"概念 基环树指$n$个点$n$条边的连通图, 仅比树多一条边, 有大部分树的性质.\n重要的事 找环 tarjan找环, 给一份容易实现的代码（不能找重边和自环）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int n, m; vector\u0026lt;int\u0026gt; G[MAXN]; namespace Circle { vector\u0026lt;int\u0026gt; circle; // 环上的点 int tag[MAXN]; // 标记: -1未访问; 1访问过; 2在环上 void init() { for(int i = 0; i \u0026lt; n; i ++) { tag[i] = -1; } } int dfs(int u, int f) { if(tag[u] != -1) { return u; } tag[u] = 1; for(auto v : G[u]) if(v != f) { int t = dfs(v, u); if(t \u0026gt; -1) { circle.push_back(u); tag[u] = 2; return t == u ? -1 : t; } } return -1; } } 例题 旅行 题目大意: 给定一棵普通树或者基环树, 求访问全部节点最小的字典序(每个节点只记录第一次访问).\n对于一般树, 可以$1$节点开始跑; 对于基环树, 可以删一条边使其变成普通树, 时间复杂度$O(n^2)$\n参考代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 5e3 + 10, mod = 998244353; int n, m; vector\u0026lt;int\u0026gt; G[MAXN]; vector\u0026lt;int\u0026gt; res, ste; namespace Circle { vector\u0026lt;int\u0026gt; circle; int tag[MAXN]; void init() { for(int i = 0; i \u0026lt; n; i ++) { tag[i] = -1; } } int dfs(int u, int f) { if(tag[u] != -1) { return u; } tag[u] = 1; for(auto v : G[u]) if(v != f) { int t = dfs(v, u); if(t \u0026gt; -1) { circle.push_back(u); tag[u] = 2; return t == u ? -1 : t; } } return -1; } } bool cmp(vector\u0026lt;int\u0026gt; \u0026amp;a, vector\u0026lt;int\u0026gt; \u0026amp;b) { if(!a.size()) { return 1; } else { for(int i = 0; i \u0026lt; n; i ++) { if(a[i] != b[i]) { return a[i] \u0026gt; b[i]; } } } return 0; } int du = -1, dv = -1; // 删的边 void dfs(int u, int p) { ste.push_back(u); for(int v : G[u]) if(v != p) { if((du == u \u0026amp;\u0026amp; dv == v) || du == v \u0026amp;\u0026amp; dv == u) continue; dfs(v, u); } } int main() { ios::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; for(int i = 0; i \u0026lt; m; i ++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u --, v --; G[u].push_back(v), G[v].push_back(u); } for(int i = 0; i \u0026lt; n; i ++) { sort(G[i].begin(), G[i].end()); } if(m == n - 1) { dfs(0, 0); res = ste; } else { Circle::init(); Circle::dfs(0, 0); for(int i = 0; i \u0026lt; Circle::circle.size() - 1; i ++) { du = Circle::circle[i], dv = Circle::circle[i + 1]; ste.clear(); dfs(0, 0); if(cmp(res, ste)) { res = ste; } } } for(int i : res) { cout \u0026lt;\u0026lt; i + 1 \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } } ","permalink":"https://solarain.net.cn/posts/%E5%9F%BA%E7%8E%AF%E6%A0%91%E7%AE%80%E8%BF%B0/","summary":"基环树指的是一棵树多一条边所形成的图（应该是不考虑自环和重边的）, 多出现在树上dp类型的题目中，是一种类型的题目。","title":"基环树简述"},{"content":"数组分块简述 在某些情况下，我们可能需要暴力地处理数组的每一个数，数组分块提供了一种简单的优化来降低复杂度，使得原本的$O(n)$降为$O(\\sqrt n)$，这可能不是最优解，但一定足够简单（事实上，在1e5范围内，$O(\\log^2 n)$和$O(\\sqrt n)$差距不大）。 这类问题的“整体”性质更弱，有时不存在高效的信息合并化简方法，需要在“批量”和“零散”之间找到平衡点。\n基础形式 我们将一个数组分为形如下图的几段，每段大小为B，当然为了规范，如果$B \\nmid n$，我们要在数组后面填充$inf$（在后面的一些处理中会用到这个）。对每次操作只需要对左右两个散块单独处理和对中间一些整块处理。\n理解了分块的概念以后，你就可以开始着手尝试用分块去解决一些线段树问题了（虽然此时分块对比线段树不占优）。\n例题 模板题 题目大意：区间上所有元素加一个数$k$；求区间所有元素的和。 对于这个问题，简单地开三个数组$a,s,v$ 分别表示：数组值；分块中除去标记的数组值和；标记：分块中的每个元素 + k。\n参考代码： 每个块的左右边界可以用结构体记录下来，也可以直接计算，这里采用的是直接计算的方式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 1e5 + 10, mod = 998244353; int BS; ll a[MAXN], s[405], v[405]; void add(int l, int r, int k) { int bl = l / BS, br = r / BS; if(bl == br) { for(int i = l; i \u0026lt;= r; i ++) { a[i] += k, s[bl] += k; } } else { for(int i = l; i \u0026lt; (bl + 1) * BS; i ++) { a[i] += k, s[bl] += k; } for(int i = bl + 1; i \u0026lt; br; i ++) { v[i] += k, s[i] += 1ll * BS * k; } for(int i = br * BS; i \u0026lt;= r; i ++) { a[i] += k, s[br] += k; } } } ll query(int l, int r) { ll res = 0; int bl = l / BS, br = r / BS; if(bl == br) { for(int i = l; i \u0026lt;= r; i ++) { res += a[i] + v[bl]; } } else { for(int i = l; i \u0026lt; (bl + 1) * BS; i ++) { res += a[i] + v[bl]; } for(int i = bl + 1; i \u0026lt; br; i ++) { res += s[i]; } for(int i = br * BS; i \u0026lt;= r; i ++) { res += a[i] + v[br]; } } return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; BS = sqrt(n) + 1; for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; s[i / BS] += a[i]; } while(m -- ) { int op, l, r, k; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; l --, r --; if(op == 1) { cin \u0026gt;\u0026gt; k; add(l, r, k); } else { cout \u0026lt;\u0026lt; query(l, r) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } 分块的大小可以人为确定，需要计算时间复杂度后再确定。此题中理论时间复杂度为$O(q (B + n / B))$， 另$B = \\sqrt n$，得到最优时间复杂度$O(q \\sqrt n)$。\n教主的魔法 题目大意：1、区间加；2、求区间大于$c$的元素个数。\n首先来思考一下如何找出一个整块中大于$c$的元素个数 \u0026hellip;\u0026hellip; 如果一个整块中的元素有序，那么是不是可以二分。\n在上题中我们用$s$来维护区间和，现在我们用$s$来维护区间的有序数组。那么一个区间内大于$c$的元素个数就等于散块大于$c$的个数 + 每个整块中大于$c$的个数（这部分用lower_bound来处理）。单次询问时间复杂度为$O((n/B) log B + B)$。\n在来思考一下区间加，由于需要额外维护一个有序数组，每次加操作可以对散块进行暴力排序，整块就在标记上加值，单词加的时间复杂度为$O(B log B + n / B)$。\n关于B的取值问题，首先应该考虑平衡两种操作的时间复杂度，其次应该考虑整块的时间复杂度和散块部分的时间复杂度，具体而言就是让两者相等求出B。比如此题中对散块重新排序可以优化成提取有序的两部分（有改动和无改动）再merge，此时加操作的时间复杂度就降为$O(B + n / B)$，发现这个复杂度是恒小于询问操作的复杂度的，此时就应该考虑适当增大B的大小，可以对$(n / B) log B + B$ 求导计算出极值，也可以用三分来求，事实上这个值约等于$\\sqrt{n\\log \\sqrt n}$。\n参考代码（不加优化）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 1100000, mod = 998244353; int n, q, BS; int a[MAXN], v[410], s[MAXN]; void add(int l, int r, int k) { int bl = l / BS, br = r / BS; if(bl == br) { for(int i = l; i \u0026lt;= r; i ++) { a[i] += k; } for(int i = bl * BS; i \u0026lt; (bl + 1) * BS; i ++) { s[i] = a[i]; } sort(s + bl * BS, s + (bl + 1) * BS); } else { for(int i = l; i \u0026lt; (bl + 1) * BS; i ++) { a[i] += k; } for(int i = bl * BS; i \u0026lt; (bl + 1) * BS; i ++) { s[i] = a[i]; } sort(s + bl * BS, s + (bl + 1) * BS); for(int i = bl + 1; i \u0026lt; br; i ++) { v[i] += k; } for(int i = br * BS; i \u0026lt;= r; i ++) { a[i] += k; } for(int i = br * BS; i \u0026lt; (br + 1) * BS; i ++) { s[i] = a[i]; } sort(s + br * BS, s + (br + 1) * BS); } } int query(int l, int r, int c) { int bl = l / BS, br = r / BS; int C, res = 0; if(bl == br) { C = c - v[bl]; for(int i = l; i \u0026lt;= r; i ++) { if(a[i] \u0026gt;= C) res ++; } } else { C = c - v[bl]; for(int i = l; i \u0026lt; (bl + 1) * BS; i ++) { if(a[i] \u0026gt;= C) res ++; } for(int i = bl + 1; i \u0026lt; br; i ++) { C = c - v[i]; res += BS - (lower_bound(s + BS * i, s + BS * (i + 1), C) - (s + BS * i)); } C = c - v[br]; for(int i = br * BS; i \u0026lt;= r; i ++) { if(a[i] \u0026gt;= C) res ++; } } return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); memset(a, 0x3f3f3f3f, sizeof a); cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q; BS = sqrt(n) + 1; for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; s[i] = a[i]; } for(int i = 0; i * BS \u0026lt;= n ; i ++) { sort(s + i * BS, s + (i + 1) * BS); } while(q --) { char op; int l, r, x; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r \u0026gt;\u0026gt; x; l --, r --; if(op == \u0026#39;M\u0026#39;) add(l, r, x); else cout \u0026lt;\u0026lt; query(l, r, x) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 稍稍写一下归并的代码（给一个结构体写的板子）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 int n, m, BS; ll a[MAXN]; struct BLOCK { int l, r; ll ex; vector\u0026lt;ll\u0026gt; B; vector\u0026lt;int\u0026gt; mp; BLOCK(int s) { B.resize(s); ex = 0; } BLOCK(int _l, int _r) { ex = 0; l = _l, r = _r; int s = r - l + 1; B.resize(s); mp.resize(s); for(int i = l; i \u0026lt;= r; i ++) { mp[i - l] = i; } sort(mp.begin(), mp.end(), [\u0026amp;] (int x, int y) { return a[x] \u0026lt; a[y]; }); for(int i = 0; i \u0026lt; s; i ++) { B[i] = a[mp[i]]; } } void rebuild(int s, int t, int k) { vector\u0026lt;int\u0026gt; _1, _2; for(int i = 0; i \u0026lt; mp.size(); i ++) { int x = mp[i]; if(x \u0026gt;= s \u0026amp;\u0026amp; x \u0026lt;= t) { _1.push_back(x); } else { _2.push_back(x); } } for(int i : _1) a[i] += k; int i = 0, j = 0, c = 0; while(i \u0026lt; _1.size() \u0026amp;\u0026amp; j \u0026lt; _2.size()) { if(a[_1[i]] \u0026lt; a[_2[j]]) mp[c ++] = _1[i ++]; else mp[c ++] = _2[j ++]; } while(i \u0026lt; _1.size()) mp[c ++] = _1[i ++]; while(j \u0026lt; _2.size()) mp[c ++] = _2[j ++]; for(int i = 0; i \u0026lt; mp.size(); i ++) B[i] = a[mp[i]]; } }; vector\u0026lt;BLOCK\u0026gt; block; void add(int l, int r, int k) { int lb = l / BS, rb = r / BS; if(lb == rb) { block[lb].rebuild(l, r, k); } else { block[lb].rebuild(l, block[lb].r, k); block[rb].rebuild(block[rb].l, r, k); for(int i = lb + 1; i \u0026lt; rb; i ++) { block[i].ex += k; } } } 由乃打扑克 给你一个长为 $n$ 的序列 $a$，需要支持 $m$ 次操作，操作有两种： 1、查询区间 $[l,r]$ 的第 $k$ 小值。 2、区间 $[l,r]$ 加上 $k$。\n分块 + 整体二分\n第$k$小的值$x$就相当于区间内存在$k-1$个数小于等于$x$，并且求这个值符合二分的规则，故可以整体二分确定$x$的大小，具体的check方法与上一例题一模一样。\n几个优化： 1. 二分前将左右两块散块合并，这样可以用$O(log\\ B)$来代替对整个散块遍历的$O(B)$ 2. 二分前计算出二分的上下界，以减少二分次数。 复杂度分析： add 时间复杂度$O(B + n / B）$ query 时间复杂度$O(B + (n / B)log\\ B\\ log\\ w)$ （$log\\ w$ 是二分是次数，大概为$20-30$）。\n确定块长：理论上复杂度的计算应该是查询时间复杂度 对$B$求偏导得到的，但是按照理论来算的话，这题这么做是可以被卡掉的。于是需要调亿点点块长，下面这份代码块长取$912$可以拿到$82pts$（已经不想扣常数了）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 1e5 + 9, mod = 998244353; void read() {} template\u0026lt;typename T,typename... Ts\u0026gt; inline void read(T \u0026amp;arg,Ts\u0026amp;... args) { T x = 0, f = 1; char c = getchar(); while(!isdigit(c)){if(c == \u0026#39;-\u0026#39;) f = -1; c = getchar();} while(isdigit(c)){x = (x \u0026lt;\u0026lt; 3) +(x \u0026lt;\u0026lt; 1) + (c - \u0026#39;0\u0026#39;);c = getchar();} arg = x * f; read(args...); } int n, m, BS; ll a[MAXN]; int _1[MAXN], _2[MAXN], t1, t2; struct BLOCK { int l, r; ll ex; vector\u0026lt;ll\u0026gt; B; vector\u0026lt;int\u0026gt; mp; BLOCK(int s) { B.resize(s); ex = 0; } BLOCK(int _l, int _r) { ex = 0; l = _l, r = _r; int s = r - l + 1; B.resize(s); mp.resize(s); for(int i = l; i \u0026lt;= r; ++ i) { mp[i - l] = i; } sort(mp.begin(), mp.end(), [\u0026amp;] (int x, int y) { return a[x] \u0026lt; a[y]; }); for(int i = 0; i \u0026lt; s; i ++) { B[i] = a[mp[i]]; } } void rebuild(int s, int t, int k) { t1 = t2 = 0; for(int i = 0; i \u0026lt; mp.size(); ++ i) { int x = mp[i]; if(x \u0026gt;= s \u0026amp;\u0026amp; x \u0026lt;= t) _1[t1 ++] = x; else _2[t2 ++] = x; } for(int i = 0; i \u0026lt; t1; ++ i) a[_1[i]] += k; int i = 0, j = 0, c = 0; while(i \u0026lt; t1 \u0026amp;\u0026amp; j \u0026lt; t2) { if(a[_1[i]] \u0026lt; a[_2[j]]) mp[c ++] = _1[i ++]; else mp[c ++] = _2[j ++]; } while(i \u0026lt; t1) mp[c ++] = _1[i ++]; while(j \u0026lt; t2) mp[c ++] = _2[j ++]; for(int i = 0; i \u0026lt; mp.size(); i ++) B[i] = a[mp[i]]; } }; vector\u0026lt;BLOCK\u0026gt; block; inline void add(int l, int r, int k) { int lb = l / BS, rb = r / BS; if(lb == rb) { block[lb].rebuild(l, r, k); } else { block[lb].rebuild(l, block[lb].r, k); block[rb].rebuild(block[rb].l, r, k); for(int i = lb + 1; i \u0026lt; rb; i ++) { block[i].ex += k; } } } inline ll query(int l, int r, int k) { int lb = l / BS, rb = r / BS; if(lb == rb) { int c = 0; for(int i : block[lb].mp) { if(i \u0026gt;= l \u0026amp;\u0026amp; i \u0026lt;= r) c ++; if(c == k) return a[i] + block[lb].ex; } } else { BLOCK _ex(block[lb].r - l + 1 + r - block[rb].l + 1); t1 = t2 = 0; for(int i : block[lb].mp) { if(i \u0026gt;= l) _1[t1 ++] = a[i] + block[lb].ex; } for(int i : block[rb].mp) { if(i \u0026lt;= r) _2[t2 ++] = a[i] + block[rb].ex; } int c = 0, i = 0, j = 0; while(i \u0026lt; t1 \u0026amp;\u0026amp; j \u0026lt; t2) { if(_1[i] \u0026lt; _2[j]) _ex.B[c ++] = _1[i ++]; else _ex.B[c ++] = _2[j ++]; } while(i \u0026lt; t1) _ex.B[c ++] = _1[i ++]; while(j \u0026lt; t2) _ex.B[c ++] = _2[j ++]; ll L = 1e18, R = -1e18; L = min(L, _ex.B[0]); R = max(R, _ex.B.back()); for(int i = lb + 1; i \u0026lt; rb; ++ i) { L = min(L, block[i].B[0] + block[i].ex); R = max(R, block[i].B.back() + block[i].ex); } L --, R ++; while(R \u0026gt; L + 1) { ll M = L + R \u0026gt;\u0026gt; 1; int cnt = 0; cnt += lower_bound(_ex.B.begin(), _ex.B.end(), M) - _ex.B.begin(); for(int i = lb + 1; i \u0026lt; rb; i ++) { cnt += lower_bound(block[i].B.begin(), block[i].B.end(), M - block[i].ex) - block[i].B.begin(); } if(cnt \u0026gt;= k) R = M; else L = M; } return R - 1; } } int main() { read(n, m); for(int i = 0; i \u0026lt; n; ++ i) { read(a[i]); } BS = 912; for(int i = 0; 1ll * i * BS \u0026lt; n; ++ i) { int l = i * BS, r = min(n - 1, (i + 1) * BS - 1); block.push_back(BLOCK(l, r)); } while(m --) { int op, l, r, k; read(op, l, r, k); l --, r --; if(op == 1) { if(r - l + 1 \u0026lt; k) { cout \u0026lt;\u0026lt; \u0026#34;-1\\n\u0026#34;; } else { printf(\u0026#34;%lld\\n\u0026#34;, query(l, r, k)); } } else { add(l, r, k); } } } 弹飞绵羊 有$n$个点，每个点都有一个值$k$，第$i$个点会通过一次弹射到$i + k_i$上，直到点不存在。两种操作，一种询问第$i$个点一共会经历几次弹射，另一种修改$k_i$。\n如果不存在修改操作，那么只需要预处理出所有点的答案，每次查询只需要$O(1)$的复杂度就可以求出解。而修改的操作是$O(n)$的，因此需要一种方法来平衡两种操作。发现可以将所有点任意地分割成$x$段，每一小段中的每个点都存在一个$k'_i$从所在段中弹飞，并且落到另一段中的某个点，因此可以记录每个段中每个点从所在段弹飞的所需的次数以及落点，此时每次修改只需要修改所在块即可。故得出解：将$n$个点分成$\\sqrt n$段，同时记录次数和位置，修改就重构块，查询就对该点在所有块中的次数求和。两种操作的时间复杂度均为$\\sqrt n$。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; typedef long long ll; typedef pair\u0026lt;int, int\u0026gt; PII; const int INF = 1e9 + 7, MAXN = 2e5 + 400, mod = 998244353; int n, BS, m, B; int a[MAXN], c[MAXN], to[MAXN]; void upd(int l, int r) { for(int i = r; i \u0026gt;= l; i --) { if(i + a[i] \u0026gt; r) { c[i] = 1; to[i] = i + a[i]; } else { c[i] = c[i + a[i]] + 1; to[i] = to[i + a[i]]; } } } int query(int x) { int res = 0, pos = x; do { res += c[pos]; pos = to[pos]; } while(pos \u0026lt; n); return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; n; BS = sqrt(n) + 1; for(int i = 0; i \u0026lt; n; i ++) { cin \u0026gt;\u0026gt; a[i]; } for(int i = 0; i \u0026lt; n; i += BS) { upd(i, min(n - 1, i + BS - 1)); } cin \u0026gt;\u0026gt; m; while (m --) { int op, x, y; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; x; if(op == 1) { cout \u0026lt;\u0026lt; query(x) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } else { cin \u0026gt;\u0026gt; y; a[x] = y; int b = x / BS; upd(b * BS, min(n - 1, (b + 1) * BS - 1)); } } } 有时间再补 简单的数列题 作诗 [Violet]蒲公英 任务分配问题 ","permalink":"https://solarain.net.cn/posts/%E5%88%86%E5%9D%97%E7%AE%80%E8%BF%B0/","summary":"称之为万能数据结构，整体的性质更弱，凡是对区间的操作基本都能用分块实现，但是对于存在log级别实现的题来说在复杂度上就有天然劣势，可能需要卡时限或者原本就不能利用分块来解决。","title":"分块简述"},{"content":"这里是“晓染的天空”的 About 页面。\n站点：记录学习、折腾与生活的笔记 作者：SolaRyan 技术方向：算法、系统、工具链、Linux 等 你可以在导航栏找到所有文章与归档，也可以通过搜索快速定位内容。\n联系方式：\nGitHub: https://github.com/RyanTsi Bilibili: https://space.bilibili.com/285870200 ","permalink":"https://solarain.net.cn/about/","summary":"关于本站与作者的简要介绍","title":"About"}]