Solution from problem setter.

简单算法场

A. Amiable Parameter

背景

福师大第20届校赛 E 题 改编。

题意

给定 \(f(x) = |x - a_1| + |x - a_2| + \ldots + |x - a_n| + b_1 + b_2 + \ldots + b_n\),找出最小的 \(x\),使 \(f(x)\) 最小。

思路

计算中位数。

我们可以用点到数轴上的距离来形象化式子的左半部分。

如果数轴上有 \(n\) 个点,要找到一个点 \(p\),使 \(p\) 和这些点的距离之和最小,显然我们取这些数的中位数。

你可以发现,如果中位数有两个,一定是取最小的那个中位数,因为两个中位数的答案是一样的。

那么,我们将 \(a_i\) 读入到数组中 并 升序排序,并统计 \(b_i\) 的总和 \(sum_b\),然后我们对中位数算一下答案即可。

最后,别忘了加上右半部分式子的结果 \(sum_b\)

注意,本题无法使用复杂度为 \(O(n ^ 2)\) 的排序算法通过。

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int inf = 0x3f3f3f3f3f3f3f3f;

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    int sum_b = 0;
    for(int i=0;i<n;i++){
        int A, B;
        cin >> A >> B;
        a[i] = A, sum_b += B;
    }
    sort(a.begin(), a.end());
    vector<int> mid = {a[n / 2]};
    if(n % 2 == 0) mid.push_back(a[n / 2 - 1]);
    int at = -1, ans = inf;
    for(auto x : mid){
        int cur = 0;
        for(int i=0;i<n;i++) cur += abs(a[i] - x);
        if(cur < ans) ans = cur, at = x;
        else if(cur == ans) at = min(at, x);
    }
    cout << at << ' ' << ans + sum_b << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

bonus: 原题的修改是在线的,需要使用对顶堆维护

B. Breaking Dawn

题意

给定 \(n\) 张牌,输出是否存在一种方案,满足:

  1. 从中取出若干张牌,伤害总和 \(\geq X\)

  2. 剩余的牌伤害总和 \(\geq Y\)

思路

背包问题。

首先,乱贪心是完全错误的,无论怎么排序,你都不能保证最后一定是最优解。

我们不妨这么考虑:设 \(sum\) 为所有牌的伤害总和,那么如果存在一种方案,满足伤害总和恰好为 \(p\),且满足 \(p \geq X, sum - p \geq Y\),那么这个方案就是我们想要的。

这里,我们需要使用 \(01\) 背包 来解决这个问题 (动态规划)。

这是一个经典的 \(01\) 背包,我们将伤害总和想象为一个背包的容量,将一张牌的伤害想象为这张牌所占用的体积。

那么,我们定义 \(dp[i]\) 为容积为 \(i\) 的背包是否能装满。

你可以用二维空间的 \(01\) 背包解决问题(需要滚动数组),但显然一维空间会更优。

我们可以从前往后枚举所有牌,并从大到小枚举背包的容量。

如果某个牌的体积是 \(a_i\),那么如果 \(dp[c - a_i]\) 为真,也就是容量为 \(c - a_i\) 的背包可以装满,放入这个牌之后,容量为 \(c\) 的背包也就可以装满了。

从而有状态转移方程:$dp[c] = dp[c]  |   dp[c - a_i] $。(取或)

最后,我们检查是否存在一个满足上面条件的 \(p\),有 \(dp[p] = true\) 即可。

当然,\(dp[0] = true\)

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

对应AC代码1

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n, x, y;
    cin >> n >> x >> y;
    vector<int> a(n + 1);
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum += a[i];
    }
    vector<int> dp(100010); //干脆开大一点,省事(
    dp[0] = true;
    for (int i = 1; i <= n; i++) {
        for (int v = x; v >= a[i]; v--) {
            dp[v] |= dp[v - a[i]];
        }
    }
    for (int i = x; i <= sum && sum - i >= y; i++) {
        if (dp[i]) {
            cout << "Breaking Dawn!" << '\n';
            return;
        }
    }
    cout << "Adventure!" << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

本题也可以用 \(\mathtt{bitset}\) 优化复杂度,这也是很经典的。但好心的出题人并没有想要卡掉上面的做法。

时间复杂度:\(O(nx / 64)\)

对应AC代码2

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int N, X, Y, s = 0, a;
    cin >> N >> X >> Y;
    bitset<100007> bit;
    bit[0] = 1;
    for (int i = 1; i <= N; i++) {
        cin >> a;
        s += a;
        bit |= bit << a;
    }
    for (int i = X; i <= s && s - i >= Y; i++) {
        if (bit[i]) {
            cout << "Breaking Dawn!\n";
            return;
        }
    }
    cout << "Adventure!\n";
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

背包专题在 oj 里有

C. Calculate my Ptt, please!

题意

给定 \(n\) 个两位小数,令 \(ptt = (\) 最大的 \(10\) 个数之和 \(+\) 最大的 \(30\) 个数之和 \() / 40\),将 \(ptt\) 保留两位小数,并去掉多余的小数位,而不是四舍五入。

输出 \(ptt\)

思路

坑人签到题。

本题有三个坑点:

  1. 不要使用 \(O(n ^ 2)\) 的排序算法;你可以使用自带的 \(sort\) 函数(\(c\) 语言下是 \(qsort\));

  2. 避免对 \(double\) 进行累加求和;本题中,你可以把数字全都乘 \(100\) 并转为 \(long\ long\),来规避这个问题;

  3. 注意审题,不要四舍五入;本题中,你可以在上面的操作基础上,向下取整,接着除 \(100\),最后保留两位小数输出即可。

避开这三个坑点,这题就是签到题,模拟一下过程即可。

基本上挂的人都是挂在累加 \(double\),然后乘 \(100\),向下取整,然后除 \(100\)

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n;
    cin >> n;
    vector<int> a(n);
    for(int i = 0; i < n; i ++) {
        double cur;
        cin >> cur;
        a[i] = (int) (cur * 100); //全都乘个100,方便算
    }
    sort(a.rbegin(), a.rend()); //自带函数复杂度 O(n log n)
    double ptt = 0;
    for(int i = 0; i < 30; i ++){
        ptt += (i < 10 ? 2.0 : 1.0) * a[i];
    }
    ptt /= 40;
    cout << fixed << setprecision(2) << ((int) ptt) / 100.0 << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

其实样例就是我的 ptt((

D. Dozens of Move

题意

给定一个包含 .# 的字符串,指定一开始的位置。

有一个机器人在上面移动,一开始机器人向右移动,若遇到 #,机器人会将其改为 .,并更改移动方向;如果遇到左右边界,也会更改移动方向。

输出机器人在移动了几次后,字符串里的所有 # 才能变为 .

思路

双指针。

首先,最朴素的做法就是模拟移动,但这样的复杂度最坏可以达到 \(O(n ^ 2)\) 级别,显然是无法通过的。

我们可以发现,每次移动后,我们都在扩展我们的可移动区间,并且扩展之后,这些地方都会被重新走好几次。

我们记当前扩展得到的区间为 \([l, r]\)

那么比如我们这一次需要向左扩展,我们就完全没必要从 \(r\) 再模拟走到 \(l\),因为我们知道 \([l, r]\) 上面一定没有 #,而且需要走的步数是已知的 \(r - l\),那么我们不妨将答案加上 \(r - l\),然后直接跳到 \(l\),并继续向左扩展。向右扩展的方法是类似的。

那么,我们可以发现,现在每个格子就只会被访问到一次了,从而将时间复杂度降为了最坏 \(O(n)\)

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

对应AC代码 (Ocean)

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n, a;
    cin >> n >> a;
    string s;
    cin >> s;
    s = " " + s; //让下标从1开始
    int cnt = 0;
    for(auto &e : s){
        if(e == '#') cnt ++;
    }
    int l = a, r = a;
    int ans = 0;
    int d = 1; //1 right, -1 left
    while(cnt > 0){
        ans += r - l;
        if(d == 1) {
            while(r <= n && s[r] == '.') r ++, ans ++;
            if(r <= n) s[r] = '.', cnt --;
        }else{
            while(l >= 1 && s[l] == '.') l --, ans ++;
            if(l >= 1) s[l] = '.', cnt --;
        }
        d = -d;
    }
    cout << ans << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

对应AC代码 (Std)

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

int main(){
    int N, A;
    string S;
    cin >> N >> A >> S;
    int l = A-1, r = A-1, n = A-1, d = 1, cnt = 0;
    for(int i=0; i<S.size(); i++){
        if(S[i] == '#') cnt++;
    }
    long long ans = 0;
    while(cnt > 0){
        if(d == 1){
            ans++, n++;
            if(n < N) r = n;
            if(n == N || S[n] == '#'){
                if(n < N) cnt--;
                d = -1;
                if(cnt > 0) ans += n - l, n = l; // skip
            }
        }
        else{
            ans++, n--;
            if(n >= 0) l = n;
            if(n == -1 || S[n] == '#'){
                if(n >= 0) cnt--;
                d = 1;
                if(cnt > 0) ans += r - n, n = r; // skip
            }
        }
    }
    cout << ans << endl;
      return 0;
}

严格来说也不是双指针算法,只是有两个指针罢了(

E. End with NO 0

背景

灵感来自暑假牛客集训营的某一题(有点忘了)。

题意

对于 \(1, 2, \ldots, n\),删除尽可能少的数字,来让剩余数字的乘积不包含后缀 \(0\)。输出剩余数字的最大个数。

思路

简单思维题。

首先,既然要让乘积末尾出现 \(0\),乘积中肯定要同时存在 \(2\) 的倍数和 \(5\) 的倍数。

那么很明显,我们只要让剩余数字中不包含 \(2\) 的倍数,或者不包含 \(5\) 的倍数就好了。

显然,从 \(1\) 开始的话,肯定是 \(5\) 的倍数更少,从而我们考虑删除 \(5\) 的倍数,剩下的数字个数就是我们要的答案。

好心的出题人没有把范围开到 \(10 ^ {18}\),他怕你一顿操作爆 \(long\ long\) 了(x

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n;
    cin >> n;
    cout << (n - n / 5) << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

下一场要么来卡一下 \(long\ long\)

F. Force Ripening

背景

题目来自 Codeforces CodeTON Round 4 (Div. 1 + Div. 2) B题,难度 \(800\)

题意

对于 \(x\),一开始 \(x = 1\),定义操作为下面选项任选一:

  1. \(x := 2x - 1\)

  2. \(x := 2x + 1\)

现在,给定操作后的数,判断是否可以还原操作方案,不可以输出 \(-1\),可以的话输出方案。

思路

思维题。

首先,很显然,操作后得到的数不可能是偶数,那么我们就可以进行特判。

在上面的基础上,我们可以发现,若操作后的数是 \(p\),那么原来的数可以是 \(\frac{p - 1}{2}, \frac{p + 1}{2}\)

显然,这两个数是一奇一偶的,而原来的数也不可能是偶数,从而原来的数是唯一确定的。

那么,我们可以倒推,只要能推到 \(1\),就可以还原操作方案(当然,既然我们是倒推,记得反着输出)。

事实上,上面的操作可以将 \(1\) 变为任意正奇数。

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n;
    cin >> n;
    if(n % 2 == 0){
        cout << -1 << '\n';
        return;
    }
    vector<int> ans;
    while(n > 1){
        if((n - 1) / 2 % 2 == 0) n = (n + 1) / 2, ans.emplace_back(1); //≈push_back
        else n = (n - 1) / 2, ans.emplace_back(2);
    }
    reverse(ans.begin(), ans.end()); //反转一下
    cout << ans.size() << '\n';
    for(auto each : ans) cout << each << ' ';
    cout << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while(t --) solve();
}

逆向思维(

G. Genshin Impact, Start!

题意

给定一个灰度值矩阵,找出与 \(255\) 的差值不小于 \(k\) 的点的个数 \(cnt\),如果 \(cnt\) 不超过 \(p\),那么判定启动。

思路

简单签到题。

显然,我们将所有数全都读进来,然后根据题面模拟计算即可。

注意题面中的 "不小于","不超过"。

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int k, p, n, m;
    cin >> k >> p >> n >> m;
    int cnt = 0;
    for (int i = 0; i < n * m; i ++) {
        int cur;
        cin >> cur;
        if (255 - cur >= k) cnt ++;
    }
    if (cnt <= p) cout << "zenmenile" << '\n';
    else cout << "wande" << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

本来想出的更麻烦一点,但这种 ___ 题还是去 \(AtCoder\) 做吧

H. Homework of Math

题意

给定整数 \(n\),输出 \((a + b) ^ n\) 的表达式,如果系数是 \(1\) 需要忽略,次方是 \(0\) 也需要忽略。

例如 \((a + b) ^ 2 = a ^ 2 + 2ab + b ^ 2\)

思路

组合数问题。

显然,总共有 \(n + 1\) 项,第 \(i\) 项的系数是 \(C_n^{i - 1}\)

对于系数,你可以暴力计算(只要不是太暴力),也可以使用杨辉三角递推。

出题人想要卡掉暴力计算和 \(long\ long\),但是被好心的组题人和验题人拦下了(x

但,于是这题变成了可以打表的题(?

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

对应AC代码 (Ocean)

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n;
    cin >> n;
    vector<int> p(n + 2);
    p[0] = 1;
    for (int i = 1; i <= n; i++) { //杨辉三角
        for (int j = i; j >= 1; j --) p[j] += p[j - 1];
    }
    cout << "(a+b)^" << n << "=";
    for(int i=0;i<=n;i++){
        if(p[i] != 1) cout << p[i];
        if(i != n) {
            cout << "a";
            if(i != n - 1) cout << '^' <<  (n - i);
        }
        if(i != 0) {
            cout << "b";
            if(i != 1) cout << '^' << i;
        }
        if(i != n) cout << "+";
    }
    cout << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

对应AC代码 (Std)

#include <cstdio>
#define FOR(i, l, r) for(int i = l; i <= r; i++)
using namespace std;
unsigned long long f[71];
int n;

int main()
{
    scanf("%d", &n); f[0] = 1;
    FOR(i, 1, n) for(int j = i; j; j--) f[j] += f[j - 1];
    printf("(a+b)^%d=", n);
    FOR(i, 0, n)
    {
        if (i) putchar('+');
        if (i && n - i) printf("%llu", f[i]);
        if (n - i)
        {
            putchar('a');
            if (n - i != 1) printf("^%d", n - i);
        }
        if (i)
        {
            putchar('b');
            if (i != 1) printf("^%d", i);
        }
    }
    puts("");
    return 0;
}

bonus: 如果 \(n \leq 67\) 呢?

I. I Super, Explosion

背景

2023 牛客寒假训练营 2 的 I 题 改编。

题意

给定 \(n\) 个音符,每个音符有对应偏移值 \(c\),定义 \(m\) 个判定区间 \((- \infty,a_1), [a_1, a_2), \ldots, [a_{m - 1},+\infty)\)\(c\) 落在第 \(i\) 个区间内可获得对应判定分 \(v_i\)。输出将所有的 \(c\) 加上或减去任意值后判定分总和的最大值。

思路

思维+排序。

首先,我们可以将所有音符的偏移值全都减去一个相同的且很大的值,来让所有音符都位于第一个判定区间。

然后,我们可以按照 每个音符 距离进入下一个判定区间 所需的偏移值更改量,从小到大进行偏移值的更改。

因为偏移值是整体移动的,所以如果按照上面的排序方式,当我们枚举到第三小的偏移值更改时,前面两个偏移值也一定进行了更改。

从而,我们只需在枚举的时候,累加每次更改偏移值后的分数变化,最后找出分数和最大的一种更改即可。

形象地来说,你可以将音符理解为数轴上的点,一开始所有点都在第一个判定区间,然后所有点向右整体移动,累加每次移动带来的分数变化,最后最大的分数和就是调整后判定分总和的最大值了。

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

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> c(n + 1), a(m), v(m + 1);
    for (int i = 1; i <= n; i++) {
        cin >> c[i];
        c[i] -= 2e9; //全部挪到第一判定区间
    }
    for (int i = 1; i < m; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) cin >> v[i];
    vector<pair<int, int>> p;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j < m; j++) {
            p.emplace_back(a[j] - c[i], v[j + 1] - v[j]); //设备偏移值,修改后分数的变化
        }
    }
    sort(p.begin(), p.end());
    int cur = v[1] * n, ans = cur;
    for (auto &e: p) {
        cur += e.second;
        ans = max(ans, cur);
    }
    cout << ans << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
}

听说有人想做牛客寒假的题(x

牛客的原题是可以二分的,这题不好说(x