ACM - 贪心 - 基础(区间问题 + Huffman树 + 排序不等式 + 绝对值不等式 + 推公式)


总的来说,贪心就是对于求解最优解问题,每一步都做当前最优的措施,当然这需要子问题最优必定能推出总的最优才能这么做,不然很可能需要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 个湖。

所以分析下来,基本上代码逻辑就是:

  1. 枚举John是停在第 i 个湖的最优钓鱼策略
  2. 对于每一次枚举,先把第 0 个湖到第 i 个湖,所有每 5 min能钓到的鱼数目记录到数组fishs里面
  3. 对fishs数组排序,排序规则是假如鱼数目相等,湖序号小的排在前面;假如鱼数目不相等,那么鱼多的排前面,这样可以保证选出来的一定是符合所有要求的,如果这一步一时想不明白,拿个样例试一遍就ok了
  4. 算出有多少分钟能用在钓鱼,然后对这些分钟数除以 5 得到了一个数值 b,那么fishs数组前 b 个元素就是当前枚举的最优方案
  5. 比较是不是当前枚举的方案更优,如果是更新结果
  6. 按要求输出即可

(插一句……我一开始信心满满写完后,自己写的样例也过了,但一直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);
} 

————————————————————————————————

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值