NWERC2020 A.Automic Engery【动态规划】

题目

gym 链接

动态规划 数论

题意

给你 个原子,第 个原子有 个中子,以及由 的能量。

接下来由 次询问,每次输入一个 ,表示有 个中子的原子。

  • ,则释放对应原子的能量
  • 则会分裂成若干原子,每次分裂只会分裂出两个原子,分裂后只有满足 的原子会继续分裂,以此类推,使分裂后的每个原子的中子数小于 时结束。

求最小能量。

题解

首先不难想到贪心,若 足够大,刚开始的几次一定是选择性价比比较好的,即 尽可能小的。

但硬贪一定是有问题的,原因主要是当分裂后原子的中子数小于 ,就不会继续分裂。这就有可能存在继续贪下去得到的是局部最优解而非全局最优解。

中子数为 , 求所能获得的最小能量,不难想到这是一个背包问题。所以,设某次分裂后原子的中子数为 ,且有 ,对于剩下的 就贪心处理。

对于 的范围,我们要用到鸽巢原理的推论: 个数一定存在某个区间的和能 整除。

假设我们得到性价比最好的中子数为 ,选出 个原子去消去,每个的原子的最大中子数为 ,又因为最后一次分裂还有两个原子,所以预处理的区间为

时间复杂度为

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <cstring>

using namespace std;

typedef long long ll;
const int N = 1e5 + 10;

int n, q, k, p;
ll ans, num;
ll a[N];
ll dp[N];
double mn = 1.0 * 0x3f3f3f3f;

int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (1.0 * a[i] / i < mn) {
mn = 1.0 * a[i] / i;
p = i;
}
}

memset(dp, 0x3f, sizeof(dp));
for (int i = n+1; i <= n * 2; i++) { //预处理区间[n+1, n*2],都由两个数之和转移而来
for (int j = 1; j <= n; j++) {
if (i - j <= n) dp[i] = min(dp[i], a[i-j] + a[j]);
}
}
for (int i = n+1; i <= n * (n+1); i++) {
for (int j = 1; j <= n; j++) {
if (i - j > n) dp[i] = min(dp[i], dp[i-j] + a[j]);
}
}

while (q--) {
cin >> k;
if (k <= n) {
cout << a[k] << endl;
} else {
if (k > n * (p+1)) { // k - num * p ∈ [1, (p-1) * n] (num ∈ N)
num = (k - n * (p+1) + p - 1) / p; // 整除向上取整
ans = num * a[p] + dp[k - num * p];
cout << ans << endl;
} else {
cout << dp[k] << endl;
}
}
}
return 0;
}