Solved 9 of 12.

坑一堆(

A. 小L的三则运算

题意

给定一个大于 \(1\) 的正整数 \(x\) 和一个二元运算符(加减乘之一),输出两个正整数,满足使用该二元运算符计算得到的结果为 \(x\)

思路

钦定其中一个正整数为 \(1\) 即可。

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

对应AC代码

void solve() {
    int x;
    string op;
    cin >> x >> op;
    if(op == "*") cout << x << ' ' << 1 << '\n';
    else if(op == "+") cout << x - 1 << ' ' << 1 << '\n';
    else cout << x + 1 << ' ' << 1 << '\n';
}

签到

B. 小L出师了

题意

给定三个正整数 \(n, t, k\),删去长度为 \(n\) 的数组中的 \(k\) 个不同的位置对应的元素,分割得到若干段,输出长度大于等于 \(t\) 的最多段落数量。

思路

因为我们可以随意选择删除的位置,所以我们可以先全都放在最后,求出段数后再把要删除的位置往前挪,那么显然我们最多可以得到 \(\lfloor \frac{n - k}{t} \rfloor\) 个长度为 \(t\) 的段落。

可是需要注意我们最多只能分得 \(k + 1\) 个段落,需要跟边界取最小值。

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

对应AC代码

void solve() {
    int n, t, k;
    cin >> n >> t >> k;
    cout << min(k + 1, (n - k) / t) << '\n';
}

简化了一下题意

C. 小L的位运算

题意

给定三个 \(n\) 位二进制数 \(a, b, c\),定义一次操作为下面四种操作任选其一:

  1. 花费 \(x\) 元,反置二进制数 \(a\) 的任意一位;

  2. 花费 \(x\) 元,反置二进制数 \(b\) 的任意一位;

  3. 花费 \(y\) 元,交换二进制数 \(a\) 的任意两个数位;

  4. 花费 \(y\) 元,交换二进制数 \(b\) 的任意两个数位。

在可以进行任意次操作的情况下,输出使得 \(a \oplus b = c\) 需要的最小花费。

思路

我们先不考虑花费,那么不难发现 \(a \oplus b\)\(c\) 的任意一位不对应时,\(a\)\(b\) 在该位的值只有四种可能,分别为 \(00, 01, 10, 11\)。而有趣的是任意一对都有至少一个元素不同,所以我们只需配对交换即可。通过一些贪心策略可以发现,当四种可能的数量中的最大值超过总和的一半时,多余的将无法配对,那么我们将多余的反置即可。

那么如果要考虑花费呢?因为我们上面的操作本质上是两两配对交换,那么这其实等价于分别反置需要交换的两位。所以我们可以比较一下两次反置和一次交换的大小关系,如果前者更小那么全都反置即可,否则采取前面的策略是最优的。

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

对应AC代码

void solve() {
    int n, x, y;
    cin >> n >> x >> y;
    string a, b, c;
    cin >> a >> b >> c;
    vector<int> cnt(4);
    for(int i=0;i<n;i++) {
        int p = a[i] - '0', q = b[i] - '0', r = c[i] - '0';
        if(!(p ^ q ^ r)) continue;
        cnt[p * 2 + q] ++;
    }
    int sum = accumulate(cnt.begin(), cnt.end(), 0ll), mx = *max_element(cnt.begin(), cnt.end());
    cout << min(x * sum, x * ((mx % 2 == 0 && sum % 2 == 1) + max(0ll, mx - sum / 2)) + y * (sum / 2)) << '\n';
}

要输出方案就头大了

D. 小L的字符串翻转

题意

给定一个长为 \(n\) 的二进制串,将其按照 \(k\) 个元素一段从左到右依次分段,最后一段元素个数可能小于 \(k\)。定义一次操作为选定一段,对整段执行反置或重排列。

在可以任意次操作的情况下,操作完成后将段落重新拼接,并将得到的二进制串中连续的 \(0\) 替换为一个 \(0\),连续的 \(1\) 替换为一个 \(1\),记 \(ans_k\) 为替换完成后的长度。输出下式的值:

\(\displaystyle \bigoplus_{k=1}^{n} ans_k\)

思路

首先,\(ans\) 即为拼接完成后 \(01, 10\) 的个数 \(+ 1\)

如果我们只有一段,那么显然将 \(0\)\(1\) 分开放在两端是最合适的。所以说在一个段落内,最多只会有一个 \(01, 10\),这取决于段落内是否同时包含 \(0\)\(1\)

那么答案如果段落变多,那么我们会希望拼接的时候两端都是 \(0\) 或都是 \(1\)。但显然这个是可控的,钦定第一段的右端点为 \(1\),第二段的左端点完全可以通过反置和重排列变为 \(1\),而后面也是一样的。

所以说,拼接对我们来说毫无影响,我们只需累加每一段的答案即可。

至于如何检查段落内是否同时包含 \(0\)\(1\),这可以用前缀和实现,因为我们完全可以做到枚举每一段的左端点,复杂度是调和级数。

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

对应AC代码

void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    s = " " + s;
    vector<int> pre(n + 1);
    for(int i=1;i<=n;i++) pre[i] = pre[i - 1] + (s[i] - '0');
    int ans = 0;
    for(int k=1;k<=n;k++) {
        int now = 1;
        for(int l=1;l<=n;l+=k) {
            if(pre[min(n, l + k - 1)] - pre[l - 1] == 0 || pre[min(n, l + k - 1)] - pre[l - 1] == min(n, l + k - 1) - l + 1) continue;
            now ++;
        }
        ans ^= now;
    }
    cout << ans << '\n';
}

差点以为复杂度是平方(

E. 小L的井字棋

题意

给定一个井字棋残局,约定残局合法且双方落子数量相同,在残局基础上,先手当前可以连下两个棋,然后继续正常比赛。在两人足够聪明的条件下,判断先手是否有必胜策略。

思路

首先我们无法找出一个通用的解法,比较无脑的是写搜索,但是没必要。

我们对落子数量进行分讨:

  1. 数量为 \(0\),此时下在中央和任意位置,先手必胜,可以手动模拟验证先手的必胜策略。

  2. 数量为 \(2\),此时先手的棋子只有一条线被封锁,任选一条没被封锁的线填满即可,先手必胜。

  3. 数量为 \(4\),此时先手的棋子最多只有一个被完全封锁,任选一条没被封锁的线填满即可,先手必胜。

而剩下的我们只需暴力填充,因为要么先手只能填入一个,要么后手只能填入一个。

时间复杂度:\(O(能过)\)

对应AC代码

void solve() {
    vector<string> s(3);
    for(auto &it : s) cin >> it;
    vector<vector<pii>> ls = {
        {{0, 0}, {1, 0}, {2, 0}},
        {{0, 1}, {1, 1}, {2, 1}},
        {{0, 2}, {1, 2}, {2, 2}},
        {{0, 0}, {0, 1}, {0, 2}},
        {{1, 0}, {1, 1}, {1, 2}},
        {{2, 0}, {2, 1}, {2, 2}},
        {{0, 0}, {1, 1}, {2, 2}},
        {{0, 2}, {1, 1}, {2, 0}}
    };
    int cnt = 0;
    for(auto &it : s) cnt += count(it.begin(), it.end(), 'G');
    cnt = 9 - cnt;
    if(cnt <= 4) {
        cout << "Yes\n";
        return;
    }
    for(int i1=0;i1<3;i1++) {
        for(int j1=0;j1<3;j1++) {
            if(s[i1][j1] != 'G') continue;
            for(int i2=0;i2<3;i2++) {
                for(int j2=0;j2<3;j2++) {
                    if(s[i2][j2] != 'G') continue;
                    auto ss = s;
                    ss[i1][j1] = ss[i2][j2] = 'X';
                    bool ok = false;
                    for(auto &l : ls) {
                        bool now = true;
                        for(auto &[x, y] : l) {
                            if(ss[x][y] != 'X') now = false;
                        }
                        ok |= now;
                    }
                    if(ok) {
                        cout << "Yes\n";
                        return;
                    }
                }
            }
        }
    }
    cout << "No\n";
}

如果去找规律这题就完蛋了(x

F. 小L的抽卡

待补充

G. 小L的三元组

待补充

H. 小L的min-max问题

题意

给定一个长为 \(n\) 的数组,将其切成 \(k\) 段,切法的权值为所有段落最小值与最大值之积的总和。输出所有切法的权值总和。

思路

首先,我们可以单独计算每一段的权值,并计算这一段会出现多少次,相乘累加即可得到题目所求。

因而我们只需考虑每一段会出现多少次。假设本段左边有 \(x\) 个元素,右侧有 \(y\) 个元素,那么我们可以枚举左边分割为 \(i\) 段,右侧即为 \(k - 1 - i\) 段。

\(n\) 个元素分成 \(m\) 段,且每段内至少有一个元素,这相当于在 \(n\) 个元素之间插板,也就是 \(n - 1\) 个空隙中选择 \(m - 1\) 个,用组合数表示即为:

\(\displaystyle \binom{n - 1}{m - 1}\)

那么如果 \(x = 0\)\(y = 0\),用这个组合数即可得到答案,否则我们可以将出现次数表示为:

\(\displaystyle \sum_{i=1}^{k-2} \binom{x - 1}{i - 1} \binom{y - 1}{k - i - 2}\)

我们稍微变一下式子,将 \(i - 1\) 整体替换:

\(\displaystyle \sum_{i=0}^{k-3} \binom{x - 1}{i} \binom{y - 1}{k - 3 - i}\)

这是经典的范德蒙德卷积,我们可以直接得出答案:

\(\displaystyle \sum_{i=0}^{k-3} \binom{x - 1}{i} \binom{y - 1}{k - 3 - i} = \binom{x + y - 2}{k - 3}\)

至于权值,在遍历的时候顺便更新区间最小最大即可,不过用 ST 表自然也是可以的。

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

对应AC代码

constexpr int P = 998244353;
using Z = MInt<P>;

//线性求阶乘,逆元,阶乘逆元
static void get_fact_inv(const int n, int fac[], int inv[], int fac_inv[], const int p = mod) {
    fac[0] = fac_inv[0] = inv[0] = fac[1] = fac_inv[1] = inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        fac[i] = fac[i - 1] * i % p;
        inv[i] = (p - p / i * inv[p % i] % p) % p; //线性求逆元
        fac_inv[i] = fac_inv[i - 1] * inv[i] % p;
    }
}

int fac[N], inv[N], fac_inv[N];

void init() {
    get_fact_inv(1e4, fac, inv, fac_inv);
}

Z C(int n, int m) {
    if(n < m) return {0};
    return Z{fac[n]} * fac_inv[m] * fac_inv[n - m];
}

void solve() {
    int n, k;
    cin >> n >> k;
    vector<int> a(n + 1);
    for(int i=1;i<=n;i++) cin >> a[i];
    Z ans = 0;
    for(int l=1;l<=n;l++) {
        int mx = -inf, mn = inf;
        for(int r=l;r<=n;r++) {
            if(n - (r - l + 1) < k - 1) break;
            mx = max(mx, a[r]);
            mn = min(mn, a[r]);
            Z now = 1;
            if(l > 1 && r < n) now = C(l - 1 + n - r - 2, k - 3);
            if(l == 1 && r < n) now = C(n - r - 1, k - 2);
            if(l > 1 && r == n) now = C(l - 2, k - 2);
            ans += now * mx * mn;
        }
    }
    cout << ans << '\n';
}

怎么感觉老是能在牛客看到范德蒙德卷积

I. 小L的数学题

题意

给定两个整数 \(n, m\),定义一次操作为下面二者任选其一:

  1. 将数字乘 \(2\)

  2. 将数字开平方根后下取整

判断能否经过任意次操作,将 \(n\) 变为 \(m\)

思路

我们先处理 \(0\) 的情况,因为正整数不可能开平方根到 \(0\),所以如果两者其一为 \(0\),其一非 \(0\),就是无解。而显然相等有解。

接下来考虑正整数之间的变化。首先任何正整数经过有限次开平方根操作后都会变成 \(1\),我们来考虑怎么从 \(1\) 通过上述操作到达任意数字。

首先,显然存在 \(x, k\),满足 \(k^{2^j} \leq x < (k + 1)^{2^j}\),那么我们可以通过开 \(j\) 次平方根从 \(x\) 变成 \(k\)

我们将式子两边取以 \(2\) 为底的对数,有 \(2^j \log_2 k \leq \log_2 x < 2^j \log_2 (k + 1)\)

我们将左右两端点作差,得到 \(2^j \log_2 \frac{k + 1}{k}\),显然只要 \(j\) 足够大,结果就会大于等于 \(1\)。所以一定存在整数 \(p\) 满足 \(2^j \log_2 k \leq p < 2^j \log_2 (k + 1)\)。回到原式,可得 \(k^{2^j} \leq 2^p < (k + 1)^{2^j}\)

所以我们只需将 \(n\) 先开平方根到 \(1\),然后一定可以找出整数 \(p, j\) 满足 \(2^j \log_2 m \leq p < 2^j \log_2 (m + 1)\),乘 \(p\)\(2\) 之后,开 \(j\) 次平方根得到 \(m\)

所以当且仅当两者其一为 \(0\),其一非 \(0\),就是无解。否则一定有解。

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

对应AC代码

void solve() {
    int n, m;
    cin >> n >> m;
    cout << ((n != m && (n == 0 || m == 0)) ? "No\n" : "Yes\n");
}

证明参考了出题人题解

J. 小L的汽车行驶问题

题意

给定一个整数 \(n\),在 \(n\) 秒内,每秒的时刻初可以进行三种操作:

  1. 踩油门,速度瞬间增大 \(10\)

  2. 踩刹车,速度瞬间降低 \(5\)

  3. 踩离合,当前时刻速度降低 \(10\),时刻末恢复原速度

在速度不低于 \(0\) 的前提下,给定每秒的操作,计算位移。

思路

模拟即可。

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

对应AC代码

void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    int ans = 0, v = 0;
    for(auto &it : s) {
        if(it == '0') {
            v += 10;
            ans += v;
        }else if(it == '1') {
            v = max(0ll, v - 5);
            ans += v;
        }else {
            ans += max(0ll, v - 10);
        }
    }
    cout << ans << '\n';
}

好直白

K. 小L的几何

待补充

L. 小L的构造

题意

给定一个整数 \(n\),在每个数最多可以使用一次的情况下,输出最多能构造的三元组 \((x, y, z)\) 数量,满足刚好有两对数互质。

思路

首先,注意到存在下面的周期为 \(6\) 的构造方案:

\((i, i + 1, i + 3), (i + 2, i + 4, i + 5)\)

因为周期为偶数,所以奇偶性不变,而由于偶数一定不互质,所以 \((i + 1, i + 3)\) 不互质。

其次,相邻数一定互质,所以 \((i, i + 1), (i + 4, i + 5)\) 互质。

接着,相邻奇数一定互质,因为最小的奇质数大于 \(2\),所以 \((i + 2, i + 4)\) 互质。

然后,根据辗转相除法可知,两个数的最大公因数如果不是其中的某个数,就一定小于两个数的差的绝对值。而由于 \(i\)\(i + 3\) 相差为 \(3\),且 \(i\)\(6\)\(1\),所以两者的最大公因数只能小于 \(3\),即 \(2\) 或互质。由于 \(i\) 为奇数,所以不可能出现偶数因子,所以 \((i, i + 3)\) 互质。同理可得 \((i + 2, i + 5)\) 不互质,因为 \(i + 2\)\(3\) 的倍数。

但是当 \(n \bmod 6 \geq 3\) 时,可能存在漏解。我们考虑构造 \(n = 9\) 的情况,如下:

\((1, 2, 4), (3, 5, 9), (6, 7, 8)\)

因为为固定数字,可以通过注意力得出正确性。

而后续的方案需要换成下面的样子(不唯一):

\((i, i + 1, i + 4), (i + 2, i + 3, i + 5)\)

通过类似的方法可以证明正确性。

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

对应AC代码

void solve() {
    int n;
    cin >> n;
    if(n <= 3) {
        cout << 0 << '\n';
        return;
    }
    if(n <= 5) {
        cout << 1 << '\n' << 2 << ' ' << 3 << ' ' << 4 << '\n';
        return;
    }
    cout << n / 3 << '\n';
    int i = 1;
    if(n % 6 >= 3) {
        cout << i << ' ' << i + 1 << ' ' << i + 3 << '\n';
        cout << i + 2 << ' ' << i + 4 << ' ' << i + 8 << '\n';
        cout << i + 5 << ' ' << i + 6 << ' ' << i + 7 << '\n';
        i += 9;
        for(;i+5<=n;i+=6) {
            cout << i << ' ' << i + 1 << ' ' << i + 4 << '\n';
            cout << i + 2 << ' ' << i + 3 << ' ' << i + 5 << '\n';
        }
    }else {
        for(;i+5<=n;i+=6) {
            cout << i << ' ' << i + 1 << ' ' << i + 3 << '\n';
            cout << i + 2 << ' ' << i + 4 << ' ' << i + 5 << '\n';
        }
    }
}

全是注意力