2025CCPC郑州部分题解
個、個、個、この世で個っ端みじんに壊れた個
个、个、个、在这世上碎成一“个个”渣的“个”
比赛链接
J. Subrectangle Count
给定一个长度为 n(n ≥ 2)的整数序列 a₁, a₂, ..., aₙ,和一个长度为 m(m ≥ 2)的整数序列 b₁, b₂, ..., bₘ。我们可以构造一个 n × m 的矩阵 c,其中矩阵元素 cᵢ,ⱼ = aᵢ ⊕ bⱼ(1 ≤ i ≤ n, 1 ≤ j ≤ m),这里的 ⊕ 表示按位异或运算。
请计算有多少对 (i, j)(1 ≤ i < n, 1 ≤ j < m)满足:矩阵 c 中由 (i, j) 为左上角的 2×2 子矩形:

的形式为:

这等价于同时满足以下三个条件:
cᵢ,ⱼ₊₁ = cᵢ,ⱼ + 1cᵢ₊₁,ⱼ = cᵢ,ⱼ + 2cᵢ₊₁,ⱼ₊₁ = cᵢ,ⱼ + 3
输入
第一行包含一个整数 T(1 ≤ T ≤ 10⁵),表示测试用例的数量。
对于每个测试用例:
- 第一行包含两个用空格分隔的整数
n, m(2 ≤ n, m ≤ 2 × 10⁵),表示矩阵c的行数和列数。 - 第二行包含
n个用空格分隔的整数,表示序列a₁, a₂, …, aₙ(0 ≤ aᵢ < 2³⁰)。 - 第三行包含
m个用空格分隔的整数,表示序列b₁, b₂, …, bₘ(0 ≤ bᵢ < 2³⁰)。
保证所有测试用例的 n 之和不超过 2 × 10⁵,所有测试用例的 m 之和不超过 2 × 10⁵。
输出
对于每个测试用例,输出一行,包含一个整数,表示满足条件的 (i, j) 对数。
题解
设 \(x=c_{i,j}\),那么我们可以得到一些式子:
-
\(x \oplus x+1=c_{i,j} \oplus c_{i,j+1}=(a_i\oplus b_j) \oplus (a_{i}\oplus b_{j+1})=b_j\oplus b_{j+1}\)
-
\(x \oplus x+2=c_{i,j} \oplus c_{i+1,j}=(a_i\oplus b_j) \oplus (a_{i+1}\oplus b_{j})=a_i\oplus a_{i+1}\)
-
\(x+1 \oplus x+3=c_{i,j+1} \oplus c_{i+1,j+1}=(a_{i}\oplus b_{j+1}) \oplus (a_{i+1}\oplus b_{j+1})=a_i\oplus a_{i+1}\)
-
\(x+2 \oplus x+3=c_{i+1,j} \oplus c_{i+1,j+1}=(a_{i+1}\oplus b_j) \oplus (a_{i+1}\oplus b_{j+1})=b_j\oplus b_{j+1}\)
考虑 \(x\oplus (x+1)=(x+2)\oplus(x+3)=b_j\oplus b_{j+1}=D_b\)。
\(D_b\) 的性质
-
当 \(x\) 是偶数的时候,\((x+1)\)第一位是 \(1\) ,所以 \(x\oplus (x+1)=1=D_b\),同理 \((x+2)\) 也是偶数,可得 \((x+2)\oplus (x+3)=1=D_b\)
-
当 \(x\) 是奇数的时候,假设 \(x=011_{(2)}\),那么 \(x+1=100_{(2)},x+2=101_{(2)},x+3=110_{(2)}\),\(x\oplus (x+1)=111_{(2)}\neq (x+2)\oplus (x+3)=011_{(2)}\),易得 \(x\) 不能是奇数。
因此 \(x\) 必须是偶数,并且 \(D_b=1\)。
考虑完了关于 \(b\) 数组性质后,接下来考虑 \(a\) 数组性质。
设 \(D_a\) 表示 \(x\oplus (x+2)\)
\(D_a\) 的性质
既然 \(x=2k\),不妨设 \(D_a=x\oplus(x+2)=2k\oplus (2k+2)=2(k\oplus(k+1))\),\(k\oplus (k+1)\) 必须是形如 \(2^p-1\),所以 \(D_a=x\oplus (x+2)=2^{p+1}-2\),设 \(D_a=x\oplus (x+2)=2^v-2\),那么稍加推理可以得到 \(x\) 的形状是 \(x\equiv 2^{v-1}-2\pmod{2^v}\).

算法步骤:
- 统计所有满足 \(b_j \oplus b_{j+1} = 1\) 的下标 \(j\)。
- 遍历所有可能的 \(v \in [2, 30]\)。对于每一个 \(v\),找出所有满足 \(a_i \oplus a_{i+1} = 2^v - 2\) 的下标 \(i\)。
- 利用公式 \(x \equiv 2^{v-1}-2 \pmod{2^v}\),由于 \(x = a_i \oplus b_j\),该条件等价于:
- 使用排序配合二分查找来快速统计满足条件的 \((i, j)\) 数量。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
const int N=2e5+5;
int a[N],b[N];
inline void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>b[i];
}
vector<int> posj;
for(int j=1;j<m;j++){
if((b[j]^b[j+1])==1){
posj.push_back(j);
}
}
vector<int> aa[32];
for(int i=1;i<n;i++){
int Da=a[i]^a[i+1];
int tar=Da+2;
if(tar>=4&&(tar&(tar-1))==0){//tar是2的次方
int v=0;
while(tar>1){
tar>>=1;
v++;
}
aa[v].push_back(i);
}
}
int ans=0;
for(int v=2;v<=30;v++){
if(aa[v].empty()||posj.empty()) continue;
int mod=(1<<v);
vector<int> bmods;
for(int j:posj){
bmods.push_back(b[j]%mod);
}
sort(bmods.begin(),bmods.end());
int xmask=(1<<(v-1))-2;
for(int i:aa[v]){
int tarbmod=(a[i]%mod)^xmask;//套用公式计算b的应该的值
auto range=equal_range(bmods.begin(),bmods.end(),tarbmod);//在升序数组中统计bmods中和tarbmod值相等的区间,左开右闭
ans+=distance(range.first,range.second);//区间范围长度
}
}
cout<<ans<<'\n';
return;
}
signed main(void){
cin.tie(NULL)->sync_with_stdio(false);
int T=1;
cin>>T;
while(T--){
solve();
}
return 0;
}
多理解
I. Dumb Problem II
题目大意:
- 前缀最大值集合 \(g(p)\):对于一个长度为 \(n\) 的排列 \(p\),如果某个元素 \(p_i\) 比它之前的所有元素都大,那么 \(p_i\) 就是一个前缀最大值。\(g(p)\) 是该排列中所有这类数值组成的集合。
- 随机过程:独立且均匀地随机选择 \(k\) 个排列 \(p_1, p_2, \dots, p_k\)。
- 目标:求这 \(k\) 个排列所产生的 \(k\) 个集合 \(\{g(p_1), g(p_2), \dots, g(p_k)\}\) 中,不同集合的期望个数。
- 数据范围:\(n, k \le 5000\),答案对 \(998244353\) 取模。
题解
假设得到的前缀最大值集合是 \(S\)。我们从大到小插入数字 \(x=n,n-1,\dots 2,1\),有两种情况:
-
\(x \in S\),因为是从大到小插入,所以当前的 \(x\) 必须得排在所有已经插入的数字的最前面,只有 \(1\) 种情况。
-
\(x \notin S\),插入 \(x\) 时,前面已经插入了 \(n-x\) 个数,当前 \(x\) 不能是最大值,所以不能插在最前面,后面都可以插,共有 \(n-x\) 种可能。
因此,产生集合 \(S\) 的排列个数为 \(f(S) = \prod_{x \notin S} (n-x)\)。
单个排列拥有集合 \(S\) 的概率为 \(P(S) = \frac{f(S)}{n!}\)。
然后随机选择了 \(k\) 个集合,要求不同集合的期望数。也就是对于所有集合 \(S\),在抽到的 \(k\) 个排列中至少出现一次的概率之和。
利用公式 \((1-x)^k = \sum_{i=0}^k \binom{k}{i} (-1)^i x^i\),上式化为:
发现当 \(i=0\) 时,\(\binom{k}{i} (-1)^i P(S)^i=1\),所以原式继续化简:
交换求和符号:
现在想办法求出 \(g_i=\sum_{S} P(S)^i\)
可以把这个对每个集合单独考虑变成对每个元素单独考虑。对于元素 \(x\),如果 \(x \in S\),那么贡献 \(1\),如果 \(x \notin S\),那么贡献 \((n-x)^i\)。
所以我们计算最终结果:
由于 \(n, k \le 5000\),我们可以用 \(O(nk)\) 的复杂度计算所有的 \(g_i\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k;
const int N=5005;
int fact[N];
const int mod=998244353;
inline int qpow(int a,int xx){
int res=1;
while(xx){
if(xx&1){
res=(res*a)%mod;
}
a=a*a%mod;
xx>>=1;
}
return res;
}
inline int inv(int x){
return qpow(x,mod-2)%mod;
}
inline int C(int n,int m){
if(m<0||m>n) return 0;
return ((fact[n]*inv(fact[m])%mod)*inv(fact[n-m])%mod)%mod;
}
signed main(void){
cin.tie(NULL)->sync_with_stdio(false);
cin>>n>>k;
fact[0]=1;
for(int i=1;i<=max(n,k);i++){
fact[i]=fact[i-1]*i%mod;
}
vector<int> g(k+1);
vector<int> cur(k+1,1);
for(int i=1;i<=k;i++){
int tmp=1;
for(int j=0;j<n;j++){
tmp=tmp*(1+qpow(j,i))%mod;
}
g[i]=tmp*qpow(inv(fact[n]),i)%mod;
}
int ans=0;
for(int i=1;i<=k;i++){
int tmp=(C(k,i)*g[i])%mod;
if(i&1){
ans=(ans+tmp)%mod;
}
else{
ans=(ans-tmp+mod)%mod;
}
}
cout<<ans<<'\n';
return 0;
}
补充
首先,考虑一个更通用的多项式展开。假设我们有一组数 \(B_1, B_2, \dots, B_n\),观察以下乘积的展开过程:
当我们把这个乘积完全展开时,得到的每一项都是从这 \(n\) 个括号中各选一个元素(要么选 \(1\),要么选 \(B_x\))相乘的结果。
- 如果对于某个元素 \(x\),我们选择了 \(B_x\),则认为 \(x \notin S\)。
- 如果选择了 \(1\),则认为 \(x \in S\)。
因此,展开后的每一项都对应一个唯一的集合 \(S \subseteq \{1, \dots, n\}\),其表达式为 \(\prod_{x \notin S} B_x\)。整个展开式的和就是:
G.Plus Xor
题意
给定 \(a,b,c\) ,判定能否通过将 \(a\) 加上 \(b\) 或者异或上 \(b\) 的操作变成 \(c\)。
题解
由于 \(b\) 的范围很小(\(b \le 1000\)),而 \(a\) 和 \(c\) 的范围很大(\(10^{18}\)),我们需要找到一种能够有效缩减状态空间的方法。
如果我们可以通过一系列操作到达某个值 \(a'\),只要满足以下两个条件,就一定能到达 \(c\):
- \(a' \equiv c \pmod b\)
- \(a' \le c\)。
直接在 \(10^{18}\) 的空间搜索是不可能的。我们需要观察 \(a \leftarrow a \oplus b\) 这个操作对 \(a\) 的影响:
- 位运算的影响范围:令 \(b\) 在二进制下有 \(L\) 位(即 \(2^{L-1} \le b < 2^L\))。\(a \oplus b\) 只会改变 \(a\) 的低 \(L\) 位。
- 数值的变化量:\(a\) 异或 \(b\) 后,新值与旧值的差额 \(\Delta = (a \oplus b) - a\)。
这个差额 \(\Delta\) 仅仅取决于 \(a\) 的低 \(L\) 位(即 \(a \pmod{2^L}\))。因此,为了追踪 \(a\) 的变化以及它对模 \(b\) 余数的影响,我们只需要记录两个信息:
- \(x = a \pmod{2^L}\):决定了下一次异或操作会带来多大的数值增量。
- \(y = a \pmod b\):记录当前的余数。
我们将 \((x, y)\) 视为图中的一个节点。状态总数为 \(2^L \times b\)。由于 \(b \le 1000\),\(L \approx 10\),总状态数约为 \(1024 \times 1000 \approx 10^6\),在可接受范围内。
转移方程:
- 加法操作 (\(a+b\)):
- 新状态:\(((x + b) \pmod{2^L}, y)\)
- \(a\) 的变化:\(a=(a + b)\)
- 异或操作 (\(a \oplus b\)):
- 新状态:\(((x \oplus b) \pmod{2^L}, (y + (x \oplus b) - x) \pmod b)\)
- \(a\) 的变化:\(a=(a \oplus b)\)
我们要找的是到达余数状态 \((c \pmod{2^L}, c \pmod b)\) 时,\(a\) 的最小值。
算法
直接使用 \(\text{Dijkstra}\) 算法从起点 \((a \mod{2^L},a \mod{b})\) 开始跑单源最短路。最后找终点 \((c \mod{2^L}, c \mod b)\) 的值是否可行。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int a, b, c;
map<pair<int, int>, int> mp;
struct node{
int A,B,C;
friend bool operator<(node A,node B){
return A.C>B.C;
}
};
inline void solve()
{
cin >> a >> b >> c;
mp.clear();
// vis.clear();
int L = 0;
for (int i = 15; i >= 0; i--)
{
if (b >> i & 1)
{
L = i;
break;
}
}
L++;
int Mod = (1LL << L);
node st = {a % Mod, a % b,a};
pair<int,int> st1={a % Mod, a % b};
priority_queue<node> q;
q.push(st);
mp[st1] = a;
while (q.size())
{
auto xx = q.top();
q.pop();
pair<int,int> xxx={xx.A,xx.B};
int T1=mp[xxx]+b,T2=mp[xxx]^b;
int x = xx.A, y = xx.B;
pair<int, int> tmp1 = {(x + b) % Mod, y};
pair<int, int> tmp2 = {(x ^ b) % Mod, ((y + (x ^ b) - x) + b) % b};
node tmp11 = {(x + b) % Mod, y,T1};
node tmp22 = {(x ^ b) % Mod, ((y + (x ^ b) - x) + b) % b,T2};
if(!mp[tmp1]||mp[tmp1]>T1){
mp[tmp1] = T1;
q.push(tmp11);
}
if(!mp[tmp2]||mp[tmp2]>T2){
mp[tmp2] = T2;
q.push(tmp22);
}
} if (mp[{c % Mod, c % b}] && mp[{c % Mod, c % b}] <= c && ((c - mp[{c % Mod, c % b}]) % b == 0))
{
cout << "YES\n";
}
else
{
cout << "NO\n";
}
return;
}
signed main(void)
{
cin.tie(NULL)->sync_with_stdio(false);
int T = 1;
cin >> T;
while (T--)
{
solve();
}
return 0;
}

浙公网安备 33010602011771号