专栏文章

别样的Parity大战

P12465题解参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@mioy4vtk
此快照首次捕获于
2025/12/03 03:03
3 个月前
此快照最后确认于
2025/12/03 03:03
3 个月前
查看原文

别样的 Parity 大战

一天,仵沉蛋给我打来电话。他说:“你敢不敢和我举行 Parity 大战?”我豪爽地答应了:“我当然敢!周日下午在 xx 路 xx 大厦,给定长度为 nn 的二进制字符串 SSqq 次查询,每次查询对于 [l,r][l,r],要计算 x=0Sub(l,r)Pari(x)mod998244353\sum_{x=0}^{\operatorname{Sub}(l,r)}\operatorname{Pari}(x) \bmod 998244353,谁不来谁就是怂货。”
我原本以为我恐吓了仵沉蛋,仵沉蛋应该躲在家,不敢找我,便准备用 O(N)O(N) 暴力枚举计算:
CPP
long long ans = 0;
for (int x = 0; x <= N; x++) {
    ans += __builtin_popcount(x) % 2;
}
可正当这时,我听到了音乐声,一看,竟然是我的暴力枚举 TLE 了,仵沉蛋打来电话:“小废物,不会用 Pari\operatorname{Pari} 函数的性质?再不会你的锣鼓账号就要被我机惨了!”我回击道:“我要用 S(N)=(1)popcount(x)S(N)=∑(-1)^{popcount(x)} 的公式和预处理,把你 hack 掉,你说好不好啊。”
他吓得没再回应我,可是到了周日,仵沉蛋竟然又给我打电话了,他还真要和我举行 Parity 大战,于是我按照约定,到达了 xx 大厦,可他已经等我很久了。
第一回合,我占上风,利用公式 f(N)=(N+1)S(N)2f(N)=\frac{(N+1)-S(N)}{2} 发起进攻:
CPP
long long X = (Nv + 1) % mod;
long long ans = (X - Sv) * inv2 % mod;
仵沉蛋还在手算 Sub(l,r)\operatorname{Sub}(l,r) 的值,他比不过我,到了第六回合,他就主动认输了。
第二回合,他开始占上风,我也不甘势弱,我们僵持了一百多个回合,我轻敌地只预处理了前缀和:
CPP
Fw[0] = s[0]-'0';
for(int i=1; i<n; i++) {
    Fw[i] = (Fw[i-1]*2 + (s[i]-'0')) % mod;
}
却忽略了全零子串的边界情况:
CPP
if(nxt[l0] == -1 || nxt[l0] > r0) {
    // 全0子串处理
}
被他击败了。
从那时开始,我就不轻敌了,我认真研究他的套路,于是我总结出了一种方案:
  1. 预处理 nxtnxt 快速定位首个 11
CPP
int last = -1;
for(int i=n-1; i>=0; i--){
    if(s[i]=='1') last = i;
    nxt[i] = last;
}
  1. CwCw 统计前缀 11 的个数。
  2. pow2tpow2t 存储 2imod9982443532^i \bmod 998244353**。
第二天,我们举行第三局,他使用祖传暴力算法,对我发动猛烈的攻击,我们势均力敌,平分秋色,2×1052\times 10^5 的查询使我们比了 33 个多小时,也没分出胜负。
后来,他不知不觉的睡着了,我趁着这个好机会,一记凌车漂移,一飞冲天,精准的预处理和 O(1)O(1) 查询:
CPP
int pos = nxt[l0];
if(s[r0]=='1') Sv=0;
else {
    int m = Cw[r0] - (pos?Cw[pos-1]:0);
    Sv = (m%2)? -1:1;
}
long long Nv = (Fw[r0] - Fw[pos-1]*pow2t[len]) % mod;
long long ans = ((Nv+1 - Sv) * inv2 % mod + mod) % mod;
打的他不敢还手,对他的打击比取模时忘记处理负模数还大:
CPP
if(ans < 0) ans += mod;  // 致命一击

下面是正常版题解

题意简述

题目要求计算对于给定的二进制字符串的子串所表示的数字 NN,求解:
x=0NPari(x)mod998244353\sum_{x=0}^{N} \operatorname{Pari}(x) \bmod 998244353
其中 Pari(x)\operatorname{Pari}(x)xx 的二进制表示中 11 的个数的奇偶性(即 11 的个数模 22)。

思路

我们可以将问题进行转化:定义函数 S(N)=x=0N(1)popcount(x)S(N) = \sum_{x=0}^{N} (-1)^{\text{popcount}(x)},则所求答案可表示为:
f(N)=(N+1)S(N)2mod998244353f(N) = \frac{(N + 1) - S(N)}{2} \bmod 998244353
其中 NN 是子串表示的二进制数。
所以,我们只需思考如何求 S(N)S(N)。考虑如下计算方式:
  • N=0N = 0,则 S(0)=1S(0) = 1
  • 对于 N>0N > 0,设 aa 是满足 2aN2^a \leq N 的最大整数(即二进制表示的最高位 11 的位置),b=N2ab = N - 2^a。则:
\begin{cases} 0 & \text{当 } a = 0 \text{ (即 } N=1\text{)} \\ -S(b) & \text{其他情况} \end{cases}$$ - 设二进制子串去除前导零后为 $T$,则: - 若 $T$ 为空(即 $N = 0$),$S(N) = 1$。 - 若 $T$ 的最后一个字符是 $1$,则 $S(N) = 0$。 - 否则,设 $m$ 为 $T$ 中 $1$ 的个数,则 $S(N) = (-1)^m$。 然后,为了提升算法效率,考虑如下优化: - **预处理**: - `nxt[i]`:从位置 $i$ 开始向右的第一个 $1$ 的位置(用于快速定位子串中第一个 $1$)。 - `Fw[i]`:前缀子串 $s[0..i]$ 表示的二进制数的值(取模后)。 - `Cw[i]`:前缀子串 $s[0..i]$ 中 $1$ 的个数。 - `pow2t[i]`:预处理 $2^i$ 取模后的值。 - **查询**: 1. 定位子串中第一个 $1$ 的位置 $pos$。 2. 若不存在 $1$,则 $N = 0$,直接 `return 0`。 3. 否则,根据子串 $s[pos..r]$ 的最后一个字符和其中 $1$ 的个数计算 $S(N)$。 4. 计算子串表示的二进制数 $N$(利用前缀和与快速幂)。 5. 最后,代入公式计算答案即可。 ### Code ```cpp #include <iostream> #include <cstring> using namespace std; const int MAXN = 200000; // 最大字符串长度 const int mod = 998244353; // 模数 const long long inv2 = (mod + 1) / 2; // 2的逆元,用于除法 int n, q; // 字符串长度和查询数 char s[MAXN + 10]; // 存储输入的二进制字符串(下标0开始) int nxt[MAXN + 10]; // nxt[i]: 从位置i开始向右的第一个'1'的位置,没有则为-1 long long Fw[MAXN + 10]; // Fw[i]: 前缀子串s[0..i]的二进制值取 mod int Cw[MAXN + 10]; // Cw[i]: 前缀子串s[0..i]中'1'的个数 long long pow2t[MAXN + 10]; // pow2t[i]: 2^i 取 mod int main() { cin >> n >> q; cin >> s; // 预处理幂表:计算2^i mod mod (0 <= i <= MAXN) pow2t[0] = 1; for (int i = 1; i <= MAXN; i++) { pow2t[i] = (pow2t[i - 1] * 2) % mod; } // 预处理nxt:从右向左扫描,记录每个位置开始向右的第一个'1' int last = -1; // 记录最近遇到的'1'的位置 for (int i = n - 1; i >= 0; i--) { if (s[i] == '1') { last = i; // 更新最近'1'的位置 } nxt[i] = last; // 记录当前位置开始的第一个'1' } // 预处理前缀和数组Fw和Cw if (n > 0) { // 初始化第一个字符 Fw[0] = s[0] - '0'; // 数值 Cw[0] = (s[0] == '1') ? 1 : 0; // '1'的个数 // 计算后续位置 for (int i = 1; i < n; i++) { // Fw[i] = 前i-1位的值左移1位 + 当前位的值 Fw[i] = (Fw[i - 1] * 2 + (s[i] - '0')) % mod; // Cw[i] = 前i-1位的'1'个数 + 当前位是否为'1' Cw[i] = Cw[i - 1] + ((s[i] == '1') ? 1 : 0); } } // 处理每个查询 while (q--) { int l, r; cin >> l >> r; // 转换为0-indexed下标 int l0 = l - 1; int r0 = r - 1; // 情况1:子串中没有'1'(全0) if (nxt[l0] == -1 || nxt[l0] > r0) { // N = 0, S(0)=1 long long X = 1; // X = N + 1 = 1 long long Sv = 1; // S(0)=1 // f(N) = (X - Sv) / 2 long long ans = (X - Sv) % mod; if (ans < 0) ans += mod; // 处理负数(仵沉蛋就是这一步忘记了!) ans = ans * inv2 % mod; // 乘以逆元 cout << ans << endl; } // 情况2:子串中有'1' else { int pos = nxt[l0]; // 第一个'1'的位置(0-indexed) long long Sv; // 存储S(N)的值 // 判断子串最后一个字符(决定N的奇偶性) if (s[r0] == '1') { // N为奇数,S(N)=0 Sv = 0; } else { // N为偶数,计算有效子串中'1'的个数m int m; if (pos == 0) { // 有效子串从0开始,直接取前缀 m = Cw[r0]; } else { // 有效子串从pos开始,计算Cw[r0] - Cw[pos-1] m = Cw[r0] - Cw[pos - 1]; } // S(N) = (-1)^m if (m % 2 == 0) { Sv = 1; } else { Sv = -1; } } // 计算有效子串表示的数值N long long Nv; int len = r0 - pos + 1; // 有效子串长度 if (pos == 0) { // 有效子串从0开始,直接取前缀值 Nv = Fw[r0]; } else { // 计算:子串值 = 整个前缀值 - 前pos-1部分左移len位的值 Nv = (Fw[r0] - Fw[pos - 1] * pow2t[len]) % mod; if (Nv < 0) Nv += mod; // 处理负数(仵沉蛋就是这一步忘记了!) } // 计算答案 f(N) = ((N+1) - S(N)) / 2 long long X = (Nv + 1) % mod; // X = N+1 long long temp = (X - Sv) % mod; // temp = (N+1) - S(N) if (temp < 0) temp += mod; // 处理负数(仵沉蛋就是这一步忘记了!) long long ans = temp * inv2 % mod; // 乘以逆元 cout << ans << endl; } } return 0; } ``` 其中查询时间复杂度为 $O(1)$,总时间复杂度 $O(n + q)$。

评论

0 条评论,欢迎与作者交流。

正在加载评论...