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;
}
總結:貪心一般是第一題和第二題,偏簡單。比賽的時候可以手算樣例然後看直覺來貪心。