首页 > 其他分享 >NOIP2018PJ T3 摆渡车(2023.10第二版题解)

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

时间:2023-10-18 19:56:57浏览次数:41  
标签:const limits int 题解 2023.10 T3 times 发车 include

题目链接

 

题意:

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

 

程序1(30pts):

  考虑DP。

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

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

    $$f_i=\sum\limits_{j=0}^{i} c_j\times(i-j)$$

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

    $$f_i=\mathop{min}\limits_{0\le j\le i-m}\{f_j+\sum\limits_{k=j+1}^i c_k\times(i-k)\}$$

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

  DP做完了。然而时间复杂度为$O(T^3)$,只有$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):

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

  $$\sum\limits_{j=0}^{i} c_j\times(i-j)=i\sum\limits_{j=0}^i c_j - \sum\limits_{j=0}^i j\times c_j$$

  设$c_i$的前缀和为$C_i$,$i\times c_i$的前缀和为$S_i$,则原式

  $$=i\times C_i - S_i$$

  再看

  $$\begin{matrix}\sum\limits_{k=j+1}^i c_k(i-k)&=i\sum\limits_{k=j+1}^i c_k-\sum\limits_{k=j+1}^i k\times c_k\\&=i(C_i-C_j)-(S_i-S_j)\end{matrix}$$

  于是我们就可以$O(1)$转移了,整体时间复杂度下降到$O(T^2)$,可以得到$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$状态开始枚举上一步,显然有很多冗余,需要剪去无用转移

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

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

  $$f_i=\mathop{min}\limits_{i-2m<j\le i-m}\{f_j+i(C_i-C_j)-(S_i-S_j)\}$$

  现在我们的DP时间复杂度为$O(T\times 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):

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

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

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

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

  $$f_i\leftarrow f_{i-m},\qquad C_i=C_{i-m}$$

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

 

 

#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。

 

标签:const,limits,int,题解,2023.10,T3,times,发车,include
From: https://www.cnblogs.com/Hansue/p/17770061.html

相关文章

  • 胜利一中 2023 秋提高级友好学校赛前联测 2 T3
    乱杀题目描述乐孤星和WA90准备联合参加下一次的NOB(NationalOlympiadinBadminton)。他们想要在一场比赛中击回对手打出的所有球从而赢得比赛,因为WA90非常强,所以可以预先知道对手打出的每一个球的位置,他们想要计算一下打败对手需要多认真。形式化的,我们将羽毛球场比作......
  • 2023.10.17 测试总结
    预计得分:145实际得分:148T1考场上没有想出来,打了一个高精度暴力。问题大概在:1.对哈希算法不熟悉。2.数学上对对数的计算不熟悉。耗时:1hT2暴力。没有挂分,正解属于是难以想到的。耗时:1hT3极为接近正解,但是挂分过多。问题有:1.没有检查出来数组开小了。2.......
  • T3-lichee编译环境设置
    sudoaptinstallmakegccbcu-boot-toolsbzip2fakerootgawkmkbootimgbusybox 问题1:usr/bin/ld:scripts/dtc/dtc-parser.tab.o:(.bss+0x50):multipledefinitionof`yylloc’;scripts/dtc/dtc-lexer.lex.o:(.bss+0x0):firstdefinedhere原因:gcc版本过高解决方......
  • 杂题乱做 - 2023.10
    目录写在前面CF1872GThe2021ICPCAsiaJinanRegionalContest-JDeterminant写在最后写在前面如题,杂题乱做。有的时候闲得无聊就写上几题。唉,菜。加训!CF1872G2000https://codeforces.com/contest/1872/problem/G记非1位置坐标依次为:\(p_1,p_2,\dots,p_k\),显然......
  • 2023.10.13NOIPSIM3总结
    T1卡牌赛时打了一个\(\Omicron(nm)\)的暴力,拿到30分。我们发现第\(i\)张牌对BOSS造成的伤害为$att_i*\lceil\frac{hp_i}{Att}\rceil$,那么考虑以卡牌血量值域为下标开一个桶,储存相同血量的卡牌的\(\sumatt\)。对于每一级BOSS的攻击力,我们都可以在桶上根据\(\lceil......
  • AT_arc100_b 题解
    题意这道题是让我们把一段区间分成四个不为空的连续子序列,并算出每个区间的和,最后用四个和的最大值减去最小值,算出最终答案。分析大家首先想到的肯定是暴力法用三个循环枚举四个区间,对于每一个区间,在单独算和,这样的时间复杂度$O(n^4)$,肯定会超时。现在我们进行优化:最后求和的......
  • P4899 [IOI2018] werewolf 狼人 题解
    P4899[IOI2018]werewolf狼人题解题目描述省流:\(n\)个点,\(m\)条边,\(q\)次询问,对于每一次询问,给定一个起点\(S\)和终点\(T\),能否找到一条路径,前半程不能走\(0\thicksimL-1\)这些点,后半程不能走\(R+1\thicksimN-1\)这些点。中途必须有一个点在\(L\thicksimR\)之......
  • AT_abc134_d Preparing Boxes题解
    简述题意这什么破翻译,看了AtCoder的英文才看懂。给定一个长度为\(n\)序列\(a\),要求构造一个数列\(b\),使得对于任意\(i\),满足:\(1\lei\len\)将\(b\)序列下标为\(i\)的倍数的值相加使得这个总和模2等于\(a_i\)。求序列\(b\)中值为1的个数与值为1......
  • P9700题解
    思路看数据范围,发现范围很小,直接用搜索。搜索时枚举每个点,如果有棋子就枚举方向,如果这个方向合法,则将剩余棋子数减一,继续搜索。搜索时参数只需要传当前棋子数就行了。有以下几点需要注意多组数据每次需要初始化。判断是否合法时要注意。每次记得回溯棋子。ACCODE......
  • SP26719题解
    考虑动态规划。思路设\(dp_{i,j}\)为\((1,1)\)到\((i,j)\)的方案数,而如果要到这个点,肯定是从左边和上边来。所以递推公式为:\(dp_{i,j}=dp_{i,j-1}+dp_{i-1,j}\)。预处理:将横或纵坐标为1的点赋值为1,因为到达这些点的只有一种方法。注意:每次需要清零数组。ACC......