蓝桥杯算法精讲:贪心算法之推公式例题深度剖析

目录

  • 前言
  • 一、贪心算法
    • [1.1 推公式](#1.1 推公式)
      • [1.1.1 拼数](#1.1.1 拼数)
      • [1.1.2 保卫家园](#1.1.2 保卫家园)
      • [1.1.3 奶牛玩杂技](#1.1.3 奶牛玩杂技)
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、贪心算法

1.1 推公式

如果细说的话,这个专题应该叫推公式+排序 。其中推公式就是寻找排序规则 ,排序就是在该排序规则下对整个对象排序

在解决某些问题的时,当我们发现最终结果需要调整每个对象的先后顺序,也就是对整个对象排序时,那么我们就可以用推公式的方式,得出我们的排序规则,进而对整个对象排序。

正确性证明:

利用排序解决问题,最重要的就是需要证明"在新的排序规则下,整个集合可以排序"。这需要用到离散数学中"全序关系"的知识。我会在第一道题中证明该题的排序规则下,整个集合是可以排序的。

但是证明过程很麻烦,后续题目中我们只要发现该题最终结果需要排序,并且交换相邻两个元素的时候,对其余元素不会产生影响,那么我们就可以推导出排序的规则,然后直接去排序,就不去证明了。

1.1.1 拼数

拼数

在该题目中,数据范围已经达到了109,并且拼接完之后数是逐渐变长的,用int是存不下最终的结果,因此这里用字符串把所有的数读进来,最终把这个字符串拼起来输出即可,这样也不用写高精度的计算了

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 25;
int n;
string a[N];

bool cmp(string& x, string& y)
{
    return x + y > y + x;
}

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

    // 排序
    sort(a + 1, a + 1 + n, cmp);

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

    return 0;
}

补充要点:
一、字符串字典序的基础比较规则

字符串的字典序比较,本质是逐位对比字符的 ASCII 码值,步骤如下:

  1. 从左到右依次比较两个字符串的每一位字符。
  2. 若某一位字符不同,字符更大的字符串整体字典序更大
  3. 若一个字符串是另一个的前缀(如 "53" 和 "534"),则较短的字符串字典序更小(如 "53" < "534")。

二、为什么字符串 + 能直接拼接?

C++ 的标准库为 std::string 类重载了 + 运算符,它的行为和普通数值(int/float)的 + 完全不同:

  • 数值类型 (如 int a=123, b=456):a + b 是数值相加,结果是 579;
  • 字符串类型 (如 string a="123", b="456"):a + b 是字符串拼接,结果是 "123456"。

这个重载是 C++ 为字符串专门设计的语法糖,目的就是让字符串拼接操作更直观、更简洁。

字符串拼接后直接比较大小等价于数值大小比较

三、C++ sort 函数和自定义比较器的核心工作原理

C++ 的 sort 函数是一个通用排序工具,它本身不知道你想按 "从小到大""从大到小" 还是 "拼接更大" 排序 ------ 它完全依赖你传入的 cmp 函数来判断:

  • cmp (a, b) 返回 true → 表示 a 应该排在 b 的前面;
  • cmp (a, b) 返回 false → 表示 a 不应该排在 b 前面(即 b 应该排在 a 前面)

这个规则是所有自定义排序的核心,cmp 函数的唯一作用就是给 sort 提供 "顺序判断依据",而实际的元素交换、排序逻辑都由 sort 内部完成(比如快排 / 归并算法),cmp 只负责 "投票"。

结合实际情况来说:
情况 1:x + y > y + x → 返回 true

比如 x="343",y="312":x+y="343312" > y+x="312343" → cmp(x,y)=true;

sort 收到 "true" 的结果,就知道 "x 应该在 y 前面",于是会把 x 放在 y 的前面(如果原本顺序不对,sort 会自动交换)。

情况 2:x + y < y + x → 返回 false

比如 x="13",y="312":x+y="13312" < y+x="31213" → cmp(x,y)=false;

sort 收到 "false" 的结果,就知道 "x 不应该在 y 前面",于是会把 y 放在 x 的前面。

情况 3:x + y = y + x → 返回 false

比如 x="12",y="12",此时 cmp 返回 false,sort 认为两者顺序无关紧要(谁前谁后拼接结果都一样)。

1.1.2 保卫家园

保卫家园


一、题目本质与核心思路

这是一道贪心排序类算法题 ,核心目标是通过最优赶牛顺序,让奶牛吃掉的总花数最少。

  • 每赶一头牛,来回需要 2×Ti 分钟,这段时间里其他奶牛会继续吃花。
  • 问题本质:确定赶牛顺序,使得「每头牛在被赶回前的存活时间 × 它的吃花速度 Di」的总和最小。

二、算法原理:相邻交换法推导排序规则

相邻交换法 找到最优排序规则:
1. 问题简化

假设我们有两个相邻的奶牛 i(Ti​,Di​)和 j(Tj​,Dj​),交换它们的顺序不会影响:

  • 前面已处理奶牛的时间(已经固定)
  • 后面奶牛的总时间(2Ti+2Tj 交换后总和不变)因此只需比较交换 i 和 j 前后,这两头牛对总花数的贡献差异。

2. 贡献计算

  • 情况 1:i 在前,j 在后

    总贡献 = T×Di​+(T+2Ti​)×Dj​(T 是处理 i,j 前已用的时间)

  • 情况 2:j 在前,i 在后

    总贡献 = T×Dj​+(T+2Tj​)×Di​

3. 推导排序规则

如果 i 在 j 前比较好,对应的 sum1 应该小于 sum2:
T×Di​+(T+2Ti​)×Dj​ < T×Dj​+(T+2Tj​)×Di​消去相同项 T(Di​+Dj​) 后化简:2Ti​Dj ​< 2Tj​Di​Ti​×Dj​<Tj​×Di​这就是最优排序规则

Ti​×Dj​<Tj​×Di​ 时,i 应该排在 j 前面。

cpp 复制代码
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1e5 + 10;
typedef long long LL;
LL n;

struct node{
	LL t;
	LL d;
}a[N];

bool cmp(node& x, node& y)
{
	return x.t * y.d < y.t * x.d;
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> a[i].t >> a[i].d;
	}
	sort(a + 1, a + 1 + n, cmp);
	// ret:总花数,t:当前已用时间
	LL ret = 0, t = 0;
	for(int i = 1; i <= n; i++)
	{
		// 当前牛在被赶前,吃了t分钟花
		ret += a[i].d * t;
		// 赶这头牛的来回时间,累加到总时间
		t += 2 * a[i].t;
	}
	cout << ret << endl;
	return 0;
}

三、样例验证

样例输入最优顺序:6→2→3→4→1→5

对应数据:

6: t=1,d=6

2: t=2,d=5

3: t=2,d=3

4: t=3,d=2

1: t=3,d=1

5: t=4,d=1

1.1.3 奶牛玩杂技

奶牛玩杂技


一、题目要求

我们需要将所有牛竖直堆叠 成一列,每头牛的 "压扁指数" 定义为:它上方所有牛的总重量(不包含自身重量)减去它的力量 si​。

我们的目标是:找到一种堆叠顺序,使得所有牛的压扁指数的最大值尽可能小,最终输出这个最小的最大值。

二、贪心策略推导
1. 局部最优分析

考虑任意两头牛 i 和 j,比较两种堆叠顺序:

  • 情况 1:i 在上,j 在下
  • 情况 2:j 在上,i 在下

设它们上方已有总重量为 W 的牛(这部分不影响两者相对顺序的比较)。

情况 1(i 在上,j 在下)

  • i 的压扁指数:W−Si
  • j 的压扁指数:W+Wi−Sj
  • 两者最大值:max(W−Si, W+Wi−Sj)

情况 2(j 在上,i 在下)

  • j 的压扁指数:W−Sj
  • i 的压扁指数:W+Wj−Si
  • 两者最大值:max(W−Sj, W+Wj−Si)

当i 应该放在 j 上方时:我们要让情况 1 的最大值 ≤ 情况 2 的最大值,即:max(W−Si​, W+Wi​−Sj​)≤max(W−Sj​, W+Wj​−Si​)

2. 不等式化简

两边同时减去 W,不等式方向不变:max(−Si​, Wi​−Sj​)≤max(−Sj​, Wj​−Si​)

进一步推导可得:当 Wi​+Si​<Wj​+Sj​ 时,上述不等式成立 → i 应该放在 j 上方。

3. 全局策略

将所有牛按 w+s 升序排列(w+s 小的在上,大的在下),即可得到全局最优解。

cpp 复制代码
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 5e4 + 10;
typedef long long LL;
LL n;

struct node{
	LL w;
	LL s;
}a[N];

bool cmp(node& x, node& y)
{
	//个人感觉不化简最好,少些操作不容易犯错 
	return max(-x.s, x.w - y.s) < max(y.w - x.s, -y.s);
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i].w >> a[i].s;
	sort(a + 1, a + 1 + n, cmp);
	LL ret = -1e9 - 10;
	//记录当前牛上方所有牛的总重量(初始为 0,因为最上面的牛上方没有牛)
	LL wight = 0;
	for(int i = 1; i <= n; i++)
	{
		//计算当前牛的压扁指数,更新最大值 
		ret = max(ret, wight - a[i].s);
		//将当前牛的重量加入总重量 
		wight += a[i].w;
	}
	cout << ret << endl;
	return 0;
}

补充要点:
ret 初始值为什么是 - 1e9-10?

压扁指数可能为负数(比如力量很大的牛),初始值需要足够小,确保第一个牛的压扁指数能覆盖它;

如果初始值为 0,会导致负数的压扁指数无法更新 ret,最终答案错误。


结语

相关推荐
客卿1232 小时前
力扣--组合,子集--回溯法的再探索--总结回溯法
java·算法·leetcode
_日拱一卒2 小时前
LeetCode(力扣):环形链表
算法·leetcode·链表
做怪小疯子2 小时前
Leetcode刷题——链表就地反转
算法·leetcode·链表
仟濹2 小时前
【算法打卡day22(2026-03-14 周六)今日算法or技巧:双指针 & 链表】9个题
数据结构·算法·链表·双指针
RechoYit2 小时前
数学建模——评价与决策类模型
python·算法·数学建模·数据分析
地平线开发者2 小时前
地平线 Sparse 多任务参考算法 SparseBevFusionMultitaskOE-V1.0
算法·自动驾驶
OKkankan3 小时前
红黑树的原理及实现
开发语言·数据结构·c++·算法
Jasmine_llq3 小时前
《B3953 [GESP202403 一级] 找因数》
算法·因数枚举算法(核心逻辑)·顺序遍历算法·单输入处理·逐行输出处理·整数算术运算
我是伪码农3 小时前
16届蓝桥杯
职场和发展·蓝桥杯