Files
python/TangDou/CSP-S/T1/484.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

209 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## $AcWing$ $484$ 过河
### 一、问题描述
青蛙需要从独木桥的起点($0$)跳到终点($L$)或更远的位置。每次跳跃的距离在$[S,T]$范围内。桥上有一些石子,青蛙要尽可能少地踩到石子。求最少需要踩到的石子数。
### 二、简单版本
如果$L$不是很大,比如$100000$,可以直接使用动态规划:
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 100500;
int n, s, t, m; // 桥长n,跳跃范围[s,t],石子数m
int w[N], f[N]; // w[i]:位置i是否有石子,f[i]:到位置i的最少石子数
const int INF = 0x3f3f3f3f;
int main() {
cin >> n >> s >> t >> m;
for (int i = 1; i <= m; i++) {
int x; cin >> x;
w[x] = 1;
}
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for (int i = 1; i < n + t; i++)
for (int j = i - t; j <= i - s; j++)
if (j >= 0)
f[i] = min(f[i], f[j] + w[i]);
int res = INF;
for (int i = n; i < n + t; i++)
res = min(res, f[i]);
printf("%d\n", res);
return 0;
}
```
### 三、解题思路
本题数据范围$L<=1e9$,需要优化。
#### 1. 特殊情况处理
当$S=T$时,青蛙只能跳固定距离$S$,只需检查石子位置是否是$S$的倍数:
```cpp {.line-numbers}
if (S == T) {
int res = 0;
while (m--) {
int x; cin >> x;
if (x % S == 0) res++;
}
printf("%d\n", res);
return 0;
}
```
#### 2. 离散化优化
$Q$:为什么需要离散化?
$A$: 本题中$L$的范围是$1e9$,但石子数量$m$最多只有$100$个。这意味着在桥上有大量没有石子的位置,这些位置在DP过程中造成了巨大的空间和时间浪费。我们需要压缩这些无用的空间。
$Q$:如何确定哪些位置可以压缩?
$A$: 考虑两个相邻石子$i$和$i+1$,它们的位置分别为$stones[i]$和$stones[i+1]$。我们需要分析这两个石子之间的空间是否可以压缩:
1. 关键观察:
- 青蛙从$stones[i]$出发,最多可以跳$T$步
- 青蛙要到达$stones[i+1]$,最少需要跳$S$步
- 因此,我们只需要关注$stones[i]$前后各$T$个位置和$stones[i+1]$前后各$T$个位置
2. 数论优化:
- 基于定理:当$p$与$q$互质时,$(p-1)*(q-1)-1$是$p$与$q$无法表示的最大数
- 取$p=10,q=9$(因为$S,T\leq10$),则最大不可表示数为$71$
- 这意味着:如果两个石子之间的距离大于$71+2*10=91$(加上前后各$10$的缓冲),那么中间的空间可以被压缩到$91$以内
3. 压缩原理:
- 设两个石子距离为$d$
- 如果$d>91$,我们可以将中间的空间压缩到$91$
- 这样压缩不会影响最优解,因为:
* 压缩后的空间仍然保留了所有可能的跳跃路径
* 压缩不会改变石子的相对位置关系
* 压缩后的空间足够大,可以保证所有可能的跳跃组合都能被表示
4. 具体实现:
```cpp
int last = 0, k = 0;
for (int i = 0; i < m; i++) {
// 压缩两个石子之间的空间
for (int j = 0; j < min(stones[i] - last, 91); j++)
w[++k] = 0; // 标记压缩后的无石子位置
w[k] = 1; // 标记石子的位置
last = stones[i];
}
```
5. 优化效果:
- 原始空间:$O(L)$,$L$可达$1e9$
- 压缩后空间:$O(m*91)$,$m$最多$100$
- 空间优化:从$1e9$降到约$10000$
- 时间优化:DP过程从$O(L*T)$降到$O(m*91*T)$
#### 3. 动态规划
- $f[i]$:跳到位置$i$的最少石子数
- 状态转移:$f[i] = min(f[i], f[i-j] + w[i])$,$j∈[S,T]$
```cpp
for (int i = 1; i <= k + 10; i++) {
f[i] = 1e9;
for (int j = S; j <= T; j++)
if (i - j >= 0)
f[i] = min(f[i], f[i - j] + w[i]);
}
```
#### 4. 求解答案
在最后可能到达的位置中找最小值:
```cpp
int res = 1e9;
for (int i = k + 1; i <= k + 10; i++)
res = min(res, f[i]);
```
### 四、复杂度分析
- 时间复杂度:$O(m*T)$
- 空间复杂度:$O(N)$,$N=10020$
### 五、示例分析
输入:
```
10
2 3 5
2 3 5 6 7
```
最优解为2,路径:0->2->5->8->11,踩到位置2和5的石子。
### 六、最终代码
```cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 10020; // 定义一个足够大的数组空间
// 全局变量声明
int L, S, T, m; // L:桥长度, S:最小跳跃距离, T:最大跳跃距离, m:石子数量
int stones[N]; // 存储石子的位置
int w[N]; // 记录每个位置是否有石子(离散化后)
int f[N]; // dp数组,f[i]表示跳到位置i最少需要踩的石子数
int main() {
// 读入基本数据
cin >> L >> S >> T >> m;
// 特殊情况处理:当最小跳跃距离等于最大跳跃距离时
// 此时青蛙只能跳固定距离S
if (S == T) {
int res = 0;
while (m--) {
int x;
cin >> x;
// 如果石子位置是S的倍数,青蛙一定会踩到
if (x % S == 0)
res++;
}
printf("%d\n", res);
return 0;
}
// 读入所有石子的位置
for (int i = 0; i < m; i++)
cin >> stones[i];
sort(stones, stones + m); // 将石子位置从小到大排序
// 核心算法:离散化处理
int last = 0, k = 0; // last:上一个处理的位置, k:离散化后的新位置
for (int i = 0; i < m; i++) {
// 重要优化:基于数论的优化
// 当两个相邻石子距离过大时,中间的空隙可以被压缩
// 91是基于(p-1)(q-1)-1+20得来,其中p=10,q=9
// 这个优化基于:当两点距离大于91时,一定可以通过组合S和T的跳法到达
// 核心:桥太长,无法遍历,但有一个突破口,就是石子数量有限
// 如何利用这点呢?使用上面的定理,我们知道,间距太大的间隙可以被压缩,只有大于91的空隙,就可以压缩
// 如何压缩,压缩完是什么样的东西呢?
// 就是把无意义的没有石子的位置移除掉!无意义的是指肯定能通过ST踩到的并且无石子的位置
// 有意义的就是可能踩到也可能踩不到的位置,这些位置保留下来,无意义的干掉就行了。
// 下面的循环,枚举的是两个相邻石子之间的空位。
for (int j = 0; j < min(stones[i] - last, 91); j++)
w[++k] = 0; // 在新的离散化空间中标记无石子的位置
w[k] = 1; // 标记有石子的位置
last = stones[i];
}
// 动态规划初始化
memset(f, 0x3f, sizeof(f)); // 预求最大先设最小
f[0] = 0; // 注意边界的初始化,思考现实含义:位置 0最少需要踩的石子数为 0
// 动态规划过程
for (int i = 1; i <= k + T; i++) {
// 枚举所有可能的跳跃距离
for (int j = S; j <= T; j++)
if (i - j >= 0) // 确保不会跳到负数位置
f[i] = min(f[i], f[i - j] + w[i]); // 状态转移
}
// 在最后可能到达的位置中找最小值
int res = 1e9;
for (int i = k; i < k + T; i++)
res = min(res, f[i]);
printf("%d\n", res);
return 0;
}
```