Mengapa C ++ STL tidak menyediakan wadah "pohon"?


373

Mengapa C ++ STL tidak menyediakan wadah "tree", dan apa hal terbaik untuk digunakan?

Saya ingin menyimpan hierarki objek sebagai pohon, daripada menggunakan pohon sebagai peningkatan kinerja ...


7
Saya perlu pohon untuk menyimpan representasi hierarki.
Roddy

20
Aku bersama lelaki yang turun memilih jawaban "benar", yang tampaknya; "Pohon tidak berguna". Ada yang penting jika penggunaan pohon tidak jelas.
Joe Soul-bringer

Saya pikir alasannya sepele - belum ada yang mengimplementasikannya di perpustakaan standar. Ini seperti perpustakaan standar tidak std::unordered_mapdan std::unordered_setsampai saat ini. Dan sebelum itu tidak ada wadah STL di perpustakaan standar sama sekali.
doc

1
Pikiran saya (karena tidak pernah membaca standar yang relevan, maka ini adalah komentar bukan jawaban) adalah bahwa STL tidak peduli dengan struktur data tertentu, ia peduli dengan spesifikasi mengenai kompleksitas dan operasi apa yang didukung. Jadi struktur dasar yang digunakan dapat bervariasi antara implementasi dan / atau arsitektur target, asalkan memenuhi spesifikasi. Saya cukup yakin std::mapdan std::setakan menggunakan pohon dalam setiap implementasi di luar sana, tetapi mereka tidak perlu jika jika beberapa struktur non-pohon juga memenuhi spesifikasi.
Mark K Cowan

Jawaban:


182

Ada dua alasan Anda ingin menggunakan pohon:

Anda ingin mencerminkan masalah menggunakan struktur seperti pohon:
Untuk ini, kami telah meningkatkan perpustakaan grafik

Atau Anda ingin wadah yang memiliki karakteristik akses seperti pohon Untuk ini, kami memiliki

Pada dasarnya karakteristik kedua wadah ini sedemikian rupa sehingga praktis harus diimplementasikan menggunakan pohon (meskipun ini sebenarnya bukan persyaratan).

Lihat juga pertanyaan ini: Implementasi C tree


64
Ada banyak, banyak alasan untuk menggunakan pohon, bahkan jika ini adalah yang paling umum. Paling umum! Sama dengan semua.
Joe Soul-bringer

3
Alasan utama ketiga untuk menginginkan pohon adalah untuk daftar yang selalu diurutkan dengan penyisipan / penghapusan cepat, tetapi untuk itu ada std: multiset.
VoidStar

1
@ Durga: Tidak yakin bagaimana kedalaman relevan ketika Anda menggunakan peta sebagai wadah yang diurutkan. Log jaminan peta (n) penyisipan / penghapusan / pencarian (dan mengandung elemen dalam urutan diurutkan). Ini semua peta digunakan untuk dan diimplementasikan (biasanya) sebagai pohon merah / hitam. Pohon merah / hitam memastikan bahwa pohon itu seimbang. Jadi kedalaman pohon berhubungan langsung dengan jumlah elemen dalam pohon.
Martin York

14
Saya tidak setuju dengan jawaban ini, baik pada 2008 dan sekarang. Pustaka standar tidak "memiliki" dorongan, dan ketersediaan sesuatu yang meningkatkan tidak boleh (dan belum) menjadi alasan untuk tidak mengadopsinya ke dalam standar. Selain itu, BGL bersifat umum dan cukup terlibat untuk mendapatkan kelas pohon khusus yang independen darinya. Juga, fakta bahwa std :: map dan std :: set memerlukan pohon adalah, IMO, argumen lain untuk memiliki stl::red_black_treedll. Akhirnya, std::mapdan std::setpohon seimbang, std::treemungkin tidak.
einpoklum

1
@einpoklum: "ketersediaan sesuatu dalam boost seharusnya tidak menjadi alasan untuk tidak menerapkannya ke dalam standar" - mengingat salah satu tujuan boost adalah untuk bertindak sebagai pembuktian perpustakaan yang bermanfaat sebelum dimasukkan dalam standar, saya hanya bisa katakan "tentu saja!".
Martin Bonner mendukung Monica

94

Mungkin karena alasan yang sama bahwa tidak ada wadah pohon dalam meningkatkan. Ada banyak cara untuk menerapkan wadah seperti itu, dan tidak ada cara yang baik untuk memuaskan semua orang yang akan menggunakannya.

Beberapa masalah untuk dipertimbangkan:

  • Apakah jumlah anak untuk suatu simpul tetap atau variabel?
  • Berapa overhead per node? - yaitu, apakah Anda memerlukan pointer orangtua, pointer saudara, dll.
  • Algoritma apa yang harus disediakan? - berbagai iterator, algoritma pencarian, dll.

Pada akhirnya, masalahnya adalah wadah pohon yang cukup bermanfaat bagi semua orang, terlalu berat untuk memuaskan sebagian besar orang yang menggunakannya. Jika Anda mencari sesuatu yang kuat, Boost Graph Library pada dasarnya adalah superset dari apa perpustakaan pohon dapat digunakan untuk.

Berikut adalah beberapa implementasi pohon generik lainnya:


5
"... tidak ada cara yang baik untuk memuaskan semua orang ..." Kecuali itu karena stl :: map, stl :: multimap, dan stl :: set didasarkan pada stl's rb_tree, itu harus memenuhi banyak kasus seperti yang dilakukan oleh tipe dasar .
Catskul

44
Mengingat tidak ada cara untuk mengambil anak-anak dari simpul a std::map, saya tidak akan memanggil wadah pohon itu. Itu adalah wadah asosiatif yang biasanya diimplementasikan sebagai pohon. Perbedaan besar.
Mooing Duck

2
Saya setuju dengan Mooing Duck, bagaimana Anda menerapkan pencarian pertama yang luas di std :: map? Ini akan menjadi sangat mahal
Marco A.

1
Saya mulai menggunakan tree.hh Kasper Peeters, namun setelah meninjau lisensi untuk GPLv3, atau versi GPL lainnya, itu akan mencemari perangkat lunak komersial kami. Saya akan merekomendasikan melihat treetree yang disediakan dalam komentar oleh @ hplbsh jika Anda memerlukan struktur untuk tujuan komersial.
Jake88

3
Berbagai persyaratan khusus pada pohon adalah argumen untuk memiliki berbagai jenis pohon, bukan untuk tidak memilikinya sama sekali.
André

50

Filosofi STL adalah bahwa Anda memilih wadah berdasarkan jaminan dan bukan berdasarkan bagaimana wadah diterapkan. Misalnya, pilihan wadah Anda mungkin didasarkan pada kebutuhan untuk pencarian cepat. Untuk semua yang Anda pedulikan, wadah dapat diimplementasikan sebagai daftar searah - selama pencarian sangat cepat Anda akan bahagia. Itu karena Anda tidak menyentuh internal, Anda menggunakan iterator atau fungsi anggota untuk akses. Kode Anda tidak terikat pada bagaimana wadah diterapkan tetapi seberapa cepat itu, atau apakah itu memiliki pemesanan tetap dan pasti, atau apakah itu efisien pada ruang, dan sebagainya.


12
Saya tidak berpikir dia berbicara tentang implementasi wadah, dia berbicara tentang wadah pohon yang sebenarnya.
Mooing Duck

3
@ MoooDuck Saya pikir apa yang dimaksud wilhelmtell adalah bahwa pustaka standar C ++ tidak mendefinisikan kontainer berdasarkan pada struktur data yang mendasarinya; itu hanya mendefinisikan wadah dengan antarmuka mereka dan karakteristik yang dapat diamati seperti kinerja asimptotik. Ketika Anda memikirkannya, sebatang pohon sebenarnya bukan wadah (seperti yang kita ketahui). Mereka bahkan tidak memiliki lurus ke depan end()dan begin()dengan mana Anda dapat beralih melalui semua elemen, dll.
Jordan Melo

7
@JordanMelo: Omong kosong pada semua poin. Itu adalah benda yang mengandung benda. Sangat sepele untuk mendesainnya untuk memulai () dan mengakhiri () dan iterator dua arah untuk diulangi. Setiap wadah memiliki karakteristik berbeda. Akan bermanfaat jika seseorang juga dapat memiliki karakteristik pohon. Seharusnya cukup mudah.
Mooing Duck

Jadi seseorang ingin memiliki wadah yang menyediakan pencarian cepat untuk simpul anak dan orang tua, dan persyaratan memori yang wajar.
doc

@JordanMelo: Dari perspektif itu, juga adaptor seperti antrian, tumpukan atau antrian prioritas tidak akan menjadi milik STL (mereka juga tidak memiliki begin()dan end()). Dan ingat bahwa antrian prioritas biasanya tumpukan, yang setidaknya secara teori adalah pohon (meskipun implementasi sebenarnya). Jadi, bahkan jika Anda menerapkan pohon sebagai adaptor menggunakan beberapa struktur data dasar yang berbeda, itu akan memenuhi syarat untuk dimasukkan dalam STL.
andreee

48

"Aku ingin menyimpan hierarki objek sebagai pohon"

C ++ 11 telah datang dan pergi dan mereka masih tidak melihat kebutuhan untuk menyediakan std::tree, meskipun idenya muncul (lihat di sini ). Mungkin alasan mereka belum menambahkan ini adalah karena itu mudah untuk membuat sendiri di atas wadah yang ada. Sebagai contoh...

template< typename T >
struct tree_node
   {
   T t;
   std::vector<tree_node> children;
   };

Traversal sederhana akan menggunakan rekursi ...

template< typename T >
void tree_node<T>::walk_depth_first() const
   {
   cout<<t;
   for ( auto & n: children ) n.walk_depth_first();
   }

Jika Anda ingin mempertahankan hierarki dan Anda ingin itu bekerja dengan algoritma STL , maka segala sesuatunya mungkin menjadi rumit. Anda dapat membangun iterator Anda sendiri dan mencapai beberapa kompatibilitas, namun banyak algoritma yang tidak masuk akal untuk hierarki (apa pun yang mengubah urutan kisaran, misalnya). Bahkan mendefinisikan rentang dalam hierarki bisa menjadi bisnis yang berantakan.


2
Jika proyek dapat memungkinkan anak-anak dari tree_node untuk diurutkan, maka menggunakan std :: set <> di tempat std :: vector <> dan kemudian menambahkan operator <() ke objek tree_node akan sangat meningkatkan kinerja 'pencarian' dari objek seperti 'T'.
J Jorgenson

4
Ternyata mereka malas dan benar-benar membuat contoh pertama Anda Perilaku Tidak Terdefinisi.
user541686

2
@Mehrdad: Saya akhirnya memutuskan untuk menanyakan detail di balik komentar Anda di sini .
nobar

many of the algorithms simply don't make any sense for a hierarchy. Masalah interpretasi. Bayangkan sebuah struktur pengguna stackoverflow dan setiap tahun Anda menginginkan mereka dengan jumlah poin reputasi yang lebih tinggi untuk menjadi bos bagi mereka yang memiliki poin reputasi lebih rendah. Dengan demikian memberikan iterator BFS dan perbandingan yang tepat, setiap tahun Anda hanya menjalankan std::sort(tree.begin(), tree.end()).
doc

Dengan cara yang sama, Anda dapat dengan mudah membangun pohon asosiatif (untuk memodelkan catatan nilai kunci yang tidak terstruktur, seperti JSON misalnya) dengan mengganti vectordengan mapdalam contoh di atas. Untuk dukungan penuh dari struktur seperti JSON, Anda bisa menggunakan variantuntuk mendefinisikan node.
nobar

43

Jika Anda mencari implementasi RB-tree, maka stl_tree.h mungkin cocok untuk Anda juga.


14
Anehnya ini adalah satu-satunya jawaban yang benar-benar menjawab pertanyaan awal.
Catskul

12
Mengingat ia menginginkan "Heiarchy", Tampaknya aman untuk menganggap bahwa apa pun dengan "menyeimbangkan" adalah jawaban yang salah.
Mooing Duck

11
"Ini adalah file tajuk internal, yang disertakan oleh tajuk perpustakaan lain. Anda tidak boleh mencoba menggunakannya secara langsung."
Dan

3
@Dan: Menyalinnya bukan berarti menggunakannya secara langsung.
einpoklum

12

std :: map didasarkan pada pohon merah-hitam . Anda juga dapat menggunakan wadah lain untuk membantu Anda menerapkan jenis pohon Anda sendiri.


13
Biasanya menggunakan pohon merah-hitam (Tidak diharuskan untuk melakukannya).
Martin York

1
GCC menggunakan pohon untuk mengimplementasikan peta. Adakah yang ingin melihat direktori VC mereka untuk melihat apa yang digunakan microsoft?
JJ

// Kelas pohon merah-hitam, dirancang untuk digunakan dalam menerapkan STL // wadah asosiatif (set, multiset, peta, dan multimap). Raih itu dari file stl_tree.h saya.
JJ

@JJ Setidaknya di Studio 2010, ia menggunakan ordered red-black tree of {key, mapped} values, unique keyskelas internal , didefinisikan dalam <xtree>. Tidak memiliki akses ke versi yang lebih modern saat ini.
Justin Time - Reinstate Monica

8

Di satu sisi, std :: map adalah pohon (harus memiliki karakteristik kinerja yang sama dengan pohon biner seimbang) tetapi tidak mengekspos fungsionalitas pohon lainnya. Kemungkinan alasan di balik tidak termasuk struktur data pohon nyata mungkin hanya masalah tidak termasuk semua yang ada di stl. Stl dapat dilihat sebagai kerangka kerja untuk digunakan dalam mengimplementasikan algoritma dan struktur data Anda sendiri.

Secara umum, jika ada fungsi pustaka dasar yang Anda inginkan, itu tidak ada di stl, perbaikannya adalah dengan melihat pada BOOST .

Jika tidak, ada sekelompok dari perpustakaan di luar sana , tergantung pada kebutuhan pohon Anda.


6

Semua wadah STL secara eksternal direpresentasikan sebagai "urutan" dengan satu mekanisme iterasi. Pohon tidak mengikuti idiom ini.


7
Struktur data pohon dapat menyediakan traversal preorder, inorder atau postorder melalui iterator. Sebenarnya inilah yang dilakukan std :: map.
Andrew Tomazos

3
Ya dan tidak ... itu tergantung pada apa yang Anda maksud dengan "tree". std::mapdiimplementasikan secara internal sebagai btree, tetapi secara eksternal muncul sebagai SEQUENCE of PAIRS yang diurutkan. Mengingat elemen apa pun yang Anda dapat secara universal bertanya siapa yang sebelum dan siapa yang mengejar. Struktur pohon umum yang mengandung elemen yang masing-masing berisi elemen lainnya tidak memaksakan penyortiran atau arah. Anda dapat mendefinisikan iterator yang berjalan dalam struktur pohon dengan berbagai cara (sallow | deep first | last ...) tetapi begitu Anda melakukannya, sebuah std::treewadah harus mengembalikan salah satu dari mereka dari suatu beginfungsi. Dan tidak ada alasan yang jelas untuk mengembalikan satu atau yang lain.
Emilio Garavaglia

4
Std :: map umumnya diwakili oleh pohon pencarian biner seimbang, bukan B-tree. Argumen yang sama yang Anda buat dapat diterapkan ke std :: unordered_set, tidak memiliki urutan alami, namun hadiah mulai dan berakhir iterator. Persyaratan awal dan akhir hanyalah bahwa ia mengulangi semua elemen dalam suatu urutan deterministik, bukan bahwa harus ada yang alami. preorder adalah urutan iterasi yang valid untuk sebuah pohon.
Andrew Tomazos

4
Implikasi dari jawaban Anda adalah bahwa tidak ada struktur data st-n-tree karena tidak memiliki antarmuka "urutan". Ini tidak benar.
Andrew Tomazos

3
@EmiloGaravaglia: Seperti dibuktikan oleh std::unordered_set, yang tidak memiliki "cara unik" untuk mengulangi anggotanya (sebenarnya urutan iterasi pseudo-acak dan implementasi didefinisikan), tetapi masih merupakan wadah stl - ini membuktikan maksud Anda. Iterasi setiap elemen dalam sebuah wadah masih merupakan operasi yang berguna, bahkan jika urutannya tidak ditentukan.
Andrew Tomazos

4

Karena STL bukan perpustakaan "segalanya". Ini berisi, pada dasarnya, struktur minimum yang diperlukan untuk membangun sesuatu.


13
Pohon biner adalah fungsi yang sangat dasar, dan pada kenyataannya, lebih mendasar daripada wadah lain karena jenis seperti std :: map, std :: multimap, dan stl :: set. Karena tipe-tipe itu berdasarkan pada mereka, Anda akan mengharapkan tipe yang mendasarinya akan terbuka.
Catskul

2
Saya tidak berpikir OP meminta pohon biner , dia meminta pohon untuk menyimpan hierarki.
Mooing Duck

Tidak hanya itu, menambahkan "wadah" pohon ke STL akan berarti menambahkan banyak konsep baru, misalnya navigator pohon (generalisasi Iterator).
alfC

5
"Struktur minimum untuk membangun sesuatu" adalah pernyataan yang sangat subyektif. Anda dapat membangun sesuatu dengan konsep C ++ mentah, jadi saya pikir benar minimum tidak akan ada STL sama sekali.
doc


3

IMO, suatu kelalaian. Tapi saya pikir ada alasan bagus untuk tidak memasukkan struktur Tree dalam STL. Ada banyak logika dalam memelihara pohon, yang paling baik ditulis sebagai fungsi anggota ke TreeNodeobjek dasar . Ketika TreeNodedibungkus dalam header STL, itu hanya akan berantakan.

Sebagai contoh:

template <typename T>
struct TreeNode
{
  T* DATA ; // data of type T to be stored at this TreeNode

  vector< TreeNode<T>* > children ;

  // insertion logic for if an insert is asked of me.
  // may append to children, or may pass off to one of the child nodes
  void insert( T* newData ) ;

} ;

template <typename T>
struct Tree
{
  TreeNode<T>* root;

  // TREE LEVEL functions
  void clear() { delete root ; root=0; }

  void insert( T* data ) { if(root)root->insert(data); } 
} ;

7
Itu banyak memiliki pointer mentah yang Anda miliki di sana, banyak yang tidak perlu menjadi pointer sama sekali.
Mooing Duck

Sarankan Anda menarik jawaban ini. Kelas TreeNode adalah bagian dari implementasi pohon.
einpoklum

3

Saya pikir ada beberapa alasan mengapa tidak ada pohon STL. Terutama Pohon adalah bentuk struktur data rekursif yang, seperti wadah (daftar, vektor, set), memiliki struktur halus yang sangat berbeda yang membuat pilihan yang benar rumit. Mereka juga sangat mudah dibangun dalam bentuk dasar menggunakan STL.

Pohon berakar terbatas dapat dianggap sebagai sebuah wadah yang memiliki nilai atau muatan, katakan sebuah instance dari kelas A dan, koleksi pohon (sub) yang mungkin kosong; pohon dengan koleksi sub pohon yang kosong dianggap sebagai daun.

template<class A>
struct unordered_tree : std::set<unordered_tree>, A
{};

template<class A>
struct b_tree : std::vector<b_tree>, A
{};

template<class A>
struct planar_tree : std::list<planar_tree>, A
{};

Kita harus berpikir sedikit tentang desain iterator dll. Dan operasi produk dan produk samping mana yang memungkinkan untuk mendefinisikan dan menjadi efisien di antara pohon - dan STL asli harus ditulis dengan baik - sehingga set kosong, vektor atau daftar kontainer adalah benar-benar kosong dari muatan apa pun dalam kasus default.

Pohon memainkan peran penting dalam banyak struktur matematika (lihat makalah klasik dari Jagal, Grossman dan Larsen; juga makalah Connes dan Kriemer untuk contoh mereka dapat bergabung, dan bagaimana mereka digunakan untuk menghitung). Tidaklah benar untuk berpikir bahwa peran mereka hanya untuk memfasilitasi operasi-operasi tertentu lainnya. Sebaliknya mereka memfasilitasi tugas-tugas itu karena peran mendasar mereka sebagai struktur data.

Namun, selain pohon ada juga "co-tree"; pohon di atas semua memiliki properti yang jika Anda menghapus root Anda menghapus semuanya.

Pertimbangkan iterator di pohon, mungkin mereka akan direalisasikan sebagai setumpuk iterator sederhana, ke sebuah simpul, dan ke induknya, ... hingga ke akar.

template<class TREE>
struct node_iterator : std::stack<TREE::iterator>{
operator*() {return *back();}
...};

Namun, Anda dapat memiliki sebanyak yang Anda suka; secara kolektif mereka membentuk "pohon" tetapi di mana semua panah mengalir ke arah root, co-tree ini dapat diiterasi melalui iterator menuju iterator dan root yang sepele; namun tidak dapat dinavigasi melintasi atau ke bawah (iterator lain tidak diketahui) atau ensemble iterator tidak dapat dihapus kecuali dengan melacak semua instance.

Pohon sangat berguna, mereka memiliki banyak struktur, ini membuatnya menjadi tantangan serius untuk mendapatkan pendekatan yang benar benar. Dalam pandangan saya ini adalah mengapa mereka tidak diterapkan di STL Selain itu, di masa lalu, saya telah melihat orang-orang menjadi religius dan menemukan ide tentang jenis wadah berisi contoh dari jenisnya sendiri menantang - tetapi mereka harus menghadapinya - itulah yang diwakili oleh jenis pohon - itu adalah simpul yang mengandung mungkin koleksi kosong pohon (lebih kecil). Bahasa saat ini memungkinkan tanpa tantangan menyediakan konstruktor default untuk container<B>tidak mengalokasikan ruang pada heap (atau tempat lain) untuk B, dll.

Untuk satu orang akan senang jika ini, dalam bentuk yang baik, menemukan jalan ke standar.


0

Membaca jawaban di sini alasan umum yang disebutkan adalah bahwa seseorang tidak dapat beralih melalui pohon atau bahwa pohon tidak menganggap antarmuka yang sama dengan wadah STL lainnya dan orang tidak dapat menggunakan algoritma STL dengan struktur pohon tersebut.

Setelah itu dalam pikiran saya mencoba merancang struktur data pohon saya sendiri yang akan memberikan antarmuka seperti STL dan akan dapat digunakan dengan ALGorthims STL yang ada sebanyak mungkin.

Gagasan saya adalah bahwa pohon harus didasarkan pada wadah STL yang ada dan tidak boleh menyembunyikan wadah, sehingga dapat diakses untuk digunakan dengan algoritma STL.

Fitur penting lain yang harus disediakan oleh pohon adalah iterator yang melintas.

Inilah yang saya dapat membuat: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

Dan inilah tesnya: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp


-9

Semua wadah STL dapat digunakan dengan iterator. Anda tidak dapat memiliki iterator dan pohon, karena Anda tidak memiliki cara yang benar untuk melewati pohon tersebut.


3
Tetapi Anda dapat mengatakan BFS atau DFS adalah cara yang benar. Atau dukung keduanya. Atau yang lain yang bisa Anda bayangkan. Hanya memberi tahu pengguna apa itu.
tomas789

2
di std :: map ada iterator pohon.
Jai

1
Sebuah pohon dapat mendefinisikan jenis iterator kustomnya sendiri yang melintasi semua node secara berurutan dari satu "ekstrim" ke yang lain (yaitu untuk setiap pohon biner dengan jalur 0 & 1, ia dapat menawarkan iterator yang bergerak dari "semua 0s" ke "semua 1s "dan iterator reverse yang tidak sebaliknya, karena pohon dengan kedalaman 3 dan mulai simpul s, misalnya, bisa iterate atas node sebagai s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(" paling kiri" untuk "paling kanan"); bisa juga menggunakan pola kedalaman traversal ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Reinstate Monica

, dll.), atau beberapa pola lain, asalkan ia mengulangi setiap simpul sedemikian rupa sehingga masing-masing hanya dilewati satu kali.
Justin Time - Pasang kembali Monica

1
@ doc, poin yang sangat bagus. Saya pikir std::unordered_setitu "dibuat" urutan karena kita tidak tahu cara yang lebih baik untuk mengulangi elemen-elemen selain beberapa cara sewenang-wenang (diberikan secara internal oleh fungsi hash). Saya pikir itu adalah kasus yang berlawanan dari pohon: iterasi lebih dari unordered_setkurang ditentukan, secara teori tidak ada "cara untuk mendefinisikan iterasi selain mungkin" secara acak ". Dalam kasus pohon ada banyak cara "baik" (tidak acak). Tapi, sekali lagi, maksud Anda valid.
alfC
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.