第 4.3 章:排序、匹配与构造
这一章的贪心都有一个共同形态:给元素找到一个「正确的顺序」——自定义比较器、双序列同向配对、或一遍扫描构造答案。USACO 里「按某种顺序排列 N 个东西」的题几乎都在这里。
📌 前置: 第 4.1 章的交换论证法(证明「正确顺序」为什么正确)。
4.3.1 排列贪心:自定义排序标准
经典题一:最小化总完成时间(最短作业优先)
题目: N 个作业在一台机器上顺序运行,作业 i 的处理时间为 t[i]。作业的完成时间 = 之前所有作业与自身的处理时间之和。最小化所有作业完成时间之和。(1 ≤ N ≤ 10⁵,1 ≤ t[i] ≤ 10⁴;总和用 long long。)
样例输入:
5
3 1 4 1 5
样例输出:
31
(升序排序后为 1,1,3,4,5,完成时间为 1,2,5,9,14,总和 31)
贪心策略: 按处理时间升序排序(最短作业优先,SJF)。
为什么 SJF 最优?(交换论证)
设最优顺序中两个相邻作业 A(处理时间 a)和 B(处理时间 b),a > b(B 更短但排在 A 后面)。设 T 是这两个作业之前的累计完成时间:
| 顺序 | A 的完成时间 | B 的完成时间 | 两者之和 |
|---|---|---|---|
| A → B | T + a | T + a + b | 2T + 2a + b |
| B → A | T + b | T + b + a | 2T + a + 2b |
由于 a > b,2T + 2a + b > 2T + a + 2b,所以 B→A 给出更小的和。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<int> t(n);
for (int &x : t) cin >> x;
sort(t.begin(), t.end()); // SJF:按处理时间升序
long long totalCompletion = 0;
long long curTime = 0;
for (int i = 0; i < n; i++) {
curTime += t[i];
totalCompletion += curTime;
}
cout << totalCompletion << "\n";
return 0;
}
// 时间复杂度:O(N log N)
经典题二:最大数(拼接贪心)
题目: 给定 N 个非负整数,将它们排列后拼接成最大的数。输出为字符串。
样例输入:
5
3 30 34 5 9
样例输出:
9534330
(排序比较规则:a+b > b+a。注意 3 应排在 30 前面,因为 "330" > "303")
数据规模: 1 ≤ N ≤ 10⁵;每个数 ≤ 10⁹(作为字符串读入更方便);结果可能非常长(上百万位),必须按字符串处理。
贪心策略: 自定义比较器:对两个数 a 和 b(作为字符串),若 str(a) + str(b) > str(b) + str(a) 则 a 排在 b 前面。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<string> nums(n);
for (string &s : nums) cin >> s;
// 自定义排序:a+b > b+a 时 a 排在前面
sort(nums.begin(), nums.end(), [](const string &a, const string &b) {
return a + b > b + a;
});
// 边界情况:全为零
if (nums[0] == "0") {
cout << "0\n";
return 0;
}
string result = "";
for (const string &s : nums) result += s;
cout << result << "\n";
return 0;
}
// 时间复杂度:O(N log N · L),L = 最大数字位数
逐步追踪(nums = ["3", "30", "34", "5", "9"]):
比较部分对:
"3"+"30"="330" vs "30"+"3"="303" → "3" 排前面
"9"+"5"="95" vs "5"+"9"="59" → "9" 排前面
"34"+"3"="343" vs "3"+"34"="334" → "34" 排前面
排序后:["9", "5", "34", "3", "30"]
结果: "9534330" ✓
⚠️ 警告: 不能简单地按数值大小排序!例如 "3" > "30" 按数值,但拼接 "330" > "303",所以 "3" 应排前面。始终用拼接比较器。
经典题三:最大化(或最小化)内积
题目: 给定两个长度均为 N 的数组 A 和 B,各自可以任意重排,求 ∑ A[i] × B[i] 的最大值和最小值。(1 ≤ N ≤ 10⁵,1 ≤ A[i], B[i] ≤ 10⁹;乘积之和超出 int 范围,用 long long。)
贪心规则:
- 最大化
∑ A[i] × B[i]:A 和 B 都按升序排序(同方向配对) - 最小化
∑ A[i] × B[i]:A 按升序,B 按降序排序(反方向配对)
这是排列不等式:大配大、小配小最大化;大配小、小配大最小化。
示例: A = [1, 2, 3],B = [4, 5, 6](与下方 rearrangement_inequality.svg 图中数据一致)。 同序配对(升序×升序):1×4 + 2×5 + 3×6 = 32(最大值);反序配对(升序×降序):1×6 + 2×5 + 3×4 = 28(最小值)。
// 最大化 sum(A[i] * B[i])
sort(A.begin(), A.end());
sort(B.begin(), B.end());
long long maxSum = 0;
for (int i = 0; i < n; i++) maxSum += (long long)A[i] * B[i];
// 最小化 sum(A[i] * B[i])
sort(A.begin(), A.end());
sort(B.begin(), B.end(), greater<int>());
long long minSum = 0;
for (int i = 0; i < n; i++) minSum += (long long)A[i] * B[i];
经典题四(USACO Bronze):最小代价排序(相邻交换)
题目: N(1 ≤ N ≤ 10⁵)头奶牛站成一排,奶牛 i 的「暴躁值」为 g[i](1 ≤ g[i] ≤ 10⁹,两两互不相同)。想排序这一排使暴躁值严格递增。唯一允许的操作是交换两头相邻的奶牛。交换第 i 和 j 位置的奶牛(相邻)时,代价为 g[i] + g[j]。求排序所需的最小总代价。答案可能超过 int 范围,用 64 位整数。
输入格式: 第一行 N;第二行 N 个整数 g₁, …, g_N。 数据规模: 1 ≤ N ≤ 10⁵,1 ≤ g[i] ≤ 10⁹。下方 O(N²) 逆序对暴力用于理解;正式提交需改为归并排序数逆序对(O(N log N))。
样例输入:
3
3 1 2
样例输出:
9
逆序对方案(O(N²)):
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<long long> g(n);
for (long long &x : g) cin >> x;
// 总代价 = 每对逆序对 (i,j)(i<j,g[i]>g[j])的 g[i]+g[j] 之和
long long totalCost = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (g[i] > g[j]) {
totalCost += g[i] + g[j]; // 这个逆序对的代价
}
}
}
cout << totalCost << "\n";
return 0;
}
// 时间:O(N²) — N ≤ 10^5 时用归并排序逆序对计数(O(N log N))
验证: 对 [3,1,2] 的冒泡排序:
- 交换(3,1) = 代价 4 → [1,3,2]
- 交换(3,2) = 代价 5 → [1,2,3]
- 总计 = 9 ✓
4.3.2 任务分配:双序列匹配
任务分配问题涉及将两个有序序列的元素匹配以优化某个目标。关键模式:对两个序列排序,然后用双指针扫描或直接下标配对。
最大化完成任务数(双指针)
题目: N 个工人各有能力值 ability[i],N 个任务各有难度 difficulty[i]。能力值 ≥ 难度的工人才算完成该任务,每个工人至多做一项任务。最大化完成的任务数。(1 ≤ N ≤ 2×10⁵,1 ≤ ability[i], difficulty[i] ≤ 10⁹。)
样例输入:
3
2 5 3
4 3 6
(格式:第一行 N,第二行能力值,第三行任务难度)
样例输出:
2
(排序后 ability=[2,3,5]、difficulty=[3,4,6]:能力 3 的工人完成任务 3,能力 5 的工人完成任务 4;能力 2 的工人做不了任何任务)
贪心: 对两个序列排序,用双指针贪心匹配。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<int> ability(n), difficulty(n);
for (int &x : ability) cin >> x;
for (int &x : difficulty) cin >> x;
sort(ability.begin(), ability.end());
sort(difficulty.begin(), difficulty.end());
// 双指针:贪心地把最弱的有能力工人分配给每个任务
int completed = 0;
int i = 0, j = 0; // i:工人指针,j:任务指针
while (i < n && j < n) {
if (ability[i] >= difficulty[j]) {
// 工人 i 能完成任务 j——匹配
completed++;
i++;
j++;
} else {
// 工人 i 太弱——尝试更强的工人
i++;
}
}
cout << completed << "\n";
return 0;
}
// 时间复杂度:O(N log N)
经典题(USACO Silver):配对——排列不等式实战
题目: 你有 A 组 N 头奶牛和 B 组 N 头奶牛(1 ≤ N ≤ 10⁵)。必须将 A 中每头奶牛与 B 中恰好一头配对(一对一)。奶牛 a 与奶牛 b 配对的利润是 min(a, b),其中 1 ≤ a, b ≤ 10⁹。最大化 N 对的总利润。总利润可能超过 int 范围。
输入格式: 第一行 N;第二行 A 组的 N 个整数;第三行 B 组的 N 个整数。 数据规模: 1 ≤ N ≤ 10⁵,1 ≤ a[i], b[i] ≤ 10⁹。排序 O(N log N) 轻松通过。
样例输入:
3
1 3 5
2 4 6
样例输出:
9
追踪(两数组升序排序后按下标配对):
A 排序后:[1, 3, 5]
B 排序后:[2, 4, 6]
配对 (1,2):min=1
配对 (3,4):min=3
配对 (5,6):min=5
总计 = 1+3+5 = 9 ✓
为什么同向排序? 交换论证:若某个配对中 a₁ < a₂ 与 b₁ > b₂ 配对(A 升序但 B 不是),交换到同向配对总是不差的(见 4.3.1 节排列不等式)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> A(n), B(n);
for (int &x : A) cin >> x;
for (int &x : B) cin >> x;
sort(A.begin(), A.end());
sort(B.begin(), B.end());
long long total = 0;
for (int i = 0; i < n; i++) {
total += min(A[i], B[i]); // 第 i 小的与第 i 小的配对
}
cout << total << "\n";
return 0;
}
贪心模拟(USACO Bronze):奶牛信号
许多 USACO Bronze 题是带贪心技巧的纯模拟:按时间顺序处理事件,在每步贪心维护最优状态。关键是确定模拟什么以及按什么顺序。
题目: N(1 ≤ N ≤ 10⁵)头奶牛各想从牛棚出发到达牧场。奶牛 i 准备在时间 t[i](0 ≤ t[i] ≤ 10⁹)离开。牛棚和牧场之间的路同时最多容纳 C(1 ≤ C ≤ N)头奶牛,过路恰好需要 1 个时间单位。路满时奶牛在牛棚等待。假设尽早送奶牛出发,最后一头奶牛何时到达?
输入格式: 第一行 N 和 C;第二行 N 个整数 t₁, …, t_N。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, c;
cin >> n >> c;
vector<int> t(n);
for (int &x : t) cin >> x;
sort(t.begin(), t.end()); // 按出发时间顺序处理
int ans = 0;
// 每 c 头奶牛一组处理
for (int i = 0; i < n; i += c) {
// 这批奶牛中最早的是 t[i]
ans = max(ans, t[i]); // 这批出发时间至少要等最早的奶牛准备好
ans++; // 过路需要 1 个时间单位
}
cout << ans << "\n";
return 0;
}
4.3.3 区间合并
区间合并是另一种经典贪心:将所有重叠区间合并为一组不重叠的区间。
题目: 给定 N 个区间 [l[i], r[i]],把相互重叠(或共享端点)的区间合并,输出合并后的区间列表。(1 ≤ N ≤ 10⁵,−10⁹ ≤ l[i] ≤ r[i] ≤ 10⁹。)
样例输入:
5
0 2
1 4
2 6
4 9
5 10
样例输出:
1 个合并区间:
[0,10]
(5 个区间首尾相连,全部重叠,合并为一个 [0,10])
样例输入二:
4
1 3
2 6
8 10
15 18
样例输出二:
3 个合并区间:
[1,6] [8,10] [15,18]
([1,3] 与 [2,6] 重叠合并;[8,10] 与 [15,18] 与前后均不相连)
贪心算法:
- 按左端点升序排序区间
- 维护当前合并区间 [curL, curR]
- 对每个新区间 [l, r]:
- 若 l ≤ curR(重叠或相邻):延伸 curR = max(curR, r)
- 否则:完成当前合并区间,开始新的
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<pair<int,int>> intervals(n);
for (auto &[l, r] : intervals) cin >> l >> r;
sort(intervals.begin(), intervals.end()); // 按左端点排序
vector<pair<int,int>> merged;
for (auto [l, r] : intervals) {
if (merged.empty() || l > merged.back().second) {
// 无重叠——开始新的合并区间
merged.push_back({l, r});
} else {
// 重叠——延伸右端点
merged.back().second = max(merged.back().second, r);
}
}
cout << merged.size() << " 个合并区间:\n";
for (auto [l, r] : merged) {
cout << "[" << l << "," << r << "] ";
}
cout << "\n";
return 0;
}
// 时间复杂度:O(N log N)
逐步追踪(输入已排序:[1,3],[2,6],[8,10],[15,18]):
[1,3]: merged 为空 → 直接加入。merged=[[1,3]]
[2,6]: 2 <= 3(重叠)→ 延伸:[1, max(3,6)]=[1,6]。merged=[[1,6]]
[8,10]: 8 > 6(不重叠)→ 加新区间。merged=[[1,6],[8,10]]
[15,18]: 15 > 10(不重叠)→ 加新区间。merged=[[1,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]] ✓
4.3.4 字典序构造贪心
「构造字典序最小/最大的序列」是一大类贪心题的统称。核心思路一致:序列前面的位置影响最大,所以贪心地让越靠前的位置越优——每一步在「合法的候选」里选使前缀最优的那个,用一个可增删的候选集合维护后面的选择空间。
本节的三个经典题全是这个模式的不同外衣:删数字 = 每步保留让前缀最小的数字;跳跃/股票 = 从左到右每步取局部最优。
经典题:删除 K 个数字得到最小数(字典序最小构造)
题目: 给定一串数字(表示一个大整数),恰好删除 K 个数字(保持剩余数字的原始顺序)以形成最小的整数。
数据规模: 数字串长度 L ≤ 10⁶,1 ≤ K < L;结果可能带前导零,需去除。O(N) 单调栈是唯一可行做法。
例子:
"1432219",K=3 → "1219"
贪心思路: 维护单调栈。从左到右扫描:
- 若栈顶 > 当前数字且还有删除机会:弹出栈顶(删除较大的数字)
- 否则:压入当前数字
- 扫描完后若还有删除机会:从栈的右端删除
#include <bits/stdc++.h>
using namespace std;
int main() {
string num;
int k;
cin >> num >> k;
string stk = ""; // 用字符串作单调栈
for (char c : num) {
// 当栈顶大于当前数字且还有删除机会时弹出
while (k > 0 && !stk.empty() && stk.back() > c) {
stk.pop_back();
k--;
}
stk.push_back(c);
}
// 若还有删除机会,从右端删除
stk.resize(stk.size() - k);
// 移除前导零
int start = 0;
while (start < (int)stk.size() - 1 && stk[start] == '0') start++;
cout << stk.substr(start) << "\n";
return 0;
}
// 时间复杂度:O(N)
经典题:最少跳跃次数
题目: 给定数组 A[0..n-1],A[i] 是从位置 i 可以跳跃的最大步数(可以跳 0 到 A[i] 步)。从索引 0 出发,用最少次数到达最后一个索引。
样例输入:
5
2 3 1 1 4
样例输出:
2
(0→1→4:第一跳最多到 1,第二跳从 1 最多到 4,到达末尾)
数据规模: 1 ≤ N ≤ 10⁵(更大数据也可,O(N) 扫描),0 ≤ A[i] ≤ 10⁵;保证总能到达最后一个索引。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> A(n);
for (int &x : A) cin >> x;
int jumps = 0;
int curEnd = 0; // 当前跳跃能到达的最远位置
int farthest = 0; // 目前可到达的最远位置
for (int i = 0; i < n - 1; i++) {
farthest = max(farthest, i + A[i]); // 更新最远可达
if (i == curEnd) { // 到达当前跳跃范围的末尾
jumps++;
curEnd = farthest; // 跳到最远位置
if (curEnd >= n - 1) break; // 已经可以到达末尾
}
}
cout << jumps << "\n";
return 0;
}
// 时间复杂度:O(N)
经典题:买卖股票最大利润(贪心版)
题目: 给定每日股价,可以无限次买卖(但每次只能持有一股),最大化总利润。
样例输入:
6
7 1 5 3 6 4
样例输出:
7
(捕获所有上涨段:(5−1) + (6−3) = 4 + 3 = 7)
数据规模: 1 ≤ N ≤ 10⁵,1 ≤ price[i] ≤ 10⁹。N = 10⁵ 时逐日 O(N) 贪心即可,无需更复杂方法。
贪心思路: 只要明天价格高于今天,就「今天买明天卖」。等价于累加所有正日差。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> prices(n);
for (int &x : prices) cin >> x;
int profit = 0;
for (int i = 1; i < n; i++) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1]; // 捕获每次上涨
}
}
cout << profit << "\n";
return 0;
}
// 时间复杂度:O(N)
⚠️ 注意: 这是「无限次交易」版本。「最多一次交易」→ 追踪最低买入价的单次扫描。「最多两次/K 次交易」→ 需要 DP。
4.3.5 中位数贪心——「均分」与「聚集」
有一类题问的是:让若干个量「尽量靠近」——把牌均分、把仓库建在哪、人往哪里聚。它们的答案几乎总是中位数。为什么不是平均数?看这个直觉:
到定点 x 的距离之和
Σ|a[i] − x|:x 往右挪 1,左边的每个点距离 +1、右边的每个 −1。挪到左右点数相等(中位数)时就再也不动了——这就是最优解。平均数会被极端值拉偏:仓库选址题里一个超远客户会把平均数拖过去,而中位数岿然不动。
经典题一:一维仓库选址
N 个客户在数轴上(坐标可重复),建一个仓库使到所有客户的距离之和最小。
样例输入:
5
1 2 3 4 100
样例输出:
101
(中位数是 3,距离和 = 2+1+0+1+97 = 101。若选平均数 22,距离和 = 21+20+19+18+78 = 156,差得多。)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<long long> x(n);
for (auto& v : x) cin >> v;
sort(x.begin(), x.end());
long long med = x[n / 2]; // 中位数
long long ans = 0;
for (auto v : x) ans += llabs(v - med);
cout << ans << "\n";
}
经典题二:均分纸牌(前缀和 + 中位数)
N 堆纸牌(第 i 堆 a[i] 张),总数保证能被 N 整除。每次只能把一张牌从一堆移到相邻的一堆。求让所有堆相等的最少移动次数。
样例输入:
4
9 8 17 6
样例输出:
8
转化(关键一步): 设平均值 avg。令 S[i] = (a[1]−avg) + … + (a[i]−avg)——「前 i 堆净多出的牌数」。第 i 堆与第 i+1 堆之间的移动量至少是 |S[i]|(前 i 堆多余的牌必须穿过这条边界流向右边,不足则从右边流入)。总移动次数 = Σ|S[i]|——又变成了「仓库选址」!让所有 S[i] 靠近某个值 t,最优 t 就是 S[1..N−1] 的中位数。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<long long> a(n);
for (auto& x : a) cin >> x;
long long total = 0;
for (long long x : a) total += x;
long long avg = total / n; // 题目保证整除
vector<long long> S(n + 1, 0);
for (int i = 1; i <= n; i++) S[i] = S[i-1] + a[i-1] - avg;
sort(S.begin(), S.end());
long long med = S[n / 2];
long long ans = 0;
for (int i = 0; i <= n; i++) ans += llabs(S[i] - med);
cout << ans << "\n";
}
(样例:avg=10,S = [0, −1, −2, 5, −1],中位数 −1,Σ|S[i]−(−1)| = 1+0+1+6+0 = 8。)
💡 环形版本(进阶): 若堆围成一圈,「断开位置」有 N 种选法——对每个断点算一次上述答案,取最小。经典优化是把断点的贡献排序后一次扫描求出,总复杂度 O(N log N)。这题在 USACO 风格的「Circular Barn」系列中反复出现。
中位数贪心的识别信号: 题目要「最小化 |距离/差| 的总和」或「均分」,且每个量的移动/调整是独立的——找中位数,不是平均数。
练习题
| 题目 | 关键技术 | 难度 |
|---|---|---|
| 4.3.1 统计干草堆(USACO Bronze) | 排序 + 二分统计 | 🟢 简单 |
| 4.3.2 睡觉奶牛排序(USACO Bronze) | 有序后缀观察 | 🟢 简单 |
| 4.3.3 任务调度器 | 频率公式贪心 | 🟡 中等 |
题目 4.3.1 — USACO 2016 December Bronze:统计干草堆
题目: N 捆干草放在数轴上(位置可重复)。Q 次查询:范围 [L, R] 内有多少捆干草?
输入格式: 第一行 N 和 Q;第二行 N 个整数位置;接下来 Q 行每行两个整数 L 和 R。 数据规模: 1 ≤ N, Q ≤ 10⁵,0 ≤ 位置 ≤ 10⁹,0 ≤ L ≤ R ≤ 10⁹。每次查询 O(log N) 的二分才能通过;O(N) 扫描每次查询会超时。
示例:
N=7, Q=4
位置:6 3 2 7 5 1 4
查询:2 5 / 1 1 / 4 8 / 10 15
输出:
4
1
4
0
💡 提示
排序位置,然后用 lower_bound / upper_bound 二分搜索统计 [L, R] 内的元素。这道题练习「排序 + 二分搜索」思维——大多数贪心算法的第一步。
排序后,[L, R] 内的数量 = upper_bound(R) - lower_bound(L)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, q;
cin >> n >> q;
vector<int> pos(n);
for (int &x : pos) cin >> x;
sort(pos.begin(), pos.end()); // 关键:先排序以支持二分搜索
while (q--) {
int l, r;
cin >> l >> r;
auto lo = lower_bound(pos.begin(), pos.end(), l);
auto hi = upper_bound(pos.begin(), pos.end(), r);
cout << (hi - lo) << "\n";
}
return 0;
}
// 时间复杂度:O(N log N + Q log N)
题目 4.3.2 — USACO 2019 February Bronze:睡觉奶牛排序
题目: N(1 ≤ N ≤ 10⁵)头奶牛编号 1~N 以随机顺序站成一排。每次操作:取出排尾的奶牛,将其插入任意位置。最少几次操作能让队伍变为 1, 2, ..., N 的顺序?
输入格式: 第一行 N;第二行 N 个整数表示队伍从头到尾的编号。 数据规模: 1 ≤ N ≤ 10⁵。O(N) 扫描即可。
示例:
N=5
顺序:1 4 2 5 3
输出:4
💡 提示
核心思路: 找队尾已经有序的最长后缀(连续的 k, k+1, ..., N)。这些奶牛不需要移动。答案 = N − 后缀长度。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
vector<int> cows(n);
for (int &x : cows) cin >> x;
// 找最长已排序后缀:cows[i], cows[i+1], ..., cows[n-1]
// 条件:cows[i] + 1 == cows[i+1](连续递增)
int keep = 1; // 至少最后一头奶牛留下
for (int i = n - 2; i >= 0; i--) {
if (cows[i] + 1 == cows[i + 1]) {
keep++;
} else {
break; // 后缀必须连续——遇到第一个断点就停
}
}
cout << n - keep << "\n";
return 0;
}
// 时间复杂度:O(N)
题目 4.3.3 — 任务调度器 🟡 中等
题目: N 个标有 A~Z 的任务(1 ≤ N ≤ 10⁴,任务标号仅大写字母),每个需要 1 个时间单位。执行标有 X 的任务后,CPU 必须等待至少 k(0 ≤ k ≤ 100)个时间单位才能再次执行 X(期间可运行其他任务或空闲)。找完成所有任务的最短总时间。
输入格式: 第一行 N 和 k;第二行 N 个用空格分隔的大写字母。
示例:
tasks = [A, A, A, B, B, B], k = 2
输出:8(A→B→空闲→A→B→空闲→A→B)
💡 提示
关键公式:ans = max(任务总数, (maxCount-1)*(k+1) + countMax)
其中 maxCount = 出现次数最多的任务的频率。
贪心策略:每个「帧」(每 k+1 个时间单位)先填最频繁的剩余任务。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, k;
cin >> n >> k;
vector<int> freq(26, 0);
for (int i = 0; i < n; i++) {
char c; cin >> c;
freq[c - 'A']++;
}
int maxCount = *max_element(freq.begin(), freq.end());
int countMax = count(freq.begin(), freq.end(), maxCount);
int ans = max(n, (maxCount - 1) * (k + 1) + countMax);
cout << ans << "\n";
return 0;
}
// 时间复杂度:O(N)