فهرست مقاله
فهرستِ رخدادهای متن میتواند چند بازهٔ تودرتو یا متداخل داشته باشد. اگر بخواهیم بخشی از آنها را برای یک نمایشِ آزمایشی انتخاب کنیم، نگهداشتنِ همهٔ رخدادها دیگر پاسخِ همان مسئله نیست. این درس، بیشترین جمعِ امتیازِ بازههای بدونِ همپوشانی را روی دادهٔ ساختگی پیدا میکند. امتیازها را خودمان دادهایم؛ موتورِ جستوجو، مدلِ رتبهبندی، رابطِ کاربر یا کیفیتِ نتیجهٔ واقعی آزمایش نمیشود.
۱. بازههای متن و امتیازِ دادهشده را جدا تعریف کنیم

تابع، متنِ str دقیق با حداکثر ۱۲۸ نقطهٔ کد و تاپلی با حداکثر ده نامزد میگیرد. هر نامزد دقیقاً تاپلِ سهعضویِ آغاز، پایان و امتیاز است. دو مرز، عددِ صحیحِ دقیق با آغازِ نامنفی، پایانِ بزرگتر از آغاز و پایانِ حداکثر برابرِ طولِ متناند. امتیاز، عددِ صحیحِ دقیق از یک تا صد است؛ bool، زیرکلاس، شناور و رشتهٔ عددی تبدیل نمیشوند.
بازه نیمهباز است: آغاز داخل و پایان بیرون. دو بازه که پایانِ اول با آغازِ دوم برابر باشد، مشترک نیستند و میتوانند همزمان انتخاب شوند. هویتِ نامزد، اندیسِ صفرمبنای آن در ورودی است؛ دو رکوردِ کاملاً برابر همچنان دو هویت دارند، اما چون همان جایگاهها را میپوشانند، هر دو با هم پذیرفته نمیشوند. مجموعهٔ تهی نیز مجاز و امتیازش صفر است.
تعریفِ str در پایتون، رشته را دنبالهٔ نقاطِ کدِ Unicode معرفی میکند. واحدِ مرزهای ما همین است؛ نه بایت، توکنِ مدل یا خوشهٔ نویسهای. درسِ offset و قطعهبندیِ سند تفاوتِ این واحدها را جدا میسنجد. برنامه، متن را نرمال نمیکند و از درستیِ مرزها، یکسانبودنِ نسخهٔ سند یا اعتبارِ محتوایش را نتیجه نمیگیرد.
۲. انتخابِ حریصانه چرا جمعِ بیشینه نمیسازد؟

متنِ دستی، دوازده حرفِ انگلیسیِ پیاپی از A تا L است. چهار قطعهٔ سهحرفیِ پشتِ سرِ هم، به ترتیب امتیازهای شش، شش، چهار و چهار دارند. نامزدِ دیگری شش حرفِ نخست را با امتیازِ ده میگیرد؛ نامزدِ بلندِ متداخل نیز بازهٔ دو تا پیش از ده را با امتیازِ پانزده میپوشاند. اینها مطلوبیتهای قراردادیِ بیواحدند، نه احتمالِ مرتبطبودن یا نتیجهٔ کلیکِ کاربران.
انتخابِ بزرگترین امتیازِ تکی، نامزدِ پانزده را میگیرد. آن نامزد با همهٔ بقیه تقاطع دارد، پس انتخابِ دیگری کنارش جا نمیشود. چهار قطعهٔ کوتاه، جمعِ بیست میسازند و بهترند. حتی نامزدِ دهامتیازی همراهِ دو قطعهٔ آخر فقط هجده میدهد. بنابراین سودِ یک بازه را باید همراهِ انتخابهای سازگارِ باقیمانده دید.
قاعدهٔ «زودتر تمام میشود» نیز هدفِ وزنی را خودکار حل نمیکند. در شاهدی جدا، بازهٔ صفر تا یک یک امتیاز و بازهٔ صفر تا دو پنج امتیاز دارد؛ اولی زودتر تمام میشود، اما با انتخابِ آن دومی کنار میرود. بخشِ زمانبندیِ بازههای وزندار از ویجای گارگ، دانشگاه تگزاس مسئلهٔ بیشینهٔ جمعِ وزنِ سازگار و رابطهٔ برنامهریزیِ پویا را توضیح میدهد. کاربردِ آن برای نامزدهای متنی، مدلسازیِ همین درس است.
۳. پایانِ مرتب، پیشینهٔ سازگار و بازسازیِ پاسخ

نامزدها را با پایانِ صعودی، سپس آغازِ صعودی و سرانجام هویتِ ورودی مرتب میکنیم. برای هر نامزد، تعدادِ نامزدهای قبلی را که پایانشان حداکثر برابرِ آغازِ اوست پیدا میکنیم. این عدد طولِ یک پیشوند است، نه جایگاهِ نویسه در متن. خانهٔ صفرِ جدول، پاسخِ پیشوندِ تهی و برابرِ صفر است.
پاسخِ هر پیشوند از دو انتخاب میآید: حذفِ نامزدِ آخر و نگهداشتنِ پاسخِ قبلی، یا گرفتنِ امتیازِ او بهعلاوهٔ پاسخِ پیشوندِ سازگار. برای جستوجوی مرز از bisect_right استفاده میکنیم. قراردادِ رسمیِ bisect_right، مقدارهای مساوی را سمتِ چپِ نقطهٔ درج نگه میدارد؛ همین ویژگی تماسِ دو سرِ نیمهباز را مجاز میکند. حدِ بالایِ جستوجو نیز فقط نامزدهای قبلی است.
در نمونهٔ اصلی، جدولِ امتیازهای پیشوندی از صفر به شش، ده، دوازده، شانزده، شانزده و بیست میرسد. برای بازسازی از آخر برمیگردیم: اگر انتخابِ نامزد اکیداً بهتر باشد، هویتش را نگه میداریم و به پیشوندِ سازگار میپریم؛ در غیرِ این صورت یک خانه عقب میرویم. نتیجه در ترتیبِ متن، چهار قطعهٔ ABC، DEF، GHI و JKL است.
در تساوی، حذفِ نامزدِ فعلی سیاستِ صریحِ ماست. در شاهدِ چهارحرفی، یک بازهٔ بلندِ دهامتیازی با دو بازهٔ کوتاهِ پنجامتیازی برابر است؛ کد همان تکبازه را برمیگرداند. این سیاست، بیشترین تعدادِ قطعه یا کوچکترین دنبالهٔ هویتها را تضمین نمیکند. بازچینیِ ورودی، هویتها و بعضی پاسخهای مساوی را میتواند عوض کند؛ یکتاییِ پاسخِ عددی به معنای یکتاییِ مجموعه نیست.
۴. کدِ کامل و مرجعِ تمامِ زیرمجموعهها

فایلِ weighted_snippet_spans.py را ذخیره و با پایتون اجرا کنید. اجرای ثبتشده Python 3.14.5 و فقط کتابخانهٔ استاندارد است؛ امتیازها صحیح و جمعها دقیقاند. کد، جدول و هویتهای مرتب را هم برمیگرداند تا رابطهٔ تصمیم با خروجی قابلِ بررسی باشد. از این اجرای کوچک، اندازهگیریِ سرعت یا تضمینِ ظرفیتِ یک سرویس به دست نمیآید.
"""Own bounded codepoint-span utility optimizer; no relevance or HTML model."""
from bisect import bisect_right
from itertools import permutations, product
import json
import platform
def select_spans(text, spans):
"""Exact str<=128; tuple<=10 of exact(start,end,weight) integer tuples.
Half-open nonempty source-codepoint spans; integer utility1..100 supplied.
Sort by(end,start,inputID); strictly prefer inclusion, exclude on equality.
Selected IDs/pieces are returned in source order, not sorted by utility.
"""
if type(text) is not str or len(text) > 128:
raise ValueError("text")
if type(spans) is not tuple or len(spans) > 10:
raise ValueError("spans")
for record in spans:
if type(record) is not tuple or len(record) != 3:
raise ValueError("span_record")
start, end, weight = record
if (type(start) is not int or type(end) is not int
or not 0 <= start < end <= len(text)):
raise ValueError("span_bounds")
if type(weight) is not int or not 1 <= weight <= 100:
raise ValueError("weight")
order = sorted(range(len(spans)), key=lambda i: (spans[i][1], spans[i][0], i))
finishes = [spans[i][1] for i in order]
previous_prefix = []
best = [0]
for j, i in enumerate(order):
start, end, weight = spans[i]
# The returned insertion position is a COUNT of earlier compatible jobs.
prefix = bisect_right(finishes, start, 0, j)
previous_prefix.append(prefix)
best.append(max(best[-1], weight + best[prefix]))
selected = []
j = len(order)
while j:
i = order[j - 1]
include = spans[i][2] + best[previous_prefix[j - 1]]
if include > best[j - 1]:
selected.append(i)
j = previous_prefix[j - 1]
else:
j -= 1 # Own local tie policy; not globally lexicographic selection.
selected.sort(key=lambda i: (spans[i][0], spans[i][1], i))
return {"selected_input_ids": tuple(selected), "total_weight": best[-1],
"pieces": tuple(text[spans[i][0]:spans[i][1]] for i in selected),
"finish_sorted_input_ids": tuple(order),
"compatible_prefix_lengths": tuple(previous_prefix),
"prefix_optimum_weights": tuple(best)}
def subset_oracle(spans):
"""Valid bounded fixtures only: enumerate subsets using occupied positions.
No finish sorting, predecessor search, DP table, main call or local tie rule.
Keep ALL maximizing ID sets, so the reference does not impose a fake tie.
"""
best = -1
optima = set()
for enabled in product((False, True), repeat=len(spans)):
occupied, score, picked = set(), 0, set()
for i, on in enumerate(enabled):
if not on:
continue
start, end, weight = spans[i]
cells = set(range(start, end))
if occupied & cells:
break
occupied.update(cells)
score += weight
picked.add(i)
else:
if score > best:
best, optima = score, {frozenset(picked)}
elif score == best:
optima.add(frozenset(picked))
return best, optima
def rejects(text, spans, category):
try:
select_spans(text, spans)
except ValueError as error:
return str(error) == category
return False
def run():
text = "ABCDEFGHIJKL"
spans = ((0, 6, 10), (0, 3, 6), (3, 6, 6),
(6, 9, 4), (9, 12, 4), (2, 10, 15))
result = select_spans(text, spans)
checks = {}
checks["hand_four_compatible_pieces_have_weight20_and_source_order"] = (
result["selected_input_ids"] == (1, 2, 3, 4)
and result["pieces"] == ("ABC", "DEF", "GHI", "JKL")
and result["total_weight"] == 20
)
checks["hand_predecessor_prefixes_and_optimum_table_are_exact"] = (
result["finish_sorted_input_ids"] == (1, 0, 2, 3, 5, 4)
and result["compatible_prefix_lengths"] == (0, 0, 1, 3, 0, 4)
and result["prefix_optimum_weights"] == (0, 6, 10, 12, 16, 16, 20)
)
highest = spans[5]
checks["highest_single_weight_and_earliest_finish_are_not_general_solutions"] = (
highest[2] == 15 < result["total_weight"]
and all(max(highest[0], s) < min(highest[1], e)
for s, e, w in spans[:5])
and select_spans("AB", ((0, 1, 1), (0, 2, 5)))["total_weight"] == 5
)
checks["adjacent_half_open_spans_are_compatible_but_one_cell_overlap_is_not"] = (
select_spans("ABCD", ((0, 2, 5), (2, 4, 6)))["total_weight"] == 11
and select_spans("ABCD", ((0, 3, 5), (2, 4, 6)))["selected_input_ids"] == (1,)
)
tied = ((0, 2, 5), (0, 4, 10), (2, 4, 5))
tie = select_spans("ABCD", tied)
tie_score, tie_sets = subset_oracle(tied)
checks["exclude_on_equal_is_deterministic_not_lexicographic_or_most_pieces"] = (
tie["selected_input_ids"] == (1,) and tie_score == 10
and tie_sets == {frozenset((1,)), frozenset((0, 2))}
and (0, 2) < (1,)
)
checks["duplicate_interval_values_keep_distinct_input_identity"] = (
select_spans("AB", ((0, 2, 7), (0, 2, 7)))["selected_input_ids"] == (0,)
)
empty = select_spans("", ())
checks["empty_candidate_set_and_empty_source_have_zero_optimum"] = (
empty["selected_input_ids"] == () and empty["pieces"] == ()
and empty["total_weight"] == 0 and empty["prefix_optimum_weights"] == (0,)
and select_spans("unused", ())["total_weight"] == 0
)
positions = tuple((s, e) for s in range(3) for e in range(s + 1, 4))
cases, oracle_ok = 0, True
for weights in product((0, 1, 2), repeat=len(positions)):
fixture = tuple((s, e, w) for (s, e), w in zip(positions, weights) if w)
actual = select_spans("ABC", fixture)
score, choices = subset_oracle(fixture)
oracle_ok &= actual["total_weight"] == score
oracle_ok &= frozenset(actual["selected_input_ids"]) in choices
cases += 1
checks["all729_small_weighted_fixtures_match_independent_subset_oracle"] = (
cases == 729 and bool(oracle_ok)
)
target = {(0, 3, 6), (3, 6, 6), (6, 9, 4), (9, 12, 4)}
permutation_ok = True
count = 0
for permutation in permutations(spans):
got = select_spans(text, permutation)
permutation_ok &= got["total_weight"] == 20
permutation_ok &= {permutation[i] for i in got["selected_input_ids"]} == target
count += 1
checks["all720_permutations_of_unique_optimum_preserve_spans_not_input_ids"] = (
count == 720 and bool(permutation_ok)
)
maximum = tuple((i, i + 1, 100) for i in range(10))
mx = select_spans("0123456789", maximum)
max_score, max_sets = subset_oracle(maximum)
checks["ten_candidate_upper_bound_and1024_subsets_have_weight1000"] = (
mx["selected_input_ids"] == tuple(range(10)) and max_score == mx["total_weight"] == 1000
and max_sets == {frozenset(range(10))}
and rejects("x" * 11, tuple((i, i + 1, 1) for i in range(11)), "spans")
)
class TextSubclass(str):
pass
class TupleSubclass(tuple):
pass
class IntSubclass(int):
pass
checks["strict_source_container_record_types_and_text_limit"] = (
all(rejects(t, (), "text") for t in (None, b"x", ["x"], TextSubclass("x")))
and select_spans("x" * 128, ())["total_weight"] == 0
and rejects("x" * 129, (), "text")
and all(rejects("ab", x, "spans") for x in ([], None, TupleSubclass(())))
and all(rejects("ab", (x,), "span_record") for x in ([0, 1, 1], (0, 1), (0, 1, 1, 1), TupleSubclass((0, 1, 1))))
)
checks["exact_endpoints_and_positive_integer_utility_are_validated"] = (
all(rejects("ab", ((s, e, 1),), "span_bounds") for s, e in
((True, 1), (0, False), (0.0, 1), (-1, 1), (0, 0), (1, 0), (0, 3), (IntSubclass(0), 1)))
and all(rejects("ab", ((0, 1, w),), "weight")
for w in (True, 1.0, "1", 0, -1, 101, IntSubclass(1)))
)
unicode_text = "A\u200c\U0001f642e\u0301B"
unicode_result = select_spans(unicode_text, ((1, 3, 4), (3, 5, 5)))
checks["codepoint_slices_are_not_bytes_graphemes_or_HTML_rendering"] = (
len(unicode_text) == 6 and len(unicode_text.encode("utf-8")) == 12
and unicode_result["pieces"] == ("\u200c\U0001f642", "e\u0301")
and select_spans("e\u0301", ((0, 1, 1),))["pieces"] == ("e",)
and select_spans("<b>", ((0, 3, 1),))["pieces"] == ("<b>",)
)
exposed = select_spans(text, spans)
exposed["pieces"] = ("changed",)
checks["source_candidates_and_fresh_result_are_independent"] = (
text == "ABCDEFGHIJKL" and spans == ((0, 6, 10), (0, 3, 6), (3, 6, 6), (6, 9, 4), (9, 12, 4), (2, 10, 15))
and select_spans(text, spans) == result
)
if not all(type(v) is bool and v for v in checks.values()):
raise AssertionError(checks)
return {"python": platform.python_version(), "dependencies": "standard library only",
"scope": "Own synthetic supplied-utility nonoverlap spans; not relevance, search engine or rendered HTML",
"source_text": text, "candidates_start_end_weight": spans, "selection": result,
"highest_single_candidate_id": 5, "highest_single_candidate_weight": 15,
"equal_optimum_tie_input": tied, "tie_selected_input_ids": tie["selected_input_ids"],
"unit": "source Unicode codepoint offsets; unitless supplied positive integer utility",
"oracle_cases_inside_groups": {"weighted_small_fixtures": cases, "unique_optimum_input_permutations": count,
"maximum_case_subsets": 2 ** len(maximum)},
"unicode_probe_codepoints": len(unicode_text), "unicode_probe_utf8_bytes": len(unicode_text.encode("utf-8")),
"checks": checks, "check_count": len(checks), "all_passed": True,
"limitations": ["No candidate detection/scoring, query/model/relevance measurement or user study",
"No snippet count/length budget, cross-document selection or source-version authentication",
"No grapheme/word preservation, normalization, escaping or safe HTML renderer",
"Exact str<=128 and at most10 exact tuple spans, nonempty halfopen, exact integers only",
"Ties depend on declared end/start/inputID order; no globally lexicographic or maximum-count promise"]}
if __name__ == "__main__":
print(json.dumps(run(), ensure_ascii=False, indent=2, allow_nan=False))
مرجعِ مستقل تمامِ زیرمجموعهها را برمیشمرد. برای هر مجموعه، جایگاههای صحیحِ هر بازه را مجموعه میسازد؛ تقاطعِ جایگاهها یعنی ناسازگاری. سپس جمعِ وزنِ مجموعههای مجاز را مقایسه و همهٔ برندههای مساوی را نگه میدارد. این مسیر، مرتبسازیِ پایان، bisect، جدولِ پویا یا سیاستِ تساویِ تابعِ اصلی را ندارد. پاسخِ کد باید هم امتیازِ بیشینه و هم عضویت در مجموعهٔ برندهها را داشته باشد.
روی متنِ سهحرفی، شش بازهٔ ناتهی ممکن داریم. هرکدام یا غایب است یا وزنِ یک یا دو دارد؛ ۷۲۹ ورودی در یک خانواده با مرجع مقایسه میشوند. برای نمونهٔ دارای پاسخِ یکتایِ بیست، هر ۷۲۰ جایگشتِ ورودی همان بازهها را نگه میدارد، اگرچه شمارههای هویت تغییر میکنند. شاهدِ دهنامزدی نیز ۱۰۲۴ زیرمجموعه و جمعِ بیشینهٔ هزار دارد. این حالتها داخلِ چهارده گروهِ نامدارند، نه هزاران گروهِ تازه.
{
"python": "3.14.5",
"dependencies": "standard library only",
"scope": "Own synthetic supplied-utility nonoverlap spans; not relevance, search engine or rendered HTML",
"source_text": "ABCDEFGHIJKL",
"candidates_start_end_weight": [
[
0,
6,
10
],
[
0,
3,
6
],
[
3,
6,
6
],
[
6,
9,
4
],
[
9,
12,
4
],
[
2,
10,
15
]
],
"selection": {
"selected_input_ids": [
1,
2,
3,
4
],
"total_weight": 20,
"pieces": [
"ABC",
"DEF",
"GHI",
"JKL"
],
"finish_sorted_input_ids": [
1,
0,
2,
3,
5,
4
],
"compatible_prefix_lengths": [
0,
0,
1,
3,
0,
4
],
"prefix_optimum_weights": [
0,
6,
10,
12,
16,
16,
20
]
},
"highest_single_candidate_id": 5,
"highest_single_candidate_weight": 15,
"equal_optimum_tie_input": [
[
0,
2,
5
],
[
0,
4,
10
],
[
2,
4,
5
]
],
"tie_selected_input_ids": [
1
],
"unit": "source Unicode codepoint offsets; unitless supplied positive integer utility",
"oracle_cases_inside_groups": {
"weighted_small_fixtures": 729,
"unique_optimum_input_permutations": 720,
"maximum_case_subsets": 1024
},
"unicode_probe_codepoints": 6,
"unicode_probe_utf8_bytes": 12,
"checks": {
"hand_four_compatible_pieces_have_weight20_and_source_order": true,
"hand_predecessor_prefixes_and_optimum_table_are_exact": true,
"highest_single_weight_and_earliest_finish_are_not_general_solutions": true,
"adjacent_half_open_spans_are_compatible_but_one_cell_overlap_is_not": true,
"exclude_on_equal_is_deterministic_not_lexicographic_or_most_pieces": true,
"duplicate_interval_values_keep_distinct_input_identity": true,
"empty_candidate_set_and_empty_source_have_zero_optimum": true,
"all729_small_weighted_fixtures_match_independent_subset_oracle": true,
"all720_permutations_of_unique_optimum_preserve_spans_not_input_ids": true,
"ten_candidate_upper_bound_and1024_subsets_have_weight1000": true,
"strict_source_container_record_types_and_text_limit": true,
"exact_endpoints_and_positive_integer_utility_are_validated": true,
"codepoint_slices_are_not_bytes_graphemes_or_HTML_rendering": true,
"source_candidates_and_fresh_result_are_independent": true
},
"check_count": 14,
"all_passed": true,
"limitations": [
"No candidate detection/scoring, query/model/relevance measurement or user study",
"No snippet count/length budget, cross-document selection or source-version authentication",
"No grapheme/word preservation, normalization, escaping or safe HTML renderer",
"Exact str<=128 and at most10 exact tuple spans, nonempty halfopen, exact integers only",
"Ties depend on declared end/start/inputID order; no globally lexicographic or maximum-count promise"
]
}
Aho–Corasick و گزارشِ همهٔ رخدادها میتواند مسئلهٔ متفاوتِ تولیدِ نامزد را توضیح دهد؛ این برنامه آن ماشین را اجرا یا به آن وصل نمیکند. وزن، انتخابِ بازه، برشِ منبع و نمایش، مسئولیتهای جدا هستند. جمعِ بیشترِ وزنِ ساختگی، دقتِ بازیابی یا خواناییِ بهتر را ثابت نمیکند.
۵. چهار پرسش دربارهٔ مرز، تساوی و نمایش

پرسش اول: آیا پایانِ دو و آغازِ دو تقاطع دارند؟ پاسخ: در قراردادِ نیمهباز خیر؛ بازهٔ اول موقعیتِ دو را ندارد و دومی از همانجا آغاز میشود. کد شاهدِ همجواری را با تقاطعِ واقعیِ یک جایگاه جدا میآزماید؛ تغییر به دو سرِ بسته، قاعدهٔ دیگری میخواهد.
پرسش دوم: امتیازِ ده همیشه دو قطعه میدهد؟ پاسخ: خیر؛ در شاهدِ تساوی، تکبازه و دو قطعه هر دو دهاند، اما سیاستِ حذف در تساوی تکبازه را نگه میدارد. برای کمینهٔ طول یا بیشینهٔ تعداد باید هدفِ ثانویه را جدا تعریف و در جدول و مرجع پیاده کنیم.
پرسش سوم: برشِ معتبر برای نمایش هم آماده است؟ پاسخ: خیر؛ برش میتواند علامتِ ترکیبی را از حرف جدا کند یا رشتهای شبیه برچسبِ HTML برگرداند. برنامه فقط متنِ خام میدهد؛ مرزبندیِ نمایشی و escaping را انجام نمیدهد و ادعای رندرِ امن ندارد.
پرسش چهارم: آیا محدودیتِ تعداد یا طولِ نمایش اعمال شده؟ پاسخ: خیر؛ تنها سقفِ ورودیِ ده نامزد داریم، نه بودجهٔ خروجی. چند سند، خلاصهسازی، تشخیصِ نامزد و سنجشِ ارتباط نیز خارج از مدلاند. منابع در ۲۷ مهر ۱۴۰۵ خوانده شدند؛ مستندات Python 3.14.8 و بخشِ بیتاریخِ گارگ مرجعاند، نه خبرِ تازه یا گزارشِ بهبودِ محصول.
منابع و مطالعهٔ بیشتر
- ویجای گارگ، دانشگاه تگزاس؛ رابطهٔ برنامهریزی پویای بازههای وزندار
- Python3.14؛ نقطهٔ درجِ bisect_right و مرزِ کوچکتر یا مساوی
- Python3.14؛ مقدمهٔ str و واحدِ نقطهٔ کدِ Unicode
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

