مقدمة في علوم الحاسوب النظرية › السكر النحوي واحتساب كل دالة 6 / 26 السابق التالي

السكر النحوي، وحساب كل دالة {#finiteuniversalchap }

السكر النحوي، وحساب كل دالة

  • أن تتمرّس على السكر النحوي أو الترجمة التلقائية للمنطق عالي المستوى إلى البوابات منخفضة المستوى. \
  • أن تتعلّم برهان نتيجة رئيسية: كل دالة متناهية يمكن حسابها بدائرة منطقية. \
  • أن تبدأ التفكير كمّيًا في عدد الأسطر التي تتطلّبها عملية الحساب.

«[في عام 1951] كان لديّ مُصرِّف (compiler) يعمل، ولم يقترب منه أحد لأنهم، كما قالوا بعناية، لا يمكن للحواسيب سوى أن تقوم بالحساب العددي (arithmetic)؛ إذ لا يمكنها تنفيذ البرامج.»، غريس موراي هوبِر، 1986.

«السكر النحوي يُسبّب سرطان الفاصلة المنقوطة.»، آلان بيرليس، 1982.

النماذج الحوسبية التي نظرناها حتى الآن في أبسط صورة ممكنة لها. على سبيل المثال، لا تمتلك «لغة البرمجة» NAND-CIRC لدينا سوى عملية واحدة وهي foo = NAND(bar,blah). في هذا الفصل سنرى أن هذه النماذج البسيطة هي في الواقع متكافئة مع نماذج أكثر تطوّرًا. الملاحظة المفتاحية هي أننا نستطيع تنفيذ سمات أكثر تعقيدًا باستخدام لبناتنا الأساسية، ثم نستخدم هذه السمات الجديدة بدورها لبناتٍ أكثر تعقيدًا. ويُعرف هذا بـ«السكر النحوي» (syntactic sugar) في مجال تصميم لغات البرمجة، لأننا لا نعدّل نموذج البرمجة الأساسي نفسه، بل نكتفي بتنفيذ سمات جديدة عبر تحويل برنامج يستعمل هذه السمات نحويًا إلى برنامج لا يستعملها.

يوفّر هذا الفصل «عدّة أدوات» (toolkit) يمكن استعمالها لإظهار أن كثيرًا من الدوال يمكن حسابها ببرامج NAND-CIRC، وبالتالي أيضًا بدوائر منطقية. وسنستعمل هذه العدّة أيضًا لإثبات مبرهنة أساسية: كل دالة متناهية f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m يمكن حسابها بدائرة منطقية، انظر مبرهنة — شمولية الدوائر المنطقية أدناه. ولأهمية عدّة السكر النحوي في ذاتها، يمكن أيضًا إثبات مبرهنة — شمولية الدوائر المنطقية مباشرة دون استعمال هذه العدّة. ونعرض هذا البرهان البديل في حساب كل دالة: برهان بديل. انظر الشكل التوضيحي لتجد ملخّص نتائج هذا الفصل.

ملخّص نتائج هذا الفصل. في secsyntacticsugar نقدّم عدّة أدوات تحويل «سكر نحوي» تبيّن كيف يمكن تنفيذ سمات مثل الدوال المعرَّفة من المُبرمِج (programmer-defined functions) والجمل الشرطية (conditional statements) في NAND-CIRC. نستعمل هذه الأدوات في seclookupfunc لنقدّم برنامج NAND-CIRC (أو بدلًا من ذلك دائرة منطقية) يحسب دالة . ثم نبني على هذه النتيجة لنبيّن في seccomputeallfunctions أن برامج NAND-CIRC (أو ما يماثلها من دوائر منطقية) تستطيع حساب كل دالة متناهية. ويُعرض برهان بديل مباشر للنتيجة نفسها في seccomputalternative.

في هذا الفصل سنرى نتيجتنا الرئيسية الأولى: كل دالة متناهية يمكن حسابها بدائرة منطقية ما (انظر مبرهنة — شمولية الدوائر المنطقية وfinitecomputation). ويُعرف هذا أحيانًا بـ«شمولية» (universality) عمليات ANDAND وOROR وNOTNOT (وباستعمال تكافؤ تعريف الحوسبة، كذلك NANDNAND)

ولأهمية هذه النتيجة، إلا أن مبرهنة — شمولية الدوائر المنطقية ليس صعب الإثبات في الحقيقة. إذ يعرض حساب كل دالة: برهان بديل برهانًا مباشرًا بسيطًا نسبيًا لهذه النتيجة. غير أننا في بعض أمثلة السكر النحوي ودالة LOOKUP نستنتج هذه النتيجة باستعمال مفهوم «السكر النحوي» (انظر synsugar). وهذا مفهوم مهم في نظرية لغات البرمجة وفي ممارستها. الفكرة وراء «السكر النحوي» هي أننا نستطيع توسيع لغة برمجة بتنفيذ سمات متقدّمة انطلاقًا من مكوّناتها الأساسية. على سبيل المثال، يمكننا أن نأخذ لغتَي البرمجة AON-CIRC وNAND-CIRC اللتين رأيناهما في تعريف الحوسبة، ونوسّعهما للحصول على سمات مثل الدوال المعرَّفة من المستخدم (user-defined functions) (مثل def Foo(...))، والجمل الشرطية (conditional statements) (مثل if blah ...)، وغيرها. وبعد أن تتوفّر لدينا هذه السمات، ليس من الصعب البتّة أن نبيّن أننا نستطيع أن نأخذ «جدول الصدق» (truth table)، أي جدول كل المُداخل والمُخارج، لأي دالة، ونستعمله لإنشاء برنامج AON-CIRC أو NAND-CIRC يُسقط كل مُدخل على مُخرجه المقابل.

وسنلمّ في هذا الفصل أيضًا لأول مرة إلى المقادير الكمية (quantitative measures). فبينما تخبرنا مبرهنة — شمولية الدوائر المنطقية أن كل دالة يمكن حسابها بدائرة ما، فإن عدد البوابات في هذه الدائرة قد يكون أُسّيّ النمو. (لا نستعمل هنا كلمة «أُسّيّ» بالمعنى العامي الذي يعني «كبير جدًا جدًا»، بل بمعنى رياضي دقيق جدًّا، وهو ما يتزامن بالمصادفة مع كونه كبيرًا جدًا جدًا.) ويبيّن الأمر في الواقع أن بعض الدوال (مثل جمع الأعداد الصحيحة وضربها) يمكن حسابها فعلًا بعدد بوابات أقل بكثير. وسنتناول مسألة «تعقيد البوابات» (gate complexity) بمزيد من التعمّق في الشيفرة بيانات والبيانات شيفرة والفصول التالية.

بعض أمثلة السكر النحوي

نعرض الآن بعض أمثلة تحويلات «السكر النحوي» التي يمكننا استعمالها في بناء برامج مستقيمة أو دوائر. نركّز على رؤية «لغة البرمجة المستقيمة» (straight-line programming language) لنماذجنا الحوسبية، وتحديدًا (من أجل الاتّضاح) على لغة البرمجة NAND-CIRC. وهذا مريح لأن كثيرًا من تحويلات السكر النحوي التي نعرضها يسهل التفكير فيها بوصفها تطبيقات لعمليات «بحث واستبدال» (search and replace) على الشيفرة المصدرية لبرنامج. غير أنه، بموجب مبرهنة — التكافؤ بين نماذج الحوسبة المتناهية، تنطبق كل نتائجنا على الدوائر على قدم المساواة، سواء كانت دوائر تستعمل بوابات NAND أو دوائر منطقية تستعمل العمليات AND وOR وNOT. قد يكون استعراض أمثلة تحويلات السكر النحوي هذه مملًّا بعض الشيء، غير أننا نفعله لسببين:

  1. كي نُقنعك بأنه، رغم بساطتها الظاهرة ومحدودياتها، فإن النماذج البسيطة مثل الدوائر المنطقية أو لغة البرمجة NAND-CIRC قوية في الواقع إلى حدٍّ كبير.

  2. كي تدرك كم أنت محظوظ لأنك تدرس مساقًا في نظرية الحوسبة (theory of computation) لا مساقًا في مُصرِّفات المترجِمين (compilers)... :)

إجراءات معرَّفة من المستخدم

من أركان أي لغة برمجة تقريبًا القدرة على تعريف إجراءات (procedures) أو دوال فرعية (subroutines) ثم تنفيذها. (وتُسمّى هذه في بعض لغات البرمجة دوال (functions)، لكننا نفضّل اسم إجراءات (procedures) تفاديًا للالتباس مع الدالة التي يحسبها البرنامج.) ليس في لغة البرمجة NAND-CIRC هذه الآلية مُدمجة. غير أننا نستطيع تحقيق الأثر نفسه باستعمال التقنية العريقة «نسخ ولصق» (copy and paste). على وجه التحديد، يمكننا استبدال شيفرة تعرّف إجراءً مثل

def Proc(a,b):
    proc_code
    return c
some_code
f = Proc(d,e)
some_more_code

بالشيفرة التالية التي «نلصق» فيها شيفرة Proc

some_code
proc_code'
some_more_code

وحيث إنّ proc_code' يُنتَج باستبدال كل ورود لـ a بـ d، وb بـ e، وc بـ f. وعندئذٍ سنحتاج إلى التأكّد من أن بقية المتغيّرات الظاهرة في proc_code' لا تتداخل مع متغيّرات أخرى. ويمكننا دائمًا فعل ذلك بإعادة تسمية المتغيّرات إلى أسماء جديدة لم تُستعمل من قبل. ويؤدّي الاستدلال أعلاه إلى برهان المبرهنة التالية:

مبرهنة — سكر نحوي لتعريف الإجراءات

لتكن NAND-CIRC-PROC هي لغة البرمجة NAND-CIRC معزَّزة بالصياغة أعلاه لتعريف الإجراءات. عندئذٍ، لكل برنامج NAND-CIRC-PROC من الشكل PP، يوجد برنامج NAND-CIRC معياري (أي «خالٍ من السكر») P′P' يحسب الدالة نفسها التي يحسبها PP.

ملاحظة — لا إجراءات تكرارية

لا تسمح NAND-CIRC-PROC إلّا بـ_إجراءات غير تكرارية_ (non-recursive). وعلى وجه الخصوص، لا تستطيع شيفرة إجراء Proc أن تستدعي Proc، بل تستعمل فقط الإجراءات التي عُرِّفت قبله. ومن دون هذا القيد، قد لا ينتهي إجراء «البحث والاستبدال» أعلاه أبدًا، ولن يكون مبرهنة — سكر نحوي لتعريف الإجراءات صحيحًا.

يمكن إثبات مبرهنة — سكر نحوي لتعريف الإجراءات باستعمال التحويل أعلاه، لكن بما أن البرهان الرسمي طويل ومملّ بعض الشيء، فإننا نُهمله هنا.

مثال — حساب دالة الأغلبية انطلاقًا من NAND باستعمال السكر النحوي

تتيح لنا الإجراءات التعبير عن برامج NAND-CIRC بوضوح وإيجاز أكبر بكثير. على سبيل المثال، بما أننا نستطيع حساب AND وOR وNOT باستعمال NAND، يمكننا حساب دالة الأغلبية (Majority) على النحو التالي:

def NOT(a):
    return NAND(a,a)
def AND(a,b):
    temp = NAND(a,b)
    return NOT(temp)
def OR(a,b):
    temp1 = NOT(a)
    temp2 = NOT(b)
    return NAND(temp1,temp2)

def MAJ(a,b,c):
    and1 = AND(a,b)
    and2 = AND(a,c)
    and3 = AND(b,c)
    or1 = OR(and1,and2)
    return OR(or1,and3)

print(MAJ(0,1,1))
# 1

يعرض الشكل التوضيحي برنامج NAND-CIRC «الخالي من السكر» (والدائرة المقابلة له) الذي ينتج عن «فكّ» هذا البرنامج، أي استبدال استدعاءات الإجراءات بتعريفاتها.

متى بيّنا أن نموذجًا حوسبًا XX متكافئ مع نموذج يملك السمة YY، استطعنا أن نفترض توفّر YY عند إظهار أن دالة ff قابلة للحساب بواسطة XX.

برنامج NAND-CIRC معياري (أي «خالٍ من السكر») يُنتَج بفكّ تعريفات الإجراءات في برنامج الأغلبية (majcircnand). الدائرة المقابلة له على اليمين. لاحظ أن هذه ليست أكثر دائرة NAND ولا أكثر برنامج NAND كفاءةً للأغلبية: يمكننا توفير بعض البوابات عبر «اختصار» الخطوات التي تحسب فيها بوابة  القيمة  ثم تحسب بوابة  القيمة  (كما تُشير إليه الأسهم الخضراء المتقطّعة في الشكل أعلاه).

ملاحظة — عدّ الأسطر

رغم أننا نستطيع استعمال السكر النحوي لـ_عرض_ برامج NAND-CIRC بصورة أكثر قابلية للقراءة، فإننا لم نغيّر تعريف اللغة نفسها. لذلك، كلما قلنا إن دالة ما ff لها برنامج NAND-CIRC من ss سطرًا، فإننا نعني برنامج NAND-CIRC معياريًا «خالٍ من السكر»، قد فُكّ فيه كل السكر النحوي. على سبيل المثال، برنامج مثال — حساب دالة الأغلبية انطلاقًا من NAND باستعمال السكر النحوي هو برنامج من 1212 سطرًا لحساب دالة MAJMAJ، رغم أنه يمكن كتابته في أسطر أقل باستعمال NAND-CIRC-PROC.

البرهان بـ Python (اختياري)

يمكننا كتابة برنامج بلغة Python يُنفّذ برهان مبرهنة — سكر نحوي لتعريف الإجراءات. وهذا برنامج Python يأخذ برنامج NAND-CIRC-PROC من الشكل PP الذي يتضمّن تعريفات إجراءات، ويستعمل «بحثًا واستبدالًا» بسيطًا (search and replace) لتحويل PP إلى برنامج NAND-CIRC معياري (أي «خالٍ من السكر») P′P' يحسب الدالة نفسها التي يحسبها PP دون استعمال أي إجراء. والفكرة بسيطة: إذا احتوى البرنامج PP على تعريف إجراء Proc ذي وسيطين هما x وy، فعند كل ما نرى سطرًا من الشكل foo = Proc(bar,blah) يمكننا استبدال هذا السطر بـ:

  1. جسم الإجراء Proc (مع استبدال كل ورود لـ x وy بـ bar وblah على الترتيب).

  2. سطر foo = exp، حيث exp هو التعبير الوارد بعد عبارة return في تعريف الإجراء Proc.

ولجعل هذا أمتن، نضيف بادئةً إلى المتغيّرات الداخلية التي يستعملها Proc حتى لا تتنازع مع متغيّرات PP؛ نبساطةٍ نُهمل هذه المسألة في الشيفرة أدناه مع أنها يمكن إضافتها بسهولة.

وتحقّق شيفرة الدالة desugar في Python أدناه هذا النوع من التحويلات.

Python code for transforming NAND-CIRC-PROC programs into standard sugar-free NAND-CIRC programs.

def desugar(code, func_name, func_args,func_body):
    """
    Replaces all occurences of 
       foo = func_name(func_args) 
    with
       func_body[x->a,y->b]
       foo = [result returned in func_body]    
    """
    # Uses Python regular expressions to simplify the search and replace,
    # see https://docs.python.org/3/library/re.html and Chapter 9 of the book

    # regular expression for capturing a list of variable names separated by commas
    arglist = ",".join([r"([a-zA-Z0-9\_\[\]]+)" for i in range(len(func_args))])
    # regular expression for capturing a statement of the form
    # "variable = func_name(arguments)"
    regexp = fr'([a-zA-Z0-9\_\[\]]+)\s*=\s*{func_name}\({arglist}\)\s*$'
    while True:
        m = re.search(regexp, code, re.MULTILINE)
        if not m: break
        newcode = func_body 
        # replace function arguments by the variables from the function invocation
        for i in range(len(func_args)): 
            newcode = newcode.replace(func_args[i], m.group(i+2))
        # Splice the new code inside
        newcode = newcode.replace('return', m.group(1) + " = ")
        code = code[:m.start()] + newcode + code[m.end()+1:]
    return code

يبيّن الشكل التوضيحي نتيجة تطبيق desugar على برنامج مثال — حساب دالة الأغلبية انطلاقًا من NAND باستعمال السكر النحوي الذي يستعمل السكر النحوي لحساب دالة الأغلبية. وعلى وجه التحديد، نطبّق desugar أولًا لإزالة استعمال دالة OR، ثم نطبّقه لإزالة استعمال دالة AND، وأخيرًا نطبّقه ثالث مرة لإزالة استعمال دالة NOT.

ملاحظة — تحليل تعريفات الدوال (اختياري)

تفترض الدالة desugar في Python code for transforming NAND-CIRC-PROC programs into standard sugar-free NAND-CIRC programs. أنها تتلقّى الإجراء وقد قُسِّم مسبقًا إلى اسمه ووسيطيه وجسمه. وليس من الضروري لأغراضنا أن نصف بدقّة كيفية تفحّص تعريف وتقسيمه إلى هذه المكوّنات، لكن إن كنت فضوليًا فيمكن إنجاز ذلك في Python عبر الشيفرة التالية:

def parse_func(code):
    """Parse a function definition into name, arguments and body"""
    lines = [l.strip() for l in code.split('\n')]
    regexp = r'def\s+([a-zA-Z\_0-9]+)\(([\sa-zA-Z0-9\_,]+)\)\s*:\s*'
    m = re.match(regexp,lines[0])
    return m.group(1), m.group(2).split(','), '\n'.join(lines[1:])

الجمل الشرطية

من السمات المفقودة بشدّة في NAND-CIRC الجملة الشرطية مثل بُنى if/then الموجودة في كثير من لغات البرمجة. غير أننا نستطيع، باستعمال الإجراءات، الحصول على بديل (ersatz) لبنية if/then. أوّلًا يمكننا حساب الدالة IF:{0,1}3→{0,1}IF:\{0,1\}^3 \rightarrow \{0,1\} بحيث تساوي IF(a,b,c)IF(a,b,c) القيمة bb إذا كان a=1a=1 والقيمة cc إذا كان a=0a=0.

توقّف وتأمّل

قبل أن تتابع القراءة، حاول أن ترى كيف يمكنك حساب الدالة IFIF باستعمال NANDs. وبعد أن تتمكّن من ذلك، حاول أن ترى كيف يمكنك استعماله لتقليد بُنى من نوع if/then.

يمكن تنفيذ الدالة IFIF انطلاقًا من NANDs على النحو التالي (انظر تمرين — حساب MUX):

def IF(cond,a,b):
    notcond = NAND(cond,cond)
    temp = NAND(b,notcond)
    temp1 = NAND(a,cond)
    return NAND(temp,temp1)

وتُعرف الدالة IFIF أيضًا باسم دالة التبادل (multiplexing)، إذ يمكن التفكير في cond بوصفه مفتاحًا يحدّد ما إذا كان الخرج موصولًا بـ aa أم بـ bb. وبعد أن يتوفّر لدينا إجراء لحساب الدالة IFIF، يمكننا تنفيذ الجمل الشرطية في NAND. والفكرة أنك نستبدل شيفرة من الشكل

if (condition):  assign blah to variable foo

بالشيفرة من الشكل

foo   = IF(condition, blah, foo)

وهي شيفرة تُسند إلى foo قيمته القديمة عندما يساوي condition القيمة 00، وتُسند إلى foo قيمة blah في غير ذلك. والأعمّ من ذلك، يمكننا استبدال شيفرة من الشكل

if (cond):
    a = ...
    b = ...
    c = ...

بالشيفرة من الشكل

temp_a = ...
temp_b = ...
temp_c = ...
a = IF(cond,temp_a,a)
b = IF(cond,temp_b,b)
c = IF(cond,temp_c,c)

وباستعمال تحويلات من هذا النوع، يمكننا إثبات المبرهنة التالية. ونُهمل مرة أخرى البرهان الرسمي الكامل (الذي لا يقدّم فكرة غير بديهية)، لكن انظر البرهان بـ Python (اختياري) لبعض القرائن على كيفية إتمامه.

مبرهنة — السكر النحوي للجمل الشرطية

لتكن NAND-CIRC-IF هي لغة البرمجة NAND-CIRC معزَّزة بعبارات if/then/else التي تسمح بتنفيذ الشيفرة شرطًا على أساس ما إذا كان متغيّر ما يساوي 00 أم 11.
عندئذٍ، لكل برنامج NAND-CIRC-IF من الشكل PP، يوجد برنامج NAND-CIRC معياري (أي «خالٍ من السكر») P′P' يحسب الدالة نفسها التي يحسبها PP.

مثال موسَّع: الجمع والضرب (اختياري)

باستعمال «السكر النحوي» يمكننا كتابة دالة جمع الأعداد الصحيحة على النحو التالي:

# Add two n-bit integers
# Use LSB first notation for simplicity
def ADD(A,B):
    Result = [0]*(n+1)
    Carry  = [0]*(n+1)
    Carry[0] = zero(A[0])
    for i in range(n):
        Result[i] = XOR(Carry[i],XOR(A[i],B[i]))
        Carry[i+1] = MAJ(Carry[i],A[i],B[i])
    Result[n] = Carry[n]
    return Result

ADD([1,1,1,0,0],[1,0,0,0,0]);;
# [0, 0, 0, 1, 0, 0]

حيث zero هي دالة الصفر الثابت، وMAJ وXOR تمثّلان على الترتيب دالتي الأغلبية وXOR. ولأغراض الراحة نستعمل صياغة Python، لكنّ nn في هذا المثال عددٌ صحيح ثابت، ومن ثمّ فلكل قيمة من هذه القيم يكون ADD دالةً متناهية تأخذ 2n2n بتًّا مُدخلًا وتُخرج n+1n+1 بتًّا. وبالذات، لكل nn يمكننا إزالة بِنية الحلقة for i in range(n) ببساطة بتكرار الشيفرة nn مرة، مع استبدال قيمة i بـ 0,1,2,…,n−10,1,2,\ldots,n-1. وبفكّ كل السمات، لكل قيمة من nn يمكننا ترجمة البرنامج أعلاه إلى برنامج NAND-CIRC معياري («خالٍ من السكر»). ويصوّر الشكل التوضيحي ما نحصل عليه عند n=2n=2.

برنامج NAND-CIRC والدائرة المقابلة له لإضافة عددين ثنائيّين، ويُحصل عليهما بفكّ كل السكر النحوي. يتكوّن البرنامج/الدائرة من 43 سطرًا/بوابة، وهذا ليس ضروريًا البتّة. ومن الممكن جمع أعداد بطول  بت باستخدام  بوابة NAND، انظر halffulladderex.

وبمراجعة البرنامج أعلاه بعناية وحساب عدد البوابات، نرى أنّه يؤدّي إلى برهان المبرهنة التالية (انظر أيضًا الشكل التوضيحي):

مبرهنة — الجمع ببرامج NAND-CIRC

لكل n∈Nn\in \N، لتكن ADDn:{0,1}2n→{0,1}n+1ADD_n:\{0,1\}^{2n}\rightarrow \{0,1\}^{n+1} الدالة التي، عند إعطائها x,x′∈{0,1}nx,x'\in \{0,1\}^n، تحسب تمثيل مجموع العددين اللذين يمثّلهما xx وx′x'. عندئذٍ يوجد ثابت c≤30c \leq 30 بحيث لكل nn يوجد برنامج NAND-CIRC من cncn سطرًا على الأكثر يحسب ADDnADD_n.[1]

/images/introtcs/lec_03a_computing_every_function-1.webp

وبعد أن يتوفّر لدينا الجمع، يمكننا استعمال خوارزمية المدرسة الابتدائية للحصول على الضرب أيضًا، وبذلك نحصل على المبرهنة التالية:

مبرهنة — الضرب ببرامج NAND-CIRC

لكل nn، لتكن MULTn:{0,1}2n→{0,1}2nMULT_n:\{0,1\}^{2n}\rightarrow \{0,1\}^{2n} الدالة التي، عند إعطائها x,x′∈{0,1}nx,x'\in \{0,1\}^n، تحسب تمثيل حاصل ضرب العددين اللذين يمثّلهما xx وx′x'. عندئذٍ يوجد ثابت cc بحيث لكل nn يوجد برنامج NAND-CIRC من cn2cn^2 سطرًا على الأكثر يحسب الدالة MULTnMULT_n.

ونُهمل البرهان، لكنّنا في تمرين — الضرب نطلب منك تقديم «برهان بنائي» (constructive proof) على صورة برنامج (بلغتك البرمجية المفضّلة) يُدخل إليه عدد nn ويُخرج شيفرة برنامج NAND-CIRC من 1000n21000n^2 سطرًا على الأكثر يحسب دالة MULTnMULT_n. وفي الواقع، يمكننا استعمال خوارزمية كارا تسوبا (Karatsuba) لإظهار أنّ هناك برنامج NAND-CIRC من O(nlog⁡23)O(n^{\log_2 3}) سطرًا لحساب MULTnMULT_n (ويمكننا أيضًا الحصول على تحسينات أُسّية (asymptotic) أبعد باستخدام خوارزميات أفضل).

دالة LOOKUPLOOKUP

ستلعب دالة LOOKUPLOOKUP دورًا مهمًا في هذا الفصل وفيما بعده. وهي معرَّفة على النحو التالي:

تعريف — دالة البحث

لكل kk، تُعرَّف دالةُ البحث (lookup) من الرتبة kk، LOOKUPk:{0,1}2k+k→{0,1}LOOKUP_k: \{0,1\}^{2^k+k}\rightarrow \{0,1\}، على النحو التالي: لكل x∈{0,1}2kx\in\{0,1\}^{2^k} وi∈{0,1}ki\in \{0,1\}^k، LOOKUPk(x,i)=xi LOOKUP_k(x,i)=x_i حيث xix_i تدلّ على المُدخل رقم ithi^{th} من xx، مع استعمال التمثيل الثنائي لتحديد ii بعدد ضمن {0,…,2k−1}\{0,\ldots,2^k - 1 \}.

تدخل دالة  مُدخلًا في ، نرمّز له بـ  (مع  و). والمخرج هو : الإحداثي رقم  من ، حيث نحدّد  بعدد في  باستعمال التمثيل الثنائي. في المثال أعلاه  و. وبما أنّ  هو التمثيل الثنائي للعدد ، فإن مُخرج  في هذه الحالة هو .

انظر الشكل التوضيحي لتجد تصويرًا توضيحيًا لدالة LOOKUP. ويبيّن الأمر أنّ لكل kk يمكننا حساب LOOKUPkLOOKUP_k باستعمال برنامج NAND-CIRC:

مبرهنة — دالة البحث

لكل k>0k>0، يوجد برنامج NAND-CIRC يحسب الدالة LOOKUPk:{0,1}2k+k→{0,1}LOOKUP_k: \{0,1\}^{2^k+k}\rightarrow \{0,1\}. وزيادةً على ذلك، فإن عدد الأسطر في هذا البرنامج هو 4⋅2k4\cdot 2^k على الأكثر.

ومن النتائج الفورية لـ مبرهنة — دالة البحث أنّ لكل k>0k>0 يمكن حساب LOOKUPkLOOKUP_k بدائرة منطقية (ذات بوابات AND وOR وNOT) من 8⋅2k8 \cdot 2^k بوابة على الأكثر.

بناء برنامج NAND-CIRC لدالة LOOKUPLOOKUP

نُثبت مبرهنة — دالة البحث بالاستدلال (induction). في الحالة k=1k=1، تُسقط LOOKUP1LOOKUP_1 العنصر (x0,x1,i)∈{0,1}3(x_0,x_1,i) \in \{0,1\}^3 على xix_i. بعبارة أخرى، إذا كان i=0i=0 فإنها تُخرج x0x_0، وإلّا فإنها تُخرج x1x_1، وهذا (حتى إعادة ترتيب المتغيّرات) هو نفسه الدالة IFIF المعروضة في الجمل الشرطية، والتي يمكن حسابها ببرنامج NAND-CIRC من أربعة أسطر.

وتمرينًا تمهيديًا على الحالة العامة kk، لننظر في الحالة k=2k=2. إذا أُعطينا مُدخلًا x=(x0,x1,x2,x3)x=(x_0,x_1,x_2,x_3) لدالة LOOKUP2LOOKUP_2 وفهرسًا i=(i0,i1)i=(i_0,i_1)، وكان البتّ الأعلى قيمةً i0i_0 من الفهرس يساوي 00، فإن LOOKUP2(x,i)LOOKUP_2(x,i) تساوي x0x_0 إذا كان i1=0i_1=0 وتساوي x1x_1 إذا كان i1=1i_1=1. وبالمثل، إذا كان البتّ الأعلى قيمةً i0i_0 يساوي 11، فإن LOOKUP2(x,i)LOOKUP_2(x,i) تساوي x2x_2 إذا كان i1=0i_1=0 وتساوي x3x_3 إذا كان i1=1i_1=1. وتُقال هذه بطريقة أخرى، إنّنا نستطيع كتابة LOOKUP2LOOKUP_2 على النحو التالي:

def LOOKUP2(X[0],X[1],X[2],X[3],i[0],i[1]):
    if i[0]==1:
        return LOOKUP1(X[2],X[3],i[1])
    else:
        return LOOKUP1(X[0],X[1],i[1])

أو بعبارة أخرى،

def LOOKUP2(X[0],X[1],X[2],X[3],i[0],i[1]):
    a = LOOKUP1(X[2],X[3],i[1])
    b = LOOKUP1(X[0],X[1],i[1])
    return IF( i[0],a,b)

والأعمّ من ذلك، كما تبيّن الملاحظة التالية، يمكننا حساب LOOKUPkLOOKUP_k باستعمال استدعاءين لـ LOOKUPk−1LOOKUP_{k-1} واستدعاء واحد لـ IFIF:

لمّة — تكرار دالة البحث

لكل k≥2k \geq 2، فإن LOOKUPk(x0,…,x2k−1,i0,…,ik−1)LOOKUP_k(x_0,\ldots,x_{2^k-1},i_0,\ldots,i_{k-1}) تساوي IF(i0,LOOKUPk−1(x2k−1,…,x2k−1,i1,…,ik−1),LOOKUPk−1(x0,…,x2k−1−1,i1,…,ik−1)) IF \left(i_0, LOOKUP_{k-1}(x_{2^{k-1}},\ldots,x_{2^k-1},i_1,\ldots,i_{k-1}), LOOKUP_{k-1}(x_0,\ldots,x_{2^{k-1}-1},i_1,\ldots,i_{k-1}) \right)

برهان

إذا كان البتّ الأعلى قيمةً i0i_{0} من ii يساوي صفرًا، فإن الفهرس ii يقع في {0,…,2k−1−1}\{0,\ldots,2^{k-1}-1\}، ومن ثمّ يمكننا إجراء البحث في «النصف الأول» من xx، وستكون نتيجة LOOKUPk(x,i)LOOKUP_k(x,i) نفسها هي a=LOOKUPk−1(x0,…,x2k−1−1,i1,…,ik−1)a=LOOKUP_{k-1}(x_0,\ldots,x_{2^{k-1}-1},i_1,\ldots,i_{k-1}). ومن جهة أخرى، إذا كان هذا البتّ الأعلى قيمةً i0i_{0} مساويًا لـ 11، فإن الفهرس يقع في {2k−1,…,2k−1}\{2^{k-1},\ldots,2^k-1\}، وعندئذٍ تكون نتيجة LOOKUPk(x,i)LOOKUP_k(x,i) نفسها هي b=LOOKUPk−1(x2k−1,…,x2k−1,i1,…,ik−1)b=LOOKUP_{k-1}(x_{2^{k-1}},\ldots,x_{2^k-1},i_1,\ldots,i_{k-1}). وعليه يمكننا حساب LOOKUPk(x,i)LOOKUP_k(x,i) بحساب aa وbb أولًا ثم بإخراج IF(i0,b,a)IF(i_0,b,a).

برهان مبرهنة — دالة البحث انطلاقًا من لمّة — تكرار دالة البحث. وبعد أن صار لدينا لمّة — تكرار دالة البحث، يمكننا إتمام برهان مبرهنة — دالة البحث. سنُثبت بالاستدلال على kk أنّ هناك برنامج NAND-CIRC من 4⋅(2k−1)4\cdot (2^k-1) سطرًا على الأكثر لـ LOOKUPkLOOKUP_k. وبالنسبة إلى k=1k=1، فإن ذلك يتّضح من برنامج IFIF ذي الأربعة أسطر الذي رأيناه من قبل. وبالنسبة إلى k>1k>1، نستعمل الشيفرة شبه البرمجية (pseudocode) التالية:

a = LOOKUP_(k-1)(X[0],...,X[2^(k-1)-1],i[1],...,i[k-1])
b = LOOKUP_(k-1)(X[2^(k-1)],...,X[2^(k-1)],i[1],...,i[k-1])
return IF(i[0],b,a)

إذا تركنا L(k)L(k) تدلّ على عدد الأسطر المطلوبة لـ LOOKUPkLOOKUP_k، فإن الشيفرة شبه البرمجية أعلاه تُظهر أنّ L(k)≤2L(k−1)+4  . L(k) \leq 2L(k-1)+4 \;. وبموجب فرضية الاستدلال لدينا L(k−1)≤4(2k−1−1)L(k-1) \leq 4(2^{k-1}-1)، نحصل على أنّ L(k)≤2⋅4(2k−1−1)+4=4(2k−1)L(k) \leq 2\cdot 4 (2^{k-1}-1) + 4 = 4(2^k - 1) وهو ما كنا نريد إثباته. انظر الشكل التوضيحي لتجد رسمًا بيانيًا لعدد الأسطر الفعلي في تنقيذنا لدالة LOOKUPkLOOKUP_k.

/images/introtcs/lec_03a_computing_every_function-2.webp

حساب كل دالة

حتى هذه اللحظة نعرف الحقائق التالية عن برامج NAND-CIRC (وعن الدوائر المنطقية، وبالتالي، وعن نماذجنا الأخرى المتكافئة معها):

  1. إنّها تحسب على الأقل بعض الدوال غير البديهية.

  2. إنّ ابتكار برامج NAND-CIRC لدوال مختلفة مهمّة بالغة.

لذلك لا ألوم القارئ إن لم يكن متلهّفًا على نحو خاص لمطَوّلة من الأمثلة للدوال التي يمكن حسابها ببرامج NAND-CIRC. غير أنّه تبيّن أنّنا لن نحتاج إلى هذا، إذ يمكننا في ضربة واحدة أن نبيّن أنّ برامج NAND-CIRC تستطيع حساب كل دالة متناهية:

مبرهنة — شمولية NAND

يوجد ثابت ما c>0c>0 بحيث لكل n,m>0n,m>0 ولكل دالة f:{0,1}n→{0,1}mf: \{0,1\}^n\rightarrow \{0,1\}^m، يوجد برنامج NAND-CIRC من c⋅m2nc \cdot m 2^n سطرًا على الأكثر يحسب الدالة ff.

وبموجب مبرهنة — التكافؤ بين نماذج الحوسبة المتناهية، فإن نماذج دوائر NAND وبرامج NAND-CIRC وبرامج AON-CIRC والدوائر المنطقية متكافئة فيما بينها جميعًا، ومن ثمّ فإن مبرهنة — شمولية NAND تنطبق على كل هذه النماذج. وبالذات، المبرهنة التالية تكافئ مبرهنة — شمولية NAND:

مبرهنة — شمولية الدوائر المنطقية

يوجد ثابت ما c>0c>0 بحيث لكل n,m>0n,m>0 ولكل دالة f:{0,1}n→{0,1}mf: \{0,1\}^n\rightarrow \{0,1\}^m، توجد دائرة منطقية من c⋅m2nc \cdot m 2^n بوابة على الأكثر تحسب الدالة ff.

كل دالة متناهية يمكن حسابها بدائرة منطقية كبيرة بما يكفي.

حدود محسّنة. وإن كانت أهميتها ليست بالغة بالنسبة لنا، فمن الممكن تحسين برهان مبرهنة — شمولية NAND وتشذيب عامل إضافي من nn، فضلًا عن تحسين الثابت cc، وبذلك نُثبت أنّ لكل ϵ>0\epsilon>0 وm∈Nm\in \N وnn كبير بما يكفي، إذا كانت f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m فإن ff يمكن حسابها بدائرة NAND من (1+ϵ)m⋅2nn(1+\epsilon)\tfrac{m\cdot 2^n}{n} بوابة على الأكثر. برهان هذه النتيجة خارج نطاق هذا الكتاب، لكنّنا نناقش في تحسين بمعامل n (اختياري) كيفية الحصول على حدٍّ من الشكل O(m⋅2nn)O(\tfrac{m \cdot 2^n}{n})؛ وانظر أيضًا الملاحظات الببليوغرافية.

برهان شمولية NAND

لإثبات مبرهنة — شمولية NAND، نحتاج إلى إعطاء دائرة NAND، أو بما يكافئها برنامج NAND-CIRC، لكل دالة ممكنة. سنقصر اهتمامنا على حالة الدوال المنطقية (أي m=1m=1). يطلب منك تمرين — دوال متعدّدة البتات توسيع البرهان على جميع قيم mm. يمكن تحديد دالة F:{0,1}n→{0,1}F: \{0,1\}^n\rightarrow \{0,1\} بجدول من قيمها لكل واحد من الـ 2n2^n مُدخلًا. على سبيل المثال، يصف الجدول أدناه دالة بعينها G:{0,1}4→{0,1}G: \{0,1\}^4 \rightarrow \{0,1\}:[2]

المُدخل (xx) المُخرج (G(x)G(x))
00000000 1
00010001 1
00100010 0
00110011 0
01000100 1
01010101 0
01100110 0
01110111 1
10001000 0
10011001 0
10101010 0
10111011 0
11001100 1
11011101 1
11101110 1
11111111 1

جدول: مثال على دالة G:{0,1}4→{0,1}G:\{0,1\}^4 \rightarrow \{0,1\}.

لكل x∈{0,1}4x\in \{0,1\}^4، لدينا G(x)=LOOKUP4(1100100100001111,x)G(x)=LOOKUP_4(1100100100001111,x)، ومن ثمّ فالشيفرة التالية هي «شيفرة شبه برمجية» (pseudocode) بلغة NAND-CIRC لحساب GG باستعمال السكر النحوي للإجراء LOOKUP_4.

G0000 = 1
G1000 = 1
G0100 = 0
...
G0111 = 1
G1111 = 1
Y[0] = LOOKUP_4(G0000,G1000,...,G1111,
                X[0],X[1],X[2],X[3])

يمكننا ترجمة هذه الشيفرة شبه البرمجية إلى برنامج NAND-CIRC حقيقي بإضافة ثلاثة أسطر لتعريف المتغيّرين zero وone اللذين يُهيَّآن على 00 و11 على الترتيب، ثم باستبدال عبارة مثل Gxxx = 0 بـ Gxxx = NAND(one,one)، وعبارة مثل Gxxx = 1 بـ Gxxx = NAND(zero,zero). وسيُستبدَل الاستدعاء إلى LOOKUP_4 ببرنامج NAND-CIRC الذي يحسب LOOKUP4LOOKUP_4، مع تلقيم المُداخل المناسبة.

لم يكن في الاستدلال أعلاه شيء يخصّ الدالة GG بعينها. ولكل دالة F:{0,1}n→{0,1}F: \{0,1\}^n \rightarrow \{0,1\}، يمكننا كتابة برنامج NAND-CIRC يفعل ما يلي:

  1. تهيئة 2n2^n متغيّرًا من الشكل F00...0 إلى F11...1 بحيث لكل z∈{0,1}nz\in\{0,1\}^n يُسنَد إلى المتغيّر المقابل لـ zz القيمة F(z)F(z).

  2. حساب LOOKUPnLOOKUP_n على الـ 2n2^n متغيّرًا المهيّأة في الخطوة السابقة، مع كون متغيّر الفهرس هو متغيّرات المُدخل X[00 ],...,X[n−1n-1 ]. أي أنّنا، تمامًا كما في الشيفرة شبه البرمجية للدالة G أعلاه، نستعمل Y[0] = LOOKUP(F00..00,...,F11..1,X[0],..,X[n−1n-1])

مجموع عدد الأسطر في البرنامج الناتج هو 3+2n3+2^n سطرًا لتهيئة المتغيّرات، إضافةً إلى 4⋅2n4\cdot 2^n سطرًا ندفع ثمنها مقابل حساب LOOKUPnLOOKUP_n. وهذا يُتمّ برهان مبرهنة — شمولية NAND.

ملاحظة — النتيجة في منظور

ولأنّ مبرهنة — شمولية NAND تبدو مذهلة في البداية، فمن المحتمل — إذا أعدنا النظر إليها — ألّا يكون مُدهشًا البتّة أن كل دالة متناهية يمكن حسابها ببرنامج NAND-CIRC. فبعد كلّ شيء، يمكن تمثيل دالة متناهية F:{0,1}n→{0,1}mF: \{0,1\}^n \rightarrow \{0,1\}^m بمجرّد قائمة مُخرجاتها لكل واحد من الـ 2n2^n قيمة مُدخل. ولذلك يكون من المعقول أن نتمكّن من كتابة برنامج NAND-CIRC بحجم مماثل لحسابها. والأكثر إثارةً للاهتمام هو أنّ بعض الدوال، مثل الجمع والضرب، لها تمثيل أكثر كفاءة بكثير: تمثيل يتطلّب O(n2)O(n^2) سطرًا أو حتى أقل.

تحسين بمعامل nn (اختياري)

بمزيد من العناية قليلًا، يمكننا تحسين حدّ مبرهنة — شمولية NAND وإظهار أنّ كل دالة F:{0,1}n→{0,1}mF:\{0,1\}^n \rightarrow \{0,1\}^m يمكن حسابها ببرنامج NAND-CIRC من O(m2n/n)O(m 2^n/n) سطرًا على الأكثر. بعبارة أخرى، يمكننا إثبات الصيغة المحسّنة التالية:

مبرهنة — شمولية دوائر NAND، بحدّ محسّن

يوجد ثابت c>0c>0 بحيث لكل n,m>0n,m>0 ولكل دالة f:{0,1}n→{0,1}mf: \{0,1\}^n\rightarrow \{0,1\}^m، يوجد برنامج NAND-CIRC من c⋅m2n/nc \cdot m 2^n / n سطرًا على الأكثر يحسب الدالة ff.[3]

برهان

كما من قبل، يكفي إثبات الحالة التي يكون فيها m=1m=1. ومن ثمّ نترك f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\}، وهدفنا هو إثبات أنّ هناك برنامج NAND-CIRC من O(2n/n)O(2^n/n) سطرًا (أو بما يكافئه دائرة منطقية من O(2n/n)O(2^n/n) بوابة) يحسب ff.

نترك k=log⁡(n−2log⁡n)k= \log(n-2\log n) (وسيصير سبب هذا الاختيار واضحًا لاحقًا). نعرّف الدالة g:{0,1}k→{0,1}2n−kg:\{0,1\}^k \rightarrow \{0,1\}^{2^{n-k}} على النحو التالي: g(a)=f(a0n−k)f(a0n−k−11)⋯f(a1n−k)  . g(a) = f(a0^{n-k})f(a0^{n-k-1}1) \cdots f(a1^{n-k}) \;. بعبارة أخرى، إذا استعملنا التمثيل الثنائي المعتاد لتحديد الأعداد {0,…,2n−k−1}\{0,\ldots, 2^{n-k}-1 \} مع النصوص {0,1}n−k\{0,1\}^{n-k}، فإن لكل a∈{0,1}ka\in \{0,1\}^k وb∈{0,1}n−kb\in \{0,1\}^{n-k} g(a)b=f(ab)  . g(a)_b = f(ab) \;.

/images/introtcs/lec_03a_computing_every_function-3.webp

تعني المعادلة أنّ لكل x∈{0,1}nx\in \{0,1\}^n، إذا كتبنا x=abx=ab مع a∈{0,1}ka\in \{0,1\}^k وb∈{0,1}n−kb\in \{0,1\}^{n-k}، فإننا نستطيع حساب f(x)f(x) بحساب النص T=g(a)T=g(a) الطويل من 2n−k2^{n-k} أولًا، ثم بحساب LOOKUPn−k(T  ,  b)LOOKUP_{n-k}(T\;,\; b) لاسترجاع العنصر من TT عند الموضع المقابل لـ bb (انظر الشكل التوضيحي). تكلّفة حساب LOOKUPn−kLOOKUP_{n-k} هي O(2n−k)O(2^{n-k}) سطرًا/بوابة، وتكلّفة حساب ff مقيسةً بأسطر NAND-CIRC (أو البوابات المنطقية) هي على الأكثر cost(g)+O(2n−k)  , cost(g) + O(2^{n-k}) \;, حيث cost(g)cost(g) هو عدد العمليات (أي أسطر برامج NAND-CIRC أو بوابات الدائرة) اللازمة لحساب gg.

ولإتمام البرهان نحتاج إلى إعطاء حدٍّ لـ cost(g)cost(g). وبما أنّ gg دالة تُسقط {0,1}k\{0,1\}^k على {0,1}2n−k\{0,1\}^{2^{n-k}}، يمكننا أيضًا التفكير فيها بوصفها مجموعة من 2n−k2^{n-k} دالة g0,…,g2n−k−1:{0,1}k→{0,1}g_0,\ldots, g_{2^{n-k}-1}: \{0,1\}^k \rightarrow \{0,1\}، حيث gi(x)=g(a)ig_i(x) = g(a)_i لكل a∈{0,1}ka\in \{0,1\}^k وi∈[2n−k]i\in [2^{n-k}]. (أي أنّ gi(a)g_i(a) هو البتّ رقم ii من g(a)g(a).) بشكل ساذج، يمكننا استعمال مبرهنة — شمولية NAND لحساب كل gig_i في O(2k)O(2^k) سطرًا، لكن عندئذٍ تكون التكلّفة الكلية O(2n−k⋅2k)=O(2n)O(2^{n-k} \cdot 2^k) = O(2^n) وهو ما لا يوفّر علينا شيئًا. غير أنّ الملاحظة الحاسمة هي أنّه لا يوجد سوى 22k2^{2^k} دالة متمايزة تُسقط {0,1}k\{0,1\}^k على {0,1}\{0,1\}. على سبيل المثال، إذا كانت g17g_{17} هي الدالة نفسها g67g_{67}، فهذا يعني أنّنا إذا كنّا قد حسبنا g17(a)g_{17}(a) نستطيع عندئذٍ حساب g67(a)g_{67}(a) بعدد عمليات ثابت فقط: بمجرّد نسخ القيمة نفسها! وبشكل عام، إذا كانت لديك مجموعة من NN دالة g0,…,gN−1g_0,\ldots,g_{N-1} تُسقط {0,1}k\{0,1\}^k على {0,1}\{0,1\}، منها SS على الأكثر متمايزة، فإن لكل قيمة a∈{0,1}ka\in \{0,1\}^k نستطيع حساب القيم g0(a),…,gN−1(a)g_0(a),\ldots,g_{N-1}(a) البالغة NN قيمة باستعمال O(S⋅2k+N)O(S\cdot 2^k + N) عملية على الأكثر (انظر الشكل التوضيحي).

/images/introtcs/lec_03a_computing_every_function-4.webp

في حالتنا، ولأنّ عدد الدوال المتمايزة التي تُسقط {0,1}k\{0,1\}^k على {0,1}\{0,1\} هو 22k2^{2^k} على الأكثر، يمكننا حساب الدالة gg (وبالتالي أيضًا ff بموجب المعادلة) باستعمال O(22k⋅2k+2n−k)O(2^{2^k} \cdot 2^k + 2^{n-k}) عمليات. وكل ما تبقّى الآن هو التعويض في المعادلة عن اختيارنا k=log⁡(n−2log⁡n)k = \log (n-2\log n). وبالتعريف، 2k=n−2log⁡n2^k = n-2\log n، ما يعني أنّ المعادلة يمكن تحديد حدٍّ له كـ O(2n−2log⁡n⋅(n−2log⁡n)+2n−log⁡(n−2log⁡n))≤ O\left(2^{n-2\log n} \cdot (n-2\log n) + 2^{n-\log(n-2\log n)}\right) \leq

O(2nn2⋅n+2nn−2log⁡n)≤O(2nn+2n0.5n)=O(2nn) O\left(\tfrac{2^n}{n^2} \cdot n + \tfrac{2^n}{n-2\log n} \right) \leq O\left(\tfrac{2^n}{n} + \tfrac{2^n}{0.5n} \right) = O\left( \tfrac{2^n}{n} \right)

وهو ما كنا نريد إثباته. (استعملنا أعلاه الحقيقة أنّ n−2log⁡n≥0.5log⁡nn - 2\log n \geq 0.5 \log n لـ nn كبير بما يكفي.)

وباستعمال الصلة بين برامج NAND-CIRC والدوائر المنطقية، فإنّ النتيجة الفورية لـ مبرهنة — شمولية دوائر NAND، بحدّ محسّن هي التحسين التالي لـ مبرهنة — شمولية الدوائر المنطقية:

مبرهنة — شمولية الدوائر المنطقية، بحدّ محسّن

يوجد ثابت ما c>0c>0 بحيث لكل n,m>0n,m>0 ولكل دالة f:{0,1}n→{0,1}mf: \{0,1\}^n\rightarrow \{0,1\}^m، توجد دائرة منطقية من c⋅m2n/nc \cdot m 2^n / n بوابة على الأكثر تحسب الدالة ff.

حساب كل دالة: برهان بديل

مبرهنة — شمولية الدوائر المنطقية نتيجة أساسية في نظرية الحوسبة (وفي ممارستها أيضًا!). في هذا القسم نعرض برهانًا بديلًا لهذه الحقيقة الأساسية القائلة إنّ الدوائر المنطقية تستطيع حساب كل دالة متناهية. ويعطي هذا البرهان البديل حدًّا كمّيًا أضعف قليلًا لعدد البوابات، لكنّ له ميزة أنّه أبسط، إذ يعمل مباشرةً مع الدوائر ويتفادى استعمال آليات السكر النحوي بأسرها. (غير أنّ تلك الآليات مفيدة في ذاتها، وستجد لها تطبيقات أخرى لاحقًا.)

مبرهنة — شمولية الدوائر المنطقية (صياغة بديلة)

يوجد ثابت ما c>0c>0 بحيث لكل n,m>0n,m>0 ولكل دالة f:{0,1}n→{0,1}mf: \{0,1\}^n\rightarrow \{0,1\}^m، توجد دائرة منطقية من c⋅m⋅n2nc \cdot m\cdot n 2^n بوابة على الأكثر تحسب الدالة ff.

معطاةً الدالة ، نُعرِّف  لتكون مجموعة المُداخل التي تتحقّق فيها ، ولاحظ أنّ . يمكننا التعبير عن  بأنها OR لـ  لكل ، حيث تُعرَّف الدالة  (لكل ) على النحو التالي:  إذا ونحو فقط إذا كان . ويمكننا حساب OR لـ  قيمة باستعمال  بوابة OR ثنائية المُدخل. لذلك، إذا كانت لدينا دائرة بحجم  لحساب  لكل ، فيمكننا حساب  باستعمال دائرة بحجم .

فكرة البرهان مصوّرة في الشكل التوضيحي. وكما من قبل، يكفي التركيز على الحالة التي يكون فيها m=1m=1 (أي أنّ الدالة ff لها مُخرج واحد)، إذ يمكننا دائمًا توسيع ذلك إلى الحالة m>1m>1 بالنظر في تركيب mm دائرة، كل واحدة تحسب بتًّا مختلفًا من مُخرجات الدالة ff. نبدأ بإظهار أنّ لكل α∈{0,1}n\alpha \in \{0,1\}^n توجد دائرة بحجم O(n)O(n) تحسب الدالة δα:{0,1}n→{0,1}\delta_\alpha:\{0,1\}^n \rightarrow \{0,1\} المعرَّفة على النحو التالي: δα(x)=1\delta_\alpha(x)=1 إذا ونحو فقط إذا كان x=αx=\alpha (أي أنّ δα\delta_\alpha تُخرج 00 على كل المُداخل ما عدا المُدخل α\alpha). وعندئذٍ يمكننا كتابة أي دالة f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\} بأنها OR لـ 2n2^n دالة δα\delta_\alpha على الأكثر، حيث α\alpha تلك القيم التي تتحقّق عندها f(α)=1f(\alpha)=1.

برهان

نُثبت المبرهنة للحالة m=1m=1. ويمكن توسيع النتيجة إلى m>1m>1 كما من قبل (انظر أيضًا تمرين — دوال متعدّدة البتات). لتكن f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\}. سنُثبت أنّ هناك دائرة منطقية بحجم O(n⋅2n)O(n\cdot 2^n) لحساب ff عبر الخطوات التالية:

  1. نُبيّن أنّ لكل α∈{0,1}n\alpha\in \{0,1\}^n توجد دائرة بحجم O(n)O(n) تحسب الدالة δα:{0,1}n→{0,1}\delta_\alpha:\{0,1\}^n \rightarrow \{0,1\}، حيث δα(x)=1\delta_\alpha(x)=1 إذا ونحو فقط إذا كان x=αx=\alpha.

  2. ثم نُبيّن أنّ هذا يقتضي وجود دائرة بحجم O(n⋅2n)O(n\cdot 2^n) تحسب ff، بكتابة f(x)f(x) بأنها OR لـ δα(x)\delta_\alpha(x) لكل α∈{0,1}n\alpha\in \{0,1\}^n بحيث f(α)=1f(\alpha)=1. (وإذا كانت ff هي دالة الصفر الثابت، وبالتالي لا توجد مثل هذه القيمة α\alpha، فيمكننا استعمال الدائرة f(x)=x0∧x‾0f(x) = x_0 \wedge \overline{x}_0.)

نبدأ بالخطوة 1:

ادّعاء: لكل α∈{0,1}n\alpha \in \{0,1\}^n، نُعرِّف δα:{0,1}n\delta_\alpha:\{0,1\}^n على النحو التالي: δα(x)={1x=α0otherwise  . \delta_\alpha(x) = \begin{cases}1 & x=\alpha \\ 0 & \text{otherwise} \end{cases} \;. عندئذٍ توجد دائرة منطقية تستعمل 2n2n بوابة على الأكثر تحسب δα\delta_\alpha.

برهان الادّعاء: البرهان مصوَّر في الشكل التوضيحي. مثالًا، لننظر في الدالة δ011:{0,1}3→{0,1}\delta_{011}:\{0,1\}^3 \rightarrow \{0,1\}. تُخرج هذه الدالة 11 على xx إذا ونحو فقط إذا كان x0=0x_0=0 وx1=1x_1=1 وx2=1x_2=1، ومن ثمّ يمكننا كتابة δ011(x)=x0‾∧x1∧x2\delta_{011}(x) = \overline{x_0} \wedge x_1 \wedge x_2، وهو ما ينطوي على دائرة منطقية ببوابة NOT واحدة وبوابة AND ثانية. وبشكل عام، لكل α∈{0,1}n\alpha \in \{0,1\}^n يمكننا التعبير عن δα(x)\delta_{\alpha}(x) على صورة (x0=α0)∧(x1=α1)∧⋯∧(xn−1=αn−1)(x_0 = \alpha_0) \wedge (x_1 = \alpha_1) \wedge \cdots \wedge (x_{n-1} = \alpha_{n-1})، حيث إذا كان αi=0\alpha_i=0 نستبدل xi=αix_i = \alpha_i بـ xi‾\overline{x_i}، وإذا كان αi=1\alpha_i=1 نستبدل xi=αix_i=\alpha_i ببساطة xix_i. وهذا يعطي دائرة تحسب δα\delta_\alpha باستعمال nn بوابة AND وnn بوابة NOT على الأكثر، أي بمجموع 2n2n بوابة على الأكثر.

والآن، لكل دالة f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\}، يمكننا كتابة

f(x)=δx0(x)∨δx1(x)∨⋯∨δxN−1(x) f(x) = \delta_{x_0}(x) \vee \delta_{x_1}(x) \vee \cdots \vee \delta_{x_{N-1}}(x)

حيث S={x0,…,xN−1}S=\{ x_0 ,\ldots, x_{N-1}\} هي مجموعة المُداخل التي تُخرج فيها ff القيمة 11. (ولرؤية ذلك، يمكنك التحقّق من أنّ الطرف الأيمن من المعادلة يُقيَّم إلى 11 على x∈{0,1}nx\in \{0,1\}^n إذا ونحو فقط إذا كان xx في المجموعة SS.)

لذلك يمكننا حساب ff باستعمال دائرة منطقية من 2n2n بوابة على الأكثر لكل واحدة من الدوال δxi\delta_{x_i} وعددها NN، ثمّ دمجها مع NN بوابة OR على الأكثر، فنحصل بذلك على دائرة من 2n⋅N+N2n\cdot N + N بوابة على الأكثر. وبما أنّ S⊆{0,1}nS \subseteq \{0,1\}^n، فإنّ حجمها NN هو 2n2^n على الأكثر، ومن ثمّ فإنّ مجموع عدد البوابات في هذه الدائرة هو O(n⋅2n)O(n\cdot 2^n).

/images/introtcs/lec_03a_computing_every_function-5.webp

الصنف SIZEn,m(s)SIZE_{n,m}(s)

لقد رأينا أنّ كل دالة f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m يمكن حسابها بدائرة بحجم O(m⋅2n)O(m\cdot 2^n)، وأنّ بعض الدوال (مثل الجمع والضرب) يمكن حسابها بدوائر أصغر بكثير. نُعرِّف SIZEn,m(s)SIZE_{n,m}(s) لتكون مجموعة الدوال التي تُسقط nn بتًّا على mm بتًّا والتي يمكن حسابها بدوائر NAND من ss بوابة على الأكثر (أو بما يكافئها، ببرامج NAND-CIRC من ss سطرًا على الأكثر). والتعريف الرسمي هو كما يلي:

تعريف — صنف حجم الدوال

لكل الأعداد الطبيعية n,m,sn,m,s، لتُدلّ SIZEn,m(s)SIZE_{n,m}(s) على مجموعة كل الدوال f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m بحيث توجد دائرة NAND من ss بوابة على الأكثر تحسب ff. ونرمز بـ SIZEn(s)SIZE_n(s) إلى المجموعة SIZEn,1(s)SIZE_{n,1}(s). ولكل عدد صحيح s≥1s \geq 1، نضع SIZE(s)=∪n,mSIZEn,m(s)SIZE(s) = \cup_{n,m} SIZE_{n,m}(s) لتكون مجموعة كل الدوال ff التي توجد دائرة NAND من ss بوابة على الأكثر تحسب ff.

يصوّر الشكل التوضيحي المجموعة SIZEn,1(s)SIZE_{n,1}(s). لاحظ أنّ SIZEn,m(s)SIZE_{n,m}(s) مجموعة دوال لا مجموعة برامج! فالسؤال عمّا إذا كان برنامج أو دائرة عضوًا في SIZEn,m(s)SIZE_{n,m}(s) هو خطأ في التصنيف (category error) بالمعنى المذكور في الشكل التوضيحي. وكما ناقشنا في المواصفة مقابل التنفيذ (مرة أخرى) (وميّزوا الدوال عن البرامج!)، فإنّ التمييز بين البرامج و_الدوال_ بالغ الأهمية. وينبغي أن تتذكّر دائمًا أنّ البرنامج يحسب الدالة، لكنه ليس مساويًا لها. وبالذات، كما رأينا، قد يوجد أكثر من برنامج واحد يحسب الدالة نفسها.

هناك  دالة تُسقط  على ، وعددٌ لا نهائي من الدوائر ذات  بتًّا مُدخلًا وبتّ خرج واحد. كل دائرة تحسب دالة واحدة، لكن كل دالة يمكن حسابها بدوائر عديدة. نقول إنّ  إذا كانت أصغر دائرة تحسب  لها  بوابة أو أقل. مثلًا . وتُظهر NAND-univ-thm أنّ كل دالة  قابلة للحساب بدائرة ما من  بوابة على الأكثر، ومن ثمّ فإنّ  يقابل مجموعة كل الدوال من  إلى .

ولأَنّنا عرّفنا SIZEn(s)SIZE_n(s) بالنسبة إلى بوابات NAND، فسنحصل على الصنف نفسه جوهريًا لو عرّفناه بالنسبة إلى بوابات AND/OR/NOT:

لمّة

لتُدلّ SIZEn,mAON(s)SIZE^{AON}_{n,m}(s) على مجموعة كل الدوال f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m التي يمكن حسابها بدائرة منطقية AND/OR/NOT من ss بوابة على الأكثر. عندئذٍ، SIZEn,m(s/2)⊆SIZEn,mAON(s)⊆SIZEn,m(3s) SIZE_{n,m}(s/2) \subseteq SIZE^{AON}_{n,m}(s) \subseteq SIZE_{n,m}(3s)

برهان

إذا كانت ff قابلة للحساب بدائرة NAND من s/2s/2 بوابة على الأكثر، فإنّه باستبدال كل NAND ببوابتَي NOT وAND يمكننا الحصول على دائرة منطقية AND/OR/NOT من ss بوابة على الأكثر تحسب ff. ومن جهة أخرى، إذا كانت ff قابلة للحساب بدائرة منطقية AND/OR/NOT من ss بوابة على الأكثر، فإنّها بموجب مبرهنة — NAND عملية شاملة قابلة للحساب بدائرة NAND من 3s3s بوابة على الأكثر.

/images/introtcs/lec_03a_computing_every_function-6.webp

يمكن صياغة النتائج التي رأيناها في هذا الفصل على صورة أنّ ADDn∈SIZE2n,n+1(100n)ADD_n \in SIZE_{2n,n+1}(100 n) وMULTn∈SIZE2n,2n(10000nlog⁡23)MULT_n \in SIZE_{2n,2n}(10000 n^{\log_2 3}). وتُظهر مبرهنة — شمولية NAND أنّه لبعض الثابت cc، تكون SIZEn,m(cm2n)SIZE_{n,m}(c m 2^n) مساوية لمجموعة كل الدوال من {0,1}n\{0,1\}^n إلى {0,1}m\{0,1\}^m.

ملاحظة — الدوال المتناهية في مقابل اللانهائية

على خلاف لغات البرمجة مثل Python أو C أو JavaScript، فإن لغتَي البرمجة NAND-CIRC وAON-CIRC لا تملكان مصفوفات (arrays). ولبرنامج NAND-CIRC PP عددٌ ثابت nn من مُدخلات وعددٌ ثابت mm من متغيّرات مُخرج. ولذلك، مثلًا، لا يوجد برنامج NAND-CIRC واحد يستطيع حساب دالة الزيادة INC:{0,1}∗→{0,1}∗INC:\{0,1\}^* \rightarrow \{0,1\}^* التي تُسقط نصًّا xx (الذي نحدّده بعددٍ عبر التمثيل الثنائي) على النص الذي يمثّل x+1x+1. بل إنّ لكل n>0n>0 يوجد برنامج NAND-CIRC PnP_n يحسب التقييد INCnINC_n للدالة INCINC على المُداخل من الطول nn. وبما أنّه يمكن إظهار أنّه لكل n>0n>0 يوجد مثل هذا البرنامج PnP_n بطول 10n10n على الأكثر، فإنّ INCn∈SIZEn,n+1(10n)INC_n \in SIZE_{n,n+1}(10n) لكل n>0n>0.

أمّا في الوقت الحالي فسيتركّز اهتمامنا على الدوال المتناهية، لكنّنا سنناقش لاحقًا في الحساب غير المنتظم كيفية توسيع تعريف تعقيد الحجم على الدوال ذات أطوال المُداخل غير المحدودة.

تمرين محلول — انغلاق SIZESIZE تحت التكامل.

في هذا التمرين نُثبت خاصيةَ انغلاق (closure property) معيّنة للصنف SIZEn(s)SIZE_n(s). أي أنّنا نُبيّن أنّه إذا كانت ff في هذا الصنف فإنّ تكاملها — أي الدالة g(x)=1−f(x)g(x)=1-f(x) — يكون كذلك فيه (حتى حدٍّ جُمعِي صغير).

أثبِت أنّه يوجد ثابت cc بحيث لكل f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\} وs∈Ns\in \N، إذا كان f∈SIZEn(s)f \in SIZE_n(s) فإنّ 1−f∈SIZEn(s+c)1-f \in SIZE_n(s+c).

الحل

إذا كان f∈SIZEn(s)f\in SIZE_n(s) فإنّ هناك برنامج NAND-CIRC PP من ss سطرًا يحسب ff. يمكننا إعادة تسمية المتغيّر Y[0] في PP إلى متغيّر اسمه temp وإضافة السطر

Y[0] = NAND(temp,temp)

في نهايته تمامًا للحصول على برنامج P′P' يحسب 1−f1-f.

خلاصة

  • يمكننا تعريف فكرة حساب دالة عبر «لغة برمجة» مبسّطة، بحيث إنّ حساب الدالة FF في TT خطوة يقابل وجود برنامج NAND-CIRC من TT سطرًا يحسب FF.
  • ولأَنّ لغة البرمجة NAND-CIRC لا تملك سوى عملية واحدة، فإنّ العمليات الأخرى مثل الدوال والتنفيذ الشرطي يمكن تنفيذها باستعمالها.
  • كل دالة f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m يمكن حسابها بدائرة من O(m2n)O(m 2^n) بوابة على الأكثر (وفي الحقيقة من O(m2n/n)O(m 2^n/n) بوابة على الأكثر).
  • أحيانًا (وربما دائمًا؟) يمكننا ترجمة خوارزمية كفؤة لحساب ff إلى دائرة تحسب ff بعدد بوابات يقارن عدد خطوات هذه الخوارزمية.

تمارين

تمرين — الاقتران

يطلب منك هذا التمرين إعطاء خريطة (map) واحد-إلى-واحد (one-to-one) من N2\N^2 إلى N\N. ويمكن أن يكون ذلك مفيدًا لتنفيذ المصفوفات ثنائية الأبعاد بوصفها «سكرًا نحويًا» في لغات البرمجة التي لا تملك إلّا مصفوفات أحادية البعد.

  1. أثبِت أنّ الخريطة F(x,y)=2x3yF(x,y)=2^x3^y هي خريطة واحد-إلى-واحد من N2\N^2 إلى N\N.

  2. بيّن أنّه توجد خريطة واحد-إلى-واحد F:N2→NF:\N^2 \rightarrow \N بحيث لكل x,yx,y، لدينا F(x,y)≤100⋅max⁡{x,y}2+100F(x,y) \leq 100\cdot \max\{x,y\}^2+100.

  3. لكل kk، بيّن أنّه توجد خريطة واحد-إلى-واحد F:Nk→NF:\N^k \rightarrow \N بحيث لكل x0,…,xk−1∈Nx_0,\ldots,x_{k-1} \in \N، لدينا F(x0,…,xk−1)≤100⋅(x0+x1+…+xk−1+100k)kF(x_0,\ldots,x_{k-1}) \leq 100 \cdot (x_0+x_1+\ldots+x_{k-1}+100k)^k.

تمرين — حساب MUX

أثبِت أنّ برنامج NAND-CIRC أدناه يحسب الدالة MUXMUX (أي LOOKUP1LOOKUP_1)، حيث تساوي MUX(a,b,c)MUX(a,b,c) القيمة aa إذا كان c=0c=0 والقيمة bb إذا كان c=1c=1:

t = NAND(X[2],X[2])
u = NAND(X[0],t)
v = NAND(X[1],X[2])
Y[0] = NAND(u,v)

تمرين — على الأقل اثنين / الأغلبية

اكتب برنامج NAND-CIRC من 6 أسطر على الأكثر يحسب الدالة MAJ:{0,1}3→{0,1}MAJ:\{0,1\}^3 \rightarrow \{0,1\} حيث MAJ(a,b,c)=1MAJ(a,b,c) = 1 إذا ونحو فقط إذا كان a+b+c≥2a+b+c \geq 2.

تمرين — الجمل الشرطية

في هذا التمرين سنستكشف مبرهنة — السكر النحوي للجمل الشرطية: تحويل برامج NAND-CIRC-IF التي تستعمل شيفرة مثل if .. then .. else .. إلى برامج NAND-CIRC معيارية.

  1. قدّم «برهانًا بالشيفرة» لـ مبرهنة — السكر النحوي للجمل الشرطية: برنامجًا بلغة برمجة من اختيارك يحوّل برنامج NAND-CIRC-IF من الشكل PP إلى برنامج NAND-CIRC «خالٍ من السكر» P′P' يحسب الدالة نفسها. انظر الحاشية للتلميح.[4]

  2. أثبِت العبارة التالية، وهي جوهر مبرهنة — السكر النحوي للجمل الشرطية: افترض أنّه يوجد برنامج NAND-CIRC من ss سطرًا يحسب f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\}، وبرنامج NAND-CIRC من s′s' سطرًا يحسب g:{0,1}n→{0,1}g:\{0,1\}^n \rightarrow \{0,1\}. أثبِت أنّه يوجد برنامج NAND-CIRC من s+s′+10s+s'+10 أسطر على الأكثر يحسب الدالة h:{0,1}n+1→{0,1}h:\{0,1\}^{n+1} \rightarrow \{0,1\} حيث h(x0,…,xn−1,xn)h(x_0,\ldots,x_{n-1},x_n) تساوي f(x0,…,xn−1)f(x_0,\ldots,x_{n-1}) إذا كان xn=0x_n=0 وتساوي g(x0,…,xn−1)g(x_0,\ldots,x_{n-1}) في غير ذلك. (جميع البرامج في هذا البند هي برامج NAND-CIRC معيارية «خالية من السكر».)

تمرين — نصف الجامع والجامع الكامل

  1. نصف الجامع (half adder) هو الدالة HA:{0,1}2:→{0,1}2HA:\{0,1\}^2 :\rightarrow \{0,1\}^2 التي تقابل إضافة بتّين ثنائيين. أي أنّه لكل a,b∈{0,1}a,b \in \{0,1\}، لدينا HA(a,b)=(e,f)HA(a,b)= (e,f) حيث 2e+f=a+b2e+f = a+b. أثبِت أنّه توجد دائرة NAND من خمس بوابات NAND على الأكثر تحسب HAHA.

  2. الجامع الكامل (full adder) هو الدالة FA:{0,1}3→{0,1}2FA:\{0,1\}^3 \rightarrow \{0,1\}^{2} التي تأخذ بتّين وبتّ «احتفاظ» (carry) وتُخرج مجموعها. أي أنّه لكل a,b,c∈{0,1}a,b,c \in \{0,1\}، لدينا FA(a,b,c)=(e,f)FA(a,b,c) = (e,f) بحيث 2e+f=a+b+c2e+f = a+b+c. أثبِت أنّه توجد دائرة NAND من تسع بوابات NAND على الأكثر تحسب FAFA.

  3. أثبِت أنّه إذا كانت هناك دائرة NAND من cc بوابة تحسب FAFA، فإنّ هناك دائرة من cncn بوابة تحسب ADDnADD_n، حيث (كما في مبرهنة — الجمع ببرامج NAND-CIRC) ADDn:{0,1}2n→{0,1}n+1ADD_n:\{0,1\}^{2n} \rightarrow \{0,1\}^{n+1} هي الدالة التي تُخرج مجموع عددين مُدخلين من nn بت. انظر الحاشية للتلميح.[5]

  4. بيّن أنّه لكل nn يوجد برنامج NAND-CIRC يحسب ADDnADD_n بـ 9n9n سطرًا على الأكثر.

تمرين — الجمع

اكتب برنامجًا بلغتك البرمجية المفضّلة، يُدخل إليه عدد صحيح nn ويُخرج برنامج NAND-CIRC يحسب ADDnADD_n. هل تستطيع أن تضمن أنّ البرنامج الذي يُخرجه لـ ADDnADD_n يكون في أقلّ من 10n10n سطرًا؟

تمرين — الضرب

اكتب برنامجًا بلغتك البرمجية المفضّلة، يُدخل إليه عدد صحيح nn ويُخرج برنامج NAND-CIRC يحسب MULTnMULT_n. هل تستطيع أن تضمن أنّ البرنامج الذي يُخرجه لـ MULTnMULT_n يكون في أقلّ من 1000⋅n21000\cdot n^2 سطرًا؟

تمرين — ضرب كفؤ (تحدٍّ)

اكتب برنامجًا بلغتك البرمجية المفضّلة، يُدخل إليه عدد صحيح nn ويُخرج برنامج NAND-CIRC يحسب MULTnMULT_n ولا يتجاوز 10000n1.910000 n^{1.9} سطرًا.[6] ما أصغر عدد أسطر تستطيع استعماله لضرب عددين من 2048 بت؟

تمرين — دوال متعدّدة البتات

في متن الكتاب، لم تُثبَت مبرهنة — شمولية NAND إلّا للحالة m=1m=1. وفي هذا التمرين ستوسّع البرهان لكل mm.

أثبِت أنّ

  1. إذا كان هناك برنامج NAND-CIRC من ss سطرًا يحسب f:{0,1}n→{0,1}f:\{0,1\}^n \rightarrow \{0,1\} وبرنامج NAND-CIRC من s′s' سطرًا يحسب f′:{0,1}n→{0,1}f':\{0,1\}^n \rightarrow \{0,1\}، فإنّ هناك برنامجًا من s+s′s+s' سطرًا يحسب الدالة g:{0,1}n→{0,1}2g:\{0,1\}^n \rightarrow \{0,1\}^2 بحيث g(x)=(f(x),f′(x))g(x)=(f(x),f'(x)).

  2. لكل دالة f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m، يوجد برنامج NAND-CIRC من 10m⋅2n10m\cdot 2^n سطرًا على الأكثر يحسب ff. (يمكنك استعمال حالة m=1m=1 من مبرهنة — شمولية NAND، وكذلك البند 1.)

تمرين — التبسيط باستعمال السكر النحوي

لتكن PP برنامج NAND-CIRC التالي:

Temp[0] = NAND(X[0],X[0])
Temp[1] = NAND(X[1],X[1])
Temp[2] = NAND(Temp[0],Temp[1])
Temp[3] = NAND(X[2],X[2])
Temp[4] = NAND(X[3],X[3])
Temp[5] = NAND(Temp[3],Temp[4])
Temp[6] = NAND(Temp[2],Temp[2])
Temp[7] = NAND(Temp[5],Temp[5])
Y[0] = NAND(Temp[6],Temp[7])
  1. اكتب برنامجًا P′P' من ثلاثة أسطر شيفرة على الأكثر يستعمل كلًّا من NAND والسكر النحوي OR، ويحسب الدالة نفسها التي يحسبها PP.

  2. ارسم دائرة تحسب الدالة نفسها التي يحسبها PP وتستعمل بوابات ANDAND وNOTNOT فقط.

في التمارين التالية يُطلب منك مقارنة القدرة (power) لثنائيات من لغات البرمجة. وبـ«مقارنة القدرة» لغتَي برمجة XX وYY فإننا نعني تحديد العلاقة بين مجموعة الدوال القابلة للحساب ببرامج مكتوبة بـ XX وببرامج مكتوبة بـ YY على الترتيب. أي أنّ للإجابة عن مثل هذا السؤال عليك أن تفعل ما يلي معًا:

  1. إمّا أن تُثبت أنّ لكل برنامج PP في XX يوجد برنامج P′P' في YY يحسب الدالة نفسها التي يحسبها PP، أو أن تعطي مثالًا لدالة قابلة للحساب ببرنامج في XX وغير قابلة للحساب ببرنامج في YY.

و

  1. إمّا أن تُثبت أنّ لكل برنامج PP في YY يوجد برنامج P′P' في XX يحسب الدالة نفسها التي يحسبها PP، أو أن تعطي مثالًا لدالة قابلة للحساب ببرنامج في YY وغير قابلة للحساب ببرنامج في XX.

وعندما تعطي مثالًا كما سبق لدالة قابلة للحساب في لغة برمجة واحدة وغير قابلة للحساب في الأخرى، فعليك أن تُثبت أنّ الدالة التي أظهرتها هي (1) قابلة للحساب في لغة البرمجة الأولى و_(2)_ غير قابلة للحساب في لغة البرمجة الثانية.

تمرين — قارن IF وNAND

لتكن IF-CIRC هي لغة البرمجة التي نملك فيها العمليات التالية foo = 0 وfoo = 1 وfoo = IF(cond,yes,no) (أي أنّنا نستطيع استعمال الثابتين 00 و11، والدالة IF:{0,1}3→{0,1}IF:\{0,1\}^3 \rightarrow \{0,1\} بحيث تساوي IF(a,b,c)IF(a,b,c) القيمة bb إذا كان a=1a=1 والقيمة cc إذا كان a=0a=0). قارن بين قدرة لغة البرمجة NAND-CIRC ولغة البرمجة IF-CIRC.

تمرين — قارن XOR وNAND

لتكن XOR-CIRC هي لغة البرمجة التي نملك فيها العمليات التالية foo = XOR(bar,blah) وfoo = 1 وbar = 0 (أي أنّنا نستطيع استعمال الثابتين 00 و11 والدالة XORXOR التي تُسقط a,b∈{0,1}2a,b \in \{0,1\}^2 على a+bmod  2a+b \mod 2). قارن بين قدرة لغة البرمجة NAND-CIRC ولغة البرمجة XOR-CIRC. انظر الحاشية للتلميح.[7]

تمرين — دوائر للأغلبية

أثبِت أنّه يوجد ثابت ما cc بحيث لكل n>1n>1، لدينا MAJn∈SIZEn(cn)MAJ_n \in SIZE_n(cn)، حيث MAJn:{0,1}n→{0,1}MAJ_n:\{0,1\}^n \rightarrow \{0,1\} هي دالة الأغلبية على nn بتًّا مُدخلًا. أي أنّ MAJn(x)=1MAJ_n(x)=1 إذا ونحو فقط إذا كان ∑i=0n−1xi>n/2\sum_{i=0}^{n-1}x_i > n/2. انظر الحاشية للتلميح.[8]

تمرين — دوائر للعتبة

أثبِت أنّه يوجد ثابت ما cc بحيث لكل n>1n>1، ولكل أعداد صحيحة a0,…,an−1,b∈{−2n,−2n+1,…,−1,0,+1,…,2n}a_0,\ldots,a_{n-1},b \in \{-2^n,-2^n+1,\ldots,-1,0,+1,\ldots,2^n\}، توجد دائرة NAND من ncn^c بوابة على الأكثر تحسب دالةَ العتبة (threshold function) fa0,…,an−1,b:{0,1}n→{0,1}f_{a_0,\ldots,a_{n-1},b}:\{0,1\}^n \rightarrow \{0,1\} التي على مُدخل x∈{0,1}nx\in \{0,1\}^n تُخرج 11 إذا ونحو فقط إذا كان ∑i=0n−1aixi>b\sum_{i=0}^{n-1} a_i x_i > b.

ملاحظات ببليوغرافية

انظر كتابَي Jukna وWegener [@Jukna12, @wegener1987complexity] لمناقشة أوسع بكثير للدوائر. أثبت Shannon أنّ كل دالة منطقية يمكن حسابها بدائرة أُسّية الحجم [@Shannon1938]. والحدّ المحسّن c⋅2n/nc \cdot 2^n/n (مع القيمة المثلى لـ cc بالنسبة لقواعد كثيرة) هو من وضع Lupanov [@Lupanov1958]. وعرضٌ له في حالة NAND (حيث c=1c=1) موجود في الفصل 4 من كتابه [@lupanov1984]. (شكرًا لـ Sasha Golovnev على البحث عن هذا المرجع!)

يُعرف مفهوم «السكر النحوي» أيضًا بـ«الماكرو» (macros) أو بـ«البرمجة فوق البرمجية» (meta-programming)، وأحيانًا ما يُنفَّذ عبر معالِج أولي (preprocessor) أو لغة ماكرو داخل لغة برمجة أو محرّر نصوص. ومن الأمثلة الحديثة محوِّل صياغة JavaScript المعروف بـBabel، الذي يحوّل برامج JavaScript المكتوبة بأحدث الميزات إلى صيغةٍ تقبلها المتصفّحات الأقدم. بل إنّ له بنية إضافات (plug-ins) تتيح للمستخدمين أن يضيفوا سكّرهم النحوي الخاص إلى اللغة.


  1. يمكن تحسين قيمة cc إلى 99، انظر تمرين — نصف الجامع والجامع الكامل. ↩︎

  2. إن كنت فضوليًا، فإن هذه هي الدالة التي على المُدخل i∈{0,1}4i\in \{0,1\}^4 (الذي نفسّره بعدد في [16][16]) تُخرج الرقم رقم ii من π\pi في الأساس الثنائي. ↩︎

  3. الثابت cc في هذه المبرهنة هو 1010 على الأكثر، ويمكن في الواقع أن يقترب من 11 إلى أي حدّ شئنا، انظر ملاحظات ببليوغرافية. ↩︎

  4. يمكنك البدء بتحويل PP إلى برنامج NAND-CIRC-PROC يستعمل عبارات الإجراءات، ثمّ استعمال شيفرة Python code for transforming NAND-CIRC-PROC programs into standard sugar-free NAND-CIRC programs. لتحويل الأخير إلى برنامج NAND-CIRC «خالٍ من السكر». ↩︎

  5. استعمل «سلسلة» (cascade) من إضافة البتات واحدًا تلو الآخر، بدءًا من الرقم الأقل قيمة، تمامًا كما في خوارزمية المدرسة الابتدائية. ↩︎

  6. تلميح: استعمل خوارزمية كارا تسوبا (Karatsuba). ↩︎

  7. يمكنك استعمال الحقيقة (a+b)+cmod  2=a+b+cmod  2(a+b)+c \mod 2 = a+b+c \mod 2. وبخاصة فهي تعني أنّه إذا كانت لديك السطران d = XOR(a,b) وe = XOR(d,c) فإنّ e يأخذ مجموع المتغيّرات a وb وc على مقياس 22. ↩︎

  8. أحد الطرق لحلّ هذا هو استعمال التكرارية وما يُعرف بـمبرهنة المُعلِّم (Master Theorem). ↩︎