最近在算法竞赛中遇到两道很有意思的题目,都涉及到数据结构的巧妙应用。作为算法竞赛老手,我想分享一下这两道题的解题思路和实现细节,希望能帮助到正在准备比赛的朋友们。
这道题给定一个数组,要求我们计算通过相邻元素的位运算(与、或、异或)可以生成的所有可能值的数量。题目链接:Expanding Array - QOJ.ac
关键观察点:
位运算有几个重要性质值得我们注意:
通过实验发现,对于任意两个整数a和b,经过多次组合运算后,最终能得到的不同的值不会超过8个。这是因为位运算的组合会在有限步后达到稳定状态。
cpp复制#include <bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++) cin >> a[i];
set<int> s;
s.insert(0); // 初始包含0
for(int i = 1; i < n; i++) {
int l = a[i-1], r = a[i];
s.insert(l), s.insert(r);
vector<int> ans = {l & r, l | r, l ^ r};
for(int j = 0; j < 3; j++) {
s.insert(ans[j]);
s.insert(l & ans[j]);
s.insert(l | ans[j]);
s.insert(l ^ ans[j]);
s.insert(r & ans[j]);
s.insert(r | ans[j]);
s.insert(r ^ ans[j]);
}
}
cout << s.size() << endl;
}
实现要点:
注意:在实际比赛中,这种打表找规律的方法很实用,但需要确保规律的正确性。建议在小规模数据上验证后再应用到代码中。
这道题要求计算满足特定条件的字符串排列方案数。题目链接:Athlete Welcome Ceremony - QOJ.ac
关键约束条件:
DP状态设计:
转移方程需要考虑两种情况:
cpp复制vector dp(2, vector(n+1, vector(n+1, vector<int>(3))));
// 初始化处理...
for(int i = 2; i <= n; i++) {
if(s[i] == '?') cnt++;
// 清空当前状态
for(int j = 0; j <= cnt; j++)
for(int k = 0; k+j <= cnt; k++)
for(int p = 0; p <= 2; p++)
dp[i&1][j][k][p] = 0;
// 状态转移
for(int j = 0; j <= cnt; j++)
for(int k = 0; k+j <= cnt; k++)
for(int p = 0; p <= 2; p++) {
if(s[i] == '?') {
if(j && p != 0) dp[i&1][j][k][0] = (dp[i&1][j][k][0] + dp[i-1&1][j-1][k][p]) % mod;
if(k && p != 1) dp[i&1][j][k][1] = (dp[i&1][j][k][1] + dp[i-1&1][j][k-1][p]) % mod;
if(p != 2) dp[i&1][j][k][2] = (dp[i&1][j][k][2] + dp[i-1&1][j][k][p]) % mod;
} else {
// 类似处理固定字符情况
}
}
}
为了高效回答多个查询,我们使用三维前缀和来预处理所有可能的查询结果:
cpp复制vector f(n+1, vector(n+1, vector<int>(n+1)));
// 填充初始值...
// 计算三维前缀和
for(int i = 0; i <= n; i++)
for(int j = 0; j <= n; j++)
for(int k = 0; k <= n; k++) {
if(i >= 1) f[i][j][k] = (f[i][j][k] + f[i-1][j][k]) % mod;
if(j >= 1) f[i][j][k] = (f[i][j][k] + f[i][j-1][k]) % mod;
if(k >= 1) f[i][j][k] = (f[i][j][k] + f[i][j][k-1]) % mod;
// 处理重叠部分...
}
在实际比赛中,我发现很多选手容易犯的几个错误:
cpp复制const int mod = 1e9 + 7;
// 正确的模加法
int add(int a, int b) {
return (a + b) % mod;
}
// 处理可能的负数
int fix(int x) {
return (x % mod + mod) % mod;
}
cpp复制// 使用i&1实现滚动
dp[i&1][j][k][p] = (dp[i&1][j][k][p] + dp[i-1&1][j][k][p]) % mod;
// 每次迭代前清空当前状态
for(int j = ...)
for(int k = ...)
for(int p = ...)
dp[i&1][j][k][p] = 0;
cpp复制ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
这个技巧可以显著提高C++的I/O速度,但在使用后不能混用C风格的scanf/printf。
在测试过程中,我发现:
为了加深对这类问题的理解,我推荐以下几个类似的题目:
这些题目都涉及到类似的技巧,通过练习可以更好地掌握位运算和动态规划的应用。