过河
链接:https://ac.nowcoder.com/acm/problem/16655
题目描述
在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点:0,1,……,L(其中L是桥的长度)。坐标为0的点表示桥的起点,坐标为L的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是S到T之间的任意正整数(包括S,T)。当青蛙跳到或跳过坐标为L的点时,就算青蛙已经跳出了独木桥。
题目给出独木桥的长度L,青蛙跳跃的距离范围S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。
输入描述:
第一行有一个正整数L(1<=L<=109),表示独木桥的长度。
第二行有三个正整数S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离,及桥上石子的个数,其中1<=S<=T<=10,1<=M<=100。
第三行有M个不同的正整数分别表示这M个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。
所有相邻的整数之间用一个空格隔开。
输出描述:
只包括一个整数,表示青蛙过河最少需要踩到的石子数。
示例1
输入
10
2 3 5
2 3 5 6 7
输出
2
备注:
对于30%的数据,L<=10000;
对于全部的数据,L<=109。
思路 :
观察题目数据范围 石子的数目很少 m <= 100, 青蛙跳的步幅 s,t<=10但是总长度 L 却是 1e9 级别的
假如LL 很小的话, 我们很容易考虑 dp[i]dp[i] 表示第 ii 这个位置踩的最小石头数
有转移方程dp[i]=dp[i−j]+[i∈stone],s<=j<=t
考虑上图的情况, 在 L 高达 1e9 的时候, 每个石头之间的距离 len 将会很大
但是这个距离 len 有很多是无用的情况
为什么呢? 我们可以通过不断的跳缩短距离, 最后决定是否要踩到石头的仅仅是一小段距离
前面的都能通过不停地往前跳抵消掉
无用距离解析 :
方法一 :
那么最后一小段距离最大有多少呢? 我这里取了 2520, 因为 2520 是 1 到 10 的最小公倍数
我们对石头进行缩点, 相邻的距离过大的先对 2520 取模(因为大于等于 2520 总可以通过弹跳抵消)
缩点后总长度会大幅度减小, 数量级别降到了 2e5, 直接做上面的 dp 即可
注意还要特判 t == s 的情况, 此时只需要统计 ai % t == 0
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long inf = 1e14;
const int N = 1e5 + 5;
const double eps = 1e-10;
const ll mod = 1e9 + 7;
int a[N];
int dp[5000005];
int vis[5000005];
int dist[5000005];
int main() {
ios::sync_with_stdio(false), cin.tie(nullptr);
int L;
int S, T, n;
cin >> L >> S >> T >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
a[n + 1] = L;
if(S == T) { // 特判
int cnt = 0;
for(int i = 1; i <= n; i++) {
if(a[i] % T == 0) {
cnt++;
}
}
cout << cnt << "\n";
return 0;
}
for(int i = 1; i <= n; i++) {
dist[i] = (a[i] - a[i - 1]) % 2520; // 缩点
}
for(int i = 1; i <= n; i++) { // 缩点
a[i] = a[i - 1] + dist[i];
vis[a[i]] = 1; // 此处有石头
}
L = a[n];
//cout << L << "\n";
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for(int i = 1; i <= L + T; i++) {
for(int j = S; j <= T; j++) {
if(i - j >= 0)
dp[i] = min(dp[i], dp[i - j] + vis[i]);
}
}
int ans = 1e9;
for(int i = L; i <= L + T - 1; i++) {
ans = min(ans, dp[i]);
}
cout << ans << "\n";
return 0;
}
方法二 :
因为这道题我们要考虑的因素只有该点是不是石子,所以一维数组就行了。
状态转移方程就是:
dp[i] = min(dp[i], dp[i - j] + visited[i]);
//s <= j <= t,判断该位和前面可以跳到这一位的转移数据的最小值
//visited数组表示当前位是否有石子,有为1,没有为0(表示有石子就要+1计数)
下面的重点就是,离散化:
这是我第一次写离散化的题目。
简单来说就是将一个超长的区间变短,但是要保持效果是一样的。
我们现在用数组模拟一个数轴,数组取值0/1,表示该位置上有没有石头,但是题目大小是1e9,所以我们要离散化压缩。
我们在这里离散化的长度为S * T,也就是说,只要相邻两个点之间的距离大于S * T的话,直接把距离变成S * T。
为什么变成S * T呢?假如要到当前点,在该点前S * T位置之前的点,是一定可以有方法到的。
所以我们的离散化操作就是:
for (int i = 1; i <= m; i++) {
int dis = stone[i] - stone[i - 1];
if (dis >= s * t) dis = s * t;
now[i] = now[i - 1] + dis;
visited[now[i]] = 1;
}
打代码打代码:
先用一个数组储存下变量和我们原来所以石头的位置。
因为不知道变量是不是有序的,所以我们排个序。
特判一下相等的情况,不相等就离散化。
按照上述方法离散化。
然后按照上述方法递推。
最后再在dp数组在最后一个石头后面找最小值。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
const int INF = 0x3f3f3f3f;
int stone[105],now[105];
int vis[N],dp[N];
int l,s,t,m;
int main(){
cin >> l;
cin >> s >> t >> m;
for(int i = 1;i <= m;i ++) cin >> stone[i];
sort(stone + 1,stone + 1 + m);
//特判
if(s == t){
int cnt = 0;
for(int i = 1;i <= m;i ++){
if(stone[i] % s == 0)
cnt ++;
}
cout << cnt << endl;
return 0;
}
//缩点
now[0] = 0;
for(int i = 1;i <= m;i ++){
int dis = stone[i] - stone[i - 1];
if(dis >= s * t) dis = s * t;
now[i] = now[i - 1] + dis;
vis[now[i]] = 1;
}
//dp[i]走了i的长度至少要踩多少石子
l = now[m] + s * t;
memset(dp,INF,sizeof dp);
dp[0] = 0;
for(int i = 1;i <= l;i ++){
for(int j = s;j <= t;j ++){
if(i >= j)
dp[i] = min(dp[i],dp[i - j] + vis[i]);
}
}
//搜答案
int ans = INF;
for(int i = l - s;i <= l;i ++)
ans = min(ans,dp[i]);
cout << ans << endl;
return 0;
}