算法基础篇(8)贪心算法
贪心算法,或者说是贪心策略,就是企图用局部最优找出全局最优。
1.把解决问题的过程分为若干步
2.解决每一步时,都选择"当前看起来最优的"解法
3."希望"得到全局的最优解
8.1 简单贪心
8.1.1 货仓选址

根据贪心的思想,我们最希望把仓库建在中间位置。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;const int N = 1e5 + 10;
int n;
long long a[N];int main()
{cin >> n;for (int i = 1;i <= n;i++){cin >> a[i];}sort(a + 1, a + 1 + n);long long ret = 0;for (int i = 1;i <= n;i++){ret += abs(a[i] - a[(1 + n) / 2]);}cout << ret << endl;return 0;
}
那么,这个贪心策略是否就是本题的最优解呢?下面开始证明:
这里就要用到数学上的绝对值不等式:|a-x| + |b - x| ≥ |a-b|

根据上面的出来的公式,这道题的代码还可以这么写:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;const int N = 1e5 + 10;
int n;
long long a[N];int main()
{cin >> n;for (int i = 1;i <= n;i++){cin >> a[i];}sort(a + 1, a + 1 + n);long long ret = 0;/*for (int i = 1;i <= n;i++){ret += abs(a[i] - a[(1 + n) / 2]);}*/for (int i = 1;i <= n / 2;i++){ret += a[n + 1 - i] - a[i];}cout << ret << endl;return 0;
}
8.1.2 最大子段和

根据贪心的思想,我们希望sum ≥ 0,就继续向后累加;sum < 0,就舍弃这一段,从下个位置开始重新累加。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;const int N = 2e5 + 10;
int n;
long long a[N];int main()
{cin >> n;for (int i = 1;i <= n;i++)cin >> a[i];long long ret = -1e6, sum = 0;for (int i = 1;i <= n;i++){sum += a[i];ret = max(ret, sum);if (sum < 0)sum = 0;}cout << ret << endl;return 0;
}
下面证明该贪心思想是正确的:

8.1.3 纪念品分组

贪心策略:在剩余物品中,取出最小值x和最大值y,如果x + y ≤ w,就放在一起;如果x + y > w,就把y单独放,x待定。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
using namespace std;const int N = 3e4 + 10;
int n, w;
int a[N];int main()
{cin >> w >> n;for (int i = 1;i <= n;i++)cin >> a[i];sort(a + 1, a + 1 + n);int i = 1, j = n;long long cnt = 0;while (i <= j){if (a[i] + a[j] <= w)i++;cnt++;j--;}cout << cnt << endl;return 0;
}
8.2 公式法
细说的话,这里应该是推公式+排序。其中推公式就是寻找排序规则,排序就是在该排序规则下对整个对象排序。在解决某些问题时,当我们发现最终结果需要调整每个对象的先后顺序,那么就可以使用推公式的方式来得出排序规则,进而对整个对象进行排序。但是这里的证明过程会很麻烦(需要用到离散数学中的"全序关系"),后续题目中我们只要发现该题的最终结果需要排序,并且交换相邻两个元素的时候对其余元素不会产生影响,那么就可以推导出排序的规则,然后直接去排序,就不用证明了。
8.2.1 拼数

直接拼接,然后确定先后关系。ab > ba,a前b后;ab < ba,b前a后。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
using namespace std;const int N = 30;
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;
}
8.2.2 保卫花园


#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
using namespace std;const int N = 1e5 + 10;
int n;struct node
{int t;int 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);long long sum = 0, t = 0;for (int i = 1;i <= n;i++){sum += a[i].d * t;t += 2 * a[i].t;}cout << sum << endl;return 0;
}
8.3 区间问题
区间问题是另一种比较经典的贪心问题。题目面对的对象是一个一个的区间,让我们在每一个区间上做出取舍。这种题目的解决方式一般就是按照区间的左端点或者是右端点排序,然后在排序区间上,根据题目要求制定出贪心策略,进而得到最优解。
8.3.1 线段覆盖

这道题的本质就是有若干个区间,让我们尽可能多的选择一些区间,使它们之间互不重叠。面对区间问题,第一步先排序。排序分为两种——左端点升序/降序 以及 右端点升序/降序。这里我们按照左端点升序排列。当区间重叠的时候,尽可能保留右端点较小的区间;当区间不重叠的时候,以下一个区间为基准,继续判断。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
using namespace std;const int N = 1e6 + 10;
int n;struct node
{int left;int right;
}a[N];bool cmp(node& x, node& y)
{return x.left < y.left;
}int main()
{cin >> n;for (int i = 1;i <= n;i++)cin >> a[i].left >> a[i].right;sort(a + 1, a + 1 + n, cmp);int cnt = 1;int r = a[1].right; //以第一个区间为基准,向后选择for (int i = 2;i <= n;i++){int left = a[i].left;int right = a[i].right;if (left < r) //重叠{r = min(r, right);}else{cnt++;r = right;}}cout << cnt << endl;return 0;
}
8.3.2 雷达安装

这道题如果直接在二维平面内分析的话会十分复杂,因为我们要枚举x轴上所有雷达的位置,然后计算每座岛屿到雷达的距离。所以第一步也是最关键的一步,就是把二维转化为一维。针对一座岛屿,当我们想要覆盖这座岛屿的时候,x轴上雷达的极限位置应该在哪一段区间内。第二步,在所有区间中,找出互相重叠的一共有多少组。先根据左端点排序,互相重叠的区间是连续的。当两个区间重叠时,应该用右端点较小的区间继续往后找。不重叠时,以下一个区间为基准,继续往后找。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;const int N = 1010;
int n;
double d;struct node
{double l;double r;
}a[N];bool cmp(node& x, node& y)
{return x.l < y.l;
}int main()
{int cnt = 0;while (cin >> n >> d, n && d){cnt++;bool flag = false;for (int i = 1;i <= n;i++){double x, y;cin >> x >> y;if (y > d)flag = true;double l = sqrt(d * d - y * y);a[i].l = x - l;a[i].r = x + l;}cout << "Case " << cnt << ": ";if (flag)cout << -1 << endl;else{sort(a + 1, a + 1 + n, cmp);int ret = 1;double r = a[1].r;for (int i = 2;i <= n;i++){double left = a[i].l;double right = a[i].r;if (left <= r) //有重叠{r = min(r, right);}else{ret++;r = right;}}cout << ret << endl;}}return 0;
}