Header Ads

প্রাইম ডিভিজর এবং ফ্যাক্টরাইজেশন(Prime Divisors and Factorization)


একটি নাম্বারের প্রাইম ডিভিজর গুলো বের করব কীভাবে সেটি নিয়েই আজকের এই লেখা বা, টিউটোরিয়াল।
যেহেতু ডিভিজর এবং প্রাইম নাম্বার দেখেছি, সেহেতু আমাদের কাছে এটি তেমন কঠিন বিষয় হওয়ার কথা না। একটু ব্রুট ফোর্স পদ্ধদিতে যদি চিন্তা করে দেখি, তাহলে নাম্বার টির বর্গমূল পর্যন্ত লুপ চালিয়ে প্রথমে ডিভিজর গুলো বের করে নিয়ে তারপর সেই ডিভিজর গুলোর আবার বর্গমূল পর্যন্ত লুপ চালিয়েই প্রাইম কিনা দেখতে পারি। কিন্তু যারা ইরাটোস থেনিসের সিভ জানেন তারা হয়ত ভাবতেছেন ডিভিজর গুলোর আবার তাদের বর্গমূল পর্যন্ত আমার লুপ চালানোর কি দরকার? ইনপুটের লিমিট যদি 100000000 এর ছোট বা সমান হয় তাহলে সিভ দিয়ে আগের প্রাইম নাম্বার গুলো বের করে রেখে তারপর O(1) টাইমেই বলে দিতে পারি বর্তমান ডিভিজর টি প্রাইম না কম্পোজিট। 

সত্যিই সিভ দিয়ে আগে ইনপুট লিমিট পর্যন্ত সবগুলো নাম্বার থেকে এদের মাঝে প্রাইম নাম্বার গুলো কে যদি আগে মার্ক করে রাখতে পারি তাহলে কিন্তু আমাদের কাজ অনেক কমে যাচ্ছে। এর জন্যে প্রাইম নাম্বার গুলো আলাদা অ্যারে তে নেওয়ার ও প্রয়োজন নেই। মার্ক অ্যারেই যথেষ্ট আমাদের জন্য।



const int N = 1e8;


bool mark[N+1];


void sieve() {

  mark[0] = true;

  mark[1] = true;

  for (int i = 4;i <= N;i += 2) {

    mark[i] = true;

  }

  int lim = sqrt(N);

  for (int i = 3;i <= lim;i += 2) {

    if (mark[i] == false) {

      for (int j = i*i;j <= N;j += i+i) {

        mark[j] = true;

      }

    }

  }

}


void find_prime_div(int n) {

  // i*i <= n will make sure that i will be

  // not greater then square root of n

  for (int i = 1;i*i <= n;i++) {

    if (n%i==0) {

      if (mark[i]==false) { // if mark[x] is false then x is prime

        cout << i << ' ';

      }

      if (n/i != i) { // if i is not square root of n

        if (mark[n/i]==false) {

          cout << n/i << ' ';

        }

      }

    }

  }

  cout << '\n';

}


দুঃখজনক ভাবে আমাদের এই অ্যালগরিদমেও দুইটি বড় সমস্যা আছে। প্রথম সমস্যা হচ্ছে ইনপুট যদি 100000000 (1e8) এর থেকে বেশি হয় তাহলে আমাদের আগের নিয়মেই ফিরে যেতে হচ্ছে। দ্বিতীয় ও মূল সমস্যা হচ্ছে 100000 (1e5) সাইজের একটি অ্যারে যদি 100000000 (1e8) লিমিটের সবগুলো নাম্বারের প্রাইম ডিভিজর বের করতে বলে তাহলে অনেক সময় লেগে যাবে।

এখন যে নিয়ম টি দেখব তাকে বলা হয় প্রাইম ফ্যাক্টরাইজেশন। এটি কীভাবে কাজ করে? প্রথমে সবথেকে ছোট প্রাইম মানে 2 থেকে শুরু করব। 2 দিয়ে যতক্ষণ ভাগ যাবে ততক্ষণ ভাগ করতেই থাকব যেমন, n%2==0 then, n = n / 2; এক সময় 2 দ্বারা ভাগ করা যাবে না তখন n বিজোড় হবে। এখন, পরবর্তী প্রাইম 3 দিয়ে চেষ্টা করব। এভাবে n এর বর্গমূল পর্যন্ত চেষ্টা করব। n যদি 1 এর সমান হয়ে যায় অথবা বর্তমান প্রাইম নাম্বারটি যদি n এর বর্গমূল থেকে বড় হয়ে যায় তাহলে আমাদের কাজ শেষ। বর্তমান প্রাইম তখন ই বড় হবে যখন n এর ভ্যালু 1 বা n নিজেই প্রাইম হবে। আচ্ছা কিছু সংখ্যার জন্যে কাজ টি দেখে নেই তাহলে আরো পরিস্কার হবে।

n এর ভ্যালু যদি 12 হয়,
12 = 2 X 6
6 = 2 X 3
3 = 3 X 1
12 = 2 X 2 X 3
n এর ভ্যালু যদি 30 হয়, 
30 = 2 X 15
15 = 3 X 5
5 = 5 X 1
30 = 2 X 3 X 5
n এর ভ্যালু যদি 90 হয়,
90 = 2 X 45
45 = 3 X 15
15 = 3 X 5
5 = 5 X 1
90 = 2 X 3 X 3 X 5

নিচের ছবিটি দেখলে আশাকরি আরো ভালো করে বুঝা যাবে। আর আমার মত ফাঁকিবাজ রা এটাও বুঝতে পারবে যে এটা আসলে হাই স্কুল এর সাধারণ গণিত :| 

প্রাইম ফ্যাক্টরাইজেশন


এভাবে, আমরা প্রাইম ডিভিজর গুলো পেয়ে যেতে পারি। এখন হয়ত আমার মত আপনারো প্রশ্ন আসতে পারে, এটা কীভাবে কাজ করলো? এটা কী সব নাম্বারের জন্যেই সঠিক আউটপুট দিবে? উত্তর হ্যাঁ। আর এটা কীভাবে কাজ করলো তা একটু বলার চেষ্টা করি। n যদি 2 দিয়ে ভাগ হয় তাহলে খেয়াল করলে দেখবেন n ছাড়া n এর সবথেকে বড় ডিভিজর কখনোই n/2 এর থেকে বড় হবে না। তাহলে n = n/2 নিয়ে বাকি নাম্বার গুলো বাতিল করে দিয়ে n কে ছোট করে নিতে পারি। একই ভাবে বর্তমান n যদি এখন 3 দিয়ে ভাগ করা যায় তাহলে n ছাড়া n এর সবথেকে বড় ডিভিজর n/3 এর থেকে বড় হবে না। যদি বড় থাকেও তাহলে সেটি আগের প্রাইম দিয়ে কাটা হয়ে গেছে এবং সেটি কোনভাবেই প্রাইম নয়। আমাদের দরকার প্রাইম ডিভিজর। আবার কাটা না পড়ে থাকলে তাহলে বর্তমান প্রাইমের আগের গুলো দিয়ে সেটি বিভাজ্য নয়। যেমন 65 কে যদি ধরি, তাহলে 65 কে বাদ দিলে 65 এর সবথেকে বড় ডিভিজর 13 যা 5 দিয়ে ভাগ করার ফলে হয়েছে। এবং 5 এর আগের কোন প্রাইম দিয়েই 65 বিভাজ্য নয়। ভালো করলে খেয়াল করলে দেখবেন এই কাজ গুলো সিভের আইডিয়া থেকেই এসেছে।



const int N = 1e8;


vector <int> p;

bool mark[N+1];


void sieve() {

  mark[0] = true;

  mark[1] = true;

  for (int i = 4;i <= N;i += 2) {

    mark[i] = true;

  }

  p.push_back(2);

  int lim = sqrt(N);

  for (int i = 3;i <= lim;i += 2) {

    if (mark[i] == false) {

      p.push_back(i);

      for (int j = i*i;j <= N;j += i+i) {

        mark[j] = true;

      }

    }

  }

}


vector <int> p_d;


void find_prime_div(int n) {

  for (int i = 0;i < p.size() && p[i]*p[i] <= n;i++){

    // if current n is prime then stop

    if (mark[n]==false) break;

    if (n%p[i] == 0) {

      // current prime divides n then divide n

      // until current n is not divisible by

      // current prime and store current prime

      while (n%p[i] == 0) {

        p_d.push_back(p[i]);

        n /= p[i];

      }

    }

  }


  // if n is greater than 1 then n must be prime

  if (n > 1)p_d.push_back(n);


  // printing all prime factor

  // printing all prime factor

  for (int i = 0;i < p_d.size();i++) {

    cout << p_d[i] << ' ';

  }cout << '\n';

}


এবার আমাদের প্রাইম নাম্বার গুলো আলাদা একটি অ্যারেতে নিয়ে নিতে হয়েছে। তবে 100000000 (1e8) এর জন্য সবগুলো প্রাইম নেওয়ার প্রয়োজন নেই। N এর বর্গমূল পর্যন্ত নিলেই হয়ে যাবে। এরপর find_prime_div(); ফাংশনে বর্তমান n এর বর্গমূল পর্যন্ত যাচ্ছি এবং যে প্রাইম দিয়ে ভাগ যাচ্ছে তাকে দিয়ে ভাগ করে n কে আবার ছোট করে নিচ্ছি। বর্তমান n যদি আবার প্রাইম হয় তাহলে কাজ এখানেই শেষ। আমাদের আবার শুধু বর্তমান n এর বর্গমূল পর্যন্ত যাওয়ার প্রয়োজন নেই। 

আচ্ছা আমরা চেয়েছিলাম শুধু প্রাইম ডিভিজর গুলো, কিন্তু এখানে একই প্রাইম একাধিকবার দেখাচ্ছি। এর কারণ হচ্ছে প্রাইম ফ্যাক্টরাইজ বুঝার জন্য। আপনি আপনার প্রয়োজন মত শুধু প্রাইম ডিভিজর গুলো একবার করে দেখাতে পারেন। যেহেতু n পর্যন্ত সব গুলো প্রাইম কে মার্ক করে রাখছি তাই sieve(); ফাংশনের টাইম কমপ্লেক্সিটি হবে O(N log(N)); এর থেকেও কম বা O(N log(log(N))); এবং find_prime_div(); এর কমপ্লেক্সিটি O(log(n));।

আমরা এইভাবে প্রাইম ফ্যাক্টরাইজ করে 10000000000000000 (1e16) পর্যন্ত নাম্বারের জন্যেও প্রাইম ডিভিজর গুলো বের করে ফেলতে পারব। তবে এই প্রোগ্রামে একটু সমস্যা আছে। 100000000 (1e8) এর উপরে গেলেই আমি মার্ক অ্যারে ব্যবহার করতে পারব না। তবে চিন্তা নেই কারণ যেহেতু আমার N এর বর্গমূল পর্যন্ত প্রাইম গুলোই শুধু লাগছে তাই মার্ক অ্যারেতে N এর বর্গমূল এর থেকে বেশি ভেরিয়েবল লাগবে না। এবার sieve(); ফাংশনের কমপ্লেক্সিটি হবে O(sqrt(N) log(log(sqrt(N))));। তবে, find_prime_div(); ফাংশন টি এবার সবথেকে খারাপ কেইস মানে ইনপুট নিজেই প্রাইম হলে কমপ্লেক্সিটি প্রায় O(sqrt(n)); কাছাকাছি চলে যাবে।



vector <long long> p_d;


void find_prime_div(long long n) {

  for (int i = 0;i < p.size() && p[i]*p[i] <= n;i++){

    long long cur = p[i];

    // if current prime is greater then sqrt(n) then stop

    if (cur*cur > n)break;

    if (n%cur == 0) {

      // current prime divides n then divide n

      // until current n is not divisible by

      // current prime and store current prime

      while (n%p[i] == 0) {

        p_d.push_back(p[i]);

        n /= p[i];

      }

    }

  }


  // if n is greater than 1 then n must be prime

  if (n > 1)p_d.push_back(n);


  // printing all prime factor

  // printing all prime factor

  for (int i = 0;i < p_d.size();i++) {

    cout << p_d[i] << ' ';

  }cout << '\n';

}


এবার লিমিট বড় তাই long long টাইপ ব্যবহার করতে হচ্ছে। আর শুধু আগের কোডে মার্ক অ্যারে ব্যবহার করেছিলাম বর্তমান n প্রাইম হলে সাথে সাথে কাজ শেষ করে দেওয়ার জন্যে। কিন্তু এখন তা পারছি না লিমিট বড় হয়ে যাওয়ার জন্যে। আশাকরি sieve(); ফাংশন টি নিজে নিজেই ইমপ্লিমেন্ট করে নিতে পারবেন।

এই বিষয়ে আরো টিউটোরিয়াল।
টিউটোরিয়াল ১
টিউটোরিয়াল ২
টিউটোরিয়াল ৩
টিউটোরিয়াল ৪

No comments

Powered by Blogger.