یکی از پرکاربرد ترین داده ساختار های مورد استفاده در المپیاد کامپیوتر بی شک Segment Tree است. نوع درخت دودویی آن Full Binary Tree است.
با استفاده از این داده ساختار می توان جمع یک بازه از آرایه و بیشترین یا کمترین مقدار یک آرایه را بدست آورد.
هر راس این درخت بیانگر یک بازه از آرایه مورد پرسش است.
بچه های هر راس (اگر موجود باشد طبیعتاً دو تا است) بازه پدر خود را نصف می کنند یا به عبارت دیگر اگر بازه راسی
برای بهتر فهمیدن رابطه ی بین راس ها و بازه ها به شکل زیر نگاه کنید.
در هر کدام از این راس ها یک اطلاعات مشخصی از بازه متناظر آن راس در آرایه در دسترس است که بسته به استفاده ما از این داده ساختار می تواند متفاوت باشد. برای نمونه اگر دنبال جمع یک بازه از آرایه هستیم هر کدام از راس ها در خود مجموع بازه متناظرشان را ذخیره می کنند.
برای درک بیشتر از این قسمت به شکل زیر نگاه کنید که چطور هر راس مقداری را ذخیره کرده.
ارتفاع این درخت
برای شماره گذاری راس ها هم می توان به ریشه عدد یک داد و به بچه های راست و چپ هر راس به ترتیب
نحوه اجرای این الگوریتم برای انواع سوال های قابل حل با این داده ساختار تقریبا یکسان است و ما با استفاده از یکی از پرسش های مشهور این داده ساختار، به توضیح این الگوریتم می پردازیم.
در ابتدا به ما یک آرایه داده شده و در هر مرحله از ما می خواهند که یا مقدار یک عضو آرایه را تغییر دهیم و یا جمع یک بازه را گزارش دهیم.
برای حل ابتدا Segment Tree را از روی آرایه می سازیم. برای اینکار اول ساختار اصلی Segment Tree را می سازیم و بعد مقدار هر راس را برابر جمع مقدار بچه هایش می گذاریم و مقدار راس های تک عضوی را مقدار عضو متناظر می گذاریم.
void build(int u = 1, int ul = 0, int ur = n){
if(ur - ul < 2){
seg[u] = a[ul];
return;
}
int mid = (ul + ur) / 2;
build(u * 2, ul, mid);
build(u * 2 + 1, mid, ur);
seg[u] = seg[u * 2] + seg[u * 2 + 1];
}
مقدار تمام راس هایی که بازه آن ها شامل این عضو می شود را تغییر می دهیم. توجه کنید که تعداد این بازه ها حداکثر برابر ارتفاع درخت است چرا که هر طبقه از درخت، آرایه را افراز می کند بنابراین در هر طبقه باید مقدار حداکثر یک راس تغییر کند بنابراین اردر این عملیات
void update(int i, int x, int u = 1, int ul = 0, int ur = n){
seg[u] += x - a[i];
if(ur - ul < 2){
a[i] = x;
return;
}
int mid = (ul + ur)/2;
if(i < mid)
update(i, x, u * 2, ul, mid);
else
update(i, x, u * 2 + 1, mid, ur);
}
از روش بازگشتی استفاده می کنیم و در هر مرحله جمع بازه خواسته شده را با فرض روی راس u بودن بدست می آوریم. سه حالت برای بازه خواسته شده و بازه راس u وجود دارد. حالت اول این است که این دو بازه برابر باشند که خب جواب مقدار راس u است. حالت دوم زمانیست که بازه خواسته شده به صورت کامل در بازه یکی از بچه های راس u باشد که آنگاه جواب را در بچه ای که بازه خواسته شده در آن است پیدا می کنیم. حال آخر زمانیست که مقداری از بازه خواسته شده در بازه بچه چپ و بقیه آن در بازه بچه راست u باشد، بدین منظور به صورت بازگشتی ابتدا مقدار قسمتی از بازه خواسته شده که در بازه بچه چپ u است را بدست می آوریم و بعد مقدار قسمتی که در بازه بچه راست u است و بعد جمع این دو جواب است. برای بدست آوردن جمع بازه خواسته شده کافیست با همین روش از راس یک شروع کنیم.
برای فهم بهتر فرض کنید
اردر این عملیات
int sum(int l, int r, int u = 1, int ul = 0, int ur = n){
if(x >= ur || ul >= y)return 0;
if(x <= ul && ur <= y)return seg[u];
int mid = (ul + ur) / 2;
return sum(l, r, u * 2, ul, mid) + sum(l, r, u * 2 + 1, mid, ur);
}
فرض کنید در عملیات اول بجای تغییر یک مقدار، تغییر یک بازه مطرح باشد. به طور مثال به ما بگوید به بازه
void upd(int u, int ul, int ur, int x){
lazy[u] += x;
seg[u] += (ur - ul) * x;
}
void shift(int u, int ul, int ur){
int mid = (ul + ur) / 2;
upd(u * 2, ul, mid, lazy[u]);
upd(u * 2 + 1, mid, ur, lazy[u]);
lazy[u] = 0;
}
void increase(int l, int r, int x, int u = 1, int ul = 0, int ur = n){
if(l >= ur || ul >= r)return;
if(l <= ul && ur <= r){
upd(u, ul, ur, x);
return;
}
shift(u, ul, ur);
int mid = (ul + ur) / 2;
increase(l, r, x, u * 2, ul, mid);
increase(l, r, x, u * 2 + 1, mid, ur);
seg[u] = seg[u * 2] + seg[u * 2 + 1];
}
int sum(int l, int r, int u = 1, int ul = 0, int ur = n){
if(l >= ur || ul >= r)return 0;
if(l <= ul && ur <= r)return seg[u];
shift(u, ul, ur);
int mid = (ul + ur) / 2;
return sum(l, r, u * 2, ul, mid) + sum(l, r, u * 2 + 1, mid, ur);
}