数学
牛客 E 题:门牌号码查询
(格雷码)
一、问题重述
给定 ( k ),共有 ( 2^k ) 个位置,位置编号 ( x ) 从 1 到 ( 2^k )。第 ( x ) 个位置的门牌号 ( c_x ) 按如下规则生成:
- 令 ( n = x - 1 )
- 计算中间数:( \text{mid} = n \oplus \lfloor n/2 \rfloor )
- 将
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)),处理流程如下:
-
特殊情况 ( h = 0 ):
因为 ( 2^0 = 1 ),任意整数模 1 的余数均为 0,所以区间内全部满足。
[
\text{答案} = r - l + 1
] -
一般情况 ( 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})
-
所有输入值均为整数。
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
- (q) 个完整的区间 ([0, M)),每个计数
关键性质:余数区间长度 (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)。
操作过程:
- 每个顶点放一张卡片,剩下的那一张被吃掉(设为 (e))。
- 每条边上写入两端顶点卡片数字的异或值 (X_i)。
- 给定图结构与边权 (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)
对于任意选定的 s(0 ≤ 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;
}

浙公网安备 33010602011771号