目录
1.题目描述
2.解题思路
1. LIS 模型与本题的联系
2. 为什么可以看作 LIS 变种?
3. 本题能够清楚的说明动态规划的本质:
4. 本题的结果计算有别于普通DP:
5.本题的优化思想:滑动窗口指路-->优化技巧--滑动窗口-CSDN博客
3.代码展示
暴力做法(会超时)
单调队列法(最优解法)
在幻想乡,琪露诺是以笨蛋闻名的冰之妖精。
某一天,琪露诺又在玩速冻青蛙,就是用冰把青蛙瞬间冻起来。但是这只青蛙比以往的要聪明许多,在琪露诺来之前就已经跑到了河的对岸。于是琪露诺决定到河岸去追青蛙。
小河可以看作一列格子依次编号为 0 到 N,琪露诺只能从编号小的格子移动到编号大的格子。而且琪露诺按照一种特殊的方式进行移动,当她在格子 i 时,她只移动到区间 [i+L,i+R] 中的任意一格。你问为什么她这么移动,这还不简单,因为她是笨蛋啊。
每一个格子都有一个冰冻指数 Ai,编号为 0 的格子冰冻指数为 0。当琪露诺停留在那一格时就可以得到那一格的冰冻指数 Ai。琪露诺希望能够在到达对岸时,获取最大的冰冻指数,这样她才能狠狠地教训那只青蛙。
但是由于她实在是太笨了,所以她决定拜托你帮它决定怎样前进。
开始时,琪露诺在编号 0 的格子上,只要她下一步的位置编号大于 N 就算到达对岸。
输入格式
第一行三个正整数 N,L,R。
第二行共 N+1 个整数,第 i 个数表示编号为 i−1 的格子的冰冻指数 Ai−1。
输出格式
一个整数,表示最大冰冻指数。
输入输出样例
输入
5 2 3
0 12 3 11 7 -2输出
11
说明/提示
对于 60% 的数据,N≤104。
对于 100% 的数据,N≤2×105,−103≤Ai≤103,1≤L≤R≤N。数据保证最终答案不超过 231−1。
状态表示:dp[ i ]表示到达第 i 个格子所得的最大冰冻指数。
状态计算(计算每一个格子的最大冰冻指数)
dp[ i ] = max ( dp[ i ] , dp[ j ] + a[ i ] ) ,
j ∈ [i-R, i-L]。
1. LIS 模型与本题的联系
1. 核心思想:状态转移依赖于前序元素
传统 LIS:在序列中找到最长的递增子序列,状态转移方程为:
dp[i] = max(dp[j] + 1)
,其中j < i
且a[j] < a[i]
。
每个元素i
的状态依赖于前面比它小的元素j
。本题:在格子中找到最大冰冻指数路径,状态转移方程为:
dp[i] = max(dp[j] + A[i])
,其中j ∈ [i-R, i-L]
且j ≥ 0
。
每个格子i
的状态依赖于前面区间[i-R, i-L]
内的格子j
。2. 序列约束:元素必须按顺序选择
- LIS:子序列中的元素必须保持原序列的顺序(但不要求连续)。
- 本题:跳跃方向必须从编号小的格子到编号大的格子(
i > j
)。2. 为什么可以看作 LIS 变种?
1. 问题结构相似
两者都属于序列上的动态规划问题,状态转移都依赖于前序元素的状态。
2. 变种点在于 “可达条件”
- LIS 的可达条件是
a[j] < a[i]
,而本题的可达条件是j ∈ [i-R, i-L]
。- 本质上都是在满足特定条件的前序元素中选择最优解。
3. 本题能够清楚的说明动态规划的本质:
1.动归的核心思想:
动态规划的核心思想正是通过预先计算每个状态的值,避免重复计算,从而在最终求解时直接利用这些状态值进行快速决策。
2.状态计算:计算每个状态值
- 状态
dp[i]
表示到达格子i
时的最大冰冻指数,它是前序状态(dp[j], j∈[i-R,i-L]
)的函数。- 状态转移方程
dp[i] = max(dp[j] + A[i])
体现了通过前序状态推导当前状态的逻辑。- 存储
dp
数组 避免了对每个i
重复计算前序状态,直接复用已计算的dp[j]
。3.结果计算:直接复用状态值
最终求解时,无需重新计算每个起跳点
i
的冰冻指数,而是直接从dp
数组中读取dp[i]
的值,并比较得出最大值。这体现了动态规划的核心优势:通过预处理状态值,将最终决策转化为简单的查表和比较操作。4. 本题的结果计算有别于普通DP:
结果计算需要全局筛选:
普通 DP 的结果通常是 DP 表的 “终点”(如dp[n]
)或全局最大值,而冰冻题需要从所有可达终点的状态中筛选出满足条件的最优解。最终答案是所有能一步跳出终点的位置 i 中,dp[i] 的最大值。⚠️⚠️⚠️
5.本题的优化思想:滑动窗口指路-->优化技巧--滑动窗口-CSDN博客
- 用
dp[i]
表示到达第 (i) 格子的最大冰冻指数。- 枚举 (i) 从 1 到 (N),对于每个 (i),要找到区间 ([i-R, i-L]) 内所有的 (dp[j]) 的最大值(即上一步能跳到 (i) 的所有 (j))。
- 用单调队列维护这个区间最大值。
#include
#include
#include
using namespace std;
int main() {
int N, L, R;
cin >> N >> L >> R;
vector A(N + 1);
for (int i = 0; i <= N; ++i) cin >> A[i];
vector dp(N + 1, -1e9);
dp[0] = 0;
for (int i = 1; i <= N; ++i) {
for (int j = i - R; j <= i - L; ++j) { //依赖于前几个状态
if (j >= 0 && dp[j] != -1e9) { //注意可达性检测⚠️⚠️⚠️
dp[i] = max(dp[i], dp[j] + A[i]);
}
}
}
int ans = -1e9;
for (int i = 0; i <= N; ++i) {
if (dp[i] == -1e9) continue;
// 只需判断i能否一步跳出终点即可
if (i + L > N) ans = max(ans, dp[i]);
else if (i + R > N) ans = max(ans, dp[i]);
}
cout << ans << endl;
return 0;
}
手搓一个例子:
#include
#include
#include
#include
using namespace std;
int main() {
int N, L, R;
cin >> N >> L >> R;
vector A(N + 1);
for (int i = 0; i <= N; ++i) cin >> A[i];
vector dp(N + 1, -1e9); // -inf
dp[0] = 0;
deque q; // 单调队列,维护窗口[i-R, i-L]最大dp
for (int i = 1; i <= N; ++i) {
// 右端点:i-L,i-L>=0才可能是前驱
if (i - L >= 0) {
// 弹出队尾所有比当前dp小的
while (!q.empty() && dp[q.back()] <= dp[i - L])
q.pop_back();
q.push_back(i - L);
}
// 左端点:i-R
while (!q.empty() && q.front() < i - R)
q.pop_front();
if (!q.empty())
dp[i] = dp[q.front()] + A[i];
// 若队列空,则说明不可达,dp[i]=-inf
}
int ans = -1e9;
// 枚举所有可一步跳出终点的格子
for (int i = 0; i <= N; ++i) {
if (dp[i] == -1e9) continue;
// 只需判断i能否一步跳出终点即可
//最终答案是所有能一步跳出终点的位置 i 中,dp[i] 的最大值。⚠️⚠️⚠️
if (i + L > N) ans = max(ans, dp[i]);
else if (i + R > N) ans = max(ans, dp[i]);⚠️⚠️⚠️
}
cout << ans << endl;
return 0;
}