礼物

礼物

农夫约翰想给他的 N 头奶牛购买礼物,但是他的预算只有 B 元。

奶牛 i 希望获得的礼物的价格为 Pi,运输成本为 Si,也就是说约翰要帮奶牛 i 买礼物,共需花费 Pi+Si 元钱。

约翰有一张特殊的优惠券,如果使用该优惠券来订购一份礼物,那么该礼物的价格会变为只有正常价格的一半。

如果约翰用该优惠券给奶牛i 买礼物,那么他只需要支付 Pi/2+Si 元钱。

请帮助约翰确定他最多可以给多少头奶牛购买礼物。

输入格式

第一行包含两个整数 NB

接下来 N 行,每行包含两个整数 PiSi

输出格式

输出约翰可以购买礼物的奶牛最大数量。

数据范围

1N1000,
1B109,
0Pi,Si109

输入样例:

5 24
4 2
2 0
8 1
6 3
12 5

输出样例:

4

样例解释

一种最佳方案是约翰给前 4 头奶牛购买礼物,在给第 3 头奶牛购买礼物时使用优惠券。

花费为 (4+2)+(2+0)+(4+1)+(6+3)=22

 

解题思路

枚举+贪心

  首先可以发现用优惠卷肯定比不用优惠卷好,因为使用优惠卷后总费用肯定是不会增加的,所以优惠卷肯定是要用的。

  因此我们先枚举将优惠卷用到哪个礼物上,比如我把优惠卷用到第i个礼物上。因为使用优惠卷的礼物肯定是要购买的,因此先在总费用中将第i个礼物的费用减去,得到剩余的费用M=M(pi/2+si),然后要用M的钱从剩余的礼物中买尽可能多的礼物。接下来就是贪心,先将所有礼物的总费用(p+s)从小到大排序,然后从费用最小的购买,直到剩余的钱不够。

  时间复杂度为O(nlogn+n2)=O(n2),AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 
 7 struct Node {
 8     int p, s;
 9     
10     bool operator<(const Node &t) {
11         return p + s < t.p + t.s;
12     }
13 }a[N];
14 
15 int main() {
16     int n, m;
17     scanf("%d %d", &n, &m);
18     for (int i = 0; i < n; i++) {
19         scanf("%d %d", &a[i].p, &a[i].s);
20     }
21     
22     sort(a, a + n);
23     
24     int ret = 0;
25     for (int i = 0; i < n; i++) {   // 枚举优惠卷用到哪个礼物
26         int t = m - (a[i].p / 2 + a[i].s);  // 购买第i个礼物后剩余的钱
27         if (t < 0) continue;
28         
29         int cnt = 1;    // 已购买第i个礼物
30         for (int j = 0; j < n; j++) {
31             if (i == j) continue;
32             if (t >= a[j].p + a[j].s) { // 剩余的钱可以购买第j个礼物
33                 cnt++;  // 购买的礼物数量+1
34                 t -= a[j].p + a[j].s;   // 更新剩余钱数
35             }
36             else {
37                 break;
38             }
39         }
40         
41         ret = max(ret, cnt);
42     }
43     
44     printf("%d", ret);
45     
46     return 0;
47 }
复制代码

 

二分

  首先答案符合二段性,假设最优解(答案)为ans。当购买礼物数量小于ans,由于钱数可以购买数量为ans个礼物,那么必然可以购买数量小于ans个礼物。当购买礼物数量大于ans,因为最优解为ans,即最多可以购买ans个礼物,因此如果还可以购买数量大于ans个礼物,就于最优解矛盾了,因此不可能购买数量大于ans个礼物。因此答案满足二段性,可以二分答案。

  礼物的总费用(p+s)从小到大排序,假设二分出可以购买mid个礼物,对于总费用排序后的数组,我们先将前mid个礼物的费用加起来,得到sum。同时对于前mid个礼物,我们要找到单价p最大的那个礼物,假设这个礼物的单价为pi运费为si;对于剩下的礼物(nmid),我们要找到使用优惠卷后总费用(pj/2+sj)最小的那个礼物。

  然后对第i个礼物使用优惠卷,看看ij使用优惠卷后哪个更便宜,即取两种情况的最小值,再看看是否不超过M。即要满足Mmin{sum(pipi/2), sum(pi+si)+(pj/2+sj)}

  要这么做是因为,如果只是在前mid个礼物中对单价最大的那个礼物使用优惠卷,得到的总费用并不一定是最小的,因为在剩下的某个礼物中使用了优惠卷后可能费用变得比前面那个使用优惠卷后的礼物要便宜。比如说下面这种情况:

2 170
10 100
20 50

  时间复杂度为O(nlogn),AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 typedef pair<int, int> PII;
 7 typedef long long LL;
 8 
 9 const int N = 1010;
10 
11 int n, m;
12 PII a[N];
13 
14 bool check(int len) {
15     // sum表示前len个礼物的总费用,maxl和s分别表示前len个礼物中单价最大的那个礼物的总费用和对应的单价,minr表示剩余的礼物中优惠后最小的费用
16     LL sum = 0, maxl = 0, s = 0, minr = 2e9;
17     for (int i = 0; i < len; i++) {
18         sum += a[i].first;
19         if (a[i].second >= maxl) {  // 找到前len个礼物中,单价最大的那个礼物
20             maxl = a[i].second;
21             s = a[i].first;
22         }
23     }
24     
25     for (int i = len; i < n; i++) { // 找到剩下的礼物中,优惠后费用最小的那个礼物
26         minr = min(minr, a[i].first - (a[i].second - a[i].second / 2ll));
27     }
28     
29     return m >= min(sum - (maxl - maxl / 2), sum - s + minr);
30 }
31 
32 int main() {
33     scanf("%d %d", &n, &m);
34     for (int i = 0; i < n; i++) {
35         int x, y;
36         scanf("%d %d", &x, &y);
37         a[i] = {x + y, x};  // first表示总费用p+s,second表示单价p
38     }
39     
40     sort(a, a + n);
41     
42     int l = 0, r = n;
43     while (l < r) {
44         int mid = l + r + 1 >> 1;
45         if (check(mid)) l = mid;
46         else r = mid - 1;
47     }
48     printf("%d", l);
49     
50     return 0;
51 }
复制代码

 

动态规划

  这题还可以用动态规划来做。

  为什么会用这些状态来表示呢,或者说为什么不用费用来表示状态,而是用f来表示费用。首先因为总费用的取值范围是1B109,因此不可能作为状态来表示。然后我猜是因为再dfs搜索的时候,会用到迭代加深来枚举购买的礼物数量,即状态j,因此按照这种思路就把j作为维度,把费用用f来表示,并且是最小的费用。

  最后答案就是从大到小枚举j,如果发现f(n,j,1)M,那么就输出j,表示最大可以购买j个礼物。

  用动态规划还可以推广到使用k个优惠卷的情况。

  时间复杂度为O(n2×k),AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 typedef long long LL;
 7 
 8 const int N = 1010;
 9 
10 int p[N], s[N];
11 LL f[N][N][2];
12 
13 int main() {
14     int n, m;
15     scanf("%d %d", &n, &m);
16     for (int i = 1; i <= n; i++) {
17         scanf("%d %d", p + i, s + i);
18     }
19     
20     memset(f, 0x3f, sizeof(f));
21     f[0][0][0] = f[0][0][1] = 0;    // 处理边界,从前0个礼物中购买0个的总费用为0
22     for (int i = 1; i <= n; i++) {
23         for (int j = 0; j <= n; j++) {
24             for (int k = 0; k <= 1; k++) {
25                 f[i][j][k] = min(f[i][j][k], f[i - 1][j][k]);   // 不购买第i个礼物
26                 if (j) {    // j要满足j>0,表示购买礼物i
27                     f[i][j][k] = min(f[i][j][k], f[i - 1][j - 1][k] + p[i] + s[i]);
28                     if (k) f[i][j][k] = min(f[i][j][k], f[i - 1][j - 1][k - 1] + p[i] / 2 + s[i]);  // k要满足k>0,表示第i个礼物使用优惠卷
29                 }
30             }
31         }
32     }
33     
34     // 从小到大枚举礼物的购买数量
35     for (int i = n; i >= 0; i--) {
36         if (f[n][i][1] <= m) {  // 发现可以购买i个,直接输出
37             printf("%d", i);
38             return 0;
39         }
40     }
41     
42     return 0;
43 }
复制代码

  其中f数组可以把第一维优化掉,AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 typedef long long LL;
 7 
 8 const int N = 1010;
 9 
10 int p[N], s[N];
11 LL f[N][2];
12 
13 int main() {
14     int n, m;
15     scanf("%d %d", &n, &m);
16     for (int i = 1; i <= n; i++) {
17         scanf("%d %d", p + i, s + i);
18     }
19     
20     memset(f, 0x3f, sizeof(f));
21     f[0][0] = f[0][1] = 0;
22     for (int i = 1; i <= n; i++) {
23         for (int j = n; j >= 0; j--) {
24             for (int k = 0; k <= 1; k++) {
25                 f[j][k] = min(f[j][k], f[j][k]);
26                 if (j) {
27                     f[j][k] = min(f[j][k], f[j - 1][k] + p[i] + s[i]);
28                     if (k) f[j][k] = min(f[j][k], f[j - 1][k - 1] + p[i] / 2 + s[i]);
29                 }
30             }
31         }
32     }
33     
34     for (int i = n; i >= 0; i--) {
35         if (f[i][1] <= m) {
36             printf("%d", i);
37             return 0;
38         }
39     }
40     
41     return 0;
42 }
复制代码

 

 参考资料

  AcWing 2040. 礼物(春季每日一题2022):https://www.acwing.com/video/3853/

  AcWing 2040. 礼物:https://www.acwing.com/solution/content/114603/

  AcWing 2040. 礼物(2种方法:枚举贪心/动态规划) :https://www.acwing.com/solution/content/114584/

posted @   onlyblues  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示