• 2021牛客暑期多校训练营5


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

    B,D,H,K,15,差点做出J。

    H签到题。B是期望题,Y很快推出来了,但是写了写WA了,就先搁置;G在写K,但写不出来,后来发现做法有点问题。我在看D,想了一会,然后G说了个DP,我也觉得行,于是他去写了,写完又WA了,也先搁置了。又看了看别的题,还是回来看D,G换了种DP方式,就过了;又看看B,忽然发现没排序,排序后就过了;然后G又想到K的另种简单做法,也过了。这之后就做J,我想了个做法,写完却WA了,仔细想想有点不对,因为那些关于t的二次函数在原点右边也可以有交点。这题应该是个二分图匹配;G上去写网络流,但是T了;Y又写KM算法,复杂度应该没问题,但还是一直T。然后比赛结束。讲题说J的正解就是KM算法,不知是哪里写错了。

    总之我心情还是挺轻松的。有句话说得好啊,比赛中只有队伍,没有个人!心思重会很疲惫。

    B

    分析:

    ( C )最多花一次,而且要花就在最开始花;

    所以有两种策略,一种是全开,代价( sum w_i );

    另一种是先花( C ),然后按( w_i )升序一个一个往后开,开到后缀全是同色为止,代价( C + sum w_i * (1-frac{1}{2^{n-i}} ) ),意思是如果后面( n-i )个都确定了,那第( i )个也不用开了。

    C

    分析:

    利用(W,L)的前缀和;记录每个(W,L)的位置;记录分数相同时的终点位置;然后一段一段地判断即可。

    小心(n=1或n=2)时,不要让for循环陷入死循环^_^

    代码如下:

    #include<iostream>
    #include<cstdio>
    #define ll long long
    using namespace std;
    int const N=1e6+5,md=998244353;
    int n,p,sumw[N],suml[N],posw[N],posl[N],tie[N];
    int f[N];
    char s[N];
    void init()
    {
        tie[n]=n+1; tie[n-1]=n+1;
        //for(int i=n-2;i;i--)tie[i]=(s[i+1]==s[i+2])?i+2:tie[i+2];
        for(int i=n-2;i>=1;i--)tie[i]=(s[i+1]==s[i+2])?i+2:tie[i+2];///!!!
        for(int i=1;i<=n;i++)
        {
            sumw[i]=sumw[i-1]+(s[i]=='W');
            suml[i]=suml[i-1]+(s[i]=='L');
            if(s[i]=='W')posw[sumw[i]]=i;
            if(s[i]=='L')posl[suml[i]]=i;
        }
    }
    void work(int k)
    {
        if(!p||p>n)return;
        int cw=sumw[p-1],cl=suml[p-1];
        if(cw+k<=sumw[n])//W先达到k局
        {
            int psw=posw[cw+k];
            if(suml[psw]-cl<=k-2){f[k]++; p=psw+1; return;}
            if(suml[psw]-cl==k-1)
            {
                if(psw==n){p=psw+1; return;}
                else if(s[psw+1]=='W'){f[k]++; p=psw+2; return;}
                else
                {
                    if(tie[psw+1]<=n)f[k]+=(s[tie[psw+1]]=='W');
                    p=tie[psw+1]+1; return;
                }
            }
        }
        if(cl+k<=suml[n])//L先达到k局
        {
            int psl=posl[cl+k];
            if(sumw[psl]-cw<=k-2){p=psl+1; return;}
            if(sumw[psl]-cw==k-1)
            {
                if(psl==n){p=psl+1; return;}
                else if(s[psl+1]=='L'){p=psl+2; return;}
                else
                {
                    if(tie[psl+1]<=n)f[k]+=(s[tie[psl+1]]=='W');
                    p=tie[psl+1]+1; return;
                }
            }
        }
        p=n+1; return;
    }
    int main()
    {
        scanf("%d%s",&n,s+1); init();
        for(int i=1;i<=n;i++)
        {
            p=1;
            while(p+i-1<=n)work(i);
        }
        int ans=0,mul=1;
        for(int i=1;i<=n;i++)
            ans=(ans+(ll)f[i]*mul%md)%md,mul=(ll)mul*(n+1)%md;
        printf("%d
    ",ans);
        return 0;
    }
    me

    D

    分析:

    想到枚举( a_i < b_j )的点了,然后前面一个DP是相同的子序列方案数,后面一个DP是两段字符串任意取相同长度子序列的方案数;但是这DP一时间没想清楚。其实这种DP很简单的。

    代码如下:

    #include<iostream>
    #include<cstring>
    #define ll long long
    using namespace std;
    int const N=5005,md=1e9+7;
    int n,m,f[N][N],g[N][N],ans;
    char a[N],b[N];
    int main()
    {
        scanf("%s%s",a+1,b+1);
        n=strlen(a+1); m=strlen(b+1);
        for(int i=0;i<=n;i++)f[i][0]=g[i][0]=1;//i=0
        for(int j=0;j<=m;j++)f[0][j]=g[0][j]=1;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                g[i][j]=((ll)g[i-1][j]+g[i][j-1]-g[i-1][j-1]+g[i-1][j-1])%md;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                f[i][j]=((ll)f[i-1][j]+f[i][j-1]+md-f[i-1][j-1])%md;//
                if(a[i]==b[j])f[i][j]=((ll)f[i][j]+f[i-1][j-1])%md;
                if(a[i]<b[j])ans=((ll)ans+(ll)f[i-1][j-1]*g[n-i][m-j]%md)%md;
            }
        printf("%d
    ",ans);
        return 0;
    }
    me

    E

    分析:

    因为有( d )的存在,每次值都会变,再搞位运算很困难;

    但是发现( d )的范围只有100。所以可以对每个( d )都做一次,这样就可以只关注位运算了。当然,这样的话每个( d )需要( O(n) )出结果。

    但是没关系。像这种子树内所有路径如何如何的问题,容易想到树形DP。关键是如何转移三种运算的答案。

    设( f[u][0/1/2] )分别表示以( u )为根的子树下所有路径的或/与/异或值。我们针对当前( u )的儿子( v ),考虑三个值如何转移。

    如果( u —> v )这条边上的运算是“或”:

    对于( f[u][0] ):子树内所有路径值 ( | w_u)后再全部( | )起来,同先( | )起来以后再 ( | w_u )。所以 ( f[u][0] |= w_u | f[v][0] )

    对于( f[u][1] ):子树内所有路径值 ( | w_u )后再全部 ( & )起来,同先 ( & )起来以后再 ( | w_u )。所以 ( f[u][1] &= w_u | f[v][1] )

    对于( f[u][2] ):子树内所有路径值( | w_u )后再全部 ( igoplus )起来。由于所有路径值在 ( w_u ) 为(1)的那几位上都是(1),所以这些位完全与异或的次数有关,也就是与(v)子树的大小有关。这些位异或奇数次全(1),异或偶数次全(0)。其他位与( w_u )就没有关系了,所以其他位就是( f[v][2] )。这里想要分开位做,可以通过( & w_u )和 ( & (sim w_u) )实现。

    如果( u —> v )这条边上的运算是“与”:

    ( & )这个操作,不管放在内层还是外层,( & 0)就是(0),( & 1)就是原来的数。所以内层或外层、操作几次都是一样的。当然不放心的话就自己再稍微想一想。

    对于( f[u][0] ):( f[u][0] |= w_u & f[v][0] )

    对于( f[u][1] ):( f[u][1] &= w_u & f[v][1] )

    对于( f[u][2] ):( f[u][2] igoplus= w_u & f[v][2] )

    如果( u —> v )这条边上的运算是“异或”:

    对于( f[u][0] ):分别考虑( w_u )是(0)的位和是(1)的位。是(0)的位对原来那些路径值没有影响,所以还是( f[v][0] )。是(1)的位会把路径值上对应位都取反;而取反以后再( | )起来的效果和取反以前( & )起来的效果是一样的。这挺奇妙,自己验证一下或者想一想就可知。所以是(1)的位要用的是( f[v][1] )。

    对于( f[u][1] ):完全同上。但这里写的时候要注意呀!!因为是( & )操作,所以分开位的时候不能分别( & )呀!要 ( | )起来再整体 ( & )呀!TAT

    对于( f[u][2] ):都是异或,具有结合律,括号随便拆。所以( f[u][2] igoplus= f[v][2] ),如果(v)子树大小是奇数,再( igoplus w_u )。

    做完这些以后还要注意个小细节,就是每个点算答案的时候是不包含自己的,但是转移的时候又需要把自己转移上去。所以这里另开一个数组存答案吧。

    代码如下:

    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #define ll long long
    using namespace std;
    int const N=1e5+5;
    int n,q,hd[N],cnt,nxt[N],to[N],s[N],d,siz[N];
    ll a[N],f[N][3],ans[N][3];
    struct Nd{
        int d,u,id;
        ll a0,a1,a2;
    }qr[N];
    bool cmp(Nd x,Nd y){return x.d<y.d;}
    bool cmp2(Nd x,Nd y){return x.id<y.id;}
    ll rd()
    {
        ll 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 add(int x,int y,int t){nxt[++cnt]=hd[x]; hd[x]=cnt; to[cnt]=y; s[cnt]=t;}
    void dfs(int u)
    {
        ll w=a[u]+u*d; siz[u]=1;
        f[u][0]=0; f[u][1]=(1ll<<62)-1; f[u][2]=0;
        for(int i=hd[u],v;i;i=nxt[i])
        {
            dfs(v=to[i]); siz[u]+=siz[v];
            if(s[i]==0)
            {
                f[u][0]|=(w|f[v][0]);
                f[u][1]&=(w|f[v][1]);
                f[u][2]^=( (~w) & f[v][2] );
                if(siz[v]&1)f[u][2]^=w;
            }
            if(s[i]==1)
            {
                f[u][0]|=(w&f[v][0]);
                f[u][1]&=(w&f[v][1]);
                f[u][2]^=(w&f[v][2]);
            }
            if(s[i]==2)
            {
                f[u][0]|=( (~w) & f[v][0] );
                f[u][0]|=( w & (~f[v][1]) );
                //f[u][1]&=( (~w) & f[v][1] );
                //f[u][1]&=( w & (~f[v][0]) );
                f[u][1]&=( ( (~w) & f[v][1] ) | ( w & (~f[v][0]) ) );//!
                // if(!d&&u==3&&v==2)printf("f[u][1]=%d w=%d (%d)&(%d)=%d (%d)&(%d)=%d
    ",
                //                             f[u][1],w,~w,f[v][1],(~w)&f[v][1],w,~f[v][0],w&(~f[v][0]));
                f[u][2]^=f[v][2];
                if(siz[v]&1)f[u][2]^=w;
            }
            //if(d==0)printf("u=%d v=%d f[%d][1]=%d f[%d][1]=%d
    ",u,v,v,f[v][1],u,f[u][1]);
        }
        // if(siz[u]==1)
        //     f[u][0]=w,f[u][1]=w,f[u][2]=w;
        for(int i=0;i<=2;i++)ans[u][i]=f[u][i];
        f[u][0]|=w; f[u][1]&=w; f[u][2]^=w;
    }
    int main()
    {
        n=rd(); q=rd();
        for(int i=1;i<=n;i++)a[i]=rd();
        for(int i=2,f,t;i<=n;i++)
            f=rd(),t=rd(),add(f,i,t);
        for(int i=1;i<=q;i++)qr[i].d=rd(),qr[i].u=rd(),qr[i].id=i;
        sort(qr+1,qr+q+1,cmp); d=-1;
        for(int i=1;i<=q;i++)
        {
            if(d!=qr[i].d)d=qr[i].d,dfs(1); int u=qr[i].u;
            qr[i].a0=ans[u][0]; qr[i].a1=ans[u][1]; qr[i].a2=ans[u][2];
        }
        sort(qr+1,qr+q+1,cmp2);
        for(int i=1;i<=q;i++)
            printf("%lld %lld %lld
    ",qr[i].a0,qr[i].a1,qr[i].a2);
        return 0;
    }
    me

    G

    分析:

    是……子集DP。看了G的博客。做法很直观,枚举子集的技巧挺不错的。

    __int128 要自己写输出。

    H

    分析:

    就00110011....和11001100...交替即可。

    J

    分析:

    要给每个东西选一个时间拿;也就是( n*n )的矩阵,每行每列只能选一个,求最小总和。

    行作为一边,列作为一边,用KM算法做最小权值二分图匹配即可。

    板子不好背,总之积累下来了。这里放一下G的代码。

    代码如下:

    #include<bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int MAXN=310;
    const ll Inf=1e18;
    int n;
    ll Ans,Edge[MAXN][MAXN];
    namespace KM
    {   int Pre[MAXN],Cp[MAXN];
        ll Delta,Slack[MAXN],Val1[MAXN],Val2[MAXN];
        bool Vis1[MAXN],Vis2[MAXN];
        void Match(int St)
        {   int Le,Ri=0,Nr=0;
            for(int i=0;i<=n;i++)   Pre[i]=0,Slack[i]=Inf;
            for(Cp[Ri]=St;Cp[Ri]!=-1;Ri=Nr)
            {
                Le=Cp[Ri],Delta=Inf,Vis2[Ri]=1;
                for(int i=1;i<=n;i++)
                {
                    if(Vis2[i]) continue ;
                    if(Slack[i]>Val1[Le]+Val2[i]-Edge[Le][i])
                        Slack[i]=Val1[Le]+Val2[i]-Edge[Le][i],Pre[i]=Ri;
                    if(Slack[i]<Delta) Delta=Slack[i],Nr=i;
                }
                for(int i=0;i<=n;i++)
                    if(Vis2[i]) Val1[Cp[i]]-=Delta,Val2[i]+=Delta;
                    else Slack[i]-=Delta;
            }
            while(Ri) Cp[Ri]=Cp[Pre[Ri]],Ri=Pre[Ri];
        }
        void Solve()
        {   for(int i=0;i<=n;i++) Val1[i]=Val2[i]=0,Cp[i]=-1;
            for(int i=1;i<=n;i++) fill(Vis2,Vis2+n+1,0),Match(i);
            for(int i=1;i<=n;i++) Ans+=Edge[Cp[i]][i]*(Cp[i]!=-1);
        }
    }using namespace KM;
    int main()
    {   scanf("%d",&n);
        for(int i=1,X,Y,Z,V;i<=n;i++)
        {   scanf("%d%d%d%d",&X,&Y,&Z,&V);
            for(int j=0;j<n;j++) Edge[i][j+1]=-(X*X+Y*Y+Z*Z+2ll*Z*j*V+1ll*j*j*V*V);
        }
        Solve(),printf("%lld
    ",-Ans);
    }
    G

    K

    分析:

    对于每个询问,枚举( r ),找到第一个不符合条件的( l );可以知道( l )是只会右移的;

    用单调队列维护递减的最大值,递增的最小值,就可以判断当前( l )是否符合条件了。

    代码如下:

    #include<iostream>
    #define ll long long
    using namespace std;
    int const N=1e5+5;
    int n,m,a[N],mx[N],hdx,tlx,mn[N],hdn,tln;
    ll ans;
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        for(int i=1,k;i<=m;i++)
        {
            scanf("%d",&k); ans=0;
            hdx=hdn=1; tlx=tln=0;
            for(int r=1,l=1;r<=n;r++)
            {
                while(hdx<=tlx&&a[mx[tlx]]<a[r])tlx--; mx[++tlx]=r;
                while(hdn<=tln&&a[mn[tln]]>a[r])tln--; mn[++tln]=r;
                //for(int j=hdx;j<=tlx;j++)printf("mx[%d]=%d ",j,mx[j]); printf("
    ");
                //for(int j=hdn;j<=tln;j++)printf("mn[%d]=%d ",j,mn[j]); printf("
    ");
                while(l<r&&a[mx[hdx]]-a[mn[hdn]]>k)
                {
                    if(mx[hdx]==l&&hdx<=tlx)hdx++;
                    if(mn[hdn]==l&&hdn<=tln)hdn++;
                    l++;
                }
                //printf("l-1=%d r=%d
    ",l-1,r);
                ans+=l-1;
            }
            printf("%lld
    ",ans);
        }
        return 0;
    }
    me
  • 相关阅读:
    cf 786B. Legacy(线段树区间建图)
    cf 1416D. Graph and Queries (生成树重构+线段树维护dfs序)
    cf 1437E. Make It Increasing
    cf 1434D. Roads and Ramen (树上最长偶权链)
    cf 1413C (贪心排序+双指针)
    cf 1421E. Swedish Heroes (dp)
    CF1428 F.Fruit Sequences
    11.Redis详解(十一)------ 过期删除策略和内存淘汰策略
    10.Redis详解(十)------ 集群模式详解
    9.Redis详解(九)------ 哨兵(Sentinel)模式详解
  • 原文地址:https://www.cnblogs.com/Zinn/p/15086960.html
Copyright © 2020-2023  润新知