برای این کار ابتدا باید بعد از دریافت ورودی، یک متغیر برای جمعهای بیشتر از یک رقم تعریف کرد. آیا عدد ورودی تک رقمی است یا خیر؟ اگر تک رقمی بود a+b در غیر این صورت یکان و دهگان را جداگانه جمع میکنیم. اگر جمع یکان عدد بیشتر از ده بود، یکان را نگه میداریم و سپس در هر مرحبه مقدار متغیر کمکی در مرحلهی قبل را در دهگان عدد جمع میکنیم.
برای تفریق، ابتدا بعد از دریافت ورودی باید ببینیم که کدام بزرگتر است. اگر عدد دوم بزرگتر از عدد اول بود، باید جای آنها را با یکدیگر عوض کنیم. بعد عدد دوم را از عدد اول کم کرده، سپس نتیجه را نمایش دهیم.
الگوریتم این برنامه خیلی ساده هست. به طوریکه ابتدا مثبت یا منفی بودن عددهای ورودی رو با یه شرط مشخص میکنیم. بعد هم داخل یک حلقه که به تعداد عدد اول a تکرار میشه عدد دوم b رو داخل یک متغیر قرار میدهیم و هر بار به اندازه عدد دوم به اون اضافه یا کم ( بسته به مثبت یا منفی بودن) اعداد میکنیم.
برای حل این مسئله ابتدا ورودیها را دریافت میکنیم بعد چهار حالت زیر پیش میآید:
سپس متناسب با حالتی که پیش میاید، اعداد رو داخل حلقهها کم یا زیاد میکنیم تا جواب بدست آید.
ابتدا ورودیهایمان را دریافت میکنیم، سپس به تعداد عدد دوم، عدد اول را به خودش ضرب میکنیم و در یک متغیر قرار میدهیم. برای این کار از یک حلقه استفاده میکنیم.
دو عدد از ورودی دریافت کرده و یک عدد برای جمع گذاشته میشود. سپس حلقهای تعریف میکنیم که از i=0 باشد تا انتهای عدد دوم. و اگر مساوی عدد دوم بود تکرار کن c=c+a و اگر c مساوی عدد دوم به پایان برود و c را چاپ کند.
برای حل این الگوریتم دو متغیر به عنوان پایه و نمای توان مانند x, y در نظر گرفتهایم. حال اگر بخواهیم عمل توان را پیادهساوزی نماییم، نیاز داریم که از خاصیت عضو خنثی در ضرب استفاده شود. همانطور که میدانید عضو خنثی در ضرب، عدد ۱ میباشد و نیاز به یک حبقه داریم که باید تا عدد دوم شمارش شود و مقدار متغیر عضو خنثی در داخل حلقهی ضرب در عدد اول میشود تا در اتمام کار حلقه، توان محاسبه شود.
ده عدد از ورودی دریافت میکنیم. سپس حلقهای تعریف میکنیم که ده بار عدد دریافتی را با عدد جدید در یک متغیر جمع میکنیم. آنگاه اگر ده عدد دریافت شد، متغیر جمع را در ده تقسیم میکنیم و در خروجی پاسخ تقسیم را چاپ میکنیم. در غیر این صورت حلقه را تکرار میکنیم تا ده بار تکرار شود و از حلقه خارج شود.
از دید کامپیوتری، همانطور که مشاهده میکنید برای محاسبهی فاکتوریل، ما یک متغیر داریم که از آن یک واحد کم شده و در عین حال در اعداد قبلی ضرب میشود. در الگوریتم زیر، متغیر مورد نظر، n بوده و برای سیر نزولی اعداد از خود متغیر a استفاده کردیم. با اجرای خط ۴ و ۵ همزمان، هم a متغیر مورد نظر ضرب شده، هم اینکه از آن یک واحد کم میشود. در خط ۶ یک کنترل گذاشته شده تا a به صفر نرسد. چون اگر a برابر یک بشود و دوباره حلقه ادامه پیدا کند، در این دور، a برابر صفر شده و n را برابر صفر کرده و کل محاسبات را خراب میکند.
توضیح: عدد ورودی را دریافت میکنیم و داخل یک متغیر میگذاریم. سپس متغیری برای نگهداری جمع، تعریف میکنیم. و مقدار ۱ را به آن اختصاص میدهیم. و تا جایی که عدد ورودی، بزرگتر از یک بود، مقدار متغیر را در عدد ورودی ضرب و در هر مرحله یک واحد از عدد ورودی کم میکنیم و دوباره در متغیر n ضرب میکنیم. و زمانی که شرط پایان پذیرفت، جواب را چاپ میکنیم.
فرض کنید که به شما لیستی از اعداد را میدهند، برای پیدا کردن کوچکترین عدد در لیست، اولین عدد را به عنوان کوچکترین عدد در نظر میگیریم. سپس عدد بعدی را با آن مقایسه میکنیم، اگر عدد جدید از عدد قبلی کوچکتر بود، عدد جدید را به عنوان کوچکترین عدد در نظر میگیریم، در غیر این صورت همان عدد قبلی کوچکترین خواهد بود. این روند را تا انتهای لیست ادامه میدهیم. در پایان، عددی را که در هر بررسی به عنوان کوچکترین عدد بود، جواب مورد نظر ما خواهد بود. توجه کنید که در این روال همواره یک عدد را در ذهن خود در نظر گرفته بودیم.
برای نوشتن الگوریتم مورد نظر، یک خانه از حافظه را به کوچکترین عدد در هر مرحله اختصاص میدهیم.
یک متغیر برای جمع تعریف میکنیم sum = 0. و یک متغیر شرط با مقدار ۳ قرار میدهیم i = 3 تا جایی که sum = sum + i را در هر مرحله متغیر شرطی با متغیر، جمع میکنیم. در هر بار متغیر شرطی را با ۳ جمع میکنیم. اگر متغیر شرطی کوچکتر از ۵۰ بود. در پایان متغیر جمع را چاپ میکنیم.
ابتدا یک متغیر کمکی قرار میدهیم تا بتوانیم مقادیر را جابجا کنیم. سپس دو عدد از ورودی گرفته a, b مقادیر a را درون متغیر کمکی ریخته، سپس مقدار b را درون a میریزیم. آنگاه با خالی شدن b مقدار a را از متغیر کمکی به b انتقال میدهیم.
عددی را از ورودی دریافت میکنیم، شرط میگذاریم که اگر باقیماندهی تقسیم عدد ورودی بر ۲ ، برابر صفر بود، آنگاه زوج است در غیر این صورت عدد مربوطه فرد است.
سه عدد از ورودی دریافت میکنیم و شرط میگذاریم اگر جمع دو عدد اول بزرگتر از عدد سوم و عدد دوم از عدد اول بزرگتر و جمع عدد اول و سوم بزرگتر از عدد سوم بود، چاپ کن که اضلاع مثلث میباشند.
برای مثال، اگر تاریخ ۳/۲ وارد شد، عدد ۶۴ را نمایش دهد
توضیح: در علم حساب و برنامهنویسی کامپیوتر الگوریتم تعمیمیافته اقلیدس تعمیمی بر الگوریتم اقلیدس است که در کنار محاسبهی بزرگترین مقسومم علیه مشترک دو عدد صحیح a , b ضرایب بزو را هم محاسبه میکند.
x, y اعداد صحیح هستند. ax + by = gcd(a,b)همچنین میتوان بدون هزینهی اضافی خارج قسمت a,b را با بزرگترین مقسم علیه مشترک محاسبه کرد.
الگوریتم تعمیم یافتهی اقلیدس به الگوریتمی بسیار مشابه به الگوریتمی برای محاسبه بزرگترین مقسوم علیه مشترک چند جملهای و ضرایب قضیهی بزو از دو چند جملهای با یک متغیر ارجاع دارد. الگوریتم تعمیم یافته اقلیدس زمانی که a,b نسبت به هم اول هستند مفیدتر است. چون x برابر وارون ضربی پیمانهای a به پیمانهی b و y برابر وارون ضریب پیمانه b به پیمانهی a است. در الگوریتم اقلیدس برای چند جملهای میتان وارون ضربی را در بسطها ی میدان جبری محاسبه کرد.
از آن نتیجه میشود که هر دو نوع الگوریتم اقلیدسی تعمیم یافته معمولی و چند جملهای کاربردی گسترده در رمزنگاری دارند. بخصوص در محاسبهی وارون ضربی پیمانهای در روش RSA یک گام ضروری است.
توضیح: در الگوریتم اقلیدسی معمولی فقط باقیمانده ها نگاه داشته میشوند و خارج قسمت استفاده نمیشود. اما در الگوریتم تعمیم یافته اقلیدسی خارج قسمت های متوالی ستفاده می شوند. به طور دقیقتر، در الگوریتم تعمیم یافته که a، b را به عنوان ورودی میگیرد، دنبالهی q1 ... . .q(k)از خارج قسمتها و دنبالهی r0 ... .r(k)+1 از باقیماندهها را شامل میشود.
مهمترین ویژگی تقسیم اقلیدسی نامساوی سمت راست است که ri+1 را متفاوت از ri-1و riتعریف میکند. وقتی باقیمانده به صفر رسید محاسبه متوققف میشود. بزرگترین مقسوم علیه مشترک آخرین باقیماندهی غیر صفر است. الگوریتم تعمیم یافتهی اقلیدسی روشی مشابه دارد با این تفاوت که دو دنبالهی زیر نیز به آن اضافه میشود:
در این الگوریتم هم وقتی rk + 1 = 0میشود کار الگوریتم به اتمام میرسد.
rkبزرگترین مقسوم علیه برای دو ورودی a = R0 و b = R1 است.
ضرایب بزو هم که بربر sk , tkدر رابطهی زیر صدق میکند:
خارج قسمت تقسیم a,b بر بزرگترین مقسوم علیه مشترکشان به این صورت ایست: که این یعنی جفت ضرایب بزو که از این الگوریتم به دست میآیند یکی از کمترین جفت ضرایب بزو هستند.
عدد کامل عددی است که مجموع مقسوم علیههای آن عدد به جز خود عدد برابر با خود عدد باشد. مثال:
مقسوم علیه عدد ش برابر است با ۱ و ۲ و ۳ که جمع اینن سه عدد برابر است با ۶ . و این یعنی این که عدد شش کامل است.
پس ما برای این کار باید از یک for استفاده کنیم که متغیر شمارندهی آن از شمارهی یک شروع شود و تا یکی مانده به عدد تمام شود. در داخل بلوک کد for باید بنویسیم که اگر باقیمانده تقسیم a بر i برابر با صفر باشد. بعلاوه متغیر S میشود و در sقرار میگیرد. همین عمل چند بار انجام میگیرد.
در آخر باید بررسی کنیم که عدد s برابر با عدد a هست یا خیر. اگر برابر باشد عدد کامل است و اگر نه عدد کامل نیست.
عدد اول عددی طبیعی بزرگتر از یک است که بر هیچ عددی بجز خود و یک بخش پذیر نباشد. تنها استثنا عدد یک است که جزو این اعداد قرار نمیگیرد. اگر عددی طبیعی و بزرگتر از یک اول نباشد مرکب است.
رقم یکان اعداد بزرگتر از ده فقط ممکن است ارقام ۱، ۳، ۷، ۹ باشد.
عددی را از ورودی دریافت میکنیم و یک متغیر را ۲ قرار میدهیم. و شرط یمگذاریم از یک تا خود عدد تکرار شود و یک یک شمارنده را معادل یک قرار میدهیم. اگر باقیمانده تقسیم آن صفر شود عدد اول است در غیر اینصورت عدد اول نیست.
خواص اعداد اول:
عر عدد اول برابر است با n+1 یا n-1 که n یک عدد صحیح است.
مجذور هر عدد اول برابر است با n+1.24
تفاضل مجذورهای دو عدد اول مضربی از 24 است.
حاصل ضرب هر دو عدد اول بجز ۲ و ۳ مضربی از ۶ بعلاوه یا منهای یک است.
توان چهارم هر عدد اول بجز ۲ و ۳ مضربی از ۲۴۰ بعلاوه یک است.
از روش غربال برای ب.م.م استفاده میکنیم. به این ترتیب عدد بزرگتر را بر عدد کوچکتر تقسیم و اگر باقیمانده صفر شد عدد کوچکتر که مقسومعلیه تقسیم است ب.م.م میباشد. اگر صفر نشد مقسوم علیه قرار داده و دوباره عمل تقسیم انجام میشود تا زمانی که باقیمانده صفر شد که آنرا صفر باشد ب.م.م است.
عدد مورد نظر n رقمی است. و رقم یکان آن از تقسیم عدد بر ده بدست میآید که همان باقیمانده تقسیم است برای محاسبهی رقم دهگان خارج قسمت تقسیم الی، را دوباره بر ده تقسیم میکنیم . باقیماندهی آن رقم دهگان است و این تا زمانی انجام میشود که خارج قسمت صفر شود.
ابتدا تعداد ارقام را محاسبه میکنیم. سپس حلقهای تشکیل داده و به اندازهی تعداد ارقام عدد هر بار رقم جدا میکنیم. سپس آن رقم را با توجه به محل قرارگیریاش در عدد اصلی به صورت معکوس در عدد وارون قرار میدهیم. برای جدا کردن ارقام از خارج قسمت عدد بر توانهای عدد ده استفاده میکنیم. برای قرار دادن ارقام در عدد وارون هر رقم را در توان ده ضرب میکنیم و با عدد قبلی جمع میکنیم به گونهای که عدد تشکیل شده وارون عدد گردد.
یک عدد را از ورودی میخوانیم و در این صورت سه حالت پیش میآید، اگر a=0 بود عدد ورودی برابر با صفر است و عدد صفر را چاپ میکنیم. اگر عدد ورودی از صفر بزرگتر بود علامت عدد مربوطه مثبت بوده و اگر عدد ورودی کوچکتر از صفر بود در خروجی چاپ کند که علامت عدد ورودی منفی است.
هر عددی که درون قدر مطلق قرار بگیرد در خروجی مثبت میشود. قدر مطلق روی اعداد مثبت تاثیری ندارد چون آنها از اول مثبت هستند پس اگر عددی مثبت داخل قدر مطلق قرار گرفت خروجی خود عدد خواهد بود.
عددی را دریافت میکنیم. با استفاده از دستورالعمل شرطی، مثبت بودن عدد بررسی میشود. اگر مثبت باشد، قدر مطلق آن عدد برابر با خودش خواهد بود و عدد خروجی را چاپ میکنیم. در غیر این صورت آن را در منفی یک، ضرب میکنیم تا مثبت شود. سپس عدد را در خروجی چاپ میکنیم.
مربع کامل، عددی است که بتواند به صورت ضرب یک عدد صحیح در خودش نوشته شود. مثلا ۳۶ برابر است با ۶*۶. و ۴۹ برابر است با ۷ ضرب هفت.
نکته:
تمام مربعهای کامل به ۰، ۱، ۴، ۵، ۶ و ۹ ختم میشود.
یک عدد را برای تعداد خطهای چاپ ستاره وارد میکنیم سپس تا جایی که (تعداد خط یا سطر) i کوچکتر از عدد وارد شده بود و همچنین ستونهای آن از j = 1 تا جایی که کوچکتر یا مساوی با مقدار ه بود، ستاره را چاپ کن و این عمل تکرار شود تا زمانی که به عدد وارد شده رسیدیم و پایان یابد.
ما میدانیم که برای حل یک دستگاه دو معادله دو مجهولی ابتدا باید یا x و یا y را از دو عبارت موجود حذف کنیم. البته در این سورس، x از هر دو عبارت حذف میشود.
برای حذف x از هر دو عبارت باید ضریب x در هر دو عبارت صفر شود. که برای این کار، عبارت اول را بر قرینهی عدد d ضرب میکنیم و عبارت دوم را نیز بر عدد a ضرب میکنیم.
بعد از انجام این کار یک دستگاه دو معادلهی دو مجهولی جدید به وجود میآید که در آن هر دو عبارت را با هم جمع میکنیم. در نتیجه x از عبارتها پاک میشود. اکنون ما با توجه به عبارت زیر، y را بدست میآوریم.
b y = c ----> y = c / b
بعد از به دست آوردن y آنرا در یکی از عبارتها جایگزین میکنیم و x را با استفاده از فرمول زیر محاسبه میکنیم.
x = ( c - (b * y ) ) / a
البته در عبارت بالا و سورس و الگوریتم برنامه از اولین عبارت ax + by = c برای محاسبهی مقدار x استفاده شده است.
این الگوریتم برای پیدا کردن مقادیر مورد نظر ما بکار میرود. در واقع سادهترین جستوجوگر ممکن در زبانهای برنامهنویسی میباشد. روش این الگوریتم روش مقایسه است. بطور مثال در یک آرایه در عضوی تا زمان پیدا کردن عدد مورد نظر ، آن عدد را با تمام خانههای آرایه مقایسه میکند. و این موضوع یکی ار معایب آن به حساب میآید زیرا اگر آرایهی ما متشکل از چندین هزار خانه باشد، تا یافتن آن مقدار باید عدد وارده را با تمام خانهها مقایسه کند. به طوری که اگر آنرا نیابد تمام چیدن هزار خانه را یک دور با آن مقدار مقایسه میکند.
int A[4] = 14 15 16 5
برای مثال: اگر عدد مورد نظر ما ۵ باشد، ابتدا چک میکند که آیا ۵ و ۱۴ برابر هستند یا نه. در صورت نابرابری به خانهی بعد میرود و آنقدر این کار را انجام میدهد تا عدد را پیدا کند.
جست و جوی عمق محدود، یک جست و جوی ناآگاهانه است. این جست و جو دقیقا مشابه جست و جوی اول عمق عمل میکند. اما با تحمیل کردن یک محدودیت حداکثری روی عمق جست و جو، از مشکلاتی که مربوزط به تمامیت اول-عمق است جلوگیری میکند. حتی اگر جست و جو هنوز هم بتواند یک داس فراتر از آن عمق گسترش دهد، نمیتواند چنین کاری را انجام دهد و در نتیجه مسیرهای با عمق نامحدود را دنبال نمیکند یا به عبارتی در حلقهها گیر نمیافتد. بنابراین جست و جوی با عمق محدود در صورتی جواب را پیدا خواهد نمود که این جواب در فاصلهی اولین سطح تا عمق محدود قرار داشته باشد، که این محدودیت حداقل تمامیت را روی تمامی گرافها تضمین میکند.
یک الگوریتم برای پیدا کردن یک کلید در یک آرایهی مرتب است. این الگوریتم با چک کردن همهی آیتمهای lkm، جایی که l، آرایه است و k، عضو اعداد طبیعی و m اندازهی بلوکی هست که در آن کلید را جستجو میکند و این الگوریتم ادامه میدهد این جستجو را تا زمانی که یک آیتم پیدا شود که از کلید جستجو بزرگتر باشد. در آن صورت از یک مرحله قبلش به اندازهی m یکی یکی چک میکند و در صورت وجود کلید آنرا پیدا میکند.
مقدار بهینهی m رادیکال n است. جایی که n اندازهی آرایه l است. در هر دو مرحله الگوریتم به اندازهی رادیکال n، پیش میرود پس این الگوریتم در o (√n) اجرا میشود که این الگوریتم از الگوریتم خطی بهتر و از الگوریتم دو دویی بدتر است. اما مزیت این الگوریتم به الگوریتم دودویی این است که در این الگوریتم فقط یکبار به عقب برمیگردد و لی در الگوریتم دودویی میتواند تا log n بار به عقب برگردد و این در مواقعی که برگشت به عقب موثر است از به جلو پیش رفتن خیلی اهمیت دارد.
در الگوریتم مرتبسازی، قصدمان این است که اعداد یک آرایهی عددی را به صورت صعودی یا نزولی مرتب کنیم.
الگوریتمهای مرتبسازی آرایه انواع مختلفی دارند. مانند: حبابی، تعویضی، shell، quicksort، ...
در این الگوریتم مرتبسازی هر اندیس آرایه با اندیسهای بعد از خود مقایسه میشود و طبق شرط مقایسهمان اعداد آرایه را مرتب میکنیم.
شرط مقایسه:
صعودی: اگر این عدد از این اندیس آرایه بزرگتر از اعداد بعد از خود بود، آنها را با هم تعویض کن.
نزولی: اگر این عدد از این اندیس آرایه کوچکتر از اعداد بعد از خود بود، آنها را با هم تعویض کن.
فرض کنید آرایهی ما این باشد:
int A[5] = {3, 0, 1, 2, 5,};
روش مرتبسازی به این صورت خواهد بود:
گام اول: اندیس صفرم یعنی ۳، ابتدا با ۰ و بعد با ۱، سپس با ۲ و سپس با ۵ مقایسه میشود.
گام دوم: اندیس یک یعنی ۰ با ۱ و ۲ و ۵ مقایسه میشود.
و در گامهای بعدی هم به همین صورت ادامه خواهد داشت. در تصویر زیر میتوانید بهتر متوجه شوید. مقایسهها با خط نمایش داده شدهاست.
بسیاری از فرآیندهای طبیعی از جمله ترکیب ساختار بدن موجودات زنده، نظم مشخصی دارند و از دنبالهی اعدادی تبعیت میکنند که امروزه با نام دنبالهی اعداد فیبوناچی شناخته میشود. مشهرترین خاصیت این اعداد نسبت دو جملهی متوالی آنها به ازای جملات بزرگ دنباله است که به عدد طلایی مشهور است.
همانگونه که از تعریف مشخص است، جملات این دنباله از جمع دو جملهی قبلی آن با شروع از دو مقدار صفر و یک به دست میآیند:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... .
الگوریتم Risch برای تابع اولیه انتگرال استفاده شدهاست. اینها توابعی هستند که شامل ترکیب تابعهای نمایی، لگاریتمها، رادیکالها، مثلثات و چهار عمل اصلی علم حساب میباشند.
انتگرال نامعین یک تابع منطقی، تابعی است منطقی و یک عدد از ضرب تعداد محدودی از ضربهای ثابت لگاریتم تابع منطقی است.
Liouville، مسئلهی حلشده توسط الگوریتم Risch، را به صورت فرمول در آورده است. Liouville، به روش تحلیلی اثبات کرد که اگر یک راه حل ابتدایی یا اولیه که اسمش g باشد وجود داشته باشد برای معادلهی g'=f آنگاه برای ثابت ai و تابع اصلی ui و v پاسخ به شکل زیر میباشد.
Risch، روشی ساخت که اجازه میداد شخص فقط نگران مجموعهای محدود از توابع اولیه به صورت فرمول لیوویل باشد.
فراست الگوریتم ریش، از وفتار توابع نمایی و لگاریتمی در مشتق گیری میآید. برای توابع feg که f و g توابع متغیری هستند که ما داریم.
لذا اگر eg در نتیجه انتگرال نامعین بود، باید انتظار داشت که داخلِ انتگرال نیز باشد همانگونه که :
سپس اگر lnng در نتیجه انتگرال باشد آنوقت فقط توانهای کمی از لگاریتم می توان انتظار داشت..
تبدیلِ فرایندِ تصمیمِ رِیش به یک الگوریتم که بتواند توسط کامپیوتر اجرا شود وظیفه ی پیچیدهای است که نیازمند مطالعات ابتکاری و پالایشِ زیاد می باشد. هیچ نرمافزاری (تا فوریه ۲۰۱۱) شناخته نشد که بتواند الگوریتمِ رِیش را به طورِ کامل پیاده سازی کند، اگرچه چندین سیستم جبری کامپیوتری جزئی از آن را پیاده سازی کردهاند.
برای حل این مسئله، میتوانیم از یک الگوی باینری که تعداد صفر و یکهای اون برابر n هست استفاده کنیم. مثلا برای یک مجموعهی ۳ عضوی میتوانیم از الگوی زیر استفاده کنیم.
000
001
010
011
100
101
110
111
به طوری که به ازای هر عدد ۱، عضو مورد نظر نمایش داده شود. بدین ترتیب همهی زیرمجموعههای مجموعه نمایش داده میشود.
برای انجام این کار باید اعداد ۱ تا ۲ (n + 1) - 1 رو به صورت عدد باینری داخل یک آرایه قرار دهیم. بعد داخل حلقه، بررسی میکنیم و در صورتی که تعداد ۰ و ۱ های اعضای ارایه برابر با تعداد اعضای مجموعه باشد، به ازای هر عدد ۱، عضو مربوط را نمایش میدهیم.
کد ملی، شمارهای است ده رقمی که از سمت چپ، سه رقم کد شهرستان محل صدور شناسنامه، شش رقم بعدی کد منحصر به فرد دارندهی شناسنامه در شهرستان محل صدور، و رقم آخر آن هم یک رقم کنترل است که از روی ۹رقم سمت چپ بدست میآید. برای بررسی کنترل کد کافی است که مجدد از روی ۹رقم سمت چپ رقم کنترل محاسبه کنیم.
از آنجایی که در سیسستم کد ملی معمولا قبل از کد، تعدادی صفر وجود دارد، و در بسیاری از موارد ممکن است کاربر این صفرهارا وارد نکردهباشد، و یا نرمافزار، این صفرها را ذخیره نکرده باشد، بهتر است قبل از هر کاری در صورتی که طول کد، بزرگتر یا مساوی ۸ و کمتر از ۱۰ باشد، به تعداد لازم به سمت چپ عدد اضافه کنیم. ساختار کد ملی در زیر نشان دادهشده است.
مثال:
آیا کد ۷۷۳۱۶۸۹۹۵۱، یک کد ملی معتبر است؟ برای این منظور،حاصل جمع ضرب ارقام ۲ لی ۱۰ را در موقعیت آنها محاسبه میکنیم.
۷*۱۰+۷*۹+۳*۸+۱*۷+۶*۶+۸*۵+۹*۳+۵*۲=۳۱۳
۳۱۳÷۱۱=۲۸
R= ۵
چون باقیمانده برابر با ۵ و بزرگتر مساوی ۲ است، پس باید رقم کنترل این کد برابر با ۶ باشد. با دقت در کد متوجه میشویم که رقم کنترل ورودی برابر با ۱ است. پس کد مورد نظر به عنوان یک کد معتبر قابل قبول نیست.
هر ویراست از یک کتاب دارای شمارهٔ شابک خاص خود است و این شماره دارای ۱۰ تا ۱۳ رقم است و پنج بخش دارد:
اگر شماره ۱۳رقمی باشد دارای یک عددِ ۹۷۸ یا ۹۷۹ است.
هر زبان یا کشور دارای شمارهٔ خاص خود است.
برای مثال، کد کشور انگلیس صفر یا یک، فرانسه دو، آلمان سه، و کد ایران ۹۶۴ یا ۶۰۰ است. اگر شابکی این کد را نداشتهباشد در جهان رسمیت ندارد. این کد برای کشورها حداکثر پنج رقم خواهد بود.
هر انتشارات دارای شمارهٔ خاص خود است که در شابک گنجانده میشود.
هر کتاب دارای شمارهٔ خاص خود است.
یک نویسه وارسی در مبنای یازده است و در اصل یک مجموع مقابلهای (Checksum) از تمام شناسهاست. بهوسیلهٔ این نویسه میتوان صحت شابک موردنظر را بررسی کرد.
نویسهٔ وارسی (عدد کنترل) با استفاده از روش زیر محاسبه میشود:
هریک از نویسههای شناسهٔ شابک (از چپ به راست) در وزنی که از ۱۰ تا ۲ بهترتیب برای هریک از آنها در نظر گرفته میشود، ضرب میشود.
حاصل نتایج ضربها با هم جمع میشود.
نویسهٔ وارسی با استفاده از حاصل نتایج ضرب در مبنای ۱۱ تعیین میشود (یعنی برای مقدار مجموع حاصلضربهای برابر با ۱۰، نویسهٔ X استفاده میشود).
مثلاً برای:
۱۰×۶ + ۹×۰ + ۸×۰ + ۷×۵ + ۶×۸ + ۵×۲ + ۴×۵ + ۳×۰ + ۲×۰ = ۶۰ + ۰ + ۰ + ۳۵ + ۴۸ + ۱۰ + ۲۰ + ۰ + ۰ = ۱۷۳
حاصل مضرب کامل قبلی «۱۱ × ۱۵ = ۱۶۵» ۱۱ است.
باقیمانده تقسیم ۱۷۳ بر ۱۱ عدد ۸ میشود. به عبارت دیگر: ۱۷۳–۱۶۵ = ۸ عدد کنترل شابک این مثال ۸ است که سمت راست عدد آمدهاست.
همانطور که میبینید حاصل محاسبه فوق عدد ۱۳۲ شده است و اکنون کافیست این عدد را بر عدد ۱۱ تقسیم کنیم. چنانچه باقیمانده صفر شود بعنی شابک مورد نظر صحیح است و در غیر اینصورت اشتباه میباشد.
شابک ۱۳ رقمی:
برای اعتبارسنجی شابک ۱۳ رقمی باید تمام اعداد را از سمت چپ به ترتیب در اعداد ۱ یا ۳ ضرب کرده و سپس حاصل هرکدام را با هم جمع کنیم. به عنوان مثال شابک ۷-۴۰۶۱۵-۳۰۶-۰-۹۷۸ را به صورت زیر اعتبارسنجی میکنیم.
همانطور که میبینید نتیجه محاسبه عدد ۱۰۰ شده و چنانچه نتیجه را بر عدد ۱۰ تقسیم کنیم خواهیم دید که باقیمانده صفر خواهد شد و بنابراین شابک مورد نظر ما صحیح است. لازم است ذکر شود در ماشین حساب معمولی اگر جواب صفر شد ملاک ما است و در ماشین حساب مهندسی دکمه mod وجود دارد که برای محاسبه باقیمانده استفاده میشود.
تمامی کارتهای اعتباری بانکی دارای الگوریتمی بینالمللی هستند که فرمول آن به این شکل است که ابتدا از رقم اول، دو تا دو تا جدا میکنیم و آنها را در دو ضرب میکنیم. در صورتی که این ارقام از ده بیشتر باشند، آنها را از ۹ کم میکنیم، نتایج را ذخیره میکنیم. حال مابقی ارقام را که از ده بیشتر نبودند را نیز ذخیره کرده و ارقامی که در این عملیات شرکت نداشتند را هم ذخیره میکنیم و ارقام ذخیره شده را نهایتا با هم جمع میکنیم. حاصل باید مضربی از ۱۰ باشد.
6104 3370 2926 0526
6 1 0 4 3 3 7 0 2 9 2 6 0 5 2 6
x2 x2 x2 x2 x2 x2 x2 x2
12-9 0 6 14-9 4 4 0 4
3 1 0 4 6 3 5 0 4 9 4 6 0 5 4 6
3+1+0+4+6+3+5+0+4+9+4+6+0+5+4+6= 60
60 mod 10 = 0
این کارت یک کارت معتبر است.
برای ساخت ۱۹ رقم سمت راست شبا، به ترتیبی که در الگوریتم محاسبهی سیستم حساب هر بانک ذکر شدهاست عمل میشود. پس از ساخت این ۱۹ رقم، قواعد محاسبه شبا همانند دستورالعمل صورت میپذیرد. در این الگوریتمها شمارهی حساب بانک به معنی ۱۹ رقم سمت راست شبا است.
اولین رقم شماره حساب بانک به صورت زیر معین میشود:
در صورتی که نوع حساب مشخص نباشد، به طور پیشفرض حساب سپرده در نظر گرفته میشود.
به هنگام تبدیل شماره حساب بانک مندرج در شبا به شمارهی حساب داخلی بانک، طول آن باید ۱۹ رقم باشد. ارقام سمت چپ این شماره باید یکی از ارقام صفر تا چهار باشد. در غیر این صورت، شمارهی حساب معتبر نیست.
فرمت شماره سریال استاندارد بینالمللی یک شماره هشت رقمی است که توسط اتصالها و فواصل به دو قسمت چهار رقمی تقسیم میشود. عدد آخر که میتواند از ۰ تا ۹ و یا x باشد، شمارهی کنترل نامیده میشود. گزارش خوانش این شماره برای مثال ممکن است چیزی مانند ۵۹۵۵-۰۳۷۸ باشد که از طریق الگوریتم زیر به تطبیق و کنترل آن مبادرت میشود.
محاسبهی حاصل جمع هفت رقم ابتدایی شمارهی issn به ترتیب، در حاصل ضرب در شمارههای ۸، ۷، ۶، ۵، ۴، ۳، ۲،:
0*8+3*7+7*6+8*5+5*4+9*3+5*2 == 0+21+42+40+20+27+10= 160
سپس ضریب عدد ۱۶۰ را به پیمانهی ۱۱ محاسبه میکنیم:
۱۶۰ بر ۱۱ = ۱۶
شما میبایست متناوبا این تقسیم را بر پایهی ۱۱ انجام دهید.
۱۶۰ ÷ ۱۱ = ۱۴.۶
سپس قدر مطلق باقیمانده که در بالا شش هست، ارزش شماره کنترل ما را وقتی که از میزان ۱۱ تفریق میشود را مشخص میکند:
۱۱ -۶ = ۵
شمارهی کنترل ما پنج میشود. همچنین x نشاندهندهی شمارهی ده در شمارهی کنترل میباشد.
همچنین طبق الگوی بالا یک سیستم چککننده معتبر بودن ایاساس وجود دارد است که از طریق آن میتوان معتبر بودن یا جعلی بودن آیاساسان را تشخیص داد.
این نوع بارکد با عنوان کد جهانی محصول یا UPC شناخته میشود. در ابتدا دانشآموزان با الگوریتم آشنا میشوند و سپس به تعیین اعتبار عدد میپردازند. سپس دانشآموزان باید برای تعیین یک رقم چک کننده برای یک بارکد تلاش کنند. در سیستم UPC باید باقیماندهی شمارهی UPC بر ۱۰ صفر باشد.
این سیستم از فاکتور ۳ برای رقمهایی که در جایگاه زوج قرار دارند استفاده میکند. به این معنی که رقمهای جایگاه زوج در ۳ ضرب میشوند. به عنوان مثال: از UPC داده شده در زیر که مربوط به فیلم شگفتانگیزان میباشد، استفاده کنید.
برای بررسی این شماره، مراحل زیر را دنبال کنید:
تقسیم بر 10 دارای باقی مانده ی صفر می باشد (11=110/10). پس این شماره ی UPC، معتبر است.
دانش آموزان ممکن است از ضرایب فاکتور بگیرند، به این صورت که ابتدا رقم های موجود در جایگاه زوج را با یکدیگر جمع و سپس در 3 ضرب کنند، رقم های موجود در جایگاه فرد را نیز جمع کرده و در 1 ضرب کنند.
سپس شماره ی UPC مقابل را برای دانشجویان مثال بزنید: 7-96714-78601-y در این جا y، رقم چک کننده است. با استفاده از فرآیند فوق، دانش آموزان باید رقم چک کننده را تعیین کنند. مجموع نتایج 112 است. پس رقم چک کننده باید 8 باشد، زیرا باقیمانده ی تقسیم (8+112) بر 10، 0 است. در تمرین شماره ی 1، دانش آموزان باید رقم چک کننده را برای 2 شماره ی UPC به دست آورند.
نوع دیگری از سیستم های بارکد، شماره ی استاندارد بین المللی کتاب یا ISBN است. این سیستم در اواخر 1960 و اوایل 1970 به وجود آمد. آشکار است که سیستم واحدی برای شناسایی کتاب هایی که در تمام نقاط دنیا انتشار می یابند، مورد نیاز است. اکنون هر کتابی می تواند یک شماره ی شناسایی انحصاری داشته باشد. ISBN، یک عدد ده رقمی است که شامل بلوک های عددی با معانی متفاوت است. این شماره شامل چهار قسمت می باشد که با خط تیره یا فاصله از هم جدا شده اند. اولین قسمت شماره که نشان دهنده ی زبان یا کشور است، شناسه ی گروه نام دارد و حداکثر 5 رقمی است. بخش دوم شماره، نشان دهنده ی ناشر و حداکثر 7 رقمی است. سومین بخش شماره، نشان دهنده ی تجدید چاپ یا تعداد چاپ است و بیش از 6 رقم نیست. قسمت نهایی، رقم چک کننده است. قسمتی از انعطاف پذیری این سیستم در این نکته است که تعداد زیادی شماره جهت استفاده موجود می باشد. به یاد آورید که یک شماره ی 10 رقمی داریم که رقم آخر آن، رقم چک کننده است. بنابراین سه قسمت اول شماره می باید در مجموع، یک عدد نه رقمی را تشکیل دهند. رقم های صفر، به عنوان پر کننده های فضا در ابتدای عدد، در صورتی که تعداد رقم ها به تعداد کافی نباشد، استفاده می شوند. شکل زیر، نمونه ای از شماره ی ISBN را نشان می دهد.
در این سیستم، رقم چک کننده، به روشی متفاوت با روش UPC محاسبه می شود. به این صورت که اولین رقم در 10 ضرب می شود، رقم دوم در 9، رقم سوم در 8 و این فرایند را ادامه می دهیم تا رقم نهم که در 2 ضرب می شود. سپس مجموع نتایج را محاسبه می کنیم. این سیستم پیمانه ی11 نامیده می شود، به این معنی که مجموع نتایج نه رقم اول به علاوه ی رقم چک کننده باید مضرب 11 باشد. یکی از مشکلات این روش این است که ممکن است رقم چک کننده 10 باشد، در حالی که ما فقط می توانیم از ارقام 0 تا 9 برای این منظور استفاده کنیم و بنابر این X به جای رقم چک کننده نوشته می شود. (این X نشان دهنده ی شماره 10 رومی است.) تمرین های شماره ی 3 تا 6، مشخصات مربوط به ISBN می باشند.
کارت های اعتباری، از یک سیستم بلوک های شماره ای، مشابه سیستم ISBN استفاده می کنند. یک تفاوت واضح این است که در این جا حداکثر طول شماره 19 رقم است، اگرچه بسیاری از شماره ها بین 13 تا 16 رقم می باشند.
یک تابع بنهانی درهم تصویب شده H را انتخاب کنید. بر روی طول کلید L, N تصمیم بگیرید. این اندازهگیری اولیه قدرت پنهانی کلید است. دیاساس اصلی مارا وادار میکند تا مضربی از ۶۴ بین ۵۱۲ و ۱۰۲۴ باشد.
پارامترهای الگوریتم (p, q, g) ممکن است بین کاربران سیستم به اشتراک گذاشتهشود. به ازای هر کاربر یک مجموعه از پارامترها به کلیدهها تخصیص میابد. مرحلهی دوم کلیدهای عمومی و اختصاصی را برای یک کاربر مجزا محاسبه میکند.
روش کار به این صورت است:
برای POP کردن از پشته، دادهای که در اندیس Top قرار دارد برگردانده میشود و Top یک واحد کم میشود.
اندیس عنصر بالای پشته Stack را نگهمیدارد. Top=n دلالت بر خالی بودن پشته دارد.
برای Push کردن یک دادهی جدید به پشته، به Top یکی اضافه میشود. و دادهی جدید در آرایه در اندیس Top درج میکند.
برای اضافه کردن عدد به صف، باید از انتهای صف یک واحد اضافه کنیم تا بتوانیم عدد را وارد کنیم.
در صف دو اشارهگر Front که به ابتدای ارایه اشاره و Rear به انتهای آرایه اشاره میکند.
از عمل اصلی Queue برای تست خالی بودن یا پر بودن استفاده میشود. اگر Rear ما برابر با n بود، چاپ کن که پر است. در غیر این صورت، به Rear یک واحد اضافه و عدد را وارد صف کن.
صف، لیست مرتبی است که عناصر در انتهای آن اضافه و از ابتادی آن حذف میشود. به عبارت دیگر طول صف از انتهای آن افزایش و از ابتدای آن کاهش مییابد.
اولین عنصری که وارد صف میشود اولین عنصری است که از صف خارج میشود. بنابراین عناصر به همان ترتیبی که به صف اضافه میشوند از آن حذف میشوند. به همین دلیل به صف، لیست FIFO یا First In First Out گفتهمیشود.
از عمل Quek برای پر بودن یا خالی بودن استفاده میشود.
اگر Front برابر با Rear بود، چاپ کن که خالی است در غیر این صورت از ابتدای صف، یک واحد اضافه کن و موقعیت Front یک خانه جلو آمده و عدد اولی حذف گردد.
در علوم کامپیوتر یکی از الگوریتم های فراابتکاری، الگوریتم جستجوی بیم یا پرتو محلی است که گراف را با استفاده از راسهایی که در یک مجموعه خاص احتمال وجود بیشتری دارند میپیماید. در واقع این جستجو حالت بهینۀ الگوریتم جستجوی اول سطح است که در آن حافظه مورد نیاز را کاهش میدهد. الگوریتم جستجوی اول سطح پیدا کردن کمترین فاصله بین راس شروع و پایان را در گراف بیوزن تضمین میکند ولی برای جستجو در فضاهای بزرگ این کار تقریباً غیر عملی است چرا که حافظۀ بسیار زیادی مصرف میکند ممکن است قبل از اینکه به جواب برسد حافظه پر بشود.
الگوریتم بیم برای اینکه در حافظه صرفهجویی کند یک تابع h برای پیشبینی هزینۀ رسیدن به راس موردنظر از راس دادهشده در نظر می گیرد.همچنین از یک پارامتر به نام B(عرض بیم) استفاده میکند که نشاندهندۀ تعداد راسهایی است که در هر مرحله از الگوریتم جستجوی اول سطح ذخیره شدهاست. بنابراین زمانی که الگوریتم جستجوی اول سطح همۀ راسهای مرزی ( راسهایی که با نزدیکترین راس ارتباط دارند)را ذخیره میکند الگوریتم بیم فقط راسهای B با بهترین مقدار در هر مرحله از جستجو را ذخیره میکند. ایده اصلی این است که تابع h به الگوریتم اجازه میدهد که راسهایی انتخاب شوند که مسیر را به سمت راس مقصد راهنمایی کنند و پارامتر B باعث میشود که الگوریتم فقط این راسهای مهم را ذخیره کند و از پر شدن حافظه قبل از رسیدن به هدف جلوگیری میکند.برخلاف الگوریتم جستجوی اول سطح که از یک لیست(open list)استفاده میکند این الگوریتم راسهایی را که در حلقۀ بعد الگوریتم بسط داده میشود رادر(BEAM)ذخیره میکند؛ همچنین از یک جدولهش برای ذخیرۀ راسهایی که دیده شدهاند استفاده میکند شبیه(close list)درالگوریتم جستجوی اول سطح.
الگوریتم بیم در ابتدا راس شروع را به BEAM وجدولهش اضافه میکند سپس در هر مرحله از حلقۀ اصلی از الگوریتم، راسهای همسایه باراسهای BEAM را به یک مجموعه که شامل راسهای جانشین(successor)است اضافه میکند و سپس راسهای B با بهترین مقدار از مجموعۀ جانشین را به BEAM و جدولهش اضافه میکند. راسهایی که در حال حاضر در جدولهش قرار دارند به BEAM اضافه نمیشوند چرا که مسیر کوتاهتر برای آن راس پیدا شده.این فرایند ادامه دارد تا زمانی که راس مقصد پیدا شود، جدولهش پر شود، یا BEAM بعد از حلقۀ اصلی خالی شود.
در ادامه شبه کد الگوریتم آورده شده.
/* initialization */ g = 0; hash_table = { start }; BEAM = { start }; /* main loop */ while(BEAM ≠ ∅){ // loop until the BEAM contains no nodes SET = ∅; // the empty set /* generate the SET nodes */ for(each state in BEAM){ for(each successor of state){ if(successor == goal) return g + 1; SET = SET ∪ { successor }; // add successor to SET } } BEAM = ∅; // the empty set g = g + 1; /* fill the BEAM for the next loop */ while((SET ≠ ∅) AND (B> |BEAM|)){ // set is not empty and the number of nodes in BEAM is less than B state = successor in SET with smallest h value; SET = SET \ { state }; // remove state from SET if(state ∉ hash_table){ // state is not in the hash_table if(hash_table is full) return ∞; hash_table = hash_table ∪ { state }; // add state to hash_table BEAM = BEAM ∪ { state }; // add state to BEAM } } }
الگوریتم اسپیگوت (به انگلیسی: Spigot Algorithm) یک نوع خاص از الگوریتمها است که برای محاسبه ی مقدار یک ثابت ریاضی مانند π یا e استفاده میشود، که میتواند یک رشته ی خروجی از ارقام را بدون نیاز به استفاده ی مجدد از آنها تولید کند.این اسم از کاربرد کلمۀ اسپیگوت در برخی از شاخههای انگلیسی به معنی دریچه یا شیری که جریان یک مایع را کنترل میکند برگفته شده است.
علاقه به چنین الگوریتمهایی در روزهای آغازین ریاضیات محاسباتی با تاکید شدید بر حافظه رواج پیدا کرد، و یک نمونه از این الگوریتمها برای محاسبۀ ارقام عدد e میباشد. منتشرشد. نام الگوریتم اسپیگوت توسط استنلی رابینوویتز و استان واگن ابداع شد که الگوریتم ابداعی آنها برای محاسبه عدد π برخی اوقات تحت عنوان اگوریتم اسپگوت برای "π" نام برده میشو
الگوریتم اسپیگوت رابینوویتز و ویگن کراندار است،به این معنا که تعداد ارقام خواسته شده باید از قبل مشخص باشد. جرمی گیبسون از اصطلاح الگوریتمهای زنجیرهای برای معرفی الگوریتمهایی که به صورت نامحدود و بدون کران از پیش تعیین شده میتوانند اجرا شوند،استفاده میکند . پیشرفتهای بیشتر منجر به ابداع الگوریتم ای گردید که قابلیت محاسبه یک عدد دلخواه را بدون نیاز به محاسبه ارقام پیشین آن در وهله اول،دارا است : فرمول بیلی-بوروین-پلوفه نمونهای از این الگوریتم است.یک الگوریتم تجزیه ارقامی برای عدد π که ارقام را در مبنای ۶ تولید میکند.
این مثال نحوه ی کار کردن الگوریتم اسپیگوت را با محاسبه ی ارقام باینری لگاریتم طبیعی ۲ نشان میدهد (دنباله ی A068426 درOEIS ) با استفاده از تساوی ذیل :
برای محاسبه ارقام باینری از جایگاه هشتم ما طرفین تساوی فوق را در 27 ضرب میکنیم ( با توجه به اینکه ۷=۸-۱)
سپس ما سری نا محدود را به دو قسمت تقسیم میکنیم : قسمت "head" که در آن توان ۲ بزرگتر یا مساوی ۰ است و قسمت "tail" که در آن توان ۲ منفی است.
از آنجایی که ما فقط علاقهمند به بخش کسری این مقدار هستیم میتوانیم هر مؤلفه سری در "head" را با مقدار زیر جایگزین کنیم
با محاسبه هر کدام از این مقادیر و اضافه کردن آنها به سری کلی که در حال اجراست ( که مجدداً در این سری کلی ما تنها علاقهمند به بخش کسری هستیم) به نتایج زیر میرسیم :
ما مقادیری را به بخش "tail" میافزاییم،با توجه به اینکه خطای تولید شده به موجب کوتاه کردن سری کمتر از مقدار نهایی است.
با اضافه کردن بخش "head" و تعداد اندکی از مقادیر نخستین بخش "tail" به نتایج زیر میرسیم :
بنابراین ارقام ۸ ام تا ۱۱ ام در بسط باینری (ln(2 ارقام ۱،۱،۰،۱ میباشند.توجه کنید که ما مقادیر ۷ رقم نخست باینری را محاسبه نکردهایم - در واقع تمامی اطلاعات مربوط به آنها با استفاده از هم نهشتی(نظریه اعداد) در بخش سری "head" به صورت عمدی حذف شده است. روش مشابهی میتواند برای محاسبه ی ارقام بسط باینری (ln(2 با شروع از جایگاه دلخواه n ام مورد استفاده قرار بگیرد. تعداد عبارتهای موجود درقسمت "head" سری به صورت خطی با n افزایش میابد ، اما چناچه یک روش کارآمدی با استفاده از بهتوانرساندن مدولار به کار گرفته شود پیچیدگی هرعبارت تنها با لگاریتم n افزایش میابد. دقت محاسبات،نتایج میانی و تعداد عبارتهای گرفته شده از قسمت "tail" سری همگی مستقل از n میباشند و تنها وابسته به تعداد ارقام باینری هستند که در حال محاسبه است - یک واحد محاسباتی میتواند برای محاسبه حدودا ۱۲ رقم باینری صرف نظر از جایگاه شروع مورد استفاده قرار گیرد.
یکی از روشهای پیدا کردن محل تقاطع امتداد دو پاره خط با استفاده از اعداد مختلط صورت میگیرد.در این روش به دلیل وجود امکان ضرب کردن متغیرها و نقاط در یکدیگر مزیتهایی وجود دارد که به طور مثال می توان به نمایش نقطه برخورد خطوط به صورت یک عدد مختلط اشاره کرد .هم چنین در بسیاری از معادلات و محاسبات پیشرفتهتر که به بررسی وضعیت نقاط و خطوط میپردازد ، نمایش محل برخورد اجزا به صورت یک عدد ، کمک بسیاری در تحلیل بهتر معادلات میکند.
برای محاسبه محل تقاطع دو خط کافیست معادله آن دو را حل کرده و در صورتی که جواب داشته باشند آن دو خط متقاطع اند. حال برای بررسی این که آیا دو پاره خط با هم متقاطع هستند یا نه باید به صورت زیر عمل کنیم :
برای انجام قسمت 1 کافیست معادله خط را با داشتن 2 نقطه از آن بدست آورده و آن ها را حل کنیم. برای قسمت 2 نیز می بایست پاره خط را با بردار نقطه تقاطع به مبدا انتقال دهیم و بررسی کنیم که آیا دو سر پاره خط در دو ربع متفاوت هستند یا خیر.
هر پاره خط در صفحه مختلط به وسیله 2 عدد قابل نمایش است که همان ابتدا و انتهای پاره خط میباشد.مثلا ab پاره خطیست که عدد a یک سر آن و عدد b سر دیگر پاره خط است.حال برای پیدا کردن محل تقاطع 2 پاره خط بایستی شرطهایی لازم و کافی برای یک نقطه دلخواه مانند X بیابیم که در صورت برقراری آنها نقطه X روی خط حاصل از امتداد پاره خط ab قرار داشته باشد.حال فرض کنید نقطه X روی خط حاصل از امتداد ab باشد ، در این صورت خواهیم داشت :
بنابراین:
حال دو طرف معادله (1) را در (d-c) ضرب میکنیم :
هم چنین دو طرف معادله (2) را در (b-a) ضرب میکنیم :
حال از تفاضل معادله (3) و (4) خواهیم داشت :
که مقدار x بدست میآید :
برای پیاده سازی اعداد مختلط با استفاده از دستگاه مختصات دو بعدی میبایست قوانین جمع و ضرب اعداد مختلط را برای دوتاییهای (x,y) به وسیله ایجاد تعدادی تابع در برنامه تعریف کرد .اما به طور مستقل نیز میتوان با اضافه کردن کتاب خانه اعداد مختلط در برنامه ، از توابع تعریف شده در این کتاب خانه کمک گرفت :
#include <complex> using namespace std; typedef complex<double> point; //The Dot And Cross Products double dot (const point &a, const point &b) {return real (conj (a) * b);} double cross (const point &a, const point &b) {return imag (conj (a) * b);} // returns intersection of infinite lines ab and pq (undefined if they are parallel) point intersect (const point &a, const point &b, const point &p, const point &q) {double d1 = cross (p -a, b -a); double d2 = cross (q -a, b -a); return (d1 * q -d2 * p) / (d1 -d2);}
یکی از ویژگیهای این روش این است که میتوان مختصات محل تقاطع را به صورت یک نقطه داشت فلذا کار با آن و استفاده از آن در سایر معادلات بسیار آسانتر و قابل بررسیتر میشود.هم چنین در محاسبه پوش محدب تعدادی نقطه و سایر تعاریف هندسی نیز کاربرد فراوانی دارد.
در نظریه گراف و علومکامپیوتر پایینترین جد مشترک برای دو راس مثل
و
در هر درخت ریشه دار پایینترین راسی است که هر دوی
و
در زیر درخت آن باشند . درخت
راسی ریشهدار
و
پرسش به صورت
به طوری که
و
راسهایی از
هستند دادهشده است. مسئله پیدا کردن پایینترین جد مشترک برای هر دو راس
و
است.
مثلا در شکل زیر راس 4 پایینترین جد مشترک راسهای 10 و 8 است.
سادهترین راهی که به ذهن میرسد این است که به ازای هر پرسش ، ابتدا راسی که در عمق پایینتری وجود دارد را تا جایی که عمق آن برابر با عمق راس دیگر شود بالا بیاوریم(یعنی ).
سپس تا زمانی که دو راس برابر نشدند در هر مرحله هر دو را یکی بالا میاوریم. از آنجایی که راس ریشه جد مشترک آنهاست این فرایند حتما متوقف میشود و راسی که در آن متوقف شدیم پایینترین راسیاست که جد مشترک و است. این الگوریتم اولیه از مرتبهی برای هر پرسش عمل میکند و هزینه کل از مرتبهی خواهد بود. (چرا؟)
ایدهایی که برای بهبود عملکرد این الگوریتم به ذهن میرسد استفاده از روشی است که i-امین راس بالای هر راس دلخواه را در مرتبهی خوبی در اختیار ما قرار بدهد. مقدار را برابر با -امین راس بالای راس v در نظر میگیریم. برای تعیین مقادیر آرایه ancestor از روش برنامهریزی پویا استفاده میکنیم:
در قسمت اول الگوریتم ابتدا لازم بود که v و u را هم ارتفاع کنیم، میتوانیم برای پیدا کردن x-امین راس بالای v با گامهای به اندازهی
بالا برویم و در
مرحله راس مورد نظر را پیدا کنیم. (چرا؟) در قسمت بعدی برای پیدا کردن پایینترین جد مشترک u و v میتوانیم با جستجویدودویی روی فاصلهی راس جواب و v جواب را در مرتبهی
پیدا کنیم. میتوانیم با کمی هوشمندی مرتبهی پیدا کردن جواب برای هر پرسش را از
کاهش دهیم.
(راهنمایی: نمایش دودویی هر عدد کوچکتر از n حداکثر
رقم دارد).
#include <iostream> #include <vector> using namespace std; const int MAXN = 100 * 1000 + 1; const int MAX_LG_N = 17 + 1; const int MAXM = 100 * 1000; int ancestor[MAXN][MAX_LG_N]; int father[MAXN]; int n, m; int query[MAXM][2]; int answer[MAXM]; int depth[MAXN]; vector<int> child[MAXN]; bool dfs_mark[MAXN]; void dfs(int v); int up(int v, int dist); void input(); void preprocess(); int calculate_lca(int v, int u); int main(){ // get tree from input cin >> n; for(int i = 2; i <= n; i++){ cin >> father[i]; child[ father[i] ].push_back(i); } for(int i = 1; i <= m; i++) cin >> query[i][0] >> query[i][1]; preprocess(); // get queries form input and print answers cin >> m; for(int i = 1; i <= m; i++){ int v, u; cin >> v >> u; cout << calculate_lca(v, u) << endl; } return 0; } void preprocess(){ // calculate ancestor[v][i] father[1] = 1; // father[root] = root for(int i = 1; i <= n; i++) ancestor[i][0] = father[i]; for(int j = 1; j < MAX_LG_N; j++) for(int i = 1; i <= n; i++) ancestor[i][j] = ancestor[ ancestor[i][j-1] ][j-1]; // calculate depth[v] dfs(1); // dfs(root) } int calculate_lca(int v, int u){ if(depth[v] > depth[u]) swap(u, v); // to sure depth[v] < depth[u] u = up(u, depth[u] - depth[v]); for(int j = MAX_LG_N - 1; j >= 0; j--) if(ancestor[u][j] != ancestor[v][j]){ u = ancestor[u][j]; v = ancestor[v][j]; } if(u == v) return v; return ancestor[v][0]; } void dfs(int v){ dfs_mark[v] = true; for(int i = 0; i < child[v].size(); i++){ int u = child[v][i]; if(!dfs_mark[u]){ depth[u] = depth[v] + 1; dfs(u); } } } int up(int v, int dist){ for(int i = MAX_LG_N - 1; i >= 0; i--) if(dist >= (1 << i)){ v = ancestor[v][i]; dist -= (1 << i); } return v; }
در نظریهٔ گراف الگوریتم حذف معکوس(به انگلیسی: Reverse-delete algorithm) الگوریتمی است که در یک گراف همبند، با یال وزن دار درخت پوشای کمینه را بدست میآورد. اگر گراف ناهمبند باشد این الگوریتم درخت پوشای کمینه را برای هر مولفهٔ همبندی مییابد که در این صورت مجموعهٔ این درختهای پوشای کمینه را یک جنگل پوشای کمینه گویند.
این الگوریتم الگوریتمی حریصانه است که در هر لحظه بهترین انتخاب را انجام میدهد و برعکس الگوریتم کروسکال که الگوریتم دیگری برای پیدا کردن درخت پوشای کمینهاست، عمل میکند. الگوریتم کراسکال با یک گراف خالی شروع میکند و یالها را به آن اضافه میکند در حالیکه الگوریتم حذف معکوس با گراف اصلی شروع میکند و یالها را از آن حذف میکند. الگوریتم به صورت زیر عمل میکند:
الگوریتم حذف معکوس این تضمین را میدهد که گراف همبند باقی بماند، چون تنها در صورتی یالی را حذف میکند که باعث ناهمبند شدن گراف نشود. هر یالی که حذف میشود قبل از حذف در دوری شرکت داشتهاست. از آنجایی که الگوریتم از یال با بیشترین وزن شروع به حذف کردن میکند، آن یال بزرگ ترین یال در دور مربوط به خود است. پس بنابر تعریف درخت پوشای کمینه، یال حذف شده جزء درخت پوشای کمینه نخواهد بود.
نمونهی برنامه:
function ReverseDelete(edges[] E) sort E in decreasing order Define an index i ← 0 while i <size(E) Define edge temp ← E[i] delete E[i] if temp.v1 is not connected to temp.v2 E[i] ← temp i ← i + 1 return edges[] E
تعدادی از تکنیکهای کلاسبندی آماری و یادگیری ماشینی در روش کلاسبندی در متن کاوی به کار بده میشود. برخی از این تکنیکهاعبارتند از مدلهای رگرسیون، کلاسهبندهای بیزی، درختهای تصمیمگیری، کلاسهبندهای نزدیکترین همسایه و ماشینهای برداری و ... که ما در این مقاله به بررسی و تحلیل بعضی از این تکنیکها میپردازیم.
KNN: K-Nearest Neighbor
مجموعه سندهای آموزشی است. D = {D1, D2, ..., Dn}
Q سند جدیدی است که قرار است مورد بررسی قرار گیرد.
TF، یا Term Frequency، یک عبارت در یک سند است که به عنوان دادههای آموزشی پیشپردازش شدهاست.
V، مقدار کسینوس شباهت Q و Di است. به طوری که (i=1, 2, ..., n)
تحلیل الگوریتم:
برج هانوی یک مسأله معروف الگوریتمی است، که پیشینهی تاریخی بسیار طولانی دارد. اولین بار این مسأله در یک معبد شکل گرفت. سه برج الماس در این معبد وجود داشت که در اولین برج، 64 حلقه از کوچک به بزرگ مرتب شده بودند. کاینان قصد داشتند تا این حلقه ها را از روی این برج، به برج دیگر منتقل کنند و معتقد بودند با انجام این کار، عمر دنیا به پایان خواهد رسید. شرح مسأله بدین صورت است : سه میله داریم که در اولین میله n مهره قرار دارد. میخواهیم این مهرهها را به سومین میله منتقل کنیم با این شرط که در هر مرحله و در هر میله، مهرههای موجود در آن میله از بالا به پایین به ترتیب کوچک به بزرگ باشند.
فرض کنید میلهها را با اعداد 1 تا 3 شمارهگذاری کنیم. هدف بردن n دیسک از میله شماره 1 به میله شماره 3 است. میتوان مسأله را به این صورت شکست : ایتدا فرض میکنیم بزرگترین حلقه وجود ندارد. این فرض صحیح است، چرا که حلقهای وجود ندارد که اندازهی آن از این میله بزرگتر باشد. مسآله را تبدلی می کنیم به این مسآله که، می خواهیم n-1 حلقه را از میله شماره 1 به میله شماره 2 منتقل کنیم. سپس حلقهی شماره n را به میلهی شماره 3 منتقل کنیم و سپس مسألهی انتقال n-1 حلقه از میلهی شماره 2 به میلهی شماره 3 را حل کنیم. پیاده سازی این الگوریتم به صورت زیر است :
void Hanoi (int start, int end, int count) { if(count == 1 ) { cout<<"Ring number 1 from "<<start<<" to "<<end<<endl; return; } Hanoi(start, 6-start-end, count-1); cout << "Ring number "<<count<<" from "<<start<<" to "<<end<<endl; Hanoi(6-start-end, end , count-1); }
دو ظرف به ظرفیتهای ۵ و ۶ لیتری، ابتدا هر دو ظرف خالی هستند.
در این مسئله عملکرد یک جاروبرقی هوشمند را مورد بررسی قرار می دهیم. فرض می کنیم که دو اتاق وجود دارد که هر کدام از آن ها ممکن است شامل خاک باشد یا نباشد و عامل ممکن است در محیط ۱ یا ۲ باشد. عامل می تواند مستقیم برود و یا به چپ یا راست بپیچد. بنابراین هشت حالت ممکن، به عنوان عمل بعدی، وجود دارد.
ابتدا تصور کنید که حسگر های عامل به او اطلاعات کافی می دهند.(دنیا قابل دسترسی است.) و همچنین عامل می داند هر کدام از اعمالش چه تغییری در محیط ایجاد می کنند و سپس می تواند محاسبه کند با کدام وضعیت پس از انجام عمل رو به رو خواهد شد. این ساده ترین حالت مسئله است که به آن مسئله تک حالته می گویند.
حالا تصور کنید که عامل تمام اثرهای عمل هایش را می داند، اما دسترسی به محیط محدود است. برای مثال، عامل هیچ حسگری ندارد. در این حالت فقط می داند که وضعیت اولیه اش یکی از اعضای مجموعه (۱،۲،۳،۴،۵،۶،۷،۸) است. ممکن است فکر کنید که وضعیت عامل ناامید کننده است! اما در حقیقت اینقدر ها هم که به نظر می آید ناگوار نیست. زیرا عامل می داند که هر کدام از اعمالش چه اثری دارند. برای مثال می تواند محاسبه کند که عمل راست موجب می شود تا در یکی از حالات (۲،۴،۶،۸) باشد. بنابراین می تواند با انتخاب یک دنباله عملیاتی، به هدف برسد. به طور خلاصه هنگامی که محیط تماما قابل دسترسی نیست، عامل باید در مورد مجموعه حالت هایی که ممکن است هدف برسد، استدلال کند. به این نوع از مسئله مسئله چند حالته می گویند.
اگر عامل اثر اعمال خودش را نادیده بگیرد می تواند به مشکلات دیگری دچار شود. برای مثال تصور کنید که محیط غیر قطعی باشد از این رو باید از قانون مورفی(Murphy) تبعیت کند. برای مثال عامل می داند که در یکی از وضعیت های ۱ یا ۳ است. با انجام هر عمل در یک حالت دیگر قرار می گیرد که امکان دارد آن را به هدف برساند و یا با شکست مواجه شود. در چنین مواردی عامل تمام درخت عملیات را محاسبه کند. به طور کلی هر شاخه درخت با یک امکان احتمالی که از آن ناشی می شود، بررسی می شود. به همین علت به آن مسئله احتمالی می گویند.
حال تصور کنید که عامل هیچ اطلاعی در مورد اثرات اعمالش و اینکه در کجا قرار دارد، ندارد.(بدبخت تر از این عامل سراغ ندارید!؟) مانند کسی که در یک کشور غریب و بدون هیچ نقشه ای گم شده است. در ای حالت عامل باید تجربه کند و به تدریج کشف کند که چه عملی باید انجام شود و چه وضعیت هایی وجود دارند. این روش یک نوع جستجو است که بر خلاف جستجو در دنیای فرضی، می تواند عامل را با خطرات جدی مواجه کند. به مسائلی از این قبیل، مسئله اکتشافی می گویند.
از جستجوی ابتدا بهترین استفاده میکند و کوتاه ترین مسیر را بین دو گره مبدا و مقصد داده شده به وسیله گرههای دیگر مییابد.
این روش با ترکیب
هزینه رسیدن به گره
و
هیوریستیک یا تخمین هزینه رسیدن از گره
م تا هدف گرهها را ارزیابی میکند:
از آنجایی که
هزینه مسیر از گره شروع به گره
و
هزینه تخمینی ارزان ترین مسیر از
به هدف است، داریم:
= هزینه برآورد ارزان ترین راه حل از طریق
بنابراین اگر به دنبال ارزان ترین راه حل هستیم، اولین کار معقول امتحان گرهای است که کمترین مقدار را داراست. مشخص میشود که این راهبرد کاملاً معقولانهاست. اگر تابع آروین شرایط خاصی داشته باشد. جستجوی A* هم کامل و هم بهینهاست.
اگر A* همراه با الگوریتم جستجوی درخت استفاده شود، آنگاه تحلیل بهینگی آن بسیار ساده و سر راست میشود. A* بهینهاست اگر یک آروین قابل قبول باشد. یعنی هرگز هزینه رسیدن به هدف را بیش از اندازه واقعی برآورد نکند. این آروینها ذاتاً بهینه هستند زیرا آنها فکر میکنند که هزینه راه حل مسئله کمتر از مقدار واقعی آن است. از آنجا که هزینه رسیدن به را به طور دقیق نشان میدهد، میتوان بلافاصله نتیجه گرفت که هرگز هزینه واقعی راه حلی که از میگذرد را اضافه برآورد نمیکند.
function A*(start,goal) closedset:= the empty set // The set of nodes already evaluated. openset:= {start} // The set of tentative nodes to be evaluated, // initially containing the start node came_from:= the empty map // The map of navigated nodes. g_score[start]:= 0 // Cost from start along best known path. h_score[start]:= heuristic_cost_estimate(start, goal) f_score[start]:= g_score[start] + h_score[start] // Estimated total cost // from start to goal through y. while openset is not empty x:= the node in openset having the lowest f_score[] value if x = goal return reconstruct_path(came_from, came_from[goal]) remove x from openset add x to closedset foreach y in neighbor_nodes(x) if y in closedset continue tentative_g_score:= g_score[x] + dist_between(x,y) if y not in openset add y to openset tentative_is_better:= true else if tentative_g_score <g_score[y] tentative_is_better:= true else tentative_is_better:= false if tentative_is_better = true came_from[y]:= x g_score[y]:= tentative_g_score h_score[y]:= heuristic_cost_estimate(y, goal) f_score[y]:= g_score[y] + h_score[y] return failure function reconstruct_path(came_from, current_node) if came_from[current_node] is set p:= reconstruct_path(came_from, came_from[current_node]) return (p + current_node) else return current_node
جستوجوی اولسطح (Breadth First Search) که به bfs مشهور است، روشی برای پیمایش گراف است که بعد از جستوجوی عمقاول مهمترین و پرکاربردترین الگوریتم پیمایش گراف است. این الگوریتم معمولا در گرافهای وزن دار کاربرد خاصی ندارد و عمده نقش آن در گرافهای بیوزن و برای پیدا کردن کوتاهترین فاصله بین یک راس تا بقیهی راسهااست.
این الگوریتم ابتدا یک راس ریشه مانند v را به عنوان شروع میگیرد. سپس راسها را سطح بندی میکند. سطح بندی به این صورت است که تمام راسهای مجاور v را در سطح اول قرار میدهیم، حال برای سطح راس u که مجاور یکی از رئوس سطح است را در این سطح قرار میدهیم به شرطی که u در هیچ سطح دیگری نیامده باشد. به عبارت دیگر راس هارا بر اساس کوتاهترین فاصله از v سطح بندی میکنیم. حال برای پیمایش به ترتیب سطح، و در هر سطح به ترتیب دلخواه وارد راسها میشویم. پس اولین زمانی که به هر راس میرسیم، با کمترین فاصله ممکن نسبت به v رسیده ایم.
با توجه به اینکه به هر راس حداکثر یکبار وارد میشویم در نتیجه هر یالی هم حداکثر دوبار به ازای دو سر آن شمرده میشود، پس در کل هزینه الگوریتم از است که e تعداد یالها و n تعداد راسها است.
برای ایجاد سطح های مختلف و حرکت به ترتیب سطح ها نیاز به استفاده از صف داریم. فرض کرده ایم که لیست مجاورت راس ها را داده اند و الگوریتم راس آغازین را نیز از ورودی میگیرد.
#include <queue> #include <vector> const int MAXN = 100 * 1000 + 10; bool mark[MAXN]; int vector<int> adj[MAXN]; void bfs(int v) { queue<int> q; mark[v] = 1; q.push(v); while(q.size()) { v = q.front(); // راس ابتدایی را از صف بر میداریم q.pop(); // کارهای پیشترتیب را اینجا مینویسیم for(int i = 0; i < adj[v].size(); i++) { int u = adj[v][i]; if (mark[u] == 1) // قبلا این راس را به صف اضافه نکرده باشیم continue; mark[u] = 1; q.push(u); } } }
میدانیم که جستجوی عمیق کننده تکراری تکنیکی مفید برای کاهش درخواست حافظهاست حال میتوانیم از این تکنیک استفاده نموده و هر بار یک جستجوی عمقی تا هزینهf-limite را جستجو کنیم. اگر به جواب نرسیدیم هزینه f-limite را افزایش داده و از اول به بسط فضای حالت میپردازیم.
نته:جستوجوی عمقاول که به معروف است در واقع الگوریتمی برای پیمایش گراف است. شاید با کمی شک بتوان گفت که پرکاربردترین الگوریتم در گراف همین الگوریتم است چراکه هم کد آن کم است، هم هزینه زمانی و حافظهای آن کم است، هم برای اکثر سوالهای گراف نیاز به پیمایش است.
الگوریتم به این شکل است که ابتدا یک رأس مانند v را انتخاب میکنیم و آن را ریشه مینامیم. ریشه را علامتگذاری میکنیم. سپس یک رأس دل خواه علامت نخوردهی مجاور با را انتخاب میکنیم و آن را مینامیم. را یکی از بچههای میکنیم، سپس را علامت میزنیم. حال همین الگوریتم را روی از ابتدا اجرا میکنیم (یعنی یکی از همسایههای مجاور و علامت نخورده را انتخاب میکنیم و …).
الگوریتم گفته شده زمانی به بنبست میخورد که به ازای راسی مانند ، تمام همسایههایش علامت خورده باشند. در این صورت به راس پدر (رأسی که از آن وارد شدیم) برمیگردیم و دوباره همین الگوریتم را از ادامه اجرا میکنیم (یعنی وارد پدر میشویم و اگر همسایهی علامت نخوردهای داشت وارد آن میشویم و اگر نداشت به رأس پدرش باز میگردیم).
برنامه زمانی متوقف میشود که به رأس برگشته باشیم و تمام همسایههایش علامت خورده باشند که در این صورت میگوییم الگوریتم پایان یافته است. \\دقت کنید که اگر گراف شما همبند نباشد، این جستوجو تنها رأسهای مؤلفه ریشه را پیمایش میکند پس اگر برای پیمایش روی تمام رأسها این الگوریتم را به ازای هر رأس علامتنخوردهای تکرار میکنیم.
جستوجوی اولعمق یالهایی که تشکیل دور میدهند را نمیرود؛ در نتیجه اگر یالهای رفته شده را کنار هم بگذاریم، تشکیل یک درخت ریشهدار میدهند که به آن درخت جستوجوی اولعمق میگویند. همچنین زمان ورود و خروج رأسها نیز ویژگیهای منحصر به فردی دارد. مجموع این ویژگیهاباعث شده که این الگوریتم تبدیل به الگوریتمی مهم و کاربردی شود.
در طول پیمایش گراف، رأسها را به طور خاصی رنگ میکنیم. در ابتدای کار همه ی رأسها را سفید میگیریم. حال اولین زمانی که وارد هر رأس شدیم آن را خاکستری میکنیم و وارد رأسهای همسایهاش میشویم. بدین ترتیب رأس خاکستری یعنی رأسی که هنوز کار آن تمام نشده و منتظر است تا کار بچههایش تمام شود اما رأس سفید یعنی رأسی که هنوز ملاقات نشده است. حال هنگامی که تمام همسایههای یک رأس دیده شده بودند و در حال بازگشت به رأس پدر بودیم، آن راس را سیاه میکنیم. در نتیجه هر رأسی که کارش تمام شود، سیاه میشود پس در آخر کار همه راس ها سیاه هستند.
زمان ورود یا شروع ( ) و خروج یا پایان ( ) را به ترتیب اینگونه تعریف میکنند: اولین زمان دیده شدن رأس و آخرین زمان دیده شدن رأس. یعنی زمانی که برای اولین بار وارد یک راس میشویم و آن را علامت گذاری میکنیم را زمان ورود و آخرین زمانی که از رأس خارج میشویم و تمام همسایههایش دیده شده است و در حال بازگشت به راس پدر هستیم را زمان خروج میگیریم. پس اگر بخواهیم با رنگها این دو زمان را معادل کنیم، زمان خاکستری شدن برابر زمان شروع و زمان سیاه شدن برابر زمان خروج است.
در زیر سه لم در مورد این زمانها آوردهایم که اثبات دو لم اول راحت است و لم سوم نیز با استفاده از این دولم ثابت میشود.
در نتیجه خاصیت خوبی که این درخت و این الگوریتم به ما میدهد این است که میتوانیم فرض کنیم هنگامی که از یک رأس خارج میشویم، کار تمام زیر درخت آن تمام شده است. در نتیجه با توجه به جواب بچههای این رأس، جواب این راس را محاسبه میکنیم. برای همین معمولا در صورت نیاز به برنامهریزی پویا روی گراف از جستوجوی عمقاول استفاده میکنند.
درخت جستوجوی عمقاول در واقع یک درخت ریشهدار است که ریشه آن همان رأسی است که جستوجو از آن آغاز شده است. این درخت شامل تمام یالهایی است که الگوریتم روی آنها حرکت کرده و و پدر هر راسی، راسی است که از آن وارد این راس شدهایم. پس واضح است که برخی از یالها در درخت نمیآیند و همچنین این درخت ریشهدار دور ندارد چون هر رأسی تنها یک پدر دارد! به طور کلی یالهای گراف اصلی را میتوان به ۴ دسته تقسیم کرد:
در گرافهای بدونجهت دسته دو و سه یکی هستند زیرا یالها جهتی ندارند و یک یال جلورو حتما یک یال عقبرو است و برعکس. به همین ترتیب یالی از نوع چهارم نیز ندارند؛ چراکه اگر پیمایش به یال میانی برمیخورد، وارد آن میشد و آن یال باید درختی میشد.
همچنین در گراف های جهتدار یال میانی از سمت چپ درخت به سمت راست نداریم. (یعنی یال میانی از v به u داریم اگر و تنها اگر زمان خاتمه u زودتر از v باشد).
از آنجایی که به رأس حداکثر یکبار وارد میشویم در نتیجه الگوریتم هر یالی را حداکثر دوبار میبیند (یکبار به ازای هر سر یال) پس در کل زمان اجرای آن از است. که تعداد رأسها و تعداد یالها است.
اگر گراف را به صورت لیست مجاورت داده باشند، کد آن به صورت زیر میشود.
#include <vector> #include <iostream> const int MAXN = 100 * 1000 + 10; using namespace std; bool mark[MAXN]; int color[MAXN]; // رنگ رأس int start[MAXN]; // زمان شروع int finish[MAXN]; // زمان پایان vector <int> adj[MAXN]; // لیست مجاورت int n; // تعداد رأسها int m; // تعداد یالها int now_time; // زمان فعلی void dfs(int v) { mark[v] = 1; color[v] = 1; // این رأس را خاکستری کن start[v] = now_time++; // کارهای پیشترتیب را انجام بده for(int i = 0; i < adj[v].size(); i++) { int u = adj[v][i]; if(mark[u] != 1) dfs(u); // کارهای پسترتیب این یال را انجام بده } color[v] = 2; // این رأس را سیاه کن finish[v] = now_time; // میتوانید هنگام خروج هم زمان را اضافه کنید. // کارهای پسترتیب این رأس را انجام بده } void input() { cin >> n >> m; for (int i = 0; i < m; i++) { int v, u; cin >> v >> u; adj[--v].push_back(--u); adj[u].push_back(v); } } int main() { input(); for (int i = 0; i < n; i++) if(mark[i] == 0) dfs(i); }
در روش قبلی(*IDA)استفاده بسیار جزئی از حافظه داشت و بین تکرارها این جستجو فقط یک عدد خاص از محدوده جاری را نگه میداشت (f-cost) و چون نمیتوانست تاریخچه اش را به خاطر آورد مجبور به تکرار میشد الگوریتم *SMA قادر خواهد بود تا از تمام حافظه موجود برای اجرای جستجو استفاده نماید هرچه حافظه بیشتر باشد کارآیی جستجو بالاتر خواهد بود.
ویژگیها:زمانی که نیاز به تولید فرزند باشد ولی حافظهای در اختیار نداشته باشیم نیاز به نوشتن مجدد به روی حافظه قبلی است برای انجام این امر الگوریتم یک گره را حذف میکند و فرزند جدید از حافظه آن استفاده خواهد کرد گرههایی که بدین طریق حذف میشوند گرههای فراموش شده نام دارند. همیشه گرههایی برای حذف شدن انتخاب میشوند که هزینهٔ آﻧﻬا بالا است. برای جلوگیری از جستجوی مجدد زیر درختهایی که از حافظه حذف شدهاند، در گره پدر آﻧﻬا اطلاعاتی در مورد کیفیت بهترین مسیر در زیر درخت فراموش شده نگهداری میشود. بدین طریق فقط زمانی زیر درختها دوباره تولید میشوند که ثابت شود تمام مسیرهای دیگر بدتر از مسیر فراموش شده باشد.
چگونه می توان مهرهی وزیر شطرنج را در یک صفحهی شطرنج چید به طوری که هیچ ۲ مهره از این مهره یک دیگر را تهدید نکنند؟
سادهترین شکل الگوریتم به این صورت است:
از سطر اول صفحه شروع کرده ، در هر سطر به صورت بازگشتی هر دفعه یکی از ستون ۱ تا
را به وزیر سطر
اختصاص داده و سپس به سطر های بعد میرویم. در صورتی که دو وزیر همدیگر را تهدید کنند به مرحلهی قبل باز میگردیم. در صورتی که همهی
وزیر را در صفحه قرار دهیم، جواب را گزارش می کنیم.
پیچیدگی الگوریتم بالا است.
کد زیر برای حالت شمارش جواب ها ( SINGLE_SOLUTION=0 ) تا جواب میدهد و برای حالت گزارش یک جواب تا در زمان خوبی جواب میدهد.
#include <iostream> #define SINGLE_SOLUTION 0 #define PRINT_SOLUTIONS 0 using namespace std; const int maxn = 100; int n; int a[maxn]; int mark[maxn]; int markd1[maxn*2]; int markd2[maxn*2]; int ans=0; void acceptboard(){ ans++; if(PRINT_SOLUTIONS){ for(int i = 0 ; i < n ; ++i){ for(int j = 0 ; j < n ; ++j) if(a[i]==j) cout << "Q"; else{ cout << "."; } cout << endl; } cout << endl; } if(SINGLE_SOLUTION){ exit(0); } }
حالت یافتن یک جواب را میتوان با روش های پیشرفته تر از روش پسگرد تا اندازهی زیادی سریع کرد. مثلا کد زیر در طی چند ثانیه یک جایگشت از ستون های ۱۰۰۰۰ وزیر که یک دیگر را تهدید نمیکنند به دست میآورد. برای مطالعه ی بیشتر میتوانید به لینک های زیر در ویکیپدیا مراجعه کنید:
Hill Climbing
Simulated annealing
#include <iostream> #include <iostream> #include <cstdlib> #include <algorithm> #include <queue> #include <deque> #include <set> #include <vector> #include <map> #include <string> #include <cstring> #include <iomanip> #include <cstdio> #include <fstream> #include <sstream> #define For(i,a,n) for(int i =a ; i < n ; ++i ) #define all(x) (x).begin(),(x).end() #define n(x) (int)(x).size() #define pb(x) push_back(x) using namespace std; typedef long long ll; typedef pair<int,int> pii; const int maxn = 50000; int n; int t=100*1000; int dl[maxn*2]; int dr[maxn*2]; int ja[maxn]; int r(int i , int j) { return j-i+n-1; } int l(int i, int j) { return i+j; } ll k; int it; int main() { ios::sync_with_stdio(false); srand(4131); cin >> n; For(i,0,n) ja[i]=i; do { random_shuffle(ja,ja+n); it=0; For(i,0,2*n) dl[i]=dr[i]=0; For(i,0,n) { dl[l(i,ja[i])]++; dr[r(i,ja[i])]++; } k=0; For(i,0,n) k-=dr[r(i,ja[i])]-1; For(i,0,n) k-=dl[l(i,ja[i])]-1; k/=2; while(it<t) { For(f,0,n){ For(s,f+1,n){ it++; // int f=rand()%n; // int s=f; // while(s==f) // s=rand()%n; int si=dr[r(s,ja[s])]+dl[l(s,ja[s])]-dl[l(s,ja[f])]-dr[r(s,ja[f])]-2; dr[r(s,ja[s])]--; dl[l(s,ja[s])]--; dl[l(s,ja[f])]++; dr[r(s,ja[f])]++; int fi=dr[r(f,ja[f])]+dl[l(f,ja[f])]-dl[l(f,ja[s])]-dr[r(f,ja[s])]-2; dr[r(f,ja[f])]--; dl[l(f,ja[f])]--; dl[l(f,ja[s])]++; dr[r(f,ja[s])]++; if(fi+si>0|| (!(rand()%(5))) && (k < -1000000)) { swap(ja[f],ja[s]); k+=fi+si; } else { dr[r(s,ja[s])]++; dl[l(s,ja[s])]++; dl[l(s,ja[f])]--; dr[r(s,ja[f])]--; dr[r(f,ja[f])]++; dl[l(f,ja[f])]++; dl[l(f,ja[s])]--; dr[r(f,ja[s])]--; } } } } }while(k); const int DBG=0; if(DBG) { For(i,0,n) { For(j,0,ja[i]) cout << "."; cout << "Q"; For(j,ja[i]+1,n) cout << "."; cout << endl; } } else { For(i,0,n) cout << ja[i]+1 << endl; } return 0; } // //el psy congroo //
این مسئله شکلهای متنوعای دارد، یکی از فرمهای آن در کولهپشتی پویا بررسی شدهاست. در حالت کلی این مسئله به صورت زیر تعریف میشود:
فرض کنید یک کولهپشتی با حجمی ثابت و مجموعهای از اشیاء دارید که هر کدام از آن ها حجمی و ارزشی دارند. میخواهید کولهپشتی خود را به نحوی پرکنید که حجم اشیا برداشته شده از حجم کولهپشتی بیشتر نباشد و مجموع ارزش اشیا بیشینه باشد.
در بعضی از شکل های مسئله تعداد یک شی بیشمار و در بعضی محدود است، در بعضی از اشکال اشیا ارزشی برابر دارند و در بعضی اشیا میتوانند به صورت پیوسته برداشته شوند (2.5 کیلوگرم شن یا بنزین در یک مخزن(کولهپشتی)).
لازم به ذکر است که در بیشتر این شکل ها الگوریتم حریصانه جواب بهینه را نمیدهد.
برای حالت گسسته معمولا مثال نقضی پیدا میشود که این الگوریتم جواب بهینه را ندهد. برای همین در اینجا حالت پیوسته را بررسی میکنیم، فرض کنید نوع شی متفاوت داریم که از امین نوع، به حجم و ارزش موجود است ، میخواهیم به حجم از آنها برداریم به صورتی که مجموع ارزش بیشینه شود، می توان کسری از یک شی را برداشت.
فرض کنید شی
ام بیشترین مقدار
را دارد، یک جواب بهینه دلخواه را در نظر بگیرید، در صورتی که مقداری از این شی استفاده نشده باشد و در کولهپشتی مقداری از شی دیگری (مثلا
) باشد ، میتوان جواب را با تعویض مقداری از
با مقداری از
بهتر کرد یا بهینه نگهداشت. پس شی
انتخاب حریصانهی مناسبی است و الگوریتم زیر جواب بهینه را می دهد :
از کولهپشتی خالی شروع کرده و در هر مرحله کوله پشتی را با شیئی که بیشترین ارزش را نسبت به حجم دارد پر میکنیم تا وقتی که کولهپشتی پرشود یا آن شی تمام شود.
در صورت مرتب بودن اشیا بر حسب ، پیچیدگی الگوریتم از است، در غیر این صورت از میباشد.
// مرتب سازی در صورت نیاز انجام شود for(int i = 0 ; i < n ; ++i){ double v=min(W,w[i]); W-=v; ans+=v*c[i];
الگوریتم تپه نوردی (Hill climbing) الگوریتمی است که برای یافتن بهترین پاسخ یک مسئله یا برای پیدا کردن پاسخی از مسئله که به اندازهٔ کافی مناسب و بهینه باشد، استفاده می شود.
برای بررسی الگوریتم تپه نوردی مسئلهٔ زیر را بررسی می کنیم:
برای بررسی الگوریتم تپه نوردی مسئلهٔ زیر را بررسی می کنیم:
- تابع f به هر یک از اعضای مجموعهٔ متناهی S یک عدد حقیقی نسبت می دهد. هدف ما یافتن عضوی از S است که بیشترین (یا کم ترین) مقدار به آن نسبت داده شده است.
همان گونه که گفته شد در این جا فرض بر این است که مقدار تابع f برای چندین عضو مختلف S بیشینه است و هدف ما یافتن تنها یکی از آن هاست. (در غیر این صورت معمولاً الگوریتم تپه نوردی، الگوریتم کار آمدی نیست.)
ابتدا یکی از اعضای مجموعهٔ S را (به صورت تصادفی) انتخاب می کنیم و آن را A می نامیم. سپس در میان اعضایی از S که به A نزدیکند به دنبال پاسخ مناسب تری برای مسئله می گردیم. به بیانی دیگر نلاش می کنیم در میان اعضایی که به A نزدیکند عضوی بیابیم که مقدار تابع f برای آن بیش تر از
باشد. سپس این روند را ادامه می دهیم تا به یک بیشینهٔ نسبی برای f دست یابیم.
بدیهی است که بیشینهٔ نسبی به دست آمده لزوماً پاسخ خواسته شده نیست. ولی اگر الگوریتم گفته شده چندین بار تکرار شود می توان به یافتن پاسخی مطلوب امیدوار بود. (در هر بار اجرای الگوریتم نخستین عضو به صورت تصادفی انتخاب می شود.) بدی این الگوریتم این است که در یک تابع که مینیمم ویا ماگزیمم محلی زیادی دارد(مانند تابع Ackley)به راحتی در ان گیر میفتد و قدرت خروج از انجا را ندارد چون این الگوریتم فقط نقاط اطراف خود را میبیند که در لحظه حضور در یک ماگزیمم محلی این احساس را دارد که از همه نقاط بالاتر است در حالی که چنین نیست و ان در یک ماگزیمم محلی اسیر شده است.
سورس برنامه تپه نوردی در محیط دو بعدی (که با f سه بعدی می شود)
Hill Climbing Algorithm bestEval = -INF; currentNode = startNode; bestNode = NULL; for MAX times if (EVAL(currentNode)> bestEval) bestEval = EVAL(currentNode); bestNode = currentNode; L = NEIGHBORS(currentNode); nextEval = -INF; for all x in L if (EVAL(x)> nextEval) currentNode = x; nextEval = EVAL(x); return currentNode
همانطور که در الگوریتم تپه نوردی بررسی کردیم، این روش جستجوی محلی دارای مشکل قرار گرفتن در بهینگی محلی است. این مشکل تا حدی است که حتی در مورد مسائل ساده ای همچون مسئله 8 وزیر نیز این روش جستجو از درصد موفقیت بسیار پایینی برخوردار بود.
با این حال می توان تغییر کوچکی در این الگوریتم داد و تا حدودی میزان موقیت الگوریتم تپه نوردی را در حل مسائله بهینه سازی بالا برد. مشکل الگوریتم تپه نوردی این بود که هنگام قادر به تغییر حالت از یک محل بهینگی به محل بهینگی دیگر نبود. اما برای گریز از بهینگی های محلی می توانیم ترتیبی اتخاذ کنیم که هنگام قرار گرفتن در بهینگی های محلی به بخش دیگری از فضای حالت جهش کنیم و سپس در آنجا به جستجوی جواب با روش تپه نوردی بپردازیم و این عمل را تا رسیدن به یک ماکزیمم سراسری تکرار کنیم. شبه کد زیر نحوه اجرای این الگوریتم را نشان می دهد :
Procedure HillClimbing Generate a solution ( s' ) Best = S' Loop S = Best S' = Neighbors (S) Best = SelectBest (S') IF there is no changes in Best solution THEN Jump to new state in state space Until stop criterion satisfied
الگوریتم تبرید شبیهسازی شده (Simulated Annealing) (SA)، یک الگوریتم بهینهسازی فراابتکاری ساده و اثربخش در حل مسائل بهینهسازی است. منشأ الگوریتم تبرید شبیهسازی شده، کارهای کریک پاتریک و کرنی و همکارانشان در سالهای ۱۹۸۳ و ۱۹۸۵ است. کریک پاتریک و همکارانش، متخصصانی در زمینهٔ فیزیک آماری بودند. آنها برای حل مسائل سخت بهینهسازی، روشی مبتنی بر تکنیک تبرید تدریجی پیشنهاد نمودند. تکنیک تبرید تدریجی، به وسیلهٔ متالورژیستها برای رسیدن به حالتی که در آن ماده جامد، به خوبی مرتب و انرژی آن کمینه شده باشد، استفاده میشود. این تکنیک شامل قرار دادن ماده در دمای بالا و سپس کم کردن تدریجی این دماست.
در روش شبیهسازی تبریدی (SA)، هر نقطه s در فضای جستجو مشابه یک حالت از یک سیستم فیزیکی است، و تابع (E(s که باید کمینه شود، مشابه با انرژی داخلی سیستم در آن حالت است. در این روش، هدف انتقال سیستم از حالت اولیه دلخواه، به حالتی است که سیستم در آن کمترین انرژی را داشته باشد.
برای حل یک مسئلهٔ بهینهسازی، الگوریتم SA ابتدا از یک جواب اولیه شروع میکند و سپس در یک حلقه تکرار به جوابهای همسایه حرکت میکند. اگر جواب همسایه بهتر از جواب فعلی باشد، الگوریتم آن را به عنوان جواب فعلی قرار میدهد (به آن حرکت میکند)، در غیر این صورت، الگوریتم آن جواب را با احتمال exp(-ΔE/T) به عنوان جواب فعلی میپذیرد. در این رابطه ΔE تفاوت بین تابع هدف جواب فعلی و جواب همسایهاست و T یک پارامتر به نام دما است. در هر دما، چندین تکرار اجرا میشود و سپس دما به آرامی کاهش داده میشود. در گامهای اولیه دما خیلی بالا قرار داده میشود تا احتمال بیشتری برای پذیرش جوابهای بدتر وجود داشته باشد. با کاهش تدریجی دما، در گامهای پایانی احتمال کمتری برای پذیرش جوابهای بدتر وجود خواهد داشت و بنابراین الگوریتم به سمت یک جواب خوب همگرا میشود. الگوریتم SAیک الگوریتم غیرمقید میباشد که برای طراحیهای سخت به کار میرود.
همسایههای یک حالت (جواب)، حالتهای جدیدی از مسئله هستند که با تغییر در حالت کنونی و با توجه به روشی از پیش تعیین شده ایجاد میشوند. برای مثال در مسئله فروشندهٔ دورهگرد، هر حالت به طور کلی یک جایگشت خاص از شهرهایی است که باید ملاقات شوند. همسایهٔ یک جواب، جایگشتهایی هستند که با انتخاب یک جفت از شهرهای هم جوار، از کل مجموعه جایگشتها، و جابجا کردن آن دو شهر ایجاد میشوند. عمل تغییر در جواب فعلی و رفتن به جوابهای همسایه «حرکت» (move) خوانده میشود و «حرکت» های متفاوت، همسایههای گوناگون را بدست میدهد.
الگوریتم پریم، الگوریتمی برای پیدا کردن درخت پوشای کمینه یک گراف وزندار است که مشابه الگوریتم دایکسترا برای درخت کوتاهترین مسیر است.
در روش پریم ابتدا یک راس را انتخاب میکنیم. سپس کم وزنترین یالهای این راس را به عنوان یک یال درخت انتخاب میکنیم. حال بین یال های این دو راس کموزنترین یال را پیدا کرده و به عنوان عضوی از درخت انتخاب میکنیم. حال بین یالهای این سه راس کموزنترین را انتخاب میکنیم. همینطور ادامه میدهیم تا درخت کامل شود. باید توجه داشت که یالی که هر بار اضافه میکنیم دور ایجاد نکند یا به عبارت دیگر یک سر آن در بین راسهای غیر اتخاب شده باشد.
ادعا: اگر گراف را به دو مجموعه و از راسها افراز کنیم، کموزنترین یال بین یالهایی که یک طرفشان در مجموعه و طرف دیگرشان در مجموعه است باید جزء درخت پوشای کمینه باشد. (اگر چند یال با کمترین وزن وجود داشت، باید دقیقا یکی از آنها جزء درخت پوشای کمینه باشد).
اثبات: اگر یالی بین این دو مجموعه انتخاب نشود، گراف همبند نمیشود، اگر ۲ یال یا بیشتر انتخاب شود با فرض همبندی در هر دو مجموعه دور ایجاد خواهد شد. و اگر یالی که کمترین وزن را ندارد انتخاب شود، میتوان با عوض کردن آن با کموزن ترین ویژگی های درخت را حفظ کرد و مجموع وزن ها را کاهش داد. پس کموزنترین انتخاب میشود.
تصویر زیر نمایشی از اجرای الگوریتم پریم روی یک مثال است.
یالهای سیاه، گزینههایی انتخابی در هر مرحلهاند. یالهای قرمز و راسهای سیاه انتخاب شدهاند. یالها و راسهای خاکستری انتخاب نشدهاند.
یک روش خوب برای بهینه کردن الگوریتم نگه داشتن کمترین فاصلهی هر راس تا راسهای انتخابیست. وقتی یک راس به مجموعهی ما اضافه شد. فاصلهی بقیه راسها را به روز میکنیم. در این صورت هر بار برای پیدا کردن راس نزدیکتر و به روز رسانی عملیات انجام میدهیم و چون بار این کار انجام میدهیم، پیچیدگی کل الگوریتم میشود. اگر فاصلههر راس تا نزدیکترین راس از بین راسهایی که در مجموعه قرار گرفتهاند را در داهساختاری مناسب ذخیره کنیم میتوانیم پیچیدگی الگوریتم را بهتر کنیم. اگر این دادهساختار هرم کمینه باشد، چون حذف و اضافه از است و به ازای یال حداکثر یک بار عملیات اضافه کردن رخ میدهد و چون قطعا تعداد عملیات حذف کمتر از تعداد عملیات اضافه کردن است پیچیدگی الگوریتم به تغییر میکند که برای حالاتی که تعداد یالها از کمتر باشد پیچیدگی کل کاهش پیدا میکند. حال اگر از هرم فیبوناچی استفاده کنیم پیچیدگی به کاهش پیدا میکند.
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int INF=2*1000*1000*1000+5; // ثابتی بسیار بزرگ که از هر ورودی بزرگتر باشد const int N=1000; // حداکثر تعداد راسها int n,m; //به ترتیب تعداد راسها، تعداد یالها int min_e[N]; // کمترین فاصلهی هر راس تا راسهای انتخاب شده int sel_e[N]; // نزدیکترین راس از بین راسهای انتخاب شده bool used[N]; // جزء مجموعه شده یا نه vector <pair<int,int> > ans; // پشتهای که جواب را نگه میدارد vector <pair<int,int> > V[N]; // وزن و شماره راسهایی که به آن یال دارد bool find_mst(){ fill_n(min_e,n,INF); // مقدار دهی اولیه fill_n(sel_e,n,-1); // مقدار دهی اولیه fill_n(used,n,false); // مقدار دهی اولیه min_e[0]=0; // صفر قرار میدهیم تا ابتدا راس صفر(یک) انتخاب شود for (int i=0; i<n; i++) { int v = -1; for (int j=0; j<n; j++) if (!used[j] && (v == -1 || min_e[j] < min_e[v])) v = j; if (min_e[v] == INF) return false; // هیچ یالی از راسهایی انتخابی به بیرون وجود ندارد used[v] = true; if (sel_e[v] != -1) ans.push_back(make_pair(sel_e[v],v)); // به مجموعه v اضافه کردن راس for (int j=0; j<(int)V[v].size(); j++) // به روز کردن مقادیر آرایهها if (V[v][j].second < min_e[V[v][j].first]) { min_e[V[v][j].first] =V[v][j].second; sel_e[V[v][j].first] = v; } } return true; } int main() { cin >> n>>m; for(int i=0; i<m; i++){ // حلقه گرفتن یالها int a,b,w; cin >>a>>b>>w; // به ترتیب ۲ راس و سپس وزن یال V[a-1].push_back(make_pair(b-1,w)); // است n-1 یکی کم میکنیم چون بازهی معتبر ما صفر تا V[b-1].push_back(make_pair(a-1,w)); // است n-1 یکی کم میکنیم چون بازهی معتبر ما صفر تا } if(find_mst()==false) cout << "No MST!\n"; }
در انتها پشتهی ans شامل یالهای درخت پوشای کمینه است.
اینبار نزدیکترین راساز بین راسهای انتخاب شده را در یک دادهساختار آماده ++C به اسم set میریزیم که عملیات حذف و اضافه را در انجام میدهد و پیچیدگی کل را به تغییر میدهد. این پیادهسازی برای زمانی که تعداد یالها خیلی زیاد نباشد سریعتر از پیادهسازی اولیه عمل میکند.
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int INF=2*1000*1000*1000+5; // ثابتی بسیار بزرگ که از هر ورودی بزرگتر باشد const int N=1000; // حداکثر تعداد راسها int n,m; //به ترتیب تعداد راسها، تعداد یالها int min_e[N]; // کمترین فاصلهی هر راس تا راسهای انتخاب شده int sel_e[N]; // نزدیکترین راس از بین راسهای انتخاب شده vector <pair<int,int> > ans; // پشتهای که جواب را نگه میدارد vector <pair<int,int> > V[N]; // وزن و شماره راسهایی که به آن یال دارد bool find_mst(){ fill_n(min_e,n,INF); // مقدار دهی اولیه fill_n(sel_e,n,-1); // مقدار دهی اولیه fill_n(used,n,false); // مقدار دهی اولیه min_e[0]=0; // صفر قرار میدهیم تا ابتدا راس صفر(یک) انتخاب شود set < pair<int,int> > q; // دادهساختاری که فاصله هر راسی که به مجومه انتخاب شده راه دارد را نگه میدارد q.insert (make_pair (0, 0)); for (int i=0; i<n; ++i) { if (q.empty()) return false; int v = q.begin()->second; // کوچکترین عضو یعنی نزدیکترین عضو است set عضو ابتدایی q.erase (q.begin()); if (sel_e[v] != -1) ans.push_back(make_pair(sel_e[v],v)); for (int j=0; j<V[v].size(); j++) { int to = V[v][j].first, cost = V[v][j].second; if (cost < min_e[to]) { q.erase (make_pair (min_e[to], to)); min_e[to] = cost; sel_e[to] = v; q.insert (make_pair (min_e[to], to)); } } } } int main() { cin >> n>>m; for(int i=0; i<m; i++){ // حلقه گرفتن یالها int a,b,w; cin >>a>>b>>w; // به ترتیب ۲ راس و سپس وزن یال V[a-1].push_back(make_pair(b-1,w)); // است n-1 یکی کم میکنیم چون بازهی معتبر ما صفر تا V[b-1].push_back(make_pair(a-1,w)); // است n-1 یکی کم میکنیم چون بازهی معتبر ما صفر تا } if(find_mst()==false) cout << "No MST!\n"; }
الگوریتم کروسکال، الگوریتم برای پیدا کردن درخت پوشای کمینه یک گراف وزندار است. این الگوریتم بر خلاف الگوریتم پریم لزوما اجزایی که در حین اجرا جزء درخت پوشای کمینه تشخیص میدهد همبند نیستند و تنها تضمین میکند که در پایان این شرط برقرار است.
در این الگوریتم، یالها را بر اساس وزن به صورت صعودی مرتب میکنیم. سپس از اولین یال شروع میکنیم و هر یالی که با یالهایی که قبلا انتخاب کردیم دور نمیسازد را انتخاب میکنیم. تا جایی که درخت تشکیل شود.
اما چگونه متوجه شویم که یال جدید با یالهای انتخابی قبلی دور نمیسازد؟ کافیست گرافی که تا کنون از یالهای انتخابی تشکیل شده را به مؤلفههای همبندی تقسیم کنیم و اگر یال جدید هر دو سر آن در یک مؤلفه بود، یال جدید دور ایجاد میکند و در غیر اینصورت اضافه کردن یال جدید مشکلی ندارد.
هر راس متغیری همراه خود دارد که شماره مولفهی همبندیش را نشان میدهد. در ابتدا هر راس، خود یک مؤلفهی همبندیست. و وقتی یک یال بین دو مؤلفه ارتباط ایجاد میکند باید شماره مولفهی همبندی هر دو گروه یکی شود.
در شکل یک مثال برای اجرای این الگوریتم را میبینید. توجه کنید یالهای انتخاب شده تنها در آخر کار یک مجموعهی همبند تشکیل میدهند ولی در الگوریتم پریم همیشه در طول ساخت درخت، مجموعه ما همبند بود.
فرض خلف: فرض کنید درخت پوشای کمینهی وجود دارد که مجموع وزن یالهای ساخته شده توسط آن کمتر از درخت تولید شده توسط الگوریتم کروسکال به نام باشد. کمورنترین یالی در که عضو است ولی عضو نیست را انتخاب کنید. این یال حتما توسط الگوریتم کروسکال انتخاب میشد مگر اینکه با یالهای قبلی دور بسازد و چون تمام یال های سبک تر از این یال در هر دو درخت وجود دارد به معنی آن است که در درخت دور وجود دارد که تناقض است.
مرتب کردن یال ها و بررسی یالها از است که برابر است و هربار اتصال دو مولفه از است که چون اتصال بار انجام میشود پیچیدگی الگوریتم میشود.
همانطور که گفته شده پیچیدگی این پیادهسازی از است.
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int N=1000; // حداکثر تعداد راس ها int n,m; vector <pair <int, pair <int, int> > > g; // پشته شامل یال و وزن انها vector <pair <int, int> > MST; // پشتهای که در آن درخت پوشای کمینه را خواهیم ریخت int tree_ID [N]; // شماره مولفههمبندی هر راس void find_mst(){ sort (g.begin (), g.end ()); MST.clear(); for(int i=0; i<n; i++) tree_ID[i]=i; for(int i=0; i<m; i++){ int f=g[i].second.first; int t=g[i].second.second; if(tree_ID[f]!=tree_ID[t]){ MST.push_back(g[i].second); int old_ID = tree_ID [t], new_ID = tree_ID [f]; for(int j=0; j<n; j++) // یکی کردن شمارهمولفه همبندی ها برای ادغام دو مجموعه if (tree_ID [j] == old_ID) tree_ID [j] = new_ID; } } } int main() { cin >> n>>m; g.clear(); for(int i=0; i<m; i++){ int f,t,w; cin >>f>>t>>w; g.push_back(make_pair (w, make_pair (--f, --t))); /* * است n است ولی بازه معتبر راس ها در ورودی ۱ تا n-۱ بازه معتبر راس ها برای ما صفر تا * پس ما باید قبل از ذخیره یکی از آنها کم کنیم */ } find_mst(); }
درخت پوشای کمینه در پشته MST ریخنه میشود. میتوانید بر حسب نیاز به جای اینکار مجموع وزن یالهای درخت را نگه دارید یا همراه با یالها وزنشان را هم نگه دارید.
میتوان از دادهساختار مجموعههای مجزا برای مولفههای همبندی استفده کرد، در این صورت پیچیدگی الگوریتم به که برابر است کاهش پیدا میکند.
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int N=1000; int n,m; vector <pair <int, pair <int, int>>> g; // پشته شامل یال و وزن انها vector <pair <int, int>> MST; // پشتهای که در آن درخت پوشای کمینه را خواهیم ریخت int parent [N]; int root (int a){ // هر مولفه یک ریشه دارد که نماد آن است if(parent[a] ==a) return a; parent[a]=root(parent[a]); return root(parent[a]); } void Union (int a,int b){ // تابع ادغام parent[root(a)]=root(b); } void find_mst(){ sort (g.begin (), g.end ()); MST.clear(); for(int i=0; i<n; i++) parent[i]=i; for(int i=0; i<m; i++){ int f=g[i].second.first; int t=g[i].second.second; if(root(f)!=root(t)){ MST.push_back(g[i].second); Union(f,t); } } } int main() { cin >> n>>m; g.clear(); for(int i=0; i<m; i++){ int f,t,w; cin >>f>>t>>w; g.push_back(make_pair (w, make_pair (--f, --t))); /* * است n است ولی بازه معتبر راس ها در ورودی ۱ تا n-۱ بازه معتبر راس ها برای ما صفر تا * پس ما باید قبل از ذخیره یکی از آنها کم کنیم */ } find_mst(); }
ماتریسها به طور ساده، همانند آرایههای دوبعدی اند. یک ماتریس یعنی ماتریسی که n سطر و ستون دارد (تعداد سطرها و ستونها میتواند برابر باشد). برای ضرب کردن دو ماتریس، باید تعداد ستونهای اولی با تعداد سطرهای دومی برابر باشد. ضربکردن یک ماتریس در یک ماتریس به اندازهی هزینه (زمان) میبرد و خروجیاش یک ماتریس است.
اما اگر بیش از دو ماتریس داشته باشیم، این که به چه ترتیبی این ضربها را انجام بدهیم (یا چگونه بین ماتریسها پرانتزگذاری کنیم) در هزینهی نهایی تاثیر دارد (دقت کنید که جواب یکسان است، فقط هزینهای که ما صرف میکنیم فرق میکند.)
برای مثال، فرض کنید، سه ماتریس به ابعاد (۲،۳) و (۳،۴) و (۴،۵) داشته باشیم. اگر ابتدا اولی و دومی و سپس نتیجه را با سومی ضرب کنیم، هزینهی ما ۶۴ واحد میشود. اما اگر ابتدا دومی و سومی را ضرب کرده و سپس اولی را با نتیجه ضرب کنید، ۹۰ واحد باید هزینه کنید.
فرض کنید ابعاد ماتریس به شما داده شده است. کمترین هزینهی ضرب کردن این ماتریسها در هم چقدر میشود؟ (دقت کنید که شما نمیتوانید جای ماتریسها را با هم عوض کنید، بلکه فقط میتوانید ترتیب انجام شدن ضربها را انتخاب کنید.)
اگر کمی سوال را بررسی کنید، میتوانید ببینید که تعداد حالتهای مختلف انجام این پرانتزگذاری بسیار زیاد است. در واقع این مقدار برابر
است. اما در مجموع، برای حل بسیاری از این حالتهای مختلف، باید مقادیر تکراری زیادی حساب کنیم.
همانطور که در ابتدا گفته شد، در این مسئله فقط باید مشخص کنیم که پرانتزگذاری بین ماتریسها باید چگونه باشد. و اگر دقت کنید، در صورتی که تمام ماتریسهای
ام تا
ام در یک پرانتز کلی قرار داشته باشند، فرقی نمیکند که چگونه عملیات ضرب را در این تکه انجام دهید، چون در هر صورت، ماتریس پایانی این مجموعه یکسان است (درنتیجه ابعاد آن نیز یکسان است). پس بهتر است این تکه را هم به بهترین روش (با کمترین هزینه) حل کنیم. این مسئله خود شبیه مسئلهی اصلی است با این تفاوت که دنبالهی ماتریسهایمان فقط ماتریسهای
ام تا
ام اند.
با داشتن این ایده در ذهن بیایید به سراغ سوال اصلی برویم. میخواهیم سوال اصلی را به یک سری از زیرسوالهایی که در بالا تعریف کردیم، تبدیل کنیم. با کمی بررسی بیشتر، متوجه میشویم که اگر آخرین ضربی که باید انجام شود را تعیین کنیم، باید چیزی به این شکل باشد:
که تکهی اول و دوم آن را میتوان با زیرمسئلههایی که در بالا تعریف کردیم حل کنیم. پس راه حل به دست آمده را میتوان به زبان ریاضی به شکل زیر تعریف کرد: (فرض کنید اندازهی بعد اول یا همان سطرهای ماتریس اول تا
ام را
تا
و بعد دوم یا ستونهای ماتریس آخر را
بنامیم. میدانیم که باید برای مثال اندازهی بعد دوم ماتریس
ام، برابر بعد اول ماتریس
باشد، وگرنه ضرب ممکن نیست).
مقدار
را برابر کمترین هزینهی ضرب کردن ماتریس
ام تا
ام در نظر بگیرید. پس جواب مسأله برابر مقدار
است.
مقدار اولیه:
برابر صفر است، چون هیچ نیاز به انجام هیچ ضربی نیست.
به روز رسانی: مقدار
با شرط
بسته به اینکه کدام ضرب در انتها انجام شود، به صورت روبرو تعریف میشود:
شبه کد (باید دقت کنید که ابتدا دستههای به طول ۱ را پر کنید، سپس طول ۲ و …) :
for i from 1 to k d[i][i] = 0 for len from 1 to k for i from 1 to k if i+len > k break j = i+len d[i][j] = inf for m from i to j-1 value = d[i][m] + d[m+1][j] + a[i] * a[m+1] * a[j+1] d[i][j] = min( d[i][j] , value )
حافظه مورد نیاز از است. و هر به روز رسانی نیز از است. پس پیچیدگی زمانی از است.
#include <iostream> using namespace std; typedef pair<int, int> pii; const int MAXK = 1000; const int INF = 1<<30; // 2 ^ 30 pii inp[MAXK+10]; int a[MAXK+10]; int d[MAXK+10][MAXK+10]; int main() { ios::sync_with_stdio(false); int k; cin >> k; for(int i=0; i<k; i++) cin >> inp[i].first >> inp[i].second; for(int i=1; i<k; i++){ if( inp[i].first != inp[i-1].second ){ cout << "matrix sizes are not valid" << endl; return 0; } a[i] = inp[i].first; } a[0] = inp[0].first; a[k] = inp[k-1].second; for(int i=0; i<k; i++) d[i][i] = 0; for(int len=1; len<=k; len++) for(int i=0; i<k; i++){ if( i+len >= k ) break; int j = i+len; d[i][j] = INF; for(int m=i; m<j; m++) d[i][j] = min( d[i][j] , d[i][m] + d[m+1][j] + a[i] * a[m+1] * a[j+1] ); } cout << d[0][k-1] << endl; return 0; }
والکر استراسن الگوریتم استراسن را در سال ۱۹۶۹ منتشر کرد. اگرچه الگوریتم او فقط کمی سریع تر از الگوریتمهای استاندارد برای ضرب ماتریس است، اما او اولین کسی بود که اشاره کرد رویکرد استاندارد مطلوب نمیباشد. مقالهٔ او به جستجوی الگوریتمهای سریعتر مانند الگوریتم پیچیده تر Coppersmith–Winograd منتشر شده در سال ۱۹۸۷ پرداخت.
ستون سمت چپ، ضرب ماتریسی ۲x۲ را نشان میدهد. ضرب ماتریسی Naïve به یک ضرب برای هر “۱” از ستون سمت چپ نیاز دارد. هر یک از ستونهای دیگر نشان دهندهٔ یک واحد از ۷ ضرب در الگوریتم میباشند و از مجموع ستونها، ضرب ماتریس کامل در سمت چپ حاصل میشود.
بگذارید A،B دو ماتریس مربعی در یک حلقه R باشند. ما میخواهیم حاصل ماتریس C را به صورت زیر محاسبه کنیم:
اگر ماتریسهای A،B از نوع ۲n x ۲n نباشند، ما ردیفها و ستونهای خالی را با صفر پر میکنیم.
ما A،B و C را به ماتریسهای بلوکی هم سایز تجزیه میکنیم.
ما با این ساختار، تعداد ضربها را کاهش ندادیم. هنوز هم به ۸ ضرب نیاز داریم تا ماتریسهای Ci،j را محاسبه کنیم، زمانی که از ضرب ماتریسی استاندارد استفاده میکنیم نیز به همان تعداد ضرب نیاز داریم.
حالا بخش مهم در اینجا آمدهاست. ما ماتریسهای جدید را به شرح زیر تعریف میکنیم:
که سپس برای بیان Ci،j به صورت Mk مورد استفاده قرار میگیرند. با تعریف مان از Mk میتوانیم یک ضرب ماتریس را حذف کنیم و تعداد ضربها را به ۷ کاهش دهیم (یک ضرب برای هر Mk) و Ci،j را به صورت زیر بیان کنیم:
ما این فرایند تقسیم را n بار تکرار میکنیم تا زمانی که ماتریسهای فرعی به اندازه کافی کوچک شده و قابل تقسیم نباشند. (اجزای حلقه R).
کاربردهای عملی الگوریتم استراسن با روشهای استاندارد ضرب ماتریسی برای ماتریسهای فرعی کوچک تعویض شدند که آن الگویتمها برایشان موثرتر میباشند. نقطه هم گذری خاص که الگوریتمهای استراسن برایشان موثرتر است به کاربرد و سختافزار ویژه بستگی دارد.
ضرب ماتریسی استاندارد تقریباً عملیات محاسباتی ۲n^۳ (که n=۲^n) (جمعها و ضربها) را میپذیرد؛ پیچیدگی مجانبی (O(N۳میباشد. تعداد جمعها و ضربهای مورد نیاز در الگوریتم استراسن را میتوان به صورت زیر محاسبه کرد:
اجازه دهید (f(n تعداد عملیات برای یک ماتریس ۲n × ۲n باشد. آنگاه با عملیات بازگشتی الگوریتم استراسن میبینیم که برای برخی l ثابت که به تعداد جمعهای انجام شده در هر عملیات الگوریتم وابستهاست. یعنی پیچیدگی مجانبی برای ضرب ماتریسها با استفاده از الگوریتم استراسن به شرح زیر است:
هرچند کاهش در تعداد عملیات محاسباتی تا حدودی به بهای کاهش ثبات عددی میباشد.
برای اطلاعات بیشتر:
نمایش ماتریس Z-order
الگوریتم مؤلفه قوی مبتنی بر مسیر (به انگلیسی: Path-based strong component algorithm) در نظریه گراف برای پیدا کردن مولفههای قویا همبند یک گراف جهتدار استفاده میشود. قبل از این الگوریتم، الگوریتم کساراجو و الگوریتم تارژان نیز به همین منظور ارایه شده است.
گراف قویا همبند
به گراف جهتداری گویند که هر جفت راس آن از یکدیگر قابل دسترس باشد. مانند شکل زیر:
مولفه قویا همبند:
مولفه قویا همبند یک گراف به بیشینه زیرمجموعهای از راسها گویند که قویا همبند هستند. یعنی از هر جفت راس در آن مجموعه به یکدیگر مسیر مستقیم وجود دارد. مانند شکل زیر:
فرض کنید گراف G داده شده است. در این الگوریتم رأسهای گراف را از ۱ تا n و مولفههای قویا همبند را با شروع از n+۱ شمارهگذاری میکنیم. همچنین در این الگوریتم گراف H را که در ابتدا همان گراف G میباشد در نظر میگیریم و در این گراف مسیر P را به صورت زیر میسازیم: ابتدا یک رأس دلخواه از گراف را در نظر میگیریم و از این رأس در جهت کاملاً دلخواه حرکت میکنیم تا یکی از دو حالت زیر رخ دهد:
این الگوریتم از الگوریتم جستجوی عمق اول و همچنین دو پشته برای نمایش مسیر P استفاده میکند. پشته S که شامل دنباله رأسهای مسیر P است و پشته B که شامل کرانهای رأسهای منقبض شده است. در این الگوریتم آرایه I نیز وجود دارد که هم اندیس پشته S را و هم شماره مولفه قویا همبند را ذخیره میکند. به عبارت دقیق تر این ارایه به صورت زیر تعریف میشود:
این الگوریتم شامل قسمت اصلی STRONG و قسمت بازگشتی DFS است:
AlgorithmSTRONG(G) empty stacks S and B for v ∈ V do I[v]=۰ c=n for v ∈ V do if I[v]=۰ then DFS(v)
AlgorithmDFS(G) PUSH(v,S);I[v]=TOP(S);PUSH(I[v],B);/*add v to the end of P*/ for edges (v,w) ∈ E do if I[w]=۰ then DFS(w) else /*contract if necessary*/ while I[w]<B[TOP(B)] do POP(B) if I[v]=B[TOP(B) then{/*number vertices of the next strong component*/ POP(B);increase c by 1 while I[v]≤TOP(S) do I[POP(S)]=c}
در قضیه زیر برهان درستی و همچنین پیچیدگی این الگوریتم آمده است:
هنگامی که (STRONG(G متوقف شود هر رأس v ∈ V به مولفه قویا همبندی که توسط [I[v شماره گذاری شده است تعلق دارد. همچنین زمان و حافظه مصرفی (O(m+n میباشند.
برای دیدن الگوریتمهای مرتبط با این الگوریتم میتوانید به الگوریتم کساراجو و الگوریتم مؤلفههای همبند قوی تارجان مراجعه کنید.
الگوریتم چریَن- ملورن/ گابو در نظریه گراف، یک روش با پیچیدگی زمانی خطی برای یافتن مولفههای همبندی قوی در یک گراف جهتدار است. این الگوریتم در سال ۱۹۹۶ توسط جوزف چِریَن و کورت ملورن ارائه شد و در سال ۱۹۹۹ توسط هارولد گابو بهبود یافت.
الگوریتم گابو همانند الگوریتم مولفههای همبند قوی تارجان، تنها یک جستجوی عمق اول در گراف داده شده انجام میدهد، و از یک پشته برای نگهداری گرههایی که
به مولفهای اختصاص داده نشدهاند استفاده میکند، و بعد از اختصاص دادن آخرین گره به مولفه همبندی، این گرهها را به یک مولفهٔ جدید منتقل میکند.
الگوریتم تارجان از یک آرایه با اندیس گرهها شامل اعداد پیشترتیب استفاده میکند، که به ترتیب دیده شدن در جستجوی عمق اول مقداردهی شدهاند.
از آرایهٔ پیشترتیبی برای دانستن اینکه چه زمانی مولفهٔ همبندی جدید ایجا شود استفاده میگردد. الگوریتم گابو به جای استفاده از آرایه، از پشتهٔ دیگری به این منظور استفاده میکند.
الگوریتم گابو با پشتیبانی دو پشتهٔ S و P یک جستجوی عمق اول بر روی گراف داده شدهٔ G انجام میدهد. پشتهٔ S تمامی گرههایی را تاکنون به یک مولفهٔ همبندی قوی اختصاص داده نشدهاند، شامل میشود. گرهها در این پشته به ترتیبی که جستجوی عمق اول به آن میرسد قرار دارند. پشتهٔ P شامل گرههایی است که هنوز اختصاص آنها به مولفههای همبندی قوی متفاوت از هم، تعیین نشده است. همچنین در الگوریتم از یک شمارندهٔ C که تعداد گرههای مشاهده شده تاکنون است، برای محاسبهٔ اعداد پیشترتیب گرهها استفاده میشود.
هنگامی که جستجوی عمق اول به یک گره v میرسد، الگوریتم مراحل زیر را به ترتیب انجام میدهد:
الگوریتم کلی شامل یک حلقه روی گرههای گراف G است، که این جستجوی بازگشتی را برای گرههایی که هنوز عدد پیشترتیب آنها مقدار دهی نشده است، صدا میزند.
الگوریتم دایکسترا راهکاری برای پیدا کردن کموزن مسیر از رأس مشخص آغاز به بقیه رئوس در گراف جهتدار و وزندار (با وزنهای مثبت) میدهد. وزن یک مسیر در گراف وزندار برابر مجموع وزن یالهای آن است. جهتدار نبودن یالها هم مشکلی ایجاد نمیکند و میتوان برای یالهای غیر جهتدار دو یال فرض کرد.
فرض کنید
که در آن رأس
رأس آغاز است و فرض کنید:
و به ازای هر
فرض کنید مجموعهی
برابر رئوسی باشد که تا کنون کم وزنترین مسیر آنها را پیدا کردهایم. این الگوریتم در هر مرحله نزدیکترین رأس به
را که تا کنون به مجموعهی
اضافه نشده را انتخاب میکند (مثلا
) و آن را به مجموعهی
اضافه میکند و فاصلهی دیگر رأسها را با توجه به فاصلهی
بروز میکند. به ازای هر رأس
خارج
:
که در آن
برابر وزن یال بین
و
است. این الگوریتم در هر مرحله رأسی را که کوتاهترین فاصلهی آن تا
پیدا شده است را به
اضافه میکند. زیرا کوتاه ترین مسیر این رأس از یکی از رأسهای
میگذرد که در مراحل قبلی فاصله آنها بدست آمده و دیگر رئوس را بروز کردهاند.
در گراف زیر، روند اجرای این الگوریتم را میتوانید مشاهده کنید:
در صورت منفی بودن یالها فرض اینکه در هر مرحله رأسی که کوتاهترین مسیر آن پیدا شده اضافه میشود زیر سوال میرود. زیرا این رأس میتواند بدون استفاده از رأسهای و یالهای منفی مسیری کوتاهتر به پیدا کند.
به ازای هر رأس باید روند بالا را طی کنیم. یعنی دنبال آن بگردیم و همهی همسایههای آن را پیمایش کنیم. پس پیچیدگی زمانی برنامه از است. هر چند میتوان با استفاده از داده ساختار هرم پیادهسازی از ارائه داد.
function Dijkstra(s): dist[s] = 0 for each vertex v in Graph: // Initializations if v ≠ s dist[v] = infinity end if end for while Size(T) ≠ n : // The main loop u := vertex not in T with min dist[u] add u to T for each neighbor v of u: dist[v] = min(dist[v], dist[u]+w[u][v]) end for end while end function
#include <iostream> using namespace std; const int MAXN = 1000 + 10; const int INF = 1000*1000*1000; int n, m; int adj[MAXN][MAXN]; int w[MAXN][MAXN]; int dist[MAXN]; int mark[MAXN]; void readInput(){ cin >> n >> m; for(int i=0; i<m; ++i){ int v, u, z; cin >> u >> v >> z; adj[u][v] = 1; w[u][v] = z; } } void dijkstra(int s){ dist[s] = 0; for(int i=0; i<n; ++i) if(i!=s) dist[i] = INF; for(int rep=0; rep<n; ++rep) { int u = -1; int du = INF; for(int i=0; i<n; ++i) if(!mark[i] && dist[i] <= du){ u = i; du = dist[i]; } mark[u] = 1; for(int v=0; v<n; ++v) if(adj[u][v]) dist[v] = min(dist[v], dist[u]+w[u][v]); } } void writeOutput(){ for(int i=0; i<n; ++i) cout << dist[i] << " "; cout << endl; } int main(){ readInput(); dijkstra(0); writeOutput(); return 0; }
میتوان یافتن نزدیکترین رأس در هر مرحله را به کمک این داده ساختار انجام داد. در این صورت باید گراف را به صورت لیست مجاورت نگه داریم. که در برای راحتی کار از استفاده می کنیم.
#include <iostream> #include <vector> #include <set> using namespace std; const int MAXN = 1000*100 + 10; const int INF = 1000*1000*1000; typedef pair<int, int> pii; int n, m; vector<int> adj[MAXN]; vector<int> w[MAXN]; set<pii> q; int dist[MAXN]; void readInput(){ cin >> n >> m; for(int i=0; i<m; ++i){ int v, u, z; cin >> u >> v >> z; adj[u].push_back(v); w[u].push_back(z); } } void dijkstra(int s){ dist[s] = 0; for(int i=0; i<n; ++i) if(i!=s) dist[i] = INF; for(int i=0; i<n; ++i) q.insert(pii(dist[i], i)); while(!q.empty()) { set<pii> :: iterator it = q.begin(); int u = it->second; q.erase(it); for(int i=0; i<adj[u].size(); ++i){ int v = adj[u][i]; q.erase(pii(dist[v], v)); dist[v] = min(dist[v], dist[u]+w[u][i]); q.insert(pii(dist[v], v)); } } } void writeOutput(){ for(int i=0; i<n; ++i) cout << dist[i] << " "; cout << endl; } int main(){ readInput(); dijkstra(0); writeOutput(); return 0; }
درخت پوشای کمینه در گرافهایی تعریف میشود که یالهای گراف وزن(ارزش) داشته باشند. به اصلاح در گرافهای وزندار تعریف میشود. زیر مجموعه از یالهای گراف که یک درخت شامل تمام راسها تشکیل میدهد و مجوع وزن یالهایشان کمترین مقدار ممکن بین تمام چنین درختهایی باشد، درخت پوشای کمینه میگویند. در واقع مسئله پیدا کردن یک زیر مجموعه از یالهای گراف با کمترین مجموع وزن است که بین هر دو راس این زیر گراف همچنان مسیر وجود داشته باشد.
نمونهای از الگوریتمها برای پیدا کردن درخت پوشای کمینه، الگوریتم پریم و الگوریتم کروسکال است.
الگوریتم میانگین–خطی درخت پوشای کمینه یک الگوریتم تصادفی برای یافت درخت پوشای کمینه در یک گراف وزن دار بدون راس تنها است. این الگوریتم توسط David Karger، فیلیپ کلین و Robert Tarjan ابداع شده است. این الگوریتم متکی بر شیوه الگوریتم بروکا است که برای یافت درخت پوشای کمینه در زمانی خطی است. این الگوریتم تشکیل شده از الگوریتم تقسیم و حل، الگوریتم حریصانه و الگوریتمهای تصادفی برای رسیدن به زمان خطی مورد نظر. الگوریتمهای قطعی برای یافت درخت پوشای کمینه عبارتند از: الگوریتمهای پریم، کروسکال، بروکا.
کلید این الگوریتم تقسیم تصادفی گراف به دو زیرگراف و تقسیم یالها بین این زیرگراف هاست. الگوریتم به صورت بازگشتی جواب را برای یک زیرگراف مییابد و سپس آن را به کل گراف تعمیم میدهد. روش گرفته شده از [الگوریتم برفکا] برای کاهش حجم گراف در هر مرحله بازگشتی است.
هر تکرار الگوریتم مبتنی بر الگوریتم بروفکا را یک گام در نظر میگیریم.
یک گام بروفکا از اردر m است (تعداد یال ها) زیرا هر یال حداکثر از دو طرفش دیده می شود. بعد از این مرحله تعداد مولفه ها حداکثر n/2 است.
در هر تقسیم یال هایی با خواصی که آن ها را از بودن در درخت پوشای کمینه محروم می کند حذف می شوند. این یال ها را F-heavy می نامیم که این گونه اند: فرض کنید F یک جنگل پوشای کمینه از گراف H است، یک F-heavy یالی است که راس های u و v را به هم دیگر متصل می کند که وزنش از وزن بیشترین یال مسیر u و v در F بیشتر است. هر یالی که F-heavy نیست F-light نام دارد. اگر H یک زیرگراف از G باشد، هیچ F-heavy نمیتواند در درخت کمینه باشد. برای گراف G می توان F-heavy ها را در زمان مورد نظر خطی محاسبه کرد.
درستی الگوریتم را با استقرا روی تعداد رئوس گراف ثابت می کنیم. پایه به وضوح درست است. T را د.پ.ک (درخت پوشای کمینه) ی G در نظر می گیریم. هر یال انتخاب شده در گام بروکا از همه ی یالهال کات آن سوپرراس ها و همچنین دورهای شامل آن کوچکتر است. هر یال F-heavy حذف شده نیز در د.پ.ک نیست زیرا در دوری است که یال کوچکتر از آن وجود دارد یا در هیچ کاتی نیست که کوچکترین یال باشد. همچنین حداکثر تعداد یالهای F-light که به گراف اضافه می شود برابر n-1 است و هر د.پ.ک ای نیز n-1 یال دارد. پس تعداد یالهای درخت محدود به یالهای F-heavy است و یالهای F-heavy نیز n-1 تاست. پس د.پ.ک ساخته می شود.
کارایی در صورتی تضمین می شود که نمونه گیری تصادفی باشد. اثر مرحله نمونه گیری تصادفی، توسط لم زیر که یک حد بر روی تعداد یال های F-light در G در نتیجه محدود کردن اندازه زیر مسئله دوم است توصیف شده.
لم، نشان می دهد اگر H زیرگرافی از G باشد به گونه ای که هر یال G به احتمال p در آن باشد، و F د.پ.ک H باشد، تعداد یالهای F-light در G حداکثر n/p است (n تعداد رئوس G است). برای اثبات لم تعداد یالهای G که به H اضافه شده اند را حساب می کنیم. تعداد یالهای F-light در G بستگی به روش انتخاب H دارد در حالیکه د.پ.ک H برای همه ی انتخاب ها یکی است. برای اثبات انتخاب یالها را از سبک به سنگین در نظر می گیریم.
با نادیده گرفتن کار انجام شده در زیر مسئله بازگشتی مقدار کل کار انجام شده در یک فراخوانی از الگوریتم، کاری خطی تعداد یالهای گراف ورودی است. مرحله ی اول زمان ثابت می خواهد. گام های بروکا می توانند در زمان خطی به نحو گفته شده محاسبه شوند.
https://en.wikipedia.org/wiki/Minimum_spanning_tree#Possible_multiplicity
http://en.wikipedia.org/wiki/Expected_linear_time_MST_algorithm
مسئله انتخاب فعالیتها از مسائل بهینهسازی ریاضی است که میتوان برای آن الگوریتمی به روش حریصانه تولید کرد. برای نمونه فرض کنید n سخنران خود را به عنوان ورودی مسئله اعلام کردهاند. هدف انتخاب بیشترین تعداد سخنران به گونهای است که هیچ دو سخنرانی با هم اشتراک بازهٔ زمانی نداشته باشند.
جهت عدم تداخل در زمان شروع یا پایان میتوان بدون کمشدن از کلیت مسئله فرض کرد که پایان بازه سخنرانیها باز است یعنی به صورت (...} است.
ورودیها:
شبه کد الگوریتم را در زیر میبینیم (فرض بر این است که فعالیتها بر حسب صعودی زمان پایانشان مرتب شدهاند):
Procedure Activity_selector (A, S, F, n) A {1} j 1 for i 2 to n do if Si ≥Fj then A A ∪{i} j i endif repeat end.
تطابق با روش حریصانه: فعالیتی که زودترین زمان پایان را داشته باشد
Select :
با آخرین فعالیت انتخاب شده تداخل زمانی نداشته باشد
Feasibility :
اضافه کردن آن به مجموعه انتخاب شدهها
Union :
تحلیل زمانی :بیشترین زمان در الگوریتم مربوط به مرتب سازی ابتدای کار است و لذا الگوریتم دارای مرتبه زمانی (o(n log n میباشد.
الگوریتم امید ریاضی-بیشینهسازی (EM) یک روش تکرارشونده (iterative) است که به دنبال یافتن برآوردی با بیشترین درست نمایی برای پارامترهای یک توزیع پارامتری است. این الگوریتم روش متداول برای زمانهایی است که برخی از متغیرهای تصادفی پنهان هستند.
فرض کنید که مشاهدات را با نمایش دهیم، متغیرهای پنهان را با و همه ی پارامترهای توزیع را نیز با در این صورت لگاریتم درست نمایی کل داده ها (پنهان و نمایان=مشاهدات) برابر خواهد بود با:
از آنجا که لگاریتم تابع اکیداً صعودی است، می توان لگاریتم درست نمایی کل داده ها را نسبت به
بیشینه کرد. هرچند، آرگومان لگاریتم یک مجموع است و نمی توان به سادگی پاسخ تحلیلی برای
یافت. از این رو، الگوریتم ب-ا ترفندی را برای بیشینه کردن حد پایین لگاریتم درست نمایی بکار می برد.
این حد پایین از نابرابری جنسن بدست می آید. بر اساس نابرابری جنسن که از مجموعه مقعر بودن تابع لگاریتم استفاده می کند برای هر دسته
تایی از
ها و
ها اگر
و
خواهیم داشت:
اکنون را به صورت زیر باز می نویسیم:
با گزینش نابرابری بالا تنگ می شود. این به معنای آن است که نابرابری به برابری تبدیل می شود. این گام الگوریتم همانند بیشینه کردن حدپایین درست نمایی نسبت به است. در نتیجه روش کار الگوریتم امید ریاضی-بیشینه کردن به صورت زیر است:
این دیدگاه نسبت به الگوریتم امید ریاضی-بیشینه کردن متعلق به نیل و هینتون است.
بدین ترتیب در هر گام الگوریتم، حد پایین درست نمایی کل داده ها افزایش می یابد تا آنجا که در یک بیشینه محلی همگرا شود. برای رهایی از بیشینه های محلی، این الگوریتم را معمولاً چندین بار با شرایط آغازین متفاوت اجرا می کنند.
فرض کنید عبارت زیر را دارید :
((a+b)*(c-d))/(e-f)
عبارت بالا یک عبارت infix (میان ترتیب) است زیرا عملگر بین عملوند هایش آمده است ، به طور مثال در عبارت بالا ” – ” بین c و d آمده است.
در اینجا عباراتی را که بررسی می کنیم شامل عملگرهای + , – , / , * و پرانتز است. اولویت این عملگرها به ترتیب زیر است:
و این عملگرها شرکت پذیری از سمت چپ به راست دارند.
درخت عبارت بالا به شکل زیر می شود:
اگر عبارت بالا را به صورت inorder پیمایش کنیم یعنی ابتدا ریشه را ببینیم بعد فرزند چپ و بعد فرزند راست، دقیقا عبارت بالا بدون پرانتز به دست می آید که همان شکل infix است:
In-order: 1-Traverse the left subtree by recursively calling the in-order function 2-Display the data part of root element (or current element) 3-Traverse the right subtree by recursively calling the in-order function inorder(node) if node == null then return inorder(node.left) visit(node) inorder(node.right)
a + b * c – d / e – f
prefix (پیش ترتیب) یک عبارت محاسباتی یعنی عملگر قبل از عملوند هایش بیاید،برای اینکه شکل prefix عبارت بالا را به دست بیاوریم باید درخت بالا به صورت preorder پیمایش کنیم یعنی ابتدا ریشه را ببینیم بعد فرزند چپ و بعد فرزند راست که عبارت زیر به دست می آید:
Pre-order: 1-Display the data part of root element (or current element) 2-Traverse the left subtree by recursively calling the pre-order function. 3-Traverse the right subtree by recursively calling the pre-order function. preorder(node) if node == null then return visit(node) preorder(node.left) preorder(node.right)
/ * + a b – c d – e f
postfix (پس ترتیب) یک عبارت محاسباتی یعنی عملگر بعد از عملوندهایش بیاید، که می توان شکل postfix یک عبارت محاسباتی را از روی درختش با استفاده از پیمایش postorder با کمی تغییر به دست آورد.
در پیمایش postorder ابتدا فرزند چپ و بعد فرزند راست و بعد ریشه را می بینیم ولی باید در این الگوریتم کمی تغییر ایجاد کنیم، و آن این است که به جای اینکه ابتدا فززند چپ و بعد راست و بعد ریشه را ببینیم ، ایتدا فرزند راست و بعد فرزند چپ و بعد ریشه را ببینیم که عبارت زیر به دست می آید:
Post-order: Traverse the right subtree by recursively calling the post-order function. Traverse the left subtree by recursively calling the post-order function. Display the data part of root element (or current element). postorder(node) if node == null then return postorder(node.right) postorder(node.left) visit(node)
f e – d c – b a + * /
روش هایی که در بالا گفتیم به صورت بازگشتی از روی درخت عبارت محاسباتی شکل های infix ، postfix و prefix یک عبارت محاسباتی را حساب می کند. ولی می توان به صورت مستقیم نمایش prefix و postfix یک عبارت محاسباتی را از روی نمایش infix آن عبارت با استفاده از یک stack (پشته) به دست آورد.
ابتدا از سمت راست شروع به خواندن تک تک اجزای عبارت محاسباتی می کنیم ،که به صورت infix است و به سمت چپ حرکت می کنیم.
یک استک را برای نگه داری عملگرهای در نظر می گیریم. اگر جزیی را که از عبارت خوانده ایم یک عدد یا متغییر است (عملوند) آن را به صورت مستقیم در خروجی چاپ می کنیم. اگر عملگر یا پرانتز بود کار های زیر را انجام می دهیم:
اگر تک تک اجزای عبارت را از سمت راست به چپ را دیدیم ولی استک خالی نشده بود تمام محتویات استک را تا زمانی که خالی شود pop می کنیم و عملگرها را در خروجی نمایش می دهیم.
مراحل بالا را برای عبارت داده شده را در زیر می بینید:
همین طور که نمایش postfix عبارت ابتدای متن
f e – d c – b a + * /
می شود که برابر با postfix است که با پیمایش درخت به دست آوردیم.
دقیقا مانند مرحله ی قبل عمل می کنیم و نمایش postfix را به دست می آوریم سپس خروجی را برعکس می کنیم و نمایش prefix به دست می آید:
f e – d c – b a + * / –> / * + a b – c d – e f
الگوریتم DES در دههٔ ۷۰ میلادی در آمریکا بهعنوان یک استاندارد کدگذاری مطرح شد. این الگوریتم اینگونه عمل میکند که رشتهای از متن اصلی با طول ثابت را به عنوان ورودی میگیرد و پس از انجام یک سری اعمال پیچیده روی آن خروجی را که طولی برابر طول ورودی دارد تولید میکند. DES هم چنین از یک کلید برای ایجاد رمز استفاده میکند و تنها کسانی قادر به رمزگشایی خواهند بود که مقدار کلید را میدانند. اگرچه تحلیلهایی که دربارهٔ DES انجام شدهاست از هر روش رمز قطعهای دیگری بیشتر است ولی عملیترین حمله علیه این الگوریتم جستجوی جامع فضای کلید است. سه حمله تئوریکی برای این الگوریتم وجود دارند که زمان کمتری نسبت به جستجوی جامع فضای کلید نیاز دارند ولی این روشها در عمل امکانپذیر نیستند.
با شکسته شدن الگوریتمDES این استاندارد در سال ۱۹۹۸ تمدید نشد و در سال ۲۰۰۱، الگوریتم AES به عنوان استاندارد جایگزین آن تصویب شد. این الگوریتم مانند DES یک الگوریتم رمزقطعهای است ولی بر خلاف DES از ساختار فیستل استفاده نمیکند. تا سال ۲۰۰۶ تنها حمله مؤثر علیه الگوریتمAES حمله side channel بودهاست. در ژوئن سال ۲۰۰۳ دولت آمریکا اعلام کرد که ازAES میتوان برای حفاظت از اطلاعات ردهبندی شده و سری نیز استفاده کرد. برای اطلاعات فوق سری و محرمانه باید از کلیدهایی با طول ۱۹۲ یا ۲۵۶ بیت استفاده کرد.
در سال ۱۹۷۲ مؤسسه بینالمللی استاندارد و فناوری آمریکااعلام کرد که به یک الگوریتم برای حفاظت از اطلاعات غیر ردهبندی شده خود نیاز دارد. این الگوریتم میبایست ارزان، قابل دسترس وبسیار مطمئن میبود. در سال ۱۹۷۳،NIST فراخوانی برای چنین الگوریتمی اعلام نمود ولی هیچیک از الگوریتمهایی که در پاسخ به این فراخوان ارائه شدند شرایط لازم را نداشتند. دومین فراخوان در سال ۱۹۷۴ مطرح شد در این زمان IBM الگوریتم خود را مطرح نمود که به نظر میرسید میتواند که نیازهای NISTرا بر طرف کند. این الگوریتم به عنوان یک استاندارد فدرال در سال ۱۹۷۶ تصویب شد ودر سال ۱۹۷۷ منتشر شد. با امکانپذیر شدن حمله جستجوی جامع فضای کلید برای این الگوریتم سازمان ملی استاندارد و فناوری آمریکا در آغاز سال ۱۹۹۷اعلام کرد که برای تدوین استاندارد پیشرفته رمزنگاری تلاشی را آغازکردهاست در سپتامبر همان سال این سازمان به طور رسمی فراخوانی را برای ارائه الگوریتمهای رمزنگاری اعلام نمود.
در کنفرانس اول، AES-1، ۱۵ الگوریتم کاندیدا انتخاب شدند، NIST از تمام دانشمندان و موسسههای علمی خواست که نظ رات خود را در مورد این الگوریتمها ارائه دهند. هم چنین NIST با کمک جامعه بینالمللی رمزنگاری و تشکیل کمیتههایی اقدام به بررسی قابلیتها و تواناییهای الگوریتمهای ارائه شده نمود در آگوست سال بعد در سمینار دوم، AES-2، ۵الگوریتم انتخاب و برای رقابت نهایی معرفی شدند این الگوریتمها عبارتند از Rijndael - RC6 - MARS - Twofish - Serpent
آخرین نظرات و انتقادات تا تاریخ ۱۵ مه ۱۹۹۹ جمعآوری شد و بالاخره در سمینار AES-3 پس از بررسی گزارش کمیتههای بررسی کننده، الگوریتم Rijndael به عنوان الگوریتم استاندارد پذیرفته شد.
در DES طول قطعات ۶۴ بیت است. کلید نیز شامل ۶۴ بیت است ولی در عمل تنها از ۵۶ بیت آن استفاده میشود و از ۸ بیت دیگر فقط برای چک کردن parity استفاده میشود. الگوریتم شامل ۱۶ مرحله مشابهاست که هر مرحله یک دور ۴نامیده میشود. متنی که قرار است رمزگذاری شود ابتدا در معرض یک جایگشت اولیه (IP)قرار میگیرد. سپس یک سری اعمال پیچیده وابسته به کلید روی آن انجام میشود و در نهایت در معرض یک جایگشت نهایی (FP) قرار میگیرد. IP,FP معکوس هم هستند FP عملی که توسط IP انجام شدهاست را خنثی میکند؛ بنابراین از جنبه رمزنگاری اهمیت چندانی ندارند و برای تسهیل نمودن بار کردن قطعات داده در سختافزارهای دهه ۱۹۷۰ استفاده شدند ولی اجرای DES در نرمافزار را کند کردند. قبل از دور اصلی، داده به دو بخش ۳۲ بیتی تقسیم میشودکه این دو نیمه به طور متناوب مورد پردازش قرار میگیرند این تقاطع به عنوان شکل فیستل شناخته میشود. ساختار فیستل تضمین میکند که رمزگذاری و رمزگشایی دو رویه کاملاً مشابه هم هستند و تنها تفاوت آنها این است که زیر کلیدها در زمان رمزگشایی در جهت معکوس رمزگذاری به کار برده میشوند؛ و بقیه الگوریتم درهر دو یکسان است که این امر پیادهسازی رابه خصوص در سختافزاربسیار آسان میکند و دیگر نیازی به الگوریتمهای متفاوت برای رمزگذاری و رمزگشایی نیست. تابعی که خروجی IP را میگیرد وپس از شانزده مرحله ورودی FP را فراهم میکند تابع F نامیده میشود. این تابع یک ورودی ۳۲ بیتی و یک ورودی ۴۸ بیتی دارد و یک خروجی ۳۲ بیتی تولید میکند. بلاک ورودی شامل ۳۲ بیت که نیمه سمت چپ را تشکیل میدهد و با L نشان داده میشود و به دنبال آن ۳۲ بیت دیگر که نیمه راست را تشکیل میدهد و با R نمایش داده میشود است پس کل بلاک را میتوان به صورت LR نمایش داد.
اگر K یک بلاک ۴۸ بیتی باشد که از کلید اصلی ۶۴ بیتی مشتق شدهاست و خروجی یک دور با ورودی LR و خروجی L1R1 به صورت زیر تعریف میشود. L1=R R1=L XOR F(R,K) اگر KS تابعی باشد که کلید ۶۴ بیتی KEY و یک عدد صحیح در محدوده ۱ تا ۱۶ را به عنوان ورود ی میگیرد و کلید ۴۸ بیتی Kn را به عنوان خروجی تولید میکند به طوری که بیتهای Kn از تغییر محل بیتهای KEY حاصل شدهاند داریم: Kn= KS (n.KEY)
KS را تابع key schedule مینامند؛ بنابراین در حالت کلی داریم: Ln=Rn-1 Rn=Ln-1 XOR f(Rn-1,Kn) برای رمزگشایی نیز داریم: R=L1 L=R1 XOR f(L1,K)
در نتیجه رمزگشایی با همان الگوریتمی که برای رمزگذاری استفاده شد انجام میشودو در هر مرحله همان K بیتی که به عنوان کلید برای رمزگذاری استفاده شده بود مورد استفاده قرار میگیرد بنابراین میتوان نوشت: Rn-1=Ln Ln-1=Rn XOR f(Ln,Kn)
برای محاسبات رمزگشایی R16L16 ورودی IP و R0L0 ورودی FP است. کلید شانزدهم در مرحله اول، کلید پانزدهم در مرحله دوم و به همین ترتیب کلید اول در مرحله شانزدهم مورد استفاده قرار میگیرد.
بسط: در این مرحله با استفاده از یک جایگشت انبساطی ۳۲ بیت به ۴۸ بیت گسترش داده میشود.
ترکیب کلید: در این مرحله حاصل مرحله قبل با یک زیر کلید XOR میشود. شش کلید ۴۸ بیتی با استفاده از الگوریتم key schedule از کلید اصلی تولید میشود.
جایگزینی: بعد از ترکیب کلید هر قطعه داده به هشت بخش ۶ بیتی تقسیم میشود) قبل از پردازش توسط جعبههای جایگزینی (هر کدام از s-boxها ورودی ۶ بیتی خود را با استفاده از یک تبدیل غیر خطی که به شکل یک جدول look up است به یک خروجی ۴ بیتی تبدیل میکند S-boxها قلب DES هستند و بدون آنها رمز خطی خواهد بود و در نتیجه قابل شکستن خواهد شد.
جایگشت: در نهایت ۳۲ بیت خروجی S-boxها با استفاده از یک جایگشت ثابت مجدداً سازماندهی میشود (P-box).
پروتکل TLS به برنامههای Client/Server اجازه میدهد که در شبکه از طریقی که از eavesdropping(شنود)، message forgery(جعل پیام) جلوگیری میکند با یکدیگر ارتباط برقرار کنند. authentication TLS(احراز هویت) و communications confidentiality(ارتباط مطمئن) در اینترنت را از طریق استفاده از cryptography(رمز نگاری) فراهم میکند.
Client باید برای Server مشخص کند که آیا می خواهد یک اتصال TLS داشته باشد با نه. دو راه برای رسیدن به این هدف وجود دارد: یک راه این است که از شماره پورت متفاوتی برای اتصال TLS استفاده شود(برای مثال پورت 443 پروتکل امن انتقال ابرمتن) و دیگر اینکه اختصاص یک پورت مشخص از طریق سرور به کلاینت، که کلاینت آن را درخواست کرده باشد با استفاده از یک مکانیسم پروتکل خاص (برای مثال STARTTLS).
زمانی که کلاینت و سرور تصمیم گرفتند از اتصال TLS استفاده کنند، به مذاکره با استفاده از روش handshaking می پردازند. سپس سرور و کلاینت بر روی پارامترهای مختلفی که برای ایجاد امنیت اتصال استفاده می شود به توافق می رسند:
اکنون SSL handshake کامل است و ارتباط شروع می شود. کلاینت و سرور از کلید جلسه برای رمزگذاری و رمزگشایی اطلاعاتی که برای هم می فرستند استفاده می کنند. اگر یکی از قدم های بالا با شکست مواجه شود TLS دچار شکست شده و ارتباط برقرار نمیشود. در قدم سوم مشتری باید گواهی سرور را به درستی چک کند تا باعث بروز مشکل نشود.
الگوریتم RSA پس از آنکه ران ریوست (Ron Rivest)، آدام شامیر (Adam Shamir) و لن ادلمن (Len Adleman) در سال ۱۹۷۷ آنرا بدست آوردند به این نام مشهور شد، هرچند تکنیک های اولیه آن پیشتر در سال ۱۹۷۳ توسط فردی بنام کلیفورد کوکس (Clifford Cocks) بدست آمده بود اما تا سال ۱۹۷۷ اولا” الگورتیم کاملا” محرمانه بود و ثانیا” به سادگی آنچه در زیر بیان خواهیم کرد نبود.
بطور خلاصه روش کار برای تهیه کلیدها به شرح زیر است :
حال پس از طی این مراحل شما می توانید از e و n بعنوان کلید عمومی و از d و n بعنوان کلید اختصاصی استفاده کنید.
برای کد کردن اطلاعات کافی است عدد منتصب به هر کاراکتر - مثلا” ASCII - را که در اینجا M می نامیم در فرمول زیر قرار دهید و بجای ارسال آن عدد C = Me mod n را ارسال کنید. در واقع دراینجا شما توانسته اید با کمک کلید عمومی، کاراکتر M را به C تبدیل کنید.
حال گیرنده برای آشکار سازی کافی است عدد دریافتی یعنی C را با استفاده از کلید خصوصی به M تبدیل کند. برای اینکار کافی است از این فرمول استفاده کنید : M = Cd mod n ، بنابراین شما با دریافت کاراکتر کد شده C و در دست داشتن کلید خصوصی توانسته اید کاراکتر اصلی را مشخص نمایید.
با توجه به روشی که در مطلب قبل ارایه کردیم در اینجا بعنوان نمونه مثالی از نحوه تعریف کلید های عمومی و خصوصی خواهیم آورد. اما برای سادگی محاسبات از اختیار کردن اعداد بزرگ دوری خواهیم کرد و توجه شما را به این نکته جلب می کنیم که هرچقدر اعداد اولیه بزرگتر باشند احتمال شکستن رمز در مدت زمان محدود ناچیزتر می شود.
بنابراین هم اکنون شما یک جدول تبدیل کد دارید که با کمک کلید عمومی اعداد صفر تا ۳۲ را به اعدادی کد شده و در همین رنج تبدیل کرده اید. اما اگر دقت کنید تعدادی از اعداد دقیقا” به همان عدد خود تبدیل شده اند که به اینها unconcealed یا مخفی نشده گفته می شود. اولآ باید بدانیم که ۰ و ۱ همواره به همین اعداد تبدیل می شوند و مطلب دیگر اینکه اگر رنج دو عدد اول ابتدایی را بزرگ در نظر بگیریم دیگر مشکلی پیش نخواهد آمد.
حال کافی است فرض کنیم A=۲ ، B=۳ ، C=۴ و … Z=۲۷ و جملات مربوطه را کد نماییم. دقت کنید که معمولا” از ۰ و ۱ برای کدینگ استفاده نمی شود.
اگر دنباله مقادير پذيرفته شده توسط هر ويژگي از توزيع احتمال معيني پيروي كند،آن دنباله مقادير تصادفي است.تصميمات مبتني بر مقادير موجود در دنباله تصادفي شمرده مي شودو در نتيجه شبيه سازي برخوردار از رفتار تصادفي خواهد بود.
هر دنباله از اعداد تصادفي مانند R 1 ,R 2 ,….بايد دو خاصيت آماري مهم داشته باشدكه عبارتند از
مقصود از شبه تاكيد بر اين مطلب است كه استفاده از يك روش قطعي و مشخص براي اعداد تصادفي،امكان بالقوه تصادفي بودن واقعي را از بين مي برد.پس اعداد توليد شده واقعا" تصادفي نيست.هدف هر روش اينست كه به نحوي اعداد تصادفي در محدوده(1و0) توليد كند تا دو خاصيت استقلال و توزيع يكنواخت را داشته باشد.
نمونه هاي خطاهايي كه بدليل خواص استقلال و توزيع احتمال يكنواخت به هنگام توليد اعداد شبه تصادفي ايجاد مي شوند عبارتند از:
در بررسي هاي شبيه سازي،اعداد تصادفي بوسيله يك كامپيوتر رقمي توليد مي شود.
اين روش با يك عدد اوليه به نام هسته شروع بكار مي كند.روال كار چنين است كه هسته را مربع مي كنند و ارقام مياني آن را تعيين و پس از نوشتن صفر-مميز در سمت چپ ارقام مياني،اولين عدد تصادفي را توليد مي كنند.به منظور توليد عدد تصادفي دوم بايد ارقام مياني در مرحله قبل را مربع،ارقام مياني حاصل را تعيين وبه اعداد اعشاري تبديل كردو... اگر هسته n رقمي باشد،مربع آنرا 2n-1 تا 2n رقمي خواهد بود و اگر n زوج باشد،باحذف ارقام از هر سمت مربع آن مي توان ارقام مياني را تعيين كرد. بزرگترين عدد n رقمي،بزرگترين طول دنباله را مشخص مي كند.
یکی از مسائلی که بسیاری از مواقع با آن روبرو میشویم، مسئلهی به توان رساندن یک عدد یا یک ماتریس است. با روش تقسیم و حل میتوان یک عدد یا مشابه آن یک ماتریس را به توان رساند و زمان این کار را کم کرد.
فرض کنید عدد یا ماتریس به شما داده شده است. همچنین عدد نیز در اختیار شما قرار گرفتهاست. شما میخواهید حاصل را محاسبه کنید.
میخواهیم از روش تقسیم و حل برای این سؤال استفاده کنیم. فرض کنید
زوج باشد و مقدار
را در اختیار شما گذاشته باشیم. شما با یک ضرب ساده به صورت
مقدار
را میتوانید محاسبه کنید. حال اگر
فرد باشد و داشته باشیم:
اگر مقدار
را محاسبه کنید،
به دست میآید. بنابراین کافی است
را نصف کرده، سؤال را حل کنید و از روی جواب این زیرمسئله، مقدار جواب نهایی مورد نظر خود را محاسبه کنید.
فرض کنید ضربها را در زمان انجام بدهید، در این صورت میتوان کل الگوریتم را در زمان به پایان رسانید.
#include <iostream> using namespace std; int power(int A, int B) // you can imagine that A is matrix, but don't forget to write a function to multiply! { if(B == 0) return 1; int x = power(A, B / 2); x *= x; if(B % 2 == 1) x *= A; return x; } int main() { int A, B; cout << power(A, B) << endl; }
توان رساندن ماتریسها میتواند کاربردهای زیادی داشته باشد. اما ما یکی از کاربردهای معروف را بیان میکنیم:
اگر شما ماتریس مجاورت یک گراف را به توان
برسانید، در درایهی
این ماتریس، تعداد گشتها از
به
به طول
نوشته شده است. برای اثبات این قضیه میتوانید از استقرا کمک بگیرید.
کاربرد مسئلهی بالا در برخی روابط بازگشتی و برنامهنویسی پویا است. میتوان برخی از مسائلی را که راه حل پویا دارند، به گراف تبدیل کرد، سپس با به توان رساندن ماتریس مجاورت گراف متناظر با مسئله، میتوان جواب سؤال را در زمان بهتری پیدا کرد. برای مثال در صورتی که ماتریسی که درایههای آن به صورت زیر هستند:
را به توان برسانید، میتوانید امین عدد فیبوناچی را به دست آورید.
مرکز گراف (graph center)، رأسی است که شعاع را در گراف موجب میشود. یا بطور سادهتر میتوان گفت رأسی که بیشترین فاصلهاش کمینه باشد. مسئله پیدا کردن این رأس در گراف داده شده است. برای مثال نقاط قرمز شکل روبرو مرکزهای گراف هستند.
این مسئله برای گرافهای مختلف، الگوریتمهای مختلفی دارد زیرا هرچه گراف کلیتر شود، حل مشکلتر و الگوریتم ها پیچیدهتر میشوند. در اینجا راهحلی برای گرافهای بدون وزن و بدون جهت ارائه میدهیم.
الگوریتم بسیار مانند پیدا کردن قطر است چراکه قطر حداکثر خروج ازمرکز و شعاع حداقل خروج از مرکز است. پس کافیست از هر رأسی بزنیم. طبق پیمایش اولسطح به ازای هر پیمایش، آخرین رأسی که علامتزده میشود، دورترین رأس از ریشه است و فاصلهاش برابر سطحی که قرار گرفته است میباشد. بنابراین مرکز گراف، ریشهای است که بین همه بیشترین فاصلهها، کمترین مقدار را تولید کرده است.
فرض کنید تعداد رأسها و تعداد یالها باشد. چون به ازای هر رأسی یکبار میزنیم و چون مرتبه اجرایی هر برابر است، پس مرتبه اجرایی الگوریتم از مرتبه است.
توجه کنید که عمل کمینه پیدا کردن نیز از است، اما چون مرتبه اش کمتر است پس مرتبه کل را زیاد نمیکند و همین باقی میماند.
#include <queue> #include <vector> using namespace std; const int MAXN = 100 * 1000 + 10; bool mark[MAXN]; vector<int> adj[MAXN]; // لیست مجاورت رأسها int n; // تعداد رأسها int bfs(int v) { int far = 0; queue<int> q; // صفی که رأسها در آن قرار میگیریند queue<int> depth; // صفی که ارتفاع رأسهای متناظر در صف بالا قرار دارند mark[v] = 1; q.push(v); depth.push(0); // ارتفاع ریشه از خودش صفر است while(q.size()) { v = q.front(); far = depth.front(); q.pop(); depth.pop(); for(unsigned int i = 0; i < adj[v].size(); i++) { int u = adj[v][i]; if (mark[u] == 1) continue; mark[u] = 1; q.push(u); depth.push(far + 1); // ارتفاع همسایه یک واحد بیشتر از ارتفاع این رأس است } } return far; } int find_center() { int dist = 1<<30; // تقریبا بینهایت برای اینکه در کمینه تاثیری نگذارد و شعاع را نگه میدارد int v = 0; // مرکز گراف را در این متغیر نگه میداریم for (int i = 0; i < n; i++) { int tmp = bfs(i); if(tmp < dist) { dist = tmp; v = i; } } return v; }
الگوریتم بلمن-فورد راهکاری برای پیدا کردن کموزن مسیر از رأس مشخص آغاز به بقیه رئوس در گراف جهتدار و وزندار میدهد. وزن یک مسیر در گراف وزندار برابر مجموع وزن یالهای آن است. جهتدار نبودن یالها هم مشکلی ایجاد نمیکند و میتوان برای یالهای غیر جهتدار دو یال فرض کرد.
یکی از مزیت های این الگوریتم نسبت به الگوریتم دایکسترا توانایی اجرا شدن روی گرافها با یال منفی است.
این الگوریتم علاوه بر پیدا کردن کوتاهترین مسیر به پیدا کردن دور منفی در گرافها کمک میکند. بنابراین استفادههای بیشتری از الگوریتمهای مشابه میتواند داشته باشد.
ابتدا روش کار این الگوریتم را بررسی میکنیم و سپس درستی آن را بررسی میکنیم. فرض کنید
و به ازای هر
این الگوریتم به تعداد یکی کمتر از رأس ها در هر مرحله روی همهی یالها عملیات زیر را انجام میدهد:
در واقع فاصله دو سر یال را با توجه به وزن آن تصحیح میکند. به این عملیات در اصطلاح
کردن یال ها می گویند.
درستی این الگوریتم را به استقرا ثابت میکنیم. فرض کنید این الگوریتم تا -امین بار اجرا شدن کوتاهترین فاصله تمامی رئوسی که کموزنترین مسیر آنها حداکثر یال دارد را پیدا کند. پایه استقرا: در مرحله صفر-ام رأس آغاز فاصلهاش صفر است؛ پس درست است. گام استقرا: هر یک از رأسهایی که کوتاهترین مسیرشان حداکثر یال دارد آخرین یالشان حتما به یک رأسی است که در مرحله قبلی فاصلهشان پیدا شده است(در واقع حداکثر از یال استفاده میکنند). پس بعد از کردن یال ها برای بار -ام جواب تمامی رأسهایی که کوتاهترین مسیرشان یالی است را پیدا کردهایم. پس درستی الگوریتم ثابت میشود.
در گراف زیر، روند اجرای این الگوریتم را میتوانید مشاهده کنید:
به ازای هر رأس باید روند بالا را طی کنیم. یعنی دنبال آن بگردیم و همهی همسایههای آن را پیمایش کنیم. پس پیچیدگی زمانی برنامه از است. هر چند میتوان با استفاده از داده ساختار هرم پیادهسازی از ارائه داد.
شبه کد:
function BellmanFord(list vertices, list edges, vertex s) // Step 1: initialize graph for each vertex v in vertices: if v ≠ s then dist[v] = INF // Step 2: relax edges repeatedly for i from 1 to size(vertices)-1: for each edge (u, v) with weight w in edges: if dist[u] + w < dist[v]: dist[v] = dist[u] + w
#include <iostream> #include <vector> using namespace std; typedef pair<int, int> pii; const int MAXN = 1000*100 + 10; const int INF = 1000*1000*1000; int dist[MAXN]; vector<pii> edge; vector<int> w; int n, m; void readInput(){ cin >> n >> m; for(int i=0; i<m; ++i){ int u, v, z; cin >> u >> v >> z; edge.push_back(pii(u, v)); w.push_back(z); } } void bellmanFord(int s) { dist[s] = 0; for(int i=0; i<n; ++i) if(i!=s) dist[i] = INF; for(int i=0; i<n-1; ++i) for(int j=0; j<(int)edge.size(); ++j){ int u = edge[j].first; int v = edge[j].second; dist[v] = min(dist[v], dist[u]+w[j]); } } void writeOutput() { for(int i=0; i<n; ++i) cout << dist[i] << " "; cout << endl; } int main(){ readInput(); bellmanFord(0); writeOutput(); return 0; }
کمر گراف، طول کوتاهترین دور در گراف است. برای مثال کمر گرافی که تنها شامل مثلث است، سه است؛ کمر گرافی که یک مربع با قطرهایش است نیز ۳ است. توجه کنید اگر گراف هیچ دوری نداشته باشد، کمر گراف را ۰ فرض میکنیم.
برای راحتی، مسئله را میتوانیم به چند حالت تقسیم کنیم:
به ازای هر رأس مانند جستوجوی سطحاول میزنیم.
در هر پیمایش، رأسهای دیده شده را علامتگذاری میکنیم. اولین باری که به رأسی علامتخورده رسیدیم، یعنی قبلا با یک مسیر به آن رسیده بودیم و الان با یک مسیر دیگر رسیده ایم، پس حتما یک دور پیدا شده است. اندازه این دور برابر جمع طول این دو مسیر است. از آنجایی که طول مسیر اول و دوم برابر ارتفاع (سطح) این رأس و یا یک واحد بیشتر است، پس این طول به راحتی قابل محاسبه است. این مقدار را ذخیره و پیمایش بعدی را آغاز میکنیم.
بین همه دورهای پیدا شده، کمترین را به عنوان کمر گزارش میکنیم.
فرض کنید کمر گراف شامل رأس باشد. پیمایشی که ریشه آن بوده را در نظر بگیرید. اولین دوری که پیدا میکنیم، طولش برابر کمر است؛ زیرا قطعا به رأس روبرویی (رأسی که فاصله اش از دو طرف برابر باشد) از دو مسیر مختلف میرسیم و چون همه مسیرها را بطور موازی جلو میبرد، پس این دور را در ارتفاع نصف کمر (اگر فرد بود، سقف میگیریم) پیدا میکنیم یا دوری دیگر را که طول برابری دارد. پس به ازای هر رأسی که عضو کمر باشد، طول دور پیدا شده با پیمایش از آن رأس، برابر کمر است.
توجه کنید که اگر در مرحلهای طول دور کمینه پیدا شده برابر باشد، آنگاه اگر در پیمایشمان به ارتفاع بیش از برسیم پس حتما دوری که پیدا میشود بیشتر از میشود بنابراین کمر از این رأس نمیگذرد و پیمایش را متوقف کرده و از رأس بعدی آغاز میکنیم.
فرض کنید تعداد رأسها و تعداد یالها است. چون از هر رأسی یکبار جستوجوی سطحاول میزنیم و هزینه هر جستوجو برابر است، پس هزینه کل از است. توجه کنید که پیدا کردن کمینه از است و تأثیری در پیچیدگی کل ندارد.
#include <queue> #include <vector> #include <cstring> #include <iostream> using namespace std; const int MAXN = 100 * 1000 + 10; const int INF = 1<<30; // بینهایت bool mark[MAXN]; vector<int> adj[MAXN]; // لیست مجاورت رأسها int depth[MAXN]; // ارتفاع هر رأس int parent[MAXN]; // پدر هر رأس int n; // تعداد رأس ها توجه کنید شماره رأسها از ۰ شروع میشود int m; // تعداد یالها int bfs(int v, int max_depth) { queue<int> q; // صفی که رأسها در آن قرار میگیریند memset(depth, -1, sizeof(depth)); memset(mark, 0, sizeof(mark)); mark[v] = 1; depth[v] = 0; q.push(v); while(q.size()) { v = q.front(); q.pop(); if (depth[v] == max_depth) // دوری که در آینده پیدا میشود نمیتواند کمینه باشد return INF; for(unsigned int i = 0; i < adj[v].size(); i++) { int u = adj[v][i]; if (mark[u] == 1 && u != parent[v]) // پس دور پیدا شده است return (depth[v]+1) + depth[u]; // ارتفاع قبلی پیدا شده برای این رأس + ارتفاع الان پیدا شده mark[u] = 1; q.push(u); parent[u] = v; depth[u] = depth[v] + 1; // ارتفاع همسایه یک واحد بیشتر از ارتفاع این رأس است } } return INF; } int find_min_cycle() { int dist = INF; // تقریبا بینهایت برای اینکه در کمینه تاثیری نگذارد و دور را نگه میدارد for (int i = 0; i < n; i++) dist = min(dist, bfs(i, dist/2)); return dist; } void input() { cin >> n >> m; for (int i = 0; i < m; i++) { int v, u; cin >> v >> u; adj[--v].push_back(--u); adj[u].push_back(v); } } int main() { input(); cout << find_min_cycle() << endl; }
فاصله بین دو رأس را طول کوتاهترین مسیر ممکن بین آن دو تعریف میکنیم. حال به ازای هر رأسی اگر اسم بیشترین فاصله آن تا سایر رأسها را خروج از مرکز آن رأس بگیریم، قطر گراف برابر بیشترین خروج از مرکز است.
توجه کنید که قطر همیشه برابر طولانیترین مسیر نیست، برای مثال در گراف متشکل از یک دور، طولانیترین مسیر برابر
است، ولی قطر
است.
این مسئله در حالت کلی کمی سخت است و حلهای گوناگونی برای آن وجود دارد، بدین منظور این مسئله را به چند حالت تقسیم میکنیم و تنها بخش اول را در اینجا شرح میدهیم.
به دنبال یافتن قطر گراف بدونجهت و بیوزن
هستیم. برای حل این مسئله میتوانیم ابتدا خروج از مرکز هر رأسی را حساب کنیم، سپس بیشترین مقدار را به عنوان خروجی برگردانیم.
برای محاسبه خروج از مرکز هر رأسی کافیست، از آن رأس جستوجوی سطحاول بزنیم و فاصله دورترین رأس بدست آمده را برابر خروج از مرکز قرار دهیم چراکه این نوع پیمایش طول کوتاهترین فاصله تا هر رأس را محاسبه میکند.
در انتها هم همانطور که گفته شد بیشترین خروج از مرکز را بر میگردانیم.
قابل ذکر است که این الگوریتم برای گرافهای همبند درست است، در غیر اینصورت قطر برابر بینهایت میشود.
چون در روش گفته شده، به ازای هر رأسی یک پیمایش انجام میدهیم پس زمان اجرا از
است. سپس بین
مقدار ممکن ماکسیمم میگیریم که در
قابل محاسبه است، پس زمان اجرا برابر
است.
از آنجا که به ازای هر رأسی تنها یک عدد نگه میداریم پس مرتبه حافظه از مرتبه پیمایش است.
فرض میکنیم گراف را به صورت لیست مجاورت به ما دادهاند.
#include <queue> #include <vector> const int MAXN = 100 * 1000 + 10; bool mark[MAXN]; int vector<int> adj[MAXN]; void bfs(int v) { int far = 0; queue<int> q; queue<int> depth; mark[v] = 1; q.push(v); depth_v.push(0); while(q.size()) { v = q.front(); far = depth.front(); q.pop(); depth.pop() for(int i = 0; i < adj[v].size(); i++) { int u = adj[v][i]; if (mark[u] == 1) continue; mark[u] = 1; q.push(u); depth.push(far + 1); } } return far; } int find_diameter() { int ans = 0; for (int i = 0; i < n; i++) ans = max(bfs(i), ans); return ans; }
این روش را با انتخاب منظم رأسها بهبود بخشید.
ابتدا دورترین رأس را از یک رأس دلخواه مانند
پیدا میکنیم و نام آن را
میگیریم. حال به سراغ
میرویم و دوباره دورترین رأس از آن را پیدا میکنیم و همین کار را اینبار با شروع از آن انجام میدهیم. این روش باعث میشود هر مرحله قطر پیدا شده کمی بزرگتر شود و مادامی که قطر بدست آمده در یک مرحله با مرحله پیش برابر بود، الگوریتم را متوقف و مقدار پیدا شده را گزارش میکنیم. چون این الگوریتم ممکن است قبل از شروع از همه رأس ها متوقف شود، پس سریعتر عمل میکند.
پیدا کردن سقف شعاع
فرض کنید یک گراف جهتدار وزندار با مجموعه رئوس
داریم. اگر گراف بدون جهت بود، میتوان از هر یال، ۲ یال جهتدار ساخت و الگوریتم را اجرا کرد. وزن یالهای گراف میتواند مثبت یا منفی باشد، اما گراف نباید دور با مجموع وزن منفی داشته باشد.
وزن یک مسیر را، مجموع وزن یالهای آن مسیر مینامیم. فاصلهی بین دو رأس را وزن کوتاهترین (کموزنترین) مسیر بین آنها در نظر میگیریم. میخواهیم به ازای هر دو رأس از این گراف، فاصلهی بین آن دو رأس را پیدا کنیم. الگوریتم فلوید-وارشال، راه حلی برای این مسئله ارائه میکند.
فرض کنید
باشد. تمام مسیرهایی از رأس
به رأس
در نظر بگیرید که رأسهای میانی مسیر، فقط بتوانند از رأسهای
باشند. مقدار
را وزن کوتاهترین (کموزنترین) مسیر در بین این مسیرها میگوییم. میخواهیم مقادیر
را به ازای
های مختلف پیدا کنیم. برای
میدانیم:
و اگر
به
یال با وزن
داشته باشد:
و اگر
به
یال نداشته باشد:
حال مقادیر را به ازای حساب میکنیم. فرض کنید به ازای یک ، تمام مقادیر را به ازای های مختلف، میدانیم. با این فرض، میخواهیم تمام مقادیر را به ازای های مختلف بیابیم.
دو رأس
را در نظر بگیرید. میخواهیم
را بیابیم. مسیرهایی که باید برای محاسبهی
در نظر گرفته شوند، دو حالت دارند؛ یا شامل رأس میانی
نیستند یا شامل رأس میانی
هستند. کموزنترین مسیری که شامل رأس میانی
نیست، وزن
را دارد و کموزنترین مسیری که شامل رأس میانی
است، وزن
را دارد (زیرا باید ابتدا از
به
برویم و سپس به
برویم. پس:
در گراف زیر، روند اجرای این الگوریتم را میتوانید مشاهده کنید:
به ازای هر باید روند بالا را طی کنیم. به ازای هر نیز باید dist بین هر ۲ رأس را محسابه کنیم. پس پیچیدگی زمانی برنامه از است. برای حافظه نیز یک آرایهی ۳ بعدی از نیاز داریم. پس حافظه نیز از است. هر چند جلوتر راهکاری برای کمکردن حافظه ارائه میشود.
شبه کد
let dist be a 3D array of minimum distances initialized to ∞ for i from 1 to n dist[i][i][0] = 0; for each edge from vertex a to vertex b dist[a][b][0] = w //w is the weight of uv edge for k from 1 to n for i from 1 to n for j from 1 to n dist[i][j][k] = min(dist[i][j][k-1], dist[i][k][k-1] + dist[k][j][k-1]
با کمی دقت در مییابیم اگر بعد سوم حافظه را نیز در نظر نگیریم، برنامه به درستی کار میکند. پس حافظه را میتوان از در نظر گرفت.
#include <iostream> #include <algorithm> using namespace std; const int MAXN = 10 * 1000 + 10; const int oo = 1000 * 1000 * 1000 + 10; int n, m; int dist[MAXN][MAXN]; void inputEdges(); void floydWarshall(); void output(); int main() { cin >> n >> m; for (int i = 0; i < n; i++) { fill(dist[i], dist[i] + n, oo); dist[i][i] = 0; } inputEdges(); floydWarshall(); output(); } void inputEdges() { for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; u--, v--; dist[u][v] = min(dist[u][v], w); } } void floydWarshall() { for (int k = 1; k <= n; k++) for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); } void output() { for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) cout << "distance from vertex " << i + 1 << " to vertex " << j + 1 << " is " << dist[i][j] << '\n'; }
میخواهیم چک کنیم آیا میتوان رأسهای گراف را به دو بخش افراز کرد به گونهای که تمام یالها بین این دو بخش بیافتد.
بنابر قضیههای گراف، شرط دوبخشی بودن با دور فرد نداشتن متناظر است، پس کافیست این شرط را چک کنیم.
برای تست این شرط و افراز به دو مجموعه، میتوانیم از هر دو الگوریتم جستوجوی عمقاول و جستوجوی سطحاول استفاده کنیم. کافیست نحوه علامت گذاری مزاعفی در این دو الگوریتم را به گونه ای اضافه کنیم که از رأسهای با رنگ ۰ به رأسهای با رنگ ۱ برویم و برعکس. حال اگر به رأسی با علامت ۱ رسیدیم که همسایهای از آن از قبل رنگ ۱ را داشت، پس در نتیجه دور فرد پیدا شده است و نمیتوان این کار را کرد؛ برای علامت ۰ نیز به همین ترتیب است. حال اگر هیچ یک از این دو حالت اتفاق نیافتد، در نتیجه میتوان این کار را کرد و رأسهایی که علامت ۱ را دارند در یک دسته و بقیه را در دسته مقابل قرار میدهیم. دقت کنید که ممکن است گراف ناهمبند باشد در نتیجه برای تمام رأسهای دیده نشده این الگوریتم را تکرار میکنیم.
از آنجا که از الگوریتم های پیمایش استفاده میکنیم، مرتبه زمانی برابر مرتبه زمانی جستوجو ها است که برابر است. تعداد رأسها و تعداد یالها است.
برای کوتاهی کد، آن را با جستوجوی عمقاول پیادهسازی میکنیم و فرض میکنیم که گراف را به صورت لیست مجاورت به ما دادهاند.
#include <vector> const int MAXN = 100 * 1000 + 10; bool mark[MAXN]; bool color[MAXN]; vector <int> adj[MAXN]; // اگر خروجی تابع ۱ باشد یعنی مؤلفهی v دوبخشی است و اگر ۰ باشد یعنی دور فرد دارد. bool dfs(int v, bool c){ mark[v] = 1; color[v] = c; for(int i=0; i<adj[v].size(); i++) { int u = adj[v][i]; if(mrk[u] != 1) { bool ret = dfs(u, 1-c); // با رنگ مخالف وارد رأس همسایه میشویم if (ret == 0) return 0; } if(color[u] == c) // اگر رنگ همسایه همرنگ این رأس بود return 0; } } return 1; }
عبارتی بولی مانند را در نظر بگیرید. این عبارت از 3 بند تشکیل شدهاست که بین بندهای آن وجود دارد. هر بند دو متغیر دارد که بین آنها است. به مسئله تشخیص توانایی درست بودن یک عبارت بولی که از چند بند که هر بند از دو متغیر(هر متغیر ممکن است خودش یا نقیضش استفاده شده باشد) تشکیل شده است، مسئله دو صدقپذیری می گویند.
ابتدا به ازای هر متغیر یک راس و به ازای نقیض هر متغیر نیز یک راس در نظر می گیریم. حال به ازای هر بند در عبارت بولی مانند یک یال جهتدار از راس به راس و یک یال جهتدار از راس به راس میگذاریم. حال به کمک الگوریتم پیدا کردن مولفههای قویا همبند، مولفه های قویا همبند گراف ایجاد شده را بدست می آوریم. اگر متغیری بود که با نقیضش در یک مولفه بود، عبارت بولی نمی تواند جواب نهاییش صحیح باشد. در غیر اینصورت عبارت بولی می تواند مقدارش صحیح باشد.
در گراف ایجاد شده اگر از راس به راس مسیر وجود داشته باشد، درست بودن درست بودن قرار می دهیم این خاصیت را دارد). پس اگر دو راس در یک مولفه قویا همبند باشند مقدار نهایی آن ها برای صحیح بودن مقدار کل باید برابر باشد. پس متغیری با نقیضش نمیتواند در یک مولفه قویاهمبند باشد. حال اگر هیچ متغیری با نقیضش در یک مولفه قویاهمبند نباشد، می توان به ازای هر متغیر در صورت کمتر بودن شمارهی مولفه قویاهمبندش نسبت به شمارهی مولفه قویاهمبند نقیضش، مقدار نادرست را به این متغیر بدهیم و در غیراینصورت مقدار صحیح را به این متغیر بدهیم (شمارهی مولفه در پیدا کردن مولفههای قویا همبند مشخص شده است). در این حالت هیچ دو راسی مانند و بوجود نمیآید که از به مسیر باشد و مقدار صحیح باشد اما مقدار صحیح نباشد. زیر اگر اینطوری باشد، آنگاه با توجه به صحیح بودن شماره مولفه از بیشتر است و با توجه به مسیر به شماره مولفه بزرگتر مساوی شماره مولفه است. همینطور بدلیل صحیح نبودن شماره مولفه از شماره مولفه بیشتر است و در نهایت بدلیل مسیر از به ، مسیر از به وجود دارد که باعث می شود شماره مولفه بیشتر مساوی شماره مولفه شود که تناقض دارد. درنتیجه مقدار دهی گفته شده برای متغیرها خروجی صحیح به عبارت بولی میدهد.
فرض کنید تعداد متغیرها و تعداد بندها تا باشد. آنگاه ابتدا با گرافی با راس و یال ساختیم و با مولفههای قویا همبند آن را بدست آورده و برای هر متغیر شرط یکسان نبودن مولفهاش با نقیضش را چک کردیم. پس الگوریتم ما در کل دارای مرتبه زمانی است.
#include<iostream> #include<vector> using namespace std; const int maxn=1e5; int n, m, comps, comp[maxn]; bool mark[maxn]; vector<int>G[maxn], BG[maxn], myStack; void back_dfs(int v) { comp[v]=comps; mark[v]=true; for(int i=0;i<BG[v].size();i++) { int u=BG[v][i]; if(!mark[u]) back_dfs(u); } } void dfs(int v) { mark[v]=true; for(int i=0;i<G[v].size();i++) { int u=G[v][i]; if(!mark[u]) dfs(u); } myStack.push_back(v); } void find_comps() { for(int i=1;i<=2*n;i++) if(!mark[i]) dfs(i); fill_n(mark, 2*n+1, false); for(int i=myStack.size()-1;i>-1;i--) if(!mark[myStack[i]]) { comps++; back_dfs(myStack[i]); } } void add_edge(int x,int y) { G[x].push_back(y); BG[y].push_back(x); } int main() { cin>>n>>m; for(int i=0;i<m;i++) { int x,y;// است i به معنای نقیض (n*2-i+1) cin>>x>>y; add_edge(2*n-x+1,y); add_edge(2*n-y+1,x); } find_comps(); for(int i=1;i<=n;i++) if(comp[i] == comp[2*n-i+1]) { cout<<"NO\n"; return 0; } cout<<"YES\n"; }
یکی از مسایل مهم در زمینه پردازش تصویر و بررسی الگوریتمهای هندسی محاسبه مساحت چند ضلعیهاست.فرض کنید چند ضلعی دارای رئوس میباشد که به ترتیب پیمایش شدهاند. حال نقطه ای دلخواه مانند در صفحه درنظر بگیرید. ادعا میکنیم که مساحت با در نظر گرفتن علامت برابر است با حاصل جمع علامتدار مساحت مثلثهای .با بیانی واضحتر، مساحت چند ضلعی برابر است با حاصل جمع ضرب خارجیهای متوالی بردارهای .
ادعا را با استقرا به روی تعداد رئوس
اثبات میکنیم. به این ترتیب که ابتدا چندضلعی را با کشیدن یک قطر داخلی مثلا
(البته با این شرط که این قطر کاملا درون چندضلعی قرار گیرد که نیز نیاز به اثبات داردو بر عهده خواننده میباشد!) به دو چندضلعی کوچک تر
و
تقسیم میکنیم. حال مقدار
را یک بار با علامت مثبت و یک بار با علامت منفی با حاصل جمع ذکر شده در ابتدای صفحه جمع کرده و با استفاده از فرض استقرا حکم را نتیجه میگیریم:
الگوریتم ذکر شده از مرتبه زمانی و حافطه مصرف شده از مرتبه میباشد.
int cross(vct a,vct b,vct c) { vct ab,bc; ab=b-a; bc=c-b; return ab.x*bc.y-ab.y*bc.x; } double area(vct p[],int n) { int ar=0; for(i=1;i+1<n;i++) { vct a=p[i]-p[0]; vct b=p[i+1]-p[0]; area+=cross(a,b); } return abs(area/2.0); }
به طور کلی مجموع زوایای داخلی هر
ضلعی منتظم برابر است با
.از طرفی برای محاسبه مجموع زوایا با استفاده از پیمایش روی رئوس میبایست به ترتیب ساعتگرد راس های چند ضلعی را پیمایش کرده و با استفاده از مقدار ضرب داخلی دو برداری که از یک راس خارج میشوند و تقسیم آن بر مقدار حاصل ضرب طول آن دو بردار کسینوس آن زاویه را پیدا کرده و با استفاده از تابع
مقدار زاویه را محاسبه کنیم. ( با مرتبه زمانی
مجموع زوایا را میتوان محاسبه کرد.)
یکی از مسائلی که در زمینههای مختلف مرتبط با کامپوتر مطرح است، مسائل مربوط به پیدا کردن یک رشته (عموما نه چندان بلند) در میان یک رشتهی بسیار بلند است. به صورت دقیقتر، فرض کنید میخواهیم یک رشتهی حرفی را در یک رشتهی حرفی به صورت یک زیررشتهی متوالی پیدا کنیم. پیادهسازی بدیهی چنین جستجویی، دارای پیچیدگی زمانی خواهد بود. الگوریتم رابین - کارپ با استفاده از تکنیک درهمسازی این مسئله را در پیچیدگی زمانی حل میکند.
فرض کنید یک زیررشتهی متوالی شامل حروف تا یک رشته رابا نشان دهیم. فرض کنید بخواهیم رشتهی را در پیدا کنیم. الگوریتم جستجو به شکل زیر عمل میکند:
به ازای هر ، زیررشتهی را با مقایسه کنید و در صورت برابر بودن این زیررشته را به عنوان یک نسخه از در گزارش کنید.
اگر زمان مقایسهی دو رشته به طول را با نشان دهیم الگوریتم در انجام میشود. در حالت عادی . الگوریتم رابینکارپ روشی برای مقایسهی دو رشته ارائه میدهد که به صورت متوسط . هر چند در واقع در بدترین حالت یک جستجو میتواند زمان طول بکشد اما چنین اتفاقی با احتمال کمی رخ میدهد.
الگوریتم رابینکارپ جستجو را به این گونه تغییر میدهد که پیش از مقایسهی دو رشته در ابتدا مقدار یک تابع درهمساز را برای آنها محاسبه کرده و تنها در صورت برابری این مقایسه را انجام میدهد. نکتهی الگوریتم رابینکارپ محاسبهی این تابع به گونهای است که در مجموع در زمان قابل انجام است. بعلاوه با وجود اینکه احتمال برابری تابع درهمساز برای دو رشتهی نابرابر وجود دارد، این احتمال برای تابع درهمساز مورد استفاده کم است.
function NaiveSearch(string s[1..n], string pattern[1..m]) for i from 1 to n-m+1 for j from 1 to m if s[i+j-1] ≠ pattern[j] jump to next iteration of outer loop return i return not found
function RabinKarp(string s[1..n], string pattern[1..m]) hpattern := hash(pattern[1..m]); for i from 1 to n-m+1 hs := hash(s[i..i+m-1]) if hs = hpattern if s[i..i+m-1] = pattern[1..m] return i return not found
تابع درهمساز «اثر انگشت رابین» تابعی است که در این الگوریتم برای درهمسازی رشتهها استفاده میشود. این الگوریتم هر زیررشته را معادل یک عدد در یک مبنای ثابت در نظر میگیرد. مبنای مورد استفاده معمولا یک عدد اول بزرگ است. در این الگوریتم هر حرف معادل یک رقم است و مقدار آن برابر کد ASCII آن حرف در نظر گرفته میشود. برای مثال :
// ASCII a = 97, b = 98, r = 114. hash("abr") = (97 × 1012) + (98 × 1011) + (114 × 1010) = 999,509
نکتهی این الگوریتم آن است که اگر مقدار آن برای زیررشتهی محاسبه شده باشد، مقدار آن برای زیررشتهی در قابل محاسبه است. برای مثال اگر میتوان از روی مقدار hash(«abr») مقدار hash(«bra») را به شکل زیر محاسبه کرد:
// base old hash old 'a' new 'a' hash("bra") = [1011 × (999,509 - (97 × 1012))] + (97 × 1010) = 1,011,309
پس بنابراین در الگوریتم مقایسهی بالا، کافی است در ابتدا در مقدار تابع برای را محاسبه کرد و پس از آن در هرمرحله مقدار تابع برای را از روی مقدار تابع برای محاسبه کرد. در این صورت پیچیدگی زمانی از خواهد بود.
الگوریتم KMP،یکی از الگوریتمهای مطرح در علوم کامپیوتر است که میتواند تمام رخدادهای یک رشته به طول را در یک رشته به طول پیدا کند. منظور از یک رخداد یک زیررشتهی متوالی است که با رشتهی مورد نظر برابر است.
بدیهی ترین الگوریتم برای پیدا کردن رخدادها بررسی تمام زیررشتههای متوالی به طول به صورت جداگانه است. پیچیدگی زمانی این روش خواهد بود در حالی که الگوریتم KMP در پیچیدگی زمانی و حافظهی موفق به پیدا کردن تمام رخدادها میشود.
فرض کنید میخواهیم رخدادهای در را پیدا کنیم. زیررشتهی متوالی حروف تا یک رشته مانند را با نشان میدهیم. فرض کنید بلندترین پیشوند از باشد که پسوند نیز هست اما با آن برابر نیست. برای ، فرض میکنیم و . برای مثال:
i 0 1 2 3 4 5 6 W[i] A B C D A B D T[i] -1 0 0 0 0 1 2
فرض کنید بتوانیم این تابع را در برای محاسبه کنیم. الگوریتم KMP با استفاده از این تابع میتواند در مسئلهی یافتن رخدادها را حل کند. برای اینکار الگوریتم روی حروف حرکت میکند و با اضافه شدن هر حرف، عدد را به روزرسانی میکند طوری که عدد بعد از اضافه شدن حرف برابر با طول بلندترین پیشوند باشد که پسوند است.
برای اینکار کافی است به این نکته توجه شود که در هر لحظه و به ازای هر ، و به ازای هر ، . بنابراین اگر داریم . بنابراین در محاسبهی پس از اضافه شدن حرف ام تنها زیررشتهی موثر است. براساس همین استدلال میتوان به صورت استقرایی نتیجه گرفت که برای محاسبهی بعدی میتوان از تکهکد زیر استفاده کرد:
while x > -1 and t[x] != s[i + 1] # Note that x is length and t is zero-base indexed x = T(x) x = x + 1
حال اگر به ازای هر ، مقدار برابر ، طول رشتهی شود یک رخداد پیدا میشود.
فرض شد که بتوانیم قسمت اول الگوریتم، یعنی محاسبهی را در انجام دهیم. جهت بررسی پیچیدگی سادهتر قسمت دوم فرض کنید در این صورت مقدار اکیدا صعودی است. در ابتدا و در انتها پس در کل پیچیدگی زمانی این قسمت از الگوریتم از خواهد بود. پس به طور کلی پیچیدگی زمانی الگوریتم خواهد بود.
تابع را تابع شکست (failure function) رشتهی میگویند. برای محاسبهی این تابع در ابتدا میدانیم و . حال دقیقا مطابق استدلالهایی که در قسمت اصلی الگوریتم انجام شد میتوان را از روی محاسبه کرد.
کد زیر پیادهسازی مشابهی از الگوریتم KMP است.
void preKmp(char *x, int m, int kmpNext[]) { int i, j; i = 0; j = kmpNext[0] = -1; while (i < m) { while (j > -1 && x[i] != x[j]) j = kmpNext[j]; i++; j++; if (x[i] == x[j]) kmpNext[i] = kmpNext[j]; else kmpNext[i] = j; } } void KMP(char *x, int m, char *y, int n) { int i, j, kmpNext[XSIZE]; /* Preprocessing */ preKmp(x, m, kmpNext); /* Searching */ i = j = 0; while (j < n) { while (i > -1 && x[i] != y[j]) i = kmpNext[i]; i++; j++; if (i >= m) { OUTPUT(j - i); i = kmpNext[i]; } } }
فاصلهی ویرایشی بین دو کلمه، که فاصلهی لوانشتین (Levenshtein) نیز نامیده میشود، کمترین تعداد حذف، اضافه، یا تغییر حروف است که نیاز داریم تا یکی از این کلمات را به دیگری تبدیل کنیم. برای مثال فرض کنید میخواهیم فاصلهی کلمهی «اجتماع» و «مجموعه» را محاسبه کنیم. این مقدار حداکثر برابر چهار است:
مجموعه → مجموع → مجماع → مجتماع → اجتماع
میتوان این روند را با زیر هم نوشتن کلمات نیز نشان داد:
ا ج ت م ا ع م ج م و ع ه
یک خاصیت این تعریف این است که فاصلهی دو کلمه، به ترتیب آنها بستگی ندارد. یعنی فاصلهی کلمهی الف با کلمهی ب، برابر است با فاسلهی کلمهی ب و کلمهی الف. چون میتوان تمام مراحل را به صورت وارونه اجرا کرد.
همچنین در این سوال دلیلی ندارد که فقط از حروف فارسی یا انگلیسی استفاده کنیم بلکه میتوانیم به جای دو کلمه، فاصلهی دو سری از اعداد را با هم مقایسه کنیم. اما چون شهود داشتن روی تبدیل کلمات به هم راحتتر است، در مثالها از آن استفاده میکنیم.
بیایید برای حل سوال از روش برنامهریزی پویا کمک بگیریم. فرض کنید میخواهیم فاصلهی بین i خانهی اول از کلمهی اول و j خانهی اول از کلمهی دوم را محاسبه کنیم. به این مقدار
میگوییم. پس جواب مسئله
است اگر
و
را به ترتیب تعداد حروف کلمهی اول و دوم در نظر بگیریم.
اگر مثلا
برابر صفر باشد، جواب برابر
است، چون باید تمام
حرف را به کلمهی اول اضافه کنیم. همچنین اگر
برابر صفر باشد، جواب برابر
است، چون باید تمام
خانهی کلمهی اول را حذف کنیم تا به کلمه دوم برسیم. در غیر اینصورت، یا میتوانیم یکی از سه کار زیر را انجام دهیم:
شبه کد: ( را حرف ام کلمهی اول و را حرف ام کلمهی دوم در نظر بگیرید.)
for i from 0 to n d[i][0] = i for j from 0 to n d[0][j] = j for i from 1 to n for j from 1 to n if a[i] == b[j] plus = 0 else plus = 1 d[i][j] = min( d[i-1][j] + 1 , d[i][j-1] + 1 , d[i-1][j-1] + plus )
از آنجایی که هر ستون فقط از روی خودش و ستون قبلش به روز رسانی میشود، میتوان به جای نگه داشتن یک آرایه از یک آرایهی که از است استفاده کنیم. اما اگر برای ما به جز تعداد تغییرات در بهترین حالت، خود این تغییرات نیز مهم باشد، در هر صورت باید یک آرایهی برای نگهداری مسیر برگشت استفاده کنیم که اندازهی حافظه از میشود. اما در هر دو صورت باید مقدار را محاسبه کنیم و هر محاسبه هم از است. پس پیچیدگی زمانی است.
#include<iostream> using namespace std; const int MAXN=10*1000; int d[MAXN+10][MAXN+10]; int a[MAXN+10]; int b[MAXN+10]; int main(){ ios::sync_with_stdio(false); int n, m; cin >> n; for(int i=0; i<n; i++) cin >> a[i]; cin >> m; for(int j=0; j<m; j++) cin >> b[j]; for(int i=0; i<=n; i++) d[i][0] = i; for(int j=0; j<=m; j++) d[0][j] = j; for(int i=1; i<=n; i++) for(int j=1; j<=m; j++){ int plus = 1; if( a[i-1] == b[j-1] ) // a and b are 0-base plus = 0; d[i][j] = min( min( d[i-1][j] + 1 , d[i][j-1] + 1 ) , d[i-1][j-1] + plus ); } cout << d[n][m] << endl; return 0; }
فرض کنید شهر و طول مسیر های بین آنها به شما داده شده است، کمترین مسافتی که باید طی شود که با شروع از یک شهر همهی شهر ها دقیقا یک بار دیده شوند و دو باره به همان شهر اولیه برگردیم چه قدر است؟
این
شهر را با یک گراف
راسی مدل میکنیم.
از یک لیست خالی شروع می کنیم و در هر مرحله به ته لیست یک راس از همسایه های راس انتهایی لیست اضافه میکنیم و مسیر را به صورت بازگشتی کامل میکنیم. در انتها، مسیر را میبندیم و در صورتی که (طول مسیر)
جواب کمینه را به روز رسانی میکنیم و قرار می دهیم
.در هر مرحله اگر مسیر توانایی کامل شدن به دوری با طول کمتر از
نداشته باشد،به عبارت دیگر کران پایین آن
از
بزرگ تر باشد آن را کنار گذاشته و به مرحلهی قبل باز میگردیم.
می توان را برابر با جمع طول (مسیر) یعنی و طول کوتاهترین مسیر بین دو سر آن قرار داد. یا این که برابر با که اندازهی و و x طول کم وزن ترین یال در بین راس های باقی مانده است در نظر گرفت، زیرا برای برگشت حتما باید راس پیموده شود و هر پیمایش حداقل به اندازهی مسافت هزینه خواهد داشت.این کران را میتوان با متغیر در نظر گرفتن بهبود داد یعنی که وزن کموزن ترین یال ورودیی راس در بین راس های پیمایش نشده است.
زمان اجرای این الگوریتم است.
#include <iostream> #include <cstdlib> #include <algorithm> #include <queue> #include <deque> #include <set> #include <vector> #include <map> #include <string> #include <cstring> #include <iomanip> #include <cstdio> #include <fstream> #include <sstream> using namespace std; typedef long long ll; typedef pair<int,int> pii; const int inf = 1000*1000*1000+100; const int maxn = 200; const double eps = 1e-7; int n; int rem; double dist[maxn][maxn]; double B; double l; int mark[maxn]; vector<int> path; vector<int> best; double g(int v) { double ret=0; for(int i=0; i < n ; ++i) { if(!mark[i]) { double mi = inf; for(int j=0 ; j < n ; ++j) if( (!mark[j]||j==v) && j!=i) mi=min(mi,dist[j][i]); ret+=mi; } } return ret; } void BnB(int v) { if(v==0 ){ if(mark[v]){ if(!rem){ if(l < B){ best=path; B=l; cerr << B << endl; } } return; } } if( g(v) > B-eps ){ return; } for(int i = 0 ; i < n ; ++i) if(!mark[i]) { rem--; mark[i]=true; path.push_back(i); l+=dist[v][i]; BnB(i); l-=dist[v][i]; path.pop_back(); mark[i]=false; rem++; } } int main() { ios::sync_with_stdio(false); cin >> n; for(int i = 0 ; i < n ; ++i){ for(int j = 0 ; j < n ; ++j){ cin >> dist[i][j]; } } rem = n; B=inf; BnB(0); cout << B << " " << (int)(best).size() << endl; for(int i = 0 ; i < (int)best.size() ; ++i) cout << best[i] << " "; cout << endl; return 0; }
این سؤال یکی از سؤالهایی است که راه حل بدیهی از
دارد. اما یک راه حل هم با
دارد. ما در زیر به بررسی هر دو روش میپردازیم.
در یک مهمانی،
نفر حضور دارند. بین هر دو نفر
یک رابطه دوستی به این صورت برقرار است که یا
نفر
را میشناسد، یا بالعکس. میخواهیم با تعدادی پرسش، به صورت زیر، متوجه بشویم که آیا شخصی وجود دارد که همه را بشناسد یا خیر.
پرسش: آیا شخص
شخص
را میشناسد؟
یک روش بدیهی از
به این صورت است که به ازای هر شخص، در صورتی که این شخص همه افراد دیگر را میشناخت، شخص مورد نظر را به عنوان جواب سؤال در نظر بگیریم.
اما روشی با
ارائه میدهیم:
اگر شخص
شخص
را بشناسد، میتوانیم مطمئن شویم که
نمیتواند مشهورترین فرد باشد. پس با یک پرسش به راحتی میتوان یک کاندید را از کاندیدهای مشهورترین فرد بودن، حذف کرد. حال عضو
را در نظر نگیرید. با
پرسش، از بین
نفر باقیمانده، کاندید خود را انتخاب کنید. سپس با یک پرسش، بین کاندید خود و نفر
ام، یک نفر را به عنوان کاندید نهایی انتخاب کنید. حال که کاندید نهایی را دارید، با
پرسش دیگر، بررسی کنید که آیا این کاندید مشهورترین شخص است یا خیر.
به وضوح پیچیدگی الگوریتم اول و دومی است.
#include <iostream> using namespace std; int main() { int celeb = -1; for(int i=0;i<n;++i) { bool flag = true; for(int j=0;j<n;++j) if(i != j) if(ask(i, j) == false) //imagine you have a finction ask, which tell you the answer for question ask(a, b). if b know a it returns true flag = false; if(flag == true) celeb = i; } if(celeb != -1) cout << "We find it!! celebrity is: " << celeb + 1 << endl; else cout << "There is no celebrity :(" << endl; return 0; } پیادهسازی سریعتر celebrity_fast.cpp #include <iostream> using namespace std; int main() { int celeb = 0; for(int i=1;i<n;++i) if(ask(celeb, i) == false) //imagine you have a finction ask, which tell you the answer for question ask(a, b). if b know a it returns true celeb = i; bool flag = true; for(int i=0;i<n;++i) if(celeb != i && ask(celeb, i) == false) flag = false; if(flag == true) cout << "We find it!! celebrity is: " << celeb + 1 << endl; else cout << "There is no celebrity :(" << endl; return 0; }