1. 程式人生 > 其它 >CF335E Counting Skyscrapers 題解

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)\),可以通過本題,時間與空間上都更優。