题目描述

有 n 名同学要乘坐摆渡车从人大附中前往人民大学,第 i 位同学在第 ti 分钟去
等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发、 把车上的同学送到人民大学、再回到人大附中(去接其他同学),这样往返一趟总共花费 m 分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的等车时间之和最小为多少呢?
注意:摆渡车回到人大附中后可以即刻出发。

输入描述:

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

输出描述:

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

示例1

输入
5 1
3 4 4 3 5
输出
0
说明
同学 1 和同学 4 在第 3 分钟开始等车,等待 0 分钟,在第 3 分钟乘坐摆渡车 出发。摆渡车在第 4 分钟回到人大附中。
同学 2 和同学 3 在第 4 分钟开始等车,等待 0 分钟,在第 4 分钟乘坐摆渡车 出发。摆渡车在第 5 分钟回到人大附中。
同学 5 在第 5 分钟开始等车,等待 0 分钟,在第 5 分钟乘坐摆渡车出发。自此 所有同学都被送到人民大学。总等待时间为 0。

示例2

输入
5 5
11 13 1 5 5
输出
4
说明
同学 3 在第 1 分钟开始等车,等待 0 分钟,在第 1 分钟乘坐摆渡车出发。摆渡车在第 6 分钟回到人大附中。
同学 4 和同学 5 在第 5 分钟开始等车,等待 1 分钟,在第 6 分钟乘坐摆渡车出发。摆渡车在第 11 分钟回到人大附中。
同学 1 在第 11 分钟开始等车,等待 2 分钟;同学 2 在第 13 分钟开始等车,等待 0 分钟。他/她们在第 13 分钟乘坐摆渡车出发。自此所有同学都被送到人民大学。 总等待时间为 4。可以证明,没有总等待时间小于 4 的方案。

备注:

对于 10% 的数据,
对于 30% 的数据,
对于 50%的数据,
另有 20%的数据,
对于 100% 的数据,

解答

此题正解:DP +各种剪枝 or 优化

一、引理

对于每个乘客,能搭载ta的车的发车时间只有 m 种情况
设这个乘客开始等候的时间是  ,则对应的 m 种情况是(方括号 和 圆括号 表示左闭右开区间)。
证明
1、如果存在一种情况,其发车时间是 的,则由题意可知,发车时间可以提早若干轮(也就是减去若干个 m )到达这个区间,这样做不会影响发车时间那趟车,不会有后效性
2、如果 的话,那这个乘客根本就坐不上这趟车,所以不需要考虑。

二、基本思想

  • 首先,题目给定我们的这 n 个人开始等候的时间是乱的,所以我们要先按照开始等车的时间把这 n 个人排个序

  • 设  表示用摆渡车已经载了前 i 个人,且搭载了第 i 个人(不一定只搭载第 i 个人)的那趟摆渡车的发车时间是( )的最小等候时间和。(的意义与题意相同)

    这里要注意:同时还需要满足 
    因为如果,那这趟车就可以把第个人也搭上了,违反了  状态的定义

  • 对于每个 ,枚举上一趟摆渡车的出发时间。

等等!数据范围写着:

你跟我说枚举时间?你这最起码都 的时间复杂度了,怎么 AC ?

别着急啊,我还没说完呢。

  • 其实引理已经告诉我们,我们不需要把整个 枚举完

    由引理可得,对于前个乘客,每个乘客能搭载的摆渡车的发车时间只有 m 种情况,所以我们只需要枚举这 种情况即可。其他情况都是废的,不需要去考虑。

    这样做的枚举量为  ,相比之前直接枚举 的时间复杂度  来讲,已经是飞跃了。

  • 接着,假设前一趟摆渡车已经载了前  个人,那么我们要做的就只有两件事:

    1. 再枚举一个  ,得到  的最小值。
    2. 计算出第 个人到第  个人等候当前这趟摆渡车的等候时间和。
  • 在状态转移方程中的体现就是:
这当中,表示第  个乘客到第 i 个乘客等候发车时间为  的那趟摆渡车的时间和。

这里也有一个地方要注意:在枚举 k 和 时,要满足  

理由和上面一样,是因为这样做违反了  中状态的定义
其实上面的这个状态转移方程并不严谨,不过本人会慢慢把它修改得严谨起来(其实下文中的修改过程就是本人在考场上的思路)。
但这个方程很重要,以后的所有优化全都源自于这个方程,为了节省篇幅,本人不会重述这个方程,所以请大家记住这个转移方程。
先抛开正确性,我们可以来计算一下上面这个状态转移方程的时间复杂度。
  1. 首先, i 和 j 必须枚举,所以是  的时间复杂度。
  2. 其次, k 和 l 也是要枚举的,所以又是一个  。
  3. 最后,每次枚举  ,都要计算一次  函数,而这个  函数的时间复杂度是  的。
综上所述,这个状态转移方程的时间复杂度为  。
这时间复杂度……也太可观了吧
所以我们需要优化!优化!优化!

三、程序实现 or 剪枝

  • 我们可以发现,这 n 个人中有一些人开始等候的时间是相同的

    对于这些人,我们可以进行去重(开一个结构体,把相同时间的人压进一个结构体里面,结构体里则用一个变量表示这些人开始等候的时间,用另一个变量表示这些重复的人的人数)

    虽然这样做时间复杂度会多增加了  ,但这样可以在  时减少很多繁琐的特判

  • 我们来关注一下这个式子:

对于每个,当 k 每增加一时,  的值就只会减掉(  表示结构体中第 k 个元素的重复的人的数量)。

所以我们可以在枚举每个 i 和 j 时,就把 算出来(用一个变量  存起来),然后,每当 k 增加 1 ,  就减去 

状态转移方程就变为:

这样一抽出来,时间复杂度就变成了 

只保留最高次项后,时间复杂度就降为了 !!!又是一个飞跃!!!

注意!接下来的内容全都是难点,请大家做好心理准备!

  • 其实大家有没有想过,枚举  这个操作显得有些多余,可不可以省去呢?(毕竟只是求一个最小值而已,我求完一次就把这个最小值存起来不就行了吗?)

    没错,上面的想法是正确的!

则之前的状态转移方程可以简化为:

可以在求每个的时候顺带维护

因为这里只枚举了 ,所以 的时间复杂度是  !!!

这个时间复杂度足以通过本题了!!!

但是,这样做真的是对的吗?

假设有这样一个例子:


(数轴上的两个点表示的是乘客开始等候的时间,圈3和圈4表示两个乘客在(结构体)数组中的编号)

这时,我们直接用  来为做  就不行了。

假设我们要求  的值(也就是说最后一趟车的发车时间和第4个乘客开始等候的时间相同的情况),当我们枚举到 的情况时,我们只能取 的情况。(如果 的话,前一趟车的时间就有可能和当前这趟车的时间重叠了,违反了  中状态的定义!)

也就是说,不能直接用  数组来  !(因为  包含了 到 的最小值,直接用会出错!

那应该怎么办?(我在考场上最绝望的时刻就是思考这个问题的过程)

后来一想:可不可以直接特判呢?

事实证明,可以!

首先,对于每个,我们都为其开一个变量  作为一个“防护墙”

对于每个 k ,如果  ,那么  就不适用于  这个状态(因为对于这个 k ,有些 加上 m 以后会和  重叠

对于这些 k ,我们就重新帮它枚举一个 (满足 不然超过了会重叠)去  。

由此也可以得出,如果,就可以直接退出当前循环了。(因为你没有 再去枚举了)

这样一来,状态转移方程就开始有些恶心了:

这样优化后的时间复杂度是多少呢?

首先,无论如何, 是一定要枚举的,所以时间复杂度至少会有  。

然后,我们可以把两个方程分开考虑:

  1. 第一个方程枚举了  ,时间复杂度为 

  2. 因为 k 和  总共只会计算 m 次(因为只有当  时才会进入第二种情况。而  每次至少增长 1,最慢也就是 而已)
    所以第二条方程的时间复杂度为 

加起来就是 ,因为 ,所以可以简化成 这个时间复杂度可以通过本题!

至此,我们终于得出了此题的可行解法!

四、考场代码

我的代码在结构体边界上有一些预处理,这个重在意会即可相信大家应该看得懂

#include<stdio.h>
#include<algorithm>
using namespace std;

const int maxn=502,maxm=102;
const int INF=0x7fffffff;

int f[maxn][maxm];
int Min[maxn];

int a[maxn];

struct Node
{
    int pos,num;
}Mem[maxn];
int sz;

int col(int l,int r,int pos)
{
    int res=0;

    for(int i=l;i<=r;i++)
        res+=(pos-Mem[i].pos)*Mem[i].num;

    return res;
}

int main(int argc, char const *argv[])
{
    int n,m;
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);

    sort(a+1,a+n+1);

    Mem[0].pos=-m*2-2;
    a[0]=-1;

    for(int i=1;i<=n;i++)
    {
        if( a[i]^a[i-1] )
            Mem[++sz].pos=a[i];

        Mem[sz].num++;
    }

    Mem[sz+1].pos=Mem[sz].pos+m+2;

    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            f[i][j]=INF;
        Min[i]=INF;
    }

    Min[0]=0;

    for(int i=1;i<=sz;i++)
        for(int j=0;j<min(m,Mem[i+1].pos-Mem[i].pos);j++)
        {
            int pos=Mem[i].pos+j,lpos=pos-m;

            int val=col(1,i,pos);
            f[i][j]=val;

            for(int k=0; k<i and Mem[k].pos<=lpos ;k++)
            {
                val-=(pos-Mem[k].pos)*Mem[k].num;

                //如果Mem[k+1].pos-1<=lpos,那么Min[k]照样能用(因为Mem[k+1].pos-1也是l的边界之一,只要l小于两个边界中较小的那个值,就可以直接用Min[k])
                if( min( Mem[k].pos+m-1,Mem[k+1].pos-1 )<=lpos ) f[i][j]=min( f[i][j],Min[k]+val );
                else
                {
                    for(int kk=0; Mem[k].pos+kk<Mem[k+1].pos and Mem[k].pos+kk<=lpos and kk<m;kk++)
                        f[i][j]=min( f[i][j],f[k][kk]+val );
                }
            }

            Min[i]=min( Min[i],f[i][j] );
        }

    printf("%d",Min[sz]);

    return 0;
}


来源:Avalon Evergarden