团体程序设计天梯赛 L2-052 吉利矩阵 (DFS+剪枝)(Java实现)
所有元素为非负整数,且各行各列的元素和都等于 7 的 3×3 方阵称为“吉利矩阵”,因为这样的矩阵一共有 666 种。
本题就请你统计一下,把 7 换成任何一个 [2,9] 区间内的正整数 L,把矩阵阶数换成任何一个 [2,4] 区间内的正整数 N,满足条件“所有元素为非负整数,且各行各列的元素和都等于 L”的 N×N 方阵一共有多少种?
输入格式:
输入在一行中给出 2 个正整数 L 和 N,意义如题面所述。数字间以空格分隔。
输出格式:
在一行中输出满足题目要求条件的方阵的个数。
输入样例:
7 3
输出样例:
666
给定两个正整数 L 和 N,求满足以下条件的 N×N 矩阵的个数:
- 矩阵中每个元素都是非负整数;
- 每一行的元素之和恰好为 L;
- 每一列的元素之和也恰好为 L。
输出满足条件的矩阵总数。
数据范围:1≤N≤9,1≤L≤9。
解题思路
直接枚举所有 个格子的时间复杂度过高,因此需要采用 降维搜索 策略,利用问题的对称性和约束条件减少搜索空间。
核心观察:最后一行与最后一列可由前 N−1 行/列唯一确定
我们只对矩阵的前 (N−1)×(N−1)子矩阵进行深度优先搜索(DFS)。一旦该子矩阵确定,其余部分可被唯一推导:
对于第 i 行(1≤i<N),其最后一列的值为:
对于第 j 列(1≤j<N),其最后一行的值为:
此时,最后一个元素 a[N][N] 可从两个方向计算:
从第 N 行看:
从第 N 列看:
这两个值在数学上是相等的,因为它们都等于: 其中 S 是前 (N−1)×(N−1)(N−1)×(N−1) 子矩阵所有元素之和。
合法性条件
要使整个矩阵合法,只需满足以下两个条件:
最后一列的前 N−1 个元素之和不超过 L:
最后一行的前 N−1 个元素之和不超过 L:
由于这两个和在数值上恒等(都等于 L⋅(N−1)−S),因此只需检查其中一个是否 ≤L 即可。若成立,则 a[N][N]=L−(L⋅(N−1)−S)=L⋅(2−N)+S≥0,且所有行列和均为 L,矩阵合法。
算法设计
1. 搜索策略
使用 DFS 枚举前 (N−1)×(N−1)子矩阵中每个位置的取值。每个位置 a[i][j]a[i][j] 的取值范围为:
其中:
- rowSum[i] 表示第 i 行当前已填元素之和;
- colSum[j] 表示第 j 列当前已填元素之和。
2. 剪枝优化
- 在枚举过程中,若某行或某列的当前和已超过 L,则剪枝。
- 由于 N≤9,L≤9,且搜索空间仅为
,实际运行效率很高。
3. 终止条件
当 DFS 填完所有 (N−1)×(N−1) 的格子后,计算:
- sumLastColPrefix =
- sumLastRowPrefix =
若满足: 且
则计数器加一。
4. 特判
当 N=1时,矩阵只有一个元素 a[1][1],其值必须为 L,因此答案为 1。
以下给出剪枝版本和打表版本,代码清晰易理解,希望互相学习。
Java实现:
import java.io.*;/*** 计算 N×N 非负整数矩阵的个数,满足:* - 每行元素之和为 L* - 每列元素之和为 L* * 核心思想:只搜索前 (N-1)×(N-1) 的子矩阵* 最后一行和最后一列由前 N-1 行/列自动确定* 只需检查最后一行前 N-1 列之和 ≤ L,且最后一列前 N-1 行之和 ≤ L* 由于数学恒等式,两者实际相等,因此只需检查上限即可。*/
public class Main {private static int targetSum; // 每行/每列的目标和 Lprivate static int matrixSize; // 矩阵维度 Nprivate static int resultCount; // 合法矩阵总数// 记录前 N-1 行/列的当前部分和(索引 1 ~ N-1 使用)private static int[] rowPartialSum = new int[10]; private static int[] colPartialSum = new int[10]; public static void main(String[] args) throws IOException {// 读取输入BufferedReader br = new BufferedReader(new InputStreamReader(System.in));String[] input = br.readLine().split(" ");targetSum = Integer.parseInt(input[0]);matrixSize = Integer.parseInt(input[1]);// 特判:如果 N == 1,则只有一种可能:[L]if (matrixSize == 1) {System.out.println(1);return;}// 初始化计数器和部分和数组resultCount = 0;for (int i = 0; i < 10; i++) {rowPartialSum[i] = 0;colPartialSum[i] = 0;}// 从子矩阵的左上角 (1,1) 开始深度优先搜索dfs(1, 1);// 输出结果System.out.println(resultCount);}/*** 深度优先搜索填充 (N-1) × (N-1) 子矩阵* * @param currentRow 当前行索引(从 1 开始)* @param currentCol 当前列索引(从 1 开始)*/private static void dfs(int currentRow, int currentCol) {// 如果当前列超出 N-1,换到下一行第一列if (currentCol > matrixSize - 1) {currentCol = 1;currentRow++;}// 如果已填完前 N-1 行,进行合法性检查if (currentRow == matrixSize) {checkAndCount();return;}// 枚举当前格子 (currentRow, currentCol) 可填的值// 不能超过所在行或所在列的剩余容量int maxAllowedValue = Math.min(targetSum - rowPartialSum[currentRow], // 行剩余空间targetSum - colPartialSum[currentCol] // 列剩余空间);for (int value = 0; value <= maxAllowedValue; value++) {// 做选择rowPartialSum[currentRow] += value;colPartialSum[currentCol] += value;// 递归填下一个格子dfs(currentRow, currentCol + 1);// 回溯rowPartialSum[currentRow] -= value;colPartialSum[currentCol] -= value;}}/*** 检查当前 (N-1)×(N-1) 子矩阵是否能扩展为完整合法矩阵* 条件:最后一列前 N-1 行之和 ≤ L,且最后一行前 N-1 列之和 ≤ L* 数学上这两个和相等,因此只需检查是否都不超过 L。*/private static void checkAndCount() {int sumLastColPrefix = 0; // 最后一列的前 N-1 个元素之和int sumLastRowPrefix = 0; // 最后一行的前 N-1 个元素之和// 计算最后一列前 N-1 行的值:每行缺多少才能到 Lfor (int i = 1; i < matrixSize; i++) {sumLastColPrefix += (targetSum - rowPartialSum[i]);}// 计算最后一行前 N-1 列的值:每列缺多少才能到 Lfor (int j = 1; j < matrixSize; j++) {sumLastRowPrefix += (targetSum - colPartialSum[j]);}// 只要两者都不超过 L,就能合法补全矩阵if (sumLastColPrefix <= targetSum && sumLastRowPrefix <= targetSum) {resultCount++;}}
}
打表法:
import java.io.*;
import java.util.StringTokenizer;public class Main {static class FastReader {BufferedReader br;StringTokenizer st;public FastReader() {br = new BufferedReader(new InputStreamReader(System.in));}String next() {while (st == null || !st.hasMoreElements()) {try {st = new StringTokenizer(br.readLine());} catch (IOException e) {e.printStackTrace();}}return st.nextToken();}int nextInt() {return Integer.parseInt(next());}}public static void main(String[] args) {// 预计算的表(索引从 0 开始,对应 l = 0,1,2,...,9)int[] n2 = {0, 0, 3, 4, 5, 6, 7, 8, 9, 10};int[] n3 = {0, 0, 21, 55, 120, 231, 406, 666, 1035, 1540};int[] n4 = {0, 0, 282, 2008, 10147, 40176, 132724, 381424, 981541, 2309384};FastReader fr = new FastReader();int l = fr.nextInt();int n = fr.nextInt();if (n == 2) {System.out.println(n2[l]);} else if (n == 3) {System.out.println(n3[l]);} else if (n == 4) {System.out.println(n4[l]);}}
}