一只怪兽的两种身份互不干扰:它至多发起一次攻击,也只可能被消灭一次。于是「安排攻击顺序」这件事可以看成在怪兽之间做一次配对:每一对里,攻击力大的那只消灭攻击力小的那只。
游戏结束时未退出的怪兽数量至少是
证明:每次成功消灭都消耗掉一只攻击者(它此后再也不能攻击)与一只被消灭者,两者都是原本的怪兽;同一只怪兽重复使用不可能,所以全部成功消灭对应一组配对。反过来,给定一组配对,让每对中攻击力较大的那只去攻击较小的那只即可实现这组配对,中间不会互相干扰。
把怪兽按攻击力升序排列:
用两个指针:
证明:设某个最优解第一次与贪心不一致时,贪心用
1#include <bits/stdc++.h>
2using namespace std;
3
4int main() {
5 int n;
6 cin >> n;
7 vector<int> a(n);
8 for (int i = 0; i < n; i++)
9 cin >> a[i];
10 sort(a.begin(), a.end());
11 for (int i = 0, j = 1; i < n; i++) {
12 while (j < n && a[j] == a[i])
13 j++;
14 if (j == n) {
15 cout << n - i << '\n';
16 break;
17 }
18 // j打死i
19 j++;
20 }
21}
while (j < n && a[j] == a[i]) j++; 这一句是把「严格大于」落实成代码的关键:攻击力相同的怪兽互相打不动,所以 break 结束:一旦 | 测试点 | 涉及知识点 | |
|---|---|---|
| 1—4 | 10 | 枚举、DFS 与 BFS |
| 5—10 | 20 | DFS 与 BFS |
| 11—15 | 300 | 搜索的剪枝优化 |
| 16—20 | 贪心法、排序 |
官方报告的口径是:前 15 个测试点可以用搜索与剪枝拿到
知识点:枚举、模拟
难度:普及−
小 Y 有一把五个拨圈的密码锁,每个拨圈上是从
她的锁车方式是从正确密码开始随机转动仅一次:每次以某个幅度仅转动一个拨圈,或者同时转动两个相邻的拨圈,此时两个拨圈转动的幅度相同——可以把
小 Y 记下了锁车后密码锁的
设
若
证明:一次转动只做两件事之一,都是在若干位置上把数字加上同一个幅度
反过来从
要求的是「对每个给出的状态
利用引理 1 把方向倒过来:
对每个给出的状态
一个状态
给出的状态都不是正确密码这一条不需要单独处理:从
每个给出状态
每个状态共
状态只有五位十进制数,直接编码成一个整数:
用一个长度为
1#include <bits/stdc++.h>
2using namespace std;
3int cnt[100000];
4int s[5], t[5];
5void count() {
6 // {0, 0, 1, 1, 5} => 115
7 int x = 0;
8 for (int i = 0; i < 5; i++)
9 x = x * 10 + t[i];
10 cnt[x]++;
11}
12
13int main() {
14 int n;
15 cin >> n;
16 for (int _ = 0; _ < n; _++) {
17 for (int i = 0; i < 5; i++)
18 cin >> s[i];
19 for (int j = 1; j < 10; j++) { //转动幅度
20 // 转一个轮
21 for (int i = 0; i < 5; i++) {
22 for (int k = 0; k < 5; k++) {
23 if (k == i)
24 t[k] = (s[k] + j) % 10;
25 else
26 t[k] = s[k];
27 }
28 count();
29 }
30 //转两个相邻的轮
31 for (int i = 0; i < 4; i++) {
32 for (int k = 0; k < 5; k++) {
33 if (k == i || k == i + 1)
34 t[k] = (s[k] + j) % 10;
35 else
36 t[k] = s[k];
37 }
38 count();
39 }
40 }
41 }
42
43 int ans = 0;
44 for (int i = 0; i < 100000; i++)
45 if (cnt[i] == n)
46 ans++;
47 cout << ans << '\n';
48
49 return 0;
50}
x = x * 10 + t[i] 这一句保留前导零,例如 0 0 1 1 5 编成 j = 1..9,不需要 j = 0(那是没转动),也不需要 知识点:贪心、排序
难度:普及/提高−
算法协会招收了
如果第
小 L 不希望某个部门人数过多:方案中不能有部门被分配多于
先不考虑人数限制:让每个人都选自己最满意的部门,得到的满意度之和记为
问题只剩下:为了满足人数限制,最少要损失多少满意度。
在「每人选最优」的方案里,至多只有一个部门的人数超过
证明:若有两个部门的人数都超过
于是只需处理唯一超标的那个部门。
设超标部门的人数为
恰好调出
证明:超标部门之外的总人数是
一个人从最优部门改到次优部门,满意度会下降
称为这个人的损失。因为超标部门之外的人数不会超标,所以每个人只需要考虑去次优部门,不必考虑第三选择。
答案为
证明:任何合法方案都必须把超标部门的至少
把每个人的三个满意度排序,取最小的
1#include <bits/stdc++.h>
2using namespace std;
3
4const int maxn = 1e5 + 5;
5struct S {
6 int myd;
7 int id;
8};
9S a[maxn][3];
10
11bool cmp(S x, S y) {
12 return x.myd > y.myd;
13}
14
15void solve() {
16 int n;
17 cin >> n;
18 vector<int> cnt(3);
19 int ans = 0;
20 for (int i = 0; i < n; i++) {
21 for (int j = 0; j < 3; j++) {
22 cin >> a[i][j].myd;
23 a[i][j].id = j;
24 }
25 sort(a[i], a[i] + 3, cmp);
26 cnt[a[i][0].id]++;
27 ans += a[i][0].myd;
28 }
29 for (int i = 0; i < 3; i++)
30 if (cnt[i] > n / 2) {
31 vector<int> loss;
32 for (int j = 0; j < n; j++) {
33 if (a[j][0].id == i) {
34 loss.push_back(a[j][0].myd - a[j][1].myd);
35 }
36 }
37 sort(loss.begin(), loss.end());
38 ans -= accumulate(loss.begin(), loss.begin() + cnt[i] - n / 2, 0);
39 }
40 cout << ans << '\n';
41}
42
43int main() {
44 int T;
45 cin >> T;
46 while (T--) {
47 solve();
48 }
49}
S{myd, id} 保存并排序:a[i][0] 是最优部门,a[i][1] 是次优部门,损失就是两者之差;存 id 是为了知道最优部门是哪一个。a 与排序都开在全局 / 函数外层,每组数据都会重新赋值,不需要额外清空。int 就够(long long 更稳妥。accumulate(loss.begin(), loss.begin() + cnt[i] - n / 2, 0) 取的是排序后最小的 loss.size(),因为超标部门里的人数就是 | 测试点 | 特殊性质 | |
|---|---|---|
| 1—4 | 2 至 10 | 无 |
| 5—8 | 30 | 无 |
| 9—11 | 200 | 9 为 B |
| 12 | A | |
| 13—14 | B | |
| 15—16 | C | |
| 17—20 | 无 |
特殊性质
怎么拿分
知识点:贪心、有序表、前缀和
难度:普及+/提高
机场分为国内区与国际区:国内航班只能停靠国内区的廊桥,国际航班只能停靠国际区的廊桥;没有空闲廊桥时飞机停在远机位。
L 市新建的机场一共有
给定未来一段时间两区飞机的抵达与离开时刻,请把
分配方案有
国内区拿到
证明:国内航班只停国内区,国际航班只停国际区,两区的飞机互不竞争,模拟过程完全独立。给定国内区
于是只需要两个数组
把「先到先得」按廊桥拆开看:第
设
证明思路(对
把所有航班存进一张以抵达时刻为键、离开时刻为值的 map。由于题目保证只有一条跑道,全机场不存在同时抵达的飞机,所以抵达时刻互不相同,可以直接作键。
对第 t.upper_bound(x) 取「抵达时刻严格大于
cnt[i]++),把 map 里删掉它。最后对 cnt 求前缀和,得到
总代价 map 操作加上
1#include <bits/stdc++.h>
2using namespace std;
3
4int n;
5
6vector<int> solve(int m) {
7 map<int, int> t;
8 for (int i = 0; i < m; i++) {
9 int a, b;
10 cin >> a >> b;
11 t.insert({a, b});
12 }
13 vector<int> cnt(n + 1);// cnt[i]:停在第i个廊桥的飞机的数量
14 for (int i = 1; i <= n; i++) {
15 int x = 0;// 当前时刻
16 while (1) {
17 auto it = t.upper_bound(x);
18 if (it == t.end())
19 break;
20 cnt[i]++;
21 x = it->second;
22 t.erase(it);
23 }
24 }
25 for (int i = 1; i <= n; i++)
26 cnt[i] += cnt[i - 1];
27 return cnt;
28}
29
30int main() {
31 int m1, m2;
32 cin >> n >> m1 >> m2;
33 vector<int> c1 = solve(m1);
34 vector<int> c2 = solve(m2);
35
36 int ans = 0;
37 for (int i = 0; i <= n; i++)
38 ans = max(ans, c1[i] + c2[n - i]);
39 cout << ans << '\n';
40}
upper_bound(x) 是「抵达时刻严格大于 map 以抵达时刻为键,这一点依赖题目「只有一条跑道、不存在同时抵达」的保证;若数据里出现相同抵达时刻,insert 会丢掉后一架飞机。solve 直接在函数里读入:两区各调用一次,各自读自己的 知识点:模拟、二分定位、日期计算
难度:普及+/提高
天文学家们使用儒略日来表达时间。儒略日定义为从公元前
输出时,公元后的年份写成 日 月 年,公元前年份写成 日 月 年 BC。
注意公元零年并不存在,即公元前
用天文纪年表示年份:公元前
其中
于是
要求的是第
在
单个询问的二分代价是
算出年份之后,从
日 月 年,abs(y*) 年 BC。单个询问的总代价是
1int n_run(int y) { // y 年之前(不含 y 年)的闰年个数
2 if (y <= 1600) {
3 return (4713 + y + (y < 0) + 3) / 4;
4 }
5 y -= 1600;
6 return n_run(1600) + y / 4 - y / 100 + y / 400;
7}
8
9bool is_run(int y) {
10 if (y < 0)
11 return -y % 4 == 1;
12 if (y <= 1582)
13 return y % 4 == 0;
14 return y % 400 == 0 or (y % 4 == 0 and y % 100 != 0);
15}
16
17int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
18
19long long n_day(int y) { // 儒略日 0 到 y 年 1 月 1 日的天数
20 long long ans = (y + 4713 + (y < 0)) * 365LL + n_run(y);
21 if (y >= 1582)
22 ans -= 10;
23 return ans;
24}
25void solve(long long x) {
26 int l = -4713, r = 1000000000;
27 // 最小的 y 使 n_day(y) >= x + 1
28 while (l <= r) {
29 int y = (l + r) / 2;
30 if (y == 0)
31 y = 1;
32 if (n_day(y) >= x + 1) {
33 r = y - 1;
34 if (r == 0)
35 r = -1;
36 } else {
37 l = y + 1;
38 if (l == 0)
39 l = 1;
40 }
41 }
42 int y = l;
43 int py = y == 1 ? -1 : y - 1;
44 long long c = n_day(py);
45 for (int m = 1; m <= 12; m++) {
46 int t = days[m];
47 t += m == 2 and is_run(y);
48 for (int d = 1; d <= t; ++d) {
49 if (y == 1582 and m == 10 and d >= 5 and d < 15)
50 continue; // 1582-10-05 至 10-14 不存在
51 if (++c == x + 1) {
52 cout << d << ' ' << m << ' ' << abs(y) << (y < 0 ? " BC\n" : "\n");
53 return;
54 }
55 }
56 }
57}
58
59int main() {
60 int n;
61 cin >> n;
62 for (int i = 0; i < n; ++i) {
63 long long x;
64 cin >> x;
65 solve(x);
66 }
67}
二分加年内逐日(本页代码)
周期定位
代码/02_julian.cpp两份实现在
4 10 1582,15 10 1582。abs(y) 并补 BC;-y % 4 == 1。int、天数用 long long:年份答案可到 知识点:BFS、枚举优化
难度:提高+/省选−
地图上有
小熊要从家出发游玩
每段行程最多转车
一段行程最多转车
从每个点出发做一次 BFS,把距离存进二维数组:
一步的代价是 int,约 25 MB。
不可达的标记不能用「
有了
代价是
测试点表的规模提示了目标复杂度:最后 6 个测试点
| 测试点 | |||
|---|---|---|---|
| 1—8 | 0 或 100 | ||
| 9—11 | 300 | 1000 | 0 |
| 12—14 | 100 | 1000 | 100 |
| 15—20 | 2500 | 10000 | 0 或 100 |
固定中间的两个景点
对每个点
证明:设最优解里的
外层枚举无序的中间点对
只枚举
这一步的代价是
1#include <bits/stdc++.h>
2using namespace std;
3
4const int maxn = 2500 + 5;
5int n, m, k;
6vector<int> g[maxn];
7long long p[maxn];
8vector<int> cand[maxn];
9int dist[maxn][maxn];
10const int INF = 1e9;
11
12void bfs(int s) {
13 for (int i = 1; i <= n; i++)
14 dist[s][i] = INF;
15 queue<int> q;
16 q.push(s);
17 dist[s][s] = 0;
18 while (!q.empty()) {
19 int u = q.front();
20 q.pop();
21 for (int v : g[u])
22 if (dist[s][v] == INF) {
23 dist[s][v] = dist[s][u] + 1;
24 q.push(v);
25 }
26 }
27}
28bool cmp(int i, int j) {
29 return p[i] > p[j];
30}
31
32
33// cand[i]:能到点1和点i的点的列表
34// g[i] 是一个 vector<int>,是点i的邻接表
35int main() {
36 cin >> n >> m >> k;
37 for (int i = 2; i <= n; i++)
38 cin >> p[i];
39 for (int i = 0; i < m; i++) {
40 int u, v;
41 cin >> u >> v;
42 g[u].push_back(v);
43 g[v].push_back(u);
44 }
45 for (int i = 1; i <= n; i++)
46 bfs(i);
47
48 vector<int> a;
49 for (int i = 2; i <= n; i++)
50 if (dist[1][i] <= k + 1)
51 a.push_back(i);
52
53 sort(a.begin(), a.end(), cmp);
54
55 for (int i = 2; i <= n; i++)
56 for (int v : a) {
57 if (v != i && dist[i][v] <= k + 1) {
58 cand[i].push_back(v);
59 if (cand[i].size() == 3) break;
60 }
61 }
62
63 long long ans = 0;
64
65 for (int c = 2; c <= n; c++)
66 for (int d = c + 1; d <= n; d++)
67 if (dist[c][d] <= k + 1)
68 for (int b : cand[c])
69 for (int e : cand[d])
70 if (b != d && e != c && b != e) {
71 // if (p[b] + p[c] + p[d] + p[e] > ans)
72 // cout << b << ' ' << c << ' ' << d << ' ' << e << '\n';
73 ans = max(ans, p[b] + p[c] + p[d] + p[e]);
74 }
75 cout << ans << '\n';
76}
先写
1long long ans = 0;
2for (int A = 2; A <= n; A++) if (can[1][A])
3 for (int B = 2; B <= n; B++) if (B != A && can[A][B])
4 for (int C = 2; C <= n; C++) if (C != A && C != B && can[B][C])
5 for (int D = 2; D <= n; D++)
6 if (D != A && D != B && D != C && can[C][D] && can[D][1])
7 ans = max(ans, s[A] + s[B] + s[C] + s[D]);
8cout << ans << '\n';
它的代码量很小,而且顺带把可达性预处理验证了一遍;在正解写出来之后,它还可以作为小数据上的参照。
dist;按分数从高到低筛出每个点的前 3 个候选 cand;枚举中间点对并配对候选。cand[i] 里只存 3 个点,靠 if (cand[i].size() == 3) break; 提前结束;候选取自按分数排序的数组 a,顺序天然是从高到低。b != d(e != c(b != e(long long;dist 只用 int,三类套路,六道题各归一类
模拟:把过程拆成周期整体跳过——儒略日
枚举:把判定换成计数(密码锁)、把枚举压成候选(假期计划)
贪心:把选择化成配对(决斗)、先取最优再按最小损失调整(社团招新)、逐个资源链式贪心加前缀和(廊桥分配)
一套查错习惯:数据类型与溢出、等号与取整、多组数据的清空
| 这道题 | 相似题 | 解法核心 |
|---|---|---|
| 2024 T1 决斗 | P1309 [NOIP2011 普及组] 瑞士轮 | 排一次序,之后用双指针线性扫,不再重排 |
| 2023 T1 密码锁 | P2010 [NOIP2016 普及组] 回文日期 | 枚举一个维度,其余维度由约束直接推出,再逐条判定 |
| 2025 T1 社团招新 | P5019 [NOIP2018 提高组] 铺设道路 | 先取局部最优,再用交换论证或差分说明全局最优 |
| 2021 T1 廊桥分配 | P2827 [NOIP2016 提高组] 蚯蚓 | 用堆或单调结构维护「下一个该取出的元素」,把查找压到 |
| 2020 T1 儒略日 | P3952 [NOIP2017 提高组] 时间复杂度 | 规则多的大模拟:把规则写成状态与转移,边界靠清单逐条核对 |
| 2022 T1 假期计划 | B4428 [CSP-X2025 山东] 勇者斗恶龙 | 证明最优解只需取值于常数大小的候选集合,把枚举压下来 |