NOIP2018普及组复赛——T3摆渡车

题目描述

n n n名同学要乘坐摆渡车从人大附中前往人民大学,第 i i i 位同学在第 t i t_i ti​ 分钟去 等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发、 把车上的同学送到人民大学、再回到人大附中(去接其他同学),这样往返一趟总共花费 m m m分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。

凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的等车时间之和最小为多少呢?

注意:摆渡车回到人大附中后可以即刻出发。

输入格式
第一行包含两个正整数 n , m n,m n,m,以一个空格分开,分别代表等车人数和摆渡车往返 一趟的时间。
第二行包含 n n n 个正整数,相邻两数之间以一个空格分隔,第 i i i 个非负整数 t i t_i ti 代 表第 i i i 个同学到达车站的时刻。

输出格式
输出一行,一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。

输入样例

5 5
11 13 1 5 5

输出样例

4

算法思想

如果把时间看作一个数轴,那么所有同学到达车站的时刻就是数轴上的点,如下图所示:
在这里插入图片描述
安排车辆的工作就是把数轴划分成若干个左开右闭的子区间,如下图所示:
在这里插入图片描述

例如测试样例中摆渡车安排在时刻1、6、13时,所有同学的等待时间之和最少,为4。一共安排了3次乘车,将数轴划分3段左开右闭的区间,分别是: ( − ∞ , 1 ] , ( 1 , 6 ] , ( 6 , 13 ] (-\infty,1],(1,6],(6,13] (,1],(1,6],(6,13],每段长度都 ≥ m \ge m m,上图中m=5
这样,等待时间之和就转换成了所有点到各自所属区间右边界的距离之和

状态表示

f [ i ] f[i] f[i]表示数轴上对于前 i i i个点,且最后一段的右边界为 i i i,位于 ( 0 , i ] (0,i] (0,i]所有点到各自所属区间右边界的距离之和的最小值

状态转移

设最后一段是 ( j , i ] (j,i] (j,i],而每段长度 i − j ≥ m i-j\ge m ijm,则有 j ≤ i − m j\le i-m jim。如果第 k k k位同学到达时间 t [ k ] t[k] t[k]属于区间 ( j , i ] (j,i] (j,i],即 j < t [ k ] ≤ i j<t[k]\le i j<t[k]i,他到右边界 i i i的距离是 i − t [ k ] i-t[k] it[k]。那么累加所有属于区间 ( j , i ] (j,i] (j,i]的点到 i i i点距离之和,然后加上上一阶段的状态 f [ j ] f[j] f[j],就得到了状态转移方程:
f [ i ] = min ⁡ j ≤ i − m { f [ j ] + ∑ j < t [ k ] ≤ i ( i − t [ k ] ) } f[i]= \min\limits_{j\le i-m}\{f[j]+\sum\limits_{j<t[k]≤i} (i-t[k]) \} f[i]=jimmin{ f[j]+j<t[k]i(it[k])}

进一步思考, ∑ j < t [ k ] ≤ i ( i − t [ k ] ) = ( ∑ i ) − ( ∑ t [ k ] ) \sum\limits_{j<t[k]≤i} (i-t[k])=(\sum i) - (\sum t[k]) j<t[k]i(it[k])=(i)(t[k]),其中 ∑ i \sum i i ∑ t [ k ] \sum t[k] t[k]都可以通过前缀和计算出来。

∑ i = ( c n t [ i ] − c n t [ j ] ) × i \sum i = (cnt[i]-cnt[j]) \times i i=(cnt[i]cnt[j])×i c n t [ i ] cnt[i] cnt[i]表示区间 ( − ∞ , i ] (-\infty,i] (,i]中同学的个数。

∑ t [ k ] = s u m [ i ] − s u m [ j ] \sum t[k] = sum[i]-sum[j] t[k]=sum[i]sum[j] s u m [ i ] sum[i] sum[i]表示区间 ( − ∞ , i ] (-\infty,i] (,i]中所有同学到达的时间之和。

那么状态转移方程可以写成:
f [ i ] = min ⁡ j ≤ i − m { f [ j ] + ( c n t [ i ] − c n t [ j ] ) × i − ( s u m [ i ] − s u m [ j ] ) } f[i]= \min\limits_{j\le i-m}\{ f[j]+(cnt[i] - cnt[j])\times i - (sum[i]-sum[j])\} f[i]=jimmin{ f[j]+(cnt[i]cnt[j])×i(sum[i]sum[j])}

时间复杂度

f [ i ] f[i] f[i]表示的是在时间轴上对于前 i i i个点的最优解, i ≤ 4 × 1 0 6 i\le4\times10^6 i4×106,所以时间复杂度为 O ( 1 0 12 ) O(10^{12}) O(1012)

代码实现

#include <iostream>

using namespace std;

const int N = 4000110;

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

int main()
{
    
    
    int n, m, t, T; //T表示最后一个同学到达车站的时间
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) 
    {
    
    
        cin >> t;
        T = max(T, t);
        cnt[t] ++, sum[t] += t; 
    }
    
    //注意,这里应计算到最后一同学可能等到的时间
    for(int i = 1; i < T + m; i ++)
    {
    
    
        cnt[i] += cnt[i - 1]; //求人数的前缀和
        sum[i] += sum[i - 1]; //求时间的前缀和
    }
    
    for(int i = 1; i < T + m; i ++)
    {
    
    
        f[i] = i * cnt[i] - sum[i]; //特殊处理i<m的情况
        for(int j = 0; j <= i - m; j ++)
            f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
    }
    
    int ans = 1e9;
    for(int i = T; i < T + m; i ++)
        ans = min(ans, f[i]);
        
    cout << ans << endl;
    
    return 0;
}

算法优化

DP优化常用的两种方法,剪去无用转移和无用状态。

  • 剪去无用转移
    考虑区间 ( j , i ] (j, i] (j,i],在计算f[i]时,j0开始枚举。其实可以缩小j的范围,从i - 2 * m开始枚举。因为当区间的长度 ≥ 2 m \ge2m 2m时,可以安排摆渡车跑一个来回,等待时间不会变长。通过此性质,可剪去大量无用转移,状态转移方程更新为:
    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]= \min\limits_{i-2m < j\le i-m}\{f[j] + (cnt[i] - cnt[j])\times i - (sum[i]-sum[j])\} f[i]=i2m<jimmin{ f[j]+(cnt[i]cnt[j])×i(sum[i]sum[j])}
  • 剪去无用状态
    对于某个阶段的状态f[i],如果在区间 ( i − m , i ] (i-m, i] (im,i]中没有任何点,那么状态f[i]=f[i-m]。因为区间中没有任何学生,可以不安排摆渡车,不会增加学生的等待时间。

优化后时间复杂度 O ( n m 2 + t ) O(nm^2 + t) O(nm2+t)

优化代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 4000110;

int ans = 1e9;
int cnt[N], sum[N], f[N];

int main() 
{
    
    
    //T表示最后一个同学到达车站的时间,初始化为0
    int n, m, t, T = 0;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) 
    {
    
    
        cin >> t;
        T = max(T, t);
        cnt[t]++, sum[t] += t;
    }
    
    //注意,这里应计算到最后一同学可能等到的时间 T + m - 1
    for (int i = 1; i < T + m; i++) 
    {
    
     
        cnt[i] += cnt[i - 1]; //求人数的前缀和
        sum[i] += sum[i - 1]; //求时间的前缀和
    }
    
    for (int i = 1; i < T + m; i++)
    {
    
    
        if(i >= m && cnt[i] == cnt[i - m])
        {
    
    
            f[i] = f[i - m]; 
            continue;
        }
        
        f[i] = i * cnt[i] - sum[i]; //特殊处理i<m的情况
        
        for(int j = max(0, i - 2 * m + 1); j <= i - m; j ++)
            f[i] = min(f[i], f[j] + (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]));
    }
    
    //注意,最后一名同学上车的时刻在[T,T + m),求 其中最小值。
    for(int i = T; i < T + m; i ++)
         ans = min(ans, f[i]);
        
    cout << ans << endl;
    
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qiaoxinwei/article/details/108460512