Skip to content

20231019 B 组模拟赛 题解

前言

暂时先写 T4(因为是推式子题),其它的等有时间了再补。

密码是成外通用密码,大小写敏感

T1

这道题做法贼多,我这里说一下我的 $O(n\log n)$ exKMP 做法,应该是带 $\log$ 的做法中跑的最快的。

约定:字符串下标从 $0$ 开始。

先求出 exKMP 的 $nxt$ 数组,容易发现每个点的决策只有两种,分别是加上一个字符复制一遍后删掉若干个字符,且在 $i$ 处至少要删掉 $i-nxt_i$ 个(不然就和 $S$ 不匹配了)。

那么容易想到 DP,设 $dp_{i}$ 表示匹配完 $[0,i)$ 的最小代价,根据刚才的结论,转移有两种:

$$ dp_i\gets dp_{i-1}+t_{s_{i-1}}\\ dp_i\gets \min_{j<i}^{j+nxt_j\ge i}\begin{Bmatrix}dp_j+t_1+t_2\times (2j-i)\end{Bmatrix} $$

然后第二个转移可以用优先队列维护,就做完了。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
using pii=pair<i64,i64>;
#define fi first
#define se second
#define mkp make_pair
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=1e6+5,inf=1e18;
i64 n,a[N],cs[26],t1,t2;
char str[N];
i64 nxt[N],dp[N],ff[N];
void exKMP(){
    nxt[0]=0;
    i64 j=1,p=0;
    while(str[p]==str[p+1]) ++p;
    nxt[1]=p;
    forup(i,2,n-1){
        if(i>=j+p){
            p=0;
            while(str[p]==str[p+i]) ++p;
            nxt[i]=p;j=i;
            continue;
        }
        if(nxt[i-j]<j+p-i){
            nxt[i]=nxt[i-j];
        }else{
            p=j+p-i;
            while(str[p]==str[i+p]) ++p;
            nxt[i]=p;j=i;
        }
    }
}
struct cmp{
    bool operator ()(pii a,pii b){
        return a.fi+a.se*t2>b.fi+b.se*t2;
    }
};
priority_queue<pii,vector<pii>,cmp> q;
i64 gval(pii nw,i64 i){
    return nw.fi+(nw.se-i)*t2;
}
signed main(){
    scanf(" %s",str);
    n=strlen(str);
    forup(i,0,25) cs[i]=read();
    forup(i,0,n-1) a[i]=cs[str[i]-'a'];
    forup(i,0,n) dp[i]=inf;
    t1=read();t2=read();
    exKMP();
    dp[0]=0;
    forup(i,0,n-1){
        while(q.size()&&q.top().se<i) q.pop();
        if(q.size()){
            pii nw=q.top();
            dp[i]=min(dp[i],gval(nw,i));
        }
        dp[i+1]=min(dp[i+1],dp[i]+a[i]);
        i64 pn=min(nxt[i],i);
        if(pn!=0){
            q.push(mkp(dp[i]+t1+t2*(i-pn),i+pn));
        }
    }
    while(q.size()&&q.top().se<n) q.pop();
    if(q.size()){
        pii nw=q.top();
        dp[n]=min(dp[n],gval(nw,n));
    }
    printf("%lld\n",dp[n]);
}

T2

很奇妙的一道题,我赛时臆想了一个做法,就是离散化后跑 DDP,但是没想出怎么维护区间操作就跳过这道题了。希望有有缘人可以尝试一下。

我赛后写的官方题解的做法。

首先容易发现每个连通的整块只有两种涂色方法,且两种涂法黑白两色的差值是一样的。那么我们可以考虑只维护差值 $diff$,然后黑白的答案就分别是 $\frac{sum \pm diff}{2}$。

那么我们就要维护两个东西,分别是连通块区间内黑白色差值,其中连通块可以用 ODT 维护,这个很简单就不具体讲了。我们现在重点讲第二个。

首先,由于两种情况差值相同,我们不妨给每一个点钦定一个颜色(黑白交替),其中奇数列奇数行为白色,偶数行为黑色,偶数列反之,那么我们要维护区间深度加 $d$,考虑区间加 $d$ 会对差值 黑色 $-$ 白色 产生什么影响。

首先,若 $d$ 是偶数,那么相当于在下面加偶数行块,容易发现这一块的差为 $0$,而上面原有的不会变。故若 $d$ 为偶数可以直接不操作。

若 $d$ 为奇数,那么相当于区间内原有块黑白色交换,然后再加偶数行完整,再加一行。其中偶数行差为 $0$,不会产生影响,只考虑加的这一行反转原来的颜色

对于反转颜色,如果某一列深度为偶数,那么这一列不会产生额外影响。反之,如果是第奇数列,那么黑色 $+1$,白色 $-1$,差值 $+2$,若为偶数列则白色 $+1$ 黑色 $-1$,差值 $-2$。

然后考虑加的这一行,贡献显然是 偶数列数量 $-$ 奇数列数量。

那么我们可以维护区间内 深度为奇数的偶数列 $o_0$深度为奇数的奇数列 $o_1$区间内奇数列的数量 $c$ 来得出差值的变化。

那么设区间长度为 $l$,差值就是:

$$2o_1-2o_0+(l-2c)$$

而容易发现 $o_1-c$ 就是深度为偶数的奇数列的数量,那么我们可以改为维护 深度与下标奇偶性不同的列数 $o=(o_1-c)-o_0$,那么差值就是 $2o-l$,且修改后,$o$ 变为 $l-o$。

那么这个显然可以动态开点线段树维护,注意数组不要开小了,以及维护连通块的边界问题。

复杂度 $O(n\log n)$。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
using pii=pair<i64,i64>;
#define fi first
#define se second
#define mkp make_pair
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=1e5+5;
i64 n,m,sum,diff;
struct SegTree{
    #define mid ((l+r)>>1)
    #define lson l,mid,ls[id]
    #define rson mid+1,r,rs[id]
    private:
        i64 ls[N*80],rs[N*80],queryodd[N*80],querydif[N*80],mark[N*80],cntn=0;
        i64 New(i64 l,i64 r){
            i64 id=++cntn;
            if(l%2==1){
                queryodd[id]=(r-l+2)/2;
            }else{
                queryodd[id]=(r-l+1)/2;
            }
            return id;
        }
        void Modify(i64 id,i64 len){
            i64 s=queryodd[id];
            queryodd[id]=len-s;
            querydif[id]+=s*2-len;
            mark[id]^=1;
        }
        void PushUp(i64 id){
            queryodd[id]=queryodd[ls[id]]+queryodd[rs[id]];
            querydif[id]=querydif[ls[id]]+querydif[rs[id]];
        }
        void PushDown(i64 id,i64 l,i64 r){
            Modify(ls[id],mid-l+1);
            Modify(rs[id],r-mid);
            mark[id]=0;
        }
    public:
        void init(){
            New(1,n);
        }
        void Update(i64 L,i64 R,i64 l=1,i64 r=n,i64 id=1){
            if(L<=l&&r<=R){
                Modify(id,r-l+1);
                return;
            }
            if(!ls[id]) ls[id]=New(l,mid);
            if(!rs[id]) rs[id]=New(mid+1,r);
            if(mark[id]) PushDown(id,l,r);
            if(L<=mid) Update(L,R,lson);
            if(mid< R) Update(L,R,rson);
            PushUp(id);
        }
        i64 Query(i64 L,i64 R,i64 l=1,i64 r=n,i64 id=1){
            if(L<=l&&r<=R){
                return querydif[id];
            }
            if(!ls[id]) ls[id]=New(l,mid);
            if(!rs[id]) rs[id]=New(mid+1,r);
            if(mark[id]) PushDown(id,l,r);
            i64 res=0;
            if(L<=mid) res+=Query(L,R,lson);
            if(mid< R) res+=Query(L,R,rson);
            return res;
        }
}mt;
set<pii> odt;
signed main(){
    n=read();m=read();
    mt.init();
    forup(Case,1,m){
        i64 l=read(),r=read(),d=read();
        ++l;
        sum+=(r-l+1)*d;
        set<pii>::iterator st=odt.lower_bound(mkp(l-1,-1)),ed=odt.lower_bound(mkp(r,-1));
        i64 ll=l,rr=r;
        if(st!=odt.end()) ll=min(ll,st->se);
        if(ed!=odt.end()&&ed->se<=r+1){
            rr=max(rr,ed->fi);
            ++ed;
        }
        for(set<pii>::iterator it=st;it!=ed;odt.erase(prev(++it))){
            i64 rs=abs(mt.Query(it->se,it->fi));
            diff-=rs;
        }
        if(d%2==1) mt.Update(l,r);
        odt.insert(mkp(rr,ll));
        i64 rs=abs(mt.Query(ll,rr));
        diff+=rs;
        printf("%lld %lld\n",(sum-diff)/2,(sum+diff)/2);
    }
}

T3

我认为是全场最简单题。

首先一眼看到如果是树会很好做,那么考虑变成树。

容易发现边双有很好的性质,你发现边双内任意两点之间必定存在一条路径可以经过某·指定边,因为任意两点之间都有至少两条路径,然后随意构造一下大概就行了。

那么缩点之后直接跑换根 DP 就行了。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
using pii=pair<int,int>;
#define fi first
#define se second
#define mkp make_pair
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=2e5+5;
int n,m,ans[N];
struct edge{
    int u,v,p;
};
vector<edge> se;
vector<int> e[N],pt[N];
vector<pii> e1[N];
int csc,Tm,dfn[N],low[N],blg[N],ist[N],sf[N],sz[N];
stack<int> stk;
void Tarjan(int x,int fa){
    low[x]=dfn[x]=++Tm;
    ist[x]=1;stk.push(x);
    for(auto i:e[x]){
        if(i==fa) continue;
        if(!dfn[i]){
            Tarjan(i,x);
            low[x]=min(low[x],low[i]);
        }else if(ist[i]){
            low[x]=min(low[x],low[i]);
        }
    }
    if(dfn[x]==low[x]){
        ++csc;
        while(stk.top()!=x){
            blg[stk.top()]=csc;
            ist[stk.top()]=0;
            pt[csc].push_back(stk.top());sz[csc]++;
            stk.pop();
        }
        blg[x]=csc;
        ist[x]=0;
        pt[csc].push_back(x);sz[csc]++;
        stk.pop();
    }
}
int dp[N],pd[N],siz[N];
void dfs1(int x,int fa){
    siz[x]=sz[x];
    for(auto i:e1[x]){
        int v=i.fi;
        if(v==fa) continue;
        dfs1(v,x);
        siz[x]+=siz[v];
        if(sf[x]||sf[v]||i.se){
            dp[x]+=siz[v];
        }else{
            dp[x]+=dp[v];
        }
    }
}
void dfs2(int x,int fa){
    for(auto i:e1[x]){
        int v=i.fi;
        if(v==fa) continue;
        if(sf[v]){
            pd[v]=n-sz[v];
        }else if(sf[x]||i.se){
            pd[v]=n-siz[v]+dp[v];
        }else{
            pd[v]=pd[x];
        }
        dfs2(v,x);
    }
}
signed main(){
    n=read();m=read();
    forup(i,1,m){
        int u=read(),v=read(),p=read();
        se.push_back(edge{u,v,p});
        e[u].push_back(v);
        e[v].push_back(u);
    }
    Tarjan(1,0);
    for(auto i:se){
        int u=i.u,v=i.v,p=i.p;
        if(blg[u]==blg[v]){
            if(p) sf[blg[u]]=1;
            continue;
        }
        u=blg[u],v=blg[v];
        e1[u].push_back(mkp(v,p));
        e1[v].push_back(mkp(u,p));
    }
    dfs1(1,0);
    pd[1]=dp[1];
    dfs2(1,0);
    forup(i,1,csc){
        for(auto j:pt[i]){
            if(sf[i]){
                ans[j]=pd[i]+sz[i]-1;
            }else{
                ans[j]=pd[i];
            }
        }
    }
    forup(i,1,n){
        printf("%d ",ans[i]);
    }
}

T4

首先把排列拆成环($i\to p_i$,比如排列 $3,5,4,1,2,6$ 就可以拆成三个环 $(3,4,1)(5,2)(6)$),为保证交换次数最小,只能再每个环内部交换,那么容易构造出每次用环上最小值当跳板和其他值交换,而这样显然是代价最小的,是最小值乘以其它值的和。那么整个排列的代价就是所有环的代价之和。

那么考虑枚举 $i$ 在所有情况下能作为环内最小值产生贡献的代价之和,易得以下式子:

$$\sum_{i=1}^{n-1}i\sum_{S\subseteq[i+1,n]}\left(\sum_{x\in S}x\right)|S|!(n-|S|-1)!$$

式子很好理解,前半部分枚举内容很显然,其中 $S$ 是去掉 $i$ 后环上点的集合。最后的 $(n-|S|-1)!$ 是环外的排列。$|S|!$ 比较复杂,考虑 $i\to p_i$ 的环上的顺序,我们钦定一个点断开后其余点排列方案数就是 $|S|!$,然后每个环都能唯一对应 $i\to p_i$ 的映射集合(显然是双射吧),故环内的方案数为 $|S|!$。

然后容易发现如果要枚举集合 $S$ 显然不可做,考虑枚举集合大小 $j=|S|$,然后每个 $x\in[i+1,n]$ 都能在 $\binom{n-i-1}{j-1}$ 个不同的 $S$ 中产生贡献,故上式可化为这样:

$$\sum_{i-1}^{n-1}\sum_{j=1}^{n-i}i\dfrac{(n+i+1)(n-i)}{2}\dbinom{n-i-1}{j-1}j!(n-j-1)!$$

不会连等差数列求和公式都看不懂吧?

容易看到中间有个组合数,旁边还有一堆阶乘,考虑把组合数拆成阶乘再化简:

$$\sum_{i-1}^{n-1}\sum_{j=1}^{n-i}i\dfrac{(n+i+1)(n-i)}{2}\times\dfrac{(n-i-1)!}{(j-1)!(n-i-j)!}j!(n-j-1)!$$

稍微约分一下:

$$\frac{1}{2}\sum_{i-1}^{n-1}i(n+i+1)(n-i)!\sum_{j=1}^{n-i}\dfrac{j(n-j-1)!}{(n-j-i)!}$$

这个式子虽然变好看了,但仍然需要 $O(n^2)$ 才能做,考虑拆掉后面的 $\sum$,但是为了消掉它,我们有一个套路,需要把它拆成难看的组合数。

$$ \begin{aligned} &\sum_{j=1}^{n-i}\dfrac{j(n-j-1)!}{(n-j-i)!}\\ =&(i-1)!\sum_{j=1}^{n-i}\dfrac{j(n-j-1)!}{(n-j-i)!(i-1)!}\\ =&(i-1)!\sum_{j=1}^{n-i}\dbinom{n-j-1}{i-1}\dbinom{j}{1} \end{aligned} $$

这里有一个套路,我们把它放到一个更一般的情况:


$$\sum_{x=d-c}^{a-b}\dbinom{a-x}{b}\dbinom{c+x}{d}=\dbinom{a+c+1}{b+d+1}$$

证明:

考虑组合意义,即在两段长度分别为 $a-x,c+x$ 的序列上分别找 $b,d$ 个点的方案数。

那么容易发现两段序列长度的和是固定的 $a+c$,我们再在中间塞一个分界点,且钦定它被选,左边恰好有 $b$ 个被选,右边有 $d$ 个(其实就是在选出来的所有 $b+d+1$ 个点中第 $b+1$ 个),那么一共就要选 $b+d+1$ 个,总方案数就是 $\binom{a+c+1}{b+d+1}$。

为什么这样就是对的呢?考虑分界点的移动,容易发现它最左为 $b+1$,最右为 $a+b-d$,恰好分别是 $a-x,c+x$ 的下界。换句话说,这样选恰好在 $\sum$ 约束的范围内算到了所有 $x$ 取值的方案数总和。


那么把这个情况放回式子中,若 $a=n-1,b=i-1,c=0,d=1$,那么 $x$ 的范围为 $[1,n-i]$,这不巧了吗,恰好是 $j$ 的范围。那么我们刚刚的式子恰好完美满足优化条件,可以化成这样:

$$(i-1)!\dbinom{n}{i+1}$$

再塞回原来的式子:

$$ \begin{aligned} &\frac{1}{2}\sum_{i-1}^{n-1}i(n+i+1)(n-i)!(i-1)!\dbinom{n}{i+1}\\ =&\frac{1}{2}\sum_{i-1}^{n-1}i!(n+i+1)(n-i)!\dfrac{n!}{(i+1)!(n-i-1)!}\\ =&\frac{1}{2}\sum_{i-1}^{n-1}\dfrac{(n+i+1)(n-i)n!}{i+1} \end{aligned} $$

那么我们就能得到一个又好算又好看的式子了。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int mod=1e9+7;
int n,ans,fn;
int inv[10000007];
signed main(){
    n=read();fn=1;
    forup(i,1,n){
        fn=1ll*fn*i%mod;
    }
    inv[1]=1;
    forup(i,2,n) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
    forup(i,1,n-1){
        (ans+=1ll*(n+i+1)*(n-i)%mod*fn%mod*inv[i+1]%mod)%=mod;
    }
    ans=1ll*inv[2]*ans%mod;
    printf("%d\n",ans);
}

Comments