ABC选做(一):ABC230~238


ABC选做(一):ABC230~238

主要是选择了末两题,毕竟做F包括之前的题对我来说没什么用了。

当然还是很难的,近十五道题里只有两三道是赛场秒的,其它都没有自己独立搞出来。

ABC230

G. GCD Permutation

题意:

给定一个长度为 \(n\) 的排列 \(p\),求有多少对 \((i,j)\) 满足:

\[\begin{cases}1\le i\le j\le n \\ \gcd(i,j)\neq 1 \\ \gcd(p_i,p_j)\neq 1 \end{cases} \]

\(n\le 2\times 10^5\)

分析:

Solution 1

首先,\(i=j\) 的时候特判处理,然后只考虑 \(i\lt j\) 的计数。

我的想法首先是容斥:求出 \(\gcd(i,j)=1\) 的数目 \(cnt_1\)\(\gcd(p_i,p_j)=1\) 的数目容易发现也是 \(cnt_1\)。然后求出 \(\gcd(i,j)=1\and \gcd(p_i,p_j)=1\) 的数目 \(cnt_2\)。那么答案是 \(\dbinom{n}{2}-2\times cnt_1+cnt_2\)

\(cnt_1\) 的求法是莫反模板,这里可以 \(O(n)\) 解决,不需要追求 \(O(\sqrt n)\) 的整除分块做法。

我们知道,\(\mu\) 的和函数 \(\delta\) 满足:

\[\delta(n)=\begin{cases}1(n=1)\\ 0(n\gt 1) \end{cases} \]

那么我们传统的莫反就是根据这个来的:\([\gcd(i,j)=1]=\sum_{d\mid \gcd(i,j)}\mu (d)\)

这里求 \(cnt_2\) 亦然:

\[\sum_{i

交换求和顺序:先枚举 \(d_1,d_2\) 的值:

\[\sum_{d_1}^n\sum_{d_2}^n\mu(d_1)\mu(d_2)\sum_{i

发现那个艾弗森约定里的式子很恶心,首先用人话翻译一下就是 \(d_1\)\(i,j\) 的约数,\(d_2\) 同时得是 \(p_i,p_j\) 的约数。

这个莫反的优化是本题的核心。

首先 \(\mu(d_1)\mu(d_2)\) 不能为 \(0\),就是说,\(d_1\)\(d_2\) 都满足由若干个不同质因子构成。

然后我们先枚举一个 \(d_1\),此时可以确定所有满足 \(d_1\mid i\) 的下标 \(i\),它们构成一个下标集合 \(S\)。对于我当前枚举的 \(d_1\),如果和式右边的这个方括号取到 \(1\),肯定里面的 \(i,j\)\(S\) 的两个不同元素。然后设 \(S=\{s_1,s_2,...,s_k\}\),那么对于一个 \(d_2\),它有意义至少得满足:存在一个 \(x\),满足 \(d_2\mid p_{s_x}\),就是说最后我们方括号里 \(i,j\) 是从 \(S\) 里选两个出来,那么 \(p_{i,j}\) 就是从 \(\{p_{s_1},p_{s_2},...,p_{s_k}\}\) 里选两个出来,那我选的 \(d_2\) 必须得整除某个 \(p_{s_x}\),否则,没有一对 \(p_i,p_j\) 会有公因数 \(d_2\)

注意到 \(\le 2\,\times 10^5\) 的数,最多有 \(6\) 个不同质因子。我们可以枚举 \(S\) 中每个元素 \(idx\),然后 \(p_{idx}\) 就最多有 \(6\) 个不同质因子,也就是说,满足 \(x\mid p_{idx}\)\(\mu(x)\neq 0\)\(x\) 至多只有 \(2^6=64\) 个。

对于每个 \(d_1\),我们可以这样求出所有有意义的 \(d_2\)。注意到在上面那个过程里我们其实还可以确定 \(S\) 里有多少个 \(idx\),满足 \(d_2\mid p_{idx}\),记作 \(k\),那么 \(\mu(d_1)\mu(d_2)\dbinom{k}{2}\) 就是当前这组 \(d_1,d_2\) 的贡献。

根据调和级数理论时间复杂度 \(O(64n\log n)\)

Solution 2

其实和第一种解法也差不了多少,反演是一样的。

不想容斥做两次反演,就想正着求答案,也是可以做的。

还是得考虑类似莫反的形式,这次我们得设计一个函数 \(g\)\(g\) 的和函数 \(f\) 满足:

\[f(n)=\begin{cases}0(n=1) \\ 1(n\gt 1) \end{cases} \]

注意到和 \(\mu\) 的和函数恰好反过来了...

反正我是不会设计的。AT官方告诉你这个 \(g\) 是这样的:

\[g(n)=\begin{cases}0(n=1 或 n 有平方因子) \\ -1(n有偶数个不同质因子) \\ 1(n有奇数个不同质因子)\end{cases} \]

发现和 \(\mu\) 还是挺像的,神奇。

然后仿照解一求 \(cnt_2\) 的过程,直接用 \(g\) 替换掉 \(\mu\) 就行了。

这样复杂度是没变的,常数其实也没有快多少,代码也没少写几行...

注:关于函数 \(g\) 的确定(我的一点思考过程)

开始我想了半天为什么有人类可以直接确定这个 \(g\),后来我发现我 naive了:

由于 \(f\)\(g\) 的和函数且形式简单,我们知道了 \(f\) 要逆推 \(g\),那么可以考虑莫比乌斯反演:

\[g(n)=\sum_{d\mid n}\mu(\frac{n}{d})f(d) \\ \Leftrightarrow g(n)=\sum_{d\mid n,d>1}\mu(\frac{n}{d}) \]

也就是说,\(g(n)=\delta(n)-\mu(n)\)

然后就确定出来了...

代码

const int MAXN=2e5+10,LIM=2e5;
int prime[MAXN],tag[MAXN],minp[MAXN],tot;
ll miu[MAXN];
int n,p[MAXN],bucket[MAXN],vis[MAXN];
ll ans0,ans1,ans2;
vectord[MAXN],s;
ll S(ll n){return n*(n-1)/2;}
void pre(){
    miu[1]=1;
    rep(i,2,LIM){
        if(!tag[i])miu[i]=-1,prime[++tot]=i,minp[i]=i;
        rep(j,1,tot){
            if(i*prime[j]>LIM)break;
            tag[prime[j]*i]=1;
            minp[prime[j]*i]=prime[j];
            if(i%prime[j]==0)break;
            else miu[i*prime[j]]=-miu[i];
        }
    }
    rep(i,2,LIM){
        int num=i;
        while(num>1){
            int val=minp[num];
            d[i].push_back(val);
            while(num%val==0)num/=val;
        }
    }
}
int main(){
    pre();
    cin>>n;
    rep(i,1,n)cin>>p[i];
    ans0=S(n)+n;
    rep(i,1,n){
        ll ret=miu[i];
        ll cnt=n/i;ret=ret*S(cnt);
        ans1=(ans1+ret);
    }
    ans1++;ans1*=2;
    rep(d1,1,n){
        if(miu[d1]==0)continue;
        s.clear();
        rep(qwq,1,n){
            if(d1*qwq>n)break;
            int x=d1*qwq,sz=d[p[x]].size();
            rep(mask,0,(1<>idx&1)d2*=d[p[x]][idx];
                bucket[d2]++;
                if(!vis[d2])vis[d2]=1,s.push_back(d2);
            }
        }
        for(auto d2:s){
            ans2=(ans2+miu[d1]*miu[d2]*S(bucket[d2]));
            bucket[d2]=vis[d2]=0;  
        }
    }
    if(p[1]==1)ans2++;
    ans0=ans0-ans1+ans2;
    cout<

ABC231

G. Balls in Boxes

题意:

\(n\) 个盒子,开始第 \(i\) 个盒子有 \(a_i\) 个球。接下来进行 \(k\) 次操作,每次操作等概率选出一个盒子,往里面加入一个球。

设最后第 \(i\) 个盒子有 \(b_i\) 个球,求 \(E(\prod b_i)\) 的值对 \(998244353\) 取模的结果。

\(n\le 1000,k\le 10^9\)

分析:

Solution 1:

从概率的角度来入手。

首先设 \(k\) 次操作完毕后第 \(i\) 个盒子的增量为 \(c_i\),那么所求为 \(E(\prod (a_i+c_i))\),我们考虑暴力展开这个式子。

那么会形成一个多项式,每一项形如 \(a_{i_1}a_{i_2}...a_{i_x}c_{j_1}c_{j_2}...c_{j_y}\),其中 \(i_1,且所有 \(i\) 与所有 \(j\) 的交为空,并为 \(\{1,2,...,n\}\)。(这个约束真tm多)。

根据期望线性性,研究多项式整体的期望等于每一项的期望的和。而 \(a\) 是定值,所以其实对于每一项,研究的是:

\(a_{i_1}a_{i_2}...a_{i_x}\,\cdot E(c_{j_1}c_{j_2}...c_{j_y})\)

观察发现所有 \(x\) 相同的项是有共同性的:它们的 \(E(\prod c)\) 是一样的。换言之,\(E(c_{j_1}c_{j_2}...c_{j_y})=E(c_1c_2...c_y)\)

这是本题的第一个关键点。因为忽略掉里面本来有的球,盒子和盒子之间是没有差别的,所以不管是哪 \(y\) 个盒子,新增球的积的期望都是一样的。

所以设 \(f(i,j)\) 是从 \(a_{1\sim i}\) 中选出 \(j\) 项组成的单项式的和。显然有转移:

\[f(i,j)\rightarrow f(i+1,j) \\ f(i,j)\rightarrow a_{i+1}f(i+1,j+1) \]

(上面是刷表的转移方程。)

那么答案为 \(\sum_{m=1}^n f(n,m)E(c_1c_2...c_m)\)

关键就是这个 \(E(c_1c_2...c_m)\) 怎么求。

牛逼的是,设 \(f_{i,j}\) 表示 \(c_i\) 是否在第 \(j\) 次被操作到(是为 \(1\),不是为 \(0\))。那么 \(c_i=\sum_j f_{i,j}\)

所求变为 \(E(\prod (f_{i,1}+f_{i,2}+...+f_{i,k}))\),展开,得到 \(E(\sum_{p_1p_2...p_m}f_{i,p_i})\),根据期望线性性,只要对每一个 \(p_1p_2...p_m\)\(E(f_{i,p_i})\) 再求和即可。

注意如果 \(p\) 中有两个相同的数那这一项为 \(0\),所以 \(p\) 取遍 \(k\) 的所有 \(m\) 排列,即 \(P_{k}^{m}=k\times(k-1)\times...\times(k-m+1)\),每个排列 \(p\) 的权值是 \(1\),而概率是均等的 \((\frac{1}{n})^m\)。所以 \(E(c_1,c_2...c_m)=P_{k}^{m}\times(\frac{1}{n})^m\)

时间复杂度 \(O(n^2)\)

Solution 2:

从组合的角度来入手,因为期望等于 \(\sum 概率 \times 权值\),而本题里每种情况的概率都是均等的 \((\frac{1}{n})^k\),所以一个想法是求所有情况的 \(\prod b_i\) 的和。

还是把 \(b_i\) 写成 \((a_i+c_i)\) 然后展开。类似地,要求 \(\sum_{m=1}^{n}f(n,m)E(m)\)

这里 \(E(m)\) 不是代表期望了,而是代表所有情况下,\(m\) 个盒子的新增球的积的和(就是上面求的是新增球的积的期望,现在我不要期望了,改算和了)。

这个和也是不能硬做的。一样设 \(f_{i,j}\) 表示 \(c_i\) 是否在第 \(j\) 次被操作到。然后求的是 \(\sum_{所有情况下} \prod_i (\sum_j f_{i,j})\)

还是研究一个排列 \(p_1p_2...p_m\) 的贡献,它的贡献是 \(n^{m-k}\),所以 \(E(m)=P_{k}^{m}\times n^{m-k}\)

但是我们发现最后外面乘上一个 \((\frac{1}{n})^k\)? 后,其实两种解法的式子完全一样,殊途同归。

一点总结:

对于这种盒子放球的问题,有时候从增量入手是一个方法,这种思想的简单题有一道 ARC134C。

对于若干项相加的期望可以利用期望线性性,对于若干项相乘的期望则不能直接拆开相乘了,可以考虑把每一项乘数写成一个和式,展开成一个多项式,这个多项式的期望可以拆成几个单项式的期望来看。

Solution 3:

考虑生成函数。

由于我不会生成函数,先咕了...

代码:

const int MAXN=1010,mod=998244353;
ll mypow(ll a,ll n){
    if(!n)return 1;
    ll tmp=mypow(a,n/2);tmp=tmp*tmp%mod;
    if(n&1)tmp=tmp*a%mod;return tmp;
}
ll n,k,a[MAXN],f[MAXN][MAXN],ans;
void upd(ll& x,ll y){x=(x+y)%mod;}
ll d(ll a,ll b){return a*mypow(b,mod-2)%mod;}
ll P(ll x){
    ll ret=d(1,n);ret=mypow(ret,x);
    rep(i,1,x)ret=ret*(k-i+1)%mod;
    return ret;
}
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>k;
    rep(i,1,n)cin>>a[i];
    f[0][0]=1;
    rep(i,0,n-1)rep(j,0,i){
        upd(f[i+1][j],f[i][j]);
        upd(f[i+1][j+1],f[i][j]*a[i+1]%mod);
    }
    rep(i,0,n)upd(ans,f[n][i]*P(n-i)%mod);
    cout<

ABC232

G. Modulo Shortest Path

题意:

给定两个长度为 \(n\) 的序列 \(A,B\),现在有一张 \(n\) 点完全图。第 \(i\) 个点和第 \(j\) 个点之间(\(i\neq j\))的边权为 \((A_i+B_j)\bmod M\)。求点 \(1\) 到点 \(n\) 的最短路。

\(n\le 2\times 10^5,M\le 10^9\)

分析:

考虑优化建图。

可以按照 \(b\) 排序,然后对每个 \(a_i\),都是和两个区间的点连边,这样线段树优化建图,但是会出现负边,跑不得。

还有一种想法是,由于这里的边权之和 \(A,B\) 有关,我们按照模 \(M\) 剩余系建出 \(M\) 个虚点 \(d_0,d_1,...,d_{M-1}\) 来。然后 \(d_{i}\rightarrow d_{(i+1)\bmod M}\) 连一条边权为 \(1\) 的边。

然后 \(i\rightarrow d_{A_i},d_{B_i}\rightarrow i\) 连边权为 \(0\) 的边。但这样从 \(i\)\(j\) 的边权是 \((B_j-A_i)\bmod M\)。容易发现改成 \(i\rightarrow d_{(-A_i)\bmod M}\) 就可以了。

由于最多只有 \(2n\) 个有意义的虚点,那些不会和实点连边的虚点忽略。比如如果 \(d_2\) 不和任何的点连,那就直接 \(d_1\rightarrow d_3\) 连边权为 \(2\) 的边。

由于点数和边数都是 \(O(n)\) 级别的,时间复杂度 \(O(n\log n)\)

代码:

const ll MAXN=2e5+10,INF=1e16;
int n,m,a[MAXN],b[MAXN];
int val[MAXN*2],tot;
int vis[MAXN*3];ll d[MAXN*3];
vector >e[MAXN*3];
int dis(int i,int j){if(in2.d;}};
priority_queueq;
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>m;
    rep(i,1,n)cin>>a[i],val[++tot]=m-a[i];
    rep(i,1,n)cin>>b[i],val[++tot]=b[i];
    sort(val+1,val+1+tot);
    tot=unique(val+1,val+1+tot)-val-1;
    rep(i,1,tot-1)e[n+i].push_back({n+i+1,dis(val[i],val[i+1])});
    e[n+tot].push_back({n+1,dis(val[tot],val[1])});
    rep(i,1,n)e[i].push_back({n+qry(m-a[i]),0}),e[n+qry(b[i])].push_back({i,0});
    rep(i,2,n+tot)d[i]=INF;q.push({1,0});
    while(q.size()){
        Node tmp=q.top();q.pop();
        int u=tmp.u;if(vis[u])continue;vis[u]=1;
        for(auto p:e[u]){
            int v=p[0],w=p[1];
            if(d[v]>d[u]+w){
                d[v]=d[u]+w;
                q.push({v,d[v]});
            }
        }
    }
    cout<

H. King's Tour

题意:

给定 \(n\times m\) 的网格图,试求一条从 \((1,1)\) 出发,\((x,y)\) 结束,恰好经过每个点一次的路径(保证 \((x,y)\neq (1,1)\))。行走方式为八连通。可以保证一定有解。

\(2\le n,m\le 100\)

分析:

”一定有解“这个事实很有启发性,而且注意到 \(n,m\ge 2\)。因为我们发现,\(n=1\or m=1\) 的时候,比如说 \(n=1,m=5\)\((1,1)\) 开始 \((1,2)\) 结束的路是不存在的。

然后我们来看 \(n=2\) 的情况怎么构造解,这个我相信大家都会构造。

同理 \(m=2\) 的时候,旋转一下就是 \(n=2\) 的情况了。

然后 \(n,m\gt 2\) 的时候,自然地想把问题规模缩小。最开始的想法是我绕边缘一圈,如果 \((x,y)\) 不在边上那么就缩减成了 \((n-2)\times (m-2)\) 的情况。但是 \((x,y)\) 可能在边上,这个时候很难处理。

然后发现,不如一次缩小一个维度。就是要么把第一行全走了,走到 \((2,m)\);要么把第一列全走了,走到 \((n,2)\)。这两条路径,至少有一种是 \((x,y)\) 不在上面的。选择这种走法,问题就缩减为 \((n-1)\times m\) 或者 \(n\times (m-1)\) 的情况了。

至于实现,有一个比较简单的方法。就是同样把子问题看做从 \((1,1)\)\((x,y)\) 的路径,然后再刚才的递归中,就要发生旋转(比如如果我把第一列全走了,我得把 \((n,2)\) 旋转成新的 \((1,1)\)。)那我先递归下去求出路径。然后递归结束后把这部分经过的格子通过变换变换会正常状态。这样复杂度上限是 \(O(n^2m^2)\)? 的,但运行的很快,只有28ms。

代码:

const int MAXN=110;
int n,m,a,b;
vector >ans;
void solve(int n,int m,int a,int b){
    if(n==2){
        rep(i,1,b-1)ans.push_back({1,i}),ans.push_back({2,i});
        if(a==1){rep(i,b,m)ans.push_back({2,i});per(i,m,b)ans.push_back({1,i});}
        else{rep(i,b,m)ans.push_back({1,i});per(i,m,b)ans.push_back({2,i});}
        return;
    }
    if(m==2){
        int cur=ans.size();
        solve(m,n,b,a);
        int sz=ans.size();
        rep(i,cur,sz-1)swap(ans[i][0],ans[i][1]);
        return;
    }
    if(b!=1 && (a!=n || b!=2)){
        rep(i,1,n)ans.push_back({i,1});
        int cur=ans.size();
        solve(m-1,n,b-1,(n-a+1));
        int sz=ans.size();
        rep(i,cur,sz-1)swap(ans[i][0],ans[i][1]),ans[i][0]=(n-ans[i][0]+1),ans[i][1]++;
        return;
    }else{
        rep(i,1,m)ans.push_back({1,i});
        int cur=ans.size();
        solve(m,n-1,(m-b+1),a-1);
        int sz=ans.size();
        rep(i,cur,sz-1)swap(ans[i][0],ans[i][1]),ans[i][1]=(m-ans[i][1]+1),ans[i][0]++;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>m>>a>>b;
    solve(n,m,a,b);
    for(auto p:ans)cout<

ABC233

G. Strongest Takahashi

题意:

给定一个 \(n\times n\) 网格图,有些格子为 #,代表为障碍。有些格子为 .,代表为空地。

你可以执行若干次操作:每次选择一个 \(D\),然后把一个 \(D\times D\) 范围内的障碍全部摧毁,花费 \(D\) 的代价。

求消灭所有障碍的最少代价。\(n\le 50\)

分析:

考虑 \(dp\):设 \(f(x_1,y_1,x_2,y_2)\) 为消灭以 \((x_1,y_1)\) 为左上角,\((x_2,y_2)\) 为右下角的矩形内所有障碍的最小代价。

设这个矩形是 \(n\times m\) 的。那么如果每行,每列上都有障碍,显然花费 \(\max(n,m)\) 的代价进行一次操作是最优秀答案,这也是 \(f\) 的上界。

考虑转移:把这个矩形分成若干个矩形加起来,容易发现可以以某行空行或者某列空列进行分割。但分割了不一定最优秀。

比如 \(3\times 10\) 的矩形,按中间行分割成两个 \(1\times 10\) 的矩形。这两个矩形全部都是障碍,那么分割后的代价是 \(20\),但实际上一次操作 \(10\) 就可以了。

所以应该是所有可能转移取 \(\min\),时间复杂度 \(O(n^5)\)

代码:

const int MAXN=60;
int n;
char s[MAXN][MAXN];
int sum[MAXN][MAXN];
int f[MAXN][MAXN][MAXN][MAXN];
int qry(int x1,int y1,int x2,int y2){
    return sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y1-1];
}
int dp(int x1,int y1,int x2,int y2){
    if(x1>x2 || y1>y2)return 0;
    if(qry(x1,y1,x2,y2)==0)return 0;
    int r=x2-x1+1,c=y2-y1+1;
    int& ans=f[x1][y1][x2][y2];
    if(ans!=-1)return ans;
    ans=max(r,c);
    rep(x,x1,x2){
        if(qry(x,y1,x,y2)==0){
            ans=min(ans,dp(x1,y1,x-1,y2)+dp(x+1,y1,x2,y2));
        }
    }
    rep(y,y1,y2){
        if(qry(x1,y,x2,y)==0){
            ans=min(ans,dp(x1,y1,x2,y-1)+dp(x1,y+1,x2,y2));
        }
    }
    return ans;
}
int main(){
    scanf("%d",&n);
    rep(i,1,n){
        scanf("%s",s[i]+1);
        rep(j,1,n){
            if(s[i][j]=='#')sum[i][j]=1;
        }
    }
    rep(i,1,n)rep(j,1,n){
        sum[i][j]+=(sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]);
    }
    rep(i,1,n)rep(j,1,n)rep(ii,1,n)rep(jj,1,n)f[i][j][ii][jj]=-1;
    printf("%d",dp(1,1,n,n));
 
    return 0;
}

H. Manhattan Christmas Tree

题意:

给定平面上 \(n\) 个整点。回答 \(q\) 次询问:

给定 \(x,y,k\),问距离点 \((x,y)\)\(k\) 远的点和点 \((x,y)\) 的距离是多少。

这里的距离是曼哈顿距离

\(n,q\le 10^5\)

分析:

ABC H里思维最少的题之一了。

首先二分答案,然后统计和一个点 \((x,y)\) 曼哈顿距离 \(\le k\) 的不好做。所以一开始曼哈顿转切比雪夫,就是把每个点变换成 \((x+y,x-y)\) 的形式。然后距离的定义转成了切比雪夫距离。此时从数菱形变成了数正方形。发现需要做静态在线二维数点,主席树即可。

复杂度 \(O((n+q)\log^2 n)\)

代码:

const int MAXN=1e5+10,LIM=2e5+100,ADD=1e5+10,MAXM=MAXN*2*20;
int n,q,k[MAXN];
int rt[MAXN*2],ans[MAXN];
pair,int>p[MAXN*2];
vectorv[MAXN*2];
struct Node{
    int lc,rc,sum;
};
struct Tree{
    Node t[MAXM];int tot;
    void copy(int from,int to){
        t[to].lc=t[from].lc;
        t[to].rc=t[from].rc;
        t[to].sum=t[from].sum;
    }
    void pushup(int x){t[x].sum=t[t[x].lc].sum+t[t[x].rc].sum;}
    int upd(int x,int l,int r,int pos,int val){
        int p=++tot;copy(x,p);
        if(l==r){
            t[p].sum+=val;return p;
        }
        int mid=(l+r)>>1;
        if(pos<=mid)t[p].lc=upd(t[p].lc,l,mid,pos,val);
        else t[p].rc=upd(t[p].rc,mid+1,r,pos,val);
        pushup(p);
        return p;
    }
    int qry(int x,int y,int l,int r,int ql,int qr){
        if(ql<=l && qr>=r)return t[y].sum-t[x].sum;
        int mid=(l+r)>>1,ret=0;
        if(ql<=mid)ret+=qry(t[x].lc,t[y].lc,l,mid,ql,qr);
        if(qr>mid)ret+=qry(t[x].rc,t[y].rc,mid+1,r,ql,qr);
        return ret;
    }
}t;
int main(){
    scanf("%d",&n);
    rep(i,1,n){
        scanf("%d%d",&p[i].fr.fr,&p[i].fr.se);
        int x=p[i].fr.fr,y=p[i].fr.se;
        p[i].fr=mp(x+y,x-y);
        p[i].se=i;
        v[x-y+ADD].pb(x+y);
    }
    scanf("%d",&q);
    rep(i,1,q){
        int a,b;
        scanf("%d%d%d",&a,&b,&k[i]);
        p[n+i].fr=mp(a+b,a-b);
        p[n+i].se=n+i;
    }
    rep(i,-1e5,1e5){
        rt[i+ADD]=rt[i-1+ADD];
        for(auto x:v[i+ADD]){
            rt[i+ADD]=t.upd(rt[i+ADD],1,LIM,x+1,1);
        }
    
    }
    rep(i,1,n+q){
        if(p[i].se<=n)continue;
        int idx=p[i].se-n,x=p[i].fr.fr,y=p[i].fr.se;
        int l=0,r=2e5,ret=0;
        while(l<=r){
            int mid=(l+r)>>1;
            int x1=x-mid,y1=y-mid,x2=x+mid,y2=y+mid;
            x1=max(x1,(int)0),y1=max(y1,(int)-1e5);
            x2=min(x2,(int)2e5),y2=min(y2,(int)1e5);
            int cnt=t.qry(rt[y1-1+ADD],rt[y2+ADD],1,LIM,x1+1,x2+1);
            if(cnt

ABC234

H. Enumerate Pairs

题意:

给定平面上 \(n\) 个点和参数 \(k\),找到所有的点对,满足它们的欧几里得距离 \(\le k\),保证这样的点对最多有 \(4\times 10^5\) 对。

\(n\le 2\times 10^5,k\le 1.5\times 10^9\)

分析:

这个题很妙。

首先可以 kdt,但是我不会。

考虑暴力一点的想法。一个想法是分块:把平面用 \(k\times k\) 的正方形分成若干块。这样对于一个点,和它距离 \(\le k\) 的点只会存在与它所在块 \(b\) ,还有所有和 \(b\) 的切比雪夫距离等于 \(1\)\(8\) 个块中。

然后对于每个点,暴力和这 \(9\) 个块中的所有点检查是否满足距离 \(\le k\),就可以通过。

本题最绝妙的就是复杂度分析:

设总共有 \(\Phi\) 对符合条件的点对;

设一个块内有 \(n\) 个点,我们要证明块内符合条件的点对数是 \(n^2\) 级别的。

注意到可以把 \(k\times k\) 的正方形用四个边长为 \(\frac{k\sqrt{2}}{2}\) 的小正方形覆盖掉。而小正方形内任意两点的距离都是小于等于 \(k\) 的。根据鸽笼原理至少有 \(\frac{n}{4}\) 个点会在一个小正方形内。所以一个块内至少有 \(\frac{n^2}{16}\) 级别的符合条件点对。

也就是说对于每个块 \(n^2\) 的暴力,计算量是 \(O(\Phi)\) 的。

考虑对于一个大小为 \(x\) 的块,和一个和它相邻的,大小为 \(y\) 的块,我们会进行 \(xy\) 次判断,而 \(xy\le \frac{x^2+y^2}{2}\),所以这 \(xy\) 次判断,等价于对两个块分别进行一次 \(n^2\) 暴力。由于一个块只会和 \(8\) 个块进行关连,是一个常数,那么加起来计算量还是 \(O(\Phi)\) 的。

所以总复杂度是 $ O(\Phi)/O(\Phi \log \Phi)$,取决于实现。

代码:

const int MAXN=2e5+10;
ll n,k,x[MAXN],y[MAXN];
map >f;
vector ret;
int ck(int i,int j){
    return (x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])<=k*k;
}
int main(){
    cin>>n>>k;
    rep(i,1,n)cin>>x[i]>>y[i];
    rep(i,1,n){
        pi idx=mp(x[i]/k,y[i]/k);
        rep(a,-1,1)rep(b,-1,1){
            pi flag=mp(idx.fr+a,idx.se+b);
            vectortmp=f[flag];
            for(auto p:tmp){
                if(ck(p,i)){
                    ret.pb(mp(p,i));
                }
            }
            
        }
        f[idx].pb(i);
    }
    sort(ret.begin(),ret.end());
    printf("%u\n",ret.size());
    for(auto p:ret)printf("%d %d\n",p.fr,p.se);
    return 0;
}

ABC235

G. Gardens

题意:

给定 \(n\) 个盒子,\(A\) 个红球,\(B\) 个黄球,\(C\) 个蓝球,相同颜色的球之间没有差别,不同的盒子有差别。

需要把球放置在盒子里,使得:

  • 每个盒子非空
  • 一个盒子里不能有相同颜色的球

注意不一定要放完所有的球。

问放置方式对 \(998244353\) 取模的结果。

\(n\le 5\times 10^6,A,B,C\le n\)

分析:

被我场上二十分钟过的题,想来大家都会了吧。

考虑去掉第一个限制的情况。

注意到三种颜色的球独立。对于红球,枚举它最后使用的个数 \(0\le x\le A\),那么红球的总放置方案是 \(\sum_{x=0}^A \dbinom{n}{x}\)

黄球,蓝球同理,相乘即可。

此时加入第一个限制:考虑二项式反演:设 \(f(x)\) 表示至多 \(x\) 个盒子非空的方案数,\(g(x)\) 是恰好 \(x\) 个盒子非空的方案数,我们要求的是 \(g(n)\)。容易发现:

\[f(x)=\sum_{i=0}^{x}\dbinom{x}{i}g(i) \]

那么根据二项式反演得:

\[g(x)=\sum_{i=0}^x(-1)^{x-i}\dbinom{x}{i}f(i) \]

本题求 \(g(n)\) 就是 \(\sum_{i=0}^{n}(-1)^{n-i}\dbinom{n}{i}f(i)\)

发现瓶颈在于求 \(f(i)\),开始讲过了 \(f(i)=(\sum_{x=0}^A \dbinom{i}{x})(\sum_{y=0}^B \dbinom{i}{y})(\sum_{z=0}^C \dbinom{i}{z})\)

但是这样单次求 \(f(n)\)\(n^2\) 的。发现这个下标是一个下指标变化的求和,而我们一般都是做上指标变化的求和,也不方便搞。

确实,我们很难为这个式子找出一个通项。但是我们可以递推!

\(S_n=\sum_{x=0}^{A}\dbinom{n}{x}\),我们考虑从 \(S_n\) 推出 \(S_{n+1}\)

由于 \(\dbinom{n+1}{x}=\dbinom{n}{x}+\dbinom{n}{x-1}\)。所以我们不难推出:\(S_{n+1}=2\times S_n - \dbinom{n}{A}\)

这样对于红色球就可以 \(O(n)\) 预处理了。

黄蓝球亦然。时间复杂度 \(O(n)\)

代码:

const int MAXN=5e6+10,mod=998244353,LIM=5e6+5;
ll power(ll a,ll n){
    if(!n)return 1;
    ll tmp=power(a,n/2);tmp=tmp*tmp%mod;
    if(n&1)tmp=tmp*a%mod;return tmp;
}
ll n,a,b,c;
ll fact[MAXN],inv[MAXN];
ll sa[MAXN],sb[MAXN],sc[MAXN];
ll C(ll n,ll m){
    if(n<0 || m<0 || n>n>>a>>b>>c;
    sa[0]=sb[0]=sc[0]=1;
    rep(i,1,n){
        sa[i]=2*sa[i-1]%mod;
        sa[i]=(sa[i]-C(i-1,a)%mod+mod)%mod;
        sb[i]=2*sb[i-1]%mod;
        sb[i]=(sb[i]-C(i-1,b)%mod+mod)%mod;
        sc[i]=2*sc[i-1]%mod;
        sc[i]=(sc[i]-C(i-1,c)%mod+mod)%mod;
    }
    printf("%lld",G(n));
    return 0;
}

H. Painting Weighted Graph(推荐)

题意:

给定一张 \(n\)\(m\) 边带权无向图 \(G\),开始所有点是黑色的。

称点 \(u\) 和点 \(v\) 是 ”\(x\) 可达的“,当且仅当图上存在一条从 \(u\)\(v\) 的路径,满足路径上边权不超过 \(x\)

你可以进行至多 \(k\) 次操作:任选一个点 \(u\) 和参数 \(x\),把所有和 \(u\) 是 "\(x\) 可达" 的点 \(v\) 都涂成红色(包括点 \(u\) 本身)。

求最后的图多少种颜色。两张图颜色不同,当且仅当存在一个点,在一张图是黑的,另一张图是红的,对 \(998244353\) 取模。

\(n,m\le 10^5,k\le 500\)

分析:

Part. 1

这个 \(x\) 可达容易让我们想到建重构树。然后一次操作就等价于把子树的点全部染红。

但是想一想,不是所有子树都可以选择的。如果一个点的权值和它的父亲权值一样,那这个点就不能操作。

为了避免这种麻烦(虽然最后这样也能做),我们来魔改一下重构树:

每次加入一种权值 \(w\) 的边后,我们会剩下几个连通块,对于一个连通块,我们把加入 \(w\) 条边的两端点的父亲都设成一个点。

举个简单的例子:

如果 \(1\leftrightarrow 2\leftrightarrow 3\),边权都为 \(1\)。传统的重构树会搞出 \(1\)\(2\) 的父亲是 \(4\)\(4\)\(3\) 的父亲是 \(5\)\(4,5\) 是重构树搞出来的虚点),但我们这里的重构树是 \(1,2,3\) 的父亲都为 \(4\))。

如果这个时候还有 \(4\leftrightarrow 5\leftrightarrow 6\) 了,那么就是 \(1,2,3\) 的父亲都为 \(7\)\(4,5,6\) 的父亲都为 \(8\) 这样。

这个实现不是很难。

由于没保证最后是连通的,所以可以建个虚点 \(0\) 把重构森林变成重构树,只不过记得这个虚根不能操作。

然后就变成了树上操作:每次选定一个节点(可以是叶子)操作一次,把子树内所有的叶子染红。此时就方便考虑了。

Part. 2

首先要明白一个事情:我们只关注每个叶子的染色状态,不关注非叶子节点的染色状态。因为重构树上的叶子节点才是原图中的点。

容易想到设计一个 \(O(nk)\) 的树形 \(dp\)。然后你发现这个 \(dp\) 的状态设计是有讲究的。因为本题求的是至多 \(k\) 次操作,势必有些状态有多种方案可以达到。比如说如果是“所有点都红色”这个状态,可以对根节点直接进行一次操作,也可以对根节点的所有儿子进行操作,效果一样。

所以设 \(f(i,j)\)\(i\) 为根的子树,有多少种叶子结点的状态,是最少 \(j\) 步可以完成的。

最终答案是 \(\sum_{i=0}^{k} f(0,i)\)

发现转移是个类似背包的东西,就是每次节点 \(u\) 遍历到一个新的儿子 \(v\),然后 \(f'(u,x+y)+=f(u,x)f(v,y)\) 这样的转移。众所周知,这个转移的复杂度是看上去 \(O(nk^2)\) 实际上 \(O(nk)\) 的。但是存在一些细节:

  • \(0\) 节点特判,不能染色
  • 对于非 \(0\) (也不是叶子)的节点,注意到如果我们不对根节点进行操作,那么开始初始化 \(f(u,0)=1\),然后执行上述背包转移即可。如果我们对根节点进行操作了,那么”子树里所有点都为红色“这个状态的最少步数是 \(1\),所以 \(f(u,1)\) 加一。但是在之前,这个状态的最少步数是 \(u\) 的儿子个数 \(son\),所以 \(f(u,son)\) 减一。

时间复杂度 \(O(nk)\)

代码:

const int MAXN=1e5+10,MAXM=510,mod=998244353;
int n,m,k,tot;
int f[MAXN*2],vis[MAXN*2];
int dp[MAXN*2][MAXM],tmp[MAXM],sz[MAXN*2];
struct Edge{
    int u,v,w;
    bool operator<(const Edge& e2)const{return wt[MAXN*2],son[MAXN*2];
settag;
void dfs(int u){
    vis[u]=1;
    son[tot].pb(u);
    f[u]=tot;
    for(int v:t[u]){
        if(vis[v])continue;
        dfs(v);
    }
}
void build(int l,int r){
    tag.clear();
    rep(i,l,r){
        t[Find(e[i].u)].clear();
        t[Find(e[i].v)].clear();
        tag.is(Find(e[i].u));
        tag.is(Find(e[i].v));
        vis[Find(e[i].u)]=vis[Find(e[i].v)]=0;
    }
    rep(i,l,r){
        if(Find(e[i].u)==Find(e[i].v))continue;
        t[Find(e[i].u)].pb(Find(e[i].v));
        t[Find(e[i].v)].pb(Find(e[i].u));
    }
    for(int u:tag){
        if(vis[u])continue;
        if(t[u].empty())continue;
        tot++;f[tot]=tot;
        dfs(u);
    }
}
void dfs2(int u){
    sz[u]=1;dp[u][0]=1;
    for(int v:son[u]){
        dfs2(v);
        rep(i,0,k)tmp[i]=0;
        rep(i,0,min(k,sz[u]))rep(j,0,min(k-i,sz[v])){
            tmp[i+j]=(tmp[i+j]+1ll*dp[u][i]*dp[v][j])%mod;
        }
        rep(i,0,k)dp[u][i]=tmp[i];
        sz[u]+=sz[v];
    }
    if(u){dp[u][1]=(dp[u][1]+1)%mod;}
    int cnt=son[u].size();
    if(u && cnt>0 && cnt<=k){
        dp[u][cnt]=(dp[u][cnt]+mod-1)%mod;
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&k);tot=n;
    rep(i,1,m){scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);}
    sort(e+1,e+1+m);
    rep(i,1,n)f[i]=i;
    //build tree
    for(int l=1,r;l<=m;l=r+1){
        r=l;
        while(e[r+1].w==e[r].w)r++;
        build(l,r);
    }
    rep(i,1,tot){if(Find(i)!=i)continue;son[0].pb(i);}
    dfs2(0);
    int ans=0;rep(i,0,k)ans=(ans+dp[0][i])%mod;
    printf("%d",ans);
    return 0;
}

ABC236

G. Good Vertices

题意:

给定一张 \(n\) 点有向图,初始没有边。接下来进行 \(m\) 次加边操作。(允许自环)

称一个点 \(i\) 是好的,当且仅当存在一条长度恰好\(L\) 的,从 \(1\)\(i\) 的路径。

对于每个点,求出从加入第几条边开始,它是好的。

\(n\le 100,m\le n^2,L\le 10^9\)

分析:

看上去很像矩阵快速幂的模板,但是我们不好直接做。

我们很难每次加边就查看哪些点成为了好点。而且也没有在线询问我们。所以不妨离线这个问题:给每条边赋上一个边权,边权为它被加入的时间。

这样,对于每个点 \(u\) 就是找到一个最小的 \(i\),满足 \(1\rightarrow u\) 存在一条长度恰好为 \(L\) 的,路径上最大值不超过 \(i\) 的路径。

这个长度恰好为 \(L\),一看就是矩阵快速幂优化,说人话就是朴素的 \(dp\) 设计里要带上这一维”当前经过的边数“。

考虑设 \(dp(i,j)\)\(i\) 是走了几步,\(j\) 是走到的点,代表这个状态下,路径上最大值的最小可能,初始只有 \(dp(0,1)=0\)

那么转移,有 \(dp(i,j)=\min\{\max\{dp(i-1,k),w_{k,j}\}\}\)

发现这个转移方式不管 \(i\) 是多少都是不变的,而且 \(i\) 又很大,那么上矩阵快速幂优化就好了,时间复杂度 \(O(n^3\log L)\)

注:

我们经常会碰到一些类似形式的转移,长得很像矩阵乘法但是不清楚能不能矩阵快速幂优化。AtCoder 在官方题解中给出了能运用矩阵快速幂的充要条件:

如果有转移 \(f(i,j)=\sum g(i,k)h(k,j)\),这里 \(+\)\(\times\) 必须满足的运算性质:

  • 加法是可交换幺半群(注意不要求有逆元)。
  • 乘法是幺半群。
  • 乘法有零元.
  • 两种分配律成立:\((a+b)\times c=a\times c+b\times c\) 以及 \(a\times (b+c)=a\times b+a\times c\)

事实上这个性质还算是自然的,也不难记忆。

代码:

const int MAXN=110,INF=1e9;
int n,m,l;
int g[MAXN][MAXN];
struct Matrix{
    int val[MAXN][MAXN],n,m;
    void operator=(const Matrix& m2){
        n=m2.n,m=m2.m;
        rep(i,1,n)rep(j,1,m)val[i][j]=m2.val[i][j];
    } 
    Matrix operator*(const Matrix& m2)const{
        Matrix ret;ret.n=n,ret.m=m2.m;
        rep(i,1,ret.n)rep(j,1,ret.m){
            ret.val[i][j]=INF;
            rep(k,1,m){
                ret.val[i][j]=min(ret.val[i][j],max(val[i][k],m2.val[k][j]));
            }
        }
        return ret;
    }
    Matrix mypow(int n)const{
        if(n==1)return (*this);
        Matrix tmp=mypow(n/2);
        tmp=tmp*tmp;
        if(n&1)tmp=tmp*(*this);
        return tmp;
    }
}f,A;
int main(){
    cin>>n>>m>>l;
    rep(i,1,n)rep(j,1,n)g[i][j]=INF; 
    rep(i,1,m){
        int u,v;cin>>u>>v;
        g[u][v]=i;
    }
    f.n=1,f.m=n;
    A.n=A.m=n;
    rep(i,1,n)rep(j,1,n)A.val[i][j]=g[i][j];
    rep(i,2,n)f.val[1][i]=INF;
    f=f*A.mypow(l);
    rep(i,1,n){
        if(f.val[1][i]==INF)printf("%d ",-1);
        else printf("%d ",f.val[1][i]);
    }
    return 0;
}

H. Distinct Multiples(推荐)

题意:

给定长度为 \(n\) 的序列 \(d_1,d_2,...,d_n\),对符合条件的 \(a_1,a_2,...,a_n\) 计数:

  • \(1\le a_i\le m\)
  • \(a_i\neq a_j(i\neq j)\)
  • \(d_i\mid a_i\)

答案对 \(998244353\) 取模。

\(n\le 16,1\le d_i\le m\le 10^{18}\)

分析:

个人认为是本篇8场比赛里质量最高的一道(事实上我认为236H,238H,235H是所有题目中质量最高的三道),值得反复品味。

碰到这种多条件,每个条件都看上去比较简单的计数容易想到容斥。对这三个条件做容斥,基本都会选择容斥掉条件 \(2\):因为 \(1,3\) 条件的结合是自然的。

那这个 \(2\) 条件,很容易让我们想到转成无向图连边。先不谈这个,从组合角度入手,设条件 \(A_{i,j}\) 代表 \(a_{i}=a_{j}(i ,那么我们要求的是 \(\mid \overline{A_{1,2}} \cap \overline{A_{1,3}} \cap ... \cap ... \overline{A_{n-1,n}}\mid\) 。根据容斥原理,我们设 \(f(S)\) 是集合 \(S\) 里的条件都满足的方案数,答案是 \(\sum (-1)^{|S|}f(S)\)

但是条件有 \(n^2\) 级别个,直接状压啥的是瞎扯淡。

那么把条件 \(A_{i,j}\) 看作一条 \(i\leftrightarrow j\) 的边,它成立就对应无向图上这条边出现的情况。如果我们能对点集做一个统计就好了。

我们现在只有一个很模糊的思路:设 \(dp(V)\) 代表点集 \(V\) 里的点,所有情况(就是点集 \(V\) 中每条边出现或者不出现都会分两种情况考虑)的 \((-1)^{|S|}\) 总和(\(S\) 是当前情况下的所有出现的边构成的集合),那么 \(dp(\{1,2,...,n\})\) 就是答案。

考虑划分连通块进行 \(dp\),就是枚举 \(T\subset S\),强制 \(T\) 中所有点连通,设这个贡献(注意这里贡献是啥也不清楚,只是一个思路)\(g(T)\),那么转移就是 \(dp(S)\) 加上 \(g(T)dp(S-T)\)

那么第一个问题是考虑去重,一个经典套路是强制 \(T\) 包含 \(S\) 的最低位。

第二个问题是 \(g\) 到底是什么玩意。注意到一个事情就是 \((-1)^{x}\) 是有良好运算性质的:一个边集 \(S\)\((-1)^{|S|}\),可以从图上看,把它拆成若干个连通块,设第 \(i\) 个连通块有 \(k_i\) 条边,那么 \(\prod (-1)^{k_i}=(-1)^{|S|}\)

所以 \(g(T)\) 的定义是这样的:对于每一种让点集 \(T\) 连通的方式,如果边数为偶数加上 \(1\),否则加上 \(-1\)(开始为 \(0\))。

如果能算 \(g(T)\),就能知道答案。

首先设 \(G\)\(T\) 中的点的 \(d\)\(lcm\),那么取值一共有 \(\left\lceil\frac{M}{lcm}\right\rceil\) 种(所以转移的时候实际上是 \(g(T)dp(S-T)\) 还要乘上这个东西)。对于每一种,我们要进行上面的计数。

注意此时的计数只和 \(T\) 的点数有关。具体来说是 \(g(T)=(-1)^{|T|-1}(|T|-1)!\)(就是阶乘)。

这个 \(g\) 的确定非常牛逼(以下 \(g(x)\) 是指点数为 \(x\) 的集合的答案,换种说法,容斥系数):

\(g(1)=1\) 这是显然的。

考虑 \(x\gt 1\) 的情况。连通块相关的一个方法是容斥:我从所有可能情况里去掉不连通的情况。首先,所有情况的 \(1/-1\) 的和是 \(0\),因为选偶数条边的方案数等于选奇数条边的方案数。然后考虑容斥:去掉不全联通的情况的贡献。为了去重设 \(T\) 为第一个点所在的集合,而 \(S\)\(x\) 个点里 \(T\) 的补集。那么此时的和是 \(T\) 内部连通的 \(-1/1\) 之和 $\times $ \(S\) 内部的边随便选的 \(-1/1\) 之和。当 \(|S|>1\) 的时候显然第二个乘数为 \(0\) 所以我们忽略它。那么只用考虑 \(|S|=1\) 的情况,这个点有 \((x-1)\) 种选择,那么 \(g(x)=-(x-1)g(x-1)\),加上 \(g(1)=1\) 的初始条件,可以推导出通项。

时间复杂度 \(O(3^n)\),听说最后有啥高科技还可以加速这个过程。

代码:

const int MAXN=20,MAXM=(1<<20),mod=998244353;
ll n,m,d[MAXN],fact[MAXN];
ll g[MAXM],dp[MAXM],pcnt[MAXM];
ll H(int n){
	if(even((n-1)))return fact[n-1];
	else return (mod-fact[n-1]);
}
ll gcd(ll a,ll b){
	if(!b)return a;
	return gcd(b,a%b);
}
int main(){
	fact[0]=1;rep(i,1,19)fact[i]=fact[i-1]*i%mod;
	cin>>n>>m;
	rep(i,1,(1<>d[i];
	rep(i,0,n-1)g[1<b){
			g[i]=-1;
		}else{
			g[i]=v1/d*v2;
		}
	}	
	rep(i,1,(1<

ABC237

H. Hakata

题意:

给定一个长度为 \(n\) 的字符串,问最多能从中选出几个回文的子串,使得不存在一个子串是另一个子串的子串...

一个串 \(A\)\(B\) 的子串当且仅当 \(B\) 可以从开头结尾删去任意个字符(包括 \(0\) 个)得到 \(A\)

\(n\le 200\)

分析:

这题还是比较简单的。

首先抛开回文限制,一共有 \(n^2\) 个串,按照包含关系连边,会形成一个 dag(先把串去重,这是容易的)。

题目问的就是这张 dag 的最长反链。根据 dilworth 定理有 dag 最长反链长度等于最小链覆盖。

至于 dag 最小链覆盖是经典的网络流问题了...

但这样复杂度看似是 \(n^6\) 往上的,因为点数都是 \(n^2\) 的了。

发现还没用到回文串性质,我们猜测这个东西是用来缩小点数的。事实上如果你直接莽会发现跑的飞快。

引理:一个长度为 \(n\) 的串的本质不同回文子串个数不超过 \(n\)

证明:考虑归纳。长度为 \(1\) 的串的回文子串只有本身一个。每次新加入一个字母 \(c\),以 \(c\) 为结尾的回文子串(是若干个后缀)中,如果有多个,那么长度较短的哪些都会被包含在长度最长的那个里。所以新增的回文子串最多只有 \(1\) 个。

那么点数就是 \(n\) 的,边数撑死了是 \(n^2\) 那么确定上界是 \(O(n^4)\)。不过最后最慢的点也就跑了 8ms...

代码:

const int MAXN=210,MAXM=(MAXN*2)*(MAXN*2)*2,base=13331,INF=1e9;
int n,f[MAXN][MAXN],tot;
char s[MAXN];
ull h[MAXN],power[MAXN];
arraytmps[MAXN*2];
ull qry(int l,int r){return h[r]-h[l-1]*power[r-l+1];}
int check(int x,int y){
    int m=tmps[y][1]-tmps[y][0]+1;
    rep(i,tmps[x][0],tmps[x][1]-m+1)if(qry(i,i+m-1)==qry(tmps[y][0],tmps[y][1]))return 1;
    return 0;
}
void add(int x,int y){
    rep(i,1,tot)if(qry(x,y)==qry(tmps[i][0],tmps[i][1]))return;
    tmps[++tot]={x,y};
}
namespace G{
    int n,s,t;
    int fst[MAXN*2],nxt[MAXM],tot;
    int dis[MAXN*2],cur[MAXN*2];
    queueq;
    struct E{int u,v,c,f;}edge[MAXM];
    void adde(int u,int v,int c){edge[++tot]=(E){u,v,c,0};nxt[tot]=fst[u];fst[u]=tot;}
    void link(int u,int v,int c){adde(u,v,c);adde(v,u,0);}
    void build(int tot){
        n=tot;s=n*2+1,t=s+1;
        rep(i,1,n)rep(j,1,n)if(i!=j){
            if(check(i,j))link(i,j+n,1);
        }
        rep(i,1,n)link(s,i,1),link(i+n,t,1);
    }
    int bfs(){
        rep(i,1,t)dis[i]=INF;dis[s]=0;q.push(s);
        while(q.size()){
            int u=q.front();q.pop();
            for(int j=fst[u];j;j=nxt[j]){
                if(edge[j].f==edge[j].c)continue;
                int v=edge[j].v;
                if(dis[v]>dis[u]+1){
                    dis[v]=dis[u]+1;q.push(v);
                }
            }
        }
        return dis[t]!=INF;
    }
    int dfs(int u,int f){
        if(u==t || !f)return f;
        int ret=0;
        for(int& j=cur[u];j;j=nxt[j]){
            int v=edge[j].v;if(dis[v]!=dis[u]+1)continue;
            int val=dfs(v,min(f,edge[j].c-edge[j].f));
            ret+=val;edge[j].f+=val;
            f-=val;edge[op(j)].f-=val;
            if(!f)break;
        }
        return ret;
    }
    int dinic(){
        int ret=0;
        while(bfs()){
            rep(i,1,t)cur[i]=fst[i];
            ret+=dfs(s,INF);
        }
        return ret;
    }
};
int main(){
    power[0]=1;rep(i,1,200)power[i]=power[i-1]*base;
    ios::sync_with_stdio(false);
    cin>>(s+1);n=strlen(s+1);
    rep(i,1,n)f[i][i-1]=f[i][i]=1,h[i]=h[i-1]*base+(s[i]-'a'+1);
    rep(len,2,n)rep(i,1,n){
        int j=i+len-1;if(j>n)break;
        f[i][j]=(s[i]==s[j])&f[i+1][j-1];
    }
    rep(i,1,n){
        rep(d,0,n){
            if(i-d<1||i+d>n)break;
            if(f[i-d][i+d])add(i-d,i+d);
        }
        if(i==n)break;
        rep(d,0,n){
            if(i-d<1||i+d+1>n)break;
            if(f[i-d][i+d+1])add(i-d,i+d+1);
        }
    }
    G::build(tot);
    cout<

ABC238

H. Removing People(推荐)

题意:

\(n\) 个人围成一圈,相邻两个人之间的距离都是 \(1\)。每个人有一个方向,可能是顺时针,也可能是逆时针。

定义第 \(a\) 个人到第 \(b\) 个人的距离 \(dis(a,b)\) 如下(\(a\neq b\)) :

  • 如果 \(a\) 是顺时针方向,那么从 \(a\) 出发,沿着顺时针走,走到 \(b\) 时所走的路程就是 \(a\)\(b\) 的距离。

  • 反之,从 \(a\) 出发,沿着逆时针走,走到 \(b\) 时所走的路程就是 \(a\)\(b\) 的距离。

现在进行 \(n-1\) 次操作,初始得分为 \(0\)。每次操作等概率从剩下的人里选择一个人 \(a\),设剩下的人里令 \(dis(a,x)\) 取到最小值的人是 \(x=b\),那么移除第 \(b\) 个人,并且得分累加上 \(dis(a,b)\)

求最后得分的期望模 \(998244353\)

\(n\le 300\)

分析:

这种算期望,然后每种情况的概率都均等的题,都是诈骗的,只要算出所有情况的得分之和最后除以每种情况的那个均等概率——本题里是 \(\frac{1}{n!}\) ——就好了。

那么删人不好做,常见套路是倒过来变成加人。这样有一个什么好处,就是不用考虑是不是什么离你最近的人了,你想加入谁就加入谁。

然后我们枚举最后剩下那位仁兄是第 \(i\) 个人,这时候可能还有点难理解,那就再还原一部,随便找一个老兄(最后选择到 \(i\) ,然后被删除的那位) \(j\),此时场上剩下两个人。这个时候就有点门道了:\(i\rightarrow j\) 这样顺时针是一个区间,\(j\leftarrow i\) 这样逆时针又是一个区间,现在就变成了左右两个区间的人独立添加,然后合并两个区间的事情了——只要用一个组合数就好了。

这是一个大致的思路,接下来考虑区间 \(dp\)\(f(i,j)\)\(i\rightarrow j\) 这个区间内(\(i\) 出发顺时针走到 \(j\),如果 \(i=j\) 那就是整个一圈),\(i\)\(j\) 两个人已经放好了,接下来要按顺序放入区间里剩下的所有人,所有情况的计数器之和。

发现这样还是无法转移,所以再设一个 \(g(i,j)\) 表示方案。

先转移 \(g\):枚举 \((i,j)\) 区间里第一个被放的人 \(k\)。注意这时候有门道了:可能是选择了 \(i\),然后放下了 \(k\)。也可能是选择了 \(j\) 然后放下了 \(k\)。这得取决于是 \(i\)\(j\) 的面向。所以第一个放 \(k\) 的方式会有 \(0/1/2\) 种记为 \(c_1\)。那么 \(g(i,j)\) 要加上 \(c1\times g(i,k)\times g(k,j)\times \dbinom{cnt_l+cnt_r}{cnt_l}\)。其中 \(cnt_l\)\(i\sim k\) 区间里的人数,类似的 \(cnt_r\)\(k\sim j\) 区间里的人数(不含 \(i,j,k\))。这种合并的时候乘组合数的套路应该不会陌生。

然后转移 \(f\):自然是有 \(c1\times f(i,k)\times g(k,j)\times \dbinom{cnt_l+cnt_r}{cnt_l}\)\(c1\times g(i,k)\times f(k,j)\times \dbinom{cnt_l+cnt_r}{cnt_r}\) 这两个——这是左右区间合并后的贡献。

但是不要忘记 \(i\sim k\) 或者 \(k\sim j\) 这段的距离还要算上。具体地,\(c2=[i面向顺时针]\times dis(i,k) + [j面向逆时针]\times dis(j,k)\)

然后 \(f(i,j)\) 就另外还得加上 \(c2\times g(i,k)\times g(k,j)\times \dbinom{cnt_l+cnt_r}{cnt_l}\)

时间复杂度 \(O(n^3)\)?。

代码:

const int MAXN=310,mod=998244353;
ll mypow(ll a,ll n){if(!n)return 1;ll tmp=mypow(a,n/2);tmp=tmp*tmp%mod;if(n&1)tmp=tmp*a%mod;return tmp;}
ll n,c[MAXN][MAXN],fact[MAXN],inv[MAXN];
ll f[MAXN][MAXN],g[MAXN][MAXN],ans;
char s[MAXN];
void upd(ll& x,ll y){x=(x+y)%mod;}
void dp(){
    rep(i,0,n-1)f[i][(i+1)%n]=1;
    rep(len,2,n){
        rep(i,0,n-1){
            int j=(i+len)%n;
            int k=(i+1)%n,cnt=0;
            while(k!=j){
                ll c1=(s[i]=='R')+(s[j]=='L');
                ll c2=(s[i]=='R')*(k>i?k-i:k-i+n)+(s[j]=='L')*(j>k?j-k:j-k+n);
                upd(f[i][j],c1*f[i][k]%mod*f[k][j]%mod*c[len-2][cnt]%mod);
                upd(g[i][j],c2*f[i][k]%mod*f[k][j]%mod*c[len-2][cnt]%mod);
                upd(g[i][j],c1*g[i][k]%mod*f[k][j]%mod*c[len-2][cnt]%mod);
                upd(g[i][j],c1*f[i][k]%mod*g[k][j]%mod*c[len-2][cnt]%mod);
                k=(k+1)%n;cnt++;
            }
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>s;
    c[0][0]=1;rep(i,1,300){c[i][0]=1;rep(j,1,i)c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;}
    fact[0]=inv[0]=1;rep(i,1,300)fact[i]=fact[i-1]*i%mod,inv[i]=mypow(fact[i],mod-2);
    dp();
    rep(i,0,n-1)upd(ans,g[i][i]);
    ans=ans*inv[n]%mod;cout<

这次的题解就到这里啦qwq。

AT