一、知识铺垫
最优化问题
最优化问题是指这样一类问题:问题给定某些约束条件,满足这些约束条件的问题的解称为可行解
。如果可行解只有一个,那么该解就是最优解;否则,引入某个数值函数,称为目标函数
,用来衡量可行解的好坏。使得目标函数取极值
(最大、最小)的可行解称为最优解
。
二、什么是贪心法
贪心法是一种求解最优化问题
的算法设计策略,通过分步决策来求解问题。贪心法在求解问题的每一步都要依靠一种策略进行决策,这种策略称为最优量度标准
(贪心选择性质、贪心准则)。通过局部最优推出全局最优。
三、贪心法的使用场景
求解最优化问题。问题具备两种特性:最优量度标准
,最优子结构
。例如:部分背包问题、活动选择问题、带时限的作业排序问题、最佳合并模式(Haffman)、最小生成树MST(Prim、Kruskal)、单源最短路径(Dijkstra)。
四、贪心法的解题步骤
SolutionType Greedy(SType a[], int n){
SolutionType solution = NULL:
for(int i = 0; i < n; i ++){
SType x = select(a); // 通过贪心选择性质挑选该步的解分量
if(Feasilble(solution, x) //加入分量后,如果满足约束条件
solution = Union(solution, x); // 将分量x加入解向量
}
}
五、典例
- 部分背包问题
最优量度标准:每次挑选p/w最大的物品放入背包。
int n, m;
double x[N];
void knapsack(){
// 所以物品已经按p/w非递增排序
double u = m; // 剩余背包体积
int i;
for( i = 0; i < n; i ++){
if(u >= w[i]){
u -= w[i];
x[i] = 1;
}
}
if(i < n) x[i] = u/w[i]; // 如果没装满
}
- 带时限作业排序问题
最优量度标准:每次挑选价值最高的作业加入。然后进行可行性检测。
int x[N], n;
void JS(){
int k = 0;
x[0] = 0;
for(int i = 1; i < n; i ++){ // 枚举每一个作业
int r = k;
auto t = job[i];
while(r >= 0 && job[x[r]].d > t.d && job[x[r]].d > r + 1) r --; //寻找插入位置
if((r < 0 || job[x[r]].d <= t.d) && t.d > r + 1){ // 判断是否超限,可以插入
for(int j = k; j >= r + 1; j --) // 插入位置及以后的作业右移
x[j + 1] = x[j];
x[r + 1] = i; // 插入作业
k ++;
}
}
}
- 最小生成树问题
最优量度标准:每次挑选距离增量最小的边加入边集S,对于其不同解释,产生了Prim、Kruskal两种算法。
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
int g[N][N];
bool st[N];
int dist[N];
int n, m;
long long ans;
bool prim(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i ++){
int t = -1;
for(int j = 1; j <= n; j ++){
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
}
if(dist[t] == 0x3f3f3f3f) return false;
st[t] = true;
ans += dist[t];
for(int j = 1; j <= n; j ++)
dist[j] = min(g[t][j], dist[j]);
}
return true;
}
int main(){
cin >> n >> m;
memset(g, 0x3f, sizeof g);
while(m --){
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
if(prim()) cout << ans;
else puts("impossible");
return 0;
}
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
const int M = 2e5 + 10;
int p[N];
struct Edge{
int a, b, c;
bool operator<(const Edge &e){
return c < e.c;
}
};
long long cnt = 0, sum;
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
int n, m;
cin >> n >> m;
vector<Edge> v;
for(int i = 1; i <= m; i ++){
int a, b, c;
cin >> a >> b >> c;
v.push_back({a,b,c});
}
for(int i = 1; i <= n; i ++) p[i] = i;
sort(v.begin(),v.end());
for(auto t:v){
int pa = find(t.a);
int pb = find(t.b);
if(pa == pb) continue;
p[pa] = pb;
cnt ++;
sum += t.c;
if(cnt == n - 1) break;
}
if(cnt == n - 1) cout << sum;
else puts("impossible");
}
- 最短路径问题
Dijkstra
最优量度标准:使得从s(源点)到其他所有点的代价增量之和最小。也就是求距离s第一短的路径
、除去之前的点第一短的…
原题链接:https://www.acwing.com/problem/content/description/851/
#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 510;
int g[N][N], dist[N];
bool vis[N];
int n, m;
typedef long long LL;
void dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i ++){
int t = -1;
for(int j = 1; j <= n; j ++)
if(!vis[j] && (t == -1 || dist[j] < dist[t]))
t = j;
vis[t] = true;
for(int j = 1; j <= n; j ++)
dist[j] = min(dist[j], g[t][j] + dist[t]);
}
}
int main(){
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m -- ) {
int a, b , c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
dijkstra();
if(dist[n] == INF) cout << -1;
else cout << dist[n];
return 0;
}
六、总结
不同的贪心选择性质可能会导致不同的解。最优贪心选择性质可能不存在,因为贪心法是一种短视的算法,每步只考虑当前的最佳选择,不一定总能产生最优解。如果找不到最优量度标准,可以考虑使用DP求解。
另外:贪心法的正确性需要证明,通常通过归纳法或者 cut & paste 方法。