贪心

本文用于总结我学习的贪心思想及好题

邻项交换

这是一种典型的贪心证明方法,通过交换相邻的数据或操作,以寻找或证明贪心策略,通常适用于不需要进行筛选,只需要决定操作顺序的问题中

UVA1205 Color a Tree

题解

首先容易想到一种错误的思路:每次在可染色节点中选择权值最大的节点。
但是,经过尝试,我们发现似乎似乎可以根据“权值密度”来进行选择。

很遗憾,也是错的 qwq

于是,在经过不懈探(看)索(书)后,又有一条性质:
整棵树中权值最大的节点必定在他的父节点染色后立即被染色

假设最大权值为\(a\),在\(t\)时刻时,其父节点被染色,
那么如果立即对该节点染色,则代价

\[\begin{equation} w_1 = ta + \sum^{t+k}_{i=t+1} ia_i \end{equation} \]

若不立即染色,

\[w_2 = \sum^{t + k}_{i = t+1} (i-1)a_i + (t + k)a \]

其中\(a_i\) 表示在\(a\)被染色后再染色的\(k\)个元素

\[\begin{align*} w1 - w2 &= \sum^{t+k}_{i=t+1} (i - i + 1) a_i - ka \\ &= \sum^{t + k}_{i=t+1} a_i - ka \\ &= \sum^{t+k}_{i=t+1} (a_i - a) \end{align*} \]

\(a\)是最大值,所以对\(\forall a_i \neq a\), \(a > a_i\)
所以\(w_1 - w_2 < 0\), 即\(w_1 < w_2\)
故性质成立

那么,由于最大权值节点会立即被染色,我们可以考虑将它和它的父亲合并

然后,考虑对这两个节点在整棵树中染色的位置

即,有三个节点\(x,y,z\)\(x,y\)必须同时染色
有两种顺序

  1. \(x, y\) => \(z\)
  2. \(z\) => \(x, y\)
    对于两种顺序,\(w_1 = x + 2y + 3z\)\(w_2=z + 2x + 3y\)

\[\begin{align*} w_1 - w_2 &=(x+2y+3z)-(z+2x+3y) \\ &=2z - (x + y) \\ &=2(z - \frac{x+y}{2}) \end{align*} \]

所以,只需比较\(z\)\(\frac{x+y}{2}\)的大小即可
同理,该性质也可以拓展到\(n,m\)个节点团合并顺序的确定
也就是\(\frac{\sum a}{s}\)\(a\)是已合并节点的权值,\(k\)是合并节点的数量)
是不是和开头的“权值密度”有些相似?

进一步思考可以发现,每次合并完后我们都会得到一颗新树,所以只需要对它重复上述操作即可

具体的,记录每个节点的染色顺序,每次从树中选择权值最大的节点,并向上合并,将该节点添加到父亲的操作序列末尾,将其子结点变为其父亲的子结点,并用平均权值更新父亲的权值
重复这一操作直到只剩根节点(注:根节点是不参与比较过程的)

Code

#include <cstdio>
#include <vector>
#include <queue>
using namespace std;

const int N = 1000;
int n, r, w[N+5];
int wtot[N+5], siz[N+5];
int fa[N+5];
vector<int> son[N+5];

struct inf {
	int w, s, id;
};

bool operator <(inf a, inf b)
{
	return a.w * b.s < b.w * a.s;
}

priority_queue<inf> q;
queue<int> op[N+5];

int main(void)
{
	while (scanf("%d %d", &n, &r) == 2 && (n | r)) {
		for (int i = 1; i <= n; i++)
			scanf("%d", &w[i]);
		for (int i = 1; i <= n - 1; i++) {
			int p, u;
			scanf("%d %d", &p, &u);
			son[p].push_back(u);
			fa[u] = p;
		}

		for (int i = 1; i <= n; i++) wtot[i] = w[i], siz[i] = 1;

		for (int i = 1; i <= n; i++)
			if (i != r)
				q.push((inf) {w[i], 1, i});

		while (!q.empty()) {
			int u = q.top().id;
			// 跳过已经与子结点合并但尚未更新的节点
			if (q.top().s != siz[u]) {
				q.pop();
				continue;
			}

			q.pop();
			int p = fa[u];

			// 将子结点连到父亲上
			for (int i = 0; i < son[u].size(); i++) {
				int v = son[u][i];
				fa[v] = p;
				son[p].push_back(v);
			}

			// 记录操作顺序,并合并至父亲
			op[p].push(u);
			while (!op[u].empty()) {
				op[p].push(op[u].front());
				op[u].pop();
			}

			wtot[p] += wtot[u];
			siz[p] += siz[u];
			if (p != r)
				q.push((inf) {wtot[p], siz[p], p});
		}
                // 按根的操作序列计算答案
		int t = 1;
		int ans = w[r];
		while (!op[r].empty()) {
			ans += ++t * w[op[r].front()];
			op[r].pop();
		}
		printf("%d\n", ans);
	}
	return 0;
}

总结

整体思路:根据题目要求,关注权值最大结点 => 考虑结点与父亲,其他子结点的顺序 => 合并的idea
=> 继续考虑合并后结点与其他结点的顺序
关键词:操作顺序,交换比较大小 => 邻项交换

数学归纳法与决策包容性

数学归纳法证明贪心算法的核心逻辑是:

  1. 归纳基础:证明存在最优解包含第一步的贪心选择

  2. 归纳步骤:假设前\(k\)步正确,证明第\(k+1\)步也正确

from DeepSeek

而决策包容性体现的就是归纳法的核心思想

决策包容性的三层含义

  1. 存在性保证:不要求所有最优解都包含贪心选择,只要求至少存在一个

  2. 安全性保证:选择贪心行动不会"断送"达到最优解的可能性

  3. 延续性保证:做出贪心选择后,剩余子问题仍然保持相同的性质

决策包容性 vs 数学归纳法

实际上,决策包容性是数学归纳法证明贪心算法的核心思想:

  • 数学归纳法的基础步骤就是在证明第一步的决策包容性

  • 数学归纳法的归纳步骤就是在证明后续每一步的决策包容性

from DeepSeek

以最经典的活动选择问题为例,我们首先将活动按结束时间递增排序,贪心策略是每次从可选的集合当中选择结束时间最早的活动。

首先,设所有活动中结束最早的是\(x\),假设有一个最优解所选取的活动的集合是\(S\),如果其中结束时间最早的活动是\(y\)
那么,有 \(r_x \leq r_y\),如果我们将\(y\)替换为\(x\)得到一个新解\(S'\),显然\(|S|=|S'|\),故\(S'\)也是一个最优解。
进而,我们可以不断重复这一替换过程,进行归纳,从而证明每一步都执行贪心策略是正确的。

例题 P2887 Sunscreen

为方便叙述,下文用\(l, r\)代替原题中的\(minSPF, maxSPF\)
题意:每头奶牛需要\([l_i, r_i]\)范围内的防晒霜,给定所有防晒霜的\(SPF\)值,求最多能满足多少奶牛的要求

这种区间类贪心,经典套路就是按左端点或右端点排序,我们先尝试按\(l_i\)降序排序
那么,我们先假设贪心策略是对于每头奶牛,选择在其区间内\(SPF\)最大的防晒霜

考虑一个最优解\(S\),设第一头奶牛为\(A\)\(x = max\{SPF_i \ |\ SPF_i \in [l_A, r_A]\}\)
\(S\)\(x\)分配给\(A\),则与策略相符,
否则,设分配给\(A\)的是\(y\)\(x\)分配给了奶牛\(B\),则\(y < x\)\(l_B \leq l_A\)(按\(l\)排序),\(y \in [l_A, r_A], x \in [l_B, r_B]\)

\[\begin{align*} &\because y < x, x \in [l_B, r_B] \\ &\therefore y \leq l_B \end{align*} \]

但是,我们却无法得出\(y \geq r_B\),无法交换\(x, y\),于是我们尝试按\(r\)排序,再次证明,就成功了!
于是,正确的贪心策略是按\(r_i\)降序排序,对于每头奶牛,选择在其区间内\(SPF\)最大的防晒霜

从上面的例子可以看出,调整、交换、替换仍是证明的重要方法,我们也会发现,证明同时也可能引导我们发现正确的贪心策略

几个经典的区间贪心问题

活动选择问题

上面说过了

区间选点问题

给定\(n\)个区间\([l_i, r_i]\),在数轴上选若干个整点,使得每个区间内都至少有一个点,求选取的最小点数

贪心策略:将区间按\(r\)递增排序,依次考虑每一个区间,若区间内已经有点,跳过,否则选取最靠右的点(即\(r_i\)

证明:假设第一个区间选的是\(x < r_1\),假设\(x\)被区间\(I_1, I_2, \cdots, I_t\)所共有,由于排序过,所以\(r_1 \leq r_2 \leq \cdots \leq r_t\),那么将\(x\)换成\(r_1\),仍满足被这些区间包含,因而也是一个最优解,进一步归纳可得结论

另一种策略见这篇博客

这一问题也可拓展为第\(i\)个区间选\(k_i\)个点,策略变为选最右侧未被选的\(k_i\)个点即可,见AcWing 362 区间

反悔贪心

有些贪心问题中,可能无法每次通过局部最优得到整体最优,于是我们可以边决策边修正,也就是可以“反悔”的贪心

posted @ 2025-07-30 13:24  zhm0725  阅读(23)  评论(0)    收藏  举报