2021 年蓝桥杯第一次省赛题目全解答

发布时间 2023-03-29 12:48:03作者: PatrickyTau

做题链接:A组 B组 C组

填空题

卡片

直接模拟。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    std::map<char, int> mp;
    for (int i = 0; i <= 9; i++) {
        mp[i + '0'] = 2021;
    }
    
    for (int i = 1; ; i++) {
        for (char j : std::to_string(i)) {
            if (mp[j] > 0) {
                mp[j] -= 1;
            } else {
                std::cout << i - 1 << '\n';
                std::exit(0);
            }
        }
    }
    return 0;
}

直线

分类讨论:水平、竖直或者是斜着(记下斜率 \(k\) 和截距 \(b\)),注意到给定坐标情况下,使用 std::atan2(y, x) 计算斜率 \(k = \dfrac{y}{x}\);直线方程使用两点式:

\[\dfrac{y - y_0}{y_1 - y_0} = \dfrac{x - x_0}{x_1 - x_0} \]

从而解出截距 \(b = \dfrac{x_0y_1-x_1y_0}{x_1-x_0}\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    std::vector<std::pair<int, int>> p;
    
    const int n = 20, m = 21;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            p.emplace_back(i, j);
        }
    }
    
    std::set<std::pair<double, double>> s;
    std::set<int> kind[2]{};
    int t = p.size();
    for (int i = 0; i < t; i++) {
        int x_0 = p[i].first, y_0 = p[i].second;
        for (int j = i + 1; j < t; j++) {
            int f = 0;
            int x_1 = p[j].first, y_1 = p[j].second;
            
            if (x_0 == x_1) kind[0].insert(x_0), f++;
            if (y_0 == y_1) kind[1].insert(y_0), f++;
            
            if (f == 0) {
                double k = std::atan2(y_1 - y_0, x_1 - x_0);
                double t = 1. * (x_0 * y_1 - x_1 * y_0) / (x_1 - x_0);
                s.emplace(k, t);
            }
        }
    }
    
    std::cout << kind[0].size() + kind[1].size() + s.size() << '\n';
    
    return 0;
}

货物摆放

即求有多少可重排列,使得 \(n = 2021041820210418 = 2\times3^3\times17\times131\times2857\times5882453 = pqr\)。由隔板法,答案为 \(3!{8 + 3 - 1 \choose 3 - 1} = 2430\)

路径

\(n = 2000\),在本机使用 Floyd 跑出结果之后交。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
//    std::cout << 10266837 << '\n';
//    return 0;
    
    const int N = 2021 + 10;
    ll g[N][N]{};
    
    for (int i = 1; i <= 2021; i++) {
        for (int j = 1; j <= 2021; j++) {
            if (std::abs(i - j) <= 21) {
                g[i][j] = g[j][i] = 1ll * i * j / std::__gcd(i, j);
            } else {
                g[i][j] = g[j][i] = 0x3f3f3f3f;
            }
        }
    }
    
    for (int k = 1; k <= 2021; k++) {
        for (int i = 1; i <= 2021; i++) {
            for (int j = 1; j <= 2021; j++) {
                g[i][j] = std::min(g[i][j], g[i][k] + g[k][j]);
            }
        }
    }
    
    std::cout << g[1][2021] << '\n';
    
    return 0;
}

回路计数

即求哈密顿回路数,状压即可。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
//     std::cout << "881012367360" << '\n';
//     return 0;
    
    const int n = 21;
    static std::array<std::array<int, n>, n> g{};
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if (std::__gcd(i + 1, j + 1) == 1) {
                g[i][j] = g[j][i] = 1;
            }
        }
    }
    
    static std::array<std::array<ll, n>, 1 << n> f{};
    
    f[1][0] = 1;
    for (int i = 1; i < 1 << n; i++) {
        for (int j = 0; j < n; j++) {
            if ((i >> j & 1) && f[i][j]) { // `j` selected
                for (int k = 0; k < n; k++) {
                    if (((i ^ (1 << j)) >> k & 1) && g[k][j]) {
                        f[i][j] += f[i ^ (1 << j)][k];
                    }
                }
            }
        }
    }
    
    std::cout << std::accumulate(f.back().begin(), f.back().end(), 0ll) << '\n';
    
    return 0;
}

空间

\(1 \mathrm{Byte} = 8 \mathrm{bit}s\),因此答案为 \(\dfrac{256\times1024^2\times8}{32} = 67108864\)

相乘

即求 \(2021x \equiv 999999999 \bmod 1000000007\)\(x \equiv 999999999 \times 2021 ^ {-1}\) 求得 \(x = 17812964\)

mod = 1000000007
x = pow(2021, mod - 2, mod) * 999999999 % mod
print(x)

编程题

砝码称重

同 01 背包,只是转移到左右。可使用 std::bitsetoperator<< 实现。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }
    
    std::bitset<100010> f;
    f[0] = 1;
    for (int i : a) f |= f << i;
    for (int i : a) f |= f >> i;
    f[0] = 0;
    
    std::cout << f.count() << '\n';
    
    return 0;
}

特别注意如果使用 01 背包写法,想在一个循环里完成,需要开两维。

核心代码
int B = 100000;
std::vector<std::vector<int>> f(2, std::vector<int>(2 * B + 10, 0));
f[0][0 + B] = 1;

for (int i = 1; i <= n; i++) {
    int w = a[i - 1];
    for (int j = -m + B; j <= m + B; j++) {
        f[i & 1][j] = f[~i & 1][j];
        if (j - w >= -m + B) f[i & 1][j] |= f[~i & 1][j - w];
        if (j + w <= +m + B) f[i & 1][j] |= f[~i & 1][j + w];
    }
}

int ans = 0;
for (int i = 1 + B; i <= m + B; i++) {
    ans += f[n & 1][i];
}

异或数列

容易发现平局当且仅当 \(\bigoplus\limits_{i=1}^n X_n = 0\):记下每一位的次数,从高到低按位贪心,可见如果当前位为偶数则必然不会栽在这个位上。

话接上文,如果当前为出现次数 \(c = 1\),则先手胜利;若为其他奇数,后手可以选择一个 \(c \ne 1\) 的数来反转,因此当 \(n - c \equiv 0 \bmod 2\) 时先手胜利,否则后手胜利。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int test = 1;
    std::cin >> test;
    void solve();
    while (test--) solve();
    return 0;
}

void solve() {
    int n;
    std::cin >> n;
    
    std::array<int, 30> cnt{};
    
    int s = 0;
    for (int i = 0; i < n; i++) {
        int x;
        std::cin >> x;
        for (int i = 20; ~i; i--) if (x >> i & 1) cnt[i] += 1;
        s ^= x;
    }
    
    if (!s) {
        std::cout << "0\n";
        return;
    }
    
    for (int i = 20; ~i; i--) if (cnt[i] & 1) {
        if (cnt[i] == 1) std::cout << "1\n";
        else if ((n - cnt[i]) & 1) std::cout << "-1\n";
        else std::cout << "1\n";
        break;
    }
}

左孩子右兄弟

\(f_u\) 表示 \(u\) 节点对应的答案,可见 \(f_u\) 至少是 \(u\) 的儿子数量,另有 \(\mathop\max\limits_{v \in u}\{f_v\}\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector<std::vector<int>> g(n + 1);
    for (int i = 1; i < n; i++) {
        int u;
        std::cin >> u;
        g[u].push_back(i + 1);
    }
    
    std::vector<int> f(n + 1);
    std::function<void(int)> dfs = [&](int u) -> void {
        f[u] = g[u].size();
        int max = 0;
        for (int v : g[u]) dfs(v), max = std::max(max, f[v]);
        f[u] += max;
    };
    dfs(1);
    
    std::cout << f[1] << '\n';
    
    return 0;
}

括号序列

既考虑左括号又考虑右括号很麻烦,不如分开考虑,最终的方案数相乘即可。如对于序列 \())((\),可以只添加左括号使之成为:\(\color{red}{((}\)\())((\)\(\color{red}{(}\)\()\)\(\color{red}{(}\)\()((\);只添加右括号时,要转换为上面的问题,将序列反转再翻转:\())((\),方案是一样的。因此最终答案为 \(4\)。接下来只需解决放左括号的方案数。

将原始序列按照右括号分成若干片段,研究某一段还需要放多少左括号。用 \(f_{i, j}\) 表示前缀 \(i\) 放置 \(j\) 个左括号使之合法(这里指的是左括号不少于右括号数量)的方案数。如果 \(s_i = \tt `('\),方案数同 \(f_{i - 1, j - 1}\);否则 \(f_{i, j} = \sum\limits_{k = 0}^{j + 1} f_{i - 1, k} = f_{i, j - 1} + f_{i - 1, j + 1}\)。最终答案即为前缀 \(i\) 放置至少 \(j'\) 个左括号使之合法的方案数。按照定义即使得 \(f_{n, j'} \ge 0\) 的最小 \(j'\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

const int mod = 1000000007, N = 5002;

ll f[N][N];

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    std::string s;
    std::cin >> s;
    
    int n = s.size();
    
    f[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        if (s[i - 1] == '(') {
            for (int j = 1; j <= n; j++) {
                f[i][j] = f[i - 1][j - 1];
            }
        } else {
            f[i][0] = (f[i - 1][0] + f[i - 1][1]) % mod;
            for (int j = 1; j <= n; j++) {
                f[i][j] = (f[i - 1][j + 1] + f[i][j - 1]) % mod;
            }
        }
    }
    
    ll l = -1;
    for (int i = 0; i <= n; i++) {
        if (f[n][i]) {
            l = f[n][i];
            break;
        }
    }
    
    for (auto &si : s) {
        si ^= '(' ^ ')';
    }
    std::reverse(s.begin(), s.end());
    
    for (int i = 0; i <= n + 1; i++) {
        for (int j = 0; j <= n + 1; j++) {
            f[i][j] = 0;
        }
    }
    f[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        if (s[i - 1] == '(') {
            for (int j = 1; j <= n; j++) {
                f[i][j] = f[i - 1][j - 1];
            }
        } else {
            f[i][0] = (f[i - 1][0] + f[i - 1][1]) % mod;
            for (int j = 1; j <= n; j++) {
                f[i][j] = (f[i - 1][j + 1] + f[i][j - 1]) % mod;
            }
        }
    }
    
    ll r = -1;
    for (int i = 0; i <= n; i++) {
        if (f[n][i]) {
            r = f[n][i];
            break;
        }
    }
    
    std::cout << l * r % mod << '\n';
    
    return 0;
}

时间显示

不考虑毫秒,直接 /= 1000,接着直接分解各位。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int test = 1;
    std::cin >> test;
    void solve();
    while (test--) solve();
    return 0;
}

void solve() {
    ll x;
    std::cin >> x;
    x /= 1000;
    ll h = 24, m = 60, s = 60;
    x %= h * m * s;
    
    std::cout << std::fixed << std::setw(2) << std::setfill('0');
    std::cout << x / (m * s) << ':';
    std::cout << std::fixed << std::setw(2) << std::setfill('0');
    std::cout << x % (m * s) / m << ':';
    std::cout << std::fixed << std::setw(2) << std::setfill('0');
    std::cout << x % s << '\n';
}

iostream sucks~

杨辉三角形

看到数据范围 \(10^9\),大概率也是二分或者有结论。尝试二分的话,得寻找单调性,杨辉三角最明显的单调性莫过于每一条斜线了(自上而下)。注意到杨辉三角对称,不妨按照 \({n \choose n / 2}\) 来找。注意组合数容易爆精度,只要大于 \(x\) 就直接返回,反正需要的也只是 \(\ge x\) 的单调性而已。因为 \(10^9 \leq {30 \choose 15}\)

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int test = 1;
    std::cin >> test;
    void solve();
    while (test--) solve();
    return 0;
}

int x;

ll binom(int n, int m) {
    ll res = 1;
    for (int i = n, j = 1; j <= m; i--, j++) {
        res = res * i / j;
        if (res > x) {
            return res;
        }
    }
    return res;
}

void solve() {
    std::cin >> x;
    for (int i = 16;; i--) {
        int l = 2 * i, r = std::max(x, l);
        while (l < r) {
            int mid = (l + r) / 2;
            if (binom(mid, i) >= x) r = mid;
            else l = mid + 1;
        }
        if (binom(r, i) == x) {
            std::cout << 1ll * (r + 1) * r / 2 + i + 1 << '\n';
            return;
        }
    }
    
    __builtin_unreachable();
}

双向排序

注意到有许多步骤,是不需要的:例如 \(x \leq y\)\([1, x], [1, y]\) 中就不需要跑 \([1, x]\),另一个方向也类似。另外如果第一次操作为 \([x, n]\) 是不需要跑的。

接下来问题转变为两种操作交替时,该如何减少操作次数。观察下面的操作:

\(1. 0, 3\), \([\underline{3, 2, 1}, 4, 5, 6, 7]\)
\(2. 1, 4\), \([3, 2, 1, \underline{4, 5, 6, 7}]\)
\(3. 0, 5\), \([\underline{5, 4, 3, 2, 1}, 6, 7]\)

在这里,前两次操作没有交集,这两次结果被第三次覆盖了,相当于直接从初始状态到第三步。

下面要解决的问题就只有放置了,由于任意相邻两步必有交,因此只能放那些与自己无关的区间,例如 \(0, 5\) 就可以将答案数组置为 \([0, 0, 0, 0, 0, \underline{6, 7}]\);再下一步 \(1, 2\),可以将答案置为 \([5, 0, 0, 0, 0, 6, 7]\), ... 以此类推。

注意如果所有操作区间的并不为 \([1, n]\),还需要按照最后一次操作种类填满。

展开代码
#include <bits/stdc++.h>

using ll = long long;

const int N = 100010;
int stk[N][2], ans[N];
int top;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n, m;
    std::cin >> n >> m;
    
    while (m--) {
        int p, q;
        std::cin >> p >> q;
        if (p == 0) {
            while (top && stk[top][0] == 0)
                q = std::max(q, stk[top --][1]);
            while (top >= 2 && stk[top - 1][1] <= q)
                top -= 2;
            stk[++top][0] = p, stk[top][1] = q;
        } else if (top) {
            while (top && stk[top][0] == 1)
                q = std::min(q, stk[top --][1]);
            while (top >= 2 && stk[top - 1][1] >= q)
                top -= 2;
            stk[++top][0] = p, stk[top][1] = q;
        }
    }
    
    int k = n, l = 1, r = n;
    for (int i = 1; i <= top; i++) {
        if (stk[i][0] == 0) {
            while (l <= r && r > stk[i][1])
                ans[r--] = k--;
        } else {
            while (l <= r && l < stk[i][1])
                ans[l++] = k--;
        }
        if (l > r)
            break;
    }
    
    if (top & 1) {
        while (l <= r) ans[l++] = k--;
    } else {
        while (l <= r) ans[r--] = k--;
    }
    
    std::copy(ans + 1, ans + n + 1, std::ostream_iterator<int> {std::cout, " "});
    
    return 0;
}

最少砝码

实际上为平衡三进制最少需要多少底数。实际上有,\(n\) 位平衡三进制,每一位都包含 \(1, z\),可以表示 \([-\dfrac{3^n - 1}{2}, \dfrac{3^n - 1}{2}]\) 内的所有数,即需要 \(\log_3(2 n + 1)\) 个数。

展开代码
#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int test = 1;
    std::cin >> test;
    void solve();
    while (test--) solve();
    return 0;
}

void solve() {
    ll x;
    std::cin >> x;
    
    auto y = std::log(2 * x + 1) / std::log(3);
    std::cout << (int)(std::ceil(y) + .5) << '\n';
}