Primitive Root
Primitive Root
一、 问题的数学转化
题目要求统计满足 (g \le m) 且 (g \oplus (P-1) \equiv 1 \pmod P) 的非负整数 (g) 的数量。
我们将同余式展开,令异或结果为 (kP + 1 \ (k \ge 0))。由于异或具有可逆性((a \oplus b = c \iff a \oplus c = b)),我们得到:
[
g = (kP + 1) \oplus (P-1)
]
因此,题目转化为:求有多少个非负整数 (k),使得 (f(k) = (kP + 1) \oplus (P-1) \le m)。
二、 核心数学性质:宏观单调与局部震荡
1. 宏观单调性(整体递增)
利用异或运算的绝对值不等式性质:(a - b \le a \oplus b \le a + b)。
令 (a = kP + 1, b = P - 1),代入可得:
[
(kP + 1) - (P - 1) \le f(k) \le (kP + 1) + (P - 1)
]
即:
[
(k-1)P + 2 \le f(k) \le (k+1)P
]
这说明 (f(k)) 在宏观上是随着 (k) 的增大而逐渐增大的,大致趋势是线性的。
2. 局部震荡与“错位”范围
虽然 (f(k)) 整体递增,但由于异或的按位特性,在相邻的项之间会出现局部的“错位”或“凹陷”(即 (f(k+1) < f(k)))。
关键定理:错位范围极小(跨度不超过2)。
我们来比较 (f(k+2)) 与 (f(k)) 的大小关系:
- (f(k+2)) 的最小可能值:((k+2-1)P + 2 = (k+1)P + 2)
- (f(k)) 的最大可能值:((k+1)P)
因为 ((k+1)P + 2 > (k+1)P),所以必然有:
[
f(k+2) > f(k)
]
这个结论极其重要! 它意味着:
- 奇数序列 (f(1), f(3), f(5), \dots) 是严格递增的。
- 偶数序列 (f(0), f(2), f(4), \dots) 也是严格递增的。
- 绝对不可能出现连续两个位置比前面小的情况。如果 (f(k+1) < f(k))(发生了一次凹陷),那么 (f(k+2)) 必然“弹”回去,直接跨过 (f(k))。
三、 代码逻辑拆解
代码核心是使用 check(k) 判断 (f(k) \le m),并用二分查找寻找边界。
auto check = [&](ll k) -> bool {
return (((__int128)(p - 1) ^ ((__int128)k * p + 1)) <= (__int128)m);
};
### 1. 二分查找在震荡序列中的表现
尽管序列不是完全单调的(有局部凹陷),但你使用的二分模板:
```cpp
while(l < r) {
ll k = (l + r + 1) / 2;
if(check(k)) l = k;
else r = k - 1;
}
实际上在这个“宏观递增、局部微调”的序列中,二分最终必然会收敛到发生错位的“基准线”附近(即 l 会停在 q-1, q, q+1 之一)。
2. 补丁代码的完美自洽(四种情况展开)
非常精妙的四行代码:
ans = l;
if(check(l)) ans ++;
if(check(l + 2)) ans ++;
if(l > 0 && !check(l - 1)) ans --;
我们把 f(k) 在边界附近的 4 种真假组合列出来,看看你的代码是如何完美兜底的:
情况 1:f(q) 和 f(q+1) 都是 False
- 真假序列:... True, True,
f(q)=False,f(q+1)=False, False ... - 二分落点:二分查找会停在
l = q-1(最后一个 True)。 - 代码执行:
ans = q-1。check(l)即f(q-1)是 True,ans++变成 (q)。check(l+2)即f(q+1)是 False,不执行。check(l-1)是 True,不执行ans--。
- 最终答案:(q)。(即 (0 \sim q-1) 满足)。完美。
情况 2:f(q) 是 False,f(q+1) 是 True(局部错位/凹陷)
- 真假序列:... True, True,
f(q)=False,f(q+1)=True, False ... - 二分落点:这种非单调情况二分依然会停在
l = q-1(因为检验到 (q) 是 False 就收缩右边界了)。 - 代码执行:
ans = q-1。check(l)即f(q-1)是 True,ans++变成 (q)。check(l+2)即f(q+1)是 True,ans++变成 (q+1)。check(l-1)是 True,不执行。
- 最终答案:(q+1)。(即 (0 \sim q-1) 和 (q+1) 满足)。完美。你的
check(l+2)就是跨坑打击!
情况 3:f(q) 是 True,f(q+1) 是 False
- 真假序列:... True, True,
f(q)=True,f(q+1)=False, False ... - 二分落点:二分查找会停在
l = q(最后一个 True)。 - 代码执行:
ans = q。check(l)是 True,ans++变成 (q+1)。check(l+2)即f(q+2)是 False,不执行。check(l-1)是 True,不执行。
- 最终答案:(q+1)。(即 (0 \sim q) 满足)。完美。
情况 4:f(q) 和 f(q+1) 都是 True
- 真假序列:... True, True,
f(q)=True,f(q+1)=True, False ... - 二分落点:二分查找会停在
l = q+1。 - 代码执行:
ans = q+1。check(l)即f(q+1)是 True,ans++变成 (q+2)。check(l+2)是 False,不执行。check(l-1)即f(q)是 True,所以!check(l-1)是 False,不执行ans--。
- 最终答案:(q+2)。(即 (0 \sim q+1) 满足)。完美。
隐藏兜底情况:二分向右偏,停留在了错误位置
- 假设:
f(q-1)是 True,但f(q)是 False,f(q+1)是 False。 - 二分落点:理论上应该停在前面的 True 上,但二分由于
(l + r + 1) / 2的向上取整特性,可能恰好探到了l = q。 - 代码执行:
ans = q。check(l)即f(q)是 False,不增加。check(l+2)即f(q+2)是 False,不增加。check(l-1)即f(q-1)是 True,!check(l-1)是 False,不执行ans--。
- 最终答案:(q)。(其实这里
ans是 (q),而真实的满足条件个数也是 (q),结果依然正确)。若l落在假的位置且前一个也假,!check(l-1)为真,会触发ans--,修正右偏。
3. 特判 P = 2 的必然性
当 (P=2) 时,(P-1=1)。
原式 (g \oplus 1 \equiv 1 \pmod 2)。
翻转最低位后必须是奇数,所以 (g) 必须是偶数。
(0 \le g \le m) 中的偶数个数是 (\lfloor \frac{m}{2} \rfloor + 1)。
代码中的 ans = m / p + 1; 即 m / 2 + 1,完全正确。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
ll p, m; cin >> p >> m;
ll ans;
if(p == 2) ans = m / p + 1;
else {
auto check = [&](ll k) -> bool{
return (((__int128)(p - 1) ^ ((__int128)k * p + 1)) <= (__int128)m);
};
ll l = 0, r = m;
while(l < r) {
ll k = (l + r + 1) / 2;
//cout << l << " "<< r<< " " << ((p - 1) ^ (k * p + 1LL)) << "\n";
if(check(k)) l = k;
else r = k - 1;
}
ans = l;
if(check(l)) ans ++;
if(check(l + 2)) ans ++;
if(l > 0 && !check(l - 1)) ans --;
}
cout << ans << "\n";
}
int main() {
cin.tie(0)->ios::sync_with_stdio(false);
int _ = 1; cin >> _;
while(_ --) solve();
return 0;
}

浙公网安备 33010602011771号