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\n",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\n",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 \)后再全部 \( \bigoplus \)起来。由于所有路径值在 \( 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] \bigoplus= 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] \bigoplus= f[v][2] \),如果\(v\)子树大小是奇数,再\( \bigoplus 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\n",
            //                             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\n",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\n",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\n",-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("\n");
            //for(int j=hdn;j<=tln;j++)printf("mn[%d]=%d ",j,mn[j]); printf("\n");
            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\n",l-1,r);
            ans+=l-1;
        }
        printf("%lld\n",ans);
    }
    return 0;
}
me

 

posted @ 2021-08-01 18:24  Zinn  阅读(205)  评论(0编辑  收藏  举报