算法基础篇(8)贪心算法

贪心算法,或者说是贪心策略,就是企图用局部最优找出全局最优。

1.把解决问题的过程分为若干步

2.解决每一步时,都选择"当前看起来最优的"解法

3."希望"得到全局的最优解

8.1 简单贪心

8.1.1 货仓选址

根据贪心的思想,我们最希望把仓库建在中间位置。

cpp 复制代码
#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|

根据上面的出来的公式,这道题的代码还可以这么写:

cpp 复制代码
#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,就舍弃这一段,从下个位置开始重新累加。

cpp 复制代码
#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待定。

cpp 复制代码
#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后。

cpp 复制代码
#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 保卫花园

cpp 复制代码
#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 线段覆盖

这道题的本质就是有若干个区间,让我们尽可能多的选择一些区间,使它们之间互不重叠。面对区间问题,第一步先排序。排序分为两种------左端点升序/降序 以及 右端点升序/降序。这里我们按照左端点升序排列。当区间重叠的时候,尽可能保留右端点较小的区间;当区间不重叠的时候,以下一个区间为基准,继续判断。

cpp 复制代码
#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轴上雷达的极限位置应该在哪一段区间内。第二步,在所有区间中,找出互相重叠的一共有多少组。先根据左端点排序,互相重叠的区间是连续的。当两个区间重叠时,应该用右端点较小的区间继续往后找。不重叠时,以下一个区间为基准,继续往后找。

cpp 复制代码
#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;
}
相关推荐
时间的情敌4 小时前
Vite 工作原理
1024程序员节
可惜我是水瓶座__4 小时前
[Spark] TaskMetrics指标收集
spark·1024程序员节
qq_4696035894 小时前
最新选题-基于Spark的二氧化碳排放量数据分析系统设计
1024程序员节·大数据项目·co2分析·二氧化碳分析
AI科技星4 小时前
接近光速运动下的光速不变性:基于张祥前统一场论的推导与验证
数据结构·人工智能·经验分享·算法·计算机视觉
天下·第二4 小时前
【大模型与OCR】配合应用的示例demo
1024程序员节
Greedy Alg4 小时前
LeetCode 39. 组合总和
1024程序员节
im_AMBER4 小时前
React 04
前端·react.js·前端框架·1024程序员节
lpfasd1234 小时前
第四章-Tomcat线程模型与运行方式
1024程序员节