1. 程式人生 > 實用技巧 >luogu P6373 【「StOI-1」IOI計數】

luogu P6373 【「StOI-1」IOI計數】

官方題解對於思路的講述已經非常的清晰了.

分享一個個人心得:

線段樹對於左右區間的運用

線段樹主要就是妙在它的合併區間功能以及對於一個整塊進行分治的思想.你會發現,每次線段樹的更新基本上都是基於左右兒子這兩個左右區間的.對於可以區間維護的資訊(基本上也就是可以分裂成子問題的問題),我們總是每次更新後就遞歸回溯根據當前點的左右兒子節點維護資訊.線段樹最妙的地方正是在於此處,不管是合併資訊(合併答案),或者是分治處理資訊(修改 or 查詢),實際上我們都是得用到左右區間的.

例如這個題目:

要求我們維護一段區間內"IOI"的個數,實際上我們可以把問題轉化為左右兩個區間來解決.假設我們要查詢的區間是\([L,R]\)

,我們可以先處理\([L,mid]\)的答案以及\([mid+1,R]\) 的答案.

同時我們發現有一部分答案是橫跨了左右兩個區間的,這時候我們就可以處理出左邊的"I"的數量以及"IO"的數量(顯然左區間的"OI"對於這種橫跨左右兩邊的答案沒有貢獻),同時也處理出右區間"OI"、"I"的數量,這時候我們根據乘法原理(具體可以看官方題解)遞迴處理問題即可

而線段樹是可以很好的適應這個問題的,因為其實線段樹運用的也是分治的思想,它剛好也有左右區間,而且可以維護區間資訊,並且有著優秀的\(logn\)(儘管常數比較大......)的時間複雜度,所以線段樹就可以解決這道題目了.

貼上這道題的AC程式碼:

Code

#include <bits/stdc++.h>
using namespace std;
#define int long long 
int n,Q;
char A[500005];
int a[500005];

struct node{
	
	int i,o,oi,io,ioi,l,r;
	void clean()
	{
		i = o = io = oi = ioi = 0;
		return ;
	}
	
}T[2000005];

node updata(node x,node l,node r)
{
	x.oi = l.o * r.i + l.oi + r.oi;
	x.io = l.i * r.o + l.io + r.io;
	x.i = l.i + r.i;
	x.o = l.o + r.o;
	x.ioi = l.i * r.oi + l.io * r.i + l.ioi + r.ioi;
	return x;
}

void build_tree(int x,int l,int r)
{
	T[x].clean();
	T[x].l = l , T[x].r = r;
	if(l == r)
	{
		if(a[l] == 0)T[x].o = 1;
		else T[x].i = 1;
		return ;
	}
	int mid = (l + r) >> 1;
	build_tree(x << 1 , l , mid);
	build_tree(x << 1 | 1 , mid + 1 , r );
	T[x] = updata(T[x],T[x << 1] , T[x << 1 | 1]);
	return ;
}

node getans(int x,int l,int r)
{
	if(T[x].l >= l && T[x].r <= r)return T[x];
	int mid = (T[x].l + T[x].r) >> 1;
	node Sum ;
	Sum.clean();
	if(l <= mid)Sum = updata(Sum,Sum,getans(x << 1, l , r));
	if(r  > mid)Sum = updata(Sum,Sum,getans(x << 1 | 1, l , r ));
	return Sum;
}

void change(int x,int pos,int k)
{
	if(T[x].l == T[x].r && T[x].l == pos)
	{
		if(k == 1)T[x].i = 1, T[x].o = 0;
		else T[x].i = 0 , T[x].o = 1;
		return ;
	}
	int mid = (T[x].l + T[x].r) >> 1;
	if(pos <= mid)change(x << 1 , pos , k);
	else change(x << 1 | 1 , pos , k );
	T[x] = updata(T[x],T[x << 1] , T[x << 1 | 1]);
	return ;
}

signed main()
{
	cin >> n >> Q;
	cin >> A;
	for(int i = 0 ; i < n ; i ++)
	{
		if(A[i] == 'I')
		a[i + 1] = 1;
		else a[i + 1] = 0;
	}
	build_tree(1 , 1 , n);
	while(Q)
	{
		int op,l,r;
		char pos;
		cin >> op >> l;
		if(op == 2)
		{
			cin >> r;
			cout << getans(1,l,r).ioi << endl;
		}
		else 
		{
			cin >> pos;
			if(pos == 'I')
			change(1,l,1);
			else change(1,l,0);
		}
		Q--;
	}
	return 0;
}

小結

線段樹的題目我們不應該一開始就直接線段樹暴力搞,而是應該先分析問題為什麼能用線段樹做,線段樹做這道題優越性在哪裡,這樣子收穫也許會多一點,也可以加深自己對於線段樹的理解,線段樹不僅僅是一個數據結構,或許也可以說成是一種思想