I used to...|

AcidBarium

园龄:10个月粉丝:4关注:0

2024牛客多校第一场A Bit Common & A Bit More Common

A Bit Common

时间限制:3s(C++/C) / 6s
内存限制:1048576K(C++/C) / 2097152K

题目描述

Given two integers n and m, among all the sequences containing n non-negative integers less than 2m, you need to count the number of such sequences A that there exists a non-empty subsequence of A in which the bitwise AND of the integers is 1.

Note that a non-empty subsequence of a sequence A is a non-empty sequence that can be obtained by deleting zero or more elements from A and arranging the remaining elements in their original order.

Since the answer may be very large, output it modulo a positive integer q.

The bitwise AND of non-negative integers A and B, A AND B is defined as follows:

  • When A AND B is written in base two, the digit in the 2d's place (d0) is 1 if those of A and B are both 1, and 0 otherwise.

For example, we have 4 AND 6 = 4 (in base two: 100 AND 110 = 100).

Generally, the bitwise AND of k non-negative integers p1,p2,,pk is defined as (((p1 AND p2) AND p3) AND  AND pk)
and we can prove that this value does not depend on the order of p1,p2,,pk.

输入描述

The only line contains three integers n (1n5000), m (1m5000) and q (1q109).

输出描述

Output a line containing an integer, denoting the answer.

示例1

输入

2 3 998244353

输出

17

示例2

输入

5000 5000 998244353

输出

2274146

笔者翻译

给你两个整数nm,找一个长度为n的序列,序列中的每个元素(正整数)都小于2m,并且这个序列中存在一段子序列使得子序列的所有元素的按位与为1,请问这样的序列有多少种,由于答案特别大需要对q取模布拉布拉......

思路

关键词按位与小于2m
不难想到这道题中出现的数字不能简单的当成十进制的数字来看,当成二进制数来看比较合适。也就是说,每一个元素都当作一个长度为m的矩阵来看会比较合适。

存在一段子序列使得子序列所有的元素的按位与为1,假设这段子序列的长度为k,这k个数在二进制表示中的最后一位必须都是1(有的题解中会说是奇数),这样才能保证最后一位按位与之后会得到1而不是0(如果有任何一个元素的最后一个元素存在0,那么最后按位与得到的数一定是0)。我们不妨把这k个元素合在一起看成一个k行m列的矩阵,基于上述理论,这个矩阵的第m列也就是最后一列中的所有元素都为1;而对于前m-1列来说,每一列的按位与都必须为0(和最后一列的情况恰恰相反),也就是说每一列中至少有一个元素为0。每一列所有可能的情况有2k种,除去全为1的情况,前m1每一列都有(2k1)种情况,m1列就有(2k1)m1

下面的这张图或许能帮助你更好的理解

对于没有被选种的nk个元素来说,每个元素的最后一位必须是0,前m1位可以随便,故有2(nk)×(m1)种情况。把上面的答案综合在一起,同时由于从n个元素中选择k个元素的时候需要进行一次排列组合,就是说乘以 Cnk.

那么显而易见的,最后的答案就是:k=1n(Cnk(2k1)m12(nk)(m1))

具体的代码实现细节

其实式子列到这个地方,这道题的关键部分就已经结束了,剩下的都是些写代码的无关紧要的小事罢。不过我还是说一些我觉得可以优化的地方。
首先是求组合数的时候,如果每个循环都单独求一次组合数,未免太浪费时间了,比较常用的做法是在主程序开始之前就用O(n2)把所有需要的组合数算完,需要的时候O(1)查询,这样会快的很多。计算组合数的时候我们需要使用公式Cnm=Cn1m+Cn1m1.用代码具体实现的话就是

 rep(i, 0, 5004) _rep(j, 0, i) if (!j) c[i][j] = 1;
    else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
    //把C[i][0]标记为1,其他的用公式递推
    //rep是for循环但是i<,_rep是i<=,下同

式子中还有很多求幂的地方,使用快速幂来优化

ll binpow(ll a, ll b, ll m)
{
    a %= m;
    ll res = 1;
    while (b > 0)
    {
        if (b & 1)
            res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

同时,式子中出现了许多求2的次方的地方,虽然使用快速幂可以在O(log)内完成计算,但如果我们事先把2的所有次方用O(nm)跑完,最后只需要O(1)来进行查询,用代码实现就是

twoFang[0] = 1;
_rep(i, 1, 25000006)
    twoFang[i] = twoFang[i - 1] * 2 % mod;

最后只需要代入一点点算就行了

 _rep(i, 1, n)
    {
        ll cnk = c[n][i];
        ll temp = cnk * binpow((twoFang[i] - 1), m - 1, mod) % mod;
        temp = temp * twoFang[(n - i) * (m - 1)] % mod;
        ans = (ans + temp) % mod;
    }

第一问完整代码

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
#define endl '\n'
#define _rep(i, a, b) for (int i = (a); i <= (b); ++i)
#define rep(i, a, b) for (int i = (a); i < (b); ++i)
#define Forget_that return
#define Just_go 0
#define Endl endl
#define ednl endl
#define eldn endl
#define dnel endl
#define enndl endl
#define Ednl endl
#define Edml endl
#define llmax 9223372036854775807
#define intmax 2147483647

int n;
ll mod;
ll twoFang[30000007];
ll c[5005][5005];

ll binpow(ll a, ll b, ll m)
{
    a %= m;
    ll res = 1;
    while (b > 0)
    {
        if (b & 1)
            res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    ll n, m;
    cin >> n >> m;
    cin >> mod;

    rep(i, 0, 5004) _rep(j, 0, i) if (!j) c[i][j] = 1;
    else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

    twoFang[0] = 1;
    _rep(i, 1, 30000006)
        twoFang[i] = twoFang[i - 1] * 2 % mod;

    ll ans = 0;

    _rep(i, 1, n)
    {
        ll cnk = c[n][i];
        ll temp = cnk * binpow((twoFang[i] - 1), m - 1, mod) % mod;
        temp = temp * twoFang[(n - i) * (m - 1)] % mod;
        ans = (ans + temp) % mod;
    }

    cout << ans;

    Forget_that Just_go;
}

没有时间为第一问的解决感到开心,接下来到场的是第二问Orz

A Bit More Common

时间限制:3s(C++/C) / 6s
内存限制:1048576K(C++/C) / 2097152K

题目描述

Given two integers n and m, among all the sequences containing n non-negative integers less than 2m, you need to count the number of such sequences A that there exists two different non-empty subsequences of A in each of which the bitwise AND of the integers is 1.

Note that a non-empty subsequence of a sequence A is a non-empty sequence that can be obtained by deleting zero or more elements from A and arranging the remaining elements in their original order. Two subsequences are different if they are composed of different locations in the original sequence****.

Since the answer may be very large, output it modulo a positive integer q.

The bitwise AND of non-negative integers A and B, A AND B is defined as follows:

  • When A AND B is written in base two, the digit in the 2d's place (d0) is 1 if those of A and B are both 1, and 0 otherwise.

For example, we have 4 AND 6 = 4 (in base two: 100 AND 110 = 100).

Generally, the bitwise AND of k non-negative integers p1,p2,,pk is defined as
(((p1 AND p2) AND p3) AND  AND pk)
and we can prove that this value does not depend on the order of p1,p2,,pk.

示例1

输入

2 3 998244353

输出

7

示例2

输入

5000 5000 998244353

输出

530227736

笔者翻译

这一题和第一问不同的地方在于,第一问只需要找到序列中含有子序列满足题意的数列,第二问要求找到序列中存在两个子序列,使得子序列的所有元素的按位与为1,求这样的序列有多少种。一言以蔽之,第一问对于特殊的子序列的数量没有要求,第二问要求至少存在两个子序列满足题目的要求。

思路

都把第一问写完了,这第一问肯定不是白写的啊,我们不难发现,如果记序列中只含有一个子序列使得子序列的所有元素的按位与为1的序列数量为ans2的话,那么上一问的答案减去ans2就是这道题的答案。
所以,这道题的重点就成了求序列中只含有一个子序列使得子序列的所有元素的按位与为1的序列数量。

一样的,我们设该序列的这段子序列的长度为k,这段子序列的所有元素的最后一位二进制必须为1。对于前面的km1列,我们必须满足这样一条性质:每一行都有0,且每一行都存在一个0,这个0是这一列中的唯一一个0;并且每一列中都有0. 如此,假如删去任意一个元素(删除任意一行),将其他的元素按位与,都会至少在某一列中出现一个1(因为每一个都存在一个0使得这个0是这一列中唯一的一个0).那么这段子序列就是符合要求的,因为这个子序列不存在一个子序列使得其元素按位与为1.也就是说,这个子序列是这个序列中唯一一个满足题意的子序列。

特别的,当这段子序列长度为1时,由于其不存在子序列,所以需要分开来单独讨论。我们不难发现,如果,这个子序列长度为1,为了满足条件,这个子序列中的唯一一个元素只能是1.这个子序列从n个元素中选出,就是Cn1× 1

剩下的,我们只需要找到满足下面性质的矩阵数量就行了

  • km1
  • 矩阵中的元素不是1就是0
  • 每行都有0是这一列中唯一一个0
  • 每行可以有很多0
  • 每一列都必须有一个0

不难发现,这个矩阵按列进行划分,可以划分成两种列

  1. 每一列含有两个以上包括两个的0
  2. 每一列只有一个0,同时所有的这些列的0会在矩阵的所有k行中出现(矩阵的每一行都有这些0中的某些)

我们设第二种情况的列有t列,相应的,第一种情况的列有m1t

首先对于第一种情况

对于每一列来说都有2k种情况,除去一列里面全是1的情况有2k1种情况,再除去每一列种含有1个0的情况就是我们需要的答案为2k1k.由于这样的列共有m1t列,则共有(2k1k)m1t种情况。

接着对于第二种情况

直接计算会有点麻烦,读者不妨自己试一下,这里我使用大家都推荐的一种方法---动态规划
我们设满足下面条件的矩阵的数量为dpij

  • ij
  • 每一列都只有10(这些0出现在所有的i行中)
  • 每一行都有0

先说结论:dpij=i× (dpi1j1+dpij1)

解释

总体思想就是:第j列的状态可以从第j1列转化过来。
对于ij1列的满足条件的矩阵,可以在最右侧加入第j列,在第ji行中的任意一行中填一个0,其他的地方填上1就可以使得新的到的这个ij列的矩阵满足要求(i行共i种)。下面的图可能会更方便理解一些:

如图所示,将一个3×3的满足题意的矩阵按照上述方式可以得到一个3× 4的满足题意的矩阵,这个矩阵的前3列组成的矩阵就已经满足题意。
同时,有一种前3列组成的矩阵并不满足题意的情况,即从i1j1列的满足题意的矩阵转移来。
对于一个i1j1列的满足题意的矩阵,我们首先在最右侧扩充第j列,并在任意行添加第一行,在这一行的最右列填上0,其他地方填上1即可。(由于共有i行,所以添加任意一行的添法共有i种)。下面的这张图可能会更方便您理解一点

如图所示,将一个2×3的满足题意的矩阵变成了三个3×4的满足题意的矩阵,由于前3列所组成的矩阵并不满足题意,所以显然的,这种情况和上一种情况是相互独立的。

故而dpij=i× (dpi1j1+dpij1)

综上所述,共有k=2nCnkt=km1Cm1t(2kk1)m1tdpkt+Cn12(n1)(m1)
最后拿第一问的答案减去上面这个就行了。

具体的代码实现细节

首先是那个动态规划的部分

  dp[1][1] = 1;
  dp[0][0] = 1;
  _rep(i, 1, 5003) _rep(j, i, 5003)
     dp[i][j] = i * (dp[i - 1][j - 1] + dp[i][j - 1]) % mod;

当子序列长度为1时的特殊情况

 ans = (((ans - n * twoFang[(n - 1) * (m - 1)] % mod) % mod) + mod) % mod;

对于非特殊情况的求解

    _rep(k, 2, n)
    {
        ll temp = c[n][k];
        ll suffix = 0;
        ll btm = 1;
        for (int t = m - 1; t >= k; t--)
        {
            ll mark = c[m - 1][t];
            mark = mark * btm % mod;
            mark = mark * dp[k][t] % mod;
            suffix = (suffix + mark) % mod;
            btm = btm * (twoFang[k] - k - 1) % mod;
        }
        temp = temp * suffix % mod * twoFang[(n - k) * (m - 1)] % mod;
        ans = ((ans - temp) % mod + mod) % mod;
    }

这里有一点需要注意,笔者在第一次写的时候,求(2kk1)m1t的时候用的是快速幂,但是给时间复杂度增加了一个log,不出意外的超时了。解决办法就是,从m1开始算,算到k;而不是从k算到m1,每次用btm存储那个幂数,每次对btm(2kk1) .如此时间复杂度少了一个log,完美地解决了问题。

完整代码

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
#define endl '\n'
#define _rep(i, a, b) for (int i = (a); i <= (b); ++i)
#define rep(i, a, b) for (int i = (a); i < (b); ++i)
#define Forget_that return
#define Just_go 0
#define Endl endl
#define ednl endl
#define eldn endl
#define dnel endl
#define enndl endl
#define Ednl endl
#define Edml endl
#define llmax 9223372036854775807
#define intmax 2147483647

int n;
ll mod;
ll twoFang[30000007];
ll c[5005][5005];
ll dp[5005][5005];

ll binpow(ll a, ll b, ll m)
{
    a %= m;
    ll res = 1;
    while (b > 0)
    {
        if (b & 1)
            res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    ll n, m;
    cin >> n >> m;
    cin >> mod;

    rep(i, 0, 5004) _rep(j, 0, i) if (!j) c[i][j] = 1;
    else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

    twoFang[0] = 1;
    _rep(i, 1, 25000006)
        twoFang[i] = twoFang[i - 1] * 2 % mod;

    ll ans = 0;

    _rep(i, 1, n)
    {
        ll cnk = c[n][i];
        ll temp = cnk * binpow((twoFang[i] - 1), m - 1, mod) % mod;
        temp = temp * twoFang[(n - i) * (m - 1)] % mod;
        ans = (ans + temp) % mod;
    }

    ans = (((ans - n * twoFang[(n - 1) * (m - 1)] % mod) % mod) + mod) % mod;

    dp[1][1] = 1;
    dp[0][0] = 1;
    _rep(i, 1, 5003) _rep(j, i, 5003)
        dp[i][j] = i * (dp[i - 1][j - 1] + dp[i][j - 1]) % mod;

    _rep(k, 2, n)
    {
        ll temp = c[n][k];
        ll suffix = 0;
        ll btm = 1;
        for (int t = m - 1; t >= k; t--)
        {
            ll mark = c[m - 1][t];
            mark = mark * btm % mod;
            mark = mark * dp[k][t] % mod;
            suffix = (suffix + mark) % mod;
            btm = btm * (twoFang[k] - k - 1) % mod;
        }
        temp = temp * suffix % mod * twoFang[(n - k) * (m - 1)] % mod;
        ans = ((ans - temp) % mod + mod) % mod;
    }

    cout << ans;

    Forget_that Just_go;
}

总结

虽然像我这么做,时空复杂度都不小,第二问花了1700多毫秒才过。但是好歹过了。具体的优化可以根据n、m来自主分配所需要的空间和常量。

本文作者:AcidBarium

本文链接:https://www.cnblogs.com/acidbarium/p/18324582

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   AcidBarium  阅读(127)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起