CSP-J 模拟赛 7 讲评
CSP-J 模拟赛 7 讲评
目录
T1 序列重排(arrange)
题意简述
给定长度为 \(n\) 的序列 \(A\),可以任意重排。定义权值为 \(\operatorname{mex}\{A_1+A_2, A_2+A_3, \ldots, A_{n-1}+A_n\}\),即相邻两数之和的集合中最小的未出现非负整数。求最小可能权值。
关键观察
手玩几组样例可以发现:答案只可能是 0、1 或 2。
- 相邻和不可能很大,mex 不会超过 2(因为两个非负数相加至少为 0,而相邻和的值域被序列内容限制)。
分类讨论
答案为 0 的条件:\(0\) 的个数 \(\le \lfloor\frac{n+1}{2}\rfloor\)
可以把序列排成 \(0, x, 0, y, 0, \ldots\) 交替的形式,保证没有两个 \(0\) 相邻,这样 mex 为 \(0\)(因为 \(0+0\) 不会出现,而 \(0+x \ge 0\),且可能出现 \(0\) 的情况... 实际上 mex=0 要求相邻和集合中不包含 0,即没有任何相邻两数之和为 0。由于所有数非负,和为 0 仅当两个数都是 0。所以只要不让两个 0 相邻即可。)
答案为 1 的条件(不满足答案为 0 的前提下):
- 情况一:所有数全是 \(0\)。此时无论如何排列,相邻和全是 \(0\),mex 为 \(1\)。
- 情况二:存在某个 \(A_i > 1\)。用这个大于 \(1\) 的数可以隔开 \(0\) 和 \(1\),避免出现和为 \(1\) 的相邻对。
答案为 2:其余所有情况(即 0 的个数太多,且没有大于 1 的数来隔开)。
时间复杂度
\(\mathcal{O}(n)\),只需遍历一次统计 0 的个数和检查是否存在大于 1 的数。
空间复杂度
\(\mathcal{O}(n)\)(存储序列)。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,a[N];
int main()
{
freopen("arrange.in","r",stdin);
freopen("arrange.out","w",stdout);
cin>>n;
int s=0; // 统计0的个数
for(int i=1;i<=n;i++) cin>>a[i],s+=(a[i]==0);
// 答案为0:0的个数不超过 (n+1)/2,可以0,x,0,y,0,...交替放
if(s<=(n+1)/2)
{
cout<<0<<endl;
return 0;
}
// 答案为1的情况
if(s==n) // 全是0
{
cout<<1<<endl;
return 0;
}
for(int i=1;i<=n;i++)
{
if(a[i]>1) // 存在>1的数,可以把0和1隔开
{
cout<<1<<endl;
return 0;
}
}
// 否则答案为2
cout<<2<<endl;
return 0;
}
T2 划分(partition)
题意简述
将长度为 \(n\) 的序列 \(A\) 划分成至少 \(2\) 段连续子段,使得各子段和的 \(\gcd\) 最大,求该最大值。
关键观察
核心性质:
证明:设 \(d = \gcd(a, b, c)\),则 \(d \mid a\) 且 \(d \mid b\),故 \(d \mid (a+b)\)。又 \(d \mid c\),所以 \(d \mid \gcd(a+b, c)\),即 \(\gcd(a+b, c) \ge d\)。
这个性质说明:将相邻的两段合并,gcd 不会变小,只会更大或不变。 因此最优解一定只需划分为 \(2\) 段。若划分为更多段,将前两段合并一定不劣;反复合并直到只剩两段即可。
正解
- 预处理前缀和 \(sum[i] = \sum_{k=1}^{i} A_k\)
- 枚举分割点 \(i\)(\(1 \le i < n\)),两段和分别为 \(sum[i]\) 和 \(sum[n] - sum[i]\)
- 计算 \(\gcd(sum[i], sum[n] - sum[i])\),取所有可能中的最大值
时间复杂度
\(\mathcal{O}(n \log V)\),其中 \(V\) 是值域(\(\gcd\) 的复杂度为 \(\mathcal{O}(\log V)\))。
空间复杂度
\(\mathcal{O}(n)\)(前缀和数组)。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
long long a[N],sum[N];
int main()
{
freopen("partition.in","r",stdin);
freopen("partition.out","w",stdout);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i]; // 前缀和
}
long long ans=0;
// 关键性质:gcd(a+b,c) >= gcd(a,b,c),因此只需划分为两段
// 枚举分割点 i,两段分别为 sum[i] 和 sum[n]-sum[i]
for(int i=1;i<n;i++)
{
long long g=__gcd(sum[i],sum[n]-sum[i]);
ans=max(ans,g);
}
cout<<ans<<endl;
return 0;
}
T3 蛋糕(cake)
题意简述
有 \(m\) 块蛋糕,每块大小为 \(2\) 的整数幂。每次可以将一块大小 \(>1\) 的蛋糕切成两块大小为原来一半的蛋糕(代价为 1)。问最少切多少次,可以选出一些蛋糕使得大小之和恰好为 \(n\)。若不可能则输出 \(-1\)。
部分分 1:20%(\(n \le 256,\ m \le 4\))
题解
数据范围极小,直接 BFS 暴力搜索。状态定义为每种尺寸蛋糕的数量(用指数表示,如 \(2^k\) 对应下标 \(k\))。每次操作选择一块 \(>\) 1 的蛋糕切一刀,产生两块更小的蛋糕。用 01 背包判断当前状态能否拼出 \(n\)。
复杂度
状态数较少(\(n \le 256\) 限制了切蛋糕的上限),可以接受。
代码
#include <bits/stdc++.h>
using namespace std;
long long n,m;
long long a[10];
// 将蛋糕大小映射到指数:a_i = 2^{idx}
int idx(long long x)
{
int r=0;
while(x>1) x/=2,r++;
return r;
}
// 20%数据:n≤256, m≤4, a_i≤512
// BFS:状态是各幂次蛋糕的数量,每次切一块蛋糕
map<vector<int>,int> mp; // state->min_cuts
// 检查当前状态能否拼出n(01背包)
bool check(vector<int> &cur)
{
vector<bool> dp(n+1,0);
dp[0]=1;
for(int k=0;k<=12;k++)
{
long long sz=1LL<<k;
if(sz>n) break; // 比n大的蛋糕跳过
for(int t=0;t<cur[k];t++)
{
for(int j=n;j>=sz;j--)
if(dp[j-sz]) dp[j]=1;
}
}
return dp[n];
}
int main()
{
freopen("cake.in","r",stdin);
freopen("cake.out","w",stdout);
cin>>n>>m;
long long tot=0;
for(int i=1;i<=m;i++) cin>>a[i],tot+=a[i];
if(tot<n) {cout<<-1<<endl;return 0;}
// 初始状态:每块蛋糕对应一个幂次
vector<int> init(13,0); // 2^0 ~ 2^12, 最大512=2^9
for(int i=1;i<=m;i++) init[idx(a[i])]++;
// BFS
queue<vector<int>> q;
q.push(init);
mp[init]=0;
int ans=-1;
while(q.size())
{
auto cur=q.front(); q.pop();
int cuts=mp[cur];
if(check(cur))
{
ans=cuts;
break;
}
// 尝试切一块蛋糕
for(int k=1;k<=12;k++)
{
if(cur[k]>0) // 有一块大小为2^k的蛋糕,切成两块2^{k-1}
{
auto nxt=cur;
nxt[k]--;
nxt[k-1]+=2;
if(mp.find(nxt)==mp.end())
{
mp[nxt]=cuts+1;
q.push(nxt);
}
}
}
}
cout<<ans<<endl;
return 0;
}
部分分 2:\(m=1\)(只有一块蛋糕)
题解
当只有一块蛋糕时,问题变得简单:从 \(n\) 的最高位向最低位处理,若 \(n\) 某位为 1 就需要对应大小的蛋糕。由于只有一块蛋糕,必须从大往小切。具体做法同正解中的低位贪心,但此时只有一块初始蛋糕,贪心过程更直观。
说明:\(m=1\) 的算法与正解相同,参考下方正解代码即可通过。此处不单独给出代码。
部分分 3:\(m=2\)(只有两块蛋糕)
题解
与 \(m=1\) 类似,两块蛋糕的情况同样使用正解的贪心算法即可解决。区别仅在于初始有两块蛋糕可供切割/合并。
说明:\(m=2\) 的算法与正解相同,参考下方正解代码即可通过。
正解(100%)
题解
由于所有蛋糕大小都是 \(2\) 的幂次,在二进制下考虑。
核心贪心:从低位到高位处理 \(n\) 的每一位。
- 用
cnt[k]表示当前大小为 \(2^k\) 的蛋糕数量。 - 对于 \(n\) 的第 \(i\) 位(二进制),若该位为 \(1\),需要一块大小为 \(2^i\) 的蛋糕。
- 优先用低位合成:若 \(0 \sim i\) 位的蛋糕总大小足够拼出 \(2^i\),则不需要切割(低位向高位合成是免费的,两块 \(2^{k}\) 可以当一块 \(2^{k+1}\) 用)。
- 若低位不够,从高位切:找到最小的大于 \(2^i\) 的蛋糕,一路切到 \(2^i\),代价为
j-i(\(j\) 是高位的指数)。切的过程中会产生中间大小的蛋糕,计入cnt。
无解判断:所有蛋糕大小之和 \(< n\) 时无解。
贪心正确性:从低位到高位保证已处理位不再受影响,且每次优先用低位合成(免费)再切高位(有代价),是最优策略。
时间复杂度
\(\mathcal{O}(m + \log^2 n)\)。预处理蛋糕的指数分布需 \(\mathcal{O}(m)\),主循环 63 位,每次可能向上扫描找到可切的大蛋糕,均摊 \(\mathcal{O}(\log n)\)。
空间复杂度
\(\mathcal{O}(\log n)\)(cnt 和 qs 数组大小约为 64)。
AC 代码
#include <bits/stdc++.h>
using namespace std;
long long n,m;
long long a[100005];
long long cnt[65]; // cnt[i]表示大小为2^i的蛋糕数量
long long qs[65]; // qs[i]表示0~i位的蛋糕总大小
int main()
{
freopen("cake.in","r",stdin);
freopen("cake.out","w",stdout);
cin>>n>>m;
long long tot=0;
for(int i=1;i<=m;i++) cin>>a[i],tot+=a[i];
// 无解:总大小不够
if(tot<n) {cout<<-1<<endl;return 0;}
// 统计每种2的幂次的蛋糕数量
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=m;i++)
{
long long x=a[i];
int k=0;
while(x>1) x/=2,k++;
cnt[k]++;
}
long long ans=0,S=0; // ans:切的总次数, S:当前已选蛋糕的总大小
// 从低位到高位贪心
for(int i=0;i<=62;i++)
{
// 更新前缀和
qs[0]=cnt[0];
for(int j=1;j<=62;j++) qs[j]=qs[j-1]+(1LL<<j)*cnt[j];
if(n>>i&1) // n这一位为1,需要一块大小为2^i的蛋糕
{
// 检查低位蛋糕的总值是否够拼出2^i
if((1LL<<i) > qs[i]-S)
{
// 低位不够,需要从高位切一块下来
int j=i+1;
while(j<=62 && cnt[j]==0) j++;
if(j>62) {cout<<-1<<endl;return 0;}
cnt[j]--; // 大蛋糕被切掉
ans+=j-i; // 切j-i刀
cnt[i]+=2; // 最后切出两块2^i
for(int k=i+1;k<j;k++) cnt[k]++; // 中间产生的蛋糕
}
// 更新前缀和
qs[0]=cnt[0];
for(int j=1;j<=62;j++) qs[j]=qs[j-1]+(1LL<<j)*cnt[j];
S+=(1LL<<i);
}
}
cout<<ans<<endl;
return 0;
}
T4 矩阵(matrix)
题意简述
给定一个 \(n \times m\) 的 01 矩阵,每次操作可以选择一行将其所有元素 01 翻转。定义"好列"为恰好包含一个 1 的列。求经过任意次操作后,最多能有多少个好列。
部分分 1:20%(\(n, m \le 16\))
题解
\(n \le 16\),可以暴力枚举所有 \(2^n\) 种行翻转方案。对于每种方案,逐列统计 1 的个数,若恰好为 \(1\) 则该列为好列。取所有方案中的最大值。
时间复杂度
\(\mathcal{O}(2^n \cdot nm)\),\(n=16\) 时约 \(10^7\) 量级,可过。
空间复杂度
\(\mathcal{O}(nm)\)。
代码
#include <bits/stdc++.h>
using namespace std;
int n,m;
char s[20][20];
// 20%数据:n,m≤16
// 暴力枚举所有行的翻转方案(2^n种),统计好列数
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
cin>>n>>m;
for(int i=0;i<n;i++) cin>>s[i];
int ans=0;
for(int mask=0;mask<(1<<n);mask++) // 枚举哪些行翻转
{
int cnt=0;
for(int j=0;j<m;j++) // 统计每一列
{
int ones=0;
for(int i=0;i<n;i++)
{
int val=s[i][j]-'0';
if(mask>>i&1) val^=1; // 该行翻转则取反
ones+=val;
}
if(ones==1) cnt++; // 好列
}
ans=max(ans,cnt);
}
cout<<ans<<endl;
return 0;
}
部分分 2:40%(\(n, m \le 100\))
题解
这道题的 40% 部分分(解法2)用到了一个非常经典的“逆向思维”技巧。
为了让你彻底搞懂,我们先不看代码,用大白话和生活中的例子来梳理一遍逻辑。
第一步:转变思路(从“试钥匙”到“问锁头”)
在 20% 的暴力解法中,我们的思路是:
“枚举所有可能的翻转方案(试钥匙),看看每种方案能让多少列变成好列(能开几把锁),最后选效果最好的。”
但当 \(N=100\) 时,翻转方案有 \(2^{100}\) 种,宇宙毁灭了你也试不完。所以必须换个思路:
“我们直接去问每一列(问锁头):‘喂!你要变成好列的话,需要我采用哪种翻转方案(你需要什么钥匙)?’”
第二步:一列到底需要什么“翻转方案”?
什么是“好列”?恰好只有一个 1 的列。
假设我们有一列,从上到下共有 \(N=4\) 行,原本的数字是:
第0行:1
第1行:0
第2行:1
第3行:0
(这列本身有两个 1,目前不是好列)
现在我们要把它变成好列,它最终的模样只有 4 种可能(因为唯一的那个 1 只能在第 0、1、2 或 3 行):
-
可能 A:想让唯一的 1 留在第 0 行
最终目标必须是:1, 0, 0, 0
对比原数字:1, 0, 1, 0
推导需要的翻转方案: 第 0 行不变,第 1 行不变,第 2 行必须翻转(1变0),第 3 行不变。
我们把这个方案记作字符串(1代表翻转,0代表不翻):0010 -
可能 B:想让唯一的 1 留在第 1 行
最终目标必须是:0, 1, 0, 0
对比原数字:1, 0, 1, 0
推导需要的翻转方案: 第 0 行翻转,第 1 行翻转,第 2 行翻转,第 3 行不变。
记作翻转方案:1110 -
可能 C:想让唯一的 1 留在第 2 行
推导需要的翻转方案:1000 -
可能 D:想让唯一的 1 留在第 3 行
推导需要的翻转方案:1011
【核心结论 1】:
对于任意一列,想让它变成好列,只有恰好 \(N\) 种翻转方案能够做到。
第三步:寻找“最受欢迎的翻转方案”
既然每列都会提供 \(N\) 个能让自己变成好列的“专属翻转方案”。
假设矩阵有 \(M\) 列:
- 第 1 列说:“方案A、方案B、方案C 能让我变好。”
- 第 2 列说:“方案B、方案D、方案E 能让我变好。”
- 第 3 列说:“方案B、方案X、方案Y 能让我变好。”
你会选择哪个方案?当然是方案B!因为它被 3 列同时需要,如果使用了方案 B,就能同时让 3 列变成好列。
【核心结论 2】:
我们只需要把每一列对应的 \(N\) 种翻转方案全部求出来,丢到一个“记票箱”(map<string, int>)里。哪个方案得到的票数最多,这个票数就是最多能得到的好列数量!
第四步:代码里的“神仙操作”是怎么回事?
你可能有一个疑问:上面我们在推导“翻转方案”时,需要拿“目标状态”和“原状态”去对比,看看哪些行不同。为什么代码里直接 mask[i]=(mask[i]=='0'?'1':'0'); 就完事了?
这里有一个极其巧妙的数学规律:
设原数列为 S,翻转方案为 P,目标数列为 Target。
因为翻转相当于异或操作(不同为1,相同为0),所以:
S 异或 P = Target
根据异或的性质,我们可以反推:
P = S 异或 Target
你想想 Target 是什么样子的?
Target 是一个只有第 \(i\) 行是 1,其他所有行都是 0 的数列。
那么,用原数列 S 去异或 Target 会发生什么?
- 对于不是 \(i\) 的行,
Target那里是0。任何数异或 0 都不变。所以P在这些行就等于S。 - 对于第 \(i\) 行,
Target那里是1。任何数异或 1 都会取反(0变1,1变0)。所以P在第 \(i\) 行等于S取反。
【终极捷径】:
我们要找的翻转方案 P,在字面上,就等于把原数列 S 的第 \(i\) 个字符取反,其余字符原封不动抄下来!
结合代码再看一遍,豁然开朗:
int main()
{
// ... 前面的输入省略 ...
map<string,int> mp; // 这是一个“记票箱”,用来统计每种翻转方案出现的次数
for(int j=0;j<m;j++) // 遍历每一列(问锁头)
{
string base;
for(int i=0;i<n;i++) base+=s[i][j]; // 把第 j 列原本的 01 序列拿出来,记为 base (即上文的 S)
for(int i=0;i<n;i++) // 枚举唯一的那个 '1' 最终留在哪一行
{
string mask=base; // 直接把 base 抄过来
mask[i]=(mask[i]=='0'?'1':'0'); // 终极捷径:只把第 i 行取反。这就是这一列需要的“翻转方案”!
mp[mask]++; // 把这个方案扔进记票箱,票数 +1
}
}
int ans=0;
// 找出得票最多的翻转方案,它的票数就是能同时满足的最多的“好列”数量
for(auto &[mask,cnt]:mp) ans=max(ans,cnt);
cout<<ans<<endl;
return 0;
}
时间复杂度
\(\mathcal{O}(n^2 m)\),约 \(10^6\) 量级,可过。
空间复杂度
\(\mathcal{O}(nm)\)。
代码
#include <bits/stdc++.h>
using namespace std;
int n,m;
char s[105][105];
// 40%数据:n,m≤100
// 关键思想:对于第j列,若想让唯一的'1'在第i行,
// 翻转mask = base_j XOR (1<<i),其中base_j是第j列本身
// 枚举所有(i,j),统计每种mask出现的次数,取最大值
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
cin>>n>>m;
for(int i=0;i<n;i++) cin>>s[i];
map<string,int> mp;
for(int j=0;j<m;j++)
{
string base;
for(int i=0;i<n;i++) base+=s[i][j]; // 第j列的01串作为mask
for(int i=0;i<n;i++)
{
string mask=base;
mask[i]=(mask[i]=='0'?'1':'0'); // XOR (1<<i)
mp[mask]++;
}
}
int ans=0;
for(auto &[mask,cnt]:mp) ans=max(ans,cnt);
cout<<ans<<endl;
return 0;
}
部分分 3:60%(\(n, m \le 500\))
题解
40% 的做法中,每次构造 mask 字符串是 \(\mathcal{O}(n)\) 的,总复杂度 \(\mathcal{O}(n^2 m)\),在 \(n,m=500\) 时约 \(1.25 \times 10^8\) 次字符串拷贝,会超时。
优化方法:使用多项式哈希替代字符串比较。对于列 \(j\) 的基础哈希值:
当唯一的 1 在第 \(i\) 行时,mask 的哈希只需 \(\mathcal{O}(1)\) 修改:
(若 \(A_{i,j}=1\) 则减去 \(p^i\),若 \(A_{i,j}=0\) 则加上 \(p^i\))
将所有哈希值放入 unordered_map 统计频率,最大值即为答案。
时间复杂度
\(\mathcal{O}(nm)\),每个 \((i,j)\) 的哈希计算为 \(\mathcal{O}(1)\)。
空间复杂度
\(\mathcal{O}(nm)\)(哈希表大小)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=505;
typedef unsigned long long ull;
const ull p=13331;
int n,m;
char a[N][N];
ull pw[N];
unordered_map<ull,int> mp;
// 60%数据:n,m≤500
// 使用哈希优化枚举(i,j)的mask比较
// 对每个(i,j),mask哈希 = base列哈希 翻转第i位
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>(a[i]+1);
pw[0]=1;
for(int i=1;i<=n;i++) pw[i]=pw[i-1]*p;
int ans=0;
for(int j=1;j<=m;j++)
{
// 计算第j列的hash
ull base=0;
for(int i=1;i<=n;i++)
if(a[i][j]=='1') base+=pw[i];
// 枚举唯一的'1'在哪一行
for(int i=1;i<=n;i++)
{
ull mask;
if(a[i][j]=='1')
mask=base-pw[i]; // 原来是1,翻转后变0
else
mask=base+pw[i]; // 原来是0,翻转后变1
mp[mask]++;
if(mp[mask]>ans) ans=mp[mask];
}
}
cout<<ans<<endl;
return 0;
}
正解(100%)
题解
当 \(n\) 和 \(m\) 都很大但 \(n \times m \le 3 \times 10^5\) 时,无法用 \(N \times N\) 的二维数组存储矩阵。需要逐列处理。
由于 \(n \times m\) 有上限,读入时按行存储(每行是一个 vector<int>),处理时逐列扫描。对每列:
- 预处理前缀哈希
pre[i]和后缀哈希suf[i](用于 \(\mathcal{O}(1)\) 计算翻转第 \(i\) 行的 mask 哈希)。 - 对于每一行 \(i\),mask 哈希为:
- 用
unordered_map统计哈希频率,取最大值。
时间复杂度
\(\mathcal{O}(nm)\),每列扫描一次,每个位置 \(\mathcal{O}(1)\) 计算哈希。
空间复杂度
\(\mathcal{O}(nm)\)(存储矩阵和哈希表)。
AC 代码
#include <bits/stdc++.h>
using namespace std;
const int N=3e5+5;
typedef unsigned long long ull;
const ull p=13331;
int n,m;
vector<int> G[N]; // G[i][j] 表示第i行第j列的值
ull pw[N],pre[N],suf[N];
unordered_map<ull,int> mp;
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
cin>>n>>m;
// 预处理p的幂
pw[0]=1;
for(int i=1;i<=n;i++) pw[i]=pw[i-1]*p;
// 读入矩阵(转置存储:G[i][j] = A[i][j])
for(int i=1;i<=n;i++)
{
G[i].push_back(0); // 1-indexed
for(int j=1;j<=m;j++)
{
char c;cin>>c;
G[i].push_back(c-'0');
}
}
int ans=0;
// 逐列处理
for(int j=1;j<=m;j++)
{
// 前缀hash:pre[i] = sum_{k=1..i} G[k][j] * p^k
for(int i=1;i<=n;i++)
pre[i]=pre[i-1]+G[i][j]*pw[i];
// 后缀hash:suf[i] = sum_{k=i..n} G[k][j] * p^k
suf[n+1]=0;
for(int i=n;i>=1;i--)
suf[i]=suf[i+1]+G[i][j]*pw[i];
// 枚举第j列唯一的'1'在第i行
for(int i=1;i<=n;i++)
{
// flip mask hash = 前缀 + 翻转后的第i行 + 后缀
ull val=pre[i-1]+suf[i+1]+(1-G[i][j])*pw[i];
mp[val]++;
if(mp[val]>ans) ans=mp[val];
}
}
cout<<ans<<endl;
return 0;
}
赛场经验总结
1. 小范围手玩找规律
T1 是典型的小范围手玩题。拿到题目后先对 \(n=2,3,4\) 手动枚举,发现答案只有 0/1/2。不要一上来就写复杂算法,先观察答案值域是否有限。类似技巧在 CSP-J 的结论题中经常出现。
2. 贪心性质的数学证明
T2 的核心是 \(\gcd(a+b, c) \ge \gcd(a, b, c)\) 的推导。遇到"合并不会更差"的直觉时,尝试用数学语言证明。一旦证明了,复杂度从指数级降到线性级。
经验:\(\gcd\) 相关问题的常见套路是"合并段"或"考虑相邻两项的 \(\gcd\)"。
3. 二进制视角处理 2 的幂次
T3 所有蛋糕大小都是 2 的幂次,立即联想到二进制拆分。处理这种问题时:
- 低位向高位合成是免费的(两块 \(2^k\) = 一块 \(2^{k+1}\))
- 高位向低位拆分是有代价的(一块 \(2^k\) 切一刀 = 两块 \(2^{k-1}\))
- 贪心方向:从低位到高位扫描,优先用免费操作
这是类似"硬币找零"问题的变种,二进制表示天然具备贪心性质。
4. 转换枚举角度
T4 的 40% -> 60% -> 100% 体现了经典的优化路径:
- 暴力枚举翻转方案 (\(2^n\)) → 不可行
- 转换视角:枚举"谁是唯一的 1"\((i,j)\),翻转方案被唯一确定
- 发现本质:问题等价于"统计相同 mask 的最大出现次数"
- 哈希加速:用多项式哈希替代字符串比较,\(\mathcal{O}(n) \to \mathcal{O}(1)\)
当正向枚举(枚举翻转方案)太慢时,尝试反向思考(枚举目标状态)。
5. 哈希在"统计相同模式"问题中的应用
T4 的正解使用多项式哈希将比较两个长度 \(n\) 的 01 串的复杂度从 \(\mathcal{O}(n)\) 降到 \(\mathcal{O}(1)\)。类似技巧在字符串匹配、矩阵匹配等问题中广泛使用。推荐选取大质数作为基数(如 13331)并用 unsigned long long 自然溢出,既简单又高效。
6. 考试策略建议
| 题目 | 建议时间 | 目标分数 | 策略 |
|---|---|---|---|
| T1 | 20min | 100 | 结论题,快速拿满 |
| T2 | 30min | 100 | 发现两段性质后可秒 |
| T3 | 50min | 40-100 | 先拿暴力 20%,再尝试正解 |
| T4 | 60min | 20-40 | 先拿暴力 20%,有时间再优化 |
| 检查 | 30min | — | 检查边界、文件读写、long long |
讲评完毕,祝各位选手比赛顺利!
浙公网安备 33010602011771号