209 lines
7.1 KiB
Markdown
209 lines
7.1 KiB
Markdown
## $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;
|
||
}
|
||
``` |