《算法竞赛·快冲300题》每日一题:“奶牛优惠券”

算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。


奶牛优惠券” ,链接: http://oj.ecustacm.cn/problem.php?id=1895

题目描述

【题目描述】 农夫约翰需要新奶牛!
目前有 N 头奶牛出售,农夫预算只有M元,奶牛 i 花费 P_i 。
但是农夫有 K 张优惠券,当对奶牛 i 使用优惠券时,只需要花费 C_i(C_i <= P_i)。
每头奶牛只能使用一张优惠券。
求农夫最多可以养多少头牛?
【输入格式】 第一行三个正整数 N,K,M,1 <= N <= 50,000,1 <= M <= 10^14,1 <= K <= N。
接下来 N 行,每行两个整数 P_i 和 C_i ,1 <= P_i <= 10^9,1 <= C_i <= P_i。
【输出格式】 输出一个整数表示答案。
【输入样例】

4 1 7
3 2
2 2
8 1
4 3

【输出样例】

3

题解

   题意简述如下:有n个数,每个数可以替换为较小的数;从n个数中选出一些,选的时候允许替换最多k个,要求这些数相加不大于m,问最多能选出多少个数。
   这个问题可以用女生买衣服类比。女生带着m元去买衣服,目标是尽量多买几件,越多越好。每件衣服都有优惠,但是必须使用优惠券,一件衣服只能用一张。衣服的优惠幅度不一样,有可能原价贵的优惠后反而更便宜。女生有k张优惠券,问她最多能买多少件衣服。
   男读者可以问问女朋友,她会怎么买衣服。聪明的她可能马上问:优惠券是不是无限多?如果优惠券用不完,那么衣服的原价形同虚设,按优惠价从小到大买就行。可惜,优惠券总是不够用。
   她想出了这个方法:按优惠价排序,先买优惠价便宜的,直到用完优惠券;如果还有钱,再买原价便宜的。
   但是这个方法不是最优的,因为优惠价格低的可能优惠幅度小,导致优惠劵被浪费了。例如:
      衣 服:a, b, c, d, e
      优惠价:3, 4, 5, 6, 7
      原 价:4, 5, 6, 15,10
   设有m = 20元,k = 3张优惠券。把3张优惠券用在a、b、c上并不是最优的,这样只能买3件。最优解是买4件:a、b、d用优惠价,c用原价,共19元。
   下面对这个方法进行改进。既然有优惠幅度很大的衣服,就试试把优惠券转移到这件衣服上,看能不能获得更大的优惠。把这次转移称为“反悔”。
   设优惠价最便宜的前k件用完了k张优惠券。现在看第i = k+1件衣服,要么用原价买,要么转移一张优惠券过来用优惠价买,看哪种结果更好。设原价是p,优惠价是c。
   反悔之前,第i件用原价pi买,前面第j件用优惠价cj买,共花费:
      tot + pi + cj,其中tot是其它已经买的衣服的花费。
   反悔后,第j件把优惠券转给第i件,改成原价pj,第i件用优惠价ci,共花费:
      tot + ci + pj
   如果反悔更好,则有:
      tot + pi + cj < tot + cj + pi,
   即pj - cj< pi - ci,设Δ= p - c, 有Δj <Δi, Δ是原价和优惠价的差额。
   也就是说,只要在使用优惠券的衣服中,存在一个j,有Δj <Δi,也就是说第j件的优惠幅度不如第i件的优惠幅度,那么把j的优惠券转给i会有更好的结果。
   但是上述讨论还是有问题,它可能导致超过总花费。例如:
      衣 服:a, b, c
      优惠价:20, 40, 42
      原 价:30, 80, 49
   m = 69元,k = 1张优惠券。先用优惠券买a;下一步发现Δa <Δb,把优惠券转移给b,现在的花费是30 + 40 = 70,超过m了。而最优解是a仍然用优惠价,c用原价。所以简单地计算候选衣服i的差额Δi= pi - ci然后与Δj比较并不行。
   那么如何在候选衣服中选一件才能最优惠呢?
   (1)用原价买,那么应该是这些衣服中的最低原价;
   (2)用优惠价买,那么应该是这些衣服中的最低优惠价。
   所以在候选衣服中的最低原价和最低优惠价之间计算差额,并与Δj比较才是有意义的。
   编码时,用3个优先队列处理3个关键数据:
   (1)已使用优惠券的衣服的优惠幅度Δ。已经使用优惠券的衣服中,谁应该拿出来转移优惠券?应该是那个优惠幅度Δ最小的,这样转移之后才能获得更大优惠。用优先队列d找最小的Δ。
   (2)没使用优惠券的衣服的原价。用一个优先队列p找最便宜的原价。
   (3)没使用优惠券的衣服的优惠价。用一个优先队列c找最便宜的优惠价。
   代码中这样处理优惠券:
   (1)先用完k个优惠券,从c中连续取出k个最便宜的即可。
   (2)优惠券替换。从p中取出原价最便宜的p1,从c中取出优惠价最便宜的c2,然后从d中取出优惠幅度最小的d:
   1)若d > p1-c2,说明替换优惠券不值得,不用替换。下一件衣服用原价买p1。
   2)若d ≤ p1-c2,说明替换优惠券值得,下一件衣服用优惠价买c2,原来用优惠券的改成原价。
   本题总体上是贪心,用三个优先队列处理贪心。但是优惠券的替换操作是贪心的“反悔”,所以称为“反悔贪心”。贪心是连续做局部最优操作,但是有时局部最优推不出全局最优时,可以用反悔贪心,撤销之前做出的决策,换条路重新贪心。。
【重点】 贪心 。

C++代码

//代码参考 https://www.luogu.com.cn/blog/Leo2007-05-24/solution-p3045
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=50010;
int p[N],c[N];
bool buy[N];                         //buy[i]=1: 第i个物品被买了
int ans=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >P,C;
priority_queue<int,vector<int>,greater<int> >D;
signed main(){
	int n,k,m; cin>>n>>k>>m;
	for(int i=1;i<=n;i++){
		cin>>p[i]>>c[i];
		P.push(make_pair(p[i],i));    //原价,  还没买的在这里,如果买了就pop出去
		C.push(make_pair(c[i],i));    //优惠价,还没买的在这里,如果买了就pop出去
	}
	for(int i=1;i<=k;i++)   D.push(0);   //k张优惠券,开始时每个优惠为0
	while(!P.empty() && !C.empty()){
		pair<int,int> p1 = P.top();     //取出原价最便宜的
		pair<int,int> c2 = C.top();     //取出优惠价最便宜的
		if(buy[p1.second]){ P.pop(); continue;}       //这个已经买了,跳过
		if(buy[c2.second]){	C.pop(); continue;}    //这个已经买了,跳过
		if(D.top() > p1.first-c2.first){              //用原价买i更划算,不用替换优惠券
			m -= p1.first;                           //买原价最便宜的
			P.pop();                    //这里不要C.pop(),因为买的是p1,不是c2
			buy[p1.second] = true;      //标记p1买了
		}
		else{                             //替换优惠券。头k个都先执行这里
			m -= c2.first+D.top();       //买优惠价最便宜的
			C.pop();                     //这里不要p.pop(),因为买的是c2,不是p1
			buy[c2.second]=true;         //标记c2买了
			D.pop();                     //原来用优惠券的退回优惠券
			D.push(p[c2.second]-c[c2.second]);   //c2使用优惠券,重新计算delta并进队列
		}
		if(m >= 0) ans++;
		else       break;
	}
	cout<<ans<<endl;
	return 0;
}

Java代码

import java.util.*;
public class Main {
   public static void main(String[] args) {
       Scanner scanner = new Scanner(System.in);
       int n = scanner.nextInt();
       int k = scanner.nextInt();
       long m = scanner.nextLong();
       int[] p = new int[n + 1];
       int[] c = new int[n + 1];
       boolean[] buy = new boolean[n + 1];
       PriorityQueue<int[]> P = new PriorityQueue<>((a, b) -> a[0] - b[0]);
       PriorityQueue<int[]> C = new PriorityQueue<>((a, b) -> a[0] - b[0]);
       PriorityQueue<Integer> D = new PriorityQueue<>();
       for (int i = 1; i <= n; i++) {
           p[i] = scanner.nextInt();
           c[i] = scanner.nextInt();
           P.add(new int[]{p[i], i});
           C.add(new int[]{c[i], i});
       }
       for (int i = 1; i <= k; i++)   D.add(0);
       long ans = 0;
       while (!P.isEmpty() && !C.isEmpty()) {
           int[] p1 = P.peek();
           int[] c2 = C.peek();
           if (buy[p1[1]]) { P.poll();  continue; }
           if (buy[c2[1]]) { C.poll();  continue; }
           if (D.peek() > p1[0] - c2[0]) {
               m -= p1[0];
               P.poll();
               buy[p1[1]] = true;
           } else {
               m -= c2[0] + D.peek();
               C.poll();
               buy[c2[1]] = true;
               D.poll();
               D.add(p[c2[1]] - c[c2[1]]);
           }
           if (m >= 0)    ans++;
           else           break;
       }
       System.out.println(ans);
   }
}

Python代码

import heapq
n, k, m = map(int, input().split())
p = [0] * (n+1)
c = [0] * (n+1)
buy = [False] * (n+1)
P = []
C = []
D = []
for i in range(1, n+1):
    p[i], c[i] = map(int, input().split())
    heapq.heappush(P, (p[i], i))
    heapq.heappush(C, (c[i], i))
for i in range(1, k+1):  heapq.heappush(D, 0)
ans = 0
while P and C:
    p1 = P[0]
    c2 = C[0]
    if buy[p1[1]]: heapq.heappop(P); continue
    if buy[c2[1]]: heapq.heappop(C); continue
    if D[0] > p1[0]-c2[0]:
        m -= p1[0]
        heapq.heappop(P)
        buy[p1[1]] = True
    else:
        m -= c2[0]+D[0]
        heapq.heappop(C)
        buy[c2[1]] = True
        heapq.heappop(D)
        heapq.heappush(D, p[c2[1]]-c[c2[1]])
    if m >= 0:    ans += 1
    else:       break
print(ans)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

罗勇军

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

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

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

打赏作者

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

抵扣说明:

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

余额充值