前言
这是一道蓝桥杯第六届JavaB组省赛的一道算法题,官网给的数据:
总通过次数: 891 | 总提交次数: 2115 | 通过率: 42.1%
我第一次做的时候说实话,解不出,先看了解析,没看代码,还是不太理解,看了第一遍代码,再写,报错了,矩阵的构建那块和计算不太明白,大概看了三遍,最后写第四遍才慢慢理解,挺菜的,哈哈哈哈,见怪不怪了!算法还是需要提高了,下面是详细解析和自己的理解!
题目
题目描述
赌圣 atm 晚年迷恋上了垒骰子,就是把骰子一个垒在另一个上边,不能歪歪扭扭,要垒成方柱体。
经过长期观察,atm 发现了稳定骰子的奥秘:有些数字的面贴着会互相排斥!
我们先来规范一下骰子:1 的对面是 4,2 的对面是 5,3 的对面是 6。
假设有 mm 组互斥现象,每组中的那两个数字的面紧贴在一起,骰子就不能稳定的垒起来。
atm 想计算一下有多少种不同的可能的垒骰子方式。
两种垒骰子方式相同,当且仅当这两种方式中对应高度的骰子的对应数字的朝向都相同。
由于方案数可能过多,请输出模 109+7109+7 的结果。
不要小看了 atm 的骰子数量哦~
输入描述
输入第一行两个整数 n,mn,m,nn 表示骰子数目;
接下来 mm 行,每行两个整数 a,ba,b ,表示 aa 和 bb 数字不能紧贴在一起。
其中,0<n≤109,m≤360<n≤109,m≤36。
输出描述
输出一行一个数,表示答案模 109+7109+7 的结果。
输入输出样例
示例
输入:
2 1 1 2
输出:
544
运行限制
- 最大运行时间:2s
- 最大运行内存: 256M
题目分析
该题是一个计数问题,需要计算满足相邻面限制的骰子堆叠方案数。由于骰子数量 n 可能极大,采用动态规划结合矩阵快速幂的方法。定义状态为骰子的顶面点数,构建6×6的转移矩阵,表示相邻骰子间的合法转移方案数。通过矩阵快速幂计算转移矩阵的(n-1)次幂,将线性递推转化为对数时间复杂度的矩阵运算,最后结合第一个骰子的旋转情况得到总方案数,时间复杂度为O(log n)。具体代码或者讲解看下面具体分析!
代码
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
final long MOD = 1_000_000_007;
int[] opp = {0, 4, 5, 6, 1, 2, 3}; // 骰子对面关系数组
Scanner scan = new Scanner(System.in);
long n = scan.nextLong(); // 骰子数量
int m = scan.nextInt(); // 冲突对数
// 初始化冲突矩阵
boolean[][] conflict = new boolean[7][7];
for (int i = 0; i < m; i++) {
int a = scan.nextInt();
int b = scan.nextInt();
conflict[a][b] = true;
conflict[b][a] = true;
}
// 特殊情况处理
if (n == 1) {
System.out.println(24);
scan.close();
return;
}
// 构建转移矩阵
long[][] T = new long[6][6];
for (int top = 1; top <= 6; top++) {
for (int nextTop = 1; nextTop <= 6; nextTop++) {
int bottom = opp[nextTop]; // 下一个骰子的底面
if (!conflict[top][bottom]) {
T[top - 1][nextTop - 1] = 4;
}
}
}
// 计算转移矩阵的(n-1)次幂
long[][] M = matrixPower(T, n - 1, MOD, opp);
// 计算最终结果
long ans = 0;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
ans = (ans + 4 * M[i][j]) % MOD;
}
}
System.out.println(ans);
scan.close();
}
// 矩阵乘法
static long[][] multiply(long[][] A, long[][] B, long MOD) {
int n = A.length;
int p = B.length;
int m = B[0].length;
long[][] C = new long[n][m];
for (int i = 0; i < n; i++) {
for (int k = 0; k < p; k++) {
if (A[i][k] == 0) continue;
for (int j = 0; j < m; j++) {
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
}
}
return C;
}
// 矩阵快速幂运算
static long[][] matrixPower(long[][] base, long exp, long MOD, int[] opp) {
int n = base.length;
long[][] res = new long[n][n];
// 初始化为单位矩阵
for (int i = 0; i < n; i++) {
res[i][i] = 1;
}
while (exp > 0) {
if ((exp & 1) == 1) {
res = multiply(res, base, MOD);
}
base = multiply(base, base, MOD);
exp >>= 1;
}
return res;
}
}
案例带跑
输入数据:
text
n = 4 m = 2 冲突1: 1 2 冲突2: 3 4代码执行流程与结果:
1. 初始化设置
final long MOD = 1_000_000_007; int[] opp = {0, 4, 5, 6, 1, 2, 3}; // 1↔4, 2↔5, 3↔62. 构建冲突矩阵
boolean[][] conflict = new boolean[7][7]; conflict[1][2] = conflict[2][1] = true; conflict[3][4] = conflict[4][3] = true; // 其他位置都是false3. 构建转移矩阵T
计算逻辑 :
对于每个top(1-6)和nextTop(1-6):
bottom = opp[nextTop]
如果conflict[top][bottom]==true,则T[top-1][nextTop-1]=0,否则=4
具体计算:
当top=1时:
nextTop=1: bottom=opp[1]=4, conflict[1][4]=false → T[0][0]=4
nextTop=2: bottom=opp[2]=5, conflict[1][5]=false → T[0][1]=4
nextTop=3: bottom=opp[3]=6, conflict[1][6]=false → T[0][2]=4
nextTop=4: bottom=opp[4]=1, conflict[1][1]=false → T[0][3]=4
nextTop=5: bottom=opp[5]=2, conflict[1][2]=true → T[0][4]=0
nextTop=6: bottom=opp[6]=3, conflict[1][3]=false → T[0][5]=4
当top=2时:
nextTop=1: bottom=opp[1]=4, conflict[2][4]=false → T[1][0]=4
nextTop=2: bottom=opp[2]=5, conflict[2][5]=false → T[1][1]=4
nextTop=3: bottom=opp[3]=6, conflict[2][6]=false → T[1][2]=4
nextTop=4: bottom=opp[4]=1, conflict[2][1]=true → T[1][3]=0
nextTop=5: bottom=opp[5]=2, conflict[2][2]=false → T[1][4]=4
nextTop=6: bottom=opp[6]=3, conflict[2][3]=false → T[1][5]=4
当top=3时:
nextTop=1: bottom=opp[1]=4, conflict[3][4]=true → T[2][0]=0
nextTop=2: bottom=opp[2]=5, conflict[3][5]=false → T[2][1]=4
nextTop=3: bottom=opp[3]=6, conflict[3][6]=false → T[2][2]=4
nextTop=4: bottom=opp[4]=1, conflict[3][1]=false → T[2][3]=4
nextTop=5: bottom=opp[5]=2, conflict[3][2]=false → T[2][4]=4
nextTop=6: bottom=opp[6]=3, conflict[3][3]=false → T[2][5]=4
当top=4时:
nextTop=1: bottom=opp[1]=4, conflict[4][4]=false → T[3][0]=4
nextTop=2: bottom=opp[2]=5, conflict[4][5]=false → T[3][1]=4
nextTop=3: bottom=opp[3]=6, conflict[4][6]=false → T[3][2]=4
nextTop=4: bottom=opp[4]=1, conflict[4][1]=false → T[3][3]=4
nextTop=5: bottom=opp[5]=2, conflict[4][2]=false → T[3][4]=4
nextTop=6: bottom=opp[6]=3, conflict[4][3]=true → T[3][5]=0
当top=5,6时:无任何冲突,所有位置都是4
最终转移矩阵T:
T = [ [4, 4, 4, 4, 0, 4], // top=1 [4, 4, 4, 0, 4, 4], // top=2 [0, 4, 4, 4, 4, 4], // top=3 [4, 4, 4, 4, 4, 0], // top=4 [4, 4, 4, 4, 4, 4], // top=5 [4, 4, 4, 4, 4, 4] // top=6 ]4. 计算M = T^(n-1) = T³
使用矩阵快速幂计算T³
二进制快速幂过程 :
exp = 3(二进制11)
循环1:exp=3(二进制11,最低位1)
// 条件成立,执行res = multiply(res, base) res = I × T = T // base = multiply(base, base) base = T × T = T² exp >>= 1 → exp=1循环2:exp=1(二进制1,最低位1)
// 条件成立,执行res = multiply(res, base) res = T × T² = T³ // base = multiply(base, base) base = T² × T² = T⁴ exp >>= 1 → exp=0结束,返回res = T³
5. 计算矩阵乘法过程
首先计算T² = multiply(T, T, MOD):
以计算T²[0][0]为例:
T²[0][0] = T[0][0]×T[0][0] + T[0][1]×T[1][0] + T[0][2]×T[2][0] + T[0][3]×T[3][0] + T[0][4]×T[4][0] + T[0][5]×T[5][0] = 4×4 + 4×4 + 4×0 + 4×4 + 0×4 + 4×4 = 16 + 16 + 0 + 16 + 0 + 16 = 64类似计算所有位置,得到T²矩阵:
T² = [ [64, 64, 64, 48, 64, 64], [64, 64, 48, 64, 64, 64], [64, 48, 64, 64, 64, 64], [48, 64, 64, 64, 64, 64], [64, 64, 64, 64, 64, 64], [64, 64, 64, 64, 64, 64] ]然后计算T³ = multiply(T, T², MOD):
以计算T³[0][0]为例:
T³[0][0] = T[0][0]×T²[0][0] + T[0][1]×T²[1][0] + T[0][2]×T²[2][0] + T[0][3]×T²[3][0] + T[0][4]×T²[4][0] + T[0][5]×T²[5][0] = 4×64 + 4×64 + 4×64 + 4×48 + 0×64 + 4×64 = 256 + 256 + 256 + 192 + 0 + 256 = 1216类似计算所有位置,得到T³矩阵:
T³ = [ [1216, 1216, 1216, 1152, 1216, 1216], [1216, 1216, 1152, 1216, 1216, 1216], [1152, 1216, 1216, 1216, 1216, 1216], [1216, 1216, 1216, 1216, 1216, 1152], [1216, 1216, 1216, 1216, 1216, 1216], [1216, 1216, 1216, 1216, 1216, 1216] ]6. 计算最终结果
long ans = 0; for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { ans = (ans + 4 * M[i][j]) % MOD; } }计算所有元素和:
行1: 1216+1216+1216+1152+1216+1216 = 7232 行2: 1216+1216+1152+1216+1216+1216 = 7232 行3: 1152+1216+1216+1216+1216+1216 = 7232 行4: 1216+1216+1216+1216+1216+1152 = 7232 行5: 1216+1216+1216+1216+1216+1216 = 7296 行6: 1216+1216+1216+1216+1216+1216 = 7296 总和 = 7232×4 + 7296×2 = 28928 + 14592 = 43520最终结果:
ans = 4 × 43520 = 174080由于174080 < MOD=1,000,000,007,所以不需要取模。
最终输出:
174080验证逻辑:
第一个骰子:24种方式
后面3个骰子:转移3次,每次转移方案数由T矩阵决定
最终所有合法堆叠方案数为174080种
代码讲解
1、opp数组
下标1-6表示骰子点数,值表示对面点数,表示题目要求的对应关系
int[] opp = {0, 4, 5, 6, 1, 2, 3}; // 骰子对面关系数组
2、conflict矩阵
定义7*7的矩阵,即0~6,索引1-6使用;键盘输入冲突关系,记录为"true",有两个,因为1不能对2,那肯定2也不能对1。
// 初始化冲突矩阵
boolean[][] conflict = new boolean[7][7];
for (int i = 0; i < m; i++) {
int a = scan.nextInt();
int b = scan.nextInt();
conflict[a][b] = true;
conflict[b][a] = true;
}
3、特殊情况,即只有一个骰子
6个面都可以作为底面,然后剩下4个面可以自由旋转,就是朝向不同,即4*6=24。
if (n == 1) {
System.out.println(24);
scan.close();
return;
}
4、构建转移矩阵
先循环第一个骰子的顶面,在循环第二个骰子的顶面,因为顶面对底面是严格要求的,在opp数组规定了,所以根据第二个骰子的顶面得到第二个骰子的底面,此时在判断顶面和底面是否冲突,如果冲突就不记录,不冲突记录为4,因为在确定顶面和底面,还可以旋转4个面的朝向,索引是0开始的,骰子是1~6,所以需要减1,转1~6为0~5。
第一个骰子是1,下一个骰子是1~6的方案数。循环!
// 构建转移矩阵
long[][] T = new long[6][6];
for (int top = 1; top <= 6; top++) {
for (int nextTop = 1; nextTop <= 6; nextTop++) {
int bottom = opp[nextTop]; // 下一个骰子的底面
if (!conflict[top][bottom]) {
T[top - 1][nextTop - 1] = 4;
}
}
}
5、矩阵的n-1次幂
为什么是n-1次幂:
-
第一个骰子单独考虑
-
后面n-1个骰子需要进行n-1次状态转移
-
状态转移n-1次 = 转移矩阵T连乘n-1次 = T^(n-1)
long[][] M = matrixPower(T, n - 1, MOD, opp);
6、结果计算
-
M[i][j]表示从第一个骰子顶面i+1开始,经过n-1次转移,到最后一个骰子顶面j+1的路径数 -
但是,每条路径中,第一个骰子还有4种旋转方式没有考虑
-
所以每条路径需要乘以4(第一个骰子的旋转方式)
// 计算最终结果
long ans = 0;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
ans = (ans + 4 * M[i][j]) % MOD;
}
}
System.out.println(ans);
scan.close();
}
7、矩阵乘法
计算两个矩阵的乘法
A = 从步骤1到步骤2的转移矩阵
B = 从步骤2到步骤3的转移矩阵
C = A × B = 从步骤1直接到步骤3的转移矩阵
// 矩阵乘法
static long[][] multiply(long[][] A, long[][] B, long MOD) {
int n = A.length;
int p = B.length;
int m = B[0].length;
long[][] C = new long[n][m];
for (int i = 0; i < n; i++) {
for (int k = 0; k < p; k++) {
if (A[i][k] == 0) continue;
for (int j = 0; j < m; j++) {
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
}
}
return C;
}
8、矩阵快速幂运算
矩阵的快速幂运算
-
初始化结果矩阵为单位矩阵:单位矩阵相当于乘法的1,任何矩阵乘以单位矩阵等于它本身
-
循环计算:
-
如果当前指数的最低位是1:将结果乘以当前底数
-
底数自乘(平方)
-
指数右移一位(除以2)
-
-
继续直到指数为0
-
13的二进制:1101
3¹³ = 3⁸ × 3⁴ × 3¹
算法过程:
初始:res=1, base=3, exp=13
-
exp=13(1101),最低位是1:res=1×3=3,base=3×3=9,exp=6
-
exp=6(110),最低位是0:res不变,base=9×9=81,exp=3
-
exp=3(11),最低位是1:res=3×81=243,base=81×81=6561,exp=1
-
exp=1(1),最低位是1:res=243×6561=1594323,base自乘,exp=0
结束:res=3¹³
-
// 矩阵快速幂运算
static long[][] matrixPower(long[][] base, long exp, long MOD, int[] opp) {
int n = base.length;
long[][] res = new long[n][n];
// 初始化为单位矩阵
for (int i = 0; i < n; i++) {
res[i][i] = 1;
}
while (exp > 0) {
if ((exp & 1) == 1) {
res = multiply(res, base, MOD);
}
base = multiply(base, base, MOD);
exp >>= 1;
}
return res;
}
结语
这道题用了动态规划,构建矩阵还是很难想的吧,其次矩阵的计算那个代码是模板,没怎么写过的话,理解起来还是有点难度的,但没事,慢慢就理解了,希望这个可以帮助你理解,有什么问题或者有帮助理解的方法和更优解的算法,欢迎评论呀!加油加油!