洛谷·bzoj ·花神的数论题

初见安~这里是两个传送门:洛谷P4317 & bzoj P3209

Description

背景
众所周知,花神多年来凭借无边的神力狂虐各大 OJ、OI、CF、TC …… 当然也包括 CH 啦。
描述
话说花神这天又来讲课了。课后照例有超级难的神题啦…… 我等蒟蒻又遭殃了。
花神的题目是这样的
设 sum(i) 表示 i 的二进制表示中 1 的个数。给出一个正整数 N ,花神要问你
派(Sum(i)),也就是 sum(1)—sum(N) 的乘积。

Input

一个正整数 N。

Output

一个数,答案模 10000007 的值。

Sample Input

3

Sample Output

2

Hint

对于样例一,1*1*2=2;

数据范围与约定

对于 100% 的数据,N≤10^15

Sol

对于我这种数位dp入门级别的选手来说这神仙就是个毒瘤题!!!!各大题解的思路我都很明白,但是具体的代码就是有些地方看不懂,也不给个注释QwQ……最后卡了一整天在牙牙姐的帮助下才终于搞明白了【实名感谢!!!】

进入正题。

题目意思很明显——求出1~n每个数的二进制下1的个数的乘积。如果我们按照1~n的固定思路来思考,那么除了暴力累乘你也没有别的出路了。如果打乱了来看呢?有没有发现只要是位数<n的数,按位数放在一起就是一个组合数?那我们就按位数的思路来思考吧【本来就是个数位dp你不考虑位数你咋整!!】我们设f[ i ] [ j ] 为以0开头的一个i位数有j个1的情况数量,g[ i ] [ j ]为保证第一位为1的情况下i位数有j个1的情况数量。很明显我们可以根据定义得到一个递推式:f[i][j] =f[i - 1][j] +g[i - 1][j]【加进来的数可以是0】这里其实f和g的定义都差不多,推导方式也都比较灵活,甚至可以只开f,开两个是为了某些细节处理方便。同时g也可以得出一个递推式:g[i][j] = f[i - 1][j - 1] + g[i - 1][j - 1]

然后我们要考虑从高位到低位地利用f。从低位开始可能不太好处理大小限制。【其实只有0和1的话还是很好处理的】处理出每一段可以得到多少种情况,然后累乘快速幂就行了【这是最大的思路,下面详解】

我们举个例子:1100101这个数【已经是二进制状态】

如果最高位为0,那么后面的6位就可以是一个在6个空位里放1~6个1的排列组合,得出所有满足要求的6位数。那么如果最高位取1呢?是不是就很明显受到后面的数的限制了很麻烦?那我们就从后面的数的角度来处理这个1就好。什么意思——我们在最开始处理这个7位数时,是假设当前位——第七位——为0,就可以合法快速幂出后面的6位的全排列【0111111 < 1100101】。如果我们继续往后走,到第六位,明显我们再直接假设这一位是0就会重复一些前面7位时枚举过的情况,所以前面的第七位我们要固定为1了。这样一来就是形如10……,有5位我们在枚举情况,但是最多可以有6个1,因为第七位的1已经固定,随意的是后面的几个。同理往后推,遇到1就处理然后固定………………

可能你有个疑问——为什么在枚举这一步中调用的是f而不是g?实质上我们在枚举的时候就很明显——首先开头的那个数是一定可以为0的。其次,f其实虽然定义上是以0开头,但是我们在递推的时候到了i == j的时候还是会正常算出以1开头的全是1的数的情况,也因为递推的时候一定不会用到这个末端的情况所以完全不影响。所以f其实是很灵活的,也正好适合这个灵活的枚举过程。

真的是巧妙极了呢。下面是代码及详解——

#include<bits/stdc++.h>
#define ll long long
#define int ll//这个纯属是我不知道哪里错了所以乱搞。
using namespace std;
ll f[100][100], g[100][100];// f 0 g 1
ll n, p = 10000007;
ll pow(ll a, ll b)
{
    ll ans = 1;
    while(b)
    {
        if(b & 1) ans = ans * a % p;
        a = a * a % p; b >>= 1;
    }
    return ans;
}

ll cal(ll x)
{
    ll t = 0, c = 0, ans = 1;//t为剩下的可处理位数,c为已经固定了的1的数量
    while(1ll << t <= x) t++;//统计x除了最高位的位数
    for( ; t; t--)
    {
        if((1ll << t - 1) & x)//x的第t位上为1,就可以拆出来
        {
            for(int i = 1; i <= t; i++)
                ans = ans * pow(i + c, f[t][i]) % p;//有i + c个1,但是真正可以活动的只有i个
            if(c) ans = ans * c % p;//十分巧妙的细节处理,较难理解,其实算的时候最高位一直是0
            c++;//这一位为1,c要累加
        }
    }
    return ans * c % p;
}

signed main()//就是int main()
{
    scanf("%lld", &n);
    f[1][0] = 1, g[1][1] = 1;
    for(int i = 2; i <= 60; i++)
        for(int j = 0; j <= i; j++)//预处理出f和g
        {
            f[i][j] = f[i - 1][j] + g[i - 1][j];
            if(j) g[i][j] = g[i - 1][j - 1] + f[i - 1][j - 1];
        }
    printf("%lld", cal(n));
    return 0;
}

真的是个毒瘤题啊。

迎评:)
——End——

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值