Atcoder Grand Contest 054 题解
阅读原文时间:2023年07月10日阅读:3

那天晚上由于毕业晚会与同学吃饭喝酒没打 AGC,第二天稍微补了下题,目前补到了 E,显然 AGC 的 F 对于我来说都是不可做题就没补了(bushi

A

简单题,不难发现如果我们通过三次及以上的操作将这个串消完,那么我们完全可以把它压缩到两次以内,因此如果两段字符不同答案就是 \(1\),否则我们枚举分割点然后判断分割点两段是否都可以一次消完,如果存在这样的分割点答案就是 \(2\),否则答案为 \(-1\)。

B

注意到如果我们将原序列分成和相等的两部分并两部分将它们排成一列,那么有且只有一种排列方式对应出来是这个序列,具体证明就哪边小就往哪边塞即可,这个很好理解,因此我们可以设 \(dp_{i,j,k}\) 表示在前 \(i\) 个数里面选择了 \(k\) 个数,和为 \(j\) 的方案数,转移就正常按照背包的套路转移即可,记 \(S=\sum\limits_{i=1}^nw_i\),那么最终答案即为 \(\sum\limits_{i=1}^{n-1}dp_{n,S/2,i}·i!·(n-i)!\)。时间复杂度 \(\mathcal O(n^4)\)

C

带家好,这就是一个猜出结论然后没敢写的 sb,晚上交了一发,竟然过了

知乎习惯:先抛结论,再给证明。记 \(S\) 为满足逆序对数为 \(k\) 的下标的集合,那么答案即为 \(\prod\limits_{x\in S}(n-x+1)\)

证明:这是我自己 yy 出来的一个证明,错了不要打我

首先考虑对于一个固定的排列 \(p\),最优策略长什么样,显然每次交换最多使得某个 \(p_i\) 的逆序对数减 \(1\),我们记 \(p_i\) 的逆序对数为 \(x_i\),故交换次数有下界 \(\sum\limits_{i=1}^n\max(x_i-k,0)\),而如果我们每次都找到一个满足 \(x_i>k\) 且 \(p_{i-1}>p_i\) 的下标 \(i\) 并交换 \(p_{i-1},p_i\),那么每次交换都能使 \(x_i>k\) 的逆序对数恰好 \(-1\),也就能达到这个下界。那么问题又来了,是否每次都能找到这样的下标 \(i\) 呢?答案是肯定的,考虑反证法,如果对于所以 \(x_i>k\) 的 \(i\) 都有 \(p_{i-1}k\),同理可得 \(p_{i-2}k\),如此一直推下去可以推得 \(p_1<p_2<p_3<\cdots<p_i\),即 \(x_i=0\),而 \(k\ge 0\),矛盾!因此我们有了以下算法:

Algorithm. 每次找到一个满足 \(x_i>k,p_{i-1}>p_i\) 的下标 \(i\) 并交换 \(p_{i-1},p_i\),如此进行下去直到不存在 \(x_i>k\) 即可。

接下来考虑原问题,假设满足 \(x_i=k\) 的下标集合为 \(S=\{i_1,i_2,\cdots,i_k\}(i_m<i_{m+1},m\in[1,k-1])\),那么我们只需证明对于任意将 \(S\) 中的元素相对位置向右移的方案都有它还原出来的排列为 \(P'\)。

由于对于最终排列 \(P'\) 而言都有 \(x_i\le k\),因此 \(p'_{i_t}\) 必然小于 \(p'_{i_t+1},p'_{i_t+2},\cdots,p'_n\),否则假设 \(p'_ji_t\),那么 \(j\) 的逆序对数大于 \(x_{i_t}=k\),矛盾。又 \(i_mk\) 且 \(\forall j\in[i_1,v-1],p_j>p_{i_1}\),因此我们会一直向左移动 \(i_1\) 直到它的逆序对数 \(=k\) 为止,而显然这个终止位置就是最后的 \(i_1\),如此继续下去,还原 \(i_2,i_3,\cdots,i_m\) 即可得到排列 \(P'\)(当然我这里证明比较玄乎,如果严谨证明的话可用归纳法表述)

时间复杂度 \(\mathcal O(n\log n)\)

最慢的一个点跑了 7ms,也太慢了吧(说反话 ing

D

神仙题。

首先看到最少交换次数可以自然想到 \(dp\),\(dp_{i,j,k,l}\) 表示填了 \(i\) 个 (,\(j\) 个 ),\(k\) 个 o,\(l\) 个 x 的最少交换次数,根据这题的套路,最小的交换次数就是原序列与交换后的序列中相对位置发生改变的字符对数,也就是说加入一个字符后对交换次数的贡献是可以 \(\mathcal O(1)\) 求出来的,然鹅并没有什么卵用,这个状态就已经达到了 \(n^4\),一脸过不去。

于是我们不妨来分析一下这个东西有什么性质,看到括号序列我们可以很自然地将左括号当作 \(1\),右括号当作 \(-1\),那么我们可以将这四个字符分为两类,()(表示的数为 \(0\))和 ox(对应的数为 \(0\)),那么可以发现以下性质:

Observation 1. 我们显然不会交换 ox

证明:如果我们交换的 ox 这类字符中有 o,由于 o 展开来是 (),本来就是一个合法的字符串,因此 o 的位置并不影响括号串的合法性,而如果我们交换的两个字符都是 x 那我们交换它们干嘛呢。

这样一来我们假设括号序列的顺序为 \(S\),那么括号序列内部的交换次数显然是可以扫一遍求出的,而 ()ox 两类字符对答案的贡献是可以 \(dp\) 的,\(dp_{i,j}\) 表示填好了 \(S\) 的前 \(i\) 位和 ox 的序列的前 \(j\) 位的最少交换次数,这个 \(dp\) 显然是可以在常数时间内转移的,注意如果括号序列前缀和为 \(0\) 就不能放 x。因此这部分时间复杂度是 \(\mathcal O(\dfrac{n^2}{4})\) 的,没有问题,也就是说如果我们能确定 \(S\) 的顺序那这题就做完了。

那怎么确定 \(S\) 的顺序呢?

Observation 2. 最终的括号序列一定是在原括号顺序的前提下,使用最少交换次数交换求得的括号序列

说人话,就是如果原来左括号右括号顺序是 \(S'\),那么设我们通过最少次交换使 \(S'\) 变成合法括号序列后,\(S'\) 变成了 \(S\),那么 \(S\) 就是最终括号串的顺序。

为什么?这里再给出一个更不严谨的证明。不难发现我们交换括号序列的原因只有一点,就是使得不能放 x 的地方可以放 x,但事实上我们与其交换这些括号,还不如交换这些 x 们,把它们放到括号中来得更优,比方说 ()xxxx(),如果我们交换括号的话要将最后一个 x 后面的 ( 移动五格,而如果我们移动 x 只要将每个 x 向右移动 \(1\) 格,总共四步,因此我们大可不必花这些精力移动括号。(真是玄乎到不能再玄乎了呢,哪位好心的鸽鸽知道这东西的 proof 麻烦来教教蒟蒻呗/kel)

于是这题就真的做完了(

const int MAXN=8e3;
int n,n1,n2,ans=0,ps1[MAXN+5],ps2[MAXN+5];
char s[MAXN+5],s1[MAXN+5],s2[MAXN+5];
int dp[MAXN+5][MAXN+5],pre1[MAXN+5],pre2[MAXN+5],preo[MAXN+5],prex[MAXN+5];
int sum1[MAXN+5],sum2[MAXN+5],sumo[MAXN+5],sumx[MAXN+5];
int main(){
    scanf("%s",s+1);n=strlen(s+1);
    for(int i=1;i<=n;i++) (s[i]>97)?(s2[++n2]=s[i],ps2[n2]=i):(s1[++n1]=s[i]);
    for(int i=1;i<=n;i++){
        pre1[i]=pre1[i-1]+(s[i]=='(');
        pre2[i]=pre2[i-1]+(s[i]==')');
        preo[i]=preo[i-1]+(s[i]=='o');
        prex[i]=prex[i-1]+(s[i]=='x');
    } //cout<<s1+1<<endl;
    for(int i=1,s=0;i<=n1;i++){
        s+=1-((s1[i]==')')<<1);
        if(s<0){
            s=1;
            for(int j=i+1;j<=n1;j++) if(s1[j]=='('){
//                printf("%d %d\n",j,i);
                swap(s1[j],s1[i]);ans+=j-i;break;
            }
        }
    } memset(dp,63,sizeof(dp));dp[0][0]=0;
//    cout<<s1+1<<endl;printf("%d\n",ans);
    for(int i=1,p1=0,p2=0;i<=n1;i++){
        if(s1[i]=='('){
            do{p1++;} while(s[p1]!='(');
            ps1[i]=p1;
        } else {
            do{p2++;} while(s[p2]!=')');
            ps1[i]=p2;
        }
    }
    for(int i=1;i<=n1;i++){
        sum1[i]=sum1[i-1]+(s1[i]=='(');
        sum2[i]=sum2[i-1]+(s1[i]==')');
    }
    for(int i=1;i<=n2;i++){
        sumo[i]=sumo[i-1]+(s2[i]=='o');
        sumx[i]=sumx[i-1]+(s2[i]=='x');
    }
//    for(int i=1;i<=n1;i++) printf("%d%c",ps1[i]," \n"[i==n1]);
//    for(int i=1;i<=n2;i++) printf("%d%c",ps2[i]," \n"[i==n2]);
//    for(int i=1;i<=n;i++) printf("%d%c",pre1[i]," \n"[i==n]);
//    for(int i=1;i<=n;i++) printf("%d%c",pre2[i]," \n"[i==n]);
//    for(int i=1;i<=n;i++) printf("%d%c",prex[i]," \n"[i==n]);
//    for(int i=1;i<=n;i++) printf("%d%c",preo[i]," \n"[i==n]);
    for(int i=0,s=0;i<=n1;i++){
        for(int j=0;j<=n2;j++){
            if(i^n1) chkmin(dp[i+1][j],dp[i][j]+max(sumo[j]-preo[ps1[i+1]],0)+max(sumx[j]-prex[ps1[i+1]],0));
            if(j^n2&&(s||(s2[j+1]=='o'))) chkmin(dp[i][j+1],dp[i][j]+max(sum1[i]-pre1[ps2[j+1]],0)+max(sum2[i]-pre2[ps2[j+1]],0));
        } s+=1-((s1[i+1]==')')<<1);
    } printf("%d\n",ans+dp[n1][n2]);
//    for(int i=0;i<=n1;i++) for(int j=0;j<=n2;j++) printf("%d %d %d\n",i,j,dp[i][j]);
    return 0;
}

E

知乎好习惯:只抛结论,不给证明,因为我不会证明,Atcoder 题解也写得不是太详细

Observation. 以 \(p_1<p_n\) 的情况为例,序列合法当且仅当 \(\exists k\in[1,n-1]\) 满足 \(p_k\le p_1,p_{k+1}\ge p_n\)

证明不会

考虑怎样计算这个东西,咱们不妨从反面入手,拿总方案减去不合法的方案数,记 \(f(n,A)\) 表示 \(p_1=A,p_n>p_1\) 的不合法的序列的个数,那么 \(ans=(n-1)!-f(n,A)-f(n,n-A)\),因此我们只需求出 \(f(n,A)\) 即可。

考虑怎样求 \(f(n,A)\),我们假设 \(p_n=p_1+k\),那么 \((p_1,p_n)\) 这 \(k-1\) 个数随便填,\((k-1)!\) 种可能,当我们依次填入 \([1,p_1)\) 时,填入 \(i\) 时有 \(k-2+i\) 种填法,再依次填入 \((p_n+1,n]\) 时,填入 \(p_n+i\) 时有 \(k-2+i\) 种填法,乘在一起是 \((k-1)!\times\dfrac{(k-2+A-1)!}{(k-2)!}\times\dfrac{(k-2+n-A-k)!}{(k-2)!}=(A+k-3)!\times(n-A-2)!\times\dfrac{1}{(k-2)!}\times(k-1)\),因此填法总数就是 \(\sum\limits_{2\le k\le n-A}(A+k-3)!\times(n-A-2)!\times\dfrac{1}{(k-2)!}\times(k-1)=(n-A-2)!\sum\limits_{2\le k\le n-A}\dbinom{A+k-3}{k-2}(k-1)\),再推个柿子可以得到:

\[\begin{aligned}
ans&=(n-A-2)!\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}(k+1)\\
&=(n-A-2)!(\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}k+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k})\\
&=(n-A-2)!(\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k-1}A+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}))\\
&=(n-A-2)!(A·\sum\limits_{0\le k\le n-A-3}\dbinom{A+k}{A}+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{A-1})\\
&=(n-A-2)!(A·\dbinom{n-2}{A+1}+\dbinom{n-2}{A})
\end{aligned}
\]

大功告成。

搬 Atcoder 题解 ing

const int MAXN=2e6;
const int MOD=998244353;
int fac[MAXN+5],ifac[MAXN+5];
void init_fac(int n){
    for(int i=(fac[0]=ifac[0]=ifac[1]=1)+1;i<=n;i++) ifac[i]=1ll*ifac[MOD%i]*(MOD-MOD/i)%MOD;
    for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*ifac[i]%MOD;
}
int binom(int x,int y){
    if(x<0||y<0||x-y<0) return 0;
    return 1ll*fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;
}
int calc(int n,int A){
    return 1ll*fac[n-A-2]*fac[A-1]%MOD*((1ll*A*binom(n-2,A+1)+binom(n-2,A))%MOD)%MOD;
}
void solve(){
    int n,A;scanf("%d%d",&n,&A);
    printf("%d\n",(0ll+fac[n-1]-calc(n,A)-calc(n,n-A+1)+MOD+MOD)%MOD);
}
int main(){
    init_fac(MAXN);
    int qu;scanf("%d",&qu);while(qu--) solve();
    return 0;
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器