1. 算法竞赛中的经典问题解析
最近在准备算法竞赛的过程中,我遇到了几道非常有意思的题目。这些题目涵盖了数学计算、动态规划、贪心算法等多个领域,对于提升算法思维和编程能力很有帮助。下面我将详细解析这些题目的解题思路和实现方法。
2. 题目详解与解法分析
2.1 T1:整数分式求和问题
这道题目要求计算从1到n的所有整数除n的向下取整结果之和。直接暴力计算的时间复杂度是O(n),对于大数n来说效率太低。
2.1.1 优化思路
观察发现,当i超过√n后,n/i的结果会重复出现。我们可以利用这个性质将时间复杂度降低到O(√n)。具体做法是:
- 对于i从1到√n,直接计算n/i
- 对于大于√n的部分,找出使得n/i等于某个值的i的范围,然后批量计算
2.1.2 代码实现
cpp复制#include<iostream>
using namespace std;
int n;
long long ans = 0;
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
int v = n / i;
int j = n / v;
ans += v * (j - i + 1);
i = j;
}
cout << ans;
return 0;
}
注意:这里的关键是找到使得n/i值相同的i的范围,然后一次性计算这个范围内的贡献。
2.2 T2:序列差的平方和
给定一个序列,要求计算所有无序数对差的平方之和。直接计算的时间复杂度是O(n²),对于大n不可行。
2.2.1 数学推导
我们可以将差的平方展开:
(a_i - a_j)² = a_i² - 2a_i a_j + a_j²
然后利用前缀和优化计算:
- 维护a_i的前缀和s
- 维护a_i²的前缀和s2
- 对于每个a_i,其贡献为(i-1)a_i² - 2a_i*s[i-1] + s2[i-1]
2.2.2 代码实现
cpp复制#include<iostream>
using namespace std;
int n, ans = 0;
int a[100005], s[100005], s2[100005];
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i];
s[i] = (s[i - 1] + a[i]) % 1000000007;
s2[i] = (s2[i - 1] + 1LL*a[i]*a[i]) % 1000000007;
}
for (int i = 2; i <= n; i++){
ans = (ans + 1LL*(i-1)*a[i]%1000000007*a[i]%1000000007) % 1000000007;
ans = (ans - 2LL*a[i]*s[i-1]%1000000007 + 1000000007) % 1000000007;
ans = (ans + s2[i-1]) % 1000000007;
}
cout << ans;
return 0;
}
提示:注意取模运算的处理,避免出现负数结果。
2.3 T3:数字变换问题
给定一个正整数n,每次操作可以减1或除以一个质因数,求变为1的最少操作次数。
2.3.1 动态规划解法
定义dp[i]表示将i变为1的最少操作次数:
- 预处理所有数的质因数
- 对于每个i,dp[i] = min(dp[i-1], min{dp[i/p]}) + 1,其中p是i的质因数
2.3.2 代码实现
cpp复制#include<bits/stdc++.h>
using namespace std;
bool a[1000005];
int t, n;
int dp[1000005];
vector<int> v[1000005];
int main(){
memset(a, 1, sizeof(a));
a[1] = 0;
for (int i = 2; i <= 1000000; i++){
if (a[i]){
for (int j = i; j <= 1000000; j += i){
a[j] = false;
v[j].push_back(i);
}
}
}
dp[1] = 0;
for (int i = 2; i <= 1000000; i++){
int tmp = 0x7f7f7f7f;
for (int j = 0; j < v[i].size(); j++){
tmp = min(tmp, dp[i / v[i][j]]);
}
dp[i] = min(dp[i - 1], tmp) + 1;
}
cin >> t;
while(t--){
cin >> n;
cout << dp[n] << endl;
}
return 0;
}
技巧:使用埃拉托斯特尼筛法预处理质因数可以大大提高效率。
2.4 T4:课程安排问题
选择若干节课,要求时间不重叠,最大化总上课时长。
2.4.1 贪心+动态规划
- 按结束时间排序
- dp[i]表示前i节课的最大时长
- 对于第i节课,找到结束时间不超过其开始时间的最后一节课j
- dp[i] = max(dp[i-1], dp[j] + duration[i])
2.4.2 代码实现
cpp复制#include<bits/stdc++.h>
using namespace std;
struct node{
int s;
int e;
} a[100005];
int n;
int e[100005], dp[100005];
bool cmp(node a, node b){
return a.e < b.e;
}
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i].s >> a[i].e;
}
sort(a + 1, a + n + 1, cmp);
for (int i = 1; i <= n; i++){
e[i] = a[i].e;
}
for (int i = 1; i <= n; i++){
int pos = upper_bound(e + 1, e + i, a[i].s) - e - 1;
dp[i] = max(dp[i - 1], dp[pos] + a[i].e - a[i].s);
}
cout << dp[n];
return 0;
}
注意:使用upper_bound可以快速找到符合条件的最后一节课。
2.5 T5:子数组乘积统计
统计所有子数组乘积为正、负、零的数量。
2.5.1 动态规划解法
定义dp[i][0/1/2]表示以a[i]结尾的子数组乘积为0/正/负的数量:
- 根据a[i]的值不同进行状态转移
- 最终结果是所有dp[i][...]的累加
2.5.2 代码实现
cpp复制#include<iostream>
using namespace std;
long long n, positive = 0, zero = 0, impositive = 0;
long long a[100005], dp[100005][3];
int main(){
cin >> n;
for (long long i = 1; i <= n; i++){
cin >> a[i];
if (a[i] > 0){
dp[i][0] = dp[i - 1][0];
dp[i][1] = dp[i - 1][1] + 1;
dp[i][2] = dp[i - 1][2];
}
else if (a[i] < 0){
dp[i][0] = dp[i - 1][0];
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 1][1] + 1;
}
else{
dp[i][0] = i;
dp[i][1] = 0;
dp[i][2] = 0;
}
impositive += dp[i][2];
zero += dp[i][0];
positive += dp[i][1];
}
cout << impositive << ' ' << positive << ' ' << zero;
return 0;
}
提示:注意处理a[i]为0时的特殊情况。
2.6 T6:倒牛奶问题
通过倒牛奶操作使得某个桶中的牛奶量恰好为v/2。
2.6.1 BFS解法
- 使用BFS遍历所有可能的状态
- 每个状态记录三个桶中的牛奶量
- 每次操作有6种可能的倒法
- 使用三维数组记录访问过的状态
2.6.2 代码实现
cpp复制#include<bits/stdc++.h>
using namespace std;
struct node{
int x, y, z, step;
};
queue<node> q;
bool v[205][205][205];
int z, y, x;
int bfs(){
q.push({x, 0, 0, 0});
v[x][0][0] = 1;
while (!q.empty()){
node n1 = q.front(); q.pop();
if (n1.x == x / 2 || n1.y == x / 2 || n1.z == x / 2){
return n1.step;
}
node n2;
// 所有可能的倒法
int tmp = min(y - n1.y, n1.x);
n2 = {n1.x - tmp, n1.y + tmp, n1.z, n1.step + 1};
if (!v[n2.x][n2.y][n2.z]){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
// 其他5种倒法类似...
}
return -1;
}
int main(){
cin >> x >> y >> z;
if (x % 2){
cout << -1;
return 0;
}
cout << bfs();
return 0;
}
注意:需要处理v不是偶数的情况,此时直接返回-1。
3. 算法竞赛经验分享
在实际编程中,我发现以下几点特别重要:
-
数学推导能力:很多看似复杂的问题都可以通过数学推导简化为更高效的计算方式,如T1和T2。
-
预处理技巧:像T3中预处理质因数的技巧可以大大提高后续计算的效率。
-
状态转移的清晰定义:动态规划问题的关键在于明确定义状态和转移方程,如T4和T5。
-
边界条件的处理:特别是取模运算和数组越界等问题,需要格外小心。
-
空间复杂度的优化:对于BFS问题,合理设计状态表示可以节省大量内存。
这些题目虽然各有特点,但都体现了算法竞赛中常见的问题解决思路。通过不断练习和总结,相信大家都能在算法能力上有显著提升。