作为一名参加过多次算法竞赛的老兵,我深知寒假是提升算法能力的黄金时期。这次集训的六道题目涵盖了数论、动态规划、贪心算法等核心知识点,都是非常典型的竞赛题型。下面我将逐题解析解题思路和实现细节,分享一些我在实战中总结的技巧。
给定正整数n,计算∑(i=1到n)⌊n/i⌋的值。直接暴力计算的O(n)复杂度在n较大时(如1e12)会超时。
这道题需要用到数论分块(整除分块)的技巧。观察发现⌊n/i⌋的值会形成多个连续的区间,在每个区间内⌊n/i⌋的值相同。我们可以找到每个值对应的区间范围,批量计算贡献。
关键公式:
cpp复制#include<bits/stdc++.h>
using namespace std;
int main() {
long long n,sum=0;
cin>>n;
for(long long i=1;i<=n;i++){
long long v=n/i;
long long r=n/v;
sum+=v*(r-i+1);
i=r;
}
cout<<sum;
return 0;
}
使用数论分块后,时间复杂度从O(n)降低到O(√n),可以轻松处理n=1e12的情况。
提示:数论分块的关键在于理解⌊n/i⌋的值会形成O(√n)个连续区间。这个技巧在莫比乌斯反演等问题中也非常常见。
给定数组a,计算所有子数组的a[i]*a[j]之和(i<j)。直接枚举所有子数组的O(n²)方法无法通过大数据。
我们可以将问题转化为数学公式:
∑(i<j)a[i]a[j] = 1/2 * [(∑a[i])² - ∑a[i]²]
证明:
(∑a[i])² = ∑a[i]² + 2∑(i<j)a[i]a[j]
因此 ∑(i<j)a[i]a[j] = [(∑a[i])² - ∑a[i]²]/2
cpp复制#include<bits/stdc++.h>
using namespace std;
long long a[100005];
long long s,s2;
int main() {
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
s=0,s2=0;
for(int i=1;i<=n;i++){
s=(s+a[i])%1000000007;
s2=(s2+a[i]*a[i]%1000000007)%1000000007;
}
long long ans=(s*s%1000000007-s2+1000000007)%1000000007;
ans=ans*500000004%1000000007; // 乘以1/2的模逆元
cout<<ans<<endl;
return 0;
}
给定数字n,每次操作可以:1) n减1;2) 如果n能被m整除,可以n除以m。求将n变为1的最少操作次数。
采用动态规划预处理1到1e6的所有解:
优化点:
cpp复制#include<bits/stdc++.h>
using namespace std;
vector<int> a[1010001];
int g[1100101],dp[1000100];
int main(){
int t;
cin>>t;
// 筛法预处理质因数
for(int i=2;i<=1000000;i++){
if(!g[i]){
for(int j=i;j<=1000000;j+=i){
g[j]=1;
a[j].push_back(i);
}
}
}
// DP预处理
memset(dp,0x3f,sizeof(dp));
dp[1]=0;
for(int i=2;i<=1000000;i++){
for(int j=0;j<a[i].size();j++){
dp[i]=min(dp[i],dp[i/a[i][j]]+1);
}
dp[i]=min(dp[i],dp[i-1]+1);
}
// 处理查询
while(t--){
int s;
scanf("%d",&s);
cout<<dp[s]<<endl;
}
}
预处理时间复杂度O(n log log n),查询O(1),可以高效处理大量查询。
选择若干不重叠的课程,使得总上课时间最长。这是经典的活动选择问题的变种。
贪心策略:按结束时间排序,每次选择结束时间最早且不与已选课程重叠的课程。
动态规划解法:
cpp复制#include<bits/stdc++.h>
using namespace std;
struct contest{
int a,b;
};
contest aa[100005];
long long ed[100005],dp[100005];
bool cmp(contest x,contest y) {
return x.b<y.b;
}
int main() {
int n;
cin >> n;
for (int i=1;i<=n;i++){
cin>>aa[i].a>>aa[i].b;
}
sort(aa+1,aa+n+1,cmp);
for(int i=1;i<=n;i++){
ed[i]=aa[i].b;
}
for(int i=1;i<=n;i++){
int pos=upper_bound(ed+1,ed+i,aa[i].a)-ed-1;
dp[i]=max(dp[i-1],dp[pos]+aa[i].b-aa[i].a);
}
cout<<dp[n];
return 0;
}
贪心选择性质:每次选择结束时间最早的课程,可以留下更多时间给后续课程。可以通过交换论证证明其最优性。
统计所有非空子数组乘积为负、正、零的数量。元素只能是-1、0、1。
乘积性质由负数的奇偶性和是否存在零决定:
维护三个状态:
状态转移根据当前元素值决定。
cpp复制#include<iostream>
using namespace std;
long long n, positive = 0, zero = 0, negative = 0;
long long a[100005], dp[100005][5];
int main(){
cin >> n;
for (long long i = 1; i <= n; i++){
cin >> a[i];
}
for (long long i = 1; i <= n; 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];
}
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;
}
if (a[i] == 0){
dp[i][0] = i;
dp[i][1] = 0;
dp[i][2] = 0;
}
negative += dp[i][2];
zero += dp[i][0];
positive += dp[i][1];
}
cout << negative << ' ' << positive << ' ' << zero;
return 0;
}
时间复杂度O(n),空间复杂度O(n),可以优化到O(1)空间。
三个杯子容量为X,Y,Z,初始X满。每次可以将一个杯子的水倒入另一个杯子直到倒满或倒空。求最少操作使某两个杯子各有X/2水。
广度优先搜索(BFS)探索所有可能状态:
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] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
tmp = min(z - n1.z, n1.x);
n2 = {n1.x - tmp, n1.y, n1.z + tmp, n1.step + 1};
if (v[n2.x][n2.y][n2.z] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
tmp = min(x - n1.x, n1.y);
n2 = {n1.x + tmp, n1.y - tmp, n1.z, n1.step + 1};
if (v[n2.x][n2.y][n2.z] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
tmp = min(z - n1.z, n1.y);
n2 = {n1.x, n1.y - tmp, n1.z + tmp, n1.step + 1};
if (v[n2.x][n2.y][n2.z] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
tmp = min(x - n1.x, n1.z);
n2 = {n1.x + tmp, n1.y, n1.z - tmp, n1.step + 1};
if (v[n2.x][n2.y][n2.z] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
tmp = min(y - n1.y, n1.z);
n2 = {n1.x, n1.y + tmp, n1.z - tmp, n1.step + 1};
if (v[n2.x][n2.y][n2.z] == 0){
q.push(n2);
v[n2.x][n2.y][n2.z] = 1;
}
}
return -1;
}
int main(){
cin >> x >> y >> z;
if (x % 2){
cout << -1;
return 0;
}
cout << bfs();
return 0;
}
这次集训的六道题目涵盖了算法竞赛中的多个重要知识点。在实际比赛中,我有以下几点经验分享:
这些题目在各大OJ上都有类似题型,建议读者可以找相关题目进行巩固练习。我在实际比赛中发现,对经典算法的深刻理解和灵活运用往往比知道更多算法更重要。