礼物

礼物

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

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

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

如果约翰用该优惠券给奶牛$i$ 买礼物,那么他只需要支付 $\left\lfloor {P_{i}/2} \right\rfloor + S_{i}$ 元钱。

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

输入格式

第一行包含两个整数 $N$ 和 $B$。

接下来 $N$ 行,每行包含两个整数 $P_{i}$ 和 $S_{i}$。

输出格式

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

数据范围

$1 \leq N \leq 1000$,
$1 \leq B \leq {10}^{9}$,
$0 \leq P_{i},S_{i} \leq {10}^{9}$

输入样例:

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

输出样例:

4

样例解释

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

花费为 $\left( 4+2 \right)+\left( 2+0 \right)+\left( 4+1 \right)+\left( 6+3 \right)=22$。

 

解题思路

枚举+贪心

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

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

  时间复杂度为$O \left( nlogn + n^2 \right) = O \left(n^2 \right)$,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$个礼物。因此答案满足二段性,可以二分答案。

  礼物的总费用$\left( p + s \right)$从小到大排序,假设二分出可以购买$mid$个礼物,对于总费用排序后的数组,我们先将前$mid$个礼物的费用加起来,得到$sum$。同时对于前$mid$个礼物,我们要找到单价$p$最大的那个礼物,假设这个礼物的单价为$p_{i}$运费为$s_{i}$;对于剩下的礼物$\left( n-mid \text{个} \right)$,我们要找到使用优惠卷后总费用$\left( {\left\lfloor {p_{j}/2} \right\rfloor + s_{j}} \right)$最小的那个礼物。

  然后对第$i$个礼物使用优惠卷,看看$i$和$j$使用优惠卷后哪个更便宜,即取两种情况的最小值,再看看是否不超过$M$。即要满足$$M \geq min \left\{ {sum - \left( p_{i} - \left\lfloor p_{i}/2 \right\rfloor \right),~ sum - \left( p_{i} + s_{i} \right)} + \left( {\left\lfloor {p_{j}/2} \right\rfloor + s_{j}} \right) \right\}$$

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

2 170
10 100
20 50

  时间复杂度为$O \left( nlogn \right)$,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$来表示费用。首先因为总费用的取值范围是$1 \leq B \leq {10}^{9}$,因此不可能作为状态来表示。然后我猜是因为再dfs搜索的时候,会用到迭代加深来枚举购买的礼物数量,即状态$j$,因此按照这种思路就把$j$作为维度,把费用用$f$来表示,并且是最小的费用。

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

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

  时间复杂度为$O \left( n^2 \times k \right)$,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 @ 2022-05-11 17:45  onlyblues  阅读(46)  评论(0编辑  收藏  举报
Web Analytics