Java --> Lambda表示式
禮物
農夫約翰想給他的 $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/