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\) 最大,求该最大值。

关键观察

核心性质:

\[\gcd(a+b, c) \ge \gcd(a, b, c) \]

证明:设 \(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\) 段。若划分为更多段,将前两段合并一定不劣;反复合并直到只剩两段即可。

正解

  1. 预处理前缀和 \(sum[i] = \sum_{k=1}^{i} A_k\)
  2. 枚举分割点 \(i\)\(1 \le i < n\)),两段和分别为 \(sum[i]\)\(sum[n] - sum[i]\)
  3. 计算 \(\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)\)cntqs 数组大小约为 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\) 的基础哈希值:

\[H(\text{base}_j) = \sum_{k=1}^{n} A_{k,j} \cdot p^k \]

当唯一的 1 在第 \(i\) 行时,mask 的哈希只需 \(\mathcal{O}(1)\) 修改:

\[H(\text{mask}) = H(\text{base}_j) \pm p^i \]

(若 \(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>),处理时逐列扫描。对每列:

  1. 预处理前缀哈希 pre[i] 和后缀哈希 suf[i](用于 \(\mathcal{O}(1)\) 计算翻转第 \(i\) 行的 mask 哈希)。
  2. 对于每一行 \(i\),mask 哈希为:

\[H = \text{pre}[i-1] + \text{suf}[i+1] + (1 - A_{i,j}) \cdot p^i \]

  1. 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

讲评完毕,祝各位选手比赛顺利!

posted @ 2026-07-15 23:32  surprise_ying  阅读(44)  评论(0)    收藏  举报