【合集】HZOI2024——冲刺NOIP2024

前言

喵喵于 \(2024.3.18\) 建立 \(vjudge\) 团队 \(NOIP2024\) ,成员为全体 \(HZOI2024\) 初三现役人员,旗下三个板块的专题训练,分别为动态规划、图论、字符串,其中题目非紫即黑,存在少量蓝。

并于 \(2024.3.19\) 成功关闭 \(luogu\)

接下来做的题我会按照开题顺序(大抵等同于从易到难)将 \(A\) 掉的题记录下来,同时可能有类似题的扩展。

动态规划专题

B - Birds

  • \(3.19\)

混合背包 \(DP\)

定义 \(f_{i,j}\) 表示取到鸟巢 \(i\) ,获得 \(j\) 只小鸟时所剩的魔力值。

显然有 \(f_{0,0}=1\)

转移为:

\[f_{i+1,j+k}=\max(f_{i+1,j+k},\min(f_{i,j}-k\times cost_i+x,w+(j+k)\times b)) \]

其中 \(k\) 表示对于鸟巢 \(i\) 取了几个鸟,其余变量意义与上述表达或题面相同。

特别的,有任意 \(f_{i+1,j+k}\leq w+(j+k)\times b\) ,又题意可得。

注意:

  • 将所有 \(f_{i,j}\) 初始化为 \(-1\) (表示没有更新过,而 \(0\) 可能是恰好为 \(0\) 并非未更新,会产生歧义),若 \(f_{i,j}\) 没有更新过,即他此时所剩魔力值 \(<0\) ,则无法更新 \(f_{i+1,j+k}\)

  • \(f_{i,j}-k\times cost_i<0\) 说明无法取这么多鸟,那么显然更大的 \(k\) 也无法取到,所以 \(break\)

  • 对于 \(j\) 应循环到 \(sum_i\)\(sum_i\) 表示 \(c_i\) 的前缀和,即最多取这么多鸟。

    同理的,\(k\) 循环到 \(c_i\)

    当然,\(j,k\) 均从 \(0\) 开始循环。

最后处理答案,显然我们取完鸟巢 \(n\) 后的答案将体现在 \(f_{n+1,j}\) 中,答案为所有 \(\geq 0\)\(f_{n+1,j}\)\(j\) 的最大值。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=1e3+10,M=1e4+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,w,b,x,ans,c[N],v[N],sum[N],f[N][M];
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(w),read(b),read(x);
    for(int i=1;i<=n;i++) 
        read(c[i]),
        sum[i]=sum[i-1]+c[i];
    for(int i=1;i<=n;i++) read(v[i]);
    memset(f,-1,sizeof(f));
    f[0][0]=w;
    for(int i=0;i<=n;i++)
        for(int j=0;j<=sum[i];j++)
            for(int k=0;k<=c[i];k++)
            {
                int s=f[i][j];
                if(s<0) break;
                s-=k*v[i];
                if(s<0) break;
                s=min(s+x,w+(j+k)*b);
                f[i+1][j+k]=max(f[i+1][j+k],s);
            }
    for(int i=0;i<=sum[n];i++)
        if(f[n+1][i]>=0)
            ans=max(ans,i);
    cout<<ans;
}

I - Game on Sum (Easy Version)

  • \(3.20\)

此题为简单版,可以直接跑 \(DP\)

定义 \(f_{i,j}\) 表示进行了 \(i\) 轮,其中 \(Bob\) 选择加的有 \(j\) 轮时的分数。

\(x_i\) 表示 \(Alice\) 本轮选择的数。

  • 若选择加,则有本轮分数为 \(f_{i-1,j-1}+x_i\)

  • 若选择减,则有本轮分数为 \(f_{i-1,j}-x_i\)

显然 \(Bob\) 会选择 \(\min(f_{i-1,j-1}+x_i,f_{i-1,j}-x_i)\)

已知两者相加为定值,那么显然当两者相等时,\(\min(f_{i-1,j-1}+x_i,f_{i-1,j}-x_i)\) 最大。

所以 \(Alice\) 会选择两者相等时的情况,则有:

\[f_{i,j}=\min(f_{i-1,j-1}+x_i,f_{i-1,j}-x_i)=\dfrac{f_{i-1,j-1}+f_{i-1,j}}{2} \]

同时,显然有 \(f_{i,0}=0\)\(f_{i,i}=i\times k\)

最后答案为 \(f_{n,m}\)

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=2010,P=1e9+7;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int t,n,m,k,f[N][N],inv2;
int qpow(int a,int b)
{
    int ans=1;
    for(;b;b>>=1)
    {
        if(b&1) (ans*=a)%=P;
        (a*=a)%=P;
    }
    return ans;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(t);
    inv2=qpow(2,P-2);
    while(t--)
    {
        read(n),read(m),read(k);
        for(int i=1;i<=n;i++)
            for(int j=0;j<=min(i,m);j++)
                if(i==j) f[i][j]=(k*i)%P;
                else if(j==0) f[i][j]=0;
                else f[i][j]=(((f[i-1][j]+f[i-1][j-1])%P)*inv2)%P;
        cout<<f[n][m]<<endl;
    }
}

Game on Sum (Hard Version)

  • \(3.20\)

此题为困难版,将 \(n,m,t\) 的范围都大大增加,无法跑正常的 \(DP\)

所以去思考上面所述 \(DP\) 中关于答案的贡献。

不难发现,上述 \(DP\) 可以组成一个类似于杨辉三角的东西。

去考虑 \(f_{i,i}\) 对于答案的贡献,因为只有 \(f_{i,i}\) 在跑 \(DP\) 之前是确定的。

我们发现对于 \(f_{i,j}\) ,他将对 \(f_{i+1,j}\)\(f_{i+1.j+1}\) 产生 \(1\) 的贡献( \(1\) 指 $1\times $ 自身)。

那么以此类推,\(f_{i,i}\) 将对 \(f_{n,m}\) 产生 \(n-i\) 的贡献,其中选择 \(m-i\) 去加。

但同时如果从 \(f_{i,i}\) 去考虑的话,他还会给 \(f_{i+1,i+1}\) 产生 \(1\) 的贡献,但这里已经填好了,所以直接从 \(f_{i+1,i}\) 开始考虑即可,从 \(f_{i+1,i}\)\(f_{n,m}\) 要对 \(n\) 产生 \(n-i-1\) 次贡献,从中选择 \(m-i\) 次对 \(m\) 产生贡献。

也就是 \(f_{i,i}\)\(f_{n,m}\) 的贡献为 \(\dfrac{i\times k\times \text{C}_{n-i-1}^{m-i}}{2^{n-i}}\)

这个 \(2^{n-i}\) 显然,每次都是要 \(÷2\) 的,而 \(i\times k\) 表示 \(f_{i,i}\) 自身。

由此最后的答案就为:

\[\sum\limits_{i=1}^{m}\dfrac{i\times k\times \text{C}_{n-i-1}^{m-i}}{2^{n-i}} \]

至于只循环到 \(m\) ,因为 \(Bob\) 只会选择 \(m\) 轮去加。

关于代码:首先需要预处理阶乘与每个 \(2^i\),乘法逆元可用费马小定理 \(+\) 快速幂,因为预处理需要到 \(1e9\) 显然会炸。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=1e6+10,P=1e9+7;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int t,n,m,k,jc[N],inv2[N];
int qpow(int a,int b)
{
    int ans=1;
    for(;b;b>>=1)
    {
        if(b&1) (ans*=a)%=P;
        (a*=a)%=P;
    }
    return ans;
}
void pre()
{
    jc[0]=jc[1]=1;
    for(int i=2;i<=N-1;i++) jc[i]=(jc[i-1]*i)%P;
    inv2[0]=1,inv2[1]=2;
    for(int i=2;i<=N-1;i++) inv2[i]=(inv2[i-1]*2)%P;
}
int C(int m,int n)
{
    if(m==0||n==m) return 1;
    return (((jc[n]*qpow(jc[m],P-2))%P)*qpow(jc[n-m],P-2))%P;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    pre();
    read(t);
    while(t--)
    {
        read(n),read(m),read(k);
        if(n==m)
        {
            cout<<(n*k)%P<<endl;
            continue;
        }
        int ans=0;
        for(int i=1;i<=m;i++)
            (ans+=(((((i*k)%P)*C(m-i,n-i-1))%P)*qpow(inv2[n-i],P-2))%P)%=P;
        cout<<ans<<endl;
    }
}

切切糕

  • \(3.21\)

  • 多倍经验。

这个乏味范围是小的,\(DP\) 即可。

贪心思想,\(Tinytree\) 会将优先权给尽可能大的糕,所以将 \(a_i\) 从大到小排序。

当其拥有优先权时,设 \(Kiana\) 会将 \(a_i\) 分成 \(x_i\)\(a_i-x_i\) ,那么显然 \(Tinytree\) 会将 \(\min(x_i,a_i-x_i)\)\(Kiana\)

定义 \(f_{i,j}\)\(Kiana\) 在分完第 \(i\) 块,其中 \(Tinytree\) 用了 \(j\) 次优先权时分到的蛋糕大小。

与上面类似的,使 \(\min(f_{i,j-1}+x_i,f_{i,j}+a_i-x_i)\) 最大,有:

\[f_{i,j-1}+x_i=f_{i,j}+a_i-x_i=\dfrac{f_{i,j-1}+f_{i,j}+a_i}{2} \]

最后答案为 \(sum-f_{n,m}\)\(sum\)\(\sum\limits_{i=1}^na_i\)

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=2510;
const double eps=1e-12;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m;
double a[N],sum[N],f[N][N];
bool cmp(double a,double b) {return a-b>eps;}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    for(int i=1;i<=n;i++) cin>>a[i];
    stable_sort(a+1,a+1+n,cmp);
    for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=min(i,m);j++)
            if(j==0) f[i][j]=0;
            else if(i==j) f[i][j]=sum[i]/2.0;
            else f[i][j]=max((f[i-1][j-1]+f[i-1][j]+a[i])/2.0,f[i-1][j]);
    printf("%.6f",sum[n]-f[n][m]);
}

A - Helping People

  • \(3.24\)

    主要是 \(3.21\) 打的,当时由于某些纸张问题没调出来,而中间经历了 \(whk\) 考试与放假,所以相隔了 \(3\) 天。

树形 \(DP\)概率 \(DP\)

首先我们需要明确他问的是啥:

  • “好处”:所有人拥有的金额中的最大值。

  • “期望”:从期望的本质去想 ,其意义为 \(\sum\limits_{i=1}^nq_ix_i\) ,也就是每个最大指乘上他的概率再加一起。

可见他问的是最大值的期望,并非期望的最大值。

我们发现他每个区间要么包含要么不相交,所以可以将其转化为一个树的结构去跑树形 \(DP\) ,类似于线段树的一个结构,每个节点表示一个区间。

当然他可能是个森林,所以再加一个 \([1,n]\) 的区间,对应概率为 \(0\) 的节点作为根节点。

那么我们将他按照区间长度从大到小排序,就可以简单的简称一棵树。

定义 \(a_i\) 为第 \(i\) 个人的初始值,对于区间 \(i\)\(a\) 的最大值 为 \(mx_i\) 。不难发现这个区间最后的最大值 \(\in {mx_i\sim mx_i+m}\)

那么我们定义 \(f_{i,j}\) 为对于区间 \(i\) 的最大值 \(\leq mx_i+j\) 时的概率,对此有转移方程:

\[f_{i,j}=q_i\times f_{son_i,j-mx_{son_i}+mx_i-1}+(1-q_i)\times f_{son_i,j-mx_{son_i}+mx_i} \]

最后答案为 \(\sum\limits_{i=0}^m(f_{1,i}-f_{1,i-1})\times (i+mx_1)\) ,因为显然第 \(1\) 个区间为 \([1,n]\) 。当然 \(0\) 要特判。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=1e5+10,M=5010;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,mx[N][20];
double ans,f[M][M];
vector<int>son[N];
struct aa
{
    int l,r,mx;
    double p;
}e[M];
bool cmp(aa a,aa b) {return a.r-a.l>b.r-b.l;}
void init()
{
    for(int j=1;j<=log2(n);j++)
        for(int i=1;i<=n-(1<<j)+1;i++)
            mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]);
}
int ask(int l,int r)
{
    int t=log2(r-l+1);
    return max(mx[l][t],mx[r-(1<<t)+1][t]);
}
void build()
{
    stable_sort(e+1,e+1+m,cmp);
    for(int i=1;i<=m;i++)
        for(int j=i-1;j>=1;j--)
            if(e[j].l<=e[i].l&&e[j].r>=e[i].r)
            {
                son[j].push_back(i);
                break;
            }
}
void dfs(int i)
{
    f[i][0]=1-e[i].p;
    for(int j:son[i]) 
        dfs(j),
        f[i][0]*=f[j][min(m,e[i].mx-e[j].mx)];
    for(int k=1;k<=m;k++)
    {
        double sum1=1,sum2=1;
        for(int j:son[i])
            sum1*=f[j][min(m,k-e[j].mx+e[i].mx-1)],
            sum2*=f[j][min(m,k-e[j].mx+e[i].mx)];
        f[i][k]=e[i].p*sum1+(1-e[i].p)*sum2;
    }
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    for(int i=1;i<=n;i++)   
        read(mx[i][0]);
    init();
    for(int i=1;i<=m;i++)
        read(e[i].l),read(e[i].r),
        cin>>e[i].p,
        e[i].mx=ask(e[i].l,e[i].r);
    e[++m].l=1,e[m].r=n,e[m].p=0,e[m].mx=ask(1,n);
    build();
    dfs(1);
    for(int i=0;i<=m;i++)
        ans+=(f[1][i]-(i==0?0:f[1][i-1]))*(i+e[1].mx);
    printf("%.9f",ans);
}

C - Positions in Permutations

  • \(3.26\)

\(DP+\) 容斥 \(+\) 组合计数

定义 \(f_{i,j,k,l}\) 为前 \(i\) 个数中有 \(j\) 个是好的,第 \(i\) 位和第 \(i+1\) 位被占用情况分别为 \(l,k\) (布尔)。

去思考转移方程,有:

\[f_{i,j,0,0}=f_{i-1,j,0,0}+f_{i-1,j,1,0}+f_{i-1,j-1,0,0} \]

\[f_{i,j,1,0}=f_{i-1,j,0,1}+f_{i-1,j,1,1}+f_{i-1,j-1,0,1} \]

\[f_{i,j,0,1}=f_{i-1,j-1,0,0}+f_{i-1,j-1,1,0} \]

\[f_{i,j,1,1}=f_{i-1,j-1,0,1}+f_{i-1,j-1,1,1} \]

按照 \(i-1,i,i+1\) 是否被选分别考虑即可,其中后两个因为选了 \(i+1\) 所以一定多了一个“好的”,就只有 \(j-1\) 的情况。

那么所有排列中至少有 \(i\) 个是“好的”的方案数就是 \(ans_i=(f_{n,i,1,0}+f_{n,i,0,0})\times (n-i)!\)

于是发现需要容斥。

思考 \(f_{n,i}\) 的贡献为 \(\text{C}_i^m\times ans_i\)\(m\leq i\leq n\)),于是通过容斥,有:

\[\sum\limits_{i=m}^n\times (-1)^{i-m}\times \text{C}_{i}^m\times ans_i \]

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
const int N=1010,P=1e9+7;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,f[N][N][2][2],jc[N],anss,ans[N],C[N];
int qpow(int a,int b)
{
    int ans=1;
    for(;b;b>>=1)
    {
        if(b&1) (ans*=a)%=P;
        (a*=a)%=P;
    }
    return ans;
}
void pre()
{
    jc[0]=jc[1]=1;
    for(int i=2;i<=N-1;i++)
        jc[i]=(jc[i-1]*i)%P;
    C[m]=1;
    for(int i=m+1;i<=n;i++)
        C[i]=(((C[i-1]*i)%P)*qpow(i-m,P-2))%P;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    pre();
    f[1][1][0][1]=f[1][0][0][0]=1;
    for(int i=2;i<=n;i++)
        for(int j=0;j<=i;j++)
        {
            (f[i][j][0][0]+=f[i-1][j][1][0]+f[i-1][j][0][0])%=P;
            if(j) (f[i][j][0][0]+=f[i-1][j-1][0][0])%=P;
            (f[i][j][1][0]+=f[i-1][j][0][1]+f[i-1][j][1][1])%=P;
            if(j) (f[i][j][1][0]+=f[i-1][j-1][0][1])%=P;
            if(i!=n&&j)
                (f[i][j][0][1]+=f[i-1][j-1][0][0]+f[i-1][j-1][1][0])%=P,
                (f[i][j][1][1]+=f[i-1][j-1][0][1]+f[i-1][j-1][1][1])%=P;
        }
    for(int i=m;i<=n;i++)
        ans[i]=(((f[n][i][1][0]+f[n][i][0][0])%P)*jc[n-i])%P;
    for(int i=m,t=0;i<=n;i++,t++)
        (anss+=(((qpow(-1,t)*C[i])%P)*ans[i])%P+P)%=P;
    cout<<anss;
}

F - ZS Shuffles Cards

  • \(3.30\)

    前几天去调模拟赛和分块了。

    感觉挺水的,为啥评 \(3000\)\(luogu\) 上都降紫了,个人感觉比 \(A\) 简单多了。

概率期望 \(DP\)

首先如果直接跑期望 \(DP\) 的话。

\(f_i\) 表示已经取了 \(i\) 张数字牌,还需要的期望,\(f_n=0\)

  • \(\dfrac{n-i}{n+m-i}\) 的概率取到新的牌。

  • \(\dfrac{m}{n+m-i}\) 的概率取到 joker ,此时就要从头开始了。

那么有:

\[f_i=\dfrac{n-i}{n+m-i}\times f_{i+1}+\dfrac{m}{n+m-i}\times f_0+1 \]

显然这个式子非常好想,但是发现不满足无后效性,不能递推实现,需要高斯消元,那么 \(O(n^3)\) 显然 \(TLE\) 了,而且及其难打。

所以需要转变思路。

不放将期望分成两个部分:

  • 轮数的期望。

  • 每轮所需秒数的期望。

那么显然这两个东西乘起来就是最后的答案。

  • 先求轮数的期望:

    定义 \(f_i\) 表示还需要取 \(i\) 张数字牌,换而言之就是已经取了 \(n-i\) 张数字牌时还需要的轮数的期望。

    \(f_0=1\) ,因为当所有数字牌都取完时,根据题意,还需要再取到一张 joker 才能结束,剩下的牌显然都是 joker 了,所以还需要 \(1\) 轮。

    发现是正着跑的,并非通常的倒着跑,其实没有太大的区别,只是这么写的话代码能少打几个字,仔细想的话,已经取了 \(i\) 张数字牌和还需要 \(i\) 张数字牌没有本质的区别。

    那么转移方程也非常好想:

    \[f_i=\dfrac{i}{m+i}\times f_{i-1}+\dfrac{m}{m+i}\times (f_i+1) \]

    化简为:

    \[f_i=f_{i-1}+\dfrac{m}{i} \]

    解释一下:

    • \(\dfrac{i}{m+i}\) 的概率取到新的牌。

    • \(\dfrac{m}{m+i}\) 的概率取到 joker ,显然就需要开启新的一轮了。

  • 每轮所需秒数的期望:

    • 第一种理解方法:

      我们发现取到的是哪一个数字牌不重要,重要的是取到的是数字牌。

      那么不放将这一堆数字牌看做一个,那么在取到 joker 前一个取到数字牌的概率就为 \(\dfrac{1}{m+1}\)

      不难发现该式子表示的就是对于每一个数字牌,在他后面开启新的一轮的概率。

      思考期望的定义:\(\sum\limits_{i=1}^nq_i\times x_i\) ,每一张牌他对秒数的贡献都为 \(1\) ,而在他后面开启新的一轮的概率为 \(\dfrac{1}{m+1}\)

      那么有每轮的秒数期望值 \(=1+\sum\limits_{i=1}^n 1 \times \dfrac{1}{m+1}=1+\dfrac{n}{m+1}\) ,至于为什么 \(+1\) ,取到 joker 也算一秒。

    • 第二种理解方法:

      我们知道如果对于这一秒他开启新的一局的概率为 \(\dfrac{1}{a}\) ,那么他这一局进行的秒数的期望就为 \(a\)

      那么对于这个场景,我们设他在第 \(i\) 秒结束,在第 \(i\) 秒时他取到 joker 的概率为 \(\dfrac{m}{n+m-(i-1)}\) ,那么取他的倒数,有:

      \[i=\dfrac{n+m-(i-1)}{m} \]

      解这个方程,有 \(i=\dfrac{n+m+1}{m+1}=1+\dfrac{n}{m+1}\)

最后答案就为 \(f_n\times (1+\dfrac{n}{m+1})\)

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=4e6+10,P=998244353;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,f[N];
int qpow(int a,int b)
{
    int ans=1;
    for(;b;b>>=1)
    {
        if(b&1) (ans*=a)%=P;
        (a*=a)%=P;
    }
    return ans;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    f[0]=1;
    for(int i=1;i<=n;i++)
        f[i]=(f[i-1]+(m*qpow(i,P-2))%P)%P;
    cout<<(f[n]*(1+(n*qpow(m+1,P-2))%P)%P)%P;
}

H - Tavas in Kansas

  • \(4.1\)

    周测没和喵喵请下来假。

分别以 \(s,t\) 为起点先跑两边 \(dijkstra\) ,处理出其到每个点的距离 \(d_{s,i},d_{t,i}\)

那么根据此我们可以知道每个点他距离 \(s,t\) 分别是第几远的,并将其离散化成 \(x_i,y_i\) ,由此形成一个 \(n\times n\) 矩阵。

Tavas每次取一行,Nafas每次取一列,那么现在问题就转化的不那么复杂了。

我们想要知道 TavasNafas 谁的得分高,并不需要知道其各自的具体分数,所以定义 \(f_{i,j,0/1}\) 分别表示目前 Tavas 取到第 \(i\) 行,Nafas 取到第 \(j\) 列时,TavasNafas 的得分差,其中 \(0\) 表示轮到 Tavas 取,\(1\) 表示轮到 Nafas 取。

于是在此遇到三个问题:

  • 最后答案是轮到谁的问题。

  • 取过的点不能再取的问题。

  • 每次必须取一个新点的问题。

一次解决这些问题:

  • 最后答案轮到谁?

    发现从前往后递推,到答案时我们需要处理出轮到谁。

    然而我们知道 Tavas 为先手,如果从后往前跑的话,本质上不会影响答案,且知道到答案时一定是轮到 Tavas ,所以我们选择从后往前递推。

  • 取过的点不能再取。

    我们现在已知他取到第 \(i\) 行第 \(j\) 列,也就是说在第 \(i\) 行第 \(j\) 列之前都已经取过了。

    • 那么对于 Tavas ,他可以取 \(i,j\)\(i,n\) 中的点。

    • 同样对于 Nafas ,她可以取 \(i,j\)\(n,j\) 中的点。

    问题解决。

  • 每次都要取到新点。

    根据我们上一个问题的分析,我们知道两人本次活动取什么范围内的点。

    首先该范围内的点一定是没有取过的。

    那么如果该范围存在点,接等同于存在新点,于是可以转移。

    不妨用一个新的变量处理每个范围内有几个点。

上面所说的一些均可以用二维前缀和维护。

转移方程:

\[f_{i,j,0}=\begin{cases} f_{i+1,j,0} & sum2(i,j,i,n)=0 \\ \max(f_{i+1,j,0},f_{i+1,j,1})+sum1(i,j,i,n) & sum2(i,j,i,n) \ne 0 \end{cases} \]

\[f_{i,j,1}=\begin{cases} f_{i,j+1,0} & sum2(i,j,n,j)=0 \\ \min(f_{i,j+1,0},f_{i,j+1,1})-sum1(i,j,n,j) & sum2(i,j,n,j) \ne 0 \end{cases} \]

因为我们 \(f\) 表示的是 Tavas 得分与 Nafas 得分的差,所以 Tavas 希望差尽可能大,Nafas 希望得分尽可能小。

其中 \(sum1(x1,y1,x2,y2)\) 表示从 \(x1,y1\)\(x2,y2\) 这一范围内权值和,\(sum2(x1,y1,x2,y2)\) 表示 \(x1,y1\)\(x2,y2\) 这一范围点的个数。

最后根据 \(f_{1,1,0}\) 的正负输出答案即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2010,M=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,s,t,p[N],x[N],y[N],d[N],dis[N],a[N][N],b[N][N],sum1[N][N],sum2[N][N],f[N][N][2];
bool v[N];
int head[N],to[M],w[M],nxt[M],tot;
void add(int x,int y,int z)
{
    nxt[++tot]=head[x];
    to[tot]=y;
    w[tot]=z;
    head[x]=tot;
}
void dijkstra(int s,int a[])
{
    memset(d,0x3f,sizeof(d));
    memset(v,0,sizeof(v));
    priority_queue<pair<int,int>>q;
    d[s]=0;
    q.push(make_pair(0,s));
    while(!q.empty())
    {
        int u=q.top().second;
        q.pop();
        if(!v[u])
        {
            v[u]=1;
            for(int i=head[u];i;i=nxt[i])
            {
                int v=to[i],z=w[i];
                if(d[v]>d[u]+z)
                    d[v]=d[u]+z,
                    q.push(make_pair(-d[v],v));
            }
        }
    }
    for(int i=1;i<=n;i++)
        dis[i]=d[i];
    sort(dis+1,dis+1+n);
    dis[0]=unique(dis+1,dis+1+n)-(dis+1);
    for(int i=1;i<=n;i++)
        a[i]=lower_bound(dis+1,dis+1+dis[0],d[i])-dis;
}   
int ask(int x,int y,int xx,int yy,int sum[N][N]) 
{
    return sum[xx][yy]-sum[x-1][yy]-sum[xx][y-1]+sum[x-1][y-1];
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m),read(s),read(t);
    for(int i=1;i<=n;i++) read(p[i]);
    for(int i=1,u,v,z;i<=m;i++)
        read(u),read(v),read(z),
        add(u,v,z),
        add(v,u,z);
    dijkstra(s,x),dijkstra(t,y);
    for(int i=1;i<=n;i++)
        a[x[i]][y[i]]+=p[i],
        b[x[i]][y[i]]++;
    for(int i=1;i<=n+1;i++)
        for(int j=1;j<=n+1;j++)
            sum1[i][j]=sum1[i-1][j]+sum1[i][j-1]-sum1[i-1][j-1]+a[i][j],
            sum2[i][j]=sum2[i-1][j]+sum2[i][j-1]-sum2[i-1][j-1]+b[i][j];
    for(int i=n+1;i>=1;i--)
        for(int j=n+1;j>=1;j--)
            if(i!=n+1||j!=n+1)
                f[i][j][0]=(ask(i,j,i,n,sum2)==0)?f[i+1][j][0]:max(f[i+1][j][0],f[i+1][j][1])+ask(i,j,i,n,sum1),
                f[i][j][1]=(ask(i,j,n,j,sum2)==0)?f[i][j+1][1]:min(f[i][j+1][0],f[i][j+1][1])-ask(i,j,n,j,sum1);
    if(f[1][1][0]<0) puts("Cry");
    if(f[1][1][0]==0) puts("Flowers");
    if(f[1][1][0]>0) puts("Break a heart");          
}

E - Bear and Cavalry

  • \(4.1\)

    关于大多数人都只做了五六道时就把所有题都讲了这件事。

    不出意外明天就要开字符串了。

结论题。

首先如果不考虑限制的话,将 \(w_i,h_i\) 都从小到大排序,显然有答案为 \(\sum\limits_{i=1}^nw_ih_i\)

接下来考虑不能骑自己马怎么搞。

结论:满足限制的匹配单元仅有以下 \(4\) 种:

image

先从 \(n=3\) 开始分析:

定义 \(ban_i\) 表示 \(i\) 的马。

  • \(ban_1\neq 1,ban_2\neq 2,ban3\neq 3。\)

    image

  • \(ban_1\neq 1,ban_2=2,ban_3=3。\)

    image

  • \(ban_1=1,ban_2=2,ban_3=3。\)

    image

    image

以此类以的分析,当 \(n>3\) 时,也只会产生上述 \(4\) 种匹配单元。

那么对于每次修改,至多对左右两边三个产生影响,有:

\[f_{i}=\max \begin{cases} f_{i-1}+w_{i}h_{i} \\ f_{i-2}+w_{i}h_{i-1}+w_{i-1}h_{i} \\ f_{i-3}+\max(w_{i}h_{i-1}+w_{i-1}h_{i-2}+w_{i-2}h_{i},w_{i}h_{i-2}+w_{i-1}h_{i}+w_{i-2}h_{i-1}) \end{cases} \]

如果暴力修改的话,发现会 \(TLE\) ,但是只 \(TLE\) 一点点,发现时限是 \(3000ms\) ,我们不卡常都对不起这个 \(3000ms\)

发现因为在转移时用了多个 \(if\) ,不放在每次转移前先将其 \(w_ih_i\) (以此类推)处理出来,能少好多 \(if\)

于是我们就能勉强通过此题,\(3000ms\) 的时限用了 \(2700ms\) ,甚至因为评测姬波动有时候还会 \(TLE\) ,不过没关系,多交几遍就 \(AC\) 了。

显然我们是卡常过的,并非正解。

所以负责任的将正解的题解放在这里:\(@wang54321\)的题解

懒得打正解了。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=3e5+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,posa[N],posb[N],ban[N],w[N][4],f[N];
struct aa
{
    int w,id;
}a[N],b[N];
bool cmp(aa a,aa b) {return a.w<b.w;}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    for(int i=1;i<=n;i++) 
        read(a[i].w),
        a[i].id=i;
    for(int i=1;i<=n;i++) 
        read(b[i].w),
        b[i].id=i;
    sort(a+1,a+1+n,cmp),sort(b+1,b+1+n,cmp);
    for(int i=1;i<=n;i++)
        posa[a[i].id]=posb[b[i].id]=i;
    for(int i=1;i<=n;i++)
        ban[i]=posb[a[i].id];
    for(int i=1;i<=n;i++)
    {
        w[i][1]=-0x3f3f3f3f;
        if(i-1>=0&&ban[i]!=i)
            w[i][1]=max(w[i][1],a[i].w*b[i].w);
        if(i-2>=0&&ban[i]!=i-1&&ban[i-1]!=i)
            w[i][2]=max(w[i][2],a[i].w*b[i-1].w+a[i-1].w*b[i].w);
        if(i-3>=0&&ban[i]!=i-2&&ban[i-1]!=i&&ban[i-2]!=i-1)
            w[i][3]=max(w[i][3],a[i].w*b[i-2].w+a[i-1].w*b[i].w+a[i-2].w*b[i-1].w);
        if(i-3>=0&&ban[i]!=i-1&&ban[i-1]!=i-2&&ban[i-2]!=i)
            w[i][3]=max(w[i][3],a[i].w*b[i-1].w+a[i-1].w*b[i-2].w+a[i-2].w*b[i].w);
    }
    for(int x,y,l,r;m;m--)
    {
        read(x),read(y);
        swap(ban[posa[x]],ban[posa[y]]);
        l=max(1ll,posa[x]-3),r=min(n,posa[x]+3);
        for(int i=l;i<=r;i++)
        {
            w[i][1]=w[i][2]=w[i][3]=-0x3f3f3f3f;
            if(i-1>=0&&ban[i]!=i)
                w[i][1]=max(w[i][1],a[i].w*b[i].w);
            if(i-2>=0&&ban[i]!=i-1&&ban[i-1]!=i)
                w[i][2]=max(w[i][2],a[i].w*b[i-1].w+a[i-1].w*b[i].w);
            if(i-3>=0&&ban[i]!=i-2&&ban[i-1]!=i&&ban[i-2]!=i-1)
                w[i][3]=max(w[i][3],a[i].w*b[i-2].w+a[i-1].w*b[i].w+a[i-2].w*b[i-1].w);
            if(i-3>=0&&ban[i]!=i-1&&ban[i-1]!=i-2&&ban[i-2]!=i)
                w[i][3]=max(w[i][3],a[i].w*b[i-1].w+a[i-1].w*b[i-2].w+a[i-2].w*b[i].w);
        }
        l=max(1ll,posa[y]-3),r=min(n,posa[y]+3);
        for(int i=l;i<=r;i++)
        {
            w[i][1]=w[i][2]=w[i][3]=-0x3f3f3f3f;
            if(i-1>=0&&ban[i]!=i)
                w[i][1]=max(w[i][1],a[i].w*b[i].w);
            if(i-2>=0&&ban[i]!=i-1&&ban[i-1]!=i)
                w[i][2]=max(w[i][2],a[i].w*b[i-1].w+a[i-1].w*b[i].w);
            if(i-3>=0&&ban[i]!=i-2&&ban[i-1]!=i&&ban[i-2]!=i-1)
                w[i][3]=max(w[i][3],a[i].w*b[i-2].w+a[i-1].w*b[i].w+a[i-2].w*b[i-1].w);
            if(i-3>=0&&ban[i]!=i-1&&ban[i-1]!=i-2&&ban[i-2]!=i)
                w[i][3]=max(w[i][3],a[i].w*b[i-1].w+a[i-1].w*b[i-2].w+a[i-2].w*b[i].w);
        }
        f[0]=0;
        for(int i=1;i<=n;i++)
        {
            if(i-1>=0) 
                f[i]=f[i-1]+w[i][1];
            if(i-2>=0) 
                f[i]=max(f[i],f[i-2]+w[i][2]);
            if(i-3>=0)
                f[i]=max(f[i],f[i-3]+w[i][3]);
        }
        cout<<f[n]<<endl;
    }
}

总结

动态规划专题到这儿就结束了,虽然还有 \(D,G\) 两道题设计未学过知识点的题没有做,实际上这个专题最早开还是有原因的,毕竟这东西涉及未学过知识点最少,且主要是锻炼思维,之前应该是从来没有做过这样难度的 \(DP\) ,同时尽可能的锻炼独立思考能力,能不 \(hè\) 坚决不 \(hè\) ,在这里入门晚、过知识点太仓促的缺陷也体现出来了,需要在今后的学习中努力弥补。

字符串专题

前言

没学过的知识点太多了,感觉 \(NOIP\) 题单不小心把 \(P\) 去了的感觉,后续开题的话有几道现学知识可做的,差不多就后续开题了。

I - Minimax

  • \(4.2\)

构造、思路题。

首先,明确 \(proper\) 前缀是什么意思。

分析题面可得,这东西和 \(kmp\) 求出来的 \(next\) 数组是一个东西。

首先此题 \(CF\) 评了 \(2100\) ,所以自然不是什么高级算法,暴力构造就好了。

思考几种情况:

  • 所有字母出现次数均为 \(1\)

    按照最小字典序输出即可。

    • \(eg\)vkcup

      \(ans\)ckpuv

  • 仅出现过一种字母:

    输出原字符即可。

    • \(eg\)zzzzzz

      \(ans\)zzzzzz

  • 若有一种字母仅出现过一次:

    将这个字母放在最前面,剩下的按照字典序输出。

    • \(eg\)aaabccc

      \(ans\)baaaccc

    解释:该字母仅有一个,那么将他放在最前面后面一定没有字母能与其匹配,从而使 \(f(t)=0\)

  • 每个字母出现次数均 \(\geq 2\)

    为了方便,我们设 a 为字典序最小的字母,b 为字典序第二小的字母,c 作为字典序第三小的字母,\(num_i\) 为字母 \(i\) 出现的次数,\(n\) 为字符串长度。

    首先明确,在此情况下无论如何,均可以使 \(f(t)=1\) ,很好理解的。

    • 最前面可以放 aa

      • \(eg\)abababa

        \(ans\)aababab

      首先这种情况的条件为 \(num_a-2\leq n-num_a\)

      因为首位放了 aa ,为了使 \(f(t)=1\) ,后面一定不能存在连续的 aa ,所以一定要有 \(n-num_a\) 个其他字母将 \(num_a-1\)\(a\) 隔开。

      这种情况的构造方式就是在除首位外其余地方不可出现连续的 aa 的字典序最小情况。

    • 最前面不可以放 aa

      也就是 \(num_a-2>n-num_a\) 的情况,此情况亦分为两种情况:

      • 仅有 \(2\) 种不同的字母:

        • \(eg\) aaababaabaaaa

          \(ans\)abbbaaaaaaaaa

        即先输出一个 a ,然后将 b 全部输出,再将剩余的 a 全部输出。

        解释:在此情况下,显然后面不可以出现连续的 ab ,于是有上述结论。

      • 存在 \(3\) 个及以上种不同的字母:

        • \(eg\)aaaababaccdaada

          \(ans\)abaaaaaaaacbcdd

        即先输出一个 a ,在输出一个 b ,再将所有的 a 输出,再输出一个 c ,再将所有的 b 输出,剩余的按照字典序输出。

        解释:即用一个 cab 隔开。

按照上述规则构造即可。

本次的代码比较屎,懒得优化了。
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e5+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int t,n,num[26],sum,id,id2,abc;
string s;
bool used;
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(t);
    while(t--)
    {
        memset(num,0,sizeof(num));
        abc=sum=used=id=id2=0;
        cin>>s;
        n=s.size();
        for(int i=0;i<n;i++)
        {
            num[s[i]-'a']++;
            if(num[s[i]-'a']==1) abc++;
        }
        if(abc==1)
        {
            cout<<s<<endl;
            continue;
        }
        int flag=-1;
        for(int i=0;i<26;i++)
            if(num[i]==1)
            {
                cout<<char(i+'a');
                flag=i;
                break;
            }
        if(flag!=-1)
        {
            for(int i=0;i<26;i++)
                if(i!=flag)
                    while(num[i]--)
                        cout<<char(i+'a');
            cout<<endl;
            continue;
        }
        for(int i=0;i<26;i++)
            if(num[i])
            {
                id=i;
                sum=num[i];
                break;
            }
        if(sum-2<=n-sum) 
        {
            cout<<char(id+'a')<<char(id+'a');
            sum-=2;
            used=1;
            n-=2;
            while(n--)
                if(!used&&sum) 
                    used=1,
                    cout<<char(id+'a'),
                    sum--;
                else
                {
                    used=0;
                    for(int i=id+1;i<26;i++)
                        if(num[i])
                        {
                            cout<<char(i+'a');
                            num[i]--;
                            break;
                        }
                }
            while(sum) cout<<char(id+'a'),sum--;
        }
        else 
        {
            if(abc==2)
            {
                cout<<char(id+'a');
                sum--;
                for(int i=id+1;i<26;i++)
                    while(num[i]--)
                        cout<<char(i+'a');
                while(sum--)
                    cout<<char(id+'a');
            }
            else 
            {
                cout<<char(id+'a');
                sum--;
                for(int i=id+1;i<26;i++)
                    if(num[i])
                    {
                        cout<<char(i+'a');
                        num[i]--;
                        flag=i;
                        break;
                    }
                while(sum--) cout<<char(id+'a');
                for(int i=flag+1;i<26;i++)
                    if(num[i])
                    {
                        cout<<char(i+'a');
                        num[i]--;
                        break;
                    }
                while(num[flag]--) cout<<char(flag+'a');
                for(int i=flag+1;i<26;i++)
                    while(num[i]--)
                        cout<<char(i+'a');
            }
        }
        cout<<endl;
    }
}

H - Scissors

  • \(4.3\)

    hangry\(CF\) 提交记录……

    点击查看提交记录

    image

    \(KMP\) 已死,\(Hash\) 当立!

    本来昨天就想出来怎么做的题,今天调了一下午。

    后面会说为什么 \(KMP┏┛墓┗┓\)

标签给的 暴力字符串Hash 可解。

\(Hash\) 处理出 \(pre(t,i)\) 在主串 \(s\) 最靠左匹配到的位置(此处的最左应是在 \(>k\) 的前提下),以及 \(suf(t,i)\) 在主串 \(s\) 最靠右匹配到的位置(同理在 \(<n-k+1\) 前提下),分别用 \(l_i,r_i\) 表示。

找到一组 \(l_i,r_{m-i}\) ,满足:

\[l_i<r_{m-i}且k\leq l_i\leq n且1\leq r_{m-i}\leq n-k+1 \]

时,即匹配成功。

但问题还没有结束,若存在某一字符串在主串中完整出现过,那么他可能尽在两端的其中一段出现过,不满足上面的条件,所以特判即可。

思路不难,但是各种细节非常恶心(要不然我也不能调一下午)。

  • \(KMP┏┛墓┗┓\)

    我们要的 \(l_i\)\(r_i\) 应尽可能靠左或靠右,但是 \(KMP\) 算法在乎的只是将模式串尽可能的在主串中完整的匹配出来,由此他跑出来的 \(l_i,r_i\) 是错误的。

    • \(Hack\)

      27 11 9
      bbaabababaaaabbbbabaababaab
      abababaabab
      

    上述样例用 \(KMP\) 跑他会告诉你 \(l_3=20\) ,但显然应为 \(10\) ,因为在主串中 \(sub(4,11)\) 均是匹配成功的,他就直接从 \(12\) 开始跑了。

  • 当模式串能整个在主串中匹配时,需要判断的地方也很多:

    我们设 \(ans\) 为他匹配到的位置。

    且此时是满足 \(m\leq k\) 的,否则还是需要拼出来。

    • \(ans-k+1\) 是否 \(\geq 1\)

      是,则输出 \(1\)

    • \(ans-k+1\) 是否与 \(n-k+1\) 有重叠:

      是,则输出 \(n-k+1-k\)

      解释一下,右面部分从最后开始剪即可,因为左半部分已经包含全部模式串。

  • 处理 \(l_i,r_i\) 时:

    \(i\) 循环到 \(\min(k,m)\) ,再往后跑会出现负数导致出错,此处看代码理解。

最后,\(Hack\) 数据在 \(CF\) 中十分充盈,本人算法可能还是有漏洞,欢迎 \(Hack\)

让我们为 KMP 节哀
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=5e5+10,P=1e9+7;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,k,ba[N],a[N],b[N],l[N],r[N],ans;
char s[N],t[N];
void pre()
{
    ba[0]=0,ba[1]=233;
    for(int i=2;i<=n;i++)
        ba[i]=(ba[i-1]*ba[1])%P;
}
void Hash(char s[],int x[],int len)
{
    x[0]=0;
    for(int i=1;i<=len;i++)
        x[i]=((x[i-1]*ba[1])%P+s[i])%P;
}
int ask(int x[],int l,int r)
{
    return (x[r]-x[l-1]*ba[r-l+1]%P+P)%P;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m),read(k);
    cin>>s+1>>t+1;
    pre();
    Hash(s,a,n),Hash(t,b,m);
    for(int i=m;i<=n;i++)
        if(ask(b,1,m)==ask(a,i-m+1,i))
        {
            ans=i;
            break;
        }
    if(ans!=0&&m<=k)
    {
        puts("Yes");
        if(ans-k+1<=0) cout<<1;
        else if(ans>=n-k+1) cout<<n-k*2+1;
        else cout<<ans-k+1;
        cout<<' '<<n-k+1;
        return 0;
    }
    if(ans!=0&&m>k)
    {
        puts("Yes");
        cout<<max(1ll,ans-m/2-k+1)<<' '<<max(1ll,ans-m/2-k+1)+k;
        return 0;
    }
    for(int i=1,j=k;i<=min(k,m);i++)
    {
        while(j<=n&&ask(b,1,i)!=ask(a,j-i+1,j))
            j++;
        if(ask(b,1,i)==ask(a,k-i+1,k))
            j=k;
        l[i]=j;
    }
    for(int i=1,j=n-k+1;i<=min(k,m);i++)
    {
        while(j>=1&&ask(b,m-i+1,m)!=ask(a,j,j+i-1))
            j--;
        if(ask(b,m-i+1,m)==ask(a,n-k+1,n-k+i))
            j=n-k+1;
        r[i]=j;
    }
    for(int i=1;i<=m;i++)
        if(l[i]<r[m-i]&&l[i]>=k&&l[i]<=n&&r[m-i]+k-1<=n&&r[m-i]>=1)
        {
            puts("Yes");
            cout<<l[i]-k+1<<' '<<r[m-i];
            return 0;
        }
    puts("No");
}

更正

  • \(4.12\)

确切的说 \(KMP\) 没有死。

前面说到不好解决匹配更新问题,但事实上只要疯狂的跳 \(next\) 即可。

然而每一个点只会被跳一次,所以复杂度仍为 \(O(n)\) 。带 \(3\) 倍常数,因为 \(KMP\) 本身就是 \(2\) 倍常数。

但无论如何常数比 \(Hash\) 小的,所以跑的飞快。

具体打法询问 \(@wang54321\)

G - x-prime Substrings

  • \(4.4\)

正解是 DFS \(+\) AC自动机,但是 状压 DP 可解。

\(f_{i,sub}\) 表示枚举到第 \(i\) 个点,前缀和状态为 \(sub\) 时其后面需要删除的最小个数。

根据定义,选择倒序递归处理,同时记忆化减小时间复杂度,目标 \(f_{1,1}\)

可能比较难理解,一个一个解释:

  • \(sub\)

    二进制表示前缀和状态。

    若其在 \(2^x\) 位置为 \(1\) ,表示其前缀和中存在 \(x\)

    比如一个前缀能组成的和有 \(1,2,6,7,8\) ,那么他的状态就表示为 \(111000111\) ,其中最后一位是为 \(2^0\) ,此处没有实际意义,但后面有用处。

  • 如何更新 \(sub\)

    继续用上面的例子,若要将这个前缀和 \(+4\) ,即将 \(sub\times 2^4+1\) ,变为 \(1110001110001\) ,发现其能组成的和就为 \(4,5,6,10,11,12\) ,级将上面的 \(1,2,6,7,8\)\(+4\) ,而这个 \(4\) 就是 \(0+4\) ,这也是为什么要保持最后一位 \(=1\)

  • 如何判断 \(sub\) 是否合法?

    • 先判断该 \(sub\) 是否包含 \(x\) ,即 \(2^x\) 位置是否为 \(1\) ,如果不存在,那一定是合法了。

    • 如果存在 \(x\) ,再判断其是否存在 \(x\) 的约数,若有,则仍是合法的。

    但这样显然不正确,需要改进,不放先将 \(>x\) 的前缀和全部删去(必定合法),再将 \(>x\) 的约数的前缀和全部删去,这样的话保证了正确性,同时减少了状态数。

  • 状态转移:

    \[f_{i,sub}=\min(f_{i+1,sub}+1,f_{i+1,sub\times 2^{a_i}+1}) \]

    即位置 \(i\) 的数是否被选,若不选显然需要删除的点数 \(+1\) ,否则前缀和状态 \(+a_i\)\(a_i\) 为当前点表示的数。

    边界 \(f_{i,sub}=0(i>n)\)

目标 \(f_{1,1}\)

当然状态太大数组存不下,用 \(unordered\_map\) 即可。

\(2000ms\) 的时限跑了 \(1999ms\)\(\large{😅😅😅}\)

看似复杂度会假,实际上 \(x-prime\) 很少,勉强能过。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1010;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
char s[N];
int m,n,a[N],con;
unordered_map<int,int>f[N];
void calc(int &sub)
{
    sub&=((1<<m)-1);
    int tmp=sub&con;
    if(tmp==0) return ;
    tmp&=-tmp;
    sub&=(tmp-1);
}
bool check(int sub)
{
    if((sub&(1<<m))==0) return 1;
    if(sub&con) return 1;
    return 0;
}
int dfs(int x,int sub)
{
    if(x>n) return 0;
    calc(sub);
    if(f[x].count(sub)>0) return f[x][sub];
    int lsub=sub,ans=dfs(x+1,sub)+1;
    sub=(sub<<a[x])|1;
    if(check(sub))
        ans=min(ans,dfs(x+1,sub));
    return f[x][lsub]=ans;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    cin>>s+1;
    read(m);
    n=strlen(s+1);
    for(int i=1;i<=n;i++) 
        a[i]=s[i]-'0';
    for(int i=1;i<m;i++)
        if(m%i==0)
            con|=(1<<i);
    cout<<dfs(1,1);
}

B - Yet Another LCP Problem

  • \(4.13\)

    \(4.10\) 那个版本有点瑕疵,而且不符合本人代码风格,经过反思,改成适合自己的方法(和之前打的 \(luogu~P4248\) 差异)一样的方法。

\(SA+\) 单调栈。

对于每一组给定的 \(a,b\) 数组,不妨将其拼成 \(c\) 数组。

我们定义 \(ans_x=\sum\limits_{i=1}^{len_x}\sum\limits_{j=1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})\) ,那么有:

\[\sum\limits_{i = 1}^{i = k} \sum\limits_{j = 1}^{j = l}{LCP(s_{a_i\sim n}, s_{b_j \sim n})}=\dfrac{ans_c-ans_a-ans_b}{2} \]

至于为什么 \(\dfrac{1}{2}\) 如下:

image

我们知道 \(\sum\limits_{i=1}^{len_x}\sum\limits_{j=i+1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})=\dfrac{\sum\limits_{i=1}^{len_x}\sum\limits_{j=1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})}{2}\) ,不妨直接另 \(ans_x=\sum\limits_{i=1}^{len_x}\sum\limits_{j=i+1}^{len_x}LCP(s_{x_i\sim n},s_{x_j\sim n})\) ,那么求 \(ans_c-ans_a-ans_b\) 即可。

结合单调栈求 \(ans_x\)

先将 \(x\) 数组按照 \(rk[x_1]<rk[x_2]\) 排序,使其符合单调性。

定义 \(val_i=LCP(x_{i-1},x_i)\) ,类似 \(luogu~P4248\) 差异 这道题,去计算 \(val_i\) 的贡献,实际上 \(val\) 的定义和 \(height\) 是类似的,因为 \(x_i\) 并不连续,所以需要这样操作,显然有 \(val_1=0\)

接下来单调栈维护的过程与 \(luogu~P4248\) 差异 是类似的,处理出最靠右的 \(0\leq l<i,val_l<val_i\) 的位置,以及最靠左的 \(i<r\leq n,val_r<val_i\) 的位置,那么 \(l\sim r\) 这一段的 \(LCP\) 均为 \(val_i\) ,依据乘法原理, \(val_i\) 的贡献就是 \(val_i\times (i-l)\times (r-i)\) ,不妨定义 \(l_i=i-l,r_i=r-i\)

那么 \(ans_x=\sum\limits_{i=1}^{len_x}val_i\times l_i\times r_i\)

至于 \(LCP\) ,可以用 \(ST\) 表维护。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=4e5+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int n,m,ans,sum,sa[N],rk[N],id[N],oldrk[N],cnt[N],key[N],height[N],l[N],r[N],a[N],b[N],c[N],mi[N][30],val[N];
char s[N],t[N];
int sta[N],top;
void clean()
{
    memset(rk,0,sizeof(rk));
    memset(sa,0,sizeof(sa));
    memset(id,0,sizeof(id));
    memset(oldrk,0,sizeof(rk));
    memset(cnt,0,sizeof(cnt));
    memset(key,0,sizeof(key));
    memset(height,0,sizeof(height));
    memset(l,0,sizeof(l));
    memset(r,0,sizeof(r));
}
void count_sort(int n,int m)
{
    memset(cnt,0,sizeof(cnt));
    for(int i=1;i<=n;i++)
        cnt[key[i]]++;
    for(int i=1;i<=m;i++)   
        cnt[i]+=cnt[i-1];
    for(int i=n;i>=1;i--)
        sa[cnt[key[i]]]=id[i],
        cnt[key[i]]--;
}
void init(char s[],int n)
{
    int m=127,tot=0,num=0;
    for(int i=1;i<=n;i++)
        rk[i]=(int)s[i],
        id[i]=i,
        key[i]=rk[id[i]];
    count_sort(n,m);
    for(int w=1;w<n&&tot!=n;w<<=1,m=tot)
    {
        num=0;
        for(int i=n;i>=n-w+1;i--)
            num++,
            id[num]=i;
        for(int i=1;i<=n;i++)
            if(sa[i]>w)
                num++,
                id[num]=sa[i]-w;
        for(int i=1;i<=n;i++)
            key[i]=rk[id[i]];
        count_sort(n,m);
        for(int i=1;i<=n;i++)
            oldrk[i]=rk[i];
        tot=0;
        for(int i=1;i<=n;i++)
            tot+=(oldrk[sa[i]]!=oldrk[sa[i-1]]||oldrk[sa[i]+w]!=oldrk[sa[i-1]+w]),
            rk[sa[i]]=tot;
    }
    for(int i=1,k=0;i<=n;i++)
    {
        if(rk[i]==0) continue;
        if(k) k--;
        while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
        height[rk[i]]=k;
    }
}
void RMQ()
{
    memset(mi,0x3f,sizeof(mi));
    for(int i=1;i<=n;i++)
        mi[i][0]=height[i];
    for(int j=1;j<=log2(n);j++)
        for(int i=1;i<=n-(1<<j)+1;i++)
            mi[i][j]=min(mi[i][j-1],mi[i+(1<<(j-1))][j-1]);
}
int ask(int l,int r)
{
    int t=log2(r-l+1);
    return min(mi[l][t],mi[r-(1<<t)+1][t]);
}
bool cmp(int a,int b) {return rk[a]<rk[b];}
int calc(int x[],int len)
{
    sort(x+1,x+1+len,cmp);
    for(int i=2;i<=len;i++)
        if(x[i]==x[i-1])
            val[i]=n-x[i]+1;
        else 
            val[i]=ask(rk[x[i-1]]+1,rk[x[i]]);
    sum=ans=top=0;
    for(int i=1;i<=len;i++)
    {
        while(val[sta[top]]>val[i]) top--;
        l[i]=i-sta[top];
        sta[++top]=i;
    }
    val[len+1]=-1,sta[++top]=len+1;
    for(int i=len;i>=1;i--)
    {
        while(val[sta[top]]>=val[i]) top--;
        r[i]=sta[top]-i;
        sta[++top]=i;
    }
    for(int i=1;i<=len;i++)
        ans+=l[i]*r[i]*val[i];
    return ans;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    cin>>s+1;
    init(s,n);
    RMQ();
    for(int i=1,len1,len2;i<=m;i++)
    {
        read(len1),read(len2);
        for(int j=1;j<=len1;j++)
            read(a[j]),
            c[j]=a[j];
        for(int j=1;j<=len2;j++)
            read(b[j]),
            c[len1+j]=b[j];
        cout<<calc(c,len1+len2)-calc(a,len1)-calc(b,len2)<<endl;
    }
}

F - String Distance

不难发现 \(f(s_i,s_j)\) 的值只可能是 \(0,1,2,1337\) ,数据保证没有相同的串,故此只有 \(1,2,1337\) 三种情况。

  • \(1337\) :两个字符串字符集不同。

  • \(2\) :将 \(s_i,s_j\) 两个串内部分别按照字典序排序。

  • \(1\) :若 \(s_{i,l\sim r}\) 为非递减排序,且 \(s_{i,1\sim l-1}=s_{j,1\sim l-1}\&s_{i,r+1\sim len}=s_{j,r+1\sim len}\) ,则对 \(s_{j,l\sim r}\) 进行一次偏序即可。

\(1337\) 的情况显然好求,不妨求出 \(1\) 的个数,则 \(2\) 的个数即用总数 \(\dfrac{n(n-1)}{2}\) 减去 \(1337,1\) 的个数,于是问题转化为求 \(1\) 的个数。

因为此时满足两字符串除去 \(s_{l\sim r}\) 后前后缀相同,考虑如何处理前后缀与内部极长非递减子串。

处理出每个串内部存在的若干个极长非递减子串(左右均不能扩展),从贪心的角度,同时避免重复,不妨只保存每个子串的左端点,那么他的右端点就是下一个左端点 \(-1\) ,可以使长度为 \(1\) 的也存入,因为数据保证不存在相同字符串。

借鉴求 \(height\) 数组的思想,不难发现,将 \(s_1\sim s_n\) 按照字典序排好后,\(LCP(s_i,s_j)=\min_{i+1\leq k\leq j}(LCP(s_{k-1},s_k))\) ,由此考虑处理出每个 \(LCP(s_{i-1},s_i)\) ,并使用单调栈维护。

单调栈中对于 \(p=s_x\sim s_y\)\(s_i\) 相同的 \(LCP\) ,通过前面的预处理,定位到 \(s_i\) 中的极长不下降子串 \(s_{p+1\sim q}\) ,统计 \(j\in \{x\sim y\},s_{j,q+1\sim len}=s_{i,q+1\sim len}\) 的个数 。

对此考虑将反串建一个 \(tire\) 树,每个节点上建一个 \(vector\) , 定位到 \(tire\) 树上节点后二分查找即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=2e5+10;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=true;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int len,tot,sum,ans,t[N][26],lcp[N];
map<string,vector<string>>mp;
vector<int>id[N],pos[N];
int sta[N],top;
void init(string s,int x)
{
    pos[x].clear();
    for(int i=0;i<=len;i++)
        pos[x].push_back(0);
    id[1].push_back(x);
    pos[x][len]=1;
    for(int i=len-1,p=1;i>=0;i--)
    {
        int c=s[i]-'a';
        if(!t[p][c]) t[p][c]=++tot;
        p=t[p][c];
        pos[x][i]=p;
        id[p].push_back(x);
    }
}
int ask(int x,int l,int r)
{
    return lower_bound(id[x].begin(),id[x].end(),r)-lower_bound(id[x].begin(),id[x].end(),l);
}
int solve(vector<string> a)
{
    int n=a.size();
    sort(a.begin(),a.end());
    for(int i=0;i<=tot;i++)
    {
        for(int j=0;j<26;j++)
            t[i][j]=0;
        id[i].clear();
    }
    tot=1;
    for(int i=0;i<n;i++)
        init(a[i],i);
    vector<int> dec[n];
    for(int i=0;i<n;i++)
    {
        for(int j=1;j<len;j++)
            if(a[i][j-1]>a[i][j])
                dec[i].push_back(j);
        dec[i].push_back(len);
    }
    // vector<int> lcp[n];
    for(int i=1;i<n;i++)
    {
        int j=0;
        while(j<len&&a[i][j]==a[i-1][j]) j++;
        lcp[i]=j;
    }
    int res=n*(n-1);
    top=0;
    sta[++top]=n,lcp[n]=-1;
    for(int i=n-1;i>=0;i--)
    {
        for(int j=2;j<=top;j++)
            res-=ask(pos[i][*upper_bound(dec[i].begin(),dec[i].end(),lcp[sta[j]])],sta[j],sta[j-1]);
        while(top>=1&&lcp[sta[top]]>=lcp[i]) 
            top--;
        sta[++top]=i;
    }
    return res;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    int n;
    read(n);
    for(int i=1;i<=n;i++)
    {
        string s,t;
        cin>>s;
        t=s;
        len=s.size();
        sort(t.begin(),t.end());
        mp[t].push_back(s);
    }
    for(auto it:mp)
    {
        vector<string> a=it.second;
        ans+=1337*a.size()*sum;
        ans+=solve(a);
        sum+=a.size();
    }
    cout<<ans;
}
posted @ 2024-04-04 15:39  卡布叻_周深  阅读(149)  评论(6编辑  收藏  举报