[蓝桥杯][2015年第六届真题]垒骰子
题目描述:
赌圣atm晚年迷恋上了垒骰子,就是把骰子一个垒在另一个上边,不能歪歪扭扭,要垒成方柱体。
经过长期观察,atm 发现了稳定骰子的奥秘:有些数字的面贴着会互相排斥!
我们先来规范一下骰子:1 的对面是 4,2 的对面是 5,3 的对面是 6。
假设有 m 组互斥现象,每组中的那两个数字的面紧贴在一起,骰子就不能稳定的垒起来。 atm想计算一下有多少种不同的可能的垒骰子方式。
两种垒骰子方式相同,当且仅当这两种方式中对应高度的骰子的对应数字的朝向都相同。
由于方案数可能过多,请输出模 10^9 + 7 的结果。
不要小看了 atm 的骰子数量哦~
输入
第一行两个整数 n m
n表示骰子数目
接下来 m 行,每行两个整数 a b ,表示 a 和 b 不能紧贴在一起。
输出
一行一个数,表示答案模 10^9 + 7 的结果。
样例输入
2 1
1 2
样例输出
544
提示
「数据范围」
对于 30% 的数据:n <= 5 对于 60% 的数据:n <= 100
对于 100% 的数据:0 < n <= 10^9, m <= 36
资源约定:
峰值内存消耗 < 256M CPU消耗 < 2000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意: main函数需要返回0
注意: 只使用ANSI C/ANSI C++ 标准,不要调用依赖于编译环境或操作系统的特殊函数。 注意: 所有依赖的函数必须明确地在源文件中 #include , 不能通过工程设置而省略常用头文件。
提交时,注意选择所期望的编译器类型。
思路:应该马上能想到用暴力解法,也就是深收、递归的做法。虽然明显会超时。。。应该有30%的分值
做法也非常简单,就是总数会等于最上面的数目(分别六种朝上的面)即6来乘以剩下的n-1颗骰子的情况。。。。
然后一直递归下去。。。
细心的朋友已经发现了,你不用转动吗?
所以我们最后的结果会乘以4n
不要忘记%MOD
#include<iostream>
using namespace std;
int op[7];
bool conflict[7][7];
const int MOD=100000007;
//define MOD 1000000007
void init(){
op[1]=4;
op[4]=1;
op[2]=5;
op[5]=2;
op[3]=6;
op[6]=3;
}
int f(int up,int cnt){//up代表上一颗骰子的朝向,cut代表剩下的骰子数目
//出口
if(cnt==0) return 1; //这里代表上一层朝上的是up,但是这一层我们没有骰子了
//所以上一层朝上是up的数目是4*1
long long ant=0;
for(int upp=1;upp<=6;upp++){
if(conflict[op[up]][upp]==true) continue;
ant=(ant+4*f(upp,cnt-1))%MOD;
}
return ant;
}
int main(){
int n,m;
cin>>n>>m;
init();
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
conflict[x][y]=true;
conflict[y][x]=true;
}
long long ans=0;
for(int up=1;up<=6;up++){
ans=((ans+4*f(up,n-1))%MOD);
}
cout<<ans;
return 0;
}
思路二:动态规划
刚才是从上往下来算,这次我们最下面一个骰子开始(其实都一样,我只是定义一个正反向,方便在计算骰子的“对面”时不搞混)
虽然还是不能拿满分,但是可以拿70%的分值
本质就是定义一个滚动数组来记录 “ 两层 ”值,0层是上一次放好的每面向上的个数,1层用来更新数据,给下次用。
代码及解释如下:
#include<iostream>
using namespace std;
int op[7];
long long dp[2][7];//滚动使用
bool conflict[7][7];
const int MOD=100000007;
//define MOD 1000000007
void init(){
op[1]=4;
op[4]=1;
op[2]=5;
op[5]=2;
op[3]=6;
op[6]=3;
}
int main(){
int n,m;
cin>>n>>m;
init();
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
conflict[x][y]=true;
conflict[y][x]=true;
}
//以上输入已完成
//初始化dp
for(int j=1;j<=6;j++){
dp[0][j] = 1;
}
int cur = 0;//用来使dp滚动
for(int level=2;level<=n; ++level){
cur = 1-cur;
//尝试将6个面放在当前这一层的朝上方向
for(int j=1;j<=6;++j){
dp[cur][j] = 0;//初始化以便下面更新
//将与op[j]不冲突的上一次层格子里面的数目累加起来
for(int i=1;i<=6;++i){
if(conflict[op[j]][i]) continue;//i代表上一次格子的i朝上的累加总数
dp[cur][j] = (dp[cur][j]+dp[1-cur][i]) % MOD;
}
}
}
long long sum=0;
for(int k=1;k<=6;++k){
sum = (sum+dp[cur][k])%MOD;
}
//快速幂运算
//求4的n次幂
//将2^22转化为 2^16 * 2^0 * 2^4 * 2^1 * 2^0
//22的二进制10110
long long ans = 1;
long long tem = 4;
long long p = n;
while(p!=0){
//位与运算,先将n和1转化为4*8个bit的二进制,如何1代表trueo
//0代表false,然后就是正常的与运算了
if(p & 1==1) ans=(ans*tem)%MOD;
tem=(tem*tem)%MOD;
p >>= 1; //p=p/2; 有符号右移运算 p=p/2^1
//本质上是转化为2进制补码然后右移,符号位补齐
}
cout<<(sum*ans)%MOD;
return 0;
}
思路三:说难想到的话其实也还好,当你在做动态规划的时候就会有这么点感觉,好像每次都是乘以一样的东西,然后我们把这东西定义为矩阵就行啦
但是问题是如果比赛已经将动态规划写好,还有时间改吗
代码及注释:
//跟动态规划一样,先放最下面一层
//冲突矩阵a[i][j]代表i+1向上时会不会与下面的j+1冲突
#include<iostream>
//#include<bits/stdc++.h>
#include<map>
#include<vector>
using namespace std;
typedef long long LL;
int n,m;
map <int,int> op;
const int MOD=1000000007;
//define MOD 1000000007
void init(){
op[1]=4;
op[4]=1;
op[2]=5;
op[5]=2;
op[3]=6;
op[6]=3;
}
//定义全1矩阵
struct M{
LL a[6][6];
M(){
for(int i=0;i<6;i++){
for(int j=0;j<6;j++){
a[i][j]=1;
}
}
}
};
//矩阵乘法
M mMultiply(M m1,M m2){
M ans;
for(int i=0;i<6;i++){
for(int j=0;j<6;j++){
ans.a[i][j]=0;
for(int k=0;k<6;k++){
ans.a[i][j]=(ans.a[i][j]+m1.a[i][k]*m2.a[k][j])%MOD;
}
}
}
return ans;
}
//矩阵M的k次幂
M mPow(M m,int k){
M ans;
//初始化ans为单位矩阵
for(int i=0;i<6;i++){
for(int j=0;j<6;j++){
if(i==j)
ans.a[i][j]=1;
else
ans.a[i][j]=0;
}
}
while(k!=0){
if((k & 1)==1) {
ans = mMultiply(ans,m);
}
m = mMultiply(m,m);
k >>= 1;
}
return ans;
}
//快速幂运算
//求4的n次幂
//将2^22转化为 2^16 * 2^0 * 2^4 * 2^1 * 2^0
//22的二进
LL TM(LL tem,LL n){
long long ans = 1;
//long long tem = 4;
long long p = n;
while(p!=0){
//位与运算,先将n和1转化为4*8个bit的二进制,如何1代表trueo
//0代表false,然后就是正常的与运算了
if(p & 1==1) ans=(ans*tem)%MOD;
tem=(tem*tem)%MOD;
p >>= 1; //p=p/2; 有符号右移运算 p=p/2^1
//本质上是转化为2进制补码然后右移,符号位补齐
}
return ans;
}
int main(){
init();
cin>>n>>m;
M cMatrix;//冲突矩阵
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
//冲突矩阵a[i][j]代表i+1向上时会不会与下面的j+1冲突
cMatrix.a[op[a]-1][b-1]=0;
cMatrix.a[op[b]-1][a-1]=0;
}
M cM_n_1 = mPow(cMatrix,n-1);
LL ans=0;
for(int j=0;j<6;j++){
for(int i=0;i<6;i++){
ans=(ans+cM_n_1.a[j][i])%MOD;
}
}
LL a = TM(4,n);
cout<<(ans*a)%MOD;
return 0;
}
本题要掌握的有
- 快速幂
- 矩阵定义,及矩阵乘法
- 矩阵快速幂
- 当出现这种”树状“,考虑动态规划及矩阵幂