2021牛客暑期多校训练营7

比赛链接:https://ac.nowcoder.com/acm/contest/11258

F,H,I,12。讨论了半天K,还是不会做……

 

B

分析:

区间操作题,要想办法在\( logn \)级别的时间内做完每个操作。

如果把一个最长不减子序列分成两段,仍然可以算贡献,而且两段内部的贡献不受影响,只要考虑断点的改变;基于这种可拆分的性质,我们想到用线段树。

线段树维护的东西是要用来计算两段“缝合”起来时改变的贡献的。可以看到这和前一段的最大值、最大值所在位置的\( b \)值有关。所以线段树要维护区间最大值和区间最大值所在位置的\( b \)值。

接着,考虑查询。为了保证复杂度,需要在每个区间存下整个区间的答案;但是答案不仅和区间内部的值有关,还要考虑前一个接过来的元素的大小。换句话说,即使查询到了这个区间,取的也不一定是整个区间的答案,而是根据前面的数,从某个位置开始的子序列的答案。

为了化解这一点,同时又快速查询,我们可以只记录这种情况:左区间取了最大值后右区间的答案。由于已经记录了区间最大值和对应的\( b \),所以可以知道右区间的答案应该从哪个值开始。把右区间这个答案记成\( cal[u] \)。

这样,每次线段树上\( pushup \)的时候就需要对右区间重新进行查询(因为起始值改变了);进行修改操作的复杂度就成了\( O(log^2n) \);

而查询操作由于只有右区间能直接利用,所以左区间会一直查到单个位置,故查询复杂度(大约……)也是\( O(log^2n) \)。

查询时务必注意前一段对后一段的影响!为此还要用结构体返回查询值。

代码如下:

#include<iostream>
#define ls (u<<1)
#define rs ((u<<1)|1)
#define mid ((l+r)>>1)
using namespace std;
int const N=2e5+5;
int n,a[N],b[N],q,mx[N<<2],pa[N<<2],cal[N<<2],lz[N<<2];
struct Nd{
    int ans,x,p;
};
int rd()
{
    int ret=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
    while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar();
    return ret*f;
}
void pdn(int u)
{
    if(!lz[u])return; lz[u]=0;
    pa[ls]^=1; lz[ls]^=1; pa[rs]^=1; lz[rs]^=1;
}
int findb(int u,int l,int r,int p)
{
    if(l==r)return pa[u];
    pdn(u);
    if(p<=mid)return findb(ls,l,mid,p);
    else return findb(rs,mid+1,r,p);
}
Nd qry(int u,int l,int r,int ql,int qr,int x,int p)//ql~qr内,前一个数为x,其位置的b为p,取出最长不降子序列
{
    if(ql>qr||x>mx[u])return (Nd){0,x,p}; Nd ans;
    //printf("qry(l=%d r=%d x=%d p=%d ql=%d qr=%d)\n",l,r,x,p,ql,qr);
    if(l==r){ans=(mx[u]>=x)?(Nd){(pa[u]!=p),mx[u],pa[u]}:(Nd){0,x,p};///!
                //printf("0ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p);
                return ans;
            }
    pdn(u);
    if(l>=ql&&r<=qr)
    {
        if(x<=mx[ls]){ans=qry(ls,l,mid,ql,qr,x,p); //ans.ans+=cal[u]; ans.x=mx[rs]; ans.p=pa[rs];
                        if(mx[rs]>=ans.x)ans.ans+=cal[u],ans.x=mx[rs],ans.p=pa[rs];///!
                //printf("1ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p);
                return ans;}
        else {ans=qry(rs,mid+1,r,ql,qr,x,p);
             //printf("2ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p);
             return ans;}
    }
    if(mid>=ql)
    {
        if(mid>=qr){ans=qry(ls,l,mid,ql,qr,x,p);
            //printf("3ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p);
            return ans;}
        else
        {
            ans=qry(ls,l,mid,ql,qr,x,p); Nd a2=qry(rs,mid+1,r,ql,qr,ans.x,ans.p);
            //printf("4ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,ansx=%d,ansp=%d,ansa=%d,a2x=%d,a2p=%d,a2a=%d\n",ql,qr,l,r,x,p,ans.ans+a2.ans,ans.x,ans.p,ans.ans,a2.x,a2.p,a2.ans);
            return (Nd){ans.ans+a2.ans,a2.x,a2.p};
        }
    }
    else {ans=qry(rs,mid+1,r,ql,qr,x,p);
             //printf("5ql=%d qr=%d l=%d r=%d x=%d p=%d = %d,%d,%d\n",ql,qr,l,r,x,p,ans.ans,ans.x,ans.p);
             return ans;}
}
void upt(int u,int l,int r)
{
    if(mx[ls]>mx[rs])mx[u]=mx[ls],pa[u]=pa[ls],cal[u]=0;
    else mx[u]=mx[rs],pa[u]=pa[rs],cal[u]=qry(rs,mid+1,r,mid+1,r,mx[ls],pa[ls]).ans;
}
void build(int u,int l,int r)
{
    if(l==r){mx[u]=a[l]; pa[u]=b[l]; return;}
    build(ls,l,mid); build(rs,mid+1,r);
    upt(u,l,r);
    //printf("l=%d r=%d mid=%d cal=%d\n",l,r,mid,cal[u]);
}
void change1(int u,int l,int r,int ps,int x)//更新mx
{
    if(l==r){mx[u]=x; return;}
    pdn(u);
    if(ps<=mid)change1(ls,l,mid,ps,x);
    else change1(rs,mid+1,r,ps,x);
    upt(u,l,r);
}
void change2(int u,int l,int r,int ql,int qr)//更新b
{
    if(l>=ql&&r<=qr){pa[u]^=1; lz[u]^=1; return;}
    pdn(u);
    if(mid>=ql)change2(ls,l,mid,ql,qr);
    if(mid<qr)change2(rs,mid+1,r,ql,qr);
    upt(u,l,r);
}
int main()
{
    n=rd();
    for(int i=1;i<=n;i++)a[i]=rd();
    for(int i=1;i<=n;i++)b[i]=rd();
    build(1,1,n); q=rd();
    for(int i=1,tp,t1,t2;i<=q;i++)
    {
        tp=rd(); t1=rd(); t2=rd();
        if(tp==1)a[t1]=t2,change1(1,1,n,t1,t2);
        if(tp==2)change2(1,1,n,t1,t2);
        if(tp==3)printf("%d\n",qry(1,1,n,t1+1,t2,a[t1],findb(1,1,n,t1)).ans);
    }
    return 0;
}
me

 

F

分析:

第一棵树(下称树1)中选取的链一定是连续的,容易想到dfs;因为dfs到的当前点的祖先链都已经dfs过了。

所以我们想利用]\( fa[u] \)的答案来计算\( u \)的答案。我们可以考虑每个点向上最长能延伸多少个点。

在树2上的祖先关系可以转化成dfs序上的区间覆盖问题。那么,在对树1dfs的过程中,我们希望记录下当前点祖先在树2上的覆盖信息,以此计算答案。因为dfs有撤销的过程,所以我们可以用主席树来实现:

对于树1上dfs到的点\(u\),继承\(fa[u]\)的主席树,然后在\(u\)的dfs序子树区间上覆盖\(dep[u]\)这个值;

查询\(fa[u]\)的主席树上\(u\)子树区间内的最大值,这个值表示会与\(u\)产生冲突的最深的祖先点;所以\(ans[u]=dep[u]-query(fa[u], l[u], r[u]) \);

但是还要考虑到\(u\)的祖先点彼此之间的冲突;由于\(u\)向上延伸的链和\(fa[u]\)向上延伸的链是重合的,所以\(ans[u]\)只需要再对\(ans[fa[u]]\)取个\(min\)值即可。

注意主席树上\(lazy\)标记的用法……挺妙的。

时间复杂度\(O(nlogn)\)。

代码如下:

#include<iostream>
#include<cstring>
#define mid ((l+r)>>1)
using namespace std;
int const N=3e5+5;
int T,n,hd1[N],cnt1,nxt1[N<<1],to1[N<<1],hd2[N],cnt2,nxt2[N<<1],to2[N<<1];
int tim,dfn[N],siz[N],dep[N],ans[N];
int rt[N],tcnt,ls[N<<5],rs[N<<5],mx[N<<5],lz[N<<5];
int rd()
{
    int ret=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
    while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar();
    return ret*f;
}
void add1(int x,int y){nxt1[++cnt1]=hd1[x]; hd1[x]=cnt1; to1[cnt1]=y;}
void add2(int x,int y){nxt2[++cnt2]=hd2[x]; hd2[x]=cnt2; to2[cnt2]=y;}
void init()
{
    cnt1=0; cnt2=0; memset(hd1,0,sizeof hd1); memset(hd2,0,sizeof hd2);
    tim=0; memset(ans,0,sizeof ans);
    tcnt=0; memset(rt,0,sizeof rt); memset(mx,0,sizeof mx);
}
void dfs2(int u,int fa)
{
    dfn[u]=++tim; siz[u]=1;
    for(int i=hd2[u],v;i;i=nxt2[i])
        if((v=to2[i])!=fa)dfs2(v,u),siz[u]+=siz[v];
}
int build(int l,int r)
{
    int ret=++tcnt; mx[ret]=0; lz[ret]=0;
    if(l>=r)return ret;
    ls[ret]=build(l,mid);
    rs[ret]=build(mid+1,r);
    return ret;
}
int qry(int u,int l,int r,int ql,int qr)
{
    if(l>=ql&&r<=qr)return mx[u];
    int ret=lz[u];//
    if(mid>=ql)ret=max(ret,qry(ls[u],l,mid,ql,qr));
    if(mid<qr)ret=max(ret,qry(rs[u],mid+1,r,ql,qr));
    return ret;
}
int update(int u,int l,int r,int ql,int qr,int x)
{
    int ret=++tcnt;
    mx[ret]=max(mx[u],x); ls[ret]=ls[u]; rs[ret]=rs[u]; lz[ret]=0;//
    if(l>=ql&&r<=qr){lz[ret]=x; return ret;}//
    if(mid>=ql)ls[ret]=update(ls[u],l,mid,ql,qr,x);
    if(mid<qr)rs[ret]=update(rs[u],mid+1,r,ql,qr,x);
    return ret;
}
void dfs1(int u,int fa)
{
    dep[u]=dep[fa]+1;
    if(fa)ans[u]=min(ans[fa]+1,dep[u]-qry(rt[fa],1,n,dfn[u],dfn[u]+siz[u]-1));
    else ans[u]=1;
    //printf("ans[%d]=%d\n",u,ans[u]);
    rt[u]=update(rt[fa],1,n,dfn[u],dfn[u]+siz[u]-1,dep[u]);
    for(int i=hd1[u],v;i;i=nxt1[i])
        if((v=to1[i])!=fa)dfs1(v,u);
}
int main()
{
    T=rd();
    while(T--)
    {
        n=rd(); init();
        for(int i=1,u,v;i<n;i++)
            u=rd(),v=rd(),add1(u,v),add1(v,u);
        for(int i=1,u,v;i<n;i++)
            u=rd(),v=rd(),add2(u,v),add2(v,u);
        dfs2(1,0);
        rt[0]=build(1,n);
        dfs1(1,0);
        int anss=0;
        for(int i=1;i<=n;i++)anss=max(anss,ans[i]);
        printf("%d\n",anss);
    }
    return 0;
}
me

 

H

分析:

签到题。用桶记录出现的数,直接枚举因子计数。复杂度\( O(nlogn) \)。

 

I

分析:

签到题。按位考虑。

 

J

分析:

题解第一种做法,有点不好想到,但复杂度靠谱。

还看到一种比较直接的做法,就是对错误Floyd各种剪枝。核心思想是对于转移方程\( dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]) \),只遍历有可能使\( dis[i][j] \)正确的那些\( k \)。

错误Floyd的转移有很整齐的顺序,对于\(k\)我们可以分成小于\(j\)的和大于\(j\)的。小于\(j\)的\(k\)在前面已经转移得到了\( dis[i][k] \),而大于\(j\)的\(k\)除了直接与\(i\)连边的,其他都是\(dis[i][k]=inf\)。

所以对于大于\(j\)的\(k\),\( dis[i][k] = inf \)的\(k\)就不遍历了,也就是只找和\(i\)直接连边的点\(k\);

对于小于\(j\)的\(k\),只考虑有可能使接下来的\(dis[i][j]\)答案正确的。也就是如果\(dis[i][j]\)答案正确,就把\(j\)加到\(i\)要遍历的点集里。

这个点集初始只有\(i\)直接相连的点。每次枚举到一个\(j\),遍历这个点集中的点来更新答案。

但是这样还是会TLE。再加点剪枝,比如正确答案就是\(inf\)的就不更新了,还有已经是正确答案的也不更新了。

时间复杂度??,呃反正是能过,还不慢呢。(毕竟各种做法都是加速错误Floyd这个过程)

代码如下:

#include<iostream>
#include<queue>
#include<cstring>
#include<vector>
#define pb push_back
using namespace std;
int const N=2005,M=5005;
int n,m,d[N][N],dis[N][N],hd[N],cnt,nxt[M],to[M],w[M],inf;
bool vis[N];
struct Nd{
    int d,id;
    bool operator < (const Nd &a) const
    {return d>a.d;}
};
priority_queue<Nd>q;
vector<int>ed[N];
void add(int x,int y,int t){nxt[++cnt]=hd[x]; hd[x]=cnt; to[cnt]=y; w[cnt]=t;}
void dijk(int st)
{
    memset(vis,0,sizeof vis); q.push((Nd){0,st});
    while(q.size())
    {
        int nd=q.top().d,u=q.top().id; q.pop();
        if(vis[u])continue; vis[u]=1;
        for(int i=hd[u],v;i;i=nxt[i])
        {
            if(vis[v=to[i]])continue;
            if(d[st][v]>nd+w[i])
                d[st][v]=nd+w[i],q.push((Nd){d[st][v],v});
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(d,0x3f3f3f3f,sizeof d); memset(dis,0x3f3f3f3f,sizeof dis); inf=d[0][0];
    for(int i=1;i<=n;i++)d[i][i]=0,dis[i][i]=0;
    for(int i=1,u,v,t;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&t);
        add(u,v,t); dis[u][v]=t; ed[u].pb(v);
    }
    for(int i=1;i<=n;i++)dijk(i);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            if(d[i][j]==dis[i][j]||d[i][j]==inf)continue;
            for(int k:ed[i])
                dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
            if(d[i][j]==dis[i][j])ed[i].pb(j);
        }
    int ans=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            ans+=(d[i][j]==dis[i][j]);//,printf("d[%d][%d]=%d dis[%d][%d]=%d\n",i,j,d[i][j],i,j,dis[i][j]);
    printf("%d\n",ans);
    return 0;
}
me

 

K

分析:

比赛时想了半天,也想过贪心之类的,较小的数减成0、较大的数加成\( k \)这种。但是区间内的元素彼此牵连,总是很乱不能做。

看了题解才发现应该差分,这样区间加减就变成单点加减了;区间归零也可以转换成差分归零。一下子清晰好多。

先考虑只有一次询问而且没有\( k \)的情况要怎么做:在差分数组\( b_i \)上,一次操作会把某个位置\( +1 \),另一个位置\( -1 \);而差分数组的总和等于\( 0 \)(在原数组前后插两个\( 0 \),差分数组一共\( n+1 \)个元素)。所以最小次数就是\( \sum_{i=1}^{i=n+1} |b_i| / 2 \)。

现在考虑引入\( k \):每个差分既可以变成\( 0 \),也可以变成\( \pm k \)。为了方便考虑,这时我们可以把负数\( x \)变为正数\( k+x \),这样所有数都在\( [0,k) \)这个区间里。可以想到贪心的做法就是让较小的数变成\( 0 \),较大的数变成\( k \);我们可以把整个数组排序,然后二分一下在哪里分界。当把较小数变成\( 0 \)的操作次数\( pref \)与把较大数变成\( k \)的操作次数\( suf \)最接近时,答案最优,二分结束。

现在,回到我们的问题:有多次询问,每次询问有一个区间和一个\( k \)。这要求我们在\( logn \)级别的时间内把一个区间的差分取出来,负数变成正数(这个过程和\( k \)有关,不能预处理),然后排序,再二分。为了快速取区间并排序,可以想到对差分序列建立权值主席树(主席树额外的好处是动态建点,不需要离散化)。

但是,负数变正数与\( k \)有关,还是不能做。我们可以干脆分开正负考虑;对于负数\( x \),我们原本把它变成\( k+x \),然后与二分到的值\( mid \)作比较;现在我们保留\( x \),让它与\( mid-k \)作比较。这样就在二分的过程中解决负数的问题了。

还要注意值恰好是二分的\( mid \)的那些数;它们可以一部分取到\( 0 \),一部分取到\( \pm k \),所以这里还需要再二分,求最优的答案。

时间复杂度\( O(nlogk+qlog^2k) \)。

写完不知为何TLE了,感觉不是时间复杂度的问题,应该是某些递归边界没写好。后来把主席树查询函数改了一下,当前区间没有值了就直接返回,就过了。仔细想想,不判断这个的话会把范围内的所有数不停地找下去,确实会TLE,和我一开始要在\( 0 \)号点build一个主席树犯了一样的错误。笑。

代码如下:

#include<iostream>
#define ll long long
#define mid ((l+r)>>1)
using namespace std;
int const N=2e5+5;
ll const inf=1e17;
int n,q,a[N],b[N],up,dn;
int cnt,rt[2][N],ls[2][N<<5],rs[2][N<<5],num[2][N<<5];
ll sum[2][N<<5];
struct Nd{
    ll s; int num;
    Nd operator + (const Nd &a) const
        {return (Nd){s+a.s,num+a.num};}
};
int rd()
{
    int ret=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1; ch=getchar();}
    while(ch>='0'&&ch<='9')ret=(ret<<3)+(ret<<1)+ch-'0',ch=getchar();
    return ret*f;
}
/*
int build(int tp,int l,int r)
{
   int ret=++cnt;
   if(l>=r)return ret;
   ls[tp][ret]=build(tp,l,mid); rs[tp][ret]=build(tp,mid+1,r);
   return ret;
}
*/
int update(int tp,int pre,int l,int r,int x)
{
    int ret=++cnt;
    ls[tp][ret]=ls[tp][pre]; rs[tp][ret]=rs[tp][pre];
    sum[tp][ret]=sum[tp][pre]+x; num[tp][ret]=num[tp][pre]+1;
    if(l==r)return ret;
    if(x<=mid)ls[tp][ret]=update(tp,ls[tp][pre],l,mid,x);
    else rs[tp][ret]=update(tp,rs[tp][pre],mid+1,r,x);
    return ret;
}
void Build()
{
    //rt[0][0]=build(0,0,up); rt[1][0]=build(1,1,dn);
    for(int i=1;i<=n;i++)//
    {
        if(b[i]>=0)
        {
            rt[0][i]=update(0,rt[0][i-1],0,up,b[i]);
            rt[1][i]=rt[1][i-1];
        }
        else
        {
            rt[0][i]=rt[0][i-1];
            rt[1][i]=update(1,rt[1][i-1],1,dn,-b[i]);
        }
    }
}
Nd qry(int tp,int pre,int nw,int l,int r,int ql,int qr)
{
    if(ql>qr||l>r||num[tp][nw]-num[tp][pre]==0)return (Nd){0,0};//!
    //if(ql>qr||l>r||l>qr||r<ql)return (Nd){0,0};//TLE
    if(l>=ql&&r<=qr)return (Nd){sum[tp][nw]-sum[tp][pre],num[tp][nw]-num[tp][pre]};
    Nd ret=(Nd){0,0};
    if(mid>=ql)ret=ret+qry(tp,ls[tp][pre],ls[tp][nw],l,mid,ql,qr);
    if(mid<qr)ret=ret+qry(tp,rs[tp][pre],rs[tp][nw],mid+1,r,ql,qr);
    return ret;
}
int main()
{
    n=rd(); q=rd();
    for(int i=1;i<=n;i++)
    {
        a[i]=rd(); b[i]=a[i]-a[i-1];
        if(b[i]>up)up=b[i]; if(b[i]<dn)dn=b[i];
    }
    //b[n+1]=0-a[n];
    //if(b[n+1]>up)up=b[n+1]; if(b[n+1]<dn)dn=b[n+1];
    dn=-dn; Build();
    for(int i=1,L,R,k;i<=q;i++)
    {
        L=rd(); R=rd(); k=rd();
        Nd qup1,qup2,qdn1,qdn2; int l=0,r=k; ll ans=inf;
        while(l<=r)
        {
            //printf("l=%d r=%d mid=%d\n",l,r,mid);
            qup1=qry(0,rt[0][L],rt[0][R],0,up,0,mid-1); qup2=qry(0,rt[0][L],rt[0][R],0,up,mid+1,up);
            qdn1=qry(1,rt[1][L],rt[1][R],1,dn,1,k-mid-1); qdn2=qry(1,rt[1][L],rt[1][R],1,dn,k-mid+1,dn);


            int midc=(R-L)-qup1.num-qup2.num-qdn1.num-qdn2.num;
            if(a[L]<mid)qup1.s+=a[L],qup1.num++;
            else if(a[L]>mid)qup2.s+=a[L],qup2.num++;
            else midc++;
            /*
            printf("midc=%d\n",midc);
            printf("up1=%lld cnt1=%d up2=%lld cnt2=%d\n",qup1.s,qup1.num,qup2.s,qup2.num);
            printf("dn1=%lld cnt1=%d dn2=%lld cnt2=%d\n",qdn1.s,qdn1.num,qdn2.s,qdn2.num);*/

            ll pref=qup1.s+((ll)k*qdn2.num-qdn2.s);
            ll suf=((ll)k*qup2.num-qup2.s)+qdn1.s;
            if(!midc)ans=min(ans,max(pref,suf));
            else
            {
                int cl=0,cr=midc;
                while(cl<=cr)
                {
                    int Mid=((cl+cr)>>1);
                    ll cpref=pref+(ll)Mid*mid, csuf=suf+(ll)(midc-Mid)*(k-mid);
                    ans=min(ans,max(cpref,csuf));
                    if(cpref<csuf)cl=Mid+1;
                    else cr=Mid-1;
                }
            }

            if(pref<suf)l=mid+1;
            else r=mid-1;
        }
        printf("%lld\n",ans);
    }
    return 0;
}
me

 

posted @ 2021-08-10 11:15  Zinn  阅读(41)  评论(0编辑  收藏  举报