当年明|

tkdqmx

园龄:6个月粉丝:2关注:1

9.2 上午 becoder 模拟赛总结 & 题解

T1 加法

最开始看了好久没想出来,先做 T2 去了,然后回来想了一会儿发现自己可能智商有点问题。

看到求最小值最大,第一反应肯定是二分,那我们怎么应该怎么 check 呢?

考虑顺次枚举序列 A 中的每一个数,然后如果这个数没有达到 mid 的要求,我们肯定是要添加区间的。

那么我们怎么添加区间呢?因为我们已经枚举到了 Ai,所以之前的数肯定都已经满足了条件,我们就只需要考虑 Ai 及后面的数了。

那么对于所有左端点小于等于 i 的区间,我们就可以不考虑左端点了,要加入的就是这些区间中右端点最大的区间了。

这样的贪心是满足局部最优 = 全局最优的,我们现在只需要考虑怎么完成这个过程就可以了。

假设现在枚举到了 i,我们需要把左端点小于等于 i 的区间中的右端点最大值求出来,这一过程可以用优先队列来完成。

那我们可以怎么去给后面的 A 数组去增加值呢?最好想的当然是用树状数组或者线段树去进行维护。

但其实是可以不用的,如果这个区间被加入了答案,我们可以维护一个 sum 值,表示当前总共被增加的所有值的和。

对于被计入答案的区间的值如何在适当时减去,我们可以在 i 这个点的答案计算完成后,枚举所有右端点在 i 的区间,如果这个区间被记入了答案,我们减去它对答案的贡献就可以了。

至于如何判断是否被计入答案,用最简单的 bool 数组维护就行了。

时间复杂度 O(nlog2n),代码贴在下面,具体细节请看代码(100pts):

#define N 200005
#define LL long long
bool vis[N];
LL t,n,m,k,a,v[N];//v在这里是表示的序列A
struct Node{int l,r,num;}s1[N],s2[N];
priority_queue<pair<int,int> > q;
bool check(LL mid,int in=0,int all=0){
    while(!q.empty())  q.pop();
    for(int i=1;i<=m;i++)  vis[i]=0;
    for(int i=1,l=1,r=1;i<=n;i++){
        while(l<=m&&s1[l].l<=i)  q.emplace(s1[l].r,s1[l].num),l++;
        while(r<=m&&s2[r].r<i)   in-=vis[s2[r++].num];
        while((mid-v[i]+a-1)/a-in>0){
            if(q.empty()||q.top().first<i||++all>k)  return 0;
            in++,vis[q.top().second]=1,q.pop();
        }
    }
    return 1;
}//s1表示按左端点从小到大排列后的区间,s2表示按右端点从小到大排列后的区间

T2 期末考试

考虑枚举是在哪一天结束的所有课程,那我们需要求的就是对于在指定的一天结束课程,会产生多少不愉快度。

首先我们来计算学生的不愉快度,这个过程我们可以通过预处理前缀和来求出。

接下来我们考虑老师的不愉快度,分两种情况进行讨论:

第一种,BA 的情况,明显我们只需要进行操作 2,所以只需要求出所有课程需要提前的总天数就可以了,这个过程也可以用前缀和完成。

第二种,B>A 的情况,那我们就需要尽量进行操作 1,比第一种要多求的就是所有课程可以延后的总天数,这个过程仍然可以使用前缀和来完成。

值得注意的是对于测试点 13-14,因为 C 太大了,所以处理过程中会炸 long long,要进行特判。

具体实现看代码,时间复杂度 O(nlogn),瓶颈在排序(100pts):

#define N 100005
#define LL long long
LL A,B,C,n,m,ans=LLONG_MAX,smin=ans,b[N],sum[N],res[N];
int main(){
	scanf("%lld%lld%lld%lld%lld",&A,&B,&C,&n,&m);
    for(LL i=1,x;i<=n;i++)  scanf("%lld",&x),res[x+1]++,smin=min(smin,x);
    for(int i=1;i<=m;i++)   scanf("%lld",b+i);
    sort(b+1,b+m+1);
    for(int i=1;i<=m;i++)   sum[i]=sum[i-1]+b[i];
    if(int l=1,r=1;C==10000000000000000ll){
        while(l<=m&&b[l]<smin)   l++;
        while(r<=m&&b[r]<=smin)  r++;
        LL lc=(l-1ll)*smin-sum[l-1],rc=sum[m]-sum[r-1]-(m-r+1)*smin;
        if(B<=A) return printf("%lld\n",l,r,rc*B);
        return printf("%lld\n",min(lc,rc)*A+max(rc-lc,0ll)*B),0;
    }
    for(int i=1;i<=N-5;i++)  res[i]+=res[i-1];
    for(int i=1;i<=N-5;i++)  res[i]+=res[i-1];
    for(int i=1,l=1,r=1;i<=N-5;i++){
        while(l<=m&&b[l]<i)   l++;
        while(r<=m&&b[r]<=i)  r++;
        LL lc=(l-1ll)*i-sum[l-1],rc=sum[m]-sum[r-1]-(m-r+1)*i;
        if(B<=A)  ans=min(ans,rc*B+res[i]*C);
        else  ans=min(ans,min(lc,rc)*A+max(rc-lc,0ll)*B+res[i]*C);
    }
    printf("%lld\n",ans);
}

T3 观光公交

先来说下 48pts 的 O(n2k) 做法。

首先,我们来想一下如果对于一个区间,我们用了一个氮气加速器后会发生什么情况:

大家都知道的是现在坐在车上的所有人都会少等 1 个单位的时间,那会不会对后面等车的人产生什么影响呢?

可以发现,只要是车到达的时候所有人都已经在车站在等了,那么就会让后面等车的人少等 1 个单位的时间。

所以对于 k 个氮气加速器,我们可以每个加速器 O(n2) 去寻找到能减少最多等待时间的区间,然后给这个区间的列车行驶时间减 1 就行了。

代码如下(48pts,原版数据 60pts):

#define N 100005
int n,m,k,ans,d[N],t[N],x[N],y[N],ari[N],lat[N],off[N];
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<n;i++)  scanf("%d",d+i);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",t+i,x+i,y+i);
        lat[x[i]]=max(lat[x[i]],t[i]),off[y[i]]++;
    }
    for(int i=1,now=0;i<=n;i++)  ari[i]=now,now=max(now,lat[i])+d[i];
    for(int i=1;i<=k;i++){
        int smax=0,num=0;
        for(int j=2;j<=n;j++){
            if(!d[j-1])  continue;int sum=0;
            for(int k=j;k<=n;k++){
                sum+=off[k];
                if(ari[k]<=lat[k])  break;
            }
            if(sum>smax)  smax=sum,num=j;
        }
        d[num-1]--;
        for(int j=num;j<=n;j++)  if(--ari[j]<lat[j])  break;
    }
    for(int i=1;i<=m;i++)  ans+=ari[y[i]]-t[i];
    printf("%d\n",ans);
}

接下来我们来看 100pts 的 O(nlogn) 做法。

现设最终答案为 ans,在初始时我们将它值赋值成 Ti,相当于提前把要减的值减了。

接下来我们设 lati 表示 i 车站最晚来的人的时间,si 表示车比 lati 晚到的时间,offi 表示第 i 个车站下的人数。

所以有 si=max(si1+lati1+Di1lati,0)ans+=offi(si1+lati1+Di1)

考虑一个区间 [l,r],如果 (l,r] 里面的 si(l<ir) 的值均大于 0,那么我们就可以通过使用 min(dl,si,k) 个加速器来让整个区间内的到达时间减少等量的值。

这样答案就会减少这个值乘上区间内成为乘客终点的数量和(对于每个车站,所有的乘客均单独计算)。

我们把这样的区间都称作有意义的区间,即能对答案产生贡献的区间。

初始时,我们可以把原序列拆分成至多 n 个极大的有意义区间,然后把它们放进优先队列里面,并以区间内成为乘客终点的数量和作为关键字做一个大顶堆。

这样我们取出的一定是单个加速器能产生最大贡献的区间。

对于每次处理完一个区间后,如果 k0一定会产生 dl<0si<0
的情况,我们就以这里为断点,cut 成两个区间重新丢回优先队列里。

找断点的这一步可以通过线段树来维护。

这样我们这一道题基本上就处理完了,剩下的就只是代码的实现了,虽然不是很简单就是了,代码贴在下面(100pts):

#define N 100005
#define LL long long
LL ans,s[N];
int n,m,k,lst=1,d[N],ari[N],lat[N],off[N];
struct Node{
    int l,r;
    friend bool operator<(Node a,Node b){return off[a.r]-off[a.l]<off[b.r]-off[b.l];}
};
priority_queue<Node> q;
struct Segment_tree{
    void build(int l,int r,int p)//对s[i]进行区间最小值build
    void update(int sl,int sr,int x,int l,int r,int p)//让[sl,sr]区间中的s[i]值减小x 
    pair<int,int> query(int sl,int sr,int l,int r,int p)//返回first为最小值,second为在s数组中的下标
}SGT;//模板省略
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<n;i++)  scanf("%d",d+i);
    for(int i=1,t,x,y;i<=m;i++){
        scanf("%d%d%d",&t,&x,&y);
        lat[x]=max(lat[x],t),off[y]++,ans-=t;
    }
    for(int i=2;i<=n;i++)  off[i]+=off[i-1];
    for(int i=2;i<=n;i++){
        s[i]=max(0ll,s[i-1]+lat[i-1]+d[i-1]-lat[i]);
        if(!s[i])  q.push({lst,i}),lst=i;
        ans+=(LL)(off[i]-off[i-1])*(s[i-1]+lat[i-1]+d[i-1]);
    }
    if(lst<n)  q.push({lst,n});
    SGT.build(2,n,1);
    while(k&&!q.empty()){
        Node now=q.top();q.pop();int num=0,sum=0;
        if(now.l+1<now.r){
            auto tmp=SGT.query(now.l+1,now.r-1,2,n,1);
            sum=tmp.first,num=tmp.second;
        }
        if(!num)  sum=0x3f3f3f3f;
        int use=min({k,d[now.l],sum}),cut=(use==d[now.l]?now.l+1:num);
        ans-=(LL)use*(off[now.r]-off[now.l]),k-=use;
        if(!k)  break;
        if(use&&now.l+1<now.r)  SGT.update(now.l+1,now.r-1,use,2,n,1);
        d[now.l]-=use;
        if(now.l<cut)  q.push({now.l,cut});
        if(cut<now.r)  q.push({cut,now.r});
    }
    printf("%lld\n",ans);
}

T4 侦察守卫

最开始我本来定义的状态是用 dpi,j,0/1 表示对于顶点 i,在 j 层内是/否有侦察点时的最小值。

可惜这个状态对于祖先侦察到儿孙的情况不好转移,所以想了一下过后拆成了两个状态 fi,jgi,j

其中,fi,j 表示 i 节点的子树全被覆盖到,还能向上覆盖 j 层的最小价值,而 gi,j 则表示 i 子树的 j 层以下全部被覆盖到,j 层以内啥都不知道的最小价值。

让结点 1 作为根节点,易得我们要求取的答案就是 f1,0/g1,0,接下来我们考虑如何转移,记当前枚举到的节点为 x,子节点为 y

fx,i 分两种情况转移,一是用前面的子节点去覆盖上面的节点,二是用当前的子节点去覆盖上面的节点,两种情况取 min 就行了:

fx,i=min(fx,i+gy,i,fy,i+1+gx,i+1),由定义我们还可以知道:fx,i=min(fx,i,fx,i+1)

gx,i 的转移就简单了:gx,i=gx,i+gy,i1,由定义我们还可以知道:gx,i=min(gx,i,gx,i1),接下来我们要考虑的就是初始状态了:

因为 fx,i 会从 fx,i+1 转移,所以 fx,d+1=inf,对于只有一个点且这个点还需要被监视的情况 fx,0=gx,0=wx

还有就是显而易见的:fx,i=wx(1id),状态转移就是这样,代码贴在下面(100pts):

void dfs(int x,int fa){
    f[x][d+1]=0x3f3f3f3f;
    if(vis[x])  f[x][0]=g[x][0]=w[x];//vis是否需要被监视
    for(int i=1;i<=d;i++)  f[x][i]=w[x]
    for(auto y:v[x]){//v数组存图
        if(y==fa)  continue;
        dfs(y,x);
        for(int i=d;i>=0;i--)  f[x][i]=min(f[x][i]+g[y][i],f[y][i+1]+g[x][i+1]);
        for(int i=d;i>=0;i--)  f[x][i]=min(f[x][i],f[x][i+1]);
        g[x][0]=f[x][0];//易得,用于满足g[fa][1]的转移
        for(int i=1;i<=d;i++)  g[x][i]+=g[y][i-1];
        for(int i=1;i<=d;i++)  g[x][i]=min(g[x][i],g[x][i-1]);
    }
}

本文作者:tkdqmx

本文链接:https://www.cnblogs.com/tkdqmx/p/18395276

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   tkdqmx  阅读(99)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起