坑一堆(

A. 小L的三则运算

题意

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

思路

钦定其中一个正整数为 即可。

时间复杂度:

对应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出师了

题意

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

思路

因为我们可以随意选择删除的位置,所以我们可以先全都放在最后,求出段数后再把要删除的位置往前挪,那么显然我们最多可以得到 个长度为 的段落。

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

时间复杂度:

对应AC代码

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

简化了一下题意

C. 小L的位运算

题意

给定三个 位二进制数 ,定义一次操作为下面四种操作任选其一:

  1. 花费 元,反置二进制数 的任意一位;

  2. 花费 元,反置二进制数 的任意一位;

  3. 花费 元,交换二进制数 的任意两个数位;

  4. 花费 元,交换二进制数 的任意两个数位。

在可以进行任意次操作的情况下,输出使得 需要的最小花费。

思路

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

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

时间复杂度:

对应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的字符串翻转

题意

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

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

思路

首先, 即为拼接完成后 的个数

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

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

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

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

时间复杂度:

对应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. 数量为 ,此时下在中央和任意位置,先手必胜,可以手动模拟验证先手的必胜策略。

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

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

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

时间复杂度:

对应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问题

题意

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

思路

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

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

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

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

我们稍微变一下式子,将 整体替换:

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

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

时间复杂度:

对应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的数学题

题意

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

  1. 将数字乘

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

判断能否经过任意次操作,将 变为

思路

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

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

首先,显然存在 ,满足 ,那么我们可以通过开 次平方根从 变成

我们将式子两边取以 为底的对数,有

我们将左右两端点作差,得到 ,显然只要 足够大,结果就会大于等于 。所以一定存在整数 满足 。回到原式,可得

所以我们只需将 先开平方根到 ,然后一定可以找出整数 满足 ,乘 之后,开 次平方根得到

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

时间复杂度:

对应AC代码

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

证明参考了出题人题解

J. 小L的汽车行驶问题

题意

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

  1. 踩油门,速度瞬间增大

  2. 踩刹车,速度瞬间降低

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

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

思路

模拟即可。

时间复杂度:

对应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的构造

题意

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

思路

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

因为周期为偶数,所以奇偶性不变,而由于偶数一定不互质,所以 不互质。

其次,相邻数一定互质,所以 互质。

接着,相邻奇数一定互质,因为最小的奇质数大于 ,所以 互质。

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

但是当 时,可能存在漏解。我们考虑构造 的情况,如下:

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

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

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

时间复杂度:

对应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';
        }
    }
}

全是注意力