[蓝桥杯 2023 省 A] 平方差

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

$x=y^2-z^2$ 等价于 $x$ 有同奇偶的因子对,即 $x$ 为奇数或 $4$ 的倍数;用前缀计数 $F(R)-F(L-1)$ 即可 $O(1)$ 求出答案。

OJ: luogu

题目 ID: P9231

难度:普及-

标签:数学数论思维计数前缀和

创建: 2026-10-02 15:07

更新: 2026-10-03 10:37

形式化题目

给定两个整数 L,RL,R(1⩽L⩽R⩽1091 \leqslant L \leqslant R \leqslant 10^9),统计区间 [L,R][L,R] 中有多少个整数 xx,满足存在整数 y,zy,z,使得

x=y2−z2.x = y^2 - z^2.

题面中的 y,zy,z 只需要是整数,可以取 00 或负数,这一点在后面推导时要用到。

样例 L=1,R=5L=1,R=5 的可行解如下:

xx 表示方式
11 12−021^2-0^2
33 22−122^2-1^2
44 22−022^2-0^2
55 32−223^2-2^2

22 不在其中,[1,5][1,5] 内共有 44 个满足条件的数。

暴力解法

思路

直接从平方差的形式出发不好枚举:y,zy,z 的取值没有天然上界。把它因式分解就能把问题变简单:

y2−z2=(y−z)(y+z).y^2 - z^2 = (y-z)(y+z).

记 a=y−z, b=y+za = y-z,\ b = y+z,那么 x=abx = ab,并且反过来

y=a+b2,z=b−a2.y = \frac{a+b}{2},\qquad z = \frac{b-a}{2}.

于是「xx 能写成平方差」完全等价于:

存在两个因子 a,ba,b,满足 ab=xab = x,且 a+ba+b 是偶数(也就是 a,ba,b 同奇偶),并且解出的 y,zy,z 是整数。

y,zy,z 允许为负、越小越负,所以因子 a,ba,b 允许为负也没关系;x>0x > 0 时只要考虑同号因子,枚举 1⩽a⩽x1 \leqslant a \leqslant \sqrt{x} 就覆盖了所有因子对(b=x/a⩾ab = x/a \geqslant a)。

这就是暴力解的全部逻辑:对区间里的每个 xx,枚举它不超过 x\sqrt{x} 的因子 aa,检查 a+x/aa + x/a 是否为偶数。

代码

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-03 10:37
 * update_at: 2026-10-03 10:37
 */
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 做法:把 x = y^2 - z^2 看成 (y-z)(y+z),令 a = y-z >= 1、b = y+z >= a,
// 则 a*b = x 且 a、b 必须同奇偶。反过来,只要找到同奇偶的因子对
// 就能还原 y = (a+b)/2、z = (b-a)/2,所以逐个 x 枚举 a <= sqrt(x) 即可判断。
// 复杂度 O((R-L+1) * sqrt(R)),只适合小范围数据,不能当作正解。
#include <iostream>
using namespace std;

typedef long long ll;

// 判断 x 能否写成两个整数平方的差(y,z 取非负整数即可覆盖所有情况)。
bool can_represent(ll x) {
    if (x == 0) {
        return true; // 取 y = z 即可
    }
    // 枚举较小的因子 a,b = x / a 一定满足 b >= a。
    for (ll a = 1; a * a <= x; a++) {
        if (x % a != 0) {
            continue;
        }
        ll b = x / a;
        if ((a + b) % 2 == 0) {
            return true; // a、b 同奇偶,可以还原出整数 y、z
        }
    }
    return false;
}

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

    ll L, R;
    cin >> L >> R;

    ll ans = 0;
    // 朴素做法:区间里每个数都单独判断一次。
    for (ll x = L; x <= R; x++) {
        if (can_represent(x)) {
            ans++;
        }
    }

    cout << ans << '\n';

    return 0;
}

复杂度

逐个判断 xx 需要 O(x)O(\sqrt{x}),区间里共有 R−L+1R-L+1 个数,总复杂度 O((R−L+1)R)O\big((R-L+1)\sqrt{R}\big)。

瓶颈

暴力解本身完全正确,但它有两个致命问题:

  1. RR 可以到 10910^9,光是区间长度就可能达到 10910^9,逐个数枚举已经不可能。
  2. 即使只处理一个数,O(x)O(\sqrt{x}) 也意味着约 3×1043\times 10^4 次循环,乘上区间长度后彻底超时。

所以要拿满分,必须找出「哪些数可行」的充要条件,然后不再逐个判断。

正解

思路

回到暴力的判据:x=abx = ab 且 a,ba,b 同奇偶。把「同奇偶」按两种情形展开就能看出规律。

设有因子对 a⋅b=xa\cdot b = x,且 a,ba,b 同奇偶:

  • 都是奇数:奇数乘奇数还是奇数,所以 xx 是奇数;
  • 都是偶数:写 a=2a′, b=2b′a = 2a',\ b = 2b',则 x=4a′b′x = 4a'b',所以 xx 是 44 的倍数。

反过来看,两种情形都真的能构造出来:

  • xx 是奇数时,取 a=1, b=xa = 1,\ b = x,两个因子都是奇数,y=x+12, z=x−12y = \dfrac{x+1}{2},\ z = \dfrac{x-1}{2} 是整数。例如 5=32−225 = 3^2 - 2^2。
  • xx 是 44 的倍数时,x/2x/2 是偶数,取 a=2, b=x/2a = 2,\ b = x/2,两个因子都是偶数,y=x+24, z=x−24y = \dfrac{x+2}{4},\ z = \dfrac{x-2}{4} 是整数。例如 4=22−024 = 2^2 - 0^2,8=32−128 = 3^2 - 1^2。

至于一个奇数一个偶数的情况,a+ba+b 是奇数,yy 不是整数,永远不可行。

于是得到本题最关键的一句话:

xx 可以表示为两个整数平方之差,当且仅当 xx 是奇数或者 44 的倍数。

用模运算写就是 x≡1,3(mod2)x \equiv 1,3 \pmod 2 或 x≡0(mod4)x \equiv 0 \pmod 4;等价地说,偶数中只有 44 的倍数可行,x≡2(mod4)x \equiv 2 \pmod 4 的数(2,6,10,…2,6,10,\dots)全部不行。

接下来只是计数问题。由于「奇数」和「44 的倍数」这两类互不相交(44 的倍数都是偶数,不可能是奇数),可以在 [1,n][1,n] 里直接相加:

F(n)=⌊n+12⌋⏟奇数个数+⌊n4⌋⏟4 的倍数个数.F(n) = \underbrace{\left\lfloor \frac{n+1}{2} \right\rfloor}_{\text{奇数个数}} + \underbrace{\left\lfloor \frac{n}{4} \right\rfloor}_{\text{4 的倍数个数}}.

答案就是前缀相减:

ans=F(R)−F(L−1).\text{ans} = F(R) - F(L-1).

例如 L=1,R=5L=1,R=5:奇数有 1,3,51,3,5 共 33 个,44 的倍数有 44 共 11 个,F(5)=4F(5)=4;F(0)=0F(0)=0,所以答案是 44,与样例一致。

边界上注意 F(0)F(0) 表示「[1,0][1,0] 中可行数的个数」,应当是 00,代码里对 n⩽0n \leqslant 0 直接返回 00 就稳了。题面保证 L⩾1L \geqslant 1,因此 L−1⩾0L-1 \geqslant 0,不会出现负数下取整的问题;不过把 n⩽0n \leqslant 0 提前返回,可以让这个函数在任何输入下都对。

代码

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-03 10:37
 * update_at: 2026-10-03 10:37
 */
// P9231 [蓝桥杯 2023 省 A] 平方差
// 核心结论:x 能写成 x = y^2 - z^2 当且仅当 x 是奇数或者 4 的倍数。
// 于是答案 = [1,R] 中这类数的个数 减去 [1,L-1] 中这类数的个数。
#include <iostream>
using namespace std;

typedef long long ll;

// 统计 [1,n] 中“奇数或 4 的倍数”的个数。
// 一个数不可能同时是奇数和 4 的倍数,所以两类直接相加,不会重复计算。
ll prefix_count(ll n) {
    if (n <= 0) {
        return 0;
    }
    ll odd = (n + 1) / 2; // 1,3,5,... 的个数
    ll mul4 = n / 4;      // 4,8,12,... 的个数
    return odd + mul4;
}

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

    ll L, R;
    cin >> L >> R;

    // 前缀和思想:区间答案 = F(R) - F(L-1)。
    cout << prefix_count(R) - prefix_count(L - 1) << '\n';

    return 0;
}

复杂度

每个 F(n)F(n) 只有两次除法和一次加法,答案需要算两次 FF,所以时间复杂度是 O(1)O(1),空间复杂度 O(1)O(1)。

由于 R⩽109R \leqslant 10^9,答案最大不超过 7.5×1087.5\times 10^8,而中间量 RR 与 R+1R+1 都在 10910^9 量级,32 位 int 其实也不会溢出;但习惯上仍然使用 long long,避免以后改动代码时踩坑。

总结

这道题的推导链条非常短,值得完整记住:

  1. 用平方差公式分解:x=y2−z2=(y−z)(y+z)x = y^2 - z^2 = (y-z)(y+z);
  2. 于是 xx 可行等价于「存在同奇偶的因子对」,即 xx 是奇数或 44 的倍数;
  3. 奇数与 44 的倍数互不重叠,用前缀计数 F(R)−F(L−1)F(R)-F(L-1) 一次算出答案。

最容易踩的坑是把条件记成「偶数都行」:真正的偶数是 44 的倍数,2,6,10,…2,6,10,\dots 这类模 44 余 22 的数永远写不成平方差。写代码时再注意两件事:区间用前缀相减而不是直接枚举,以及把 n⩽0n \leqslant 0 的情况单独返回 00。