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)
]

这个结论极其重要! 它意味着:

  1. 奇数序列 (f(1), f(3), f(5), \dots) 是严格递增的。
  2. 偶数序列 (f(0), f(2), f(4), \dots) 也是严格递增的。
  3. 绝对不可能出现连续两个位置比前面小的情况。如果 (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;
}
posted @ 2026-11-03 21:44  thenrify  阅读(2)  评论(0)    收藏  举报