交替

GitHub跳转原题关系图返回列表

通过位移算子将差分与相邻求和转化为多项式乘法,利用 (1+x) 和 (1-x) 的组合找出规律,从而做到 $O(n)$ 的线性求解。

OJ: roj

题目 ID: 10017

难度:提高+/省选-

标签:组合数学数学

创建: 2026-10-10 07:11

更新: 2026-10-10 07:11

形式化题目

给定一个长度为 nn 的数组 AA。每一秒钟,数组会通过变换变成长度减少 11 的新数组 A′A',变换规则如下:

  1. 如果当前数组 AA 的长度 nn 为偶数,则 A′[i]=A[i]+A[i+1]A'[i] = A[i] + A[i+1](1≤i<n1 \le i < n)。
  2. 如果当前数组 AA 的长度 nn 为奇数,则 A′[i]=A[i]−A[i+1]A'[i] = A[i] - A[i+1](1≤i<n1 \le i < n)。

经过 n−1n-1 秒后,数组只剩下一个数字。求该数字对 109+710^9+7 取模后的结果。

数据范围

  • 对于 40% 的数据,满足 n≤1000n \le 1000。
  • 对于 100% 的数据,满足 1≤n≤1051 \le n \le 10^5,1≤Ai≤1091 \le A_i \le 10^9。

其他约束

  • 题面未给出具体的时空限制,常规可假设为 1000ms / 128MB(config.json 的 "memory": 128)。
  • 输入为单组数据。第一行为 nn,接下来一行为 nn 个整数。

正解

思路

朴素的做法是直接模拟上述过程,因为每次操作长度减 11,总共进行 n−1n-1 次操作,每次操作需要遍历数组,所以总时间复杂度为 O(n2)O(n^2),可以拿到 40%40\% 的分数。 由于 n≤105n \le 10^5,我们要寻找更低复杂度的算法。

本题可以通过多项式和位移算子 EE(满足 EAi=Ai+1E A_i = A_{i+1})来简化操作:

  • 长度为偶数时的操作(A[i]+A[i+1]A[i]+A[i+1])相当于对原数组乘上多项式 1+E1+E。
  • 长度为奇数时的操作(A[i]−A[i+1]A[i]-A[i+1])相当于对原数组乘上多项式 1−E1-E。

最后数组剩下的唯一一个元素,相当于把原数组 AA 作用了一系列多项式的乘积 P(E)P(E) 后取其首项,即为 ∑j=0n−1cjAj+1\sum_{j=0}^{n-1} c_j A_{j+1},其中 cjc_j 是多项式 P(x)P(x) 中 xjx^j 的系数。

我们分析 P(x)P(x) 的规律。由于数组长度从 nn 逐渐递减到 22,奇偶性交替出现:

  • 如果 nn 是奇数(设 n=2k+1n = 2k + 1): 操作的多项式依次为 (1−x)(1-x),(1+x)(1+x),(1−x)(1-x),(1+x)(1+x) … 并以 (1+x)(1+x) 结尾。 总共有 2k2k 步,恰好包含了 kk 次 (1−x)(1-x) 和 kk 次 (1+x)(1+x)。 所以 P(x)=((1−x)(1+x))k=(1−x2)kP(x) = ( (1-x)(1+x) )^k = (1-x^2)^k。 将式子二项式展开,非零项只能是偶数次项 x2px^{2p},其系数为 (−1)p(kp)(-1)^p \binom{k}{p}。奇数次项系数则为 00。

  • 如果 nn 是偶数(设 n=2kn = 2k): 操作的多项式依次为 (1+x)(1+x),(1−x)(1-x),(1+x)(1+x) … 并以 (1+x)(1+x) 结尾。 总共有 2k−12k-1 步,包含了 kk 次 (1+x)(1+x) 和 k−1k-1 次 (1−x)(1-x)。 所以 P(x)=((1+x)(1−x))k−1⋅(1+x)=(1−x2)k−1(1+x)P(x) = ( (1+x)(1-x) )^{k-1} \cdot (1+x) = (1-x^2)^{k-1} (1+x)。 展开后可以看出,x2px^{2p} 和 x2p+1x^{2p+1} 拥有相同的系数,均为 (−1)p(k−1p)(-1)^p \binom{k-1}{p}。

利用上述公式,我们可以很轻松地通过组合数直接求出 AA 数组中每个元素最终在答案中的系数。提前 O(n)O(n) 预处理阶乘以及逆元后,就能在 O(n)O(n) 时间内算出最终答案。

代码

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-10 08:04
# update_at: 2026-10-10 08:42

import sys

MOD = 1_000_000_007
type FactList = list[int]  # 组合数预处理数组,下标 i 存 i! 或其逆元


def build_combinations(n: int) -> tuple[FactList, FactList]:
    """预处理阶乘与阶乘逆元,用来 O(1) 求组合数。"""
    fact = [1] * (n + 1)
    inv_fact = [1] * (n + 1)
    for i in range(1, n + 1):
        fact[i] = fact[i - 1] * i % MOD
    inv_fact[n] = pow(fact[n], MOD - 2, MOD)
    for i in range(n - 1, -1, -1):
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD
    return fact, inv_fact


def coefficient(n: int, pos: int, fact: FactList, inv_fact: FactList) -> int:
    """第 pos 个数(从 1 开始)在最终答案中的系数:C(⌈n/2⌉-1, ⌈pos/2⌉-1) 带 (−1)^(⌈pos/2⌉-1)。"""
    if n % 2 == 1 and pos % 2 == 0:
        return 0  # n 为奇数时 (1−x²)^k 只有偶次项,奇数下标全为 0
    half_pos = (pos + 1) // 2
    half_n = (n + 1) // 2
    choose = fact[half_n - 1] * inv_fact[half_pos - 1] % MOD * inv_fact[half_n - half_pos] % MOD
    return -choose if half_pos % 2 == 0 else choose


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    try:
        n = next(data)
    except StopIteration:
        return

    fact, inv_fact = build_combinations(n)
    values = [next(data) for _ in range(n)]
    answer = sum(value * coefficient(n, pos, fact, inv_fact)
                 for pos, value in enumerate(values, 1)) % MOD
    print(answer)


if __name__ == "__main__":
    solve()
cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-10 08:04
 * update_at: 2026-10-10 08:40
 */
#include <iostream>
using namespace std;

typedef long long ll;

const ll MOD = 1000000007LL;
const int MAXN = 200000 + 5;

ll fact[MAXN];     // fact[i] = i! mod MOD
ll inv_fact[MAXN]; // inv_fact[i] = (i!)^{-1} mod MOD

ll quick_power(ll base, ll exp) {
    ll res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) {
            res = res * base % MOD;
        }
        base = base * base % MOD;
        exp /= 2;
    }
    return res;
}

void init_comb(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n; i++) {
        fact[i] = fact[i - 1] * i % MOD;
    }
    inv_fact[n] = quick_power(fact[n], MOD - 2);
    for (int i = n - 1; i >= 0; i--) {
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
    }
}

ll comb(int n, int m) {
    if (m < 0 || m > n) {
        return 0;
    }
    return fact[n] * inv_fact[m] % MOD * inv_fact[n - m] % MOD;
}

// 第 pos 个数(从 1 开始)在最终答案中的系数。
ll coefficient(int n, int pos) {
    if ((n % 2 == 1) && (pos % 2 == 0)) {
        return 0;
    }
    int half_pos = (pos + 1) / 2;
    int half_n = (n + 1) / 2;
    ll c = comb(half_n - 1, half_pos - 1);
    if (half_pos % 2 == 0) {
        return -c;
    }
    return c;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    if (!(cin >> n)) {
        return 0;
    }

    init_comb(n);

    ll ans = 0;
    for (int i = 1; i <= n; i++) {
        ll a = 0; // 预置 0:输入被截断时不读未初始化值
        cin >> a;
        ans = (ans + (a % MOD) * coefficient(n, i)) % MOD;
    }

    cout << (ans + MOD) % MOD << '\n';
    return 0;
}

复杂度

  • 时间复杂度:O(n)O(n)。预处理阶乘及其逆元需要 O(n)O(n) 的时间(加上一次求逆元的 O(log⁡MOD)O(\log MOD)),随后遍历计算每个元素的系数并累加同样耗时 O(n)O(n),总体时间复杂度 O(n)O(n)。
  • 空间复杂度:O(n)O(n)。需要大小为 O(n)O(n) 的数组来存储原数据序列,以及预处理的阶乘与逆元。

总结

  • 这是一道经典地利用位移算子和多项式,把多轮交替的差分与相邻求和问题转化为组合数求解的题目。
  • 解题关键是敏锐地发现多项式相乘后引发的奇偶消项效应:(1−x)(1+x)=1−x2(1-x)(1+x) = 1-x^2。
  • 代码实现细节:由于计算过程中系数有可能为负数,在 C++ 计算中应当正确处理对取模的影响,常采用 (val % MOD + MOD) % MOD。

验证

验证记录(R2,与官方 std.cpp 三方对拍)

⚠ 本题无本地 data/(data.json 的 files=[])⇒ check_new_analysis 报 「无 data/(跳过)」= 空转(T1/T65)。证据来源是官方 std.cpp(★★★★)。

结论

main.cpp ≡ std.cpp ≡ main.py,在全部六类证据上 0 分歧 ⇒ 与官方 std.cpp 三方一致 (★ 不能写成「验证通过」)。

★ T67 语义核对(第一优先级)—— 对专家描述的订正

结论:专家的「多项式乘法 (1-x)(1+x)=1-x²」属实,但表述不完整。

std.cpp(42 行)不是逐轮模拟,而是闭式组合公式:对第 i 项(1-based)返回

条件 结果
n 奇 且 i 偶 0((1-x²)^k 无奇次项)
否则 c = C(⌈n/2⌉−1, ⌈i/2⌉−1),符号 (−1)^(⌈i/2⌉−1)

代码为 cal():if(n&1 && !(m&1)) return 0; → m=(m+1)/2; n=(n+1)/2; now=comb(n-1,m-1); m%2==0 ? -now : now。

核对项 结果
① 循环结构 单组(cin>>n 一次);拼两组输入 ⇒ 只输出 1 行,main.cpp 同
② 输入变量是否都参与 n、a[] 都参与;无 no-op 变量
③ 输出格式 ans mod 1e9+7 一行,endl
④ 边界 n=1 ⇒ a1;n=2 ⇒ a1+a2;n=1..2000 系数向量与 main.cpp 逐项相同
⑤ UB ★ 空输入时 std 打印 679502094(int n 未初始化)⇒ std 自身 UB;
main.cpp 有 if(!(cin>>n)) return 0;,安全

六类证据(T14 分级)

手法 规模 结果
真进程三方对拍 380 组(n=1..20 ∪ {99,100,101,1000,1001} 定值 + 随机 n≤60,
含全 1e9、大质数 999999937、1+999999937 混合)
0 分歧
真进程三方(大规模) n ∈ {49999,50000,50001,77777,77778,99998,99999,100000} 0 分歧
同进程 harness:系数向量 n=1..2000 全部 i(≈200 万项)std_cal ≡ user_coef 0 不匹配
★ V6 异源 brute(逐轮模拟题面规则,非公式) n≤25 小值 2000 组 + 大值(≤1e9) 2000 组 0 分歧
★ 穷举(非抽样) n≤10、a_i ∈ {1,2,3} 全枚举 88,572 组 0 分歧
多行输入 / CRLF / 多余空白 — 三者输出均 1000000000 ✓
奇偶覆盖 奇 n、偶 n 均单独覆盖 ✓

V1 反事实表(300 组真进程对比 std)

变体 失败数 判定
关掉「奇 n 偶位置 ⇒ 0」 146/300(首个 n=3:1000000006 vs 1000000000) 承重
关掉负号(half_pos%2==0 → false) 286/300(首个 n=3:9) 承重
去掉 (ans+MOD)%MOD 148/300(首个 n=3:−7) 承重
(pos+1)/2 改成 pos/2 300/300(首个 n=1:0 vs 5) 承重
去掉 (a % MOD) 0/300 ★ 不承重(a ≤ 1e9 < MOD,恒等 no-op)
关掉 comb 越界检查 0/300 ★ 不承重(公式内 m ≤ n 恒成立)

★ 探针有效性:4 个变体各失败 146–300 次 ⇒ 「0 失败」的 V1d/V1e 是真 no-op, 不是探针失效(T35 判据鉴别力)。

V9 UBSan + 对照组 · T38

  • ★ 对照组(强制 INT_MIN 取反)确实报错 negation of -2147483648 cannot be represented… ⇒ 探针有效;main.cpp UBSan 跑多组(含 n=99999 全 1e9、n=1e5 随机)stderr 全空,-ftrapv rc=0
  • config.json:time=1000ms / memory=128MB。实测 main 0.01s / 8.4MB、 std 0.08s / 8.4MB、main.py 0.05s / 34MB ⇒ 全部通过,内存 ≥3.7× 余量

与 std.cpp 的逐行差异点(7 条,均不影响语义)

std.cpp main.cpp 影响
inv[] 线性递推求逆元 快速幂求 inv_fact[n] + 后缀递推 否(系数逐项相同)
maxn=2e5+5,init() 全表预处理 init_comb(n) 只算到 n 否(更快:0.01s vs 0.08s)
cin>>n 无检查 if(!(cin>>n)) return 0; 否(空输入安全性提升)
ll a; 未初始化 ll a = 0;(本次修改) 否(截断输入不读未初始化值)
a * cal(...) (a % MOD) * coefficient(...) 否(a<MOD,V1d 实测不承重)
(ans+mod)%mod (ans+MOD)%MOD 一致
单组输入 单组输入 一致

本次对 index.md 的两处修正(worker 提出,父会话机械核实后执行)

  1. ★ 「其他约束」原写 1000ms / **256MB**,而 config.json 是 "memory": **128** ⇒ 已改为 128MB(机械核实:grep '"memory"' config.json → 128)
  2. ★ 原「验证」节称「数据文件与官方 std 的推断结论完全一致」 —— 本题无本地 data/,该句无证据支撑(T65)⇒ 已删除该表述, 改为「与官方 std.cpp 在 388 组合成数据上三方一致(无本地 data/)」