A. A Bit Common
确认情况的划分方法
基于与的性质,若要使得存在一个子序列的按位与为1,而1为00...001,最低位为1,其余位为0。那么:
- 对于
低1位
,至少要有一个1。否则全是0,无论怎么选取子序列,都不可能让最终结果为1。 - 对于
其它位
至少要有一个0。否则全是1,无论怎么选取子序列,都不可能让最终结果为0。
但是这样子考虑只能跑暴力枚举作为校验条件使用,而题目并不要求我们求出每一种情况的具体内容,只需要计次就行,显然是要求我们推导出简便的数学公式。
所以说需要细化我们的结论,不然不好推公式。
不妨考虑符合条件的子序列内部的情况:
低1位
必须全部是1- 至少要有一个数在第
位为0,其中
指代任意的
非低1位
的位
而对于子序列的外部,每一位的排布都可以是任意的
但是这样子很难不重复考虑,因为存在这种情况:
一行是一个数的二进制形式,低位在右边
0 0 0 1
1 1 1 1
既可以把两个数都看出子序列内部,也可以把其中一个数看成子序列内部,另一个数看成子序列外部。说明这样子划分无法特异区分出这两种不同的情况,显然是不行的。
我们注意到低1位
较为特殊,所以说可以考虑以低1位
为依据划分子序列与非子序列。
如果已有一个子序列,那么新增一个任意的最低位为1的数并不会破坏已有的子序列,所以最低位为1的数都可以看成子序列内部的元素。
推导公式
如上图,我们有个数(图中
),
个二进制位(图中
)。
记子序列部分的长度为,可以发现
。当
时,子序列内部的数为1即可。当
时,只需要满足结论1即可。
我们开始考虑更一般的情况:
- 在子序列内部讨论:单独看某一个
非低1位
的一列,里面至少有一个0。那么考虑0的个数为1的情况
+
0个数为2的情况
+ ... +
0个数为k的情况
,即
。实际上,就是随机排布的情况
减一,也就是除了全是1的情况皆可。而对于整个子序列来说,
非低1位
一共有个,这些是同时发生的,所以说用的是乘法。这部分总的情况数为
。
- 讨论非子序列的部分:根据前面的划分规则,非子序列为
低1位
为0的情况,剩余所有位都没有限制,所以说对于每一个非低1位
,有种情况。
非低1位
有个,所以说一共的情况数为:
。
- 子序列可以和非子序列混合,从n个数里面选取出k个数为子序列成员,有
种情况。之所以不使用排列,是因为子序列内部用的公式为
,实际上已经包含了排列,如果这里再用排列就重复了。
这三种情况是同时发生的,所以说乘起来就是最终的公式:
最后我们只需要用个for取遍然后累加即可:
组合数用杨辉三角递推。
虽然理论上可直接拆分为阶乘,但是在模意义下除法得用乘法逆元,需要确保要求逆元的数和q互质,而本题的q不保证是质数!所以说会错!
AC代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e3 + 10;
// 杨辉三角求组合数
int comb[N][N];
// 模q意义下
int q;
// 预处理组合数,5000*5000=2e7不会T
void init() {
int onemod = 1 % q;
comb[0][0] = comb[1][0] = comb[1][1] = onemod;
for (int i = 2; i <= 5000; i++) {
comb[i][0] = onemod;
comb[i][i] = onemod;
for (int j = 1; j < i; j++) {
comb[i][j] = (comb[i - 1][j] + comb[i - 1][j - 1]) % q;
}
}
}
// 模q快速幂
int qpow(int a, int b) {
int ans = 1ll;
for (; b; b >>= 1ll) {
if (b & 1ll) {
ans = (ans*a) % q;
}
a = (a*a) % q;
}
return ans;
}
signed main() {
int n, m;
cin >> n >> m >> q;
init();
int ans = 0;
// 执行公式
for (int k = 1; k <= n; k++) {
// ((2^k)-1)^(m-1)
int t = qpow(qpow(2ll, k) - 1ll, m - 1ll);
// 2^((n-k)*(m-1))
t = (t*qpow(2ll, (n - k) * (m - 1ll))) % q;
// C(n,k)
t = (t * comb[n][k]) % q;
// 累加
ans = (ans + t) % q;
}
cout << ans;
return 0;
}