[HDU6304][數學] Chiaki Sequence Revisited
-杭電多校2018第一場G
題目描述
現在拋給你一個數列\(A\)
現在需要你計算它的前綴和 \(\sum\limits_{i=1}^{n}a_i \ mod \ (10^9+7)\)
數據范圍 \(n(1\le n\le 10^{18})\)
題目分析
不可描述的做法
拿到題目第一步對序列\(A\)打一個100的表,對吧,然后 "OEIS" 一下,發現真的有
\(a[1..] = {1,2,2,3,4,4,4,5,6,6,7,8,8,8,8,9,....}\)
結果發現並沒有什么用,既沒有通項公式,更別說求和公式了。
大概正確的規律
可以發現每種數字的出現次數是由規律可循的,\(1\)出現了\(1\)次,\(2\)出現了\(2\)次,\(3\)出現了\(1\)次
我們設\(f(x)\)代表數字\(x\)出現的次數
那么 \(f[1..] = {1,2,1,3,1,2,1,4,1,2,1,3,1,2,1,5,....}\)
通過觀察100項的表可以的出一個大概正確的規律,數字\(x\)會出現\(1+log_2lowbit(x)\)次
至於\(lowbit(x)\) 是啥,可以去了解一下樹狀數組,表示 \(\min\{2^k:2^k|x\}\)
經過總結規律可以發現遞推式,那么有
也就是 http://oeis.org/A001511 中提到的數列,但是知道出現次數並沒有什么用,我們需要計算的是\(f(x)\)
的前綴和,這樣我們就可以知道\(n\)位置上表示的數字的值,即\(a_n\)的值。
我們設 $g(n) = \sum^{n}_{i=1}f(i) $ ,可以簡單推導一下
也就是說我們現在可以在\(O(\log N)\)時間計算出\(g(n)\) ,由於該函數顯然是單調的,那么我們現在可以通過二分求得\(a_n\)對應的值,即 \(a_n = \min\{k\ |\ g(k)\ge n\}\)。
然而題目要我們求前綴和,那么問題來了,我們現在計算單個值就需要\(O(\log^2N )\)時間
如何計算出前綴和呢?考慮通過每個數字的出現次數入手。
由於每一行都相當於一個等差數列,現在的目標就是找到每一行的末項就好了。
也就是找到__最后一個小於\(a_n\)的值__,再用等差數列求和公式\(O(1)\)計算出每一行的值,最后所有行加起來就是答案的主要部分了。
會發現經過上面的計算所有等於\(a_n\)的項沒有計算入答案,我們只要計算出等於\(a_n\)的有多少項,最后再累加到答案,這道題就做完了。容易得到 \(a_n\)需要計算\(n-g(a_n-1)\)次。根據最開始我對數列的偏移,正確的答案還需要再+1。整體復雜度為\(O(\log^2N+logN)\)
注:計算\(a_n\)需要 \(O(\log^2N)\)時間,需要估計二分上下界,否則會超時。
無比准確的題解
以下是多校官方給的題解。
考慮這個數列的差分數列,除了個別項,本質就是:\(1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0,...\)。
可以觀測到,這個序列可以這么生成:一開始只有一個\(1\),\(1\)變成\(110\),\(0\)保持不變。迭代無窮多次后就是這個差分序列。
知道差分序列,可以應用阿貝爾變換,把\(a\)的前綴和搞成差分序列相關。不妨令差分序列是\(da\),那么\(a\)的前綴和$$s(n)=(n-1)\sum_{i=0}^{n-2}da(i) - \sum_{i=0}^{n-2}da(i)i + 1$$。
利用\(da\)的分形結構,很容易算出\(s(n)\)。
代碼Code
/*
[HDU6304][數學]
Chiaki Sequence Revisited
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MOD = 1e9+7;
const int inv2 = 500000004;
int T;
LL n;
LL calc(LL n) {
if(n<=1) return n;
else return calc(n/2)+n;
}
void solve() {
LL l=n/2-30,r=n/2+30,m,p=-1;
//需要預先估計上下界減少二分次數,否則會TLE.
while(l<=r) {
m = (l+r)/2;
if(calc(m)>n) r=m-1;
else l=m+1,p=m;
}
LL rest = ((n - calc(p))%MOD+MOD)%MOD;
LL ans = 0, s, t, e, k, c=1, x, y;
for(LL i=1;; i<<=1,c++) {
if(i>p) break;
x = i%MOD;
y = 2*i%MOD;
s = x;
k = ((p-i)/(2*i)+1)%MOD;
e = (y*(k-1)%MOD+i)%MOD;
ans = (ans+c*(s+e)%MOD*k%MOD*inv2%MOD)%MOD;
}
ans = (ans + rest*((p+1)%MOD)%MOD)%MOD;
printf("%lld\n",ans+1);
}
int main() {
scanf("%d",&T);
while(T--) {
scanf("%lld",&n);
n--; //偏移一項
solve();
}
return 0;
}