Skip to content

Commit fe6c830

Browse files
committed
Modify the coding regulation of suffix array. Add adaptive simpson's rule algorithm and manacher algorithm. Add a new chapter named Numeric to the book.
1 parent 4d71387 commit fe6c830

File tree

4 files changed

+321
-15
lines changed

4 files changed

+321
-15
lines changed

main.tex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,30 @@ \section{后缀数组}
299299

300300
\lstinputlisting[language=C++, caption=suffix\_array.cpp, style=MyCStyle]{./str/suffix_array.cpp}
301301

302+
\section{最长回文子串}
303+
304+
最长回文子串问题是指: 在一个字符串中寻找其中最长的对称子串. 换句话说, 这个子串正着读和反着读完全相同.
305+
306+
对于最长回文子串问题, 一种朴素的解法是枚举中心并向两边扩展, 或者使用其它动态规划的方法, 但是这些方式在字符串较长时往往会遇到效率瓶颈.
307+
308+
Manacher算法是一种在线性时间内解决最长回文子串的算法. 它通过在原始字符串中插入分隔符并利用回文的对称性, 避免了重复计算, 将复杂度降低到$O(n)$. 这种高效性使得它不仅是理论研究的成果, 也在一些实际场景中得到了应用. 例如, 在DNA序列分析中寻找对称片段, 在自然语言处理里发现对称的语言结构, 以及在数据压缩中利用回文模式进行优化等.
309+
310+
\lstinputlisting[language=C++, caption=manacher.cpp, style=MyCStyle]{./str/manacher.cpp}
311+
312+
\chapter{数值方法}
313+
314+
在生产实践中, 尤其是大型工业软件的开发过程中, 数值计算的速度和精度通常和软件的运行效率息息相关. 在数学建模, 仿真, 有限元分析等领域, 使用精度高, 效率高的算法通常可以使程序的性能, 比使用低效算法的相同程序提升十倍甚至百倍. 而在生产环境, 微不足道的浮点误差可以造成惨重的人员和财产损失. 由此观之, 在建设中国式现代化的过程中, 为了解决工业软件领域的 "卡脖子" 难题, 提升数值计算的效率是非常重要的一环. 而掌握正确的计算方法, 则是每一位立志成为 "总师型人才" 的计算机专业学生的必修课. 下面笔者将提供一些简单常用的数值计算方法的实现以供参考, 所有代码都配备详尽注释, 力求通俗易懂, 方便移植改造.
315+
316+
\section{辛普森法}
317+
318+
辛普森法是一种常用的数值积分方法, 它的原理是利用二次插值多项式在小区间上近似被积函数, 从而得到定积分的近似值.
319+
320+
然而, 当积分区间较大或被积函数变化复杂时, 单一的辛普森公式往往难以保证精度. 为此, 可以采用复化辛普森法: 将积分区间划分为若干个小区间, 在每个小区间分别应用辛普森公式, 然后将结果加总.这样虽然提高了精度, 但由于分块数目固定, 可能会在函数比较平滑的区间上产生不必要的计算.
321+
322+
自适应辛普森法则通过递归的方式在尽可能少分块的前提下保证精度. 它先在整个区间上应用一次辛普森公式, 再将区间一分为二分别应用辛普森公式, 并比较两者结果.如果二者差异超过误差容许范围, 就继续对子区间递归分裂; 否则接受结果.这样, 自适应辛普森法能够在函数平滑的区域少分块, 在函数变化剧烈的区域多分块, 从而兼顾效率和精度.
323+
324+
在现实应用中, 辛普森法和自适应辛普森法被广泛用于工程与科学计算. 例如, 在航天工程中进行复杂气动力模型的积分估算, 在医学成像中计算复杂函数的积分, 或在金融工程中估算涉及不规则函数的积分问题.
325+
326+
\lstinputlisting[language=C++, caption=asr.cpp, style=MyCStyle]{./numeric/asr.cpp}
327+
302328
\end{document}

numeric/asr.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @file asr.cpp
3+
* @brief
4+
* @author Haoming Bai <haomingbai@hotmail.com>
5+
* @date 2025-08-20
6+
*
7+
* Copyright © 2025 Haoming Bai
8+
* SPDX-License-Identifier: MIT
9+
*
10+
* @details
11+
*/
12+
13+
#include <cmath>
14+
#include <concepts>
15+
#include <cstdlib>
16+
17+
/**
18+
* @brief Simpson's Rule for numerical integration.
19+
*
20+
* Simpson's Rule is a method for approximating the definite integral of a
21+
* function by fitting a quadratic polynomial to the function at three points
22+
* (the endpoints and the midpoint) and integrating that polynomial over the
23+
* interval [a, b]. The formula for the approximation is: (b - a)/6 * [f(a) +
24+
* 4f((a+b)/2) + f(b)].
25+
*
26+
* This method provides exact results for polynomials up to degree 3 and
27+
* generally offers better accuracy than the trapezoidal rule for smooth
28+
* functions.
29+
*
30+
* @tparam Func Callable type. Must be invocable with a floating-point argument
31+
* and return a value convertible to a floating-point type.
32+
* @tparam F1 Floating-point type for the lower bound.
33+
* @tparam F2 Floating-point type for the upper bound.
34+
*
35+
* @param f The function to be integrated. Should be continuous on [a, b].
36+
* @param a Lower limit of integration.
37+
* @param b Upper limit of integration.
38+
*
39+
* @return Approximation of the integral ∫_{a}^{b} f(x) dx.
40+
* The return type is deduced as the common type of F1 and F2.
41+
*
42+
* @note This is a non-adaptive implementation that provides a single
43+
* approximation over the entire interval. For higher accuracy over complex
44+
* functions, consider using an adaptive method like Adaptive Simpson's Rule
45+
* (ASR).
46+
* @note Requires that `f` be defined and continuous on [a, b].
47+
*
48+
* Example usage:
49+
* @code{.cpp}
50+
* auto result = Simpson([](double x) { return std::sin(x); }, 0.0, 3.14159);
51+
* @endcode
52+
*/
53+
template <typename Func, std::floating_point F1, std::floating_point F2>
54+
auto Simpson(Func f, F1 a, F2 b)
55+
-> decltype(std::declval<F1>() + std::declval<F2>()) {
56+
using ReturnType = decltype(std::declval<F1>() + std::declval<F2>());
57+
58+
ReturnType mid = a + (b - a) / 2.0;
59+
ReturnType result = f(a) + f(b) + f(mid) * 4.0;
60+
result *= (b - a);
61+
result /= 6.0;
62+
63+
return result;
64+
}
65+
66+
/**
67+
* @brief Adaptive Simpson's Rule (ASR) for numerical integration.
68+
*
69+
* Adaptive Simpson's Rule is a recursive method for approximating the definite
70+
* integral of a function. It dynamically refines the integration interval by
71+
* subdividing it into smaller segments where the function exhibits more
72+
* variation, and uses coarse segments where the function is smooth. This allows
73+
* for efficient computation by minimizing the number of function evaluations
74+
* while achieving the desired accuracy.
75+
*
76+
* The method compares the Simpson's rule estimates on the current segment and
77+
* its two halves. If the difference between the composite estimate and the
78+
* whole segment estimate is within the error tolerance, the result is accepted.
79+
* Otherwise, the algorithm recurses on both halves.
80+
*
81+
* @tparam Func Callable type. Must be invocable with a floating-point argument
82+
* and return a value convertible to a floating-point type.
83+
* @tparam F1 Floating-point type for the lower bound.
84+
* @tparam F2 Floating-point type for the upper bound.
85+
* @tparam Ferr Floating-point type for the error tolerance.
86+
*
87+
* @param f The function to be integrated. Must be continuous on [a, b].
88+
* @param a Lower limit of integration.
89+
* @param b Upper limit of integration.
90+
* @param error Maximum allowable error tolerance for the integral
91+
* approximation.
92+
*
93+
* @return Approximation of the integral ∫_{a}^{b} f(x) dx.
94+
* The return type is deduced as the common type of F1 and F2.
95+
*
96+
* @note The function uses recursion and may cause stack overflow for extremely
97+
* tight tolerances or highly oscillatory functions over large intervals.
98+
* @note Requires that `f` be defined and continuous on [a, b].
99+
*
100+
* Example usage:
101+
* @code{.cpp}
102+
* auto result = ASR([](double x) { return std::sin(x); }, 0.0, 3.14159, 1e-6);
103+
* @endcode
104+
*/
105+
template <typename Func, std::floating_point F1, std::floating_point F2,
106+
std::floating_point Ferr>
107+
auto ASR(Func f, F1 a, F2 b, Ferr error)
108+
-> decltype(std::declval<F1>() + std::declval<F2>()) {
109+
using ReturnType = decltype(std::declval<F1>() + std::declval<F2>());
110+
ReturnType mid = a + (b - a) / 2.0;
111+
112+
// 先对整体使用辛普森法
113+
auto S0 = Simpson(f, a, b);
114+
// 再将区间分成两块求解
115+
auto S1 = Simpson(f, a, mid);
116+
auto S2 = Simpson(f, mid, b);
117+
118+
// 求解此时的误差
119+
Ferr curr_error = std::abs(S0 - (S1 + S2));
120+
121+
// 误差如果超过最大允许范围,
122+
// (因为在返回过程中, 差值会被除以15,
123+
// 所以curr_error是15倍的当前误差)
124+
// 那么就递归求解两侧的面积,
125+
// 同时要求两侧的误差不超过最大误差的一半.
126+
// 否则就直接返回结果.
127+
if (curr_error >= 15.0 * error) {
128+
ReturnType result =
129+
ASR(f, a, mid, error / 2.0) + ASR(f, mid, b, error / 2.0);
130+
return result;
131+
} else {
132+
ReturnType result = (S1 + S2) + (S1 + S2 - S0) / 15.0;
133+
return result;
134+
}
135+
}

str/manacher.cpp

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @file manacher.cpp
3+
* @brief
4+
* @author Haoming Bai <haomingbai@hotmail.com>
5+
* @date 2025-08-21
6+
*
7+
* Copyright © 2025 Haoming Bai
8+
* SPDX-License-Identifier: MIT
9+
*
10+
* @details
11+
*/
12+
13+
#include <algorithm>
14+
#include <array>
15+
#include <concepts>
16+
#include <vector>
17+
18+
#include "../concepts.cpp"
19+
20+
struct ManacherResult {
21+
long start, len;
22+
};
23+
24+
template <std::equality_comparable E, RandomStdContainer<E> Container>
25+
ManacherResult Manacher(Container &&str) {
26+
if (str.empty()) {
27+
return {.start = -1, .len = 0};
28+
}
29+
30+
if (str.size() == 1) {
31+
return {.start = 0, .len = 1};
32+
}
33+
34+
enum Type : bool { ODD, EVEN };
35+
36+
std::array<std::vector<long>, 2> palindrome_lens{
37+
std::vector<long>(str.size(), 1), std::vector<long>(str.size() - 1, 0)};
38+
39+
// 先讨论奇数长度的最长回文串.
40+
{
41+
// 这两个变量表示当前最靠右的回文串的左右边界.
42+
// 初始状态下, 因为单字符是回文串,
43+
// 所以二者下标为0, 因为第0个字符为中心的最长回文串就是str[0].
44+
long L = 0, R = 0;
45+
46+
for (long i = 1; i < str.size(); i++) {
47+
// 这个curr_len表示从这个长度开始扩张.
48+
long curr_len;
49+
50+
// 分两种情况讨论, 如果被遍历到的i
51+
// 在当前最右的回文串内部, 那么可以从对称位置开始扩张.
52+
// 如果不在, 那么直接用朴素算法开始扩张.
53+
if (i <= R) {
54+
// 这个时候满足:
55+
// R - i = mirror_i - L
56+
auto mirror_i = R - i + L;
57+
58+
curr_len = std::min(palindrome_lens[ODD][mirror_i], (R - i) * 2 + 1);
59+
} else {
60+
curr_len = 1;
61+
}
62+
63+
auto curr_stretch = curr_len / 2;
64+
65+
while (i > curr_stretch && i + curr_stretch + 1 < str.size()) {
66+
// 尝试把边界延伸1
67+
// 如果新加入的两个字符相等, 则还能延伸1
68+
if (str[i + curr_stretch + 1] == str[i - curr_stretch - 1]) {
69+
curr_stretch++;
70+
} else {
71+
break;
72+
}
73+
}
74+
75+
auto curr_l = i - curr_stretch, curr_r = i + curr_stretch;
76+
// 如果延伸出去了, 就更新L和R
77+
if (curr_r > R) {
78+
L = curr_l, R = curr_r;
79+
}
80+
81+
// 更新长度.
82+
curr_len = curr_stretch * 2 + 1;
83+
palindrome_lens[ODD][i] = curr_len;
84+
}
85+
}
86+
87+
// 偶数长度的串, 情况要复杂很多,
88+
// 这里朴素算法只能默认从0长度开始.
89+
{
90+
long L = 0, R = -1;
91+
92+
for (long i = 0; i < str.size() - 1; i++) {
93+
// 这个curr_len表示从这个长度开始扩张.
94+
long curr_len;
95+
96+
if (i + 1 <= R) {
97+
// R - i = mirror_i + 1 - L
98+
auto mirror_i = R - i + L - 1;
99+
curr_len = std::min(palindrome_lens[EVEN][mirror_i], (R - i) * 2);
100+
} else {
101+
curr_len = 0;
102+
}
103+
104+
auto curr_stretch = curr_len / 2 - 1;
105+
while (i - curr_stretch > 0 && i + curr_stretch + 1 < str.size() - 1) {
106+
if (str[i + 1 + curr_stretch + 1] == str[i - curr_stretch - 1]) {
107+
curr_stretch++;
108+
} else {
109+
break;
110+
}
111+
}
112+
113+
auto curr_l = i + curr_stretch + 1, curr_r = i - curr_stretch;
114+
if (curr_r > R) {
115+
L = curr_l, R = curr_r;
116+
}
117+
118+
curr_len = (curr_stretch + 1) * 2;
119+
palindrome_lens[EVEN][i] = curr_len;
120+
}
121+
}
122+
123+
ManacherResult result;
124+
125+
{
126+
long start = 0, len = 1;
127+
for (long i = 0; i < str.size(); i++) {
128+
if (palindrome_lens[ODD][i] > len) {
129+
auto curr_stretch = palindrome_lens[ODD][i] / 2;
130+
start = i - curr_stretch;
131+
len = palindrome_lens[ODD][i];
132+
}
133+
}
134+
for (long i = 0; i < str.size() - 1; i++) {
135+
if (palindrome_lens[EVEN][i] > len) {
136+
auto curr_stretch = palindrome_lens[EVEN][i] / 2 - 1;
137+
start = i - curr_stretch;
138+
len = palindrome_lens[EVEN][i];
139+
}
140+
}
141+
142+
result = {.start = start, .len = len};
143+
}
144+
145+
return result;
146+
}

0 commit comments

Comments
 (0)