Jump to page: 1 2 3
Thread overview
N elemanın M'li kombinasyonlarını veren aralık
Jul 14, 2012
Salih Dinçer
Jul 14, 2012
Kadir Can
Jul 14, 2012
Salih Dinçer
Jul 14, 2012
Salih Dinçer
Jul 14, 2012
Salih Dinçer
Jul 15, 2012
Salih Dinçer
Jul 15, 2012
Salih Dinçer
Jul 16, 2012
Salih Dinçer
Jul 16, 2012
Salih Dinçer
Jul 16, 2012
Salih Dinçer
Jul 22, 2012
Salih Dinçer
Jul 22, 2012
Salih Dinçer
Jul 22, 2012
Salih Dinçer
June 20, 2012

N elemanın 6'lı kombinasyonlarını oluşturmanın bir yolu aşağıdaki gibidir. Aklıma gelen sakıncalarını açıklama satırlarına yazdım:

/**
* Kombinasyonları oluşturmak için elle yazılabilen işlev. Sakıncaları:
*
* - Hane adedi değiştiğinde kodun değiştirilmesi gerekir
*
* - Değişken isimleri hataya açık
*
* - Yalnızca RandomAccessRange aralıklarıyla kullanılabilir
*
* - Kombinasyonların koşut olarak işletilmeleri kolay değil çünkü en içteki
*   döngü içinde std.parallesims.Task nesnelerinin açıkça oluşturulmaları,
*   başlatılmaları, ve bitmelerinin açıkça beklenmeleri gerekir.
*/
void altıDöngülü(const int[] dilim)
{
   writeln("Sırayla işleterek:");

   foreach (i0; 0 .. dilim.length - 5) {
       foreach (i1; i0 + 1 .. dilim.length) {
           foreach (i2; i1 + 1 .. dilim.length) {
               foreach (i3; i2 + 1 .. dilim.length) {
                   foreach (i4; i3 + 1 .. dilim.length) {
                       foreach (i5; i4 + 1 .. dilim.length) {
                           işle([dilim[i0], dilim[i1], dilim[i2],
                                 dilim[i3], dilim[i4], dilim[i5] ]);
                       }
                   }
               }
           }
       }
   }
}

Kombinasyonları bir aralık olarak sunabilsek hem daha fazla aralık çeşidiyle kullanılabilir hem de koşut olarak işletilebilirler. Bayağı debelenerek :) aşağıdaki Kombinasyon aralığını yazdım. (Uyarı: Doğru çalıştığından emin olmadan kullanmayın!)

Deneyen programın tamamını veriyorum. Açıklamalar da ekledim:

import std.stdio;
import std.string;
import std.range;
import std.traits;
import std.conv;
import core.thread;
import std.parallelism;
import std.exception;

/**
* Verilen aralığın belirtilen sayıda elemandan oluşan kombinasyonlarını
* dilimler halinde üretir.
*/
struct Kombinasyon(A)
   if (isForwardRange!A)
{
private:

   size_t kaçlı;            /* Kaçlı kombinasyon olduğu */

   A[] karalama;           /* Elemanları üretmek için kullanılan
                            * aralıklar. Bir anlamda bir karalama tablosu
                            * olarak kullanılacaklar. (Performans notu: Bu
                            * üye front() hiç çağrılmayacak olsa bile
                            * hesaplanır.) */

   ElementType!A[] baştaki; /* Baştaki eleman (Performans notu: Bu üye
                             * front() hiç çağrılmayacak olsa bile
                             * hesaplanır.) */

   /**
    * Kombinasyonları üretmek için kullanılacak olan aralıkları ve baştaki
    * elemanı hesaplar.
    */
   void hazırla()
   in
   {
       assert(!karalama.empty); /* Eksiği bulunabilir. */
       assert(baştaki.length == kaçlı);
   }
   body
   {
       if (karalama.length == kaçlı) {
           /* Karalama tablolarında düzenleme gerekmiyor. */

       } else {
           /* popFront() tarafından olasılıkla kaybedilmiş olan aralıkları
            * yerine koy. Her aralık, bir önceki aralığın baştan bir eleman
            * eksiğidir. */
           karalama.length = kaçlı;

           for (size_t i = 1; i < kaçlı; ++i) {
               if (karalama[i].empty) {
                   karalama[i] = karalama[i - 1].save;

                   if (!karalama[i].empty) {
                       karalama[i].popFront();
                   }
               }
           }
       }

       /* Baştaki kombinasyon aralığını hesapla. */
       foreach (i, aralık; karalama) {
           if (!aralık.empty) {
               baştaki[i] = aralık.front();
           }
       }
   }

public:

   this(A aralık, size_t kaçlı)
   {
       this.kaçlı = kaçlı;
       this.karalama ~= aralık.save;
       this.baştaki.length = kaçlı;

       hazırla();

       /* Uzunluğu belli olan aralıklarda ısrarcı olmamak için baştan
        * aralık.length'e bakmıyoruz. Onun yerine, bütün aralıklar'ın
        * dolabilmiş olmasına bakıyoruz. */
       enforce(!karalama[$-1].empty,
               format("Bu %s aralığıyla %s elemanlı kombinasyon oluşturulamaz",
                      typeid(A), kaçlı));
   }

   bool empty() const @property
   {
       return karalama.empty;
   }

   ElementType!A[] front() @property
   {
       enforce(!empty);
       return baştaki.dup;
   }

   void popFront()
   {
       enforce(!empty);

       /* Sondaki hanenin başındaki çıkart ve bütün karalama aralıklarını
        * gerektiği kadar düzenle. */

       karalama.back.popFront();

       size_t i = kaçlı - 1;
       for ( ; i > 0; --i) {
           if (karalama[i].length < kaçlı - i) {
               /* Bu hane için eleman kalmamış. Bunu çıkartalım ve bir önceki
                * hanenin de başındakini çıkartalım. */
               karalama.length = i;
               karalama[i - 1].popFront();

           } else {
               /* Baştaki hanelere karşılık gelen karalama aralıklarına
                * bakmaya gerek yok. */
               break;
           }
       }

       /* En baştaki karalama aralığında yeterli yer kalmadığında bütün
        * işimiz bitmiş demektir. */
       if (karalama[0].length < kaçlı) {
           karalama.popFront();
           assert(karalama.empty);

       } else {
           hazırla();
       }
   }
}

/**
* Kombinasyon türüyle ve onun şablon parametreleriyle uğraşmamak için
* kullanılan geleneksel kolaylık işlevi.
*/
auto kombinasyon(A)(A aralık, size_t kaçlı)
{
   return Kombinasyon!A(aralık, kaçlı);
}

/**
* Üretilen aralıkları kullanan işlev.
*/
void işle(int[] aralık)
{
   writeln("baş ", aralık);
   Thread.sleep(dur!("msecs")(10));
   writeln("son ", aralık);
}

/**
* Kombinasyonları oluşturmak için elle yazılabilen işlev. Sakıncaları:
*
* - Hane adedi değiştiğinde kodun değiştirilmesi gerekir
*
* - Değişken isimleri hataya açık
*
* - Yalnızca RandomAccessRange aralıklarıyla kullanılabilir
*
* - Kombinasyonların koşut olarak işletilmeleri kolay değil çünkü en içteki
*   döngü içinde std.parallesims.Task nesnelerinin açıkça oluşturulmaları,
*   başlatılmaları, ve bitmelerinin açıkça beklenmeleri gerekir.
*/
void altıDöngülü(const int[] dilim)
{
   writeln("Sırayla işleterek:");

   foreach (i0; 0 .. dilim.length - 5) {
       foreach (i1; i0 + 1 .. dilim.length) {
           foreach (i2; i1 + 1 .. dilim.length) {
               foreach (i3; i2 + 1 .. dilim.length) {
                   foreach (i4; i3 + 1 .. dilim.length) {
                       foreach (i5; i4 + 1 .. dilim.length) {
                           işle([dilim[i0], dilim[i1], dilim[i2],
                                 dilim[i3], dilim[i4], dilim[i5] ]);
                       }
                   }
               }
           }
       }
   }
}

void aralıklı(A)(A aralık)
{
   writeln("Koşut işleterek:");

   foreach(eleman; parallel(aralık, 1)) {
       işle(eleman);
   }
}

void main()
{
   int[] dilim;
   foreach (i; 0 .. 12) {
       dilim ~= i;
   }

   writeln("Bütün elemanlar: ", dilim);
   writeln("Altılı kombinasyonları:");

   aralıklı(kombinasyon(dilim, 6));

   // altıDöngülü(dilim);
}

Ali

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 13, 2012

Başka bir çözüm daha deniyorum. Önce notlar:

  • Yalnızca int dilimi kullanıyor ama başka RandomAccessRange'lere de genellenebilir

  • Özyinelemeli bir çözüm daha kolay geldiği için InputRange işlevleri değil, opApply()'lar kullandım.

  • Bunun sonucunda bilmediğim bir şey öğrendim: std.parallelism.parallel gibi yararlı araçlar InputRange işlevlerini gerektiyorlar. opApply() sunan türler için açıkça Task nesneleri kullanmak gerekiyor. Neyse ki D'de bunlar son derece kolay! :)

  • Aralık hızlı olsun diye, oluşturulan kombinasyonlar için aynı ara belleği kullandım. Koşut işlemler gibi sorun çıkartan durumlarda programcının .dup'u (veya .idup'u) çağırması gerekir.

Tür şöyle:

import std.exception;
import std.string;

struct DilimKombinasyonu
{
   const int[] dilim;    // asıl dilim
   size_t kaçlı;         // kombinasyonun kaçlı olacağı
   int[] depo;           // sonuçların depolandığı alan

   this(const int[] dilim, size_t kaçlı)
   {
       enforce(dilim.length >= kaçlı,
               format("%s elemanlı dilim ile %s elemanlı kombinasyon"
                      " oluşturulamaz.", dilim.length, kaçlı));

       this.dilim = dilim;
       this.kaçlı = kaçlı;
       this.depo = new int[](kaçlı);
   }

   /*
    * Kombinasyonu özyinelemeli olarak oluşturur
    *
    * Params:
    *     baş = asıl dilimin hangi noktasında bulunduğu
    *     hazır = kombinasyonun kaç elemanının hazır olduğu
    *     işlem = foreach döngüsünün içindeki işlemler, tek temsilci olarak
    *     sayaç = foreach döngüsü sayacı
    *
    * Returns: foreach döngüsü işlemlerinin sonucu (break ile çıkılmışsa 1)
    */
   int loop_R(size_t baş,
              size_t hazır,
              int delegate(size_t, const int[] dilim) işlem,
              ref size_t sayaç)
   {
       if (hazır == kaçlı) {
           return işlem(sayaç++, depo);
       }

       foreach (i; baş .. dilim.length) {
           depo[hazır] = dilim[i];

           immutable yeniBaş = i + 1;
           immutable sonuç = loop_R(yeniBaş, hazır + 1, işlem, sayaç);

           if (sonuç) {
               return sonuç;
           }
       }

       return 0;
   }

   /**
    * Sayaçlı foreach döngüsü
    *
    * Dikkat: Kombinasyonlar aynı ara belleği kullandıkları için gerektiğinde
    *         foreach içinde .dup ile kopyalanmalıdırlar.
    *
    * Examples:
    * ---
    * auto kombinasyonlar = DilimKombinasyonu([ 1, 2, 3, 4, 5 ], 3);
    * foreach (i, kombinasyon; kombinasyonlar) {
    *     if (i == 0) assert (kombinasyon == [ 1, 2, 3 ]);
    *     if (i == 1) assert (kombinasyon == [ 1, 2, 4 ]);
    * }
    * ---
    */
   int opApply (int delegate(size_t, const int[] dilim) işlem) {
       size_t sayaç = 0;
       return loop_R(0, 0, işlem, sayaç);
   }

   /**
    * Sayaçsız foreach döngüsü
    *
    * Dikkat: Kombinasyonlar aynı ara belleği kullandıkları için gerektiğinde
    *         foreach içinde .dup ile kopyalanmalıdırlar.
    *
    * Examples:
    * ---
    * auto kombinasyonlar = DilimKombinasyonu([ 1, 2, 3, 4, 5 ], 3);
    * foreach (kombinasyon; kombinasyonlar) {
    *     // ...
    * }
    * ---
    */
   int opApply (int delegate(const(int)[] dilim) işlem) {
       /* Sayacı yutan bir temsilci oluştur ve öteki opApply()'ı bu temsilciyle
        * çağır. */
       immutable sayaçlıTemsilci =
           (size_t sayaç, const int[] dilim) => işlem(dilim);
       return opApply(sayaçlıTemsilci);
   }
}

Onu deneyen bir program:

import std.stdio;
import std.parallelism;
import core.thread;
import std.range;

// Yavaş bir faktöriyel
ulong faktöriyel(ulong n)
{
   return n == 0 ? 1 : n * faktöriyel(n - 1);
}

void kombinasyonuKullan(size_t i, const int[] kombinasyon)
{
   writefln("baş - %s: %s", i, kombinasyon);

   foreach (x; 0 .. 10_000) {
       faktöriyel(123);
   }

   // Thread.sleep(dur!("msecs")(10));
   writefln("son - %s: %s", i, kombinasyon);
}

void main()
{
   int[] dilim;
   foreach (i; 0 .. 12) {
       dilim ~= i;
   }

   auto kombinasyonlar = DilimKombinasyonu(dilim, 7);

   foreach (i, kombinasyon; kombinasyonlar) {
       kombinasyonuKullan(i, kombinasyon);
   }
}

Yavaş işlesin diye gereksiz işlemler ekledim. :D Benim sistemimdeki çıktısı:

'$ time ./deneme > /dev/null

real 0m4.905s // <--
user 0m4.884s
sys 0m0.012s
'

Koşut olarak işletmek için foreach döngüsünü şöyle değiştirin (std.parallelism modülü gerekiyor ama zaten yukarıda eklenmiş):

   foreach (i, kombinasyon; kombinasyonlar) {
       auto görev = task!kombinasyonuKullan(i, kombinasyon.dup);
       görev.executeInNewThread();
   }

'kombinasyon.dup yapılması gerektiğine dikkat edin!' Şimdiki çıktısı:

'$ time ./deneme > /dev/null

real 0m1.874s // <-- 2.6 kat hızlanmış
user 0m7.396s
sys 0m0.036s
'

Benim sistemimde dört katına kadar hızlanmasını bekleyebilirdim ama sanırım .dup nedeniyle gereken bellek ayırma işlemleri hızı etkiliyor.

Ali

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 14, 2012

@Salih;
Kombinasyon seçme işlemidir, sıralamayı gerektirmez. Yani 4 elemanlı küme içerisinden 4 eleman seçeceksek tek bir kombinasyonumuz vardır: Elemanların hepsini seçmek.
Senin gösterdiğin örnekler permütasyon için geçerli. Permütasyon sıralama işlemidir. Örneğin 4 elemanlı bir kümenin 4 elemanlı permütasyonları, elemanların seçilmesi ve sıralanmasından oluşur. Seçme kısmında tek şansımız vardır: hepsini seçmek. Ama sıralama kısmında 24 farklı dizilim sağlayabiliriz.( 24 = 4!)

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 14, 2012

Hocam öncelikle eline sağlık çünkü ikinci iletideki çift opApply uygulaması ile şahane olmuş. Ancak ne yalan söyleyeyim; bu iletiyi ilk gördüğümde bir şey anlamamıştım. Çünkü ben ne permütasyondan, ne de kombinasyondan anlarım. Yani bir çokları gibi matematiğim zayıftır ama notasyon nedir bilirim, yeter mi ki... :rolleyes:

Belki de bu kadar zayıf matematik ile programcılıkla da uğraşmamak lazım ya. Neyse iyi kafa çalıştırdığı için SuDoKu (Japonlar soo-DOH-koo gibi okuyormuş!) yerine programcılık çok daha güzel...:)

Bu arada kodlarını anlamaya çalışmak için gayret ettim. Yanlış anlaşılmasın, sadece anlamak için soruyorum; şu kodun çıktısı daha fazla olması gerekmez miydi? Çünkü "dilim.length == kaçlı" olduğunda tek olasılık mı var?

   auto kombinasyonlar = DilimKombinasyonu([ 1, 2, 3, 4, ], 4);

   foreach (i, kombinasyon; kombinasyonlar) {
       writefln("%s: %s", i+1, kombinasyon);
   }

Çıktısı:
'1: [1, 2, 3, 4]'

Yani 4 elemanlı bir kümemiz (torbamız) var ve biz bunların içinden 4 seçimli kombinasyon (tombala) yapıyoruz diyelim. Peki oluşabilecek olasılıklardan diğerlerinin bir kısmı şu olmaz mıydı?

'2: [1, 2, 4, 3]
3: [1, 4, 2, 3]
4: [4, 1, 2, 3]
5: [4, 1, 3, 2]
6: [4, 3, 1, 2]
7: [4, 3, 2, 1]
8: [4, 2, 3, 1]
9: [4, 2, 1, 3]
..'

Dip Not: Torba ve tombala benzetmelerimi mazur görünüz. Sadece anlamayı kolaylaştırmak için örnekledim.

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 14, 2012

Evet öyleymiş, ben de iletini okumadan evvel neymiş şu Permütasyon (http://tr.wikipedia.org/wiki/Perm%C3%BCtasyon) ve Kombinasyon (http://tr.wikipedia.org/wiki/Kombinasyon) diye bakayım dedim. Meğer her ikisini karıştırıyormuşum. Uzun süredir görmediğim ve çoktan unuttuğum bir dersi de hatırlamış oldum. Ama bir itirazım var...:)

Bizim Türkçe'de bir şeyin kombinasyonu dendiğinde farklı olasılıkların tümü olarak algılanıyor ya da ben öyle biliyorum. Belki de dilimize/dilime böyle yerleşmesi matematik eğitiminin bir eksiliğinin göstergesi (işaretçi mi deseydik...:)) olsa gerek!

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 14, 2012

Bu yapıyı, parallelism olanaklarını keşfetmek için basit bir çift çekirdekli işlemcide ve şu işlevler ile denedim:

void kombinasyonuKullan(size_t i, const int[] kombinasyon)
{
   writefln("%s; %s", i+1, kombinasyon);
}
void main()
{
   int[] dilim;
   foreach (i; 0 .. 14) {
       dilim ~= i;
   }

   auto kombinasyonlar = DilimKombinasyonu(dilim, dilim.length/2);
   foreach (i, kombinasyon; kombinasyonlar) {
       //kombinasyonuKullan(i, kombinasyon.dup); /*
       auto görev = task!kombinasyonuKullan(i, kombinasyon.dup);
       görev.executeInNewThread(); //*/
   }
} /*
C:\DMD\windows\bin>dmd /Users/Test/Desktop/kombinasyon.d -release -wi
C:\DMD\windows\bin>kombinasyon > liste.csv
C:\DMD\windows\bin>start liste.csv | dir *.csv
*/

Sistem hafızası 2 GB. olduğundan mı bilemiyorum; yaklaşık 105 KB. dosya oluşması lazımda ama çoğunlukla ortalarda bir yerde aşağıdaki hatayı veriyor ve dosya boyutu da olması gerekenden oldukça küçüktü!
'(Not: Bir iki kere bu hatayı vermeden geçtiğini gördüm...)'

Alıntı:

>

2855 [2, 3, 6, 7, 10, 12, 13]
core.thread.ThreadException@src\core\thread.d(818): Error creating thread

41AC94
41AB0B
40AB24
402303
4020EE
...

2856 [2, 3, 6, 7, 11, 12, 13]

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 14, 2012

Sorunu aşağıdaki satırı döngü içine koyarak çözdüğümü zannediyorum:

if(i%10 == 0) görev.yieldForce();

Buradaki amaç, her 10 görevden birini beklemesini sağlayarak işlemciye nefes alacak vakit tanımak...:)

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 15, 2012

Ali hocam, ben şu ana kadar (en azından son örnekte) std.parallelism'in faydasını görebilmiş değilim. Yanlış anlaşılmasın; eleştirmekten çok bunu görmeyi çok arzuluyorum.

Şimd 4 çekirdekli 8 GB'.lık bir makinede, 22C11'i önce normal denedim 3-4 sn. sürdü:
'real 0m4.559s
user 0m3.218s
sys 0m1.257s'

Alıntı:

>
>     auto kombinasyonlar = DilimKombinasyonu(dilim, dilim.length/2);
>     foreach (i, kombinasyon; kombinasyonlar) {
>         //kombinasyonuKullan(i, kombinasyon.dup); /*
>         auto görev = task!kombinasyonuKullan(i, kombinasyon.dup);
>         görev.executeInNewThread();
>         if(i%1000 == 0) görev.yieldForce();//*/
> ```

>

Sonra yukarıdaki kodun ilgili satırını gizleyip derlediğimde neredeyse 10 kat daha uzun sürdü:
'real	0m23.878s
user	0m13.037s
sys	 0m33.849s'

Dikkatlerinizi bir noktaya çekmek isterim: Her 10 thread (iş parçasın) da bir bekletmek yerine 1000 değerini verdim. Bu da topu topu 705 defa bekleme yaptığı anlamına geliyor. O satırı kaldırdığımda ise parçalama hatası veriyor. Sanırım hızlı makinede ya da 64 bit Linux olması sebebiyle böyle bir farklılık oldu.

Sanırım iş parçalarını işletmek için daha kontrollü farklı bir yöntem gerekiyor. Yoksa yavaşlık ile çökme arasında debeleniyoruz gibime geliyor. Ayrıca .dup olması da gerekiyormuş. Yoksa her iş parçacığı, ne hikmetse benzer (yakınlarındaki) sonucu üretiyor.

-- 
[ Bu gönderi, <http://ddili.org/forum>'dan dönüştürülmüştür. ]
July 14, 2012

Evet, galiba deli gibi iş parçacığı oluşturduğum için olmuştur. :) std.parallelism bu işi kontrollü yapıyor. Hiçbir anda belirli sayıdan fazla iş parçacığı işletmiyor.

Ali

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

July 15, 2012

Denedim ama öncelikle belirtmeliyim; bu çözümler, işletim sisteminin türüne ve donanım konfigurasyonlarına göre farklılık gösteriyor. Örneğin şimdi başka sistemdeyim ama yine Linux ve bu sefer 32 bit. Dizideki eleman sayısını 14'de bile alsam, program, kısa sürede çöküyor. Ancak son önerinin bir yerde faydasını gördüm...

Biliyorsunuz; eleman sayısının yarısı (dilim.length/2) en fazla kombinasyonu veriyormuş. Eğer 10'a düşürürsem bu 10C5 demek ve 251 (farklı) + 1 kombinasyon demekmiş, yani 252 adetlik gibi küçük bir döngüden bahsediyoruz.

Şu an bir netbook'dayım ve sistem kaynaklar yerlerde sürünüyor. Bu denemelerde de büyük hassasiyet anlamına geliyor. Öyle ki 11C5'de bile ilk denediğim çözüm (görev.yieldForce) 4xx'nci adımlarda çöküyor. Oysa 462'de bitecekti ama işlemci mi dayanamıyor artık, program bir yerde patlıyor...:)

Ali hocamın son çözümünü denediğimde bu soruna, bu sistemde ve 11C5'de ilaç oluyor. Ama 12C6'da yine patlıyor. İşin ilginci, ilk denemelerimi yine aynı makinada ama Windows platformunda yapmıştım ve bu kadar küçük döngü adetlerinde sorun yapmamıştı. Acaba diğer arkadaşlar da kendi sistemlerinde deneyebilir mi? Belki de benim sistemlerde bir kısıtlama veya sorun vardır!

Dip Not: Ayrıca her 10 iş parçacığında bir kez '.workForce()' ve '.spinForce()''u da denedim. Çok faydasını göremedim en azından bu sistemde. Ancak spin denen şey, bir denemede program çökmeden birden fazla yerde "Error creating thread" hatası veriyor. Özetle, biz 1 byte'ın sayısal değerinden fazla adette iş parçacığı (thread) oluştururken dikkatli olmamız gerekiyor. Çünkü program bir sistemde çalışırken başkasında çökebiliyor.

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]

« First   ‹ Prev
1 2 3