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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章