倍增
倍增字面上意思是:成倍地增加。當模擬一個過程時,一步一步進行太慢,考慮把模擬的步數二進制分解;經過一些預處理,每次可以模擬 \(2^i\) 步,從而達到優化復雜度的目的。
倍增主要模型有RMQ,LCA等。
例題
給出一個長度為 n 的環和一個常數 k,每次可以從第 i 個點跳到第 (i + k) mod (n+1) 個點,總共跳 m 次。第 i 個點的權值為 a[i],求 m 次跳躍的起點的權值之和 mod 1e9 + 7 。
數據范圍:$ 1 ≤ n ≤ 10^6 , 1 ≤ m ≤ 10^{18} , 1 ≤ k ≤ n , 0 ≤ a[i] ≤ 10^9 $
問題分析
這里顯然不能暴力模擬跳 m 次。因為 最大可到 \(10^{18}\) 級別,如果暴力模擬的話,時間承受不住。
所以就需要進行一些預處理,提前整合一些信息,以便於在查詢的時候更快得出結果。如果記錄下來每一個可能的跳躍次數的結果的話,不論是時間還是空間都難以承受。
倍增思想:每個數都可以表示成二進制的形式, 對於從每個點開始的 \(2^i\) 步,記錄一個 go[i][x] 表示第 x 個點跳 \(2^i\) 步之后的終點,而 sum[i][x] 表示第 x 個點跳 \(2^i\) 步之后能獲得的點權和。對於跳 \(2^i\) 步的信息,預處理的時候可以看作是先跳了 \(2^{i−1}\) 步,再跳了 \(2^{i−1}\) 步。
即有 \(sum[i][x] = sum[i-1][x]+sum[i-1][go[i-1][x]] ,且 go[i][x] = go[i-1][go[i-1][x]]\) 。
例如,從1到14的整個跳躍過程由 \(2^3 + 2^2 + 2^0\) 三步組成。也就是說,對於環上這 n 個位置,預處理出每一個位置向前跳 1, 2, 4, ... 次的位置,則必然能夠到達 m。
實現代碼
#include <bits/stdc++.h>
using namespace std;
const int mod = 1000000007;
int modadd(int a, int b) {
if (a + b >= mod) return a + b - mod; // 減法代替取模,加快運算
return a + b;
}
int vi[1000005];
int go[75][1000005]; // 將數組稍微開大以避免越界,小的一維盡量定義在前面
int sum[75][1000005];
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
scanf("%d", vi + i);
}
for (int i = 1; i <= n; ++i) {
go[0][i] = (i + k) % n + 1;
sum[0][i] = vi[i];
}
//int logn = 31 - __builtin_clz(n); // 一個快捷的取對數的方法
int logn = 65;
for (int i = 1; i <= logn; ++i) {
for (int j = 1; j <= n; ++j) {
go[i][j] = go[i - 1][go[i - 1][j]];
sum[i][j] = modadd(sum[i - 1][j], sum[i - 1][go[i - 1][j]]);
}
}
long long m;
scanf("%lld", &m);
int ans = 0;
int curx = 1;
for (int i = 0; m; ++i) {
if (m & (1 << i)) { // 參見位運算的相關內容,意為 m 的第 i 位是否為 1
ans = modadd(ans, sum[i][curx]);
curx = go[i][curx];
m ^= 1ll << i; // 將第 i 位置零
}
}
printf("%d\n", ans);
}
這題的 \(m≤10^{18}\) ,雖然看似恐怖,但是實際上只需要預處理出 65 以內的 i ,就可以輕松解決,比起暴力枚舉快了很多。用行話講,這個做法的時間復雜度是預處理 O(nlogm) ,查詢每次 O(logm) 。
倍增除了作為一種獨立的思想以外,還經常被應用到各種算法里面,例如 快速冪、LCA 和 RMQ 問題 。
總結: 這就是倍增預處理出以二的整數次冪為單位的信息:
*在遞推中,如果狀態空間很大,可以通過成倍增長的方式,只遞推出狀態空間在2的整數次冪的值作為代表。
*每個數都可以表示成二進制的形式,可用之前的求出的代表值拼成所需的值。
*要求這個遞推問題的狀態空間關於2的次冪具有可划分性。
注意:為了保證統計的時候不重不漏,一般預處理出左閉右開的點權和。
Luogu P1419 尋找段落
題目描述
給定一個長度為n的序列ai,定義a[i]為第i個元素的價值。現在需要找出序列中最有價值的“段落”。段落的定義是長度在[S,T]之間的連續序列。最有價值段落是指平均值最大的段落,段落的平均值=段落總價值/段落長度。
輸入輸出格式
輸入格式:
第一行一個整數n,表示序列長度。
第二行兩個整數S和T,表示段落長度的范圍,在[S,T]之間。
第三行到第n+2行,每行一個整數表示每個元素的價值指數。
輸出格式:
一個實數,保留3位小數,表示最優段落的平均值。
輸入輸出樣例
輸入樣例#1:
3
2 2
3
-1
2
輸出樣例#1:
1.000
數據范圍
對於30%的數據有n<=1000。
對於100%的數據有n<=100000,1<=S<=T<=n,-10000<=元素價值<=10000。
解題思路
可以看出所求問題的答案具有單調性,考慮二分答案,變為判定性問題。
對於可能的答案k,設b[i]=a[i]-k。
就是求b數組一段長度在s,t之間的和的最大值,判斷其是否大於等於0。求區間和的最大值可以用前綴和+單調隊列維護。
具體過程,貼一份洛谷題解的圖:
時間復雜度
假設數據范圍為A,則二分答案是O(logA)的,判斷一次用了前綴和和單調隊列,復雜度是O(n)的,總時間復雜度為O(nlogA)。
實現代碼
#include <bits/stdc++.h>
using namespace std;
int n,S,T,a[100005],q[100005];//q數組用來記錄前綴和的下標
double Sum[100005]; //前綴和記錄到第i個的減去平均值的和
int check(double m)
{
Sum[0]=0;
for(int i=1;i<=n;i++)
Sum[i]=Sum[i-1]+a[i]-m; //初始化差值的前綴和
int head=1,tail=0; //head為單調(上升)隊列的左端點,tail為右端點。
for(int i=1;i<=n;i++)
{
if(i>=S)
{
//一直減小單調隊列的右端點,隊列右端點處的位置其前綴和小於Sum[i-S]
//單調隊列 q[] 中存的是,假設區間的右端點是i,區間前面的左端點最小可行位置 i-S
while(head<=tail&&Sum[i-S]<Sum[q[tail]])
tail--; //維護隊列的右端點
q[++tail]=i-S; //對於每一個右邊的i,都將它的區間左端點初始化為 i-S;
}
//單調隊列的隊首存儲的區間左端點位置,小於滿足區間長度[S,T]范圍時,i位置的左端點最小值;
if(head<=tail&&i-q[head]>T)
head++;
//如果這一段區間內的和大於等於0,則證明有這個值可行,且可能由更大的平均值存在。
if(head<=tail&&Sum[i]-Sum[q[head]]>=0)
return 1;
}
return 0;
}
int main()
{
cin>>n>>S>>T;
for(int i=1;i<=n;++i)
{
cin>>a[i];
}
double l=-10005,m,r=10005;
while(l+1e-5<r) // 二分區間平均值的最大值,注意控制精度保留三位小數
{
m=(l+r)/2;
if(check(m))
l=m;
else
r=m;
}
cout <<fixed<<setprecision(3)<<l<< endl;
return 0;
}