楼上大佬都推式子,对于推不出来式子的蒟蒻(我),提供一种找规律的方法。
看到样例提示,开始打表找规律。
找到的规律:
-
发现 1 ∼ n 1 \sim n 1∼n 立方根向下取整后,是有规律的,会出现固定次数个1,固定次数个2,……,以此类推。
-
经过统计,发现立方根每一相邻数字出现的次数的差是一个公差为 6 6 6 的等差数列。
开始打表 ing…
int n = 1e3;
vector <int> v(n + 1);
// 求立方根
for(int i = 1; i <= n; i ++){
cout << (v[i] = (int)cbrt(1.0*i + 0.5)) << ' ';
}
cout << endl << endl;
// 求连续出现的次数
vector <int> cnt;
int t = 1;
for(int i = 2; i <= n; i ++){
if(v[i] == v[i - 1]) ++ t;
else{
cout << t << endl;
cnt.push_back(t);
t = 1;
}
}
cout << endl << endl;
// 输出连续出现的次数的差值
for(int i = 1; i < cnt.size(); i ++){
cout << cnt[i] - cnt[i - 1] << endl;
}
得到数据如下:
x
⊆
{
1
,
1000
}
,
⌊
x
1
3
⌋
=
{
1
,
1
,
1
,
1
,
1
,
1
,
1
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
2
,
3
,
3
,
3
,
3
,
3
,
3
,
3
,
3
,
3
,
3
⋯
}
x \subseteq \{1,1000\},\lfloor \sqrt{x^\frac{1}{3}} \rfloor=\{1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3\cdots\}
x⊆{1,1000},⌊x31⌋={1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3⋯}
其中相同数字连续出现的次数:
1 1 1 出现 7 7 7 次;
2 2 2 出现 19 19 19 次;
3 3 3 出现 37 37 37 次;
4 4 4 出现 61 61 61 次;
5 5 5 出现 91 91 91 次;
6 6 6 出现 127 127 127 次;
7 7 7 出现 169 169 169 次;
8 8 8 出现 217 217 217 次;
9 9 9 出现 271 271 271 次;
10 10 10 因为没有完全出现完,不计入统计。
相同数字连续出现的次数相邻项之间的差:
12
,
18
,
24
,
30
,
36
,
42
,
48
,
54
12,18,24,30,36,42,48,54
12,18,24,30,36,42,48,54
可以发现是一个公差为 6 6 6 的等差数列。
由此,可以先求出每个数字连续出现的次数。
vector <int> u; // u[i]表示第i个数字连续出现的次数
u.push_back(0); // 让下标从1开始
u.push_back(7);
for(int i = 2; i <= 10000; i ++){ // x最大1e12,1e12的立方根下取整=10000
u[i] = u[1] + (12 + (12 + (i - 1 - 1) * 6)) * (i - 1) / 2; // 等差数列求和公式:(首项+尾项)*项数/2,尾项(第n项)=首项+(n-1)*公差
}
然后对出现次数求前缀和。
vector <ll> sum(u.size()); // u的前缀和
for(int i = 1; i < u.size(); i ++){
sum[i] = sum[i - 1] + u[i];
}
接着,对于每个输入的 x x x 二分找到第一个大于等于x的前缀和,以确定第x个立方根是几。
int l = 1, r = maxn, ans = -1;
while(l <= r){
int mid = (l + r) >> 1;
if(sum[mid] >= x){
ans = mid;
r = mid - 1; // 继续向左找
}else l = mid + 1;
} // 循环结束后,ans表示第x个数的立方根是几
然后分三种情况:
- 连续立方根中第 x x x 个数正好是 1 1 1,直接输出 1 1 1 即可。
if(ans == 1){
cout << x << '\n'; // x的立方根数字是1时的答案就是1*x
}
- 第 x x x 个数正好是连续立方根出现的最后一次,此时可以直接计算,答案为 1 × u 1 + 2 × u 2 + ⋯ + a n s × u a n s 1 \times u_1 + 2 \times u_2 + \cdots + ans \times u_{ans} 1×u1+2×u2+⋯+ans×uans。
ll cnt = 0;
for(int i = 1; i <= ans; i ++) cnt += i * u[i];
cout << cnt << endl;
- 第 x x x 个数是连续立方根中间的数,此时采取分段计算的方法,答案为前面所有连续立方根的和,加上多出来立方根的和,即 1 × u 1 + 2 × u 2 + ⋯ + a n s × u a n s − 1 + a n s × ( x − s u m a n s − 1 ) 1 \times u_1 + 2 \times u_2 + \cdots + ans \times u_{ans-1} + ans \times (x - sum_{ans-1}) 1×u1+2×u2+⋯+ans×uans−1+ans×(x−sumans−1)。
ll cnt = 0;
for(int i = 1; i <= ans - 1; i ++) cnt += i * u[i];
cout << cnt + ans * (x - sum[ans - 1]) << endl;
发现这样每次计算量太大,会超时。
题目中说:保证给出的 x 1 ∼ x q x_1 \sim x_q x1∼xq 单调不降。所以可以采用一个 h h h 数组,存储情况 2 2 2 中公式的前缀和,即 h k = ∑ i = 1 k i × u i h_k=\sum\limits_{i=1}\limits^{k} i \times u_i hk=i=1∑ki×ui,后面接着计算时可以接着前面的,无需重复计算。
int k = 1; // 表示h数组该计算哪个位置了
if(ans == 1){
cout << x << '\n'; // x的立方根数字是1时的答案就是1*x
}else if(x == sum[ans]){ // 说明x正好是ans出现的最后一次,可以直接计算
// 答案=1*u[1] + 2*u[2] + ... + ans*u[ans],可以借助前面的h[i]简化计算
for(int i = k; i <= ans; i ++) h[i] = h[i - 1] + i * u[i];
if(k < ans) k = ans;
cout << h[ans] << '\n';
}else{ // 先直接计算前面整个的,后面多出来的单独计算
for(int i = k; i <= ans - 1; i ++) h[i] = h[i - 1] + i * u[i];
if(k < ans - 1) k = ans - 1;
cout << h[ans - 1] + ans * (x - sum[ans - 1]) << '\n'; // 分段计算
}
由于 x m a x = 1 0 12 x_{max}=10^{12} xmax=1012, ⌊ ( 1 0 12 ) 1 3 ⌋ = 1 0 4 \lfloor \sqrt{(10^{12})^\frac{1}{3}} \rfloor=10^4 ⌊(1012)31⌋=104,所以求 h h h 数组的循环最多只会执行 1 0 4 10^4 104 次,显然不会超时。
呼~终于做完了,贴个完整代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 10000;
void solve()
{
vector <ll> u; // u[i]表示第i个数字连续出现的次数
u.push_back(0); // 让下标从1开始
u.push_back(7);
for(int i = 2; i <= maxn; i ++){ // x最大1e12,1e12的立方根下取整=10000
u.push_back(7 + (12 + (12 + (i - 1 - 1) * 6)) * (i - 1) / 2); // 等差数列求和公式:(首项+尾项)*项数/2,尾项(第n项)=首项+(n-1)*公差
}
vector <ll> sum(u.size()); // u的前缀和
for(int i = 1; i < u.size(); i ++){
sum[i] = sum[i - 1] + u[i];
}
vector <ll> h(maxn + 1); // h[i]表示当询问的x=i时,输出的答案
int k = 1; // 表示h数组应该记录哪里了
int q;
cin >> q;
while(q --){
ll x;
cin >> x;
// 二分找到第一个大于等于x的前缀和,以确定第x个立方根是几
int l = 1, r = maxn, ans = -1;
while(l <= r){
int mid = (l + r) >> 1;
if(sum[mid] >= x){
ans = mid;
r = mid - 1; // 继续向左找
}else l = mid + 1;
} // 循环结束后,ans表示第x个数的立方根是几
if(ans == 1){
cout << x << '\n'; // x的立方根数字是1时的答案就是1*x
}else if(x == sum[ans]){ // 说明x正好是ans出现的最后一次,可以直接计算
// 答案=1*u[1] + 2*u[2] + ... + ans*u[ans],可以借助前面的h[i]简化计算
for(int i = k; i <= ans; i ++) h[i] = h[i - 1] + i * u[i];
if(k < ans) k = ans;
cout << h[ans] << '\n';
}else{ // 先直接计算前面整个的,后面多出来的单独计算
for(int i = k; i <= ans - 1; i ++) h[i] = h[i - 1] + i * u[i];
if(k < ans - 1) k = ans - 1;
cout << h[ans - 1] + ans * (x - sum[ans - 1]) << '\n'; // 分段计算
}
}
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
solve();
return 0;
}
谢谢大家!