2015蓝桥杯省赛C/C++ 垒骰子 两种思路讨论

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


思路一

第一种思路来自笨蛋博主------我。

不要被题干吓到, 有很多骰子, 然后每个都有不同的情况进行组合, 这不是很明显的深度优先搜索嘛! (其实这题还蛮好想的)

这个题其实就分三个部分:

  1. 输入输出
  2. check一下是不是允许贴合的情况。
  3. 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;
}

  1. 看到这里(甚至说你看不到这里), 你可能已经觉得我很捞, 就这水平还来写博客呢, 数据规模这么大, 还用int定义变量。
  2. 其实, 你在第二层, 而我在第五层, 我压根没想用这种方法完全解出来。
  3. 虽然只是省赛, 虽然只是暴力杯, 但是你想用dfs完美地解出第九题, 那是不可能滴。
  4. 一般题做到这里, 考的就不是你的解题能力了, 考的是你的优化能力, dfs确实能解出给的样例, 但是数据规模这么大, 时间这么短, dfs一定是不能满足所有检查点, 甚至说解出样例已经很成功了。
  5. 这个题要用更优化的方法。也就是dp。

思路二

本思路参考于某大佬的解答, 顺便催一催大佬的更, 好几年前挖的坑还没填(笑)。

由于代码不是我写的, 所以我就给带伙说一下思路。

dp三板斧:

  1. 数组
  2. 临界条件
  3. 状态转移方程

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;
}

顶底都一样啦, 不要在意这么多细节~

好久之后的一篇博客啊, 有自己的思路见解(我很辣鸡)才会写一点跟大家分享, 虽然说本着开源的精神, 但是架不住我是条懒狗…后续肯定会再分享的, 先咕咕咕(笑)。

蓝桥杯只是一个起点, 虽然学着很恶心很痛苦, 但你总有信手拈来的那一天。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值