20240519比赛总结

T1 Color

https://gxyzoj.com/d/hzoj/p/3692

显然,答案与元素的位置无关,只与个数有关

考虑每个元素能经过若干次操作变成n个的概率,记\(p_i\)为i个数能变到n个数的概率

进行一次操作后,会分成三种情况,+1,-1,和不变,所以式子是:

\[p_i=\dfrac{i(n-i)}{n(n-1)} p_{i-1}+\dfrac{i(n-i)}{n(n-1)} p_{i+1}+(1-2 \dfrac{i(n-i)}{n(n-1)}) p_i \]

化简得:\(p_i=\dfrac{p_{i-1}+p_{i+1}}{2}\)

去分母,得:\(2p_i=p_{i-1}+p_{i+1}\)

即:\(p_i-p_{i-1}=p_{i+1}-p_i\)

所以,转移到n的概率为等差数列,因为\(p_0=0,p_n=1\),所以,\(p_i=\dfrac{i}{n}\)

接下来考虑期望,记\(dp_i\)表示当前种类目前有i个,转移到n还需的期望次数

根据概率,单次改变数值的概率为\(2 \dfrac{i(n-i)}{n(n-1)}\),所以改变数值的期望次数即为其倒数\(\dfrac{n(n-1)}{2i(n-i)}\)

因为并不是所有情况都能转移到n,所以要乘上一定的概率,并且由概率,可以知道从\(i+1\)\(i-1\)转移的概率是相等的,所以要乘以\(\dfrac{1}{2}\),所以式子是:

\[p_i dp_i=p_i \dfrac{n(n-1)}{2i(n-i)}+\dfrac{1}{2}(p_{i-1} dp_{i-1}+p_{i+1} dp_{i+1}) \]

\(f_i=p_i dp_i\),并将上文求出的\(p_i\)代入,所以可以得到:

\[f_i=\dfrac{n-1}{2(n-i)}+\dfrac{1}{2}(f_{i-1}+f_{i+1}) \]

然后按照上面的方法处理一下,就可以的到这个式子:

\[f_i-f_{i-1}-\dfrac{n-1}{(n-i)}=f_{i+1}-f_i \]

不断将这个式子进行代入就会得到:

\[f_i=if_1-\dfrac{n-1}{n-1}(i-1)-\dfrac{n-1}{n-2}(i-2)-\cdots-\dfrac{n-1}{n-i+1} \]

\(n-1\)题出来就会得到:

\[f_i=if_1-(n-1)(\dfrac{i-1}{n-1}+\dfrac{i-2}{n-2}+\cdots+\dfrac{i-i+1}{n-i+1}) \]

\(i=n\)

\(f_n=nf_1-(n-1)^2=0\)

\(f_i=\dfrac{(n-1)^2}{n}\)

所以,直接O(n)递推即可

\(cnt_s\)为字符s的个数,则答案为\(\sum f_{cnt_s}\)

代码:

#include<cstdio>
#include<string>
#include<iostream>
using namespace std;
string s;
int cnt[27];
double f[100005],n,ans;
int main()
{
	cin>>s;
	n=s.size();
	f[1]=(n-1)*(n-1)/n;
	for(int i=2;i<n;i++)
	{
		f[i]=2.0*f[i-1]-f[i-2]-(n-1.0)/(n-1.0*i+1.0);
	}
	for(int i=0;i<n;i++)
	{
		cnt[s[i]-'A']++;
	}
	for(int i=0;i<26;i++)
	{
		ans+=f[cnt[i]];
		if(cnt[i]==n)
		{
			printf("0.0");
			return 0;
		}
	}
	printf("%.1lf",ans);
	return 0;
}

T2 SSY的队列

https://gxyzoj.com/d/hzoj/p/2385

20pts:直接暴搜即可

70pts:因为n比较小,所以考虑状压

\(f_{i,s}\)表示放好的人的状态为s,最后放的人是i,则可以枚举状态,本次放的人和上次放的人,暴力判断即可

代码:

#include<cstdio>
#define ll long long
using namespace std;
const int p=1234567891;
int n,m,a[35],b[35];
ll f[100005][20];
int lowbit(int x)
{
	return x & (-x);
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	scanf("%d",&m);
	if(n>15)
	{
		ll ans=1;
		for(int i=1;i<=n;i++)
		{
			ans=ans*i%p;
		}
		printf("%lld",ans);
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if((a[i]-a[j])%m)
			{
				b[i]+=(1<<(j-1));
			}
		}
	}
	int tmp=(1<<n)-1;
	for(int i=1;i<=n;i++)
	{
		f[1<<(i-1)][i]=1;
	}
	for(int i=1;i<=tmp;i++)
	{
		if(i-lowbit(i)==0) continue;
		for(int j=1;j<=n;j++)
		{
			if(((1<<(j-1))&i)==0) continue;
			int st=(i&b[j]);
			for(int k=1;k<=n;k++)
			{
				if(((1<<(k-1))&st)==0) continue;
				f[i][j]=(f[i][j]+f[i^(1<<(j-1))][k])%p;
			//	printf("%d %d %d %d\n",i,j,k,f[i][j]);
			}
		}
	}
	ll sum=0;
	for(int i=1;i<=n;i++)
	{
		sum+=f[tmp][i];
	}
	printf("%lld",sum%p);
	return 0;
}

正解:

很明显,在\(n\le 30\)时,状压已经不能求解了,回到20分的做法,继续考虑搜索

很明显,当两个数的对m取模的结果相同时,则必然不能相邻,所以可以先统计相邻的情况

可以发现,三个数的余数为3,2,2和余数为3,1,1的方案数是一样的

所以可以发现,我们其实并不关心余数额分别是多少,而是关心有多少种余数以及每种余数的个数

所以可以使用哈希记录每一步的状态,记忆化搜索

代码:

#include<cstdio>
#include<algorithm>
#include<map>
#include<cstring>
#define ll long long
#define ull unsigned long long 
using namespace std;
const ll p=1234567891,p1=1331;
int n,m,cnt[35],tot,b[35],mx;
ll a[35],fac[35];
map<ull,ll> mp[35];
bool vis[35];
ll dfs(int x,int lst)
{
	if(x>n) return 1;
	memset(b,0,sizeof(b));
	for(int i=1;i<=tot;i++)
	{
		if(i!=lst) b[cnt[i]]++;
	}
	ull h=cnt[0];
	for(int i=0;i<=mx;i++)
	{
		h=h*p1+b[i];
	}
	h=h*p1+cnt[lst];
	if(mp[x].find(h)!=mp[x].end()) return mp[x][h];
	ll res=0;
	if(cnt[0]>0)
	{
		cnt[0]--;
		res=(res+dfs(x+1,0))%p;
		cnt[0]++;
	}
	for(int i=1;i<=tot;i++)
	{
		if(i==lst||cnt[i]<=0) continue;
		cnt[i]--;
		res=(res+dfs(x+1,i))%p;
		cnt[i]++;
	}
	mp[x][h]=res;
	return res;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
	}
	scanf("%d",&m);
	for(int i=1;i<=n;i++)
	{
		a[i]=(a[i]%m+m)%m;
	}
	for(int i=1;i<=n;i++)
	{
		if(vis[i]) continue;
		int tmp1=0;
		for(int j=1;j<=n;j++)
		{
			if(a[j]==a[i])
			{
				tmp1++;
				vis[j]=1;
			}
		}
		if(tmp1==1) cnt[0]++;
		else cnt[++tot]=tmp1;
		mx=max(mx,tmp1);
	}
	mx=max(mx,cnt[0]);
	fac[0]=1;
	for(int i=1;i<=n;i++)
	{
		fac[i]=fac[i-1]*i%p;
	}
	ll ans=1;
	for(int i=0;i<=tot;i++)
	{
		ans=ans*fac[cnt[i]]%p;
	}
	ans=ans*dfs(1,0)%p;
	printf("%lld",ans);
	return 0;
}

T3 [JXOI2018] 游戏

https://gxyzoj.com/d/hzoj/p/3661

题意是按照一定顺序检查,问在查到第几个时都认真工作,求每种排列答案的和

从特殊性质开始,如果l=1,则1的位置就是最少要查的次数

如果l=2,则显然要将所有的质数都检查一次,因为除1之外没有数能够删掉质数

将它推广,就可以得到100%的数据解法,显然,找出所有无法被其他数删除的数,然后排列组合就是答案

关于如何求解,可以枚举最后一个无法被删除的数的位置,然后算前面有多少种排法,然后再对能或不能两类数的内部求排列,相乘就是答案

代码:

#include<cstdio>
#define ll long long
using namespace std;
const int p=1e9+7,N=1e7;
int l,r;
int fac[N+1],inv[N+1];
ll qpow(ll x,int y)
{
	ll res=1;
	while(y)
	{
		if(y&1) res=res*x%p;
		x=x*x%p;
		y>>=1;
	}
	return res;
}
ll C(int n,int m)
{
	return 1ll*fac[n]*inv[m]%p*inv[n-m]%p;
}
bool vis[N+1];
int pr[2000005],m;
void prime()
{
	for(int i=2;i<=N;i++)
	{
		if(!vis[i]) pr[++m]=i;
		for(int j=1;i*pr[j]<N&&j<=m;j++)
		{
			vis[i*pr[j]]=1;
			if(i%pr[j]==0) break;
		}
	}
}
int cnt;
void get_p(int l,int r)
{
	for(int i=l;i<=r;i++)
	{
		if(!vis[i]) cnt++;
		for(int j=1;i*pr[j]<N&&j<=m;j++)
		{
			vis[i*pr[j]]=1;
			if(i%pr[j]==0) break;
		}
	}
}
int main()
{
	fac[0]=1;
	for(int i=1;i<=1e7;i++)
	{
		fac[i]=1ll*fac[i-1]*i%p; 
	}
	inv[N]=qpow(fac[N],p-2);
	for(int i=1e7-1;i>=0;i--)
	{
		inv[i]=1ll*inv[i+1]*(i+1)%p;
	}
	prime();
	scanf("%d%d",&l,&r);
	ll ans=0;
	if(l==1)
	{
		ans=1ll*(r+1)*r/2;
		ans=ans%p*fac[r-1]%p;
		printf("%lld",ans);
		return 0;
	}
	int sum=r-l+1;
	for(int i=l;i<=r;i++) vis[i]=0;
	get_p(l,r);
	for(int i=cnt;i<=sum;i++)
	{
		ans+=1ll*i*C(i-1,cnt-1)%p;
		ans%=p; 
	}
	ans=ans*fac[cnt]%p*fac[sum-cnt]%p;
	printf("%lld",ans);
	return 0;
}

T4 [MtOI2019] 小铃的烦恼

https://gxyzoj.com/d/hzoj/p/3664

注意到\(\left(\sum\limits_{a=1}^{n}\sum\limits_{b=1}^{n}p_{a,b}\right) = n^2\)

所以\(p_{a,b}\)的限制没有影响,所有的都能100%的成功,所以直接粘T1的代码即可

posted @ 2024-05-19 21:10  wangsiqi2010916  阅读(11)  评论(0编辑  收藏  举报