2015蓝桥杯省赛C/C++ 垒骰子 两种思路讨论
好久没写博客了,上次写好像还是上一届蓝桥杯…
话不多说,我们先上题。
赌圣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
思路一
第一种思路来自笨蛋博主------我。
不要被题干吓到, 有很多骰子, 然后每个都有不同的情况进行组合, 这不是很明显的深度优先搜索嘛! (其实这题还蛮好想的)
这个题其实就分三个部分:
- 输入输出
- check一下是不是允许贴合的情况。
- dfs
前两个难吗? 一点都不难。
稍微复杂一点的来自于第三个dfs, 直接来看dfs的部分:
// num: 当前已经垒了几个骰子
// c: 上一个骰子(下方骰子)的上方数字
// check函数反映了两个数字能不能贴合到一起
// b[6] = {4, 5, 6, 1, 2, 3}
void dfs(int num, int c)
{
// 如果判断了n个骰子, 说明是一种情况
if (num == n)
{
sum++;
return;
}
// i表示上方骰子的下方数字
for (int i = 1; i <= 6; i++)
{
// 如果可以放, 则进入上方骰子的判断
if (check(i, c))
dfs(num+1, b[i - 1]);
}
}
}
其实也没什么好讲的, 去遍历上方骰子的下方数字, 查一下跟下方骰子的上方数字能不能贴合, 能就进入上方骰子的深搜, 搜到顶部就情况加1。
但是这样就完了吗? 给你五秒钟思考。
忽略的地方刚刚我们的dfs过程考虑了每一种情况吗? 对于上下面来说, 是的, 但是不要忘了, 骰子有六个面, 所以即使上下面是一样的, 它也可以左右动一动, 所以说, 每多一个骰子, 我们就要乘一个4, 也就是下方完整代码里, 结果处理乘的4^n。
下面贴上完整代码:
dfs方法完整代码#include <bits/stdc++.h>
using namespace std;
const long long mod = 10e9 + 7;
// 该题是一个典型的dfs, 用数组去记录不允许紧贴的情况
// 去深搜第一个骰子的六种情况, 要注意的是
// 每个骰子有四个侧面, 而dfs只深搜紧贴面的情况
// 所以结果要乘4^n
int m, n;
// 两个不能贴在一起的数组
int a[7][7];
// 对应储存
int b[6] = {4, 5, 6, 1, 2, 3};
int sum = 0;
// 检查是否有该不允许的情况
bool check(int first, int second)
{
if (a[first][second] || a[second][first])
return false;
return true;
}
// num表示当前已判定骰子的数目, c代表下方骰子的上方数字
void dfs(int num, int c)
{
// 如果判断了n个骰子, 说明是一种情况
if (num == n)
{
sum++;
return;
}
// i表示上方骰子的下方数字
for (int i = 1; i <= 6; i++)
{
// 如果可以放, 则进入上方骰子的判断
if (check(i, c))
dfs(num+1, b[i - 1]);
}
}
int main()
{
// 输入
cin >> n >> m;
int t1, t2;
for (int i = 1; i <= m; i++)
{
cin >> t1 >> t2;
a[t1][t2] = 1;
}
// 遍历搜索
for (int i = 1; i <= 6; i++)
{
dfs(1, i);
}
// 结果处理
for (int i = 1; i <= n; i++)
{
sum *= 4;
sum %= mod;
}
cout << sum;
}
- 看到这里(甚至说你看不到这里), 你可能已经觉得我很捞, 就这水平还来写博客呢, 数据规模这么大, 还用int定义变量。
- 其实, 你在第二层, 而我在第五层, 我压根没想用这种方法完全解出来。
- 虽然只是省赛, 虽然只是暴力杯, 但是你想用dfs完美地解出第九题, 那是不可能滴。
- 一般题做到这里, 考的就不是你的解题能力了, 考的是你的优化能力, dfs确实能解出给的样例, 但是数据规模这么大, 时间这么短, dfs一定是不能满足所有检查点, 甚至说解出样例已经很成功了。
- 这个题要用更优化的方法。也就是dp。
思路二
本思路参考于某大佬的解答, 顺便催一催大佬的更, 好几年前挖的坑还没填(笑)。
由于代码不是我写的, 所以我就给带伙说一下思路。
dp三板斧:
- 数组
- 临界条件
- 状态转移方程
1.首先想一下, 第n个骰子某个底面的情况有多少种? 是不是只跟第n-1个骰子有关系?
那么我们不妨假设: f[m][n]表示第m个骰子底面为n时, 有多少种情况。
2.我们知道的临界条件: 只摆放第一个骰子的时候, 它的六种底面的情况数都是1(如果你非得觉得自己闲的淡疼, 往上随便你写~)
3.状态转移方程: 其实看到这里状态转移方程就比较清晰了:
f[m][n]等于第m-1层中所有不与n相斥的方案数累加。
最后补充一个小知识点: 滚动数组
属于是dp的继续优化(折磨), 其实很简单, 因为第n层只跟第n-1层有关系, 而我们要的是最后的总结果, 所以前面用完就丢掉就好了, 这样数组就只需要两行, 比起之前的n行, 那节省的空间可相当大(很多时候时间合格了, 但是空间利用太大被卡住就很难受)。
下面奉上完整代码, 稍微改动了一点小白不好理解的地方, 如果看客老爷够厉害, 可以直接去看大佬的代码
如果看不懂, 把滚动数组的部分去掉可能会清晰很多
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL Mod = 1e9 + 7;
int n, m, pos = 0;
LL res, f[2][7], sum;
bool a[7][7];
int b[7] = {0, 4, 5, 6, 1, 2, 3};
// 检查是否有该不允许的情况
bool check(int first, int second)
{
if (a[first][second] || a[second][first])
return false;
return true;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
a[x][y] = a[y][x] = 1;
}
// 边界条件 第一层每面向上为1
for (int i = 1; i <= 6; i++)
f[pos][i] = 1;
// 枚举2-n层
for (LL i = 2; i <= n; i++)
{
//滚动 0 1交替
pos = 1 - pos;
// 点数为j的向上
for (int j = 1; j <= 6; j++)
{
// 滚动回来先清零
f[pos][j] = 0;
// i-1层的顶面
for (int k = 1; k <= 6; k++)
// 点数j的对应面能否不相斥
if (check(b[j],k))
f[pos][j] += f[1 - pos][k];
// 防止溢出
f[pos][j] %= Mod;
}
}
// 最上方骰子6个面朝下的总情况数加到一起
for (int i = 1; i <= 6; i++)
sum = (sum + f[pos][i]) % Mod;
// 跟思路一一样的结果处理
for (int i = 0; i < n; i++)
{
sum *= 4;
sum %= Mod;
}
// 大佬原本用的位运算, 我给改成最简单的循环了
// 位运算要比循环快多了
// res = ksm(4, n);
// sum = sum * res % Mod;
cout << sum;
}
顶底都一样啦, 不要在意这么多细节~
好久之后的一篇博客啊, 有自己的思路见解(我很辣鸡)才会写一点跟大家分享, 虽然说本着开源的精神, 但是架不住我是条懒狗…后续肯定会再分享的, 先咕咕咕(笑)。
蓝桥杯只是一个起点, 虽然学着很恶心很痛苦, 但你总有信手拈来的那一天。