Description
对于给定的数字\(n,l,r\),求在 \(C_n^l,C_n^{l+1},C_n^{l+2},\dots,C_n^r\) 中共有多少个奇数。
行号\(n\)从0开始计数。特别地,我们规定\(C_0^0 = 1\)。
数据保证 \(0 \leq n \leq 10^{18}\)
Algorithm1: Divide and Conquer
拿到一个1e18的题,第一反应肯定是先打个表看看嘛。
我们知道有杨辉三角:
我们把其中的奇数标成1,偶数标成0,于是得到:
你也可以再列出一些,不过总之我们发现,这个01构成的三角形似乎有着某种绝佳的对称性。
实际上,这正是一种分形,名叫谢尔宾斯基三角形。
根据这种绝佳的对称性,我们容易设计出一种分治算法:
typedef long long ll;
ll solve(ll n, ll l, ll r)
{
ll ret = 0, cnt_0, L, R;
if(n == 0) ret = 1;
else
{
cnt_0 = powt(ceil(log2(n + 1))) - n - 1;
R = (n + 1 - cnt_0) / 2 - 1;
L = R + cnt_0 + 1;
if(l <= R) ret += solve(R, l, min(R, r));
if(L <= r) ret += solve(R, max(L, l) - L, r - L);
}
return ret;
}
我们用cnt_0
统计所求行最中间0的个数,
R
表示将中间的0去掉后左侧部分的右端点,
L
表示将中间的0去掉后右侧部分的左端点。
然后分别向左右两侧递归地计算答案。
这种写法比较直观,但不是最优美简洁的。
该算法的复杂度分析是比较困难的,不过我们容易发现 \(n = 2 ^ t - 1\) 时是最坏的情况。
此时有 \(T(n) = 2 \times T(\frac{n}{2}) + log_2 n\),复杂度是略大于\(O(n)\)的。
但经测试,在数据随机的背景下,\(n\) 在 1e11 的量级时该算法仍然有较好的表现。
平均复杂度的证明留给读者。
Algorithm2: Lucas' Theorem
求奇偶性,不就是求 \(mod\ 2\) 的值吗?
求组合数取模后的结果,不就是Lucas定理吗?
此外,利用上面的切尔宾斯基三角形其实也是证明Lucas定理的一种方法。
Lucas's Theorem:
其中,
只是另一种表达方式罢了,只不过这样写更容易看清。
接下来我们考虑求 \(C_n^m \ mod \ 2\),可以考虑对 \(C_n^m\) 连续使用Lucas定理。
我们将\(n, m\)两个数二进制表示:
这里 \(n,m\) 的二进制位数当然不一定相同,我们可以在最高位补0,效果相同。
那么我们有,
如果 \(C_n^m\) 是奇数,那么右式就应当为得1,否则即为0。
而我们知道\(a_i, b_i\)的取值只有\(0, 1\),又有:
稍作观察就能发现:
由此,我们 \(O(n)\) 地枚举 \([l, r]\) 间的所有整数即可。
register long long cnt = 0;
for(register long long i = l; i <= r; ++i)
cnt += ((i | n) == n);
代码就这。
Algorithm3: Dynamic Programing
可以发现我们用了一下Lucas定理之后平均复杂度可能还不如找规律。
这是因为我们枚举了 \([l, r]\) 间的所有整数的缘故。
但考虑现在的问题:求 \([l, r]\) 间或\(n\)得\(n\)的数的个数,这是一个容易解决的数位DP问题。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<bool> lim;
ll n, l, r, dp[64][2];
int len;
ll dfs(int ith, bool ful)
{
if(ith == len) return 1;
if(dp[ith][ful]) return dp[ith][ful];
ll res = 0;
if((1ll << (len - ith - 1)) & n)
{
if(lim[ith])
{
res += dfs(ith + 1, ful);
res += dfs(ith + 1, false);
}
else
{
if(ful) res += dfs(ith + 1, ful);
else res += 2 * dfs(ith + 1, ful);
}
}
else res += dfs(ith + 1, ful & (!lim[ith]));
return dp[ith][ful] = res;
}
ll solve(ll x)
{
if(x == -1) return 0;
lim.clear();
memset(dp, 0, sizeof(dp));
while(x) {
lim.push_back(x & 1);
x >>= 1;
}
len = lim.size();
reverse(lim.begin(), lim.end());
return dfs(0, true);
}
int main()
{
freopen("math.in", "r", stdin);
freopen("math.out", "w", stdout);
cin >> n >> l >> r;
cout << solve(r) - solve(l - 1) << endl;
return 0;
}
复杂度是易于分析的\(O(logn)\),证明留给读者。