模拟49 T3 题解+注解std

首先是两个结论
image
所以可以首先分层,然后每层进行dp,最后想办法统计答案
设在这一层时,\(f_{i,j}\)代表这一层已经坐了\(i\)个人,剩\(j\)个偶数区间的概率,考虑当前的人选了奇数还是偶数
为什么这么设计是因为他奇数和偶数显然不能等同考虑,因为奇数的话他位置固定,偶数则可以有两种选择
最后的时候翻转有点费脑子,就是两边互换的操作
image
你默认了他选择\(p\),他实际上还可以选\(p+1\),那么选了\(p+1\)的左区间的答案就相当与原来的右区间的答案,现在右区间就相当于原来左区间的答案,由于概率均等要除以二
菜鸡搞了一天的std,精华都在里面
手动注释50行,加入了一些自己的理解,可能有误欢迎指正
感谢zjx强者帮助理解,%%%

//注意
//本题中odd为偶数,even为奇数
#include<bits/stdc++.h>
using namespace std;
const int N=1030;
int n,mod;
int qpow(int x,int k,int ans=1){
    while(k){
        if(k&1) ans=ans*x%mod;
        x=x*x%mod;
        k>>=1;
    }
    return ans;
}
//dp为答案
//f为题解中dp数组
int dp[N][N],f[N][N],g[N][N],vis[N],inv[N],cnt[N],odd[N],pos[N];
int main(){
    scanf("%d%d",&n,&mod);
    for(int i=1;i<=n;++i) inv[i]=qpow(i,mod-2);
    vis[0]=vis[n+1]=true;
    for(int i=1;i<=n;++i){
        int pl=0,pr=0,mx;
        for(int j=0;j<=n;++j){
            int r=j+1;
            while(!vis[r]) ++r;
            if(r-j>pr-pl) pl=j,pr=r;
            j=r-1;
        }
        ++cnt[mx=(pr-pl)>>1]; odd[mx]+=(pr-pl)&1;
        pos[i]=pl+mx; vis[pl+mx]=true;
        //这种方法保证了在mx相等的时候,人一定先坐奇区间再坐偶区间
    }
//cnt[i]代表最小距离最大值为i的层中要坐的人数   
//mx的意思是最小距离最大值  
//odd[i]代表最小距离最大值为i的层中包含的长度偶数的区间个数
//直接模拟坐下过程,每个人在之前基础上选择最靠左的位置坐下,由于定理1的存在,cnt与odd可以得到正确的值
//pos[k]代表按这种方法每个人最终坐下的位置
    for(int i=1;i<=n;i++)cout<<pos[i]<<" ";puts("");
    int sum=n;
    //sum代表还没有坐下的人数
    for(int i=1;i<=n;++i){
        if(!cnt[i]) continue;//剪枝
        //不存在这个层,一共只有log层
        int l=sum-cnt[i]+1,r=sum;
        //当前这个层要坐cnt[i]个人,l是在这一层坐的最后一个人
        if(i==1) for(int j=l;j<=r;++j) for(int k=l;k<=r;++k) dp[j][pos[k]]=inv[cnt[i]];//
        //到底特判 
        //对于每个要在这一层坐下的人,他们所有人的位置都是公共的,因为都在一层里
        //所以每个人他的位置是完全随机的,别人坐的位置他都能坐,概率均等,所以是人数分之一
        else{
        		//f[j][k]代表这一层已经坐了j个人,还有k个长度为偶数区间的概率
            for(int j=0;j<=cnt[i];++j) for(int k=0;k<=odd[i];++k) f[j][k]=0;//清空数组
            //memset(f,0,sizeof(f));
            f[0][odd[i]]=1; //还没有坐人,那么该层剩下odd[i]个偶数区间的概率为1
            int p=l+odd[i]-1; //偶数区间长度的最后一个人
            //感性理解为一个分界点,所以可以视为l到p都坐偶数,p到r都坐奇数
            //正确性在于一层之内离最近的人的最大距离固定,况且我们的pos本来就只模拟了多种情况中的一种
            //因此这一层之内的人的顺序本来就是可以变的,所以我们可以对坐在奇区间和偶区间的分开考虑
            for(int j=1;j<=cnt[i];++j){//枚举每个人
                int oddw=0,evenw=0;//odd为偶数 even为奇数
                for(int k=0;k<=odd[i];++k){
                    if(!f[j-1][k]) continue;//dp转移 所以f值为0的时候可以剪枝 k表示剩余多少个偶数区间
                    int frac=(cnt[i]-(j-1))+k,w=0; //括号内表示剩余的区间个数 +k表示剩余多少个转移点
                    //奇区间一个转移点,偶区间两个转移点
                    if(k){//还有偶区间剩余
                        w=f[j-1][k]*k*2%mod*inv[frac]%mod;//占一个偶区间位置 那么概率为k*2/转移点
                        //w对下一个f的贡献,即还剩k个偶数区间时选到下一个偶数区间的概率
                        oddw=(oddw+w*inv[odd[i]*2])%mod; //方便累加答案 对于这一次的转移 可能作用在不同的转移点
                        //oddw就是对于一个人j他选到每一个偶数区间的概率
                        //所有w累加而来得到的是他选到偶数区间的概率,除以总区间数就是i一个区间的概率
                        (f[j][k-1]+=w)%=mod;
                    }
                    if(cnt[i]-odd[i]){//可以向奇区间转移
                        w=f[j-1][k]*(frac-2*k)%mod*inv[frac]%mod;//向奇数区间转移的概率
                        evenw=(evenw+w*inv[(cnt[i]+odd[i])-odd[i]*2])%mod;//向不同奇数区间转移
                        //这里的w,evenw和上面同理,只不过把偶数换成了奇数
                        (f[j][k]+=w)%=mod;
                    }
                }
                //f这个dp主要算出了oddw,evenw这个数值,来更新最终答案
                //cout<<(oddw*odd[i]*2%mod+evenw*((cnt[i]+odd[i])-odd[i]*2)%mod)%mod<<endl;可以发现输出这个是1
                //l+j-1代表这个人在所有人之中是第几个
                //对于枚举到的这个人,计算他最后的答案
            	 //他可能坐在l到r的每个pos位置,因此都要累积
                for(int u=l;u<=p;++u) (dp[l+j-1][pos[u]]+=oddw)%=mod,(dp[l+j-1][pos[u]+1]+=oddw)%=mod;//累加答案
                //统计人坐偶数的答案
                //此时每个人都坐在偶数区间,他既然可以坐在偶数区间的偏左一侧(pos),也一定可以坐在偶数区间的偏右一侧,概率相等
                //对于每个偶数位置的概率都要加上刚才得到的oddw
                for(int u=p+1;u<=r;++u) (dp[l+j-1][pos[u]]+=evenw)%=mod;
                //统计人坐奇数区间的答案,只能坐在中间所以不要加
            }
            //这里已经做完了dp  
            //由于钦定了偶数区间坐在左边,那么坐在右边也是一样的答案  
            //利用对称性推出偶数区间的答案
            for(int j=l;j<=p;++j){//这些j坐在偶数区间
                int L=pos[j]-i+1,R=pos[j]+i;//当前的偶区间左右端点,i是段长
                //因为他是按着层来的,所以实际上是倒着推的,后面人的概率都已经算过
                for(int v=L;v<=R;++v){
                    if(v==pos[j])continue;
                    //不能是选择的点
                    //这里不加也没有关系,因为当v==pos[j]的时候没有贡献
                  //由于枚举的是下面的点,那么下面的点对于任意j,dp[u][pos[j]]都是0
                    for(int u=r+1;u<=n;++u){//后面每一个人
                        int s=v<pos[j]?v+i+1:v-i,w=dp[u][v]*inv[2]%mod;//后一个人在位置v的概率除2
                        //s找到他对称的点在哪里
                        //这里不是简单的对称,因为换了之后左右会互换
                        (g[u][v]+=w)%=mod; (g[u][s]+=w)%=mod;//平均一下 
                        //每个人其实在这一层都有均等的概率坐在偶数区间的靠左和靠右的地方,但是刚才只算了靠左的
                        //事实上等概率坐在左侧和右侧,所以要除以二再平均分
                    }
                }
                for(int v=L;v<=R;++v) for(int u=r+1;u<=n;++u) dp[u][v]=g[u][v],g[u][v]=0;
            }
        }
        sum-=cnt[i];//考虑下一层 剩余人数减少
    }
    for(int i=1;i<=n;i++,puts("")) for(int j=1;j<=n;j++) printf("%d ",dp[i][j]);
    return 0;
}

如果觉得缩进毒瘤就看看洛谷链接

那么这个题是咋想出来的呢
出题人是个毒瘤
首先看出能分层是不难的,关键在于你不知道怎么dp,以及为什么要这样dp
把答案分成区间考虑这个确实想不到,理论上来讲似乎能根据他的层间独立性来做
dp过程应该不难,统计答案是一个思维难点,不过从概率角度分析应该也能分析出来
关键是最后你不能想到他还要搞一个翻转,本质上是对你一开始类似贪心选择的正确性和局限性要有一个把握
细节很多,考场作出可能性不大,除非你特别清楚你要干什么以及你应该怎么干否则很难在不参考std的情况下写出来
%%%沈队似乎有更好的思路,%%%大帝12h成功实现

那么收获是什么呢
首先是他这个dp时的策略,在直接做不好做的时候开辅助数组的思想,这个题算是应用的淋漓尽致
还有他这个进行非常数次dp,也就是每层dp一次的思想,十分开阔眼界
虽然现在不太能应用,但是至少算见过这样的题了
dp修补策略,就是最后的翻转,对答案进行修改使他正确
觉得最神的还是这个求pos的操作,感觉就是搞了一个随便的东西然后把它变成对的
在这种概率题当中发现不变或无影响的东西,然后用简单的方法实现
总之对dp的认识算是达到了一个新高度吧abab

posted @ 2021-09-09 09:39  D'A'T  阅读(85)  评论(1)    收藏  举报