XOR Sum

XOR Sum

组合数学 + 数位dp + 二进制运算(进位加法)

题目大意

给定 \(n, m, k\),求有多少个长度为 \(k\) 的非负整数序列 \(A = [a_1, a_2, \dots, a_k]\),满足:

  1. \(0 \le a_i \le m\)(\(1 \le i \le k\))
  2. 序列的“价值”等于 \(n\),其中价值的定义为 \(\sum_{i=1}^k \sum_{j=1}^{i-1} (a_i \oplus a_j)\)。

答案对 \(10^9 + 7\) 取模。

数据范围:\(n \le 10^{15}\),\(m \le 10^{12}\),\(k \le 18\)。

核心思路:按位DP(数位DP)

由于 \(n\) 和 \(m\) 非常大,但 \(k\) 非常小(\(k \le 18\)),我们需要从二进制位的角度来考虑问题。
对于异或运算,每一位是独立的。如果我们知道了在第 \(b\) 位上,\(k\) 个数字中有 \(c\) 个填了 \(1\),那么这一位产生的异或对数量就是 \(c \times (k - c)\)。

因此,我们可以从高位到低位枚举每一位,同时处理两个限制:

  1. 限制 \(m\):用状态 tight 表示当前有多少个数字的前缀完全等于 \(m\) 的前缀(即“贴着上界”)。
  2. 限制 \(n\):用状态 g 表示目标值 \(n\) 与当前异或和在高位上的差值。

状态设计

定义 \(dp[pos][tight][g]\):

  • pos:当前枚举到的二进制位(从第59位向第0位遍历)。
  • tight:当前还有多少个数字的前缀等于 \(m\) 的前缀。初始状态为 \(k\)。剩下的 \(k - tight\) 个数字在更高位已经小于 \(m\),当前位可以随便填 \(0\) 或 \(1\)。
  • g:目标值 \(n\) 的高位部分,减去已经确定的异或和高位部分,剩余的差值在当前位的缩放结果。

状态转移

在 dfs(pos, tight, g) 中,我们枚举当前位填 \(1\) 的个数:

  • 设 up = ((m >> pos) & 1) ? tight : 0。
    • 如果 \(m\) 的当前位是 \(1\),贴着上界的 tight 个数字可以填 \(0\)(解除限制)或 \(1\)(继续贴边)。
    • 如果 \(m\) 的当前位是 \(0\),贴着上界的数字只能填 \(0\),继续贴边。
  • 外层循环 i:从贴着上界的 tight 个数字中,选 i 个填 \(1\)。方案数为 \(C(tight, i)\)。
  • 内层循环 j:从已经解除限制的 k - tight 个数字中,选 j 个填 \(1\)。方案数为 \(C(k - tight, j)\)。

1. 计算当前位的异或和贡献

当前位总共填了 \(sum = i + j\) 个 \(1\),剩下的 \(k - sum\) 个数字填 \(0\)。
这一位产生的异或对数量为:

\[v = sum \times (k - sum) \]

2. 更新差值 g 与 n 的关系

我们要判断最终的异或和是否等于 \(n\)。这等价于判断 \(n - \text{异或和} = 0\)。
我们从高位向低位做二进制竖式减法,g 记录的就是高位的剩余差值。
当从 \(pos+1\) 位走到 \(pos\) 位时,高位的差值权重降低,需要乘以 \(2\)(相当于左移一位)。更新公式为:

\[ng = g \times 2 + ((n >> pos) \& 1) - v \]

  • g * 2:高位差值退位到当前位,权重乘 \(2\)。
  • (n >> pos & 1):加上 \(n\) 在当前位的值。
  • - v:减去当前位异或和产生的值。

剪枝:

  • 如果 \(ng < 0\),说明当前异或和已经大于 \(n\),后面无法弥补,直接剪枝。
  • 如果 \(ng > lim\),说明 \(n\) 比异或和大了太多,后面全填 \(1\) 也追不上,直接剪枝。\(lim\) 的最大值就是 \(v\) 的最大值,即 \(v_{max} = \lfloor k/2 \rfloor \times \lceil k/2 \rceil\)。

3. 更新 tight

  • 如果 \(m\) 的当前位是 \(1\),只有填 \(1\) 的 i 个数字会继续保持贴边,所以 newtight = i。
  • 如果 \(m\) 的当前位是 \(0\),所有贴边数字只能填 \(0\),继续贴边,所以 newtight = tight。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
const int N = 20;
ll fact[N], invfact[N];
ll modpow(ll a, ll b) {
    ll r = 1;
    while(b) {
        if(b & 1) r = r * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return r;
}
void init() {
    fact[0] = 1;
    for(int i = 1; i < N; i ++) fact[i] = fact[i - 1] * i % MOD;
    invfact[N-1] = modpow(fact[N-1], MOD - 2);
    for(int i = N - 2; i >= 0; i --) invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}
ll C(int a, int b) {
    if(b < 0 || b > a) return 0;
    return fact[a] * invfact[b] % MOD * invfact[a - b] % MOD;
}
ll n, m; int k, lim;
ll dp[60][20][100];
//pos表示当前位,tight表示多少m达到上限,g表示上层n的余量在当前位需要多少补足
ll dfs(int pos, int tight, int g) {
    if(pos < 0) return g == 0;
    if(dp[pos][tight][g] != -1) return dp[pos][tight][g];

    int up = ((m >> pos) & 1) ? tight : 0;
    ll res = 0;
    for(int i = 0; i <= up; i ++) {
        for(int j = 0; j <= k - tight; j ++) {
            int sum = i + j;
            int v = (k - sum) * sum;
            int ng = g * 2 + ((n >> pos) & 1) - v;
            if(ng < 0 || ng > lim) continue;

            int newtight = ((m >> pos) & 1) ? i : tight;
            res +=  C(tight, i) * C(k - tight, j) % MOD * dfs(pos - 1, newtight, ng);
            res %= MOD;
        }
    }
    dp[pos][tight][g] = res;
    return res;
}

void solve() {
    cin >> n >> m >> k;
    int d = k / 2, u = (k + 1) / 2;
    lim = d * u;
    fill(&dp[0][0][0], &dp[0][0][0] + sizeof(dp) / sizeof(ll), -1LL);
    cout << dfs(59, k, 0) << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    init();
    int _ = 1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-13 14:41  thenrify  阅读(8)  评论(0)    收藏  举报