20240519比赛总结
T1 Color
https://gxyzoj.com/d/hzoj/p/3692
显然,答案与元素的位置无关,只与个数有关
考虑每个元素能经过若干次操作变成n个的概率,记\(p_i\)为i个数能变到n个数的概率
进行一次操作后,会分成三种情况,+1,-1,和不变,所以式子是:
化简得:\(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}\),所以式子是:
记\(f_i=p_i dp_i\),并将上文求出的\(p_i\)代入,所以可以得到:
然后按照上面的方法处理一下,就可以的到这个式子:
不断将这个式子进行代入就会得到:
将\(n-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的代码即可