【一句話題意】給n塊長度爲1~n的木板,組成一個長度爲n的序列,滿足相鄰的木板都比自己長或短。方案按字典序排列。有多組詢問,問當有ai塊木板時,第bi號方案是什麼。
ai<=20,bi<=263
【分析】類似於倍增dp的“拼湊”思想和手推康託展開時的方式,我們可以用“試填”的方式來確定第bi號方案中各個木板的長度。比如從小到大枚舉,如果當第一塊木板長爲h時,N-1塊木板的構成的方案數T大於bi,則第一塊木板長度就應該是h;否則就應該是更長的,同時從bi中減去減去T。
爲了加快“試填“的速度,我們要先預處理出所有的T值。這就需要把整個程序分成兩個部分,dp預處理和“試填”(對詢問的回答)。
dp部分定義f[i,j,k]表示用i塊長度不同木板構成左邊的序列,同時最左邊的木塊長度從小到大排爲第j位,並且狀態爲k(k=0表示處於低位,k=1表示處在高位)的方案總數。轉移方程爲
同樣的dp方式還可以用於離散化之後的問題,兩者是等價的。
再是“試填”部分。特別的第一塊木板既有可能是高位又有可能是低位,所以要特殊處理。其他的操作大致就是開頭所講。
【code】
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
LL n,m,T;
LL f[21][21][2];
void prework(){
f[1][1][0]=f[1][1][1]=1;
for(int i=2;i<=20;i++)
for(int j=1;j<=i;j++){
for(int p=j;p<i;p++)
f[i][j][0]+=f[i-1][p][1];
for(int p=1;p<j;p++)
f[i][j][1]+=f[i-1][p][0];
}
}
int main(){
prework();
cin>>T;
while(T--){
cin>>n>>m;
bool used[21];
memset(used,0,sizeof(used));
int last,k;
for(int j=1;j<=n;j++){
if(f[n][j][1]>=m){
last=j,k=1;
break;
}
else m-=f[n][j][1];
if(f[n][j][0]>=m){
last=j,k=0;
break;
}
else m-=f[n][j][0];
}
used[last]=1;
printf("%d",last);
for(int i=2;i<=n;i++){
k^=1;
int j=0;
for(int len=1;len<=n;len++){
if(used[len]) continue;
j++;
if(k==0&&len<last||k==1&&len>last){
if(f[n-i+1][j][k]>=m){
last=len;
break;
}
else m-=f[n-i+1][j][k];
}
}
used[last]=1;
printf(" %d",last);
}
printf("\n");
}
return 0;
}