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;
}
posted @ 2025-10-19 11:02  Water_M  阅读(2)  评论(0)    收藏  举报