পুচ্ছ পুনরাবৃত্তি ঠিক কীভাবে কাজ করে?


121

আমি প্রায় বুঝতে পারি যে টেল রিকার্সন কীভাবে কাজ করে এবং এর মধ্যে পার্থক্য এবং একটি সাধারণ পুনরাবৃত্তি। আমি কেবল বুঝতে পারি না কেন এটির ফেরতের ঠিকানা মনে রাখার জন্য স্ট্যাকের প্রয়োজন হয় না

// tail recursion
int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

int factorial (int n) {
    return fac_times (n, 1);
}

// normal recursion
int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

একটি পুচ্ছ পুনরাবৃত্তি ফাংশন নিজেই একটি ফাংশন কল করার পরে কিছুই করার নেই তবে এটি আমার পক্ষে লাভজনক নয়।


16
লেঙ্গুড় পুনরাবৃত্তির হয় "স্বাভাবিক" পুনরাবৃত্তির। এর কেবলমাত্র অর্থ হ'ল ফাংশনের শেষে পুনরাবৃত্তি ঘটে।
পিট বেকার

7
... তবে স্ট্যাকের গভীরতা হ্রাস করে এটি সাধারণ পুনরাবৃত্তির চেয়ে আইএল স্তরে ভিন্ন উপায়ে প্রয়োগ করা যেতে পারে।
কিথস

2
বিটিডাব্লু, জিসিসি এখানে "সাধারণ" উদাহরণে লেজ পুনরাবৃত্তি নির্মূল করতে পারে।
dmckee --- প্রাক্তন-মডারেটর বিড়ালছানা

1
@ গীক - আমি একটি সি # দেব, সুতরাং আমার "সমাবেশের ভাষা" এমএসআইএল বা কেবল আইএল। সি / সি ++ এর জন্য, এএসএম দিয়ে আইএল প্রতিস্থাপন করুন।
কিথস

1
@ শ্যাননসিয়েভারেন্স আমি দেখতে পেলাম যে সিসি সহজে ছাড়াই নির্গত সমাবেশ কোডটি পরীক্ষা করে সহজ সমীক্ষক দ্বারা এটি করছে -O3। লিঙ্কটি পূর্বের আলোচনার জন্য যা খুব অনুরূপ স্থলটি কভার করে এবং এই অপটিমাইজেশন বাস্তবায়নের জন্য প্রয়োজনীয় কী তা নিয়ে আলোচনা করে।
ডিএমকেকে --- প্রাক্তন মডারেটর বিড়ালছানা

উত্তর:


169

সংকলকটি কেবল এটি রূপান্তর করতে সক্ষম

int fac_times (int n, int acc) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

এরকম কিছুতে:

int fac_times (int n, int acc) {
label:
    if (n == 0) return acc;
    acc *= n--;
    goto label;
}

2
@ মিঃ 32 আমি আপনার প্রশ্নটি বুঝতে পারি না। আমি ফাংশনটিকে সমতুল্য রূপে রূপান্তর করেছি তবে সুস্পষ্ট পুনরাবৃত্তি ছাড়াই (এটি সুস্পষ্ট ফাংশন কল ছাড়াই)। আপনি যদি যুক্তিটিকে অ-সমতুল্য কিছুতে পরিবর্তন করেন তবে আপনি কিছু বা সমস্ত ক্ষেত্রে ফাংশনটি লুপটি করে দিতে পারেন।
আলেক্সি ফ্রুঞ্জ

18
সুতরাং লেজ পুনরাবৃত্তি শুধুমাত্র সংকলক এটি অপ্টিমাইজ করার কারণে কার্যকর ? এবং অন্যথায় এটি স্ট্যাক মেমরি ওয়াইসের ক্ষেত্রে সাধারণ পুনরাবৃত্তির সমান হবে?
অ্যালান করোমানো

34
হাঁ। সংকলক যদি লুপটিতে পুনরাবৃত্তি হ্রাস করতে না পারে তবে আপনি পুনরাবৃত্তি নিয়ে আটকে যাচ্ছেন। সব অথবা কিছুই না.
অ্যালেক্সি ফ্রুঞ্জ

3
@ অ্যালানডার্ট: সঠিক আপনি লেজ পুনরাবৃত্তিটিকে "টেল কল অপটিমাইজেশন" এর একটি বিশেষ কেস হিসাবে বিবেচনা করতে পারেন, বিশেষ কারণ লেজ কলটি একই ফাংশনটিতে ঘটে। সাধারণভাবে, যে কোনও লেজ কল (যেমন "কোনও কাজ বাকী থাকবে না" তেমন প্রয়োজনীয়তার সাথে লেজ পুনরাবৃত্তির ক্ষেত্রে প্রযোজ্য, এবং যেখানে লেজ কলটির রিটার্ন মানটি সরাসরি ফিরে আসে) কম্পাইলার যদি কলটি করতে পারে তবে অনুকূলিত করা যেতে পারে লেজ কলটি করা ঠিকানার পরিবর্তে, লেজ কল করা ফাংশনের রিটার্ন ঠিকানা হিসাবে কল করা ফাংশনের রিটার্ন ঠিকানা সেট করে way
স্টিভ জেসোপ

1
সি এ্যালানডার্টটি এটি কেবলমাত্র একটি অপ্টিমাইজেশন যা কোনও স্ট্যান্ডার্ড দ্বারা প্রয়োগ করা হয় না, সুতরাং পোর্টেবল কোড এটির উপর নির্ভর করে না। তবে ভাষা রয়েছে (স্কিম একটি উদাহরণ), যেখানে লেজ পুনরাবৃত্তি অপ্টিমাইজেশন মান দ্বারা প্রয়োগ করা হয়, সুতরাং আপনার কিছু চিন্তা করার দরকার নেই যে এটি কিছু পরিবেশে ওভারফ্লোতে স্ট্যাক করবে।
জান ভ্রোবেল

57

আপনি কেন জিজ্ঞাসা করেন "এটির ফিরে আসার ঠিকানাটি মনে রাখার জন্য স্ট্যাকের প্রয়োজন নেই"।

আমি এটিকে ঘুরিয়ে দিতে চাই এটা তোলে করেন ফিরতি ঠিকানা মনে রাখতে স্ট্যাক ব্যবহার করুন। কৌশলটি হ'ল যে ফাংশনে লেজ পুনরাবৃত্তি ঘটে তার স্ট্যাকের নিজস্ব রিটার্ন ঠিকানা থাকে এবং যখন এটি ডাকা ফাংশনে লাফিয়ে যায়, এটি এটি তার নিজস্ব ফেরতের ঠিকানা হিসাবে বিবেচনা করবে।

কংক্রিটলি, লেজ কল অপ্টিমাইজেশন ছাড়াই:

f: ...
   CALL g
   RET
g:
   ...
   RET

এই ক্ষেত্রে, যখন gডাকা হবে, স্ট্যাকটি দেখতে পাবেন:

   SP ->  Return address of "g"
          Return address of "f"

অন্যদিকে, লেজ কল অপ্টিমাইজেশন সহ:

f: ...
   JUMP g
g:
   ...
   RET

এই ক্ষেত্রে, যখন gডাকা হবে, স্ট্যাকটি দেখতে পাবেন:

   SP ->  Return address of "f"

স্পষ্টতই, gফিরে আসার পরে, এটি সেই জায়গায় ফিরে আসবে যেখানে fথেকে ফোন করা হয়েছিল।

সম্পাদনা : উপরের উদাহরণটি এমন কেসটি ব্যবহার করে যেখানে একটি ফাংশন অন্য ফাংশনকে ডাকে। যখন ফাংশনটি নিজেকে কল করে তখন প্রক্রিয়াটি অভিন্ন।


8
এটি অন্যান্য উত্তরের চেয়ে অনেক ভাল উত্তর। সংকলকটি সম্ভবত টেল রিকার্সিভ কোডটি রূপান্তর করার জন্য কিছু জাদুকরী বিশেষ কেস রাখে না। এটি কেবলমাত্র একটি সাধারণ শেষ কল অপটিমাইজেশন সম্পাদন করে যা একই ফাংশনে যাওয়ার জন্য ঘটে।
আর্ট

12

লেজ পুনরাবৃত্তি সাধারণত সংকলক দ্বারা একটি লুপে রূপান্তরিত হতে পারে, বিশেষত যখন সংগ্রহকারী ব্যবহার করা হয়।

// tail recursion
int fac_times (int n, int acc = 1) {
    if (n == 0) return acc;
    else return fac_times(n - 1, acc * n);
}

মত কিছু সংকলন হবে

// accumulator
int fac_times (int n) {
    int acc = 1;
    while (n > 0) {
        acc *= n;
        n -= 1;
    }
    return acc;
}

3
আলেক্সির প্রয়োগের মতো চতুর নয় ... এবং হ্যাঁ এটি একটি প্রশংসা।
ম্যাথিউ এম।

1
প্রকৃতপক্ষে, ফলাফলটি সহজ দেখাচ্ছে তবে আমি মনে করি এই রূপান্তরটি বাস্তবায়নের কোডটি লেবেল / গোটো বা কেবল টেল কল নির্মূলকরণের চেয়ে অনেক বেশি "চালাক" হবে (লিন্ডাইড্যান্সারের উত্তর দেখুন)।
ফোব

যদি এটি সমস্ত লেজ পুনরাবৃত্তি হয়, তবে লোকেরা কেন এটি সম্পর্কে এত উত্তেজিত হয়? লুপ করার সময় কাউকে দেখে উত্তেজিত হতে দেখছি না।
বুহ বুহ

@ বুহবু: এটিতে স্ট্যাকওভারফ্লো নেই এবং স্ট্যাকের ধাক্কা / পরামিতিগুলি পপিং এড়ানো হয়। এর মতো টাইট লুপের জন্য এটি একটি বিশ্বকে আলাদা করতে পারে। তা ছাড়া লোকদের উত্তেজিত করা উচিত নয়।
হাঁস মুচিং

11

দুটি উপাদান রয়েছে যা অবশ্যই পুনরাবৃত্তির কার্যক্রমে উপস্থিত থাকতে পারে:

  1. পুনরাবৃত্তি কল
  2. প্রত্যাবর্তনের মান গণনা রাখার জন্য একটি জায়গা।

একটি "নিয়মিত" রিকার্সিভ ফাংশন (2) স্ট্যাক ফ্রেমে রাখে।

নিয়মিত রিকার্সিভ ফাংশনে রিটার্ন মানগুলি দুটি ধরণের মান নিয়ে গঠিত:

  • অন্যান্য ফেরতের মান
  • নিজস্ব ফাংশন গণনার ফলাফল

আসুন আপনার উদাহরণটি দেখুন:

int factorial (int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

উদাহরণস্বরূপ, ফ্রেম চ (5) তার নিজস্ব গণনার ফলাফল (5) "সঞ্চয়" করে এবং f (4) এর মান। যদি আমি স্ট্যাক্টরিয়াল (5) কল করি, স্ট্যাক কলগুলি ক্ষয় হওয়া শুরু হওয়ার ঠিক আগে, আমার কাছে রয়েছে:

 [Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]

লক্ষ্য করুন যে প্রতিটি স্ট্যাকের মধ্যে আমি উল্লেখ করা মানগুলি ছাড়াও ফাংশনের পুরো সুযোগ রয়েছে। সুতরাং, পুনরাবৃত্তি ফাংশনটির জন্য মেমরির ব্যবহার হ'ল ও (এক্স), যেখানে আমার করা পুনরাবৃত্ত কলগুলির সংখ্যা x is সুতরাং, ফ্যাকটোরিয়াল (1) বা ফ্যাকটোরিয়াল (2) গণনা করার জন্য যদি আমার 1 কেবি র‌্যামের প্রয়োজন হয়, তবে ফ্যাকটোরিয়াল (100) গণনা করার জন্য আমার 100k ডলার প্রয়োজন।

এর আর্গুমেন্টগুলিতে একটি টেল রিকার্সিভ ফাংশন (2) রাখে।

একটি টেল রিকার্সনে, আমি প্রতিটি পুনরাবৃত্ত ফ্রেমের আংশিক গণনার ফলাফলটি প্যারামিটারগুলি ব্যবহার করে পরবর্তী একটিতে পাস করি। আসুন আমাদের কল্পিত উদাহরণটি দেখুন, টেল রিকার্সিভ:

int ফ্যাক্টরিয়াল (int n) {int helper (int num, int સંચিত) num যদি num == 0 রিটার্ন জমা হয় অন্য রিটার্ন সহায়ক (সংখ্যা - 1, সঞ্চিত * সংখ্যা) num রিটার্ন সহায়ক (n, 1)
}

আসুন এর ফ্রেমগুলি ফ্যাক্টরিয়াল (4) এ দেখুন:

[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]

পার্থক্য দেখুন? "নিয়মিত" পুনরাবৃত্ত কলগুলিতে রিটার্ন ফাংশনগুলি পুনরাবৃত্তভাবে চূড়ান্ত মানটি রচনা করে। টেল রিকার্সনে তারা কেবল বেস কেসটি উল্লেখ করে (শেষেরটি মূল্যায়ন করা হয়) । আমরা সংগ্রহকারীকে আর্গুমেন্ট বলি যা পুরানো মানগুলি ট্র্যাক করে।

পুনরাবৃত্তি টেমপ্লেট

নিয়মিত পুনরাবৃত্তি ফাংশন নিম্নলিখিত হিসাবে যান:

type regular(n)
    base_case
    computation
    return (result of computation) combined with (regular(n towards base case))

এটি একটি টেল পুনরাবৃত্তিতে রূপান্তর করতে আমরা:

  • একটি সহায়ক ফাংশন পরিচয় করান যা সঞ্চালককে বহন করে
  • বেস কেসে অ্যাকসুমুলেটর সেট করে মূল ফাংশনের ভিতরে সহায়ক ফাংশনটি চালান run

দেখুন:

type tail(n):
    type helper(n, accumulator):
        if n == base case
            return accumulator
        computation
        accumulator = computation combined with accumulator
        return helper(n towards base case, accumulator)
    helper(n, base case)

পার্থক্যটা দেখ?

টেইল কল অপ্টিমাইজেশন

যেহেতু কোনও রাজ্যই টেল কল স্ট্যাকের অ-সীমান্ত-কেসগুলিতে সংরক্ষণ করা হচ্ছে না, সেগুলি এত গুরুত্বপূর্ণ নয়। কিছু ভাষা / দোভাষী তারপরে নতুনটির সাথে পুরানো স্ট্যাকটি প্রতিস্থাপন করে। সুতরাং, কলগুলির সংখ্যা সীমাবদ্ধ করে কোনও স্ট্যাক ফ্রেম ছাড়াই, টেল কলগুলি এই ক্ষেত্রে ঠিক লুপের মতো আচরণ করে

এটি অপ্টিমাইজ করা আপনার সংকলকের উপর নির্ভর করে, না।


6

এখানে একটি সাধারণ উদাহরণ যা পুনরাবৃত্ত ফাংশনগুলি কীভাবে কাজ করে তা দেখায়:

long f (long n)
{

    if (n == 0) // have we reached the bottom of the ocean ?
        return 0;

    // code executed in the descendence

    return f(n-1) + 1; // recurrence

    // code executed in the ascendence

}

টেল রিকার্সন একটি সাধারণ পুনরাবৃত্তি ফাংশন, যেখানে পুনরাবৃত্তিটি ফাংশন শেষে করা হয়, সুতরাং কোনও কোড আরোহীতে করা হয় না, যা উচ্চ স্তরের প্রোগ্রামিং ভাষার বেশিরভাগ সংকলককে টেল রিকার্সন অপটিমাইজেশন নামে পরিচিত যা করতে সাহায্য করে, এর একটিও রয়েছে টেল রিকার্সন মডুলো হিসাবে পরিচিত আরও জটিল অপ্টিমাইজেশন


1

পুনরাবৃত্তি ফাংশন একটি ফাংশন যা নিজেই কল করে

এটি প্রোগ্রামারদের ন্যূনতম পরিমাণ কোড ব্যবহার করে দক্ষ প্রোগ্রাম লেখার অনুমতি দেয় ।

ক্ষয়ক্ষতিটি হ'ল তারা যথাযথভাবে লেখা না থাকলে অসীম লুপ এবং অন্যান্য অপ্রত্যাশিত ফলাফলের কারণ হতে পারে

আমি সাধারণ পুনরাবৃত্তি ফাংশন এবং টেল রিকার্সিভ ফাংশন উভয়ই ব্যাখ্যা করব

একটি সাধারণ পুনরাবৃত্তির কাজ লিখতে যাতে

  1. বিবেচনা করার প্রথম বিষয়টি হ'ল লুপটি কখন লুপ থেকে বেরিয়ে আসার সিদ্ধান্ত নেবে
  2. দ্বিতীয়টি হ'ল আমরা আমাদের নিজস্ব ফাংশন হলে কী প্রক্রিয়া করব

প্রদত্ত উদাহরণ থেকে:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

উপরের উদাহরণ থেকে

if(n <=1)
     return 1;

লুপ থেকে প্রস্থান করার সময় সিদ্ধান্ত নেওয়ার কারণ

else 
     return n * fact(n-1);

আসল প্রক্রিয়াজাতকরণ করা

সহজ বোঝার জন্য আমাকে একের পর এক কাজটি বিরতি দিন।

আমি দৌড়ে গেলে অভ্যন্তরীণভাবে কী ঘটে তা দেখা যাক fact(4)

  1. প্রতিস্থাপন n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifলুপ ব্যর্থ হয় তাই এটি elseলুপ যায় তাই এটি ফিরে4 * fact(3)

  1. স্ট্যাক মেমরি, আমরা আছে 4 * fact(3)

    প্রতিস্থাপন n = 3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifলুপ ব্যর্থ তাই এটি elseলুপ যায়

সুতরাং এটি ফিরে আসে 3 * fact(2)

মনে রাখবেন আমরা called `` 4 * ফ্যাক্ট (3) `` বলেছি `

জন্য আউটপুট fact(3) = 3 * fact(2)

এখনও পর্যন্ত স্ট্যাক আছে 4 * fact(3) = 4 * 3 * fact(2)

  1. স্ট্যাক মেমরি, আমরা আছে 4 * 3 * fact(2)

    প্রতিস্থাপন n = 2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifলুপ ব্যর্থ তাই এটি elseলুপ যায়

সুতরাং এটি ফিরে আসে 2 * fact(1)

মনে আছে আমরা ডেকেছি 4 * 3 * fact(2)

জন্য আউটপুট fact(2) = 2 * fact(1)

এখনও পর্যন্ত স্ট্যাক আছে 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. স্ট্যাক মেমরি, আমরা আছে 4 * 3 * 2 * fact(1)

    প্রতিস্থাপন n = 1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If লুপ সত্য

সুতরাং এটি ফিরে আসে 1

মনে আছে আমরা ডেকেছি 4 * 3 * 2 * fact(1)

জন্য আউটপুট fact(1) = 1

এখনও পর্যন্ত স্ট্যাক আছে 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

শেষ অবধি, ফলাফলের ফলাফল (4) = 4 * 3 * 2 * 1 = 24

এখানে চিত্র বর্ণনা লিখুন

লেঙ্গুড় Recursion হবে

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}
  1. প্রতিস্থাপন n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifলুপ ব্যর্থ হয় তাই এটি elseলুপ যায় তাই এটি ফিরেfact(3, 4)

  1. স্ট্যাক মেমরি, আমরা আছে fact(3, 4)

    প্রতিস্থাপন n = 3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifলুপ ব্যর্থ তাই এটি elseলুপ যায়

সুতরাং এটি ফিরে আসে fact(2, 12)

  1. স্ট্যাক মেমরি, আমরা আছে fact(2, 12)

    প্রতিস্থাপন n = 2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifলুপ ব্যর্থ তাই এটি elseলুপ যায়

সুতরাং এটি ফিরে আসে fact(1, 24)

  1. স্ট্যাক মেমরি, আমরা আছে fact(1, 24)

    প্রতিস্থাপন n = 1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If লুপ সত্য

সুতরাং এটি ফিরে আসে running_total

জন্য আউটপুট running_total = 24

অবশেষে, ফলাফলের ফলাফল (4,1) = 24

এখানে চিত্র বর্ণনা লিখুন


0

আমার উত্তরটি আরও অনুমান, কারণ পুনরাবৃত্তি অভ্যন্তরীণ বাস্তবায়নের সাথে সম্পর্কিত।

লেজ পুনরাবৃত্তিতে পুনরাবৃত্ত ফাংশনটিকে একই ফাংশনের শেষে ডাকা হয়। সম্ভবত সংকলক নীচের উপায়ে অপ্টিমাইজ করতে পারে:

  1. চলমান ফাংশনটি চলতে দিন (যেমন ব্যবহৃত স্ট্যাকটি আবার স্মরণ করা হয়)
  2. অস্থায়ী স্টোরেজে ফাংশনের আর্গুমেন্ট হিসাবে ব্যবহৃত হতে চলেছে এমন ভেরিয়েবলগুলি সংরক্ষণ করুন
  3. এর পরে অস্থায়ীভাবে সঞ্চিত যুক্তি দিয়ে ফাংশনটি আবার কল করুন

আপনি দেখতে পাচ্ছেন, একই ফাংশনের পরবর্তী পুনরাবৃত্তির আগে আমরা মূল ফাংশনটি ঘুরিয়ে দিচ্ছি, সুতরাং আমরা স্ট্যাকটি আসলে "ব্যবহার" করছি না।

তবে আমি বিশ্বাস করি যদি ফাংশনের অভ্যন্তরে যদি ধ্বংসকারীদের ডেকে আনা হয় তবে এই অপ্টিমাইজেশন প্রয়োগ নাও হতে পারে।


0

সংকলক লেজ পুনরাবৃত্তি বুঝতে যথেষ্ট বুদ্ধিমান case ক্ষেত্রে, পুনরাবৃত্তি কল থেকে ফিরে যখন, কোন অপেক্ষারত অপারেশন নেই এবং পুনরাবৃত্তি কল শেষ বিবৃতি, পুচ্ছ পুনরাবৃত্তি বিভাগে আসা। সংকলক মূলত লেজ পুনরাবৃত্তি অপ্টিমাইজেশান সম্পাদন করে স্ট্যাক প্রয়োগটি সরিয়ে দেয় code কোডের নীচে বিবেচনা করুন।

void tail(int i) {
    if(i<=0) return;
    else {
     system.out.print(i+"");
     tail(i-1);
    }
   }

অপ্টিমাইজেশান সম্পাদন করার পরে, উপরের কোডটি নীচে একটিতে রূপান্তরিত হয়।

void tail(int i) {
    blockToJump:{
    if(i<=0) return;
    else {
     system.out.print(i+"");
     i=i-1;
     continue blockToJump;  //jump to the bolckToJump
    }
    }
   }

টাইল রিকার্সন অপটিমাইজেশন এইভাবে সংকলক করে।

আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.