# Greedy Algorithms By Colin --- ## Greedy Algorithms 貪婪演算法 - 每個步驟都選擇當下最佳的選擇 - 透過一次次局部最佳解獲得全局最佳解 - 通常具有較低的時間複雜度 - 不是所有問題都可以用Greedy解決 --- - 來看些例子吧 --- Example 1: 付錢問題 [CSDC267 請支援收銀](https://csdc.tw/problem/267/) [CSDC348 出國囉!](https://csdc.tw/problem/348/) 假設有1000元、500元、100元、50元、10元、5元、1元共七種面額的紙鈔各無限多張,若要付$n$元,則最少需要以幾張紙鈔支付(不可找零)? ---- 想法: - 只要一直以最大面額的鈔票支付,必定可以以最少張數湊出$n$元 - 例如: - 12789元= - 1000元\*12張 + 500元\*1張 + 100元\*2張 + 50元\*1張 + 10元\*3張 + 5元\*1張 + 1元\*4張 - 共24張鈔票 ---- CSDC267 請支援收銀 AC code ```cpp= #include <iostream> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int arr[7]={1000,500,100,50,10,5,1}; signed main(){ int n,ans=0; cin>>n; int a=n/1000; n=(a+1)*1000-n; for(int i=0;i<7;i++){ ans+=n/arr[i]; n%=arr[i]; } cout<<ans; return 0; } ``` ---- CSDC348 出國囉! AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; signed main(){ int N,a=0,b=0; cin>>N; N*=4; int value[10]={10000,5000,2000,1000,500,100,50,10,5,1}; for(int i=0;i<10;i++){ if(N>=value[i]){ if(i<=3) a+=N/value[i]; else b+=N/value[i]; N%=value[i]; } } cout<<a<<' '<<b<<'\n'; return 0; } ``` ---- - 付錢問題可以Greedy的條件是每種紙鈔的面額呈整數倍關係 - 若不是,則可能會有問題 - 例如:有1000元、900元、100元共三種面額的紙鈔,若要付1800元,若照前述作法需要1000元\*1張 + 100元\*8張 共9張鈔票,但最佳解為900元\*2張 共2張鈔票。遇到這種情形就要用DP解決(找零錢問題) ---- 找零錢問題[Minimizing Coins](https://cses.fi/problemset/task/1634) DP解(無限背包問題) ```cpp= //陣列太大會RE //dp(bottom up) //時間複雜度:O(nx) //空間複雜度:O(nx) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,x; vector<int> c(105); vector<vector<int>> dp(105,vector<int>(1000005,1e18)); signed main(){ //horaceorz; cin>>n>>x; for(int i=1;i<=n;i++) cin>>c[i]; for(int i=0;i<=n;i++) dp[i][0]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=x;j++){ if(j>=c[i]) dp[i][j]=min(dp[i-1][j],dp[i][j-c[i]]+1); else dp[i][j]=dp[i-1][j]; } } if(dp[n][x]>x) cout<<-1<<'\n'; else cout<<dp[n][x]<<'\n'; return 0; } ``` ---- AC code ```cpp= //dp(bottom up)+滾動數組優化 //時間複雜度:O(nx) //空間複雜度:O(x) #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,x; vector<int> c(105); vector<int> dp(1000005,1e18); signed main(){ //horaceorz; cin>>n>>x; for(int i=1;i<=n;i++) cin>>c[i]; dp[0]=0; for(int i=1;i<=n;i++){ for(int j=c[i];j<=x;j++){ dp[j]=min(dp[j],dp[j-c[i]]+1); } } if(dp[x]>x) cout<<-1<<'\n'; else cout<<dp[x]<<'\n'; return 0; } ``` --- Example 2: 最佳對戰組合問題 [zerojudge n175. 田忌賽馬](https://zerojudge.tw/ShowProblem?problemid=n175) 對戰雙方各有$n$名選手,要比$n$場比賽,每位選手都必須出戰一場。已知雙方每位選手的戰力值,以及對方出賽的順序,比賽時雙方派出的選手戰力值較高者勝,求我方最多能贏幾場比賽? ---- - 遇到類似Greedy問題就要大膽的猜~ ---- 想法: - 每次都以我方目前最弱的選手去對戰對方目前最弱的選手,一定可以得到最佳解 ---- 證明: - 假設我方目前最弱的戰力是$a$,對方目前最弱的戰力是$b$ - Case1: $a\le b$ - $a$必輸或平手,可以忽略 ---- - Case2: $a>b$ - 我方:$a,y$,對方:$b,x$ - 若最佳解中,$a$對戰$x$(輸贏不一定)而$y$對戰$b$(必贏),將其交換改成$a$對$b$(必贏)而$y$對$x$(輸贏不一定) - 根據假設可知$b<a\le y$且$b\le x$ - 若$b<a\le y\le x$,交換前後戰績皆不變 - 若$b<a\le x\le y$,交換後戰績可能增加或不變 - 若$b\le x<a\le y$,交換前後戰績皆不變 - 綜上所述,交換後的戰績不會比交換前差 - 所以「每次都以我方目前最弱的選手去對戰對方目前最弱的選手,一定可以得到最佳解」成立 ---- zerojudge n175. 田忌賽馬 AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,general[200005],king[200005],ans=0; signed main(){ horaceorz; cin>>n; for(int i=0;i<n;i++) cin>>general[i]; for(int i=0;i<n;i++) cin>>king[i]; sort(general,general+n); sort(king,king+n); for(int i=n-1,j=n-1;i>=0&&j>=0;i--){ if(general[i]<king[j]){ ans++; j--; } } cout<<ans<<'\n'; return 0; } ``` --- Example 3: 最小完成時間總和 [zerojudge n174. 非常不妙的寒假作業](https://zerojudge.tw/ShowProblem?problemid=n174) 有$n$項工作,已知完成每項工作所需花費時間,求每項工作完成的最小時間總和為何? ---- 例如有5項工作,完成每項工作所需花費時間分別為3,2,5,1,4 若按照這個順序, - 第一項作業完成的時間=3 - 第二項作業完成的時間=3+2=5 - 第三項作業完成的時間=3+2+5=10 - 第四項作業完成的時間=3+2+5+1=11 - 第五項作業完成的時間=3+2+5+1+4=15 - 每項作業完成的時間總和=3+5+10+11+15=44 ---- 想法: - 從花費時間最少的工作開始做,必可得到每項工作完成的最小時間總和 ---- 證明: - 假設最佳的工作順序是從第$1$項~第$n$項 - 若存在兩項工作符合$cost[i]>cost[i+1]$ - 若交換這兩項工作,第$1$~$i-1$項與第 $i+1$~$n$項工作完成的時間總和皆不會改變,但第$i$項完成的時間總和會變小 - 所以「從花費時間最少的工作開始做,必可得到每項工作完成的最小時間總和」成立 ---- zerojudge n174. 非常不妙的寒假作業 AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; int n,arr[200005],ans=0; signed main(){ horaceorz; cin>>n; for(int i=0;i<n;i++){ string s; cin>>s>>arr[i]; } sort(arr,arr+n); for(int i=0,p=0;i<n;i++){ p+=arr[i]; ans+=p; } cout<<ans<<'\n'; return 0; } ``` --- Example 4: 線段覆蓋長度 [2016.03 APCS 3. 線段覆蓋長度](https://zerojudge.tw/ShowProblem?problemid=b966) 在一維座標軸上有$n$段線段,已知每段線段的兩端點座標,求此$n$段線段覆蓋的總長度為何? ---- 想法: - 將所有線段依左端點座標排序 - 遍歷所有線段,設原線段左端點為$L$,右端點為$R$ - 若新線段$l>R$,則令$ans+=R-L,L=l,R=r$ - 若新線段$l\le R$且$r>R$,則令$R=r$ - 若新線段$l\le R$且$r\le R$,則不須更動$L,R$ ---- AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); #define l first #define r second using namespace std; int n,ans=0; vector<pair<int,int>> seg(10005); signed main(){ //horaceorz; cin>>n; for(int i=0;i<n;i++) cin>>seg[i].l>>seg[i].r; sort(seg.begin(),seg.begin()+n); int L=seg[0].l,R=seg[0].r; for(int i=1;i<n;i++){ if(seg[i].l>R){ ans+=R-L; L=seg[i].l; R=seg[i].r; }else if(seg[i].r>R){ R=seg[i].r; } } ans+=R-L; cout<<ans<<'\n'; return 0; } ``` --- Example 5: 交換的Greedy [CSDC238 不會整理書的笨蛋](https://csdc.tw/problem/238/) [2017.10 APCS 4. 物品堆疊](https://zerojudge.tw/ShowProblem?problemid=c471) ---- 想法: - 重量(W)、次數(T)皆會影響最佳放置順序 - 重量(W)越輕要放越上面 - 次數(T)越多要放越上面 - =>$\frac{重量(W)}{次數(T)}越小要放越上面$ - 假設第$i$本書放在第$i+1$本書上面 - =>$\frac{W_i}{T_i}<\frac{W_{i+1}}{T_{i+1}}$ - =>$W_i\times T_{i+1}<W_{i+1}\times T_i$ ---- AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; struct book{ int w,t; }; bool cmp(book a,book b){ return a.w*b.t<b.w*a.t; } int n,ans=0; vector<book> arr(100005); signed main(){ //horaceorz; cin>>n; for(int i=0;i<n;i++) cin>>arr[i].w; for(int i=0;i<n;i++) cin>>arr[i].t; sort(arr.begin(),arr.begin()+n,cmp); int W=arr[0].w; for(int i=1;i<n;i++){ ans+=W*arr[i].t; W+=arr[i].w; } cout<<ans<<'\n'; return 0; } ``` --- Example 6: 排程問題 [2023.01 APCS 4. 機器出租](https://zerojudge.tw/ShowProblem?problemid=j608) ---- 想法: - 將所有活動依照結束時間排序 - 設$free[i]$表示第$i$個機器上一個舉辦的活動的結束時間 - 如果$k$個機器中$free$最小的$\ge$當前活動的開始時間,則此活動不可能被舉辦,不必繼續檢查 - 否則找到可以舉辦當前活動的機器中free最大的=>浪費的時間最少 ---- AC code ```cpp= #include <bits/stdc++.h> #define int long long #define horaceorz ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; struct job{ int l,r; }; bool cmp(job a,job b){ return a.r<b.r; } int n,k,ans=0; vector<job> arr(100005); vector<int> Free(105,0); signed main(){ //horaceorz; cin>>n>>k; for(int i=0;i<n;i++) cin>>arr[i].l; for(int i=0;i<n;i++) cin>>arr[i].r; sort(arr.begin(),arr.begin()+n,cmp); for(int i=0;i<n;i++){ int mini=1e18,idx_mini; for(int j=0;j<k;j++){ if(Free[j]<mini){ mini=Free[j]; idx_mini=j; } } if(mini>=arr[i].l) continue; int maxi=-1e18,idx_maxi; for(int j=0;j<k;j++){ if(Free[j]<arr[i].l){ if(Free[j]>maxi){ maxi=Free[j]; idx_maxi=j; } } } Free[idx_maxi]=arr[i].r; ans++; } cout<<ans<<'\n'; return 0; } ``` --- 練習題: [CSDC205 泡泡與她的小石頭](https://csdc.tw/problem/205/) [CSDC192 種菜好好玩](https://csdc.tw/problem/192/) [CSDC316 當你發現你的Greedy不夠Greedy](https://csdc.tw/problem/316/) [112北二 3a.忍術學園的期中考](https://zerojudge.tw/ShowProblem?problemid=m603) --- Thank you for listening!
{"description":"By Colin","title":"Greedy Algorithms","contributors":"[{\"id\":\"fd160699-cd06-45fd-abd0-c09edbc85980\",\"add\":9248,\"del\":709}]"}
    105 views