当前位置: 首页 > news >正文

算法基础篇:(八)贪心算法之简单贪心:从直觉到逻辑的实战指南

目录

前言

一、贪心算法是什么?—— 不止是 “鼠目寸光”

1.1 贪心的核心思想

1.2 贪心算法的三大特点

1.3 学习贪心的正确姿势

二、经典例题实战 —— 简单贪心的 5 个核心场景

例题 1:货仓选址(洛谷 P10452)—— 中位数的魔力

题目描述

题目分析

贪心策略

逻辑证明(反证法)

代码实现

拓展结论

例题 2:最大子段和(洛谷 P1115)—— 果断舍弃 “负资产”

题目描述

题目分析

贪心策略

逻辑证明(反证法)

代码实现

注意点

例题 3:纪念品分组(洛谷 P1094)—— 双指针的 “配对艺术”

题目描述

题目分析

贪心策略

逻辑证明(交换论证法)

代码实现

优化点

例题 4:排座椅(洛谷 P1056)—— 贪心的 “优先级选择”

题目描述

题目分析

贪心策略

逻辑证明

代码实现

注意点

例题 5:矩阵消除游戏(牛客网)—— 贪心 + 枚举的 “组合拳”

题目描述

题目分析

贪心策略

逻辑证明

代码实现

优化点

三、简单贪心的常见策略总结

1. 排序 + 选择类

2. 收益最大化类

3. 舍弃负资产类

4. 枚举 + 贪心类

四、贪心算法的常见误区与避坑指南

1. 误区 1:认为 “贪心就是选最大 / 最小”

2. 误区 2:不证明策略正确性,直接编码

3. 误区 3:忽视边界情况

4. 避坑指南

总结


前言

        在算法的世界里,有一类算法如同生活中的 “即时决策派”—— 不纠结于未来的所有可能性,只专注于当下的最优选择,这就是贪心算法。它看似简单直观,甚至有些 “鲁莽”,却能高效解决一大批经典问题。但贪心算法也是出了名的 “两极分化”:简单题能让你觉得 “理所当然”,难题却能让你怀疑人生。今天我们就聚焦贪心算法的入门核心 —— 简单贪心,通过经典例题拆解其思想、逻辑证明和代码实现,帮你从 “知其然” 到 “知其所以然”,真正掌握这份 “当下最优” 的智慧。下面就让我们正式开始吧!


一、贪心算法是什么?—— 不止是 “鼠目寸光”

1.1 贪心的核心思想

        贪心算法,本质上是一种 “局部最优导向全局最优” 的策略。它将复杂问题拆分成一系列连续的子问题,在每个子问题中都做出当前看起来最优的选择,并且不回溯、不反悔,最终希望通过这些局部最优解的累积得到全局最优解。

        举个生活中的例子:你要从家到公司,想最快到达。贪心策略就是:每个路口都选择当前路况最好、距离最近的路线,而不是提前规划全程所有可能路径 —— 这种 “走一步看一步” 的决策方式,正是贪心的核心。

        但这里有个关键前提:并非所有问题都能用贪心解决。只有当问题满足 “局部最优解的累积必然导致全局最优解” 时,贪心才有效。这也是贪心算法的难点所在:策略的提出不难,难的是证明它是正确的

1.2 贪心算法的三大特点

  1. 无后效性:每个子问题的决策只依赖当前状态,不影响之前的决策,也不被之前的决策所影响。
  2. 贪心选择性质:全局最优解可以通过一系列局部最优解(贪心选择)得到。
  3. 无固定模板:与动态规划有明确的状态转移方程不同,贪心策略需要根据具体问题场景设计,没有通用的套路可循。

1.3 学习贪心的正确姿势

        很多初学者会有困惑:为什么做了几十道贪心题,遇到新题还是没思路?其实这是正常现象!贪心的灵活性决定了它需要大量的题型积累和逻辑训练。学习时要注意以下几点:

  • 前期重点吸收各种题型的贪心策略,把它们当成 “经验库”;
  • 尽量尝试证明策略的正确性(常用反证法、数学归纳法、交换论证法),培养严谨思维;
  • 比赛中无需过度纠结证明:如果策略能通过几个边界情况,就可以大胆尝试编码。

二、经典例题实战 —— 简单贪心的 5 个核心场景

        下面我们通过 5 道经典例题,拆解简单贪心的常见场景、策略设计和代码实现。每道题都会从 “题目分析→贪心策略→逻辑证明→代码实现” 四个维度展开,帮你吃透核心逻辑。

例题 1:货仓选址(洛谷 P10452)—— 中位数的魔力

题目链接:https://www.luogu.com.cn/problem/P10452

题目描述

        在一条数轴上有 n 家商店,坐标分别为 a₁, a₂, ..., aₙ。现在要在数轴上建立一家货仓,求货仓建在何处,能使货仓到所有商店的距离之和最小。

        输入:第一行 n,第二行 n 个整数表示商店坐标(n≤1e5,aᵢ≤1e6)

        输出:距离之和的最小值

        示例输入:46 2 9 1

        示例输出:12

题目分析

        这是一道典型的 “一维选址” 问题。直观上,我们可能会想到 “平均值”—— 毕竟平均值是数据的中心。但实际上,中位数才是最优解。为什么?

贪心策略

  1. 将所有商店坐标从小到大排序;
  2. 货仓建在中位数位置:
    • 若 n 为奇数,选第 (n+1)/2 个位置;
    • 若 n 为偶数,选第 n/2 到第 n/2+1 个位置之间的任意点(距离和相同)。

逻辑证明(反证法)

        假设货仓建在非中位数位置 x,中位数为 m。我们证明将 x 移到 m 会使距离和减小:

        设排序后的坐标为 a₁≤a₂≤...≤aₙ,中位数 m=a [k](k=(n+1)/2)

  • x>m:此时在 m 左侧有 k 家商店,右侧有 n-k 家商店(n-k≤k)。将 x 向左移动 1 单位,左侧商店的总距离增加 1×k,右侧商店的总距离减少 1×(n-k),总距离变化为 k-(n-k)=2k-n≥0(因为 k≥n/2),即总距离减小或不变
  • x<m:同理,将 x 向右移动 1 单位,总距离会减小或不变

        因此,只有当 x=m 时,距离和达到最小值

代码实现

#include <iostream>
#include <algorithm>
using namespace std;typedef long long LL;
const int N = 1e5 + 10;
int n;
LL a[N];int main() {cin >> n;for (int i = 1; i <= n; i++) cin >> a[i];sort(a + 1, a + 1 + n);LL ret = 0;// 利用结论计算:(a[n]-a[1]) + (a[n-1]-a[2]) + ... for (int i = 1; i <= n / 2; i++) {ret += a[n - i + 1] - a[i];}cout << ret << endl;return 0;
}

拓展结论

        对于公式 sum=\sum |a [i]-x|(i=1 到 n),当 x 取数组的中位数时,sum 取得最小值。这个结论在后续的贪心、动态规划问题中会频繁用到,一定要牢记!

例题 2:最大子段和(洛谷 P1115)—— 果断舍弃 “负资产”

题目链接:https://www.luogu.com.cn/problem/P1115

题目描述

        给出一个长度为 n 的序列 a,选出其中连续且非空的一段,使得这段的和最大。

        输入:第一行 n,第二行 n 个整数(1≤n≤2e5,-1e4≤aᵢ≤1e4)

        输出:最大子段和

        示例输入:72 -4 3 -1 2 -4 3

        示例输出:4(对应子段 [3,-1,2])

题目分析

        这道题是贪心算法的经典应用,也是面试高频题。核心矛盾是:当当前累积和为负数时,是否要舍弃前面的子段?

贪心策略

        从前往后遍历序列,维护一个当前累积和 sum 和最大子段和 ret

  1. 每次将当前元素加入 sum;
  2. 更新 ret 为 ret 和 sum 中的较大值
  3. sum<0:说明当前子段对后续累加毫无贡献(甚至会拖后腿),直接将 sum 重置为 0,从下一个元素重新开始累积。

逻辑证明(反证法)

        我们需要证明:当 sum<0 时,舍弃当前子段是最优选择。

        假设存在一段区间 [a,b],其和 sum [a,b]<0。若不舍弃这段区间,后续存在子段 [b+1,c],使得 sum [a,c] 是最大子段和。但 sum [a,c] = sum [a,b] + sum [b+1,c] < sum [b+1,c],这与 sum [a,c] 是最大子段和矛盾。因此,舍弃 sum<0 的子段不会错过最优解。

        进一步证明:区间内不存在比当前起点更优的起点。假设区间 [a,b] 中存在点 c(a<c≤b),使得 sum [c,b] 是更优的起点。则 sum [a,c-1] = sum [a,b] - sum [c,b] < 0,这与贪心策略中 “sum≥0 时才继续累积” 的逻辑矛盾 —— 因为如果 sum [a,c-1]<0,我们早就在 c 点重新开始累积了。因此,当前起点是最优的。

代码实现

#include <iostream>
using namespace std;typedef long long LL;
const int N = 2e5 + 10;
int n;
LL a[N];int main() {cin >> n;for (int i = 1; i <= n; i++) cin >> a[i];LL sum = 0, ret = -1e6; // ret初始化为最小值,应对全负序列for (int i = 1; i <= n; i++) {sum += a[i];ret = max(ret, sum);if (sum < 0) sum = 0; // 舍弃负资产}cout << ret << endl;return 0;
}

注意点

  • 初始时 ret 要设为极小值(如 - 1e6),而不是 0—— 因为序列可能全为负数,此时最大子段和是最大的那个负数。
  • 时间复杂度 O (n),空间复杂度 O (1),是该问题的最优解法。

例题 3:纪念品分组(洛谷 P1094)—— 双指针的 “配对艺术”

题目链接:https://www.luogu.com.cn/problem/P1094

题目描述

        元旦晚会要发放纪念品,每组最多 2 件,且每组总价格不超过 w。求最少的分组数目。

        输入:第一行 w(每组价格上限),第二行 n(纪念品总数),后续 n 行是每件纪念品的价格(1≤n≤3e4,80≤w≤200,5≤pᵢ≤w)

        输出:最少分组数

        示例输入:1009902020305060708090示例输出:6

题目分析

        要使分组数最少,核心思路是 “尽可能让每件纪念品都和其他纪念品配对”,避免单独分组。最优策略是让 “最便宜的和最贵的配对”—— 如果两者之和不超过 w,就配对;否则最贵的只能单独分组(因为它无法和任何其他纪念品配对)。

贪心策略

  1. 将所有纪念品价格从小到大排序;
  2. 用双指针 l(指向当前最便宜的)和 r(指向当前最贵的);
  3. a [l]+a [r]≤w:配对成功,l++r--,分组数 + 1;
  4. a [l]+a [r]>w:最贵的单独分组,r--,分组数 + 1;
  5. 重复直到 l>r

逻辑证明(交换论证法)

        交换论证法的核心是:假设存在最优解,通过交换其中的元素,证明最优解可以转化为贪心解,从而说明贪心解是最优的。

        假设最优解中,存在一组配对方式与贪心策略不同:

  • 情况 1:a [r](当前最贵)单独分组。贪心解中 a [r] 单独分组,最优解中 a [r] 也必须单独分组(因为没有比 a [l] 更便宜的纪念品能和它配对),两者一致。
  • 情况 2:a [l]+a [r]≤w,但最优解中 a [l] 与 a [k](k<r)配对,a [r] 与 a [m](m>l)配对。此时交换 a [k] 和 a [r],得到 (a [l]+a [r]) 和 (a [k]+a [m]),两组总和都不超过 w,分组数不变,最优解转化为贪心解。

        因此,贪心策略得到的分组数是最少的。

代码实现

#include <iostream>
#include <algorithm>
using namespace std;const int N = 3e4 + 10;
int w, n;
int a[N];int main() {cin >> w >> n;for (int i = 1; i <= n; i++) cin >> a[i];sort(a + 1, a + 1 + n);int l = 1, r = n, ret = 0;while (l <= r) {if (a[l] + a[r] <= w) {l++;r--;} else {r--;}ret++;}cout << ret << endl;return 0;
}

优化点

  • 排序后用双指针遍历,时间复杂度 O (nlogn)(主要消耗在排序),对于 n=3e4 完全适用;
  • 无需额外空间存储分组,直接通过指针移动计数,空间复杂度 O (1)

例题 4:排座椅(洛谷 P1056)—— 贪心的 “优先级选择”

题目链接:https://www.luogu.com.cn/problem/P1056

题目描述

        教室有 M 行 N 列座位,有 D 对同学会交头接耳(前后或左右相邻)。要设置 K 条横向通道和 L 条纵向通道,使交头接耳的同学对数最少。通道会隔开相邻的行或列,从而阻止这对同学交头接耳。

        输入:第一行 M、N、K、L、D,后续 D 行是交头接耳的同学坐标(2≤M,N≤1e3,0≤K<M,0≤L<N,D≤2e3)

        输出:第一行 K 个横向通道的位置,第二行 L 个纵向通道的位置(按升序排列)

        示例输入:4 5 1 2 34 2 4 32 3 3 32 5 2 4

        示例输出:22 4

题目分析

        核心观察:横向通道只影响前后相邻的同学,纵向通道只影响左右相邻的同学。因此,横向和纵向通道的设置是独立的,可以分开处理。

        对于横向通道:我们需要选择 K 个位置,使每个位置能隔开的交头接耳对数最多;纵向通道同理。这是一种 “选最大收益” 的贪心策略。

贪心策略

  1. 分别统计每个横向位置(第 i 行和第 i+1 行之间)能隔开的交头接耳对数;
  2. 分别统计每个纵向位置(第 j 列和第 j+1 列之间)能隔开的交头接耳对数;
  3. 对横向位置按 “收益”(隔开的对数)降序排序,选前 K 个;
  4. 对纵向位置按 “收益” 降序排序,选前 L 个;
  5. 将选中的位置按升序排列后输出。

逻辑证明

        由于我们选择的是收益最大的 K 个横向位置和 L 个纵向位置,不存在其他选择能隔开更多的交头接耳对数。因此,这种 “选最大收益” 的策略必然是最优的。

代码实现

#include <iostream>
#include <algorithm>
using namespace std;const int N = 1010;struct Node {int index; // 通道位置(第index行/列和index+1之间)int cnt;   // 能隔开的交头接耳对数
} row[N], col[N];// 按收益降序排序
bool cmp1(Node& x, Node& y) {return x.cnt > y.cnt;
}// 按位置升序排序
bool cmp2(Node& x, Node& y) {return x.index < y.index;
}int main() {int m, n, k, l, d;cin >> m >> n >> k >> l >> d;// 初始化:每个位置的index就是其本身for (int i = 1; i <= m; i++) row[i].index = i;for (int i = 1; i <= n; i++) col[i].index = i;while (d--) {int x1, y1, x2, y2;cin >> x1 >> y1 >> x2 >> y2;if (x1 == x2) { // 左右相邻,统计纵向通道int pos = min(y1, y2);col[pos].cnt++;} else { // 前后相邻,统计横向通道int pos = min(x1, x2);row[pos].cnt++;}}// 选择收益最大的K个横向通道和L个纵向通道sort(row + 1, row + 1 + m, cmp1);sort(col + 1, col + 1 + n, cmp1);// 按位置升序排列sort(row + 1, row + 1 + k, cmp2);sort(col + 1, col + 1 + l, cmp2);// 输出横向通道for (int i = 1; i <= k; i++) {cout << row[i].index << " ";}cout << endl;// 输出纵向通道for (int i = 1; i <= l; i++) {cout << col[i].index << " ";}cout << endl;return 0;
}

注意点

  • 通道位置是 “第 index 行 / 列和 index+1 之间”,因此统计时要取 min (x1,x2)min (y1,y2)
  • 最终输出需要按位置升序排列,符合题目要求。

例题 5:矩阵消除游戏(牛客网)—— 贪心 + 枚举的 “组合拳”

题目链接:https://ac.nowcoder.com/acm/problem/200190

题目描述

        n 行 m 列的矩阵,每个单元格有一个权值。进行 k 个回合,每个回合可选择一行或一列,将其所有单元格权值变为 0,并获得该行列的权值和。求最大得分。

        输入:第一行 n、m、k,后续 n 行 m 列整数(1≤n,m≤15,1≤aᵢⱼ≤1e6,1≤k≤n*m)

        输出:最大得分

        示例输入:3 3 2101 1 1021 202 1100 8 100

        示例输出:414(选择第一列和第三列,和为 101+1+100 + 102+1+100 = 414)

题目分析

        这道题很容易陷入 “贪心陷阱”:每次选择当前权值和最大的行或列。但这种策略是错误的 —— 因为选择一行后,会影响后续列的权值和(该行的列元素会变为 0),反之亦然。

        例如:

100  9  100  0  0
100  0  10

        当k=2 时,贪心会选第一列(和 200)和第三列(和 20),总得分 220;但最优解是选第一行(和 119)和第三行(和 110),总得分 229。

        因此,这道题需要 “贪心 + 枚举” 的组合策略:先枚举所有可能的行选择,再对列进行贪心选择。

贪心策略

  1. 枚举所有行的选择情况(因为 n≤15,总共有 2¹⁵=32768 种情况,可行);
  2. 对于每种行选择(选 t 行):
    • 计算选中行的权值和;
    • 计算剩余列的权值和(未被选中的行的列元素和);
    • 从剩余列中选择 k-t 个权值和最大的列,累加得分;
  3. 所有情况中取最大得分。

逻辑证明

        由于 n 和 m 很小,枚举所有行选择是可行的。对于每种行选择,列的选择必然是贪心选最大的 —— 因为列之间是独立的(选择一列不会影响其他列的权值和),因此 “选最大的 k-t 列” 是最优的。

代码实现

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;const int N = 20;
int n, m, k;
int a[N][N];
int col[N]; // 存储每列的权值和// 统计x的二进制中1的个数(选中的行数)
int count_one(int x) {int ret = 0;while (x) {ret++;x -= x & -x; // 消除最低位的1}return ret;
}// 按权值和降序排序
bool cmp(int a, int b) {return a > b;
}int main() {cin >> n >> m >> k;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> a[i][j];}}int max_score = 0;// 枚举所有行选择情况(st的二进制位表示是否选该行)for (int st = 0; st < (1 << n); st++) {int selected_rows = count_one(st);if (selected_rows > k) continue; // 选的行数超过k,跳过int remaining_cols = k - selected_rows; // 还能选的列数memset(col, 0, sizeof col);int score = 0;// 计算选中行的得分和剩余列的权值和for (int i = 0; i < n; i++) {if (st >> i & 1) { // 该行被选中for (int j = 0; j < m; j++) {score += a[i][j];}} else { // 该行未被选中,累加列权值和for (int j = 0; j < m; j++) {col[j] += a[i][j];}}}// 选择剩余列中权值和最大的remaining_cols个sort(col, col + m, cmp);for (int i = 0; i < remaining_cols; i++) {score += col[i];}max_score = max(max_score, score);}cout << max_score << endl;return 0;
}

优化点

  • 用二进制枚举行选择,能够高效遍历所有可能;
  • 列的权值和计算可以通过预处理优化,但由于 n 和 m 很小,直接计算也完全可行;
  • 时间复杂度 O (2ⁿ × m log m),对于 n=15、m=15,2¹⁵=32768,m log m≈50,总运算量约 1.6e6,效率很高。

三、简单贪心的常见策略总结

        通过上面的例题,我们可以总结出简单贪心的几种常见策略模板,帮你快速应对同类问题:

1. 排序 + 选择类

  • 核心思路:先排序,再根据问题选择 “最小 / 最大”、“中位数” 等关键位置。
  • 适用场景:货仓选址(中位数)、纪念品分组(双指针配对)、最大子段和(排序后贪心选择,本题未用排序但属于选择类)。
  • 技巧:排序后常用双指针、遍历等方式进行贪心选择。

2. 收益最大化类

  • 核心思路:计算每个选择的 “收益”,优先选择收益最大的。
  • 适用场景:排座椅(通道位置的收益是隔开的对数)、矩阵消除游戏(列的收益是权值和)。
  • 技巧:用数组统计收益,排序后选择前 k 个。

3. 舍弃负资产类

  • 核心思路:当当前累积的结果为负时,果断舍弃,重新开始。
  • 适用场景:最大子段和(sum<0 时重置)、股票买卖(LeetCode 121,当当前价格低于买入价时重新买入)。
  • 技巧:维护一个累积变量,实时更新最优结果,负累积时重置。

4. 枚举 + 贪心类

  • 核心思路:当直接贪心有陷阱时,枚举部分选择,再对剩余部分贪心。
  • 适用场景:矩阵消除游戏(枚举行选择,贪心选列)。
  • 技巧:枚举的维度要小(如 n≤15),否则会超时。

四、贪心算法的常见误区与避坑指南

1. 误区 1:认为 “贪心就是选最大 / 最小”

        很多初学者会误以为贪心就是简单地选最大或最小,但实际上贪心策略是根据问题场景设计的。例如货仓选址选的是中位数,而不是最大或最小;最大子段和是舍弃负累积,而不是选最大的元素。

2. 误区 2:不证明策略正确性,直接编码

        贪心算法的正确性不是直觉能保证的。例如矩阵消除游戏的 “选当前最大” 策略看似正确,实则有反例。因此,即使是简单贪心,也要尽量通过反证法、交换论证法等验证策略。

3. 误区 3:忽视边界情况

  • 全负序列的最大子段和(需初始化为极小值);
  • 纪念品分组中 n=1 的情况(必须单独分组);
  • 矩阵消除游戏中 k≥n+m 的情况(选所有行和列)。

4. 避坑指南

  • 遇到贪心问题,先尝试构造反例:如果能找到反例,说明策略错误;
  • 若无法构造反例,再尝试证明策略的正确性;
  • 编码时优先处理边界情况,再处理一般情况;
  • 对于数据量较大的问题(如 n≤1e5),优先选择 O (n) 或 O (n log n) 的贪心策略。

总结

        贪心算法是一种 “当下最优” 的决策艺术,它不需要复杂的状态转移,却能高效解决很多问题。简单贪心作为入门基础,核心在于 “排序 + 选择”、“收益最大化”、“舍弃负资产” 等策略,而难点在于策略的设计和正确性证明。

        学习贪心算法,不要急于求成,要通过大量例题积累经验,培养 “贪心直觉”。记住:贪心没有固定模板,但有通用思路 —— 将问题拆分成子问题,每个子问题做出局部最优选择,并且证明这些选择能累积成全局最优。

        当你能熟练解决简单贪心问题后,就可以进阶到更复杂的贪心问题(如哈夫曼编码、区间调度、推公式类贪心,这些我在后续的博客中会为大家详解),逐步构建完整的贪心算法知识体系。

        最后,算法学习的核心是 “理解 + 实践”。希望这篇文章能帮你打开贪心算法的大门,多动手编码、多思考证明,你会发现贪心算法的魅力所在!

http://www.dtcms.com/a/610061.html

相关文章:

  • 昊源建设监理有限公司网站外贸网站代码
  • 大专生就业:学历限制的现实考量与能力突围路径
  • Node.js 与 Docker 深度整合:轻松部署与管理 Node.js 应用
  • 中国企业500强榜单2021廊坊seo排名优化
  • 基于高光谱成像和偏最小二乘法(PLS)的苹果糖度检测MATLAB实现
  • 随访系统如何支持临床研究和数据分析?
  • idea 刷新maven,提示java.lang.RuntimeException: java.lang.OutOfMemoryError
  • 邢台本地网站vue做的pc线上网站
  • Arang Briket木炭块检测与识别:基于Mask R-CNN的精确识别方案详解
  • 怎么在百度建设一个网站工业设计大学排名前50
  • 【C++:封装红黑树】C++红黑树封装实战:从零实现MyMap与MySet
  • 构建AI智能体:九十四、Hugging Face 与 Transformers 完全指南:解锁现代 NLP 的强大力量
  • 保定网站排名哪家公司好有没一些网站只做临床药学
  • 目前做网站流行的语言网站策划书市场分析2000字
  • 18.HTTP协议(一)
  • 【每天一个AI小知识】:什么是逻辑回归?
  • Moe框架分析
  • Windows下nacos开机自启动
  • C++ 11 中的move赋值运算符
  • Java:startsWith()
  • 【Linux】进程间通信(四)消息队列、信号量与内核管理ipc资源机制
  • php整站最新版本下载html5 网站开发工具
  • wordpress更换网站数据库中国网络公司排名前十
  • 病床脚轮制动系统公差优化:CETOL 6σ建模与装配顺序重构,根治对中漂移
  • 专注网站建设与制作做网站费用多少钱
  • 潍坊网站建设哪家专业门户网站php源码
  • 【架构】安全
  • 大数据生态Sql引擎
  • 使用 .NET 8 构建 RESTful Web API
  • Servlet进阶