可后悔贪心 -- 解题报告

反悔贪心_Elicsyd的博客-CSDN博客

感觉普通贪心是每一个维度都是平等的,没有优先级。而可后悔贪心是存在某个维度是不可变的,不能直接用排序或者堆进行维护,常常需要经过某种处理,通过挖掘出题目中关于不可变维度的特殊性质,使其可以用排序或者堆等数据结构进行贪心。

可后悔贪心常用堆(priority_queue)进行维护。

[E. Buy Low Sell High](Problem - E - Codeforces)

问题描述:

您可以完美预测某只股票未来 N 天的价格。您想利用这一知识获利,但每天只想交易一股股票。也就是说,每天你要么买入一股,要么卖出一股,要么什么也不做。起初你拥有的股票为零,当你没有股票时,你不能卖出股票。在N天结束时,你希望再次拥有零股,但希望拥有尽可能多的资金。 — 插件 cf better 翻译

思路:这一题跟股票买卖差不多,但是这个是每次都是可以买/卖/不进行操作,而且对于买卖股票没有次数限制。直接贪心:i天买入,j天卖出,要求j天对于i来说是最大的,但是没有限制次数,这样贪心需要维护区间内的,可能需要用dp。

这题为什么会后悔:价格 a b c,a < b对于贪心来说要卖出,但是卖出后,到c发现a < b < c,即|c - a| 大于 |b - a|,这时会发现我们的贪心就错误了,应该在c再卖出。发现b - a + c - b == c - a,利用差值可以将局部最优转为全局最优。

具体代码思路:用一个小顶堆维护之前最小的价值,当当前价值大于之前最小的价值时可以卖出,pop后再将当前价值push到小顶堆中。之后在将当前价值push到小顶堆中,表示以当前价值为买入点的股票低点。

1 4 10 20为例,答案为25,是1 --> 10 4 --> 20 或者 1 --> 20 4 --> 10这样。

代码:

void solve() {
    int n; cin>>n;
    vector<int> a(n);
    for(auto &t: a) cin>>t;
    priority_queue<int, vector<int>, greater<int>> q;
    LL ans = 0;
    for(auto t: a) {
        if(q.size() && q.top() < t) {
            ans += t - q.top();
            q.pop();
            q.push(t);
        }
        q.push(t);
    }
    cout<<ans<<endl;
}

[P8769 [蓝桥杯 2021 国 C] 巧克力]https://www.luogu.com.cn/problem/P8769)

问题描述:

小蓝很喜欢吃巧克力,他每天都要吃一块巧克力。

一天小蓝到超市想买一些巧克力。超市的货架上有很多种巧克力,每种巧克力有自己的价格、数量和剩余的保质期天数,小蓝只吃没过保质期的巧克力,请问小蓝最少花多少钱能买到让自己吃 x 天的巧克力。

思路:这题跟上一个类似,对abc按b进行排序,按b从大到小(从大到小如果后悔简单且一定是可以的)开始进行后悔贪心。与上题不同在于这题还有个数量c,上题没有。可以发现,满足条件只需要x个面包,且对于在i天而言,只要面包的保质期大于等于i,无脑选价钱最小的即可。因此可以按天数从结束天到开始天进行模拟,每次找没过保质期的最小价格的面包。

具体代码思路:用array<int,3>依次存入a b c,按b从大到小进行排序。用一个自定义小顶堆来维护对于遍历到i满足面包不过期的剩余面包的最小价格。每次找一个最小价格加入答案即可。

代码:

void solve() {
	int n,x; cin>>x>>n;
	vector<array<int,3>> va(n);
	for(auto &t:va) cin>>t[0]>>t[1]>>t[2]; // a b c
	sort(all(va), [](array<int,3> pre, array<int,3> suf) {
		return pre[1] > suf[1];
	});
	auto cmp = [](PII pre, PII suf) {
		return pre.vf > suf.vf;
	};
	priority_queue<PII, vector<PII>, decltype(cmp)> maq(cmp);
	LL ans = 0;
	int pos = 0;
	for(int i = x; i >= 1; --i) {
		while(pos < n && va[pos][1] >= i) {
			maq.push({va[pos][0], va[pos][2]});
			pos++;
		}
		if(maq.size() == 0) {
			cout<<-1;
			return ;
		}
		auto tmp = maq.top(); maq.pop();
		ans += tmp.vf; tmp.vs--;
		if(tmp.vs != 0) {
			maq.push({tmp});
		}
	}
	cout<<ans;
}

[P4053 [JSOI2007] 建筑抢修]([P4053 JSOI2007] 建筑抢修 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

问题描述:

小刚在玩 JSOI 提供的一个称之为“建筑抢修”的电脑游戏:经过了一场激烈的战斗,T 部落消灭了所有 Z 部落的入侵者。但是 T 部落的基地里已经有 �N 个建筑设施受到了严重的损伤,如果不尽快修复的话,这些建筑设施将会完全毁坏。现在的情况是:T 部落基地里只有一个修理工人,虽然他能瞬间到达任何一个建筑,但是修复每个建筑都需要一定的时间。同时,修理工人修理完一个建筑才能修理下一个建筑,不能同时修理多个建筑。如果某个建筑在一段时间之内没有完全修理完毕,这个建筑就报废了。你的任务是帮小刚合理的制订一个修理顺序,以抢修尽可能多的建筑。

思路:每一个建筑有一个最后截止时间和抢修耗时。对截止时间从小到大进行(原理同上)排序,用一个变量记录当前时间,如果当前时间加上维修时间在截止时间之内则可以,否则在前面可以的建筑中找一个最耗时的,如果那个最好时的大于当前耗时则进行替换,挤出点时间,为后面进行成功增加几率。

具体代码思路:用pair存入维护时间,截止时间。按截止时间进行升序排序。用一个变量lasttm记录当前进行到的时间,如果lasttm + vf <= vs表示这个建筑在截止前可以抢修成功则更新lasttm,并将维护时间vf放入到大顶堆中;如果不满足要求,表示这个建筑不可能被维修成功在截止时间之前,则在之前维修好的建筑中将一个维修时间较长的跟这个进行替代,lasttm更新,次数更新的lasttm一定比之前的未更新的lasttm要小。

代码:

void solve() {
    int n; cin>>n;
    vector<PII> vp(n);
    for(auto &t: vp) cin>>t.vf>>t.vs;
    sort(all(vp), [&](PII pre, PII suf) {
        return pre.vs < suf.vs;
    });
    int cnt = 0;
    int lasttm = 0;
    priority_queue<int> pq;
    for(int i = 0; i < n; ++i) {
        if(lasttm + vp[i].vf <= vp[i].vs) {
            cnt++;
            lasttm += vp[i].vf;
            pq.push(vp[i].vf);
        } else if(pq.top() > vp[i].vf) {
            lasttm -= pq.top(); pq.pop();
            lasttm += vp[i].vf; pq.push(vp[i].vf);
        }
    }
    cout<<cnt;
}

[tokitsukaze and Soldier](tokitsukaze and Soldier (nowcoder.com))

问题描述:

链接:https://ac.nowcoder.com/acm/problem/50439
来源:牛客网

在一个游戏中,tokitsukaze需要在n个士兵中选出一些士兵组成一个团去打副本。
第i个士兵的战力为v[i],团的战力是团内所有士兵的战力之和。
但是这些士兵有特殊的要求:如果选了第i个士兵,这个士兵希望团的人数不超过s[i]。(如果不选第i个士兵,就没有这个限制。)
tokitsukaze想知道,团的战力最大为多少。

思路:按特殊要求的不超过人数进行降序排,因为如果要求人数下降了,可以将之前进入的士兵无脑出队,而不用额外考虑其他信息。

具体代码思路:用pair依次存入战力和要求人数,按要求人数进行降序排序。用小顶堆维护满足要求人数限制条件的士兵的战力。对于第i个士兵,进入答案++,之后处理满足当前士兵要求的人数限制,如果有多余的则将战力最低的出队。每次遍历士兵时都取了max(因为可能中间要求得是最大战力之和,但是记录是遍历到的最后一个士兵时的最大战力之和。

代码:

void solve() {
    int n; cin>>n;
    vector<PII> vp(n);
    for(auto &t: vp) cin>>t.vf>>t.vs; // v s1
    sort(all(vp), [](PII pre, PII suf) {
        return pre.vs > suf.vs;
    });
    LL ans = 0;
    LL ma = -3;
    priority_queue<int,vector<int>,greater<int>> pq;
    for(auto t: vp) {
        ans += t.vf;
        pq.push(t.vf);
        while(pq.size() > t.vs) {
            ans -= pq.top(); pq.pop();
        }
        ma = max(ma, ans);
    }
    cout<<ma;
}

[E. Kolya and Movie Theatre](Problem - 1862E - Codeforces)

最近,科里亚发现他所在的城市即将新开一家电影院,在 n n n天内每天都会放映一部新电影。因此,在数字 1 ≤ i ≤ n 1 \le i \le n 1in 的那一天,电影院将首映 i i i 部电影。此外,科里亚还找出了电影的时间表,并为每部电影分配了娱乐价值,用 a i a_i ai 表示。

然而,科里亚不去电影院的时间越长,下一部电影的娱乐价值的下降幅度就越大。这种下降相当于 d ⋅ c n t d \cdot cnt dcnt,其中 d d d是一个预定值, c n t cnt cnt是距离上一次去电影院的天数。我们还知道,科里亚在新电影院开业的前一天成功地去了另一家电影院,这一天的数字是 0 0 0因此,如果我们在数字 i i i的那一天第一次去电影院,那么 c n t cnt cnt}- 距离上次去电影院的天数就等于 i i i

例如,如果有 d = 2 d = 2 d=2 a = [ 3 , 2 , 5 , 4 , 6 ] a = [3, 2, 5, 4, 6] a=[3,2,5,4,6],那么通过观看指数为 1 1 1 3 3 3的电影, 1 1 1这一天的 c n t cnt cnt值将等于 1 − 0 = 1 1 - 0 = 1 10=1 3 3 3这一天的 c n t cnt cnt值将等于 3 − 1 = 2 3 - 1 = 2 31=2,因此电影的总娱乐值将为 a 1 − d ⋅ 1 + a 3 − d ⋅ 2 = 3 − 2 ⋅ 1 + 5 − 2 ⋅ 2 = 2 a_1 - d \cdot 1 + a_3 - d \cdot 2 = 3 - 2 \cdot 1 + 5 - 2 \cdot 2 = 2 a1d1+a3d2=321+522=2

不幸的是,科里亚最多只能看**部 m m m电影。帮助他制定一个参观电影院的计划,使他参观的所有电影的总娱乐价值最大化。

思路:dpO(n*n)过不去,将a - (i - p) * d拆开发现是只跟p有关,但是发现不管咋样选,一定是1到某一个下标,减去的永远是 p * d,而这个p就是最后一个选的下标值。

同时发现选这个,可以那么再往后选可能有最好的,可以将前面的给替代出来,进而得到可后悔贪心算法。

void solve() {
    int n,m,d; cin>>n>>m>>d;
    priority_queue<int, vector<int>, greater<int>> q;
    int sum = 0;
    int ans = 0;
    for(int i = 1; i <= n; ++i) {
        int t; cin>>t;
        if(t < 0) continue;
        if(q.size() < m) q.push(t), sum += t;
        else {
            if(q.top() < t) {
                sum -= q.top(); q.pop();
                sum += t;
                q.push(t);
            }
        }
        // 可能存在在半路上是最大的值
        ans = max(ans, sum - d * i);
    }
    cout<<ans<<endl;
}

我第一次写的可后悔贪心是不对的,因为我用了一个pair来存在i下标的正贡献和(i - p)的值,这两个变量不能单单按照正贡献来算,可能会有正贡献不是最低的,但是总的是最低的可以更优的替代。错误代码如下:

void solve() {
    int n,m,d; cin>>n>>m>>d;
    priority_queue<PII, vector<PII>, greater<PII>> q;
    // priority_queue<int, vector<int>, greater<int>> q;
    int p = 0;
    int ans = 0;
    for(int i = 1; i <= n; ++i) {
        int t; cin>>t;
        if(t <= 0) continue;
        if(q.size() < m) {
            int tmp = t - (i - p) * d;
            if(tmp < 0) continue;
            ans += tmp;
            q.push({tmp, i - p});
            p = i;
        } else {
            int now = t - (i - p) * d;
            if(now > q.top().vf + q.top().vs * d) {
                ans += now - q.top().vs * d;
                int tmp = q.top().vs + (i - p);
                now -= q.top().vs * d;
                q.pop();
                q.push({now, tmp});
                p = i;
            }
        }
    }
    cout<<ans<<endl;
}

总结

可后悔贪心感觉用排序和堆完美的用上了题目中的性质,解决掉了普通贪心的不足。

发现是贪心 --> 常规贪心有点奇怪 --> 考虑可后悔贪心 / 换算法 / 再挣扎挣扎 --> 其他

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

golemon.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值