这道题目要求计算从1到n的所有⌊n/i⌋之和。直接暴力计算的时间复杂度是O(n),当n很大时(比如1e12)会非常低效。我们需要更聪明的数学方法——数论分块。
数论分块的核心观察是:对于i∈[1,n],⌊n/i⌋的值会形成若干个连续的区间。例如当n=10时:
每个区间的右端点可以通过r = n/(n/i)计算得到。这样我们就能把O(n)的计算优化到O(√n)。
关键技巧:对于每个区间[l,r],贡献值为(r-l+1)*(n/l)。这样我们只需要遍历所有不同的n/i值区间即可。
这道题要求计算所有无序数对(ai,aj)的差的平方之和。直接双重循环的O(n²)解法显然不适用于大数据量。
我们可以通过数学展开来优化:
∑(ai-aj)² = ∑(ai² - 2aiaj + aj²) = n∑ai² - 2∑ai∑aj
利用前缀和数组可以高效计算:
这样对于每个i,它对答案的贡献就是:
(i-1)ai² - 2ai*s[i-1] + s2[i-1]
这是一个典型的动态规划问题。我们需要为每个数字计算变成1的最小操作次数。
定义dp[i]为将i变成1的最小操作次数。转移方程有两种情况:
预处理质数可以使用埃拉托斯特尼筛法,同时记录每个数的最小质因数,这样可以在O(n log log n)时间内完成预处理。
这是经典的区间调度问题,但目标是最优化总时长而非课程数量。
解法步骤:
这道题需要统计乘积为负、正、零的子数组数量。直接枚举所有子数组的O(n²)方法不够高效。
我们可以使用动态规划:
转移规则根据当前元素的值有所不同:
这是一个状态空间搜索问题,可以使用BFS来探索所有可能的倒奶状态。
关键点:
cpp复制#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ll n, ans = 0;
cin >> n;
for(ll l = 1, r; l <= n; l = r + 1) {
ll val = n / l;
r = n / val;
ans += (r - l + 1) * val;
}
cout << ans;
return 0;
}
优化点:
cpp复制#include<bits/stdc++.h>
using namespace std;
const int MOD = 1e9+7;
int main() {
int n; cin >> n;
vector<long long> a(n+1), s(n+1), s2(n+1);
for(int i=1; i<=n; i++) {
cin >> a[i];
s[i] = (s[i-1] + a[i]) % MOD;
s2[i] = (s2[i-1] + a[i]*a[i]) % MOD;
}
long long ans = 0;
for(int i=1; i<=n; i++) {
long long term1 = (i-1) * (a[i]*a[i] % MOD) % MOD;
long long term2 = 2 * a[i] % MOD * s[i-1] % MOD;
long long term3 = s2[i-1];
ans = (ans + term1 - term2 + term3 + MOD) % MOD;
}
cout << ans;
return 0;
}
注意事项:
cpp复制#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e6+5;
int dp[MAX];
vector<int> primes[MAX];
void sieve() {
for(int i=2; i<MAX; i++) {
if(primes[i].empty()) {
for(int j=i; j<MAX; j+=i) {
primes[j].push_back(i);
}
}
}
}
int main() {
sieve();
dp[1] = 0;
for(int i=2; i<MAX; i++) {
dp[i] = dp[i-1] + 1;
for(int p : primes[i]) {
dp[i] = min(dp[i], dp[i/p] + 1);
}
}
int T; cin >> T;
while(T--) {
int n; cin >> n;
cout << dp[n] << endl;
}
return 0;
}
优化技巧:
常见错误:
调试方法:
常见陷阱:
解决方案:
cpp复制// 正确写法示例
ans = (ans + term1) % MOD;
ans = (ans - term2 + MOD) % MOD;
ans = (ans + term3) % MOD;
当n很大时(1e6),需要注意:
为什么按结束时间排序是最优的?
如何验证DP转移的正确性?
当桶容量很大时(200),三维状态会占用很多内存。可以考虑:
| 题目 | 暴力解法 | 优化解法 |
|---|---|---|
| T1 | O(n) | O(√n) |
| T2 | O(n²) | O(n) |
| T3 | O(n²) | O(n log log n) |
| T4 | O(2ⁿ) | O(n log n) |
| T5 | O(n²) | O(n) |
| T6 | O(状态数) | O(状态数) |
在n=1e6量级时:
这个求和式实际上是除数函数d(n)的前缀和,即D(n)=∑d(i) for i=1..n,其中d(i)是i的正除数个数。数论分块技巧在计算数论函数前缀和时非常有用。
如果要求计算三维数组的差的平方和,可以类似展开:
∑(ai-aj-ak)² = ...
同样可以使用前缀和技巧优化,但需要更高维的前缀和数组。
如果不同操作有不同的代价(比如减1操作代价为2,除法代价为1),问题就变成了带权最短路问题,可以用Dijkstra算法解决。
如果有多个教室可用(即允许最多k节课时间重叠),问题就变成了带资源约束的区间调度,可以用贪心+堆的解法。
如果问题改为查询区间乘积在某个范围内的子数组数量,可以使用前缀积+离散化+树状数组的方法。
如果有k个牛奶桶,状态空间会呈指数增长,这时需要更智能的搜索策略,如A*算法。