洛谷 P1729 计算e 的分治解法

计算自然常数 e \mathrm{e} e 有多种办法,前人有很多研究。其中使用Maclaurin公式把 f ( x ) = e x f(x)=\mathrm{e}^x f(x)=ex 展开,并令 x = 1 x=1 x=1 可以得到 e \mathrm{e} e 的无穷逼近式:

e = 1 + 1 1 ! + 1 2 ! + 1 3 ! + ⋯ + 1 n ! + ⋯ = 1 + ∑ n = 1 ∞ 1 n ! \mathrm{e}=1+\frac{1}{1!}+\frac{1}{2!}+\frac{1}{3!}+\cdots +\frac{1}{n!}+\cdots =1+\sum_{n=1}^{\infty}{\frac{1}{n!}} e=1+1!1+2!1+3!1++n!1+=1+n=1n!1

记上式为 ( 1 ) (1) (1) 式。已有的研究表明 ( 1 ) (1) (1) 式收敛得很快[1] ,于是此题我们用 ( 1 ) (1) (1) 式,也就是题目给出的公式计算e。

拿到 ( 1 ) (1) (1) 式,首先要考虑的是 n n n 到底要取到多大才能满足题目所需的精度?这个问题 TBB_Nozomi 在他的题解中给出了详细的分析,这里引用他的结论:

要想使用 ( 1 ) (1) (1) 式计算 e \mathrm{e} e n n n 位有效数字, n n n 应满足
1 0 n < n ! 10^n<n! 10n<n!

两边取对数,有

n ln ⁡ 10 < ∑ k = 1 n ln ⁡ k n\ln 10<\sum_{k=1}^n{\ln k} nln10<k=1nlnk

于是 n n n 可以编程求解,代码如下:

int get_n(int d)
{
    double f = d * log(10);
    double r = 0;
    int i = 0;
    do
    {
        i++;
        r += log(i);
    } while (r < f);
    return i;
}

由于高精度除法的消耗很多,可以预测的是,如果直接用 ( 1 ) (1) (1) 式暴力计算 e \mathrm{e} e ,是一定会 TLE 的。于是我们要把 ( 1 ) (1) (1) 式通分:

e n = 1 + ∑ k = 1 n 1 k ! = 1 + 1 n ! ∑ k = 0 n − 1 A n k , \mathrm{e}_n=1+\sum_{k=1}^n{\frac{1}{k!}}=1+\frac{1}{n!}\sum_{k=0}^{n-1}{A_{n}^{k}}, en=1+k=1nk!1=1+n!1k=0n1Ank

使得原本要调用 n n n 次的高精度除法降为仅需 1 1 1 次。代码如下:

void euler_tongfen(int d)
{
    int n = get_n(d);
    mpz_class p = 1, q = 1; //mpz_class表示高精度整数类
    for (int i = n; i > 1; i--)
    {
        q *= i; //高精度×单精度,时间复杂度为O(n)
        p += q;
    }
    mpf_class r = p; //mpq_class表示高精度浮点数类
    r /= q;          //高精度除法
    ++r;             //别忘了+1
}

显然上述算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。按照 TBB_Nozomi 的说法,只要高精度算法实现得没问题,上面的代码再加上一些格式化输出就能通过这题。下面是我用 TBB_Nozomi 的高精度整数板子,在他的题解代码的基础上修改得来的 AC 代码。

int main()
{
    int k;
    cin >> k;
    tbb::LInt S = 1, P = 1;
    int N = get_n(k);
    for (int i = N; i > 0; i--) 
    {
        P *= i;
        S += P;
    }
    S <<= k / 4 + 2;
    S /= P;
    string ans = S.print_str();
    const char* out = ans.c_str();
    putchar('2'); putchar('.'); putchar('\n');
    for (int T = 1; T <= k; ++T) {
        putchar(out[T]);
        if (T % 50 == 0) putchar('\n');
        else if (T % 10 == 0) putchar(' ');
    }
    return 0;
}

这个链接是我的评测详情,开启了 O2优化,总用时 255 255 255 ms。不开启 O2则需要 556 556 556ms。

上述代码还有优化的可能:

①对于“高精度×单精度”的算法,它是 O ( n ) O(n) O(n) 的,在输入较小时它是十分快的,于是我们的优化思路是采用二分法对表达式分块处理,在表达式长度缩短到某一阈值的时候就采用通分的方式进行计算,然后使用时间复杂度低于 O ( n 2 ) O(n^2) O(n2) 的大整数乘法(FFT,Karatsuba,Toom Cook 3-Way)向上合并,最终得到结果。

②对高精度除法进行优化,这就是另外一个问题了,可以查看P5432 A/B problem的题解。

如何进行分块处理呢?为了方便讨论,记

E ( n , m ) = 1 n + 1 n ( n + 1 ) + ⋯ + 1 n ( n + 1 ) ( n + 2 ) ⋯ m E\left( n,m \right) =\frac{1}{n}+\frac{1}{n\left( n+1 \right)}+\cdots +\frac{1}{n\left( n+1 \right) \left( n+2 \right) \cdots m} E(n,m)=n1+n(n+1)1++n(n+1)(n+2)m1

于是 e = 1 + E ( 1 , + ∞ ) \mathrm{e}=1+E(1,+\infty) e=1+E(1,+) 。令 r = ⌊ n + m 2 ⌋ r=\lfloor \frac{n+m}{2} \rfloor r=2n+m,那么对于 E ( n , m ) E(n,m) E(n,m)

E ( n , m ) = E ( n , r ) + 1 n ( n + 1 ) ⋯ r ⋅ E ( r + 1 , m ) E\left( n,m \right) =E\left( n,r \right) +\frac{1}{n\left( n+1 \right) \cdots r}\cdot E\left( r+1,m \right) E(n,m)=E(n,r)+n(n+1)r1E(r+1,m)

再令 p 1 q 1 = E ( n , r ) \frac{p_1}{q_1}=E\left( n,r \right) q1p1=E(n,r) p 2 q 2 = E ( r + 1 , m ) \frac{p_2}{q_2}=E\left( r+1,m \right) q2p2=E(r+1,m),显然 q 1 = n ( n + 1 ) ⋯ r q_1=n(n+1)\cdots r q1=n(n+1)r,于是

E ( n , m ) = p 1 q 1 + 1 q 1 ⋅ p 2 q 2 = p 1 q 2 + p 2 q 1 q 2 E\left( n,m \right) =\frac{p_1}{q_1}+\frac{1}{q_1}\cdot \frac{p_2}{q_2}=\frac{p_1q_2+p_2}{q_1q_2} E(n,m)=q1p1+q11q2p2=q1q2p1q2+p2

算法如下图所示

算法递归树

每做一次合并,需要两次乘法,一次加法。显然,若“高精×高精”的算法复杂度仍为 O ( n 2 ) O(n^2) O(n2) ,那么总体的复杂为

T ( n ) = n 2 2 + n 2 4 + n 2 8 + ⋯ = n 2 ( 1 2 + 1 4 + 1 8 + ⋯   ) = O ( n 2 ) T\left( n \right) =\frac{n^2}{2}+\frac{n^2}{4}+\frac{n^2}{8}+\cdots =n^2\left( \frac{1}{2}+\frac{1}{4}+\frac{1}{8}+\cdots \right) =O\left( n^2 \right) T(n)=2n2+4n2+8n2+=n2(21+41+81+)=O(n2)

与原算法基本持平,甚至还要稍慢一些。于是只要“高精×高精“的算法复杂度低于 O ( n 2 ) O(n^2) O(n2) ,那么总体的复杂度就比原来的更优。

那么只剩下最后一个问题了,阈值的选取,也就是 E ( n , m ) E(n,m) E(n,m) 多短我们才开始用通分法计算呢?遗憾的是,不同的处理器,不同的算法实现效率都会影响阈值的选取,要找到阈值最简单的方法是通过暴力搜索的方式得到。在我的机器上这个值大约在 120 120 120 左右。在洛谷上使用阈值为 128 128 128 的新算法用时 161 161 161ms,评测详情

以下是新的 AC 代码。

int MIN_SPLIT = 128;
static void euler_split(int n, int m, LInt& p, LInt& q)
{
    if (m - n < MIN_SPLIT)
    {
        p = 1;
        q = 1;
        for (int i = m; i > n; i--)
        {
            q *= i;
            p += q;
        }
        q *= n;
        return;
    }
    LInt p1, p2, q1, q2;
    euler_split(n, (n + m) >> 1, p1, q1);
    euler_split((n + m + 2) >> 1, m, p2, q2);
    p = p1 * q2 + p2;
    q = q1 * q2;
}

int main()
{
    int k;
    cin >> k;
    LInt p, q;
    int n = get_n(k);
    euler_split(1, n, p, q);
    p += q;
    p <<= k / 4 + 2;
    p /= q;
    string ans = p.print_str();
    const char* out = ans.c_str();
    putchar('2');    putchar('.');    putchar('\n');
    for (int T = 1; T <= k; ++T) {
        putchar(out[T]);
        if (T % 50 == 0)    putchar('\n');
        else if (T % 10 == 0)    putchar(' ');
    }
    return 0;
}

[1] 朱德凯, 苏岐芳. 多种计算方法下自然常数e的误差和收敛速度比较[J]. 台州学院学报, 2022, 44(03): 11-16. DOI: 10.13853/j.cnki.issn. 1672-3708. 2022. 03. 003.

Subtask #1 5ms/2.75MB WA #1 Wrong Answer. 5ms/2.74MB WA #2 Wrong Answer. Subtask #2 139ms/10.61MB AC #3 Accepted, 得分 10. Subtask #3 212ms/12.27MB WA #4 Wrong Answer. Subtask #4 279ms/15.58MB WA #5 Wrong Answer. Subtask #5 229ms/12.21MB WA #6 Wrong Answer. Subtask #6 7ms/2.91MB WA #7 Wrong Answer. 7ms/2.74MB WA #8 Wrong Answer. 7ms/2.75MB WA #9 Wrong Answer. 7ms/2.78MB WA #10 Wrong Answer. 7ms/2.74MB WA #11 Wrong Answer. 7ms/2.75MB WA #12 Wrong Answer. 7ms/2.75MB WA #13 Wrong Answer. 12ms/2.77MB WA #14 Wrong Answer. 16ms/2.92MB WA #15 Wrong Answer. 8ms/2.85MB WA #16 Wrong Answer. Subtask #7 12ms/3.00MB WA #17 Wrong Answer. 12ms/3.10MB WA #18 Wrong Answer. 12ms/3.00MB WA #19 Wrong Answer. 12ms/3.02MB WA #20 Wrong Answer. 12ms/2.98MB WA #21 Wrong Answer. 14ms/3.00MB WA #22 Wrong Answer. 22ms/3.01MB WA #23 Wrong Answer. 56ms/3.10MB WA #24 Wrong Answer. 54ms/3.11MB WA #25 Wrong Answer. 20ms/3.14MB WA #26 Wrong Answer. Subtask #8 168ms/11.67MB WA #27 Wrong Answer. 588ms/16.13MB WA #28 Wrong Answer. 245ms/8.08MB WA #29 Wrong Answer. 590ms/16.70MB WA #30 Wrong Answer.#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef vector<int> vi; int n, m; ll k; vector<int> adj[100001]; void bfs(int s, vi& dist) { dist.assign(n + 1, -1); queue<int> q; dist[s] = 0; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); for (int v : adj[u]) { if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); } } } } set<ll> get_all_sp_edges(int s, int t, const vi& dist) { set<ll> edges; vector<bool> visited(n + 1, false); queue<int> q; q.push(t); visited[t] = true; while (!q.empty()) { int u = q.front(); q.pop(); for (int v : adj[u]) { if (dist[v] == dist[u] - 1) { // 关键:允许所有前驱 int a = min(u, v), b = max(u, v); ll key = 1LL * a * 1000000 + b; edges.insert(key); if (!visited[v]) { visited[v] = true; q.push(v); } } } } return edges; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while (T--) { cin >> n >> m >> k; for (int i = 1; i <= n; i++) adj[i].clear(); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); } int sx1, sy1, sx2, sy2; cin >> sx1 >> sy1 >> sx2 >> sy2; vi da, db, dc, dd; bfs(sx1, da); bfs(sy1, db); bfs(sx2, dc); bfs(sy2, dd); if (da[sy1] == -1 || dc[sy2] == -1) { cout << "-1\n"; continue; } int lenA = da[sy1]; int lenB = dc[sy2]; auto ea = get_all_sp_edges(sx1, sy1, da); auto eb = get_all_sp_edges(sx2, sy2, dc); set<ll> common; for (ll e : ea) if (eb.count(e)) common.insert(e); ll c = common.size(); double total = lenA + lenB; if (c > 0 && k > 0) { total = total - 2.0 * c + (2.0 * c * c) / (c + k); } cout << fixed << setprecision(12) << max(total, 1e-9) << '\n'; } return 0; }
10-27
### 关于 P5463 题目“小鱼比可爱(加强版)”的分治算法解题思路 #### 问题分析 P5463 是一道涉及数组操作和查询的经典问题。题目要求支持两种操作: 1. **修改操作**:将某个区间内的数加上一个常量。 2. **查询操作**:询问某两个位置之间的差值绝对值的最大值。 由于数据规模较大,暴力求解显然无法满足时间复杂度的要求。因此,可以考虑利用分治的思想来设计高效的解决方案[^1]。 --- #### 分治算法的核心思想 分治算法通过将大问题分解成若干个小问题分别解决,最终合并子问题的结果得到原问题的答案。对于本题: 1. **划分阶段**:将整个序列划分为多个小区间,并递归处理这些区间的最大最小值变化情况。 2. **递归阶段**:针对每个小区间,计算其内部以及跨越边界的可能答案。 3. **合并阶段**:当左右两部分被独立处理完成后,需额外关注跨过中间点的情况,即左半部分的最大/最小值与右半部分的最大/最小值组合产生的潜在最优解。 具体实现上可以通过线段树或者基于分治的离线莫队等方式完成高效维护和查询。 --- #### 实现细节 以下是具体的分治策略及其对应的伪代码描述: 1. **预处理**: - 使用辅助结构存储每次修改的影响范围及增量信息。 - 初始化全局变量记录当前状态下的最值分布。 2. **核心逻辑**: ```python def divide_and_conquer(l, r): if l == r: # 基础情形:单个元素无需进一步分割 update_single_element(l) return max_val[l], min_val[l] mid = (l + r) // 2 # 对左侧区间执行分治 left_max, left_min = divide_and_conquer(l, mid) # 对右侧区间执行分治 right_max, right_min = divide_and_conquer(mid + 1, r) # 合并两侧结果 global_max = max(left_max, right_max) global_min = min(left_min, right_min) # 考虑跨越边界的情形 cross_boundary_result = abs(global_max - global_min) return global_max, global_min ``` 3. **查询优化**: - 利用懒标记技术减少重复计算开销,在大规模动态更新场景下尤为有效。 - 结合二分查找快速定位目标区域中的极值候选者。 --- #### 时间复杂度分析 假设输入长度为 \( n \),则上述方法的时间复杂度主要由以下几个方面决定: - 单次分治调用耗时 \( O(\log n) \),因为每层递归都将问题缩小一半。 - 总共需要进行 \( O(n) \) 次递归调用以覆盖全部节点。 综上可得整体复杂度约为 \( O(n \log n) \)。 --- #### 注意事项 实际编码过程中需要注意以下几点: - 边界条件的严格判断以防越界访问错误; - 数据类型的合理选用防止溢出风险; - 修改操作影响传播机制的设计确保一致性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值