BUPT 2022 Spring Training #12
链接:https://vjudge.net/contest/488579
A - Agile permutation
题意
现在有一个长度为\(n\)的排列\(p_1,p_2,\cdots,p_n\)以及两种操作:
1.把两个元素交换位置,代价为\(a\)。
2.随机排列整个序列,代价为\(b\)。
你可以进行任意次操作。
现在给定\(n,a,b,p_1,p_2,\cdots,p_n\),求在最优策略下将排列\(p\)有序化所需代价的期望。
思路
这是一道很有意思的组合计数/概率题。
首先,如果我们进行了操作1,那么就不可能再进行操作2了,否则这一步操作1完全浪费了。那么最优策略应该是确定一个阈值\(m\),不断地进行如下迭代:
- 若当前的排列能只用小于等于\(m\)步的操作1有序化,那么就不断地进行操作1直至完成任务
- 若当前地排列不能只用小于等于\(m\)步地操作1有序化,那么就进行操作2.
设
\(E_m\)=随机一个排列,在阈值为\(m\)时将其有序化的期望代价
\(p_k\)=随机一个排列,只用操作1使其有序化所需最小步数为\(k\)的概率
那么对于当前的排列\(P\)有两种情况:
- 若可行最小步数小于等于阈值\((k\leq m)\),则直接全部进行操作1直至完成任务,期望贡献为\(akp_k\)
- 若可行最小步数大于阈值\((k\geq m)\),则进行一次操作2后变成了一个与原来情况完全一样的问题,期望贡献为\((1-\displaystyle \sum^{m}_{k=0}{p_k})(E_m+b)\)
所以:
那么现在的问题就是:怎么算\(p_k\)呢?
因为\(p_k=最少经过k步操作1就能有序化的排列数/n!\),所以我们只需要求 \(最少经过k步操作1就能有序化的排列数\) 就行了。
考虑一个排列\(P\),如果采用\(k \rightarrow P_k\)的方式连边的话,排除已经就位\((k=P_k)\)的位置,剩下的应该会形成一些环,每个环内只需要交换(环的大小-1)次就能有序化。
所以对一个排列\(P\),它的最少操作1步数为\(\displaystyle \sum^{}_{环}{(环的大小-1)}\)
于是我们可以dp求解。
令\(dp[k][i]\)=长度为k,最少需要i步操作1就能有序化的排列数。
现在有两种情况:
- 这样的排列中的环不包括第k项,\(P_k=k\).
- 这样的排列中的环包括第k项,\(P_k\neq k\)
第一种情况较为简单,直接从\(dp[k-1][i]\)继承即可。
第二种情况下,可以发现枚举包括第k项的环除第k项的集合可以不重不漏的枚举所有情况,且对于任何包含k的环的大小为j+1的情况,j对\(dp[k][i]\)的贡献均为
那么环内排列数怎么求呢?
对一个大小为j+1的环,从第1项开始填数。显然第1项有j种选择。若第一项填x,那么第x项有j-1种选择,因为既不能填x也不能填1.如果1,那么1和x就形成了一个独立的环,无法满足要求。
以此类推,如果第x项填y,那么第y项有j-2种选择。所以
j对\(dp[k][i]\)的贡献为$$C_{k-1}^{j}dp[k-1-j][i-j]j!$$
特别的,\(dp[k][0]=1,dp[k][k]=0\)
所以\(dp\)的递推公式为:
进一步可得$$p_k=\frac{dp[k][n]}{n!}$$
现在我们已经求出了所有的\(E_m\),可以从0到n-1遍历所有的m,求出\(E_m\)的最小值\(E_{min}=min \{ E_0,E_1,\cdots,E_{n-1}\}\)
回到题目本身,一开始我们得到排列\(p_1,p_2,\cdots,p_n\)后有两条路可走:要么直接全用操作1,要么先走一步操作2再说.
假设一开始的排列只用操作1最少用k步可有序化,那么最终答案\(E=min \{ ka,b+E_{min}\}\)
只需要预处理组合数以及阶乘。其实比上次那个好写多了。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<iomanip>
#include<algorithm>
#define int long long
using namespace std;
const double INF=1e50;
const int N=30;
int c[N][N];
int jc[N];
int dp[N][N];
double p[N];
double e[N];
double em;
int lis[N];
bool vis[N];
int n,a,b;
/*long double min(long double x,long double y)
{
if(x<y)return x;
return y;
}*/
void init_()
{
int k,i,j;
double tmp1,tmp2;
memset(c,0,sizeof(c));
memset(dp,0,sizeof(dp));
c[0][0]=1;
for(k=1;k<=n;k++)
{
c[k][0]=1;
for(i=1;i<=k;i++)c[k][i]=c[k-1][i-1]+c[k-1][i];
}
jc[0]=jc[1]=1;
for(k=2;k<=n;k++)jc[k]=jc[k-1]*k;
dp[0][0]=1;
for(k=1;k<=n;k++)
{
dp[k][0]=1; dp[k][k]=0;
for(i=1;i<k;i++)
{
dp[k][i]=dp[k-1][i];
for(j=1;j<=i;j++)dp[k][i]+=c[k-1][j]*dp[k-1-j][i-j]*jc[j];
}
}
for(k=0;k<n;k++)p[k]=(double)1.0*dp[n][k]/jc[n];
em=INF;
tmp1=tmp2=0;
/*for(k=0;k<n;k++)
{
tmp1+=dp[n][k]; tmp2+=k*dp[n][k];
e[k]=(double)1.0*(tmp2*a+(jc[n]-tmp1)*b)/tmp1;
em=min(em,e[k]);
}*/
for(k=0;k<n;k++)
{
tmp1+=p[k]; tmp2+=k*p[k];
e[k]=(double)1.0*(tmp2*a+(1-tmp1)*b)/tmp1;
em=min(em,e[k]);
}
/*i=0;
for(k=0;k<n;k++)
{
printf("%lld %lld\n",k,dp[n][k]);
i+=dp[n][k];
}
printf("* %lld %lld\n",jc[n],i);*/
return;
}
int bfs(int x)
{
int ans=0;
while(!vis[lis[x]])
{
vis[lis[x]]=true;
x=lis[x];
ans++;
}
return ans;
}
signed main()
{
int k,i,j;
int count=0;
scanf("%lld%lld%lld",&n,&a,&b);
for(k=1;k<=n;k++)scanf("%lld",&lis[k]);
init_();
memset(vis,false,sizeof(vis));
for(k=1;k<=n;k++)
{
if(lis[k]==k)continue;
if(!vis[k])
{
vis[k]=true;
count+=bfs(k);
}
}
//cout << "* " << fixed << setprecision(20) << em << endl;
//printf("* %lld\n",count);
printf("%0.20lf\n",(min((double)count*a,em+b)));
//cout<<min((long double)count*a,em+b)<<endl;
//cout << fixed << setprecision(20) << min((long double)count*a,em+b) << endl;
return 0;
}
C - Crossed out letter
水题
I - Items in boxes
题意
求\(a^{2^n} mod 2^{n+2},(1\leq a,n\leq 10^9)\)
思路
运用人类智慧感性理解一下,当n很大的时候直接输出a&1.
\(a^{2^n}\)可以看成a不断地乘自己。这样操作很多次之后膜意义下一定是越来越“靠近”1的,因为只要有一次到1了,后面就全是1了。
而且如果不是1,这玩意也很难输出。理论上这玩意可能有接近\(10^9\)位,光输出就超时了。
所以当n<30的时候就算一下,否则直接输出a&1.
代码
#include<cstdio>
#define int long long
int n,a;
signed main()
{
int k,i,j;
int b,c;
scanf("%lld%lld",&n,&a);
if(n>30)
{
if(a%2==0)printf("0\n");
else printf("1\n");
return 0;
}
b=4;
for(k=0;k<n;k++)b*=2;
for(k=0;k<n;k++)a=a*a%b;
printf("%lld\n",a);
return 0;
}

浙公网安备 33010602011771号