12届蓝桥杯软件类省赛第一场_CB

文章目录

    • 试题 A: 空间
    • 试题 B: 卡片
    • 试题 C: 直线
    • 试题 D: 货物摆放
    • 试题 E: 路径
    • 试题 F: 时间显示
    • 试题 G: 砝码称重
    • 试题 H: 杨辉三角形
    • 试题 I: 双向排序
    • 试题 J: 括号序列
    • 赛后总结

第一次参加蓝桥杯省赛,补题写博客留个纪念~

试题 A: 空间

256 M B 256MB 256MB 的空间可以存储多少个 32 32 32 位二进制整数。

>>> 256*1024*1024*8//32
67108864

试题 B: 卡片

小蓝手里有 0 0 0 9 9 9 的卡片各 2021 2021 2021 张,共 20210 20210 20210 张,问小蓝可以从 1 1 1 拼到多少。

/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 B: 卡片
 * 解决方案: 模拟。记录各卡片剩余数量,每拼一个数字就减少相应的卡片,直到卡片不足。
 */

#include 
using namespace std;

int res[10];

// 拼数字x,失败返回false
bool handle(int x)
{
    while (x) {
        if (--res[x%10] < 0) return false;
        x /= 10;
    }
    return true;
}
int main()
{
    for (int i = 0; i < 10; ++i) res[i] = 2021;
    int n = 1;  // 从1开始拼
    while (handle(n)) {  // 拼不出n则退出循环
        ++n;
    }
    cout << n-1;  // 可以从1拼到n-1
    // 输出3181
    return 0;
}

试题 C: 直线

给定平面上 20 × 21 20 × 21 20×21 个整点 ( x , y ) ∣ 0 ≤ x < 20 , 0 ≤ y < 21 , x ∈ Z , y ∈ Z {(x, y)|0 ≤ x < 20, 0 ≤ y < 21, x ∈ Z, y ∈ Z} (x,y)0x<20,0y<21,xZ,yZ ,问这些点一共确定了多少条不同的直线。

解决方案

掌握直线的表示与去重即可。
用两点式表示直线有
y − y 1 x − x 1 = y 2 − y 1 x 2 − x 1 \frac{y-y_1}{x-x_1}=\frac{y_2-y_1}{x_2-x_1} xx1yy1=x2x1y2y1
整理得
y = y 2 − y 1 x 2 − x 1 x + x 2 y 1 − x 1 y 2 x 2 − x 1 y=\frac{y_2-y_1}{x_2-x_1}x+\frac{x_2y_1-x_1y_2}{x_2-x_1} y=x2x1y2y1x+x2x1x2y1x1y2
注意:

  • 比较两直线的斜率和截距时建议不要用浮点类型(谨防浮点误差)。
  • 存储时保证 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)位于 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),以方便比较直线大小。
/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 C: 直线
 * 解决方案: 枚举。用两点式保存直线,枚举所有可能存在的直线,用set去重。
 */

#include 
using namespace std;

// 两点确定一条直线
class Line {
    int x1, y1, x2, y2;
public:
    Line(int a, int b, int c, int d) : x1(a), y1(b), x2(c), y2(d) {
        // 规范直线,保证(x2,y2)在(x1,y1)的右上方,方便比较截距大小
        if (x2 < x1) swap(x1,x2), swap(y1,y2);
        if (x1 == x2 && y1 > y2) swap(y1,y2);
    }
    bool operator < (const Line &t) const
    {
        // 首先根据斜率大小排序
        // 斜率相等则按截距大小排序(注意斜率不存在的情况)
        if ((y1-y2)*(t.x1-t.x2) != (x1-x2)*(t.y1-t.y2)) return (y1-y2)*(t.x1-t.x2) < (x1-x2)*(t.y1-t.y2);
        if (x1 == x2) return x1 < t.x1;  // 斜率不存在的情况下比较横截距
        return (x2*y1-x1*y2)*(t.x2-t.x1) < (x2-x1)*(t.x2*t.y1-t.x1*t.y2);  // 否则比较纵截距
    }
};
int main()
{
    int n = 20, m = 21;
    // n = 2, m = 3;
    set<Line> st;
    // 数据范围很小,四层循环暴力枚举两点
    for (int x1 = 0; x1 < n; ++x1) {
        for (int y1 = 0; y1 < m; ++y1) {
            for (int x2 = 0; x2 < n; ++x2) {
                for (int y2 = 0; y2 < m; ++y2) {
                    if (x1 == x2 && y1 == y2) continue;  // 保证两点不同
                    st.insert(Line(x1,y1,x2,y2));
                }
            }
        }
    }

    // 集合的大小即为不同直线的数目
    cout << st.size();
    // 输出40257
    return 0;
}

试题 D: 货物摆放

n n n 拆为 3 3 3 个正整数的乘积。

例如,当 n = 4 n = 4 n=4 时,有以下 6 种方案: 1 × 1 × 4 1×1×4 1×1×4 1 × 2 × 2 1×2×2 1×2×2 1 × 4 × 1 1×4×1 1×4×1 2 × 1 × 2 2×1×2 2×1×2 2 × 2 × 1 2 × 2 × 1 2×2×1 4 × 1 × 1 4 × 1 × 1 4×1×1

请问,当 n = 2021041820210418 n = 2021041820210418 n=2021041820210418 (注意有 16 16 16 位数字)时,总共有多少种
方案?

解决方案
先求出各 n n n 的所有质因子及其个数,问题便转化为: 将 n n n 的所有质因子放入三个不同的盒子,问有几种放法?

例如,当 n = 4 = 2 × 2 n=4=2\times2 n=4=2×2 时,两个相同的质因子放入三个不同的盒子,便有 C 3 1 + C 3 2 = 6 C_3^1+C_3^2=6 C31+C32=6 种。

注意到 2021041820210418 = 20210418 × 100000001 2021041820210418=20210418\times 100000001 2021041820210418=20210418×100000001 ,因此直接无脑暴力因式分解也不会超时。

那就先来因式分解一波:

/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 D: 货物摆放
 * 解决方案: 暴力出奇迹
 */

#include 
using namespace std;

typedef long long LL;
void factors(LL x)
{
    for (LL i = 2; i*i <= x; ++i) {
        while (x%i == 0) x /= i, cout << i << ' ';
    }
    if (x > 1) cout << x;
}
int main()
{
    LL n = 2021041820210418;
    factors(n);
    return 0;
}
// 输出: 2 3 3 3 17 131 2857 5882353

结果非常的 A mazing \text{{\color{red}A}\color{black}mazing} Amazing !

2021041820210418 2021041820210418 2021041820210418 居然只有 8 8 8 个质因子,遂手算即可。

  • 个数为 1 1 1 的质因子放入三个不同的盒子,有 C 3 1 = 3 C_3^1=3 C31=3 种方案
  • 个数为 1 1 1 的质因子放入三个不同的盒子,有 C 3 3 + 2 C 3 2 + C 3 1 = 10 C_3^3+2C_3^2+C_3^1=10 C33+2C32+C31=10 种方案
  • 因此总方案数为 3 × 10 × 3 × 3 × 3 × 3 = 2430. 3\times10\times3\times3\times3\times3=2430. 3×10×3×3×3×3=2430.

试题 E: 路径

对于两个不同的结点 a a a, b b b,如果 a a a b b b 的差的绝对值大于 21 21 21,则两个结点
之间没有边相连;如果 a a a b b b 的差的绝对值小于等于 21 21 21,则两个点之间有一条
长度为 a a a b b b 的最小公倍数的无向边相连。

问结点 1 1 1 和结点 2021 2021 2021 之间的最短路径长度是多少?

解决方案

  • 方案一: 直接跑图论最短路, Dijkstra \text{Dijkstra} Dijkstra 或者 Floyd \text{Floyd} Floyd 均可,后者虽然时间复杂度高,但代码量少,且大概几十秒就能跑出来。
/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 E: 路径
 * 解决方案: 图论或dp
 */

#include 
using namespace std;

const int N = 2022;
int dis[N][N];  // dp[i][j], 结点i到j的最短路
int main()
{
    // 初始化
    for (int i = 1; i < N; ++i) {
        for (int j = 1; j < N; ++j) {
            if (i == j) dis[i][j] = 0;
            else if (abs(i-j) <= 21) dis[i][j] = i/__gcd(i,j)*j;
            else dis[i][j] = INT_MAX/2;
        }
    }

    for (int i = 1; i < N; ++i) {
        for (int j = 1; j < N; ++j) {
            for (int k = 1; k < N; ++k) {
                if (dis[i][j] > dis[i][k]+dis[k][j]) {
                    dis[i][j] = dis[i][k]+dis[k][j];
                }
            }
        }
    }
    cout << dis[1][2021];  // 10266837
    return 0;
}

  • 方案二: 动态规划。 d p i dp_i dpi 代表从结点 1 1 1 到结点 i i i 之间的最短路径长度。状态转移方程为
    d p i = min { d p j + lcm ( i , j ) } ( i − 21 ≤ j < i ) . dp_i=\text{min}\lbrace dp_{j}+\text{lcm}(i,j) \rbrace(i-21\le j\lt i). dpi=min{dpj+lcm(i,j)}(i21j<i).
    这是参照网上大佬的做法,但我并不理解。 这 是 巧 合 吗 ? 为 什 么 不 用 考 虑 从 j 之 后 的 结 点 中 转 呢 ? \text{\color{red}这是参照网上大佬的做法,但我并不理解。}\\ 这是巧合吗?为什么不用考虑从j之后的结点中转呢? 这是参照网上大佬的做法,但我并不理解。j
    如果有人知道,还望告知,谢谢! \text{\color{#c6f}如果有人知道,还望告知,谢谢!} 如果有人知道,还望告知,谢谢!
/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 E: 路径
 * 解决方案: 图论或dp
 */

#include 
using namespace std;

const int N = 2022;
int dp[N];
int main()
{
    dp[1] = 0;
    for (int i = 2; i <= 2021; ++i) {
        dp[i] = INT_MAX;
        for (int j = max(1, i-21); j < i; ++j) {
            dp[i] = min(dp[i], dp[j]+i/__gcd(i,j)*j);
        }
    }
    cout << dp[2021];  // 10266837
    return 0;
}

试题 F: 时间显示

给定从 1970 1970 1970 1 1 1 1 1 1 00 : 00 : 00 00:00:00 00:00:00 到某时刻经过的毫秒数,要求输出该时刻对应的时分秒。

解决方案
时间复杂度 O ( 1 ) . O(1). O(1).

/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 F: 时间显示
 * 解决方案: 送分题
 */

#include 
using namespace std;

typedef long long LL;
int main()
{
    LL t;
    cin >> t;
    int h, m, s;
    t /= 1000; s = t%60;
    t /= 60; m = t%60;
    t /= 60; h = t%24;
    printf("%02d:%02d:%02d", h, m, s);
    return 0;
}

出考场听见有人说不知道1s=1000ms,或许是大一新生吧。 \text{\color{#ccc}出考场听见有人说不知道1\text{s}=1000\text{ms},或许是大一新生吧。} 出考场听见有人说不知道1s=1000ms,或许是大一新生吧。

试题 G: 砝码称重

给定 n n n 个砝码,计算一共可以称出多少种不同的重量?

解决方案
注意到 n ≤ 100 n\le100 n100 且 砝码总重 M M M 不超过 100000. 100000. 100000.
考虑 d p i , j dp_{i,j} dpi,j 代表前 i i i 个砝码能否称出重量 j j j 0 0 0 1 1 1 是,则有状态转移方程
d p i , j = d p i − 1 , j ∣ d p i − 1 , j − w ∣ d p i − 1 , j + w dp_{i,j}=dp_{i-1,j}|dp_{i-1,j-w}|dp_{i-1,j+w} dpi,j=dpi1,jdpi1,jwdpi1,j+w
注意:

  • i i i 的状态只与 i − 1 i-1 i1 的状态有关,因此可以采用滚动数组,减少内存消耗。
  • 运算过程中可能出现 j j j 为负数的情况,因此可映射到数组下标 j + M j+M j+M 的位置。
  • 最终结果只用统计能称出的正的重量。

时间复杂度 O ( n M ) . O(nM). O(nM).

/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 G: 砝码称重
 * 解决方案: 动态规划。每多一个砝码就标记哪些重量可以称出。
 */

#include 
using namespace std;

const int N = 2e5+50, M = 1e5+5;
bool dp[2][N];  // dp[i][j]代表前i个砝码能否称出重量为j-M的物品,i用滚动数组形式
int main()
{
    int n;
    scanf("%d", &n);

    dp[0][M] = 1;  // 重量为0的物品肯定能称出

    for (int i = 1; i <= n; ++i) {
        int w;
        scanf("%d", &w);
        for (int j = 0; j < N; ++j) {
            int cur = i&1, pre = cur^1;
            // 加入第i个砝码后,能否称出重量j
            // 取决于前i-1个砝码能否称出重量j或j-w或j+w
            dp[cur][j] = dp[pre][j]
                       | (j-w >= 0 ? dp[pre][j-w] : false)
                       | (j+w < N ? dp[pre][j+w] : false);
        }
    }

    int ans = 0;  // 统计能称出的大于0, 及下标大于M的重量数
    for (int i = M+1; i < N; ++i) {
        ans += dp[n&1][i];
    }
    printf("%d", ans);
    return 0;
}

试题 H: 杨辉三角形

给定正整数 n n n,求 n n n 在杨辉三角数列中第一次出现位置?

解决方案

如果纯粹暴力遍历求解,当给定数字过大时会超时。事实上,可以做一些“剪枝”操作。

  • 如果某行某列大于 n n n 了还没出现,那么该行的该列之后的数也不会有 n n n 了。
  • 特别的,如果某行第三列的数大于 n n n 还没出现,那么 n n n 必在接下来某行的第二列。由 C k 1 = n ⇒ k = n \text{C}_k^1=n\Rightarrow k=n Ck1=nk=n 易知 n n n 将出现在第 n + 1 n+1 n+1 行第二列。
/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 H: 杨辉三角形
 * 解决方案: 模拟,剪枝。
 */

#include 
using namespace std;

typedef long long LL;
const int N = 1e6;
LL C[2][N];  // 滚动数组存储杨辉三角相邻两行
int main()
{
    int n = 99999999;
    scanf("%d", &n);

    int cnt = 0;
    for (int i = 0; ; ++i) {
        int cur = i&1, pre = cur^1;
        for (int j = 0; j <= i; ++j) {
            ++cnt;
            if (i == 0 || j == 0) C[cur][j] = 1;
            else {
                C[cur][j] = C[pre][j] + C[pre][j-1];
            }
            if (C[cur][j] == n) {
                printf("%d", cnt);
                return 0;
            }

            // 如果当前列超过n的最大值,那该行后面的列一定不会出现n
            if (C[cur][j] > n) {
                cnt += i-j;
                break;
            }
        }

        // 杨辉三角第三列大于n则说明数字n必在后面某行的第二列
        if (C[cur][2] > n) break;
    }

    // 第n+1行第2列是第几个数?
    cout << 1LL*n*(n+1)/2+2;
    return 0;
}

容易验证,内层循环代码最多执行 18 18 18 万次,完全不用担心 TLE \text{TLE} TLE

试题 I: 双向排序

给定序列 ( a 1 , a 2 , ⋅ ⋅ ⋅ , a n ) = ( 1 , 2 , ⋅ ⋅ ⋅ , n ) (a1, a2, · · · , an) = (1, 2, · · · , n) (a1,a2,,an)=(1,2,,n),即 a i = i ai = i ai=i
小蓝将对这个序列进行 m m m 次操作,每次可能是将 a 1 , a 2 , ⋅ ⋅ ⋅ , a q i a_1, a_2, · · · , a_{qi} a1,a2,,aqi 降序排列,或者将 a q i , a q i + 1 , ⋅ ⋅ ⋅ , a n a_{qi}, a_{qi+1}, · · · , a_n aqi,aqi+1,,an 升序排列。
请求出操作完成后的序列。

解决方案

  • 方案一: 考场上只想到 O ( m n ) O(mn) O(mn) 的做法。注意到数组的值总是呈 V \text{V} V 型的,即先减后增。直接模拟 m m m 次排序,由于每次排序都只需合并两个有序数组,因此可以用双指针做到单次排序时间复杂度 O ( n ) O(n) O(n)
  • 方案二: 线段树。考虑到数组总是先减后增,将所有单调递减的数标记为 0 0 0,递增的数标记为 1 1 1,则根据 01 01 01 序列便可唯一确定数组状态。而排序操作不过是将递减的数中较小的数变为递增的数(即将标记 0 0 0 改为 1 1 1),或者递增的数中较小的数变为递减的数(即将标记 1 1 1 改为 0 0 0),线段树区间修改时间复杂度 O ( log ⁡ n ) O(\log n) O(logn) 。总体时间复杂度 O ( n + m log ⁡ n ) O(n+m\log n) O(n+mlogn)
/**
 * 第十二届蓝桥杯大赛软件赛省赛 C/C++ 大学 B 组
 * 试题 I: 双向排序
 * 解决方案: 数据结构(线段树/文艺树)
 */


#include 
using namespace std;

class SegTree {
    vector<int> tree;  // tree[rt]记录rt代表的区间中递增区间的个数,即1的个数
    vector<int> lazy;  // 代表将相应区间置为lazy, 初始化-1(无效值)
    void pushUp(int rt) {tree[rt] = tree[rt<<1]+tree[rt<<1|1];}
    void pushDown(int L, int R, int rt) {
        if (lazy[rt] == -1) return;
        int m = L+R >> 1;
        tree[rt<<1] = lazy[rt]*(m-L+1);
        tree[rt<<1|1] = lazy[rt]*(R-m);
        lazy[rt<<1] = lazy[rt<<1|1] = lazy[rt];
        lazy[rt] = -1;
    }
    void build(int L, int R, int rt) {
        if (L == R) {
            tree[rt] = 1;  // 初始化为n个数递增排列,全部标记为1。
            return;
        }
        int m = L+R >> 1;
        build(L, m, rt<<1);
        build(m+1, R, rt<<1|1);
        pushUp(rt);
    }
public:
    SegTree(int n) : tree(n << 2), lazy(n << 2, -1) {build(1, n, 1);}
    void change0(int num, int L, int R, int rt) {
        // 将最左的 num 个 0 变为 1
        if (num <= 0) return;
        if (R-L+1-tree[rt] <= num) {  // 不足num个0直接置1
            tree[rt] = R-L+1;
            lazy[rt] = 1;
            return;
        }
        pushDown(L, R, rt);
        int m = L+R >> 1;
        int t = min(m-L+1-tree[rt<<1], num);
        change0(num-t, m+1, R, rt<<1|1);
        change0(t, L, m, rt<<1);  // 当前区间1的个数多于num则左递归
        pushUp(rt);
    }
    void change1(int num, int L, int R, int rt) {
        // 将最左的 num 个 1 变为 0
        if (num <= 0) return;
        if (tree[rt] <= num) {  // 不足num个1直接置0
            tree[rt] = 0;
            lazy[rt] = 0;
            return;
        }
        pushDown(L, R, rt);
        int m = L+R >> 1;
        int t = min(tree[rt<<1], num);
        change1(num-t, m+1, R, rt<<1|1);
        change1(t, L, m, rt<<1);  // 当前区间1的个数多于num则左递归
        pushUp(rt);
    }
    void print0(int L, int R, int rt) {
        // 按递减顺序打印标记为0的数字
        if (L == R) {
            if (tree[rt] == 0) printf("%d ", L);
            return;
        }
        pushDown(L, R, rt);
        int m = L+R >> 1;
        print0(m+1, R, rt<<1|1);
        print0(L, m, rt<<1);
    }
    void print1(int L, int R, int rt) {
        // 按递增顺序打印标记为1的数字
        if (L == R) {
            if (tree[rt] == 1) printf("%d ", L);
            return;
        }
        pushDown(L, R, rt);
        int m = L+R >> 1;
        print1(L, m, rt<<1);
        print1(m+1, R, rt<<1|1);
    }
};
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    SegTree st(n);
    int last_min = 1;  // 记录上一次最小值所在的位置,初始化为1
    int mark_min = 1;  // 记录最小值是标的0还是1
    for (int i = 1; i <= m; ++i) {
        int p, q;
        scanf("%d%d", &p, &q);
        if (p == 0 && q > last_min) {
            st.change1(q - last_min + (mark_min==1), 1, n, 1);
            mark_min = 0;
            last_min = q;
        }
        else if (p == 1 && q < last_min) {
            st.change0(last_min - q + (mark_min==0), 1, n, 1);
            mark_min = 1;
            last_min = q;
        }
    }
    st.print0(1, n, 1);  // 降序打印标记为0的数字
    st.print1(1, n, 1);  // 升序打印标记为1的数字
    return 0;
}

  • 方案三: 伸展树。

试题 J: 括号序列

给定一个括号序列,要求尽可能少地添加若干括号使得括号序列变得合法,当添加完成后,会产生不同的添加结果,问有多少种本质不同的添加结果。

解决方案
动态规划。 d p i , j dp_{i,j} dpi,j 代表前在前 i i i 个字符构成的字符串中插入最少括号,使左括号比右括号多 j j j 个的方案数。

过 程 还 没 想 清 楚 , 改 天 补 。 。 \color{#aaa}过程还没想清楚,改天补。。

赛后总结

估计我的得分情况应该是 5 + 5 + 10 + 0 + 15 + 15 + 10 → 20 ( 砝 码 称 重 用 的 set , 不 知 道 会 不 会 超 时 ) + 20 × 20 % + 0 = 79 → 89 5+5+10+0+15+15+10\to 20 (砝码称重用的\text{set},不知道会不会超时)+20\times 20\%+0=79\to 89 5+5+10+0+15+15+1020(set)+20×20%+0=7989
满分 150 150 150 89 89 89 分连及格线没到。

以前听很多人说蓝桥杯很水,甚至说有手就行。

但参加之后我才发现,蓝桥杯其实一点儿也不水,题目有深度、有细节,难度循序渐进,重点考察基本功。

当然,蓝桥杯获了奖也并不能说明问题,获奖除了能加德育分,没有什么值得高兴的,毕竟奖项是按本省相对排名分配的,而且有 60 % 60\% 60% 的获奖率。

每 参 加 一 次 比 赛 , 应 该 要 看 到 自 己 在 这 场 比 赛 中 暴 露 出 的 问 题 , 而 不 是 为 了 一 个 无 足 轻 重 的 结 果 而 喜 怒 哀 乐 , 这 样 才 能 有 所 提 升 。 \color{red}每参加一次比赛,应该要看到自己在这场比赛中暴露出的问题,\\而不是为了一个无足轻重的结果而喜怒哀乐\color{dark},这样才能有所提升。

你可能感兴趣的:(笔记)