• 自主命题:奇组合数个数统计


    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\ 1 1\ 1 2 1\ 1 3 3 1\ 1 4 6 4 1\ 1 5 10 10 5 1\ 1 6 15 20 15 6 1 \ 1 7 21 35 35 21 7 1 \ dots dots dots ]

    我们把其中的奇数标成1,偶数标成0,于是得到:

    [1\ 1 1\ 1 0 1\ 1 1 1 1\ 1 0 0 0 1\ 1 1 0 0 1 1\ 1 0 1 0 1 0 1 \ 1 1 1 1 1 1 1 1 \ dots dots dots ]

    你也可以再列出一些,不过总之我们发现,这个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 imes T(frac{n}{2}) + log_2 n),复杂度是略大于(O(n))的。

    但经测试,在数据随机的背景下,(n) 在 1e11 的量级时该算法仍然有较好的表现。


    平均复杂度的证明留给读者。


    Algorithm2: Lucas' Theorem

    求奇偶性,不就是求 (mod 2) 的值吗?

    求组合数取模后的结果,不就是Lucas定理吗?

    此外,利用上面的切尔宾斯基三角形其实也是证明Lucas定理的一种方法。

    Lucas's Theorem:

    [inom{x_1 cdot p + y_1}{x_2 cdot p + y_2} equiv inom{x_1}{x_2} cdot inom{y_1}{y_2} (mod p) ]

    其中,

    [inom{m}{n} = C_n^m ]

    只是另一种表达方式罢了,只不过这样写更容易看清。

    接下来我们考虑求 (C_n^m mod 2),可以考虑对 (C_n^m) 连续使用Lucas定理。

    我们将(n, m)两个数二进制表示:

    [n = sum_{i = 0}^K a_i cdot 2^i \ m = sum_{i = 0}^K b_i cdot 2^i ]

    这里 (n,m) 的二进制位数当然不一定相同,我们可以在最高位补0,效果相同。

    那么我们有,

    [inom{m}{n} equiv prod _ {i = 0} ^ K inom{b_i}{a_i} (mod p) ]

    如果 (C_n^m) 是奇数,那么右式就应当为得1,否则即为0。

    而我们知道(a_i, b_i)的取值只有(0, 1),又有:

    [inom00 = 1, inom01 = 1, inom11 = 1, inom10 = 0 ]

    稍作观察就能发现:

    [inom{m}{n} mod 2 = cases{0 & m or n $ e$ n\1 & m or n = n} ]

    由此,我们 (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)),证明留给读者。

  • 相关阅读:
    零点存在定理与介值定理
    moco入门
    git的基础操作-入门
    有道笔记链接地址 -----关于python
    Linux学习---linux的svn的配置与安装
    Linux学习---linux系统下安装配置Jenkins
    Linux学习-linux系统下安装jdk和tomcat,以及遇到的问题清单
    Linux学习-linux系统下python升级到python3.6步骤详解,以及遇到的问题解决
    Linux学习---Linux目录结构说明
    电脑屏幕左上角出现一个小的白块
  • 原文地址:https://www.cnblogs.com/Shimarin/p/13747517.html
Copyright © 2020-2023  润新知