【每日一题】AcWing 5271. 易变数 | 思维 | 中等

题目内容

原题链接

给定一个二进制表示的数 s s s
定义函数 f ( x ) f(x) f(x) x x x 的二进制位中为 1 1 1 的数量。每次操作可以使得 x → f ( x ) x\rightarrow f(x) xf(x) ,问在最少操作次数下,恰好 k k k 次操作后为 1 1 1 的数有多少,答案对 1 0 9 + 7 10^9+7 109+7 取模

数据范围

  • 1 ≤ s < 2 1000 1\leq s<2^{1000} 1s<21000
  • 0 ≤ k ≤ 1000 0\leq k\leq 1000 0k1000

题解

这个数 s s s 的上限很大,该怎么考虑呢?

考虑 x x x 2 1000 − 1 2^{1000}-1 210001 时,其二进制位为 1000 1000 1000 1 1 1 f ( x ) = 1000 f(x)=1000 f(x)=1000

所以只需要暴力计算下 1000 1000 1000 以内的数到 1 1 1 的最小次数即可。

定义 d p [ x ] dp[x] dp[x] 表示 x x x 通过 f f f 函数到达 1 1 1 的最小次数。

状态转移为: d p [ x ] = d p [ f ( x ) ] + 1 dp[x]=dp[f(x)]+1 dp[x]=dp[f(x)]+1

考虑为 d p [ x ] = k − 1 dp[x]=k-1 dp[x]=k1 x x x ,那么二进制位中 1 1 1 的个数为 x x x 的数,到达 1 1 1 的最小次数恰好为 k k k

所以我们找到所有 d p [ x ] = k − 1 dp[x]=k-1 dp[x]=k1 x x x ,然后找到小于等于 s s s 的,二进制位中 1 1 1 的数量为 x x x 的所有的数。

这里考虑到数据范围,很容易联想到数位 d p dp dp

但是这里由于初始化的问题,容易超时,需要细化数位 d p dp dp 的过程。

n n n s s s 的二进制表示的长度,

即如果我们选择 i i i

  • s [ i ] = 1 s[i]=1 s[i]=1

    • 考虑这里选择的数 y y y y [ i ] = 0 y[i]=0 y[i]=0 ,则 y [ i + 1 , ⋯ s − 1 ] y[i+1,\cdots s-1] y[i+1,s1] 都可以为 1 1 1 ,即有 s − i − 1 s-i-1 si1 个位置中,选择若干个位置为 1 1 1 ,这个若干是由剩余可选位置来决定的。
      我们需要有 x x x 1 1 1 ,而 s [ 0 , i ] s[0,i] s[0,i] 中有 m m m 个为 1 1 1 ,那么我们可以选择的就是 m − x m-x mx 位。即 C ( s − i − 1 , m − x ) C(s-i-1,m-x) C(si1,mx)
    • 考虑这里选择的数 y y y y [ i ] = 1 y[i]=1 y[i]=1 ,则后面只能选择 m − x − 1 m-x-1 mx1 1 1 1 了。但是这样直接考虑会超 s s s 的上限,所以将这个问题继续往后抛。直到最后如果 s s s 的二进制位为 1 1 1 的数量恰好为 x x x ,则答案 + 1 。
  • s [ i ] = 0 s[i]=0 s[i]=0 ,则不考虑,因为没有选择, y [ i ] y[i] y[i] 只能为 0 0 0

上述的整体过程,其实就是数位 d p dp dp 的过程。

时间复杂度: O ( n 2 ) O(n^2) O(n2)

代码

#include 
using namespace std;

const int MOD = 1e9 + 7;
int qp(int a, int b) {
    int res = 1;
    while (b > 0) {
        if (b & 1) res = 1ll * res * a % MOD;
        a = 1ll * a * a % MOD;
        b >>= 1;
    }
    return res;
}

int main()
{
    string s;
    int k;
    
    cin >> s >> k;
    
    int n = int(s.size());
    
    if (k == 0) {
        cout << "1\n";
        return 0;
    }
    
    if (k == 1) {
        cout << n - 1 << "\n";
        return 0;
    }
    
    vector<int> fac(n + 1), ifac(n + 1);
    fac[0] = 1;
    for (int i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % MOD;
    ifac[n] = qp(fac[n], MOD - 2);
    for (int i = n - 1; i >= 0; --i) ifac[i] = 1ll * ifac[i + 1] * (i + 1) % MOD;
    
    auto C = [&](int a, int b) -> int {
        if (b < 0 || a < b) return 0;
        return int(1ll * fac[a] * ifac[b] % MOD * ifac[a - b] % MOD);
    };
    
    int ans = 0;
    
    vector<int> dp(n + 1);
    dp[1] = 0;
    for (int i = 2; i <= n; ++i) {
        int x = i;
        int cnt = 0;
        while (x > 0) {
            cnt += 1;
            x -= x & -x;
        }
        dp[i] = dp[cnt] + 1;
        
        if (dp[i] + 1 == k) {
            
            // 选择 <= s 的,二进制位个数为 i 的数
            // 当前第 j 位为 0 ,还剩 n - j - 1 位,要选择这么些个 1
            int res = 0;
            int one = 0;
            for (int j = 0; j < n; ++j) {
                if (s[j] == '1') {
                    // 当前第 j 位有两种选择
                    // 第 j 位为 0
                    // 剩余 n - j - 1 位都可以为 1,前面已经占据了 one 个 1,后面再选择 k - one 个即可
                    res = (res + C(n - j - 1, i - one)) % MOD;
                    // 第 j 位为 1,如果 j 不是最后一位,即 j + 1 < n ,那么让 one += 1 ,交由后面的位继续考虑
                    // 如果 j 是最后一位,那么考虑 one + 1 是否等于 k 即可
                    
                    // 对于后面的位来说 one 增加了
                    one += 1;
                }
                if (j + 1 == n && one == i) {
                    res = (res + 1) % MOD;
                }
            }
            ans = (ans + res) % MOD;
        }
    }
    
    cout << ans << "\n";
    return 0;
}

你可能感兴趣的:(算法竞赛,算法)