ACM - 贪心(经典母题+POJ一些练习题)

Posted 肆呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM - 贪心(经典母题+POJ一些练习题)相关的知识,希望对你有一定的参考价值。


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

以上是关于ACM - 贪心(经典母题+POJ一些练习题)的主要内容,如果未能解决你的问题,请参考以下文章

ACM - 贪心(经典母题+POJ一些练习题)

ACM - 贪心(经典母题+POJ一些练习题)

ACM入门学啥

2021算法竞赛入门班第一节课枚举贪心习题

[ACM] POJ 1328 Radar Installation (贪心,区间选点问题)

ACM/ICPC 之 经典动规(POJ1088-滑雪)