跳转至

J-L25 贪心

覆盖词条:KJ-31a | 先修:J-L20/L23

贪心入门 · 排序取最值

规范币制找零1

规范币制找零

题目描述

售货机里有无限张 10 元、5 元、2 元、1 元纸币。顾客付款后需要找零 \(n\) 元,每张纸币都算一张。求找零所用的最少纸币张数(要求用贪心:优先使用大面额)。

输入格式

一个整数 \(n\)

输出格式

一个整数,为最少张数。

样例输入

23

样例输出

4

(10 + 10 + 2 + 1,共 4 张。)

数据范围

  • \(1 \le n \le 1000\)

⬇ 点击下载数据包

规范币制找零 AC 代码

解题思路

贪心:每种面额能用多少张就用多少张,从大到小扫一遍。这种币制下贪心一定最优——大面额总能被若干小面额替换成更多张,交换论证成立。与下一题成对:换一种币制,贪心就会失效。

AC代码

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

// 规范币制找零:优先用大面额,兑换后 n 一定能被剩余面额凑出
int main() {
    int n;
    cin >> n;
    int cnt = 0;
    for (int v : {10, 5, 2, 1}) {
        cnt += n / v;
        n %= v;
    }
    cout << cnt << endl;
    return 0;
}

反例币制找零2

反例币制找零

题目描述

售货亭只有 7 元、5 元、1 元三种纸币(每种无数张),要找零 \(n\) 元。小 X 的收银程序按「优先用最大面额」找零。请分别输出:

  1. 小 X 的贪心程序会用掉的张数;
  2. 真正的最少张数;
  3. 若两者相同输出 OK,否则输出 FAIL

输入格式

一个整数 \(n\)

输出格式

共三行:贪心张数、最少张数、OKFAIL

样例输入

10

样例输出

1
2
3
4
2
FAIL

(贪心拿 7+1+1+1 共 4 张;最优是 5+5 只需 2 张——贪心失效。)

数据范围

  • \(1 \le n \le 100\)

⬇ 点击下载数据包

反例币制找零 AC 代码

解题思路

贪心部分与上题同型;「真正最少张数」用三重循环枚举三种面额的张数(\(n \le 100\) 规模足够)。本题是贪心适用性的标本:面额 {7,5,1} 不满足「大面额可被小面额替换」的性质,贪心不再保证最优——使用贪心前必须先论证正确性。

AC代码

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

// 反例币制找零:面额 {7,5,1},「优先大面额」的贪心不再保证最优
int main() {
    int n;
    cin >> n;
    int g = n, gcnt = 0;                    // 小 X 的收银程序:贪心
    for (int v : {7, 5, 1}) {
        gcnt += g / v;
        g %= v;
    }
    // 真正最少张数:三重循环枚举三种面额各用几张
    int best = n + 1;
    for (int a = 0; a * 7 <= n; a++)
        for (int b = 0; a * 7 + b * 5 <= n; b++) {
            int rest = n - a * 7 - b * 5;
            best = min(best, a + b + rest);
        }
    cout << gcnt << "\n" << best << "\n" << (gcnt == best ? "OK" : "FAIL") << "\n";
    return 0;
}

购买文具22

购买文具

题目描述

文具店有 \(n\) 件文具,第 \(i\) 件价格 \(p_i\) 元。预算为 \(m\) 元,每件最多买一件,求最多能买多少件。

输入格式

第一行两个整数 \(n, m\);第二行 \(n\) 个整数 \(p_i\)

输出格式

一个整数,为最多件数。

样例输入

5 20
4 8 5 3 9

样例输出

4

(最便宜的 3+4+5+8 = 20 元恰好用完预算,共 4 件。)

数据范围

  • \(1 \le n \le 10^5\)
  • \(1 \le p_i \le 10^4\)
  • \(1 \le m \le 10^9\)

⬇ 点击下载数据包

购买文具 AC 代码

解题思路

件数最多,就先买便宜的:升序排序后从最便宜开始累加,加不动为止。交换论证:任何方案里的贵文具换成没买的便宜文具,件数不变、花费更小——所以「最便宜的先拿」不会更差。

AC代码

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

int main() {
    int n;
    long long m;
    cin >> n >> m;
    vector<long long> p(n);
    for (auto &x : p) cin >> x;
    sort(p.begin(), p.end());
    long long sum = 0;
    int cnt = 0;
    for (auto x : p) {
        if (sum + x > m) break;
        sum += x;
        cnt++;
    }
    cout << cnt << endl;
    return 0;
}

混合牛奶3

混合牛奶

题目描述

Marry 乳业每天需要采购 \(n\) 单位牛奶,有 \(m\) 位奶农,第 \(i\) 位奶农牛奶单价为 \(p_i\),一天最多卖出 \(a_i\) 单位。每位奶农的牛奶可以只买一部分(整数数量)。求买够 \(n\) 单位牛奶的最小花费。

输入格式

第一行两个整数 \(n, m\);接下来 \(m\) 行每行两个整数 \(p_i, a_i\)

输出格式

一个整数,为最小费用。

样例输入

1
2
3
4
5
6
100 5
5 20
9 40
3 10
8 80
6 30

样例输出

630

数据范围

  • \(0 \le n, a_i \le 2 \times 10^6\)\(0 \le m \le 5000\)\(0 \le p_i \le 1000\)
  • 保证所有奶农的总产量不少于 \(n\)
混合牛奶 AC 代码

解题思路

排序贪心入门:按单价从低到高排序,便宜的先买满,不够再买下一家——「便宜的多拿」就是这里的贪心策略。

AC代码

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

// P1208 混合牛奶:单价低的奶农先买满
int main() {
    long long n;
    int m;
    cin >> n >> m;
    vector<pair<long long, long long>> f(m);   // (单价, 产量)
    for (auto& p : f) cin >> p.first >> p.second;
    sort(f.begin(), f.end());
    long long cost = 0;
    for (auto& p : f) {
        long long take = min(n, p.second);
        cost += take * p.first;
        n -= take;
        if (n == 0) break;
    }
    cout << cost << endl;
    return 0;
}

部分背包问题4

部分背包问题

题目描述

藏宝洞里有 \(N\) 堆金币,第 \(i\) 堆总重量 \(m_i\)、总价值 \(v_i\)。背包承重为 \(T\),金币可以随意分割(分割后单位价值不变)。求能装走的最大总价值。

输入格式

第一行两个整数 \(N, T\);接下来 \(N\) 行每行两个整数 \(m_i, v_i\)

输出格式

一个实数,表示最大价值,输出两位小数。

样例输入

1
2
3
4
5
4 50
10 60
20 100
25 100
15 45

样例输出

240.00

数据范围

  • \(1 \le N \le 100\)\(1 \le m_i, v_i \le 100\)\(1 \le T \le 1000\)
部分背包问题 AC 代码

解题思路

可分割 → 按单位价值 \(v_i / m_i\) 从高到低装,装满整个或装到背包满为止。注意输入是先重量后价值。若金币不可分割,就变成 L28 的 0/1 背包——「可分割」正是贪心能用的前提。

AC代码

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

// P2240 部分背包:金币可分割,按单位价值从高到低装
int main() {
    int n;
    double t;
    cin >> n >> t;
    vector<pair<double, double>> a(n);         // (单位价值, 重量)
    for (auto& p : a) {
        double m, v;
        cin >> m >> v;
        p = {v / m, m};
    }
    sort(a.rbegin(), a.rend());
    double ans = 0;
    for (auto& p : a) {
        double take = min(t, p.second);
        ans += take * p.first;
        t -= take;
        if (t <= 0) break;
    }
    printf("%.2f\n", ans);
    return 0;
}

陶陶摘苹果·升级版5

陶陶摘苹果·升级版

题目描述

苹果树结了 \(n\) 个果子,陶陶有一个 \(a\) 公分的椅子,手伸直最大长度 \(b\):苹果高度 \(x_i \le a+b\) 才够得着。每摘一个苹果要花 \(y_i\) 点力气,陶陶总共有 \(s\) 点力气。求他最多能摘到多少个苹果。

输入格式

第 1 行两个整数 \(n, s\);第 2 行两个整数 \(a, b\);接下来 \(n\) 行每行两个整数 \(x_i, y_i\)

输出格式

一个整数,为最多能摘到的苹果数。

样例输入

8 15
20 130
120 3
150 2
110 7
180 1
50 8
200 0
140 3
120 2

样例输出

4

数据范围

  • \(n \le 5000\)\(a \le 50\)\(b \le 200\)\(s \le 1000\)\(x_i \le 280\)\(y_i \le 100\)
陶陶摘苹果·升级版 AC 代码

解题思路

两步贪心:先按高度 \(x_i \le a+b\) 过滤出够得着的苹果,再按力气 \(y_i\) 从小到大摘——「代价小的优先」保证个数最多。

AC代码

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

// P1478 陶陶摘苹果升级版:够得着的苹果里,挑力气花费最小的先摘
int main() {
    int n, s, a, b;
    cin >> n >> s >> a >> b;
    int reach = a + b;
    vector<int> cost;
    for (int i = 0; i < n; i++) {
        int x, y;
        cin >> x >> y;
        if (x <= reach) cost.push_back(y);
    }
    sort(cost.begin(), cost.end());
    int cnt = 0;
    for (int c : cost) {
        if (s < c) break;
        s -= c;
        cnt++;
    }
    cout << cnt << endl;
    return 0;
}

仓库选址23

仓库选址

题目描述

数轴上有 \(n\) 家商店,第 \(i\) 家在坐标 \(x_i\)。要建一个仓库(建在整数坐标上),使所有商店到仓库的距离总和最小。求这个最小总和。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数 \(x_i\)

输出格式

一个整数,为最小距离总和。

样例输入

5
1 3 7 9 10

样例输出

15

(仓库放在 7:6 + 4 + 0 + 2 + 3 = 15。)

数据范围

  • \(1 \le n \le 2000\)
  • \(0 \le x_i \le 10^9\)

⬇ 点击下载数据包

仓库选址 AC 代码

解题思路

排序取中位数 \(x_{\lceil n/2 \rceil}\)。直觉:仓库每向右挪 1,左边每家店多走 1、右边每家少走 1——挪到「两边家数打平」的点就再也不亏,这个点正是中位数(\(n\) 为偶数时两个中间位置之间的任意点都最优)。距离总和最多约 \(2000 \times 10^9\),开 long long

AC代码

#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 best = x[(n - 1) / 2], total = 0;
    for (auto v : x) total += llabs(v - best);
    cout << total << endl;
    return 0;
}

数列分段 Section I6

数列分段 Section I

题目描述

给定长度为 \(N\) 的数列 \(A_i\),把它分成连续的若干段,使每段之和都不超过 \(M\)。求最少能分成多少段。

输入格式

第一行两个正整数 \(N, M\);第二行 \(N\) 个非负整数 \(A_i\)

输出格式

一个整数,为最少段数。

样例输入

5 6
4 2 4 5 1

样例输出

3

(划分成 [4]、[2 4]、[5 1] 三段。)

数据范围

  • \(1 \le N \le 10^5\)\(M \le 10^9\)\(M\) 大于数列中的最大值,\(A_i\) 之和不超过 \(10^9\)
数列分段 Section I AC 代码

解题思路

顺序装箱:从左到右扫,能塞进当前段就塞,塞不下就另起一段——「能不新开段就不新开」显然最优。与 J-L22 的「数列分段 Section II」成对:那题求最大段最小化,要用二分答案;本题顺序固定,贪心一遍即可。

AC代码

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

// P1181 数列分段 Section I:顺序装箱,装不下就另起一段
int main() {
    int n;
    long long m;
    cin >> n >> m;
    int cnt = 1;
    long long cur = 0;
    for (int i = 0; i < n; i++) {
        long long x;
        cin >> x;
        if (cur + x <= m) cur += x;
        else { cnt++; cur = x; }
    }
    cout << cnt << endl;
    return 0;
}

交换论证 · 排序键设计

排队接水7

排队接水

题目描述

\(n\) 个人在一个水龙头前排队接水,第 \(i\) 个人接水需要 \(T_i\) 时间。请找出一种排队顺序,使 \(n\) 个人的平均等待时间最小(等待时间不包括自己接水的时间)。若两人接水时间相同,编号小的应排前面。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数 \(T_i\)

输出格式

共两行:第一行为排队顺序(编号序列);第二行为平均等待时间,精确到小数点后两位。

样例输入

10
56 12 1 99 1000 234 33 55 99 812

样例输出

3 2 7 8 1 4 9 6 10 5
291.90

数据范围

  • \(1 \le n \le 1000\)\(1 \le T_i \le 10^6\)(不保证 \(T_i\) 互不相同)
排队接水 AC 代码

解题思路

交换论证的启蒙题:把相邻两人交换,总等待时间的变化只取决于这两人的接水时间——短的在前总时间更小,所以「短作业优先」最优。用 (时间, 编号) 二元组排序,天然处理同时间小编号在前。

AC代码

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

// P1223 排队接水:短作业优先(交换论证可证),同时间小编号在前
int main() {
    int n;
    cin >> n;
    vector<pair<long long, int>> t(n);         // (接水时间, 编号)
    for (int i = 0; i < n; i++) {
        cin >> t[i].first;
        t[i].second = i + 1;
    }
    sort(t.begin(), t.end());
    double sum = 0, wait = 0;
    for (int i = 0; i < n; i++) {
        cout << t[i].second << " \n"[i == n - 1];
        sum += wait;                            // 第 i 个人等前面所有人接完
        wait += t[i].first;
    }
    cout << fixed << setprecision(2) << sum / n << "\n";
    return 0;
}

拼最大数24

拼最大数

题目描述

给定 \(n\) 个正整数,把它们全部拼接到一起(每个数用且用一次,数与数之间不加空格),使得到的数尽量大。输出这个最大的数。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个正整数。

输出格式

拼接成的最大整数(一行输出)。

样例输入

3
13 312 343

样例输出

34331213

(拼接顺序 343、312、13。)

数据范围

  • \(1 \le n \le 1000\)
  • \(1 \le a_i \le 10^9\)(输入不含前导零)

⬇ 点击下载数据包

拼最大数 AC 代码

解题思路

不能按数值排(3 应排在 312 前面),也不能按字典序盲排——正确的比较是拼接后比\(a\)\(b\) 谁前谁后,看 \(a+b\)\(b+a\) 哪个大。这正是一次交换论证:相邻两数交换只影响这两数拼接处的值,局部最优推广到全局(该比较具有传递性)。把数当字符串存,自定义比较器排序后依次拼接。

AC代码

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

int main() {
    int n;
    cin >> n;
    vector<string> s(n);
    for (auto &x : s) cin >> x;
    // 交换论证:两个串 a、b,谁拼在前面大就谁在前——比较 a+b 与 b+a
    sort(s.begin(), s.end(), [](const string &a, const string &b) {
        return a + b > b + a;
    });
    string ans;
    for (auto &x : s) ans += x;
    cout << ans << endl;
    return 0;
}

跳跳!8

跳跳!

题目描述

地面高度为 0,有 \(n\) 块高度为 \(h_i\) 的石头。小跳蛙从地面出发,每块石头都要恰好踩一次,从第 \(i\) 块跳到第 \(j\) 块耗费 \((h_i - h_j)^2\) 的体力(从地面跳上第 \(i\) 块耗费 \(h_i^2\))。求耗费体力值的最大值

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个正整数 \(h_i\)

输出格式

一个整数,为最大体力值。

样例输入

2
2 1

样例输出

5

数据范围

  • \(1 \le n \le 100\)\(1 \le h_i \le 10^4\)
跳跳! AC 代码

解题思路

排序后「最高 → 最低 → 次高 → 次低」交替跳:先跳上最高的石头(从地面起跳落差最大),之后每一步都在当前极值间来回横跳,让每步落差的平方尽量大。注意 \((h_i - h_j)^2\) 最大约 \(10^8\),用 long long。

AC代码

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

// P4995 跳跳!:排序后「最高→最低→次高→次低」交替跳,每步落差最大
int main() {
    int n;
    cin >> n;
    vector<long long> h(n);
    for (auto& x : h) cin >> x;
    sort(h.begin(), h.end());
    long long total = 0, cur = 0;              // cur 从地面 0 出发
    int i = 0, j = n - 1;
    bool big = true;
    while (i <= j) {
        long long nxt = big ? h[j--] : h[i++];
        total += (nxt - cur) * (nxt - cur);
        cur = nxt;
        big = !big;
    }
    cout << total << endl;
    return 0;
}

排座椅9

排座椅

题目描述

教室坐成 \(M\)\(N\) 列,有 \(D\) 对同学上课时交头接耳。要增设 \(K\) 条横向通道(隔开前后同学)和 \(L\) 条纵向通道(隔开左右同学),每条通道能隔开恰好相邻的一对同学。求一种通道方案,使被隔开的交头接耳对数最多。

输入格式

第一行五个整数 \(M, N, K, L, D\);接下来 \(D\) 行每行四个整数 \(x, y, p, q\),表示坐在 \((x,y)\)\((p,q)\) 的同学在交头接耳(保证两人要么同行、要么同列且相邻)。

输出格式

共两行:第一行 \(K\) 个整数,为横向通道开在第几行之后(升序);第二行 \(L\) 个整数,为纵向通道开在第几列之后(升序)。

样例输入

1
2
3
4
4 5 1 2 3
4 2 4 3
2 3 3 3
2 5 2 4

样例输出

2
2 4

数据范围

  • \(2 \le N, M \le 1000\)\(0 \le K < M\)\(0 \le L < N\)\(0 \le D \le 2000\)
  • 数据保证存在唯一最佳方案
排座椅 AC 代码

解题思路

横纵通道互不影响,独立做两次贪心:统计每个「行间隙」「列间隙」能隔开多少对说话同学,各自取前 \(K\) / \(L\) 大的位置——「收益大的位置优先」。

AC代码

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

// P1056 排座椅:横纵通道独立统计各自能隔开多少对,取前 k/l 大
int rowCnt[1005], colCnt[1005];

int main() {
    int m, n, k, l, d;
    cin >> m >> n >> k >> l >> d;
    for (int i = 0; i < d; i++) {
        int x, y, p, q;
        cin >> x >> y >> p >> q;
        if (x == p) colCnt[min(y, q)]++;       // 同行相邻 → 在两列之间开竖通道
        else rowCnt[min(x, p)]++;              // 同列相邻 → 在两行之间开横通道
    }
    vector<pair<int, int>> rows, cols;         // (隔开对数, 位置)
    for (int i = 1; i < m; i++) if (rowCnt[i]) rows.push_back({rowCnt[i], i});
    for (int j = 1; j < n; j++) if (colCnt[j]) cols.push_back({colCnt[j], j});
    sort(rows.rbegin(), rows.rend());
    sort(cols.rbegin(), cols.rend());
    vector<int> ra, ca;
    for (int i = 0; i < k && i < (int)rows.size(); i++) ra.push_back(rows[i].second);
    for (int i = 0; i < l && i < (int)cols.size(); i++) ca.push_back(cols[i].second);
    sort(ra.begin(), ra.end());
    sort(ca.begin(), ca.end());
    for (int i = 0; i < (int)ra.size(); i++) cout << ra[i] << " \n"[i == (int)ra.size() - 1];
    if (ra.empty()) cout << "\n";
    for (int i = 0; i < (int)ca.size(); i++) cout << ca[i] << " \n"[i == (int)ca.size() - 1];
    if (ca.empty()) cout << "\n";
    return 0;
}

删数问题10

删数问题

题目描述

输入一个高精度正整数 \(n\)(不超过 250 位),去掉其中任意 \(k\) 个数字后,剩下的数字按原左右次序组成一个新数。求新数最小是多少。

输入格式

第一行一个高精度正整数 \(n\);第二行一个整数 \(k\)

输出格式

一个整数,为剩下的最小数(不含前导零)。

样例输入

175438
4

样例输出

13

数据范围

  • \(n\) 的位数 \(\le 250\)\(1 \le k <\) 位数
删数问题 AC 代码

解题思路

每轮从左往右找第一个下降位\(s_i > s_{i+1}\)),删掉 \(s_i\)——高位越小越好;若整串一直递增,删末位。重复 \(k\) 轮后去掉前导零。

AC代码

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

// P1106 删数问题:每轮删掉「第一个下降位」的数字,剩 k 轮
int main() {
    string s;
    int k;
    cin >> s >> k;
    while (k--) {
        int i = 0;
        while (i + 1 < (int)s.size() && s[i] <= s[i + 1]) i++;
        s.erase(i, 1);                          // 全程递增则删末位
    }
    while (s.size() > 1 && s[0] == '0') s.erase(0, 1);   // 去前导零
    if (s.empty()) s = "0";
    cout << s << endl;
    return 0;
}

保留 k 位最大数25

保留 k 位最大数

题目描述

输入一个数字串 \(s\)(首位不为 0),从中保留 \(k\) 个数字、保持原有左右顺序,使剩下的 \(k\) 位数字组成的数最大。输出这个最大的 \(k\) 位数。

输入格式

第一行一个数字串 \(s\);第二行一个整数 \(k\)

输出格式

保留 \(k\) 位后能组成的最大数(按 \(k\) 位原样输出)。

样例输入

175438
4

样例输出

7548

(相当于删去 1 和 3,保留 7 5 4 8——与「删数问题」是同一个数字串。)

数据范围

  • 数字串长度 \(2 \le L \le 15\),首位不为 0
  • \(1 \le k < L\)

⬇ 点击下载数据包

保留 k 位最大数 AC 代码

解题思路

与「删数问题」是同一枚硬币的两面:删去 \(L - k\) 位使剩余最大 ⟺ 保留 \(k\) 位使数最大。贪心用:扫描每位,当栈顶数字比当前位、且还有删除名额时,弹掉栈顶(高位换更大的数字必然更优);扫完若名额没用完,把末尾截掉。两者都是「邻位交换只会更差」的贪心。

AC代码

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

int main() {
    string s;
    int k;
    cin >> s >> k;
    int rem = (int)s.size() - k;   // 还可以丢弃的字符数
    string st;                     // 栈:保留的数字(保持原相对次序)
    for (char c : s) {
        // 栈顶比当前数字小还舍得丢吗?丢掉它能让高位变大
        while (!st.empty() && rem > 0 && st.back() < c) {
            st.pop_back();
            rem--;
        }
        st.push_back(c);
    }
    // 若一路递增没丢够,把末尾多余的截掉
    cout << st.substr(0, k) << endl;
    return 0;
}

纪念品分组11

纪念品分组

题目描述

\(n\) 件纪念品,价格为 \(P_i\)。要把它们分组发放,每组最多 2 件,且组内价格之和不超过 \(w\)。求最少需要分多少组。

输入格式

第一行一个整数 \(w\);第二行一个整数 \(n\);接下来 \(n\) 行每行一个整数 \(P_i\)

输出格式

一个整数,为最少分组数。

样例输入

100
9
90
20
20
30
50
60
70
80
90

样例输出

6

数据范围

  • \(1 \le n \le 3 \times 10^4\)\(80 \le w \le 200\)\(5 \le P_i \le w\)
纪念品分组 AC 代码

解题思路

排序 + 首尾双指针:最贵的纪念品一定要占一组,能带上最便宜的就带上(交换论证:这样不会让任何其他配对变差),带不上就独占一组。

AC代码

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

// P1094 纪念品分组:排序后最贵 + 最便宜配对,配不上则最贵的独占一组
int main() {
    int w, n;
    cin >> w >> n;
    vector<int> p(n);
    for (auto& x : p) cin >> x;
    sort(p.begin(), p.end());
    int i = 0, j = n - 1, cnt = 0;
    while (i <= j) {
        if (i < j && p[i] + p[j] <= w) i++;    // 最便宜的能搭最贵的就带上
        j--;                                    // 最贵的无论如何都占一组
        cnt++;
    }
    cout << cnt << endl;
    return 0;
}

区间贪心

线段覆盖12

线段覆盖

题目描述

各大 OJ 上有 \(n\) 个比赛,第 \(i\) 个比赛的开始、结束时间分别为 \(a_i, b_i\)。参加一个比赛必须善始善终,且不能同时参加两场比赛。求最多能参加多少个比赛。

输入格式

第一行一个整数 \(n\);接下来 \(n\) 行每行两个整数 \(a_i, b_i\)

输出格式

一个整数,为最多能参加的比赛数。

样例输入

1
2
3
4
3
0 2
2 4
1 3

样例输出

2

数据范围

  • \(1 \le n \le 10^6\)\(0 \le a_i \le b_i \le 10^6\)
线段覆盖 AC 代码

解题思路

活动选择经典:按结束时间从早到晚排序,能参加就参加——早结束的先选,给后面留的机会最多(交换论证)。与下一题「区间覆盖」成对:一个求最多不重叠,一个求最少盖满,排序键一个用右端点、一个用左端点。

AC代码

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

// P1803 线段覆盖:按结束时间排序,结束早的先参加(活动选择经典)
int main() {
    int n;
    cin >> n;
    vector<pair<int, int>> v(n);               // (结束时间, 开始时间)
    for (auto& p : v) cin >> p.second >> p.first;
    sort(v.begin(), v.end());
    int cnt = 0, last = -1;
    for (auto& p : v) {
        if (p.second >= last) { cnt++; last = p.first; }
    }
    cout << cnt << endl;
    return 0;
}

区间覆盖13

区间覆盖

题目描述

数轴上有一条目标线段 \([0, L]\),另有 \(n\) 条可选线段,第 \(i\) 条为 \([a_i, b_i]\)。选最少的线段,使它们的并恰好完全覆盖 \([0, L]\)(允许重叠)。若无解输出 \(-1\)

输入格式

第一行两个整数 \(n, L\);接下来 \(n\) 行每行两个整数 \(a_i, b_i\)

输出格式

一个整数,为最少线段数;无解输出 \(-1\)

样例输入

1
2
3
4
5
6
5 10
0 3
2 5
4 8
7 10
8 9

样例输出

4

数据范围

  • \(1 \le n \le 10^5\)\(0 \le L \le 10^9\)\(0 \le a_i \le b_i \le 10^9\)

⬇ 点击下载数据包

区间覆盖 AC 代码

解题思路

按左端点排序后向右延伸:维护已覆盖到 \(cur\),每次在所有左端点 \(\le cur\) 的线段中挑右端点最远的一条(贪心:延伸得越远,用的条数越少);延伸不动且没到 \(L\) 即无解。与「线段覆盖」成对辨析排序键:按右端点求最多、按左端点求最少。

AC代码

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

int main() {
    int n;
    long long L;
    cin >> n >> L;
    vector<pair<long long, long long>> seg(n);
    for (auto& s : seg) cin >> s.first >> s.second;
    sort(seg.begin(), seg.end());           // 按左端点排序
    long long cur = 0;                      // 已覆盖 [0, cur]
    int cnt = 0;
    for (int i = 0; i < n && cur < L; ) {
        if (seg[i].second <= cur) { i++; continue; }    // 整条被盖住,跳过
        if (seg[i].first > cur) break;                   // 与已覆盖区断档:无解
        long long reach = cur;                           // 左端点 ≤ cur 的线段里挑右端点最远的
        while (i < n && seg[i].first <= cur) {
            reach = max(reach, seg[i].second);
            i++;
        }
        cur = reach;
        cnt++;
    }
    if (cur < L) cout << -1 << "\n";
    else cout << cnt << "\n";
    return 0;
}

射爆气球26

射爆气球

题目描述

数轴上有 \(n\) 个气球,第 \(i\) 个覆盖区间 \([l_i, r_i]\)(含端点)。一支箭射中坐标 \(x\) 会引爆所有满足 \(l_i \le x \le r_i\) 的气球。求引爆全部气球的最少箭数。

输入格式

第一行一个整数 \(n\);接下来 \(n\) 行每行两个整数 \(l_i, r_i\)

输出格式

一个整数,为最少箭数。

样例输入

1
2
3
4
5
4
10 16
2 8
1 6
7 12

样例输出

2

(一支箭放在 x=6 射爆 [1,6] 和 [2,8];一支放在 x=12 射爆 [7,12] 和 [10,16]。)

数据范围

  • \(1 \le n \le 10^5\)
  • \(0 \le l_i \le r_i \le 10^9\)

⬇ 点击下载数据包

射爆气球 AC 代码

解题思路

右端点从小到大排序:第一支箭放在第一个区间的右端点——放得尽量靠右,才能「顺带」引爆后面所有左端点不超过它的区间;遇到左端点超出当前箭射程的气球,再补一支新箭(同样放在它的右端点)。与「线段覆盖」互为对偶:一个用最少的点扎穿所有区间,一个用最多的互不重叠区间。

AC代码

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

int main() {
    int n;
    cin >> n;
    vector<pair<long long, long long>> seg(n);   // {左端点, 右端点}
    for (auto &[l, r] : seg) cin >> l >> r;
    // 按右端点从小到大:箭放在当前未被引爆区间的右端点,
    // 能把它和后面所有左端点 <= 该点的区间一起引爆
    sort(seg.begin(), seg.end(), [](auto &a, auto &b) {
        return a.second < b.second;
    });
    int cnt = 0;
    long long cur = LLONG_MIN;   // 上一支箭的位置
    for (auto &[l, r] : seg) {
        if (l > cur) {           // 这支箭够不着它,需要一支新箭
            cnt++;
            cur = r;
        }
    }
    cout << cnt << endl;
    return 0;
}

移走比赛27

移走比赛

题目描述

\(n\) 圆比赛,第 \(i\) 场占用时间区间 \([s_i, e_i)\)。两场比赛重叠(一场未结束时另一场已开始)就不能都保留。求最少移走多少场比赛,使剩下的比赛互不重叠。

输入格式

第一行一个整数 \(n\);接下来 \(n\) 行每行两个整数 \(s_i, e_i\)

输出格式

一个整数,为最少移走的场数。

样例输入

1
2
3
4
5
4
1 3
2 4
3 5
6 8

样例输出

1

(移走 [2,4]:[1,3] 与 [3,5] 端点相接不算重叠,[6,8] 互不影响。)

数据范围

  • \(1 \le n \le 10^5\)
  • \(0 \le s_i < e_i \le 10^9\)

⬇ 点击下载数据包

移走比赛 AC 代码

解题思路

「线段覆盖」的镜像:那边求最多保留、这边求最少移走,而「最少移走 $= n - $ 最多保留」——同一个按结束时间排序的活动选择贪心,换个问法而已。判定保留时用 \(s_i \ge \text{上一场结束时间}\)(端点相接不算重叠)。

AC代码

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

int main() {
    int n;
    cin >> n;
    vector<pair<long long, long long>> seg(n);   // {结束时间, 开始时间}
    for (auto &[e, s] : seg) cin >> s >> e;
    // 按结束时间排序,能留下就留下(活动选择),最少移走 = n - 最多留下
    sort(seg.begin(), seg.end());
    long long lastEnd = LLONG_MIN;
    int kept = 0;
    for (auto &[e, s] : seg) {
        if (s >= lastEnd) {   // 与已选区间不重叠(端点相接允许)
            kept++;
            lastEnd = e;
        }
    }
    cout << n - kept << endl;
    return 0;
}

修理牛棚14

修理牛棚

题目描述

一排编号 \(1 \sim s\) 的牛棚,其中有 \(c\) 头牛(给出每头牛所在牛棚编号)。要用木板把所有有牛的牛棚都挡住(一块木板可以连续盖若干牛棚),最多只能买 \(m\) 块木板。求木板总长度的最小值。

输入格式

第一行三个整数 \(m, s, c\);接下来 \(c\) 行每行一个整数,为牛所在牛棚的编号。

输出格式

一个整数,为木板最小总长度。

样例输入

4 50 18
3
4
6
8
14
15
16
17
21
25
26
27
30
31
40
41
42
43

样例输出

25

数据范围

  • \(1 \le m \le 50\)\(1 \le c \le s \le 200\)
修理牛棚 AC 代码

解题思路

逆向贪心:先用一整块木板盖住所有牛(长度 = 最右牛棚 − 最左牛棚 + 1),再把牛之间的空隙从大到小断开 \(m-1\) 个——每断开一个空隙省下一段木板,省得多的先断。

AC代码

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

// P1209 修理牛棚:先用一整块盖住所有牛,再把最大的 m-1 个空隙断开
int main() {
    int m, s, c;
    cin >> m >> s >> c;
    vector<int> a(c);
    for (auto& x : a) cin >> x;
    sort(a.begin(), a.end());
    int total = a[c - 1] - a[0] + 1;
    vector<int> gap;
    for (int i = 1; i < c; i++) gap.push_back(a[i] - a[i - 1] - 1);
    sort(gap.rbegin(), gap.rend());
    for (int i = 0; i < m - 1 && i < (int)gap.size(); i++) total -= gap[i];
    cout << total << endl;
    return 0;
}

防晒15

防晒

题目描述

\(C\) 头奶牛晒日光浴,第 \(i\) 头需要阳光强度在 \([minSPF_i, maxSPF_i]\) 之间。有 \(L\) 种防晒霜,第 \(i\) 种能把强度稳定为 \(SPF_i\),共有 \(cover_i\) 瓶。每头奶牛必须涂一瓶,求最多能满足多少头奶牛。

输入格式

第一行两个整数 \(C, L\);接下来 \(C\) 行每行两个整数 \(minSPF_i, maxSPF_i\);再接下来 \(L\) 行每行两个整数 \(SPF_i, cover_i\)

输出格式

一个整数,为最多能满足的奶牛数。

样例输入

1
2
3
4
5
6
3 2
3 10
2 5
1 5
6 2
4 1

样例输出

2

数据范围

  • \(1 \le C, L \le 2500\)\(1 \le minSPF_i \le maxSPF_i \le 1000\)\(1 \le SPF_i \le 1000\)\(1 \le cover_i \le 2500\)
防晒 AC 代码

解题思路

奶牛按 \(minSPF\) 升序处理,每头牛用「能用的瓶中 SPF 最小」的那瓶(交换论证:大 SPF 瓶留给下限更高的牛才不浪费)——排序键 + 匹配的复合贪心。

AC代码

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

// P2887 防晒:奶牛按 minSPF 升序,每头用「能用瓶中 SPF 最小」的那瓶
int main() {
    int c, l;
    cin >> c >> l;
    vector<pair<int, int>> cow(c);             // (minSPF, maxSPF)
    for (auto& p : cow) cin >> p.first >> p.second;
    sort(cow.begin(), cow.end());
    vector<pair<int, int>> lot(l);             // (SPF, 瓶数)
    for (auto& p : lot) cin >> p.first >> p.second;
    int ans = 0;
    for (auto& p : cow) {
        int best = -1;                          // 保住大 SPF 瓶给下限更高的牛
        for (int i = 0; i < l; i++) {
            if (lot[i].second > 0 && lot[i].first >= p.first && lot[i].first <= p.second)
                if (best < 0 || lot[i].first < lot[best].first) best = i;
        }
        if (best >= 0) { ans++; lot[best].second--; }
    }
    cout << ans << endl;
    return 0;
}

最少会议室28

最少会议室

题目描述

\(n\) 圆会议,第 \(i\) 场在时刻 \(s_i\) 开始、\(e_i\) 结束(一个会议室同一时刻只能进行一场;一场在 \(t\) 结束、另一场在 \(t\) 开始可以共用会议室)。求最少需要多少个会议室。

输入格式

第一行一个整数 \(n\);接下来 \(n\) 行每行两个整数 \(s_i, e_i\)

输出格式

一个整数,为最少会议室数。

样例输入

1
2
3
4
3
1 4
2 5
7 9

样例输出

2

([1,4] 与 [2,5] 重叠,需要两间;[7,9] 开始时前两场都已结束,可复用房间。)

数据范围

  • \(1 \le n \le 10^5\)
  • \(0 \le s_i < e_i \le 10^9\)

⬇ 点击下载数据包

最少会议室 AC 代码

解题思路

答案 = 同时在进行的会议数的峰值。把所有开始时刻、结束时刻各自排序,双指针扫描:开始时刻更早就新增一间,否则先释放一间,过程中记录峰值。另一个等价理解是事件扫:开始记 \(+1\)、结束记 \(-1\),前缀和的最大值就是答案。与「移走比赛」对照着体会:重叠结构一样,一个删活动、一个加房间。

AC代码

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

int main() {
    int n;
    cin >> n;
    vector<long long> st(n), en(n);
    for (int i = 0; i < n; i++) cin >> st[i] >> en[i];
    sort(st.begin(), st.end());
    sort(en.begin(), en.end());
    // 开始时刻升序、结束时刻升序各排一遍,双指针数「同时在开」的峰值
    int rooms = 0, best = 0, i = 0, j = 0;
    while (i < n) {
        if (st[i] < en[j]) { rooms++; best = max(best, rooms); i++; }
        else { rooms--; j++; }   // 有的会议结束了,腾出一个房间
    }
    cout << best << endl;
    return 0;
}

扫描 · 配对 · 综合实战

均分纸牌16

均分纸牌

题目描述

\(N\) 堆纸牌,第 \(i\) 堆有 \(A_i\) 张,总数是 \(N\) 的倍数。只能在相邻堆之间移动纸牌,求最少移动多少次使每堆张数相同。

输入格式

第一行一个整数 \(N\);第二行 \(N\) 个整数 \(A_i\)

输出格式

一个整数,为最少移动次数。

样例输入

4
9 8 17 6

样例输出

3

数据范围

  • \(1 \le N \le 100\)\(1 \le A_i \le 10000\)
均分纸牌 AC 代码

解题思路

扫描贪心:从左往右扫,当前堆不等于平均值就必须移一次(多了往右送、缺了从右拿,都可以在一次移动里完成),把盈亏累加给下一堆。相邻移动的限制决定了「能一次结算就一次结算」最优。

AC代码

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

// P1031 均分纸牌:从左往右扫,当前堆不平就必须移一次,多退少补给下一堆
int main() {
    int n;
    cin >> n;
    vector<long long> a(n);
    long long sum = 0;
    for (auto& x : a) { cin >> x; sum += x; }
    long long avg = sum / n, cnt = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] != avg) cnt++;
        a[i + 1] += a[i] - avg;
    }
    cout << cnt << endl;
    return 0;
}

最大子段和29

最大子段和

题目描述

给定长度为 \(n\) 的整数序列(可能有负数),选出非空连续的一段,使其和最大。求这个最大和。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数 \(a_i\)

输出格式

一个整数,为最大子段和。

样例输入

9
-2 1 -3 4 -1 2 1 -5 4

样例输出

6

(和最大的段是 4 −1 2 1,总和为 6。)

数据范围

  • \(1 \le n \le 10^5\)
  • \(|a_i| \le 10^4\)

⬇ 点击下载数据包

最大子段和 AC 代码

解题思路

贪心视角一遍扫:维护「以当前位置结尾的最大子段和」cur——累加上 \(a_i\) 后若 cur < 0,这段负前缀接到任何后面子段上都只会拖累,果断清零重来;过程中记录全局最大 best。注意 best 初值要设成极小(或第一个元素),全负序列时答案是最小的那个负数。这题在 L28 会再见到 DP 视角,两种做法殊途同归。

AC代码

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

int main() {
    int n;
    cin >> n;
    long long cur = 0, best = LLONG_MIN;
    for (int i = 1; i <= n; i++) {
        long long x;
        cin >> x;
        cur += x;
        best = max(best, cur);
        if (cur < 0) cur = 0;   // 负前缀只会拖累后面的子段,果断舍弃
    }
    cout << best << endl;
    return 0;
}

跳跃游戏30

跳跃游戏

题目描述

队列游戏:\(n\) 个格子排成一行(从 1 编号),你在格子 1,站在格子 \(i\) 上最多向右跳 \(a_i\) 格(可以跳更少)。判断能否恰好到达格子 \(n\),能输出 YES,否则输出 NO

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数 \(a_i\)

输出格式

YESNO

样例输入

5
2 3 1 0 4

样例输出

YES

数据范围

  • \(1 \le n \le 10^5\)
  • \(0 \le a_i \le 10^5\)

⬇ 点击下载数据包

跳跃游戏 AC 代码

解题思路

从左到右维护最远可达位置 far:扫到格子 \(i\) 时若 \(i > far\),说明 \(i\) 根本到不了,后面全部到不了,直接判 NO;否则用 \(i + a_i\) 更新 far,一旦 far >= n 就是 YES。每个格子只看一眼,\(O(n)\)——「不可达即整体不可达」的单调性是正确性的来源。

AC代码

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

int main() {
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];
    int far = 1;               // 目前能到达的最远位置
    bool ok = false;
    for (int i = 1; i <= n; i++) {
        if (i > far) break;    // 这个位置根本到不了
        far = max(far, i + a[i]);
        if (far >= n) { ok = true; break; }
    }
    cout << (ok ? "YES" : "NO") << endl;
    return 0;
}

独木桥17

独木桥

题目描述

长度为 \(L\) 的独木桥上坐标 \(1 \sim L\) 处有 \(N\) 个士兵,速度均为 1。士兵初始朝向未知,两个士兵相遇时会各自转身(转身不耗时)。求所有士兵全部离开独木桥(到达 0 或 \(L+1\))的最短可能时间最长可能时间

输入格式

第一行一个整数 \(L\);第二行一个整数 \(N\);第三行 \(N\) 个整数,为士兵初始坐标。

输出格式

一行两个整数,分别是最短时间与最长时间,用空格分隔。

样例输入

1
2
3
4
2
1 3

样例输出

2 4

数据范围

  • \(1 \le L \le 5000\)\(0 \le N \le 5000\),初始坐标互不相同且 \(N \le L\)
独木桥 AC 代码

解题思路

思维转化:两个士兵相遇掉头,等价于互相穿过(继续走原来的方向)——队伍里「谁是谁」根本不重要,只看每个位置有没有人出发。于是最短时间 = 每人走较近一端的最大值,最长时间 = 每人走较远一端的最大值。

AC代码

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

// P1007 独木桥:相遇掉头等价于互相穿过——每人只需选一个方向走到底
int main() {
    int L, n;
    cin >> L >> n;
    int mn = 0, mx = 0;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        mn = max(mn, min(x, L + 1 - x));       // 最短:每人走向较近一端
        mx = max(mx, max(x, L + 1 - x));       // 最长:每人走向较远一端
    }
    cout << mn << " " << mx << endl;
    return 0;
}

公路18

公路

题目描述

公路上有 \(n\) 个站点,站点 \(i\)\(i+1\) 相距 \(v_i\) 公里;站点 \(i\) 的油价为 \(a_i\) 元/升(每个站点只卖整数升)。每升油可行驶 \(d\) 公里。小苞从站点 1 空箱出发开到站点 \(n\),油箱足够大。求最少加油花费。

输入格式

第一行两个整数 \(n, d\);第二行 \(n-1\) 个整数 \(v_i\);第三行 \(n\) 个整数 \(a_i\)

输出格式

一个整数,为最小花费。

样例输入

1
2
3
5 4
10 10 10 10
9 8 9 6 5

样例输出

79

(在站点 1 买 3 升、站点 2 买 5 升、站点 4 买 2 升。)

数据范围

  • \(1 \le n \le 10^5\)\(1 \le d, v_i, a_i \le 10^5\)
公路 AC 代码

解题思路

贪心策略:变量 curMin 记录到当前站点为止的最低油价;每一段路都在「历史最低价」把油补足到能走完这一段(整数升向上取整,油可以带余)——贵价站永远不买油,一定不劣。一遍扫描 \(O(n)\),只用变量与取整。

AC代码

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

// P9749 公路(主解·课内做法):维护至今最低油价,每段路在最低价站补足整升
int main() {
    int n;
    long long d;
    cin >> n >> d;
    vector<long long> v(n), a(n);
    for (int i = 1; i < n; i++) cin >> v[i];
    for (int i = 0; i < n; i++) cin >> a[i];
    long long cost = 0, remain = 0;            // remain:现有油还能走的公里数
    long long curMin = 4e18;
    for (int i = 1; i < n; i++) {
        curMin = min(curMin, a[i - 1]);        // 站点 1..i 中的最低油价
        long long buy = max(0LL, (v[i] - remain + d - 1) / d);   // 补足本段,向上取整
        cost += buy * curMin;
        remain += buy * d - v[i];
    }
    cout << cost << endl;
    return 0;
}
公路 补充解法:单调栈跳到下一个更便宜站点(可过 100% 数据)

解题思路

进阶写法:用单调栈从右往左预处理 nxt[i] = i 之后第一个油价更低的站点,买油时直接跳到那里按前缀和一次性买够。总花费与主解完全相同,但要用到 J-L25 之后的「单调栈」技巧(S-L03),作拓展视野。

AC代码

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

// P9749 公路(补充解法·单调栈):直接跳到下一个更便宜的站点一次性买够
int main() {
    int n;
    long long d;
    cin >> n >> d;
    vector<long long> v(n), a(n), pre(n, 0);
    for (int i = 1; i < n; i++) { cin >> v[i]; pre[i] = pre[i - 1] + v[i]; }
    for (int i = 0; i < n; i++) cin >> a[i];
    vector<int> nxt(n, n - 1);                 // nxt[i]:i 之后第一个油价更低的站点
    {
        vector<int> st;
        for (int i = n - 1; i >= 0; i--) {
            while (!st.empty() && a[st.back()] >= a[i]) st.pop_back();
            nxt[i] = st.empty() ? n - 1 : st.back();
            st.push_back(i);
        }
    }
    long long cost = 0, remain = 0;
    int i = 0;
    while (i < n - 1) {
        int j = nxt[i];
        long long need = pre[j] - pre[i];
        long long buy = max(0LL, (need - remain + d - 1) / d);
        cost += buy * a[i];
        remain += buy * d - need;
        i = j;
    }
    cout << cost << endl;
    return 0;
}

田忌赛马19

田忌赛马

题目描述

田忌和齐王各有 \(n\) 匹马进行 \(n\) 场比赛,每匹马恰好出场一次,对阵由田忌决定。速度高的马获胜,速度相同为平局(双方都不得分)。田忌每胜一场得 1 分,输了和平局都不得分。已知双方每匹马的速度,求田忌最多能得多少分。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数,为田忌各匹马的速度;第三行 \(n\) 个整数,为齐王各匹马的速度。

输出格式

一个整数,为田忌最多能得的分数。

样例输入

1
2
3
3
8 5 2
9 6 3

样例输出

2

(用 2 号马消耗齐王最快的 9,剩下 5 胜 3、8 胜 6,得 2 分。)

数据范围

  • \(1 \le n \le 10^5\),速度为不超过 \(10^9\) 的正整数

⬇ 点击下载数据包

田忌赛马 AC 代码

解题思路

排序 + 双指针:双方都按速度升序。我的最慢马若能赢对方最慢马,就稳稳赢下这一场;赢不动就拿它去消耗对方最快的马——「最慢的代价换掉最大的威胁」。与「正版田忌」的区别在平局规则:本题平局不得分也不扣分。

AC代码

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

// 田忌赛马:排序 + 双指针。赢不了时用最慢的马消耗对方最快的马
int main() {
    int n;
    cin >> n;
    vector<long long> a(n), b(n);
    for (auto& x : a) cin >> x;
    for (auto& x : b) cin >> x;
    sort(a.begin(), a.end());
    sort(b.begin(), b.end());
    int win = 0;
    int i = 0, j = 0, k = n - 1;            // i 我的慢马;j 对方慢马;k 对方最快
    while (i < n && j <= k) {
        if (a[i] > b[j]) { win++; i++; j++; }   // 我最慢的马也吃得动对方最慢的:稳赢一场
        else { i++; k--; }                       // 吃不动:拿它消耗对方最快的马
    }
    cout << win << endl;
    return 0;
}

公交换乘20

公共交通换乘

题目描述

B 市地铁换乘优惠规则:乘一次地铁获得一张优惠票,有效期为 45 分钟(\(t_{bus} - t_{subway} \le 45\)),有效期内可免费搭乘一次票价不超过地铁票价的公交车;优惠票可累积;乘公交时能用优惠票一定用,且优先消耗获得最早的满足条件的票。给定 \(n\) 条按时间升序的乘车记录(0 为地铁、1 为公交),求总花费。

输入格式

第一行一个整数 \(n\);接下来 \(n\) 行每行三个整数 \(type, price, t\),分别表示交通工具(0 地铁 / 1 公交)、票价与开始时刻(分钟)。

输出格式

一个整数,为总花费。

样例输入

1
2
3
4
5
6
7
6
0 10 3
1 5 46
0 12 50
1 3 96
0 5 110
1 6 135

样例输出

36

数据范围

  • \(1 \le n \le 10^5\)\(t_i \le 10^9\)\(1 \le price_i \le 1000\)
  • 记录按开始时刻升序给出,同一分钟不会有多条记录
公交换乘 AC 代码

解题思路

队列模拟 × 贪心综合(L17 队列回归):地铁票按获得时间先进先出排队;乘公交时先从队首清掉过期票(超过 45 分钟),再找队里第一张票价足够的票消耗——「最早的先用」由题面钦定,恰好就是队列序。注意 45 分钟窗口内至多 45 张票,扫描开销很小。

AC代码

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

// P5661 公交换乘:地铁票按时间排队;公交用「最早获得且够价」的票
int main() {
    int n;
    cin >> n;
    vector<pair<int, int>> tk;                 // 未用优惠票 (票价, 时刻),时间升序
    int head = 0;
    long long cost = 0;
    for (int r = 0; r < n; r++) {
        int type, price, t;
        cin >> type >> price >> t;
        if (type == 0) {
            cost += price;                      // 地铁必须花钱
            tk.push_back({price, t});
        } else {
            while (head < (int)tk.size() && t - tk[head].second > 45) head++;  // 队首过期
            bool used = false;
            for (int i = head; i < (int)tk.size(); i++) {
                if (t - tk[i].second > 45) break;                               // 后面只会更晚
                if (tk[i].first >= price) {                                     // 最早且够价
                    used = true;
                    tk.erase(tk.begin() + i);
                    break;
                }
            }
            if (!used) cost += price;
        }
    }
    cout << cost << endl;
    return 0;
}

合并果子21

合并果子

题目描述

果园里有 \(n\) 堆果子,第 \(i\) 堆有 \(a_i\) 个。每次可以把任意两堆合并成一堆,消耗的体力等于两堆数目之和。把所有果子合成一堆,求消耗体力的最小值。

输入格式

第一行一个整数 \(n\);第二行 \(n\) 个整数 \(a_i\)

输出格式

一个整数,为最小体力耗费。

样例输入

3
1 2 9

样例输出

15

数据范围

  • \(1 \le n \le 10^4\)\(1 \le a_i \le 2 \times 10^4\)
合并果子 AC 代码

解题思路

每次合并最小的两堆(先合并的被算多次,小的该早合并——哈夫曼思想)。主解用「排序 + 两个队列」:原始堆升序一队、新合成堆一队(天然保持升序),每轮从两队队首取最小两个,完全用 L17 已学的队列实现,不需要优先队列。

AC代码

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

// P1090 合并果子:每次合并最小的两堆。排序 + 两个队列(原始堆 / 新合成堆),免去优先队列
int main() {
    int n;
    cin >> n;
    vector<long long> a(n);
    for (auto& x : a) cin >> x;
    sort(a.begin(), a.end());
    queue<long long> q1, q2;                   // q1 原始堆升序;q2 新合成堆也升序
    for (long long x : a) q1.push(x);
    auto takeMin = [&]() {
        bool useQ1 = !q1.empty() && (q2.empty() || q1.front() <= q2.front());
        long long v;
        if (useQ1) { v = q1.front(); q1.pop(); }
        else { v = q2.front(); q2.pop(); }
        return v;
    };
    long long cost = 0;
    for (int r = 1; r < n; r++) {
        long long x = takeMin(), y = takeMin();
        cost += x + y;
        q2.push(x + y);
    }
    cout << cost << endl;
    return 0;
}
合并果子 补充解法:小根堆 priority_queue(可过 100% 数据)

解题思路

标准竞赛写法:把所有堆放进小根堆,每次弹出两个最小的合并,再把和放回堆里,直到只剩一堆。比「排序 + 两队列」更直观,但要用到优先队列(堆,S 侧数据结构内容),作拓展视野。

AC代码

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

// P1090 合并果子(补充解法·小根堆):priority_queue 每次弹最小两堆
int main() {
    int n;
    cin >> n;
    priority_queue<long long, vector<long long>, greater<>> pq;   // 小根堆
    for (int i = 0; i < n; i++) {
        long long w;
        cin >> w;
        pq.push(w);
    }
    long long cost = 0;
    while (pq.size() > 1) {
        long long a = pq.top(); pq.pop();
        long long b = pq.top(); pq.pop();
        cost += a + b;                         // 每次合并代价 = 两堆之和
        pq.push(a + b);
    }
    cout << cost << "\n";
    return 0;
}

  1. 找零·规范币制 · 经典模板题(自拟)。 

  2. 找零·贪心失效 · 经典辨析题(自拟)。 

  3. 排序贪心 · 洛谷 P1208 · USACO。 

  4. 分数背包 · 洛谷 P2240。 

  5. 过滤 + 排序贪心 · 洛谷 P1478。 

  6. 顺序装箱 · 洛谷 P1181。 

  7. 短作业优先 · 洛谷 P1223。 

  8. 排序交替贪心 · 洛谷 P4995。 

  9. 行列独立贪心 · 洛谷 P1056 · NOIP2008。 

  10. 邻位删除贪心 · 洛谷 P1106。 

  11. 双指针配对 · 洛谷 P1094 · NOIP2007。 

  12. 活动选择 · 洛谷 P1803。 

  13. 区间覆盖 · 经典模板题(自拟)。 

  14. 缺口贪心 · 洛谷 P1209 · USACO。 

  15. 排序匹配贪心 · 洛谷 P2887 · USACO。 

  16. 扫描贪心 · 洛谷 P1031 · NOIP2002。 

  17. 相遇转化 · 洛谷 P1007。 

  18. 油价贪心 · 洛谷 P9749 · CSP-J 2023。 

  19. 双指针配对 · 经典模型题(自拟)。 

  20. 队列 × 贪心综合 · 洛谷 P5661 · CSP-J 2019。 

  21. 合并最小两堆 · 洛谷 P1090 · NOIP2004。 

  22. 排序累加贪心 · 经典入门题(自拟)。 

  23. 中位数选址 · 经典模板题(自拟)。 

  24. 字符串比较排序 · 经典模板题(自拟)。 

  25. 邻位贪心 · 删数问题镜像 · 经典模板题(自拟)。 

  26. 最少选点覆盖区间 · 经典模板题(自拟)。 

  27. 活动选择镜像 · 经典模板题(自拟)。 

  28. 排序双扫描 · 经典模板题(自拟)。 

  29. 负前缀舍弃 · 经典模板题(自拟)。 

  30. 最远可达扫描 · 经典模板题(自拟)。