蓝桥杯---垒骰子(Java实现,代码注释,图文讲解)

前言

这是一道蓝桥杯第六届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↔6

2. 构建冲突矩阵

复制代码
boolean[][] conflict = new boolean[7][7];
conflict[1][2] = conflict[2][1] = true;
conflict[3][4] = conflict[4][3] = true;
// 其他位置都是false

3. 构建转移矩阵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. 循环计算

    • 如果当前指数的最低位是1:将结果乘以当前底数

    • 底数自乘(平方)

    • 指数右移一位(除以2)

  3. 继续直到指数为0

  4. 13的二进制:1101

    3¹³ = 3⁸ × 3⁴ × 3¹

    算法过程:

    初始:res=1, base=3, exp=13

    1. exp=13(1101),最低位是1:res=1×3=3,base=3×3=9,exp=6

    2. exp=6(110),最低位是0:res不变,base=9×9=81,exp=3

    3. exp=3(11),最低位是1:res=3×81=243,base=81×81=6561,exp=1

    4. 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;

}

结语

这道题用了动态规划,构建矩阵还是很难想的吧,其次矩阵的计算那个代码是模板,没怎么写过的话,理解起来还是有点难度的,但没事,慢慢就理解了,希望这个可以帮助你理解,有什么问题或者有帮助理解的方法和更优解的算法,欢迎评论呀!加油加油!

相关推荐
小白|1 小时前
OpenHarmony + Flutter 混合开发深度实践:构建支持国密算法(SM2/SM3/SM4)与安全存储的金融级应用
算法·安全·flutter
数据门徒1 小时前
《人工智能现代方法(第4版)》 第9章 一阶逻辑中的推断 学习笔记
人工智能·笔记·学习·算法
喜欢吃燃面1 小时前
算法竞赛之排序算法
c++·学习·算法
秋深枫叶红1 小时前
嵌入式第三十篇——数据结构——哈希表
数据结构·学习·算法·哈希算法
Keep__Fighting1 小时前
【机器学习:决策树】
人工智能·算法·决策树·机器学习·scikit-learn
✎ ﹏梦醒͜ღ҉繁华落℘1 小时前
编程基础--数据结构
数据结构·算法
小毅&Nora1 小时前
【后端】【C++】泛型算法:从传统到C++20 Ranges的进化之旅
算法·c++20·泛函算法
沧澜sincerely2 小时前
蓝桥杯11 路径之谜
c++·蓝桥杯·stl·dfs·剪枝
ULTRA??2 小时前
最小生成树kruskal算法实现python,kotlin
人工智能·python·算法