浅谈贪心算法
贪心算法,指在问题求解时,每一步都做出“当前看起来最好的决策”。它没有固定的算法模板,灵活性强。在 OI 领域,无论是入门组,还是省选,NOI,或多或少都出过贪心题。可见贪心的重要性之大。
使用贪心算法解决问题,必须满足 “无后效性”。满足 “无后效性” 不一定当前的决策对后续选择绝对无影响,只要满足当前决策不会影响后续最优解的选择即可。反之,动态规划问题必须满足当前决策对后续决策绝对无影响。在动态规划问题中,我们开多层状态,主要目的在于满足无后效性。
诚然,贪心策略的选择是重要的,但结论的证明也不可忽视。下文将给出部分贪心常用证明方法。
证明
邻项交换法
此类题目往往需确定一个决策顺序,当交换两者决策顺序,不影响其他决策时,可使用邻项交换法确定决策顺序。
例题精讲
共有 \(n\) 个物品,每个物品有两个值,分别为 \(w_i,s_i\)。
你可以对这 \(n\) 个物品任意排序。每个物品的贡献 \(W_i=\sum\limits_{j=i+1}^n w_j-s_i\),最小化 $\max{W_i} $ 。
\(1\le n \le 5\times 10^4\)
Source
一看数据范围,排序题。大概率是按照某种策略进行排序然后模拟。
难点在于排序策略。当然多重贪心加上随机化有可能获得可观的分数。
我们用邻项交换的方式思考本题。因为不难发现,交换 \(i,i+1\) 物品后,不会对其他物品产生影响。
从上到下的物品贡献可以表示为 \(\sum\limits_{k=1}^{i-1}w_k-s_i\)。
下文记 \(sum=\sum\limits_{k=1}^{i-1}w_k\)。
交换前,两个物品的贡献分别为 \(sum-s_i,sum+w_i-s_{i+1}\)。
交换后则分别为 \(sum-s_{i+1},sum+w_{i+1}-s_i\)。
因此,当满足
交换可能会使答案更优。
下面的操作基于以下原则,需要记住。
\(\max(a,b)<c \Leftrightarrow a<c\text{且}b<c\)
\(\max(a,b)>c \Leftrightarrow a>c\text{或}b>c\)
\(\max(a+b,a+c)=a+\max(b,c)\)
因此,上式可以消去 \(sum\),得
也就得到了交换条件。
依据 \(\max(a,b)>c \Leftrightarrow a>c\text{或}b>c\),我们将上式拆开。
因为 \(s_i,w_i,s_{i+1},w_{i+1}\) 恒大于 \(0\),故下列条件一定满足。
证明还是依据 \(\max(a,b)>c \Leftrightarrow a>c\text{或}b>c\)。这里不再赘述。
刚才提到,若交换条件
成立,则一定有
成立
前面已经证明,一式一定成立,我们只需要关心第二个式子。
还是把它拆开,显然 \(w_{i+1}-s_i<-s_i\) 是一定成立的,我们只需要关心 \(w_{i+1}-s_i<w_i-s_{i+1}\)。
移项,得。
证毕.
归纳法
数学归纳法
一般的,证明一个与正整数 \(n\) 有关的命题,可使用数学归纳法。具体步骤如下。
- (归纳奠基)证明当 \(n=n_0(n_0\in\operatorname{\N_{+}})\) 时成立。
- (归纳递推)以 “当 \(n=k(k\in \operatorname{\N_{+}},k\ge n_0)\) 时命题成立” 为条件,推出 “当 \(n=k+1\)” 时命题也成立。
由上述二步骤即可断定命题对 从 \(n_0\) 开始的所有正整数 \(n\) 都成立。
正确性. 由皮亚诺公理中 “归纳公理“ 可得,若某个性质在 \(0\) 成立,并且对于任意自然数 \(n\),若该性质在 \(n\) 成立,则它在 \(S(n)\) 也成立,那么该性质对于所有自然数都成立。事实上,数学归纳法和 “归纳公理” 本质相同,这也就意味着,数学归纳法作为一个公理,正确性是毋庸置疑的。
我们亦可这样理解,皮亚诺公理规定,任何一个自然数 \(i\) 有且仅有一个后继 \(i'\),既然 \(P(i)\Rightarrow P(i')\),且证得 \(P(s)\) 成立,则从 \(P(s)\) 可推得任意在区间内的 \(x\),命题 \(P\) 都成立。
Claim. 对于任意的自然数 \(n\),都有 \(1+2+3+4+\dots +n=\dfrac{n(n+1)}{2}\).
Proof.
最小的自然数为 \(1\),显然当 \(n=1\) 时命题成立.
设当 \(n=k(k\in \operatorname{\N})\) 时,命题成立,即得 \(1+2+3+4+\dots k=\dfrac{k(k+1)}{2}\),下面证明当 \(n=k+1\) 时命题成立,即证 \(1+2+3+4+\dots k+1=\dfrac{(k+1)(k+2)}{2}\).
命题成立.
即对于任意的自然数 \(n\),都有 \(1+2+3+4+\dots +n=\dfrac{n(n+1)}{2}\).
应用归纳法证明经典贪心算法
Kruskal 最小生成树算法
算法流程. 将所有候选边按照边权升序排序,依次决策。若加入该边,图上无环,则加入。否则跳过。当加入 \(n-1\) 条边后决策结束。时间复杂度 \(O(m\log m)\),其中 \(m\) 为边数。
一般的,我们认为,Kruskal 的时间复杂度只与边数 \(m\) 有关。这是一个和自然数有关的命题,考虑使用数学归纳法证明之。
Proof.
-
(归纳奠基)当 \(m=1\) 时显然成立.
-
(归纳递推)设当 \(m=k\) 时,执行 Kruskal 算法所取的边集为 \(T'\),还未选择的,且加入 \(T'\) 后不会形成环的,边权最小的边为 \(e\)。当 \(m=k+1\) 时,最小生成树为 \(T\)。下面证明 \(T=T'+e\)。
- 若 \(e\in T\),显然成立.
- 若 \(e\notin T\),将 \(e\) 放入边集 \(T\) 中,会形成环。环上必定存在至少一条边 \(f\) 不在 \(T'\) 中,我们分析 \(f\) 和 \(e\) 的关系.
- \(f<e\),执行 kruskal 算法时,\(f\) 应当在 \(e\) 之前被选。但 \(f\) 不应当在 \(T'\) 中(否则当前会形成环),矛盾.
- \(f>e\),此时,我们通过舍弃 \(f\),使用 \(e\) 可获得比原本 MST 更小的生成树。矛盾.
综上所述,\(f=e\)。命题 \(e\notin T\) 不成立.
综上,我们证明了执行 Kruskal 算法,每一步添加的边必定在 MST 中.
基于贪心思想的区间覆盖问题
区间完全覆盖问题
有 \(n\) 条线段,第 \(i\) 条线段覆盖区间 \([l,r]\),求至少需要多少条线段,覆盖区间 \([1,m]\)。
这是一个经典问题。我们采用贪心的策略解决,具体如下。
Greedy select. 令 \(pos\) 为当前已覆盖区间右端点,找到一条左端点 \(\le pos+1\),右端点最大的线段,并更新 \(pos\)。
Proof. 该策略每次选择的为合法的,右端点最大的线段。每次的选择能够将 \(pos\) 扩展到最大,即将已覆盖区间扩展到最大。
我们亦可从全局最优性证明。由于每次选择区间必须合法,显然,\(pos\) 越大,可选择的区间越多。故该贪心策略必定导致答案更优不劣。
Example.
namespace solution
{#define x first#define y secondtypedef pair<int,int> PII;vector <PII> line;int n,m;void solve(){cin>>n>>m;for(int i=1;i<=n;i++){int l,r;cin>>l>>r;line.push_back(PII(l,r));}sort(line.begin(),line.end());int pos = 1,tot = 0,ans = 0;while(pos < m){int maxn = 0;for(int i = tot;i < line.size();i++) {if(line[i].x <= pos + 1) maxn = max(maxn,line[i].y);else {tot = i;break;} }pos = maxn;ans ++;}cout<<ans<<"\n";return;}
}
给定 \(k\) 条线段,询问 \(q\) 次,每次询问最少用多少条线段覆盖区间 \([l,r]\)。
一个比较显然的策略是,对于每次询问,令 \(pos=l\) 执行上述 Greedy select。但复杂度并不理想。
Claim. 从 \(i\) 经线段 \([i,j]\) 跳到 \(j\) 后,执行 Greedy select 选择的线段和前面选择的必不重复。
Proof. 若从 \(j\) 贪心选择线段和前面重复,则前面可选择该线段以获得更大的 \(pos\)。矛盾。
因此,对于任意的 \(i\),执行 \(k\) 次 Greedy select 是 独立 的,不受前面选择影响。据此,我们预处理 \(f_{i,j}\) 表示 \(i\) 执行 \(2^j\) 次 Greedy select 扩展到的 \(pos\)。对于每次询问,从 \(l\) 倍增跳即可。
Example.
for(int i=1,j=1,maxn = 0;i<=n*2;i++)
{while(j <= k && p[j].x <= i) {r = max(r,p[j].y+1);j++; }f[0][i] = r;
}
for(int i=1;i<=20;i++)for(int j=1;j<=n;j++) f[i][j] = f[i-1][f[i-1][j]];
给定一个长度为 \(n\) 的环,有 \(k\) 个区域被覆盖,求最少需要几条线段覆盖整个环。
Source:洛谷 P6902 [ICPC2014 WF] Surveillance
该问题和上面类似,只是将序列变为环。常见的处理思路是断环为链。具体的,将所有跨过 \(1\) 的线段 \([l,r]\) 视为 \([l,n+r]\)。一条线段 \([l,r]\) 跨过 \(1\) 当且仅当 \(l>r\)。
Example. paste
最大不相交区间数问题
数轴上有 \(n\) 个区间 \([a_i,b_i]\),选择尽可能多的区间,使得区间两两不相交。
Greedy select. 将所有区间按照右端点排序,若右端点相同,按照左端点排序。顺序选模拟即可。
Proof / Analysis. 考虑反证法。假设贪心算法执行完毕后,扔掉其选择的一段区间 \([u,v]\),可选择另外若干条新区间 \([a_1,b_1],[a_2,b_2]\dots\) 且不会对其他已选择区间造成影响。分类讨论新区间与 \([u,v]\) 的关系。
-
\([a_i,b_i]\subseteq [u,v]\),这种情形可推广为 \(b_i\le v\)。根据贪心算法,不可能。
-
\(b_i\ge v\) 前面提到,不可能存在 \(b_i\le v\)。故交换二者不可能更优。
证毕.
带需求约束的最小集合覆盖问题
在一条数轴上,给定若干区间 \([l_i,r_i]\),每个区间都有一个约束 \(k_i\),要求区间内 至少 有 \(k\) 个点被染色,求满足所有约束条件下,至少染色多少个点。
P1250 种树 P1986 元旦晚会 SP116 POI2015 KIN
CSP-S2024 超速检测
Greedy Select. 考虑将所有询问区间离线到数轴上。将所有区间按照右端点排序。若该区间已经满足条件,忽略。否则从右边开始,枚举没染色的点染色。我们一定尽可能希望给一个区间染色能影响下一个区间,故从右往左染色。实现时可使用树状数组加速。
Proof. 由于我们总是从每个区间的右端开始满足需求,对于每一个区间,我们在尽可能靠右的位置放置必要的点以满足它的需求。任何尝试减少点数的方法都会导致某些区间无法满足需求,因为去除任何点都会导致至少一个区间的覆盖需求不足。
Example.
void solve()
{n = read<int>(),m = read<int>();bit.clear();ask.clear();memset(tree,0,sizeof(tree));for(int i=1;i<=m;i++) {int a,b,c;a = read<int>(),b = read<int>(),c = read<int>();a ++,b ++;ask.push_back({a,b,c});}sort(ask.begin(),ask.end());int res = 0;for(auto [a,b,c]:ask){int tot = bit.query(b) - bit.query(a-1);if(tot >= c) continue;for(int i = b;i>=a;i--){if(tot >= c) break;if(!tree[i]) {tree[i] = 1;bit.update(i,1);tot ++,res ++;}}}print(res,'\n');return;
}