OneCoder Avatar
OneCodercoderli.com · 937 篇博文
四级

C++ 算法考级专栏

真题分析、矩阵探测、递归回溯与基础语法

🎨 视觉封面

【NOIP】1997真题解析 luogu-P2241 统计方形(数据加强版) | GESP三、四级以上可练习

📅 2026-09-02·✍️ OneCoder·计算中...·⏱️ 14 分钟
#GESP#NOIP#C++#枚举#数学#洛谷

NOIP 1997 普及组第一题(洛谷 P2241 数据加强版),主要考察组合计数原理循环枚举数学规律推导。本题既可以通过双重循环枚举矩形的长宽直观求解,也可以通过组合数学与求和公式实现 O(min(n,m))\mathcal{O}(\min(n, m)) 乃至 O(1)\mathcal{O}(1) 的高效求解。同时本题也是经典的“防溢出(long long)”教学题。GESP 三、四级以上可练习。题目难度⭐⭐☆☆☆,洛谷难度等级普及−

luogu-P2241 统计方形(数据加强版)

题目要求

题目背景

1997 年普及组第一题

题目描述

有一个 n×mn \times m 方格的棋盘,求其方格包含多少正方形、长方形(不包含正方形)。

输入格式

一行,两个正整数 n,mn,mn5000,m5000n \leq 5000,m \leq 5000)。

输出格式

一行,两个正整数,分别表示方格包含多少正方形、长方形(不包含正方形)。

输入输出样例 #1

输入 #1
TEXT
2 3
输出 #1
TEXT
8 10

题目分析

这道题目是一道经典的平面几何计数问题。我们需要在 n×mn \times m 个小方格组成的网格中分别统计出:

  1. 正方形 的总个数;
  2. 长方形(不包含正方形) 的总个数。

1. 核心关系与转化

在几何学中,正方形是特殊的长方形(矩形)。因此: 所有矩形总数(包含正方形)=正方形个数+纯长方形个数\text{所有矩形总数(包含正方形)} = \text{正方形个数} + \text{纯长方形个数}

由此可得: 纯长方形个数=所有矩形总数正方形个数\text{纯长方形个数} = \text{所有矩形总数} - \text{正方形个数}

只要我们分别求出 正方形个数矩形总数,即可直接相减得出答案。


2. 矩形总数的计算(组合计数原理)

一个 n×mn \times m 的方格网格由 (n+1)(n+1) 条横向网格线和 (m+1)(m+1) 条纵向网格线构成:

  • 任意选择 2 条不同的横线(从 n+1n+1 条中选取),即可确定矩形的上下边界,选法共有: (n+12)=n(n+1)2\binom{n+1}{2} = \frac{n(n+1)}{2}
  • 任意选择 2 条不同的竖线(从 m+1m+1 条中选取),即可确定矩形的左右边界,选法共有: (m+12)=m(m+1)2\binom{m+1}{2} = \frac{m(m+1)}{2}

根据乘法原理,网格中包含的所有矩形总数为: Total=(n+12)×(m+12)=n(n+1)2×m(m+1)2\text{Total} = \binom{n+1}{2} \times \binom{m+1}{2} = \frac{n(n+1)}{2} \times \frac{m(m+1)}{2}


3. 正方形个数的计算

正方形要求长和宽相等。设正方形的边长为 kk,显然 kk 的取值范围为 1kmin(n,m)1 \le k \le \min(n, m)

  • 对于边长为 kk 的正方形:
    • 在水平方向(高为 nn)上有 (nk+1)(n - k + 1) 种不同的放置位置;
    • 在垂直方向(宽为 mm)上有 (mk+1)(m - k + 1) 种不同的放置位置;
  • 因此,边长为 kk 的正方形总共有 (nk+1)×(mk+1)(n - k + 1) \times (m - k + 1) 个。
  • 累加所有可能的边长 kk,得到正方形的总数为: S=k=1min(n,m)(nk+1)(mk+1)S = \sum_{k=1}^{\min(n, m)} (n - k + 1)(m - k + 1)

4. 算法实现的三种层次

层次一:O(n×m)\mathcal{O}(n \times m) 双重循环枚举(适合初学者)
  • 双重循环分别枚举矩形的长 i[1,n]i \in [1, n] 和宽 j[1,m]j \in [1, m]
  • 尺寸为 i×ji \times j 的矩形数量为 (ni+1)×(mj+1)(n - i + 1) \times (m - j + 1)
  • i==ji == j,累加到正方形计数中;若 iji \ne j,累加到长方形计数中;
  • 运算次数约为 5000×5000=2.5×1075000 \times 5000 = 2.5 \times 10^7,在 C++ 1 秒时限内完全可以通过(耗时约 30ms)。
层次二:O(min(n,m))\mathcal{O}(\min(n, m)) 单层循环枚举(推荐做法)
  • 利用公式 Total=n(n+1)2×m(m+1)2\text{Total} = \frac{n(n+1)}{2} \times \frac{m(m+1)}{2}O(1)\mathcal{O}(1) 时间内求出矩形总数;
  • 单层循环枚举边长 k[1,min(n,m)]k \in [1, \min(n, m)],累加求出正方形数量 SS
  • 纯长方形数量即为 TotalS\text{Total} - S
  • 循环次数最多仅 50005000 次,运行时间在 1ms 以内,代码极其简洁。
层次三:O(1)\mathcal{O}(1) 纯数学公式法(极限优化)

将正方形计数式展开: S=k=1K(nk+1)(mk+1)(其中 K=min(n,m))S = \sum_{k=1}^K (n - k + 1)(m - k + 1) \quad (\text{其中 } K = \min(n, m))A=n+1,B=m+1A = n + 1, B = m + 1,则: (Ak)(Bk)=AB(A+B)k+k2(A - k)(B - k) = AB - (A + B)k + k^2 代入数列求和公式 k=1Kk=K(K+1)2\sum_{k=1}^K k = \frac{K(K+1)}{2} 与平方和公式 k=1Kk2=K(K+1)(2K+1)6\sum_{k=1}^K k^2 = \frac{K(K+1)(2K+1)}{6},即可直接在 O(1)\mathcal{O}(1) 复杂度内求出 SS


⚠️ 核心易错点分析

1. 变量范围与整型溢出(必须使用 long long

  • 题目中 n,m5000n, m \le 5000
  • 极端情况下(n=5000,m=5000n = 5000, m = 5000),矩形总数约为: Total5000×50012×5000×500121.25×107×1.25×1071.56×1014\text{Total} \approx \frac{5000 \times 5001}{2} \times \frac{5000 \times 5001}{2} \approx 1.25 \times 10^7 \times 1.25 \times 10^7 \approx 1.56 \times 10^{14}
  • 32 位有符号整型 int 的最大值约为 2.14×1092.14 \times 10^9
  • 1.56×10141.56 \times 10^{14} 远超 int 范围,若使用 int 存储或计算,会发生整型溢出导致结果为负数或错误答案。
  • 所有涉及结果累加与乘法的变量必须使用 long long 类型!

2. “不包含正方形”的审题陷阱

  • 题目第二问明确要求输出“长方形(不包含正方形)”。
  • 切勿直接输出所有矩形总数,必须减去正方形的个数。

示例代码

方法一:单层循环 O(min(n,m))\mathcal{O}(\min(n, m))(推荐)

CPP
#include <algorithm>
#include <iostream>

using namespace std;

int main() {
    // 提高输入输出效率
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    long long n, m;
    if (!(cin >> n >> m)) {
        return 0;
    }

    // 1. 利用组合公式计算所有矩形(包含正方形)的总数
    long long total_rectangles = (n * (n + 1) / 2) * (m * (m + 1) / 2);

    // 2. 单层循环枚举正方形的边长 k
    long long squares = 0;
    long long limit = min(n, m);
    for (long long k = 1; k <= limit; ++k) {
        squares += (n - k + 1) * (m - k + 1);
    }

    // 3. 纯长方形数量 = 总矩形数 - 正方形数
    long long pure_rectangles = total_rectangles - squares;

    cout << squares << " " << pure_rectangles << "\n";

    return 0;
}

方法二:双重循环 O(n×m)\mathcal{O}(n \times m)(直观枚举)

CPP
#include <iostream>

using namespace std;

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

    long long n, m;
    cin >> n >> m;

    long long squares = 0;         // 正方形数量
    long long pure_rectangles = 0; // 不含正方形的长方形数量

    // 枚举矩形的长 i 和宽 j
    for (long long i = 1; i <= n; ++i) {
        for (long long j = 1; j <= m; ++j) {
            long long count = (n - i + 1) * (m - j + 1);
            if (i == j) {
                squares += count;
            } else {
                pure_rectangles += count;
            }
        }
    }

    cout << squares << " " << pure_rectangles << "\n";

    return 0;
}

方法三:纯数学公式 O(1)\mathcal{O}(1)(极致常数)

CPP
#include <algorithm>
#include <iostream>

using namespace std;

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

    long long n, m;
    cin >> n >> m;

    // 总矩形数
    long long total_rectangles = (n * (n + 1) / 2) * (m * (m + 1) / 2);

    // 正方形总数公式展开计算
    long long K = min(n, m);
    long long A = n + 1;
    long long B = m + 1;

    // sum_1 = sum(k, 1..K) = K*(K+1)/2
    // sum_2 = sum(k^2, 1..K) = K*(K+1)*(2*K+1)/6
    long long sum_1 = K * (K + 1) / 2;
    long long sum_2 = K * (K + 1) * (2 * K + 1) / 6;

    long long squares = K * A * B - (A + B) * sum_1 + sum_2;
    long long pure_rectangles = total_rectangles - squares;

    cout << squares << " " << pure_rectangles << "\n";

    return 0;
}

复杂度对比与总结

方法 时间复杂度 空间复杂度 优缺点与适用场景
双重循环枚举 O(n×m)\mathcal{O}(n \times m) O(1)\mathcal{O}(1) 逻辑直观、易理解,适用于初学者理解几何分割
单层循环边长 O(min(n,m))\mathcal{O}(\min(n, m)) O(1)\mathcal{O}(1) 最推荐,计算量仅 5000\le 5000,兼顾了简洁性与运行速度
纯数学公式 O(1)\mathcal{O}(1) O(1)\mathcal{O}(1) 速度极限(常数时间),适用于 n,m109n, m \le 10^9 的超大规模数据

知识点拓展总结

  1. 网格计数问题化简法:将二维网格矩形选取转化为一维线段选取的组合问题(乘法原理);
  2. 正反面转化思想:当直接统计某种形状较为复杂时,可用“总方案数 - 反面/特殊方案数”;
  3. 数据规模敏感度:时刻关注 n,mn, m 计算产生的结果范围,在竞赛与等级考试中养成对平方、乘积级别结果使用 long long 的良好习惯。
💡 OneCoder 资源指引

所有代码开源上传至 GitHub:yummy-code 仓库 · GESP 专题站:GESP WIKI

🤝 技术交流与答疑

欢迎加入:C++ GESP/CSP 考级答疑群(688906745)Java/Python交流群(982860385),点击可直接加群。

📚

猜你想读 · 相关文章推荐

OneCoder

OneCoder (lihongzheshuai)

一个中年人的自留地,记录学习 C++、GESP/NOI、Java、Python 与算法架构的心得体会。本站唯一网址:coderli.com

💬 读者留言与交流

0 条讨论
✨ 支持 Markdown 语法格式
还没有留言,快来成为第一个讨论者吧!