# 14. 战士技能规划设计[200分]
# 题目内容
你为一款动作游戏设计战士角色的技能:战士每个技能会消耗不同能量,释放技能有 2 个约束:
- 连续释放技能数量不能超过 $m$ 个
- 技能能量总和不能超过能量上限 $k$
如果超过则必须中断当前技能,进入调息状态(即分段)。
战士有一项爆发技巧:在单次战斗中有一次能量上限翻倍至 $2k$ 的机会,此项场景下战士需使用连续的 $w$ 个技能(技能数量限制 $m$ 依然生效)。
任务目标: 作为战术分析师,你需要为战士规划最优的技能释放序列。给定一套技能的能量消耗列表 $a$,请计算在合理使用爆发机会(或选择不使用)的前提下,释放完所有技能所需的最少分段数(即最少调息次数)。若存在某个技能的能耗过高,即使开启爆发也无法释放(大于 $2k$),则判定为无解,返回 $-1$。
# 输入描述
输入参数说明:
- $k$:能量上限,正整数
- $m$:单次调息最大技能数,正整数
- $w$:爆发持续技能数,正整数
- $a$:技能能量消耗列表,长度为 $n$,每个元素为正整数
数据范围:
- $n$ 为技能数量,即 $a$ 的长度
- $1 \le w \le n$
- $1 \le n \le 100000$
- $1 \le m \le n$
- $1 \le k \le 1000000000$
- $1 \le a[i] \le 1000000000$
# 输出描述
输出满足条件的最少分段数(最少调息次数)。若无解则输出 $-1$。
# 样例
# 样例 1
输入
5 3 2
3,4,3,4
1
2
2
输出
3
1
说明: 爆发窗口覆盖索引 $0 \sim 1$ 的 [3,4] 为 1 段,剩余 [3]、[4] 各 1 段 $\to$ 共 3 段。
# 样例 2
输入
5 3 2
6,1,2,3
1
2
2
输出
2
1
说明: $a[0] = 6 > k = 5$ 且 $a[0] < 2k = 10$,必须靠爆发窗口覆盖 $a[0]$ 才有解。
# 样例 3
输入
10 5 3
1,1,1,1,1
1
2
2
输出
1
1
说明: $sum(a) = 5 \le k = 10$ 且 $n = 5 \le m = 5$,无需爆发即可 1 段放完。若开启爆发,窗口最多覆盖 $w = 3$ 个技能,剩余 1 个技能需另开 1 段,反而变成 2 段,因此不使用爆发更优。
# 代码
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let lines = [];
rl.on('line', (input) => {
lines.push(input);
if (lines.length === 2) {
// 解析输入
const [k, m, w] = lines[0].split(' ').map(Number);
const a = lines[1].split(',').map(Number);
const n = a.length;
// ========== 无解判断 ==========
// 如果某个技能大于2k,即使开启爆发也无法释放
for (let v of a) {
if (v > 2 * k) {
console.log(-1);
return;
}
}
// ========== 核心函数:计算从某位置开始最多能放几个技能 ==========
function getMaxLen(start, limitK) {
let sum = 0; // 当前段累计能量
let cnt = 0; // 当前段技能数量
for (let i = start; i < n; i++) {
// 约束1:技能数量不能超过m
if (cnt + 1 > m) break;
// 约束2:能量总和不能超过limitK
if (sum + a[i] > limitK) break;
sum += a[i];
cnt++;
}
return cnt;
}
// ========== 计算不爆发时的最少分段数 ==========
function calcSegments(limitK) {
let seg = 0;
let i = 0;
while (i < n) {
let len = getMaxLen(i, limitK);
// 如果len=0,说明当前技能大于limitK,无法放入任何段
if (len === 0) return Infinity;
seg++; // 新开一段
i += len; // 跳过已放的技能
}
return seg;
}
// ========== 情况1:不使用爆发 ==========
let ans = calcSegments(k);
// ========== 情况2:使用爆发 ==========
// 枚举爆发窗口的起始位置
for (let start = 0; start < n; start++) {
// 检查是否有足够的技能
if (start + w > n) break;
// ---- 检查爆发窗口是否可行 ----
let sum = 0;
let canBurst = true;
// 检查从start开始的w个技能
for (let i = start; i < start + w; i++) {
sum += a[i];
if (sum > 2 * k) {
canBurst = false;
break;
}
}
// 如果窗口不可行,跳过
if (!canBurst) continue;
// ---- 计算使用爆发时的总分段数 ----
let seg = 0;
let i = 0;
// 1) 爆发前的部分(使用正常能量k)
while (i < start) {
let len = getMaxLen(i, k);
if (len === 0) {
seg = Infinity;
break;
}
seg++;
i += len;
}
if (seg === Infinity) continue;
// 2) 爆发窗口(单独一段),固定w个技能
seg++;
i = start + w; // 跳过爆发窗口
// 3) 爆发后的部分(使用正常能量k)
while (i < n) {
let len = getMaxLen(i, k);
if (len === 0) {
seg = Infinity;
break;
}
seg++;
i += len;
}
// 更新最优解
if (seg < ans) {
ans = seg;
}
}
// ========== 输出结果 ==========
console.log(ans === Infinity ? -1 : ans);
}
});
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
← 云南菌子加工[200分] 数字系统转换器 →