作为一名参加过多次算法竞赛的老兵,我深知在Codeforces这类平台上遇到的好题往往蕴含着精妙的算法思想和实用的解题技巧。今天我就来分享几道让我印象深刻的题目,并详细解析它们的解题思路和实现细节。
这道题要求我们构造一个特定条件下的序列。题目大意是:给定一个初始数组和需要添加的元素数量k,要求每次向数组末尾添加3个不同的数,且添加的第一个数不能与原数组最后一个数相同。
关键点在于理解题目要求的构造规则:每次必须添加3个不同的数,且首数不能与当前末尾数相同。这保证了序列的多样性和特定模式的延续。
我的解题思路分为两个主要情况处理:
情况一:当原数组已经包含所有可能的数时(即没有缺失的数字),我们只需要循环使用数组最后三个数进行填充。这种模式保证了每次添加的三个数都满足题目要求。
情况二:当数组中存在缺失的数字时:
这种策略确保了:
cpp复制void solve() {
int n, k;
cin >> n >> k;
vector<int> a(n), cnt(n + 1);
for(int i = 0; i < n; i++) {
cin >> a[i];
cnt[a[i]]++;
}
// 寻找缺失的数字
int x = -1;
for(int i = 1; i <= n; i++) {
if(cnt[i] == 0) {
x = i;
break;
}
}
if(x == -1) {
// 情况一:无缺失数字
for(int i = 0; i < k; i++) {
cout << a[n - 3 + (i % 3)] << " \n"[i == k - 1];
}
} else {
// 情况二:有缺失数字
int y = -1;
for(int i = 1; i <= n; i++) {
if(i != x && i != a[n - 1]) {
y = i;
break;
}
}
vector<int> ans{x, y, a[n - 1]};
for(int i = 0; i < k; i++) {
cout << ans[i % 3] << " \n"[i == k - 1];
}
}
}
优化技巧:
这道题要求构造一个长度为n的序列,满足:
通过分析题目条件,我们可以得出mex的理论最大值是max(k, n/(m+1))。这个结论基于以下观察:
cpp复制void solve() {
int n, m, k;
cin >> n >> m >> k;
int mex = max(n / (m + 1), k);
vector<int> a(mex);
iota(a.begin(), a.end(), 0);
for(int i = 0; i < n; i++) {
cout << a[i % mex] << " \n"[i == n - 1];
}
}
实现要点:
题目给出平面上一系列点,要求统计满足特定方向关系(在同一水平线、垂直线或对角线)的点对数量。关键在于高效地对点进行分类和计数。
直接两两比较点对的时间复杂度是O(n²),对于大规模数据不可行。我们采用以下优化策略:
使用四个哈希表分别记录:
对每个哈希表中的点集,计算组合数C(cnt,2) = cnt*(cnt-1)/2
累加所有哈希表的计算结果
cpp复制void solve() {
int n;
cin >> n;
map<int, int> X, Y, diag1, diag2;
for(int i = 1; i <= n; i++) {
int x, y;
cin >> x >> y;
X[x]++;
Y[y]++;
diag1[x - y]++;
diag2[x + y]++;
}
int ans = 0;
for(auto& t : X) ans += t.second * (t.second - 1);
for(auto& t : Y) ans += t.second * (t.second - 1);
for(auto& t : diag1) ans += t.second * (t.second - 1);
for(auto& t : diag2) ans += t.second * (t.second - 1);
cout << ans << endl;
}
性能分析:
给定两个数组a和b,定义顶点i强于顶点j的条件是a[i]-a[j]≥b[i]-b[j]。要求找出所有满足对于任何其他顶点j都强于j的顶点i。
将不等式a[i]-a[j]≥b[i]-b[j]变形得到:
(a[i]-b[i]) ≥ (a[j]-b[j])
这意味着顶点i强于所有其他顶点的条件是:
a[i]-b[i]是所有(a[k]-b[k])中的最大值
cpp复制void solve() {
int n;
cin >> n;
vector<int> a(n+1), b(n+1);
vector<PII> c(n+1);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) cin >> b[i];
for(int i = 1; i <= n; i++) {
c[i].first = a[i] - b[i];
c[i].second = i;
}
sort(c.begin()+1, c.end(), [](const PII& A, const PII& B) {
return A.first > B.first;
});
vector<int> ans;
for(int i = 1; i <= n; i++) {
if(c[i].first == c[1].first) {
ans.push_back(c[i].second);
}
}
sort(ans.begin(), ans.end());
cout << ans.size() << endl;
for(int i = 0; i < ans.size(); i++) {
cout << ans[i] << " \n"[i == ans.size()-1];
}
}
实现细节:
题目要求对于给定的n个点,对每个点s计算f(s) = Σ|s - x_i| + (x_i与s之间的区间长度)。这可以转化为数学表达式的优化问题。
经过推导,我们发现f(s)可以表示为:
f(s) = n + s*(2i - n) - Σ_{j=1}^i x_j + Σ_{j=i+1}^n x_j
其中i是排序后s的位置索引。这个表达式可以通过前缀和和后缀和来高效计算。
cpp复制void solve() {
int n;
cin >> n;
vector<int> ans(n+1);
vector<PII> a(n+1);
int sum = 0;
for(int i = 1; i <= n; i++) {
cin >> a[i].first;
a[i].second = i;
sum += a[i].first;
}
sort(a.begin()+1, a.end());
int s = 0;
for(int i = 1; i <= n; i++) {
s += a[i].first;
sum -= a[i].first;
ans[a[i].second] = n + a[i].first*(2*i - n) - s + sum;
}
for(int i = 1; i <= n; i++) {
cout << ans[i] << " \n"[i == n];
}
}
算法优化:
给定一个数组和多个查询,每个查询给出b和c,要求找出数组中满足x+y=b且x*y=c的数对(x,y)的数量。
这个问题可以转化为解一元二次方程:
x² - bx + c = 0
解为:
x = [b ± √(b²-4c)] / 2
需要验证解是否为整数,以及是否存在于原数组中。
cpp复制map<int, int> mp;
int my_sqrt(int a) {
int l = 0, r = 1e10;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(mid * mid <= a) l = mid;
else r = mid - 1;
}
return l;
}
int get(int b, int c) {
int D = b * b - 4 * c;
if(D < 0) return 0;
int sqrt_D = my_sqrt(D);
if(sqrt_D * sqrt_D != D) return 0;
int x1 = (b - sqrt_D) / 2;
int x2 = (b + sqrt_D) / 2;
if(x1 + x2 != b || x1 * x2 != c) return 0;
if(x1 == x2) return mp[x1] * (mp[x1] - 1) / 2;
else return mp[x1] * mp[x2];
}
void solve() {
int n;
cin >> n;
mp.clear();
for(int i = 0, x; i < n; i++) {
cin >> x;
mp[x]++;
}
int q;
cin >> q;
for(int i = 1; i <= q; i++) {
int b, c;
cin >> b >> c;
cout << get(b, c) << " \n"[i == q];
}
}
关键点:
在长期参加算法竞赛的过程中,我总结出以下几点宝贵经验:
问题转化技巧:很多看似复杂的问题,经过适当的数学转化或重新表述,可以变为经典问题或更易处理的形式。如Strong Vertices问题中的不等式转化。
预处理与查询分离:对于包含多个查询的问题,尽量将预处理和查询处理分开,如Sum and Product问题中使用哈希表预处理,使得每个查询可以快速回答。
数学工具的应用:熟练掌握组合数学、数论、代数等数学工具,能在解题时提供关键思路。如Power of Points问题中的前缀和技巧。
边界条件处理:特别注意各种边界情况,如空输入、极值、重复元素等。在Beautiful Palindromes问题中就对全包含和部分包含情况分别处理。
代码优化习惯:养成使用高效数据结构和算法的习惯,如优先使用unordered_map而非map当不需要排序时,注意I/O优化等。