思想歸納

1:當急需一個數據結構,可以在無序的數列中查詢第一個大於等於(或其他)一個數值(或其他)的元素,可以用線段樹實現(^-^)。假如把序列分成兩個區域P1和P2,那麼如果存在一個大於等於x的元素,則這個元素一定會出現在P1或者P2中,線段樹的工作就是查詢P1和P2的最大值,如果P1的最大值大於等於x,那麼就可以縮小搜索的範圍,就在P1中找就行了,同理,如果P1的最大值小於x,P2的最大值大於等於x,那麼就在P2中找。以此類推,不斷地縮小搜索範圍,就可以找出元素的下標。查詢複雜度O(log(n)),建樹複雜度O(n)。mx[p]存的是區間p的最大值。

void build(int p=1,int l=1,int r=n){
	if(l==r){mx[p]=A[l];return;}
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	mx[p]=max(mx[p<<1],mx[p<<1|1]);
}
void query(int x,int p=1,int l=1,int r=n){
	if(l==r)return l;
	int mid=l+r>>1,ans=-1;
	if(mx[p<<1]>=x)ans=query(x,p<<1,l,mid);
	else if(mx[p<<1|1]>=x)ans=query(x,p<<1|1,mid+1,r);
	return ans;
}

上面的線段樹只能在[1,n]的範圍內尋找第一個滿足某條件的下標,當如果要在一段區間[a,b]內尋找第一個滿足條件的下標時,那麼就要升級一下線段樹,mx[p]存的p區間最大值的下標。

void build(int p=1,int l=1,int r=n){
	if(l==r){mx[p]=l;return;}
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	mx[p]=(A[mx[p<<1]]>=A[mx[p<<1|1]])?mx[p<<1]:mx[p<<1|1];
}
int query(int a,int b,int x,int p=1,int l=1,int r=n){
	if(l>b||r<a)return 0;
	if(l==r)return l;
	int mid=l+r>>1,ans=0;
	if(mx[p<<1]>=x)ans=query(a,b,x,p<<1,l,mid);
	if(!ans&&mx[p<<1|1]>=x)ans=query(a,b,x,p<<1|1,mid+1,r);
	return ans;
}

2:如果一個問題的點,滿足在某種條件下進行跳躍,每個點跳躍一次後都能轉換成一個固定的點,並且當問題需要知道一個點跳躍多次後的點時,可以用倍增法進行點的快速跳躍。用F[i][j]來表示j這個點跳2^i步後的點,所以只需求出每個點的F[0][j],就可以預處理出每個點跳2^i後的點。

for(int i=1;i<S;i++)for(int j=1;j<=n;j++)F[i][j]=F[i-1][F[i-1][j]];//預處理

當我們要跳step步時,可以用二進制判斷step&(1<<i)是否爲真,若爲真則讓這個點跳躍2^i步,就令x=F[i][x]。

for(int i=0;i<S;i++)if(step&(1<<i))x=F[i][x];//運用

也可以用位運算更快速地求出跳step步後的點。

while(step){//效率更高地運用
	int op=step&-step;
	op=mp[op];
	x=F[op][x];
	step^=op;
}

3:當一個問題中需要用到一種需要修改的前綴和時,可以用樹狀數組來維護。可以說樹狀數組是可以單點修改的前綴和,那麼當遇到需要修改的前綴和時,就能用樹狀數組實現log(n)的修改,log(n)的查詢。樹狀數組上的每個節點存的不是一個點的信息而是一段區間的信息,把數組的下標轉換爲二進制數來表示,一個下標管理的區間長度是2^(二進制數的末尾0的個數),例如1000管理的長度是8,10110管理的長度是2,對於一個下標i,i管理的區間是i-2^(二進制數i末尾0的個數)+1到i,這樣就可以實現高效的修改與查詢,修改前綴和時每次讓i+=i&-i,查詢時讓i-=i&-i,因此查詢與修改都最多在log(n)的時間內完成,這樣樹狀數組的速度就會比線段樹更快(但是樹狀數組能夠實現的線段樹一定能夠實現...)。

void update(int *A,int i,int x){//將數組下標從i開始的數組下標都加上x
	while(i<=n){
		A[i]+=x;
		i+=i&-i;
	}
}
int query(int *A,int i){//查詢下標爲[1,i]區間內的數組權值之和
	int res=0;
	while(i){
		res+=A[i];
		i-=i&-i;
	}return res;
}

同時,樹狀數組也可以實現區間修改以及區間查詢。與線段樹不同的是,線段樹的區間查詢需要稍微修改數據結構,而樹狀數組的區間查詢只需換一種使用的方法,就可以實現區間的修改。當我們需修改[L,R]區間內的前綴和數組時,分別對[1,L-1],[L,R],[R+1,n]區間進行分析:

(1)在[1,L-1]區間內的前綴和不變。

(2)在[L,R]區間內下標爲i的數組,前綴和的增加量=(i-l+1)*x=i*x-(l-1)*x

(3)在[R+1,n]區間內的所有數組,前綴和的增加量=(r-l+1)*x=r*x-(l-1)*x;

在(2)中,由於[L,R]中的i不斷在變,所以不能夠處理出所有的i的前綴和變化量。於是可以用bit0和bit1分別表示前綴和變化量的隨i變化而變化的量不變的量。由(2)和(3)推出的公式說明查詢i時只要query(bit1,i)*i+query(bit0,i)就行了。

/*查詢*/ 
ll res=0;
res+=query(bit0,R)+query(bit1,R)*R;
res-=query(bit0,L-1)+query(bit1,L-1)*(L-1);
cout<<res<<endl;
/*修改*/ 
add(bit0,L,-x*(L-1));
add(bit0,R+1,x*R);
add(bit1,L,x);
add(bit1,R+1,-x);

4:如果一個查詢類的問題,對於每次查詢,都要二分一個權值,然後check()一下該值是否可行,如果這樣就可以考慮一下是否能將查詢進行離線,然後把二分寫在外面(比如循環16次),讓check和查詢數組的更新同時進行,就可以大大地縮小算法的複雜度。對於每次的循環,都將查詢數組的mid進行排序,對於每次check(i),都將mid[j]==i的詢問更新一下L,R,mid和ans,然後j++,如果L已經大於R就直接跳過當前詢問。這樣check(i)的i和查詢數組的j同時在變,一次循環的複雜度就爲O(n+m)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章