• NOIP2018PJ T3 摆渡车


    题目链接

    题意:

    时间轴上分布着$n$位乘客,$i$号乘客的位置为$t_i$,用互相距离不小于$m$的车次将时间轴分为若干部分并管辖上一个区间,求最小费用和。每个车次的费用来自:管辖区间内乘客与区间左端点的距离之和。

     

    程序1(30pt):

    考虑DP。

    设在$i$时间的车次及之前所有费用之和的最小值为$f_i$,则

    ①若是第一趟车,则

    $$f_i=sumlimits_{1leq jleq i}(\, i-j\,)$$

    ②若非第一趟车,则

    $$f_i=mathop{min}limits_{1leq jleq i-m}{\, f_j+;sumlimits_{j+1leq kleq i}(\, i-k\,) ;}$$

    考虑答案在哪里,可以发现要接走最后一位乘客,设其等车时间为$T$,则最后一趟车发车时间必在$[\,T,T+m\,)$中。

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

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #define IL inline
    using namespace std;
    const int inf=1<<30;
    const int N=500;
    const int M=100;
    const int T=10000;
    
        int n,m;
        int maxt;
        int cnt[T+M+3];
        int f[T+M+3];
    
    int main(){
        scanf("%d%d",&n,&m);
        maxt=-inf;
        memset(cnt,0,sizeof cnt);
        for(int i=1;i<=n;i++){
            int x;
            scanf("%d",&x);
            cnt[x]++;
            maxt=max(maxt,x);
            
        }
        
        for(int i=1;i<maxt+m;i++){
            f[i]=0;
            for(int j=1;j<=i;j++)
                f[i]+=cnt[j]*(i-j);
            
            for(int j=1;j<=i-m;j++){
                int tot=1;
                for(int k=j+1;k<=i;k++)
                    tot+=cnt[k]*(i-k);
                
                f[i]=min(f[i],f[j]+tot);
                
            }
            
        }
        
        for(int i=1;i<m;i++)
            f[maxt]=min(f[maxt],f[maxt+i]);
        
        printf("%d",f[maxt]);
        
        return 0;
        
    }

    程序2(50pt):

    发现DP转移里的计算有很多重复部分,考虑使用前缀和优化

    对转移和式进行变形:

    $$sumlimits_{j+1leq kleq i}(\, i-k \,)=sumlimits_ki-sumlimits_kk$$

    设$cnt_i$为乘客计数的前缀和,$sum_i$为乘客时间的前缀和,则原式

    $$=(\, cnt_i-cnt_j \,) imes i;-;(\, sum_i-sum_j \,)$$

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

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #define IL inline
    using namespace std;
    const int inf=1<<30;
    const int N=500;
    const int M=100;
    const int T=4000000;
    
        int n,m;
        int maxt;
        int sum[T+M+3];
        int cnt[T+M+3];
        int f[T+M+3];
    
    int main(){
        scanf("%d%d",&n,&m);
        maxt=-inf;
        memset(cnt,0,sizeof cnt);
        memset(sum,0,sizeof sum);
        for(int i=1;i<=n;i++){
            int x;
            scanf("%d",&x);
            maxt=max(maxt,x);
            cnt[x]++;
            sum[x]+=x;
            
        }
        
        for(int i=1;i<maxt+m;i++){
            cnt[i]+=cnt[i-1];
            sum[i]+=sum[i-1];
            
        }
        
        for(int i=1;i<maxt+m;i++){
            f[i]=cnt[i]*i-sum[i];
            for(int j=1;j<=i-m;j++)
                f[i]=min(f[i]
                        ,f[j]+(cnt[i]-cnt[j])*i
                             -(sum[i]-sum[j]));
            
        }
        
        for(int i=1;i<m;i++)
            f[maxt]=min(f[maxt],f[maxt+i]);
        
        printf("%d",f[maxt]);
        
        return 0;
        
    }

    程序3(70pt):

    发现DP时每次都从$0$开始转移,显然是多余的,需要剪去无用转移。思考一下,发现如果我们切出了一个长度超过$2m$的段,我们可以至少再分一次,得到一个不劣的答案。

    于是乎

    $$f_i=mathop{min}limits_{i-2m< jleq i-m}{\, f_j+;(\, cnt_i-cnt_j \,) imes i;-;(\, sum_i-sum_j \,) ;}$$

    这次的时间复杂度是$O(tm)$的,可以得到$70$分。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #define IL inline
    using namespace std;
    const int inf=1<<30;
    const int N=500;
    const int M=100;
    const int T=4000000;
    
        int n,m;
        int maxt;
        int sum[T+M+3];
        int cnt[T+M+3];
        int f[T+M+3];
    
    int main(){
        scanf("%d%d",&n,&m);
        maxt=-inf;
        memset(cnt,0,sizeof cnt);
        memset(sum,0,sizeof sum);
        for(int i=1;i<=n;i++){
            int x;
            scanf("%d",&x);
            maxt=max(maxt,x);
            cnt[x]++;
            sum[x]+=x;
            
        }
        
        for(int i=1;i<maxt+m;i++){
            cnt[i]+=cnt[i-1];
            sum[i]+=sum[i-1];
            
        }
        
        for(int i=1;i<maxt+m;i++){
            f[i]=cnt[i]*i-sum[i];
            for(int j=max(i-2*m,1);j<=i-m;j++)
                f[i]=min(f[i]
                        ,f[j]+(cnt[i]-cnt[j])*i
                             -(sum[i]-sum[j]));
            
        }
        
        for(int i=1;i<m;i++)
            f[maxt]=min(f[maxt],f[maxt+i]);
        
        printf("%d",f[maxt]);
        
        return 0;
        
    }

    程序4(100pt):

    看起来转移已经很简洁了,但我们仍然没有得到满分。思考一下,发现我们对于每个时间点($0sim t$)都尝试进行转移,但真正有贡献的点(规模为$O(nm)$)实际上却少很多。

    考虑剪去无用状态发现一个长度不小于$m$的时间段不会产生任何贡献,即:若

    $$cnt_i\,=\,cnt_{i-m}$$

    则一定有

    $$f_i\,=\,f_{i-m}$$

    考虑时间复杂度,因为只在有贡献的地方转移,所以是$O(\,t\,+\,nm^2\,)$,可以得到满分。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #define IL inline
    using namespace std;
    const int inf=1<<30;
    const int N=500;
    const int M=100;
    const int T=4000000;
    
        int n,m;
        int maxt;
        int sum[T+M+3];
        int cnt[T+M+3];
        int f[T+M+3];
    
    int main(){
        scanf("%d%d",&n,&m);
        maxt=-inf;
        memset(cnt,0,sizeof cnt);
        memset(sum,0,sizeof sum);
        for(int i=1;i<=n;i++){
            int x;
            scanf("%d",&x);
            maxt=max(maxt,x);
            cnt[x]++;
            sum[x]+=x;
            
        }
        
        for(int i=1;i<maxt+m;i++){
            cnt[i]+=cnt[i-1];
            sum[i]+=sum[i-1];
            
        }
        
        for(int i=1;i<maxt+m;i++){
            if(i>=m)
            if(cnt[i]==cnt[i-m]){
                f[i]=f[i-m];
                continue;
                
            }
            
            f[i]=cnt[i]*i-sum[i];
            for(int j=max(i-2*m,1);j<=i-m;j++)
                f[i]=min(f[i]
                        ,f[j]+(cnt[i]-cnt[j])*i
                             -(sum[i]-sum[j]));
            
        }
        
        for(int i=1;i<m;i++)
            f[maxt]=min(f[maxt],f[maxt+i]);
        
        printf("%d",f[maxt]);
        
        return 0;
        
    }

    小结:

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

    后注:

    前文中的$mathop{min}limits_{iin S}$和$sumlimits_{iin S}$,都表示取范围内的乘客的所有等待时间。

  • 相关阅读:
    程序员创业必读的几本书
    新手上路—Java的"瑞士军刀"
    小团队互联网创业记
    Coder必须自废的两样神功
    码界新手,如何更高效的解决问题
    【转载】FckEditor 2.6.3 for Java 2.4 配置
    struts2上传多文件(b)
    Java基础-Java中的Calendar和Date类
    JAVA加减日期
    Java程序员应该了解的10个面向对象设计原则
  • 原文地址:https://www.cnblogs.com/Hansue/p/11031721.html
Copyright © 2020-2023  润新知