贪心


算法笔记:贪心算法核心模型与进阶

贪心算法的核心在于“局部最优解能否推导出全局最优解”。这组题目展示了贪心策略的几种主要推导方式:排序、数学绝对值、哈夫曼树、差分思想以及相邻交换证明法。

1. 基础模型:排队打水 (短作业优先)

来源文件排队打水.cpp

  • 🏷️ 题面抽象:$N$ 个人排队打水,每个人有不同的打水耗时。求一种排队顺序,使得所有人的总等待时间最小。
  • 💡 核心思路
  • 贪心策略:让打水快的人排在前面。因为排在前面的人的打水时间会被后面所有的人等待,耗时越短的人被放大的倍数越大,总时间越小。
  • 实现:将时间从小到大排序,然后计算前缀和,最后把所有的前缀和(即每个人的等待时间)累加即可。
点击查看代码
void solve()
{
	int n;
	cin >> n;
	vi a(n + 1);
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	sort(a.begin() + 1, a.end());

	for (int i = 1; i <= n; i++)
	{
		a[i] += a[i - 1];
	}
	int t = 0;
	for (int i = 1; i <= n; i++)
	{
		t += a[i - 1];
	}
	cout << t << endl;
}


2. 数学模型:货仓选址 (绝对值距离最小化)

来源文件货仓选址.cpp

  • 🏷️ 题面抽象:在一条数轴上有 $N$ 家商店(给出坐标),选择一个点建货仓,使得所有商店到货仓的距离之和最小。
  • 💡 核心思路
  • 贪心策略:利用绝对值不等式的性质,最优解必定是这些坐标的中位数
  • 数学推导:假设把货仓建在 $x$ 点,距离和为 $\sum |x - a_i|$。如果把 $x$ 往点密集的地方移动,距离和会减小,最终收敛于最中间的点。
  • 实现:将坐标排序,选取坐标 $t = a[(N+1)/2]$,然后遍历一遍数组累加 $|t - a_i|$ 即可。
点击查看代码
//取中位数 就是 最优解
// 开绝对值就推出来了
void solve()
{
	int n;
	cin >> n;
	vi a(n + 1);
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	sort(a.begin() + 1, a.end());
	int t = a[(n + 1) / 2];
	int ans = 0;
	for (int i = 1; i <= n; i++)
	{
		ans += abs(t - a[i]);
	}
	cout << ans << endl;
}


3. 数据结构贪心:合并果子 (哈夫曼树模型)

来源文件合并果子.cpp

  • 🏷️ 题面抽象:有 $N$ 堆果子,每次可以将两堆合并,消耗的体力为两堆重量之和。求把所有果子合并成一堆的最小总体力消耗。
  • 💡 核心思路
  • 贪心策略:每次合并产生的重量会参与到后续所有的合并中。为了让总和最小,应该让重量越小的果子被合并的次数越多
  • 实现:使用小根堆(priority_queue<int, vi, greater<int>>)维护当前所有的堆。每次弹出最小的两个合并,将其和累加到答案中,并将新生成的堆重新放入优先队列,直到队列中只剩 1 个元素。
点击查看代码
void solve()
{
	int n;
	cin >> n;
	priority_queue<int, vi, greater<int>> q;
	for (int i = 1; i <= n; i++)
	{
		int t;
		cin >> t;
		q.push(t);
	}
	int ans = 0;
	while (q.size() > 1)
	{
		int a = q.top();
		q.pop();
		int b = q.top();
		q.pop();
		ans += (a + b);
		q.push(a + b);
	}
	// if (q.size())
	// {
	// 	ans += q.top();
	// }
	cout << ans << endl;
}

4. 传递模型:均分纸牌 (相邻消除)

来源文件P_1031_NOIP_2002_提高组_均分纸牌.cpp

  • 🏷️ 题面抽象:有 $N$ 堆纸牌,每次可以从一堆向相邻的一堆移动任意张纸牌,求使得所有堆纸牌数量相等的最小移动次数。
  • 💡 核心思路
  • 贪心策略:先计算出全局平均数 ave。从左到右线性扫描,对于第 $i$ 堆,不管它是多是少,直接将其与 ave 的差值(可能为负数,代表索取)传递给第 $i+1$ 堆。
  • 实现:只要 $a_i \neq ave$,就令 $a_{i+1} += (a_i - ave)$,并算作一次移动。这种局部“多退少补”的策略,传递到最后一堆时必然正好达到平衡。
点击查看代码
void solve() {
    int n;
    cin >> n;
    vi a(n + 1);
    int ave = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        ave += a[i];
    }
    ave /= n;
    int ans = 0;
    for (int i = 1; i <= n - 1; i++) {
        if (a[i] != ave) {
            int t = ave - a[i];
            a[i] = ave;
            ans++;
            a[i + 1] -= t;
        }
    }
    cout << ans << endl;
}

5. 差分贪心:积木大赛 (区间操作)

来源文件P_1969_NOIP_2013_提高组_积木大赛.cpp

  • 🏷️ 题面抽象:目标是搭建高度为 $a_i$ 的积木阵列,每次操作可以对一段连续的区间高度全部加 1,求达到目标状态的最小操作次数。
  • 💡 核心思路
  • 贪心策略:从左向右扫描,比较当前高度 $a_i$ 和前一个高度 $a_{i-1}$。
  • 分类讨论
  1. 如果 $a_i \le a_{i-1}$:搭前一块积木的时候,当前这块可以“顺带”被一起搭好,不需要额外增加操作数。
  2. 如果 $a_i > a_{i-1}$:前一块积木高度不够,当前这块多出来的高度必须“自立门户”去新增操作,操作数增加 $a_i - a_{i-1}$。
  • 实现ans += max(0LL, a[i] - a[i-1])
点击查看代码
void solve() {
    int n;
    cin >> n;
    vi a(n + 1);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        if (a[i] > a[i - 1]) {
            ans += a[i] - a[i - 1];
        }
    }
    cout << ans << endl;
}


6. 数学证明模型:耍杂技的牛 (相邻交换法)

来源文件耍杂技的牛.cpp

  • 🏷️ 题面抽象:$N$ 头牛叠罗汉,每头牛有重量 $W$ 和强壮度 $S$。一头牛的危险值 = 它上面所有牛的重量之和 - 它的强壮度。求如何排序能使得队伍中最大的危险值最小。
  • 💡 核心思路
  • 贪心策略:按照 $W_i + S_i$ 的值从小到大排序。
  • 证明方法(相邻交换法):假设有两头相邻的牛 A 和 B(A 在 B 上面)。如果把 A 和 B 交换位置,单独列出这两头牛交换前后的最大危险值公式。化简不等式后会发现,当 $W_A + S_A < W_B + S_B$ 时,A 放在上面产生的最大危险值更小。
  • 实现:定义结构体,重载小于号为 return (u + v) < (t.u + t.v);,排序后自顶向下模拟,记录重量前缀和,算出每头牛的危险值取最大值。
点击查看代码
//推公式 因为要排序只需要列出两项 假设他们交换看哪个大哪个小 即可

struct Node
{
	int u, v;
	bool operator<(const Node &t) const
	{
		return (u + v) < (t.u + t.v);
	}
};

void solve()
{
	int n;
	cin >> n;
	vector<Node> a(n + 1);
	for (int i = 1, u, v; i <= n; i++)
	{
		cin >> u >> v;
		a[i] = {u, v};
	}
	sort(a.begin() + 1, a.end());
	int ans = -1e18;
	int t = 0;
	for (int i = 1; i <= n; i++)
	{
		ans = max(ans, t - a[i].v);
		t += a[i].u;
	}
	cout << ans << endl;
}


7. 综合进阶:积木大赛 Plus (2026西安邀请赛A)

来源文件积木大赛plus[2026西安邀请A].cpp

  • 🏷️ 题面抽象:给定数组,首先需要满足类似“均分纸牌”的平均值传递机制,但在计算步数时,采用了跳跃或交替的区间逻辑(相当于在剥离出来的差值数组上运行“积木大赛”)。
  • 💡 核心思路:这是一道结合了多个经典原题的 Composite Problem。
  • 第一步(可行性判定与差分构建):首先求平均值 ave。类似于均分纸牌,计算每个位置需要向后传递的差值,存入数组 d。如果中途出现差值 $< 0$ 或总和不被整除,则直接判定无解(输出 -1)。
  • 第二步(奇偶拆分):因为题目特有的操作性质(相邻或跳跃传递),将构建出的差值数组 d 按照奇偶下标拆分为两个独立的数组 oddeven
  • 第三步(复用积木大赛模型):对拆分出来的 oddeven 数组,分别套用《积木大赛》的核心逻辑(即计算 $\sum \max(0, x_i - x_{i-1})$),最后将两部分操作数相加即为答案。
点击查看代码
void solve() {
    int n;
    cin >> n;
    vi a(n + 1);
    int ave = 0;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        ave += a[i];
    }
    if (ave % n != 0) {
        cout << -1 << endl;
        return;
    }
    ave /= n;
    vi d;
    for (int i = 1; i <= n - 1; ++i) {
        int now = ave - a[i];
        if (now < 0) {
            cout << -1 << endl;
            return;
        }
        d.push_back(now);
        a[i] = ave;
        a[i + 1] -= now;
    }
    if (a[n] != ave) {
        cout << -1 << endl;
        return;
    }
    vi even;
    vi odd;
    for (int i = 0; i < d.size(); i += 2) {
        odd.push_back(d[i]);
    }
    for (int i = 1; i < d.size(); i += 2) {
        even.push_back(d[i]);
    }

    auto get = [&](vector<int> &x) {
        int ans = 0;
        x.insert(x.begin(), 0);
        for (int i = 1; i < x.size(); i++) {
            if (x[i] > x[i - 1]) {
                ans += x[i] - x[i - 1];
            }
        }
        return ans;
    };

    int res = 0;
    res = get(odd) + get(even);
    cout << res << endl;
}

posted @ 2026-05-16 20:09  r_123  阅读(15)  评论(0)    收藏  举报