洛谷算法1-3 暴力枚举(NOIP经典真题解析)java(持续更新)

技术笔记:算法1-3 暴力枚举(NOIP经典真题解析)

暴力枚举是算法竞赛中最基础、最直接的解题思路,它通过穷举所有可能的情况来寻找答案,尤其适合解决那些规模不大、或者可以通过剪枝优化的问题。本文将通过四道洛谷经典真题,讲解暴力枚举在不同场景下的应用和优化技巧。


一、棋盘方格(数学推导优化暴力)

题目核心要求

给定一个 n×m 的棋盘,计算其中包含的正方形总数和长方形(不包含正方形)的总数。

解题思路

这道题看似是暴力枚举,但可以通过数学公式直接推导,避免了低效的双重循环:

  1. 正方形数量 :对于边长为 k 的正方形,在 n×m 的棋盘上有 (n-k+1) * (m-k+1) 个。因此,总数是对 k 从 1 到 min(n,m) 的求和,即 sum_{k=1 to min(n,m)} (n-k+1)*(m-k+1)
  2. 长方形数量 :所有矩形(包括正方形)的总数是 n*(n+1)/2 * m*(m+1)/2。用这个值减去正方形的数量,即可得到纯长方形的数量。

完整Java代码

java 复制代码
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        long n = sc.nextLong();
        long m = sc.nextLong();
        sc.close();

        // 计算正方形的数量
        long squares = 0;
        long min = Math.min(n, m);
        for (long k = 1; k <= min; k++) {
            squares += (n - k + 1) * (m - k + 1);
        }

        // 计算所有矩形(含正方形)的数量
        long rectanglesTotal = (n * (n + 1) / 2) * (m * (m + 1) / 2);

        // 长方形数量 = 所有矩形 - 正方形
        long rectangles = rectanglesTotal - squares;

        System.out.println(squares + " " + rectangles);
    }
}

关键注意点

  1. 数据类型溢出 :由于 n 和 m 最大为 5000,计算结果会非常大,必须使用 long 类型来存储中间结果和最终答案。
  2. 数学公式的效率:使用数学公式将时间复杂度从 O(n*m) 降低到 O(min(n,m)),效率大幅提升。
  3. 问题转换:将求"长方形"的问题转换为求"所有矩形"减去"正方形",是解题的关键技巧。

二、比例三位数(全排列 + 剪枝)

题目核心要求

将 1-9 这九个数字分成三组,组成三个三位数,使它们的比例为 A:B:C。输出所有满足条件的解,若无解则输出 No!!!

解题思路

这是一个典型的暴力枚举问题,但可以通过比例关系进行剪枝,避免无效的枚举:

  1. 枚举第一个数:遍历所有可能的第一个三位数 i。
  2. 计算另外两个数 :根据比例关系计算出另外两个数 j = iB/A 和 k = iC/A。
  3. 合法性检查
    • 检查 j 和 k 是否为有效的三位数。
    • 检查 i, j, k 这三个数的拼接是否恰好包含 1-9 每个数字一次,没有重复或遗漏。
  4. 输出结果:如果检查通过,则输出这三个数。

完整Java代码

java 复制代码
import java.util.Scanner;

public class Main2 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int A = sc.nextInt();
        int B = sc.nextInt();
        int C = sc.nextInt();
        sc.close();

        boolean found = false;

        // 遍历第一个三位数
        for (int i = 100; i < 1000; i++) {
            // 剪枝:如果不能整除,直接跳过
            if (i * B % A != 0 || i * C % A != 0) continue;
            int j = i * B / A;
            int k = i * C / A;

            // 检查另外两个数是否也是三位数
            if (j >= 1000 || k >= 1000) continue;

            // 检查数字 1-9 是否恰好出现一次
            String s = "" + i + j + k;
            if (s.length() != 9) continue;
            boolean[] digits = new boolean[10];
            boolean valid = true;
            for (char c : s.toCharArray()) {
                int num = c - '0';
                if (num == 0 || digits[num]) {
                    valid = false;
                    break;
                }
                digits[num] = true;
            }

            if (valid) {
                System.out.println(i + " " + j + " " + k);
                found = true;
            }
        }

        if (!found) {
            System.out.println("No!!!");
        }
    }
}

关键注意点

  1. 剪枝优化 :在计算 j 和 k 之前,先检查 i * B % A != 0,可以过滤掉大量不可能的情况,显著提高效率。
  2. 数字唯一性验证:使用一个布尔数组来跟踪 1-9 是否都被使用过,比字符串包含检查更高效。
  3. 输出顺序:由于 i 是从小到大枚举的,输出的解自然是按第一个数升序排列的,满足题目要求。

三、组合数的输出(递归生成组合)

题目核心要求

从自然数 1, 2, ..., n 中任取 r 个数,按字典序输出所有组合。

解题思路

这是一个典型的组合生成问题,可以用递归的方式进行暴力枚举:

  1. 递归定义:定义一个递归函数,参数包括当前已选数字的位置、下一个可选数字的起始值,以及存储当前组合的数组。
  2. 递归终止条件:当已选数字的数量等于 r 时,输出当前组合。
  3. 递归过程:从起始值开始,依次选择一个数字加入当前组合,然后递归调用函数,选择下一个数字(起始值加一)。

完整Java代码

java 复制代码
import java.util.Scanner;

public class Main3 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int r = sc.nextInt();
        sc.close();

        generate(n, r);
    }

    public static void generate(int n, int r) {
        int[] result = new int[r];
        find(n, r, 0, 1, result);
    }

    public static void find(int n, int r, int pos, int begin, int[] result) {
        // 递归终止条件:选够了r个数
        if (pos == r) {
            for (int num : result) {
                System.out.printf("%3d", num);
            }
            System.out.println();
            return;
        }

        // 递归过程:从begin开始选下一个数
        for (int i = begin; i <= n; i++) {
            result[pos] = i;
            find(n, r, pos + 1, i + 1, result);
        }
    }
}

关键注意点

  1. 避免重复组合 :通过 begin 参数确保每次选择的数字都比前一个大,从而保证组合的唯一性和字典序。
  2. 格式化输出 :使用 printf("%3d", num) 确保每个数字占三个字符的宽度,满足题目对输出格式的严格要求。
  3. 递归的效率:这种方法直接生成符合要求的组合,时间复杂度为 O(C(n,r)),在题目给定的 n ≤ 20 的约束下是完全可行的。

四、全排列(回溯法生成排列)

题目核心要求

按字典序输出自然数 1 到 n 的所有不重复的全排列。

解题思路

这是一个经典的回溯法问题,通过暴力枚举所有可能的排列来解决:

  1. 回溯定义:定义一个递归函数,参数包括当前排列的位置、存储当前排列的数组,以及标记数字是否被使用过的布尔数组。
  2. 回溯终止条件:当当前排列的位置等于 n 时,输出当前排列。
  3. 回溯过程:遍历所有数字,如果该数字未被使用,则将其加入当前排列,标记为已使用,然后递归调用函数填充下一个位置。递归返回后,回溯(取消标记),尝试下一个数字。

完整Java代码

java 复制代码
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Scanner;

public class Main4 {
    // 使用 BufferedWriter 进行高效输出
    private static final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        sc.close();

        int[] current = new int[n];
        boolean[] used = new boolean[n + 1];

        backtrack(n, 0, current, used);

        // 确保所有数据都被写出
        writer.flush();
        writer.close();
    }

    private static void backtrack(int n, int pos, int[] current, boolean[] used) throws IOException {
        // 回溯终止条件:生成了一个完整的排列
        if (pos == n) {
            StringBuilder lineSb = new StringBuilder();
            for (int num : current) {
                lineSb.append("    ").append(num);
            }
            writer.write(lineSb.toString());
            writer.newLine();
            return;
        }

        // 回溯过程:尝试所有未被使用的数字
        for (int i = 1; i <= n; i++) {
            if (!used[i]) {
                used[i] = true;
                current[pos] = i;
                backtrack(n, pos + 1, current, used);
                used[i] = false; // 回溯
            }
        }
    }
}

关键注意点

  1. 输出效率 :由于 n 最大为 9,会生成 362880 个排列。使用 BufferedWriter 替代 System.out 可以大幅提高输出效率,避免超时。
  2. 回溯的核心used[i] = false 这一步是回溯的关键,它确保在尝试完一个分支后,能够回到上一步,尝试其他可能的选择。
  3. 字典序保证:由于我们是从小到大遍历数字,因此生成的排列自然是按字典序排列的,满足题目要求。

相关推荐
AIFarmer几秒前
【无标题】
开发语言·c++·算法
Nick_zcy6 分钟前
小说在线阅读网站和小说管理系统 · 功能全解析
java·后端·python·springboot·ruoyi
源码宝9 分钟前
基于 SpringBoot + Vue 的医院随访系统:技术架构与功能实现
java·vue.js·spring boot·架构·源码·随访系统·随访管理
昇腾CANN15 分钟前
TileLang-Ascend 算子性能优化方法与实操
开发语言·javascript·性能优化·昇腾·cann
AGV算法笔记21 分钟前
CVPR 2025 最新感知算法解读:GaussianLSS 如何用 Gaussian Splatting 重构 BEV 表示?
算法·重构·自动驾驶·3d视觉·感知算法·多视角视觉
沐知全栈开发26 分钟前
ionic 手势事件详解
开发语言
lsx2024061 小时前
Bootstrap 按钮
开发语言
qinqinzhang1 小时前
Java 中的 IoC、AOP、MVC
java
神仙别闹1 小时前
基于 Python 实现 BERT 的情感分析模型
开发语言·python·bert
禾叙_1 小时前
【langchain4j】结构化输出(六)
java·开发语言