10.18 学校模拟赛 T4
题意:有一个含 \(\text{NaN}\) 的排列 \(1, 2, 3, \dots, n-1, \text{NaN}\)。其中 \(n=1\) 时排列有一个元素 \(\text{NaN}\)。求这个排列构成小根堆的概率,对 \(10^9+7\) 取模。
一个排列 \(a\) 构成小根堆,当且仅当对于所有 \(2 \le i \le n\),满足 \(a_i < a_{\lfloor i/2 \rfloor}\) 不成立。而 \(\text{NaN}\) 与任何数比较的结果都是 false。
\(T\) 组数据,\(n \le 10^9,T \le 10^3\)。
这里先考虑没有 \(\text{NaN}\) 的情况。
这个排列构成一个完全二叉树对吧。
发现就是 [ZJOI2010] 排列计数 的升级版。
观察一下那道题的 DP 式子。是组合数乘上两边的状态。
展开以后不难发现,阶乘和子结点的阶乘能消,发现总方案数实际上是 \(\frac{n!}{\prod_{u=1}^n sz_u}\),其中 \(sz\) 为子树大小。
然后这里是有一个技巧的。一棵完全二叉树是可以“剖分”成 \(len=O(\log n)\) 个满二叉树,以及 \(len-1\) 个不在这些满二叉树中的点的。
并且这 \(len-1\) 个点是构成一条链的。
证明就是,一个结点的左右子树必有一个是满的。然后深度又是 \(O(\log n)\) 的,所以证毕。
优化就是,可以预处理出来所有满二叉树的 \(\prod \frac{1}{sz_u}\)。再暴力计算链上的 \(sz\),总复杂度做到 \(O(\log n)\)。
再来考虑有 \(\text{NaN}\) 的情况。
可以枚举 \(\text{NaN}\) 在哪些不同的位置。
不难发现,\(\text{NaN}\) 在一个点上,就会把整棵树分成三部分。(祖先,两个儿子)
要算的概率,就是三棵不同的树的概率之积。
而这三棵树的概率,是不依赖树的形态的,也符合上面 \(size\) 之积的结论。
总概率就是 \(size\) 之积的倒数和,除以 \(\text{NaN}\) 能放的位置数(就是 \(n\))。
具体怎么算这 \(size\) 积呢。
首先如果 \(\text{NaN}\) 放在链上某点,那么影响的 \(size\) 只会在祖先。暴力更新就可以。
如果 \(\text{NaN}\) 在剖出来的某个满二叉树里面,仍然可以暴力更新影响的祖先。这里发现满二叉树里深度相同的点是等价的。于是枚举是 \(O(\log^2)\) 的。
考虑一些逆元的计算,总复杂度是 \(O(T\log^3 n)\) 的。
#include <bits/stdc++.h>
using namespace std;
//#define filename "nan"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
#define multi_cases 1
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define ull unsigned long long
#define all(v) v.begin(), v.end()
#define upw(i, a, b) for(int i = (a); i <= (b); ++i)
#define dnw(i, a, b) for(int i = (a); i >= (b); --i)
template<class T> bool vmax(T &a, T b) { return b > a ? a = b, true : false; }
template<class T> bool vmin(T &a, T b) { return b < a ? a = b, true : false; }
template<class T> void clear(T &x) { T().swap(x); }
//const int N = _;
const int P = 1000000007;
void vadd(int &a, int b) { a += b; if(a >= P) a -= P; }
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) res = 1ll * res * a % P;
a = 1ll * a * a % P;
b >>= 1;
}
return res;
}
int inv(int x) { return x == 0 ? 1 : qpow(x, P-2); }
int pre[30];
int table[30];
vector<int> a;
void divide(int n) {
int dep = upper_bound(table, table+30, n) - table;
int tot = (1 << dep) - 1;
if(n == tot) return a.push_back(dep), void();
int half = 1 << (dep - 2);
int d = n - tot / 2;
if(d <= half) {
a.push_back(dep - 2);
divide(n - tot / 4 - 1);
}
else {
a.push_back(dep - 1);
divide(n - tot / 2 - 1);
}
}
int n;
void WaterM() {
cin >> n;
clear(a), divide(n);
// for(auto v : a) cerr << v << ' ';
// cerr << '\n';
int ans = 0, len = a.size();
int tot = 1; //不放nan的所有size之积的倒数
vector<int> b(len);
b[len-1] = (1 << a[len-1]) - 1;
dnw(i, len-2, 0) b[i] = b[i+1] + (1 << a[i]) - 1 + 1; //不放NaN的size
upw(i, 0, len-2) tot = 1ll * inv(b[i]) * tot % P;
upw(i, 0, len-1) tot = 1ll * pre[a[i]] * tot % P;
int ptot = 0;
//先管不在某一个满二叉树内的点
if(len > 1) {
dnw(i, len-2, 0) { //在深度为i+1的链上点放了NaN
//影响所有它的祖先的size
int prod = 1ll * tot * b[i] % P; //除以inv(b[i])
upw(j, 0, i-1) {
int sz = b[j] - b[i]; //影响后的size
prod = 1ll * prod * b[j] % P; //除以inv(b[j])
prod = 1ll * prod * inv(sz) % P;
}
vadd(ans, prod);
++ptot;
}
}
//再管NaN放在了剖出来的某一个满二叉树里,的情况
upw(i, 0, len-1) {
//放的位置只和深度有关
upw(d, 0, a[i]-1) {
//暴力改祖先的size
//先改满二叉树内的祖先
int prod = tot;
upw(ad, 0, d) {
prod = 1ll * prod * ((1 << a[i] - ad) - 1) % P;
prod = 1ll * prod * inv((1 << a[i] - ad) - (1 << a[i] - d)) % P;
}
//再改满二叉树上面链中的祖先
upw(j, 0, min(len-2, i)) {
prod = 1ll * prod * b[j] % P;
prod = 1ll * prod * inv(b[j] - ((1 << a[i] - d) - 1)) % P;
}
//该深度有 2^d 个位置能放
prod = 1ll * (1 << d) * prod % P;
ptot += 1 << d;
vadd(ans, prod);
}
}
cout << 1ll * ans * inv(ptot) % P << '\n';
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
pre[0] = 1;
upw(i, 1, 29) pre[i] = 1ll * pre[i-1] * pre[i-1] % P * inv((1 << i) - 1) % P;
table[0] = 1;
upw(i, 1, 29) table[i] = table[i-1] << 1;
while(_--) WaterM();
return 0;
}