Educational Codeforces Round 81 (Rated for Div. 2) 补题(B思维、C序列自动机、D欧拉函数/两种容斥、E线段树、F概率+组合+填坑dp)

心得

学了一个#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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值