CF335E Counting Skyscrapers 題解
提供一種最劣解第一且巨大難寫的做法(
Bob
顯然真正的樓量可以達到 \(314!\),是沒辦法直接做的,再加上唯一方案的樣例,可以猜測有簡單的結論。
考慮當樓高度為 \(k(k<h)\) 時,每種高度對答案的貢獻為 \(2^{k-1}\times 2^{-k}\),即 \(\frac{1}{2}\),當樓高度為 \(k(k \ge h)\) 時,每種高度對答案的貢獻和為 \(2^k\times 2^{-k}\),即 \(1\)。顯然貢獻都與 \(h\) 無關,也就是結論只和 \(n\) 有關,隨便推幾組資料再加上樣例,容易猜出答案即為 \(n\)。
Code:
void Sub2(){ scanf("%d%d",&n,&h); printf("%d\n",n); }
Alice
觀察到高度 \(\ge h\) 的樓本質上是一致的,可以欽定樓的高度區間為 \([1,h]\)(高度為 \(h\) 的概率變為 \(2^{1-h}\))。
設 \(k\) 號樓的高度最高,則 Bob 一定會經過 \(k\) 號樓。容易發現,在 \(k\) 號樓前,Bob經過的樓的高度單調不降,\(k\) 號樓後單調不升。
注意,為了防止重複計算,若高度相同,欽定靠前的最高。
然後就可以轉移了。
設 \(f_{i,j,k,0/1}\) 表示轉移完第 \(i\) 棟樓,最後一個經過的 \(x\) 號樓的高度為 \(j\),\([x+1,i]\) 中最大高度為 \(k\),是/否 經過最高的樓的期望
設 \(g_{i,j,k,0/1}\) 表示轉移完第 \(i\) 棟樓,最後一個經過的 \(x\) 號樓的高度為 \(j\),\([x+1,i]\) 中最大高度為 \(k\),是/否 經過最高的樓的概率。
DP的時候用刷表法,列舉第 \(i\) 棟樓的高度轉移即可。時間複雜度 \(O(nh^3)\) 要卡億點常並滾動陣列。
Code:
const double eps=1e-15; const int maxn=30010; const int maxh=35; int n,h; double g[2][maxh][maxh][2]; double f[2][maxh][maxh][2]; double V[maxh],S[maxh]; void Sub1(){ scanf("%d%d",&n,&h); for(int i=0;i<h;i++){ V[i]=1.0/(1<<i+1),S[i]=(1-1.0/(1<<i)); f[1][i][0][0]=V[i]+0.5,g[1][i][0][0]=V[i]; } if(h) V[h]=V[h-1],S[h]=(1-1.0/(1<<h)); else V[h]=1; for(int i=0;i<=h;i++) f[1][i][0][1]=V[i],g[1][i][0][1]=V[i]; //以上是初始化 double tmp1,tmp2,tmp; for(int i=1,ii=1,iii=0;i<n;i++,ii^=1,iii^=1){ memset(f[iii],0,sizeof(f[iii])); memset(g[iii],0,sizeof(g[iii])); //注意,為了處理當j=i時(Bob經過當前樓)k也為0的情況 //這裡列舉的k,u都加了1(j=0時k才為0) //為了卡常,轉移的式子很醜,但本質是一樣的 for(int j=0;j<=h;j++) for(int k=0;k<=j;k++){ tmp1=f[ii][j][k][0],tmp2=g[ii][j][k][0],tmp=tmp2/2; if(tmp1>eps){ //u<=k f[iii][j][k][0]+=tmp1*S[k]; g[iii][j][k][0]+=tmp2*S[k]; tmp1*=V[k],tmp2*=V[k]; //-------- for(int u=k+1;u<=h+1;u++){ if(u-1>=j) f[iii][u-1][0][0]+=tmp1+tmp,g[iii][u-1][0][0]+=tmp2; else f[iii][j][u][0]+=tmp1,g[iii][j][u][0]+=tmp2; if(u-1>j) f[iii][u-1][0][1]+=tmp1,g[iii][u-1][0][1]+=tmp2; if(u<h) tmp1/=2,tmp2/=2; } f[iii][h][0][0]+=tmp; } tmp1=f[ii][j][k][1],tmp2=g[ii][j][k][1],tmp=tmp2/2; if(tmp1>eps){ //u<=k f[iii][j][k][1]+=tmp1*S[k]; g[iii][j][k][1]+=tmp2*S[k]; tmp1*=V[k],tmp2*=V[k]; //-------- for(int u=k+1;u<=j+1;u++){ f[iii][u-1][0][1]+=tmp1+tmp,g[iii][u-1][0][1]+=tmp2; if(u-1<j) f[iii][j][u][1]+=tmp1,g[iii][j][u][1]+=tmp2; if(u<h) tmp1/=2,tmp2/=2; } if(j==h) f[iii][h][0][1]+=tmp; } } } double ans=0; for(int i=0;i<=h;i++) ans+=f[n&1][i][0][1]; printf("%.9lf",ans); }
能不能更快?
上述期望DP本質上是分了 \([1,k],[k+1,j][j+1,h]\) 三個區間分別轉移行和列,考慮將決策點放到線段樹上,則問題轉換為了線段樹的區間修改/單點查詢,時間複雜度 \(O(nh^2\log h)\)。
但由於要對 \(f,g\)、是否經過最高樓、行或列分類討論,所以要開 \(8nh\) 棵線段樹,本地極限資料要跑 \(10s+\),沒有實質效果。
能不能更快?
觀察到轉移方程與 \(n\) 無關,考慮構造 \(h\times h\) 的矩陣,跑矩陣快速冪即可,時間複雜度 \(O(h^3logn)\),可以通過本題,時間與空間上都更優。