专栏文章
「题单题解」数数入门
算法·理论参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @minn07oq
- 此快照首次捕获于
- 2025/12/02 05:04 3 个月前
- 此快照最后确认于
- 2025/12/02 05:04 3 个月前
P1758 [NOI2009] 管道取珠
题解
有一个经典的 trick 是,可以将每个东西出现次数的平方转化为选择两个相同东西的方案数。对于此题,即为将 转化为选择两个相同的输出序列的方案数。
考虑 dp:设 表示,选择两个长度为 的相同的输出序列,第一个输出序列由上管道中的 个球和下管道中的 个球构成,第二个输出序列由上管道中的 个球和下管道中的 个球构成的方案数。转移时枚举两个输出序列的下一位分别选择哪个管道即可。
对第一维做滚动优化,时间复杂度 ,空间复杂度 。
代码
Cconst int N=505,mod=1024523;
int n,m,f[2][N][N];
string s,t;
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n>>m>>s>>t;
f[0][0][0]=1;
for(int i=0;i<n+m;i++){
int x=i&1,y=(i&1)^1;
for(int j=0;j<=min(n,i);j++){
for(int k=0;k<=min(n,i);k++){
if(j<n&&k<n&&s[j]==s[k]) add(f[y][j+1][k+1],f[x][j][k]);
if(j<n&&i-k<m&&s[j]==t[i-k]) add(f[y][j+1][k],f[x][j][k]);
if(i-j<m&&k<n&&t[i-j]==s[k]) add(f[y][j][k+1],f[x][j][k]);
if(i-j<m&&i-k<m&&t[i-j]==t[i-k]) add(f[y][j][k],f[x][j][k]);
}
}
memset(f[x],0,sizeof f[x]);
}
cout<<f[(n+m)&1][n][n];
}
ABC295E Kth Number
题解
设 中 的个数为 。题目需要我们求出 的期望值,转化一下可以变成:求对于所有 种序列 , 之和,即:
求第 小的数不好求,于是考虑把 的贡献拆到每一个 的 上,式子变成:
于是我们可以枚举 ,计算有多少种 满足 。
设 中小于 的不为 的数有 个,枚举一下再添加多少个小于 的数,那么答案即为:
注意特判 的情况。
时间复杂度 。
代码
Cconst int N=2005,mod=998244353;
int n,m,k,a[N],pw[N][N],c,fac[N],infac[N],ans;
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
int C(int n,int m){
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
void solve(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]==0) c++;
}
for(int i=0;i<N;i++){
pw[i][0]=1;
for(int j=1;j<N;j++) pw[i][j]=1ll*pw[i][j-1]*i%mod;
}
fac[0]=1,infac[0]=1;
for(int i=1;i<N;i++) fac[i]=1ll*fac[i-1]*i%mod,infac[i]=ksm(fac[i],mod-2);
for(int v=1;v<=m;v++){
int x=0;
for(int i=1;i<=n;i++) if(a[i]!=0&&a[i]<v) x++;
int res=0;
for(int i=0;i<k-x;i++) if(c>=i) res+=1ll*C(c,i)*pw[v-1][i]%mod*pw[m-v+1][c-i]%mod,res%=mod;
ans=(ans+res)%mod;
}
cout<<1ll*ans*ksm(pw[m][c],mod-2)%mod;
}
ARC139D Priority Queue 2
题解
首先把总和拆开,将 转化为 。
于是考虑枚举 ,计算 的总和。
设初始时满足 的 的个数等于 ,操作过程中一共加了 个大于等于 的数:
- 若 ,则最终满足 的 的个数等于 ;
- 若 ,则最终满足 的 的个数等于 。
于是可以得到:
预处理后直接计算即可。时间复杂度 。
代码
C#define int long long
const int N=2005,mod=998244353;
int n,m,k,x,a[N],pw[N][N],fac[N],infac[N],ans;
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=res*a%mod;
b>>=1,a=a*a%mod;
}
return res;
}
int f(int c,int p){
if(c>=n-x+1) return max(n-x+1,c+p-k);
else return min(n-x+1,c+p);
}
int C(int n,int m){
return fac[n]*infac[m]%mod*infac[n-m]%mod;
}
void solve(){
cin>>n>>m>>k>>x;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=0;i<=m;i++){
pw[i][0]=1;
for(int j=1;j<=k;j++) pw[i][j]=pw[i][j-1]*i%mod;
}
fac[0]=infac[0]=1;
for(int i=1;i<=k;i++) fac[i]=fac[i-1]*i%mod;
infac[k]=ksm(fac[k],mod-2);
for(int i=k-1;i>0;i--) infac[i]=infac[i+1]*(i+1)%mod;
for(int j=1;j<=m;j++){
int c=0;
for(int i=1;i<=n;i++) if(a[i]>=j) c++;
for(int p=0;p<=k;p++) ans+=f(c,p)*pw[m-j+1][p]%mod*pw[j-1][k-p]%mod*C(k,p),ans%=mod;
}
cout<<ans<<endl;
}
ARC150D Removing Gacha
题解
显然总期望操作次数等于每个点期望操作次数之和,于是考虑对于每个点 ,计算点 被操作次数的期望。
设点 的深度为 ,那么点 是否还需要被操作只和点 到根路径上的 个点的状态有关,所以我们可以只考虑这 个点。
于是现在问题转化为了,有 个点,每次随机选择一个点,直到每个点都被选择过为止,求第 个点被选择次数的期望。
设当前已经选择了 个点,那么有 的概率选择到一个新点,也就是期望 次选择到一个新点。相加可以得到总的期望选择次数等于 ,而每个点被选择到的概率相等,所以第 个点期望被选择 次。
将所有点期望操作次数相加,得到答案为 。直接计算即可,时间复杂度 。
代码
Cconst int N=2e5+5,mod=998244353;
int n,p[N],d[N],ans,fac[N],infac[N],inv[N],pre[N];
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1,a=1ll*a*a%mod;
}
return res;
}
void solve(){
cin>>n;
for(int i=2;i<=n;i++) cin>>p[i];
fac[0]=infac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
infac[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>0;i--) infac[i]=1ll*infac[i+1]*(i+1)%mod;
for(int i=1;i<=n;i++) inv[i]=1ll*fac[i-1]*infac[i]%mod;
for(int i=1;i<=n;i++) pre[i]=(pre[i-1]+inv[i])%mod;
for(int i=1;i<=n;i++) d[i]=d[p[i]]+1,ans=(ans+pre[d[i]])%mod;
cout<<ans<<endl;
}
ARC165E Random Isolation
题解
由于每个点最多只会被删除一次,且每个连通块不会增大,所以可以将题意转化为:随机一个排列 ,从 到 依次考虑每个点 ,如果点 所处的连通块大小大于 则删除点 ,否则不进行操作,求期望删除次数。
设 表示原树的子图 作为一个完整的连通块的出现概率。如果图 包含大于 个点,那么当图 作为一个完整的连通块出现时,一定会进行恰好一次删除操作。所以,期望删除次数就等于每个包含大于 个点的子图 作为一个完整的连通块的出现概率之和,即 。
接下来考虑怎么计算 。设图 包含 个点,原树中有 个点与图 中的点相连,那么要求只有这 个点在排列中的顺序均在这 个点前,所以 等于 。
于是可以进行树形 dp:设 表示,满足图 中深度最小的点为 ,包含 个点,以 为根的子树中有 个点与图 中的点相连的原树的子图 的数量。直接从儿子处转移即可。
时间复杂度 。
代码
Cconst int N=105,mod=998244353;
int n,k,siz[N],fac[N],infac[N],f[N][N][N],g[N][N],ans;
vector <int> ve[N];
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1;
a=1ll*a*a%mod;
}
return res;
}
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
void dfs(int u,int fa){
f[u][1][0]=1,siz[u]=1;
for(auto v:ve[u]){
if(v==fa) continue;
dfs(v,u);
memset(g,0,sizeof g);
for(int i=0;i<=siz[u];i++) for(int j=0;i+j<=siz[u];j++) for(int p=0;p<=siz[v];p++) for(int q=0;p+q<=siz[v];q++) add(g[i+p][j+q],1ll*f[u][i][j]*f[v][p][q]%mod);
siz[u]+=siz[v];
for(int i=0;i<=siz[u];i++) for(int j=0;j<=siz[u];j++) f[u][i][j]=g[i][j];
}
f[u][0][1]=1;
}
void solve(){
cin>>n>>k;
for(int i=1,u,v;i<n;i++) cin>>u>>v,ve[u].pb(v),ve[v].pb(u);
fac[0]=infac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
infac[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>=1;i--) infac[i]=1ll*infac[i+1]*(i+1)%mod;
dfs(1,0);
for(int u=1;u<=n;u++){
for(int i=k+1;i<=siz[u];i++){
for(int j=0;i+j<=siz[u];j++){
int p=j+(u!=1);
add(ans,1ll*f[u][i][j]*fac[i]%mod*fac[p]%mod*infac[i+p]%mod);
}
}
}
cout<<ans<<endl;
}
P8321 『JROI-4』沈阳大街 2
题解
考虑把 看作 类数, 看作 类数,将所有 和所有 扔进一个长度为 的数列 中并从大到小排序,每次选出一个 类数和 类数进行匹配,贡献乘上较小的数的值,将问题转化为求所有匹配方案的贡献之和。
设 表示考虑到数列 的第 项,此时一共匹配了 对数的方案的贡献的和。转移时,首先根据 和前缀和数组,计算出未匹配的 类数数量 和未匹配的 类数数量 ,再根据 的类型分别处理:
- 若 为 类数:
- 若 :对 进行匹配,;
- 不对 进行匹配,。
- 若 为 类数:
- 若 :对 进行匹配,;
- 不对 进行匹配,。
初始化 ,答案即为 。
时间复杂度 。
代码
Cconst int N=5005,mod=998244353;
int n,f[N<<1][N],s[N<<1][3];
pii p[N<<1];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1,a=1ll*a*a%mod;
}
return res;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++) cin>>p[i].fi,p[i].se=1;
for(int i=1;i<=n;i++) cin>>p[n+i].fi,p[n+i].se=2;
sort(p+1,p+n+n+1),reverse(p+1,p+n+n+1);
for(int i=1;i<=n+n;i++) for(int k=1;k<=2;k++) s[i][k]=s[i-1][k]+(p[i].se==k);
f[0][0]=1;
for(int i=0;i<n+n;i++){
for(int j=0;j<=min(s[i][1],s[i][2]);j++){
int x=s[i][1]-j,y=s[i][2]-j;
if(p[i+1].se==1){
if(y>0) add(f[i+1][j+1],1ll*y*p[i+1].fi%mod*f[i][j]%mod);
add(f[i+1][j],f[i][j]);
}
else{
if(x>0) add(f[i+1][j+1],1ll*x*p[i+1].fi%mod*f[i][j]%mod);
add(f[i+1][j],f[i][j]);
}
}
}
int fac=1;
for(int i=1;i<=n;i++) fac=1ll*fac*i%mod;
cout<<1ll*ksm(fac,mod-2)*f[n+n][n]%mod<<endl;
}
ARC207A Affinity for Artifacts
题解
设总成本为 ,第 个灯是第 个被点亮的。考虑每个灯节约了多少成本,则显然有:
考虑把 看作 类数, 看作 类数,将所有 和所有 扔进一个长度为 的数列 中并从大到小排序,每次选出一个 类数和 类数进行匹配,贡献为较小的数的值。
设 表示考虑到数列 的第 项,此时一共匹配了 对数,总贡献为 的方案数。转移时,首先根据 和前缀和数组,计算出未匹配的 类数数量 和未匹配的 类数数量 ,再根据 的类型分别处理:
- 若 为 类数:
- 若 :对 进行匹配,;
- 不对 进行匹配,。
- 若 为 类数:
- 若 :对 进行匹配,;
- 不对 进行匹配,。
初始化 。设 ,特判一下 和 的情况,答案即为 。
时间复杂度 。
代码
Cconst int N=105,mod=998244353;
int n,x,a[N],s[N<<1][3],f[N<<1][N][N*(N-1)/2];
ll sum,v;
pii p[N<<1];
int get(int i,int j){
int a=s[i][1],b=s[i][2];
return b-a+j;
}
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n>>x;
for(int i=1;i<=n;i++) cin>>a[i],sum+=a[i];
v=sum-x;
if(v<0) v=0;
if(v>n*(n-1)/2) v=n*(n-1)/2+1;
for(int i=1;i<=n;i++) p[i]={a[i],1},p[n+i]={i-1,2};
sort(p+1,p+n+n+1),reverse(p+1,p+n+n+1);
for(int i=1;i<=n+n;i++) for(int j=1;j<=2;j++) s[i][j]=s[i-1][j]+(p[i].se==j);
f[0][0][0]=1;
for(int i=0;i<n+n;i++){
for(int j=0;j<=min(s[i][1],s[i][2]);j++){
int x=s[i][1]-j,y=s[i][2]-j;
for(int s=0;s<=n*(n-1)/2;s++){
if(p[i+1].se==1){
if(y!=0) add(f[i+1][j+1][s+p[i+1].fi],1ll*f[i][j][s]*y%mod);
add(f[i+1][j][s],f[i][j][s]);
}
else{
if(x!=0) add(f[i+1][j+1][s+p[i+1].fi],1ll*f[i][j][s]*x%mod);
add(f[i+1][j][s],f[i][j][s]);
}
}
}
}
int ans=0;
for(int i=v;i<=n*(n-1)/2;i++) add(ans,f[n+n][n][i]);
cout<<ans<<endl;
}
ARC121E Directed Tree
题解
将对排列 的计数转化为对其逆排列 的计数,那么此时要求结点 不在结点 的子树内。
考虑容斥。设 表示钦定 个结点不合法,且只考虑这 个结点的方案数,那么 。
接下来思考如何求 。设 表示以 为根的子树内,钦定了 个结点不合法,且只考虑这 个结点的方案数。考虑转移:
- 新加入一个儿子时,因为两个子树的不合法的集合是不交的,所以可以直接合并,转移方程为 ;
- 加入所有儿子后,枚举点 是否合法,得到转移方程为 。
于是 就等于 。时间复杂度 。
代码
Cconst int N=2005,mod=998244353;
int n,p[N],siz[N],f[N][N],tmp[N],fac[N],ans;
vector <int> ve[N];
void add(int &u,int v){
u+=v;
if(u>=mod) u-=mod;
}
void dfs(int u){
f[u][0]=1;
for(auto v:ve[u]){
dfs(v);
for(int i=0;i<=siz[u];i++) tmp[i]=f[u][i],f[u][i]=0;
for(int i=0;i<=siz[u];i++) for(int j=0;j<=siz[v];j++) add(f[u][i+j],1ll*tmp[i]*f[v][j]%mod);
siz[u]+=siz[v];
}
siz[u]++;
for(int i=siz[u]-1;i>=0;i--) add(f[u][i+1],1ll*f[u][i]*(siz[u]-i-1)%mod);
}
void solve(){
cin>>n;
for(int i=2;i<=n;i++) cin>>p[i],ve[p[i]].pb(i);
fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
dfs(1);
for(int i=0;i<=n;i++){
if(i&1) add(ans,mod-1ll*f[1][i]*fac[n-i]%mod);
else add(ans,1ll*f[1][i]*fac[n-i]%mod);
}
cout<<ans<<endl;
}
CF2048G Kevin and Matrices
题解
先考虑 的情况。此时需要满足,存在 使得存在一行都小于 ,且存在一列都大于等于 。显然这两个条件无法同时满足,故这种情况不存在。
接下来考虑 的情况。设这个值等于 ,那么相当于要求:
- 存在一行的最大值等于 ,且每一行的最大值都大于等于 ;
- 存在一列的最小值等于 ,且每一列的最小值都小于等于 。
设此时的答案为 , 表示每一行的最大值都大于等于 且每一列的最小值都小于等于 的方案数,那么做一个二维差分即可得到:
于是问题转化为了如何求 。
考虑容斥:
- 钦定 行的最大值都小于 ;
- 对于每一列:
- 先不考虑列的限制,方案数为 ;
- 再减掉这一列的最小值大于 的方案数,于是需要减去 ;
- 由于每一列是等价的,所以其对答案的贡献为:
这里我们认为 。预处理组合数后枚举 并快速幂计算即可。
时间复杂度 。
代码
Cconst int N=1e6+5,mod=998244353;
int n,m,v,fac[N],infac[N];
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1;
a=1ll*a*a%mod;
}
return res;
}
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
void init(int n){
fac[0]=infac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
infac[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>0;i--) infac[i]=1ll*infac[i+1]*(i+1)%mod;
}
int C(int n,int m){
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
int f(int a,int b){
int res=0;
for(int i=0;i<=n;i++){
int val=ad(1ll*ksm(v,n-i)*ksm(a-1,i)%mod,mod-1ll*ksm(v-b,n-i)*ksm(max(a-b-1,0),i)%mod);
int ans=1ll*C(n,i)*ksm(val,m)%mod;
if(i&1) add(res,mod-ans);
else add(res,ans);
}
return res;
}
void solve(){
cin>>n>>m>>v;
init(n);
int ans=0;
for(int k=1;k<=v;k++) add(ans,(0ll+f(k,k)-f(k+1,k)-f(k,k-1)+f(k+1,k-1)+mod+mod)%mod);
cout<<ans<<endl;
}
P7154 [USACO20DEC] Sleeping Cows P
题解
为了区分「等待被匹配」和「没有被匹配」,在本文中,若一个奶牛 / 牛棚没有被匹配,则称它被扔掉了。
首先分析一下极大匹配的性质:
- 如果存在一个奶牛 被扔掉了,那么所有满足 的牛棚 必须被匹配;
- 如果存在一个牛棚 被扔掉了,那么所有满足 的奶牛 必须被匹配。
于是考虑把所有 和 都扔进数组 里,将数组 排序(若 则 在前)之后做一个 dp:设 表示考虑到 ,在此之前有 个奶牛正在等待被匹配,且之前有 / 没有被扔掉的奶牛;注意这 个奶牛必须在之后被全部匹配。
对于转移方程,考虑分类讨论:
- 若 为奶牛(即 属于 ):
- (保留该奶牛);
- (扔掉该奶牛);
- (保留该奶牛);
- (扔掉该奶牛)。
- 若 为牛棚(即 属于 ):
- (选择一个奶牛进行匹配);
- (选择一个奶牛进行匹配);
- (扔掉该牛棚)。
答案即为 。时间复杂度 。
代码
Cconst int N=3005,mod=1e9+7;
int n,m,f[N<<1][N][2];
pii p[N<<1];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n,m=n+n;
for(int i=1;i<=n;i++) cin>>p[i].fi,p[i].se=0;
for(int i=1;i<=n;i++) cin>>p[i+n].fi,p[i+n].se=1;
sort(p+1,p+1+m);
f[0][0][1]=1;
for(int i=0;i<m;i++){
for(int j=0;j<=min(n,i);j++){
if(p[i+1].se==0){
add(f[i+1][j+1][0],f[i][j][0]);
add(f[i+1][j][0],f[i][j][0]);
add(f[i+1][j+1][1],f[i][j][1]);
add(f[i+1][j][0],f[i][j][1]);
}
else{
add(f[i+1][j-1][0],1ll*j*f[i][j][0]%mod);
add(f[i+1][j-1][1],1ll*j*f[i][j][1]%mod);
add(f[i+1][j][1],f[i][j][1]);
}
}
}
cout<<(f[m][0][0]+f[m][0][1])%mod<<endl;
}
P14364 [CSP-S 2025] 员工招聘
题解
设集合 ,考虑钦定一个集合 ,满足:
- 对于所有 ,第 个位置的人会被录取,即要求 ;
- 对于所有 ,第 个位置的人不会被录取,即要求 。
考虑容斥,把第一个条件的形式与第二个条件统一。具体地,钦定一个集合 ,满足:
- 对于所有 ,第 个位置的人不会被录取,即要求 ;
- 对于所有 ,第 个位置的人没有要求。
容斥系数为 。
考虑从前往后 dp。设 表示,考虑到第 个位置,此时一共有 个数在 中, 个数在 中,且仅考虑 中的位置填的数的方案数。初始化 ,设 ,,考虑转移:
- 若 ,;
- 若 :
- 令 ,;
- 令 ,;
- 令 ,。
其中,系数为 的原因是:当前位置填的数需要小于等于 ,且之前已经用了 个这样的数。
答案即为 ,其中 是满足 的 的数量,这些位置上的数可以任意排列。
使用滚动数组优化,空间复杂度 ,时间复杂度 。
代码
Cconst int N=505,mod=998244353;
int n,m,c[N],s[N],a[N],b[N],f[2][N][N],fac[N],ans;
string str;
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n>>m>>str;
for(int i=1;i<=n;i++) cin>>c[i],a[c[i]]++;
for(int i=1;i<=n;i++) s[i]=(str[i-1]=='1'),b[i]=b[i-1]+s[i];
for(int i=0;i<=n;i++) a[i]+=a[i-1];
f[0][0][0]=1,fac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=0;i<n;i++){
int t=i&1;
memset(f[t^1],0,sizeof f[t^1]);
for(int j=0;j<=b[i];j++){
for(int k=0;k<=j;k++){
if(s[i+1]==0) add(f[t^1][j][k],f[t][j][k]);
else{
add(f[t^1][j+1][k],f[t][j][k]);
int x=a[i-j]-k-b[i]+j;
add(f[t^1][j][k],1ll*f[t][j][k]*x%mod);
add(f[t^1][j+1][k+1],1ll*f[t][j][k]*x%mod);
}
}
}
}
for(int j=m;j<=b[n];j++){
for(int k=0;k<=j;k++){
int x=n-b[n]+j-k;
if(k&1) add(ans,mod-1ll*f[n&1][j][k]*fac[x]%mod);
else add(ans,1ll*f[n&1][j][k]*fac[x]%mod);
}
}
cout<<ans<<endl;
}
CF2129D Permutation Blackhole
题解
由于对白色区间内的某个点染色后,这个区间会分成两个互不干扰的白色子区间,因此考虑区间 dp:设 表示,位置 和位置 已经被染色,只考虑区间 内的贡献和被染色的相对顺序,位置 的得分增加了 且位置 的得分增加了 的方案数。
由于对同一个数造成多次贡献会导致区间长度每次减半,所以一个数的得分上限是 级别的。于是我们可以知道,只有数组 的前两维是 的,而后两维都是 的。
设 表示对白色区间 中的位置 染色所造成贡献的位置,那么有:
- 若 ,则 ;
- 否则,若 ,则 ;
- 否则,若 ,则 。
- 否则,。
转移考虑枚举区间 内第一个被染色的位置 ,分类讨论:
- 若 ,则 ;
- 若 ,则 。
其中, 的情况可以直接枚举 ,而 的情况可以先计算 和 再相乘,做到每次转移 。
对于所有满足 的 ,初始化 ,并对于所有不大于 的非负整数 ,初始化 ,那么答案即为 。
时间复杂度 。
代码
Cconst int N=105,L=8,mod=998244353;
int n,s[N],fac[N],infac[N],f[N][N][L][L];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return res;
}
int choose(int n,int m){
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
int w(int l,int r,int k){
if(r==n) return l-1;
else if(l==1) return r+1;
else if(k+k<=l+r) return l-1;
else return r+1;
}
void solve(){
cin>>n;
fac[0]=infac[0]=1;
for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
infac[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>=1;i--) infac[i]=1ll*infac[i+1]*(i+1)%mod;
for(int i=0;i<=n+1;i++) for(int j=0;j<=n+1;j++) for(int x=0;x<L;x++) for(int y=0;y<L;y++) f[i][j][x][y]=0;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=1;i<=n;i++) if(s[i]>L+L-2) return cout<<0<<endl,void();
for(int i=1;i<=n;i++) if(s[i]<=0) f[i][i][w(i,i,i)==i-1][w(i,i,i)==i+1]=1;
for(int i=0;i<=n;i++) f[i+1][i][0][0]=1;
for(int len=2;len<=n;len++){
for(int l=1,r=len;r<=n;l++,r++){
for(int x=0;x<L;x++){
for(int y=0;y<L;y++){
for(int k=l;k<=r;k++){
if(s[k]==-1){
int sum1=0,sum2=0;
for(int b=0;b<L;b++) add(sum1,f[l][k-1][x-(w(l,r,k)==l-1)][b]);
for(int a=0;a<L;a++) add(sum2,f[k+1][r][a][y-(w(l,r,k)==r+1)]);
add(f[l][r][x][y],1ll*sum1*sum2%mod*choose(r-l,k-l)%mod);
}
else{
for(int v=0;v<L;v++){
if(s[k]-v>=L||s[k]-v<0) continue;
int val1=f[l][k-1][x-(w(l,r,k)==l-1)][v],val2=f[k+1][r][s[k]-v][y-(w(l,r,k)==r+1)];
add(f[l][r][x][y],1ll*val1*val2%mod*choose(r-l,k-l)%mod);
}
}
}
}
}
}
}
cout<<f[1][n][1][0]<<endl;
}
P11363 [NOIP2024] 树的遍历
题解
对于原树中的某个原点 和所有与其相邻的原边 ,显然在新树中 之间的新边会形成一条链。
接下来分析新树的构建过程。不妨设遍历起始边为 :
- 把树从 处分成两部分,并让两部分分别以 和 为根;
- 对于每部分中的每个结点 ,设其父亲为 ,则所有与 相邻的原边之间的新边形成的链一定满足 为链的一个端点,而其他原边的顺序无所谓;其中 ,。
有一个巧妙的想法是,当新树固定时,对于每个结点 ,设所有所有与 相邻的原边之间的新边形成的链的另一个端点为 ,把 看成 的重儿子,则这棵树的两部分相当于各进行了一次树链剖分!设以 为链顶的重链的链底为 ,以 为链顶的重链的链底为 ,则根据树链剖分的性质可以得到,把两部分拼起来之后,原树中一定只有恰好一条从叶子到叶子的重链 。
然后我们反过来考虑,当 固定时,有多少种新树满足条件。显然,对于 上的点 (不包括 和 ),所有与 相邻的原边之间的新边形成的链的两个端点已经固定,方案数为 ;而对于不在 上的点 ,所有与 相邻的原边之间的新边形成的链的一个端点是固定的,方案数为 ;拼起来就能得到,当 固定时,满足条件的新树的数量为:
变形一下可得:
这里认为 。
注意需要满足 上要有至少一条给定的关键边,因为我们要在 上选择一条边来构建新树。
于是我们可以任选一个 不小于 的点为树的根进行 dp:设 表示在以结点 为根的子树内,从叶子 到结点 的路径上没有 / 有关键边的所有叶子 的 之和。转移时枚举儿子并计算其对 dp 值和答案的贡献即可。
时间复杂度 。
代码
CPPconst int N=1e5+5,mod=1e9+7;
vector <pii> ve[N];
int n,k,r,e[N];
ll dp[N][2],fac[N],infac[N],inv[N],ans,pro;
bool vis[N];
int ksm(ll a,int b){
ll res=1;
while(b){
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
void dfs(int u,int f){
for(auto p:ve[u]){
int v=p.fi;
if(v==f) continue;
dfs(v,u);
if(vis[p.se]){
(ans+=dp[v][0]*dp[u][0]+dp[v][0]*dp[u][1]+dp[v][1]*dp[u][0]+dp[v][1]*dp[u][1])%=mod;
(dp[u][1]+=dp[v][0]*inv[ve[u].size()-1]+dp[v][1]*inv[ve[u].size()-1])%=mod;
}
else{
(ans+=dp[v][0]*dp[u][1]+dp[v][1]*dp[u][0]+dp[v][1]*dp[u][1])%=mod;
(dp[u][1]+=dp[v][1]*inv[ve[u].size()-1])%=mod;
(dp[u][0]+=dp[v][0]*inv[ve[u].size()-1])%=mod;
}
}
// cout<<inv[ve[u].size()-1]<<' '<<u<<' '<<dp[u][0]<<' '<<dp[u][1]<<endl;
if(ve[u].size()==1) dp[u][0]=1;
}
void solve(){
cin>>n>>k;
fac[0]=1,infac[0]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
infac[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>=1;i--) infac[i]=infac[i+1]*(i+1)%mod;
for(int i=n-1;i>=1;i--) inv[i]=fac[i-1]*infac[i]%mod;
for(int i=1;i<=n;i++) ve[i].clear(),vis[i]=0,dp[i][0]=dp[i][1]=0;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
ve[u].pb({v,i});
ve[v].pb({u,i});
}
for(int i=1;i<=k;i++) cin>>e[i],vis[e[i]]=1;
if(n==2) return cout<<1<<endl,void();
for(int i=1;i<=n;i++) if(ve[i].size()>=2) r=i;
pro=1,ans=0;
dfs(r,0);
for(int i=1;i<=n;i++) pro=pro*fac[ve[i].size()-1]%mod;
cout<<ans*pro%mod<<endl;
}
ARC118E Avoid Permutations
题解
直接做有点困难,因此考虑钦定一些尚未确定的障碍点在路径上。设被钦定的障碍点的集合为 时,满足条件的路径数为 ,则根据容斥原理可知答案可以表示为 。
设 表示,考虑到第 行第 列的格子,此时一共钦定了 个障碍点,第 行没有 / 有障碍点,第 列没有 / 有障碍点时的方案数。初始时 。
设 表示第 行没有 / 有确定的障碍点, 表示第 列没有 / 有确定的障碍点。对于 ,考虑转移:
- 如果 处有障碍点,则不做转移;
- 如果 处没有障碍点:
- 如果 :
- 不在 处钦定障碍点,。
- 如果 且 :在 处钦定障碍点,。
- 如果 :
- 不在 处钦定障碍点,。
- 如果 且 :在 处钦定障碍点,。
- 如果 :
设 表示初始时 的数量,答案即为:
其中乘 是因为没有被钦定的 可以在剩余的 个位置任意排列。
时间复杂度 。
代码
Cconst int N=205,mod=998244353;
int n,m,a[N],fac[N],f[N][N][N][2][2],ans;
bool vis[N][N],x[N],y[N];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n,fac[0]=1,f[0][0][0][1][1]=1;
for(int i=1;i<=n;i++){
cin>>a[i];
if(a[i]!=-1) vis[i][a[i]]=1,x[i]=1,y[a[i]]=1;
else m++;
}
for(int i=1;i<=m;i++) fac[i]=1ll*fac[i-1]*i%mod;
for(int i=0;i<=n+1;i++){
for(int j=0;j<=n+1;j++){
if(vis[i][j]) continue;
for(int k=0;k<=min(i,j);k++){
for(int a=0;a<=1;a++){
for(int b=0;b<=1;b++){
if(i<=n){
add(f[i+1][j][k][x[i+1]][b],f[i][j][k][a][b]);
if(!a&&!b) add(f[i+1][j][k+1][x[i+1]][1],f[i][j][k][a][b]);
}
if(j<=n){
add(f[i][j+1][k][a][y[j+1]],f[i][j][k][a][b]);
if(!a&&!b) add(f[i][j+1][k+1][1][y[j+1]],f[i][j][k][a][b]);
}
}
}
}
}
}
for(int k=0;k<=m;k++){
if(k&1) add(ans,mod-1ll*f[n+1][n+1][k][0][0]*fac[m-k]%mod);
else add(ans,1ll*f[n+1][n+1][k][0][0]*fac[m-k]%mod);
}
cout<<ans<<endl;
}
ARC124E Pass to Next
题解
设第 个人给下一个人传了 个球,第 个人的手中最终有 个球,则有 ,其中我们认为 。显然,若 则可以通过使所有 都减去 的方式保持最终局面不变,而所有 所形成的最终局面又两两不同,也就是说所有 的操作序列 与最终局面 形成了双射。因此,我们考虑转化计数对象:对于所有 且 的数列 ,计算 的和。
先上一个经典手法,把所有 且 的数列 的 之和转化为,所有 的数列 的 之和减去所有 的数列 的 之和。
接下来尝试计算所有 的数列 的 之和。考虑其组合意义:对于所有最终局面,从每个人手中选一个球的方案数。
设 表示考虑完前 个人且第 个人选择了自己的球的方案数, 表示考虑完前 个人且第 个人选择了上一个人传来的球的方案数。这样设计状态是因为,若第 个人选择了自己的球,则其贡献需要在 向 或 转移时计算,而若第 个人选择了上一个人传来的球,则其贡献需要在 或 向 转移时计算。
转移方程为:
- (枚举留下几个球);
- (枚举传了几个球)。
由于这是一个环,所以我们还需要枚举一下第 个人的状态:先初始化 ,dp 一轮,给答案加上 ;再初始化 ,重新 dp 一轮,给答案加上 。
解决了 的情况, 的情况是基本相同的,但由于要求每个人都要传递至少一个球,所以转移方程变为了:
- (枚举留下几个球);
- (枚举传了几个球)。
其余部分都是一样的,按照上面的方法计算即可。
时间复杂度 。
代码
Cconst int N=3e5+5,mod=998244353,inv2=499122177,inv6=166374059;
int n,a[N],f[N],g[N];
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
int s1(int x){
return 1ll*x*(x+1)%mod*inv2%mod;
}
int s2(int x){
return 1ll*x*(x+1)%mod*(x+x+1)%mod*inv6%mod;
}
int work(int x,int y){
if(!x) f[1]=1,g[1]=0;
else f[1]=0,g[1]=1;
for(int i=1;i<=n;i++){
f[i+1]=ad(1ll*f[i]*s1(a[i]-y)%mod,1ll*g[i]*(a[i]-y+1)%mod);
g[i+1]=ad(1ll*f[i]*ad(1ll*a[i]*s1(a[i])%mod,mod-s2(a[i]))%mod,1ll*g[i]*s1(a[i])%mod);
}
if(!x) return f[n+1];
else return g[n+1];
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cout<<ad(ad(work(0,0),work(1,0)),mod-ad(work(0,1),work(1,1)))<<endl;
}
ARC184D Erase Balls 2D
题解
将所有球按照 排序,那么第 个球满足 。显然选择的球的顺序并不重要,我们只需要明确哪些球被选了。为了方便,我们在 和 处各添加一个球,并强制选择这两个球。
设选择的球所组成的集合为 ,那么显然有 。
设剩余的球所组成的集合为 :
- 对于 且 ,设 ,则显然有 ;
- 对于 ,设 ,则显然有 或 。
直接对本质不同的 进行计数比较困难,因此可以转化计数对象,对选择的球所组成的集合 进行计数。
显然不漏容易做到,于是问题变成了如何做到不重。考虑把 的贡献转化到满足下列条件的集合 上:
- ;
- 对于任意 ,;换句话说,即再选择任意一个球都会导致 变化。
显然满足条件的集合 是唯一的,因为了第二个条件相当于要求了集合 是极大的。
于是现在只需要统计有多少个集合 满足上面的条件即可。设 表示考虑完前 个球且选择了第 个球的满足条件的集合 的数量。转移时枚举 且 的整数 ,表示上一个选择的球为 ,判断是否能从 转移到 :
- 若存在 ,使得对于任意 都不满足 且对于任意 都不满足 (即选择 不会导致 变化),则不能从 转移到 ,否则可以从 转移到 。
于是做一个二维前缀和即可 检验是否能从 转移到 。总时间复杂度 。
代码
Cconst int N=305,mod=998244353;
int n,v[N],s[N][N],f[N];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
bool check(int ax,int ay,int bx,int by){
return s[ax][ay]-s[ax][by-1]-s[bx-1][ay]+s[bx-1][by-1]==1;
}
void solve(){
cin>>n,s[1][n+2]++,s[n+2][1]++,v[1]=n+2,v[n+2]=1;
for(int i=1,x,y;i<=n;i++) cin>>x>>y,x++,y++,v[x]=y,s[x][y]++;
for(int x=1;x<=n+2;x++) for(int y=1;y<=n+2;y++) s[x][y]=s[x][y]+s[x-1][y]+s[x][y-1]-s[x-1][y-1];
f[1]=1;
for(int i=2;i<=n+2;i++){
for(int j=i-1;j>=1;j--){
if(v[j]<v[i]) continue;
bool flag=1;
for(int k=j+1;k<i;k++){
if(check(k,v[k],j,v[i])&&check(i,v[j],k,v[k])){
flag=0;
break;
}
}
if(flag) add(f[i],f[j]);
}
}
cout<<f[n+2]<<endl;
}
AGC020E Encoding Subsets
题解
首先考虑对于一个确定的长度为 的 串 ,如何求 的编码方式数。
设 表示 的编码方式数, 表示 缩成一个括号或单个字符的编码方式数。
对于 ,枚举 编码后第一个字符的来源,得到转移方程:
对于 ,枚举 的循环节 ,得到转移方程:
其中,,若 为 的循环节则 ,否则 。
直接计算即可得到 的编码方式数 。
接下来考虑如何计算长度为 的 串 的所有子集的编码方式数之和。
同理,设 表示 的所有子集的编码方式数之和, 表示 的所有子集的缩成一个括号或单个字符的编码方式数之和。
对于 ,仍然枚举 编码后第一个字符的来源,得到转移方程:
对于 ,同样枚举 的循环节 ,得到转移方程:
其中 表示,把 分成长度为 的 个 串,每个 串按位与的结果。
记忆化搜索即可。有效状态数并不多,足以通过。
代码
Cconst int mod=998244353;
string s;
map <string,int> f,g;
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
int G(string s);
int F(string s){
if(s=="") return 1;
if(f.count(s)) return f[s];
int n=s.size(),ans=0;
for(int i=1;i<=n;i++) add(ans,1ll*G(s.substr(0,i))*F(s.substr(i,n-i))%mod);
return f[s]=ans;
}
int G(string s){
if(s=="") return 1;
if(s=="0") return 1;
if(s=="1") return 2;
if(g.count(s)) return g[s];
int n=s.size(),ans=0;
for(int d=1;d<n;d++){
if(n%d!=0) continue;
string t="";
for(int i=0;i<d;i++){
bool x=1;
for(int j=i;j<n;j+=d) x&=(s[j]=='1');
t+=x+'0';
}
add(ans,F(t));
}
return g[s]=ans;
}
void solve(){
cin>>s;
cout<<F(s)<<endl;
}
P10813 【MX-S2-T4】 换
题解
显然,序列 是否能通过操作排好序只和每个元素的相对大小有关,因此我们可以先求出将其离散化后的序列 。对于序列 ,设其值域为 ,我们考虑序列 拆成 个 串 ,其中 ,那么序列 能通过操作排好序当且仅当它拆成的 个 串都能通过操作排好序。
我们可以暴力求出每个 串是否合法,并把每个序列 看成一个 到 的长度为 的路径,对合法的 串做一个 dp 即可得到合法的序列 的数量。具体地,设 表示,当前是路径上的第 个 串,其状态表示为 ,此时满足条件的序列 的数量。初始化 ,转移方程为:
其中, 表示 合法, 表示 不合法。由于每次转移至少要改变一位,所以转移方程中的 不能等于 。可以使用高维前缀和优化,做到时间复杂度 。
求出 后,只需要考虑如何把序列 转回序列 了。此时相当于要在 个数中选出 个数作为离散化前每个元素的值,方案数为 ,因此答案等于 ,暴力计算即可。
时间复杂度 。
代码
Cconst int N=20,M=505,S=1<<18,mod=1e9+7;
int n,k,V,m,p[M],q[M],h[S],sum[S],f[N][S],ans;
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
int lowbit(int x){
return x&-x;
}
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1,a=1ll*a*a%mod;
}
return res;
}
void solve(){
cin>>n>>V>>m;
for(int i=1;i<=m;i++) cin>>p[i]>>q[i],p[i]--,q[i]--;
k=1<<n;
for(int s=0;s<k;s++){
int t=s;
for(int i=1;i<=m;i++) if(((t>>p[i])&1)>((t>>q[i])&1)) t=t^(1<<p[i])^(1<<q[i]);
if(lowbit(t)+t==k) h[s]=1;
}
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int s=0;s<k;s++) sum[s]=f[i-1][s];
for(int p=1;p<k;p<<=1){
for(int s=0;s<k;s++){
if(s&p) continue;
add(sum[s|p],sum[s]);
}
}
for(int s=0;s<k;s++){
if(h[s]==0) f[i][s]=0;
else f[i][s]=ad(sum[s],mod-f[i-1][s]);
}
}
for(int w=1;w<=n;w++){
int pro=1;
for(int i=V-w+1;i<=V;i++) pro=1ll*pro*i%mod;
for(int i=1;i<=w;i++) pro=1ll*pro*ksm(i,mod-2)%mod;
add(ans,1ll*pro*f[w][k-1]%mod);
}
cout<<ans<<endl;
}
ARC160F Count Sorted Arrays
题解
考虑把排列 拆成 个 串 ,其中 ,那么排列 能通过操作排好序当且仅当它拆成的 个 串都能通过操作排好序。
可以时刻维护每个 串是否合法,把每个排列 看成一个 到 的长度为 的路径,对合法的 串做一个 dp 即可得到合法的排列 的数量,时间复杂度 。
感性理解,其实只有 个操作是有效的,而其他操作都不会影响 串的合法性。于是可以时刻维护 表示是否存在 串 满足 ,只有当询问的 为真时才重新 dp 并更新所有的 和 ,否则直接输出上一次询问的结果即可。
时间复杂度 。
代码
Cconst int N=15,W=1<<15;
int n,m,V,f[N][N],g[W],h[W];
ll dp[W],ans=1;
bool get(int s,int x,int y){
return ((s>>x)&1)>((s>>y)&1);
}
void solve(){
cin>>n>>m,V=1<<n;
for(int i=0;i<n;i++) for(int j=0;j<n;j++) f[i][j]=1;
for(int s=0;s<V;s++) g[s]=s;
for(int i=0;i<=n;i++) h[((1<<i)-1)<<(n-i)]=1;
for(int c=1;c<=m;c++){
int x,y;
cin>>x>>y;
x=(x+ans)%n,y=(y+ans+ans)%n;
if(x>y) swap(x,y);
if(!f[x][y]){
cout<<ans<<endl;
continue;
}
for(int i=0;i<n;i++) f[x][i]=f[i][x]=f[y][i]=f[i][y]=0;
for(int s=0;s<V;s++){
if(get(g[s],x,y)) g[s]^=(1<<x)^(1<<y);
for(int i=0;i<n;i++){
if(get(g[s],min(i,x),max(i,x))) f[x][i]=f[i][x]=1;
if(get(g[s],min(i,y),max(i,y))) f[y][i]=f[i][y]=1;
}
}
for(int s=0;s<V;s++) dp[s]=0;
dp[0]=1;
for(int s=1;s<V;s++){
if(!h[g[s]]) continue;
for(int i=0;i<n;i++) if(s&(1<<i)) dp[s]+=dp[s^(1<<i)];
}
ans=dp[V-1];
cout<<ans<<endl;
}
}
AGC008E Next or Nextnext
题解
设从 指向 所形成的图为 ,从 指向 所形成的图为 ,考虑考查 和 之间的关系。
显然 由若干个环组成,于是考虑 中的每一个环:
- 若环上的每一个点都满足 ,则 中有一个一模一样的环;
- 若环上的每一个点都满足 且环长为奇数,则 中有一个环长相同但不一样的环;
- 若环上的每一个点都满足 且环长为偶数,则 中有两个环长为该环长度一半的环;
- 若环上既存在满足 的点又存在满足 的点,则 中会存在一棵由一个环和若干条链组成的基环内向树。
接下来考虑 如何推回 。
先考虑 中的环。设 中有 个长度为 的环,枚举要合并的环的数量 :
- 若 :
- 从 个环中选出 个环,方案数为 ;
- 从 个环中选出左侧的 个环,钦定左侧的环的顺序为 ,枚举右侧环的顺序,并去掉交换同一行两个环的方案,得到方案数为 ;
- 每对环有 种合并方式,方案数为 ;
- 若 为奇数且 ,则每个不被合并的环也有 种方案,方案数为 ;
将每个步骤的方案数相乘即可得到这个部分的结果,将每个 的结果相加即可得到每种环长的答案。
再考虑 中的基环内向树。不妨设基环内向树的环上的点依次为 ,其中对于每个不大于 的正整数 都存在一条从 指向 的边;设挂着链的点依次为 ,挂在 上的链的长度为 ,则我们需要将 插回 到 之间。设 到 之间有 条边:
- 若 ,则插回的方案数为 ;
- 若 ,则插回的方案数为 ;
- 若 ,则插回的方案数为 。
将每条链插回的方案数相乘即可得到这部分的结果,答案即为每个环和每棵基环内向树的方案数的乘积。
朴素实现的时间复杂度为 ,精细实现可以做到 。
代码
Cconst int N=1e5+5,mod=1e9+7;
int n,a[N],c[N],vis[N],ans=1,fac[N<<1],infac[N<<1];
vector <int> ve[N];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
int ad(int a,int b){
a+=b;
if(a>=mod) a-=mod;
return a;
}
int ksm(int a,int b){
int res=1;
while(b){
if(b&1) res=1ll*res*a%mod;
b>>=1,a=1ll*a*a%mod;
}
return res;
}
int C(int n,int m){
return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
void dfs(int u,bool f,int len){
f|=(ve[u].size()!=1);
vis[u]=1,len++;
if(vis[a[u]]){
if(!f) return c[len]++,void();
vector <int> s,p,l;
s.pb(u),vis[u]=2;
int v=a[u];
while(v!=u) s.pb(v),vis[v]=2,v=a[v];
int m=s.size();
for(int i=0;i<m;i++){
int u=s[i];
if(ve[u].size()==2){
if(vis[ve[u][0]]==2) v=ve[u][1];
else v=ve[u][0];
int cnt=0;
while(1){
vis[v]=1,cnt++;
if(ve[v].size()==2) cout<<0<<endl,exit(0);
if(ve[v].size()==0) break;
v=ve[v][0];
}
p.pb(i),l.pb(cnt);
}
}
int k=p.size();
for(int i=0;i<k;i++){
int e;
if(i==0) e=p[i]+m-p[k-1];
else e=p[i]-p[i-1];
if(e>l[i]) add(ans,ans);
if(e<l[i]) cout<<0<<endl,exit(0);
}
return;
}
dfs(a[u],f,len);
}
void solve(){
cin>>n;
fac[0]=infac[0]=1;
for(int i=1;i<=n+n;i++) fac[i]=1ll*i*fac[i-1]%mod;
infac[n+n]=ksm(fac[n+n],mod-2);
for(int i=n+n-1;i>0;i--) infac[i]=1ll*(i+1)*infac[i+1]%mod;
for(int i=1;i<=n;i++) cin>>a[i],ve[a[i]].pb(i);
for(int i=1;i<=n;i++) if(ve[i].size()>2) cout<<0<<endl,exit(0);
for(int i=1;i<=n;i++){
if(vis[i]) continue;
dfs(i,0,0);
}
for(int i=1;i<=n;i++){
int sum=0;
for(int j=0;j+j<=c[i];j++){
int res=1;
if(j!=0) res=1ll*C(c[i],j+j)*C(j+j,j)%mod*fac[j]%mod*ksm((mod+1)/2,j)%mod*ksm(i,j)%mod;
if((i&1)&&i!=1) res=1ll*res*ksm(2,c[i]-j-j)%mod;
add(sum,res);
}
ans=1ll*ans*sum%mod;
}
cout<<ans<<endl;
}
AGC027E ABBreviate
题解
注意到,如果我们把 看作 , 看作 ,设 表示字符串 中所有字符所对应的数字之和对 取模后的值,那么每次操作不会改变 。
首先考虑字符串 能不能变成单个字符 ()。此时显然需要满足:
- ;
- 中存在相邻的相同字符。
只要 满足上述两个条件,那么就一定可以通过不断对 中相邻的相同字符进行操作的方式使 变成 。
接下来考虑字符串 能不能变成字符串 ()。此时显然需要满足:
- ;
- 中存在相邻的相同字符;
- 设 ,则 可以被分成 段子字符串 ,其中前 段均满足 ,第 段满足 ( 可以为空串)。
同理,只要 满足上述三个条件,那么就一定可以通过相邻的相同字符调整 的每一段,使 变为 并消去 。
可以在特判 中不存在相邻的相同字符后 dp:设 表示 能变成的字符串数量,()表示最小的大于 的下标 ,满足 。
初始化 和 ,则转移方程为 和 ,答案即为 。
由于每个字符串 都恰好对 所能变成 的最短前缀处贡献了 ,所以这样做是不重不漏的。时间复杂度 。
代码
Cconst int N=1e5+5,mod=1e9+7;
int n,a[N],nxt[N][3],suf[N],dp[N],ans;
bool flag;
string s;
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>s,n=s.size();
for(int i=1;i<=n;i++) a[i]=s[i-1]=='a'?1:2,flag|=a[i]==a[i-1];
if(!flag) return cout<<1<<endl,void();
nxt[n+1][1]=nxt[n+1][2]=n+2,dp[1]=1;
for(int i=n;i>=1;i--){
suf[i]=(suf[i+1]+a[i])%3;
if(a[i]==1) nxt[i][1]=i+1,nxt[i][2]=nxt[i+1][1];
if(a[i]==2) nxt[i][1]=nxt[i+1][2],nxt[i][2]=i+1;
}
for(int i=1;i<=n;i++) add(dp[nxt[i][1]],dp[i]),add(dp[nxt[i][2]],dp[i]);
for(int i=2;i<=n+1;i++) if(suf[i]==0) add(ans,dp[i]);
cout<<ans<<endl;
}
AGC020F Arcs on a Circle
题解
将所有线段从短到长排序,考虑断环为链,以第 条线段即最长的线段的左端点作为起点,将问题转化为:最长的线段的左端点位于位置 ,其余线段的左端点在 上随机,求 都被覆盖的概率。其中,以最长的线段的左端点作为起点是为了防止存在另一条更长的线段 覆盖了 和 ,对 和 均造成贡献的情况出现。
虽然每条线段的左端点都是在实数范围内随机的,但我们其实只关心小数部分的相对大小关系,且可以认为小数部分两两不同,因此我们可以考虑直接枚举 的排列 表示前 条线段左端点小数部分的相对大小关系。于是我们现在得到了 个整数坐标 ,第 条线段的左端点只能放在 的坐标 上,我们需要求出 都被覆盖的概率。
考虑 dp:设 表示,当前考虑到坐标 ,前 个坐标都被覆盖了,当前使用了的线段的集合为 的方案数。设 ,转移时选择使用或不使用第 条线段即可。由于总方案数为 ,所以此时覆盖 的概率等于 。又由于每种小数部分的相对大小关系的出现概率相等,所以答案即为每个 所对应的 的平均数。
直接计算即可。时间复杂度 。
代码
Cconst int N=7,C=51;
int n,c,V,a[N],p[N],rk[N];
ll ans,dp[N*C][N*C][1<<N],cnt;
void solve(){
cin>>n>>c,V=1<<(n-1);
for(int i=1;i<=n;i++) cin>>a[i],p[i]=i;
sort(a+1,a+n+1);
while(1){
memset(dp,0,sizeof dp);
dp[0][a[n]*n][0]=1,cnt++;
for(int i=1;i<n;i++) rk[p[i]]=i;
for(int i=0;i<n*c;i++){
for(int j=i;j<=n*c;j++){
for(int s=0;s<V;s++){
int k=rk[i%n];
dp[i+1][j][s]+=dp[i][j][s];
if(k!=0&&(!(s&(1<<(k-1))))) dp[i+1][min(max(j,i+a[k]*n),c*n)][s|(1<<(k-1))]+=dp[i][j][s];
}
}
}
ans+=dp[n*c][n*c][V-1];
if(!next_permutation(p+1,p+n)) break;
}
printf("%.18lf",ans*1.0/cnt/pow(c,n-1));
}
P12558 [UOI 2024] Heroes and Monsters
题解
我们把 和 按照从小到大的顺序排列,考虑刻画判定大小为 的集合 合法的充要条件。为了满足条件,我们可以贪心,让 中的元素与 配对,同时让不在 中的元素与 配对。
更具体地,集合 合法,当且仅当:
- 设在 中的第 小的元素为 ,则有 ;
- 设不在 中的第 小的元素为 ,则有 。
于是,当 确定时,我们可以设 表示考虑完 且此时有 个元素被选入 中的方案数,枚举下一个元素是否选入 即可得到转移方程为:
即为 且 的询问的答案。我们对每一个 都求出 的值,再预处理一下前缀和即可做到 回答单次询问。这样做的时间复杂度为 ,考虑优化。
我们需要对每一个 都做一次 dp 是复杂度很大的原因之一,但是 是判定条件中的一个参数,有没有什么办法把判定条件中的 消掉呢?
我们可以做一个变形,把判定集合 合法的充要条件修改为:
- 设在 中的第 小的元素为 ,则有 ;
- 设不在 中的第 大的元素为 ,则有 。
显然改之后的条件和改之前的条件是等价的,我们只是把不在 中的元素的枚举顺序改变了,但这样做消去了判定条件中的 。同时,看到这个 dp 形式,我们容易联想到分别对 的前缀和后缀做一个 dp,最后把它们拼起来。
具体地,我们设 表示在只受第一个条件的限制下考虑完 且此时有 个元素被选入 中的方案数, 表示在只受第二个条件的限制下考虑完 且此时有 个元素没有被选入 中的方案数,枚举下一个元素的状态即可得到转移方程为:
接下来我们考虑怎么把数组 和数组 拼起来。如果我们随便找一个位置 将序列 拆成 的前缀与 的后缀显然是有问题的——可能在 的前缀中存在一个不在 中的 不满足 的条件,也可能在 的后缀中存在一个在 中的 不满足 的条件。
于是,我们希望对每一个集合大小 找到一个位置 ,使得 中的元素如果不在 中则一定满足 的条件,且 中的元素如果在 中则一定满足 的条件。
因为现在集合大小 是已知的,所以我们是希望判定集合 合法的条件中出现 的,于是我们可以再对判定集合 合法的充要条件做一个变形:
- 设在 中的第 大的元素为 ,则有 ;
- 设不在 中的第 小的元素为 ,则有 。
我们还是只改变了元素的枚举顺序,但这样的判定条件非常有利于我们找到满足要求的位置 。现在,位置 的要求变为了:
- 对于 中每一个不在 中的元素 ,都满足 ;
- 对于 中每一个在 中的元素 ,都满足 。
分析一下,如果我们有 则我们一定有 ,而如果我们有 则我们一定有 ,所以我们只需要满足 的前缀都小于 且 的后缀都大于 就可以满足条件了!于是我们所求的 就是最大的满足 的位置 。
接下来我们只需要对于每一个 找到划分位置 ,枚举前缀中在 中的元素的个数 ,并将 且 的询问的答案加上 即可。
同样做一个前缀和即可做到 回答单次询问,总时间复杂度 ,可以通过。
代码
Cconst int N=5e3+5,mod=998244353;
int n,a[N],b[N],f[N][N],g[N][N],q,sum[N];
void add(int &a,int b){
a+=b;
if(a>=mod) a-=mod;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
sort(a+1,a+1+n),sort(b+1,b+1+n);
f[0][0]=1,g[n+1][0]=1;
for(int i=1;i<=n;i++){
f[i][0]=f[i-1][0];
for(int j=1;j<=i;j++){
f[i][j]=f[i-1][j];
if(a[i]>b[j]) add(f[i][j],f[i-1][j-1]);
}
}
for(int i=n;i>=1;i--){
g[i][0]=g[i+1][0];
for(int j=1;j<=n-i+1;j++){
g[i][j]=g[i+1][j];
if(a[i]<b[n-j+1]) add(g[i][j],g[i+1][j-1]);
}
}
for(int m=0;m<=n;m++){
int k=0;
while(k<n&&a[k+1]<b[m]) k++;
for(int c=0;c<=m;c++) if(k>=c&&n-k-m+c>=0) add(sum[m],1ll*f[k][c]*g[k+1][n-k-m+c]%mod);
if(m) add(sum[m],sum[m-1]);
}
cin>>q;
for(int i=1,l,r;i<=q;i++){
cin>>l>>r;
if(l==0) cout<<sum[r]<<endl;
else cout<<(sum[r]-sum[l-1]+mod)%mod<<endl;
}
}
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...