反悔貪心
本蒟蒻在做題時被卡,看題解發現用反悔貪心,遂蒐羅資料,得有此篇
part.1
什麼是反悔貪心?
簡單的例子,我有一個只能裝3個物品的揹包,我要從n個價值由小到大的物品中選出3個最大的裝進包裡,但只能從頭往後選,假如我此刻的包內物品價值為1 2 3,而我要面對的下一個物品的價值為4,那麼我就要丟掉我包裡價值最小的物品,也就是反悔,然後將這個價值更大的4放入揹包內,這便是反悔貪心
也就是說,在區域性範圍內做出的最優決策對於全域性來說並不適用,於是我們就需要反悔的操作,儘可能達成最優解
part.2
堆
此處我們需要用到堆的資料結構,我們可以手寫一個堆出來,也可以使用stl中的優先佇列
在此處提一下優先佇列
優先佇列(priority_queue) 是一種stl容器,他與正常的佇列不同的是,我們可以自定義佇列內元素的優先順序
優先佇列只維護隊頭的元素
priority_queue<data type> pq;//建堆操作
//第一種寫法
//預設為大根堆
struct cmp{
bool operator ()(const int & u, const int & v) const
{
return u > v;
}
};
//若要使用小根堆將大於號換成小於即可
//當使用自己定義的結構體時過載小於號
struct Node{
int x, y;
bool operator < (const Node &u)const
{
return x == u.x ? y < u : u < x;
}
};
//另外一種寫法,比第一種方便很多
struct node {
int x, y;
bool operator < (const node &v) const
{
if(y > v.y)return(true);
else return(false);
}//過載<號的定義,規定堆為關於y的小根堆
};
part.3
反悔貪心的寫法
一般情況下有兩種:反悔堆與反悔自動機
兩種模型:用時一定模型,價值一定模型
我們先來介紹一下反悔堆
1.反悔堆
用時一定模型
P2949 [USACO09OPEN] Work Scheduling G
簡要題意,n項工作,每個工作都有一個截止日期已經完成後的回報,完成這個工作要花費一個單位的時間
簡單的貪心思想在這並不成立,可以舉的例子很多,此處就不提了
我們只考慮最優的解法 :這道題的一個優質解法就是使用反悔堆
因為題目中說,我們可以在任意時間完成一份任意的工作(前提是工作沒有逾期
那麼我們在同一時間內,既可以選擇完成a也可以選擇完成b工作
題目的思路大致如下:先排序,考慮維護一個小根堆
如果說我們能透過捨棄價值低的工作來完成報酬更高的工作
換句話說,就是堆頂的元素要比接下來要面對的元素價值小,就捨棄這個堆頂,把新的工作放進來
堆內所有元素之和就是答案
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
struct node {
int deadline, value;
bool operator < (const node &v) const
{
if(value > v.value)return(true);
else return(false);
}
}a[110000];
priority_queue<node> pq;
bool cmp(node x, node y){
if(x.deadline < y.deadline) return(true);
else return(false);
}
ll ans;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;cin >> n;
for(int i = 1;i <= n;i ++) cin >> a[i].deadline >> a[i].value;
sort(a + 1, a + n + 1, cmp);
for(int i = 1;i <= n;i ++) {
if(pq.size() < a[i].deadline){
ans += a[i].value;
pq.push(a[i]);
}//截止日期沒到,就還能做任務
else {
if(a[i].value > pq.top().value){//如果接下來的工作價值更大,就反悔
ans -= pq.top().value;pq.pop();
ans += a[i].value;
pq.push(a[i]);
}
}
}
cout << ans;
return 0;
}
價值一定模型
P4053 [JSOI2007] 建築搶修
簡要題意:n項工作,每個工作有一個完成花費的時間以及截止時間,工人可以選擇做任意一個任務,但只有修理完才能去做下一個任務,問最多能修多少個建築
與上一道題不同,這次每份工作所花費的時間不同,但是價值是相同的
那麼類似的思路,這次我們考慮使用一個大根堆來維護堆頂,堆頂的元素就是堆內目前耗時最長的工作
如果有比它耗時短的,就pop掉,然後將新的push進來
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
struct node {
int deadline, Time;
bool operator < (const node &v) const
{
if(Time < v.Time)return(true);//大根堆用小於號
else return(false);
}
}a[160000];
priority_queue<node> pq;
bool cmp(node x, node y){
if(x.deadline < y.deadline) return(true);
else return(false);
}//截止日期從小到大排序
ll last;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;cin >> n;
for(int i = 1;i <= n;i ++) cin >> a[i].Time >> a[i].deadline;
sort(a + 1, a + n + 1, cmp);
for(int i = 1;i <= n;i ++) {
if(a[i].deadline >= last + a[i].Time){
last += a[i].Time;
pq.push(a[i]);
}//如果說,這次工作完成的耗時與之前相加沒超時,那麼就push進來
else {
if(a[i].Time < pq.top().Time){//如果說接下來完成時間更小,就push入堆中
last -= pq.top().Time;pq.pop();//先pop在push
last += a[i].Time;
pq.push(a[i]);
}
}
}
cout << pq.size();//最後輸出堆內元素的數量即可
return 0;
}
2.反悔自動機
問題:你現在有A, B, C , D四個數,每次只能從中AB中選擇一個CD中選擇一個,問怎麼選價值最大
思路:我先選A 和 C,然後將B, D的值分別更新為B - A和D - C
那麼我們接下來繼續選,如果說選擇了B,那麼實際上選擇的是B - A,再加上之前的A,就相當於我們只選擇了B
也就是相當於反悔了
反悔自動機的基本思想就是,透過修改關聯點的值讓我們避免去選擇之前決定過的點
堆反悔自動機
https://codeforces.com/contest/865/problem/D
買股票問題:已知後面n天每天的股票價格,在每一天裡,你都可以買入一份股票並賣出一份股票,問n天之後你的最大收益為多少(n天之後你持有的股票數量應當為0)
設計反悔自動機的反悔策略,使所有的貪心情況都可以得到最優解
定義在Pbuy為全域性最優解中買入的當天的價格,Psell為全域性最優解中賣出的當天的價格
那麼公式如下
那麼就相當於,我們在pi天把Pbuy時的股票賣掉,再以pi的價格買回來,再在Psell時賣掉,也就是說我們買價格最小的股票再以最大的價格賣掉,以達成最優解
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
struct node {
int value;
bool operator < (const node &v) const
{
if(value > v.value)return(true);
else return(false);
}
}a[330000];
priority_queue<node>q;
ll ans;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].value;
for(int i = 1; i <= n; i++) {
q.push(a[i]);//用於貪心買價格最小的股票去買價格最大的股票
if(!q.empty() && q.top().value < a[i].value) {//假如當前的股票價格不是最優解
ans += a[i].value - q.top().value;//將差值計入全域性最優解
q.pop();//將已經統計的最小的股票價格丟出去
q.push(a[i]);//反悔策略:將當前的股票價格再放入堆中,即記錄中間變數(等式中間的Vi)
}
}
cout << ans;
return 0;
}
雙向連結串列反悔自動機
這道題的模板題是種樹
P1484 種樹(真的是種樹)
簡要題意:一條直線上有n個坑,你有k個樹種用來種樹,沒種一顆樹你都會獲得對應的利益,但相鄰的兩個坑不能種樹
考慮反悔自動機,那麼要構建差值公式
先舉個簡單的例子:假如我們有4個坑A, B, C, D,對應的利益為a, b, c, d
假如我們選擇了B點,我們能獲得的最大利益為b + d(種子一定夠用)
但能肯定此時為最優解嗎?
不一定,可能選擇A和C是最優的
觀察發現
我們新加入一個P點,他對應的價值為a + c - b,並刪點A和C點
如果我們下一次選擇了P,就證明我們後悔了
假如說我們要操作的點在兩端呢?
那麼一定不會進行反悔操作,直接刪掉即可
如果我決定刪除A,就代表A的值是大於B的值的,那我們就沒有可能反悔了。這是因為假如我們反悔了選了B不選A,那我們得到的值b小於a,並且影響了C讓C不能選了(選A時C是可以選的),可以說是賠了夫人又折兵,所以是不可能反悔的直接把A和B都刪了就可以了。
——————摘自蒟蒻大佬
感覺自己說的不太明白,就引用了一下,感謝jvruo大佬!
#include<bits/stdc++.h>
using namespace std;
struct node {
long long site, val;
bool operator < (const node &v) const {
if(v.val > val) return(true);
else return(false);
}
}now, x;
long long val[2200000];//價值
long long vis[2200000], l[2200000], r[2200000];
//vis用於記錄是否刪除(1為刪除),l,r為雙向連結串列的左右鄰居
long long t, n, m;
long long ans;
priority_queue<node>q;
int main() {
scanf("%lld%lld", &n, &m);
for(long long i = 1; i <= n; i++) scanf("%lld", &val[i]);
while(!q.empty()) q.pop();
for(long long i = 1; i <= n; i++) {
now.site = i;
now.val = val[i];
vis[i] = 0;
q.push(now);
}
for(long long i = 2; i <= n; i++) l[i] = i - 1;
for(long long i = 1; i <= n - 1; i++) r[i] = i + 1;
l[1] = r[n] = 0;
//預處理,製作堆和雙向連結串列
for(long long i = 1; i <= m; i++) {
x = q.top(); q.pop();
while(vis[x.site] == 1) {
x = q.top();
q.pop();
}//找到一個沒有被刪除的值最大的點
if(x.val < 0) break;
ans += x.val;
if(l[x.site] != 0) vis[l[x.site]] = 1;//刪除左邊的點
if(r[x.site] != 0) vis[r[x.site]] = 1;//刪除右邊的點
if(l[x.site] != 0 && r[x.site] != 0) {
now.site = x.site;
now.val = val[l[x.site]] + val[r[x.site]] - val[x.site];
val[x.site] = val[l[x.site]] + val[r[x.site]] - val[x.site];
r[l[l[x.site]]] = x.site;
l[x.site] = l[l[x.site]];
l[r[r[x.site]]] = x.site;
r[x.site] = r[r[x.site]];
q.push(now);
}//如果不在連結串列兩端
else if(l[x.site]) r[l[l[x.site]]] = 0;
else l[r[r[x.site]]] = 0;
//如果在連結串列兩端
}
printf("%lld\n", ans);
return 0;
}
part.4
總結
在網上看了不少資料,感覺還是jvruo大佬講的比較詳細,他的部落格對我的幫助很大,此處對jvruo大佬表示真摯的感謝
感覺對應反悔貪心的知識點仍需要時間去理解