貪心演算法(Greedy algorithm)
特性:
- 算法由一連串決定組成(由大到小逐一考慮各種代幣的數量),每次考慮當前最好
決定(能換盡量換),選擇後就不再更改。
- 算法的正確性必須經過證明,內容就是要證明所下的決定是對的,也就是一定有一
個最佳解包含了貪心法所做的決定,而證明的方法通常都是用換的:假設有一個最
佳解跟貪心法做的決定不一樣,那麼,我們可以換成一個與貪心法一樣的決定,而
解會更好,或至少不會變差。
note:個人覺得貪心是必須在數學規律上下功夫,找到題目的特性,所以貪心是程式簡單,但想出規律卻是難的
優先佇列(priority_queue)
- top會是最大值
- 如果要讓top為最小值可以加上負號讓大小關係反轉
priority_queue<pair<int,int>> pq;
會先比對pair的first
- 加入新元素(push),O(log(n))
- 查詢最大值(top),O(1)
- 刪除最大值(pop),O(log(n))
- 檢查是否為空(empty),O(1)
- 比set快兩倍以上!!!
例題 P-4-2. 笑傲江湖之三戰
https://judge.tcirc.tw/problem/d043
都是用貪心算法,找到最優解法
自己寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <bits/stdc++.h>
using namespace std; bool cmp(int a,int b){ return a<b; } int main() { int n; cin>>n; int temp; int ans=0; deque<int>pq1; deque<int>pq2;
for(int i=0;i<n;i++){ cin>>temp; pq2.push_back(temp); } for(int i=0;i<n;i++){ cin>>temp; pq1.push_back(temp); } sort(pq1.begin(),pq1.end(),cmp); sort(pq2.begin(),pq2.end(),cmp);
while(!pq1.empty()){ if(pq1.front()>pq2.front()){ pq1.pop_front(); pq2.pop_front(); ans++; }else{ pq1.pop_front(); pq2.pop_back(); } } cout<<ans; return 0; }
|
例題 P-4-3. 十年磨一劍 (最少完成時間)
https://judge.tcirc.tw/problem/d044
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <bits/stdc++.h> typedef long long LL; using namespace std;
int main() { LL n; cin>>n; priority_queue<LL,vector<LL>,greater<LL>> pq; LL temp; for(LL i=0;i<n;i++){ cin>>temp; pq.push(temp); } LL ans=0; temp=0; for(LL i=0;i<n;i++){ temp+=pq.top(); pq.pop(); ans+=temp; } cout<<ans; return 0; }
|
P-4-4. 幾場華山論劍(activity selection)
https://judge.tcirc.tw/problem/d045
「若[x,y]是所有現存線段中右端點最小的,那麼一定有個最佳解是挑選了[x,y]。」
證明:假設最佳解沒有挑[x,y],令[a,b]是最佳解中右端點最小的。根據我們的假設,
y <= b,因此將[x,y]取代[a,b]不會重疊到任何最佳解中的其他線段,我們可以得到
另外一個最佳解包含[x,y]。
根據此性質,我們可以不斷地將最小右端的線段挑選進解答,然後略過任何與它重疊的
線段。因為線段不會改變,一開始依照右端由小排到大,然後從頭到尾掃描一次就可以
得到答案。
自己寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <bits/stdc++.h> typedef long long LL; using namespace std; struct act{ int l; int r;
}; bool cmp(act a,act b){ return a.r<b.r; } int main() { int n; LL temp1,temp2; act s[100010]; cin>>n; for(int i=0;i<n;i++){ cin>>s[i].l>>s[i].r; } sort(s,s+n,cmp); LL ans=1; temp1=s[0].r; for(int i=1;i<n;i++){ if(temp1<s[i].l){ temp1=s[i].r; ans++; }
} cout<<ans;
return 0; }
|
P-4-5. 嵩山磨劍坊的問題(加權最小完成時間)
https://judge.tcirc.tw/problem/d046
有權重的問題都要繞彎……
重點是數學上加權的比較,觀察發現t/w的數值越小應該要排在越前面
證明:

經過推導:變化量=W[i]t[i+1]-w[i+1]*t[i]
假設t[i]/w[i] > t[i+1]/w[i+1]
交叉相乘得到
w[i+1]*t[i]<t[i+1]*w[i]
運算
w[i]*t[i+1]-w[i+1]*t[i]<0
也就是變化量為負,交換解會更好
自己寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<bits/stdc++.h> typedef long long LL;
using namespace std;
bool cmp(pair<int,int>a,pair<int,int>b){ return a.first*b.second<b.first*a.second; }
int main(){ int n; cin>>n; LL temp; deque<pair<int,int>>dq; for(int i=0;i<n;i++){ cin>>temp; dq.push_back({temp,0}); } for(int i=0;i<n;i++){ cin>>temp; dq[i]={dq[i].first,temp}; } sort(dq.begin(),dq.end(),cmp); LL total=0; temp=0; for(auto t:dq){ temp+=t.first; total+=temp*t.second; } cout<<total; return 0; }
|
Q-4-6. 少林寺的自動寄物櫃 (APCS201710)
習題
發現要自己推導出數學規律是十分困難且耗時的,而且很需要數感的天分,以後寫到背包問題後再重新寫一次看看。
自己寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include<bits/stdc++.h> typedef long long LL;
using namespace std;
bool cmp(pair<int,int> a,pair<int,int> b){ return a.first*b.second<b.first*a.second; }
int main(){ int n; cin>>n; LL temp; deque<pair<int,int>> dq; for(int i=0;i<n;i++){ cin>>temp; dq.push_back({temp,0}); } for(int i=0;i<n;i++){ cin>>temp; dq[i]={dq[i].first,temp}; } sort(dq.begin(),dq.end(),cmp); temp=0; LL ans=0; bool valid=true; for(auto t:dq){ if(valid){ valid=false; temp+=t.first; continue; } ans+=temp*t.second; temp+=t.first; } cout<<ans; return 0; }
|
P-4-7. 岳不群的併派問題 (Two-way merge) (*)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <bits/stdc++.h>
using namespace std;
int main() { int n; cin>>n; long long temp; priority_queue<long long ,vector<long long>,greater<long long>> pq; long long sum=0; for(int i=0;i<n;i++){ cin>>temp; sum+=temp; pq.push(temp); } long long ans=0; long long t; while(!pq.empty()){ temp=pq.top(); pq.pop(); t=(pq.top()+temp); pq.pop(); pq.push(t); ans+=t; if(pq.size()<=1) break; } cout<<sum<<'\n'<<ans; return 0; }
|
note. priority q
設定的是greater與less
Q-4-8. 先到先服務 (*)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <bits/stdc++.h> typedef long long LL; using namespace std;
int main() { LL n,m; cin>>n>>m; priority_queue<LL,vector<LL>,greater<LL>> pq; LL temp; LL x; for(LL i=0;i<n;i++){ cin>>x; if(((LL)pq.size())>=m){ temp=pq.top(); pq.pop(); x+=temp; pq.push(x); }else{ pq.push(x); } } while(pq.size()>1){ pq.pop(); } cout<<pq.top();
return 0; }
|
貪心演算法經常可以搭配二分搜
P-4-9. 基地台 (APCS201703)
講義裡的答案
服務點可以看成數線上的點,基地台可以看成數線上的線段,要計算以K根長度相同的
線段蓋住所有的點,最小的線段長度。這一題是給數量K要求長度R。要解決這個問題,
先看以下問題:輸入給數線上N個點以及R,請問最少要用幾根長度R的線段才能蓋住
所有輸入點。
note.他要的就是最短的線段,所以她的做法是找出最短的線段還要短的那個,找出來後+1就是答案了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <bits/stdc++.h>
using namespace std; int p[600000],n,k; bool enough(int r){ int nseg=k,endline=-1; for(int i=0;i<n;i++){ if(p[i]<=endline) continue; if(nseg==0) return false; nseg--; endline=p[i]+r; } return true; } int main() { cin>>n>>k; for(int i=0;i<n;i++){ cin>>p[i]; } sort(p,p+n); int len=0,L=p[n-1]-p[0]; for(int jump=L/2;jump>0;jump/=2){ while(len+jump<L&&!enough(len+jump)){ len+=jump; } } cout<<len+1; return 0; }
|
Q-4-10. 恢復能量的白雲熊膽丸
https://judge.tcirc.tw/problem/d054
雖然是一樣的練習題,不過自己寫出來成就感很大,一樣是找比最小的體力值更小的那個,二分搜尋法真是神奇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <bits/stdc++.h>
using namespace std; int p[5000000],n,m; bool enough(int F){ int time=m; int body=F; for(int i=0;i<n;i++){ if(body-p[i]>=0){ body-=p[i]; continue; } body=F; if(body<p[i]) return false; body-=p[i]; time--; if(time<0) return false; } return true; } int main() {
cin>>n>>m; int maxx=0; for(int i=0;i<n;i++){ cin>>p[i]; maxx+=p[i]; } int F=0; for(int jump=maxx;jump>0;jump/=2){ while(!enough(F+jump)){ F+=jump; } } cout<<F+1; return 0; }
|
P-4-11. 線段聯集 (APCS 201603)
https://judge.tcirc.tw/problem/d050
以下解法為tle解,因為下率實在是太差了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <bits/stdc++.h> typedef long long LL; using namespace std; LL s[100000050]={}; int main() { LL n; LL a,b; cin>>n; LL minn=1000000,maxx=-1; for(LL i=0;i<n;i++){ cin>>a>>b; minn=min(a,minn); maxx=max(b,maxx); for(LL j=a;j<b;j++){ s[j]++; } }
LL sum=0; for(LL i=minn;i<=maxx;i++){ if(s[i]>=1){ sum+=1; } } cout<<sum; return 0; }
|
用以下方法掃描:
時間複雜度為O(n*logn)就可以通過題目了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <bits/stdc++.h>
using namespace std; priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>pq; int main() { int n; cin>>n; int a,b; for(int i=0;i<n;i++){ cin>>a>>b; pq.push({a,b}); } pair<int,int>last=pq.top(); pq.pop(); int sum=0; while(!pq.empty()){ while(last.second>pq.top().first){ if(last.second<pq.top().second)last.second=pq.top().second; pq.pop(); if(pq.empty()){ break; } } sum+=(last.first-last.second); if(pq.empty()){ cout<<-sum; return 0; } last=pq.top(); pq.pop(); } sum+=(last.first-last.second); cout<<-sum; return 0; }
|
P-4-12. 一次買賣
https://judge.tcirc.tw/problem/d051
想像成一條掃描線
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <bits/stdc++.h>
using namespace std;
int main() { int n; cin>>n; long long s[n]; for(int i=0;i<n;i++){ cin>>s[i]; } long long minn=s[0]; long long maxx=-1; for(int i=1;i<n;i++){ maxx=max(maxx,s[i]-minn); minn=min(minn,s[i]); } cout<<maxx; return 0; }
|
P-4-13. 最大連續子陣列 (同 P-5-2)
https://judge.tcirc.tw/problem/d052
P-4-13與P-4-12有何關係呢?P-4-12的輸入是每日價格,如果把每日價格與前一
日的價差放在A[]中,A[i]=p(i)-p(i-1),那麼就變成P-4-13了,因為連續一段
時間的累積價差就等於最後一日與最前一日的價差
A[i]+A[i+1]+…A[j]=(p(i)-p(i-1))+(p(i+1)-p(i))+…+(p(j)-p(j-1))
=p(j)-p(i-1)
note.我覺得有點數學理論了,一般很難想的到
這裡的想法是原先給的陣列是一個差值陣列(A),然後把他轉成真正的數值陣列(P),那數值陣列中的max-min就會是差值陣列的解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <bits/stdc++.h> typedef long long LL; using namespace std;
int main() { LL temp; int n; cin>>n; LL s[100010]={}; s[0]=0; for(int i=1;i<=n;i++){ cin>>temp; s[i]=s[i-1]+temp; } LL minn=s[0]; LL maxx=-1; for(int i=1;i<=n;i++){ maxx=max(maxx,s[i]-minn); minn=min(minn,s[i]);
} if(maxx>0)cout<<maxx; else cout<<0; return 0; }
|
而為了更直觀,講義裡提到了:Kadane's Algorithm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <bits/stdc++.h>
using namespace std; typedef long long LL; int main() { LL n,max_sum=0; LL p_max=0; cin>>n; for(int i=0;i<n;i++){ LL p; cin>>p; p_max=(p_max>0)? p_max+p:p; max_sum=max(max_sum,p_max); } cout<<max_sum; return 0; }
|
程式邏輯(Kadane’s Algorithm)
這個程式實現了 Kadane 演算法,用於解決最大子數組和問題。
核心思想是:
遍歷數組,對於每個元素,決定是將其加入前面的子數組,還是從該元素開始新子數組。
維護兩個變數:
- p_max:以當前元素結尾的子數組的最大和。
- max_sum:全局的最大子數組和。
如果 p_max 為負數,則捨棄之前的子數組,從當前元素重新開始,因為負數只會減小後續的和。
P-4-14. 控制點 (2D-max)
https://judge.tcirc.tw/problem/d055
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <bits/stdc++.h>
using namespace std; typedef long long LL; int main() { int n; cin>>n; pair<LL,LL> s[100010]; for(int i=0;i<n;i++){ cin>>s[i].first; } for(int i=0;i<n;i++){ cin>>s[i].second; } sort(s,s+n);
int ans=1; LL max_y=s[n-1].second; for(int i=n-2;i>=0;i--){ if(s[i].second>max_y){ ans++; } max_y=max(max_y,s[i].second);
} cout<<ans; return 0; }
|
P-4-15. 最靠近的一對(closest pair) (@@)
https://judge.tcirc.tw/problem/d056
tle解,正解以後再學
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <bits/stdc++.h>
using namespace std; typedef long long LL; int main() { int n; cin>>n; pair<LL,LL> s[100010]; for(int i=0;i<n;i++){ cin>>s[i].first>>s[i].second; } sort(s,s+n); LL minn=1000000000; deque<LL> q; q.push_front(0); for(int i=1;i<n;i++){ if(abs(s[i].first-s[q.front()].first)+abs(s[i].second-s[q.front()].second)>minn){ q.pop_front(); }
for(auto t:q){ minn=min(minn,abs(s[i].first-s[t].first)+abs(s[i].second-s[t].second)); } q.push_back(i); } cout<<minn; return 0; }
|