graph TD
A["20 (1-4)"]
B["6 (1-2)"]
C["14 (3-4)"]
D["2 (1-1)"]
E["4 (2-2)"]
F["6 (3-3)"]
G["8 (4-4)"]
A --> B
A --> C
B --> D
B --> E
C --> F
C --> G
Segment Trees
Segment tree هي هيكل بيانات تسمح بمعالجة استعلامات النطاق والتحديثات على المصفوفات بكفاءة.
في هذا القسم يُفترض أن كل شيء مرقم ابتداءً من 1 ما لم يُذكر خلاف ذلك.
ما هي Segment Tree؟
هي شجرة ثنائية حيث يمثل كل node قيمة استعلام على مقطع (مصفوفة جزئية) من المصفوفة الأصلية. يتم بناء الشجرة وفق القواعد التالية:
- كل leaf node يمثل مقطعًا بطول \(1\).
- كل internal node يمثل دمجًا ((sum, min, max, etc.)) لطفليه.
لنفترض أن لدينا المصفوفة [2, 4, 6, 8]. فإن Segment Tree تحسب جمع لنطاقات مبنية على هذه المصفوفة تكون كما يلي:
لاحظ أن جميع ال leaf nodes يمثلون عنصرًا واحدًا فقط، بينما تقوم internal nodes بدمج طفليها بأخذ مجموع قيمتيهما.
عدد الـnodes والحشو
ليكن N عدد الـleaf nodes في segment tree، وn طول المصفوفة الأصلية.
عدد الـnodes
- في
segment tree، يشكل عدد nodes في كل مستوى المتتالية[1, 2, 4, …]، أي قوى العدد اثنين.
- كل قوة للعدد 2 تساوي مجموع جميع القوى السابقة للعدد 2 زائد 1.
- لذلك، فإن العدد الكلي لـ nodes في
segment treeهو2N - 1.
الحشو
- إذا كانت المصفوفة الأصلية تحتوي على
nعنصرًا وnليس قوة للعدد اثنين، نقوم بحشو المصفوفة بعناصر إضافية بحيث يصبح عدد leaf nodesNهو القوة التالية للعدد اثنين، مع ضمان أن العناصر الإضافية لا تؤثر على قيم الـnodes الأخرى.
- في أسوأ الحالات، قد نحتاج إلى إضافة ما يصل إلى
nعنصرًا، لأن القوة التالية للعدد اثنين أصغر من2n.
مثال
إذا غيرنا المثال السابق إلى [2, 4, 6]، فنحتاج إلى إضافة عنصر لجعل الطول قوة للعدد اثنين دون التأثير على القيم الأخرى. أفضل قيمة لإضافتها هي 0. ستكون segment tree الناتجة كما يلي:
graph TD
A["12 (1-4)"]
B["6 (1-2)"]
C["6 (3-4)"]
D["2 (1-1)"]
E["4 (2-2)"]
F["6 (3-3)"]
G["0 (4-4) مضافة"]
A --> B
A --> C
B --> D
B --> E
C --> F
C --> G
التمثيل
سنمثل الـsegment tree كمصفوفة تعطي موقع(أرقام) لكل node. وبالتالي سيتم إعطاء المواقع للـ segment tree السابقة كما يلي:
graph TD
A["1: sum = 12 (1-4)"]
B["2: sum = 6 (1-2)"]
C["3: sum = 6 (3-4)"]
D["4: sum = 2 (1-1)"]
E["5: sum = 4 (2-2)"]
F["6: sum = 6 (3-3)"]
G["7: sum = 0 (4-4) مضاف"]
A --> B
A --> C
B --> D
B --> E
C --> F
C --> G
كل node i لها طفلان في الموضعين 2 * i و2 * i + 1، ووالدها في الموضع i / 2.
في هذا المثال، عدد leaf nodes N هو 4، وهو أيضًا موقع أول leaf node. لذلك فإن الـleaf node في الموضع i تقابل الموقع i - N + 1 في المصفوفة الأصلية.
الاستعلام
لنعرّف الدالة query(id, l, r, ql, qr) التي تقوم بحساب ناتج استعلام من ql إلى qr، حيث:
idهو موقع الـnode الحالي.
lهو بداية النطاق الذي يغطيه الـnode الحالي.
rهو نهاية النطاق الذي يغطيه الـnode الحالي.
qlهو بداية نطاق الاستعلام.
qrهو نهاية نطاق الاستعلام.
تعمل الدالة كما يلي:
- إذا كان المجالان
[ql, qr]و[l, r]غير متقاطعين تمامًا، يتم تجاهل الـnode الحالي لأنه لا يسهم في الاستعلام.
- إذا كان المجال
[ql, qr]يغطي بالكامل المجال[l, r]، نقوم بإرجاع القيمة المخزنة في الـnode الحالي.
- خلاف ذلك، نقسم المجال عند المنتصف
m = (l + r) / 2ونحصل على حساب الاستدعاء على الأطفال:query(2 * id, l, m, ql, qr)
query(2 * id + 1, m + 1, r, ql, qr)
ثم ندمج النتائج للحصول على إجابة الـnode الحالي.
لحل استعلام [ql, qr] نستدعي query(1, 1, N, ql, qr).
تعقيد الزمن لهذه الخوارزمية هو \(O(\log n)\).
تحديث نقطة
بعض المسائل تطلب تحديث الموقع j إلى القيمة v. لتنفيذ ذلك نقوم بتحديث الـleaf node في الموضع j + N - 1 ثم نعيد حساب جميع قيم الـinternal node.
عدد الآباء لكل leaf node j + N - 1 هو logn. لذلك فإن تعقيد تحديث النقطة هو \(O(\log n)\).
طريقة الكتابة
عند بناء segment tree لدينا فقط المصفوفة الأصلية a (مرقمة ابتداء من ال0) وحجمها n.
حجم الشجرة
أولًا نحتاج إلى حساب عدد الـleaf nodes N. يمكن حسابه بطريقتين:
int N = 1 << (32 - __builtin_clz(n - 1));int N;
for (N = 1; N < n; N *= 2); // ال; مهمة جداint seg[2*N]بناء segment tree
بناء الـleaf nodes
لبناء الشجرة نضع أولًا قيم الـleaf nodes.for (int i = 0 ; i < n; i++) {
seg[i + N] = a[i];
}for (int i = n + N; i < 2*N; i++){
seg[i] = 0; // segment treeالقيم المضافة تعتمد على نوع ال
}بناء الـinternal nodes
لبناء الـinternal nodes نمر عليها بترتيب عكسي ونقوم بدمج طفلي كل internal node.for (int i = N - 1; i > 0 ; i--) {
seg[i] = seg[i * 2] + seg[i * 2 + 1]; // دمج قيم الطفلين
}الاستعلامات
int query(int id, int l, int r, int ql, int qr) {
// تأكد إذا لا يتقاطعان
if (ql > r || l > qr) {
return 0;
}
// بالكامل [l, r] يشمل [ql, qr] تأكد إذا
if (ql <= l && r <= qr) {
return seg[id];
}
// قم بالاستدعاء الذاتي والدمج
int m = (l + r) / 2;
return query(id * 2, l, m, ql, qr) + query(id * 2 + 1, m + 1, r, ql, qr);
}تحديث الـleaf node
void pointUpdate(int j, int v){
a[j] = v;
int idx = j + N - 1;
seg[idx] = v;
// أعد بناء حميع الآباء
for (int i = idx / 2; i > 0; i /= 2) {
seg[i] = seg[i * 2] + seg[i * 2 + 1];
}
}مثال على مسألة
Dynamic Range Minimum Queries
لديك المصفوفة \(x\) المكونة من \(n\) عنصر و\(q\) استعلامات. كل استعلام يكون من أحد النوعين:
- “1 \(k\) \(u\)”: تحديث القيمة في الموضع \(k\) إلى \(u\).
- “2 \(a\) \(b\)”: ما هي أصغر قيمة في المجال [\(a\),\(b\)]؟
Solution
سنقوم ببناء segment tree للقيمة الصغرى ونطبق الاستعلامات وتحديثات leaf nodes مباشرة عليها.
#include <iostream>
#include <climits>
using namespace std;
const int MAXN = 200000;
const int N = (1 << (32 - __builtin_clz(MAXN - 1)));
int seg[N * 2];
int a[MAXN];
int query(int id, int l, int r, int ql, int qr) {
if (ql > r || l > qr) {
return INT_MAX;
}
if (ql <= l && r <= qr) {
return seg[id];
}
int m = (l + r)/2;
return min(query(id * 2, l, m, ql, qr) , query(id * 2 + 1, m + 1, r, ql, qr));
}
void pointUpdate(int j, int v) {
a[j - 1] = v;
int idx = j + N - 1;
seg[idx] = v;
for (int i = idx/2; i > 0; i /= 2) {
seg[i] = min(seg[i * 2] , seg[i * 2 + 1]);
}
}
int main() {
int n, q;
cin >> n >> q;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0 ; i < n; i++) {
seg[i + N] = a[i];
}
for (int i = n + N; i < 2*N; i++) {
seg[i] = INT_MAX;
}
for (int i = N - 1; i > 0; i--) {
seg[i] = min(seg[i * 2] , seg[i * 2 + 1]);
}
while (q--) {
int type;
cin >> type;
if (type == 1) {
int k, u;
cin >> k >> u;
pointUpdate(k, u);
}
else {
int ql, qr;
cin >> ql >> qr;
cout << query(1, 1, N, ql, qr) << "\n";
}
}
}