比賽中常用的技巧——關於二分、倍增
二分
先舉一個例子:肯定很多人小的時候都玩過猜數字這個遊戲,那麼現在問題來了,如果在1-1000中去進行猜數字的遊戲,那麼我們最多要猜多少次,一些心急的人會說到1000次,如果按照順序的方法去進行遊戲的話,顯然是1000次的,但是我們在實際操作中,真的有按照從1到1000的順序去進行猜數字嗎?肯定是不會的(除非你傻)。因為我們每一次得到的回答都是我們詢問的數字與答案之間的大小關係,所以如果我們按照這個思路去進行二分的話,在最壞的情況下至少會猜10次,也就是log21000;具體的方法請看下面的程式碼。
int middle(void) { int l=1;r=1001; while (l+1<r) { mid=(l+r)/2; if (mid<=ans) r=mid; else l=mid+1; } return l; }
我們可以把二分的模型抽象的看成是一個長度為n的01陣列,那麼我們可以發現這個01陣列一定是單調遞增的:000000000000011111111111……我們每一次通過二分可以訪問一個位置,我們要求第一個1的下標,那麼問我們最少要去訪問幾次,那麼很顯然時間複雜度就是O(k log n)的,k是每次訪問的代價。
於是乎例子的問題就可以轉換為是一個長度1000的陣列,如果下標≤答案的就填成是0,而>答案的就填充為1,那麼這個陣列是單調遞增的,而每次訪問的代價是常數的時間。
從這裡我們就可以發現了二分的作用就是將時間複雜度優化,他可以將一個O(n)的演算法變成是O(log n)的,他實現的思想就是每一次都把答案的範圍縮小一半,知道最後只剩下一個答案為止。同時,二分是符合單調性的。
我們通過二分答案,花費一個log的代價,將最優化的問題轉化成了判定性的問題(這裡有一點概念性)。
我們看一下二分的經典問題:有n個數字,要求分成相鄰的k組,使得每組數字之和的最大值最小,保證答案不超過10的9次方。(每個數都為非負整數)
我一眼看下來,完全看不到正解(我太菜了),於是我果斷打了一個暴力。
#include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> using namespace std; int maxx,n,k,a[1010]; bool tf[1010]; void dg(int x,int y) { if (x==n+1 && y!=k-1) return; if (y==k-1) { int tot=0,ans=0; for (int i=1;i<=n;i++) { tot+=a[i]; if (tf[i]==true) { if (tot>ans) { ans=tot; } tot=0; } } if (tot>ans) ans=tot; if (ans<maxx) maxx=ans; } else { tf[x]=true; dg(x+1,y+1); tf[x]=false; dg(x+1,y); } } int main() { scanf("%d%d",&n,&k); for (int i=1;i<=n;i++) scanf("%d",&a[i]); maxx=2147483647; dg(1,0); printf("%d",maxx); system("pause"); return 0; }
顯然這種暴力做法的時間複雜度是十分高的,如果不是資料隨機或者很水的情況下,我們很難拿得到分。而這道題恰巧可以用二分去做,我們把二分轉化為另外一個問題,能否使得每組數字之和最大值是小於x的,那麼這個問題貪心就可以了,這道題是滿足單調性的,因為如果滿足答案小於x,那麼答案顯然會小於x+1,所以我們可以二分找出最小的x,這個就是最終的答案。程式碼如下:
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
int maxsum;
int tot,ans;
int n,k;
int a[1010];
int tt;
int middle(void)
{
int l=1,r=maxsum,mid;
while (l<r)
{
mid=(l+r)/2;
tot=0;ans=0;
for (int i=1;i<=n;i++)
{
if (tot+a[i]<=mid) tot=tot+a[i];
else
{
ans++;
tot=a[i];
}
}
ans=ans+1;
if (ans<=k) r=mid;
else l=mid+1;
}
return l;
}
int main()
{
scanf("%d%d",&n,&k);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
maxsum=maxsum+a[i];
}
tt=middle();
printf("%d",tt);
system("pause");
return 0;
}
同樣我們也可以把這題轉換成一個抽象的模型,這是一個長度為maxsum的陣列,當下標≤ans的時候為0,當下標>ans的時候為1,符合單調性,找到第一個1的代價是O(n),這道題的總複雜度就變成了O(n log S)。
倍增
首先我們瞭解一下什麼是倍增,倍增的思想就是先預處理一個倍增陣列,然後進行二進位制分解之類的東西。他可以把時間複雜度從O(n)變成是O(log n)的。
倍增的應用主要是快速冪,用O(log n)的時間計算x^n,預處理出x^1,x^2,x^4,x^8,x^16.(這裡假設每個人都會倍增的思想,如果有不會的人可以先去看看別的資料),將n進行二進位制分解,就舉個例子吧,如果n=21,那麼21=16+4+1對吧,那麼x^n就等於x^16*x^4+x^1沒錯吧。那麼這個時候的時間複雜度就變成了O(log n)了。
倍增還可以應用於Sparse Table(什麼東西?!),又叫做稀疏表。他是用來維護RMQ(又稱區間最值)。給出一個長度為n的陣列,每一次詢問區間的最大值,沒有修改。那麼我們是不是就可以預處理從每個數開始,往後1個數字,2個數字,4個數字(以此類推)的最大值呢,我們把預處理的答案記為F陣列,那麼F[i][j]就表示從第i個數開始,往後2^j個數字的最大值,這裡用了O(nlogn)的時間去預處理,當我們詢問區間[L..R]的時候,設k為[1<<(R-L+1)],答案就是max(f[L][k],f[R-2^k+1][k]),我們再舉個例子吧,對於四個數2,3,1,4;我們要查詢[1..3]的區間最大值對吧,那麼很顯然答案就是max(f[1][1],f[2][1]);那麼答案就是3;再看看比如我們要查詢[2..4]的最大值,答案就是max(f[2][1],f[3][1]),也就是4。具體怎麼操作的話看下錶。
F[i][j] |
1 |
2 |
3 |
4 |
0 |
2 |
3 |
1 |
4 |
1 |
3 |
3 |
4 |
4 |
2 |
4 |
4 |
4 |
4 |
我們看一下用倍增優化的具體的時間複雜度吧!如果是暴力的話那麼預處理就是O(n)的,每一次詢問也是O(n)的,這是比較簡單的。如果是線段樹的話預處理的時候時O(n)的,然後每一次詢問用了O(log n)的時間,可是碼量比較大(對於我這種蒟蒻來說),碼力不太夠啊!還有一種就是ST表,每一次預處理是O(n log n)的,然後單詞詢問是O(1)的,實現比較簡單吧!
還有一種倍增我不太熟悉,那就是樹上倍增求LCA,因為本身我LCA就學的不好,準確點沒學。那麼這種倍增的時間複雜度預處理的話是O(nlogn),然後單詞查詢是O(logn)的,對於這種有根樹,我們預處理出每個點上向上走1步,2步,4步(以此類推)走到的點,記為F陣列。預處理出每一個點的深度(根節點的深度為0)。假設我們詢問U,V兩點的LCA的話。我們可以調整兩點的深度相等,不妨設U深度神,設k=d[u]-d[v],然後將k二進位制分解,向上走最多log次,然後從大到小去試探,得到LCA;就像下圖一樣。
下面放一放一段程式碼:
void dfs(int u)
{
if (v in child[u])
{
d[v]=d[u]+1;
dfs(v);
}
}
voidpreprocess(void)
{
for (v in (1,n)) f[v][0]=fa[v];
for (i in (1,logn))
{
for (v in (1,n))
{
f[v][i]=f[f[v][i-1]][i-1];
}
}
}
intlca(int u,int v)
{
if (d[u]<d[v]) swap(u,v);
for (i in (0,logn))
{
if ((1<<i) &(d[u]-d[v])) u=f[u][i];
}
if (u==v) return u;
for (i in (logn,0,-1))
{
if (f[u][i]!=f[v][i])
{
u=f[u][i];
v=f[v][i];
}
}
return fa[u];
}
別調了,這是虛擬碼!!!
由於真的太菜,所以每天改題都用了很多的時間,倍增和簡單分塊來不及打了,以後會更新。因為本文是一個蒟蒻寫的,再加上沒有人幫我驗程式碼,所以將就著看一看,理解一下思路就可以了,如果有大神發現我哪裡寫錯了,歡迎來拍我一巴掌!!!!!感謝phx大犇給予了使我恍然大悟的言論,謝謝phx同學!!!!!!!!!