题解 P1869 【愚蠢的组合数】
JustinRochester · · 题解
题目
要做就要做到最好——沃·兹基硕德
为了带给大家最好的题解体验,本蒟蒻重新做了该题
以前的分析我就不改了,各位跳到第一条代码下面即可
【分析】
法一
这题其实可以暴力,优雅一点就直接过了。本蒟蒻就先根据题目,打了个表。如图1所示。(真·打表)
根据图1,我们可以很清晰地看到该图是由复制得来的。于是,我们的题目就转变为,给定坐标(n,k),求该点在复制得来的图中是否是蓝色。是,则输出
我们可以将其中的四个小单元格合并为一个单元。则得到结论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;
}
复杂度
以下代码为本蒟蒻直接手打的。
如有错误,请在评论区直接公布或者与本蒟蒻联系。
如对众位的阅读体验感产生不良影响,本蒟蒻先再次表示歉意。
【分析】
法二
在学习了卢卡斯定理后,本蒟蒻重新回来做了该题
首先,根据卢卡斯定理的内容(证明本蒟蒻不会......OI不需要证明......)
其中
当然,由于 C++本身除法就是向下取整的,所以我们可以直接这么理解:
而题目要求所求的
people &me=LRJ("想一想,为什么?");
好,那么根据卢卡斯定理,
当然,众所周知
即
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;
}
复杂度
法三
当然,会位运算的同学这边可以优化一下,不会的可以听我讲解一下:
首先,我上面列出的四个组合数都有一个规律:
如果懂得二进制的朋友可以想到,小于
所以说,如果我们右移一位(相当于除以
同时,对
因此,我们可以这么写,看着比较优雅 装逼
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
}
复杂度
法四
那么,接下来,有热情的小伙伴们还可能把
为什么要讨论它呢?显然,
-
n\&1==1$ ,那么,不论 $k\&1$ 为什么值,$C_{n\&1}^{k\&1}=C_1^{k\&1}=1 -
n\&1==0,k\&1==0$,那么 $C_{n\&1}^{k\&1}=C_0^0=1 -
n\&1==0,k\&1==1$,那么 $C_{n\&1}^{k\&1}=C_0^1=0
所以说
int c(int n,int k){
return ( (!(n&1))&(k&1) )?0:c(n>>1,k>>1);
}
当然,你可以去试试,这个代码是错的......
因为我们少了个递归边界:
当
因此,我们还要加个特判:
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);
}
复杂度
法五
有的小伙伴还中意于非递归式写法,思路和上面大体一样:
当两者都不为
int c(int n,int k){
while(n|k)
if( (!(n&1))&(k&1) ) return 0;
else n>>=1,k>>=1
return 1;
}
复杂度还是
法六
现在,众位已经很接近最优解法了
仔细看一下上面的程序:
如果用
那么,循环的条件显然是对于当前位
这边有一个比较神奇的写法:
假设有同学懂得集合的话可以这么理解:
有个集合是
而循环的条件相当于
即
根据集合的性质,易得
用位运算来描述即是
这一步即接下来的每一步都可以代入值想想,都要先想通来再往下看
那么,循环是不是表示说我从后往前枚举每一位,如果
也就是说:如果存在
如果说
所以说如果
而对于
首先,
其次,我们如果把
所以,循环会一直进行,当
也就是说,此时
因此,跳出循环,返回
综上,答案就是:
或者可以直接把该函数打成
bool c(int n,int k){ return n&k==k; }
因此,对于每次询问都是
至此,对于
还有小伙伴有疑问: 万一我考试的时候想不到怎么办?
不用方,这边再介绍最后一种方法:
如果我们知道
如果因数
同时,我们还知道
所以说,如果我们用
那么,显然有
如果说我们知道右边三个数的答案,对于
那么我们还可以知道什么呢?
首先
当且仅当
当
有看我上面的小伙伴还能想到位运算:当该数
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]++;
}
}
这样就可以实现
由于
这边给出预处理复杂度的证明(昨晚想了一宿):
对于每一个数,都会进行一次将上一个值赋值过来的操作,该操作进行
对于
对于
对于
因此,总操作数为:
因此,预处理复杂度为
觉得写得不错的麻烦点个赞吧......
写了一整节晚自习呢
最后安利一下 本蒟蒻的博客