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
根据期望线性性,研究多项式整体的期望等于每一项的期望的和。而 \(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
但是条件有 \(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。