跳转至

GESP 5级

B3941 [GESP样题 五级] 小杨的锻炼

每位同学按自己的周期锻炼,求下一次所有人同时锻炼要等多少天。
提示1-先把周期翻译成倍数

20分做法,\(n=2\)。

50分做法,\(n=4\)。

看全部数据范围,\(n\le 10\),\(a_{i}\le 50\)。

我先看两个人:相遇的天数必须同时是两个周期的倍数,最早的一天就是最小公倍数。

提示2-一次合并一个周期

我先求前两人的最小公倍数,再和第三人的周期合并,依次做下去。

我用这个公式合并两个周期:

\[ \operatorname{lcm}(x,y)=\frac{x}{\gcd(x,y)}\cdot y \]

先除后乘,结果用 long long,不能用 int。

代码-满分-逐次求最小公倍数

时间 \(O(n\log A)\),额外空间 \(O(1)\),\(A\le 50\)。所有人数范围都使用同一算法。

#include <bits/stdc++.h>

using namespace std;

long long gcd(long long a, long long b) {
    while (b != 0) {
        long long r = a % b;
        a = b;
        b = r;
    }

    return a;
}

int main() {
    int n;

    cin >> n;

    long long ans = 1;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        // 我把已有的共同周期和新周期合并,先除再乘。
        ans = ans / gcd(ans, x) * x;
    }

    cout << ans;

    return 0;
}

B3951 [GESP样题 五级] 小杨的队列

同学依次入队,每次加入后按身高排序,求最少交换任意两人的次数。
提示1-交换次数不等于人数

50分做法,身高互不相同。

看全部数据范围,\(m\le n\le 2000\),身高可达 int 上限。

我先试队伍 2、2、4,新来身高 1:和一个 2 交换,再和 4 交换,只用两次,不是三次。

提示2-每种更高的身高只需一次

原队伍已经排好,我让新来的同学和每一组更高身高的第一人依次交换,就能排好。

每个不同身高的更高组都要有人换位,至少一次。

因此我只统计已经入队、且更高的不同身高数量。

代码-满分-用集合记录身高

时间 \(O(m^{2}+m\log m)\),空间 \(O(n)\)。set 自动合并相同身高,不需要实际模拟队伍。

#include <bits/stdc++.h>

using namespace std;

const int N = 2005;

int height[N];

int main() {
    int n, m;

    cin >> n;
    for (int i = 0; i < n; i++) cin >> height[i];
    cin >> m;

    set<int> appeared;
    for (int t = 0; t < m; t++) {
        int id, ans = 0;
        cin >> id;
        // 我数的是更高的身高种类,不是更高的人数。
        for (int h : appeared) {
            if (h > height[id]) ans++;
        }
        appeared.insert(height[id]);

        if (t > 0) cout << '\n';
        cout << ans;
    }

    return 0;
}

B3871 [GESP202309 五级] 因数分解

把整数分解为质因数乘积,按从小到大顺序输出,重复因数写成幂。
提示1-不用试到 n

看全部数据范围,\(n\le 10^{12}\)。

我从 2 开始试除,每找到一个因数就除干净。

较小因数已经去掉,后来能整除的因数自然是质数。

提示2-剩下的数别漏掉

试到 \(p^2\) 大于剩余数就可以停,此时若还剩一个大于 1 的数,它就是最后一个质因数。

我用 \(p\le \frac{n}{p}\) 避免平方溢出。

输出时,只有前面输出过因子才加乘号。

代码-满分-试除分解

时间 \(O(\sqrt{n})\),额外空间 \(O(1)\)。\(n\) 和试除因子都用 long long。

#include <bits/stdc++.h>

using namespace std;

int main() {
    long long n;

    cin >> n;

    bool first = true;
    for (long long p = 2; p <= n / p; p++) {
        if (n % p != 0) continue;
        int count = 0;
        while (n % p == 0) {
            n /= p;
            count++;
        }

        // 我先处理分隔符,第一次输出前不加乘号。
        if (!first) cout << " * ";
        cout << p;
        if (count > 1) cout << '^' << count;
        first = false;
    }
    if (n > 1) {
        if (!first) cout << " * ";
        cout << n;
    }

    return 0;
}

B3872 [GESP202309 五级] 巧夺大奖

每个小游戏耗时一个时间段,只有期限内完成才获奖,求最大奖励。
提示1-奖励高的不一定先做

看全部数据范围,\(n\le 500\),期限\(\le n\),奖励\(\le 1000\)。

我先按奖励从大到小考虑,但不能一律塞到最早的时间:这样可能挡住期限更紧的游戏。

提示2-给后面的游戏留余地

当前游戏值得选,我就把它放到期限内最晚的空位,把前面的空位留给期限更紧的游戏。

没有空位就跳过。

数据只有 500,直接向前找空位足够,不必再用复杂结构。

代码-满分-按奖励安排最晚空位

时间 \(O(n^{2})\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 505;

struct Game {
    int deadline, reward;
};

Game a[N];
bool used[N];

bool cmp(Game x, Game y) {
    return x.reward > y.reward;
}

int main() {
    int n;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i].deadline;
    for (int i = 1; i <= n; i++) cin >> a[i].reward;
    sort(a + 1, a + n + 1, cmp);

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        // 我尽量晚做当前游戏,把早的空位留给期限更紧的游戏。
        for (int time = a[i].deadline; time >= 1; time--) {
            if (used[time]) continue;
            used[time] = true;
            ans += a[i].reward;
            break;
        }
    }

    cout << ans;

    return 0;
}

B3929 [GESP202312 五级] 小杨的幸运数

不小于a的平方数及其倍数为幸运数;判断查询数,否则求不小于它的最小幸运数。
提示1-先标记,不逐次猜

30分做法,\(a,x,n\le 100\)。

60分做法,\(a,x\le 10^{6}\)。

看全部数据范围,\(n\le 2\times 10^{5}\),\(x\le 1000001\)。

我发现幸运数是平方数的倍数,就像筛法一样,枚举满足条件的平方数,标记它的所有倍数。

提示2-查询上界之外也要准备

非幸运数的答案可能超过查询上界。

我找到 \(1001^2=1002001\),它大于所有查询数,且一定是超级幸运数。

标记到这里,再从右向左记录最近的幸运数,每次查询就能直接回答。

代码-满分-筛倍数与倒序预处理

设预处理上界 \(U=1002001\),时间 \(O(U+n)\),空间 \(O(U)\)。平方数倍数标记总量由 \(U\sum_{i=1}^{\lfloor\sqrt{U}\rfloor}\frac{1}{i^{2}}=O(U)\) 控制。

#include <bits/stdc++.h>

using namespace std;

const int U = 1002001, N = U + 5;

bool lucky[N];
int next_lucky[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int a, n;

    cin >> a >> n;
    for (int i = 1; i <= U / i; i++) {
        int square = i * i;
        if (square < a) continue;
        for (int j = square; j <= U; j += square) lucky[j] = true;
    }

    // 我把上界外的答案也准备好,再从右往左找最近的幸运数。
    int nearest = U;
    for (int i = U; i >= 1; i--) {
        if (lucky[i]) nearest = i;
        next_lucky[i] = nearest;
    }

    for (int t = 0; t < n; t++) {
        int x;
        cin >> x;
        if (t > 0) cout << '\n';
        if (lucky[x]) cout << "lucky";
        else cout << next_lucky[x];
    }

    return 0;
}

B3930 [GESP202312 五级] 烹饪问题

从两种不同食材中,求美味度按位与的最大值。
提示1-先写两层循环

40分做法,\(n\le 1000\)。

看全部数据范围,\(n\le 10^{6}\),\(0\le a_{i}\le 2147483647\)。

小范围我直接枚举一对食材。

百万个数不能逐对比较,但每个数只有 31 个有效二进制位。

提示2-高位能取 1 就取 1

我从高位往低位试着把答案这一位设成 1,检查至少两个数是否同时包含答案里所有的 1。

若能找到,就保留这一位。

否则放弃。

一个高位比所有低位加起来还大,所以不能为了低位牺牲它。

代码-40分-枚举食材对

时间 \(O(n^{2})\),空间 \(O(n)\),覆盖 \(n\le 1000\) 的测试点。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int a[N];

int main() {
    int n;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    int ans = 0;
    // 我先比较每一对食材,确认按位与的含义。
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) ans = max(ans, a[i] & a[j]);
    }

    cout << ans;

    return 0;
}
代码-满分-按位贪心

时间 \(O(31n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 1000005;

int a[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    int ans = 0;
    for (int bit = 30; bit >= 0; bit--) {
        int candidate = ans | (1 << bit), count = 0;
        // 我检查的是所有已选高位,不能只检查新试的这一位。
        for (int i = 1; i <= n; i++) {
            if ((a[i] & candidate) == candidate) {
            count++;
            if (count == 2) break;
        }
        }
        if (count >= 2) ans = candidate;
    }

    cout << ans;

    return 0;
}

B3968 [GESP202403 五级] 成绩排序

按三条成绩规则排名,保留并列名次,按原输入顺序输出每个人的排名。
提示1-把规则依次写清

30分做法,\(n\le 100\)且总分不同。

看全部数据范围,\(n\le 10^{4}\),各科\(\le 150\)。

我依次比较总分、语数总分、语数最高分,前一项分出高低后就不看后面的项。

提示2-名次相同,不等于下一名紧接着

排序后,三项都相同就沿用前一个人的名次。

否则名次就是当前下标加 1。

我还要记住原编号,把算好的名次放回原位置,不能按排名输出编号。

代码-满分-排序并还原顺序

时间 \(O(n\log n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 10005;

struct Student {
    int total, two, best, id;
};

Student a[N];
int ranking[N];

bool cmp(Student x, Student y) {
    if (x.total != y.total) return x.total > y.total;
    if (x.two != y.two) return x.two > y.two;

    return x.best > y.best;
}

int main() {
    int n;

    cin >> n;
    for (int i = 0; i < n; i++) {
        int c, m, e;
        cin >> c >> m >> e;
        a[i] = {c + m + e, c + m, max(c, m), i};
    }
    sort(a, a + n, cmp);

    int rank = 1;
    for (int i = 0; i < n; i++) {
        // 我在出现新的成绩组时按实际位置定名次,保留并列后的空位。
        if (i == 0 || cmp(a[i - 1], a[i])) rank = i + 1;
        ranking[a[i].id] = rank;
    }

    for (int i = 0; i < n; i++) {
        if (i > 0) cout << '\n';
        cout << ranking[i];
    }

    return 0;
}

B3969 [GESP202403 五级] B-smooth 数

统计1到n中,最大质因数不超过B的整数数量,1也计入。
提示1-先逐个分解

30分做法,\(n\le 1000\)。

另外30分做法,\(B\ge \sqrt{n}\)。

看全部数据范围,\(n,B\le 10^{6}\)。

我先逐个分解质因数,得到最大质因数。

1 没有质因数,题目把它算在答案里。

提示2-用筛法一起求

百万个数逐个分解会重复试除,我改用筛法:每个质数 \(p\) 更新它的所有倍数。

质数从小到大处理,最后留下的就是最大质因数,再统计不超过 \(B\) 的数。

代码-30分-逐个试除

时间 \(O(n\sqrt{n})\),额外空间 \(O(1)\),覆盖 \(n\le 1000\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n, b;

    cin >> n >> b;

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int x = i, largest = 1;
        for (int p = 2; p <= x / p; p++) {
            if (x % p != 0) continue;
            largest = p;
            while (x % p == 0) x /= p;
        }
        if (x > 1) largest = x;
        // 我让1的最大质因数记作1,使它自然计入答案。
        if (largest <= b) ans++;
    }

    cout << ans;

    return 0;
}
代码-满分-筛最大质因数

时间 \(O(n\log\log n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 1000005;

int largest[N];

int main() {
    int n, b;

    cin >> n >> b;

    largest[1] = 1;
    for (int p = 2; p <= n; p++) {
        if (largest[p] != 0) continue;
        // 我按质数从小到大覆盖,最后留下的因数就是最大的。
        for (int j = p; j <= n; j += p) largest[j] = p;
    }

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (largest[i] <= b) ans++;
    }
    cout << ans;

    return 0;
}

P10719 [GESP202406 五级] 黑白格

求至少含k个黑格子的最小子矩形面积,不存在时输出0。
提示1-先枚举小范围

20分做法,\(n,m\le 10\)。

另外40分做法,\(n=1\)。

看全部数据范围,\(n,m\le 100\)。

我先枚举四条边,再逐格数黑格子。

小矩阵和只有一行时,这个直接的办法就能覆盖前两个子任务。

提示2-固定上下两行

大矩阵要少枚举一条边。

我先固定上、下边界,把每一列在这两行之间的黑格子数加起来,就变成一排非负数。

提示3-非负数可以用双指针

列区间的和达到 \(k\) 后,我尝试缩短左边,直到不能再缩。

右边继续向前,寻找更好的面积。

上边界不能超过下边界。

每组上下边界只扫描一遍所有列。

代码-60分-逐格枚举矩形

时间 \(O(n^{3}m^{3})\),空间 \(O(nm)\),覆盖 \(n,m\le 10\) 和 \(n=1\) 两个子任务,不适用于一般最大规模。

#include <bits/stdc++.h>

using namespace std;

const int N = 105, INF = 1000000000;

int a[N][N];

int main() {
    int n, m, k;

    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= m; j++) a[i][j] = s[j - 1] - '0';
    }

    int ans = INF;
    for (int top = 1; top <= n; top++) {
        for (int bottom = top; bottom <= n; bottom++) {
            for (int l = 1; l <= m; l++) {
                for (int r = l; r <= m; r++) {
                    // 我先按题意逐格数,得到可以验证的小范围答案。
                    int count = 0;
                    for (int i = top; i <= bottom; i++) {
                        for (int j = l; j <= r; j++) count += a[i][j];
                    }
                    if (count >= k) ans = min(ans, (bottom - top + 1) * (r - l + 1));
                }
            }
        }
    }

    cout << (ans == INF ? 0 : ans);

    return 0;
}
代码-满分-压缩行与双指针

时间 \(O(n^{2}m)\),空间 \(O(nm)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 105, INF = 1000000000;

int a[N][N], column[N];

int main() {
    int n, m, k;

    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        for (int j = 1; j <= m; j++) a[i][j] = s[j - 1] - '0';
    }

    int ans = INF;
    for (int top = 1; top <= n; top++) {
        fill(column, column + m + 1, 0);
        for (int bottom = top; bottom <= n; bottom++) {
            for (int j = 1; j <= m; j++) column[j] += a[bottom][j];

            int l = 1, sum = 0;
            for (int r = 1; r <= m; r++) {
                sum += column[r];
                // 我利用列和非负,尽量收紧左边界。
                while (l <= r && sum - column[l] >= k) sum -= column[l++];
                if (sum >= k) ans = min(ans, (bottom - top + 1) * (r - l + 1));
            }
        }
    }

    cout << (ans == INF ? 0 : ans);

    return 0;
}

P10720 [GESP202406 五级] 小杨的幸运数字

逐个判断输入数是否恰好含两种不同的质因数。
提示1-重复的质因数只算一种

40分做法,\(n\le 100\)、数值\(\le 10^{5}\)。

看全部数据范围,\(n\le 10^{4}\)、数值\(\le 10^{6}\)。

我先试 \(12=2\times 2\times 3\):虽然有三个质因数相乘,但只有两种。

找到一个质因数后,要把它除干净再计一次。

提示2-查询多,先筛出答案

很多数重复分解不划算。

我让每个质数给自己的所有倍数加 1,得到每个数的不同质因数种类数。

筛到输入的最大值,查询时只检查计数是否为 2。

代码-40分-逐个试除

每个数时间 \(O(\sqrt{A})\),额外空间 \(O(1)\),覆盖第一个子任务。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n;

    cin >> n;
    for (int t = 0; t < n; t++) {
        int x, count = 0;
        cin >> x;
        for (int p = 2; p <= x / p; p++) {
            if (x % p != 0) continue;
            // 我把同一种质因数一次除干净,只计一种。
            while (x % p == 0) x /= p;
            count++;
        }
        if (x > 1) count++;

        if (t > 0) cout << '\n';
        cout << (count == 2 ? 1 : 0);
    }

    return 0;
}
代码-满分-筛质因数种类数

时间 \(O(A\log\log A+n)\),空间 \(O(A+n)\),\(A\) 为输入最大值。

#include <bits/stdc++.h>

using namespace std;

const int N = 1000005, M = 10005;

int count_prime[N], a[M];

int main() {
    int n, largest = 0;

    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        largest = max(largest, a[i]);
    }

    for (int p = 2; p <= largest; p++) {
        if (count_prime[p] != 0) continue;
        // 我让每个质数只给自己的倍数加一次,不看它出现几次。
        for (int j = p; j <= largest; j += p) count_prime[j]++;
    }

    for (int i = 0; i < n; i++) {
        if (i > 0) cout << '\n';
        cout << (count_prime[a[i]] == 2 ? 1 : 0);
    }

    return 0;
}

B4050 [GESP202409 五级] 挑战怪物

物理伤害依次翻倍,魔法最多用一次且伤害为质数,求恰好击败怪物的最少攻击次数。
提示1-魔法放在最后也可以

20分做法,\(h\le 10\)。

40分做法,\(h\le 100\)。

看全部数据范围,\(t\le 10\)、\(h\le 10^{5}\)。

我先固定物理攻击次数 \(k\),它们的总伤害就是 \(1+2+\cdots+2^{k-1}=2^k-1\)。

魔法可以移到最后,剩余血量就是要检查的数。

提示2-只枚举物理攻击次数

剩余为 0,答案候选是 \(k\)。

剩余是质数,答案候选是 \(k+1\)。

其他情况不能用一次魔法补上。

物理总伤害超过 \(h\) 就停,一共只需检查十几次,直接试除判质数足够。

代码-满分-枚举物理攻击次数

每组时间 \(O(\log h\sqrt{h})\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

const int INF = 1000000000;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int p = 2; p <= x / p; p++) {
        if (x % p == 0) return false;
    }

    return true;
}

int main() {
    int t;

    cin >> t;
    for (int test = 0; test < t; test++) {
        int h;
        cin >> h;
        int ans = INF;
        // 我只枚举物理攻击次数,剩余伤害交给至多一次魔法。
        for (int k = 0, damage = 0; damage <= h; k++) {
            if (damage == h) ans = min(ans, k);
            else if (is_prime(h - damage)) ans = min(ans, k + 1);
            damage += 1 << k;
        }

        if (test > 0) cout << '\n';
        cout << (ans == INF ? -1 : ans);
    }

    return 0;
}

B4051 [GESP202409 五级] 小杨的武器

每场战斗必须选择一件武器承受熟练度变化,求最终最大熟练度的最大值。
提示1-把好处集中起来

20分做法,\(n=1\)。

另外20分做法,\(m=2\)。

看全部数据范围,\(n,m\le 10^{5}\),熟练度和变化量的绝对值\(\le 10^{4}\)。

有至少两件武器时,我把正的变化都给初始熟练度最高的武器,负的变化交给另一件。

提示2-只有一件武器是例外

我会拿 \(n=1\) 检查:每场战斗都必须参加,负数也躲不开,只能把所有变化都加上。

初始熟练度可能全为负数,不能把最大值初始化成 0。

代码-满分-集中正收益

时间 \(O(n+m)\),额外空间 \(O(1)\),覆盖全部子任务。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n, m;

    cin >> n >> m;
    long long best;
    cin >> best;
    for (int i = 2; i <= n; i++) {
        long long x;
        cin >> x;
        best = max(best, x);
    }

    for (int i = 1; i <= m; i++) {
        int x;
        cin >> x;
        // 我有第二件武器时可以转移负收益,只有一件就必须全接下。
        if (n == 1 || x > 0) best += x;
    }

    cout << best;

    return 0;
}

B4070 [GESP202412 五级] 奇妙数字

选择尽量多的不同质数幂,使它们的乘积整除n。
提示1-把不同质数分开考虑

40分做法,\(n\le 1000\)。

看全部数据范围,\(n\le 10^{12}\)。

我先分解 \(n\)。

不同质数互不影响,只要分别看每个质因数的指数够用几次。

提示2-指数先用最小的

同一个质数 \(p\),\(p\)、\(p^{2}\)、\(p^{3}\) 都不同。

想选得最多,我先消耗指数 1,再消耗 2、3……

指数总量只有几十,直接依次扣掉即可。

瓶颈是分解 \(n\),不是这几次加减。

代码-满分-分解与依次扣指数

时间 \(O(\sqrt{n})\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

int take(int count) {
    int ans = 0;
    // 我先用最小的指数,给后面的不同幂留下最多余量。
    for (int exponent = 1; exponent <= count; exponent++) {
        count -= exponent;
        ans++;
    }

    return ans;
}

int main() {
    long long n;

    cin >> n;

    int ans = 0;
    for (long long p = 2; p <= n / p; p++) {
        if (n % p != 0) continue;
        int count = 0;
        while (n % p == 0) {
            n /= p;
            count++;
        }
        ans += take(count);
    }
    if (n > 1) ans++;

    cout << ans;

    return 0;
}

B4071 [GESP202412 五级] 武器强化

花钱改变材料适配的武器,让武器1的材料数严格最多,求最小费用。
提示1-不要只盯着当前最多的一类

20分做法,\(n\le 2\)。

另外20分做法,\(m\le 2\)。

看全部数据范围,\(n,m\le 1000\),费用\(\le 10^{9}\)。

只有两种武器时,改一份材料能让一边加 1、另一边减 1。

但种类多了,处理一类后还要照顾其他类,直接贪心不够稳。

提示2-先假设武器 1 的目标数量

我枚举武器 1 至少达到 \(\operatorname{target}\) 份。

其他类最多只能留 \(\operatorname{target}-1\) 份,多出来的必须转给武器 1。

每类先转最便宜的材料。

如果武器 1 仍不够,再从所有剩余材料里选最便宜的补上。

提示3-为什么只转给武器 1

改给其他武器既不增加武器 1,也可能增加竞争者。

我把同样的材料改给武器 1,不会更差。

枚举所有目标数量,取最小费用。

原本就严格最多时,答案可以是 0。

费用和用 long long。

代码-20分-只有两种武器

时间 \(O(m\log m)\),空间 \(O(m)\),覆盖 \(n\le 2\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int n, m, first = 0;
    vector<int> cost;

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int p, c;
        cin >> p >> c;
        if (p == 1) first++;
        else cost.push_back(c);
    }

    sort(cost.begin(), cost.end());
    int second = cost.size(), need = 0;
    if (n == 2 && second >= first) need = (second - first) / 2 + 1;
    long long ans = 0;
    // 我每转一份,武器1加一份,武器2少一份。
    for (int i = 0; i < need; i++) ans += cost[i];
    cout << ans;

    return 0;
}
代码-满分-枚举目标数量

时间 \(O(m^{2}\log m+mn)\),空间 \(O(n+m)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;
const long long INF = 1LL << 60;

vector<int> material[N];

int main() {
    int n, m;

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int p, cost;
        cin >> p >> cost;
        material[p].push_back(cost);
    }
    for (int i = 1; i <= n; i++) sort(material[i].begin(), material[i].end());

    long long ans = INF;
    for (int target = max(1, (int)material[1].size()); target <= m; target++) {
        int now = material[1].size();
        long long cost = 0;
        vector<int> remaining;
        for (int i = 2; i <= n; i++) {
            int count = material[i].size();
            // 我先把每个竞争者压到target-1,必选的是该类最便宜的材料。
            int need = max(0, count - target + 1);
            now += need;
            for (int j = 0; j < need; j++) cost += material[i][j];
            for (int j = need; j < count; j++) remaining.push_back(material[i][j]);
        }

        if (now < target) {
            sort(remaining.begin(), remaining.end());
            for (int c : remaining) {
                if (now >= target) break;
                cost += c;
                now++;
            }
        }
        ans = min(ans, cost);
    }

    cout << ans;

    return 0;
}

P11960 [GESP202503 五级] 平均分配

把2n件物品各分给两名买家n件,求最大收入。
提示1-小范围先枚举分配

20分做法,\(n\le 8\)。

另外20分做法,出价只有 0、1。

看全部数据范围,\(n\le 10^{5}\),出价\(\le 10^{9}\)。

我先枚举哪些物品给 \(B\),检查恰好选了 \(n\) 件,其余给 \(C\)。

提示2-先全部卖给 C 再改主意

全部卖给 \(C\) 的收入固定。

第 \(i\) 件改卖给 \(B\),收入变化是 \(b_{i}-c_{i}\)。

我只需选最大的 \(n\) 个差值。

差值即使为负也得选够 \(n\) 件,不能只选正数。

代码-20分-枚举分配集合

时间 \(O(n2^{2n})\),空间 \(O(n)\),只适用于 \(n\le 8\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 20;
const long long NEG = -(1LL << 60);

int b[N], c[N];

int main() {
    int n;

    cin >> n;
    for (int i = 0; i < 2 * n; i++) cin >> b[i];
    for (int i = 0; i < 2 * n; i++) cin >> c[i];

    long long ans = NEG;
    for (int mask = 0; mask < (1 << (2 * n)); mask++) {
        int count = 0;
        long long sum = 0;
        for (int i = 0; i < 2 * n; i++) {
            if ((mask >> i) & 1) {
                count++;
                sum += b[i];
            } else sum += c[i];
        }
        // 我只接受两个人各拿n件的分配。
        if (count == n) ans = max(ans, sum);
    }

    cout << ans;

    return 0;
}
代码-满分-按差值排序

时间 \(O(n\log n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 200005;

int b[N];
long long difference[N];

bool cmp(long long x, long long y) {
    return x > y;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n;
    long long ans = 0;

    cin >> n;
    for (int i = 0; i < 2 * n; i++) cin >> b[i];
    for (int i = 0; i < 2 * n; i++) {
        int c;
        cin >> c;
        ans += c;
        difference[i] = 1LL * b[i] - c;
    }

    // 我先全部卖给C,再挑收益变化最大的n件改给B。
    sort(difference, difference + 2 * n, cmp);
    for (int i = 0; i < n; i++) ans += difference[i];
    cout << ans;

    return 0;
}

P11961 [GESP202503 五级] 原根判断

给定质数p,判断a的幂是否第一次在指数p-1时模p等于1。
提示1-先照定义计算

40分做法,\(p\le 1000\)。

看全部数据范围,\(t\le 20\),\(p\le 10^{9}\),\(p\) 为质数,\(1<a<p\)。

我先逐次乘 \(a\) 并取模,记录第一次出现 1 的指数。

它恰好等于 \(p-1\),就符合定义。

提示2-大范围需要一个数学性质

逐次乘到十亿不行。

这里需要补一个性质:\(p\) 为质数时,第一次出现 1 的指数一定整除 \(p-1\)。

所以若提前出现,它就是 \(p-1\) 的一个真因数。

提示3-只检查几个关键指数

我分解 \(p-1\)。

对每个不同质因数 \(q\),检查 \(a^{\frac{p-1}{q}}\equiv 1\pmod p\) 是否成立。

任何真因数都能整除其中某个指数,所以这些检查能排除所有提前出现 1 的情况。

幂用快速幂计算。

代码-40分-按定义枚举指数

每组时间 \(O(p)\),额外空间 \(O(1)\),覆盖 \(p\le 1000\) 的测试点。

#include <bits/stdc++.h>

using namespace std;

int main() {
    int t;

    cin >> t;
    for (int test = 0; test < t; test++) {
        int a, p;
        cin >> a >> p;
        long long value = 1;
        int order = 0;
        // 我逐次计算余数,找到第一次回到1的指数。
        for (int i = 1; i <= p - 1; i++) {
            value = value * a % p;
            if (value == 1) {
                order = i;
                break;
            }
        }

        if (test > 0) cout << '\n';
        cout << (order == p - 1 ? "Yes" : "No");
    }

    return 0;
}
代码-满分-分解 p-1 与快速幂

每组时间 \(O(\sqrt{p}+\omega(p-1)\log p)\),额外空间 \(O(1)\);\(\omega\) 表示不同质因数个数。

#include <bits/stdc++.h>

using namespace std;

long long power(long long a, int exponent, int mod) {
    long long result = 1;
    while (exponent > 0) {
        if (exponent % 2 == 1) result = result * a % mod;
        a = a * a % mod;
        exponent /= 2;
    }

    return result;
}

bool check(int a, int p) {
    int x = p - 1;
    for (int q = 2; q <= x / q; q++) {
        if (x % q != 0) continue;
        // 我只检查不同质因数对应的关键指数,不逐次计算全部幂。
        if (power(a, (p - 1) / q, p) == 1) return false;
        while (x % q == 0) x /= q;
    }
    if (x > 1 && power(a, (p - 1) / x, p) == 1) return false;

    return true;
}

int main() {
    int t;

    cin >> t;
    for (int test = 0; test < t; test++) {
        int a, p;
        cin >> a >> p;
        if (test > 0) cout << '\n';
        cout << (check(a, p) ? "Yes" : "No");
    }

    return 0;
}

P13013 [GESP202506 五级] 奖品兑换

两种兑换方式分别消耗 (a,b)和 (b,a)张券,求最多兑换多少份奖品。
提示1-先固定一种方式的数量

60分做法,\(n,m\le 500\),\(a,b\le 100\)。

看全部数据范围,\(n,m\le 10^{9}\),\(a,b\le 10^{4}\)。

我先枚举第一种换几份,再用剩余券尽量换第二种,就能解决小范围。

提示2-反过来猜总份数

大范围不能逐份枚举。

我猜能否兑换 \(k\) 份:若能换 \(k\) 份,也能换更少,所以可以二分。

让 \(a\ge b\)。

设其中 \(x\) 份用 \((a,b)\),两种券的消耗是 \(kb+x(a-b)\)、\(ka-x(a-b)\)。

提示3-把条件变成 x 的范围

第一种券给出 \(x\) 的上界,第二种券给出下界。

再和 \(0\le x\le k\) 相交,有整数解就可行。

\(a=b\) 时单独算 \(\left\lfloor\frac{\min(n,m)}{a}\right\rfloor\),避免除以 0。

乘法和边界都用 long long。

代码-60分-枚举第一种份数

时间 \(O\left(\min\left(\frac{n}{a},\frac{m}{b}\right)+1\right)\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

int main() {
    long long n, m, a, b;

    cin >> n >> m >> a >> b;

    long long ans = 0;
    // 我先确定第一种换几份,第二种就直接用剩余券算。
    for (long long x = 0; x * a <= n && x * b <= m; x++) {
        long long y = min((n - x * a) / b, (m - x * b) / a);
        ans = max(ans, x + y);
    }

    cout << ans;

    return 0;
}
代码-满分-二分答案

时间 \(O\left(\log\left(\frac{n+m}{a+b}+1\right)\right)\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

long long n, m, a, b;

bool check(long long k) {
    if (k * b > n || k * b > m) return false;

    long long difference = a - b;
    long long l = 0;
    if (k * a > m) l = (k * a - m + difference - 1) / difference;
    long long r = min(k, (n - k * b) / difference);

    return l <= r;
}

int main() {
    cin >> n >> m >> a >> b;

    long long ans = 0;
    if (a == b) ans = min(n, m) / a;
    else {
        if (a < b) swap(a, b);
        long long l = 0, r = (n + m) / (a + b);
        // 我检查总份数是否可行,而不实际安排每一份。
        while (l <= r) {
            long long mid = (l + r) / 2;
            if (check(mid)) {
                ans = mid;
                l = mid + 1;
            } else r = mid - 1;
        }
    }

    cout << ans;

    return 0;
}

P13014 [GESP202506 五级] 最大公因数

第i次询问把所有数同时加i,求它们的最大公因数。
提示1-先按询问直接求

60分做法,\(n\le 1000\)、\(q\le 10\)。

看全部数据范围,\(n,q\le 10^{5}\),\(a_{i}\le 1000\)。

我先每次逐个合并 \(\gcd\),拿到小范围的答案,再观察所有数同时加 \(i\) 后什么没有变。

提示2-两个数的差没有变

我用 \(\gcd(x,y)=\gcd(x,y-x)\)。

全部加 \(i\) 后,\(a_{j}-a_{1}\) 仍然不变。

先求所有这些差的 \(\gcd\),记作 \(d\)。

每次只需输出 \(\gcd(a_{1}+i,d)\)。

提示3-全部相等也能自然处理

我会试 \(n=1\) 或所有数相等:这时 \(d=0\),\(\gcd(x,0)=x\),仍然正确。

不需要给这些情况另写一套算法。

代码-60分-逐次合并 gcd

时间 \(O(nq\log A)\),空间 \(O(n)\),\(A\) 为询问中数值上界。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int a[N];

int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }

    return a;
}

int main() {
    int n, q;

    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];

    for (int i = 1; i <= q; i++) {
        int ans = 0;
        // 我先按定义逐个合并,作为小范围解法。
        for (int j = 1; j <= n; j++) ans = gcd(ans, a[j] + i);
        if (i > 1) cout << '\n';
        cout << ans;
    }

    return 0;
}
代码-满分-预处理差值 gcd

时间 \(O((n+q)\log A)\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }

    return a;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, q, first, difference = 0;

    cin >> n >> q >> first;
    for (int i = 2; i <= n; i++) {
        int x;
        cin >> x;
        // 我只保留同时加数后仍不变的差值信息。
        difference = gcd(difference, abs(x - first));
    }

    for (int i = 1; i <= q; i++) {
        if (i > 1) cout << '\n';
        cout << gcd(first + i, difference);
    }

    return 0;
}

P14073 [GESP202509 五级] 数字选取

从1到n选尽量多的数,要求任意两个数互质。
提示1-先找到一个合法方案

40分做法,\(n\le 1000\)。

看全部数据范围,\(n\le 10^{5}\)。

我可以选 1 和所有质数,它们两两互质。

接下来要证明不可能选得更多。

提示2-每个数都要占一个质因数

每个大于 1 的数至少有一个质因数。

两两互质,就不能共用质因数。

所以它们的数量最多等于质数个数,再加上 1。

用筛法统计质数即可,\(n=1\) 时答案就是 1。

代码-满分-筛法统计质数

时间 \(O(n\log\log n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

bool composite[N];

int main() {
    int n;

    cin >> n;

    // 我先找出所有质数,它们加上1就达到数量上界。
    for (int p = 2; p <= n / p; p++) {
        if (composite[p]) continue;
        for (int j = p * p; j <= n; j += p) composite[j] = true;
    }
    int ans = 1;
    for (int i = 2; i <= n; i++) {
        if (!composite[i]) ans++;
    }

    cout << ans;

    return 0;
}

P14074 [GESP202509 五级] 有趣的数字和

求 [l,r]中二进制含奇数个1的整数之和。
提示1-先逐个数,找规律

40分做法,\(r\le 10^{4}\)。

另外30分做法,\(l=1\)、\(r=2^{k}-1\),\(k>1\)。

看全部数据范围,\(r\le 10^{9}\)。

我先逐个统计二进制的 1,再按四个一组观察:\(4x\)、\(4x+1\)、\(4x+2\)、\(4x+3\)。

提示2-每组的贡献固定

四个数的高位都一样,低两位分别是 00、01、10、11。

奇数个 1 的两个数,要么是两端,要么是中间两个,它们的和总是 \(8x+3\)。

提示3-整组直接求和,零头再检查

我求 0 到 \(n\) 的前缀答案。

完整组数为 \(k=\left\lfloor\frac{n+1}{4}\right\rfloor\),贡献是 \(k(4k-1)\),末尾至多剩三个数。

区间答案就是 \(f(r)-f(l-1)\)。

不能仍然逐组循环,十亿范围要用公式。

计算用 long long。

代码-40分-逐个检查二进制

时间 \(O((r-l+1)\log r)\),额外空间 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

bool interesting(long long x) {
    int parity = 0;
    while (x > 0) {
        parity ^= x % 2;
        x /= 2;
    }

    return parity == 1;
}

int main() {
    long long l, r, ans = 0;

    cin >> l >> r;
    // 我先直接统计,方便验证后面找到的分组规律。
    for (long long i = l; i <= r; i++) {
        if (interesting(i)) ans += i;
    }

    cout << ans;

    return 0;
}
代码-满分-四个一组求前缀和

时间 \(O(\log r)\),额外空间 \(O(1)\)。每次前缀计算只检查至多三个零头。

#include <bits/stdc++.h>

using namespace std;

bool interesting(long long x) {
    int parity = 0;
    while (x > 0) {
        parity ^= x % 2;
        x /= 2;
    }

    return parity == 1;
}

long long prefix(long long n) {
    long long groups = (n + 1) / 4;
    // 我把所有完整组用等差数列公式一次加完,不再逐组枚举。
    long long ans = groups * (4 * groups - 1);
    for (long long i = groups * 4; i <= n; i++) {
        if (interesting(i)) ans += i;
    }

    return ans;
}

int main() {
    long long l, r;

    cin >> l >> r;
    cout << prefix(r) - prefix(l - 1);

    return 0;
}

P14917 [GESP202512 五级] 数字移动

可以移动数值不超过x的元素,让每对相同数字相邻,求最小x。
提示1-为什么想到枚举答案

40分做法,\(n,a_i\le 100\)。

看全部数据范围,\(n,a_i\le 10^5\),每个值恰好出现两次。

我先想直接安排移动,但先动哪个、放在哪里,选择太多。

题目只问最小的 \(x\),我就换个问法:如果 \(x\) 已经确定,能不能完成?

小范围的数值最多只有 \(100\),我可以从 \(x=0\) 开始逐个试。

第一个能完成的值,就是最小答案,不用枚举每一步怎么移动。

提示2-固定答案后,哪些数不能动

大于 \(x\) 的数不能移动,它们之间的相对顺序也不能改变。

我把不超过 \(x\) 的数暂时拿掉,只检查留下的数能否按顺序两两配对。

例如序列 \(1,2,1,2\):\(x=0\) 时都不能动,无法配对。

\(x=1\) 时拿掉两个 \(1\),剩下 \(2,2\),已经成对。

再把两个 \(1\) 搬到一起即可。

提示3-40分怎样枚举并判断

我依次检查 \(x=0,1,2,\ldots,A\),其中 \(A\) 是最大元素。

检查时跳过能移动的数,把剩下的数每两个分成一组。

有一组不同,这个 \(x\) 就不行。

如果每组都相同,就能把可移动的数也按对放到序列两端,完成排列。

\(x=A\) 时所有数都能动,一定可行,所以不会漏掉答案。

每个 \(x\) 扫描一遍序列,总时间是 \(O(nA)\)。

这就是40分枚举答案的做法。

提示4-怎样从枚举改成二分

\(x\) 越大,可以移动的数越多。

原来能完成的移动,现在仍能完成,所以可行性不会从“能”变回“不能”。

我用二分寻找第一个可行的 \(x\),不用再逐个试,时间降为 \(O(n\log A)\)。

注意操作是移动一个元素,不是交换两个元素。

代码-40分-枚举体力上限

时间 \(O(nA)\),空间 \(O(n)\),\(A\) 为最大元素;只适用于 \(n,A\le 100\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int a[N], n;

bool check(int limit) {
    int last = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] <= limit) continue;
        if (last == 0) last = a[i];
        else {
            if (last != a[i]) return false;
            last = 0;
        }
    }

    return last == 0;
}

int main() {
    int largest = 0;

    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        largest = max(largest, a[i]);
    }

    int ans = 0;
    // 我先从小到大试上限,找到第一个能让固定数字成对的值。
    while (ans <= largest && !check(ans)) ans++;
    cout << ans;

    return 0;
}
代码-满分-二分体力上限

时间 \(O(n\log A)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int a[N], n;

bool check(int limit) {
    int last = 0;
    // 我只比较不能移动的数字,保留它们原来的相对顺序。
    for (int i = 0; i < n; i++) {
        if (a[i] <= limit) continue;
        if (last == 0) last = a[i];
        else {
            if (last != a[i]) return false;
            last = 0;
        }
    }

    return last == 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int largest = 0;

    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        largest = max(largest, a[i]);
    }

    int l = 0, r = largest;
    while (l < r) {
        int mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }

    cout << l;

    return 0;
}

P14918 [GESP202512 五级] 相等序列

每次乘或除一个质数花一金币,求让所有整数相等的最小费用。
提示1-把乘除换成指数加减

60分做法,\(n,a_{i}\le 100\)。

看全部数据范围,\(n,a_{i}\le 10^{5}\)。

我先分解质因数。

一次乘或除 \(p\),只会让 \(p\) 的指数加 1 或减 1,不影响其他质数。

提示2-每个质数单独找中位数

所有数最终相等,就要让每种质因数的指数一致。

调整次数是到目标指数的距离和,中位数能让这个和最小。

没有这个质因数的数,它的指数是 0,不能漏掉。

提示3-零很多,不用真的补进数组

我只存出现过的正指数。

排序后,前面还有 \(n\) 减去记录数个 0,照这个数量定位中位数即可。

这样不会为每个质数都开一个长度 \(n\) 的数组。

先筛最小质因数,再分解每个输入数。

代码-60分-完整记录小范围指数

时间 \(O(A\sqrt{A}+An\log n)\),空间 \(O(n)\),仅适用于 \(n,A\le 100\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 105;

int a[N], exponent[N];

bool is_prime(int x) {
    for (int p = 2; p <= x / p; p++) {
        if (x % p == 0) return false;
    }

    return x >= 2;
}

int main() {
    int n, largest = 1;

    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        largest = max(largest, a[i]);
    }

    long long ans = 0;
    for (int p = 2; p <= largest; p++) {
        if (!is_prime(p)) continue;
        for (int i = 0; i < n; i++) {
            int x = a[i];
            exponent[i] = 0;
            while (x % p == 0) {
                x /= p;
                exponent[i]++;
            }
        }
        sort(exponent, exponent + n);
        // 我把没有这个质因数的0也放进排序,再取中位数。
        int middle = exponent[n / 2];
        for (int i = 0; i < n; i++) ans += abs(exponent[i] - middle);
    }

    cout << ans;

    return 0;
}
代码-满分-筛分解与稀疏指数

设 \(A\) 为最大值、\(K\) 为非零指数记录总数,时间 \(O(A\log\log A+n\log A+K\log n)\),空间 \(O(A+n+K)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int a[N], smallest[N];
vector<int> exponent[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, largest = 1;

    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        largest = max(largest, a[i]);
    }

    for (int p = 2; p <= largest; p++) {
        if (smallest[p] != 0) continue;
        smallest[p] = p;
        if (p > largest / p) continue;
        for (int j = p * p; j <= largest; j += p) {
            if (smallest[j] == 0) smallest[j] = p;
        }
    }
    for (int i = 0; i < n; i++) {
        int x = a[i];
        while (x > 1) {
            int p = smallest[x], count = 0;
            while (x % p == 0) {
                x /= p;
                count++;
            }
            exponent[p].push_back(count);
        }
    }

    long long ans = 0;
    for (int p = 2; p <= largest; p++) {
        if (exponent[p].empty()) continue;
        sort(exponent[p].begin(), exponent[p].end());
        int zeros = n - exponent[p].size(), middle = 0;
        // 我不实际补零,只把它们的数量算进中位数的位置。
        if (n / 2 >= zeros) middle = exponent[p][n / 2 - zeros];
        ans += 1LL * zeros * middle;
        for (int x : exponent[p]) ans += abs(x - middle);
    }

    cout << ans;

    return 0;
}

P15798 [GESP202603 五级] 有限不循环小数

统计区间 [L,R]中,有多少个整数a的倒数是有限小数。
提示1-从小数的分母想起

看全部数据范围,\(1\le L\le R\le 10^{6}\)。

我先想有限小数是怎样写成分数的:分母可以是 10、100、1000……它们只有 2 和 5 两种质因数。

提示2-把 2 和 5 除干净

所以我检查 \(a\):不断除掉因数 2,再不断除掉因数 5。

最后剩下 1,倒数就能化为有限小数。

区间最多一百万个数,直接逐个检查就够了。

\(a=1\) 也要计数。

代码-满分-逐个检查

时间 \(O((R-L+1)\log R)\),额外空间 \(O(1)\)。原题未列出部分分子任务。

#include <bits/stdc++.h>

using namespace std;

bool check(int x) {
    // 我只留下不能被十进制分母消掉的因数。
    while (x % 2 == 0) x /= 2;
    while (x % 5 == 0) x /= 5;

    return x == 1;
}

int main() {
    int l, r;

    cin >> l >> r;

    int ans = 0;
    for (int i = l; i <= r; i++) {
        if (check(i)) ans++;
    }

    cout << ans;

    return 0;
}

P15799 [GESP202603 五级] 找数

两个数组各自没有重复元素,求同时出现在两个数组中的数的数量。
提示1-先逐对比较

40分做法,\(n,m\le 1000\)。

看全部数据范围,\(n,m\le 10^{5}\),元素\(\le 10^{9}\)。

我先枚举 \(A\)、\(B\) 的每一对数,相等就计数。

数组内部没有重复,不需要额外去重。

提示2-排序后只往前走

两层循环太慢,我把两个数组都排好序,再比较当前的两个数。

较小的数不可能匹配另一个数组后面的数,就让它往前走。

相等时计数,两边都往前走。

代码-40分-两层循环

时间 \(O(nm)\),空间 \(O(n+m)\)。仅适用于 \(n,m\le 1000\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int a[N], b[N];

int main() {
    int n, m;

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) cin >> b[i];

    int ans = 0;
    // 我先直接检查每一对数,作为小范围的解法。
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i] == b[j]) ans++;
        }
    }

    cout << ans;

    return 0;
}
代码-满分-排序与双指针

时间 \(O(n\log n+m\log m)\),空间 \(O(n+m)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 100005;

int a[N], b[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int n, m;

    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) cin >> b[i];
    sort(a + 1, a + n + 1);
    sort(b + 1, b + m + 1);

    int i = 1, j = 1, ans = 0;
    while (i <= n && j <= m) {
        // 我让较小的数往前走,寻找可能相等的数。
        if (a[i] < b[j]) i++;
        else if (a[i] > b[j]) j++;
        else {
            ans++;
            i++;
            j++;
        }
    }

    cout << ans;

    return 0;
}

P17010 [GESP202606 五级] 排排坐

调整座位顺序,让所有前缀和的总和最大。
提示1-一个数会被算几次

看全部数据范围,\(n\le 1000\),\(a_{i}\le 1000\)。

我把每个人的糖果数展开,发现第一位的数被算 \(n\) 次,第二位被算 \(n-1\) 次,最后一位只算一次。

提示2-大的数应该坐哪里

既然越靠左被算得越多,我就把大的数放前面。

交换一对前小后大的数,总量只会增加。

降序排序,再累加每个前缀和。

用 long long 保存总量。

代码-满分-降序排序

时间 \(O(n\log n)\),空间 \(O(n)\)。原题未列出部分分子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int a[N];

bool cmp(int x, int y) {
    return x > y;
}

int main() {
    int n;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    // 我把大的数放在前面,让它被更多个前缀重复计算。
    sort(a + 1, a + n + 1, cmp);
    long long sum = 0, ans = 0;
    for (int i = 1; i <= n; i++) {
        sum += a[i];
        ans += sum;
    }

    cout << ans;

    return 0;
}

P17011 [GESP202606 五级] 晚宴

恰好选择两道美味度互质的菜,求美味度之和的最大值。
提示1-先看枚举够不够

看全部数据范围,\(n\le 1000\),美味度\(\le 10^{6}\),互不相同,保证存在合法方案。

我先算一下规模:两两组合不到五十万对,可以直接检查,不必按美味度开大数组。

提示2-互质就是最大公约数为 1

我枚举 \(i<j\),用辗转相除法求 \(\gcd\)。

只有 \(\gcd=1\) 时,才用两数之和更新答案。

不能只取最大的两个数,它们可能不互质。

也不能把一道菜选两次。

代码-满分-枚举菜肴对

时间 \(O(n^{2}\log V)\),空间 \(O(n)\),\(V\) 为最大美味度。原题未列出部分分子任务。

#include <bits/stdc++.h>

using namespace std;

const int N = 1005;

int a[N];

int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }

    return a;
}

int main() {
    int n;

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        // 我只向后找另一道菜,避免重复枚举和选择同一道菜。
        for (int j = i + 1; j <= n; j++) {
            if (gcd(a[i], a[j]) == 1) ans = max(ans, a[i] + a[j]);
        }
    }

    cout << ans;

    return 0;
}

P17455 [GESP202609 五级] 哥德巴赫猜想

统计把偶数n写成两个质数之和的方法数,交换顺序不算新方案。
提示1-只枚举较小的那个

40分做法,\(n\le 100\)。

看全部数据范围,\(n\le 10^{6}\)。

我先枚举 \(p\),另一个数就是 \(n-p\)。

为了不重复计数,只让 \(p\le n-p\),也就是 \(p\le \frac{n}{2}\)。

提示2-重复判质数不划算

小范围可以每次试除。

大范围要检查很多数,我先用筛法把 \(n\) 以内的合数标出来,之后每次判断只要一次查表。

\(2+2\) 也是合法方案,1 则不是质数。

代码-40分-逐次试除

时间 \(O(n\sqrt{n})\),额外空间 \(O(1)\)。覆盖 \(n\le 100\) 的子任务。

#include <bits/stdc++.h>

using namespace std;

bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i++) {
        if (x % i == 0) return false;
    }

    return true;
}

int main() {
    int n;

    cin >> n;

    int ans = 0;
    // 我只枚举较小的质数,交换顺序就不会重复算。
    for (int p = 2; p <= n / 2; p++) {
        if (is_prime(p) && is_prime(n - p)) ans++;
    }

    cout << ans;

    return 0;
}
代码-满分-筛法

时间 \(O(n\log\log n)\),空间 \(O(n)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 1000005;

bool composite[N];

int main() {
    int n;

    cin >> n;

    composite[0] = composite[1] = true;
    // 我先统一标记合数,避免枚举分解时反复试除。
    for (int p = 2; p <= n / p; p++) {
        if (composite[p]) continue;
        for (int j = p * p; j <= n; j += p) composite[j] = true;
    }

    int ans = 0;
    for (int p = 2; p <= n / 2; p++) {
        if (!composite[p] && !composite[n - p]) ans++;
    }

    cout << ans;

    return 0;
}

P17456 [GESP202609 五级] 饮品调制

每种原料有一定体积和甜度,可以取小数升。
求混合后甜度恰好为t时,能得到的最大体积,保留三位小数。
提示1-先弄明白怎样配出目标甜度

40分做法,\(n=2\)。

看全部数据范围,\(n\le2000\),每种原料最多 \(100\) 升,原料甜度和目标甜度都在 \(0\) 到 \(200\) 之间。

甜度就是每升有多少克糖。我先拿目标甜度 \(10\) 举例。

一升甜度 \(14\) 的原料,比要求多 \(4\) 克糖;一升甜度 \(8\) 的原料,比要求少 \(2\) 克糖。

所以一升前者配两升后者,多出的 \(4\) 克恰好补上少的 \(4\) 克,混合后甜度就是 \(10\)。

提示2-40分:两种原料分情况考虑

有原料甜度恰好为目标值,就能直接使用。另一种若不等于目标值,混进去只会把甜度带偏。

两种都比目标甜,或者都比目标淡,就配不出来,答案为 \(0\)。

一种偏甜、一种偏淡时,我让多出的糖和少的糖恰好抵消,直到其中一种用完。

例如甜度 \(14\) 的有 \(2\) 升,甜度 \(8\) 的有 \(3\) 升,目标为 \(10\)。偏淡原料只能补上 \(6\) 克糖差,所以用 \(1.5\) 升偏甜原料和 \(3\) 升偏淡原料,共 \(4.5\) 升。

代码中的sugar表示两边能够抵消的糖差,取两边可提供糖差的较小值,再分别除以每升的偏差,得到两种原料的使用体积。

提示3-原料多了,先假设全部用上

我想让体积尽量大,不妨先把全部原料算进去,再看需要少用多少。

对每种原料,记 \(d_i=s_i-t\)。它的糖差为 \(v_i d_i\),所有原料的总糖差为:

\[ D=\sum_{i=1}^{n}v_i(s_i-t) \]

\(D=0\):已经正好,全部保留。

\(D>0\):整体偏甜,要少用一些偏甜原料。少用偏淡原料反而会更甜,不划算。

\(D<0\):整体偏淡,道理相同,少用一些偏淡原料。

甜度恰好为目标值的原料没有糖差,始终全部保留。

提示4-为什么先少用甜度偏差最大的

目标仍为 \(10\),假设总共多了 \(4\) 克糖差。

少用甜度 \(18\) 的原料,每少用一升,糖少了 \(18\) 克,但目标糖量也少了 \(10\) 克,所以糖差只减少 \(8\) 克。

少用甜度 \(12\) 的原料,每少用一升,糖差只减少 \(2\) 克。

因而前者只需少用 \(0.5\) 升,后者却要少用 \(2\) 升。我想保留更多体积,就应先少用偏差更大的原料。

一种原料全去掉仍不够,就继续下一种;够了就只去掉需要的小数升,立刻停止。

提示5-代码怎样统一处理偏甜和偏淡

偏甜时,正的 \(d_i\) 是需要删减的那一侧,数值越大,每少用一升越有效。

偏淡时,我把 \(D\) 和所有 \(d_i\) 都取相反数,也变成同样的处理:按 \(d_i\) 从大到小,只考虑正数。

若这份原料的糖差不超过剩余要消去的糖差,就整份去掉;否则只去掉 \(D/d_i\) 升。

我用选择排序:每轮在剩余原料中找到最大的 \(d_i\),交换到当前位置。\(n\le2000\),约两百万次比较就够了。

最后一次除法用小数计算。代码只用数组、结构体和普通循环,支持C++14及更早的C++11标准。

代码-40分-两种原料配平

只适用于 \(n=2\)。时间、空间均为 \(O(1)\)。

#include <bits/stdc++.h>

using namespace std;

const int N = 5;

int v[N], s[N];

int main() {
    int n, t;
    cin >> n >> t;
    for (int i = 0; i < n; i++) cin >> v[i] >> s[i];

    int d1 = s[0] - t, d2 = s[1] - t;
    double ans = 0;
    if (d1 == 0 || d2 == 0) {
        if (d1 == 0) ans += v[0];
        if (d2 == 0) ans += v[1];
    } else if (d1 * d2 < 0) {
        // 我先找两边最多能抵消多少糖差,再换算各自需要多少升。
        int sugar = min(v[0] * abs(d1), v[1] * abs(d2));
        ans = 1.0 * sugar / abs(d1) + 1.0 * sugar / abs(d2);
    }

    cout << fixed << setprecision(3) << ans;

    return 0;
}
代码-满分-按每升糖差贪心删减

时间 \(O(n^2)\),空间 \(O(n)\)。选择排序可处理全部范围;总糖差保留为整数,最后一次部分删减才做小数除法。

#include <bits/stdc++.h>

using namespace std;

const int N = 2005;

struct Drink {
    int v, d;
} a[N];

int main() {
    int n, t;
    cin >> n >> t;

    long long total = 0;
    double ans = 0;
    for (int i = 0; i < n; i++) {
        int s;
        cin >> a[i].v >> s;
        a[i].d = s - t;
        ans += a[i].v;
        total += 1LL * a[i].v * a[i].d;
    }

    // 整体偏淡时,我把符号反过来,仍然只删减正偏差的原料。
    if (total < 0) {
        total = -total;
        for (int i = 0; i < n; i++) a[i].d = -a[i].d;
    }

    // 我每轮找出剩余原料中偏差最大的,把整份记录交换过来。
    for (int i = 0; i < n - 1; i++) {
        int k = i;
        for (int j = i + 1; j < n; j++) {
            if (a[j].d > a[k].d) k = j;
        }

        Drink temp = a[i];
        a[i] = a[k];
        a[k] = temp;
    }

    // 我先少用每升能消去最多糖差的原料,尽量保留体积。
    for (int i = 0; i < n; i++) {
        if (total == 0 || a[i].d <= 0) break;

        long long sugar = 1LL * a[i].v * a[i].d;
        if (sugar <= total) {
            ans -= a[i].v;
            total -= sugar;
        } else {
            // 这一份只需要去掉一部分,必须用小数除法。
            ans -= 1.0 * total / a[i].d;
            total = 0;
        }
    }

    cout << fixed << setprecision(3) << ans;

    return 0;
}