《算法竞赛·快冲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)