2018NOIP普及组真题 3. 摆渡车

文章讲述了如何通过动态规划方法解决线上编程竞赛中关于等车时间的问题,利用区间划分和前缀和计算等技巧,求解学生在特定时间内等车时间的最小总和。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线上OJ:

一本通:http://ybt.ssoier.cn:8088/problem_show.php?pid=1980

这道题出现在普及组第三题,真的是有难度的。不应叫摆渡车,应叫灵魂摆渡车。

核心思想:
  1. 首先,这道题可以考虑用动态规划 DP 来做。
  2. 其次,由于题目问的是 “第 i 位同学在第 ti 分钟去等车,求等车时间之和最小值”,我们可以考虑设
    f[i] 表示 “截止到第 i 分钟时,等车时间的最小和”

我们对题目进行梳理,发现等车时间其实就是一个水平的数轴,如下图所示。

在这里插入图片描述

通过对上图的分析,我们发现 等待时间 其实就是 区间内每个点到区间右边界的距离

比如在 1 - 6 这个区间:5时刻到达的同学等待时间就是 (6-5)* 2人 = 1分钟 * 2人 = 2分钟
比如在 11 - 16 这个区间:13时刻到达的同学等待时间就是 (16-13)* 1人 = 3分钟 * 1人 = 3分钟
比如在 6 - 13 这个区间:11时刻到达的同学等待时间就是 (13-11)* 1人 = 2分钟 * 1人 = 2分钟

此时,可得到以下三个结论:

结论1:题目所求的等待时间之和 实际就是求 所有点到各自所属区间右边界的距离之和

结论2:由于车辆往返一趟的时间是 m 分钟, 所以 每个区间的宽度不可能小于 m (即: 下一次发车的时间 - 上一次发车的时间 不可能小于 m),因为只有一辆摆渡车,车子还没回来,无法发车。

结论3:由于车辆往返一趟的时间是 m 分钟, 所以 每个区间的宽度不应该 大于等于 2 倍的m (即: 下一次发车的时间 - 上一次发车的时间 不应该大于 2m),因为如果区间>2m,那就又可以多跑一趟车(反正这道题不考虑油费)。

我们重新回到 DP 的动态转移方程定义上,令

f[i] 表示时间轴上的 (0, i] 区间内所有点到 各自所属区间右边界距离之和最小值

假设区间 (0, i] 中存在一个时间点 j,则区间可划分为 (0, j](j, i]
所以状态转移方程可以写为:

f [ i ] = f [ j ] + ( j , i ] 区间内每个点到 i 的距离和,① f[i] = f[j] + (j, i]区间内每个点到 i 的距离和 ,① f[i]=f[j]+(j,i]区间内每个点到i的距离和,

我们记 (j, i] 区间内的某个点 t k t_k tk,则 j < t k ≤ i j < t_k ≤ i j<tki。所以 t k t_k tk 到 i 的距离为 i − t k i - t_k itk
如果 (j, i] 区间内有多个 t k t_k tk 这样的点,则 (j, i] 区间内 每个 t k t_k tk 点到 i距离和 可表示为:

∑ j < t k ≤ i ( i − t k ) \displaystyle\sum_{j < t_k\le i}(i - t_k) j<tki(itk) , ②

所以状态转移方程 ① 可以表示为:

f [ i ] = f [ j ] + ∑ j < t k ≤ i ( i − t k ) f[i] = f[j] + \displaystyle\sum_{j < t_k\le i}(i - t_k) f[i]=f[j]+j<tki(itk) , ③

由于题目要求的是 距离之和的最小值,所以计算 f [ i ] f[i] f[i] 时需对 f [ j ] f[j] f[j] 进行 枚举,并记录最小值作为最后的 f [ i ] f[i] f[i],所以 ③ 式 应进化为

f [ i ] = min ⁡ ( f [ j ] + ∑ j < t k ≤ i ( i − t k ) ) f[i] = \min { (f[j] + \displaystyle\sum_{j < t_k\le i}(i - t_k)) } f[i]=min(f[j]+j<tki(itk)) , ④

根据 结论2结论3,我们知道每个 区间的宽度不可能小于 m,且 不应大于等于2m
所以 m ≤ i − j < 2 m m ≤ i - j < 2m mij2m, 所以 i − 2 m < j ≤ i − m i-2m < j ≤ i - m i2mjim

所以上述 ④ 式可进化为

f [ i ] = min ⁡ i − 2 m < j ≤ i − m ( f [ j ] + ∑ j < t k ≤ i ( i − t k ) ) f[i] = \underset{i-2m < j \le i-m}{\mathop{\min}} { (f[j] + \displaystyle\sum_{j < t_k\le i}(i - t_k)) } f[i]=i2m<jimmin(f[j]+j<tki(itk)) , ⑤

在上述 ⑤ 式中还包含了一个较为复杂的 ② 式。我们尝试化简 ② 式,先把括号拆开,得

∑ j < t k ≤ i ( i − t k ) = ∑ j < t k ≤ i ( i ) − ∑ j < t k ≤ i ( t k ) \displaystyle\sum_{j < t_k\le i}(i - t_k) = \displaystyle\sum_{j < t_k\le i}(i) - \displaystyle\sum_{j < t_k\le i}(t_k) j<tki(itk)=j<tki(i)j<tki(tk) , ⑥

此时发现,拆解的两部分均可以用 前缀和 的方法来快速计算。

其中,前面的 ∑ j < t k ≤ i ( i ) \displaystyle\sum_{j < t_k\le i}(i) j<tki(i) 的数值可理解为在 区间 (j, i] 内有几个 t k t_k tk, 就加几次 i

比如在 (1, 6] 内有2个点,那数值就是 6*2=12
如果令 cnt[i] 表示 截止到 i 点共有cnt[i] 个数cnt[j] 表示 截止到 j 点共有 cnt[j] 个数。则

∑ j < t k ≤ i ( i ) = ( c n t [ i ] − c n t [ j ] ) ∗ i \displaystyle\sum_{j < t_k\le i}(i) = (cnt[i] - cnt[j])*i j<tki(i)=(cnt[i]cnt[j])i

同理, ∑ j < t k ≤ i ( t k ) \displaystyle\sum_{j < t_k\le i}(t_k) j<tki(tk) 的数值可理解为 在区间 (j, i] 内所有 t k t_k tk 点的数值和

比如在 (1, 6] 内有2个点均为 t k = 5 t_k=5 tk=5,那数值就是 5+5=10
如果令 sum[i] 表示 截止到 i 点的所有点的距离和sum[j] 表示截止到 j 点的所有点的距离和。则 (j, i] 内所有 t k t_k tk 点的数值和 可以表示为

∑ j < t k ≤ i ( t k ) = ( s u m [ i ] − s u m [ j ] ) \displaystyle\sum_{j < t_k\le i}(t_k) = (sum[i] - sum[j]) j<tki(tk)=(sum[i]sum[j])

所以 ⑥ 式可以进化为

∑ j < t k ≤ i ( i − t k ) = ( c n t [ i ] − c n t [ j ] ) ∗ i + ( s u m [ i ] − s u m [ j ] ) \displaystyle\sum_{j < t_k\le i}(i - t_k) = (cnt[i] - cnt[j])*i + (sum[i] - sum[j]) j<tki(itk)=(cnt[i]cnt[j])i+(sum[i]sum[j]) , ⑦

将 ⑦ 式带回 ⑤ 式得

f [ i ] = min ⁡ i − 2 m < j ≤ i − m ( f [ j ] + ( c n t [ i ] − c n t [ j ] ) ∗ i + ( s u m [ i ] − s u m [ j ] ) ) f[i] = \underset{i-2m < j \le i-m}{\mathop{\min}} { (f[j] + (cnt[i] - cnt[j])*i + (sum[i] - sum[j])) } f[i]=i2m<jimmin(f[j]+(cnt[i]cnt[j])i+(sum[i]sum[j])) , ⑧

⑧ 式就是我们最终的状态转移方程。

题解代码:
#include <bits/stdc++.h>
#define N 4000010
#define MAXINT 1e9

using namespace std;

int f[N], cnt[N], sum[N];

int main()
{
    int n, m, t, T = 0; // T表示最后一个同学到达车站的时间
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) 
    {
        cin >> t;
        T = max(T, t);	// 读入时要记录最后一个到达车站的时间 
        cnt[t] ++;      // 初始记录 t 时刻到达车站的人数 
        sum[t] += t; 	// 初始记录 t 的数值和 
    }
    
    for(int i = 1; i < T + m; i ++)  // i的循环区间为 [1, T + (m-1)] 
    {
        cnt[i] += cnt[i - 1]; 	// 计算前缀和:cnt[i] 表示截止到 i 时刻之前到达车站人数总和为 cnt[i] 
        sum[i] += sum[i - 1]; 	// 计算前缀和:sum[i] 表示截止到 i 时刻之前的所有点的数值和
    }

    for(int i = 1; i < T + m; i ++) // i的循环区间为 [1, T + (m-1)] 
    {
    	// 由于在主循环中,j <= i - m,当 i < m 时无法执行。故先计算 i∈(0, m] 时的 f[i] (当i∈(0, m]时,j=0,f[0]=cnt[0]=sum[0]=0) 
        f[i] = i * cnt[i] - sum[i]; 
        
        // 核心代码
        int k = max(0, i - 2 * m + 1); 	//  i-2m< j 且 j不能为负数 
        for(int j = k; j <= i - m; j ++)
            f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
    }

    int ans = MAXINT;
    for(int i = T; i < T + m; i ++)
        ans = min(ans, f[i]);

    cout << ans << endl;

    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值