NOIP2018提高组金牌训练营——贪心算法专题

地址在这http://www.51nod.com/live/liveDescription.html#!liveId=18

pdf   http://wwwwodddd.com/Greed.pdf

题目链接 https://www.51nod.com/contest/problemList.html#!contestId=54&randomCode=882897

(这是免费的,我和他们没有任何利益关系,只是这是一个很好的学习的机会)

 A.低买高买

这道题需要反过来思考,这里的思路是先卖,然后根据需要再反悔, 有点类似网络流里面的反向弧的思想

首先假装已经有了股票,然后卖掉得到收益。

但是因为实际上没有,所以我们有两种操作,一种是把卖的改成持有, 一种是持有改成

买,这两种操作都可以让现在多一个股票,这个股票也就是刚才卖掉的股票

设股票价钱为x,则这两种操作都会使得当前收益少掉x

那么也就是说每一次我们都要先卖掉一支,然后执行两个操作中的一个来补回来

那么因为要钱最多,那么两个操作都是会让收益变少的,那么我们就要让操作所减少的收益最小。

所以我们可以把操作储存起来,然后优先队列排序,每次卖掉当前股票的同时执行最优的操作

最后输出答案就好

#include<cstdio>
#include<queue>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

int main()
{
	priority_queue<int> q;
	int n, x, ans = 0;
	scanf("%d", &n);
	REP(i, 0, n)
	{
		scanf("%d", &x);
		q.push(-x); q.push(-x); //负数为了实现小根堆 
		ans += x + q.top();    //这里q.top()是负数,两个操作都是减少x元 
		q.pop();              
	}
	printf("%d\n", ans);
	return 0;
}

B.排队接水

相信大家以前都做过,比较水。
两个思路。一个从宏观的角度来考虑,肯定是实际花的少的人在前面更优
第二个从微观的角度,相邻的两个人。设第一个人花时间a,第二个人花时间b
如果第一个人在前,那么总时间为b + 2a
如果第一个人在前,那么总时间为a + 2b
b + 2a与a + 2b, 统统减去a+b
变成a和b,也就是说谁时间短就排在前面更优

#include<cstdio>
#include<algorithm>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 1123;
int a[MAXN], n, ans;

int main()
{
	scanf("%d", &n);
	REP(i, 0, n) scanf("%d", &a[i]);
	sort(a, a + n);
	REP(i, 0, n) ans += (n - i) * a[i];
	printf("%d\n", ans);
	return 0;
}

C.接水问题2

这道题是上一题的升级版,这个时候不能用第一种思路了,因为还和重要性有关
要用第二种思路,即考虑相邻两个人的情况
设第一个人重要性a[x], 时间b[x], 同理第二个人a[y], b[y]
那么如果第一个人在前,则b[x] * a[x] + a[y] * (b[x] + b[y])
那么如果第二个人在前,则b[y] * a[y] + a[x] * (b[x] + b[y])
化简之后可得a[y] * b[x] 与 a[x] * b[y]
那么我们就比较这两个就好了
然后这里还要注意有个坑,有可能为0
时间为0,放在第一个,对答案没有贡献
重要性为0(心疼),放在最后一个,对答案没有贡献
所以直接在输入的时候忽略掉这组数据即可,即n--, i--(学到了)
最后注意开long long
 

#include<cstdio>
#include<algorithm>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define fi first 
#define se second 
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN];

bool cmp(pair<int, int> a, pair<int, int> b)
{
	return a.se * b.fi < a.fi * b.se;
}

int main()
{
	int n;
	scanf("%d", &n);
	REP(i, 0, n) 
	{
		scanf("%d%d", &a[i].fi, &a[i].se);
		if(!a[i].fi || !a[i].se) i--, n--;
	}
	sort(a, a + n, cmp);
	
	long long time = 0, ans = 0;
	REP(i, 0, n)
	{
		time += a[i].se;
		ans += time * a[i].fi;
	}
	printf("%lld\n", ans);
	
	return 0;
}

D.做任务一

两个思路
(1)根据右端点排序,然后扫一遍,维护最后一个区间的右端点
每次新的区间看其左端的大不大于维护的右端点,能放就放
(2)根据左端点排序,同样能放就放,如果不能放的话试图使最后
一个区间的右端点更靠左,这样有利于后面的区间。

//右端点 
#include<cstdio>
#include<algorithm>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define fi first 
#define se second 
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN];

bool cmp(pair<int, int> x, pair<int, int> y)
{
	return x.se < y.se || x.se == y.se && x.fi < y.fi;
}

int main()
{
	int T, n, m;
	scanf("%d", &T);
	
	while(T--)
	{
		scanf("%d%d", &n, &m);
		REP(i, 0, n) scanf("%d%d", &a[i].fi, &a[i].se);
		sort(a, a + n, cmp);
	
		int ans = 0, last = 0;
		REP(i, 0, n)
			if(a[i].fi >= last)
			{
				ans++;
				last = a[i].se;
			}
		printf("%d\n", ans);
	}
	
	return 0;
}
//左端点 
#include<cstdio>
#include<algorithm>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define fi first 
#define se second 
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN];

int main()
{
	int T, n, m;
	scanf("%d", &T);
	
	while(T--)
	{
		scanf("%d%d", &n, &m);
		REP(i, 0, n) scanf("%d%d", &a[i].fi, &a[i].se);
		sort(a, a + n);
	
		int ans = 0, last = 0;
		REP(i, 0, n)
		{
			if(a[i].fi >= last)
			{
				ans++;
				last = a[i].se;
			}
			else if(a[i].se < last) 
				last = a[i].se;
		}
		printf("%d\n", ans);
	}
	
	return 0;
}

E.做任务三

依然两种做法
(1)按左端点排序,然后把结束时间加入优先队列。
每当有一个新任务的时候,看最早结束的人是否可以做,能做就做
不能做就加一个人。
其实仔细想想,当现在的任务有多个人能做的时候,无论谁做
都是最优的,不一定非要结束最早的能做,但是优先队列的作用
在于能不能至少有一个人能做,所以就看最早的那个。
(2)按照右端点排序,然后同样维护结束时间。
每当有一个新任务的时候,看最早结束的人是否可以做,如果能做
那么这里就要选结束最晚的那个人来做,因为要留下结束早的给
后面的任务。在这个思路里面如果有多个人能做,就要有抉择了,
和上一个思路不一样。同时因为这是要选择数据结构里面中间的
一个元素,所以我们用multiset。我是听了这个才知道有multiset
这个东西,以前都是用set的。
区别就是multiset允许有重复元素,set不允许,而这道题而言
结束时间是有可能一样的,是可以重复的,所以用multiset。

//左端点 
#include<cstdio>
#include<algorithm>
#include<queue>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define fi first 
#define se second 
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN];

int main()
{
	int T, n, m;
	scanf("%d", &T);
	
	while(T--)
	{
		scanf("%d%d", &n, &m);
		REP(i, 0, n) scanf("%d%d", &a[i].fi, &a[i].se);
		sort(a, a + n);
		
		priority_queue<int> q;
		int ans = 0;
		REP(i, 0, n)
		{
			if(q.size() && -q.top() <= a[i].fi) q.pop();
			else ans++;
			q.push(-a[i].se);
		}
		printf("%d\n", ans);
	}
	
	return 0;
}
//右端点
#include<cstdio>
#include<algorithm>
#include<set>
#include<vector>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define fi first 
#define se second 
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN];

int main()
{
	int T, n, m;
	scanf("%d", &T);
	
	while(T--)
	{
		scanf("%d%d", &n, &m);
		REP(i, 0, n) scanf("%d%d", &a[i].fi, &a[i].se);
		sort(a, a + n);
		
		multiset<int> s;
		int ans = 0;
		REP(i, 0, n)
		{
			if(s.size() && *s.begin() <= a[i].fi)
				s.erase(--s.upper_bound(a[i].fi))
			else ans++;
			s.insert(a[i].se);
		}

		printf("%d\n", ans);
	}
	
	return 0;
}


F.字符串连接

这道题不能直接用字典序,比如 b ba 按照字典序答案是bba,但是显然bab更优

所以这里用了一个很牛逼的比较方法,就是直接比较连接之后的大小

比较a + b与 b + a,这样的比较我还是头一次看到,牛逼。

#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 112;
string s[MAXN]; 

bool cmp(string a, string b)
{
	return a + b < b + a; 
}

int main()
{
	int n;
	scanf("%d", &n);
	REP(i, 0, n) cin >> s[i];
	sort(s, s + n, cmp); 
	REP(i, 0, n) cout << s[i];
	cout << endl; 
	return 0;
}

G. 缓存交换

我纠结了好久好久为什么是删去下次最远的那个是最优的,但还不是非常清楚

我们看样例,3要进来的时候1和2选择,如果选择下一次近一些的1,下一次1要再进来的时候

会把2或3给踢出去,这样后面2和3进来就又要多一次
而选择较远的2的话,只需要花后来2进来的次数。真正比赛的时候就手算样例吧,然后凭着直觉选下一次最远的。
实现的时候记得下标从1开始,不然会出事,代码注释里面有写

#include<cstdio>
#include<set>
#include<map>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 112345;
int a[MAXN], p[MAXN];
int n, m;
set<int> s;
map<int, int> g;

int main()
{
	scanf("%d%d", &n, &m);
	REP(i, 1, n + 1) scanf("%d", &a[i]);
	REP(i, 1, n + 1)
	{
		p[i] = n + 1;
		p[g[a[i]]] = i; //注意这里,如果a[i]是第一次那么g[a[i]] = 0 
		g[a[i]] = i;   //显然这里会覆盖掉第一个位置的值 
	}                 //所以下标从1开始 
	
	int ans = 0;
	REP(i, 1, n + 1)
	{
		if(s.find(i) != s.end()) s.erase(i);	
		else
		{
			ans++;
			if(s.size() == m)
				s.erase(--s.end());
		}
		s.insert(p[i]);
	}
	printf("%d\n", ans);
	 
	return 0;
}

H.挑剔的美食家

思路很简单,就是每个奶牛选最便宜的草就好了。

实现的话要让品质从大到小排序,然后枚举奶牛,在集合中加入能选的草,然后选最便宜的。

排序是因为这样就品质而言, 前面的奶牛可以选的草后面的奶牛一定在品质上是满足的
可以说是节省了时间,然后再从价格上去挑选符合且最便宜的就可以了。

#include<cstdio>
#include<set>
#include<vector>
#include<algorithm>
#include<functional>
#define fi first
#define se second
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 112345;
pair<int, int> a[MAXN], b[MAXN];
int n, m, p;
multiset<int> s;

int main()
{
	scanf("%d%d", &n, &m);
	REP(i, 0, n) scanf("%d%d", &a[i].se, &a[i].fi), a[i].fi *= -1;
	REP(i, 0, m) scanf("%d%d", &b[i].se, &b[i].fi), b[i].fi *= -1;
	sort(a, a + n);
	sort(b, b + m);
	
	long long ans = 0;
	REP(i, 0, n)
	{
		while(p < m && b[p].fi <= a[i].fi) s.insert(b[p++].se);
		multiset<int>::iterator it = s.lower_bound(a[i].se);
		if(it == s.end()) { ans = -1; break; }
		else { ans += *it; s.erase(it); }
	}
	printf("%lld\n", ans);
	 
	return 0;
}

I.最高的奖励

按照时间排序,然后维护一个价值从小到大的优先队列。
每一次不管能不能做,先加入,如果发现不能做的话,
那么就删除价值最小的即可
先做后来可以反悔。

#include<cstdio>
#include<vector>
#include<algorithm>
#include<queue>
#define fi first
#define se second
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 51234;
pair<int, int> a[MAXN];
int n;

int main()
{
	scanf("%d", &n);
	REP(i, 0, n) scanf("%d%d", &a[i].fi, &a[i].se);
	sort(a, a + n);
	
	long long ans = 0;
	priority_queue<int> q;
	REP(i, 0, n)
	{
		ans += a[i].se;
		q.push(-a[i].se);
		if(q.size() > a[i].fi)
		{
			ans += q.top();
			q.pop();
		}
	}
	printf("%lld\n", ans);
	 
	return 0;
}

J.夹克老爷的逢三抽一

每次选择价值最高的m[i], 然后把m[i]改成m[i+1] + m[i-1] - m[i]
表示可以后悔,如果后悔的话就是不选m[i],选m[i+1]和m[i-1]
另外注意用链表

#include<cstdio>
#include<vector>
#include<set>
#define fi first
#define se second
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

typedef long long ll;
const int MAXN = 112345;
int L[MAXN], R[MAXN], n;
ll m[MAXN];
set<pair<ll, int> > s;

void insert(int i) { s.insert(make_pair(m[i], i)); }
void erase(int i) { s.erase(make_pair(m[i], i)); }
void del(int i) 
{ 
	erase(i);
	L[R[i]] = L[i];
	R[L[i]] = R[i];
}

int main()
{
	scanf("%d", &n);
	REP(i, 0, n) 
	{
		scanf("%lld", &m[i]);
		insert(i);
		L[(i + 1) % n] = i;
		R[i] = (i + 1) % n;
	}
	
	ll ans = 0;
	REP(i, 0, n / 3)
	{
		int j = (--s.end())->se;
		ll a = m[L[j]], b = m[j], c = m[R[j]];
		ans += b;
		del(L[j]), del(R[j]);
		erase(j);
		m[j] = a + c - b;
		insert(j);
	}
	printf("%lld\n", ans);
	 
	return 0;
}

总结:贪心一般是第一题和第二题,偏简单。比赛的时候可以手算样例然后看直觉来贪心。

猜你喜欢

转载自blog.csdn.net/qq_34416123/article/details/81271450