反悔贪心-CF865D Buy Low Sell High

题目链接

题目大意

已知n天内的股票价格,每一天可以选择买进、卖出或什么也不做(只能进行三个操作中的一个,且只能做一次)。要求n天后,手中没有股票,问可以获得的最大利益。数据范围:n\leq 3\times 10^5

贪心?

很容易想到一个贪心策略:对于第i天,如果我想卖股票,那么我一定卖掉第i天之前,股票最便宜的那一个,这样可以一买一卖获得最大的利益。

但是这样很不好实现:

第一,对于第i天,如果我找到第j(j< i)天是股票最便宜的那一天,我们无法保证第j天我们买了股票,我们也无法保证,第i天我卖完之后,后面的天数里,自己不是最小的(更小的已经被自己卖掉了,所以这一天可能会成为最小的)。

第二,由于我们在选择第i天的策略的时候,想要保证最优,一定要知道n天所有的价格再做以分析。这道题目的数据范围是3e5,大于O(n\log n)的算法都无法保证时间。所以想要在第i天执行全局最优策略,是很难的。

但是,这种贪心策略完全可行,或许我们只需要在常规的贪心方法上加一点改进……

反悔贪心

一般的贪心本身不带反悔的操作,因为贪心本身就是每一步都是局部最优解,然后用局部最优解一步一步推到最后,然后变成全局的最优解。

但是,往往事情并没有想象中的那样顺利。或许我们想不出一种贪心方式,使得每一步都能是局部的最优解。但是,如果我们能意识到,之前的有一步操作不是最优的,同时我们可以反悔,让那一步撤回,同时在这一步得到那一步的更优解,一步一步推到最后,变成全局最优解。这样的增加了反悔机制的贪心方式,就是反悔贪心。

怎么反悔?

对于这道题目,我们的收益来自于“买第i天的股票,卖第j天的股票”。

运用起我们之前所提到的贪心方式:对于第j天,如果我们要卖股票,那么我们一定要找这一天的时间节点之前,没有购买股票的股票价格最低的一天(当然还要挣到钱,所以这一天的股票价格必须要低于第j天的股票价格),假设为第i天。这样的情况下,对于第j天而言,如果第j天买了股票,买第i天的股票,形成一买一卖,必定是收益最高。

所以我们在这里形成了一个思维上的转换。在题目当中,我们应当是买了之后才能卖。但是这样贪心起来很麻烦。所以我们把因果关系颠倒一下,变成:因为第j天卖股票的话,第i天买股票可以获得最大利润,所以第i天要买股票。

又有一个问题出现了:如果当前我选择的第i天,正巧是我之前卖股票的一天,怎么办?

假设“买第k天的股票,卖第i天的股票”这件事之前已经作为那个时候认为的最优决策执行了,现在又要执行“买第i天的股票,卖第j天的股票”。这两个操作是否冲突?

是不冲突的。因为将这两个结合起来来看的话,我们不妨考虑一个操作“买第k天的股票,卖第j天的股票”,这个操作的收益是不是和那两个结合起来之后的收益是一样的?(卖了又买,其实就等于没卖也没买)

所以,当我们遇到这种情况的时候,无需考虑第i天是否是卖股票的日子。

其实到这里,反悔机制就形成完了。

这道题目里面的反悔,体现在:

“买第k天的股票,卖第i天的股票”和“买第i天的股票,卖第j天的股票”都发生的时候,我们可以认为,第k天、第i天、第j天的股票价格是单调递增的。所以“买第k天的股票,卖第j天的股票”的操作,可以理解成是我们反悔了“买第k天的股票,卖第i天的股票”这步操作之后做的一个新的更优的决策。

这就是反悔贪心。看懂之后,感觉这玩意真的很显然,但做题的时候,确实想不到。

实现方法

首先是最初的贪心的实现方法。

我们每次都要找当前时间之前的最小值,如果每次都O(n)遍历会超时。而本身又无序,没办法用二分,所以我们采用优先队列来维护当前时间点可以选择的价格的可重集合。但是,优先队列本身是大根堆,我们采用存储负数的形式,这样绝对值最小的数就在优先队列的顶端。每次只需要取优先队列的堆顶即可。

大体流程:

先将第一天的价格入堆。(第一天肯定不能卖)

从第二天开始,看当前堆顶的绝对值是否小于这一天的价格:如果小于,那么就形成一次一买一卖,并构成收益,将这个收益加入答案当中,并将这一天的价格入堆;否则,将这一天的价格入堆。

最后答案就是题目所求。

反悔操作在哪里?在大体流程中加粗的位置。这一步的目的是让后面的操作看到之前的操作,如果我们可以反悔,就通过这里入堆的价格,计算盈利(这里面计算的收益相当于是原有的操作之后新增的操作所带来的收益)。这样做的正确性见上述。

AC代码

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;

inline LL read() {
	LL x = 0, y = 1; char c = getchar(); 
	while (c > '9' || c < '0') { if (c == '-') y = -1; c = getchar(); }
	while (c>='0'&&c<='9') { x=x*10+c-'0';c=getchar(); } return x*y;
}

priority_queue<LL> q;
LL n, ans = 0, x;

void main2()
{
	n = read();
	x = read();
	q.push(-x);
	for (LL i = 2; i <= n; ++i)
	{
		LL tp = q.top();
		x = read();
		if (x + tp > 0)
		{
			ans += (x + tp);
			q.pop();
			q.push(-x);
		}
		q.push(-x);
	}
	printf("%lld", ans);
}
LL T = 1;
int main()
{
	for (LL t = 1; t <= T; ++t)
	{
		main2();
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值