هياكل البيانات والخوارزميات / براهين التحسين والتجميعات المحفوظة / الدرس 16
بنيتا Fenwick وشجرة المقاطع للمجالات
احفظ عقدا عاما للمجالات نصف المفتوحة، مع دعم تحديثات النقاط وتجميع المجالات بكلفة لوغاريتمية.
في هذه الصفحة
أهداف الدرس
- تحتاج المجاميع السابقة الثابتة والتحديثات الديناميكية إلى تخزين مختلف
- تحفظ خلايا Fenwick كتلا لاحقة يحدد طولها أقل بت مضبوط
- تجمع شجرة المقاطع أجزاء منفصلة من المجال
تحتاج المجاميع السابقة الثابتة والتحديثات الديناميكية إلى تخزين مختلف
تجيب المجاميع السابقة الثابتة عن المجالات بكفاءة، لكن تغيير عنصر قد يبطل كثيرا من الإدخالات اللاحقة. تحافظ شجرتا Fenwick والمقاطع على تجميعات مختارة، بحيث تصلح التحديثات عددا لوغاريتميا من الأجزاء فقط. حدّد هل يضيف التحديث فرقا delta أم يسند قيمة جديدة. فهما عمليتان مختلفتان: إسناد 7 بدل قيمة موجودة 5 يحتاج إلى delta=2 في بنية جمع. يكشف المثالان عناصر بفهرسة صفرية واستعلامات نصف مفتوحة [left,right)، رغم اختلاف فهرستهما الداخلية. تصح القيم السالبة للجمع. ولا تستنتج أن لجميع عمليات المجالات الجبر نفسه، أو أنها تدعم حيلة المعكوس نفسها.
تحفظ خلايا Fenwick كتلا لاحقة يحدد طولها أقل بت مضبوط
باستخدام فهرس داخلي i يبدأ من الواحد، تحفظ خلية Fenwick مجموع كتلة تنتهي عند i وطولها i & -i. يطرح استعلام السابقة ذلك البت الأدنى لإزالة كتلة أخيرة منفصلة؛ ويصل التكرار إلى الصفر. أما التحديث فيضيف البت الأدنى لزيارة كتل أكبر تحوي الموضع المتغير. يحول البرنامج الأول الموضع العام الصفري إلى i=index+1 قبل التحديث، فيمنع حالة الصفر الداخلي التي لا تتقدم. يستخدم منشئه إضافات نقاط متكررة؛ لذلك يكلف بناء هذا المثال بالذات O(n log n)، وليس كنسخة البناء الخطي المتاحة بطريقة إنشاء أخرى.
class Fenwick:
def __init__(self, values):
self.n = len(values)
self.tree = [0] * (self.n + 1)
for index, value in enumerate(values):
self.add(index, value)
def add(self, index, delta):
if not 0 <= index < self.n:
raise ValueError("Invalid element index")
i = index + 1
while i <= self.n:
self.tree[i] += delta
i += i & -i
def prefix(self, end):
if not 0 <= end <= self.n:
raise ValueError("Invalid prefix end")
total, i = 0, end
while i:
total += self.tree[i]
i -= i & -i
return total
def range_sum(self, left, right):
if not 0 <= left <= right <= self.n:
raise ValueError("Invalid half-open range")
return self.prefix(right) - self.prefix(left)
tree = Fenwick([4, -2, 5, 1])
print(tree.range_sum(1, 3))
tree.add(1, 3)
print(tree.range_sum(1, 3))
print(tree.range_sum(0, 4))
3
6
11
تغير إضافة 3 القيمة الثانية من -2 إلى 1. وتظل فروق المجاميع السابقة صحيحة بعد إصلاح التجميعات بعدد لوغاريتمي من الخطوات.
تجمع شجرة المقاطع أجزاء منفصلة من المجال
يكمل البرنامج الثاني عدد الأوراق إلى قوة للعدد اثنين، ويحفظ المجاميع فوقها، ويسند ورقة واحدة قبل إعادة حساب أسلافها. ويحرك استعلام المجال مؤشري حدوده صعودا، ولا يضيف عقدة إلا عندما ينتمي مجالها الممثل بالكامل إلى المجال المطلوب. كل قطعة مختارة منفصلة عن غيرها، فيُمنع العد المكرر. البناء خطي في عدد الأوراق بعد الإكمال، وهو أقل من ضعفي n عندما يكون n موجبا. يستغرق إسناد نقطة وجمع مجال O(log n) عملا، مع O(n) مساحة. ويخصص تمثيل المدخل الفارغ جذرا محايدا أيضا، لكنه يرفض أي إسناد لعنصر.
class SegmentSum:
def __init__(self, values):
self.n = len(values)
self.base = 1
while self.base < self.n:
self.base *= 2
self.tree = [0] * (2 * self.base)
self.tree[self.base:self.base + self.n] = values
for i in range(self.base - 1, 0, -1):
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def assign(self, index, value):
if not 0 <= index < self.n:
raise ValueError("Invalid element index")
i = self.base + index
self.tree[i] = value
while i > 1:
i //= 2
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def range_sum(self, left, right):
if not 0 <= left <= right <= self.n:
raise ValueError("Invalid half-open range")
left += self.base
right += self.base
total = 0
while left < right:
if left & 1:
total += self.tree[left]
left += 1
if right & 1:
right -= 1
total += self.tree[right]
left //= 2
right //= 2
return total
tree = SegmentSum([4, -2, 5, 1])
print(tree.range_sum(1, 3))
tree.assign(1, 1)
print(tree.range_sum(1, 3))
print(tree.range_sum(0, 4))
3
6
11
يطابق الإسناد إلى 1 إضافة delta=3 في المثال الأول فقط لأن القيمة القديمة كانت -2. تكشف الواجهتان معنيين مختلفين للتحديث عن قصد.
تحدد عملية التجميع الصيغ الصالحة
يصح طرح المجاميع السابقة في الجمع لأن المعكوس يزيل مساهمة سابقة. ولا يمكن لتجميع القيمة الصغرى عموما طرح قيمة صغرى سابقة لاستخراج صغرى مجال اعتباطي. تحتاج شجرة المقاطع إلى عملية دمج تجميعية وعنصر محايد للأجزاء الفارغة؛ وتحتاج عمليات مثل وصل النصوص أيضا إلى حفظ ترتيب اليسار إلى اليمين أثناء الاستعلام. يناسب المجمّع total الواحد هنا الجمع التبادلي، وليس قالبا عاما لكل مونويد. تضيف تحديثات المجالات بالانتشار الكسول ثوابت أخرى، ولا تقدمها تنفيذات تحديث النقاط هذه. لا تدّع عملية غير مدعومة لمجرد تسمية البنية شجرة.
تظل الحدود والدلالة العددية التزامات عند الواجهة العامة
ارفض الفهارس السالبة والمجالات المعكوسة قبل الحساب الداخلي، حتى لو قبلت Python الفهرسة السالبة لولا ذلك. يعيد prefix(0) وrange_sum(k,k) صفرا بصورة مشروعة. تستخدم الأمثلة مجاميع أعداد صحيحة دقيقة؛ وقد يختلف تقريب التجميع العائم عن المسح المباشر لأن تجميع عمليات الجمع يتغير. وتغيّر أعداد Python الصحيحة الهائلة كلفة الحساب أيضا. وإذا خزن التطبيق القيم الأصلية مؤقتا لتحويل الإسناد إلى delta، فحدّث ذلك المخزن تحت مالك الشجرة نفسه. قد تفسد قيمة مخزنة متقادمة إسنادات لاحقة، مع بقاء كل حلقات البت الأدنى صحيحة بنيويا.
استخدم مصفوفة عادية مرجعا لعمليات التعديل
لمصفوفات قصيرة، نفذ تسلسلا من إضافات النقاط أو إسناداتها في البنية وفي قائمة مرجعية عادية. وبعد كل تعديل، قارن كل مجال نصف مفتوح صالح مع sum(reference[left:right]). أدرج المدخل الفارغ والمنفرد، والأطوال التي ليست قوة للعدد اثنين، والفروق السالبة، وتحديث العنصر الأخير، والحدود غير الصالحة. يكشف ذلك عيوب الفهرسة والقيم المتقادمة بفاعلية أكبر من اختبار المجموع الكامل وحده. أبق الاختبارات صغيرة ومحدودة. تفسر حجتا البت الأدنى والقطع المنفصلة سبب تعميم البنية؛ ولا تحتاج إلى قاعدة بيانات خارجية أو نظام تحليلات حي أو اختبار تحميل معياري.
الأخطاء الشائعة وإصلاحها
عدم تقدم تحديث Fenwick عند الفهرس الداخلي صفر
- لماذا يحدث؟
- لم يُحوّل الفهرس العام ذي البداية الصفرية إلى الفهرسة الداخلية.
- كيف تصلحه؟
- ابدأ التحديثات الداخلية عند index+1.
- كيف تتجنب تكراره؟
- اختبر العنصر الأول وثابت تقدم الحلقة.
معاملة الإسناد على أنه إضافة بالخطأ
- لماذا يحدث؟
- لم تُوثّق دلالة واجهة التحديث.
- كيف تصلحه؟
- استخدم فرقا delta، أو تنفيذا حقيقيا للإسناد، بصورة متسقة.
- كيف تتجنب تكراره؟
- أسند القيمة نفسها مرتين، وقارن النتيجة بقائمة عادية.
طرح القيم الصغرى للسوابق للحصول على صغرى مجال
- لماذا يحدث؟
- عُممت خاصية المعكوس في الجمع بصورة غير صحيحة.
- كيف تصلحه؟
- اختر بنية تجميع يدعم جبرها الاستعلام المطلوب.
- كيف تتجنب تكراره؟
- اختبر قيمة صغرى تقع خارج المجال المطلوب.
اختبر فهمك
1. لماذا يحقق i & -i تقدما في تحديث داخلي صحيح لبنية Fenwick؟
اعرض الحل المشروح
عندما يكون i موجبا، يكون i & -i قوة موجبة للعدد اثنين؛ لذلك تزيد إضافته i حصرا حتى يتجاوز n. وعند i=0 يساوي صفرا، وهو ما يفسر خطر عدم تحويل الفهرس العام. ويطرح الاستعلام حجم الكتلة الموجب نفسه إلى أن يصل إلى الصفر.
2. هل يمكن إعادة استخدام استعلام شجرة المقاطع دون تغيير من أجل وصل نصوص بترتيبها؟
اعرض الحل المشروح
ليس بأمان. يتجاهل مجمّع الجمع الواحد ترتيب مصادفة قطع اليسار واليمين. تحتاج عملية تجميع غير تبادلية إلى مجمّعين منفصلين لليسار واليمين، يُدمجان بالترتيب الصحيح. لا تجعل التجميعية وحدها إعادة ترتيب الأجزاء صحيحة.
أهم ما تعلمته
- تحتاج المجاميع السابقة الثابتة والتحديثات الديناميكية إلى تخزين مختلف
- استخدم مصفوفة عادية مرجعا لعمليات التعديل