心得
学了一个#ifndef ONLINE_JUDGE #endif的骚操作,以后只用粘一次样例就可以了
这次的B题经典问题很不熟练,以为要讨论很多情况,结果赛后看qls代码ABCD都是20多行,
C题序列自动机也只会套板子,实际上敲也不超过5行,
D题化简的时候差一点,前面卡题太久,不然可做
D题三种做法,二进制枚举素因子容斥,约数DAG容斥(姑且这么叫),欧拉函数输出
好好补题,让每次掉的分变得有意义
B.Infinite Prefixes(思维题)
T(T<=100)组样例,每次给出一个长度为n(1<=n<=1e5)的01串,并且该串一直重复无限长,
串长度为i的前缀中0比1多的个数记为cnt[i],现给定和值x(-1e9<=x<=1e9),求满足cnt[i]==x的i的数量
空串视为一个合法的长度为0的前缀,cnt[0]=0,
保证所有n的总和不超过1e5,如若有无限个合法的i,输出-1
cnt[n]=0特判,此时若x在函数图像极值之间特判输出-1,否则输出0
把n考虑成一个循环节,是一个给定函数和一条线y=y0的交点的个数,
函数图像每次按给定函数上升,可认为y=y0按周期下降,每周期下降cnt[n]
赛中想的是去判断y=y0与直线有多少交点,不太好判,毕竟有直线和函数图像若干情况要讨论
实际上,周期下降时,每个点最多被经过一次,依次判断i=1到i=n每个点是否会被经过即可
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int t,n,x,sum[N],mn,mx,sg;
char s[N];
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.txt","r",stdin);
#endif
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&x);
scanf("%s",s+1);
mn=mx=0;
for(int i=1;i<=n;++i)
{
sg=s[i]=='0'?1:-1;
sum[i]=sum[i-1]+sg;
mn=min(mn,sum[i]);
mx=max(mx,sum[i]);
}
if(sum[n]==0)
{
if(mn<=x&&x<=mx)puts("-1");
else puts("0");
}
else
{
int ans=0,v;
for(int i=0;i<n;++i)
{
v=x-sum[i];
if(v%sum[n]==0&&v/sum[n]>=0)ans++;
}
printf("%d\n",ans);
}
}
return 0;
}
C. Obtain The String(序列自动机)
T(T<=1e5)组样例,每次给出两个纯小写字母串,
串s和串t,1<=|s|,|t|<=1e5,保证所有|s|,|t|之和不超过2e5
每次取s的任意子序列,视为一次操作,
后续操作在之前的操作形成的串后拼接,求拼成t的最小操作数,不能拼输出-1
显然,子序列是越长的贪心越优,每次都匹配到不能匹配为止再从头扫,
那就在序列自动机上一直跑即可,赛中时抄了个板子通过了,比较冗长
赛后学到了qls通过移一位获得了每个字母的头的位置的写法,memcpy感觉也很巧
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=26;
int T,las[M];
int nex[N][M];
char s[N],t[N];
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.txt","r",stdin);
#endif
scanf("%d",&T);
while(T--)
{
scanf("%s%s",s+1,t);
int n=strlen(s+1);
//序列自动机
for(int i=0;i<M;++i)
las[i]=n+1;
for(int i=n;i>=0;--i)//引入i=0当虚位 代表为空时的选择
{
memcpy(nex[i],las,sizeof(las));
if(i)las[s[i]-'a']=i;
}
int now=0,ans=1;
for(int i=0;t[i];++i)
{
if(nex[now][t[i]-'a']>n){now=0;ans++;}//无解 尝试另起一段
if(nex[now][t[i]-'a']>n){ans=-1;break;}//另起也无解
now=nex[now][t[i]-'a'];
}
printf("%d\n",ans);
}
return 0;
}
D. Same GCDs(欧拉函数/两种容斥)
T(T<=50)组样例,每次给出两个整数a,m(1<=a<m<=1e10)
求满足0<=x<m且gcd(a,m)=gcd(a+x,m)的x的数量
记g=gcd(a,m),a=k1*g,m=k2*g,则gcd(k1*g+x,k2*g)=g,需满足x也是g的倍数
令x=k3*g,有0<=k3<m/g,即0<=k3<k2,有gcd(k1+k3,k2)=1,求合法的k3的数量
第一种做法(欧拉函数):
复杂度O(根号m)
注意到gcd(k1+k3,k2)=gcd((k1+k3)%k2,k2),
k3共k2个不同取值,
对于两个不同的k3,(k1+k3)%k2肯定不同,且都落在[0,k2)里,
构成了完全剩余系,所以答案就是phi[k2]的值
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;
ll a,m,g,x,n,p;
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.txt","r",stdin);
#endif
scanf("%d",&t);
while(t--)
{
scanf("%lld%lld",&a,&m);
g=__gcd(a,m);a/=g;m/=g;
p=x=m;
for(ll i=2;i*i<=x;++i)
if(x%i==0)
{
p=p/i*(i-1);
while(x%i==0)x/=i;
}
if(x>1)p=p/x*(x-1);
printf("%lld\n",p);
}
return 0;
}
第二种做法(二进制枚举素因子容斥):
复杂度O(2的m的素因子个数次方*m的素因子个数)
gcd>1代表至少是一个素因子的倍数,
是一个素因子prime[i]的倍数的数有(k1+k2)/prime[i]-k1/prime[i]个,
容斥,奇加偶减,统计gcd>1的数的个数,k2个数中减去gcd>1的即可
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;
ll a,m,g,x,n;
ll cal(ll x)
{
return (a+m)/x-a/x;
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("1.txt","r",stdin);
#endif
scanf("%d",&t);
while(t--)
{
scanf("%lld%lld",&a,&m);
g=__gcd(a,m);a/=g;m/=g;
vector<ll>fac;
x=m;
for(ll i=2;i*i<=x;++i)
if(x%i==0)
{
fac.push_back(i);
while(x%i==0)x/=i;
}
if(x>1)fac.push_back(x);
n=fac.size();
ll res=0;
for(int i=1;i<(1<<n);++i)
{
ll cnt=0,now=1;
for(int j=0;j<n;++j)
if(i>>j&1)now*=fac[j],cnt++;
if(cnt&1)res+=cal(now);
else res-=cal(now);
}
printf("%lld\n",m-res);
}
return 0;
}
第三种做法(约数DAG容斥):
学习的qls的做法,自己编了这么一个名字,
复杂度O(m的约数个数*m的约数个数),
注意到2的m的素因子个数次方<=m的约数个数(约数定理)
m<=1e10,m的约数个数上界在1e3左右,故可通过
初始时,cnt[i]代表gcd是i的倍数的方案数,现在要求gcd恰为i的方案数(倍数反演)
//fac[i]排增序,cnt[i]代表gcd为fac[i]的倍数的合法方案数
for(int i=n-1;i>=0;--i)
for(int j=i+1;j<n;++j)
if(fac[j]%fac[i]==0)
cnt[i]-=cnt[j];
数学归纳三段论:
①最大的约数d没有倍数要减,其本身就恰为gcd=d的方案数,
②对于其他每个约数d’,减去其倍数的方案数,使之成恰为gcd=d'的方案数
最后的gcd=1的方案数,即为所求
倍数反演也可以,但mu还得现求,不如直接容斥把答案给算了
这启发我们,约数反演也可以在时间充裕的情况下这么暴力
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;
ll a,m,g;
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%lld%lld",&a,&m);
g=__gcd(a,m);a/=g;m/=g;
vector<ll>fac;
for(ll i=1;i*i<=m;++i)
if(m%i==0)
{
fac.push_back(i);
if(i!=m/i)fac.push_back(m/i);
}
sort(fac.begin(),fac.end());
int n=fac.size();
vector<ll>cnt(n);
for(int i=0;i<n;++i)
cnt[i]=(a+m)/fac[i]-a/fac[i];
for(int i=n-1;i>=0;--i)
{
for(int j=i+1;j<n;++j)
{
if(fac[j]%fac[i]==0)
cnt[i]-=cnt[j];
}
}
printf("%lld\n",cnt[0]);
}
return 0;
}
E.Permutation Separation(线段树)
题目
给你一个长度n(2<=n<=1e5),以下两行pi(1<=pi<=n)和ai(1<=ai<=1e9)
pi是对应的1到n的一个排列,现在你要从中选取一个轴l(1<=l<=n-1),把序列劈成非空的两部分
把[1,l]的pi值给左边,[l+1,r]的pi的值给右边,
①若左侧所有的值都小于右侧,结束,有一侧为空时也满足该条件
②否则你可以让一侧的pi值移动到另一侧,移动pi的代价为ai,可以移动多个值
注意,初态两边要求均非空,终态可以有一侧为空
求最小的总代价和,输出代价和
题解
在还没有劈序列时,所有的值都在一坨,不妨都认为在右侧
通过枚举最后的值域分界线v来决定答案,复杂度是O(n^2)的
①条件等价于值域<=v归左,而>v归右,由于允许一侧有空,v的范围为[0,n]
那么对于当前枚举的v,若pi>v,无需动,pi<=v则需要加ai代价,
用线段树加速该过程,考虑对值域v建线段树,pi需要对[pi,n]区间加上ai代价,
作预处理时,左侧为空,右侧为[1,n]
考虑后续枚举轴的过程,如果枚举i(1<=i<n)为轴,即[1,i]在左侧,
那么应该把上一状态[1,i-1]中pi的值从右侧拿到左侧,
所带来的对称影响是需减去原来右->左的代价,在其补集区间里加上左->右的代价
对每次枚举轴的答案,线段树询问一下全局最小值即mn[1],取最优即可
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int n,p[N],a[N];
ll mn[N*5],cov[N*5];
void psd(int p)
{
mn[p<<1]+=cov[p],cov[p<<1]+=cov[p];
mn[p<<1|1]+=cov[p],cov[p<<1|1]+=cov[p];
cov[p]=0;
}
void upd(int p,int l,int r,int ql,int qr,int v)
{
if(ql<=l&&r<=qr)
{
mn[p]+=v;
cov[p]+=v;
return;
}
psd(p);
int mid=(l+r)/2;
if(ql<=mid)upd(p<<1,l,mid,ql,qr,v);
if(qr>mid)upd(p<<1|1,mid+1,r,ql,qr,v);
mn[p]=min(mn[p<<1],mn[p<<1|1]);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&p[i]);
//开始认为所有数都在右边 左侧为空 右侧[1,n] 便于后续挪动分界线转移
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
//对最终左边留的值的个数k建树 即左边最终留[1,k]
//对于某个给定的k,显然p[i]需要从右边放入左边[1,k]的条件是p[i]<=k
upd(1,0,n,p[i],n,a[i]);
}
ll ans=2e14;
//枚举分界线[1,i] [i+1,n]
//不断把数放入左边
for(int i=1;i<n;++i)
{
upd(1,0,n,p[i],n,-a[i]);//从右边拿掉
upd(1,0,n,0,p[i]-1,a[i]);//加入左边
ans=min(ans,mn[1]);//全局问 mn[1]即可
}
printf("%lld\n",ans);
return 0;
}
F.Good Contest(概率+组合+填坑dp)
题目
有n(n<=50)个区间,第i个区间[li,ri](0<=li<=ri<=998244351)
对于每个区间i,你从中等概率地选择一个数vi,不能不选,放在新序列的第i位
求形成的新序列是非严格递减序列的概率,
若答案为分数a/b,输出a*(b对998244353的逆元)
题解
如果l和r的范围很小,概率dp的做法O(n*maxr)的做法,比如说搞它个1e5,大概是这个画风?
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=1e5+10;
int modpow(int x,int n,int mod)
{
int res=1;
for(;n;n>>=1,x=1ll*x*x%mod)
if(n&1)res=1ll*res*x%mod;
return res;
}
int n,l,r,ans,lasl,lasr;
int sum[N],las[N],dp[N];
int main()
{
scanf("%d",&n);
lasr=N-1;
las[lasr]=1;
for(int i=1;i<=n;++i)
{
scanf("%d%d",&l,&r);
sum[lasr]=las[lasr];
for(int j=lasr-1;j>=l;--j)
sum[j]=(sum[j+1]+las[j])%mod;
memset(dp,0,sizeof dp);
int inv=modpow(r-l+1,mod-2,mod);
for(int j=l;j<=r;++j)
dp[j]=1ll*sum[j]*inv%mod;
memcpy(las,dp,sizeof dp);
lasl=l;lasr=r;
}
for(int i=l;i<=r;++i)
ans=(ans+dp[i])%mod;
printf("%d\n",ans);
return 0;
}
以下开始正解,是wls说的“填坑”dp,camp的一种经典题目,注意到n的范围很小,
先将50个区间以左闭右开的形式[l,r)离散化,
考虑把离散化后的线段缩成点,用i表示第i个离散化后的区间[li,ri)
dp[i][j]代表当前选了i个数 所有的数都选自离散化后大于等于第j个区间的方案数
即只考虑后面的区间,从这些区间里选i个数出来,
纵向看每个离散化后的区间,顺序选n个数时,
要么和上一个数选择同一区间j,要么选择小于j的区间,有递减性质
所以,对于选取第i个数(判断第i个区间)的时候,往回找第i-1,i-2,...k个数,
如果这些区间都有第j个离散化区间,就可以通过类似区间dp的枚举分界线,
把[i,k]这些数都从第j个区间里选取,记区间长为range,要选的数的个数为num,
允许重复选取求所选的数能构成非严格递减序列的方案数,
即可重组合方案数,为C(range+num-1,num),
相当于,num次选择选range个数,每种数都有无穷个,选走一个会再生成一个,
那num次选择之后,又生成出range个数,共num+range个数,区间里的每种数都至少有一个,
组合的选取方案,隔板法选range种出来,即C(range+num-1,range-1),等于上面的方案数
也有记bi=ai+i的把<=变成<的证明方法,从略,掌握一种即可
range很大num很小,为了快速求,可以用递推来计算
由于要从大于等于的方案数转移而来,所以维护大于等于,求完等于j的时候要实时维护后缀和
可行方案数除以总方案数即为概率
代码
#include<bits/stdc++.h>
using namespace std;
const int N=52,M=4*N,mod=998244353;
//dp[i][j]代表当前选了i个数 所有的数都选自离散化后大于等于第j个区间的方案数
int n,cnt,x[M],dp[N][M],l[N],r[N],all;
int modpow(int x,int n,int mod)
{
int res=1;
for(;n;n>>=1,x=1ll*x*x%mod)
if(n&1)res=1ll*res*x%mod;
return res;
}
int inv(int x)
{
return modpow(x,mod-2,mod);
}
int main()
{
scanf("%d",&n);
all=1;
for(int i=1;i<=n;++i)
{
scanf("%d%d",&l[i],&r[i]);r[i]++;
all=1ll*all*(r[i]-l[i])%mod;
x[++cnt]=l[i];x[++cnt]=r[i];
}
sort(x+1,x+cnt+1);
cnt=unique(x+1,x+cnt+1)-(x+1);
for(int i=1;i<=n;++i)
{
l[i]=lower_bound(x+1,x+cnt+1,l[i])-x;
r[i]=lower_bound(x+1,x+cnt+1,r[i])-x;
}
for(int j=1;j<=cnt;++j)
{
dp[0][j]=1;
}
for(int i=1;i<=n;++i)
{
for(int j=l[i];j<r[i];++j)
{
int C=1;
for(int k=i;k;--k)
{
if(!(l[k]<=j&&j<r[k]))break;
int num=i-k+1;
int range=x[j+1]-x[j];//标号 区间对应左端点 第j个区间的范围[x[j],x[j+1])
C=1ll*C*(range+num-1)%mod*inv(num)%mod;
dp[i][j]=(dp[i][j]+1ll*dp[k-1][j+1]*C%mod)%mod;//[k,i]这一段都选第j个区间的值构成降序列
//相当于range个数中选num个构成非严格降序列(即组合可重问题)
//答案是C(range+num-1,num) 注意到num不断+1
//C(n,k)=C(n-1,k-1)*n/k
}
}
for(int j=cnt;j;--j)
dp[i][j]=(dp[i][j]+dp[i][j+1])%mod;
}
printf("%d\n",1ll*dp[n][1]*inv(all)%mod);
return 0;
}