蓝桥杯第十届 决赛 部分 题解 C/C++ 上半部分(填空题)

爱情到来绝不能犹豫。
关注博主容不得迟缓。
§( ̄▽ ̄

试题:质数拆分

题目描述:

将2019 拆分为若干个两两不同的质数之和,一共有多少种不同的方法?
注意交换顺序视为同一种方法,例如 2+2017=2019 与 2017 + 2 = 2019视为同一种方法。

解析:

我们很容易想到,如果我们求2+2017=2019而2017又可以拆成X+X这样的形式,所以2019的方案数应该是“配对成2的个数”加“配对乘2017”所有的个数。

由此我们想到了dp的转移方案:
前i个数指的是:2,3,5,7,。。。这样的数(使用素数筛查先算出来这个序列)
dp[i][j]表示千i个数配出j的方案总数。
那么dp[i][j]由dp[i-1][j]转移一次,看看不选第i个,能不能配出。
然后再由dp[i-1][j-prime[i]]看看如果选了第i个,能不能配出。

正解代码:

#include <cstring>
#include <iostream>
using namespace std;
bool is_prime[2019 + 5];
#define N 2019
int prime[1000], p = 0;  // p就是prime元素的个数
unsigned long long dp[1000][N + 5] = {0};
void do_prime(int x) {  //埃氏素数筛法
    memset(is_prime, true, sizeof(is_prime));
    is_prime[0] = is_prime[1] = false;
    for (int i = 2; i <= x; i++) {
        if (is_prime[i]) {
            prime[p++] = i;
            for (int j = i * 2; j <= x; j += i) {
                is_prime[j] = false;
            }
        }
    }
}
int main() {
    memset(prime, 0, sizeof(prime));
    do_prime(N);  //筛选素数
    dp[0][0] = 1;
    for (int i = 0; i < p; i++) {
        for (int j = 0; j <= N; j++) {
            dp[i + 1][j] += dp[i][j];
            if (j - prime[i] >= 0)
                dp[i + 1][j] += dp[i][j - prime[i]];
        }
    }
    cout << dp[p][N];
    return 0;
}

试题:拼接

题目描述:

小明要把一根木头切成两段,然后拼接成一个直角。

如下图所示,他把中间部分分成了n X n 的小正方形,他标记了每个小正方形属于左边还是右边。然后沿两边的分界线将木头切断,将右边旋转向上后拼 接在一起。

在这里插入图片描述

要求每个小正方形都正好属于左边或右边,而且同一边的必须是连通的。在拼接时,拼接的部位必须保持在原来大正方形里面。

请问,对于 7×7 的小正方形,有多少种合法的划分小正方形的方式。

解析:

首先非常有必要理解,这个右边这个方块是怎么跑到最上面的。
实际上,这个方块跑到最上面是因为,按照左下这个点和右上这个点的连线对折,然后去折叠上去的。
请看下图:在这里插入图片描述
在这里插入图片描述
那么对于图中的样例:
我们可以模拟这个过程去找规律。在这里插入图片描述
在这里插入图片描述
我们固定标有“左”这个字符的方块不动,然后对“标有右”这个字符的方块进行反转。
其中对于右边得这个方块,在对称线(最下角到右上角)的上方的会被反转到下方。而在对称线的下方则会反转到上方。

注意观察最后一幅图片的黑色线。反转之后,黑色线会重合。这是因为,切开的方块再拼的时候,缝隙要对齐。

**程序突破口:分割线一定会按照对称线对称。**也只有这样,割下来才可能进行一个完美的反转然后拼在一起。

设计:
我们可以在分割线以下的区域进行画线。从分割线出发,一直画线到末尾。计算总共我们能画出多少条线。(分割线以上不用画线的原因是,因为存在对称线,分割线以下画好,分割线已上就自然画好了)

坑点1:我们从对称线出发,不能再次回到对称线。
在这里插入图片描述
如上图,这样的话,会导致中间割出一个空洞。

坑点2:在边缘处要特别小心。在这里插入图片描述
比如此图,右边到底链接的是(1)还是(2)。???或者说,X是掉下来的。

所以dfs当到最底下,不能再往上去搜索,注意观察X号左边(红色叉叉)的那一个边。是不能再次往上的。

还有几个坑点(1)画出的线不能交叉。(2)边界等等。

边界:我们总是想要让停止的地方在最右侧。
这是因为:在这里插入图片描述
你看,现在没有在最右侧停止,然后现在最左上方的那个木块更本就没有割下来。

对于对称线的最左顶点(0,0)只有一种就是横着画。
同一最右顶点(7,7)也只有竖着画。总共多出2种答案。

#include <iostream>
using namespace std;
int ans = 0;
int vis[8][8];
int dx[] = {1, 0, -1, 0};
int dy[] = {0, -1, 0, 1};
void dfs(int x, int y) {
    if (x == 7) {
        ans++;
        return;
    }
    vis[x][y] = 1;
    for (int i = 0; i < 4; i++) {
        int tx = x + dx[i], ty = y + dy[i];
        if (y == 0 && ty == y + 1)//对于坑点:边界要特别小心
            continue;
        if ((vis[tx][ty] == 1) || (tx < 0 || ty < 0) || (tx <= ty))
        //tx<=ty是说,判断我们有没有跑出去或者将要到分界线上。在y=x之下是y<x
            continue;
        dfs(tx, ty);
    }
    vis[x][y] = 0;
}
int main() {
    for (int i = 1; i < 7; i++) {
        dfs(i, i);  //从对角线上每一个点出发查找
    }
    //别忘记加上最后(0,0)和(7,7)两个点的方案。每个点只有一种切法,所以+2
    cout << ans + 2;
    return 0;
}

试题:路径计数

题目描述:

解析:

建立(1,1)到(6,6)的坐标系。(1,1)到(1,2)是线段长度为1的线。

本题一看就知道是dfs,具体的dfs不在我们这篇文章讨论,因为比较基础。

本题技巧就是,怎么判断这个点是(1,1)的访问次数。也就是我们访问过(1,1)然后第二次访问。那么我们在进行下一层的深搜的时候人为加上条件(Tx,Ty)为(1,1)的时候继续搜。这样就可以手动强制再搜索一次。

(1,1)第二次访问之后还是要小心:出发之后立即就返回的情况,什么意思:
在这里插入图片描述
此时step=2(虽然被两次访问),但是我们还是要排除这种答案。当step>2的时候,我们才ans++。(请结合代码理解)

#include <iostream>
using namespace std;
int map[10][10];
int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};
int ans = 0;
bool check(int x, int y) {
    return x >= 1 && x <= 6 && y >= 1 && y <= 6;
}
void dfs(int x, int y, int step) {
    if (step > 12) {  //没必要的访问
        return;
    }

    if (map[x][y] == 1) {  //这个点染过染色却还是被访问了,只有一种可能,这个点是(1,1)
        // cout << x << " " << y << " " << step << endl;
        if (step > 2)  //考虑不利己返回。如(1,2,1)然后(1,1,2)这就是立即返回了,所以是step>2
            ans++;
        return;
    }

    map[x][y] = 1;

    for (int i = 0; i < 4; i++) {
        int tx = x + dx[i], ty = y + dy[i];
        if ((tx == 1 && ty == 1) || (check(tx, ty) && map[tx][ty] == 0)) {  //扩展的条件是没被染色,或者是(1,1)
            dfs(tx, ty, step + 1);
        }
    }

    map[x][y] = 0;
}
int main() {
    dfs(1, 1, 0);
    cout << ans;
    return 0;
}

本题结束了吗?
还没有呢~我想让你观察下面的代码,然后告诉我为什么答案是208,不是206。
一个小小的提示:答案出错的原因在于step=4的时候出的错,step=4本应该有两种,可是错误答案有4种。
代码如下:

#include <iostream>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];

//判断是否合法
bool check(int x, int y) {
    return x >= 0 && x <= 5 && y >= 0 && y <= 5;
}

void dfs(int x, int y, int dep) {
    if (dep > 4) {  // dep>12 改为dep>4方便调试
        return;     //超过限制条件
    }
    if (x == 0 && y == 0 && dep > 2) {
        //达成目标计数,dep>2保证原点出发不立刻返回
        ans++;
        return;
    }
    vis[x][y] = 1;  //标记当前结点,保证路线不自交
    vis[0][0] = 0;
    for (int i = 0; i < 4; i++) {
        //从当前结点(x,y)出发四个方向继续dfs未走过的路径
        int xx = x + dir[i][0];
        int yy = y + dir[i][1];
        if (check(xx, yy) && !vis[xx][yy]) {
            dfs(xx, yy, dep + 1);
        }
    }
    vis[x][y] = 0;  //去除标记,可能会被别的路线访问
}

int main() {
    dfs(0, 0, 0);  // dfs
    cout << ans << endl;
    return 0;
}

如果你说使用debug调试,我想这也许太复杂了。我们只需要创建一个stack。模拟入栈出栈的这个过程就可以了。
例如:

            Sta.push_back(P(xx, yy));
            dfs(xx, yy, dep + 1);
            Sta.pop_back();

Debug代码全貌:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];

typedef pair<int, int> P;
vector<P> Sta;

//判断是否合法
bool check(int x, int y) {
    return x >= 0 && x <= 1 && y >= 0 && y <= 1;
}

void dfs(int x, int y, int dep) {
    if (dep > 4) {  // dep>12 改为dep>4方便调试
        return;     //超过限制条件
    }
    if (x == 0 && y == 0 && dep > 2) {
        //达成目标计数,dep>2保证原点出发不立刻返回
        ans++;
        for (auto ite = Sta.begin(); ite != Sta.end(); ite++) {
            cout << "(" << (*ite).first << "," << (*ite).second << ")" << endl;
        }
        cout << endl;
        return;
    }
    vis[x][y] = 1;  //标记当前结点,保证路线不自交
    vis[0][0] = 0;
    for (int i = 0; i < 4; i++) {
        //从当前结点(x,y)出发四个方向继续dfs未走过的路径
        int xx = x + dir[i][0];
        int yy = y + dir[i][1];
        if (check(xx, yy) && !vis[xx][yy]) {
            Sta.push_back(P(xx, yy));
            dfs(xx, yy, dep + 1);
            Sta.pop_back();
        }
    }
    vis[x][y] = 0;  //去除标记,可能会被别的路线访问
}

int main() {
    Sta.push_back(P(0, 0));
    dfs(0, 0, 0);  // dfs
    Sta.pop_back();
    cout << ans << endl;
    return 0;
}

输出结果:
在这里插入图片描述

出问题的两组是:第一组,和第四组。
因为它们含有两个(0,0)这是不正确的。
因为从第二个(0,0)不应该是扩展点。也就是我们看第一组
(0,0)(0,1)(0,0)—>(1,0)
第二个(0,0)是不能继续往下搜索的。应该及时的返回。
在这里插入图片描述
这是我的调试界面。注意左边的数据。
现在x,y是(0,0)而Sta是调试vector,现在我们调试的就是第一组数据。
然后光标指向了第30行。没有及时地退出,反而继续进行了dfs。这是错误的。
在30行之前加入代码:

    if (dep != 0 && (x == 0 && y == 0)) {  //防止dep<=2的时候弹不出去。
        return;
    }

dep可以很好的帮助我们判断是第几个(0,0)当dep=0,的时候是第一个(0,0)是不需要弹出的。
现在代码修改完毕。运行结果恢复正常。
在这里插入图片描述
我们回到原问题:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int ans = 0;
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int vis[10][10];

typedef pair<int, int> P;
vector<P> Sta;

//判断是否合法
bool check(int x, int y) {
    return x >= 0 && x <= 5 && y >= 0 && y <= 5;
}

void dfs(int x, int y, int dep) {
    if (dep > 12) {  // dep>12 改为dep>4方便调试
        return;      //超过限制条件
    }
    if (x == 0 && y == 0 && dep > 2) {
        //达成目标计数,dep>2保证原点出发不立刻返回
        ans++;
        /* 关闭调试输出
        for (auto ite = Sta.begin(); ite != Sta.end(); ite++) {
            cout << "(" << (*ite).first << "," << (*ite).second << ")" << endl;
        }
        cout << endl;
        return;
        */
    }
    if (dep != 0 && (x == 0 && y == 0)) {  //防止dep<=2的时候弹不出去。
        return;
    }
    vis[x][y] = 1;  //标记当前结点,保证路线不自交
    vis[0][0] = 0;
    for (int i = 0; i < 4; i++) {
        //从当前结点(x,y)出发四个方向继续dfs未走过的路径
        int xx = x + dir[i][0];
        int yy = y + dir[i][1];
        if (check(xx, yy) && !vis[xx][yy]) {
            Sta.push_back(P(xx, yy));
            dfs(xx, yy, dep + 1);
            Sta.pop_back();
        }
    }
    vis[x][y] = 0;  //去除标记,可能会被别的路线访问
}

int main() {
    Sta.push_back(P(0, 0));
    dfs(0, 0, 0);  // dfs
    Sta.pop_back();
    cout << ans << endl;
    return 0;
}

现在答案恢复正常了,最终答案是206,我为什么花这么长的篇幅写这道简单问题的答案。因为这个问题,实际看起来简单。确实最容易出错的那种类型。很多很多的边界条件思考不到,各种小小的错误都可能导致最后答案的错误,所以一定不能放过细节上的问题,勤加思考。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值