AtCoder Regular Contest 124 部分题题解
D.Yet Another Sorting Problem
题目链接
简要题解
假设没有\(n\)和\(m\)的限制,可以在排列内交换任意两个数的位置,该怎么做?
我们将\(i\)和\(p_i\)连边,得到了一张\(n\)个点\(n\)条边的图(允许重边自环),且每个点的度数为\(2\),那么这张图就是由若干个环组成的。
我们的目标是把这张图变成\(n\)个连通块,而我们能进行的操作是交换两个位置的数,假设交换\(i\)和\(j\)上面的数\(p_i\)和\(p_j\)。
那么有四种情况
1.若\(i,p_i\)和\(j,p_j\)分别属于两个连通块,那么我们可以将这两个连通块合并成一个连通块,连通块个数减少1
2.若\(i,p_i\)和$j,p_j属于同一个连通块,且这四个数互不相等,那么我们执行操作不能改变什么,只是把环扭了一下
3.若这四个数中有两个数相等,那么它们一定在同一个连通块内,且这三个点相邻,执行操作会将中间那个点移出环,连通块个数增加1
4.若这四个数中\(i=p_j\),\(j=p_i\),那么执行操作可以把这两个点分离,增加一个连通块
对于没有限制的情况,我们可以只执行操作3和操作4,每次增加一个连通块,直至图中拥有n个连通块。
假设初始连通块个数为\(m\),那么我们至少需要执行\(n-m\)次操作。
对于有限制的情况,我们把前\(n\)个点染成蓝色,后\(m\)个点染成红色,我们发现我们的操作必须同时涉及红蓝两种颜色的点才行。
对于一个大小大于等于2的纯色连通块,我们只有先让它与其他连通块合并成混色连通块,才能拆成一个个点。
我们设大小大于等于2的红色连通块有\(R\)个,蓝色连通块有\(B\)个,那么可求得最少操作数$$Ans=n-m+2*max(R,B)$$
不妨假设\(R\geq B\),那我们可以操作\(B\)次,每次将一个红色连通块和一个蓝色连通块合并,再操作\(R-B\)次,每次将一个红色连通块和一个混色连通块合并。这个过程减少了\(R\)个连通块,于是我们还需要\(n-(m-R)\)次操作来把连通块全部拆掉。\(R\leq B\)的情况同理。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+10;
int n,m,Ans,Tb,Tr,P[MAXN];
int Fa[MAXN],Bs[MAXN],Rs[MAXN],Vis[MAXN];
int Find(int S){ return Fa[S]==S?S:Fa[S]=Find(Fa[S]); }
int Max(int A,int B){ return A>B?A:B; }
void Merge(int A,int B)
{ A=Find(A),B=Find(B);
if(A!=B) Fa[A]=B,Bs[B]+=Bs[A],Rs[B]+=Rs[A];
}
int main()
{ scanf("%d%d",&n,&m),Ans=n+m;
for(int i=1;i<=n+m;i++) Fa[i]=i,i<=n?Bs[i]=1:Rs[i]=1;
for(int i=1;i<=n+m;i++) scanf("%d",&P[i]),Merge(i,P[i]);
for(int i=1,Now;i<=n+m;i++)
if(!Vis[Now=Find(i)]) Ans--,Vis[Now]=1,Tb+=(Bs[Now]>=2&&!Rs[Now]),Tr+=(Rs[Now]>=2&&!Bs[Now]);
printf("%d\n",Ans+2*Max(Tb,Tr));
}
E.Pass to Next
题目链接
简要题解
我们发现答案只和最终的状态有关,但是不同的操作过程可能会产生相同的结果,于是我们需要想办法使计算过程不重复。
对于这样一个过程来说,我们可以用一个序列{\(c_i\)}来描述,\(c_i\)指的是第\(i\)个人传给了第\(i+1\)个人的球数(\(c_n\)是传给第一个人)
如果\(c_i>0\)全部成立,那么我们可以把每个\(c_i\)减一,使得结果不变,重复若干次操作我们将使得\(min(c_i)=0\)的一个序列。
这时不同的{\(c_i\)}序列就对应着不同的结果,不会重复。
我们可以先算无限制的{\(c_i\)}答案,再算限制\(c_i\)全部大于\(0\)的答案,两者相减就是我们想要的答案。
不难发现,我们某一个结果的贡献,是每个人手中球数的乘积。而每个人手中的球有两个来源:自己本来就有的,上一个人传过来的。
根据乘法分配率,我们可以把这两部分贡献分开来算。
我们设\(F[i][0]\)表示,只算第\(i\)个人自己的球,前\(i-1\)个人的贡献(第\(i\)个人需要将自己的球传给下一个,所以自己的贡献现在算不了)。
还有设\(F[i][1]\)表示,只算第\(i\)个人从上一个人传过来的球,前\(i\)个人的贡献。
我们定义两个函数\(S1(x)=\sum_{i=1}^xi\),\(S2(x)=\sum_{i=1}^x i^2\),具体用处在后面会提到。
当{\(c_i\)}无限制时,状态转移方程有四个:
上式计算第\(i\)个人自己球的贡献,很明显他可以留下\(0\)~\(A[i]\)个球,这些情况全部相加得到\(S1(A[i])\)。
上式没有计算球的贡献,但是\(c_i\)有\(A[i]+1\)种取值,每种取值对应了一种结果,所以我们需要乘上情况数。
上式计算第\(i\)个人自己球的贡献,以及第\(i\)个人传给第\(i+1\)个人的球的贡献。
枚举传了几个球我们可以知道贡献为\(\sum_{k=0}^{A[i]}(A[i]-k)*k\),转换一下就是\(A[i]*S1(A[i])-S2(A[i])\)
上式计算了第\(i\)个人传给第\(i+1\)个人的球的贡献,可以传\(0\)~\(A[i]\)个球,这些情况全部相加得到\(S1(A[i])\)。
由于这\(n\)个人围成了一个环,我们需要将\(F[n][0/1]\)的贡献转移给\(F[1][0/1]\),方便起见可以存在\(F[n+1][0/1]\)
赋初值的时候,直接令\(F[1][0]=1\)或\(F[1][1]=1\)即可,但必须分开算,即两者只有一个有值,分别算一遍加起来才是我们需要的答案。
如果一起算的话,\(F[1][0]\)的贡献经过传递会进入\(F[n+1][1]\),也就是影响到了\(F[1][1]\),而显然这两种情况是独立的。
当{\(c_i\)}有限制时,只有两处不一样。
第一个转移方程的贡献为\(S1(A[i]-1)\),因为第\(i\)个人必须传球,也就是留下\(A[i]\)个球的情况不合法。
第二个转移方程的贡献为\(A[i]\),少了一种不传球的情况。
时间复杂度\(O(n)\)
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
const int Mod=998244353;
const int Inv2=499122177;
const int Inv6=166374059;
int n,Ans,A[MAXN],F[MAXN][2];
int Add(int A,int B){ return A+=B,A>=Mod?A-Mod:A; }
int S1(int S){ return 1ll*S*(S+1)%Mod*Inv2%Mod; }
int S2(int S){ return 1ll*S*(S+1)%Mod*(2*S+1)%Mod*Inv6%Mod; }
int Calc(int St,int K)
{ for(int i=0;i<=n+1;i++) F[i][0]=F[i][1]=0;
F[1][St]=1;
for(int i=1;i<=n;i++)
{ F[i+1][0]=Add(1ll*F[i][0]*S1(A[i]-K)%Mod,1ll*F[i][1]*(A[i]+1-K)%Mod);
F[i+1][1]=Add((1ll*A[i]*S1(A[i])+Mod-S2(A[i]))%Mod*F[i][0]%Mod,1ll*F[i][1]*S1(A[i])%Mod);
}
return F[n+1][St];
}
int main()
{ scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&A[i]);
A[n+1]=A[1],Ans=Add(Calc(0,0),Add(Calc(1,0),Add(Mod-Calc(0,1),Mod-Calc(1,1)))),printf("%d\n",Ans);
}