1. 程式人生 > 實用技巧 >反悔貪心 學習筆記

反悔貪心 學習筆記

反悔貪心

大家都知道貪心。我們在貪心的過程中,一定保證區域性最優解能夠得到全域性最優解。然而在某些題目中,我們無法從區域性最優得到全域性最優。這個時候我們應該怎麼辦呢?

考慮一個反悔的過程,我們需要構造一個方法去消掉之前的貢獻轉而加為正確的更優的貢獻。於是我們不需要管太多,可以直接用區域性最優解去擴充套件,因為反悔機制會幫我們修復成正確的答案,這就是反悔貪心。

至於這個過程,我們一般用堆去實現。

P3620 [APIO/CTSC 2007]資料備份

考慮這個問題。我們需要選擇多個點,使得這些點的點權之和最小。要求是選擇的點兩兩不相鄰。

根據 \(n \leq 10^6\) 的資料範圍,猜測我們需要一個 \(O(n \log n)\)

的做法去解決這個問題。考慮貪心,我們排個序,將最小的點加入我們選擇的點集中。如果下一個點和點集中的一個點相鄰了,我們就不將這個點加入我們的點集。

顯然這個貪心是錯誤的!考慮一組資料:

2 1 2 9

根據我們的演算法,我們會選擇 \(1\),然後 \(2,2\) 會被我們直接排除掉,選擇 \(9\)。答案為 \(10\),但是顯然 \(4\) 更優。怎麼辦呢?

考慮加入一個新點對應 \(i\),這個點 \(k_i\) 的權值為 \(a_{i-1}+a_{i-2}-a_i\)(表示 \(i\) 這個點左右兩個點的權值和減去這個點的權值)。假設我們現在選到一個點 \(1\),我們就把這個點刪除,然後再加入一個點 \(3\)

。顯然我們會跳過 \(2,2\)。這並不是我們想要的結果,但是我們接著會考慮下一個點 \(3\)。答案 \(4\)

我們驚奇的發現這樣就能得到正確結果,原因是我們發現在當前情況下用左右兩邊的點會比用中間的點更優秀,我們反悔選了另外兩邊兩個,最終得到了正確答案。

不難發現這樣是正確的。

於是我們用一個雙向連結串列儲存一下當前這個點是否被刪除,然後每次選擇一個節點,刪除它,然後再將 \(k_i\) 加入維護點權堆中,每次取最小值就能做了。這個過程只需要做 \(k\) 次,每次擴充套件選擇一個點即可。

再介紹兩個概念:

直接選擇,顧名思義。

反悔選擇,也就是把之前的直接選擇反悔掉,消除掉之前的貢獻並擴充套件成新的貢獻。

好像過程聽起來很簡單,實現起來確實有很多困難。。。所以我把程式碼貼一下吧。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct node{
	LL val,pos;
	node(){val=pos=0;}
	node(LL V,LL P){val=V,pos=P;}
	bool operator < (node another) const {return val>another.val;}
};
priority_queue<node> Q;
LL n,k,pre[100005],nxt[100005],h[100005],dis[100005],ans;
void del(LL x)
{
	LL l=pre[x],r=nxt[x];
	nxt[x]=nxt[r];
	pre[nxt[x]]=x;
	pre[x]=pre[l];
	nxt[pre[x]]=x;
}
int main(){
	scanf("%lld %lld",&n,&k);
	for(LL i=1;i<=n;++i)	scanf("%lld",&h[i]);
	for(LL i=1;i<=n;++i)
	{
		pre[i]=i-1;
//		if(pre[i]==0)	pre[i]=n;
		nxt[i]=i+1;
//		if(nxt[i]==n+1)	nxt[i]=1;
		dis[i]=h[i+1]-h[i];
	}
	nxt[n-1]=0;
	for(LL i=1;i<n;++i)	Q.push(node(dis[i],i));
	for(LL i=1;i<=k;++i)
	{
		node p=Q.top();
		Q.pop();
		if(p.val!=dis[p.pos])
		{
			--i;
			continue;
		}
		ans+=p.val;
		LL l=pre[p.pos],r=nxt[p.pos];
		del(p.pos);
		if(l && r)	dis[p.pos]=min(1008600100ll,dis[l]+dis[r]-dis[p.pos]);
		else	dis[p.pos]=1008600100;
		dis[l]=dis[r]=1008600100;
		Q.push(node(dis[p.pos],p.pos));
	}
	printf("%lld",ans);
	return 0;
}

CF436E Cardboard Box

也是一道有意思的反悔貪心。

我們想一下如何擴充套件選擇一顆星星。可能的情況有哪些呢?

  1. 我們在一顆星星都沒有選擇的關卡中選擇一顆星星(直接選擇);
  2. 我們在已經選擇了一顆星星的關卡中選擇兩顆星星(直接選擇);
  3. 有一個關卡選了一個,另外有一個關卡一個都沒有,於是我們把第一個選擇的關卡的星星不要了,然後在第二個選擇的關卡選擇獲得兩顆星星(反悔選擇);
  4. 有一個關卡選了兩個,另外有一個關卡一個都沒有於是我們把第一個選擇的關卡的星星變成選一個,然後在第二個選擇的關卡選擇獲得兩顆星星(返回操作)。

這樣一定是能覆蓋掉所有情況的!

於是考慮最後一個問題,我們應該維護什麼。我們先考慮上面四個選擇的貢獻分別是多少。

  1. 設選擇的關卡為 \(i\),貢獻為 \(a_i\)
  2. 設選擇的關卡為 \(i\),貢獻為 \(b_i-a_i\)
  3. 設第一次選擇和第二次選擇的關卡分別為 \(i,j\),貢獻為 \(b_j-a_i\)
  4. 設第一次選擇和第二次選擇的關卡分別為 \(i,j\),貢獻為 \(a_i-b_i+a_j\)

分析上面出現的內容,我們需要維護什麼?

為了方便,我們需要維護的東西儘量少。分析發現我們只需要維護 \(a_i,-a_i,a_i-b_i,b_i-a_i,b_i\) 就能覆蓋掉上面的所有情況。

然後好像寫起來還很麻煩而且這是 CF 資料,所以給個程式碼咯。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct node{
	LL val,pos;
	node(){val=pos=0;}
	node(LL P,LL V){val=V,pos=P;}
	bool operator < (node another) const {return val>another.val;}
};
priority_queue<node> Q[6];
/*
拓展一顆星的方案有什麼?
- 沒選過->選一個 代價ai
- 選一個->選兩個 代價bi-ai
- 選一個,另外一個不選->不選,另外一個選兩個 代價bj-ai
- 選兩個,另外一個不選->第一個選一個,另外一個選兩個 ai-bi+bj
所以用 5 個堆維護
Q1:ai ->0
Q2:-ai ->1
Q3:ai-bi ->2
Q4:bi-ai ->1
Q5:bi ->0
*/
LL n,w,a[300005],b[300005],opt[300005];
void checkQueue(LL num,LL op){while(!Q[num].empty() && opt[Q[num].top().pos]!=op)	Q[num].pop();}
int main(){
	cin>>n>>w;
	for(LL i=1;i<=n;++i)	cin>>a[i]>>b[i],Q[1].push(node(i,a[i])),Q[5].push(node(i,b[i]));
	LL ans=0;
	for(LL i=1;i<=w;++i)
	{
		LL delta=2147483647,op=0;
		node tmp1=node(),tmp2=node();
		checkQueue(1,0);
		checkQueue(2,1);
		checkQueue(3,2);
		checkQueue(4,1);
		checkQueue(5,0);
		if(!Q[1].empty() && Q[1].top().val<delta)
		{
			tmp1=Q[1].top();
			op=1;
			delta=Q[1].top().val;
		}
		if(!Q[4].empty() && Q[4].top().val<delta)
		{
			tmp1=Q[4].top();
			op=2;
			delta=Q[4].top().val;
		}
		if(!Q[2].empty() && !Q[5].empty() && Q[2].top().val+Q[5].top().val<delta)
		{
			tmp1=Q[2].top();
			tmp2=Q[5].top();
			op=3;
			delta=Q[2].top().val+Q[5].top().val;
		}
		checkQueue(5,0);
		if(!Q[3].empty() && !Q[5].empty() && Q[3].top().val+Q[5].top().val<delta)
		{
			tmp1=Q[3].top();
			tmp2=Q[5].top();
			op=4;
			delta=Q[3].top().val+Q[5].top().val;
		}
		ans+=delta;
		if(op==1)
		{
			opt[tmp1.pos]=1;
			Q[2].push(node(tmp1.pos,-a[tmp1.pos]));
			Q[4].push(node(tmp1.pos,b[tmp1.pos]-a[tmp1.pos]));
		}
		if(op==2)
		{
			opt[tmp1.pos]=2;
			Q[3].push(node(tmp1.pos,a[tmp1.pos]-b[tmp1.pos]));
		}
		if(op==3)
		{
			opt[tmp1.pos]=0,opt[tmp2.pos]=2;
			Q[1].push(node(tmp1.pos,a[tmp1.pos]));
			Q[3].push(node(tmp2.pos,a[tmp2.pos]-b[tmp2.pos]));
			Q[5].push(node(tmp1.pos,b[tmp1.pos]));
		}
		if(op==4)
		{
			opt[tmp1.pos]=1,opt[tmp2.pos]=2;
			Q[2].push(node(tmp1.pos,-a[tmp1.pos]));
			Q[3].push(node(tmp2.pos,a[tmp2.pos]-b[tmp2.pos]));
			Q[4].push(node(tmp1.pos,b[tmp1.pos]-a[tmp1.pos]));
		}
	}
	cout<<ans<<endl;
	for(LL i=1;i<=n;++i)	cout<<opt[i];
	return 0;
}