「学习笔记」反悔贪心

一.什么是反悔贪心

顾名思义,反悔贪心就是两个操作:“反悔” + “贪心”。

一般来说,贪心仅能解出局部最优解。

那么在要求全局最优解时,我们就可以利用“反悔”这个操作解决。

反悔贪心的思想是:每次都进行操作,在以后有最优情况的时候再取消这次操作。

二.基本反悔贪心

一般来说,我们采用一个堆来存选的元素,堆顶是最劣解

分两种情况:

\(1.\) 符合某种条件时,推入堆,计算答案。

\(2.\) 不符合某种条件时,与堆顶比较,做出操作。

接下来用一些例题来帮助大家理解这个算法。

三.反悔贪心例题

CF865D Buy Low Sell High

可以算是反悔贪心的模板了。

对于当前操作,我们要让收益最大,那么就要在最低点买入,最高点卖出。

我们可以用一个堆来记录今天之前的最小值,然后操作,就等于今天卖出最小值那天买入。

显然,这不是最优解法,因为我们没有考虑到后面的更优解法。

那么我们采用“反悔”操作,在卖出时,将当前价格推入堆,当做买入,那么这样计算的答案就是最优解。

也就是说,当前卖出价格 \(x\),更优卖出价格 \(y\),买入价格 \(z\),最优解 \(y - z\) 等于 \((x-z)+(y-x)\)

代码就很好写了:

#include <bits/stdc++.h>

using namespace std;

#define int long long

struct Node {
	int x;
	
	Node (int x = 0) : x(x) {
		
	}
	
	bool operator < (const Node &xx) const {
		return xx.x < x;
	} 
};

priority_queue<Node> q;//小根堆。

int n, ans = 0ll; 

signed main () {
	cin >> n;
	
	for (int i = 1, x; i <= n; i ++) {
		cin >> x;
		
		q.push (x);//每一个都当做买入。 
		
		if (!q.empty() && q.top().x < x) {
			//当前卖出价格大于最低价格时。 
			
			ans += x - q.top().x;
			//当前价格和堆顶相减,累计答案。 
			
			q.pop ();
			//弹出最低价格。 
			
			q.push (x);
			//推入当前价格,反悔操作。 
		}
	}
	
	cout << ans;
	
	return 0;
}

P2209 [USACO13OPEN]Fuel Economy S

正好打模拟赛碰到,一起写了。

显然,我们有一个反悔贪心的思路。

首先要判断无解情况:
\(1.\) \(B\) 个单位的油无法到达第一个加油站。

\(2.\)\(i\) 个加油站与第 \(i+1\) 个加油站的距离大于 \(G\) \((1\le i<N)\)

\(3.\) 在第 \(N\) 个加油站即使满油也无法到达 \(D\)

然后可以进行反悔贪心。

首先,为了从第 \(i\) 个加油站到第 \(i+1\) 个加油站(或终点),我们都需要加满油。(贪心操作)

\(i\) 个加油站到第 \(i+1\) 个加油站之间多余的油,可以退掉。(反悔操作)

而应该如何使用油呢?显然,我们优先使用单位价格低的油,这样就能使总花费最小。

#include <bits/stdc++.h>

using namespace std;

const int N = 55555;

#define int long long

int n, g, b, d;

struct Node {
	int x;
	int y;
	
	Node (int x = 0, int y = 0) : x (x), y (y){
		
	}
	
	bool operator < (const Node & xx) const {
		return x < xx.x;
	} 
}a[N];

int l = 0, r = -1;

struct Oil {
	int v;
	int t;
	
	Oil (int v = 0, int t = 0) : v (v), t (t) {
		
	}
	
	bool operator < (const Oil & xx) const {
		return v < xx.v;
	} 
	
	//单调性。	
}q[N];

int calc (int len) {//走距离为len的花费 
	int res = 0;
	
	while (len > 0) {
		if (q[l].t > len) {//不需要用完最便宜的油就可以走到。 
			res += len * q[l].v;
			
			q[l].t -= len;
			
			len = 0;
		}
		
		else {//需要用光最便宜的油。 
			res += q[l].t * q[l].v; 
			
			len -= q[l].t;
			
			l ++;
		}
	}
	
	return res;
}

signed main () {
	cin >> n >> g >> b >> d;
	
	for (int i = 1; i <= n; i ++) {
		cin >> a[i].x >> a[i].y;
	}
	
	a[++ n] = Node (d, 0);//把终点也当做加油站。 
	
	sort (a + 1, a + 1 + n);
	
	if (a[1].x >= b) {
		puts ("-1");
		
		return 0;
	}
	
	for (int i = 2; i <= n; i ++) {
		if (a[i].x - a[i - 1].x > g) {
			puts ("-1");
			
			return 0;
		}
	}
	
	//判无解。 
	
	q[++ r] = Oil (0, b);//加入初始油。 
	
	int ans = 0;
	
	a[0].x = 0;
	
	for (int i = 1; i <= n; i ++) {
		ans += calc (a[i].x - a[i - 1].x);//从第i-1个加油站到第i个加油站。 
		b -= a[i].x - a[i - 1].x;
		
		if (i == n) {
			cout << ans;
			
			return 0;
		}
		
		while (l <= r && q[r].v > a[i].y) {//反悔操作。
			//从r开始退油,保证退的油较贵。
			 
			b -= q[r].t;
			
			r --;
		}
		
		q[++ r] = Oil (a[i].y, g - b);//加满油。 
		
		b = g;
	}
	
	return 0;
} 

P2949 [USACO09OPEN]Work Scheduling G

反悔的操作比较明显。

如果可以进行操作,那么就操作。

如果当前任务已经截止,就反悔操作,与最劣解比较。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

#define int ll

const int N = 1e5 + 7;

struct Node {
	int d;
	int p;
}a[N];

inline bool cmp (Node x, Node y) {
    return x.d < y.d;
}

int n;

priority_queue<int, vector<int>, greater<int> > q;//小根堆。

signed main () {
    cin >> n;

    for (int i = 1; i <= n; i ++) {
    	cin >> a[i].d >> a[i].p;
	}
	
	sort (a + 1, a + 1 + n, cmp);//按截止时间排序。
	
	int ans = 0;
	
	for (int i = 1; i <= n; i ++) {
	    if (a[i].d <= q.size ()) {
		//每项任务花费时间都是1,那么队列长度就是当前时间。
		//于是这行就表示当前任务已经截止的情况。
	        if (q.top() < a[i].p) {
	            ans += a[i].p - q.top();//反悔操作。
	            
	            q.pop ();
	            
	            q.push (a[i].p);
	        }
	    }
	    
	    else {
	        ans += a[i].p;
	        
	        q.push (a[i].p);
			
			//贪心操作,每项都做。
	    }
	}
	
	cout << ans;

	return 0;
}


P4053 [JSOI2007]建筑抢修

和上一题很像。

我们开一个大根堆,存入工程需要的时间。

堆顶放的是最劣解,也就是说工程需要的时间越久越不好,所以开大根堆。

分两种情况:

\(1.\) 当前任务无法完成时:反悔操作,与堆顶比较,取用时短的较优解。

\(2.\) 当前任务可以完成时:贪心操作,完成该操作,推入堆。

代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

template <typename T>

inline T read() {
	T x = 0, f = 1;

	char ch = getchar();

	while (!isdigit(ch)) {
		if(ch=='-') {
	    	f = -1;

			ch = getchar();
  		}
	}

	while (isdigit (ch)) {
		  x = x * 10 + (ch ^ 48);

		  ch = getchar();
	}

	return x * f;
}

const int N = 155555;

struct Node {
	int t1;
	int t2;
	
	inline bool operator < (const Node &x) const {
		return t2 < x.t2;
	}
}a[N];

int n;

int main () {
    int n = read<int>(), mx = -1;

    for (int i = 1; i <= n; i ++) {
    	a[i].t1 = read<int>();
    	a[i].t2 = read<int>();
    }
    
    sort (a + 1, a + 1 + n);//按截止时间排序。
    
    priority_queue<int> q;
    
    int now = 0, ans = 0;
	
	//now表示当前时间。
    
    for (int i = 1; i <= n; i ++) {
    	if (now + a[i].t1 > a[i].t2) {//反悔操作。
    		if (a[i].t1 < q.top ()) {
    			now -= q.top () - a[i].t1;//时间减少。
    			
    			q.pop ();
    			
    			q.push (a[i].t1);
    		}
    	}
    	
    	else {//贪心操作。
    		q.push (a[i].t1);
    		
    		now += a[i].t1;
    		
    		ans ++;//任务可以完成。
    	}
    }
    
    cout << ans << endl;

	return 0;
}
posted @ 2021-12-05 15:39  cyhyyds  阅读(602)  评论(0编辑  收藏  举报