1 条题解

  • 0
    @ 2025-8-24 21:31:16

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar JustinRochester
    **

    搬运于2025-08-24 21:31:16,当前版本为作者最后更新于2018-02-14 16:13:50,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    题目

    要做就要做到最好——沃·兹基硕德

    为了带给大家最好的题解体验,本蒟蒻重新做了该题 1111 遍,接下来的内容个人感觉应该会比较适合各位食用

    以前的分析我就不改了,各位跳到第一条代码下面即可


    【分析】

    法一

    这题其实可以暴力,优雅一点就直接过了。本蒟蒻就先根据题目,打了个表。如图1所示。(真·打表)

    根据图1,我们可以很清晰地看到该图是由复制得来的。于是,我们的题目就转变为,给定坐标(n,k),求该点在复制得来的图中是否是蓝色。是,则输出 11 ;否,则输出 22

    我们可以将其中的四个小单元格合并为一个单元。则得到结论1:单元的右上角为偶数(如果存在的话)。

    又如图而得,单元的右上角满足n为偶数且k为奇数

    故结论1:若( (!(n&1)) & (k&1) ) 则答案为0。

    接着,因为每一个单元我们都进行了判断,所以我们可以将所有的单元都视为单元格,并重新标记n、k。结果如图3所示。

    参照上面的思路,我们又可以建立新的单元,并且又能得到结论1。如图4所示。

    由图1到图3的变化得,当整个图如图5所示时,判断完毕。(图5右上方不存在)。

    整理一下思路:我们从图中得:若( (!(n&1)) & (k&1) ) 则答案为0,否则将n/2,k/2,循环至n<2为止。


    【代码】

    本蒟蒻代码奉上:

    #include<cstdio>
    #include<cctype>
    using namespace std;
    int read(){
    	int abs=0;char c=getchar();while(!isdigit(c)) c=getchar();
    	while(isdigit(c)) {abs=abs*10+c-'0';c=getchar();}
    	return abs;
    }//读入优化
    int main(){
    	int t=read();
    	while(t--){
    		int n=read(),k=read();
    		bool i=1;
    		while(n>=2&&i){
    			if((!(n&1))&(k&1)) i=0;//x&1==1则x为奇数
    			n>>=1;k>>=1;//x>>=1等效于x/=2
    		}
    		putchar(i?'1':'0');
    		putchar('\n');
    	}
    	return 0;
    }
    

    复杂度 O(Tlogn)O(T\log n)


    以下代码为本蒟蒻直接手打的。

    如有错误,请在评论区直接公布或者与本蒟蒻联系。

    如对众位的阅读体验感产生不良影响,本蒟蒻先再次表示歉意。


    【分析】

    法二

    在学习了卢卡斯定理后,本蒟蒻重新回来做了该题

    首先,根据卢卡斯定理的内容(证明本蒟蒻不会......OI不需要证明......)

    $C_n^m \% p=C_{\lfloor {n \over p} \rfloor}^{ \lfloor {m \over p} \rfloor }C_{n \% p}^{m \% p} \% p$

    其中 pp 是质数, x\lfloor x \rfloor 代表实数 xx 的向下取整

    当然,由于 C++本身除法就是向下取整的,所以我们可以直接这么理解:

    Cnm%p=Cn/pm/pCn%pm%p%pC_n^m\%p=C_{n/p}^{m/p}C_{n\%p}^{m\%p}\%p

    而题目要求所求的 CnkC_n^k 的奇偶性(奇数为 11 , 偶数为 00),显然就是求 Cnk%2C_n^k \% 2 的值

    people &me=LRJ("想一想,为什么?");
    

    好,那么根据卢卡斯定理, 22 又是质数 ,我们可以直接这么计算答案:

    Cnk%2=Cn/2m/2Cn%2m%2%2C_n^k\%2=C_{n/2}^{m/2}C_{n\%2}^{m\%2}\%2

    当然,众所周知 C00=C10=C11=1,C01=0C_0^0=C_1^0=C_1^1=1,C_0^1=0

    00 个东西中选 00 个的方案数为 11 ,选 11 个为 00 (不存在该方案)

    11 个东西中选 00 个的方案和选 11 个的方案数都是 11

    int c(int n,int k){
    	if(n<=1) return (n==1)?1:((k==0)?1:0);
        return c(n/2,k/2)*c(n%2,k%2)%2;
    }
    

    复杂度 O(Tlogn)O(T\log n)

    法三

    当然,会位运算的同学这边可以优化一下,不会的可以听我讲解一下:

    首先,我上面列出的四个组合数都有一个规律: n<2n<2

    如果懂得二进制的朋友可以想到,小于 22 的数,其二进制最高位应该是最后一位。

    所以说,如果我们右移一位(相当于除以 22),得到的数如果不为 00 ,就直接说明这个数字大于等于 22 ,否则反之

    同时,对 22 取余相当于按位与上 11 。因为对 22 取余在二进制上就相当于取其最后一位,即按位与上除最后一位全部是 00 的数—— 11

    因此,我们可以这么写,看着比较优雅 装逼

    Cnk&1=Cn&1k&1Cn>>1k>>1&1C_n^k\&1=C_{n\&1}^{k\&1}C_{n>>1}^{k>>1}\&1

    int c(int n,int k){
        if(n>>1) return c(n>>1,k>>1)*c(n&1,k&1)&1;
    	return (n&1)?1:(!(k&1));
        //k==1=>!(k&1)=!(1&1)=!1=0
        //k==0=>!(k&1)=!(1&0)=!0=1
    }
    

    复杂度 O(Tlogn)O(T\log n) ,但常数比上一种小


    法四

    那么,接下来,有热情的小伙伴们还可能把 Cn&1k&1C_{n\&1}^{k\&1} 拉出来讨论

    为什么要讨论它呢?显然, n&1n\&1k&1k\&1 各有两种情况,一共四种,比较好讨论

    1. n&1==1n\&1==1 ,那么,不论 k&1k\&1 为什么值,Cn&1k&1=C1k&1=1C_{n\&1}^{k\&1}=C_1^{k\&1}=1

    2. n&1==0,k&1==0n\&1==0,k\&1==0,那么 Cn&1k&1=C00=1C_{n\&1}^{k\&1}=C_0^0=1

    3. n&1==0,k&1==1n\&1==0,k\&1==1,那么 Cn&1k&1=C01=0C_{n\&1}^{k\&1}=C_0^1=0

    所以说 $C_n^k\&1=\begin{cases} 0,(!(n\&1))\&(k\&1)\\ C_{n>>1}^{k>>1}\&1,else\end{cases}$

    int c(int n,int k){
    	return ( (!(n&1))&(k&1) )?0:c(n>>1,k>>1);
    }
    

    当然,你可以去试试,这个代码是错的......

    因为我们少了个递归边界: C00C_0^0

    n==k==0n==k==0 时,返回 C0>>10>>1=C00C_{0>>1}^{0>>1}=C_0^0,会导致栈溢出

    因此,我们还要加个特判:

    int c(int n,int k){
    	if( !(n|k) ) return 1;
        // 当出现 c(0,0) 时,直接返回 1
        // n|k 表示 n 和 k 按位或,若两者存在不为 0 的数,则该值不为 0,取非后不为 1
    	return ( (!(n&1))&(k&1) )?0:c(n>>1,k>>1);
    }
    

    复杂度 O(Tlogn)O(T \log n) ,但常数理论上又要比上面小


    法五

    有的小伙伴还中意于非递归式写法,思路和上面大体一样:

    当两者都不为 00 时,判是否 (!(n&1)&(k&1))(!(n\&1)\&(k\&1)) 是的话就直接返回 00 了,否则 nn,kk 都右移一位,直到两者为 00 时返回 11

    int c(int n,int k){
    	while(n|k)
        	if( (!(n&1))&(k&1) ) return 0;
            else n>>=1,k>>=1
    	return 1;
    }
    

    复杂度还是 O(Tlogn)O(T \log n),但常数还能再小


    法六

    现在,众位已经很接近最优解法了

    仔细看一下上面的程序:

    如果用 xpx_p 表示 xx 在二进制下的第 pp

    那么,循环的条件显然是对于当前位 pp ,有np==1n_p==1 或者 np==kpn_p==k_p

    这边有一个比较神奇的写法: np&kp=kpn_p\&k_p=k_p

    假设有同学懂得集合的话可以这么理解:

    有个集合是 npn_p, 一个是 kpk_p

    而循环的条件相当于 Card(np)==1Card(kp)Card(n_p)==1\geq Card(k_p) 或者 Card(np)==0==Card(kp)Card(n_p)==0==Card(k_p)

    Card(np)Card(kp)Card(n_p)\geq Card(k_p) ,在本题中可以直接视为 kpnpk_p \subset n_p

    根据集合的性质,易得 kpnp=kpk_p\bigcap n_p=k_p

    用位运算来描述即是 kp&np=kpk_p\&n_p=k_p

    这一步即接下来的每一步都可以代入值想想,都要先想通来再往下看

    那么,循环是不是表示说我从后往前枚举每一位,如果 np&kp=kpn_p\&k_p=k_p 那么继续循环,否则就直接判 00

    也就是说:如果存在 np&kpkpn_p\&k_p\neq k_p ,那么,答案为 00

    如果说 kk 中存在 kpk_p 使得 np&kpkpn_p\&k_p\neq k_p ,那么,是不是说明一定有 k&nkk\&n \neq k

    所以说如果 k&nkk\&n \neq k 可以直接判 00 跳出

    而对于 k&n==kk\&n==k 的情况,我们是不是可以这么去想它:

    首先, nn 的位数一定大于等于 kk ,否则该情况肯定不成立

    其次,我们如果把 kk 的不足的位数全部补 00 ,是不是就显然有对于 kk 中的任意位置 pp 都有 np&kp=kpn_p\&k_p=k_p

    所以,循环会一直进行,当 kk 补充的位数也全部用完, nn 的全部位数也用完了

    也就是说,此时 nk==0n|k==0

    因此,跳出循环,返回 11

    综上,答案就是: (n&k==k)?1:0(n\&k==k)?1:0

    或者可以直接把该函数打成 boolbool 型的

    bool c(int n,int k){ return n&k==k; }
    

    因此,对于每次询问都是 O(1)O(1) 的,总复杂度 O(T)O(T)

    至此,对于 O(1)O(1) 计算答案的全证明过程结束


    还有小伙伴有疑问: 万一我考试的时候想不到怎么办?

    不用方,这边再介绍最后一种方法:

    如果我们知道 CnmC_n^m 中因数 22 的个数,显然可以知道它的奇偶性

    如果因数 2200 个,即为奇数,否则为偶数

    同时,我们还知道 Cnm=n!m!(nm)!C_n^m={n ! \over m!(n-m)!}

    所以说,如果我们用 twoitwo_i 表示 ii 的因数中 22 的个数

    那么,显然有 twoCnm=twon!twom!two(nm)!two_{C_n^m}=two_{n!}-two_{m!}-two_{(n-m)!}

    如果说我们知道右边三个数的答案,对于 CnmC_n^m 是奇是偶就可以 O(1)O(1) 判断了

    那么我们还可以知道什么呢?

    首先 twoi!two(i1)!two_{i!} \geq two_{(i-1)!}

    当且仅当 ii 为奇数时取等,因为 ii 不含因数 22 ,故 twoi!=two(i1)!+0two_{i!}=two_{(i-1)!}+0

    ii 为偶数时,暴力地去判断它因数中 22 的个数,即如果除以 2200 ,就是还含有因数 22 ,就计数器 +1+1, 该数除以 22

    有看我上面的小伙伴还能想到位运算:当该数 &1\&100 时,该数右移 11,且计数器 +1+1

    int two[100010]={0};//这边的 two[i] 表示的是 two[i!] 的
    void pre(){
    	for(int i=1;i<=100000;i++){
        	two[i]=two[i-1];
            int tmp=i;
            while(tmp&1) tmp>>=1,two[i]++;
        }
    }
    

    这样就可以实现 O(1)O(1) 查询了,预处理的复杂度是 O(n)O(n) 的,总复杂度是 O(T+n)O(T+n)

    由于 TTnn 同阶,可以近似地认为是常数较大的 O(T)O(T)

    这边给出预处理复杂度的证明(昨晚想了一宿):

    对于每一个数,都会进行一次将上一个值赋值过来的操作,该操作进行 nn

    对于 22 的倍数,除赋值外还需自增一次,该操作进行 n2\lfloor {n \over 2} \rfloor

    对于 44 的倍数,还需再自增一次,该操作 n4\lfloor {n \over 4} \rfloor

    对于 88 的倍数,又需再自增一次,n8\lfloor { n\over 8} \rfloor

    因此,总操作数为:

    $n+\lfloor { n\over 2} \rfloor+\lfloor { n\over 4} \rfloor+\lfloor { n\over 8} \rfloor+\lfloor { n\over 16} \rfloor \cdots$

    $\approx n+{ n\over 2}+{ n\over 4}+{ n\over 8}+{ n\over 16}\cdots$

    =2n=2n

    因此,预处理复杂度为 O(n)O(n)


    觉得写得不错的麻烦点个赞吧......

    写了一整节晚自习呢

    最后安利一下 本蒟蒻的博客

    • 1

    信息

    ID
    838
    时间
    1000ms
    内存
    125MiB
    难度
    3
    标签
    递交数
    0
    已通过
    0
    上传者