পাইথনে ট্রাই কীভাবে তৈরি করা যায়


124

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

  • ট্রাই কি নেস্টেড ডিকশনারিগুলির একটি অবজেক্ট হওয়া উচিত? যেখানে প্রতিটি অক্ষর বর্ণগুলিতে বিভক্ত হয় তাই ইত্যাদি?
  • যদি 100k বা 500k এন্ট্রি থাকে তবে এই জাতীয় অভিধানে করা একটি অনুসন্ধান দ্রুত হবে?
  • একাধিক শব্দের -বা স্পেসের সাথে পৃথক হওয়া ওয়ার্ড-ব্লকগুলি কীভাবে কার্যকর করা যায়?
  • কীভাবে কোনও শব্দের উপসর্গ বা প্রত্যয়কে কাঠামোর অন্য অংশের সাথে যুক্ত করবেন? (DAWG এর জন্য)

কীভাবে একটি তৈরি করতে এবং ব্যবহার করতে হয় তা নির্ধারণ করার জন্য আমি সেরা আউটপুট কাঠামোটি বুঝতে চাই ।

ট্রাই সহ ডিএডাব্লুজি এর আউটপুট কী হওয়া উচিত তাও আমি প্রশংসা করব ।

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


5
পাইথনের বহিরাগত ডেটা স্ট্রাকচারের সমীক্ষার জন্য kmike.ru/python-data-structures পড়ুন
কর্নেল আতঙ্ক

উত্তর:


161

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

>>> _end = '_end_'
>>> 
>>> def make_trie(*words):
...     root = dict()
...     for word in words:
...         current_dict = root
...         for letter in word:
...             current_dict = current_dict.setdefault(letter, {})
...         current_dict[_end] = _end
...     return root
... 
>>> make_trie('foo', 'bar', 'baz', 'barz')
{'b': {'a': {'r': {'_end_': '_end_', 'z': {'_end_': '_end_'}}, 
             'z': {'_end_': '_end_'}}}, 
 'f': {'o': {'o': {'_end_': '_end_'}}}}

আপনি যদি এর সাথে পরিচিত না setdefaultহন তবে এটি অভিধানে একটি কী (এখানে, letterবা _end) সন্ধান করে। কীটি উপস্থিত থাকলে, এটি সম্পর্কিত মানটি প্রদান করে; যদি তা না হয় তবে এটি কীটিতে একটি ডিফল্ট মান নির্ধারণ করে এবং মান ( {}বা _end) প্রদান করে। (এটি একটি সংস্করণের মতোget আপডেট করে))

এর পরে, শব্দটি ট্রাইতে রয়েছে কিনা তা পরীক্ষা করার জন্য একটি ফাংশন:

>>> def in_trie(trie, word):
...     current_dict = trie
...     for letter in word:
...         if letter not in current_dict:
...             return False
...         current_dict = current_dict[letter]
...     return _end in current_dict
... 
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'baz')
True
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'barz')
True
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'barzz')
False
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'bart')
False
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'ba')
False

আমি অনুশীলন হিসাবে আপনার কাছে সন্নিবেশ এবং অপসারণটি ছেড়ে দেব।

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

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


1
সেখানে, পরিবর্তন করা হয়েছে। আমি dict.setdefault()এটির সাথে আটকে থাকব (এটি নিম্নরূপে ব্যবহৃত হয়েছে এবং এটি যথেষ্ট পরিমাণে সুপরিচিত নয়) কারণ এটি এমন কোনও বাগগুলি প্রতিরোধ করতে সহায়তা করে যা খুব সহজেই একটি defaultdict(যেখানে আপনি KeyErrorইনডেক্সিংয়ের ক্ষেত্রে অ-বিদ্যমান কীগুলির জন্য একটি পেতে পারেন না) দিয়ে তৈরি করা খুব সহজ prevent প্রোডাকশন কোডের জন্য এখন কেবলমাত্র এটি ব্যবহারযোগ্য হয়ে উঠবে তা হ'ল _end = object():-)
মার্টিজন পিটার

@ মার্তিজনপিটারস হুম, আমি বিশেষভাবে অবজেক্ট ব্যবহার না করা বেছে নিয়েছি, তবে কেন তা মনে করতে পারছি না। সম্ভবত কারণ ডেমোতে দেখা গেলে এটি ব্যাখ্যা করা শক্ত হবে? আমি অনুমান করি যে আমি একটি কাস্টম
রেপ্রের

27

এটি একবার দেখুন:

https://github.com/kmike/marisa-trie

পাইথনের জন্য স্থিতিশীল মেমরি-দক্ষ ট্রাই কাঠামো (২.x এবং 3.x)।

মারিসা-ট্রিতে স্ট্রিং ডেটা মান পাইথন ডিকের চেয়ে 50x-100x কম মেমরি নিতে পারে; কাঁচা দেখার গতি তুলনীয়; ট্রিও প্রিফিক্স অনুসন্ধানের মতো দ্রুত উন্নত পদ্ধতি সরবরাহ করে।

মারিসা-ট্রাই সি ++ লাইব্রেরির উপর ভিত্তি করে।

সফলভাবে মারিসা ট্রাই ব্যবহার করে এমন একটি সংস্থা থেকে একটি ব্লগ পোস্ট এখানে দেওয়া হয়েছে: https://www.repustate.com/blog/sharing-large-data-st संरचना-across-processes-python/

রিপুস্টেটে, আমরা আমাদের পাঠ্য বিশ্লেষণে ব্যবহার করি এমন অনেকগুলি ডেটা মডেলকে সাধারণ কী-মান জোড় বা পাইথন লিঙ্গোর অভিধান হিসাবে উপস্থাপন করা যেতে পারে। আমাদের বিশেষ ক্ষেত্রে, আমাদের অভিধানগুলি বিশাল, প্রতিটি কয়েকশ এমবি, এবং এগুলি অবিচ্ছিন্নভাবে অ্যাক্সেস করা দরকার। কোনও প্রদত্ত এইচটিটিপি অনুরোধের জন্য, 4 বা 5 টি মডেল অ্যাক্সেস করা হতে পারে, প্রতিটি 20-30 লুকআপ করে। সুতরাং আমরা যে সমস্যার মুখোমুখি হই তা হ'ল আমরা কীভাবে ক্লায়েন্টের পক্ষে জিনিসগুলি তত দ্রুত রাখি তেমনি সার্ভারের জন্য হালকাভাবে রাখি।

...

আমি এই প্যাকেজটি পেয়েছি, মারিসা চেষ্টা করে, যা মারিসা ট্রাইয়ের সি ++ প্রয়োগের চারপাশে পাইথন মোড়ক। "মারিসা" পুনরাবৃত্তির সাথে প্রয়োগকৃত স্টোরএজ সহ অ্যালগরিদমের সাথে মিলে যাওয়ার সংক্ষিপ্ত রূপ। মারিসা চেষ্টা সম্পর্কে যা দুর্দান্ত তা হল স্টোরেজ মেকানিজমটি আপনার কত স্মৃতি দরকার তা সঙ্কুচিত করে। পাইথন প্লাগইনটির লেখক 50-100X আকার হ্রাস দাবি করেছেন - আমাদের অভিজ্ঞতাও একই রকম।

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

খাঁটি-পাইথন বাস্তবায়নও বেশ কয়েকটি রয়েছে, যদিও আপনি সীমাবদ্ধ প্ল্যাটফর্মে না থাকলে আপনি সর্বোত্তম পারফরম্যান্সের জন্য উপরের সি ++ ব্যাকড প্রয়োগটি ব্যবহার করতে চান:


শেষ প্রতিশ্রুতি ছিল এপ্রিল 2018 এ, শেষ বড় প্রতিশ্রুতি ছিল 2017 এর মতো
বোরিস

25

ট্রাই প্রয়োগকারী পাইথন প্যাকেজগুলির একটি তালিকা এখানে রয়েছে:

  • মারিসা-Trie - একটি সি ++ ভিত্তিক বাস্তবায়ন।
  • পাইথন-Trie - একটি সাধারণ খাঁটি অজগর বাস্তবায়ন।
  • PyTrie - আরও উন্নত বিশুদ্ধ পাইথন বাস্তবায়ন।
  • pygtrie - গুগল দ্বারা খাঁটি অজগর বাস্তবায়ন।
  • datrie - একটি ডবল অ্যারে Trie উপর ভিত্তি করে বাস্তবায়ন libdatrie

17

senderleএর পদ্ধতি (উপরে) থেকে পরিবর্তিত । আমি পাই যে পাইথন defaultdictত্রি বা উপসর্গ গাছ তৈরির জন্য আদর্শ is

from collections import defaultdict

class Trie:
    """
    Implement a trie with insert, search, and startsWith methods.
    """
    def __init__(self):
        self.root = defaultdict()

    # @param {string} word
    # @return {void}
    # Inserts a word into the trie.
    def insert(self, word):
        current = self.root
        for letter in word:
            current = current.setdefault(letter, {})
        current.setdefault("_end")

    # @param {string} word
    # @return {boolean}
    # Returns if the word is in the trie.
    def search(self, word):
        current = self.root
        for letter in word:
            if letter not in current:
                return False
            current = current[letter]
        if "_end" in current:
            return True
        return False

    # @param {string} prefix
    # @return {boolean}
    # Returns if there is any word in the trie
    # that starts with the given prefix.
    def startsWith(self, prefix):
        current = self.root
        for letter in prefix:
            if letter not in current:
                return False
            current = current[letter]
        return True

# Now test the class

test = Trie()
test.insert('helloworld')
test.insert('ilikeapple')
test.insert('helloz')

print test.search('hello')
print test.startsWith('hello')
print test.search('ilikeapple')

মহাকাশের জটিলতা সম্পর্কে আমার ধারণা হ'ল ও (এন * মি)। কিছু এখানে আলোচনা আছে। stackoverflow.com/questions/2718816/...
dapangmao

5
@ দাপাংমাও আপনি কেবলমাত্র প্রথম চরের জন্য ডিফল্টডিক্ট্ট ব্যবহার করছেন। বিশ্রামের অক্ষরগুলি এখনও স্বাভাবিক ডিক ব্যবহার করে। নেস্টেড ডিফল্টডিক্ট্ট ব্যবহার করা আরও ভাল।
সিংহেলমেসি

3
প্রকৃতপক্ষে, কোডটি প্রথম অক্ষরের জন্য ডিফল্টডিক্টকে "ব্যবহার" বলে মনে হচ্ছে না কারণ এটি ডিফল্ট_ফ্যাক্টরি সেট করে না এবং এখনও সেট_ডিফল্ট ব্যবহার করে।
স্টুডিজেক

12

কোন "উচিত" নেই; এটা আপনার উপর নির্ভর করছে. বিভিন্ন বাস্তবায়নের বিভিন্ন কর্মক্ষমতা বৈশিষ্ট্য থাকবে, বাস্তবায়ন করতে, বুঝতে এবং সঠিকভাবে পেতে বিভিন্ন পরিমাণ সময় নিতে পারে। আমার মতে এটি সামগ্রিকভাবে সফ্টওয়্যার বিকাশের জন্য সাধারণ।

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


2
আবারও, আপনাকে ধন্যবাদ তবে আমি এখনও মনে করি যে আপনার উত্তরটি ডিএডাব্লুজি এবং টিআরআই এর কার্যকারিতাটির যুক্তি এবং কাঠামোটি নির্ধারণ করার কারণে আপনার উত্তরটির আরও আরও গভীর ব্যাখ্যা এবং ব্যাখ্যা দরকার needs আপনার পরবর্তী ইনপুট খুব দরকারী এবং প্রশংসা করা হবে।
ফিল

আপনি যদি স্লট সহ অবজেক্টগুলি ব্যবহার না করেন তবে আপনার উদাহরণের নাম স্থানটি যাইহোক অভিধান হবে।
ম্যাড পদার্থবিদ

4

আপনি যদি পাইথন ক্লাস হিসাবে কার্যকর ট্রাই করতে চান তবে সেগুলি সম্পর্কে পড়ার পরে আমি এখানে কিছু লিখেছি:

class Trie:

    def __init__(self):
        self.__final = False
        self.__nodes = {}

    def __repr__(self):
        return 'Trie<len={}, final={}>'.format(len(self), self.__final)

    def __getstate__(self):
        return self.__final, self.__nodes

    def __setstate__(self, state):
        self.__final, self.__nodes = state

    def __len__(self):
        return len(self.__nodes)

    def __bool__(self):
        return self.__final

    def __contains__(self, array):
        try:
            return self[array]
        except KeyError:
            return False

    def __iter__(self):
        yield self
        for node in self.__nodes.values():
            yield from node

    def __getitem__(self, array):
        return self.__get(array, False)

    def create(self, array):
        self.__get(array, True).__final = True

    def read(self):
        yield from self.__read([])

    def update(self, array):
        self[array].__final = True

    def delete(self, array):
        self[array].__final = False

    def prune(self):
        for key, value in tuple(self.__nodes.items()):
            if not value.prune():
                del self.__nodes[key]
        if not len(self):
            self.delete([])
        return self

    def __get(self, array, create):
        if array:
            head, *tail = array
            if create and head not in self.__nodes:
                self.__nodes[head] = Trie()
            return self.__nodes[head].__get(tail, create)
        return self

    def __read(self, name):
        if self.__final:
            yield name
        for key, value in self.__nodes.items():
            yield from value.__read(name + [key])

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

3
____Slots ____ এটাই। এটি কোনও শ্রেণীর দ্বারা ব্যবহৃত মেমরির পরিমাণ হ্রাস করে, যখন আপনার কাছে এর অনেকগুলি উদাহরণ রয়েছে।
ডিস্ট্রোমবার্গ

3

এই সংস্করণটি পুনরাবৃত্তি ব্যবহার করছে

import pprint
from collections import deque

pp = pprint.PrettyPrinter(indent=4)

inp = raw_input("Enter a sentence to show as trie\n")
words = inp.split(" ")
trie = {}


def trie_recursion(trie_ds, word):
    try:
        letter = word.popleft()
        out = trie_recursion(trie_ds.get(letter, {}), word)
    except IndexError:
        # End of the word
        return {}

    # Dont update if letter already present
    if not trie_ds.has_key(letter):
        trie_ds[letter] = out

    return trie_ds

for word in words:
    # Go through each word
    trie = trie_recursion(trie, deque(word))

pprint.pprint(trie)

আউটপুট:

Coool👾 <algos>🚸  python trie.py
Enter a sentence to show as trie
foo bar baz fun
{
  'b': {
    'a': {
      'r': {},
      'z': {}
    }
  },
  'f': {
    'o': {
      'o': {}
    },
    'u': {
      'n': {}
    }
  }
}

3
from collections import defaultdict

ট্রাই সংজ্ঞায়িত করুন:

_trie = lambda: defaultdict(_trie)

ট্রি তৈরি করুন:

trie = _trie()
for s in ["cat", "bat", "rat", "cam"]:
    curr = trie
    for c in s:
        curr = curr[c]
    curr.setdefault("_end")

খুঁজে দেখো:

def word_exist(trie, word):
    curr = trie
    for w in word:
        if w not in curr:
            return False
        curr = curr[w]
    return '_end' in curr

টেস্ট:

print(word_exist(trie, 'cam'))

1
সতর্কতা: এটি Trueকেবল একটি পুরো শব্দের জন্য প্রত্যাবর্তন করে , তবে উপসর্গের জন্য নয়, উপসর্গটি পরিবর্তিত হওয়ার return '_end' in currজন্যreturn True
শ্রীকান্ত শেটে

0
class Trie:
    head = {}

    def add(self,word):

        cur = self.head
        for ch in word:
            if ch not in cur:
                cur[ch] = {}
            cur = cur[ch]
        cur['*'] = True

    def search(self,word):
        cur = self.head
        for ch in word:
            if ch not in cur:
                return False
            cur = cur[ch]

        if '*' in cur:
            return True
        else:
            return False
    def printf(self):
        print (self.head)

dictionary = Trie()
dictionary.add("hi")
#dictionary.add("hello")
#dictionary.add("eye")
#dictionary.add("hey")


print(dictionary.search("hi"))
print(dictionary.search("hello"))
print(dictionary.search("hel"))
print(dictionary.search("he"))
dictionary.printf()

আউট

True
False
False
False
{'h': {'i': {'*': True}}}

0

পাই এর জন্য পাইথন ক্লাস


ট্রাই ডেটা স্ট্রাকচারটি O(L)স্ট্রিংয়ের দৈর্ঘ্য যেখানে স্ট্রিংয়ের দৈর্ঘ্য তাই স্ট্রিংটি মুছার জন্য একই O(NL)স্ট্রিংটিতে অনুসন্ধান করা যেতে পারে সেখানে স্ট্রিংয়ের দৈর্ঘ্য অন্তর্ভুক্ত করার জন্য O(L)ব্যবহার করা যেতে পারে।

Https://github.com/Parikshit22/pytrie.git থেকে ক্লোন করা যেতে পারে

class Node:
    def __init__(self):
        self.children = [None]*26
        self.isend = False
        
class trie:
    def __init__(self,):
        self.__root = Node()
        
    def __len__(self,):
        return len(self.search_byprefix(''))
    
    def __str__(self):
        ll =  self.search_byprefix('')
        string = ''
        for i in ll:
            string+=i
            string+='\n'
        return string
        
    def chartoint(self,character):
        return ord(character)-ord('a')
    
    def remove(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                raise ValueError("Keyword doesn't exist in trie")
        if ptr.isend is not True:
            raise ValueError("Keyword doesn't exist in trie")
        ptr.isend = False
        return
    
    def insert(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                ptr.children[i] = Node()
                ptr = ptr.children[i]
        ptr.isend = True
        
    def search(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                return False
        if ptr.isend is not True:
            return False
        return True
    
    def __getall(self,ptr,key,key_list):
        if ptr is None:
            key_list.append(key)
            return
        if ptr.isend==True:
            key_list.append(key)
        for i in range(26):
            if ptr.children[i]  is not None:
                self.__getall(ptr.children[i],key+chr(ord('a')+i),key_list)
        
    def search_byprefix(self,key):
        ptr = self.__root
        key_list = []
        length = len(key)
        for idx in range(length):
            i = self.chartoint(key[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                return None
        
        self.__getall(ptr,key,key_list)
        return key_list
        

t = trie()
t.insert("shubham")
t.insert("shubhi")
t.insert("minhaj")
t.insert("parikshit")
t.insert("pari")
t.insert("shubh")
t.insert("minakshi")
print(t.search("minhaj"))
print(t.search("shubhk"))
print(t.search_byprefix('m'))
print(len(t))
print(t.remove("minhaj"))
print(t)

কোড আউটপুট

সত্য
মিথ্যা
[ 'minakshi', 'মিনহাজ']
7
minakshi
minhajsir
Pari
parikshit
শুভ
shubham
shubhi

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