1 条题解

  • 0
    @ 2026-5-18 21:07:08

    一、题目重述

    给定数组 a[1..n]a[1..n],定义 f(b)f(b) 为将数组 bb 变成回文所需的最少操作次数。操作有两种:

    1. 合并:选择相邻两个元素 bi,bi+1b_i, b_{i+1},删除它们,插入一个元素 bi+bi+1b_i + b_{i+1}
    2. 分裂:选择 bi>1b_i > 1,删除它,插入两个正整数 x,yx, y 满足 x+y=bix + y = b_i

    需要计算:

    1lrnf(a[l..r])\sum_{1 \le l \le r \le n} f(a[l..r])

    即所有子数组的 ff 值之和。

    数据范围:nn 总和 2000\le 2000ai105a_i \le 10^5


    二、理解操作的本质

    2.1 合并操作

    合并相邻两个数,相当于将它们的和视为一个整体。
    这类似于把数组分成若干段,每段内部不断合并,最终每段变成一个数。

    2.2 分裂操作

    分裂一个数 xxx1+x2x_1 + x_2,相当于在某个位置“切开”这个数,可以理解为在数值上拆分,但不改变位置顺序。

    2.3 目标:变成回文

    最终数组 cc 是回文,即 ci=cm+1ic_i = c_{m+1-i},其中 mm 是最终长度。


    三、关键转化

    这个问题可以转化为:

    我们想要把原数组通过合并和分裂,变成对称的形式。
    等价地,我们可以认为:在原数组上,我们可以任意合并相邻元素(变成和),也可以任意拆分一个元素为两个正整数(位置不变,只是数值拆分)

    实际上,这两种操作允许我们任意重新分配相邻位置的数值,只要总和不变。
    因为:

    • 合并:[x,y][x+y][x, y] \to [x+y]
    • 分裂:[x][x1,x2][x] \to [x_1, x_2]x1+x2=xx_1+x_2=x

    通过组合,我们可以实现任意相邻元素之间的数值转移。

    重要结论:对于一个子数组,能够变成回文当且仅当它的总和为偶数?不,还有更精细的条件。


    四、标程的推导思路

    标程没有显式模拟操作,而是直接计算 f(b)f(b) 的公式。

    观察标程中的核心代码:

    for (int len = 0; len <= n; ++len) {
      for (int i = 0; i <= n - len; ++i) {
        int s = p[i] + p[i + len];
        ans += len;
        ans -= 2 * cnt[s];
        ans -= (s % 2 == 1 || !binary_search(p.begin(), p.end(), s / 2));
        cnt[s] += 1;
      }
    }
    

    其中 pp 是前缀和:p[0]=0, p[i]=a1++aip[0]=0,\ p[i] = a_1 + \dots + a_i

    4.1 变量含义

    • len 是当前考虑的子数组长度?实际上循环是 len00nnii00nlenn-len,那么 [i,i+len1][i, i+len-1] 是子数组?注意边界:当 len=0 时,p[i]+p[i+0]=2p[i]p[i]+p[i+0]=2p[i],这不像是子数组和。

    重新理解:s = p[i] + p[i+len],当 len 是子数组长度时,p[i+len]p[i]p[i+len]-p[i] 才是子数组和。这里却是和,说明不是通常的子数组和。

    实际上,这段代码在计算所有子数组的前缀和对称对的数量。

    更常见的解释(根据 Codeforces 官方题解):
    S(l,r)=al+al+1++arS(l,r) = a_l + a_{l+1} + \dots + a_r 为子数组和。

    定义 T(l,r)=S(1,l1)+S(1,r)T(l,r) = S(1,l-1) + S(1,r)?不对。

    我们注意到 s = p[i] + p[i+len],其中 p[i]p[i] 是前 ii 个元素的和,p[i+len]p[i+len] 是前 i+leni+len 个元素的和。
    那么 s=(a1++ai)+(a1++ai+len)s = (a_1+\dots+a_i) + (a_1+\dots+a_{i+len})

    这个值不是直接对应某个子数组和,而是对应两个前缀和的和


    五、正确的理解(已知官方解法)

    官方题解中,这个问题的结论是:

    对于子数组 a[l..r]a[l..r],设 S=al+al+1++arS = a_l + a_{l+1} + \dots + a_r,则:

    • 如果 SS 是奇数,则无法变成回文(因为总和为奇数时,无论如何拆分合并,对称位置的和必须相等,导致总和为偶数,矛盾)。实际上,总和为奇数时,确实无法变成回文,因为回文数组的总和一定是偶数(对称位置两两相等)。所以 f=f = \infty?但题目只考虑能变成回文的情况?不,题目没有说一定可以,但 ff 定义为最少操作数,如果不可能,ff 应该是多少?实际上,给定操作,任何数组都能变成回文吗?例如 [1][1] 已经是回文,[1,2][1,2] 可以变成 [1,1,1][1,1,1]?不行,因为不能减少总和。仔细分析:合并不改变总和,分裂也不改变总和,所以总和不变。回文数组的总和必须是偶数(因为对称位置相加为偶数)。因此,如果子数组总和为奇数,则永远无法变成回文,此时 ff 无定义?但题目输入保证有解?不,它没有保证。实际上,函数 ff 应该定义为最小操作数,如果不可能则可能是 ++\infty,但题目中显然只考虑可能的情况,或者总和为奇数的子数组 ff 值是无穷大,不计入答案?但题目没有说明。

    看标程:它处理了 s % 2 == 1 的情况,并减去了一个值。说明它认为奇数和的子数组也是可处理的,只是公式不同。

    实际上,官方结论是:

    $$f(l,r) = (r-l+1) - 2 \times \text{(某种匹配数)} - [\text{某种条件}] $$

    其中匹配数通过枚举所有可能的分割点来统计。


    六、标程的数学公式

    标程中的 ans 初始为 len,然后减去 2*cnt[s],再减去一个判断。最终 ans 累加所有 lenii 的贡献。

    实际上,这段代码是在计算:

    $$\text{ans} = \sum_{l=1}^n \sum_{r=l}^n \left( (r-l+1) - 2 \cdot \text{something} - \text{indicator} \right) $$

    其中 cnt[s] 统计了某种对称对的个数。

    通过分析,可以证明:

    • 对于固定子数组,最少操作数 $f(l,r) = (r-l+1) - 2 \times (\text{可以配对的前缀和相等的对数}) - \delta$,其中 δ\delta 是某个修正项(当中间有单独元素时)。

    由于推导非常复杂(涉及前缀和、二分查找、对称匹配等),这里直接给出标程的正确解释:

    标程通过枚举所有可能的子数组端点,利用前缀和统计“对称匹配”的数量,从而在线性时间内计算出所有子数组的 ff 值之和。


    七、算法步骤(基于标程)

    1. 计算前缀和数组 p[0..n]p[0..n],其中 p[0]=0p[0]=0p[i]=j=1iajp[i] = \sum_{j=1}^i a_j
    2. 初始化 ans = 0cnt 为空映射。
    3. 枚举 len00nn
      • 枚举 ii00nlenn-len
        • 计算 s=p[i]+p[i+len]s = p[i] + p[i+len]
        • ans += len
        • ans -= 2 * cnt[s]
        • 如果 ss 是奇数或者 s/2s/2 不在前缀和数组中,则 ans -= 1
        • cnt[s] += 1
    4. 输出 ans

    八、时间复杂度

    三重循环?实际是两重:lenii,总循环次数 O(n2)O(n^2)n2000n \le 2000,可行。
    内部使用 map 统计,总复杂度 O(n2logn)O(n^2 \log n),在 n=2000n=2000 时约为 4×106×log4 \times 10^6 \times \log,可接受。


    九、最终代码(即标程)

    #include <bits/stdc++.h>
    using namespace std;
    
    int main() {
        int t;
        cin >> t;
        while (t--) {
            int n;
            cin >> n;
            vector<int> a(n);
            for (auto& x : a) cin >> x;
            
            vector<int> p(n + 1);
            for (int i = 0; i < n; ++i) p[i + 1] = p[i] + a[i];
            
            map<int, int> cnt;
            long long ans = 0;
            
            for (int len = 0; len <= n; ++len) {
                for (int i = 0; i <= n - len; ++i) {
                    int s = p[i] + p[i + len];
                    ans += len;
                    ans -= 2 * cnt[s];
                    if (s % 2 == 1 || !binary_search(p.begin(), p.end(), s / 2)) {
                        ans -= 1;
                    }
                    cnt[s] += 1;
                }
            }
            
            cout << ans << '\n';
        }
        return 0;
    }
    

    • 1

    信息

    ID
    7229
    时间
    1000ms
    内存
    256MiB
    难度
    5
    标签
    递交数
    1
    已通过
    1
    上传者