洛谷算法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. 字典序保证:由于我们是从小到大遍历数字,因此生成的排列自然是按字典序排列的,满足题目要求。

相关推荐
_OP_CHEN2 小时前
【算法基础篇】(五十五)卡特兰数封神之路:从括号匹配到二叉树构造,组合数学的万能钥匙!
算法·蓝桥杯·c/c++·组合数学·卡特兰数·算法竞赛·acm/icpc
爱上妖精的尾巴2 小时前
8-5 WPS JS宏 match、search、replace、split支持正则表达式的字符串函数
开发语言·前端·javascript·wps·jsa
阿猿收手吧!2 小时前
【C++】inline变量:全局共享新利器
开发语言·c++
沐知全栈开发2 小时前
Python3 列表详解
开发语言
逝水如流年轻往返染尘2 小时前
正则表达式字符串
java·正则表达式
LYS_06182 小时前
寒假学习(14)(HAL库5)
java·linux·学习
小温冲冲2 小时前
通俗且全面精讲单例设计模式
开发语言·javascript·设计模式
qq_336313932 小时前
javaweb-maven单元测试
java·开发语言·maven
郝学胜-神的一滴2 小时前
Python美学的三重奏:深入浅出列表、字典与生成器推导式
开发语言·网络·数据结构·windows·python·程序人生·算法