[洛谷]摇钱树 题解
前言
这是本蒟蒻切的第一道青题,觉得很有纪念意义,就出了这篇题解,文章markdown使用deepseek润色,但保证人类贡献大于AI
一、翻译题目
有 \(n\) 棵摇钱树,第 \(i\) 棵初始时有 \(m_i\) 枚金币。每过一天,第 \(i\) 棵树会掉落 \(b_i\) 枚金币,这些掉落的金币不属于这个入。
这个入 一共可以停留 \(k\) 天,每天可以选择砍掉一棵树,并获得这棵树上当时剩余的全部金币。一棵树被砍后就不能再砍。
问:在这 \(k\) 天内,这个入 最多能获得多少金币?
输入包含多组数据,以 0 0 结束(这里非常重要)
二、贪
1. 砍树的收益公式
假设 这个入 在第 \(t\) 天砍掉第 \(i\) 棵树(\(t\) 从 1 开始计数),那么这棵树已经经历了 \(t-1\) 天的掉落,剩余金币为:
所以砍的时间越早,收益越高。
2. 最优砍树顺序
如果已经决定了要砍哪些树,那么这些树的砍伐顺序应该怎样安排?
设两棵树 \(i\) 和 \(j\),假设它们将在第 \(t\) 天和第 \(t+1\) 天被砍。
- 若先砍 \(i\) 再砍 \(j\),总收益为:\[(m_i - (t-1)b_i) + (m_j - t b_j) \]
- 若先砍 \(j\) 再砍 \(i\),总收益为:\[(m_j - (t-1)b_j) + (m_i - t b_i) \]
两者相减:
so:
- 如果 \(b_i > b_j\),则先砍 \(i\) 再砍 \(j\) 的收益更大。
- 如果 \(b_i < b_j\),则先砍 \(j\) 再砍 \(i\) 的收益更大。
结论: 为了最大化收益,所有被砍的树应该按照 \(b_i\) 从大到小 的顺序砍。
也就是说,掉落速度越快的树,越要尽早砍掉,减少损失。
三、dp
确定了砍树顺序,但还需要从 \(n\) 棵树中选出若干棵来砍(最多砍 \(k\) 棵,且不能超过 \(n\) 棵)。
不能直接贪心选 \(b_i\) 最大的前 \(k\) 棵,因为初始金币 \(m_i\) 也影响收益。
1. 排序sort
先将所有树按照 \(b_i\) 从大到小排序。
排序后,如果我们决定砍某几棵树,那么它们的砍伐顺序就是它们在排序后的相对顺序。
2. 状态定义
设 \(dp[i][j]\) 表示:考虑排序后的前 \(i\) 棵树,从中选择 \(j\) 棵树砍掉所能获得的最大金币数。
其中 \(0 \le i \le n\),\(0 \le j \le \min(i, k)\)。
由于我们按 \(b_i\) 降序处理,如果第 \(i\) 棵树被选中,并且它是选中的第 \(j\) 棵树,那么它一定是在第 \(j\) 天被砍(因为前面已经选了 \(j-1\) 棵,且它们的 \(b\) 都不小于当前树)。
所以它的收益为:
3. 状态转移方程
对于第 \(i\) 棵树,有两种选择:
-
不砍它:
继承前 \(i-1\) 棵树选 \(j\) 棵的最优值:\[dp[i][j] = dp[i-1][j] \] -
砍它:
它将成为第 \(j\) 棵被砍的树,收益为:\[dp[i-1][j-1] + m_i - (j-1) \times b_i \]
因此,完整的状态转移方程为:
其中:
- \(1 \le i \le n\)
- \(1 \le j \le \min(i, k)\)
- 初始状态:\(dp[0][0] = 0\),其余 \(dp[0][j] = -\infty\)
- 对于不合法的状态(如 \(j > i\)),也视为 \(-\infty\)
最终答案:
因为 这个入 可以选择不砍满 \(k\) 天,所以需要取所有可能砍树数量的最大值。
4. 优化
观察转移方程,\(dp[i][j]\) 只依赖于 \(dp[i-1][j]\) 和 \(dp[i-1][j-1]\),因此可以优化为一维数组:
设 \(dp[j]\) 表示当前处理到的树中,选择 \(j\) 棵树砍掉的最大收益。
对于每一棵树 \(i\)(按 \(b_i\) 降序排序后),更新:
注意: 因为每棵树能切仅能砍一次,所以 \(j\) 必须倒序枚举,从大到小更新,避免同一棵树被重复使用。
枚举范围:\(j\) 从 \(\min(i, k)\) 递减到 \(1\)。
5. 边界
- 最多能砍的树的数量为 \(K = \min(n, k)\)。
- 初始化:\(dp[0] = 0\),其余 \(dp[j] = -\infty\)。
- 最终为:\(\max_{0 \le j \le K} dp[j]\)。
四、复杂度分析(deepseek)
- 排序:\(O(n \log n)\)
- 动态规划:\(O(n \times K) \le O(n \times k)\)
- 总时间复杂度:\(O(n \log n + nk)\),其中 \(n, k \le 1000\),最多 10 组数据,完全可行。
- 空间复杂度:\(O(K)\),使用一维 DP 数组。
Code:
#include <bits/stdc++.h>
using namespace std;
struct Tree {
int m, b;
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
while (cin >> n >> k) {
if (n == 0 && k == 0) break;
vector<Tree> trees(n);
for (int i = 0; i < n; ++i) cin >> trees[i].m;
for (int i = 0; i < n; ++i) cin >> trees[i].b;
// 按掉落速度 b 从大到小排序
sort(trees.begin(), trees.end(), [](const Tree& a, const Tree& b) {
return a.b > b.b;
});
int K = min(n, k); // 最多能砍的树的数量
const long long NEG = -1e18;
vector<long long> dp(K + 1, NEG);
dp[0] = 0;
for (int i = 0; i < n; ++i) {
int m = trees[i].m;
int b = trees[i].b;
// 倒序枚举 j,保证每棵树只被选一次
for (int j = min(i + 1, K); j >= 1; --j) {
long long val = dp[j - 1] + m - 1LL * (j - 1) * b;
if (val > dp[j]) dp[j] = val;
}
}
// 答案可以是砍 0 到 K 棵树中的任意一种情况
long long ans = 0;
for (int j = 0; j <= K; ++j) {
ans = max(ans, dp[j]);
}
cout << ans << '\n';
}
return 0;
}
六、样例验证
样例 1
3 3
10 20 30
4 5 6
按 \(b\) 降序:树3(\(b=6,m=30\)),树2(\(b=5,m=20\)),树1(\(b=4,m=10\))。
- 第1天砍树3:得 30
- 第2天砍树2:得 \(20 - 5 = 15\)
- 第3天砍树1:得 \(10 - 2 \times 4 = 2\)
总收益:\(30 + 15 + 2 = 47\),与输出一致。
样例 2
4 3
20 30 40 50
2 7 6 5
按 \(b\) 降序:树2(\(b=7,m=30\)),树3(\(b=6,m=40\)),树4(\(b=5,m=50\)),树1(\(b=2,m=20\))。
- 第1天砍树2:得 30
- 第2天砍树3:得 \(40 - 6 = 34\)
- 第3天砍树4:得 \(50 - 2 \times 5 = 40\)
总收益:\(30 + 34 + 40 = 104\),与输出一致。
七、总结
本题的核心是:
- 贪心确定顺序:按掉落速度 \(b_i\) 从大到小砍。
- 动态规划选择树:在排序后的序列中,用 \(dp[j]\) 表示选 \(j\) 棵树的最大收益。
- 状态转移方程:\[dp[j] = \max \Big( dp[j],\ dp[j-1] + m_i - (j-1) \times b_i \Big) \]倒序枚举 \(j\),保证每棵树只选一次。
- 注意可以不砍满 \(k\) 天,最终答案取 \(dp[0 \dots \min(n,k)]\) 的最大值。
这样就能在 \(O(nk)\) 时间内高效解决问题。
浙公网安备 33010602011771号