范式杯2023牛客暑期多校训练营1

发布时间 2023-07-21 02:32:01作者: 空白菌

比赛链接

A

题解

知识点:构造。

设任意字符串为 \(t\) ,为了使得 \(t = s\) 时结果不有序,考虑将其中 \(s\) 一组 \(01\) 固定反序,同时 \(t \neq s\) 时一定不会反序。

我们先分别将 \(s\) 所有 \(01\) 的位置放入集合 \(S_0,S_1\) ,选择最左侧 \(1\) 和最右侧 \(0\) 的位置分别记作 \(l,r\) (因为保证 \(s\) 不有序,所以一定能找到且一定反序)。显然, \(l \leq |S_0| < r\)

第一步 ,我们要在保证不改变反序的情况,将其他的 \(t_l,t_r\) 变为有序。我们发现,若 \(t \neq s\) ,则存在位置 \(x \in S_0\) 使得 \(t_x = 1\)\(x \in S_1\) 使得 \(t_x = 0\) 。因此,对于任意字符串,我们将 \(S_0\) 位置的最大值移到最右边,将 \(S_1\) 位置的最小值移到最左边,通过操作 \(\forall x \in S_0,(x,r)\)\(\forall x\in S_1,(l,x)\) 即可。

第二步,将除了 \(l,r\) 的其他位置正常冒泡排序即可。

第三步,我们将 \(l\)\([1,|S_0|]\) 的位置里排序,将 \(r\)\([|S_0|+1,n]\) 的位置里排序,要注意排序顺序。

对于最后一步,我们对 \(l,r\) 的情况分类讨论证明:

  1. \(s = t\) ,则第一步和第二步不会对 \(t_l,t_r\) 交换,而第三步由于 \(l \leq |S_0| < r\) 因此反序是一直存在的。
  2. \(s \neq t\)\(|T_0| \geq |S_0|\) ,则第一步一定会使得 \(t_l = 0\) ,而 \(t_r\) 不确定。在第二步后, \(t_{1;|S_0|}\) 一定都是 \(0\) ,对于第三步:
    1. \(t_r = 0\) ,那么 \(t_r\) 会被交换到 \(t_{|S_0|+1}\) ,此时由于 \(t_{1;|S_0|} = 0,t_{|S_0+1|;r-1},t_{r+1,n}\) 都是有序的,因此 \(t\) 有序。
    2. \(t_r = 1\) ,那么 \(t_r\) 会被交换到 \(t_n\) ,同理 \(t\) 也是有序的。
  3. \(s \neq t\)\(|T_0| < |S_0|\) ,同理 \(t\) 也会有序。

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

bool solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    s = "?" + s;
    set<int> st[2];
    for (int i = 1;i <= n;i++) st[s[i] == '1'].insert(i);

    cout << n - 2 + (n - 2) * (n - 3) / 2 + n - 2 << '\n';

    int r = *prev(st[0].end());
    for (auto val : st[0]) {
        if (val != r)
            cout << val << ' ' << r << '\n';
    }
    int l = *st[1].begin();
    for (auto val : st[1]) {
        if (val != l)
            cout << l << ' ' << val << '\n';
    }

    for (int i = 1;i <= n;i++) {
        if (i == l || i == r) continue;
        for (int j = i + 1;j <= n;j++) {
            if (j == l || j == r) continue;
            cout << i << ' ' << j << '\n';
        }
    }

    for (int i = 1;i <= l - 1;i++) cout << i << ' ' << l << '\n';
    for (int i = st[0].size();i >= l + 1;i--) cout << l << ' ' << i << '\n';
    for (int i = st[0].size() + 1;i <= r - 1;i++) cout << i << ' ' << r << '\n';
    for (int i = n;i >= r + 1;i--) cout << r << ' ' << i << '\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;
}

D

题解

知识点:博弈论。

可以证明除了 \(1 \times 1\) ,先手必胜。

假设先手先吃了 \((1,1)\) ,且此时后手有必胜策略,那么先手可以在第一步直接采用这个策略到达必胜的局面(因为 \((1,1)\) 可以被任何子矩阵包含,所以这个局面是可以一步到达的),所以后手不可能有必胜策略,这是决策的包含关系。

貌似不存在构造性的证明,但 \(n \times n\) 的情况可以选择 \((n-1,n-1)\)

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, m;
    cin >> n >> m;
    if (n == 1 && m == 1) cout << "Walk Alone" << '\n';
    else cout << "Kelin" << '\n';
    return 0;
}

H

题解

知识点:贪心,枚举。

考虑用区间的形式表示数对,且 \(a_i \leq b_i\) 的记为正序区间,否则记为反序区间,交换不妨只交换 \(a\)

容易发现,同类型区间交换不改变答案,不同类型相交的区间交换会减少答案,改变量为 \(2\) 倍交集。因此,问题转换为,找到不同类型区间交集的最大值。

区间交集的实现可以说是五花八门,有线段树树状数组、二分端点等,这里讲一种最简单的。

将所有区间按左端点排序后遍历,过程中记录两种类型的右端点最大值,每次只需要当前区间端点和不同类型的最大右端点计算即可得到交集,更新同类型的右端点,最后取所有交集最大值。

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int a[1000007], b[1000007];
array<int, 3> area[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];
    for (int i = 1;i <= n;i++) cin >> b[i];

    for (int i = 1;i <= n;i++)
        area[i] = { min(a[i],b[i]),max(a[i],b[i]),a[i] > b[i] };
    sort(area + 1, area + n + 1);

    int mx = 0;
    array<int, 2> mxr{ (int)-1e9,(int)-1e9 };
    for (int i = 1;i <= n;i++) {
        auto [l, r, t] = area[i];
        mx = max(mx, max(0, min(r, mxr[!t]) - l));
        mxr[t] = max(mxr[t], r);
    }

    ll ans = -2LL * mx;
    for (int i = 1;i <= n;i++) ans += abs(a[i] - b[i]);
    cout << ans << '\n';
    return 0;
}

J

题解

知识点:概率期望。

注意到,每次赢 \(1\) 元都需要经历“输输输...赢”,一共需要经历 \(m\) 次才能赢 \(m\) 元。

对于本金为 \(i\) 时,最多能输 \(\left\lfloor \log_2(i+1) \right\rfloor\) 轮,此时赢一块钱的概率为 \(1-\left(\dfrac{1}{2} \right)^{\left\lfloor \log_2(i+1) \right\rfloor}\)

因此,答案为 $\displaystyle \prod_{i=n}^{n+m-1}\left( 1-\left(\dfrac{1}{2} \right)^{\left\lfloor \log_2(i+1) \right\rfloor} \right) $ ,但这样复杂度会炸。

我们发现,每次都有一长段 \(i\)\(\left\lfloor \log_2(i+1) \right\rfloor\) 是相等的,因此考虑枚举 \(\left\lfloor \log_2(i+1) \right\rfloor\) 的值配合快速幂即可解决问题。

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct Modint {
    static int P;

    int val;
    Modint(int _val = 0) :val(_val %P) { format(); }
    Modint(ll _val) :val(_val %P) { format(); }

    //if val in [-P,2P)
    Modint &format() {
        if (val < 0) val += P;
        if (val >= P) val -= P;
        return *this;
    }
    Modint inv()const { return qpow(*this, P - 2); }

    Modint &operator+=(const Modint &x) { val += x.val;return format(); }
    Modint &operator-=(const Modint &x) { val -= x.val;return format(); }
    Modint &operator*=(const Modint &x) { val = 1LL * val * x.val % P;return *this; }
    Modint &operator/=(const Modint &x) { return *this *= x.inv(); }
    friend Modint operator-(const Modint &x) { return { -x.val }; }
    friend Modint operator+(Modint a, const Modint &b) { return a += b; }
    friend Modint operator-(Modint a, const Modint &b) { return a -= b; }
    friend Modint operator*(Modint a, const Modint &b) { return a *= b; }
    friend Modint operator/(Modint a, const Modint &b) { return a /= b; }

    friend Modint qpow(Modint a, ll k) {
        Modint ans = 1;
        while (k) {
            if (k & 1) ans = ans * a;
            k >>= 1;
            a = a * a;
        }
        return ans;
    }

    friend istream &operator>>(istream &is, Modint &x) {
        ll _x;
        is >> _x;
        x = { _x };
        return is;
    }
    friend ostream &operator<<(ostream &os, const Modint &x) { return os << x.val; }
};
/// 自动取模整数,O(logk)快速幂、O(logP)逆元、O(1)运算

const int P = 998244353;
int Modint::P = ::P;

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, m;
    cin >> n >> m;
    Modint ans = 1;
    for (int i = __lg(n + 1);i <= __lg(n + m + 1);i++) {
        ans *= qpow(1 - 1 / qpow(Modint(2), i), min((ll)n + m + 1, (1LL << i + 1)) - max(1LL << i, (ll)n + 1));
    }
    cout << ans << '\n';
    return 0;
}

K

题解

知识点:BFS。

考虑先建立bfs树,那么非树边的分裂只会增加答案且无后效性,分裂多少取决于最近的一个原来的点的距离。

对于树边,无论分裂哪些边答案增加是一样的,但显然离根节点越近的点损失的越大。同时,连接非树边的叶子节点,分裂连接其的树边会导致非树边损失更多答案。因此,只分裂连接了不连接非树边的叶子节点的树边即可。

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

vector<int> g[100007];

int n, m, k;
ll ans;
int dis[100007];
bool vis[100007];
queue<pair<int, int>> q;
void bfs(int st = 1) {
    ans = 1;
    q.push({ 1,0 });
    vis[1] = 1;
    while (q.size()) {
        auto [u, fa] = q.front();
        q.pop();
        int cnt = 0;
        for (auto v : g[u]) {
            if (v == fa) continue;
            cnt++;
            if (vis[v]) {
                ans += k - dis[u];
                continue;
            }
            vis[v] = 1;
            dis[v] = dis[u] + 1;
            if (dis[v] < k) q.push({ v,u });
            ans += dis[v] <= k;
        }
        if (!cnt && fa) ans += k - dis[u];
    }
}

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

L

题解

知识点:线性同余方程组,枚举。

注意到,每三秒会有一个复合置换 \(x = a[b[c[x]]]\)\(y,z\) 同理,考虑复合置换环的性质。

我们需要求出三个置换环的循环节长度 \(T_x,T_y,T_z\) ,以及从 \(1\) 开始到环上各个元素的时间 \(tx,ty,tz\)

接下来对于目标状态 \((x',y',z')\) ,会有三种可能的终点:

  1. 正好在环上 \(3m_0\)
  2. 环元素后一秒 \(3m_1+1\)
  3. 环元素后两秒 \(3m_2+2\)

其中 \(m_0,m_1,m_2\) 是最小正整数解,取三种情况最小值即可。

对于求解,不妨考虑 \(m_0\) ,有如下线性同余方程组:

\[\left\{ \begin{aligned} m_0 &\equiv tx_{x'} &\pmod{T_x}\\ m_0 &\equiv ty_{y'} &\pmod{T_y}\\ m_0 &\equiv tz_{z'} &\pmod{T_z}\\ \end{aligned} \right. \]

但模数是 \(T_x,T_y,T_z\) 并不一定互质,因此需要用exCRT解决。

对于 \(m_1,m_2\) ,把目标状态反推 \(1,2\) 秒,再求解即可。

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

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

namespace Number_Theory {
    ll exgcd(ll a, ll b, ll &x, ll &y) {
        if (!b) { x = 1, y = 0; return a; }
        ll d = exgcd(b, a % b, x, y);
        x -= (a / b) * y, swap(x, y);
        return d;
    }
}
namespace exCRT {
    using namespace Number_Theory;
    ll solve(const vector<ll> &a, const vector<ll> &m) {
        int k = a.size() - 1;
        ll ans = a[1], M = m[1];
        for (int i = 2;i <= k;i++) {
            ll x, y;
            ll d = exgcd(M, m[i], x, y);
            ll c = a[i] - ans;
            if (c % d) return -1;
            ll md = m[i] / d;
            x = ((__int128_t)c / d * x % md + md) % md;
            ans += M * x;
            M *= md;
            ans %= M;
        }
        return ans;
    }
}

int a[100007], b[100007], c[100007];
int ia[100007], ib[100007], ic[100007];
int tx[100007], ty[100007], tz[100007];
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], ia[a[i]] = i;
    for (int i = 1;i <= n;i++) cin >> b[i], ib[b[i]] = i;
    for (int i = 1;i <= n;i++) cin >> c[i], ic[c[i]] = i;

    int Tx = 0, Ty = 0, Tz = 0;
    for (int i = 1;i <= n;i++) tx[i] = ty[i] = tz[i] = -1;
    for (int i = 1;!~tx[i];i = a[b[c[i]]], Tx++) tx[i] = Tx;
    for (int i = 1;!~ty[i];i = b[c[a[i]]], Ty++) ty[i] = Ty;
    for (int i = 1;!~tz[i];i = c[a[b[i]]], Tz++) tz[i] = Tz;

    auto calc = [&](int x, int y, int z) {
        if (tx[x] == -1 || ty[y] == -1 || tz[z] == -1) return (ll)1e18;
        ll ans = exCRT::solve({ -1, tx[x],ty[y],tz[z] }, { -1, Tx,Ty,Tz });
        return ~ans ? ans : (ll)1e18;
    };

    int q;
    cin >> q;
    while (q--) {
        int x, y, z;
        cin >> x >> y >> z;
        ll m0 = calc(x, y, z);
        ll m1 = calc(ic[z], ia[x], ib[y]);
        ll m2 = calc(ic[ib[y]], ia[ic[z]], ib[ia[x]]);
        ll ans = min({ m0 * 3LL ,m1 * 3LL + 1,m2 * 3LL + 2 });
        cout << (ans >= 1e18 ? -1 : ans) << '\n';
    }

    return 0;
}

M

题解

知识点:线性不定方程。

操作的结果一定满足 \(Ax+By = C\) 的形式。根据裴蜀定理,当且仅当 \(\gcd(A,B) \mid C\) 时,方程有解。接下来,考虑方程有解 \((x,y)\) 时的情况,可以证明此时一定有解。

答案的上界:

  1. \(xy \geq 0\) 时,可以选择往A倒水喝水 \(x\) 次,对B同理,共 \(2(x+y)\) 次操作,因此答案不超过 \(2(x+y)\)

  2. \(xy < 0\) 时,可以看官方题解构造的方法,共 \(2|x-y| - 1\) 次操作,因此答案不超过 \(2|x-y| - 1\)

上界同时也是答案的下界,严谨证明可以看官方题解的构造函数法。

感性上的理解是,每次操作对 \(|x| + |y|\) 的变化量至多是 \(1\) ,因此上界的答案就是最优的。

最后,我们需要找到最优解,显然出现在 \(x,y\) 轴附近,所以我们需要对轴两侧的对答案取最小值。

实现时,我们需要把特解变换到最接近轴的位置,然后对附近三个位置取最小值,因为不知道得到的位置是左侧还是右侧,所以需要都算一下。

时间复杂度 \(O(\log C)\)

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

代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) { x = 1, y = 0; return a; }
    ll d = exgcd(b, a % b, x, y);
    x -= (a / b) * y, swap(x, y);
    return d;
}

ll get(ll x, ll y) {
    if (x >= 0 && y >= 0) return 2 * (x + y);
    else return 2 * abs(x - y) - 1;
}

bool solve() {
    ll a, b, c;
    cin >> a >> b >> c;

    ll x, y;
    ll d = exgcd(a, b, x, y);
    if (c % d) {
        cout << -1 << '\n';
        return true;
    }

    x *= c / d;
    y *= c / d;

    ll ans = 1e18;
    for (int i = -1;i <= 1;i++) {
        ll t = (-x) / (b / d) + i;
        ans = min(ans, get(x + t * (b / d), y - t * (a / d)));
    }
    for (int i = -1;i <= 1;i++) {
        ll t = (y) / (a / d) + i;
        ans = min(ans, get(x + t * (b / d), y - t * (a / d)));
    }
    cout << ans << '\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;
}