[考试反思]0220省选模拟27:怪异

怪异的一场考试。考的有点懵

$T1$正解是模拟,人人都能切,然而也没有多少人敢笃定这就是正解吧,常数和精度上下卡着,看运气的题了。

$T2$想到了第二档分其实离正解就不远了但是时间不够没往下想。回$T1$卡常去了。

$T3$不给状压分,于是猜到一条结论之后并不敢往下想。。。

然而最后$T1$也没有卡过,考后也在这破玩意上浪费了不少时间。。。

而且$T3$数据特别谁水暴力可以过,然而因为我不会决策点单调,所以学了学知识点,并没有用暴力水过。。。

 

T1:飞行棋

大意:长度为$n$的序列,$m$人。每个人在一个点丢骰子走路,走到$i$会被传送到$a_i$。最先走到$n$及以后的获胜。求每个人获胜概率(绝对或相对误差$\le 10^{-5}$)。

保证$a_i \neq i$的点最多$20$个。不存在$i \neq a_i$的同时$i+1 \neq a_{i+1}$。$n \le 150,m\le 20$

发现最后两条保证很奇怪。这样的话有一个人获胜并用不了多少轮。

于是大力模拟就好。$dp[i][j]$表示恰好在第$i$轮时初始位置为$j$的人取胜了的概率,转移显然。

卡常就行。如果$m=2$那就需要多模拟几轮。

注意除以$6$进行的次数不要太多,先加起来再除,除法太慢了。

可以过的参数是$100000$轮,如果$m=2$就多跑$50000$轮。

浮点数真的是太慢了,用$long \ long$取代。初值为$10^{17}$。正常转移。最后得到的$dp$值除以$10^{17}$就是概率。

不知道为啥,本机$float$跑$0.35s$,$long \ long$跑$0.9s$。但是后者才能过。。。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 float Dp[155][150003],pre[23][150003],suc[23][150003];int n,m,a[158],s[23];long long dp[155][150003];
 4 int main(){
 5     cin>>n>>m;const int R=m==2?150000:100000;
 6     for(int i=1;i<=n;++i)cin>>a[i];
 7     for(int i=1;i<=m;++i)cin>>s[i];
 8     for(int i=n+1;i<=n+5;++i)a[i]=n;
 9     dp[n][0]=60000000000000000;
10     for(int i=1;i<R;++i)for(int j=1;j<n;++j)if(a[j]==j)
11         dp[j][i]+=(dp[a[j+1]][i-1]+dp[a[j+2]][i-1]+dp[a[j+3]][i-1]+dp[a[j+4]][i-1]+dp[a[j+5]][i-1]+dp[a[j+6]][i-1])/6;
12     for(int i=1;i<=n;++i)for(int j=R-1;~j;--j)dp[i][j]+=dp[i][j+1];
13     for(int i=1;i<=n;++i)for(int j=R-1;~j;--j)dp[i][j]+=60000000000000000-dp[i][1];
14     for(int i=1;i<=n;++i)for(int j=R;j;--j)Dp[i][j]=dp[i][j]/60000000000000000.0;
15     for(int i=1;i<=R;++i)pre[0][i]=suc[m+1][i]=1;
16     for(int i=1;i<=m;++i)for(int j=1;j<=R;++j)pre[i][j]=pre[i-1][j]*Dp[s[i]][j];
17     for(int i=m;~i;--i)for(int j=1;j<=R;++j)suc[i][j]=suc[i+1][j]*Dp[s[i]][j];
18     for(int i=1,x;i<=m;++i){
19         long double ans=0;
20         for(int r=1;r<R;++r)ans+=(Dp[s[i]][r]-Dp[s[i]][r+1])*pre[i-1][r+1]*suc[i+1][r];
21         printf("%.6Lf\n",ans);
22     }
23 }
View Code

然而题解有好的多的做法,精度高复杂度低。

就是仍然是一样的dp但是逐轮考虑当前有多少概率所有人都没赢,当这个变量小于$10^{-6}$时跳出。

非常好写,也非常优秀,但是我的确也就懒得写了。于是贴一份yxs大神的代码。

 1 #include<cmath>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<algorithm>
 5 using namespace std;
 6 const double eq=1.0/6;
 7 
 8 int n,m;
 9 int a[159],b[159];
10 double c[159],q[159];
11 double f[2][159],p;
12 
13 int main(){
14     scanf("%d%d",&n,&m);
15     for(int i=1;i<=n;++i) scanf("%d",&a[i]);
16     for(int i=1;i<=m;++i) scanf("%d",&b[i]),q[i]=1;
17     if(m==1) return puts("1"),0;
18     f[0][n]=p=1; a[n+1]=a[n+2]=a[n+3]=a[n+4]=a[n+5]=a[n];
19     for(int i=1;p>1e-8;++i){
20         const bool cr=i&1;
21         memset(f[cr],0,sizeof(f[cr]));
22         for(int j=1;j<n;++j){
23             for(int k=1;k<=6;++k)
24                 f[cr][j]+=f[!cr][a[j+k]];
25             f[cr][j]/=6;
26         }
27         for(int j=1;j<=m;++j) if(f[cr][b[j]]){
28             p/=q[j]; c[j]+=f[cr][b[j]]*p; q[j]-=f[cr][b[j]]; p*=q[j];
29         }
30     }
31     for(int i=1;i<=m;++i) printf("%lf\n",c[i]);
32     return 0;
33 } 
View Code

 

T2:字符串难题

大意:多个字符串,含有$0,1,?$。问号随机变为$0,1$。求所有情况下,把这些字符串插入$trie$的节点数的和。$n \le 20,|s_i |\le 50$

然而并不难。

转化题意:求所有情况下本质不同的前缀个数之和。

首先考虑都是问号的情况。枚举长度,枚举出现了几种这个长度的前缀,再进行分配,容斥组合数啥的算一算就行。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define mod 998244353
 4 char s[21][55];int n,k,x[2222],y[2222],ans,pc,c[2][2222],rt,len[22],totlen,pw[1111],dp[22][22],C[22][22];
 5 void insert(int&p,char*s,int al=0){
 6     if(!p)p=++pc,ans++;
 7     if(!s[al])return;
 8     insert(c[s[al]-'0'][p],s,al+1);
 9 }
10 int qp(int b,int t,int a=1){for(;t;t>>=1,b=1ll*b*b%mod)if(t&1)a=1ll*a*b%mod;return a;}
11 int main(){
12     cin>>n;
13     for(int i=1;i<=n;++i)cin>>s[i],len[i]=strlen(s[i]),totlen+=len[i];
14     for(int i=1;i<=n;++i)for(int j=0;s[i][j];++j)if(s[i][j]=='?')k++,x[k]=i,y[k]=j;
15     if(k==totlen){
16         pw[0]=1;
17         for(int i=1;i<=1000;++i)pw[i]=(pw[i-1]<<1)%mod;
18         for(int i=0;C[i][0]=1,i<=20;++i)for(int j=1;j<=i;++j)C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
19         for(int i=1;i<=20;++i)for(int j=1;j<=i;++j)dp[i][j]=qp(j,i);
20         for(int i=1;i<=20;++i)for(int j=1;j<=i;++j)for(int k=1;k<j;++k)dp[i][j]=(dp[i][j]-1ll*C[j][k]*dp[i][k]%mod+mod)%mod;
21         for(int l=0;l<=50;++l){
22             int bk=0,pl=1,cnt=0;
23             for(int i=1;i<=n;++i)bk+=len[i]>=l?len[i]-l:len[i],cnt+=len[i]>=l;
24             for(int x=1;x<=cnt&&x<=pw[l];++x)pl=pl*(pw[l]-x+1ll)%mod*qp(x,mod-2)%mod,ans=(ans+1ll*pl*dp[cnt][x]%mod*x%mod*pw[bk])%mod;
25         }return cout<<(ans+mod)%mod<<endl,0;
26     }
27     for(int st=0;st<1<<k;++st){
28         for(int i=0;i<k;++i)s[x[i+1]][y[i+1]]='0'+(st>>i&1);
29         for(int i=1;i<=n;++i)insert(rt,s[i]);
30         for(int i=1;i<=pc;++i)c[0][i]=c[1][i]=0;rt=pc=0;
31     }cout<<ans<<endl;
32 }
View Code

然后发现$n$只有$20$。于是枚举子集进行容斥表示子集内前缀相同。容斥系数就是$-1^{|S|-1}$

长度增加$1$时,看可能的前缀数如何变化,全是问号就乘$2$,有$0$有$1$就跳出。

然后就没了吧。。。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 #define mod 998244353
 4 int n,ans,cnt[22],len[22],pw[1111];char s[22][55];
 5 int main(){
 6     cin>>n;
 7     for(int i=0;i<n;++i)cin>>s[i]+1,len[i]=strlen(s[i]+1);
 8     for(int i=0;i<n;++i)for(int j=1;j<=len[i];++j)cnt[i]+=s[i][j]=='?';
 9     pw[0]=1;for(int i=1;i<=1000;++i)pw[i]=(pw[i-1]+pw[i-1])%mod;
10     for(int st=1;st<1<<n;++st){
11         int pl=0,r=0;
12         for(int i=0;i<n;++i)pl+=cnt[i],r+=st>>i&1;
13         if(st==1)ans=pw[pl];
14         for(int l=1;l<=50;++l){
15             int x1=0,x0=0,q=0;
16             for(int i=0;i<n;++i)if(st&1<<i)
17                 if(!s[i][l])goto xx;
18                 else if(x1&&s[i][l]=='0')goto xx;
19                 else if(x0&&s[i][l]=='1')goto xx;
20                 else if(s[i][l]=='0')x0=1;
21                 else if(s[i][l]=='1')x1=1;
22                 else pl--,q++;
23             if(!(x0|x1))pl++;
24             ans=(ans+(r&1?pw[pl]:mod-pw[pl]))%mod;
25         }xx:;
26     }cout<<ans<<endl;
27 }
View Code

 

T3:障碍雷达

大意:

最大化价值。

首先猜想要么装到最高,要么不装。显然正确,因为费用是一次函数收益是二次的。

然后按照横坐标排序之后分类讨论就挺简单了。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 struct P{int x,y;long long c;}p[8888];long long dp[8888],ans;int n,t;string s[4]={".00",".25",".50",".75"};
 4 long long f2(int x){return 1ll*x*x;}
 5 int main(){
 6     cin>>t;while(t--){
 7         cin>>n;ans=0;
 8         for(int i=1;i<=n;++i)scanf("%d%d%lld",&p[i].x,&p[i].y,&p[i].c),p[i].c*=p[i].y<<2;
 9         sort(p+1,p+1+n,[](P a,P b){return a.x<b.x;});p[0].x=-1e9;
10         for(int i=1;i<=n;++i)for(int j=0;j<i;++j)if(p[j].x+p[j].y<=p[i].x+p[i].y)
11             dp[i]=max(dp[i],dp[j]+f2(p[i].y<<1)-f2(max(0,p[j].x+p[j].y-p[i].x+p[i].y))-p[i].c);
12         for(int i=1;i<=n;++i)ans=max(ans,dp[i]),dp[i]=0;
13         cout<<(ans>>2)<<s[ans&3]<<endl;
14     }
15 }
View Code

然后想想$n^2dp$可以咋优化。然后睿智的$skyh$说:决策单调性。

于是这题就有决策单调性了。

考虑两种转移,其中要去重的部分它的那个式子看起来就很有决策单调性。

于是就可以按照单调队列维护决策点单调,维护每个点控制的决策区间然后二分就好了。时间复杂度是$O(nlogn)$

然而貌似细节挺多代码也不好写于是写的$skyh$大神的方法。

这种$dp$前后依赖可以用$CDQ$中序遍历那种思想。

然后发现左区间更新右区间时,左边按照右端点排序右边按照左端点排序后,决策点单调。

然后写那种经典的分治就好了。

只不过要注意这里因为一个点要更新多次所以进入分治时初值可能不是0,从而可能因为没有更新最大值而找不到最优决策点。

于是开一个变量记录当前做贡献的区间里的最大值(不一定有原dp值大)就可以了。

时间复杂度是$O(nlog^2n)$的然而常数不大。

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 struct P{int x,y;long long c,dp;}p[111111];long long ans;int n,t;string s[4]={".00",".25",".50",".75"};
 4 long long f2(int x){return 1ll*x*x;}
 5 void solve(int l,int r,int L,int R){
 6     if(l>r)return;
 7     int md=l+r>>1,pt=L;long long bst=-1e18;
 8     for(int i=L;i<md&&i<=R;++i)if(p[i].x+p[i].y<=p[md].x+p[md].y)
 9         if(bst<p[i].dp+f2(p[md].y<<1)-f2(max(0,p[i].x+p[i].y-p[md].x+p[md].y))-p[md].c)
10             bst=p[i].dp+f2(p[md].y<<1)-f2(max(0,p[i].x+p[i].y-p[md].x+p[md].y))-p[md].c,pt=i;
11     p[md].dp=max(p[md].dp,bst);
12     solve(l,md-1,L,pt);solve(md+1,r,pt,R);
13 }
14 void cdq(int l,int r){int md=l+r>>1;
15     if(l==r)return;    cdq(l,md);
16     sort(p+l,p+md+1,[](P a,P b){return a.x+a.y<b.x+b.y;});
17     sort(p+md+1,p+r+1,[](P a,P b){return a.x-a.y<b.x-b.y;});
18     solve(md+1,r,l,md);cdq(md+1,r);
19 }
20 int read(){int p=0,f=1;char ch=getchar();
21     while(ch>'9'||ch<'0')f=ch=='-'?-1:1,ch=getchar();
22     while(ch<='9'&&ch>='0')p=(p<<3)+(p<<1)+ch-48,ch=getchar();
23     return f*p;
24 }
25 int main(){
26     cin>>t;while(t--){
27         cin>>n;ans=0;
28         for(int i=1;i<=n;++i)p[i].x=read(),p[i].y=read(),p[i].c=read(),p[i].c*=p[i].y<<2;
29         sort(p+1,p+1+n,[](P a,P b){return a.x<b.x;});p[0].x=-1e9;
30         cdq(0,n);
31         for(int i=1;i<=n;++i)ans=max(ans,p[i].dp),p[i].dp=0;
32         cout<<(ans>>2)<<s[ans&3]<<endl;
33     }
34 }
View Code

一些常数方面的问题:用$c++11$的函数内部声明比较函数超级快!

然后最后还是要说那个伟大而没脸而有效的大神做法:$O(300n)$。给个复杂度就知道干啥了。

对付$n^2dp$题好像出题人不好好造数据都可以卡枚举界从而拿不少分甚至$AC$

 

posted @ 2020-02-21 19:18  DeepinC  阅读(...)  评论(...编辑  收藏