NOIP2022 题解

不得不说 jsnoip 的题解 写得很好,T2 ~ T4 均参考了该题解。

p.s. 下面的场切指 这组官方数据测试的结果,官方评测机的运行速度为测试评测机的 1.4 倍左右。

P8865 种花 Plant

解法

统计 \(C\) 时,枚举 \(x_2,y_0\),统计前面的 \(x_1\) 对应的 \(y_1\) 的和,和 \(x_2\) 的对应 \(y_2\),把这些内容相乘即可。统计 \(F\) 时需要计算 \(x_2\) 对应的 \(x_3\),再乘上 \(x_3\) 的数量即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1010;
const int md=998244353;
int t,n,m,c,f,i,j,l,ac,af,sa;
char s[maxn][maxn];
int tc[maxn][maxn],tf[maxn][maxn];
int main(){
    freopen("plant.in","r",stdin);
    freopen("plant.out","w",stdout);
    scanf("%d%d",&t,&n);
    while(t--){
        ac=af=0;
        scanf("%d%d%d%d",&n,&m,&c,&f);
        for(i=1;i<=n;++i){
            scanf("%s",s[i]+1);
            for(j=l=1;j<=m;++j)
                if(s[i][j]=='1')
                    for(;l<=j;++l) tc[i][l]=j-l-1;
            for(;l<=m;++l) tc[i][l]=m-l;
        }
        for(j=1;j<=m;++j){
            for(i=l=1;i<=n;++i)
                if(s[i][j]=='1')
                    for(;l<=i;++l) tf[l][j]=i-l-1;
            for(;l<=n;++l) tf[l][j]=n-l;
        }
        for(i=1;i<=n;++i){
            for(j=1;j<=m;++j){
                if(tc[i][j]<0) tc[i][j]=0;
                if(tf[i][j]<0) tf[i][j]=0;
            }
        }
        for(j=1;j<=m;++j){
            for(i=1;i<=n;++i){
                if(s[i][j]=='1') sa=0;
                else{
                    ac=(1LL*sa*tc[i][j]+ac)%md; 
                    af=((1LL*sa*tc[i][j])%md*tf[i][j]+af)%md;
                    sa-=((sa+=tc[i-1][j])>=md)*md;
                }
            }
            sa=0;
        }
        printf("%d %d\n",ac*c,af*f);
    }
    return 0;
}

P8866 喵了个喵 Meow

解法

考虑 \(k=2n-2\) 的情况。显然可以只考虑某个数 \(i\)\(\lfloor\frac{i+1}2\rfloor\) 栈内的情况(设 \(s_i\)\(i\) 需要放到的栈,则 \(k=2n-2\)\(s_i\) 可以恒为 \(\lfloor\frac{i+1}2\rfloor\))。加入 \(i\) 时,如果 \(s_i\) 栈内的栈顶为该数/栈为空,则直接将该数入栈;否则在栈 \(n\) 内入栈该数,然后弹出 \(s_i\)\(n\) 的栈底即可。显然每个栈内的数的数量不会超过 \(2\)

然后对于 \(k=2n-1\) 的情况。此时某个值不能按照上述的情况考虑。设 第一个 不能按照上述情况的数,也就是 \(n-1\) 个栈内均均已经有两个数后,出现的第一个不是已经加入的 \(2n-2\) 个数的数为 \(a\),可以按照下面的思路分类讨论:

  1. 如果 \(a\) 到下一个 \(a\) 的数只有其余栈的栈顶:可以直接将 \(a\) 放在空栈内,然后将直接把栈顶放入对应栈即可。

  2. 如果 \(a\) 到下一个 \(a\) 的数存在其余栈的栈底(设第一个出现的栈底为 \(b\)):设栈 \(s_b\) 的栈顶为 \(t\)

    1. 如果 \(a\)\(b\) 之间出现了奇数次 \(t\),则可以将 \(a\) 放入空栈,然后把 \(t\) 均放入 \(s_b\) 栈,出现 \(b\) 时也可以把 \(b\) 放入 \(s_b\) 栈内;则之后 \(s_b\) 成为了空栈,之前的空栈内有 \(a\)。(和情况 1 极其类似)
    2. 如果 \(a\)\(b\) 之间出现了偶数次 \(t\),则可以将 \(a\) 放入 \(s_b\) 栈内,把偶数个 \(t\) 放入空栈内消掉,出现 \(b\) 之后将 \(b\) 同样放入空栈中,然后消掉 \(s_b\) 和原空栈的栈底;则空栈仍然为空,原 \(s_b\) 栈内栈顶为 \(a\),栈底为 \(t\)

    之后的 \(a\) 可以均设为 \(b\),且在这部分操作后一定仍然至少存在一个空栈。重复上述的操作即可。

代码

有点毒瘤。(这种东西真的是能够在考场写 + 调的题目吗)(% sjx 巨佬 SC 唯一场切)

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=310;
const int maxm=2000010;
int T,n,m,k,i,j,u,v,w,h,t,K,ta;
int pre[maxn],nxt[maxn];
int s[maxn<<1],c[maxn<<1];
int a[maxm],x[maxn],y[maxn];
struct ans{int o,x,y;}A[maxm<<1];
int main(){
    freopen("meow.in","r",stdin);
    freopen("meow.out","w",stdout);
    scanf("%d",&T); 
    while(T--){
        scanf("%d%d%d",&n,&m,&k); K=k; 
        for(i=1;i<=m;++i) scanf("%d",a+i);
        for(i=1;i<=n;++i){nxt[i]=i+1;pre[i]=i-1;}
        nxt[n]=ta=0; h=1; t=n;
        for(i=1;i<=m;++i){
            w=a[i];
            if(!s[w]){
                if(h==t){
                    for(j=i+1;v=a[j],v!=w&&y[s[v]]!=v;++j) ++c[v];
                    if(v==w||(c[x[s[v]]]&1)){
                        A[++ta]={1,t,0};
                        for(k=i+1;k<j;++k){
                            A[++ta]={1,u=s[a[k]],0};
                            if(y[u]) x[u]=y[u],y[u]=0;
                            else y[u]=x[u],x[u]=a[k];
                        }
                        if(v==w) A[++ta]={1,t,0};
                        else{
                            A[++ta]={1,u=s[v],0};
                            pre[u]=s[w]=t; nxt[t]=u; 
                            x[t]=w; x[u]=y[u]=0; t=u;
                        }
                    }
                    else{
                        A[++ta]={1,u=s[v],0};
                        for(k=i+1;k<j;++k){
                            v=a[k];
                            if(v==x[u]) A[++ta]={1,t,0};
                            else{
                                A[++ta]={1,n=s[v],0};
                                if(y[n]) x[n]=y[n],y[n]=0;
                                else y[n]=x[n],x[n]=v;
                            }
                        }
                        A[++ta]={1,t,0};
                        A[++ta]={2,t,u};
                        y[u]=x[u]; x[u]=w; 
                        s[w]=u; s[a[j]]=0;
                    }
                    for(k=i+1;k<j;++k){
                        u=s[a[k]]; c[a[k]]=0;
                        if(!y[u]){
                            s[a[k]]=0;
                            if(!(pre[u]||nxt[u])){
                                nxt[u]=h; pre[h]=u;
                                pre[u]=0; h=u;
                            }
                        }
                    }
                    c[a[j]]=s[a[j]]=0; i=j;
                }
                else{
                    A[++ta]={1,h,0}; u=s[w]=h;
                    if(x[u]){
                        y[u]=x[u]; n=nxt[h];
                        nxt[h]=pre[n]=0; h=n;
                    }
                    x[u]=w;  
                }
            } 
            else{
                u=s[w]; s[w]=0; 
                if(x[u]==w){
                    A[++ta]={1,u,0};
                    x[u]=y[u]; y[u]=0;
                    if(x[u]){
                        pre[h]=u; nxt[u]=h;
                        pre[u]=0; h=u;
                    }
                    else{
                        pre[nxt[u]]=pre[u];
                        nxt[pre[u]]=nxt[u];
                        if(h==u) h=nxt[u];
                        pre[u]=t; t=nxt[t]=u; 
                        nxt[u]=0;
                    }
                }
                else{
                    A[++ta]={1,t,0};
                    A[++ta]={2,t,u};
                    pre[h]=u; nxt[u]=h;
                    pre[u]=0; h=u; y[u]=0;
                }
            }
        }
        printf("%d\n",ta);
        for(i=1;i<=ta;++i){
            printf("%d %d",A[i].o,A[i].x);
            if(A[i].o==2) printf(" %d",A[i].y);
            putchar('\n');
        }
    }
    return 0;
}

P8867 建造军营 Barrack

解法

为什么没有场切嗫哼哼啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊痛失 65 pts

显然某个边双连通分量内的点/边可以任选。把边双连通分量缩成点后,整个图转成了一棵树。此时可以枚举树上的连通块,设对于某个缩点之前有 \(x\) 个点的点集 \(S\),包含了其的虚树有 \(y\)树边,则其对方案数的贡献为 \((2^x-1)2^{m-y}\)。此时可以在虚树的根节点处统计信息保证不重不漏。

然后考虑 dp,设 \(dp_u\)\(u\) 的子树内满足最高点\(u\) 时的虚树数量。此时如果 \(u\) 只有一棵子树内部有点被选择,且 \(u\) 自己未选择,则对应选点方案一定不能使得该点成为虚树根节点。记 \(f_u\)\(u\) 的子树内存在关键点,且关键点和 \(\boldsymbol u\) 连成了连通块的方案数,\(E_u\)\(u\) 子树内的边(包括非树边)的数量,则非法情况为 \(\sum_{v\in son_u}f_v2^{E_u-E_v-1}\)(在 \(v\) 之外的边任选,但是不选关键点)。注意选择的点的虚树树根可能不是 \(v\),所以非法情况不只是 \(\sum_{v\in son_u}dp_v2^{E_u-E_v-1}\)。然后 \(dp_u\) 即为 \(f_u-\sum_{v\in son_u}f_v2^{E_u-E_v-1}\),答案为 \(\sum_{i=1}^n dp_i2^{m-E_i}\)

在求 \(f_u\) 时,可以分 \(u\) 是否为选择点进行考虑。设 \(u\) 缩点前的边数为 \(e_u\),点数为 \(d_u\)。在 \(u\) 的子树 \(v\) 内选择点时,一定需要将边 \((u,v)\) 选择以使其和 \(u\) 节点连通。\(u\) 不是选择点时,不能不选 \(u\) 子树的点,则方案数为 \((\prod_{v\in son_u}(f_v+2^{E_v+1}))2^{e_u}-2^{E_u}\)\(u\) 是选择点时,\(u\) 本身的选择方案为 \((2^{d_u}-1)2^{e_u}\),子树内可以不选,则总方案数为 \(((2^{d_u}-1)2^{e_u})(\prod_{v\in son_u}(f_v+2^{E_v+1}))\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
const int md=1000000007;
int n,m,i,u,v,t,tf=1,tim,ans;
int c[maxn],d[maxn],e[maxn],te[maxn];
int h[maxn],f[maxn],hf[maxn],dp[maxn];
int dfn[maxn],low[maxn],pw[maxn*3];
struct edge{int to,nxt;}E[maxn<<1],F[maxn<<2];
bool br[maxn<<1],vis[maxn<<1];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
void tarjan(int p,int fe){
    dfn[p]=low[p]=++tim;
    int lp,to;
    for(lp=hf[p];lp;lp=F[lp].nxt){
        if((lp>>1)==fe) continue;
        to=F[lp].to;
        if(!dfn[to]){
            tarjan(to,lp>>1);
            if(low[to]>dfn[p]) br[lp>>1]=1;
            else low[p]=min(low[p],low[to]);
        }
        else if(dfn[to]<dfn[p]) low[p]=min(low[p],dfn[to]);
    }
}
void col(int p,int fe){
    ++d[m]; c[p]=m;
    int lp,to,le;
    for(lp=hf[p];lp;lp=F[lp].nxt){
        if(vis[le=lp>>1]) continue;
        to=F[lp].to; 
        ++e[m]; vis[le]=1;
        if(!c[to]) col(to,le); 
    }
}
void dfs(int p,int fa){
    te[p]=e[p]; 
    int lp,to,s1=pw[e[p]+d[p]],s2=0;
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to;
        if(to==fa) continue;
        dfs(to,p); te[p]+=te[to]+1;
    }
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to; if(to==fa) continue;
        Add(s2,(1LL*f[to]*pw[te[p]-te[to]-1])%md);
        s1=(1LL*s1*(f[to]+pw[te[to]+1]))%md;
    }
    Add(s1,md-pw[te[p]]);
    f[p]=dp[p]=s1; 
    Add(dp[p],md-s2); 
}
int main(){
    freopen("barrack.in","r",stdin);
    freopen("barrack.out","w",stdout);
    pw[0]=1;
    for(i=1;i<(maxn*3);++i) Add(pw[i],pw[i-1]+pw[i-1]);
    scanf("%d%d",&n,&m);
    while(m--){
        scanf("%d%d",&u,&v);
        F[++tf]={v,hf[u]}; hf[u]=tf;
        F[++tf]={u,hf[v]}; hf[v]=tf;
    } 
    tarjan(1,0); u=m=0;
    memcpy(vis,br,sizeof(br));
    for(i=1;i<=n;++i) if(!c[i]) ++m,col(i,0);
    for(i=2;i<=tf;i+=2){
        if(br[i>>1]){
            u=c[F[i].to]; v=c[F[i|1].to];
            E[++t]={u,h[v]}; h[v]=t;
            E[++t]={v,h[u]}; h[u]=t;
        }
    }
    tf>>=1; dfs(1,0);
    for(i=1;i<=m;++i) Add(ans,(1LL*dp[i]*pw[tf-te[i]])%md);
    printf("%d",ans);
}

P8868 比赛 Match

解法

通过 这篇题解给的代码 细化(?)了一下思路。

考虑某个区间 \([l,r]\) 对应的答案 \(f(l,r)=\sum_{i=l}^r\sum_{j=i}^r(\max_{k=i}^j a_k)(\max_{k=i}^jb_k)\)\(r\) 右移一位时会怎样变化。可以设 \(g(l,r)=\sum_{i=l}^r (\max_{j=i}^r a_j)(\max_{j=i}^r b_j)\),则 \(f(l,r)=\sum_{i=l}^r g(i,r)\),且每个 \(g(l,r)\)\(r\) 右移一位时只会增加 \((\max_{i=l}^{r+1} a_i)(\max_{i=l}^{r+1} b_i)\)。可以使用扫描线的思路同时处理 \(\forall l,g(l,r)\) 的值,对应的答案即为 \(g\) 的区间和。思路和 CWOI 16th April 2022 T3 的思路很像。

\(la=\min_{i=1}^{r-1}[a_r>\max_{j=i}^{r-1}a_j]i,lb=\min_{i=1}^{r-1}[b_r>\max_{j=i}^{r-1}b_j]i\),则新的 \(r\) 只会使 \([\min(la,lb),r]\) 的对应 \(g\) 值进行改动。记 \(A_{l,r}=\max_{i=l}^r a_i,B_{l,r}=\max_{i=l}^r b_i\),则具体来讲,在 \(r\) 变化的同时 \(\forall i\in[la,r],A_{l,r}\leftarrow a_i\)\(\forall i\in[lb,r],B_{l,r}\leftarrow b_i\);然后 \(\forall i,g(l,r)\leftarrow A_{l,r}B_{l,r}\)。此时对于某个 \(g\),其加上的 \(A_{l,r}B_{l,r}\) 其中可能有零到二项值满足对应系数为 \(A_{l,r-1}\)\(B_{l,r-1}\)。此时为方便维护对应 \(g\) 之和,可以使用线段树维护区间 \(\sum A,\sum B,\sum AB\),同时把每个 \(g\) 值写成 \(m_{ab}AB+m_aA+m_bB+m_s\) 的形式下传标记。在 \(r\) 右移时,会使用新的 \(a_r\)\(b_r\) 覆盖前面的一些 \(A\)\(B\),在之后又需要把 \([1,r]\) 区间的 \(m_{ab}\) 加上 \(1\)。在给节点的标记上覆盖新标记时,需要确保先执行 \(A,B\) 的覆盖操作,然后执行 \(m\) 的区间加操作,避免新的 \(m_a/m_b\) 和之前的 \(\sum A,\sum B\) 相关。

代码

(这种东西真的是能够在考场写 + 调的题目吗)++(% Ryyyyy 用分块做法 SC 唯三场切之一)

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const int maxn=250010;
int n,i,j,q,l,r,ta,tb;
int a[maxn],b[maxn],sa[maxn],sb[maxn];
ull ans[maxn];
struct query{
    int l,r,i;
    inline bool operator <(const query &a)const{return r<a.r;}
}Q[maxn];
struct sum{
    ull sa,sb,ss,sab;
    inline sum Add(const sum &a){
        return {sa+a.sa,sb+a.sb,
                ss+a.ss,sab+a.sab};
    } 
}; 
struct tag{
    ull ca,cb,ma,mb,ms,mab;
    inline bool Empty(){return !(ma||mb||ms||ca||cb||mab);}
    inline void Add(const tag &a){
        tag ret=*this;
        if(ca){
            ret.ms+=ca*a.ma;
            if(cb) ret.ms+=ca*cb*a.mab;
            else ret.mb+=ca*a.mab;
        }
        else{
            ret.ma+=a.ma;
            if(cb) ret.ma+=cb*a.mab; 
            else ret.mab+=a.mab;
        }
        if(cb) ret.ms+=cb*a.mb;
        else ret.mb+=a.mb;
        ret.ms+=a.ms;
        if(a.ca) ret.ca=a.ca;
        if(a.cb) ret.cb=a.cb; 
        *this=ret;
    }
};
struct seg{
    int l,r,m,len;
    sum sm; tag tg; 
}tr[maxn<<2];
#define l(p) tr[p].l
#define r(p) tr[p].r
#define m(p) tr[p].m
#define ls(p) p<<1
#define rs(p) p<<1|1
#define tg(p) tr[p].tg
#define sm(p) tr[p].sm
#define len(p) tr[p].len
void Build(int p,int l,int r){
    l(p)=l; r(p)=r;
    m(p)=(l+r)>>1;
    len(p)=r-l+1;
    if(l==r) return;
    Build(ls(p),l,m(p));
    Build(rs(p),m(p)+1,r);
}
inline void PushAdd(int p,const tag &t){
    tg(p).Add(t); 
    sum sp=sm(p),smp=sm(p);
    smp.ss+=t.ma*smp.sa
           +t.mb*smp.sb
           +t.ms*len(p)
           +t.mab*smp.sab;
    if(t.ca){
        smp.sa=t.ca*len(p);
        if(t.cb) smp.sab=t.ca*t.cb*len(p);
        else smp.sab=t.ca*sp.sb;
    }
    else{
        smp.sa=sp.sa;
        if(t.cb) smp.sab=t.cb*sp.sa;
        else smp.sab=sp.sab;
    }
    if(t.cb) smp.sb=t.cb*len(p);
    else smp.sb=sp.sb; sm(p)=smp;
}
inline void Pushdown(int p){
    if(tg(p).Empty()) return;
    PushAdd(ls(p),tg(p));
    PushAdd(rs(p),tg(p));
    tg(p)={0,0,0,0,0,0}; 
}
void Change(int p,int l,int r,const tag &t){
    if(l<=l(p)&&r>=r(p)){
        PushAdd(p,t);
        return;
    }
    Pushdown(p);
    if(l<=m(p)) Change(ls(p),l,r,t);
    if(r>m(p)) Change(rs(p),l,r,t);
    sm(p)=sm(ls(p)).Add(sm(rs(p)));
}
ull Query(int p,int l,int r){
    if(l<=l(p)&&r>=r(p)) return sm(p).ss;
    Pushdown(p); ull ret=0;
    if(l<=m(p)) ret=Query(ls(p),l,r);
    if(r>m(p)) ret+=Query(rs(p),l,r);
    return ret;
}
int main(){
    freopen("match.in","r",stdin);
    freopen("match.out","w",stdout);
    scanf("%d%d",&i,&n); Build(1,1,n);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    for(i=1;i<=n;++i) scanf("%d",b+i);
    scanf("%d",&q);
    for(i=1;i<=q;++i){
        scanf("%d%d",&l,&r);
        Q[i]={l,r,i};
    }
    sort(Q+1,Q+q+1);
    for(i=j=1;i<=n;++i){
        while(ta&&a[sa[ta]]<a[i]) --ta;
        while(tb&&b[sb[tb]]<b[i]) --tb;
        Change(1,sa[ta]+1,i,{a[i],0,0,0,0,0});
        Change(1,sb[tb]+1,i,{0,b[i],0,0,0,0});
        Change(1,1,i,{0,0,0,0,0,1}); sa[++ta]=sb[++tb]=i;
        for(;Q[j].r==i;++j) ans[Q[j].i]=Query(1,Q[j].l,i);
    }
    for(i=1;i<=q;++i) printf("%llu\n",ans[i]);
    return 0;
} 

后记:NOIP2022 的暴力分真的比较多。T1 不挂分基本稳 100(然而不少人挂分了),T2 爆搜 + \(k=2n-2\) 送分部分 35,T3 指数级枚举 35,T4 考虑序列随机的性质,在单调栈上跳 + 归并 36(如果数据水能拿 52),SC rk 25 左右。不过应该在考场上想一些题的正解/特殊性质也不至于只有暴力分()例如把 T3 的正解(树上部分分)想出来,T4 结合莫队的思想想出 52 分做法,(不挂分)基本上 SC 前 5 左右。所以特殊性质等内容还是很有必要想的()。

posted @ 2022-11-27 20:36  Fran-Cen  阅读(403)  评论(0编辑  收藏  举报