蔚来杯2022牛客暑期多校训练营6 ABGJM
阅读原文时间:2023年07月08日阅读:3

比赛链接

A

知识点:数学,构造。

题目要求构造一个长为 \(m\) 的序列 \(c\) ,\(m\) 自选,使得 \(c\) 的无限循环序列 \(b\) 中任意连续 \(a_i\) 个数中都存在数 \(i\) 。因此,容易想到从起点 \(pos_i\) 开始每隔 \(a_i\) 个数就填一次 \(i\) ,即在位置 \((pos_i + ka_i) \mod m , 0\leq k < \frac{m}{a_i}\) ,由此确定了构造的基本方法是填数。

但显然,直接挨个填数很可能会使得某个数的某个落点已经被其他数占据了。比如当 \(a = [5,4]\) 时,考虑构造 \(m=5\) 的序列:先填 \(1\) 得到 \([1,0,0,0,0]\) ,再填 \(2\) 得到 \([1,2,0,0,0]\) ,发现下一个位置是 \(c[0]\) ,已经有数字了。然而事实上,我们不容易在较小的时间复杂度内得到一个合适的起点,使得这个数的落点不会被其他数占据。因此,考虑调整 \(a_i\) ,使得我们可以容易避免落点重合的情况。

证明1

显然,我们只能缩小 \(a_i\) 使得,因为缩小后的 \(a_i\) 满足时原来的 \(a_i\) 一定满足,而放大不一定。

注意到,对于任意两个数 \(a_i\) 与 \(a_j\), 当 \(a_j = da_i,d\geq1\) ,若 \(\exist k,m\) ,使得 \(pos_i + ka_i = pos_j +ma_j\) ,则有 \(pos_j = pos_i + ka_i - ma_j\) ,于是对于 \(\forall m'\),都有 \(pos_j + m'a_j = pos_i + ka_i + (m'-m)a_j = pos_i + [k+(m'-m)d]a_i\) ,即处处重合。

因此,当 \(a_j = da_i\) ,只要一处不重合,则所有落点都不重合。为了方便,将 \(a_i\) 缩小成小于等于它的最大的 \(2\) 的幂 \(a'_i\)。

但仅仅是这样只能保证两两不重合,还无法保证全都不重合。比如当 \(a = [8,8,8,8,4]\) ,考虑 \(m = 8\) ,按顺序填,填到 \(5\) 时会发现 \([1,2,3,4,5,0,0,0]\) ,下一个位置是 \(c[0]\) ,还是被占据了。因此考虑改变填数顺序,使得后一个填的数不会被前面所有的数的位置影响。

证明2

从大到小的就是上述情况,不可行。

考虑从小到大,每次在第一个空位开始填。

由于每次都是从第一个空位填,所以不允许某个 \(a_i'\) 前面填了大于等于 \(a_i'\) 个位置,否则必然会循环回来落到开头被占据的位置,如上述情况。只要我们证明,对于任意一个 \(a_i’\),其前方已经填的位置数 \(x < a_i'\) ,即可。

由于 \(a_i' > \dfrac{a_i}{2}\) ,又条件告诉我们有 \(\sum \frac{1}{a_i} \leq \frac12\) ,那么现在有 $\sum \frac{1}{a_i'} < \sum \frac{2}{a_i} \leq 1 $ 。

当填到 \(a_i' = 2^k\) 时,我们尝试构造 \(a_{1\cdots i-1}' \leq 2^k\) ,使得从 \(0\) 开始连续占用最多的位置。注意到,如果 \(a_j' = 2^ma_i',m\geq 1\),则使用 \(a_i'\) 等效于使用了 \(2^m\) 个 \(a_j'\) ,且总和 \(2^m \frac{1}{a_j'} = \frac{1}{a_i'}\) 也是等价的 ,所以我们按这个方法,无论怎么取,最优结果都是一样的。因此在 \(\sum a_i' \leq 1 \Rightarrow \frac{1}{a_1'} + \cdots + \frac{1}{a_{i-1}'} \leq 1-\frac{1}{2^k}\) 下,为了方便,我们可以优先用小的 \(a_i\) 构造,快速缩小可行范围。

显然,我们只能构造 \(a = [2,4,8,\cdots,2^k]\) ,其倒数总和刚好是 \(1-\frac{1}{2^k}\) ,而连续占用位置是 \(2^k-1\) 个。

于是我们证明了这个结论。

到这里其实最难的两个证明已经结束了。接下来我们思考一下 \(m\) 到底取多少合适。

证明3

长度 \(m\) 时占位为 \(\sum \lceil \frac{m}{a_i'} \rceil\) ,因为每个 \(a_i'\) 对应的 \(i\) 至少出现一次,可得 \(m \geq max(a)\)。

令 \(m = max(a)\) ,带入得 \(\sum \lceil \frac{m}{a_i'} \rceil = \sum \frac{m}{a_i'} \leq m\) ,因此 \(m = max(a)\) 是最小长度。

由于可能多出空位,但是可以随便填,所以全填 \(1\) 。

到此这道构造题就完全结束了。

时间复杂度 \(O(n\log n + max(a))\)

空间复杂度 \(O(n+max(a))\)

#include <bits/stdc++.h>

using namespace std;

struct node {
    int val;
    int id;
}a[100007];
int c[1000007];

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) {
        cin >> a[i].val;
        a[i].id = i;
        int tmp = 1 << 30;
        while (tmp > a[i].val) tmp >>= 1;
        a[i].val = tmp;
    }///使用2的幂,证明1
    sort(a + 1, a + n + 1, [&](node a, node b) {
        return a.val < b.val;
    });///从小到大,证明2
    int m = a[n].val;///c的长度,证明3
    for (int i = 1, pos = 0;i <= n;i++) {
        for (int j = pos;j < m;j += a[i].val) c[j] = a[i].id;///方法是填数
        while (c[pos]) pos++;
    }
    cout << m << '\n';
    for (int i = 0;i < m;i++) cout << max(1, c[i]) << ' ';///可能多出来空位随便填
    cout << '\n';
    return 0;
}

B

知识点:枚举,前缀和,差分,树。

考虑树上前缀和与差分。维护当前递归的一条树链的深度到节点编号的映射数组 \(depL\) 即可差分,最后递归求前缀和。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

#include <bits/stdc++.h>

using namespace std;

vector<int> g[2000007];
int d[2000007];
int dep[2000007], depL[2000007];///节点深度,一条链上深度对应节点
int sum[2000007];///树上差分

void dfs(int u, int fa) {
    dep[u] = dep[fa] + 1;
    depL[dep[u]] = u;
    sum[u]++;
    sum[depL[max(0, dep[u] - d[u] - 1)]]--;
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
    }
}

void cal(int u, int fa) {
    for (auto v : g[u]) {
        if (v == fa) continue;
        cal(v, u);
        sum[u] += sum[v];
    }
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1;i < n;i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    for (int i = 1;i <= n;i++) cin >> d[i];
    dfs(1, 0);
    cal(1, 0);
    for (int i = 1;i <= n;i++) cout << sum[i] << ' ';
    cout << '\n';
    return 0;
}

G

知识点:模拟。

签到题。直接打表也行,模拟也行。

时间复杂度 \(O(n^2)\)

空间复杂度 \(O(1)\)

#include <bits/stdc++.h>

using namespace std;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;

    for (int i = 1;i <= 13 * n + 19;i++) cout << '*';
    cout << '\n';

    for (int i = 1;i <= n;i++) {
        cout << "*";
        for (int j = 2;j <= 13 * n + 18;j++)
            cout << ".";
        cout << "*";
        cout << '\n';
    }

    for (int i = 1;i <= 2 * n + 3;i++) {
        cout << "*";
        for (int j = 1;j <= n + 1;j++) cout << ".";
        for (int j = 1;j <= 2 * n + 3;j++) {
            if (i == 1 || i == 2 * n + 3) {
                if (j == 1 || j == 2 * n + 3) cout << "@";
                else cout << ".";
            }
            else {
                if (j == 1 || j == 2 * n + 3 || j == i) cout << "@";
                else cout << ".";
            }
        }

        for (int j = 1;j <= n + 1;j++) cout << ".";

        for (int j = 1;j <= 2 * n + 3;j++) {
            if (i == 1 || i == n + 2) cout << "@";
            else {
                if (j == 1) cout << "@";
                else cout << ".";
            }
        }
        for (int j = 1;j <= n + 1;j++) cout << ".";

        for (int j = 1;j <= 2 * n + 3;j++) {
            if (i == 2 * n + 3) cout << "@";
            else {
                if (j == 1) cout << "@";
                else cout << ".";
            }
        }

        for (int j = 1;j <= n + 1;j++) cout << ".";

        for (int j = 1;j <= 2 * n + 3;j++) {
            if (i == 1 || i == n + 2 || i == 2 * n + 3) cout << "@";
            else if (1 < i && i < n + 2) {
                if (j == 1) cout << "@";
                else cout << ".";
            }
            else if (n + 2 < i && i < 2 * n + 3) {
                if (j == 2 * n + 3) cout << "@";
                else cout << ".";
            }
        }

        for (int j = 1;j <= n + 1;j++) cout << ".";
        cout << "*";
        cout << '\n';
    }

    for (int i = 1;i <= n;i++) {
        cout << "*";
        for (int j = 2;j <= 13 * n + 18;j++)
            cout << ".";
        cout << "*";
        cout << '\n';
    }

    for (int i = 1;i <= 13 * n + 19;i++) cout << '*';
    cout << '\n';
    return 0;
}

J

知识点:思维,数学。

A不能变,B只有A-B与B两个值,于是C的变换方法只有形如交替的A-B与B,而最终C的符号取决于偶数次变换还是奇数次。公式为:

\[\left\{
\begin{aligned}
k(2B-A) +C = x,变换偶次\\
B-k(2B-A)-C = x,变换奇次
\end{aligned}
\right.
\]

这里的 \(k\) 取负时的意义简单理解是交换了变换顺序,比如 \([A-B,B] \rightarrow [B,A]\) 。只要找到整数 \(k\) 满足等式即可,注意 \(2B-A = 0\) 时需要特判。

时间复杂度 \(O(1)\)

空间复杂度 \(O(1)\)

#include <bits/stdc++.h>
#define ll long long

using namespace std;

bool solve() {
    int a, b, c, x;
    cin >> a >> b >> c >> x;
    if (2 * b - a == 0) {
        if (x - c == 0 || x + c - b == 0) cout << "Yes" << '\n';
        else return false;
    }
    else if ((x - c) % (2 * b - a) == 0 || (x + c - b) % (a - 2 * b) == 0) cout << "Yes" << '\n';
    else return false;
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << "No" << '\n';
    }
    return 0;
}

M

知识点:线性dp,博弈论。

通过转移局面情况到起点,然后判断起点是否到达目标状态。

其中由于走法是固定向下或者向右,所以可以通过横纵坐标之和的奇偶性来决定当前是谁的回合。

对于A而言,两种走法有一种能得到目标状态就转移;对于B而言,两种走法有一种得不到目标状态就转移。

具体看代码注释qwq。

时间复杂度 \(O(nm)\)

空间复杂度 \(O(nm)\)

#include <bits/stdc++.h>
#define ll long long

using namespace std;

char dt[507][507];
int dp[507][507];
///三种局面是可以独立考虑的,分别看作能否一定走到A、B、(n,m)处状态
///用三位二进制分别表示(i, j)处A是否一定能走到A、B、(n, m)处状态
///1表示一定能走到,0表示不一定能走到(包括一定不)
///已知A处001,终点处为010,B处100

bool solve() {
    int n, m;
    cin >> n >> m;
    for (int i = 1;i <= n;i++)
        for (int j = 1;j <= m;j++)
            cin >> dt[i][j];
    for (int i = n;i >= 1;i--) {
        for (int j = m;j >= 1;j--) {
            if (dt[i][j] == 'A') { dp[i][j] = 1;continue; }
            if (dt[i][j] == 'B') { dp[i][j] = 4;continue; }
            if (i == n && j == m) { dp[i][j] = 2;continue; }
            if (i == n) dp[i][j] = dp[i][j + 1];
            else if (j == m) dp[i][j] = dp[i + 1][j];///无路可走的两种直接传递
            else if ((i + j) & 1) dp[i][j] = dp[i + 1][j] & dp[i][j + 1];///可以通过坐标之和奇偶性得知是谁的回合,因为只能往下或者往右走,每轮都只会加一
            else dp[i][j] = dp[i + 1][j] | dp[i][j + 1];///每种局面有0则B一定走0,有1则A一定走1
        }
    }
    if (dp[1][1] & 1) cout << "yes" << ' ';
    else cout << "no" << ' ';
    if (dp[1][1] & 2) cout << "yes" << ' ';
    else cout << "no" << ' ';
    if (dp[1][1] & 4) cout << "yes" << ' ';
    else cout << "no" << ' ';
    cout << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}