ای سی ام کار تازه وارد

در این وبلاگ مطالبی در مورد برخی الگوریتم ها و راهنمایی برای حل سوالات ارائه می گردد

ای سی ام کار تازه وارد

در این وبلاگ مطالبی در مورد برخی الگوریتم ها و راهنمایی برای حل سوالات ارائه می گردد

۹ مطلب با موضوع «درسنامه ها» ثبت شده است

تعاریف و مفروضات:

فرض می کنیم یه رشته داریم مثل S و می خوایم ببینیم چند بار رشته ای مثل T تکرار شده.

به رشته اصلی اصطلاحا text و به رشته ای دنبالش می گردیم، pattern می گیم.

طول text رو n فرض می کنیم و طول pattern رو m.

مثلا تو رشته aaaa رشته aa 3 بار تکرار شده.

i امین prefix رشته ای مثل S رو [S[0..i تعریف می کنیم.

i امین suffix رشته ای مثل S رو [S[i..n-1 تعریف می کنیم.


بدیهی ترین راه حل اینه که بیایم از هر نقطه شروع چک کنیم که آیا زیر رشته فعلی دقیقا برابر با pattern هست یا نه.

یعنی کاری شبیه این می کنیم:

int counter = 0;
for( int i = 0 ; i + pattern.size() <= text.size() ; i ++ )
{
  bool mismatch = false;
  for( int j = 0 ; j < pattern.size() ; j ++ )
    if( text[i+j] != pattern[j] )
    {
      mismatch = true;
      break;
    }
  if( mismatch == false )
    counter ++;
}

اگه دقت کنید این پیاده سازی (O(n*m زمان نیاز داره.

سه دانشمند به نام های Knuth, Morris, Pratt الگوریتمی ابداع کردند که تو زمان (O(n+m این کار رو انجام میده. الگوریتمی که طراحی کردند به اسم سازندگانش معروف شده.( KMP )

نحوه کار الگوریتم به این شکله:

فرض کنید تو پیاده سازی بالا برای اندیسی مثل i ما تونسته باشیم k تا کاراکتر اول pattern رو match کرده باشیم و تو اندیس k+1 از pattern به mismatch رسیده باشیم.

الگوریتم بالا میاد تمام k تا match ای که داشت رو پاک می کنه و دوباره از اول برای اندیس i+1 از text کار رو تکرار می کنه.

چیزی که تو KMP اتفاق میوفته اینه که الگوریتم بر اساس یه پیش پردازشی که انجام داده، میاد یه تعداد از k تا match ای که داشت رو پاک می کنه و ادامه میده.

چطور؟

کافیه برای هر اندیس از pattern ، "طول بلندترین suffix رو بدست میارم که prefix هم باشه".

یعنی می خوایم تو i امین prefix از pattern، عددی مثل k رو پیدا کنیم که 

pattern[0..k-1] == pattern[ i-k+1..i]

به عبارت دیگه، k کاراکتر اول از pattern دقیقا برابر k کاراکتر آخر از i امین prefix از pattern باشه.


مثلا برای رشته ای مثل abaab داریم:

برای اندیس 0:

0 امین prefix میشه : a

این رشته هیچ prefix ای نداره پس مقدار k0 = -1 فرض می کنیم.(قرارداد می کنیم که -1 یعنی هیچ)

برای اندیس 1:

1 امین prefix میشه : ab

این رشته یه prefix داره و یه suffix. اینجا هم باز چون برابر هم نیستند k1 = -1 میشه.

برای اندیس 2:

2 امین prefix میشه : aba

این رشته یه prefix به طول 1 داره که دقیقا برابر suffix به طول 1 ـه. پس k2 = 1 میشه.

برای اندیس 3:

3 امین prefix میشه : abaa

این رشته هم مثل حالت قبلی میشه. پس k3 = 1 میشه.

برای اندیس 4:

4 امین prefix میشه : abaab

این رشته یه prefix به طول 2 داره که برابر suffix به طول 2 ـه. پس k4 = 2 میشه.


این اطلاعات به چه درد می خوره؟

زمانی که تو اندیسی مثلا 3 از pattern یه mismatch رخ میده، می تونیم به جای اینکه کل match هایی که داشتیم رو پاک کنیم، می تونیم، 3 - K3  از match هارو پاک کنیم، ادامه بدیم.

برای اینکه واضح تر بشه روی یه مثال میگم

text = ababaabd

pattern = abaab

الگوریتم این طور کار می کنه:

3 کاراکتر اول از pattern رو match می کنه و به 4 امین کاراکتر می رسه.

چون [pattern[3] != text[3 پس یه mismatch اتفاق افتاده.

الگوریتم اول میاد تمام 3 تا match ای که داشت رو می ریزه دور و شروع می کنه از دومین کاراکتر text ادامه دادن.

اما KMP میاد میگه چون K3 = 1 بوده، پس می تونیم 1 دونه از match هایی که داشتیم رو نگه داریم و دنبال 2 امین match بگردیم.

اتفاقی که میوفته عملا اینجوریه:

شروع می کنیم به match کردن از اندیس 0 ام.

ababaabd

abaab

    ^

تو این اندیس mismatch داریم، الگوریتم میگه به جای اینکه برگردیم به این حالت:

ababaabd

 abaab

 ^

از مقداری که پیش محاسبه کردیم استفاده کنیم و بریم به این حالت:

ababaabd

  abaab

   ^

و ادامه بدیم به match کردن. چون match شده میریم جلو تا رشته رو پیدا کنیم یا دوباره mismatch ببینیم.

هر جا mismatch دیدیم، از مقادیری که محاسبه کردیم استفاده می کنیم.


چجوری این مقادیر رو حساب کنیم؟

خود نویسندگان یه الگوریتم برای پیدا کردن Ki ها ارائه دادن که اینجوریه:

int *fail[m+1];
int j = fail[0] = -1;
for( int i = 1 ; i <= m ; i ++ )
{
	while( j >= 0 && pattern[j] != pattern[i-1] )
		j = fail[j];
	fail[i] = ++ j;
}
آرایه fail ای که تولید شده همون K ای که بالا توضیح دادمه.
اینجوری اسمش بهتره.

کد الگوریتم چجوریه؟
کد الگوریتم خیلی شبیه قسمت بالاست.
فقط باید pattern رو روی text سرچ کنیم. یعنی:

int count = 0, k = 0;
for( int i = 0 ; i < n ; i ++ )
{
	while( k >= 0 && pattern[k] != text[i] )
		k = next[k];
	if( ++ k >= m )
	{
		k = next[k];
		count ++;//match at t[i-m+1 .. i]
	}
}
تحلیل زمانی:
برای قسمت اول:
اگه دقت کنید می تونید ببینید که برای هر j داریم: [j >= fail[j
پس عبارت [j = fail[j فقط می تونه مقدار j رو کم کنه، همچنین شرطی داریم که نمی زاره j کمتر از 0 بشه.
پس اون while داخل for حداکثر به اندازه j می تونه کار کنه.
تنها جایی که j داره زیاد میشه اون عبارت ++ j ـه که دقیقا m بار داره انجام میشه.
پس در مجموع قسمت اول الگوریتم (O(m بار اجرا میشه.

برای قسمت دوم:
تحلیل زمانی الگوریتم دقیقا مثل قسمت قبل میشه پس (O(n بار انجام میشه.

لذا در مجموع این الگوریتم به (O(n+m زمان احتیاج داره.

اگه احساس می کنید بد توضیح دادم، بگید تا اصلاح کنم.

موفق باشید
۳ نظر موافقین ۲ مخالفین ۱ ۲۹ تیر ۹۴ ، ۰۰:۱۹
رضا حسینی آشتیانی

در یک درخت ریشه دار ، "پایین ترین جد مشترک" دو گره را دورترین راس از ریشه تعریف می کنیم که جد هردو گره باشد .


این مساله معمولا برای درخت های بزرگ مثلا 105 راسی مطرح میشود .


برای حساب کردن ( LCA( x , y به صورت زیر عمل می کنیم .

بدون کاسته شدن از کلیت فرض کنید گره x در ارتفاع بیشتری باشه .

قبل از هر کاری باید گره x رو اونقدر به بالا منتقل کنیم تا هم ارتفاع با y بشه .

حالا اگر x = y باشه در نتیجه LCA همان x است

اگر x != y باشه ، تو این حالت از اونجاییکه فاصله x تا LCA و فاصله y تا LCA برابر است یک مقدار i دلخواه پیدا می کنیم که i امین پدر x و y با هم برابر نباشند ، از اونجایی که 

LCA( x , y ) = LCA( Parent( x , i ) , Parent( y , i ) )

می تونیم قرار بدیم ( x = Parent( x , i ) , y = Parent( y , i و مساله رو برای این زیر مساله حل کنیم .

اگر خوب دقت کنید میبینید که انتخاب i خیلی تو سرعت الگوریتم تاثیر داره .

منظور از ( Parent( x , y یعنی پدر y ام گره x .


ایده کلی ساختمان داده اینه که برای هر گره ، 20 امین پدر ، 21 امین پدر ، 22 امین پدر و ... رو نگه داریم .


حالا برای بدست آوردن k امین پدر یک گره کافیه k رو به صورت باینری نگاه کنیم .

از اونجایی که k = 2i1 + 2i2 + ... + 2in  می تونیم

اول 2i1 امین پدر k رو محاسبه کنیم سپس پدر k - 2iگره جدید رو محاسبه کنیم .

از اونجایی که یه عدد رو میشه به صورت حداکثر Log N تا جمعوند نوشت ، الگوریتم بعد از Log N محاسبه به جواب می رسه .


نحوه ساختن درخت

برای ساختن درخت کافیه برای هر گره بدونید پدرش کیه و تو چه ارتفاعی قرار داره .

برای این کار می تونید درخت از ریشه BFS یا DFS بزنید تا Dep و Parent هر گره رو معلوم کنید .

حالا با داشتن این مقادیر درخت رو می سازیم

for( int i = 0 ; i < N ; i ++ )
    for( int j = 0 ; ( 1 << j ) < N ; j ++ )
        Tree[i][j] = -1;
for( int i = 0 ; i < N ; i ++ )
    Tree[i][0] = Par[i];
for( int j = 1 ; ( 1 << j ) < N ; j ++ )
    for( int i = 0 ; i < N ; i ++ )
        Tree[i][j] = Tree[ Tree[i][j-1] ][j-1];

تو این درخت ، منظور از [Tree[i][j یعنی پدر 2j ام از گره i .

اول پدر همه رو -1 می زاریم ، یعنی اینکه هیچ پدری نداره .
بعد پدر سطح 20 ام رو مشخص می کنیم .

حالا چون به ترتیب از پایین به بالا حرکت می کنیم ، برای برست آوردن پدر 2j ام از راس i کافیه ،

پدر 2j-1 از راس i رو بدست بیاریم ( قبلا محاسبه کردیم و تو خونه [Tree[i][j-1 ذخیره کردیمش ) و بعد پدر 2j-1 ام اون گره رو بدست بیاریم ( این مقدار رو هم قبلا محاسبه کردیم )


برای بدست آوردن LCA دو گره کافیه تابع زیر رو فراخوانی کنیم

int LCA( int x , int y ) {
  if( Dep[x] < Dep[y] )
    swap( x , y );
  int log = 0;
  for( int log = 0 ; ( 1 << log ) < Dep[x] ; log ++ );
  log --;
  for( int j = log ; j >= 0 ; j -- )
    if( ( Dep[x] - Dep[y] ) & ( 1 << j ) )
      x = Tree[x][j];
  if( x == y )
    return x;
  for( int j = log ; j >= 0 ; j -- )
    if( Tree[x][j] != -1 && Tree[x][j] != Tree[y][j] )
      x = Tree[x][j], y = Tree[y][j];
  return Par[x];
}

پیچیدگی زمانی:
( O( N * Log N برای پیش محاسبات و ساختن درخت
( O( Log N برای بدست آوردن هر Query

حافظه : ( O( N * Log N 

۲ نظر موافقین ۱ مخالفین ۰ ۰۶ فروردين ۹۳ ، ۲۲:۵۱
رضا حسینی آشتیانی

در این مقاله الگوریتم های موجود در زمینه کوتاه ترین مسیر رو به ترتیب معرفی خواهیم کرد .

۱ نظر موافقین ۳ مخالفین ۰ ۱۸ اسفند ۹۲ ، ۲۲:۱۶
رضا حسینی آشتیانی

تعاریف :

گراف G را اویلری می گوئیم اگر و فقط اگر همبند باشد و درجه تمام رئوس آن زوج باشد .

گراف G را نیمه اویلری می گوئیم اگر و فقط اگر همبند باشد و حداکثر 2 رأس درجه فرد داشته باشد .

مسیر بسته ای که که از یک رأس شروع شده و از تمامی یال های گراف دقیقا یکبار می گذرد و به نقطه شروع باز می گردد را دور اویلری گوئیم .

مسیری که از یک رأس شروع شده و از تمامی یال های گراف دقیقا یکبار می گذرد و در نقطه ای دیگر متوقف می شود ، مسیر اویلری گوئیم .


برای پیدا کردن دور(مسیر) اویلری در یک راف ، کافیست شرط اویلری بودن رو چک کنیم .

اگر اویلری بود ، کافیه از راسی دلخواه شروع کنیم . اگر نیمه اویلری بود ، از یک رأس درجه فرد شروع می کنیم .

به راحتی میشه دید که تو گراف نیمه اویلری ، مسیر از یک رأس درجه فرد شروع میشه و به رأس فرد دیگه ختم میشه .


برای بدست آوردن دور(مسیر) الگوریتم خطی نسبت به تعداد یالها( (O(E ) وجود داره .

الگوریتم :

با کمک DFS گراف را پیمایش می کنیم . هر بار که از روی یک یال گذشتیم ، یال مورد نظر رو از گراف حذف می کنیم . زمان بازگشت از DFS رأس فعلی رو به مسیر اضافه می کنیم .

نکته : تو گراف اویلری ، تو هر راس نحوه انتخاب یال بعدی مهم نیست .

نکته : تو گراف های نیمه اویلری ، تو هر راس حداکثر 2 بار DFS فراخوانی میشه .

تو بار اول یک مسیر از این رأس به راس درجه فرد دیگه پیدا میشه اما ممکنه تعدادی از یال ها هنوز استفاده نشده باشند . چون یال های استفاده شده رو حذف کردیم ، گراف باقیمونده اویلری است ، لذا با شروع از همین نقطه دور اویلری ای پیدا می کنیم .

به اضافه کردن این دور به ابتدای مسیری که قبلا پیدا کرده بودیم ، یک مسیر اویلری برای گراف پیدا کردیم .

به عنوان مثال اگر در گراف زیر از نقطه 1 شروع کنیم و به رأس 2 بریم ، مسیر اولیه پیدا شده ولی هنوز یال استفاده نشده وجود داره . لذا دوباره از 1 شروع می کنیم و دور اویلری گراف باقیمونده رو پیدا می کنیم و به اول مسیر قبلی اضافه می کنیم .

1 --- 2

| \

|   \

3 --- 4

مسیر اویلری این گراف میشه :

1 3 4 1 2


کد :

int path[N], len = 0;
void DFS( int x )
{
    for( int i = 0 ; i < N ; i ++ )
        if( mat[x][i] > 0 )
        {
            mat[x][i] --;
            mat[i][x] --;
            DFS( i );
        }
    path[ len ++ ] = x;
}


برای بدست آوردن دور(مسیر) کافیه از main تابع رو برای یه نقطه شروع خوب ( رأس دلخواه در گراف اویلری و رأس فرد در گراف نیمه اویلری ) فراخوانی کنید .

بعد از اجرای DFS ، اگر مسیر شامل E|  + 1| رأس بود ، path شامل دور(مسیر) مورد نظر است .

در غیر این صورت گراف ناهمبند است لذا دور(مسیر) اویلری نداریم .


۳ نظر موافقین ۴ مخالفین ۰ ۰۳ بهمن ۹۲ ، ۱۳:۵۳
رضا حسینی آشتیانی

سلام

DP روشی است که بر دو مبنای ، رابطه بازگشتی و نقطه شروع بنا شده .

این طور که برای حل سعی می کنیم مساله رو به زیر مساله های کوچک تر تقسیم کنیم و بعد از جواب های بدست اومده جواب مساله کلی رو بدست بیارم .

تا اینجای کار هیچ فرقی بین DP و روش "تقسیم و حل ( غلبه )" (Divide & Conquer ) نیست .

اما وجه تمایز این دو رویکرد در اینه که تو مسائل DP جواب زیر مساله هایی که حل کردیم رو نگه می داریم ( معمولا تو یه آرایه ) و بعد تو تکرار های بعدی از این مقادیر استفاده می کنیم ( برای جلوگیری از محاسبه دوباره ) .

برای اینکه این کار ما مفید باشه باید زیر مساله های تکراری داشته باشیم ، وگرنه خود D&C می تونه مساله رو حل کنه .

به عنوان مثال تابع فاکتوریل ( Factorial ) رو در نظر بگیرید .

یه همچنین تابعی میشه :

int Factorial ( int n )

{

    if( n < 2 )

        return 1;

    return n * Factorial( n-1 );

}

تو همین تابع ساده فرض کنید می خوایم 5! رو بدست بیاریم . تمام مقادیر 1! و 2! و 3! و 4! و 5! ساخته میشن و از بین میرن .

حالا اگر دوباره مثلا 6! رو بخوایم حساب کنیم ، دوباره باید همه مقادیر قبلی رو محاسبه کنیم و ...

حالا اگر مقدار 5! که قبلا محاسبه کرده بودیم رو جایی نگه می داشتیم چی ؟

برای محاسبه 6! فقط کافی بود مقداری که قبلا محاسبه کردیم رو استخراج کنیم و جواب رو بدست بیاریم . توجه داشته باشید که با این کار از 4 بار فراخوانی تابع جلوگیری کردیم .

برای مقادیر بزرگتر و مسائل پیچیده تر این کار فوق العاده کمک می کنه .

چجوری این کارو انجام بدیم ؟

تابع رو یکم تغییر می دیم :

int DP[100] = {0};

int Factorial ( int n )

{

    if( n < 2 )

        return 1;

    if( DP[n] != 0 )

        return DP[n];

    return DP[n] = n * Factorial( n-1 );

}

تو این حالت اگر DP[n] = 0 باشه ، یعنی هوز مقدارش رو حساب نکردیم پس مجبوریم تابع رو برای n-1 فراخوانی کنیم .

اگر DP[n] != 0 بود ، یعنی قبلا این تابع فراخوانی شده بوده و مقدارش حساب شده پس کافیه مقدار DP[n] رو برگردونیم . ( تو این حالت از فراخوانی زیرمساله های تکراری جلوگیری کردیم . )


توابع دیگه ای هم هستند که این شرایط رو دارند ، مثل دنباله فیبوناچی ( سعی کنید خودتون بنویسیدش ) 


تو حل مسائل DP باید برای حل تابع بازگشتی ارائه بدید . این توابع یه سری ورودی مستقل دارند و یه سری ورودی وابسته . برای اینکه بین زیر مساله های مختلف تمایز قائل بشیم باید طوری متغیر های مستقل رو انتخاب کنیم که اولا تمامی زیرمسائل رو شامل بشه و ثانیا زیر مساله ها با هم تداخل نداشته باشند .

برای ذخیره سازی زیر مسائل معمولا یه آرایه به اسم DP می گیریم که به تعداد ورودی های مستقل بُعد داره و هر بعد از اون یه اندازه بازه ای که اون ورودی می تونه تغییرات داشته باشه اندازه داره و تو هر سلول از این آرایه جواب متناظر با اون سلول قرار می گیره .

زمان مقدار دهی اولیه ، به هر سلول عددی رو نظیر می کنیم که هیچ وقت جواب تابع نباشه ( معمولا -1 )

مثلا تو تابع بالا ، فرض کردیم که مقادیر N! رو برای N < 100 نیاز داریم . پس ورودی تابع عددی بین 0 تا 99 خواهد بود و لذا اندازه DP رو 100 در نظر گرفتیم .

به عنوان مثالی پیچیده تر "ترکیب r تایی از n عضو" رو در نظر بگیریم .( n < 100 )

می دونیم این مقدار رو با ( C( n , r  نشون میدن . 

برای این مقدار رابطه بازگشتی زیر رو ارائه میدیم :

C( n , r ) = C( n-1 , r ) + C( n-1 , r-1 )

الان میبینیم که جواب مساله به دو متغیر مستقل ، وابسته شده پس برای ذخیره کردن زیرمساله ها به یه آرایه دوبعدی نیاز داریم . لذا برای محاسبه به صورت زیر عمل می کنیم .

int DP[100][100];

for( int i = 0 ; i < 100 ; i ++ )

{

    DP[i][0] = DP[i][i] = 1;

    for( int j = 1 ; j < i ; j ++ )

        DP[i][j] = DP[i-1][j] + DP[i-1][j-1];

}


همین طور میشه برای سوال های پیچیده تر این کار رو کرد .


انواع DP :

بسته به شرایط و نوع تابع بازگشتی ای که پیدا می کنیم برای سوال می تونیم از 2 تکنیک موجود استفاده کنیم .

1. روش بالا به پایین : تو این روش هر زمان که احتیاج به زیر مساله کوچکتری داشتیم ، مقدارش رو محاسبه می کنیم . این روش معمولا با تابع بازگشتی( از نظر مفهوم برنامه نویسی ) پیاده سازی میشه . برای مثال اول ( فاکتوریل ) از این تکنیک استفاده کردیم .

خوبی : این تکنیک از نظر فهمیدن و نوشتن کد ساده است .

بدی : 

کدش طولانی میشه .

چون از تابع بازگشتی استفاده می کنیم ، احتیاج به مقدار زیادی از stack داریم . برای تابع های بزرگ یا توابعی که عمق زیادی دارند نمیشه ازش استفاده کرد .( معمولا Stack Over Flow ) میشه .


اصطلاحا به تکنیکی که تو مثال اول استفاده کردیم ، Memoization یا به اختصار Memoize هم میگیم .


2. روش پایین به بالا : تو این روش شروع می کنیم از پایین با توجه به اندازه زیرمساله ها ، همه حالت هارو حساب می کنیم . می دونیم برای بدست آوردن مسائل کلی تر ، مساله رو به زیر مسائل کوچکتر تقسیم می کنیم . حالا چون بسته به اندازی کار کردیم ، حتما جواب زیر مساله ها رو داریم ( قبلا حساب کردیم ) و فقط کافیه از مقادیرشون استفاده کنیم .

تو مثال دوم ( ترکیب ) از این تکنیک استفاده کردیم .

خوبی : کدش کوتاهه و debug کردنش راحته .

بدی : مسائلی هستند که نیاز به تمام زیر مسائل کوچکتر ندارند ، فقط به بعضی از اونها نیاز دارند . تو این شرایط زمان اجرای کد طولانی میشه .


برای شروع چند یه سوال ساده از این مبحث کنید بد نیست .

سوال 11703 از سایت UVa


علاوه بر این مطلب خوندن مطالب این مقاله و حل سوالاتی که توش مطرح کرده توصیه میشه .


اگر موردی هست که به نظرتون جا افتاده یا ... بگید تا اضافه کنم .


۳ نظر موافقین ۴ مخالفین ۰ ۰۱ بهمن ۹۲ ، ۱۵:۱۷
رضا حسینی آشتیانی

ُسلام

خلاصه ای نحوه استفاده از کتابخانه های queue و stack .


به روز رسانی : مطلب مربوط به queue یه مشکل کوچیک تو قسمت back داشت که درست شد .


Queue

Stack


۰ نظر موافقین ۱ مخالفین ۰ ۱۸ آذر ۹۱ ، ۰۵:۰۷
رضا حسینی آشتیانی

سلام

مطلب مورد نظر رو می تونید از اینجا دانلود کنید .


پانوشت:

1. یه مشکل کوچیک زمان تبدیل به pdf بوجو آمده بود ، فایل جدید دوباره آپلود شد .

2. تعاریف مربوط به تابع erase ، ایراد داشت ، فایل جدید دوباره آپلود شد .


۱ نظر موافقین ۱ مخالفین ۰ ۱۸ آذر ۹۱ ، ۰۰:۱۶
رضا حسینی آشتیانی

دومین قسمت هندسه در مورد نقطه و خط کامل شد .

از ادامه مطلب می تونید دانلود کنید .

۳ نظر موافقین ۱ مخالفین ۰ ۰۴ شهریور ۹۱ ، ۰۴:۳۱
رضا حسینی آشتیانی
سلام
با توجه به نیازی که حس می شد ، قراره یه سری مطلب در مورد هندسه از مقدمات تا عالی ترین سطح اینجا قرار بدیم .
اولین قسمت در ادامه مطلب موجوده و می تونید دانلود کنید .

فایل یه کم مشکل داره ، دارم درستش می کنم .
تو اولین فرصت upload می کنمش .

منتظر نظرات شما برای ادامه کار هستم .

۷ نظر موافقین ۲ مخالفین ۰ ۳۱ مرداد ۹۱ ، ۰۲:۱۵
رضا حسینی آشتیانی