过河 (离散化 + dp)

过河

链接: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;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值