数学

牛客 E 题:门牌号码查询

(格雷码)

一、问题重述

给定 ( k ),共有 ( 2^k ) 个位置,位置编号 ( x ) 从 1 到 ( 2^k )。第 ( x ) 个位置的门牌号 ( c_x ) 按如下规则生成:

  1. 令 ( n = x - 1 )
  2. 计算中间数:( \text{mid} = n \oplus \lfloor n/2 \rfloor )
  3. mid 写成 ( k ) 位二进制(不足补前导 0),然后完全反转位序,得到十进制数 ( c_x )

有 ( q ) 次询问,每次给出 ( (l, r, h, z) ),统计区间 ( [l, r] ) 内有多少个位置 ( x ) 满足:

[
c_x \bmod 2^h = z
]


二、核心观察:格雷码登场

公式 ( n \oplus (n \gg 1) ) 是经典的二进制反射格雷码定义:

[
G(n) = n \oplus (n \gg 1)
]

所以:

[
c_x = \text{rev}_k(G(n))
]

其中 ( \text{rev}_k ) 表示将 ( k ) 位二进制完全反转(MSB ↔ LSB)。


三、化简条件 ( c_x \bmod 2^h = z )

3.1 用二进制位展开

设 ( n ) 的 ( k ) 位二进制表示为:

[
n = \overline{n_{k-1} n_{k-2} \cdots n_1 n_0}_{(2)}
]

其中 ( n_{k-1} ) 是最高位(MSB),( n_0 ) 是最低位(LSB)。

格雷码 ( G(n) ) 的第 ( i ) 位(从 0 开始)为:

[
g_i = n_i \oplus n_{i+1}
]

特别地,最高位 ( g_{k-1} = n_{k-1} \oplus 0 = n_{k-1} )。

3.2 取模 ( 2^h ) 的含义

( c_x \bmod 2^h ) 取的是 ( c_x ) 的低 ( h ) 位(第 0 到第 ( h-1 ) 位)。

由于 ( c_x ) 是 ( G(n) ) 的位反转,所以:

  • ( c_x ) 的第 0 位(LSB)= ( G(n) ) 的第 ( k-1 ) 位 = ( n_{k-1} )
  • ( c_x ) 的第 1 位 = ( G(n) ) 的第 ( k-2 ) 位 = ( n_{k-2} \oplus n_{k-1} )
  • ( c_x ) 的第 2 位 = ( G(n) ) 的第 ( k-3 ) 位 = ( n_{k-3} \oplus n_{k-2} )
  • (\cdots)
  • ( c_x ) 的第 ( h-1 ) 位 = ( G(n) ) 的第 ( k-h ) 位 = ( n_{k-h} \oplus n_{k-h+1} )

如果 ( z ) 的二进制表示为:

[
z = \overline{z_{h-1} z_{h-2} \cdots z_1 z_0}_{(2)}
]

(( z_0 ) 为 LSB),则条件 ( c_x \bmod 2^h = z ) 等价于:

[
\begin{cases}
z_0 = n_{k-1} \
z_1 = n_{k-2} \oplus n_{k-1} \
z_2 = n_{k-3} \oplus n_{k-2} \
\cdots \
z_{h-1} = n_{k-h} \oplus n_{k-h+1}
\end{cases}
]


四、引入前缀 ( p )

定义:

[
p = \left\lfloor \frac{n}{2^{k-h}} \right\rfloor
]

即取 ( n ) 的最高 ( h ) 位。设 ( p ) 的二进制表示为:

[
p = \overline{p_{h-1} p_{h-2} \cdots p_1 p_0}_{(2)}
]

则有对应关系:

[
\begin{cases}
p_{h-1} = n_{k-1} \
p_{h-2} = n_{k-2} \
\cdots \
p_0 = n_{k-h}
\end{cases}
]

代入第 3 节的方程组:

[
\begin{cases}
z_0 = p_{h-1} \
z_1 = p_{h-2} \oplus p_{h-1} \
z_2 = p_{h-3} \oplus p_{h-2} \
\cdots \
z_{h-1} = p_0 \oplus p_1
\end{cases}
]


五、揭示本质:格雷码的双射关系

定义 ( z^R ) 为 ( z ) 的位反转(( h ) 位):

[
z^R = \text{rev}_h(z)
]

那么 ( z^R ) 的二进制位从高到低为 ( z_{h-1}, z_{h-2}, \dots, z_0 )。

把第 4 节的式子反过来看,从高到低排列:

[
\begin{cases}
z_{h-1} = p_0 \oplus p_1 \
z_{h-2} = p_1 \oplus p_2 \
\cdots \
z_0 = p_{h-1} \oplus 0
\end{cases}
]

这正是二进制数 ( p ) 到格雷码 ( z^R ) 的转换公式

[
\boxed{z^R = G(p)}
]

因为格雷码是 ( h ) 位空间上的双射(一一对应),所以:

关键结论: 对于任意给定的 ( h ) 和 ( z ),存在唯一一个 ( p )(( 0 \le p < 2^h ))满足 ( G(p) = \text{rev}_h(z) )。


六、求唯一 ( p ):格雷码逆变换

已知 ( z ),求 ( p ) 的步骤:

6.1 反转 ( z ) 的位

[
z^R = \text{rev}_h(z)
]

6.2 格雷码逆变换(从高位到低位递推)

令 ( z^R ) 的位从高到低为 ( z^R_{h-1}, z^R_{h-2}, \dots, z^R_0 ),对应的 ( p ) 位从高到低为 ( p_{h-1}, p_{h-2}, \dots, p_0 )。

格雷码定义:( z^R_i = p_i \oplus p_{i+1} ),其中定义 ( p_h = 0 )。

逆推:

  • 最高位:( p_{h-1} = z^R_{h-1} )
  • 然后依次:( p_{h-2} = z^R_{h-2} \oplus p_{h-1} )
  • ( p_{h-3} = z^R_{h-3} \oplus p_{h-2} )
  • (\cdots)
  • ( p_0 = z^R_0 \oplus p_1 )

七、由 ( p ) 反推 ( x ) 的区间

根据 ( p ) 的定义:
[
p = \left\lfloor \frac{n}{2^{k-h}} \right\rfloor
]
这等价于:
[
p \cdot 2^{k-h} \le n \le (p+1) \cdot 2^{k-h} - 1
]
由于 ( n = x - 1 ),可得:
[
p \cdot 2^{k-h} + 1 \le x \le (p+1) \cdot 2^{k-h}
]

记:
[
L = p \cdot 2^{k-h} + 1,\quad R = (p+1) \cdot 2^{k-h}
]
因此,所有满足条件的 ( x ) 恰好构成一个连续整数区间 ([L, R])。


八、查询转化为区间交集

对于每次询问 ((l, r, h, z)),处理流程如下:

  1. 特殊情况 ( h = 0 )
    因为 ( 2^0 = 1 ),任意整数模 1 的余数均为 0,所以区间内全部满足。
    [
    \text{答案} = r - l + 1
    ]

  2. 一般情况 ( h > 0 )

    • 将 ( z ) 的 ( h ) 个二进制位完全反转,得到 ( z^R )。
    • 对 ( z^R ) 执行格雷码逆变换,得到唯一的 ( p )(逆变换方法见第六节)。
    • 计算目标区间 ([L, R])。
    • 最终答案即为查询区间 ([l, r]) 与目标区间 ([L, R]) 的交集长度:
      [
      \text{ans} = \max\bigl(0,; \min(r, R) - \max(l, L) + 1\bigr)
      ]

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 将长度为 bits 的格雷码 g 转换为二进制数 p(逆变换)
ll gray_to_binary(ll g, int bits) {
    ll p = 0;
    ll prev = 0;          // 更高一位的二进制值(最高位前视为 0)
    for(int i = bits - 1; i >= 0; i --) {
        int gi = (g >> i) & 1;
        int bi = gi ^ prev;   // 因为 gi = bi ^ prev,所以 bi = gi ^ prev
        if(bi) p |= (1LL << i);
        prev = bi;
    }
    return p;
}
// 将长度为 bits 的整数 x 的二进制位完全反转
ll reverse_bits(ll x, int bits) {
    ll res = 0;
    for(int i = 0; i < bits; i ++)
        if(x & (1LL << i)) res |= (1LL << (bits - 1 - i));
    return res;
}

void solve() {
    int k, q; cin >> k >> q;
    while(q --) {
        ll l, r, h, z; cin >> l >> r >> h >> z;
        // h = 0 时,任何数模 1 余 0,区间内全部满足
        if(h == 0) {
            cout << r - l + 1 << '\n';
            continue;
        }
        // 1. 将 z 的 h 个二进制位反转得到 z_rev
        ll z_rev = reverse_bits(z, (int)h);
        // 2. 由 z_rev 恢复出唯一的前缀 p(格雷码逆变换)
        ll p = gray_to_binary(z_rev, (int)h);
        // 3. 计算目标 x 区间
        ll block = 1LL << (k - (int)h);        // 2^(k-h)
        ll L = p * block + 1;
        ll R = (p + 1) * block;
        // 4. 与查询区间 [l, r] 取交集
        ll left = max(l, L);
        ll right = min(r, R);
        if(left > right) cout << 0 << '\n';
        else cout << right - left + 1 << '\n';
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int t = 1; //cin >> t;
    while(t --) solve();
    return 0;
}

Many Mod Calculation

1. 题目描述

给定整数 (N, X) 和一个长度为 (N) 的正整数序列 (A = (A_1, A_2, \dots, A_N))。

定义函数:
[
f(x) = (\cdots((x \bmod A_1) \bmod A_2) \cdots) \bmod A_N
]
即对 (x) 依次对 (A_1, A_2, \dots, A_N) 取模。

目标:求在 (1 \le x \le X) 范围内,满足 (f(x) = 0) 的整数 (x) 的个数。

输入格式

  • 第一行:一个整数 (T),表示测试用例数。
  • 每个测试用例:
    • 第一行:两个整数 (N, X)
    • 第二行:(N) 个整数 (A_1, A_2, \dots, A_N)

输出格式

对于每个测试用例,输出一行答案。

约束条件

  • (1 \le T \le 2 \times 10^5)

  • (1 \le N \le 2 \times 10^5)

  • 所有测试用例的 (N) 之和不超过 (2 \times 10^5)

  • (1 \le X \le 10^{18})

  • (1 \le A_i \le 10^{18})

  • 所有输入值均为整数。

  • 题目链接: https://codeforces.com/contest/2238/problem/E


2. 核心思路

2.1 区间表示法

将整数集合用 连续区间 ([0, \text{len})) 表示,代表整数 (0, 1, \dots, \text{len}-1)。
每个区间附带一个 出现次数 cnt,表示该区间内的每个整数都出现了 cnt 次。

初始状态:区间 ([0, X+1)),出现次数为 1,表示考虑整数 (0, 1, \dots, X),最后再减去 (x=0)(因为题目要求从 1 开始)。

2.2 取模操作分解

对区间 ([0, x))(出现 cnt 次)应用取模 (M)(当 (x > M) 时):

  • 商:(q = \left\lfloor \frac{x}{M} \right\rfloor),余数:(r = x \bmod M)
  • 分解为:
    • (q) 个完整的区间 ([0, M)),每个计数 cnt → 相当于 ([0, M)) 总计数增加 cnt * q
    • 若 (r > 0),新增区间 ([0, r)),计数 cnt

关键性质:余数区间长度 (r < \frac{x}{2}),因此每个区间最多被分解 (O(\log X)) 次。

2.3 高效维护数据结构

使用 std::map<long long, long long>

  • 键(Key):区间长度 len
  • 值(Value):该区间的出现次数 cnt
  • map 自动按键升序排序,因此可通过 rbegin() 快速获取当前最大区间长度。

优势

  • 自动合并相同长度区间(例如 mp[M] += cnt * q
  • 无需优先队列,避免重复元素堆积,提高效率。

3. 预处理:过滤无用的模数

若序列中存在 (A_i \ge A_{i-1}),则取模 (A_i) 不会改变任何数的值,因为所有数已经小于等于 (A_{i-1} \le A_i),取模不变。

因此,只需保留 严格递减 的子序列

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

void solve() {
    ll n, x; cin >> n >> x;
    vector<ll> a;
    for(int i = 0; i < n; i ++) {
        ll A; cin >> A;
        if(a.empty() || A < a.back()) a.push_back(A);
    }
    int k = a.size();
    map<ll, ll> mp; mp[x + 1] = 1;
    for(int i = 0; i < k; i ++) {
        ll A = a[i];
        while(!mp.empty() && mp.rbegin()->first > A) {
            auto it = prev(mp.end()); 
            ll len = it->first, cnt = it->second;
            mp.erase(it);
            ll q = len / A, r = len % A;
            mp[A] += q * cnt;
            if(r > 0) mp[r] += cnt;
        }
    }
    ll ans = 0;
    for(auto &p : mp) ans += p.second;
    cout << ans - 1 << endl;
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int t = 1; cin >> t;
    while(t --) solve();
    return 0;
}

Missing Number in Graph

一、问题概述

给定一个连通无向图,顶点数为 (N),边数为 (M)。共有 (N+1) 张卡片,分别写着 (0, 1, \dots, N)。

操作过程:

  1. 每个顶点放一张卡片,剩下的那一张被吃掉(设为 (e))。
  2. 每条边上写入两端顶点卡片数字的异或值 (X_i)。
  3. 给定图结构与边权 (X_i),求被吃掉的卡片数字 (e)。若无法唯一确定,输出 -1

二、核心数学推导

1. 引入变量

  • 顶点 (v) 上的真实卡片数字 为 (c_v)。
  • 被吃掉的卡片数字 为 (e)。
  • 顶点 1 上的数字 为 (x),即 (c_1 = x)。

2. 引入“相对值” (a_v)

我们不知道 (x),但可以构建一个辅助数组 (a_v),使得:

[
c_v = x \oplus a_v
]

并且令基准点:

[
a_1 = 0
]

为什么可以这么做?
对于任意边 ((u, v)),其边权为:

[
X = c_u \oplus c_v = (x \oplus a_u) \oplus (x \oplus a_v) = a_u \oplus a_v
]

我们发现,未知数 (x) 被完全消掉了!
因此,我们只需要从顶点 1 出发 BFS,利用公式:

[
a_v = a_u \oplus X
]

就可以唯一确定所有顶点的 (a_v) 值,与真实的 (x) 无关。


三、利用全局异或等式

所有卡片数字的集合是 ({0, 1, \dots, N}),而顶点上的数字集合是 ({c_1, \dots, c_N}),缺失的就是 (e)。因此有恒等式:

[
\left(\bigoplus_{i=0}^{N} i\right) \oplus e = \bigoplus_{v=1}^{N} c_v
]

其中 (\oplus) 表示按位异或运算。

将 (c_v = x \oplus a_v) 代入右边:

[
\bigoplus_{v=1}^{N} c_v = \bigoplus_{v=1}^{N} (x \oplus a_v)
]

此时根据 (N) 的奇偶性,(x) 是否会被抵消,决定了答案能否唯一确定。


四、分类讨论

情况 1:(N) 为偶数

由于有偶数个顶点,(x) 在异或和中出现了偶数次,偶数个相同的数异或结果为 (0)。
所以:

[
\bigoplus_{v=1}^{N} c_v = \bigoplus_{v=1}^{N} a_v
]

代入全局异或等式:

[
e = \left(\bigoplus_{i=0}^{N} i\right) \oplus \left(\bigoplus_{v=1}^{N} a_v\right)
]

结论:答案被唯一确定,直接套公式计算即可。


情况 2:(N) 为奇数

此时 (x) 出现奇数次,无法消去:

[
\bigoplus_{v=1}^{N} c_v = \left(\bigoplus_{v=1}^{N} a_v\right) \oplus x
]

因此:

[
e = \text{常数} \oplus x
]

如果有一组合法方案,将所有顶点卡片数字同时异或 (1)(即 (c'_v = c_v \oplus 1)):

  • 边权不变:(c'_u \oplus c'_v = (c_u \oplus 1) \oplus (c_v \oplus 1) = c_u \oplus c_v)
  • 数字集合仍是 (0 \dots N)(因为 (N) 为奇数,集合在异或 1 下封闭,如 (0 \leftrightarrow 1, 2 \leftrightarrow 3, \dots))

此时新方案的被吃数字变为 (e' = e \oplus 1 \neq e),存在两个不同答案。

结论:答案无法唯一确定,直接输出 -1


#include <bits/stdc++.h>
using namespace std;
using ll = long long;

void solve() {
    int n, m; cin >> n >> m;
    vector<vector<pair<int, int>>> grid(n + 1);
    for(int i = 0; i < m; i ++) {
        int a, b, x; cin >> a >> b >> x;
        grid[a].emplace_back(b, x);
        grid[b].emplace_back(a, x);
    }
    if(n & 1) {cout << -1 << "\n"; return;}

    vector<int> a(n + 1), vis(n + 1, 0); a[1] = 0; vis[1] = 1;
    queue<int> q; q.push(1);
    while(!q.empty()) {
        int u = q.front(); q.pop();
        for(auto& e : grid[u]) {
            int v = e.first, w = e.second;
            if(vis[v]) continue;
            a[v] = a[u] ^ w;
            vis[v] = 1;
            q.push(v);
        }
    }
    int tol = 0;
    for(int i = 1; i <= n; i ++) {
        tol ^= i; tol ^= a[i];
    }
    cout << tol << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    int t = 1; cin >> t;
    while(t --) solve();
    return 0;
}

Adjacent Sums (hard)

1. 问题重述

给定长度为 N 的序列 A 和长度为 N-1 的序列 B,所有元素均在 [0, M-1] 范围内。

每次操作可将任意 A_i 加 1。
目标:使所有相邻元素之和模 M 等于给定的 B_i,即:
[
(A_i + A_{i+1}) \bmod M = B_i
]
求最小操作次数。


2. 最终序列的数学表示

设最终得到的序列为 A',并定义:
[
s = A'_1 \bmod M
]
由于相邻约束:
[
A'i + A' \equiv B_i \pmod M
]
可以递推出,任意位置 i 的模值必须为:
[
A'i \equiv C_i + (-1)^{i+1} \cdot s \pmod M
]
其中 C 数组定义为:
[
C_1 = 0,\quad C
= B_i - C_i
]

注意C_i整数,可以是负数或大于 M,不一定在 [0, M-1] 范围内。


3. C 数组与 D 数组的含义

C 数组:基准模板

  • C 代表当 s=0(即 A'_1 ≡ 0)时,每个位置在整数域上必须达到的“理论值”。
  • 它完全由 B 序列决定,是求解的参考基准

D 数组:当前值与基准的差值

[
D_i = C_i - A_i
]

  • D_i 表示当前位置的初始值 A_i 距离基准 C_i整数差距(可正可负)。
  • s=0 时,A_i 需要增加的最小非负次数正好是:
    [
    D_i \bmod M
    ]

4. 代价函数 F(s)

对于任意选定的 s0 ≤ s < M),位置 i 需要增加的操作次数为:
[
x_i(s) = \big(D_i + (-1)^{i+1} \cdot s\big) \bmod M
]
总操作次数:
[
F(s) = \sum_{i=1}^{N} x_i(s)
]
我们需要求:
[
\min_{0 \le s < M} F(s)
]


5. 核心优化:最优 s 只需在“零点”处枚举

为什么可以缩小范围?

  • s 增加 1 时,每个 x_i(s) 要么增加 1(奇数位),要么减少 1(偶数位)。
  • 如果某个 s 使得所有 x_i(s) 都不为 0,那么向左或向右微调 s(即 s±1),要么会使总代价严格下降,要么会使总代价不变但最小项更接近 0。
  • 因此,全局最优解一定出现在某个位置的代价恰好为 0 的点上,即:
    [
    x_i(s) = 0
    ]

由此可得候选 s 集合(枚举 i):

  • i 为奇数:s ≡ -D_i (mod M)
  • i 为偶数:s ≡ D_i (mod M)

候选 s 的数量至多为 N 个。


6. 差分更新:快速计算所有候选点的 F(s)

我们不直接对每个候选点重新求和(否则是 O(N^2)),而是从 s=0 出发,用差分维护代价变化。

6.1 计算起点 F(0)

先算出所有位置的初始代价和:
[
F(0) = \sum_{i=1}^{N} (D_i \bmod M)
]

6.2 普通变化量(base

s 增加 1 时:

  • 奇数位(i 为奇):x_i 通常 +1

  • 偶数位(i 为偶):x_i 通常 -1

    所以基础净变化量为:
    [
    \text{base} = #{\text{奇数位置}} - #{\text{偶数位置}}
    =
    \begin{cases}
    1 & (N \text{ 为奇数}) \
    0 & (N \text{ 为偶数})
    \end{cases}
    ]

6.3 特殊情况:越界突变

在基础变化之外,当某个 x_i(s) 越过模 M 边界时,会产生额外修正:

  • 奇数位越界:从 M-1 变成 0,实际变化相比“+1”少了 M,所以需要 -M 修正。
    触发位置:s 使得 x_i(s) = M-1,即 s ≡ -D_i - 1 (mod M)

  • 偶数位越界:从 0 变成 M-1,实际变化相比“-1”多了 M,所以需要 +M 修正。
    触发位置:s 使得 x_i(s) = 0,即 s ≡ D_i (mod M)

我们将这些事件记录为 (触发时刻 s, 修正值)


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using i128 = __int128;

ll mod(ll x, ll m) {
    return (x % m + m) % m;
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n; ll m; cin >> n >> m;
    vector<ll> a(n + 1), b(n);
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i < n; i ++) cin >> b[i];

    //c表示当起始点s=1时,每个点应该是什么值,d表示每个点的初始变化量
    //计算过程中可能ci是负的
    vector<ll> c(n + 1), d(n + 1); c[1] = 0; d[1] = -a[1];
    i128 f = mod(d[1], m);
    for(int i = 2; i <= n; i ++) {
        c[i] = b[i - 1] - c[i - 1];
        d[i] = c[i] - a[i];
        f += mod(d[i], m); 
    }

    //步长,s增大1,基础变化量(奇+ 偶-),跳跃点:变化量=0的点,事件点:发生变化的点
    //对于奇数,跳跃点之前,变化量达到m-1,再走一步,变化量为m,此时要-m;对于偶数,达到跳跃点,变化量恰好为0,下一步-1,要+m
    //奇偶不同,事件和跳跃就发生不同步。注意,跳跃点为0的时候,奇数可以不用处理,而偶数需要,所以把奇数的事件点前移一位

    ll base = (n & 1) ? 1 : 0;
    vector<pair<ll, ll>> event; 
    vector<ll> query;
    for(int i = 1; i <= n; i ++) {
        if(i & 1) {
            ll cand = mod(-d[i], m);
            ll es = mod(-d[i]-1, m);
            event.emplace_back(es, -m);
            query.emplace_back(cand);
        }
        else {
            ll es = mod(d[i], m);
            event.emplace_back(es, m);
            query.emplace_back(es);
        }
    }
    //query.emplace_back(m - 1); event.emplace_back(m - 1, 0);
    sort(query.begin(), query.end());
    sort(event.begin(), event.end());
    query.erase(unique(query.begin(), query.end()), query.end());
    
    i128 ans = (i128)4e36;
    int cur = 0;
    i128 step = 0;
    for(auto & k : query) {
        while(cur < event.size() && event[cur].first < k) step += event[cur++].second;
        //奇数前移,而偶数的事件晚发生,所以小于k的
        i128 p = f + step + (ll)k * base;
        ans = min(p, ans);
    }
    cout << (ll)ans << endl;

    return 0;
}
posted @ 2026-08-26 10:11  thenrify  阅读(3)  评论(0)    收藏  举报