垒骰子
题目描述
赌圣 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
总通过次数: 848 | 总提交次数: 2042 | 通过率: 41.5%
难度: 困难 标签: 2015, 省赛, 动态规划, 搜索
算法思路:矩阵快速幂优化动态规划
核心问题分析
垒骰子问题需要计算在给定互斥条件下垒n个骰子的合法方式数量,并对 109+7 取模。关键挑战在于:
- 骰子结构:1-4、2-5、3-6互为对面
- 互斥条件:若数字a与b互斥,则a的面不能与b的面紧贴
- 旋转特性:每个骰子可绕垂直轴旋转,固定上下两面后有4种摆放方式
- 超大n值:n≤109,传统动态规划会超时
优化策略:矩阵快速幂
-
状态定义:
- 设 dp[i][j] 表示第i个骰子(从下往上)顶面为j的方案数(j∈[1,6])
- 第i个骰子底面为 op[j](对面关系)
-
转移方程:

-
乘以4是因为固定上下两面后,骰子可水平旋转4次
-
矩阵化转换:
- 将DP转移表示为6×6矩阵乘法
- 构造转移矩阵 A:A[k][j]={10若 k 与 op[j] 不互斥否则
- 状态向量关系:Fn=F1×An−1
- 最终方案数:(∑Fn)×4nmod109+7
矩阵快速幂原理
- 时间复杂度:O(63logn),完美处理 n=109
C++ 代码实现
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int DICE = 6;
// 骰子对面关系:1-4, 2-5, 3-6
const int op[] = {0, 4, 5, 6, 1, 2, 3};
struct Matrix {
ll m[DICE][DICE];
Matrix() { memset(m, 0, sizeof(m)); }
// 矩阵乘法
Matrix operator*(const Matrix& other) const {
Matrix res;
for (int i = 0; i < DICE; i++)
for (int k = 0; k < DICE; k++)
for (int j = 0; j < DICE; j++)
res.m[i][j] = (res.m[i][j] + m[i][k] * other.m[k][j]) % MOD;
return res;
}
};
// 矩阵快速幂
Matrix matrix_pow(Matrix base, ll exp) {
Matrix res;
for (int i = 0; i < DICE; i++) res.m[i][i] = 1; // 单位矩阵
while (exp) {
if (exp & 1) res = res * base;
base = base * base;
exp >>= 1;
}
return res;
}
// 整数快速幂
ll pow_mod(ll base, ll exp, ll mod) {
ll res = 1;
while (exp) {
if (exp & 1) res = (res * base) % mod;
base = (base * base) % mod;
exp >>= 1;
}
return res;
}
int main() {
ll n; int m;
cin >> n >> m;
// 初始化转移矩阵(全1表示初始无约束)
Matrix A;
for (int i = 0; i < DICE; i++)
for (int j = 0; j < DICE; j++)
A.m[i][j] = 1;
// 处理互斥条件
while (m--) {
int a, b;
cin >> a >> b;
// 禁止a在下方时b在上方
A.m[a-1][op[b]-1] = 0;
// 禁止b在下方时a在上方
A.m[b-1][op[a]-1] = 0;
}
// 计算A^(n-1)
Matrix An = matrix_pow(A, n-1);
// 求和初始状态到最终状态的路径
ll total = 0;
for (int i = 0; i < DICE; i++)
for (int j = 0; j < DICE; j++)
total = (total + An.m[i][j]) % MOD;
// 乘以旋转方案数
total = total * pow_mod(4, n, MOD) % MOD;
cout << total << endl;
return 0;
}
代码解析
-
数据结构:
Matrix类封装6×6矩阵运算op[]存储骰子对面关系(索引1~6)
-
关键函数:
matrix_pow():矩阵快速幂,O(logn) 计算 An−1pow_mod():整数快速幂,高效计算 4nmod109+7
-
流程控制:
- 初始化:转移矩阵全1(无约束时所有转移合法)
- 互斥处理:对每个互斥对(a,b),将矩阵中对应位置置0
- 结果计算:矩阵求和后乘以旋转方案数
实例验证
输入:2 1 和 1 2
-
矩阵构建:
- 初始矩阵全1(36个有效转移)
- 互斥(1,2) → 禁止转移:
(1→5)和(2→4)

-
计算过程:
- A1 元素和 = 34(原36 - 2个禁止)
- 42=16
- 结果 = 34×16=544 ✓
注意事项
-
下标转换:
- 骰子编号1~6 → 矩阵索引0~5
op[b]-1将骰子值转换为矩阵列索引
-
取模时机:
- 矩阵乘法每步取模防溢出
- 最终结果取模
-
边界处理:
- n=1时直接输出 6×4=24
- m=0时矩阵保持全1
测试点设计
| 测试类型 | 输入 | 预期输出 | 验证要点 |
|---|---|---|---|
| 最小规模 | 1 0 | 24 | 单骰子旋转 |
| 无约束 | 2 0 | 576 | 全连接验证 |
| 完全互斥 | 2 36 | 0 | 无合法方案 |
| 部分互斥 | 2 1\n1 2 | 544 | 示例验证 |
| 最大规模 | 1e9 0 | 非0 | 时间/内存压力测试 |
| 对立面互斥 | 2 1\n1 4 | 0 | 对面必然紧贴 |
优化建议
-
矩阵乘法优化:
// 循环展开优化(6x6固定大小) for (int i = 0; i < 6; i++) { for (int k = 0; k < 6; k++) { if (!A.m[i][k]) continue; // 稀疏矩阵优化 for (int j = 0; j < 6; j++) { res.m[i][j] += A.m[i][k] * B.m[k][j]; } } } -
内存预分配:
- 矩阵大小固定为6×6,避免动态内存分配
-
并行计算:
#pragma omp parallel for collapse(2) for (int i = 0; i < 6; i++) for (int j = 0; j < 6; j++) // 并行计算元素 -
输入加速:
ios::sync_with_stdio(false); cin.tie(0); // 关闭同步加速输入

2120

被折叠的 条评论
为什么被折叠?



