总的来说,贪心就是对于求解最优解问题,每一步都做当前最优的措施,当然这需要子问题最优必定能推出总的最优才能这么做,不然很可能需要dp解决。
目前一点浅见,贪心更像是靠题感想到一种解决方案,然后先证明其合法性,再证明其不可能不是最优。
经典母题
这一部分主要照着AcWing上的题目写的笔记。
1、区间问题
AcWing 905. 区间选点
原题链接:https://www.acwing.com/problem/content/907/
做法
按照右端点从小到大排序后,选择第一个区间的右端点 x ,若后续有区间 w 不能被 x 选中,则 ++ ans 的同时,更新 x 为 w 的右端点。
证明
采用 “贪心 >= 最优解 && 贪心 <= 最优解” 的方式得出 贪心 == 最优解。
① 贪心 >= 最优解
首先我们每当有一个区间 w 无法包含当前的 x 时,就会重新在 w 范围内找到一个点更新 x,故而可以保证贪心出来的解法是能够囊括所有区间的,即贪心是合法解,只是不确定是否一定最优。
② 贪心 <= 最优解
首先在该贪心策略下,每一次重新选 x 的时候,都意味着当前区间和上一次选 x 的区间是互相独立的、没有任何交集的,换而言之,将每一次会导致重新选 x 的区间抽离出来,这 a 个独立区间注定需要 a 个点,而 a 即为该贪心策略下需要重新选 x 的次数。换句话说,贪心 <= 最优解。
综上,贪心 == 最优解。
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
struct range {
int l, r;
}arr[N];
bool cmp(range a, range b) {
return a.r < b.r;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
for (int i = 0; i < n; ++ i) {
rit;
ria;
arr[i] = {t, a};
}
sort(arr, arr + n, cmp);
int cnt = 0, last = -INF;
for (int i = 0; i < n; ++ i) {
if (arr[i].l > last) {
last = arr[i].r;
++ cnt;
}
}
pr("%d", cnt);
return 0;
}
AcWing 908. 最大不相交区间数量
原题链接:https://www.acwing.com/problem/content/910/
做法
这道题和 AcWing 905. 区间选点 做法一样。
按照右端点从小到大排序后,选择第一个区间的右端点 x ,若后续有区间 w 不能被 x 选中,则 ++ ans 的同时,更新 x 为 w 的右端点。
证明
采用 “贪心 <= 最优解 && 贪心 不可能< 最优解” 的方式得出 贪心 == 最优解。
① 贪心 <= 最优解
首先我们每当有一个区间 w 无法包含当前的 x 时,就会重新在 w 范围内找到一个点更新 x,故而可以保证贪心出来的所有区间是互相不会覆盖的,即贪心是合法解,只是不确定是否一定最优。
② 贪心 < 最优解 是错的
假如存在最优解 > 贪心,设最优解为 a 个区间,贪心解法为 b 个区间,在最优解情况下,至少需要 a 个点才能把所有区间覆盖,但由上一题可知 b 个点就足以覆盖,故而 a 不可能 > b,即最优解不可能 > 贪心。
综上,贪心 == 最优解。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
struct range {
int l, r;
}arr[N];
bool cmp(range a, range b) {
return a.r < b.r;
}
int main() {
cin >> n;
for (int i = 0; i < n; ++ i) {
int a, b;
cin >> a >> b;
arr[i] = {a, b};
}
sort(arr, arr + n, cmp);
int cnt = 0, last = -0x3f3f3f3f;
for (int i = 0; i < n; ++ i) {
if (arr[i].l > last) {
last = arr[i].r;
++ cnt;
}
}
cout << cnt;
return 0;
}
AcWing 906. 区间分组
原题链接:https://www.acwing.com/problem/content/908/
做法
将所有区间按照左端点从小到大排序,开一个小根堆,存每一组最右一个区间的右端点。
遍历区间数组,如果堆顶那一组能放下当前区间,则更新;不能则新开一组。
证明
① 合法性
首先,该种选法所确定的每一组都不会有区间重叠的情况,故而合法。
② 最优性
假如此时堆内有 a 组能容纳当前区间 x ,那么其实这些组中任意一组都可以放置当前区间 x。因为这些区间是按照左端点排序的,在 x 之后的任意一个区间 y (y的左端点大于等于x的左端点),想要放置在 a 中任意一组是更加没有问题的,所以并不存在说直接把堆顶那一组给 x 会影响到 y。
反之,如果连堆顶都容纳不下 x,那么其他现有的组是更加不可能的,所以需要新开一组。
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
struct range {
int l, r;
};
range arr[N];
bool cmp(range a, range b) {
return a.l < b.l;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
// cout << " n == " << n << endl;
for (int i = 0; i < n; ++ i) {
int a, b;
sc("%d%d", &a, &b);
arr[i] = {a, b};
}
sort(arr, arr + n, cmp);
priority_queue<int, vector<int>, greater<int> > h;
for (int i = 0; i < n; ++ i) {
range a = arr[i];
if (h.empty() || a.l <= h.top()) {
h.push(a.r);
}
else {
h.pop();
h.push(a.r);
}
}
cout << h.size();
return 0;
}
AcWing 907. 区间覆盖
原题链接:https://www.acwing.com/problem/content/909/
做法
将左端点按从小到大排序,last 表示目标区间目前需要被覆盖的点,遍历区间数组,在能覆盖当前的 last 的区间中选择右端点最大的,然后更新 last 为该区间的右端点,循环往复,直到 last >= 目标区间的右端点。
证明
① 正确性
首先,这种做法一定能保证选出来的区间能把目标区间完全覆盖,或者所有区间本身并不能覆盖目标区间。
因为每一次选的是区间中右端点最大的,如果连最大的都无法覆盖,那么显然不存在合法方案。
② 最优性
假设存在比当前方案更少的区间数,那么聚焦于局部,那必然是存在类似于最优解中 2 个区间就能覆盖贪心解中 3 个甚至更多区间,那么这 2 个区间中必然会存在有的区间右端点比 3 个区间中的某部分区间更长,这与该贪心策略下做法矛盾。故而不存在更少的区间数。
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
struct range {
int l, r;
}arr[N];
bool cmp(range a, range b) {
return a.l < b.l;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int st, ed;
cin >> st >> ed;
int n;
cin >> n;
for (int i = 0; i < n; ++ i) {
int a, b;
cin >> a >> b;
arr[i] = {a, b};
}
sort(arr, arr + n, cmp);
bool flag = false;
int ans = 0, last = st;
for (int i = 0; i < n; ++ i) {
range a = arr[i];
if (a.l <= last && a.r >= last) {
++ ans;
int j = i, t = j;
while (j < n && arr[j].l <= last) { //我是傻逼 忘了j < n调了好久的段错误
if (arr[t].r < arr[j].r) t = j;
++ j;
}
i = j - 1;
last = arr[t].r;
//这个if不可以放在外面,不然假设st == ed 那么会使得ans = 0就break
if (last >= ed) {
flag = true;
break;
}
}
}
if (flag) cout << ans;
else cout << -1;
return 0;
}
2、Huffman树
AcWing 148. 合并果子
原题链接:https://www.acwing.com/problem/content/150/
做法
因为不是必须相邻两堆,而是每次任意取两堆,所以其实就是构造哈夫曼树,每一次取倒数第一和第二小的合并后再放回去。
证明
简单一点来说,可以设 f(n)为合并 n 堆时的最小消耗值,那么 f(n) = f (n - 1)+ a + b。
每一次拿出最小和次小的两堆,保证了(a + b)是最小的,同时将(a + b)再次放回 f (n - 1)计算时,也是最优的做法(合并后的 a + b 和另一堆合并时,所消耗的是能做到的最小的方案)。
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 10000;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
priority_queue<int, vector<int>, greater<int> > heap;
for (int i = 0; i < n; ++ i) {
ria;
heap.push(a);
}
int ans = 0;
while (heap.size() > 1) {
int a = heap.top();
heap.pop();
a += heap.top();
heap.pop();
ans += a;
heap.push(a);
}
cout << ans;
return 0;
}
3、排序不等式
AcWing 913. 排队打水
原题链接:https://www.acwing.com/problem/content/description/915/
做法
ti 越小越先打水。
证明
假设 “ti 越小越先打水” 不是最优解,那么最优解中,一定存在某相邻两个人是打水时间 t 大的先打水,此时设这两个人的位置分别为 i 和 i + 1,需要打水的时间是 ti 和 ti+1,可见 ti > ti+1。
如果继续保留打水时间大的人在前面,那么两人花费的时间是 a = ti * (n - i) + ti+1 * (n - i - 1)。
如果交换这两个人的位置(并不影响其他人时间花费的计算),那么此时两人花费的时间是 b = ti+1 * (n - i) + ti * (n - i - 1)。
然而 a - b = ti - ti+1 > 0 ,也就是说 a > b。换句话说明明在我们的设定里,a 的做法才是最优解,但是交换后的 b 方案反而能使最终的结果更优。也就是说, “ti 越小越先打水不是最优解” 的假设不成立。也就是说,ti 越小越先打水是最优解。
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
ll arr[N];
int main() {
//freopen("D:\\in.txt", "r", stdin);
int n;
cin >> n;
for (int i = 0; i < n; ++ i) {
cin >> arr[i];
}
sort(arr, arr + n);
ll ans = 0, cnt = n - 1;
for (int i = 0; i < n; ++ i) {
ans += cnt * arr[i];
-- cnt;
}
cout << ans;
return 0;
}
4、绝对值不等式
AcWing 104. 货仓选址
原题链接:https://www.acwing.com/problem/content/106/
做法
先正序排序;
n 为奇数,取中位数;
n为偶数,取最中间两个位置之间的任意一个位置,包括端点。
证明
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
int arr[N];
int main() {
//freopen("D:\\in.txt", "r", stdin);
int n;
cin >> n;
for (int i = 0; i < n; ++ i) {
cin >> arr[i];
}
sort(arr, arr + n);
ll ans = 0;
for (int i = 0, j = n - 1; i < j; ++ i, -- j) {
ans += arr[j] - arr[i];
}
cout << ans;
return 0;
}
Acwing 105. 七夕祭(纸牌问题)
原题链接:https://www.acwing.com/problem/content/107/
思路
移动行时,只改变列。
移动列时,只改变行。
所以只需要考虑两者各自的min即可。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N = 100010;
#define ll long long
int n, m, t;
ll r[N], c[N], sum[N];
ll def(int n, ll a[]) {
memset(sum, 0, sizeof sum); //会调用两次def
//前缀和
for (int i = 1; i <= n; ++ i) sum[i] = sum[i - 1] + a[i];
//不可均分
if (sum[n] % n) return -1;
vector<ll> v;
ll avg = sum[n] / n; //均值
v.push_back(0); // |x1 - 0|
for (int i = 1; i < n; ++ i) {
v.push_back(sum[i] - i * avg);
}
sort(v.begin(), v.end());
int idx = v.size() / 2;
//计算距离之和
ll res = 0;
for (int i = 0; i < v.size(); ++ i) {
res += abs(v[i] - v[idx]);
}
return res;
}
int main() {
cin >> n >> m >> t;
for (int i = 1; i <= t; ++ i) {
int a, b;
cin >> a >> b;
++ r[a];
++ c[b];
}
ll x = def(n, r);
ll y = def(m, c);
if (x == -1 && y == -1) cout << "impossible" << endl;
else if (x != -1 && y != -1) cout << "both " << x + y << endl;
else if (x != -1) cout << "row " << x << endl;
else cout << "column " << y << endl;
return 0;
}
5、推公式
AcWing 125. 耍杂技的牛
原题链接:https://www.acwing.com/problem/content/127/
做法
按照 w + s 排序,w + s 越大越排在下面。
证明
说白了贪心就是按照正序排序,假如最优解不是正序排序,那么一定至少存在一个位置 wi + si > w(i + 1) + s(i + 1),如图推导 i 和 i + 1 位置上交换前后的变化:
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 60000;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
struct cow{
ll w, s;
}cows[N];
bool cmp(cow a, cow b) {
return a.s + a.w < b.s + b.w;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
int n;
cin >> n;
for (int i = 0; i < n; ++ i) {
ll a, b;
cin >> a >> b;
cows[i] = {a, b};
}
sort(cows, cows + n, cmp);
ll sum = 0, ans = -INF;
for (int i = 0; i < n; ++ i) {
ll num = sum - cows[i].s;
ans = max(ans, num);
sum += cows[i].w;
}
cout << ans;
return 0;
}
例题
1、POJ 百练 4151 电影节
原题链接:http://bailian.openjudge.cn/practice/4151/
描述
大学生电影节在北大举办! 这天,在北大各地放了多部电影,给定每部电影的放映时间区间,区间重叠的电影不可能同时看(端点可以重合),问李雷最多可以看多少部电影。
输入
多组数据。每组数据开头是n(n<=100),表示共n场电影。
接下来n行,每行两个整数(0到1000之间),表示一场电影的放映区间
n=0则数据结束
输出
对每组数据输出最多能看几部电影
样例输入
8
3 4
0 7
3 8
15 19
15 20
10 15
8 18
6 12
0
样例输出
3
思路
对于这道题,首先区间重叠的电影不能同时看,意味着对有重叠的部分,我们需要选择一个最优的电影来看。但是蛮一看,A 和 B、C重叠,B、C又各自和其他可能不会和A重叠的电影重叠,那么就会卡在到底什么才叫做最优?
这里有一个突破口,就是时间。想要在当天看最多的电影,首先时间是有限的,这里的 “ 有限 ” 体现在:比如说我看了这一天最早结束的电影,那么我剩下的能看其他电影的时间就会相对最多。
换句话说,假如我每一次挑选的电影都是当下能看,且最早结束的,那么就相当于我会剩下最多的时间看其他,以此类推就能看最多的电影。
针对题目给出的样例,我们按以下规则排序如下:
按区间右端点小的排在前面(说明开幕得早);
假如两个区间右端点一样,那就左端点大的排在前面(因为左端点最大的那个是最有可能不和前一个区间重叠的区间)。
(黑色代表区间左右端点,红色代表被选中的第 i 部电影)
综上,排好序再遍历一遍就可以得出最终结果。
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
using namespace std;
#define ll long long
#define MEN(x, y) memset(x, y, sizeof x)
#define rin scanf("%d", &n)
#define rln scanf("%lld", &n)
#define rit scanf("%d", &t)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 10000;
//(val & 1) == 0偶, == 1奇。
#define getlength(array,len) {len = (sizeof(array) / sizeof(array[0]));}
//电影放映的时间区间
struct movie {
int l, r;
};
//自定义排序规则
bool cmp(movie &a, movie &b) {
if (a.r == b.r) {
return a.l > b.l;
}
else return a.r < b.r;
}
movie arr[200];
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int n;
rin;
while (n != 0) {
for (int i = 0; i < n; ++ i) {
sc("%d %d", &arr[i].l, &arr[i].r);
}
sort(arr, arr + n, cmp);
int flag = 0, cnt = 1;
for (int i = 1; i < n; ++ i) {
if (arr[i].r != arr[flag].r) {
if (arr[i].l >= arr[flag].r) {
flag = i;
++ cnt;
}
}
}
pr("%d\n", cnt);
rin;
}
return 0;
}
2、POJ 3190 Stall Reservations (挤牛奶)
原题链接:http://poj.org/problem?id=3190
题目大意
一共 n 头牛,每一头牛有固定的挤奶开始时间和结束时间。现在有一排畜栏,每一个畜栏同一时间只能容纳一头牛,问要想安排这 n 头牛挤奶,最少需要多少个畜栏。
需要注意的是,畜栏不能无缝衔接,比如说畜栏A里面的奶牛的结束时间是5,那么挤奶时间区间在 5 - 9的奶牛不能刚好衔接上,需要错开。
要求输出最少畜栏数和每一头牛的被安排的畜栏编号(编号从1开始)。
输入
5
1 10
2 4
3 6
5 8
4 7
输出
4
1
2
3
2
4
思路
很明显,只要一头牛挤奶开始时间到了,就必须给它安排畜栏,而这个优先选择的畜栏要么是最早结束的那一个,要么是新开的一个。
所以代码上就是,先按照挤奶开始时间从早到晚对 n 头牛排序,然后安排畜栏:如果最早结束的畜栏可以安排,就更新这个畜栏的结束时间,否则新建一个畜栏。当然无论是哪一种情况,每一次操作后都需要对畜栏集合按照结束时间从早到晚重新排序。
所以对于这样的需求,可以开一个优先队列,以 O (log n) 的复杂度实现插入弹出。(记得储存每一头牛被分配的畜栏编号)
最后再按照牛的编号从小到大排序,在输出 cnt 和每头牛被分配的畜栏编号即可。
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
using namespace std;
#define ll long long
#define MEN(x, y) memset(x, y, sizeof x)
#define rin scanf("%d", &n)
#define rln scanf("%lld", &n)
#define rit scanf("%d", &t)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 10000;
//(val & 1) == 0偶, == 1奇。
#define getlength(array,len) {len = (sizeof(array) / sizeof(array[0]));}
//牛
struct cow {
//牛编号、开始时间、结束时间、被分配的畜栏编号
int num, l, r, cor;
};
//按照挤奶开始时间从早到晚排序
bool cmp_cow_l (cow &a, cow &b) {
return a.l < b.l;
}
//按照牛的编号从小到大排序
bool cmp_cow_num (cow &a, cow &b) {
return a.num < b.num;
}
//畜栏
struct corral {
//畜栏编号、结束时间
int num, ending;
//重载,方便优先队列top元素必定是最早结束的畜栏
bool operator < (const corral &a) const {
return ending > a.ending;
}
};
cow cows[50020];
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int n;
rin;
for (int i = 0; i < n; ++ i) {
cows[i].num = i;
sc("%d %d", &cows[i].l, &cows[i].r);
}
sort(cows, cows + n, cmp_cow_l);
priority_queue<corral> pq;
int cnt = 1;
cows[0].cor = 1;
corral cor = {cnt, cows[0].r};
pq.push(cor);
for (int i = 1; i < n; ++ i) {
cor = pq.top();
if (cor.ending < cows[i].l) {
corral temp = {cor.num, cows[i].r};
cows[i].cor = temp.num;
pq.pop();
pq.push(temp);
}
else {
++ cnt;
corral temp = {cnt, cows[i].r};
pq.push(temp);
cows[i].cor = cnt;
}
}
sort(cows, cows + n, cmp_cow_num);
pr("%d\n", cnt);
for (int i = 0; i < n; ++ i) {
pr("%d\n", cows[i].cor);
}
return 0;
}
3、POJ 1328 Radar Installation(最少雷达数)
原题链接:http://poj.org/problem?id=1328
题目大意
将 x 轴当成海岸线,现在在 y 轴上半轴有 n 个小岛,问在覆盖所有小岛的前提下,海岸线上需要建雷达的最少数目。雷达作用范围为以 d 为半径的圆。(如果站在作用区域圆上,也当作有覆盖到)(如果不能做到,输出 -1 )
输入
3 2 // n d
1 2
-3 1
2 1
1 2 // n d
0 2
0 0 // 表示输入结束
输出
Case 1: 2
Case 2: 1
思路
设小岛坐标(x,y),如果存在一个岛屿 y > d,那么直接输出 - 1;
刨除特殊的 - 1,现在开始讨论雷达的min_num:
显然对于雷达的作用域,岛屿可以是在圆上,也可以是在圆内,假如 y == d,那么必然在(x,0)处得有一颗雷达,但是如果 y < d,那么如下图所示在区间[ l,r ] 内有一个雷达即可。
那么先对所有岛屿求出其能接受的雷达位置的区间,再按照左端点从小到大排序,此时可以从前面遍历数组,也可以从后面开始遍历。前面的话就记录多个区间的共同区间即可,这里不再赘述,下面分析一下比较方便的从后面遍历:
我们可以发现对于1 - 3、2 - 4、6 - 10、6 - 15 这样一组数据,假如是从前到后,那么就是在遇到 6 - 10 的时候重新建一个雷达,因为它超出了1 - 3、2 - 4的公共区间 2 - 3,而且很明显,从 6 - 10 的左端点 6 大于 2 - 4的右端点 4 就可以判断需要加雷达。这样说可能有点像是废话,这不是明摆着的吗?
可是,当我们是从后面遍历,我们就可以先在最后一个元素的左端点特别无脑的先建一个雷达 X 作为基准,假如倒数第二个元素的右端点大于等于 X ,那么说明在囊括范围内,继而继续判断倒数第三个元素的右端点和 X 的大小,直到遇到一个元素的右端点 < X,此时说明需要新建雷达,因为超过 X 范围了。
所以在从后往前的遍历顺序下,每一次只需要比较雷达的位置和当前区间的右端点。
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
using namespace std;
#define ll long long
#define MEN(x, y) memset(x, y, sizeof x)
#define rin scanf("%d", &n)
#define rln scanf("%lld", &n)
#define rit scanf("%d", &t)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 10000;
//(val & 1) == 0偶, == 1奇。
#define getlength(array,len) {len = (sizeof(array) / sizeof(array[0]));}
struct leida {
double l, r;
};
leida arr[1010];
bool cmp(leida &a, leida &b) {
return a.l < b.l;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int n;
rin;
double d;
sc("%lf", &d);
int k = 1;
while (!(n == 0 && d == 0)) {
double a, b;
bool flag = false;
for (int i = 0; i < n; ++ i) {
sc("%lf %lf", &a, &b);
if (b > d) flag = true;
else if (b == d) arr[i].l = a, arr[i].r = a;
else {
double cha = sqrt(d * d - b * b);
arr[i].l = a - cha;
arr[i].r = a + cha;
}
}
if (flag) {
pr("Case %d: %d\n", k ++, -1);
}
else {
sort(arr, arr + n, cmp);
int cnt = 1;
double flag = arr[n - 1].l;
for (int i = n - 2; i >= 0; -- i) {
if (arr[i].r < flag) {
++ cnt;
flag = arr[i].l;
}
}
pr("Case %d: %d\n", k ++, cnt);
}
sc("%d %lf", &n, &d);
}
return 0;
}
4、POJ 1042 Gone Fishing(钓鱼)
原题链接:http://poj.org/problem?id=1042
题目大意
有 n 个湖排成直线(n ∈ [2,25]),每个湖一开始每 5 min能钓 f 条鱼,下一个 5 min能钓 f - d 条鱼,下下 5 min能钓 f - 2 * d 条鱼,以此类推,直到能钓的鱼数目为负数则不再计入,而且只能是以 5 min为时间长度去考虑。(f 和 d 都是非负整数)
John一共有 h 个小时(h ∈ [1,16]),从第 i 个湖走到第 i + 1个湖需要花费 ti * 5 min,而且John必须从第一个湖出发,且不可以走回头路,但可以停留在任意一个湖。
问:John能钓到最多鱼的方案,如果有多个方案,输出在第一个湖时长最长的方案,如果有两个方案鱼数目和第一个湖待的时长一样,则输出在第二个湖时长最长的方案,以此类推。
要求:输出每个湖最终待的时长和最终能钓到的鱼的最大数目;
当 n == 0 时结束输入。
输入
2 // n
1 // h
10 1 // fi
2 5 // di
2 //ti
4
4
10 15 20 17
0 3 4 3
1 2 3
4
4
10 15 50 30
0 3 4 3
1 2 3
0
输出
45, 5
Number of fish expected: 31
240, 0, 0, 0
Number of fish expected: 480
115, 10, 50, 35
Number of fish expected: 724
思路
这道题首先我们要最终鱼数目最大,那么对于同样的 5 min,当然是在哪个湖能钓到最多就去哪个湖,然后更新一下这个湖下一个 5 min能钓的鱼的数目,下一轮的 5 min再重新选出所有湖中 5 min内能钓的鱼最大数目,循环往复直到时间用完。
本来所要的的鱼数最优就是这么选出来的,但是这道题增加了湖与湖之间的时间消耗,那么无疑,如果在第 i 个湖虽然当下能钓的多一点,但是如果把走到第 i 个湖的时间用在 i 前面的湖钓鱼,可能会使得最终鱼数更大。这就导致了,我们并不知道要花多少时间在路途上多少时间在钓鱼上。
但是这道题湖的数目最多也就 25 个,粗略算一下,对于 sort 的 n * log n 时间消耗(这里的 n 不是湖的数目,而是需要排序的“每个湖每5 min能钓的鱼的数目”),数据都不是很大,我们可以考虑枚举,枚举假如John最终是停在了第 i 个湖。
所以分析下来,基本上代码逻辑就是:
- 枚举John是停在第 i 个湖的最优钓鱼策略
- 对于每一次枚举,先把第 0 个湖到第 i 个湖,所有每 5 min能钓到的鱼数目记录到数组fishs里面
- 对fishs数组排序,排序规则是假如鱼数目相等,湖序号小的排在前面;假如鱼数目不相等,那么鱼多的排前面,这样可以保证选出来的一定是符合所有要求的,如果这一步一时想不明白,拿个样例试一遍就ok了
- 算出有多少分钟能用在钓鱼,然后对这些分钟数除以 5 得到了一个数值 b,那么fishs数组前 b 个元素就是当前枚举的最优方案
- 比较是不是当前枚举的方案更优,如果是更新结果
- 按要求输出即可
(插一句……我一开始信心满满写完后,自己写的样例也过了,但一直wa一直wa,wa到我没脾气……需要注意的是,因为 f 和 d 题目只保证非负,假如 f 和 d 都是 0 的时候,而我一开始又是初始化最终最大鱼数目 cnt 为 0 ,这导致了 temp > cnt 一直为假,没有对最终方案进行更新过……好的 我长大了)
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
using namespace std;
#define ll long long
#define MEN(x, y) memset(x, y, sizeof x)
#define rin scanf("%d", &n)
#define rln scanf("%lld", &n)
#define rit scanf("%d", &t)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 10000;
//(val & 1) == 0偶, == 1奇。
#define getlength(array,len) {len = (sizeof(array) / sizeof(array[0]));}
struct lake{
//f - 起始的鱼数量
//d - 每5min减少的鱼
//t - 走到当前这个湖需要花在路途的时间
//minute - 这个湖最终停留时间
ll f, d, t, minute;
};
//每5min能获得第lake个湖num条鱼的数目
struct fish{
//
ll lake, num;
};
lake lakes[40];
fish fishs[6020];
//置零
void my_memset_fish() {
for (int i = 0; i < 400; ++ i) {
fishs[i].lake = 0;
fishs[i].num = 0;
}
}
void memset_lakes_minute(){
for (int i = 0; i < 30; ++ i) {
lakes[i].minute = 0;
}
}
bool cmp(fish &a, fish &b) {
if (a.num == b.num) return a.lake < b.lake;
return a.num > b.num;
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int n, c = 0;
ll h;
while (scanf("%d%lld", &n, &h) && n) {
//每个样例以空行隔开
if(c != 0)
printf("\n");
c = 1;
h *= 60;
for (int i = 0; i < n; ++ i) {
sc("%lld", &lakes[i].f);
}
for (int i = 0; i < n; ++ i) {
sc("%lld", &lakes[i].d);
}
lakes[0].t = 0;
for (int i = 1; i < n; ++ i) {
sc("%lld", &lakes[i].t);
lakes[i].t *= 5;
//前缀和 - 使得t代表的是走到当前这个湖所花在路途上的分钟数
lakes[i].t += lakes[i - 1].t;
}
ll cnt = -1; //钓到鱼的最大数
for (int i = 0; i < n; ++ i) {
ll hh = h;
hh -= lakes[i].t; //减去花在路途的时间,此时hh为最多能用在钓鱼的时间
if (hh <= 0) break;
int k = 0; //数组fishs的索引
my_memset_fish();
//将所有湖在每 5 min能钓到的鱼数目记录下来
for (int j = 0; j <= i; ++ j) {
ll numm = lakes[j].f;
ll a = 0; // a 是防止 d == 0 时while死循环
while (numm > 0 && a < hh / 5) {
fishs[k].lake = j;
fishs[k].num = numm;
numm -= lakes[j].d;
++ k;
++ a;
}
}
//能钓到鱼多的排前面,鱼一样多的按湖的序号小的排前面
sort(fishs, fishs + k, cmp);
//计算当前方案的鱼数目
ll temp = 0, length = hh / 5;
for (int j = 0; j < length; ++ j) {
temp += fishs[j].num;
}
//判断是否需要更新最终方案
if (temp > cnt) {
cnt = temp;
memset_lakes_minute();
for (int j = 0; j < length; ++ j) {
lakes[fishs[j].lake].minute += 5;
}
}
}
//输出
for(int i = 0; i < n; ++ i) {
printf("%lld",lakes[i].minute);
if(i!=n - 1)
printf(", ");
}
printf("\nNumber of fish expected: %lld\n",cnt);
}
return 0;
}
5、AcWing 1239. 乘积最大
原题链接:https://www.acwing.com/problem/content/description/1241/
思路
先对所有数正序排序,再分类讨论.
① k == n : 直接求积
② k < n
Ⅰ. k 为偶数
假如负数的个数为偶数, 那么最终乘积一定是整数;
反之, 如果负数个数是奇数, 那么由于 k < n , 完全可以从负数中剔除一个绝对值最小的负数, 使得负数数量为偶数.
此时用双指针从左右两边同时取两个数, 比较乘积, 直到个数满足 k 个即可.
Ⅱ . k 为奇数
假如所有数全为负数, 那么直接从数组右边开始乘 k 个数.
反之,假如不全为负数, 那么必然存在一个非负整数可以成为最终 k 个数中的一个, 那么此时就需要 k - 1(偶数) 个数, 和 Ⅰ的情况类似, 用双指针从两边取两个数比较即可.
#include<iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
const int modd = 1000000009;
#define ll long long
ll nums[N];
//计算从l到r的乘积
ll count1(int l, int r) {
ll res = 1;
while (l <= r) {
res *= nums[l ++];
res %= modd;
}
return res;
}
//获得在l到r(正序)之间任意k个数的最大乘积,保证k是偶数
//双指针
ll count2(int l, int r, int k) {
ll res = 1;
while (k > 0) {
k -= 2;
ll a = nums[l] * nums[l + 1];
ll b = nums[r - 1] * nums[r];
if (a < b) {
b %= modd;
res *= b;
res %= modd;
r -= 2;
}
else {
a %= modd;
res *= a;
res %= modd;
l += 2;
}
}
return res;
}
int main() {
int n, k;
scanf("%d%d", &n, &k);
int cnt = 0;
for (int i = 0; i < n; ++ i) {
scanf("%lld", &nums[i]);
if (nums[i] < 0) ++ cnt;
}
sort(nums, nums + n);
ll res = 1; //!不能初始化为0
if (k == n) res = count1(0, n - 1);
else if ((k & 1) && cnt == n) res = count1(n - k, n - 1);
else {
int l = 0, r = n - 1;
if (k & 1) {
res = nums[n - 1];
-- r;
-- k;
}
res *= count2(l, r, k);
res %= modd;
}
printf("%lld", res);
}
————————————————————————————————