1. 程式人生 > >NOIP2018提高組金牌訓練營——貪心演算法專題

NOIP2018提高組金牌訓練營——貪心演算法專題

(這是免費的,我和他們沒有任何利益關係,只是這是一個很好的學習的機會)

 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;
}

總結:貪心一般是第一題和第二題,偏簡單。比賽的時候可以手算樣例然後看直覺來貪心。