Processing math: 100%

NOIP2018PJ T3 摆渡车(2023.10第二版题解)

题目链接

 

题意:

  时间轴上分布着n位乘客(1n500),i号乘客的位置为ti0ti4×106),用互相距离不小于m的车次将时间轴分为若干部分,并管辖以自己为右端点的这个区间(除了第一趟车包括0,其他车次左开右闭),求最小费用和。每个车次的费用来自:管辖区间内所有乘客与该车次的距离之和。

 

程序1(30pts):

  考虑DP。

  设在i时刻等车的人共有ci位,再设在i时刻发车的费用以及之前所有车次费用之和的最小值为fi

  ①若这是第一趟车,则累加i时刻之前所有人等车的时间:

    fi=ij=0cj×(ij)

  ②若非第一趟车,则枚举上一趟车发车的时间j,再累加j+1i时刻的人等车的时间:

    fi=min0jim{fj+ik=j+1ck×(ik)}

  考虑最终答案怎么算。假设最后一名乘客的位置为T,则最后一趟车次时间必在[T,T+m)中,取这些时间的fi最小值即可。

  DP做完了。然而时间复杂度为O(T3),只有30分。

 

复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

const int N = 500;
const int M = 100;
const int T = 4e6;

    int n, m, t[N + 3];
    int c[T + M + 3];                        //设在i时刻等车的人共有c_i位
    int f[T + M + 3];                        //设在i时刻发车的费用以及之前所有车次费用之和的最小值为f_i
                                            //时间轴会用到T+m,对应的数组的范围也是T+M
int main(){
    scanf("%d%d", &n, &m);
    
    memset(c, 0, sizeof c);
    int maxt = 0;
    for(int i = 1; i <= n; i++){
        scanf("%d", &t[i]);
        c[t[i]]++;                            //统计c[]时刻等车的人的个数
        maxt = max(maxt, t[i]);
    }

//    memset(f, 0x3f, sizeof f);
    for(int i = 0; i < maxt + m; i++){        //最后一趟车最晚在maxt+m-1时发出
        int tmp = 0;
        for(int j = 0; j <= i; j++)
            tmp += c[j] * (i - j);
        f[i] = tmp;                            //初值:如果此时发第一趟车的费用
        
        for(int j = 0; j <= i - m; j++){    //如果上一趟车是j时刻发的
            tmp = 0;
            for(int k = j + 1; k <= i; k++)    //累加j+1~i时刻等车的人
                tmp += c[k] * (i - k);
            f[i] = min(f[i], f[j] + tmp);    //取最小值
        }
        
    }
    
    int ans = 1<<30;
    for(int i = maxt; i < maxt + m; i++)    //最后一趟车的时间可以是maxt~maxt+m-1
        ans = min(ans, f[i]);
    
    printf("%d", ans);

    return 0;

}
程序1(30pts)
复制代码

 

 

程序2(50pts):

  发现式子里有很多连续区间的运算,尝试变形一下式子以便使用前缀和优化

  ij=0cj×(ij)=iij=0cjij=0j×cj

  设ci的前缀和为Cii×ci的前缀和为Si,则原式

  =i×CiSi

  再看

  ik=j+1ck(ik)=iik=j+1ckik=j+1k×ck=i(CiCj)(SiSj)

  于是我们就可以O(1)转移了,整体时间复杂度下降到O(T2),可以得到50分。

 

 

复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

const int N = 500;
const int M = 100;
const int T = 4e6;

    int n, m, t[N + 3];
    int c[T + M + 3];                        //设在i时刻等车的人共有c_i位
    int C[T + M + 3], S[T + M + 3];            //c_i前缀和;i*c_i前缀和
    int f[T + M + 3];                        //设在i时刻发车的费用以及之前所有车次费用之和的最小值为f_i
                                            //时间轴会用到T+m,对应的数组的范围也是T+M
int main(){
    scanf("%d%d", &n, &m);
    
    memset(c, 0, sizeof c);
    int maxt = 0;
    for(int i = 1; i <= n; i++){
        scanf("%d", &t[i]);
        c[t[i]]++;                            //统计c[]时刻等车的人的个数
        maxt = max(maxt, t[i]);
    }
    
    C[0] = c[0];
    S[0] = 0;
    for(int i = 1; i <= maxt + m; i++){
        C[i] = C[i - 1] + c[i];
        S[i] = S[i - 1] + i * c[i];
    }

//    memset(f, 0x3f, sizeof f);
    for(int i = 0; i < maxt + m; i++){        //最后一趟车最晚在maxt+m-1时发出
        f[i] = i * C[i]-S[i];                //初值:如果此时发第一趟车的费用
        
        for(int j = 0; j <= i - m; j++)    //如果上一趟车是j时刻发的
            f[i] = min(f[i], f[j] + i * (C[i] - C[j]) - (S[i] - S[j]));
        
    }
    
    int ans = 1<<30;
    for(int i = maxt; i < maxt + m; i++)    //最后一趟车的时间可以是maxt~maxt+m-1
        ans = min(ans, f[i]);
    
    printf("%d", ans);

    return 0;

}
程序2(50pts)
复制代码

 

 

 

程序3(70pts):

  DP转移是枚举上一步,而这个DP里每个状态都从0状态开始枚举上一步,显然有很多冗余,需要剪去无用转移

  思考一下,发现我们不会切出一个长度2m的区间出来,因为这样的区间至少可以多切一次,得到两个长度m的区间,总费用可能减少或者不变。

  于是乎,若不是第一趟车,则

  fi=mini2m<jim{fj+i(CiCj)(SiSj)}

  现在我们的DP时间复杂度为O(T×m),理论得分70分。

  (洛谷用了比较好的评测机,我这发差不多1e9的运算也A了)

 

 

复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

const int N = 500;
const int M = 100;
const int T = 4e6;

    int n, m, t[N + 3];
    int c[T + M + 3];                        //设在i时刻等车的人共有c_i位
    int C[T + M + 3], S[T + M + 3];            //c_i前缀和;i*c_i前缀和
    int f[T + M + 3];                        //设在i时刻发车的费用以及之前所有车次费用之和的最小值为f_i
                                            //时间轴会用到T+m,对应的数组的范围也是T+M
int main(){
    scanf("%d%d", &n, &m);
    
    memset(c, 0, sizeof c);
    int maxt = 0;
    for(int i = 1; i <= n; i++){
        scanf("%d", &t[i]);
        c[t[i]]++;                            //统计c[]时刻等车的人的个数
        maxt = max(maxt, t[i]);
    }
    
    C[0] = c[0];
    S[0] = 0;
    for(int i = 1; i <= maxt + m; i++){
        C[i] = C[i - 1] + c[i];
        S[i] = S[i - 1] + i * c[i];
    }

//    memset(f, 0x3f, sizeof f);
    for(int i = 0; i < maxt + m; i++){        //最后一趟车最晚在maxt+m-1时发出
        f[i] = i * C[i]-S[i];                //初值:如果此时发第一趟车的费用
        
        for(int j = max(0, i - m * 2 + 1); j <= i - m; j++)    
                                            //如果上一趟车是j时刻发的
                                            //注意j不能为负数
            f[i] = min(f[i], f[j] + i * (C[i] - C[j]) - (S[i] - S[j]));
        
    }
    
    int ans = 1<<30;
    for(int i = maxt; i < maxt + m; i++)    //最后一趟车的时间可以是maxt~maxt+m-1
        ans = min(ans, f[i]);
    
    printf("%d", ans);

    return 0;

}
程序3(70pts)
复制代码

 

 

 

程序4(100pts):

  看起来转移已经很简洁了,但是算法的复杂度仍没有达到要求。

  思考一下,发现我们尝试对每个时间位置(0T+m,约4×106)都算了一遍转移,也就是尝试在这个位置发了一辆车(由f的定义),但实际上有效发车的时间位置一定不会超过n×m个(约5×104),我们可以剪去无用状态

  换句话说,对于在ti位置等车的人,接走他的车的时间位置一定在[ti,ti+m)范围内。

  再换句话说,如果时间轴上有一段长度不少于m的区间没有任何乘客,那么我们在这个区间右端发不发车费用都一定不会增加,可以直接继承状态,即

  fifim,Ci=Cim

  算算时间复杂度,只在有效发车时间位置做转移,所以总时间复杂度是O(T+n×m2),可以得到满分。

 

 

复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

const int N = 500;
const int M = 100;
const int T = 4e6;

    int n, m, t[N + 3];
    int c[T + M + 3];                        //设在i时刻等车的人共有c_i位
    int C[T + M + 3], S[T + M + 3];            //c_i前缀和;i*c_i前缀和
    int f[T + M + 3];                        //设在i时刻发车的费用以及之前所有车次费用之和的最小值为f_i
                                            //时间轴会用到T+m,对应的数组的范围也是T+M
int main(){
    scanf("%d%d", &n, &m);
    
    memset(c, 0, sizeof c);
    int maxt = 0;
    for(int i = 1; i <= n; i++){
        scanf("%d", &t[i]);
        c[t[i]]++;                            //统计c[]时刻等车的人的个数
        maxt = max(maxt, t[i]);
    }
    
    C[0] = c[0];
    S[0] = 0;
    for(int i = 1; i <= maxt + m; i++){
        C[i] = C[i - 1] + c[i];
        S[i] = S[i - 1] + i * c[i];
    }

//    memset(f, 0x3f, sizeof f);
    for(int i = 0; i < maxt + m; i++){        //最后一趟车最晚在maxt+m-1时发出
        if(i >= m && C[i] == C[i - m]){        //如果在i发车接不到任何人
            f[i] = f[i - m];                //直接继承状态,费用不会变化
            continue;
        }
        
        f[i] = i * C[i]-S[i];                //初值:如果此时发第一趟车的费用
        
        for(int j = max(0, i - m * 2 + 1); j <= i - m; j++)    
                                            //如果上一趟车是j时刻发的
                                            //注意j不能为负数
            f[i] = min(f[i], f[j] + i * (C[i] - C[j]) - (S[i] - S[j]));
        
    }
    
    int ans = 1<<30;
    for(int i = maxt; i < maxt + m; i++)    //最后一趟车的时间可以是maxt~maxt+m-1
        ans = min(ans, f[i]);
    
    printf("%d", ans);

    return 0;

}
程序4(100pts)
复制代码

 

 

 

小结:

  掌握DP的基础优化方法:前缀和优化剪去无用转移剪去无用状态,做到不写脑残的DP。

 

posted @   汉谡  阅读(58)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 2024年终总结:5000 Star,10w 下载量,这是我交出的开源答卷
· 一个适用于 .NET 的开源整洁架构项目模板
· AI Editor 真的被惊到了
· API 风格选对了,文档写好了,项目就成功了一半!
· 【开源】C#上位机必备高效数据转换助手
点击右上角即可分享
微信分享提示