目錄:
- 個人理解
- 反悔貪心的分類
- 反悔自動機
- 反悔堆
- 例題簡析及代碼
一、個人理解:
貪心本身是沒有反悔操作的,貪心求的就是當前的最優解。但當前的最優解有可能是局部最優解,而不是全局最優解,這時候就要進行反悔操作。
反悔操作指的是這一步的貪心不是全局最優解,我們就退回去一步(人工或自動判斷),換一種貪心策略。按照判斷方式的不同可以分為反悔自動機和反悔堆兩種方法。
二、反悔貪心的分類:
-
反悔自動機:
即設計一種反悔策略,使得隨便一種貪心策略都可以得到正解。
基本的設計思路是:每次選擇直觀上最接近全局最優解的貪心策略,若發現最優解不對,就想辦法自動支持反悔策略。(這就是自動機的意思)
具體題目具體分析。一般需要反悔自動機的題都是通過差值巧妙達到反悔的目的。
-
反悔堆:
即通過堆(大根堆、小根堆)來維護當前貪心策略的最優解,若發現最優解不對,就退回上一步,更新最優解。
由於堆的性質,使得堆的首數據一定是最優的,這就可以實現快速更新最優解。
三、例題簡析及代碼
-
USACO09OPEN 工作調度Work Scheduling (反悔堆)
Description:
有 \(n\) 項工作,每 \(i\) 項工作有一個截止時間 \(D_i\) ,完成每項工作可以得到利潤 \(P_i\) ,求最大可以得到多少利潤。
Method:
做這道題的時候並沒有想到反悔貪心,只是想到一個錯誤的貪心算法。按照截止時間為第一關鍵字,利潤為第二關鍵字排序,統計一遍即可。
顯然上面的貪心算法刻印被Hack掉。可以先不選擇當前截止時間的利潤,等一下選擇下一個更大的利潤,這樣可以得到更大的最優解。
但我們發現這個貪心策略錯誤的原因是當前的最優解可能不是全局最優解,顯然符合反悔貪心的思想。於是我們用一個反悔堆維護最優解。
假如滿足題設條件(即沒有超出截止時間)就分成兩種情況:若當前的最優解比原來的最優解(堆頂)更優秀,我們就更新全局最優解,把原來的最優解丟出去,再把當前的最優解放進去(即反悔策略);反之,就不管了。假如不滿足特設條件,就把當前的最優解丟進堆里,更新全局最優解即可。
Code:
#include<bits/stdc++.h> #define int long long #define Maxn 100010 inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } using namespace std; int n; struct node { int D,P; bool operator <(const node &x)const { return D<x.D; } }job[Maxn]; priority_queue<int,vector<int>,greater<int> >qu; signed main() { // freopen("Job.in","r",stdin); // freopen("Job.out","w",stdout); read(n); for(int i=1;i<=n;i++) { read(job[i].D),read(job[i].P); } sort(job+1,job+n+1); int ans=0; for(int i=1;i<=n;i++) { if(qu.size()>=job[i].D)//符合條件 { if(qu.top()<job[i].P)//當前的最優解比原來的最優解(堆頂)更優秀 { ans-=qu.top();//更新全局最優解 qu.pop();//把原來的最優解丟出去 qu.push(job[i].P);//把當前的最優解放進去 ans+=job[i].P;//更新全局最優解 } }else//不符合條件 { qu.push(job[i].P);//把當前的最優解丟進堆里 ans+=job[i].P;//更新全局最優解 } } printf("%lld",ans); return 0; }
-
CF865D Buy Low Sell High(反悔自動機)
Description:
已知接下來 \(n\) 天的股票價格,每天可以買入當天的股票,賣出已有的股票,或者什么都不做,求 \(n\) 天之后最大的利潤。
Method:
我們可以快速想出一種貪心策略:買入價格最小的股票,在可以賺錢的當天賣出。
顯然我們可以發現,上面的貪心策略是錯誤的,因為我們買入的股票可以等到可以賺最多的當天在賣出。
我們考慮設計一種反悔策略,使所有的貪心情況都可以得到全局最優解。(即設計反悔自動機的反悔策略)
定義 \(C_{buy}\) 為全局最優解中買入當天的價格, \(C_{sell}\) 為全局最優解中賣出當天的價格,則:
\[C_{sell}-C_{buy}=\left(C_{sell}-C_i\right)+\left(C_i-C_{buy}\right) \]\(C_i\) 為任意一天的股票價格。
即我們買價格最小的股票去賣價格最大的股票,以期得到最大的利潤。我們先把當前的價格放入小根堆一次(這次是以上文的貪心策略貪心),判斷當前的價格是否比堆頂大,若是比其大,我們就將差值計入全局最優解,再將當前的價格放入小根堆(這次是反悔操作)。相當於我們把當前的股票價格若不是最優解,就沒有用,最后可以得到全局最優解。
上面的等式即被稱為反悔自動機的反悔策略,因為我們並沒有反復更新全局最優解,而是通過差值消去中間項的方法快速得到的全局最優解。
(假如還沒有理解這道題,可以看一看代碼,有詳細的注釋)
Code:
#include<bits/stdc++.h> #define int long long using namespace std; inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } priority_queue<int,vector<int>,greater<int> >qu;//開一個小根堆 int n; int ans=0;//全局最優解 signed main() { read(n); ans=0; for(int i=1,x;i<=n;i++) { read(x);//當前的股票價格 qu.push(x);//貪心策略:買價格最小的股票去買價格最大的股票 if(!qu.empty()&&qu.top()<x)//假如當前的股票價格不是最優解 { ans+=x-qu.top();//將差值計入全局最優解 qu.pop();//將已經統計的最小的股票價格丟出去 qu.push(x);//反悔策略:將當前的股票價格再放入堆中,即記錄中間變量(等式中間無用的Ci) } } printf("%lld\n",ans);//輸出全局最優解 return 0; }
-
BZOJ2151 種樹(反悔自動機)
Description:
有 \(n\) 個位置,每個位置有一個價值。有 \(m\) 個樹苗,將這些樹苗種在這些位置上,相鄰位置不能都種。求可以得到的最大值或無解信息。
Method:
先判斷無解的情況,我們顯然可以發現,若 \(n<\frac{2}{m}\) ,則是不能在合法的條件下種上 \(m\) 棵樹的,故按題意輸出
Error!
即可。假如有解的話,我們可以很輕松的推出貪心策略:在合法的情況下選擇最大的價值。
顯然上面的策略是錯誤的,我們選擇了最大價值的點,相鄰的兩個點就不能選,而選擇相鄰兩個點得到的價值可能更大。
考慮如何設計反悔策略。
我們同樣用差值來達到反悔的目的。假設有 \(A\) ,\(B\) ,\(C\) ,\(D\) 四個相鄰的點(如圖)。
\(A\) 點的價值為 \(a\) ,其他點同理。若:
\[a+c>b+d \]則:
\[a+c-b>d \]假如我們先選了 \(B\) 點,我們就不能選 \(A\) 和 \(C\) 兩點,這顯然是不對的,但我們可以新建一個節點 \(P\) , \(P\) 點的價值為 \(a+c-b\) ,再刪去 \(B\) 點。(如圖,紅色的是刪去的點,橙色的新建的點)
下一次選擇的點是 \(P\) 的話,說明我們反悔了(即相當於 \(B\) 點沒有選),可以保證最后的貪心最優解是全局最優解。
如何快速插入 \(P\) 點和找出是否選擇 \(P\) 點呢?我們可以使用雙向鏈表和小根堆,使得最終在 \(O(n\log n)\) 的時間復雜度下快速求出全局最優解。
Code:
#include<bits/stdc++.h> #define int long long #define Maxn 2000010 using namespace std; inline void read(int &x) { int f=1;x=0;char s=getchar(); while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=f; } int n,m; int w[Maxn],lft[Maxn],rgh[Maxn]; struct node { int val,id; bool operator <(const node &n) const { return val<n.val; } }; priority_queue<node>qu; int ind,ans=0; int vis[Maxn]; signed main() { read(n),read(m); ind=n; if(n/2<m) { puts("Error!"); return 0; } for(int i=1;i<=n;i++) { read(w[i]); node tmp; tmp.id=i; tmp.val=w[i]; qu.push(tmp); if(i==1) { lft[i]=n; rgh[i]=i+1; }else if(i==n) { lft[i]=i-1; rgh[i]=1; }else { lft[i]=i-1; rgh[i]=i+1; } } for(int i=1;i<=m;i++) { while(vis[qu.top().id]) qu.pop(); int id=qu.top().id; int val=qu.top().val; qu.pop(); ans+=val; ind++; vis[lft[id]]=vis[rgh[id]]=1; lft[rgh[rgh[id]]]=ind;rgh[lft[lft[id]]]=ind; lft[ind]=lft[lft[id]];rgh[ind]=rgh[rgh[id]]; w[ind]=w[lft[id]]+w[rgh[id]]-val; int newid=ind; int newval=w[ind]; node tmp; tmp.id=newid; tmp.val=newval; qu.push(tmp); } printf("%lld\n",ans); return 0; }
Warning:
- 一定要記錄這個點選沒有選過,假如已經選過了,就從堆中丟出去;
- 1與 \(n\) 是相鄰的,一定要特判一下;
- 雙向鏈表一定不要寫掛了;
- 一定要先將新建的點的價值存入一開始的價值數組,再丟進堆里;(卡在45卡了好久)
index
是關鍵字,一定不要使用。(我成功CE了一次)